pond-ts 0.44.1 → 0.45.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,8 @@ The `@pond-ts` packages — `pond-ts`, `@pond-ts/react`, `@pond-ts/charts`,
8
8
  tag, so this file covers them all. Pre-1.0: minor bumps may include new features
9
9
  and type-level changes; patch bumps are strictly additive.
10
10
 
11
- [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.44.1...HEAD
11
+ [Unreleased]: https://github.com/pjm17971/pond-ts/compare/v0.45.0...HEAD
12
+ [0.45.0]: https://github.com/pjm17971/pond-ts/compare/v0.44.1...v0.45.0
12
13
  [0.44.1]: https://github.com/pjm17971/pond-ts/compare/v0.44.0...v0.44.1
13
14
  [0.44.0]: https://github.com/pjm17971/pond-ts/compare/v0.43.0...v0.44.0
14
15
  [0.43.0]: https://github.com/pjm17971/pond-ts/compare/v0.42.0...v0.43.0
@@ -43,6 +44,98 @@ and type-level changes; patch bumps are strictly additive.
43
44
 
44
45
  ## [Unreleased]
45
46
 
47
+ ## [0.45.0] — 2026-07-14
48
+
49
+ ### Added
50
+
51
+ - **charts:** the time axis now walks a **logical tick ladder** — clock/calendar
52
+ units (1s…30s, 1m…30m, 1H…12H, day / week / month / quarter / year), picking
53
+ the finest grain that fits the width-derived cap — and renders **two-tier labels**:
54
+ a first row at the tick grain (`14:00`, `Feb 02`, `Feb`, `2026`) plus a
55
+ **boundary row** carrying the coarser context the first row omits (the date
56
+ under clock ticks, the year under day/week/month ticks — never a unit the
57
+ first row already shows), shown once under
58
+ the first tick of each new period. Hour anchors are generated in **live**
59
+ trading time, so none lands in a collapsed gap, an early close, or a lunch
60
+ break. `TradingTimeScale` gains `tickBoundaries(count)`; a cramped leading
61
+ partial-period anchor (the `"Jun 23Jul 07"` pile-up) is dropped. Systematic
62
+ story matrix under `Charts/TimeAxisTicks`.
63
+ - **financial:** the rest of the first study batch — `rollingStdev`,
64
+ `rollingMin`, `rollingMax`, `rollingPercentile` (linear interpolation),
65
+ `zScore` (rolling), `envelope` (MA ± percent, `maType` sma/ema), and
66
+ `percentChange` (n-bar rate of change). Same shape as the others — `column` /
67
+ `output` (or `prefix`) on every one, bar-count periods, length-preserving
68
+ warm-up — and each has a fluent method (`bars.zScore({ period: 20 })`) and a
69
+ **pandas oracle** case (values cross-validated bar-for-bar). This completes the
70
+ #449 first batch.
71
+ - **financial:** opt-in **fluent studies** via `import '@pond-ts/financial/fluent'`
72
+ — mounts `sma` / `ema` / `bollinger` as chainable `TimeSeries` methods so
73
+ composition reads like the core operators beside it:
74
+ `bars.sma({ period: 20 }).ema({ period: 12 }).bollinger({ period: 20 })`.
75
+ Opt-in by import (the default entry leaves `TimeSeries` untouched, so a
76
+ non-financial series never sees `.sma()`); the methods are exactly the
77
+ standalone functions bound to `this`, fully typed (each appends its column to
78
+ the schema). Same prototype-augmentation pattern core uses for column methods.
79
+ - **financial:** `@pond-ts/financial` gains its first **studies** — `sma`, `ema`,
80
+ and `bollinger` (Bollinger Bands®) — pure functions that append a column (or a
81
+ `${prefix}Middle/Upper/Lower` family) to a bar series. Every study takes a
82
+ `column` source (default `'close'`) and an `output` name, so a study runs over
83
+ any numeric column including another study's output; periods are **bar counts**
84
+ (on core's count window, gap-correct); warm-up rows are `undefined`
85
+ (length-preserving). Plus the `OhlcvColumns` contract + `DEFAULT_OHLCV`. (Core
86
+ `AppendColumn` is now exported so study return types name their appended
87
+ column.)
88
+ - **core:** `smooth(col, 'ema', …)` gains the financial **`span`** rate
89
+ convention (`α = 2/(span+1)`, e.g. `{ span: 12 }` for a 12-period EMA) as an
90
+ alternative to `alpha` (exactly one required), and a length-preserving
91
+ **`minSamples`** warm-up that emits `undefined` for the first `N` present
92
+ values while **keeping the row count** — mirroring `rolling`'s `minSamples`,
93
+ the one warm-up convention studies use so a smoothed line aligns on its
94
+ source's time axis. (The existing `warmup` option is unchanged — it still
95
+ *drops* the head rows; `minSamples` is the length-preserving counterpart.)
96
+ - **core:** `TimeSeries.rolling` accepts a **count-based** window —
97
+ `rolling({ count: N }, mapping, opts?)` reduces the last / next / centered
98
+ `N` _rows_ (bars) by position instead of a time span. Unlike a duration
99
+ window, an N-bar window stays correct across session gaps (weekends,
100
+ overnight) where `N` bars do not equal `N × barSize` of time — the load-
101
+ bearing primitive for financial N-bar studies (SMA-20, Bollinger-20, rolling
102
+ stdev). Honours `alignment` and `minSamples` exactly like a duration window
103
+ (`minSamples: N` gives the conventional first-`N-1`-rows-`undefined` warmup);
104
+ per-row output only (not supported with a sequence). Same amortized-O(1)-per-
105
+ row sweep as the duration path.
106
+ - **charts:** `<LineChart sessionBreaks>` breaks the line at each trading-axis
107
+ discontinuity (a session / day / lunch close→open) instead of connecting the
108
+ prior close straight to the next open across the collapsed gap. For intraday
109
+ data on a `discontinuities` / `calendar` axis this gives the terminal look —
110
+ the line ends at the close and re-starts at the open, so one session's price
111
+ doesn't visually flow into the next. It's a **scale** break (driven by the
112
+ axis's collapsed gaps), orthogonal to `gaps` (a **data** break, a NaN run) —
113
+ set both independently. Default `false`; a no-op on a continuous axis or a
114
+ provider without `boundaries`. New `Charts/TradingTimeAxis/SessionBreaks`
115
+ story (connected vs broken) + `sessionRuns` / `gappingTicks` helpers.
116
+
117
+ ### Changed
118
+
119
+ - **charts:** a **plain continuous time axis** now runs the same ladder as a
120
+ trading-calendar axis (via an internal gap-free identity provider) instead of
121
+ d3's multi-scale default — so a year of daily data ticks cleanly on month
122
+ starts (`Jul Aug … Jun` + the year underneath) rather than mixed
123
+ `"Jun 23" / "Sep" / "Dec"` labels, and its tick count is now width-derived
124
+ like the trading axis's. Month/quarter-grain anchor labels are now bare
125
+ months (`Feb`, was `Feb 02`) with the year on the boundary row;
126
+ `coarsenCalendar`'s finest granularity is renamed `'session'` → `'day'`.
127
+
128
+ ### Fixed
129
+
130
+ - **core:** `TimeSeries.withColumn` now appends an **optional** column
131
+ (`required: false`) rather than a required one. It already accepted
132
+ `(number | undefined)[]`, but marking the column required meant a later
133
+ strict-intake rebuild (`smooth`, any row reconstruction) threw on the gaps —
134
+ so a column with a warm-up (a rolling study fed into an EMA, e.g.
135
+ `ema(sma(bars, …), …)`) crashed. The appended column's type is now
136
+ `number | undefined`, matching `smooth`'s optional append. (`OptionalNumberColumn`
137
+ is exported for the widened return type.)
138
+
46
139
  ## [0.44.1] — 2026-07-13
47
140
 
48
141
  A `@pond-ts/charts` patch: fixes trading-time axis tick density. `pond-ts`,
@@ -1,4 +1,4 @@
1
- import type { AlignSchema, MaterializeSchema, ArrayAggregateAppendSchema, ArrayAggregateReplaceSchema, ArrayColumnNameForSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, BaselineSchema, AggregateReducer, AggregateSchema, AppendColumn, CollapseSchema, EventDataForSchema, EventForSchema, FirstColKind, IntervalKeyedSchema, JsonRowFormat, JoinManySchema, JoinSchema, JoinType, NumericColumnNameForSchema, NormalizedObjectRow, NormalizedRowForSchema, PivotByGroupSchema, PointRowForSchema, PrefixedJoinManySchema, PrefixedJoinSchema, ReduceResult, RenameMap, ValidatedAggregateMap } from '../schema/index.js';
1
+ import type { AlignSchema, MaterializeSchema, ArrayAggregateAppendSchema, ArrayAggregateReplaceSchema, ArrayColumnNameForSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, BaselineSchema, AggregateReducer, AggregateSchema, AppendColumn, OptionalNumberColumn, CollapseSchema, EventDataForSchema, EventForSchema, FirstColKind, IntervalKeyedSchema, JsonRowFormat, JoinManySchema, JoinSchema, JoinType, NumericColumnNameForSchema, NormalizedObjectRow, NormalizedRowForSchema, PivotByGroupSchema, PointRowForSchema, PrefixedJoinManySchema, PrefixedJoinSchema, ReduceResult, RenameMap, ValidatedAggregateMap } from '../schema/index.js';
2
2
  import type { RenameSchema, RollingAlignment, RollingSchema, ColumnValue, DedupeKeep, DiffSchema, FillMapping, FillStrategy, ScalarKind, ScalarValue, SmoothMethod, SmoothAppendSchema, SmoothSchema, SelectSchema, SeriesSchema, TimeKeyedSchema, TimeSeriesJsonInput, TimeSeriesInput, TimeRangeKeyedSchema, ValueColumnKindForName, ValueColumnNameForSchema, ValueColumnsForSchema, ValueKeyedSchema } from '../schema/index.js';
3
3
  import { type ScanStep } from './operators/scan.js';
4
4
  import { ValueSeries } from './value-series.js';
@@ -1076,10 +1076,22 @@ export declare class TimeSeries<S extends SeriesSchema> {
1076
1076
  * Computes sequence-driven rolling aggregations and returns an interval-keyed series on the
1077
1077
  * supplied grid.
1078
1078
  *
1079
+ * Example: `series.rolling({ count: 20 }, { close: "avg" }, { minSamples: 20 })`.
1080
+ * A **count-based** window of the last `N` *rows* (bars), keyed on row
1081
+ * position not time. `{ count: N }` in place of a duration reduces, for each
1082
+ * row, the `N` rows in its index window — so an "N-bar" study (SMA-20,
1083
+ * Bollinger-20, rolling stdev) stays correct across session gaps (weekends,
1084
+ * overnight) where a duration window silently spans the wrong number of bars.
1085
+ * Honours `alignment` and `minSamples` exactly like a duration window
1086
+ * (`minSamples: N` gives the conventional warmup — the first `N-1` rows emit
1087
+ * `undefined`). Per-row output only; not supported with a sequence.
1088
+ *
1079
1089
  * Rolling windows are anchored either at each event's `begin()` time or at the sample point of
1080
- * each sequence bucket. Membership is determined from source event `begin()` times.
1090
+ * each sequence bucket. Membership is determined from source event `begin()` times (a duration
1091
+ * window) or from row position (a `{ count }` window).
1081
1092
  *
1082
- * Supported alignments:
1093
+ * Supported alignments (a `{ count: N }` window applies the same shape in row-index space, with
1094
+ * a centered window biased one row toward `leading` when `N` is even):
1083
1095
  * - `"trailing"`: `(t - window, t]`
1084
1096
  * - `"leading"`: `[t, t + window)`
1085
1097
  * - `"centered"`: `[t - window/2, t + window/2)`
@@ -1114,7 +1126,9 @@ export declare class TimeSeries<S extends SeriesSchema> {
1114
1126
  * therefore read as distinct values here. Scalar columns
1115
1127
  * (number / string / boolean) are unaffected (value semantics).
1116
1128
  */
1117
- rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(window: DurationInput, mapping: Mapping, options?: {
1129
+ rolling<const Mapping extends ValidatedAggregateMap<S, Mapping>>(window: DurationInput | {
1130
+ count: number;
1131
+ }, mapping: Mapping, options?: {
1118
1132
  alignment?: RollingAlignment;
1119
1133
  minSamples?: number;
1120
1134
  }): TimeSeries<RollingSchema<S, Mapping>>;
@@ -1129,6 +1143,16 @@ export declare class TimeSeries<S extends SeriesSchema> {
1129
1143
  * Applies a smoothing transform to one numeric payload column while preserving the original key
1130
1144
  * type, key values, and all non-target payload fields.
1131
1145
  *
1146
+ * **EMA rate + warm-up.** Give the rate as either `alpha` (0,1] **or** the
1147
+ * financial `span` convention (`α = 2/(span+1)` — a `span`-period EMA, e.g.
1148
+ * `{ span: 12 }`), exactly one of the two. Two warm-up options, both optional:
1149
+ * `warmup: N` **drops** the first N rows (length-changing — hides the noisy
1150
+ * initial convergence); `minSamples: N` **keeps** the rows but emits
1151
+ * `undefined` until N present values have been consumed (length-preserving —
1152
+ * mirrors {@link TimeSeries.rolling}'s `minSamples`, the convention studies
1153
+ * use so a smoothed line aligns on its source's time axis). The EMA converges
1154
+ * on the real values underneath either way.
1155
+ *
1132
1156
  * Example: `series.smooth("value", "movingAverage", { window: "5m", alignment: "centered", output: "valueAvg" })`.
1133
1157
  * Computes a moving average over the selected numeric column using anchor points derived from
1134
1158
  * event keys. `Time` keys use their timestamp. `TimeRange` and `Interval` keys use the midpoint
@@ -1162,6 +1186,12 @@ export declare class TimeSeries<S extends SeriesSchema> {
1162
1186
  smooth<const Target extends NumericColumnNameForSchema<S>, const Output extends string | undefined = undefined>(column: Target, method: SmoothMethod, options: {
1163
1187
  alpha: number;
1164
1188
  warmup?: number;
1189
+ minSamples?: number;
1190
+ output?: Output;
1191
+ } | {
1192
+ span: number;
1193
+ warmup?: number;
1194
+ minSamples?: number;
1165
1195
  output?: Output;
1166
1196
  } | {
1167
1197
  window: DurationInput;
@@ -1336,7 +1366,7 @@ export declare class TimeSeries<S extends SeriesSchema> {
1336
1366
  * (`fromTrustedColumns`, skipping the finite scan) is a deferred sibling for
1337
1367
  * when a perf-critical consumer earns it.
1338
1368
  */
1339
- withColumn<const Name extends string>(name: Name, values: ReadonlyArray<number | undefined> | Float64Array): TimeSeries<AppendColumn<S, Name, 'number'>>;
1369
+ withColumn<const Name extends string>(name: Name, values: ReadonlyArray<number | undefined> | Float64Array): TimeSeries<readonly [S[0], ...ValueColumnsForSchema<S>, OptionalNumberColumn<Name>]>;
1340
1370
  /** Example: `series.collapse(["in", "out"], "avg", fn)`. Collapses selected payload fields into a single derived field across each event in the series. */
1341
1371
  collapse<const Keys extends readonly (keyof EventDataForSchema<S>)[], Name extends string, R extends ScalarValue>(keys: Keys, output: Name, reducer: (values: Pick<EventDataForSchema<S>, Keys[number]>) => R): TimeSeries<CollapseSchema<S, Keys[number] & string, Name, R>>;
1342
1372
  collapse<const Keys extends readonly (keyof EventDataForSchema<S>)[], Name extends string, R extends ScalarValue>(keys: Keys, output: Name, reducer: (values: Pick<EventDataForSchema<S>, Keys[number]>) => R, options: {
@@ -2114,7 +2114,21 @@ export class TimeSeries {
2114
2114
  kind: spec.kind,
2115
2115
  required: false,
2116
2116
  }));
2117
- const windowMs = parseDuration(window);
2117
+ // A `{ count: N }` window reduces the last/next/centered `N` rows by index
2118
+ // rather than a time span — the count-based (N-bar) window. Detected here so
2119
+ // the duration is never parsed for it.
2120
+ const countWindow = typeof window === 'object' && window !== null && 'count' in window
2121
+ ? window.count
2122
+ : null;
2123
+ if (countWindow !== null) {
2124
+ if (!Number.isInteger(countWindow) || countWindow < 1) {
2125
+ throw new TypeError('rolling { count } must be a positive integer (row count)');
2126
+ }
2127
+ if (sequence) {
2128
+ throw new TypeError('rolling count windows ({ count }) are not supported with a sequence');
2129
+ }
2130
+ }
2131
+ const windowMs = countWindow === null ? parseDuration(window) : 0;
2118
2132
  const alignment = options.alignment ?? 'trailing';
2119
2133
  // Default 0 disables the gate — preserves prior behavior where
2120
2134
  // empty windows still invoke the reducer (custom reducers may
@@ -2229,7 +2243,46 @@ export class TimeSeries {
2229
2243
  }
2230
2244
  }
2231
2245
  };
2232
- if (alignment === 'trailing') {
2246
+ if (countWindow !== null) {
2247
+ // Count-based window: for each row, the `N` rows in its index window (not
2248
+ // a time span). `lo`/`hi` are both monotonic non-decreasing in the row
2249
+ // index for every alignment, so the same reducer add/remove sweep stays
2250
+ // amortized O(1) per row — each row enters and leaves the window once.
2251
+ // Rows are the unit (no equal-timestamp grouping — `N` counts bars), and a
2252
+ // centered even `N` biases one row toward the leading side.
2253
+ const n = countWindow;
2254
+ const leftSpan = Math.floor((n - 1) / 2);
2255
+ const rightSpan = n - 1 - leftSpan;
2256
+ for (let index = 0; index < rowCount; index++) {
2257
+ let lo;
2258
+ let hi;
2259
+ if (alignment === 'trailing') {
2260
+ lo = index - n + 1 < 0 ? 0 : index - n + 1;
2261
+ hi = index;
2262
+ }
2263
+ else if (alignment === 'leading') {
2264
+ lo = index;
2265
+ hi = index + n - 1 < rowCount ? index + n - 1 : rowCount - 1;
2266
+ }
2267
+ else {
2268
+ lo = index - leftSpan < 0 ? 0 : index - leftSpan;
2269
+ hi = index + rightSpan < rowCount ? index + rightSpan : rowCount - 1;
2270
+ }
2271
+ while (windowEnd <= hi) {
2272
+ addEvent(windowEnd);
2273
+ windowEnd += 1;
2274
+ }
2275
+ while (windowStart < lo) {
2276
+ removeEvent(windowStart);
2277
+ windowStart += 1;
2278
+ }
2279
+ const aggregated = snapshotWindow();
2280
+ for (let c = 0; c < outValues.length; c++) {
2281
+ outValues[c][index] = aggregated[c];
2282
+ }
2283
+ }
2284
+ }
2285
+ else if (alignment === 'trailing') {
2233
2286
  for (let groupStart = 0; groupStart < rowCount;) {
2234
2287
  const anchor = beginTimes[groupStart];
2235
2288
  let groupEnd = groupStart + 1;
@@ -2323,6 +2376,16 @@ export class TimeSeries {
2323
2376
  * Applies a smoothing transform to one numeric payload column while preserving the original key
2324
2377
  * type, key values, and all non-target payload fields.
2325
2378
  *
2379
+ * **EMA rate + warm-up.** Give the rate as either `alpha` (0,1] **or** the
2380
+ * financial `span` convention (`α = 2/(span+1)` — a `span`-period EMA, e.g.
2381
+ * `{ span: 12 }`), exactly one of the two. Two warm-up options, both optional:
2382
+ * `warmup: N` **drops** the first N rows (length-changing — hides the noisy
2383
+ * initial convergence); `minSamples: N` **keeps** the rows but emits
2384
+ * `undefined` until N present values have been consumed (length-preserving —
2385
+ * mirrors {@link TimeSeries.rolling}'s `minSamples`, the convention studies
2386
+ * use so a smoothed line aligns on its source's time axis). The EMA converges
2387
+ * on the real values underneath either way.
2388
+ *
2326
2389
  * Example: `series.smooth("value", "movingAverage", { window: "5m", alignment: "centered", output: "valueAvg" })`.
2327
2390
  * Computes a moving average over the selected numeric column using anchor points derived from
2328
2391
  * event keys. `Time` keys use their timestamp. `TimeRange` and `Interval` keys use the midpoint
@@ -2364,27 +2427,56 @@ export class TimeSeries {
2364
2427
  return typeof raw === 'number' ? raw : undefined;
2365
2428
  });
2366
2429
  if (method === 'ema') {
2367
- if (!('alpha' in options)) {
2368
- throw new TypeError('ema smoothing requires an alpha option');
2430
+ // Rate parameter: `alpha` directly, or the financial `span` convention
2431
+ // = 2/(span+1) a `span`-period EMA), exactly one of the two.
2432
+ const hasAlpha = 'alpha' in options && options.alpha !== undefined;
2433
+ const hasSpan = 'span' in options && options.span !== undefined;
2434
+ if (hasAlpha === hasSpan) {
2435
+ throw new TypeError('ema smoothing requires exactly one of alpha or span');
2436
+ }
2437
+ let alpha;
2438
+ if (hasSpan) {
2439
+ const span = options.span;
2440
+ if (typeof span !== 'number' || !Number.isFinite(span) || span < 1) {
2441
+ throw new TypeError('ema smoothing requires span to be a finite number >= 1');
2442
+ }
2443
+ alpha = 2 / (span + 1);
2369
2444
  }
2370
- const alpha = options.alpha;
2371
- if (typeof alpha !== 'number' ||
2372
- !Number.isFinite(alpha) ||
2373
- alpha <= 0 ||
2374
- alpha > 1) {
2375
- throw new TypeError('ema smoothing requires alpha to be a finite number in the range (0, 1]');
2445
+ else {
2446
+ alpha = options.alpha;
2447
+ if (typeof alpha !== 'number' ||
2448
+ !Number.isFinite(alpha) ||
2449
+ alpha <= 0 ||
2450
+ alpha > 1) {
2451
+ throw new TypeError('ema smoothing requires alpha to be a finite number in the range (0, 1]');
2452
+ }
2376
2453
  }
2377
2454
  // Optional warm-up: drop the first N output rows to hide the
2378
2455
  // noisy initial convergence of the EMA. The smoother still
2379
2456
  // processes those events so `previous` is correctly warmed up
2380
- // by the time we keep a row.
2457
+ // by the time we keep a row. **Length-changing** (rows are removed) —
2458
+ // for the length-preserving warmup that studies want (align on a shared
2459
+ // time axis), use `minSamples` instead.
2381
2460
  const warmup = 'warmup' in options && options.warmup !== undefined
2382
2461
  ? options.warmup
2383
2462
  : 0;
2384
2463
  if (!Number.isInteger(warmup) || warmup < 0 || !Number.isFinite(warmup)) {
2385
2464
  throw new TypeError('ema smoothing requires warmup to be a non-negative integer');
2386
2465
  }
2466
+ // Length-preserving warm-up: emit `undefined` until `minSamples` present
2467
+ // source values have been consumed, keeping the row (mirrors `rolling`'s
2468
+ // `minSamples`). The EMA still converges on the real values underneath —
2469
+ // only the emitted value is masked — so the kept tail is unchanged.
2470
+ const minSamples = 'minSamples' in options && options.minSamples !== undefined
2471
+ ? options.minSamples
2472
+ : 0;
2473
+ if (!Number.isInteger(minSamples) ||
2474
+ minSamples < 0 ||
2475
+ !Number.isFinite(minSamples)) {
2476
+ throw new TypeError('ema smoothing requires minSamples to be a non-negative integer');
2477
+ }
2387
2478
  let previous;
2479
+ let seen = 0;
2388
2480
  const resultRows = this.events.map((event) => {
2389
2481
  const raw = event.get(column);
2390
2482
  const smoothed = typeof raw !== 'number'
@@ -2394,10 +2486,14 @@ export class TimeSeries {
2394
2486
  : alpha * raw + (1 - alpha) * previous;
2395
2487
  if (smoothed !== undefined) {
2396
2488
  previous = smoothed;
2489
+ seen += 1;
2397
2490
  }
2491
+ // Mask the emitted value during the length-preserving warm-up, but keep
2492
+ // `previous`/`seen` advancing on the real value above.
2493
+ const emitted = seen < minSamples ? undefined : smoothed;
2398
2494
  const nextEvent = output === undefined
2399
- ? event.set(column, smoothed)
2400
- : event.merge({ [output]: smoothed });
2495
+ ? event.set(column, emitted)
2496
+ : event.merge({ [output]: emitted });
2401
2497
  return Object.freeze([
2402
2498
  nextEvent.key(),
2403
2499
  ...resultSchema
@@ -2435,7 +2531,7 @@ export class TimeSeries {
2435
2531
  // `missing: 'skip'` — a cell whose own value is missing stays missing
2436
2532
  // (don't fit a fabricated value across the hole from its present
2437
2533
  // neighbours). Default `'bridge'` is the prior behaviour (fit everywhere).
2438
- const skipMissing = options.missing === 'skip';
2534
+ const skipMissing = 'missing' in options && options.missing === 'skip';
2439
2535
  const resultRows = this.events.map((event, index) => {
2440
2536
  const smoothed = skipMissing && sourceValues[index] === undefined
2441
2537
  ? undefined
@@ -2967,9 +3063,15 @@ export class TimeSeries {
2967
3063
  assertColumnValuesMatchKind('number', values, `withColumn '${String(name)}'`);
2968
3064
  const column = columnFromValuesByKind('number', values);
2969
3065
  const reshaped = withColumnAppended(this.#store.store, name, column);
3066
+ // The appended column is **optional** (`required: false`): `values` may
3067
+ // carry `undefined` (a missing cell), so the column can have gaps. Marking
3068
+ // it required would make a later strict-intake rebuild (`smooth`, a row
3069
+ // reconstruction) reject those gaps — breaking composition on a column with
3070
+ // a warm-up (e.g. a rolling study fed into an EMA). Matches `smooth`'s
3071
+ // `OptionalNumberColumn` append.
2970
3072
  const resultSchema = Object.freeze([
2971
3073
  ...this.schema,
2972
- { name, kind: 'number' },
3074
+ { name, kind: 'number', required: false },
2973
3075
  ]);
2974
3076
  return TimeSeries.#fromTrustedStore(this.name, resultSchema, reshaped);
2975
3077
  }
package/dist/index.d.ts CHANGED
@@ -30,7 +30,7 @@ export { ArrayColumn } from './columnar/array-column.js';
30
30
  export { ChunkedArrayColumn, ChunkedBooleanColumn, ChunkedFloat64Column, ChunkedStringColumn, } from './columnar/chunked-column.js';
31
31
  export { type IntervalLabelKind, type KeyColumn, IntervalKeyColumn, TimeKeyColumn, TimeRangeKeyColumn, ValueKeyColumn, } from './columnar/key-column.js';
32
32
  export { type ValidityBitmap } from './columnar/validity.js';
33
- export type { AlignSchema, ArrayColumnNameForSchema, BaselineSchema, AggregateFunction, AggregateReducer, AggregateOutputMap, AggregateOutputSpec, AggregateMap, AggregateSchema, ValidatedAggregateMap, ColumnDef, CollapseData, ArrayAggregateAppendSchema, ArrayAggregateKind, ArrayAggregateReplaceSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, CollapseSchema, DedupeKeep, EventDataForSchema, EventForSchema, EventKeyForKind, FillMapping, FillStrategy, MaterializeSchema, EventKeyForSchema, FirstColKind, FirstColumn, IntervalKeyedSchema, JsonIntervalInput, JsonObjectRowForSchema, JsonRowFormat, JsonRowForSchema, JsonTimeRangeInput, JsonTimestampInput, JsonValueForKind, RollingAlignment, RollingSchema, JoinConflictMode, JoinManySchema, PrefixedJoinManySchema, PrefixedJoinSchema, LiveSource, JoinType, JoinSchema, NormalizedRowForSchema, NormalizedObjectRowForSchema, NormalizedObjectRow, NormalizedValueForKind, ReduceResult, RenameData, RenameMap, RenameSchema, RekeySchema, RowForSchema, ArrayValue, ColumnValue, ScalarKind, ScalarValue, CustomAggregateReducer, DiffSchema, NumericColumnNameForSchema, SmoothMethod, SmoothAppendSchema, SmoothSchema, SelectData, SelectSchema, SeriesSchema, TimeKeyedSchema, TimeSeriesInput, TimeSeriesJsonInput, TimeRangeKeyedSchema, ValueColumnsForSchema, ValueColumn, ValueForKind, ValueFirstColumn, ValueKeyedSchema, ValueSeriesColumnName, ValueSeriesSchema, ValueSeriesValueColumns, } from './schema/index.js';
33
+ export type { AlignSchema, ArrayColumnNameForSchema, BaselineSchema, AggregateFunction, AggregateReducer, AggregateOutputMap, AggregateOutputSpec, AggregateMap, AggregateSchema, ValidatedAggregateMap, ColumnDef, CollapseData, ArrayAggregateAppendSchema, ArrayAggregateKind, ArrayAggregateReplaceSchema, ArrayExplodeAppendSchema, ArrayExplodeReplaceSchema, CollapseSchema, DedupeKeep, EventDataForSchema, EventForSchema, EventKeyForKind, FillMapping, FillStrategy, MaterializeSchema, EventKeyForSchema, FirstColKind, FirstColumn, IntervalKeyedSchema, JsonIntervalInput, JsonObjectRowForSchema, JsonRowFormat, JsonRowForSchema, JsonTimeRangeInput, JsonTimestampInput, JsonValueForKind, RollingAlignment, RollingSchema, JoinConflictMode, JoinManySchema, PrefixedJoinManySchema, PrefixedJoinSchema, LiveSource, JoinType, JoinSchema, NormalizedRowForSchema, NormalizedObjectRowForSchema, NormalizedObjectRow, NormalizedValueForKind, ReduceResult, RenameData, RenameMap, RenameSchema, RekeySchema, RowForSchema, AppendColumn, OptionalNumberColumn, ArrayValue, ColumnValue, ScalarKind, ScalarValue, CustomAggregateReducer, DiffSchema, NumericColumnNameForSchema, SmoothMethod, SmoothAppendSchema, SmoothSchema, SelectData, SelectSchema, SeriesSchema, TimeKeyedSchema, TimeSeriesInput, TimeSeriesJsonInput, TimeRangeKeyedSchema, ValueColumnsForSchema, ValueColumn, ValueForKind, ValueFirstColumn, ValueKeyedSchema, ValueSeriesColumnName, ValueSeriesSchema, ValueSeriesValueColumns, } from './schema/index.js';
34
34
  export type { CalendarOptions, CalendarUnit, TimeZoneOptions, } from './core/calendar.js';
35
35
  export type { EventKey, IntervalInput, IntervalValue, TemporalLike, TimeRangeInput, TimestampInput, } from './core/temporal.js';
36
36
  export type { DurationInput } from './core/duration.js';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pond-ts",
3
- "version": "0.44.1",
3
+ "version": "0.45.0",
4
4
  "description": "TypeScript-first time series primitives",
5
5
  "license": "MIT",
6
6
  "repository": {