You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React(v7)中实现D3过渡动画更新图表而非重绘全图

React 17 + D3.js 柱状图组件:初始化与过渡动画实现

核心思路拆分

React组件中要明确区分仅初始化一次的代码和props变化时触发的更新代码:前者负责创建SVG容器、比例尺、坐标轴等基础结构;后者仅在绘图字段(如x/y轴绑定列)变化时,更新数据绑定并执行D3过渡动画,不重建整个图表。

具体实现步骤

1. 用useRef存储D3实例与DOM元素

通过useRef保存SVG容器、比例尺、坐标轴等不需要重复创建的对象,避免每次渲染都重新初始化:

import React, { useRef, useEffect } from 'react';
import * as d3 from 'd3';

const BarChart = ({ data, xField, yField }) => {
  // 绑定SVG容器的DOM引用
  const svgRef = useRef(null);
  // 存储D3核心实例,避免重复创建
  const d3Ref = useRef({
    xScale: null,
    yScale: null,
    xAxis: null,
    yAxis: null,
    svg: null
  });
  // 图表固定尺寸配置
  const margin = { top: 20, right: 20, bottom: 50, left: 60 };
  const width = 800 - margin.left - margin.right;
  const height = 500 - margin.top - margin.bottom;

2. 初始化代码:仅组件挂载时执行

使用空依赖数组的useEffect,确保只在组件第一次渲染时完成基础结构搭建:

useEffect(() => {
    if (!svgRef.current || !data.length) return;

    // 创建SVG根容器与绘图分组
    const svg = d3.select(svgRef.current)
      .attr('width', width + margin.left + margin.right)
      .attr('height', height + margin.top + margin.bottom)
      .append('g')
      .attr('transform', `translate(${margin.left}, ${margin.top})`);

    // 初始化比例尺
    const xScale = d3.scaleBand()
      .range([0, width])
      .padding(0.2);
    const yScale = d3.scaleLinear()
      .range([height, 0]);

    // 初始化坐标轴
    const xAxis = d3.axisBottom(xScale)
      .tickSizeOuter(0);
    const yAxis = d3.axisLeft(yScale)
      .tickSizeOuter(0);

    // 添加坐标轴容器
    svg.append('g')
      .attr('class', 'x-axis')
      .attr('transform', `translate(0, ${height})`);
    svg.append('g')
      .attr('class', 'y-axis');

    // 添加柱状图容器
    svg.append('g')
      .attr('class', 'bars');

    // 将实例存入ref,供后续更新使用
    d3Ref.current = { svg, xScale, yScale, xAxis, yAxis };
  }, []);

3. 更新代码:props变化时执行过渡动画

使用带props依赖的useEffect,仅当data、xField或yField变化时,执行数据更新与过渡动画:

useEffect(() => {
    const { svg, xScale, yScale, xAxis, yAxis } = d3Ref.current;
    if (!svg || !data.length) return;

    // 更新比例尺定义域
    xScale.domain(data.map(d => d[xField]));
    yScale.domain([0, d3.max(data, d => d[yField])]);

    // 更新坐标轴,添加过渡动画
    svg.select('.x-axis')
      .transition()
      .duration(750)
      .call(xAxis);
    svg.select('.y-axis')
      .transition()
      .duration(750)
      .call(yAxis);

    // 绑定数据到柱状图,用xField做唯一标识避免重绘
    const bars = svg.select('.bars')
      .selectAll('rect')
      .data(data, d => d[xField]);

    // 移除旧数据对应的柱子,带收缩动画
    bars.exit()
      .transition()
      .duration(750)
      .attr('height', 0)
      .attr('y', height)
      .remove();

    // 添加新数据柱子,并合并新老柱子执行统一过渡
    bars.enter()
      .append('rect')
      .attr('x', d => xScale(d[xField]))
      .attr('y', height)
      .attr('width', xScale.bandwidth())
      .attr('height', 0)
      .attr('fill', '#2c7fb8')
      .merge(bars)
      .transition()
      .duration(750)
      .attr('x', d => xScale(d[xField]))
      .attr('y', d => yScale(d[yField]))
      .attr('width', xScale.bandwidth())
      .attr('height', d => height - yScale(d[yField]));
  }, [data, xField, yField]);

4. 组件返回JSX

最后返回绑定了ref的SVG元素:

return (
    <svg ref={svgRef}></svg>
  );
};

export default BarChart;

关键注意事项

  • useRef的核心作用:存储D3实例避免重复初始化,解决初始渲染的重复创建问题。
  • 拆分useEffect:空依赖的useEffect处理初始化,带props依赖的处理更新,精准控制代码执行时机。
  • 数据绑定的key函数:data(data, d => d[xField])让D3能准确识别数据的增删改,避免无意义的重绘。
  • 过渡动画合并:enter().merge(bars)同时处理新添加和已存在的柱子,保证动画效果统一。

内容的提问来源于stack exchange,提问作者Maurice Frank

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.20 01:22:40