@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/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
|
*
|
|
@@ -305,4 +306,217 @@ export function barsFromValueSeries(series, column) {
|
|
|
305
306
|
const { begin, end } = neighbourSpans(series.axisValues(), n);
|
|
306
307
|
return { begin, end, y, length: n };
|
|
307
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;
|
|
434
|
+
const begin = new Float64Array(n);
|
|
435
|
+
const end = new Float64Array(n);
|
|
436
|
+
const values = new Float64Array(n * G);
|
|
437
|
+
for (let i = 0; i < n; i += 1) {
|
|
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
|
+
}
|
|
452
|
+
}
|
|
453
|
+
return { begin, end, groups: columns, values, length: n };
|
|
454
|
+
}
|
|
455
|
+
/**
|
|
456
|
+
* Build a {@link StackedBarSeries} (single group, `G === 1`) from an ordered list
|
|
457
|
+
* of `{ label, value }` categories — one **unit slot** `[i, i+1]` per category, in
|
|
458
|
+
* order. This is the categorical row-read's geometry: the slots are ordinal
|
|
459
|
+
* indices (the bar's pixel span comes from the container's {@link ScaleBand}), and
|
|
460
|
+
* the `label`s become the axis's ordered category names (`xCategories`). A
|
|
461
|
+
* non-finite value reads as a gap (`NaN`). Reuses the shipped stacked geometry —
|
|
462
|
+
* no new draw path.
|
|
463
|
+
*/
|
|
464
|
+
export function categoryStack(records) {
|
|
465
|
+
const n = records.length;
|
|
466
|
+
const begin = new Float64Array(n);
|
|
467
|
+
const end = new Float64Array(n);
|
|
468
|
+
const values = new Float64Array(n);
|
|
469
|
+
const marks = new Array(n);
|
|
470
|
+
for (let i = 0; i < n; i += 1) {
|
|
471
|
+
begin[i] = i;
|
|
472
|
+
end[i] = i + 1;
|
|
473
|
+
const v = records[i].value;
|
|
474
|
+
values[i] = Number.isFinite(v) ? v : NaN;
|
|
475
|
+
marks[i] = records[i].label;
|
|
476
|
+
}
|
|
477
|
+
// `marks` carry the category names — the stable per-bar identity the categorical
|
|
478
|
+
// axis selects on (the slot index `begin` renumbers on reorder; the name doesn't).
|
|
479
|
+
return { begin, end, groups: ['value'], values, length: n, marks };
|
|
480
|
+
}
|
|
481
|
+
/** A series' numeric value column names in schema order (the key column excluded). */
|
|
482
|
+
function numericValueColumns(series) {
|
|
483
|
+
const schema = series.schema;
|
|
484
|
+
return schema
|
|
485
|
+
.slice(1)
|
|
486
|
+
.filter((c) => c.kind === 'number')
|
|
487
|
+
.map((c) => c.name);
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* **The transpose reader** (categorical-axis RFC, Phase 1 PR2): read **one row**
|
|
491
|
+
* of a wide `TimeSeries` **across** — its columns become the categories, that
|
|
492
|
+
* row's cells the values — for `<BarChart categories={…}>`. This is "columns on
|
|
493
|
+
* x": the schema's numeric columns (a `pivotByGroup` output's per-group columns,
|
|
494
|
+
* a vol term structure's per-expiry columns, …) laid out at one instant.
|
|
495
|
+
*
|
|
496
|
+
* The row is picked the ordinary way (`options.at`, default the **head row**):
|
|
497
|
+
* `series.last()` / `.first()` / `.at(index)` / `.nearest(time)`. So "which row"
|
|
498
|
+
* is just row selection — the live snapshot is the head row; a static report
|
|
499
|
+
* pins a row by index or time. (Binding the row to a scrubbing time cursor is a
|
|
500
|
+
* later phase.) Pass `options.columns` to bound / order the category set; omit it
|
|
501
|
+
* to take every numeric value column. An empty series (or a row past the ends)
|
|
502
|
+
* yields `[]`; a missing / non-numeric cell reads as a gap (`NaN`).
|
|
503
|
+
*/
|
|
504
|
+
export function transposeRow(series, options = {}) {
|
|
505
|
+
const at = options.at ?? 'last';
|
|
506
|
+
const event = at === 'last'
|
|
507
|
+
? series.last()
|
|
508
|
+
: at === 'first'
|
|
509
|
+
? series.first()
|
|
510
|
+
: typeof at === 'number'
|
|
511
|
+
? series.at(at)
|
|
512
|
+
: series.nearest(at.time);
|
|
513
|
+
if (event === undefined)
|
|
514
|
+
return [];
|
|
515
|
+
const cols = options.columns ?? numericValueColumns(series);
|
|
516
|
+
const get = event.get.bind(event);
|
|
517
|
+
return cols.map((name) => {
|
|
518
|
+
const v = get(name);
|
|
519
|
+
return { label: name, value: typeof v === 'number' ? v : NaN };
|
|
520
|
+
});
|
|
521
|
+
}
|
|
308
522
|
//# 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
|
@@ -29,6 +29,7 @@ export type { YAxisProps } from './YAxis.js';
|
|
|
29
29
|
export { XAxis } from './XAxis.js';
|
|
30
30
|
export type { XAxisProps } from './XAxis.js';
|
|
31
31
|
export { TimeAxis } from './TimeAxis.js';
|
|
32
|
+
export { CategoryAxis } from './CategoryAxis.js';
|
|
32
33
|
export type { AxisFormat } from './format.js';
|
|
33
34
|
export { LineChart } from './LineChart.js';
|
|
34
35
|
export type { LineChartProps } from './LineChart.js';
|
|
@@ -45,13 +46,18 @@ export type { BarChartProps } from './BarChart.js';
|
|
|
45
46
|
export { Candlestick } from './Candlestick.js';
|
|
46
47
|
export type { CandlestickProps } from './Candlestick.js';
|
|
47
48
|
export type { CandleVariant, ColorBy } from './ohlc.js';
|
|
49
|
+
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
50
|
+
export type { TradingTimeScale, DiscontinuityProvider, } from './tradingTimeScale.js';
|
|
51
|
+
export { scaleBand } from './bandScale.js';
|
|
52
|
+
export type { ScaleBand } from './bandScale.js';
|
|
48
53
|
export { Region, Baseline, Marker } from './annotations.js';
|
|
49
54
|
export type { RegionProps, BaselineProps, MarkerProps } from './annotations.js';
|
|
50
55
|
export type { AnnotationKind, CreateSpec } from './context.js';
|
|
51
56
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
52
57
|
export type { YAxisIndicatorProps, LiveValue } from './indicators.js';
|
|
53
|
-
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries, } from './data.js';
|
|
54
|
-
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, OhlcSeries, OhlcColumns, } from './data.js';
|
|
58
|
+
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries, stacksFromGroups, stacksFromColumns, stacksFromBins, categoryStack, transposeRow, } from './data.js';
|
|
59
|
+
export type { ChartSeries, BandSeries, BoxSeries, BoxColumns, BarSeries, OhlcSeries, OhlcColumns, StackedBarSeries, BinRecord, StacksFromBinsOptions, CategoryDatum, RowAt, TransposeRowOptions, } from './data.js';
|
|
60
|
+
export type { Orientation } from './bars.js';
|
|
55
61
|
export type { RadiusEncoding, ColorEncoding } from './encoding.js';
|
|
56
62
|
export type { Curve } from './curve.js';
|
|
57
63
|
export type { GapMode } from './gaps.js';
|
package/dist/index.js
CHANGED
|
@@ -23,6 +23,7 @@ export { Layers } from './Layers.js';
|
|
|
23
23
|
export { YAxis } from './YAxis.js';
|
|
24
24
|
export { XAxis } from './XAxis.js';
|
|
25
25
|
export { TimeAxis } from './TimeAxis.js';
|
|
26
|
+
export { CategoryAxis } from './CategoryAxis.js';
|
|
26
27
|
export { LineChart } from './LineChart.js';
|
|
27
28
|
export { BandChart } from './BandChart.js';
|
|
28
29
|
export { AreaChart } from './AreaChart.js';
|
|
@@ -30,13 +31,23 @@ export { ScatterChart } from './ScatterChart.js';
|
|
|
30
31
|
export { BoxPlot } from './BoxPlot.js';
|
|
31
32
|
export { BarChart } from './BarChart.js';
|
|
32
33
|
export { Candlestick } from './Candlestick.js';
|
|
34
|
+
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
35
|
+
// The ordinal category (band) scale — the transpose view's "columns on x" axis.
|
|
36
|
+
export { scaleBand } from './bandScale.js';
|
|
33
37
|
// Annotations — user-authored marks in the turquoise register (distinct from the
|
|
34
38
|
// data): a shaded span, a horizontal value line, a vertical x line.
|
|
35
39
|
export { Region, Baseline, Marker } from './annotations.js';
|
|
36
40
|
// Axis indicators — a value pill pinned to an axis edge (the ChartIQ live tag).
|
|
37
41
|
// `createLiveValue` is the high-frequency, isolated-repaint update path.
|
|
38
42
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
39
|
-
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries,
|
|
43
|
+
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries,
|
|
44
|
+
// Stacked / histogram readers — assemble a StackedBarSeries from pond's own
|
|
45
|
+
// aggregation output: a Map of grouped series, a wide series, or byColumn bins.
|
|
46
|
+
stacksFromGroups, stacksFromColumns, stacksFromBins,
|
|
47
|
+
// Categorical row-read: one bar per `{ label, value }` on the category axis.
|
|
48
|
+
categoryStack,
|
|
49
|
+
// The transpose reader — one row of a wide series read across into categories.
|
|
50
|
+
transposeRow, } from './data.js';
|
|
40
51
|
export { defaultTheme, estelaTheme } from './theme.js';
|
|
41
52
|
// CSS-custom-property → ChartTheme bridge: build a theme from a design system's
|
|
42
53
|
// tokens (`cssVarTheme`), and a hook that re-resolves it on a `data-theme`
|
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
|
package/dist/scatter.js
CHANGED
|
@@ -109,13 +109,16 @@ export function scatterExtent(cs) {
|
|
|
109
109
|
* @param labelAt optional per-point text label; `undefined` ⇒ no labels drawn.
|
|
110
110
|
* @param font `theme.font` (family + size) for label text.
|
|
111
111
|
* @param selected the container's current selection (or `null`).
|
|
112
|
-
* @param
|
|
113
|
-
* `
|
|
112
|
+
* @param seriesId this layer's stable series identity (its `id` prop, or
|
|
113
|
+
* `undefined` when the layer isn't selectable) — the series half
|
|
114
|
+
* of the selection match. A point lights only when the selection's
|
|
115
|
+
* `id` matches, keyed to the sample by its `key`.
|
|
114
116
|
*/
|
|
115
|
-
export function drawScatter(ctx, cs, xScale, yScale, style, encoding, keyAt, labelAt, font, selected,
|
|
117
|
+
export function drawScatter(ctx, cs, xScale, yScale, style, encoding, keyAt, labelAt, font, selected, seriesId) {
|
|
116
118
|
ctx.save();
|
|
117
119
|
// The selection only lights up a point of *this* series; resolve the key once.
|
|
118
|
-
|
|
120
|
+
// A no-id (non-selectable) layer passes `undefined` and never matches.
|
|
121
|
+
const selectedKey = selected !== null && selected.id === seriesId ? selected.key : null;
|
|
119
122
|
let selPx = 0;
|
|
120
123
|
let selPy = 0;
|
|
121
124
|
let selR = 0;
|
|
@@ -183,14 +186,14 @@ const LABEL_GAP = 4;
|
|
|
183
186
|
*
|
|
184
187
|
* A point's hit radius is its drawn radius (data-driven or base) — clicking the
|
|
185
188
|
* visible disc selects it. Distance is compared squared (no `sqrt` in the loop).
|
|
186
|
-
* Returns the point's {@link SelectInfo} with
|
|
187
|
-
*
|
|
188
|
-
* and the
|
|
189
|
+
* Returns the point's {@link SelectInfo} with the series `id` (the selection
|
|
190
|
+
* identity), `key = keyAt(i)` (its event `begin` — click provenance), the encoded
|
|
191
|
+
* fill colour (so the readout swatch matches the mark), and the display `label`.
|
|
189
192
|
*
|
|
190
193
|
* Pure: takes the same `xScale`/`yScale` the row hands to `draw`, so it
|
|
191
194
|
* unit-tests without a DOM (mirrors the `sampleAt` / `resolveSelection` split).
|
|
192
195
|
*/
|
|
193
|
-
export function hitTestScatter(cs, qx, qy, xScale, yScale, encoding, keyAt, seriesLabel) {
|
|
196
|
+
export function hitTestScatter(cs, qx, qy, xScale, yScale, encoding, keyAt, id, seriesLabel) {
|
|
194
197
|
for (let i = cs.length - 1; i >= 0; i -= 1) {
|
|
195
198
|
if (!isPoint(cs, i))
|
|
196
199
|
continue;
|
|
@@ -201,6 +204,7 @@ export function hitTestScatter(cs, qx, qy, xScale, yScale, encoding, keyAt, seri
|
|
|
201
204
|
const dy = qy - py;
|
|
202
205
|
if (dx * dx + dy * dy <= r * r) {
|
|
203
206
|
return {
|
|
207
|
+
id,
|
|
204
208
|
key: keyAt(i),
|
|
205
209
|
value: cs.y[i],
|
|
206
210
|
color: encoding.colorAt(i),
|
package/dist/theme.d.ts
CHANGED
|
@@ -104,6 +104,13 @@ export interface ChartTheme {
|
|
|
104
104
|
readonly grid: string;
|
|
105
105
|
/** Gridline dash pattern (px on/off pairs); `[]` for solid. */
|
|
106
106
|
readonly gridDash: readonly number[];
|
|
107
|
+
/**
|
|
108
|
+
* Stroke for **session dividers** — the solid verticals a trading-time axis
|
|
109
|
+
* draws at each collapsed gap (session/day open). Optional; falls back to
|
|
110
|
+
* {@link grid}. Set it a touch stronger than the gridlines so a session
|
|
111
|
+
* boundary reads as structural.
|
|
112
|
+
*/
|
|
113
|
+
readonly sessionDivider?: string;
|
|
107
114
|
/**
|
|
108
115
|
* Typography for the axis **title** — the rotated y-axis unit strip and the
|
|
109
116
|
* x-axis label (distinct from the per-tick `label` colour above). Omit a
|
package/dist/theme.js
CHANGED
package/dist/tracker.d.ts
CHANGED
|
@@ -4,7 +4,41 @@
|
|
|
4
4
|
* themselves render as an SVG overlay in `Layers` (no cursor canvas); these
|
|
5
5
|
* helpers stay pure, so they're unit-tested directly.
|
|
6
6
|
*/
|
|
7
|
+
import type { Interval } from 'pond-ts';
|
|
7
8
|
import type { CursorMode } from './context.js';
|
|
9
|
+
/**
|
|
10
|
+
* The interval in the sorted, non-overlapping `buckets` that contains `t`
|
|
11
|
+
* (`begin ≤ t < end`), or `undefined` if `t` falls in no bucket. Binary search —
|
|
12
|
+
* the `region` cursor uses it to find the bucket under the pointer.
|
|
13
|
+
*/
|
|
14
|
+
export declare function bucketAt(buckets: readonly Interval[], t: number): Interval | undefined;
|
|
15
|
+
/**
|
|
16
|
+
* The `[start, end)` **span** a region cursor covers, in axis units (not pixels —
|
|
17
|
+
* the drag-release callback reports this):
|
|
18
|
+
*
|
|
19
|
+
* - **Snapping** (`buckets` non-empty, `t1` in a bucket): the bucket at `t1`, or —
|
|
20
|
+
* with a drag anchor `t2` — the union of the `t1` and `t2` buckets, so a drag
|
|
21
|
+
* extends **bucket by bucket** either direction. A `t2` in no bucket is ignored.
|
|
22
|
+
* - **Freeform** (`t1` in no bucket — e.g. no `cursorSequence` at all): a drag
|
|
23
|
+
* spans the raw `[t1, t2]`; without a drag (`t2` omitted) there's nothing to
|
|
24
|
+
* shade (the cursor renders as a plain line), so it returns `null`.
|
|
25
|
+
*/
|
|
26
|
+
export declare function regionSpan(buckets: readonly Interval[], t1: number, t2?: number): {
|
|
27
|
+
start: number;
|
|
28
|
+
end: number;
|
|
29
|
+
} | null;
|
|
30
|
+
/**
|
|
31
|
+
* The pixel band for the `region` cursor: the {@link regionSpan} for `t1` (and an
|
|
32
|
+
* optional drag anchor `t2`), its `[start, end)` mapped through `xScale` and
|
|
33
|
+
* clamped to `[0, plotWidth]`. Returns `null` when there's no span, or when the
|
|
34
|
+
* band has no width — including a span entirely in a **collapsed gap** on a
|
|
35
|
+
* trading-time scale (both edges map to the same pixel), so it draws nothing
|
|
36
|
+
* there rather than a zero-width sliver.
|
|
37
|
+
*/
|
|
38
|
+
export declare function bandRect(buckets: readonly Interval[], t1: number, xScale: (value: number) => number, plotWidth: number, t2?: number): {
|
|
39
|
+
x0: number;
|
|
40
|
+
x1: number;
|
|
41
|
+
} | null;
|
|
8
42
|
/** Default cursor mode — the synced vertical line (cursor enabled on the
|
|
9
43
|
* container by default; pair with an off-chart readout via `onTrackerChanged`). */
|
|
10
44
|
export declare const DEFAULT_CURSOR_MODE: CursorMode;
|
|
@@ -19,6 +53,9 @@ export declare function cursorParts(mode: CursorMode): {
|
|
|
19
53
|
readonly line: boolean;
|
|
20
54
|
readonly dots: boolean;
|
|
21
55
|
readonly chip: 'none' | 'inline' | 'flag' | 'axis';
|
|
56
|
+
/** `region` mode: a shaded **band** over the bucket under the pointer (from
|
|
57
|
+
* `cursorSequence`), drawn by `Layers`; no line/dots/chip of its own. */
|
|
58
|
+
readonly band: boolean;
|
|
22
59
|
};
|
|
23
60
|
/**
|
|
24
61
|
* The crosshair's plot-pixel x from the tracker inputs. A controlled
|
package/dist/tracker.js
CHANGED
|
@@ -4,6 +4,72 @@
|
|
|
4
4
|
* themselves render as an SVG overlay in `Layers` (no cursor canvas); these
|
|
5
5
|
* helpers stay pure, so they're unit-tested directly.
|
|
6
6
|
*/
|
|
7
|
+
/**
|
|
8
|
+
* The interval in the sorted, non-overlapping `buckets` that contains `t`
|
|
9
|
+
* (`begin ≤ t < end`), or `undefined` if `t` falls in no bucket. Binary search —
|
|
10
|
+
* the `region` cursor uses it to find the bucket under the pointer.
|
|
11
|
+
*/
|
|
12
|
+
export function bucketAt(buckets, t) {
|
|
13
|
+
let lo = 0;
|
|
14
|
+
let hi = buckets.length - 1;
|
|
15
|
+
while (lo <= hi) {
|
|
16
|
+
const mid = (lo + hi) >> 1;
|
|
17
|
+
const b = buckets[mid];
|
|
18
|
+
if (t < b.begin())
|
|
19
|
+
hi = mid - 1;
|
|
20
|
+
else if (t >= b.end())
|
|
21
|
+
lo = mid + 1;
|
|
22
|
+
else
|
|
23
|
+
return b;
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* The `[start, end)` **span** a region cursor covers, in axis units (not pixels —
|
|
29
|
+
* the drag-release callback reports this):
|
|
30
|
+
*
|
|
31
|
+
* - **Snapping** (`buckets` non-empty, `t1` in a bucket): the bucket at `t1`, or —
|
|
32
|
+
* with a drag anchor `t2` — the union of the `t1` and `t2` buckets, so a drag
|
|
33
|
+
* extends **bucket by bucket** either direction. A `t2` in no bucket is ignored.
|
|
34
|
+
* - **Freeform** (`t1` in no bucket — e.g. no `cursorSequence` at all): a drag
|
|
35
|
+
* spans the raw `[t1, t2]`; without a drag (`t2` omitted) there's nothing to
|
|
36
|
+
* shade (the cursor renders as a plain line), so it returns `null`.
|
|
37
|
+
*/
|
|
38
|
+
export function regionSpan(buckets, t1, t2) {
|
|
39
|
+
const a = bucketAt(buckets, t1);
|
|
40
|
+
if (a === undefined) {
|
|
41
|
+
// Freeform: no bucket under t1. A drag spans the raw [t1, t2]; a bare hover
|
|
42
|
+
// has nothing to shade (Layers draws a line for the degenerate region cursor).
|
|
43
|
+
return t2 === undefined
|
|
44
|
+
? null
|
|
45
|
+
: { start: Math.min(t1, t2), end: Math.max(t1, t2) };
|
|
46
|
+
}
|
|
47
|
+
if (t2 === undefined)
|
|
48
|
+
return { start: a.begin(), end: a.end() };
|
|
49
|
+
const b = bucketAt(buckets, t2);
|
|
50
|
+
if (b === undefined)
|
|
51
|
+
return { start: a.begin(), end: a.end() };
|
|
52
|
+
return {
|
|
53
|
+
start: Math.min(a.begin(), b.begin()),
|
|
54
|
+
end: Math.max(a.end(), b.end()),
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* The pixel band for the `region` cursor: the {@link regionSpan} for `t1` (and an
|
|
59
|
+
* optional drag anchor `t2`), its `[start, end)` mapped through `xScale` and
|
|
60
|
+
* clamped to `[0, plotWidth]`. Returns `null` when there's no span, or when the
|
|
61
|
+
* band has no width — including a span entirely in a **collapsed gap** on a
|
|
62
|
+
* trading-time scale (both edges map to the same pixel), so it draws nothing
|
|
63
|
+
* there rather than a zero-width sliver.
|
|
64
|
+
*/
|
|
65
|
+
export function bandRect(buckets, t1, xScale, plotWidth, t2) {
|
|
66
|
+
const span = regionSpan(buckets, t1, t2);
|
|
67
|
+
if (span === null)
|
|
68
|
+
return null;
|
|
69
|
+
const x0 = Math.max(0, xScale(span.start));
|
|
70
|
+
const x1 = Math.min(plotWidth, xScale(span.end));
|
|
71
|
+
return x1 > x0 ? { x0, x1 } : null;
|
|
72
|
+
}
|
|
7
73
|
/** Default cursor mode — the synced vertical line (cursor enabled on the
|
|
8
74
|
* container by default; pair with an off-chart readout via `onTrackerChanged`). */
|
|
9
75
|
export const DEFAULT_CURSOR_MODE = 'line';
|
|
@@ -15,22 +81,27 @@ export const DEFAULT_CURSOR_MODE = 'line';
|
|
|
15
81
|
* value flag stacked near the top of the row (drawn in `Layers`).
|
|
16
82
|
*/
|
|
17
83
|
export function cursorParts(mode) {
|
|
84
|
+
const base = { line: false, dots: false, chip: 'none', band: false };
|
|
18
85
|
switch (mode) {
|
|
19
86
|
case 'line':
|
|
20
|
-
return { line: true
|
|
87
|
+
return { ...base, line: true };
|
|
21
88
|
case 'point':
|
|
22
|
-
return {
|
|
89
|
+
return { ...base, dots: true };
|
|
23
90
|
case 'inline':
|
|
24
|
-
return {
|
|
91
|
+
return { ...base, dots: true, chip: 'inline' };
|
|
25
92
|
case 'flag':
|
|
26
|
-
return {
|
|
93
|
+
return { ...base, dots: true, chip: 'flag' };
|
|
27
94
|
case 'crosshair':
|
|
28
95
|
// A single reticle (not per-series): `Layers` draws the dashed vertical +
|
|
29
96
|
// full-width horizontal lines, the centre dot, and one value pill itself
|
|
30
97
|
// (so no generic line/dots here); the x-time pill is on `<XAxis>`.
|
|
31
|
-
return {
|
|
98
|
+
return { ...base, chip: 'axis' };
|
|
99
|
+
case 'region':
|
|
100
|
+
// A shaded band over the bucket under the pointer — `Layers` resolves the
|
|
101
|
+
// bucket from `cursorBuckets` and draws the rect (cropped through xScale).
|
|
102
|
+
return { ...base, band: true };
|
|
32
103
|
case 'none':
|
|
33
|
-
return {
|
|
104
|
+
return { ...base };
|
|
34
105
|
}
|
|
35
106
|
}
|
|
36
107
|
/**
|