@pond-ts/charts 0.40.0 → 0.42.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 +103 -6
- package/dist/BarChart.d.ts +117 -54
- package/dist/BarChart.js +241 -104
- package/dist/Candlestick.d.ts +93 -0
- package/dist/Candlestick.js +102 -0
- package/dist/ChartContainer.d.ts +52 -6
- package/dist/ChartContainer.js +81 -4
- package/dist/Layers.js +47 -8
- package/dist/ScatterChart.d.ts +15 -3
- package/dist/ScatterChart.js +23 -5
- package/dist/bars.d.ts +96 -5
- package/dist/bars.js +157 -10
- package/dist/context.d.ts +54 -19
- package/dist/data.d.ts +159 -1
- package/dist/data.js +208 -25
- package/dist/grid.d.ts +14 -0
- package/dist/grid.js +36 -0
- package/dist/index.d.ts +9 -3
- package/dist/index.js +6 -1
- package/dist/ohlc.d.ts +81 -0
- package/dist/ohlc.js +153 -0
- package/dist/scatter.d.ts +9 -7
- package/dist/scatter.js +12 -8
- package/dist/theme.d.ts +59 -0
- package/dist/theme.js +25 -0
- 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, 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, column, columns, as: semantic, colors, 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,231 @@ 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
|
+
if ((series === undefined) === (bins === undefined)) {
|
|
62
|
+
throw new Error('<BarChart> needs exactly one of `series` or `bins`');
|
|
63
|
+
}
|
|
64
|
+
const isMap = series instanceof Map;
|
|
65
|
+
if (isMap && columns !== undefined) {
|
|
66
|
+
throw new Error('<BarChart> with a `Map` series stacks its groups — use `column` (the shared value column), not `columns`');
|
|
67
|
+
}
|
|
68
|
+
if (column !== undefined && columns !== undefined) {
|
|
69
|
+
throw new Error('<BarChart> takes `column` or `columns`, not both');
|
|
70
|
+
}
|
|
71
|
+
// The single series' semantic label (its identity for the readout + selection):
|
|
72
|
+
// the `as` role, else the value column. Used only on the single path.
|
|
73
|
+
const label = semantic ?? column ?? id ?? 'value';
|
|
74
|
+
// Build the chart-ready data view. Single-series *vertical* stays on the
|
|
75
|
+
// original BarSeries path (its pixels are unchanged); everything else — any
|
|
76
|
+
// stack, any horizontal — builds a StackedBarSeries (G === 1 for a single
|
|
77
|
+
// horizontal bar) so one oriented draw path covers it.
|
|
78
|
+
const shape = useMemo(() => {
|
|
79
|
+
if (bins !== undefined) {
|
|
80
|
+
const cols = columns ?? (column !== undefined ? [column] : undefined);
|
|
81
|
+
if (cols === undefined) {
|
|
82
|
+
throw new Error('<BarChart bins> needs `column` or `columns`');
|
|
83
|
+
}
|
|
84
|
+
return { kind: 'stacked', ss: stacksFromBins(bins, cols, { ordinal }) };
|
|
85
|
+
}
|
|
86
|
+
if (isMap) {
|
|
87
|
+
if (column === undefined) {
|
|
88
|
+
throw new Error('<BarChart> with a `Map` series needs `column`');
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
kind: 'stacked',
|
|
92
|
+
ss: stacksFromGroups(series, column),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const s = series;
|
|
96
|
+
if (columns !== undefined) {
|
|
97
|
+
return { kind: 'stacked', ss: stacksFromColumns(s, columns) };
|
|
98
|
+
}
|
|
99
|
+
if (column === undefined) {
|
|
100
|
+
throw new Error('<BarChart> needs `column` or `columns`');
|
|
101
|
+
}
|
|
102
|
+
if (orientation === 'horizontal') {
|
|
103
|
+
// Single horizontal bar: route through the stacked path (G === 1), naming
|
|
104
|
+
// the one group with the series' label so selection matches on it.
|
|
105
|
+
const ss = stacksFromColumns(s, [column]);
|
|
106
|
+
return { kind: 'stacked', ss: { ...ss, groups: [label] } };
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
kind: 'single',
|
|
110
|
+
bs: s instanceof ValueSeries
|
|
111
|
+
? barsFromValueSeries(s, column)
|
|
112
|
+
: barsFromTimeSeries(s, column),
|
|
113
|
+
};
|
|
114
|
+
}, [series, bins, column, columns, ordinal, orientation, isMap, label]);
|
|
115
|
+
// The bin axis kind (time vs value) — a `TimeSeries`/`Map` bins on time, a
|
|
116
|
+
// `ValueSeries`/`bins`-array on a value axis. For a vertical histogram this is
|
|
117
|
+
// the shared x-kind; a horizontal one puts the *value* on x (always 'value')
|
|
118
|
+
// and the bin axis on a linear y.
|
|
119
|
+
const binAxisKind = bins !== undefined
|
|
120
|
+
? 'value'
|
|
121
|
+
: isMap
|
|
122
|
+
? 'time'
|
|
123
|
+
: series instanceof ValueSeries
|
|
124
|
+
? 'value'
|
|
125
|
+
: 'time';
|
|
59
126
|
const { bar } = container.theme;
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
//
|
|
65
|
-
const
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
//
|
|
127
|
+
// Single-series style: the `as` role → theme bar style (the single channel).
|
|
128
|
+
const singleStyle = (semantic !== undefined ? bar[semantic] : undefined) ?? bar.default;
|
|
129
|
+
const gapPx = gap ?? bar.default.gap;
|
|
130
|
+
// The stacked path's bar-thickness floor comes from `bar.default` (not the `as`
|
|
131
|
+
// role — `as` is single-series only), matching how `gapPx` sources its default.
|
|
132
|
+
const stackMinWidth = bar.default.minWidth;
|
|
133
|
+
// Stacked style: per-group fills (colors override → theme role → default),
|
|
134
|
+
// plus the shared opacity / outline from the default bar style. Memoized on the
|
|
135
|
+
// groups + colours so a selection change doesn't rebuild it.
|
|
136
|
+
const groups = shape.kind === 'stacked' ? shape.ss.groups : undefined;
|
|
137
|
+
const stackStyle = useMemo(() => {
|
|
138
|
+
const base = bar.default;
|
|
139
|
+
const fills = (groups ?? []).map((g) => colors?.[g] ?? (bar[g] ?? base).fill);
|
|
140
|
+
return { fills, opacity: base.opacity, outlineWidth: base.outlineWidth };
|
|
141
|
+
}, [bar, groups, colors]);
|
|
142
|
+
// The current selection / hover, narrowed to the identity the highlight match
|
|
143
|
+
// needs. For a stack that's (id, key, label = group); the single path uses just
|
|
144
|
+
// (id, key). Read here so a change re-registers the layer → the canvas repaints.
|
|
69
145
|
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
146
|
const hoveredMark = container.hovered;
|
|
147
|
+
const selection = useMemo(() => selected === null
|
|
148
|
+
? null
|
|
149
|
+
: { id: selected.id, key: selected.key, label: selected.label }, [selected]);
|
|
76
150
|
const hover = useMemo(() => hoveredMark === null
|
|
77
151
|
? 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
|
-
x: (bs.begin[i] + bs.end[i]) / 2,
|
|
111
|
-
value: v,
|
|
112
|
-
color: style.fill,
|
|
113
|
-
label,
|
|
152
|
+
: {
|
|
153
|
+
id: hoveredMark.id,
|
|
154
|
+
key: hoveredMark.key,
|
|
155
|
+
label: hoveredMark.label,
|
|
156
|
+
}, [hoveredMark]);
|
|
157
|
+
const entry = useMemo(() => {
|
|
158
|
+
// ── Single-series, vertical: the original bar path, pixels unchanged. ──
|
|
159
|
+
if (shape.kind === 'single') {
|
|
160
|
+
const bs = shape.bs;
|
|
161
|
+
return {
|
|
162
|
+
layer: {
|
|
163
|
+
yExtent: () => barExtent(bs),
|
|
164
|
+
xKind: binAxisKind,
|
|
165
|
+
xExtent: () => bs.length === 0 ? null : [bs.begin[0], bs.end[bs.length - 1]],
|
|
166
|
+
sampleAt: (time) => {
|
|
167
|
+
if (bs.length === 0)
|
|
168
|
+
return [];
|
|
169
|
+
const i = barIndexAtTime(bs, time);
|
|
170
|
+
if (i < 0)
|
|
171
|
+
return [];
|
|
172
|
+
const v = bs.y[i];
|
|
173
|
+
if (!Number.isFinite(v))
|
|
174
|
+
return [];
|
|
175
|
+
return [
|
|
176
|
+
{
|
|
177
|
+
x: (bs.begin[i] + bs.end[i]) / 2,
|
|
178
|
+
value: v,
|
|
179
|
+
color: singleStyle.fill,
|
|
180
|
+
label,
|
|
181
|
+
},
|
|
182
|
+
];
|
|
114
183
|
},
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
184
|
+
...(id === undefined
|
|
185
|
+
? {}
|
|
186
|
+
: {
|
|
187
|
+
hitTest: (px, py, xScale, yScale) => {
|
|
188
|
+
const baseline = resolveBarBaseline(yScale);
|
|
189
|
+
const hit = barAt(bs, px, py, xScale, yScale, baseline, gapPx, singleStyle.minWidth);
|
|
190
|
+
if (hit === null)
|
|
191
|
+
return null;
|
|
192
|
+
const [, begin, value] = hit;
|
|
193
|
+
return {
|
|
194
|
+
id,
|
|
195
|
+
key: begin,
|
|
196
|
+
value,
|
|
197
|
+
color: singleStyle.fill,
|
|
198
|
+
label,
|
|
199
|
+
};
|
|
200
|
+
},
|
|
201
|
+
}),
|
|
202
|
+
draw: (ctx, xScale, yScale) => drawBars(ctx, bs, xScale, yScale, singleStyle, resolveBarBaseline(yScale), gapPx, id, selection, hover),
|
|
203
|
+
},
|
|
204
|
+
axisId: axis,
|
|
205
|
+
index,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
// ── Stacked (or single horizontal): the oriented, transposed draw path. ──
|
|
209
|
+
const ss = shape.ss;
|
|
210
|
+
const binExtent = () => stackBinExtent(ss);
|
|
211
|
+
const valueExtent = () => stackValueExtent(ss);
|
|
212
|
+
const vertical = orientation === 'vertical';
|
|
213
|
+
return {
|
|
214
|
+
layer: {
|
|
215
|
+
// Horizontal puts the value on the shared x (always 'value'); vertical
|
|
216
|
+
// keeps the bin axis on x. The bin axis on the *other* side is a linear
|
|
217
|
+
// numeric scale either way (time ms label via <YAxis ticks>).
|
|
218
|
+
xKind: vertical ? binAxisKind : 'value',
|
|
219
|
+
xExtent: vertical ? binExtent : valueExtent,
|
|
220
|
+
yExtent: vertical ? valueExtent : binExtent,
|
|
221
|
+
// No x-scrub flag for a stack / horizontal chart — hover + click read it
|
|
222
|
+
// out instead (the flag is single-series-vertical only).
|
|
223
|
+
sampleAt: () => [],
|
|
224
|
+
...(id === undefined
|
|
225
|
+
? {}
|
|
226
|
+
: {
|
|
227
|
+
hitTest: (px, py, xScale, yScale) => {
|
|
228
|
+
const hit = stackAt(ss, px, py, orientation, xScale, yScale, gapPx, stackMinWidth);
|
|
229
|
+
if (hit === null)
|
|
230
|
+
return null;
|
|
231
|
+
const [, g, begin, name, value] = hit;
|
|
232
|
+
return {
|
|
233
|
+
id,
|
|
234
|
+
key: begin,
|
|
235
|
+
value,
|
|
236
|
+
color: stackStyle.fills[g],
|
|
237
|
+
label: name,
|
|
238
|
+
};
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
draw: (ctx, xScale, yScale) => drawStacks(ctx, ss, orientation, xScale, yScale, stackStyle, gapPx, stackMinWidth, id, selection, hover),
|
|
127
242
|
},
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
243
|
+
axisId: axis,
|
|
244
|
+
index,
|
|
245
|
+
};
|
|
246
|
+
}, [
|
|
247
|
+
shape,
|
|
248
|
+
binAxisKind,
|
|
249
|
+
orientation,
|
|
250
|
+
singleStyle,
|
|
251
|
+
stackStyle,
|
|
252
|
+
label,
|
|
253
|
+
id,
|
|
254
|
+
gapPx,
|
|
255
|
+
stackMinWidth,
|
|
256
|
+
selection,
|
|
257
|
+
hover,
|
|
258
|
+
axis,
|
|
131
259
|
index,
|
|
132
|
-
|
|
133
|
-
// A stable per-instance slot
|
|
134
|
-
//
|
|
260
|
+
]);
|
|
261
|
+
// A stable per-instance slot keeps this layer's z-position fixed across data /
|
|
262
|
+
// style / selection updates (see useSlotKey).
|
|
135
263
|
const slot = useSlotKey();
|
|
136
264
|
useEffect(() => () => layers.unregisterLayer(slot), [layers, slot]);
|
|
137
265
|
useEffect(() => {
|
|
138
266
|
layers.registerLayer(slot, entry);
|
|
139
267
|
}, [layers, slot, entry]);
|
|
140
|
-
// Also a tracker source: the container fans in this
|
|
141
|
-
//
|
|
268
|
+
// Also a tracker source: the container fans in this layer's value at the cursor
|
|
269
|
+
// for the (outside-the-chart) readout. A stacked / horizontal layer's sampleAt
|
|
270
|
+
// returns nothing, so it contributes no flag but still registers cleanly.
|
|
142
271
|
const { registerTrackerSource, unregisterTrackerSource } = container;
|
|
143
272
|
useEffect(() => () => unregisterTrackerSource(slot), [unregisterTrackerSource, slot]);
|
|
144
273
|
useEffect(() => {
|
|
145
274
|
registerTrackerSource(slot, entry.layer);
|
|
146
275
|
}, [registerTrackerSource, slot, entry.layer]);
|
|
276
|
+
// Advertise selectability (only when an `id` was given).
|
|
277
|
+
const { registerSelectable, unregisterSelectable } = container;
|
|
278
|
+
useEffect(() => {
|
|
279
|
+
if (id === undefined)
|
|
280
|
+
return;
|
|
281
|
+
registerSelectable(slot);
|
|
282
|
+
return () => unregisterSelectable(slot);
|
|
283
|
+
}, [registerSelectable, unregisterSelectable, slot, id]);
|
|
147
284
|
return null;
|
|
148
285
|
}
|
|
149
286
|
//# sourceMappingURL=BarChart.js.map
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import type { SeriesSchema, TimeSeries } from 'pond-ts';
|
|
2
|
+
import { type CandleVariant, type ColorBy } from './ohlc.js';
|
|
3
|
+
export interface CandlestickProps<S extends SeriesSchema> {
|
|
4
|
+
/**
|
|
5
|
+
* The source series. **Point-keyed** (`time`) raw OHLCV feeds straight in —
|
|
6
|
+
* each candle's slot is derived from neighbour spacing (see
|
|
7
|
+
* {@link ohlcFromTimeSeries}), no `aggregate` pass needed. An **interval /
|
|
8
|
+
* timeRange**-keyed series (an `aggregate` rollup — weekly / monthly bars) uses
|
|
9
|
+
* the key's own `[begin, end)` as the slot. The chart infers the x-kind from
|
|
10
|
+
* the data; there's no axis-type prop.
|
|
11
|
+
*/
|
|
12
|
+
series: TimeSeries<S>;
|
|
13
|
+
/** Opening-price column. **Omitted ⇒ `'open'`.** */
|
|
14
|
+
open?: string;
|
|
15
|
+
/** Session-high column. **Omitted ⇒ `'high'`.** */
|
|
16
|
+
high?: string;
|
|
17
|
+
/** Session-low column. **Omitted ⇒ `'low'`.** */
|
|
18
|
+
low?: string;
|
|
19
|
+
/** Closing-price column. **Omitted ⇒ `'close'`.** */
|
|
20
|
+
close?: string;
|
|
21
|
+
/**
|
|
22
|
+
* The series' semantic identifier — what the data _is_ (e.g. a ticker). The
|
|
23
|
+
* theme maps it to a {@link CandleStyle} (`theme.candle[as] ??
|
|
24
|
+
* theme.candle.default`). **Omitted ⇒ the `default` candle style.** It's also
|
|
25
|
+
* the tracker/readout label for the series (the primary `close` pill keys on
|
|
26
|
+
* `as`, not the raw column name).
|
|
27
|
+
*/
|
|
28
|
+
as?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Which `<YAxis>` (by its `id`) this candle scales against — the *scale*, where
|
|
31
|
+
* `as` picks the *style*. **Omitted ⇒ the row's default axis.**
|
|
32
|
+
*/
|
|
33
|
+
axis?: string;
|
|
34
|
+
/**
|
|
35
|
+
* How each mark renders — `'candle'` (default; filled body + wick), `'bar'`
|
|
36
|
+
* (OHLC tick bar), or `'hollow'` (rising hollow / falling filled). See
|
|
37
|
+
* {@link CandleVariant}.
|
|
38
|
+
*/
|
|
39
|
+
variant?: CandleVariant;
|
|
40
|
+
/**
|
|
41
|
+
* What drives the colour — `'direction'` (default; rising / falling / doji off
|
|
42
|
+
* open vs close, the market convention) or `'series'` (one colour off the `as`
|
|
43
|
+
* role, no green/red). See {@link ColorBy}.
|
|
44
|
+
*/
|
|
45
|
+
colorBy?: ColorBy;
|
|
46
|
+
/**
|
|
47
|
+
* Total horizontal inset between adjacent candles in px (half each side), so
|
|
48
|
+
* they breathe — see `barSpanPx`. **Omitted ⇒ `0`** (the body already insets to
|
|
49
|
+
* `style.bodyWidth` of the slot). A candle narrower than 1px after the inset
|
|
50
|
+
* collapses to a 1px mark, so a thin slot stays visible.
|
|
51
|
+
*/
|
|
52
|
+
gap?: number;
|
|
53
|
+
/**
|
|
54
|
+
* Fan the **full O/H/L/C** to the tracker readout (four value pills) instead of
|
|
55
|
+
* the default single `close` pill. **Omitted ⇒ `false`** — close is "the price"
|
|
56
|
+
* for a compact legend; the full quote is opt-in for a dense hover readout.
|
|
57
|
+
*/
|
|
58
|
+
showOHLC?: boolean;
|
|
59
|
+
/**
|
|
60
|
+
* @internal Declaration position among the `<Layers>` children, injected by
|
|
61
|
+
* `Layers` so z-order follows JSX order. Do not set.
|
|
62
|
+
*/
|
|
63
|
+
index?: number;
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A first-class OHLC **candlestick** draw layer — the financial sibling of
|
|
67
|
+
* {@link BoxPlot}. Reads four price columns (`open`/`high`/`low`/`close`) of
|
|
68
|
+
* `series` into an {@link OhlcSeries} and draws one candle per key: the
|
|
69
|
+
* `open→close` body (direction-coloured) and the `high–low` wick, over the key's
|
|
70
|
+
* slot x-span. Derives the body extents itself (`min`/`max` of open/close) — the
|
|
71
|
+
* consumer never runs a `withColumn` precompute. Registers into the enclosing
|
|
72
|
+
* {@link Layers}; renders nothing to the DOM — the row draws it. Gap-aware (a key
|
|
73
|
+
* missing any price draws nothing).
|
|
74
|
+
*
|
|
75
|
+
* **Draws only** — windowing stays upstream: raw daily OHLCV is a point-keyed
|
|
76
|
+
* `TimeSeries` fed straight in, and a weekly / monthly bar is the identical call
|
|
77
|
+
* on an `aggregate(Sequence.calendar('week'), …)` rollup (interval-keyed). This
|
|
78
|
+
* supersedes `BoxPlot shape='solid'` for OHLC (which needed a quantile remap, a
|
|
79
|
+
* body precompute, two overlaid layers for green/red, and a column-name tracker).
|
|
80
|
+
*
|
|
81
|
+
* **Cursor.** Unlike `BoxPlot`, a candle **participates in the crosshair x-snap**
|
|
82
|
+
* (it exposes plain `sampleAt`, not a consolidated `cursorFlag`), so the reticle
|
|
83
|
+
* lands on candles. The readout keys on `as` and shows `close` by default; pass
|
|
84
|
+
* `showOHLC` for the full four-pill quote.
|
|
85
|
+
*
|
|
86
|
+
* ```tsx
|
|
87
|
+
* <Layers>
|
|
88
|
+
* <Candlestick series={daily} as="AAPL" />
|
|
89
|
+
* </Layers>
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function Candlestick<S extends SeriesSchema>({ series, open, high, low, close, as: semantic, axis, variant, colorBy, gap, showOHLC, index, }: CandlestickProps<S>): null;
|
|
93
|
+
//# sourceMappingURL=Candlestick.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useContext, useEffect, useMemo } from 'react';
|
|
2
|
+
import { ohlcFromTimeSeries } from './data.js';
|
|
3
|
+
import { drawCandles, isFiniteOhlc, ohlcExtent, ohlcIndexAtTime, resolveCandleStyle, } from './ohlc.js';
|
|
4
|
+
import { ContainerContext, LayersContext, } from './context.js';
|
|
5
|
+
import { useSlotKey } from './use-slot-key.js';
|
|
6
|
+
/**
|
|
7
|
+
* A first-class OHLC **candlestick** draw layer — the financial sibling of
|
|
8
|
+
* {@link BoxPlot}. Reads four price columns (`open`/`high`/`low`/`close`) of
|
|
9
|
+
* `series` into an {@link OhlcSeries} and draws one candle per key: the
|
|
10
|
+
* `open→close` body (direction-coloured) and the `high–low` wick, over the key's
|
|
11
|
+
* slot x-span. Derives the body extents itself (`min`/`max` of open/close) — the
|
|
12
|
+
* consumer never runs a `withColumn` precompute. Registers into the enclosing
|
|
13
|
+
* {@link Layers}; renders nothing to the DOM — the row draws it. Gap-aware (a key
|
|
14
|
+
* missing any price draws nothing).
|
|
15
|
+
*
|
|
16
|
+
* **Draws only** — windowing stays upstream: raw daily OHLCV is a point-keyed
|
|
17
|
+
* `TimeSeries` fed straight in, and a weekly / monthly bar is the identical call
|
|
18
|
+
* on an `aggregate(Sequence.calendar('week'), …)` rollup (interval-keyed). This
|
|
19
|
+
* supersedes `BoxPlot shape='solid'` for OHLC (which needed a quantile remap, a
|
|
20
|
+
* body precompute, two overlaid layers for green/red, and a column-name tracker).
|
|
21
|
+
*
|
|
22
|
+
* **Cursor.** Unlike `BoxPlot`, a candle **participates in the crosshair x-snap**
|
|
23
|
+
* (it exposes plain `sampleAt`, not a consolidated `cursorFlag`), so the reticle
|
|
24
|
+
* lands on candles. The readout keys on `as` and shows `close` by default; pass
|
|
25
|
+
* `showOHLC` for the full four-pill quote.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* <Layers>
|
|
29
|
+
* <Candlestick series={daily} as="AAPL" />
|
|
30
|
+
* </Layers>
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
export function Candlestick({ series, open = 'open', high = 'high', low = 'low', close = 'close', as: semantic, axis, variant = 'candle', colorBy = 'direction', gap = 0, showOHLC = false, index = 0, }) {
|
|
34
|
+
const container = useContext(ContainerContext);
|
|
35
|
+
if (container === null) {
|
|
36
|
+
throw new Error('<Candlestick> must be rendered inside a <ChartContainer>');
|
|
37
|
+
}
|
|
38
|
+
const layers = useContext(LayersContext);
|
|
39
|
+
if (layers === null) {
|
|
40
|
+
throw new Error('<Candlestick> must be rendered inside a <Layers>');
|
|
41
|
+
}
|
|
42
|
+
const ohlc = useMemo(() => ohlcFromTimeSeries(series, { open, high, low, close }), [series, open, high, low, close]);
|
|
43
|
+
// Styling: semantic identifier → theme candle style. The single styling channel.
|
|
44
|
+
const { candle } = container.theme;
|
|
45
|
+
const style = (semantic !== undefined ? candle[semantic] : undefined) ?? candle.default;
|
|
46
|
+
// Series identity for the readout (the `as` role, else the close column name) —
|
|
47
|
+
// the primary `close` pill keys on this, like every other layer.
|
|
48
|
+
const label = semantic ?? close;
|
|
49
|
+
const entry = useMemo(() => ({
|
|
50
|
+
layer: {
|
|
51
|
+
yExtent: () => ohlcExtent(ohlc),
|
|
52
|
+
xKind: 'time',
|
|
53
|
+
xExtent: () => ohlc.length === 0 ? null : [ohlc.x[0], ohlc.xEnd[ohlc.length - 1]],
|
|
54
|
+
sampleAt: (time) => {
|
|
55
|
+
// The readout reads the candle **under the cursor** (containment span,
|
|
56
|
+
// not nearest-by-begin), anchored at the slot centre. Outside every
|
|
57
|
+
// candle → no readout. No `cursorFlag`: the samples flow through the
|
|
58
|
+
// normal per-series tracker path, which is also what keeps the candle
|
|
59
|
+
// in the crosshair x-snap (BoxPlot's cursorFlag opts out of both).
|
|
60
|
+
if (ohlc.length === 0)
|
|
61
|
+
return [];
|
|
62
|
+
const i = ohlcIndexAtTime(ohlc, time);
|
|
63
|
+
if (i < 0 || !isFiniteOhlc(ohlc, i))
|
|
64
|
+
return [];
|
|
65
|
+
const at = (ohlc.x[i] + ohlc.xEnd[i]) / 2;
|
|
66
|
+
const { body, wick } = resolveCandleStyle(style, ohlc.open[i], ohlc.close[i], colorBy);
|
|
67
|
+
if (!showOHLC) {
|
|
68
|
+
// Default: `close` is "the price", keyed on the series id.
|
|
69
|
+
return [{ x: at, value: ohlc.close[i], color: body, label }];
|
|
70
|
+
}
|
|
71
|
+
// Opt-in full quote: four value pills (body colour for open/close, wick
|
|
72
|
+
// colour for the high/low extremes). Each is a value-only axis pill.
|
|
73
|
+
const samples = [
|
|
74
|
+
{ x: at, value: ohlc.high[i], color: wick, label: 'high' },
|
|
75
|
+
{ x: at, value: ohlc.open[i], color: body, label: 'open' },
|
|
76
|
+
{ x: at, value: ohlc.close[i], color: body, label: 'close' },
|
|
77
|
+
{ x: at, value: ohlc.low[i], color: wick, label: 'low' },
|
|
78
|
+
];
|
|
79
|
+
return samples;
|
|
80
|
+
},
|
|
81
|
+
draw: (ctx, xScale, yScale) => drawCandles(ctx, ohlc, xScale, yScale, style, variant, colorBy, gap),
|
|
82
|
+
},
|
|
83
|
+
axisId: axis,
|
|
84
|
+
index,
|
|
85
|
+
}), [ohlc, style, label, variant, colorBy, gap, showOHLC, axis, index]);
|
|
86
|
+
// Stable per-instance slot (see useSlotKey): keeps this candle layer's
|
|
87
|
+
// z-position + identity across prop updates; the injected index drives the sort.
|
|
88
|
+
const slot = useSlotKey();
|
|
89
|
+
useEffect(() => () => layers.unregisterLayer(slot), [layers, slot]);
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
layers.registerLayer(slot, entry);
|
|
92
|
+
}, [layers, slot, entry]);
|
|
93
|
+
// Also a tracker source: the container fans in this series' OHLC at the cursor
|
|
94
|
+
// for the (outside-the-chart) readout.
|
|
95
|
+
const { registerTrackerSource, unregisterTrackerSource } = container;
|
|
96
|
+
useEffect(() => () => unregisterTrackerSource(slot), [unregisterTrackerSource, slot]);
|
|
97
|
+
useEffect(() => {
|
|
98
|
+
registerTrackerSource(slot, entry.layer);
|
|
99
|
+
}, [registerTrackerSource, slot, entry.layer]);
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=Candlestick.js.map
|