T 教程 Tutorials

Apache ECharts 从入门到实践

对照官方手册写的中文长文:引入与首图、坐标系、常用 series、核心组件、dataset/encode、事件与行为、主题、响应式、异步 loading、性能、TypeScript 与调试。

教程

Apache ECharts 是 Apache 基金会下的开源可视化库,底层基于 ZRender,覆盖折线、柱状、饼图、散点、雷达、热力、地图等大量图表类型,文档与示例以中文友好见长。本篇按官方快速上手、引入说明、数据集、事件与行为等手册章节整理,尽量给出可直接粘贴改跑的 option / API 片段。

和 Chart.js 等比一句话:Chart.js 更轻、上手快;ECharts 图表种类与交互组件更全,国内业务看板、大屏里更常见。选库看「要不要地图 / 多种坐标系 / 复杂联动」,而不是谁更「高级」。

简介与适用场景

适合:

  • 管理后台、分析报表、数据大屏中的趋势、对比、占比、分布
  • 需要 tooltip、legend、dataZoom、toolbox、visualMap 等开箱交互
  • 希望用声明式 option 驱动,而不是从零写 Canvas / SVG

不太适合或要慎重:

  • 包体积必须极小、只要一两张极简图 → 可看更轻的方案
  • 出版级统计图、强科学计算叙事 → Observable Plot / Vega-Lite / D3 可能更对口
  • 高度定制的「信息图插画」→ 往往仍要 D3 或手绘

对多数业务可视化,先把数据映射和坐标系想清楚,再谈美化。

引入方式:npm、按需引入、CDN

对照官方「获取 → 引入 → 绘制」流程。

npm 全量引入(原型最快)

npm install echarts --save
import * as echarts from 'echarts';

const myChart = echarts.init(document.getElementById('main'));
myChart.setOption({
  title: { text: 'ECharts 入门示例' },
  tooltip: {},
  xAxis: {
    data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
  },
  yAxis: {},
  series: [{ name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] }],
});

按需引入(官方推荐减小体积)

按需引入时必须自行注册渲染器(CanvasRenderer 或 SVGRenderer),否则无法绘制。

import * as echarts from 'echarts/core';
import { BarChart, LineChart, PieChart, ScatterChart, RadarChart, HeatmapChart } from 'echarts/charts';
import {
  TitleComponent,
  TooltipComponent,
  LegendComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  ToolboxComponent,
  DataZoomComponent,
  VisualMapComponent,
  PolarComponent,
  RadarComponent,
  GeoComponent,
} from 'echarts/components';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';

echarts.use([
  TitleComponent,
  TooltipComponent,
  LegendComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  ToolboxComponent,
  DataZoomComponent,
  VisualMapComponent,
  PolarComponent,
  RadarComponent,
  GeoComponent,
  BarChart,
  LineChart,
  PieChart,
  ScatterChart,
  RadarChart,
  HeatmapChart,
  LabelLayout,
  UniversalTransition,
  CanvasRenderer,
]);

export { echarts };

示例编辑页「完整代码」可根据当前 option 生成最小按需引入清单,很实用。v5.5.0 起默认模块规范为 ESM,升级时注意 breaking changes。

CDN(无构建演示)

从 jsDelivr 等取 dist/echarts.min.js(生产请钉版本号):

<!DOCTYPE html>
<html lang="zh-CN">
  <head>
    <meta charset="utf-8" />
    <title>ECharts</title>
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.1/dist/echarts.min.js"></script>
  </head>
  <body>
    <!-- 必须有明确宽高的 DOM 容器 -->
    <div id="main" style="width: 600px; height: 400px;"></div>
    <script>
      var myChart = echarts.init(document.getElementById('main'));
      var option = {
        title: { text: 'ECharts 入门示例' },
        tooltip: {},
        legend: { data: ['销量'] },
        xAxis: {
          data: ['衬衫', '羊毛衫', '雪纺衫', '裤子', '高跟鞋', '袜子'],
        },
        yAxis: {},
        series: [{ name: '销量', type: 'bar', data: [5, 20, 36, 10, 10, 20] }],
      };
      myChart.setOption(option);
    </script>
  </body>
</html>

第一个完整图表:init → setOption → dispose

生命周期记住三步:

  1. echarts.init(dom, theme?, opts?) 创建实例
  2. myChart.setOption(option, opts?) 渲染 / 更新
  3. 离开页面或销毁组件时 myChart.dispose(),避免泄漏
import * as echarts from 'echarts';

const el = document.getElementById('main') as HTMLDivElement;
const myChart = echarts.init(el);

const option: echarts.EChartsOption = {
  title: { text: '周活用户' },
  tooltip: { trigger: 'axis' },
  legend: { data: ['UV'] },
  grid: { left: 48, right: 24, top: 64, bottom: 40, containLabel: true },
  xAxis: {
    type: 'category',
    data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
  },
  yAxis: { type: 'value' },
  series: [
    {
      name: 'UV',
      type: 'line',
      smooth: true,
      data: [820, 932, 901, 934, 1290, 1330, 1320],
    },
  ],
};

myChart.setOption(option);

// SPA / 组件卸载:
window.addEventListener('resize', () => myChart.resize());
// ...
// myChart.dispose();

在 React / Vue 中务必在 DOM 已挂载 后 init,在 unmount / onBeforeUnmount 里 dispose。setOption 默认合并更新;需要整表替换时用:

myChart.setOption(fullOption, { notMerge: true });
// 或只替换 series:
myChart.setOption(partial, { replaceMerge: ['series'] });

坐标系概念:cartesian / polar / geo

ECharts 把「画在哪」和「画什么」拆开:

概念作用常见搭配
Cartesian(直角坐标)由 grid + xAxis + yAxis 组成line / bar / scatter / heatmap
Polar(极坐标)polar + radiusAxis + angleAxis极坐标柱/线、部分雷达相关布局
Geo(地理坐标)geo 组件 + 地图数据map / scatter(经纬度)/ lines
单轴 / 平行坐标等singleAxis、parallel特殊分析图

直角坐标最小骨架:

option = {
  grid: { left: '10%', right: '8%', top: 60, bottom: 40 },
  xAxis: { type: 'category', data: ['A', 'B', 'C'] },
  yAxis: { type: 'value' },
  series: [{ type: 'bar', data: [1, 2, 3] }],
};

极坐标示意(柱状沿角度展开):

option = {
  polar: {},
  angleAxis: {
    type: 'category',
    data: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
  },
  radiusAxis: {},
  series: [
    {
      type: 'bar',
      coordinateSystem: 'polar',
      data: [1, 2, 3, 4, 3, 5, 1],
    },
  ],
};

地理坐标需要先注册地图 GeoJSON(注意版权与合规),再:

echarts.registerMap('demo', geoJson);
option = {
  geo: { map: 'demo', roam: true },
  series: [
    {
      type: 'scatter',
      coordinateSystem: 'geo',
      data: [{ name: '某点', value: [116.4, 39.9, 100] }],
    },
  ],
};

系列通过 coordinateSystem: 'cartesian2d' | 'polar' | 'geo' | ...(多数直角图可省略,默认直角坐标系)声明落在哪套坐标系上。

常用 series 与可运行 option

下列片段均可在官方示例编辑器里粘贴运行(需已有 myChart)。

line 折线

option = {
  title: { text: '近 7 日转化率' },
  tooltip: { trigger: 'axis' },
  legend: { data: ['自然', '投放'] },
  xAxis: {
    type: 'category',
    boundaryGap: false,
    data: ['10-03', '10-04', '10-05', '10-06', '10-07', '10-08', '10-09'],
  },
  yAxis: { type: 'value', axisLabel: { formatter: '{value}%' } },
  series: [
    { name: '自然', type: 'line', data: [2.1, 2.4, 2.3, 2.8, 3.1, 2.9, 3.4] },
    { name: '投放', type: 'line', data: [1.4, 1.6, 1.9, 2.0, 2.2, 2.1, 2.5] },
  ],
};

bar 柱状

option = {
  tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
  legend: { data: ['本周', '上周'] },
  xAxis: { type: 'category', data: ['搜索', '社交', '邮件', '直接', '其他'] },
  yAxis: { type: 'value' },
  series: [
    { name: '本周', type: 'bar', data: [320, 240, 180, 410, 90] },
    { name: '上周', type: 'bar', data: [280, 210, 160, 380, 110] },
  ],
};

横向条形:把 xAxis / yAxis 的 type 对调(一个 value、一个 category),series.type 仍为 'bar'。

pie 饼 / 环

option = {
  title: { text: '错误类型占比', left: 'center' },
  tooltip: { trigger: 'item', formatter: '{b}: {c} ({d}%)' },
  legend: { orient: 'vertical', left: 'left' },
  series: [
    {
      name: '错误',
      type: 'pie',
      radius: ['40%', '68%'],
      data: [
        { value: 48, name: '超时' },
        { value: 32, name: '解析失败' },
        { value: 15, name: '鉴权' },
        { value: 5, name: '其他' },
      ],
    },
  ],
};

scatter 散点

option = {
  tooltip: {
    trigger: 'item',
    formatter: (p) => `x=${p.data[0]}, y=${p.data[1]}`,
  },
  xAxis: { type: 'value', scale: true, name: '时长' },
  yAxis: { type: 'value', scale: true, name: '转化%' },
  series: [
    {
      type: 'scatter',
      symbolSize: 12,
      data: [
        [2.1, 1.2],
        [3.4, 1.8],
        [5.0, 2.6],
        [6.2, 3.1],
        [8.0, 2.4],
        [9.5, 3.8],
      ],
    },
  ],
};

radar 雷达

雷达图使用 radar 组件声明各轴指标,系列 type: 'radar':

option = {
  legend: { data: ['预算', '实际'] },
  radar: {
    indicator: [
      { name: '销售', max: 100 },
      { name: '管理', max: 100 },
      { name: '信息技术', max: 100 },
      { name: '客服', max: 100 },
      { name: '研发', max: 100 },
      { name: '市场', max: 100 },
    ],
  },
  series: [
    {
      type: 'radar',
      data: [
        { name: '预算', value: [80, 70, 90, 85, 75, 70] },
        { name: '实际', value: [70, 75, 80, 90, 85, 78] },
      ],
    },
  ],
};

heatmap 热力(直角坐标系)

热力常配合 visualMap 映射颜色:

const hours = ['12a', '2a', '4a', '6a', '8a', '10a', '12p', '2p', '4p', '6p'];
const days = ['周六', '周五', '周四', '周三', '周二'];
const data = [
  [0, 0, 5],
  [1, 0, 1],
  [2, 0, 0],
  [3, 0, 3],
  [0, 1, 7],
  [1, 1, 2],
  [2, 1, 4],
  [3, 1, 6],
  [0, 2, 1],
  [1, 2, 8],
  [2, 2, 3],
  [3, 2, 5],
].map((item) => [item[0], item[1], item[2] || '-']);

option = {
  tooltip: { position: 'top' },
  grid: { height: '50%', top: '10%' },
  xAxis: { type: 'category', data: hours, splitArea: { show: true } },
  yAxis: { type: 'category', data: days, splitArea: { show: true } },
  visualMap: {
    min: 0,
    max: 10,
    calculable: true,
    orient: 'horizontal',
    left: 'center',
    bottom: '5%',
  },
  series: [
    {
      name: '访问量',
      type: 'heatmap',
      data,
      label: { show: true },
      emphasis: {
        itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' },
      },
    },
  ],
};

核心组件速查

组件职责常用字段
title主/副标题text subtext left top
legend图例显隐data orient selected
tooltip提示框trigger: 'axis'|'item' formatter
toolbox工具栏feature.saveAsImage dataZoom restore
dataZoom缩放漫游type: 'inside'|'slider'
visualMap视觉映射min max inRange dimension
grid直角坐标绘图区left/right/top/bottom containLabel
xAxis / yAxis轴type: 'category'|'value'|'time'|'log'

组合示例:

option = {
  color: ['#7A2438', '#B46C78', '#4E1424'],
  title: { text: '看板', subtext: '数据截至今日', left: 'left' },
  tooltip: { trigger: 'axis' },
  legend: { top: 28 },
  toolbox: {
    feature: {
      saveAsImage: {},
      dataZoom: {},
      restore: {},
    },
  },
  dataZoom: [
    { type: 'inside', start: 0, end: 100 },
    { type: 'slider', start: 0, end: 100 },
  ],
  grid: { left: 56, right: 24, top: 72, bottom: 64, containLabel: true },
  xAxis: { type: 'category', data: ['A', 'B', 'C', 'D', 'E'] },
  yAxis: { type: 'value', splitLine: { show: true } },
  series: [{ type: 'bar', name: '指标', data: [10, 22, 18, 30, 16] }],
};

visualMap 常与散点/热力一起用,把某一维度映射到颜色或尺寸(见上一节 heatmap,以及 dataset 一节的气泡尺寸例子)。

dataset + encode(官方推荐的数据方式)

从 ECharts 4 起更推荐用 dataset 管数据、用 series.encode 做映射,把「常变的数据」和「少变的样式配置」拆开,并让多个 series 复用同一份表。

二维表 + 默认映射

option = {
  legend: {},
  tooltip: {},
  dataset: {
    source: [
      ['product', '2015', '2016', '2017'],
      ['Matcha Latte', 43.3, 85.8, 93.7],
      ['Milk Tea', 83.1, 73.4, 55.1],
      ['Cheese Cocoa', 86.4, 65.2, 82.5],
      ['Walnut Brownie', 72.4, 53.9, 39.1],
    ],
  },
  xAxis: { type: 'category' }, // 默认映射第一列
  yAxis: {},
  series: [{ type: 'bar' }, { type: 'bar' }, { type: 'bar' }], // 后续各列一个系列
};

对象数组 + dimensions

option = {
  legend: {},
  tooltip: {},
  dataset: {
    dimensions: ['product', '2015', '2016', '2017'],
    source: [
      { product: 'Matcha Latte', 2015: 43.3, 2016: 85.8, 2017: 93.7 },
      { product: 'Milk Tea', 2015: 83.1, 2016: 73.4, 2017: 55.1 },
      { product: 'Cheese Cocoa', 2015: 86.4, 2016: 65.2, 2017: 82.5 },
    ],
  },
  xAxis: { type: 'category' },
  yAxis: {},
  series: [{ type: 'bar' }, { type: 'bar' }, { type: 'bar' }],
};

显式 encode

option = {
  dataset: {
    source: [
      ['score', 'amount', 'product'],
      [89.3, 58212, 'Matcha Latte'],
      [57.1, 78254, 'Milk Tea'],
      [74.4, 41032, 'Cheese Cocoa'],
    ],
  },
  xAxis: {},
  yAxis: { type: 'category' },
  series: [
    {
      type: 'bar',
      encode: {
        x: 'amount',
        y: 'product',
        tooltip: ['product', 'score'],
      },
    },
  ],
};

要点:

  • seriesLayoutBy: 'column' | 'row' 决定按列还是按行生成系列
  • 多份数据:dataset: [{ source: ... }, { source: ... }],系列用 datasetIndex 引用
  • 饼图等可用 encode: { itemName: ..., value: ... }
  • treemap / graph 等仍常用 series.data;百万级增量用 appendData,此时不走 dataset

事件与行为:click、dispatchAction

监听鼠标事件

事件名为小写字符串:click dblclick mouseover mouseout globalout contextmenu 等。

myChart.on('click', function (params) {
  // params.componentType / seriesType / name / dataIndex / value / color ...
  console.log(params.name, params.value);
});

// 只监听折线系列
myChart.on('click', 'series.line', function (params) {
  /* ... */
});

// 按系列名过滤
myChart.on('mouseover', { seriesName: '销量' }, function () {
  /* ... */
});

组件交互也会抛事件,例如图例切换触发的是 legendselectchanged(注意不是 legendselected):

myChart.on('legendselectchanged', function (params) {
  console.log(params.name, params.selected);
});

点击「空白处」要用 ZRender 层事件(target 为空表示点在空白):

myChart.getZr().on('click', function (event) {
  if (!event.target) {
    // 重置选中、关闭浮层等
  }
});

程序触发行为:dispatchAction

// 高亮某一数据项并弹出 tooltip(大屏轮播常用)
myChart.dispatchAction({ type: 'highlight', seriesIndex: 0, dataIndex: 2 });
myChart.dispatchAction({ type: 'showTip', seriesIndex: 0, dataIndex: 2 });
myChart.dispatchAction({ type: 'downplay', seriesIndex: 0, dataIndex: 2 });

完整 action / event 列表见官方 action 与 events 文档。

主题、暗色与 registerTheme

内置可直接:

const chart = echarts.init(dom, 'dark');

自定义主题先注册再 init(也可在主题编辑器导出 JSON):

echarts.registerTheme('cabernet', {
  color: ['#7A2438', '#B46C78', '#C8909A', '#4E1424', '#8A6068'],
  backgroundColor: 'transparent',
  textStyle: { color: '#3a2a2e' },
  title: { textStyle: { color: '#7A2438', fontWeight: 600 } },
  line: { smooth: true },
});

const chart = echarts.init(dom, 'cabernet');

从 JSON 加载:

const theme = await fetch('/theme/cabernet.json').then((r) => r.json());
echarts.registerTheme('cabernet', theme);
const chart = echarts.init(dom, 'cabernet');

站点明暗切换常见两种策略:

  1. dispose() 后用新主题重新 init
  2. 保留实例,按主题改写 textStyle / 轴线颜色再 setOption
function applySiteTheme(chart, dark) {
  chart.setOption({
    backgroundColor: 'transparent',
    textStyle: { color: dark ? '#e8dde0' : '#3a2a2e' },
    title: { textStyle: { color: dark ? '#e6b3bc' : '#7A2438' } },
  });
}

响应式:resize 与容器高度陷阱

容器尺寸变化后必须调用 myChart.resize(),否则图会拉伸变形或留白。

window.addEventListener('resize', () => myChart.resize());

// 更稳:监听容器本身(侧栏折叠、分栏拖动)
const ro = new ResizeObserver(() => myChart.resize());
ro.observe(el);
// 卸载:ro.disconnect(); myChart.dispose();

高度陷阱(最常见白屏原因):

  • 父级是 flex / grid,子项高度为 auto,图表 div 实际高度为 0
  • Tab / 抽屉初始 display: none,此时 init 量到的宽高为 0;显示后要再 resize()
  • 只写了 width: 100% 却没写高度;百分比高度要求祖先有明确高度

推荐:给图表容器固定 height: 360px(或 min-height),或由父级明确撑开后再 init。

异步数据与 loading

官方建议:先 init 出坐标系骨架,数据返回后再 setOption;加载中用内置 loading。

myChart.showLoading(); // 可传配置:{ text: '加载中', maskColor: 'rgba(255,255,255,0.6)' }

fetch('/api/metrics')
  .then((r) => r.json())
  .then((data) => {
    myChart.hideLoading();
    myChart.setOption({
      xAxis: { data: data.categories },
      series: [
        { name: '销量', type: 'bar', data: data.values },
      ],
    });
  })
  .catch(() => {
    myChart.hideLoading();
    // 错误提示 UI
  });

动态刷新时尽量带上 series.name,方便 ECharts 做差分动画;高频更新可设 animation: false 或 lazyUpdate: true。

大数据与性能

  1. 按需引入:只注册用到的 chart / component / renderer。
  2. 折线采样:sampling: 'lttb'(或 'average' 等),大数据降采样。
  3. progressive:达到阈值后分片绘制,避免一次卡死主线程。
  4. large / largeThreshold:散点等大量图形时开启大规模模式。
  5. Canvas vs SVG:默认 Canvas 更适合海量图形;需要超清打印、CSS 动画叠层时可换 SVGRenderer(按需引入时二选一注册,init 第三参 renderer: 'svg')。
  6. 百万级增量:用 appendData,此时不要用 dataset。
  7. 减少重绘:能局部 setOption 就不要 notMerge: true 全量砸。
series: [
  {
    type: 'line',
    showSymbol: false,
    sampling: 'lttb',
    large: true,
    progressive: 4000,
    progressiveThreshold: 10000,
    data: hugeArray,
  },
],
import { SVGRenderer } from 'echarts/renderers';
echarts.use([SVGRenderer /* ... */]);
const chart = echarts.init(dom, null, { renderer: 'svg' });

TypeScript 类型提示

全量引入可用 echarts.EChartsOption。按需引入时,官方推荐用 ComposeOption 拼出最小且严格的 option 类型,少注册组件时类型就能报错:

import * as echarts from 'echarts/core';
import { BarChart, LineChart } from 'echarts/charts';
import {
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
} from 'echarts/components';
import { LabelLayout, UniversalTransition } from 'echarts/features';
import { CanvasRenderer } from 'echarts/renderers';
import type { BarSeriesOption, LineSeriesOption } from 'echarts/charts';
import type {
  TitleComponentOption,
  TooltipComponentOption,
  GridComponentOption,
  DatasetComponentOption,
} from 'echarts/components';
import type { ComposeOption } from 'echarts/core';

type ECOption = ComposeOption<
  | BarSeriesOption
  | LineSeriesOption
  | TitleComponentOption
  | TooltipComponentOption
  | GridComponentOption
  | DatasetComponentOption
>;

echarts.use([
  TitleComponent,
  TooltipComponent,
  GridComponent,
  DatasetComponent,
  TransformComponent,
  BarChart,
  LineChart,
  LabelLayout,
  UniversalTransition,
  CanvasRenderer,
]);

const option: ECOption = {
  title: { text: 'TS 示例' },
  tooltip: {},
  xAxis: { type: 'category', data: ['A', 'B'] },
  yAxis: {},
  series: [{ type: 'bar', data: [1, 2] }],
};

const chart = echarts.init(document.getElementById('main')!);
chart.setOption(option);

事件回调可参考手册中的 EventParams 字段(componentType、seriesType、dataIndex 等)自行标注类型。

常见错误与调试

现象排查
完全空白容器宽高是否为 0;按需引入是否漏了 Renderer / Chart
只有轴没有线series.data 是否为空;类目轴 length 是否与 data 对齐
更新后旧系列残留使用 notMerge 或 replaceMerge: ['series']
饼图怪坐标轴饼图通常不需要 xAxis/yAxis
encode「不生效」维度名拼写是否一致(空格、大小写)
SSR 报错仅在浏览器侧 init(Next/Nuxt 用 client-only)
越来越卡是否重复 init 未 dispose;定时器是否清理
暗色看不清检查文字色、轴线色、backgroundColor

调试技巧:

  • console.log(myChart.getOption()) 看合并后的真实配置
  • 官方示例里对比同类型图的最小 option
  • 暂时改回全量 import * as echarts from 'echarts' 排除「漏注册组件」
  • 打开浏览器 Performance 看是否主线程被大数据绘制占满

官方文档与示例站


先保证映射正确、容器有尺寸、组件注册齐全,再调颜色与动画。把本篇的 option 片段丢进示例编辑器改一改数据,通常比从零读完整 Option 手册更快建立手感。

评论