@pond-ts/charts 0.69.0 → 0.70.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 +67 -1
- package/dist/BandChart.d.ts +20 -1
- package/dist/BandChart.js +18 -2
- package/dist/band.d.ts +10 -2
- package/dist/band.js +38 -10
- package/dist/decimate.d.ts +10 -1
- package/dist/decimate.js +51 -10
- package/dist/tradingAxis.fixture.d.ts +19 -0
- package/dist/tradingAxis.fixture.js +24 -0
- package/package.json +3 -3
package/API.md
CHANGED
|
@@ -716,31 +716,31 @@ Typed dataflow graphs for pipelines whose **shape is data** (runtime-assembled,
|
|
|
716
716
|
user-edited, one computation fanned out to several consumers). Chaining stays
|
|
717
717
|
the default for pipelines known at authoring time — see the package README.
|
|
718
718
|
|
|
719
|
-
| Group | Exports
|
|
720
|
-
| ----------------------- |
|
|
721
|
-
| Worker pool (Node) | `HostPool` (`start`, `run`, `close`, `size`, `inFlight`); types `HostPoolOptions`, `PoolSetup`, `PoolSetupConfig`; `toWire` / `fromWire`, types `WireResult`, `WireColumn` — subpath `@pond-ts/process/pool`
|
|
722
|
-
| Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`)
|
|
723
|
-
| Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline)
|
|
724
|
-
| Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues`
|
|
725
|
-
| Sources | `source<T>()` → `SourceNode` (`set()`), `fromLive(liveSource)` → `LiveSourceNode` (`dispose()`); `GraphSource` (bind contract — looser than core's `LiveSource`, accepts `LiveAggregation`), `SnapshotSource`, `NoInputs`
|
|
726
|
-
| Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson`
|
|
727
|
-
| 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]`
|
|
728
|
-
| 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
|
|
729
|
-
| 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
|
|
730
|
-
| 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`)
|
|
731
|
-
| 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`)
|
|
732
|
-
| 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`
|
|
733
|
-
| 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`
|
|
734
|
-
| Plan — types | `Spec`, `Plan`, `Input` (column name \| `Spec` \| `PickedOutput`), `SpecRef`, `Def` (`OpDef` \| `FoldDef`), `OpContext`, `OpResult`, `FoldContext`, `FactBody`, `isFold`, `ParamDef`, `Params`, `Units`, `InputDef`, `OutputDef`
|
|
735
|
-
| 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)
|
|
736
|
-
| 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
|
|
737
|
-
| 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`
|
|
738
|
-
| 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`
|
|
739
|
-
| 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`
|
|
740
|
-
| Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef`
|
|
741
|
-
| 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
|
|
742
|
-
| Errors | `ProcessError` (base; `code` — a stable per-class literal, minification-proof, also surfaced on `Skipped`), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError`
|
|
743
|
-
| Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput`
|
|
719
|
+
| Group | Exports | Source |
|
|
720
|
+
| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- |
|
|
721
|
+
| 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` |
|
|
722
|
+
| Ports | `Inlet`, `Outlet` (typed fields on `node.in` / `node.out`; `get()`, `peek()`, `version`, `connect`, `disconnect`) | `packages/process/src/port.ts` |
|
|
723
|
+
| Nodes | `Node` (`in`, `out`, `dirty`, `error`, `invalidate()`), `defineNode` (reusable multi-output node type), `derive` (single-output, wired inline) | `packages/process/src/node.ts` |
|
|
724
|
+
| Port declaration | `port<T>({ equals, defaultValue })`; types `PortSpec`, `PortSpecMap`, `PortValue`, `PortValues` | `packages/process/src/types.ts` |
|
|
725
|
+
| 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` |
|
|
726
|
+
| Graph view | `Graph` (`Graph.from(...roots)`, `nodes`, `order()`, `edges()`, `toJSON()`); types `GraphEdge`, `GraphJson`, `GraphNodeJson`, `GraphEdgeJson` | `packages/process/src/graph.ts` |
|
|
727
|
+
| 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` |
|
|
728
|
+
| 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` |
|
|
729
|
+
| 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` |
|
|
730
|
+
| 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` |
|
|
731
|
+
| 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` |
|
|
732
|
+
| Plan — registry | `createRegistry({ folds })` / `Registry` (`define`, `get`, `foldFor`, `outputsOf`, `resolveParams` (`{ validate: false }` applies defaults and skips every check), `byFamily`, `describe`, `toJsonSchema`, `checkArity`), param builders `int` / `num` / `choice` / `flag`, `UnknownOpError`, `ParamError`, `ArityError` | `packages/process/src/plan/registry.ts`, `params.ts` |
|
|
733
|
+
| Plan — identity | `specId(registry, spec, { validate })` (content-addressed, param-order invariant, defaults materialized; `validate: false` is **total over arbitrary JSON** — names a spec that would not compile, including malformed shapes, in a separate `p1?:` namespace that cannot collide with a valid id; a valid spec's id is identical either way. Judges op existence, params **and arity**), `SpecIdOptions`, `refToId`, `explain`, `unitOf`, `columnsOf`, `dependsOn`, `outputKey` | `packages/process/src/plan/identity.ts` |
|
|
734
|
+
| 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` |
|
|
735
|
+
| 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` |
|
|
736
|
+
| 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` — echoed verbatim, `params`/`inputs` typed `unknown`; `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` |
|
|
737
|
+
| 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` |
|
|
738
|
+
| 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` |
|
|
739
|
+
| 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` |
|
|
740
|
+
| Plan — async sources | `defineSource({ name, load })`, `createSourceRegistry()` / `SourceRegistry`, `sourceId`, `UnknownSourceError`; types `SourceRef`, `SourceParams`, `LoadedSource` (value + revision), `SourceLoadContext`, `SourceDef` | `packages/process/src/plan/source.ts` |
|
|
741
|
+
| 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` |
|
|
742
|
+
| Errors | `ProcessError` (base; `code` — a stable per-class literal, minification-proof, also surfaced on `Skipped`), `CycleError`, `UnconnectedInputError`, `MissingOutputError`, `UnsetSourceError` | `packages/process/src/errors.ts` |
|
|
743
|
+
| Node type helpers | `NodeSpec`, `NodeFactory`, `InletsFor`, `OutletsFor`, `OutletValue`, `SpecsForOutlets`, `DerivedOutput` | `packages/process/src/node.ts` |
|
|
744
744
|
|
|
745
745
|
Note: this package's `npm test` includes a `test:dts` step that typechecks the
|
|
746
746
|
**emitted** `dist/*.d.ts` from a consumer's perspective (`test-dts/`,
|
package/CHANGELOG.md
CHANGED
|
@@ -8,7 +8,8 @@ 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.70.0...HEAD
|
|
12
|
+
[0.70.0]: https://github.com/pond-ts/pond/compare/v0.69.0...v0.70.0
|
|
12
13
|
[0.69.0]: https://github.com/pond-ts/pond/compare/v0.68.0...v0.69.0
|
|
13
14
|
[0.68.0]: https://github.com/pond-ts/pond/compare/v0.67.0...v0.68.0
|
|
14
15
|
[0.67.0]: https://github.com/pond-ts/pond/compare/v0.66.0...v0.67.0
|
|
@@ -72,6 +73,71 @@ include new features and type-level changes; patch bumps are strictly additive.
|
|
|
72
73
|
|
|
73
74
|
## [Unreleased]
|
|
74
75
|
|
|
76
|
+
## [0.70.0] — 2026-09-18
|
|
77
|
+
|
|
78
|
+
### Added
|
|
79
|
+
|
|
80
|
+
- `@pond-ts/charts`: **`<BandChart sessionBreaks>`** — the same trading-axis
|
|
81
|
+
session break `<LineChart>` has had since v0.45.0. On a discontinuous
|
|
82
|
+
(`discontinuities` / `calendar`) axis the fill previously ran a near-vertical
|
|
83
|
+
sliver from one session's last sample to the next session's first, because
|
|
84
|
+
the collapsed overnight gap put them a pixel apart; `sessionBreaks` ends the
|
|
85
|
+
envelope at the close and re-starts it at the open, so a band and its centre
|
|
86
|
+
line break in step. A **scale** break, orthogonal to the NaN **data** gaps a
|
|
87
|
+
band always breaks at; default `false` (unchanged output). Decimation
|
|
88
|
+
composes: `decimateBand` folds each break instant into the pixel-column
|
|
89
|
+
edges and bakes a `NaN` sample at it, so no column merges two sessions'
|
|
90
|
+
envelopes. Stories `Axes/TradingTimeAxis / SessionBreaksBand` and
|
|
91
|
+
`Performance/Decimation / TradingSessionBreaksBand`.
|
|
92
|
+
- `@pond-ts/process`: **`ArityError`**, and **arity is now part of what `specId`
|
|
93
|
+
can judge**. A spec whose `inputs` count does not match the op's — including
|
|
94
|
+
one carrying no `inputs` at all — was named `p1:sma(;period=20)`, a _valid_
|
|
95
|
+
id for something that cannot compile, and then died at `compile` as a bare
|
|
96
|
+
`TypeError` reading `.length` of `undefined`, reaching the consumer as a
|
|
97
|
+
`Skipped` with **no `code`** (which under that contract means "op code
|
|
98
|
+
threw"). Arity is decidable from the registry alone, so strict `specId` now
|
|
99
|
+
raises `ArityError` and lenient mode marks the id `p1?:`.
|
|
100
|
+
`Registry.checkArity(op, inputs)` is the shared check `compile` uses too.
|
|
101
|
+
|
|
102
|
+
### Changed
|
|
103
|
+
|
|
104
|
+
- `@pond-ts/financial`: **`StudyOutput.id` documents the `@pond-ts/process`
|
|
105
|
+
bridge**, and a new `test/catalog-process.test.ts` pins it. The field is a
|
|
106
|
+
*financial column suffix*; process's `OutputDef.id` is a *process outlet
|
|
107
|
+
id*; they share a name and are different namespaces. Process names its own
|
|
108
|
+
columns (`specId + OutputDef.id`) and matches an op's return to its outputs
|
|
109
|
+
positionally, so a study's own column names never reach it — which means a
|
|
110
|
+
registry bridging the two chooses its own suffixes, and for the **twelve**
|
|
111
|
+
multi-output studies that claim the bare prefix (`trix`, `superTrend`,
|
|
112
|
+
`klinger`, …) it must: process rejects `''` on a multi-output op, because
|
|
113
|
+
there the column would be named exactly the spec id, itself a legal column
|
|
114
|
+
reference. The map is `outputs.length > 1 && id === ''` → `'value'` — not
|
|
115
|
+
`id === ''`, which would rename all seventy-odd single-output columns for
|
|
116
|
+
nothing. Reported by a consumer that hit the throw at module load and
|
|
117
|
+
worked around it by hand. No API change: the descriptors, the studies and
|
|
118
|
+
the guard are all unchanged, and the round-trip test is what stops the two
|
|
119
|
+
packages drifting — `catalog.test.ts` validates a descriptor against its
|
|
120
|
+
*study*, so it is structurally blind to a cross-package disagreement.
|
|
121
|
+
- `@pond-ts/process`: **`Skipped.spec` echoes the request verbatim**, and its
|
|
122
|
+
`params` / `inputs` are typed `unknown` accordingly. The plan pass normalized
|
|
123
|
+
`params: null` to `{}`, so recomputing an id from the echo produced the
|
|
124
|
+
_defaulted spec's valid id_ — keying a broken persisted entry's report onto a
|
|
125
|
+
legitimate node. The selector pass echoed the original all along, so the two
|
|
126
|
+
passes disagreed. **Migration:** a consumer reading `entry.spec.params` now
|
|
127
|
+
narrows it (`entry.spec.params as Record<string, unknown>`, or a guard) —
|
|
128
|
+
which is the point, since the value may be exactly the malformed thing that
|
|
129
|
+
failed.
|
|
130
|
+
- `@pond-ts/process`: `specId(…, { validate: false })` is **total over
|
|
131
|
+
arbitrary JSON**, not just over well-typed specs. `params: null`, a
|
|
132
|
+
non-array `inputs`, and an input entry that is neither a column name nor a
|
|
133
|
+
spec each used to raise a `TypeError`; they are now named in the `p1?:`
|
|
134
|
+
namespace, distinctly enough that two differently-broken specs stay two ids.
|
|
135
|
+
Strict mode reports them as `ParamError` / `ArityError` / `ProcessError`
|
|
136
|
+
rather than crashing.
|
|
137
|
+
|
|
138
|
+
(All three reported by Tidal against 0.62.0, after adopting it.)
|
|
139
|
+
|
|
140
|
+
|
|
75
141
|
## [0.69.0] — 2026-09-13
|
|
76
142
|
|
|
77
143
|
### Added
|
package/dist/BandChart.d.ts
CHANGED
|
@@ -27,6 +27,23 @@ export interface BandChartCommon<S extends SeriesSchema = SeriesSchema, VS exten
|
|
|
27
27
|
* Denoise the underlying values with `smooth()`, not this.
|
|
28
28
|
*/
|
|
29
29
|
curve?: Curve;
|
|
30
|
+
/**
|
|
31
|
+
* Break the envelope at each **trading-axis discontinuity** (a session / day /
|
|
32
|
+
* lunch close→open) when the container renders on a trading-time axis (a
|
|
33
|
+
* `discontinuities` / `calendar` provider). **Omitted ⇒ `false`**: the fill
|
|
34
|
+
* connects the last pre-close sample straight to the next open across the
|
|
35
|
+
* collapsed gap (a near-vertical sliver). `true` ends the fill at the close
|
|
36
|
+
* and re-starts it at the open — the intraday look, where one session's
|
|
37
|
+
* envelope shouldn't visually flow into the next. Same semantics as
|
|
38
|
+
* {@link LineChart}'s `sessionBreaks`, so a band and its centre line break in
|
|
39
|
+
* step.
|
|
40
|
+
*
|
|
41
|
+
* This is a **scale** break (driven by the axis's collapsed gaps), orthogonal
|
|
42
|
+
* to a **data** break (a NaN run on either edge, which a band always breaks
|
|
43
|
+
* at). A no-op on a continuous axis (no provider) or a provider without
|
|
44
|
+
* `boundaries`.
|
|
45
|
+
*/
|
|
46
|
+
sessionBreaks?: boolean;
|
|
30
47
|
/**
|
|
31
48
|
* **M4 viewport decimation** (charts decimator wave). **Omitted ⇒ `true`**:
|
|
32
49
|
* once the visible envelope is denser than ~2 samples per device pixel, it is
|
|
@@ -34,6 +51,8 @@ export interface BandChartCommon<S extends SeriesSchema = SeriesSchema, VS exten
|
|
|
34
51
|
* the samples span, so it covers the same pixels from O(plot width) points.
|
|
35
52
|
* Applies with a linear `curve`; pass `false` to always fill every sample, or
|
|
36
53
|
* `{ threshold }` to tune. Shares {@link LineChart}'s `DecimateOption`.
|
|
54
|
+
* Composes with {@link sessionBreaks}: the break instants are folded into the
|
|
55
|
+
* pixel-column edges so no column merges two sessions' envelopes.
|
|
37
56
|
*/
|
|
38
57
|
decimate?: DecimateOption;
|
|
39
58
|
/**
|
|
@@ -92,6 +111,6 @@ export type BandChartProps<S extends SeriesSchema = SeriesSchema, VS extends Val
|
|
|
92
111
|
* </Layers>
|
|
93
112
|
* ```
|
|
94
113
|
*/
|
|
95
|
-
export declare function BandChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, lower, upper, as: semantic, axis, curve, decimate, legend, index, }: BandChartProps<S, VS>): null;
|
|
114
|
+
export declare function BandChart<S extends SeriesSchema = SeriesSchema, VS extends ValueSeriesSchema = ValueSeriesSchema>({ series, lower, upper, as: semantic, axis, curve, sessionBreaks, decimate, legend, index, }: BandChartProps<S, VS>): null;
|
|
96
115
|
export {};
|
|
97
116
|
//# sourceMappingURL=BandChart.d.ts.map
|
package/dist/BandChart.js
CHANGED
|
@@ -6,6 +6,9 @@ import { resolveCurve } from './curve.js';
|
|
|
6
6
|
import { ContainerContext, LayersContext } from './context.js';
|
|
7
7
|
import { legendLabelFor, useLegendItems, } from './swatch.js';
|
|
8
8
|
import { useSlotKey } from './use-slot-key.js';
|
|
9
|
+
/** Stable empty boundary list — so `sessionBreaks={false}` keeps a referentially
|
|
10
|
+
* constant array and the layer entry isn't rebuilt every render. */
|
|
11
|
+
const NO_BREAKS = [];
|
|
9
12
|
/**
|
|
10
13
|
* A variance-band draw layer: fills the envelope between the `lower` and `upper`
|
|
11
14
|
* columns of `series` (typically `rollingByColumn` percentiles), gap-aware, and
|
|
@@ -23,7 +26,7 @@ import { useSlotKey } from './use-slot-key.js';
|
|
|
23
26
|
* </Layers>
|
|
24
27
|
* ```
|
|
25
28
|
*/
|
|
26
|
-
export function BandChart({ series, lower, upper, as: semantic, axis, curve, decimate = true, legend, index = 0, }) {
|
|
29
|
+
export function BandChart({ series, lower, upper, as: semantic, axis, curve, sessionBreaks = false, decimate = true, legend, index = 0, }) {
|
|
27
30
|
const container = useContext(ContainerContext);
|
|
28
31
|
if (container === null) {
|
|
29
32
|
throw new Error('<BandChart> must be rendered inside a <ChartContainer>');
|
|
@@ -35,6 +38,18 @@ export function BandChart({ series, lower, upper, as: semantic, axis, curve, dec
|
|
|
35
38
|
const bs = useMemo(() => series instanceof ValueSeries
|
|
36
39
|
? bandFromValueSeries(series, lower, upper)
|
|
37
40
|
: bandFromTimeSeries(series, lower, upper), [series, lower, upper]);
|
|
41
|
+
// Trading-axis session breaks: the collapse instants inside this band's span
|
|
42
|
+
// (session/day/lunch opens the axis skips). Data instants, not pixels — so the
|
|
43
|
+
// set is view-independent (pan/zoom reuse it). Only computed when opted in and
|
|
44
|
+
// the container carries a boundary-reporting discontinuity provider. The same
|
|
45
|
+
// lookup `<LineChart>` does, so a band and its centre line break identically.
|
|
46
|
+
const sessionBreakInstants = useMemo(() => {
|
|
47
|
+
const provider = container.discontinuities;
|
|
48
|
+
if (!sessionBreaks || provider?.boundaries === undefined || bs.length < 2) {
|
|
49
|
+
return NO_BREAKS;
|
|
50
|
+
}
|
|
51
|
+
return provider.boundaries(bs.x[0], bs.x[bs.length - 1]);
|
|
52
|
+
}, [sessionBreaks, container.discontinuities, bs]);
|
|
38
53
|
// Styling: semantic identifier → theme band style. The single styling channel.
|
|
39
54
|
const { band } = container.theme;
|
|
40
55
|
const style = (semantic !== undefined ? band[semantic] : undefined) ?? band.default;
|
|
@@ -118,7 +133,7 @@ export function BandChart({ series, lower, upper, as: semantic, axis, curve, dec
|
|
|
118
133
|
},
|
|
119
134
|
];
|
|
120
135
|
},
|
|
121
|
-
draw: (ctx, xScale, yScale) => drawBand(ctx, bs, xScale, yScale, style, curveFactory, decimate),
|
|
136
|
+
draw: (ctx, xScale, yScale) => drawBand(ctx, bs, xScale, yScale, style, curveFactory, sessionBreakInstants, decimate),
|
|
122
137
|
},
|
|
123
138
|
axisId: axis,
|
|
124
139
|
index,
|
|
@@ -129,6 +144,7 @@ export function BandChart({ series, lower, upper, as: semantic, axis, curve, dec
|
|
|
129
144
|
upper,
|
|
130
145
|
style,
|
|
131
146
|
curveFactory,
|
|
147
|
+
sessionBreakInstants,
|
|
132
148
|
decimate,
|
|
133
149
|
axis,
|
|
134
150
|
index,
|
package/dist/band.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { type CurveFactory } from 'd3-shape';
|
|
2
2
|
import type { BandSeries } from './data.js';
|
|
3
|
-
import type
|
|
3
|
+
import { type Scale } from './line.js';
|
|
4
4
|
import type { BandStyle } from './theme.js';
|
|
5
5
|
import type { LayerDrawStats } from './context.js';
|
|
6
6
|
import { type DecimateOption } from './decimate.js';
|
|
@@ -25,9 +25,17 @@ export declare function bandExtent(band: BandSeries): [number, number] | null;
|
|
|
25
25
|
* filled envelope's break wants its own treatment (sharp edge vs. blurred),
|
|
26
26
|
* still to be designed; for now a band always breaks honestly at a gap.
|
|
27
27
|
*
|
|
28
|
+
* `boundaries` are trading-axis **session-break** instants (`<BandChart
|
|
29
|
+
* sessionBreaks>`): the envelope is split into per-session runs wherever one
|
|
30
|
+
* falls between two consecutive samples (see {@link sessionRuns}), each run its
|
|
31
|
+
* own closed subpath, so the fill ends at the last pre-close sample and re-starts
|
|
32
|
+
* at the first post-open one — a **scale** break, orthogonal to the NaN **data**
|
|
33
|
+
* gaps handled within each run. With no boundaries the output is identical to a
|
|
34
|
+
* single-pass draw. Mirrors `drawLine`'s treatment exactly.
|
|
35
|
+
*
|
|
28
36
|
* `band.lower` (a `Float64Array`) is the datum iterable; every accessor reads by
|
|
29
37
|
* index, so there's no per-point object allocation. `globalAlpha` carries the
|
|
30
38
|
* opacity and is restored so it doesn't leak into later layers.
|
|
31
39
|
*/
|
|
32
|
-
export declare function drawBand(ctx: CanvasRenderingContext2D, band: BandSeries, xScale: Scale, yScale: Scale, style: BandStyle, curve?: CurveFactory, decimate?: DecimateOption): LayerDrawStats;
|
|
40
|
+
export declare function drawBand(ctx: CanvasRenderingContext2D, band: BandSeries, xScale: Scale, yScale: Scale, style: BandStyle, curve?: CurveFactory, boundaries?: readonly number[], decimate?: DecimateOption): LayerDrawStats;
|
|
33
41
|
//# sourceMappingURL=band.d.ts.map
|
package/dist/band.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import { area as d3area, curveLinear } from 'd3-shape';
|
|
2
|
+
import { sessionRuns } from './line.js';
|
|
2
3
|
import { cullBandSeries } from './culling.js';
|
|
3
4
|
import { decimateBand } from './decimate.js';
|
|
4
5
|
import { gapUnscalable } from './gaps.js';
|
|
6
|
+
/** Shared empty boundary list — passed to `sessionRuns` when a decimated band
|
|
7
|
+
* already carries its session breaks as baked-in `NaN` samples. */
|
|
8
|
+
const EMPTY_BOUNDARIES = [];
|
|
5
9
|
/**
|
|
6
10
|
* The `[min, max]` vertical extent of the **drawn** band — the lowest `lower`
|
|
7
11
|
* and highest `upper` over samples where both edges are finite — or `null` if
|
|
@@ -37,11 +41,19 @@ export function bandExtent(band) {
|
|
|
37
41
|
* filled envelope's break wants its own treatment (sharp edge vs. blurred),
|
|
38
42
|
* still to be designed; for now a band always breaks honestly at a gap.
|
|
39
43
|
*
|
|
44
|
+
* `boundaries` are trading-axis **session-break** instants (`<BandChart
|
|
45
|
+
* sessionBreaks>`): the envelope is split into per-session runs wherever one
|
|
46
|
+
* falls between two consecutive samples (see {@link sessionRuns}), each run its
|
|
47
|
+
* own closed subpath, so the fill ends at the last pre-close sample and re-starts
|
|
48
|
+
* at the first post-open one — a **scale** break, orthogonal to the NaN **data**
|
|
49
|
+
* gaps handled within each run. With no boundaries the output is identical to a
|
|
50
|
+
* single-pass draw. Mirrors `drawLine`'s treatment exactly.
|
|
51
|
+
*
|
|
40
52
|
* `band.lower` (a `Float64Array`) is the datum iterable; every accessor reads by
|
|
41
53
|
* index, so there's no per-point object allocation. `globalAlpha` carries the
|
|
42
54
|
* opacity and is restored so it doesn't leak into later layers.
|
|
43
55
|
*/
|
|
44
|
-
export function drawBand(ctx, band, xScale, yScale, style, curve = curveLinear, decimate = true) {
|
|
56
|
+
export function drawBand(ctx, band, xScale, yScale, style, curve = curveLinear, boundaries = [], decimate = true) {
|
|
45
57
|
const sourceCount = band.length; // pre-cull, pre-decimation (for draw stats)
|
|
46
58
|
// Viewport culling (Phase 2): clip the envelope to the visible slice (+1 each
|
|
47
59
|
// side) before filling, so a pan strokes O(visible). The solid fill has no
|
|
@@ -54,11 +66,13 @@ export function drawBand(ctx, band, xScale, yScale, style, curve = curveLinear,
|
|
|
54
66
|
// pixels. Gated off a smoothing `curve` (which would distort the per-column
|
|
55
67
|
// envelope) and `decimate === false`; `decimateBand` itself no-ops on a sparse
|
|
56
68
|
// envelope or a domainless test scale, so this stays byte-identical there.
|
|
69
|
+
// The session-break instants ride along so a column never straddles a break
|
|
70
|
+
// and the decimated envelope carries the breaks as baked-in NaN samples.
|
|
57
71
|
let decimated = false;
|
|
58
72
|
if (decimate !== false && curve === curveLinear) {
|
|
59
73
|
const k = typeof decimate === 'object' ? decimate.threshold : undefined;
|
|
60
74
|
const before = band;
|
|
61
|
-
band = decimateBand(band, xScale, ctx, k);
|
|
75
|
+
band = decimateBand(band, xScale, ctx, k, boundaries);
|
|
62
76
|
decimated = band !== before;
|
|
63
77
|
}
|
|
64
78
|
// An edge with no position on the y scale becomes an ordinary NaN gap, so the
|
|
@@ -73,18 +87,32 @@ export function drawBand(ctx, band, xScale, yScale, style, curve = curveLinear,
|
|
|
73
87
|
if (gapLower !== band.lower || gapUpper !== band.upper) {
|
|
74
88
|
band = { ...band, lower: gapLower, upper: gapUpper };
|
|
75
89
|
}
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
90
|
+
// Split into independent index runs at each session break; no boundary inside
|
|
91
|
+
// the data ⇒ one run over the whole envelope (the hot path — no slicing, so the
|
|
92
|
+
// draw is byte-identical to the pre-boundary single pass). When the band was
|
|
93
|
+
// decimated, `decimateBand` already baked the breaks in as NaN samples aligned
|
|
94
|
+
// to the break instants, so re-cutting here would mis-attribute the boundary
|
|
95
|
+
// samples — pass `[]` and let the baked-in breaks split the sessions.
|
|
96
|
+
const runs = sessionRuns(band.x, band.length, decimated ? EMPTY_BOUNDARIES : boundaries);
|
|
97
|
+
const singleRun = runs.length === 1;
|
|
83
98
|
ctx.save();
|
|
84
99
|
ctx.fillStyle = style.fill;
|
|
85
100
|
ctx.globalAlpha = style.opacity;
|
|
101
|
+
// One path across every run. Each run's generator opens with its own moveTo
|
|
102
|
+
// (and closes its own polygon), so a run boundary is a clean pen-up — the
|
|
103
|
+
// session break — and a single fill covers them all.
|
|
86
104
|
ctx.beginPath();
|
|
87
|
-
|
|
105
|
+
for (const [s, e] of runs) {
|
|
106
|
+
const gen = d3area()
|
|
107
|
+
.defined((_, j) => Number.isFinite(band.lower[s + j]) &&
|
|
108
|
+
Number.isFinite(band.upper[s + j]))
|
|
109
|
+
.x((_, j) => xScale(band.x[s + j]))
|
|
110
|
+
.y0((_, j) => yScale(band.lower[s + j]))
|
|
111
|
+
.y1((_, j) => yScale(band.upper[s + j]))
|
|
112
|
+
.curve(curve)
|
|
113
|
+
.context(ctx);
|
|
114
|
+
gen(singleRun ? band.lower : band.lower.subarray(s, e));
|
|
115
|
+
}
|
|
88
116
|
ctx.fill();
|
|
89
117
|
ctx.restore();
|
|
90
118
|
return { sourceCount, drawnCount: band.length, decimated };
|
package/dist/decimate.d.ts
CHANGED
|
@@ -224,8 +224,17 @@ export declare function m4Polyline(edges: Float64Array, mn: Float64Array, mx: Fl
|
|
|
224
224
|
* `upper` are finite **together** per sample (the paired-percentile shape bands
|
|
225
225
|
* are built from); a column where only one edge has finite samples would bin a
|
|
226
226
|
* band segment that no single sample carried.
|
|
227
|
+
*
|
|
228
|
+
* `boundaries` are trading-axis session-break instants (`<BandChart
|
|
229
|
+
* sessionBreaks>`), handled exactly as {@link decimateM4} does for a line: each
|
|
230
|
+
* in-domain instant is unioned into the bucket edges so no column merges two
|
|
231
|
+
* sessions' envelopes across the discontinuity, **and** a `NaN` sample is
|
|
232
|
+
* emitted at the instant so the fill ends at the close and re-starts at the
|
|
233
|
+
* open — otherwise the closing and opening columns would sit as adjacent finite
|
|
234
|
+
* samples and the envelope would flow straight across the collapsed gap. The
|
|
235
|
+
* caller's `sessionRuns` then sees the break baked in and passes no boundaries.
|
|
227
236
|
*/
|
|
228
|
-
export declare function decimateBand(band: BandSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number): BandSeries;
|
|
237
|
+
export declare function decimateBand(band: BandSeries, xScale: Scale, ctx: CanvasRenderingContext2D, k?: number, boundaries?: readonly number[]): BandSeries;
|
|
229
238
|
/**
|
|
230
239
|
* Decimate an {@link OhlcSeries} to one **aggregate candle per device-pixel
|
|
231
240
|
* column** — `open = first`, `high = max`, `low = min`, `close = last` over the
|
package/dist/decimate.js
CHANGED
|
@@ -400,8 +400,17 @@ export function m4Polyline(edges, mn, mx, first, last, W, breakAt = NO_BREAKS) {
|
|
|
400
400
|
* `upper` are finite **together** per sample (the paired-percentile shape bands
|
|
401
401
|
* are built from); a column where only one edge has finite samples would bin a
|
|
402
402
|
* band segment that no single sample carried.
|
|
403
|
+
*
|
|
404
|
+
* `boundaries` are trading-axis session-break instants (`<BandChart
|
|
405
|
+
* sessionBreaks>`), handled exactly as {@link decimateM4} does for a line: each
|
|
406
|
+
* in-domain instant is unioned into the bucket edges so no column merges two
|
|
407
|
+
* sessions' envelopes across the discontinuity, **and** a `NaN` sample is
|
|
408
|
+
* emitted at the instant so the fill ends at the close and re-starts at the
|
|
409
|
+
* open — otherwise the closing and opening columns would sit as adjacent finite
|
|
410
|
+
* samples and the envelope would flow straight across the collapsed gap. The
|
|
411
|
+
* caller's `sessionRuns` then sees the break baked in and passes no boundaries.
|
|
403
412
|
*/
|
|
404
|
-
export function decimateBand(band, xScale, ctx, k = 2) {
|
|
413
|
+
export function decimateBand(band, xScale, ctx, k = 2, boundaries = []) {
|
|
405
414
|
if (!shouldDecimateCount(band.length, ctx, k))
|
|
406
415
|
return band;
|
|
407
416
|
const dom = scaleDomain(xScale);
|
|
@@ -412,18 +421,50 @@ export function decimateBand(band, xScale, ctx, k = 2) {
|
|
|
412
421
|
if (invert === null || plotWidthCss === null)
|
|
413
422
|
return band;
|
|
414
423
|
const W = deviceBucketCount(ctx);
|
|
415
|
-
const
|
|
424
|
+
const pixels = pixelEdges(invert, plotWidthCss, W);
|
|
425
|
+
// Session-break instants inside the visible domain — unioned into the edges
|
|
426
|
+
// (so a column never straddles a break) AND marked as explicit break samples.
|
|
427
|
+
// `mergeGapEdges` keeps their exact values, so the set matches the edges.
|
|
428
|
+
const breaks = boundaries.length > 0
|
|
429
|
+
? boundaries.filter((b) => b > dom[0] && b < dom[1])
|
|
430
|
+
: [];
|
|
431
|
+
const edges = breaks.length > 0 ? mergeGapEdges(pixels, breaks, dom[0], dom[1]) : pixels;
|
|
432
|
+
const buckets = edges.length - 1;
|
|
416
433
|
const lowerMin = new Float64Column(band.lower, band.length).binBy(band.x, edges, 'min');
|
|
417
434
|
const upperMax = new Float64Column(band.upper, band.length).binBy(band.x, edges, 'max');
|
|
418
|
-
const
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
435
|
+
const breakAt = breaks.length > 0 ? new Set(breaks) : null;
|
|
436
|
+
// One sample per column + one NaN break slot per session break. `breaks` are
|
|
437
|
+
// strictly inside the domain and `mergeGapEdges` keeps every one, so each
|
|
438
|
+
// lands on some `edges[b]` with `b > 0` and the arrays fill exactly; the
|
|
439
|
+
// subarray trim below is a guard against a provider instant that sorts ahead
|
|
440
|
+
// of the first pixel edge under float rounding, not an expected path.
|
|
441
|
+
const cap = buckets + (breakAt === null ? 0 : breakAt.size);
|
|
442
|
+
const x = new Float64Array(cap);
|
|
443
|
+
const lower = new Float64Array(cap);
|
|
444
|
+
const upper = new Float64Array(cap);
|
|
445
|
+
let n = 0;
|
|
446
|
+
for (let b = 0; b < buckets; b += 1) {
|
|
447
|
+
// Explicit session break: this column opens a new session → end the fill
|
|
448
|
+
// first. A NaN on both edges is the band's own gap signal (`.defined`).
|
|
449
|
+
if (breakAt !== null && b > 0 && breakAt.has(edges[b])) {
|
|
450
|
+
x[n] = edges[b];
|
|
451
|
+
lower[n] = NaN;
|
|
452
|
+
upper[n] = NaN;
|
|
453
|
+
n += 1;
|
|
454
|
+
}
|
|
455
|
+
x[n] = (edges[b] + edges[b + 1]) / 2; // column centre
|
|
456
|
+
lower[n] = lowerMin[b]; // NaN on an empty column → the fill break
|
|
457
|
+
upper[n] = upperMax[b];
|
|
458
|
+
n += 1;
|
|
425
459
|
}
|
|
426
|
-
return
|
|
460
|
+
return n === cap
|
|
461
|
+
? { x, lower, upper, length: n }
|
|
462
|
+
: {
|
|
463
|
+
x: x.subarray(0, n),
|
|
464
|
+
lower: lower.subarray(0, n),
|
|
465
|
+
upper: upper.subarray(0, n),
|
|
466
|
+
length: n,
|
|
467
|
+
};
|
|
427
468
|
}
|
|
428
469
|
/**
|
|
429
470
|
* Decimate an {@link OhlcSeries} to one **aggregate candle per device-pixel
|
|
@@ -50,6 +50,25 @@ export declare function ticks(sessions: Session[], stepMs: number): TimeSeries<t
|
|
|
50
50
|
* pen-up. (Plain {@link ticks} walks continuously across sessions, so close ≈
|
|
51
51
|
* next open and the break is invisible — this is the fixture that shows it.) */
|
|
52
52
|
export declare function gappingTicks(sessions: Session[], stepMs: number): TimeSeries<typeof tickSchema>;
|
|
53
|
+
export declare const envelopeSchema: readonly [{
|
|
54
|
+
readonly name: "time";
|
|
55
|
+
readonly kind: "time";
|
|
56
|
+
}, {
|
|
57
|
+
readonly name: "price";
|
|
58
|
+
readonly kind: "number";
|
|
59
|
+
}, {
|
|
60
|
+
readonly name: "lo";
|
|
61
|
+
readonly kind: "number";
|
|
62
|
+
}, {
|
|
63
|
+
readonly name: "hi";
|
|
64
|
+
readonly kind: "number";
|
|
65
|
+
}];
|
|
66
|
+
/** {@link gappingTicks} plus a `lo` / `hi` envelope around the price (a slowly
|
|
67
|
+
* breathing spread), so a `<BandChart>` on the trading axis shows the same
|
|
68
|
+
* overnight jump the line does: connected, the fill runs a near-vertical sliver
|
|
69
|
+
* from one session's close to the next open; with `sessionBreaks` it ends at
|
|
70
|
+
* the close and re-starts at the open. */
|
|
71
|
+
export declare function gappingEnvelope(sessions: Session[], stepMs: number): TimeSeries<typeof envelopeSchema>;
|
|
53
72
|
export declare const OHLC: {
|
|
54
73
|
readonly open: {
|
|
55
74
|
readonly from: "price";
|
|
@@ -195,6 +195,30 @@ export function gappingTicks(sessions, stepMs) {
|
|
|
195
195
|
});
|
|
196
196
|
return new TimeSeries({ name: 'ticks', schema: tickSchema, rows });
|
|
197
197
|
}
|
|
198
|
+
export const envelopeSchema = [
|
|
199
|
+
{ name: 'time', kind: 'time' },
|
|
200
|
+
{ name: 'price', kind: 'number' },
|
|
201
|
+
{ name: 'lo', kind: 'number' },
|
|
202
|
+
{ name: 'hi', kind: 'number' },
|
|
203
|
+
];
|
|
204
|
+
/** {@link gappingTicks} plus a `lo` / `hi` envelope around the price (a slowly
|
|
205
|
+
* breathing spread), so a `<BandChart>` on the trading axis shows the same
|
|
206
|
+
* overnight jump the line does: connected, the fill runs a near-vertical sliver
|
|
207
|
+
* from one session's close to the next open; with `sessionBreaks` it ends at
|
|
208
|
+
* the close and re-starts at the open. */
|
|
209
|
+
export function gappingEnvelope(sessions, stepMs) {
|
|
210
|
+
const rows = [];
|
|
211
|
+
let i = 0;
|
|
212
|
+
sessions.forEach((s, si) => {
|
|
213
|
+
const base = 100 + si * 6; // each session gaps ~6 above the last
|
|
214
|
+
for (let t = s.open; t < s.close; t += stepMs, i++) {
|
|
215
|
+
const price = base + 4 * Math.sin(i / 18) + 1.5 * Math.sin(i / 3.5);
|
|
216
|
+
const spread = 1.5 + 0.8 * Math.sin(i / 40);
|
|
217
|
+
rows.push([t, price, price - spread, price + spread]);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
return new TimeSeries({ name: 'envelope', schema: envelopeSchema, rows });
|
|
221
|
+
}
|
|
198
222
|
export const OHLC = {
|
|
199
223
|
open: { from: 'price', using: 'first' },
|
|
200
224
|
high: { from: 'price', using: 'max' },
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pond-ts/charts",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.70.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Canvas-rendered, streaming-first React time-series charts for pond-ts: line, area, band, bar, scatter, box, candlestick, with cursors, selection and annotations",
|
|
6
6
|
"keywords": [
|
|
@@ -62,8 +62,8 @@
|
|
|
62
62
|
"perf": "PERF_BENCH=1 playwright test perf.spec.ts --workers=1"
|
|
63
63
|
},
|
|
64
64
|
"peerDependencies": {
|
|
65
|
-
"@pond-ts/react": "^0.
|
|
66
|
-
"pond-ts": "^0.
|
|
65
|
+
"@pond-ts/react": "^0.70.0",
|
|
66
|
+
"pond-ts": "^0.70.0",
|
|
67
67
|
"react": "^18.0.0 || ^19.0.0"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|