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
相关产品推荐
相关产品推荐

