Bubble chart using d3-force. Code adapted from Jim Vallandingham's tutorial, Creating Bubble Charts with d3v4.
forked from officeofjane's block: Bubble chart with d3-force
license: mit |
Bubble chart using d3-force. Code adapted from Jim Vallandingham's tutorial, Creating Bubble Charts with d3v4.
forked from officeofjane's block: Bubble chart with d3-force
<!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 |