5. Advanced SVG: Layouts and Geospatial Data
D3.js extends far beyond basic bar and line charts. It provides powerful modules for organizing complex data into structured visual layouts and for rendering geographical information. This chapter will introduce you to these advanced SVG capabilities, enabling you to create sophisticated visualizations like hierarchical diagrams, network graphs, and interactive maps.
5.1 Hierarchical Layouts: Trees and Treemaps
Hierarchical data (data with parent-child relationships) is common in many domains. D3.js offers several layout algorithms to visualize this data effectively, including tree diagrams and treemaps.
Detailed Explanation
d3.hierarchy(data, [children]):
This is the foundational step for all hierarchical layouts. It takes your raw hierarchical data (e.g., a nested JSON object) and converts it into a D3.js-specific hierarchical data structure. Each node in this structure gets useful properties like depth, height, parent, children, data (original data), and value (sum of children’s values).
d3.tree():
The tree layout is designed to visualize hierarchical data as a node-link diagram, typically arranged radially or with children flowing from left-to-right or top-to-bottom.
.size([width, height]): Sets the size of the layout area..nodeSize([width, height]): If using fixed node sizes, specifies the size..separation(function): Controls the separation between sibling and non-sibling nodes.
d3.treemap():
The treemap layout displays hierarchical data as a set of nested rectangles, where the size of each rectangle is proportional to its value. It’s excellent for showing relative proportions and hierarchical structure in a compact space.
.size([width, height]): Sets the size of the treemap area..padding(amount): Sets padding around and between cells..tile(tileMethod): Specifies how rectangles are arranged (e.g.,d3.treemapSquarify,d3.treemapBinary).
Code Examples: Tree Diagram
Let’s create a simple tree diagram.
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3.js Tree Layout</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; }
.chart-container {
border: 1px solid #ccc;
background-color: #f9f9f9;
margin-top: 20px;
border-radius: 8px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
}
.node circle {
fill: #fff;
stroke: steelblue;
stroke-width: 3px;
}
.node text {
font-size: 11px;
font-family: sans-serif;
}
.link {
fill: none;
stroke: #ccc;
stroke-width: 2px;
}
</style>
</head>
<body>
<h1>D3.js: Tree Diagram</h1>
<div class="chart-container">
<svg id="tree-chart" width="960" height="500"></svg>
</div>
<script type="module" src="./app.js"></script>
</body>
</html>
app.js
import * as d3 from 'd3';
// Sample hierarchical data
const treeData = {
"name": "Root",
"children": [
{
"name": "Branch A",
"children": [
{ "name": "Leaf A1", "value": 10 },
{ "name": "Leaf A2", "value": 20 }
]
},
{
"name": "Branch B",
"children": [
{ "name": "Leaf B1", "value": 30 },
{ "name": "Branch B2",
"children": [
{ "name": "Leaf B2a", "value": 15 },
{ "name": "Leaf B2b", "value": 25 }
]
}
]
},
{ "name": "Leaf C", "value": 40 }
]
};
const svg = d3.select("#tree-chart");
const width = +svg.attr("width");
const height = +svg.attr("height");
const g = svg.append("g").attr("transform", "translate(40,0)"); // Shift right for labels
// 1. Create the D3.js hierarchy
const root = d3.hierarchy(treeData)
.sum(d => d.value); // Sum the 'value' property for leaf nodes, or sum children's values
// 2. Create the tree layout generator
const treeLayout = d3.tree()
.size([height, width - 160]); // [height, width] for horizontal tree
// 3. Compute the layout
treeLayout(root);
// 4. Draw the links (paths between nodes)
g.selectAll(".link")
.data(root.links()) // root.links() returns an array of {source, target} objects
.join("path")
.attr("class", "link")
.attr("d", d3.linkHorizontal() // Use d3.linkHorizontal for straight lines
.x(d => d.y) // x becomes y for horizontal layout
.y(d => d.x)); // y becomes x for horizontal layout
// 5. Draw the nodes (circles and text)
const node = g.selectAll(".node")
.data(root.descendants()) // root.descendants() returns all nodes (root, branches, leaves)
.join("g")
.attr("class", d => `node ${d.children ? "node--internal" : "node--leaf"}`)
.attr("transform", d => `translate(${d.y},${d.x})`); // y and x are computed by the tree layout
node.append("circle")
.attr("r", 10);
node.append("text")
.attr("dy", "0.31em")
.attr("x", d => d.children ? -15 : 15) // Position text left for internal, right for leaf
.attr("text-anchor", d => d.children ? "end" : "start")
.text(d => d.data.name);
// Optional: Add interactivity
node.on("mouseover", function(event, d) {
d3.select(this).select("circle").attr("r", 12).style("fill", "lightsteelblue");
d3.select(this).raise(); // Bring to front
})
.on("mouseout", function(event, d) {
d3.select(this).select("circle").attr("r", 10).style("fill", "#fff");
})
.on("click", function(event, d) {
alert(`Clicked: ${d.data.name} (Value: ${d.value || 'N/A'})`);
});
Exercises/Mini-Challenges for Tree Layout
- Vertical Tree: Modify the tree layout to display vertically (root at top, children flowing down). You’ll need to adjust
treeLayout.size()andd3.linkVertical(), and swapxandycoordinates inattr("transform")andlinkHorizontal()accessor functions. - Collapsible Tree: Implement a feature where clicking on an internal node toggles its children’s visibility (collapsing/expanding the subtree). This involves modifying the data structure (e.g., storing original children and current children) and re-drawing the chart.
- Color by Depth: Assign different fill colors to nodes based on their
depthproperty (fromroot.descendants()).
Code Examples: Treemap
Now, let’s visualize hierarchical data with a treemap.
index.html (Add a new SVG for the treemap)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3.js Treemap Layout</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; }
.chart-container {
border: 1px solid #ccc;
background-color: #f9f9f9;
margin-top: 20px;
border-radius: 8px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
}
.node-rect {
fill: steelblue;
opacity: 0.8;
stroke: white;
stroke-width: 1px;
transition: opacity 0.2s ease-out;
}
.node-rect:hover {
opacity: 1;
stroke: black;
}
.node-text {
font-size: 10px;
font-family: sans-serif;
fill: white;
pointer-events: none; /* Text should not block mouse events for rect */
text-shadow: 1px 1px 2px rgba(0,0,0,0.5);
}
</style>
</head>
<body>
<h1>D3.js: Treemap Diagram</h1>
<div class="chart-container">
<svg id="treemap-chart" width="960" height="500"></svg>
</div>
<script type="module" src="./app.js"></script>
</body>
</html>
app.js (Add the treemap code after the tree diagram code)
// ... (previous D3.js Tree Layout code) ...
// Treemap Data
const treemapData = {
name: "Analytics",
children: [
{ name: "Sales", children: [
{ name: "North", value: 300 },
{ name: "South", value: 450 },
{ name: "East", value: 200 },
{ name: "West", value: 350 }
]},
{ name: "Marketing", children: [
{ name: "Social Media", value: 180 },
{ name: "Email Campaigns", value: 220 },
{ name: "SEO", value: 150 }
]},
{ name: "Development", children: [
{ name: "Frontend", value: 280 },
{ name: "Backend", value: 320 },
{ name: "Database", value: 190 },
{ name: "Mobile", value: 160 }
]},
{ name: "Operations", value: 250 } // A leaf node at higher level
]
};
const treemapSvg = d3.select("#treemap-chart");
const treemapWidth = +treemapSvg.attr("width");
const treemapHeight = +treemapSvg.attr("height");
// 1. Create the D3.js hierarchy for treemap
const treemapRoot = d3.hierarchy(treemapData)
.sum(d => d.value) // Sum the 'value' for leaf nodes
.sort((a, b) => b.value - a.value); // Sort children by value
// 2. Create the treemap layout generator
const treemapLayout = d3.treemap()
.size([treemapWidth, treemapHeight])
.padding(1) // Padding between cells
.round(true); // Round to nearest pixel for crisp edges
// 3. Compute the layout
treemapLayout(treemapRoot);
// 4. Create a color scale for categories
const treemapColor = d3.scaleOrdinal(d3.schemePaired) // Categorical color scheme
.domain(treemapRoot.children.map(d => d.data.name)); // Top-level categories
// 5. Draw the rectangles and labels
const treemapNode = treemapSvg.selectAll("g")
.data(treemapRoot.descendants()) // All nodes, including parent and children
.join("g")
.attr("transform", d => `translate(${d.x0},${d.y0})`); // Position group at top-left of cell
treemapNode.append("rect")
.attr("class", "node-rect")
.attr("width", d => d.x1 - d.x0) // Width of the cell
.attr("height", d => d.y1 - d.y0) // Height of the cell
.attr("fill", d => d.children ? treemapColor(d.data.name) : treemapColor(d.parent.data.name)) // Color by parent category
.on("mouseover", function(event, d) {
d3.select(this).style("opacity", 1).style("stroke", "black").style("stroke-width", 2);
})
.on("mouseout", function(event, d) {
d3.select(this).style("opacity", 0.8).style("stroke", "white").style("stroke-width", 1);
})
.on("click", function(event, d) {
if (d.data.value) {
alert(`${d.data.name}: ${d.data.value}`);
} else {
alert(`Parent: ${d.data.name}`);
}
});
treemapNode.append("text")
.attr("class", "node-text")
.attr("x", 4)
.attr("y", 14)
.text(d => {
// Only show text for leaves or large enough internal nodes
const rectWidth = d.x1 - d.x0;
const rectHeight = d.y1 - d.y0;
if (rectWidth > 30 && rectHeight > 15) { // Ensure enough space
return d.data.name;
}
return ""; // Hide if not enough space
});
Exercises/Mini-Challenges for Treemap
- Padding Levels: Experiment with
treemapLayout.paddingOuter(),paddingInner(), andpaddingTop()to create different visual separation effects in the treemap. - Color by Value: Instead of coloring by category, create a
d3.scaleSequential()(e.g.,d3.interpolateBlues) and color the leaf nodes based on theirvalue. - Zoomable Treemap: Implement a “zoom” functionality where clicking on a parent node re-renders the treemap to focus only on that node’s children, making it the new “root.” A “breadcrumb” trail could allow navigation back up. This is a complex challenge, but great for learning!
5.2 Force-Directed Graphs: Visualizing Networks
Force-directed graphs are excellent for visualizing relationships (networks) between entities. D3.js’s d3-force module simulates physical forces to position nodes and links in a way that reveals their clusters and connections.
Detailed Explanation
The d3.forceSimulation() creates a simulation that applies forces to nodes. You define the nodes and links, and the simulation iteratively calculates their positions.
.nodes(nodesArray): Sets the array of nodes. Each node object should have at least anidand will gainx,y,vx,vyproperties from the simulation..force(name, forceFunction): Adds or removes a force.d3.forceLink(linksArray): Connects related nodes. Links should havesourceandtargetproperties (either indices or nodeids).d3.forceManyBody(): Simulates repulsion between nodes (like charged particles).d3.forceCenter([x, y]): Pulls nodes towards a specified center point.d3.forceX(x)/d3.forceY(y): Forces nodes towards a specific X or Y coordinate.
.on("tick", listener): Event listener that fires with each step of the simulation, allowing you to update the visual positions of nodes and links..alphaTarget(target): Sets the simulation’s target alpha (a measure of cooling). A higher target means a more active simulation.
Code Examples: Force-Directed Graph
index.html (Add a new SVG for the force graph)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3.js Force-Directed Graph</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; }
.chart-container {
border: 1px solid #ccc;
background-color: #f0f8ff;
margin-top: 20px;
border-radius: 8px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
}
.link {
stroke: #999;
stroke-opacity: 0.6;
stroke-width: 1.5px;
}
.node circle {
stroke: #fff;
stroke-width: 1.5px;
}
.node text {
font-size: 10px;
font-family: sans-serif;
pointer-events: none;
text-shadow: 0 1px 0 #fff, 1px 0 0 #fff, 0 -1px 0 #fff, -1px 0 0 #fff;
}
</style>
</head>
<body>
<h1>D3.js: Force-Directed Graph</h1>
<div class="chart-container">
<svg id="force-graph" width="800" height="600"></svg>
</div>
<script type="module" src="./app.js"></script>
</body>
</html>
app.js (Add the force graph code after the treemap code)
// ... (previous D3.js Treemap Layout code) ...
// Force-Directed Graph Data
const graph = {
"nodes": [
{"id": "Myriel", "group": 1},
{"id": "Napoleon", "group": 1},
{"id": "Mlle.Baptistine", "group": 1},
{"id": "Mme.Magloire", "group": 1},
{"id": "CountessdeLo", "group": 2},
{"id": "Labarre", "group": 2},
{"id": "Gribier", "group": 2},
{"id": "Fantine", "group": 3},
{"id": "Cosette", "group": 3},
{"id": "Valjean", "group": 3},
{"id": "Javert", "group": 4},
{"id": "Tholomyes", "group": 5},
{"id": "Fauchelevent", "group": 6},
{"id": "Bamatabois", "group": 7},
{"id": "Champmathieu", "group": 8}
],
"links": [
{"source": "Napoleon", "target": "Myriel", "value": 1},
{"source": "Mlle.Baptistine", "target": "Myriel", "value": 8},
{"source": "Mme.Magloire", "target": "Myriel", "value": 10},
{"source": "CountessdeLo", "target": "Myriel", "value": 1},
{"source": "Labarre", "target": "Napoleon", "value": 1},
{"source": "Gribier", "target": "Mme.Magloire", "value": 4},
{"source": "Fantine", "target": "Myriel", "value": 1},
{"source": "Fantine", "target": "Valjean", "value": 1},
{"source": "Cosette", "target": "Valjean", "value": 1},
{"source": "Javert", "target": "Valjean", "value": 5},
{"source": "Tholomyes", "target": "Fantine", "value": 6},
{"source": "Fauchelevent", "target": "Valjean", "value": 3},
{"source": "Bamatabois", "target": "Fantine", "value": 3},
{"source": "Champmathieu", "target": "Valjean", "value": 1}
]
};
const forceSvg = d3.select("#force-graph");
const forceWidth = +forceSvg.attr("width");
const forceHeight = +forceSvg.attr("height");
const forceG = forceSvg.append("g");
// Color scale for node groups
const groupColor = d3.scaleOrdinal(d3.schemeCategory10);
// 1. Create the force simulation
const simulation = d3.forceSimulation(graph.nodes)
.force("link", d3.forceLink(graph.links).id(d => d.id).distance(50)) // Links between nodes
.force("charge", d3.forceManyBody().strength(-300)) // Node repulsion
.force("center", d3.forceCenter(forceWidth / 2, forceHeight / 2)); // Pulls nodes to center
// 2. Add drag behavior
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x; // Fix position
d.fy = d.y;
}
function dragged(event, d) {
d.fx = event.x;
d.fy = event.y;
}
function dragended(event, d) {
if (!event.active) simulation.alphaTarget(0);
d.fx = null; // Release fixed position
d.fy = null;
}
const drag = d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended);
// 3. Draw the links
const link = forceG.append("g")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.selectAll("line")
.data(graph.links)
.join("line")
.attr("stroke-width", d => Math.sqrt(d.value)); // Thicker lines for higher value links
// 4. Draw the nodes
const node = forceG.append("g")
.attr("stroke", "#fff")
.attr("stroke-width", 1.5)
.selectAll("circle")
.data(graph.nodes)
.join("circle")
.attr("r", 8)
.attr("fill", d => groupColor(d.group))
.call(drag); // Apply drag behavior to nodes
// Add node labels
const labels = forceG.append("g")
.selectAll("text")
.data(graph.nodes)
.join("text")
.attr("dy", "0.31em")
.attr("text-anchor", "middle")
.text(d => d.id)
.style("fill", "#333");
// 5. Update positions on each tick of the simulation
simulation.on("tick", () => {
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);
node
.attr("cx", d => d.x)
.attr("cy", d => d.y);
labels
.attr("x", d => d.x)
.attr("y", d => d.y + 15); // Position label below node
});
Exercises/Mini-Challenges for Force-Directed Graph
- Node Sizing by Degree: Modify the nodes to have a radius (
r) that is proportional to their “degree” (number of connections). You’ll need to pre-calculate the degree for each node before the simulation starts. - Highlight on Hover: When hovering over a node, highlight that node and all its directly connected links and nodes. This will involve using
mouseoverandmouseoutevents. - Dynamic Force Parameters: Add controls (e.g., sliders) to adjust the
strength()of theforceManyBodyor thedistance()offorceLinkdynamically. See how the graph structure changes. - Pinning Nodes: Instead of simply fixing nodes while dragging, implement a “pinning” mechanism where a clicked node becomes permanently fixed (
d.fx = d.x; d.fy = d.y;) until clicked again.
5.3 Geospatial Data: Interactive Maps
D3.js provides a powerful set of tools in d3-geo for creating interactive maps from geographical data (GeoJSON or TopoJSON). This involves projection, path generation, and scaling.
Detailed Explanation
GeoJSON/TopoJSON: These are standard formats for encoding geographical data. GeoJSON uses geographic coordinates, while TopoJSON is a more compact extension that encodes topology (shared boundaries) and allows for smaller file sizes.
d3.geoPath():
This generator takes a GeoJSON feature and converts it into an SVG path d string.
.projection(projectionFunction): Specifies the geographical projection to use.
Projections (d3.geoMercator(), d3.geoAlbersUsa(), etc.):
Map projections transform 3D spherical coordinates (latitude, longitude) into 2D Cartesian coordinates for display on a flat surface. D3.js offers a wide array of projections:
d3.geoMercator(): A cylindrical projection, very common for web maps (like Google Maps).d3.geoAlbersUsa(): A composite projection designed for the contiguous United States, placing Alaska and Hawaii appropriately.- Many others for various purposes (conic, azimuthal, etc.).
projection.fitSize([width, height], geojsonObject):
A convenient method to automatically scale and translate a projection so that a given geojsonObject fits within a specified [width, height] bounding box.
Zooming and Panning (d3.zoom()):
For interactive maps, d3.zoom() is crucial. It captures pan and zoom gestures and applies transformations to a <g> element containing the map.
Code Examples: Basic Choropleth Map
Let’s create a simple map of U.S. states and color them based on some data.
index.html (Add a new SVG for the map)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>D3.js Geospatial Map</title>
<style>
body { font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; margin: 20px; }
.chart-container {
border: 1px solid #ccc;
background-color: #f0f8ff;
margin-top: 20px;
border-radius: 8px;
box-shadow: 2px 2px 8px rgba(0,0,0,0.1);
}
.state {
fill: #ccc;
stroke: #fff;
stroke-width: 0.5px;
transition: fill 0.2s ease;
}
.state:hover {
fill-opacity: 0.8;
}
.tooltip {
position: absolute;
text-align: center;
padding: 8px;
font: 12px sans-serif;
background: lightyellow;
border: 1px solid #333;
border-radius: 4px;
pointer-events: none;
opacity: 0;
}
</style>
</head>
<body>
<h1>D3.js: Geospatial Map (US States)</h1>
<p>Data is randomly generated for demonstration. Hover over states!</p>
<div class="chart-container">
<svg id="us-map" width="960" height="600"></svg>
<div class="tooltip" id="map-tooltip"></div>
</div>
<script type="module" src="./app.js"></script>
</body>
</html>
app.js (Add the map code after the force graph code)
// ... (previous D3.js Force-Directed Graph code) ...
// Geospatial Map
const mapSvg = d3.select("#us-map");
const mapWidth = +mapSvg.attr("width");
const mapHeight = +mapSvg.attr("height");
const mapTooltip = d3.select("#map-tooltip");
// 1. Define a projection
// Using d3.geoAlbersUsa() is good for US maps as it handles Alaska and Hawaii
const projection = d3.geoAlbersUsa()
.scale(1280) // Initial scale
.translate([mapWidth / 2, mapHeight / 2]); // Center the map
// 2. Create a geoPath generator with the projection
const pathGenerator = d3.geoPath()
.projection(projection);
// 3. Create a color scale for the map (e.g., for a choropleth map)
const colorScale = d3.scaleQuantize() // Quantize scale for continuous data mapped to discrete colors
.range(d3.schemeBlues[7]); // 7 shades of blue
// Random data for states
const stateData = new Map();
// We'll populate this with random values later
// Load GeoJSON data (you'd typically fetch this from a file)
// For this example, we'll use a placeholder for US states GeoJSON URL.
// In a real project, you would download 'us-states.json' or 'us-10m.v1.json' (TopoJSON)
const usStatesGeoJsonUrl = "https://cdn.jsdelivr.net/npm/us-atlas@3/states-10m.json"; // TopoJSON version
// Using d3.json to load the data
d3.json(usStatesGeoJsonUrl).then(us => {
// Convert TopoJSON to GeoJSON
const states = topojson.feature(us, us.objects.states);
// Generate random data for states
states.features.forEach(d => {
stateData.set(d.id, Math.random() * 100); // Associate a random value with each state ID
});
// Update color scale domain based on generated data
colorScale.domain(d3.extent(Array.from(stateData.values())));
// Append a group for the states
const statesGroup = mapSvg.append("g");
// Draw the states
statesGroup.selectAll("path")
.data(states.features)
.join("path")
.attr("class", "state")
.attr("d", pathGenerator) // Use the path generator to draw each state
.attr("fill", d => colorScale(stateData.get(d.id))) // Fill color based on data
.on("mouseover", function(event, d) {
d3.select(this).attr("fill-opacity", 0.8);
mapTooltip.html(`<strong>${d.properties.name}</strong><br/>Value: ${stateData.get(d.id).toFixed(2)}`)
.style("left", (event.pageX + 10) + "px")
.style("top", (event.pageY - 28) + "px")
.style("opacity", 1);
})
.on("mouseout", function(event, d) {
d3.select(this).attr("fill-opacity", 1);
mapTooltip.style("opacity", 0);
})
.on("click", function(event, d) {
alert(`State clicked: ${d.properties.name}, Value: ${stateData.get(d.id).toFixed(2)}`);
});
// Optional: Add state borders (can be a separate path for clearer lines)
statesGroup.append("path")
.attr("fill", "none")
.attr("stroke", "#fff")
.attr("stroke-linejoin", "round")
.attr("d", pathGenerator(topojson.mesh(us, us.objects.states, (a, b) => a !== b))); // Draws internal borders
// Implement Zoom and Pan
const zoom = d3.zoom()
.scaleExtent([1, 8]) // Allow zooming from 1x to 8x
.on("zoom", function(event) {
statesGroup.attr("transform", event.transform); // Apply zoom transform to the group
});
mapSvg.call(zoom); // Apply zoom behavior to the SVG
})
.catch(error => {
console.error("Error loading GeoJSON data:", error);
mapSvg.append("text")
.attr("x", mapWidth / 2)
.attr("y", mapHeight / 2)
.attr("text-anchor", "middle")
.attr("fill", "red")
.text("Error loading map data. Please check console.");
});
Note: For the geospatial example, you’ll need the topojson library if you’re loading TopoJSON files. You can include it via CDN (<script src="https://unpkg.com/topojson-client@3"></script>) or install it locally (npm install topojson-client).
Exercises/Mini-Challenges for Geospatial Data
- Change Projection: Experiment with a different projection, like
d3.geoConicEqualArea()ord3.geoOrthographic()(for a globe view). You’ll need to adjust thescaleandtranslatefor each. - Add City Markers: Load a separate dataset of major US cities (latitude, longitude). For each city, add a
<circle>or<svg:image>marker on the map, usingprojection([longitude, latitude])to get the screen coordinates. Add tooltips to these markers. - Color Legend: Create a small color legend (a series of colored rectangles with associated value ranges) to explain what the different shades of blue in the choropleth map represent.
- Interactive Zoom Reset: Add a button that, when clicked, resets the map zoom and pan to its original state (
mapSvg.call(zoom.transform, d3.zoomIdentity)).
You’ve now explored advanced SVG capabilities with D3.js, including powerful layouts for hierarchical data and the intricacies of geospatial visualization. These techniques open up a vast array of possibilities for presenting complex information in compelling ways. In the next chapter, we’ll shift gears from SVG to learn about using HTML Canvas for high-performance visualizations.