This clustered bubble force graph shows college majors by size, grouped by major category. This is an extension of Shan Carter's Clustered Force Layout 4.0. The data is was found in FiveThirtyEight's College Majors data set in their Data Repo.
forked from mcgovey's block: Clustered Interactive Force Layout v4
forked from Thanaporn-sk's block: Clustered Interactive Force Layout v4
xxxxxxxxxx
<meta charset="utf-8">
<style type="text/css">
div.tooltip {
position: absolute;
text-align: center;
width: 150px;
/*height: 28px; */
padding: 2px;
font: 12px sans-serif;
background: lightgrey;
border: 0px;
border-radius: 8px;
pointer-events: none;
}
</style>
<body>
<button onclick="addNew()">add new</button>
<script src="//d3js.org/d3.v4.min.js"></script>
<script>
let margin = {top: 100, right: 100, bottom: 100, left: 100};
let width = 960,
height = 500,
padding = 1.5, // separation between same-color circles
clusterPadding = 6, // separation between different-color circles
maxRadius = height*0.1;
let n = 200, // total number of nodes
m = 10, // number of distinct clusters
z = d3.scaleOrdinal(d3.schemeCategory20),
clusters = new Array(m);
let svg = d3.select('body')
.append('svg')
.attr('height', height)
.attr('width', width)
.append('g').attr('transform', 'translate(' + width / 2 + ',' + height / 2 + ')');
// Define the div for the tooltip
let div = d3.select("body").append("div")
.attr("class", "tooltip")
.style("opacity", 0);
let addNewData;
//load college major data
d3.csv("college-majors.csv", function(d){
let categories = d.map(item => {
return {
code: +item.Major_category_code,
cat: item.Major_category
}
}
)
.filter((value, index, self) => self.indexOf(value) === index)
let radiusScale = d3.scaleLinear()
.domain(d3.extent(d, function(d) { return +d.Total;} ))
.range([4, maxRadius]);
console.log(radiusScale(300000));
let nodes = d.map((d) => {
return mapDataItem(d)
});
function mapDataItem(d) {
// scale radius to fit on the screen
let scaledRadius = radiusScale(+d.Total),
forcedCluster = +d.Major_category_code;
// add cluster id and radius to array
d = {
cluster : forcedCluster,
r : scaledRadius,
major : d.Major,
major_cat : d.Major_category
};
// add to clusters array if it doesn't exist or the radius is larger than another radius in the cluster
if (!clusters[forcedCluster] || (scaledRadius > clusters[forcedCluster].r)) clusters[forcedCluster] = d;
return d;
}
// append the circles to svg then style
// add functions for interaction
let circles = svg.append('g')
.datum(nodes)
.selectAll('.circle')
.data(d => d)
.enter().append('circle')
.attr('r', (d) => d.r)
.attr('fill', (d) => z(d.cluster))
.attr('stroke', 'black')
.attr('stroke-width', 1)
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended))
// add tooltips to each circle
.on("mouseover", function(d) {
div.transition()
.duration(200)
.style("opacity", .9);
div .html( "The major " + d.major+ "<br/>In the category " + d.major_cat )
.style("left", (d3.event.pageX) + "px")
.style("top", (d3.event.pageY - 28) + "px");
})
.on("mouseout", function(d) {
div.transition()
.duration(500)
.style("opacity", 0);
});
var simulation = d3.forceSimulation()
// keep entire simulation balanced around screen center
.force('center', d3.forceCenter(width/2, height/2))
// cluster by section
.force('cluster', clustering()
.strength(0.2))
// apply collision with padding
.force('collide', d3.forceCollide(d => d.radius + 1.5)
.strength(0.7))
.on('tick', ticked)
.nodes(nodes);
// // create the clustering/collision force simulation
// let simulation = d3.forceSimulation(nodes)
// .velocityDecay(0.2)
// .force("x", d3.forceX().strength(.0005))
// .force("y", d3.forceY().strength(.0005))
// .force("collide", collide)
// .force("cluster", clustering)
// .on("tick", ticked);
addNewData = function() {
var catIndex = Math.floor(Math.random() * categories.length)
let randomData = {
id: `${Math.floor(Math.random()*100000000)}`,
Major: 'asdaksdaksjdhaksd',
Total: `${Math.floor(Math.random()*1000)}`,
Men: `${Math.floor(Math.random()*900)}`,
Women: `${Math.floor(Math.random()*100)}`,
Major_category_code: `${categories[catIndex].code}`,
Major_category: `${categories[catIndex].cat}`
}
nodes.push(mapDataItem(randomData))
simulation.nodes(nodes)
simulation.alpha(1).restart
}
function ticked() {
circles
.attr('cx', (d) => d.x)
.attr('cy', (d) => d.y);
}
// Drag functions used for interactivity
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
// Move d to be adjacent to the cluster node.
// from: https://bl.ocks.org/mbostock/7881887
function clustering () {
var nodes,
strength = 0.1;
function force (alpha) {
// scale + curve alpha value
alpha *= strength * alpha;
nodes.forEach(function(d) {
var cluster = clusters[d.cluster];
if (cluster === d) return;
let x = d.x - cluster.x,
y = d.y - cluster.y,
l = Math.sqrt(x * x + y * y),
r = d.radius + cluster.radius;
if (l != r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
cluster.x += x;
cluster.y += y;
}
});
}
force.initialize = function (_) {
nodes = _;
}
force.strength = _ => {
strength = _ == null ? strength : _;
return force;
};
return force;
}
function collide(alpha) {
var quadtree = d3.quadtree()
.x((d) => d.x)
.y((d) => d.y)
.addAll(nodes);
nodes.forEach(function(d) {
var r = d.r + maxRadius + Math.max(padding, clusterPadding),
nx1 = d.x - r,
nx2 = d.x + r,
ny1 = d.y - r,
ny2 = d.y + r;
quadtree.visit(function(quad, x1, y1, x2, y2) {
if (quad.data && (quad.data !== d)) {
var x = d.x - quad.data.x,
y = d.y - quad.data.y,
l = Math.sqrt(x * x + y * y),
r = d.r + quad.data.r + (d.cluster === quad.data.cluster ? padding : clusterPadding);
if (l < r) {
l = (l - r) / l * alpha;
d.x -= x *= l;
d.y -= y *= l;
quad.data.x += x;
quad.data.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
});
}
});
function addNew(){
if (typeof addNewData === 'function')
{
addNewData();
}
}
</script>
https://d3js.org/d3.v4.min.js