@pond-ts/charts 0.48.0 → 0.49.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 +215 -1
- package/dist/AreaChart.d.ts +18 -1
- package/dist/AreaChart.js +23 -2
- package/dist/BandChart.d.ts +21 -2
- package/dist/BandChart.js +68 -9
- package/dist/BarChart.d.ts +12 -1
- package/dist/BarChart.js +34 -1
- package/dist/BoxPlot.d.ts +18 -1
- package/dist/BoxPlot.js +60 -3
- package/dist/Candlestick.d.ts +8 -1
- package/dist/Candlestick.js +40 -6
- package/dist/ChartContainer.d.ts +23 -13
- package/dist/ChartContainer.js +86 -32
- package/dist/ChartRow.js +22 -3
- package/dist/Layers.js +37 -14
- package/dist/Legend.d.ts +62 -0
- package/dist/Legend.js +169 -0
- package/dist/LineChart.d.ts +20 -1
- package/dist/LineChart.js +23 -2
- package/dist/ScatterChart.d.ts +8 -1
- package/dist/ScatterChart.js +24 -1
- package/dist/XAxis.js +9 -2
- package/dist/YAxis.d.ts +9 -1
- package/dist/YAxis.js +27 -6
- package/dist/annotations.d.ts +21 -3
- package/dist/annotations.js +36 -15
- package/dist/area.d.ts +2 -1
- package/dist/area.js +29 -4
- package/dist/band.d.ts +2 -1
- package/dist/band.js +18 -1
- package/dist/bars.js +8 -1
- package/dist/box.d.ts +14 -1
- package/dist/box.js +56 -2
- package/dist/context.d.ts +51 -4
- package/dist/culling.d.ts +165 -0
- package/dist/culling.js +286 -0
- package/dist/data.d.ts +3 -1
- package/dist/decimate.d.ts +193 -0
- package/dist/decimate.js +359 -0
- package/dist/format.d.ts +20 -11
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/line.d.ts +2 -1
- package/dist/line.js +38 -3
- package/dist/ohlc.js +6 -1
- package/dist/scatter.js +42 -7
- package/dist/swatch.d.ts +104 -0
- package/dist/swatch.js +96 -0
- package/dist/theme.d.ts +27 -0
- package/dist/theme.js +12 -0
- package/dist/useChartLegend.d.ts +106 -0
- package/dist/useChartLegend.js +122 -0
- package/dist/yticks.d.ts +20 -0
- package/dist/yticks.js +28 -0
- package/package.json +3 -3
package/dist/swatch.d.ts
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<Legend>` support — the swatch vocabulary, the per-layer registration hook,
|
|
3
|
+
* and the pure ordering/dedup pipeline the component renders from.
|
|
4
|
+
*
|
|
5
|
+
* The mechanism ([PND-LEGEND], #508 item 2): each draw layer registers its
|
|
6
|
+
* **resolved** style as a {@link SwatchSpec} alongside its readout identity, so
|
|
7
|
+
* a legend can never drift from the plot — the property an app-side legend
|
|
8
|
+
* only gets by manually sharing a palette. The legend renders the registry;
|
|
9
|
+
* layers self-describe.
|
|
10
|
+
*/
|
|
11
|
+
import { type ContainerFrame } from './context.js';
|
|
12
|
+
/**
|
|
13
|
+
* A legend item's **swatch** — the layer's resolved style, in the mark's own
|
|
14
|
+
* vocabulary (a line shows stroke + dash, a band shows its translucent fill, a
|
|
15
|
+
* candle shows its up/down pair). Resolved means post-theme: the exact values
|
|
16
|
+
* the canvas draws with, so the swatch and the mark can never disagree.
|
|
17
|
+
*/
|
|
18
|
+
export type SwatchSpec = {
|
|
19
|
+
readonly kind: 'line';
|
|
20
|
+
readonly color: string;
|
|
21
|
+
readonly width: number;
|
|
22
|
+
readonly dash?: readonly number[] | undefined;
|
|
23
|
+
} | {
|
|
24
|
+
readonly kind: 'area';
|
|
25
|
+
readonly line: string;
|
|
26
|
+
readonly fill: string;
|
|
27
|
+
readonly fillOpacity: number;
|
|
28
|
+
} | {
|
|
29
|
+
readonly kind: 'band';
|
|
30
|
+
readonly fill: string;
|
|
31
|
+
readonly opacity: number;
|
|
32
|
+
} | {
|
|
33
|
+
readonly kind: 'scatter';
|
|
34
|
+
readonly color: string;
|
|
35
|
+
readonly radius: number;
|
|
36
|
+
readonly outline?: string | undefined;
|
|
37
|
+
} | {
|
|
38
|
+
readonly kind: 'box';
|
|
39
|
+
readonly whisker: string;
|
|
40
|
+
readonly whiskerWidth: number;
|
|
41
|
+
} | {
|
|
42
|
+
readonly kind: 'bar';
|
|
43
|
+
readonly fill: string;
|
|
44
|
+
} | {
|
|
45
|
+
readonly kind: 'candle';
|
|
46
|
+
readonly up: string;
|
|
47
|
+
readonly down: string;
|
|
48
|
+
};
|
|
49
|
+
/**
|
|
50
|
+
* One row as a layer registers it (and as `<Legend items>` accepts it):
|
|
51
|
+
* the display `label` (the layer's readout identity `as ?? column`, or its
|
|
52
|
+
* `legend="name"` override), the resolved {@link SwatchSpec}, and — when the
|
|
53
|
+
* layer is interactive — its selection `id`.
|
|
54
|
+
*/
|
|
55
|
+
export interface LegendItemInput {
|
|
56
|
+
readonly label: string;
|
|
57
|
+
readonly swatch: SwatchSpec;
|
|
58
|
+
/** The layer's selection identity (its `id` prop) — gates the legend's
|
|
59
|
+
* default hover/select interactions, and keys dedup ahead of `label`. */
|
|
60
|
+
readonly id?: string | undefined;
|
|
61
|
+
}
|
|
62
|
+
/** A registered legend row: the input plus its place in the chart (chart row,
|
|
63
|
+
* declaration index, position within the layer), which drives display order. */
|
|
64
|
+
export interface LegendItemSpec extends LegendItemInput {
|
|
65
|
+
readonly rowKey: symbol;
|
|
66
|
+
readonly index: number;
|
|
67
|
+
/** Position within a multi-row layer (a stacked bar registers one row per
|
|
68
|
+
* column, in stack order) — `0` for the single-row marks. */
|
|
69
|
+
readonly subIndex: number;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Order + dedup the registered rows for display: chart-row order first (the
|
|
73
|
+
* container's top-to-bottom `rowOrder`), declaration `index` within a row —
|
|
74
|
+
* the existing z-order convention — then `subIndex` (stack order within a
|
|
75
|
+
* multi-row layer), then label as a final stable tiebreak. Dedup keys on
|
|
76
|
+
* `(id ?? label, subIndex)` — the A2.2 selection model makes `id` the series
|
|
77
|
+
* identity (a theme-role label can repeat), and the `subIndex` component
|
|
78
|
+
* keeps a **multi-group layer's** rows distinct (its groups all share the
|
|
79
|
+
* layer's `id`; without it a stacked bar with an `id` would collapse to one
|
|
80
|
+
* row). Two single-row layers still collapse on a shared identity (both are
|
|
81
|
+
* `subIndex 0`) — the first row in display order stands, exactly as the
|
|
82
|
+
* tracker readout merges keys.
|
|
83
|
+
*/
|
|
84
|
+
export declare function orderLegendItems(items: Iterable<LegendItemSpec>, rowOrder: readonly symbol[]): LegendItemSpec[];
|
|
85
|
+
/**
|
|
86
|
+
* Resolve a layer's `legend` prop + identity into what it registers:
|
|
87
|
+
* `false` ⇒ `null` (opt out — no row); a string ⇒ that display name; omitted /
|
|
88
|
+
* `true` ⇒ the layer's own readout identity. Kept as a helper so every mark
|
|
89
|
+
* resolves the prop identically.
|
|
90
|
+
*/
|
|
91
|
+
export declare function legendLabelFor(legend: boolean | string | undefined, identity: string): string | null;
|
|
92
|
+
/**
|
|
93
|
+
* Register this layer instance's legend row(s) — one for the single-row marks,
|
|
94
|
+
* one per column for a stacked bar — keyed off the layer's per-instance
|
|
95
|
+
* `slot` (row *i* registers under a derived per-index key, so the layer's rows
|
|
96
|
+
* live and die together); unregister all on unmount. Pass `rows: null` (or
|
|
97
|
+
* `[]`) to register nothing (the layer opted out via `legend={false}`).
|
|
98
|
+
* `rows` must be **memoised by the caller** (like `useRegisterAnnotation`'s
|
|
99
|
+
* `xs`) so the effect re-runs only when a swatch / label genuinely changes,
|
|
100
|
+
* not every render. Reads the enclosing chart row's key itself — a layer
|
|
101
|
+
* outside a `<ChartRow>` (impossible today) simply doesn't register.
|
|
102
|
+
*/
|
|
103
|
+
export declare function useLegendItems(container: ContainerFrame, slot: symbol, index: number, rows: readonly LegendItemInput[] | null): void;
|
|
104
|
+
//# sourceMappingURL=swatch.d.ts.map
|
package/dist/swatch.js
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `<Legend>` support — the swatch vocabulary, the per-layer registration hook,
|
|
3
|
+
* and the pure ordering/dedup pipeline the component renders from.
|
|
4
|
+
*
|
|
5
|
+
* The mechanism ([PND-LEGEND], #508 item 2): each draw layer registers its
|
|
6
|
+
* **resolved** style as a {@link SwatchSpec} alongside its readout identity, so
|
|
7
|
+
* a legend can never drift from the plot — the property an app-side legend
|
|
8
|
+
* only gets by manually sharing a palette. The legend renders the registry;
|
|
9
|
+
* layers self-describe.
|
|
10
|
+
*/
|
|
11
|
+
import { useContext, useEffect, useRef } from 'react';
|
|
12
|
+
import { RowContext } from './context.js';
|
|
13
|
+
/**
|
|
14
|
+
* Order + dedup the registered rows for display: chart-row order first (the
|
|
15
|
+
* container's top-to-bottom `rowOrder`), declaration `index` within a row —
|
|
16
|
+
* the existing z-order convention — then `subIndex` (stack order within a
|
|
17
|
+
* multi-row layer), then label as a final stable tiebreak. Dedup keys on
|
|
18
|
+
* `(id ?? label, subIndex)` — the A2.2 selection model makes `id` the series
|
|
19
|
+
* identity (a theme-role label can repeat), and the `subIndex` component
|
|
20
|
+
* keeps a **multi-group layer's** rows distinct (its groups all share the
|
|
21
|
+
* layer's `id`; without it a stacked bar with an `id` would collapse to one
|
|
22
|
+
* row). Two single-row layers still collapse on a shared identity (both are
|
|
23
|
+
* `subIndex 0`) — the first row in display order stands, exactly as the
|
|
24
|
+
* tracker readout merges keys.
|
|
25
|
+
*/
|
|
26
|
+
export function orderLegendItems(items, rowOrder) {
|
|
27
|
+
const rowPos = new Map();
|
|
28
|
+
rowOrder.forEach((k, i) => rowPos.set(k, i));
|
|
29
|
+
const sorted = [...items].sort((a, b) => (rowPos.get(a.rowKey) ?? rowOrder.length) -
|
|
30
|
+
(rowPos.get(b.rowKey) ?? rowOrder.length) ||
|
|
31
|
+
a.index - b.index ||
|
|
32
|
+
a.subIndex - b.subIndex ||
|
|
33
|
+
a.label.localeCompare(b.label));
|
|
34
|
+
const seen = new Set();
|
|
35
|
+
const out = [];
|
|
36
|
+
for (const it of sorted) {
|
|
37
|
+
const key = `${it.id ?? it.label} ${it.subIndex}`;
|
|
38
|
+
if (seen.has(key))
|
|
39
|
+
continue;
|
|
40
|
+
seen.add(key);
|
|
41
|
+
out.push(it);
|
|
42
|
+
}
|
|
43
|
+
return out;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Resolve a layer's `legend` prop + identity into what it registers:
|
|
47
|
+
* `false` ⇒ `null` (opt out — no row); a string ⇒ that display name; omitted /
|
|
48
|
+
* `true` ⇒ the layer's own readout identity. Kept as a helper so every mark
|
|
49
|
+
* resolves the prop identically.
|
|
50
|
+
*/
|
|
51
|
+
export function legendLabelFor(legend, identity) {
|
|
52
|
+
if (legend === false)
|
|
53
|
+
return null;
|
|
54
|
+
return typeof legend === 'string' && legend.length > 0 ? legend : identity;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Register this layer instance's legend row(s) — one for the single-row marks,
|
|
58
|
+
* one per column for a stacked bar — keyed off the layer's per-instance
|
|
59
|
+
* `slot` (row *i* registers under a derived per-index key, so the layer's rows
|
|
60
|
+
* live and die together); unregister all on unmount. Pass `rows: null` (or
|
|
61
|
+
* `[]`) to register nothing (the layer opted out via `legend={false}`).
|
|
62
|
+
* `rows` must be **memoised by the caller** (like `useRegisterAnnotation`'s
|
|
63
|
+
* `xs`) so the effect re-runs only when a swatch / label genuinely changes,
|
|
64
|
+
* not every render. Reads the enclosing chart row's key itself — a layer
|
|
65
|
+
* outside a `<ChartRow>` (impossible today) simply doesn't register.
|
|
66
|
+
*/
|
|
67
|
+
export function useLegendItems(container, slot, index, rows) {
|
|
68
|
+
const rowFrame = useContext(RowContext);
|
|
69
|
+
const rowKey = rowFrame?.rowKey;
|
|
70
|
+
const { registerLegendItem, unregisterLegendItem } = container;
|
|
71
|
+
// Stable per-subIndex child keys, grown lazily; `slot` itself keys row 0 so
|
|
72
|
+
// the common single-row mark registers exactly one symbol.
|
|
73
|
+
const childKeys = useRef([slot]);
|
|
74
|
+
useEffect(() => {
|
|
75
|
+
const keys = childKeys.current;
|
|
76
|
+
return () => keys.forEach((k) => unregisterLegendItem(k));
|
|
77
|
+
}, [unregisterLegendItem]);
|
|
78
|
+
useEffect(() => {
|
|
79
|
+
const keys = childKeys.current;
|
|
80
|
+
const want = rowKey === undefined ? [] : (rows ?? []);
|
|
81
|
+
while (keys.length < want.length) {
|
|
82
|
+
keys.push(Symbol(`legend-${keys.length}`));
|
|
83
|
+
}
|
|
84
|
+
want.forEach((row, i) => registerLegendItem(keys[i], {
|
|
85
|
+
...row,
|
|
86
|
+
rowKey: rowKey,
|
|
87
|
+
index,
|
|
88
|
+
subIndex: i,
|
|
89
|
+
}));
|
|
90
|
+
// Drop rows beyond the current count (a stack that lost a column).
|
|
91
|
+
for (let i = want.length; i < keys.length; i += 1) {
|
|
92
|
+
unregisterLegendItem(keys[i]);
|
|
93
|
+
}
|
|
94
|
+
}, [registerLegendItem, unregisterLegendItem, slot, rows, rowKey, index]);
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=swatch.js.map
|
package/dist/theme.d.ts
CHANGED
|
@@ -179,6 +179,33 @@ export interface ChartTheme {
|
|
|
179
179
|
readonly color: string;
|
|
180
180
|
readonly fillOpacity: number;
|
|
181
181
|
readonly depth: readonly [number, number, number];
|
|
182
|
+
/**
|
|
183
|
+
* **Optional per-role overrides** — a small map from a role name to its
|
|
184
|
+
* `color` (and optionally `fillOpacity`), so distinct marks can be styled
|
|
185
|
+
* at once without splitting the whole register: a `<Baseline role="atm">`
|
|
186
|
+
* green, a `<Marker role="ref">` in another hue, each still drawn through
|
|
187
|
+
* the shared {@link depth} ramp. A mark's `role` resolves
|
|
188
|
+
* `roles[role] ?? { color, fillOpacity }` (an unknown/unset role is the
|
|
189
|
+
* base register). Colour stays a **theme** concern — there is no per-mark
|
|
190
|
+
* colour prop (the one-styling-channel discipline).
|
|
191
|
+
*/
|
|
192
|
+
readonly roles?: {
|
|
193
|
+
readonly [role: string]: {
|
|
194
|
+
readonly color: string;
|
|
195
|
+
readonly fillOpacity?: number;
|
|
196
|
+
};
|
|
197
|
+
};
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* The **`<Legend>` card** — background, border, and label text of the
|
|
201
|
+
* in-chart series key. **Optional**: when absent the legend derives from
|
|
202
|
+
* existing tokens (`chip.background`, `axis.grid`, `axis.label`), so a
|
|
203
|
+
* hand-built theme keeps compiling and reads coherently without opting in.
|
|
204
|
+
*/
|
|
205
|
+
readonly legend?: {
|
|
206
|
+
readonly background: string;
|
|
207
|
+
readonly border: string;
|
|
208
|
+
readonly text: string;
|
|
182
209
|
};
|
|
183
210
|
}
|
|
184
211
|
/** A resolved line style: stroke colour + width (px). */
|
package/dist/theme.js
CHANGED
|
@@ -131,6 +131,12 @@ export const defaultTheme = {
|
|
|
131
131
|
fillOpacity: 0.1,
|
|
132
132
|
depth: [1, 0.7, 0.4],
|
|
133
133
|
},
|
|
134
|
+
// The in-chart series key: chip-white card, gridline border, axis-label text.
|
|
135
|
+
legend: {
|
|
136
|
+
background: '#ffffff',
|
|
137
|
+
border: '#e2e8f0',
|
|
138
|
+
text: '#64748b',
|
|
139
|
+
},
|
|
134
140
|
};
|
|
135
141
|
/**
|
|
136
142
|
* The estela theme — estela's real `@estela/ui` palette as *one theme*, on its
|
|
@@ -271,5 +277,11 @@ export const estelaTheme = {
|
|
|
271
277
|
fillOpacity: 0.1,
|
|
272
278
|
depth: [1, 0.7, 0.4],
|
|
273
279
|
},
|
|
280
|
+
// The in-chart series key on the dark ground: deep panel, abyss-line border.
|
|
281
|
+
legend: {
|
|
282
|
+
background: '#0B4E58', // --es-deep (the chip panel)
|
|
283
|
+
border: '#1B6B75',
|
|
284
|
+
text: '#B7D9DD',
|
|
285
|
+
},
|
|
274
286
|
};
|
|
275
287
|
//# sourceMappingURL=theme.js.map
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { type ContainerFrame } from './context.js';
|
|
2
|
+
import type { SelectInfo } from './context.js';
|
|
3
|
+
import { type LegendItemInput, type SwatchSpec } from './swatch.js';
|
|
4
|
+
/** One legend **item** as {@link useChartLegend} serves it — a series entry:
|
|
5
|
+
* the registered identity + resolved swatch, plus its live interaction
|
|
6
|
+
* state. (Items are grouped into {@link LegendRow}s by chart row.) */
|
|
7
|
+
export interface LegendItem extends LegendItemInput {
|
|
8
|
+
/** This item's series is the container's current selection (id-keyed). */
|
|
9
|
+
readonly selected: boolean;
|
|
10
|
+
/** This item's series is the container's current hover (id-keyed). */
|
|
11
|
+
readonly hovered: boolean;
|
|
12
|
+
}
|
|
13
|
+
/** One **chart row's** group of legend {@link LegendItem}s — the grouping
|
|
14
|
+
* matches the chart's `<ChartRow>` layout, so a legend can mirror the rows
|
|
15
|
+
* (a flat legend is `rows.flatMap((r) => r.items)`). */
|
|
16
|
+
export interface LegendRow {
|
|
17
|
+
/** The chart row these items belong to (its stable per-row key). */
|
|
18
|
+
readonly rowKey: symbol;
|
|
19
|
+
/** The row's items, in display order (declaration → stack position). */
|
|
20
|
+
readonly items: readonly LegendItem[];
|
|
21
|
+
}
|
|
22
|
+
/** What {@link useChartLegend} returns — the data shape + the sync verbs a
|
|
23
|
+
* custom-rendered legend needs. */
|
|
24
|
+
export interface ChartLegend {
|
|
25
|
+
/** The registered items **grouped by chart row** (top-to-bottom), each
|
|
26
|
+
* group's items in declaration → stack order, deduped by
|
|
27
|
+
* `(id ?? label, stack position)`. A flat list is
|
|
28
|
+
* `rows.flatMap((r) => r.items)`; a row-scoped legend has one entry. */
|
|
29
|
+
readonly rows: readonly LegendRow[];
|
|
30
|
+
/**
|
|
31
|
+
* The container's reserved axis gutters in px — how far the **plot** is
|
|
32
|
+
* inset from the chart box on each side. A custom legend laid out above /
|
|
33
|
+
* below the chart pads by `gutters.left` (and `gutters.right`) to align
|
|
34
|
+
* with the plot instead of the y-axis column.
|
|
35
|
+
*/
|
|
36
|
+
readonly gutters: {
|
|
37
|
+
readonly left: number;
|
|
38
|
+
readonly right: number;
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* The **cursor's x position** in axis units — epoch ms on a time axis, the
|
|
42
|
+
* axis value on a value axis (the `TrackerInfo.time` convention) — or
|
|
43
|
+
* `null` when no cursor is live. The values-in-the-legend seam: with a
|
|
44
|
+
* row's identity and this instant, look up each series' value at the
|
|
45
|
+
* cursor (`series.nearest(cursorTime)`), falling back to the latest sample
|
|
46
|
+
* when `null` — a "current or cursor value" readout per row. Live: the
|
|
47
|
+
* frame rebuilds as the cursor moves, so a hook consumer re-renders with
|
|
48
|
+
* it.
|
|
49
|
+
*/
|
|
50
|
+
readonly cursorTime: number | null;
|
|
51
|
+
/**
|
|
52
|
+
* Echo a row hover into the container's `hovered` channel (the same one an
|
|
53
|
+
* in-plot hover drives — marks light up where they support it, and
|
|
54
|
+
* `onHover` fires); `null` clears. Id-gated: a row without an `id` is a
|
|
55
|
+
* no-op (there is no series identity to point at).
|
|
56
|
+
*/
|
|
57
|
+
hover(row: LegendItemInput | null): void;
|
|
58
|
+
/**
|
|
59
|
+
* Toggle the container selection to this row's series (fires `onSelect`;
|
|
60
|
+
* clears when the row is already selected — the `<Legend>` click
|
|
61
|
+
* semantics). Id-gated no-op for rows without an `id`.
|
|
62
|
+
*/
|
|
63
|
+
select(row: LegendItemInput): void;
|
|
64
|
+
}
|
|
65
|
+
/** The series-scoped {@link SelectInfo} a legend interaction reports: no
|
|
66
|
+
* sample under it, so `key`/`value` are deliberately `NaN` (see the
|
|
67
|
+
* provenance note on {@link SelectInfo.key}); `color` is the swatch's
|
|
68
|
+
* primary colour. */
|
|
69
|
+
export declare function seriesSelectInfo(row: LegendItemInput): SelectInfo;
|
|
70
|
+
/** The swatch's primary colour — what a series-scoped `SelectInfo` reports. */
|
|
71
|
+
export declare function swatchColor(s: SwatchSpec): string;
|
|
72
|
+
/** Build the {@link ChartLegend} from a frame — the pure core `<Legend>` and
|
|
73
|
+
* {@link useChartLegend} share, so a custom-rendered legend and the built-in
|
|
74
|
+
* card can never disagree about rows or sync semantics. Pass a `rowKey` to
|
|
75
|
+
* **scope** the rows to a single chart row (the layers registered under it);
|
|
76
|
+
* omit it for the whole container. */
|
|
77
|
+
export declare function buildChartLegend(container: ContainerFrame, rowKey?: symbol): ChartLegend;
|
|
78
|
+
/**
|
|
79
|
+
* **Headless legend** — the registry `<Legend>` renders, as data, plus the
|
|
80
|
+
* hover/select verbs already wired to the chart. For consumers whose legend
|
|
81
|
+
* is a design of its own (a horizontal strip, a ticker-compare pair with the
|
|
82
|
+
* secondary dimmed, values-in-the-legend): render from `rows` (items grouped
|
|
83
|
+
* by chart row; a flat legend is `rows.flatMap((r) => r.items)`), call
|
|
84
|
+
* `hover`/`select` from your item handlers, and read each item's
|
|
85
|
+
* `selected`/`hovered` state back — the same contract the built-in card uses,
|
|
86
|
+
* because both are built by {@link buildChartLegend}.
|
|
87
|
+
*
|
|
88
|
+
* **Readout integration:** an item's `label` is the layer's readout identity —
|
|
89
|
+
* the same string the tracker's `onTrackerChanged` samples carry — so merging
|
|
90
|
+
* live cursor values into your legend is a label-keyed join, no extra
|
|
91
|
+
* plumbing.
|
|
92
|
+
*
|
|
93
|
+
* **Scope follows placement:** called at the container level it enumerates
|
|
94
|
+
* **all** rows; called inside a `<Layers>` (i.e. under a `<ChartRow>`) it
|
|
95
|
+
* **scopes to that row's** layers — a per-row legend needs no prop, just
|
|
96
|
+
* placement, the same way annotations scope to the row they sit in.
|
|
97
|
+
*
|
|
98
|
+
* **Placement:** it reads the frame, so it works anywhere under the
|
|
99
|
+
* `<ChartContainer>`. To render the markup *outside* the chart's box, portal
|
|
100
|
+
* it out (`createPortal`) — context flows through portals.
|
|
101
|
+
*
|
|
102
|
+
* Must be under a `<ChartContainer>`; throws otherwise (for a fully detached
|
|
103
|
+
* key, `<Legend items>` renders explicit rows without a chart).
|
|
104
|
+
*/
|
|
105
|
+
export declare function useChartLegend(): ChartLegend;
|
|
106
|
+
//# sourceMappingURL=useChartLegend.d.ts.map
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { useContext, useMemo } from 'react';
|
|
2
|
+
import { ContainerContext, RowContext, } from './context.js';
|
|
3
|
+
import { orderLegendItems, } from './swatch.js';
|
|
4
|
+
/** The series-scoped {@link SelectInfo} a legend interaction reports: no
|
|
5
|
+
* sample under it, so `key`/`value` are deliberately `NaN` (see the
|
|
6
|
+
* provenance note on {@link SelectInfo.key}); `color` is the swatch's
|
|
7
|
+
* primary colour. */
|
|
8
|
+
export function seriesSelectInfo(row) {
|
|
9
|
+
return {
|
|
10
|
+
id: row.id,
|
|
11
|
+
key: NaN,
|
|
12
|
+
value: NaN,
|
|
13
|
+
color: swatchColor(row.swatch),
|
|
14
|
+
label: row.label,
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
/** The swatch's primary colour — what a series-scoped `SelectInfo` reports. */
|
|
18
|
+
export function swatchColor(s) {
|
|
19
|
+
switch (s.kind) {
|
|
20
|
+
case 'line':
|
|
21
|
+
return s.color;
|
|
22
|
+
case 'area':
|
|
23
|
+
return s.line;
|
|
24
|
+
case 'band':
|
|
25
|
+
return s.fill;
|
|
26
|
+
case 'scatter':
|
|
27
|
+
return s.color;
|
|
28
|
+
case 'box':
|
|
29
|
+
return s.whisker;
|
|
30
|
+
case 'bar':
|
|
31
|
+
return s.fill;
|
|
32
|
+
case 'candle':
|
|
33
|
+
return s.up;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** Build the {@link ChartLegend} from a frame — the pure core `<Legend>` and
|
|
37
|
+
* {@link useChartLegend} share, so a custom-rendered legend and the built-in
|
|
38
|
+
* card can never disagree about rows or sync semantics. Pass a `rowKey` to
|
|
39
|
+
* **scope** the rows to a single chart row (the layers registered under it);
|
|
40
|
+
* omit it for the whole container. */
|
|
41
|
+
export function buildChartLegend(container, rowKey) {
|
|
42
|
+
const scoped = Array.from(container.legendItems.values()).filter((it) => rowKey === undefined || it.rowKey === rowKey);
|
|
43
|
+
// Ordered + deduped specs (chart-row first), each carrying its `rowKey`;
|
|
44
|
+
// group consecutive same-row specs into a LegendRow, mapping spec → item.
|
|
45
|
+
const rows = [];
|
|
46
|
+
for (const spec of orderLegendItems(scoped, container.rowOrder)) {
|
|
47
|
+
const item = {
|
|
48
|
+
label: spec.label,
|
|
49
|
+
swatch: spec.swatch,
|
|
50
|
+
...(spec.id !== undefined ? { id: spec.id } : {}),
|
|
51
|
+
selected: spec.id !== undefined && container.selected?.id === spec.id,
|
|
52
|
+
hovered: spec.id !== undefined && container.hovered?.id === spec.id,
|
|
53
|
+
};
|
|
54
|
+
const last = rows[rows.length - 1];
|
|
55
|
+
if (last !== undefined && last.rowKey === spec.rowKey) {
|
|
56
|
+
last.items.push(item);
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
rows.push({ rowKey: spec.rowKey, items: [item] });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
// The cursor pixel → axis units, exactly as the tracker fan-in resolves it
|
|
63
|
+
// (in-bounds guard included, so an off-plot cursor reads as "no cursor").
|
|
64
|
+
const cursorTime = container.cursorX !== null &&
|
|
65
|
+
container.cursorX >= 0 &&
|
|
66
|
+
container.cursorX <= container.plotWidth
|
|
67
|
+
? +container.xScale.invert(container.cursorX)
|
|
68
|
+
: null;
|
|
69
|
+
return {
|
|
70
|
+
rows,
|
|
71
|
+
gutters: { left: container.leftGutter, right: container.rightGutter },
|
|
72
|
+
cursorTime,
|
|
73
|
+
hover: (row) => {
|
|
74
|
+
if (row === null)
|
|
75
|
+
return container.setHovered(null);
|
|
76
|
+
if (row.id !== undefined)
|
|
77
|
+
container.setHovered(seriesSelectInfo(row));
|
|
78
|
+
},
|
|
79
|
+
select: (row) => {
|
|
80
|
+
if (row.id === undefined)
|
|
81
|
+
return;
|
|
82
|
+
container.select(container.selected?.id === row.id ? null : seriesSelectInfo(row));
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* **Headless legend** — the registry `<Legend>` renders, as data, plus the
|
|
88
|
+
* hover/select verbs already wired to the chart. For consumers whose legend
|
|
89
|
+
* is a design of its own (a horizontal strip, a ticker-compare pair with the
|
|
90
|
+
* secondary dimmed, values-in-the-legend): render from `rows` (items grouped
|
|
91
|
+
* by chart row; a flat legend is `rows.flatMap((r) => r.items)`), call
|
|
92
|
+
* `hover`/`select` from your item handlers, and read each item's
|
|
93
|
+
* `selected`/`hovered` state back — the same contract the built-in card uses,
|
|
94
|
+
* because both are built by {@link buildChartLegend}.
|
|
95
|
+
*
|
|
96
|
+
* **Readout integration:** an item's `label` is the layer's readout identity —
|
|
97
|
+
* the same string the tracker's `onTrackerChanged` samples carry — so merging
|
|
98
|
+
* live cursor values into your legend is a label-keyed join, no extra
|
|
99
|
+
* plumbing.
|
|
100
|
+
*
|
|
101
|
+
* **Scope follows placement:** called at the container level it enumerates
|
|
102
|
+
* **all** rows; called inside a `<Layers>` (i.e. under a `<ChartRow>`) it
|
|
103
|
+
* **scopes to that row's** layers — a per-row legend needs no prop, just
|
|
104
|
+
* placement, the same way annotations scope to the row they sit in.
|
|
105
|
+
*
|
|
106
|
+
* **Placement:** it reads the frame, so it works anywhere under the
|
|
107
|
+
* `<ChartContainer>`. To render the markup *outside* the chart's box, portal
|
|
108
|
+
* it out (`createPortal`) — context flows through portals.
|
|
109
|
+
*
|
|
110
|
+
* Must be under a `<ChartContainer>`; throws otherwise (for a fully detached
|
|
111
|
+
* key, `<Legend items>` renders explicit rows without a chart).
|
|
112
|
+
*/
|
|
113
|
+
export function useChartLegend() {
|
|
114
|
+
const container = useContext(ContainerContext);
|
|
115
|
+
if (container === null) {
|
|
116
|
+
throw new Error('useChartLegend() must be used inside a <ChartContainer>');
|
|
117
|
+
}
|
|
118
|
+
// A RowContext in scope (rendered inside a <Layers>) narrows to that row.
|
|
119
|
+
const rowKey = useContext(RowContext)?.rowKey;
|
|
120
|
+
return useMemo(() => buildChartLegend(container, rowKey), [container, rowKey]);
|
|
121
|
+
}
|
|
122
|
+
//# sourceMappingURL=useChartLegend.js.map
|
package/dist/yticks.d.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **y-axis auto-tick count** — the `count` a `<YAxis>`'s labels and the
|
|
3
|
+
* row's gridlines both pass to `scale.ticks(count)` / `tickFormat(count)`, so
|
|
4
|
+
* a label and its gridline stay on the same instants (the alignment the two
|
|
5
|
+
* hardcoded `5`s in `YAxis` + `Layers` used to hold by agreeing).
|
|
6
|
+
*
|
|
7
|
+
* Height-derived by default — a short strip gets fewer ticks than a tall row,
|
|
8
|
+
* so a 72px histogram lane no longer crushes 5 labels into the space a 380px
|
|
9
|
+
* row uses (the #508 vol-surface friction). This mirrors the trading-time x
|
|
10
|
+
* axis, whose count is width-derived (0.44.1). An explicit `<YAxis tickCount>`
|
|
11
|
+
* overrides the derivation; explicit `<YAxis ticks>` bypasses this entirely.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Resolve a y-axis's auto-tick count: the explicit `tickCount` when given,
|
|
15
|
+
* else `floor(height / Y_TICK_PX)` floored at 2 (a drawable minimum even on a
|
|
16
|
+
* pre-layout zero height). `ticks(count)` treats it as a target and returns
|
|
17
|
+
* nice 1-2-5 values near it, so a larger count on a tall row is exactly right.
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveYTickCount(height: number, explicit?: number | undefined): number;
|
|
20
|
+
//# sourceMappingURL=yticks.d.ts.map
|
package/dist/yticks.js
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The **y-axis auto-tick count** — the `count` a `<YAxis>`'s labels and the
|
|
3
|
+
* row's gridlines both pass to `scale.ticks(count)` / `tickFormat(count)`, so
|
|
4
|
+
* a label and its gridline stay on the same instants (the alignment the two
|
|
5
|
+
* hardcoded `5`s in `YAxis` + `Layers` used to hold by agreeing).
|
|
6
|
+
*
|
|
7
|
+
* Height-derived by default — a short strip gets fewer ticks than a tall row,
|
|
8
|
+
* so a 72px histogram lane no longer crushes 5 labels into the space a 380px
|
|
9
|
+
* row uses (the #508 vol-surface friction). This mirrors the trading-time x
|
|
10
|
+
* axis, whose count is width-derived (0.44.1). An explicit `<YAxis tickCount>`
|
|
11
|
+
* overrides the derivation; explicit `<YAxis ticks>` bypasses this entirely.
|
|
12
|
+
*/
|
|
13
|
+
/** Target px of row height per y tick. A y label is one line, so this is the
|
|
14
|
+
* vertical breathing room between gridlines — a touch tighter than the x
|
|
15
|
+
* axis's per-tick budget, since stacked numbers need less room than dates. */
|
|
16
|
+
const Y_TICK_PX = 48;
|
|
17
|
+
/**
|
|
18
|
+
* Resolve a y-axis's auto-tick count: the explicit `tickCount` when given,
|
|
19
|
+
* else `floor(height / Y_TICK_PX)` floored at 2 (a drawable minimum even on a
|
|
20
|
+
* pre-layout zero height). `ticks(count)` treats it as a target and returns
|
|
21
|
+
* nice 1-2-5 values near it, so a larger count on a tall row is exactly right.
|
|
22
|
+
*/
|
|
23
|
+
export function resolveYTickCount(height, explicit) {
|
|
24
|
+
if (explicit !== undefined)
|
|
25
|
+
return Math.max(1, Math.floor(explicit));
|
|
26
|
+
return Math.max(2, Math.floor(height / Y_TICK_PX));
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=yticks.js.map
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.49.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.49.0",
|
|
42
|
+
"pond-ts": "^0.49.0",
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|