javascriptchart.js

Hide and collapse x-Axis when no data available


I would like to create a graph using chart.js where the x-Axis shows only those ticks for whose there is data available. Of course I could do some preprocessing of the data and eliminate those labels and corresponding 0-values in the data. But once the graph is displayed, i would like to hide/show some datasets and i want the graph to collapse eliminating the zero-values.

All datasets visible:

Graph with all four years of data

Two datasets hidden, some months disappeared from x-Axis:

Graph with only two datasets visible

(To get this picture, i cheated the input data in Excel)

I haven't found any axis property that would achieve this. Ticks can be shown or hidden, but can the space on the axis collapse so there is no gap? Is there any way to do it with a callback on each hide/show of a dataset?

I need this as a "workaround solution" for my other question here

Edit: Based on the jsfiddle of @kikon (thanks a lot), i created this working example: if you hide the years 2021 and 2022 some months remain empty. I wish to have these months removed from the graph.

const config = {
    type: "bar",
    data: {
        labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
        datasets: [
            {
                label: '2020',
                data: [null, null, null, 2, 10,25,31,28,10,2, null, null],
                backgroundColor: 'red'
            },
      {
                label: '2021',
                data: [1, 4 , 4, 6, 8, 23, 28, 30, 5, 1, null, null],
                backgroundColor: 'blue'
        },
      {
                label: '2022',
                data: [1, 4 , 4, 4, 12, 30, 26, 30, 30, 10, 4, 4, 4],
                backgroundColor: 'green'
            },
      {
                label: '2023',
                data: [1, null , null, 4, 20, 29, 31, 31, 10, null, null, 2],
                backgroundColor: 'orange'
            },
        ]
    },
    options: {
        maintainAspectRatio: false,
        barPercentage: 0.99,
        categoryPercentage: 0.95,
        skipNull: true,
        scales:{
            x: {
                ticks: {
                    minRotation: 90
                }
            }
        },
        plugins: {
            legend: {
                display: true
            },
          title: {
                display: true,
              text: 'Lifeguard working days',
          }
        }
    }
};

const chart = new Chart('myChart', config);

For my original question @kikon has already built a working solution, so I am more than happy. However, this solution does not apply to this case, where the different datasets share the same labels on the x-axis.


Solution

  • Based on @kikon's answer to this other question of mine, i could build a solution that works even for this use case.

    In both cases, the trick is to assign a callback function to plugins.labels.onClickin the Chart options. So each time the callback function is called, it is checked which datasets are visible, then for the visible ones it is checked which labels on the x-axis have data to be displayed. Then the labels and datasets are shrunk accordingly.

    See the code below or in the jsfiddle

    const config = {
        type: "bar",
        data: {
            labels: ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'],
            datasets: [
                {
                    label: '2020',
                    data: [null, null, null, 2, 10,25,31,28,10,2, null, null],
                    backgroundColor: 'red'
                },
                {
                    label: '2021',
                    data: [1, 4 , 4, 6, 8, 23, 28, 30, 5, 1, null, null],
                    backgroundColor: 'blue'
                },
                {
                    label: '2022',
                    data: [1, 4 , 4, 4, 12, 30, 26, 30, 30, 10, 4, 4, 4],
                    backgroundColor: 'green'
                },
                {
                    label: '2023',
                    data: [1, null , null, 4, 20, 29, 31, 31, 10, null, null, 2],
                    backgroundColor: 'orange'
                },
            ]
        },
        options: {
            maintainAspectRatio: false,
            barPercentage: 0.99,
            categoryPercentage: 0.95,
            skipNull: false, //otherwise columns might vary in width
            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 (chart.data.labels.length > unhiddenData.labels.length){
        unhiddenData.labels = [... chart.data.labels];
      }
      if (chart.data.datasets.length > unhiddenData.data.length){
        unhiddenData.data = [];
        for (const dataset of chart.data.datasets){
            unhiddenData.data.push([...dataset.data]);
          }
      }
    }
    
    function _updateData(chart){
        const nDatasets = chart.data.datasets.length,
            hidden = Array.from({length: nDatasets}, (_, i)=>chart.getDatasetMeta(i).hidden),
        nLabels = unhiddenData.labels.length;
        if(hidden.length < nDatasets){
            hidden.push(true);
        }
        // Check for which month there is data available
      const hasValues = Array.from({length: nLabels}, ()=>false);
      for (let i = 0; i < nDatasets; i++){
        if (!hidden[i]){
            for(let j = 0; j < nLabels; j++){
            if (unhiddenData.data[i][j] !== null){    // Change comparison value to 0 if zero values shall be omitted
                hasValues[j] = true;
            }
          }
        }
      }
      // Construct data-vectors, omitting those where everything is null
        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 < nLabels; j++){
                    if(hasValues[j]){
                        data.push(unhiddenData.data[i][j]);
                    }
                }
            }
        }
      chart.data.labels = [];
      const labels = chart.data.labels;
      for(let i = 0; i < nLabels; i++){
        if(hasValues[i]){
            labels.push(unhiddenData.labels[i]);
        }
      }
    }
    
    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);
    

    The two use-cases differ because in this case we want to keep the labels on the x-axis as long as there is matching data in at least one of the visible datasets. In the other case, the labels on the x-axis were "proprietary" to one dataset and thus always removed when the dataset is hidden.

    Edit: If you wish to add data to the chart later than at creation, you must add the following function to your code and use this to add the data. Adding the data directly to data.datasets would destroy the structure. Note that any added data must have the same length as the datasets already on the chart.

    function addDataGroup(chart, newGroupLabel, 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)
    
        _updateData(chart);
    
        chart.update();
    }