diff --git a/src/components/Comparison.tsx b/src/components/Comparison.tsx index 2020b67..4bcc487 100644 --- a/src/components/Comparison.tsx +++ b/src/components/Comparison.tsx @@ -161,7 +161,8 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM const ds = new Date(cs0.getTime() + i * 86400000); return ds.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }); }); - const museumList = selMuseums.length > 0 ? selMuseums : museums; + const museumList = (selMuseums.length > 0 ? selMuseums : museums) + .filter(museum => currData.some(r => r.museum_name === museum)); const multiMuseum = museumList.length >= 2; const museumDatasets = museumList.map((museum, idx) => { const mg = group(currData.filter(r => r.museum_name === museum), currStart); @@ -215,12 +216,34 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM const trendOpts: any = useMemo(() => ({ ...chartOpts, interaction: { mode: 'nearest', intersect: false }, - layout: { - ...chartOpts.layout, - padding: { ...(chartOpts.layout?.padding ?? {}), right: trendResult.multiMuseum ? 110 : 5 }, - }, plugins: { ...chartOpts.plugins, + legend: { + display: true, + position: 'right' as const, + labels: { + padding: 14, + font: { size: 11, weight: 'bold' as const }, + usePointStyle: true, + generateLabels: (chart: any) => + chart.data.datasets.map((ds: any, i: number) => { + const color: string = ds.borderColor || '#64748b'; + const pill = document.createElement('canvas'); + pill.width = 10; pill.height = 10; + const pCtx = pill.getContext('2d'); + if (pCtx) { + pCtx.strokeStyle = color; + pCtx.lineWidth = 1; + pCtx.beginPath(); + pCtx.arc(5, 5, 4, 0, Math.PI * 2); + pCtx.stroke(); + } + return { text: ds.label, fillStyle: color, strokeStyle: color, + fontColor: color, lineWidth: 0, pointStyle: pill, + hidden: !chart.isDatasetVisible(i), datasetIndex: i }; + }), + }, + }, tooltip: { ...chartOpts.plugins.tooltip, callbacks: { @@ -228,7 +251,7 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM } } } - }), [chartOpts, trendResult.tooltipLabels, trendResult.multiMuseum]); + }), [chartOpts, trendResult.tooltipLabels]); const metricOpts = [ { value:'revenue', label:L.revenue }, { value:'visitors', label:L.visitors }, diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index f0b3165..d18a48f 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -122,7 +122,8 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set return ds.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }); }); const prevYear = parseInt(start.slice(0,4))-1; - const museumList = selMuseums.length > 0 ? selMuseums : allMuseums; + const museumList = (selMuseums.length > 0 ? selMuseums : allMuseums) + .filter(museum => filteredData.some(r => r.museum_name === museum)); const multiMuseum = museumList.length >= 2; const museumDatasets = museumList.map((museum, idx) => { const mg = group(filteredData.filter(r => r.museum_name === museum), start); @@ -214,12 +215,34 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set const trendOpts: any = useMemo(() => ({ ...chartOpts, interaction: { mode: 'nearest', intersect: false }, - layout: { - ...chartOpts.layout, - padding: { ...(chartOpts.layout?.padding ?? {}), right: trendResult.multiMuseum ? 110 : 5 }, - }, plugins: { ...chartOpts.plugins, + legend: { + display: true, + position: 'right' as const, + labels: { + padding: 14, + font: { size: 11, weight: 'bold' as const }, + usePointStyle: true, + generateLabels: (chart: any) => + chart.data.datasets.map((ds: any, i: number) => { + const color: string = ds.borderColor || '#64748b'; + const pill = document.createElement('canvas'); + pill.width = 10; pill.height = 10; + const pCtx = pill.getContext('2d'); + if (pCtx) { + pCtx.strokeStyle = color; + pCtx.lineWidth = 1; + pCtx.beginPath(); + pCtx.arc(5, 5, 4, 0, Math.PI * 2); + pCtx.stroke(); + } + return { text: ds.label, fillStyle: color, strokeStyle: color, + fontColor: color, lineWidth: 0, pointStyle: pill, + hidden: !chart.isDatasetVisible(i), datasetIndex: i }; + }), + }, + }, tooltip: { ...chartOpts.plugins.tooltip, callbacks: { @@ -227,7 +250,7 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set } } } - }), [chartOpts, trendResult.tooltipLabels, trendResult.multiMuseum]); + }), [chartOpts, trendResult.tooltipLabels]); const pieOptions: any = useMemo(() => ({ responsive: true, maintainAspectRatio: false, diff --git a/src/config/chartConfig.ts b/src/config/chartConfig.ts index 6dda432..f3828b3 100644 --- a/src/config/chartConfig.ts +++ b/src/config/chartConfig.ts @@ -113,7 +113,9 @@ export const createBaseOptions = (showDataLabels: boolean): any => { titleFont: { size: 12 }, bodyFont: { size: 11 }, rtl: false, - textDirection: 'ltr' + textDirection: 'ltr', + usePointStyle: true, + boxPadding: 6, }, datalabels: createDataLabelConfig(showDataLabels, { color: theme.textPrimary, @@ -157,71 +159,6 @@ const trendLinePlugin = { } }, - // ── 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; - 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);