Skip to content

Recipes

Recipes show how to compose existing geoms to achieve effects that aren’t directly configurable on a single layer. All imports are from "insomni-plot" (and "insomni" for color helpers).


Reference: a weather temperature curve where the historical portion is solid and the forecast portion is dashed.

Why two layers? line() takes one dash treatment per series: either dashStyle?: "dashed" | "dotted" or dashPattern?: readonly number[]. There is no per-segment dash on a single line() call. Plot also shares one data array across all layers — .layer() takes only a geom, with no per-layer data override. So the idiomatic way to show a dashed past → solid future transition is to render two line() layers over the same data, each masking the half it should not draw by returning NaN from its y accessor. Non-finite points break the path, so each layer renders only its own segment, and because both layers keep the boundary point finite the solid and dashed runs meet exactly.

import { plot, line } from "insomni-plot";
import { rgba } from "insomni";
type Row = { date: Date; temp: number };
const data: Row[] = [/* ... full series, past + forecast, in order ... */];
// Index of the first forecast row.
const boundary = 10;
const stroke = rgba(0.2, 0.7, 0.9, 1);
const canvas = document.getElementById("chart") as HTMLCanvasElement;
plot<Row>({ data })
// Solid past: real values up to & including the boundary, NaN afterwards.
.layer(
line<Row>(
{ x: (d) => d.date, y: (d, i) => (i <= boundary ? d.temp : NaN) },
{ stroke, strokeWidth: 2 },
),
)
// Dashed future: NaN before the boundary, real values from the boundary on.
.layer(
line<Row>(
{ x: (d) => d.date, y: (d, i) => (i >= boundary ? d.temp : NaN) },
{ stroke, strokeWidth: 2, dashStyle: "dashed" },
),
)
.mount(canvas, { device });

Both layers resolve against the same plot scales (computed from the single plot({ data }) extent that covers the whole series), so the two runs align without any extra .scale() wiring.


Icons in marks (current fallback + deferral)

Section titled “Icons in marks (current fallback + deferral)”

Reference: weather capsules with a sun/rain icon embedded inside each mark, and an icon row beneath the temperature curve.

Current status — deferred. Core insomni exposes Layer.pushSprite for GPU sprite atlases and a CPU glyph atlas via insomni/text-ttf’s ln() helper, but these primitives are not yet surfaced through plot’s geom API. A dedicated icon() / marker() geom that accepts an image or SVG icon per datum is a planned follow-up once the pushSprite path is wired into the grammar layer.

The simplest stand-in is a text() geom whose accessor returns an emoji string:

import { plot, text } from "insomni-plot";
type Row = { month: string; icon: string; value: number };
const data: Row[] = [
{ month: "Jan", icon: "🌧️", value: 4 },
{ month: "Feb", icon: "", value: 6 },
{ month: "Mar", icon: "☀️", value: 9 },
/* ... */
];
const canvas = document.getElementById("chart") as HTMLCanvasElement;
plot<Row>({ data })
.layer(
text<Row>(
{
x: (d) => d.month,
y: () => 0, // fixed row along the baseline
text: (d) => d.icon,
},
{ fontSize: 18, align: "center" },
),
)
.mount(canvas, { device });

For a single prominent icon on a scatter mark, use point() with overlayGlyph (limited to the built-in PointShapeKind set — not arbitrary images).

Weather capsule recipe (pill bar + value label + emoji icon)

Section titled “Weather capsule recipe (pill bar + value label + emoji icon)”

Compose three layers on one plot to approximate the monthly weather capsule look: a pill-shaped bar() (achieved via a large cornerRadius), a text() value label, and an emoji icon row.

import { plot, bar, text } from "insomni-plot";
import { rgba } from "insomni";
type Month = { month: string; value: number; icon: string };
const data: Month[] = [
{ month: "Jan", value: 7, icon: "🌨️" },
{ month: "Feb", value: 9, icon: "🌧️" },
{ month: "Mar", value: 14, icon: "" },
{ month: "Apr", value: 18, icon: "🌤️" },
{ month: "May", value: 23, icon: "☀️" },
];
const canvas = document.getElementById("chart") as HTMLCanvasElement;
plot<Month>({ data })
// Pill-shaped capsule bar (baseline → value). A large cornerRadius rounds the ends.
.layer(
bar<Month>(
{ x: (d) => d.month, y: (d) => d.value },
{ fill: rgba(0.2, 0.55, 0.9, 0.85), cornerRadius: 20 },
),
)
// Value label at the top of each bar.
.layer(
text<Month>(
{ x: (d) => d.month, y: (d) => d.value, text: (d) => `${d.value}°` },
{ fontSize: 11, align: "center", offsetY: -8 },
),
)
// Emoji icon row near the baseline.
.layer(
text<Month>(
{ x: (d) => d.month, y: () => 0, text: (d) => d.icon },
{ fontSize: 16, align: "center", offsetY: 14 },
),
)
.mount(canvas, { device });

A bar() fills from the baseline to its value, so the rounded ends read as a capsule. A floating low→high range capsule with rounded ends is not directly expressible today (bar fills from the baseline; interval draws a stroked range with no fill or corner radius) — that, like per-datum image icons, waits on the pushSprite/range-rect follow-up.

Follow-up: once Layer.pushSprite is wired into the plot grammar as an icon() geom, the text(emoji) layers above can be replaced with proper per-datum image/SVG icons with exact sizing and positioning.