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).
Dashed-to-solid line (forecast split)
Section titled “Dashed-to-solid line (forecast split)”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.
Fallback: emoji text() layer
Section titled “Fallback: emoji text() 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.