I have the following bar chart code in react and d3 (following the declarative way). I would like to animate the bars and axes when they are first rendered, and after that whenever the data change, like using d3 api using the imperative way (with React I would like to keep things the declarative way). So far I managed to animate the bars using react-spring
when they are first rendered, and when I delete a bar by clicking on it, the width of the bars is adjusted. However, I am not sure how to animate the axes when the data is updated. I am also not sure how to animate the position of the bars after one of them is deleted. I am thinking I may need an array of useRef
for the x position, height and axes's ticks, but not sure how to go about it.
To summarize my questions:
data.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>
));
EDIT: This is the code on codesandbox
Here is an example of calling D3 transition in a React component with useEffect
:
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>
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.