ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Vue ECharts 折线图:平滑曲线、过冲与数据拟合实战

Vue ECharts 折线图:平滑曲线、过冲与数据拟合实战 产品经理指着屏幕说这条线太硬了能不能柔和一点我顺手加了smooth: true结果曲线像喝多了最低点直接跑到负值区间业务方当场问我们销量怎么可能为负。这件事之后我把 Vue 里引入 ECharts 画折线图、平滑曲线图以及平滑背后那套三次贝塞尔的数学公式完整捋了一遍。这篇内容适合三类人刚上手 Vue 需要做数据可视化的前端、被平滑曲线看着不对困扰的开发者、以及想把埋点数据反推成趋势公式做预测的同学。折线图本身很简单真正花时间的是那些文档里不写的边界情况——容器没高度、打包后布局错位、万级数据卡顿、曲线过冲。下面按我实际踩坑的顺序展开。1. Vue 项目里引入 ECharts先把地基打对1.1 全量引入和按需引入体积账要算清楚安装没什么可说的npm i echarts一条命令。真正值得聊的是引入方式。全量引入就一行import * as echarts from echarts;方便是方便但打包后 ECharts 会实打实占掉几百 KB。我实测过一个 Vue3 Vite 的中后台项目全量引入后 vendor 包里 ECharts 占约 1MB未压缩gzip 之后也在 300KB 上下。如果这个页面只需要折线图和柱状图这笔开销是浪费。按需引入的写法是分别引核心和图表类型import * as echarts from echarts/core; import { LineChart } from echarts/charts; import { GridComponent, TooltipComponent, LegendComponent, DataZoomComponent } from echarts/components; import { CanvasRenderer } from echarts/renderers; echarts.use([ LineChart, GridComponent, TooltipComponent, LegendComponent, DataZoomComponent, CanvasRenderer ]);这里有个坑我踩过use()里漏注册组件图表不会报错而是静默不显示。表现是图上有线但没坐标轴或者tooltip 死活不出来。排查方法很简单把组件名对着官方文档的模块列表一个个核。我现在的习惯是先在开发环境全量引入把功能跑通确认没问题再改成按需最后对比一次构建产物体积确认收益。还有一个容易忽略的点echarts/core和echarts两个包同时被引入会导致核心代码被打进去两份。用import * as echarts from echarts/core之后就不要再从echarts里引任何东西包括类型。类型声明也要走echarts/core。1.2 容器没高度等于白画这是我带新人时见到的最高频问题代码全对页面空白控制台没报错。原因几乎都是容器div没有高度。!-- 错误示范高度为 0canvas 画出来也看不见 -- div refchartRef/div !-- 正确示范 -- div refchartRef stylewidth: 100%; height: 360px;/divECharts 初始化时读取容器的clientWidth和clientHeight如果读到 0它不会抛异常只是在 0×0 的画布上画了一遍。所以判断标准很明确图表不显示时第一步永远是console.log(el.clientWidth, el.clientHeight)。宽度撑不满也是同一个道理。父容器用 flex 布局时子元素默认min-width: auto内容会把容器撑出预期宽度。解决办法是在图表容器的父级加min-width: 0;让 flex 允许收缩。1.3 初始化和销毁必须成对出现Vue3 组合式 API 里我的标准写法是这样import { ref, onMounted, onUnmounted, shallowRef } from vue; import * as echarts from echarts/core; const chartRef ref(null); const chartInstance shallowRef(null); onMounted(() { chartInstance.value echarts.init(chartRef.value); chartInstance.value.setOption(option); window.addEventListener(resize, handleResize); }); onUnmounted(() { window.removeEventListener(resize, handleResize); chartInstance.value?.dispose(); chartInstance.value null; });两个细节值得说明。第一实例用shallowRef而不是ref。ECharts 实例内部有大量循环引用和 DOM 引用用ref会被 Vue 深度代理性能下降不说某些版本还会报奇怪的错。第二dispose()之后一定要把变量置空否则组件卸载后如果有异步回调比如接口返回慢了继续调setOption会报 Instance is disposed。resize监听我更推荐用ResizeObserver替代window.resize因为侧边栏折叠、Tab 切换这类容器尺寸变化不会触发 window 的 resize 事件const ro new ResizeObserver(() chartInstance.value?.resize()); ro.observe(chartRef.value);卸载时记得ro.disconnect()。2. 一条折线图的 option逐字段拆开看2.1 xAxis、yAxis、series 这三根柱子ECharts 的配置项看起来多本质上就是回答三个问题横轴是什么、纵轴是什么、数据怎么连。最小可用的折线图配置如下const option { xAxis: { type: category, data: [1月, 2月, 3月, 4月, 5月, 6月] }, yAxis: { type: value }, series: [ { name: 订单量, type: line, data: [820, 932, 901, 934, 1290, 1330] } ] };xAxis.type有两个常见值category类目轴等距排布和time时间轴按真实时间间隔排布。很多人做日期图表时用了category结果数据缺了几天横轴却看不出来——因为类目轴把所有点等距摊开了。只要 x 轴是时间且数据可能不连续就该用time并且数据格式写成[timestamp, value]。yAxis.type常见的还有log对数轴。做增长类数据时特别有用因为线性轴下早期数据会被压成一条贴地的直线。2.2 数据的三种投喂方式第一种是series.data直接给数组最简单。第二种是给对象数组格式{ value: [x, y], name: 标签 }适合需要在 tooltip 里显示额外信息的场景。第三种是用dataset统一管理把原始二维表交给 ECharts 自己解析const option { dataset: { source: [ [月份, 订单量, 退款量], [1月, 820, 30], [2月, 932, 45] ] }, xAxis: { type: category }, yAxis: { type: value }, series: [ { type: line, encode: { x: 月份, y: 订单量 } }, { type: line, encode: { x: 月份, y: 退款量 } } ] };dataset的好处是数据和样式彻底分离接口返回的表格结构可以原样丢进去。坏处是排查问题时要多绕一层encode的映射关系。我个人的取舍是多条系列 数据来自后端表格就用 dataset单条系列或者数据结构特殊比如带自定义标记点就直接写 series.data。2.3 那些影响观感但常被忽略的字段boundaryGap控制折线两端是否留白。类目轴默认是true线从第一个类目的中间开始改成false后线会贴着边界看起来更满。折线图我一般设成false。axisTick.alignWithLabel和axisLabel.interval决定刻度标签是否和刻度线对齐、是否隔项显示。数据点多的时候标签会挤成一团interval: auto是默认行为但如果你想强制每 5 个显示一个可以写函数axisLabel: { interval: (index) index % 5 0 }showSymbol决定是否显示每个数据点的圆点。数据点超过 50 个时建议设为false只留线否则满屏圆点比折线还扎眼。关于填充渐变折线下方那块半透明色的做法是areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ { offset: 0, color: rgba(64, 158, 255, 0.45) }, { offset: 1, color: rgba(64, 158, 255, 0.02) } ]) }注意echarts.graphic在按需引入模式下也要从echarts/core里取别写成从echarts取否则又会引入重复的核心包。2.4 双 Y 轴yAxisIndex 的配对规则业务里经常遇到订单金额万元和订单数笔画在一张图上量级差几百倍。这时候单 Y 轴会让小的那条线贴地。解决办法是配两个 yAxisyAxis: [ { type: value, name: 金额(万元) }, { type: value, name: 笔数, splitLine: { show: false } } ], series: [ { name: 金额, type: line, yAxisIndex: 0, data: [...] }, { name: 笔数, type: line, yAxisIndex: 1, data: [...] } ]规则很直白series.yAxisIndex对应yAxis数组的下标。有几个细节要注意第二个 yAxis 我习惯关掉splitLine否则两套网格线交叉图看着很乱左右两个轴的min、max最好手动指定不然 ECharts 自动计算的刻度可能让你得出错误结论。还有一点双轴图有天然的误导性两条线在某个点交叉完全是刻度缩放造成的视觉巧合做汇报时最好在说明里讲清楚。3. smooth 到底动了什么从折角到三次贝塞尔3.1 折角是从哪来的先把问题说清楚。折线图的相邻两段是直线在连接点处左右两段的斜率不一样也就是一阶导数不连续。视觉上斜率突变的地方就是尖角。人眼对这个非常敏感所以数据稍微抖动图看起来就毛刺感很重。要消除尖角就得让连接点两侧的切线方向一致也就是让曲线在该点一阶连续C1 连续。工程上最省事的做法是用分段三次曲线去穿过所有数据点——因为三次多项式刚好有四个自由度可以同时锁定两个端点和两个端点处的切线方向。3.2 Catmull-Rom 到三次贝塞尔的换算三次贝塞尔曲线由四个点定义起点 P1、终点 P2、两个控制点 C1 和 C2。参数方程是B(t) (1-t)³ · P1 3(1-t)²t · C1 3(1-t)t² · C2 t³ · P2对它求导t 0 处的切向量是3(C1 - P1)。这个式子说明一件事起点处的切线方向完全由 C1 相对 P1 的位置决定。所以只要保证相邻两段在共享点处算出的切线方向相同曲线就不会有折角。Catmull-Rom 样条给出了一个非常经典的取法。对于数据点 P1它的前一个点是 P0后一个点是 P2把这条线段转成三次贝塞尔时控制点取C1 P1 (P2 - P0) / 6 C2 P2 - (P3 - P1) / 6这里的 P3 是 P2 后面的那个点P0 是 P1 前面的点。看着有点绕但逻辑很清楚某点的切线方向用它的前后邻居连线来近似系数 1/6 是经过验证的经验值能让曲线在视觉上最贴近自然过渡。首段和末段没有前驱或后继点直接用端点本身做控制点退化成直线。把这个公式写成代码可以直接算出每一段贝塞尔的四个坐标function catmullRomToBezier(points, tension 0.5) { const segs []; const n points.length; const k tension / 3; // tension0.5 时等价于系数 1/6 for (let i 0; i n - 1; i) { const p0 points[i - 1] || points[i]; const p1 points[i]; const p2 points[i 1]; const p3 points[i 2] || points[i 1]; const c1 { x: p1.x (p2.x - p0.x) * k, y: p1.y (p2.y - p0.y) * k }; const c2 { x: p2.x - (p3.x - p1.x) * k, y: p2.y - (p3.y - p1.y) * k }; segs.push({ p1, c1, c2, p2 }); } return segs; }拿到segs之后无论画 SVG 的path还是 Canvas 的bezierCurveTo都只是拼字符串的事。3.3 ECharts 的 smooth 是同一族思路ECharts 内部的平滑实现走的也是切线方向 邻边长度加权这一类方案只是具体系数是它自己的实现细节。用几何语言描述的话可以这样理解对内部点 Pi先算前一段长度 a |Pi - P(i-1)|、后一段长度 b |P(i1) - Pi|把前后邻居连线方向d P(i1) - P(i-1)归一化成单位切向量 t。然后第 i 段的两个控制点按段长比例分摊C1 Pi t_i · (s · b) C2 P(i1) - t_(i1) · (s · b)其中 s 就是smooth的取值。自定义实现可以这样写function smoothControlPoints(points, smooth 0.3) { const n points.length; const dist (a, b) Math.hypot(b.x - a.x, b.y - a.y); const tan []; for (let i 0; i n; i) { if (i 0) { const dx points[1].x - points[0].x; const dy points[1].y - points[0].y; const len Math.hypot(dx, dy) || 1; tan[i] { x: dx / len, y: dy / len }; } else if (i n - 1) { const dx points[n - 1].x - points[n - 2].x; const dy points[n - 1].y - points[n - 2].y; const len Math.hypot(dx, dy) || 1; tan[i] { x: dx / len, y: dy / len }; } else { const dx points[i 1].x - points[i - 1].x; const dy points[i 1].y - points[i - 1].y; const len Math.hypot(dx, dy) || 1; tan[i] { x: dx / len, y: dy / len }; } } const segs []; for (let i 0; i n - 1; i) { const segLen dist(points[i], points[i 1]); const c1 { x: points[i].x tan[i].x * segLen * smooth, y: points[i].y tan[i].y * segLen * smooth }; const c2 { x: points[i 1].x - tan[i 1].x * segLen * smooth, y: points[i 1].y - tan[i 1].y * segLen * smooth }; segs.push({ p1: points[i], c1, c2, p2: points[i 1] }); } return segs; }把结果画到 Canvas 上跟smooth: 0.3的 ECharts 曲线并列摆放肉眼几乎分不出差别。这也算是对公式理解的一次验证——能自己写出来才算真的懂了。用贝塞尔求值公式可以在任意 t 处取点用来做曲线上的标记点或者判断曲线是否穿过某个阈值function bezierAt(seg, t) { const mt 1 - t; const a mt * mt * mt; const b 3 * mt * mt * t; const c 3 * mt * t * t; const d t * t * t; return { x: a * seg.p1.x b * seg.c1.x c * seg.c2.x d * seg.p2.x, y: a * seg.p1.y b * seg.c1.y c * seg.c2.y d * seg.p2.y }; }3.4 过冲曲线跑到负数是怎么发生的这就是我开头遇到的那个问题。原因在于平滑只约束了切线方向没有约束曲线的取值范围。当两个数据点之间出现一个很陡的起伏控制点被拉得很远三次贝塞尔在中间段就可能越出两端点的 y 区间也就是所谓的过冲overshoot。举个直观的例子三个点分别是 (0, 100)、(1, 2)、(2, 100)。中间的深坑会让 t 在 0.5 附近把曲线压到比 2 还低甚至穿过 0。销量、库存、人数这类天然非负的指标出现负值就非常尴尬。处理的思路有三条我一般组合使用把 smooth 调小。0.2 到 0.4 是大多数场景的安全区间。smooth: true等价于 0.3我见过不少人写成smooth: 1那曲线基本就飞了。手动钳制控制点的 y 值让它落在相邻两端点 y 的区间内。这是自定义 Canvas 实现时的做法。业务上根本不该平滑的时候就别平滑。比如日活、订单量这种波动本身就是信息平滑反而是掩盖问题。// 控制点 y 钳制避免曲线越过相邻端点的值域 const lo Math.min(p1.y, p2.y); const hi Math.max(p1.y, p2.y); c1.y Math.min(hi, Math.max(lo, c1.y)); c2.y Math.min(hi, Math.max(lo, c2.y));注意钳制之后再画曲线在极值点附近会有一点点扁的手感这是为了保证不过冲必须付出的代价。数据可视化的第一原则是别撒谎好看要排在准确之后。4. 反过来把一串数据点反推成数学公式4.1 什么场景真的需要公式化平滑是把点变成曲线严格说只是画法变了数据本身没变。另一类需求更实在给定一批采样点想得到一个函数表达式。常见场景有几种——实验数据要写进报告里需要给出经验公式做趋势外推想估算下个季度的量级把一条曲线压缩成几个参数传给后端减少传输量。这类问题的通解是最小二乘拟合。核心思想是先假设函数形式多项式、指数、对数再找一组参数让所有数据点到曲线的竖直距离平方和最小。平方而不是绝对值是因为平方可导能解析求导得到线性方程组。4.2 多项式拟合的 JS 实现假设要找 n 次多项式y c0 c1·x c2·x² … cn·xⁿ最小二乘会导出正规方程(AᵀA)c Aᵀy其中 A 是范德蒙矩阵。直接构造并求解// 高斯消元解线性方程组 function solve(A, b) { const n b.length; const M A.map((row, i) [...row, b[i]]); for (let col 0; col n; col) { let pivot col; for (let r col 1; r n; r) { if (Math.abs(M[r][col]) Math.abs(M[pivot][col])) pivot r; } [M[col], M[pivot]] [M[pivot], M[col]]; if (Math.abs(M[col][col]) 1e-12) continue; for (let r 0; r n; r) { if (r col) continue; const factor M[r][col] / M[col][col]; for (let c col; c n; c) M[r][c] - factor * M[col][c]; } } return M.map((row, i) row[n] / row[i]); } function polyFit(xs, ys, degree) { const m degree 1; const A Array.from({ length: m }, () new Array(m).fill(0)); const b new Array(m).fill(0); for (let i 0; i m; i) { for (let j 0; j m; j) { let s 0; for (let k 0; k xs.length; k) s Math.pow(xs[k], i j); A[i][j] s; } let t 0; for (let k 0; k xs.length; k) t Math.pow(xs[k], i) * ys[k]; b[i] t; } return solve(A, b); // [c0, c1, ..., cn] }调用polyFit(xs, ys, 2)就得到二次拟合的系数。这里必须提醒一个实践中的大坑x 的数值范围一旦偏大比如时间戳直接当 x幂次一上去x^n会变成天文数字正规方程的条件数爆炸解出来的系数全是噪声。我踩过这个坑同一批数据用时间戳直接拟合三次多项式算出来是一条诡异的振荡线把 x 归一化成(x - mean) / std立刻恢复正常。const mean xs.reduce((a, b) a b, 0) / xs.length; const std Math.sqrt(xs.reduce((a, b) a (b - mean) ** 2, 0) / xs.length) || 1; const nx xs.map(v (v - mean) / std);预测时把新的 x 用同样的 mean 和 std 变换过去再代入系数。4.3 指数增长和幂律先变换再拟合很多业务数据的形态是指数型用户增长、病毒传播或幂律型。这类数据直接上多项式会很别扭要么次数高到过拟合要么拟合得一塌糊涂。正确做法是先做变换把非线性变成线性。指数模型y a·e^(bx)两边取自然对数得ln y ln a b·x。对(x, ln y)做一次线性拟合得到截距ln a和斜率b再还原a e^(截距)。function expFit(xs, ys) { const lny ys.map(v Math.log(Math.max(v, 1e-9))); const [intercept, slope] polyFit(xs, lny, 1); return { a: Math.exp(intercept), b: slope }; }注意Math.log(0)是-Infinity会直接把整个拟合毁掉。采样值里出现 0 的情况很常见某天没有订单所以上面加了Math.max(v, 1e-9)兜底。这个细节几乎每份教程都不提但线上一定会遇到。幂律模型y a·x^b同理两边取对数后ln y ln a b·ln x对(ln x, ln y)做线性拟合。4.4 怎么判断拟合得好不好以及次数怎么选判断标准是决定系数 R²计算方式是1 - SS_res / SS_tot其中SS_res是残差平方和SS_tot是相对均值的总平方和。R² 越接近 1 拟合越好。function rSquared(xs, ys, coef) { const predict x coef.reduce((sum, c, i) sum c * Math.pow(x, i), 0); const mean ys.reduce((a, b) a b, 0) / ys.length; let ssRes 0, ssTot 0; for (let i 0; i xs.length; i) { ssRes (ys[i] - predict(xs[i])) ** 2; ssTot (ys[i] - mean) ** 2; } return 1 - ssRes / ssTot; }关于次数选择我的经验是先用多项式次数 1 和 2 试R² 提升不明显就说明数据本身关系简单别硬往上加。次数每加 1参数就多一个拟合曲线会越来越贴合训练数据但在数据区间之外会剧烈发散。经典的例子是 7 个点用 6 次多项式一定完美穿过每一个点R² 1但两点之间的形状完全不受控制——这就是过拟合。一个更务实的判断方法是留出验证集把最后 20% 的点不参与拟合用拟合出的公式去预测它们看误差。如果训练集 R² 是 0.98 而验证集误差大得离谱就是过拟合无疑。把公式画回 ECharts 的做法是在原有 series 之外再推一条type: line数据为公式算出来的密集采样点lineStyle设为虚线、showSymbol: false、smooth: false图例里标成趋势拟合。series: [ { name: 实际, type: line, data: raw }, { name: 趋势拟合, type: line, showSymbol: false, smooth: false, lineStyle: { type: dashed, width: 2 }, data: denseXs.map(x [x, predict(x)]) } ]5. 那些真正消耗时间的坑5.1 tooltip 内容太长一行撑出屏幕默认 tooltip 是white-space: nowrap内容长了会横向撑出去在图边缘直接溢出。做法是两处配合tooltip: { trigger: axis, confine: true, extraCssText: max-width: 300px; white-space: normal; word-break: break-all;, formatter(params) { const list Array.isArray(params) ? params : [params]; return list .map(p ${p.marker}${p.seriesName}br/${p.name}${p.value}) .join(br/); } }confine: true让 tooltip 被限制在图表容器内靠近边缘时自动换边。white-space: normal配合max-width才会真正换行。自定义formatter返回 HTML 字符串时br/是唯一可靠的换行方式\n在 HTML 渲染模式下无效。5.2 打包后布局异常以及 pxtorem 为什么对 ECharts 失灵本地开发好好的打包上线图表宽度就不对这个问题我遇到过两次原因完全不同。第一次是 CSS 顺序问题。ECharts 的容器宽度来自父级如果打包后 CSS 压缩合并改变了加载顺序父级的flex: 1被另一条更高优先级的规则覆盖容器宽度就变了。解决办法是给图表容器显式设一个min-width或者用 ResizeObserver 兜住后续变化。第二次跟postcss-pxtorem有关。这个插件会把 CSS 里的 px 自动转成 rem但 ECharts 的尺寸是运行时用 JS 算的走的是 canvas 的 px插件管不着。听起来没问题问题出在它把 echarts 源码里内联样式和 className 相关的一部分 px 也转了导致 tooltip、图例的内部布局错乱。// postcss.config.js module.exports { plugins: { postcss-pxtorem: { rootValue: 37.5, propList: [*], // 关键排除依赖包里的样式 exclude: /node_modules/ } } };如果用selectorBlackList可以指定.echarts-前缀来排除 ECharts 注入的样式。另外要清楚一点根字号变化后图表容器尺寸变了必须主动调resize()ECharts 不会自己感知 rem 变化。5.3 万级数据点不开降采样就是卡我做过一个每 10 秒刷新一次、单条线 2 万个点的监控面板不优化的话刷新一次页面卡半秒。几个开关效果立竿见影series: [{ type: line, sampling: lttb, // 最大保留三角桶降采样视觉几乎无损 large: true, largeThreshold: 2000, showSymbol: false, animation: false, lineStyle: { width: 1.5 } }]sampling: lttb是最推荐的它按面积重要性筛点能在保留曲线形态的前提下把点降到千级。animation: false一定要关大数据量下的入场动画纯粹是负担。progressive和progressiveThreshold适合超大数据集分批渲染但会带来渐进显现的效果实时大屏上我一般不用。还有个小技巧dataZoom配合inside类型能让用户鼠标滚轮缩放比默认的滑块占用更少的视觉空间。5.4 定时刷新引起的泄漏实时曲线最常见的写法是setInterval里调setOption刷新数据。这里有两个隐患。一个是定时器没清。组件卸载后定时器还在跑实例已经dispose()了setOption每次都抛错。一定要在onUnmounted里clearInterval。另一个是setOption的合并行为。默认它是合并模式新传的series会和旧的按索引合并。如果你每次刷新都传不同数量的 series就会残留幽灵系列。做实时曲线时我建议改成整体替换chartInstance.value.setOption(option, { notMerge: true });代价是每次刷新都会重建所有元素闪烁感稍强。想两全其美的话把series写成固定长度只更新 data这样默认合并模式反而是最平滑的chartInstance.value.setOption({ series: [{ data: newData }] });6. 封成一个团队能直接复用的组件6.1 组合式函数把初始化那套样板收起来上面那些初始化、resize、销毁的代码每个图表写一遍太啰嗦。抽成组合式函数import { ref, shallowRef, onMounted, onUnmounted, watch } from vue; import * as echarts from echarts/core; export function useChart(optionRef, theme null) { const el ref(null); const chart shallowRef(null); let ro null; const render () { if (!chart.value || !optionRef.value) return; chart.value.setOption(optionRef.value, { notMerge: false }); }; onMounted(() { chart.value echarts.init(el.value, theme); render(); ro new ResizeObserver(() chart.value?.resize()); ro.observe(el.value); }); watch(optionRef, render, { deep: true }); onUnmounted(() { ro?.disconnect(); chart.value?.dispose(); chart.value null; }); return { el, chart }; }组件里就只剩配置和数据template div refel stylewidth: 100%; height: 360px;/div /templatewatch这里有个取舍deep: true能捕捉到series.data数组的原地修改但数据量大时深度遍历本身就有开销。如果确定每次都整个替换option把deep关掉、只监听引用变化更划算。6.2 主题和暗黑模式切换主题在init时传入运行中没法直接换。切换暗黑模式的做法是销毁重建function switchTheme(name) { const el chart.value.getDom(); chart.value.dispose(); chart.value echarts.init(el, name); chart.value.setOption(optionRef.value, { notMerge: true }); }如果不想重建另一种做法是定义两套颜色变量切换时只setOption覆盖颜色相关的部分chart.value.setOption({ backgroundColor: isDark ? #0f1115 : #ffffff, xAxis: { axisLabel: { color: isDark ? #9aa4b2 : #5a6672 } }, yAxis: { splitLine: { lineStyle: { color: isDark ? #232a35 : #eef1f5 } } } });这种方式没有重建开销曲线不会闪一下我更喜欢。缺点是颜色配置分散在组件里要靠约定维护。6.3 几个我固定会加的默认配置封装组件时有些默认值我几乎每个项目都要加上索性固化进去const baseOption { grid: { left: 12, right: 20, top: 32, bottom: 8, containLabel: true }, tooltip: { trigger: axis, axisPointer: { type: line } }, legend: { icon: roundRect, itemWidth: 10, itemHeight: 10 } };containLabel: true是我最推荐的一个它能保证坐标轴标签不被裁掉。不加这个y 轴数值大的时候左边的数字会被切一半。grid的四个方向值也不用调得太精细containLabel会兜底。我个人的体会是ECharts 的配置项确实多但真正常用的就那么二三十个。折线图这条线上值得反复琢磨的只有三件事数据格式和坐标轴类型是否匹配、平滑在数学上做了什么、以及数据量大或结构复杂时的性能开关。把这三点吃透剩下的基本是查文档的体力活。至于把数据反推成公式我的建议是把它当成探索工具而不是结论——拟合出来的 R² 再漂亮它也只是对已有数据的一种解释方式拿去做外推预测时永远留出安全边际。
返回列表