Skip to content

Commit 99b65cb

Browse files
authored
Merge pull request #46 from ParkTrack-Project/AnalyticsPanel
feat: Improve analytics charts readability and zone/source UX polish
2 parents 27da70d + a2dabdd commit 99b65cb

2 files changed

Lines changed: 120 additions & 2 deletions

File tree

src/pages/AnalyticsPage.tsx

Lines changed: 95 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -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>

src/styles.css

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,31 @@ body { background: var(--bg); color: var(--text); font-family: Inter, system-ui,
172172
font-weight: 700;
173173
}
174174

175+
.chart-point-marker {
176+
transition: r 0.12s ease;
177+
}
178+
179+
.chart-point-hit {
180+
cursor: pointer;
181+
outline: none;
182+
}
183+
184+
.chart-tooltip-guide {
185+
stroke: rgba(18, 138, 69, 0.28);
186+
stroke-width: 1;
187+
stroke-dasharray: 4 4;
188+
}
189+
190+
.chart-tooltip rect {
191+
fill: #111827;
192+
filter: drop-shadow(0 8px 14px rgba(15, 23, 42, 0.25));
193+
}
194+
195+
.chart-tooltip text {
196+
fill: #fff;
197+
font-size: 11px;
198+
}
199+
175200
.chart-meta {
176201
margin-top: 8px;
177202
color: var(--muted);

0 commit comments

Comments
 (0)