Skip to content

Instantly share code, notes, and snippets.

@HarryStevens
Last active December 28, 2017 12:35
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save HarryStevens/bd3ce1862ccfbb5b0c78b56aca8fca6c to your computer and use it in GitHub Desktop.
Save HarryStevens/bd3ce1862ccfbb5b0c78b56aca8fca6c to your computer and use it in GitHub Desktop.
Linked Beeswarm
license: gpl-3.0
height: 650
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
<!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="http://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> &amp; <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 + " &amp; " + 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment