Search code examples
reactjstypescriptd3.js

How can I create a D3 transition animation to update a React component without re-rendering the whole plot?


I have a D3js Plot wrapped inside a React component (v7). For example a Bar Plot with a data table and a parameter for which column to plot. On change of the plotting variable, I do not want to re-render the whole plot but instead execute a D3 transition animation to the new variable.

Right now I have tried it following the stump-code here, but first I have problems getting the initial plot to render and second I really would like to understand what the correct React hook way is to achieve this…

import * as React from "react";
import * as d3 from "d3";

export function BarPlot({
  data,
  x,
  width,
  height,
}: {
  data: DataTable;
  x: string;
  width: number;
  height: number;
}) {
  const svgRef = React.useRef<SVGSVGElement>(null);
  const svg = d3.select(svgRef.current);

  const prevX = React.useRef<string>(x);
  
  
  if (svgRef.current === null) {
    svg.selectAll("*").remove();
    svg
      .append("g")
      .selectAll()
      .data(data)
    // …
    // Normal d3 plotting code here
    // …
  }


  if (prevX.current !== x) {
    // Update the plot, animate the transition from plotting the old bars to the new bars

    prevX.current = x;
  }

  return <svg ref={svgRef} width={width} height={height} />;
}

When looking around for the correct React-way to do this, it seems useEffect is not the right choice here. I also tried to use useMemo to save the inital plot, but even then I need to manually check whether the transitionable parameters have changed…

Abstract, I think the question is how to have a React component, where part of the render code is executed intially and another part only if the already rendered component has a change in one of the props.


Solution

  • Here is an example of animated bar chart using React with D3.

    Just add a useEffect on the SVG element ref and build the chart when ref is valid (when the component is mounted)

    const MAX_VALUE = 200;
    
    const BarChart = ({ data, height, width }) => {
      const svgRef = React.useRef(null);
    
      React.useEffect(() => {
        const svg = d3.select(svgRef.current);
    
        const xScale = d3.scaleBand()
          .domain(data.map((value, index) => index.toString()))
          .range([0, width])
          .padding(0.1);
    
        const yScale = d3.scaleLinear()
          .domain([0, MAX_VALUE])
          .range([height, 0]);
    
        const xAxis = d3.axisBottom(xScale)
          .ticks(data.length)
          .tickFormat((_, index) => data[index].label);
    
        svg
          .select("#x-axis")
          .style("transform", `translateY(${height}px)`)
          .style("font-size", '16px')
          .call(xAxis);
    
        const yAxis = d3.axisLeft(yScale);
        svg
          .select("#y-axis")
          .style("font-size", '16px')
          .call(yAxis)
    
        svg.selectAll('g.tick');
    
        const bars = svg
          .selectAll(".bar")
          .data(data)
          .join("g")
          .classed("bar", true);
          
        bars.append("rect")
          .style("transform", "scale(1, -1)")
          .attr("x", (_, index) => xScale(index.toString()))
          .attr("y", -height)
          .attr("width", xScale.bandwidth())
          .transition()
          .delay((_, index) => index * 500)
          .duration(1000)
          .attr("fill", d => d.color)
          .attr("height", (d) => height - yScale(d.value));
          
      }, [data]);
    
      return (
          <svg ref={svgRef} height={height} width={width} />
      );
    };
    
    const data = [
      {value: 50, color: '#008'}, 
      {value: 100, color: '#00C'}, 
      {value: 150, color: '#00f'}
    ];
    
    ReactDOM.render(
      <BarChart data={data} width={300} height={170} />, 
      document.getElementById("chart")
    );
    <script src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.4/d3.min.js"></script>
    
    <script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
    
    <script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>
    
    <div id='chart'></div>