Last active
July 5, 2022 16:10
-
-
Save Rendiere/cd54cde6b58db4353861f03a2bc0be8e to your computer and use it in GitHub Desktop.
Bubble chart with interactive bubbles on click
This file contains 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
{ | |
"name": "data", | |
"children": [ | |
{ | |
"name": "indoor", | |
"value": 33, | |
"colour": "#F00", | |
"children": [ | |
{ | |
"name": "question_1_1", | |
"value": 1 | |
}, | |
{ | |
"name": "question_1_2", | |
"value": 1 | |
}, | |
{ | |
"name": "question_1_3", | |
"value": 2 | |
}, | |
{ | |
"name": "question_1_4", | |
"value": 3 | |
}, | |
{ | |
"name": "question_1_5", | |
"value": 5 | |
}, | |
{ | |
"name": "question_1_6", | |
"value": 8 | |
}, | |
{ | |
"name": "question_1_7", | |
"value": 13 | |
} | |
] | |
}, | |
{ | |
"name": "outdoor", | |
"value": 92, | |
"colour": "0F0", | |
"children": [ | |
{ | |
"name": "question_2_1", | |
"value": 21 | |
}, | |
{ | |
"name": "question_2_2", | |
"value": 8 | |
}, | |
{ | |
"name": "question_2_3", | |
"value": 5 | |
}, | |
{ | |
"name": "question_2_4", | |
"value": 13 | |
}, | |
{ | |
"name": "question_2_5", | |
"value": 3 | |
}, | |
{ | |
"name": "question_2_6", | |
"value": 34 | |
}, | |
{ | |
"name": "question_2_7", | |
"value": 8 | |
} | |
] | |
}, | |
{ | |
"name": "lifestyle", | |
"value": 99, | |
"colour": "#00F", | |
"children": [ | |
{ | |
"name": "question_3_1", | |
"value": 2 | |
}, | |
{ | |
"name": "question_3_2", | |
"value": 34 | |
}, | |
{ | |
"name": "question_3_3", | |
"value": 55 | |
}, | |
{ | |
"name": "question_3_4", | |
"value": 8 | |
} | |
] | |
} | |
] | |
} |
This file contains 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> | |
<html lang="en"> | |
<head> | |
<meta charset="UTF-8"> | |
<title>Title</title> | |
<script src="https://d3js.org/d3.v4.min.js"></script> | |
<link rel="stylesheet" href="styles.css"></link> | |
</head> | |
<body> | |
<div id="chart"></div> | |
<!--<script type="application/javascript" src="bubble.js"></script>--> | |
<script type="application/javascript"> | |
var colours = getColours(); | |
var width = 500, height = 500; | |
var svg = d3.select('#chart') | |
.append('svg') | |
.attr('height', height) | |
.attr('width', width) | |
.append('g'); | |
var radiusScale = d3.scaleSqrt().domain([0, 100]).range([10, 50]); | |
// Collection of forces | |
var xForce = d3.forceX(width / 2).strength(0.05); | |
var yForce = d3.forceY(height / 2).strength(0.05); | |
var manyBodyForce = d3.forceManyBody().strength(function (d) { | |
return -0.07 * Math.pow(radiusScale(d.data.value), 2); | |
}); | |
// Define the force simulation and attach these forces | |
var simulation = d3.forceSimulation() | |
.force('x', xForce) | |
.force('y', yForce) | |
.force('charge', manyBodyForce); | |
d3.queue() | |
.defer(d3.json, 'data.json') | |
.await(ready); | |
function ready(err, root) { | |
if (err) throw(err); | |
root = d3.hierarchy(root); | |
/* | |
root.leaves gives us all the final children (hence leaves) of each child in root.children | |
So essentially we are making a flat hierarchial structure | |
Each node represents a question, with its category accesible through its node.parent | |
*/ | |
var nodes = root.leaves(); | |
// Initialize the force simulation with the nodes as objects to act upon. | |
// Let the simulation call the tick function for every clock cycle | |
simulation.nodes(nodes).on('tick', tick); | |
// These nodes will be a grouper for the circles and text | |
// Then we can apply transformation functions to the nodes and | |
// it will be applied to both. | |
var svgNodes = svg.selectAll('.node') | |
.data(nodes) | |
.enter() | |
.append('g') | |
.attr('class', 'node') | |
.attr('id', function (d) { | |
return d.data.name | |
}) | |
.on('click', click); | |
svgNodes.append('circle') | |
.attr('class', function (d) { | |
return d.parent.data.name | |
}) | |
.attr('r', function (d) { | |
return radiusScale(d.data.value); | |
}) | |
.style('fill', function (d) { | |
return colours[d.parent.data.name.toUpperCase()] | |
}); | |
svgNodes.append('text') | |
.attr('font-size', '12px') | |
.style('text-anchor', 'middle') | |
.text(function (d) { | |
return d.data.value; | |
}); | |
/** | |
* Function that gets called on each time tick. | |
*/ | |
function tick() { | |
svgNodes.attr("transform", function (d) { | |
return "translate(" + d.x + "," + d.y + ")"; | |
}); | |
} | |
/** | |
* Checks if any other bubbles are expanded and collapses them if they are | |
* @param d | |
*/ | |
function clickCheck(d) { | |
// First check if any other bubble were expanded | |
var expandedNode = nodes.find(function (node) { | |
return node.data['activated'] === true; | |
}); | |
if (expandedNode) { | |
// Decrease the size of the expanded circle | |
d3.select("#" + expandedNode.data.name) | |
.select('circle') | |
.transition() | |
.duration(300) | |
.attr('r', function (d) { | |
return radiusScale(d.data['old_value']); | |
}); | |
// Update its value in the node | |
expandedNode.data.value = expandedNode.data['old_value']; | |
expandedNode.data['activated'] = false; | |
} | |
} | |
function click(d) { | |
clickCheck(d); | |
if (d.data['activated']) { | |
d3.select(this) | |
.select('circle') | |
.transition() | |
.duration(300) | |
.attr('r', function (d) { | |
return radiusScale(d.data['old_value']); | |
}); | |
} else { | |
// The maximum value that any circle will have (not radius) | |
// this translates to a radius of 80 | |
// TODO: Refactor this to be varants elsewhere | |
var v = 300; | |
// Save the old value for setting back to original size | |
d.data['activated'] = true; | |
d.data['old_value'] = d.data.value; | |
// Set this node to have max size | |
d.data.value = v; | |
// Enlarge this circle | |
d3.select(this) | |
.select('circle') | |
.transition() | |
.duration(500) | |
.attr('r', function (d) { | |
return radiusScale(d.data.value); | |
}); | |
} | |
// Update the nodes used by the simulation to calculate forces | |
simulation.nodes(nodes); | |
// TODO: Figure out why after expansion some bubbles are overlapping. | |
// Restart the simulation | |
simulation.alphaTarget(1).restart(); | |
} | |
} | |
function getColours() { | |
return { | |
"INDOOR": '#ff9587', | |
"OUTDOOR": '#0dce72', | |
"LIFESTYLE": 'lightblue' | |
}; | |
} | |
</script> | |
</body> | |
</html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment