feat(charts): right-side bold legend with circle indicators + tooltip polish
Deploy HiHala Dashboard / deploy (push) Successful in 10s
Deploy HiHala Dashboard / deploy (push) Successful in 10s
- Legend moved to right, bold text, color matches line, circle outline indicator - Museums with no data in current period excluded from chart and legend - Tooltip uses circle point style and boxPadding for readable spacing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -161,7 +161,8 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM
|
|||||||
const ds = new Date(cs0.getTime() + i * 86400000);
|
const ds = new Date(cs0.getTime() + i * 86400000);
|
||||||
return ds.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' });
|
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 multiMuseum = museumList.length >= 2;
|
||||||
const museumDatasets = museumList.map((museum, idx) => {
|
const museumDatasets = museumList.map((museum, idx) => {
|
||||||
const mg = group(currData.filter(r => r.museum_name === museum), currStart);
|
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(() => ({
|
const trendOpts: any = useMemo(() => ({
|
||||||
...chartOpts,
|
...chartOpts,
|
||||||
interaction: { mode: 'nearest', intersect: false },
|
interaction: { mode: 'nearest', intersect: false },
|
||||||
layout: {
|
|
||||||
...chartOpts.layout,
|
|
||||||
padding: { ...(chartOpts.layout?.padding ?? {}), right: trendResult.multiMuseum ? 110 : 5 },
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
...chartOpts.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: {
|
tooltip: {
|
||||||
...chartOpts.plugins.tooltip,
|
...chartOpts.plugins.tooltip,
|
||||||
callbacks: {
|
callbacks: {
|
||||||
@@ -228,7 +251,7 @@ export default function PeriodSelectorDemo({ data, seasons, includeVAT, allowedM
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}), [chartOpts, trendResult.tooltipLabels, trendResult.multiMuseum]);
|
}), [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 },
|
||||||
|
|||||||
@@ -122,7 +122,8 @@ export default function DashboardDemo({ data, seasons: _seasons, includeVAT, set
|
|||||||
return ds.toLocaleDateString('en-GB', { day: 'numeric', month: 'long', year: 'numeric' });
|
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;
|
||||||
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 multiMuseum = museumList.length >= 2;
|
||||||
const museumDatasets = museumList.map((museum, idx) => {
|
const museumDatasets = museumList.map((museum, idx) => {
|
||||||
const mg = group(filteredData.filter(r => r.museum_name === museum), start);
|
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(() => ({
|
const trendOpts: any = useMemo(() => ({
|
||||||
...chartOpts,
|
...chartOpts,
|
||||||
interaction: { mode: 'nearest', intersect: false },
|
interaction: { mode: 'nearest', intersect: false },
|
||||||
layout: {
|
|
||||||
...chartOpts.layout,
|
|
||||||
padding: { ...(chartOpts.layout?.padding ?? {}), right: trendResult.multiMuseum ? 110 : 5 },
|
|
||||||
},
|
|
||||||
plugins: {
|
plugins: {
|
||||||
...chartOpts.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: {
|
tooltip: {
|
||||||
...chartOpts.plugins.tooltip,
|
...chartOpts.plugins.tooltip,
|
||||||
callbacks: {
|
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(() => ({
|
const pieOptions: any = useMemo(() => ({
|
||||||
responsive: true, maintainAspectRatio: false,
|
responsive: true, maintainAspectRatio: false,
|
||||||
|
|||||||
@@ -113,7 +113,9 @@ export const createBaseOptions = (showDataLabels: boolean): any => {
|
|||||||
titleFont: { size: 12 },
|
titleFont: { size: 12 },
|
||||||
bodyFont: { size: 11 },
|
bodyFont: { size: 11 },
|
||||||
rtl: false,
|
rtl: false,
|
||||||
textDirection: 'ltr'
|
textDirection: 'ltr',
|
||||||
|
usePointStyle: true,
|
||||||
|
boxPadding: 6,
|
||||||
},
|
},
|
||||||
datalabels: createDataLabelConfig(showDataLabels, {
|
datalabels: createDataLabelConfig(showDataLabels, {
|
||||||
color: theme.textPrimary,
|
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);
|
ChartJS.register(trendLinePlugin);
|
||||||
|
|||||||
Reference in New Issue
Block a user