我的设计师使用 Figma 创建了以下原型。
我的堆栈是:
所以我的问题是我们可以在点击时在某些部分添加此自定义边框吗?
目标: 添加不同颜色的“自定义边框”并仅启用 Top 边框
我做了我的研究,但仍然没有达到确切的期望:
我的关闭结果codepen
<!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>
可以使用自定义插件绘制自定义边框。这 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>