You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在SPFx React的Chart.js饼图中给数据标签、图例加图片与边框

在SPFx React项目的Chart.js饼图中添加图标到数据标签和图例(含图例图标边框)

我已创建SPFx React项目并集成了chart.js@3.3.2,现需实现以下需求:

  • 在饼图的数据标签和图例中添加图片/图标
  • 给图例中的图片添加外边框

以下是当前代码片段:

<Chart type="pie" data={chartData} options={chartOptions} plugins={pluginsForShowDataOnPieChart} />

const iconImageArray = [
  'https://primefaces.org/cdn/primereact/images/galleria/galleria1.jpg',
  'https://primefaces.org/cdn/primereact/images/galleria/galleria2.jpg',
  'https://primefaces.org/cdn/primereact/images/galleria/galleria3.jpg',
];

const labelsNameArray = ["Label 1","Label 2","Label 3"]

const pieChartData = () => {
        const documentStyle = getComputedStyle(document.documentElement);
        const data = {
            labels: labelsNameArray,
            datasets: [
                {
                    data: percentageDataArray,
                    backgroundColor: [
                        '#5066BB',
                        '#6FC66E',
                        '#ECAD68',
                        '#EF876C',
                        documentStyle.getPropertyValue('--cyan-500'),
                        documentStyle.getPropertyValue('--teal-500'),
                        documentStyle.getPropertyValue('--pink-500'),
                    ],
                    hoverBackgroundColor: [
                        '#5066BB',
                        '#6FC66E',
                        '#ECAD68',
                        '#EF876C',
                        documentStyle.getPropertyValue('--cyan-500'),
                        documentStyle.getPropertyValue('--teal-500'),
                        documentStyle.getPropertyValue('--pink-500'),
                    ],
                    toggledVisibility: Array(productArray.length).fill(true)
                }
            ]
        };

        const options = {
            aspectRatio: 2,
            plugins: {
                legend: {
                    position: 'right',
                    labels: {
                        usePointStyle: true,
                        padding: 20,
                        generateLabels: (chart) => {
                            const dataset = chart.data.datasets[0];
                            return dataset.data.map((data, i) => {
                                const img = new Image();
                                img.src = iconImageArray[i];
                                img.height = 10;
                                img.width = 10;
                                img.style.border = '1px solid';
                                img.style.borderColor = legendColor[i];
                                return {
                                    text: chart.data.labels[i],
                                    fillStyle: dataset.backgroundColor[i],
                                    hidden: chart.getDatasetMeta(0).data[i].hidden,
                                    index: i,
                                    fontColor: dataset.toggledVisibility[i] ? '#495057' : '#888888',
                                    textDecoration: dataset.toggledVisibility[i] ? 'none' : 'line-through',
                                    pointStyle: img
                                };
                            });
                        }
                    },
                    onClick: function (event, legendItem, legend) {
                        const ci = legend.chart;
                        const index = legendItem.index;
                        const meta = ci.getDatasetMeta(0);
                        meta.data[index].hidden = !meta.data[index].hidden;
                        const dataset = ci.data.datasets[0];
                        dataset.toggledVisibility[index] = !dataset.toggledVisibility[index];
                        ci.update();
                    }
                },
                tooltip: {
                    callbacks: {
                        label: (tooltipItem) => {
                            const countValue = productArray[tooltipItem.dataIndex]?.count || 0;
                            const renewCountValue = productArray[tooltipItem.dataIndex]?.renewCount || 0;
                            return `${tooltipItem.label}: ${countValue} | ${renewCountValue}`;
                        },
                    },
                },
                datalabels: {
                    formatter: (value) => `${value}%`,
                    color: '#fff',
                    anchor: 'end',
                    align: 'start',
                    offset: -10,
                    font: {
                        weight: 'bold',
                    },
                },
            },
        };

        setChartData(data);
        setChartOptions(options);
    };

// Plugin to display percentage inside the Pie Chart
const pluginsForShowDataOnPieChart = [
    {
        afterDraw: (chart) => {
            const { ctx } = chart;
            chart.data.datasets.forEach((dataset, i) => {
                const numericData = dataset.data.map(value => parseFloat(value) || 0);
                const total = numericData.reduce((sum, value) => sum + value, 0);

                chart.getDatasetMeta(i).data.forEach((datapoint, index) => {
                    if (dataset.toggledVisibility[index] && dataset.data[index] != 0) {
                        if (numericData[index] !== 0) {
                            const { x, y } = datapoint.tooltipPosition();
                            const percentage = ((numericData[index] / total) * 100).toFixed(1);

                            ctx.fillStyle = '#495057';
                            ctx.font = 'bold 12px Open Sans';
                            ctx.textAlign = 'center';
                            ctx.textBaseline = 'middle';

                            ctx.fillText(`${percentage}%`, x, y + 7);
                        }
                    }
                });
            });
        },
        beforeDraw: (chart) => {
            const { ctx } = chart;
            ctx.restore();
            ctx.fillStyle = '#495057';
            ctx.font = 'bold 12px Open Sans';
            ctx.textAlign = 'center';
            ctx.textBaseline = 'middle';

            let total = 0;
            let isTotalVisible = false;

            if (chart.data.datasets[0] && chart.data.datasets[0].data) {
                total = chart.data.datasets[0].data.reduce((sum, value) => sum + value, 0);
                chart.data.datasets.forEach((dataset, i) => {
                    chart.getDatasetMeta(i).data.forEach((datapoint, index) => {
                        if (dataset.data[index] !== 0) {
                            isTotalVisible = true;
                            return;
                        }
                    });
                });
            }

            ctx.save();
        }
    }
];

解决方案

1. 修复图例图标边框(Canvas绘制适配)

原代码中直接给Image元素设置style.border不会生效,因为Chart.js通过Canvas绘制内容而非DOM元素。需通过自定义pointStyle函数手动绘制带边框的图标:

修改options.plugins.legend.labels.generateLabels部分:

generateLabels: (chart) => {
  const dataset = chart.data.datasets[0];
  return dataset.data.map((data, i) => {
    const img = new Image();
    img.src = iconImageArray[i];
    img.width = 16;
    img.height = 16;

    return {
      text: chart.data.labels[i],
      fillStyle: dataset.backgroundColor[i],
      hidden: chart.getDatasetMeta(0).data[i].hidden,
      index: i,
      fontColor: dataset.toggledVisibility[i] ? '#495057' : '#888888',
      textDecoration: dataset.toggledVisibility[i] ? 'none' : 'line-through',
      // 自定义绘制带边框的图标
      pointStyle: (context) => {
        const { ctx, x, y } = context;
        // 绘制边框
        ctx.strokeStyle = legendColor[i] || '#000';
        ctx.lineWidth = 1;
        ctx.strokeRect(x - 8, y - 8, 16, 16);
        // 绘制图标
        if (img.complete) {
          ctx.drawImage(img, x - 8, y - 8, 16, 16);
        } else {
          img.onload = () => {
            ctx.drawImage(img, x - 8, y - 8, 16, 16);
            chart.draw();
          };
        }
      }
    };
  });
}

2. 数据标签添加图标

在饼图内部的百分比标签旁添加图标,需修改pluginsForShowDataOnPieChart的afterDraw方法:

afterDraw: (chart) => {
  const { ctx } = chart;
  chart.data.datasets.forEach((dataset, i) => {
    const numericData = dataset.data.map(value => parseFloat(value) || 0);
    const total = numericData.reduce((sum, value) => sum + value, 0);

    chart.getDatasetMeta(i).data.forEach((datapoint, index) => {
      if (dataset.toggledVisibility[index] && dataset.data[index] != 0 && numericData[index] !== 0) {
        const { x, y } = datapoint.tooltipPosition();
        const percentage = ((numericData[index] / total) * 100).toFixed(1);
        const img = new Image();
        img.src = iconImageArray[index];
        img.width = 12;
        img.height = 12;

        // 绘制图标(放在百分比左侧)
        if (img.complete) {
          ctx.drawImage(img, x - 20, y + 7, 12, 12);
        } else {
          img.onload = () => {
            ctx.drawImage(img, x - 20, y + 7, 12, 12);
            chart.draw();
          };
        }

        // 绘制百分比文本
        ctx.fillStyle = '#495057';
        ctx.font = 'bold 12px Open Sans';
        ctx.textAlign = 'center';
        ctx.textBaseline = 'middle';
        ctx.fillText(`${percentage}%`, x, y + 7);
      }
    });
  });
}

额外优化建议

  • 预加载图标:避免图片加载延迟导致绘制异常,在组件挂载时提前加载所有图标:
useEffect(() => {
  iconImageArray.forEach(src => {
    const img = new Image();
    img.src = src;
  });
}, []);
  • 使用本地图标:SPFx项目建议将图标放在src/assets目录下,使用相对路径避免跨域问题。

内容的提问来源于stack exchange,提问作者Bhoomesh Joshi

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.13 16:42:01