@pond-ts/charts 0.34.1 → 0.36.0

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.
package/CHANGELOG.md CHANGED
@@ -8,7 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`, and
8
8
  them all. Pre-1.0: minor bumps may include new features and type-level changes;
9
9
  patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.34.1...HEAD
11
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.36.0...HEAD
12
+ [0.36.0]: https://github.com/pjm17971/pond-ts/compare/v0.35.0...v0.36.0
13
+ [0.35.0]: https://github.com/pjm17971/pond-ts/compare/v0.34.1...v0.35.0
12
14
  [0.34.1]: https://github.com/pjm17971/pond-ts/compare/v0.34.0...v0.34.1
13
15
  [0.34.0]: https://github.com/pjm17971/pond-ts/compare/v0.33.0...v0.34.0
14
16
  [0.33.0]: https://github.com/pjm17971/pond-ts/compare/v0.32.0...v0.33.0
@@ -30,6 +32,55 @@ patch bumps are strictly additive.
30
32
  [0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
31
33
  [0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
32
34
 
35
+ ## [0.36.0] — 2026-07-02
36
+
37
+ A `@pond-ts/charts` release: a CSS-custom-property → theme bridge so a canvas
38
+ chart can follow a design system's tokens and dark/light toggle. `pond-ts`,
39
+ `@pond-ts/react`, and `@pond-ts/fit` carry no code changes — republished in
40
+ lock-step (their `pond-ts` / `@pond-ts/react` peer ranges widen to `^0.36.0`).
41
+
42
+ ### Added
43
+
44
+ - **Charts — `cssVarTheme(base, resolve, opts?)`.** Builds a `ChartTheme` by
45
+ overlaying CSS custom properties onto a base theme: a typed `resolve`
46
+ receives a `readVar` and returns only the slots to override. An unresolved
47
+ var keeps the base value (a missing token never blanks a colour). DOM-only by
48
+ design; safe under SSR / worker (returns the base + any literal fallbacks).
49
+ The typed `ChartTheme` stays the single styling channel — this generates it
50
+ from CSS rather than adding a second one. (#315)
51
+ - **Charts — `useChartTheme(base, resolve, opts?)`.** Wraps `cssVarTheme` and
52
+ re-resolves on a `data-theme` / `class` change (a `MutationObserver` on the
53
+ root, configurable via `{ target, attributes }`), so a chart follows
54
+ dark/light with the page — no `mode` prop threaded through. Returns a new
55
+ theme reference only when the resolved theme actually changed (the repaint
56
+ signal `ChartContainer` keys on), so an unrelated attribute toggle doesn't
57
+ repaint. Lives in `@pond-ts/charts` (not `@pond-ts/react`) to keep the
58
+ package graph acyclic. (#315)
59
+ - **Docs — charts recipes.** [Theming charts](https://pjm17971.github.io/pond-ts/docs/recipes/theming)
60
+ (the `ChartTheme` model, semantic identifiers, per-series dash, the CSS-var
61
+ bridge), [Using @pond-ts/charts](https://pjm17971.github.io/pond-ts/docs/recipes/using-charts)
62
+ (install, the Storybook `react-docgen` gotcha, the repaint contract,
63
+ in-dev consumption), and
64
+ [Resizable multi-panel layout](https://pjm17971.github.io/pond-ts/docs/recipes/resizable-panels).
65
+ (#314, #315, #316)
66
+
67
+ ## [0.35.0] — 2026-07-02
68
+
69
+ A `@pond-ts/charts` release: per-series line dash patterns. `pond-ts`,
70
+ `@pond-ts/react`, and `@pond-ts/fit` carry no code changes — republished in
71
+ lock-step (their `pond-ts` / `@pond-ts/react` peer ranges widen to `^0.35.0`).
72
+
73
+ ### Added
74
+
75
+ - **Charts — per-series line dash (`LineStyle.dash`).** A theme's line style
76
+ accepts an optional `dash?: readonly number[]` — a px on/off pattern
77
+ (`[6, 4]` dashed, `[2, 3]` ≈ dotted; omit or `[]` = solid) applied to the
78
+ series stroke. Lets a theme set a **modeled / forecast** line (e.g. GARCH
79
+ vol) apart from an observed one at a glance. Distinct from a `GapMode`'s
80
+ inferred gap-bridge dashing (which marks *missing data*, not the whole
81
+ line). Additive: existing themes are unaffected; a solid line never touches
82
+ `setLineDash`. New `Charts/LineChart → LineStyles` story. (#313)
83
+
33
84
  ## [0.34.1] — 2026-07-01
34
85
 
35
86
  A `pond-ts` core patch: fixes a performance regression introduced in 0.34.0.
@@ -0,0 +1,50 @@
1
+ import type { ChartTheme } from './theme.js';
2
+ /**
3
+ * A deep-partial of {@link ChartTheme} — every leaf optional, arrays kept whole
4
+ * (so `axis.gridDash` / `annotation.depth` replace rather than merge
5
+ * element-wise). The shape a {@link cssVarTheme} resolver returns: name only the
6
+ * slots you're driving from CSS, everything else falls through to the base.
7
+ */
8
+ export type ChartThemeOverrides = DeepPartial<ChartTheme>;
9
+ type DeepPartial<T> = T extends readonly unknown[] ? T : T extends object ? {
10
+ [K in keyof T]?: DeepPartial<T[K]> | undefined;
11
+ } : T;
12
+ /**
13
+ * Reads a CSS custom property's computed value. Returns the trimmed value, or
14
+ * `fallback` when the property is empty / unset / there's no DOM (SSR, worker).
15
+ * `undefined` from a resolver leaf means "leave the base theme's value" — so a
16
+ * missing var never blanks a colour.
17
+ */
18
+ export type VarReader = (name: string, fallback?: string) => string | undefined;
19
+ /**
20
+ * Build a {@link ChartTheme} by overlaying CSS-custom-property values onto a
21
+ * `base` theme — the adapter that lets a chart track a design system's tokens
22
+ * (and its dark/light toggle) without hand-mirroring hex values.
23
+ *
24
+ * `resolve` receives a {@link VarReader} and returns only the slots to override
25
+ * (a {@link ChartThemeOverrides}); the result is `base` deep-merged with them.
26
+ * The typed `ChartTheme` stays the one styling channel — this **generates** it
27
+ * from CSS, it doesn't add a second one.
28
+ *
29
+ * ```ts
30
+ * const theme = cssVarTheme(defaultTheme, (v) => ({
31
+ * line: { default: { color: v('--td-primary') }, secondary: { color: v('--td-secondary') } },
32
+ * axis: { label: v('--td-text-3'), grid: v('--td-hairline') },
33
+ * cursor: v('--td-text-3'),
34
+ * font: { family: v('--td-font-mono') },
35
+ * }));
36
+ * ```
37
+ *
38
+ * **DOM-only, by design.** It reads `getComputedStyle` off `opts.element` (or
39
+ * `document.documentElement`). With no DOM — SSR, an OffscreenCanvas worker —
40
+ * every `readVar` returns its fallback (or `undefined`, kept from `base`), so
41
+ * the call is safe and returns the base theme (plus any literal fallbacks). For
42
+ * a live chart that follows a theme toggle, use {@link useChartTheme}, which
43
+ * wraps this and re-resolves on a `data-theme` change — don't call `cssVarTheme`
44
+ * per frame (`getComputedStyle` is a layout read).
45
+ */
46
+ export declare function cssVarTheme(base: ChartTheme, resolve: (readVar: VarReader) => ChartThemeOverrides, opts?: {
47
+ element?: Element;
48
+ }): ChartTheme;
49
+ export {};
50
+ //# sourceMappingURL=css-theme.d.ts.map
@@ -0,0 +1,72 @@
1
+ /**
2
+ * Build a {@link ChartTheme} by overlaying CSS-custom-property values onto a
3
+ * `base` theme — the adapter that lets a chart track a design system's tokens
4
+ * (and its dark/light toggle) without hand-mirroring hex values.
5
+ *
6
+ * `resolve` receives a {@link VarReader} and returns only the slots to override
7
+ * (a {@link ChartThemeOverrides}); the result is `base` deep-merged with them.
8
+ * The typed `ChartTheme` stays the one styling channel — this **generates** it
9
+ * from CSS, it doesn't add a second one.
10
+ *
11
+ * ```ts
12
+ * const theme = cssVarTheme(defaultTheme, (v) => ({
13
+ * line: { default: { color: v('--td-primary') }, secondary: { color: v('--td-secondary') } },
14
+ * axis: { label: v('--td-text-3'), grid: v('--td-hairline') },
15
+ * cursor: v('--td-text-3'),
16
+ * font: { family: v('--td-font-mono') },
17
+ * }));
18
+ * ```
19
+ *
20
+ * **DOM-only, by design.** It reads `getComputedStyle` off `opts.element` (or
21
+ * `document.documentElement`). With no DOM — SSR, an OffscreenCanvas worker —
22
+ * every `readVar` returns its fallback (or `undefined`, kept from `base`), so
23
+ * the call is safe and returns the base theme (plus any literal fallbacks). For
24
+ * a live chart that follows a theme toggle, use {@link useChartTheme}, which
25
+ * wraps this and re-resolves on a `data-theme` change — don't call `cssVarTheme`
26
+ * per frame (`getComputedStyle` is a layout read).
27
+ */
28
+ export function cssVarTheme(base, resolve, opts) {
29
+ const el = opts?.element ??
30
+ (typeof document !== 'undefined' ? document.documentElement : undefined);
31
+ const style = el && typeof getComputedStyle === 'function'
32
+ ? getComputedStyle(el)
33
+ : undefined;
34
+ const readVar = (name, fallback) => {
35
+ const raw = style?.getPropertyValue(name).trim();
36
+ return raw ? raw : fallback;
37
+ };
38
+ return deepMerge(base, resolve(readVar));
39
+ }
40
+ /**
41
+ * Deep-merge `partial` over `base`: recurse into plain objects, replace at
42
+ * leaves and arrays, and **skip `undefined`** in `partial` (so an unresolved
43
+ * var keeps the base value). Never mutates `base`; each merged object level is
44
+ * freshly spread, and untouched subtrees are shared by reference (safe — a
45
+ * `ChartTheme` is read-only).
46
+ */
47
+ function deepMerge(base, partial) {
48
+ if (partial === undefined)
49
+ return base;
50
+ if (base === null ||
51
+ typeof base !== 'object' ||
52
+ Array.isArray(base) ||
53
+ Array.isArray(partial) ||
54
+ typeof partial !== 'object' ||
55
+ partial === null) {
56
+ // Leaf, array (replace whole), or a partial that introduces a value where
57
+ // the base had none — the partial wins.
58
+ return partial;
59
+ }
60
+ const out = { ...base };
61
+ for (const key of Object.keys(partial)) {
62
+ // Never let a resolver's key rewrite the prototype chain.
63
+ if (key === '__proto__' || key === 'constructor' || key === 'prototype')
64
+ continue;
65
+ const pv = partial[key];
66
+ if (pv === undefined)
67
+ continue;
68
+ out[key] = deepMerge(base[key], pv);
69
+ }
70
+ return out;
71
+ }
72
+ //# sourceMappingURL=css-theme.js.map
package/dist/index.d.ts CHANGED
@@ -52,5 +52,9 @@ export type { Curve } from './curve.js';
52
52
  export type { GapMode } from './gaps.js';
53
53
  export { defaultTheme, estelaTheme } from './theme.js';
54
54
  export type { ChartTheme, LineStyle, BandStyle, AreaStyle, ScatterStyle, BoxStyle, BarStyle, } from './theme.js';
55
+ export { cssVarTheme } from './css-theme.js';
56
+ export type { ChartThemeOverrides, VarReader } from './css-theme.js';
57
+ export { useChartTheme } from './useChartTheme.js';
58
+ export type { UseChartThemeOptions } from './useChartTheme.js';
55
59
  export type { CursorMode, TrackerInfo, TrackerSample, SelectInfo, } from './context.js';
56
60
  //# sourceMappingURL=index.d.ts.map
package/dist/index.js CHANGED
@@ -34,4 +34,9 @@ export { BarChart } from './BarChart.js';
34
34
  export { Region, Baseline, Marker } from './annotations.js';
35
35
  export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, } from './data.js';
36
36
  export { defaultTheme, estelaTheme } from './theme.js';
37
+ // CSS-custom-property → ChartTheme bridge: build a theme from a design system's
38
+ // tokens (`cssVarTheme`), and a hook that re-resolves it on a `data-theme`
39
+ // toggle so a canvas chart follows dark/light (`useChartTheme`).
40
+ export { cssVarTheme } from './css-theme.js';
41
+ export { useChartTheme } from './useChartTheme.js';
37
42
  //# sourceMappingURL=index.js.map
package/dist/line.js CHANGED
@@ -69,7 +69,19 @@ export function drawLine(ctx, cs, xScale, yScale, style, curve = curveLinear, ga
69
69
  gen(ys);
70
70
  ctx.strokeStyle = style.color;
71
71
  ctx.lineWidth = style.width;
72
- ctx.stroke();
72
+ // Per-series dash (a modeled/forecast line reads dashed). Applied only when
73
+ // set — a solid line never touches `setLineDash` — then reset to solid right
74
+ // after the stroke so it can't leak into the gap-bridge overlay below (which
75
+ // sets its own dash) or the next layer drawn on this context.
76
+ const dash = style.dash;
77
+ if (dash && dash.length > 0) {
78
+ ctx.setLineDash(dash.slice());
79
+ ctx.stroke();
80
+ ctx.setLineDash([]);
81
+ }
82
+ else {
83
+ ctx.stroke();
84
+ }
73
85
  // Overlay bridges for the inferred-gap modes. `dashed` / `step` are faint
74
86
  // dashed connectors (gapConnectorOpacity); only `fade` drops to the axis floor.
75
87
  if (gaps === 'dashed' || gaps === 'step' || gaps === 'fade') {
package/dist/theme.d.ts CHANGED
@@ -138,6 +138,15 @@ export interface ChartTheme {
138
138
  export interface LineStyle {
139
139
  readonly color: string;
140
140
  readonly width: number;
141
+ /**
142
+ * Optional dash pattern — px on/off lengths (`[6, 4]` = 6 on, 4 off; `[2, 3]`
143
+ * ≈ dotted). Omit or `[]` for a solid stroke. This is the *series'* own style
144
+ * — distinct from a {@link GapMode}'s inferred faint gap-bridge dashing (that
145
+ * marks missing data; this marks the whole line). Use it to set a **modeled**
146
+ * series (a forecast / smoothed estimate, e.g. GARCH vol) apart from an
147
+ * observed one at a glance.
148
+ */
149
+ readonly dash?: readonly number[];
141
150
  }
142
151
  /** A resolved band style: fill colour + opacity (0–1) for the variance envelope. */
143
152
  export interface BandStyle {
@@ -0,0 +1,50 @@
1
+ import { type ChartThemeOverrides, type VarReader } from './css-theme.js';
2
+ import type { ChartTheme } from './theme.js';
3
+ /** Options for {@link useChartTheme}. */
4
+ export interface UseChartThemeOptions {
5
+ /**
6
+ * Element whose CSS custom properties are read and whose attribute changes
7
+ * are watched. **Default `document.documentElement`** (`<html>`) — the usual
8
+ * home of a `data-theme` toggle. Pass a scoped element to theme one subtree.
9
+ */
10
+ target?: Element;
11
+ /**
12
+ * Attributes that, when they change on `target`, trigger a re-resolve.
13
+ * **Default `['data-theme', 'class']`** — the two common dark/light switches.
14
+ */
15
+ attributes?: readonly string[];
16
+ }
17
+ /**
18
+ * Live {@link ChartTheme} bound to CSS custom properties: resolves `resolve`
19
+ * against the DOM (via {@link cssVarTheme}) and **re-resolves whenever the
20
+ * theme toggle flips** — a `MutationObserver` watches `target`'s
21
+ * `data-theme` / `class`, so `<ChartContainer theme={useChartTheme(...)} />`
22
+ * follows dark/light with no `mode` prop threaded through and no hand-ordered
23
+ * attribute-then-read dance.
24
+ *
25
+ * When the resolved theme changes it returns a **new** reference, which is the
26
+ * repaint signal — `ChartContainer` redraws when handed a new `theme`. A
27
+ * watched mutation that doesn't change the resolved values (e.g. an app
28
+ * toggling an unrelated `class` on `<html>`) returns the *same* reference, so
29
+ * it doesn't repaint. Resolution runs on mount and on watched-attribute changes
30
+ * only — never per frame — so the `getComputedStyle` read stays cheap.
31
+ *
32
+ * ```tsx
33
+ * const theme = useChartTheme(defaultTheme, (v) => ({
34
+ * line: { default: { color: v('--td-primary') } },
35
+ * axis: { label: v('--td-text-3'), grid: v('--td-hairline') },
36
+ * }));
37
+ * return <ChartContainer width={w} theme={theme}>…</ChartContainer>;
38
+ * ```
39
+ *
40
+ * `base` and `resolve` are read fresh on every resolve (held in refs), so
41
+ * inline literals are fine — they don't need memoizing and don't re-subscribe
42
+ * the observer. **But** because a resolve only fires on mount + a watched
43
+ * mutation, changing `base`/`resolve` alone won't re-resolve until the next
44
+ * toggle; if you need to swap them and re-resolve immediately, change `target`
45
+ * / `attributes` (which re-subscribes) or remount. SSR-safe: the first value
46
+ * resolves with no DOM (returns `base` + any literal fallbacks); the client
47
+ * re-resolves on mount.
48
+ */
49
+ export declare function useChartTheme(base: ChartTheme, resolve: (readVar: VarReader) => ChartThemeOverrides, opts?: UseChartThemeOptions): ChartTheme;
50
+ //# sourceMappingURL=useChartTheme.d.ts.map
@@ -0,0 +1,79 @@
1
+ import { useEffect, useRef, useState } from 'react';
2
+ import { cssVarTheme, } from './css-theme.js';
3
+ /**
4
+ * Live {@link ChartTheme} bound to CSS custom properties: resolves `resolve`
5
+ * against the DOM (via {@link cssVarTheme}) and **re-resolves whenever the
6
+ * theme toggle flips** — a `MutationObserver` watches `target`'s
7
+ * `data-theme` / `class`, so `<ChartContainer theme={useChartTheme(...)} />`
8
+ * follows dark/light with no `mode` prop threaded through and no hand-ordered
9
+ * attribute-then-read dance.
10
+ *
11
+ * When the resolved theme changes it returns a **new** reference, which is the
12
+ * repaint signal — `ChartContainer` redraws when handed a new `theme`. A
13
+ * watched mutation that doesn't change the resolved values (e.g. an app
14
+ * toggling an unrelated `class` on `<html>`) returns the *same* reference, so
15
+ * it doesn't repaint. Resolution runs on mount and on watched-attribute changes
16
+ * only — never per frame — so the `getComputedStyle` read stays cheap.
17
+ *
18
+ * ```tsx
19
+ * const theme = useChartTheme(defaultTheme, (v) => ({
20
+ * line: { default: { color: v('--td-primary') } },
21
+ * axis: { label: v('--td-text-3'), grid: v('--td-hairline') },
22
+ * }));
23
+ * return <ChartContainer width={w} theme={theme}>…</ChartContainer>;
24
+ * ```
25
+ *
26
+ * `base` and `resolve` are read fresh on every resolve (held in refs), so
27
+ * inline literals are fine — they don't need memoizing and don't re-subscribe
28
+ * the observer. **But** because a resolve only fires on mount + a watched
29
+ * mutation, changing `base`/`resolve` alone won't re-resolve until the next
30
+ * toggle; if you need to swap them and re-resolve immediately, change `target`
31
+ * / `attributes` (which re-subscribes) or remount. SSR-safe: the first value
32
+ * resolves with no DOM (returns `base` + any literal fallbacks); the client
33
+ * re-resolves on mount.
34
+ */
35
+ export function useChartTheme(base, resolve, opts) {
36
+ const baseRef = useRef(base);
37
+ baseRef.current = base;
38
+ const resolveRef = useRef(resolve);
39
+ resolveRef.current = resolve;
40
+ const target = opts?.target;
41
+ // Stable key for the effect dep so an inline `attributes` array doesn't
42
+ // re-subscribe every render.
43
+ const attrKey = (opts?.attributes ?? ['data-theme', 'class']).join(',');
44
+ const compute = () => cssVarTheme(baseRef.current, resolveRef.current, target ? { element: target } : undefined);
45
+ const [theme, setTheme] = useState(compute);
46
+ // Re-resolve, but only push a new reference when the resolved theme actually
47
+ // changed — so a watched-but-unrelated mutation (e.g. an app toggling a
48
+ // scroll-lock `class` on `<html>`) doesn't force a repaint. Identical values
49
+ // ⇒ return the previous reference ⇒ React bails out.
50
+ const resolveAndApply = () => {
51
+ const next = compute();
52
+ setTheme((prev) => (themesEqual(prev, next) ? prev : next));
53
+ };
54
+ useEffect(() => {
55
+ const el = target ??
56
+ (typeof document !== 'undefined' ? document.documentElement : undefined);
57
+ if (!el || typeof MutationObserver === 'undefined')
58
+ return;
59
+ // Re-resolve on mount: the SSR/first value was computed without a DOM.
60
+ resolveAndApply();
61
+ const observer = new MutationObserver(resolveAndApply);
62
+ observer.observe(el, {
63
+ attributes: true,
64
+ attributeFilter: attrKey.split(','),
65
+ });
66
+ return () => observer.disconnect();
67
+ // `resolveAndApply` closes over stable refs; re-subscribe only on
68
+ // target/attr change.
69
+ // eslint-disable-next-line react-hooks/exhaustive-deps
70
+ }, [target, attrKey]);
71
+ return theme;
72
+ }
73
+ /** Value-equality for two resolved themes. A `ChartTheme` is a plain tree of
74
+ * strings / numbers / small arrays (no functions, stable key order), so a
75
+ * JSON compare is correct and cheap at the mount/toggle cadence this runs at. */
76
+ function themesEqual(a, b) {
77
+ return a === b || JSON.stringify(a) === JSON.stringify(b);
78
+ }
79
+ //# sourceMappingURL=useChartTheme.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pond-ts/charts",
3
- "version": "0.34.1",
3
+ "version": "0.36.0",
4
4
  "private": false,
5
5
  "description": "Canvas-rendered, streaming-first time-series charts for pond-ts",
6
6
  "license": "MIT",
@@ -38,8 +38,8 @@
38
38
  "perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
39
39
  },
40
40
  "peerDependencies": {
41
- "@pond-ts/react": "^0.34.0",
42
- "pond-ts": "^0.34.0",
41
+ "@pond-ts/react": "^0.36.0",
42
+ "pond-ts": "^0.36.0",
43
43
  "react": "^18.0.0 || ^19.0.0"
44
44
  },
45
45
  "devDependencies": {