@vielzeug/codex 26.10.3 → 26.10.5

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.
@@ -1,6 +1,6 @@
1
1
  # Vielzeug: Full Documentation
2
2
 
3
- > Complete documentation for 41 packages. Version: 26.10.2
3
+ > Complete documentation for 41 packages. Version: 26.10.4
4
4
 
5
5
  ---
6
6
 
@@ -16403,64 +16403,60 @@ The IndexedDB adapter is built on Vault. Use Vault directly for unrelated storag
16403
16403
 
16404
16404
  ## Why Prism?
16405
16405
 
16406
- Charting libraries typically require a framework binding, bundle heavy dependencies, or force canvas rendering that can't be styled with CSS. Prism takes a different approach:
16406
+ Prism 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.
16407
16407
 
16408
16408
  ```ts
16409
- // Before: Chart.js, imperative setup with a canvas you can't CSS-theme
16410
- import Chart from 'chart.js/auto';
16411
- const ctx = document.getElementById('myChart') as HTMLCanvasElement;
16412
- new Chart(ctx, {
16413
- type: 'line',
16414
- data: { labels, datasets: [{ data: values }] },
16415
- // re-create or mutate the Chart.js instance when data changes
16416
- });
16417
-
16418
- // After: Prism, responsive SVG with an explicit update boundary
16419
16409
  import { createLineChart } from '@vielzeug/prism';
16420
16410
 
16421
- const chart = createLineChart(document.getElementById('chart')!, {
16422
- a11y: { ariaLabel: 'Users by day' },
16423
- series: [
16424
- {
16425
- data: [
16426
- { key: 1, value: 12 },
16427
- { key: 2, value: 40 },
16428
- { key: 3, value: 28 },
16429
- ],
16430
- name: 'Users',
16431
- },
16432
- ],
16433
- tooltip: true,
16411
+ const host = document.createElement('div');
16412
+ host.style.cssText = 'width:640px;height:320px';
16413
+ document.body.append(host);
16414
+
16415
+ // Before: replace a hand-drawn SVG each time values change.
16416
+ function drawLine(values: number[]) {
16417
+ const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
16418
+ const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
16419
+ const low = Math.min(...values);
16420
+ const span = Math.max(...values) - low || 1;
16421
+ const points = values.map((value, index) => {
16422
+ const x = (index / Math.max(1, values.length - 1)) * 640;
16423
+ const y = 320 - ((value - low) / span) * 320;
16424
+ return `${index === 0 ? 'M' : 'L'}${x},${y}`;
16425
+ });
16426
+ svg.setAttribute('viewBox', '0 0 640 320');
16427
+ path.setAttribute('d', points.join(' '));
16428
+ path.setAttribute('fill', 'none');
16429
+ path.setAttribute('stroke', 'currentColor');
16430
+ svg.append(path);
16431
+ host.replaceChildren(svg);
16432
+ }
16433
+ drawLine([10, 25, 18]);
16434
+ drawLine([10, 25, 18, 32]);
16435
+
16436
+ // After: keep the chart instance and replace its series data.
16437
+ const chart = createLineChart(host, {
16438
+ a11y: { ariaLabel: 'Revenue by month' },
16439
+ series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }], name: 'Revenue' }],
16434
16440
  });
16435
-
16436
- chart.update([
16437
- {
16438
- data: [
16439
- { key: 1, value: 12 },
16440
- { key: 2, value: 40 },
16441
- { key: 3, value: 28 },
16442
- { key: 4, value: 65 },
16443
- ],
16444
- name: 'Users',
16445
- },
16446
- ]);
16441
+ chart.update([{
16442
+ data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }, { key: 4, value: 32 }],
16443
+ name: 'Revenue',
16444
+ }]);
16445
+ chart.dispose();
16447
16446
  ```
16448
16447
 
16449
- | Feature | Prism | Chart.js | Lightweight Charts | D3 |
16450
- | ------------------ | -------------------------------------------- | ---------------------------------------- | -------------------------------------------- | -------------------------------------------- |
16451
- | Bundle size | | ~60 kB | ~45 kB | ~30 kB (core) |
16452
- | Zero dependencies | (Orbit) | | | (modular) |
16453
- | Renderer | SVG | Canvas | Canvas | SVG/Canvas |
16454
- | Data updates | Explicit `update()` | Instance mutation | Series mutation | Manual |
16455
- | CSS themeable | | | Limited | |
16456
- | Framework-neutral | | | | |
16457
- | Accessible SVG | | | | Manual |
16458
- | Keyboard navigation | | | | Manual |
16459
- | TypeScript-first | | Partial | | Types available |
16448
+ | Feature | Prism | Hand-written SVG |
16449
+ | --- | --- | --- |
16450
+ | Bundle size | | No chart package |
16451
+ | Zero dependencies | (uses Orbit) | |
16452
+ | Data updates | `ChartHandle.update()` replaces chart data | You implement the update path |
16453
+ | Responsive sizing | `ResizeObserver` recalculates the chart layout | You implement the resize behavior |
16454
+ | Chart interactions | Built-in pointer and keyboard interactions on supported charts | You implement them |
16455
+ | SVG accessibility | Informative charts support an accessible name and keyboard focus | You implement the accessible behavior |
16460
16456
 
16461
- **Use Prism when** you need lightweight, framework-neutral charts with explicit updates and SVG output that can be styled with CSS. It fits dashboards, admin panels, and data-heavy applications.
16457
+ **Use Prism when** you need chart factories, responsive SVG output, and an explicit data-update boundary without adopting a charting framework.
16462
16458
 
16463
- **Consider alternatives when** you need 50+ chart types (ECharts), financial trading charts (Lightweight Charts), or low-level visualization grammar (D3).
16459
+ **Consider hand-written SVG when** you need a single specialized graphic and prefer to own its scales, resizing, labels, and interactions directly.
16464
16460
 
16465
16461
  ## Installation
16466
16462
 
@@ -16478,61 +16474,48 @@ yarn add @vielzeug/prism
16478
16474
 
16479
16475
  ## Quick Start
16480
16476
 
16477
+ Create and size a host element before creating the chart. Import `theme.css` once to load Prism's default styles and CSS custom properties.
16478
+
16481
16479
  ```ts
16482
16480
  import { createLineChart } from '@vielzeug/prism';
16483
- import '@vielzeug/prism/theme';
16481
+ import '@vielzeug/prism/theme.css';
16484
16482
 
16485
- const chart = createLineChart(document.getElementById('chart')!, {
16483
+ const host = document.createElement('div');
16484
+ host.style.cssText = 'width:640px;height:320px';
16485
+ document.body.append(host);
16486
+
16487
+ const chart = createLineChart(host, {
16486
16488
  a11y: { ariaLabel: 'Revenue by month' },
16487
- series: [
16488
- {
16489
- color: '#3b82f6',
16490
- data: [
16491
- { key: 1, value: 10 },
16492
- { key: 2, value: 25 },
16493
- { key: 3, value: 18 },
16494
- { key: 4, value: 32 },
16495
- ],
16496
- name: 'Revenue',
16497
- },
16498
- ],
16499
- xAxis: { position: 'bottom' },
16500
- yAxis: { position: 'left', grid: true },
16489
+ series: [{
16490
+ data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }],
16491
+ name: 'Revenue',
16492
+ }],
16501
16493
  tooltip: true,
16502
- crosshair: true,
16503
- onHover: (event) => console.log(event?.datum),
16504
16494
  });
16505
16495
 
16506
- // Update chart data explicitly
16507
- chart.update([
16508
- {
16509
- color: '#3b82f6',
16510
- data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }, { key: 4, value: 32 }, { key: 5, value: 28 }],
16511
- name: 'Revenue',
16512
- },
16513
- ]);
16496
+ chart.update([{
16497
+ data: [{ key: 1, value: 10 }, { key: 2, value: 25 }, { key: 3, value: 18 }, { key: 4, value: 32 }],
16498
+ name: 'Revenue',
16499
+ }]);
16514
16500
 
16515
- // Cleanup when done
16516
16501
  chart.dispose();
16517
16502
  ```
16518
16503
 
16519
16504
  ## Features
16520
16505
 
16521
- - **`createLineChart(container, config)`**: line chart with linear, monotone, or step interpolation
16522
- - **`createBarChart(container, config)`**: bar chart with four layout variants: grouped, stacked, grouped-horizontal, stacked-horizontal
16523
- - **`createAreaChart(container, config)`**: filled area with a gradient or solid fill
16524
- - **`createPieChart(container, config)`**: pie, donut, or semi-circle donut chart; labels that do not fit their slice are hidden
16525
- - **`createRadarChart(container, config)`**: radar chart for comparing values across 3+ axes, with per-axis ranges
16526
- - **`createSparkline(container, config)`**: minimal inline line, area, bar, or stack chart
16527
- - **`linearScale`, `timeScale`, `bandScale`**: standalone scales with nice tick generation
16528
- - **`ChartHandle.update(data)`**: replace chart data synchronously without coupling to a state library
16529
- - **`setTheme(theme)` / `resetTheme()`**: apply or clear custom colors, font, and grid tokens at runtime
16530
- - **Series comparison**: hover or keyboard focus reports every series at the active key in the tooltip, `onHover`, and screen-reader announcements
16531
- - **Keyboard navigation**: arrow keys, `Enter`, and `Escape` on every labelled chart except sparklines
16532
- - **CSS custom properties**: full theme control via `--prism-*` tokens, with dark mode built in
16533
- - **Responsive**: auto-resizes via `ResizeObserver`
16534
- - **`ChartHandle.tap()`**: observe resize and dispose events outside the render path, with zero cost when untapped
16535
- - **`Symbol.dispose`**: explicit resource management following the TC39 proposal
16506
+ - **`createLineChart()` / `createAreaChart()`**: plot continuous numeric or date-keyed series
16507
+ - **`createBarChart()`**: compare categories with grouped, stacked, or horizontal bars
16508
+ - **`createScatterChart()`**: plot independent numeric `(x, y)` points
16509
+ - **`createPieChart()`**: render pie, donut, or semi-circle slices
16510
+ - **`createRadarChart()`**: compare series across fixed, labelled axes
16511
+ - **`createSparkline()`**: render compact line, area, bar, or stacked values without axes
16512
+ - **`ChartHandle.update(data)`**: replace chart data without handing state ownership to Prism
16513
+ - **`ChartHandle.dispose()` / `Symbol.dispose`**: release the SVG, observer, overlays, and chart-owned animation
16514
+ - **`ChartHandle.tap()`**: observe resize and disposal events
16515
+ - **`setTheme()` / `resetTheme()`**: set supported palette, typography, axis, grid, and tooltip tokens globally or on one element
16516
+ - **CSS custom properties**: style the chart palette, marks, axes, labels, tooltip, legend, and motion
16517
+ - **Resize observation**: recalculate SVG layout when the host element changes size
16518
+ - **Accessible interaction**: labelled interactive charts expose keyboard navigation and polite value announcements; sparklines are pointer-only without value announcements
16536
16519
 
16537
16520
  ## Documentation
16538
16521
 
@@ -16543,37 +16526,44 @@ chart.dispose();
16543
16526
 
16544
16527
  ## See Also
16545
16528
 
16546
- - [Refine](/refine/): accessible web components that pair well with Prism for dashboards
16547
- - [Orbit](/orbit/): floating element positioning for chart tooltips and popovers
16529
+ - [Refine](/refine/): web components and theme tokens for building controls and layouts around chart hosts
16530
+ - [Orbit](/orbit/): floating-element positioning used internally by Prism's built-in tooltip
16548
16531
 
16549
16532
  ### API Reference
16550
16533
 
16551
16534
  ## API Overview
16552
16535
 
16536
+ All 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.
16537
+
16553
16538
  | Symbol | Purpose | Execution mode | Common gotcha |
16554
16539
  | --- | --- | --- | --- |
16555
- | `createLineChart()` | Render a line chart | Sync | Line keys must be numbers or dates |
16556
- | `createAreaChart()` | Render an area chart | Sync | Line keys must be numbers or dates |
16557
- | `createBarChart()` | Render grouped or stacked bars | Sync | Stacked negative values are clamped to zero |
16558
- | `createPieChart()` | Render pie, donut, or semi-circle slices | Sync | `onHover` receives `null` when the pointer leaves |
16559
- | `createRadarChart()` | Compare values across 3+ axes, such as hero stats | Sync | Events report a whole axis, not one series |
16560
- | `createSparkline()` | Render an inline line, area, bar, or stack | Sync | Omitted `a11y` makes the chart decorative |
16561
- | `linearScale()` | Create a numeric scale | Sync | `nice` defaults to `true` |
16562
- | `timeScale()` | Create a date scale | Sync | Invalid dates produce an invalid domain |
16563
- | `bandScale()` | Create a categorical scale | Sync | Unknown categories map to `0` and warn in development |
16564
- | `setTheme()` | Set Prism CSS custom properties | Sync | Values apply to `document.documentElement` |
16565
- | `resetTheme()` | Remove Prism theme overrides | Sync | Removes only properties managed by `setTheme()` |
16566
- | `seriesColor()` | Resolve a series palette color | Sync | Palette indexes wrap after eight colors |
16540
+ | `createLineChart()` | Render one or more continuous line series | Sync | `key` values must be numbers or dates |
16541
+ | `createAreaChart()` | Render continuous series with filled areas | Sync | Fills use a gradient by default |
16542
+ | `createScatterChart()` | Plot independent `(x, y)` points | Sync | `key` is the x-value; the y-domain is fitted to data |
16543
+ | `createBarChart()` | Render categorical bars | Sync | Negative values in stacked variants are clamped to zero |
16544
+ | `createPieChart()` | Render pie, donut, or semi-circle slices | Sync | `variant` is fixed at creation; `update()` replaces slices |
16545
+ | `createRadarChart()` | Compare series across fixed axes | Sync | Requires at least three axes to draw |
16546
+ | `createSparkline()` | Render a compact line, area, bar, or stack | Sync | Has no axes, legend, or keyboard navigation |
16547
+ | `linearScale()` | Create a numeric scale | Sync | `nice` defaults to `true`; set `nice: false` to preserve the domain |
16548
+ | `timeScale()` | Create a date scale | Sync | `nice` defaults to `true` |
16549
+ | `bandScale()` | Create an ordered categorical scale | Sync | Unknown categories map to `0` and warn in development |
16550
+ | `setTheme()` | Set Prism CSS custom properties | Sync | Writes inline styles to `document.documentElement` by default |
16551
+ | `resetTheme()` | Remove theme overrides set by `setTheme()` | Sync | Only clears the selected target |
16552
+ | `seriesColor()` | Resolve a palette CSS value | Sync | Palette positions wrap after eight |
16553
+ | `PrismError` | Base class for Prism errors | Sync | Use `instanceof` to catch any Prism error |
16554
+ | `PrismRenderError` | Report an invalid chart container or failed chart operation | Sync | Also thrown by `update()` after disposal |
16567
16555
 
16568
16556
  ## Package Entry Point
16569
16557
 
16570
16558
  | Import | Purpose |
16571
16559
  | --- | --- |
16572
- | `@vielzeug/prism` | Chart factories, scales, theme helpers, errors, and public types |
16573
- | `@vielzeug/prism/theme` | Default CSS custom properties and dark-mode values |
16560
+ | `@vielzeug/prism` | Chart factories, scale and theme functions, error classes, and public types |
16561
+ | `@vielzeug/prism/theme.css` | Default chart styles, CSS custom properties, dark-mode values, and reduced-motion rules |
16574
16562
 
16575
16563
  ## Chart Factories
16576
16564
 
16565
+ Each 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.
16566
+
16577
16567
  ### `createLineChart()`
16578
16568
 
16579
16569
  ```ts
@@ -16583,28 +16573,32 @@ function createLineChart(
16583
16573
  ): ChartHandle;
16584
16574
  ```
16585
16575
 
16586
- Renders a line chart and returns a handle that replaces the complete series array through `update()`.
16576
+ Renders 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.
16577
+
16578
+ Series 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.
16587
16579
 
16588
16580
  | Parameter | Type | Description |
16589
16581
  | --- | --- | --- |
16590
- | `container` | `HTMLElement` | Host that receives the SVG and optional overlays |
16591
- | `config` | `LineChartConfig` | Initial series, axes, interaction, accessibility, and transition settings |
16582
+ | `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
16583
+ | `config` | `LineChartConfig` | Series, axes, interaction, accessibility, and motion settings |
16592
16584
 
16593
16585
  **Returns:** `ChartHandle`.
16594
16586
 
16595
16587
  ```ts
16596
16588
  import { createLineChart } from '@vielzeug/prism';
16597
16589
 
16598
- const chart = createLineChart(container, {
16590
+ const host = document.createElement('div');
16591
+ host.style.cssText = 'width:640px;height:320px';
16592
+ document.body.append(host);
16593
+
16594
+ const chart = createLineChart(host, {
16599
16595
  a11y: { ariaLabel: 'Revenue by month' },
16600
- series: [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }],
16596
+ series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }], name: 'Revenue' }],
16601
16597
  });
16602
-
16603
- chart.update([{ data: [{ key: 2, value: 20 }], name: 'Revenue' }]);
16598
+ chart.update([{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }, { key: 3, value: 18 }], name: 'Revenue' }]);
16599
+ chart.dispose();
16604
16600
  ```
16605
16601
 
16606
- ---
16607
-
16608
16602
  ### `createAreaChart()`
16609
16603
 
16610
16604
  ```ts
@@ -16614,24 +16608,67 @@ function createAreaChart(
16614
16608
  ): ChartHandle;
16615
16609
  ```
16616
16610
 
16617
- Renders an area chart and returns a handle that replaces the complete series array through `update()`.
16611
+ Renders continuous area series with a gradient fill by default. `update()` replaces the complete series array.
16612
+
16613
+ Area 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.
16618
16614
 
16619
16615
  | Parameter | Type | Description |
16620
16616
  | --- | --- | --- |
16621
- | `container` | `HTMLElement` | Host that receives the SVG and optional overlays |
16622
- | `config` | `AreaChartConfig` | Initial series, axes, interaction, accessibility, and transition settings |
16617
+ | `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
16618
+ | `config` | `AreaChartConfig` | Series, axes, interaction, accessibility, and motion settings |
16623
16619
 
16624
16620
  **Returns:** `ChartHandle`.
16625
16621
 
16626
16622
  ```ts
16627
16623
  import { createAreaChart } from '@vielzeug/prism';
16628
16624
 
16629
- const chart = createAreaChart(container, {
16630
- series: [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }],
16625
+ const host = document.createElement('div');
16626
+ host.style.cssText = 'width:640px;height:320px';
16627
+ document.body.append(host);
16628
+
16629
+ const chart = createAreaChart(host, {
16630
+ a11y: { ariaLabel: 'Visitors over time' },
16631
+ series: [{ data: [{ key: 1, value: 10 }, { key: 2, value: 18 }], name: 'Visitors' }],
16631
16632
  });
16633
+ chart.dispose();
16632
16634
  ```
16633
16635
 
16634
- ---
16636
+ ### `createScatterChart()`
16637
+
16638
+ ```ts
16639
+ function createScatterChart(
16640
+ container: HTMLElement,
16641
+ config: ScatterChartConfig,
16642
+ ): ChartHandle;
16643
+ ```
16644
+
16645
+ Plots 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.
16646
+
16647
+ Axes 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.
16648
+
16649
+ | Parameter | Type | Description |
16650
+ | --- | --- | --- |
16651
+ | `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
16652
+ | `config` | `ScatterChartConfig` | Independent points, axes, interaction, accessibility, and motion settings |
16653
+
16654
+ **Returns:** `ChartHandle`.
16655
+
16656
+ ```ts
16657
+ import { createScatterChart } from '@vielzeug/prism';
16658
+
16659
+ const host = document.createElement('div');
16660
+ host.style.cssText = 'width:640px;height:320px';
16661
+ document.body.append(host);
16662
+
16663
+ const chart = createScatterChart(host, {
16664
+ a11y: { ariaLabel: 'Height against weight' },
16665
+ series: [{ data: [{ key: 160, value: 55 }, { key: 175, value: 70 }], name: 'Cohort A' }],
16666
+ tooltip: true,
16667
+ });
16668
+ chart.dispose();
16669
+ ```
16670
+
16671
+ Scatter interaction reports one datum and series rather than a `values` array. Keyboard navigation visits points across series in x order.
16635
16672
 
16636
16673
  ### `createBarChart()`
16637
16674
 
@@ -16642,25 +16679,32 @@ function createBarChart(
16642
16679
  ): ChartHandle;
16643
16680
  ```
16644
16681
 
16645
- Renders grouped, stacked, grouped-horizontal, or stacked-horizontal bars.
16682
+ Renders 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.
16683
+
16684
+ The default variant is `grouped`; tooltip and legend are disabled unless configured.
16646
16685
 
16647
16686
  | Parameter | Type | Description |
16648
16687
  | --- | --- | --- |
16649
- | `container` | `HTMLElement` | Host that receives the SVG and optional overlays |
16650
- | `config` | `BarChartConfig` | Initial series, variant, axes, accessibility, and interaction settings |
16688
+ | `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
16689
+ | `config` | `BarChartConfig` | Series, layout variant, axes, interaction, accessibility, and motion settings |
16651
16690
 
16652
16691
  **Returns:** `ChartHandle`.
16653
16692
 
16654
16693
  ```ts
16655
16694
  import { createBarChart } from '@vielzeug/prism';
16656
16695
 
16657
- const chart = createBarChart(container, {
16658
- series: [{ data: [{ key: 'Open', value: 12 }], name: 'Tasks' }],
16659
- variant: 'grouped',
16696
+ const host = document.createElement('div');
16697
+ host.style.cssText = 'width:640px;height:320px';
16698
+ document.body.append(host);
16699
+
16700
+ const chart = createBarChart(host, {
16701
+ a11y: { ariaLabel: 'Tasks by status' },
16702
+ series: [{ data: [{ key: 'Open', value: 12 }, { key: 'Done', value: 8 }], name: 'Tasks' }],
16660
16703
  });
16704
+ chart.dispose();
16661
16705
  ```
16662
16706
 
16663
- ---
16707
+ For stacked variants, negative values are treated as zero and produce a development warning.
16664
16708
 
16665
16709
  ### `createPieChart()`
16666
16710
 
@@ -16671,26 +16715,30 @@ function createPieChart(
16671
16715
  ): ChartHandle;
16672
16716
  ```
16673
16717
 
16674
- Renders pie, donut, or semi-circle slices.
16718
+ Renders 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.
16675
16719
 
16676
16720
  | Parameter | Type | Description |
16677
16721
  | --- | --- | --- |
16678
- | `container` | `HTMLElement` | Host that receives the SVG and optional overlays |
16679
- | `config` | `PieChartConfig` | Initial slices, variant, geometry, accessibility, and interaction settings |
16722
+ | `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
16723
+ | `config` | `PieChartConfig` | Slices, shape, geometry, legend, tooltip, and interaction settings |
16680
16724
 
16681
16725
  **Returns:** `ChartHandle`.
16682
16726
 
16683
16727
  ```ts
16684
16728
  import { createPieChart } from '@vielzeug/prism';
16685
16729
 
16686
- const chart = createPieChart(container, {
16687
- data: [{ label: 'Complete', value: 72 }, { label: 'Remaining', value: 28 }],
16730
+ const host = document.createElement('div');
16731
+ host.style.cssText = 'width:320px;height:240px';
16732
+ document.body.append(host);
16733
+
16734
+ const chart = createPieChart(host, {
16735
+ a11y: { ariaLabel: 'Orders by channel' },
16736
+ data: [{ label: 'Direct', value: 72 }, { label: 'Referral', value: 28 }],
16688
16737
  variant: 'donut',
16689
16738
  });
16739
+ chart.dispose();
16690
16740
  ```
16691
16741
 
16692
- ---
16693
-
16694
16742
  ### `createRadarChart()`
16695
16743
 
16696
16744
  ```ts
@@ -16700,47 +16748,32 @@ function createRadarChart(
16700
16748
  ): ChartHandle;
16701
16749
  ```
16702
16750
 
16703
- Renders one closed shape per series across three or more axes. `update()` replaces the series; the axes are fixed at creation.
16751
+ Renders 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.
16704
16752
 
16705
16753
  | Parameter | Type | Description |
16706
16754
  | --- | --- | --- |
16707
- | `container` | `HTMLElement` | Host that receives the SVG and optional overlays |
16708
- | `config` | `RadarChartConfig` | Axes, series, scale, grid, appearance, accessibility, and interaction settings |
16709
-
16710
- | Field | Type | Default | Description |
16711
- | --- | --- | --- | --- |
16712
- | `axes` | `RadarAxisConfig[]` | N/A | Ordered axes, drawn clockwise; needs at least 3 |
16713
- | `series` | `RadarSeriesConfig[]` | N/A | Values keyed by `axis.key` |
16714
- | `domain` | `[number, number]` | `[0, nice max]` | Shared value range; `axis.min`/`axis.max` override it per axis |
16715
- | `grid` | `boolean \| RadarGridConfig` | `true` | Rings, bands, and level labels; `false` hides the grid |
16716
- | `fill` | `'gradient' \| 'none' \| 'solid'` | `'solid'` | Shape fill; `gradient` deepens toward the rim |
16717
- | `curve` | `'linear' \| 'rounded'` | `'linear'` | Straight edges or a smooth closed curve |
16718
- | `showPoints` | `boolean` | `true` | Vertex dots; a missing value shows a hollow dot at the centre |
16719
- | `showValues` | `boolean` | `false` | Print each value beside its vertex |
16720
- | `startAngle` | `number` | `0` | Degrees clockwise from 12 o'clock for the first axis |
16721
- | `onHover` | `(event: RadarEvent \| null) => void` | N/A | Nearest axis under the pointer or keyboard focus |
16722
- | `onClick` | `(event: RadarEvent) => void` | N/A | Axis clicked, or activated with Enter/Space |
16755
+ | `container` | `HTMLElement` | Host that receives the chart SVG and optional overlays |
16756
+ | `config` | `RadarChartConfig` | Fixed axes, series, scale, grid, appearance, and interaction settings |
16723
16757
 
16724
16758
  **Returns:** `ChartHandle`.
16725
16759
 
16726
16760
  ```ts
16727
16761
  import { createRadarChart } from '@vielzeug/prism';
16728
16762
 
16729
- const chart = createRadarChart(container, {
16763
+ const host = document.createElement('div');
16764
+ host.style.cssText = 'width:400px;height:320px';
16765
+ document.body.append(host);
16766
+
16767
+ const chart = createRadarChart(host, {
16730
16768
  a11y: { ariaLabel: 'Hero stats' },
16731
- axes: [
16732
- { key: 'str', label: 'Strength' },
16733
- { key: 'agi', label: 'Agility' },
16734
- { key: 'end', label: 'Endurance' },
16735
- ],
16769
+ axes: [{ key: 'str', label: 'Strength' }, { key: 'agi', label: 'Agility' }, { key: 'end', label: 'Endurance' }],
16736
16770
  domain: [0, 10],
16737
16771
  series: [{ data: [{ key: 'str', value: 8 }, { key: 'agi', value: 6 }, { key: 'end', value: 9 }], name: 'Adam' }],
16738
16772
  });
16773
+ chart.dispose();
16739
16774
  ```
16740
16775
 
16741
- Hovering or arrowing onto an axis highlights its spoke and reports every series' value on it. Without a tooltip, the chart announces the values through its own `role="status"` region. A value for an unknown axis, duplicate axis keys, or fewer than 3 axes warns in development.
16742
-
16743
- ---
16776
+ Radar 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.
16744
16777
 
16745
16778
  ### `createSparkline()`
16746
16779
 
@@ -16751,22 +16784,29 @@ function createSparkline(
16751
16784
  ): ChartHandle;
16752
16785
  ```
16753
16786
 
16754
- Renders a compact chart without axes, margin, or legend.
16787
+ Renders 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.
16755
16788
 
16756
16789
  | Parameter | Type | Description |
16757
16790
  | --- | --- | --- |
16758
- | `container` | `HTMLElement` | Host that receives the SVG |
16759
- | `config` | `SparklineConfig` | Initial values, variant, appearance, accessibility, and callbacks |
16791
+ | `container` | `HTMLElement` | Host that receives the chart SVG |
16792
+ | `config` | `SparklineConfig` | Values, variant, appearance, accessibility, and pointer callbacks |
16760
16793
 
16761
16794
  **Returns:** `ChartHandle`.
16762
16795
 
16763
16796
  ```ts
16764
16797
  import { createSparkline } from '@vielzeug/prism';
16765
16798
 
16766
- const chart = createSparkline(container, { data: [2, 5, 3, 8], variant: 'area' });
16799
+ const host = document.createElement('div');
16800
+ host.style.cssText = 'width:240px;height:48px';
16801
+ document.body.append(host);
16802
+
16803
+ const chart = createSparkline(host, { data: [2, 5, 3, 8], variant: 'area' });
16767
16804
  chart.update([2, 5, 3, 8, 13]);
16805
+ chart.dispose();
16768
16806
  ```
16769
16807
 
16808
+ `StackSegment[]` is for `variant: 'stack'`. Pointer callbacks are available for numeric line, area, and bar sparklines; sparklines do not support keyboard navigation.
16809
+
16770
16810
  ## Scale Factories
16771
16811
 
16772
16812
  ### `linearScale()`
@@ -16775,14 +16815,16 @@ chart.update([2, 5, 3, 8, 13]);
16775
16815
  function linearScale(config: LinearScaleConfig): Scale;
16776
16816
  ```
16777
16817
 
16778
- | Field | Type | Default | Description |
16818
+ Maps a numeric domain to a numeric range. `nice` defaults to `true`; `clamp` defaults to `false` and applies to `map()`, not `invert()`.
16819
+
16820
+ | Parameter | Type | Default | Description |
16779
16821
  | --- | --- | --- | --- |
16780
- | `domain` | `[number, number]` | N/A | Input extent |
16781
- | `range` | `[number, number]` | N/A | Output extent |
16782
- | `clamp` | `boolean` | `false` | Clamp mapped and inverted values |
16783
- | `nice` | `boolean` | `true` | Expand the domain to rounded boundaries |
16822
+ | `config.domain` | `[number, number]` | Required | Input extent |
16823
+ | `config.range` | `[number, number]` | Required | Output extent |
16824
+ | `config.clamp` | `boolean` | `false` | Clamp mapped values to the range |
16825
+ | `config.nice` | `boolean` | `true` | Expand the domain to rounded boundaries |
16784
16826
 
16785
- **Returns:** `Scale`.
16827
+ **Returns:** `Scale`, with `map`, `invert`, `ticks`, `domain`, and `range`.
16786
16828
 
16787
16829
  ```ts
16788
16830
  import { linearScale } from '@vielzeug/prism';
@@ -16791,21 +16833,21 @@ const scale = linearScale({ domain: [0, 100], range: [0, 500] });
16791
16833
  scale.map(50); // 250
16792
16834
  ```
16793
16835
 
16794
- ---
16795
-
16796
16836
  ### `timeScale()`
16797
16837
 
16798
16838
  ```ts
16799
16839
  function timeScale(config: TimeScaleConfig): Scale;
16800
16840
  ```
16801
16841
 
16802
- | Field | Type | Default | Description |
16842
+ Maps date values to a numeric range. `nice` defaults to `true` and rounds the domain to a time interval.
16843
+
16844
+ | Parameter | Type | Default | Description |
16803
16845
  | --- | --- | --- | --- |
16804
- | `domain` | `[Date, Date]` | N/A | Input date extent |
16805
- | `range` | `[number, number]` | N/A | Output extent |
16806
- | `nice` | `boolean` | `true` | Expand the domain to rounded interval boundaries |
16846
+ | `config.domain` | `[Date, Date]` | Required | Input date extent |
16847
+ | `config.range` | `[number, number]` | Required | Output extent |
16848
+ | `config.nice` | `boolean` | `true` | Expand the domain to time-interval boundaries |
16807
16849
 
16808
- **Returns:** `Scale`.
16850
+ **Returns:** `Scale`, with `map`, `invert`, `ticks`, `domain`, and `range`.
16809
16851
 
16810
16852
  ```ts
16811
16853
  import { timeScale } from '@vielzeug/prism';
@@ -16814,48 +16856,81 @@ const scale = timeScale({
16814
16856
  domain: [new Date('2026-01-01'), new Date('2026-12-31')],
16815
16857
  range: [0, 500],
16816
16858
  });
16859
+ scale.map(new Date('2026-07-02'));
16817
16860
  ```
16818
16861
 
16819
- ---
16820
-
16821
16862
  ### `bandScale()`
16822
16863
 
16823
16864
  ```ts
16824
16865
  function bandScale(config: BandScaleConfig): BandScale;
16825
16866
  ```
16826
16867
 
16827
- | Field | Type | Default | Description |
16868
+ Maps 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.
16869
+
16870
+ | Parameter | Type | Default | Description |
16828
16871
  | --- | --- | --- | --- |
16829
- | `domain` | `string[]` | N/A | Ordered categories |
16830
- | `range` | `[number, number]` | N/A | Output extent |
16831
- | `padding` | `number` | `0.1` | Inner gap ratio |
16832
- | `paddingOuter` | `number` | `padding` | Outer gap ratio |
16872
+ | `config.domain` | `string[]` | Required | Ordered categories |
16873
+ | `config.range` | `[number, number]` | Required | Output extent |
16874
+ | `config.padding` | `number` | `0.1` | Inner gap as a ratio of bandwidth |
16875
+ | `config.paddingOuter` | `number` | `padding` | Outer gap as a ratio of bandwidth |
16833
16876
 
16834
- **Returns:** `BandScale`.
16877
+ **Returns:** `BandScale`, with `map`, `ticks`, `bandwidth`, `gap`, `domain`, and `range`.
16835
16878
 
16836
16879
  ```ts
16837
16880
  import { bandScale } from '@vielzeug/prism';
16838
16881
 
16839
16882
  const scale = bandScale({ domain: ['A', 'B'], range: [0, 200] });
16883
+ scale.bandwidth();
16840
16884
  ```
16841
16885
 
16842
16886
  ## Theme Utilities
16843
16887
 
16888
+ `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.
16889
+
16844
16890
  ### `setTheme()`
16845
16891
 
16846
16892
  ```ts
16847
- function setTheme(theme: PrismTheme): void;
16893
+ function setTheme(theme: PrismTheme, options?: ThemeScope): void;
16848
16894
  ```
16849
16895
 
16850
- Sets Prism color, font, and grid custom properties on `document.documentElement`.
16896
+ Sets 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.
16897
+
16898
+ | Parameter | Type | Description |
16899
+ | --- | --- | --- |
16900
+ | `theme` | `PrismTheme` | Palette and supported axis, grid, text, font, and tooltip properties |
16901
+ | `options.scope` | `HTMLElement` | Element that scopes the inline custom properties |
16902
+
16903
+ **Returns:** `void`.
16904
+
16905
+ ```ts
16906
+ import { setTheme } from '@vielzeug/prism';
16907
+
16908
+ const host = document.createElement('div');
16909
+ document.body.append(host);
16910
+ setTheme({ colors: ['#2563eb', '#16a34a'], textColor: '#111827' }, { scope: host });
16911
+ ```
16851
16912
 
16852
16913
  ### `resetTheme()`
16853
16914
 
16854
16915
  ```ts
16855
- function resetTheme(): void;
16916
+ function resetTheme(options?: ThemeScope): void;
16856
16917
  ```
16857
16918
 
16858
- Removes every custom property managed by `setTheme()`.
16919
+ Removes 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.
16920
+
16921
+ | Parameter | Type | Description |
16922
+ | --- | --- | --- |
16923
+ | `options` | `ThemeScope` | Optional target; defaults to `document.documentElement` |
16924
+
16925
+ **Returns:** `void`.
16926
+
16927
+ ```ts
16928
+ import { resetTheme, setTheme } from '@vielzeug/prism';
16929
+
16930
+ const scope = document.createElement('div');
16931
+ setTheme({ textColor: '#111827' }, { scope });
16932
+ resetTheme({ scope });
16933
+ ```
16859
16934
 
16860
16935
  ### `seriesColor()`
16861
16936
 
@@ -16863,1307 +16938,972 @@ Removes every custom property managed by `setTheme()`.
16863
16938
  function seriesColor(index: number, override?: string): string;
16864
16939
  ```
16865
16940
 
16866
- Returns `override` when provided; otherwise returns `var(--prism-color-N)` with an eight-color wrap.
16941
+ Returns `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.
16942
+
16943
+ | Parameter | Type | Description |
16944
+ | --- | --- | --- |
16945
+ | `index` | `number` | Zero-based palette position |
16946
+ | `override` | `string` | Optional color returned instead of a CSS variable |
16947
+
16948
+ **Returns:** `string`.
16949
+
16950
+ ```ts
16951
+ import { seriesColor } from '@vielzeug/prism';
16952
+
16953
+ seriesColor(0); // 'var(--prism-color-1)'
16954
+ seriesColor(1, '#2563eb'); // '#2563eb'
16955
+ ```
16956
+
16957
+ ## Chart Handle
16958
+
16959
+ `ChartHandle` 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`.
16960
+
16961
+ `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.
16962
+
16963
+ `[Symbol.dispose]()` delegates to `dispose()`, allowing explicit resource management where supported.
16964
+
16965
+ All chart handles expose the same lifecycle members; interactive cartesian and pie/radar 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.
16966
+
16967
+ | Method or property | Contract |
16968
+ | --- | --- |
16969
+ | `el` | The chart's root `SVGSVGElement` |
16970
+ | `update(data)` | Replace chart data and render; throws `PrismRenderError` after disposal |
16971
+ | `dispose()` | Idempotently release chart-owned DOM, observation, and motion |
16972
+ | `disposed` | Whether the handle has been disposed |
16973
+ | `disposalSignal` | `AbortSignal` aborted during disposal |
16974
+ | `tap(handler, options?)` | Observe resize/dispose events; returns an unsubscribe function |
16975
+ | `[Symbol.dispose]()` | Calls `dispose()` |
16976
+
16977
+ `PrismEvent` reports a changed SVG content size as `{ type: 'resize', width, height }`; the `dispose` event is emitted once before teardown completes.
16867
16978
 
16868
16979
  ## Types
16869
16980
 
16870
- ### Core types
16981
+ The following are all types and interfaces exported from `@vielzeug/prism`. Definitions mirror the package entry-point contracts.
16982
+
16983
+ ### Animation Types
16871
16984
 
16872
16985
  ```ts
16873
- interface ChartHandle {
16874
- readonly disposalSignal: AbortSignal;
16875
- readonly disposed: boolean;
16876
- readonly el: SVGSVGElement;
16877
- tap(handler: (event: PrismEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;
16878
- update(data: TData): void;
16879
- dispose(): void;
16880
- [Symbol.dispose](): void;
16986
+ export type EasingFn = (t: number) => number;
16987
+ export type EasingName = 'back-out' | 'ease-in' | 'ease-in-out' | 'ease-out' | 'expo-out' | 'linear';
16988
+
16989
+ export interface TransitionConfig {
16990
+ /** Milliseconds per animated lane. Defaults to 300 (420 for a line entrance, 400 for radar). */
16991
+ duration?: number;
16992
+ easing?: EasingName | ((t: number) => number);
16993
+ /** `system` skips animation when reduced motion is preferred. */
16994
+ preference?: 'always' | 'never' | 'system';
16995
+ /** Per-bar delay on bar charts or per-series delay on radar charts. */
16996
+ stagger?: number;
16881
16997
  }
16882
16998
 
16883
- type PrismEvent =
16884
- | { readonly height: number; readonly type: 'resize'; readonly width: number }
16885
- | { readonly type: 'dispose' };
16999
+ export type TransitionOption = TransitionConfig | boolean;
17000
+ ```
16886
17001
 
16887
- interface ChartMargin {
16888
- top: number;
16889
- right: number;
17002
+ ### Theme Types
17003
+
17004
+ ```ts
17005
+ export interface PrismTheme {
17006
+ axisColor?: string;
17007
+ colors?: string[];
17008
+ fontFamily?: string;
17009
+ gridColor?: string;
17010
+ gridOpacity?: number;
17011
+ textColor?: string;
17012
+ textColorSecondary?: string;
17013
+ tooltipBg?: string;
17014
+ tooltipBorder?: string;
17015
+ tooltipColor?: string;
17016
+ }
17017
+
17018
+ export interface ThemeScope {
17019
+ readonly scope?: HTMLElement;
17020
+ }
17021
+ ```
17022
+
17023
+ `PrismTheme.colors` writes up to eight palette slots. The scalar fields map as follows:
17024
+
17025
+ | Field | CSS custom property |
17026
+ | --- | --- |
17027
+ | `axisColor` | `--prism-axis-color` |
17028
+ | `fontFamily` | `--prism-font-family` |
17029
+ | `gridColor` | `--prism-grid-color` |
17030
+ | `gridOpacity` | `--prism-grid-opacity` |
17031
+ | `textColor` | `--prism-text-color` |
17032
+ | `textColorSecondary` | `--prism-text-color-secondary` |
17033
+ | `tooltipBg` | `--prism-tooltip-bg` |
17034
+ | `tooltipBorder` | `--prism-tooltip-border` |
17035
+ | `tooltipColor` | `--prism-tooltip-color` |
17036
+
17037
+ ### Core and Data Types
17038
+
17039
+ ```ts
17040
+ export interface ChartMargin {
16890
17041
  bottom: number;
16891
17042
  left: number;
17043
+ right: number;
17044
+ top: number;
16892
17045
  }
16893
17046
 
16894
- interface ChartDimensions {
16895
- width: number;
17047
+ export interface ChartDimensions {
16896
17048
  height: number;
16897
17049
  margin: ChartMargin;
17050
+ width: number;
16898
17051
  }
16899
17052
 
16900
- type ChartA11y =
16901
- | { readonly decorative: true }
16902
- | { readonly ariaLabel: string; readonly decorative?: false };
16903
- ```
16904
-
16905
- `update()` renders synchronously and throws `PrismRenderError` after disposal. Omitting `a11y` makes the SVG decorative with `aria-hidden="true"`.
16906
-
16907
- `tap()` observes runtime behavior outside the render path: it receives `resize` and `dispose` events (see `PrismEvent`). It returns an unsubscribe function; pass `options.signal` to detach automatically. Handler errors are swallowed, and tapping a disposed handle returns a no-op unsubscribe.
17053
+ export type PrismEvent =
17054
+ | { readonly height: number; readonly type: 'resize'; readonly width: number }
17055
+ | { readonly type: 'dispose' };
16908
17056
 
16909
- ---
17057
+ export interface ChartHandle {
17058
+ readonly disposalSignal: AbortSignal;
17059
+ dispose(): void;
17060
+ readonly disposed: boolean;
17061
+ readonly el: SVGSVGElement;
17062
+ tap(handler: (event: PrismEvent) => void, options?: { readonly signal?: AbortSignal }): () => void;
17063
+ update(data: TData): void;
17064
+ [Symbol.dispose](): void;
17065
+ }
16910
17066
 
16911
- ### Data types
17067
+ export type ChartA11y =
17068
+ | { readonly decorative: true }
17069
+ | { readonly ariaLabel: string; readonly decorative?: false };
16912
17070
 
16913
- ```ts
16914
- interface Datum {
17071
+ export interface Datum {
17072
+ dash?: string;
16915
17073
  key: TKey;
16916
- value: number;
16917
17074
  meta?: Record;
16918
- dash?: string; // stroke-dasharray for this datum's marks, e.g. '5 5'
16919
- opacity?: number; // opacity multiplier 0-1 for this datum's marks, e.g. 0.4
17075
+ opacity?: number;
17076
+ value: number;
16920
17077
  }
16921
17078
 
16922
- type ContinuousDatum = Datum;
17079
+ export type ContinuousDatum = Datum;
16923
17080
 
16924
- interface Series {
16925
- name: string;
16926
- data: TDatum[];
17081
+ export interface Series {
16927
17082
  color?: string;
16928
- id?: string; // stable data-series-id on the series group; defaults to series-
17083
+ data: TDatum[];
17084
+ id?: string;
17085
+ name: string;
16929
17086
  }
16930
17087
 
16931
- interface ChartEvent {
17088
+ export interface ChartEvent {
16932
17089
  datum: Datum;
16933
- series: Series;
16934
17090
  originalEvent: Event;
16935
- values?: SeriesValue[]; // every series at the same key (line, area, bar)
17091
+ series: Series;
17092
+ values?: SeriesValue[];
16936
17093
  }
16937
17094
 
16938
- interface SeriesValue {
17095
+ export interface SeriesValue {
16939
17096
  datum: Datum | undefined;
16940
17097
  series: Series;
16941
17098
  }
16942
17099
  ```
16943
17100
 
16944
- Line and area series use `ContinuousDatum`; bar series accept the complete `Datum` key range. On line, area, and bar charts, `datum`/`series` name the series nearest the pointer, and `values` lists every series at that key (a missing value is `undefined`). `originalEvent` is a `MouseEvent`, or a `KeyboardEvent` for keyboard navigation.
16945
-
16946
- #### Per-datum presentation
16947
-
16948
- `dash` and `opacity` style an individual datum's marks without splitting the series — the usual case is a forecast tail where points past "today" are faded or dashed. They are styling-only: interaction, tooltip, and accessibility behavior never change. Support is per chart type:
17101
+ `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.
16949
17102
 
16950
17103
  | Chart | `dash` | `opacity` |
16951
17104
  | --- | --- | --- |
16952
- | Bar | `stroke-dasharray` on the datum's `rect`, plus a 1px series-color stroke so the dash is visible on the fill. | Element `opacity` on the datum's `rect`. |
16953
- | Line | The segment from this datum to the next (Chart.js-style segments). The last datum's `dash` has no segment and is ignored. | That same segment, plus this datum's own point marker. |
16954
- | Area | The top line's segment from this datum to the next (last datum ignored). The fill keeps the series-level treatment. | Same segment on the top line. |
16955
- | Sparkline, pie, radar | Ignored. | Ignored. |
17105
+ | Bar | Strokes that datum's bar | Applies to that bar |
17106
+ | Line | Applies to the segment from this datum to the next | Applies to that segment and its point marker |
17107
+ | Area | Applies to the top-line segment from this datum to the next | Applies to that segment |
17108
+ | Scatter | Ignored | Applies to that point |
17109
+ | Pie, radar, sparkline | Ignored | Ignored |
16956
17110
 
16957
- Consecutive datums that share one `dash`/`opacity` render as a single path run. Absent fields keep the series/theme default, and a series with no presentation fields renders byte-identically to a chart that never used them. See [Forecast / emphasis styling](./usage.md#forecast--emphasis-styling).
17111
+ For 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.
16958
17112
 
16959
- #### Stable series identity
17113
+ For 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`.
16960
17114
 
16961
- `Series.id` is rendered as `data-series-id` on the chart's series group (`.prism-line-series`, `.prism-bar-series`, `.prism-area-series`, `.prism-radar-series`) so tooltips, tests, and external CSS can address a series without positional assumptions. Characters outside `[A-Za-z0-9_-]` are replaced with `-`. It defaults to `series-`, which only stays stable while the series order does — set `id` to survive reordering.
16962
-
16963
- ---
16964
-
16965
- ### Chart configurations
17115
+ ### Scale Types
16966
17116
 
16967
17117
  ```ts
16968
- interface BaseChartConfig {
16969
- a11y?: ChartA11y;
16970
- legend?: boolean | LegendConfig;
16971
- margin?: Partial;
16972
- onClick?: (event: ChartEvent) => void;
16973
- onHover?: (event: ChartEvent | null) => void;
16974
- tooltip?: boolean | TooltipConfig;
16975
- transition?: TransitionConfig;
16976
- xAxis?: XAxisConfig;
16977
- yAxis?: YAxisConfig;
17118
+ export interface Scale {
17119
+ readonly domain: readonly [T, T];
17120
+ invert(pixel: number): T;
17121
+ map(value: T): number;
17122
+ readonly range: readonly [number, number];
17123
+ ticks(count?: number): T[];
16978
17124
  }
16979
17125
 
16980
- interface LineChartConfig extends BaseChartConfig {
16981
- crosshair?: boolean | CrosshairConfig;
16982
- rightYAxis?: Omit;
16983
- series: LineSeriesConfig[];
17126
+ export interface BandScale {
17127
+ bandwidth(): number;
17128
+ readonly domain: readonly string[];
17129
+ gap(): number;
17130
+ map(value: string): number;
17131
+ readonly range: readonly [number, number];
17132
+ ticks(count?: number): string[];
16984
17133
  }
16985
17134
 
16986
- interface AreaChartConfig extends BaseChartConfig {
16987
- crosshair?: boolean | CrosshairConfig;
16988
- series: AreaSeriesConfig[];
17135
+ export interface LinearScaleConfig {
17136
+ clamp?: boolean;
17137
+ domain: [number, number];
17138
+ nice?: boolean;
17139
+ range: [number, number];
16989
17140
  }
16990
17141
 
16991
- interface BarChartConfig extends BaseChartConfig {
16992
- series: BarSeriesConfig[];
16993
- variant?: BarVariant;
17142
+ export interface TimeScaleConfig {
17143
+ domain: [Date, Date];
17144
+ nice?: boolean;
17145
+ range: [number, number];
16994
17146
  }
16995
17147
 
16996
- interface PieChartConfig
16997
- extends Omit {
16998
- data: PieSliceConfig[];
16999
- variant?: PieVariant;
17000
- innerRadius?: number;
17001
- cornerRadius?: number;
17002
- padPixels?: number;
17003
- onClick?: (event: PieEvent) => void;
17004
- onHover?: (event: PieEvent | null) => void;
17148
+ export interface BandScaleConfig {
17149
+ domain: string[];
17150
+ padding?: number;
17151
+ paddingOuter?: number;
17152
+ range: [number, number];
17005
17153
  }
17154
+ ```
17006
17155
 
17007
- interface RadarChartConfig
17008
- extends Omit {
17009
- axes: RadarAxisConfig[];
17010
- series: RadarSeriesConfig[];
17011
- curve?: 'linear' | 'rounded';
17012
- domain?: [number, number];
17013
- fill?: 'gradient' | 'none' | 'solid';
17014
- grid?: boolean | RadarGridConfig;
17015
- showPoints?: boolean;
17016
- showValues?: boolean;
17017
- startAngle?: number;
17018
- onClick?: (event: RadarEvent) => void;
17019
- onHover?: (event: RadarEvent | null) => void;
17020
- }
17021
-
17022
- interface SparklineConfig {
17023
- data: number[] | StackSegment[];
17024
- variant?: SparklineVariant;
17025
- a11y?: ChartA11y;
17026
- color?: string;
17027
- cornerRadius?: number;
17028
- curve?: 'linear' | 'monotone' | 'step';
17029
- fillOpacity?: number; // overrides --prism-spark-fill-opacity (area variant)
17030
- onClick?: (event: SparklineEvent) => void;
17031
- onHover?: (event: SparklineEvent | null) => void;
17032
- padPixels?: number;
17033
- showEndPoint?: boolean; // default true: dot on the latest value (line, area)
17034
- strokeWidth?: number;
17035
- transition?: TransitionConfig;
17036
- }
17037
- ```
17038
-
17039
- ---
17040
-
17041
- ### Series and slice configurations
17156
+ ### Axis and Interaction Types
17042
17157
 
17043
17158
  ```ts
17044
- interface LineSeriesConfig extends Series {
17045
- yAxis?: 'left' | 'right'; // defaults to 'left'
17046
- curve?: 'linear' | 'monotone' | 'step';
17047
- pointRadius?: number;
17048
- showPoints?: boolean;
17049
- strokeWidth?: number; // overrides --prism-line-width
17050
- }
17051
-
17052
- interface AreaSeriesConfig extends Series {
17053
- curve?: 'linear' | 'monotone' | 'step';
17054
- fill?: 'gradient' | 'solid'; // default 'gradient', fading toward the baseline
17055
- fillOpacity?: number; // overrides --prism-area-opacity
17056
- showLine?: boolean;
17057
- }
17058
-
17059
- interface BarSeriesConfig extends Series {
17060
- borderRadius?: number; // overrides --prism-bar-radius; stacked bars stay square
17061
- }
17062
-
17063
- interface PieSliceConfig {
17064
- value: number;
17065
- color?: string;
17066
- label?: string;
17067
- }
17068
-
17069
- interface PieEvent {
17070
- index: number;
17071
- originalEvent: Event;
17072
- slice: PieSliceConfig;
17073
- }
17074
-
17075
- interface RadarAxisConfig {
17076
- key: string;
17077
- label: string;
17078
- min?: number;
17079
- max?: number;
17080
- format?: (value: number) => string;
17081
- }
17082
-
17083
- type RadarSeriesConfig = Series>;
17084
-
17085
- interface RadarGridConfig {
17086
- bands?: boolean; // default true
17087
- labels?: boolean; // default false; drawn only when every axis shares one domain
17088
- levels?: number; // default 4
17089
- shape?: 'circle' | 'polygon'; // default 'polygon'
17090
- }
17091
-
17092
- interface RadarEvent {
17093
- axis: RadarAxisConfig;
17094
- index: number;
17095
- values: RadarAxisValue[];
17096
- originalEvent: Event;
17097
- }
17159
+ export type HorizontalAxisPosition = 'bottom' | 'top';
17160
+ export type VerticalAxisPosition = 'left' | 'right';
17161
+ export type AxisPosition = HorizontalAxisPosition | VerticalAxisPosition;
17098
17162
 
17099
- interface RadarAxisValue {
17100
- datum: Datum | undefined;
17101
- series: RadarSeriesConfig;
17102
- }
17103
-
17104
- interface StackSegment {
17105
- value: number;
17163
+ export interface GridConfig {
17106
17164
  color?: string;
17107
- label?: string;
17108
- }
17109
-
17110
- interface SparklineEvent {
17111
- index: number;
17112
- originalEvent: Event;
17113
- value: number;
17165
+ dash?: string;
17114
17166
  }
17115
- ```
17116
17167
 
17117
- A custom radar `tooltip.render` receives `datum.meta` with `axis`, `values`, and the default `text`.
17118
-
17119
- ---
17120
-
17121
- ### Axis, interaction, and transition configurations
17122
-
17123
- ```ts
17124
- type HorizontalAxisPosition = 'bottom' | 'top';
17125
- type VerticalAxisPosition = 'left' | 'right';
17126
- type AxisPosition = HorizontalAxisPosition | VerticalAxisPosition;
17127
-
17128
- interface AxisConfig {
17129
- grid?: boolean | GridConfig;
17168
+ export interface AxisConfig {
17169
+ grid?: GridConfig | boolean;
17130
17170
  label?: string;
17131
17171
  position?: TPosition;
17132
17172
  tickCount?: number;
17133
17173
  tickFormat?: (value: Date | number | string) => string;
17134
17174
  }
17135
17175
 
17136
- type XAxisConfig = AxisConfig;
17137
- type YAxisConfig = AxisConfig;
17138
-
17139
- interface GridConfig {
17140
- color?: string;
17141
- dash?: string;
17142
- }
17176
+ export type XAxisConfig = AxisConfig;
17177
+ export type YAxisConfig = AxisConfig;
17143
17178
 
17144
- interface TooltipConfig {
17179
+ export interface TooltipConfig {
17145
17180
  offset?: number;
17146
17181
  render?: (datum: Datum, series: Series) => Node | string;
17147
17182
  }
17148
17183
 
17149
- interface CrosshairConfig {
17184
+ export interface CrosshairConfig {
17150
17185
  horizontal?: boolean;
17151
17186
  snap?: boolean;
17152
17187
  vertical?: boolean;
17153
17188
  }
17154
17189
 
17155
- interface LegendConfig {
17156
- position?: LegendPosition;
17157
- }
17158
-
17159
- type LegendPosition = 'bottom' | 'left' | 'right' | 'top';
17190
+ export type LegendPosition = 'bottom' | 'left' | 'right' | 'top';
17160
17191
 
17161
- interface TransitionConfig {
17162
- duration?: number;
17163
- easing?: EasingFn | 'ease-in' | 'ease-in-out' | 'ease-out' | 'linear';
17164
- preference?: 'always' | 'never' | 'system';
17165
- stagger?: number;
17192
+ export interface LegendConfig {
17193
+ position?: LegendPosition;
17166
17194
  }
17167
-
17168
- type EasingFn = (t: number) => number;
17169
17195
  ```
17170
17196
 
17171
- Tooltip strings are assigned through `textContent`. Return a `Node` for structured content.
17197
+ The 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.
17172
17198
 
17173
- ---
17199
+ 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.
17174
17200
 
17175
- ### Scale types
17201
+ ### Chart Configuration Types
17176
17202
 
17177
17203
  ```ts
17178
- interface Scale {
17179
- readonly domain: readonly [T, T];
17180
- readonly range: readonly [number, number];
17181
- map(value: T): number;
17182
- invert(pixel: number): T;
17183
- ticks(count?: number): T[];
17204
+ export interface BaseChartConfig {
17205
+ a11y?: ChartA11y;
17206
+ legend?: LegendConfig | boolean;
17207
+ margin?: Partial;
17208
+ onClick?: (event: ChartEvent) => void;
17209
+ onHover?: (event: ChartEvent | null) => void;
17210
+ tooltip?: TooltipConfig | boolean;
17211
+ transition?: TransitionOption;
17212
+ xAxis?: XAxisConfig | false;
17213
+ yAxis?: YAxisConfig | false;
17184
17214
  }
17185
17215
 
17186
- interface BandScale {
17187
- readonly domain: readonly string[];
17188
- readonly range: readonly [number, number];
17189
- map(value: string): number;
17190
- ticks(count?: number): string[];
17191
- bandwidth(): number;
17192
- gap(): number;
17216
+ export interface LineSeriesConfig extends Series {
17217
+ curve?: 'linear' | 'monotone' | 'step';
17218
+ pointRadius?: number;
17219
+ showPoints?: boolean;
17220
+ strokeWidth?: number;
17221
+ yAxis?: 'left' | 'right';
17193
17222
  }
17194
17223
 
17195
- interface LinearScaleConfig {
17196
- domain: [number, number];
17197
- range: [number, number];
17198
- clamp?: boolean;
17199
- nice?: boolean;
17224
+ export interface LineChartConfig extends BaseChartConfig {
17225
+ crosshair?: CrosshairConfig | boolean;
17226
+ rightYAxis?: Omit;
17227
+ series: LineSeriesConfig[];
17200
17228
  }
17201
17229
 
17202
- interface TimeScaleConfig {
17203
- domain: [Date, Date];
17204
- range: [number, number];
17205
- nice?: boolean;
17230
+ export interface AreaSeriesConfig extends Series {
17231
+ curve?: 'linear' | 'monotone' | 'step';
17232
+ fill?: 'gradient' | 'solid';
17233
+ fillOpacity?: number;
17234
+ showLine?: boolean;
17206
17235
  }
17207
17236
 
17208
- interface BandScaleConfig {
17209
- domain: string[];
17210
- range: [number, number];
17211
- padding?: number;
17212
- paddingOuter?: number;
17237
+ export interface AreaChartConfig extends BaseChartConfig {
17238
+ crosshair?: CrosshairConfig | boolean;
17239
+ series: AreaSeriesConfig[];
17213
17240
  }
17214
- ```
17215
17241
 
17216
- ---
17217
-
17218
- ### Remaining unions and theme type
17219
-
17220
- ```ts
17221
- type BarVariant = 'grouped' | 'grouped-horizontal' | 'stacked' | 'stacked-horizontal';
17222
- type PieVariant = 'donut' | 'pie' | 'semi';
17223
- type SparklineVariant = 'area' | 'bar' | 'line' | 'stack';
17224
-
17225
- interface PrismTheme {
17226
- colors?: string[];
17227
- fontFamily?: string;
17228
- gridColor?: string;
17229
- gridOpacity?: number;
17242
+ export interface ScatterSeriesConfig extends Series {
17243
+ pointRadius?: number;
17230
17244
  }
17231
- ```
17232
-
17233
- ## Errors
17234
-
17235
- ### `PrismError`
17236
-
17237
- Base class for every Prism-originated error. It supports standard `ErrorOptions` cause chaining.
17238
-
17239
- ### `PrismRenderError`
17240
-
17241
- Extends `PrismError`. Thrown for invalid containers, failed initial renders, and attempts to update disposed charts.
17242
-
17243
- ### Usage Guide
17244
-
17245
- ## Basic Usage
17246
-
17247
- Every chart needs a container element with defined dimensions and the theme CSS:
17248
-
17249
- ```ts
17250
- import { createLineChart } from '@vielzeug/prism';
17251
- import '@vielzeug/prism/theme';
17252
-
17253
- const container = document.querySelector('#chart')!;
17254
- const chart = createLineChart(container, {
17255
- series: [
17256
- {
17257
- name: 'Revenue',
17258
- data: [
17259
- { key: 1, value: 10 },
17260
- { key: 2, value: 16 },
17261
- ],
17262
- },
17263
- ],
17264
- });
17265
-
17266
- chart.dispose();
17267
- ```
17268
-
17269
- ```html
17270
-
17271
- ```
17272
-
17273
- Prism observes the container size via `ResizeObserver` and re-renders automatically on resize. If the container has zero dimensions at mount time, a `warn` is emitted in development: ensure the container has layout before calling the chart factory.
17274
-
17275
- ## Updating Data
17276
-
17277
- Chart factories render synchronously and return a typed handle. Call `update()` with the same data shape used by the factory when application state changes.
17278
-
17279
- ```ts
17280
- import { createLineChart } from '@vielzeug/prism';
17281
-
17282
- const chart = createLineChart(container, {
17283
- series: [{ data: [{ key: 1, value: 10 }], name: 'Live' }],
17284
- });
17285
-
17286
- chart.update([
17287
- {
17288
- data: [
17289
- { key: 1, value: 10 },
17290
- { key: 2, value: 20 },
17291
- ],
17292
- name: 'Live',
17293
- },
17294
- ]);
17295
- ```
17296
-
17297
- `update()` is state-library neutral. Call it from a framework effect, store subscription, event handler, or request callback. It throws after the chart is disposed instead of silently retaining detached state.
17298
-
17299
- ## Line Charts
17300
-
17301
- ```ts
17302
- import { createLineChart } from '@vielzeug/prism';
17303
-
17304
- const chart = createLineChart(container, {
17305
- series: [
17306
- {
17307
- name: 'Revenue',
17308
- data: [
17309
- { key: 1, value: 100 },
17310
- { key: 2, value: 150 },
17311
- { key: 3, value: 130 },
17312
- ],
17313
- color: '#3b82f6',
17314
- curve: 'monotone', // 'linear' | 'monotone' | 'step'
17315
- strokeWidth: 2,
17316
- showPoints: true,
17317
- pointRadius: 4,
17318
- },
17319
- ],
17320
- xAxis: { position: 'bottom' },
17321
- yAxis: { position: 'left', grid: true },
17322
- tooltip: true,
17323
- crosshair: true,
17324
- });
17325
- ```
17326
17245
 
17327
- ### Multiple Series
17328
-
17329
- ```ts
17330
- const chart = createLineChart(container, {
17331
- series: [
17332
- { name: 'Revenue', data: revenueData, color: '#3b82f6' },
17333
- { name: 'Expenses', data: expenseData, color: '#ef4444' },
17334
- ],
17335
- xAxis: { position: 'bottom' },
17336
- yAxis: { position: 'left', grid: true },
17337
- });
17338
- ```
17339
-
17340
- ### Independent Value Axes
17341
-
17342
- Assign a line series to `yAxis: 'right'` when its values need a different range. Both axes share the same X scale, while tooltips retain the original values.
17343
-
17344
- ```ts
17345
- const chart = createLineChart(container, {
17346
- series: [
17347
- { name: 'Health', data: [{ key: 0, value: 30 }, { key: 1, value: 20 }] },
17348
- { name: 'Damage', yAxis: 'right', data: [{ key: 0, value: 0 }, { key: 1, value: 300 }] },
17349
- ],
17350
- yAxis: { label: 'Health' },
17351
- rightYAxis: { label: 'Damage' },
17352
- tooltip: true,
17353
- });
17354
- ```
17355
-
17356
- The primary axis is on the left when right-axis series are present. Grid configuration belongs to the primary axis. Dual-axis curve crossings do not imply equal numeric values. Preserve each marker's axis assignment when calling `update()`.
17357
-
17358
- ### Time-based X Axis
17359
-
17360
- When data points use `Date` objects for `key`, Prism automatically applies a time scale. The domain spans exactly the first to the last date, so the series fills the plot width:
17361
-
17362
- ```ts
17363
- const chart = createLineChart(container, {
17364
- series: [
17365
- {
17366
- name: 'Signups',
17367
- data: [
17368
- { key: new Date('2024-01-01'), value: 50 },
17369
- { key: new Date('2024-02-01'), value: 80 },
17370
- { key: new Date('2024-03-01'), value: 120 },
17371
- ],
17372
- },
17373
- ],
17374
- xAxis: { position: 'bottom', tickFormat: (d) => (d as Date).toLocaleDateString() },
17375
- yAxis: { position: 'left' },
17376
- });
17377
- ```
17378
-
17379
- ## Bar Charts
17380
-
17381
- ```ts
17382
- import { createBarChart } from '@vielzeug/prism';
17383
-
17384
- const chart = createBarChart(container, {
17385
- series: [
17386
- {
17387
- name: 'Sales',
17388
- data: [
17389
- { key: 'Q1', value: 200 },
17390
- { key: 'Q2', value: 350 },
17391
- { key: 'Q3', value: 280 },
17392
- { key: 'Q4', value: 400 },
17393
- ],
17394
- borderRadius: 4,
17395
- },
17396
- ],
17397
- xAxis: { position: 'bottom' },
17398
- yAxis: { position: 'left', grid: true },
17399
- tooltip: true,
17400
- });
17401
- ```
17402
-
17403
- ### Variants
17404
-
17405
- Select the bar layout with `variant`:
17406
-
17407
- | Value | Layout |
17408
- | ---------------------- | -------------------------- |
17409
- | `'grouped'` | Vertical grouped (default) |
17410
- | `'stacked'` | Vertical stacked |
17411
- | `'grouped-horizontal'` | Horizontal grouped |
17412
- | `'stacked-horizontal'` | Horizontal stacked |
17413
-
17414
- ```ts
17415
- const chart = createBarChart(container, {
17416
- variant: 'stacked',
17417
- series: [
17418
- { name: 'Mobile', data: mobileData, color: '#3b82f6', borderRadius: 0 },
17419
- { name: 'Desktop', data: desktopData, color: '#10b981', borderRadius: 0 },
17420
- ],
17421
- xAxis: { position: 'bottom' },
17422
- yAxis: { position: 'left', grid: true },
17423
- tooltip: true,
17424
- legend: true,
17425
- });
17426
- ```
17427
-
17428
- For horizontal layouts, categories appear on the Y axis and values on the X axis:
17429
-
17430
- ```ts
17431
- const chart = createBarChart(container, {
17432
- variant: 'grouped-horizontal',
17433
- series: [{ name: 'Revenue', data, color: '#3b82f6' }],
17434
- xAxis: { position: 'bottom', grid: true },
17435
- yAxis: { position: 'left' },
17436
- });
17437
- ```
17438
-
17439
- ### Grouped Bars
17440
-
17441
- Multiple series with `variant: 'grouped'` (default) render side-by-side:
17442
-
17443
- ```ts
17444
- const chart = createBarChart(container, {
17445
- series: [
17446
- { name: '2023', data: lastYearData, color: '#94a3b8' },
17447
- { name: '2024', data: thisYearData, color: '#3b82f6' },
17448
- ],
17449
- });
17450
- ```
17451
-
17452
- ## Area Charts
17453
-
17454
- Area fills use a vertical gradient that fades toward the baseline. Set `fill: 'solid'` for a flat fill:
17455
-
17456
- ```ts
17457
- import { createAreaChart } from '@vielzeug/prism';
17458
-
17459
- const chart = createAreaChart(container, {
17460
- series: [
17461
- {
17462
- name: 'Users',
17463
- data: userData,
17464
- curve: 'monotone',
17465
- fill: 'gradient', // 'gradient' (default) | 'solid'
17466
- showLine: true,
17467
- },
17468
- ],
17469
- xAxis: { position: 'bottom' },
17470
- yAxis: { position: 'left', grid: true },
17471
- crosshair: true,
17472
- });
17473
- ```
17474
-
17475
- `fillOpacity` overrides the theme opacity for one series.
17476
-
17477
- ## Forecast / emphasis styling
17478
-
17479
- A single series often mixes points that should look different — actuals plus a
17480
- forecast tail, or a run that crossed a budget. Rather than split the series,
17481
- carry `dash` and `opacity` on the individual datums. They are styling-only:
17482
- tooltips, hover, keyboard, and screen-reader behavior never change.
17483
-
17484
- For a forecast tail, map a parallel `types` array onto the datums:
17485
-
17486
- ```ts
17487
- import { createLineChart } from '@vielzeug/prism';
17488
-
17489
- const types = ['REAL', 'REAL', 'FORECAST', 'FORECAST'];
17490
-
17491
- const chart = createLineChart(container, {
17492
- series: [
17493
- {
17494
- name: 'Cost',
17495
- data: values.map((value, i) => ({
17496
- key: months[i],
17497
- value,
17498
- ...(types[i] === 'FORECAST' ? { dash: '5 5', opacity: 0.4 } : {}),
17499
- })),
17500
- },
17501
- ],
17502
- });
17503
- ```
17504
-
17505
- On a line chart, a datum's `dash` styles the segment from that point to the
17506
- next, so the forecast run renders dashed while the actuals stay solid; its
17507
- `opacity` fades that segment and the point marker. The last datum's `dash` has
17508
- no segment to paint and is ignored. On a bar chart, both fields apply to that
17509
- datum's own bar — a `dash` adds a 1px series-color stroke so the dash reads on
17510
- the filled rect. On an area chart they style the top line only; the fill keeps
17511
- its series-level treatment.
17512
-
17513
- The same mapping fades bars:
17246
+ export interface ScatterChartConfig extends BaseChartConfig {
17247
+ crosshair?: CrosshairConfig | boolean;
17248
+ series: ScatterSeriesConfig[];
17249
+ }
17514
17250
 
17515
- ```ts
17516
- import { createBarChart } from '@vielzeug/prism';
17251
+ export interface BarSeriesConfig extends Series {
17252
+ borderRadius?: number;
17253
+ }
17517
17254
 
17518
- const chart = createBarChart(container, {
17519
- series: [
17520
- {
17521
- name: 'Cost',
17522
- data: values.map((value, i) => ({
17523
- key: months[i],
17524
- value,
17525
- ...(types[i] === 'FORECAST' ? { dash: '5 5', opacity: 0.4 } : {}),
17526
- })),
17527
- },
17528
- ],
17529
- });
17530
- ```
17255
+ export type BarVariant = 'grouped' | 'grouped-horizontal' | 'stacked' | 'stacked-horizontal';
17531
17256
 
17532
- Because the fields live on the data, a normal `update(newSeries)` re-applies
17533
- them; removing the fields on the next update clears the styling. A series with
17534
- no `dash`/`opacity` anywhere renders exactly as it did before these fields
17535
- existed.
17257
+ export interface BarChartConfig extends BaseChartConfig {
17258
+ series: BarSeriesConfig[];
17259
+ variant?: BarVariant;
17260
+ }
17536
17261
 
17537
- ## Pie, Donut, and Semi-circle Charts
17262
+ export type PieVariant = 'donut' | 'pie' | 'semi';
17538
17263
 
17539
- All three variants use `createPieChart` with the `variant` field:
17264
+ export interface PieSliceConfig {
17265
+ color?: string;
17266
+ label?: string;
17267
+ value: number;
17268
+ }
17540
17269
 
17541
- ```ts
17542
- import { createPieChart } from '@vielzeug/prism';
17270
+ export interface PieEvent {
17271
+ index: number;
17272
+ originalEvent: Event;
17273
+ slice: PieSliceConfig;
17274
+ }
17543
17275
 
17544
- const chart = createPieChart(container, {
17545
- data: [
17546
- { label: 'Direct', value: 42, color: '#3b82f6' },
17547
- { label: 'Organic', value: 28, color: '#10b981' },
17548
- { label: 'Referral', value: 18, color: '#f59e0b' },
17549
- { label: 'Social', value: 12, color: '#8b5cf6' },
17550
- ],
17551
- variant: 'donut', // 'pie' | 'donut' | 'semi'
17552
- tooltip: true,
17553
- transition: { duration: 400, easing: 'ease-out' },
17554
- });
17555
- ```
17276
+ export interface PieChartConfig extends Omit {
17277
+ cornerRadius?: number;
17278
+ data: PieSliceConfig[];
17279
+ innerRadius?: number;
17280
+ onClick?: (event: PieEvent) => void;
17281
+ onHover?: (event: PieEvent | null) => void;
17282
+ padPixels?: number;
17283
+ variant?: PieVariant;
17284
+ }
17556
17285
 
17557
- ### Variants
17286
+ export interface RadarAxisConfig {
17287
+ format?: (value: number) => string;
17288
+ key: string;
17289
+ label: string;
17290
+ max?: number;
17291
+ min?: number;
17292
+ }
17558
17293
 
17559
- | Value | Shape |
17560
- | --------- | ------------------------------------------------------- |
17561
- | `'pie'` | Full circle, no hole |
17562
- | `'donut'` | Full circle with inner hole (~55% of outer by default) |
17563
- | `'semi'` | Top-half semicircle with inner hole: useful for gauges |
17294
+ export type RadarSeriesConfig = Series>;
17564
17295
 
17565
- ### Inner Radius
17296
+ export interface RadarGridConfig {
17297
+ bands?: boolean;
17298
+ labels?: boolean;
17299
+ levels?: number;
17300
+ shape?: 'circle' | 'polygon';
17301
+ }
17566
17302
 
17567
- `innerRadius` overrides the automatic calculation:
17303
+ export interface RadarAxisValue {
17304
+ datum: Datum | undefined;
17305
+ series: RadarSeriesConfig;
17306
+ }
17568
17307
 
17569
- ```ts
17570
- createPieChart(container, {
17571
- data,
17572
- variant: 'donut',
17573
- innerRadius: 60, // explicit pixels
17574
- });
17575
- ```
17308
+ export interface RadarEvent {
17309
+ axis: RadarAxisConfig;
17310
+ index: number;
17311
+ originalEvent: Event;
17312
+ values: RadarAxisValue[];
17313
+ }
17576
17314
 
17577
- ### Slice Labels
17315
+ export interface RadarChartConfig extends Omit {
17316
+ axes: RadarAxisConfig[];
17317
+ curve?: 'linear' | 'rounded';
17318
+ domain?: [number, number];
17319
+ fill?: 'gradient' | 'none' | 'solid';
17320
+ grid?: RadarGridConfig | boolean;
17321
+ onClick?: (event: RadarEvent) => void;
17322
+ onHover?: (event: RadarEvent | null) => void;
17323
+ series: RadarSeriesConfig[];
17324
+ showPoints?: boolean;
17325
+ showValues?: boolean;
17326
+ startAngle?: number;
17327
+ }
17578
17328
 
17579
- Set `label` on each `PieSliceConfig` to render text at the arc centroid. A label that does not fit inside its slice is not drawn; the value stays available through the tooltip, legend, and keyboard announcements:
17329
+ export type SparklineVariant = 'area' | 'bar' | 'line' | 'stack';
17580
17330
 
17581
- ```ts
17582
- { value: 42, label: 'Direct' }
17583
- ```
17331
+ export interface StackSegment {
17332
+ color?: string;
17333
+ label?: string;
17334
+ value: number;
17335
+ }
17584
17336
 
17585
- Style labels via CSS:
17337
+ export interface SparklineEvent {
17338
+ index: number;
17339
+ originalEvent: Event;
17340
+ value: number;
17341
+ }
17586
17342
 
17587
- ```css
17588
- :root {
17589
- --prism-pie-label-color: #fff;
17590
- --prism-pie-label-size: 11px;
17343
+ export interface SparklineConfig {
17344
+ a11y?: ChartA11y;
17345
+ color?: string;
17346
+ cornerRadius?: number;
17347
+ curve?: 'linear' | 'monotone' | 'step';
17348
+ data: number[] | StackSegment[];
17349
+ fillOpacity?: number;
17350
+ onClick?: (event: SparklineEvent) => void;
17351
+ onHover?: (event: SparklineEvent | null) => void;
17352
+ padPixels?: number;
17353
+ showEndPoint?: boolean;
17354
+ strokeWidth?: number;
17355
+ transition?: TransitionOption;
17356
+ variant?: SparklineVariant;
17591
17357
  }
17592
17358
  ```
17593
17359
 
17594
- ### Updating Data
17360
+ ## Errors
17361
+
17362
+ ### `PrismError`
17595
17363
 
17596
17364
  ```ts
17597
- const chart = createPieChart(container, {
17598
- data: [
17599
- { label: 'A', value: 40 },
17600
- { label: 'B', value: 60 },
17601
- ],
17602
- variant: 'donut',
17603
- });
17604
-
17605
- chart.update([
17606
- { label: 'A', value: 55 },
17607
- { label: 'B', value: 45 },
17608
- ]);
17365
+ class PrismError extends Error {
17366
+ constructor(message: string, opts?: ErrorOptions);
17367
+ }
17609
17368
  ```
17610
17369
 
17611
- ### Event Hooks
17370
+ Base class for Prism-originated errors. Its `name` is set to the concrete error class name, and it supports the standard `ErrorOptions` including `cause`.
17371
+
17372
+ ### `PrismRenderError`
17612
17373
 
17613
17374
  ```ts
17614
- createPieChart(container, {
17615
- data,
17616
- onHover: (event) => {
17617
- // event is PieEvent | null (null on mouseleave)
17618
- if (event) console.log(event.index, event.slice.label, event.slice.value);
17619
- },
17620
- onClick: (event) => {
17621
- console.log('clicked', event.index, event.slice.label);
17622
- },
17623
- });
17375
+ class PrismRenderError extends PrismError {}
17624
17376
  ```
17625
17377
 
17626
- `PieEvent` provides `index`, the hovered/clicked `slice` (`PieSliceConfig`), and the `originalEvent` (`MouseEvent` or `KeyboardEvent`).
17378
+ Thrown 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.
17627
17379
 
17628
- ## Radar Charts
17380
+ ### Usage Guide
17381
+
17382
+ ## Basic Usage
17629
17383
 
17630
- `createRadarChart` compares several values on one shape, such as a hero's stats. Each series supplies values keyed by `axis.key`:
17384
+ Create 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.
17631
17385
 
17632
17386
  ```ts
17633
- import { createRadarChart } from '@vielzeug/prism';
17387
+ import { createLineChart } from '@vielzeug/prism';
17388
+ import '@vielzeug/prism/theme.css';
17634
17389
 
17635
- const axes = [
17636
- { key: 'str', label: 'Strength' },
17637
- { key: 'agi', label: 'Agility' },
17638
- { key: 'int', label: 'Intellect' },
17639
- { key: 'end', label: 'Endurance' },
17640
- { key: 'spd', label: 'Speed', max: 5 },
17641
- ];
17390
+ const container = document.createElement('div');
17391
+ container.style.cssText = 'width:640px;height:320px';
17392
+ document.body.append(container);
17642
17393
 
17643
- const chart = createRadarChart(container, {
17644
- a11y: { ariaLabel: 'Hero stats' },
17645
- axes,
17646
- domain: [0, 10],
17647
- fill: 'gradient',
17648
- showValues: true,
17394
+ const chart = createLineChart(container, {
17395
+ a11y: { ariaLabel: 'Revenue by month' },
17649
17396
  series: [
17650
17397
  {
17651
- name: 'Adam',
17398
+ name: 'Revenue',
17652
17399
  data: [
17653
- { key: 'str', value: 8 },
17654
- { key: 'agi', value: 6 },
17655
- { key: 'int', value: 4 },
17656
- { key: 'end', value: 9 },
17657
- { key: 'spd', value: 3 },
17400
+ { key: 1, value: 10 },
17401
+ { key: 2, value: 16 },
17658
17402
  ],
17659
17403
  },
17660
17404
  ],
17405
+ tooltip: true,
17661
17406
  });
17407
+
17408
+ chart.dispose();
17662
17409
  ```
17663
17410
 
17664
- ### Scales
17411
+ Each 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.
17665
17412
 
17666
- Every axis shares `domain` (or a nice range around the data when omitted). Give an axis its own `min`/`max` when its stat uses a different range: above, Speed runs 0 to 5 while the rest run 0 to 10, so each still fills the chart.
17413
+ ## Updating Data
17667
17414
 
17668
- ### Appearance
17415
+ Keep 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.
17669
17416
 
17670
- | Option | Values | Use it for |
17671
- | --- | --- | --- |
17672
- | `fill` | `'solid'`, `'gradient'`, `'none'` | `gradient` for a single hero; `solid` or `none` when overlaying several |
17673
- | `curve` | `'linear'`, `'rounded'` | Rounded reads softer; linear keeps exact vertices |
17674
- | `grid.shape` | `'polygon'`, `'circle'` | Polygon matches the axes; circle suits many axes |
17675
- | `grid.levels` / `grid.bands` | number / boolean | Ring count and alternate shading |
17676
- | `grid.labels` | boolean | Level values; drawn only when every axis shares one domain |
17677
- | `showValues` | boolean | Value beside each vertex, formatted by `axis.format` |
17417
+ ```ts
17418
+ import { createLineChart } from '@vielzeug/prism';
17678
17419
 
17679
- ### Comparing Series
17420
+ const container = document.createElement('div');
17421
+ container.style.cssText = 'width:640px;height:320px';
17422
+ document.body.append(container);
17680
17423
 
17681
- Pass several series with `legend: true`. Hovering one shape dims the others, and hovering an axis reports every series' value on it:
17424
+ let series = [{ data: [{ key: 1, value: 10 }], name: 'Live' }];
17425
+ const chart = createLineChart(container, { series });
17682
17426
 
17683
- ```ts
17684
- createRadarChart(container, {
17685
- axes,
17686
- legend: true,
17687
- series: [adam, eve],
17688
- tooltip: true, // "Agility: Adam 6, Eve 9"
17689
- onHover: (event) => event && console.log(event.axis.label, event.values),
17690
- });
17427
+ series = [{ data: [{ key: 1, value: 10 }, { key: 2, value: 20 }], name: 'Live' }];
17428
+ chart.update(series);
17429
+ chart.dispose();
17691
17430
  ```
17692
17431
 
17693
- ## Sparklines
17432
+ `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; 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()`.
17433
+
17434
+ ## Line Charts
17694
17435
 
17695
- Sparklines are minimal inline charts with no axes, no legend, and no margin: designed to live inline with text or inside table cells.
17436
+ Line 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`.
17696
17437
 
17697
17438
  ```ts
17698
- import { createSparkline } from '@vielzeug/prism';
17439
+ import { createLineChart } from '@vielzeug/prism';
17440
+
17441
+ const container = document.createElement('div');
17442
+ container.style.cssText = 'width:640px;height:320px';
17443
+ document.body.append(container);
17699
17444
 
17700
- const spark = createSparkline(container, {
17701
- data: [12, 18, 14, 22, 19, 28],
17702
- variant: 'line', // 'line' | 'area' | 'bar' | 'stack' (default: 'line')
17703
- color: '#3b82f6',
17704
- curve: 'monotone',
17705
- strokeWidth: 1.5,
17445
+ const chart = createLineChart(container, {
17446
+ a11y: { ariaLabel: 'Revenue and expenses' },
17447
+ series: [
17448
+ {
17449
+ curve: 'monotone',
17450
+ data: [{ key: 1, value: 100 }, { key: 2, value: 150 }, { key: 3, value: 130 }],
17451
+ name: 'Revenue',
17452
+ showPoints: true,
17453
+ },
17454
+ {
17455
+ color: '#c2410c',
17456
+ data: [{ key: 1, value: 70 }, { key: 2, value: 95 }, { key: 3, value: 110 }],
17457
+ name: 'Expenses',
17458
+ },
17459
+ ],
17460
+ tooltip: true,
17461
+ xAxis: { label: 'Month' },
17462
+ yAxis: { grid: true, label: 'Dollars' },
17706
17463
  });
17707
17464
 
17708
- spark.dispose();
17465
+ chart.dispose();
17709
17466
  ```
17710
17467
 
17711
- ### Variants
17468
+ ### Independent Value Axes
17712
17469
 
17713
- - **`line`**: simple polyline path (default)
17714
- - **`area`**: gradient-filled area + line overlay
17715
- - **`bar`**: vertical bar for each data point
17716
- - **`stack`**: horizontal proportional segments; use `StackSegment[]` for `data` with per-segment colors
17470
+ Assign 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.
17717
17471
 
17718
- Line and area sparklines mark the latest value with a dot. Set `showEndPoint: false` to hide it.
17472
+ ```ts
17473
+ import { createLineChart } from '@vielzeug/prism';
17719
17474
 
17720
- ### Updating Data
17475
+ const container = document.createElement('div');
17476
+ container.style.cssText = 'width:640px;height:320px';
17477
+ document.body.append(container);
17721
17478
 
17722
- ```ts
17723
- const spark = createSparkline(container, {
17724
- data: [12, 18, 14, 22],
17725
- variant: 'area',
17479
+ const chart = createLineChart(container, {
17480
+ a11y: { ariaLabel: 'Health and damage by level' },
17481
+ rightYAxis: { label: 'Damage' },
17482
+ series: [
17483
+ { data: [{ key: 1, value: 30 }, { key: 2, value: 20 }], name: 'Health' },
17484
+ { data: [{ key: 1, value: 100 }, { key: 2, value: 300 }], name: 'Damage', yAxis: 'right' },
17485
+ ],
17486
+ yAxis: { grid: true, label: 'Health' },
17726
17487
  });
17727
17488
 
17728
- spark.update([12, 18, 14, 22, 30]);
17489
+ chart.dispose();
17729
17490
  ```
17730
17491
 
17731
- ### Event Hooks
17492
+ When 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.
17732
17493
 
17733
- Sparklines use simplified hooks: `SparklineEvent` rather than full `ChartEvent`. With a hook set, a marker follows the hovered value:
17494
+ ### Time-Based X Axis
17495
+
17496
+ Use `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.
17734
17497
 
17735
17498
  ```ts
17736
- const spark = createSparkline(container, {
17737
- data: [10, 20, 30],
17738
- onHover: (event) => {
17739
- // event is SparklineEvent | null (null on mouseleave)
17740
- if (event) console.log(`Hovering point ${event.index}: ${event.value}`);
17741
- },
17742
- onClick: (event) => {
17743
- console.log(`Clicked point ${event.index}: ${event.value}`);
17744
- },
17499
+ import { createLineChart } from '@vielzeug/prism';
17500
+
17501
+ const container = document.createElement('div');
17502
+ container.style.cssText = 'width:640px;height:320px';
17503
+ document.body.append(container);
17504
+
17505
+ const formatter = new Intl.DateTimeFormat(undefined, { month: 'short', day: 'numeric' });
17506
+ const chart = createLineChart(container, {
17507
+ a11y: { ariaLabel: 'Signups over time' },
17508
+ series: [{
17509
+ data: [
17510
+ { key: new Date('2026-09-01'), value: 50 },
17511
+ { key: new Date('2026-09-08'), value: 80 },
17512
+ { key: new Date('2026-09-15'), value: 120 },
17513
+ ],
17514
+ name: 'Signups',
17515
+ }],
17516
+ xAxis: { tickFormat: (value) => value instanceof Date ? formatter.format(value) : String(value) },
17745
17517
  });
17518
+
17519
+ chart.dispose();
17746
17520
  ```
17747
17521
 
17748
- `SparklineEvent` provides `index`, the hovered/clicked `value`, and the `originalEvent`.
17522
+ `tickFormat` formats axis labels only. Tooltip content and accessibility announcements use the underlying keys and values.
17749
17523
 
17750
- > **Note:** Sparklines have no keyboard navigation. Label one with `a11y.ariaLabel` only when the trend carries meaning, and state the key value in surrounding text.
17524
+ ## Bar Charts
17751
17525
 
17752
- ## Axes and Grid
17526
+ Bar 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.
17753
17527
 
17754
17528
  ```ts
17755
- {
17756
- xAxis: {
17757
- position: 'bottom', // 'top' | 'bottom'
17758
- tickCount: 5,
17759
- tickFormat: (v) => `$${v}`,
17760
- label: 'Month',
17761
- grid: true, // or { color: '#ddd', dash: '4 2' }
17762
- },
17763
- yAxis: {
17764
- position: 'left', // 'left' | 'right'
17765
- grid: { color: '#f0f0f0' },
17766
- label: 'Revenue ($)',
17767
- },
17768
- }
17769
- ```
17770
-
17771
- Category axes show every label when the labels fit and thin them out only when they would overlap. Set `tickCount` to choose the density yourself.
17529
+ import { createBarChart } from '@vielzeug/prism';
17772
17530
 
17773
- ## Tooltips
17531
+ const container = document.createElement('div');
17532
+ container.style.cssText = 'width:640px;height:320px';
17533
+ document.body.append(container);
17774
17534
 
17775
- Enable with `tooltip: true`. On line, area, bar, and radar charts the default tooltip compares every series at the active key: a title, then a colour swatch, name, and value per series. Provide a custom `render` function to replace it. Strings are rendered as text. Return a DOM node for structured content:
17535
+ const chart = createBarChart(container, {
17536
+ a11y: { ariaLabel: 'Sales by quarter' },
17537
+ series: [{
17538
+ data: [{ key: 'Q1', value: 200 }, { key: 'Q2', value: 350 }, { key: 'Q3', value: 280 }],
17539
+ name: 'Sales',
17540
+ }],
17541
+ tooltip: true,
17542
+ });
17776
17543
 
17777
- ```ts
17778
- {
17779
- tooltip: {
17780
- offset: 12,
17781
- render: (datum, series) => {
17782
- const content = document.createElement('strong');
17783
- content.textContent = `${series.name}: ${datum.value.toLocaleString()}`;
17784
- return content;
17785
- },
17786
- },
17787
- }
17544
+ chart.dispose();
17788
17545
  ```
17789
17546
 
17790
- Prism never injects tooltip strings as HTML, so custom rendering does not require a sanitizer.
17547
+ Choose `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.
17791
17548
 
17792
- The tooltip element is scoped inside the chart container (not `document.body`) and is removed automatically on `dispose()`.
17549
+ Axes 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.
17793
17550
 
17794
- ## Crosshair
17551
+ ## Area Charts
17795
17552
 
17796
- A vertical guide that snaps to the nearest data point:
17553
+ Area 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.
17797
17554
 
17798
17555
  ```ts
17799
- {
17556
+ import { createAreaChart } from '@vielzeug/prism';
17557
+
17558
+ const container = document.createElement('div');
17559
+ container.style.cssText = 'width:640px;height:320px';
17560
+ document.body.append(container);
17561
+
17562
+ const chart = createAreaChart(container, {
17563
+ a11y: { ariaLabel: 'Visitors over time' },
17800
17564
  crosshair: true,
17801
- // or configure:
17802
- crosshair: { vertical: true, horizontal: true, snap: true },
17803
- }
17565
+ series: [{
17566
+ curve: 'monotone',
17567
+ data: [{ key: 1, value: 12 }, { key: 2, value: 18 }, { key: 3, value: 15 }],
17568
+ fill: 'solid',
17569
+ fillOpacity: 0.2,
17570
+ name: 'Visitors',
17571
+ showLine: true,
17572
+ }],
17573
+ });
17574
+
17575
+ chart.dispose();
17804
17576
  ```
17805
17577
 
17806
- ## Legend
17578
+ ## Scatter Charts
17807
17579
 
17808
- Enable with `legend: true` (defaults to `bottom`) or configure position:
17580
+ Scatter charts treat every datum as an independent point: `key` is the numeric 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.
17809
17581
 
17810
17582
  ```ts
17811
- {
17812
- legend: true,
17813
- // or:
17814
- legend: { position: 'top' }, // 'top' | 'bottom' | 'left' | 'right'
17815
- }
17816
- ```
17583
+ import { createScatterChart } from '@vielzeug/prism';
17817
17584
 
17818
- The legend renders as a `div` placed outside the SVG. Each item shows a color swatch and the series `name`. Customize via CSS:
17585
+ const container = document.createElement('div');
17586
+ container.style.cssText = 'width:640px;height:320px';
17587
+ document.body.append(container);
17819
17588
 
17820
- ```css
17821
- :root {
17822
- --prism-legend-gap: 1rem;
17823
- --prism-legend-dot-size: 0.5rem;
17824
- --prism-legend-font-size: 0.75rem;
17825
- }
17589
+ const chart = createScatterChart(container, {
17590
+ a11y: { ariaLabel: 'Height against weight' },
17591
+ series: [{
17592
+ data: [{ key: 160, value: 55 }, { key: 175, value: 70 }, { key: 190, value: 88 }],
17593
+ name: 'Cohort A',
17594
+ pointRadius: 5,
17595
+ }],
17596
+ tooltip: true,
17597
+ });
17598
+
17599
+ chart.dispose();
17826
17600
  ```
17827
17601
 
17828
- ## Event Hooks
17602
+ Scatter callbacks identify one nearest point and do not provide cross-series `values`, because points do not share a category key.
17603
+
17604
+ ## Forecast and Emphasis Styling
17829
17605
 
17830
- All charts expose `onClick` and `onHover` callbacks on the config:
17606
+ Add `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.
17831
17607
 
17832
17608
  ```ts
17609
+ import { createLineChart } from '@vielzeug/prism';
17610
+
17611
+ const container = document.createElement('div');
17612
+ container.style.cssText = 'width:640px;height:320px';
17613
+ document.body.append(container);
17614
+
17615
+ const values = [10, 13, 15, 18];
17833
17616
  const chart = createLineChart(container, {
17834
- series: [{ name: 'Revenue', data }],
17835
- onHover: (event) => {
17836
- // event is ChartEvent | null (null on mouseleave)
17837
- if (event) console.log(event.datum, event.series);
17838
- },
17839
- onClick: (event) => {
17840
- console.log('clicked', event.datum);
17841
- },
17617
+ series: [{
17618
+ data: values.map((value, index) => ({
17619
+ key: index + 1,
17620
+ value,
17621
+ ...(index >= 2 ? { dash: '5 5', opacity: 0.4 } : {}),
17622
+ })),
17623
+ name: 'Forecast',
17624
+ showPoints: true,
17625
+ }],
17842
17626
  });
17843
- ```
17844
17627
 
17845
- `ChartEvent` provides:
17846
-
17847
- - `datum`: the datum of the series nearest the pointer
17848
- - `series`: the corresponding `Series` config
17849
- - `values`: every series at the same key (line, area, and bar charts)
17850
- - `originalEvent`: the raw `MouseEvent` or, for keyboard navigation, `KeyboardEvent`
17628
+ chart.dispose();
17629
+ ```
17851
17630
 
17852
- > **Pie, radar, and sparkline events differ**: pie hooks receive a `PieEvent` (`index`, `slice`, `originalEvent`), sparkline hooks a `SparklineEvent` (`index`, `value`, `originalEvent`), and radar hooks a `RadarEvent` describing an axis. See [`PieChartConfig`](./api.md#chart-configurations) and [`createRadarChart()`](./api.md#createradarchart).
17631
+ For 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.
17853
17632
 
17854
- ## Animations
17633
+ ## Pie, Donut, and Semi Charts
17855
17634
 
17856
- Pass a `transition` config to animate enter and update transitions:
17635
+ Use `createPieChart()` for all three shapes. `variant` is fixed when the chart is created; `update()` replaces the slice data.
17857
17636
 
17858
17637
  ```ts
17859
- {
17860
- transition: {
17861
- duration: 400,
17862
- easing: 'ease-out',
17863
- stagger: 30, // ms delay between bars (bar charts) or series (radar charts)
17864
- },
17865
- }
17866
- ```
17867
-
17868
- All chart types use `requestAnimationFrame`-based interpolation and skip animation when the user prefers reduced motion (`preference: 'system'`, the default). Bar and radar charts apply `stagger` on entry; line, area, and pie charts ignore it.
17638
+ import { createPieChart } from '@vielzeug/prism';
17869
17639
 
17870
- ## Theming
17640
+ const container = document.createElement('div');
17641
+ container.style.cssText = 'width:320px;height:240px';
17642
+ document.body.append(container);
17871
17643
 
17872
- Import the default theme:
17644
+ const chart = createPieChart(container, {
17645
+ a11y: { ariaLabel: 'Orders by channel' },
17646
+ data: [
17647
+ { label: 'Direct', value: 42 },
17648
+ { label: 'Organic', value: 28 },
17649
+ { label: 'Referral', value: 18 },
17650
+ { label: 'Social', value: 12 },
17651
+ ],
17652
+ legend: true,
17653
+ tooltip: true,
17654
+ variant: 'donut',
17655
+ });
17873
17656
 
17874
- ```ts
17875
- import '@vielzeug/prism/theme';
17657
+ chart.dispose();
17876
17658
  ```
17877
17659
 
17878
- ### Programmatic Theme with `setTheme`
17660
+ The `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.
17661
+
17662
+ ## Radar Charts
17879
17663
 
17880
- Call `setTheme` once at app startup to apply custom tokens programmatically:
17664
+ Define 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.
17881
17665
 
17882
17666
  ```ts
17883
- import { setTheme } from '@vielzeug/prism';
17667
+ import { createRadarChart } from '@vielzeug/prism';
17668
+
17669
+ const container = document.createElement('div');
17670
+ container.style.cssText = 'width:400px;height:320px';
17671
+ document.body.append(container);
17884
17672
 
17885
- setTheme({
17886
- colors: ['#6366f1', '#22d3ee', '#f59e0b', '#10b981'], // replaces --prism-color-1 through -4
17887
- fontFamily: 'Inter, system-ui, sans-serif', // sets --prism-font-family
17888
- gridColor: '#e2e8f0', // sets --prism-grid-color
17889
- gridOpacity: 0.6, // sets --prism-grid-opacity
17673
+ const axes = [
17674
+ { key: 'strength', label: 'Strength' },
17675
+ { key: 'agility', label: 'Agility' },
17676
+ { key: 'endurance', label: 'Endurance' },
17677
+ ];
17678
+ const chart = createRadarChart(container, {
17679
+ a11y: { ariaLabel: 'Hero stats' },
17680
+ axes,
17681
+ domain: [0, 10],
17682
+ series: [{
17683
+ data: [{ key: 'strength', value: 8 }, { key: 'agility', value: 6 }, { key: 'endurance', value: 9 }],
17684
+ name: 'Adam',
17685
+ }],
17686
+ tooltip: true,
17890
17687
  });
17688
+
17689
+ chart.dispose();
17891
17690
  ```
17892
17691
 
17893
- `setTheme` writes to `document.documentElement` style, so it takes precedence over CSS file defaults. Call `resetTheme()` to clear every custom property `setTheme` can set and restore the default theme: useful for a theme-switcher's "reset" action or test teardown:
17692
+ The 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.
17693
+
17694
+ ## Sparklines
17695
+
17696
+ Sparklines omit axes, margins, and legends. Choose `line`, `area`, or `bar` for numeric arrays, and `stack` for an array of `StackSegment` objects.
17894
17697
 
17895
17698
  ```ts
17896
- import { resetTheme } from '@vielzeug/prism';
17699
+ import { createSparkline } from '@vielzeug/prism';
17897
17700
 
17898
- resetTheme();
17899
- ```
17701
+ const container = document.createElement('div');
17702
+ container.style.cssText = 'width:240px;height:48px';
17703
+ document.body.append(container);
17900
17704
 
17901
- ### Custom Theme (CSS)
17705
+ const chart = createSparkline(container, {
17706
+ a11y: { ariaLabel: 'Weekly orders' },
17707
+ data: [10, 14, 12, 18, 22],
17708
+ variant: 'area',
17709
+ });
17902
17710
 
17903
- ```css
17904
- :root {
17905
- --prism-color-1: #6366f1;
17906
- --prism-color-2: #22c55e;
17907
- --prism-axis-color: #71717a;
17908
- --prism-grid-color: #f4f4f5;
17909
- --prism-text-color: #18181b;
17910
- --prism-tooltip-bg: #27272a;
17911
- --prism-font-family: 'Inter', system-ui, sans-serif;
17912
- }
17711
+ chart.update([10, 14, 12, 18, 22, 25]);
17712
+ chart.dispose();
17913
17713
  ```
17914
17714
 
17915
- ### Scoped Themes
17715
+ For 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.
17916
17716
 
17917
- Apply tokens to a specific container:
17717
+ ## Interaction
17918
17718
 
17919
- ```css
17920
- .dark-dashboard {
17921
- --prism-axis-color: #64748b;
17922
- --prism-grid-color: #334155;
17923
- --prism-text-color: #e2e8f0;
17924
- }
17925
- ```
17719
+ Set `tooltip: true` to show Prism's default tooltip, or pass a `TooltipConfig` to format its content. A `render` callback receives the active `Datum` and `Series`. Returned strings are inserted as text; return a DOM `Node` for structured content.
17926
17720
 
17927
- ### Available Tokens
17721
+ ```ts
17722
+ import { createLineChart } from '@vielzeug/prism';
17928
17723
 
17929
- | Token | Default | Description |
17930
- | ------------------------- | -------------------------- | ---------------------- |
17931
- | `--prism-color-{1-8}` | Colorblind-safe palette | Series color palette |
17932
- | `--prism-bg` | `transparent` | Chart background |
17933
- | `--prism-axis-color` | `hsl(215deg 16% 47%)` | Axis lines and ticks |
17934
- | `--prism-grid-color` | `hsl(220deg 10% 75%)` | Grid lines |
17935
- | `--prism-text-color` | `hsl(215deg 25% 27%)` | Axis labels and text |
17936
- | `--prism-font-family` | `system-ui` | Chart font |
17937
- | `--prism-font-size` | `0.75rem` | Label font size |
17938
- | `--prism-tooltip-bg` | `hsl(222deg 47% 11%)` | Tooltip background |
17939
- | `--prism-tooltip-color` | `hsl(210deg 40% 98%)` | Tooltip text |
17940
- | `--prism-tooltip-radius` | `0.375rem` | Tooltip border radius |
17941
- | `--prism-crosshair-color` | `hsl(215deg 16% 47%)` | Crosshair line |
17942
- | `--prism-crosshair-dash` | `4 2` | Crosshair dash pattern |
17943
-
17944
- The palette follows Wong (2011) so adjacent series stay distinguishable under common color-vision deficiencies. Dark mode applies automatically through `prefers-color-scheme` or an `html.dark` class.
17945
-
17946
- Radar charts add their own tokens, each defaulting to a shared one:
17947
-
17948
- | Token | Default | Description |
17949
- | --- | --- | --- |
17950
- | `--prism-radar-grid-color` | `--prism-grid-color` | Rings |
17951
- | `--prism-radar-grid-opacity` | `0.7` | Ring and spoke opacity |
17952
- | `--prism-radar-band-fill` | `--prism-grid-color` | Alternate ring shading |
17953
- | `--prism-radar-band-opacity` | `0.14` | Shading opacity |
17954
- | `--prism-radar-spoke-color` | `--prism-radar-grid-color` | Spokes |
17955
- | `--prism-radar-fill-opacity` | `--prism-area-opacity` | Solid shape fill |
17956
- | `--prism-radar-stroke-width` | `--prism-line-width` | Shape outline |
17957
- | `--prism-radar-point-radius` | `--prism-point-radius` | Vertex dots |
17958
- | `--prism-radar-point-radius-active` | `--prism-point-radius-hover` | Dots on the active axis |
17959
- | `--prism-radar-label-color` / `-size` | Secondary text tokens | Axis labels |
17960
- | `--prism-radar-value-color` / `-size` | Text tokens | Vertex values |
17961
- | `--prism-radar-dim-opacity` | `--prism-dim-opacity` | Other shapes while one is hovered |
17962
-
17963
- Shared interaction and chart tokens:
17964
-
17965
- | Token | Default | Description |
17966
- | --- | --- | --- |
17967
- | `--prism-dim-opacity` | `0.35` | Series or slices outside the active one |
17968
- | `--prism-active-point-radius` | `--prism-point-radius-hover` | Points marked at the active key |
17969
- | `--prism-point-ring` | `Canvas` | Ring separating active and end points from the line |
17970
- | `--prism-focus-color` | `--prism-color-1` | Focus outline of a keyboard-focused chart |
17971
- | `--prism-bar-radius` | `4px` | Bar corner radius (stacked bars stay square) |
17972
- | `--prism-bar-band-fill` / `-opacity` | Grid colour / `0.18` | Shaded active category |
17973
- | `--prism-bar-dim-opacity` | `0.55` | Bars outside the active category |
17974
- | `--prism-area-gradient-start` / `-end` | `0.45` / `0.02` | Gradient fill opacity at the line and the baseline |
17975
- | `--prism-spark-gradient-start` / `-end` | `0.35` / `0` | Sparkline gradient fill |
17724
+ const container = document.createElement('div');
17725
+ container.style.cssText = 'width:640px;height:320px';
17726
+ document.body.append(container);
17727
+
17728
+ const chart = createLineChart(container, {
17729
+ series: [{ data: [{ key: 1, value: 4200 }, { key: 2, value: 5100 }], name: 'Revenue' }],
17730
+ tooltip: {
17731
+ render: (datum, series) => `${series.name}: ${new Intl.NumberFormat().format(datum.value)}`,
17732
+ },
17733
+ });
17976
17734
 
17977
- Explicit config such as `strokeWidth`, `fillOpacity`, or `borderRadius` always wins over these tokens.
17735
+ chart.dispose();
17736
+ ```
17978
17737
 
17979
- ## Scales (Standalone)
17738
+ `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, pie, radar, and sparkline use their chart-specific event types.
17980
17739
 
17981
- Scales can be used independently for custom visualizations:
17740
+ `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.
17982
17741
 
17983
- ```ts
17984
- import { linearScale, timeScale, bandScale } from '@vielzeug/prism';
17742
+ ### Keyboard and Announcements
17985
17743
 
17986
- const y = linearScale({ domain: [0, 100], range: [300, 0] });
17987
- y.map(50); // → 150
17988
- y.invert(150); // → 50
17989
- y.ticks(5); // → [0, 20, 40, 60, 80, 100]
17744
+ Set `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.
17990
17745
 
17991
- const x = bandScale({ domain: ['A', 'B', 'C'], range: [0, 300] });
17992
- x.map('B'); // → pixel left edge of band B
17993
- x.bandwidth(); // → width of each band
17994
- ```
17746
+ When `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 and pie/radar 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.
17995
17747
 
17996
- ## Lifecycle and Cleanup
17748
+ ## Animation
17997
17749
 
17998
- Every chart returns a `ChartHandle`. Always call `dispose()` when removing a chart:
17750
+ Chart 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.
17999
17751
 
18000
17752
  ```ts
18001
- const chart = createLineChart(container, config);
17753
+ import { createBarChart } from '@vielzeug/prism';
18002
17754
 
18003
- // When done:
18004
- chart.dispose();
17755
+ const container = document.createElement('div');
17756
+ container.style.cssText = 'width:640px;height:320px';
17757
+ document.body.append(container);
18005
17758
 
18006
- // Or with TC39 explicit resource management:
18007
- {
18008
- using chart = createLineChart(container, config);
18009
- // auto-disposed at block end
18010
- }
18011
- ```
17759
+ const chart = createBarChart(container, {
17760
+ series: [{ data: [{ key: 'A', value: 4 }, { key: 'B', value: 7 }], name: 'Count' }],
17761
+ transition: { duration: 500, easing: 'ease-out', preference: 'system' },
17762
+ });
18012
17763
 
18013
- Calling `dispose()`:
17764
+ chart.dispose();
17765
+ ```
18014
17766
 
18015
- - Cancels in-flight transitions
18016
- - Disconnects the `ResizeObserver`
18017
- - Removes the SVG element, tooltip, and legend from the DOM
18018
- - Restores container styles changed for tooltip positioning
18019
- - Is idempotent: safe to call multiple times
17767
+ Columns 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.
18020
17768
 
18021
- Call `update()` only while the handle is active. Updating a disposed chart throws `PrismRenderError`.
17769
+ `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 or radar series; bar charts cap the effective delay so long category axes 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.
18022
17770
 
18023
- ## Responsive Behavior
17771
+ ## Responsive Layout and Lifecycle
18024
17772
 
18025
- Charts resize automatically when the container dimensions change. Prism uses `ResizeObserver` internally: no manual `resize()` call is needed.
17773
+ Size the host with CSS. Prism observes its content box and updates the SVG dimensions and chart layout when it changes. Bar and line 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.
18026
17774
 
18027
- ## Runtime Observation
17775
+ Call `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.
18028
17776
 
18029
- Every handle exposes `tap()` to observe runtime behavior — resize and dispose — outside the render path. It reuses the chart's own `ResizeObserver`, so observing costs nothing extra, and handler errors are swallowed: observation never affects chart behavior.
17777
+ Use `tap()` when external code needs resize or dispose notifications rather than observing the chart DOM itself:
18030
17778
 
18031
17779
  ```ts
18032
17780
  import { createLineChart } from '@vielzeug/prism';
18033
17781
 
17782
+ const container = document.createElement('div');
17783
+ container.style.cssText = 'width:640px;height:320px';
17784
+ document.body.append(container);
17785
+
18034
17786
  const chart = createLineChart(container, {
18035
17787
  series: [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }],
18036
17788
  });
18037
- const controller = new AbortController();
18038
- const unsubscribe = chart.tap(
18039
- (event) => {
18040
- if (event.type === 'resize') console.log('resized', event.width, event.height);
18041
- if (event.type === 'dispose') console.log('disposed');
18042
- },
18043
- { signal: controller.signal },
18044
- );
17789
+ const stopObserving = chart.tap((event) => {
17790
+ if (event.type === 'resize') console.log(event.width, event.height);
17791
+ if (event.type === 'dispose') console.log('chart disposed');
17792
+ });
17793
+
17794
+ stopObserving();
17795
+ chart.dispose();
18045
17796
  ```
18046
17797
 
18047
- `tap()` returns an unsubscribe function; pass `options.signal` to detach automatically instead. The `resize` event carries the new `width`/`height` and fires after the chart re-laid-out; `dispose` fires once, before teardown completes. Tapping a disposed handle returns a no-op unsubscribe. This is separate from prism's internal validation warnings, which run automatically in development and are never part of the public API.
17798
+ `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.
18048
17799
 
18049
- ## Accessibility
17800
+ ## Theming
18050
17801
 
18051
- Label informative charts with `a11y: { ariaLabel }`. A labelled chart renders with `role="img"`, is focusable, and supports keyboard navigation. Charts without `a11y` are decorative and render with `aria-hidden="true"`.
17802
+ Import 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.
18052
17803
 
18053
- ```ts
18054
- createLineChart(container, {
18055
- a11y: { ariaLabel: 'Revenue by month' },
18056
- series: [...],
18057
- });
17804
+ ```css
17805
+ .dashboard-chart {
17806
+ --prism-color-1: #2563eb;
17807
+ --prism-color-2: #16a34a;
17808
+ --prism-grid-color: #94a3b8;
17809
+ --prism-line-width: 3;
17810
+ --prism-text-color: #172033;
17811
+ --prism-tooltip-bg: #111827;
17812
+ }
18058
17813
  ```
18059
17814
 
18060
- Mark a chart decorative explicitly when surrounding text already states its meaning:
17815
+ `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.
18061
17816
 
18062
17817
  ```ts
18063
- createSparkline(container, {
18064
- a11y: { decorative: true },
18065
- data: [...],
18066
- });
17818
+ import { resetTheme, setTheme } from '@vielzeug/prism';
17819
+
17820
+ const container = document.createElement('div');
17821
+ container.style.cssText = 'width:640px;height:320px';
17822
+ document.body.append(container);
17823
+
17824
+ setTheme({ colors: ['#2563eb', '#16a34a'], fontFamily: 'Inter, sans-serif' }, { scope: container });
17825
+ // Call resetTheme({ scope: container }) when this scoped theme is no longer needed.
17826
+ resetTheme({ scope: container });
18067
17827
  ```
18068
17828
 
18069
- ### Keyboard and announcements
17829
+ ### Available Tokens
17830
+
17831
+ The 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.
18070
17832
 
18071
- Every labelled chart except sparklines shares one keyboard model:
17833
+ | Group | CSS custom properties |
17834
+ | --- | --- |
17835
+ | Palette and surfaces | `--prism-color-1` through `--prism-color-8`, `--prism-bg`, `--prism-border-radius` |
17836
+ | Axes and grid | `--prism-axis-color`, `--prism-axis-width`, `--prism-grid-color`, `--prism-grid-width`, `--prism-grid-dash`, `--prism-grid-opacity` |
17837
+ | 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` |
17838
+ | Tooltip | `--prism-tooltip-bg`, `--prism-tooltip-color`, `--prism-tooltip-border`, `--prism-tooltip-radius`, `--prism-tooltip-padding`, `--prism-tooltip-shadow`, `--prism-tooltip-font-size` |
17839
+ | Crosshair | `--prism-crosshair-color`, `--prism-crosshair-width`, `--prism-crosshair-dash`, `--prism-crosshair-opacity` |
17840
+ | Points | `--prism-point-radius`, `--prism-point-radius-hover`, `--prism-point-stroke-width`, `--prism-point-stroke` |
17841
+ | Bars | `--prism-bar-radius`, `--prism-bar-hover-opacity`, `--prism-bar-gap`, `--prism-bar-band-fill`, `--prism-bar-band-opacity`, `--prism-bar-dim-opacity` |
17842
+ | 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` |
17843
+ | Interaction | `--prism-dim-opacity`, `--prism-active-point-radius`, `--prism-point-ring`, `--prism-focus-color` |
17844
+ | Sparklines | `--prism-spark-fill-opacity`, `--prism-spark-gradient-start`, `--prism-spark-gradient-end` |
17845
+ | 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` |
17846
+ | Motion | `--prism-duration-fast`, `--prism-duration`, `--prism-duration-slow`, `--prism-ease`, `--prism-ease-spring`, `--prism-transition`, `--prism-transition-fast` |
17847
+ | Legend and pie labels | `--prism-legend-gap`, `--prism-legend-dot-size`, `--prism-legend-font-size`, `--prism-pie-label-color`, `--prism-pie-label-size` |
18072
17848
 
18073
- | Chart | Arrow keys move between | `Home` / `End` | Announced |
18074
- | --- | --- | --- | --- |
18075
- | Line, area | x positions | First / last position | `1: Adam 3, Eve 6` |
18076
- | Bar | categories | First / last category | `Str: Adam 8, Eve 5` |
18077
- | Pie | slices | First / last slice | `Wins: 15 (75%)` |
18078
- | Radar | axes (wrapping) | Not supported | `Agility: Adam 6, Eve 9` |
17849
+ The 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.
18079
17850
 
18080
- The first arrow press focuses the first item. `Enter` or `Space` fires `onClick` for the focused item, and `Escape` clears it. The tooltip speaks the text when one is enabled; otherwise the chart's own `role="status"` region (`.prism-live`) does, so each value is announced once.
17851
+ ## Errors and Diagnostics
18081
17852
 
18082
- ### Hover feedback
17853
+ Prism 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.
18083
17854
 
18084
- The active key is shown the same way on every chart. Line and area charts mark each series' point and dim the series farther from the pointer. Bar charts shade the category band and dim the other categories. Pie charts dim the other slices. Radar charts highlight the active spoke and its points, and dim the other shapes while one is hovered.
17855
+ Empty 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.
18085
17856
 
18086
17857
  ## Framework Integration
18087
17858
 
18088
- Prism has no framework adapter. Connect the same three operations to your framework's lifecycle: create after mount, update when state changes, and dispose before unmount.
17859
+ Prism 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:
18089
17860
 
18090
17861
  ```ts
18091
- import { createLineChart, type ChartHandle, type ContinuousDatum, type LineSeriesConfig } from '@vielzeug/prism';
18092
-
18093
- let chart: ChartHandle | undefined;
17862
+ import { createLineChart, type LineSeriesConfig } from '@vielzeug/prism';
18094
17863
 
18095
- export function mountChart(container: HTMLElement, data: ContinuousDatum[]): void {
18096
- chart = createLineChart(container, { series: [{ data, name: 'Series' }] });
18097
- }
18098
-
18099
- export function updateChart(data: ContinuousDatum[]): void {
18100
- chart?.update([{ data, name: 'Series' }]);
18101
- }
17864
+ function mountChart(host: HTMLElement, series: LineSeriesConfig[]) {
17865
+ const chart = createLineChart(host, { a11y: { ariaLabel: 'Revenue' }, series });
18102
17866
 
18103
- export function unmountChart(): void {
18104
- chart?.dispose();
18105
- chart = undefined;
17867
+ return {
17868
+ update(next: LineSeriesConfig[]) {
17869
+ chart.update(next);
17870
+ },
17871
+ dispose() {
17872
+ chart.dispose();
17873
+ },
17874
+ };
18106
17875
  }
18107
- ```
18108
-
18109
- ## Working with Other Vielzeug Libraries
18110
-
18111
- ### With Ripple
18112
17876
 
18113
- Keep state ownership in Ripple and connect it to Prism through an explicit subscription.
18114
-
18115
- ```ts
18116
- import { createLineChart } from '@vielzeug/prism';
18117
- import { signal } from '@vielzeug/ripple';
18118
-
18119
- const data = signal([{ key: 1, value: 10 }]);
18120
- const toSeries = () => [{ data: data.value, name: 'Series' }];
18121
- const chart = createLineChart(container, { series: toSeries() });
18122
- const unsubscribe = data.subscribe(() => chart.update(toSeries()));
18123
-
18124
- // Cleanup both owners together.
18125
- unsubscribe();
18126
- chart.dispose();
17877
+ const host = document.createElement('div');
17878
+ host.style.cssText = 'width:640px;height:320px';
17879
+ document.body.append(host);
17880
+ const view = mountChart(host, [{ data: [{ key: 1, value: 10 }], name: 'Revenue' }]);
17881
+ view.update([{ data: [{ key: 1, value: 10 }, { key: 2, value: 15 }], name: 'Revenue' }]);
17882
+ view.dispose();
18127
17883
  ```
18128
17884
 
18129
- ### With Sourcerer
18130
-
18131
- Bind chart data to a Sourcerer remote source so charts update whenever the list refreshes.
17885
+ The application owns its state and host; Prism owns only the SVG, observer, and optional overlay elements it creates.
18132
17886
 
18133
- ```ts
18134
- import { createBarChart } from '@vielzeug/prism';
18135
- import { createPageSource } from '@vielzeug/sourcerer';
17887
+ ## Working with Other Vielzeug Libraries
18136
17888
 
18137
- const source = createPageSource({
18138
- load: async ({ page, pageSize, signal }) => {
18139
- const result = await api.stats.list({ page, pageSize }, { signal });
18140
- return { items: result.data, totalItems: result.total };
18141
- },
18142
- });
18143
- const toSeries = (state) => [
18144
- {
18145
- data: state.items.map((item) => ({ key: item.label, value: item.count })),
18146
- name: 'Series',
18147
- },
18148
- ];
18149
- const chart = createBarChart(container, { series: toSeries(source.state) });
18150
- const unsubscribe = source.subscribe((state) => chart.update(toSeries(state)));
18151
- void source.reload().catch(() => undefined);
18152
- ```
17889
+ Use 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.
18153
17890
 
18154
17891
  ## Best Practices
18155
17892
 
18156
- - Ensure the container element has explicit dimensions before calling a chart factory: `ResizeObserver` needs a non-zero layout size to trigger the first render.
18157
- - Call `chart.dispose()` in your framework's unmount/cleanup phase to cancel transitions, disconnect resize observation, and remove DOM nodes.
18158
- - Call `chart.update(data)` from your application state boundary; Prism does not require or own a state library.
18159
- - Set `a11y: { ariaLabel: '…' }` on every chart that conveys meaningful data: unlabelled charts are hidden from assistive technology and receive no keyboard navigation.
18160
- - Observe lifecycle with `chart.tap(handler)` rather than polling `chart.disposed` or wrapping the container: it costs nothing when untapped and detaches via `signal`.
18161
- - For SSR, skip chart creation server-side: Prism depends on DOM APIs and `ResizeObserver`. Render charts only after hydration in a `onMounted`/`useEffect` callback.
17893
+ - Give the host a non-zero size before creating a chart.
17894
+ - Set `a11y.ariaLabel` when a chart communicates information.
17895
+ - Use `Date` or number keys consistently across each line or area chart.
17896
+ - Pass the complete new data value to `update()`.
17897
+ - Keep chart options fixed after creation; recreate the chart to change them.
17898
+ - Dispose the handle when its host leaves the owning view.
17899
+ - Use `transition: false` for deterministic synchronous rendering.
17900
+ - Prefer CSS custom properties for reusable theme overrides.
18162
17901
 
18163
17902
  ### Examples
18164
17903
 
18165
17904
  - [Create a line chart](./examples/line-chart.md)
18166
17905
  - [Plot a time series](./examples/time-series.md)
17906
+ - [Plot a scatter chart](./examples/scatter-chart.md)
18167
17907
  - [Compare categories with a bar chart](./examples/bar-chart.md)
18168
17908
  - [Render compact charts](./examples/compact-charts.md)
18169
17909
  - [Compare hero stats with a radar chart](./examples/radar-chart.md)