【问题标题】:Animating bar chart using d3 and react使用 d3 动画条形图并做出反应
【发布时间】:2021-08-19 05:36:14
【问题描述】:

我在 react 和 d3 中有以下条形图代码(遵循声明性方式)。我想在第一次渲染条形和轴时对其进行动画处理,然后在数据更改时设置动画,例如使用命令式方式使用 d3 api(使用 React,我希望以声明性方式保持内容)。到目前为止,我设法在第一次渲染时使用react-spring 设置了动画条,当我通过单击删除条时,调整了条的宽度。但是,我不确定如何在数据更新时为轴设置动画。我也不确定如何在其中一个条被删除后为条的位置设置动画。我想我可能需要一个 useRef 数组来表示 x 位置、高度和轴的刻度,但不知道该怎么做。

总结一下我的问题:

  1. 如何在数据更新时为条形的 x 位置和高度设置动画?
  2. 如何在数据更新时为坐标区的刻度和标签设置动画?

数据.json

[
  {
    "id": 0,
    "country": "China",
    "population": 1400000000
  },
  {
    "id": 1,
    "country": "India",
    "population": 1200000000
  },
  {
    "id": 2,
    "country": "USA",
    "population": 450000000
  }
]

App.js

import BarChart from './components/BarChart';

function App() {
  return <BarChart />;
}

export default App;

BarChart.js

import { useEffect, useRef } from 'react';
import { scaleBand, scaleLinear, max, format } from 'd3';
import initialData from '../data/data.json';
import { AxisBottom, AxisLeft } from './Axes';
import AnimatedMarks from './AnimatedMarks';
import Marks from './Marks';
import { useState } from 'react';

const svgDimensions = {
  width: 600,
  height: 600,
};

const margins = {
  left: 100,
  top: 20,
  right: 20,
  bottom: 100,
};

const gDimensions = {
  width: svgDimensions.width - margins.left - margins.right,
  height: svgDimensions.height - margins.top - margins.bottom,
};

const xValue = (d) => d.country;
const yValue = (d) => d.population;

const BarChart = () => {
  const [data, setData] = useState(initialData);

  const yScale = scaleLinear()
    .domain([0, max(data, yValue)])
    .range([gDimensions.height, 0]);

  const xScale = scaleBand()
    .domain(data.map(xValue))
    .range([0, gDimensions.width])
    .paddingInner(0.1)
    .paddingOuter(0.1);

  const prevBandwidth = useRef(xScale.bandwidth());
  

  useEffect(() => {
    prevBandwidth.current = xScale.bandwidth();
  }, [data]);

  c

  const deleteMarkOnClick = (i) => {
    setData(data.filter((d) => d.id !== i));
  };

  return (
    <svg width={svgDimensions.width} height={svgDimensions.height}>
      <g
        width={gDimensions.width}
        height={gDimensions.height}
        transform={`translate(${margins.left},${margins.top})`}
      >
        <AxisLeft
          yScale={yScale}
          tickFormat={(n) => format('.2s')(n).replace('G', 'B')}
        />

        <AxisBottom xScale={xScale} gDimensions={gDimensions} />

        <text
          className='xAxis-label'
          x={gDimensions.width / 2}
          y={gDimensions.height + 50}
        >
          COUNTRY
        </text>

        {data.map((d) => (
          <AnimatedMarks
            prevBandwidth={prevBandwidth}
            deleteMarkOnClick={deleteMarkOnClick}
            d={d}
            xScale={xScale}
            yScale={yScale}
            gDimensions={gDimensions}
            xValue={xValue}
            yValue={yValue}
          />
        ))}
      </g>
    </svg>
  );
};

export default BarChart;

AnimatedMarks.js

import { useEffect, useRef } from 'react';
import { useSpring, animated } from 'react-spring';
const AnimatedMarks = ({
  d,
  xScale,
  yScale,
  gDimensions,
  xValue,
  yValue,
  deleteMarkOnClick,
  prevBandwidth,
}) => {
  const style = useSpring({
    config: {
      duration: 1000,
    },
    from: {
      width: prevBandwidth.current,
      y: gDimensions.height,
      height: 0,
      opacity: 0,
    },
    to: {
      width: xScale.bandwidth(),
      y: yScale(yValue(d)),
      height: gDimensions.height - yScale(yValue(d)),
      opacity: 1,
    },
  });

  return (
    <animated.rect
      onClick={() => deleteMarkOnClick(d.id)}
      className='mark'
      key={d.id}
      x={xScale(xValue(d))}
      // y={yScale(yValue(d))}
      // width={xScale.bandwidth()}
      {...style}
      // height={gDimensions.height - yScale(yValue(d))}
    />
  );
};

export default AnimatedMarks;

Axes.js

export const AxisBottom = ({ xScale, gDimensions }) =>
  xScale.domain().map((xTickValue) => (
    <g
      className='tick'
      key={xTickValue}
      transform={`translate(${xScale(xTickValue) + xScale.bandwidth() / 2},${
        gDimensions.height + 20
      })`}
    >
      <line y1='-15' y2='-20' />
      <text>{xTickValue}</text>
    </g>
  ));

export const AxisLeft = ({ yScale, tickFormat }) =>
  yScale.ticks().map((yTickValue) => (
    <g
      className='tick'
      key={yTickValue}
      transform={`translate(0,${yScale(yTickValue)})`}
    >
      <line x1='5' x2='15' stroke='black' />
      <text dy='0.32em' style={{ textAnchor: 'end' }}>
        {tickFormat(yTickValue)}
      </text>
    </g>
  ));

编辑:这是codesandbox上的代码

【问题讨论】:

  • 你能准备一个工作沙箱并发布一个链接吗?
  • @MichaelRovinsky 我将代码添加到代码沙箱中,并将链接包含在我的问题中。
  • 刚刚检查了您的沙箱。每次更改 BarChart 组件中的数据时,都需要重新创建 xScaleyScalexScale = useMemo(..., [data])
  • @MichaelRovinsky 这在我更新数据时会自动发生,因为数据是一个状态变量。 xScale 和 yScale 正确更新,轴刻度和标签正确更改。我的问题是如何动画轴刻度和标签的变化。在d3 api中我们使用.transition().duration(),但是在React中当我们使用声明式的方式时,如何实现这个转换呢?我实现了带宽的转换,因为它对所有条形都是固定的。但其他过渡似乎很棘手。

标签: javascript reactjs d3.js


【解决方案1】:

这是一个在 React 组件中使用 useEffect 调用 D3 转换的示例:

const {useRef, useState, useEffect} = React;

const Chart = () => {
  const [rectWidth, setRectWidth] = useState(100);
  
  const svgRef = useRef();
  
  useEffect(() => {
    setInterval(() => {
      const width = 50 + Math.random() * 200;
      setRectWidth(width);
    }, 1000);
  }, []);
  
  useEffect(() => {
    if (svgRef.current) {
      d3.select(svgRef.current)
        .select('rect')
        .transition()
        .duration(500)
        .attr('width', rectWidth)
    }
  }, [rectWidth, svgRef]);

  return (
      <svg ref={svgRef} width={300} height={100}>
        <rect x={10} y={10} height={20} />
      </svg>
    )
}

ReactDOM.render(<Chart />, document.querySelector("#chart"))
svg {
  background-color: white;
}

rect {
  stroke: none;
  fill: green;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.7.0/d3.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.production.min.js"></script>

<div id="chart"></div>

【讨论】:

  • 我知道我们在 React 中使用本机 d3 api 的这种命令式方法。但我试图避免它,因为 React 现在支持 JSX 中的 svg 元素,因此我想在 JSX 中使用声明性方法,这使得事情变得更容易和人类可读,除非涉及到动画/过渡,它我正在尝试使用 react-spring 从头开始​​实施。
  • 如果你使用了D3的缩放功能,那么使用其他的D3​​特性比如transition是没有问题的
  • 很公平。那么,如何在不将其更改为命令式的情况下将转换功能合并到我的代码中?
  • 就像在 sn-p 中一样:使用 useRef 从 SVG 元素中获取 ref,捕获 useEffect 中的数据更改并调用转换以进行适当的 D3 选择
猜你喜欢
  • 2021-07-21
  • 1970-01-01
  • 2014-01-25
  • 1970-01-01
  • 2015-12-07
  • 1970-01-01
  • 2014-07-18
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多