DEV Community

Son Pham
Son Pham

Posted on

Build a hierarchy force graph by D3v7 and React + Vite

In recent features, I was involved to building graph visualization which built used D3js(v7). In this post, i'll show you how to create simple hierarchy force graph using D3 for visualizing connections in your data.

Creating The App

We'll start by creating a new React app using Vite and TypeScript

yarn create vite d3-hierarchy-force-graph --template react-ts
Enter fullscreen mode Exit fullscreen mode

This command will generate a new Vite and React Typescript project. After the project was created, get into the app folder and add D3 to it by using the following command:

yarn add d3 @types/d3
Enter fullscreen mode Exit fullscreen mode

Now that all the libraries we need are in the project, it’s time to move forward

Building Graph

Here is the code for the Graph component container:

export const HierarchyForceGraph = () => {
const containerRef = useRef(null)
useEffect(() => {
let _destroy = null;
if(containerRef.current) {
const {destroy} = runGraph({container: containerRef.current})
_destroy = destroy
}
return () => _destroy.()
}, []);
return <div ref={containerRef} style={{height: 500, width: 600}}/>
}

We will add the D3 to generate the graph

const rect = container.getBoundingClientRect();
const {width, height} = rect;
const svg = d3.select(container).append("svg").attr("width", "100%").attr("height", "100%").attr("viewBox", `0 0 ${width} ${height}`)
const simulation = d3
.forceSimulation()
.force(
"collide",
d3
.forceCollide()
.radius(() => 50)
.strength(0.2)
)
.force(
"x",
d3.forceX().x(() => width / 2)
)
.force("charge", d3.forceManyBody().strength(100));
const drawLinks = (links: any) => {
svg.append("g").attr("class", "links");
let link = svg
.select(".links")
.selectAll(".link")
.data(links, (d) => {
return d.index as number;
});
link.exit().remove();
const linkEnter = link
.enter()
.append("line")
.attr("class", "link")
.attr("stroke", "#9D9D9D")
.attr("stroke-dasharray", () => "4, 6")
.attr("stroke-width", () => "0.85px");
link = linkEnter.merge(link);
};
const drawNodes = (nodes: any) => {
svg.append("g").attr("class", "nodes");
let nodeContainer = svg
.select(".nodes")
.selectAll(".node")
.data(nodes, (d) => d.index as number);
nodeContainer.exit().remove();
const nodeEnter = nodeContainer
.enter()
.append("g").attr("class", "node")
;
nodeEnter
.append("circle")
.attr("r", () => NODE_RADIUS)
.attr("fill", "#E9E9E9");
nodeEnter
.append("text")
.attr("y", 5)
.attr("text-anchor", "middle")
.attr("font-size", "16px")
.attr("font-weight", "bold")
.text((d: any) => {
return d.data.influencer_name
?.split(" ")
.slice(0, 2)
.map((str: string) => str.charAt(0))
.join("")
.toUpperCase();
});
// prevent link overlap title
nodeEnter
.append("text")
.attr("y", () => NODE_RADIUS * 1.5 + 3.5)
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.attr("fill", "none")
.attr("stroke", "white")
.attr("stroke-width", "11px")
.text((d: any) => {
return d.data.influencer_name;
});
nodeEnter
.append("text")
.attr("y", () => NODE_RADIUS * 1.5)
.attr("text-anchor", "middle")
.attr("font-size", "13px")
.text((d: any) => d.data.influencer_name);
nodeContainer = nodeEnter.merge(nodeContainer);
};
view raw runGraph.tsx hosted with ❤ by GitHub

When the graph is ready we will add nodes and links to simulation then add a few event handlers to handle what is going to happen when tick is happening

const {links, nodes} = flattenNode(data);
simulation.stop();
simulation.nodes(nodes);
const link = d3
.forceLink()
.id((d) => {
return d.index as number;
})
.distance(120);
link.links(links);
simulation.force("link", link);
drawLinks(links);
drawNodes(nodes);
simulation.on("tick", () => {
const linkElems = svg.select(".links").selectAll(".link");
const nodeElems = svg.select(".nodes").selectAll(".node");
nodes.forEach((d: SimulationNodeDatum & {depth: number}) => {
d.y = d.depth < 0 ? -80 : (d.depth) * 120 + 60;
});
nodeElems.attr("transform", (d: any) => {
return `translate(${d.x}, ${d.y})`;
});
linkElems
.attr("x1", (d: any) => {
return d.source.x
})
.attr("y1", (d: any) => {
return d.source.y + NODE_RADIUS + 10;
})
.attr("x2", (d: any) => d.target.x)
.attr("y2", (d: any) => d.target.y);
});
simulation.restart().alpha(1).alphaTarget(0);

Now that everything is set in place you can run the app and look at your fancy force graph.

Hierarchy force graph

Summary
In the post I showed how to create a hierarchy force graph using React + Vite and D3. You can find the graph code here.

Image of Timescale

🚀 pgai Vectorizer: SQLAlchemy and LiteLLM Make Vector Search Simple

We built pgai Vectorizer to simplify embedding management for AI applications—without needing a separate database or complex infrastructure. Since launch, developers have created over 3,000 vectorizers on Timescale Cloud, with many more self-hosted.

Read more →

Top comments (0)

Image of Docusign

🛠️ Bring your solution into Docusign. Reach over 1.6M customers.

Docusign is now extensible. Overcome challenges with disconnected products and inaccessible data by bringing your solutions into Docusign and publishing to 1.6M customers in the App Center.

Learn more