Published
Edited
Jun 30, 2020
1 fork
Insert cell
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [0, 0, width, height]);
svg.append("g").call(xAxisBottom);
svg.append("g").call(xAxisTop);
// grid lines along decades
svg.selectAll(".grid-line")
.data(x.ticks())
.join("line")
.attr("class", "grid-line")
.attr("x1", d => x(d))
.attr("x2", d => x(d))
.attr("y1", margin.top)
.attr("y2", height - margin.top)
.attr("stroke-width", 1)
.attr("stroke", "lightgray");
// thin lines separating beeswarm plots
svg.selectAll(".line-beeswarm")
.data(families)
.join("line")
.attr("class", "line-beeswarm")
.attr("x1", margin.left - 10)
.attr("x2", width - margin.right + 10)
.attr("y1", d => y(d))
.attr("y2", d => y(d))
.attr("stroke-width", 0.2)
.attr("stroke", d => color(d));
// party family labels
svg.selectAll(".label-family")
.data(families)
.join("text")
.attr("class", "label-family")
.attr("x", 0)
.attr("y", d => y(d))
.attr("alignment-baseline", "middle")
.text(d => d ? d : "other");
const pair = svg.selectAll(".beeswarm-pair")
.data(nested)
.join("g")
.attr("class", (d) => "beeswarm-pair")
.attr("fill", (d) => color(d.key));
// beeswarm plots top
pair.selectAll(".circle-top")
.data((d) => dodge(d.values.filter(e => e.currentShare), radius * 2 + padding))
.join("circle")
.attr("class", "circle-top")
.attr("cx", (e) => e.x)
.attr("cy", (e) => y(e.data.family) - radius - padding - e.y);
// beeswarm plots bottom
pair.selectAll(".circle-bottom")
.data((d) => dodge(d.values.filter(e => !e.currentShare), radius * 2 + padding))
.join("circle")
.attr("class", 'circle-top')
.attr("cx", (e) => e.x)
.attr("cy", (e) => y(e.data.family) + radius + padding + e.y)
.attr('opacity', 0.25);
pair.selectAll("circle")
.attr("r", radius)
.append("title")
.text(d => `${d.data.party} (${d.data.country}, ${formatYear(d.data.date)}); ${d.data.share} -> ${d.data.currentShare}`);
return svg.node();
}
Insert cell
nested = d3.nest()
.key((d) => d.family)
.entries(data)
Insert cell
color = d3.scaleOrdinal()
.domain(families)
.range(d3.schemeTableau10)
Insert cell
y = d3.scaleBand()
.domain(families)
.range([margin.top, height - margin.bottom,])
.padding(1)
Insert cell
x = d3.scaleTime()
.domain(timeExtent)
.nice()
.range([margin.left, width - margin.right])
Insert cell
families = d3.map(data, (d) => d.family).keys()
Insert cell
yAxis = g => g
.attr('transform', `translate(${margin.left}, 0)`)
.call(d3.axisLeft(y))
Insert cell
xAxisTop = g => g
.attr('transform', `translate(0, ${margin.top})`)
.call(d3.axisTop(x))
Insert cell
xAxisBottom = g => g
.attr('transform', `translate(0, ${height - margin.top})`)
.call(d3.axisBottom(x))
Insert cell
timeExtent = {
const dates = data.map((d) => d.date).sort(d3.ascending);
return [dates[0], dates[dates.length - 1]];
}
Insert cell
data = rawData
Insert cell
rawData = {
const parseTime = d3.timeParse("%Y-%m-%d");
return d3.csvParse(
await FileAttachment("parlgov2018.csv").text(),
d => ({
party: d.party_name_english,
date: parseTime(d.election_date),
value: parseTime(d.election_date), // dodge function accesses datum.value
country: d.country_name,
family: d.family_name_short && !["none", "code"].includes(d.family_name_short) ? d.family_name_short : "",
share: +d.vote_share,
currentShare: +d.most_recent_vote_share
})
);
}
Insert cell
formatYear = d3.timeFormat("%Y")
Insert cell
margin = ({
left: 50,
right: 30,
top: 50,
bottom: 50
})
Insert cell
padding = 1.5
Insert cell
radius = 3.5
Insert cell
height = 1200
Insert cell
import {dodge} with {x as x} from "@d3/beeswarm"
Insert cell
d3 = require("d3@5")
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more