javascriptchartschart.jsdatasetbar-chart

Dataset scaling in a Chart.js bar chart for better comparison


I’m using Chart.js to graph two datasets on the same bar chart for a time-based comparison (x-axis). However, I’m encountering an issue where the height of one graph squashes or expands to fit the height of the other, making it difficult to compare them effectively.

For example, the purple graph below increases by 26x, while the blue graph increases by only 5x. Due to the scaling, it’s hard to assess the impact of these changes, as the purple graph is squashed into the height of the blue graph.

I think starting the purple graph at the same level as the blue graph at the first data point might help, but I’m concerned that the data will still be squashed to fit the blue graph’s height.

Are there specific Chart.js settings or configurations I can adjust to improve the comparability of these datasets?

My code for chart scales is as follows:

y: {
    beginAtZero: true,
    grid:  {
        display: false,
        drawBorder: false,
        drawOnChartArea: false,
     },
     ticks: {
        display: true,
     },
   },
},
y1: {
     beginAtZero: true,
     position: 'right',
     grid: {
        display: false,
        drawBorder: false,
        drawOnChartArea: false,
     },
     ticks: {
        display: true,
     },
   },
}

Current graph

Multi dataset bar chart

Expected graph

enter image description here


Solution

  • There isn't a chart.js option that would arrange the values such that the first bars of the two datasets have equal height, but it can be arranged by computing the value of axes max dynamically:

    function computeMax(which, data){
        // computes max for scale `which` (0 -> 'y', 1 -> 'y1') such that the first
        // bars of the two datasets have the same height
        const other = 1 - which,
            dataValues = data.datasets.map(dataset=>dataset.data),
            dataMax = dataValues.map(data => Math.max(...data)),
            firstToMax = dataValues.map((data, i) => data[0] / dataMax[i])
        if(firstToMax[which] > firstToMax[other]){
            return firstToMax[which]/firstToMax[other] * dataMax[which];
        }
        else{
            return dataMax[which];
        }
    }
    

    the function computeMax being called for the scriptable max property of each axis, like this:

    options: {
        ........... other options
        scales: {
            y: {
                .......... other y axis options
                 ticks: {
                     display: true,
                     includeBounds: false
                 },
                 max: ({chart}) => computeMax(0, chart.data)
            },
            y1: {
                .......... other y1 axis options
                ticks: {
                     display: true,
                     includeBounds: false
                 },
                 max: ({chart}) => computeMax(1, chart.data)
            }
        }
    }      
    

    Here's a snippet that includes that piece of code:

    function computeMax(which, data){
        // computes max for scale `which` (0 -> 'y', 1 -> 'y1') such that the first
        // bars of the two datasets have the same height
        const other = 1 - which,
            dataValues = data.datasets.map(dataset=>dataset.data),
            dataMax = dataValues.map(data => Math.max(...data)),
            firstToMax = dataValues.map((data, i) => data[0] / dataMax[i])
        if(firstToMax[which] > firstToMax[other]){
            return firstToMax[which]/firstToMax[other] * dataMax[which];
        }
        else{
            return dataMax[which];
        }
    }
    
    
    const config = {
        type: "bar",
    
        data: {
            labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
            datasets: [
                {
                    label: 2023,
                    data: [20, 65, 80, 102, 55, 56, 65, 59, 80, 81, 56, 55],
                },
                {
                    label: 2024,
                    data: [50, 59, 80, 81, 56, 55, 65, 108, 80, 81, 56, 55].map(x => x * 10),
                    yAxisID: 'y1'
                },
            ]
        },
    
        options: {
            maintainAspectRatio: false,
            scales: {
                y: {
                    grid: {
                        display: false,
                        drawBorder: false,
                        drawOnChartArea: false
                    },
                    ticks: {
                        display: true,
                        includeBounds: false
                    },
                    max: ({chart}) => computeMax(0, chart.data)
                },
                y1: {
                    beginAtZero: true,
                    position: 'right',
                    grid: {
                        display: false,
                        drawBorder: false,
                        drawOnChartArea: false,
                    },
                    ticks: {
                        display: true,
                        includeBounds: false
                    },
                    max: ({chart}) => computeMax(1, chart.data)
                },
            }
        }
    };
    
    new Chart('myChart', config);
    <div style="height: 300px">
        <canvas id="myChart">
        </canvas>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

    Since the axes had beginAtZero set true it was clear that all the values are positive. A generalization with multiple datasets and including negative values can be found in this fiddle.