@pond-ts/charts 0.41.0 → 0.43.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 +157 -6
- package/dist/BarChart.d.ts +139 -54
- package/dist/BarChart.js +301 -104
- package/dist/CategoryAxis.d.ts +16 -0
- package/dist/CategoryAxis.js +19 -0
- package/dist/ChartContainer.d.ts +99 -6
- package/dist/ChartContainer.js +157 -5
- package/dist/Layers.js +119 -12
- package/dist/ScatterChart.d.ts +15 -3
- package/dist/ScatterChart.js +23 -5
- package/dist/XAxis.js +41 -2
- package/dist/annotations.d.ts +38 -1
- package/dist/annotations.js +68 -25
- package/dist/bandScale.d.ts +57 -0
- package/dist/bandScale.js +67 -0
- package/dist/bars.d.ts +113 -5
- package/dist/bars.js +189 -10
- package/dist/context.d.ts +122 -31
- package/dist/data.d.ts +174 -1
- package/dist/data.js +214 -0
- package/dist/grid.d.ts +14 -0
- package/dist/grid.js +36 -0
- package/dist/index.d.ts +8 -2
- package/dist/index.js +12 -1
- package/dist/scatter.d.ts +9 -7
- package/dist/scatter.js +12 -8
- package/dist/theme.d.ts +7 -0
- package/dist/theme.js +1 -0
- package/dist/tracker.d.ts +37 -0
- package/dist/tracker.js +77 -6
- package/dist/tradingTimeScale.d.ts +97 -0
- package/dist/tradingTimeScale.js +152 -0
- package/dist/viewport.d.ts +23 -0
- package/dist/viewport.js +51 -0
- package/package.json +3 -3
package/dist/BarChart.js
CHANGED
|
@@ -1,49 +1,53 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo } from 'react';
|
|
2
2
|
import { ValueSeries } from 'pond-ts';
|
|
3
|
-
import { barsFromTimeSeries, barsFromValueSeries } from './data.js';
|
|
4
|
-
import { barAt, barExtent, barIndexAtTime, drawBars, resolveBarBaseline, } from './bars.js';
|
|
3
|
+
import { barsFromTimeSeries, barsFromValueSeries, categoryStack, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
|
|
4
|
+
import { barAt, barExtent, barIndexAtTime, drawBars, drawStacks, resolveBarBaseline, stackAt, stackBinExtent, stackValueExtent, } from './bars.js';
|
|
5
5
|
import { ContainerContext, LayersContext, } from './context.js';
|
|
6
6
|
import { useSlotKey } from './use-slot-key.js';
|
|
7
7
|
/**
|
|
8
|
-
* A bar draw layer
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
8
|
+
* A bar / histogram draw layer. In its simplest form, one rectangle per event
|
|
9
|
+
* spanning the key's `[begin, end]` from the axis baseline to a numeric
|
|
10
|
+
* `column`'s value (see below). It also draws **stacked** bars (a group-by
|
|
11
|
+
* dimension → segments, `columns` / a `Map` series / `bins`) and **horizontal**
|
|
12
|
+
* bars (`orientation='horizontal'`, bins on the y axis) — first-class histogram
|
|
13
|
+
* support. Registers into the enclosing {@link Layers} and renders nothing to the
|
|
14
|
+
* DOM; the row draws it.
|
|
13
15
|
*
|
|
14
|
-
* **
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
16
|
+
* **Data sources.** A time / value `TimeSeries` or `ValueSeries` (`column`), a
|
|
17
|
+
* wide series or `bins` array (`columns`), or a `Map<group, TimeSeries>`
|
|
18
|
+
* (`column`) — the last three stack. Every shape composes from pond's own
|
|
19
|
+
* aggregation (`aggregate` / `byColumn` / `partitionBy`); the histogram guide
|
|
20
|
+
* has the recipes.
|
|
18
21
|
*
|
|
19
|
-
* **
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* so two series sharing a timestamp don't both light up — draws highlighted
|
|
23
|
-
* (outlined for the committed select, fill-only for the transient hover). Both
|
|
24
|
-
* resolve by **containment**: the tracker by the bar's `[begin, end]` time span
|
|
25
|
-
* (`barIndexAtTime`), the click by the bar's pixel rect (`barAt`) — so the
|
|
26
|
-
* readout reads the same bar you click, even across a wide bucket (they differ
|
|
27
|
-
* only by the `gap` inset, where the pixel rect is narrower than the span).
|
|
22
|
+
* **Baseline (single, vertical).** Bars rest on the zero line when the axis
|
|
23
|
+
* domain spans zero, or on the axis floor when an explicit `<YAxis min>` sits
|
|
24
|
+
* above zero (see {@link resolveBarBaseline}).
|
|
28
25
|
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
26
|
+
* **Baseline (stacked).** A stack is **cumulative from value 0** — the segments
|
|
27
|
+
* sum upward from the zero line, so its value axis **must include 0**. The
|
|
28
|
+
* auto-fit guarantees this: {@link stackValueExtent} always returns `[0, maxTotal]`.
|
|
29
|
+
* An explicit `<YAxis min>` **above** 0 is therefore unsupported for a stack — it
|
|
30
|
+
* would hide the bottom of the cumulative column; only the portion above the floor
|
|
31
|
+
* draws (clipped cleanly at the plot floor, as any bar below an explicit floor is).
|
|
32
|
+
* Segment values are assumed **non-negative** (a negative or zero segment is
|
|
33
|
+
* skipped — diverging stacks are out of scope).
|
|
34
34
|
*
|
|
35
|
-
* **
|
|
36
|
-
* (
|
|
37
|
-
*
|
|
38
|
-
*
|
|
35
|
+
* **Interaction (opt-in via `id`).** Hover lights the bar / segment under the
|
|
36
|
+
* cursor (hit-tested by pixel rect, so it works in both orientations); click
|
|
37
|
+
* selects it (outlined). A stacked segment's identity is `(id, key = bin begin,
|
|
38
|
+
* label = group)`. Both channels are controllable from outside via the container
|
|
39
|
+
* (`selected`/`onSelect`, `hovered`/`onHover`). The in-chart `flag`/`crosshair`
|
|
40
|
+
* value cursor is single-series-vertical only.
|
|
39
41
|
*
|
|
40
42
|
* ```tsx
|
|
41
43
|
* <Layers>
|
|
42
44
|
* <BarChart series={hourlyVolume} column="count" />
|
|
45
|
+
* <BarChart series={byHost} column="n" colors={{ web1: '#…' }} />
|
|
46
|
+
* <BarChart bins={powerDist} column="seconds" orientation="horizontal" ordinal />
|
|
43
47
|
* </Layers>
|
|
44
48
|
* ```
|
|
45
49
|
*/
|
|
46
|
-
export function BarChart({ series, column, as: semantic, axis, gap, index = 0, }) {
|
|
50
|
+
export function BarChart({ series, bins, categories, column, columns, as: semantic, colors, binColors, orientation = 'vertical', ordinal = false, id, axis, gap, index = 0, }) {
|
|
47
51
|
const container = useContext(ContainerContext);
|
|
48
52
|
if (container === null) {
|
|
49
53
|
throw new Error('<BarChart> must be rendered inside a <ChartContainer>');
|
|
@@ -52,98 +56,291 @@ export function BarChart({ series, column, as: semantic, axis, gap, index = 0, }
|
|
|
52
56
|
if (layers === null) {
|
|
53
57
|
throw new Error('<BarChart> must be rendered inside a <Layers>');
|
|
54
58
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
+
// Validate the data-source / value-column combination up front (throws are
|
|
60
|
+
// stable across renders, so no need to memoize them).
|
|
61
|
+
const nSources = (series !== undefined ? 1 : 0) +
|
|
62
|
+
(bins !== undefined ? 1 : 0) +
|
|
63
|
+
(categories !== undefined ? 1 : 0);
|
|
64
|
+
if (nSources !== 1) {
|
|
65
|
+
throw new Error('<BarChart> needs exactly one of `series`, `bins`, or `categories`');
|
|
66
|
+
}
|
|
67
|
+
if (categories !== undefined) {
|
|
68
|
+
if (column !== undefined || columns !== undefined) {
|
|
69
|
+
throw new Error('<BarChart categories> takes no `column`/`columns` (each datum carries its own value)');
|
|
70
|
+
}
|
|
71
|
+
if (orientation === 'horizontal') {
|
|
72
|
+
throw new Error('<BarChart categories> is vertical only (categories on x); horizontal category axes are not yet supported');
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
const isMap = series instanceof Map;
|
|
76
|
+
if (isMap && columns !== undefined) {
|
|
77
|
+
throw new Error('<BarChart> with a `Map` series stacks its groups — use `column` (the shared value column), not `columns`');
|
|
78
|
+
}
|
|
79
|
+
if (column !== undefined && columns !== undefined) {
|
|
80
|
+
throw new Error('<BarChart> takes `column` or `columns`, not both');
|
|
81
|
+
}
|
|
82
|
+
// The single series' semantic label (its identity for the readout + selection):
|
|
83
|
+
// the `as` role, else the value column. Used only on the single path.
|
|
84
|
+
const label = semantic ?? column ?? id ?? 'value';
|
|
85
|
+
// Build the chart-ready data view. Single-series *vertical* stays on the
|
|
86
|
+
// original BarSeries path (its pixels are unchanged); everything else — any
|
|
87
|
+
// stack, any horizontal — builds a StackedBarSeries (G === 1 for a single
|
|
88
|
+
// horizontal bar) so one oriented draw path covers it.
|
|
89
|
+
const shape = useMemo(() => {
|
|
90
|
+
if (categories !== undefined) {
|
|
91
|
+
// Categorical row-read: one unit-slot bar per category (G === 1), drawn on
|
|
92
|
+
// the container's band scale. The reused stacked geometry — only the axis
|
|
93
|
+
// (band scale + labels) is new.
|
|
94
|
+
return { kind: 'stacked', ss: categoryStack(categories) };
|
|
95
|
+
}
|
|
96
|
+
if (bins !== undefined) {
|
|
97
|
+
const cols = columns ?? (column !== undefined ? [column] : undefined);
|
|
98
|
+
if (cols === undefined) {
|
|
99
|
+
throw new Error('<BarChart bins> needs `column` or `columns`');
|
|
100
|
+
}
|
|
101
|
+
return { kind: 'stacked', ss: stacksFromBins(bins, cols, { ordinal }) };
|
|
102
|
+
}
|
|
103
|
+
if (isMap) {
|
|
104
|
+
if (column === undefined) {
|
|
105
|
+
throw new Error('<BarChart> with a `Map` series needs `column`');
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
kind: 'stacked',
|
|
109
|
+
ss: stacksFromGroups(series, column),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
const s = series;
|
|
113
|
+
if (columns !== undefined) {
|
|
114
|
+
return { kind: 'stacked', ss: stacksFromColumns(s, columns) };
|
|
115
|
+
}
|
|
116
|
+
if (column === undefined) {
|
|
117
|
+
throw new Error('<BarChart> needs `column` or `columns`');
|
|
118
|
+
}
|
|
119
|
+
if (orientation === 'horizontal') {
|
|
120
|
+
// Single horizontal bar: route through the stacked path (G === 1), naming
|
|
121
|
+
// the one group with the series' label so selection matches on it.
|
|
122
|
+
const ss = stacksFromColumns(s, [column]);
|
|
123
|
+
return { kind: 'stacked', ss: { ...ss, groups: [label] } };
|
|
124
|
+
}
|
|
125
|
+
return {
|
|
126
|
+
kind: 'single',
|
|
127
|
+
bs: s instanceof ValueSeries
|
|
128
|
+
? barsFromValueSeries(s, column)
|
|
129
|
+
: barsFromTimeSeries(s, column),
|
|
130
|
+
};
|
|
131
|
+
}, [
|
|
132
|
+
series,
|
|
133
|
+
bins,
|
|
134
|
+
categories,
|
|
135
|
+
column,
|
|
136
|
+
columns,
|
|
137
|
+
ordinal,
|
|
138
|
+
orientation,
|
|
139
|
+
isMap,
|
|
140
|
+
label,
|
|
141
|
+
]);
|
|
142
|
+
// The category labels — the ordinal axis's ordered column set (`xCategories`),
|
|
143
|
+
// and the per-bar readout label. `null` unless this is a categorical chart.
|
|
144
|
+
const categoryLabels = useMemo(() => categories?.map((c) => c.label) ?? null, [categories]);
|
|
145
|
+
// The bin axis kind — `'category'` for a categorical chart, else `'time'`/`'value'`
|
|
146
|
+
// (a `TimeSeries`/`Map` bins on time, a `ValueSeries`/`bins`-array on a value
|
|
147
|
+
// axis). For a vertical chart this is the shared x-kind; a horizontal one puts
|
|
148
|
+
// the *value* on x (always 'value') and the bin axis on a linear y.
|
|
149
|
+
const binAxisKind = categories !== undefined
|
|
150
|
+
? 'category'
|
|
151
|
+
: bins !== undefined
|
|
152
|
+
? 'value'
|
|
153
|
+
: isMap
|
|
154
|
+
? 'time'
|
|
155
|
+
: series instanceof ValueSeries
|
|
156
|
+
? 'value'
|
|
157
|
+
: 'time';
|
|
59
158
|
const { bar } = container.theme;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
159
|
+
// Single-series style: the `as` role → theme bar style (the single channel).
|
|
160
|
+
const singleStyle = (semantic !== undefined ? bar[semantic] : undefined) ?? bar.default;
|
|
161
|
+
const gapPx = gap ?? bar.default.gap;
|
|
162
|
+
// The stacked path's bar-thickness floor comes from `bar.default` (not the `as`
|
|
163
|
+
// role — `as` is single-series only), matching how `gapPx` sources its default.
|
|
164
|
+
const stackMinWidth = bar.default.minWidth;
|
|
165
|
+
// Stacked style: per-group fills (colors override → theme role → default),
|
|
166
|
+
// plus the shared opacity / outline from the default bar style. Memoized on the
|
|
167
|
+
// groups + colours so a selection change doesn't rebuild it.
|
|
168
|
+
const groups = shape.kind === 'stacked' ? shape.ss.groups : undefined;
|
|
169
|
+
const stackStyle = useMemo(() => {
|
|
170
|
+
const base = bar.default;
|
|
171
|
+
const fills = (groups ?? []).map((g) => colors?.[g] ?? (bar[g] ?? base).fill);
|
|
172
|
+
return {
|
|
173
|
+
fills,
|
|
174
|
+
opacity: base.opacity,
|
|
175
|
+
outlineWidth: base.outlineWidth,
|
|
176
|
+
...(binColors !== undefined ? { binFills: binColors } : {}),
|
|
177
|
+
};
|
|
178
|
+
}, [bar, groups, colors, binColors]);
|
|
179
|
+
// The current selection / hover, narrowed to the identity the highlight match
|
|
180
|
+
// needs. For a stack that's (id, key, label = group); the single path uses just
|
|
181
|
+
// (id, key). Read here so a change re-registers the layer → the canvas repaints.
|
|
69
182
|
const selected = container.selected;
|
|
70
|
-
const selection = useMemo(() => selected === null ? null : { key: selected.key, label: selected.label }, [selected]);
|
|
71
|
-
// The transient hover-highlight, narrowed to the match key (key + label) like
|
|
72
|
-
// the selection. Read here so a hover change re-registers the layer → the data
|
|
73
|
-
// canvas repaints with the lit bar. Deduped in the container, so this only
|
|
74
|
-
// fires on a bar transition (not every pointer move).
|
|
75
183
|
const hoveredMark = container.hovered;
|
|
184
|
+
const selection = useMemo(() => selected === null
|
|
185
|
+
? null
|
|
186
|
+
: {
|
|
187
|
+
id: selected.id,
|
|
188
|
+
key: selected.key,
|
|
189
|
+
label: selected.label,
|
|
190
|
+
...(selected.mark !== undefined ? { mark: selected.mark } : {}),
|
|
191
|
+
}, [selected]);
|
|
76
192
|
const hover = useMemo(() => hoveredMark === null
|
|
77
193
|
? null
|
|
78
|
-
: {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
color: style.fill,
|
|
113
|
-
label,
|
|
194
|
+
: {
|
|
195
|
+
id: hoveredMark.id,
|
|
196
|
+
key: hoveredMark.key,
|
|
197
|
+
label: hoveredMark.label,
|
|
198
|
+
...(hoveredMark.mark !== undefined
|
|
199
|
+
? { mark: hoveredMark.mark }
|
|
200
|
+
: {}),
|
|
201
|
+
}, [hoveredMark]);
|
|
202
|
+
const entry = useMemo(() => {
|
|
203
|
+
// ── Single-series, vertical: the original bar path, pixels unchanged. ──
|
|
204
|
+
if (shape.kind === 'single') {
|
|
205
|
+
const bs = shape.bs;
|
|
206
|
+
return {
|
|
207
|
+
layer: {
|
|
208
|
+
yExtent: () => barExtent(bs),
|
|
209
|
+
xKind: binAxisKind,
|
|
210
|
+
xExtent: () => bs.length === 0 ? null : [bs.begin[0], bs.end[bs.length - 1]],
|
|
211
|
+
sampleAt: (time) => {
|
|
212
|
+
if (bs.length === 0)
|
|
213
|
+
return [];
|
|
214
|
+
const i = barIndexAtTime(bs, time);
|
|
215
|
+
if (i < 0)
|
|
216
|
+
return [];
|
|
217
|
+
const v = bs.y[i];
|
|
218
|
+
if (!Number.isFinite(v))
|
|
219
|
+
return [];
|
|
220
|
+
return [
|
|
221
|
+
{
|
|
222
|
+
x: (bs.begin[i] + bs.end[i]) / 2,
|
|
223
|
+
value: v,
|
|
224
|
+
color: singleStyle.fill,
|
|
225
|
+
label,
|
|
226
|
+
},
|
|
227
|
+
];
|
|
114
228
|
},
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
229
|
+
...(id === undefined
|
|
230
|
+
? {}
|
|
231
|
+
: {
|
|
232
|
+
hitTest: (px, py, xScale, yScale) => {
|
|
233
|
+
const baseline = resolveBarBaseline(yScale);
|
|
234
|
+
const hit = barAt(bs, px, py, xScale, yScale, baseline, gapPx, singleStyle.minWidth);
|
|
235
|
+
if (hit === null)
|
|
236
|
+
return null;
|
|
237
|
+
const [, begin, value] = hit;
|
|
238
|
+
return {
|
|
239
|
+
id,
|
|
240
|
+
key: begin,
|
|
241
|
+
value,
|
|
242
|
+
color: singleStyle.fill,
|
|
243
|
+
label,
|
|
244
|
+
};
|
|
245
|
+
},
|
|
246
|
+
}),
|
|
247
|
+
draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover),
|
|
248
|
+
},
|
|
249
|
+
axisId: axis,
|
|
250
|
+
index,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// ── Stacked (or single horizontal): the oriented, transposed draw path. ──
|
|
254
|
+
const ss = shape.ss;
|
|
255
|
+
const binExtent = () => stackBinExtent(ss);
|
|
256
|
+
const valueExtent = () => stackValueExtent(ss);
|
|
257
|
+
const vertical = orientation === 'vertical';
|
|
258
|
+
return {
|
|
259
|
+
layer: {
|
|
260
|
+
// Horizontal puts the value on the shared x (always 'value'); vertical
|
|
261
|
+
// keeps the bin axis on x. The bin axis on the *other* side is a linear
|
|
262
|
+
// numeric scale either way (time ms label via <YAxis ticks>).
|
|
263
|
+
xKind: vertical ? binAxisKind : 'value',
|
|
264
|
+
xExtent: vertical ? binExtent : valueExtent,
|
|
265
|
+
yExtent: vertical ? valueExtent : binExtent,
|
|
266
|
+
// A categorical chart hands the container its ordered category names — the
|
|
267
|
+
// ordinal axis domain the shared band scale + label formatter build on.
|
|
268
|
+
...(categoryLabels !== null
|
|
269
|
+
? { xCategories: () => categoryLabels }
|
|
270
|
+
: {}),
|
|
271
|
+
// No x-scrub flag for a stack / horizontal chart — hover + click read it
|
|
272
|
+
// out instead (the flag is single-series-vertical only).
|
|
273
|
+
sampleAt: () => [],
|
|
274
|
+
...(id === undefined
|
|
275
|
+
? {}
|
|
276
|
+
: {
|
|
277
|
+
hitTest: (px, py, xScale, yScale) => {
|
|
278
|
+
const hit = stackAt(ss, px, py, orientation, xScale, yScale, gapPx, stackMinWidth);
|
|
279
|
+
if (hit === null)
|
|
280
|
+
return null;
|
|
281
|
+
const [bi, g, begin, name, value] = hit;
|
|
282
|
+
// A categorical bar carries a stable per-bar `mark` (its column
|
|
283
|
+
// name); the selection keys on `(id, mark)` so it survives a
|
|
284
|
+
// reorder. `bi` is the exact bin index from the hit.
|
|
285
|
+
const stableMark = ss.marks?.[bi];
|
|
286
|
+
return {
|
|
287
|
+
id,
|
|
288
|
+
key: begin,
|
|
289
|
+
value,
|
|
290
|
+
// A per-bin colour override wins over the group fill, so the
|
|
291
|
+
// readout pill reads the bar's own colour.
|
|
292
|
+
color: stackStyle.binFills?.[bi] ?? stackStyle.fills[g],
|
|
293
|
+
// A categorical bar reports its category name; a stack reports
|
|
294
|
+
// the group.
|
|
295
|
+
label: stableMark ?? name,
|
|
296
|
+
...(stableMark !== undefined ? { mark: stableMark } : {}),
|
|
297
|
+
};
|
|
298
|
+
},
|
|
299
|
+
}),
|
|
300
|
+
draw: (ctx, xScale, yScale) => drawStacks(ctx, ss, orientation, xScale, yScale, stackStyle, gapPx, stackMinWidth, id, selection, hover),
|
|
127
301
|
},
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
302
|
+
axisId: axis,
|
|
303
|
+
index,
|
|
304
|
+
};
|
|
305
|
+
}, [
|
|
306
|
+
shape,
|
|
307
|
+
binAxisKind,
|
|
308
|
+
categoryLabels,
|
|
309
|
+
orientation,
|
|
310
|
+
singleStyle,
|
|
311
|
+
stackStyle,
|
|
312
|
+
label,
|
|
313
|
+
id,
|
|
314
|
+
gapPx,
|
|
315
|
+
stackMinWidth,
|
|
316
|
+
selection,
|
|
317
|
+
hover,
|
|
318
|
+
axis,
|
|
131
319
|
index,
|
|
132
|
-
|
|
133
|
-
// A stable per-instance slot
|
|
134
|
-
//
|
|
320
|
+
]);
|
|
321
|
+
// A stable per-instance slot keeps this layer's z-position fixed across data /
|
|
322
|
+
// style / selection updates (see useSlotKey).
|
|
135
323
|
const slot = useSlotKey();
|
|
136
324
|
useEffect(() => () => layers.unregisterLayer(slot), [layers, slot]);
|
|
137
325
|
useEffect(() => {
|
|
138
326
|
layers.registerLayer(slot, entry);
|
|
139
327
|
}, [layers, slot, entry]);
|
|
140
|
-
// Also a tracker source: the container fans in this
|
|
141
|
-
//
|
|
328
|
+
// Also a tracker source: the container fans in this layer's value at the cursor
|
|
329
|
+
// for the (outside-the-chart) readout. A stacked / horizontal layer's sampleAt
|
|
330
|
+
// returns nothing, so it contributes no flag but still registers cleanly.
|
|
142
331
|
const { registerTrackerSource, unregisterTrackerSource } = container;
|
|
143
332
|
useEffect(() => () => unregisterTrackerSource(slot), [unregisterTrackerSource, slot]);
|
|
144
333
|
useEffect(() => {
|
|
145
334
|
registerTrackerSource(slot, entry.layer);
|
|
146
335
|
}, [registerTrackerSource, slot, entry.layer]);
|
|
336
|
+
// Advertise selectability (only when an `id` was given).
|
|
337
|
+
const { registerSelectable, unregisterSelectable } = container;
|
|
338
|
+
useEffect(() => {
|
|
339
|
+
if (id === undefined)
|
|
340
|
+
return;
|
|
341
|
+
registerSelectable(slot);
|
|
342
|
+
return () => unregisterSelectable(slot);
|
|
343
|
+
}, [registerSelectable, unregisterSelectable, slot, id]);
|
|
147
344
|
return null;
|
|
148
345
|
}
|
|
149
346
|
//# sourceMappingURL=BarChart.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type XAxisProps } from './XAxis.js';
|
|
2
|
+
/**
|
|
3
|
+
* The category-flavoured preset of {@link XAxis} — `<CategoryAxis />` is
|
|
4
|
+
* `<XAxis />`. The axis kind follows the data: on a **category** container (a
|
|
5
|
+
* layer that plots on the ordinal column-domain axis) it ticks once per category,
|
|
6
|
+
* labelling each band centre with the category name (the container's shared
|
|
7
|
+
* formatter). Kept as the familiar name for categorical charts, mirroring
|
|
8
|
+
* {@link TimeAxis}. Forwards every {@link XAxisProps} (`label`, `side`, …).
|
|
9
|
+
*
|
|
10
|
+
* A high-cardinality axis (many categories) thins + truncates its labels to stay
|
|
11
|
+
* legible (categorical-axis RFC, Phase 1). The labels **come from the data** (the
|
|
12
|
+
* `categories` list), so a d3 `format` prop does not apply here (it can't name a
|
|
13
|
+
* category); customize a label by changing the `categories` datum's `label`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function CategoryAxis(props?: XAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=CategoryAxis.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { XAxis } from './XAxis.js';
|
|
3
|
+
/**
|
|
4
|
+
* The category-flavoured preset of {@link XAxis} — `<CategoryAxis />` is
|
|
5
|
+
* `<XAxis />`. The axis kind follows the data: on a **category** container (a
|
|
6
|
+
* layer that plots on the ordinal column-domain axis) it ticks once per category,
|
|
7
|
+
* labelling each band centre with the category name (the container's shared
|
|
8
|
+
* formatter). Kept as the familiar name for categorical charts, mirroring
|
|
9
|
+
* {@link TimeAxis}. Forwards every {@link XAxisProps} (`label`, `side`, …).
|
|
10
|
+
*
|
|
11
|
+
* A high-cardinality axis (many categories) thins + truncates its labels to stay
|
|
12
|
+
* legible (categorical-axis RFC, Phase 1). The labels **come from the data** (the
|
|
13
|
+
* `categories` list), so a d3 `format` prop does not apply here (it can't name a
|
|
14
|
+
* category); customize a label by changing the `categories` datum's `label`.
|
|
15
|
+
*/
|
|
16
|
+
export function CategoryAxis(props = {}) {
|
|
17
|
+
return _jsx(XAxis, { ...props });
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=CategoryAxis.js.map
|
package/dist/ChartContainer.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
|
+
import { type DiscontinuityProvider, type TradingCalendarLike } from './tradingTimeScale.js';
|
|
3
|
+
import { Sequence, BoundedSequence } from 'pond-ts';
|
|
2
4
|
import type { TimeRange } from 'pond-ts';
|
|
3
5
|
import { type AnnotationKind, type CreateSpec, type CursorMode, type SelectInfo, type TrackerInfo } from './context.js';
|
|
4
6
|
import { type AxisFormat } from './format.js';
|
|
@@ -12,6 +14,45 @@ export interface ChartContainerProps {
|
|
|
12
14
|
* the data — so a tuple stays a time domain on a time chart.
|
|
13
15
|
*/
|
|
14
16
|
range?: readonly [number, number] | TimeRange;
|
|
17
|
+
/**
|
|
18
|
+
* A **trading-calendar** discontinuity provider — closed-market time
|
|
19
|
+
* (weekends, holidays, overnight, lunch breaks) collapsed. Supply it to turn
|
|
20
|
+
* the shared x axis into a **trading-time** axis: gaps disappear and time
|
|
21
|
+
* stays proportional within each session. A `@pond-ts/financial`
|
|
22
|
+
* `TradingCalendar.discontinuities()` satisfies this structurally (charts
|
|
23
|
+
* never imports that package). The **low-level** primitive: pass
|
|
24
|
+
* `calendar.discontinuities()` (or a `{ spacing, period }` variant) directly.
|
|
25
|
+
* Only affects a **time** axis (ignored on a value axis). Takes precedence
|
|
26
|
+
* over {@link calendar} if both are given.
|
|
27
|
+
*
|
|
28
|
+
* **Pass a stable reference.** The scale (and container frame) rebuild when
|
|
29
|
+
* this prop's identity changes, so memoize it — `const disc = useMemo(() =>
|
|
30
|
+
* calendar.discontinuities(), [calendar])` — rather than calling
|
|
31
|
+
* `.discontinuities()` inline in JSX, which would rebuild every render.
|
|
32
|
+
*/
|
|
33
|
+
discontinuities?: DiscontinuityProvider;
|
|
34
|
+
/**
|
|
35
|
+
* The **high-level** sugar for {@link discontinuities}: a trading calendar the
|
|
36
|
+
* container derives the provider from itself (`calendar.discontinuities({
|
|
37
|
+
* spacing })`), so you don't wire the low-level prop. A `@pond-ts/financial`
|
|
38
|
+
* `TradingCalendar` satisfies the structural {@link TradingCalendarLike} shape
|
|
39
|
+
* (charts never imports that package). Combine with {@link spacing}. For the
|
|
40
|
+
* full option matrix (a bar `period`, a scoped `range`) use the low-level
|
|
41
|
+
* `discontinuities` prop instead. Only affects a **time** axis.
|
|
42
|
+
*
|
|
43
|
+
* The provider is memoized on `(calendar, spacing)`, so pass a **stable**
|
|
44
|
+
* calendar reference (build it once, not inline in JSX).
|
|
45
|
+
*/
|
|
46
|
+
calendar?: TradingCalendarLike;
|
|
47
|
+
/**
|
|
48
|
+
* The trading axis **metric**, when a {@link calendar} is supplied
|
|
49
|
+
* (trading-calendar RFC Q7). `'proportional'` (default) keeps time
|
|
50
|
+
* proportional within and across sessions — a half-day is half as wide.
|
|
51
|
+
* `'uniform'` gives every session equal width (the TradingView ordinal look).
|
|
52
|
+
* Ignored without `calendar` (a low-level `discontinuities` provider already
|
|
53
|
+
* carries its own metric).
|
|
54
|
+
*/
|
|
55
|
+
spacing?: 'proportional' | 'uniform';
|
|
15
56
|
/** Total width in CSS pixels (plot + axis gutters). */
|
|
16
57
|
width: number;
|
|
17
58
|
/** Vertical space between rows in CSS pixels (not under the axis). Default 0. */
|
|
@@ -35,9 +76,55 @@ export interface ChartContainerProps {
|
|
|
35
76
|
* via `<ChartRow cursor>`). **Default `'line'`** — the synced vertical line,
|
|
36
77
|
* with values surfaced *outside* the chart via {@link onTrackerChanged}.
|
|
37
78
|
* `'point'` / `'inline'` / `'flag'` add per-series marks; `'none'` hides it.
|
|
79
|
+
* `'region'` shades the bucket under the pointer (needs {@link cursorSequence}).
|
|
38
80
|
* See {@link CursorMode}.
|
|
39
81
|
*/
|
|
40
82
|
cursor?: CursorMode;
|
|
83
|
+
/**
|
|
84
|
+
* The bucketing for `cursor="region"` — the interval highlighted under the
|
|
85
|
+
* pointer. A pond {@link Sequence} (duration or calendar-aware —
|
|
86
|
+
* `Sequence.every('1d')`, `Sequence.calendar('month')`) is realized over the
|
|
87
|
+
* current view; a {@link BoundedSequence} (e.g. a `TradingCalendar`'s
|
|
88
|
+
* `sessionSequence()` / `barSequence()`) is used as-is, so the band can track
|
|
89
|
+
* whole **sessions**. Either way the band maps through `xScale`, so on a
|
|
90
|
+
* trading-time axis the closed part of the bucket collapses. Ignored unless
|
|
91
|
+
* `cursor="region"`.
|
|
92
|
+
*
|
|
93
|
+
* **Time axis only.** A bucket is a *time* interval, so the region cursor is
|
|
94
|
+
* gated to a **time** x-axis — on a **value** axis (a horizontal histogram, a
|
|
95
|
+
* value-keyed chart) it's a no-op (highlighting a value *band* on a horizontal
|
|
96
|
+
* histogram would be a different, y-oriented cursor).
|
|
97
|
+
*
|
|
98
|
+
* **Pass a stable reference.** The buckets are memoized on this value + the
|
|
99
|
+
* view range; a `Sequence`/`BoundedSequence` rebuilt inline every render
|
|
100
|
+
* re-realizes the buckets on each pointer move (harmless for a coarse
|
|
101
|
+
* day/session sequence, wasteful for a fine one over a wide view) — hoist it or
|
|
102
|
+
* `useMemo` it.
|
|
103
|
+
*/
|
|
104
|
+
cursorSequence?: Sequence | BoundedSequence;
|
|
105
|
+
/**
|
|
106
|
+
* Makes the `region` cursor **draggable**: drag across the plot and the band
|
|
107
|
+
* extends **bucket by bucket** (snapping to `cursorSequence` points); on
|
|
108
|
+
* release this fires **once** with the selected `[start, end)` `TimeRange`, and
|
|
109
|
+
* the cursor reverts to the single-bucket highlight (it does not keep the
|
|
110
|
+
* range). Typical use — zoom the view to the returned range (the container
|
|
111
|
+
* doesn't zoom itself; that's the consumer's call).
|
|
112
|
+
*
|
|
113
|
+
* With **no `cursorSequence`** the region cursor is the degenerate case — it
|
|
114
|
+
* renders as a **line** on hover and the drag is **freeform** (raw `[start,
|
|
115
|
+
* end)`, no bucket snapping); the same callback fires on release. No-op unless
|
|
116
|
+
* `cursor="region"` (and a **time** x-axis).
|
|
117
|
+
*/
|
|
118
|
+
onRegionSelect?: (range: TimeRange) => void;
|
|
119
|
+
/**
|
|
120
|
+
* Which modifier a region-drag needs — set `'shift'` when you also enable
|
|
121
|
+
* `panZoom` and want **plain drag to pan, shift-drag to select**. It's only
|
|
122
|
+
* enforced while `panZoom` is on (with pan off there's no gesture conflict, so
|
|
123
|
+
* shift is optional — either drag selects). **Omitted** ⇒ a region-drag
|
|
124
|
+
* **preempts** pan (drag always selects; document that precedence for users).
|
|
125
|
+
* Wheel-zoom is unaffected in every case.
|
|
126
|
+
*/
|
|
127
|
+
regionSelectModifier?: 'shift';
|
|
41
128
|
/**
|
|
42
129
|
* Fires on pointer move with the hovered time + every series' value there (so
|
|
43
130
|
* you can render a readout outside the chart), and `null` on leave.
|
|
@@ -46,15 +133,21 @@ export interface ChartContainerProps {
|
|
|
46
133
|
/**
|
|
47
134
|
* Controlled selection — the selected mark (echo the `onSelect` arg back), or
|
|
48
135
|
* `null`. **Omitted ⇒ uncontrolled** (a click on a selectable layer manages it
|
|
49
|
-
* internally; pass `null` to force nothing selected).
|
|
50
|
-
*
|
|
51
|
-
*
|
|
136
|
+
* internally; pass `null` to force nothing selected). A layer is **selectable
|
|
137
|
+
* only when it carries an `id`** (the stable series identity) — `BarChart` /
|
|
138
|
+
* `ScatterChart` highlight the mark matching the selection's `id` (the series)
|
|
139
|
+
* and its `key` (the sample), so two series sharing a timestamp don't both
|
|
140
|
+
* light up, and the selection survives a data update (it keys on the stable
|
|
141
|
+
* `id`, not the sample `key`). A layer with no `id` renders + reads out but
|
|
142
|
+
* can't be selected.
|
|
52
143
|
*/
|
|
53
144
|
selected?: SelectInfo | null;
|
|
54
145
|
/**
|
|
55
146
|
* Fires when a selectable layer's mark is clicked, with the hit mark, or `null`
|
|
56
|
-
* when a click misses every mark (
|
|
57
|
-
* works in both controlled and
|
|
147
|
+
* when a click misses every mark (or hits a layer with no `id` — display-only,
|
|
148
|
+
* so it reads as empty space). Notification only — works in both controlled and
|
|
149
|
+
* uncontrolled mode. If this or `selected` is set but no layer has an `id`, a
|
|
150
|
+
* dev-warning notes that nothing is selectable.
|
|
58
151
|
*/
|
|
59
152
|
onSelect?: (hit: SelectInfo | null) => void;
|
|
60
153
|
/**
|
|
@@ -180,5 +273,5 @@ export interface ChartContainerProps {
|
|
|
180
273
|
* {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
|
|
181
274
|
* (`<YAxis>`).
|
|
182
275
|
*/
|
|
183
|
-
export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, theme, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
276
|
+
export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, theme, discontinuities, calendar, spacing, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
184
277
|
//# sourceMappingURL=ChartContainer.d.ts.map
|