This post describes how to build a clean
circular packing with d3.js. It
shows how to build several convenient feature: nodes are draggable,
their size depends on a numerical value, their color on a categorical
value. Hovering them display more info about them. This example works
with d3.js v4
and v6
.csv
file with one line per country,
one column per feature. Provides the world population per
country.
Read more.
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>
<!-- Color palette -->
<script src="https://d3js.org/d3-scale-chromatic.v1.min.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<style>
.node:hover{
stroke-width: 7px !important;
opacity: 1 !important;
}
</style>
<!DOCTYPE html>
<meta charset="utf-8">
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v6.js"></script>
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
<script>
// set the dimensions and margins of the graph
var width = 460
var height = 460
// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", height)
// Read data
d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/11_SevCatOneNumNestedOneObsPerGroup.csv", function(data) {
// Filter a bit the data -> more than 1 million inhabitants
data = data.filter(function(d){ return d.value>10000000 })
// Color palette for continents?
var color = d3.scaleOrdinal()
.domain(["Asia", "Europe", "Africa", "Oceania", "Americas"])
.range(d3.schemeSet1);
// Size scale for countries
var size = d3.scaleLinear()
.domain([0, 1400000000])
.range([7,55]) // circle will be between 7 and 55 px wide
// create a tooltip
var Tooltip = d3.select("#my_dataviz")
.append("div")
.style("opacity", 0)
.attr("class", "tooltip")
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "2px")
.style("border-radius", "5px")
.style("padding", "5px")
// Three function that change the tooltip when user hover / move / leave a cell
var mouseover = function(d) {
Tooltip
.style("opacity", 1)
}
var mousemove = function(d) {
Tooltip
.html('<u>' + d.key + '</u>' + "<br>" + d.value + " inhabitants")
.style("left", (d3.mouse(this)[0]+20) + "px")
.style("top", (d3.mouse(this)[1]) + "px")
}
var mouseleave = function(d) {
Tooltip
.style("opacity", 0)
}
// Initialize the circle: all located at the center of the svg area
var node = svg.append("g")
.selectAll("circle")
.data(data)
.enter()
.append("circle")
.attr("class", "node")
.attr("r", function(d){ return size(d.value)})
.attr("cx", width / 2)
.attr("cy", height / 2)
.style("fill", function(d){ return color(d.region)})
.style("fill-opacity", 0.8)
.attr("stroke", "black")
.style("stroke-width", 1)
.on("mouseover", mouseover) // What to do when hovered
.on("mousemove", mousemove)
.on("mouseleave", mouseleave)
.call(d3.drag() // call specific function when circle is dragged
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Features of the forces applied to the nodes:
var simulation = d3.forceSimulation()
.force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
.force("charge", d3.forceManyBody().strength(.1)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.2).radius(function(d){ return (size(d.value)+3) }).iterations(1)) // Force that avoids circle overlapping
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(data)
.on("tick", function(d){
node
.attr("cx", function(d){ return d.x; })
.attr("cy", function(d){ return d.y; })
});
// What happens when a circle is dragged?
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(.03).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(.03);
d.fx = null;
d.fy = null;
}
})
</script>
<script>
// set the dimensions and margins of the graph
const width = 460
const height = 460
// append the svg object to the body of the page
const svg = d3.select("#my_dataviz")
.append("svg")
.attr("width", width)
.attr("height", height)
// Read data
d3.csv("https://raw.githubusercontent.com/holtzy/data_to_viz/master/Example_dataset/11_SevCatOneNumNestedOneObsPerGroup.csv").then( function(data) {
// Filter a bit the data -> more than 1 million inhabitants
data = data.filter(function(d){ return d.value>10000000 })
// Color palette for continents?
const color = d3.scaleOrdinal()
.domain(["Asia", "Europe", "Africa", "Oceania", "Americas"])
.range(d3.schemeSet1);
// Size scale for countries
const size = d3.scaleLinear()
.domain([0, 1400000000])
.range([7,55]) // circle will be between 7 and 55 px wide
// create a tooltip
const Tooltip = d3.select("#my_dataviz")
.append("div")
.style("opacity", 0)
.attr("class", "tooltip")
.style("background-color", "white")
.style("border", "solid")
.style("border-width", "2px")
.style("border-radius", "5px")
.style("padding", "5px")
// Three function that change the tooltip when user hover / move / leave a cell
const mouseover = function(event, d) {
Tooltip
.style("opacity", 1)
}
const mousemove = function(event, d) {
Tooltip
.html('<u>' + d.key + '</u>' + "<br>" + d.value + " inhabitants")
.style("left", (event.x/2+20) + "px")
.style("top", (event.y/2-30) + "px")
}
var mouseleave = function(event, d) {
Tooltip
.style("opacity", 0)
}
// Initialize the circle: all located at the center of the svg area
var node = svg.append("g")
.selectAll("circle")
.data(data)
.join("circle")
.attr("class", "node")
.attr("r", d => size(d.value))
.attr("cx", width / 2)
.attr("cy", height / 2)
.style("fill", d => color(d.region))
.style("fill-opacity", 0.8)
.attr("stroke", "black")
.style("stroke-width", 1)
.on("mouseover", mouseover) // What to do when hovered
.on("mousemove", mousemove)
.on("mouseleave", mouseleave)
.call(d3.drag() // call specific function when circle is dragged
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
// Features of the forces applied to the nodes:
const simulation = d3.forceSimulation()
.force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
.force("charge", d3.forceManyBody().strength(.1)) // Nodes are attracted one each other of value is > 0
.force("collide", d3.forceCollide().strength(.2).radius(function(d){ return (size(d.value)+3) }).iterations(1)) // Force that avoids circle overlapping
// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
.nodes(data)
.on("tick", function(d){
node
.attr("cx", d => d.x)
.attr("cy", d => d.y)
});
// What happens when a circle is dragged?
function dragstarted(event, d) {
if (!event.active) simulation.alphaTarget(.03).restart();
d.fx = d.x;
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(.03);
d.fx = null;
d.fy = null;
}
})
</script>
Wondering what chart type you should use? Check my
Data To Viz project! It is a
comprehensive classification of chart types organized by data
input format. Get a high-resolution version of the decision tree in your
inbox now!