feat: add interactive category filtering to cospend bar chart
Some checks failed
CI / update (push) Failing after 1m26s

Allow users to click on bar segments or legend items to filter to a single category. Clicking again restores all categories. Totals displayed above bars now dynamically update to reflect only visible categories.
This commit is contained in:
2025-11-13 13:14:45 +01:00
parent 650a0bcf31
commit a2df59f11d

View File

@@ -8,6 +8,7 @@
let canvas; let canvas;
let chart; let chart;
let hiddenCategories = new Set(); // Track which categories are hidden
// Register Chart.js components // Register Chart.js components
Chart.register(...registerables); Chart.register(...registerables);
@@ -126,6 +127,30 @@
size: 14, size: 14,
weight: 'bold' weight: 'bold'
} }
},
onClick: (event, legendItem, legend) => {
const datasetIndex = legendItem.datasetIndex;
const clickedMeta = chart.getDatasetMeta(datasetIndex);
// Check if only this dataset is currently visible
const onlyThisVisible = chart.data.datasets.every((dataset, idx) => {
const meta = chart.getDatasetMeta(idx);
return idx === datasetIndex ? !meta.hidden : meta.hidden;
});
if (onlyThisVisible) {
// Show all categories
chart.data.datasets.forEach((dataset, idx) => {
chart.getDatasetMeta(idx).hidden = false;
});
} else {
// Hide all except the clicked one
chart.data.datasets.forEach((dataset, idx) => {
chart.getDatasetMeta(idx).hidden = idx !== datasetIndex;
});
}
chart.update();
} }
}, },
title: { title: {
@@ -175,6 +200,31 @@
interaction: { interaction: {
intersect: true, intersect: true,
mode: 'point' mode: 'point'
},
onClick: (event, activeElements) => {
if (activeElements.length > 0) {
const datasetIndex = activeElements[0].datasetIndex;
// Check if only this dataset is currently visible
const onlyThisVisible = chart.data.datasets.every((dataset, idx) => {
const meta = chart.getDatasetMeta(idx);
return idx === datasetIndex ? !meta.hidden : meta.hidden;
});
if (onlyThisVisible) {
// Show all categories
chart.data.datasets.forEach((dataset, idx) => {
chart.getDatasetMeta(idx).hidden = false;
});
} else {
// Hide all except the clicked one
chart.data.datasets.forEach((dataset, idx) => {
chart.getDatasetMeta(idx).hidden = idx !== datasetIndex;
});
}
chart.update();
}
} }
}, },
plugins: [{ plugins: [{
@@ -189,28 +239,33 @@
ctx.textAlign = 'center'; ctx.textAlign = 'center';
ctx.textBaseline = 'bottom'; ctx.textBaseline = 'bottom';
// Calculate and display monthly totals // Calculate and display monthly totals (only for visible categories)
chart.data.labels.forEach((label, index) => { chart.data.labels.forEach((label, index) => {
let total = 0; let total = 0;
chart.data.datasets.forEach(dataset => { chart.data.datasets.forEach((dataset, datasetIndex) => {
total += dataset.data[index] || 0; // Only add to total if the dataset is visible
const meta = chart.getDatasetMeta(datasetIndex);
if (meta && !meta.hidden) {
total += dataset.data[index] || 0;
}
}); });
if (total > 0) { if (total > 0) {
// Get the x position for this month from any dataset // Get the x position for this month from any visible dataset
const meta = chart.getDatasetMeta(0); let x = null;
if (meta && meta.data[index]) { let maxY = chartArea.bottom;
const x = meta.data[index].x;
// Find the highest point for this month across all datasets for (let datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
let maxY = chartArea.bottom; const datasetMeta = chart.getDatasetMeta(datasetIndex);
for (let datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) { if (datasetMeta && !datasetMeta.hidden && datasetMeta.data[index]) {
const datasetMeta = chart.getDatasetMeta(datasetIndex); if (x === null) {
if (datasetMeta && datasetMeta.data[index]) { x = datasetMeta.data[index].x;
maxY = Math.min(maxY, datasetMeta.data[index].y);
} }
maxY = Math.min(maxY, datasetMeta.data[index].y);
} }
}
if (x !== null) {
// Display the total above the bar // Display the total above the bar
ctx.fillText(`CHF ${total.toFixed(0)}`, x, maxY - 10); ctx.fillText(`CHF ${total.toFixed(0)}`, x, maxY - 10);
} }
@@ -271,5 +326,10 @@
canvas { canvas {
max-width: 100%; max-width: 100%;
height: 100% !important; height: 100% !important;
cursor: pointer;
}
canvas:hover {
opacity: 0.95;
} }
</style> </style>