图表库源码剖析 - Chart.js 最流行的 Canvas 图表库

3,356 阅读4分钟

原发于知乎: zhuanlan.zhihu.com/p/32740553

引言, 为什么想要研究 Chartjs

继之前我们研究了SVG.js 和 Frappe Charts 后, 我们对于 svg 的图表库已经有了初步的了解, 但是对于可视化世界的 canvas, 我们更应该投入精力去了解学习.
在看到 chartist.js 讲到自己的优势的时候, 提到一些图表库使用了错误的技术 canvas, 那我们就更有兴趣去了解, 为什么会有这种说法. 首先让我们一起来了解一下 Chartjs.

Chartjs 介绍

Chartjs 的官方介绍是一个简单灵活的图表库, 相对而言 Chartjs 在图表库中的优势, 主要是配置简单, 动画比较优雅, 而基于 canvas 的特性, 让 Chartjs 性能会更有优势. Chartjs 目前拥有 34.4 K的 star, 几乎已经是 canvas 版本的图表代名词, 也是最流行的基于 canvas 的图表库. 在 GitHub 上搜索 chart, 可以看到 Chartjs 的流行度排名仅次于 D3.
先来看一下 Chartjs 如何使用吧?
<canvas id="myChart"></canvas> 

var ctx = document.getElementById('myChart').getContext("2d");
var myChart = new Chart(ctx, {
  type: 'line',
  data: {
    labels: ["JAN", "FEB", "MAR", "APR", "MAY", "JUN", "JUL"],
    datasets: [{
      label: "Data",
      borderColor: "#80b6f4",
      fill: false,
      data: [50, 120, 150, 170, 180, 170, 160]
    }]
  }
});
得到如图的趋势图

Chart.js 代码组织方式

Chart.js 图表创建过程

从源文件的 core.controller.js 分析, 来看 Chartjs 初始化图表的过程如下:
左侧为触发的插件机制的事件.

Chartjs 插件机制

Chartjs 的插件机制看起来很简单, 但是也很有效. 插件直接注册到 plugin 里面, 拥有全部的执行的生命周期, 而且可以直接访问 Chart 的全局变量, 拥有所有 API 的访问权限.
Chart.plugins.register({
    beforeInit() {}
    afterInit() {}
    afterUpdate() {}
    afterLayout() {}
    afterDatasetsUpdate() {}
    afterDatasetUpdate() {}
    afterRender() {}
    afterDraw() {}
    afterDatasetsDraw() {}
    afterDatasetDraw() {}
    afterEvent() {}
    resize() {}
    destroy() {}
});

Chartjs 鼠标事件和动画

对于 canvas 类型的图表而言, 处理相应的鼠标时间一直是件比较麻烦的事情. 让我们来看下 Chartjs 是怎么做的吧? 从源文件的core.controller.js 文件的 handleEvent 可以看出, Chartjs 在根元素位置, 监听对应的鼠标事件, 然后通过之前记录的元素位置, 找到最近的对应元素, 响应对应的事件.
if (e.type === 'mouseout') {
  me.active = [];
} else {
  me.active = me.getElementsAtEventForMode(e, hoverOptions.mode, hoverOptions);
}
...
me.updateHoverStyle(me.active, hoverOptions.mode, true);

从代码中可以看出, Chartjs 是通过 getElementsAtEventForMode 方法去获取对应的元素. Chartjs 提供了 6 种模式, 来响应交互. 我们一起来看一下这六种模式.
  • point: 找到 鼠标位置相交的 对应元素
  • nearest: 找到对应距离最近的元素
  • index: 根据位置, 找到不同数据集中 对应 index 的数据
  • dataset: 根据位置, 找到只在同一数据集的元素
  • x: 只根据鼠标位置的 x 轴值, 找到与 x 轴值相交的元素, 适应于垂直光标的场景
  • y: 只根据鼠标位置的 y 轴值, 找到与 y 轴值相交的元素, 适应于垂直光标的场景
而对于动画, 在core.animation.js文件的实现了对于 animation 的堆栈, 针对动画依次使用 requestAnimationFrame 进行动画的调用. 动画中也内置了常见的各种缓动函数, 用于常见的动画效果. 我们可以看一下动画的核心实现, 里面的 advance 方法.
while (i < animations.length) {
  animation = animations[i];
  chart = animation.chart;
  animation.currentStep = (animation.currentStep || 0) + count;
  animation.currentStep = Math.min(animation.currentStep, animation.numSteps);
  helpers.callback(animation.render, [chart, animation], chart);
  helpers.callback(animation.onAnimationProgress, [animation], chart);
  if (animation.currentStep >= animation.numSteps) {
    helpers.callback(animation.onAnimationComplete, [animation], chart);
    chart.animating = false;
    animations.splice(i, 1);
  } else {
    ++i;
  }
}
上述代码中的, animation.render 根据动画中的当前动画的进度, 来绘制出动画所涉及元素的中间状态.

Chartjs 浮点数问题

在阅读 Chartjs 源码的过程中, 发现源码部分没有针对浮点数问题做任何处理, 很多地方也都没有考虑过浮点数问题. 所以 Chartjs 在使用过程中可能会有如下的问题:

One more thing

在使用很多通用图表的时候, 相信大家都会遇到通用图表的定制化困难这种问题, 下期我们将分析一下可视化图形语法G2, 看下 G2 是怎么实现对于图表的高度的易用性和扩展性.
在看 Chartjs源码的同时, 我们也动手用 canvas 实践了一些基础图表, 具体可以参见 Taco.
如果想来和我们一起研究可视化,欢迎投递简历 linhui.wlh@alibaba-inc.com