Skip to content
D3.js Advanced — Geographies, Force Layout & Hierarchies Explained

D3.js Advanced — Geographies, Force Layout & Hierarchies Explained

DodaTech Updated Jun 6, 2026 12 min read

Advanced D3.js techniques let you build geographic maps, network visualizations, hierarchical layouts, and complex interactive graphics.

What You’ll Learn

By the end of this tutorial, you will be able to:

  • Render geographic maps using GeoJSON data and projections
  • Create choropleth maps that color regions by data values
  • Build force-directed network graphs with draggable nodes
  • Understand hierarchical layouts: treemap, pack, partition, and tree
  • Configure force simulation parameters for realistic physics

Why Advanced D3.js Matters

Simple bar charts and line charts handle most business data. But when Durga Antivirus Pro needs to visualize a malware infection spreading across a network, it needs a force-directed graph. When DodaZIP analyzes compression ratios across file type hierarchies, a treemap shows the proportions at a glance. When a security dashboard displays threat origins on a world map, geographic projections turn coordinates into an interactive visualization. These advanced techniques differentiate basic data presentation from professional data storytelling.

Learning Path

    flowchart LR
    A["D3.js Basics"] --> B["D3.js SVG"]
    B --> C["D3.js Scales & Axes"]
    C --> D["D3.js Charts"]
    D --> E["D3.js Interactions"]
    D --> F["D3.js Data Handling"]
    E --> G["D3.js Advanced"]
    F --> G
    G --> H["D3.js Reference"]
    G:::current
    
    classDef current fill:#4CAF50,color:#fff,stroke:#333,stroke-width:2px
  
Prerequisites: Complete the D3.js Charts and D3.js Interactions tutorials. These advanced topics build on everything you’ve learned so far.

Geographies (Maps)

D3 renders maps using GeoJSON or TopoJSON data with geographic projections that convert latitude/longitude to pixel coordinates.

Understanding Geographic Data

  • GeoJSON: Standard format storing geography as coordinates (lat/lng). Each feature has a geometry (point, line, or polygon) and properties (data).
  • TopoJSON: An extension of GeoJSON that stores geometry as shared arcs, reducing file size significantly (often 80% smaller). Must be converted to GeoJSON for rendering.

Projections — Flattening the Globe

A projection maps 3D spherical coordinates to 2D screen pixels. Different projections serve different purposes:

// Common projections
d3.geoMercator()            // Standard web map — familiar but distorts polar areas
d3.geoAlbersUsa()           // USA-focused — composite projection
d3.geoEquirectangular()     // Simple lat/lng grid — straightforward
d3.geoOrthographic()        // 3D globe effect — visually impressive
d3.geoNaturalEarth1()       // Aesthetic world map — good balance

Why projections matter: Without a projection, your map would use raw lat/lng as pixel coordinates — a 180° wide image that’s heavily distorted. Projections correct the distortion and center the map.

Drawing a World Map

<!DOCTYPE html>
<html>
<head>
    <title>World Map</title>
</head>
<body>
    <svg width="800" height="500" id="map"></svg>
    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/topojson-client@3"></script>
    <script>
        var width = 800, height = 500;
        var svg = d3.select("#map");

        // Step 1: Create a projection (Mercator, scaled and centered)
        var projection = d3.geoMercator()
            .scale(130)
            .translate([width / 2, height / 1.4]);

        // Step 2: Create a path generator with the projection
        var path = d3.geoPath().projection(projection);

        // Step 3: Load TopoJSON world data
        d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json")
            .then(function(world) {
                // Convert TopoJSON to GeoJSON features
                var countries = topojson.feature(world, world.objects.countries).features;

                // Step 4: Draw each country as a path
                svg.selectAll("path")
                    .data(countries)
                    .enter()
                    .append("path")
                    .attr("d", path)
                    .attr("fill", "#69b3a2")
                    .attr("stroke", "#fff")
                    .attr("stroke-width", 0.5);
            });
    </script>
</body>
</html>

Line by line:

  1. d3.geoMercator() creates the projection function.
  2. .scale(130) sets zoom level (higher = more zoomed in).
  3. .translate([width/2, height/1.4]) centers the map.
  4. d3.geoPath().projection(projection) creates a function that converts GeoJSON to SVG path strings.
  5. topojson.feature(world, world.objects.countries).features converts TopoJSON to GeoJSON features.

Choropleth Map — Color by Data Value

// data is a Map: country ID → value
var color = d3.scaleSequential()
    .domain([0, d3.max(data.values())])
    .interpolator(d3.interpolateBlues);

svg.selectAll("path")
    .data(features)
    .enter()
    .append("path")
    .attr("d", path)
    .attr("fill", function(d) {
        return color(data.get(d.id) || 0);  // color by value, gray if no data
    })
    .attr("stroke", "#fff");

Real-world use: A security dashboard showing threat levels by country. Countries with more detected threats get darker colors, helping analysts spot outbreak regions instantly.

Force-Directed Graph — Network Visualization

Force layouts simulate physical forces — nodes repel each other (like magnets), edges attract connected nodes (like springs), and a centering force keeps everything on screen.

var nodes = [
    { id: "Alice" }, { id: "Bob" }, { id: "Charlie" },
    { id: "Diana" }, { id: "Eve" }
];
var links = [
    { source: "Alice", target: "Bob" },
    { source: "Bob", target: "Charlie" },
    { source: "Charlie", target: "Diana" },
    { source: "Diana", target: "Alice" },
    { source: "Eve", target: "Bob" }
];

// Create the simulation — it runs continuously!
var simulation = d3.forceSimulation(nodes)
    .force("link", d3.forceLink(links).id(function(d) { return d.id; }))
    .force("charge", d3.forceManyBody().strength(-100))
    .force("center", d3.forceCenter(300, 200))
    .on("tick", ticked);

How the simulation works:

  1. Initialization: Nodes are placed randomly.
  2. Tick loop: On every “tick” (frame), forces are computed and positions updated.
  3. Convergence: Over time, the system settles into a stable layout.
  4. Tick handler: You update the DOM on every tick to reflect new positions.

The Tick Function

function ticked() {
    link.attr("x1", function(d) { return d.source.x; })
        .attr("y1", function(d) { return d.source.y; })
        .attr("x2", function(d) { return d.target.x; })
        .attr("y2", function(d) { return d.target.y; });

    node.attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; });
}

Force Configuration Reference

d3.forceSimulation(nodes)
    // Link force: how strongly edges pull nodes together
    .force("link", d3.forceLink(links).distance(50).strength(0.5))

    // Many-body force: repulsion (negative) or attraction (positive)
    .force("charge", d3.forceManyBody().strength(-200))

    // Centering force: pulls all nodes toward center
    .force("center", d3.forceCenter(width / 2, height / 2))

    // Collision: prevents overlap
    .force("collide", d3.forceCollide().radius(20))

    // Position forces: pin nodes to x/y lines
    .force("x", d3.forceX().strength(0.1))
    .force("y", d3.forceY().strength(0.1));

Making Nodes Draggable

node.call(d3.drag()
    .on("start", function(event, d) {
        if (!event.active) simulation.alphaTarget(0.3).restart();
        d.fx = d.x;  // fix node position
        d.fy = d.y;
    })
    .on("drag", function(event, d) {
        d.fx = event.x;
        d.fy = event.y;
    })
    .on("end", function(event, d) {
        if (!event.active) simulation.alphaTarget(0);
        d.fx = null;  // release node
        d.fy = null;
    })
);

Why fx/fy? Setting d.fx = value fixes the node’s x position — the simulation won’t move it. Setting to null releases it.

Hierarchical Layouts

Hierarchical (tree-structured) data powers treemaps, circle packing, and sunburst diagrams.

Data Format

Hierarchical data uses nested objects with a children array:

var data = {
    name: "Root",
    children: [
        { name: "A", value: 10 },
        { name: "B", children: [
            { name: "B1", value: 5 },
            { name: "B2", value: 8 }
        ]}
    ]
};

Creating a Hierarchy

var root = d3.hierarchy(data)
    .sum(function(d) { return d.value; });  // compute total value for each node

.sum() walks the tree and computes a value for each node by adding its own value to its children’s values. Required for size-based layouts (treemap, pack).

Treemap — Rectangular Subdivisions

Treemaps show proportions by dividing a rectangle into smaller rectangles:

// Compute treemap layout
d3.treemap()
    .size([width, height])
    .padding(2)(root);

// Draw leaves as rectangles
svg.selectAll("rect")
    .data(root.leaves())
    .enter()
    .append("rect")
    .attr("x", function(d) { return d.x0; })
    .attr("y", function(d) { return d.y0; })
    .attr("width", function(d) { return d.x1 - d.x0; })
    .attr("height", function(d) { return d.y1 - d.y0; })
    .attr("fill", function(d) { return color(d.parent.data.name); })
    .attr("stroke", "#fff");

// Add labels
svg.selectAll("text")
    .data(root.leaves())
    .enter()
    .append("text")
    .attr("x", function(d) { return d.x0 + 3; })
    .attr("y", function(d) { return d.y0 + 13; })
    .text(function(d) { return d.data.name; })
    .attr("font-size", "11px")
    .attr("fill", "white");

Real-world use: DodaZIP could use a treemap showing file sizes by type — large video files dominate the rectangle, tiny config files barely register. At a glance you see where storage is going.

Pack Layout (Circle Packing)

Nested circles — each parent circle contains its children:

d3.pack()
    .size([width, height])
    .padding(5)(root);

svg.selectAll("circle")
    .data(root.descendants())
    .enter()
    .append("circle")
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.y; })
    .attr("r", function(d) { return d.r; })
    .attr("fill", function(d) {
        return d.children ? "lightgray" : "steelblue";
    })
    .attr("stroke", "#999")
    .attr("fill-opacity", 0.6);

Tree Layout

Standard node-link tree diagram:

d3.tree()
    .size([height, width - 200])(root);

// Draw links
svg.selectAll("line")
    .data(root.links())
    .enter()
    .append("line")
    .attr("x1", function(d) { return d.source.x; })
    .attr("y1", function(d) { return d.source.depth * 80; })
    .attr("x2", function(d) { return d.target.x; })
    .attr("y2", function(d) { return d.target.depth * 80; })
    .attr("stroke", "#ccc");

// Draw nodes
svg.selectAll("circle")
    .data(root.descendants())
    .enter()
    .append("circle")
    .attr("cx", function(d) { return d.x; })
    .attr("cy", function(d) { return d.depth * 80; })
    .attr("r", 4)
    .attr("fill", "steelblue");

Choosing the Right Layout

LayoutBest ForAnalogy
TreemapShowing proportions within a wholeA chocolate bar broken into pieces
PackShowing containment relationshipsRussian nesting dolls
TreeShowing hierarchical structureA family tree
PartitionShowing relative proportions at each levelA stacked bar chart in hierarchy form
ClusterDendrograms, evolutionary treesSame as tree but leaves align

Chord Diagram — Flow Between Groups

Shows relationships between entities as arcs and ribbons:

var matrix = [
    [0, 10, 20],
    [10, 0, 15],
    [20, 15, 0]
];
var names = ["A", "B", "C"];

var chord = d3.chord()
    .padAngle(0.05)
    .sortSubgroups(d3.descending)(matrix);

var arc = d3.arc()
    .innerRadius(innerRadius)
    .outerRadius(outerRadius);

var ribbon = d3.ribbon()
    .radius(innerRadius);

// Draw group arcs (outer rings)
svg.selectAll("g.group")
    .data(chord.groups)
    .enter().append("path")
    .attr("d", arc)
    .attr("fill", function(d) { return color(names[d.index]); })
    .attr("stroke", "#000");

// Draw ribbons (connections between groups)
svg.selectAll("path.ribbon")
    .data(chord)
    .enter().append("path")
    .attr("d", ribbon)
    .attr("fill", function(d) { return color(names[d.source.index]); })
    .attr("opacity", 0.7);

Common Mistakes

1. GeoJSON/TopoJSON Not Loading Due to CORS

Loading map data from external URLs requires CORS support. If you get errors in Doda Browser, use reliable CDNs or serve files from your own domain.

2. Force Simulation Not Starting

Call simulation.restart() after adding or modifying nodes. The simulation won’t automatically run if .alpha() has decayed to 0.

3. Forgetting .sum() on Hierarchy Data

d3.treemap() and d3.pack() need node values to determine sizes. If you skip .sum(), all values default to 0 and nothing renders.

4. Incorrect Projection Parameters

Geographic projections need proper .scale() and .translate() for the given map extent. Default values rarely fit the visualization area. Use .fitSize([w, h], geojson) to auto-fit.

5. Force Layout Nodes Overlapping

Without d3.forceCollide(), nodes may overlap. Always add a collision force with an appropriate radius.

6. Not Updating Force Links on Data Change

When adding/removing nodes, you must update both the nodes array and the links:

simulation.nodes(newNodes);
simulation.force("link").links(newLinks);
simulation.alpha(1).restart();

Practice Questions

Question 1

What is the difference between GeoJSON and TopoJSON?

Answer: GeoJSON stores geometry as explicit coordinates. TopoJSON stores geometry as shared arcs (edges between points), which eliminates duplicate coordinates between adjacent features. TopoJSON files are typically 80% smaller and must be converted via topojson.feature() for D3 rendering.

Question 2

How do you update a force simulation when data changes?

Answer: Call simulation.nodes(newNodes) and simulation.force("link").links(newLinks), then simulation.alpha(1).restart() to reset the energy and restart the simulation.

Question 3

What does d3.hierarchy().sum() do?

Answer: It walks the tree and computes a value for each node by summing its own value (if any) with the values of its descendants. Size-based layouts (treemap, pack) use these values to determine proportions.

Question 4

Which projection is best for a world map?

Answer: d3.geoNaturalEarth1() is aesthetically pleasing and shows minimal distortion. d3.geoMercator() is familiar (Google Maps style) but distorts polar areas. d3.geoEquirectangular() is simple and fast.

Question 5

What is d.fx and d.fy in a force simulation?

Answer: They fix a node’s position — when set, the simulation won’t change that node’s x/y. Setting them to null releases the node. Used in drag handlers to let users pin and move nodes.

Challenge

Build a force-directed graph with 15 nodes and 20 links. Add hover highlighting: when hovering over a node, highlight its connected edges and dim everything else. Add a “reset” button that re-centers the graph.

FAQ

What is the difference between GeoJSON and TopoJSON?
GeoJSON stores geometry as coordinates (lat/lng). TopoJSON stores geometry as arcs shared between features, resulting in much smaller file sizes. TopoJSON must be converted to GeoJSON via topojson.feature() for D3 rendering.
How do I update the force simulation when data changes?
Modify the data array, rebind it, call simulation.nodes(newNodes) and simulation.force("link").links(newLinks) then simulation.alpha(1).restart().
What is d3.hierarchy() used for?
It converts a nested data object into a D3 hierarchy with methods like .sum(), .count(), .sort(), .leaves(), .descendants(), and .links().
Can I make geographic maps interactive?
Yes. Add event handlers to the path elements for hover effects, zoom, and tooltips. Use d3.zoom() with geographic projections for pan/zoom.
What is the best projection for world maps?
d3.geoNaturalEarth1() is aesthetically pleasing for world maps. d3.geoMercator() is familiar but distorts polar areas. d3.geoEquirectangular() is simple and fast.

Try It Yourself

Here’s a complete interactive network graph with drag, hover highlighting, and physics:

<!DOCTYPE html>
<html>
<head>
    <title>Network Graph — Try It Yourself</title>
    <style>
        body { font-family: sans-serif; padding: 20px; }
        svg { border: 1px solid #ddd; }
        .node { fill: #69b3a2; stroke: #fff; stroke-width: 2; cursor: pointer; }
        .node:hover { fill: #e91e63; }
        .link { stroke: #999; stroke-opacity: 0.6; }
        .label { font-size: 11px; fill: #333; }
    </style>
</head>
<body>
    <h2>Social Network Graph</h2>
    <p>Drag nodes to rearrange. Hover to highlight connections.</p>
    <svg width="600" height="400" id="graph"></svg>

    <script src="https://d3js.org/d3.v7.min.js"></script>
    <script>
        var svg = d3.select("#graph"), w = 600, h = 400;
        var data = {
            nodes: [
                { id: "Alice" }, { id: "Bob" }, { id: "Charlie" },
                { id: "Diana" }, { id: "Eve" }, { id: "Frank" },
                { id: "Grace" }, { id: "Hank" }
            ],
            links: [
                { source: "Alice", target: "Bob" },
                { source: "Alice", target: "Charlie" },
                { source: "Alice", target: "Diana" },
                { source: "Bob", target: "Charlie" },
                { source: "Bob", target: "Eve" },
                { source: "Charlie", target: "Frank" },
                { source: "Diana", target: "Grace" },
                { source: "Diana", target: "Hank" },
                { source: "Eve", target: "Frank" },
                { source: "Grace", target: "Hank" }
            ]
        };

        var link = svg.append("g").selectAll("line").data(data.links)
            .enter().append("line").attr("class", "link");

        var node = svg.append("g").selectAll("circle").data(data.nodes)
            .enter().append("circle").attr("class", "node").attr("r", 8)
            .call(d3.drag()
                .on("start", function(e, d) {
                    if (!e.active) sim.alphaTarget(0.3).restart();
                    d.fx = d.x; d.fy = d.y;
                })
                .on("drag", function(e, d) { d.fx = e.x; d.fy = e.y; })
                .on("end", function(e, d) {
                    if (!e.active) sim.alphaTarget(0);
                    d.fx = null; d.fy = null;
                })
            );

        var labels = svg.append("g").selectAll("text").data(data.nodes)
            .enter().append("text").attr("class", "label")
            .attr("dx", 12).attr("dy", 4)
            .text(function(d) { return d.id; });

        node.on("mouseover", function(e, d) {
            var connected = new Set();
            data.links.forEach(function(l) {
                if (l.source.id === d.id) connected.add(l.target.id);
                if (l.target.id === d.id) connected.add(l.source.id);
            });
            node.attr("opacity", function(n) {
                return n.id === d.id || connected.has(n.id) ? 1 : 0.2;
            });
            link.attr("opacity", function(l) {
                return l.source.id === d.id || l.target.id === d.id ? 1 : 0.1;
            });
        }).on("mouseout", function() {
            node.attr("opacity", 1);
            link.attr("opacity", 0.6);
        });

        var sim = d3.forceSimulation(data.nodes)
            .force("link", d3.forceLink(data.links).id(function(d) { return d.id; }).distance(80))
            .force("charge", d3.forceManyBody().strength(-150))
            .force("center", d3.forceCenter(w/2, h/2))
            .force("collide", d3.forceCollide().radius(20))
            .on("tick", function() {
                link.attr("x1", function(d) { return d.source.x; })
                    .attr("y1", function(d) { return d.source.y; })
                    .attr("x2", function(d) { return d.target.x; })
                    .attr("y2", function(d) { return d.target.y; });
                node.attr("cx", function(d) { return d.x; })
                    .attr("cy", function(d) { return d.y; });
                labels.attr("x", function(d) { return d.x; })
                    .attr("y", function(d) { return d.y; });
            });
    </script>
</body>
</html>

Try this: Drag a node and watch the physics rebalance. Hover over Alice to see her connections highlighted. Add more nodes to the array and see the layout adapt.

What’s Next

You’ve now covered the full spectrum of D3.js capabilities:

TutorialWhat You’ll Learn
D3.js ReferenceComplete API cheatsheet for daily development

Related topics: JavaScript, SVG, Data Visualization, Node.js, React


Built by the developers of Doda Browser, DodaZIP, and Durga Antivirus Pro — bringing secure, high-performance software to your digital life.

What’s Next

Congratulations on completing this D3Js Advanced tutorial! Here’s where to go from here:

  • Practice daily — Consistency is more important than long study sessions
  • Build a project — Apply what you learned by building something real
  • Explore related topics — Check out other tutorials in the same category
  • Join the community — Discuss with other learners and share your progress

Remember: every expert was once a beginner. Keep coding!

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro