Day 52 · ECharts 入门 — option 不是配置文件,是你手写过的东西的声明式包装
两天手写攒下的心智模型今天开始变现。学 ECharts 的正确姿势不是"背 option",而是每学一个配置项都问:这对应我手写的哪个函数? 今天先搭好骨架:init、option 结构、组件模型、setOption 合并机制、响应式 resize,最后用 ECharts 把 Day 50 的柱状图一行配置复刻出来——复刻对照表是今天的毕业证。
目录
- 一、安装与初始化
- 二、option 结构总览:组件化思维
- 三、grid 与 axis:手写骨架的声明版
- 四、tooltip 与 legend:手写交互的声明版
- 五、setOption 的合并机制
- 六、响应式 resize
- 七、复刻 Day 50:对照验证实验
- 八、TypeScript 下的类型提示
- 九、常见坑点
- 十、自测挑战
- 十一、总结
一、安装与初始化
1.1 安装
# 项目内安装(推荐:按需引入、tree-shaking 友好)
npm install echarts
1.2 最小可运行示例
<div id="chart" style="width: 800px; height: 400px;"></div>
import * as echarts from "echarts";
/**
* ECharts 初始化三步曲:拿容器 → init → setOption
*/
function demo(): void {
// 1. init:创建图表实例(一个容器只能 init 一次!)
const chart = echarts.init(document.querySelector<HTMLDivElement>("#chart")!);
// 2. setOption:用声明式配置描述"你想要什么"
chart.setOption({
title: { text: "车间温度监控" },
tooltip: {},
xAxis: { type: "category", data: ["1车间", "2车间", "3车间", "4车间"] },
yAxis: { type: "value" },
series: [{ type: "bar", data: [82, 67, 91, 78] }],
});
}
四行 option 替代了你昨天 300 行的 BarChart 类——这就是声明式的威力:你描述结果,库负责过程。而你现在恰好知道过程里发生了什么。
1.3 init 的隐藏细节(手写过的都懂)
// 渲染器选择:canvas(默认,大数据量强)或 svg(交互元素多、需要 DOM 事件时强)
const chart = echarts.init(el, null, { renderer: "canvas" });
// init 时容器必须已布局完成(clientWidth > 0),
// 否则 ECharts 拿不到尺寸 —— 你在 Day 29 踩过的 DPR/布局时序问题,这里同样存在
二、option 结构总览:组件化思维
2.1 option 的两大板块
option
├── 组件(component):图表的"地"—— 轴、网格、提示、图例、标题
│ ├── title 标题
│ ├── legend 图例
│ ├── tooltip 悬浮提示
│ ├── grid 绘图区(含边距计算)
│ ├── xAxis / yAxis 坐标轴(比例尺 + 刻度算法都在这)
│ └── ...
└── 系列(series):图表的"楼"—— 数据图形本体
└── { type: 'bar' | 'line' | 'pie' | 'gauge' | ..., data: [...] }
对应 Day 50 的图表解剖学:组件 = 骨架,系列 = 数据图形。ECharts 内部渲染顺序也是先组件后系列——和你手写的一致。
2.2 组件的通用规律
几乎所有组件都遵循**“单组件用对象、多组件用数组”**:
// 单 grid:直接对象
option.grid = { left: 60, right: 20, top: 40, bottom: 30 };
// 多 grid(明天讲):数组 + index 关联
option.grid = [{ ... }, { ... }];
option.xAxis = [{ gridIndex: 0 }, { gridIndex: 1 }];
三、grid 与 axis:手写骨架的声明版
3.1 grid:绘图区边距
/**
* grid 配置:对应手写时"给轴标签留空间"的四边距计算
*/
const option = {
grid: {
left: 60, // 给 y 轴刻度文字留的宽度
right: 20,
top: 40, // 给 title + legend 留的高度
bottom: 30, // 给 x 轴类目标签留的高度
containLabel: true, // ⭐ 自动把轴标签算进边距(不用手动估 60px 够不够)
},
};
💡
containLabel: true解决的是你手写时最烦的问题:刻度文字宽度随数量级变化(“10000” 比 “10” 宽),写死 left 会出事。ECharts 帮你先量文字再定边距。
3.2 axis:比例尺与刻度的声明版
const option = {
xAxis: {
type: "category", // 类目轴(对应手写"按数组下标 + 带宽布局")
data: ["1车间", "2车间", "3车间", "4车间"],
},
yAxis: {
type: "value", // 数值轴(对应手写 linear 比例尺)
min: 0,
max: (value: { min: number; max: number }) => value.max * 1.1, // 函数式动态范围
splitNumber: 5, // 期望刻度数(内部跑 nice 算法,可能微调)
axisLabel: {
formatter: "{value} ℃", // 刻度文字模板
},
},
};
验证实验:故意把 splitNumber 设成 3、5、7,观察刻度数量——你会发现实际刻度数不总是等于设定值,因为 nice 算法优先保证刻度值好看(你 Day 50 写过的那个取舍,ECharts 替你做了同样的决定)。
3.3 你手写的每个刻度参数都有对应物
| 手写概念 | ECharts 配置 |
|---|---|
niceTicks(min, max, 6) |
splitNumber: 6 |
| 刻度文字格式化 | axisLabel.formatter |
| 网格线样式 | splitLine.lineStyle |
| 刻度小线段 | axisTick |
| 轴线本身 | axisLine |
四、tooltip 与 legend:手写交互的声明版
4.1 tooltip:两种触发模式
const option = {
tooltip: {
// trigger: 'item' → 命中具体图形(你 Day 50 的 hitBar AABB)
// trigger: 'axis' → 最近点 + 十字准线(你 Day 51 的 nearestPoint + crosshair)
trigger: "axis",
axisPointer: {
type: "cross", // 'line' | 'shadow' | 'cross' | 'none'
},
formatter: (params: any) => {
// params 是 ECharts 准备好的命中结果数组
return `<b>${params[0].name}</b><br/>温度:${params[0].value}℃`;
},
},
};
🎯 昨天的 5.3 节在这里闭环:
trigger: 'axis'就是"按 x 找最近点"的语义,trigger: 'item'就是"图形命中"的语义。你推导过的选型原则,ECharts 用两个枚举值表达。
4.2 legend
const option = {
legend: {
data: ["1号炉", "2号炉"], // 系列名列表(默认自动收集,可省略)
selected: { "1号炉": true, "2号炉": false }, // 初始开关状态
// 点击图例 → 系列 visible 翻转 → y 轴自动重算范围
// 你 Day 51 坑 5 手动修的问题,这里全自动
},
};
五、setOption 的合并机制
这是 ECharts 最容易被误解、也最重要的机制。
5.1 默认是"合并"不是"替换"
// 第一次 setOption
chart.setOption({
title: { text: "温度" },
xAxis: { data: ["1车间", "2车间"] },
series: [{ type: "bar", data: [82, 67] }],
});
// 第二次 setOption:只想更新数据
chart.setOption({
series: [{ data: [95, 43] }], // 只传变化的部分
});
// 结果:title 和 xAxis 保留(合并),series.data 更新 ✅
// 这就是"局部更新"——性能远优于整图重建
5.2 三种更新模式
// 1. 默认合并:适合增量更新(实时数据流的标准用法)
chart.setOption({ series: [{ data: newData }] });
// 2. notMerge: true — 完全替换:option 结构大变时用(如换图表类型)
chart.setOption(newOption, { notMerge: true });
// 3. lazyUpdate: true — 合并 + 惰性执行:高频更新时把多次 setOption 合成一次渲染
chart.setOption({ series: [{ data: newData }] }, { lazyUpdate: true });
⚠️ 合并机制的经典坑:系列数量减少时(从 3 条线变 1 条),默认合并会保留多余系列——这时必须用
replaceMerge: ['series']:chart.setOption(newOption, { replaceMerge: ["series"] });
六、响应式 resize
/**
* 响应式:容器尺寸变化 → chart.resize()
* 关键点:resize 有成本(重算布局重绘图),要防抖
*/
function bindResize(chart: echarts.ECharts): void {
let timer = 0;
window.addEventListener("resize", () => {
clearTimeout(timer);
timer = window.setTimeout(() => chart.resize(), 150); // 防抖 150ms
});
}
// 更专业的方案:ResizeObserver 监听容器本身(大屏缩放、侧栏折叠都能响应)
const ro = new ResizeObserver(() => chart.resize());
ro.observe(container);
大屏场景的特殊性:工业大屏常用 transform: scale() 整体缩放页面——此时容器 CSS 尺寸不变,resize 不触发,但视觉字号会糊。应对策略在 Day 55 详讲。
七、复刻 Day 50:对照验证实验
7.1 任务
用 ECharts 复刻你手写的柱状图,要求逐项对齐:
import * as echarts from "echarts";
/**
* 用 ECharts 复刻 Day 50 手写柱状图
* 每一行配置都标注了对应的手写函数
*/
export function replicateBarChart(el: HTMLElement): echarts.ECharts {
const chart = echarts.init(el);
chart.setOption({
// ← drawTitle(手写略过的部分)
title: { text: "车间温度监控", left: "center", textStyle: { color: "#e0e8f0" } },
// ← drawYAxis 的网格线
grid: { left: "3%", right: "4%", bottom: "3%", containLabel: true },
// ← drawXAxis 类目标签
xAxis: {
type: "category",
data: ["1车间", "2车间", "3车间", "4车间"],
axisLabel: { color: "#6a7a94" },
},
// ← niceTicks + linear 比例尺
yAxis: {
type: "value",
splitNumber: 6,
axisLabel: { color: "#6a7a94", formatter: "{value} ℃" },
splitLine: { lineStyle: { color: "rgba(106,122,148,0.15)" } },
},
// ← hitBar + drawTooltip
tooltip: { trigger: "item" },
// ← layoutBars + drawBar 渐变 + 错开动画
series: [
{
type: "bar",
data: [82, 67, 91, 78],
barMaxWidth: 60, // ← 坑 4 的修复(柱宽上限)
itemStyle: {
borderRadius: [4, 4, 0, 0], // ← 顶部圆角
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: "#00c6ff" }, // ← 柱顶亮色
{ offset: 1, color: "#0072b0" }, // ← 柱底深色
]),
},
animationDuration: 600, // ← animate(600)
animationEasing: "cubicOut", // ← easeOutCubic
animationDelay: (idx: number) => idx * 30, // ← 错开动画 i×30ms
},
],
});
return chart;
}
7.2 验证清单(逐项打勾)
- [ ] 刻度是 nice 值(改
splitNumber观察微调行为) - [ ] hover 柱子变亮 + tooltip(
emphasis状态) - [ ] 入场动画:波浪式生长,节奏与手写版一致
- [ ] 柱宽在数据少时不超过 60px
- [ ] 开 DevTools → Sources → 断点进
echarts.js,搜索niceScale/interval相关函数,看它算刻度的过程
最后一项是本周最重要的一次实验:亲眼确认"库的内部就是你写过的算法",从此你对 ECharts 的信任从"迷信"升级为"知根知底"。
八、TypeScript 下的类型提示
// ECharts 5 自带完整类型定义,但 option 是巨大的联合类型,
// 直接写对象字面量时补全不友好。最佳实践:显式标注 ComposeOption
import type {
BarSeriesOption,
LineSeriesOption,
} from "echarts/charts";
import type {
TitleComponentOption,
TooltipComponentOption,
GridComponentOption,
} from "echarts/components";
import type { ComposeOption } from "echarts/core";
/** 本项目的 option 类型:系列与组件按需组合 */
export type ECOption = ComposeOption<
| BarSeriesOption
| LineSeriesOption
| TitleComponentOption
| TooltipComponentOption
| GridComponentOption
>;
const option: ECOption = {
xAxis: { type: "category", data: [...] }, // 现在有完整提示了
series: [{ type: "bar", data: [...] }],
};
💡 按需引入(
echarts/core+echarts/charts+echarts/components)可以把打包体积从 ~1MB 压到 300KB 级——第 4 周工程化知识的应用,Day 56 BOSS 战会用到。
九、常见坑点
坑 1:容器宽高为 0 时 init
现象:图表空白,控制台警告 “Can’t get DOM width or height”。
修复:确保容器已布局(clientWidth > 0)再 init;在弹窗/Tab 页里渲染时,等显示后再 init。
坑 2:一个容器 init 两次
现象:There is a chart instance already initialized on the dom。
修复:单页应用里用 echarts.getInstanceByDom(el) 先查再建;组件卸载时 dispose()。
坑 3:setOption 系列数量变少后残留旧系列
见 5.2 —— 用 replaceMerge: ['series']。
坑 4:formatter 里 this 指向丢失
用箭头函数时 this 不是 params。修复:用函数参数 params(推荐),或用普通函数 + this(ECharts 显式绑定)。
坑 5:整页 transform: scale 后图表模糊
大屏 scale() 缩放导致 canvas 像素被拉伸。修复方案见 Day 55(按缩放比重建 canvas 尺寸)。
十、自测挑战
T1 · 复刻 Day 51 折线图(40 分钟)
用 ECharts 复刻昨天的折线图:smooth + areaStyle 渐变 + trigger: 'axis' + axisPointer: 'cross' + legend 切换 + 生长动画(animationDuration,注意 line 系列的生长是 clip 式的——验证 7.1 的映射)。今天核心作业。
T2 · 探索式实验(20 分钟)
在官方 examples 打开任意图,逐个修改这些参数并记录效果:grid.containLabel、yAxis.splitNumber、tooltip.trigger、series.barWidth、animationDelay。
T3 · 源码下钻(30 分钟)
DevTools 断点进 echarts 源码,找到刻度计算函数(提示:搜索 roundNumber / nice 相关),截屏记录调用栈。写进博客:“我确认了 ECharts 的刻度算法与我手写的一致”。
T4 · 按需引入改造(20 分钟)
新建一个 Vite + TS 项目,用 echarts/core 按需引入 bar + line + 必要组件,对比全量引入的打包体积差异(vite build 后看 dist 大小)。
十一、总结
今天建立了 ECharts 的骨架认知:
| 概念 | 本质 | 手写对应 |
|---|---|---|
| option | 声明式描述"要什么" | 你的 Chart 类的构造参数 |
| 组件(title/grid/axis) | 图表的"地" | 骨架绘制函数 |
| 系列(series) | 图表的"楼" | drawBar/drawSmoothLine |
| setOption 合并 | 局部更新的机制 | 你没写过的部分——ECharts 真正的工程贡献 |
| resize | 响应式 | 你没写过的部分(防抖 + 重算布局) |
明天深入两个工程级能力:dataset(数据与视图分离) 和多组件联动(多 grid、markLine、visualMap)——它们是"能画图"与"能做面板"的分界线。