This is a rebuild of Lazaro Gamio's What to know about all of the men facing sexual misconduct allegations published in Axios. It's what seaborn calls a "swarmplot" – a beeswarm faceted by a property of the data.
Just as I was finishing this, I realized that the original chart is made of absolutely positioned HTML divs. My version is SVG. I also didn't go all out on the tooltips, but the basic idea is there.
I downloaded the data from https://graphics.axios.com/2017-12-12-sexual-misconduct-cases/data/data.json on 4 February, 2018.
xxxxxxxxxx
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<style>
body {
font-family: "Helvetica Neue", sans-serif;
margin: 0;
}
#wrapper {
max-width: 900px;
margin: 0 auto;
}
.circle {
pointer-events: none;
}
.circle-bg {
stroke: steelblue;
fill: steelblue;
fill-opacity: .3;
pointer-events: none;
}
.circle-hover {
opacity: 0;
}
.axis .domain {
display: none;
}
.axis text {
font-size: 1.2em;
}
.axis.y.right .tick text {
fill: steelblue;
}
.axis.y .tick line {
stroke: #eee;
stroke-width: 10px;
}
.axis.x .tick line {
stroke: #ccc;
}
.tip {
position: absolute;
font-size: .8em;
text-align: center;
text-shadow: -1px -1px 1px #ffffff, -1px 0px 1px #ffffff, -1px 1px 1px #ffffff, 0px -1px 1px #ffffff, 0px 1px 1px #ffffff, 1px -1px 1px #ffffff, 1px 0px 1px #ffffff, 1px 1px 1px #ffffff;
}
</style>
</head>
<body>
<div id="wrapper"></div>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-moveto@0.0.3/build/d3-moveto.min.js"></script>
<script src="https://unpkg.com/jeezy@1.12.13/lib/jeezy.min.js"></script>
<script>
var radius = 12;
var margin = {top: radius * 2.5 + 10, left: 90, bottom: radius, right: 30},
width = +jz.str.keepNumber(d3.select("#wrapper").style("width")) - margin.left - margin.right,
height = 600 - margin.top - margin.bottom,
svg = d3.select("#wrapper").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 + ")");
var x = d3.scaleTime()
.rangeRound([0, width]);
var y = d3.scaleBand()
.rangeRound([0, height]);
var curr_month = "";
var x_axis = d3.axisBottom(x)
.tickSizeOuter(0)
.ticks(d3.timeDay.every(1))
.tickFormat(function(d){
var s = d.toString().split(" ");
var m = s[1];
if (m !== curr_month){
curr_month = m;
return m + ".";
} else {
return null;
}
});
var y_axis_left = d3.axisLeft(y)
.tickSize(0)
var y_axis_right = d3.axisRight(y)
.tickSizeOuter(0)
.tickSizeInner(-width);
var v = d3.voronoi()
.extent([[0, 0], [width, height]])
.x(function(d) { return d.x; })
.y(function(d) { return d.y; });
var parseDate = function(x){
var s = x.split("-");
var d = [s[2], s[0], s[1]].join("-");
return new Date(d);
}
// defs for images
var defs = d3.select("svg").append("defs");
// append the tip
var tip = d3.select("#wrapper").append("div")
.attr("class", "tip");
d3.json("data.json", function(error, data){
if (error) throw error;
data = data.include;
data.forEach(function(d){
d.accuse_date = parseDate(d.accuse_date);
d.slug = jz.str.toSlugCase(d.name);
return d;
});
x.domain([new Date(2017, 9, 1), data[data.length - 1].accuse_date]);
var industries = jz.arr.sortBy(jz.arr.pivot(data, "industry"), "count", "desc");
y.domain(industries.map(function(d){ return d.value; }));
x_axis.tickSizeInner(-height + y.bandwidth() / 2 - 3)
svg.append("g")
.attr("class", "axis y left")
.call(y_axis_left)
.selectAll(".tick text")
.attr("dx", -radius);
svg.append("g")
.attr("class", "axis y right")
.attr("transform", "translate(" + width + ", 0)")
.call(y_axis_right.tickFormat(function(d){ return industries.filter(function(c){ return c.value == d })[0].count; }))
.selectAll(".tick text")
.attr("dx", radius);
svg.append("g")
.attr("class", "axis x")
.attr("transform", "translate(0, " + height + ")")
.call(x_axis)
.selectAll(".tick line")
.style("display", function(d){
var s = d.toString().split(" ");
var m = s[1];
if (m !== curr_month){
curr_month = m;
return "block";
} else {
return "none";
}
});
// images
var img = defs.selectAll("pattern")
.data(data, function(d){ return d.slug; })
.enter().append("pattern")
.attr("id", function(d){ return "img-" + d.slug; })
.attr("x", "0%")
.attr("y", "0%")
.attr("height", "100%")
.attr("width", "100%")
.attr("viewBox", "0 0 " + radius + " " + radius)
.append("image")
.attr("x", "0%")
.attr("y", "0%")
.attr("width", radius)
.attr("height", radius)
.attr("xlink:href", function(d){ return "https://graphics.axios.com/2017-12-12-sexual-misconduct-cases/img/" + d.image_url; })
forceSim();
draw();
window.addEventListener("resize", function(){
// all of these things need to be updated on resize
width = +jz.str.keepNumber(d3.select("#wrapper").style("width")) - margin.left - margin.right;
d3.select(".axis.y.right").attr("transform", "translate(" + width + ", 0)").call(y_axis_right.tickSizeInner(-width));
x.rangeRound([0, width]);
forceSim();
d3.select(".x.axis")
.call(x_axis);
draw();
});
function draw(){
// circle
var circle = svg.selectAll(".circle")
.data(v.polygons(data), function(d){ return d.data.slug; });
circle.enter().append("circle")
.attr("class", function(d) { return "circle circle-" + d.data.slug; })
.attr("r", radius)
.style("fill", function(d){ return "url(#img-" + d.data.slug + ")"; })
.merge(circle)
.attr("cx", function(d) { return d ? d.data.x : null; })
.attr("cy", function(d) { return d ? d.data.y : null; });
// background
var bg_circle = svg.selectAll(".circle-bg")
.data(v.polygons(data), function(d){ return d.data.slug; });
bg_circle.enter().append("circle")
.attr("class", function(d) { return "circle-bg circle-bg-" + d.data.slug; })
.attr("r", radius)
.merge(bg_circle)
.attr("cx", function(d) { return d ? d.data.x : null; })
.attr("cy", function(d) { return d ? d.data.y : null; });
// hover
var hover_circle = svg.selectAll(".circle-hover")
.data(v.polygons(data), function(d){ return d.data.slug; });
hover_circle.enter().append("circle")
.attr("class", function(d) { return "circle-hover circle-hover-" + d.data.slug; })
.attr("r", radius)
.merge(hover_circle)
.attr("cx", function(d) { return d ? d.data.x : null; })
.attr("cy", function(d) { return d ? d.data.y : null; });
svg.selectAll(".circle-hover").on("mouseover", function(d){
tip.html(d.data.name);
d3.select(".circle-" + d.data.slug).attr("r", radius * 2.5).moveToFront();
d3.select(".circle-bg-" + d.data.slug).style("fill-opacity", 0).attr("r", radius * 2.5).style("stroke-width", 3).moveToFront();
var tip_width = +jz.str.keepNumber(tip.style("width"));
var tip_height = +jz.str.keepNumber(tip.style("height"));
var circle_node = d3.select(this).node().getBoundingClientRect();
var circle_left = circle_node.left;
var circle_top = circle_node.top;
var tip_left = circle_left - tip_width / 2 + radius;
var tip_top = circle_top - radius * 1.5 - tip_height;
tip
.style("left", tip_left + "px")
.style("top", tip_top + "px");
}).on("mouseout", function(d){
d3.select(".circle-" + d.data.slug).attr("r", radius);
d3.select(".circle-bg-" + d.data.slug).style("fill-opacity", .3).attr("r", radius).style("stroke-width", 1);
tip
.style("left", "-1000px")
.style("top", "-1000px");
});
}
function forceSim(){
var simulation = d3.forceSimulation(data)
.force("y", d3.forceY(function(d){ return y(d.industry) + y.bandwidth() / 2; }).strength(1))
.force("x", d3.forceX(function(d){ return x(d.accuse_date); }).strength(1))
.force("collide", d3.forceCollide(radius + 1))
.stop();
for (var i = 0; i < 200; ++i) simulation.tick();
}
});
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js
https://unpkg.com/d3-moveto@0.0.3/build/d3-moveto.min.js
https://unpkg.com/jeezy@1.12.13/lib/jeezy.min.js