如何创建一个D3过渡动画来更新React组件,而不重新渲染整个图表?

huangapple go评论65阅读模式
英文:

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

问题

我有一个包装在React组件(v7)中的D3js绘图。例如,一个带有数据表和用于绘制的列参数的条形图。在更改绘图变量时,我不想重新渲染整个图,而是执行一个D3过渡动画来切换到新的变量。

现在,我已经尝试了以下存根代码,但首先我在尝试让初始图绘制时遇到了问题,其次我真的很想了解正确的React Hook方式来实现这一点...

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)
    // …
    // 正常的D3绘图代码在这里
    // …
  }


  if (prevX.current !== x) {
    // 更新图表,将旧条形图切换到新条形图进行动画过渡

    prevX.current = x;
  }

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

当寻找正确的React方式来实现这一点时,似乎useEffect不是正确的选择。我还尝试使用useMemo来保存初始图表,但即使在那种情况下,我也需要手动检查过渡参数是否发生了变化...

总的来说,我认为问题是如何创建一个React组件,其中部分渲染代码在初始时执行,只有在已经渲染的组件中的某个属性发生更改时才执行另一部分。

英文:

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.

答案1

得分: 1

以下是使用React和D3创建动画条形图的示例。

只需在SVG元素的ref上添加一个useEffect,并在ref有效时构建图表(即组件挂载时)。

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>
英文:

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)

<!-- begin snippet: js hide: false console: false babel: true -->

<!-- language: lang-js -->

const MAX_VALUE = 200;
const BarChart = ({ data, height, width }) =&gt; {
const svgRef = React.useRef(null);
React.useEffect(() =&gt; {
const svg = d3.select(svgRef.current);
const xScale = d3.scaleBand()
.domain(data.map((value, index) =&gt; 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) =&gt; data[index].label);
svg
.select(&quot;#x-axis&quot;)
.style(&quot;transform&quot;, `translateY(${height}px)`)
.style(&quot;font-size&quot;, &#39;16px&#39;)
.call(xAxis);
const yAxis = d3.axisLeft(yScale);
svg
.select(&quot;#y-axis&quot;)
.style(&quot;font-size&quot;, &#39;16px&#39;)
.call(yAxis)
svg.selectAll(&#39;g.tick&#39;);
const bars = svg
.selectAll(&quot;.bar&quot;)
.data(data)
.join(&quot;g&quot;)
.classed(&quot;bar&quot;, true);
bars.append(&quot;rect&quot;)
.style(&quot;transform&quot;, &quot;scale(1, -1)&quot;)
.attr(&quot;x&quot;, (_, index) =&gt; xScale(index.toString()))
.attr(&quot;y&quot;, -height)
.attr(&quot;width&quot;, xScale.bandwidth())
.transition()
.delay((_, index) =&gt; index * 500)
.duration(1000)
.attr(&quot;fill&quot;, d =&gt; d.color)
.attr(&quot;height&quot;, (d) =&gt; height - yScale(d.value));
}, [data]);
return (
&lt;svg ref={svgRef} height={height} width={width} /&gt;
);
};
const data = [
{value: 50, color: &#39;#008&#39;}, 
{value: 100, color: &#39;#00C&#39;}, 
{value: 150, color: &#39;#00f&#39;}
];
ReactDOM.render(
&lt;BarChart data={data} width={300} height={170} /&gt;, 
document.getElementById(&quot;chart&quot;)
);

<!-- language: lang-html -->

&lt;script src=&quot;https://cdnjs.cloudflare.com/ajax/libs/d3/7.8.4/d3.min.js&quot;&gt;&lt;/script&gt;
&lt;script crossorigin src=&quot;https://unpkg.com/react@16/umd/react.development.js&quot;&gt;&lt;/script&gt;
&lt;script crossorigin src=&quot;https://unpkg.com/react-dom@16/umd/react-dom.development.js&quot;&gt;&lt;/script&gt;
&lt;div id=&#39;chart&#39;&gt;&lt;/div&gt;

<!-- end snippet -->

huangapple
  • 本文由 发表于 2023年6月1日 22:53:29
  • 转载请务必保留本文链接:https://go.coder-hub.com/76383192.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定