资讯动态

用Echarts拟合正态分布曲线:前端统计计算与可视化实践

发布时间:2026/10/1 6:15:48 来源:尧图企业网站定制
前阵子有个做质量分析的同事找我给我扔了一堆产品检测数据说想看看这些数据的分布情况是不是符合预期。我第一反应是用直方图但他一句话把我问住了“直方图只能看出大概落在哪个区间能不能画出一条理论分布曲线叠上去最好能对比实际值和正态分布的差距。”这个需求很常见但在前端领域Matplotlib 里一句stats.norm.pdf就能搞定的事换到 Echarts 里却没有直接可用的现成图表类型——它不像折线图、饼图那样开箱即用。于是就有了这篇文章用 Echarts 把任意一组原始数据自动拟合成一条正态分布曲线。这个需求覆盖的质量分析、考试成绩分布、流量波动监控、用户行为周期分析等场景非常广泛只要你手头有一堆原始观测值想直观判断“这堆数据靠不靠近正态分布”这套方案就能直接套用。我把整个实现过程拆成了几个层面统计指标如何计算、概率密度函数如何在工程上简化、Echarts 曲线如何配置、真实数据会出现哪些坑。只要照着做无论什么领域的数据都能快速生成一张带均值线、标准差区间和概率密度曲线的正态分布图。1. 先解决一个认知问题正态分布图不是 Echarts 的内置图表类型很多人第一次接到这个需求时第一反应是去 Echarts 官方文档里搜“正态分布图”搜半天发现根本没这个类型。这不奇怪Echarts 本质上是一个通用可视化库它提供的是折线图、柱状图、散点图、饼图等基础图形元素而正态分布曲线本质上是“由无数个坐标点连成的光滑曲线”——所以它的底层实现就是折线图只不过数据点不是直接来自业务数据库而是来自概率密度函数的计算。这也解释了为什么网上很多教程都在绕圈子有的用柱状图模拟直方图再去叠加一条散点图拟合线有的用 ECharts 的markLine标注几个标准差的位置就草草收场还有的直接在服务端用 Python 算好坐标点返回前端。这些方案都可行但它们都绕开了一个最核心的问题前端如何根据“任意原始数据”自行拟合出正态分布曲线。如果服务端算那么每次数据更新都要请求后端不够灵活如果只画固定公式的曲线那和真正的业务数据就完全脱节了。所以正确的思路是前端拿到原始数据数组后先用统计学公式计算出均值和标准差再依据正态分布概率密度函数生成一组曲线坐标点最后用 Echarts 的折线图去渲染。前两步是数学问题最后一步是可视化问题两者通过一个中间数据结构衔接——[{x: 值, y: 密度}, ...]。我建议把这个实现封装成一个独立的纯函数模块不依赖任何 UI 框架。这样不管你在用原生 JS、jQuery、Vue、React 还是和大屏框架配合直接引入调用即可。下面这张表先概括整个方案的技术构成模块层次作用关键技术点数据预处理过滤原始数据中的异常值、空值数组过滤、类型判断统计计算计算均值、标准差样本标准差公式、无偏估计曲线采样生成概率密度函数坐标点概率密度函数、区间采样图表渲染绘制曲线、面积、标注线Echarts 折线图、markLine、markArea交互增强悬浮提示统计信息formatter 回调、HTML 模板这个分层也对应了我下面几个章节的顺序。先把数学工具准备好再讲工程实现最后谈避坑一步步来。2. 正态分布背后的统计学原理均值、标准差与概率密度函数的关系2.1 概率密度函数的真正含义正态分布又叫高斯分布它的概率密度函数Probability Density Function, PDF长这样[ f(x) \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{(x-\mu)^2}{2\sigma^2}} ](\mu) 是均值决定了曲线的中心位置(\sigma) 是标准差决定了曲线的“胖瘦”。(\sigma) 越大曲线越扁平数据越分散(\sigma) 越小曲线越尖峰数据越集中。生活化的理解方式把一群人的身高数据画成分布曲线均值告诉你这群人平均多高标准差告诉你高矮差异有多悬殊——标准差小说明大家身高都差不多标准差大说明有特别高或特别矮的人。但很多人忽略了一个关键性质概率密度函数的值并不直接等于“概率”它只是“密度”。真正计算某个区间内数据出现的概率需要对这个函数做定积分。工程上我们不大会真的去做积分因为绝大多数场景下我们只是需要“看到曲线形状”用来和数据直方图做对比看实际数据是否符合正态分布、偏差在哪里、数据是否集中在这个区间内。所以只要按照 PDF 公式采样点坐标即可。2.2 均值和标准差的计算方式均值的计算很简单所有数据求和除以数量function calcMean(data) { const sum data.reduce((acc, val) acc val, 0); return sum / data.length; }标准差则稍微隐蔽一点。这里有两种选择总体标准差除以 n和样本标准差除以 n - 1。实际处理业务数据时我们几乎永远拿不到“总体”拿到的都是抽样样本比如生产线上抽检的 100 个产品、一个月内每天的用户访问量。用样本标准差更科学它通过减去一个自由度来修正小样本下对总体标准差低估的偏差。这也是 Excel 中STDEV.S和STDEV.P的区别很多人不注意这个细节导致画出来的曲线比实际更窄、更尖。function calcStdDev(data, mean) { if (data.length 2) return 0; const variance data.reduce((acc, val) acc Math.pow(val - mean, 2), 0) / (data.length - 1); return Math.sqrt(variance); }当数据量较大时比如成千上万条n 和 n-1 的差异微乎其微但数据量只有几十条时这个差异会在曲线形状上明确体现出来所以写代码时我建议默认用 n-1。2.3 为什么实际工程不需要柯尔莫哥洛夫检验这类复杂判断正规的统计分析中要判断一组数据是否符合正态分布通常是做 Shapiro-Wilk 检验或 Kolmogorov-Smirnov 检验甚至还要看偏度和峰度系数。但在前端可视化场景里这种做法的性价比很低一方面这类检验的计算复杂度不低不适合在大数据量下交给浏览器主线程另一方面产品和技术团队想要的往往不是“数据是否显著符合正态分布”这个结论而是“我先看一眼曲线长什么样”一个直观的视觉判断往往比一个 p 值更有说服力。所以在这套方案中我只关注两件事一是算出准确的均值和标准差二是画出正确的概率密度曲线。至于样本到底符合不符合正态分布让看图表的人自己判断。基于常见实践我建议在数据量小于 30 个时给出提示——“样本量偏少拟合结果可能不稳定”但不阻止绘图。30 这个数字来自中心极限定理的常见经验阈值虽然它不是严格的数学界限但作为提示阈值非常实用。3. 核心算法如何把“任意数据”转换为概率密度曲线采样点3.1 采样范围的确定μ ± 4σ 是最合理的边界数学上正态分布曲线的定义域是负无穷到正无穷但实际绘图不可能画到无穷远。大多数教材上的正态分布图都只画到 (\mu \pm 3\sigma)因为在 (\mu \pm 3\sigma) 范围内已覆盖了约 99.73% 的面积数据量。但这里有个视觉问题如果边界只取到 (\mu \pm 3\sigma)曲线的两端会显得“突然被截断”——因为 3σ 处概率密度并不是零直接切掉会让曲线像被剪刀剪过一样。而取 (\mu \pm 4\sigma) 时3σ 以外的尾部还有一段平滑过渡下降的距离4σ 处概率密度已经非常接近零视觉效果自然得多。4σ 覆盖了约 99.994% 的数据对于 Echarts 渲染来说再多就是纯粹浪费坐标点了。采样间隔由采样点数决定。如果等差地取 200 个点间隔就是 ((8\sigma) / 200 0.04\sigma)这个粒度生成的多边形折线足够光滑人眼几乎看不出贝塞尔曲线和平滑曲线的区别。3.2 核心函数实现/** * 根据原始数据生成正态分布曲线采样点 * param {Arraynumber} data 原始数据数组 * param {number} [points200] 采样点数默认200 * returns {{ points: Array[number, number], mean: number, stdDev: number }} */ function generateNormalDistribution(data, points 200) { // 1. 过滤掉无效数据 const validData data.filter( (val) typeof val number isFinite(val) !isNaN(val) ); if (validData.length 0) { throw new Error(数据为空或没有有效数值); } // 2. 计算均值和样本标准差 const mean calcMean(validData); const stdDev calcStdDev(validData, mean); const effectiveStdDev stdDev 0 ? 1 : stdDev; // 防止标准差为0导致除零错误 // 3. 确定采样范围 const minX mean - 4 * effectiveStdDev; const maxX mean 4 * effectiveStdDev; const step (maxX - minX) / (points - 1); // 4. 按概率密度函数计算每个采样点的y值 const pointsArr []; for (let i 0; i points; i) { const x minX i * step; const exponent -Math.pow(x - mean, 2) / (2 * effectiveStdDev * effectiveStdDev); const y (1 / (effectiveStdDev * Math.sqrt(2 * Math.PI))) * Math.exp(exponent); pointsArr.push([parseFloat(x.toFixed(4)), parseFloat(y.toFixed(6))]); } return { points: pointsArr, mean, stdDev: effectiveStdDev }; }这里有几个值得解释的设计决定为什么用isFinite和isNaN双重过滤isFinite能把Infinity和-Infinity排除掉isNaN能把NaN排除。比如数据里混入了后端返回的null、undefined、空字符串等typeof val number先把非 number 类型全部挡住剩下的再过滤特殊数值。这个顺序必须先判断类型再做值判断否则null会在某些隐式转换下混过去。toFixed的作用这是为了控制坐标点精度。原始数据如果保留太多小数位会产生大量视觉上无差异的重复坐标点对 Canvas 渲染也是负担。x保留 4 位小数、y保留 6 位小数在绝大多数场景下足够精确——y的单位通常很小保留 6 位才能看得出曲线的尾部变化。标准差为 0 时的降级处理如果数据全部是同一个值比如 100 条数据都是 5标准差为 0PDF 公式会出现除以零。把标准差强制设为 1 后画出来的是一条均值为该值的尖峰曲线虽然数学上不严谨但至少 UI 不报错而且这种数据的分布问题本身就是业务侧需要关注的异常情况曲线正好能直观暴露它。3.3 为什么要返回[x, y]二元组数组Echarts 的折线图数据集有两种常见格式一种是data: [y1, y2, y3]x 轴依赖类目或xAxis的data另一种是data: [[x1, y1], [x2, y2], ...]。对于连续型数据的分布曲线强烈推荐第二种原因有三个正态分布曲线的 x 轴是连续的数值比如 99.5、99.7、100.1用类目轴去表示会把它们当成离散的字符串图表的坐标轴含义就错了。直接使用二元组数组时Echarts 的xAxis和yAxis都设置成type: value即可无需再维护一个平行的 x 轴数据数组数据同步性更好。这个格式和markPoint的coord、markLine的xAxis数值标注天然兼容后面添加均值线、标准差区间时几乎不用额外转换。4. Echarts 配置详解让正态分布曲线真正具备可读性4.1 用折线图承载密度曲线而不是散点图核心实现选型上我最终选择的是line系列配合以下配置option { xAxis: { type: value, name: 数值区间, nameLocation: middle, nameGap: 28 }, yAxis: { type: value, name: 概率密度, nameLocation: middle, nameGap: 42 }, series: [{ type: line, data: distributionPoints, smooth: true, symbol: none, lineStyle: { width: 3, color: #3f7fbf }, areaStyle: { color: { type: linear, x: 0, y: 0, x2: 0, y2: 1, colorStops: [ { offset: 0, color: rgba(63, 127, 191, 0.35) }, { offset: 1, color: rgba(63, 127, 191, 0.02) } ] } } }] };关键配置项的取舍逻辑smooth: true曲线平滑。Echarts 底层用贝塞尔曲线把相邻数据点衔接起来视觉上比折线要自然得多。不过要注意平滑是视觉上的插值不代表概率密度值被修改了只是为了好看。symbol: none隐藏每个采样点的小圆点。200 个圆点画上去非常杂乱这个必须设成none。areaStyle用渐变透明的面积图这样能把曲线下方区域填充出来读图的人可以直观对比不同区间的面积大小也就是概率高低。渐变方向是自上而下从半透明到几乎透明避免大面积色块遮挡柱状图如果你后续叠加了直方图。lineStyle.width: 3密度曲线往往只有一条稍微加粗一点比较像统计图里的分布曲线太细会显得单薄。4.2 用 markLine 标记均值与标准差位置正态分布图最核心的信息就是“峰值在哪里”和“数据有多分散”。用markLine分别标记均值 (\mu) 和 (\mu \pm \sigma) 三条竖直参考线读图效率会大幅提升markLine: { silent: true, symbol: none, label: { formatter: (params) { if (params.value mean) return 均值 μ; if (Math.abs(params.value - (mean stdDev)) 0.001) return μ σ; if (Math.abs(params.value - (mean - stdDev)) 0.001) return μ - σ; return ; }, position: insideEndTop }, lineStyle: { type: dashed, width: 1.5 }, data: [ { xAxis: mean, lineStyle: { color: #d9534f } }, { xAxis: mean - stdDev, lineStyle: { color: #f0ad4e } }, { xAxis: mean stdDev, lineStyle: { color: #f0ad4e } } ] }label 的 formatter 返回带希腊字母的文本时有个隐藏问题如果项目里没有引入额外的字体库部分操作系统对μσ字符渲染没问题但到了 Linux 服务器上的部分渲染环境可能显示成方框。稳妥的做法是在 HTML 里就用mu;实体替代或者在 formatter 里直接返回中文“均值”“标准差线”避免字符编码带来的风险。4.3 用 markArea 标注 ±1σ、±2σ 的置信范围对于想快速判断数据集中程度的读者把 ±1σ 和 ±2σ 区间用不同深浅的色块标注出来是最直观的表达。±1σ 范围覆盖约 68.27% 的数据±2σ 覆盖约 95.45%这两个色块一画不需要任何统计学基础就能理解“大部分数据落在哪个范围内”。markArea: { silent: true, itemStyle: { color: rgba(63, 127, 191, 0.06) }, data: [ [ { xAxis: mean - stdDev, itemStyle: { color: rgba(63, 127, 191, 0.12) } }, { xAxis: mean stdDev } ], [ { xAxis: mean - 2 * stdDev, itemStyle: { color: rgba(63, 127, 191, 0.05) } }, { xAxis: mean 2 * stdDev } ] ] }markArea的 data 项是一个二维数组每项代表一块矩形区域的左上角坐标和右下角坐标。因为 x 轴是 value 类型所以用xAxis绑定数值即可y 轴默认延伸到整个绘图区。设置silent: true是为了避免区域块响应鼠标事件防止拖动大屏时出现意外的交互闪烁。4.4 tooltip 格式化一行一个统计量并解决换行问题这篇博文的标题是“任意数据”所以 tooltip 里最好能动态显示当前数据的一些统计摘要。默认的 tooltip 在 value 轴上只显示 x、y 两个值我们需要扩展成均值、标准差、当前 x 位置对应的概率密度以及“该点落在哪个标准差区间内”。这里有个容易踩的坑formatter里如果返回 HTML 字符串使用\n换行是无效的必须用br/标签。Echarts 的 tooltip 默认是富文本 HTML 渲染\n会被当成普通空白字符。而且如果 tooltip 内容太多导致超出图表容器边界还会出现被裁剪的问题可以在tooltip.extraCssText里加上white-space: nowrap或者自定义最大宽度tooltip: { trigger: axis, extraCssText: max-width: 260px; white-space: normal; line-height: 1.6;, formatter: (params) { const xVal params[0].value[0]; const yVal params[0].value[1]; const sigmaDistance ((xVal - mean) / stdDev).toFixed(2); const sigmaLabel Math.abs(sigmaDistance) 0.01 ? 均值处 : μ ${sigmaDistance 0 ? : -} ${Math.abs(sigmaDistance)}σ; return [ div stylefont-weight:bold;margin-bottom:4px;正态分布/div, div数值${xVal}/div, div概率密度${yVal}/div, div均值 μ${mean.toFixed(2)}/div, div标准差 σ${stdDev.toFixed(2)}/div, div位置${sigmaLabel}/div ].join(); } }这里对“位置”的判断是个很实用的细节把当前 hover 的 x 值和均值之间的距离换算成几个标准差用户能立刻感知到这个数值在整条分布中的相对位置。比如 tooltip 显示“μ 1.56σ”说明当前值明显偏右大概率属于取值较大的少数群体。4.5 完整的可运行示例把上述所有配置拼合起来你可以直接保存成 HTML 文件打开运行!DOCTYPE html html langzh-CN head meta charsetUTF-8 / titleEcharts 正态分布图/title script srchttps://cdn.jsdelivr.net/npm/echarts5/dist/echarts.min.js/script /head body stylemargin:0;padding:20px;background:#f6f7f9; div idchart stylewidth:100%;height:520px;background:#fff;border-radius:8px;/div script // 统计计算 function calcMean(data) { const sum data.reduce((acc, val) acc val, 0); return sum / data.length; } function calcStdDev(data, mean) { if (data.length 2) return 0; const variance data.reduce((acc, val) acc Math.pow(val - mean, 2), 0) / (data.length - 1); return Math.sqrt(variance); } function generateNormalDistribution(data, points 200) { const validData data.filter( (val) typeof val number isFinite(val) !isNaN(val) ); if (validData.length 0) throw new Error(数据为空或没有有效数值); const mean calcMean(validData); const stdDev calcStdDev(validData, mean); const effectiveStdDev stdDev 0 ? 1 : stdDev; const minX mean - 4 * effectiveStdDev; const maxX mean 4 * effectiveStdDev; const step (maxX - minX) / (points - 1); const pointsArr []; for (let i 0; i points; i) { const x minX i * step; const exponent -Math.pow(x - mean, 2) / (2 * effectiveStdDev * effectiveStdDev); const y (1 / (effectiveStdDev * Math.sqrt(2 * Math.PI))) * Math.exp(exponent); pointsArr.push([parseFloat(x.toFixed(4)), parseFloat(y.toFixed(6))]); } return { points: pointsArr, mean, stdDev: effectiveStdDev }; } // 模拟原始数据 function generateSampleData(count 500, mean 100, stdDev 12) { const data []; for (let i 0; i count; i) { // Box-Muller 变换生成近似正态分布随机数 const u1 1 - Math.random(); const u2 Math.random(); const z Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2); data.push(mean z * stdDev); } return data; } const rawData generateSampleData(500, 100, 12); const { points, mean, stdDev } generateNormalDistribution(rawData, 200); // Echarts 配置 const chartDom document.getElementById(chart); const myChart echarts.init(chartDom); const option { title: { text: 数据分布与正态拟合曲线, subtext: 样本数${rawData.length}均值${mean.toFixed(2)}标准差${stdDev.toFixed(2)}, left: center, textStyle: { fontSize: 16 } }, tooltip: { /* 按 4.4 节配置 */ }, xAxis: { type: value, name: 数值, nameLocation: middle, nameGap: 28 }, yAxis: { type: value, name: 密度, nameLocation: middle, nameGap: 42 }, series: [{ type: line, data: points, smooth: true, symbol: none, lineStyle: { width: 3, color: #3f7fbf }, areaStyle: { /* 按 4.1 节配置 */ }, markLine: { /* 按 4.2 节配置 */ }, markArea: { /* 按 4.3 节配置 */ } }], grid: { top: 80, left: 60, right: 40, bottom: 60 } }; myChart.setOption(option); // 响应窗口大小变化 window.addEventListener(resize, () myChart.resize()); /script /body /html这里的generateSampleData用了 Box-Muller 变换生成符合正态分布的模拟数据这是概率统计里非常经典的随机数生成方法。实际业务中你不需要这部分直接把原始测量数据扔进generateNormalDistribution即可。5. 实测中的坑从数据预处理到视觉呈现的排障记录5.1 数据量小到样本标准差失真怎么办前面提到样本量小于 30 时拟合不稳定实际踩坑时你会发现 30 条和 300 条数据画出的曲线观感差异非常大。极端情况下只有 5 条数据时标准差的估计可能完全是噪音曲线形状可能会与数据的真实分布相去甚远。我在项目里给这个场景做了一个温和的提示当数据量小于 30 时在图表区域上方显示一条浅黄色的 warning 语句——“样本量偏少正态拟合结果仅供参考”。不打断流程但提前打了心理预防针。与其在数据量不足时强行展示一条看似精确的曲线不如诚实告诉读者这条曲线的置信度有限。5.2 数据里有离群值曲线被“拉平”了这是实际业务数据中最常遇到的问题。比如分析一批产品重量的分布正常情况下数据集中在 100g 左右标准差可能只有 2g但记录里混了一两条实验阶段的 200g 异常记录计算结果瞬间被带偏均值被拉到 102g标准差被拉到 12g曲线变得又宽又扁实际数据密集区反而看不清楚了。一种简单的工程化处理方式是做基于分位数的截断function filterOutliers(data, k 1.5) { const sorted [...data].sort((a, b) a - b); const q1 sorted[Math.floor(sorted.length * 0.25)]; const q3 sorted[Math.floor(sorted.length * 0.75)]; const iqr q3 - q1; const lowerBound q1 - k * iqr; const upperBound q3 k * iqr; return data.filter((val) val lowerBound val upperBound); }这是统计学中经典的 IQR 四分位距法(k1.5) 是常用阈值。但注意这个操作要谨慎使用如果你的业务场景本身就是在监控离群值比如故障检测过滤掉异常点反而会让你看不到问题。我在实际项目中把“是否过滤离群点”做成了图表配置项默认关闭给业务方一个可选的开关。5.3 双峰分布的数据画正态曲线形态会很怪还有一种不太容易被注意的问题数据本身可能是双峰分布比如“早班和晚班的产量数据混在一起”画出来的概率密度曲线会非常扁平看起来跟数据完全不吻合。这种情况再如何调整 sigma 也无法拟合出好的效果。排查思路很简单先用 Echarts 的bar系列画一个原始数据的直方图和正态曲线叠加在一起。如果直方图明显有两个峰说明数据实际上不是单峰正态分布此时正确的业务动作是拆分数据源不要硬拿全部数据去做正态拟合。这种“正态图直方图”的叠加方案其实非常适合生产环境既能看到真实数据的形态又能看到理论分布的差异两者一对照洞察立刻就有了。5.4 采样点太多导致 Echarts 渲染卡顿我最早实现时直接把采样点数设成了 5000想着点越多曲线越精细。结果在数据大屏场景下当图表数量较多或需要动画过渡时能感觉到明显卡顿。问题在于Echarts 对smooth曲线会在相邻点之间插入贝塞尔控制点5000 个数据点意味着生成上万段曲线指令Canvas 的绘制开销完全不必要。实际上200 个点已经足够让曲线在人眼分辨率下接近完美500 点已经是强迫症级别的追求。一个值得参考的量化标准假设图表渲染区域宽度为 800px那么超过 800 个采样点的信息已经超过了屏幕的物理像素分辨率再增加点数只是浪费计算资源。我把默认值设在 200如果遇到高分辨率大屏比如 4K可以调到 400但基本不会超过 500。5.5 图表在 Vue3 项目中使用 rem 适配导致的问题热词里有一个现象“pxtorem 对 echarts 没起到效果 vue3”。这个问题在实践中确实存在。核心原因是Echarts 基于 Canvas 绘制图表尺寸是由init(dom)时的容器宽高决定的而不是由 CSS 字体大小决定的。rem 适配站在 CSS 层面对 Canvas 内部的绘制坐标没有任何影响。如果大屏容器宽度是 1920pxChart 容器初始化时按 1920 计算适配方案只是缩放了 CSS 像素Canvas 内部的像素密度并不会跟着变。解决办法是在大屏适配函数里同时主动调用myChart.resize()并且在resize时根据当前缩放比例显式设置容器和图表尺寸不能指望 CSS 缩放自动同步。如果你用基于 rem 的缩放方案建议配合window.addEventListener(resize, () myChart.resize())并传入新的尺寸参数或者干脆用 Echarts 的media特性做响应式配置。这个话题展开又是另一篇文章这里先点明方向。6. 进阶用法多组数据对比和图表自动联动6.1 两条或多条正态曲线叠加对比实际业务中经常要比较两组数据的分布差异比如“改进前后的产品一致性对比”“ A/B 两个方案的转化率波动”。只要对两组数据分别调用generateNormalDistribution得到两组坐标点然后在 Echarts 的series数组里添加两个line系列即可叠加展示const resA generateNormalDistribution(dataA, 200); const resB generateNormalDistribution(dataB, 200); series: [ { type: line, name: 方案A, data: resA.points, smooth: true, symbol: none, lineStyle: { width: 2.5, color: #3f7fbf } }, { type: line, name: 方案B, data: resB.points, smooth: true, symbol: none, lineStyle: { width: 2.5, color: #e67e22 } } ]这时关键卖点就出来了只看一圈重叠程度就能判断两组数据的均值和离散度差异。如果方案B的曲线整体比方案A偏右且更窄说明方案B平均值更高、波动更小这比看纯数字表格直观得多。6.2 配合原始数据直方图一起看曲线很重要但千万不要忽略原始数据本身。把原始数据按区间切分成柱状图直方图再在同一个图表里叠加正态曲线才是完整的数据分布分析视图。Echarts 的bar系列配合xAxis: type: value需要使用histogram效果通常做法是先对数据做分箱bin统计得到[{x: 区间中点, y: 频数}]再把它画成柱状图。不过要注意直方图的 y 轴是频数密度曲线的 y 轴是概率密度两者量纲不同。要让它们可比需要对直方图做归一化处理或用双 y 轴。这个内容可以单独写一篇我在实际项目中通常是让开发同学在服务端分好箱、返回频数前端只负责展示曲线和柱状图。6.3 与后端数据联动的思路如果你是通过 jQuery/Ajax 从后端拉数据这套方案的结构也能很平滑地嵌入。后端只需要返回一个原始数据数组前端拿到后用generateNormalDistribution计算出曲线点再setOption。也可以在数据量大到前端计算吃力时让后端直接用同样的公式算好坐标点返回。不过我个人更推荐前端算一来减轻接口传输压力二来图表交互比如切换正态性过滤、调整采样点数可以即时响应不用再请求后端。结合热搜词里那条“将原生 js、jquery、ajax、echarts 结合制作网页”这套实现天然就支持先用$.ajax或fetch获取数据然后交给generateNormalDistribution计算最后myChart.setOption()渲染。整个过程不依赖框架和 jQuery/Ajax 的配合非常顺滑。7. 从这套实现中沉淀下来的一个通用方法论写了这么多年 Echarts最大的体会是Echarts 本身不限制你能画什么它限制的是你的思维方式。遇到“没有现成图表类型”的需求时第一反应不要是“这做不了”或“找个专门的统计库”而是先拆解这个图表本质上是哪种基础图表的组合概率密度曲线本质上就是折线图只是数据生成方式不同带置信区间的曲线本质上就是折线图 markArea多组对比本质上就是多个 line 系列叠加。把需求拆到 Echarts 的基础元素层大部分看起来复杂的可视化需求都能找到解法。用 Echarts 做正态分布图真正难的点自始至终不在 Echarts 本身而在对统计概念的理解和工程化落地的取舍。你理解了均值、标准差、概率密度函数之间的关系知道怎么把数学公式转成采样点再把采样点交给 Echarts 渲染其他的都只是参数调节问题。最后再分享一个我实际操作中形成的习惯写完这套工具函数后我会先用一组已知参数的数据做测试。比如生成 10000 个均值为 100、标准差为 15 的数据看计算出来的均值和标准差是否非常接近 100 和 15再用 Echarts 画出来看峰值位置是否正确。先用模拟数据验证逻辑再上真实业务数据能省下大量排查问题的时间。这个思路适用于几乎所有数据处理类的可视化开发——先把数学算算对再让图形变得好看。

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

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

免费获取报价 →
↑