Skip to content

Commit d55b911

Browse files
Merge branch 'feature/ev-price-tracker'
2 parents 586590d + d3770e5 commit d55b911

File tree

2 files changed

+62
-75
lines changed

2 files changed

+62
-75
lines changed

src/components/OverviewChart.css

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,22 @@
4040
position: relative;
4141
height: 400px;
4242
}
43+
44+
.chart-labels {
45+
position: absolute;
46+
top: 0;
47+
left: 0;
48+
width: 100%;
49+
height: 100%;
50+
pointer-events: none;
51+
}
52+
53+
.chart-label {
54+
pointer-events: auto;
55+
cursor: pointer;
56+
transition: all 0.2s ease;
57+
}
58+
59+
.chart-label:hover {
60+
text-decoration: underline !important;
61+
}

src/components/OverviewChart.jsx

Lines changed: 43 additions & 75 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import './OverviewChart.css';
66
export default function OverviewChart({ data, onModelSelect }) {
77
const chartRef = useRef(null);
88
const chartInstance = useRef(null);
9+
const labelsContainerRef = useRef(null);
910

1011
const modelColors = {
1112
'Hyundai Ioniq 5': '#667eea',
@@ -24,9 +25,6 @@ export default function OverviewChart({ data, onModelSelect }) {
2425
useEffect(() => {
2526
if (!data || data.length === 0) return;
2627

27-
// Store label positions for click detection
28-
const labelBounds = [];
29-
3028
// Group data by model and date
3129
const models = [...new Set(data.flatMap(d => d.listings.map(getModelKey)))];
3230
const dates = [...new Set(data.map(d => d.scraped_at.split('T')[0]))].sort();
@@ -118,8 +116,6 @@ export default function OverviewChart({ data, onModelSelect }) {
118116
const meta = chart.getDatasetMeta(i);
119117
if (!meta.hidden && meta.data.length > 0) {
120118
const lastPoint = meta.data[meta.data.length - 1];
121-
ctx.font = 'bold 12px sans-serif';
122-
const textWidth = ctx.measureText(dataset.label).width;
123119

124120
labels.push({
125121
text: dataset.label,
@@ -129,7 +125,6 @@ export default function OverviewChart({ data, onModelSelect }) {
129125
dataPointY: lastPoint.y,
130126
x: lastPoint.x + 20,
131127
y: lastPoint.y,
132-
width: textWidth,
133128
height: 16
134129
});
135130
}
@@ -154,10 +149,7 @@ export default function OverviewChart({ data, onModelSelect }) {
154149
}
155150
}
156151

157-
// Clear previous label bounds
158-
labelBounds.length = 0;
159-
160-
// Draw callout lines and labels
152+
// Draw callout lines only
161153
labels.forEach(label => {
162154
// Draw callout line if label was moved
163155
if (Math.abs(label.y - label.originalY) > 2) {
@@ -171,82 +163,57 @@ export default function OverviewChart({ data, onModelSelect }) {
171163
ctx.stroke();
172164
ctx.globalAlpha = 1.0;
173165
}
166+
});
174167

175-
// Draw label
176-
ctx.fillStyle = label.color;
177-
ctx.font = 'bold 12px sans-serif';
178-
ctx.textAlign = 'left';
179-
ctx.textBaseline = 'middle';
180-
ctx.fillText(label.text, label.x, label.y);
168+
// Create DOM labels
169+
if (labelsContainerRef.current) {
170+
labelsContainerRef.current.innerHTML = '';
171+
172+
labels.forEach(label => {
173+
const linkEl = document.createElement('a');
174+
linkEl.href = `?model=${encodeURIComponent(label.model)}`;
175+
linkEl.className = 'chart-label';
176+
linkEl.style.position = 'absolute';
177+
linkEl.style.left = label.x + 'px';
178+
linkEl.style.top = (label.y - label.height / 2) + 'px';
179+
linkEl.style.color = label.color;
180+
linkEl.style.fontWeight = 'bold';
181+
linkEl.style.fontSize = '12px';
182+
linkEl.style.textDecoration = 'none';
183+
linkEl.style.whiteSpace = 'nowrap';
184+
linkEl.style.display = 'flex';
185+
linkEl.style.alignItems = 'center';
186+
linkEl.style.gap = '4px';
187+
188+
const textSpan = document.createElement('span');
189+
textSpan.textContent = label.text;
190+
191+
const chevron = document.createElement('span');
192+
chevron.textContent = '›';
193+
chevron.style.fontSize = '14px';
194+
chevron.style.opacity = '0.7';
195+
196+
linkEl.appendChild(textSpan);
197+
linkEl.appendChild(chevron);
198+
199+
linkEl.addEventListener('click', (e) => {
200+
e.preventDefault();
201+
if (onModelSelect) {
202+
onModelSelect(label.model);
203+
}
204+
});
181205

182-
// Store bounds for click detection
183-
labelBounds.push({
184-
x: label.x,
185-
y: label.y - label.height / 2,
186-
width: label.width,
187-
height: label.height,
188-
model: label.model
206+
labelsContainerRef.current.appendChild(linkEl);
189207
});
190-
});
208+
}
191209
}
192210
}]
193211
});
194212

195-
// Add click handler for labels
196-
const handleCanvasClick = (event) => {
197-
const rect = chartRef.current.getBoundingClientRect();
198-
const x = event.clientX - rect.left;
199-
const y = event.clientY - rect.top;
200-
201-
// Check if click is within any label bounds
202-
for (const bound of labelBounds) {
203-
if (
204-
x >= bound.x &&
205-
x <= bound.x + bound.width &&
206-
y >= bound.y &&
207-
y <= bound.y + bound.height
208-
) {
209-
if (onModelSelect) {
210-
onModelSelect(bound.model);
211-
}
212-
break;
213-
}
214-
}
215-
};
216-
217-
// Add mousemove handler to show pointer cursor over labels
218-
const handleCanvasMouseMove = (event) => {
219-
const rect = chartRef.current.getBoundingClientRect();
220-
const x = event.clientX - rect.left;
221-
const y = event.clientY - rect.top;
222-
223-
let overLabel = false;
224-
for (const bound of labelBounds) {
225-
if (
226-
x >= bound.x &&
227-
x <= bound.x + bound.width &&
228-
y >= bound.y &&
229-
y <= bound.y + bound.height
230-
) {
231-
overLabel = true;
232-
break;
233-
}
234-
}
235-
236-
chartRef.current.style.cursor = overLabel ? 'pointer' : 'default';
237-
};
238-
239-
chartRef.current.addEventListener('click', handleCanvasClick);
240-
chartRef.current.addEventListener('mousemove', handleCanvasMouseMove);
241-
242213
return () => {
243214
if (chartInstance.current) {
244215
chartInstance.current.destroy();
245216
}
246-
if (chartRef.current) {
247-
chartRef.current.removeEventListener('click', handleCanvasClick);
248-
chartRef.current.removeEventListener('mousemove', handleCanvasMouseMove);
249-
}
250217
};
251218
}, [data, onModelSelect]);
252219

@@ -258,6 +225,7 @@ export default function OverviewChart({ data, onModelSelect }) {
258225
<div className="overview-chart">
259226
<div className="chart-container">
260227
<canvas ref={chartRef}></canvas>
228+
<div ref={labelsContainerRef} className="chart-labels"></div>
261229
</div>
262230
</div>
263231
);

0 commit comments

Comments
 (0)