1+ import React , { FC , useEffect , useRef } from "react" ;
2+ import * as d3 from "d3" ;
3+
4+ interface HistogramBarsProps {
5+ data : Array < { name : string ; count : number } > ;
6+ range : { min : number ; max : number } ;
7+ highlightedTopic ?: string ;
8+ }
9+
10+ export const HistogramBars : FC < HistogramBarsProps > = ( { data, range, highlightedTopic } ) => {
11+ const svgRef = useRef < SVGSVGElement > ( null ) ;
12+
13+ useEffect ( ( ) => {
14+ if ( ! svgRef . current || ! data . length ) return ;
15+
16+ // Clear previous content
17+ d3 . select ( svgRef . current ) . selectAll ( "*" ) . remove ( ) ;
18+
19+ // Set dimensions with more bottom margin for labels
20+ const margin = { top : 30 , right : 20 , bottom : 100 , left : 40 } ;
21+ const width = svgRef . current . clientWidth - margin . left - margin . right ;
22+ const height = 300 - margin . top - margin . bottom ;
23+
24+ // Create SVG
25+ const svg = d3 . select ( svgRef . current )
26+ . attr ( "width" , width + margin . left + margin . right )
27+ . attr ( "height" , height + margin . top + margin . bottom )
28+ . append ( "g" )
29+ . attr ( "transform" , `translate(${ margin . left } ,${ margin . top } )` ) ;
30+
31+ // Create scales
32+ const x = d3 . scaleBand ( )
33+ . range ( [ 0 , width ] )
34+ . padding ( 0.1 )
35+ . domain ( data . map ( d => d . name ) ) ;
36+
37+ const y = d3 . scaleLinear ( )
38+ . range ( [ height , 0 ] )
39+ . domain ( [ 0 , d3 . max ( data , d => d . count ) || 0 ] ) ;
40+
41+ // Create and add bars
42+ svg . selectAll ( ".bar" )
43+ . data ( data )
44+ . enter ( )
45+ . append ( "rect" )
46+ . attr ( "class" , "bar" )
47+ . attr ( "x" , d => x ( d . name ) || 0 )
48+ . attr ( "width" , x . bandwidth ( ) )
49+ . attr ( "y" , d => y ( d . count ) )
50+ . attr ( "height" , d => height - y ( d . count ) )
51+ . attr ( "fill" , d => {
52+ if ( highlightedTopic === d . name ) return '#ffc107' ; // Highlight color
53+ return ( d . count >= range . min && d . count <= range . max ) ? '#0d6efd' : '#e9ecef' ;
54+ } )
55+ . attr ( "opacity" , d => highlightedTopic && highlightedTopic !== d . name ? 0.5 : 1 )
56+ . on ( "mouseover" , ( event , d ) => {
57+ // Show tooltip
58+ const tooltip = svg . append ( "g" )
59+ . attr ( "class" , "tooltip" )
60+ . attr ( "transform" , `translate(${ x ( d . name ) || 0 } ,${ y ( d . count ) - 20 } )` ) ;
61+
62+ tooltip . append ( "text" )
63+ . attr ( "x" , x . bandwidth ( ) / 2 )
64+ . attr ( "y" , 0 )
65+ . attr ( "text-anchor" , "middle" )
66+ . style ( "font-size" , "12px" )
67+ . text ( `${ d . name } : ${ d . count } ` ) ;
68+ } )
69+ . on ( "mouseout" , ( ) => {
70+ // Remove tooltip
71+ svg . selectAll ( ".tooltip" ) . remove ( ) ;
72+ } ) ;
73+
74+ // Add count labels on top of bars
75+ svg . selectAll ( ".count-label" )
76+ . data ( data )
77+ . enter ( )
78+ . append ( "text" )
79+ . attr ( "class" , "count-label" )
80+ . attr ( "x" , d => ( x ( d . name ) || 0 ) + x . bandwidth ( ) / 2 )
81+ . attr ( "y" , d => y ( d . count ) - 5 )
82+ . attr ( "text-anchor" , "middle" )
83+ . style ( "font-size" , "12px" )
84+ . style ( "fill" , "#6c757d" )
85+ . text ( d => d . count ) ;
86+
87+ // Add y-axis
88+ svg . append ( "g" )
89+ . call ( d3 . axisLeft ( y ) . ticks ( 5 ) ) ;
90+
91+ // Add x-axis with rotated labels
92+ svg . append ( "g" )
93+ . attr ( "transform" , `translate(0,${ height } )` )
94+ . call ( d3 . axisBottom ( x ) )
95+ . selectAll ( "text" )
96+ . style ( "text-anchor" , "end" )
97+ . style ( "font-size" , "14px" )
98+ . style ( "font-weight" , "500" )
99+ . attr ( "dx" , "-1em" )
100+ . attr ( "dy" , ".15em" )
101+ . attr ( "transform" , "rotate(-45)" ) ;
102+
103+ } , [ data , range , highlightedTopic ] ) ;
104+
105+ return (
106+ < div style = { { width : '100%' , height : '400px' , padding : '10px' } } >
107+ < svg ref = { svgRef } style = { { width : '100%' , height : '100%' } } > </ svg >
108+ </ div >
109+ ) ;
110+ } ;
0 commit comments