@pond-ts/charts 0.35.0 → 0.37.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 +67 -1
- package/dist/ChartRow.js +18 -3
- package/dist/XAxis.d.ts +14 -1
- package/dist/XAxis.js +23 -10
- package/dist/YAxis.d.ts +25 -1
- package/dist/YAxis.js +38 -13
- package/dist/context.d.ts +4 -0
- package/dist/css-theme.d.ts +50 -0
- package/dist/css-theme.js +72 -0
- package/dist/domain.d.ts +6 -1
- package/dist/domain.js +15 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +5 -0
- package/dist/theme.d.ts +11 -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.37.0...HEAD
|
|
12
|
+
[0.37.0]: https://github.com/pjm17971/pond-ts/compare/v0.36.0...v0.37.0
|
|
13
|
+
[0.36.0]: https://github.com/pjm17971/pond-ts/compare/v0.35.0...v0.36.0
|
|
12
14
|
[0.35.0]: https://github.com/pjm17971/pond-ts/compare/v0.34.1...v0.35.0
|
|
13
15
|
[0.34.1]: https://github.com/pjm17971/pond-ts/compare/v0.34.0...v0.34.1
|
|
14
16
|
[0.34.0]: https://github.com/pjm17971/pond-ts/compare/v0.33.0...v0.34.0
|
|
@@ -31,6 +33,70 @@ patch bumps are strictly additive.
|
|
|
31
33
|
[0.19.0]: https://github.com/pjm17971/pond-ts/compare/v0.18.0...v0.19.0
|
|
32
34
|
[0.18.0]: https://github.com/pjm17971/pond-ts/compare/v0.17.1...v0.18.0
|
|
33
35
|
|
|
36
|
+
## [0.37.0] — 2026-07-02
|
|
37
|
+
|
|
38
|
+
A `@pond-ts/charts` release: the axis wave — label, tick, and domain controls
|
|
39
|
+
driven by the Tidal terminal's friction. `pond-ts`, `@pond-ts/react`, and
|
|
40
|
+
`@pond-ts/fit` carry no code changes — republished in lock-step (peer ranges
|
|
41
|
+
widen to `^0.37.0`).
|
|
42
|
+
|
|
43
|
+
### Added
|
|
44
|
+
|
|
45
|
+
- **Charts — axis title typography (`theme.axis.title`).** The rotated y-axis
|
|
46
|
+
title now renders a touch larger than the ticks by default and is fully
|
|
47
|
+
themeable (`{ color, size, opacity }`, shared with the x-axis label). (#318)
|
|
48
|
+
- **Charts — `YAxis labelPlacement`.** `'rotated'` (default) or `'top'` — a
|
|
49
|
+
horizontal title above the axis, aligned to the axis line, in a reserved
|
|
50
|
+
header band that clears the top tick. (#318, #320)
|
|
51
|
+
- **Charts — `XAxis align`.** `'center'` (**new default**), `'auto'` (previous
|
|
52
|
+
behaviour: centred but first/last end-anchored), or `'right'` (label beside an
|
|
53
|
+
extended tick). (#318)
|
|
54
|
+
- **Charts — `YAxis pad`.** Fractional headroom added to each side of the
|
|
55
|
+
resolved domain (`0` default) — lifts a tight domain off the plot edges
|
|
56
|
+
without hand-computing bounds. (#319)
|
|
57
|
+
- **Charts — `YAxis boundaryLabels`.** `false` drops the top & bottom tick
|
|
58
|
+
numbers (gridlines stay) for stacked layouts where the edge labels crowd. (#319)
|
|
59
|
+
- **Charts — new `Charts/Axes` Storybook gallery** covering the above. (#318)
|
|
60
|
+
|
|
61
|
+
### Changed
|
|
62
|
+
|
|
63
|
+
- **Charts — domain-extreme y-tick labels now clamp inside the row** instead of
|
|
64
|
+
half-overflowing the top/bottom edge (resolves Tidal friction F-charts-6). (#319)
|
|
65
|
+
- **Charts — `XAxis` tick-label default is now `'center'`** (was the
|
|
66
|
+
end-anchored `'auto'`). Pass `align="auto"` for the old behaviour. (#318)
|
|
67
|
+
|
|
68
|
+
## [0.36.0] — 2026-07-02
|
|
69
|
+
|
|
70
|
+
A `@pond-ts/charts` release: a CSS-custom-property → theme bridge so a canvas
|
|
71
|
+
chart can follow a design system's tokens and dark/light toggle. `pond-ts`,
|
|
72
|
+
`@pond-ts/react`, and `@pond-ts/fit` carry no code changes — republished in
|
|
73
|
+
lock-step (their `pond-ts` / `@pond-ts/react` peer ranges widen to `^0.36.0`).
|
|
74
|
+
|
|
75
|
+
### Added
|
|
76
|
+
|
|
77
|
+
- **Charts — `cssVarTheme(base, resolve, opts?)`.** Builds a `ChartTheme` by
|
|
78
|
+
overlaying CSS custom properties onto a base theme: a typed `resolve`
|
|
79
|
+
receives a `readVar` and returns only the slots to override. An unresolved
|
|
80
|
+
var keeps the base value (a missing token never blanks a colour). DOM-only by
|
|
81
|
+
design; safe under SSR / worker (returns the base + any literal fallbacks).
|
|
82
|
+
The typed `ChartTheme` stays the single styling channel — this generates it
|
|
83
|
+
from CSS rather than adding a second one. (#315)
|
|
84
|
+
- **Charts — `useChartTheme(base, resolve, opts?)`.** Wraps `cssVarTheme` and
|
|
85
|
+
re-resolves on a `data-theme` / `class` change (a `MutationObserver` on the
|
|
86
|
+
root, configurable via `{ target, attributes }`), so a chart follows
|
|
87
|
+
dark/light with the page — no `mode` prop threaded through. Returns a new
|
|
88
|
+
theme reference only when the resolved theme actually changed (the repaint
|
|
89
|
+
signal `ChartContainer` keys on), so an unrelated attribute toggle doesn't
|
|
90
|
+
repaint. Lives in `@pond-ts/charts` (not `@pond-ts/react`) to keep the
|
|
91
|
+
package graph acyclic. (#315)
|
|
92
|
+
- **Docs — charts recipes.** [Theming charts](https://pjm17971.github.io/pond-ts/docs/recipes/theming)
|
|
93
|
+
(the `ChartTheme` model, semantic identifiers, per-series dash, the CSS-var
|
|
94
|
+
bridge), [Using @pond-ts/charts](https://pjm17971.github.io/pond-ts/docs/recipes/using-charts)
|
|
95
|
+
(install, the Storybook `react-docgen` gotcha, the repaint contract,
|
|
96
|
+
in-dev consumption), and
|
|
97
|
+
[Resizable multi-panel layout](https://pjm17971.github.io/pond-ts/docs/recipes/resizable-panels).
|
|
98
|
+
(#314, #315, #316)
|
|
99
|
+
|
|
34
100
|
## [0.35.0] — 2026-07-02
|
|
35
101
|
|
|
36
102
|
A `@pond-ts/charts` release: per-series line dash patterns. `pond-ts`,
|
package/dist/ChartRow.js
CHANGED
|
@@ -12,6 +12,10 @@ const IMPLICIT_AXIS_ID = '__default__';
|
|
|
12
12
|
/** Axis tick count for the per-axis formatter — matches `<YAxis>`'s tick count
|
|
13
13
|
* so the readout formatter is calibrated exactly as the labels are. */
|
|
14
14
|
const AXIS_TICK_COUNT = 5;
|
|
15
|
+
/** Vertical band (px) reserved above the plot for a `labelPlacement="top"` axis
|
|
16
|
+
* title, so it clears the top tick + plotted data. Sized for the default title
|
|
17
|
+
* (~`font.size + 1`); a much larger themed title may want more room. */
|
|
18
|
+
const TOP_LABEL_HEADER = 16;
|
|
15
19
|
/**
|
|
16
20
|
* A horizontal band sharing the container's time axis. `ChartRow` owns the
|
|
17
21
|
* **horizontal layout** (axes left/right around a `<Layers>` plot area) and
|
|
@@ -91,6 +95,8 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
91
95
|
width: 0,
|
|
92
96
|
min: undefined,
|
|
93
97
|
max: undefined,
|
|
98
|
+
pad: 0,
|
|
99
|
+
labelPlacement: 'rotated',
|
|
94
100
|
format: undefined,
|
|
95
101
|
tickValues: undefined,
|
|
96
102
|
index: 0,
|
|
@@ -126,6 +132,11 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
126
132
|
const containerLeftSlots = container.leftSlots;
|
|
127
133
|
const containerRightSlots = container.rightSlots;
|
|
128
134
|
const { axisSlots, leftPad, rightPad } = useMemo(() => placeAxisSlots(leftAxes, rightAxes, containerLeftSlots, containerRightSlots), [leftAxes, rightAxes, containerLeftSlots, containerRightSlots]);
|
|
135
|
+
// Header band reserved at the top of the plot when any axis draws a `'top'`
|
|
136
|
+
// title — the scale range then starts below it (see below).
|
|
137
|
+
const topHeader = effectiveAxes.some((ax) => ax.labelPlacement === 'top')
|
|
138
|
+
? TOP_LABEL_HEADER
|
|
139
|
+
: 0;
|
|
129
140
|
// One y-scale per axis. A layer counts toward an axis when its (late-resolved)
|
|
130
141
|
// axis id matches; `resolveYDomain` handles the auto-fit + empty/flat/inverted
|
|
131
142
|
// edges. yExtent() is O(points), so only walk the layers when a bound auto-fits.
|
|
@@ -137,11 +148,15 @@ export function ChartRow({ height, cursor, children }) {
|
|
|
137
148
|
.filter((entry) => (entry.axisId ?? defaultAxisId) === ax.id)
|
|
138
149
|
.map((entry) => entry.layer.yExtent())
|
|
139
150
|
: [];
|
|
140
|
-
const [lo, hi] = resolveYDomain(ax.min, ax.max, extents);
|
|
141
|
-
|
|
151
|
+
const [lo, hi] = resolveYDomain(ax.min, ax.max, extents, ax.pad);
|
|
152
|
+
// Reserve a header band at the top when any axis draws a `'top'` title,
|
|
153
|
+
// so the title clears the top tick + plot (the whole row shifts down
|
|
154
|
+
// uniformly, keeping stacked axes aligned). No top titles ⇒ range top 0,
|
|
155
|
+
// so nothing changes for existing charts.
|
|
156
|
+
map.set(ax.id, scaleLinear().domain([lo, hi]).range([height, topHeader]));
|
|
142
157
|
}
|
|
143
158
|
return map;
|
|
144
|
-
}, [effectiveAxes, layerList, height, defaultAxisId]);
|
|
159
|
+
}, [effectiveAxes, layerList, height, defaultAxisId, topHeader]);
|
|
145
160
|
// A value formatter per axis (its `format` resolved against its scale) — shared
|
|
146
161
|
// by the axis tick labels and the cursor readout so a value reads the same.
|
|
147
162
|
const formats = useMemo(() => {
|
package/dist/XAxis.d.ts
CHANGED
|
@@ -24,6 +24,19 @@ export interface XAxisProps {
|
|
|
24
24
|
readonly at: number;
|
|
25
25
|
readonly label: string;
|
|
26
26
|
}>;
|
|
27
|
+
/**
|
|
28
|
+
* Horizontal placement of each tick label relative to its tick.
|
|
29
|
+
* - **`'center'` (default)** — every label centred on its tick. Note the
|
|
30
|
+
* first/last labels can then extend past the plot edges (the strip doesn't
|
|
31
|
+
* clip), so a wide first label may reach into the left y-axis gutter; use
|
|
32
|
+
* `'auto'` if that crowds.
|
|
33
|
+
* - `'auto'` — centred, but the first label left-anchors and the last
|
|
34
|
+
* right-anchors so the edge labels stay inside the plot (the old default).
|
|
35
|
+
* - `'right'` — the label sits to the **right** of an extended tick that
|
|
36
|
+
* drops from the axis line (label beside the tick, not under it) — useful
|
|
37
|
+
* for dense or wide labels that would collide when centred.
|
|
38
|
+
*/
|
|
39
|
+
align?: 'auto' | 'center' | 'right';
|
|
27
40
|
}
|
|
28
41
|
/**
|
|
29
42
|
* The shared **x axis**, a sibling of {@link YAxis} for the horizontal axis. A
|
|
@@ -35,5 +48,5 @@ export interface XAxisProps {
|
|
|
35
48
|
*
|
|
36
49
|
* `<TimeAxis>` is the time-flavoured preset (`<XAxis />`).
|
|
37
50
|
*/
|
|
38
|
-
export declare function XAxis({ format, label, side, height, ticks: customTicks, }?: XAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
51
|
+
export declare function XAxis({ format, label, side, height, ticks: customTicks, align, }?: XAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
39
52
|
//# sourceMappingURL=XAxis.d.ts.map
|
package/dist/XAxis.js
CHANGED
|
@@ -17,7 +17,7 @@ const TICK_COUNT = 5;
|
|
|
17
17
|
*
|
|
18
18
|
* `<TimeAxis>` is the time-flavoured preset (`<XAxis />`).
|
|
19
19
|
*/
|
|
20
|
-
export function XAxis({ format, label, side = 'bottom', height, ticks: customTicks, } = {}) {
|
|
20
|
+
export function XAxis({ format, label, side = 'bottom', height, ticks: customTicks, align = 'center', } = {}) {
|
|
21
21
|
const container = useContext(ContainerContext);
|
|
22
22
|
if (container === null) {
|
|
23
23
|
throw new Error('<XAxis> must be rendered inside a <ChartContainer>');
|
|
@@ -51,23 +51,33 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
51
51
|
fontSize: `${theme.font.size}px`,
|
|
52
52
|
color: theme.axis.label,
|
|
53
53
|
}, children: [placed.map((t, i) => {
|
|
54
|
-
|
|
55
|
-
const
|
|
54
|
+
const isFirst = i === 0;
|
|
55
|
+
const isLast = i === placed.length - 1;
|
|
56
|
+
// `center`: every label centred on its tick. `auto`: centred, but the
|
|
57
|
+
// edge labels end-align so they stay within [0, plotWidth]. `right`:
|
|
58
|
+
// label left-anchored just past an extended tick (beside, not under).
|
|
59
|
+
const labelTransform = align === 'right'
|
|
56
60
|
? 'none'
|
|
57
|
-
:
|
|
58
|
-
? '
|
|
59
|
-
: '
|
|
61
|
+
: align === 'auto' && isFirst
|
|
62
|
+
? 'none'
|
|
63
|
+
: align === 'auto' && isLast
|
|
64
|
+
? 'translateX(-100%)'
|
|
65
|
+
: 'translateX(-50%)';
|
|
66
|
+
// `right` drops a longer tick alongside the label; others keep the 4px stub.
|
|
67
|
+
const tickHeight = align === 'right' ? theme.font.size + 4 : 4;
|
|
68
|
+
const labelLeft = align === 'right' ? t.x + 4 : t.x;
|
|
69
|
+
const labelOffset = align === 'right' ? 2 : 6;
|
|
60
70
|
return (_jsxs(Fragment, { children: [_jsx("div", { style: {
|
|
61
71
|
position: 'absolute',
|
|
62
72
|
left: `${t.x}px`,
|
|
63
73
|
[onTop ? 'bottom' : 'top']: 0,
|
|
64
74
|
width: '1px',
|
|
65
|
-
height:
|
|
75
|
+
height: `${tickHeight}px`,
|
|
66
76
|
background: theme.axis.grid,
|
|
67
77
|
} }), _jsx("div", { style: {
|
|
68
78
|
position: 'absolute',
|
|
69
|
-
left: `${
|
|
70
|
-
[onTop ? 'bottom' : 'top']:
|
|
79
|
+
left: `${labelLeft}px`,
|
|
80
|
+
[onTop ? 'bottom' : 'top']: `${labelOffset}px`,
|
|
71
81
|
transform: labelTransform,
|
|
72
82
|
whiteSpace: 'nowrap',
|
|
73
83
|
}, children: t.label })] }, `${t.x}-${i}`));
|
|
@@ -77,7 +87,10 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
77
87
|
width: '100%',
|
|
78
88
|
textAlign: 'center',
|
|
79
89
|
[onTop ? 'top' : 'bottom']: 0,
|
|
80
|
-
|
|
90
|
+
// Themeable axis-title text (shared with the rotated y-axis title).
|
|
91
|
+
fontSize: `${theme.axis.title?.size ?? theme.font.size + 1}px`,
|
|
92
|
+
color: theme.axis.title?.color ?? theme.axis.label,
|
|
93
|
+
opacity: theme.axis.title?.opacity ?? 0.85,
|
|
81
94
|
whiteSpace: 'nowrap',
|
|
82
95
|
}, children: label }))] }));
|
|
83
96
|
}
|
package/dist/YAxis.d.ts
CHANGED
|
@@ -11,9 +11,25 @@ export interface YAxisProps {
|
|
|
11
11
|
side?: 'left' | 'right';
|
|
12
12
|
/** Display label / unit (e.g. `bpm`); defaults to `id`. */
|
|
13
13
|
label?: string;
|
|
14
|
+
/**
|
|
15
|
+
* How the axis title (`label`) is drawn:
|
|
16
|
+
* - **`'rotated'` (default)** — a thin vertical strip down the outer edge
|
|
17
|
+
* (the standard y-axis convention; fits long labels in a narrow gutter).
|
|
18
|
+
* - `'top'` — horizontal, at the top of the axis, aligned to its side. Reads
|
|
19
|
+
* better for short unit labels; keep it terse and pair it with a domain
|
|
20
|
+
* that has headroom (auto-fit / padded) so it doesn't crowd the top tick.
|
|
21
|
+
*/
|
|
22
|
+
labelPlacement?: 'rotated' | 'top';
|
|
14
23
|
/** Explicit domain bounds; omit to auto-fit the charts linked to this axis. */
|
|
15
24
|
min?: number;
|
|
16
25
|
max?: number;
|
|
26
|
+
/**
|
|
27
|
+
* Fractional headroom added to each side of the resolved domain — `0` (the
|
|
28
|
+
* default) means none. Lifts a tight domain off the plot edges without
|
|
29
|
+
* hand-computing bounds (e.g. `pad={0.05}` adds 5% of the span top & bottom).
|
|
30
|
+
* Applies to an explicit `[min, max]` or an auto-fit domain.
|
|
31
|
+
*/
|
|
32
|
+
pad?: number;
|
|
17
33
|
/**
|
|
18
34
|
* Value formatting for the tick labels (and the cursor readout, which matches):
|
|
19
35
|
* a d3 format specifier string (e.g. `'.0%'`, `',.2f'`) or a `(value) => string`
|
|
@@ -37,6 +53,14 @@ export interface YAxisProps {
|
|
|
37
53
|
readonly at: number;
|
|
38
54
|
readonly label: string;
|
|
39
55
|
}>;
|
|
56
|
+
/**
|
|
57
|
+
* Render the tick labels at the domain extremes (the top & bottom ticks)?
|
|
58
|
+
* **Default `true`.** `false` drops just those two numbers — the gridlines
|
|
59
|
+
* stay — for when the min/max labels crowd a stacked row's edges and you'd
|
|
60
|
+
* rather omit them than keep them. (Extreme labels are otherwise clamped to
|
|
61
|
+
* stay inside the row, never overflowing the edge.)
|
|
62
|
+
*/
|
|
63
|
+
boundaryLabels?: boolean;
|
|
40
64
|
/** Gutter width in CSS pixels (default 50). */
|
|
41
65
|
width?: number;
|
|
42
66
|
/**
|
|
@@ -53,5 +77,5 @@ export interface YAxisProps {
|
|
|
53
77
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
54
78
|
* (default: the first axis).
|
|
55
79
|
*/
|
|
56
|
-
export declare function YAxis({ id, side, label, min, max, format, ticks, width, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
80
|
+
export declare function YAxis({ id, side, label, min, max, format, ticks, pad, boundaryLabels, width, labelPlacement, index, }: YAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
57
81
|
//# sourceMappingURL=YAxis.d.ts.map
|
package/dist/YAxis.js
CHANGED
|
@@ -13,7 +13,7 @@ const TICK_COUNT = 5;
|
|
|
13
13
|
* tick marks + labels from that scale. Charts attach via `<LineChart axis="id">`
|
|
14
14
|
* (default: the first axis).
|
|
15
15
|
*/
|
|
16
|
-
export function YAxis({ id, side = 'left', label, min, max, format, ticks, width = DEFAULT_WIDTH, index = 0, }) {
|
|
16
|
+
export function YAxis({ id, side = 'left', label, min, max, format, ticks, pad = 0, boundaryLabels = true, width = DEFAULT_WIDTH, labelPlacement = 'rotated', index = 0, }) {
|
|
17
17
|
const container = useContext(ContainerContext);
|
|
18
18
|
if (container === null) {
|
|
19
19
|
throw new Error('<YAxis> must be rendered inside a <ChartContainer>');
|
|
@@ -28,10 +28,12 @@ export function YAxis({ id, side = 'left', label, min, max, format, ticks, width
|
|
|
28
28
|
width,
|
|
29
29
|
min,
|
|
30
30
|
max,
|
|
31
|
+
pad,
|
|
32
|
+
labelPlacement,
|
|
31
33
|
format,
|
|
32
34
|
tickValues: ticks?.map((t) => t.at),
|
|
33
35
|
index,
|
|
34
|
-
}), [id, side, width, min, max, format, ticks, index]);
|
|
36
|
+
}), [id, side, width, min, max, pad, labelPlacement, format, ticks, index]);
|
|
35
37
|
// A stable per-instance slot (see useSlotKey) keeps this axis in a fixed
|
|
36
38
|
// registry position, so a min/max/side change updates in place rather than
|
|
37
39
|
// re-appending (which would move the first axis behind a later one and
|
|
@@ -76,27 +78,50 @@ export function YAxis({ id, side = 'left', label, min, max, format, ticks, width
|
|
|
76
78
|
fontSize: `${theme.font.size}px`,
|
|
77
79
|
color: theme.axis.label,
|
|
78
80
|
}, children: [yScale &&
|
|
79
|
-
tickList.map(({ value, label }) =>
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
81
|
+
tickList.map(({ value, label }, i) => {
|
|
82
|
+
// Drop just the top & bottom labels when boundary labels are off
|
|
83
|
+
// (gridlines are drawn separately, so they stay).
|
|
84
|
+
if (!boundaryLabels && (i === 0 || i === tickList.length - 1))
|
|
85
|
+
return null;
|
|
86
|
+
// Clamp the label's centre so a domain-extreme label stays inside
|
|
87
|
+
// the row instead of half-overflowing the top/bottom edge (and
|
|
88
|
+
// colliding across a splitter in a stacked layout) — F-charts-6.
|
|
89
|
+
const half = theme.font.size / 2 + 1;
|
|
90
|
+
const top = Math.max(half, Math.min(row.height - half, yScale(value)));
|
|
91
|
+
return (_jsx("div", { style: {
|
|
92
|
+
position: 'absolute',
|
|
93
|
+
top: `${top}px`,
|
|
94
|
+
[side === 'left' ? 'right' : 'left']: '4px',
|
|
95
|
+
transform: 'translateY(-50%)',
|
|
96
|
+
whiteSpace: 'nowrap',
|
|
97
|
+
}, children: label }, value));
|
|
98
|
+
}), labelPlacement === 'top' ? (_jsx("div", { style: {
|
|
99
|
+
position: 'absolute',
|
|
100
|
+
top: 0,
|
|
101
|
+
// Align to the axis line (the plot-facing edge), matching the tick
|
|
102
|
+
// labels' alignment, rather than floating at the outer gutter edge.
|
|
103
|
+
[side === 'left' ? 'right' : 'left']: '4px',
|
|
104
|
+
fontSize: `${theme.axis.title?.size ?? theme.font.size + 1}px`,
|
|
105
|
+
color: theme.axis.title?.color ?? theme.axis.label,
|
|
106
|
+
opacity: theme.axis.title?.opacity ?? 0.85,
|
|
107
|
+
whiteSpace: 'nowrap',
|
|
108
|
+
pointerEvents: 'none',
|
|
109
|
+
}, children: label ?? id })) : (_jsx("div", { style: {
|
|
86
110
|
position: 'absolute',
|
|
87
111
|
[side === 'left' ? 'left' : 'right']: '1px',
|
|
88
112
|
top: 0,
|
|
89
113
|
bottom: 0,
|
|
90
|
-
width: `${theme.font.size +
|
|
114
|
+
width: `${(theme.axis.title?.size ?? theme.font.size + 1) + 3}px`,
|
|
91
115
|
display: 'flex',
|
|
92
116
|
alignItems: 'center',
|
|
93
117
|
justifyContent: 'center',
|
|
94
|
-
fontSize: `${theme.font.size
|
|
95
|
-
|
|
118
|
+
fontSize: `${theme.axis.title?.size ?? theme.font.size + 1}px`,
|
|
119
|
+
color: theme.axis.title?.color ?? theme.axis.label,
|
|
120
|
+
opacity: theme.axis.title?.opacity ?? 0.85,
|
|
96
121
|
pointerEvents: 'none',
|
|
97
122
|
}, children: _jsx("span", { style: {
|
|
98
123
|
whiteSpace: 'nowrap',
|
|
99
124
|
transform: `rotate(${side === 'left' ? -90 : 90}deg)`,
|
|
100
|
-
}, children: label ?? id }) })] }) }));
|
|
125
|
+
}, children: label ?? id }) }))] }) }));
|
|
101
126
|
}
|
|
102
127
|
//# sourceMappingURL=YAxis.js.map
|
package/dist/context.d.ts
CHANGED
|
@@ -396,6 +396,10 @@ export interface AxisSpec {
|
|
|
396
396
|
/** Explicit domain bounds, or `undefined` to auto-fit linked layers. */
|
|
397
397
|
readonly min: number | undefined;
|
|
398
398
|
readonly max: number | undefined;
|
|
399
|
+
/** Fractional headroom added to each side of the resolved domain (`0` = none). */
|
|
400
|
+
readonly pad: number;
|
|
401
|
+
/** Title placement; `'top'` makes the row reserve a header band above the plot. */
|
|
402
|
+
readonly labelPlacement: 'rotated' | 'top';
|
|
399
403
|
/** Value formatting for the tick labels + the cursor readout ({@link AxisFormat}),
|
|
400
404
|
* or `undefined` for the scale's d3 default. */
|
|
401
405
|
readonly format: AxisFormat | undefined;
|
|
@@ -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/domain.d.ts
CHANGED
|
@@ -14,6 +14,11 @@
|
|
|
14
14
|
* can otherwise invert it (e.g. `min=5` with no data would naively give
|
|
15
15
|
* `[5, 1]`). Two explicit bounds are returned as-is (an inverted explicit domain
|
|
16
16
|
* is a deliberate axis flip; we don't second-guess it).
|
|
17
|
+
*
|
|
18
|
+
* `pad` (fractional, default `0`) expands the resolved domain outward by
|
|
19
|
+
* `pad × span` on each side — headroom without hand-computing bounds, useful to
|
|
20
|
+
* lift a tight **explicit** domain off the plot edges. Applied last, to whatever
|
|
21
|
+
* domain was resolved (explicit or auto); `0` is a no-op.
|
|
17
22
|
*/
|
|
18
|
-
export declare function resolveYDomain(min: number | undefined, max: number | undefined, extents: Iterable<readonly [number, number] | null
|
|
23
|
+
export declare function resolveYDomain(min: number | undefined, max: number | undefined, extents: Iterable<readonly [number, number] | null>, pad?: number): [number, number];
|
|
19
24
|
//# sourceMappingURL=domain.d.ts.map
|
package/dist/domain.js
CHANGED
|
@@ -15,8 +15,22 @@ import { scaleLinear } from 'd3-scale';
|
|
|
15
15
|
* can otherwise invert it (e.g. `min=5` with no data would naively give
|
|
16
16
|
* `[5, 1]`). Two explicit bounds are returned as-is (an inverted explicit domain
|
|
17
17
|
* is a deliberate axis flip; we don't second-guess it).
|
|
18
|
+
*
|
|
19
|
+
* `pad` (fractional, default `0`) expands the resolved domain outward by
|
|
20
|
+
* `pad × span` on each side — headroom without hand-computing bounds, useful to
|
|
21
|
+
* lift a tight **explicit** domain off the plot edges. Applied last, to whatever
|
|
22
|
+
* domain was resolved (explicit or auto); `0` is a no-op.
|
|
18
23
|
*/
|
|
19
|
-
export function resolveYDomain(min, max, extents) {
|
|
24
|
+
export function resolveYDomain(min, max, extents, pad = 0) {
|
|
25
|
+
const result = resolveBase(min, max, extents);
|
|
26
|
+
if (pad) {
|
|
27
|
+
const [lo, hi] = result;
|
|
28
|
+
const p = pad * (hi - lo);
|
|
29
|
+
return [lo - p, hi + p];
|
|
30
|
+
}
|
|
31
|
+
return result;
|
|
32
|
+
}
|
|
33
|
+
function resolveBase(min, max, extents) {
|
|
20
34
|
// Both bounds explicit: trust them verbatim (allows an intentional flip).
|
|
21
35
|
if (min !== undefined && max !== undefined)
|
|
22
36
|
return [min, max];
|
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/theme.d.ts
CHANGED
|
@@ -88,6 +88,17 @@ export interface ChartTheme {
|
|
|
88
88
|
readonly grid: string;
|
|
89
89
|
/** Gridline dash pattern (px on/off pairs); `[]` for solid. */
|
|
90
90
|
readonly gridDash: readonly number[];
|
|
91
|
+
/**
|
|
92
|
+
* Typography for the axis **title** — the rotated y-axis unit strip and the
|
|
93
|
+
* x-axis label (distinct from the per-tick `label` colour above). Omit a
|
|
94
|
+
* field to fall back: `color` → `label`, `size` → `font.size + 1` (a touch
|
|
95
|
+
* larger than the ticks so the rotated strip reads), `opacity` → `0.85`.
|
|
96
|
+
*/
|
|
97
|
+
readonly title?: {
|
|
98
|
+
readonly color?: string;
|
|
99
|
+
readonly size?: number;
|
|
100
|
+
readonly opacity?: number;
|
|
101
|
+
};
|
|
91
102
|
};
|
|
92
103
|
/** Label / tick typography. One source for axes + chrome. */
|
|
93
104
|
readonly font: {
|
|
@@ -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.37.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.37.0",
|
|
42
|
+
"pond-ts": "^0.37.0",
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|