【图表+ECharts】day52-echarts-core

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

Day 52 · ECharts 入门 — option 不是配置文件,是你手写过的东西的声明式包装

两天手写攒下的心智模型今天开始变现。学 ECharts 的正确姿势不是"背 option",而是每学一个配置项都问:这对应我手写的哪个函数? 今天先搭好骨架:init、option 结构、组件模型、setOption 合并机制、响应式 resize,最后用 ECharts 把 Day 50 的柱状图一行配置复刻出来——复刻对照表是今天的毕业证


目录


一、安装与初始化

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.containLabelyAxis.splitNumbertooltip.triggerseries.barWidthanimationDelay

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)——它们是"能画图"与"能做面板"的分界线。

评论