pond-ts 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/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.67.0...HEAD
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,21 +1,44 @@
1
1
  # pond-ts
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/pond-ts?label=pond-ts)](https://www.npmjs.com/package/pond-ts)
4
+ [![CI](https://github.com/pond-ts/pond/actions/workflows/ci.yml/badge.svg)](https://github.com/pond-ts/pond/actions/workflows/ci.yml)
5
+ [![license: MIT](https://img.shields.io/npm/l/pond-ts)](https://github.com/pond-ts/pond/blob/main/LICENSE)
6
+ [![docs](https://img.shields.io/badge/docs-pond--ts.org-1f6feb)](https://pond-ts.org)
7
+
3
8
  **Highly optimised, fully typed Timeseries library for TypeScript**
4
9
 
5
10
  Schema-driven events, composable batch transforms, push-based streaming
6
- ingest, multi-entity partitioning, and an optional React integration —
7
- all strict TypeScript end to end, all immutable.
11
+ ingest, multi-entity partitioning — and, optionally, React hooks and
12
+ canvas charts that read the series directly. All strict TypeScript end to
13
+ end, all immutable.
8
14
 
9
15
  **pond-ts** is the TypeScript-first successor to
10
16
  [pondjs](https://github.com/esnet/pond), rewritten from scratch with a
11
17
  focus on type safety, composability, and the live-streaming patterns
12
18
  that pondjs never grew.
13
19
 
20
+ ## The packages
21
+
22
+ Three packages carry most projects. The core has no dependency on the other
23
+ two; add them only if you render.
24
+
25
+ | Package | What it is | Needs |
26
+ | --------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------- |
27
+ | **[`pond-ts`](https://www.npmjs.com/package/pond-ts)** — core | `TimeSeries` (batch) and `LiveSeries` (streaming) with one operator vocabulary: aggregate, rolling, align, fill, partition, join, typed columns. Node or browser, no React. | nothing |
28
+ | **[`@pond-ts/charts`](https://www.npmjs.com/package/@pond-ts/charts)** — optional | Declarative React charts on a canvas data plane that consume a pond series with no adapter: line, area, band, bar, scatter, box, candlestick, heat map; cursors, selection, pan/zoom, annotations. | `pond-ts`, `@pond-ts/react`, React 18/19 |
29
+ | **[`@pond-ts/react`](https://www.npmjs.com/package/@pond-ts/react)** — optional | Hooks to own a series in a component and read live views on a throttled snapshot cadence (`useLiveSeries`, `useSnapshot`, …). | `pond-ts`, React 18/19 |
30
+
14
31
  ```sh
15
- npm install pond-ts # core
16
- npm install @pond-ts/react # React hooks (optional)
32
+ npm install pond-ts # core — enough for Node pipelines and non-React apps
33
+ npm install @pond-ts/charts @pond-ts/react pond-ts # add the React chart stack
17
34
  ```
18
35
 
36
+ Two domain packages ([`@pond-ts/financial`](#domain-packages) for markets,
37
+ [`@pond-ts/fit`](#domain-packages) for activity data) and one experimental
38
+ runtime ([`@pond-ts/process`](#domain-packages)) sit on top — see
39
+ [Domain packages](#domain-packages) below. All six release together under
40
+ one version; keep them in step.
41
+
19
42
  - **Typed schemas** — declare once, every transform downstream narrows
20
43
  off it. `event.get('cpu')` returns `number | undefined` straight from
21
44
  the schema; no `as` casts.
@@ -108,6 +131,43 @@ The full live surface (`filter`, `map`, `select`, `window`, `aggregate`,
108
131
  `sample`) is incremental — events flow, views emit, retention bounds
109
132
  memory.
110
133
 
134
+ ## Quick start: charts (React)
135
+
136
+ `@pond-ts/charts` reads a `TimeSeries` or `LiveSeries` directly — do the maths
137
+ in pond, hand the result to a layer. Rows share one x scale, so they pan,
138
+ zoom and track the cursor together.
139
+
140
+ ```tsx
141
+ import {
142
+ BandChart,
143
+ ChartContainer,
144
+ ChartRow,
145
+ Layers,
146
+ LineChart,
147
+ YAxis,
148
+ } from '@pond-ts/charts';
149
+
150
+ // `bands` is the baseline() result from the batch quick start:
151
+ // cpu + avg / sd / upper / lower columns.
152
+ export function CpuChart({ width }: { width: number }) {
153
+ return (
154
+ <ChartContainer width={width} cursor="crosshair" panZoom>
155
+ <ChartRow height={240}>
156
+ <YAxis id="cpu" format=".0%" />
157
+ <Layers>
158
+ <BandChart series={bands} lower="lower" upper="upper" axis="cpu" />
159
+ <LineChart series={bands} column="cpu" axis="cpu" />
160
+ </Layers>
161
+ </ChartRow>
162
+ </ChartContainer>
163
+ );
164
+ }
165
+ ```
166
+
167
+ Pass `width="auto"` to measure the parent instead. Live data renders through
168
+ the same layers: own the series with `useLiveSeries` from `@pond-ts/react`
169
+ and pass its snapshot as `series`.
170
+
111
171
  ## Quick start: multi-entity
112
172
 
113
173
  `partitionBy` routes events into per-key buffers. Every stateful
@@ -182,6 +242,26 @@ it is behind. Run locally:
182
242
  npm run build && node packages/core/bench/vs-pondjs.cjs
183
243
  ```
184
244
 
245
+ ## Domain packages
246
+
247
+ Optional, domain-specific, all on plain pond series:
248
+
249
+ - **[`@pond-ts/financial`](https://www.npmjs.com/package/@pond-ts/financial)**
250
+ — sixty-plus oracle-verified technical studies (SMA, EMA, RSI, MACD,
251
+ Bollinger, ATR, VWAP, …) that append columns to a bar series, a fluent
252
+ `bars.sma({ period: 20 }).rsi({ period: 14 })` form, and a
253
+ `TradingCalendar` so session-aligned bars, rolling windows and chart axes
254
+ stop at the close.
255
+ - **[`@pond-ts/fit`](https://www.npmjs.com/package/@pond-ts/fit)** — fitness
256
+ and activity analytics: typed quantities with units, canonical activity
257
+ series, geo (distance, elevation, best efforts), power (NP / IF / TSS,
258
+ curves), heart-rate zones, splits.
259
+ - **[`@pond-ts/process`](https://www.npmjs.com/package/@pond-ts/process)** —
260
+ **experimental.** Computations as data: processing graphs authored fluently
261
+ or composed as JSON, resolved against a declared op vocabulary with
262
+ content-addressed caching, provenance and per-node timings. The API is
263
+ expected to move.
264
+
185
265
  ## Documentation
186
266
 
187
267
  The full guide is at **<https://pond-ts.org/>**.
@@ -202,6 +282,26 @@ The full guide is at **<https://pond-ts.org/>**.
202
282
  — TypeDoc output, every public class and method.
203
283
  - **[CHANGELOG](./CHANGELOG.md)** — what shipped in each release.
204
284
 
285
+ ## For coding agents
286
+
287
+ pond is built by agents and expects to be used by them. Three things exist so
288
+ an agent can go from "never heard of pond" to working code without a human in
289
+ the loop:
290
+
291
+ - **`AGENTS.md` + `API.md` ship inside every npm tarball** —
292
+ `node_modules/pond-ts/AGENTS.md` is a one-read guide (which package for
293
+ which task, the idioms, the mistakes agents make); `API.md` maps every
294
+ public export to its source file. Source:
295
+ [docs/agents/USING_POND.md](docs/agents/USING_POND.md), [API.md](API.md).
296
+ - **<https://pond-ts.org/llms.txt>** — every docs page with a one-line
297
+ description, plus `llms-<area>.txt` single-fetch dumps per package.
298
+ - **Claude Code plugin** — skills for core, charts and financial, versioned
299
+ with the library:
300
+ ```
301
+ /plugin marketplace add pond-ts/pond
302
+ /plugin install pond-ts@pond-ts
303
+ ```
304
+
205
305
  ## Examples
206
306
 
207
307
  - **[pond-ts-dashboard](https://github.com/pjm17971/pond-ts-dashboard)**
@@ -213,20 +313,22 @@ The full guide is at **<https://pond-ts.org/>**.
213
313
 
214
314
  ## Develop
215
315
 
216
- The repo is an npm-workspaces monorepo with two published packages
217
- (`pond-ts`, `@pond-ts/react`). Node 18+ for runtime; Node 20+ for the
316
+ The repo is an npm-workspaces monorepo with six published packages
317
+ (`pond-ts`, `@pond-ts/react`, `@pond-ts/charts`, `@pond-ts/financial`,
318
+ `@pond-ts/fit`, `@pond-ts/process`). Node 18+ for runtime; Node 20+ for the
218
319
  docs site (Docusaurus).
219
320
 
220
321
  ```sh
221
- npm install # one-time, hoists deps for both packages
322
+ npm install # one-time, hoists deps for all packages
222
323
  npm run build # build both packages
223
324
  npm test # runtime + type-level tests on both packages
224
325
  npm run format # prettier write across the repo
225
326
  npm run verify # format check + build + test (CI parity)
226
327
  ```
227
328
 
228
- `packages/core/` is the `pond-ts` package; `packages/react/` is
229
- `@pond-ts/react`. Docs live in `website/`.
329
+ Each package lives under `packages/<name>/` (`core` is `pond-ts`, the rest
330
+ match their scoped names). Docs live in `website/` — its own npm root, not a
331
+ workspace.
230
332
 
231
333
  ## License
232
334
 
@@ -4,11 +4,29 @@ import { Sequence } from '../sequence/sequence.js';
4
4
  import type { DurationInput } from '../core/duration.js';
5
5
  import type { TemporalLike } from '../core/temporal.js';
6
6
  import type { BatchSampleStrategy } from '../sequence/sample.js';
7
- import type { AggregateSchema, AlignSchema, AppendColumn, BaselineSchema, DedupeKeep, DiffSchema, EventDataForSchema, FillMapping, FillStrategy, MaterializeSchema, NumericColumnNameForSchema, RollingAlignment, RollingSchema, SeriesSchema, SmoothAppendSchema, SmoothMethod, SmoothSchema, ValidatedAggregateMap } from '../schema/index.js';
7
+ import type { AggregateSchema, AlignSchema, AppendColumn, BaselineSchema, DedupeKeep, DiffSchema, EventDataForSchema, ValueColumnsForSchema, FillMapping, FillStrategy, MaterializeSchema, NumericColumnNameForSchema, RollingAlignment, RollingSchema, SeriesSchema, SmoothAppendSchema, SmoothMethod, SmoothSchema, ValidatedAggregateMap } from '../schema/index.js';
8
8
  import type { ScanStep } from './operators/scan.js';
9
9
  type SequenceLike = Sequence | BoundedSequence;
10
10
  type AlignMethod = 'hold' | 'linear';
11
11
  type AlignSample = 'begin' | 'center' | 'end';
12
+ /**
13
+ * The mapping a partitioned `aggregate` / `rolling` actually runs with
14
+ * ([PND-PARTCOL]). The runtime (`augmentMappingWithPartitionCols`) appends
15
+ * every partition column the user's mapping does not already name as a
16
+ * `'first'` spec, so the collected series carries the partition key. This
17
+ * is the same rule at the type level: keys the user wrote win (kind and
18
+ * all — `host: 'count'` stays an optional number), the rest are added as
19
+ * `'first'` and so keep the source column's kind. `By` defaults to `never`
20
+ * on an untyped view, which leaves the mapping untouched; a widened
21
+ * `string` `By` does too (see the conditional). One knowing lie: when the
22
+ * partition column is a union-typed variable (`c: 'host' | 'region'`), the
23
+ * type names both as `string | undefined` while the runtime carries only
24
+ * the one actually passed — harmless because injected columns are already
25
+ * optional.
26
+ */
27
+ export type WithPartitionColumns<S extends SeriesSchema, Mapping, By extends string> = string extends By ? Mapping : string extends ValueColumnsForSchema<S>[number]['name'] ? Mapping : Mapping & {
28
+ readonly [C in Exclude<By, keyof Mapping>]: 'first';
29
+ };
12
30
  /**
13
31
  * View over a `TimeSeries` that scopes stateful transforms to within
14
32
  * each partition. Created by `TimeSeries.partitionBy(by)`.
@@ -52,10 +70,26 @@ type AlignSample = 'begin' | 'center' | 'end';
52
70
  * );
53
71
  * ```
54
72
  */
55
- export declare class PartitionedTimeSeries<S extends SeriesSchema, K extends string = string> {
73
+ export declare class PartitionedTimeSeries<S extends SeriesSchema, K extends string = string, By extends string = never> {
56
74
  #private;
57
75
  readonly source: TimeSeries<S>;
58
76
  readonly by: ReadonlyArray<keyof EventDataForSchema<S> & string>;
77
+ /**
78
+ * Phantom, erased at runtime. Pins `By` **contravariantly** so a view
79
+ * partitioned by one column cannot be assigned where a view partitioned
80
+ * by another is claimed (`PartitionedTimeSeries<S, K, 'host'>` ←
81
+ * `partitionBy('region')` is an error), while a specialised view still
82
+ * assigns to the legacy `PartitionedTimeSeries<S>` / `<S, K>` shape.
83
+ * Without it `By` only appears inside a conditional in return positions
84
+ * and TypeScript treats it as freely convertible (Codex finding on #724).
85
+ *
86
+ * Spelling note: **`never` (the default) is the "any / unknown column"
87
+ * view**, and it is the top of this pin — every specialised view assigns
88
+ * to it. `string` is the *bottom* (a `string`-typed `By` assigns to any
89
+ * literal), so do not write `PartitionedTimeSeries<S, K, string>` to mean
90
+ * "any column"; the `partitionBy` overloads never produce it.
91
+ */
92
+ readonly __partitionColumns?: (by: By) => void;
59
93
  /**
60
94
  * Declared partition values when `partitionBy(col, { groups })` was
61
95
  * used. When set, `toMap` iterates in declared order (not insertion
@@ -205,12 +239,12 @@ export declare class PartitionedTimeSeries<S extends SeriesSchema, K extends str
205
239
  * reservoir. Safe by construction; no `unsafeGlobal: true` token.
206
240
  * See {@link TimeSeries.sample}.
207
241
  */
208
- sample(strategy: BatchSampleStrategy): PartitionedTimeSeries<S, K>;
242
+ sample(strategy: BatchSampleStrategy): PartitionedTimeSeries<S, K, By>;
209
243
  /** Per-partition `fill`. See {@link TimeSeries.fill}. */
210
244
  fill(strategy: FillStrategy | FillMapping<S>, options?: {
211
245
  limit?: number;
212
246
  maxGap?: DurationInput;
213
- }): PartitionedTimeSeries<S, K>;
247
+ }): PartitionedTimeSeries<S, K, By>;
214
248
  /**
215
249
  * Per-partition `dedupe`. The duplicate key becomes "same partition
216
250
  * columns AND same timestamp" — `partitionBy` provides the partition
@@ -221,13 +255,13 @@ export declare class PartitionedTimeSeries<S extends SeriesSchema, K extends str
221
255
  */
222
256
  dedupe(options?: {
223
257
  keep?: DedupeKeep<S>;
224
- }): PartitionedTimeSeries<S, K>;
258
+ }): PartitionedTimeSeries<S, K, By>;
225
259
  /** Per-partition `align`. See {@link TimeSeries.align}. */
226
260
  align(sequence: SequenceLike, options?: {
227
261
  method?: AlignMethod;
228
262
  sample?: AlignSample;
229
263
  range?: TemporalLike;
230
- }): PartitionedTimeSeries<AlignSchema<S>, K>;
264
+ }): PartitionedTimeSeries<AlignSchema<S>, K, By>;
231
265
  /**
232
266
  * Per-partition `materialize`. See {@link TimeSeries.materialize}.
233
267
  *
@@ -243,18 +277,18 @@ export declare class PartitionedTimeSeries<S extends SeriesSchema, K extends str
243
277
  sample?: AlignSample;
244
278
  select?: 'first' | 'last' | 'nearest';
245
279
  range?: TemporalLike;
246
- }): PartitionedTimeSeries<MaterializeSchema<S>, K>;
280
+ }): PartitionedTimeSeries<MaterializeSchema<S>, K, By>;
247
281
  /** Per-partition `rolling`. See {@link TimeSeries.rolling}. */
248
282
  rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(window: DurationInput, mapping: Mapping, options?: {
249
283
  alignment?: RollingAlignment;
250
284
  minSamples?: number;
251
- }): PartitionedTimeSeries<RollingSchema<S, Mapping>, K>;
285
+ }): PartitionedTimeSeries<RollingSchema<S, WithPartitionColumns<S, Mapping, By>>, K, By>;
252
286
  rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(sequence: SequenceLike, window: DurationInput, mapping: Mapping, options?: {
253
287
  alignment?: RollingAlignment;
254
288
  sample?: AlignSample;
255
289
  range?: TemporalLike;
256
290
  minSamples?: number;
257
- }): PartitionedTimeSeries<AggregateSchema<S, Mapping>, K>;
291
+ }): PartitionedTimeSeries<AggregateSchema<S, WithPartitionColumns<S, Mapping, By>>, K, By>;
258
292
  /** Per-partition `smooth`. See {@link TimeSeries.smooth}. */
259
293
  smooth<const Target extends NumericColumnNameForSchema<S>, const Output extends string | undefined = undefined>(column: Target, method: SmoothMethod, options: {
260
294
  alpha: number;
@@ -267,7 +301,7 @@ export declare class PartitionedTimeSeries<S extends SeriesSchema, K extends str
267
301
  } | {
268
302
  span: number;
269
303
  output?: Output;
270
- }): PartitionedTimeSeries<Output extends string ? SmoothAppendSchema<S, Output> : SmoothSchema<S, Target>>;
304
+ }): PartitionedTimeSeries<Output extends string ? SmoothAppendSchema<S, Output> : SmoothSchema<S, Target>, K, By>;
271
305
  /** Per-partition `baseline`. See {@link TimeSeries.baseline}. */
272
306
  baseline<const Col extends NumericColumnNameForSchema<S>, const AvgName extends string = 'avg', const SdName extends string = 'sd', const UpperName extends string = 'upper', const LowerName extends string = 'lower'>(col: Col, options: {
273
307
  window: DurationInput;
@@ -280,41 +314,41 @@ export declare class PartitionedTimeSeries<S extends SeriesSchema, K extends str
280
314
  upper?: UpperName;
281
315
  lower?: LowerName;
282
316
  };
283
- }): PartitionedTimeSeries<BaselineSchema<S, AvgName, SdName, UpperName, LowerName>>;
317
+ }): PartitionedTimeSeries<BaselineSchema<S, AvgName, SdName, UpperName, LowerName>, K, By>;
284
318
  /** Per-partition `outliers`. See {@link TimeSeries.outliers}. */
285
319
  outliers<const Col extends NumericColumnNameForSchema<S>>(col: Col, options: {
286
320
  window: DurationInput;
287
321
  sigma: number;
288
322
  alignment?: RollingAlignment;
289
323
  minSamples?: number;
290
- }): PartitionedTimeSeries<S, K>;
324
+ }): PartitionedTimeSeries<S, K, By>;
291
325
  /** Per-partition `diff`. See {@link TimeSeries.diff}. */
292
326
  diff<const Target extends NumericColumnNameForSchema<S>>(columns: Target | readonly Target[], options?: {
293
327
  drop?: boolean;
294
- }): PartitionedTimeSeries<DiffSchema<S, Target>, K>;
328
+ }): PartitionedTimeSeries<DiffSchema<S, Target>, K, By>;
295
329
  /** Per-partition `rate`. See {@link TimeSeries.rate}. */
296
330
  rate<const Target extends NumericColumnNameForSchema<S>>(columns: Target | readonly Target[], options?: {
297
331
  drop?: boolean;
298
- }): PartitionedTimeSeries<DiffSchema<S, Target>, K>;
332
+ }): PartitionedTimeSeries<DiffSchema<S, Target>, K, By>;
299
333
  /** Per-partition `pctChange`. See {@link TimeSeries.pctChange}. */
300
334
  pctChange<const Target extends NumericColumnNameForSchema<S>>(columns: Target | readonly Target[], options?: {
301
335
  drop?: boolean;
302
- }): PartitionedTimeSeries<DiffSchema<S, Target>, K>;
336
+ }): PartitionedTimeSeries<DiffSchema<S, Target>, K, By>;
303
337
  /** Per-partition `cumulative`. See {@link TimeSeries.cumulative}. */
304
338
  cumulative<const Targets extends NumericColumnNameForSchema<S>>(spec: {
305
339
  [K in Targets]: 'sum' | 'max' | 'min' | 'count' | ((acc: number, value: number) => number);
306
- }): PartitionedTimeSeries<DiffSchema<S, Targets>, K>;
340
+ }): PartitionedTimeSeries<DiffSchema<S, Targets>, K, By>;
307
341
  /** Per-partition `scan`. See {@link TimeSeries.scan}. */
308
- scan<const Source extends NumericColumnNameForSchema<S>, A>(source: Source, step: ScanStep<A>, init: A): PartitionedTimeSeries<DiffSchema<S, Source>, K>;
342
+ scan<const Source extends NumericColumnNameForSchema<S>, A>(source: Source, step: ScanStep<A>, init: A): PartitionedTimeSeries<DiffSchema<S, Source>, K, By>;
309
343
  scan<const Source extends NumericColumnNameForSchema<S>, const Name extends string, A>(source: Source, step: ScanStep<A>, init: A, options: {
310
344
  output: Name;
311
- }): PartitionedTimeSeries<AppendColumn<S, Name, 'number'>, K>;
345
+ }): PartitionedTimeSeries<AppendColumn<S, Name, 'number'>, K, By>;
312
346
  /** Per-partition `shift`. See {@link TimeSeries.shift}. */
313
- shift<const Target extends NumericColumnNameForSchema<S>>(columns: Target | readonly Target[], n: number): PartitionedTimeSeries<DiffSchema<S, Target>, K>;
347
+ shift<const Target extends NumericColumnNameForSchema<S>>(columns: Target | readonly Target[], n: number): PartitionedTimeSeries<DiffSchema<S, Target>, K, By>;
314
348
  /** Per-partition `aggregate`. See {@link TimeSeries.aggregate}. */
315
349
  aggregate<const Mapping extends ValidatedAggregateMap<S, Mapping>>(sequence: SequenceLike, mapping: Mapping, options?: {
316
350
  range?: TemporalLike;
317
- }): PartitionedTimeSeries<AggregateSchema<S, Mapping>, K>;
351
+ }): PartitionedTimeSeries<AggregateSchema<S, WithPartitionColumns<S, Mapping, By>>, K, By>;
318
352
  }
319
353
  export {};
320
354
  //# sourceMappingURL=partitioned-time-series.d.ts.map
@@ -956,8 +956,8 @@ export declare class TimeSeries<S extends SeriesSchema> {
956
956
  */
957
957
  partitionBy<Col extends keyof EventDataForSchema<S> & string, const Groups extends ReadonlyArray<string>>(by: Col | readonly [Col], options: {
958
958
  groups: Groups;
959
- }): PartitionedTimeSeries<S, Groups[number]>;
960
- partitionBy(by: (keyof EventDataForSchema<S> & string) | ReadonlyArray<keyof EventDataForSchema<S> & string>): PartitionedTimeSeries<S>;
959
+ }): PartitionedTimeSeries<S, Groups[number], Col>;
960
+ partitionBy<const Col extends keyof EventDataForSchema<S> & string>(by: Col | ReadonlyArray<Col>): PartitionedTimeSeries<S, string, Col>;
961
961
  /**
962
962
  * Example: `series.pivotByGroup("host", "cpu")`.
963
963
  * Reshapes long-form data into wide rows. Each distinct value of
@@ -1,5 +1,5 @@
1
- import { Temporal } from '@js-temporal/polyfill';
2
- export type CalendarUnit = 'day' | 'week' | 'month';
1
+ /** A calendar bucket unit. Boundaries are wall-clock in an IANA zone, so a `'day'` is 23, 24 or 25 hours across DST. */
2
+ export type CalendarUnit = 'day' | 'week' | 'month' | 'quarter' | 'year';
3
3
  export type WeekStartsOn = 1 | 2 | 3 | 4 | 5 | 6 | 7;
4
4
  export type TimeZoneOptions = {
5
5
  timeZone?: string;
@@ -10,9 +10,6 @@ export type CalendarOptions = TimeZoneOptions & {
10
10
  export declare function resolveTimeZone(options?: TimeZoneOptions): string;
11
11
  export declare function normalizeWeekStartsOn(value: number | undefined): WeekStartsOn;
12
12
  export declare function parseTimestampString(value: string, options?: TimeZoneOptions): number;
13
- export declare function toPlainDateStart(instantMs: number, timeZone: string, unit: CalendarUnit, weekStartsOn: WeekStartsOn): Temporal.PlainDate;
14
- export declare function plainDateToStart(date: Temporal.PlainDate, timeZone: string): Temporal.ZonedDateTime;
15
- export declare function nextCalendarStart(current: Temporal.PlainDate, unit: CalendarUnit): Temporal.PlainDate;
16
13
  export declare function dayRangeForDate(reference: string, options?: TimeZoneOptions): {
17
14
  start: number;
18
15
  end: number;
@@ -1,4 +1,5 @@
1
1
  import { Temporal } from '@js-temporal/polyfill';
2
+ import { TimeZone } from './time-zone.js';
2
3
  const DATE_ONLY_RE = /^\d{4}-\d{2}-\d{2}$/;
3
4
  const YEAR_MONTH_RE = /^\d{4}-\d{2}$/;
4
5
  const DATE_TIME_LOCAL_RE = /^\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?$/;
@@ -42,51 +43,6 @@ export function parseTimestampString(value, options = {}) {
42
43
  }
43
44
  return Temporal.Instant.from(value).epochMilliseconds;
44
45
  }
45
- export function toPlainDateStart(instantMs, timeZone, unit, weekStartsOn) {
46
- // **Floor to the containing millisecond.** `Temporal.Instant` refuses a
47
- // fractional epoch ms outright — `fromEpochMilliseconds(1577836800000.37)`
48
- // throws `epoch milliseconds must be an integer` — and a fraction is not a
49
- // caller error here. A chart's wheel-zoom derives its view range from pixel
50
- // positions through `xScale.invert()`, so a perfectly ordinary gesture hands
51
- // us `1.7e12 + 0.37`; realizing a calendar sequence over that range then
52
- // crashed the page outright.
53
- //
54
- // Flooring is the only sane reading rather than a papering-over: the epoch
55
- // millisecond *is* the atomic unit of this model, so an over-precise input
56
- // can only mean the millisecond containing it, and calendar boundaries are
57
- // themselves whole milliseconds — so the bucket containing `t` and the one
58
- // containing `t + 0.37` are necessarily the same. Integer inputs are
59
- // untouched.
60
- //
61
- // `Math.floor`, not `Math.trunc`: before 1970 they disagree, and `-5.5` lies
62
- // inside the millisecond spanning `[-6, -5)`, which is `-6`.
63
- const zoned = Temporal.Instant.fromEpochMilliseconds(Math.floor(instantMs)).toZonedDateTimeISO(timeZone);
64
- const date = zoned.toPlainDate();
65
- if (unit === 'day') {
66
- return date;
67
- }
68
- if (unit === 'month') {
69
- return Temporal.PlainDate.from({
70
- year: date.year,
71
- month: date.month,
72
- day: 1,
73
- });
74
- }
75
- const offset = (date.dayOfWeek - weekStartsOn + 7) % 7;
76
- return date.subtract({ days: offset });
77
- }
78
- export function plainDateToStart(date, timeZone) {
79
- return date.toZonedDateTime({ timeZone }).startOfDay();
80
- }
81
- export function nextCalendarStart(current, unit) {
82
- if (unit === 'day') {
83
- return current.add({ days: 1 });
84
- }
85
- if (unit === 'week') {
86
- return current.add({ weeks: 1 });
87
- }
88
- return current.add({ months: 1 });
89
- }
90
46
  export function dayRangeForDate(reference, options = {}) {
91
47
  const timeZone = resolveTimeZone(options);
92
48
  const start = Temporal.PlainDate.from(reference)
@@ -102,12 +58,10 @@ export function calendarRangeForReference(unit, reference, options = {}) {
102
58
  const timeZone = resolveTimeZone(options);
103
59
  const weekStartsOn = normalizeWeekStartsOn(options.weekStartsOn);
104
60
  const referenceMs = parseTimestampString(reference, { timeZone });
105
- const startDate = toPlainDateStart(referenceMs, timeZone, unit, weekStartsOn);
106
- const start = plainDateToStart(startDate, timeZone);
107
- const end = plainDateToStart(nextCalendarStart(startDate, unit), timeZone);
61
+ const zone = TimeZone.of(timeZone);
108
62
  return {
109
- start: start.epochMilliseconds,
110
- end: end.epochMilliseconds,
63
+ start: zone.startOf(unit, referenceMs, { weekStartsOn }),
64
+ end: zone.next(unit, referenceMs, { weekStartsOn }),
111
65
  };
112
66
  }
113
67
  //# sourceMappingURL=calendar.js.map
@@ -6,4 +6,6 @@ export type { DurationInput, DurationUnit } from './duration.js';
6
6
  export { ValidationError } from './errors.js';
7
7
  export type { EventKey, IntervalInput, IntervalValue, TemporalLike, TimeRangeInput, TimestampInput, } from './temporal.js';
8
8
  export type { CalendarOptions, CalendarUnit, TimeZoneOptions, } from './calendar.js';
9
+ export { TimeZone } from './time-zone.js';
10
+ export type { Disambiguation, StartOfOptions, ZonedParts, ZonedPartsInput, } from './time-zone.js';
9
11
  //# sourceMappingURL=index.d.ts.map
@@ -3,4 +3,5 @@ export { Interval } from './interval.js';
3
3
  export { Time } from './time.js';
4
4
  export { TimeRange, toTimeRange } from './time-range.js';
5
5
  export { ValidationError } from './errors.js';
6
+ export { TimeZone } from './time-zone.js';
6
7
  //# sourceMappingURL=index.js.map