Day 53 · ECharts dataset 与组件进阶 — 数据与视图分离,多图联动的正确姿势
昨天你把 series 的
data写死在 option 里——只有一个图表时没问题,但工业面板是一个数据源喂多个图表 + 数据频繁更新 + 图表间联动。今天解决三件事:dataset 让数据独立于视图、transform 让数据加工声明化、多 grid + markLine + visualMap 让单图变面板。学完今天,你就能搭出 BOSS 战监控面板的静态骨架。
目录
- 一、为什么需要 dataset:数据与配置耦合的痛
- 二、dataset 基础:source 与 dimensions
- 三、encode:列的角色分配
- 四、transform:声明式数据加工
- 五、多 grid 布局与轴联动
- 六、markLine 与 markArea:阈值与预警区
- 七、visualMap:数值到颜色的映射
- 八、组件联动实战:温度监控面板
- 九、常见坑点
- 十、自测挑战
- 十一、总结
一、为什么需要 dataset:数据与配置耦合的痛
1.1 痛点场景
假设一份车间数据要同时喂三个视图:
同一份原始数据:
时间 | 1号炉温度 | 2号炉温度 | 产量
三个视图:
柱状图 → 只看最新一天的各炉温度
折线图 → 看 1 号炉 24 小时趋势
排名图 → 按温度给车间排序
如果数据写死在每个 series 的 data 里,同样的数据要复制三份、改三种格式——数据更新时要同步改三处,漏一处就是数据不一致事故。
1.2 dataset 的解法
dataset(唯一数据源)
│
├── 柱状图 series:引用 dataset + encode 取特定列
├── 折线图 series:引用 dataset + encode 取另一列
└── 排名图 series:dataset → transform 排序 → 引用
一份数据,多个视图,零复制。这就是"数据与视图分离"——第 1 周 ES Module 分层思想的图表版。
二、dataset 基础:source 与 dimensions
2.1 两种 source 格式
const option = {
dataset: {
// 格式一:对象数组(可读性最好,推荐接口返回后直接用)
source: [
{ time: "08:00", furnace1: 82, furnace2: 76, output: 120 },
{ time: "09:00", furnace1: 85, furnace2: 79, output: 135 },
{ time: "10:00", furnace1: 91, furnace2: 83, output: 142 },
],
},
// 系列直接从 dataset 取数(默认:第一列当 x,后面依次当 y)
xAxis: { type: "category" },
yAxis: {},
series: [
{ type: "line", encode: { x: "time", y: "furnace1" } },
{ type: "line", encode: { x: "time", y: "furnace2" } },
],
};
// 格式二:二维数组(第一行是列名,体积更小,适合大数据量)
const option2 = {
dataset: {
dimensions: ["time", "furnace1", "furnace2", "output"],
source: [
["08:00", 82, 76, 120],
["09:00", 85, 79, 135],
],
},
};
2.2 dimensions 的作用
// dimensions 显式声明列名和顺序:
// 1. 二维数组格式必须声明(否则列名是 0/1/2)
// 2. 可以指定列的类型(ordinal 类目 / number 数值),省去 ECharts 猜测
const option = {
dataset: {
dimensions: [
{ name: "time", type: "ordinal" },
{ name: "furnace1", type: "number" },
],
source: [...],
},
};
三、encode:列的角色分配
encode 告诉系列"哪列当 x、哪列当 y、哪列当 tooltip、哪列当 itemLabel"——同一份数据,不同视角:
/**
* 同一个 dataset,两种图表视角
*/
const option = {
dataset: {
source: [
{ workshop: "1车间", temp: 82, output: 120 },
{ workshop: "2车间", temp: 67, output: 135 },
{ workshop: "3车间", temp: 91, output: 142 },
],
},
series: [
// 视角一:温度柱状图(车间为 x,温度为 y)
{
type: "bar",
encode: { x: "workshop", y: "temp" },
// tooltip 里顺带显示产量(不需要它参与坐标映射)
tooltip: ["output"],
},
// 视角二:温度-产量散点图(温度为 x,产量为 y,车间名做标签)
{
type: "scatter",
encode: { x: "temp", y: "output", itemName: "workshop" },
},
],
};
🎯 encode 是"数据多面性"的表达:一列数据在这张图里是 x,在另一张图里可以是 y。写工业面板时这个能力天天用——同一批设备数据,运维看趋势(折线)、管理者看相关性(散点)、大屏看排名(柱状)。
四、transform:声明式数据加工
4.1 内置 transform
const option = {
dataset: [
// 原始数据(dataset 0)
{
source: [
{ workshop: "1车间", temp: 82 },
{ workshop: "2车间", temp: 67 },
{ workshop: "3车间", temp: 91 },
{ workshop: "4车间", temp: 54 },
],
},
// 派生数据(dataset 1):只保留温度 ≥ 60 的车间
{
transform: {
type: "filter",
config: { dimension: "temp", gte: 60 },
},
},
// 派生数据(dataset 2):按温度降序排(排名图用)
{
transform: {
type: "sort",
config: { dimension: "temp", order: "desc" },
},
},
],
series: [
// 告警图:只画超限的
{ type: "bar", datasetIndex: 1, encode: { x: "workshop", y: "temp" } },
// 排名图:画排序后的
{ type: "bar", datasetIndex: 2, encode: { x: "workshop", y: "temp" } },
],
};
4.2 自定义 transform(TypeScript)
import type { ExternalDataTransform } from "echarts/types/dist/shared";
/**
* 自定义转换:温度转状态等级(≥90 危险 / ≥80 警告 / 正常)
*/
const levelTransform: ExternalDataTransform = {
type: "custom.level",
transform: (params) => {
const rows = params.upstream.list as { temp: number }[];
return {
dimensions: ["workshop", "temp", "level"],
list: rows.map((r) => ({
...r,
level: r.temp >= 90 ? "危险" : r.temp >= 80 ? "警告" : "正常",
})),
};
},
};
// 注册后即可在 transform.type 中使用
echarts.registerTransform(levelTransform);
💡 transform vs 自己在 JS 里先加工:数据量小时两者没差别;但 transform 后的数据和原始数据一起参与流式更新(第 9 周实时数据接入后,原始数据 appendData,派生数据自动跟着变)——把加工逻辑放进声明链,实时面板就自动正确。
五、多 grid 布局与轴联动
5.1 上下双图共享 x 轴(工业面板经典布局)
/**
* 上面画温度折线、下面画产量柱状,共享时间轴,axisPointer 联动
*/
const option = {
// 两个绘图区,垂直堆叠
grid: [
{ left: 60, right: 20, top: 40, height: 180 }, // 上:温度
{ left: 60, right: 20, top: 280, height: 100 }, // 下:产量
],
// 时间轴只显示一个(下面的只留刻度线不留文字)
xAxis: [
{ type: "time", gridIndex: 0, axisLabel: { show: false } },
{ type: "time", gridIndex: 1 },
],
yAxis: [
{ gridIndex: 0, name: "温度℃" },
{ gridIndex: 1, name: "产量" },
],
// 关键:tooltip + axisPointer 用 axisIndex 数组跨图联动
tooltip: { trigger: "axis" },
axisPointer: {
link: [{ xAxisIndex: "all" }], // ⭐ 两图十字线同步移动
},
series: [
{ type: "line", xAxisIndex: 0, yAxisIndex: 0, data: tempData },
{ type: "bar", xAxisIndex: 1, yAxisIndex: 1, data: outputData },
],
};
axisPointer.link 的效果:鼠标在温度图上移动,下方产量图同步显示同一时刻的十字线和数值——两图共用"当前时刻"这个隐式状态。手写实现它要在两个 canvas 间同步 hover 索引(Day 47 事件路由的跨层版),ECharts 一行搞定。
5.2 左右双图(对比布局)
grid 改为水平排列,x/y 轴的 gridIndex 相应调整即可。规律:grid 定位置,轴认 grid,系列认轴。
六、markLine 与 markArea:阈值与预警区
const option = {
series: [
{
type: "line",
data: tempData,
markLine: {
symbol: "none", // 两端不画箭头
data: [
// 告警阈值:横向虚线
{
yAxis: 90,
lineStyle: { color: "#ff4d4f", type: "dashed" },
label: { formatter: "告警阈值 90℃", position: "insideEndTop" },
},
// 均值线(支持动态计算型写法)
{ type: "average", name: "均值" },
],
},
markArea: {
// 预警区:夜间低谷时段背景着色(数据驱动,非写死像素)
data: [
[
{ xAxis: "00:00", itemStyle: { color: "rgba(255,77,79,0.08)" } },
{ xAxis: "06:00" },
],
],
},
},
],
};
🎯 工业语义:markLine 是"阈值"(SLA 线、告警线、设计值),markArea 是"区间"(检修时段、夜谷时段、超限区)。两者都是数据坐标系里的标注——缩放平移时自动跟随,这是它比"在 canvas 上画条线"高明的地方。
七、visualMap:数值到颜色的映射
7.1 连续型(continuous)
/**
* 温度 → 颜色渐变映射:低温柔蓝 → 高温警示红
* 作用在 y 轴维度上,折线/柱子颜色随数值变化
*/
const option = {
visualMap: {
type: "continuous",
min: 40,
max: 100,
dimension: 1, // 数据的第 1 维(y 值)参与映射
inRange: {
color: ["#00c6ff", "#ffd666", "#ff4d4f"],
},
right: 0, // 组件位置
top: "center",
textStyle: { color: "#6a7a94" },
calculable: true, // 拖动手柄过滤显示范围
},
series: [{ type: "line", data: tempData }],
};
7.2 分段型(piecewise)
/**
* 状态分段:设备状态枚举 → 颜色
*/
const option = {
visualMap: {
type: "piecewise",
dimension: 1,
pieces: [
{ gte: 90, label: "危险", color: "#ff4d4f" },
{ gte: 80, lt: 90, label: "警告", color: "#ffd666" },
{ lt: 80, label: "正常", color: "#00c6ff" },
],
orient: "horizontal",
left: "center",
bottom: 0,
},
series: [{ type: "bar", data: workshopTemps }],
};
💡 visualMap 与 Day 53 自定义 transform 的关系:transform 算出 level 字段(数据加工),visualMap 把数值映射成颜色(视觉通道)——一个负责数据语义,一个负责视觉编码,组合起来就是完整的"状态渲染"链路。
八、组件联动实战:温度监控面板
把今天所有知识组装成一个静态面板(BOSS 战的预演):
import * as echarts from "echarts";
/**
* 车间温度监控面板:dataset + 多grid + markLine + visualMap + 联动
* 数据静态,Day 56 接入实时数据流
*/
export function createMonitorPanel(el: HTMLElement): echarts.ECharts {
const chart = echarts.init(el);
const option = {
// ===== 唯一数据源 =====
dataset: {
dimensions: ["time", "furnace1", "furnace2"],
source: [
["08:00", 82, 76], ["09:00", 85, 79], ["10:00", 91, 83],
["11:00", 95, 88], ["12:00", 88, 90], ["13:00", 84, 85],
],
},
// ===== 双图布局 =====
grid: [
{ left: 60, right: 80, top: 40, height: 200 }, // 上:折线趋势
{ left: 60, right: 80, top: 300, height: 140 }, // 下:分段柱状
],
xAxis: [
{ type: "category", gridIndex: 0, axisLabel: { show: false } },
{ type: "category", gridIndex: 1 },
],
yAxis: [
{ gridIndex: 0, name: "温度℃" },
{ gridIndex: 1, show: false },
],
// ===== 联动 =====
tooltip: { trigger: "axis" },
axisPointer: { link: [{ xAxisIndex: "all" }] },
// ===== 阈值标注 =====
// ===== 状态映射 =====
visualMap: {
type: "piecewise",
dimension: 1,
pieces: [
{ gte: 90, label: "危险", color: "#ff4d4f" },
{ gte: 80, lt: 90, label: "警告", color: "#ffd666" },
{ lt: 80, label: "正常", color: "#00c6ff" },
],
right: 0,
top: "center",
},
series: [
// 上图:1号炉趋势 + 告警线
{
type: "line",
name: "1号炉",
xAxisIndex: 0, yAxisIndex: 0,
encode: { x: "time", y: "furnace1" },
smooth: true,
markLine: {
symbol: "none",
data: [{ yAxis: 90, lineStyle: { color: "#ff4d4f", type: "dashed" },
label: { formatter: "告警 90℃" } }],
},
},
// 上图:2号炉趋势
{
type: "line",
name: "2号炉",
xAxisIndex: 0, yAxisIndex: 0,
encode: { x: "time", y: "furnace2" },
smooth: true,
},
// 下图:1号炉温度分段状态柱
{
type: "bar",
name: "1号炉状态",
xAxisIndex: 1, yAxisIndex: 1,
encode: { x: "time", y: "furnace1" },
barWidth: 18,
},
],
};
chart.setOption(option);
return chart;
}
这个面板包含了今天全部知识点:dataset 单源 → encode 双视角 → 多 grid → axisPointer 联动 → markLine 阈值 → visualMap 状态色。跑通它,Day 56 的 BOSS 战就是"往这个骨架里填实时数据和更多模块"。
九、常见坑点
坑 1:series 里既写 data 又想用 dataset
data 和 dataset 互斥——写了 data 就不再读 dataset。排查:图上没数据时先检查是不是 accidentally 带了空 data: []。
坑 2:encode 的列名拼错无报错
ECharts 对 encode 里不存在的维度静默失败(图空了但控制台干净)。排查:console.log(chart.getOption().dataset[0].source.slice(0,1)) 对比列名。
坑 3:多 grid 时系列没绑定对轴
系列默认绑定 xAxisIndex: 0, yAxisIndex: 0——双图布局里第二个系列必须显式指定轴索引,否则两坨数据画进同一个 grid。
坑 4:axisPointer.link 不生效
link 关联的是轴(xAxisIndex),不是 grid。写 link: [{ gridIndex: 'all' }] 无效,正确写法是 link: [{ xAxisIndex: 'all' }]。
坑 5:visualMap dimension 数错了
dimension 是数据维度的下标(0 是 x,1 是 y),不是"第几个系列"。折线图想让线条随 y 值变色就是 dimension: 1。
十、自测挑战
T1 · 跑通监控面板(30 分钟)
把第八节的完整 option 敲进项目跑通,观察:图例切换、十字线联动、visualMap 拖动过滤。今天核心作业。
T2 · transform 排名图(20 分钟)
基于第八节 dataset,用 sort transform 派生"按 13:00 温度降序"的数据集,在右侧新增第三个 grid 画排名水平柱状图(yAxis: { type: 'category' })。
T3 · markArea 检修时段(15 分钟)
给上图添加 markArea:10:00-11:00 标注"计划检修"(灰色半透明),观察缩放时标注如何跟随数据坐标。
T4 · 一个 dataset 喂两个图表实例(进阶,25 分钟)
把同一个 dataset 分别配到"折线图实例"和"饼图实例"(各一个容器)。思考:跨实例共享数据时,是复制 source 好,还是用组件层的 store 好?结合第 1 周的模块分层思想写 200 字分析进博客。
十一、总结
今天从"能画图"进阶到"能搭面板":
| 能力 | 配置 | 解决的问题 |
|---|---|---|
| dataset + encode | 一份数据多视角 | 数据复制与不一致 |
| transform | 声明式加工 | 数据预处理与实时派生 |
| 多 grid + axisPointer.link | 上下双图联动 | 多图共享"当前时刻" |
| markLine / markArea | 数据坐标标注 | 阈值/时段/预警区 |
| visualMap | 数值→颜色映射 | 状态可视化 |
明天进入性能深水区:10 万个点的折线图如何保持 60fps——sampling 降采样、large 模式、渐进渲染,这些是"demo 级"与"生产级"图表的分水岭。