简介这是一份轻量级、开箱即用的纯前端图表可视化插件面向Web前端初学者、Vue项目开发者及需快速嵌入统计图表的中小型应用工程师解决无后端依赖下动态生成折线图、柱状图、饼图的常见需求。资源为单HTML文件共1个体积仅3KB内含完整HTML结构、原生JavaScript逻辑与CSS样式无需引入第三方CDN支持在table中手动录入数据并实时刷新图表具备图表类型切换、本地下载PNG功能并已验证可直接集成至Vue项目中使用。目前已有833人学习下载读者可直接运行该HTML文件体验交互全流程从表格增删行列、填写原始数据到一键切换图表类型、导出可视化结果全部操作均基于原生JS实现代码结构清晰、注释友好是理解前端图表封装逻辑与轻量化数据可视化的优质实践样本。1. 为什么一个“纯 HTML”的图表插件反而在嵌入式看板、离线报告和老旧系统里成了救命稻草你有没有遇到过这样的场景给某高校实验室部署一套设备状态监控页但目标机只装了 IE11 本地文件协议file://连 HTTP 服务都不允许起或者给某公司老旧 MES 系统加个产量统计模块对方明确拒绝任何外部 CDN、不许加载远程 JS、连fetch都被策略禁用又或者要生成一份可邮件直发的离线周报 PDF图表必须内联、无依赖、打开即见——这时候D3.js 的力导向图、ECharts 的炫酷动画、Chart.js 的模块化打包全成了摆设。“统计折线图-柱状图-饼图纯html-charts插件”不是另一个轮子而是对「零运行时依赖、单 HTML 文件可执行、不碰 DOM 外部状态、不依赖构建链」这一硬约束的精准回应。它不追求 WebGL 渲染、不支持响应式重绘、不提供 API 注册回调但它能用canvas或纯 SVG 内联 CSS 在一个.html文件里把三类基础统计图表折线、柱状、饼图一次性画出来数据靠 JSON 字符串传入样式靠style块固化交互仅保留最朴素的 hover 提示用原生 title 属性或极简 tooltip div。适合嵌入式 HMI 页面、离线培训材料、政府单位内网静态报表、老旧工控 Web 界面等「技术退行性场景」。如果你正被“不能联网”“不能装 Node”“不能改服务器配置”卡住这篇就是为你写的落地笔记。2. 从零手写一个最小可用版30 行 HTML 60 行 JS 就跑通三图同屏这个插件的核心价值不在功能多而在“删无可删”。我们不基于任何框架不引入任何第三方包所有逻辑压进一个script标签。下面这段是真实可运行的最小闭环——它不叫“插件”但它是所有后续封装的原子基底。2.1 用 canvas 绘制折线图数据映射与坐标系对齐是关键canvas idline-chart width600 height300/canvas script const ctx document.getElementById(line-chart).getContext(2d); const data [12, 19, 3, 5, 2, 3]; // 步骤1定义绘图区域边界留白防文字截断 const margin { top: 20, right: 20, bottom: 40, left: 50 }; const width 600 - margin.left - margin.right; const height 300 - margin.top - margin.bottom; // 步骤2计算 X/Y 轴比例尺非 D3纯数学映射 const xScale d margin.left (d * width) / (data.length - 1); const yScale d margin.top height - (d * height) / Math.max(...data); // 步骤3绘制坐标轴仅线条无刻度文字 ctx.beginPath(); ctx.moveTo(margin.left, margin.top); ctx.lineTo(margin.left, height margin.top); ctx.lineTo(width margin.left, height margin.top); ctx.stroke(); // 步骤4绘制折线带圆点标记 ctx.beginPath(); data.forEach((d, i) { const x xScale(i); const y yScale(d); if (i 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.strokeStyle #3b82f6; ctx.lineWidth 2; ctx.stroke(); // 步骤5绘制数据点 data.forEach((d, i) { const x xScale(i); const y yScale(d); ctx.beginPath(); ctx.arc(x, y, 4, 0, Math.PI * 2); ctx.fillStyle #10b981; ctx.fill(); }); /script逻辑说明这里没有用scaleLinear()而是用两行纯算术完成坐标映射。xScale把数组索引i映射到画布横坐标yScale把数值d映射到纵坐标注意 Y 轴翻转Canvas 原点在左上角。这种写法牺牲了灵活性但换来绝对可控——你知道每个像素是谁画的出错时直接查xScale(i)输出值就能定位。参数说明margin是硬编码的留白实际项目中建议抽成配置对象Math.max(...data)是简易最大值计算若数据含负数需改为Math.max(...data) - Math.min(...data)并调整 yScale 偏移。2.2 用 SVG 绘制柱状图用rect替代 canvas path 更易调试svg idbar-chart width600 height300 styleborder:1px solid #e5e7eb;/svg script const svg document.getElementById(bar-chart); const barData [25, 40, 30, 35, 20]; const barWidth 40; const spacing 20; const chartHeight 250; const chartTop 30; // 计算最大值用于比例缩放 const maxValue Math.max(...barData); const barHeight d (d / maxValue) * chartHeight; // 逐个创建 rect barData.forEach((d, i) { const x 50 i * (barWidth spacing); const y chartTop chartHeight - barHeight(d); const height barHeight(d); const rect document.createElementNS(http://www.w3.org/2000/svg, rect); rect.setAttribute(x, x); rect.setAttribute(y, y); rect.setAttribute(width, barWidth); rect.setAttribute(height, height); rect.setAttribute(fill, i % 2 0 ? #8b5cf6 : #ec4899); rect.setAttribute(rx, 4); // 圆角 svg.appendChild(rect); // 添加数值标签 const text document.createElementNS(http://www.w3.org/2000/svg, text); text.setAttribute(x, x barWidth / 2); text.setAttribute(y, y - 5); text.setAttribute(text-anchor, middle); text.setAttribute(font-size, 12); text.textContent d; svg.appendChild(text); }); /script为什么选 SVG 而非 canvas柱状图本质是离散矩形集合SVG 的rect元素天然支持 CSS 动画、hover 伪类、无障碍属性aria-label且开发者工具里可直接 inspect 元素查坐标。Canvas 里画 100 个柱子出错只能靠console.log(x,y)推算而 SVG 里右键“检查元素”就能看到x120——这对现场快速排障意义重大。参数说明barWidth和spacing控制柱宽与间距建议按width / (data.length * 1.5)反推初始值rx4是圆角半径设为 0 即直角柱text-anchormiddle让文字水平居中对齐柱子。2.3 用 SVG path 绘制饼图用扇形路径指令A替代 canvas arcsvg idpie-chart width300 height300 viewBox0 0 300 300/svg script const pieData [ { label: A类, value: 35 }, { label: B类, value: 25 }, { label: C类, value: 20 }, { label: D类, value: 20 } ]; const total pieData.reduce((sum, d) sum d.value, 0); const centerX 150; const centerY 150; const radius 100; let startAngle -Math.PI / 2; // 从12点钟方向开始 pieData.forEach((item, i) { const sliceAngle (item.value / total) * Math.PI * 2; const endAngle startAngle sliceAngle; // 计算起点和终点坐标 const startX centerX radius * Math.cos(startAngle); const startY centerY radius * Math.sin(startAngle); const endX centerX radius * Math.cos(endAngle); const endY centerY radius * Math.sin(endAngle); // 大弧标志sliceAngle π 时为1否则为0 const largeArcFlag sliceAngle Math.PI ? 1 : 0; // SVG 路径指令M 移动到起点 → A 画弧 → L 画线回中心 → Z 闭合 const pathData M ${centerX} ${centerY} L ${startX} ${startY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${endX} ${endY} Z ; const path document.createElementNS(http://www.w3.org/2000/svg, path); path.setAttribute(d, pathData); path.setAttribute(fill, [#3b82f6, #10b981, #8b5cf6, #f59e0b][i]); path.setAttribute(stroke, #fff); path.setAttribute(stroke-width, 2); // 添加 title 实现 hover 提示原生支持无需 JS const title document.createElementNS(http://www.w3.org/2000/svg, title); title.textContent ${item.label}: ${item.value} (${((item.value/total)*100).toFixed(1)}%); path.appendChild(title); document.getElementById(pie-chart).appendChild(path); startAngle endAngle; }); /script关键细节A指令的第五个参数是large-arc-flag决定走大弧还是小弧。很多初学者在这里翻车——明明角度算对了画出来却是反向缺口。记住口诀“超过半圈填 1不到半圈填 0”。本例中sliceAngle是弧度值直接与Math.PI比较即可。避坑提示不要用cx/cy/r属性画饼图SVGcircle只能画整圆。必须用pathA指令。另外viewBox0 0 300 300确保缩放时图形不失真比固定width/height更鲁棒。3. 封装成可复用插件用 IIFE 配置对象收口三图逻辑手写三段代码只是验证可行性。真正投入项目你需要一个能new HtmlChart()调用的轻量封装。我们不搞 class 继承、不搞事件总线就用一个立即执行函数表达式IIFE把逻辑锁死对外只暴露render()方法。3.1 插件骨架IIFE 封装 链式调用风格// html-charts.min.js单文件无依赖 (function (global) { use strict; function HtmlChart(containerId) { this.container typeof containerId string ? document.getElementById(containerId) : containerId; this.config { type: line, // line | bar | pie data: [], options: { title: , colors: [], fontSize: 12, showLegend: true } }; } HtmlChart.prototype.setConfig function (cfg) { Object.assign(this.config, cfg); return this; // 支持链式 }; HtmlChart.prototype.render function () { if (!this.container) return console.error(Container not found); // 清空容器避免重复渲染叠加 this.container.innerHTML ; switch (this.config.type) { case line: return this._renderLine(); case bar: return this._renderBar(); case pie: return this._renderPie(); default: console.warn(Unknown chart type:, this.config.type); } }; // 后续实现 _renderLine 等私有方法... // 此处省略见 3.2~3.4 // 暴露全局 global.HtmlChart HtmlChart; })(typeof window ! undefined ? window : global);为什么用 IIFE 而不用 ES Module因为你要塞进file://协议页面而浏览器对本地文件的 ES Module 加载有跨域限制import不被允许。IIFE 是唯一能在script srcxxx.js中安全执行的模式且天然避免变量污染全局。链式调用的价值让调用方代码像这样干净div idmy-chart/div script srchtml-charts.min.js/script script new HtmlChart(my-chart) .setConfig({ type: bar, data: [12, 35, 28, 42], options: { title: 月度故障数 } }) .render(); /script3.2 折线图增强添加 X/Y 轴标签、网格线、平滑曲线开关HtmlChart.prototype._renderLine function () { const { data, options } this.config; const width 600; const height 300; const margin { top: 40, right: 20, bottom: 50, left: 60 }; const innerWidth width - margin.left - margin.right; const innerHeight height - margin.top - margin.bottom; // 创建 canvas 并获取上下文 const canvas document.createElement(canvas); canvas.width width; canvas.height height; this.container.appendChild(canvas); const ctx canvas.getContext(2d); // 绘制标题 ctx.font bold 16px sans-serif; ctx.textAlign center; ctx.fillText(options.title || , width / 2, margin.top - 10); // 绘制网格线水平 ctx.strokeStyle #e5e7eb; ctx.lineWidth 1; for (let i 0; i 5; i) { const y margin.top (i / 5) * innerHeight; ctx.beginPath(); ctx.moveTo(margin.left, y); ctx.lineTo(width - margin.right, y); ctx.stroke(); } // 数据映射同前略 const maxValue Math.max(...data); const xScale i margin.left (i / (data.length - 1)) * innerWidth; const yScale d margin.top innerHeight - (d / maxValue) * innerHeight; // 绘制折线支持平滑贝塞尔插值 if (options.smooth true data.length 2) { this._drawSmoothLine(ctx, data, xScale, yScale, margin, innerWidth, innerHeight); } else { this._drawPolyline(ctx, data, xScale, yScale); } // 绘制 X 轴标签简单数字索引 ctx.font ${options.fontSize || 12}px sans-serif; ctx.textAlign center; data.forEach((_, i) { const x xScale(i); ctx.fillText(第${i1}周, x, height - margin.bottom / 2); }); // 绘制 Y 轴标签0 ~ max分 5 档 for (let i 0; i 5; i) { const y margin.top (i / 5) * innerHeight; const val Math.round((i / 5) * maxValue); ctx.textAlign right; ctx.fillText(val, margin.left - 10, y 4); } }; HtmlChart.prototype._drawPolyline function (ctx, data, xScale, yScale) { ctx.beginPath(); data.forEach((d, i) { const x xScale(i); const y yScale(d); if (i 0) ctx.moveTo(x, y); else ctx.lineTo(x, y); }); ctx.strokeStyle #3b82f6; ctx.lineWidth 2; ctx.stroke(); }; HtmlChart.prototype._drawSmoothLine function (ctx, data, xScale, yScale, margin, w, h) { // 简化贝塞尔取相邻三点控制点 ctx.beginPath(); for (let i 0; i data.length; i) { const x xScale(i); const y yScale(data[i]); if (i 0) { ctx.moveTo(x, y); } else { const prevX xScale(i - 1); const prevY yScale(data[i - 1]); const nextX i data.length - 1 ? xScale(i 1) : x; const nextY i data.length - 1 ? yScale(data[i 1]) : y; const cp1x (prevX x) / 2; const cp1y (prevY y) / 2; const cp2x (x nextX) / 2; const cp2y (y nextY) / 2; ctx.bezierCurveTo(cp1x, cp1y, cp2x, cp2y, x, y); } } ctx.strokeStyle #8b5cf6; ctx.lineWidth 2; ctx.stroke(); };参数说明options.smooth是布尔开关默认falseoptions.fontSize控制所有文字大小margin已固化为常量如需动态适配容器尺寸可增加autoSize: true选项并监听resize但会引入额外复杂度本插件默认不启用。为什么网格线只画水平不画垂直垂直网格线在折线图中干扰视觉主干且 X 轴是离散维度周/月画线反而误导“连续性”。这是经验法则折线图重趋势柱状图重对比饼图重占比——网格线策略必须服从图表语义。3.3 柱状图增强支持分组柱状图双数据系列、自定义颜色映射HtmlChart.prototype._renderBar function () { const { data, options } this.config; const width 600; const height 300; const margin { top: 40, right: 20, bottom: 50, left: 60 }; const innerWidth width - margin.left - margin.right; const innerHeight height - margin.top - margin.bottom; const svg document.createElementNS(http://www.w3.org/2000/svg, svg); svg.setAttribute(width, width); svg.setAttribute(height, height); this.container.appendChild(svg); // 标题 const title document.createElementNS(http://www.w3.org/2000/svg, text); title.setAttribute(x, width / 2); title.setAttribute(y, margin.top - 10); title.setAttribute(text-anchor, middle); title.setAttribute(font-size, 16); title.setAttribute(font-weight, bold); title.textContent options.title || ; svg.appendChild(title); // 支持两种 data 格式[12,35,28] 或 [[12,15],[35,29],[28,33]] let series Array.isArray(data[0]) ? data : [data]; const groupCount series.length; const barCount series[0].length; const barWidth Math.min(40, innerWidth / (barCount * groupCount groupCount - 1)); const spacing barWidth * 0.5; // 计算全局最大值用于 Y 轴缩放 const allValues series.flat(); const maxValue Math.max(...allValues); // 绘制每组柱子 for (let g 0; g groupCount; g) { const groupData series[g]; const color options.colors?.[g] || [#3b82f6, #10b981, #8b5cf6, #f59e0b][g % 4]; groupData.forEach((d, i) { const x margin.left i * (barWidth * groupCount spacing) g * barWidth; const y margin.top innerHeight - (d / maxValue) * innerHeight; const h (d / maxValue) * innerHeight; const rect document.createElementNS(http://www.w3.org/2000/svg, rect); rect.setAttribute(x, x); rect.setAttribute(y, y); rect.setAttribute(width, barWidth); rect.setAttribute(height, h); rect.setAttribute(fill, color); rect.setAttribute(rx, 3); svg.appendChild(rect); // 数值标签仅顶部显示避免重叠 if (h 20) { const text document.createElementNS(http://www.w3.org/2000/svg, text); text.setAttribute(x, x barWidth / 2); text.setAttribute(y, y - 5); text.setAttribute(text-anchor, middle); text.setAttribute(font-size, 12); text.textContent d; svg.appendChild(text); } }); } // 图例仅当 series 1 且 showLegend 为 true if (groupCount 1 options.showLegend ! false) { const legend document.createElementNS(http://www.w3.org/2000/svg, g); legend.setAttribute(transform, translate(${margin.left}, ${height - margin.bottom 20})); series.forEach((_, g) { const color options.colors?.[g] || [#3b82f6, #10b981, #8b5cf6, #f59e0b][g % 4]; const rect document.createElementNS(http://www.w3.org/2000/svg, rect); rect.setAttribute(x, g * 100); rect.setAttribute(y, 0); rect.setAttribute(width, 12); rect.setAttribute(height, 12); rect.setAttribute(fill, color); legend.appendChild(rect); const text document.createElementNS(http://www.w3.org/2000/svg, text); text.setAttribute(x, g * 100 18); text.setAttribute(y, 10); text.setAttribute(font-size, 12); text.textContent [计划值, 实际值, 同比, 环比][g] || 系列${g1}; legend.appendChild(text); }); svg.appendChild(legend); } };核心能力通过data [[计划值], [实际值]]格式支持双系列分组柱状图barWidth自适应计算防止溢出图例位置硬编码在底部避免计算 bbox 的复杂度。颜色数组options.colors优先级高于内置色板。血泪经验分组柱状图的x坐标公式是margin.left i * (barWidth * groupCount spacing) g * barWidth。其中i是柱子序号g是组序号spacing是组间空隙。初版我漏了 g * barWidth导致所有组柱子完全重叠——调试时打印x值才揪出。3.4 饼图增强支持环形图donut、图例悬浮高亮、百分比精度控制HtmlChart.prototype._renderPie function () { const { data, options } this.config; const width 300; const height 300; const centerX width / 2; const centerY height / 2; const radius 100; const innerRadius options.donut ? 40 : 0; // 环形图内径 const svg document.createElementNS(http://www.w3.org/2000/svg, svg); svg.setAttribute(width, width); svg.setAttribute(height, height); svg.setAttribute(viewBox, 0 0 ${width} ${height}); this.container.appendChild(svg); // 总值计算 const total data.reduce((sum, d) sum d.value, 0); let startAngle -Math.PI / 2; const colors options.colors || [#3b82f6, #10b981, #8b5cf6, #f59e0b, #ef4444]; // 绘制每个扇形 data.forEach((item, i) { const sliceAngle (item.value / total) * Math.PI * 2; const endAngle startAngle sliceAngle; const outerStartX centerX radius * Math.cos(startAngle); const outerStartY centerY radius * Math.sin(startAngle); const outerEndX centerX radius * Math.cos(endAngle); const outerEndY centerY radius * Math.sin(endAngle); const innerStartX centerX innerRadius * Math.cos(startAngle); const innerStartY centerY innerRadius * Math.sin(startAngle); const innerEndX centerX innerRadius * Math.cos(endAngle); const innerEndY centerY innerRadius * Math.sin(endAngle); const largeArcFlag sliceAngle Math.PI ? 1 : 0; // 环形路径外弧 → 内弧逆向 → 闭合 let pathData M ${outerStartX} ${outerStartY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${outerEndX} ${outerEndY} L ${innerEndX} ${innerEndY} A ${innerRadius} ${innerRadius} 0 ${largeArcFlag} 0 ${innerStartX} ${innerStartY} Z ; if (innerRadius 0) { // 饼图路径无内环 pathData M ${centerX} ${centerY} L ${outerStartX} ${outerStartY} A ${radius} ${radius} 0 ${largeArcFlag} 1 ${outerEndX} ${outerEndY} Z ; } const path document.createElementNS(http://www.w3.org/2000/svg, path); path.setAttribute(d, pathData); path.setAttribute(fill, colors[i % colors.length]); path.setAttribute(stroke, #fff); path.setAttribute(stroke-width, 2); // 悬浮高亮hover 时放大 1.05 倍用 transform path.addEventListener(mouseenter, () { path.style.transform scale(1.05); path.style.transition transform 0.2s ease; }); path.addEventListener(mouseleave, () { path.style.transform scale(1); }); // Tooltip title原生支持 const title document.createElementNS(http://www.w3.org/2000/svg, title); const percent ((item.value / total) * 100).toFixed(options.precision || 1); title.textContent ${item.label}: ${item.value} (${percent}%); path.appendChild(title); svg.appendChild(path); startAngle endAngle; }); // 绘制中心文字环形图专用 if (options.donut) { const centerText document.createElementNS(http://www.w3.org/2000/svg, text); centerText.setAttribute(x, centerX); centerText.setAttribute(y, centerY 5); centerText.setAttribute(text-anchor, middle); centerText.setAttribute(dominant-baseline, middle); centerText.setAttribute(font-size, 16); centerText.setAttribute(font-weight, bold); centerText.textContent ${total}; svg.appendChild(centerText); } };参数说明options.donut: true启用环形图options.precision: 0可设为整数百分比colors数组长度自动循环避免越界。玄学细节dominant-baselinemiddle让文字垂直居中更准比ycenterY手动调更可靠transform: scale()比修改r属性更轻量且 hover 时不会触发重绘整个 SVG。4. 避坑指南这 5 个问题让我在客户现场反复重启了 3 台工控机这类“纯 HTML”插件最大的陷阱不是功能做不出来而是环境太脏、约束太死、报错太静默。以下是我在线上环境踩出的血坑按发生频率排序4.1 现象图表完全空白控制台无报错network 面板显示file://协议下html-charts.min.js状态为(blocked)原因Chrome/Edge 对本地文件协议有严格策略禁止file://页面加载本地 JS即使同目录除非启动时加--allow-file-access-from-files参数——但客户工控机不允许改启动项。解决放弃script srcxxx.js改用内联脚本。把html-charts.min.js内容复制进script标签确保整个 HTML 是单文件。虽然失去缓存但换来 100% 可运行。4.2 现象IE11 下饼图扇形全部错位canvas 折线图线条断裂原因IE11 不支持Math.PI * 2这种写法会报Invalid character也不支持const声明且canvas.getContext(2d)在某些旧显卡驱动下返回 null。解决全局替换const→varMath.PI * 2→6.283185307179586硬编码canvas 初始化加兜底var ctx canvas.getContext canvas.getContext(2d); if (!ctx) { // 降级方案用 SVG 替代 canvas this._renderLineAsSvg(); return; }4.3 现象柱状图数值标签在某些分辨率下被截断Y 轴最大值显示为NaN原因Math.max(...data)遇到空数组或含null数据时返回-Infinity再参与除法得NaN标签y坐标未做边界检查超出 SVG 画布即消失。解决数据校验前置const cleanData data.map(d typeof d number ? d : 0); const maxValue cleanData.length ? Math.max(...cleanData) : 1;标签y坐标加保护const labelY Math.max(margin.top 5, Math.min(height - margin.bottom - 5, y - 5)); text.setAttribute(y, labelY);4.4 现象饼图 hover 提示title在触摸屏设备上不触发原因title是 SVG 原生 tooltip依赖鼠标mouseenter而 Android/iOS 触摸设备默认不模拟 hover 事件。解决不依赖title改用绝对定位divtouchstart事件path.addEventListener(touchstart, function(e) { e.preventDefault(); const tooltip document.createElement(div); tooltip.className html-chart-tooltip; tooltip.textContent this.title.textContent; tooltip.style.left (e.touches[0].clientX 10) px; tooltip.style.top (e.touches[0].clientY 10) px; document.body.appendChild(tooltip); // 3秒后自动移除 setTimeout(() { if (tooltip.parentNode) document.body.removeChild(tooltip); }, 3000); });并在 CSS 中定义.html-chart-tooltip { position: absolute; background: rgba(0,0,0,0.8); color: white; padding: 4px 8px; border-radius: 3px; font-size: 12px; pointer-events: none; z-index: 9999; }4.5 现象导出为 PDF 后图表模糊、文字锯齿、颜色失真原因Chrome 打印预览对canvas渲染质量极差且 SVG 中rx圆角、shadow等效果在 PDF 导出时被忽略。解决强制高清 canvas设置canvas.width 1200; canvas.height 600;再用 CSS 缩放#my-chart canvas { width: 600px; height: 300px; }PDF 友好 SVG禁用rx、filter、gradient所有颜色用十六进制避免rgb(59, 130, 246)终极方案导出前调用window.print()并在 CSS 中加media print { body * { visibility: hidden; } #my-chart, #my-chart * {本文还有配套的精品资源点击获取