@pond-ts/charts 0.42.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 +83 -1
- package/dist/BarChart.d.ts +24 -2
- package/dist/BarChart.js +82 -22
- package/dist/CategoryAxis.d.ts +16 -0
- package/dist/CategoryAxis.js +19 -0
- package/dist/ChartContainer.d.ts +48 -1
- package/dist/ChartContainer.js +78 -3
- package/dist/Layers.js +73 -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 +23 -6
- package/dist/bars.js +47 -15
- package/dist/context.d.ts +69 -13
- package/dist/data.d.ts +72 -0
- package/dist/data.js +67 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +8 -1
- package/dist/tracker.d.ts +37 -0
- package/dist/tracker.js +77 -6
- package/package.json +3 -3
package/dist/bars.js
CHANGED
|
@@ -172,9 +172,12 @@ export function barAt(cs, px, py, xScale, yScale, baseline, gapPx, minWidthPx) {
|
|
|
172
172
|
return null;
|
|
173
173
|
}
|
|
174
174
|
/**
|
|
175
|
-
* The `[min, max]` extent of the **value (stacked) axis
|
|
176
|
-
* where `maxTotal` is the tallest bin's summed finite
|
|
177
|
-
*
|
|
175
|
+
* The `[min, max]` extent of the **value (stacked) axis**. For a true multi-group
|
|
176
|
+
* stack it is `[0, maxTotal]`, where `maxTotal` is the tallest bin's summed finite
|
|
177
|
+
* non-negative segments. For a **single-group** series (`G === 1` — the plain /
|
|
178
|
+
* categorical bar case) it spans the values' own `[min, max]`, so a **negative**
|
|
179
|
+
* bar's floor is in the domain (segments below the baseline stay visible). `0` is
|
|
180
|
+
* always pulled in so the bars rest on a visible baseline (the bar analog of
|
|
178
181
|
* {@link barExtent}). An empty / all-gap series returns `[0, 1]` so the axis still
|
|
179
182
|
* has a usable domain. Feeds the y auto-fit for a vertical histogram, the x
|
|
180
183
|
* auto-fit for a horizontal one.
|
|
@@ -182,17 +185,32 @@ export function barAt(cs, px, py, xScale, yScale, baseline, gapPx, minWidthPx) {
|
|
|
182
185
|
export function stackValueExtent(ss) {
|
|
183
186
|
const G = ss.groups.length;
|
|
184
187
|
let max = 0;
|
|
188
|
+
let min = 0;
|
|
185
189
|
for (let b = 0; b < ss.length; b += 1) {
|
|
186
190
|
let cum = 0;
|
|
187
191
|
for (let g = 0; g < G; g += 1) {
|
|
188
192
|
const v = ss.values[b * G + g];
|
|
189
|
-
if (Number.isFinite(v)
|
|
190
|
-
|
|
193
|
+
if (!Number.isFinite(v))
|
|
194
|
+
continue;
|
|
195
|
+
if (G === 1) {
|
|
196
|
+
// Single-group: a bar honours its sign, so track both ends.
|
|
197
|
+
if (v > max)
|
|
198
|
+
max = v;
|
|
199
|
+
if (v < min)
|
|
200
|
+
min = v;
|
|
201
|
+
}
|
|
202
|
+
else if (v > 0) {
|
|
203
|
+
cum += v; // True stack: sum the positive segments.
|
|
204
|
+
}
|
|
191
205
|
}
|
|
192
206
|
if (cum > max)
|
|
193
207
|
max = cum;
|
|
194
208
|
}
|
|
195
|
-
|
|
209
|
+
// Empty / all-gap / all-zero → a usable unit domain; otherwise the real extent
|
|
210
|
+
// (with 0 pulled in via the `min`/`max` seeds above).
|
|
211
|
+
if (min === 0 && max === 0)
|
|
212
|
+
return [0, 1];
|
|
213
|
+
return [min, max];
|
|
196
214
|
}
|
|
197
215
|
/**
|
|
198
216
|
* The `[min, max]` extent of the **bin axis** — the first bin's `begin` to the
|
|
@@ -224,9 +242,14 @@ export function stackBinExtent(ss) {
|
|
|
224
242
|
export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, gapPx, minSpanPx) {
|
|
225
243
|
const G = ss.groups.length;
|
|
226
244
|
const v = ss.values[b * G + g];
|
|
227
|
-
// Skip non-finite
|
|
228
|
-
//
|
|
229
|
-
|
|
245
|
+
// Skip non-finite (a gap) or zero (a zero-extent rect that can't draw or be
|
|
246
|
+
// hit-tested). A **negative** value is a gap only in a true multi-group stack
|
|
247
|
+
// (`G > 1`) — stacking a negative segment is undefined. A **single-group**
|
|
248
|
+
// series (`G === 1`) is a plain bar: it honours its sign and draws from the
|
|
249
|
+
// baseline *down* to a negative value (the categorical row-read's P&L / delta
|
|
250
|
+
// case), so negatives are kept and the `Math.min/Math.max` below normalizes the
|
|
251
|
+
// below-baseline rect.
|
|
252
|
+
if (!Number.isFinite(v) || v === 0 || (v < 0 && G > 1))
|
|
230
253
|
return null;
|
|
231
254
|
if (orientation === 'vertical') {
|
|
232
255
|
const [x0, x1] = barSpanPx(ss.begin[b], ss.end[b], xScale, gapPx, minSpanPx);
|
|
@@ -242,8 +265,10 @@ export function segmentRect(ss, b, g, orientation, xScale, yScale, cumBefore, ga
|
|
|
242
265
|
/**
|
|
243
266
|
* Fill every segment of every bin in `ss`, stacking each bin's groups from the
|
|
244
267
|
* value baseline outward (bottom → top vertical, left → right horizontal). A gap
|
|
245
|
-
* (non-finite
|
|
246
|
-
* total, so the segments above it close the space
|
|
268
|
+
* (non-finite, or a negative segment of a true multi-group stack) is skipped and
|
|
269
|
+
* adds nothing to the running total, so the segments above it close the space; a
|
|
270
|
+
* single-group series draws its negative bars below the baseline (see
|
|
271
|
+
* {@link segmentRect}). A segment matching the current
|
|
247
272
|
* `selection` (same series `id`, bin `key` **and** group `label`) draws in its
|
|
248
273
|
* group's `highlight` **and** outlined; one matching `hover` draws in `highlight`
|
|
249
274
|
* without the outline; all others use the flat `fill`. `globalAlpha` carries the
|
|
@@ -265,20 +290,27 @@ export function drawStacks(ctx, ss, orientation, xScale, yScale, style, gapPx, m
|
|
|
265
290
|
if (rect === null)
|
|
266
291
|
continue;
|
|
267
292
|
const [x0, x1, yTop, yBottom] = rect;
|
|
293
|
+
// With `marks` (the categorical axis), match on the stable per-bin name so a
|
|
294
|
+
// pinned selection survives a column reorder; otherwise on the sample `key`
|
|
295
|
+
// (begin) + group `label`, as a time / value stack does.
|
|
296
|
+
const stableMark = ss.marks?.[b];
|
|
268
297
|
const matches = (m) => m !== null &&
|
|
269
298
|
m.id === seriesId &&
|
|
270
|
-
|
|
271
|
-
|
|
299
|
+
(stableMark !== undefined
|
|
300
|
+
? m.mark === stableMark
|
|
301
|
+
: m.key === ss.begin[b] && m.label === ss.groups[g]);
|
|
272
302
|
const selected = matches(selection);
|
|
273
303
|
const isHovered = matches(hover);
|
|
274
304
|
// A hovered / selected segment pops to full opacity in its own colour; a
|
|
275
305
|
// resting one draws at the shared alpha.
|
|
276
306
|
ctx.globalAlpha = selected || isHovered ? 1 : style.opacity;
|
|
277
|
-
|
|
307
|
+
// A per-bin colour (the single-series band case) overrides the group fill.
|
|
308
|
+
const fill = style.binFills?.[b] ?? style.fills[g];
|
|
309
|
+
ctx.fillStyle = fill;
|
|
278
310
|
ctx.fillRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
279
311
|
if (selected) {
|
|
280
312
|
ctx.lineWidth = style.outlineWidth;
|
|
281
|
-
ctx.strokeStyle =
|
|
313
|
+
ctx.strokeStyle = fill;
|
|
282
314
|
ctx.strokeRect(x0, yTop, x1 - x0, yBottom - yTop);
|
|
283
315
|
}
|
|
284
316
|
}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import type { ScaleLinear, ScaleTime } from 'd3-scale';
|
|
2
2
|
import type { ChartTheme } from './theme.js';
|
|
3
3
|
import type { AxisFormat } from './format.js';
|
|
4
|
+
import type { Interval, TimeRange } from 'pond-ts';
|
|
4
5
|
import type { TradingTimeScale, DiscontinuityProvider } from './tradingTimeScale.js';
|
|
6
|
+
import type { ScaleBand } from './bandScale.js';
|
|
5
7
|
/**
|
|
6
8
|
* The frame a {@link ChartContainer} provides to its rows and the time axis.
|
|
7
9
|
* The container owns the **shared x geometry**: each side is split into *slots*
|
|
@@ -64,6 +66,38 @@ export interface ContainerFrame {
|
|
|
64
66
|
* (`yScale.invert`). The x always snaps to the data grid either way.
|
|
65
67
|
*/
|
|
66
68
|
readonly crosshairSnap: boolean;
|
|
69
|
+
/**
|
|
70
|
+
* `cursor="region"` buckets — the intervals (from `cursorSequence`) realized
|
|
71
|
+
* over the current view, sorted + non-overlapping. `Layers` finds the one under
|
|
72
|
+
* the pointer and shades it (mapped through `xScale`, so on a trading-time axis
|
|
73
|
+
* the closed part of the bucket collapses). `undefined` when no `cursorSequence`
|
|
74
|
+
* is set.
|
|
75
|
+
*/
|
|
76
|
+
readonly cursorBuckets: readonly Interval[] | undefined;
|
|
77
|
+
/**
|
|
78
|
+
* The `region`-cursor **drag anchor** (epoch ms), or `null` when not dragging.
|
|
79
|
+
* A drag on a region cursor (only when {@link onRegionSelect} is set) records
|
|
80
|
+
* the press time here; the band then spans from the anchor's bucket to the
|
|
81
|
+
* pointer's bucket (extending bucket by bucket). Cleared on release.
|
|
82
|
+
*/
|
|
83
|
+
readonly regionAnchor: number | null;
|
|
84
|
+
/** Set / clear the region-drag anchor (see {@link regionAnchor}). */
|
|
85
|
+
setRegionAnchor(time: number | null): void;
|
|
86
|
+
/**
|
|
87
|
+
* One-shot callback fired when a `region`-cursor **drag** is released, with the
|
|
88
|
+
* selected `[start, end)` `TimeRange` (snapped to the `cursorSequence` buckets).
|
|
89
|
+
* Providing it is what makes the region cursor **draggable**; the cursor does
|
|
90
|
+
* not keep the range (it reverts to the single-bucket highlight). Typical use:
|
|
91
|
+
* zoom the view to the returned range.
|
|
92
|
+
*/
|
|
93
|
+
readonly onRegionSelect: ((range: TimeRange) => void) | undefined;
|
|
94
|
+
/**
|
|
95
|
+
* Require a modifier key held to start a region-drag — set to `'shift'` to make
|
|
96
|
+
* plain drag **pan** and **shift**-drag select, when `panZoom` is on. Only
|
|
97
|
+
* enforced while pan is enabled (with no pan there's no gesture to share, so the
|
|
98
|
+
* modifier is optional). `undefined` ⇒ a region-drag preempts pan.
|
|
99
|
+
*/
|
|
100
|
+
readonly regionSelectModifier: 'shift' | undefined;
|
|
67
101
|
/**
|
|
68
102
|
* The selected mark, or `null`. Shared across rows (single selection). A layer
|
|
69
103
|
* highlights the mark matching the selection's series **`id`** and the clicked
|
|
@@ -139,7 +173,7 @@ export interface ContainerFrame {
|
|
|
139
173
|
* `invert` / `ticks` / `tickFormat` surface, but the mapping runs through
|
|
140
174
|
* trading time so closed-market gaps collapse (see {@link discontinuities}).
|
|
141
175
|
*/
|
|
142
|
-
readonly xScale: ScaleTime<number, number> | ScaleLinear<number, number> | TradingTimeScale;
|
|
176
|
+
readonly xScale: ScaleTime<number, number> | ScaleLinear<number, number> | TradingTimeScale | ScaleBand;
|
|
143
177
|
/**
|
|
144
178
|
* The discontinuity provider backing a **trading-time** x axis, if one was
|
|
145
179
|
* supplied to the container — closed-market time (weekends, holidays,
|
|
@@ -149,12 +183,13 @@ export interface ContainerFrame {
|
|
|
149
183
|
*/
|
|
150
184
|
readonly discontinuities?: DiscontinuityProvider | undefined;
|
|
151
185
|
/**
|
|
152
|
-
* The resolved kind of the shared x scale — `'time'` (a `scaleTime`)
|
|
153
|
-
* `'value'` (a `scaleLinear`),
|
|
154
|
-
*
|
|
155
|
-
*
|
|
186
|
+
* The resolved kind of the shared x scale — `'time'` (a `scaleTime`),
|
|
187
|
+
* `'value'` (a `scaleLinear`), or `'category'` (a {@link ScaleBand}: an ordinal
|
|
188
|
+
* column-domain axis, one slot per category). Inferred from the layers' data.
|
|
189
|
+
* `<XAxis>` reads it to pick its default tick formatter (time / number / the
|
|
190
|
+
* category label), and the cursor readout to format the x position.
|
|
156
191
|
*/
|
|
157
|
-
readonly xKind: 'time' | 'value';
|
|
192
|
+
readonly xKind: 'time' | 'value' | 'category';
|
|
158
193
|
/** Pan/zoom enabled — the plot drag-pans and wheel-zooms the shared time range. */
|
|
159
194
|
readonly panZoom: boolean;
|
|
160
195
|
/** Minimum visible duration (ms) — the zoom-in floor. */
|
|
@@ -314,17 +349,26 @@ export interface RowLayer {
|
|
|
314
349
|
yExtent(): [number, number] | null;
|
|
315
350
|
/**
|
|
316
351
|
* The **kind of x axis** this layer's data lives on — `'time'` for a
|
|
317
|
-
* `TimeSeries`, `'value'` for a `ValueSeries
|
|
318
|
-
*
|
|
319
|
-
*
|
|
352
|
+
* `TimeSeries`, `'value'` for a `ValueSeries`, `'category'` for a categorical
|
|
353
|
+
* (ordinal column-domain) layer. The container infers the one shared x scale
|
|
354
|
+
* from its layers (all must agree — a mix is an error), so the axis kind never
|
|
355
|
+
* needs declaring. See {@link ContainerFrame.xScale}.
|
|
320
356
|
*/
|
|
321
|
-
readonly xKind: 'time' | 'value';
|
|
357
|
+
readonly xKind: 'time' | 'value' | 'category';
|
|
322
358
|
/**
|
|
323
359
|
* This layer's `[min, max]` along the **x** axis (the key / value-axis extent),
|
|
324
360
|
* or `null` if empty. The container unions these to auto-fit the shared x
|
|
325
|
-
* domain when no explicit `range` is given.
|
|
361
|
+
* domain when no explicit `range` is given. For a `'category'` layer this is
|
|
362
|
+
* the slot extent `[0, n]` (n = category count).
|
|
326
363
|
*/
|
|
327
364
|
xExtent(): readonly [number, number] | null;
|
|
365
|
+
/**
|
|
366
|
+
* A `'category'` layer's ordered category names (the ordinal axis domain the
|
|
367
|
+
* container builds a {@link ScaleBand} + label formatter from). `undefined` /
|
|
368
|
+
* absent for a `'time'` or `'value'` layer. Category layers in one container
|
|
369
|
+
* must agree on this list (a mix is an error), the same way {@link xKind} must.
|
|
370
|
+
*/
|
|
371
|
+
xCategories?(): readonly string[] | null;
|
|
328
372
|
/**
|
|
329
373
|
* The layer's value(s) at `time` — the nearest sample — for the scrub tracker:
|
|
330
374
|
* one for a line, two (lower/upper) for a band, empty at a gap. Each carries
|
|
@@ -394,8 +438,10 @@ export interface CursorFlag {
|
|
|
394
438
|
*/
|
|
395
439
|
export interface TrackerSource {
|
|
396
440
|
sampleAt(time: number): readonly TrackerSample[];
|
|
397
|
-
readonly xKind: 'time' | 'value';
|
|
441
|
+
readonly xKind: 'time' | 'value' | 'category';
|
|
398
442
|
xExtent(): readonly [number, number] | null;
|
|
443
|
+
/** A `'category'` source's ordered category names (see {@link RowLayer.xCategories}). */
|
|
444
|
+
xCategories?(): readonly string[] | null;
|
|
399
445
|
}
|
|
400
446
|
/**
|
|
401
447
|
* One selection — what {@link RowLayer.hitTest} returns and `onSelect` reports.
|
|
@@ -423,6 +469,16 @@ export interface SelectInfo {
|
|
|
423
469
|
readonly color: string;
|
|
424
470
|
/** Display label (`as` ?? column ?? id) — labels the selection in a readout. */
|
|
425
471
|
readonly label: string;
|
|
472
|
+
/**
|
|
473
|
+
* An optional **stable per-mark identity within the layer** — a *category's
|
|
474
|
+
* column name* on the categorical axis, where every bar shares the layer's
|
|
475
|
+
* `id` but each column needs its own stable handle. When present, the
|
|
476
|
+
* highlight match + controlled `selected` echo key on `(id, mark)` instead of
|
|
477
|
+
* the sample `key`, so a pinned selection survives a column reorder / data
|
|
478
|
+
* update (the slot index is not stable; the column name is). `undefined` for
|
|
479
|
+
* marks whose sample `key` is already their identity (a time / value bar).
|
|
480
|
+
*/
|
|
481
|
+
readonly mark?: string;
|
|
426
482
|
}
|
|
427
483
|
/** The hover snapshot handed to `onTrackerChanged` — the cursor time + every
|
|
428
484
|
* series' value there, so a consumer can render the readout outside the chart. */
|
|
@@ -446,7 +502,7 @@ export interface TrackerInfo {
|
|
|
446
502
|
* time pinned to the x-axis. The ChartIQ / trading-terminal readout. Values
|
|
447
503
|
* snap to the series (the axis pills read like ticks), not the raw mouse Y.
|
|
448
504
|
*/
|
|
449
|
-
export type CursorMode = 'none' | 'line' | 'point' | 'inline' | 'flag' | 'crosshair';
|
|
505
|
+
export type CursorMode = 'none' | 'line' | 'point' | 'inline' | 'flag' | 'crosshair' | 'region';
|
|
450
506
|
/** A registered layer plus the axis id it draws against. */
|
|
451
507
|
export interface LayerEntry {
|
|
452
508
|
readonly layer: RowLayer;
|
package/dist/data.d.ts
CHANGED
|
@@ -122,6 +122,14 @@ export interface StackedBarSeries {
|
|
|
122
122
|
readonly groups: readonly string[];
|
|
123
123
|
readonly values: Float64Array;
|
|
124
124
|
readonly length: number;
|
|
125
|
+
/**
|
|
126
|
+
* Optional **stable per-bin identity** — `marks[b]` names bin `b` (a category's
|
|
127
|
+
* column name on the categorical axis). When present, the draw / hit-test /
|
|
128
|
+
* selection key on this name instead of the bin's `begin` slot index, so a
|
|
129
|
+
* pinned selection survives a column reorder (the slot index is not stable).
|
|
130
|
+
* `undefined` for a time / value series whose `begin` is already stable.
|
|
131
|
+
*/
|
|
132
|
+
readonly marks?: readonly string[];
|
|
125
133
|
}
|
|
126
134
|
/** The five quantile column names a {@link boxFromTimeSeries} reads, in order. */
|
|
127
135
|
export interface BoxColumns {
|
|
@@ -343,4 +351,68 @@ export interface StacksFromBinsOptions {
|
|
|
343
351
|
* A missing / non-finite aggregate reads as a gap (`NaN`).
|
|
344
352
|
*/
|
|
345
353
|
export declare function stacksFromBins(bins: readonly BinRecord[], columns: readonly string[], options?: StacksFromBinsOptions): StackedBarSeries;
|
|
354
|
+
/**
|
|
355
|
+
* One category's `{ label, value }` for a categorical bar chart — the row-read /
|
|
356
|
+
* transpose view's `(columnName, cell)` pair (categorical-axis RFC, Phase 1). An
|
|
357
|
+
* ordered list of these is the explicit categorical data source; Phase 2's
|
|
358
|
+
* transpose reader produces the same list from a wide series' row.
|
|
359
|
+
*
|
|
360
|
+
* **Labels are the stable identity** — the axis maps each `label` to a slot and
|
|
361
|
+
* selection/highlight key on it (so a pick survives a reorder). They must be
|
|
362
|
+
* **unique** within the list: two categories sharing a label collapse to one axis
|
|
363
|
+
* tick and both highlight together on a pick. The transpose reader satisfies this
|
|
364
|
+
* for free (a series' column names are unique); only a hand-built list can break
|
|
365
|
+
* it. `value` may be **negative** — a single-series category bar draws it below
|
|
366
|
+
* the baseline (the P&L / delta case).
|
|
367
|
+
*/
|
|
368
|
+
export interface CategoryDatum {
|
|
369
|
+
readonly label: string;
|
|
370
|
+
readonly value: number;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Build a {@link StackedBarSeries} (single group, `G === 1`) from an ordered list
|
|
374
|
+
* of `{ label, value }` categories — one **unit slot** `[i, i+1]` per category, in
|
|
375
|
+
* order. This is the categorical row-read's geometry: the slots are ordinal
|
|
376
|
+
* indices (the bar's pixel span comes from the container's {@link ScaleBand}), and
|
|
377
|
+
* the `label`s become the axis's ordered category names (`xCategories`). A
|
|
378
|
+
* non-finite value reads as a gap (`NaN`). Reuses the shipped stacked geometry —
|
|
379
|
+
* no new draw path.
|
|
380
|
+
*/
|
|
381
|
+
export declare function categoryStack(records: readonly CategoryDatum[]): StackedBarSeries;
|
|
382
|
+
/** Which row {@link transposeRow} reads across. */
|
|
383
|
+
export type RowAt = 'first' | 'last' | number | {
|
|
384
|
+
readonly time: number;
|
|
385
|
+
};
|
|
386
|
+
/** Options for {@link transposeRow}. */
|
|
387
|
+
export interface TransposeRowOptions {
|
|
388
|
+
/**
|
|
389
|
+
* Which row to read across. **Default `'last'`** — the head / latest row (the
|
|
390
|
+
* live snapshot). `'first'`, an **index** (negative from the end), or
|
|
391
|
+
* `{ time }` for the row nearest a key.
|
|
392
|
+
*/
|
|
393
|
+
readonly at?: RowAt;
|
|
394
|
+
/**
|
|
395
|
+
* The columns to lay on the axis, **in order** — a declared / bounded set (a
|
|
396
|
+
* watchlist, or a top-N computed upstream; the RFC §7 "bound in the data layer"
|
|
397
|
+
* stance). Omit to use **every numeric value column** of the series, in schema
|
|
398
|
+
* order. A named column that's missing / non-numeric in the row reads as a gap.
|
|
399
|
+
*/
|
|
400
|
+
readonly columns?: readonly string[];
|
|
401
|
+
}
|
|
402
|
+
/**
|
|
403
|
+
* **The transpose reader** (categorical-axis RFC, Phase 1 PR2): read **one row**
|
|
404
|
+
* of a wide `TimeSeries` **across** — its columns become the categories, that
|
|
405
|
+
* row's cells the values — for `<BarChart categories={…}>`. This is "columns on
|
|
406
|
+
* x": the schema's numeric columns (a `pivotByGroup` output's per-group columns,
|
|
407
|
+
* a vol term structure's per-expiry columns, …) laid out at one instant.
|
|
408
|
+
*
|
|
409
|
+
* The row is picked the ordinary way (`options.at`, default the **head row**):
|
|
410
|
+
* `series.last()` / `.first()` / `.at(index)` / `.nearest(time)`. So "which row"
|
|
411
|
+
* is just row selection — the live snapshot is the head row; a static report
|
|
412
|
+
* pins a row by index or time. (Binding the row to a scrubbing time cursor is a
|
|
413
|
+
* later phase.) Pass `options.columns` to bound / order the category set; omit it
|
|
414
|
+
* to take every numeric value column. An empty series (or a row past the ends)
|
|
415
|
+
* yields `[]`; a missing / non-numeric cell reads as a gap (`NaN`).
|
|
416
|
+
*/
|
|
417
|
+
export declare function transposeRow<S extends SeriesSchema>(series: TimeSeries<S>, options?: TransposeRowOptions): CategoryDatum[];
|
|
346
418
|
//# sourceMappingURL=data.d.ts.map
|
package/dist/data.js
CHANGED
|
@@ -452,4 +452,71 @@ export function stacksFromBins(bins, columns, options = {}) {
|
|
|
452
452
|
}
|
|
453
453
|
return { begin, end, groups: columns, values, length: n };
|
|
454
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
|
+
}
|
|
455
522
|
//# sourceMappingURL=data.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';
|
|
@@ -47,13 +48,15 @@ export type { CandlestickProps } from './Candlestick.js';
|
|
|
47
48
|
export type { CandleVariant, ColorBy } from './ohlc.js';
|
|
48
49
|
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
49
50
|
export type { TradingTimeScale, DiscontinuityProvider, } from './tradingTimeScale.js';
|
|
51
|
+
export { scaleBand } from './bandScale.js';
|
|
52
|
+
export type { ScaleBand } from './bandScale.js';
|
|
50
53
|
export { Region, Baseline, Marker } from './annotations.js';
|
|
51
54
|
export type { RegionProps, BaselineProps, MarkerProps } from './annotations.js';
|
|
52
55
|
export type { AnnotationKind, CreateSpec } from './context.js';
|
|
53
56
|
export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
54
57
|
export type { YAxisIndicatorProps, LiveValue } from './indicators.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';
|
|
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';
|
|
57
60
|
export type { Orientation } from './bars.js';
|
|
58
61
|
export type { RadiusEncoding, ColorEncoding } from './encoding.js';
|
|
59
62
|
export type { Curve } from './curve.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';
|
|
@@ -31,6 +32,8 @@ export { BoxPlot } from './BoxPlot.js';
|
|
|
31
32
|
export { BarChart } from './BarChart.js';
|
|
32
33
|
export { Candlestick } from './Candlestick.js';
|
|
33
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';
|
|
34
37
|
// Annotations — user-authored marks in the turquoise register (distinct from the
|
|
35
38
|
// data): a shaded span, a horizontal value line, a vertical x line.
|
|
36
39
|
export { Region, Baseline, Marker } from './annotations.js';
|
|
@@ -40,7 +43,11 @@ export { YAxisIndicator, createLiveValue } from './indicators.js';
|
|
|
40
43
|
export { fromTimeSeries, bandFromTimeSeries, boxFromTimeSeries, barsFromTimeSeries, ohlcFromTimeSeries,
|
|
41
44
|
// Stacked / histogram readers — assemble a StackedBarSeries from pond's own
|
|
42
45
|
// aggregation output: a Map of grouped series, a wide series, or byColumn bins.
|
|
43
|
-
stacksFromGroups, stacksFromColumns, stacksFromBins,
|
|
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';
|
|
44
51
|
export { defaultTheme, estelaTheme } from './theme.js';
|
|
45
52
|
// CSS-custom-property → ChartTheme bridge: build a theme from a design system's
|
|
46
53
|
// tokens (`cssVarTheme`), and a hook that re-resolves it on a `data-theme`
|
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
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.43.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Canvas-rendered, streaming-first time-series charts for pond-ts",
|
|
6
6
|
"license": "MIT",
|
|
@@ -38,8 +38,8 @@
|
|
|
38
38
|
"perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
|
-
"@pond-ts/react": "^0.
|
|
42
|
-
"pond-ts": "^0.
|
|
41
|
+
"@pond-ts/react": "^0.43.0",
|
|
42
|
+
"pond-ts": "^0.43.0",
|
|
43
43
|
"react": "^18.0.0 || ^19.0.0"
|
|
44
44
|
},
|
|
45
45
|
"devDependencies": {
|