Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to position positive value on top and negative value on bottom of stacked bar in chart js

The formatter function gives me the total of each group of stacked bars, I am trying to do the datalabels positioning it up if the total is positive and positioning it down if it is negative. Please any help would be very grateful.

enter image description here

datalabels: {
    anchor: (context) =>{
        const anchor = [];
        let sum = ?;
        
        if(parseFloat(sum) >=0){
            anchor.push('end');
        }else{
            anchor.push('start');
        }
        return anchor;
    },
    align: (context) =>{
        const align = [];
        let sum = ?;

        if(parseFloat(sum) >=0){
            align.push('end');
        }else{
            align.push('bottom');
        }
        return align;

    },
    formatter:(value, context) =>{
        const datasetArray = [];
        context.chart.data.datasets.forEach((dataset) =>{
            if(dataset.data[context.dataIndex] != undefined){
                datasetArray.push(dataset.data[context.dataIndex]);
            }
            
        });
        
        function totalSum(total, datapoint){                                           
            return +total + +datapoint;
        }
        
        let sum = datasetArray.reduce(totalSum);
        if(context.datasetIndex === datasetArray.length - 1){
            return parseFloat(sum).toFixed(2) ;
        }else{
            return '';
        }
    },
}
like image 761
Abzal Ali Avatar asked Sep 16 '25 05:09

Abzal Ali


1 Answers

It's unclear at this point how the bars are stacked - there are many unknown options. A possible combination based on the visual appearance of the image chart could be the following:

const N = 10;
const dataGenerator = (plus = 600, minus = 400, pNull = 1) =>
    Array.from({length: N}, ()=>Math.random() < pNull ?
        Math.round(Math.random()*(plus+minus)-minus)/4 : null)
const ctx1 = document.getElementById('chart1');
new Chart(ctx1, {
    type: "bar",
    plugins: [ChartDataLabels],
    data: {
        labels: Array.from({length: N}, (_, i)=>'l'+(i+1)),
        datasets: [
            {
                data: dataGenerator(),
                stack: 'a',
            },
            {
                data: dataGenerator() ,
                stack: 'a',
            },
            {
                data: dataGenerator(100, 50, 0.5),
                stack: 'a',
            },
        ]
    },
    options: {
        indexAxis: 'x',
        layout: {
            padding: {
                top: 20,
                bottom: 20
            }
        },
        animation: {
            duration: 0
        },
        scales: {
            x: {
                ticks:{
                    display: false
                }
            },
            y: {
                stacked: true,
                beginAtZero: true
            }
        },
        plugins:{
            legend:{
                display: false
            },
            datalabels:{
                formatter: (value, context) => {
                    const {dataIndex, datasetIndex, chart} = context;
                    const dataForDataIndex = chart.data.datasets.map(
                        dataset=>dataset.data[dataIndex] ?? 0
                    );
                    const total = dataForDataIndex.reduce((s, x)=>s+x)
                    // the index of the dataset that contains the last point (at dataIndex)
                    // with the same sign as the total - that is the one that should carry
                    // the total label
                    const datasetIndexLast = dataForDataIndex.findLastIndex(x => x * total > 0);
                    context.total = total;
                    return datasetIndexLast === datasetIndex ? total.toFixed(2) : null
                },
                anchor: (context) => {
                    return context.total > 0 ? 'end' : 'start'
                },
                align: (context) => {
                    return context.total > 0 ? 'top' : 'bottom';
                },

                clip: false
            }
        }
    }
});
<div style="height:500px">
    <canvas id="chart1"></canvas>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/Chart.js/4.3.0/chart.umd.js"
        integrity="sha512-CMF3tQtjOoOJoOKlsS7/2loJlkyctwzSoDK/S40iAB+MqWSaf50uObGQSk5Ny/gfRhRCjNLvoxuCvdnERU4WGg=="
        crossOrigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/chartjs-plugin-datalabels/2.2.0/chartjs-plugin-datalabels.min.js" integrity="sha512-JPcRR8yFa8mmCsfrw4TNte1ZvF1e3+1SdGMslZvmrzDYxS69J7J49vkFL8u6u8PlPJK+H3voElBtUCzaXj+6ig==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>

The idea behind this is to have datalabels enabled for all bars from the dataset, but choose the one that is correctly positioned to represent the sum from the formatter - namely the last dataset whose value (for the current dataIndex) has the same sign as the sum. Things might be different if the order option were used.

The code also uses the fact that the context object is shared, for the same datasetIndex, between the formatter, align and anchor methods, formatter being always the first -- if formatter returns null the others are not even called. Thus, it saves the sum for the current datasetIndex in the context object to be used by the only one point (per datasetIndex) for which align and anchor are called. For a safer solution, that doesn't use this undocumented fact, one may recompute the sum in align and anchor, or cache the sum using an external object, which can be a solution to optimise the computation by only computing the sum once, for the first time the formatter is called for a dataset.

like image 63
kikon Avatar answered Sep 18 '25 19:09

kikon