From 1070490ad262c0fd67aa608c8d5bd0fd3f5f187d Mon Sep 17 00:00:00 2001 From: fahed Date: Thu, 30 Apr 2026 10:37:05 +0300 Subject: [PATCH] feat(charts): show actual dates in trend chart tooltips MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace opaque W1/D1/month abbreviation tooltip titles with human-readable period labels (e.g. "Week 1 · 1 Apr – 7 Apr", "1 April 2025", "April 2025") in both Dashboard and Comparison trend charts. Co-Authored-By: Claude Sonnet 4.6 --- src/components/Comparison.tsx | 47 ++++++++++++++++++++++++++++------ src/components/Dashboard.tsx | 48 +++++++++++++++++++++++++++++------ 2 files changed, 79 insertions(+), 16 deletions(-) diff --git a/src/components/Comparison.tsx b/src/components/Comparison.tsx index b7dd3f9..278cfec 100644 --- a/src/components/Comparison.tsx +++ b/src/components/Comparison.tsx @@ -128,7 +128,7 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM return sy===ey ? sy : `${L.monthShort[parseInt(s.slice(5,7))-1]} '${sy.slice(-2)}–${L.monthShort[parseInt(e.slice(5,7))-1]} '${ey.slice(-2)}`; }; - const trendData = useMemo(() => { + const trendResult = useMemo(() => { const group = (rows: MuseumRecord[], ps: string) => { const s=new Date(ps); const acc: Record = {}; rows.forEach(r => { @@ -142,17 +142,36 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM }; const pg = group(prevData, prevStart), cg = group(currData, currStart); const maxK = Math.max(...Object.keys(pg).map(Number), ...Object.keys(cg).map(Number), 1); + const cs0 = new Date(currStart); + const fmt = (d: Date) => d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }); const labels = Array.from({length:maxK}, (_,i) => - gran==='week' ? `W${i+1}` : gran==='month' ? L.monthShort[(new Date(currStart).getMonth()+i)%12] : `D${i+1}` + gran==='week' ? `W${i+1}` : gran==='month' ? L.monthShort[(cs0.getMonth()+i)%12] : `D${i+1}` ); + const tooltipLabels = Array.from({length:maxK}, (_,i) => { + if (gran==='week') { + const ws = new Date(cs0.getTime() + i * 7 * 86400000); + const we = new Date(cs0.getTime() + (i+1) * 7 * 86400000 - 86400000); + return `Week ${i+1} · ${fmt(ws)} – ${fmt(we)}`; + } + if (gran==='month') { + const ms = new Date(cs0.getFullYear(), cs0.getMonth() + i, 1); + return ms.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' }); + } + const ds = new Date(cs0.getTime() + i * 86400000); + return ds.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }); + }); return { - labels, - datasets: [ - { label:periodLabel(prevStart,prevEnd), data:labels.map((_,i) => pg[i+1]||0), borderColor:chartColors.muted, backgroundColor:'transparent', borderWidth:2, tension:0.4, pointRadius:gran==='week'?3:1, pointBackgroundColor:chartColors.muted }, - { label:periodLabel(currStart,currEnd), data:labels.map((_,i) => cg[i+1]||0), borderColor:chartColors.primary, backgroundColor:chartColors.primary+'15', borderWidth:2, tension:0.4, fill:true, pointRadius:gran==='week'?4:2, pointBackgroundColor:chartColors.primary }, - ] + tooltipLabels, + data: { + labels, + datasets: [ + { label:periodLabel(prevStart,prevEnd), data:labels.map((_,i) => pg[i+1]||0), borderColor:chartColors.muted, backgroundColor:'transparent', borderWidth:2, tension:0.4, pointRadius:gran==='week'?3:1, pointBackgroundColor:chartColors.muted }, + { label:periodLabel(currStart,currEnd), data:labels.map((_,i) => cg[i+1]||0), borderColor:chartColors.primary, backgroundColor:chartColors.primary+'15', borderWidth:2, tension:0.4, fill:true, pointRadius:gran==='week'?4:2, pointBackgroundColor:chartColors.primary }, + ] + } }; }, [prevData, currData, prevStart, currStart, prevEnd, currEnd, metric, gran, getVal, L]); + const trendData = trendResult.data; const museumData = useMemo(() => { const all = [...new Set(data.map(r => r.museum_name))].filter(Boolean) as string[]; @@ -173,6 +192,18 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM const chartOpts: any = { ...baseOpts, plugins:{ ...baseOpts.plugins, legend:{ position:'top', align:'end', labels:{ boxWidth:12, padding:12 } } } }; return { chartOpts }; }, [baseOpts]); + const trendOpts: any = useMemo(() => ({ + ...chartOpts, + plugins: { + ...chartOpts.plugins, + tooltip: { + ...chartOpts.plugins.tooltip, + callbacks: { + title: (items: any[]) => trendResult.tooltipLabels[items[0]?.dataIndex] ?? items[0]?.label, + } + } + } + }), [chartOpts, trendResult.tooltipLabels]); const metricOpts = [ { value:'revenue', label:L.revenue }, { value:'visitors', label:L.visitors }, @@ -274,7 +305,7 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM {granOpts.map(o => )} -
+
diff --git a/src/components/Dashboard.tsx b/src/components/Dashboard.tsx index c1836c4..d29e59b 100644 --- a/src/components/Dashboard.tsx +++ b/src/components/Dashboard.tsx @@ -88,7 +88,7 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set return rows.reduce((s,r) => s + parseFloat(String((r as any)[f[m]]||0)), 0); }, [revenueField]); - const trendData = useMemo(() => { + const trendResult = useMemo(() => { const group = (rows: MuseumRecord[], ps: string) => { const s = new Date(ps); const acc: Record = {}; rows.forEach(r => { @@ -102,18 +102,37 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set }; const pg = group(prevData, prevStart), cg = group(filteredData, start); const maxK = Math.max(...Object.keys(pg).map(Number), ...Object.keys(cg).map(Number), 1); + const s0 = new Date(start); + const fmt = (d: Date) => d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short' }); const labels = Array.from({length:maxK}, (_,i) => - gran==='week' ? `W${i+1}` : gran==='month' ? L.monthShort[(new Date(start).getMonth()+i)%12] : `D${i+1}` + gran==='week' ? `W${i+1}` : gran==='month' ? L.monthShort[(s0.getMonth()+i)%12] : `D${i+1}` ); + const tooltipLabels = Array.from({length:maxK}, (_,i) => { + if (gran==='week') { + const ws = new Date(s0.getTime() + i * 7 * 86400000); + const we = new Date(s0.getTime() + (i+1) * 7 * 86400000 - 86400000); + return `Week ${i+1} · ${fmt(ws)} – ${fmt(we)}`; + } + if (gran==='month') { + const ms = new Date(s0.getFullYear(), s0.getMonth() + i, 1); + return ms.toLocaleDateString('en-GB', { month: 'long', year: 'numeric' }); + } + const ds = new Date(s0.getTime() + i * 86400000); + return ds.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' }); + }); const prevYear = parseInt(start.slice(0,4))-1; return { - labels, - datasets: [ - { label:`${prevYear}`, data:labels.map((_,i) => pg[i+1]||0), borderColor:chartColors.muted, backgroundColor:'transparent', borderWidth:1.5, tension:0.4, pointRadius:0, borderDash:[4,3] }, - { label:start.slice(0,4), data:labels.map((_,i) => cg[i+1]||0), borderColor:chartColors.primary, backgroundColor:chartColors.primary+'18', borderWidth:2.5, tension:0.4, fill:true, pointRadius:gran==='week'?3:1, pointBackgroundColor:chartColors.primary }, - ] + tooltipLabels, + data: { + labels, + datasets: [ + { label:`${prevYear}`, data:labels.map((_,i) => pg[i+1]||0), borderColor:chartColors.muted, backgroundColor:'transparent', borderWidth:1.5, tension:0.4, pointRadius:0, borderDash:[4,3] }, + { label:start.slice(0,4), data:labels.map((_,i) => cg[i+1]||0), borderColor:chartColors.primary, backgroundColor:chartColors.primary+'18', borderWidth:2.5, tension:0.4, fill:true, pointRadius:gran==='week'?3:1, pointBackgroundColor:chartColors.primary }, + ] + } }; }, [filteredData, prevData, prevStart, start, metric, gran, getVal, L]); + const trendData = trendResult.data; const museumData = useMemo(() => { const g = groupByMuseum(filteredData, includeVAT); @@ -172,6 +191,19 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set const barNoLegend: any = { ...chartOpts, plugins:{ ...chartOpts.plugins, legend:{ display:false } } }; return { chartOpts, barHorizOpts, barNoLegend }; }, [baseOpts]); + const trendOpts: any = useMemo(() => ({ + ...chartOpts, + plugins: { + ...chartOpts.plugins, + tooltip: { + ...chartOpts.plugins.tooltip, + callbacks: { + title: (items: any[]) => trendResult.tooltipLabels[items[0]?.dataIndex] ?? items[0]?.label, + } + } + } + }), [chartOpts, trendResult.tooltipLabels]); + const pieOptions: any = useMemo(() => ({ responsive: true, maintainAspectRatio: false, plugins: { @@ -250,7 +282,7 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set {granOpts.map(o => )}
-
+