@pond-ts/charts 0.61.0 → 0.63.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/API.md +25 -25
- package/CHANGELOG.md +165 -1
- package/dist/ChartContainer.d.ts +72 -0
- package/dist/ChartContainer.js +151 -24
- package/dist/ChartRow.d.ts +39 -3
- package/dist/ChartRow.js +194 -7
- package/dist/Layers.js +16 -2
- package/dist/XAxis.d.ts +6 -0
- package/dist/XAxis.js +80 -2
- package/dist/YAxis.d.ts +66 -2
- package/dist/YAxis.js +141 -4
- package/dist/annotations.js +14 -7
- package/dist/chip.d.ts +50 -7
- package/dist/chip.js +66 -9
- package/dist/context.d.ts +121 -4
- package/dist/cursors.js +33 -8
- package/dist/domain.d.ts +18 -0
- package/dist/domain.js +33 -0
- package/dist/use-axis-gestures.d.ts +79 -0
- package/dist/use-axis-gestures.js +259 -0
- package/package.json +3 -3
package/API.md
CHANGED
|
@@ -521,31 +521,31 @@ Typed dataflow graphs for pipelines whose **shape is data** (runtime-assembled,
|
|
|
521
521
|
user-edited, one computation fanned out to several consumers). Chaining stays
|
|
522
522
|
the default for pipelines known at authoring time — see the package README.
|
|
523
523
|
|
|
524
|
-
| Group | Exports
|
|
525
|
-
| ----------------------- |
|
|
526
|
-
| Worker pool (Node) | `HostPool` (`start`, `run`, `close`, `size`, `inFlight`); types `HostPoolOptions`, `PoolSetup`, `PoolSetupConfig`; `toWire` / `fromWire`, types `WireResult`, `WireColumn` — subpath `@pond-ts/process/pool`
|
|
527
|
-
| Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`)
|
|
528
|
-
| Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline)
|
|
529
|
-
| Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues`
|
|
530
|
-
| Sources | `source<T>()` → `SourceNode` (`set()`), `fromLive(liveSource)` → `LiveSourceNode` (`dispose()`); `GraphSource` (bind contract — looser than core's `LiveSource`, accepts `LiveAggregation`), `SnapshotSource`, `NoInputs`
|
|
531
|
-
| Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson`
|
|
532
|
-
| Range output buffers | `prepareRange(length, keep, prior)` → `RangeOutput` (`values`, `bits`, `set`, `clear`) carrying `[0, keep)` forward as blocks — values **and** validity; `sealRange(out, length)` → `Float64Column`; `validityByteCount`. Reached from an op as `ctx.out[n]`
|
|
533
|
-
| Ranged recompute | `graph.setSourceFrom(series, changedFrom)` — declares which row first changed; `graph.recomputes` → `{ ranged, full }`. An op opts in with `OpDef.runRange(ctx)`, receiving `{ from, to, previous, previousView, out }` (type `RangeContext`) — write into `out` and return nothing for the block path alongside the usual context. Requires `lookback`. Falls back to a full `run` whenever anything is missing
|
|
534
|
-
| Node budget | `bind(series, { registry, budgetBytes })` — engine-wide cap on retained node values, LRU, enforced after each `run`; `graph.retainedBytes` / `graph.evictions` / `graph.enforceBudget()`. Unbounded when omitted. Skips a node whose consumer still holds its outlet
|
|
535
|
-
| Plan history | `requiredHistory(registry, plan)` → `{ known, rows?, undeclared, byOp }` — the minimum safe tail in rows, folded from per-op `OpDef.lookback`. Sums along nesting, maxes across siblings. `known: false` names ops with no declared lookback rather than defaulting to zero (type `HistoryResult`)
|
|
536
|
-
| Column values | `packColumn` (values → packed `Float64Column`, NaN = missing), `columnBytes` (retained size, for a byte budget), `appendColumn` (column → series; boxing-free when gapless), `columnBuffers` / `columnFromBuffers` (the buffer pair a column is, for an isolate boundary; type `ColumnBuffers`), `columnView` (zero-copy borrowed read view for in-process folds; type `ColumnView`)
|
|
537
|
-
| Plan — registry | `createRegistry({ folds })` / `Registry` (`define`, `get`, `foldFor`, `outputsOf`, `resolveParams
|
|
538
|
-
| Plan — identity | `specId` (content-addressed, param-order invariant, defaults materialized), `refToId`, `explain`, `unitOf`, `columnsOf`, `dependsOn`, `outputKey`
|
|
539
|
-
| Plan — types | `Spec`, `Plan`, `Input` (column name \| `Spec` \| `PickedOutput`), `SpecRef`, `Def` (`OpDef` \| `FoldDef`), `OpContext`, `OpResult`, `FoldContext`, `FactBody`, `isFold`, `ParamDef`, `Params`, `Units`, `InputDef`, `OutputDef`
|
|
540
|
-
| Plan — bind / run | `bind(series, { registry, units })` → `BoundGraph` (`compile`, `setSource`, `ids`, `series`, `columnOf`), `run(graph, { plan, select, onError })` → `RunResult`, `UnitError`
|
|
541
|
-
| Plan — request/response | `RunRequest` (`PlanRequest` \| `SlotRequest`), `RunOptions`, `RunResult`, `Select` (`{ on, output?, name? }` — points at a node; what comes back is what that node produces), `ErrorPolicy`, `Fact` (carries `op`), `OutputInfo`, `Skipped`, `NodeTiming` (`slot`, `pulled`, `cached`, `ms`, `inputs`)
|
|
542
|
-
| Plan — host | `createHost({ registry, units, sources })` → `Host` (`add`, `has`, `datasets`, `graphFor`, `run`, `runAsync`), `toWire`, `UnknownDatasetError`; local-string `Envelope` (`PlanEnvelope` \| `SlotEnvelope`), remote-capable `AsyncEnvelope` (`AsyncPlanEnvelope` \| `AsyncSlotEnvelope` \| `Envelope`), `DatasetInfo`, `WireResult`
|
|
543
|
-
| Plan — slots | `expandSlots(slots, columns)` → `Map<slot, Spec>` (expands to the nested form, so ids match by construction; `slot#Output` picks one output), `SlotError`; types `SlotDef` (`{ op, params, in }`), `Slots`
|
|
544
|
-
| Plan — builder | `plan(from)` → low-level `PlanBuilder`; `process(registry, from)` → typed fluent `ProcessBuilder` (`column`, op methods, `outputs`), `BuilderError`; types `NodeHandle`, `OutputHandle`, `FluentColumnRef`, `SingleColumnNode`, `MultiColumnNode`, `ColumnSelection`, `FactRef`, `BuiltRequest`
|
|
545
|
-
| Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef`
|
|
546
|
-
| Plan — folds | `STANDARD_FOLDS` and the four it holds — `last`, `extremes`, `percentileRank`, `shape` — pre-registered by `createRegistry()`; each a plain `FoldDef`, so a consumer can `define` over one
|
|
547
|
-
| Errors | `ProcessError` (base), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError`
|
|
548
|
-
| Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput`
|
|
524
|
+
| Group | Exports | Source |
|
|
525
|
+
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
526
|
+
| Worker pool (Node) | `HostPool` (`start`, `run`, `close`, `size`, `inFlight`); types `HostPoolOptions`, `PoolSetup`, `PoolSetupConfig`; `toWire` / `fromWire`, types `WireResult`, `WireColumn` — subpath `@pond-ts/process/pool` | `packages/process/src/pool/index.ts` |
|
|
527
|
+
| Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`) | `packages/process/src/port.ts` |
|
|
528
|
+
| Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline) | `packages/process/src/node.ts` |
|
|
529
|
+
| Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues` | `packages/process/src/types.ts` |
|
|
530
|
+
| Sources | `source<T>()` → `SourceNode` (`set()`), `fromLive(liveSource)` → `LiveSourceNode` (`dispose()`); `GraphSource` (bind contract — looser than core's `LiveSource`, accepts `LiveAggregation`), `SnapshotSource`, `NoInputs` | `packages/process/src/source.ts` |
|
|
531
|
+
| Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson` | `packages/process/src/graph.ts` |
|
|
532
|
+
| Range output buffers | `prepareRange(length, keep, prior)` → `RangeOutput` (`values`, `bits`, `set`, `clear`) carrying `[0, keep)` forward as blocks — values **and** validity; `sealRange(out, length)` → `Float64Column`; `validityByteCount`. Reached from an op as `ctx.out[n]` | `packages/process/src/column.ts` |
|
|
533
|
+
| Ranged recompute | `graph.setSourceFrom(series, changedFrom)` — declares which row first changed; `graph.recomputes` → `{ ranged, full }`. An op opts in with `OpDef.runRange(ctx)`, receiving `{ from, to, previous, previousView, out }` (type `RangeContext`) — write into `out` and return nothing for the block path alongside the usual context. Requires `lookback`. Falls back to a full `run` whenever anything is missing | `packages/process/src/plan/graph.ts` |
|
|
534
|
+
| Node budget | `bind(series, { registry, budgetBytes })` — engine-wide cap on retained node values, LRU, enforced after each `run`; `graph.retainedBytes` / `graph.evictions` / `graph.enforceBudget()`. Unbounded when omitted. Skips a node whose consumer still holds its outlet | `packages/process/src/plan/graph.ts` |
|
|
535
|
+
| Plan history | `requiredHistory(registry, plan)` → `{ known, rows?, undeclared, byOp }` — the minimum safe tail in rows, folded from per-op `OpDef.lookback`. Sums along nesting, maxes across siblings. `known: false` names ops with no declared lookback rather than defaulting to zero (type `HistoryResult`) | `packages/process/src/plan/history.ts` |
|
|
536
|
+
| Column values | `packColumn` (values → packed `Float64Column`, NaN = missing), `columnBytes` (retained size, for a byte budget), `appendColumn` (column → series; boxing-free when gapless), `columnBuffers` / `columnFromBuffers` (the buffer pair a column is, for an isolate boundary; type `ColumnBuffers`), `columnView` (zero-copy borrowed read view for in-process folds; type `ColumnView`) | `packages/process/src/column.ts` |
|
|
537
|
+
| Plan — registry | `createRegistry({ folds })` / `Registry` (`define`, `get`, `foldFor`, `outputsOf`, `resolveParams` (`{ validate: false }` applies defaults and skips every check), `byFamily`, `describe`, `toJsonSchema`), param builders `int` / `num` / `choice` / `flag`, `UnknownOpError`, `ParamError` | `packages/process/src/plan/registry.ts`, `params.ts` |
|
|
538
|
+
| Plan — identity | `specId(registry, spec, { validate })` (content-addressed, param-order invariant, defaults materialized; `validate: false` is **total** — names a spec that would not compile in a separate `p1?:` namespace that cannot collide with a valid id; a valid spec's id is identical either way), `SpecIdOptions`, `refToId`, `explain`, `unitOf`, `columnsOf`, `dependsOn`, `outputKey` | `packages/process/src/plan/identity.ts` |
|
|
539
|
+
| Plan — types | `Spec`, `Plan`, `Input` (column name \| `Spec` \| `PickedOutput`), `SpecRef`, `Def` (`OpDef` \| `FoldDef`), `OpContext`, `OpResult`, `FoldContext`, `FactBody`, `isFold`, `ParamDef`, `Params`, `Units`, `InputDef`, `OutputDef` | `packages/process/src/plan/types.ts` |
|
|
540
|
+
| Plan — bind / run | `bind(series, { registry, units })` → `BoundGraph` (`compile`, `setSource`, `ids`, `series`, `columnOf`), `run(graph, { plan, select, onError })` → `RunResult`, `UnitError`, `UnknownColumnError` (a raw string input naming no column of the bound series, checked over the whole closure and re-checked on the warm path) | `packages/process/src/plan/graph.ts`, `run.ts` |
|
|
541
|
+
| Plan — request/response | `RunRequest` (`PlanRequest` \| `SlotRequest`), `RunOptions`, `RunResult`, `Select` (`{ on, output?, name? }` — points at a node; what comes back is what that node produces), `ErrorPolicy`, `Fact` (carries `op`), `OutputInfo`, `Skipped` (`spec`, `select`, `reason`, `code` — the failure's kind, matching the error class a throw would have carried), `NodeTiming` (`slot`, `pulled`, `cached`, `ms`, `inputs`) | `packages/process/src/plan/run.ts` |
|
|
542
|
+
| Plan — host | `createHost({ registry, units, sources })` → `Host` (`add`, `has`, `datasets`, `graphFor`, `run`, `runAsync`), `toWire`, `UnknownDatasetError`; local-string `Envelope` (`PlanEnvelope` \| `SlotEnvelope`), remote-capable `AsyncEnvelope` (`AsyncPlanEnvelope` \| `AsyncSlotEnvelope` \| `Envelope`), `DatasetInfo`, `WireResult` | `packages/process/src/plan/host.ts` |
|
|
543
|
+
| Plan — slots | `expandSlots(slots, columns)` → `Map<slot, Spec>` (expands to the nested form, so ids match by construction; `slot#Output` picks one output), `SlotError`; types `SlotDef` (`{ op, params, in }`), `Slots` | `packages/process/src/plan/slots.ts` |
|
|
544
|
+
| Plan — builder | `plan(from)` → low-level `PlanBuilder`; `process(registry, from)` → typed fluent `ProcessBuilder` (`column`, op methods, `outputs`), `BuilderError`; types `NodeHandle`, `OutputHandle`, `FluentColumnRef`, `SingleColumnNode`, `MultiColumnNode`, `ColumnSelection`, `FactRef`, `BuiltRequest` | `packages/process/src/plan/builder.ts`, `fluent.ts` |
|
|
545
|
+
| Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef` | `packages/process/src/plan/source.ts` |
|
|
546
|
+
| Plan — folds | `STANDARD_FOLDS` and the four it holds — `last`, `extremes`, `percentileRank`, `shape` — pre-registered by `createRegistry()`; each a plain `FoldDef`, so a consumer can `define` over one | `packages/process/src/plan/folds.ts` |
|
|
547
|
+
| Errors | `ProcessError` (base; `code` — a stable per-class literal, minification-proof, also surfaced on `Skipped`), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError` | `packages/process/src/errors.ts` |
|
|
548
|
+
| Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput` | `packages/process/src/node.ts` |
|
|
549
549
|
|
|
550
550
|
Note: this package's `npm test` includes a `test:dts` step that typechecks the
|
|
551
551
|
**emitted** `dist/*.d.ts` from a consumer's perspective (`test-dts/`,
|
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,9 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
|
|
|
8
8
|
under a single `v*` tag, so this file covers them all. Pre-1.0: minor bumps may
|
|
9
9
|
include new features and type-level changes; patch bumps are strictly additive.
|
|
10
10
|
|
|
11
|
-
[Unreleased]: https://github.com/pond-ts/pond/compare/v0.
|
|
11
|
+
[Unreleased]: https://github.com/pond-ts/pond/compare/v0.63.0...HEAD
|
|
12
|
+
[0.63.0]: https://github.com/pond-ts/pond/compare/v0.62.0...v0.63.0
|
|
13
|
+
[0.62.0]: https://github.com/pond-ts/pond/compare/v0.61.0...v0.62.0
|
|
12
14
|
[0.61.0]: https://github.com/pond-ts/pond/compare/v0.60.0...v0.61.0
|
|
13
15
|
[0.60.0]: https://github.com/pond-ts/pond/compare/v0.59.0...v0.60.0
|
|
14
16
|
[0.59.0]: https://github.com/pond-ts/pond/compare/v0.58.0...v0.59.0
|
|
@@ -64,6 +66,168 @@ include new features and type-level changes; patch bumps are strictly additive.
|
|
|
64
66
|
|
|
65
67
|
## [Unreleased]
|
|
66
68
|
|
|
69
|
+
## [0.63.0] — 2026-08-18
|
|
70
|
+
|
|
71
|
+
### Added
|
|
72
|
+
|
|
73
|
+
- `@pond-ts/charts`: **axis pan/zoom**, behind a new
|
|
74
|
+
**`<ChartContainer axisPanZoom>`** (`'none'` — the default — / `'x'` / `'y'` /
|
|
75
|
+
`'xy'`). Opted in, the `<XAxis>` strip and each `<YAxis>` gutter become
|
|
76
|
+
grabbable, and **double-click** puts one back (the declared `range` on x, its
|
|
77
|
+
fit on y). The cursor stays an ordinary arrow at rest and becomes directional
|
|
78
|
+
(`↕` / `↔`) only while a gesture is running.
|
|
79
|
+
|
|
80
|
+
**The opt-in is deliberately separate from `panZoom`, in both directions.** An
|
|
81
|
+
already-interactive chart does not grow axis gestures on upgrade — nothing
|
|
82
|
+
changes for any existing chart — and a chart can scale its y axes with
|
|
83
|
+
`panZoom` off entirely, which is what you want when the plot's drag belongs to
|
|
84
|
+
a selection sweep.
|
|
85
|
+
|
|
86
|
+
**The x strip is the canvas gesture, moved to the axis** — drag pans, wheel
|
|
87
|
+
zooms about the pointer — reusing the plot's own domain-space maths, so
|
|
88
|
+
`bounds` and `minDuration` still fence the view and a trading-time axis still
|
|
89
|
+
pans and floors in _trading_ time. A category axis has no continuous domain and
|
|
90
|
+
stays inert.
|
|
91
|
+
|
|
92
|
+
**A y gutter zooms, and only the axis you grabbed** — its sibling and every
|
|
93
|
+
other row hold still. That needed a new per-axis pixel transform
|
|
94
|
+
(`RowFrame.axisTransforms`, layered under the container's uniform
|
|
95
|
+
`yTransform`), since the uniform one exists precisely so a _plot_ gesture never
|
|
96
|
+
has to pick an axis; unlike it, this is not floored at `k ≥ 1`, because
|
|
97
|
+
squashing an axis you grabbed is the point.
|
|
98
|
+
|
|
99
|
+
- `@pond-ts/charts`: **`<YAxis onBoundsChange>`** — the auto-vs-manual hand-off
|
|
100
|
+
for a scaled y axis. Fires with the `[min, max]` a gutter gesture reached, and
|
|
101
|
+
with `null` when the axis is released back to auto-fit, so a UI can show the
|
|
102
|
+
bounds, badge the scale "manual", and offer a toggle back. Providing it makes
|
|
103
|
+
the axis **controlled** (the gesture only reports; `min`/`max` fed back are
|
|
104
|
+
what it draws), exactly as `onTimeRangeChange` does for the x view; omit it and
|
|
105
|
+
the axis holds the zoom itself. `ContainerFrame` also gains `seedRange` — the
|
|
106
|
+
declared view, as against the gestured `timeRange` — which is the x reset's
|
|
107
|
+
target.
|
|
108
|
+
|
|
109
|
+
- **charts: `<ChartContainer height>` + `<ChartRow flex>` — container-owned
|
|
110
|
+
vertical layout** ([PND-HEIGHT]). `height` takes a pixel number or `'auto'`
|
|
111
|
+
(measured by the same `ResizeObserver` as `width="auto"`); omitted stays the
|
|
112
|
+
classic mode where rows declare pixels. A managed container renders as a
|
|
113
|
+
**flex column** — the rows block flexes, the x-axis strip keeps its natural
|
|
114
|
+
height — and flex rows (a bare `<ChartRow>` is `flex={1}`) divide what the
|
|
115
|
+
browser says is left, then read that height back for their y-scales.
|
|
116
|
+
|
|
117
|
+
The design constraint was that **CSS does the subtraction**: the axis
|
|
118
|
+
strip's height depends on its `label`, the theme font size, the tick
|
|
119
|
+
ladder's calendar band row at the current grain, and stacked marker pills —
|
|
120
|
+
it is not a constant a caller can subtract, and every consumer who tried
|
|
121
|
+
carried a wrong one (`20` and `24` in one codebase; this site's own
|
|
122
|
+
resizable-panels recipe said `22`). It also means non-row children between
|
|
123
|
+
rows — the recipe's draggable splitter — keep taking their natural space,
|
|
124
|
+
so that recipe reduces to its drag handler: one flex row absorbing slack
|
|
125
|
+
over one fixed row the drag resizes, no `AXIS_H`, no measuring hook.
|
|
126
|
+
|
|
127
|
+
A flex row paints nothing until its first measurement, latches its last
|
|
128
|
+
non-zero height while hidden (a `display: none` tab switch keeps its
|
|
129
|
+
scales), and warns in dev when mounted in a container that never sizes it.
|
|
130
|
+
The container **warns in dev when a measured dimension stays 0** — the
|
|
131
|
+
unconstrained-parent deadlock, which for height is the _default_ (a
|
|
132
|
+
flex-column child's height is its content) rather than an edge case.
|
|
133
|
+
|
|
134
|
+
Fixed-`height` rows keep their pixels everywhere, managed or not; a
|
|
135
|
+
container with no `height` behaves exactly as before.
|
|
136
|
+
|
|
137
|
+
### Fixed
|
|
138
|
+
|
|
139
|
+
- **charts: the crosshair's value pill lands on the axis that measured the
|
|
140
|
+
value, in that axis's colour.** With two y-axes on one side, the pill was
|
|
141
|
+
placed by side alone — always against the plot edge — so a reading taken off
|
|
142
|
+
the **outer** axis appeared over the **inner** axis's ticks, in the cursor's
|
|
143
|
+
grey: a number pinned to a ruler that never measured it. The reticle picks one
|
|
144
|
+
series, so the resolved sample now carries its axis's gutter **offset** (the
|
|
145
|
+
reserved widths of the columns between it and the plot) and its
|
|
146
|
+
`<YAxis color>`, and the pill uses both — position _and_ ink say which of two
|
|
147
|
+
stacked scales the number is on. A single axis per side is unchanged (offset
|
|
148
|
+
`0`), as is an axis that sets no colour (the theme's cursor ink).
|
|
149
|
+
|
|
150
|
+
A pill placed further out is now **bridged back to the plot edge** by a 1px
|
|
151
|
+
connector in its own colour — the y-side twin of the crosshair's x-axis time
|
|
152
|
+
connector, without which a pill a column out reads as a value floating in a
|
|
153
|
+
gutter. It draws at half opacity because, unlike the time connector's empty
|
|
154
|
+
strip, it crosses another axis's tick labels; nothing is drawn on the innermost
|
|
155
|
+
axis, where the pill already meets the line.
|
|
156
|
+
|
|
157
|
+
`<YAxis color>` is now part of the axis's registered spec rather than
|
|
158
|
+
presentation-only, because the pill is drawn by the row's cursor overlay, not
|
|
159
|
+
by `<YAxis>`. `<Baseline indicator>` — the other on-axis value pill — got the
|
|
160
|
+
same placement fix and the same connector. New `Cursors/Crosshair` stories fan
|
|
161
|
+
the states out (`AxisColor`, `StackedAxes`, `StackedAxesColored`,
|
|
162
|
+
`StackedAxesLeft`, `StackedAxesBothSides`).
|
|
163
|
+
|
|
164
|
+
Not covered: `<YAxisIndicator>` still takes an explicit `side` alongside its
|
|
165
|
+
`axis`, so it can be pointed at a gutter its axis isn't in; reconciling those
|
|
166
|
+
two props is a public-API question, left for its own change.
|
|
167
|
+
|
|
168
|
+
## [0.62.0] — 2026-08-16
|
|
169
|
+
|
|
170
|
+
### Added
|
|
171
|
+
|
|
172
|
+
- `@pond-ts/process`: **`specId` is total under `{ validate: false }`** — a
|
|
173
|
+
third options argument that names a spec which would not compile
|
|
174
|
+
(`specId(registry, spec, { validate: false })`). Identity used to be coupled
|
|
175
|
+
to validity, so the moments a consumer most needs an id — labelling the chip
|
|
176
|
+
it is skipping, keying "this persisted entry is broken", logging what was
|
|
177
|
+
rejected — were exactly the moments it threw, leaving the consumer to
|
|
178
|
+
re-implement canonicalization or carry a second key. Lenient mode still
|
|
179
|
+
applies defaults and sorts keys, carries an undeclared param through rather
|
|
180
|
+
than dropping it, and recurses into nested inputs; **a valid spec has the
|
|
181
|
+
same id under either mode**, so nothing needs a second cache line. Validity
|
|
182
|
+
stays `compile`'s job. `Registry.resolveParams` takes the same
|
|
183
|
+
`{ validate: false }`.
|
|
184
|
+
|
|
185
|
+
An id that did **not** validate is minted in a separate namespace — marked
|
|
186
|
+
`p1?:` instead of `p1:`, with type-preserving param encoding — so it can
|
|
187
|
+
never collide with a legal id. Both measures are confined to that branch:
|
|
188
|
+
a valid id is byte-identical to what shipped in 0.61.0. The mark rides up a
|
|
189
|
+
chain, so a spec over an unvalidated input is unvalidated too.
|
|
190
|
+
|
|
191
|
+
- `@pond-ts/process`: **`Skipped.code`** — every entry in `RunResult.skipped`
|
|
192
|
+
now carries the failure's kind (`'UnknownColumnError'`, `'ParamError'`,
|
|
193
|
+
`'UnitError'`, `'SlotError'`, …) beside its human `reason`. Under
|
|
194
|
+
`onError: 'skip' | 'collect'` nothing is thrown, so `instanceof` — the right
|
|
195
|
+
discriminator when a consumer catches — never reached a consumer reading
|
|
196
|
+
`skipped`, leaving it to match on prose whose wording is not a contract. The
|
|
197
|
+
value is `ProcessError.code`, a **literal declared per class** rather than
|
|
198
|
+
`constructor.name`, so a consumer's minifier cannot silently rename it. It is
|
|
199
|
+
absent when the throw did not come from this package, which is itself the
|
|
200
|
+
signal: op code failed, not the plan layer.
|
|
201
|
+
|
|
202
|
+
### Fixed
|
|
203
|
+
|
|
204
|
+
- `@pond-ts/process`: **`run` under `onError: 'throw'` — the default — now
|
|
205
|
+
raises the original error rather than a base `ProcessError` rebuilt from its
|
|
206
|
+
message.** A caught `UnknownColumnError`, `ParamError`, `UnitError` or
|
|
207
|
+
`SlotError` reached the caller as a bare `ProcessError`, so `instanceof`
|
|
208
|
+
could not discriminate on the throw path at all.
|
|
209
|
+
|
|
210
|
+
- `@pond-ts/process`: **a raw string input naming a column the bound series
|
|
211
|
+
does not carry is now rejected**, at `compile`, with a new
|
|
212
|
+
`UnknownColumnError`. Nothing checked it at compile or at pull: the op ran
|
|
213
|
+
against an un-widened series, and one that doesn't defend its own inputs
|
|
214
|
+
appended a plausible-looking column of garbage under the spec's id — with
|
|
215
|
+
`skipped` empty and `onError` never engaged. A persisted plan citing a column
|
|
216
|
+
the feed has since dropped now skips (or throws) instead of returning a
|
|
217
|
+
value. The check is the one `expandSlots` already made against the same
|
|
218
|
+
column list, so the two request forms no longer disagree; it runs before the
|
|
219
|
+
unit check, whose "is 'unitless'" answer for an absent column named the wrong
|
|
220
|
+
problem. The key/time column is not a value column and is rejected too.
|
|
221
|
+
|
|
222
|
+
The check covers the **whole spec closure** (a missing column under a typed
|
|
223
|
+
parent otherwise surfaced as `UnitError`) and runs on the **warm** path as
|
|
224
|
+
well as the cold one — `setSource` replaces the data under compiled nodes by
|
|
225
|
+
design, so a memoized node could outlive the column it reads and go on
|
|
226
|
+
emitting the garbage column this fix exists to prevent. A node that fails the
|
|
227
|
+
re-check is dropped from the graph.
|
|
228
|
+
(Both items reported by Tidal —
|
|
229
|
+
`docs/notes/tidal-process-adoption-friction-2026-08.md`.)
|
|
230
|
+
|
|
67
231
|
## [0.61.0] — 2026-08-16
|
|
68
232
|
|
|
69
233
|
### Added
|
package/dist/ChartContainer.d.ts
CHANGED
|
@@ -294,6 +294,48 @@ export interface ChartContainerProps {
|
|
|
294
294
|
* render.
|
|
295
295
|
*/
|
|
296
296
|
width?: number | 'auto';
|
|
297
|
+
/**
|
|
298
|
+
* Total height in CSS pixels, or `'auto'` to fill the available height —
|
|
299
|
+
* **the container-owned vertical layout** ([PND-HEIGHT]). Omitted means the
|
|
300
|
+
* classic mode: rows declare pixel heights and the container's height is
|
|
301
|
+
* their sum.
|
|
302
|
+
*
|
|
303
|
+
* With a height, the container renders as a **flex column** — the rows
|
|
304
|
+
* block flexes, the x-axis strip keeps its natural height at the bottom —
|
|
305
|
+
* and `<ChartRow flex>` rows (a bare `<ChartRow>` is `flex={1}`) divide
|
|
306
|
+
* whatever the browser says is left. That "whatever the browser says" is
|
|
307
|
+
* the point: the axis strip's height depends on its `label`, the theme's
|
|
308
|
+
* font size, whether the tick ladder is showing its calendar band row at
|
|
309
|
+
* the current grain, and how many marker pills stack — it is not a constant
|
|
310
|
+
* a caller could subtract, and every consumer who tried carried a wrong
|
|
311
|
+
* number (20, 24, and the recipe's 22 were all in the wild for one strip).
|
|
312
|
+
* CSS does the subtraction, so there is no number to know.
|
|
313
|
+
*
|
|
314
|
+
* A single full-bleed chart is therefore zero arithmetic:
|
|
315
|
+
*
|
|
316
|
+
* ```tsx
|
|
317
|
+
* <ChartContainer width="auto" height="auto">
|
|
318
|
+
* <ChartRow>
|
|
319
|
+
* <YAxis id="v" />
|
|
320
|
+
* <Layers>…</Layers>
|
|
321
|
+
* </ChartRow>
|
|
322
|
+
* </ChartContainer>
|
|
323
|
+
* ```
|
|
324
|
+
*
|
|
325
|
+
* Fixed-`height` rows keep their pixels inside a managed container, and
|
|
326
|
+
* non-row children (a draggable splitter between two rows) take their
|
|
327
|
+
* natural space — so the resizable-panels shape becomes one `flex` row
|
|
328
|
+
* absorbing slack over one fixed row the drag resizes, with no reserved
|
|
329
|
+
* strip constant and no measuring hook.
|
|
330
|
+
*
|
|
331
|
+
* `'auto'` measures with the same `ResizeObserver` as `width="auto"`, gates
|
|
332
|
+
* the first paint until both needed dimensions exist, latches the last
|
|
333
|
+
* non-zero size while hidden, and — because a flex-**column** child's
|
|
334
|
+
* height defaults to its content — **warns in dev when a measured dimension
|
|
335
|
+
* stays 0**: the parent needs a definite height, or the deadlock is the
|
|
336
|
+
* default.
|
|
337
|
+
*/
|
|
338
|
+
height?: number | 'auto';
|
|
297
339
|
/** Vertical space between rows in CSS pixels (not under the axis). Default 0. */
|
|
298
340
|
rowGap?: number;
|
|
299
341
|
/**
|
|
@@ -438,8 +480,38 @@ export interface ChartContainerProps {
|
|
|
438
480
|
* The boolean form is the back-compat shorthand (`true` ⇒ `'panZoom'`,
|
|
439
481
|
* `false` ⇒ `'none'`). Bound the reachable range with {@link bounds}
|
|
440
482
|
* (zoom-out / pan extent) and {@link minDuration} (zoom-in floor).
|
|
483
|
+
*
|
|
484
|
+
* **This prop is about the plot only.** Gestures on the axis strips are a
|
|
485
|
+
* separate opt-in — see {@link axisPanZoom} — so turning pan/zoom on here does
|
|
486
|
+
* not silently make the axes grabbable.
|
|
441
487
|
*/
|
|
442
488
|
panZoom?: boolean | 'none' | 'pan' | 'panZoom' | 'panZoomX' | 'panZoomY' | 'panZoomXY';
|
|
489
|
+
/**
|
|
490
|
+
* Which **axis strips** take gestures — the opt-in for grabbing an axis, and
|
|
491
|
+
* **`'none'` by default** so no existing chart changes behaviour:
|
|
492
|
+
*
|
|
493
|
+
* - `'none'` (or `false`, the **default**) — the strips are inert chrome.
|
|
494
|
+
* - `'x'` — the `<XAxis>` strip **pans on drag and zooms on wheel**, exactly as
|
|
495
|
+
* the plot's own gestures do (same maths, same sign, same {@link bounds} /
|
|
496
|
+
* {@link minDuration} fences). Double-click returns to the declared
|
|
497
|
+
* {@link range}. A category axis has no continuous domain and stays inert.
|
|
498
|
+
* - `'y'` — each `<YAxis>` gutter **zooms that one axis** on drag or wheel,
|
|
499
|
+
* double-click releasing it back to its fit. Report it to a scale UI with
|
|
500
|
+
* {@link YAxisProps.onBoundsChange}.
|
|
501
|
+
* - `'xy'` (or `true`) — both.
|
|
502
|
+
*
|
|
503
|
+
* **Deliberately independent of {@link panZoom}**, in both directions. A chart
|
|
504
|
+
* can scale its y axes without letting the plot capture vertical drags (which
|
|
505
|
+
* would fight a selection sweep), and an interactive plot does not hand its
|
|
506
|
+
* axes gestures nobody asked for. The one thing they share is the view itself:
|
|
507
|
+
* the x strip moves the same range the plot's pan does, and reports through
|
|
508
|
+
* {@link onTimeRangeChange} the same way.
|
|
509
|
+
*
|
|
510
|
+
* The pairing to reach for on a time-series chart is
|
|
511
|
+
* `panZoom="panZoom" axisPanZoom="xy"` — drag the plot to pan, drag the x strip
|
|
512
|
+
* to pan, wheel either to zoom, and drag a y gutter to override its fit.
|
|
513
|
+
*/
|
|
514
|
+
axisPanZoom?: boolean | 'none' | 'x' | 'y' | 'xy';
|
|
443
515
|
/**
|
|
444
516
|
* **Outer pan/zoom extent** — `[min, max]` (same units as {@link range}) the
|
|
445
517
|
* view can never move outside. Panning into an edge stops there (the window
|
package/dist/ChartContainer.js
CHANGED
|
@@ -58,17 +58,24 @@ function normalizeRange(range) {
|
|
|
58
58
|
* ChartContainerProps.width} and {@link AutoWidthContainer}.
|
|
59
59
|
*/
|
|
60
60
|
export function ChartContainer(props) {
|
|
61
|
-
const { width } = props;
|
|
61
|
+
const { width, height } = props;
|
|
62
62
|
// The measure pass is a *different component* rather than a branch inside
|
|
63
63
|
// the resolved one, because the resolved container may not render at all
|
|
64
|
-
// until
|
|
65
|
-
// component by the
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
|
|
69
|
-
|
|
64
|
+
// until its dimensions exist — and ~60 hooks cannot be conditional. Choosing
|
|
65
|
+
// the component by the props' kinds (number vs auto) means flipping between
|
|
66
|
+
// fixed and auto remounts; that is a layout change, and a remount is the
|
|
67
|
+
// honest response to one.
|
|
68
|
+
//
|
|
69
|
+
// The dimensions default differently, deliberately: an omitted `width`
|
|
70
|
+
// means `'auto'` (a chart must have a width, and filling is the sensible
|
|
71
|
+
// way to get one), while an omitted `height` means *unmanaged* — the
|
|
72
|
+
// classic mode where rows declare pixel heights and the container is their
|
|
73
|
+
// sum. `'auto'` height is opt-in because it changes who answers "how tall
|
|
74
|
+
// is a row".
|
|
75
|
+
if (typeof width === 'number' && height !== 'auto') {
|
|
76
|
+
return _jsx(ResolvedChartContainer, { ...props, width: width, height: height });
|
|
70
77
|
}
|
|
71
|
-
return _jsx(
|
|
78
|
+
return _jsx(AutoSizeContainer, { ...props });
|
|
72
79
|
}
|
|
73
80
|
/**
|
|
74
81
|
* The `width="auto"` half: render a plain full-width box, measure it, and
|
|
@@ -88,24 +95,47 @@ export function ChartContainer(props) {
|
|
|
88
95
|
* never overflow its own measurement. A caller who wants a bordered frame
|
|
89
96
|
* puts it on a wrapper *outside* the container.
|
|
90
97
|
*/
|
|
91
|
-
function
|
|
98
|
+
function AutoSizeContainer(props) {
|
|
92
99
|
const boxRef = useRef(null);
|
|
93
|
-
const [measured, setMeasured] = useState(0);
|
|
100
|
+
const [measured, setMeasured] = useState({ width: 0, height: 0 });
|
|
101
|
+
// Which dimensions this instance is responsible for. A numeric width with
|
|
102
|
+
// height="auto" measures height only, and vice versa.
|
|
103
|
+
const needWidth = typeof props.width !== 'number';
|
|
104
|
+
const needHeight = props.height === 'auto';
|
|
105
|
+
// The needs, readable from the long-lived measure closure without going
|
|
106
|
+
// stale — `props.width` can legally flip number ↔ 'auto' without leaving
|
|
107
|
+
// this component (the dispatcher only remounts on the managed/unmanaged
|
|
108
|
+
// boundary).
|
|
109
|
+
const needsRef = useRef({ needWidth, needHeight });
|
|
110
|
+
needsRef.current = { needWidth, needHeight };
|
|
94
111
|
useLayoutEffect(() => {
|
|
95
112
|
const el = boxRef.current;
|
|
96
113
|
if (el === null)
|
|
97
114
|
return;
|
|
98
115
|
const measure = () => setMeasured((prev) => {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
//
|
|
107
|
-
//
|
|
108
|
-
|
|
116
|
+
const need = needsRef.current;
|
|
117
|
+
const r = el.getBoundingClientRect();
|
|
118
|
+
// **Latch the last non-zero value, per dimension.** A box measures 0
|
|
119
|
+
// whenever it is not laid out — most often because an ancestor went
|
|
120
|
+
// `display: none` (a tab switch, a collapsed accordion), which is a
|
|
121
|
+
// *hidden* chart, not a resized one. Writing that 0 through would
|
|
122
|
+
// unmount the resolved container and discard everything it owns:
|
|
123
|
+
// pan/zoom position, selection, hover, and every layer's memoized
|
|
124
|
+
// draw state, all rebuilt on the way back. Keeping the stale value
|
|
125
|
+
// holds the chart mounted through the hide, and the next real
|
|
126
|
+
// measurement corrects it.
|
|
127
|
+
const w = Math.round(r.width);
|
|
128
|
+
const h = Math.round(r.height);
|
|
129
|
+
// Track only the dimensions this instance is responsible for
|
|
130
|
+
// (Layer-2 review find): a width-only container that also stored
|
|
131
|
+
// height would re-render its whole tree on every *content*-height
|
|
132
|
+
// change — the classic splitter drag, an axis strip growing a band
|
|
133
|
+
// row — where the pre-[PND-HEIGHT] width-only measure bailed.
|
|
134
|
+
const width = need.needWidth && w > 0 ? w : prev.width;
|
|
135
|
+
const height = need.needHeight && h > 0 ? h : prev.height;
|
|
136
|
+
return width === prev.width && height === prev.height
|
|
137
|
+
? prev
|
|
138
|
+
: { width, height };
|
|
109
139
|
});
|
|
110
140
|
measure();
|
|
111
141
|
// Guarded rather than assumed: a non-browser render target (SSR, an older
|
|
@@ -117,10 +147,58 @@ function AutoWidthContainer(props) {
|
|
|
117
147
|
ro.observe(el);
|
|
118
148
|
return () => ro.disconnect();
|
|
119
149
|
}, []);
|
|
120
|
-
|
|
150
|
+
const width = needWidth ? measured.width : props.width;
|
|
151
|
+
const height = needHeight
|
|
152
|
+
? measured.height
|
|
153
|
+
: props.height;
|
|
154
|
+
const ready = width > 0 && (!needHeight || measured.height > 0);
|
|
155
|
+
// **A measured dimension that stays 0 is a standing deadlock, not a slow
|
|
156
|
+
// start** — the parent's size is content-derived and the chart is the
|
|
157
|
+
// content that would have given it one, so nothing will ever paint and
|
|
158
|
+
// nothing errors. Worse for height than width: a flex-*column* child's
|
|
159
|
+
// height defaults to `auto`, so there the deadlock is the default, not an
|
|
160
|
+
// edge case. Say so once, in dev, after layout has had ample time.
|
|
161
|
+
const warnedZeroRef = useRef(false);
|
|
162
|
+
useEffect(() => {
|
|
163
|
+
if (!isDev || ready || warnedZeroRef.current)
|
|
164
|
+
return;
|
|
165
|
+
const t = setTimeout(() => {
|
|
166
|
+
if (ready || warnedZeroRef.current)
|
|
167
|
+
return;
|
|
168
|
+
const el = boxRef.current;
|
|
169
|
+
if (el === null)
|
|
170
|
+
return;
|
|
171
|
+
const r = el.getBoundingClientRect();
|
|
172
|
+
const stuck = [
|
|
173
|
+
...(needWidth && Math.round(r.width) === 0 ? ['width'] : []),
|
|
174
|
+
...(needHeight && Math.round(r.height) === 0 ? ['height'] : []),
|
|
175
|
+
];
|
|
176
|
+
if (stuck.length === 0)
|
|
177
|
+
return;
|
|
178
|
+
warnedZeroRef.current = true;
|
|
179
|
+
console.warn(`[pond-charts] <ChartContainer> measured ${stuck.join(' and ')} of 0 ` +
|
|
180
|
+
`and it has not changed — the chart will stay blank. The measured ` +
|
|
181
|
+
`box fills its parent, so the parent needs a definite ` +
|
|
182
|
+
`${stuck.join('/')} (a sized ancestor, a flex basis, or ` +
|
|
183
|
+
`\`min-${stuck[0]}: 0\` on a flex child); a parent sized by its ` +
|
|
184
|
+
`own content deadlocks, because the chart is that content.`);
|
|
185
|
+
}, ZERO_SIZE_WARNING_MS);
|
|
186
|
+
return () => clearTimeout(t);
|
|
187
|
+
}, [ready, needWidth, needHeight]);
|
|
188
|
+
return (_jsx("div", { ref: boxRef, style: {
|
|
189
|
+
width: '100%',
|
|
190
|
+
// Only claim the parent's height when asked to measure it: a
|
|
191
|
+
// width-only auto container must keep its intrinsic height (the rows'
|
|
192
|
+
// sum), or every pre-[PND-HEIGHT] consumer's layout changes.
|
|
193
|
+
...(needHeight ? { height: '100%', minHeight: 0 } : {}),
|
|
194
|
+
}, children: ready && (_jsx(ResolvedChartContainer, { ...props, width: width, height: height })) }));
|
|
121
195
|
}
|
|
196
|
+
/** How long a measured dimension may stay 0 before the dev warning names the
|
|
197
|
+
* deadlock (see {@link AutoSizeContainer}). Long enough for any real layout
|
|
198
|
+
* pass; a chart legitimately gated this long is not painting anyway. */
|
|
199
|
+
const ZERO_SIZE_WARNING_MS = 600;
|
|
122
200
|
/** {@link ChartContainer} with its width resolved to a concrete pixel number. */
|
|
123
|
-
function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidth, bandAlign = 'start', width, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, onDrawStats, panZoom = false, bounds, onTimeRangeChange, minDuration = 1, cursor: cursorProp, cursorSequence: cursorSequenceProp, onRegionSelect, regionSelectModifier, cursorTime: cursorTimeProp, crosshairSnap: crosshairSnapProp, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, cursorFormat: cursorFormatProp, origin, theme, discontinuities, calendar, spacing, xScale: xScaleKind = 'linear', grid = true, sessionDividers = 'none', children, }) {
|
|
201
|
+
function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidth, bandAlign = 'start', width, height, rowGap = 0, showAxis = true, trackerPosition, onTrackerChanged, onDrawStats, panZoom = false, axisPanZoom = false, bounds, onTimeRangeChange, minDuration = 1, cursor: cursorProp, cursorSequence: cursorSequenceProp, onRegionSelect, regionSelectModifier, cursorTime: cursorTimeProp, crosshairSnap: crosshairSnapProp, editAnnotations = false, creating = null, onCreate, onSelectAnnotation, onHoverAnnotation, onEditAnnotation, snap = true, timeFormat, cursorFormat: cursorFormatProp, origin, theme, discontinuities, calendar, spacing, xScale: xScaleKind = 'linear', grid = true, sessionDividers = 'none', children, }) {
|
|
124
202
|
// ── Legacy cursor props (deprecated) ───────────────────────────────────────
|
|
125
203
|
// The string surface keeps working for one minor: the resolved mode is
|
|
126
204
|
// synthesized into the equivalent mounted preset below (`<LegacyCursor>`),
|
|
@@ -128,6 +206,10 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
128
206
|
// *explicitly* set (never on the defaults). Mounted cursor components in the
|
|
129
207
|
// same scope override the shim. See docs/rfcs/interaction.md §9 / A4.4.
|
|
130
208
|
const cursor = cursorProp ?? DEFAULT_CURSOR_MODE;
|
|
209
|
+
// [PND-HEIGHT] Whether this container owns vertical layout (see the
|
|
210
|
+
// `height` prop). Carried on the frame so a `<ChartRow flex>` can tell a
|
|
211
|
+
// home that can size it from one that never will.
|
|
212
|
+
const managesHeight = height !== undefined;
|
|
131
213
|
// [PND-IGNITECAT] The declared slot list, normalized to `null` when absent
|
|
132
214
|
// and held by **content** identity. An inline `categories={['a', 'b']}` is a
|
|
133
215
|
// fresh array every render; keying the kind/scale memos off the raw prop
|
|
@@ -221,6 +303,11 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
221
303
|
panZoom === 'panZoomX' ||
|
|
222
304
|
panZoom === 'panZoomXY';
|
|
223
305
|
const zoomY = panZoom === 'panZoomY' || panZoom === 'panZoomXY';
|
|
306
|
+
// Axis-strip gestures are their own opt-in (see `axisPanZoom`), so they are
|
|
307
|
+
// resolved from that prop alone — never from `panZoom`, which would make every
|
|
308
|
+
// already-interactive chart grow axis gestures on upgrade.
|
|
309
|
+
const axisPanZoomX = axisPanZoom === true || axisPanZoom === 'x' || axisPanZoom === 'xy';
|
|
310
|
+
const axisPanZoomY = axisPanZoom === true || axisPanZoom === 'y' || axisPanZoom === 'xy';
|
|
224
311
|
const panX = zoomX || panZoom === 'pan';
|
|
225
312
|
const panY = zoomY;
|
|
226
313
|
const panEnabled = panX || panY;
|
|
@@ -244,7 +331,14 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
244
331
|
const k = Math.max(1, next.k);
|
|
245
332
|
setYTransform((prev) => prev.k === k && prev.ty === next.ty ? prev : { k, ty: next.ty });
|
|
246
333
|
}, []);
|
|
247
|
-
|
|
334
|
+
// The x **strip**'s gestures move the same view the plot's do, so they must
|
|
335
|
+
// make the container own a view as well. Leaving `axisPanZoomX` out of this
|
|
336
|
+
// silently broke the headline combination — `axisPanZoom="x"` with the default
|
|
337
|
+
// `panZoom="none"`: `applyRange` wrote `internalRange` while `view` kept
|
|
338
|
+
// reading `seed`, so an uncontrolled strip captured the drag and drew nothing.
|
|
339
|
+
// (`axisPanZoomY` is absent on purpose: a gutter zoom is per-axis row state,
|
|
340
|
+
// not the shared x view.)
|
|
341
|
+
const interactive = panEnabled || zoomEnabled || axisPanZoomX;
|
|
248
342
|
// The explicit base domain from `range` (a tuple or a TimeRange). `undefined`
|
|
249
343
|
// ⇒ auto-fit (resolved from the layers below). Pan/zoom seeds from it; `seed`
|
|
250
344
|
// is the placeholder while auto-fitting.
|
|
@@ -1210,6 +1304,11 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1210
1304
|
// it lives in `cursorFrame` below, [PND-HOVCTX] — but the tuple stays a memo
|
|
1211
1305
|
// to hold the line for every other rebuild path.)
|
|
1212
1306
|
const timeRangeTuple = useMemo(() => [d0, d1], [d0, d1]);
|
|
1307
|
+
// The declared view (`range`), as against the gestured one above — the x
|
|
1308
|
+
// strip's double-click reset target. Memoized on its endpoints for the same
|
|
1309
|
+
// reason `timeRangeTuple` is: it sits on the frame, and a fresh tuple each
|
|
1310
|
+
// render would re-identify it for every draw callback that reads the frame.
|
|
1311
|
+
const seedRangeTuple = useMemo(() => [seed[0], seed[1]], [seed[0], seed[1]]);
|
|
1213
1312
|
// The per-move cursor state, split into its own context so a mousemove
|
|
1214
1313
|
// re-identifies only this small object — not the ~50-field frame below, which
|
|
1215
1314
|
// stays stable across hovers so `YAxis` / `Bar` / `Box` don't re-render. See
|
|
@@ -1221,6 +1320,10 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1221
1320
|
}), [cursorX, hoverPoint]);
|
|
1222
1321
|
const frame = useMemo(() => ({
|
|
1223
1322
|
timeRange: timeRangeTuple,
|
|
1323
|
+
seedRange: seedRangeTuple,
|
|
1324
|
+
axisPanZoomX,
|
|
1325
|
+
axisPanZoomY,
|
|
1326
|
+
managesHeight,
|
|
1224
1327
|
width,
|
|
1225
1328
|
theme: theme ?? defaultTheme,
|
|
1226
1329
|
plotWidth,
|
|
@@ -1302,6 +1405,10 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1302
1405
|
firstRowKey,
|
|
1303
1406
|
}), [
|
|
1304
1407
|
timeRangeTuple,
|
|
1408
|
+
seedRangeTuple,
|
|
1409
|
+
axisPanZoomX,
|
|
1410
|
+
axisPanZoomY,
|
|
1411
|
+
managesHeight,
|
|
1305
1412
|
width,
|
|
1306
1413
|
theme,
|
|
1307
1414
|
plotWidth,
|
|
@@ -1385,13 +1492,33 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1385
1492
|
// The 'line' default nobody asked for is IMPLICIT — the one cursor a
|
|
1386
1493
|
// <MultiSelector>'s resting block preview may replace with the
|
|
1387
1494
|
// brush band. An explicit `cursor` prop (any mode) still wins.
|
|
1388
|
-
implicit: cursorProp === undefined }), _jsxs("div", { style: {
|
|
1495
|
+
implicit: cursorProp === undefined }), _jsxs("div", { style: {
|
|
1496
|
+
width: `${width}px`,
|
|
1497
|
+
// [PND-HEIGHT] A managed height makes the outer box a flex
|
|
1498
|
+
// column: the rows block below flexes, the axis strip keeps its
|
|
1499
|
+
// natural height at the bottom, and CSS subtracts one from the
|
|
1500
|
+
// other. That subtraction being layout rather than arithmetic is
|
|
1501
|
+
// the feature — the strip's height varies with label, font size,
|
|
1502
|
+
// calendar bands and pill lanes, so no constant is correct.
|
|
1503
|
+
...(height !== undefined
|
|
1504
|
+
? {
|
|
1505
|
+
height: `${height}px`,
|
|
1506
|
+
display: 'flex',
|
|
1507
|
+
flexDirection: 'column',
|
|
1508
|
+
}
|
|
1509
|
+
: {}),
|
|
1510
|
+
}, children: [_jsx("div", { style: {
|
|
1389
1511
|
display: 'flex',
|
|
1390
1512
|
flexDirection: 'column',
|
|
1391
1513
|
gap: `${rowGap}px`,
|
|
1392
1514
|
// The positioned ancestor for overlay chrome (`<Legend>`): the
|
|
1393
1515
|
// card anchors to the rows block, never the axis strip below.
|
|
1394
1516
|
position: 'relative',
|
|
1517
|
+
// The rows block takes what the axis strip leaves. `minHeight:
|
|
1518
|
+
// 0` lets it shrink below its content — without it a flex
|
|
1519
|
+
// child's floor is its content and nothing can ever get
|
|
1520
|
+
// smaller.
|
|
1521
|
+
...(height !== undefined ? { flex: '1 1 0%', minHeight: 0 } : {}),
|
|
1395
1522
|
}, children: children }), showAxis && _jsx(TimeAxis, {})] })] }) }));
|
|
1396
1523
|
}
|
|
1397
1524
|
//# sourceMappingURL=ChartContainer.js.map
|