在悬停时自定义 ChartJS 甜甜圈边框

问题描述 投票:0回答:1

我的设计师使用 Figma 创建了以下原型。

我的堆栈是:

  • Vuejs
  • 引导程序
  • 图表JS

所以我的问题是我们可以在点击时在某些部分添加此自定义边框吗?

目标: 添加不同颜色的“自定义边框”并仅启用 Top 边框

我做了我的研究,但仍然没有达到确切的期望:

  • 添加第二层图表(X),它创建完整的图表而不是数据的大小
  • 在悬停时添加边框,但问题是我找不到一种方法来设置仅“顶部”边框处于活动状态。

预期结果 enter image description here

我的关闭结果codepen enter image description here

    <!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Chart.js Doughnut Chart with Clickable Legend</title>
    <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
    <style>
        /* Limit the size of the chart */
        #chart-container {
            width: 500px;
            height: 500px;
            margin: auto; /* Center the chart horizontally */
        }

        canvas {
            display: block;
        }
    </style>
</head>
<body>
    <div id="chart-container">
        <canvas id="myDoughnutChart"></canvas>
    </div>
    <script>
        const ctx = document.getElementById('myDoughnutChart').getContext('2d');

        const myDoughnutChart = new Chart(ctx, {
            type: 'doughnut',
            data: {
                labels: ['Green', 'Yellow', 'Orange', 'Purple', 'Blue', 'Pink'],
                datasets: [{
                    data: [25, 25, 25, 25, 25, 30],
                    backgroundColor: [
                        'rgb(71,159,182)',
                        'rgb(248,210,103)',
                        'rgb(238,129,48)',
                        'rgb(66,44,142)',
                        'rgb(41,99,246)',
                        'rgb(221,84,112)'
                    ],
                    borderColor: [
                        'rgb(71,159,182)',
                        'rgb(248,210,103)',
                        'rgb(238,129,48)',
                        'rgb(66,44,142)',
                        'rgb(41,99,246)',
                        'rgb(221,84,112)'
                    ],
                    borderWidth: Array(6).fill(5),
                    borderAlign: 'outer'
                }]
            },
            options: {
                responsive: true,
                plugins: {
                    legend: {
                        position: 'right',
                        onClick: function(e, legendItem, legend) {
                            const dataset = legend.chart.data.datasets[0];
                            const index = legendItem.index;

                            // Reset all borders to 5px
                            dataset.borderWidth = dataset.borderWidth.map(() => 5);
                            // dataset.borderColor = dataset.borderColor.map(() => '');
                          

                            // Set the border width of the selected segment to 10px
                            dataset.borderWidth[index] = 20;
                            // dataset.borderAlign[index] = 'outer';

                            // Update the chart to apply the changes
                            legend.chart.update();
                        }
                    }
                }
            }
        });
    </script>
</body>
</html>

chart.js vue-chartjs
1个回答
0
投票

可以使用自定义插件绘制自定义边框。这 hook 用于绘图 绘制其他元素后在画布上是

afterDraw
。插件可以有一个状态,存储在其选项中。如何检索插件代码中的选项以及如何/在哪里设置它们在图表配置对象或其他地方都有详细记录,并包含在许多类似的小插件中,这些插件包含在 SO 的答案中。

我假设,根据您的代码,您希望通过单击图例来激活自定义边框;在下面的代码片段中,后续单击将禁用边框。

new Chart('myDoughnutChart', {
    type: 'doughnut',
    data: {
        labels: ['Green', 'Yellow', 'Orange', 'Purple', 'Blue', 'Pink'],
        datasets: [{
            data: [25, 25, 25, 25, 25, 30],
            backgroundColor: [
                'rgb(71,159,182)',
                'rgb(248,210,103)',
                'rgb(238,129,48)',
                'rgb(66,44,142)',
                'rgb(41,99,246)',
                'rgb(221,84,112)'
            ],
            borderColor: [
                'rgb(71,159,182)',
                'rgb(248,210,103)',
                'rgb(238,129,48)',
                'rgb(66,44,142)',
                'rgb(41,99,246)',
                'rgb(221,84,112)'
            ],
            borderWidth: 0,
            hoverBorderWidth: 0,
            borderAlign: 'outer'
        }]
    },
    options: {
        responsive: true,
        radius: "95%", // use to create space between the doughnut and the legend
        plugins: {
            // "custom-border": {
            //     color: '#8fa',   // default '#48f', in plugin code, indexable
            //     size: 20         // default 10, in plugin code, indexable
            // },
            legend: {
                position: 'right',
                onClick: function({chart}, {index}) {
                    chart.options.plugins["custom-border"].on[index] = !chart.options.plugins["custom-border"].on[index];
                    chart.update('none');
                }
            }
        }
    },
    plugins:[
        {
            id: "custom-border",
            beforeInit(chart){
                if(!chart.options.plugins['custom-border']){
                    chart.options.plugins['custom-border'] = {};
                }
                if(!chart.options.plugins['custom-border'].on || chart.options.plugins['custom-border'].on.length === 0){
                    chart.options.plugins['custom-border'].on = Array(chart.data.datasets[0].data.length).fill(false);
                }
            },
            afterDraw(chart, _, options){
                if(!options.on || !options.on.includes(true)){
                    return;
                }
                const meta = chart.getDatasetMeta(0);
                for(let i = 0; i < options.on.length; i++){
                    if(options.on[i]){
                        const {x, y, startAngle, endAngle, outerRadius: r} = meta.data[i];
                        const {ctx} = chart;
                        ctx.save();
                        const colorOption = chart.options.plugins['custom-border'].color;
                        const color = Array.isArray(colorOption) ? colorOption[i] : colorOption;
                        ctx.fillStyle = color || '#48f';
                        const sizeOption = chart.options.plugins['custom-border'].size;
                        const size = Array.isArray(sizeOption) ? sizeOption[i] : sizeOption;
                        const dr = size ?? 10;
                        ctx.beginPath();
                        ctx.arc(x, y, r - 1, startAngle, endAngle);
                        ctx.lineTo(x + (r + dr) * Math.cos(endAngle), y + (r + dr) * Math.sin(endAngle));
                        ctx.arc(x, y, r + dr, endAngle, startAngle, true);
                        ctx.closePath();
                        ctx.fill();
                        ctx.restore();
                    }
                }
            }
        }
    ]
});
#chart-container {
    width: 500px;
    height: 500px;
    margin: auto; /* Center the chart horizontally */
}

canvas {
    display: block;
}
<div id="chart-container">
  <canvas id="myDoughnutChart"></canvas>
</div>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

下一个片段添加了一些附加功能,上面的最小解决方案未涵盖。不过,它可能对某些人来说很有趣,因为它包含显示如何使用插件

defaults
以及如何检索插件外部的默认值的代码(请参阅
radius
回调) 以及如何更改图例颜色框的外观,在本例中指示该数据项的自定义边框已打开:

new Chart('myDoughnutChart', {
    type: 'doughnut',
    data: {
        labels: ['Green', 'Yellow', 'Orange', 'Purple', 'Blue', 'Pink'],
        datasets: [{
            data: [25, 25, 25, 25, 25, 30],
            backgroundColor: [
                'rgb(71,159,182)',
                'rgb(248,210,103)',
                'rgb(238,129,48)',
                'rgb(66,44,142)',
                'rgb(41,99,246)',
                'rgb(221,84,112)'
            ],
            borderColor: [
                'rgb(71,159,182)',
                'rgb(248,210,103)',
                'rgb(238,129,48)',
                'rgb(66,44,142)',
                'rgb(41,99,246)',
                'rgb(221,84,112)'
            ],
            borderWidth: 0,
            hoverBorderWidth: 0,
            borderAlign: 'outer'
        }]
    },
    options: {
        responsive: true,
        radius: function({chart}){
            // used to create space between the doughnut and the legend
            // might also be set manually
            const pluginCustomBorder = chart.registry.plugins.get("custom-border") ??
                chart.config.plugins.find(({id})=>id==='custom-border');
            let customBorderSize = chart.options.plugins["custom-border"].size ??
                pluginCustomBorder.defaults.size;
            if(Array.isArray(customBorderSize)){
                customBorderSize = Math.max(...customBorderSize);
            }
            customBorderSize += 5;
            const radiusFrac = 1-customBorderSize/chart.chartArea.width;
            return `${Math.floor(radiusFrac*100)}%`;
        },

        plugins: {
            // "custom-border": {
            //     color: ['#8fa', '#f8a', '#a8f', '#8fa', '#f8a', '#a8f'],   // default '#48f', in plugin code, indexable
            //     size: 15         // default 10, in plugin code, indexable
            // },
            legend: {
                position: 'right',
                labels:{
                    usePointStyle: true,
                    pointStyleWidth: 40,
                    pointStyleHeight: 12,
                    generateLabels: function(chart){
                        const labelPlugin = this;
                        if(!labelPlugin.$cachedImages){
                            labelPlugin.$cachedImages = Array(chart.data.datasets[0].data.length).fill(null);
                        }
                        const ret = Array.from({length: chart.data.datasets[0].data.length}, (_, index) => {
                            let pointStyle = 'rect';

                            if(chart.options.plugins["custom-border"].on[index]){
                                if(labelPlugin.$cachedImages[index]){
                                    pointStyle = labelPlugin.$cachedImages[index];
                                }
                                else{
                                    const canvasImg = document.createElement('canvas');
                                    const width = chart.legend.options.labels.pointStyleWidth ?? 40,
                                        height = chart.legend.options.labels.pointStyleHeight ?? 12;
                                    canvasImg.width = width;
                                    canvasImg.height = height;
                                    const ctx = canvasImg.getContext('2d');
                                    ctx.fillStyle = chart.data.datasets[0].backgroundColor[index];
                                    ctx.fillRect(0, Math.round(height/4) + 1, width, height);
                                    const colorOption = chart.options.plugins['custom-border'].color;
                                    const color = Array.isArray(colorOption) ? colorOption[index] : colorOption;
                                    ctx.fillStyle = color || '#48f';
                                    ctx.fillRect(0, 0, width, Math.round(height/4));
                                    const image = new Image(width, height);
                                    image.onload = function(){
                                        chart.update('none');
                                    }
                                    image.src = canvasImg.toDataURL("image/png").replace("image/png", "image/octet-stream");
                                    labelPlugin.$cachedImages[index] = image;
                                    pointStyle = image;
                                }
                            }
                            return {
                                text: chart.data.labels[index],
                                fillStyle: chart.data.datasets[0].backgroundColor[index],
                                strokeStyle: chart.data.datasets[0].backgroundColor[index],
                                index,
                                pointStyle
                            }
                        });
                        return ret;
                    }
                },
                onClick: function({chart}, {index}) {
                    chart.options.plugins["custom-border"].on[index] = !chart.options.plugins["custom-border"].on[index];
                    chart.update('none');
                }
            }
        }
    },
    plugins:[
        {
            id: "custom-border",
            defaults: {
                color: '#48f',
                size: 10
            },
            beforeInit(chart){
                if(!chart.options.plugins['custom-border']){
                    chart.options.plugins['custom-border'] = {};
                }
                if(!chart.options.plugins['custom-border'].on || chart.options.plugins['custom-border'].on.length === 0){
                    chart.options.plugins['custom-border'].on = Array(chart.data.datasets[0].data.length).fill(false);
                }
            },
            afterDraw(chart, _, options){
                if(!options.on || !options.on.includes(true)){
                    return;
                }
                const meta = chart.getDatasetMeta(0);
                for(let i = 0; i < options.on.length; i++){
                    if(options.on[i]){
                        const {x, y, startAngle, endAngle, outerRadius: r} = meta.data[i];
                        const {ctx} = chart;
                        ctx.save();
                        const colorOption = chart.options.plugins['custom-border'].color;
                        const color = Array.isArray(colorOption) ? colorOption[i] : colorOption;
                        ctx.fillStyle = color || this.defaults.color;
                        const sizeOption = chart.options.plugins['custom-border'].size;
                        const size = Array.isArray(sizeOption) ? sizeOption[i] : sizeOption;
                        const dr = size ?? this.defaults.size;
                        ctx.beginPath();
                        ctx.arc(x, y, r - 1, startAngle, endAngle);
                        ctx.lineTo(x + (r + dr) * Math.cos(endAngle), y + (r + dr) * Math.sin(endAngle));
                        ctx.arc(x, y, r + dr, endAngle, startAngle, true);
                        ctx.closePath();
                        ctx.fill();
                        ctx.restore();
                    }
                }
            }
        }
    ]
});
#chart-container {
    width: 500px;
    height: 500px;
    margin: auto; /* Center the chart horizontally */
}

canvas {
    display: block;
}
<div id="chart-container">
    <canvas id="myDoughnutChart"></canvas>
</div>

<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>

© www.soinside.com 2019 - 2024. All rights reserved.