From a2dabdd8cfdfbcad90347c4728878c322908297b Mon Sep 17 00:00:00 2001 From: 666mxvbee Date: Mon, 1 Jun 2026 19:29:20 +0300 Subject: [PATCH] feat: add interactive analytics chart tooltips --- src/pages/AnalyticsPage.tsx | 97 ++++++++++++++++++++++++++++++++++++- src/styles.css | 25 ++++++++++ 2 files changed, 120 insertions(+), 2 deletions(-) diff --git a/src/pages/AnalyticsPage.tsx b/src/pages/AnalyticsPage.tsx index f324d6b..9799763 100644 --- a/src/pages/AnalyticsPage.tsx +++ b/src/pages/AnalyticsPage.tsx @@ -1069,6 +1069,18 @@ function LineChart({ yLabel?: string; }) { const [hidden, setHidden] = useState>(() => new Set()); + const [tooltip, setTooltip] = useState<{ + key: string; + x: number; + y: number; + boxX: number; + boxY: number; + width: number; + height: number; + lines: string[]; + color: string; + pinned: boolean; + } | undefined>(); const visibleSeries = series .filter(item => !hidden.has(item.key)) .map(item => ({ ...item, points: compactLinePoints(item.points, MAX_CHART_POINTS, granularity) })); @@ -1088,6 +1100,10 @@ function LineChart({ const totalPointCount = visibleSeries.reduce((sum, item) => sum + item.points.length, 0); const showMarkers = totalPointCount <= MAX_MARKER_POINTS; + useEffect(() => { + setTooltip(undefined); + }, [series, granularity, hidden]); + if (!series.length || !series.some(item => item.points.length)) { return
{emptyMessage}
; } @@ -1121,10 +1137,44 @@ function LineChart({ label: points[index]?.x ? formatAxisDateTime(points[index].x) : '' }; }).filter(tick => tick.label); + const tooltipValue = (value: number) => { + const digits = unit === '%' ? 1 : Math.abs(value) >= 10 ? 0 : 1; + return `${value.toFixed(digits)}${unit ?? ''}`; + }; + const pointTooltip = (item: ChartSeries, point: ChartPoint, index: number, pinned: boolean) => { + if (point.y === null) return undefined; + const pointX = toX(point, index, item.points.length); + const pointY = toY(point.y); + const lines = [item.label, formatAxisDateTime(point.x), tooltipValue(point.y)]; + const tooltipWidth = Math.min(230, Math.max(128, Math.max(...lines.map(line => line.length)) * 6.4 + 18)); + const tooltipHeight = 58; + let boxX = pointX + 12; + let boxY = pointY - tooltipHeight - 12; + if (boxX + tooltipWidth > width - padding.right) boxX = pointX - tooltipWidth - 12; + if (boxX < padding.left) boxX = padding.left; + if (boxY < padding.top) boxY = pointY + 12; + if (boxY + tooltipHeight > height - padding.bottom) boxY = height - padding.bottom - tooltipHeight; + return { + key: `${item.key}-${index}`, + x: pointX, + y: pointY, + boxX, + boxY, + width: tooltipWidth, + height: tooltipHeight, + lines, + color: item.color, + pinned + }; + }; + const showPointTooltip = (item: ChartSeries, point: ChartPoint, index: number, pinned = false) => { + const next = pointTooltip(item, point, index, pinned); + if (next) setTooltip(next); + }; return (
- + setTooltip(undefined)}> {yTicks.map((tick, index) => { const y = toY(tick); return ( @@ -1159,11 +1209,38 @@ function LineChart({ {showMarkers && visibleSeries.map(item => item.points.map((point, index) => { if (point.y === null) return null; return ( - + {`${item.label}\n${formatDateTime(point.x)}\n${point.y.toFixed(1)}${unit ?? ''}`} ); }))} + {visibleSeries.map(item => item.points.map((point, index) => { + if (point.y === null) return null; + const pointX = toX(point, index, item.points.length); + const pointY = toY(point.y); + return ( + showPointTooltip(item, point, index)} + onFocus={() => showPointTooltip(item, point, index)} + onClick={(event) => { + event.stopPropagation(); + showPointTooltip(item, point, index, true); + }} + onMouseLeave={() => setTooltip(current => current?.pinned ? current : undefined)} + onBlur={() => setTooltip(current => current?.pinned ? current : undefined)} + /> + ); + }))} {xTicks.map((tick, index) => ( {tick.label} @@ -1181,6 +1258,22 @@ function LineChart({ > {yLabel} + {tooltip && ( + + + + + + + {tooltip.lines.map((line, index) => ( + + {line} + + ))} + + + + )} {granularity && (
Детализация: {GRANULARITY_LABELS[granularity]}
diff --git a/src/styles.css b/src/styles.css index de700cf..fe328c1 100644 --- a/src/styles.css +++ b/src/styles.css @@ -172,6 +172,31 @@ body { background: var(--bg); color: var(--text); font-family: Inter, system-ui, font-weight: 700; } +.chart-point-marker { + transition: r 0.12s ease; +} + +.chart-point-hit { + cursor: pointer; + outline: none; +} + +.chart-tooltip-guide { + stroke: rgba(18, 138, 69, 0.28); + stroke-width: 1; + stroke-dasharray: 4 4; +} + +.chart-tooltip rect { + fill: #111827; + filter: drop-shadow(0 8px 14px rgba(15, 23, 42, 0.25)); +} + +.chart-tooltip text { + fill: #fff; + font-size: 11px; +} + .chart-meta { margin-top: 8px; color: var(--muted);