@pond-ts/charts 0.40.0 → 0.42.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -3,12 +3,14 @@
3
3
  All notable changes to this project are documented here.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
- The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`, and
7
- `@pond-ts/fit` — release together under a single `v*` tag, so this file covers
8
- them all. Pre-1.0: minor bumps may include new features and type-level changes;
9
- patch bumps are strictly additive.
10
-
11
- [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.40.0...HEAD
6
+ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
7
+ `@pond-ts/fit`, and `@pond-ts/financial` — release together under a single `v*`
8
+ tag, so this file covers them all. Pre-1.0: minor bumps may include new features
9
+ and type-level changes; patch bumps are strictly additive.
10
+
11
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.42.0...HEAD
12
+ [0.42.0]: https://github.com/pjm17971/pond-ts/compare/v0.41.0...v0.42.0
13
+ [0.41.0]: https://github.com/pjm17971/pond-ts/compare/v0.40.0...v0.41.0
12
14
  [0.40.0]: https://github.com/pjm17971/pond-ts/compare/v0.39.0...v0.40.0
13
15
  [0.39.0]: https://github.com/pjm17971/pond-ts/compare/v0.38.0...v0.39.0
14
16
  [0.38.0]: https://github.com/pjm17971/pond-ts/compare/v0.37.0...v0.38.0
@@ -38,6 +40,101 @@ patch bumps are strictly additive.
38
40
 
39
41
  ## [Unreleased]
40
42
 
43
+ ## [0.42.0] — 2026-07-10
44
+
45
+ The **trading-calendar** release: a new `@pond-ts/financial` package (its first
46
+ publish) and a discontinuous **trading-time x axis** in `@pond-ts/charts` that
47
+ collapses closed-market time (weekends, holidays, overnight, lunch breaks).
48
+
49
+ ### Added
50
+
51
+ - **`@pond-ts/financial` — new package (first release).** A calendar/analytics
52
+ layer on `pond-ts` (peer dep; ESM, no React; `@js-temporal/polyfill` for
53
+ DST-correct session generation):
54
+ - **`TradingCalendar`** — `fromSessions` (explicit schedule) and `fromRules`
55
+ (weekmask / holidays / early-closes / breaks, DST-correct via Temporal);
56
+ query surface (`sessionOn`, `sessionContaining`, `isOpen`, `sessionsInRange`,
57
+ `nextSession`, `previousSession`).
58
+ - **Bucketing seam** — `sessionSequence()` / `barSequence(period)` return a
59
+ `BoundedSequence` that flows straight through `aggregate` / `materialize`,
60
+ so every bucket is a real trading session/bar (no weekend/holiday buckets,
61
+ no bucket spanning a closure). Zero core edits.
62
+ - **`tagSessions(series, { column?, stamped? })`** — appends a session-id
63
+ column (`number | undefined`) for `partitionBy` so stateful ops don't bridge
64
+ a session boundary. `stamped: 'close'` bins a bar stamped at its close into
65
+ the closing session (`(open, close]`) for OHLC feeds.
66
+ - **`DiscontinuityProvider`** — the d3fc-style 5-method provider
67
+ (`clampUp`/`clampDown`/`distance`/`offset`/`copy` + optional `boundaries`);
68
+ `identityDiscontinuity`, `weekendSkip` (bundled reference), and
69
+ `segmentDiscontinuity(segments, { spacing })`.
70
+ - **`TradingCalendar.discontinuities({ range?, spacing?, period? })`** — the
71
+ chart-ready provider; `spacing: 'proportional'` (default, true-time) or
72
+ `'uniform'` (equal-width per session/bar, the TradingView ordinal look).
73
+ - **`@pond-ts/charts`: trading-time x axis.** Pass a `@pond-ts/financial`
74
+ provider (structurally — charts never imports that package) to collapse
75
+ closed-market gaps:
76
+ - **`ChartContainer` `discontinuities` prop** (low-level) and **`calendar` +
77
+ `spacing` props** (high-level sugar; `calendar` is a structural
78
+ `TradingCalendarLike`, `spacing` defaults to proportional).
79
+ - **`scaleTradingTime`** — a d3-scale-shaped discontinuous time scale; ticks
80
+ coarsen to a **calendar grain** (week/month/quarter/year starts) with
81
+ date/year labels, and **session dividers** draw at the collapse points
82
+ (`theme.axis.sessionDivider`), aligned with the labels.
83
+ - `Charts/TradingTimeAxis` stories (weekend/holiday/half-day/intraday,
84
+ continuous-vs-trading, daily-months, proportional-vs-uniform).
85
+ - **`@pond-ts/charts`:** selection now has a stable series identity. `SelectInfo`
86
+ carries an `id`, and `BarChart` / `ScatterChart` take an optional `id` prop —
87
+ the series identity used for selection + hover. An `id` **gates interactivity**:
88
+ a layer is selectable/hoverable only when given one (a layer with no `id`
89
+ renders and reads out but can't be selected). A dev-warning fires when
90
+ `selected`/`onSelect` is wired but no layer carries an `id`. First slice of the
91
+ selection RFC (`docs/rfcs/selection.md`, Amendments 2–3).
92
+ - **`pond-ts`: `bin(W, 'minMaxFirstLast')`** — the four-channel M4 downsampling
93
+ reducer (per-bin min/max/first/last, validity-aware, chunked-delegating); the
94
+ foundation for the charts decimator wave.
95
+ - **`pond-ts`: `binBy`** — key-domain bucketed reduction (the M4 gappy-data
96
+ decimation path).
97
+
98
+ ### Changed
99
+
100
+ - **BREAKING (`@pond-ts/charts`):** `SelectInfo` gained a required `id` field
101
+ (`{ id, key, value, color, label }`) — `id` is the selection identity, `key` /
102
+ `value` are now click provenance. Code that constructs a `SelectInfo` by hand
103
+ must add `id`, and selection equality/dedup now keys on `id`, not the sample
104
+ `begin`.
105
+ - **BREAKING (`@pond-ts/charts`):** `BarChart` / `ScatterChart` selection now
106
+ requires an explicit `id` prop — the previous implicit `as ?? column` selection
107
+ identity is gone. A selectable bar/scatter layer must add `id` (e.g.
108
+ `<BarChart series={s} column="v" id="v" />`); without it the layer is
109
+ display-only.
110
+
111
+ ## [0.41.0] — 2026-07-06
112
+
113
+ ### Added
114
+
115
+ - **`@pond-ts/charts`: `<Candlestick>` — a first-class OHLC mark** (Phase 1 of
116
+ the financial-charts RFC, Tidal-driven). `open`/`high`/`low`/`close` props
117
+ default to the conventional names (`<Candlestick series={s} />` for a standard
118
+ OHLCV series); draws-only (body extents derived per-mark); **point- or
119
+ interval-keyed** so raw daily OHLCV feeds straight in (no `aggregate`), while a
120
+ weekly/monthly rollup is the identical call. `variant: 'candle' | 'bar' |
121
+ 'hollow'`, `colorBy: 'direction' | 'series'`, `gap`, and `showOHLC` (four-pill
122
+ O/H/L/C hover readout; default is a single `close` pill keyed on `as`).
123
+ Participates in the crosshair x-snap (unlike `BoxPlot`). Supersedes `BoxPlot
124
+ shape='solid'` for OHLC data.
125
+ - **`@pond-ts/charts`: `ohlcFromTimeSeries`** + the `OhlcSeries` / `OhlcColumns`
126
+ types — read four price columns into a chart-ready columnar view (exported
127
+ alongside the existing `*FromTimeSeries` builders).
128
+
129
+ ### Changed
130
+
131
+ - **`@pond-ts/charts`: `ChartTheme` gains a required `candle` slot** (a
132
+ `CandleStyle`: `rising`/`falling`/`neutral` body+wick pairs, `bodyWidth`,
133
+ `wickWidth`). `defaultTheme` and `estelaTheme` ship neutral, **unbranded**
134
+ up/down pairs — market green/red is a `cssVarTheme` overlay, not a library
135
+ default. **Breaking (type-level):** a hand-built `ChartTheme` that doesn't
136
+ derive from a shipped theme must add a `candle` slot to compile.
137
+
41
138
  ## [0.40.0] — 2026-07-05
42
139
 
43
140
  A **core + charts** release from the estela `DataChart`-port friction wave.
@@ -1,42 +1,101 @@
1
1
  import { ValueSeries } from 'pond-ts';
2
2
  import type { SeriesSchema, TimeSeries, ValueSeriesSchema } from 'pond-ts';
3
+ import { type BinRecord } from './data.js';
4
+ import { type Orientation } from './bars.js';
3
5
  export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema> {
4
6
  /**
5
- * The source series. **Interval / timeRange-keyed** `TimeSeries` is the primary
6
- * form — each event's key `[begin, end]` is a bar's x-span. A **point-keyed**
7
- * (`time`) series is supported too: each bar's width is derived from neighbour
8
- * spacing (see {@link barsFromTimeSeries}). A **`ValueSeries`**
9
- * (`series.byValue('dist')`) bars against its value axis — also point-keyed, so
10
- * the same neighbour-spacing span applies (see {@link barsFromValueSeries}); the
11
- * container infers the x-kind from the data, no axis-type prop (mirrors the
12
- * other layers).
7
+ * The source series. Provide **exactly one** of `series` or `bins`.
13
8
  *
14
- * **Live charts:** `series.byValue(…)` mints a *fresh* projection each call, so
15
- * an inline `series={s.byValue('dist')}` re-registers this layer every render —
16
- * on a frequently re-rendering chart, memoize the projection (`useMemo`).
9
+ * - A **`TimeSeries`** (interval / timeRange-keyed is the primary form — each
10
+ * event's key `[begin, end]` is a bar's x-span; a point-keyed series derives
11
+ * its width from neighbour spacing) single-series bars via `column`, or
12
+ * stacked bars from a **wide** series via `columns`.
13
+ * - A **`ValueSeries`** (`series.byValue('dist')`) bars against its value axis.
14
+ * - A **`ReadonlyMap<group, TimeSeries>`** — one series per stack group, all on
15
+ * the same bin grid, the shape
16
+ * `series.partitionBy('host', { groups }).aggregate(seq, m).toMap()` returns.
17
+ * Stacked bars, `column` names the shared value column, groups = map order.
18
+ *
19
+ * **Live charts:** `series.byValue(…)` / `.toMap()` mint fresh objects each
20
+ * call, so an inline `series={…}` re-registers this layer every render — on a
21
+ * frequently re-rendering chart, memoize the projection (`useMemo`).
22
+ */
23
+ series?: TimeSeries<S> | ValueSeries<VS> | ReadonlyMap<string, TimeSeries<S>>;
24
+ /**
25
+ * `byColumn` **bin records** — `Array<{ start, end, …aggregates }>` from a
26
+ * value-band aggregation
27
+ * (`series.byColumn('power', { width: 20 }, { seconds: … })`). The value-axis
28
+ * alternative to `series`: `column` / `columns` name the aggregate field(s) to
29
+ * draw. Pair with `ordinal` for a category (band) axis.
30
+ */
31
+ bins?: readonly BinRecord[];
32
+ /** Name of the numeric value column for the bar height (single series). Provide
33
+ * `column` **or** `columns`, not both. */
34
+ column?: string;
35
+ /**
36
+ * Stacked-segment columns, **bottom → top** — one segment per name. Use with a
37
+ * **wide** `series` (e.g. `pivotByGroup` output) or with `bins`. Mutually
38
+ * exclusive with `column`, and invalid with a `Map` series (there the segments
39
+ * are the map's groups; use `column`).
17
40
  */
18
- series: TimeSeries<S> | ValueSeries<VS>;
19
- /** Name of the numeric value column for the bar height. */
20
- column: string;
41
+ columns?: readonly string[];
21
42
  /**
22
- * The series' semantic identifier — what the data _is_ / how it should read.
23
- * The theme maps it to a {@link BarStyle} (`theme.bar[as] ?? theme.bar.default`).
24
- * **Omitted the `default` style** `column` is the data, `as` is the
25
- * identity, and there's no per-component colour override (the single styling
26
- * channel; restyle via the theme).
43
+ * The single series' semantic identifier — what the data _is_. The theme maps
44
+ * it to a {@link BarStyle} (`theme.bar[as] ?? theme.bar.default`). **Single
45
+ * series only** **ignored** (not an error) on a stacked chart, which colours
46
+ * its segments per group instead (see `colors`).
27
47
  */
28
48
  as?: string;
29
49
  /**
30
- * Which `<YAxis>` (by its `id`) this bar scales against picks the *scale*,
31
- * where `as` picks the *style* (separate concerns). **Omitted ⇒ the row's
32
- * default axis.**
50
+ * Per-group colour override for a **stacked** chart `{ group: cssColor }`.
51
+ * A segment resolves `colors[group] ?? theme.bar[group]?.fill ??
52
+ * theme.bar.default.fill`, so named roles (e.g. a `crit` band styled in the
53
+ * theme) come from the theme while ad-hoc groups (five hosts) take a colour
54
+ * here without minting a theme role. The single styling channel still holds:
55
+ * this is the stack's one colour input.
56
+ */
57
+ colors?: Readonly<Record<string, string>>;
58
+ /**
59
+ * Bar growth direction (the histogram orientation). **Default `'vertical'`.**
60
+ *
61
+ * - `'vertical'` — bars grow **up** from a value baseline, bins on the **x**
62
+ * axis (time buckets, value bands). The column / time-histogram look.
63
+ * - `'horizontal'` — bars grow **right**, bins on the **y** axis (a band axis
64
+ * like heart-rate zones). Label the bands with `<YAxis ticks={[{ at, label }]}>`.
65
+ *
66
+ * A `'horizontal'` chart puts the **value** on the shared x axis, so its
67
+ * container's x-kind is `'value'` — it cannot share a `<ChartContainer>` with
68
+ * time-series rows (each horizontal histogram stands alone). Vertical charts
69
+ * have no such constraint. The in-chart `flag` / `crosshair` value cursor is
70
+ * drawn for the **single-series vertical** case only; stacked and horizontal
71
+ * charts read out via hover / click (`onHover` / `onSelect`).
72
+ */
73
+ orientation?: Orientation;
74
+ /**
75
+ * For `bins`: lay the bands out as uniform **unit slots** (`[i, i+1]`) instead
76
+ * of their numeric `[start, end]` edges — an ordinal band axis where every band
77
+ * reads the same width (heart-rate zones). Ignored for `series`.
78
+ */
79
+ ordinal?: boolean;
80
+ /**
81
+ * The **stable series identity** for selection + hover — and it **gates
82
+ * interactivity** (a bar layer is selectable/hoverable only when given an
83
+ * `id`). For a stack, a clicked / hovered **segment** is identified by
84
+ * `(id, key = bin begin, label = group)`, so two segments in one bin don't both
85
+ * light up.
86
+ */
87
+ id?: string;
88
+ /**
89
+ * Which `<YAxis>` (by its `id`) this layer scales against — the *scale* (`as`
90
+ * picks the *style*). **Omitted ⇒ the row's default axis.** For a horizontal
91
+ * histogram this is the **bin (band) axis**; for a vertical one the **value**
92
+ * axis.
33
93
  */
34
94
  axis?: string;
35
95
  /**
36
- * Pixel gap between adjacent bars — each bar's key span is inset by this total
37
- * (half each side), so neighbours breathe. **Omitted ⇒ the theme's
38
- * `bar[as].gap`.** A span the gap would invert collapses to the style's
39
- * `minWidth`, so a too-thin bucket stays visible.
96
+ * Pixel gap between adjacent bars / bins the bar's key span is inset by this
97
+ * total (half each side). **Omitted ⇒ the theme's `bar` `gap`.** A span the gap
98
+ * would invert collapses to the style's `minWidth`.
40
99
  */
41
100
  gap?: number;
42
101
  /**
@@ -46,43 +105,47 @@ export interface BarChartProps<S extends SeriesSchema = SeriesSchema, VS extends
46
105
  index?: number;
47
106
  }
48
107
  /**
49
- * A bar draw layer: one rectangle per event, spanning the key's `[begin, end]`
50
- * (inset by `gap`) from the axis baseline to a numeric `column`'s value. Reads
51
- * the key endpoints + column into a {@link BarSeries}, registers into the
52
- * enclosing {@link Layers} (scaling against its `axis`), and renders nothing to
53
- * the DOM the row draws it. A gap (missing value) is skipped (no bar).
108
+ * A bar / histogram draw layer. In its simplest form, one rectangle per event
109
+ * spanning the key's `[begin, end]` from the axis baseline to a numeric
110
+ * `column`'s value (see below). It also draws **stacked** bars (a group-by
111
+ * dimension segments, `columns` / a `Map` series / `bins`) and **horizontal**
112
+ * bars (`orientation='horizontal'`, bins on the y axis) first-class histogram
113
+ * support. Registers into the enclosing {@link Layers} and renders nothing to the
114
+ * DOM; the row draws it.
54
115
  *
55
- * **Baseline.** Bars rest on the zero line when the axis domain spans zero (the
56
- * common all-positive auto-fit case {@link barExtent} pulls `0` into the
57
- * domain), or on the axis floor when an explicit `<YAxis min={…}>` sits above
58
- * zero (see {@link resolveBarBaseline}).
116
+ * **Data sources.** A time / value `TimeSeries` or `ValueSeries` (`column`), a
117
+ * wide series or `bins` array (`columns`), or a `Map<group, TimeSeries>`
118
+ * (`column`) the last three stack. Every shape composes from pond's own
119
+ * aggregation (`aggregate` / `byColumn` / `partitionBy`); the histogram guide
120
+ * has the recipes.
59
121
  *
60
- * **Interaction.** Hover joins the tracker (`sampleAt` the value of the bar
61
- * **under the cursor**) and lights that bar (hover-highlight). Click selects the
62
- * hit bar (`hitTest`); the matching bar — same key **and** this series' `label`,
63
- * so two series sharing a timestamp don't both light up — draws highlighted
64
- * (outlined for the committed select, fill-only for the transient hover). Both
65
- * resolve by **containment**: the tracker by the bar's `[begin, end]` time span
66
- * (`barIndexAtTime`), the click by the bar's pixel rect (`barAt`) — so the
67
- * readout reads the same bar you click, even across a wide bucket (they differ
68
- * only by the `gap` inset, where the pixel rect is narrower than the span).
122
+ * **Baseline (single, vertical).** Bars rest on the zero line when the axis
123
+ * domain spans zero, or on the axis floor when an explicit `<YAxis min>` sits
124
+ * above zero (see {@link resolveBarBaseline}).
69
125
  *
70
- * Both channels are also **controllable from outside** the chart via the
71
- * container: `selected`/`onSelect` (committed) and `hovered`/`onHover` (transient)
72
- * pass either to pin the lit/selected bar from a legend or list row, and read
73
- * the callback to mirror a bar-originated hover/click out-of-band. Symmetric pair,
74
- * keyed by the same {@link SelectInfo} identity.
126
+ * **Baseline (stacked).** A stack is **cumulative from value 0** the segments
127
+ * sum upward from the zero line, so its value axis **must include 0**. The
128
+ * auto-fit guarantees this: {@link stackValueExtent} always returns `[0, maxTotal]`.
129
+ * An explicit `<YAxis min>` **above** 0 is therefore unsupported for a stack — it
130
+ * would hide the bottom of the cumulative column; only the portion above the floor
131
+ * draws (clipped cleanly at the plot floor, as any bar below an explicit floor is).
132
+ * Segment values are assumed **non-negative** (a negative or zero segment is
133
+ * skipped — diverging stacks are out of scope).
75
134
  *
76
- * **Value axis** bars also scale on a value axis when fed a `ValueSeries`
77
- * (`series.byValue('dist')`): estela's distance-domain splits/laps, one bar per
78
- * segment over a monotonic axis. A `ValueSeries` is point-keyed, so the span is
79
- * neighbour-derived like a point `TimeSeries` (see {@link barsFromValueSeries}).
135
+ * **Interaction (opt-in via `id`).** Hover lights the bar / segment under the
136
+ * cursor (hit-tested by pixel rect, so it works in both orientations); click
137
+ * selects it (outlined). A stacked segment's identity is `(id, key = bin begin,
138
+ * label = group)`. Both channels are controllable from outside via the container
139
+ * (`selected`/`onSelect`, `hovered`/`onHover`). The in-chart `flag`/`crosshair`
140
+ * value cursor is single-series-vertical only.
80
141
  *
81
142
  * ```tsx
82
143
  * <Layers>
83
144
  * <BarChart series={hourlyVolume} column="count" />
145
+ * <BarChart series={byHost} column="n" colors={{ web1: '#…' }} />
146
+ * <BarChart bins={powerDist} column="seconds" orientation="horizontal" ordinal />
84
147
  * </Layers>
85
148
  * ```
86
149
  */
87
- export declare function BarChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, column, as: semantic, axis, gap, index, }: BarChartProps<S, VS>): null;
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;
88
151
  //# sourceMappingURL=BarChart.d.ts.map