@@ -1069,6 +1069,18 @@ function LineChart({
10691069 yLabel ?: string ;
10701070} ) {
10711071 const [ hidden , setHidden ] = useState < Set < string > > ( ( ) => new Set ( ) ) ;
1072+ const [ tooltip , setTooltip ] = useState < {
1073+ key : string ;
1074+ x : number ;
1075+ y : number ;
1076+ boxX : number ;
1077+ boxY : number ;
1078+ width : number ;
1079+ height : number ;
1080+ lines : string [ ] ;
1081+ color : string ;
1082+ pinned : boolean ;
1083+ } | undefined > ( ) ;
10721084 const visibleSeries = series
10731085 . filter ( item => ! hidden . has ( item . key ) )
10741086 . map ( item => ( { ...item , points : compactLinePoints ( item . points , MAX_CHART_POINTS , granularity ) } ) ) ;
@@ -1088,6 +1100,10 @@ function LineChart({
10881100 const totalPointCount = visibleSeries . reduce ( ( sum , item ) => sum + item . points . length , 0 ) ;
10891101 const showMarkers = totalPointCount <= MAX_MARKER_POINTS ;
10901102
1103+ useEffect ( ( ) => {
1104+ setTooltip ( undefined ) ;
1105+ } , [ series , granularity , hidden ] ) ;
1106+
10911107 if ( ! series . length || ! series . some ( item => item . points . length ) ) {
10921108 return < div className = "empty-state" > { emptyMessage } </ div > ;
10931109 }
@@ -1121,10 +1137,44 @@ function LineChart({
11211137 label : points [ index ] ?. x ? formatAxisDateTime ( points [ index ] . x ) : ''
11221138 } ;
11231139 } ) . filter ( tick => tick . label ) ;
1140+ const tooltipValue = ( value : number ) => {
1141+ const digits = unit === '%' ? 1 : Math . abs ( value ) >= 10 ? 0 : 1 ;
1142+ return `${ value . toFixed ( digits ) } ${ unit ?? '' } ` ;
1143+ } ;
1144+ const pointTooltip = ( item : ChartSeries , point : ChartPoint , index : number , pinned : boolean ) => {
1145+ if ( point . y === null ) return undefined ;
1146+ const pointX = toX ( point , index , item . points . length ) ;
1147+ const pointY = toY ( point . y ) ;
1148+ const lines = [ item . label , formatAxisDateTime ( point . x ) , tooltipValue ( point . y ) ] ;
1149+ const tooltipWidth = Math . min ( 230 , Math . max ( 128 , Math . max ( ...lines . map ( line => line . length ) ) * 6.4 + 18 ) ) ;
1150+ const tooltipHeight = 58 ;
1151+ let boxX = pointX + 12 ;
1152+ let boxY = pointY - tooltipHeight - 12 ;
1153+ if ( boxX + tooltipWidth > width - padding . right ) boxX = pointX - tooltipWidth - 12 ;
1154+ if ( boxX < padding . left ) boxX = padding . left ;
1155+ if ( boxY < padding . top ) boxY = pointY + 12 ;
1156+ if ( boxY + tooltipHeight > height - padding . bottom ) boxY = height - padding . bottom - tooltipHeight ;
1157+ return {
1158+ key : `${ item . key } -${ index } ` ,
1159+ x : pointX ,
1160+ y : pointY ,
1161+ boxX,
1162+ boxY,
1163+ width : tooltipWidth ,
1164+ height : tooltipHeight ,
1165+ lines,
1166+ color : item . color ,
1167+ pinned
1168+ } ;
1169+ } ;
1170+ const showPointTooltip = ( item : ChartSeries , point : ChartPoint , index : number , pinned = false ) => {
1171+ const next = pointTooltip ( item , point , index , pinned ) ;
1172+ if ( next ) setTooltip ( next ) ;
1173+ } ;
11241174
11251175 return (
11261176 < div className = "analytics-chart" >
1127- < svg viewBox = { `0 0 ${ width } ${ height } ` } role = "img" aria-label = { `${ yLabel } по оси ${ xLabel } ` } >
1177+ < svg viewBox = { `0 0 ${ width } ${ height } ` } role = "img" aria-label = { `${ yLabel } по оси ${ xLabel } ` } onClick = { ( ) => setTooltip ( undefined ) } >
11281178 { yTicks . map ( ( tick , index ) => {
11291179 const y = toY ( tick ) ;
11301180 return (
@@ -1159,11 +1209,38 @@ function LineChart({
11591209 { showMarkers && visibleSeries . map ( item => item . points . map ( ( point , index ) => {
11601210 if ( point . y === null ) return null ;
11611211 return (
1162- < circle key = { `${ item . key } -${ index } ` } cx = { toX ( point , index , item . points . length ) } cy = { toY ( point . y ) } r = "3" fill = { item . color } >
1212+ < circle key = { `${ item . key } -${ index } ` } cx = { toX ( point , index , item . points . length ) } cy = { toY ( point . y ) } r = "3" fill = { item . color } className = "chart-point-marker" >
11631213 < title > { `${ item . label } \n${ formatDateTime ( point . x ) } \n${ point . y . toFixed ( 1 ) } ${ unit ?? '' } ` } </ title >
11641214 </ circle >
11651215 ) ;
11661216 } ) ) }
1217+ { visibleSeries . map ( item => item . points . map ( ( point , index ) => {
1218+ if ( point . y === null ) return null ;
1219+ const pointX = toX ( point , index , item . points . length ) ;
1220+ const pointY = toY ( point . y ) ;
1221+ return (
1222+ < circle
1223+ key = { `hit-${ item . key } -${ index } ` }
1224+ cx = { pointX }
1225+ cy = { pointY }
1226+ r = { showMarkers ? 9 : 7 }
1227+ fill = "transparent"
1228+ pointerEvents = "all"
1229+ tabIndex = { 0 }
1230+ role = "button"
1231+ aria-label = { `${ item . label } : ${ formatAxisDateTime ( point . x ) } , ${ tooltipValue ( point . y ) } ` }
1232+ className = "chart-point-hit"
1233+ onMouseEnter = { ( ) => showPointTooltip ( item , point , index ) }
1234+ onFocus = { ( ) => showPointTooltip ( item , point , index ) }
1235+ onClick = { ( event ) => {
1236+ event . stopPropagation ( ) ;
1237+ showPointTooltip ( item , point , index , true ) ;
1238+ } }
1239+ onMouseLeave = { ( ) => setTooltip ( current => current ?. pinned ? current : undefined ) }
1240+ onBlur = { ( ) => setTooltip ( current => current ?. pinned ? current : undefined ) }
1241+ />
1242+ ) ;
1243+ } ) ) }
11671244 { xTicks . map ( ( tick , index ) => (
11681245 < text key = { `x-${ index } ` } x = { tick . x } y = { height - padding . bottom + 22 } textAnchor = "middle" className = "chart-axis-tick" >
11691246 { tick . label }
@@ -1181,6 +1258,22 @@ function LineChart({
11811258 >
11821259 { yLabel }
11831260 </ text >
1261+ { tooltip && (
1262+ < g className = "chart-tooltip" pointerEvents = "none" >
1263+ < line x1 = { tooltip . x } y1 = { padding . top } x2 = { tooltip . x } y2 = { height - padding . bottom } className = "chart-tooltip-guide" />
1264+ < circle cx = { tooltip . x } cy = { tooltip . y } r = "5" fill = "#fff" stroke = { tooltip . color } strokeWidth = "3" />
1265+ < g transform = { `translate(${ tooltip . boxX } ${ tooltip . boxY } )` } >
1266+ < rect width = { tooltip . width } height = { tooltip . height } rx = "4" />
1267+ < text x = "10" y = "18" >
1268+ { tooltip . lines . map ( ( line , index ) => (
1269+ < tspan key = { `${ tooltip . key } -${ index } ` } x = "10" dy = { index === 0 ? 0 : 16 } fontWeight = { index === 0 ? 700 : 500 } >
1270+ { line }
1271+ </ tspan >
1272+ ) ) }
1273+ </ text >
1274+ </ g >
1275+ </ g >
1276+ ) }
11841277 </ svg >
11851278 { granularity && (
11861279 < div className = "chart-meta" > Детализация: { GRANULARITY_LABELS [ granularity ] } </ div >
0 commit comments