【图表+ECharts】day53-dataset-components

作者:mario 发布时间: 2026-09-03 阅读量:3 评论数:0

Day 53 · ECharts dataset 与组件进阶 — 数据与视图分离,多图联动的正确姿势

昨天你把 series 的 data 写死在 option 里——只有一个图表时没问题,但工业面板是一个数据源喂多个图表 + 数据频繁更新 + 图表间联动。今天解决三件事:dataset 让数据独立于视图、transform 让数据加工声明化、多 grid + markLine + visualMap 让单图变面板。学完今天,你就能搭出 BOSS 战监控面板的静态骨架。


目录


一、为什么需要 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

datadataset 互斥——写了 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。

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 级"与"生产级"图表的分水岭。

评论