Each bubble is an occupation. Hover bubbles to see info on that occupation
The size of the bubble shows how many jobs in this occupation there are in the US in 2014. Bubbles on the left are lower paying occupations and bubbles on the right are higher paying jobs. Bubbles are bunched vertically into larger occupation categories (management occupations, production occupations, etc.).
Uses the d3.forceChart() plugin to draw the bubble layout. Design inspired by this graphic from the New York Times. Data source: Bureau of Labor Statistics.
Notes: Because this uses a force layout for bubble positioning, the x-axis only approximates where an occupation’s bubble should be based on its wage. Wages from this dataset are top-coded at $187,000. Wages for jobs at this top-coded level may, in reality, have higher median annual wages.
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Bubbly Jobs Chart</title>
<style>
body {
font: 14px monospace;
}
.median-wage {
pointer-events: none;
}
.median-wage line {
stroke: black;
stroke-width: 2px;
stroke-dasharray: 2, 5;
shape-rendering: crispEdges;
}
.median-wage text {
font-size: 12px;
text-shadow:
-1px -1px 0px #fff,
1px -1px 0px #fff,
-1px 1px 0px #fff,
1px 1px 0px #fff;
}
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
.axis path {
display: none;
}
.axis .label {
font-size: 12px;
}
.tooltip {
width: 200px;
background-color: rgba(255, 255, 255, 0.9);
border: 1px solid rgb(203, 203, 203);
padding: 0px 20px;
}
.tooltip h4 {
margin-top: 10px;
}
.tooltip p {
margin: 10px 0px;
}
.tooltip.hidden {
display: none;
}
</style>
</head>
<body>
<script src="//d3js.org/d3.v3.min.js" charset="utf-8"></script>
<script src="force-chart.js"></script>
<script>
var margin = { top: 20, left: 200, bottom: 30, right: 70 },
width = 960 - margin.right - margin.left,
height = 2500 - margin.top - margin.bottom;
var majorGroupNames = {
"11": "Management",
"13": "Business and financial operations",
"15": "Computer and mathematical",
"17": "Architecture and engineering",
"19": "Life, physical, and social science",
"21": "Community and social service",
"23": "Legal",
"25": "Education, training, and library",
"27": "Art, design, entertainment, sport and media",
"29": "Healthcare practitioners and technical",
"31": "Healthcare support",
"33": "Protective service",
"35": "Food preparation and serving related",
"37": "Building and ground cleaning and maintenance",
"39": "Personal care and service",
"41": "Sales and related",
"43": "Office and administration",
"45": "Farming, fishing, and forestry",
"47": "Construction and extraction",
"49": "Installation, maintenance, and repair",
"51": "Production",
"53": "Transportation and material moving"
};
var majorGroupWages = [
{ majorGroup: "11", wage: 97230 },
{ majorGroup: "13", wage: 64790 },
{ majorGroup: "15", wage: 79420 },
{ majorGroup: "17", wage: 75780 },
{ majorGroup: "19", wage: 61450 },
{ majorGroup: "21", wage: 41290 },
{ majorGroup: "23", wage: 76860 },
{ majorGroup: "25", wage: 46660 },
{ majorGroup: "27", wage: 45180 },
{ majorGroup: "29", wage: 61710 },
{ majorGroup: "31", wage: 26440 },
{ majorGroup: "33", wage: 37180 },
{ majorGroup: "35", wage: 19130 },
{ majorGroup: "37", wage: 23270 },
{ majorGroup: "39", wage: 21260 },
{ majorGroup: "41", wage: 25360 },
{ majorGroup: "43", wage: 32520 },
{ majorGroup: "45", wage: 20250 },
{ majorGroup: "47", wage: 41380 },
{ majorGroup: "49", wage: 42110 },
{ majorGroup: "51", wage: 31720 },
{ majorGroup: "53", wage: 29530 }
];
var scale = {
x: d3.scale.linear().domain([0, 200000]).range([0, width]),
y: d3.scale.ordinal().rangeBands([height, 0], .25),
area: d3.scale.linear().range([25, 750]),
color: d3.scale.category20()
};
var dollarFormat = d3.format("$,");
var thousandFormat = d3.format(",");
var axis = {
x: d3.svg.axis().scale(scale.x).orient("bottom")
.tickFormat(dollarFormat),
y: d3.svg.axis().scale(scale.y).orient("left")
.tickFormat(function(d) { return majorGroupNames[d]; })
};
var bubbleChart = d3.forceChart()
.size([width, height])
.x(function(d) { return scale.x(d.wage); })
.y(function(d) { return scale.y(d.majorGroup) + scale.y.rangeBand()/2; })
.r(function(d) { return Math.sqrt(scale.area(d.employment)/Math.PI); })
.xGravity(10)
.yGravity(1/2)
.rGravity(2)
.rStart(2);
var body = d3.select("body");
var svg = body.append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("g").attr("class", "x axis")
.attr("transform", "translate(0," + height + ")");
svg.append("g").attr("class", "y axis");
var tooltip = body.append("div")
.attr("class", "tooltip")
.style("position", "absolute")
.classed("hidden", true);
tooltip.append("h4").attr("class", "title")
tooltip.append("p").attr("class", "employment")
tooltip.append("p").attr("class", "wage");
d3.json("occupation.json", ready);
function ready(error, nodes) {
if (error) throw error;
// Update scales
scale.y.domain(
majorGroupWages
.sort(function(a, b) { return a.wage - b.wage; })
.map(function(d) { return d.majorGroup; })
);
scale.area.domain([0, d3.max(nodes, function(d) { return d.employment; })]);
// Draw axes
svg.select(".x.axis").call(axis.x)
.append("text")
.attr("class", "label")
.attr("x", width)
.attr("y", -6)
.style("text-anchor", "end")
.text("Median Annual Wage");
svg.select(".y.axis").call(axis.y)
.selectAll(".tick text")
.call(wrap, 180)
.selectAll("tspan")
.attr("x", -9);
// Draw bubbles
svg.append("g").call(bubbleChart, nodes)
.attr("class", "bubbles")
.selectAll(".node").append("circle")
.attr("r", function(d) { return d.r0; })
.style("fill", function(d) { return scale.color(d.majorGroup); })
.style("stroke", function(d) {
return d3.hsl(scale.color(d.majorGroup)).darker(2);
})
.on("mouseenter", mouseenter) // draw tooltip
.on("mouseleave", mouseleave); // remove tooltip
// Draw median wage annotation
svg.append("g").attr("class", "annotation")
.selectAll(".median-wage").data(majorGroupWages)
.enter().append("g")
.attr("transform", function(d) {
return "translate(" + scale.x(d.wage) + "," + scale.y(d.majorGroup) + ")";
})
.attr("class", "median-wage")
.each(function(d) {
// draw line
d3.select(this).append("line")
.attr("y2", scale.y.rangeBand());
// draw text
d3.select(this).append("text")
.attr("dx", 5)
.attr("dy", 8)
.text(dollarFormat(d.wage));
});
}
function mouseenter(d) {
// Draw tooltip when bubble is entered
x = d.x + 300,
y = d.y - 50;
// shift left if mouse is too far right
x = x > (960 - 300) ? x - 400 : x;
tooltip
.classed("hidden", false)
.style("left", x + "px")
.style("top", y + "px");
tooltip.select(".title").text(d.title);
tooltip.select(".employment")
.text(thousandFormat(d.employment*1000) + " jobs");
tooltip.select(".wage")
.text("Wage: " + dollarFormat(d.wage));
}
function mouseleave() {
// Remove tooltip when bubble is exited
tooltip.classed("hidden", true);
}
function wrap(text, width) {
text.each(function() {
var text = d3.select(this),
words = text.text().split(/\s+/).reverse(),
word,
line = [],
lineNumber = 0,
lineHeight = 1.1, // ems
y = text.attr("y"),
dy = parseFloat(text.attr("dy")),
tspan = text.text(null).append("tspan").attr("x", 0).attr("y", y).attr("dy", dy + "em");
while (word = words.pop()) {
line.push(word);
tspan.text(line.join(" "));
if (tspan.node().getComputedTextLength() > width) {
line.pop();
tspan.text(line.join(" "));
line = [word];
tspan = text.append("tspan").attr("x", 0).attr("y", y).attr("dy", ++lineNumber * lineHeight + dy + "em").text(word);
}
}
});
}
</script>
</body>
</html>
d3.forceChart = function() {
var width = 400,
height = 300,
padding = 3,
x = function(d) { return d[0]; },
y = function(d) { return d[1]; },
r = function(d) { return d[2]; },
xStart = function(d) { return x(d) + 50*Math.random() - 25},
yStart = function(d) { return y(d) + 50*Math.random() - 25},
rStart = function(d) { return r(d); },
draggable = true,
xGravity = function(d) { return 1; },
yGravity = function(d) { return 1; },
rGravity = function(d) { return 1; },
shape = "circle",
tickUpdate = function() {};
var force = d3.layout.force()
.charge(0)
.gravity(0);
function chart(selection, nodes) {
if (shape === "circle") { collide = collideCircle; }
else if (shape === "square") { collide = collideSquare; }
else { console.error("forceChart.shape must be 'circle' or 'square'"); }
nodes = nodes
.map(function(d) {
d.x = xStart(d);
d.y = yStart(d);
d.r = rStart(d);
d.x0 = x(d);
d.y0 = y(d);
d.r0 = r(d);
return d;
});
var gNodes = selection.selectAll(".node").data(nodes)
.enter().append("g")
.attr("class", "node")
.call(draggable ? force.drag : null);
force
.size([width, height])
.nodes(nodes)
.on("tick", tick)
.start();
function tick(e) {
gNodes
.each(gravity(e.alpha * .1))
.each(collide(.5))
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
.call(tickUpdate);
}
function gravity(k) {
return function(d) {
var dx = d.x0 - d.x,
dy = d.y0 - d.y,
dr = d.r0 - d.r;
d.x += dx * k * xGravity(d);
d.y += dy * k * yGravity(d);
d.r += dr * k * rGravity(d);
};
}
function collideCircle(k) {
var q = d3.geom.quadtree(nodes);
return function(node) {
var nr = node.r + padding,
nx1 = node.x - nr,
nx2 = node.x + nr,
ny1 = node.y - nr,
ny2 = node.y + nr;
q.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
l = x * x + y * y,
r = nr + quad.point.r;
if (l < r * r) {
l = ((l = Math.sqrt(l)) - r) / l * k;
node.x -= x *= l;
node.y -= y *= l;
quad.point.x += x;
quad.point.y += y;
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
function collideSquare(k) {
var q = d3.geom.quadtree(nodes);
return function(node) {
var nr = node.r + padding,
nx1 = node.x - nr,
nx2 = node.x + nr,
ny1 = node.y - nr,
ny2 = node.y + nr;
q.visit(function(quad, x1, y1, x2, y2) {
if (quad.point && (quad.point !== node)) {
var x = node.x - quad.point.x,
y = node.y - quad.point.y,
lx = Math.abs(x),
ly = Math.abs(y),
r = nr + quad.point.r;
if (lx < r && ly < r) {
if (lx > ly) {
lx = (lx - r) * (x < 0 ? -k : k);
node.x -= lx;
quad.point.x += lx;
} else {
ly = (ly - r) * (y < 0 ? -k : k);
node.y -= ly;
quad.point.y += ly;
}
}
}
return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
});
};
}
}
chart.size = function(_) {
if (!arguments.length) return [width, height];
width = _[0];
height = _[1];
return chart;
};
chart.x = function(_) {
if (!arguments.length) return x;
if (typeof _ === "number") {
x = function() { return _; };
}
else if (typeof _ === "function") {
x = _;
}
return chart;
};
chart.y = function(_) {
if (!arguments.length) return y;
if (typeof _ === "number") {
y = function() { return _; };
}
else if (typeof _ === "function") {
y = _;
}
return chart;
};
chart.r = function(_) {
if (!arguments.length) return r;
if (typeof _ === "number") {
r = function() { return _; };
}
else if (typeof _ === "function") {
r = _;
}
return chart;
};
chart.draggable = function(_) {
if (!arguments.length) return draggable;
draggable = _;
return chart;
};
chart.padding = function(_) {
if (!arguments.length) return padding;
padding = _;
return chart;
};
chart.xGravity = function(_) {
if (!arguments.length) return xGravity;
if (typeof _ === "number") {
xGravity = function() { return _; };
}
else if (typeof _ === "function") {
xGravity = _;
}
return chart;
};
chart.yGravity = function(_) {
if (!arguments.length) return yGravity;
if (typeof _ === "number") {
yGravity = function() { return _; };
}
else if (typeof _ === "function") {
yGravity = _;
}
return chart;
};
chart.rGravity = function(_) {
if (!arguments.length) return rGravity;
if (typeof _ === "number") {
rGravity = function() { return _; };
}
else if (typeof _ === "function") {
rGravity = _;
}
return chart;
};
chart.xStart = function(_) {
if (!arguments.length) return xStart;
if (typeof _ === "number") {
xStart = function() { return _; };
}
else if (typeof _ === "function") {
xStart = _;
}
return chart;
};
chart.yStart = function(_) {
if (!arguments.length) return yStart;
if (typeof _ === "number") {
yStart = function() { return _; };
}
else if (typeof _ === "function") {
yStart = _;
}
return chart;
};
chart.rStart = function(_) {
if (!arguments.length) return rStart;
if (typeof _ === "number") {
rStart = function() { return _; };
}
else if (typeof _ === "function") {
rStart = _;
}
return chart;
};
chart.shape = function(_) {
if (!arguments.length) return shape;
shape = _;
return chart;
};
chart.tickUpdate = function(_) {
if (!arguments.length) return tickUpdate;
tickUpdate = _;
return chart;
};
return chart;
};