@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/data.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { ValueSeries } from 'pond-ts';
|
|
1
2
|
/**
|
|
2
3
|
* Read a numeric column into a `Float64Array`, missing cells as `NaN`.
|
|
3
4
|
*
|
|
@@ -170,6 +171,41 @@ export function boxFromTimeSeries(series, columns) {
|
|
|
170
171
|
length: series.length,
|
|
171
172
|
};
|
|
172
173
|
}
|
|
174
|
+
/**
|
|
175
|
+
* Build an {@link OhlcSeries} from a pond `TimeSeries` — four numeric price
|
|
176
|
+
* columns (`open`/`high`/`low`/`close`) plus the candle's horizontal slot.
|
|
177
|
+
*
|
|
178
|
+
* **Key-shape aware, like {@link barsFromTimeSeries}.** An **interval /
|
|
179
|
+
* timeRange**-keyed series (an `aggregate` rollup — weekly / monthly bars) uses
|
|
180
|
+
* the key's own `[begin, end)` as the slot. A **point**-keyed (`time`) series —
|
|
181
|
+
* raw daily OHLCV — has `begin === end` (zero width), so the slot is derived from
|
|
182
|
+
* neighbour spacing (each candle centred on its timestamp, reaching halfway to
|
|
183
|
+
* each neighbour; see {@link neighbourSpans}). This is the ergonomic win over the
|
|
184
|
+
* interval-only {@link boxFromTimeSeries}: raw OHLC feeds straight in with no
|
|
185
|
+
* `aggregate` pass.
|
|
186
|
+
*
|
|
187
|
+
* A key with any of the four prices missing reads as a gap (the candle draws
|
|
188
|
+
* nothing). Detected by `keyColumn().kind === 'time'`.
|
|
189
|
+
*
|
|
190
|
+
* @throws RangeError if any price column does not exist.
|
|
191
|
+
* @throws TypeError if any price column is not a numeric column.
|
|
192
|
+
*/
|
|
193
|
+
export function ohlcFromTimeSeries(series, columns) {
|
|
194
|
+
const open = readNumericColumn(series, columns.open);
|
|
195
|
+
const high = readNumericColumn(series, columns.high);
|
|
196
|
+
const low = readNumericColumn(series, columns.low);
|
|
197
|
+
const close = readNumericColumn(series, columns.close);
|
|
198
|
+
const n = series.length;
|
|
199
|
+
if (series.keyColumn().kind !== 'time') {
|
|
200
|
+
// Interval / timeRange: the key's own endpoints are the candle slot.
|
|
201
|
+
const { begin, end } = keyBeginEnd(series);
|
|
202
|
+
return { x: begin, xEnd: end, open, high, low, close, length: n };
|
|
203
|
+
}
|
|
204
|
+
// Point key (begin === end): synthesize the slot from neighbour spacing so raw
|
|
205
|
+
// daily OHLCV renders as contiguous candles without a pre-key to intervals.
|
|
206
|
+
const { begin, end } = neighbourSpans(series.keyColumn().begin, n);
|
|
207
|
+
return { x: begin, xEnd: end, open, high, low, close, length: n };
|
|
208
|
+
}
|
|
173
209
|
/**
|
|
174
210
|
* Per-row begin/end buffers for the key column, each aligned to the logical
|
|
175
211
|
* length (zero-copy views). For an interval / timeRange key these are the key's
|
|
@@ -184,6 +220,30 @@ function keyBeginEnd(series) {
|
|
|
184
220
|
// (point-in-time), which the caller's point-key fallback replaces.
|
|
185
221
|
return { begin: key.begin.subarray(0, n), end: key.end.subarray(0, n) };
|
|
186
222
|
}
|
|
223
|
+
/**
|
|
224
|
+
* Synthesize per-point `[begin, end]` spans from a monotonic axis buffer by
|
|
225
|
+
* **neighbour spacing**: each point is centred on its own value and reaches
|
|
226
|
+
* halfway to each neighbour (a Voronoi cell on the axis). The first / last points
|
|
227
|
+
* mirror their single adjacent gap so the end cells match their interior width; a
|
|
228
|
+
* lone point (length 1) keeps zero width (the renderer's `minWidth` floor takes
|
|
229
|
+
* over). Shared by the point-keyed `TimeSeries` bars, the `ValueSeries` bars, and
|
|
230
|
+
* the point-keyed OHLC reader. `axis` is a zero-copy key buffer (must not be
|
|
231
|
+
* mutated) — fresh output buffers are allocated.
|
|
232
|
+
*/
|
|
233
|
+
function neighbourSpans(axis, n) {
|
|
234
|
+
const begin = new Float64Array(n);
|
|
235
|
+
const end = new Float64Array(n);
|
|
236
|
+
for (let i = 0; i < n; i += 1) {
|
|
237
|
+
const x = axis[i];
|
|
238
|
+
// Half-gap to the previous neighbour (mirror the next gap at the left edge).
|
|
239
|
+
const prevGap = i > 0 ? x - axis[i - 1] : i + 1 < n ? axis[i + 1] - x : 0;
|
|
240
|
+
// Half-gap to the next neighbour (mirror the previous gap at the right edge).
|
|
241
|
+
const nextGap = i + 1 < n ? axis[i + 1] - x : i > 0 ? x - axis[i - 1] : 0;
|
|
242
|
+
begin[i] = x - prevGap / 2;
|
|
243
|
+
end[i] = x + nextGap / 2;
|
|
244
|
+
}
|
|
245
|
+
return { begin, end };
|
|
246
|
+
}
|
|
187
247
|
/**
|
|
188
248
|
* Build a {@link BarSeries} from a pond `TimeSeries` — one bar per event, the
|
|
189
249
|
* key's `[begin, end]` as the x-span and `column` as the height.
|
|
@@ -215,20 +275,8 @@ export function barsFromTimeSeries(series, column) {
|
|
|
215
275
|
return { begin, end, y, length: n };
|
|
216
276
|
}
|
|
217
277
|
// Point key (begin === end): synthesize a span from neighbour spacing so the
|
|
218
|
-
// bars have width
|
|
219
|
-
|
|
220
|
-
const src = series.keyColumn().begin;
|
|
221
|
-
const begin = new Float64Array(n);
|
|
222
|
-
const end = new Float64Array(n);
|
|
223
|
-
for (let i = 0; i < n; i += 1) {
|
|
224
|
-
const t = src[i];
|
|
225
|
-
// Half-gap to the previous point (mirror the next gap at the left edge).
|
|
226
|
-
const prevGap = i > 0 ? t - src[i - 1] : i + 1 < n ? src[i + 1] - t : 0;
|
|
227
|
-
// Half-gap to the next point (mirror the previous gap at the right edge).
|
|
228
|
-
const nextGap = i + 1 < n ? src[i + 1] - t : i > 0 ? t - src[i - 1] : 0;
|
|
229
|
-
begin[i] = t - prevGap / 2;
|
|
230
|
-
end[i] = t + nextGap / 2;
|
|
231
|
-
}
|
|
278
|
+
// bars have width (see neighbourSpans).
|
|
279
|
+
const { begin, end } = neighbourSpans(series.keyColumn().begin, n);
|
|
232
280
|
return { begin, end, y, length: n };
|
|
233
281
|
}
|
|
234
282
|
/**
|
|
@@ -253,20 +301,155 @@ export function barsFromTimeSeries(series, column) {
|
|
|
253
301
|
export function barsFromValueSeries(series, column) {
|
|
254
302
|
const y = readValueColumn(series, column);
|
|
255
303
|
const n = series.length;
|
|
256
|
-
// axisValues() is the monotonic key buffer (zero-copy)
|
|
257
|
-
//
|
|
258
|
-
const
|
|
304
|
+
// axisValues() is the monotonic key buffer (zero-copy); neighbourSpans reads it
|
|
305
|
+
// and allocates fresh span buffers (never mutates the source).
|
|
306
|
+
const { begin, end } = neighbourSpans(series.axisValues(), n);
|
|
307
|
+
return { begin, end, y, length: n };
|
|
308
|
+
}
|
|
309
|
+
/**
|
|
310
|
+
* The per-bin `[begin, end]` slots for a `TimeSeries`, key-shape aware — the same
|
|
311
|
+
* rule {@link barsFromTimeSeries} applies: an interval / timeRange key uses its
|
|
312
|
+
* own endpoints; a point (`time`) key synthesizes a span from neighbour spacing
|
|
313
|
+
* (see {@link neighbourSpans}). Shared by the stacked readers so a stack draws
|
|
314
|
+
* true bucket spans over an `aggregate` rollup and contiguous bars over a raw
|
|
315
|
+
* point series.
|
|
316
|
+
*/
|
|
317
|
+
function seriesSlots(series) {
|
|
318
|
+
const n = series.length;
|
|
319
|
+
if (series.keyColumn().kind !== 'time') {
|
|
320
|
+
return keyBeginEnd(series);
|
|
321
|
+
}
|
|
322
|
+
return neighbourSpans(series.keyColumn().begin, n);
|
|
323
|
+
}
|
|
324
|
+
/**
|
|
325
|
+
* Build a {@link StackedBarSeries} from a **`Map` of grouped series** — one series
|
|
326
|
+
* per stack group. This is the natural reader for pond's grouped-aggregate output:
|
|
327
|
+
* `series.partitionBy('host', { groups }).aggregate(Sequence.every('5m'), { n: 'count' }).toMap()`
|
|
328
|
+
* yields a `Map<host, TimeSeries>`, one interval-keyed series per host. The stack
|
|
329
|
+
* order (`groups`, bottom → top) is the map's **insertion order** (stable when you
|
|
330
|
+
* pass `partitionBy`'s `{ groups }` option).
|
|
331
|
+
*
|
|
332
|
+
* **Aligned by bucket key, not by index.** Each partition's `aggregate` spans only
|
|
333
|
+
* *its own* events' range, so the groups generally have **different** grids (host A
|
|
334
|
+
* might have buckets 0–8, host B buckets 3–9). This reader takes the **union** of
|
|
335
|
+
* every group's `[begin, end)` slots (ascending) and places each group's `column`
|
|
336
|
+
* value at the matching `begin`; a bucket a group is missing reads as a gap
|
|
337
|
+
* (`NaN`, contributing nothing to that stack). So the segments always line up on
|
|
338
|
+
* the real bucket, never on a positional accident. (Pass `aggregate`'s
|
|
339
|
+
* `{ range }` option if you want every group padded to one dense grid — the union
|
|
340
|
+
* is then that grid.) When two groups carry the **same `begin`**, the first
|
|
341
|
+
* group's `end` sets that slot's width — correct for the uniform-width buckets
|
|
342
|
+
* `aggregate` / `pivotByGroup` produce (all groups share the grid width), which is
|
|
343
|
+
* the intended input.
|
|
344
|
+
*
|
|
345
|
+
* @throws Error if `groups` is empty.
|
|
346
|
+
* @throws RangeError / TypeError (via {@link readNumericColumn}) if `column` is
|
|
347
|
+
* missing or non-numeric in any member.
|
|
348
|
+
*/
|
|
349
|
+
export function stacksFromGroups(groups, column) {
|
|
350
|
+
const names = [...groups.keys()];
|
|
351
|
+
if (names.length === 0) {
|
|
352
|
+
throw new Error('stacksFromGroups: `groups` map is empty');
|
|
353
|
+
}
|
|
354
|
+
const series = [...groups.values()];
|
|
355
|
+
const G = names.length;
|
|
356
|
+
// Union of all groups' slots, keyed by begin (each begin → its end).
|
|
357
|
+
const ends = new Map();
|
|
358
|
+
const perGroupSlots = series.map((s) => seriesSlots(s));
|
|
359
|
+
for (let g = 0; g < G; g += 1) {
|
|
360
|
+
const { begin, end } = perGroupSlots[g];
|
|
361
|
+
for (let i = 0; i < series[g].length; i += 1) {
|
|
362
|
+
if (!ends.has(begin[i]))
|
|
363
|
+
ends.set(begin[i], end[i]);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
const begins = [...ends.keys()].sort((a, b) => a - b);
|
|
367
|
+
const n = begins.length;
|
|
368
|
+
const beginArr = new Float64Array(n);
|
|
369
|
+
const endArr = new Float64Array(n);
|
|
370
|
+
const slotOf = new Map();
|
|
371
|
+
for (let i = 0; i < n; i += 1) {
|
|
372
|
+
beginArr[i] = begins[i];
|
|
373
|
+
endArr[i] = ends.get(begins[i]);
|
|
374
|
+
slotOf.set(begins[i], i);
|
|
375
|
+
}
|
|
376
|
+
const values = new Float64Array(n * G);
|
|
377
|
+
values.fill(NaN);
|
|
378
|
+
for (let g = 0; g < G; g += 1) {
|
|
379
|
+
const { begin } = perGroupSlots[g];
|
|
380
|
+
const col = readNumericColumn(series[g], column);
|
|
381
|
+
for (let i = 0; i < series[g].length; i += 1) {
|
|
382
|
+
const slot = slotOf.get(begin[i]);
|
|
383
|
+
if (slot !== undefined)
|
|
384
|
+
values[slot * G + g] = col[i];
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
return { begin: beginArr, end: endArr, groups: names, values, length: n };
|
|
388
|
+
}
|
|
389
|
+
/**
|
|
390
|
+
* Build a {@link StackedBarSeries} from a **wide** series — one numeric column
|
|
391
|
+
* per stack group. This is the reader for pond's `pivotByGroup` output (long →
|
|
392
|
+
* wide reshape: each group value becomes its own column), or any series that is
|
|
393
|
+
* already wide (e.g. `in` / `out` traffic). `columns` names the segment columns
|
|
394
|
+
* **bottom → top**; a `ValueSeries` bins on its value axis (neighbour-spaced
|
|
395
|
+
* slots), a `TimeSeries` on its key (interval spans or neighbour-spaced points).
|
|
396
|
+
*
|
|
397
|
+
* @throws RangeError / TypeError if any column is missing or non-numeric.
|
|
398
|
+
*/
|
|
399
|
+
export function stacksFromColumns(series, columns) {
|
|
400
|
+
const n = series.length;
|
|
401
|
+
const G = columns.length;
|
|
402
|
+
const isValue = series instanceof ValueSeries;
|
|
403
|
+
const { begin, end } = isValue
|
|
404
|
+
? neighbourSpans(series.axisValues(), n)
|
|
405
|
+
: seriesSlots(series);
|
|
406
|
+
const values = new Float64Array(n * G);
|
|
407
|
+
for (let g = 0; g < G; g += 1) {
|
|
408
|
+
const col = isValue
|
|
409
|
+
? readValueColumn(series, columns[g])
|
|
410
|
+
: readNumericColumn(series, columns[g]);
|
|
411
|
+
for (let i = 0; i < n; i += 1) {
|
|
412
|
+
values[i * G + g] = col[i];
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
return { begin, end, groups: columns, values, length: n };
|
|
416
|
+
}
|
|
417
|
+
/**
|
|
418
|
+
* Build a {@link StackedBarSeries} from **`byColumn` bin records** — the array of
|
|
419
|
+
* `{ start, end, …aggregates }` a value-band aggregation returns
|
|
420
|
+
* (`series.byColumn('power', { width: 20 }, { seconds: { from: 'dt', using: 'sum' } })`).
|
|
421
|
+
* `columns` names the aggregate field(s) to draw as segments (`['seconds']` for a
|
|
422
|
+
* plain distribution; several for a stacked value-band histogram).
|
|
423
|
+
*
|
|
424
|
+
* By default each bin keeps its real numeric `[start, end]` edges — a true value
|
|
425
|
+
* axis (power W, risk %). Pass `{ ordinal: true }` for uniform unit slots
|
|
426
|
+
* (`[i, i+1]`) when the bins are **categories** whose numeric width shouldn't
|
|
427
|
+
* distort the layout (heart-rate zones); label them with `<YAxis ticks>`.
|
|
428
|
+
*
|
|
429
|
+
* A missing / non-finite aggregate reads as a gap (`NaN`).
|
|
430
|
+
*/
|
|
431
|
+
export function stacksFromBins(bins, columns, options = {}) {
|
|
432
|
+
const n = bins.length;
|
|
433
|
+
const G = columns.length;
|
|
259
434
|
const begin = new Float64Array(n);
|
|
260
435
|
const end = new Float64Array(n);
|
|
436
|
+
const values = new Float64Array(n * G);
|
|
261
437
|
for (let i = 0; i < n; i += 1) {
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
438
|
+
const bin = bins[i];
|
|
439
|
+
if (options.ordinal) {
|
|
440
|
+
begin[i] = i;
|
|
441
|
+
end[i] = i + 1;
|
|
442
|
+
}
|
|
443
|
+
else {
|
|
444
|
+
begin[i] = bin.start;
|
|
445
|
+
end[i] = bin.end;
|
|
446
|
+
}
|
|
447
|
+
const fields = bin;
|
|
448
|
+
for (let g = 0; g < G; g += 1) {
|
|
449
|
+
const v = fields[columns[g]];
|
|
450
|
+
values[i * G + g] = typeof v === 'number' && Number.isFinite(v) ? v : NaN;
|
|
451
|
+
}
|
|
269
452
|
}
|
|
270
|
-
return { begin, end,
|
|
453
|
+
return { begin, end, groups: columns, values, length: n };
|
|
271
454
|
}
|
|
272
455
|
//# sourceMappingURL=data.js.map
|
package/dist/grid.d.ts
CHANGED
|
@@ -8,4 +8,18 @@
|
|
|
8
8
|
* data layers that draw next.
|
|
9
9
|
*/
|
|
10
10
|
export declare function drawGrid(ctx: CanvasRenderingContext2D, xTicks: readonly number[], yTicks: readonly number[], width: number, height: number, color: string, dash: readonly number[]): void;
|
|
11
|
+
/**
|
|
12
|
+
* Greedily thin an **ascending** list of pixel positions so no two kept lines
|
|
13
|
+
* are closer than `minGap` px — keeps the axis from crowding when collapse
|
|
14
|
+
* points are dense (e.g. a divider at every daily candle). Keeps the first of
|
|
15
|
+
* each cluster.
|
|
16
|
+
*/
|
|
17
|
+
export declare function thinPixels(xs: readonly number[], minGap: number): number[];
|
|
18
|
+
/**
|
|
19
|
+
* Stroke **session dividers** — solid vertical lines at each `xs` pixel, spanning
|
|
20
|
+
* the plot height. Drawn a touch stronger than the dashed gridlines (solid, so a
|
|
21
|
+
* session/day boundary reads as structural, not just another tick) at the
|
|
22
|
+
* discontinuity provider's collapse points (see `DiscontinuityProvider.boundaries`).
|
|
23
|
+
*/
|
|
24
|
+
export declare function drawDividers(ctx: CanvasRenderingContext2D, xs: readonly number[], height: number, color: string): void;
|
|
11
25
|
//# sourceMappingURL=grid.d.ts.map
|
package/dist/grid.js
CHANGED
|
@@ -26,4 +26,40 @@ export function drawGrid(ctx, xTicks, yTicks, width, height, color, dash) {
|
|
|
26
26
|
ctx.stroke();
|
|
27
27
|
ctx.restore();
|
|
28
28
|
}
|
|
29
|
+
/**
|
|
30
|
+
* Greedily thin an **ascending** list of pixel positions so no two kept lines
|
|
31
|
+
* are closer than `minGap` px — keeps the axis from crowding when collapse
|
|
32
|
+
* points are dense (e.g. a divider at every daily candle). Keeps the first of
|
|
33
|
+
* each cluster.
|
|
34
|
+
*/
|
|
35
|
+
export function thinPixels(xs, minGap) {
|
|
36
|
+
const out = [];
|
|
37
|
+
for (const x of xs) {
|
|
38
|
+
if (out.length === 0 || x - out[out.length - 1] >= minGap)
|
|
39
|
+
out.push(x);
|
|
40
|
+
}
|
|
41
|
+
return out;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Stroke **session dividers** — solid vertical lines at each `xs` pixel, spanning
|
|
45
|
+
* the plot height. Drawn a touch stronger than the dashed gridlines (solid, so a
|
|
46
|
+
* session/day boundary reads as structural, not just another tick) at the
|
|
47
|
+
* discontinuity provider's collapse points (see `DiscontinuityProvider.boundaries`).
|
|
48
|
+
*/
|
|
49
|
+
export function drawDividers(ctx, xs, height, color) {
|
|
50
|
+
if (xs.length === 0)
|
|
51
|
+
return;
|
|
52
|
+
ctx.save();
|
|
53
|
+
ctx.strokeStyle = color;
|
|
54
|
+
ctx.lineWidth = 1;
|
|
55
|
+
ctx.setLineDash([]);
|
|
56
|
+
ctx.beginPath();
|
|
57
|
+
for (const x of xs) {
|
|
58
|
+
const px = Math.round(x) + 0.5;
|
|
59
|
+
ctx.moveTo(px, 0);
|
|
60
|
+
ctx.lineTo(px, height);
|
|
61
|
+
}
|
|
62
|
+
ctx.stroke();
|
|
63
|
+
ctx.restore();
|
|
64
|
+
}
|
|
29
65
|
//# sourceMappingURL=grid.js.map
|
package/dist/index.d.ts
CHANGED
|
@@ -42,18 +42,24 @@ export { BoxPlot } from './BoxPlot.js';
|
|
|
42
42
|
export type { BoxPlotProps } from './BoxPlot.js';
|
|
43
43
|
export { BarChart } from './BarChart.js';
|
|
44
44
|
export type { BarChartProps } from './BarChart.js';
|
|
45
|
+
export { Candlestick } from './Candlestick.js';
|
|
46
|
+
export type { CandlestickProps } from './Candlestick.js';
|
|
47
|
+
export type { CandleVariant, ColorBy } from './ohlc.js';
|
|
48
|
+
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
49
|
+
export type { TradingTimeScale, DiscontinuityProvider, } from './tradingTimeScale.js';
|
|
45
50
|
export { Region, Baseline, Marker } from './annotations.js';
|
|
46
51
|
export type { RegionProps, BaselineProps, MarkerProps } from './annotations.js';
|
|
47
52
|
export type { AnnotationKind, CreateSpec } from './context.js';
|
|
48
53
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
49
54
|
export type { YAxisIndicatorProps, LiveValue } from './indicators.js';
|
|
50
|
-
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, } from './data.js';
|
|
51
|
-
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, } from './data.js';
|
|
55
|
+
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries, stacksFromGroups, stacksFromColumns, stacksFromBins, } from './data.js';
|
|
56
|
+
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, OhlcSeries, OhlcColumns, StackedBarSeries, BinRecord, StacksFromBinsOptions, } from './data.js';
|
|
57
|
+
export type { Orientation } from './bars.js';
|
|
52
58
|
export type { RadiusEncoding, ColorEncoding } from './encoding.js';
|
|
53
59
|
export type { Curve } from './curve.js';
|
|
54
60
|
export type { GapMode } from './gaps.js';
|
|
55
61
|
export { defaultTheme, estelaTheme } from './theme.js';
|
|
56
|
-
export type { ChartTheme, LineStyle, BandStyle, AreaStyle, ScatterStyle, BoxStyle, BarStyle, } from './theme.js';
|
|
62
|
+
export type { ChartTheme, LineStyle, BandStyle, AreaStyle, ScatterStyle, BoxStyle, CandleStyle, BarStyle, } from './theme.js';
|
|
57
63
|
export { cssVarTheme } from './css-theme.js';
|
|
58
64
|
export type { ChartThemeOverrides, VarReader } from './css-theme.js';
|
|
59
65
|
export { useChartTheme } from './useChartTheme.js';
|
package/dist/index.js
CHANGED
|
@@ -29,13 +29,18 @@ export { AreaChart } from './AreaChart.js';
|
|
|
29
29
|
export { ScatterChart } from './ScatterChart.js';
|
|
30
30
|
export { BoxPlot } from './BoxPlot.js';
|
|
31
31
|
export { BarChart } from './BarChart.js';
|
|
32
|
+
export { Candlestick } from './Candlestick.js';
|
|
33
|
+
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
32
34
|
// Annotations — user-authored marks in the turquoise register (distinct from the
|
|
33
35
|
// data): a shaded span, a horizontal value line, a vertical x line.
|
|
34
36
|
export { Region, Baseline, Marker } from './annotations.js';
|
|
35
37
|
// Axis indicators — a value pill pinned to an axis edge (the ChartIQ live tag).
|
|
36
38
|
// `createLiveValue` is the high-frequency, isolated-repaint update path.
|
|
37
39
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
38
|
-
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries,
|
|
40
|
+
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries,
|
|
41
|
+
// Stacked / histogram readers — assemble a StackedBarSeries from pond's own
|
|
42
|
+
// aggregation output: a Map of grouped series, a wide series, or byColumn bins.
|
|
43
|
+
stacksFromGroups, stacksFromColumns, stacksFromBins, } from './data.js';
|
|
39
44
|
export { defaultTheme, estelaTheme } from './theme.js';
|
|
40
45
|
// CSS-custom-property → ChartTheme bridge: build a theme from a design system's
|
|
41
46
|
// tokens (`cssVarTheme`), and a hook that re-resolves it on a `data-theme`
|
package/dist/ohlc.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { OhlcSeries } from './data.js';
|
|
2
|
+
import type { Scale } from './line.js';
|
|
3
|
+
import type { CandleStyle } from './theme.js';
|
|
4
|
+
/**
|
|
5
|
+
* How an OHLC mark renders (pjm17971's fork 2 — bundled as one component, like
|
|
6
|
+
* {@link BoxShape}, not split into a separate `<OHLCBar>`):
|
|
7
|
+
*
|
|
8
|
+
* - **`candle`** (default) — a filled `open→close` body with a `high–low` wick.
|
|
9
|
+
* - **`bar`** — an OHLC tick bar: a `high–low` stem with a left tick at `open`
|
|
10
|
+
* and a right tick at `close`, no body.
|
|
11
|
+
* - **`hollow`** — like `candle`, but a **rising** candle (close > open) draws a
|
|
12
|
+
* *hollow* (outlined) body and a **falling / doji** one a filled body.
|
|
13
|
+
*/
|
|
14
|
+
export type CandleVariant = 'candle' | 'bar' | 'hollow';
|
|
15
|
+
/**
|
|
16
|
+
* What drives a candle's colour:
|
|
17
|
+
*
|
|
18
|
+
* - **`direction`** (default, market convention) — `rising` when close > open,
|
|
19
|
+
* `falling` when close < open, `neutral` when equal (a doji).
|
|
20
|
+
* - **`series`** — one colour off the `as` role (the style's `rising` pair),
|
|
21
|
+
* *no* green/red. Keeps "colour = series" when a candle sits beside coloured
|
|
22
|
+
* lines and the up/down split would read as a second, conflicting encoding.
|
|
23
|
+
*/
|
|
24
|
+
export type ColorBy = 'direction' | 'series';
|
|
25
|
+
/**
|
|
26
|
+
* The `[min, max]` vertical extent of the **drawn** candles — the lowest `low`
|
|
27
|
+
* and highest `high` over keys where **all four** prices are finite — or `null`
|
|
28
|
+
* if none are. Gap keys (any price `NaN`) are excluded, matching what
|
|
29
|
+
* {@link drawCandles} draws, so they don't drag the y-domain.
|
|
30
|
+
*
|
|
31
|
+
* Only `low`/`high` bound the extent: they are the outermost reach of a candle,
|
|
32
|
+
* so `open`/`close` lie within `[low, high]` for any well-formed OHLC row and
|
|
33
|
+
* never widen it. (A malformed row where, say, `close > high` would clip — an
|
|
34
|
+
* upstream data error, not the chart's to paper over.)
|
|
35
|
+
*/
|
|
36
|
+
export declare function ohlcExtent(ohlc: OhlcSeries): [number, number] | null;
|
|
37
|
+
/**
|
|
38
|
+
* The index of the candle whose slot `[x, xEnd]` contains `time` — the candle
|
|
39
|
+
* **under the cursor** — or `-1` if `time` is in no slot. Containment (the box
|
|
40
|
+
* analog {@link boxIndexAtTime}), not nearest-by-`begin` (which flips to the next
|
|
41
|
+
* candle past a wide one's midpoint). Candles are sorted by `x`; at a shared edge
|
|
42
|
+
* the left candle wins. A gap candle (some price non-finite) still owns its span
|
|
43
|
+
* here; the caller drops it on the finiteness check. O(N) over the candles
|
|
44
|
+
* (view-scale).
|
|
45
|
+
*/
|
|
46
|
+
export declare function ohlcIndexAtTime(ohlc: OhlcSeries, time: number): number;
|
|
47
|
+
/** All four prices finite at `i` — i.e. this candle is drawn. */
|
|
48
|
+
export declare function isFiniteOhlc(ohlc: OhlcSeries, i: number): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Resolve the `{ body, wick }` colours for one candle from its `open`/`close`
|
|
51
|
+
* and the {@link ColorBy} mode. `direction` picks `rising` (close > open) /
|
|
52
|
+
* `falling` (close < open) / `neutral` (equal — a doji, falling back to `rising`
|
|
53
|
+
* when the style omits it); `series` always returns `rising` (one colour, no
|
|
54
|
+
* up/down split). The single source of the colour decision, shared by
|
|
55
|
+
* {@link drawCandles} and `<Candlestick>`'s tracker readouts so the pill colour
|
|
56
|
+
* matches the mark.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveCandleStyle(style: CandleStyle, open: number, close: number, colorBy: ColorBy): {
|
|
59
|
+
body: string;
|
|
60
|
+
wick: string;
|
|
61
|
+
};
|
|
62
|
+
/**
|
|
63
|
+
* Draw one candle per key of `ohlc`, mapping data→pixels through
|
|
64
|
+
* `xScale`/`yScale`. The OHLC sibling of {@link drawBox}: each key gets its own
|
|
65
|
+
* mark over its slot x-span (`barSpanPx`, inset by `gapPx` so adjacent candles
|
|
66
|
+
* breathe), in the chosen {@link CandleVariant}, coloured per {@link ColorBy}.
|
|
67
|
+
*
|
|
68
|
+
* The body extents are derived here (`min`/`max` of open/close) — the consumer
|
|
69
|
+
* never precomputes them. A doji (open === close) draws a {@link MIN_BODY_HEIGHT_PX}
|
|
70
|
+
* body so it stays visible. The body is a fraction (`style.bodyWidth`, default
|
|
71
|
+
* {@link DEFAULT_BODY_WIDTH}) of the slot, centred; the wick / OHLC-bar stem sits
|
|
72
|
+
* at the slot centre.
|
|
73
|
+
*
|
|
74
|
+
* **Gap-aware**: a key with any price non-finite is skipped entirely (no partial
|
|
75
|
+
* candle) — the same contract as a box / band gap.
|
|
76
|
+
*
|
|
77
|
+
* O(N) over the keys, a fixed number of path ops each — no per-key allocation
|
|
78
|
+
* beyond the `barSpanPx` tuple.
|
|
79
|
+
*/
|
|
80
|
+
export declare function drawCandles(ctx: CanvasRenderingContext2D, ohlc: OhlcSeries, xScale: Scale, yScale: Scale, style: CandleStyle, variant?: CandleVariant, colorBy?: ColorBy, gapPx?: number, minWidthPx?: number): void;
|
|
81
|
+
//# sourceMappingURL=ohlc.d.ts.map
|
package/dist/ohlc.js
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { barSpanPx } from './range.js';
|
|
2
|
+
/** Default body width as a fraction of the candle slot when the style omits one. */
|
|
3
|
+
const DEFAULT_BODY_WIDTH = 0.8;
|
|
4
|
+
/** Minimum body height in px so a doji (open === close) still shows a mark. */
|
|
5
|
+
const MIN_BODY_HEIGHT_PX = 1;
|
|
6
|
+
/**
|
|
7
|
+
* The `[min, max]` vertical extent of the **drawn** candles — the lowest `low`
|
|
8
|
+
* and highest `high` over keys where **all four** prices are finite — or `null`
|
|
9
|
+
* if none are. Gap keys (any price `NaN`) are excluded, matching what
|
|
10
|
+
* {@link drawCandles} draws, so they don't drag the y-domain.
|
|
11
|
+
*
|
|
12
|
+
* Only `low`/`high` bound the extent: they are the outermost reach of a candle,
|
|
13
|
+
* so `open`/`close` lie within `[low, high]` for any well-formed OHLC row and
|
|
14
|
+
* never widen it. (A malformed row where, say, `close > high` would clip — an
|
|
15
|
+
* upstream data error, not the chart's to paper over.)
|
|
16
|
+
*/
|
|
17
|
+
export function ohlcExtent(ohlc) {
|
|
18
|
+
let min = Infinity;
|
|
19
|
+
let max = -Infinity;
|
|
20
|
+
for (let i = 0; i < ohlc.length; i += 1) {
|
|
21
|
+
if (!isFiniteOhlc(ohlc, i))
|
|
22
|
+
continue;
|
|
23
|
+
const lo = ohlc.low[i];
|
|
24
|
+
const hi = ohlc.high[i];
|
|
25
|
+
if (lo < min)
|
|
26
|
+
min = lo;
|
|
27
|
+
if (hi > max)
|
|
28
|
+
max = hi;
|
|
29
|
+
}
|
|
30
|
+
return min === Infinity ? null : [min, max];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The index of the candle whose slot `[x, xEnd]` contains `time` — the candle
|
|
34
|
+
* **under the cursor** — or `-1` if `time` is in no slot. Containment (the box
|
|
35
|
+
* analog {@link boxIndexAtTime}), not nearest-by-`begin` (which flips to the next
|
|
36
|
+
* candle past a wide one's midpoint). Candles are sorted by `x`; at a shared edge
|
|
37
|
+
* the left candle wins. A gap candle (some price non-finite) still owns its span
|
|
38
|
+
* here; the caller drops it on the finiteness check. O(N) over the candles
|
|
39
|
+
* (view-scale).
|
|
40
|
+
*/
|
|
41
|
+
export function ohlcIndexAtTime(ohlc, time) {
|
|
42
|
+
for (let i = 0; i < ohlc.length; i += 1) {
|
|
43
|
+
if (time >= ohlc.x[i] && time <= ohlc.xEnd[i])
|
|
44
|
+
return i;
|
|
45
|
+
}
|
|
46
|
+
return -1;
|
|
47
|
+
}
|
|
48
|
+
/** All four prices finite at `i` — i.e. this candle is drawn. */
|
|
49
|
+
export function isFiniteOhlc(ohlc, i) {
|
|
50
|
+
return (Number.isFinite(ohlc.open[i]) &&
|
|
51
|
+
Number.isFinite(ohlc.high[i]) &&
|
|
52
|
+
Number.isFinite(ohlc.low[i]) &&
|
|
53
|
+
Number.isFinite(ohlc.close[i]));
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the `{ body, wick }` colours for one candle from its `open`/`close`
|
|
57
|
+
* and the {@link ColorBy} mode. `direction` picks `rising` (close > open) /
|
|
58
|
+
* `falling` (close < open) / `neutral` (equal — a doji, falling back to `rising`
|
|
59
|
+
* when the style omits it); `series` always returns `rising` (one colour, no
|
|
60
|
+
* up/down split). The single source of the colour decision, shared by
|
|
61
|
+
* {@link drawCandles} and `<Candlestick>`'s tracker readouts so the pill colour
|
|
62
|
+
* matches the mark.
|
|
63
|
+
*/
|
|
64
|
+
export function resolveCandleStyle(style, open, close, colorBy) {
|
|
65
|
+
if (colorBy === 'series')
|
|
66
|
+
return style.rising;
|
|
67
|
+
if (close > open)
|
|
68
|
+
return style.rising;
|
|
69
|
+
if (close < open)
|
|
70
|
+
return style.falling;
|
|
71
|
+
return style.neutral ?? style.rising;
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Draw one candle per key of `ohlc`, mapping data→pixels through
|
|
75
|
+
* `xScale`/`yScale`. The OHLC sibling of {@link drawBox}: each key gets its own
|
|
76
|
+
* mark over its slot x-span (`barSpanPx`, inset by `gapPx` so adjacent candles
|
|
77
|
+
* breathe), in the chosen {@link CandleVariant}, coloured per {@link ColorBy}.
|
|
78
|
+
*
|
|
79
|
+
* The body extents are derived here (`min`/`max` of open/close) — the consumer
|
|
80
|
+
* never precomputes them. A doji (open === close) draws a {@link MIN_BODY_HEIGHT_PX}
|
|
81
|
+
* body so it stays visible. The body is a fraction (`style.bodyWidth`, default
|
|
82
|
+
* {@link DEFAULT_BODY_WIDTH}) of the slot, centred; the wick / OHLC-bar stem sits
|
|
83
|
+
* at the slot centre.
|
|
84
|
+
*
|
|
85
|
+
* **Gap-aware**: a key with any price non-finite is skipped entirely (no partial
|
|
86
|
+
* candle) — the same contract as a box / band gap.
|
|
87
|
+
*
|
|
88
|
+
* O(N) over the keys, a fixed number of path ops each — no per-key allocation
|
|
89
|
+
* beyond the `barSpanPx` tuple.
|
|
90
|
+
*/
|
|
91
|
+
export function drawCandles(ctx, ohlc, xScale, yScale, style, variant = 'candle', colorBy = 'direction', gapPx = 0, minWidthPx = 1) {
|
|
92
|
+
const bodyFraction = style.bodyWidth ?? DEFAULT_BODY_WIDTH;
|
|
93
|
+
for (let i = 0; i < ohlc.length; i += 1) {
|
|
94
|
+
if (!isFiniteOhlc(ohlc, i))
|
|
95
|
+
continue;
|
|
96
|
+
const open = ohlc.open[i];
|
|
97
|
+
const close = ohlc.close[i];
|
|
98
|
+
const [x0, x1] = barSpanPx(ohlc.x[i], ohlc.xEnd[i], xScale, gapPx, minWidthPx);
|
|
99
|
+
const mid = (x0 + x1) / 2;
|
|
100
|
+
const bodyHalf = ((x1 - x0) * bodyFraction) / 2;
|
|
101
|
+
const bx0 = mid - bodyHalf;
|
|
102
|
+
const bodyW = bodyHalf * 2;
|
|
103
|
+
const yOpen = yScale(open);
|
|
104
|
+
const yHigh = yScale(ohlc.high[i]);
|
|
105
|
+
const yLow = yScale(ohlc.low[i]);
|
|
106
|
+
const yClose = yScale(close);
|
|
107
|
+
const { body, wick } = resolveCandleStyle(style, open, close, colorBy);
|
|
108
|
+
if (variant === 'bar') {
|
|
109
|
+
// OHLC bar: a high–low stem, a left tick at open, a right tick at close —
|
|
110
|
+
// all one colour (the `body` role), no filled body.
|
|
111
|
+
ctx.strokeStyle = body;
|
|
112
|
+
ctx.lineWidth = style.wickWidth;
|
|
113
|
+
ctx.beginPath();
|
|
114
|
+
ctx.moveTo(mid, yHigh); // stem
|
|
115
|
+
ctx.lineTo(mid, yLow);
|
|
116
|
+
ctx.moveTo(bx0, yOpen); // open tick (points left)
|
|
117
|
+
ctx.lineTo(mid, yOpen);
|
|
118
|
+
ctx.moveTo(mid, yClose); // close tick (points right)
|
|
119
|
+
ctx.lineTo(mid + bodyHalf, yClose);
|
|
120
|
+
ctx.stroke();
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// candle / hollow: the high–low wick first (so the body overlaps it), then
|
|
124
|
+
// the open→close body.
|
|
125
|
+
ctx.strokeStyle = wick;
|
|
126
|
+
ctx.lineWidth = style.wickWidth;
|
|
127
|
+
ctx.beginPath();
|
|
128
|
+
ctx.moveTo(mid, yHigh);
|
|
129
|
+
ctx.lineTo(mid, yLow);
|
|
130
|
+
ctx.stroke();
|
|
131
|
+
// Body extents, with a doji floor so open === close still shows a mark.
|
|
132
|
+
let top = Math.min(yOpen, yClose);
|
|
133
|
+
let h = Math.abs(yClose - yOpen);
|
|
134
|
+
if (h < MIN_BODY_HEIGHT_PX) {
|
|
135
|
+
top -= (MIN_BODY_HEIGHT_PX - h) / 2;
|
|
136
|
+
h = MIN_BODY_HEIGHT_PX;
|
|
137
|
+
}
|
|
138
|
+
// `hollow`: a rising candle is outlined (hollow), a falling / doji one filled
|
|
139
|
+
// — the same strict-`>` boundary resolveCandleStyle uses (equality → neutral),
|
|
140
|
+
// so a doji's fill and its colour agree.
|
|
141
|
+
const hollow = variant === 'hollow' && close > open;
|
|
142
|
+
if (hollow) {
|
|
143
|
+
ctx.strokeStyle = body;
|
|
144
|
+
ctx.lineWidth = style.wickWidth;
|
|
145
|
+
ctx.strokeRect(bx0, top, bodyW, h);
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
ctx.fillStyle = body;
|
|
149
|
+
ctx.fillRect(bx0, top, bodyW, h);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=ohlc.js.map
|
package/dist/scatter.d.ts
CHANGED
|
@@ -44,13 +44,15 @@ export declare function scatterExtent(cs: ChartSeries): [number, number] | null;
|
|
|
44
44
|
* @param labelAt optional per-point text label; `undefined` ⇒ no labels drawn.
|
|
45
45
|
* @param font `theme.font` (family + size) for label text.
|
|
46
46
|
* @param selected the container's current selection (or `null`).
|
|
47
|
-
* @param
|
|
48
|
-
* `
|
|
47
|
+
* @param seriesId this layer's stable series identity (its `id` prop, or
|
|
48
|
+
* `undefined` when the layer isn't selectable) — the series half
|
|
49
|
+
* of the selection match. A point lights only when the selection's
|
|
50
|
+
* `id` matches, keyed to the sample by its `key`.
|
|
49
51
|
*/
|
|
50
52
|
export declare function drawScatter(ctx: CanvasRenderingContext2D, cs: ChartSeries, xScale: Scale, yScale: Scale, style: ScatterStyle, encoding: ResolvedEncoding, keyAt: (i: number) => number, labelAt: ((i: number) => string | undefined) | undefined, font: {
|
|
51
53
|
readonly family: string;
|
|
52
54
|
readonly size: number;
|
|
53
|
-
}, selected: SelectInfo | null,
|
|
55
|
+
}, selected: SelectInfo | null, seriesId: string | undefined): void;
|
|
54
56
|
/**
|
|
55
57
|
* Hit-test plot-pixel `(qx, qy)` against the scatter's points — the topmost
|
|
56
58
|
* point whose circle contains the click, or `null`. "Topmost" = the
|
|
@@ -59,12 +61,12 @@ export declare function drawScatter(ctx: CanvasRenderingContext2D, cs: ChartSeri
|
|
|
59
61
|
*
|
|
60
62
|
* A point's hit radius is its drawn radius (data-driven or base) — clicking the
|
|
61
63
|
* visible disc selects it. Distance is compared squared (no `sqrt` in the loop).
|
|
62
|
-
* Returns the point's {@link SelectInfo} with
|
|
63
|
-
*
|
|
64
|
-
* and the
|
|
64
|
+
* Returns the point's {@link SelectInfo} with the series `id` (the selection
|
|
65
|
+
* identity), `key = keyAt(i)` (its event `begin` — click provenance), the encoded
|
|
66
|
+
* fill colour (so the readout swatch matches the mark), and the display `label`.
|
|
65
67
|
*
|
|
66
68
|
* Pure: takes the same `xScale`/`yScale` the row hands to `draw`, so it
|
|
67
69
|
* unit-tests without a DOM (mirrors the `sampleAt` / `resolveSelection` split).
|
|
68
70
|
*/
|
|
69
|
-
export declare function hitTestScatter(cs: ChartSeries, qx: number, qy: number, xScale: Scale, yScale: Scale, encoding: ResolvedEncoding, keyAt: (i: number) => number, seriesLabel: string): SelectInfo | null;
|
|
71
|
+
export declare function hitTestScatter(cs: ChartSeries, qx: number, qy: number, xScale: Scale, yScale: Scale, encoding: ResolvedEncoding, keyAt: (i: number) => number, id: string, seriesLabel: string): SelectInfo | null;
|
|
70
72
|
//# sourceMappingURL=scatter.d.ts.map
|