Skip to content

← Examples

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 },
});