javascriptchartschart.jsdatasetbar-chart

Chart.js - Alignment of 0 values for multiple datasets


I am trying to compare 2 datasets in chart.js. There are large differences in values so in order to compare the values a have decided to graph the data on 2 different y axes.

1 of the datasets has some negative values and the base of the graph starts from a negative value. The other dataset only has positive values and the base of the graph starts from 0.

Is there any setting will allow me to align the 0's on the y axes (when working with 2 different axes)?

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,
     },
   },
}

Example graph:

Alignment of Y axes


Solution

  • The same as for your previous question, while there's no specific option for what you want to achieve, it can be done by setting the y and y1 axes scriptable min and max. In this case, the important parameter that needs to be set using a proportionality formula is min, while max can be set to the maximum value of data to avoid the additional space to the next multiple of the axis step:

    function computeMin(which, data){
        // computes min for scale `which` (0 -> 'y', 1 -> 'y1') such that the
        // level of 0 is the same for all datasets
        // note: there's no negative-only dataset
        const dataValues = data.datasets.map(dataset=>dataset.data),
            dataMin = dataValues.map(data => Math.min(0, ...data)),
            dataMax = dataValues.map(data => Math.max(0, ...data)),
            ratios = dataMin.map((min_i, i) => -min_i/(-min_i + dataMax[i])),
            bestRatio = Math.max(...ratios);
        if(ratios[which] < bestRatio){
            const delta = (dataMax[which] * bestRatio - dataMin[which] * (bestRatio - 1))/(bestRatio - 1);
            return dataMin[which] + delta;
        }
        return dataMin[which]
    }
    

    used as:

    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)
            }
        }
    }      
    

    In a demo snippet:

    function computeMin(which, data){
        // computes min for scale `which` (0 -> 'y', 1 -> 'y1') such that the
        // level of 0 is the same for all datasets
        // note: no negative-only datasets are not supported, to support those
        // see https://jsfiddle.net/oruvz3dx/, https://jsfiddle.net/oruvz3dx/1
        const dataValues = data.datasets.map(dataset=>dataset.data),
            dataMin = dataValues.map(data => Math.min(0, ...data)),
            dataMax = dataValues.map(data => Math.max(0, ...data)),
            ratios = dataMin.map((min_i, i) => -min_i/(-min_i + dataMax[i])),
            bestRatio = Math.max(...ratios);
        if(ratios[which] < bestRatio){
            const delta = (dataMax[which] * bestRatio - dataMin[which] * (bestRatio - 1))/(bestRatio - 1);
            return dataMin[which] + delta;
        }
        return dataMin[which]
    }
    
    
    const config = {
        type: "bar",
    
        data: {
            labels: ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'],
            datasets: [
                {
                    label: 2023,
                    data: [50, -59, -80, -81, -56, 55, 65, 108, 80, -41, 56, 55],
    
                },
                {
                    label: 2024,
                    data: [20, 65, 80, 102, 55, 56, 65, 59, 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
                    },
                    min: ({chart}) => computeMin(0, chart.data),
                    max: ({chart}) => Math.max(...chart.data.datasets[0].data)
                },
                y1: {
                    position: 'right',
                    grid: {
                        display: false,
                        drawBorder: false,
                        drawOnChartArea: false,
                    },
                    ticks: {
                        display: true,
                        includeBounds: false
                    },
                    min: ({chart}) => computeMin(1, chart.data),
                    max: ({chart}) => Math.max(...chart.data.datasets[1].data)
                },
            }
        }
    };
    
    new Chart('myChart', config);
    <div style="height: 300px">
        <canvas id="myChart">
        </canvas>
    </div>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

    This solution is adapted to the stated parameters of the problem: there are only two datasets, none of which has negative-only values.

    A generalisation of this solution, that allows for multiple datasets with negative-only, positive-only and mixed positive-negative values, is set in this fiddle; here's a version of that fiddle that caches the computed min and max values, to avoid repeated computation.