Hi, this is part 10 of my Using React (Hooks) with D3 video tutorial series, and in this one, we create an animated tree!
Code can be found here:
https://github.com/muratkemaldar/using-react-hooks-with-d3/tree/10-hierarchy
This is the data we work with and pass to the TreeChart component (see below):
const initialData = {
name: "😐",
children: [
{
name: "🙂",
children: [
{
name: "😀"
},
{
name: "😁"
},
{
name: "🤣"
}
]
},
{
name: "😔"
}
]
};
This is code I am using for creating a TreeChart component:
import React, { useRef, useEffect } from "react";
import { select, hierarchy, tree, linkHorizontal } from "d3";
import useResizeObserver from "./useResizeObserver";
function usePrevious(value) {
const ref = useRef();
useEffect(() => {
ref.current = value;
});
return ref.current;
}
function TreeChart({ data }) {
const svgRef = useRef();
const wrapperRef = useRef();
const dimensions = useResizeObserver(wrapperRef);
// only save the old data if we rendered it (with dimensions)
// otherwise the data in useEffect is always equal.
// reason: we want to animate when the data changes, but not the dimensions.
// we skip the initial render (with no dimensions, but data)
const previouslyRenderedData = usePrevious(dimensions ? data : null);
// will be called initially and on every data change
useEffect(() => {
const svg = select(svgRef.current);
if (!dimensions) return;
// utility to transform hierarchical data
// to be able to extract nodes (descendants) and links
const root = hierarchy(data);
const treeLayout = tree().size([dimensions.height, dimensions.width]);
const linkGenerator = linkHorizontal()
.x(link => link.y)
.y(link => link.x);
// enrich hierarchical data with coordinates
treeLayout(root);
// nodes
svg
.selectAll(".node")
.data(root.descendants())
.join(enter => enter.append("circle").attr("opacity", 0))
.attr("class", "node")
.attr("cx", node => node.y)
.attr("cy", node => node.x)
.attr("r", 4)
.transition()
.duration(500)
.delay(node => node.depth * 300)
.attr("opacity", 1);
// links
const enteringAndUpdatingLinks = svg
.selectAll(".link")
.data(root.links())
.join("path")
.attr("class", "link")
.attr("d", linkGenerator)
.attr("stroke-dasharray", function() {
const length = this.getTotalLength();
return `${length} ${length}`;
})
.attr("stroke", "black")
.attr("fill", "none")
.attr("opacity", 1);
// only if data has changed, do the animations
if (data !== previouslyRenderedData) {
enteringAndUpdatingLinks
.attr("stroke-dashoffset", function() {
return this.getTotalLength();
})
.transition()
.duration(500)
.delay(link => link.source.depth * 500)
.attr("stroke-dashoffset", 0);
}
// labels
svg
.selectAll(".label")
.data(root.descendants())
.join(enter => enter.append("text").attr("opacity", 0))
.attr("class", "label")
.attr("x", node => node.y)
.attr("y", node => node.x - 12)
.attr("text-anchor", "middle")
.attr("font-size", 24)
.text(node => node.data.name)
.transition()
.duration(500)
.delay(node => node.depth * 300)
.attr("opacity", 1);
}, [data, dimensions, previouslyRenderedData]);
return (
<div ref={wrapperRef} style={{ marginBottom: "2rem" }}>
<svg ref={svgRef}></svg>
</div>
);
}
export default TreeChart;
Enjoy!
Top comments (0)