This is the code for the “linked beeswarm” chart I made for a story on the history of Hercule Poirot.
I added a checkbox to let you see the Voronoi diagram, which optimizes the size of the mouseover / tap area of each circle.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<style>
body {
margin: 0;
font-family: "Helvetica Neue", sans-serif;
}
.cell path {
fill: none;
pointer-events: all;
}
.cell.selected circle {
stroke: #000;
stroke-width: 2px;
}
#linked-beeswarm {
max-width: 600px;
width: 100%;
margin: auto;
}
.intro {
max-width: 600px;
width: 100%;
margin: auto;
font-size: .9em;
margin-bottom: 20px;
}
#linked-beeswarm .axis .domain {
display: none;
}
#linked-beeswarm .axis .tick line {
stroke: #ccc;
stroke-dasharray: 5, 5;
}
#linked-beeswarm .axis .tick text {
fill: #888;
}
#linked-beeswarm .time-label {
font-size: .8em;
font-weight: bold;
fill: #888;
}
#linked-beeswarm .top-label {
font-weight: bold;
text-anchor: middle;
}
.count-label {
text-anchor: middle;
font-size: .8em;
}
.tip-line {
stroke: #000;
stroke-width: 1.5px;
fill: none;
}
.tip {
position: absolute;
top: 0;
left: 0;
text-align: center;
pointer-events: none;
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;
}
.tip .kill {
font-size: .8em;
}
.tip .book-name {
font-weight: bold;
font-size: .9em;
background: rgba(255, 255, 255, .8);
margin-bottom: 10px;
}
.tip .type {
font-size: .8em;
}
.show {
position: absolute;
font-size: .8em;
}
/*THE POINT AT WHICH THE TABLE IS TOO WIDE*/
@media only screen and (max-width: 600px) {
html, body {
max-width: 100%;
overflow-x: hidden;
}
.intro {
padding: 0px 20px;
width: auto;
}
}
</style>
</head>
<body>
<div class="show">Show voronoi <input type="checkbox"></div>
<div class="intro">Each <b>circle</b> represents a murderer or a victim. Earlier stories are on <b>top</b>; later ones are on <b>bottom</b>. <b><span class="hover-tap">Hover or tap</span></b> on a circle for more information.</div>
<div id="linked-beeswarm"></div>
<svg height="0">
<marker id="markerArrow" markerWidth="13" markerHeight="13" refX="2" refY="6" orient="auto">
<path d="M2,2 L2,11 L10,6 L2,2" style="fill: #000000;" />
</marker>
</svg>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://unpkg.com/d3-marcon@2.0.2/build/d3-marcon.min.js"></script>
<script src="https://unpkg.com/jeezy@1.12.10/lib/jeezy.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.2.1/jquery.min.js"></script>
<script src="//www.hindustantimes.com/static/common/js/jquery.smartresize.js"></script>
<script>
var w = $(window).width();
$(document).ready(function(){
draw();
});
$(window).smartresize(function(){
// only on width change
if ($(window).width() != w){
draw();
w = $(window).width()
}
});
function draw(){
var first_draw = true;
$("#linked-beeswarm").empty();
$(".tip").remove();
// magic numbers
var ww = $(window).width();
var bp = 510;
// setup tip
var tip = d3.select("#linked-beeswarm").append("div")
.attr("class", "tip");
tip.append("div").attr("class", "book-name");
tip.append("div").attr("class", "type murderer");
tip.append("div").attr("class", "kill");
tip.append("div").attr("class", "type victim");
var colors = {red: "#df5a49", blue: "#2880b9"};
var color_names = {man: colors.blue, woman: colors.red};
var element = "#linked-beeswarm";
var margin = {left: 30, top: 60};
var setup = d3.marcon()
.element(element)
.width(+jz.str.keepNumber(d3.select(element).style("width")))
.height(ww < bp ? 400 : 600)
.left(margin.left)
.right(margin.left)
.top(margin.top)
.bottom(20);
setup.render();
var width = setup.innerWidth(), height = setup.innerHeight(), svg = setup.svg();
var x = d3.scaleBand()
.rangeRound([0, width]);
var y = d3.scaleLinear()
.rangeRound([0, height]);
var size = ww < bp ? 5 : 8;
d3.csv("data.csv", function(err, data){
// data types
data.forEach(function(d){
d.book_year = +d.book_year;
return d;
});
var genders = jz.arr.uniqueBy(data, "gender");
var types = jz.arr.uniqueBy(data, "type");
var books_data = jz.arr.uniqueBy(data, "book").map(function(book){
var lookup = data.filter(function(d){ return d.book == book; });
var this_data = [];
types.forEach(function(type){
genders.forEach(function(gender){
this_data.push({
type: type,
gender: gender,
count: lookup.filter(function(d){ return d.type == type && d.gender == gender}).length
});
});
});
function filter_facet(type, gender){
return this_data.filter(function(d){ return d.type == type && d.gender == gender; })[0].count;
}
return {
book: book,
murderer_man: filter_facet("murderer", "man"),
murderer_woman: filter_facet("murderer", "woman"),
victim_man: filter_facet("victim", "man"),
victim_woman: filter_facet("victim", "woman"),
}
});
// domains
x.domain(types);
y.domain(d3.extent(data, function(d){ return d.book_year; }));
// time label
svg.append("text")
.attr("class", "time-label")
.attr("x", ww < bp ? -margin.left + 4 : -margin.left)
.attr("y", 0)
.attr("dy", -10)
.text("Time ↓");
// top labels
var top_label = svg.selectAll(".top-label")
.data(types)
.enter().append("text")
.attr("class", "top-label")
.attr("x", function(d){ return x(d) + (x.bandwidth() / 2); })
.attr("y", -margin.top)
.attr("dy", 12)
.text(function(d){ return jz.str.toStartCase(d) + "s"; });
var types_data = types.map(function(d){
var match = data.filter(function(e){ return e.type == d; });
return {
type: d,
data: jz.arr.pivot(match, "gender")
}
});
var count_label = svg.selectAll(".count-label")
.data(types_data)
.enter().append("text")
.attr("class", "count-label")
.attr("x", function(d){ return x(d.type) + (x.bandwidth() / 2); })
.attr("y", -margin.top)
.attr("dy", 30)
.html(function(d){
return "<tspan style='fill: " + color_names.man + "'>" + d.data[0].count + " men</tspan> & <tspan style='fill: " + color_names.woman + "'>" + d.data[1].count + " women</tspan>";
});
var simulation = d3.forceSimulation(data)
.force("y", d3.forceY(function(d){ return y(d.book_year); }).strength(1))
.force("x", d3.forceX(function(d){ return x(d.type) + (x.bandwidth() / 2); }))
.force("collide", d3.forceCollide(size + 1))
.stop();
// 250 ticks
for (var i = 0; i < 250; ++i) simulation.tick();
// for loop for axes because you can't send different functions into .call()
types.forEach(function(type){
var axis = type == "murderer" ? d3.axisLeft(y) : d3.axisRight(y);
axis
.tickFormat(function(d){ return +d; })
.tickSizeOuter(0)
.tickSizeInner(type == "murderer" ? -width : 0);
svg.append("g")
.attr("class", "axis")
.attr("transform", "translate(" + (type == "murderer" ? 0 : width) + ", 0)")
.call(axis);
});
// JOIN
var cell = svg.append("g")
.attr("class", "cells")
.selectAll("g").data(d3.voronoi()
.extent([[0, 0], [width, height]])
.x(function(d) { return d.x; })
.y(function(d) { return d.y; })
.polygons(data))
.enter().append("g")
.attr("class", function(d){ return "cell " + d.data.type + " " + jz.str.toSlugCase(d.data.name) + " " +jz.str.toSlugCase(d.data.book); });
// voronoi
var voronoi = cell.append("path")
.attr("d", function(d) { return d == undefined ? null : "M" + d.join("L") + "Z"; });
// circle
cell.append("circle")
.attr("r", size)
.style("fill", function(d){ return color_names[d.data.gender]; })
.attr("cx", function(d) { return d == undefined ? null : d.data.x; })
.attr("cy", function(d) { return d == undefined ? null : d.data.y; });
svg.selectAll(".cell")
.on("mouseover", tipon);
function tipon(d){
d3.selectAll(".cell")
.classed("selected", false);
d3.selectAll(".cell." + jz.str.toSlugCase(d.data.book))
.classed("selected", true);
var book_lookup = books_data.filter(function(book_obj){
return book_obj.book == d.data.book;
})[0];
// content in the tip
d3.select(".tip .book-name").html(d.data.book + " (" + d.data.book_year + ")");
d3.select(".tip .type.murderer").html(makeHTML("murderer"));
d3.select(".tip .kill").html(d3.sum([book_lookup.murderer_man, book_lookup.murderer_woman]) == 1 ? "kills" : "kill")
d3.select(".tip .type.victim").html(makeHTML("victim"));
function makeHTML(type){
var man_html = makeGenderHTML(type, "man");
var woman_html = makeGenderHTML(type, "woman");
return book_lookup[type + "_man"] > 0 && book_lookup[type + "_woman"] > 0 ? man_html + " & " + woman_html :
book_lookup[type + "_man"] > 0 ? man_html :
woman_html;
}
function makeGenderHTML(type, gender){
return "<span style='color: " + color_names[gender] + "'>" + book_lookup[type + "_" + gender] + " " + (book_lookup[type + "_" + gender] == 1 ? gender : gender.replace("a", "e")) + "</span>";
}
var tip_pos = d3.select(".tip").node().getBoundingClientRect();
var window_padding = 40;
var y_pos = y(d.data.book_year);
var svg_offset = $("#linked-beeswarm svg").position();
var top = y_pos - (ww < bp ? tip_pos.height * .8 : tip_pos.height * 1.2) + svg_offset.top;
top = top < svg_offset.top ? svg_offset.top : top;
if (!first_draw){
top = top < $(window).scrollTop() + window_padding ? $(window).scrollTop() + window_padding : top;
} else {
first_draw = false;
}
d3.select(".tip")
.style("left", (ww / 2) - (tip_pos.width / 2) + "px")
.style("top", top + "px");
var lines_data = data.filter(function(r){ return r.book == d.data.book; });
lines_data.forEach(function(line){
var x1 = calcx1(line);
var x2 = calcx2(line);
var y1 = y(line.book_year);
var y2 = top - svg_offset.top;
var orient = line.book == "Murder on the Orient Express";
line.points = [
{
x: line.type == "murderer" ? x1 - size : x2,
y: line.type == "murderer" ? y1 - (orient ? 0 : size) : y2
}, {
x: line.type == "murderer" ? x2 : x1 + (y1 < 50 ? -size * 2 : 0),
y: line.type == "murderer" ? y2 : y1 + (y1 < 50 ? 0 : -size * 2)
}
];
if (ww < bp){
line.points[1].x += 5;
}
});
var line = svg.selectAll(".tip-line")
.data(lines_data, function(d){ return d.name; })
line.exit().remove();
var already_drew_murderer = false;
line.enter().append("path")
.attr("class", "tip-line")
.attr("d", function(d){
var dx = d.points[1].x - d.points[0].x,
dy = d.points[1].y - d.points[0].y,
dr = Math.sqrt(dx * dx + dy * dy);
return "M" + d.points[0].x + "," + d.points[0].y + "A" + dr + "," + dr + " 0 0,1 " + d.points[1].x + "," + d.points[1].y;
})
.attr("marker-end", function(d){
if (d.type == "murderer" && !already_drew_murderer){
already_drew_murderer = true;
return "url(#markerArrow)";
} else if (d.type !== "murderer") {
return "url(#markerArrow)";
} else {
return "";
}
});
function calcx1(d){
var relativePos = calcRelPos("#linked-beeswarm", ".cell." + jz.str.toSlugCase(d.name) + " circle");
return relativePos.left - margin.left + (d.type == "murderer" ? size * 2 : 0);
}
function calcx2(d){
return (d.type == "murderer" ? -50 : 50) + width / 2;
}
function calcRelPos(parent, child){
var parentPos = d3.select(parent).node().getBoundingClientRect(),
childrenPos = d3.select(child).node().getBoundingClientRect(),
relativePos = {};
relativePos.top = childrenPos.top - parentPos.top,
relativePos.right = childrenPos.right - parentPos.right,
relativePos.bottom = childrenPos.bottom - parentPos.bottom,
relativePos.left = childrenPos.left - parentPos.left;
return relativePos;
}
}
var starter = data.filter(function(d){ return d.book == "The Clocks"; })[0];
d3.timeout(function(){ tipon({data: starter})}, 2000);
});
}
$(".show input").change(function(){
$(".cell path").css("stroke", $(this).prop("checked") ? "#000" : "none");
});
</script>
</body>
</html>
book,gender,name,book_year,type
The Mysterious Affair at Styles,man,Alfred Inglethorp,1920,murderer
Murder on the Links,woman,Marthe Daubreuil,1923,murderer
The Murder of Roger Ackroyd,man,Dr James Sheppard,1926,murderer
The Big Four,man,Abe Rylan,1927,murderer
The Big Four,man,Claude Darrell,1927,murderer
The Big Four,man,Li Chang Yen,1927,murderer
The Big Four,woman,Madame Olivier,1927,murderer
The Mystery of the Blue Train,man,Major Knighton,1928,murderer
The Mystery of the Blue Train,woman,Ada Mason aka Kitty Kidd,1928,murderer
Peril at End House,woman,"Magdala ""Nick"" Buckley",1931,murderer
Lord Edgware Dies,woman,Jane Wilkinson,1933,murderer
Murder on the Orient Express,woman,Mary Debenham,1934,murderer
Murder on the Orient Express,woman,Mrs. Hubbard,1934,murderer
Murder on the Orient Express,man,Colonel Arbuthnot,1934,murderer
Murder on the Orient Express,woman,Princess Dragomiroff,1934,murderer
Murder on the Orient Express,man,Hector McQueen,1934,murderer
Murder on the Orient Express,woman,Countess Andrenyi,1934,murderer
Murder on the Orient Express,man,Count Andrenyi,1934,murderer
Murder on the Orient Express,man,Cyrus Hardman,1934,murderer
Murder on the Orient Express,man,Antonoi Foscanelli,1934,murderer
Murder on the Orient Express,woman,Greta Ohlsson,1934,murderer
Murder on the Orient Express,woman,Hildegarde Schmidt,1934,murderer
Murder on the Orient Express,man,Pierre Michelle,1934,murderer
Three Act Tragedy,man,Charles Cartwright,1934,murderer
Death in The Clouds,man,Norman Gale,1935,murderer
Murder in Mesopotamia,man,Dr Erich Leidner aka Frederick Bosner,1935,murderer
The ABC Murders,man,Franklin Clarke ,1936,murderer
Cards on the Table,man,Dr Geoffrey Roberts,1936,murderer
Dumb Witness,woman,Bella Tanios,1937,murderer
Death on the Nile,man,Simon Doyle,1937,murderer
Death on the Nile,woman,Jacqueline de Bellefort,1937,murderer
Appointment with Death,woman,Lady Westholme,1937,murderer
Hercule Poirot's Christmas,man,Superintendent Sugden,1938,murderer
Sad Cypress,woman,Jessie Hopkins,1940,murderer
Evil Under the Sun,man,Patrick Redfern,1940,murderer
Evil Under the Sun,woman,Christine Redfern,1940,murderer
"One, Two, Buckle my Shoe",man,Martin Alistair Blunt,1940,murderer
Five Little Pigs,woman,Elsa Greer,1941,murderer
The Hollow,woman,Gerda Christow,1946,murderer
Taken at the Flood,man,David Hunter,1948,murderer
Mrs McGinty's Dead,man,Robin Upward,1952,murderer
After the Funeral,woman,Miss Gilchrist,1953,murderer
Hickory Dickory Dock,man,Nigel Chapman,1955,murderer
Dead Man's Folly,man,James,1956,murderer
Cat Among the Pigeons,woman,Ann Shapland,1959,murderer
The Clocks,man,Josiah Bland,1963,murderer
The Clocks,woman,Valerie Bland,1963,murderer
Third Girl,woman,Frances Cary,1966,murderer
Hallowe'en Party,woman,Rowena Drake,1969,murderer
Elephants can Remember,man,Alistair Ravenscroft,1972,murderer
Curtain,man,Hercule Poirot,1975,murderer
The Mysterious Affair at Styles,woman,Mrs Inglethorpe,1920,victim
Murder on the Links,man,Paul Renauld,1923,victim
The Murder of Roger Ackroyd,man,Roger Ackroyd,1926,victim
The Murder of Roger Ackroyd,woman,Mrs Ferrars,1926,victim
The Big Four,man,Mayerling,1927,victim
The Big Four,man,Mr Jonathan Whalley,1927,victim
The Big Four,man,Mr Paynter,1927,victim
The Big Four,man,Gilmour Wilson,1927,victim
The Big Four,man,John Ingles,1927,victim
The Big Four,woman,Miss Monro,1927,victim
The Mystery of the Blue Train,woman,Ruth Kettering,1928,victim
Peril at End House,woman,Maggie,1931,victim
Lord Edgware Dies,man,Lord Edgware,1933,victim
Lord Edgware Dies,man,Donald Ross,1933,victim
Lord Edgware Dies,woman,Carlotta Adams,1933,victim
Murder on the Orient Express,man,Ratchett/Cassetti,1934,victim
Three Act Tragedy,man,Reverend Babbington,1934,victim
Three Act Tragedy,man,Dr Strange,1934,victim
Three Act Tragedy,woman,Mrs De Rushbridger,1934,victim
Death in The Clouds,woman,Madame Giselle ,1935,victim
Murder in Mesopotamia,woman,Louise Leidner,1935,victim
Murder in Mesopotamia,woman,Miss Johnson,1935,victim
The ABC Murders,woman,Alice Ascher,1936,victim
The ABC Murders,woman,Betty Barnard,1936,victim
The ABC Murders,man,Sir Carmichael Clarke,1936,victim
Cards on the Table,man,Mr Shaitana,1936,victim
Cards on the Table,woman,Mrs Lorrimer,1936,victim
Cards on the Table,woman,Anne,1936,victim
Dumb Witness,woman,Emily Arundell,1937,victim
Death on the Nile,woman,Linnet Doyle,1937,victim
Death on the Nile,woman,Louise,1937,victim
Death on the Nile,woman,Mrs Otterbourne,1937,victim
Appointment with Death,woman,Mrs Boynton,1937,victim
Hercule Poirot's Christmas,man,Simeon Lee,1938,victim
Sad Cypress,woman,Laura Welman,1940,victim
Sad Cypress,woman,Mary Gerrard,1940,victim
Evil Under the Sun,man,Henry Morley,1940,victim
"One, Two, Buckle my Shoe",woman,Nameless,1940,victim
Five Little Pigs,man,Amyas Crale,1941,victim
The Hollow,man,John Christow,1946,victim
Taken at the Flood,man,Enoch Arden,1948,victim
Mrs McGinty's Dead,woman,Mrs McGinty,1952,victim
After the Funeral,man,Richard Abernethie,1953,victim
Hickory Dickory Dock,woman,Celia Austin,1955,victim
Dead Man's Folly,woman,Marlene Tucker,1956,victim
Cat Among the Pigeons,woman,Miss Springer,1959,victim
Cat Among the Pigeons,woman,Miss Vansittart,1959,victim
The Clocks,man,Merlina Rival,1963,victim
The Clocks,woman,Nameless dead man,1963,victim
Third Girl,man,David Baker,1966,victim
Hallowe'en Party,woman,Joyce Reynolds,1969,victim
Elephants can Remember,woman,Mrs Ravenscroft,1972,victim
Curtain,man,Stephen Norton,1975,victim