Skip to content

Instantly share code, notes, and snippets.

@Rendiere
Last active July 5, 2022 16:10
Show Gist options
  • Save Rendiere/cd54cde6b58db4353861f03a2bc0be8e to your computer and use it in GitHub Desktop.
Save Rendiere/cd54cde6b58db4353861f03a2bc0be8e to your computer and use it in GitHub Desktop.
Bubble chart with interactive bubbles on click
{
"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
}
]
}
]
}
<!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