Skip to content

Commit fe63a91

Browse files
committed
Add performance guard for hover sync lookup
1 parent 9a3061f commit fe63a91

File tree

2 files changed

+64
-17
lines changed

2 files changed

+64
-17
lines changed

src/components/ChartContainer.jsx

Lines changed: 26 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,31 @@ ChartJS.register(
2828
zoomPlugin
2929
);
3030

31+
export const computeActiveElementsForStep = (chart, step) => {
32+
if (!chart || !chart.data || !chart.data.datasets) return [];
33+
34+
const activeElements = [];
35+
const seen = new Set();
36+
37+
chart.data.datasets.forEach((dataset, datasetIndex) => {
38+
if (!dataset || !dataset.data || !Array.isArray(dataset.data)) return;
39+
const idx = dataset.data.findIndex(p => p && typeof p.x !== 'undefined' && p.x === step);
40+
41+
if (idx !== -1 && dataset.data[idx]) {
42+
const elementKey = `${datasetIndex}-${idx}`;
43+
const indexInRange = idx >= 0 && idx < dataset.data.length;
44+
const datasetInRange = datasetIndex >= 0 && datasetIndex < chart.data.datasets.length;
45+
46+
if (!seen.has(elementKey) && datasetInRange && indexInRange) {
47+
activeElements.push({ datasetIndex, index: idx });
48+
seen.add(elementKey);
49+
}
50+
}
51+
});
52+
53+
return activeElements;
54+
};
55+
3156
const ChartWrapper = ({ data, options, chartId, onRegisterChart, onSyncHover, syncRef }) => {
3257
const chartRef = useRef(null);
3358

@@ -171,23 +196,7 @@ export default function ChartContainer({
171196
chart.tooltip.setActiveElements([], { x: 0, y: 0 });
172197
chart.draw();
173198
} else if (id !== sourceId) {
174-
const activeElements = [];
175-
const seen = new Set(); // avoid adding duplicate points
176-
chart.data.datasets.forEach((dataset, datasetIndex) => {
177-
if (!dataset || !dataset.data || !Array.isArray(dataset.data)) return;
178-
const idx = dataset.data.findIndex(p => p && typeof p.x !== 'undefined' && p.x === step);
179-
if (idx !== -1 && dataset.data[idx]) {
180-
const elementKey = `${datasetIndex}-${idx}`;
181-
if (!seen.has(elementKey)) {
182-
// Validate element
183-
if (datasetIndex >= 0 && datasetIndex < chart.data.datasets.length &&
184-
idx >= 0 && idx < dataset.data.length) {
185-
activeElements.push({ datasetIndex, index: idx });
186-
seen.add(elementKey);
187-
}
188-
}
189-
}
190-
});
199+
const activeElements = computeActiveElementsForStep(chart, step);
191200

192201
// Only set when activeElements are valid
193202
if (activeElements.length > 0) {
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
import { describe, expect, test } from 'vitest';
2+
import { performance } from 'node:perf_hooks';
3+
import { computeActiveElementsForStep } from './ChartContainer.jsx';
4+
5+
const createMockChart = (datasetCount = 50, pointsPerDataset = 2000) => {
6+
const datasets = Array.from({ length: datasetCount }, (_, datasetIndex) => ({
7+
label: `dataset-${datasetIndex}`,
8+
data: Array.from({ length: pointsPerDataset }, (_, pointIndex) => ({
9+
x: pointIndex,
10+
y: datasetIndex + pointIndex
11+
}))
12+
}));
13+
14+
return {
15+
data: { datasets },
16+
tooltip: { setActiveElements: () => {} },
17+
setActiveElements: () => {},
18+
scales: { x: { getPixelForValue: () => 0 } },
19+
draw: () => {}
20+
};
21+
};
22+
23+
describe('computeActiveElementsForStep performance', () => {
24+
test('processes large datasets within an acceptable time budget', () => {
25+
const chart = createMockChart();
26+
const targetStep = 1500; // exists in every dataset
27+
28+
const start = performance.now();
29+
const activeElements = computeActiveElementsForStep(chart, targetStep);
30+
const durationMs = performance.now() - start;
31+
32+
// Each dataset should yield one active element for the target step
33+
expect(activeElements.length).toBe(chart.data.datasets.length);
34+
35+
// Guardrail to catch regressions; high enough to avoid flakiness in CI
36+
expect(durationMs).toBeLessThan(50);
37+
});
38+
});

0 commit comments

Comments
 (0)