Chart.js 插件实战:为 Doughnut 图表实现空状态(Empty State)绘制
2026/9/18 16:49:12 网站建设 项目流程

Chart.js 插件实战:为 Doughnut 图表实现空状态(Empty State)绘制

【免费下载链接】Chart.jsSimple HTML5 Charts using thetag项目地址: https://gitcode.com/gh_mirrors/ch/Chart.js

导读

当一个 doughnut(环形)图表的数据集为空时,画布上只会呈现一个空心的圆环轮廓,缺乏任何提示信息,用户很容易误以为页面加载失败或图表异常。本文基于 Chart.js 官方示例仓库中的 doughnut-empty-state.md 示例,讲解如何通过自定义插件(Plugin)在afterDraw阶段检测空数据并绘制醒目的空状态指示圆环。读完本文,你将掌握 Chart.js 插件的基本结构、afterDraw钩子的触发时机与底层调用链,以及如何让一个插件同时处理数据检测与画布绘制,并据此扩展出自己的图表空状态方案。

示例要解决的问题与核心思路

该示例的核心场景是:doughnut 图表在没有任何数据点(data数组为空)时,通过一个 id 为emptyDoughnut的插件,在图表绘制完成后用 Canvas API 在图表区域中心画一个橙色半透明的圆环,作为"空状态"的视觉提示;一旦数据被填充(例如点击示例中的 Randomize 按钮),空状态圆环立即消失,恢复正常渲染。

这个思路之所以可行,是因为 Chart.js 的插件体系允许在任何图表实例上挂载自定义插件,并通过官方定义的钩子函数(Hook)介入图表的生命周期。空状态绘制属于"绘制之后"的行为,因此示例选用的是afterDraw钩子。

完整示例代码与逐块解析

示例整体以js chart-editor代码块形式存在于 docs/samples/plugins/doughnut-empty-state.md 中,并通过module.exports导出actionsconfig,供官方 Sample Editor 直接运行和交互。下面按块拆解。

数据块:初始化空数据

// <block:data:2> const data = { labels: [], datasets: [ { label: 'Dataset 1', data: [] } ] }; // </block:data>

初始状态下labelsdata均为空数组,这正是为了演示"没有数据"时的空状态效果。关于labels与数据结构的约定,可参考 Data structures 文档;doughnut/pie 图表的data应为数值数组,Chart.js 会汇总所有数值并计算每段弧的比例,具体约定见 Doughnut and Pie Charts 的 Data Structure 一节。

插件块:空状态检测与绘制

// <block:plugin:1> const plugin = { id: 'emptyDoughnut', afterDraw(chart, args, options) { const {datasets} = chart.data; const {color, width, radiusDecrease} = options; let hasData = false; for (let i = 0; i < datasets.length; i += 1) { const dataset = datasets[i]; hasData |= dataset.data.length > 0; } if (!hasData) { const {chartArea: {left, top, right, bottom}, ctx} = chart; const centerX = (left + right) / 2; const centerY = (top + bottom) / 2; const r = Math.min(right - left, bottom - top) / 2; ctx.beginPath(); ctx.lineWidth = width || 2; ctx.strokeStyle = color || 'rgba(255, 128, 0, 0.5)'; ctx.arc(centerX, centerY, (r - radiusDecrease || 0), 0, 2 * Math.PI); ctx.stroke(); } } }; // </block:plugin>

这个插件对象只包含两个关键要素:

  1. id:插件必须定义一个全局唯一的 id 才能被配置。Chart.js 对插件 id 有命名约束(不能以点或下划线开头、不能含大写字母、不能含非 URL 安全字符),详见 Plugins 文档的 Plugin ID 一节。这里的emptyDoughnut即用于在options.plugins.emptyDoughnut下挂载该插件的配置项。
  2. afterDraw(chart, args, options):这是插件绘制阶段的钩子函数。chart是当前图表实例(内含datachartAreactx等),options是解析后的插件配置对象。

绘制逻辑分两步:

  • 空数据检测:遍历chart.data.datasets,只要任一数据集的data数组长度大于 0,就认为有数据。这里的hasData |= ...是位或赋值写法,等价于"任一数据集非空即为真",即便后续数据集为空也能保持hasDatatrue
  • 绘制空状态圆环:当hasDatafalse时,从chart.chartArea取出绘图区域的四边坐标,计算中心点与半径,然后使用ctx.arc画一个完整圆(02 * Math.PI)并用ctx.stroke描边。

配置块:挂载插件并传入选项

// <block:config:0> const config = { type: 'doughnut', data: data, options: { plugins: { emptyDoughnut: { color: 'rgba(255, 128, 0, 0.5)', width: 2, radiusDecrease: 20 } } }, plugins: [plugin] }; // </block:config>

这里展示了插件使用的两个关键机制:

  • 按图表实例挂载config.plugins数组把plugin实例绑定到当前图表。Chart.js 支持在多个图表间共享同一个插件对象、在图表配置中内联定义插件,以及通过Chart.register()注册全局插件,详见 Plugins 的 Using plugins 与 Global plugins 两节。
  • 按插件 id 隔离配置options.plugins.emptyDoughnut下存放该插件的配置项。Chart.js 会把这些选项与插件默认值合并后作为afterDraw的第三个参数options传入。若某图表希望禁用全局插件,可将对应 id 的配置设为false,或将整个options.plugins设为false

动作块:随机填充数据以验证状态切换

const actions = [ { name: 'Randomize', handler(chart) { chart.data.datasets.forEach(dataset => { dataset.data = Utils.points(NUMBER_CFG); }); chart.update(); } }, ]; module.exports = { actions, config, };

Sample Editor 会渲染一个 "Randomize" 按钮,点击后调用handler(chart):用Utils.points(NUMBER_CFG)生成一组随机数值填入每个数据集,随后调用chart.update()触发重绘。由于此时hasData变为true,插件不再绘制空状态圆环。这一步直观验证了插件"有数据即隐藏、无数据即显示"的动态行为。

参数说明:插件的三个可配置项

参数名类型示例默认值作用
colorstring(CSS 颜色值)'rgba(255, 128, 0, 0.5)'空状态圆环的描边颜色;未传入时插件内会回退到'rgba(255, 128, 0, 0.5)'
widthnumber(像素)2描边线宽,直接赋给ctx.lineWidth;未传入时回退为2
radiusDecreasenumber(像素)20从计算半径中减去的像素数,使空状态圆环比图表区域略小、留出内缩边距

插件代码中的回退逻辑width || 2color || 'rgba(255, 128, 0, 0.5)'r - radiusDecrease || 0表明这些配置都是可选的。更规范的做法是在插件对象中提供defaults字段声明默认值(参见 Plugins 的 Plugin defaults 一节),示例代码则选择了在钩子内部做兜底处理。

从源码看afterDraw的触发时机

示例选用afterDraw而非beforeDrawbeforeDatasetsDraw,是因为空状态圆环需要绘制在所有图表内容之上,语义上属于"图表演示完成后的收尾绘制"。

从源码可以确认这一时序。在 core.controller.js 的_draw流程中:

  • 先通过this.notifyPlugins('beforeDraw', {cancelable: true})发出beforeDraw(若插件返回false可取消后续绘制);
  • 随后按层级绘制图层并调用this._drawDatasets()绘制各数据集;
  • 最后在 core.controller.js 处调用this.notifyPlugins('afterDraw')

也就是说,afterDraw在所有数据集和图层绘制完成后、一次帧渲染的末尾被触发,此时画布上下文ctx上已叠加了完整图表内容,非常适合追加"空状态提示"这类覆盖层。而在 core.plugins.js 中,notify会遍历图表关联的插件描述符并逐一调用对应钩子,_notify内部还会处理钩子返回值为false时的中断(仅对可取消钩子生效),这正是插件机制能够影响绘制流程的底层实现。

半径与中心点计算:与 doughnut 图表配置的关系

示例中空状态圆环的几何参数完全来自chart.chartArea(即绘图区域,不含标题、图例与轴标签等周边布局),而非整个画布:

  • 中心点:(left + right) / 2(top + bottom) / 2
  • 半径:Math.min(right - left, bottom - top) / 2,取宽高较小者的一半,保证圆环始终完整内切于图表区域;
  • 实际半径:r - radiusDecrease,即默认再向内收缩 20 像素。

由于 doughnut 图表默认的cutout'50%'(见 controller.doughnut.js 中的默认配置:cutout: '50%'circumference: 360radius: '100%'),正常有数据时图表中央本就会有一个被挖空的内圈。空状态圆环落在内圈附近时,与既有 doughnut 视觉语言(一个环形)天然协调,这正是示例选择"画圆环"而非"画文本或矩形"作为空状态提示的原因——它延续了 doughnut 图表本身的环形外观。若数据量较大、图表中心被占用,也可通过调整radiusDecrease或改用ctx.fillText绘制提示文字来扩展该方案。

如何在自己的项目中复用该插件

将该示例落地到实际项目只需四步:

  1. plugin对象(或稍作改造的版本)复制到你的代码中;
  2. 在图表配置的plugins: [plugin]数组中挂载它(若希望所有图表生效,可改用Chart.register(plugin)全局注册);
  3. 按需在options.plugins.emptyDoughnut中配置colorwidthradiusDecrease
  4. 通过chart.data.datasets[i].data的增删与chart.update()调用触发状态切换,插件会自动响应。

注意事项:示例中的afterDraw只检查dataset.data.length > 0,属于最简判定。在真实项目中,你可能还需要考虑数据全为null/undefined、数据被parsing解析后无有效数值等边界情况(doughnut 数据解析约定见 Doughnut and Pie Charts 与 Data structures),此时应在循环中补充更严格的"有效数据点"判断。

总结

doughnut-empty-state示例虽小,却完整展示了 Chart.js 插件开发的三个核心要点:用唯一的id定义可配置插件、用afterDraw钩子介入绘制流程收尾阶段、用chart.chartAreactx直接操作 Canvas 完成自定义绘制。它的实现同时印证了 core.controller.js 中notifyPlugins('afterDraw')的调用时序,以及插件选项按options.plugins.{plugin-id}作用域解析的机制。掌握了这个模式,你不仅可以做"空状态提示",还能举一反三地实现图表区域边框(参见同目录下的 chart-area-border.md 示例)、自定义标注、水印等一切"绘制在图表之上"的需求。

延伸阅读

  • Plugins(插件开发指南)
  • Doughnut and Pie Charts(环形/饼图图表参考)
  • Data structures(数据结构约定)
  • Chart Area Border(同类插件示例:绘制图表区域边框)

【免费下载链接】Chart.jsSimple HTML5 Charts using thetag项目地址: https://gitcode.com/gh_mirrors/ch/Chart.js

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询