@pond-ts/charts 0.42.0 → 0.44.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 +164 -1
- package/dist/BarChart.d.ts +24 -2
- package/dist/BarChart.js +106 -23
- package/dist/BoxPlot.d.ts +80 -28
- package/dist/BoxPlot.js +67 -40
- package/dist/CategoryAxis.d.ts +16 -0
- package/dist/CategoryAxis.js +19 -0
- package/dist/ChartContainer.d.ts +56 -1
- package/dist/ChartContainer.js +101 -3
- package/dist/Layers.js +77 -5
- package/dist/ScatterChart.d.ts +33 -6
- package/dist/ScatterChart.js +36 -16
- 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/box.d.ts +26 -9
- package/dist/box.js +86 -42
- package/dist/context.d.ts +88 -13
- package/dist/data.d.ts +145 -30
- package/dist/data.js +146 -21
- package/dist/index.d.ts +5 -2
- package/dist/index.js +8 -1
- package/dist/scatter.d.ts +2 -2
- package/dist/scatter.js +8 -5
- package/dist/tracker.d.ts +37 -0
- package/dist/tracker.js +77 -6
- package/package.json +3 -3
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
|
|
|
8
8
|
tag, so this file covers them all. Pre-1.0: minor bumps may include new features
|
|
9
9
|
and type-level changes; patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.44.0...HEAD
|
|
12
|
+
[0.44.0]: https://github.com/pjm17971/pond-ts/compare/v0.43.0...v0.44.0
|
|
13
|
+
[0.43.0]: https://github.com/pjm17971/pond-ts/compare/v0.42.0...v0.43.0
|
|
12
14
|
[0.42.0]: https://github.com/pjm17971/pond-ts/compare/v0.41.0...v0.42.0
|
|
13
15
|
[0.41.0]: https://github.com/pjm17971/pond-ts/compare/v0.40.0...v0.41.0
|
|
14
16
|
[0.40.0]: https://github.com/pjm17971/pond-ts/compare/v0.39.0...v0.40.0
|
|
@@ -40,6 +42,157 @@ and type-level changes; patch bumps are strictly additive.
|
|
|
40
42
|
|
|
41
43
|
## [Unreleased]
|
|
42
44
|
|
|
45
|
+
## [0.44.0] — 2026-07-11
|
|
46
|
+
|
|
47
|
+
The **value-axis charts** release: cross-sectional data (a volatility smile keyed
|
|
48
|
+
by strike) becomes a first-class charting surface. `ValueSeries.fromColumns` is
|
|
49
|
+
the direct columnar door; `<ScatterChart>` and `<BoxPlot>` join `<LineChart>` on
|
|
50
|
+
the value axis; `<BoxPlot>` gains range-only (bid→ask) marks, `offset` pairing,
|
|
51
|
+
and `capWidth`; and the region cursor works on value axes and snaps to histogram
|
|
52
|
+
bins.
|
|
53
|
+
|
|
54
|
+
### Added
|
|
55
|
+
|
|
56
|
+
- **`<BoxPlot>` finished for the value axis + range-only marks** (`@pond-ts/charts`).
|
|
57
|
+
Four coordinated changes, driven by the volatility smile's per-strike bid/ask IV
|
|
58
|
+
segments (`docs/notes/vol-smile-followups-2026-07.md` §1):
|
|
59
|
+
- **Accepts a `ValueSeries`** (`series.byValue('strike')` /
|
|
60
|
+
`ValueSeries.fromColumns`) — boxes on a value axis, the same instanceof branch
|
|
61
|
+
as `<LineChart>` / `<ScatterChart>`. The box **width** now comes from neighbour
|
|
62
|
+
spacing for a **point** key (a `ValueSeries`, or a point-keyed `TimeSeries`) —
|
|
63
|
+
like bars/candles — instead of collapsing to the 1px floor; an interval-keyed
|
|
64
|
+
`TimeSeries` still uses its `[begin, end)`.
|
|
65
|
+
- **Optional `q1`/`median`/`q3`** — omit `q1`+`q3` for a **range-only** box: a
|
|
66
|
+
whisker-only `lower→upper` segment, no body (a bid→ask IV mark honestly named,
|
|
67
|
+
not a candlestick abuse). Omitting exactly one of `q1`/`q3` throws.
|
|
68
|
+
- **`offset` prop** (`<BoxPlot>` and `<ScatterChart>`) — a **pixel** shift for
|
|
69
|
+
pairing same-key marks (call/put at one strike) side by side, zoom-stable. On
|
|
70
|
+
the scatter it moves the draw **and** the click hit-test together; on the box
|
|
71
|
+
the readout hit-tests in un-shifted data space (keep the offset small).
|
|
72
|
+
- **`capWidth` prop** (`<BoxPlot>`) — the whisker end-cap width in **pixels**
|
|
73
|
+
(else half the box width). A small fixed cap keeps two `offset`-paired marks'
|
|
74
|
+
T-bars from overlapping when the value-axis slot is wide; clamped to the box
|
|
75
|
+
width, `'whisker'` shape only.
|
|
76
|
+
- **Readout labels** carry the series' `as` identity (`iv upper`, `iv median`)
|
|
77
|
+
when set, instead of bare column names — the `as ?? column` convention
|
|
78
|
+
Line/Scatter already use.
|
|
79
|
+
- **`<ScatterChart>` accepts a `ValueSeries`** (`@pond-ts/charts`) — scatter
|
|
80
|
+
marks on the value axis, the same instanceof-branched adapter as
|
|
81
|
+
`<LineChart>` (the container infers the x kind from the data). The
|
|
82
|
+
data-driven `radius` / `color` encodings work unchanged on a value axis;
|
|
83
|
+
the per-point `label` reads through a new columnar branch (a `ValueSeries`
|
|
84
|
+
has no per-row events) — IV marks keyed by strike with open-interest
|
|
85
|
+
radius is the driving composition (vol smile). New value-axis Storybook
|
|
86
|
+
fan-out (`ValueAxis` / `ValueAxisEncoded` / `ValueAxisSmile` /
|
|
87
|
+
`ValueAxisFlag`) + Linux visual baselines.
|
|
88
|
+
|
|
89
|
+
- **`ValueSeries.fromColumns({ name, schema, columns, sort? })`** (`pond-ts`) —
|
|
90
|
+
the direct columnar door into value-land, for data that is _natively_
|
|
91
|
+
value-keyed (cross-sectional): an options chain keyed by strike, a spectrum
|
|
92
|
+
keyed by frequency. Exact `TimeSeries.fromColumns` contract with the axis in
|
|
93
|
+
place of time — same polymorphic `number[]` / `Float64Array` inputs, same
|
|
94
|
+
zero-copy adoption, same stable opt-in `sort`, same gap rule — the two doors
|
|
95
|
+
share one ingest engine. Previously cross-sectional callers had to launder
|
|
96
|
+
the axis through a fake `time` column (`TimeSeries.fromColumns` +
|
|
97
|
+
`byValue`); that detour is no longer needed.
|
|
98
|
+
|
|
99
|
+
- **The region cursor snaps to a histogram's bins** (`@pond-ts/charts`). On a
|
|
100
|
+
`<BarChart>` histogram, `cursor="region"` now snaps **bar by bar** with no
|
|
101
|
+
`cursorSequence`: hovering highlights the bar under the pointer, a drag extends
|
|
102
|
+
across whole bars, and `onRegionSelect` reports the selected bin range
|
|
103
|
+
`[lo, hi]` at the bar edges. The bar layer publishes its `[begin, end)` spans
|
|
104
|
+
(a new internal `binIntervals` channel) as the region cursor's snap buckets —
|
|
105
|
+
the same machinery a `cursorSequence` drives on a time axis, so it also covers a
|
|
106
|
+
time-axis histogram. Only a **vertical** bar layer on a continuous (time /
|
|
107
|
+
value) x axis publishes bins; a horizontal chart (value on x) and a categorical
|
|
108
|
+
axis stay freeform / excluded. An explicit `cursorSequence` still takes
|
|
109
|
+
precedence. New `HistogramBins` region-cursor story.
|
|
110
|
+
|
|
111
|
+
### Changed
|
|
112
|
+
|
|
113
|
+
- **BREAKING (`@pond-ts/charts`): the region cursor works on a value x-axis, and
|
|
114
|
+
`onRegionSelect` reports a neutral `[lo, hi]` pair.** The drag-select callback
|
|
115
|
+
fired a `TimeRange`; it now fires `readonly [number, number]` in **axis units** —
|
|
116
|
+
epoch ms on a time axis, the axis value (strike, distance, …) on a value axis —
|
|
117
|
+
mirroring the container's polymorphic `range` input (which never takes the axis
|
|
118
|
+
_kind_ from its value). A time-axis consumer that wants a `TimeRange` builds one
|
|
119
|
+
from the pair (`new TimeRange({ start: lo, end: hi })`). The cursor itself is
|
|
120
|
+
ungated from time-only to any **continuous** x-axis (time **or** value; a
|
|
121
|
+
**category** axis stays excluded — an ordinal-slot select is a different gesture).
|
|
122
|
+
Bucket **snapping** stays time-only (a `cursorSequence` bucket is a time
|
|
123
|
+
interval), so a value axis is always **freeform** (hover line + raw-span drag).
|
|
124
|
+
|
|
125
|
+
## [0.43.0] — 2026-07-11
|
|
126
|
+
|
|
127
|
+
The **categorical x-axis** release: a first-class ordinal band scale (ticker /
|
|
128
|
+
account / expiry on x — the transpose view of a time series), plus the charts
|
|
129
|
+
**interaction** wave that landed after v0.42.0 was cut — the region cursor and
|
|
130
|
+
its drag-to-select gesture, per-bin band colour, and annotation edges that snap
|
|
131
|
+
to session boundaries.
|
|
132
|
+
|
|
133
|
+
### Added
|
|
134
|
+
|
|
135
|
+
- **`@pond-ts/charts`: a first-class categorical x-axis.** `<BarChart
|
|
136
|
+
categories={[{ label, value }]}>` draws one bar per category on an ordinal
|
|
137
|
+
**band scale** (the transpose view's "columns on x" — ticker / account / zone
|
|
138
|
+
on x). The container infers `xKind:'category'` and builds a `scaleBand` over the
|
|
139
|
+
labels; `<CategoryAxis>` ticks once per category. Colour per category via
|
|
140
|
+
`binColors`; selection reports the category name. **Negative** category values
|
|
141
|
+
draw below the baseline (the P&L / delta case) — a single-series category bar
|
|
142
|
+
honours its sign. New exports `scaleBand` / `ScaleBand`, `CategoryAxis`,
|
|
143
|
+
`categoryStack` / `CategoryDatum`. Additive — a new x-kind alongside time /
|
|
144
|
+
value; existing charts are unchanged. First slice of the categorical-axis RFC
|
|
145
|
+
(`docs/rfcs/categorical-axis.md`, Phase 1).
|
|
146
|
+
- **`@pond-ts/charts`: `transposeRow`** — read one **row** of a wide `TimeSeries`
|
|
147
|
+
**across** into `{ label, value }[]` for `<BarChart categories>`: the schema's
|
|
148
|
+
numeric columns (a `pivotByGroup` output's per-group columns, a term
|
|
149
|
+
structure's per-expiry columns) become the categories at one instant. Pick the
|
|
150
|
+
row with `at` (`'last'` — the head/live row — by default; `'first'`, an index,
|
|
151
|
+
or `{ time }`); bound / order the set with `columns`.
|
|
152
|
+
- **`@pond-ts/charts`: stable per-column selection identity.** `SelectInfo` gains
|
|
153
|
+
an optional **`mark`** — a stable per-mark identity within a layer. A categorical
|
|
154
|
+
bar reports its **column name** as `mark`, and a controlled `selected` echo /
|
|
155
|
+
the highlight match key on `(id, mark)`, so a pinned selection survives a column
|
|
156
|
+
reorder (the slot index doesn't; the name does). Additive — `mark` is
|
|
157
|
+
`undefined` for a time / value bar (whose sample `key` is already its identity).
|
|
158
|
+
Plus a category-axis **label policy**: a dense axis thins (keeps every k-th) and
|
|
159
|
+
ellipsis-truncates its labels so they stay legible while every bar draws.
|
|
160
|
+
- **`@pond-ts/charts`: region cursor (`cursor="region"`).** A shaded **band**
|
|
161
|
+
highlights the bucket under the pointer, bucketed by a new **`cursorSequence`**
|
|
162
|
+
prop — a `Sequence` (`Sequence.every('15m')`, `Sequence.calendar('week')`)
|
|
163
|
+
realized over the view, or a `BoundedSequence` (a `TradingCalendar`'s
|
|
164
|
+
`sessionSequence()` / `barSequence()`) used as-is. The band maps through the x
|
|
165
|
+
scale, so on a trading-time axis the closed part of a bucket collapses (crops
|
|
166
|
+
to live sessions). Time-axis only (a no-op on a value axis). (#409, #413)
|
|
167
|
+
- **`@pond-ts/charts`: draggable region cursor → one-shot select.** Opt-in
|
|
168
|
+
**`onRegionSelect?: (range: TimeRange) => void`** makes the region cursor
|
|
169
|
+
draggable: the band extends bucket by bucket and fires **once** on release
|
|
170
|
+
with the selected `[start, end)` `TimeRange` (the cursor doesn't keep it —
|
|
171
|
+
typical use is to zoom the view). With **no `cursorSequence`** it degenerates
|
|
172
|
+
to a hover **line** + **freeform** drag. **`regionSelectModifier="shift"`**
|
|
173
|
+
resolves the gesture conflict with `panZoom` (plain drag pans, shift-drag
|
|
174
|
+
selects); omitted, a region-drag preempts pan. (#416)
|
|
175
|
+
- **`@pond-ts/charts`: `binColors` — per-bin colour for single-series bars.**
|
|
176
|
+
`<BarChart binColors={[...]}>` colours each bar/band segment individually (one
|
|
177
|
+
colour per bin, in order), the single-series analog of the stacked `colors`
|
|
178
|
+
prop — used by the category axis (colour per category) and any single-series
|
|
179
|
+
band chart. (#408)
|
|
180
|
+
- **`@pond-ts/charts`: annotation edges snap to session boundaries.** When a
|
|
181
|
+
`<ChartContainer>` carries a trading calendar (disjoint x axis), dragging a
|
|
182
|
+
`<Region>` edge (or creating one) snaps to the nearest **session boundary**
|
|
183
|
+
rather than raw wall-clock, so a drawn span aligns with real market sessions.
|
|
184
|
+
(#410)
|
|
185
|
+
|
|
186
|
+
### Fixed
|
|
187
|
+
|
|
188
|
+
- **`@pond-ts/charts`: region body-move no longer distorts across a session
|
|
189
|
+
boundary.** On a trading-time (discontinuous) axis, dragging a `<Region>`
|
|
190
|
+
annotation by its body now translates it rigidly in pixel space, so the box
|
|
191
|
+
keeps its width as it crosses a collapsed gap (it previously applied one
|
|
192
|
+
value-delta to both edges, which stretched the box in the different
|
|
193
|
+
rate-contexts either side of a session boundary). No-op on a continuous axis.
|
|
194
|
+
(#405)
|
|
195
|
+
|
|
43
196
|
## [0.42.0] — 2026-07-10
|
|
44
197
|
|
|
45
198
|
The **trading-calendar** release: a new `@pond-ts/financial` package (its first
|
|
@@ -82,6 +235,16 @@ collapses closed-market time (weekends, holidays, overnight, lunch breaks).
|
|
|
82
235
|
(`theme.axis.sessionDivider`), aligned with the labels.
|
|
83
236
|
- `Charts/TradingTimeAxis` stories (weekend/holiday/half-day/intraday,
|
|
84
237
|
continuous-vs-trading, daily-months, proportional-vs-uniform).
|
|
238
|
+
- **`@pond-ts/charts`: first-class histograms.** `<BarChart>` gains **stacking**
|
|
239
|
+
— a group-by dimension → stacked segments, from a wide series (`columns`), a
|
|
240
|
+
`Map<group, TimeSeries>` (the `partitionBy().aggregate().toMap()` shape), or a
|
|
241
|
+
`byColumn` `bins` array; per-group colour via `colors` or theme roles — and an
|
|
242
|
+
**`orientation`** prop (`'vertical'` default | `'horizontal'`, bars grow right
|
|
243
|
+
with the bins on a y band axis). New readers `stacksFromGroups` /
|
|
244
|
+
`stacksFromColumns` / `stacksFromBins` plus `StackedBarSeries` / `BinRecord` /
|
|
245
|
+
`Orientation` types. All data generation composes from existing operators
|
|
246
|
+
(`aggregate` / `byColumn` / `partitionBy`); no core changes. Guide: How-to
|
|
247
|
+
guides → Histograms. (#401)
|
|
85
248
|
- **`@pond-ts/charts`:** selection now has a stable series identity. `SelectInfo`
|
|
86
249
|
carries an `id`, and `BarChart` / `ScatterChart` take an optional `id` prop —
|
|
87
250
|
the series identity used for selection + hover. An `id` **gates interactivity**:
|
package/dist/BarChart.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ValueSeries } from 'pond-ts';
|
|
2
2
|
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
3
|
-
import { type BinRecord } from './data.js';
|
|
3
|
+
import { type BinRecord, type CategoryDatum } from './data.js';
|
|
4
4
|
import { type Orientation } from './bars.js';
|
|
5
5
|
export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
6
6
|
/**
|
|
@@ -29,6 +29,17 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
|
|
|
29
29
|
* draw. Pair with `ordinal` for a category (band) axis.
|
|
30
30
|
*/
|
|
31
31
|
bins?: readonly BinRecord[];
|
|
32
|
+
/**
|
|
33
|
+
* **Categorical** data — an ordered `{ label, value }[]`, one bar per category
|
|
34
|
+
* on a first-class **ordinal category x-axis** (the container infers
|
|
35
|
+
* `xKind:'category'` and builds a band scale over the labels). The transpose
|
|
36
|
+
* view's "columns on x": each `label` is a category (ticker / account / zone),
|
|
37
|
+
* `value` its bar height. Provide **exactly one** of `series` / `bins` /
|
|
38
|
+
* `categories`; `categories` takes no `column`/`columns` and is **vertical only**
|
|
39
|
+
* (categories on x). Colour per category via `binColors`. (Categorical-axis RFC,
|
|
40
|
+
* Phase 1.)
|
|
41
|
+
*/
|
|
42
|
+
categories?: readonly CategoryDatum[];
|
|
32
43
|
/** Name of the numeric value column for the bar height (single series). Provide
|
|
33
44
|
* `column` **or** `columns`, not both. */
|
|
34
45
|
column?: string;
|
|
@@ -55,6 +66,17 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
|
|
|
55
66
|
* this is the stack's one colour input.
|
|
56
67
|
*/
|
|
57
68
|
colors?: Readonly<Record<string, string>>;
|
|
69
|
+
/**
|
|
70
|
+
* **Per-bin** colours for a single-series band chart — `binColors[i]` fills
|
|
71
|
+
* bar `i` (aligned to the bins / bands in order), overriding the `as`/theme
|
|
72
|
+
* fill. This is the way to colour heart-rate / power **zones** or value bands
|
|
73
|
+
* each their own colour (the `colors` map above is per-**group**, for stacks).
|
|
74
|
+
* An `undefined`/short entry falls back to the theme fill. Meant for a
|
|
75
|
+
* single-series chart (`column` + `bins`, or a horizontal single series); on a
|
|
76
|
+
* multi-group stack it would tint every segment of a bin alike, so it's not
|
|
77
|
+
* the tool there.
|
|
78
|
+
*/
|
|
79
|
+
binColors?: readonly (string | undefined)[];
|
|
58
80
|
/**
|
|
59
81
|
* Bar growth direction (the histogram orientation). **Default `'vertical'`.**
|
|
60
82
|
*
|
|
@@ -147,5 +169,5 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
|
|
|
147
169
|
* </Layers>
|
|
148
170
|
* ```
|
|
149
171
|
*/
|
|
150
|
-
export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, bins, column, columns, as: semantic, colors, orientation, ordinal, id, axis, gap, index, }: BarChartProps<S, VS>): null;
|
|
172
|
+
export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, bins, categories, column, columns, as: semantic, colors, binColors, orientation, ordinal, id, axis, gap, index, }: BarChartProps<S, VS>): null;
|
|
151
173
|
//# sourceMappingURL=BarChart.d.ts.map
|
package/dist/BarChart.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { useContext, useEffect, useMemo } from 'react';
|
|
2
|
-
import { ValueSeries } from 'pond-ts';
|
|
3
|
-
import { barsFromTimeSeries, barsFromValueSeries, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
|
|
2
|
+
import { Interval, ValueSeries } from 'pond-ts';
|
|
3
|
+
import { barsFromTimeSeries, barsFromValueSeries, categoryStack, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
|
|
4
4
|
import { barAt, barExtent, barIndexAtTime, drawBars, drawStacks, resolveBarBaseline, stackAt, stackBinExtent, stackValueExtent, } from './bars.js';
|
|
5
5
|
import { ContainerContext, LayersContext, } from './context.js';
|
|
6
6
|
import { useSlotKey } from './use-slot-key.js';
|
|
@@ -47,7 +47,7 @@ import { useSlotKey } from './use-slot-key.js';
|
|
|
47
47
|
* </Layers>
|
|
48
48
|
* ```
|
|
49
49
|
*/
|
|
50
|
-
export function BarChart({ series, bins, column, columns, as: semantic, colors, orientation = 'vertical', ordinal = false, id, axis, gap, index = 0, }) {
|
|
50
|
+
export function BarChart({ series, bins, categories, column, columns, as: semantic, colors, binColors, orientation = 'vertical', ordinal = false, id, axis, gap, index = 0, }) {
|
|
51
51
|
const container = useContext(ContainerContext);
|
|
52
52
|
if (container === null) {
|
|
53
53
|
throw new Error('<BarChart> must be rendered inside a <ChartContainer>');
|
|
@@ -58,8 +58,19 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
58
58
|
}
|
|
59
59
|
// Validate the data-source / value-column combination up front (throws are
|
|
60
60
|
// stable across renders, so no need to memoize them).
|
|
61
|
-
|
|
62
|
-
|
|
61
|
+
const nSources = (series !== undefined ? 1 : 0) +
|
|
62
|
+
(bins !== undefined ? 1 : 0) +
|
|
63
|
+
(categories !== undefined ? 1 : 0);
|
|
64
|
+
if (nSources !== 1) {
|
|
65
|
+
throw new Error('<BarChart> needs exactly one of `series`, `bins`, or `categories`');
|
|
66
|
+
}
|
|
67
|
+
if (categories !== undefined) {
|
|
68
|
+
if (column !== undefined || columns !== undefined) {
|
|
69
|
+
throw new Error('<BarChart categories> takes no `column`/`columns` (each datum carries its own value)');
|
|
70
|
+
}
|
|
71
|
+
if (orientation === 'horizontal') {
|
|
72
|
+
throw new Error('<BarChart categories> is vertical only (categories on x); horizontal category axes are not yet supported');
|
|
73
|
+
}
|
|
63
74
|
}
|
|
64
75
|
const isMap = series instanceof Map;
|
|
65
76
|
if (isMap && columns !== undefined) {
|
|
@@ -76,6 +87,12 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
76
87
|
// stack, any horizontal — builds a StackedBarSeries (G === 1 for a single
|
|
77
88
|
// horizontal bar) so one oriented draw path covers it.
|
|
78
89
|
const shape = useMemo(() => {
|
|
90
|
+
if (categories !== undefined) {
|
|
91
|
+
// Categorical row-read: one unit-slot bar per category (G === 1), drawn on
|
|
92
|
+
// the container's band scale. The reused stacked geometry — only the axis
|
|
93
|
+
// (band scale + labels) is new.
|
|
94
|
+
return { kind: 'stacked', ss: categoryStack(categories) };
|
|
95
|
+
}
|
|
79
96
|
if (bins !== undefined) {
|
|
80
97
|
const cols = columns ?? (column !== undefined ? [column] : undefined);
|
|
81
98
|
if (cols === undefined) {
|
|
@@ -111,18 +128,53 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
111
128
|
? barsFromValueSeries(s, column)
|
|
112
129
|
: barsFromTimeSeries(s, column),
|
|
113
130
|
};
|
|
114
|
-
}, [
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
131
|
+
}, [
|
|
132
|
+
series,
|
|
133
|
+
bins,
|
|
134
|
+
categories,
|
|
135
|
+
column,
|
|
136
|
+
columns,
|
|
137
|
+
ordinal,
|
|
138
|
+
orientation,
|
|
139
|
+
isMap,
|
|
140
|
+
label,
|
|
141
|
+
]);
|
|
142
|
+
// The category labels — the ordinal axis's ordered column set (`xCategories`),
|
|
143
|
+
// and the per-bar readout label. `null` unless this is a categorical chart.
|
|
144
|
+
const categoryLabels = useMemo(() => categories?.map((c) => c.label) ?? null, [categories]);
|
|
145
|
+
// The bin axis kind — `'category'` for a categorical chart, else `'time'`/`'value'`
|
|
146
|
+
// (a `TimeSeries`/`Map` bins on time, a `ValueSeries`/`bins`-array on a value
|
|
147
|
+
// axis). For a vertical chart this is the shared x-kind; a horizontal one puts
|
|
148
|
+
// the *value* on x (always 'value') and the bin axis on a linear y.
|
|
149
|
+
const binAxisKind = categories !== undefined
|
|
150
|
+
? 'category'
|
|
151
|
+
: bins !== undefined
|
|
152
|
+
? 'value'
|
|
153
|
+
: isMap
|
|
154
|
+
? 'time'
|
|
155
|
+
: series instanceof ValueSeries
|
|
156
|
+
? 'value'
|
|
157
|
+
: 'time';
|
|
158
|
+
// The bars' `[begin, end)` spans as pond `Interval`s — the region cursor's snap
|
|
159
|
+
// buckets (a region drag snaps bar by bar; a hover highlights the bar under the
|
|
160
|
+
// pointer). Published only for a **vertical** bar layer on a **continuous**
|
|
161
|
+
// (time / value) x axis: a horizontal chart puts the value/count on x (snapping
|
|
162
|
+
// it is meaningless) and a categorical (ordinal-slot) axis is out of the region
|
|
163
|
+
// cursor's scope. Memoized off the shape alone, so a hover / selection change
|
|
164
|
+
// (which rebuilds the layer entry) doesn't re-allocate the intervals.
|
|
165
|
+
const binBuckets = useMemo(() => {
|
|
166
|
+
if (orientation !== 'vertical' || binAxisKind === 'category')
|
|
167
|
+
return null;
|
|
168
|
+
const { begin, end, length } = shape.kind === 'single' ? shape.bs : shape.ss;
|
|
169
|
+
if (length === 0)
|
|
170
|
+
return null;
|
|
171
|
+
const out = new Array(length);
|
|
172
|
+
for (let i = 0; i < length; i += 1) {
|
|
173
|
+
const b = begin[i];
|
|
174
|
+
out[i] = new Interval({ value: b, start: b, end: end[i] });
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}, [shape, orientation, binAxisKind]);
|
|
126
178
|
const { bar } = container.theme;
|
|
127
179
|
// Single-series style: the `as` role → theme bar style (the single channel).
|
|
128
180
|
const singleStyle = (semantic !== undefined ? bar[semantic] : undefined) ?? bar.default;
|
|
@@ -137,8 +189,13 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
137
189
|
const stackStyle = useMemo(() => {
|
|
138
190
|
const base = bar.default;
|
|
139
191
|
const fills = (groups ?? []).map((g) => colors?.[g] ?? (bar[g] ?? base).fill);
|
|
140
|
-
return {
|
|
141
|
-
|
|
192
|
+
return {
|
|
193
|
+
fills,
|
|
194
|
+
opacity: base.opacity,
|
|
195
|
+
outlineWidth: base.outlineWidth,
|
|
196
|
+
...(binColors !== undefined ? { binFills: binColors } : {}),
|
|
197
|
+
};
|
|
198
|
+
}, [bar, groups, colors, binColors]);
|
|
142
199
|
// The current selection / hover, narrowed to the identity the highlight match
|
|
143
200
|
// needs. For a stack that's (id, key, label = group); the single path uses just
|
|
144
201
|
// (id, key). Read here so a change re-registers the layer → the canvas repaints.
|
|
@@ -146,13 +203,21 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
146
203
|
const hoveredMark = container.hovered;
|
|
147
204
|
const selection = useMemo(() => selected === null
|
|
148
205
|
? null
|
|
149
|
-
: {
|
|
206
|
+
: {
|
|
207
|
+
id: selected.id,
|
|
208
|
+
key: selected.key,
|
|
209
|
+
label: selected.label,
|
|
210
|
+
...(selected.mark !== undefined ? { mark: selected.mark } : {}),
|
|
211
|
+
}, [selected]);
|
|
150
212
|
const hover = useMemo(() => hoveredMark === null
|
|
151
213
|
? null
|
|
152
214
|
: {
|
|
153
215
|
id: hoveredMark.id,
|
|
154
216
|
key: hoveredMark.key,
|
|
155
217
|
label: hoveredMark.label,
|
|
218
|
+
...(hoveredMark.mark !== undefined
|
|
219
|
+
? { mark: hoveredMark.mark }
|
|
220
|
+
: {}),
|
|
156
221
|
}, [hoveredMark]);
|
|
157
222
|
const entry = useMemo(() => {
|
|
158
223
|
// ── Single-series, vertical: the original bar path, pixels unchanged. ──
|
|
@@ -163,6 +228,7 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
163
228
|
yExtent: () => barExtent(bs),
|
|
164
229
|
xKind: binAxisKind,
|
|
165
230
|
xExtent: () => bs.length === 0 ? null : [bs.begin[0], bs.end[bs.length - 1]],
|
|
231
|
+
...(binBuckets !== null ? { binIntervals: () => binBuckets } : {}),
|
|
166
232
|
sampleAt: (time) => {
|
|
167
233
|
if (bs.length === 0)
|
|
168
234
|
return [];
|
|
@@ -218,6 +284,12 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
218
284
|
xKind: vertical ? binAxisKind : 'value',
|
|
219
285
|
xExtent: vertical ? binExtent : valueExtent,
|
|
220
286
|
yExtent: vertical ? valueExtent : binExtent,
|
|
287
|
+
...(binBuckets !== null ? { binIntervals: () => binBuckets } : {}),
|
|
288
|
+
// A categorical chart hands the container its ordered category names — the
|
|
289
|
+
// ordinal axis domain the shared band scale + label formatter build on.
|
|
290
|
+
...(categoryLabels !== null
|
|
291
|
+
? { xCategories: () => categoryLabels }
|
|
292
|
+
: {}),
|
|
221
293
|
// No x-scrub flag for a stack / horizontal chart — hover + click read it
|
|
222
294
|
// out instead (the flag is single-series-vertical only).
|
|
223
295
|
sampleAt: () => [],
|
|
@@ -228,13 +300,22 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
228
300
|
const hit = stackAt(ss, px, py, orientation, xScale, yScale, gapPx, stackMinWidth);
|
|
229
301
|
if (hit === null)
|
|
230
302
|
return null;
|
|
231
|
-
const [, g, begin, name, value] = hit;
|
|
303
|
+
const [bi, g, begin, name, value] = hit;
|
|
304
|
+
// A categorical bar carries a stable per-bar `mark` (its column
|
|
305
|
+
// name); the selection keys on `(id, mark)` so it survives a
|
|
306
|
+
// reorder. `bi` is the exact bin index from the hit.
|
|
307
|
+
const stableMark = ss.marks?.[bi];
|
|
232
308
|
return {
|
|
233
309
|
id,
|
|
234
310
|
key: begin,
|
|
235
311
|
value,
|
|
236
|
-
|
|
237
|
-
|
|
312
|
+
// A per-bin colour override wins over the group fill, so the
|
|
313
|
+
// readout pill reads the bar's own colour.
|
|
314
|
+
color: stackStyle.binFills?.[bi] ?? stackStyle.fills[g],
|
|
315
|
+
// A categorical bar reports its category name; a stack reports
|
|
316
|
+
// the group.
|
|
317
|
+
label: stableMark ?? name,
|
|
318
|
+
...(stableMark !== undefined ? { mark: stableMark } : {}),
|
|
238
319
|
};
|
|
239
320
|
},
|
|
240
321
|
}),
|
|
@@ -246,6 +327,8 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
246
327
|
}, [
|
|
247
328
|
shape,
|
|
248
329
|
binAxisKind,
|
|
330
|
+
binBuckets,
|
|
331
|
+
categoryLabels,
|
|
249
332
|
orientation,
|
|
250
333
|
singleStyle,
|
|
251
334
|
stackStyle,
|
package/dist/BoxPlot.d.ts
CHANGED
|
@@ -1,18 +1,35 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { ValueSeries } from 'pond-ts';
|
|
2
|
+
import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
|
|
2
3
|
import { type BoxShape } from './box.js';
|
|
3
|
-
export interface BoxPlotProps<S extends SeriesSchema> {
|
|
4
|
-
/**
|
|
5
|
-
*
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
export interface BoxPlotProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
|
|
5
|
+
/**
|
|
6
|
+
* The source series. A `TimeSeries` plots against the time axis; a `ValueSeries`
|
|
7
|
+
* (`series.byValue('strike')`, or `ValueSeries.fromColumns` for natively
|
|
8
|
+
* value-keyed data — a per-strike IV distribution) against its value axis — the
|
|
9
|
+
* container infers which from the data, no axis-type prop (mirrors `<LineChart>`
|
|
10
|
+
* / `<ScatterChart>`). The box x-span is the key's `[begin, end)` for an
|
|
11
|
+
* interval-keyed `TimeSeries`, else synthesized from neighbour spacing (a
|
|
12
|
+
* point-keyed `TimeSeries`, or a `ValueSeries`) so the box keeps real width.
|
|
13
|
+
*/
|
|
14
|
+
series: TimeSeries<S> | ValueSeries<VS>;
|
|
15
|
+
/** Name of the numeric column for the lower whisker end (e.g. `p5` / `min`).
|
|
16
|
+
* **Required** — with `upper` it's the whisker reach. */
|
|
8
17
|
lower: string;
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Name of the numeric column for the box bottom — first quartile (e.g. `p25`).
|
|
20
|
+
* **Optional:** omit `q1` **and** `q3` together for a **range-only** box — a
|
|
21
|
+
* whisker-only `lower→upper` segment, no body (a bid→ask IV mark). Giving just
|
|
22
|
+
* one of `q1`/`q3` throws.
|
|
23
|
+
*/
|
|
24
|
+
q1?: string;
|
|
25
|
+
/** Name of the numeric column for the median line (e.g. `p50`). **Optional** —
|
|
26
|
+
* omit for no centre line (independent of the box body). */
|
|
27
|
+
median?: string;
|
|
28
|
+
/** Name of the numeric column for the box top — third quartile (e.g. `p75`).
|
|
29
|
+
* **Optional** — omit with `q1` for a range-only box (see `q1`). */
|
|
30
|
+
q3?: string;
|
|
31
|
+
/** Name of the numeric column for the upper whisker end (e.g. `p95` / `max`).
|
|
32
|
+
* **Required** — with `lower` it's the whisker reach. */
|
|
16
33
|
upper: string;
|
|
17
34
|
/**
|
|
18
35
|
* The box series' semantic identifier — what the spread _is_ (e.g. `latency`).
|
|
@@ -38,12 +55,39 @@ export interface BoxPlotProps<S extends SeriesSchema> {
|
|
|
38
55
|
* How each box renders its spread — `'whisker'` (default; thin stems + caps),
|
|
39
56
|
* `'solid'` (the candlestick look: a light outer bar over the full range with a
|
|
40
57
|
* darker inner q1→q3 box, no stems), or `'none'` (the q1→q3 box only, no spread
|
|
41
|
-
* marks). See {@link BoxShape}.
|
|
58
|
+
* marks). See {@link BoxShape}. On a **range-only** box (no `q1`/`q3`),
|
|
59
|
+
* `'whisker'` is one full `lower→upper` stem and `'solid'` the outer bar; `'none'`
|
|
60
|
+
* would draw **nothing** (no body + no spread), so use `'whisker'`/`'solid'` there.
|
|
42
61
|
*/
|
|
43
62
|
shape?: BoxShape;
|
|
44
|
-
/** Draw the median (centre) line across each box.
|
|
45
|
-
*
|
|
63
|
+
/** Draw the median (centre) line across each box. Default `true`, but a no-op
|
|
64
|
+
* when the `median` column is omitted (nothing to draw). The `median` prop
|
|
65
|
+
* names the *column*; this toggles the line. */
|
|
46
66
|
showMedian?: boolean;
|
|
67
|
+
/**
|
|
68
|
+
* A **pixel** shift applied to every box's x — zoom-stable (unlike a data-space
|
|
69
|
+
* nudge). **Default `0`.** For pairing marks that share a key side by side: e.g.
|
|
70
|
+
* a call and a put box at the same strike, `offset={-4}` / `offset={+4}` (the
|
|
71
|
+
* react-timeseries-charts side-by-side-bars precedent). Pairs with
|
|
72
|
+
* `<ScatterChart offset>`.
|
|
73
|
+
*
|
|
74
|
+
* Only the **draw** is shifted. The box's **readouts** stay in un-shifted data
|
|
75
|
+
* space — the off-chart hover finds a box by span containment (`xScale.invert`),
|
|
76
|
+
* and the in-chart `flag` staff anchors at the box's data centre — so both can
|
|
77
|
+
* sit up to `offset` px from the pixel-shifted box. Keep the offset small (a
|
|
78
|
+
* pairing nudge, not a layout tool) and it's imperceptible. (`<ScatterChart
|
|
79
|
+
* offset>` has no such gap — its hit-test is pixel-space and shifts too.)
|
|
80
|
+
*/
|
|
81
|
+
offset?: number;
|
|
82
|
+
/**
|
|
83
|
+
* Whisker end-cap **total width in pixels** (the top/bottom bars of the `T`).
|
|
84
|
+
* **Omitted ⇒ half the box width** (responsive — scales with the slot). Set a
|
|
85
|
+
* small fixed value (e.g. `6`) to keep the caps narrow so two `offset`-paired
|
|
86
|
+
* marks (call/put at one strike) don't overlap their T-bars — the caps no longer
|
|
87
|
+
* grow with the wide value-axis slot. Clamped to the box width. Only affects the
|
|
88
|
+
* `'whisker'` shape (`'solid'`/`'none'` have no caps).
|
|
89
|
+
*/
|
|
90
|
+
capWidth?: number;
|
|
47
91
|
/**
|
|
48
92
|
* @internal Declaration position among the `<Layers>` children, injected by
|
|
49
93
|
* `Layers` so z-order follows JSX order. Do not set.
|
|
@@ -52,26 +96,34 @@ export interface BoxPlotProps<S extends SeriesSchema> {
|
|
|
52
96
|
}
|
|
53
97
|
/**
|
|
54
98
|
* A discrete box-and-whisker draw layer — the bar-chart analog of the variance
|
|
55
|
-
* band. Reads
|
|
99
|
+
* band. Reads **pre-computed quantile columns** of `series` (typically a
|
|
56
100
|
* `rolling`/`aggregate` percentile pass — the chart does **not** compute them)
|
|
57
|
-
* into a {@link BoxSeries} and draws one box per key: the q1→q3
|
|
58
|
-
* line, and whiskers out to lower/upper
|
|
59
|
-
*
|
|
60
|
-
*
|
|
101
|
+
* into a {@link BoxSeries} and draws one box per key: the q1→q3 body, the median
|
|
102
|
+
* line, and whiskers out to lower/upper. Registers itself into the enclosing
|
|
103
|
+
* {@link Layers}; renders nothing to the DOM — the row draws it.
|
|
104
|
+
*
|
|
105
|
+
* - **Any axis.** A `TimeSeries` plots on time, a `ValueSeries`
|
|
106
|
+
* (`series.byValue('strike')` or `ValueSeries.fromColumns`) on its value axis —
|
|
107
|
+
* a vol smile's per-strike IV. The box width is the interval key's `[begin, end)`
|
|
108
|
+
* or, for a point key (a `ValueSeries`, or a point-keyed `TimeSeries`),
|
|
109
|
+
* neighbour spacing — so it never collapses to the 1px floor.
|
|
110
|
+
* - **Range-only.** `q1`/`q3` (the body) and `median` (the centre line) are
|
|
111
|
+
* optional: omit `q1`+`q3` for a whisker-only `lower→upper` segment — a bid→ask
|
|
112
|
+
* IV mark. Gap-aware: a key missing any **present** quantile draws nothing.
|
|
113
|
+
* - **`offset`** nudges the whole layer in pixel space, for pairing same-key marks
|
|
114
|
+
* (call/put at one strike) side by side.
|
|
61
115
|
*
|
|
62
116
|
* There's no baseline — a box is a spread, not a bar to a floor; the y-domain
|
|
63
117
|
* auto-fits the whisker reach (lower→upper).
|
|
64
118
|
*
|
|
65
119
|
* ```tsx
|
|
66
120
|
* <Layers>
|
|
67
|
-
* <BoxPlot
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
71
|
-
* gap={6}
|
|
72
|
-
* />
|
|
121
|
+
* <BoxPlot series={q} lower="p5" q1="p25" median="p50" q3="p75" upper="p95"
|
|
122
|
+
* as="latency" gap={6} />
|
|
123
|
+
* // range-only bid→ask on a value axis (a vol smile):
|
|
124
|
+
* <BoxPlot series={smile} lower="bid" upper="ask" />
|
|
73
125
|
* </Layers>
|
|
74
126
|
* ```
|
|
75
127
|
*/
|
|
76
|
-
export declare function BoxPlot<S extends SeriesSchema>({ series, lower, q1, median, q3, upper, as: semantic, axis, gap, shape, showMedian, index, }: BoxPlotProps<S>): null;
|
|
128
|
+
export declare function BoxPlot<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, lower, q1, median, q3, upper, as: semantic, axis, gap, shape, showMedian, offset, capWidth, index, }: BoxPlotProps<S, VS>): null;
|
|
77
129
|
//# sourceMappingURL=BoxPlot.d.ts.map
|