Theming & styling
A Theme is just a value object — a plain record of colors, sizes, durations,
and behavior flags. The chart keeps it in a reactive signal, so swapping
themeDefault for themeMinimalGrid (or a fully custom theme) re-renders
live without touching your data or mark layers. Because themes are values, they
compose cleanly: the theme() factory deep-merges a partial override into any
base, so you only touch what you mean to change.
Two built-in themes ship out of the box:
| Export | Background | Text | Good for |
|---|---|---|---|
themeDefault | Near-black rgba(0.02, 0.04, 0.07, 1) | Light | Dashboards, dark UIs |
themeMinimalGrid | White | Dark | Reports, light UIs, print |
Apply a theme in whichever way fits your flow:
import { plot, theme, themeDefault, themeMinimalGrid } from "insomni-plot";import { rgba } from "insomni";
// 1. Pass it in the specplot({ data, theme: themeMinimalGrid }).layer(/* … */);
// 2. Builder method — returns a new Chart, original is unchangedplot({ data }).theme(themeMinimalGrid).layer(/* … */);
// 3. Deep-merge a partial override onto a baseconst myTheme = theme( { marks: { strokeWidth: 2, barCornerRadius: 3 }, axis: { gridColor: rgba(0, 0, 0, 0.05) }, }, themeMinimalGrid, // base; defaults to themeDefault when omitted);
plot({ data, theme: myTheme }).layer(/* … */);Theme fields reference
Section titled “Theme fields reference”The Theme interface has 14 top-level fields. Most are sub-objects you’ll
partially override rather than replace wholesale.
background
Section titled “background”Color — the panel fill used when the chart paints its own background. The
accessibility system uses this as the reference background when computing
contrast for axis labels and legends.
| Field | Type | Default (themeDefault) |
|---|---|---|
color | Color | rgba(0.85, 0.9, 0.98, 1) |
fontFamily | string | "system-ui, -apple-system, Inter, sans-serif" |
Base style for all non-title/subtitle text that doesn’t have its own theme slot. Override font-family here to retheme everything at once.
title and subtitle
Section titled “title and subtitle”| Field | Type | Default (themeDefault) |
|---|---|---|
title.fontSize | number | 14 |
title.fontWeight | string | "600" |
title.color | Color | near-white |
subtitle.fontSize | number | 14 |
subtitle.color | Color | muted near-white |
Override on the chart: theme({ title: { fontSize: 16, fontWeight: "700" } }).
| Field | Type | Notes |
|---|---|---|
color | Color | Spine and tick color |
gridColor | Color | Default gridline color |
labelFontSize | number | Tick label font size (px) |
labelColor | Color | Tick label color |
titleFontSize | number | Axis title font size (px) |
titleColor | Color | Axis title color |
Per-axis visual overrides (gridline style, placement, etc.) go through the
.axes() builder — see § Axis visual overrides and
Axes & coordinates.
legend
Section titled “legend”| Field | Type | Default |
|---|---|---|
fontSize | number | 14 |
labelColor | Color | Light near-white |
swatchGap | number | 6 |
entryGap | number | 14 |
The most-used sub-object — geoms read these as their per-layer defaults before any geom-level option overrides kick in.
| Field | Type | Default | Consumed by |
|---|---|---|---|
pointRadius | number | 3 | point, connectedScatter |
pointStroke | Color | undefined | rgba(1,1,1,0.18) | point |
pointStrokeWidth | number | 0.75 | point |
strokeWidth | number | 1.5 | line, smooth, statRolling, area outline, aggregate |
barCornerRadius | number | 1 | bar, histogram |
fillAlpha | number | 0.85 | bar, area, violin, boxplot |
labelFontSize | number | 14 | bar value labels, text geom |
ruleStrokeWidth | number | 1 | rule |
ruleLabelInset | number | 4 | rule annotations |
annotationFontSize | number | 14 | rule, band labels |
ribbonFillAlpha | number | 0.18 | ribbon |
bandFillAlpha | number | 0.08 | band |
sizeRange | [number, number] | [2, 14] | size channel pixel range |
alphaRange | [number, number] | [0.2, 1] | alpha channel range |
palettes
Section titled “palettes”The default color palettes the chart uses when no explicit scale palette is
set. Three slots: categorical (defaults to category10), continuous
(defaults to viridis), and diverging. Override any or all:
import { theme, themeDefault } from "insomni-plot";import { viridis, tableau10 } from "insomni-plot/core";
const t = theme({ palettes: { categorical: tableau10, continuous: viridis } }, themeDefault);accents
Section titled “accents”Semantic colors for chart-level annotations — reference regions, threshold rules, callouts:
| Field | Suggested use |
|---|---|
positive | Uptrend lines, positive bands |
negative | Downtrend lines, alert bands |
warn | Warning thresholds |
info | Informational references |
accessibility, textEffects, paletteBlendSpace
Section titled “accessibility, textEffects, paletteBlendSpace”Covered in dedicated sections below.
motion, interactions
Section titled “motion, interactions”Covered in Motion & interaction tokens.
Color palettes
Section titled “Color palettes”Continuous palettes
Section titled “Continuous palettes”All continuous palettes are sampled as palette(t) where t ∈ [0, 1].
| Export | Kind | Character |
|---|---|---|
viridis | Sequential | Purple → green → yellow |
plasma | Sequential | Purple → orange → yellow |
inferno | Sequential | Black → red → yellow-white |
magma | Sequential | Black → purple → cream |
cividis | Sequential | Navy → blue-grey → yellow (CVD-safe) |
blues, greens, oranges, reds, purples, greys | Sequential single-hue | ColorBrewer |
coolwarm | Diverging | Blue → grey → red |
rdbu | Diverging | Red → white → blue |
spectral | Diverging | Red → yellow → blue |
brbg, prgn, piyg, puor | Diverging | ColorBrewer |
Build your own: continuousPalette(hexStops) accepts an array of hex strings
and returns a sampler with the same (t) => Color interface.
Categorical palettes
Section titled “Categorical palettes”| Export | Size | Source |
|---|---|---|
category10 | 10 | D3 / Observable (chart default) |
tableau10 | 10 | Tableau |
set1, set2, set3 | 9 / 8 / 12 | ColorBrewer |
dark2 | 8 | ColorBrewer |
accent | 8 | ColorBrewer |
paired | 12 | ColorBrewer (12 paired hues) |
pastel | 8 | — |
Build your own: categoricalPalette(hexColors) takes a string array and
returns a categorical palette compatible with the color scale.
colorScale — bridge to the scale system
Section titled “colorScale — bridge to the scale system”When you need a palette as a callable color function outside of insomni-plot’s
scale machinery, colorScale wraps it:
import { colorScale, viridis, tableau10 } from "insomni-plot/core";
// Continuous: map a numeric domain to colorsconst heat = colorScale(viridis, [0, 100]);heat(42); // → Color
// Categorical: map string keys to colorsconst byCategory = colorScale(tableau10, ["low", "med", "high"]);byCategory("med"); // → ColorpaletteBlendSpace
Section titled “paletteBlendSpace”theme.paletteBlendSpace: BlendSpace controls how the chart interpolates
continuous palette stops when building color scales and color bars. The
default is "oklch" — perceptually uniform with hue preserved along the
gradient. Fall back to "srgb" for Web-standard interpolation:
theme({ paletteBlendSpace: "srgb" }, themeDefault);To pin a single palette’s blend space independently of the theme, use
palette.withBlendSpace("oklch").
Gradients
Section titled “Gradients”Pass a Gradient as a geom’s fill to paint a screen-space gradient rather
than a flat color or a per-datum color scale. Useful for area charts and large
filled shapes where a continuous hue ramp adds depth without encoding data.
import { plot, area, linearGradient } from "insomni-plot";import { rgba } from "insomni";
const grad = linearGradient( [ { offset: 0, color: rgba(0.3, 0.6, 1, 0.9) }, { offset: 1, color: rgba(0.3, 0.6, 1, 0.05) }, ], { along: "y" }, // "x" | "y", default "y");
plot({ data: series }) .layer(area({ x: (d) => d.date, y: (d) => d.value, fill: grad })) .mount(canvas, { device });linearGradient(stops, options?) — requires ≥ 2 stops. Each stop is
{ offset: number; color: Color } where offset is in [0, 1]. The
along option defaults to "y" (top → bottom).
isGradient(x) is a type-guard you can use when building geom wrappers
that accept either a Color or a Gradient.
Accessibility
Section titled “Accessibility”The chart applies a readability policy at every text draw site that knows its local background — axis labels over the panel, legend labels over the background, tile labels over their cell color. The defaults are intentionally strict:
export const DEFAULT_ACCESSIBILITY: Accessibility = { enabled: true, metric: "apca", // "apca" | "wcag" wcagLevel: 7, // contrast ratio floor; only used in wcag mode apcaTarget: "auto", // |Lc| floor or "auto" (font-size lookup) mode: "auto-color", // "auto-color" | "outline" | "shadow" | "none" warn: true, // console.warn when a color fails and gets fixed equalize: true, // normalize mark labels to the same contrast level themeBias: 0.5, // blend toward nearest passing theme accent on fix blendSpace: "oklch",};metric — APCA ("apca") models perceived contrast better than WCAG for
modern displays and is the default. Switch to "wcag" if your organization
has a hard WCAG ratio requirement.
equalize — when true, tile and heatmap labels are normalized to the
same contrast floor across all cells (the worst cell’s reachable ceiling
becomes the shared floor). The internal helper is layerEqualizeTarget(theme, backgrounds) — useful if you’re building a custom geom.
mode — what to do when a color fails:
"auto-color"— shift the color toward the nearest passing accent (default)"outline"/"shadow"—TextEffectsstub; falls back toauto-colorwith a one-time console warning (not yet rendered by the SDF text path)"none"— pass the original color through unchanged
Override per chart:
theme({ accessibility: { metric: "wcag", wcagLevel: 4.5, warn: false } });Axis visual overrides
Section titled “Axis visual overrides”Per-call overrides go through the .axes() builder and apply on top of the
theme’s axis sub-object. See Axes & coordinates for
the full tick / domain / coordinate-system API.
AxisSpec field | Type | Notes |
|---|---|---|
gridLines | boolean | Show or hide gridlines |
gridDash | "solid" | "dashed" | "dotted" | number[] | Default "solid" |
gridColor | Color | Overrides theme.axis.gridColor on this axis only |
gridWidth | number | Gridline width (px) |
axisLine | boolean | Show or hide the axis spine (default true) |
placement | "outside" | "inside" | Labels inside the plot frame |
labelAnchor | "on-line" | "above-line" | "below-line" | Vertical label position for inside placement |
side | "left" | "right" | Which side a y-axis renders on |
fullBleed + inside placement — fullBleed removes the gutter on a given
edge but does not hide the axis. Pair with placement: "inside" to keep labels
readable over the marks:
plot({ data }) .fullBleed({ left: true, right: true }) .axes({ x: { placement: "inside", gridDash: "dashed" } });Layout knobs
Section titled “Layout knobs”The chart’s outer geometry is controlled by three independent knobs:
| API | Where | Default | Effect |
|---|---|---|---|
padding builder | .padding(spec) | 0 | Outer inset applied before axes and legend are reserved |
framePadding spec field | plot({ framePadding }) | 0 | Inner slack between the axis lines and the data viewport |
fullBleed builder | .fullBleed(spec) | false | Bypasses axis-gutter reservation on the given edges |
DEFAULT_PADDING = 0 — the plot fills its canvas by default with no outer
margin. Add padding when you need breathing room around the whole chart, or
framePadding when you want the axis lines to sit away from the data without
shrinking the outer container.
Both padding and framePadding accept a uniform number or a per-side
object { top?, right?, bottom?, left? }. fullBleed accepts true (all
edges) or the same per-side object.
Shape, border-style & overlay-glyph palettes
Section titled “Shape, border-style & overlay-glyph palettes”When point or connectedScatter map a categorical channel to shape,
borderStyle, or overlayGlyph, these are the default orderings. Override
any of them via .scale(channel, { palette: [...] }).
Shape palette
Section titled “Shape palette”POINT_SHAPE_PALETTE — ordered by silhouette distinctness:
"circle", "triangle", "square", "diamond", "plus", "cross", "star","circle-open", "triangle-open", "square-open", "diamond-open"Border-style palette
Section titled “Border-style palette”DEFAULT_BORDER_STYLE_PALETTE:
["solid", "open", "dashed", "dotted"];PointBorderStyle = "solid" | "dashed" | "dotted" | "open".
Overlay-glyph palette
Section titled “Overlay-glyph palette”DEFAULT_OVERLAY_GLYPH_PALETTE:
[null, "plus", "cross", "star", "diamond"];The first slot is null — the most-common category gets no overlay, which
reduces visual clutter in the majority case.
distinguishable — co-optimized color + shape
Section titled “distinguishable — co-optimized color + shape”When you’re multi-encoding (same variable on both color and shape), use
palettes.distinguishable to get palettes that are co-optimized for the same
number of categories:
import { plot, point, palettes } from "insomni-plot";
const { color, shape } = palettes.distinguishable({ for: { color: 5, shape: 5 }, scheme: "tableau10", // optional; defaults to "category10"});
plot({ data: rows }) .layer(point({ x: (d) => d.x, y: (d) => d.y, color: (d) => d.species, shape: (d) => d.species })) .scale("color", { palette: color }) .scale("shape", { palette: shape }) .legend({ merge: ["color", "shape"] }) .mount(canvas, { device });Available scheme values: "category10", "tableau10", "set1", "set2",
"set3", "dark2", "paired", "pastel".
Motion & interaction tokens
Section titled “Motion & interaction tokens”ThemeMotion
Section titled “ThemeMotion”theme.motion is the master motion budget. Geoms and interactions read named
durations and easings rather than hard-coded milliseconds — a single theme
swap retunes the whole chart’s feel.
| Field | Type | Default |
|---|---|---|
enabled | boolean | true |
duration.fast | number (ms) | 120 |
duration.base | number (ms) | 240 |
duration.slow | number (ms) | 480 |
easing.standard | Easing | ease-in-out cubic |
easing.emphasized | Easing | ease-out cubic |
easing.decelerate | Easing | ease-out quad |
easing.linear | Easing | linear |
data | ThemeMotionChannel | { duration: "base", easing: "emphasized" } |
axis | ThemeMotionChannel | { duration: "base", easing: "standard" } |
tooltip.showDelay | number (ms) | — |
tooltip.hideDelay | number (ms) | — |
tooltip.fadeMs | number (ms) | — |
tooltip.settleDelay | number (ms) | — |
motion.enabled is the master kill switch for animation. Set it to false
to suppress all transitions. To respect the OS-level preference, read the
standard media query yourself and pass the result into the theme:
import { plot, theme } from "insomni-plot";
// Respect the OS-level setting with the standard media query:const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;const chart = plot({ data }).theme(theme({ motion: { enabled: reduce ? false : true } }));ThemeInteractions
Section titled “ThemeInteractions”theme.interactions controls what happens visually when a row is hovered or
selected.
hover and selection are each a ThemeInteractionEmphasis:
| Field | Type | Notes |
|---|---|---|
enabled | boolean | Master switch; false skips halo and dim |
dim | number | Multiplier on non-active rows’ fill alpha (1 = no dim) |
haloStrokeWidth | number | Halo ring width (CSS px); 0 skips the halo |
haloColor | Color | undefined | Omit to auto-pick a high-contrast foreground |
durationMs | number | undefined | Dim animation duration; <= 0 snaps |
hoverSwapGraceMs (default 50) — coalesces a hover-exit → hover-enter
pair within this window so adjacent marks don’t flash an un-dim/re-dim cycle.
tooltipAccents — semantic colors for tooltip rows using the
accent: "positive" | "negative" shorthand:
theme({ interactions: { hover: { dim: 0.3, haloStrokeWidth: 2 }, tooltipAccents: { positive: rgba(0.1, 0.75, 0.4, 1), negative: rgba(0.9, 0.25, 0.25, 1), }, },});