feat(charts): show actual dates in trend chart tooltips
Deploy HiHala Dashboard / deploy (push) Successful in 11s

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 <noreply@anthropic.com>
This commit is contained in:
fahed
2026-04-30 10:37:05 +03:00
parent c858075232
commit 1070490ad2
2 changed files with 79 additions and 16 deletions
+39 -8
View File
@@ -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)}`; 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 group = (rows: MuseumRecord[], ps: string) => {
const s=new Date(ps); const acc: Record<number,MuseumRecord[]> = {}; const s=new Date(ps); const acc: Record<number,MuseumRecord[]> = {};
rows.forEach(r => { 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 pg = group(prevData, prevStart), cg = group(currData, currStart);
const maxK = Math.max(...Object.keys(pg).map(Number), ...Object.keys(cg).map(Number), 1); 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) => 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 { return {
labels, tooltipLabels,
datasets: [ data: {
{ 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 }, labels,
{ 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 }, 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]); }, [prevData, currData, prevStart, currStart, prevEnd, currEnd, metric, gran, getVal, L]);
const trendData = trendResult.data;
const museumData = useMemo(() => { const museumData = useMemo(() => {
const all = [...new Set(data.map(r => r.museum_name))].filter(Boolean) as string[]; 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 } } } }; const chartOpts: any = { ...baseOpts, plugins:{ ...baseOpts.plugins, legend:{ position:'top', align:'end', labels:{ boxWidth:12, padding:12 } } } };
return { chartOpts }; return { chartOpts };
}, [baseOpts]); }, [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 = [ const metricOpts = [
{ value:'revenue', label:L.revenue }, { value:'visitors', label:L.visitors }, { 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 => <button key={o.value} type="button" aria-pressed={gran===o.value} className={`alt-ctrl${gran===o.value?' alt-ctrl-on':''}`} onClick={() => setGran(o.value)}>{o.label}</button>)} {granOpts.map(o => <button key={o.value} type="button" aria-pressed={gran===o.value} className={`alt-ctrl${gran===o.value?' alt-ctrl-on':''}`} onClick={() => setGran(o.value)}>{o.label}</button>)}
</div> </div>
</div> </div>
<div className="alt-chart-wrap"><Line data={trendData} options={chartOpts} /></div> <div className="alt-chart-wrap"><Line data={trendData} options={trendOpts} /></div>
</div> </div>
<div className="alt-chart-card"> <div className="alt-chart-card">
<div className="alt-chart-header"> <div className="alt-chart-header">
+40 -8
View File
@@ -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); return rows.reduce((s,r) => s + parseFloat(String((r as any)[f[m]]||0)), 0);
}, [revenueField]); }, [revenueField]);
const trendData = useMemo(() => { const trendResult = useMemo(() => {
const group = (rows: MuseumRecord[], ps: string) => { const group = (rows: MuseumRecord[], ps: string) => {
const s = new Date(ps); const acc: Record<number, MuseumRecord[]> = {}; const s = new Date(ps); const acc: Record<number, MuseumRecord[]> = {};
rows.forEach(r => { 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 pg = group(prevData, prevStart), cg = group(filteredData, start);
const maxK = Math.max(...Object.keys(pg).map(Number), ...Object.keys(cg).map(Number), 1); 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) => 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; const prevYear = parseInt(start.slice(0,4))-1;
return { return {
labels, tooltipLabels,
datasets: [ data: {
{ 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] }, labels,
{ 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 }, 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]); }, [filteredData, prevData, prevStart, start, metric, gran, getVal, L]);
const trendData = trendResult.data;
const museumData = useMemo(() => { const museumData = useMemo(() => {
const g = groupByMuseum(filteredData, includeVAT); 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 } } }; const barNoLegend: any = { ...chartOpts, plugins:{ ...chartOpts.plugins, legend:{ display:false } } };
return { chartOpts, barHorizOpts, barNoLegend }; return { chartOpts, barHorizOpts, barNoLegend };
}, [baseOpts]); }, [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(() => ({ const pieOptions: any = useMemo(() => ({
responsive: true, maintainAspectRatio: false, responsive: true, maintainAspectRatio: false,
plugins: { plugins: {
@@ -250,7 +282,7 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set
{granOpts.map(o => <button key={o.value} type="button" aria-pressed={gran===o.value} className={`alt-ctrl${gran===o.value?' alt-ctrl-on':''}`} onClick={() => setGran(o.value)}>{o.label}</button>)} {granOpts.map(o => <button key={o.value} type="button" aria-pressed={gran===o.value} className={`alt-ctrl${gran===o.value?' alt-ctrl-on':''}`} onClick={() => setGran(o.value)}>{o.label}</button>)}
</div> </div>
</div> </div>
<div className="alt-chart-wrap alt-chart-wrap--tall"><Line data={trendData} options={chartOpts} /></div> <div className="alt-chart-wrap alt-chart-wrap--tall"><Line data={trendData} options={trendOpts} /></div>
</div> </div>
<div className="alt-chart-card"> <div className="alt-chart-card">