A Deep Dive into Graph Visualization with D3.js
D3.js has become the go-to library for creating custom interactive data visualizations on the web. Its flexibility and extensive feature set make it a powerful tool in the hands of any data visualization practitioner.
While D3 can be used to build a wide variety of chart types, it really shines when it comes to visualizing graphs and networks. Its force-directed layout and support for advanced techniques like edge bundling make it ideal for visualizing complex connected data.
In this post, we‘ll take a deep dive into graph visualization with D3. We‘ll walk through the code to create some basic graph visualizations, discuss some more advanced techniques, and study some real-world examples to get you inspired. By the end, you‘ll have the knowledge and tools you need to start building your own stunning graph visualizations with D3.
The Building Blocks of Graph Visualization in D3
At its core, a graph visualization built with D3 consists of two main elements – nodes and links. Nodes represent the entities in the graph, while links represent the relationships or connections between those nodes.
To create a basic graph visualization in D3, you start by defining your nodes and links as data. Here‘s a simple example:
const nodes = [
{ id: "Alice" },
{ id: "Bob" },
{ id: "Charlie" },
{ id: "Diana" }
];
const links = [
{ source: "Alice", target: "Bob" },
{ source: "Alice", target: "Charlie" },
{ source: "Bob", target: "Diana" },
{ source: "Charlie", target: "Diana" }
];
With your data defined, you can then use D3‘s data binding to create SVG elements for each node and link. Here‘s what that looks like:
// Select the SVG element
const svg = d3.select("svg");
// Create a link element for each link in the data
const link = svg
.selectAll(".link")
.data(links)
.join("line")
.attr("class", "link")
.attr("stroke", "#999");
// Create a node element for each node in the data
const node = svg
.selectAll(".node")
.data(nodes)
.join("circle")
.attr("class", "node")
.attr("r", 10)
.attr("fill", "#333");
This gives you the basic visual elements of the graph, but they still need to be positioned. This is where D3‘s force layout comes in.
Bringing the Graph to Life with D3‘s Force Layout
The force layout is the heart of most D3 graph visualizations. It uses a physics simulation to calculate the positions of nodes based on the links between them. Linked nodes attract each other, while all nodes are repelled from each other to avoid overlap.
Here‘s how you create a force layout and apply it to the nodes and links:
// Initialize the force layout
const force = d3.forceSimulation(nodes)
.force("charge", d3.forceManyBody().strength(-100))
.force("link", d3.forceLink(links).distance(50))
.force("center", d3.forceCenter(width / 2, height / 2));
// Apply the force layout to the nodes and links
force.on("tick", () => {
node.attr("cx", d => d.x).attr("cy", d => d.y);
link
.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
});
The forceSimulation function creates the layout and applies it to the array of nodes. The force functions define the forces acting on the nodes – in this case a charge force that repels nodes from each other, a link force that attracts linked nodes, and a centering force.
On each "tick" of the simulation, the nodes‘ positions are updated based on the forces acting on them. The "tick" handler function updates the positions of the SVG node and link elements accordingly.
With this code, you have a basic force-directed graph visualization! The nodes will bounce around based on the forces until they reach a stable layout.
Of course, this is just a starting point. You can customize the force parameters, add interactivity, incorporate images and labels, and much more. But these are the essential building blocks that most D3 graph visualizations are built upon.
Taking Graph Visualization to the Next Level
While a simple force-directed graph can be illuminating on its own, D3 also supports a number of more advanced techniques to take your graph visualizations to the next level. Let‘s look at a few of them.
Hierarchical Edge Bundling
Hierarchical edge bundling is a technique for reducing clutter in a graph visualization by grouping related edges together into "bundles". This is particularly useful for visualizing large graphs with many interconnected nodes, like social networks or dependency graphs.
D3 has built-in support for hierarchical edge bundling. Here‘s a basic example:
// Create the edge bundle layout
const bundle = d3.layout.bundle();
// Calculate the bundled paths
const paths = bundle(links);
// Create path elements for each bundled path
svg.selectAll(".link")
.data(paths)
.join("path")
.attr("class", "link")
.attr("d", d3.svg.line().tension(0.5))
.attr("stroke", "#999");
The d3.layout.bundle function calculates the bundled paths based on the link data. These paths are then used to create SVG path elements, with some custom path rendering using the d3.svg.line().tension() function to smooth out the bundles.
The result is a graph visualization where related edges are bundled together, making the overall structure much clearer.
Combining Graphs with Maps
Another powerful technique is combining graph data with geographical data to create a "geo-graph". In a geo-graph, the nodes are positioned based on geographical coordinates, and the links represent connections between these locations.
D3 makes this easy by allowing you to overlay your graph elements on top of a geographical map using its extensive mapping and projection capabilities. Here‘s a simplified example:
// Create a geo projection
const projection = d3.geoMercator()
.scale(100)
.center([0, 50])
.translate([width / 2, height / 2]);
// Create a path generator for the map
const path = d3.geoPath().projection(projection);
// Draw the map
svg.append("path")
.datum(mapData)
.attr("d", path)
.attr("fill", "#eee")
.attr("stroke", "#ccc");
// Draw the nodes on the map
node.attr("cx", d => projection([d.lon, d.lat])[0])
.attr("cy", d => projection([d.lon, d.lat])[1]);
First, a geographical projection is created using d3.geoMercator(). This projection is used to convert geographical coordinates (longitude/latitude) into screen coordinates.
The map is drawn using a path element, with the shape defined by passing the map GeoJSON data through the d3.geoPath().projection() function.
Finally, the node positions are set by passing each node‘s lon and lat coordinates through the projection function.
The end result is a graph visualization where nodes are positioned based on their real-world geographical locations, providing spatial context to the graph data.
Matrix Views
For dense graphs with lots of interconnections, a matrix view can be a more effective visualization than a node-link diagram. In a matrix view, nodes are represented as rows and columns, and the links between them are represented as cells at the intersections.
D3‘s d3.scaleBand() function makes it straightforward to create a matrix layout:
const matrix = [];
nodes.forEach((source, a) => {
nodes.forEach((target, b) => {
matrix.push({
source, target,
x: b, y: a,
value: links.some(l => l.source === source.id && l.target === target.id) ? 1 : 0
});
});
});
const x = d3.scaleBand()
.range([0, width])
.domain(d3.range(nodes.length));
const y = d3.scaleBand()
.range([0, height])
.domain(d3.range(nodes.length));
svg.selectAll(".cell")
.data(matrix)
.join("rect")
.attr("class", "cell")
.attr("x", d => x(d.x))
.attr("y", d => y(d.y))
.attr("width", x.bandwidth())
.attr("height", y.bandwidth())
.attr("fill-opacity", d => d.value)
The matrix is generated by creating an entry for each possible source-target pair. The x and y band scales are used to calculate the position of each cell based on the source and target indices.
The cells are then rendered as rect elements, with the opacity set based on whether a link exists for that source-target pair (1 if a link exists, 0 if not).
Matrix views can reveal patterns in the graph structure that may not be evident in a node-link diagram, such as clusters of highly interconnected nodes.
Real-World Inspiration
To help get your creative juices flowing, let‘s look at a few impressive real-world examples of graph visualizations built with D3.
Visualizing Music Trends
The Every Noise at Once project uses D3 to visualize the relationships between over 5,000 musical genres. Each genre is represented as a node, with node size based on popularity. Genres are clustered based on similarity, and the user can explore the graph by zooming and panning.

This is a great example of using graph visualization to explore a complex and highly interconnected dataset. The use of clustering and interactivity makes it easy to dive deep into the data.
Mapping Global Migrations
The Global Migration Map is an interactive geo-graph that visualizes migration flows between countries. Each country is represented as a node, positioned geographically. Links represent migration flows, with the width of the link proportional to the size of the flow.

This visualization beautifully illustrates the power of combining graph data with geographical data. The use of a map projection provides instant context, while the graph overlay reveals patterns in global migration that might be hard to discern from the raw data.
Social Network Analysis
Graph visualization is a natural fit for social network analysis, as demonstrated by this visualization of character co-occurrence in Victor Hugo‘s Les Misérables.

Each character is a node, with links representing co-occurrence in the same chapter of the book. Node size is proportional to the number of chapters the character appears in. The force-directed layout reveals natural clusters of characters that frequently interact.
This is a classic example of using a force-directed graph for character network analysis. It shows how graph visualization can reveal insights into the structure of a story and the relationships between characters.
Just the Beginning
This post has provided an in-depth introduction to graph visualization with D3, but it‘s really just scratching the surface of what‘s possible. With its flexibility and extensive ecosystem of plugins and companion libraries, D3 provides endless opportunities for creating innovative and insightful graph visualizations.
Some additional topics to explore include:
- Using the cola.js library for constraint-based graph layouts
- Techniques for large graph visualization, such as WebGL rendering
- Creating interactive graph explorers
- Animating changes in graph structure over time
The possibilities are endless. I encourage you to dive into the examples and resources linked throughout this post, and to start experimenting with building your own graph visualizations using D3.
With a little creativity and a lot of tinkering, you‘ll be creating stunning and insightful graph visualizations in no time. Happy visualizing!