I am trying to build a bar-graph using chart.js where i add "random" groups of data. Each group should be labeled with a group name and each bar with the property it really represents.
Data is fetched from a DB and the groups may differ in size, depending on the selection criteria of the user.
In Excel it is possible to achieve a result similar to the desired one (padding each new dataset with 0
for each already existing bar). The drawback is that with each new dataset the bars grow thinner and thinner.
A similar result is achievable with Chart.js using the same trick. Each new dataset is padded with null
so the bars don't get mixed up with the previous ones. By setting skipNull: true
the bars don't grow thinner.
So far so good, but when you click on a category name in the legend, its data is hidden, but the labels on X-Axis and the gaps left by the hidden bars still remain. It would be very useful to hide/show some of the added categories to enable a closer comparison between two categories.
In a previous SO-Question, the only answer tells it is not possible to achieve my desired result, but this QA is seven years old, so things might have changed.
Another SO-Post achieves a similar result by reordering the data and putting the Bar-Names as labels on top of the bars, but it loses the ability to hide a category. (The legend is hidden to avoid it, if you show the legend and click on one item, it will not hide a category but the n-th element of all categories. Try it out on the demo of the accepted answer, edit line 76)
An alternative approach would be to add a separate "chart-object" to the same canvas sharing the axes, is there any possibility to achieve this?
The bars shouldn't shrink if skipNull
option is set true
and one adds null
s for the datasets that don't have data at certain x
positions; if each dataset is to have an exclusive, contiguous
region of the x
axis, then its data
should have null
s for all positions of the regions allocated to other datasets. Those null
s should be kept in sync whenever new data is added. Here's a snippet implementing those ideas, and also adds an empty x category to separate consecutive datasets:
const config = {
type: "bar",
data: {
labels: ['Cox-Orange', 'GS', 'Golden', 'Redlove'],
datasets: [
{
label: 'Apples',
data: Array.from({length: 4}, () => 100 + 200 * Math.random()),
backgroundColor: 'red'
},
]
},
options: {
maintainAspectRatio: false,
barPercentage: 0.99,
categoryPercentage: 0.95,
skipNull: true,
scales:{
x: {
ticks: {
minRotation: 90
}
}
}
}
};
function addDataGroup(newGroupLabel, newAxisLabels, newData, color){
const nNew = newAxisLabels.length,
datasets = chart.data.datasets,
nExisting = datasets[datasets.length-1].data.length;
for(const dataset of datasets){
dataset.data.push(...Array(nNew + 1).fill(null));
}
const newDataset = {
label: newGroupLabel,
data: Array(nExisting+1).fill(null).concat(newData),
backgroundColor: color
}
datasets.push(newDataset);
chart.data.labels.push('', ...newAxisLabels);
chart.update();
}
const chart = new Chart('myChart', config);
setTimeout(
()=>{
addDataGroup('Citrus', ['Oranges', 'Lemon', 'Pomelo', 'Tangerine'],
Array.from({length: 4}, () => 100 + 200 * Math.random()), 'orange');
}, 2000
);
setTimeout(
()=>{
addDataGroup('Cucumber', ['Straight', 'Curved', 'Long'],
Array.from({length: 3}, () => 100 + 200 * Math.random()), 'green');
}, 5000
);
setTimeout(
()=>{
addDataGroup('Exotic', ['Pomelo', 'Papaya', 'Pineapple', 'Banana'],
Array.from({length: 4}, () => 100 + 200 * Math.random()), 'yellow');
}, 8000
);
<div style="height: 300px">
<canvas id="myChart">
</canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
As for the x
axis adapting to datasets being hidden through the legend, by eliminating the
categories that no longer have visible data, that is an interesting proposition. It can be
implemented through legend.onClick
and then manipulating the chart.data.labels
. One has to also adjust the datasets themselves in order to keep the null
s mentioned above in sync with the labels - remove the null
s for a dataset that was just hidden and add them back when the dataset is made visible again:
const config = {
type: "bar",
data: {
labels: ['Cox-Orange', 'GS', 'Golden', 'Redlove'],
datasets: [
{
label: 'Apples',
data: Array.from({length: 4}, () => 100 + 200 * Math.random()),
backgroundColor: 'red'
},
]
},
options: {
maintainAspectRatio: false,
barPercentage: 0.99,
categoryPercentage: 0.95,
skipNull: true,
scales:{
x: {
ticks: {
minRotation: 90
}
}
},
plugins:{
legend:{
labels: {
color: 'black'
},
onClick: toggleGroupWithLabels
}
}
}
};
const unhiddenData = {data:[], labels:[]};
// can be linked to the chart object if global variables are inconvenient
function initUpdates(chart){
if(unhiddenData.data.length === 0){
if(chart.data.datasets.length > 1){
throw new Error('Cannot start with multiple datasets');
}
else if(chart.data.datasets.length === 1){
unhiddenData.data.push([...chart.data.datasets[0].data]);
unhiddenData.labels = [[...chart.data.labels]];
}
}
}
function addDataGroup(chart, newGroupLabel, newAxisLabels, newData, color){
chart.options.plugins.legend.onClick = () => null; // disable legend toggle while updating
chart.options.plugins.legend.labels.color = 'lightgray'; // visual indication for above
chart.options.animation.onComplete = () => {
chart.options.plugins.legend.onClick = toggleGroupWithLabels;
chart.options.plugins.legend.labels.color = 'black';
setTimeout(()=>chart.update('none'), 0);
}
chart.data.datasets.push({label: newGroupLabel, backgroundColor: color, data: []})
unhiddenData.data.push(newData)
unhiddenData.labels.push(newAxisLabels);
_updateData(chart);
chart.update();
}
function _updateData(chart){
const nDatasets = chart.data.datasets.length,
hidden = Array.from({length: nDatasets}, (_, i)=>chart.getDatasetMeta(i).hidden);
if(hidden.length < nDatasets){
hidden.push(true);
}
chart.data.labels = [];
const labels = chart.data.labels;
for(let i = 0; i < nDatasets; i++){
if(!hidden[i]){
if(labels.length > 0){
labels.push('');
}
labels.push(...unhiddenData.labels[i]);
}
}
for(let i = 0; i < nDatasets; i++){
if(!hidden[i]){
chart.data.datasets[i].data = [];
const data = chart.data.datasets[i].data;
for(let j = 0; j < nDatasets; j++){
if(!hidden[j]){
if(data.length > 0){
data.push(null);
}
if(i === j){
data.push(...unhiddenData.data[j])
}
else{
data.push(...unhiddenData.data[j].map(() => null));
}
}
}
}
}
}
function toggleGroupWithLabels(...args){
const {chart} = args[0],
{datasetIndex} = args[1];
const metaToggle = chart.getDatasetMeta(datasetIndex);
metaToggle.hidden = !metaToggle.hidden;
_updateData(chart);
chart.update('none');
}
const chart = new Chart('myChart', config);
initUpdates(chart)
setTimeout(
()=>{
addDataGroup(chart, 'Citrus', ['Oranges', 'Lemon', 'Pomelo', 'Tangerine'],
Array.from({length: 4}, () => 100 + 200 * Math.random()), 'orange');
}, 3000
);
setTimeout(
()=>{
addDataGroup(chart, 'Cucumber', ['Straight', 'Curved', 'Long'],
Array.from({length: 3}, () => 100 + 200 * Math.random()), 'green');
}, 6000
);
setTimeout(
()=>{
addDataGroup(chart, 'Exotic', ['Pomelo', 'Papaya', 'Pineapple', 'Banana'],
Array.from({length: 4}, () => 100 + 200 * Math.random()), 'yellow');
}, 9000
)
<div style="height: 300px">
<canvas id="myChart">
</canvas>
</div>
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
or as jsFiddle.