资讯动态

轻量JS图表组件自包含方案:基于Canvas 2D实现柱状图与饼图

发布时间:2026/10/18 9:14:48 来源:尧图企业网站定制
简介一套面向网页开发初学者的轻量JS绘图组件资源基于jscharts脚本实现柱状图、饼图、折线图等常见图表适合在数据分析、报告展示、后台统计面板中快速呈现数据。压缩包共51个文件以27个HTML示例页面为主包含12张JPG效果截图、9个XML配置以及核心JS文件、PDF说明文档和readme使用指南整体大小仅286KB体积小且便于阅读理解。目前已有172人学习下载。包内PDF文档详解调用方式与常用配置readme梳理安装步骤和常见问题sources目录则保留组件核心实现适合想深入原理或二次修改的读者。柱状图与饼图拆分为多个难度递进的实例覆盖基础蓝色、自定义红色、暖色、黑白、彩虹等主题并额外给出折线图、无网格、紧凑型等定制场景对照这些代码可以快速掌握数据绑定、样式配置、悬停交互等关键写法也能直接抽取组件嵌入自己的项目。1. 什么样的项目会需要手写绘图组件一个轻量 js chart 的自包含方案如果你在一个 js 项目里被要求「图表别引太重的依赖画个柱状图、饼图就行」事情就没那么简单。市面上的 chart 库要么体积大要么样式定制绕圈子。这套绘图组件就是冲这个场景来的不依赖任何第三方绘图工具基于 Canvas 2D 实现坐标轴、网格、柱状图、饼图及基础动画一个配置对象出一个图表实例适合统计页、数据看板这类需要稳定可控、能改样式的可视化需求。过完源码后我的判断是绘图流程被拆成数据预处理、坐标计算、图形绘制、事件交互四段每段都能单独拿出来改。适合两类人——被现成图表库定制成本折磨过的业务前端以及想完整走一遍 Canvas 绘图链路的人。下面的章节会沿架构、核心代码、运行方式、踩坑记录的顺序展开最后一章用新增折线图的例子演示怎么二次开发。2. 技术选型与整体架构Canvas 2D 打底分层解耦的组件骨架2.1 为什么不用现成图表库而是自己封装先回答一个绕不开的问题柱状图、饼图这种基础图形现成的图表库一抓一大把为什么要自己写我拆这套组件时的第一感受是它的定位非常明确——业务系统里那种「图表不要求炫酷但样式必须跟设计稿完全一致」的需求。现成库的配置项动辄上百个翻几页文档才能找到一个间距参数最后渲染出来的间距还未必是像素级精确。这里的思路是图形怎么画、坐标轴怎么排、颜色怎么配全部走自己的代码改一个变量全局生效。另一个理由是依赖体积。后台项目往往只用到两三种图为一个柱状图加一个入场动画引入整个绘图工具库首屏要多花几百 KB。这套组件的策略是零运行时依赖连动画都是自己用 requestAnimationFrame 写的加载多少用多少这对内网管理系统或者嵌入到别人框架里的场景很友好。技术上还有一个关键选择用 SVG 还是 Canvas 2D。SVG 的优势是每个图形都是 DOM 节点事件和样式天然支持但图形一多几十根柱子就是几十个节点加动画时浏览器要反复做布局和重绘容易卡顿。Canvas 2D 是「一块黑板」所有图形画在同一块画布上每帧重绘的成本可控渐变、圆角、阴影这些效果也是一两条 API 的事。代价是事件要自己做命中检测Tooltip 和 Hover 都得手动维护——这是整套组件里最花时间的一块也是阅读源码时最值得看的部分。2.2 目录结构四个目录各管一段整个包解压后是这样一个结构我按源码实际组织方式做了精简js-chart-component/ ├── src/ │ ├── index.js # 统一出口工厂函数注册 │ ├── core/ │ │ ├── Chart.js # 图表基类画布初始化、生命周期、公共尺寸 │ │ ├── Canvas.js # 画布构建与高分屏适配 │ │ └── Scale.js # 数据值 ↔ 像素坐标的比例尺、刻度计算 │ ├── charts/ │ │ ├── BarChart.js # 柱状图 │ │ └── PieChart.js # 饼图 / 环形图 │ ├── components/ │ │ ├── Axis.js # 坐标轴与网格线 │ │ ├── Legend.js # 图例 │ │ └── Tooltip.js # 悬浮提示 │ └── utils/ │ ├── data.js # 数据求和、最大值、归一化 │ └── animation.js # 缓动函数与动画帧管理 ├── examples/ │ ├── bar.html # 柱状图示例 │ ├── pie.html # 饼图示例 │ └── dashboard.html # 组合示例一屏多图表 └── README.md每个目录的职责很清晰core 处理「一个图表从数据到像素」的公共链路charts 放具体的图形实现components 放可复用的绘制件utils 放纯函数工具。我一般会特别强调一个设计约束core 里不允许出现任何跟「柱状图」或「饼图」相关的业务文案新增图表类型时只需要在 charts 目录加文件不用动核心代码。这是组件能否横向扩展的关键也是这套代码和单文件脚本的本质区别。2.3 对外接口一个配置对象进一个实例出入口文件 src/index.js 是整个组件的门面代码非常短import { BarChart } from ./charts/BarChart.js; import { PieChart } from ./charts/PieChart.js; export function createChart(container, options) { const { type bar, data [], ...rest } options || {}; if (type bar) { return new BarChart(container, { data, ...rest }); } if (type pie) { return new PieChart(container, { data, ...rest }); } throw new Error(不支持的图表类型: ${type}); } export { BarChart, PieChart };逻辑说明createChart 是一个工厂函数把「类型字符串 配置对象」转成具体的图表实例调用方不用关心内部是 BarChart 还是 PieChart后续新增折线图时只需要在 factory 里加一个分支其余代码完全不动。参数说明container 既支持 CSS 选择器字符串也支持直接传 DOM 节点options 里的 type 是图表类型data 是数据数组剩余的字段通过展开运算符原样传给具体图表类。这样每个图表的专属配置比如饼图的 innerRadius不会污染统一接口。再看基类 core/Chart.js它把「创建画布」这件所有图表都要做的事收拢到了一处export class Chart { constructor(container, options {}) { this.container typeof container string ? document.querySelector(container) : container; this.options options; this.data options.data || []; this.width options.width || this.container.clientWidth; this.height options.height || this.container.clientHeight; this.initCanvas(); } initCanvas() { const canvas document.createElement(canvas); const dpr window.devicePixelRatio || 1; canvas.width this.width * dpr; canvas.height this.height * dpr; canvas.style.width this.width px; canvas.style.height this.height px; this.ctx canvas.getContext(2d); this.ctx.scale(dpr, dpr); this.canvas canvas; this.container.appendChild(canvas); } render() { throw new Error(render 方法需要由子类实现); } setData(data) { this.data data; this.render(); } destroy() { this.container.removeChild(this.canvas); } }逻辑说明构造函数统一解析容器、尺寸、数据随后 initCanvas 创建 canvas 并挂到容器上。这里藏着一个很容易翻车的细节canvas.width 和 canvas.height 乘以了 devicePixelRatio而 CSS 的宽高仍然用逻辑像素这行的作用是把高分屏的物理像素占满否则图形在设备像素比大于 1 的屏幕上会发虚。render 在基类里抛异常强制子类必须实现自己的绘制逻辑这是模板方法模式的常见用法。参数说明width 和 height 不传时会回退到容器的 clientWidth 和 clientHeight如果容器此时是隐藏的clientWidth 会是 0这就是「图表渲染出来是空白」的经典坑后面避坑章节会展开。setData 是二次开发时最常用的方法直接替换数据并触发重绘destroy 里把 canvas 从容器移除切换图表类型前记得调用避免页面上堆积一堆看不见的 canvas 节点。3. 核心模块拆解一个图表从配置到像素的完整链路3.1 数据预处理与比例尺Scale 是绘图的翻译官很多人把图表源码当成黑匣子看完 Scale 才发现绘图组件的地基是什么。柱状图的数值 42 在画布上应该画多高饼图的数据 120 应该占多少度这些换算全部由比例尺完成。core/Scale.js 的核心实现如下// src/core/Scale.js export function createScale(min, max, height, padding { top: 20, bottom: 30 }) { const range max - min || 1; // 防除零 const usableHeight height - padding.top - padding.bottom; return { toY(value) { return padding.top ((max - value) / range) * usableHeight; }, toX(value, width, paddingLeft 40, paddingRight 20) { const usableWidth width - paddingLeft - paddingRight; return paddingLeft ((value - min) / range) * usableWidth; }, niceTicks(count 5) { const rawStep range / count; const step niceStep(rawStep); const ticks []; for (let v Math.ceil(min / step) * step; v max; v step) { ticks.push(Number(v.toFixed(10))); } return ticks; } }; } function niceStep(raw) { const pow Math.pow(10, Math.floor(Math.log10(raw))); const fraction raw / pow; if (fraction 1) return pow; if (fraction 2) return 2 * pow; if (fraction 5) return 5 * pow; return 10 * pow; }逻辑说明toY 是核心换算注意 Canvas 的 y 轴方向向下所以数值越大 y 越小这里用 max - value 保证大数值画在上方。niceTicks 生成坐标轴刻度关键在 niceStep 这个「1、2、5 规则」——把原始步长归一化到 1、2、5、10 这几个整数上保证刻度是 0、20、40 这种可读数字而不是 3.57 这种没法看的刻度。参数说明padding 给顶部留出图例和标题空间底部留出横轴刻度文字空间count 控制刻度密度默认 5 条。二次开发时如果要画负值柱状图min 要改成负数的最大值而不是写死 0否则负数部分会被裁掉。3.2 柱状图宽度分配与圆角处理BarChart 继承 Chart它只实现「柱状图专属」的绘制逻辑。柱子的宽度不是写死的而是根据数据条数和画布宽度自动分配// src/charts/BarChart.js import { Chart } from ../core/Chart.js; import { createScale } from ../core/Scale.js; import { maxOf } from ../utils/data.js; export class BarChart extends Chart { constructor(container, options) { super(container, options); this.gapRatio options.gapRatio ?? 0.3; // 整组柱子中空隙占的比例 this.radius options.radius ?? 0; // 柱顶圆角半径0 表示直角 this.colors options.colors || [#4a7df7]; this.render(); } render() { const { ctx, width, height } this; ctx.clearRect(0, 0, width, height); this.drawAxisAndGrid(); this.drawBars(); } drawAxisAndGrid() { const scale createScale(0, maxOf(this.data), this.height); const ticks scale.niceTicks(5); ticks.forEach((tick) { const y scale.toY(tick); this.ctx.strokeStyle #eceff4; this.ctx.lineWidth 1; this.ctx.beginPath(); this.ctx.moveTo(40, y); this.ctx.lineTo(this.width, y); this.ctx.stroke(); }); } drawBars() { const { ctx, width, height, data } this; if (!data.length) return; const scale createScale(0, maxOf(data), height); const groupWidth width / data.length; const barWidth groupWidth * (1 - this.gapRatio); const offset (groupWidth - barWidth) / 2; data.forEach((value, i) { const x i * groupWidth offset; const y scale.toY(value); const h height - 30 - y; const color this.colors[i % this.colors.length]; ctx.fillStyle color; if (this.radius 0) { this.roundRect(x, y, barWidth, h, this.radius); } else { ctx.fillRect(x, y, barWidth, h); } }); } roundRect(x, y, w, h, r) { const ctx this.ctx; const radius Math.min(r, w / 2, h / 2); ctx.beginPath(); ctx.moveTo(x radius, y); ctx.lineTo(x w - radius, y); ctx.quadraticCurveTo(x w, y, x w, y radius); ctx.lineTo(x w, y h); ctx.lineTo(x, y h); ctx.lineTo(x, y radius); ctx.quadraticCurveTo(x, y, x, y radius); ctx.closePath(); ctx.fill(); } }逻辑说明barWidth 由 groupWidth 乘以 1 - gapRatio 得到意味着空子间距占整组的 30%柱子越宽越拥挤offset 把柱子推到整组的正中间。roundRect 里用了 Math.min(r, w / 2, h / 2)防止柱子很矮时圆角半径超过高度的一半导致图形变形这是从设计稿还原时最容易翻车的地方。参数说明colors 支持数组按索引循环取色数据条数多于颜色数时会从头再取一遍grid 线每根都经过 40 像素的左边距对齐保证网络线和坐标轴刻度在视觉上共用同一条垂直参考线。二次开发时如果想给柱子加 hover 高亮把 mousemove 事件的 offsetX 除以 groupWidth 取整就能算出当前是哪根柱子这就是事件交互段的入口。3.3 饼图扇区角度累加与动画补间饼图比柱状图少一个维度没有坐标轴核心是角度分配。这里最容易踩的坑是角度累加每个扇区的起始角度必须是上一个扇区的结束角度一旦中间某个数据算错整个圆就会错位// src/charts/PieChart.js import { Chart } from ../core/Chart.js; import { sum } from ../utils/data.js; export class PieChart extends Chart { constructor(container, options) { super(container, options); this.innerRadius options.innerRadius ?? 0; // 大于 0 时渲染环图 this.animationDuration options.animationDuration ?? 600; this.colors options.colors || [#4a7df7, #f5a623, #6ad1a0, #ef6a6a]; this.progress 0; this.rafId null; this.render(); this.playAnimation(); } playAnimation() { const start performance.now(); const duration this.animationDuration; const step (now) { const t Math.min((now - start) / duration, 1); this.progress easeOutCubic(t); this.render(); if (t 1) { this.rafId requestAnimationFrame(step); } }; this.rafId requestAnimationFrame(step); } render() { const { ctx, width, height, data, innerRadius } this; ctx.clearRect(0, 0, width, height); const total sum(data); if (total 0 || data.length 0) { ctx.beginPath(); ctx.arc(width / 2, height / 2, Math.min(width, height) / 4, 0, Math.PI * 2); ctx.fillStyle #e5e8ee; ctx.fill(); return; } const centerX width / 2; const centerY height / 2; const outerRadius Math.min(width, height) / 2 - 20; let startAngle -Math.PI / 2; // 从 12 点方向开始 data.forEach((value, i) { const sliceAngle (value / total) * Math.PI * 2 * this.progress; ctx.beginPath(); ctx.moveTo(centerX, centerY); ctx.arc(centerX, centerY, outerRadius, startAngle, startAngle sliceAngle); ctx.closePath(); ctx.fillStyle this.colors[i % this.colors.length]; ctx.fill(); startAngle sliceAngle; }); if (innerRadius 0) { ctx.clearRect(centerX - innerRadius, centerY - innerRadius, innerRadius * 2, innerRadius * 2); } } destroy() { if (this.rafId) cancelAnimationFrame(this.rafId); super.destroy(); } } function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }逻辑说明startAngle 从 -Math.PI / 2 开始也就是圆的 12 点方向这是图表约定俗成的起点。每个扇区画完后立刻累加 startAngle保证下一扇区无缝衔接。动画部分没有用 CSS而是每帧用 requestAnimationFrame 更新 this.progresseaseOutCubic 让动画先快后慢视觉上比匀速舒服很多。参数说明animationDuration 传 0 时progress 会直接变 1动画一帧完成等于关闭入场动画innerRadius 大于 0 时绘制完成后用 clearRect 把中间圆挖掉渲染成环图省去了再画一层背景色的麻烦。destroy 方法里先 cancelAnimationFrame 再移除 canvas这是防止「图表已销毁、动画还在跑」的关键后面避坑章节还会细说。4. 从拿到源码到跑通本地启动、最小示例与参数速查4.1 本地启动一条命令起个静态服务由于入口文件用了 ES Module 的 import 和 export直接双击 examples/bar.html 会触发跨域限制控制台报模块加载失败。常见做法是在项目根目录起一个静态文件服务器python3 -m http.server 8080然后浏览器访问 http://localhost:8080/examples/bar.html就能看到柱状图示例。如果你不想用 Python换任意一个你顺手的本地静态服务器工具都行原理一样让页面通过 http:// 协议访问而不是 file://。这一步做完能省掉后面一大半「为什么加载不出来」的排查时间。4.2 最小调用示例一个配置对象出一张图examples/bar.html 的核心逻辑就这几行!DOCTYPE html html langzh-CN body div idapp stylewidth: 720px; height: 360px;/div script typemodule import { createChart } from ../src/index.js; const chart createChart(#app, { type: bar, data: [34, 52, 41, 63, 48, 57], colors: [#4a7df7, #6ad1a0], radius: 4, // 柱顶圆角 gapRatio: 0.3, // 柱子之间的空隙比例 animationDuration: 500 // 入场动画时长毫秒 }); // 5 秒后换一组数据观察动画过渡 setTimeout(() { chart.setData([60, 32, 55, 41, 66, 43]); }, 5000); /script /body /html逻辑说明第 21 行的 createChart 把配置对象交给工厂函数返回一个 BarChart 实例setData 在基类里封装好了替换数据后自动重绘。第 7 行给容器写死了宽高这是图表能正常渲染的前提容器没有宽高时 clientWidth 会取到 0。参数说明汇总如下参数类型默认值说明typestringbar图表类型bar / piedatanumber[][]绘图数据纯数值数组width / heightnumber容器尺寸画布逻辑像素尺寸colorsstring[]内置配色按序给每个数据项配色gapRationumber0.3柱状图柱子间隙占整组比例radiusnumber0柱状图柱顶圆角半径innerRadiusnumber0饼图内径大于 0 渲染为环形图animationDurationnumber600入场动画时长0 关闭动画想换成饼图只需要改两个字段const pie createChart(#app, { type: pie, data: [320, 240, 180, 120], innerRadius: 60, // 60px 内径 → 环形图 animationDuration: 800 });逻辑说明type 从 bar 改成 pie工厂函数会走另一个分支innerRadius 传 60 后render 末尾的 clearRect 会挖掉中心圆渲染成环形图。这里能看出分层设计的好处——调用方只关心配置完全不碰绘图 API。4.3 读源码的入口一条调用链路走下来我拆这套代码时习惯在浏览器里打断点沿着一条完整的调用链走一遍createChart 工厂函数 → BarChart 构造函数 → 基类 Chart 的 initCanvas → render → drawAxisAndGrid → drawBars。每一步都在浏览器开发者工具的 Sources 面板里单步执行同时盯着 this.ctx 上的状态变化很快就能把「配置对象怎么变成像素」串起来。有个小技巧把 data 换成 [1, 2, 3, 4, 5]再把断点打在第 20 行drawBars 里取 scale 的位置看 scale.toY 对每个值算出的 y 坐标再对照画布上的实际柱子位置你对绘图组件的坐标系就有了直观认识。事件交互段Tooltip、Hover也是这么读的断点打在 components/Tooltip.js 的 mousemove 回调上看它怎么用 offsetX 反推数据索引。5. 避坑清单Canvas 绘图最容易翻车的 5 个实际问题这一章我把跑 demo、改代码时踩过的坑整理成 5 条每条按现象、原因、解决三段写基本是拿真机换来的血泪经验可以直接对着排查。5.1 高分屏下图表边缘发虚现象图表在设备像素比大于 1 的屏幕上看起来毛刺明显柱子边缘和文字像被羽化过一样。原因Canvas 内部绘图缓冲的尺寸默认等于 CSS 像素尺寸而高分屏上每个 CSS 像素对应多个物理像素。画布按 1:1 采样后放大边缘自然发虚。解决初始化时把 canvas 的实际宽高乘以 devicePixelRatio再用 ctx.scale(dpr, dpr) 把所有绘图坐标统一到逻辑像素坐标系。这就是基类 initCanvas 里那段代码的作用二次开发时如果自己 new 了一个 canvas务必复制这段逻辑const dpr window.devicePixelRatio || 1; canvas.width width * dpr; canvas.height height * dpr; canvas.style.width width px; canvas.style.height height px; ctx.scale(dpr, dpr);5.2 容器隐藏时渲染成空白现象页面初始加载时图表区域是 display:none切到该标签页后图表是空的或者窗口缩放后图表「缺」了一块。原因构造函数里 width 回退到 container.clientWidth容器隐藏时这个值是 0canvas 被创建成 0 宽 0 高。resize 场景则是尺寸读取先于布局完成拿到的是上一帧的旧值。解决确保容器可见后再创建图表需要响应式时用 ResizeObserver 在回调里重新取尺寸并调用 renderconst ro new ResizeObserver(() { chart.width container.clientWidth; chart.height container.clientHeight; chart.render(); }); ro.observe(container);注意 ResizeObserver 的回调在下一帧才触发如果想在回调里立刻读取布局要先用 requestAnimationFrame 包一层否则读到的还是旧尺寸。5.3 动画重复触发导致鬼影闪烁现象快速切换数据源时图表上同时出现新旧两组图形像鬼影一样交替闪烁。原因每次 setData 都启动一个新的 requestAnimationFrame 循环旧的循环没有被取消两个循环各自调用 render互相覆盖到一半就被下一次 clearRect 打断。解决在基类里维护一个 rafId启动新动画前先 cancelAnimationFrame 旧的setData(data) { this.data data; if (this.rafId) cancelAnimationFrame(this.rafId); this.render(); }同理destroy 方法里也必须 cancelAnimationFrame否则组件销毁后动画循环还在后台空转浏览器控制台会一直有绘制任务在跑。5.4 数据全 0 时饼图直接画挂现象接口返回的全是 0 或空数组时饼图出现 NaN 角度控制台报错严重时整个图表区域被异常绘制搞成黑色。原因total 0value / total 得到 Infinityarc 的角度变成 NaN。Canvas 对 NaN 的处理是直接放弃绘制或把路径留在画布上表现就是「画了一半的圆」。解决render 开头对 total 0 和 data.length 0 做短路画一个灰色占位圆直接 return。这是最省事的兜底也是我在 PieChart 源码里看到的标准做法if (total 0 || data.length 0) { ctx.beginPath(); ctx.arc(centerX, centerY, Math.min(width, height) / 4, 0, Math.PI * 2); ctx.fillStyle #e5e8ee; ctx.fill(); return; }柱状图同理drawBars 开头那句 if (!data.length) return 也是同一目的。空数据不是异常是正常业务状态组件必须在任何数据情况下都能画出「至少一个看得懂的东西」。5.5 绘图状态残留导致颜色错乱现象柱状图先渲染的柱子带圆角后面的柱子变成直角或者 fillStyle 扩散到了网格线上网格从浅灰变成深蓝。原因Canvas 是状态机fillStyle、lineWidth、shadow 这些属性会一直保留到下次被修改。子类绘制时改了全局状态却没有还原后面的绘制全部继承状态像墨汁一样从一根柱子洇到整张图。解决绘制分阶段组件时每一段开始前 ctx.save()结束后 ctx.restore()用了 translate 或 scale 的必须成对出现。我一般还会在 render 最开头先 ctx.setTransform(1, 0, 0, 1, 0, 0) 重置变换再重新 scale(dpr, dpr)避免上一次的位移污染后续所有坐标计算。这两件事都做了颜色错乱的问题基本绝迹。6. 二次开发实战给组件新增折线图的完整过程6.1 新增折线图继承基类只写自己特有的部分前面章节反复强调「加新图表不动核心代码」现在实操一遍。在 charts 目录新建 LineChart.js继承 Chart 基类。折线图和柱状图最大的差别是没有柱子宽度分配不需要逐根处理圆角核心是算坐标、连线、加平滑。// src/charts/LineChart.js import { Chart } from ../core/Chart.js; import { createScale } from ../core/Scale.js; import { maxOf } from ../utils/data.js; export class LineChart extends Chart { constructor(container, options) { super(container, options); this.smooth options.smooth ?? true; // 是否平滑曲线 this.lineColor options.lineColor || #4a7df7; this.lineWidth options.lineWidth ?? 2; this.render(); } render() { const { ctx, width, height, data } this; ctx.clearRect(0, 0, width, height); if (data.length 2) return; // 少于两个点没法连线 const scale createScale(0, maxOf(data), height); const points data.map((value, i) ({ x: (i / (data.length - 1)) * width, y: scale.toY(value) })); ctx.save(); ctx.strokeStyle this.lineColor; ctx.lineWidth this.lineWidth; ctx.lineJoin round; ctx.beginPath(); ctx.moveTo(points[0].x, points[0].y); for (let i 1; i points.length; i) { const mx (points[i - 1].x points[i].x) / 2; const my (points[i - 1].y points[i].y) / 2; ctx.quadraticCurveTo(points[i - 1].x, points[i - 1].y, mx, my); } ctx.lineTo(points[points.length - 1].x, points[points.length - 1].y); ctx.stroke(); ctx.restore(); } }逻辑说明x 坐标按索引均分最后一个点落在画布右边缘所以用 data.length - 1 做分母。平滑曲线的思路是取相邻两点的中点用 quadraticCurveTo 画贝塞尔曲线经过中点视觉上就是一条过所有数据点的 S 型过渡线。这样折线不会像 SVG 默认的折线那样出现尖锐折角也不会像高阶插值那样剧烈抖动。然后回到 index.js 的工厂函数里注册新类型import { LineChart } from ./charts/LineChart.js; if (type line) { return new LineChart(container, { data, ...rest }); }至此调用方写 type: line 就能生成折线图整个改动只需要新增一个文件和处理一个分支core 目录一行没动这正是组件分层带来的扩展性红利。6.2 改造后的验证方法新增图表不是「画出来就算完」我给自己定了一个三连验证按顺序跑一遍能拦下九成的问题第一正常数据验证。用柱状图示例的数据切到 line确认折线穿过所有数据点、没有超出画布、最后一个点完整可见。第二边界数据验证。data 传 [] 和 [0, 0, 0]确认页面无报错、控制台无 NaN 警告至少画出一个占位提示。第三高分屏与缩放验证。把窗口从窄拉到宽再缩回来确认折线跟随容器重新计算坐标边缘不发虚、不残留旧图形。这三步分别对应数据正确性、健壮性和渲染一致性图表组件的回归测试没有比这三样更省事的组合了。从那以后我每次改完图表组件都会强制自己把这三件事走一遍先喂一组边界数据再放大到高分屏看边缘最后拉着窗口来回缩放。图表相关的 bug 大多藏在细节里跑完这三步再交给测试基本不会再被打回来。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑