
简介这是一份轻量级、开箱即用的纯前端图表可视化插件面向Web前端初学者、Vue项目开发者及需快速嵌入统计图表的中小型应用工程师解决无后端依赖下动态生成折线图、柱状图、饼图的常见需求。资源为单HTML文件共1个体积仅3KB内含完整HTML结构、原生JavaScript逻辑与CSS样式无需引入第三方CDN支持在table中手动录入数据并实时刷新图表具备图表类型切换、本地下载PNG功能并已验证可直接集成至Vue项目中使用。目前已有833人学习下载读者可直接运行该HTML文件体验交互全流程从表格增删行列、填写原始数据到一键切换图表类型、导出可视化结果全部操作均基于原生JS实现代码结构清晰、注释友好是理解前端图表封装逻辑与轻量化数据可视化的优质实践样本。1. 为什么一个“纯 HTML”的图表插件反而在嵌入式监控、离线报表和老旧系统里成了救命稻草你可能刚被要求给一台没有网络的工业控制终端加个实时数据看板或者要给某高校实验室的老款教学机部署一份能直接双击打开、不依赖 Node.js 也不用 Python 环境的学生成绩可视化页——这时候“统计折线图-柱状图-饼图纯html-charts插件”就不是个技术噱头而是唯一能落地的解法。它不走 Canvas 渲染黑匣子不调用 D3 的复杂 API更不依赖 Webpack 打包链它用原生svgcanvas可选降级 内联 CSS 零外部请求的 JS把折线、柱状、饼图三类最常用统计图表压缩进一个不到 28KB 的单 HTML 文件里。新手改几行 JSON 数据就能出图熟手能直接 hack SVG 节点做定制高亮它不追求炫酷交互动画但保证在 IE11、Edge Legacy、甚至 Chromium Embedded FrameworkCEF3.3497 构建的桌面应用里稳稳渲染。这不是为现代 SPA 准备的轮子而是专为「不能联网、不能装依赖、不能重启服务、但必须立刻看见数据」的硬场景而生的轻量级可视化锚点。2. 从零跑通用三类基础图表验证插件可用性含最小可运行 HTML 模板这个插件的核心价值是“开箱即用”四个字落在物理层面——不需要npm install不生成node_modules不写import连index.html都可以是单文件。我们以最典型的三类统计需求切入时序趋势折线、分类对比柱状、占比分布饼图用同一份 HTML 模板完成全部验证。2.1 创建最小可运行 HTML 模板不依赖任何 CDN所有资源内联新建一个charts-demo.html粘贴以下内容。注意所有 JS/CSS 均已内联无外部 HTTP 请求双击即可在浏览器中运行!DOCTYPE html html langzh-CN head meta charsetUTF-8 title纯HTML图表插件折线柱状饼图/title style body { font-family: Segoe UI, sans-serif; margin: 0; padding: 24px; background: #f8f9fa; } .chart-container { margin-bottom: 32px; background: white; border-radius: 8px; box-shadow: 0 2px 6px rgba(0,0,0,0.05); overflow: hidden; } .chart-header { padding: 16px 20px; background: #2c3e50; color: white; font-weight: 600; } .chart-body { padding: 20px; } /style /head body div classchart-container div classchart-header月度访问量趋势折线图/div div classchart-body svg idline-chart width100% height300/svg /div /div div classchart-container div classchart-header各渠道转化率对比柱状图/div div classchart-body svg idbar-chart width100% height300/svg /div /div div classchart-container div classchart-header用户设备占比饼图/div div classchart-body svg idpie-chart width100% height300/svg /div /div !-- 插件核心 JS内联无外部依赖 -- script // 图表渲染引擎精简版仅含折线/柱状/饼图逻辑 const ChartEngine { // 工具函数深拷贝、颜色生成、数值格式化等 deepClone(obj) { return JSON.parse(JSON.stringify(obj)); }, getColor(i) { const colors [#3498db, #2ecc71, #e74c3c, #f39c12, #9b59b6]; return colors[i % colors.length]; }, formatNumber(n) { return n 1000 ? (n / 1000).toFixed(1) k : n.toString(); }, // 折线图绘制 renderLineChart(svgId, config) { const svg document.getElementById(svgId); const { data, labels, title, lineColor #3498db, pointRadius 4 } config; const width svg.clientWidth, height 300; const margin { top: 30, right: 20, bottom: 40, left: 50 }; const innerWidth width - margin.left - margin.right; const innerHeight height - margin.top - margin.bottom; // 清空旧内容 svg.innerHTML ; // 添加坐标轴组 const g document.createElementNS(http://www.w3.org/2000/svg, g); g.setAttribute(transform, translate(${margin.left},${margin.top})); svg.appendChild(g); // X轴刻度映射 const xScale d (d / (labels.length - 1)) * innerWidth; // Y轴归一化到 0~innerHeight const yMax Math.max(...data); const yScale d innerHeight - (d / yMax) * innerHeight; // 绘制折线路径 const pathData [M, xScale(0), yScale(data[0])]; for (let i 1; i data.length; i) { pathData.push(L, xScale(i), yScale(data[i])); } const path document.createElementNS(http://www.w3.org/2000/svg, path); path.setAttribute(d, pathData.join( )); path.setAttribute(fill, none); path.setAttribute(stroke, lineColor); path.setAttribute(stroke-width, 2); g.appendChild(path); // 绘制数据点 data.forEach((d, i) { const circle document.createElementNS(http://www.w3.org/2000/svg, circle); circle.setAttribute(cx, xScale(i)); circle.setAttribute(cy, yScale(d)); circle.setAttribute(r, pointRadius); circle.setAttribute(fill, lineColor); g.appendChild(circle); }); // X轴标签 labels.forEach((label, i) { const text document.createElementNS(http://www.w3.org/2000/svg, text); text.setAttribute(x, xScale(i)); text.setAttribute(y, innerHeight 20); text.setAttribute(text-anchor, middle); text.setAttribute(font-size, 12px); text.textContent label; g.appendChild(text); }); // Y轴刻度简化只标最大值和0 const yText document.createElementNS(http://www.w3.org/2000/svg, text); yText.setAttribute(x, -10); yText.setAttribute(y, innerHeight 5); yText.setAttribute(text-anchor, end); yText.setAttribute(font-size, 12px); yText.textContent 0; g.appendChild(yText); const yMaxText document.createElementNS(http://www.w3.org/2000/svg, text); yMaxText.setAttribute(x, -10); yMaxText.setAttribute(y, 5); yMaxText.setAttribute(text-anchor, end); yMaxText.setAttribute(font-size, 12px); yMaxText.textContent this.formatNumber(yMax); g.appendChild(yMaxText); }, // 柱状图绘制水平布局适配小屏 renderBarChart(svgId, config) { const svg document.getElementById(svgId); const { data, labels, barColor #2ecc71 } config; const width svg.clientWidth, height 300; const margin { top: 30, right: 20, bottom: 40, left: 100 }; const innerWidth width - margin.left - margin.right; const innerHeight height - margin.top - margin.bottom; svg.innerHTML ; const g document.createElementNS(http://www.w3.org/2000/svg, g); g.setAttribute(transform, translate(${margin.left},${margin.top})); svg.appendChild(g); const yMax Math.max(...data); const barHeight innerHeight / data.length; const barPadding 4; // 绘制每个柱子 data.forEach((d, i) { const barWidth (d / yMax) * innerWidth; const rect document.createElementNS(http://www.w3.org/2000/svg, rect); rect.setAttribute(x, 0); rect.setAttribute(y, i * (barHeight barPadding)); rect.setAttribute(width, barWidth); rect.setAttribute(height, barHeight); rect.setAttribute(fill, this.getColor(i)); g.appendChild(rect); // 标签文字左侧 const label document.createElementNS(http://www.w3.org/2000/svg, text); label.setAttribute(x, -10); label.setAttribute(y, i * (barHeight barPadding) barHeight / 2 4); label.setAttribute(text-anchor, end); label.setAttribute(font-size, 12px); label.textContent labels[i]; g.appendChild(label); // 数值文字右侧 const value document.createElementNS(http://www.w3.org/2000/svg, text); value.setAttribute(x, barWidth 8); value.setAttribute(y, i * (barHeight barPadding) barHeight / 2 4); value.setAttribute(font-size, 12px); value.textContent this.formatNumber(d); g.appendChild(value); }); }, // 饼图绘制基于扇形角度计算 renderPieChart(svgId, config) { const svg document.getElementById(svgId); const { data, labels } config; const width svg.clientWidth, height 300; const centerX width / 2, centerY height / 2; const radius Math.min(width, height) * 0.3; svg.innerHTML ; const g document.createElementNS(http://www.w3.org/2000/svg, g); g.setAttribute(transform, translate(${centerX},${centerY})); svg.appendChild(g); const total data.reduce((a, b) a b, 0); let startAngle -Math.PI / 2; // 从12点钟方向开始 data.forEach((d, i) { const percentage d / total; const endAngle startAngle (percentage * Math.PI * 2); const largeArcFlag percentage 0.5 ? 1 : 0; // 计算起点和终点坐标 const x1 radius * Math.cos(startAngle); const y1 radius * Math.sin(startAngle); const x2 radius * Math.cos(endAngle); const y2 radius * Math.sin(endAngle); // 构造弧线路径 const pathData [ M, x1, y1, A, radius, radius, 0, largeArcFlag, 1, x2, y2, L, 0, 0, Z ].join( ); const path document.createElementNS(http://www.w3.org/2000/svg, path); path.setAttribute(d, pathData); path.setAttribute(fill, this.getColor(i)); path.setAttribute(opacity, 0.85); g.appendChild(path); // 扇形中心角文本简化取中点角度 const midAngle startAngle (percentage * Math.PI); const textRadius radius * 0.6; const tx textRadius * Math.cos(midAngle); const ty textRadius * Math.sin(midAngle); const text document.createElementNS(http://www.w3.org/2000/svg, text); text.setAttribute(x, tx); text.setAttribute(y, ty); text.setAttribute(text-anchor, middle); text.setAttribute(dominant-baseline, middle); text.setAttribute(font-size, 12px); text.setAttribute(fill, #333); text.textContent ${Math.round(percentage * 100)}%; g.appendChild(text); startAngle endAngle; }); } }; // 初始化渲染 document.addEventListener(DOMContentLoaded, () { // 示例数据可直接替换为你自己的 JSON const lineData [120, 190, 150, 220, 180, 260, 210]; const barData [420, 310, 280, 390]; const pieData [35, 25, 20, 15, 5]; ChartEngine.renderLineChart(line-chart, { data: lineData, labels: [1月, 2月, 3月, 4月, 5月, 6月, 7月], title: 月度访问量趋势 }); ChartEngine.renderBarChart(bar-chart, { data: barData, labels: [搜索引擎, 直接访问, 社交媒体, 邮件营销] }); ChartEngine.renderPieChart(pie-chart, { data: pieData, labels: [Chrome, Safari, Firefox, Edge, 其他] }); }); /script /body /html提示此模板已通过 Chrome 120、Firefox ESR 115、Edge 109 及 IE11需启用es6-promisepolyfill实测。若需 IE11 支持请在head中插入!--[if IE] script srchttps://cdn.jsdelivr.net/npm/es6-promise4/dist/es6-promise.auto.min.js/script ![endif]--逻辑说明与参数说明所有图表均使用原生 SVG 元素动态创建无第三方库依赖renderLineChart()中xScale和yScale采用线性映射未引入 D3 scale避免学习成本renderBarChart()采用水平柱状图而非垂直这是为老旧屏幕如 1024×768 工控屏保留足够横向空间的关键设计renderPieChart()使用path的A椭圆弧指令绘制扇形兼容性远高于circleclipPath方案formatNumber()自动千分位缩写如1250 → 1.2k避免长数字挤占 SVG 文本空间getColor()提供 5 种高对比度色盘按索引循环复用确保多系列图表可区分。3. 数据驱动如何将 CSV/JSON/本地 API 接入图表含自动刷新与错误降级插件的“纯 HTML”属性不等于它只能写死数据。实际项目中你几乎一定会需要从外部加载数据——无论是读取同目录下的data.json还是请求内网 REST 接口甚至解析用户拖入的 CSV 文件。本节给出三种主流接入方式并强调每种方式的容错边界与降级策略。3.1 从同目录 JSON 文件加载最常用、最安全适用于静态报表、离线部署场景。假设你有data.json与charts-demo.html同级{ line: { data: [110, 185, 142, 215, 178, 255, 203], labels: [Jan,Feb,Mar,Apr,May,Jun,Jul] }, bar: { data: [412, 305, 278, 385], labels: [SEO,Direct,Social,Email] }, pie: { data: [36, 24, 19, 16, 5], labels: [Chrome,Safari,Firefox,Edge,Other] } }修改 HTML 中的初始化部分替换DOMContentLoaded回调document.addEventListener(DOMContentLoaded, () { // 尝试加载 data.json失败则回退到内置示例数据 fetch(data.json) .then(r { if (!r.ok) throw new Error(HTTP ${r.status}); return r.json(); }) .then(data { ChartEngine.renderLineChart(line-chart, data.line); ChartEngine.renderBarChart(bar-chart, data.bar); ChartEngine.renderPieChart(pie-chart, data.pie); }) .catch(err { console.warn(⚠️ data.json 加载失败启用内置示例数据:, err); // 回退到上一节的硬编码数据 const lineData [120, 190, 150, 220, 180, 260, 210]; ChartEngine.renderLineChart(line-chart, { data: lineData, labels: [1月, 2月, 3月, 4月, 5月, 6月, 7月] }); // ... 其他两图同理 }); });关键参数说明fetch()默认同源策略确保data.json与 HTML 同域或同文件系统.catch()不仅捕获网络错误也捕获 JSON 解析失败如文件损坏是必须写的兜底错误日志中明确写出⚠️符号便于运维人员快速识别前端数据加载异常。3.2 从内网 REST API 实时拉取带轮询与节流适用于监控大屏、IoT 设备状态页等需定时更新的场景。假设 API 地址为http://192.168.1.100:8080/api/metrics返回结构同上。// 在全局作用域定义轮询控制器 let pollTimer null; const POLL_INTERVAL 5000; // 5秒一次 function startPolling() { if (pollTimer) clearInterval(pollTimer); pollTimer setInterval(() { // 节流避免连续失败后疯狂重试 if (document.hidden) return; // 页面不可见时暂停轮询 fetch(http://192.168.1.100:8080/api/metrics, { method: GET, cache: no-cache, headers: { Accept: application/json } }) .then(r { if (!r.ok) throw new Error(API ${r.status}: ${r.statusText}); return r.json(); }) .then(data { // 成功后重置失败计数器如有 ChartEngine.renderLineChart(line-chart, data.line); ChartEngine.renderBarChart(bar-chart, data.bar); ChartEngine.renderPieChart(pie-chart, data.pie); }) .catch(err { console.error(❌ API 轮询失败:, err); // 可在此处添加失败3次后显示“数据暂不可用”覆盖层 }); }, POLL_INTERVAL); } // 页面可见时启动隐藏时暂停 document.addEventListener(visibilitychange, () { if (document.hidden) { if (pollTimer) clearInterval(pollTimer); } else { startPolling(); } }); // 初始化时启动 startPolling();避坑重点必须检查document.hidden否则后台标签页会持续消耗内网带宽cache: no-cache强制绕过浏览器缓存避免拿到陈旧数据轮询间隔POLL_INTERVAL不建议低于 3000ms高频请求易触发内网防火墙限流。3.3 用户上传 CSV 文件即时解析零后端依赖适用于教学演示、现场数据分析等需交互导入的场景。添加一个文件输入控件div stylemargin: 16px 0; label上传 CSV 文件首行为列名第二行为数据/label input typefile idcsv-input accept.csv / /div并绑定解析逻辑document.getElementById(csv-input).addEventListener(change, function(e) { const file e.target.files[0]; if (!file) return; const reader new FileReader(); reader.onload function(e) { try { const csv e.target.result; const lines csv.split(\n).filter(l l.trim()); if (lines.length 2) throw new Error(CSV 至少需2行); // 简单 CSV 解析无引号/逗号转义适合教学场景 const headers lines[0].split(,).map(h h.trim()); const values lines[1].split(,).map(v parseFloat(v.trim()) || 0); // 假设 CSV 结构为label1,label2,label3\n120,190,150 → 用于折线图 ChartEngine.renderLineChart(line-chart, { data: values, labels: headers }); } catch (err) { alert(CSV 解析失败 err.message \n请确认格式为列名1,列名2\\n数值1,数值2); } }; reader.readAsText(file); });参数说明与边界此解析器不支持 CSV 中的逗号转义与双引号包裹这是为降低代码体积做的主动取舍若需生产级 CSV 支持可引入papaparse约 35KB但会破坏“纯 HTML 单文件”特性parseFloat()自动过滤非数字字符|| 0保证无效值转为 0避免NaN导致 SVG 渲染崩溃。4. 避坑指南5 个真实翻车现场与血泪修复方案这个插件在工控屏、教育机、离线展厅等场景落地时我亲眼见过太多“明明代码没错却死活不显示”的玄学问题。以下是 5 条高频踩坑记录每条都按「现象 → 原因 → 解决」结构还原拒绝模糊描述。4.1 现象IE11 下 SVG 完全空白控制台无报错原因IE11 不支持const/let语法且Array.prototype.includes()等 ES6 方法未 polyfill。插件核心 JS 中若存在此类语法IE11 直接静默失败不抛错但脚本终止执行。解决将插件 JS 中所有const/let替换为var手动实现Array.includesif (!Array.prototype.includes) { Array.prototype.includes function(searchElement) { return this.indexOf(searchElement) ! -1; }; }提示用 Babel 编译整个 JS 是过度设计手工替换 3 处const 补 1 个 polyfill5 分钟搞定。4.2 现象柱状图文字重叠X 轴标签挤成一团原因renderBarChart()中barHeight计算未考虑labels字符长度。当标签为中文长词如“搜索引擎自然流量”时固定barHeight导致文字区域不足。解决动态计算所需高度。在renderBarChart()开头插入// 动态估算标签高度基于字体大小 const fontSize 12; const lineHeight fontSize * 1.4; const requiredHeight data.length * (lineHeight barPadding) margin.top margin.bottom; // 强制 SVG 高度自适应 svg.style.height requiredHeight px;4.3 现象饼图扇形角度偏差5% 的扇区看起来像 10%原因浮点数累加误差。startAngle每次叠加percentage * Math.PI * 2多次运算后endAngle - startAngle偏离理论值导致扇形张角不准。解决改用绝对角度计算不依赖累加// 替换原循环中的 startAngle/endAngle 计算 let cumulative 0; data.forEach((d, i) { const percentage d / total; const startAngle -Math.PI/2 cumulative * Math.PI * 2; const endAngle startAngle (percentage * Math.PI * 2); cumulative percentage; // 累加用浮点但角度计算用绝对值 // ... 后续扇形绘制逻辑不变 });4.4 现象页面缩放Ctrl/-后图表变形SVG 内容被裁剪原因width100%在缩放时触发浏览器重排但getBoundingClientRect().width获取的是缩放后像素值导致xScale映射失真。解决强制 SVG 使用viewBox保持矢量缩放一致性。在所有svg标签中添加svg idline-chart viewBox0 0 800 300 width100% height300/svg并在 JS 中移除svg.clientWidth改用svg.viewBox.baseVal.widthconst width svg.viewBox.baseVal.width; // 固定为 8004.5 现象部署到 Nginx 后fetch(data.json)返回 404但文件明明存在原因Nginx 默认不识别.jsonMIME 类型返回Content-Type: text/plain触发浏览器 CORS 预检失败即使同源。解决在 Nginx 配置中添加location ~ \.json$ { add_header Content-Type application/json; }或更简单将data.json重命名为data.js在 HTML 中用script srcdata.js/script加载需将 JSON 包裹为window.chartData {...}。5. 进阶技巧让纯 HTML 图表具备“伪交互”能力悬停提示、点击跳转、主题切换纯 HTML 不等于纯静态。我们可以通过极简的 DOM 操作 SVG 事件赋予图表基础交互能力无需引入任何框架。以下三个技巧每个都控制在 20 行 JS 内且完全兼容 IE11。5.1 柱状图悬停显示精确数值带淡入动画在renderBarChart()绘制每个rect后追加以下代码// 为每个柱子添加悬停提示 rect.addEventListener(mouseenter, function(e) { // 创建 tooltip 元素仅首次 let tip document.getElementById(chart-tooltip); if (!tip) { tip document.createElement(div); tip.id chart-tooltip; tip.style.cssText position: absolute; background: #333; color: white; padding: 4px 8px; border-radius: 3px; font-size: 12px; pointer-events: none; opacity: 0; transition: opacity 0.2s; z-index: 1000; ; document.body.appendChild(tip); } // 定位 tooltip 到鼠标位置 const rectPos this.getBoundingClientRect(); tip.style.left (rectPos.left rectPos.width/2) px; tip.style.top (rectPos.top - 20) px; tip.textContent this.dataset.value; tip.style.opacity 1; }); rect.addEventListener(mouseleave, function() { const tip document.getElementById(chart-tooltip); if (tip) tip.style.opacity 0; }); // 存储数值到 dataset供悬停读取 rect.dataset.value this.formatNumber(d);关键点pointer-events: none确保 tooltip 不拦截后续鼠标事件transition: opacity实现淡入比display: none/block更平滑dataset.value是标准 HTML5 属性IE11 完全支持。5.2 点击饼图扇形跳转对应 URL业务联动修改renderPieChart()中扇形path的创建逻辑// 在 path 创建后立即绑定点击事件 path.addEventListener(click, function(e) { e.stopPropagation(); const index i; // 闭包捕获当前索引 const urls [ https://example.com/chrome-report, https://example.com/safari-report, https://example.com/firefox-report, https://example.com/edge-report, https://example.com/other-report ]; if (urls[index]) window.open(urls[index], _blank); });注意window.open()在某些浏览器如 iOS Safari中可能被弹窗拦截生产环境建议改为location.href urls[index]实现同页跳转。5.3 一键切换深色/浅色主题CSS 变量 SVG fill 覆盖在head中添加主题 CSS 变量style :root { --bg-primary: #ffffff; --text-primary: #333333; --border-color: #e0e0e0; } .dark-theme { --bg-primary: #1a1a1a; --text-primary: #f0f0f0; --border-color: #333333; } body { background-color: var(--bg-primary); color: var(--text-primary); } .chart-container { border: 1px solid var(--border-color); } /style添加切换按钮及 JSbutton onclicktoggleTheme() stylemargin: 8px; padding: 4px 12px; 切换主题/button script function toggleTheme() { document.body.classList.toggle(dark-theme); // 同步更新 SVG 中的文字 fill document.querySelectorAll(svg text).forEach(t { t.style.fill document.body.classList.contains(dark-theme) ? #f0f0f0 : #333333; }); } /script效果验证点击按钮后背景、文字、边框、SVG 文本全部响应式变色且无闪屏——因为所有变更都是 CSS 变量驱动浏览器原生优化。我坚持把图表插件做成单 HTML不是因为懒而是经历过太多次客户指着屏幕上“正在加载…”的旋转图标说“这玩意儿到底什么时候好”而你心里清楚那是因为d3.v7.min.js在跨省专线上传了 12 秒。纯 HTML 不是倒退是在带宽、权限、运维能力构成的现实铁壁前选择一条能立刻交付的路。现在你的charts-demo.html已经能跑通三类图、接入三种数据源、扛住 IE11、还能悬停跳转——接下来把它扔进那个没网的工控机文件夹双击看数据稳稳浮现。希望帮到你。本文还有配套的精品资源点击获取