@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/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,8 @@ 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.43.0...HEAD
|
|
12
|
+
[0.43.0]: https://github.com/pjm17971/pond-ts/compare/v0.42.0...v0.43.0
|
|
12
13
|
[0.42.0]: https://github.com/pjm17971/pond-ts/compare/v0.41.0...v0.42.0
|
|
13
14
|
[0.41.0]: https://github.com/pjm17971/pond-ts/compare/v0.40.0...v0.41.0
|
|
14
15
|
[0.40.0]: https://github.com/pjm17971/pond-ts/compare/v0.39.0...v0.40.0
|
|
@@ -40,6 +41,77 @@ and type-level changes; patch bumps are strictly additive.
|
|
|
40
41
|
|
|
41
42
|
## [Unreleased]
|
|
42
43
|
|
|
44
|
+
## [0.43.0] — 2026-07-11
|
|
45
|
+
|
|
46
|
+
The **categorical x-axis** release: a first-class ordinal band scale (ticker /
|
|
47
|
+
account / expiry on x — the transpose view of a time series), plus the charts
|
|
48
|
+
**interaction** wave that landed after v0.42.0 was cut — the region cursor and
|
|
49
|
+
its drag-to-select gesture, per-bin band colour, and annotation edges that snap
|
|
50
|
+
to session boundaries.
|
|
51
|
+
|
|
52
|
+
### Added
|
|
53
|
+
|
|
54
|
+
- **`@pond-ts/charts`: a first-class categorical x-axis.** `<BarChart
|
|
55
|
+
categories={[{ label, value }]}>` draws one bar per category on an ordinal
|
|
56
|
+
**band scale** (the transpose view's "columns on x" — ticker / account / zone
|
|
57
|
+
on x). The container infers `xKind:'category'` and builds a `scaleBand` over the
|
|
58
|
+
labels; `<CategoryAxis>` ticks once per category. Colour per category via
|
|
59
|
+
`binColors`; selection reports the category name. **Negative** category values
|
|
60
|
+
draw below the baseline (the P&L / delta case) — a single-series category bar
|
|
61
|
+
honours its sign. New exports `scaleBand` / `ScaleBand`, `CategoryAxis`,
|
|
62
|
+
`categoryStack` / `CategoryDatum`. Additive — a new x-kind alongside time /
|
|
63
|
+
value; existing charts are unchanged. First slice of the categorical-axis RFC
|
|
64
|
+
(`docs/rfcs/categorical-axis.md`, Phase 1).
|
|
65
|
+
- **`@pond-ts/charts`: `transposeRow`** — read one **row** of a wide `TimeSeries`
|
|
66
|
+
**across** into `{ label, value }[]` for `<BarChart categories>`: the schema's
|
|
67
|
+
numeric columns (a `pivotByGroup` output's per-group columns, a term
|
|
68
|
+
structure's per-expiry columns) become the categories at one instant. Pick the
|
|
69
|
+
row with `at` (`'last'` — the head/live row — by default; `'first'`, an index,
|
|
70
|
+
or `{ time }`); bound / order the set with `columns`.
|
|
71
|
+
- **`@pond-ts/charts`: stable per-column selection identity.** `SelectInfo` gains
|
|
72
|
+
an optional **`mark`** — a stable per-mark identity within a layer. A categorical
|
|
73
|
+
bar reports its **column name** as `mark`, and a controlled `selected` echo /
|
|
74
|
+
the highlight match key on `(id, mark)`, so a pinned selection survives a column
|
|
75
|
+
reorder (the slot index doesn't; the name does). Additive — `mark` is
|
|
76
|
+
`undefined` for a time / value bar (whose sample `key` is already its identity).
|
|
77
|
+
Plus a category-axis **label policy**: a dense axis thins (keeps every k-th) and
|
|
78
|
+
ellipsis-truncates its labels so they stay legible while every bar draws.
|
|
79
|
+
- **`@pond-ts/charts`: region cursor (`cursor="region"`).** A shaded **band**
|
|
80
|
+
highlights the bucket under the pointer, bucketed by a new **`cursorSequence`**
|
|
81
|
+
prop — a `Sequence` (`Sequence.every('15m')`, `Sequence.calendar('week')`)
|
|
82
|
+
realized over the view, or a `BoundedSequence` (a `TradingCalendar`'s
|
|
83
|
+
`sessionSequence()` / `barSequence()`) used as-is. The band maps through the x
|
|
84
|
+
scale, so on a trading-time axis the closed part of a bucket collapses (crops
|
|
85
|
+
to live sessions). Time-axis only (a no-op on a value axis). (#409, #413)
|
|
86
|
+
- **`@pond-ts/charts`: draggable region cursor → one-shot select.** Opt-in
|
|
87
|
+
**`onRegionSelect?: (range: TimeRange) => void`** makes the region cursor
|
|
88
|
+
draggable: the band extends bucket by bucket and fires **once** on release
|
|
89
|
+
with the selected `[start, end)` `TimeRange` (the cursor doesn't keep it —
|
|
90
|
+
typical use is to zoom the view). With **no `cursorSequence`** it degenerates
|
|
91
|
+
to a hover **line** + **freeform** drag. **`regionSelectModifier="shift"`**
|
|
92
|
+
resolves the gesture conflict with `panZoom` (plain drag pans, shift-drag
|
|
93
|
+
selects); omitted, a region-drag preempts pan. (#416)
|
|
94
|
+
- **`@pond-ts/charts`: `binColors` — per-bin colour for single-series bars.**
|
|
95
|
+
`<BarChart binColors={[...]}>` colours each bar/band segment individually (one
|
|
96
|
+
colour per bin, in order), the single-series analog of the stacked `colors`
|
|
97
|
+
prop — used by the category axis (colour per category) and any single-series
|
|
98
|
+
band chart. (#408)
|
|
99
|
+
- **`@pond-ts/charts`: annotation edges snap to session boundaries.** When a
|
|
100
|
+
`<ChartContainer>` carries a trading calendar (disjoint x axis), dragging a
|
|
101
|
+
`<Region>` edge (or creating one) snaps to the nearest **session boundary**
|
|
102
|
+
rather than raw wall-clock, so a drawn span aligns with real market sessions.
|
|
103
|
+
(#410)
|
|
104
|
+
|
|
105
|
+
### Fixed
|
|
106
|
+
|
|
107
|
+
- **`@pond-ts/charts`: region body-move no longer distorts across a session
|
|
108
|
+
boundary.** On a trading-time (discontinuous) axis, dragging a `<Region>`
|
|
109
|
+
annotation by its body now translates it rigidly in pixel space, so the box
|
|
110
|
+
keeps its width as it crosses a collapsed gap (it previously applied one
|
|
111
|
+
value-delta to both edges, which stretched the box in the different
|
|
112
|
+
rate-contexts either side of a session boundary). No-op on a continuous axis.
|
|
113
|
+
(#405)
|
|
114
|
+
|
|
43
115
|
## [0.42.0] — 2026-07-10
|
|
44
116
|
|
|
45
117
|
The **trading-calendar** release: a new `@pond-ts/financial` package (its first
|
|
@@ -82,6 +154,16 @@ collapses closed-market time (weekends, holidays, overnight, lunch breaks).
|
|
|
82
154
|
(`theme.axis.sessionDivider`), aligned with the labels.
|
|
83
155
|
- `Charts/TradingTimeAxis` stories (weekend/holiday/half-day/intraday,
|
|
84
156
|
continuous-vs-trading, daily-months, proportional-vs-uniform).
|
|
157
|
+
- **`@pond-ts/charts`: first-class histograms.** `<BarChart>` gains **stacking**
|
|
158
|
+
— a group-by dimension → stacked segments, from a wide series (`columns`), a
|
|
159
|
+
`Map<group, TimeSeries>` (the `partitionBy().aggregate().toMap()` shape), or a
|
|
160
|
+
`byColumn` `bins` array; per-group colour via `colors` or theme roles — and an
|
|
161
|
+
**`orientation`** prop (`'vertical'` default | `'horizontal'`, bars grow right
|
|
162
|
+
with the bins on a y band axis). New readers `stacksFromGroups` /
|
|
163
|
+
`stacksFromColumns` / `stacksFromBins` plus `StackedBarSeries` / `BinRecord` /
|
|
164
|
+
`Orientation` types. All data generation composes from existing operators
|
|
165
|
+
(`aggregate` / `byColumn` / `partitionBy`); no core changes. Guide: How-to
|
|
166
|
+
guides → Histograms. (#401)
|
|
85
167
|
- **`@pond-ts/charts`:** selection now has a stable series identity. `SelectInfo`
|
|
86
168
|
carries an `id`, and `BarChart` / `ScatterChart` take an optional `id` prop —
|
|
87
169
|
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
2
|
import { ValueSeries } from 'pond-ts';
|
|
3
|
-
import { barsFromTimeSeries, barsFromValueSeries, stacksFromBins, stacksFromColumns, stacksFromGroups, } from './data.js';
|
|
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,33 @@ 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';
|
|
126
158
|
const { bar } = container.theme;
|
|
127
159
|
// Single-series style: the `as` role → theme bar style (the single channel).
|
|
128
160
|
const singleStyle = (semantic !== undefined ? bar[semantic] : undefined) ?? bar.default;
|
|
@@ -137,8 +169,13 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
137
169
|
const stackStyle = useMemo(() => {
|
|
138
170
|
const base = bar.default;
|
|
139
171
|
const fills = (groups ?? []).map((g) => colors?.[g] ?? (bar[g] ?? base).fill);
|
|
140
|
-
return {
|
|
141
|
-
|
|
172
|
+
return {
|
|
173
|
+
fills,
|
|
174
|
+
opacity: base.opacity,
|
|
175
|
+
outlineWidth: base.outlineWidth,
|
|
176
|
+
...(binColors !== undefined ? { binFills: binColors } : {}),
|
|
177
|
+
};
|
|
178
|
+
}, [bar, groups, colors, binColors]);
|
|
142
179
|
// The current selection / hover, narrowed to the identity the highlight match
|
|
143
180
|
// needs. For a stack that's (id, key, label = group); the single path uses just
|
|
144
181
|
// (id, key). Read here so a change re-registers the layer → the canvas repaints.
|
|
@@ -146,13 +183,21 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
146
183
|
const hoveredMark = container.hovered;
|
|
147
184
|
const selection = useMemo(() => selected === null
|
|
148
185
|
? null
|
|
149
|
-
: {
|
|
186
|
+
: {
|
|
187
|
+
id: selected.id,
|
|
188
|
+
key: selected.key,
|
|
189
|
+
label: selected.label,
|
|
190
|
+
...(selected.mark !== undefined ? { mark: selected.mark } : {}),
|
|
191
|
+
}, [selected]);
|
|
150
192
|
const hover = useMemo(() => hoveredMark === null
|
|
151
193
|
? null
|
|
152
194
|
: {
|
|
153
195
|
id: hoveredMark.id,
|
|
154
196
|
key: hoveredMark.key,
|
|
155
197
|
label: hoveredMark.label,
|
|
198
|
+
...(hoveredMark.mark !== undefined
|
|
199
|
+
? { mark: hoveredMark.mark }
|
|
200
|
+
: {}),
|
|
156
201
|
}, [hoveredMark]);
|
|
157
202
|
const entry = useMemo(() => {
|
|
158
203
|
// ── Single-series, vertical: the original bar path, pixels unchanged. ──
|
|
@@ -218,6 +263,11 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
218
263
|
xKind: vertical ? binAxisKind : 'value',
|
|
219
264
|
xExtent: vertical ? binExtent : valueExtent,
|
|
220
265
|
yExtent: vertical ? valueExtent : binExtent,
|
|
266
|
+
// A categorical chart hands the container its ordered category names — the
|
|
267
|
+
// ordinal axis domain the shared band scale + label formatter build on.
|
|
268
|
+
...(categoryLabels !== null
|
|
269
|
+
? { xCategories: () => categoryLabels }
|
|
270
|
+
: {}),
|
|
221
271
|
// No x-scrub flag for a stack / horizontal chart — hover + click read it
|
|
222
272
|
// out instead (the flag is single-series-vertical only).
|
|
223
273
|
sampleAt: () => [],
|
|
@@ -228,13 +278,22 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
228
278
|
const hit = stackAt(ss, px, py, orientation, xScale, yScale, gapPx, stackMinWidth);
|
|
229
279
|
if (hit === null)
|
|
230
280
|
return null;
|
|
231
|
-
const [, g, begin, name, value] = hit;
|
|
281
|
+
const [bi, g, begin, name, value] = hit;
|
|
282
|
+
// A categorical bar carries a stable per-bar `mark` (its column
|
|
283
|
+
// name); the selection keys on `(id, mark)` so it survives a
|
|
284
|
+
// reorder. `bi` is the exact bin index from the hit.
|
|
285
|
+
const stableMark = ss.marks?.[bi];
|
|
232
286
|
return {
|
|
233
287
|
id,
|
|
234
288
|
key: begin,
|
|
235
289
|
value,
|
|
236
|
-
|
|
237
|
-
|
|
290
|
+
// A per-bin colour override wins over the group fill, so the
|
|
291
|
+
// readout pill reads the bar's own colour.
|
|
292
|
+
color: stackStyle.binFills?.[bi] ?? stackStyle.fills[g],
|
|
293
|
+
// A categorical bar reports its category name; a stack reports
|
|
294
|
+
// the group.
|
|
295
|
+
label: stableMark ?? name,
|
|
296
|
+
...(stableMark !== undefined ? { mark: stableMark } : {}),
|
|
238
297
|
};
|
|
239
298
|
},
|
|
240
299
|
}),
|
|
@@ -246,6 +305,7 @@ export function BarChart({ series, bins, column, columns, as: semantic, colors,
|
|
|
246
305
|
}, [
|
|
247
306
|
shape,
|
|
248
307
|
binAxisKind,
|
|
308
|
+
categoryLabels,
|
|
249
309
|
orientation,
|
|
250
310
|
singleStyle,
|
|
251
311
|
stackStyle,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type XAxisProps } from './XAxis.js';
|
|
2
|
+
/**
|
|
3
|
+
* The category-flavoured preset of {@link XAxis} — `<CategoryAxis />` is
|
|
4
|
+
* `<XAxis />`. The axis kind follows the data: on a **category** container (a
|
|
5
|
+
* layer that plots on the ordinal column-domain axis) it ticks once per category,
|
|
6
|
+
* labelling each band centre with the category name (the container's shared
|
|
7
|
+
* formatter). Kept as the familiar name for categorical charts, mirroring
|
|
8
|
+
* {@link TimeAxis}. Forwards every {@link XAxisProps} (`label`, `side`, …).
|
|
9
|
+
*
|
|
10
|
+
* A high-cardinality axis (many categories) thins + truncates its labels to stay
|
|
11
|
+
* legible (categorical-axis RFC, Phase 1). The labels **come from the data** (the
|
|
12
|
+
* `categories` list), so a d3 `format` prop does not apply here (it can't name a
|
|
13
|
+
* category); customize a label by changing the `categories` datum's `label`.
|
|
14
|
+
*/
|
|
15
|
+
export declare function CategoryAxis(props?: XAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
16
|
+
//# sourceMappingURL=CategoryAxis.d.ts.map
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
+
import { XAxis } from './XAxis.js';
|
|
3
|
+
/**
|
|
4
|
+
* The category-flavoured preset of {@link XAxis} — `<CategoryAxis />` is
|
|
5
|
+
* `<XAxis />`. The axis kind follows the data: on a **category** container (a
|
|
6
|
+
* layer that plots on the ordinal column-domain axis) it ticks once per category,
|
|
7
|
+
* labelling each band centre with the category name (the container's shared
|
|
8
|
+
* formatter). Kept as the familiar name for categorical charts, mirroring
|
|
9
|
+
* {@link TimeAxis}. Forwards every {@link XAxisProps} (`label`, `side`, …).
|
|
10
|
+
*
|
|
11
|
+
* A high-cardinality axis (many categories) thins + truncates its labels to stay
|
|
12
|
+
* legible (categorical-axis RFC, Phase 1). The labels **come from the data** (the
|
|
13
|
+
* `categories` list), so a d3 `format` prop does not apply here (it can't name a
|
|
14
|
+
* category); customize a label by changing the `categories` datum's `label`.
|
|
15
|
+
*/
|
|
16
|
+
export function CategoryAxis(props = {}) {
|
|
17
|
+
return _jsx(XAxis, { ...props });
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=CategoryAxis.js.map
|
package/dist/ChartContainer.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type ReactNode } from 'react';
|
|
2
2
|
import { type DiscontinuityProvider, type TradingCalendarLike } from './tradingTimeScale.js';
|
|
3
|
+
import { Sequence, BoundedSequence } from 'pond-ts';
|
|
3
4
|
import type { TimeRange } from 'pond-ts';
|
|
4
5
|
import { type AnnotationKind, type CreateSpec, type CursorMode, type SelectInfo, type TrackerInfo } from './context.js';
|
|
5
6
|
import { type AxisFormat } from './format.js';
|
|
@@ -75,9 +76,55 @@ export interface ChartContainerProps {
|
|
|
75
76
|
* via `<ChartRow cursor>`). **Default `'line'`** — the synced vertical line,
|
|
76
77
|
* with values surfaced *outside* the chart via {@link onTrackerChanged}.
|
|
77
78
|
* `'point'` / `'inline'` / `'flag'` add per-series marks; `'none'` hides it.
|
|
79
|
+
* `'region'` shades the bucket under the pointer (needs {@link cursorSequence}).
|
|
78
80
|
* See {@link CursorMode}.
|
|
79
81
|
*/
|
|
80
82
|
cursor?: CursorMode;
|
|
83
|
+
/**
|
|
84
|
+
* The bucketing for `cursor="region"` — the interval highlighted under the
|
|
85
|
+
* pointer. A pond {@link Sequence} (duration or calendar-aware —
|
|
86
|
+
* `Sequence.every('1d')`, `Sequence.calendar('month')`) is realized over the
|
|
87
|
+
* current view; a {@link BoundedSequence} (e.g. a `TradingCalendar`'s
|
|
88
|
+
* `sessionSequence()` / `barSequence()`) is used as-is, so the band can track
|
|
89
|
+
* whole **sessions**. Either way the band maps through `xScale`, so on a
|
|
90
|
+
* trading-time axis the closed part of the bucket collapses. Ignored unless
|
|
91
|
+
* `cursor="region"`.
|
|
92
|
+
*
|
|
93
|
+
* **Time axis only.** A bucket is a *time* interval, so the region cursor is
|
|
94
|
+
* gated to a **time** x-axis — on a **value** axis (a horizontal histogram, a
|
|
95
|
+
* value-keyed chart) it's a no-op (highlighting a value *band* on a horizontal
|
|
96
|
+
* histogram would be a different, y-oriented cursor).
|
|
97
|
+
*
|
|
98
|
+
* **Pass a stable reference.** The buckets are memoized on this value + the
|
|
99
|
+
* view range; a `Sequence`/`BoundedSequence` rebuilt inline every render
|
|
100
|
+
* re-realizes the buckets on each pointer move (harmless for a coarse
|
|
101
|
+
* day/session sequence, wasteful for a fine one over a wide view) — hoist it or
|
|
102
|
+
* `useMemo` it.
|
|
103
|
+
*/
|
|
104
|
+
cursorSequence?: Sequence | BoundedSequence;
|
|
105
|
+
/**
|
|
106
|
+
* Makes the `region` cursor **draggable**: drag across the plot and the band
|
|
107
|
+
* extends **bucket by bucket** (snapping to `cursorSequence` points); on
|
|
108
|
+
* release this fires **once** with the selected `[start, end)` `TimeRange`, and
|
|
109
|
+
* the cursor reverts to the single-bucket highlight (it does not keep the
|
|
110
|
+
* range). Typical use — zoom the view to the returned range (the container
|
|
111
|
+
* doesn't zoom itself; that's the consumer's call).
|
|
112
|
+
*
|
|
113
|
+
* With **no `cursorSequence`** the region cursor is the degenerate case — it
|
|
114
|
+
* renders as a **line** on hover and the drag is **freeform** (raw `[start,
|
|
115
|
+
* end)`, no bucket snapping); the same callback fires on release. No-op unless
|
|
116
|
+
* `cursor="region"` (and a **time** x-axis).
|
|
117
|
+
*/
|
|
118
|
+
onRegionSelect?: (range: TimeRange) => void;
|
|
119
|
+
/**
|
|
120
|
+
* Which modifier a region-drag needs — set `'shift'` when you also enable
|
|
121
|
+
* `panZoom` and want **plain drag to pan, shift-drag to select**. It's only
|
|
122
|
+
* enforced while `panZoom` is on (with pan off there's no gesture conflict, so
|
|
123
|
+
* shift is optional — either drag selects). **Omitted** ⇒ a region-drag
|
|
124
|
+
* **preempts** pan (drag always selects; document that precedence for users).
|
|
125
|
+
* Wheel-zoom is unaffected in every case.
|
|
126
|
+
*/
|
|
127
|
+
regionSelectModifier?: 'shift';
|
|
81
128
|
/**
|
|
82
129
|
* Fires on pointer move with the hovered time + every series' value there (so
|
|
83
130
|
* you can render a readout outside the chart), and `null` on leave.
|
|
@@ -226,5 +273,5 @@ export interface ChartContainerProps {
|
|
|
226
273
|
* {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
|
|
227
274
|
* (`<YAxis>`).
|
|
228
275
|
*/
|
|
229
|
-
export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, theme, discontinuities, calendar, spacing, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
276
|
+
export declare function ChartContainer({ range, width, rowGap, showAxis, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom, onTimeRangeChange, minDuration, cursor, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime, crosshairSnap, editAnnotations, creating, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap, timeFormat, theme, discontinuities, calendar, spacing, children, }: ChartContainerProps): import("react/jsx-runtime").JSX.Element;
|
|
230
277
|
//# sourceMappingURL=ChartContainer.d.ts.map
|
package/dist/ChartContainer.js
CHANGED
|
@@ -2,6 +2,8 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
2
2
|
import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState, } from 'react';
|
|
3
3
|
import { scaleLinear, scaleTime } from 'd3-scale';
|
|
4
4
|
import { scaleTradingTime, } from './tradingTimeScale.js';
|
|
5
|
+
import { scaleBand } from './bandScale.js';
|
|
6
|
+
import { Sequence } from 'pond-ts';
|
|
5
7
|
import { ContainerContext, } from './context.js';
|
|
6
8
|
import { maxSlotWidths, sum } from './slots.js';
|
|
7
9
|
import { computeLabelLanes } from './annotations.js';
|
|
@@ -32,7 +34,7 @@ function normalizeRange(range) {
|
|
|
32
34
|
* {@link TimeAxis} at the bottom, aligned under the plots. Y axes are per-row
|
|
33
35
|
* (`<YAxis>`).
|
|
34
36
|
*/
|
|
35
|
-
export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorTime = false, crosshairSnap = true, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, theme, discontinuities, calendar, spacing, children, }) {
|
|
37
|
+
export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, selected, onSelect, hovered, onHover, panZoom = false, onTimeRangeChange, minDuration = 1, cursor = DEFAULT_CURSOR_MODE, cursorSequence, onRegionSelect, regionSelectModifier, cursorTime = false, crosshairSnap = true, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, theme, discontinuities, calendar, spacing, children, }) {
|
|
36
38
|
// The explicit base domain from `range` (a tuple or a TimeRange). `undefined`
|
|
37
39
|
// ⇒ auto-fit (resolved from the layers below). Pan/zoom seeds from it; `seed`
|
|
38
40
|
// is the placeholder while auto-fitting.
|
|
@@ -78,6 +80,8 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
78
80
|
// still cursor stays put while a live window slides under it; a controlled
|
|
79
81
|
// `trackerPosition` resolves to a pixel below.
|
|
80
82
|
const [hoverX, setHoverX] = useState(null);
|
|
83
|
+
// The region-cursor drag anchor (epoch ms) — set on press, cleared on release.
|
|
84
|
+
const [regionAnchor, setRegionAnchor] = useState(null);
|
|
81
85
|
// The free-form crosshair also needs the pointer's y + which row (row-specific,
|
|
82
86
|
// unlike the shared x). One state object so a move updates both atomically.
|
|
83
87
|
const [hoverPoint, setHoverPoint] = useState(null);
|
|
@@ -151,11 +155,31 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
151
155
|
else if (kind !== s.xKind) {
|
|
152
156
|
throw new Error(`ChartContainer: rows mix x-axis kinds ('${kind}' and '${s.xKind}'). ` +
|
|
153
157
|
`A container has one shared x axis — every row must plot the same ` +
|
|
154
|
-
`kind (all time-keyed,
|
|
158
|
+
`kind (all time-keyed, all value-keyed, or all category).`);
|
|
155
159
|
}
|
|
156
160
|
}
|
|
157
161
|
return kind ?? 'time';
|
|
158
162
|
}, [sources]);
|
|
163
|
+
// A `'category'` container's ordered category names — the ordinal axis domain.
|
|
164
|
+
// Every category layer must agree on the same list (a mix is an error, like the
|
|
165
|
+
// kind), so the shared band scale has one authoritative slot order. `null` when
|
|
166
|
+
// no category layer has registered (or the kind isn't category).
|
|
167
|
+
const categories = useMemo(() => {
|
|
168
|
+
let cats = null;
|
|
169
|
+
for (const s of sources.values()) {
|
|
170
|
+
const c = s.xCategories?.() ?? null;
|
|
171
|
+
if (c === null)
|
|
172
|
+
continue;
|
|
173
|
+
if (cats === null)
|
|
174
|
+
cats = c;
|
|
175
|
+
else if (cats.length !== c.length || cats.some((v, i) => v !== c[i])) {
|
|
176
|
+
throw new Error(`ChartContainer: category rows disagree on the axis categories. ` +
|
|
177
|
+
`Every category layer in one container must share the same ordered ` +
|
|
178
|
+
`column set (got [${cats.join(', ')}] and [${c.join(', ')}]).`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
return cats;
|
|
182
|
+
}, [sources]);
|
|
159
183
|
// Auto-fit extent — the union of the layers' x extents — used as the domain
|
|
160
184
|
// when no explicit `range` is given. (Same source registry as the kind; the
|
|
161
185
|
// two-pass register→resolve applies.)
|
|
@@ -300,6 +324,17 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
300
324
|
: undefined, [resolvedKind, discontinuities, calendar, spacing]);
|
|
301
325
|
const xDiscontinuities = resolvedKind === 'time' ? (discontinuities ?? calendarProvider) : undefined;
|
|
302
326
|
const { xScale, formatTime } = useMemo(() => {
|
|
327
|
+
if (resolvedKind === 'category') {
|
|
328
|
+
// Ordinal column-domain axis: a band scale over the category slots. The
|
|
329
|
+
// domain is **always** `[0, n]` (one unit slot per category) — NOT the
|
|
330
|
+
// resolved `[d0, d1]`: a category axis ignores an explicit `range` (its
|
|
331
|
+
// slots are absolute `0..n`, matching `categoryStack`), so an out-of-`[0,n]`
|
|
332
|
+
// range can't silently offset the labels from the bars. The pixel mapping
|
|
333
|
+
// stays linear; the formatter is the category-name lookup.
|
|
334
|
+
const cats = categories ?? [];
|
|
335
|
+
const s = scaleBand(cats).domain([0, cats.length]).range([0, plotWidth]);
|
|
336
|
+
return { xScale: s, formatTime: (v) => s.label(v) };
|
|
337
|
+
}
|
|
303
338
|
if (resolvedKind === 'value') {
|
|
304
339
|
const s = scaleLinear().domain([d0, d1]).range([0, plotWidth]);
|
|
305
340
|
return {
|
|
@@ -323,12 +358,42 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
323
358
|
xScale: s,
|
|
324
359
|
formatTime: resolveTimeFormat(s, TIME_TICK_COUNT, timeFormat),
|
|
325
360
|
};
|
|
326
|
-
}, [
|
|
361
|
+
}, [
|
|
362
|
+
resolvedKind,
|
|
363
|
+
categories,
|
|
364
|
+
d0,
|
|
365
|
+
d1,
|
|
366
|
+
plotWidth,
|
|
367
|
+
timeFormat,
|
|
368
|
+
xDiscontinuities,
|
|
369
|
+
]);
|
|
327
370
|
// The crosshair pixel (see resolveCursorX). A stored hoverX is a *plot* pixel;
|
|
328
371
|
// if plotWidth changes mid-hover (a gutter reserving, or a width change) it's
|
|
329
372
|
// briefly stale until the next pointer move — rare, and the bounds check below
|
|
330
373
|
// hides an out-of-plot crosshair meanwhile.
|
|
331
374
|
const cursorX = resolveCursorX(trackerPosition, hoverX, xScale);
|
|
375
|
+
// `cursor="region"` buckets: realize the `cursorSequence` over the current view
|
|
376
|
+
// (a `Sequence` → `.bounded`; a `BoundedSequence` used as-is), so the band can
|
|
377
|
+
// find the interval under the pointer. Memoized on the sequence + view range;
|
|
378
|
+
// a coarse sequence (days / sessions) is a handful of intervals.
|
|
379
|
+
// A `Sequence` bucket is a **time** interval, so gate it to a time axis — on a
|
|
380
|
+
// value axis (a horizontal histogram, a value-keyed chart) the value domain is
|
|
381
|
+
// not epoch-ms, so realizing time buckets over it is meaningless (it would
|
|
382
|
+
// shade the whole plot). Region cursor is time-axis only.
|
|
383
|
+
const cursorBuckets = useMemo(() => {
|
|
384
|
+
if (cursorSequence === undefined || resolvedKind !== 'time')
|
|
385
|
+
return undefined;
|
|
386
|
+
if (!(cursorSequence instanceof Sequence))
|
|
387
|
+
return cursorSequence.intervals();
|
|
388
|
+
// `bounded` (sample 'begin') drops a partial *leading* bucket — the one that
|
|
389
|
+
// contains the view start begins before it. Widen the realized range back by
|
|
390
|
+
// one bucket width so that covering bucket is included (a coarse calendar
|
|
391
|
+
// unit is bounded at ~a year; a fixed step uses its own width).
|
|
392
|
+
const back = cursorSequence.kind() === 'fixed'
|
|
393
|
+
? cursorSequence.stepMs()
|
|
394
|
+
: 366 * 86_400_000;
|
|
395
|
+
return cursorSequence.bounded({ start: d0 - back, end: d1 }).intervals();
|
|
396
|
+
}, [cursorSequence, d0, d1, resolvedKind]);
|
|
332
397
|
// Emit { time, values } for an outside readout — recomputed as the cursor moves
|
|
333
398
|
// *or* the window slides under it (xScale change → new time at the same pixel).
|
|
334
399
|
// Out of the plot (null, or a controlled trackerPosition d3 extrapolated past
|
|
@@ -369,6 +434,11 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
369
434
|
cursorRowKey: hoverPoint?.rowKey ?? null,
|
|
370
435
|
setHoverY,
|
|
371
436
|
crosshairSnap,
|
|
437
|
+
cursorBuckets,
|
|
438
|
+
regionAnchor,
|
|
439
|
+
setRegionAnchor,
|
|
440
|
+
onRegionSelect,
|
|
441
|
+
regionSelectModifier,
|
|
372
442
|
draggingKey,
|
|
373
443
|
setDragging,
|
|
374
444
|
selected: selectedValue,
|
|
@@ -417,6 +487,11 @@ export function ChartContainer({ range, width, rowGap = 0, showAxis = true, trac
|
|
|
417
487
|
hoverPoint,
|
|
418
488
|
setHoverY,
|
|
419
489
|
crosshairSnap,
|
|
490
|
+
cursorBuckets,
|
|
491
|
+
regionAnchor,
|
|
492
|
+
setRegionAnchor,
|
|
493
|
+
onRegionSelect,
|
|
494
|
+
regionSelectModifier,
|
|
420
495
|
draggingKey,
|
|
421
496
|
setDragging,
|
|
422
497
|
selectedValue,
|