Croissant
A croissant chart of the standard normal distribution: the density curve cut at its quintiles into five slices of equal probability, each with a dot at its median, in the colors of the paper's anatomy figure.
ts
import { Bin, chart, circle, derive, field, intersect, layer, partition, quantileRows, region, ribbon, scatter } from "gofish-graphics";
const FILL = "#D1E4F6";
const STROKE = "#467DA8";
const PAPER_FILL = "#CBA45F";
const PAPER_STROKE = "#745931";
const normPdf = (x: number, mu = 0, sd = 1) =>
Math.exp(-0.5 * ((x - mu) / sd) ** 2) / (sd * Math.sqrt(2 * Math.PI));
const gridOf = (f: (x: number) => number, lo: number, hi: number, n: number) =>
Array.from({ length: n }, (_, i) => {
const x = lo + ((hi - lo) * i) / (n - 1);
return { x, pdf: f(x) };
});
const croissant = (
grid: { x: number; pdf: number }[],
{ n = 10, fill = FILL, stroke = STROKE } = {}
) =>
layer([
chart(grid)
.flow(scatter({ x: "x" }))
.mark(ribbon({ h: "pdf" }).name("density").hidden()),
]).relate(({ density }) => [
chart(grid)
.flow(
derive((rows) =>
quantileRows(rows, { x: "x", density: "pdf", n: 1000 })
),
partition({ by: field("x").bin(Bin.quantiles(n)), dir: "x" })
)
.mark(
layer([
intersect({ fill, stroke, strokeWidth: 2 }, [
density,
region({ inset: 3 }),
]),
circle({ cx: field("x").median(), cy: 6, r: 3, fill: stroke }),
])
),
]);
const normalGrid = gridOf((x) => normPdf(x), -4, 4, 401);
const container = document.getElementById("app");
croissant(normalGrid, {
n: 5,
fill: PAPER_FILL,
stroke: PAPER_STROKE,
}).render(container, {
w: 480,
h: 200,
axes: { x: true, y: false },
});