feat: add interactive category filtering to cospend bar chart
Some checks failed
CI / update (push) Failing after 1m26s
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:
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user