Skip to content

Instantly share code, notes, and snippets.

@officeofjane
Last active February 22, 2023 17:45
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save officeofjane/a70f4b44013d06b9c0a973f163d8ab7a to your computer and use it in GitHub Desktop.
Save officeofjane/a70f4b44013d06b9c0a973f163d8ab7a to your computer and use it in GitHub Desktop.
Bubble chart with d3-force
license: mit
<!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>
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