用 ECharts + React 打造环形饼图:从 0 到 1 入门
这篇入门文会带你用"正确姿势"在 React 中接入 ECharts,完成一个可复用、可维护、可扩展的环形饼图组件,并解释关键的生命周期、类型与性能要点。示例代码参考 Demo.tsx
。
为什么选 ECharts
- 能力强:内置几十种图表与丰富交互。
- 生态成熟:社区活跃,示例与文档完善。
- 高可定制:主题、动画、标签、交互都可细调。
环境准备
- 安装依赖:
bash
# 二选一
npm i echarts
pnpm add echarts
- 建议使用 TypeScript:更安全的参数与事件类型提示。
组件结构与关键思路
-
DOM 容器与实例引用
- 用
useRef<HTMLDivElement>(null)
拿到真实 DOM 容器。 - 用
useRef<echarts.ECharts | null>(null)
存放 ECharts 实例,便于resize()
/dispose()
。
- 用
-
受控数据与类型
- 统一数据结构,利于类型提示与后续扩展:
ts
interface ChartData {
value: number;
name: string;
}
- 生命周期管理(核心)
- 在
useEffect
初始化并渲染图表。 - 绑定
window.resize
做自适应。 - 返回清理函数移除监听并
dispose()
,避免内存泄漏。 - 以数据
chartData
为依赖,数据变更自动刷新图表。
- 在
手把手实现要点
- 初始化实例:
ts
const chartInstance = echarts.init(chartRef.current!);
chartInstanceRef.current = chartInstance;
- 配置 Option(标题/提示框/图例/系列/颜色/动画):
- 标题与副标题:
title
- 提示框:
tooltip
,用"运行时守卫"写法规避类型不匹配 - 图例:
legend.data = chartData.map(i => i.name)
- 系列:
type: 'pie'
+radius: ['40%', '70%']
实现环形 - 颜色与动画:
color
、animation*
- 标题与副标题:
ts
chartInstance.setOption(option);
- 自适应与清理:
ts
const handleResize = () => {
if (chartInstance && !chartInstance.isDisposed()) {
chartInstance.resize();
}
};
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
if (chartInstance && !chartInstance.isDisposed()) {
chartInstance.dispose();
}
chartInstanceRef.current = null;
};
在页面中使用
tsx
import Demo from './components/Demo';
export default function Page() {
return (
<div style={{ height: '100vh' }}>
<Demo />
</div>
);
}
换成你的真实数据
- 将
useState<ChartData[]>(...)
的 mock 数据替换为接口数据。 - 常见写法:
ts
const [chartData, setChartData] = useState<ChartData[]>([]);
useEffect(() => {
let cancelled = false;
fetch('/api/metrics')
.then(res => res.json())
.then(data => {
if (!cancelled) setChartData(data);
});
return () => { cancelled = true; };
}, []);
- 因为
useEffect([...])
依赖了chartData
,数据更新会自动触发图表刷新。
常见问题与排查
- 容器尺寸为 0,图表不显示
确保容器有明确宽高(如800x500
或父级flex
填充)。 - 重复 init 造成内存泄漏
一个 DOM 只init
一次;组件卸载时务必dispose()
。 - 类型报错(尤其是
tooltip.formatter
)
用unknown
+ 运行时守卫从容器对象里安全取name/value/percent
。 - SSR 环境(Next.js)
只在客户端渲染时init
,例如用动态导入禁用 SSR。
常用定制方向
- 改主题/颜色:修改
color
数组或全局主题。 - 改布局:
legend.{left, top, orient}
与series[0].center
。 - 标签与强调:
label
、emphasis.label
控制中心强调与悬停样式。 - 响应式容器:用
width: 100%/height: 100%
+ 外层自适应布局。
性能与工程建议
- 避免在每次渲染都
init
,只在首次挂载执行,数据更新仅setOption
。 - 大量 resize 事件可做节流/防抖(小型场景直接调用也可)。
- 异步数据请求要做"已卸载保护"(如上
cancelled
写法)。
一个最小可用 Option 速查
- 标题 :
title.text/subtext/left/top
- 提示框 :
tooltip.trigger/formatter
- 图例 :
legend.orient/left/top/data
- 系列-饼图 :
series: [{ type: 'pie', radius, center, data }]
- 样式 :
itemStyle.borderRadius/borderColor/borderWidth
- 动画 :
animation/animationDuration/animationEasing
如果你把以上要点串起来,就能得到一个生命周期完整、类型安全、可扩展的 ECharts-React 图表组件。基于此模板,你可以快速拓展到轮播图、折线图、柱状图等更多场景,只需更换 series.type
与对应配置即可。
tsx
import React, { useEffect, useRef, useState } from "react";
import * as echarts from "echarts";
/**
* ECharts 演示组件
*
* 这个组件演示了如何在 React 中以"正确姿势"接入 ECharts:
* - 在首次渲染后创建图表实例,并在卸载时销毁,避免内存泄漏
* - 使用 ref 获取真实 DOM 容器,供 ECharts 渲染
* - 使用 useEffect 管理图表的创建、更新与清理,保证生命周期完整
* - 监听窗口大小变化,保持图表自适应
*
* 功能特点:
* 1. 响应式环形饼图展示
* 2. 自动窗口大小调整(resize)
* 3. 完整的生命周期管理(初始化/更新/销毁)
* 4. 基础的性能优化和内存清理
*
* 使用场景:
* - 数据可视化展示
* - 营销数据分析
* - 用户行为统计
*
* 使用方式:
* - 直接在页面中引入并渲染 <Demo /> 即可
* - 如需接入真实数据,只需将下方 chartData 的 mock 数据替换为你的数据,并触发状态更新
*/
// 统一定义图表数据的结构,便于在整个组件中获得类型提示与约束
interface ChartData {
value: number;
name: string;
}
const Demo: React.FC = () => {
// DOM 引用:用于挂载 ECharts 实例。ECharts 需要真实 DOM 节点来渲染图表。
const chartRef = useRef<HTMLDivElement>(null);
// ECharts 实例引用:保存 init 生成的图表实例,方便在清理阶段 dispose,或在需要时调用实例方法(如 resize)。
const chartInstanceRef = useRef<echarts.ECharts | null>(null);
// 图表数据状态:可配置的数据源。实际项目中可通过接口获取后 setState 更新。
const [chartData] = useState<ChartData[]>([
{ value: 100, name: '线下渠道' },
{ value: 200, name: '邮件营销' },
{ value: 300, name: '直接访问' },
{ value: 333, name: '视频广告' },
{ value: 444, name: '搜索引擎' },
]);
/**
* 初始化并渲染 ECharts 图表
*
* 步骤概览:
* 1) 确认 DOM 容器存在
* 2) 通过 echarts.init 创建实例,并保存到 ref
* 3) 准备 Option(图表的所有配置项)
* 4) setOption 应用配置
* 5) 绑定 window.resize 事件,保持自适应
* 6) 返回清理函数:移除事件监听并销毁实例
*
* 依赖项:chartData
* - 当 chartData 变化时,会重新执行 effect,以便刷新图表数据
*/
useEffect(() => {
// 确保 DOM 容器已挂载
if (!chartRef.current) {
console.warn('Chart container not found');
return;
}
try {
// 步骤1:初始化 ECharts 实例
// 注意:同一个 DOM 节点不要重复 init;这里在组件首次挂载时运行。
const chartInstance = echarts.init(chartRef.current);
chartInstanceRef.current = chartInstance;
// 步骤2:配置图表选项(Option 是 ECharts 的核心配置对象)
const option: echarts.EChartsOption = {
// 图表标题配置
title: {
text: '营销渠道分析',
subtext: '数据来源:用户行为统计',
left: 'center',
top: 20,
textStyle: {
fontSize: 18,
fontWeight: 'bold'
}
},
// 提示框配置
tooltip: {
trigger: 'item',
// 使用 unknown + 运行时守卫,兼容 ECharts v6 类型变更并避免 any
formatter: (params: unknown) => {
const single = Array.isArray(params) ? params[0] : params;
const p = (single ?? {}) as Record<string, unknown>;
const name = typeof p.name === 'string' ? p.name : '';
const value = typeof p.value === 'number' || typeof p.value === 'string' ? p.value : '';
const percent = typeof p.percent === 'number' || typeof p.percent === 'string' ? p.percent : '';
return `
<div style="padding: 8px;">
<strong>${name}</strong><br/>
访问量:${value}<br/>
占比:${percent}%
</div>
`;
}
},
// 图例配置
legend: {
orient: 'vertical',
left: 'left',
top: 'middle',
// 图例展示每个扇区的名称
data: chartData.map(item => item.name),
textStyle: {
fontSize: 12
}
},
// 系列配置
series: [
{
name: '访问来源',
type: 'pie',
radius: ['40%', '70%'], // 环形图配置
center: ['60%', '50%'], // 图表位置
avoidLabelOverlap: false,
itemStyle: {
borderRadius: 10,
borderColor: '#fff',
borderWidth: 2
},
label: {
show: false,
position: 'center'
},
emphasis: {
label: {
show: true,
fontSize: 20,
fontWeight: 'bold'
}
},
labelLine: {
show: false
},
// 最核心的数据入口:每一项代表一个饼图扇区
data: chartData
}
],
// 颜色配置
color: ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de'],
// 动画配置
animation: true,
animationDuration: 1000,
animationEasing: 'cubicOut'
};
// 步骤3:应用配置到图表
chartInstance.setOption(option);
// 步骤4:响应式处理 - 监听窗口大小变化
const handleResize = () => {
if (chartInstance && !chartInstance.isDisposed()) {
chartInstance.resize();
}
};
// 添加窗口大小变化监听
window.addEventListener('resize', handleResize);
// 返回清理函数:组件卸载时执行
return () => {
// 移除事件监听器
window.removeEventListener('resize', handleResize);
// 销毁图表实例,释放内存
if (chartInstance && !chartInstance.isDisposed()) {
chartInstance.dispose();
}
// 清空引用
chartInstanceRef.current = null;
};
} catch (error) {
console.error('Failed to initialize chart:', error);
}
}, [chartData]); // 依赖项:当数据变化时重新渲染
return (
<div style={{
width: '100%',
height: '100vh',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
backgroundColor: '#f5f5f5'
}}>
{/*
图表容器:
- ECharts 会占满容器宽高进行渲染
- 通过 ref 传递 DOM 给 echarts.init
*/}
<div
ref={chartRef}
style={{
width: '800px',
height: '500px',
backgroundColor: '#fff',
borderRadius: '8px',
boxShadow: '0 2px 8px rgba(0,0,0,0.1)',
padding: '20px'
}}
/>
</div>
);
};
export default Demo;