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 chart;
let hiddenCategories = new Set(); // Track which categories are hidden
// Register Chart.js components
Chart.register(...registerables);
@@ -126,6 +127,30 @@
size: 14,
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: {
@@ -175,6 +200,31 @@
interaction: {
intersect: true,
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: [{
@@ -189,28 +239,33 @@
ctx.textAlign = 'center';
ctx.textBaseline = 'bottom';
// Calculate and display monthly totals
// Calculate and display monthly totals (only for visible categories)
chart.data.labels.forEach((label, index) => {
let total = 0;
chart.data.datasets.forEach(dataset => {
total += dataset.data[index] || 0;
chart.data.datasets.forEach((dataset, datasetIndex) => {
// 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) {
// Get the x position for this month from any dataset
const meta = chart.getDatasetMeta(0);
if (meta && meta.data[index]) {
const x = meta.data[index].x;
// Get the x position for this month from any visible dataset
let x = null;
let maxY = chartArea.bottom;
// Find the highest point for this month across all datasets
let maxY = chartArea.bottom;
for (let datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
const datasetMeta = chart.getDatasetMeta(datasetIndex);
if (datasetMeta && datasetMeta.data[index]) {
maxY = Math.min(maxY, datasetMeta.data[index].y);
for (let datasetIndex = 0; datasetIndex < chart.data.datasets.length; datasetIndex++) {
const datasetMeta = chart.getDatasetMeta(datasetIndex);
if (datasetMeta && !datasetMeta.hidden && datasetMeta.data[index]) {
if (x === null) {
x = datasetMeta.data[index].x;
}
maxY = Math.min(maxY, datasetMeta.data[index].y);
}
}
if (x !== null) {
// Display the total above the bar
ctx.fillText(`CHF ${total.toFixed(0)}`, x, maxY - 10);
}
@@ -271,5 +326,10 @@
canvas {
max-width: 100%;
height: 100% !important;
cursor: pointer;
}
canvas:hover {
opacity: 0.95;
}
</style>