如何在Chart.js中将正值放在堆叠条形图的顶部,将负值放在底部。

huangapple go评论70阅读模式
英文:

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

问题

"formatter" 函数会给出每组堆叠条的总和,我试图在总和为正时将 "datalabels" 定位在上方,总和为负时将其定位在下方。请帮忙,非常感谢。

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

如何在Chart.js中将正值放在堆叠条形图的顶部,将负值放在底部。


<details>
<summary>英文:</summary>

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][1]][1]

    datalabels: {
        anchor: (context) =&gt;{
            const anchor = [];
            let sum = ?;
            
            if(parseFloat(sum) &gt;=0){
                anchor.push(&#39;end&#39;);
            }else{
                anchor.push(&#39;start&#39;);
            }
            return anchor;
        },
        align: (context) =&gt;{
            const align = [];
            let sum = ?;
    
            if(parseFloat(sum) &gt;=0){
                align.push(&#39;end&#39;);
            }else{
                align.push(&#39;bottom&#39;);
            }
            return align;
    
        },
        formatter:(value, context) =&gt;{
            const datasetArray = [];
            context.chart.data.datasets.forEach((dataset) =&gt;{
                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 &#39;&#39;;
            }
        },
    }


  [1]: https://i.stack.imgur.com/urCEM.png

</details>


# 答案1
**得分**: 2

以下是您要翻译的内容:

"在这一点上,尚不清楚柱状图是如何堆叠的 - 有许多未知的选项。根据图像图表的视觉外观可能的组合如下:

```js
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
            }
        }
    }
});

这背后的想法是启用所有来自数据集的柱形图的数据标签,但选择正确位置的数据集来表示 formatter 中的总和 - 也就是对于当前 dataIndex 具有相同符号的最后一个数据集。如果使用 order 选项,情况可能会有所不同。

该代码还利用了 context 对象在 formatteralignanchor 方法之间的共享,对于相同的 datasetIndex,其中 formatter 总是第一个 - 如果 formatter 返回 null,其他方法甚至都不会被调用。因此,它在 context 对象中保存了当前 datasetIndex 的总和,以便仅在第一次为数据集的 formatter 调用计算总和时由 alignanchor 调用。对于更安全的解决方案,不使用这个未记录的事实,可以在 alignanchor重新计算总和,或者使用外部对象缓存总和,这可以是优化计算的解决方案,只需在数据集的 formatter 首次调用时计算总和。"

英文:

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:

<!-- begin snippet: js hide: false console: false babel: false -->

<!-- language: lang-js -->

const N = 10;
const dataGenerator = (plus = 600, minus = 400, pNull = 1) =&gt;
    Array.from({length: N}, ()=&gt;Math.random() &lt; pNull ?
        Math.round(Math.random()*(plus+minus)-minus)/4 : null)
const ctx1 = document.getElementById(&#39;chart1&#39;);
new Chart(ctx1, {
    type: &quot;bar&quot;,
    plugins: [ChartDataLabels],
    data: {
        labels: Array.from({length: N}, (_, i)=&gt;&#39;l&#39;+(i+1)),
        datasets: [
            {
                data: dataGenerator(),
                stack: &#39;a&#39;,
            },
            {
                data: dataGenerator() ,
                stack: &#39;a&#39;,
            },
            {
                data: dataGenerator(100, 50, 0.5),
                stack: &#39;a&#39;,
            },
        ]
    },
    options: {
        indexAxis: &#39;x&#39;,
        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) =&gt; {
                    const {dataIndex, datasetIndex, chart} = context;
                    const dataForDataIndex = chart.data.datasets.map(
                        dataset=&gt;dataset.data[dataIndex] ?? 0
                    );
                    const total = dataForDataIndex.reduce((s, x)=&gt;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 =&gt; x * total &gt; 0);
                    context.total = total;
                    return datasetIndexLast === datasetIndex ? total.toFixed(2) : null
                },
                anchor: (context) =&gt; {
                    return context.total &gt; 0 ? &#39;end&#39; : &#39;start&#39;
                },
                align: (context) =&gt; {
                    return context.total &gt; 0 ? &#39;top&#39; : &#39;bottom&#39;;
                },

                clip: false
            }
        }
    }
});

<!-- language: lang-html -->

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

<!-- end snippet -->

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.

答案2

得分: 0

你可以像这样使用 chartjs-plugin-datalabels,如下所示:

Chart.defaults.set('plugins.datalabels', {
  color: '#FE777B'
});

var chart = new Chart(ctx, {
  options: {
    plugins: {
      // 修改此图表的所有标签的选项
      datalabels: {
        color: '#36A2EB',
        anchor: () => /*在此处添加您的条件*/
      }
    }
  },
  data: {
    datasets: [{
      // 仅为此数据集的标签更改选项
      datalabels: {
        color: '#FFCE56'
      }
    }]
  }
});

还可以查看 positioning 属性。

英文:

You can use the chartjs-plugin-datalabels as shown here.

Chart.defaults.set(&#39;plugins.datalabels&#39;, {
  color: &#39;#FE777B&#39;
});

var chart = new Chart(ctx, {
  options: {
    plugins: {
      // Change options for ALL labels of THIS CHART
      datalabels: {
        color: &#39;#36A2EB&#39;,
        anchor: ()=&gt;/*your condition here*/
      }
    }
  },
  data: {
    datasets: [{
      // Change options only for labels of THIS DATASET
      datalabels: {
        color: &#39;#FFCE56&#39;
      }
    }]
  }
});

Also checkout the positioning property.

huangapple
  • 本文由 发表于 2023年6月26日 05:53:41
  • 转载请务必保留本文链接:https://go.coder-hub.com/76552557.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定