用 ECharts + React 打造环形饼图:从 0 到 1 入门

用 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%'] 实现环形
    • 颜色与动画:coloranimation*
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
  • 标签与强调:labelemphasis.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;
相关推荐
jvxiao31 分钟前
搭建个人博客系列--(4) 利用Github Actions自动构建博客
前端
袁煦丞43 分钟前
SimpleMindMap私有部署团队脑力风暴:cpolar内网穿透实验室第401个成功挑战
前端·程序员·远程工作
li理1 小时前
鸿蒙 Next 布局开发实战:6 大核心布局组件全解析
前端
EndingCoder1 小时前
React 19 与 Next.js:利用最新 React 功能
前端·javascript·后端·react.js·前端框架·全栈·next.js
li理1 小时前
鸿蒙 Next 布局大师课:从像素级控制到多端适配的实战指南
前端
前端赵哈哈1 小时前
Vite 图片压缩的 4 种有效方法
前端·vue.js·vite
Nicholas681 小时前
flutter滚动视图之ScrollView源码解析(五)
前端
电商API大数据接口开发Cris1 小时前
Go 语言并发采集淘宝商品数据:利用 API 实现高性能抓取
前端·数据挖掘·api
风中凌乱的L1 小时前
vue 一键打包上传
前端·javascript·vue.js
GHOME1 小时前
Vue2与Vue3响应式原理对比
前端·vue.js·面试