Public
Edited
Mar 23
Insert cell
Insert cell
Plot.plot({
width: width,
height: height,
marginRight: 800,
y: {
// grid: true,
label: "↑ Unemployment (%)"
},
marks: [
Plot.ruleY([0]),
Plot.lineY(bls, {x: "date", y: "unemployment", z: "division"}),

// // by Observable
// Plot.text(bls.filter((elem) => elem.date.getTime() === (new Date("2013-10-01")).getTime()), occlusionY({
// x: "date",
// y: "unemployment",
// text: "division",
// textAnchor: "start",
// dx: 4,
// radius: 5
// }))

// by James Trimble
// https://observablehq.com/@jtrim-ons/label-placement-for-a-slope-chart-2
Plot.text(bls.filter((elem) => elem.date.getTime() === (new Date("2013-10-01")).getTime()), occlusionYByJamesTrimble({
x: "date",
y: "unemployment",
text: "division",
textAnchor: "start",
dx: 4,
radius: 5
}))
]
})
Insert cell
bls = FileAttachment("bls-metro-unemployment.csv").csv({typed: true})
Insert cell
width = 1400
Insert cell
height = 600;
Insert cell
// OcclusionY adds an initializer that shifts nodes vertically with a tiny force simulation.
occlusionY = ({radius = 6.5, ...options} = {}) => Plot.initializer(options, (data, facets, { y: {value: Y}, text: {value: T} }, {y: sy}, dimensions, context) => {
for (const index of facets) {
const unique = new Set();
const nodes = Array.from(index, (i) => ({
fx: 0,
y: sy(Y[i]),
visible: unique.has(T[i]) // remove duplicate labels
? false
: !!unique.add(T[i]),
i
}));
d3.forceSimulation(nodes.filter((d) => d.visible))
.force("y", d3.forceY(({y}) => y)) // gravitate towards the original y
.force("collide", d3.forceCollide().radius(radius)) // collide
.stop()
.tick(1000);
for (const { y, node, i, visible } of nodes) Y[i] = !visible ? NaN : y;
}
return {data, facets, channels: {y: {value: Y}}};
})
Insert cell
f = (zs) => {
let batches = [];
for (let z of zs) {
batches.push({ size: 1, mean: z });
while (batches.length > 1) {
let b = batches[batches.length - 2];
let c = batches[batches.length - 1];
if (b.mean < c.mean) break;
b.mean = (b.mean * b.size + c.mean * c.size) / (b.size + c.size);
b.size = b.size + c.size;
batches.pop();
}
}
let xs = [];
for (const batch of batches)
for (let i = 0; i < batch.size; i++) xs.push(batch.mean);
return xs;
}
Insert cell
g = (ys, radius) => {
let zs = ys.map((y, i) => y - i * radius * 2);
return f(zs).map((x, i) => x + i * radius * 2);
}
Insert cell
occlusionYByJamesTrimble = (options) =>
Plot.initializer(
options,
(
data,
facets,
{ y: { value: Y }, text: { value: T } },
{ y: sy },
dimensions,
context
) => {
let RADIUS = 5;
for (const index of facets) {
// Sort the nodes and call the label placement function g
const nodes = Array.from(index, (i) => ({
y: sy(Y[i]),
i
}));
nodes.sort((a, b) => a.y - b.y);
const positions = g(
nodes.map((d) => d.y),
RADIUS
);
positions.forEach((p, i) => {
Y[nodes[i].i] = p;
});
}
return { data, facets, channels: { y: { value: Y } } };
}
)
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