Public
Edited
Jan 10
45 stars
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
averages = FileAttachment("averages.csv").csv({typed: true})
Insert cell
polls = FileAttachment("polls@1.csv").csv({typed: true})
Insert cell
Insert cell
Insert cell
Insert cell
color = Plot.scale({color: { domain: ["DEM", "IND", "REP", "LIB", "GRE"] }})
Insert cell
Insert cell
r = Plot.scale({r: { domain: [0, 3], range: [0.2, 2] }})
Insert cell
Insert cell
Plot.plot({
r,
color,
marks: [
Plot.ruleY([0]),
Plot.dot(polls, {
x: "date",
y: "pct",
fill: "party",
r: "numeric_grade",
fillOpacity: 0.2
}),
Plot.lineY(averages, {
x: "date",
y: "pct",
stroke: "party",
strokeWidth: 2,
tip: { channels: { candidate: "candidate" } }
})
]
})
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
mutable peek = null
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
customOverlapRender = ({ minDistance = 26 } = {}) =>
(index, scales, values, dimensions, context, next) => {
const { channels } = values;
const Z =
channels.z?.value ?? channels.stroke?.value ?? channels.fill?.value;
if (index.length >= 2) {
const a = index.find((i) => Z[i] === "DEM");
const b = index.find((i) => Z[i] === "REP");
let d = values.y[a] - values.y[b];
if (Math.abs(d) < minDistance) {
d = ((minDistance - Math.abs(d)) * Math.sign(d)) / 2;
const Y = values.y.slice();
Y[a] += d;
Y[b] -= d;
values = { ...values, y: Y };
}
}
return next(index, scales, values, dimensions, context);
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
pointerMX = pointerMore(Plot.pointerX)
Insert cell
pointerM = pointerMore(Plot.pointer)
Insert cell
pointerMY = pointerMore(Plot.pointerY)
Insert cell
function pointerMore(pointer) {
return ({ atrest = null, selector = "point", render, ...options } = {}) => {
const I = new WeakMap(); // Memoize the at rest index for each facet (for performance).

return pointer({
...options,
render(index, scales, values, dimensions, context, next) {
const facet = context.getMarkState(this).facets[index.fi ?? 0]; // full index for the current facet
const { x, x1, x2, y, y1, y2, z, stroke, fill } = values.channels;
const X = x?.value ?? x2?.value ?? x1?.value;
const Y = y?.value ?? y2?.value ?? y1?.value;
const VX = values.x ?? values.x2 ?? values.x1;
const VY = values.y ?? values.y2 ?? values.y1;
const Z = z?.value ?? stroke?.value ?? fill?.value;

// at rest
if (!I.has(facet)) {
let select;
switch (String(atrest).toLowerCase()) {
case "null":
select = () => [];
break;
case "minx":
if (!X) throw new Error("missing channel x");
select = (index) => [d3.least(index, (i) => X[i])];
break;
case "maxx":
if (!X) throw new Error("missing channel x");
select = (index) => [d3.greatest(index, (i) => X[i])];
break;
case "miny":
if (!Y) throw new Error("missing channel y");
select = (index) => [d3.least(index, (i) => Y[i])];
break;
case "maxy":
if (!Y) throw new Error("missing channel y");
select = (index) => [d3.greatest(index, (i) => Y[i])];
break;
case "first":
select = (index) => index.slice(0, 1);
break;
case "last":
select = (index) => index.slice(-1);
break;
// TODO top, bottom, left, right… ?
}
if (!select) throw new Error(`unsupported atrest method ${atrest}`);
I.set(facet, select(facet));
}

if (index.length === 0) index = I.get(facet);

// selector
switch (String(selector).toLowerCase()) {
case "point":
break;
case "before":
case "lte":
if (!VX) throw new Error("missing channel x");
index = facet.filter((i) => VX[i] <= VX[index[0]]);
break;
case "lt":
if (!VX) throw new Error("missing channel x");
index = facet.filter((i) => VX[i] < VX[index[0]]);
break;
case "after":
case "gte":
if (!VX) throw new Error("missing channel x");
index = facet.filter((i) => VX[i] >= VX[index[0]]);
break;
case "gt":
if (!VX) throw new Error("missing channel x");
index = facet.filter((i) => VX[i] > VX[index[0]]);
break;
case "eq":
case "x":
if (!VX) throw new Error("missing channel x");
index = facet.filter((i) => VX[i] == VX[index[0]]);
break;
case "y":
if (!VY) throw new Error("missing channel y");
index = facet.filter((i) => VY[i] == VY[index[0]]);
break;
case "z":
if (!Z) throw new Error("missing channel z");
index = facet.filter((i) => Z[i] == Z[index[0]]);
break;
default:
throw new Error(`unsupported selector ${selector}`);
}

return render
? render(index, scales, values, dimensions, context, next)
: next(index, scales, values, dimensions, context);
}
});
};
}
Insert cell
Insert cell
party = (PAR) => htl.html`<span ${{style: `border-bottom: solid 2px ${color.apply(PAR)};`}}>${PAR}</span>`
Insert cell
function renderMultipleText({render: prerender, ...options} = {}) {
const multiple = (index, scales, values, dimensions, context, next) => {
const g = next(index, scales, values, dimensions, context);
const text = g.querySelectorAll("text");
const white = g.getAttribute("stroke");

index.forEach((i, j) => {
text[j].setAttribute("stroke", "none");
text[j].setAttribute("fill", white);
const [candidate, pct] = text[j].textContent.split(" | ");
text[j].textContent = pct;
g.append(
svg`<rect x=${values.x[i]-38} y=${values.y[i]-10} width=43 rx=4 height=20 fill="${values.fill[i]}"></rect>${text[j]}<text dx=10 x=${values.x[i]} y=${values.y[i]} text-anchor="start" fill="${values.fill[i]}" dy="0.38em" stroke="${white}" stroke-width=3 paint-order="stroke">${candidate}</text>`
);
})

return g;
};

// combine the renderers
const render = prerender ? (i, s, v, d, c, next) => prerender(i, s, v, d, c, (i, s, v, d, c) => multiple(i, s, v, d, c, next))
: multiple;
return {...options, render}
}
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