@vielzeug/codex 26.11.5 → 26.11.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/data/catalog.json +44 -44
  2. package/data/llms-full.txt +3269 -2941
  3. package/data/llms.txt +1 -1
  4. package/data/manifest.json +1 -1
  5. package/data/packages/archive.json +3 -3
  6. package/data/packages/arsenal.json +2 -2
  7. package/data/packages/assay.json +2 -2
  8. package/data/packages/beacon.json +3 -3
  9. package/data/packages/clockwork.json +3 -3
  10. package/data/packages/codex.json +3 -3
  11. package/data/packages/coins.json +2 -2
  12. package/data/packages/conduit.json +2 -2
  13. package/data/packages/courier.json +3 -3
  14. package/data/packages/dnd.json +2 -2
  15. package/data/packages/familiar.json +2 -2
  16. package/data/packages/flux.json +3 -3
  17. package/data/packages/focus.json +3 -3
  18. package/data/packages/forge.json +3 -3
  19. package/data/packages/gesture.json +3 -3
  20. package/data/packages/herald.json +3 -3
  21. package/data/packages/illusionist.json +4 -4
  22. package/data/packages/keymap.json +3 -3
  23. package/data/packages/ledger.json +3 -3
  24. package/data/packages/lingua.json +3 -3
  25. package/data/packages/mesh.json +3 -3
  26. package/data/packages/necromancer.json +4 -4
  27. package/data/packages/orbit.json +3 -3
  28. package/data/packages/ore.json +2 -2
  29. package/data/packages/postmaster.json +2 -2
  30. package/data/packages/prism.json +3 -3
  31. package/data/packages/pulse.json +3 -3
  32. package/data/packages/refine.json +2 -2
  33. package/data/packages/ripple.json +2 -2
  34. package/data/packages/rune.json +3 -3
  35. package/data/packages/sandbox.json +2 -2
  36. package/data/packages/scout.json +3 -3
  37. package/data/packages/scroll.json +3 -3
  38. package/data/packages/sentinel.json +2 -2
  39. package/data/packages/sigil.json +3 -3
  40. package/data/packages/sourcerer.json +2 -2
  41. package/data/packages/spell.json +3 -3
  42. package/data/packages/tandem.json +2 -2
  43. package/data/packages/tavern.json +2 -2
  44. package/data/packages/tempo.json +2 -2
  45. package/data/packages/vault.json +4 -4
  46. package/data/packages/ward.json +2 -2
  47. package/data/packages/wayfinder.json +3 -3
  48. package/data/refine.json +4176 -4176
  49. package/data/search.json +115 -115
  50. package/package.json +14 -14
  51. package/skills/vielzeug/SKILL.md +19 -17
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "apiSource": "// Public API: all exports for @vielzeug/prism\n\nexport type { EasingFn, EasingName } from './animation/easing';\n// Chart factories\nexport { createAreaChart } from './charts/area';\nexport { createBarChart } from './charts/bar';\nexport { createHeatmapChart } from './charts/heatmap';\nexport { createLineChart } from './charts/line';\nexport { createPieChart } from './charts/pie';\nexport { createRadarChart } from './charts/radar';\nexport { createScatterChart } from './charts/scatter';\nexport { createSparkline } from './charts/sparkline';\n// Error classes\nexport { PrismError, PrismRenderError } from './errors';\n// Scale factories\nexport { bandScale } from './scales/band';\nexport { linearScale } from './scales/linear';\nexport { timeScale } from './scales/time';\nexport type { BandScaleConfig, LinearScaleConfig, TimeScaleConfig } from './scales/types';\n// Theme utilities\nexport { resetTheme, seriesColor, setTheme } from './theme';\nexport type {\n AreaChartConfig,\n AreaSeriesConfig,\n AxisConfig,\n AxisPosition,\n BandScale,\n BarChartConfig,\n BarSeriesConfig,\n BarVariant,\n BaseChartConfig,\n ChartA11y,\n ChartDimensions,\n ChartEvent,\n ChartHandle,\n ChartMargin,\n ContinuousDatum,\n CrosshairConfig,\n Datum,\n GridConfig,\n HeatmapCell,\n HeatmapChartConfig,\n HeatmapData,\n HeatmapEvent,\n HorizontalAxisPosition,\n LegendConfig,\n LegendPosition,\n LineChartConfig,\n LineSeriesConfig,\n PieChartConfig,\n PieEvent,\n PieSliceConfig,\n PieVariant,\n PrismEvent,\n PrismTheme,\n RadarAxisConfig,\n RadarAxisValue,\n RadarChartConfig,\n RadarEvent,\n RadarGridConfig,\n RadarSeriesConfig,\n Scale,\n ScatterChartConfig,\n ScatterSeriesConfig,\n Series,\n SeriesValue,\n SparklineConfig,\n SparklineEvent,\n SparklineVariant,\n StackSegment,\n ThemeScope,\n TooltipConfig,\n TransitionConfig,\n TransitionOption,\n VerticalAxisPosition,\n XAxisConfig,\n YAxisConfig,\n} from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: 'Prism: Responsive SVG data visualization'\ndescription: 'Responsive SVG charts with explicit updates: line, bar, area, scatter, pie, radar, heatmap, sparkline'\npackage: prism\ncategory: ui\nkeywords: [chart, svg, visualization, responsive, line-chart, bar-chart, area-chart, scatter-chart, pie-chart, radar-chart, heatmap-chart, sparkline, accessibility, typescript]\nrelated: [refine, orbit]\nexports: [createLineChart, createBarChart, createPieChart, ChartHandle]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"prism\" />\n\n## Why Prism?\n\nPrism renders charts as SVG in a host element and keeps application state outside the chart. You can replace chart data explicitly, respond to container resizing, and style the output with CSS custom properties.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\n// Before: replace a hand-drawn SVG each time values change.\nfunction drawLine(values: number[]) {\n const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n const low = Math.min(...values);\n const span = Math.max(...values) - low || 1;\n const points = values.map((value, index) => {\n const x = (index / Math.max(1, values.length - 1)) * 640;\n const y = 320 - ((value - low) / span) * 320;\n return `${index === 0 ? 'M' : 'L'}${x},${y}`;\n });\n svg.setAttribute('viewBox', '0 0 640 320');\n path.setAttribute('d', points.join(' '));\n path.setAttribute('fill', 'none');\n path.setAttribute('stroke', 'currentColor');\n svg.append(path);\n host.replaceChildren(svg);\n}\ndrawLine([10, 25, 18]);\ndrawLine([10, 25, 18, 32]);\n\n// After: keep the chart instance and replace its series data.\nconst chart = createLineChart(host, {\n a11y: { ariaLabel: 'Revenue by month' },\n series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }], name: 'Revenue' }],\n});\nchart.update([{\n data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }, { key: 4, value: 32 }],\n name: 'Revenue',\n}]);\nchart.dispose();\n```\n\n| Feature | Prism | Hand-written SVG |\n| --- | --- | --- |\n| Bundle size | <PackageInfo package=\"prism\" type=\"size\" /> | No chart package |\n| Zero dependencies | <ore-icon name=\"x\" size=\"16\"></ore-icon> (uses Orbit) | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Data updates | `ChartHandle.update()` replaces chart data | You implement the update path |\n| Responsive sizing | `ResizeObserver` recalculates the chart layout | You implement the resize behavior |\n| Chart interactions | Built-in pointer and keyboard interactions on supported charts | You implement them |\n| SVG accessibility | Informative charts support an accessible name and keyboard focus | You implement the accessible behavior |\n\n<div class=\"decision-callout\">\n\n**Use Prism when** you need chart factories, responsive SVG output, and an explicit data-update boundary without adopting a charting framework.\n\n**Consider hand-written SVG when** you need a single specialized graphic and prefer to own its scales, resizing, labels, and interactions directly.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/prism\n```\n\n```sh [npm]\nnpm install @vielzeug/prism\n```\n\n```sh [yarn]\nyarn add @vielzeug/prism\n```\n\n:::\n\n## Quick Start\n\nCreate and size a host element before creating the chart. Import `theme.css` once to load Prism's default styles and CSS custom properties.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\nimport '@vielzeug/prism/theme.css';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createLineChart(host, {\n a11y: { ariaLabel: 'Revenue by month' },\n series: [{\n data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }],\n name: 'Revenue',\n }],\n tooltip: true,\n});\n\nchart.update([{\n data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }, { key: 4, value: 32 }],\n name: 'Revenue',\n}]);\n\nchart.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`createLineChart()` / `createAreaChart()`**: plot continuous numeric or date-keyed series\n- **`createBarChart()`**: compare categories with grouped, stacked, or horizontal bars\n- **`createScatterChart()`**: plot independent numeric `(x, y)` points\n- **`createPieChart()`**: render pie, donut, or semi-circle slices\n- **`createRadarChart()`**: compare series across fixed, labelled axes\n- **`createHeatmapChart()`**: render a row/column matrix of colored cells with a continuous or binned ramp\n- **`createSparkline()`**: render compact line, area, bar, or stacked values without axes\n- **`ChartHandle.update(data)`**: replace chart data without handing state ownership to Prism\n- **`ChartHandle.dispose()` / `Symbol.dispose`**: release the SVG, observer, overlays, and chart-owned animation\n- **`ChartHandle.tap()`**: observe resize and disposal events\n- **`setTheme()` / `resetTheme()`**: set supported palette, typography, axis, grid, and tooltip tokens globally or on one element\n- **CSS custom properties**: style the chart palette, marks, axes, labels, tooltip, legend, and motion\n- **Resize observation**: recalculate SVG layout when the host element changes size\n- **Accessible interaction**: labelled interactive charts expose keyboard navigation and polite value announcements; sparklines are pointer-only without value announcements\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/): web components and theme tokens for building controls and layouts around chart hosts\n- [Orbit](/orbit/): floating-element positioning used internally by Prism's built-in tooltip\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: 'Prism: API Reference'\ndescription: Complete reference for Prism chart factories, scales, themes, types, and errors.\n---\n\n[[toc]]\n\n## API Overview\n\nAll chart factories, scale utilities, theme helpers, and error classes are synchronous. Charts render into an HTML element and return a `ChartHandle`; they do not return a promise.\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createLineChart()` | Render one or more continuous line series | Sync | `key` values must be numbers or dates |\n| `createAreaChart()` | Render continuous series with filled areas | Sync | Fills use a gradient by default |\n| `createScatterChart()` | Plot independent `(x, y)` points | Sync | `key` is the x-value; the y-domain is fitted to data |\n| `createBarChart()` | Render categorical bars | Sync | Negative values in stacked variants are clamped to zero |\n| `createPieChart()` | Render pie, donut, or semi-circle slices | Sync | `variant` is fixed at creation; `update()` replaces slices |\n| `createRadarChart()` | Compare series across fixed axes | Sync | Requires at least three axes to draw |\n| `createHeatmapChart()` | Render a row/column matrix of colored cells | Sync | Cells whose `row`/`column` match no label are skipped; unknown labels warn in development |\n| `createSparkline()` | Render a compact line, area, bar, or stack | Sync | Has no axes, legend, or keyboard navigation |\n| `linearScale()` | Create a numeric scale | Sync | `nice` defaults to `true`; set `nice: false` to preserve the domain |\n| `timeScale()` | Create a date scale | Sync | `nice` defaults to `true` |\n| `bandScale()` | Create an ordered categorical scale | Sync | Unknown categories map to `0` and warn in development |\n| `setTheme()` | Set Prism CSS custom properties | Sync | Writes inline styles to `document.documentElement` by default |\n| `resetTheme()` | Remove theme overrides set by `setTheme()` | Sync | Only clears the selected target |\n| `seriesColor()` | Resolve a palette CSS value | Sync | Palette positions wrap after eight |\n| `PrismError` | Base class for Prism errors | Sync | Use `instanceof` to catch any Prism error |\n| `PrismRenderError` | Report an invalid chart container or failed chart operation | Sync | Also thrown by `update()` after disposal |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/prism` | Chart factories, scale and theme functions, error classes, and public types |\n| `@vielzeug/prism/theme.css` | Default chart styles, CSS custom properties, dark-mode values, and reduced-motion rules |\n\n## Chart Factories\n\nEach factory appends an SVG to `container`. A chart with no measurable container size uses a temporary `600 × 300` SVG size and emits a development warning; `ResizeObserver` applies later size changes. Unless `a11y` is informative, the SVG is hidden from the accessibility tree.\n\n### `createLineChart()`\n\n```ts\nfunction createLineChart(\n container: HTMLElement,\n config: LineChartConfig,\n): ChartHandle<LineSeriesConfig[]>;\n```\n\nRenders line series and returns a handle whose `update()` argument replaces the complete series array. Axes render by default; the value axis has grid lines by default. Pass `false` to `xAxis` or `yAxis` to suppress that axis.\n\nSeries use linear interpolation by default, omit point markers by default, and use a point radius of 3 when markers are enabled. `yAxis` defaults to `'left'`; crosshair, tooltip, and legend are disabled unless configured.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `LineChartConfig` | Series, axes, interaction, accessibility, and motion settings |\n\n**Returns:** `ChartHandle<LineSeriesConfig[]>`.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createLineChart(host, {\n a11y: { ariaLabel: 'Revenue by month' },\n series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }], name: 'Revenue' }],\n});\nchart.update([{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }, { key: 3, value: 18 }], name: 'Revenue' }]);\nchart.dispose();\n```\n\n### `createAreaChart()`\n\n```ts\nfunction createAreaChart(\n container: HTMLElement,\n config: AreaChartConfig,\n): ChartHandle<AreaSeriesConfig[]>;\n```\n\nRenders continuous area series with a gradient fill by default. `update()` replaces the complete series array.\n\nArea series use linear interpolation by default and show their top line unless `showLine: false` is set. Axes render by default, with grid lines on the value axis.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `AreaChartConfig` | Series, axes, interaction, accessibility, and motion settings |\n\n**Returns:** `ChartHandle<AreaSeriesConfig[]>`.\n\n```ts\nimport { createAreaChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createAreaChart(host, {\n a11y: { ariaLabel: 'Visitors over time' },\n series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 18 }], name: 'Visitors' }],\n});\nchart.dispose();\n```\n\n### `createScatterChart()`\n\n```ts\nfunction createScatterChart(\n container: HTMLElement,\n config: ScatterChartConfig,\n): ChartHandle<ScatterSeriesConfig[]>;\n```\n\nPlots each datum independently: `key` is the numeric or date x-value and `value` is the y-value. Points are not joined. `update()` replaces the complete series array.\n\nAxes render by default, with grid lines on the value axis. The value scale is fitted to the observed data rather than forced to include zero. Tooltip, crosshair, and legend are disabled unless configured.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `ScatterChartConfig` | Independent points, axes, interaction, accessibility, and motion settings |\n\n**Returns:** `ChartHandle<ScatterSeriesConfig[]>`.\n\n```ts\nimport { createScatterChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createScatterChart(host, {\n a11y: { ariaLabel: 'Height against weight' },\n series: [{ data: [{ key: 160, value: 55 }, { key: 175, value: 70 }], name: 'Cohort A' }],\n tooltip: true,\n});\nchart.dispose();\n```\n\nScatter interaction reports the datum and series of the point nearest the pointer in two dimensions, without a `values` array. Keyboard navigation visits points across series in x order. Tooltip `titleFormat` receives the x-value and `valueFormat` the y-value. On mount, points grow from radius zero at their final positions; updates slide existing points and grow new ones.\n\n### `createBarChart()`\n\n```ts\nfunction createBarChart(\n container: HTMLElement,\n config: BarChartConfig,\n): ChartHandle<BarSeriesConfig[]>;\n```\n\nRenders grouped, stacked, grouped-horizontal, or stacked-horizontal categories. `update()` replaces the complete series array; the variant is fixed at creation. Axes render by default, with grid lines on the value axis.\n\nThe default variant is `grouped`; tooltip and legend are disabled unless configured.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `BarChartConfig` | Series, layout variant, axes, interaction, accessibility, and motion settings |\n\n**Returns:** `ChartHandle<BarSeriesConfig[]>`.\n\n```ts\nimport { createBarChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createBarChart(host, {\n a11y: { ariaLabel: 'Tasks by status' },\n series: [{ data: [{ key: 'Open', value: 12 }, { key: 'Done', value: 8 }], name: 'Tasks' }],\n});\nchart.dispose();\n```\n\nFor stacked variants, negative values are treated as zero and produce a development warning.\n\n### `createPieChart()`\n\n```ts\nfunction createPieChart(\n container: HTMLElement,\n config: PieChartConfig,\n): ChartHandle<PieSliceConfig[]>;\n```\n\nRenders pie, donut, or semi-circle slices. The default variant is `pie`; `update()` replaces the slice array. The default inner radius is zero for pies and 55% of the outer radius for donut and semi variants. Pie slices default to no padding or rounded corners; donut and semi slices default to 8px padding and corner radius.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `PieChartConfig` | Slices, shape, geometry, legend, tooltip, and interaction settings |\n\n**Returns:** `ChartHandle<PieSliceConfig[]>`.\n\n```ts\nimport { createPieChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:320px;height:240px';\ndocument.body.append(host);\n\nconst chart = createPieChart(host, {\n a11y: { ariaLabel: 'Orders by channel' },\n data: [{ label: 'Direct', value: 72 }, { label: 'Referral', value: 28 }],\n variant: 'donut',\n});\nchart.dispose();\n```\n\n### `createRadarChart()`\n\n```ts\nfunction createRadarChart(\n container: HTMLElement,\n config: RadarChartConfig,\n): ChartHandle<RadarSeriesConfig[]>;\n```\n\nRenders one shape per series across the configured axes. `update()` replaces series data but not axes or other configuration. Fewer than three axes warn in development and draw no chart. Tooltip and legend are disabled unless configured.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `RadarChartConfig` | Fixed axes, series, scale, grid, appearance, and interaction settings |\n\n**Returns:** `ChartHandle<RadarSeriesConfig[]>`.\n\n```ts\nimport { createRadarChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:400px;height:320px';\ndocument.body.append(host);\n\nconst chart = createRadarChart(host, {\n a11y: { ariaLabel: 'Hero stats' },\n axes: [{ key: 'str', label: 'Strength' }, { key: 'agi', label: 'Agility' }, { key: 'end', label: 'Endurance' }],\n domain: [0, 10],\n series: [{ data: [{ key: 'str', value: 8 }, { key: 'agi', value: 6 }, { key: 'end', value: 9 }], name: 'Adam' }],\n});\nchart.dispose();\n```\n\nRadar defaults are a solid fill, linear curves, visible points, a four-level polygon grid with bands, no grid labels, and a first-axis angle of zero degrees. Grid labels render only when all axes share a domain.\n\n### `createHeatmapChart()`\n\n```ts\nfunction createHeatmapChart(\n container: HTMLElement,\n config: HeatmapChartConfig,\n): ChartHandle<HeatmapData>;\n```\n\nRenders one colored cell per row/column pair: rows down the left, columns across the top. The color ramp is continuous by default; `bins` turns it into discrete steps and shows a swatch legend. Cells show their formatted value when they are large enough, choosing dark or light text against the fill. `rowSummary` adds a right-hand column of pre-formatted per-row texts (e.g. a win rate the cells alone cannot show). `update()` replaces cells and may replace `rows`, `columns`, `domain`, and `rowSummary`; other configuration is fixed at creation.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `HeatmapChartConfig` | Rows, columns, cells, ramp, bins, appearance, and interaction settings |\n\n**Returns:** `ChartHandle<HeatmapData>`.\n\n```ts\nimport { createHeatmapChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:400px;height:320px';\ndocument.body.append(host);\n\nconst chart = createHeatmapChart(host, {\n a11y: { ariaLabel: 'Win rate by hero and region' },\n cells: [\n { column: 'Forest', row: 'Aria', value: 90 },\n { column: 'Swamp', row: 'Aria', value: 40 },\n { column: 'Forest', row: 'Brom', value: 20 },\n { column: 'Swamp', row: 'Brom', value: 75 },\n ],\n columns: ['Forest', 'Swamp'],\n rows: ['Aria', 'Brom'],\n});\nchart.dispose();\n```\n\nHeatmap defaults are a light-to-deep blue ramp over the data's `[min, max]` domain, a 2 px cell gap, in-cell values, and no legend. Column headers rotate -45° when they do not fit their column. Keyboard: arrows move the active cell, Enter/Space fires `onCellClick`, Escape deactivates; the tooltip (or `.prism-live` without one) describes the active cell.\n\n### `createSparkline()`\n\n```ts\nfunction createSparkline(\n container: HTMLElement,\n config: SparklineConfig,\n): ChartHandle<number[] | StackSegment[]>;\n```\n\nRenders compact data without axes, chart margins, or a legend. `update()` replaces the number or stack-segment array. The default variant is `line`; only line and area variants draw the endpoint marker by default.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `container` | `HTMLElement` | Host that receives the chart SVG |\n| `config` | `SparklineConfig` | Values, variant, appearance, accessibility, and pointer callbacks |\n\n**Returns:** `ChartHandle<number[] | StackSegment[]>`.\n\n```ts\nimport { createSparkline } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:240px;height:48px';\ndocument.body.append(host);\n\nconst chart = createSparkline(host, { data: [2, 5, 3, 8], variant: 'area' });\nchart.update([2, 5, 3, 8, 13]);\nchart.dispose();\n```\n\n`StackSegment[]` is for `variant: 'stack'`. Pointer callbacks are available for numeric line, area, and bar sparklines; sparklines do not support keyboard navigation.\n\n## Scale Factories\n\n### `linearScale()`\n\n```ts\nfunction linearScale(config: LinearScaleConfig): Scale<number>;\n```\n\nMaps a numeric domain to a numeric range. `nice` defaults to `true`; `clamp` defaults to `false` and applies to `map()`, not `invert()`.\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `config.domain` | `[number, number]` | Required | Input extent |\n| `config.range` | `[number, number]` | Required | Output extent |\n| `config.clamp` | `boolean` | `false` | Clamp mapped values to the range |\n| `config.nice` | `boolean` | `true` | Expand the domain to rounded boundaries |\n\n**Returns:** `Scale<number>`, with `map`, `invert`, `ticks`, `domain`, and `range`.\n\n```ts\nimport { linearScale } from '@vielzeug/prism';\n\nconst scale = linearScale({ domain: [0, 100], range: [0, 500] });\nscale.map(50); // 250\n```\n\n### `timeScale()`\n\n```ts\nfunction timeScale(config: TimeScaleConfig): Scale<Date>;\n```\n\nMaps date values to a numeric range. `nice` defaults to `true` and rounds the domain to a time interval.\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `config.domain` | `[Date, Date]` | Required | Input date extent |\n| `config.range` | `[number, number]` | Required | Output extent |\n| `config.nice` | `boolean` | `true` | Expand the domain to time-interval boundaries |\n\n**Returns:** `Scale<Date>`, with `map`, `invert`, `ticks`, `domain`, and `range`.\n\n```ts\nimport { timeScale } from '@vielzeug/prism';\n\nconst scale = timeScale({\n domain: [new Date('2026-01-01'), new Date('2026-12-31')],\n range: [0, 500],\n});\nscale.map(new Date('2026-07-02'));\n```\n\n### `bandScale()`\n\n```ts\nfunction bandScale(config: BandScaleConfig): BandScale;\n```\n\nMaps ordered string categories to the start of each band. `padding` defaults to `0.1`; `paddingOuter` defaults to the same value. Mapping an unknown category returns `0` and warns in development.\n\n| Parameter | Type | Default | Description |\n| --- | --- | --- | --- |\n| `config.domain` | `string[]` | Required | Ordered categories |\n| `config.range` | `[number, number]` | Required | Output extent |\n| `config.padding` | `number` | `0.1` | Inner gap as a ratio of bandwidth |\n| `config.paddingOuter` | `number` | `padding` | Outer gap as a ratio of bandwidth |\n\n**Returns:** `BandScale`, with `map`, `ticks`, `bandwidth`, `gap`, `domain`, and `range`.\n\n```ts\nimport { bandScale } from '@vielzeug/prism';\n\nconst scale = bandScale({ domain: ['A', 'B'], range: [0, 200] });\nscale.bandwidth();\n```\n\n## Theme Utilities\n\n`theme.css` supplies the base CSS values and styles. `setTheme()` writes inline custom properties, which take precedence over stylesheet declarations. For other visual controls, set CSS custom properties directly.\n\n### `setTheme()`\n\n```ts\nfunction setTheme(theme: PrismTheme, options?: ThemeScope): void;\n```\n\nSets the provided theme values on `options.scope`, or on `document.documentElement` when omitted. Omitted scalar keys remain unchanged. Providing `colors` replaces all eight palette slots, removing unused slots.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `theme` | `PrismTheme` | Palette and supported axis, grid, text, font, and tooltip properties |\n| `options.scope` | `HTMLElement` | Element that scopes the inline custom properties |\n\n**Returns:** `void`.\n\n```ts\nimport { setTheme } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\ndocument.body.append(host);\nsetTheme({ colors: ['#2563eb', '#16a34a'], textColor: '#111827' }, { scope: host });\n```\n\n### `resetTheme()`\n\n```ts\nfunction resetTheme(options?: ThemeScope): void;\n```\n\nRemoves every custom property managed by `setTheme()` from the given scope or the document root. It does not remove CSS rules or change any other custom properties.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `options` | `ThemeScope` | Optional target; defaults to `document.documentElement` |\n\n**Returns:** `void`.\n\n```ts\nimport { resetTheme, setTheme } from '@vielzeug/prism';\n\nconst scope = document.createElement('div');\nsetTheme({ textColor: '#111827' }, { scope });\nresetTheme({ scope });\n```\n\n### `seriesColor()`\n\n```ts\nfunction seriesColor(index: number, override?: string): string;\n```\n\nReturns `override` when it is provided; otherwise returns a `var(--prism-color-N)` reference. The palette has eight positions and the index wraps around after the eighth.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `index` | `number` | Zero-based palette position |\n| `override` | `string` | Optional color returned instead of a CSS variable |\n\n**Returns:** `string`.\n\n```ts\nimport { seriesColor } from '@vielzeug/prism';\n\nseriesColor(0); // 'var(--prism-color-1)'\nseriesColor(1, '#2563eb'); // '#2563eb'\n```\n\n## Chart Handle\n\n`ChartHandle<TData>` is returned by every chart factory. `update()` synchronously rebuilds the chart from its new data; enabled motion may animate the resulting SVG. `dispose()` is idempotent, removes the chart SVG, observers, tooltip and legend, cancels chart-owned animation, aborts `disposalSignal`, and emits one `dispose` tap event. `update()` after disposal throws `PrismRenderError`.\n\n`tap()` observes `resize` and `dispose` events outside the render path. It returns an unsubscribe function; an optional abort signal removes the subscription. Observer-handler exceptions are swallowed so they cannot affect chart behavior. A tap added after disposal returns a no-op unsubscribe function.\n\n`[Symbol.dispose]()` delegates to `dispose()`, allowing explicit resource management where supported.\n\nAll chart handles expose the same lifecycle members; interactive cartesian, pie, radar, and heatmap charts use a `.prism-live` status region when no tooltip is enabled. A tooltip is itself a polite status region. Sparklines have neither a tooltip nor a live value announcer.\n\n| Method or property | Contract |\n| --- | --- |\n| `el` | The chart's root `SVGSVGElement` |\n| `update(data)` | Replace chart data and render; throws `PrismRenderError` after disposal |\n| `dispose()` | Idempotently release chart-owned DOM, observation, and motion |\n| `disposed` | Whether the handle has been disposed |\n| `disposalSignal` | `AbortSignal` aborted during disposal |\n| `tap(handler, options?)` | Observe resize/dispose events; returns an unsubscribe function |\n| `[Symbol.dispose]()` | Calls `dispose()` |\n\n`PrismEvent` reports a changed SVG content size as `{ type: 'resize', width, height }`; the `dispose` event is emitted once before teardown completes.\n\n## Types\n\nThe following are all types and interfaces exported from `@vielzeug/prism`. Definitions mirror the package entry-point contracts.\n\n### Animation Types\n\n```ts\nexport type EasingFn = (t: number) => number;\nexport type EasingName = 'back-out' | 'ease-in' | 'ease-in-out' | 'ease-out' | 'expo-out' | 'linear';\n\nexport interface TransitionConfig {\n /** Milliseconds per animated lane. Defaults to 300 (420 for a line entrance, 400 for radar). */\n duration?: number;\n easing?: EasingName | ((t: number) => number);\n /** `system` skips animation when reduced motion is preferred. */\n preference?: 'always' | 'never' | 'system';\n /** Per-bar delay on bar charts, per-entering-point delay (x order) on scatter charts, or per-series delay on radar charts. Bar and scatter charts cap the total added delay at about 400 ms. */\n stagger?: number;\n}\n\nexport type TransitionOption = TransitionConfig | boolean;\n```\n\n### Theme Types\n\n```ts\nexport interface PrismTheme {\n axisColor?: string;\n colors?: string[];\n fontFamily?: string;\n gridColor?: string;\n gridOpacity?: number;\n textColor?: string;\n textColorSecondary?: string;\n tooltipBg?: string;\n tooltipBorder?: string;\n tooltipColor?: string;\n}\n\nexport interface ThemeScope {\n readonly scope?: HTMLElement;\n}\n```\n\n`PrismTheme.colors` writes up to eight palette slots. The scalar fields map as follows:\n\n| Field | CSS custom property |\n| --- | --- |\n| `axisColor` | `--prism-axis-color` |\n| `fontFamily` | `--prism-font-family` |\n| `gridColor` | `--prism-grid-color` |\n| `gridOpacity` | `--prism-grid-opacity` |\n| `textColor` | `--prism-text-color` |\n| `textColorSecondary` | `--prism-text-color-secondary` |\n| `tooltipBg` | `--prism-tooltip-bg` |\n| `tooltipBorder` | `--prism-tooltip-border` |\n| `tooltipColor` | `--prism-tooltip-color` |\n\n### Core and Data Types\n\n```ts\nexport interface ChartMargin {\n bottom: number;\n left: number;\n right: number;\n top: number;\n}\n\nexport interface ChartDimensions {\n height: number;\n margin: ChartMargin;\n width: number;\n}\n\nexport type PrismEvent =\n | { readonly height: number; readonly type: 'resize'; readonly width: number }\n | { readonly type: 'dispose' };\n\nexport interface ChartHandle<TData = unknown> {\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n readonly el: SVGSVGElement;\n tap(handler: (event: PrismEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n update(data: TData): void;\n [Symbol.dispose](): void;\n}\n\nexport type ChartA11y =\n | { readonly decorative: true }\n | { readonly ariaLabel: string; readonly decorative?: false };\n\nexport interface Datum<TKey extends Date | number | string = Date | number | string> {\n dash?: string;\n key: TKey;\n meta?: Record<string, unknown>;\n opacity?: number;\n value: number;\n}\n\nexport type ContinuousDatum = Datum<Date | number>;\n\nexport interface Series<TDatum extends Datum = Datum> {\n color?: string;\n data: TDatum[];\n id?: string;\n name: string;\n}\n\nexport interface ChartEvent {\n datum: Datum;\n originalEvent: Event;\n series: Series;\n values?: SeriesValue[];\n}\n\nexport interface SeriesValue {\n datum: Datum | undefined;\n series: Series;\n}\n```\n\n`Datum.dash` and `Datum.opacity` affect rendered marks only on the chart types that support them. On bars, `dash` adds a 1px series-color stroke so the dash is visible against the fill. `Series.id` becomes a `data-series-id` attribute on a series group; characters outside `[A-Za-z0-9_-]` are replaced with `-`. When omitted, the id is based on the series index.\n\n| Chart | `dash` | `opacity` |\n| --- | --- | --- |\n| Bar | Strokes that datum's bar | Applies to that bar |\n| Line | Applies to the segment from this datum to the next | Applies to that segment and its point marker |\n| Area | Applies to the top-line segment from this datum to the next | Applies to that segment |\n| Scatter | Ignored | Applies to that point |\n| Pie, radar, heatmap, sparkline | Ignored | Ignored |\n\nFor line and area charts, the last datum has no following segment, so its `dash` value is ignored. The chart factories group adjacent segments with the same presentation values into path runs.\n\nFor line, area, and bar charts, `ChartEvent.values` lists each series at the active key in series order. Its `datum` is `undefined` when that series has no matching key. `originalEvent` is a pointer event or a keyboard event for keyboard activation. Scatter events identify only the nearest independent point and omit `values`.\n\n### Scale Types\n\n```ts\nexport interface Scale<T> {\n readonly domain: readonly [T, T];\n invert(pixel: number): T;\n map(value: T): number;\n readonly range: readonly [number, number];\n ticks(count?: number): T[];\n}\n\nexport interface BandScale {\n bandwidth(): number;\n readonly domain: readonly string[];\n gap(): number;\n map(value: string): number;\n readonly range: readonly [number, number];\n ticks(count?: number): string[];\n}\n\nexport interface LinearScaleConfig {\n clamp?: boolean;\n domain: [number, number];\n nice?: boolean;\n range: [number, number];\n}\n\nexport interface TimeScaleConfig {\n domain: [Date, Date];\n nice?: boolean;\n range: [number, number];\n}\n\nexport interface BandScaleConfig {\n domain: string[];\n padding?: number;\n paddingOuter?: number;\n range: [number, number];\n}\n```\n\n### Axis and Interaction Types\n\n```ts\nexport type HorizontalAxisPosition = 'bottom' | 'top';\nexport type VerticalAxisPosition = 'left' | 'right';\nexport type AxisPosition = HorizontalAxisPosition | VerticalAxisPosition;\n\nexport interface GridConfig {\n color?: string;\n dash?: string;\n}\n\nexport interface AxisConfig<TPosition extends AxisPosition = AxisPosition> {\n grid?: GridConfig | boolean;\n label?: string;\n position?: TPosition;\n tickCount?: number;\n tickFormat?: (value: Date | number | string) => string;\n tickValues?: ReadonlyArray<Date | number | string>;\n}\n\nexport type XAxisConfig = AxisConfig<HorizontalAxisPosition>;\nexport type YAxisConfig = AxisConfig<VerticalAxisPosition>;\n\nexport interface TooltipConfig {\n offset?: number;\n render?: (datum: Datum, series: Series) => Node | string;\n titleFormat?: (key: Date | number | string) => string;\n valueFormat?: (value: number, datum: Datum, series: Series) => string;\n}\n\nexport interface CrosshairConfig {\n horizontal?: boolean;\n snap?: boolean;\n vertical?: boolean;\n}\n\nexport type LegendPosition = 'bottom' | 'left' | 'right' | 'top';\n\nexport interface LegendConfig {\n position?: LegendPosition;\n}\n```\n\nThe crosshair defaults to a vertical line that snaps to the nearest datum; the horizontal line is hidden by default. With `snap: false`, only the guide follows the pointer—tooltip and callback data still refer to the nearest datum.\n\n`tickValues` renders exactly the given ticks instead of scale-generated ones, and gridlines follow the same list. Values must match the axis's scale kind (numbers on a linear axis, `Date`s on a time axis, category strings on a band axis); an off-scale value warns in development. Without it, ticks come from the scale as before.\n\nNumeric tick labels at a magnitude of 1e5 or above default to compact notation (`123K`, `1.23M`) so long numbers fit the default value-axis margin; pass `tickFormat: String` for raw labels. Supplying a partial `yAxis` (or the value axis of a horizontal bar chart) merges over the default, so `{ tickFormat }` alone keeps the default grid lines.\n\n`titleFormat` and `valueFormat` shape the default comparison tooltip on line, area, and bar charts—the title from the focused key and each row's value—and the same formatting is applied to the tooltip's spoken summary. `render` still replaces the whole tooltip body and takes precedence. Tooltip strings are assigned as text, not parsed as HTML. For a radar chart, `tooltip.render` receives a datum whose `meta` contains the active axis, all series values, and the default announcement text.\n\n### Chart Configuration Types\n\n```ts\nexport interface BaseChartConfig {\n a11y?: ChartA11y;\n legend?: LegendConfig | boolean;\n margin?: Partial<ChartMargin>;\n onClick?: (event: ChartEvent) => void;\n onHover?: (event: ChartEvent | null) => void;\n tooltip?: TooltipConfig | boolean;\n transition?: TransitionOption;\n xAxis?: XAxisConfig | false;\n yAxis?: YAxisConfig | false;\n}\n\nexport interface LineSeriesConfig extends Series<ContinuousDatum> {\n curve?: 'linear' | 'monotone' | 'step';\n pointRadius?: number;\n showPoints?: boolean;\n strokeWidth?: number;\n yAxis?: 'left' | 'right';\n}\n\nexport interface LineChartConfig extends BaseChartConfig {\n crosshair?: CrosshairConfig | boolean;\n rightYAxis?: Omit<YAxisConfig, 'position' | 'grid'>;\n series: LineSeriesConfig[];\n}\n\nexport interface AreaSeriesConfig extends Series<ContinuousDatum> {\n curve?: 'linear' | 'monotone' | 'step';\n fill?: 'gradient' | 'solid';\n fillOpacity?: number;\n showLine?: boolean;\n}\n\nexport interface AreaChartConfig extends BaseChartConfig {\n crosshair?: CrosshairConfig | boolean;\n series: AreaSeriesConfig[];\n}\n\nexport interface ScatterSeriesConfig extends Series<ContinuousDatum> {\n pointRadius?: number;\n}\n\nexport interface ScatterChartConfig extends BaseChartConfig {\n crosshair?: CrosshairConfig | boolean;\n series: ScatterSeriesConfig[];\n}\n\nexport interface BarSeriesConfig extends Series {\n borderRadius?: number;\n}\n\nexport type BarVariant = 'grouped' | 'grouped-horizontal' | 'stacked' | 'stacked-horizontal';\n\nexport interface BarChartConfig extends BaseChartConfig {\n series: BarSeriesConfig[];\n /** Paints each bar's value at its end; stacked variants label the stack total. */\n showValues?: boolean;\n /** Formats the value labels. Defaults to `String`. */\n valueFormat?: (value: number) => string;\n variant?: BarVariant;\n}\n\nexport type PieVariant = 'donut' | 'pie' | 'semi';\n\nexport interface PieSliceConfig {\n color?: string;\n label?: string;\n value: number;\n}\n\nexport interface PieEvent {\n index: number;\n originalEvent: Event;\n slice: PieSliceConfig;\n}\n\nexport interface PieChartConfig extends Omit<BaseChartConfig, 'margin' | 'onClick' | 'onHover' | 'xAxis' | 'yAxis'> {\n cornerRadius?: number;\n data: PieSliceConfig[];\n innerRadius?: number;\n onClick?: (event: PieEvent) => void;\n onHover?: (event: PieEvent | null) => void;\n padPixels?: number;\n variant?: PieVariant;\n}\n\nexport interface RadarAxisConfig {\n format?: (value: number) => string;\n key: string;\n label: string;\n max?: number;\n min?: number;\n}\n\nexport type RadarSeriesConfig = Series<Datum<string>>;\n\nexport interface RadarGridConfig {\n bands?: boolean;\n labels?: boolean;\n levels?: number;\n shape?: 'circle' | 'polygon';\n}\n\nexport interface RadarAxisValue {\n datum: Datum<string> | undefined;\n series: RadarSeriesConfig;\n}\n\nexport interface RadarEvent {\n axis: RadarAxisConfig;\n index: number;\n originalEvent: Event;\n values: RadarAxisValue[];\n}\n\nexport interface RadarChartConfig extends Omit<BaseChartConfig, 'margin' | 'onClick' | 'onHover' | 'xAxis' | 'yAxis'> {\n axes: RadarAxisConfig[];\n curve?: 'linear' | 'rounded';\n domain?: [number, number];\n fill?: 'gradient' | 'none' | 'solid';\n grid?: RadarGridConfig | boolean;\n onClick?: (event: RadarEvent) => void;\n onHover?: (event: RadarEvent | null) => void;\n series: RadarSeriesConfig[];\n showPoints?: boolean;\n showValues?: boolean;\n startAngle?: number;\n}\n\nexport interface HeatmapCell {\n column: string;\n row: string;\n value: number | null;\n}\n\nexport interface HeatmapEvent {\n cell: HeatmapCell | undefined;\n column: string;\n originalEvent: Event;\n row: string;\n}\n\nexport interface HeatmapData {\n cells: HeatmapCell[];\n columns?: string[];\n domain?: [number, number];\n rows?: string[];\n rowSummary?: HeatmapChartConfig['rowSummary'];\n}\n\nexport interface HeatmapChartConfig\n extends Omit<BaseChartConfig, 'legend' | 'onClick' | 'onHover' | 'transition' | 'xAxis' | 'yAxis'> {\n binLabel?: (from: number | null, to: number | null) => string;\n bins?: number[];\n cells: HeatmapCell[];\n colorRange?: string[];\n columns: string[];\n domain?: [number, number];\n emptyColor?: string;\n emptyLabel?: string;\n gap?: number;\n legend?: boolean;\n onCellClick?: (event: HeatmapEvent) => void;\n onCellHover?: (event: HeatmapEvent | null) => void;\n rows: string[];\n rowSummary?: { header?: string; values: (string | null)[] };\n showValues?: boolean;\n valueFormat?: (value: number) => string;\n}\n\nexport type SparklineVariant = 'area' | 'bar' | 'line' | 'stack';\n\nexport interface StackSegment {\n color?: string;\n label?: string;\n value: number;\n}\n\nexport interface SparklineEvent {\n index: number;\n originalEvent: Event;\n value: number;\n}\n\nexport interface SparklineConfig {\n a11y?: ChartA11y;\n color?: string;\n cornerRadius?: number;\n curve?: 'linear' | 'monotone' | 'step';\n data: number[] | StackSegment[];\n fillOpacity?: number;\n onClick?: (event: SparklineEvent) => void;\n onHover?: (event: SparklineEvent | null) => void;\n padPixels?: number;\n showEndPoint?: boolean;\n strokeWidth?: number;\n transition?: TransitionOption;\n variant?: SparklineVariant;\n}\n```\n\n## Errors\n\n### `PrismError`\n\n```ts\nclass PrismError extends Error {\n constructor(message: string, opts?: ErrorOptions);\n}\n```\n\nBase class for Prism-originated errors. Its `name` is set to the concrete error class name, and it supports the standard `ErrorOptions` including `cause`.\n\n### `PrismRenderError`\n\n```ts\nclass PrismRenderError extends PrismError {}\n```\n\nThrown when a chart cannot be rendered because its container is not a DOM element, when initial rendering fails (the original error is available as `cause`), or when `update()` is called after disposal. Empty or malformed chart data may instead produce a development warning or an empty rendering, depending on the chart.\n",
6
- "usage": "---\ntitle: 'Prism: Usage Guide'\ndescription: Create responsive SVG charts, update their data, handle interaction, and theme @vielzeug/prism.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a host element with non-zero dimensions, then pass it to a chart factory. This example creates the host in JavaScript so the snippet can run as written.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\nimport '@vielzeug/prism/theme.css';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createLineChart(container, {\n a11y: { ariaLabel: 'Revenue by month' },\n series: [\n {\n name: 'Revenue',\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 16 },\n ],\n },\n ],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nEach factory appends an SVG to its container and returns a `ChartHandle`. Prism observes the container with `ResizeObserver`; if it has zero dimensions on creation, Prism uses a temporary `600 × 300` size and warns in development. Give the host its layout before creating the chart.\n\n## Updating Data\n\nKeep application state outside Prism and pass the next complete data value to `update()`. The update method replaces chart data synchronously; the default transition may animate the resulting SVG.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nlet series = [{ data: [{ key: 1, value: 10 }], name: 'Live' }];\nconst chart = createLineChart(container, { series });\n\nseries = [{ data: [{ key: 1, value: 10 }, { key: 2, value: 20 }], name: 'Live' }];\nchart.update(series);\nchart.dispose();\n```\n\n`update()` accepts the type shown by that factory's handle: a complete series array for line, area, scatter, bar, and radar charts; a slice array for pie charts; a `HeatmapData` object for heatmap charts; and a number or stack-segment array for sparklines. Chart options such as axes, variant, callbacks, and accessibility are set at creation. For example, radar axes and a pie chart's variant do not change through `update()`.\n\n## Line Charts\n\nLine and area series use number or `Date` keys. All series in one chart must use the same key kind. Use `curve` to choose interpolation, and enable markers with `showPoints`.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createLineChart(container, {\n a11y: { ariaLabel: 'Revenue and expenses' },\n series: [\n {\n curve: 'monotone',\n data: [{ key: 1, value: 100 }, { key: 2, value: 150 }, { key: 3, value: 130 }],\n name: 'Revenue',\n showPoints: true,\n },\n {\n color: '#c2410c',\n data: [{ key: 1, value: 70 }, { key: 2, value: 95 }, { key: 3, value: 110 }],\n name: 'Expenses',\n },\n ],\n tooltip: true,\n xAxis: { label: 'Month' },\n yAxis: { grid: true, label: 'Dollars' },\n});\n\nchart.dispose();\n```\n\n### Independent Value Axes\n\nAssign a line series to the right axis when it uses a different value range. Both axes share the x-scale; `rightYAxis` configures the second axis and cannot configure its position or grid.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createLineChart(container, {\n a11y: { ariaLabel: 'Health and damage by level' },\n rightYAxis: { label: 'Damage' },\n series: [\n { data: [{ key: 1, value: 30 }, { key: 2, value: 20 }], name: 'Health' },\n { data: [{ key: 1, value: 100 }, { key: 2, value: 300 }], name: 'Damage', yAxis: 'right' },\n ],\n yAxis: { grid: true, label: 'Health' },\n});\n\nchart.dispose();\n```\n\nWhen any series uses the right axis, the primary value axis is placed on the left. Grid lines belong to the primary `yAxis`. The independent numeric scales mean that curve crossings do not imply equal values.\n\n### Time-Based X Axis\n\nUse `Date` values for every key when plotting time. Line and area charts use a time scale whose domain fits the data instead of rounding outward to tick boundaries.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst formatter = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' });\nconst chart = createLineChart(container, {\n a11y: { ariaLabel: 'Signups over time' },\n series: [{\n data: [\n { key: new Date('2026-09-01'), value: 50 },\n { key: new Date('2026-09-08'), value: 80 },\n { key: new Date('2026-09-15'), value: 120 },\n ],\n name: 'Signups',\n }],\n xAxis: { tickFormat: (value) => value instanceof Date ? formatter.format(value) : String(value) },\n});\n\nchart.dispose();\n```\n\n`tickFormat` formats axis labels only. Tooltip content and accessibility announcements use the underlying keys and values. To render an exact set of ticks instead of scale-generated ones, pass `tickValues`; gridlines follow the same list. Values must match the axis's scale kind, and an off-scale value warns in development:\n\n```ts\n// Force a tick on the last month, which nice-stepped ticks can skip.\nconst chart = createLineChart(container, {\n series: [{ data: [{ key: 0, value: 90 }, { key: 11, value: 128 }], name: 'Costs' }],\n xAxis: { tickValues: [0, 6, 11] },\n});\n```\n\nNumeric tick labels at a magnitude of `1e5` or above default to compact notation (`123K`, `1.23M`) so long numbers fit the default value-axis margin; pass `tickFormat: String` for raw labels. Supplying a partial value-axis config merges over its default, so `yAxis: { tickFormat }` alone keeps the default grid lines.\n\n## Bar Charts\n\nBar chart keys are string categories. The default `grouped` variant puts series side by side; the horizontal variants put categories on the y-axis and values on the x-axis.\n\n```ts\nimport { createBarChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createBarChart(container, {\n a11y: { ariaLabel: 'Sales by quarter' },\n series: [{\n data: [{ key: 'Q1', value: 200 }, { key: 'Q2', value: 350 }, { key: 'Q3', value: 280 }],\n name: 'Sales',\n }],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nChoose `grouped`, `stacked`, `grouped-horizontal`, or `stacked-horizontal` in `variant`. Negative values in a stacked chart are clamped to zero and produce a development warning. Bars in stacked variants remain square; `borderRadius` applies to grouped bars.\n\nAxes render by default, with grid lines on the value axis. For horizontal variants, `xAxis` is the value axis and `yAxis` is the category axis. Configure `grid` on the value axis.\n\nSet `showValues: true` to paint each bar's value at its end so the chart reads without hover; `valueFormat` shapes the label text. Stacked variants label the stack total at the top of the stack, and labels that would overflow the plot flip inside the bar with a contrasting fill. The labels are decorative (`aria-hidden`) — the accessible read stays with the chart's data table.\n\n## Area Charts\n\nArea series use a gradient fill by default. Set `fill: 'solid'` for a flat fill, or set `fillOpacity` to override the theme opacity for that series.\n\n```ts\nimport { createAreaChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createAreaChart(container, {\n a11y: { ariaLabel: 'Visitors over time' },\n crosshair: true,\n series: [{\n curve: 'monotone',\n data: [{ key: 1, value: 12 }, { key: 2, value: 18 }, { key: 3, value: 15 }],\n fill: 'solid',\n fillOpacity: 0.2,\n name: 'Visitors',\n showLine: true,\n }],\n});\n\nchart.dispose();\n```\n\n## Scatter Charts\n\nScatter charts treat every datum as an independent point: `key` is the numeric or `Date` x-value and `value` is the y-value. The scales fit the observed values rather than forcing the y-domain to include zero. Set `pointRadius` on a series to change its marker size (default `3`).\n\n```ts\nimport { createScatterChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createScatterChart(container, {\n a11y: { ariaLabel: 'Height against weight' },\n series: [{\n data: [{ key: 160, value: 55 }, { key: 175, value: 70 }, { key: 190, value: 88 }],\n name: 'Cohort A',\n pointRadius: 5,\n }],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nScatter callbacks identify the nearest point in two dimensions and do not provide cross-series `values`, because points do not share a category key. Keyboard navigation visits every point across series in x order. With a tooltip, `titleFormat` formats the x-value and `valueFormat` the y-value, in both the visible row and the spoken summary.\n\n## Forecast and Emphasis Styling\n\nAdd `dash` or `opacity` to individual `Datum` objects when one part of a series needs distinct styling. These fields do not change interaction, tooltip, or accessibility behavior.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst values = [10, 13, 15, 18];\nconst chart = createLineChart(container, {\n series: [{\n data: values.map((value, index) => ({\n key: index + 1,\n value,\n ...(index >= 2 ? { dash: '5 5', opacity: 0.4 } : {}),\n })),\n name: 'Forecast',\n showPoints: true,\n }],\n});\n\nchart.dispose();\n```\n\nFor line and area charts, each datum's `dash` styles the segment from that datum to the next; the final datum has no outgoing segment. Line opacity also fades the datum's marker. Area styling affects the top line, not the fill. Bar opacity and dashes apply to the individual bar; scatter opacity applies to its point and scatter dashes are ignored. Pie, radar, and sparkline ignore these two fields.\n\n## Pie, Donut, and Semi Charts\n\nUse `createPieChart()` for all three shapes. `variant` is fixed when the chart is created; `update()` replaces the slice data.\n\n```ts\nimport { createPieChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:320px;height:240px';\ndocument.body.append(container);\n\nconst chart = createPieChart(container, {\n a11y: { ariaLabel: 'Orders by channel' },\n data: [\n { label: 'Direct', value: 42 },\n { label: 'Organic', value: 28 },\n { label: 'Referral', value: 18 },\n { label: 'Social', value: 12 },\n ],\n legend: true,\n tooltip: true,\n variant: 'donut',\n});\n\nchart.dispose();\n```\n\nThe `pie` default has no inner radius. `donut` and `semi` default to an inner radius of 55% of the outer radius. `innerRadius` overrides that size in pixels. Slice labels are omitted when they do not fit; keep a tooltip or legend when small slices need identifying.\n\n## Radar Charts\n\nDefine at least three axes when creating a radar chart. Series data uses string keys that match `RadarAxisConfig.key`; `update()` replaces series but keeps axes and other configuration fixed.\n\n```ts\nimport { createRadarChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:400px;height:320px';\ndocument.body.append(container);\n\nconst axes = [\n { key: 'strength', label: 'Strength' },\n { key: 'agility', label: 'Agility' },\n { key: 'endurance', label: 'Endurance' },\n];\nconst chart = createRadarChart(container, {\n a11y: { ariaLabel: 'Hero stats' },\n axes,\n domain: [0, 10],\n series: [{\n data: [{ key: 'strength', value: 8 }, { key: 'agility', value: 6 }, { key: 'endurance', value: 9 }],\n name: 'Adam',\n }],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nThe default shared domain starts at zero and uses a nice maximum; set `domain` to compare series on a fixed range. Per-axis `min` and `max` override the chart domain for that axis. Grid rings, bands, curves, fills, vertex dots, and values are configurable; see `RadarChartConfig` and `RadarGridConfig` in the API reference.\n\n## Heatmaps\n\nDefine `rows`, `columns`, and `cells`; each cell names its `row` and `column` labels. A cell with `value: null` (or no matching cell at all) renders in `emptyColor` without in-cell text. `rowSummary` adds a right-hand column of pre-formatted per-row texts (aligned with `rows`, with an optional header) — useful for a per-row rate the cells cannot show. `update()` replaces cells and may replace `rows`, `columns`, `domain`, and `rowSummary`.\n\n```ts\nimport { createHeatmapChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:400px;height:320px';\ndocument.body.append(container);\n\nconst chart = createHeatmapChart(container, {\n a11y: { ariaLabel: 'Win rate by hero and region' },\n bins: [50, 75],\n cells: [\n { column: 'Forest', row: 'Aria', value: 90 },\n { column: 'Swamp', row: 'Aria', value: 40 },\n { column: 'Forest', row: 'Brom', value: 20 },\n { column: 'Swamp', row: 'Brom', value: 75 },\n ],\n columns: ['Forest', 'Swamp'],\n rows: ['Aria', 'Brom'],\n tooltip: true,\n valueFormat: (value) => `${value}%`,\n});\n\nchart.dispose();\n```\n\nWithout `bins` the ramp is continuous over the data's `[min, max]` domain (or an explicit `domain`); with `bins` each value maps to a discrete step and a swatch legend appears. `colorRange` sets the ramp low → high, `valueFormat` shapes in-cell values, tooltip values, and bin labels, and `showValues: false` hides the in-cell text.\n\n## Sparklines\n\nSparklines omit axes, margins, and legends. Choose `line`, `area`, or `bar` for numeric arrays, and `stack` for an array of `StackSegment` objects.\n\n```ts\nimport { createSparkline } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:240px;height:48px';\ndocument.body.append(container);\n\nconst chart = createSparkline(container, {\n a11y: { ariaLabel: 'Weekly orders' },\n data: [10, 14, 12, 18, 22],\n variant: 'area',\n});\n\nchart.update([10, 14, 12, 18, 22, 25]);\nchart.dispose();\n```\n\nFor a stacked sparkline, each segment can set `value`, `color`, and `label`. Sparklines do not offer keyboard navigation, a tooltip, or a live value announcer. Add a visible value or summary when the trend conveys important information.\n\n## Interaction\n\nSet `tooltip: true` to show Prism's default tooltip, or pass a `TooltipConfig` to format its content. `titleFormat` and `valueFormat` shape the default comparison tooltip on line, area, and bar charts—the title from the focused key and each row's value—without rebuilding its markup, and the same formatting is applied to the tooltip's spoken summary. A `render` callback receives the active `Datum` and `Series` and replaces the whole body, taking precedence over the formatters. Returned strings are inserted as text; return a DOM `Node` for structured content.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst compact = new Intl.NumberFormat(undefined, { notation: 'compact' });\nconst months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n\nconst chart = createLineChart(container, {\n series: [{ data: [{ key: 11, value: 128000 }], name: 'EC2' }],\n tooltip: {\n titleFormat: (key) => months[Number(key)] ?? String(key),\n valueFormat: (value) => compact.format(value),\n },\n});\n\nchart.dispose();\n```\n\n`onHover` receives an event while a point, category, slice, or radar axis is active, then `null` when the interaction clears. `onClick` receives the selected event. For line, area, and bar charts, `ChartEvent.values` contains each series' datum at that shared key; a missing datum is `undefined`. Scatter events are `ChartEvent` objects without `values`; pie, radar, and sparkline use their chart-specific event types.\n\n`crosshair` is available on line, area, and scatter charts. By default it shows a vertical guide and snaps the line to the nearest datum. Set `horizontal: true` to add a horizontal guide or `vertical: false` to hide the vertical one. Set `snap: false` to make the guide follow the pointer; tooltip and callback data still come from the nearest datum.\n\n### Keyboard and Announcements\n\nSet `a11y: { ariaLabel }` on an informative chart. Prism gives the SVG `role=\"img\"`, an accessible name, and keyboard focus. Arrow keys move among chart data; `Home` and `End` move to the first and last item where supported; `Enter` or `Space` activates an item when an `onClick` callback is configured; `Escape` clears the active selection. Sparklines do not support keyboard navigation.\n\nWhen `a11y` is omitted, or set to `{ decorative: true }`, the SVG is marked `aria-hidden=\"true\"` and does not enter the keyboard tab order. Only mark charts decorative when their information is available elsewhere. Interactive cartesian, pie, radar, and heatmap charts use the tooltip as a polite live region when enabled; otherwise they announce active values in `.prism-live`. Sparklines have no live value announcements, so provide an equivalent visible value or summary.\n\n## Animation\n\nChart motion is enabled by default, with a 300 ms duration per animated lane (radar uses 400 ms). Pass `transition: false` to render updates synchronously, or pass `true` to state the default explicitly.\n\n```ts\nimport { createBarChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createBarChart(container, {\n series: [{ data: [{ key: 'A', value: 4 }, { key: 'B', value: 7 }], name: 'Count' }],\n transition: { duration: 500, easing: 'ease-out', preference: 'system' },\n});\n\nchart.dispose();\n```\n\nColumns grow together from their baseline by default over 300 ms. Lines trace their final curve over 420 ms, uncovering point markers along the way without fading or shifting the series. Line data updates retain the default 300 ms geometry transition; an explicit `duration` overrides both timings.\n\n`preference: 'system'` (the default) disables JavaScript motion when the user prefers reduced motion. Use `'always'` to ignore that preference or `'never'` to disable motion. `stagger` sets an optional delay between bars, entering scatter points (in x order), or radar series; bar and scatter charts cap the effective delay so many marks do not add unbounded time. Named easing values are `ease-in`, `ease-in-out`, `ease-out`, `back-out`, `expo-out`, and `linear`; you can also provide an easing function.\n\n## Responsive Layout and Lifecycle\n\nSize the host with CSS. Prism observes its content box and updates the SVG dimensions and chart layout when it changes. Bar, line, and scatter resize redraws apply immediately rather than animating between layout sizes; entrance animations and explicit data-update transitions remain enabled. The legend is rendered as container chrome outside the SVG and included in resize calculations.\n\nCall `dispose()` when the view no longer owns the chart. Disposal is idempotent: it removes the SVG, tooltip, legend, and resize observation, stops in-flight chart motion, and aborts `disposalSignal`. `chart[Symbol.dispose]()` calls the same cleanup method.\n\nUse `tap()` when external code needs resize or dispose notifications rather than observing the chart DOM itself:\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createLineChart(container, {\n series: [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }],\n});\nconst stopObserving = chart.tap((event) => {\n if (event.type === 'resize') console.log(event.width, event.height);\n if (event.type === 'dispose') console.log('chart disposed');\n});\n\nstopObserving();\nchart.dispose();\n```\n\n`tap()` returns an unsubscribe function and accepts `{ signal }` for automatic unsubscription. A listener exception is swallowed by the observer so it cannot interrupt chart rendering.\n\n## Theming\n\nImport the stylesheet once to load Prism's structural styles, default tokens, dark-mode rules, and reduced-motion CSS. You can override any CSS custom property on an ancestor of the chart.\n\n```css\n.dashboard-chart {\n --prism-color-1: #2563eb;\n --prism-color-2: #16a34a;\n --prism-grid-color: #94a3b8;\n --prism-line-width: 3;\n --prism-text-color: #172033;\n --prism-tooltip-bg: #111827;\n}\n```\n\n`setTheme()` provides a typed subset of theme properties. A scoped call writes inline values on the provided element; omit `scope` to write them to `document.documentElement`. Inline values override stylesheet tokens until reset.\n\n```ts\nimport { resetTheme, setTheme } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nsetTheme({ colors: ['#2563eb', '#16a34a'], fontFamily: 'Inter, sans-serif' }, { scope: container });\n// Call resetTheme({ scope: container }) when this scoped theme is no longer needed.\nresetTheme({ scope: container });\n```\n\n### Available Tokens\n\nThe stylesheet defines defaults for the following token groups. Pie-label color and size are read with fallback values by the renderer but are not declared in the base token block. Set a token on the chart container or an ancestor to scope an override.\n\n| Group | CSS custom properties |\n| --- | --- |\n| Palette and surfaces | `--prism-color-1` through `--prism-color-8`, `--prism-bg`, `--prism-border-radius` |\n| Axes and grid | `--prism-axis-color`, `--prism-axis-width`, `--prism-grid-color`, `--prism-grid-width`, `--prism-grid-dash`, `--prism-grid-opacity` |\n| Text | `--prism-font-family`, `--prism-font-size`, `--prism-font-size-label`, `--prism-font-size-title`, `--prism-font-weight`, `--prism-font-weight-title`, `--prism-text-color`, `--prism-text-color-secondary` |\n| Tooltip | `--prism-tooltip-bg`, `--prism-tooltip-color`, `--prism-tooltip-border`, `--prism-tooltip-radius`, `--prism-tooltip-padding`, `--prism-tooltip-shadow`, `--prism-tooltip-font-size` |\n| Crosshair | `--prism-crosshair-color`, `--prism-crosshair-width`, `--prism-crosshair-dash`, `--prism-crosshair-opacity` |\n| Points | `--prism-point-radius`, `--prism-point-radius-hover`, `--prism-point-stroke-width`, `--prism-point-stroke` |\n| Bars | `--prism-bar-radius`, `--prism-bar-hover-opacity`, `--prism-bar-gap`, `--prism-bar-band-fill`, `--prism-bar-band-opacity`, `--prism-bar-dim-opacity` |\n| Areas and lines | `--prism-area-opacity`, `--prism-area-stroke-width`, `--prism-area-gradient-start`, `--prism-area-gradient-end`, `--prism-line-width`, `--prism-line-width-hover` |\n| Interaction | `--prism-dim-opacity`, `--prism-active-point-radius`, `--prism-point-ring`, `--prism-focus-color` |\n| Sparklines | `--prism-spark-fill-opacity`, `--prism-spark-gradient-start`, `--prism-spark-gradient-end` |\n| Radar | `--prism-radar-grid-color`, `--prism-radar-grid-opacity`, `--prism-radar-band-fill`, `--prism-radar-band-opacity`, `--prism-radar-spoke-color`, `--prism-radar-fill-opacity`, `--prism-radar-stroke-width`, `--prism-radar-point-radius`, `--prism-radar-point-radius-active`, `--prism-radar-label-color`, `--prism-radar-label-size`, `--prism-radar-value-color`, `--prism-radar-value-size`, `--prism-radar-dim-opacity` |\n| Heatmap | `--prism-heatmap-label-color`, `--prism-heatmap-label-size`, `--prism-heatmap-value-size`, `--prism-heatmap-highlight-color` |\n| Motion | `--prism-duration-fast`, `--prism-duration`, `--prism-duration-slow`, `--prism-ease`, `--prism-ease-spring`, `--prism-transition`, `--prism-transition-fast` |\n| Legend and pie labels | `--prism-legend-gap`, `--prism-legend-dot-size`, `--prism-legend-font-size`, `--prism-pie-label-color`, `--prism-pie-label-size` |\n\nThe default palette and structural colors adapt to `prefers-color-scheme: dark`, `html.dark`, or `[data-prism-theme='dark']` on an ancestor. Scoped inline values set with `setTheme()` take precedence in either mode. The stylesheet also provides `prefers-reduced-motion` overrides for CSS transitions.\n\n## Errors and Diagnostics\n\nPrism throws `PrismRenderError` when the container is not a DOM element, when initial rendering fails, or when `update()` is called on a disposed chart. A wrapped initial-render error exposes its cause. Use `PrismError` to catch all Prism-originated errors.\n\nEmpty or malformed data is not always an exception. Depending on the chart, Prism may draw nothing or emit a development-only `console.warn`; examples include an unknown `bandScale` category, negative values in stacked bars, and radar axes with fewer than three entries or duplicate keys. Correct the input rather than relying on a warning for validation.\n\n## Framework Integration\n\nPrism has no React, Vue, or Svelte adapter. In any framework, give Prism a host element after it is mounted, call `update()` when data changes, and call `dispose()` before the host is removed. This plain DOM pattern shows the ownership boundary without depending on a framework-specific lifecycle API:\n\n```ts\nimport { createLineChart, type LineSeriesConfig } from '@vielzeug/prism';\n\nfunction mountChart(host: HTMLElement, series: LineSeriesConfig[]) {\n const chart = createLineChart(host, { a11y: { ariaLabel: 'Revenue' }, series });\n\n return {\n update(next: LineSeriesConfig[]) {\n chart.update(next);\n },\n dispose() {\n chart.dispose();\n },\n };\n}\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\nconst view = mountChart(host, [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }]);\nview.update([{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }], name: 'Revenue' }]);\nview.dispose();\n```\n\nThe application owns its state and host; Prism owns only the SVG, observer, and optional overlay elements it creates.\n\n## Working with Other Vielzeug Libraries\n\nUse Refine components for dashboard controls and keep Prism in a sized host element. Map Refine theme tokens to Prism CSS custom properties or call `setTheme()` with a scoped element; Prism does not require a Refine adapter. Prism uses Orbit internally to place tooltips, so chart users do not need to create Orbit floating elements for the built-in tooltip.\n\n## Best Practices\n\n- Give the host a non-zero size before creating a chart.\n- Set `a11y.ariaLabel` when a chart communicates information.\n- Use `Date` or number keys consistently across each line or area chart.\n- Pass the complete new data value to `update()`.\n- Keep chart options fixed after creation; recreate the chart to change them.\n- Dispose the handle when its host leaves the owning view.\n- Use `transition: false` for deterministic synchronous rendering.\n- Prefer CSS custom properties for reusable theme overrides.\n",
4
+ "index": "---\ntitle: 'Prism: Responsive SVG data visualization'\ndescription: 'Responsive SVG charts with explicit updates: line, bar, area, scatter, pie, radar, heatmap, sparkline'\npackage: prism\ncategory: ui\nkeywords:\n [\n chart,\n svg,\n visualization,\n responsive,\n line-chart,\n bar-chart,\n area-chart,\n scatter-chart,\n pie-chart,\n radar-chart,\n heatmap-chart,\n sparkline,\n accessibility,\n typescript,\n ]\nrelated: [refine, orbit]\nexports: [createLineChart, createBarChart, createPieChart, ChartHandle]\nenvironments: [browser]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"prism\" />\n\n## Why Prism?\n\nPrism renders charts as SVG in a host element and keeps application state outside the chart. You can replace chart data explicitly, respond to container resizing, and style the output with CSS custom properties.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\n// Before: replace a hand-drawn SVG each time values change.\nfunction drawLine(values: number[]) {\n const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');\n const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');\n const low = Math.min(...values);\n const span = Math.max(...values) - low || 1;\n const points = values.map((value, index) => {\n const x = (index / Math.max(1, values.length - 1)) * 640;\n const y = 320 - ((value - low) / span) * 320;\n return `${index === 0 ? 'M' : 'L'}${x},${y}`;\n });\n svg.setAttribute('viewBox', '0 0 640 320');\n path.setAttribute('d', points.join(' '));\n path.setAttribute('fill', 'none');\n path.setAttribute('stroke', 'currentColor');\n svg.append(path);\n host.replaceChildren(svg);\n}\ndrawLine([10, 25, 18]);\ndrawLine([10, 25, 18, 32]);\n\n// After: keep the chart instance and replace its series data.\nconst chart = createLineChart(host, {\n a11y: { ariaLabel: 'Revenue by month' },\n series: [\n {\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 25 },\n { key: 3, value: 18 },\n ],\n name: 'Revenue',\n },\n ],\n});\nchart.update([\n {\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 25 },\n { key: 3, value: 18 },\n { key: 4, value: 32 },\n ],\n name: 'Revenue',\n },\n]);\nchart.dispose();\n```\n\n| Feature | Prism | Hand-written SVG |\n| ------------------ | ---------------------------------------------------------------- | -------------------------------------------- |\n| Bundle size | <PackageInfo package=\"prism\" type=\"size\" /> | No chart package |\n| Zero dependencies | <ore-icon name=\"x\" size=\"16\"></ore-icon> (uses Orbit) | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Data updates | `ChartHandle.update()` replaces chart data | You implement the update path |\n| Responsive sizing | `ResizeObserver` recalculates the chart layout | You implement the resize behavior |\n| Chart interactions | Built-in pointer and keyboard interactions on supported charts | You implement them |\n| SVG accessibility | Informative charts support an accessible name and keyboard focus | You implement the accessible behavior |\n\n<div class=\"decision-callout\">\n\n**Use Prism when** you need chart factories, responsive SVG output, and an explicit data-update boundary without adopting a charting framework.\n\n**Consider hand-written SVG when** you need a single specialized graphic and prefer to own its scales, resizing, labels, and interactions directly.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/prism\n```\n\n```sh [npm]\nnpm install @vielzeug/prism\n```\n\n```sh [yarn]\nyarn add @vielzeug/prism\n```\n\n:::\n\n## Quick Start\n\nCreate and size a host element before creating the chart. Import `theme.css` once to load Prism's default styles and CSS custom properties.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\nimport '@vielzeug/prism/theme.css';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createLineChart(host, {\n a11y: { ariaLabel: 'Revenue by month' },\n series: [\n {\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 25 },\n { key: 3, value: 18 },\n ],\n name: 'Revenue',\n },\n ],\n tooltip: true,\n});\n\nchart.update([\n {\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 25 },\n { key: 3, value: 18 },\n { key: 4, value: 32 },\n ],\n name: 'Revenue',\n },\n]);\n\nchart.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`createLineChart()` / `createAreaChart()`**: plot continuous numeric or date-keyed series\n- **`createBarChart()`**: compare categories with grouped, stacked, or horizontal bars\n- **`createScatterChart()`**: plot independent numeric `(x, y)` points\n- **`createPieChart()`**: render pie, donut, or semi-circle slices\n- **`createRadarChart()`**: compare series across fixed, labelled axes\n- **`createHeatmapChart()`**: render a row/column matrix of colored cells with a continuous or binned ramp\n- **`createSparkline()`**: render compact line, area, bar, or stacked values without axes\n- **`ChartHandle.update(data)`**: replace chart data without handing state ownership to Prism\n- **`ChartHandle.dispose()` / `Symbol.dispose`**: release the SVG, observer, overlays, and chart-owned animation\n- **`ChartHandle.tap()`**: observe resize and disposal events\n- **`setTheme()` / `resetTheme()`**: set supported palette, typography, axis, grid, and tooltip tokens globally or on one element\n- **CSS custom properties**: style the chart palette, marks, axes, labels, tooltip, legend, and motion\n- **Resize observation**: recalculate SVG layout when the host element changes size\n- **Accessible interaction**: labelled interactive charts expose keyboard navigation and polite value announcements; sparklines are pointer-only without value announcements\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Refine](/refine/): web components and theme tokens for building controls and layouts around chart hosts\n- [Orbit](/orbit/): floating-element positioning used internally by Prism's built-in tooltip\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: 'Prism: API Reference'\ndescription: Complete reference for Prism chart factories, scales, themes, types, and errors.\n---\n\n[[toc]]\n\n## API Overview\n\nAll chart factories, scale utilities, theme helpers, and error classes are synchronous. Charts render into an HTML element and return a `ChartHandle`; they do not return a promise.\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| ---------------------- | ----------------------------------------------------------- | -------------- | ----------------------------------------------------------------------------------------- |\n| `createLineChart()` | Render one or more continuous line series | Sync | `key` values must be numbers or dates |\n| `createAreaChart()` | Render continuous series with filled areas | Sync | Fills use a gradient by default |\n| `createScatterChart()` | Plot independent `(x, y)` points | Sync | `key` is the x-value; the y-domain is fitted to data |\n| `createBarChart()` | Render categorical bars | Sync | Negative values in stacked variants are clamped to zero |\n| `createPieChart()` | Render pie, donut, or semi-circle slices | Sync | `variant` is fixed at creation; `update()` replaces slices |\n| `createRadarChart()` | Compare series across fixed axes | Sync | Requires at least three axes to draw |\n| `createHeatmapChart()` | Render a row/column matrix of colored cells | Sync | Cells whose `row`/`column` match no label are skipped; unknown labels warn in development |\n| `createSparkline()` | Render a compact line, area, bar, or stack | Sync | Has no axes, legend, or keyboard navigation |\n| `linearScale()` | Create a numeric scale | Sync | `nice` defaults to `true`; set `nice: false` to preserve the domain |\n| `timeScale()` | Create a date scale | Sync | `nice` defaults to `true` |\n| `bandScale()` | Create an ordered categorical scale | Sync | Unknown categories map to `0` and warn in development |\n| `setTheme()` | Set Prism CSS custom properties | Sync | Writes inline styles to `document.documentElement` by default |\n| `resetTheme()` | Remove theme overrides set by `setTheme()` | Sync | Only clears the selected target |\n| `seriesColor()` | Resolve a palette CSS value | Sync | Palette positions wrap after eight |\n| `PrismError` | Base class for Prism errors | Sync | Use `instanceof` to catch any Prism error |\n| `PrismRenderError` | Report an invalid chart container or failed chart operation | Sync | Also thrown by `update()` after disposal |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --------------------------- | --------------------------------------------------------------------------------------- |\n| `@vielzeug/prism` | Chart factories, scale and theme functions, error classes, and public types |\n| `@vielzeug/prism/theme.css` | Default chart styles, CSS custom properties, dark-mode values, and reduced-motion rules |\n\n## Chart Factories\n\nEach factory appends an SVG to `container`. A chart with no measurable container size uses a temporary `600 × 300` SVG size and emits a development warning; `ResizeObserver` applies later size changes. Unless `a11y` is informative, the SVG is hidden from the accessibility tree.\n\n### `createLineChart()`\n\n```ts\nfunction createLineChart(container: HTMLElement, config: LineChartConfig): ChartHandle<LineSeriesConfig[]>;\n```\n\nRenders line series and returns a handle whose `update()` argument replaces the complete series array. Axes render by default; the value axis has grid lines by default. Pass `false` to `xAxis` or `yAxis` to suppress that axis.\n\nSeries use linear interpolation by default, omit point markers by default, and use a point radius of 3 when markers are enabled. `yAxis` defaults to `'left'`; crosshair, tooltip, and legend are disabled unless configured.\n\n| Parameter | Type | Description |\n| ----------- | ----------------- | ------------------------------------------------------------- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `LineChartConfig` | Series, axes, interaction, accessibility, and motion settings |\n\n**Returns:** `ChartHandle<LineSeriesConfig[]>`.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createLineChart(host, {\n a11y: { ariaLabel: 'Revenue by month' },\n series: [\n {\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 15 },\n ],\n name: 'Revenue',\n },\n ],\n});\nchart.update([\n {\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 15 },\n { key: 3, value: 18 },\n ],\n name: 'Revenue',\n },\n]);\nchart.dispose();\n```\n\n### `createAreaChart()`\n\n```ts\nfunction createAreaChart(container: HTMLElement, config: AreaChartConfig): ChartHandle<AreaSeriesConfig[]>;\n```\n\nRenders continuous area series with a gradient fill by default. `update()` replaces the complete series array.\n\nArea series use linear interpolation by default and show their top line unless `showLine: false` is set. Axes render by default, with grid lines on the value axis.\n\n| Parameter | Type | Description |\n| ----------- | ----------------- | ------------------------------------------------------------- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `AreaChartConfig` | Series, axes, interaction, accessibility, and motion settings |\n\n**Returns:** `ChartHandle<AreaSeriesConfig[]>`.\n\n```ts\nimport { createAreaChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createAreaChart(host, {\n a11y: { ariaLabel: 'Visitors over time' },\n series: [\n {\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 18 },\n ],\n name: 'Visitors',\n },\n ],\n});\nchart.dispose();\n```\n\n### `createScatterChart()`\n\n```ts\nfunction createScatterChart(container: HTMLElement, config: ScatterChartConfig): ChartHandle<ScatterSeriesConfig[]>;\n```\n\nPlots each datum independently: `key` is the numeric or date x-value and `value` is the y-value. Points are not joined. `update()` replaces the complete series array.\n\nAxes render by default, with grid lines on the value axis. The value scale is fitted to the observed data rather than forced to include zero. Tooltip, crosshair, and legend are disabled unless configured.\n\n| Parameter | Type | Description |\n| ----------- | -------------------- | ------------------------------------------------------------------------- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `ScatterChartConfig` | Independent points, axes, interaction, accessibility, and motion settings |\n\n**Returns:** `ChartHandle<ScatterSeriesConfig[]>`.\n\n```ts\nimport { createScatterChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createScatterChart(host, {\n a11y: { ariaLabel: 'Height against weight' },\n series: [\n {\n data: [\n { key: 160, value: 55 },\n { key: 175, value: 70 },\n ],\n name: 'Cohort A',\n },\n ],\n tooltip: true,\n});\nchart.dispose();\n```\n\nScatter interaction reports the datum and series of the point nearest the pointer in two dimensions, without a `values` array. Keyboard navigation visits points across series in x order. Tooltip `titleFormat` receives the x-value and `valueFormat` the y-value. On mount, points grow from radius zero at their final positions; updates slide existing points and grow new ones.\n\n### `createBarChart()`\n\n```ts\nfunction createBarChart(container: HTMLElement, config: BarChartConfig): ChartHandle<BarSeriesConfig[]>;\n```\n\nRenders grouped, stacked, grouped-horizontal, or stacked-horizontal categories. `update()` replaces the complete series array; the variant is fixed at creation. Axes render by default, with grid lines on the value axis.\n\nThe default variant is `grouped`; tooltip and legend are disabled unless configured.\n\n| Parameter | Type | Description |\n| ----------- | ---------------- | ----------------------------------------------------------------------------- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `BarChartConfig` | Series, layout variant, axes, interaction, accessibility, and motion settings |\n\n**Returns:** `ChartHandle<BarSeriesConfig[]>`.\n\n```ts\nimport { createBarChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\n\nconst chart = createBarChart(host, {\n a11y: { ariaLabel: 'Tasks by status' },\n series: [\n {\n data: [\n { key: 'Open', value: 12 },\n { key: 'Done', value: 8 },\n ],\n name: 'Tasks',\n },\n ],\n});\nchart.dispose();\n```\n\nFor stacked variants, negative values are treated as zero and produce a development warning.\n\n### `createPieChart()`\n\n```ts\nfunction createPieChart(container: HTMLElement, config: PieChartConfig): ChartHandle<PieSliceConfig[]>;\n```\n\nRenders pie, donut, or semi-circle slices. The default variant is `pie`; `update()` replaces the slice array. The default inner radius is zero for pies and 55% of the outer radius for donut and semi variants. Pie slices default to no padding or rounded corners; donut and semi slices default to 8px padding and corner radius.\n\n| Parameter | Type | Description |\n| ----------- | ---------------- | ------------------------------------------------------------------ |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `PieChartConfig` | Slices, shape, geometry, legend, tooltip, and interaction settings |\n\n**Returns:** `ChartHandle<PieSliceConfig[]>`.\n\n```ts\nimport { createPieChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:320px;height:240px';\ndocument.body.append(host);\n\nconst chart = createPieChart(host, {\n a11y: { ariaLabel: 'Orders by channel' },\n data: [\n { label: 'Direct', value: 72 },\n { label: 'Referral', value: 28 },\n ],\n variant: 'donut',\n});\nchart.dispose();\n```\n\n### `createRadarChart()`\n\n```ts\nfunction createRadarChart(container: HTMLElement, config: RadarChartConfig): ChartHandle<RadarSeriesConfig[]>;\n```\n\nRenders one shape per series across the configured axes. `update()` replaces series data but not axes or other configuration. Fewer than three axes warn in development and draw no chart. Tooltip and legend are disabled unless configured.\n\n| Parameter | Type | Description |\n| ----------- | ------------------ | --------------------------------------------------------------------- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `RadarChartConfig` | Fixed axes, series, scale, grid, appearance, and interaction settings |\n\n**Returns:** `ChartHandle<RadarSeriesConfig[]>`.\n\n```ts\nimport { createRadarChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:400px;height:320px';\ndocument.body.append(host);\n\nconst chart = createRadarChart(host, {\n a11y: { ariaLabel: 'Hero stats' },\n axes: [\n { key: 'str', label: 'Strength' },\n { key: 'agi', label: 'Agility' },\n { key: 'end', label: 'Endurance' },\n ],\n domain: [0, 10],\n series: [\n {\n data: [\n { key: 'str', value: 8 },\n { key: 'agi', value: 6 },\n { key: 'end', value: 9 },\n ],\n name: 'Adam',\n },\n ],\n});\nchart.dispose();\n```\n\nRadar defaults are a solid fill, linear curves, visible points, a four-level polygon grid with bands, no grid labels, and a first-axis angle of zero degrees. Grid labels render only when all axes share a domain.\n\n### `createHeatmapChart()`\n\n```ts\nfunction createHeatmapChart(container: HTMLElement, config: HeatmapChartConfig): ChartHandle<HeatmapData>;\n```\n\nRenders one colored cell per row/column pair: rows down the left, columns across the top. The color ramp is continuous by default; `bins` turns it into discrete steps and shows a swatch legend. Cells show their formatted value when they are large enough, choosing dark or light text against the fill. `rowSummary` adds a right-hand column of pre-formatted per-row texts (e.g. a win rate the cells alone cannot show). `update()` replaces cells and may replace `rows`, `columns`, `domain`, and `rowSummary`; other configuration is fixed at creation.\n\n| Parameter | Type | Description |\n| ----------- | -------------------- | ---------------------------------------------------------------------- |\n| `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |\n| `config` | `HeatmapChartConfig` | Rows, columns, cells, ramp, bins, appearance, and interaction settings |\n\n**Returns:** `ChartHandle<HeatmapData>`.\n\n```ts\nimport { createHeatmapChart } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:400px;height:320px';\ndocument.body.append(host);\n\nconst chart = createHeatmapChart(host, {\n a11y: { ariaLabel: 'Win rate by hero and region' },\n cells: [\n { column: 'Forest', row: 'Aria', value: 90 },\n { column: 'Swamp', row: 'Aria', value: 40 },\n { column: 'Forest', row: 'Brom', value: 20 },\n { column: 'Swamp', row: 'Brom', value: 75 },\n ],\n columns: ['Forest', 'Swamp'],\n rows: ['Aria', 'Brom'],\n});\nchart.dispose();\n```\n\nHeatmap defaults are a light-to-deep blue ramp over the data's `[min, max]` domain, a 2 px cell gap, in-cell values, and no legend. Column headers rotate -45° when they do not fit their column. Keyboard: arrows move the active cell, Enter/Space fires `onCellClick`, Escape deactivates; the tooltip (or `.prism-live` without one) describes the active cell.\n\n### `createSparkline()`\n\n```ts\nfunction createSparkline(container: HTMLElement, config: SparklineConfig): ChartHandle<number[] | StackSegment[]>;\n```\n\nRenders compact data without axes, chart margins, or a legend. `update()` replaces the number or stack-segment array. The default variant is `line`; only line and area variants draw the endpoint marker by default.\n\n| Parameter | Type | Description |\n| ----------- | ----------------- | ----------------------------------------------------------------- |\n| `container` | `HTMLElement` | Host that receives the chart SVG |\n| `config` | `SparklineConfig` | Values, variant, appearance, accessibility, and pointer callbacks |\n\n**Returns:** `ChartHandle<number[] | StackSegment[]>`.\n\n```ts\nimport { createSparkline } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:240px;height:48px';\ndocument.body.append(host);\n\nconst chart = createSparkline(host, { data: [2, 5, 3, 8], variant: 'area' });\nchart.update([2, 5, 3, 8, 13]);\nchart.dispose();\n```\n\n`StackSegment[]` is for `variant: 'stack'`. Pointer callbacks are available for numeric line, area, and bar sparklines; sparklines do not support keyboard navigation.\n\n## Scale Factories\n\n### `linearScale()`\n\n```ts\nfunction linearScale(config: LinearScaleConfig): Scale<number>;\n```\n\nMaps a numeric domain to a numeric range. `nice` defaults to `true`; `clamp` defaults to `false` and applies to `map()`, not `invert()`.\n\n| Parameter | Type | Default | Description |\n| --------------- | ------------------ | -------- | --------------------------------------- |\n| `config.domain` | `[number, number]` | Required | Input extent |\n| `config.range` | `[number, number]` | Required | Output extent |\n| `config.clamp` | `boolean` | `false` | Clamp mapped values to the range |\n| `config.nice` | `boolean` | `true` | Expand the domain to rounded boundaries |\n\n**Returns:** `Scale<number>`, with `map`, `invert`, `ticks`, `domain`, and `range`.\n\n```ts\nimport { linearScale } from '@vielzeug/prism';\n\nconst scale = linearScale({ domain: [0, 100], range: [0, 500] });\nscale.map(50); // 250\n```\n\n### `timeScale()`\n\n```ts\nfunction timeScale(config: TimeScaleConfig): Scale<Date>;\n```\n\nMaps date values to a numeric range. `nice` defaults to `true` and rounds the domain to a time interval.\n\n| Parameter | Type | Default | Description |\n| --------------- | ------------------ | -------- | --------------------------------------------- |\n| `config.domain` | `[Date, Date]` | Required | Input date extent |\n| `config.range` | `[number, number]` | Required | Output extent |\n| `config.nice` | `boolean` | `true` | Expand the domain to time-interval boundaries |\n\n**Returns:** `Scale<Date>`, with `map`, `invert`, `ticks`, `domain`, and `range`.\n\n```ts\nimport { timeScale } from '@vielzeug/prism';\n\nconst scale = timeScale({\n domain: [new Date('2026-01-01'), new Date('2026-12-31')],\n range: [0, 500],\n});\nscale.map(new Date('2026-07-02'));\n```\n\n### `bandScale()`\n\n```ts\nfunction bandScale(config: BandScaleConfig): BandScale;\n```\n\nMaps ordered string categories to the start of each band. `padding` defaults to `0.1`; `paddingOuter` defaults to the same value. Mapping an unknown category returns `0` and warns in development.\n\n| Parameter | Type | Default | Description |\n| --------------------- | ------------------ | --------- | --------------------------------- |\n| `config.domain` | `string[]` | Required | Ordered categories |\n| `config.range` | `[number, number]` | Required | Output extent |\n| `config.padding` | `number` | `0.1` | Inner gap as a ratio of bandwidth |\n| `config.paddingOuter` | `number` | `padding` | Outer gap as a ratio of bandwidth |\n\n**Returns:** `BandScale`, with `map`, `ticks`, `bandwidth`, `gap`, `domain`, and `range`.\n\n```ts\nimport { bandScale } from '@vielzeug/prism';\n\nconst scale = bandScale({ domain: ['A', 'B'], range: [0, 200] });\nscale.bandwidth();\n```\n\n## Theme Utilities\n\n`theme.css` supplies the base CSS values and styles. `setTheme()` writes inline custom properties, which take precedence over stylesheet declarations. For other visual controls, set CSS custom properties directly.\n\n### `setTheme()`\n\n```ts\nfunction setTheme(theme: PrismTheme, options?: ThemeScope): void;\n```\n\nSets the provided theme values on `options.scope`, or on `document.documentElement` when omitted. Omitted scalar keys remain unchanged. Providing `colors` replaces all eight palette slots, removing unused slots.\n\n| Parameter | Type | Description |\n| --------------- | ------------- | -------------------------------------------------------------------- |\n| `theme` | `PrismTheme` | Palette and supported axis, grid, text, font, and tooltip properties |\n| `options.scope` | `HTMLElement` | Element that scopes the inline custom properties |\n\n**Returns:** `void`.\n\n```ts\nimport { setTheme } from '@vielzeug/prism';\n\nconst host = document.createElement('div');\ndocument.body.append(host);\nsetTheme({ colors: ['#2563eb', '#16a34a'], textColor: '#111827' }, { scope: host });\n```\n\n### `resetTheme()`\n\n```ts\nfunction resetTheme(options?: ThemeScope): void;\n```\n\nRemoves every custom property managed by `setTheme()` from the given scope or the document root. It does not remove CSS rules or change any other custom properties.\n\n| Parameter | Type | Description |\n| --------- | ------------ | ------------------------------------------------------- |\n| `options` | `ThemeScope` | Optional target; defaults to `document.documentElement` |\n\n**Returns:** `void`.\n\n```ts\nimport { resetTheme, setTheme } from '@vielzeug/prism';\n\nconst scope = document.createElement('div');\nsetTheme({ textColor: '#111827' }, { scope });\nresetTheme({ scope });\n```\n\n### `seriesColor()`\n\n```ts\nfunction seriesColor(index: number, override?: string): string;\n```\n\nReturns `override` when it is provided; otherwise returns a `var(--prism-color-N)` reference. The palette has eight positions and the index wraps around after the eighth.\n\n| Parameter | Type | Description |\n| ---------- | -------- | ------------------------------------------------- |\n| `index` | `number` | Zero-based palette position |\n| `override` | `string` | Optional color returned instead of a CSS variable |\n\n**Returns:** `string`.\n\n```ts\nimport { seriesColor } from '@vielzeug/prism';\n\nseriesColor(0); // 'var(--prism-color-1)'\nseriesColor(1, '#2563eb'); // '#2563eb'\n```\n\n## Chart Handle\n\n`ChartHandle<TData>` is returned by every chart factory. `update()` synchronously rebuilds the chart from its new data; enabled motion may animate the resulting SVG. `dispose()` is idempotent, removes the chart SVG, observers, tooltip and legend, cancels chart-owned animation, aborts `disposalSignal`, and emits one `dispose` tap event. `update()` after disposal throws `PrismRenderError`.\n\n`tap()` observes `resize` and `dispose` events outside the render path. It returns an unsubscribe function; an optional abort signal removes the subscription. Observer-handler exceptions are swallowed so they cannot affect chart behavior. A tap added after disposal returns a no-op unsubscribe function.\n\n`[Symbol.dispose]()` delegates to `dispose()`, allowing explicit resource management where supported.\n\nAll chart handles expose the same lifecycle members; interactive cartesian, pie, radar, and heatmap charts use a `.prism-live` status region when no tooltip is enabled. A tooltip is itself a polite status region. Sparklines have neither a tooltip nor a live value announcer.\n\n| Method or property | Contract |\n| ------------------------ | ----------------------------------------------------------------------- |\n| `el` | The chart's root `SVGSVGElement` |\n| `update(data)` | Replace chart data and render; throws `PrismRenderError` after disposal |\n| `dispose()` | Idempotently release chart-owned DOM, observation, and motion |\n| `disposed` | Whether the handle has been disposed |\n| `disposalSignal` | `AbortSignal` aborted during disposal |\n| `tap(handler, options?)` | Observe resize/dispose events; returns an unsubscribe function |\n| `[Symbol.dispose]()` | Calls `dispose()` |\n\n`PrismEvent` reports a changed SVG content size as `{ type: 'resize', width, height }`; the `dispose` event is emitted once before teardown completes.\n\n## Types\n\nThe following are all types and interfaces exported from `@vielzeug/prism`. Definitions mirror the package entry-point contracts.\n\n### Animation Types\n\n```ts\nexport type EasingFn = (t: number) => number;\nexport type EasingName = 'back-out' | 'ease-in' | 'ease-in-out' | 'ease-out' | 'expo-out' | 'linear';\n\nexport interface TransitionConfig {\n /** Milliseconds per animated lane. Defaults to 300 (420 for a line entrance, 400 for radar). */\n duration?: number;\n easing?: EasingName | ((t: number) => number);\n /** `system` skips animation when reduced motion is preferred. */\n preference?: 'always' | 'never' | 'system';\n /** Per-bar delay on bar charts, per-entering-point delay (x order) on scatter charts, or per-series delay on radar charts. Bar and scatter charts cap the total added delay at about 400 ms. */\n stagger?: number;\n}\n\nexport type TransitionOption = TransitionConfig | boolean;\n```\n\n### Theme Types\n\n```ts\nexport interface PrismTheme {\n axisColor?: string;\n colors?: string[];\n fontFamily?: string;\n gridColor?: string;\n gridOpacity?: number;\n textColor?: string;\n textColorSecondary?: string;\n tooltipBg?: string;\n tooltipBorder?: string;\n tooltipColor?: string;\n}\n\nexport interface ThemeScope {\n readonly scope?: HTMLElement;\n}\n```\n\n`PrismTheme.colors` writes up to eight palette slots. The scalar fields map as follows:\n\n| Field | CSS custom property |\n| -------------------- | ------------------------------ |\n| `axisColor` | `--prism-axis-color` |\n| `fontFamily` | `--prism-font-family` |\n| `gridColor` | `--prism-grid-color` |\n| `gridOpacity` | `--prism-grid-opacity` |\n| `textColor` | `--prism-text-color` |\n| `textColorSecondary` | `--prism-text-color-secondary` |\n| `tooltipBg` | `--prism-tooltip-bg` |\n| `tooltipBorder` | `--prism-tooltip-border` |\n| `tooltipColor` | `--prism-tooltip-color` |\n\n### Core and Data Types\n\n```ts\nexport interface ChartMargin {\n bottom: number;\n left: number;\n right: number;\n top: number;\n}\n\nexport interface ChartDimensions {\n height: number;\n margin: ChartMargin;\n width: number;\n}\n\nexport type PrismEvent =\n | { readonly height: number; readonly type: 'resize'; readonly width: number }\n | { readonly type: 'dispose' };\n\nexport interface ChartHandle<TData = unknown> {\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n readonly el: SVGSVGElement;\n tap(handler: (event: PrismEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;\n update(data: TData): void;\n [Symbol.dispose](): void;\n}\n\nexport type ChartA11y = { readonly decorative: true } | { readonly ariaLabel: string; readonly decorative?: false };\n\nexport interface Datum<TKey extends Date | number | string = Date | number | string> {\n dash?: string;\n key: TKey;\n meta?: Record<string, unknown>;\n opacity?: number;\n value: number;\n}\n\nexport type ContinuousDatum = Datum<Date | number>;\n\nexport interface Series<TDatum extends Datum = Datum> {\n color?: string;\n data: TDatum[];\n id?: string;\n name: string;\n}\n\nexport interface ChartEvent {\n datum: Datum;\n originalEvent: Event;\n series: Series;\n values?: SeriesValue[];\n}\n\nexport interface SeriesValue {\n datum: Datum | undefined;\n series: Series;\n}\n```\n\n`Datum.dash` and `Datum.opacity` affect rendered marks only on the chart types that support them. On bars, `dash` adds a 1px series-color stroke so the dash is visible against the fill. `Series.id` becomes a `data-series-id` attribute on a series group; characters outside `[A-Za-z0-9_-]` are replaced with `-`. When omitted, the id is based on the series index.\n\n| Chart | `dash` | `opacity` |\n| ------------------------------ | ----------------------------------------------------------- | -------------------------------------------- |\n| Bar | Strokes that datum's bar | Applies to that bar |\n| Line | Applies to the segment from this datum to the next | Applies to that segment and its point marker |\n| Area | Applies to the top-line segment from this datum to the next | Applies to that segment |\n| Scatter | Ignored | Applies to that point |\n| Pie, radar, heatmap, sparkline | Ignored | Ignored |\n\nFor line and area charts, the last datum has no following segment, so its `dash` value is ignored. The chart factories group adjacent segments with the same presentation values into path runs.\n\nFor line, area, and bar charts, `ChartEvent.values` lists each series at the active key in series order. Its `datum` is `undefined` when that series has no matching key. `originalEvent` is a pointer event or a keyboard event for keyboard activation. Scatter events identify only the nearest independent point and omit `values`.\n\n### Scale Types\n\n```ts\nexport interface Scale<T> {\n readonly domain: readonly [T, T];\n invert(pixel: number): T;\n map(value: T): number;\n readonly range: readonly [number, number];\n ticks(count?: number): T[];\n}\n\nexport interface BandScale {\n bandwidth(): number;\n readonly domain: readonly string[];\n gap(): number;\n map(value: string): number;\n readonly range: readonly [number, number];\n ticks(count?: number): string[];\n}\n\nexport interface LinearScaleConfig {\n clamp?: boolean;\n domain: [number, number];\n nice?: boolean;\n range: [number, number];\n}\n\nexport interface TimeScaleConfig {\n domain: [Date, Date];\n nice?: boolean;\n range: [number, number];\n}\n\nexport interface BandScaleConfig {\n domain: string[];\n padding?: number;\n paddingOuter?: number;\n range: [number, number];\n}\n```\n\n### Axis and Interaction Types\n\n```ts\nexport type HorizontalAxisPosition = 'bottom' | 'top';\nexport type VerticalAxisPosition = 'left' | 'right';\nexport type AxisPosition = HorizontalAxisPosition | VerticalAxisPosition;\n\nexport interface GridConfig {\n color?: string;\n dash?: string;\n}\n\nexport interface AxisConfig<TPosition extends AxisPosition = AxisPosition> {\n grid?: GridConfig | boolean;\n label?: string;\n position?: TPosition;\n tickCount?: number;\n tickFormat?: (value: Date | number | string) => string;\n tickValues?: ReadonlyArray<Date | number | string>;\n}\n\nexport type XAxisConfig = AxisConfig<HorizontalAxisPosition>;\nexport type YAxisConfig = AxisConfig<VerticalAxisPosition>;\n\nexport interface TooltipConfig {\n offset?: number;\n render?: (datum: Datum, series: Series) => Node | string;\n titleFormat?: (key: Date | number | string) => string;\n valueFormat?: (value: number, datum: Datum, series: Series) => string;\n}\n\nexport interface CrosshairConfig {\n horizontal?: boolean;\n snap?: boolean;\n vertical?: boolean;\n}\n\nexport type LegendPosition = 'bottom' | 'left' | 'right' | 'top';\n\nexport interface LegendConfig {\n position?: LegendPosition;\n}\n```\n\nThe crosshair defaults to a vertical line that snaps to the nearest datum; the horizontal line is hidden by default. With `snap: false`, only the guide follows the pointer—tooltip and callback data still refer to the nearest datum.\n\n`tickValues` renders exactly the given ticks instead of scale-generated ones, and gridlines follow the same list. Values must match the axis's scale kind (numbers on a linear axis, `Date`s on a time axis, category strings on a band axis); an off-scale value warns in development. Without it, ticks come from the scale as before.\n\nNumeric tick labels at a magnitude of 1e5 or above default to compact notation (`123K`, `1.23M`) so long numbers fit the default value-axis margin; pass `tickFormat: String` for raw labels. Supplying a partial `yAxis` (or the value axis of a horizontal bar chart) merges over the default, so `{ tickFormat }` alone keeps the default grid lines.\n\n`titleFormat` and `valueFormat` shape the default comparison tooltip on line, area, and bar charts—the title from the focused key and each row's value—and the same formatting is applied to the tooltip's spoken summary. `render` still replaces the whole tooltip body and takes precedence. Tooltip strings are assigned as text, not parsed as HTML. For a radar chart, `tooltip.render` receives a datum whose `meta` contains the active axis, all series values, and the default announcement text.\n\n### Chart Configuration Types\n\n```ts\nexport interface BaseChartConfig {\n a11y?: ChartA11y;\n legend?: LegendConfig | boolean;\n margin?: Partial<ChartMargin>;\n onClick?: (event: ChartEvent) => void;\n onHover?: (event: ChartEvent | null) => void;\n tooltip?: TooltipConfig | boolean;\n transition?: TransitionOption;\n xAxis?: XAxisConfig | false;\n yAxis?: YAxisConfig | false;\n}\n\nexport interface LineSeriesConfig extends Series<ContinuousDatum> {\n curve?: 'linear' | 'monotone' | 'step';\n pointRadius?: number;\n showPoints?: boolean;\n strokeWidth?: number;\n yAxis?: 'left' | 'right';\n}\n\nexport interface LineChartConfig extends BaseChartConfig {\n crosshair?: CrosshairConfig | boolean;\n rightYAxis?: Omit<YAxisConfig, 'position' | 'grid'>;\n series: LineSeriesConfig[];\n}\n\nexport interface AreaSeriesConfig extends Series<ContinuousDatum> {\n curve?: 'linear' | 'monotone' | 'step';\n fill?: 'gradient' | 'solid';\n fillOpacity?: number;\n showLine?: boolean;\n}\n\nexport interface AreaChartConfig extends BaseChartConfig {\n crosshair?: CrosshairConfig | boolean;\n series: AreaSeriesConfig[];\n}\n\nexport interface ScatterSeriesConfig extends Series<ContinuousDatum> {\n pointRadius?: number;\n}\n\nexport interface ScatterChartConfig extends BaseChartConfig {\n crosshair?: CrosshairConfig | boolean;\n series: ScatterSeriesConfig[];\n}\n\nexport interface BarSeriesConfig extends Series {\n borderRadius?: number;\n}\n\nexport type BarVariant = 'grouped' | 'grouped-horizontal' | 'stacked' | 'stacked-horizontal';\n\nexport interface BarChartConfig extends BaseChartConfig {\n series: BarSeriesConfig[];\n /** Paints each bar's value at its end; stacked variants label the stack total. */\n showValues?: boolean;\n /** Formats the value labels. Defaults to `String`. */\n valueFormat?: (value: number) => string;\n variant?: BarVariant;\n}\n\nexport type PieVariant = 'donut' | 'pie' | 'semi';\n\nexport interface PieSliceConfig {\n color?: string;\n label?: string;\n value: number;\n}\n\nexport interface PieEvent {\n index: number;\n originalEvent: Event;\n slice: PieSliceConfig;\n}\n\nexport interface PieChartConfig extends Omit<BaseChartConfig, 'margin' | 'onClick' | 'onHover' | 'xAxis' | 'yAxis'> {\n cornerRadius?: number;\n data: PieSliceConfig[];\n innerRadius?: number;\n onClick?: (event: PieEvent) => void;\n onHover?: (event: PieEvent | null) => void;\n padPixels?: number;\n variant?: PieVariant;\n}\n\nexport interface RadarAxisConfig {\n format?: (value: number) => string;\n key: string;\n label: string;\n max?: number;\n min?: number;\n}\n\nexport type RadarSeriesConfig = Series<Datum<string>>;\n\nexport interface RadarGridConfig {\n bands?: boolean;\n labels?: boolean;\n levels?: number;\n shape?: 'circle' | 'polygon';\n}\n\nexport interface RadarAxisValue {\n datum: Datum<string> | undefined;\n series: RadarSeriesConfig;\n}\n\nexport interface RadarEvent {\n axis: RadarAxisConfig;\n index: number;\n originalEvent: Event;\n values: RadarAxisValue[];\n}\n\nexport interface RadarChartConfig extends Omit<BaseChartConfig, 'margin' | 'onClick' | 'onHover' | 'xAxis' | 'yAxis'> {\n axes: RadarAxisConfig[];\n curve?: 'linear' | 'rounded';\n domain?: [number, number];\n fill?: 'gradient' | 'none' | 'solid';\n grid?: RadarGridConfig | boolean;\n onClick?: (event: RadarEvent) => void;\n onHover?: (event: RadarEvent | null) => void;\n series: RadarSeriesConfig[];\n showPoints?: boolean;\n showValues?: boolean;\n startAngle?: number;\n}\n\nexport interface HeatmapCell {\n column: string;\n row: string;\n value: number | null;\n}\n\nexport interface HeatmapEvent {\n cell: HeatmapCell | undefined;\n column: string;\n originalEvent: Event;\n row: string;\n}\n\nexport interface HeatmapData {\n cells: HeatmapCell[];\n columns?: string[];\n domain?: [number, number];\n rows?: string[];\n rowSummary?: HeatmapChartConfig['rowSummary'];\n}\n\nexport interface HeatmapChartConfig\n extends Omit<BaseChartConfig, 'legend' | 'onClick' | 'onHover' | 'transition' | 'xAxis' | 'yAxis'> {\n binLabel?: (from: number | null, to: number | null) => string;\n bins?: number[];\n cells: HeatmapCell[];\n colorRange?: string[];\n columns: string[];\n domain?: [number, number];\n emptyColor?: string;\n emptyLabel?: string;\n gap?: number;\n legend?: boolean;\n onCellClick?: (event: HeatmapEvent) => void;\n onCellHover?: (event: HeatmapEvent | null) => void;\n rows: string[];\n rowSummary?: { header?: string; values: (string | null)[] };\n showValues?: boolean;\n valueFormat?: (value: number) => string;\n}\n\nexport type SparklineVariant = 'area' | 'bar' | 'line' | 'stack';\n\nexport interface StackSegment {\n color?: string;\n label?: string;\n value: number;\n}\n\nexport interface SparklineEvent {\n index: number;\n originalEvent: Event;\n value: number;\n}\n\nexport interface SparklineConfig {\n a11y?: ChartA11y;\n color?: string;\n cornerRadius?: number;\n curve?: 'linear' | 'monotone' | 'step';\n data: number[] | StackSegment[];\n fillOpacity?: number;\n onClick?: (event: SparklineEvent) => void;\n onHover?: (event: SparklineEvent | null) => void;\n padPixels?: number;\n showEndPoint?: boolean;\n strokeWidth?: number;\n transition?: TransitionOption;\n variant?: SparklineVariant;\n}\n```\n\n## Errors\n\n### `PrismError`\n\n```ts\nclass PrismError extends Error {\n constructor(message: string, opts?: ErrorOptions);\n}\n```\n\nBase class for Prism-originated errors. Its `name` is set to the concrete error class name, and it supports the standard `ErrorOptions` including `cause`.\n\n### `PrismRenderError`\n\n```ts\nclass PrismRenderError extends PrismError {}\n```\n\nThrown when a chart cannot be rendered because its container is not a DOM element, when initial rendering fails (the original error is available as `cause`), or when `update()` is called after disposal. Empty or malformed chart data may instead produce a development warning or an empty rendering, depending on the chart.\n",
6
+ "usage": "---\ntitle: 'Prism: Usage Guide'\ndescription: Create responsive SVG charts, update their data, handle interaction, and theme @vielzeug/prism.\n---\n\n[[toc]]\n\n## Basic Usage\n\nCreate a host element with non-zero dimensions, then pass it to a chart factory. This example creates the host in JavaScript so the snippet can run as written.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\nimport '@vielzeug/prism/theme.css';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createLineChart(container, {\n a11y: { ariaLabel: 'Revenue by month' },\n series: [\n {\n name: 'Revenue',\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 16 },\n ],\n },\n ],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nEach factory appends an SVG to its container and returns a `ChartHandle`. Prism observes the container with `ResizeObserver`; if it has zero dimensions on creation, Prism uses a temporary `600 × 300` size and warns in development. Give the host its layout before creating the chart.\n\n## Updating Data\n\nKeep application state outside Prism and pass the next complete data value to `update()`. The update method replaces chart data synchronously; the default transition may animate the resulting SVG.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nlet series = [{ data: [{ key: 1, value: 10 }], name: 'Live' }];\nconst chart = createLineChart(container, { series });\n\nseries = [\n {\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 20 },\n ],\n name: 'Live',\n },\n];\nchart.update(series);\nchart.dispose();\n```\n\n`update()` accepts the type shown by that factory's handle: a complete series array for line, area, scatter, bar, and radar charts; a slice array for pie charts; a `HeatmapData` object for heatmap charts; and a number or stack-segment array for sparklines. Chart options such as axes, variant, callbacks, and accessibility are set at creation. For example, radar axes and a pie chart's variant do not change through `update()`.\n\n## Line Charts\n\nLine and area series use number or `Date` keys. All series in one chart must use the same key kind. Use `curve` to choose interpolation, and enable markers with `showPoints`.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createLineChart(container, {\n a11y: { ariaLabel: 'Revenue and expenses' },\n series: [\n {\n curve: 'monotone',\n data: [\n { key: 1, value: 100 },\n { key: 2, value: 150 },\n { key: 3, value: 130 },\n ],\n name: 'Revenue',\n showPoints: true,\n },\n {\n color: '#c2410c',\n data: [\n { key: 1, value: 70 },\n { key: 2, value: 95 },\n { key: 3, value: 110 },\n ],\n name: 'Expenses',\n },\n ],\n tooltip: true,\n xAxis: { label: 'Month' },\n yAxis: { grid: true, label: 'Dollars' },\n});\n\nchart.dispose();\n```\n\n### Independent Value Axes\n\nAssign a line series to the right axis when it uses a different value range. Both axes share the x-scale; `rightYAxis` configures the second axis and cannot configure its position or grid.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createLineChart(container, {\n a11y: { ariaLabel: 'Health and damage by level' },\n rightYAxis: { label: 'Damage' },\n series: [\n {\n data: [\n { key: 1, value: 30 },\n { key: 2, value: 20 },\n ],\n name: 'Health',\n },\n {\n data: [\n { key: 1, value: 100 },\n { key: 2, value: 300 },\n ],\n name: 'Damage',\n yAxis: 'right',\n },\n ],\n yAxis: { grid: true, label: 'Health' },\n});\n\nchart.dispose();\n```\n\nWhen any series uses the right axis, the primary value axis is placed on the left. Grid lines belong to the primary `yAxis`. The independent numeric scales mean that curve crossings do not imply equal values.\n\n### Time-Based X Axis\n\nUse `Date` values for every key when plotting time. Line and area charts use a time scale whose domain fits the data instead of rounding outward to tick boundaries.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst formatter = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' });\nconst chart = createLineChart(container, {\n a11y: { ariaLabel: 'Signups over time' },\n series: [\n {\n data: [\n { key: new Date('2026-09-01'), value: 50 },\n { key: new Date('2026-09-08'), value: 80 },\n { key: new Date('2026-09-15'), value: 120 },\n ],\n name: 'Signups',\n },\n ],\n xAxis: { tickFormat: (value) => (value instanceof Date ? formatter.format(value) : String(value)) },\n});\n\nchart.dispose();\n```\n\n`tickFormat` formats axis labels only. Tooltip content and accessibility announcements use the underlying keys and values. To render an exact set of ticks instead of scale-generated ones, pass `tickValues`; gridlines follow the same list. Values must match the axis's scale kind, and an off-scale value warns in development:\n\n```ts\n// Force a tick on the last month, which nice-stepped ticks can skip.\nconst chart = createLineChart(container, {\n series: [\n {\n data: [\n { key: 0, value: 90 },\n { key: 11, value: 128 },\n ],\n name: 'Costs',\n },\n ],\n xAxis: { tickValues: [0, 6, 11] },\n});\n```\n\nNumeric tick labels at a magnitude of `1e5` or above default to compact notation (`123K`, `1.23M`) so long numbers fit the default value-axis margin; pass `tickFormat: String` for raw labels. Supplying a partial value-axis config merges over its default, so `yAxis: { tickFormat }` alone keeps the default grid lines.\n\n## Bar Charts\n\nBar chart keys are string categories. The default `grouped` variant puts series side by side; the horizontal variants put categories on the y-axis and values on the x-axis.\n\n```ts\nimport { createBarChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createBarChart(container, {\n a11y: { ariaLabel: 'Sales by quarter' },\n series: [\n {\n data: [\n { key: 'Q1', value: 200 },\n { key: 'Q2', value: 350 },\n { key: 'Q3', value: 280 },\n ],\n name: 'Sales',\n },\n ],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nChoose `grouped`, `stacked`, `grouped-horizontal`, or `stacked-horizontal` in `variant`. Negative values in a stacked chart are clamped to zero and produce a development warning. Bars in stacked variants remain square; `borderRadius` applies to grouped bars.\n\nAxes render by default, with grid lines on the value axis. For horizontal variants, `xAxis` is the value axis and `yAxis` is the category axis. Configure `grid` on the value axis.\n\nSet `showValues: true` to paint each bar's value at its end so the chart reads without hover; `valueFormat` shapes the label text. Stacked variants label the stack total at the top of the stack, and labels that would overflow the plot flip inside the bar with a contrasting fill. The labels are decorative (`aria-hidden`) — the accessible read stays with the chart's data table.\n\n## Area Charts\n\nArea series use a gradient fill by default. Set `fill: 'solid'` for a flat fill, or set `fillOpacity` to override the theme opacity for that series.\n\n```ts\nimport { createAreaChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createAreaChart(container, {\n a11y: { ariaLabel: 'Visitors over time' },\n crosshair: true,\n series: [\n {\n curve: 'monotone',\n data: [\n { key: 1, value: 12 },\n { key: 2, value: 18 },\n { key: 3, value: 15 },\n ],\n fill: 'solid',\n fillOpacity: 0.2,\n name: 'Visitors',\n showLine: true,\n },\n ],\n});\n\nchart.dispose();\n```\n\n## Scatter Charts\n\nScatter charts treat every datum as an independent point: `key` is the numeric or `Date` x-value and `value` is the y-value. The scales fit the observed values rather than forcing the y-domain to include zero. Set `pointRadius` on a series to change its marker size (default `3`).\n\n```ts\nimport { createScatterChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createScatterChart(container, {\n a11y: { ariaLabel: 'Height against weight' },\n series: [\n {\n data: [\n { key: 160, value: 55 },\n { key: 175, value: 70 },\n { key: 190, value: 88 },\n ],\n name: 'Cohort A',\n pointRadius: 5,\n },\n ],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nScatter callbacks identify the nearest point in two dimensions and do not provide cross-series `values`, because points do not share a category key. Keyboard navigation visits every point across series in x order. With a tooltip, `titleFormat` formats the x-value and `valueFormat` the y-value, in both the visible row and the spoken summary.\n\n## Forecast and Emphasis Styling\n\nAdd `dash` or `opacity` to individual `Datum` objects when one part of a series needs distinct styling. These fields do not change interaction, tooltip, or accessibility behavior.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst values = [10, 13, 15, 18];\nconst chart = createLineChart(container, {\n series: [\n {\n data: values.map((value, index) => ({\n key: index + 1,\n value,\n ...(index >= 2 ? { dash: '5 5', opacity: 0.4 } : {}),\n })),\n name: 'Forecast',\n showPoints: true,\n },\n ],\n});\n\nchart.dispose();\n```\n\nFor line and area charts, each datum's `dash` styles the segment from that datum to the next; the final datum has no outgoing segment. Line opacity also fades the datum's marker. Area styling affects the top line, not the fill. Bar opacity and dashes apply to the individual bar; scatter opacity applies to its point and scatter dashes are ignored. Pie, radar, and sparkline ignore these two fields.\n\n## Pie, Donut, and Semi Charts\n\nUse `createPieChart()` for all three shapes. `variant` is fixed when the chart is created; `update()` replaces the slice data.\n\n```ts\nimport { createPieChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:320px;height:240px';\ndocument.body.append(container);\n\nconst chart = createPieChart(container, {\n a11y: { ariaLabel: 'Orders by channel' },\n data: [\n { label: 'Direct', value: 42 },\n { label: 'Organic', value: 28 },\n { label: 'Referral', value: 18 },\n { label: 'Social', value: 12 },\n ],\n legend: true,\n tooltip: true,\n variant: 'donut',\n});\n\nchart.dispose();\n```\n\nThe `pie` default has no inner radius. `donut` and `semi` default to an inner radius of 55% of the outer radius. `innerRadius` overrides that size in pixels. Slice labels are omitted when they do not fit; keep a tooltip or legend when small slices need identifying.\n\n## Radar Charts\n\nDefine at least three axes when creating a radar chart. Series data uses string keys that match `RadarAxisConfig.key`; `update()` replaces series but keeps axes and other configuration fixed.\n\n```ts\nimport { createRadarChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:400px;height:320px';\ndocument.body.append(container);\n\nconst axes = [\n { key: 'strength', label: 'Strength' },\n { key: 'agility', label: 'Agility' },\n { key: 'endurance', label: 'Endurance' },\n];\nconst chart = createRadarChart(container, {\n a11y: { ariaLabel: 'Hero stats' },\n axes,\n domain: [0, 10],\n series: [\n {\n data: [\n { key: 'strength', value: 8 },\n { key: 'agility', value: 6 },\n { key: 'endurance', value: 9 },\n ],\n name: 'Adam',\n },\n ],\n tooltip: true,\n});\n\nchart.dispose();\n```\n\nThe default shared domain starts at zero and uses a nice maximum; set `domain` to compare series on a fixed range. Per-axis `min` and `max` override the chart domain for that axis. Grid rings, bands, curves, fills, vertex dots, and values are configurable; see `RadarChartConfig` and `RadarGridConfig` in the API reference.\n\n## Heatmaps\n\nDefine `rows`, `columns`, and `cells`; each cell names its `row` and `column` labels. A cell with `value: null` (or no matching cell at all) renders in `emptyColor` without in-cell text. `rowSummary` adds a right-hand column of pre-formatted per-row texts (aligned with `rows`, with an optional header) — useful for a per-row rate the cells cannot show. `update()` replaces cells and may replace `rows`, `columns`, `domain`, and `rowSummary`.\n\n```ts\nimport { createHeatmapChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:400px;height:320px';\ndocument.body.append(container);\n\nconst chart = createHeatmapChart(container, {\n a11y: { ariaLabel: 'Win rate by hero and region' },\n bins: [50, 75],\n cells: [\n { column: 'Forest', row: 'Aria', value: 90 },\n { column: 'Swamp', row: 'Aria', value: 40 },\n { column: 'Forest', row: 'Brom', value: 20 },\n { column: 'Swamp', row: 'Brom', value: 75 },\n ],\n columns: ['Forest', 'Swamp'],\n rows: ['Aria', 'Brom'],\n tooltip: true,\n valueFormat: (value) => `${value}%`,\n});\n\nchart.dispose();\n```\n\nWithout `bins` the ramp is continuous over the data's `[min, max]` domain (or an explicit `domain`); with `bins` each value maps to a discrete step and a swatch legend appears. `colorRange` sets the ramp low → high, `valueFormat` shapes in-cell values, tooltip values, and bin labels, and `showValues: false` hides the in-cell text.\n\n## Sparklines\n\nSparklines omit axes, margins, and legends. Choose `line`, `area`, or `bar` for numeric arrays, and `stack` for an array of `StackSegment` objects.\n\n```ts\nimport { createSparkline } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:240px;height:48px';\ndocument.body.append(container);\n\nconst chart = createSparkline(container, {\n a11y: { ariaLabel: 'Weekly orders' },\n data: [10, 14, 12, 18, 22],\n variant: 'area',\n});\n\nchart.update([10, 14, 12, 18, 22, 25]);\nchart.dispose();\n```\n\nFor a stacked sparkline, each segment can set `value`, `color`, and `label`. Sparklines do not offer keyboard navigation, a tooltip, or a live value announcer. Add a visible value or summary when the trend conveys important information.\n\n## Interaction\n\nSet `tooltip: true` to show Prism's default tooltip, or pass a `TooltipConfig` to format its content. `titleFormat` and `valueFormat` shape the default comparison tooltip on line, area, and bar charts—the title from the focused key and each row's value—without rebuilding its markup, and the same formatting is applied to the tooltip's spoken summary. A `render` callback receives the active `Datum` and `Series` and replaces the whole body, taking precedence over the formatters. Returned strings are inserted as text; return a DOM `Node` for structured content.\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst compact = new Intl.NumberFormat(undefined, { notation: 'compact' });\nconst months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];\n\nconst chart = createLineChart(container, {\n series: [{ data: [{ key: 11, value: 128000 }], name: 'EC2' }],\n tooltip: {\n titleFormat: (key) => months[Number(key)] ?? String(key),\n valueFormat: (value) => compact.format(value),\n },\n});\n\nchart.dispose();\n```\n\n`onHover` receives an event while a point, category, slice, or radar axis is active, then `null` when the interaction clears. `onClick` receives the selected event. For line, area, and bar charts, `ChartEvent.values` contains each series' datum at that shared key; a missing datum is `undefined`. Scatter events are `ChartEvent` objects without `values`; pie, radar, and sparkline use their chart-specific event types.\n\n`crosshair` is available on line, area, and scatter charts. By default it shows a vertical guide and snaps the line to the nearest datum. Set `horizontal: true` to add a horizontal guide or `vertical: false` to hide the vertical one. Set `snap: false` to make the guide follow the pointer; tooltip and callback data still come from the nearest datum.\n\n### Keyboard and Announcements\n\nSet `a11y: { ariaLabel }` on an informative chart. Prism gives the SVG `role=\"img\"`, an accessible name, and keyboard focus. Arrow keys move among chart data; `Home` and `End` move to the first and last item where supported; `Enter` or `Space` activates an item when an `onClick` callback is configured; `Escape` clears the active selection. Sparklines do not support keyboard navigation.\n\nWhen `a11y` is omitted, or set to `{ decorative: true }`, the SVG is marked `aria-hidden=\"true\"` and does not enter the keyboard tab order. Only mark charts decorative when their information is available elsewhere. Interactive cartesian, pie, radar, and heatmap charts use the tooltip as a polite live region when enabled; otherwise they announce active values in `.prism-live`. Sparklines have no live value announcements, so provide an equivalent visible value or summary.\n\n## Animation\n\nChart motion is enabled by default, with a 300 ms duration per animated lane (radar uses 400 ms). Pass `transition: false` to render updates synchronously, or pass `true` to state the default explicitly.\n\n```ts\nimport { createBarChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createBarChart(container, {\n series: [\n {\n data: [\n { key: 'A', value: 4 },\n { key: 'B', value: 7 },\n ],\n name: 'Count',\n },\n ],\n transition: { duration: 500, easing: 'ease-out', preference: 'system' },\n});\n\nchart.dispose();\n```\n\nColumns grow together from their baseline by default over 300 ms. Lines trace their final curve over 420 ms, uncovering point markers along the way without fading or shifting the series. Line data updates retain the default 300 ms geometry transition; an explicit `duration` overrides both timings.\n\n`preference: 'system'` (the default) disables JavaScript motion when the user prefers reduced motion. Use `'always'` to ignore that preference or `'never'` to disable motion. `stagger` sets an optional delay between bars, entering scatter points (in x order), or radar series; bar and scatter charts cap the effective delay so many marks do not add unbounded time. Named easing values are `ease-in`, `ease-in-out`, `ease-out`, `back-out`, `expo-out`, and `linear`; you can also provide an easing function.\n\n## Responsive Layout and Lifecycle\n\nSize the host with CSS. Prism observes its content box and updates the SVG dimensions and chart layout when it changes. Bar, line, and scatter resize redraws apply immediately rather than animating between layout sizes; entrance animations and explicit data-update transitions remain enabled. The legend is rendered as container chrome outside the SVG and included in resize calculations.\n\nCall `dispose()` when the view no longer owns the chart. Disposal is idempotent: it removes the SVG, tooltip, legend, and resize observation, stops in-flight chart motion, and aborts `disposalSignal`. `chart[Symbol.dispose]()` calls the same cleanup method.\n\nUse `tap()` when external code needs resize or dispose notifications rather than observing the chart DOM itself:\n\n```ts\nimport { createLineChart } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nconst chart = createLineChart(container, {\n series: [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }],\n});\nconst stopObserving = chart.tap((event) => {\n if (event.type === 'resize') console.log(event.width, event.height);\n if (event.type === 'dispose') console.log('chart disposed');\n});\n\nstopObserving();\nchart.dispose();\n```\n\n`tap()` returns an unsubscribe function and accepts `{ signal }` for automatic unsubscription. A listener exception is swallowed by the observer so it cannot interrupt chart rendering.\n\n## Theming\n\nImport the stylesheet once to load Prism's structural styles, default tokens, dark-mode rules, and reduced-motion CSS. You can override any CSS custom property on an ancestor of the chart.\n\n```css\n.dashboard-chart {\n --prism-color-1: #2563eb;\n --prism-color-2: #16a34a;\n --prism-grid-color: #94a3b8;\n --prism-line-width: 3;\n --prism-text-color: #172033;\n --prism-tooltip-bg: #111827;\n}\n```\n\n`setTheme()` provides a typed subset of theme properties. A scoped call writes inline values on the provided element; omit `scope` to write them to `document.documentElement`. Inline values override stylesheet tokens until reset.\n\n```ts\nimport { resetTheme, setTheme } from '@vielzeug/prism';\n\nconst container = document.createElement('div');\ncontainer.style.cssText = 'width:640px;height:320px';\ndocument.body.append(container);\n\nsetTheme({ colors: ['#2563eb', '#16a34a'], fontFamily: 'Inter, sans-serif' }, { scope: container });\n// Call resetTheme({ scope: container }) when this scoped theme is no longer needed.\nresetTheme({ scope: container });\n```\n\n### Available Tokens\n\nThe stylesheet defines defaults for the following token groups. Pie-label color and size are read with fallback values by the renderer but are not declared in the base token block. Set a token on the chart container or an ancestor to scope an override.\n\n| Group | CSS custom properties |\n| --------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n| Palette and surfaces | `--prism-color-1` through `--prism-color-8`, `--prism-bg`, `--prism-border-radius` |\n| Axes and grid | `--prism-axis-color`, `--prism-axis-width`, `--prism-grid-color`, `--prism-grid-width`, `--prism-grid-dash`, `--prism-grid-opacity` |\n| Text | `--prism-font-family`, `--prism-font-size`, `--prism-font-size-label`, `--prism-font-size-title`, `--prism-font-weight`, `--prism-font-weight-title`, `--prism-text-color`, `--prism-text-color-secondary` |\n| Tooltip | `--prism-tooltip-bg`, `--prism-tooltip-color`, `--prism-tooltip-border`, `--prism-tooltip-radius`, `--prism-tooltip-padding`, `--prism-tooltip-shadow`, `--prism-tooltip-font-size` |\n| Crosshair | `--prism-crosshair-color`, `--prism-crosshair-width`, `--prism-crosshair-dash`, `--prism-crosshair-opacity` |\n| Points | `--prism-point-radius`, `--prism-point-radius-hover`, `--prism-point-stroke-width`, `--prism-point-stroke` |\n| Bars | `--prism-bar-radius`, `--prism-bar-hover-opacity`, `--prism-bar-gap`, `--prism-bar-band-fill`, `--prism-bar-band-opacity`, `--prism-bar-dim-opacity` |\n| Areas and lines | `--prism-area-opacity`, `--prism-area-stroke-width`, `--prism-area-gradient-start`, `--prism-area-gradient-end`, `--prism-line-width`, `--prism-line-width-hover` |\n| Interaction | `--prism-dim-opacity`, `--prism-active-point-radius`, `--prism-point-ring`, `--prism-focus-color` |\n| Sparklines | `--prism-spark-fill-opacity`, `--prism-spark-gradient-start`, `--prism-spark-gradient-end` |\n| Radar | `--prism-radar-grid-color`, `--prism-radar-grid-opacity`, `--prism-radar-band-fill`, `--prism-radar-band-opacity`, `--prism-radar-spoke-color`, `--prism-radar-fill-opacity`, `--prism-radar-stroke-width`, `--prism-radar-point-radius`, `--prism-radar-point-radius-active`, `--prism-radar-label-color`, `--prism-radar-label-size`, `--prism-radar-value-color`, `--prism-radar-value-size`, `--prism-radar-dim-opacity` |\n| Heatmap | `--prism-heatmap-label-color`, `--prism-heatmap-label-size`, `--prism-heatmap-value-size`, `--prism-heatmap-highlight-color` |\n| Motion | `--prism-duration-fast`, `--prism-duration`, `--prism-duration-slow`, `--prism-ease`, `--prism-ease-spring`, `--prism-transition`, `--prism-transition-fast` |\n| Legend and pie labels | `--prism-legend-gap`, `--prism-legend-dot-size`, `--prism-legend-font-size`, `--prism-pie-label-color`, `--prism-pie-label-size` |\n\nThe default palette and structural colors adapt to `prefers-color-scheme: dark`, `html.dark`, or `[data-prism-theme='dark']` on an ancestor. Scoped inline values set with `setTheme()` take precedence in either mode. The stylesheet also provides `prefers-reduced-motion` overrides for CSS transitions.\n\n## Errors and Diagnostics\n\nPrism throws `PrismRenderError` when the container is not a DOM element, when initial rendering fails, or when `update()` is called on a disposed chart. A wrapped initial-render error exposes its cause. Use `PrismError` to catch all Prism-originated errors.\n\nEmpty or malformed data is not always an exception. Depending on the chart, Prism may draw nothing or emit a development-only `console.warn`; examples include an unknown `bandScale` category, negative values in stacked bars, and radar axes with fewer than three entries or duplicate keys. Correct the input rather than relying on a warning for validation.\n\n## Framework Integration\n\nPrism has no React, Vue, or Svelte adapter. In any framework, give Prism a host element after it is mounted, call `update()` when data changes, and call `dispose()` before the host is removed. This plain DOM pattern shows the ownership boundary without depending on a framework-specific lifecycle API:\n\n```ts\nimport { createLineChart, type LineSeriesConfig } from '@vielzeug/prism';\n\nfunction mountChart(host: HTMLElement, series: LineSeriesConfig[]) {\n const chart = createLineChart(host, { a11y: { ariaLabel: 'Revenue' }, series });\n\n return {\n update(next: LineSeriesConfig[]) {\n chart.update(next);\n },\n dispose() {\n chart.dispose();\n },\n };\n}\n\nconst host = document.createElement('div');\nhost.style.cssText = 'width:640px;height:320px';\ndocument.body.append(host);\nconst view = mountChart(host, [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }]);\nview.update([\n {\n data: [\n { key: 1, value: 10 },\n { key: 2, value: 15 },\n ],\n name: 'Revenue',\n },\n]);\nview.dispose();\n```\n\nThe application owns its state and host; Prism owns only the SVG, observer, and optional overlay elements it creates.\n\n## Working with Other Vielzeug Libraries\n\nUse Refine components for dashboard controls and keep Prism in a sized host element. Map Refine theme tokens to Prism CSS custom properties or call `setTheme()` with a scoped element; Prism does not require a Refine adapter. Prism uses Orbit internally to place tooltips, so chart users do not need to create Orbit floating elements for the built-in tooltip.\n\n## Best Practices\n\n- Give the host a non-zero size before creating a chart.\n- Set `a11y.ariaLabel` when a chart communicates information.\n- Use `Date` or number keys consistently across each line or area chart.\n- Pass the complete new data value to `update()`.\n- Keep chart options fixed after creation; recreate the chart to change them.\n- Dispose the handle when its host leaves the owning view.\n- Use `transition: false` for deterministic synchronous rendering.\n- Prefer CSS custom properties for reusable theme overrides.\n",
7
7
  "examples": "---\ntitle: 'Prism: Examples'\ndescription: Focused recipes for creating, updating, interacting with, and theming Prism charts.\n---\n\n- [Create a line chart](./examples/line-chart.md)\n- [Plot a time series](./examples/time-series.md)\n- [Plot a scatter chart](./examples/scatter-chart.md)\n- [Compare categories with a bar chart](./examples/bar-chart.md)\n- [Render compact charts](./examples/compact-charts.md)\n- [Compare hero stats with a radar chart](./examples/radar-chart.md)\n- [Show a win-rate matrix with a heatmap](./examples/heatmap-chart.md)\n- [Update chart data](./examples/update-data.md)\n- [Sync a details panel with hover](./examples/hover-details.md)\n- [Render a custom tooltip](./examples/custom-tooltip.md)\n- [Match charts to your design system](./examples/theming.md)\n"
8
8
  },
9
9
  "examples": [],
@@ -1,9 +1,9 @@
1
1
  {
2
2
  "apiSource": "export {\n PulseAbortError,\n PulseConnectionError,\n PulseDisposedError,\n PulseError,\n PulseProtocolError,\n PulseRoomTimeoutError,\n PulseTimeoutError,\n} from './errors';\nexport { createPulse } from './pulse';\nexport type {\n ChannelDefinition,\n ChannelDefinitions,\n ClientEvents,\n EventKey,\n HeartbeatOptions,\n MessageMap,\n OutgoingMessage,\n OutgoingTransform,\n PresenceRoomScope,\n Pulse,\n PulseChannel,\n PulseEvent,\n PulseOptions,\n PulseSchema,\n PulseStatus,\n ReconnectOptions,\n RoomDefinition,\n RoomDefinitions,\n RoomMap,\n RoomOptions,\n RoomScope,\n RoomScopeBase,\n ServerEvents,\n Subscribable,\n Unsubscribe,\n} from './types';\n",
3
3
  "docs": {
4
- "index": "---\ntitle: 'Pulse: Typed WebSocket sessions'\ndescription: Explicitly connected, typed WebSocket sessions with scoped channels, ref-counted rooms with reactive presence, reconnect restoration, and heartbeat.\npackage: pulse\ncategory: websockets\nkeywords: [websocket, realtime, channels, presence, rooms, reconnect, heartbeat, typed-messaging, ripple]\nrelated: [herald, ripple, courier, clockwork]\nexports:\n [\n createPulse,\n Pulse,\n PulseChannel,\n RoomScope,\n RoomScopeBase,\n PresenceRoomScope,\n Subscribable,\n PulseOptions,\n PulseSchema,\n ChannelDefinition,\n ChannelDefinitions,\n RoomDefinition,\n RoomDefinitions,\n RoomOptions,\n OutgoingMessage,\n OutgoingTransform,\n PulseError,\n PulseConnectionError,\n PulseTimeoutError,\n PulseRoomTimeoutError,\n PulseAbortError,\n PulseDisposedError,\n PulseProtocolError,\n PulseEvent,\n ]\nenvironments: [browser, node]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"pulse\" />\n\n## Why Pulse?\n\nNative WebSocket leaves connection ownership, event routing, reconnect restoration, and cleanup to each application. Pulse provides those boundaries while making readiness explicit: applications connect before sending, and disconnected messages never disappear silently.\n\n```ts\n// Before\nconst socket = new WebSocket('wss://api.example.com/ws');\nsocket.addEventListener('message', (event) => route(JSON.parse(event.data)));\nsocket.addEventListener('close', () => setTimeout(() => reconnect(), 1_000));\n\n// After\nconst pulse = createPulse<{ server: { 'chat:message': { text: string } }; client: { 'chat:send': { text: string } } }>(\n 'wss://api.example.com/ws',\n { reconnect: true },\n);\ntry {\n await pulse.connect();\n pulse.on('chat:message', (message) => console.log(message.text));\n pulse.send('chat:send', { text: 'Hello!' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n```\n\n| Feature | Pulse | Native WebSocket | socket.io-client |\n| --- | --- | --- | --- |\n| Bundle size | <PackageInfo package=\"pulse\" type=\"size\" /> | 0 B | ~44 kB gzip |\n| Explicit readiness | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Session restoration | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Protocol-specific |\n| Typed scoped channels | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Basic |\n| Typed rooms with presence | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Pulse when** you need a typed WebSocket session whose reconnect and cleanup behavior must be deterministic.\n\n**Consider native WebSocket when** a single untyped connection does not need retry, routing, or session restoration.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/pulse\n```\n\n```sh [npm]\nnpm install @vielzeug/pulse\n```\n\n```sh [yarn]\nyarn add @vielzeug/pulse\n```\n\n:::\n\n## Quick Start\n\nDefine the protocol schema at construction time, create scopes, then connect before sending.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype Schema = {\n server: { 'chat:message': { text: string } };\n client: { 'chat:send': { text: string } };\n channels: {\n chat: {\n client: { send: { text: string } };\n server: { message: { text: string } };\n };\n };\n rooms: {\n lobby: { presence: { name: string } };\n };\n};\n\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: true,\n});\npulse.tap((event) => {\n if (event.type === 'error') console.error(event.error);\n if (event.type === 'status-change') console.log('status:', event.status);\n});\nconst chat = pulse.channel('chat');\nconst lobby = pulse.room('lobby');\n\ntry {\n await pulse.connect();\n chat.send('send', { text: 'Hello!' });\n await lobby.joined;\n lobby.updatePresence({ name: 'Ada' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n\npulse.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`connect()`**: explicit readiness; application messages throw while disconnected.\n- **`channel()`**: named, schema-bound scopes with independent disposal and reference-counted server subscriptions.\n- **`room()`**: named, schema-bound ref-counted room scopes with optional reactive presence. The first scope sends `join`; the last disposal sends `leave`.\n- **`reconnect`**: ordered restoration of channel subscriptions, room memberships, and local presence state.\n- **`transform`**: one synchronous transform or filter for application messages.\n- **`tap()`**: subscribe to lifecycle events (status changes, errors, disposal) via a typed `PulseEvent` stream.\n- **`heartbeat`**: ping/pong liveness detection that uses the same reconnect controller.\n- **`status` and `rooms`**: framework-neutral external stores for transport and confirmed membership state.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/): bridges Pulse external stores into computed values and effects.\n- [Herald](/herald/): receives routed Pulse events in an in-process application bus.\n- [Courier](/courier/): handles request/response traffic alongside a Pulse session.\n- [Clockwork](/clockwork/): models application-level authentication or session workflows.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
- "api": "---\ntitle: 'API: Pulse'\ndescription: Complete API reference for Pulse, including schema types, options, scopes, and error classes.\npackage: pulse\ncategory: websockets\n---\n\n<!-- markdownlint-disable MD025 -->\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --- | --- | --- | --- |\n| `createPulse()` | Create a typed WebSocket session instance. | Sync (returns `Pulse`) | Does not open the connection: call `connect()`. |\n| `Pulse` | Main instance: channels, rooms, messaging, lifecycle. | Sync methods, async `connect()`/`wait()` | `send()` throws while disconnected. |\n| `PulseChannel` | Scoped channel namespace with independent disposal. | Sync methods, async `wait()` | Each call returns a new scope; ref-counted subscription. |\n| `RoomScope` | Ref-counted room membership with optional presence. | Sync methods, async `joined` | `joined` rejects on transport close or timeout. |\n| `PulseSchema` | Declares server/client events, channels, and rooms. | Type-only | Infer all named scope types from this schema. |\n| `PulseOptions` | Configuration: heartbeat, reconnect, transform. | Type-only | `reconnect` and `heartbeat` default to `false`. |\n| `Subscribable` | Framework-neutral snapshot and change subscription. | Sync | `subscribe()` does not emit the initial snapshot. |\n| `PulseError` | Base class for all Pulse errors. | Runtime | Check `instanceof` against subclasses. |\n\n## Package Entry Point\n\n| Import | Purpose |\n| --- | --- |\n| `@vielzeug/pulse` | All public exports: `createPulse`, types, and error classes. |\n| `@vielzeug/pulse/testing` | Wire-protocol-accurate `MockWebSocket` for tests and demos. |\n\n## `createPulse()`\n\n```ts\nfunction createPulse<S extends PulseSchema = PulseSchema>(url: string, options?: PulseOptions): Pulse<S>\n```\n\nCreates a Pulse instance. The WebSocket is not opened until `connect()` is called.\n\n### Type parameters\n\n| Parameter | Constraint | Description |\n| --- | --- | --- |\n| `S` | `PulseSchema` | Schema declaring server events, client events, channels, and rooms. |\n\n### Parameters\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `url` | `string` | WebSocket URL. |\n| `options` | `PulseOptions` | Optional configuration. |\n\n### Returns\n\n`Pulse<S>`: the Pulse instance.\n\n---\n\n## `PulseSchema`\n\n```ts\ntype PulseSchema = {\n server?: MessageMap;\n client?: MessageMap;\n channels?: ChannelDefinitions;\n rooms?: RoomDefinitions;\n};\n```\n\nDeclare all protocol surfaces once at construction. Named scopes infer their types from this schema.\n\n| Field | Type | Description |\n| --- | --- | --- |\n| `server` | `MessageMap` | Root events the server sends. |\n| `client` | `MessageMap` | Root events the client sends. |\n| `channels` | `ChannelDefinitions` | Named channel schemas. |\n| `rooms` | `RoomDefinitions` | Named room schemas with optional presence. |\n\n---\n\n## `PulseOptions`\n\n```ts\ntype PulseOptions = {\n heartbeat?: boolean | HeartbeatOptions;\n protocols?: string | string[];\n reconnect?: boolean | ReconnectOptions;\n transform?: OutgoingTransform;\n};\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `heartbeat` | `boolean \\| HeartbeatOptions` | `false` | Ping/pong keep-alive. |\n| `protocols` | `string \\| string[]` | N/A | Sub-protocols passed to the WebSocket constructor. |\n| `reconnect` | `boolean \\| ReconnectOptions` | `false` | Auto-reconnect on unexpected close. |\n| `transform` | `OutgoingTransform` | N/A | Transform or filter outgoing application messages. |\n\n---\n\n## `HeartbeatOptions`\n\n```ts\ntype HeartbeatOptions = {\n interval?: number;\n timeout?: number;\n};\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `interval` | `number` | `30_000` | Interval between pings in ms. |\n| `timeout` | `number` | `5_000` | How long to wait for a pong before treating the connection as dead. |\n\n---\n\n## `ReconnectOptions`\n\n```ts\ntype ReconnectOptions = {\n delay?: number | ((attempt: number) => number);\n maxAttempts?: number;\n};\n```\n\n| Option | Type | Default | Description |\n| --- | --- | --- | --- |\n| `delay` | `number \\| ((attempt: number) => number)` | Full-jitter exponential backoff capped at 30 s | Delay between reconnect attempts in ms. `attempt` is zero-based. |\n| `maxAttempts` | `number` | `5` | Maximum number of reconnect attempts after initial failure. |\n\n---\n\n## `OutgoingMessage`\n\n```ts\ntype OutgoingMessage = { channel?: string; event: string; payload: unknown };\n```\n\nAn outgoing application message before it is serialized.\n\n---\n\n## `OutgoingTransform`\n\n```ts\ntype OutgoingTransform = (message: Readonly<OutgoingMessage>) => OutgoingMessage | null;\n```\n\nTransform or filter outgoing application messages. Internal protocol frames (subscribe, join, leave, presence, ping) bypass this hook. Return `null` to drop the message.\n\n---\n\n## `Pulse`\n\n```ts\ntype Pulse<S extends PulseSchema = PulseSchema> = {\n // Channels\n channel<K extends keyof ChannelMap<S> & string>(\n name: K,\n ): PulseChannel<ChannelMap<S>[K]['server'], ChannelMap<S>[K]['client']>;\n\n // Connection\n connect(): Promise<void>;\n disconnect(code?: number, reason?: string): void;\n\n // Lifecycle\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n\n // Messaging\n on<K extends EventKey<ServerEvents<S>>>(event: K, handler: (payload: ServerEvents<S>[K]) => void): Unsubscribe;\n once<K extends EventKey<ServerEvents<S>>>(event: K, handler: (payload: ServerEvents<S>[K]) => void): Unsubscribe;\n send<K extends EventKey<ClientEvents<S>>>(event: K, payload: ClientEvents<S>[K]): void;\n wait<K extends EventKey<ServerEvents<S>>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<ServerEvents<S>[K]>;\n\n // Rooms\n room<K extends keyof RoomMap<S> & string>(name: K, opts?: RoomOptions): RoomScope<RoomMap<S>[K]>;\n readonly rooms: Subscribable<ReadonlySet<string>>;\n\n // Status\n readonly status: Subscribable<PulseStatus>;\n\n // Tap\n tap(handler: (event: PulseEvent) => void, options?: { signal?: AbortSignal }): () => void;\n\n [Symbol.dispose](): void;\n};\n```\n\n### `channel(name)`\n\nCreates an isolated message namespace over the shared connection. Each call returns an independently disposable scope. The server subscription is reference-counted.\n\n### `connect()`\n\nExplicitly opens the connection. Resolves after session restoration completes. Rejects if the connection closes before opening.\n\n### `disconnect(code?, reason?)`\n\nCloses the connection without triggering reconnection. Default code is `1000`.\n\n### `dispose()`\n\nPermanently closes the connection and releases all resources. Idempotent.\n\n### `on(event, handler)`\n\nSubscribes to a typed server event. Returns an unsubscribe function.\n\n### `once(event, handler)`\n\nSubscribes once: auto-removes after first invocation.\n\n### `send(event, payload)`\n\nSends a typed event to the server. Throws `PulseConnectionError` unless the connection is open.\n\n### `wait(event, opts?)`\n\nResolves on the next emission of the given server event. Rejects when `opts.signal` aborts, the timeout elapses, or the instance is disposed.\n\n### `room(name, opts?)`\n\nCreates a ref-counted room scope. The first scope sends `join`; the last disposal sends `leave`. When the room definition includes `presence`, the scope exposes reactive presence state.\n\n### `rooms`\n\nReactive set of rooms the client is currently a confirmed member of.\n\n### `status`\n\nReactive connection status: `'connecting' | 'open' | 'reconnecting' | 'closed'`.\n\n### `tap(handler, options?)`\n\nSubscribes to lifecycle events emitted by the Pulse instance. The handler receives a discriminated-union `PulseEvent`. Returns an unsubscribe function.\n\n| Parameter | Type | Description |\n| --- | --- | --- |\n| `handler` | `(event: PulseEvent) => void` | Called for each lifecycle event. |\n| `options.signal` | `AbortSignal` | Optional signal to stop the subscription. |\n\n```ts\nconst pulse = createPulse(url, { reconnect: true });\npulse.tap((event) => {\n if (event.type === 'error') console.error(event.error);\n if (event.type === 'status-change') console.log('status:', event.status);\n});\n```\n\n---\n\n## `Subscribable`\n\n```ts\ninterface Subscribable<T> {\n getSnapshot(): T;\n subscribe(listener: () => void): Unsubscribe;\n}\n```\n\nA framework-neutral state source used by `pulse.status`, `pulse.rooms`, and room `presence`. `getSnapshot()` returns the current immutable snapshot. `subscribe()` notifies once for each distinct snapshot and returns an unsubscribe function; listener errors are reported through `pulse.tap()` without interrupting Pulse state transitions.\n\n---\n\n## `PulseEvent`\n\n```ts\ntype PulseEvent =\n | { type: 'status-change'; status: PulseStatus }\n | { type: 'error'; error: PulseError }\n | { type: 'dispose' };\n```\n\nA discriminated union of lifecycle events emitted by a `Pulse` instance. Inspect `event.type` to narrow the payload.\n\n| `type` | Payload | When |\n| --- | --- | --- |\n| `status-change` | `status: PulseStatus` | The connection status transitions. |\n| `error` | `error: PulseError` | A typed transport or protocol error occurs. |\n| `dispose` | N/A | The instance is disposed. |\n\n---\n\n## `PulseChannel`\n\n```ts\ntype PulseChannel<TServer extends MessageMap = MessageMap, TClient extends MessageMap = MessageMap> = {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly name: string;\n dispose(): void;\n on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void;\n wait<K extends EventKey<TServer>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<TServer[K]>;\n [Symbol.dispose](): void;\n};\n```\n\n---\n\n## `RoomScope`\n\n```ts\ntype RoomScope<R extends RoomDefinition = RoomDefinition> = R extends { presence: infer P }\n ? P extends undefined\n ? RoomScopeBase\n : PresenceRoomScope<P>\n : RoomScopeBase;\n```\n\nA room scope. When the room definition includes `presence`, the scope is a `PresenceRoomScope`; otherwise it is a `RoomScopeBase`.\n\n### `RoomScopeBase`\n\n```ts\ntype RoomScopeBase = {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly name: string;\n readonly joined: Promise<void>;\n dispose(): void;\n [Symbol.dispose](): void;\n};\n```\n\n### `PresenceRoomScope`\n\n```ts\ntype PresenceRoomScope<T = unknown> = RoomScopeBase & {\n readonly presence: Subscribable<ReadonlyMap<string, T>>;\n updatePresence(state: T): void;\n onJoin(handler: (memberId: string, state: T) => void): Unsubscribe;\n onLeave(handler: (memberId: string) => void): Unsubscribe;\n};\n```\n\n| Member | Type | Description |\n| --- | --- | --- |\n| `presence` | `Subscribable<ReadonlyMap<string, T>>` | Reactive map of `memberId → state`. |\n| `updatePresence(state)` | `(state: T) => void` | Broadcast this client's presence state. Throws `PulseConnectionError` unless open. |\n| `onJoin(handler)` | `(handler) => Unsubscribe` | Called whenever a new member joins with their initial state. |\n| `onLeave(handler)` | `(handler) => Unsubscribe` | Called whenever a member leaves. |\n\n### `RoomOptions`\n\n```ts\ntype RoomOptions = {\n signal?: AbortSignal;\n timeout?: number;\n};\n```\n\n| Option | Type | Description |\n| --- | --- | --- |\n| `signal` | `AbortSignal` | Aborts the join, rejecting `joined` with `PulseAbortError`. |\n| `timeout` | `number` | Join timeout in ms. Rejects `joined` with `PulseRoomTimeoutError`. |\n\n---\n\n## Errors\n\nAll errors extend `PulseError`.\n\n### `PulseError`\n\nBase class for all Pulse errors.\n\n### `PulseConnectionError`\n\nTransport failure, send while disconnected, or room join rejected on close.\n\n### `PulseProtocolError`\n\nMalformed frame or server error frame.\n\n### `PulseTimeoutError`\n\n`wait()` timed out before the server event arrived.\n\n### `PulseRoomTimeoutError`\n\nRoom scope `joined` timed out before the server confirmed membership.\n\n### `PulseAbortError`\n\n`wait()` or room `joined` aborted via AbortSignal.\n\n### `PulseDisposedError`\n\nOperation attempted after disposal.\n\n---\n\n## Channel and room definitions\n\n### `ChannelDefinition`\n\n```ts\ntype ChannelDefinition = { client: MessageMap; server: MessageMap };\n```\n\n### `ChannelDefinitions`\n\n```ts\ntype ChannelDefinitions = Record<string, ChannelDefinition>;\n```\n\n### `RoomDefinition`\n\n```ts\ntype RoomDefinition = { presence?: unknown };\n```\n\n### `RoomDefinitions`\n\n```ts\ntype RoomDefinitions = Record<string, RoomDefinition>;\n```\n\n---\n\n## Utility types\n\n### `MessageMap`\n\n```ts\ntype MessageMap = Record<string, unknown>;\n```\n\n### `EventKey`\n\n```ts\ntype EventKey<T extends MessageMap> = keyof T & string;\n```\n\n### `ServerEvents`\n\n```ts\ntype ServerEvents<S extends PulseSchema> = S extends { server: infer M extends MessageMap } ? M : MessageMap;\n```\n\nExtract server events from a schema, defaulting to an empty map.\n\n### `ClientEvents`\n\n```ts\ntype ClientEvents<S extends PulseSchema> = S extends { client: infer M extends MessageMap } ? M : MessageMap;\n```\n\nExtract client events from a schema, defaulting to an empty map.\n\n### `RoomMap`\n\n```ts\ntype RoomMap<S extends PulseSchema> = S extends { rooms: infer R extends RoomDefinitions } ? R : RoomDefinitions;\n```\n\nExtract room definitions from a schema, defaulting to an empty map.\n\n### `Unsubscribe`\n\n```ts\ntype Unsubscribe = () => void;\n```\n\n### `PulseStatus`\n\n```ts\ntype PulseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';\n```\n\n---\n\n## Testing APIs\n\nImport from `@vielzeug/pulse/testing`.\n\n### `MockWebSocket`\n\n```ts\nimport { MockWebSocket } from '@vielzeug/pulse/testing';\n\n// Tests: drive the socket pulse created\nconst socket = MockWebSocket.instances.at(-1)!;\nsocket.open();\nsocket.receive({ type: 'joined', room: 'crm' });\n\n// Demos: subclass and install as the global\nclass ScriptedWebSocket extends MockWebSocket {\n constructor(url: string, protocols?: string | string[]) {\n super(url, protocols, { autoOpen: true });\n setTimeout(() => this.receive({ room: 'crm', type: 'joined' }), 60);\n }\n}\n(globalThis as Record<string, unknown>).WebSocket = ScriptedWebSocket;\n```\n\nA wire-protocol-accurate `WebSocket` stand-in: pulse dials it through `new WebSocket(url, protocols)`, tests drive it through `open()`, `receive()`, `drop()`, `error()`, and `close()`, and every outbound frame lands in `sentMessages` for `frames()` to decode. Instances register on the static `MockWebSocket.instances` array in construction order, so a test can grab the socket pulse just created. `MockWebSocket.deferClose = true` parks closes in `CLOSING` until `finishClose()` runs, for testing reconnect timing. Requires a browser-like environment for the `CloseEvent`/`MessageEvent` globals.\n\n### `frames(socket)`\n\nDecodes every frame the socket sent, in order.\n\n```ts\nimport { frames, MockWebSocket } from '@vielzeug/pulse/testing';\n\nexpect(frames(MockWebSocket.instances[0]!)).toContainEqual({ room: 'lobby', type: 'join' });\n```\n",
6
- "usage": "---\ntitle: 'Usage: Pulse'\ndescription: Practical guide for connecting, sending, subscribing, joining rooms, and managing lifecycle with Pulse.\npackage: pulse\ncategory: websockets\n---\n\n<!-- markdownlint-disable MD025 -->\n\n[[toc]]\n\n## Basic Usage\n\nDeclare server events, client events, channel schemas, and room schemas once at construction. Named scopes infer their types from this schema.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype Schema = {\n // Root events the server sends\n server: { 'chat:message': { text: string }; notice: string };\n // Root events the client sends\n client: { 'chat:send': { text: string } };\n // Named channel scopes\n channels: {\n chat: {\n client: { send: { text: string } };\n server: { message: { text: string } };\n };\n alerts: {\n client: { subscribe: { topic: string } };\n server: { alert: { topic: string; severity: 'info' | 'warn' | 'error' } };\n };\n };\n // Named room scopes with optional presence state\n rooms: {\n lobby: { presence: { name: string; color: string } };\n announcements: {};\n };\n};\n```\n\n## Create and connect\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: { delay: 1_000, maxAttempts: 5 },\n heartbeat: { interval: 30_000, timeout: 5_000 },\n});\n\npulse.tap((event) => {\n if (event.type === 'error') console.error(event.error);\n if (event.type === 'status-change') console.log('status:', event.status);\n});\n\ntry {\n await pulse.connect();\n} catch (error) {\n console.error('Connection failed:', error);\n}\n```\n\n`connect()` opens the WebSocket and resolves after session restoration completes. `send()` throws `PulseConnectionError` while disconnected: Pulse never silently drops or buffers application messages.\n\n## Send and receive root events\n\n```ts\npulse.on('chat:message', (message) => console.log(message.text));\npulse.send('chat:send', { text: 'Hello!' });\n```\n\n## Channels\n\nEach `channel()` call returns an independently disposable scope. The server subscription is reference-counted: the first scope sends `subscribe`, the last disposal sends `unsubscribe`.\n\n```ts\nconst chat = pulse.channel('chat');\n\nchat.on('message', (message) => console.log(message.text));\nchat.send('send', { text: 'Hello!' });\n\n// Later\nchat.dispose();\n```\n\nUse `using` for automatic cleanup:\n\n```ts\n{\n using chat = pulse.channel('chat');\n chat.on('message', (message) => console.log(message.text));\n} // chat.dispose() called automatically\n```\n\n## Rooms and presence\n\nEach `room()` call returns a ref-counted room scope. The first scope sends `join`; the last disposal sends `leave`. When the room definition includes `presence`, the scope exposes reactive presence state.\n\n```ts\nconst lobby = pulse.room('lobby');\n\n// joined resolves when the server confirms membership\nawait lobby.joined;\n\n// Reactive presence map: memberId → state\nlobby.onJoin((memberId, state) => console.log(`${memberId} joined: ${state.name}`));\nlobby.onLeave((memberId) => console.log(`${memberId} left`));\n\n// Broadcast your presence\nlobby.updatePresence({ name: 'Ada', color: 'blue' });\n\n// Read current presence\nfor (const [memberId, state] of lobby.presence.getSnapshot()) {\n console.log(`${memberId}: ${state.name}`);\n}\n\n// Leave\nlobby.dispose();\n```\n\nPlain rooms (without presence) work the same way but don't expose presence members:\n\n```ts\nconst announcements = pulse.room('announcements');\nawait announcements.joined;\nannouncements.dispose();\n```\n\n### Room scope options\n\n```ts\n// Timeout if the server doesn't confirm in time\nconst lobby = pulse.room('lobby', { timeout: 5_000 });\ntry {\n await lobby.joined;\n} catch (error) {\n console.error('Join failed:', error);\n}\n\n// Abort via AbortSignal\nconst ctrl = new AbortController();\nconst lobby = pulse.room('lobby', { signal: ctrl.signal });\nctrl.abort(); // joined rejects with PulseAbortError, scope auto-disposes\n```\n\n### Observe the rooms set\n\n`pulse.rooms` is an external store that tracks confirmed room memberships:\n\n```ts\nconst printRooms = () => console.log('Joined rooms:', [...pulse.rooms.getSnapshot()]);\nconst unsubscribe = pulse.rooms.subscribe(printRooms);\n\nprintRooms();\n// Later\nunsubscribe();\n```\n\n## Reconnect\n\nWhen the connection drops unexpectedly, Pulse reconnects using the configured strategy. On reconnect, it restores:\n\n1. Channel subscriptions (sends `subscribe` for each active channel).\n2. Room memberships (sends `join` for each active room scope).\n3. Local presence state (sends `presence` with the last successfully published state).\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: {\n delay: (attempt) => Math.min(1_000 * 2 ** attempt, 30_000),\n maxAttempts: 5,\n },\n});\n```\n\n`joined` rejects on transport close. For post-reconnect membership, read `pulse.rooms` instead.\n\n## Heartbeat\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n heartbeat: { interval: 30_000, timeout: 5_000 },\n});\n```\n\nPulse sends periodic pings. If a pong doesn't arrive before the timeout, it forces a reconnect using the same reconnect controller.\n\n## Transform outgoing messages\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n transform: (message) => {\n // Add a timestamp to all messages\n return { ...message, payload: { ...message.payload, ts: Date.now() } };\n },\n});\n```\n\nReturn `null` to drop a message:\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n transform: (message) => (message.event === 'debug' ? null : message),\n});\n```\n\n## Wait for a specific event\n\n```ts\nconst notice = await pulse.wait('notice', { timeout: 10_000 });\nconsole.log(notice);\n```\n\n## Dispose\n\n```ts\npulse.dispose();\n```\n\nDisposal is idempotent. It closes the connection, rejects pending room joins, clears all listeners, and aborts all scope disposal signals.\n\n## Error handling\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: true,\n});\n\npulse.tap((event) => {\n if (event.type === 'error') {\n if (event.error instanceof PulseConnectionError) {\n console.error('Connection error:', event.error);\n } else if (event.error instanceof PulseProtocolError) {\n console.error('Protocol error:', event.error);\n }\n }\n});\n```\n\n| Error | When |\n| --- | --- |\n| `PulseConnectionError` | Transport failure, send while disconnected, room join rejected on close. |\n| `PulseProtocolError` | Malformed frame or server error frame. |\n| `PulseTimeoutError` | `wait()` times out. |\n| `PulseRoomTimeoutError` | Room scope `joined` times out. |\n| `PulseAbortError` | `wait()` or room `joined` aborted via AbortSignal. |\n| `PulseDisposedError` | Operation attempted after disposal. |\n\n## Working with Other Vielzeug Libraries\n\nPulse external stores plug directly into Flux and Ripple bridges:\n\n```ts\nimport { fromStore } from '@vielzeug/flux';\nimport { effect, fromSubscribable } from '@vielzeug/ripple';\n\nconst statusStream = fromStore(pulse.status);\nconst presence = fromSubscribable(lobby.presence, { signal: lobby.disposalSignal });\nconst presenceEffect = effect(() => console.log([...presence.value]));\n\nconst statusSubscription = statusStream.subscribe((status) => console.log(status));\n// Later\nstatusSubscription.unsubscribe();\npresenceEffect.dispose();\n```\n\n## Best Practices\n\n- Await `connect()` before sending; never assume construction opens the transport.\n- Define the full schema at `createPulse()` so named scopes are type-safe without per-call generics.\n- Use `using` declarations for channel and room scopes so disposal is automatic at block exit.\n- Always call `dispose()` when done: it closes the connection, rejects pending joins, and clears listeners.\n- Call `tap()` to observe lifecycle events; Pulse reports transport and protocol errors there rather than throwing asynchronously.\n- Read `pulse.rooms` for post-reconnect membership; `joined` rejects on transport close.\n- Set a `timeout` on room scopes when the server may never confirm membership.\n- Keep `transform` synchronous; resolve async policy decisions before calling `send()`.\n",
4
+ "index": "---\ntitle: 'Pulse: Typed WebSocket sessions'\ndescription: Explicitly connected, typed WebSocket sessions with scoped channels, ref-counted rooms with reactive presence, reconnect restoration, and heartbeat.\npackage: pulse\ncategory: websockets\nkeywords: [websocket, realtime, channels, presence, rooms, reconnect, heartbeat, typed-messaging, ripple]\nrelated: [herald, ripple, courier, clockwork]\nexports:\n [\n createPulse,\n Pulse,\n PulseChannel,\n RoomScope,\n RoomScopeBase,\n PresenceRoomScope,\n Subscribable,\n PulseOptions,\n PulseSchema,\n ChannelDefinition,\n ChannelDefinitions,\n RoomDefinition,\n RoomDefinitions,\n RoomOptions,\n OutgoingMessage,\n OutgoingTransform,\n PulseError,\n PulseConnectionError,\n PulseTimeoutError,\n PulseRoomTimeoutError,\n PulseAbortError,\n PulseDisposedError,\n PulseProtocolError,\n PulseEvent,\n ]\nenvironments: [browser, node]\n---\n\n<!-- markdownlint-disable MD025 MD033 MD060 -->\n\n<PackageHero package=\"pulse\" />\n\n## Why Pulse?\n\nNative WebSocket leaves connection ownership, event routing, reconnect restoration, and cleanup to each application. Pulse provides those boundaries while making readiness explicit: applications connect before sending, and disconnected messages never disappear silently.\n\n```ts\n// Before\nconst socket = new WebSocket('wss://api.example.com/ws');\nsocket.addEventListener('message', (event) => route(JSON.parse(event.data)));\nsocket.addEventListener('close', () => setTimeout(() => reconnect(), 1_000));\n\n// After\nconst pulse = createPulse<{ server: { 'chat:message': { text: string } }; client: { 'chat:send': { text: string } } }>(\n 'wss://api.example.com/ws',\n { reconnect: true },\n);\ntry {\n await pulse.connect();\n pulse.on('chat:message', (message) => console.log(message.text));\n pulse.send('chat:send', { text: 'Hello!' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n```\n\n| Feature | Pulse | Native WebSocket | socket.io-client |\n| ------------------------- | -------------------------------------------- | -------------------------------------------- | -------------------------------------------- |\n| Bundle size | <PackageInfo package=\"pulse\" type=\"size\" /> | 0 B | ~44 kB gzip |\n| Explicit readiness | <ore-icon name=\"check\" size=\"16\"></ore-icon> | Manual | <ore-icon name=\"check\" size=\"16\"></ore-icon> |\n| Session restoration | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Protocol-specific |\n| Typed scoped channels | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | Basic |\n| Typed rooms with presence | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n| Zero runtime dependencies | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"check\" size=\"16\"></ore-icon> | <ore-icon name=\"x\" size=\"16\"></ore-icon> |\n\n<div class=\"decision-callout\">\n\n**Use Pulse when** you need a typed WebSocket session whose reconnect and cleanup behavior must be deterministic.\n\n**Consider native WebSocket when** a single untyped connection does not need retry, routing, or session restoration.\n\n</div>\n\n## Installation\n\n::: code-group\n\n```sh [pnpm]\npnpm add @vielzeug/pulse\n```\n\n```sh [npm]\nnpm install @vielzeug/pulse\n```\n\n```sh [yarn]\nyarn add @vielzeug/pulse\n```\n\n:::\n\n## Quick Start\n\nDefine the protocol schema at construction time, create scopes, then connect before sending.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype Schema = {\n server: { 'chat:message': { text: string } };\n client: { 'chat:send': { text: string } };\n channels: {\n chat: {\n client: { send: { text: string } };\n server: { message: { text: string } };\n };\n };\n rooms: {\n lobby: { presence: { name: string } };\n };\n};\n\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: true,\n});\npulse.tap((event) => {\n if (event.type === 'error') console.error(event.error);\n if (event.type === 'status-change') console.log('status:', event.status);\n});\nconst chat = pulse.channel('chat');\nconst lobby = pulse.room('lobby');\n\ntry {\n await pulse.connect();\n chat.send('send', { text: 'Hello!' });\n await lobby.joined;\n lobby.updatePresence({ name: 'Ada' });\n} catch (error) {\n console.error('Pulse connection failed:', error);\n}\n\npulse.dispose();\n```\n\n## Features\n\n<div class=\"features-grid\">\n\n- **`connect()`**: explicit readiness; application messages throw while disconnected.\n- **`channel()`**: named, schema-bound scopes with independent disposal and reference-counted server subscriptions.\n- **`room()`**: named, schema-bound ref-counted room scopes with optional reactive presence. The first scope sends `join`; the last disposal sends `leave`.\n- **`reconnect`**: ordered restoration of channel subscriptions, room memberships, and local presence state.\n- **`transform`**: one synchronous transform or filter for application messages.\n- **`tap()`**: subscribe to lifecycle events (status changes, errors, disposal) via a typed `PulseEvent` stream.\n- **`heartbeat`**: ping/pong liveness detection that uses the same reconnect controller.\n- **`status` and `rooms`**: framework-neutral external stores for transport and confirmed membership state.\n\n</div>\n\n## Documentation\n\n<div class=\"doc-links\">\n\n- [Usage Guide](./usage.md)\n- [API Reference](./api.md)\n- [Examples](./examples.md)\n- [Migration Guide](./migration.md)\n\n</div>\n\n## See Also\n\n<div class=\"see-also\">\n\n- [Ripple](/ripple/): bridges Pulse external stores into computed values and effects.\n- [Herald](/herald/): receives routed Pulse events in an in-process application bus.\n- [Courier](/courier/): handles request/response traffic alongside a Pulse session.\n- [Clockwork](/clockwork/): models application-level authentication or session workflows.\n\n</div>\n\n<!-- markdownlint-enable MD025 MD033 MD060 -->\n",
5
+ "api": "---\ntitle: 'API: Pulse'\ndescription: Complete API reference for Pulse, including schema types, options, scopes, and error classes.\npackage: pulse\ncategory: websockets\n---\n\n<!-- markdownlint-disable MD025 -->\n\n[[toc]]\n\n## API Overview\n\n| Symbol | Purpose | Execution mode | Common gotcha |\n| --------------- | ----------------------------------------------------- | ---------------------------------------- | -------------------------------------------------------- |\n| `createPulse()` | Create a typed WebSocket session instance. | Sync (returns `Pulse`) | Does not open the connection: call `connect()`. |\n| `Pulse` | Main instance: channels, rooms, messaging, lifecycle. | Sync methods, async `connect()`/`wait()` | `send()` throws while disconnected. |\n| `PulseChannel` | Scoped channel namespace with independent disposal. | Sync methods, async `wait()` | Each call returns a new scope; ref-counted subscription. |\n| `RoomScope` | Ref-counted room membership with optional presence. | Sync methods, async `joined` | `joined` rejects on transport close or timeout. |\n| `PulseSchema` | Declares server/client events, channels, and rooms. | Type-only | Infer all named scope types from this schema. |\n| `PulseOptions` | Configuration: heartbeat, reconnect, transform. | Type-only | `reconnect` and `heartbeat` default to `false`. |\n| `Subscribable` | Framework-neutral snapshot and change subscription. | Sync | `subscribe()` does not emit the initial snapshot. |\n| `PulseError` | Base class for all Pulse errors. | Runtime | Check `instanceof` against subclasses. |\n\n## Package Entry Point\n\n| Import | Purpose |\n| ------------------------- | ------------------------------------------------------------ |\n| `@vielzeug/pulse` | All public exports: `createPulse`, types, and error classes. |\n| `@vielzeug/pulse/testing` | Wire-protocol-accurate `MockWebSocket` for tests and demos. |\n\n## `createPulse()`\n\n```ts\nfunction createPulse<S extends PulseSchema = PulseSchema>(url: string, options?: PulseOptions): Pulse<S>;\n```\n\nCreates a Pulse instance. The WebSocket is not opened until `connect()` is called.\n\n### Type parameters\n\n| Parameter | Constraint | Description |\n| --------- | ------------- | ------------------------------------------------------------------- |\n| `S` | `PulseSchema` | Schema declaring server events, client events, channels, and rooms. |\n\n### Parameters\n\n| Parameter | Type | Description |\n| --------- | -------------- | ----------------------- |\n| `url` | `string` | WebSocket URL. |\n| `options` | `PulseOptions` | Optional configuration. |\n\n### Returns\n\n`Pulse<S>`: the Pulse instance.\n\n---\n\n## `PulseSchema`\n\n```ts\ntype PulseSchema = {\n server?: MessageMap;\n client?: MessageMap;\n channels?: ChannelDefinitions;\n rooms?: RoomDefinitions;\n};\n```\n\nDeclare all protocol surfaces once at construction. Named scopes infer their types from this schema.\n\n| Field | Type | Description |\n| ---------- | -------------------- | ------------------------------------------ |\n| `server` | `MessageMap` | Root events the server sends. |\n| `client` | `MessageMap` | Root events the client sends. |\n| `channels` | `ChannelDefinitions` | Named channel schemas. |\n| `rooms` | `RoomDefinitions` | Named room schemas with optional presence. |\n\n---\n\n## `PulseOptions`\n\n```ts\ntype PulseOptions = {\n heartbeat?: boolean | HeartbeatOptions;\n protocols?: string | string[];\n reconnect?: boolean | ReconnectOptions;\n transform?: OutgoingTransform;\n};\n```\n\n| Option | Type | Default | Description |\n| ----------- | ----------------------------- | ------- | -------------------------------------------------- |\n| `heartbeat` | `boolean \\| HeartbeatOptions` | `false` | Ping/pong keep-alive. |\n| `protocols` | `string \\| string[]` | N/A | Sub-protocols passed to the WebSocket constructor. |\n| `reconnect` | `boolean \\| ReconnectOptions` | `false` | Auto-reconnect on unexpected close. |\n| `transform` | `OutgoingTransform` | N/A | Transform or filter outgoing application messages. |\n\n---\n\n## `HeartbeatOptions`\n\n```ts\ntype HeartbeatOptions = {\n interval?: number;\n timeout?: number;\n};\n```\n\n| Option | Type | Default | Description |\n| ---------- | -------- | -------- | ------------------------------------------------------------------- |\n| `interval` | `number` | `30_000` | Interval between pings in ms. |\n| `timeout` | `number` | `5_000` | How long to wait for a pong before treating the connection as dead. |\n\n---\n\n## `ReconnectOptions`\n\n```ts\ntype ReconnectOptions = {\n delay?: number | ((attempt: number) => number);\n maxAttempts?: number;\n};\n```\n\n| Option | Type | Default | Description |\n| ------------- | ----------------------------------------- | ---------------------------------------------- | ---------------------------------------------------------------- |\n| `delay` | `number \\| ((attempt: number) => number)` | Full-jitter exponential backoff capped at 30 s | Delay between reconnect attempts in ms. `attempt` is zero-based. |\n| `maxAttempts` | `number` | `5` | Maximum number of reconnect attempts after initial failure. |\n\n---\n\n## `OutgoingMessage`\n\n```ts\ntype OutgoingMessage = { channel?: string; event: string; payload: unknown };\n```\n\nAn outgoing application message before it is serialized.\n\n---\n\n## `OutgoingTransform`\n\n```ts\ntype OutgoingTransform = (message: Readonly<OutgoingMessage>) => OutgoingMessage | null;\n```\n\nTransform or filter outgoing application messages. Internal protocol frames (subscribe, join, leave, presence, ping) bypass this hook. Return `null` to drop the message.\n\n---\n\n## `Pulse`\n\n```ts\ntype Pulse<S extends PulseSchema = PulseSchema> = {\n // Channels\n channel<K extends keyof ChannelMap<S> & string>(\n name: K,\n ): PulseChannel<ChannelMap<S>[K]['server'], ChannelMap<S>[K]['client']>;\n\n // Connection\n connect(): Promise<void>;\n disconnect(code?: number, reason?: string): void;\n\n // Lifecycle\n readonly disposalSignal: AbortSignal;\n dispose(): void;\n readonly disposed: boolean;\n\n // Messaging\n on<K extends EventKey<ServerEvents<S>>>(event: K, handler: (payload: ServerEvents<S>[K]) => void): Unsubscribe;\n once<K extends EventKey<ServerEvents<S>>>(event: K, handler: (payload: ServerEvents<S>[K]) => void): Unsubscribe;\n send<K extends EventKey<ClientEvents<S>>>(event: K, payload: ClientEvents<S>[K]): void;\n wait<K extends EventKey<ServerEvents<S>>>(\n event: K,\n opts?: { signal?: AbortSignal; timeout?: number },\n ): Promise<ServerEvents<S>[K]>;\n\n // Rooms\n room<K extends keyof RoomMap<S> & string>(name: K, opts?: RoomOptions): RoomScope<RoomMap<S>[K]>;\n readonly rooms: Subscribable<ReadonlySet<string>>;\n\n // Status\n readonly status: Subscribable<PulseStatus>;\n\n // Tap\n tap(handler: (event: PulseEvent) => void, options?: { signal?: AbortSignal }): () => void;\n\n [Symbol.dispose](): void;\n};\n```\n\n### `channel(name)`\n\nCreates an isolated message namespace over the shared connection. Each call returns an independently disposable scope. The server subscription is reference-counted.\n\n### `connect()`\n\nExplicitly opens the connection. Resolves after session restoration completes. Rejects if the connection closes before opening.\n\n### `disconnect(code?, reason?)`\n\nCloses the connection without triggering reconnection. Default code is `1000`.\n\n### `dispose()`\n\nPermanently closes the connection and releases all resources. Idempotent.\n\n### `on(event, handler)`\n\nSubscribes to a typed server event. Returns an unsubscribe function.\n\n### `once(event, handler)`\n\nSubscribes once: auto-removes after first invocation.\n\n### `send(event, payload)`\n\nSends a typed event to the server. Throws `PulseConnectionError` unless the connection is open.\n\n### `wait(event, opts?)`\n\nResolves on the next emission of the given server event. Rejects when `opts.signal` aborts, the timeout elapses, or the instance is disposed.\n\n### `room(name, opts?)`\n\nCreates a ref-counted room scope. The first scope sends `join`; the last disposal sends `leave`. When the room definition includes `presence`, the scope exposes reactive presence state.\n\n### `rooms`\n\nReactive set of rooms the client is currently a confirmed member of.\n\n### `status`\n\nReactive connection status: `'connecting' | 'open' | 'reconnecting' | 'closed'`.\n\n### `tap(handler, options?)`\n\nSubscribes to lifecycle events emitted by the Pulse instance. The handler receives a discriminated-union `PulseEvent`. Returns an unsubscribe function.\n\n| Parameter | Type | Description |\n| ---------------- | ----------------------------- | ----------------------------------------- |\n| `handler` | `(event: PulseEvent) => void` | Called for each lifecycle event. |\n| `options.signal` | `AbortSignal` | Optional signal to stop the subscription. |\n\n```ts\nconst pulse = createPulse(url, { reconnect: true });\npulse.tap((event) => {\n if (event.type === 'error') console.error(event.error);\n if (event.type === 'status-change') console.log('status:', event.status);\n});\n```\n\n---\n\n## `Subscribable`\n\n```ts\ninterface Subscribable<T> {\n getSnapshot(): T;\n subscribe(listener: () => void): Unsubscribe;\n}\n```\n\nA framework-neutral state source used by `pulse.status`, `pulse.rooms`, and room `presence`. `getSnapshot()` returns the current immutable snapshot. `subscribe()` notifies once for each distinct snapshot and returns an unsubscribe function; listener errors are reported through `pulse.tap()` without interrupting Pulse state transitions.\n\n---\n\n## `PulseEvent`\n\n```ts\ntype PulseEvent =\n | { type: 'status-change'; status: PulseStatus }\n | { type: 'error'; error: PulseError }\n | { type: 'dispose' };\n```\n\nA discriminated union of lifecycle events emitted by a `Pulse` instance. Inspect `event.type` to narrow the payload.\n\n| `type` | Payload | When |\n| --------------- | --------------------- | ------------------------------------------- |\n| `status-change` | `status: PulseStatus` | The connection status transitions. |\n| `error` | `error: PulseError` | A typed transport or protocol error occurs. |\n| `dispose` | N/A | The instance is disposed. |\n\n---\n\n## `PulseChannel`\n\n```ts\ntype PulseChannel<TServer extends MessageMap = MessageMap, TClient extends MessageMap = MessageMap> = {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly name: string;\n dispose(): void;\n on<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n once<K extends EventKey<TServer>>(event: K, handler: (payload: TServer[K]) => void): Unsubscribe;\n send<K extends EventKey<TClient>>(event: K, payload: TClient[K]): void;\n wait<K extends EventKey<TServer>>(event: K, opts?: { signal?: AbortSignal; timeout?: number }): Promise<TServer[K]>;\n [Symbol.dispose](): void;\n};\n```\n\n---\n\n## `RoomScope`\n\n```ts\ntype RoomScope<R extends RoomDefinition = RoomDefinition> = R extends { presence: infer P }\n ? P extends undefined\n ? RoomScopeBase\n : PresenceRoomScope<P>\n : RoomScopeBase;\n```\n\nA room scope. When the room definition includes `presence`, the scope is a `PresenceRoomScope`; otherwise it is a `RoomScopeBase`.\n\n### `RoomScopeBase`\n\n```ts\ntype RoomScopeBase = {\n readonly disposalSignal: AbortSignal;\n readonly disposed: boolean;\n readonly name: string;\n readonly joined: Promise<void>;\n dispose(): void;\n [Symbol.dispose](): void;\n};\n```\n\n### `PresenceRoomScope`\n\n```ts\ntype PresenceRoomScope<T = unknown> = RoomScopeBase & {\n readonly presence: Subscribable<ReadonlyMap<string, T>>;\n updatePresence(state: T): void;\n onJoin(handler: (memberId: string, state: T) => void): Unsubscribe;\n onLeave(handler: (memberId: string) => void): Unsubscribe;\n};\n```\n\n| Member | Type | Description |\n| ----------------------- | -------------------------------------- | ---------------------------------------------------------------------------------- |\n| `presence` | `Subscribable<ReadonlyMap<string, T>>` | Reactive map of `memberId → state`. |\n| `updatePresence(state)` | `(state: T) => void` | Broadcast this client's presence state. Throws `PulseConnectionError` unless open. |\n| `onJoin(handler)` | `(handler) => Unsubscribe` | Called whenever a new member joins with their initial state. |\n| `onLeave(handler)` | `(handler) => Unsubscribe` | Called whenever a member leaves. |\n\n### `RoomOptions`\n\n```ts\ntype RoomOptions = {\n signal?: AbortSignal;\n timeout?: number;\n};\n```\n\n| Option | Type | Description |\n| --------- | ------------- | ------------------------------------------------------------------ |\n| `signal` | `AbortSignal` | Aborts the join, rejecting `joined` with `PulseAbortError`. |\n| `timeout` | `number` | Join timeout in ms. Rejects `joined` with `PulseRoomTimeoutError`. |\n\n---\n\n## Errors\n\nAll errors extend `PulseError`.\n\n### `PulseError`\n\nBase class for all Pulse errors.\n\n### `PulseConnectionError`\n\nTransport failure, send while disconnected, or room join rejected on close.\n\n### `PulseProtocolError`\n\nMalformed frame or server error frame.\n\n### `PulseTimeoutError`\n\n`wait()` timed out before the server event arrived.\n\n### `PulseRoomTimeoutError`\n\nRoom scope `joined` timed out before the server confirmed membership.\n\n### `PulseAbortError`\n\n`wait()` or room `joined` aborted via AbortSignal.\n\n### `PulseDisposedError`\n\nOperation attempted after disposal.\n\n---\n\n## Channel and room definitions\n\n### `ChannelDefinition`\n\n```ts\ntype ChannelDefinition = { client: MessageMap; server: MessageMap };\n```\n\n### `ChannelDefinitions`\n\n```ts\ntype ChannelDefinitions = Record<string, ChannelDefinition>;\n```\n\n### `RoomDefinition`\n\n```ts\ntype RoomDefinition = { presence?: unknown };\n```\n\n### `RoomDefinitions`\n\n```ts\ntype RoomDefinitions = Record<string, RoomDefinition>;\n```\n\n---\n\n## Utility types\n\n### `MessageMap`\n\n```ts\ntype MessageMap = Record<string, unknown>;\n```\n\n### `EventKey`\n\n```ts\ntype EventKey<T extends MessageMap> = keyof T & string;\n```\n\n### `ServerEvents`\n\n```ts\ntype ServerEvents<S extends PulseSchema> = S extends { server: infer M extends MessageMap } ? M : MessageMap;\n```\n\nExtract server events from a schema, defaulting to an empty map.\n\n### `ClientEvents`\n\n```ts\ntype ClientEvents<S extends PulseSchema> = S extends { client: infer M extends MessageMap } ? M : MessageMap;\n```\n\nExtract client events from a schema, defaulting to an empty map.\n\n### `RoomMap`\n\n```ts\ntype RoomMap<S extends PulseSchema> = S extends { rooms: infer R extends RoomDefinitions } ? R : RoomDefinitions;\n```\n\nExtract room definitions from a schema, defaulting to an empty map.\n\n### `Unsubscribe`\n\n```ts\ntype Unsubscribe = () => void;\n```\n\n### `PulseStatus`\n\n```ts\ntype PulseStatus = 'connecting' | 'open' | 'reconnecting' | 'closed';\n```\n\n---\n\n## Testing APIs\n\nImport from `@vielzeug/pulse/testing`.\n\n### `MockWebSocket`\n\n```ts\nimport { MockWebSocket } from '@vielzeug/pulse/testing';\n\n// Tests: drive the socket pulse created\nconst socket = MockWebSocket.instances.at(-1)!;\nsocket.open();\nsocket.receive({ type: 'joined', room: 'crm' });\n\n// Demos: subclass and install as the global\nclass ScriptedWebSocket extends MockWebSocket {\n constructor(url: string, protocols?: string | string[]) {\n super(url, protocols, { autoOpen: true });\n setTimeout(() => this.receive({ room: 'crm', type: 'joined' }), 60);\n }\n}\n(globalThis as Record<string, unknown>).WebSocket = ScriptedWebSocket;\n```\n\nA wire-protocol-accurate `WebSocket` stand-in: pulse dials it through `new WebSocket(url, protocols)`, tests drive it through `open()`, `receive()`, `drop()`, `error()`, and `close()`, and every outbound frame lands in `sentMessages` for `frames()` to decode. Instances register on the static `MockWebSocket.instances` array in construction order, so a test can grab the socket pulse just created. `MockWebSocket.deferClose = true` parks closes in `CLOSING` until `finishClose()` runs, for testing reconnect timing. Requires a browser-like environment for the `CloseEvent`/`MessageEvent` globals.\n\n### `frames(socket)`\n\nDecodes every frame the socket sent, in order.\n\n```ts\nimport { frames, MockWebSocket } from '@vielzeug/pulse/testing';\n\nexpect(frames(MockWebSocket.instances[0]!)).toContainEqual({ room: 'lobby', type: 'join' });\n```\n",
6
+ "usage": "---\ntitle: 'Usage: Pulse'\ndescription: Practical guide for connecting, sending, subscribing, joining rooms, and managing lifecycle with Pulse.\npackage: pulse\ncategory: websockets\n---\n\n<!-- markdownlint-disable MD025 -->\n\n[[toc]]\n\n## Basic Usage\n\nDeclare server events, client events, channel schemas, and room schemas once at construction. Named scopes infer their types from this schema.\n\n```ts\nimport { createPulse } from '@vielzeug/pulse';\n\ntype Schema = {\n // Root events the server sends\n server: { 'chat:message': { text: string }; notice: string };\n // Root events the client sends\n client: { 'chat:send': { text: string } };\n // Named channel scopes\n channels: {\n chat: {\n client: { send: { text: string } };\n server: { message: { text: string } };\n };\n alerts: {\n client: { subscribe: { topic: string } };\n server: { alert: { topic: string; severity: 'info' | 'warn' | 'error' } };\n };\n };\n // Named room scopes with optional presence state\n rooms: {\n lobby: { presence: { name: string; color: string } };\n announcements: {};\n };\n};\n```\n\n## Create and connect\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: { delay: 1_000, maxAttempts: 5 },\n heartbeat: { interval: 30_000, timeout: 5_000 },\n});\n\npulse.tap((event) => {\n if (event.type === 'error') console.error(event.error);\n if (event.type === 'status-change') console.log('status:', event.status);\n});\n\ntry {\n await pulse.connect();\n} catch (error) {\n console.error('Connection failed:', error);\n}\n```\n\n`connect()` opens the WebSocket and resolves after session restoration completes. `send()` throws `PulseConnectionError` while disconnected: Pulse never silently drops or buffers application messages.\n\n## Send and receive root events\n\n```ts\npulse.on('chat:message', (message) => console.log(message.text));\npulse.send('chat:send', { text: 'Hello!' });\n```\n\n## Channels\n\nEach `channel()` call returns an independently disposable scope. The server subscription is reference-counted: the first scope sends `subscribe`, the last disposal sends `unsubscribe`.\n\n```ts\nconst chat = pulse.channel('chat');\n\nchat.on('message', (message) => console.log(message.text));\nchat.send('send', { text: 'Hello!' });\n\n// Later\nchat.dispose();\n```\n\nUse `using` for automatic cleanup:\n\n```ts\n{\n using chat = pulse.channel('chat');\n chat.on('message', (message) => console.log(message.text));\n} // chat.dispose() called automatically\n```\n\n## Rooms and presence\n\nEach `room()` call returns a ref-counted room scope. The first scope sends `join`; the last disposal sends `leave`. When the room definition includes `presence`, the scope exposes reactive presence state.\n\n```ts\nconst lobby = pulse.room('lobby');\n\n// joined resolves when the server confirms membership\nawait lobby.joined;\n\n// Reactive presence map: memberId → state\nlobby.onJoin((memberId, state) => console.log(`${memberId} joined: ${state.name}`));\nlobby.onLeave((memberId) => console.log(`${memberId} left`));\n\n// Broadcast your presence\nlobby.updatePresence({ name: 'Ada', color: 'blue' });\n\n// Read current presence\nfor (const [memberId, state] of lobby.presence.getSnapshot()) {\n console.log(`${memberId}: ${state.name}`);\n}\n\n// Leave\nlobby.dispose();\n```\n\nPlain rooms (without presence) work the same way but don't expose presence members:\n\n```ts\nconst announcements = pulse.room('announcements');\nawait announcements.joined;\nannouncements.dispose();\n```\n\n### Room scope options\n\n```ts\n// Timeout if the server doesn't confirm in time\nconst lobby = pulse.room('lobby', { timeout: 5_000 });\ntry {\n await lobby.joined;\n} catch (error) {\n console.error('Join failed:', error);\n}\n\n// Abort via AbortSignal\nconst ctrl = new AbortController();\nconst lobby = pulse.room('lobby', { signal: ctrl.signal });\nctrl.abort(); // joined rejects with PulseAbortError, scope auto-disposes\n```\n\n### Observe the rooms set\n\n`pulse.rooms` is an external store that tracks confirmed room memberships:\n\n```ts\nconst printRooms = () => console.log('Joined rooms:', [...pulse.rooms.getSnapshot()]);\nconst unsubscribe = pulse.rooms.subscribe(printRooms);\n\nprintRooms();\n// Later\nunsubscribe();\n```\n\n## Reconnect\n\nWhen the connection drops unexpectedly, Pulse reconnects using the configured strategy. On reconnect, it restores:\n\n1. Channel subscriptions (sends `subscribe` for each active channel).\n2. Room memberships (sends `join` for each active room scope).\n3. Local presence state (sends `presence` with the last successfully published state).\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: {\n delay: (attempt) => Math.min(1_000 * 2 ** attempt, 30_000),\n maxAttempts: 5,\n },\n});\n```\n\n`joined` rejects on transport close. For post-reconnect membership, read `pulse.rooms` instead.\n\n## Heartbeat\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n heartbeat: { interval: 30_000, timeout: 5_000 },\n});\n```\n\nPulse sends periodic pings. If a pong doesn't arrive before the timeout, it forces a reconnect using the same reconnect controller.\n\n## Transform outgoing messages\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n transform: (message) => {\n // Add a timestamp to all messages\n return { ...message, payload: { ...message.payload, ts: Date.now() } };\n },\n});\n```\n\nReturn `null` to drop a message:\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n transform: (message) => (message.event === 'debug' ? null : message),\n});\n```\n\n## Wait for a specific event\n\n```ts\nconst notice = await pulse.wait('notice', { timeout: 10_000 });\nconsole.log(notice);\n```\n\n## Dispose\n\n```ts\npulse.dispose();\n```\n\nDisposal is idempotent. It closes the connection, rejects pending room joins, clears all listeners, and aborts all scope disposal signals.\n\n## Error handling\n\n```ts\nconst pulse = createPulse<Schema>('wss://api.example.com/ws', {\n reconnect: true,\n});\n\npulse.tap((event) => {\n if (event.type === 'error') {\n if (event.error instanceof PulseConnectionError) {\n console.error('Connection error:', event.error);\n } else if (event.error instanceof PulseProtocolError) {\n console.error('Protocol error:', event.error);\n }\n }\n});\n```\n\n| Error | When |\n| ----------------------- | ------------------------------------------------------------------------ |\n| `PulseConnectionError` | Transport failure, send while disconnected, room join rejected on close. |\n| `PulseProtocolError` | Malformed frame or server error frame. |\n| `PulseTimeoutError` | `wait()` times out. |\n| `PulseRoomTimeoutError` | Room scope `joined` times out. |\n| `PulseAbortError` | `wait()` or room `joined` aborted via AbortSignal. |\n| `PulseDisposedError` | Operation attempted after disposal. |\n\n## Working with Other Vielzeug Libraries\n\nPulse external stores plug directly into Flux and Ripple bridges:\n\n```ts\nimport { fromStore } from '@vielzeug/flux';\nimport { effect, fromSubscribable } from '@vielzeug/ripple';\n\nconst statusStream = fromStore(pulse.status);\nconst presence = fromSubscribable(lobby.presence, { signal: lobby.disposalSignal });\nconst presenceEffect = effect(() => console.log([...presence.value]));\n\nconst statusSubscription = statusStream.subscribe((status) => console.log(status));\n// Later\nstatusSubscription.unsubscribe();\npresenceEffect.dispose();\n```\n\n## Best Practices\n\n- Await `connect()` before sending; never assume construction opens the transport.\n- Define the full schema at `createPulse()` so named scopes are type-safe without per-call generics.\n- Use `using` declarations for channel and room scopes so disposal is automatic at block exit.\n- Always call `dispose()` when done: it closes the connection, rejects pending joins, and clears listeners.\n- Call `tap()` to observe lifecycle events; Pulse reports transport and protocol errors there rather than throwing asynchronously.\n- Read `pulse.rooms` for post-reconnect membership; `joined` rejects on transport close.\n- Set a `timeout` on room scopes when the server may never confirm membership.\n- Keep `transform` synchronous; resolve async policy decisions before calling `send()`.\n",
7
7
  "examples": "---\ntitle: 'Examples: Pulse'\ndescription: Practical examples for common Pulse usage patterns.\npackage: pulse\ncategory: websockets\n---\n\n<!-- markdownlint-disable MD025 -->\n\n- [Basic Connection](./examples/basic-connection.md)\n- [Channel Multiplexing](./examples/channels.md)\n- [Outgoing Transform](./examples/middleware.md)\n- [Reconnect and Heartbeat](./examples/reconnect-and-heartbeat.md)\n- [Rooms and Presence](./examples/rooms-and-presence.md)\n"
8
8
  },
9
9
  "examples": [