Bubble chart using d3-force. Code adapted from Jim Vallandingham's tutorial, Creating Bubble Charts with d3v4.
Last active
February 22, 2023 17:45
-
-
Save officeofjane/a70f4b44013d06b9c0a973f163d8ab7a to your computer and use it in GitHub Desktop.
Bubble chart with d3-force
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: mit |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<script src='https://d3js.org/d3.v5.min.js'></script> | |
<style> | |
body { | |
font-family: "avenir next", Arial, sans-serif; | |
font-size: 12px; | |
margin: 0; | |
} | |
</style> | |
</head> | |
<body> | |
<div id = 'vis'></div> | |
<script> | |
// bubbleChart creation function; instantiate new bubble chart given a DOM element to display it in and a dataset to visualise | |
function bubbleChart() { | |
const width = 940; | |
const height = 500; | |
// location to centre the bubbles | |
const centre = { x: width/2, y: height/2 }; | |
// strength to apply to the position forces | |
const forceStrength = 0.03; | |
// these will be set in createNodes and chart functions | |
let svg = null; | |
let bubbles = null; | |
let labels = null; | |
let nodes = []; | |
// charge is dependent on size of the bubble, so bigger towards the middle | |
function charge(d) { | |
return Math.pow(d.radius, 2.0) * 0.01 | |
} | |
// create a force simulation and add forces to it | |
const simulation = d3.forceSimulation() | |
.force('charge', d3.forceManyBody().strength(charge)) | |
// .force('center', d3.forceCenter(centre.x, centre.y)) | |
.force('x', d3.forceX().strength(forceStrength).x(centre.x)) | |
.force('y', d3.forceY().strength(forceStrength).y(centre.y)) | |
.force('collision', d3.forceCollide().radius(d => d.radius + 1)); | |
// force simulation starts up automatically, which we don't want as there aren't any nodes yet | |
simulation.stop(); | |
// set up colour scale | |
const fillColour = d3.scaleOrdinal() | |
.domain(["1", "2", "3", "5", "99"]) | |
.range(["#0074D9", "#7FDBFF", "#39CCCC", "#3D9970", "#AAAAAA"]); | |
// data manipulation function takes raw data from csv and converts it into an array of node objects | |
// each node will store data and visualisation values to draw a bubble | |
// rawData is expected to be an array of data objects, read in d3.csv | |
// function returns the new node array, with a node for each element in the rawData input | |
function createNodes(rawData) { | |
// use max size in the data as the max in the scale's domain | |
// note we have to ensure that size is a number | |
const maxSize = d3.max(rawData, d => +d.size); | |
// size bubbles based on area | |
const radiusScale = d3.scaleSqrt() | |
.domain([0, maxSize]) | |
.range([0, 80]) | |
// use map() to convert raw data into node data | |
const myNodes = rawData.map(d => ({ | |
...d, | |
radius: radiusScale(+d.size), | |
size: +d.size, | |
x: Math.random() * 900, | |
y: Math.random() * 800 | |
})) | |
return myNodes; | |
} | |
// main entry point to bubble chart, returned by parent closure | |
// prepares rawData for visualisation and adds an svg element to the provided selector and starts the visualisation process | |
let chart = function chart(selector, rawData) { | |
// convert raw data into nodes data | |
nodes = createNodes(rawData); | |
// create svg element inside provided selector | |
svg = d3.select(selector) | |
.append('svg') | |
.attr('width', width) | |
.attr('height', height) | |
// bind nodes data to circle elements | |
const elements = svg.selectAll('.bubble') | |
.data(nodes, d => d.id) | |
.enter() | |
.append('g') | |
bubbles = elements | |
.append('circle') | |
.classed('bubble', true) | |
.attr('r', d => d.radius) | |
.attr('fill', d => fillColour(d.groupid)) | |
// labels | |
labels = elements | |
.append('text') | |
.attr('dy', '.3em') | |
.style('text-anchor', 'middle') | |
.style('font-size', 10) | |
.text(d => d.id) | |
// set simulation's nodes to our newly created nodes array | |
// simulation starts running automatically once nodes are set | |
simulation.nodes(nodes) | |
.on('tick', ticked) | |
.restart(); | |
} | |
// callback function called after every tick of the force simulation | |
// here we do the actual repositioning of the circles based on current x and y value of their bound node data | |
// x and y values are modified by the force simulation | |
function ticked() { | |
bubbles | |
.attr('cx', d => d.x) | |
.attr('cy', d => d.y) | |
labels | |
.attr('x', d => d.x) | |
.attr('y', d => d.y) | |
} | |
// return chart function from closure | |
return chart; | |
} | |
// new bubble chart instance | |
let myBubbleChart = bubbleChart(); | |
// function called once promise is resolved and data is loaded from csv | |
// calls bubble chart function to display inside #vis div | |
function display(data) { | |
myBubbleChart('#vis', data); | |
} | |
// load data | |
d3.csv('nodes-data.csv').then(display); | |
</script> | |
</body> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
id | groupid | size | |
---|---|---|---|
1 | 1 | 9080 | |
2 | 1 | 4610 | |
3 | 2 | 3810 | |
4 | 1 | 2990 | |
5 | 1 | 2820 | |
6 | 3 | 2430 | |
7 | 99 | 2400 | |
8 | 3 | 2090 | |
9 | 3 | 1580 | |
10 | 1 | 1290 | |
11 | 1 | 1230 | |
12 | 1 | 1210 | |
13 | 3 | 829 | |
14 | 3 | 768 | |
15 | 1 | 745 | |
16 | 3 | 651 | |
17 | 3 | 589 | |
18 | 1 | 569 | |
19 | 2 | 502 | |
20 | 3 | 441 | |
21 | 2 | 425 | |
22 | 5 | 388 | |
23 | 99 | 378 | |
24 | 1 | 373 | |
25 | 3 | 369 | |
26 | 2 | 364 | |
27 | 5 | 359 | |
28 | 1 | 349 | |
29 | 1 | 340 | |
30 | 3 | 338 | |
31 | 99 | 330 | |
32 | 1 | 306 | |
33 | 1 | 301 | |
34 | 99 | 283 | |
35 | 1 | 268 | |
36 | 3 | 268 | |
37 | 99 | 266 | |
38 | 99 | 264 | |
39 | 3 | 262 | |
40 | 3 | 256 | |
41 | 5 | 243 | |
42 | 1 | 237 | |
43 | 3 | 223 | |
44 | 1 | 222 | |
45 | 1 | 220 | |
46 | 99 | 220 | |
47 | 1 | 212 | |
48 | 99 | 201 | |
49 | 1 | 193 | |
50 | 3 | 190 | |
51 | 1 | 189 | |
52 | 3 | 188 | |
53 | 1 | 186 | |
54 | 1 | 179 | |
55 | 3 | 179 | |
56 | 99 | 174 | |
57 | 1 | 172 | |
58 | 3 | 165 | |
59 | 1 | 165 | |
60 | 3 | 164 | |
61 | 1 | 163 | |
62 | 1 | 157 | |
63 | 3 | 149 | |
64 | 2 | 147 | |
65 | 3 | 145 | |
66 | 1 | 142 | |
67 | 1 | 138 | |
68 | 3 | 128 | |
69 | 3 | 120 | |
70 | 2 | 97 |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment