@pond-ts/charts 0.57.0 → 0.58.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.
Files changed (73) hide show
  1. package/CHANGELOG.md +1070 -1
  2. package/dist/AreaChart.d.ts +12 -1
  3. package/dist/AreaChart.js +131 -13
  4. package/dist/BarChart.js +184 -30
  5. package/dist/BarList.d.ts +85 -5
  6. package/dist/BarList.js +25 -4
  7. package/dist/BoxList.d.ts +70 -3
  8. package/dist/BoxList.js +21 -7
  9. package/dist/BoxPlot.d.ts +2 -1
  10. package/dist/BoxPlot.js +101 -9
  11. package/dist/Candlestick.d.ts +13 -1
  12. package/dist/Candlestick.js +89 -3
  13. package/dist/ChartContainer.d.ts +36 -48
  14. package/dist/ChartContainer.js +465 -59
  15. package/dist/ChartRow.d.ts +9 -2
  16. package/dist/ChartRow.js +86 -12
  17. package/dist/HeatMap.d.ts +176 -0
  18. package/dist/HeatMap.js +344 -0
  19. package/dist/Layers.d.ts +5 -1
  20. package/dist/Layers.js +1014 -253
  21. package/dist/Legend.js +8 -4
  22. package/dist/LineChart.d.ts +18 -1
  23. package/dist/LineChart.js +165 -4
  24. package/dist/ListTable.d.ts +30 -3
  25. package/dist/ListTable.js +381 -23
  26. package/dist/ScatterChart.d.ts +3 -2
  27. package/dist/ScatterChart.js +68 -4
  28. package/dist/XAxis.js +40 -22
  29. package/dist/area.d.ts +34 -1
  30. package/dist/area.js +88 -1
  31. package/dist/bars.d.ts +57 -3
  32. package/dist/bars.js +237 -26
  33. package/dist/box.d.ts +2 -2
  34. package/dist/box.js +158 -40
  35. package/dist/brush.d.ts +142 -0
  36. package/dist/brush.js +179 -0
  37. package/dist/child-index.d.ts +27 -0
  38. package/dist/child-index.js +57 -0
  39. package/dist/context.d.ts +859 -33
  40. package/dist/cursors.d.ts +161 -0
  41. package/dist/cursors.js +503 -0
  42. package/dist/decimate.d.ts +78 -1
  43. package/dist/decimate.js +157 -0
  44. package/dist/heat.d.ts +163 -0
  45. package/dist/heat.js +659 -0
  46. package/dist/index.d.ts +11 -2
  47. package/dist/index.js +22 -0
  48. package/dist/line.d.ts +137 -0
  49. package/dist/line.js +328 -0
  50. package/dist/ohlc.d.ts +16 -1
  51. package/dist/ohlc.js +93 -4
  52. package/dist/scatter.d.ts +17 -9
  53. package/dist/scatter.js +221 -33
  54. package/dist/select.d.ts +13 -5
  55. package/dist/select.js +14 -6
  56. package/dist/selection-fixtures.d.ts +174 -0
  57. package/dist/selection-fixtures.js +569 -0
  58. package/dist/selection-stories.d.ts +73 -0
  59. package/dist/selection-stories.js +301 -0
  60. package/dist/selectors.d.ts +316 -0
  61. package/dist/selectors.js +391 -0
  62. package/dist/span.d.ts +122 -0
  63. package/dist/span.js +203 -0
  64. package/dist/sweep.d.ts +154 -0
  65. package/dist/sweep.js +282 -0
  66. package/dist/theme.d.ts +456 -5
  67. package/dist/theme.js +217 -41
  68. package/dist/tracker.d.ts +6 -0
  69. package/dist/tracker.js +6 -0
  70. package/dist/tradingAxis.fixture.d.ts +78 -0
  71. package/dist/tradingAxis.fixture.js +215 -0
  72. package/dist/useChartLegend.js +18 -3
  73. package/package.json +3 -3
package/dist/theme.js CHANGED
@@ -1,78 +1,199 @@
1
1
  /**
2
- * The neutral default theme. `default` / `primary` match the M1 `LineChart`
3
- * colour (`#2563eb`) so adopting the theme channel doesn't shift existing
4
- * renders. `primary` / `secondary` / `context` are a built-in generic role
5
- * vocabulary; an unrecognised (e.g. domain-specific) identifier falls back to
6
- * `default`.
2
+ * The neutral default theme. The shared data hue is a cerulean (`#0284c7`)
3
+ * across `line` / `band` / `area` / `scatter` / `box` / candle-rising, chosen
4
+ * to clear the bar palette's *selection* blue (`#3F5BE0`) the original M1
5
+ * royal blue (`#2563eb`) sat ~ΔE 5 from it, so a line drawn over bars read as
6
+ * nearly the selection colour. `primary` / `secondary` / `context` are a
7
+ * built-in generic role vocabulary; an unrecognised (e.g. domain-specific)
8
+ * identifier falls back to `default`.
9
+ *
10
+ * **Spreading this inherits every slot you don't override** — including colours
11
+ * for layers you haven't added yet, so a `{ ...defaultTheme, bar: … }` theme
12
+ * paints this blue the first time someone drops in a `<LineChart>`. That is the
13
+ * intended workflow, not a trap: take the defaults, change the one or two things
14
+ * that are yours. A design system that must own *every* colour should assert on
15
+ * that in its own test rather than catch it in review — walk the resolved theme
16
+ * for values outside your palette ([PND-THEMEBASE]).
17
+ *
18
+ * **The `bar` slot is the exception to "one blue".** Bars carry an interaction
19
+ * state (rest / hover / selected / dimmed), and encoding four states as four
20
+ * shades of one hue is unreadable — so `bar.default` runs its own
21
+ * **interaction-state palette**: teal at rest, blue when selected, brighter
22
+ * teal on hover. See the comment on that slot, and `brush` for the matching
23
+ * drag band.
7
24
  */
8
25
  export const defaultTheme = {
9
26
  line: {
10
- default: { color: '#2563eb', width: 1.5 },
11
- primary: { color: '#2563eb', width: 1.5 },
12
- secondary: { color: '#e8836b', width: 1.5 },
13
- context: { color: '#5eb5a6', width: 1.5 },
27
+ // **Trace interaction state is weight, plus a hue for a swept WINDOW
28
+ // only** see `LineStyle.selectedWidth` / `.spanColor`. A whole-series
29
+ // selection thickens and keeps its colour, because colour is which series
30
+ // it is; a window inside one series can take the selection blue, because
31
+ // there identity is not in question. `spanColor` is `bar.highlight`, so a
32
+ // swept region on a trace reads as the same act as the band that made it.
33
+ default: {
34
+ color: '#0284c7',
35
+ width: 1.5,
36
+ selectedWidth: 3,
37
+ dimmedOpacity: 0.32,
38
+ spanColor: '#3F5BE0',
39
+ },
40
+ primary: {
41
+ color: '#0284c7',
42
+ width: 1.5,
43
+ selectedWidth: 3,
44
+ dimmedOpacity: 0.32,
45
+ spanColor: '#3F5BE0',
46
+ },
47
+ secondary: {
48
+ color: '#e8836b',
49
+ width: 1.5,
50
+ selectedWidth: 3,
51
+ dimmedOpacity: 0.32,
52
+ spanColor: '#3F5BE0',
53
+ },
54
+ context: {
55
+ color: '#5eb5a6',
56
+ width: 1.5,
57
+ selectedWidth: 3,
58
+ dimmedOpacity: 0.32,
59
+ spanColor: '#3F5BE0',
60
+ },
14
61
  },
15
62
  band: {
16
- default: { fill: '#2563eb', opacity: 0.15 },
17
- outer: { fill: '#2563eb', opacity: 0.1 },
18
- inner: { fill: '#2563eb', opacity: 0.2 },
63
+ default: { fill: '#0284c7', opacity: 0.15 },
64
+ outer: { fill: '#0284c7', opacity: 0.1 },
65
+ inner: { fill: '#0284c7', opacity: 0.2 },
19
66
  },
20
67
  area: {
21
68
  // Outline at the line colour; graded fill from it. `in`/`out` are the
22
69
  // above/below-axis roles (esnet traffic), composed as two layers.
70
+ // Trace interaction state — see `AreaStyle.selectedWidth`. `spanColor` is
71
+ // `bar.highlight`, so a swept region reads as the same act as the band.
23
72
  default: {
24
- color: '#2563eb',
73
+ color: '#0284c7',
25
74
  width: 1.5,
26
- fill: '#2563eb',
75
+ fill: '#0284c7',
27
76
  fillOpacity: 0.3,
77
+ selectedWidth: 3,
78
+ selectedFillOpacity: 0.55,
79
+ dimmedOpacity: 0.32,
80
+ spanColor: '#3F5BE0',
28
81
  },
29
- in: { color: '#2563eb', width: 1.5, fill: '#2563eb', fillOpacity: 0.3 },
82
+ in: { color: '#0284c7', width: 1.5, fill: '#0284c7', fillOpacity: 0.3 },
30
83
  out: { color: '#e8836b', width: 1.5, fill: '#e8836b', fillOpacity: 0.3 },
31
84
  },
32
85
  scatter: {
33
- // Brand blue fill with a white ring (legible on a busy plot); the selected
34
- // point gets a darker, wider ring. `primary`/`secondary` mirror the line
35
- // roles so a scatter overlaid on a line can share its identity.
86
+ // A 9px teal point with a white ring (legible on a busy plot), and the
87
+ // shared `states` ladder over it. `primary`/`secondary` keep the LINE
88
+ // roles' hues that identity is the whole reason they exist, so a
89
+ // scatter overlaid on a line still reads as the same series — and take
90
+ // the same states with their own hue brightened for hover.
91
+ //
92
+ // The default role's rest is `#2A9D8F`, the bar's resting teal, and NOT
93
+ // the old cerulean `#0284c7`: blue has to mean *committed*, and a
94
+ // cerulean point going to selection blue is barely a change. Same rule
95
+ // the bar palette reached, for the third time.
36
96
  default: {
37
- color: '#2563eb',
38
- radius: 4,
97
+ color: '#2A9D8F',
98
+ radius: 4.5,
39
99
  outline: '#ffffff',
40
100
  outlineWidth: 1,
41
101
  selectedOutline: '#1e293b',
42
102
  selectedWidth: 2,
43
103
  label: '#334155',
104
+ states: {
105
+ hover: '#4FD0BE',
106
+ hoverRadius: 5.5,
107
+ selected: '#3F5BE0', // the shared selection blue
108
+ halo: '#ffffff',
109
+ haloWidth: 2,
110
+ dimmedRadius: 2.5,
111
+ dimmedOpacity: 0.34,
112
+ },
44
113
  },
45
114
  primary: {
46
- color: '#2563eb',
47
- radius: 4,
115
+ color: '#0284c7',
116
+ radius: 4.5,
48
117
  outline: '#ffffff',
49
118
  outlineWidth: 1,
50
119
  selectedOutline: '#1e293b',
51
120
  selectedWidth: 2,
52
121
  label: '#334155',
122
+ states: {
123
+ hover: '#38bdf8', // the primary cerulean, brightened
124
+ hoverRadius: 5.5,
125
+ selected: '#3F5BE0', // the shared selection blue
126
+ halo: '#ffffff',
127
+ haloWidth: 2,
128
+ dimmedRadius: 2.5,
129
+ dimmedOpacity: 0.34,
130
+ },
53
131
  },
54
132
  secondary: {
55
133
  color: '#e8836b',
56
- radius: 4,
134
+ radius: 4.5,
57
135
  outline: '#ffffff',
58
136
  outlineWidth: 1,
59
137
  selectedOutline: '#1e293b',
60
138
  selectedWidth: 2,
61
139
  label: '#334155',
140
+ states: {
141
+ hover: '#f5a991', // the secondary coral, brightened
142
+ hoverRadius: 5.5,
143
+ selected: '#3F5BE0', // the shared selection blue
144
+ halo: '#ffffff',
145
+ haloWidth: 2,
146
+ dimmedRadius: 2.5,
147
+ dimmedOpacity: 0.34,
148
+ },
149
+ },
150
+ },
151
+ heat: {
152
+ // A cell's colour is its value, so every state here is chrome around the
153
+ // cell (or one uniform transform over all of them) — see `HeatStates`.
154
+ default: {
155
+ veil: 'rgba(255,255,255,0.62)',
156
+ // White outside, dark teal inside: one of the pair reads wherever on
157
+ // the ramp the cell happens to sit.
158
+ hoverRing: ['#ffffff', '#12564E'],
159
+ ringWidth: 2,
160
+ // The shared selection blue — a selected region beside a selected bar
161
+ // reads as one act.
162
+ perimeter: '#3F5BE0',
163
+ perimeterWidth: 2,
62
164
  },
63
165
  },
64
166
  box: {
65
- // The blue brand box: a translucent fill outlined in the line colour, a
167
+ // The cerulean data box: a translucent fill outlined in the line colour, a
66
168
  // bolder median, and matching whiskers.
67
169
  default: {
68
- fill: '#2563eb',
170
+ fill: '#0284c7',
69
171
  fillOpacity: 0.3,
70
- stroke: '#2563eb',
71
- strokeWidth: 1.5,
72
- median: '#1e3a8a',
172
+ stroke: '#0284c7',
173
+ // 1px at rest, 1.5 when selected (`selectedStrokeWidth`) — the design's
174
+ // hairline rule. Previously a flat 1.5, which left no headroom for a
175
+ // weight change to mean anything.
176
+ strokeWidth: 1,
177
+ median: '#075985',
73
178
  medianWidth: 2,
74
- whisker: '#aabee9',
179
+ whisker: '#a3cde5',
75
180
  whiskerWidth: 1,
181
+ // The tint ladder. Lightness spacing is held across all three ladders,
182
+ // so the quantile read (outer < inner < stroke < median) never changes —
183
+ // only where the ladder sits does. It follows the bar palette's rule
184
+ // exactly: hover brightens within teal, blue means committed. Step 2 of
185
+ // each ladder IS the matching bar token (`bar.hover` / `bar.highlight`),
186
+ // so a live box beside a live bar reads as one act.
187
+ states: {
188
+ rest: ['#BFE3DE', '#7FC8BF', '#2A9D8F', '#1F7A6F'],
189
+ // Hover brightens *within teal* — blue stays reserved for a committed
190
+ // selection, the same rule `bar.hover` follows (and step 2 is exactly
191
+ // `bar.default.hover`, so a hovered box and a hovered bar match).
192
+ hover: ['#D6F1EC', '#9CDBD1', '#3FBFAE', '#2A9D8F'],
193
+ selected: ['#C0CAF6', '#8095EA', '#3F5BE0', '#1C2E9E'],
194
+ dimmedOpacity: 0.32,
195
+ },
196
+ selectedStrokeWidth: 1.5,
76
197
  },
77
198
  // The warm accent box — the second series of a paired distribution (an
78
199
  // in/out traffic list), mirroring `bar.secondary` / `line.secondary`.
@@ -89,31 +210,62 @@ export const defaultTheme = {
89
210
  },
90
211
  candle: {
91
212
  // Neutral / unbranded up-down pair — *not* market green/red (a consumer
92
- // supplies that via cssVarTheme). Rising reuses the brand blue; falling the
93
- // warm secondary accent — distinguishable at a glance on the light ground.
213
+ // supplies that via cssVarTheme). Rising reuses the data cerulean; falling
214
+ // the warm secondary accent — distinguishable at a glance on light ground.
94
215
  default: {
95
- rising: { body: '#2563eb', wick: '#1e3a8a' },
216
+ rising: { body: '#0284c7', wick: '#075985' },
96
217
  falling: { body: '#e8836b', wick: '#b4442a' },
97
218
  neutral: { body: '#94a3b8', wick: '#64748b' },
98
219
  bodyWidth: 0.7,
99
220
  wickWidth: 1,
221
+ // No state colour at all — the direction hue owns that channel. A live
222
+ // candle simply grows (its body stroked in its own colour, lines
223
+ // heavier); a selection additionally recedes the field to `.32`, the
224
+ // same step every other mark uses.
225
+ dimmedOpacity: 0.32,
226
+ liveWickWidth: 1.5,
100
227
  },
101
228
  },
102
229
  bar: {
103
- // Flat blue fill; the selected bar brightens + outlines. `secondary` reuses
104
- // the line's warm accent for a second series.
230
+ // The **interaction-state palette**: state is a *hue* difference, not a
231
+ // shade of one colour. Rest is teal; a committed selection is blue; hover
232
+ // is a *brighter teal* — deliberately not blue, because blue is reserved
233
+ // for "committed". Out-of-selection bars recede to the same teal at 0.32.
234
+ // The shared drag band (`brush` below) is the selection blue at 7%, so the
235
+ // live region reads as the same act as the selection it is about to make.
236
+ // `secondary` reuses the line's warm accent for a second series.
105
237
  default: {
106
- fill: '#2563eb',
107
- opacity: 0.85,
108
- highlight: '#1d4ed8',
238
+ fill: '#2A9D8F', // rest — teal, full opacity
239
+ opacity: 1,
240
+ highlight: '#3F5BE0', // selected — blue; committed state
241
+ hover: '#3FBFAE', // hover — brighter teal, never blue
242
+ dimmed: 'rgba(42,157,143,0.32)', // outside an active selection
109
243
  gap: 1,
110
244
  minWidth: 1,
111
245
  outlineWidth: 1.5,
112
- // The default threshold ladder: the bar's own blue as the in-range band,
246
+ // The default threshold ladder: the bar's own teal as the in-range band,
113
247
  // then amber, then red. Three entries serves the common two-threshold
114
248
  // ok/warning/alarm ladder out of the box; a longer `thresholds` needs a
115
249
  // longer ladder from the theme or `bandColors`.
116
- bands: ['#2563eb', '#e8a13c', '#d64545'],
250
+ bands: ['#2A9D8F', '#e8a13c', '#d64545'],
251
+ // The **stack group ramp**, first group first (so a vertical stack reads
252
+ // teal at the bottom up to terracotta). Four muted hues at similar
253
+ // lightness, so no segment shouts over its neighbours the way a
254
+ // saturation ladder would — the ramp says "different group", not
255
+ // "more important". It starts on a teal near the resting `fill` so a
256
+ // two-group stack still looks like the rest of the palette.
257
+ groups: ['#4c9e8f', '#5379be', '#e2a54a', '#b5604e'],
258
+ // Each ramp entry desaturated (~×0.18) and lightened toward the ground,
259
+ // keeping its hue and its *relative* lightness — so a receded bin still
260
+ // reads as four bands rather than one grey block, and the amber stays
261
+ // the lightest of them as it is in the vivid ramp.
262
+ groupsDimmed: ['#c7cecd', '#ced1d6', '#dcd8d2', '#d3cdcc'],
263
+ // Each ramp entry brightened by the same move `fill` → `hover` makes:
264
+ // hue held, lightness +0.11, saturation left alone. So a hovered segment
265
+ // still says which group it is — the thing a single hover colour cannot
266
+ // do on a stack, and under a <MultiSelector> (block-scoped hover) it is
267
+ // the whole bin that would otherwise go one flat colour.
268
+ groupsHover: ['#6bb8a9', '#7c99cd', '#eabd7a', '#c68476'],
117
269
  },
118
270
  secondary: {
119
271
  fill: '#e8836b',
@@ -140,15 +292,39 @@ export const defaultTheme = {
140
292
  size: 11,
141
293
  },
142
294
  cursor: '#64748b',
295
+ // The drag band in the bar palette's selection blue at 7%, edged at 1px —
296
+ // the live region reads as the selection it is about to commit.
297
+ brush: { fill: 'rgba(63,91,224,0.07)', edge: 'rgba(63,91,224,0.45)' },
143
298
  chip: { background: '#ffffff' },
144
299
  gap: { connectorOpacity: 0.5 },
145
- // Teal marks register — distinct from the blue data, reads on the light ground.
300
+ // Burnt-amber marks register — a deliberately warm outlier against every
301
+ // data hue, so a placed mark never reads as data (the same rule the docs
302
+ // brand's `vizMark` encodes). Verified against the whole palette: its
303
+ // nearest data neighbours are the threshold-ladder red `#d64545` (ΔE2000
304
+ // ≈ 18) and the warm secondary accent `#e8836b` (ΔE2000 ≈ 21); everything
305
+ // else — bar teal, hover teal, selection blue, the data cerulean — clears
306
+ // ΔE2000 40+. (The previous turquoise `#0d9488` sat ~ΔE 4 from the bar
307
+ // palette's resting teal `#2A9D8F` — indistinguishable at a glance.)
146
308
  annotation: {
147
- color: '#0d9488',
309
+ color: '#b45309',
310
+ // EXPERIMENTAL — a lighter orange for a swept window's edge rules, which
311
+ // mark a candidate range rather than a committed mark. See `spanEdge`.
312
+ spanEdge: '#f0b26b',
148
313
  fillOpacity: 0.1,
149
314
  depth: [1, 0.7, 0.4],
150
315
  },
151
316
  // The in-chart series key: chip-white card, gridline border, axis-label text.
317
+ // The row-chart register — see `ChartTheme.list`. The two band tints are
318
+ // the only genuinely new values: the rails are the same teal/blue pair the
319
+ // bar's interaction-state palette already uses (`hover` / `highlight`), and
320
+ // the marker ink is the near-black the annotation register reads against.
321
+ list: {
322
+ hoverBand: '#F6F6F3', // warm neutral — a lift, not a hue
323
+ hoverRail: '#4FD0BE', // brighter teal, never blue
324
+ selectedBand: '#EEF1FD', // the selection blue, washed
325
+ selectedRail: '#3F5BE0', // = bar.default.highlight — committed state
326
+ markerInk: '#1C1C1A', // reserved from the selection hue (see the doc)
327
+ },
152
328
  legend: {
153
329
  background: '#ffffff',
154
330
  border: '#e2e8f0',
package/dist/tracker.d.ts CHANGED
@@ -48,6 +48,12 @@ export declare const DEFAULT_CURSOR_MODE: CursorMode;
48
48
  * presets — `line` is line-only, `point` / `inline` / `flag` are dot-based with
49
49
  * no line, `none` draws nothing. `flag` raises a staff from each point to a
50
50
  * value flag stacked near the top of the row (drawn in `Layers`).
51
+ *
52
+ * **Superseded by the mounted cursor presets** (`cursors.tsx` — each mode's
53
+ * drawing now lives in its component's registered spec): `Layers` no longer
54
+ * reads this. Kept, with its tests, for the life of the `cursor` string-prop
55
+ * deprecation window as the pinned record of what each mode drew; delete both
56
+ * when the modes go.
51
57
  */
52
58
  export declare function cursorParts(mode: CursorMode): {
53
59
  readonly line: boolean;
package/dist/tracker.js CHANGED
@@ -79,6 +79,12 @@ export const DEFAULT_CURSOR_MODE = 'line';
79
79
  * presets — `line` is line-only, `point` / `inline` / `flag` are dot-based with
80
80
  * no line, `none` draws nothing. `flag` raises a staff from each point to a
81
81
  * value flag stacked near the top of the row (drawn in `Layers`).
82
+ *
83
+ * **Superseded by the mounted cursor presets** (`cursors.tsx` — each mode's
84
+ * drawing now lives in its component's registered spec): `Layers` no longer
85
+ * reads this. Kept, with its tests, for the life of the `cursor` string-prop
86
+ * deprecation window as the pinned record of what each mode drew; delete both
87
+ * when the modes go.
82
88
  */
83
89
  export function cursorParts(mode) {
84
90
  const base = { line: false, dots: false, chip: 'none', band: false };
@@ -0,0 +1,78 @@
1
+ import { BoundedSequence, TimeSeries } from 'pond-ts';
2
+ import type { SeriesSchema } from 'pond-ts';
3
+ import type { DiscontinuityProvider, TradingCalendarLike } from './tradingTimeScale.js';
4
+ /**
5
+ * Shared fixtures for the trading-time-axis stories (reference + interaction).
6
+ * A session calendar + **inline** discontinuity provider (in real use you'd pass
7
+ * `calendar.discontinuities()` from `@pond-ts/financial`); the axis only needs
8
+ * the structural provider, so charts stays decoupled.
9
+ */
10
+ export declare const H = 3600000;
11
+ export declare const DAY = 86400000;
12
+ export declare const MIN = 60000;
13
+ export declare const MON: number;
14
+ export declare const WIDTH = 720;
15
+ export interface Session {
16
+ date: string;
17
+ open: number;
18
+ close: number;
19
+ }
20
+ /** `count` weekday sessions from the anchor Monday (09:30–16:00 UTC), skipping
21
+ * weekends and an optional holiday date — a stand-in for a real calendar. */
22
+ export declare function weekdaySessions(count: number, holiday?: string): Session[];
23
+ /** An early-close (half-day) variant of the last session. */
24
+ export declare function withHalfDay(sessions: Session[], closeHour?: number): Session[];
25
+ /** A proportional trading-time provider over the sessions' `[open, close)` spans. */
26
+ export declare function provider(sessions: Session[]): DiscontinuityProvider;
27
+ /** A **uniform** trading-time provider — each session is one equal-width slot
28
+ * regardless of duration (a half-day is as wide as a full day). */
29
+ export declare function uniformProvider(sessions: Session[]): DiscontinuityProvider;
30
+ /** A structural {@link TradingCalendarLike} — the shape `@pond-ts/financial`'s
31
+ * `TradingCalendar` satisfies — that the container's `spacing` prop drives. */
32
+ export declare function calendarOf(sessions: Session[]): TradingCalendarLike;
33
+ /** One interval per session — the daily-bar grid. */
34
+ export declare function sessionSeq(sessions: Session[]): BoundedSequence;
35
+ /** Intraday `period`-ms bars within each session (never crossing a boundary). */
36
+ export declare function barSeq(sessions: Session[], periodMs: number): BoundedSequence;
37
+ export declare const tickSchema: readonly [{
38
+ readonly name: "time";
39
+ readonly kind: "time";
40
+ }, {
41
+ readonly name: "price";
42
+ readonly kind: "number";
43
+ }];
44
+ /** Deterministic in-session price ticks (a smooth random-ish walk on sines). */
45
+ export declare function ticks(sessions: Session[], stepMs: number): TimeSeries<typeof tickSchema>;
46
+ /** Intraday ticks with an **overnight gap**: each session opens at a distinct
47
+ * base level (a gap up from the prior close) while the intraday wiggle stays
48
+ * smooth. So a session boundary is a real price jump — the connected line shows
49
+ * a near-vertical bridge across the collapsed gap, `sessionBreaks` a clean
50
+ * pen-up. (Plain {@link ticks} walks continuously across sessions, so close ≈
51
+ * next open and the break is invisible — this is the fixture that shows it.) */
52
+ export declare function gappingTicks(sessions: Session[], stepMs: number): TimeSeries<typeof tickSchema>;
53
+ export declare const OHLC: {
54
+ readonly open: {
55
+ readonly from: "price";
56
+ readonly using: "first";
57
+ };
58
+ readonly high: {
59
+ readonly from: "price";
60
+ readonly using: "max";
61
+ };
62
+ readonly low: {
63
+ readonly from: "price";
64
+ readonly using: "min";
65
+ };
66
+ readonly close: {
67
+ readonly from: "price";
68
+ readonly using: "last";
69
+ };
70
+ };
71
+ /** Interval-keyed OHLC candles over a bucket sequence (immune to point-key slot
72
+ * widths). Return type widened to the general `SeriesSchema`: the inferred
73
+ * aggregate schema references an internal pond-ts module and can't be named
74
+ * (TS2742), so an explicit annotation is required. `<Candlestick>` reads the
75
+ * o/h/l/c columns by runtime name, so the widening is invisible to callers. */
76
+ export declare function candles(sessions: Session[], seq: BoundedSequence, stepMs: number): TimeSeries<SeriesSchema>;
77
+ export declare function rangeOf(sessions: Session[]): [number, number];
78
+ //# sourceMappingURL=tradingAxis.fixture.d.ts.map
@@ -0,0 +1,215 @@
1
+ import { BoundedSequence, Interval, TimeSeries } from 'pond-ts';
2
+ /**
3
+ * Shared fixtures for the trading-time-axis stories (reference + interaction).
4
+ * A session calendar + **inline** discontinuity provider (in real use you'd pass
5
+ * `calendar.discontinuities()` from `@pond-ts/financial`); the axis only needs
6
+ * the structural provider, so charts stays decoupled.
7
+ */
8
+ export const H = 3_600_000;
9
+ export const DAY = 86_400_000;
10
+ export const MIN = 60_000;
11
+ export const MON = Date.UTC(2026, 0, 5); // a Monday
12
+ export const WIDTH = 720;
13
+ /** `count` weekday sessions from the anchor Monday (09:30–16:00 UTC), skipping
14
+ * weekends and an optional holiday date — a stand-in for a real calendar. */
15
+ export function weekdaySessions(count, holiday) {
16
+ const out = [];
17
+ for (let dayIdx = 0; out.length < count; dayIdx++) {
18
+ const dayStart = MON + dayIdx * DAY;
19
+ const dow = new Date(dayStart).getUTCDay();
20
+ if (dow === 0 || dow === 6)
21
+ continue; // weekend
22
+ const date = new Date(dayStart).toISOString().slice(0, 10);
23
+ if (date === holiday)
24
+ continue;
25
+ out.push({ date, open: dayStart + 9.5 * H, close: dayStart + 16 * H });
26
+ }
27
+ return out;
28
+ }
29
+ /** An early-close (half-day) variant of the last session. */
30
+ export function withHalfDay(sessions, closeHour = 13) {
31
+ const last = sessions[sessions.length - 1];
32
+ const dayStart = last.open - 9.5 * H;
33
+ return [
34
+ ...sessions.slice(0, -1),
35
+ { ...last, close: dayStart + closeHour * H },
36
+ ];
37
+ }
38
+ /** A proportional trading-time provider over the sessions' `[open, close)` spans. */
39
+ export function provider(sessions) {
40
+ const segs = sessions.map((s) => [s.open, s.close]);
41
+ const cum = [0];
42
+ for (const [a, b] of segs)
43
+ cum.push(cum[cum.length - 1] + (b - a));
44
+ const total = cum[cum.length - 1];
45
+ const liveMs = (t) => {
46
+ if (t <= segs[0][0])
47
+ return 0;
48
+ if (t >= segs[segs.length - 1][1])
49
+ return total;
50
+ for (let i = 0; i < segs.length; i++) {
51
+ const [a, b] = segs[i];
52
+ if (t < a)
53
+ return cum[i];
54
+ if (t < b)
55
+ return cum[i] + (t - a);
56
+ }
57
+ return total;
58
+ };
59
+ const inst = (L) => {
60
+ if (L <= 0)
61
+ return segs[0][0];
62
+ if (L >= total)
63
+ return segs[segs.length - 1][1];
64
+ for (let i = 0; i < segs.length; i++) {
65
+ if (L < cum[i + 1])
66
+ return segs[i][0] + (L - cum[i]);
67
+ }
68
+ return segs[segs.length - 1][1];
69
+ };
70
+ const self = {
71
+ distance: (a, b) => liveMs(b) - liveMs(a),
72
+ offset: (v, amt) => inst(liveMs(v) + amt),
73
+ clampUp: (t) => t,
74
+ clampDown: (t) => t,
75
+ copy: () => self,
76
+ boundaries: (from, to) => {
77
+ const out = [];
78
+ for (let i = 1; i < segs.length; i++) {
79
+ const start = segs[i][0];
80
+ if (start > segs[i - 1][1] && start > from && start < to)
81
+ out.push(start);
82
+ }
83
+ return out;
84
+ },
85
+ };
86
+ return self;
87
+ }
88
+ /** A **uniform** trading-time provider — each session is one equal-width slot
89
+ * regardless of duration (a half-day is as wide as a full day). */
90
+ export function uniformProvider(sessions) {
91
+ const segs = sessions.map((s) => [s.open, s.close]);
92
+ const n = segs.length;
93
+ const uCoord = (t) => {
94
+ if (t <= segs[0][0])
95
+ return 0;
96
+ if (t >= segs[n - 1][1])
97
+ return n;
98
+ for (let i = 0; i < n; i++) {
99
+ const [a, b] = segs[i];
100
+ if (t < a)
101
+ return i; // in a gap before session i → the boundary
102
+ if (t < b)
103
+ return i + (t - a) / (b - a); // linear within the session
104
+ }
105
+ return n;
106
+ };
107
+ const inst = (u) => {
108
+ if (u <= 0)
109
+ return segs[0][0];
110
+ if (u >= n)
111
+ return segs[n - 1][1];
112
+ const i = Math.floor(u);
113
+ const [a, b] = segs[i];
114
+ return a + (u - i) * (b - a);
115
+ };
116
+ const self = {
117
+ distance: (x, y) => uCoord(y) - uCoord(x),
118
+ offset: (v, amt) => inst(uCoord(v) + amt),
119
+ clampUp: (t) => t,
120
+ clampDown: (t) => t,
121
+ copy: () => self,
122
+ boundaries: (from, to) => {
123
+ const out = [];
124
+ for (let i = 1; i < n; i++) {
125
+ const start = segs[i][0];
126
+ if (start > segs[i - 1][1] && start > from && start < to)
127
+ out.push(start);
128
+ }
129
+ return out;
130
+ },
131
+ };
132
+ return self;
133
+ }
134
+ /** A structural {@link TradingCalendarLike} — the shape `@pond-ts/financial`'s
135
+ * `TradingCalendar` satisfies — that the container's `spacing` prop drives. */
136
+ export function calendarOf(sessions) {
137
+ return {
138
+ discontinuities: (options) => options?.spacing === 'uniform'
139
+ ? uniformProvider(sessions)
140
+ : provider(sessions),
141
+ };
142
+ }
143
+ /** One interval per session — the daily-bar grid. */
144
+ export function sessionSeq(sessions) {
145
+ return new BoundedSequence(sessions.map((s) => new Interval({ value: s.date, start: s.open, end: s.close })));
146
+ }
147
+ /** Intraday `period`-ms bars within each session (never crossing a boundary). */
148
+ export function barSeq(sessions, periodMs) {
149
+ const ivals = [];
150
+ for (const s of sessions) {
151
+ for (let t = s.open; t < s.close; t += periodMs) {
152
+ ivals.push(new Interval({
153
+ value: t,
154
+ start: t,
155
+ end: Math.min(t + periodMs, s.close),
156
+ }));
157
+ }
158
+ }
159
+ return new BoundedSequence(ivals);
160
+ }
161
+ export const tickSchema = [
162
+ { name: 'time', kind: 'time' },
163
+ { name: 'price', kind: 'number' },
164
+ ];
165
+ /** Deterministic in-session price ticks (a smooth random-ish walk on sines). */
166
+ export function ticks(sessions, stepMs) {
167
+ const rows = [];
168
+ let i = 0;
169
+ for (const s of sessions) {
170
+ for (let t = s.open; t < s.close; t += stepMs, i++) {
171
+ const price = 100 +
172
+ 9 * Math.sin(i / 22) +
173
+ 3 * Math.sin(i / 4.5) +
174
+ 1.4 * Math.sin(i / 1.3);
175
+ rows.push([t, price]);
176
+ }
177
+ }
178
+ return new TimeSeries({ name: 'ticks', schema: tickSchema, rows });
179
+ }
180
+ /** Intraday ticks with an **overnight gap**: each session opens at a distinct
181
+ * base level (a gap up from the prior close) while the intraday wiggle stays
182
+ * smooth. So a session boundary is a real price jump — the connected line shows
183
+ * a near-vertical bridge across the collapsed gap, `sessionBreaks` a clean
184
+ * pen-up. (Plain {@link ticks} walks continuously across sessions, so close ≈
185
+ * next open and the break is invisible — this is the fixture that shows it.) */
186
+ export function gappingTicks(sessions, stepMs) {
187
+ const rows = [];
188
+ let i = 0;
189
+ sessions.forEach((s, si) => {
190
+ const base = 100 + si * 6; // each session gaps ~6 above the last
191
+ for (let t = s.open; t < s.close; t += stepMs, i++) {
192
+ const price = base + 4 * Math.sin(i / 18) + 1.5 * Math.sin(i / 3.5);
193
+ rows.push([t, price]);
194
+ }
195
+ });
196
+ return new TimeSeries({ name: 'ticks', schema: tickSchema, rows });
197
+ }
198
+ export const OHLC = {
199
+ open: { from: 'price', using: 'first' },
200
+ high: { from: 'price', using: 'max' },
201
+ low: { from: 'price', using: 'min' },
202
+ close: { from: 'price', using: 'last' },
203
+ };
204
+ /** Interval-keyed OHLC candles over a bucket sequence (immune to point-key slot
205
+ * widths). Return type widened to the general `SeriesSchema`: the inferred
206
+ * aggregate schema references an internal pond-ts module and can't be named
207
+ * (TS2742), so an explicit annotation is required. `<Candlestick>` reads the
208
+ * o/h/l/c columns by runtime name, so the widening is invisible to callers. */
209
+ export function candles(sessions, seq, stepMs) {
210
+ return ticks(sessions, stepMs).aggregate(seq, OHLC);
211
+ }
212
+ export function rangeOf(sessions) {
213
+ return [sessions[0].open, sessions[sessions.length - 1].close];
214
+ }
215
+ //# sourceMappingURL=tradingAxis.fixture.js.map