需要画一张折线图时,直觉是先找一个图表库。但如果需求只有"把一组数据画成线",引入一个几百 KB 的库,意味着你要接受它的默认样式、它的配置体系,以及它带来的加载体积。
先把坐标映射写对
画图的核心只有一件事:把数据值映射到画布像素。这一步写错,后面怎么调样式都是白费。
// 数据值 → 像素坐标
const x = i => pad + (i / (data.length - 1)) * (W - pad * 2);
const y = v => H - pad - ((v - min) / (max - min || 1)) * (H - pad * 2);
注意 max - min || 1:当所有数据点相同时分母会是 0,映射结果直接变成 NaN,整张图消失。这种边界情况在真实数据里很常见,必须处理。
devicePixelRatio 不做处理,图一定是糊的
在 Retina 或高 DPI 屏上,把 canvas 的 CSS 尺寸直接当画布尺寸用,线条会发虚。正确做法是把画布实际像素放大 dpr 倍,再用 CSS 把它缩回去:
const dpr = window.devicePixelRatio || 1;
canvas.width = W * dpr;
canvas.height = H * dpr;
canvas.style.width = W + 'px';
canvas.style.height = H + 'px';
const ctx = canvas.getContext('2d');
ctx.scale(dpr, dpr);
三种常见图形的基本画法
- 折线:
beginPath()后逐点lineTo(),一次性stroke()。不要每个点都 stroke 一次,否则线宽在拐角处会叠出一层毛刺。 - 柱状:
fillRect()就够。柱宽按可用宽度 / 数据点数量算,再留一点间隙。 - 热力图:本质是双层循环里的
fillRect(),颜色按数值在两端色之间插值。数据量不大时性能完全够用。
直接标注,比画坐标轴划算
自己实现坐标轴、刻度、网格线,是这类"手写图表"最费时间的部分,收益却很低。如果数据点不多,把数值直接写在折线末端或柱子顶部,读者反而更快获取信息——省掉了眼睛在轴和图元之间的来回移动。
必须做的一个降级处理
Canvas 里的内容对屏幕阅读器和搜索引擎都是不可见的。至少要给 <canvas> 加上替代说明,并把关键数值用文本形式列在图表下方。这既是无障碍要求,也避免图挂了之后页面变成一片空白。
什么时候该换成成熟库
出现下面任一情况,别再自己写了:需要缩放、框选、十字准线等交互;数据点上万需要增量渲染和抽稀;需要堆叠、桑基、力导等复杂图型;团队里其他人也要维护这些图。省下的几百 KB 换不来这些功能的开发与调试成本。
判断标准可以简化成一句:只是展示一组趋势,就手写;需要让人和图表交互,就用库。