@pond-ts/charts 0.67.0 → 0.69.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/AGENTS.md +253 -0
- package/API.md +88 -83
- package/CHANGELOG.md +109 -1
- package/README.md +6 -2
- package/dist/ChartContainer.d.ts +23 -0
- package/dist/ChartContainer.js +15 -4
- package/dist/XAxis.d.ts +13 -1
- package/dist/XAxis.js +35 -3
- package/dist/context.d.ts +14 -0
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/tickLadder.d.ts +66 -20
- package/dist/tickLadder.js +178 -105
- package/dist/tradingTimeScale.d.ts +44 -3
- package/dist/tradingTimeScale.js +113 -38
- package/package.json +32 -7
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.69.0...HEAD
|
|
12
|
+
[0.69.0]: https://github.com/pond-ts/pond/compare/v0.68.0...v0.69.0
|
|
13
|
+
[0.68.0]: https://github.com/pond-ts/pond/compare/v0.67.0...v0.68.0
|
|
12
14
|
[0.67.0]: https://github.com/pond-ts/pond/compare/v0.66.0...v0.67.0
|
|
13
15
|
[0.66.0]: https://github.com/pond-ts/pond/compare/v0.65.0...v0.66.0
|
|
14
16
|
[0.65.0]: https://github.com/pond-ts/pond/compare/v0.64.0...v0.65.0
|
|
@@ -70,6 +72,112 @@ include new features and type-level changes; patch bumps are strictly additive.
|
|
|
70
72
|
|
|
71
73
|
## [Unreleased]
|
|
72
74
|
|
|
75
|
+
## [0.69.0] — 2026-09-13
|
|
76
|
+
|
|
77
|
+
### Added
|
|
78
|
+
|
|
79
|
+
- **`<ChartContainer timeZone>` — the time axis in any IANA zone
|
|
80
|
+
([PND-TZAXIS]).** Day / week / month ticks land on that zone's midnights,
|
|
81
|
+
Mondays and month starts; labels, the stacked date bands, the hierarchical
|
|
82
|
+
grid, session dividers and every cursor / marker / annotation readout read
|
|
83
|
+
in it. **Omitted ⇒ the viewer's zone**, exactly as before. The d3 specifier
|
|
84
|
+
strings on `timeFormat` / `cursorFormat` are unchanged; `%Z` / `%z` now read
|
|
85
|
+
the zone's abbreviation / offset. Sub-day ticks align to the zone's wall
|
|
86
|
+
clock, so a 6 h grain reads 00 / 06 / 12 / 18 across a DST jump instead of
|
|
87
|
+
drifting by an hour until the next midnight. The resolved zone is on the
|
|
88
|
+
chart context as `timeZone`. Built on core's `TimeZone` ([PND-TZCAL]), so a
|
|
89
|
+
`Sequence.calendar('day', { timeZone })` bucket edge and the tick that
|
|
90
|
+
labels it are one instant — pinned by a cross-package test.
|
|
91
|
+
- **`<XAxis timeZone>` — a second strip in another zone.** Two time axes
|
|
92
|
+
over one shared mapping, each ticking and labelling (and pilling) in its
|
|
93
|
+
own zone: `<XAxis side="top" timeZone="America/New_York" />` above a
|
|
94
|
+
UTC container's own strip below. Backed by
|
|
95
|
+
`TradingTimeScale.withTimeZone(zone)` / `.timeZone()` and an optional
|
|
96
|
+
`DiscontinuityProvider.withTimeZone` (the identity provider re-derives its
|
|
97
|
+
day anchors; a trading calendar's session opens are zone-independent).
|
|
98
|
+
- **`TradingCalendarLike.timeZone?`** — a calendar that carries its exchange
|
|
99
|
+
zone supplies the axis default (`calendar={cal}` renders in exchange time
|
|
100
|
+
wherever it is viewed); an explicit `timeZone` prop wins.
|
|
101
|
+
- `scaleTradingTime(provider, { timeZone })` and
|
|
102
|
+
`identityProvider({ timeZone })` take the zone directly for consumers
|
|
103
|
+
building the scale themselves; `identityProvider`, `TradingCalendarLike`
|
|
104
|
+
and `ScaleTimeZoneOptions` are now exported. Internally the tick ladder
|
|
105
|
+
runs on a `TickCalendar` seam whose local implementation is the previous
|
|
106
|
+
`Date` arithmetic verbatim — the default path is unchanged.
|
|
107
|
+
- **`TradingCalendar.timeZone` ([PND-TZFIN]).** `@pond-ts/financial`'s
|
|
108
|
+
calendar keeps the zone its sessions were resolved in — `fromRules` carries
|
|
109
|
+
`rules.timeZone`, `fromSessions(list, { timeZone })` takes it — so
|
|
110
|
+
`<ChartContainer calendar={cal}>` renders the axis in exchange time with no
|
|
111
|
+
further wiring.
|
|
112
|
+
- `@pond-ts/charts` now depends on `d3-time-format` directly (it was already
|
|
113
|
+
a transitive dependency via `d3-scale`).
|
|
114
|
+
- **`TimeZone` — the zone-calendar primitive ([PND-TZCAL]).** `pond-ts`
|
|
115
|
+
exports `TimeZone.of(id)` (interned; also `TimeZone.UTC`,
|
|
116
|
+
`TimeZone.local()`) with `startOf(unit, t)`, `next(unit, t)`, `parts(t)`,
|
|
117
|
+
`instant(parts, { disambiguation })`, `offsetAt(t)` and
|
|
118
|
+
`abbreviation(t, { locale })`. Temporal underneath, but each zone caches its
|
|
119
|
+
offset transitions as it discovers them, so steady-state calls are integer
|
|
120
|
+
arithmetic: `startOf('day')` went from ~24 µs to ~23 ns per call, and a
|
|
121
|
+
three-year hourly series aggregated to `America/New_York` days from 38 ms
|
|
122
|
+
to 0.5 ms. `Sequence.calendar`, `TimeRange.fromCalendar` and
|
|
123
|
+
`Interval.fromCalendar` now bucket through it (no behaviour change; pinned
|
|
124
|
+
against Temporal on eight zones including southern-hemisphere DST, a
|
|
125
|
+
30-minute DST shift, a +05:30 zone, a day with no midnight and Samoa's
|
|
126
|
+
skipped day). This is the primitive the charts' time axis will place and
|
|
127
|
+
label ticks with, so a bucket edge and the tick that labels it are one
|
|
128
|
+
instant. First task of the time-zone plan
|
|
129
|
+
(`docs/plans/PND_TIMEZONE_PLAN.md`).
|
|
130
|
+
- **`CalendarUnit` gains `'quarter'` and `'year'`** for
|
|
131
|
+
`Sequence.calendar`, `TimeRange.fromCalendar` and `Interval.fromCalendar`.
|
|
132
|
+
|
|
133
|
+
### Changed
|
|
134
|
+
|
|
135
|
+
- **`Sequence.calendar` validates its inputs at construction.** An unknown
|
|
136
|
+
unit (`'hour'`) or zone (`'Nowhere'`) now throws `RangeError` immediately;
|
|
137
|
+
before, an unknown unit silently produced wrong buckets (the two unit
|
|
138
|
+
dispatchers fell through to different defaults — the 2026-06 audit's §6
|
|
139
|
+
finding) and an unknown zone failed only on first `bounded()`.
|
|
140
|
+
|
|
141
|
+
### Fixed
|
|
142
|
+
|
|
143
|
+
- **`pond-ts`: two type-level corners of [PND-PARTCOL] (0.68.0) found by the Codex pass on #724.** (1) On a broad `TimeSeries<SeriesSchema>` with a _literal_ partition column, the injected `'first'` could not look up a kind and typed the column as `undefined`; `WithPartitionColumns` now takes the schema and leaves the mapping alone when the schema is broad, so the result type is exactly 0.67's. (2) `By` had no variance pin, so `PartitionedTimeSeries<S, K, 'host'>` accepted a view partitioned by `region` (and an untyped view could be narrowed to any column); a phantom contravariant member now rejects both while a specialised view still assigns to the legacy `PartitionedTimeSeries<S>` shape. Type tests cover both plus the `K`-survives-`smooth`/`baseline` claim. No runtime change.
|
|
144
|
+
- **Docs said a wall-clock string without `parse.timeZone` throws. It never
|
|
145
|
+
did** ([PND-TZDOCS]) — it is read as UTC, silently. `creating.mdx`, the
|
|
146
|
+
agent guide (`AGENTS.md`) and the decision table now say so and describe
|
|
147
|
+
how the shift shows up. The agent guide also gains the one time-zone rule:
|
|
148
|
+
pass the same `timeZone` to `Sequence.calendar` and `<ChartContainer>`.
|
|
149
|
+
The aggregation page cross-links `Sequence.calendar` for weekly / monthly
|
|
150
|
+
bars (issue #358 item 1, supersedes #359). The finance gallery's off-chart
|
|
151
|
+
readout takes the calendar's zone instead of hard-coding New York; the
|
|
152
|
+
Niño 3.4 heat map's year grain uses `Sequence.calendar('year')`.
|
|
153
|
+
|
|
154
|
+
## [0.68.0] — 2026-09-13
|
|
155
|
+
|
|
156
|
+
### Added
|
|
157
|
+
|
|
158
|
+
- **Agent adoption tranche ([PND-ADOPTMETA] / [PND-ADOPTLINKS] /
|
|
159
|
+
[PND-LLMSTXT] / [PND-AGENTGUIDE] / [PND-SKILL] / [PND-CONTEXT7]).** Every
|
|
160
|
+
package now declares `keywords`, `homepage` and `bugs` (there were none —
|
|
161
|
+
`pond-ts` ranked last in `npm search "time series"`). Every tarball ships an
|
|
162
|
+
`AGENTS.md` (source `docs/agents/USING_POND.md`): which package for which
|
|
163
|
+
task, the core idioms, the mistakes agents make. `pond-ts.org/llms.txt` is
|
|
164
|
+
now llmstxt.org-shaped (titles + descriptions per page, one section per
|
|
165
|
+
docs area, `Optional` links to `API.md` / the agent guide) with per-area
|
|
166
|
+
`llms-<area>.txt` dumps so a single fetch stays small. A Claude Code plugin
|
|
167
|
+
marketplace lives in the repo (`/plugin marketplace add pond-ts/pond`) with
|
|
168
|
+
`pond-ts`, `pond-charts` and `pond-financial` skills. `context7.json`
|
|
169
|
+
configures docs-MCP indexing. Plan and baseline:
|
|
170
|
+
`docs/plans/PND_ADOPTION_PLAN.md`.
|
|
171
|
+
|
|
172
|
+
- **Agent guide + skill hardened by the first cold-start run** (`docs/agents/USING_POND.md`, shipped as `AGENTS.md`; `plugins/pond-ts/skills/pond-ts`): install with `@latest` and the `.d.ts` paths that carry signatures. Cold-start harness committed at `docs/adoption/cold-start/`.
|
|
173
|
+
|
|
174
|
+
### Fixed
|
|
175
|
+
|
|
176
|
+
- **`pond-ts`: the partition column is now in the static type after a partitioned `aggregate` / `rolling` ([PND-PARTCOL]).** `series.partitionBy('host').aggregate(seq, { p95: { from: 'ms', using: 'p95' } }).collect()` always carried `host` at runtime (auto-injected as `'first'`) but the result type omitted it, so `e.get('host')` failed to compile — every fresh agent in the cold-start experiment hit or pre-empted it. `PartitionedTimeSeries` gains a third type parameter `By` (the partition column names, captured by `partitionBy`, default `never`), and the two schema-replacing operators are typed over `WithPartitionColumns<Mapping, By>` — the user's keys win, kind and all; missing partition columns are added as `'first'`. Composite partitions and typed `groups` carry through; `smooth` / `baseline` under `partitionBy` now also keep `K`. Additive: untyped views are unchanged.
|
|
177
|
+
- `@pond-ts/charts` and `@pond-ts/fit` READMEs (rendered on npm) and eight
|
|
178
|
+
docs pages pointed at the retired `pjm17971.github.io/pond-ts` site /
|
|
179
|
+
`pjm17971/pond-ts` repo; now `pond-ts.org` / `pond-ts/pond`.
|
|
180
|
+
|
|
73
181
|
## [0.67.0] — 2026-09-11
|
|
74
182
|
|
|
75
183
|
### Added
|
package/README.md
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
# @pond-ts/charts
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/@pond-ts/charts)
|
|
4
|
+
[](https://github.com/pond-ts/pond/actions/workflows/ci.yml)
|
|
5
|
+
[](https://pond-ts.org/docs/charts/)
|
|
6
|
+
|
|
3
7
|
**React charts for [pond-ts](https://www.npmjs.com/package/pond-ts) time series.**
|
|
4
8
|
|
|
5
9
|
A composable charting layer built directly on pond-ts series: a canvas data
|
|
@@ -62,8 +66,8 @@ instead of time, with no axis-type prop.
|
|
|
62
66
|
## Documentation
|
|
63
67
|
|
|
64
68
|
Guides, the component reference, and live examples live at
|
|
65
|
-
**<https://
|
|
66
|
-
[github.com/
|
|
69
|
+
**<https://pond-ts.org/docs/charts/>**. Source and issues:
|
|
70
|
+
[github.com/pond-ts/pond](https://github.com/pond-ts/pond).
|
|
67
71
|
|
|
68
72
|
## License
|
|
69
73
|
|
package/dist/ChartContainer.d.ts
CHANGED
|
@@ -192,6 +192,29 @@ export interface ChartContainerProps {
|
|
|
192
192
|
* calendar reference (build it once, not inline in JSX).
|
|
193
193
|
*/
|
|
194
194
|
calendar?: TradingCalendarLike;
|
|
195
|
+
/**
|
|
196
|
+
* The IANA **time zone the time axis renders in** — ticks land on that
|
|
197
|
+
* zone's midnights / Mondays / month starts, labels, grid, date bands,
|
|
198
|
+
* session dividers and every cursor / marker readout read in it.
|
|
199
|
+
* **Omitted ⇒ the viewer's own zone** (the runtime's), which is what every
|
|
200
|
+
* chart did before this prop existed; `'UTC'` or any id `Intl` knows
|
|
201
|
+
* (`'Europe/Berlin'`, `'Australia/Sydney'`, …) names one. A trading
|
|
202
|
+
* {@link calendar} that carries a `timeZone` (a `@pond-ts/financial`
|
|
203
|
+
* `TradingCalendar.fromRules`) supplies the default, so a NYSE chart reads
|
|
204
|
+
* New York time wherever it is viewed; an explicit prop still wins. The
|
|
205
|
+
* calendar's zone is used even when a low-level {@link discontinuities}
|
|
206
|
+
* provider overrides its gap topology — the calendar still says which
|
|
207
|
+
* exchange this is.
|
|
208
|
+
*
|
|
209
|
+
* Pair it with the aggregate that produced the data — the same primitive
|
|
210
|
+
* (`TimeZone`) places these ticks and cuts `Sequence.calendar` buckets, so
|
|
211
|
+
* `Sequence.calendar('day', { timeZone })` and `<ChartContainer timeZone>`
|
|
212
|
+
* given the same zone put a bucket edge and its tick on one instant.
|
|
213
|
+
* Function formatters (`timeFormat`, `cursorFormat`) still receive epoch ms;
|
|
214
|
+
* read the resolved zone from the chart context. An unknown id throws
|
|
215
|
+
* `RangeError`. Only affects a **time** axis.
|
|
216
|
+
*/
|
|
217
|
+
timeZone?: string | undefined;
|
|
195
218
|
/**
|
|
196
219
|
* The trading axis **metric**, when a {@link calendar} is supplied
|
|
197
220
|
* (trading-calendar RFC Q7). `'proportional'` (default) keeps time
|
package/dist/ChartContainer.js
CHANGED
|
@@ -4,7 +4,7 @@ import { scaleLinear, scaleLog, scaleSymlog } from 'd3-scale';
|
|
|
4
4
|
import { identityProvider, scaleTradingTime, } from './tradingTimeScale.js';
|
|
5
5
|
import { scaleBand } from './bandScale.js';
|
|
6
6
|
import { scaleElapsed } from './elapsed.js';
|
|
7
|
-
import { Sequence } from 'pond-ts';
|
|
7
|
+
import { Sequence, TimeZone } from 'pond-ts';
|
|
8
8
|
import { ContainerContext, CursorContext, } from './context.js';
|
|
9
9
|
import { LegacyCursor, legacyCursorWarning, presetNameFor, warnOnDuplicateGestureOwners, } from './cursors.js';
|
|
10
10
|
import { effectiveSelectorEntries, resolveControlledHovered, resolveControlledSelected, selectorEntryEqual, warnInertClick, } from './selectors.js';
|
|
@@ -198,7 +198,7 @@ function AutoSizeContainer(props) {
|
|
|
198
198
|
* pass; a chart legitimately gated this long is not painting anyway. */
|
|
199
199
|
const ZERO_SIZE_WARNING_MS = 600;
|
|
200
200
|
/** {@link ChartContainer} with its width resolved to a concrete pixel number. */
|
|
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, }) {
|
|
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, timeZone: timeZoneProp, spacing, xScale: xScaleKind = 'linear', grid = true, sessionDividers = 'none', children, }) {
|
|
202
202
|
// ── Legacy cursor props (deprecated) ───────────────────────────────────────
|
|
203
203
|
// The string surface keeps working for one minor: the resolved mode is
|
|
204
204
|
// synthesized into the equivalent mounted preset below (`<LegacyCursor>`),
|
|
@@ -967,6 +967,13 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
967
967
|
? calendar.discontinuities(spacing ? { spacing } : undefined)
|
|
968
968
|
: undefined, [resolvedKind, discontinuities, calendar, spacing]);
|
|
969
969
|
const xDiscontinuities = resolvedKind === 'time' ? (discontinuities ?? calendarProvider) : undefined;
|
|
970
|
+
// The axis zone: the explicit prop, else the calendar's exchange zone, else
|
|
971
|
+
// runtime-local (`undefined`). Canonicalised through `TimeZone.of` so a bad
|
|
972
|
+
// id fails here, once, with its name, and so `'utc'` and `'UTC'` are one key.
|
|
973
|
+
const timeZone = useMemo(() => {
|
|
974
|
+
const id = timeZoneProp ?? calendar?.timeZone;
|
|
975
|
+
return id === undefined ? undefined : TimeZone.of(id).id;
|
|
976
|
+
}, [timeZoneProp, calendar]);
|
|
970
977
|
// The shared x-side tick count — labels, x gridlines, session dividers, and
|
|
971
978
|
// `formatTime` all pass this one value, so they derive from the same instants
|
|
972
979
|
// (the alignment previously held by three hardcoded constants agreeing).
|
|
@@ -1147,7 +1154,7 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1147
1154
|
// sessions. Same tickFormat surface as scaleTime, so the readout is shared.
|
|
1148
1155
|
// `xTickCount` reaches `tickFormat` too: the trading scale picks its anchor
|
|
1149
1156
|
// grain from the count, so labels sit on the exact instants the ticks do.
|
|
1150
|
-
const s = scaleTradingTime(xDiscontinuities)
|
|
1157
|
+
const s = scaleTradingTime(xDiscontinuities, { timeZone })
|
|
1151
1158
|
.domain([d0, d1])
|
|
1152
1159
|
.range([0, plotWidth]);
|
|
1153
1160
|
if (elapsedOrigin !== undefined)
|
|
@@ -1164,7 +1171,7 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1164
1171
|
// never d3's mixed multi-scale default. Interactions stay on continuous
|
|
1165
1172
|
// time math: the frame's `discontinuities` remains undefined, and identity
|
|
1166
1173
|
// distance/offset are plain subtraction/addition anyway.
|
|
1167
|
-
const s = scaleTradingTime(identityProvider())
|
|
1174
|
+
const s = scaleTradingTime(identityProvider({ timeZone }), { timeZone })
|
|
1168
1175
|
.domain([d0, d1])
|
|
1169
1176
|
.range([0, plotWidth]);
|
|
1170
1177
|
if (elapsedOrigin !== undefined)
|
|
@@ -1187,6 +1194,7 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1187
1194
|
elapsedOrigin,
|
|
1188
1195
|
xDiscontinuities,
|
|
1189
1196
|
xTickCount,
|
|
1197
|
+
timeZone,
|
|
1190
1198
|
]);
|
|
1191
1199
|
// The crosshair pixel (see resolveCursorX). A stored hoverX is a *plot* pixel;
|
|
1192
1200
|
// if plotWidth changes mid-hover (a gutter reserving, or a width change) it's
|
|
@@ -1361,6 +1369,8 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1361
1369
|
onEditAnnotation,
|
|
1362
1370
|
formatTime,
|
|
1363
1371
|
formatReadout,
|
|
1372
|
+
timeZone,
|
|
1373
|
+
timeFormat,
|
|
1364
1374
|
xFormatCustom: timeFormat !== undefined,
|
|
1365
1375
|
xReadoutCustom: cursorFormat !== undefined,
|
|
1366
1376
|
xTickCount,
|
|
@@ -1445,6 +1455,7 @@ function ResolvedChartContainer({ range, categories: categoriesProp, maxBandWidt
|
|
|
1445
1455
|
onEditAnnotation,
|
|
1446
1456
|
formatTime,
|
|
1447
1457
|
formatReadout,
|
|
1458
|
+
timeZone,
|
|
1448
1459
|
timeFormat,
|
|
1449
1460
|
cursorFormat,
|
|
1450
1461
|
xTickCount,
|
package/dist/XAxis.d.ts
CHANGED
|
@@ -90,6 +90,18 @@ export interface XAxisProps {
|
|
|
90
90
|
* measured fit (thin + middle-ellipsize) is what prevents collisions.
|
|
91
91
|
*/
|
|
92
92
|
align?: 'auto' | 'center' | 'right';
|
|
93
|
+
/**
|
|
94
|
+
* Render **this strip** in an IANA zone other than the container's — the
|
|
95
|
+
* second axis of a two-zone pair (`<XAxis />` in the container's zone below
|
|
96
|
+
* the plot, `<XAxis side="top" timeZone="Asia/Tokyo" />` above it). Same
|
|
97
|
+
* pixel mapping, its own calendar: day ticks on *this* zone's midnights,
|
|
98
|
+
* labels, the date bands and this strip's cursor / marker pills reading in
|
|
99
|
+
* it. Time axis only; ignored under a `transform` or explicit `ticks`. A
|
|
100
|
+
* container `cursorFormat` still wins for the pill (it is its own channel);
|
|
101
|
+
* a container `timeFormat` string is re-resolved in this zone. Omit to
|
|
102
|
+
* follow the container's `timeZone` (or the viewer's zone).
|
|
103
|
+
*/
|
|
104
|
+
timeZone?: string | undefined;
|
|
93
105
|
/**
|
|
94
106
|
* How a **time** axis lays out its date context (ignored on value / category
|
|
95
107
|
* axes, and whenever a custom `format`, `transform`, or explicit `ticks`
|
|
@@ -140,6 +152,6 @@ export interface XAxisProps {
|
|
|
140
152
|
* plot's own drag, including `bounds` / `minDuration` and the trading calendar.
|
|
141
153
|
* A category axis has no continuous domain and stays inert.
|
|
142
154
|
*/
|
|
143
|
-
export declare function XAxis({ format, label, side, height, ticks: customTicks, transform, color, align, dateStyle, onMouseEvent, }?: XAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
155
|
+
export declare function XAxis({ format, label, side, height, ticks: customTicks, transform, color, align, dateStyle, timeZone, onMouseEvent, }?: XAxisProps): import("react/jsx-runtime").JSX.Element;
|
|
144
156
|
export {};
|
|
145
157
|
//# sourceMappingURL=XAxis.d.ts.map
|
package/dist/XAxis.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { Fragment, useContext, useRef } from 'react';
|
|
2
|
+
import { Fragment, useContext, useMemo, useRef } from 'react';
|
|
3
3
|
import { scaleLinear } from 'd3-scale';
|
|
4
|
+
import { TimeZone } from 'pond-ts';
|
|
4
5
|
import { derivedTicks } from './derivedTicks.js';
|
|
5
6
|
import { ContainerContext, CursorContext, } from './context.js';
|
|
6
7
|
import { tickValues } from './yticks.js';
|
|
@@ -166,7 +167,7 @@ export function thinCategoryLabels(ticks, slot, plotWidth, fontSize, fontFamily)
|
|
|
166
167
|
* plot's own drag, including `bounds` / `minDuration` and the trading calendar.
|
|
167
168
|
* A category axis has no continuous domain and stays inert.
|
|
168
169
|
*/
|
|
169
|
-
export function XAxis({ format, label, side = 'bottom', height, ticks: customTicks, transform, color, align = 'center', dateStyle = 'flat', onMouseEvent, } = {}) {
|
|
170
|
+
export function XAxis({ format, label, side = 'bottom', height, ticks: customTicks, transform, color, align = 'center', dateStyle = 'flat', timeZone, onMouseEvent, } = {}) {
|
|
170
171
|
const container = useContext(ContainerContext);
|
|
171
172
|
if (container === null) {
|
|
172
173
|
throw new Error('<XAxis> must be rendered inside a <ChartContainer>');
|
|
@@ -175,7 +176,38 @@ export function XAxis({ format, label, side = 'bottom', height, ticks: customTic
|
|
|
175
176
|
// `xTickCount` is the container's shared x-side count — the same value the x
|
|
176
177
|
// gridlines and `formatTime` use, so labels and grid stay on the same instants
|
|
177
178
|
// (width-derived on a trading-time axis).
|
|
178
|
-
const { xScale, plotWidth, leftGutter, theme, formatTime, xKind, xTickCount, } = container;
|
|
179
|
+
const { xScale: containerScale, plotWidth, leftGutter, theme, formatTime: containerFormatTime, xKind, xTickCount, } = container;
|
|
180
|
+
// A per-strip zone: the container's shared scale re-derived with its
|
|
181
|
+
// calendar in `timeZone` — identical pixel mapping, so every `xScale(v)`
|
|
182
|
+
// below lands where the plot puts it, but ticks / labels / bands / this
|
|
183
|
+
// strip's pills come from this zone's ladder. Canonicalised (and validated)
|
|
184
|
+
// so an unknown id throws by name and `'utc'` / `'UTC'` memoize as one.
|
|
185
|
+
const zonedScale = useMemo(() => {
|
|
186
|
+
if (timeZone === undefined ||
|
|
187
|
+
xKind !== 'time' ||
|
|
188
|
+
!('withTimeZone' in containerScale)) {
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
const id = TimeZone.of(timeZone).id;
|
|
192
|
+
const s = containerScale;
|
|
193
|
+
return s.timeZone() === id ? undefined : s.withTimeZone(id);
|
|
194
|
+
}, [containerScale, timeZone, xKind]);
|
|
195
|
+
const xScale = zonedScale ?? containerScale;
|
|
196
|
+
// The label formatter this strip falls back to when no explicit `format`
|
|
197
|
+
// shapes it. The container's `formatTime` was resolved in the container's
|
|
198
|
+
// zone; a zoned strip needs the same channel in its own — the grain-aware
|
|
199
|
+
// default, or the container's `timeFormat` specifier re-resolved (a
|
|
200
|
+
// function `timeFormat` receives epoch ms and is used verbatim either way).
|
|
201
|
+
const formatTime = useMemo(() => {
|
|
202
|
+
if (zonedScale === undefined)
|
|
203
|
+
return containerFormatTime;
|
|
204
|
+
const custom = container.timeFormat;
|
|
205
|
+
if (typeof custom === 'function')
|
|
206
|
+
return custom;
|
|
207
|
+
return custom === undefined
|
|
208
|
+
? zonedScale.readoutFormat(xTickCount)
|
|
209
|
+
: resolveTimeFormat(zonedScale, xTickCount, custom);
|
|
210
|
+
}, [zonedScale, containerFormatTime, container.timeFormat, xTickCount]);
|
|
179
211
|
// The cursor's x-axis slot: did the mounted cursor in effect register one
|
|
180
212
|
// (`renderXAxis` — the crosshair's time pill)? While hovering, that is the
|
|
181
213
|
// **hovered row's** effective cursor — so a per-row override reaches this
|
package/dist/context.d.ts
CHANGED
|
@@ -270,6 +270,20 @@ export interface ContainerFrame {
|
|
|
270
270
|
* tick labels) without moving them.
|
|
271
271
|
*/
|
|
272
272
|
readonly formatReadout?: ((value: number) => string) | undefined;
|
|
273
|
+
/**
|
|
274
|
+
* The IANA zone the time axis renders in — the container's resolved
|
|
275
|
+
* `timeZone` (explicit prop, else the calendar's), canonical id; `undefined`
|
|
276
|
+
* when the axis is in the runtime's local zone. For a consumer's own
|
|
277
|
+
* formatter (`timeFormat` / `cursorFormat` functions receive epoch ms) to
|
|
278
|
+
* read the same zone the ticks do.
|
|
279
|
+
*/
|
|
280
|
+
readonly timeZone: string | undefined;
|
|
281
|
+
/**
|
|
282
|
+
* The container's raw `timeFormat` prop, for a strip that must re-resolve it
|
|
283
|
+
* in another zone (`<XAxis timeZone>`): a specifier string is re-resolved
|
|
284
|
+
* against that strip's zoned scale, a function is used verbatim.
|
|
285
|
+
*/
|
|
286
|
+
readonly timeFormat: AxisFormat | undefined;
|
|
273
287
|
/** Whether an explicit container `timeFormat` shaped {@link formatTime}. The
|
|
274
288
|
* x axis suppresses its boundary (second) label row when it's set — a
|
|
275
289
|
* custom format owns the whole label, so the ladder mustn't second-line it. */
|
package/dist/index.d.ts
CHANGED
|
@@ -68,8 +68,8 @@ export type { ChartLegend, LegendRow, LegendItem } from './useChartLegend.js';
|
|
|
68
68
|
export { useChartFrame } from './useChartFrame.js';
|
|
69
69
|
export type { ChartFrame, ChartFrameRow, ChartBands, ChartBand, } from './useChartFrame.js';
|
|
70
70
|
export type { ChartXScale } from './context.js';
|
|
71
|
-
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
72
|
-
export type { TradingTimeScale, DiscontinuityProvider, TimeGrain, } from './tradingTimeScale.js';
|
|
71
|
+
export { scaleTradingTime, identityProvider } from './tradingTimeScale.js';
|
|
72
|
+
export type { TradingTimeScale, DiscontinuityProvider, TradingCalendarLike, ScaleTimeZoneOptions, TimeGrain, } from './tradingTimeScale.js';
|
|
73
73
|
export { scaleBand } from './bandScale.js';
|
|
74
74
|
export type { ScaleBand } from './bandScale.js';
|
|
75
75
|
export { Region, Baseline, Marker, Zone } from './annotations.js';
|
package/dist/index.js
CHANGED
|
@@ -58,7 +58,7 @@ export { useChartLegend } from './useChartLegend.js';
|
|
|
58
58
|
// by mirroring the library's own gutter arithmetic — a duplicate that drifts
|
|
59
59
|
// silently the moment the library changes how a gutter is sized.
|
|
60
60
|
export { useChartFrame } from './useChartFrame.js';
|
|
61
|
-
export { scaleTradingTime } from './tradingTimeScale.js';
|
|
61
|
+
export { scaleTradingTime, identityProvider } from './tradingTimeScale.js';
|
|
62
62
|
// The ordinal category (band) scale — the transpose view's "columns on x" axis.
|
|
63
63
|
export { scaleBand } from './bandScale.js';
|
|
64
64
|
// Annotations — user-authored marks in the turquoise register (distinct from the
|
package/dist/tickLadder.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TimeZone } from 'pond-ts';
|
|
1
2
|
import type { DiscontinuityProvider } from './tradingTimeScale.js';
|
|
2
3
|
/**
|
|
3
4
|
* The logical tick ladder — grain selection for a time axis. Ticks sit on real
|
|
@@ -34,16 +35,62 @@ export type TickGranularity = 'second1' | 'second5' | 'second15' | 'second30' |
|
|
|
34
35
|
export type TimeGrain = 'year' | 'quarter' | 'month' | 'week' | 'day' | 'hour' | 'minute' | 'second';
|
|
35
36
|
/** Collapse a {@link TickGranularity} to its coarse {@link TimeGrain} unit. */
|
|
36
37
|
export declare function coarseUnitOf(g: TickGranularity): TimeGrain;
|
|
38
|
+
/** The calendar operations the tick ladder needs. Months are `1…12`. */
|
|
39
|
+
export interface TickCalendar {
|
|
40
|
+
/** Midnight of the day containing `t`. */
|
|
41
|
+
startOfDay(t: number): number;
|
|
42
|
+
/** Midnight of the day after the one containing `t`. */
|
|
43
|
+
nextDay(t: number): number;
|
|
44
|
+
/** Midnight of the Monday of the week containing `t`. */
|
|
45
|
+
startOfWeek(t: number): number;
|
|
46
|
+
/** The civil date of `t`. */
|
|
47
|
+
parts(t: number): {
|
|
48
|
+
year: number;
|
|
49
|
+
month: number;
|
|
50
|
+
day: number;
|
|
51
|
+
};
|
|
52
|
+
/** Midnight of the first of `month` (`1…12`; `13` carries into the next year, `0` into the previous). */
|
|
53
|
+
monthStart(year: number, month: number): number;
|
|
54
|
+
/** Days in `month` of `year`. */
|
|
55
|
+
daysInMonth(year: number, month: number): number;
|
|
56
|
+
/**
|
|
57
|
+
* The first clock-aligned `stepMs` multiple at or after `t`, counted from
|
|
58
|
+
* `t`'s own midnight — 00:00 / 03:00 / 06:00 … for a 3 h step. The local
|
|
59
|
+
* calendar steps **fixed milliseconds** from midnight (so on a DST day the
|
|
60
|
+
* later anchors drift off the wall clock by the shift, until the next
|
|
61
|
+
* midnight re-anchors — the pre-seam behaviour, kept); a zoned calendar
|
|
62
|
+
* aligns to the **wall clock**, so 6 h anchors read 00 / 06 / 12 / 18 on
|
|
63
|
+
* both sides of the jump, with one short or long gap at the transition.
|
|
64
|
+
* Always `>= t`.
|
|
65
|
+
*/
|
|
66
|
+
nextAligned(t: number, stepMs: number): number;
|
|
67
|
+
/**
|
|
68
|
+
* The anchor after an aligned anchor `t` at `stepMs`. The local calendar
|
|
69
|
+
* steps `t + stepMs` — fixed elapsed milliseconds, the pre-seam loop
|
|
70
|
+
* verbatim, which is what keeps a session that spans a DST midnight (a
|
|
71
|
+
* futures 18:00 → 17:00 session) ticking exactly as it did. A zoned
|
|
72
|
+
* calendar re-aligns through {@link nextAligned} so the anchors stay on
|
|
73
|
+
* the wall clock across the jump.
|
|
74
|
+
*/
|
|
75
|
+
nextAnchor(t: number, stepMs: number): number;
|
|
76
|
+
}
|
|
77
|
+
/** The runtime-local calendar — `Date`'s local accessors, exactly as the
|
|
78
|
+
* ladder computed before it had a zone. The default {@link TickCalendar}. */
|
|
79
|
+
export declare const localTickCalendar: TickCalendar;
|
|
80
|
+
/** A {@link TickCalendar} for an IANA zone, on core's `TimeZone`. */
|
|
81
|
+
export declare function zonedTickCalendar(zone: TimeZone): TickCalendar;
|
|
82
|
+
/** Resolve an optional IANA id to the calendar the ladder should use: the
|
|
83
|
+
* runtime-local calendar when `timeZone` is undefined, else the zone's. */
|
|
84
|
+
export declare function tickCalendarFor(timeZone: string | undefined): TickCalendar;
|
|
37
85
|
/**
|
|
38
|
-
* The
|
|
39
|
-
* day / week / month / quarter / year share a key.
|
|
40
|
-
* agrees with the
|
|
41
|
-
* zone
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
* anchor is its own tick), so they key by identity.
|
|
86
|
+
* The calendar bucket key for `t` at grain `g` — two instants in the same
|
|
87
|
+
* day / week / month / quarter / year share a key. Computed in `cal`'s zone
|
|
88
|
+
* (runtime-local by default) so it agrees with the label formatter for the
|
|
89
|
+
* same zone; a trading axis passes its exchange zone so the grain buckets by
|
|
90
|
+
* the exchange day rather than the viewer's. Hour grains are never bucketed
|
|
91
|
+
* (each anchor is its own tick), so they key by identity.
|
|
45
92
|
*/
|
|
46
|
-
export declare function bucketKey(t: number, g: TickGranularity): number;
|
|
93
|
+
export declare function bucketKey(t: number, g: TickGranularity, cal?: TickCalendar): number;
|
|
47
94
|
/**
|
|
48
95
|
* Thin an ascending run of **session opens** down to about `count` axis ticks.
|
|
49
96
|
* Picks the finest rung: every session → **per-month uniform session stride**
|
|
@@ -82,7 +129,7 @@ export declare function bucketKey(t: number, g: TickGranularity): number;
|
|
|
82
129
|
* This is the day-and-coarser half of the ladder; {@link buildTicks} adds the
|
|
83
130
|
* sub-day rungs.
|
|
84
131
|
*/
|
|
85
|
-
export declare function coarsenCalendar(opens: readonly number[], count: number, spanDays?: number, provider?: DiscontinuityProvider): {
|
|
132
|
+
export declare function coarsenCalendar(opens: readonly number[], count: number, spanDays?: number, provider?: DiscontinuityProvider, cal?: TickCalendar): {
|
|
86
133
|
ticks: number[];
|
|
87
134
|
granularity: TickGranularity;
|
|
88
135
|
};
|
|
@@ -105,7 +152,7 @@ export declare function coarsenCalendar(opens: readonly number[], count: number,
|
|
|
105
152
|
* the live-span estimate first (like {@link buildTicks}) and skipped when they
|
|
106
153
|
* add no anchor beyond the session opens themselves (that is the day level).
|
|
107
154
|
*/
|
|
108
|
-
export declare function buildGridLevels(provider: DiscontinuityProvider, opens: readonly number[], domainEnd: number, cap: number): Array<{
|
|
155
|
+
export declare function buildGridLevels(provider: DiscontinuityProvider, opens: readonly number[], domainEnd: number, cap: number, cal?: TickCalendar): Array<{
|
|
109
156
|
granularity: TickGranularity;
|
|
110
157
|
values: number[];
|
|
111
158
|
}>;
|
|
@@ -130,7 +177,7 @@ export declare function nominalStepMs(g: TickGranularity): number;
|
|
|
130
177
|
* themselves fit — a year of daily sessions never wastes time generating hour
|
|
131
178
|
* anchors.
|
|
132
179
|
*/
|
|
133
|
-
export declare function buildTicks(provider: DiscontinuityProvider, opens: readonly number[], domainEnd: number, cap: number): {
|
|
180
|
+
export declare function buildTicks(provider: DiscontinuityProvider, opens: readonly number[], domainEnd: number, cap: number, cal?: TickCalendar): {
|
|
134
181
|
ticks: number[];
|
|
135
182
|
granularity: TickGranularity;
|
|
136
183
|
};
|
|
@@ -181,14 +228,13 @@ export declare function bandFormatFor(g: TickGranularity): string;
|
|
|
181
228
|
* same-shade day-bands side by side on a gappy calendar. A rare, cosmetic
|
|
182
229
|
* consequence of keeping the shade fixed to the date rather than the slot.
|
|
183
230
|
*/
|
|
184
|
-
export declare function bandShaded(t: number, g: TickGranularity): boolean;
|
|
185
|
-
/** The
|
|
186
|
-
*
|
|
187
|
-
|
|
188
|
-
export declare function bandStartOf(t: number, g: TickGranularity): number;
|
|
231
|
+
export declare function bandShaded(t: number, g: TickGranularity, cal?: TickCalendar): boolean;
|
|
232
|
+
/** The start of the band grain `g` containing `t` (the band's left edge) in
|
|
233
|
+
* `cal`'s zone: midnight, month start, or Jan 1. */
|
|
234
|
+
export declare function bandStartOf(t: number, g: TickGranularity, cal?: TickCalendar): number;
|
|
189
235
|
/** The start of the band grain `g` **after** the one containing `t` — the next
|
|
190
|
-
*
|
|
191
|
-
export declare function bandNext(t: number, g: TickGranularity): number;
|
|
236
|
+
* midnight / month start / Jan 1 in `cal`'s zone. */
|
|
237
|
+
export declare function bandNext(t: number, g: TickGranularity, cal?: TickCalendar): number;
|
|
192
238
|
/**
|
|
193
239
|
* Which of `ticks` (at grain `granularity`) carry a boundary label: every tick
|
|
194
240
|
* whose boundary-grain bucket differs from the previous tick's — i.e. a
|
|
@@ -199,7 +245,7 @@ export declare function bandNext(t: number, g: TickGranularity): number;
|
|
|
199
245
|
* tick-to-tick on a live sliding window). Empty when the grain has no
|
|
200
246
|
* boundary row (year grain).
|
|
201
247
|
*/
|
|
202
|
-
export declare function boundaryTicks(ticks: readonly number[], granularity: TickGranularity, domainStart?: number): number[];
|
|
248
|
+
export declare function boundaryTicks(ticks: readonly number[], granularity: TickGranularity, domainStart?: number, cal?: TickCalendar): number[];
|
|
203
249
|
/**
|
|
204
250
|
* The terse **base** (non-promoted) flat label format for grain `g` — the label
|
|
205
251
|
* a tick carries when it opens no coarser period: the clock time for a sub-day
|
|
@@ -228,5 +274,5 @@ export declare function flatBaseFormatFor(g: TickGranularity): string;
|
|
|
228
274
|
* opening at May 1 midnight reads `May 16 …`, not `1 16 …`). Without
|
|
229
275
|
* `domainStart` the first tick is never promoted.
|
|
230
276
|
*/
|
|
231
|
-
export declare function flatFormats(ticks: readonly number[], granularity: TickGranularity, domainStart?: number): string[];
|
|
277
|
+
export declare function flatFormats(ticks: readonly number[], granularity: TickGranularity, domainStart?: number, cal?: TickCalendar): string[];
|
|
232
278
|
//# sourceMappingURL=tickLadder.d.ts.map
|