简介:这是一份轻量级、开箱即用的纯前端图表可视化插件,面向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 id="line-chart" width="600" height="300"></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 id="bar-chart" width="600" height="300" style="border: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 里右键“检查元素”就能看到x="120"——这对现场快速排障意义重大。
参数说明:
barWidth和spacing控制柱宽与间距,建议按width / (data.length * 1.5)反推初始值;rx="4"是圆角半径,设为 0 即直角柱;text-anchor="middle"让文字水平居中对齐柱子。
2.3 用 SVG path 绘制饼图:用扇形路径指令(A)替代 canvas arc
<svg id="pie-chart" width="300" height="300" viewBox="0 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属性画饼图!SVG<circle>只能画整圆。必须用<path>+A指令。另外viewBox="0 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 src="xxx.js">中安全执行的模式,且天然避免变量污染全局。
链式调用的价值:让调用方代码像这样干净:
<div id="my-chart"></div> <script src="html-charts.min.js"></script> <script> new HtmlChart('my-chart') .setConfig({ type: 'bar', data: [12, 35, 28, 42], options: { title: '月度故障数' } }) .render(); </script>
3.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(`第${i+1}周`, 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是布尔开关,默认false;options.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] || `系列${g+1}`; 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-baseline="middle"让文字垂直居中更准,比y=centerY手动调更可靠;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 src="xxx.js">,改用内联脚本。把html-charts.min.js内容复制进<script>标签,确保整个 HTML 是单文件。虽然失去缓存,但换来 100% 可运行。
4.2 现象:IE11 下饼图扇形全部错位,canvas 折线图线条断裂
原因:IE11 不支持Math.PI * 2这种写法(会报Invalid character),也不支持const声明,且canvas.getContext('2d')在某些旧显卡驱动下返回 null。
解决:
- 全局替换
const→var; Math.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,改用绝对定位div+touchstart事件:
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 * {
本文还有配套的精品资源,点击获取