diff --git a/src/config/chartConfig.ts b/src/config/chartConfig.ts index 97ee9f9..6dda432 100644 --- a/src/config/chartConfig.ts +++ b/src/config/chartConfig.ts @@ -138,6 +138,8 @@ export const createBaseOptions = (showDataLabels: boolean): any => { // Only activates for charts that have datasets marked with _isMuseumLine. const trendLinePlugin = { id: 'trendLineOverlay', + + // ── hover dim ────────────────────────────────────────────────── beforeDatasetDraw(chart: any, args: any) { if (!chart.data.datasets.some((ds: any) => ds._isMuseumLine)) return; const active = chart.getActiveElements(); @@ -149,30 +151,77 @@ const trendLinePlugin = { }, afterDatasetDraw(chart: any, args: any) { if (!chart.data.datasets.some((ds: any) => ds._isMuseumLine)) return; - const ds = chart.data.datasets[args.index] as any; const active = chart.getActiveElements(); - const isDimmed = active.length > 0 && active[0].datasetIndex !== args.index; - if (isDimmed) chart.ctx.restore(); - if (!ds._isMuseumLine) return; - const meta = chart.getDatasetMeta(args.index); - if (meta.hidden) return; - const vals = ds.data as number[]; - let lastIdx = vals.length - 1; - while (lastIdx > 0 && !vals[lastIdx]) lastIdx--; - if (!vals[lastIdx]) return; - const pt = meta.data[lastIdx] as any; - if (!pt) return; - const name: string = ds.label; - const label = name.length > 22 ? name.slice(0, 20) + '…' : name; + if (active.length > 0 && active[0].datasetIndex !== args.index) { + chart.ctx.restore(); + } + }, + + // ── end-of-line labels with collision resolution ─────────────── + afterDatasetsDraw(chart: any) { + const MIN_GAP = 13; + + // Collect last-point info for every visible museum line + type Entry = { label: string; color: string; x: number; actualY: number; placedY: number }; + const entries: Entry[] = []; + chart.data.datasets.forEach((ds: any, i: number) => { + if (!ds._isMuseumLine) return; + const meta = chart.getDatasetMeta(i); + if (meta.hidden) return; + const vals = ds.data as number[]; + let idx = vals.length - 1; + while (idx > 0 && !vals[idx]) idx--; + if (!vals[idx]) return; + const pt = meta.data[idx] as any; + if (!pt) return; + const name: string = ds.label; + entries.push({ + label: name.length > 22 ? name.slice(0, 20) + '…' : name, + color: ds.borderColor, + x: pt.x, + actualY: pt.y, + placedY: pt.y, + }); + }); + + if (entries.length === 0) return; + + // Sort top → bottom, then push labels apart + entries.sort((a, b) => a.actualY - b.actualY); + for (let i = 1; i < entries.length; i++) { + if (entries[i].placedY - entries[i - 1].placedY < MIN_GAP) { + entries[i].placedY = entries[i - 1].placedY + MIN_GAP; + } + } + + // Draw labels + connectors const ctx = chart.ctx; - ctx.save(); - ctx.font = '600 9px system-ui,-apple-system,sans-serif'; - ctx.fillStyle = ds.borderColor; - ctx.textAlign = 'left'; - ctx.textBaseline = 'middle'; - ctx.fillText(label, pt.x + 6, pt.y); - ctx.restore(); - } + for (const e of entries) { + ctx.save(); + // Small dot at the actual line tip + ctx.beginPath(); + ctx.arc(e.x, e.actualY, 2.5, 0, Math.PI * 2); + ctx.fillStyle = e.color; + ctx.fill(); + // Connector if label was displaced + if (Math.abs(e.placedY - e.actualY) > 3) { + ctx.beginPath(); + ctx.moveTo(e.x + 5, e.actualY); + ctx.lineTo(e.x + 7, e.placedY); + ctx.strokeStyle = e.color; + ctx.globalAlpha = 0.45; + ctx.lineWidth = 0.8; + ctx.stroke(); + } + ctx.globalAlpha = 1; + ctx.font = '600 9px system-ui,-apple-system,sans-serif'; + ctx.fillStyle = e.color; + ctx.textAlign = 'left'; + ctx.textBaseline = 'middle'; + ctx.fillText(e.label, e.x + 9, e.placedY); + ctx.restore(); + } + }, }; ChartJS.register(trendLinePlugin);