@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 +52 -1
- package/dist/css-theme.d.ts +50 -0
- package/dist/css-theme.js +72 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/line.js +13 -1
- package/dist/theme.d.ts +9 -0
- package/dist/useChartTheme.d.ts +50 -0
- package/dist/useChartTheme.js +79 -0
- package/package.json +3 -3
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.
|
|
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
|
-
|
|
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.
|
|
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.
|
|
42
|
-
"pond-ts": "^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": {
|