Public
Edited
Nov 24
14 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// Create a scale for the node size based on population (size)
sizeScale = d3
.scaleSqrt()
.domain([0, d3.max(data.nodes, (d) => d.size)])
.range([4, 120]) // Adjust range for visual clarity
Insert cell
projection = d3
.geoNaturalEarth1()
.scale(150) // Adjust scale as necessary
.translate([width / 2, height / 2])
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
// create the map
createForceDirectedMap(data)
Insert cell
function createForceDirectedMap(data) {
const xyStrength = 2;
const chargeStrength = -200;
// Set up the force simulation
const simulation = d3
.forceSimulation(data.nodes)
.force(
"link",
d3
.forceLink(data.links)
.id((d) => d.id)
.distance(5) // Set a reasonable distance between nodes
)
.force("charge", d3.forceManyBody().strength(chargeStrength)) // Negative value for repulsion
.force(
"x",
d3
.forceX((d) => projection([d.longitude, d.latitude])[0])
.strength(xyStrength)
) // Force x based on longitude
.force(
"y",
d3
.forceY((d) => projection([d.longitude, d.latitude])[1])
.strength(xyStrength)
) // Force y based on latitude
.force(
"collide",
d3.forceCollide((d) => sizeScale(d.size) / 2 + 2)
) // Prevent overlap by setting minimum distance between circles
//.force("center", d3.forceCenter(width / 2, height / 2)) // Optional, to center the entire layout
.on("tick", ticked); // Trigger ticked on every tick

// Create links (connections between countries)
const link = svg
.append("g")
.attr("class", "links")
.selectAll("line")
.data(data.links)
.enter()
.append("line")
.attr("class", "link")
.attr("stroke", "#999")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", 0.8);

// Define patterns for flag images to use as fills
const defs = svg.append("defs");
// Choose flag width based on the result of sizeScale(d.size)
const getFlagWidth = (size) => {
if (size > 80) return 320;
if (size > 40) return 320;
if (size > 20) return 160;
return 80;
};
data.nodes.forEach((d) => {
defs
.append("pattern")
.attr("id", `flag-${d.id}`)
.attr("patternUnits", "objectBoundingBox")
.attr("width", 1)
.attr("height", 1)
.append("image")
.attr(
"xlink:href",
`https://flagcdn.com/w${getFlagWidth(
sizeScale(d.size)
)}/${d.id.toLowerCase()}.png`
)
.attr("width", sizeScale(d.size))
.attr("height", sizeScale(d.size))
.attr("preserveAspectRatio", "xMidYMid slice");
});

// Create nodes (countries as circles filled with flags)
const node = svg
.append("g")
.attr("class", "nodes")
.selectAll("circle")
.data(data.nodes)
.enter()
.append("circle")
.attr("r", (d) => sizeScale(d.size) / 2) // Set radius of the circle based on size
.attr("cx", (d) => projection([d.longitude, d.latitude])[0]) // Set x based on projection
.attr("cy", (d) => projection([d.longitude, d.latitude])[1]) // Set y based on projection
.attr("fill", (d) => `url(#flag-${d.id})`) // Use the pattern with the flag as fill
.attr("stroke", "#333") // Optional: Add a stroke to the circle
.attr("stroke-width", 1)
.on("mouseover", function (event, d) {
if (dragging) return;
// Show tooltip on hover
tooltip
.style("visibility", "visible")
.html(
`<strong>${
d.label
}</strong><br>Population: ${d.size.toLocaleString()}`
);
})
.on("mousemove", function (event) {
// Move tooltip with the mouse
tooltip
.style("top", event.pageY - 10 + "px")
.style("left", event.pageX + 10 + "px");
})
.on("mouseout", function () {
// Hide tooltip when mouse leaves
tooltip.style("visibility", "hidden");
})
.call(
d3
.drag() // Add drag behavior to the nodes if necessary
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended)
);

// Add pan and zoom
svg.call(zoom);

// Update positions on each tick
function ticked() {
node
.attr("cx", (d) => d.x) // Update x position during simulation
.attr("cy", (d) => d.y); // Update y position during simulation
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);
}

// Define drag events
let dragging = false;
function dragstarted(event, d) {
dragging = true;
if (!event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}

function dragged(event, d) {
dragging = true;
d.fx = event.x;
d.fy = event.y;
}

function dragended(event, d) {
dragging = false;
if (!event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
}
Insert cell
Insert cell
Insert cell
Insert cell
nodes = {
return fetch("https://restcountries.com/v3.1/all")
.then((response) => response.json())
.then((data) => {
const nodes = data
.filter((country) => country.population && country.population > 0) // Filter out countries with 0 or null population
.map((country) => ({
id: country.cca2, // ISO 2-letter country code
label: country.name.common, // Country name
size: country.population, // Population
latitude: country.latlng[0], // Latitude
longitude: country.cca2 === "WS" ? 172 : country.latlng[1] // Adjust Samoa's longitude to 172
}));

return nodes;
})
.catch((error) => {
console.error("Error fetching country data:", error);
});
}
Insert cell
<style>.nodes{cursor:pointer}.nodes > circle:hover{stroke-width:3;}</style>
Insert cell
d3 = require("d3", "d3-geo-projection@4")
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more