@wick-charts/vue 0.4.1 → 0.5.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 (146) hide show
  1. package/README.md +5 -3
  2. package/dist/BarSeries.vue.js +44 -0
  3. package/dist/BarSeries.vue2.js +4 -0
  4. package/dist/CandlestickSeries.vue.js +45 -0
  5. package/dist/CandlestickSeries.vue2.js +4 -0
  6. package/dist/ChartContainer.vue.js +168 -0
  7. package/dist/ChartContainer.vue2.js +4 -0
  8. package/dist/LineSeries.vue.js +44 -0
  9. package/dist/LineSeries.vue2.js +4 -0
  10. package/dist/PieSeries.vue.js +34 -0
  11. package/dist/PieSeries.vue2.js +4 -0
  12. package/dist/composables.js +44 -0
  13. package/dist/context.js +25 -0
  14. package/dist/core/dist/animation/animator.js +72 -0
  15. package/dist/core/dist/animation/config.js +145 -0
  16. package/dist/core/dist/animation/easing.js +4 -0
  17. package/dist/core/dist/animation/scalar-spring.js +57 -0
  18. package/dist/core/dist/animation/spring.js +11 -0
  19. package/dist/core/dist/animation/time.js +19 -0
  20. package/dist/core/dist/animation/viewport-engine.js +174 -0
  21. package/dist/core/dist/animation/visible-range-spring.js +64 -0
  22. package/dist/core/dist/animation/y-range-hermite.js +72 -0
  23. package/dist/core/dist/animation/y-range-snap.js +31 -0
  24. package/dist/core/dist/animation/y-range-spring.js +59 -0
  25. package/dist/core/dist/axis/dom-labels.js +60 -0
  26. package/dist/core/dist/canvas-manager.js +109 -0
  27. package/dist/core/dist/chart/edge-indicators.js +26 -0
  28. package/dist/core/dist/chart/fit-to-data.js +9 -0
  29. package/dist/core/dist/chart/last-value.js +31 -0
  30. package/dist/core/dist/chart/options.js +35 -0
  31. package/dist/core/dist/chart/pan-zoom-math.js +66 -0
  32. package/dist/core/dist/chart/streaming-cadence.js +70 -0
  33. package/dist/core/dist/chart/streaming-target.js +22 -0
  34. package/dist/core/dist/chart/viewport-padding.js +6 -0
  35. package/dist/core/dist/chart/y-target.js +24 -0
  36. package/dist/core/dist/chart.js +1495 -0
  37. package/dist/core/dist/components/crosshair.js +9 -0
  38. package/dist/core/dist/components/edge-indicator.js +36 -0
  39. package/dist/core/dist/components/grid.js +22 -0
  40. package/dist/core/dist/components/marker.js +41 -0
  41. package/dist/core/dist/components/reference-line.js +48 -0
  42. package/dist/core/dist/components/time-region.js +35 -0
  43. package/dist/core/dist/data/decimation.js +54 -0
  44. package/dist/core/dist/data/store.js +76 -0
  45. package/dist/core/dist/data/sync.js +47 -0
  46. package/dist/core/dist/events.js +19 -0
  47. package/dist/core/dist/interactions/handler.js +84 -0
  48. package/dist/core/dist/interactions/pan.js +26 -0
  49. package/dist/core/dist/interactions/zoom.js +21 -0
  50. package/dist/core/dist/navigator/controller.js +374 -0
  51. package/dist/core/dist/navigator/decimate.js +44 -0
  52. package/dist/core/dist/navigator/interactions.js +28 -0
  53. package/dist/core/dist/navigator/render.js +68 -0
  54. package/dist/core/dist/perf/counting-context.js +19 -0
  55. package/dist/core/dist/perf/perf-hud.js +453 -0
  56. package/dist/core/dist/perf/perf-monitor.js +132 -0
  57. package/dist/core/dist/render-scheduler.js +31 -0
  58. package/dist/core/dist/scales/tick-tracker.js +110 -0
  59. package/dist/core/dist/scales/x-scale.js +145 -0
  60. package/dist/core/dist/scales/y-scale.js +156 -0
  61. package/dist/core/dist/series/bar.js +341 -0
  62. package/dist/core/dist/series/base-multi-layer.js +438 -0
  63. package/dist/core/dist/series/candlestick.js +481 -0
  64. package/dist/core/dist/series/definition.js +11 -0
  65. package/dist/core/dist/series/line.js +446 -0
  66. package/dist/core/dist/series/painters/builtins.js +36 -0
  67. package/dist/core/dist/series/painters/canvas-path.js +79 -0
  68. package/dist/core/dist/series/painters/resolve.js +32 -0
  69. package/dist/core/dist/series/pie.js +545 -0
  70. package/dist/core/dist/series/register-builtins.js +11 -0
  71. package/dist/core/dist/series/stack-math.js +17 -0
  72. package/dist/core/dist/snapshots.js +107 -0
  73. package/dist/core/dist/theme/create.js +176 -0
  74. package/dist/core/dist/theme/resolve.js +14 -0
  75. package/dist/core/dist/theme/themes/andromeda.js +33 -0
  76. package/dist/core/dist/theme/themes/ayu-mirage.js +33 -0
  77. package/dist/core/dist/theme/themes/catppuccin.js +33 -0
  78. package/dist/core/dist/theme/themes/dracula.js +35 -0
  79. package/dist/core/dist/theme/themes/github-light.js +34 -0
  80. package/dist/core/dist/theme/themes/gruvbox.js +33 -0
  81. package/dist/core/dist/theme/themes/handwritten.js +101 -0
  82. package/dist/core/dist/theme/themes/high-contrast.js +33 -0
  83. package/dist/core/dist/theme/themes/lavender-mist.js +33 -0
  84. package/dist/core/dist/theme/themes/light-pink.js +34 -0
  85. package/dist/core/dist/theme/themes/material-palenight.js +33 -0
  86. package/dist/core/dist/theme/themes/minimal-light.js +34 -0
  87. package/dist/core/dist/theme/themes/mint-breeze.js +33 -0
  88. package/dist/core/dist/theme/themes/monokai-pro.js +33 -0
  89. package/dist/core/dist/theme/themes/night-owl.js +35 -0
  90. package/dist/core/dist/theme/themes/one-dark-pro.js +33 -0
  91. package/dist/core/dist/theme/themes/panda.js +33 -0
  92. package/dist/core/dist/theme/themes/peach-cream.js +33 -0
  93. package/dist/core/dist/theme/themes/quiet-light.js +37 -0
  94. package/dist/core/dist/theme/themes/rose-pine-dawn.js +33 -0
  95. package/dist/core/dist/theme/themes/sand-dune.js +33 -0
  96. package/dist/core/dist/theme/themes/solarized-light.js +34 -0
  97. package/dist/core/dist/tooltip-position.js +10 -0
  98. package/dist/core/dist/utils/color.js +37 -0
  99. package/dist/core/dist/utils/format.js +29 -0
  100. package/dist/core/dist/utils/math.js +26 -0
  101. package/dist/core/dist/utils/poisoned-data-reporter.js +15 -0
  102. package/dist/core/dist/utils/time.js +41 -0
  103. package/dist/index.d.ts +1096 -63
  104. package/dist/index.js +142 -7754
  105. package/dist/ui/Crosshair.vue.js +53 -0
  106. package/dist/ui/Crosshair.vue2.js +4 -0
  107. package/dist/ui/InfoBar.vue.js +143 -0
  108. package/dist/ui/InfoBar.vue2.js +4 -0
  109. package/dist/ui/Legend.vue.js +212 -0
  110. package/dist/ui/Legend.vue2.js +4 -0
  111. package/dist/ui/Marker.vue.js +47 -0
  112. package/dist/ui/Marker.vue2.js +4 -0
  113. package/dist/ui/Navigator.vue.js +58 -0
  114. package/dist/ui/Navigator.vue2.js +4 -0
  115. package/dist/ui/NumberFlow.vue.js +69 -0
  116. package/dist/ui/NumberFlow.vue2.js +4 -0
  117. package/dist/ui/PieLegend.vue.js +132 -0
  118. package/dist/ui/PieLegend.vue2.js +4 -0
  119. package/dist/ui/PieTooltip.vue.js +135 -0
  120. package/dist/ui/PieTooltip.vue2.js +4 -0
  121. package/dist/ui/ReferenceLine.vue.js +44 -0
  122. package/dist/ui/ReferenceLine.vue2.js +4 -0
  123. package/dist/ui/TimeAxis.vue.js +43 -0
  124. package/dist/ui/TimeAxis.vue2.js +4 -0
  125. package/dist/ui/TimeRegion.vue.js +41 -0
  126. package/dist/ui/TimeRegion.vue2.js +4 -0
  127. package/dist/ui/Title.vue.js +54 -0
  128. package/dist/ui/Title.vue2.js +4 -0
  129. package/dist/ui/Tooltip.vue.js +195 -0
  130. package/dist/ui/Tooltip.vue2.js +4 -0
  131. package/dist/ui/YAxis.vue.js +42 -0
  132. package/dist/ui/YAxis.vue2.js +4 -0
  133. package/dist/ui/YLabel.vue.js +105 -0
  134. package/dist/ui/YLabel.vue2.js +4 -0
  135. package/package.json +2 -2
  136. package/src/BarSeries.vue +12 -13
  137. package/src/CandlestickSeries.vue +17 -6
  138. package/src/LineSeries.vue +12 -13
  139. package/src/PieSeries.vue +2 -1
  140. package/src/composables.ts +12 -2
  141. package/src/index.ts +42 -0
  142. package/src/ui/Crosshair.vue +28 -3
  143. package/src/ui/Legend.vue +1 -2
  144. package/src/ui/Marker.vue +62 -0
  145. package/src/ui/ReferenceLine.vue +58 -0
  146. package/src/ui/TimeRegion.vue +54 -0
@@ -0,0 +1,1495 @@
1
+ import { AnimationConfig as z, DEFAULT_LINE_PULSE as H } from "./animation/config.js";
2
+ import { createViewportEngine as N } from "./animation/viewport-engine.js";
3
+ import { CanvasManager as W } from "./canvas-manager.js";
4
+ import { drawEdgeIndicators as E, resolveEdgeBoundary as $ } from "./chart/edge-indicators.js";
5
+ import { computeFitToData as O } from "./chart/fit-to-data.js";
6
+ import { getLastValue as B, getPreviousClose as F, getStackedLastValue as Y } from "./chart/last-value.js";
7
+ import { resolveMaxVisibleBars as X, resolvePadding as D, resolvePerfOptions as _, isSameHorizontalPadding as C } from "./chart/options.js";
8
+ import { computePan as U, computeZoom as Z } from "./chart/pan-zoom-math.js";
9
+ import { StreamingCadence as G } from "./chart/streaming-cadence.js";
10
+ import { computeStreamingTarget as j } from "./chart/streaming-target.js";
11
+ import { resolvePaddingTime as T } from "./chart/viewport-padding.js";
12
+ import { computeTargetYRange as q, resolveBound as R } from "./chart/y-target.js";
13
+ import { renderCrosshair as J } from "./components/crosshair.js";
14
+ import { renderGrid as K } from "./components/grid.js";
15
+ import { renderMarker as Q } from "./components/marker.js";
16
+ import { renderReferenceLine as tt } from "./components/reference-line.js";
17
+ import { resolveTimeRegionColors as it, renderTimeRegion as et } from "./components/time-region.js";
18
+ import { EventEmitter as st } from "./events.js";
19
+ import { InteractionHandler as rt } from "./interactions/handler.js";
20
+ import { RenderScheduler as M } from "./render-scheduler.js";
21
+ import { XScale as nt } from "./scales/x-scale.js";
22
+ import { YScale as ht } from "./scales/y-scale.js";
23
+ import { resolveSeriesDefinition as at } from "./series/definition.js";
24
+ import { catppuccin as ot } from "./theme/themes/catppuccin.js";
25
+ import { normalizeTime as g, detectInterval as lt } from "./utils/time.js";
26
+ let dt = 0, ct = 0, mt = 0, ut = 0;
27
+ function I(m, i) {
28
+ return m === i ? !0 : !m || !i || m.length !== i.length ? !1 : m.every((t, e) => t === i[e]);
29
+ }
30
+ function ft(m, i) {
31
+ return m === i ? !0 : !m || !i || m.length !== i.length ? !1 : m.every((t, e) => t === i[e]);
32
+ }
33
+ class Ot extends st {
34
+ /** Canvas lifecycle and DPR-aware sizing. */
35
+ #n;
36
+ /** Schedules main-layer redraws (background, grid, series). */
37
+ #i;
38
+ /** Schedules overlay redraws (crosshair). */
39
+ #d;
40
+ /** Maps time values to horizontal pixel coordinates. */
41
+ timeScale;
42
+ /** Maps price/value to vertical pixel coordinates. */
43
+ yScale;
44
+ /** Zoom, pan, crosshair — null when interactive=false. */
45
+ #W;
46
+ /** All registered series (candlestick, line, bar, pie). */
47
+ #t = [];
48
+ /** Point annotations (event markers) drawn on the overlay layer, kept out of the series model. */
49
+ #c = [];
50
+ /** Time-region bands drawn on the main layer behind the series, kept out of the series model. */
51
+ #v = [];
52
+ /** Reference lines (horizontal threshold / vertical event) drawn on the overlay layer, kept out of the series model. */
53
+ #g = [];
54
+ /** Active visual theme (colors, fonts, grid style). */
55
+ #r;
56
+ /** Whether to render the background grid. */
57
+ #E;
58
+ /** Detected time interval between data points (milliseconds). */
59
+ #h = 6e4;
60
+ /** Current crosshair position, null when cursor is outside the chart. */
61
+ #m = null;
62
+ /** Whether the last overlay pass painted anything — gates the full-bitmap
63
+ * clear so an empty overlay isn't cleared again every main frame. */
64
+ #G = !1;
65
+ /** User-specified Y-axis bounds (auto, fixed, percentage). */
66
+ #a = {};
67
+ /** Cached series ID list — invalidated on add/remove. */
68
+ #A = null;
69
+ /** Axis visibility and sizing configuration. */
70
+ #I = {};
71
+ /** Host-declared state per edge — drives the edge-indicator overlay. */
72
+ #b = { left: "idle", right: "idle" };
73
+ /** Cached boundary timestamps from the last `edgeReached` emission, by side. */
74
+ #j = { left: null, right: null };
75
+ /** Host-supplied callback fired when the user releases a pan/zoom past a data edge. */
76
+ #q;
77
+ /** Nesting depth for batch updates. Suppresses recomputes while > 0. */
78
+ #w = 0;
79
+ /** True when batched operations include data changes (triggers full onDataChanged on end). */
80
+ #$ = !1;
81
+ /** True when batched operations include visibility changes (triggers Y-range + redraw on end). */
82
+ #T = !1;
83
+ /** True when batched operations bumped overlay version (emits a single overlayChange on flush). */
84
+ #O = !1;
85
+ /**
86
+ * Monotonic counter bumped on any mutation that affects overlay output —
87
+ * data, visibility, series options, theme. Used by snapshot helpers in
88
+ * `@wick-charts/core` as a cache key, so `buildHoverSnapshots` /
89
+ * `buildLastSnapshots` return the same reference between ticks when
90
+ * nothing observable has changed.
91
+ *
92
+ * @internal
93
+ */
94
+ #J = 0;
95
+ get yAxisWidth() {
96
+ const i = this.#I.y;
97
+ return i?.visible === !1 ? 0 : i?.width ?? 55;
98
+ }
99
+ get xAxisHeight() {
100
+ const i = this.#I.x;
101
+ return i?.visible === !1 ? 0 : i?.height ?? 30;
102
+ }
103
+ /** Resolved animation config derived from `options.animations` at construction. */
104
+ #f;
105
+ /**
106
+ * Single source of truth for the chart's X / Y viewport animation.
107
+ * Push-model API: chart emits intent signals (`onPointAppended`,
108
+ * `onSeriesVisibilityChanged`, `onPanZoom`, `onProgrammaticZoom`,
109
+ * `onAxisReconfig`, `onDataReplaced`, `snap`); the engine pulls the
110
+ * matching targets back through `computeXTarget` / `computeYTarget`
111
+ * closures supplied at construction.
112
+ */
113
+ #e;
114
+ /** EMA-smoothed real-wall-clock cadence between streaming appends. The
115
+ * X spring's `setSettleMs` is fed from this so the settle time stays just
116
+ * longer than the producer's tick interval — the spring never quite
117
+ * settles between ticks, so velocity carries continuously and the slide
118
+ * doesn't pulse. */
119
+ #D;
120
+ /**
121
+ * Mirrors `#dataReplaceSnapPending` for the engine signal that follows
122
+ * inside the same `onDataChanged` call. `#dataReplaceSnapPending` is
123
+ * consumed at the top of `onDataChanged` (so a stray streaming append
124
+ * couldn't re-trigger the snap), but the `computeXTarget` closure runs
125
+ * later and still needs to know whether to refit the X window. This
126
+ * field carries the decision across that gap. The closure also reads
127
+ * `#dataStart` / `#dataEnd` directly for the timestamps.
128
+ */
129
+ #K = !1;
130
+ /** Earliest data timestamp registered across all series, `null` before any data has arrived. */
131
+ #k = null;
132
+ /** Latest data timestamp registered across all series, `null` before any data has arrived. */
133
+ #s = null;
134
+ /**
135
+ * Previous {@link #dataEnd} value. Streaming-target math reads it to
136
+ * preserve any pan offset across ticks: a user who panned a few bars
137
+ * left of the tail keeps that offset as new bars arrive, instead of
138
+ * snapping back to the natural-pin position every frame.
139
+ */
140
+ #P = null;
141
+ /**
142
+ * Warm-up window flag — `setVisibleRange({ from, bars })` arms it and
143
+ * the streaming-target math suppresses pan ticks while the data hasn't
144
+ * reached the right edge yet. Cleared by user interaction or once the
145
+ * data fills the window.
146
+ */
147
+ #L = !1;
148
+ /**
149
+ * Cap for fit-to-data. When the data span exceeds this, the fit anchors
150
+ * the right edge and trims the left. Resolved once at construction from
151
+ * `options.viewport.maxVisibleBars`.
152
+ */
153
+ #Q;
154
+ /**
155
+ * Committed logical X range — the "where X should be" target. Engine
156
+ * eases its visual toward this. Pan / zoom / setRange / fitContent /
157
+ * streaming-target writes go here. Initialized to `{0,0}` and treated
158
+ * as "uninitialized" by `#computeXTarget` until first paint.
159
+ */
160
+ #o = { from: 0, to: 0 };
161
+ /** Padded Y range — engine's raw Y target plus pixel padding from `#padding`. */
162
+ #V = { min: 0, max: 0 };
163
+ /**
164
+ * Auto-scroll flag — when `true`, streaming ticks slide X to track the
165
+ * data tail. Pan flips this off when the gesture pushes the tail off
166
+ * screen; chart's per-frame autoscroll check re-engages it when a
167
+ * follow-up pan brings the tail back into view.
168
+ */
169
+ #p = !0;
170
+ /** Resolved viewport padding (top/bottom px + left/right pixels-or-intervals). */
171
+ #l;
172
+ /** Cached chart width — passed by interactions on every call; kept for the rare
173
+ * call that omits it (e.g. programmatic `chart.pan(...)` without explicit width). */
174
+ #z = 0;
175
+ /** Active performance monitor, or `null` when instrumentation is disabled (the default). */
176
+ #x;
177
+ /** When true, `destroy()` tears down the monitor; false for caller-supplied monitors we must not destroy. */
178
+ #B = !1;
179
+ /** Visible HUD overlay (mounted by the perf config); non-null only when the caller requested one. */
180
+ #F = null;
181
+ constructor(i, t) {
182
+ super(), t?.axis && (this.#I = t.axis, this.#a = { min: t.axis.y?.min, max: t.axis.y?.max }), this.#r = t?.theme ?? ot.theme, this.#E = t?.grid?.visible !== !1, this.#f = z.resolve(t?.animations), this.#Q = X(t?.viewport?.maxVisibleBars), this.#l = D(t?.padding), this.#D = new G();
183
+ const e = this.#f.axis.y, s = this.#f.axis.x;
184
+ this.#e = N({
185
+ initial: { yRange: { min: 0, max: 0 }, xRange: { from: 0, to: 0 } },
186
+ y: {
187
+ curve: e.curve,
188
+ settleMs: e.settleMs,
189
+ stickyMs: e.stickyMs,
190
+ stickyFloorMs: e.stickyFloorMs,
191
+ gestureMs: e.gestureMs,
192
+ toggleMs: this.#f.toggleMs
193
+ },
194
+ x: {
195
+ curve: s.curve,
196
+ settleMs: s.settleMs,
197
+ gestureMs: s.gestureMs
198
+ },
199
+ computeXTarget: () => this.#gt(),
200
+ computeYTarget: ({ xTarget: a }) => this.#st(a),
201
+ onWake: () => this.#i?.markDirty()
202
+ }), this.#q = t?.onEdgeReached, this.#H = t?.viewport?.initialRange;
203
+ const r = _(t?.perf);
204
+ this.#x = r.monitor, this.#B = r.ownsMonitor, this.#n = new W(i, this.#x ?? void 0), this.timeScale = new nt(), this.yScale = new ht();
205
+ const n = this.#f.axis.ticksMs;
206
+ this.timeScale.tickTracker.setFadeMs(n), this.yScale.tickTracker.setFadeMs(n), this.#i = new M((a) => this.#U(a)), this.#d = new M(
207
+ (a) => this.#Z("overlay", a, () => this.renderOverlay(a))
208
+ ), this.#x && r.hud && (this.#F = r.hud(i, this.#x));
209
+ const h = t?.interactive !== !1;
210
+ this.#W = h ? new rt(this.#n.canvas, this, this.timeScale, this.yScale) : null, this.#n.on("resize", () => {
211
+ this.syncScales(), this.#U(), this.emit("viewportChange");
212
+ }), this.syncScales(), this.#W?.on("crosshairMove", (a) => {
213
+ this.#m = a, this.#d.markDirty(), this.emit("crosshairMove", a), this.#ft(a);
214
+ });
215
+ }
216
+ /**
217
+ * Bump overlay version + emit `overlayChange`. Call from every mutation
218
+ * path whose output overlay components care about: data, series list,
219
+ * per-series/layer visibility, options, theme.
220
+ *
221
+ * Inside `batch(...)` the version still increments per call (so the
222
+ * snapshot cache invalidates at least once per logical change), but the
223
+ * `overlayChange` emission is coalesced to a single event on flush.
224
+ * That lets Legend-style isolate toggles — which batch multiple
225
+ * `setSeriesVisible` / `setLayerVisible` calls — produce one overlay
226
+ * render instead of N.
227
+ */
228
+ #S() {
229
+ if (this.#J++, this.#w > 0) {
230
+ this.#O = !0;
231
+ return;
232
+ }
233
+ this.emit("overlayChange");
234
+ }
235
+ /**
236
+ * Monotonic counter incremented on any mutation that affects overlay
237
+ * output. Snapshot helpers (`buildHoverSnapshots`, `buildLastSnapshots`)
238
+ * key their structural-equality cache on this value — same version +
239
+ * same `(time, sort, cacheKey)` returns the same frozen reference.
240
+ *
241
+ * @internal
242
+ */
243
+ getOverlayVersion() {
244
+ return this.#J;
245
+ }
246
+ /**
247
+ * Return a series ID: use the provided hint if it's non-empty and not already taken,
248
+ * otherwise generate a new auto ID. Auto-generated IDs never collide with each other
249
+ * or with user-provided IDs because they use a monotonically increasing counter.
250
+ */
251
+ #at(i) {
252
+ if (i && !this.#t.some((e) => e.id === i))
253
+ return i;
254
+ let t;
255
+ do
256
+ t = `series_${++dt}`;
257
+ while (this.#t.some((e) => e.id === t));
258
+ return t;
259
+ }
260
+ addSeries(i, t = {}) {
261
+ const e = typeof i == "string" ? at(i) : i;
262
+ if (!e)
263
+ throw new Error(
264
+ `[wick-charts] Unknown series type '${String(i)}'. The string form of addSeries resolves through the series registry — call registerBuiltinSeries() once at startup, or pass the definition directly: chart.addSeries(LineSeriesDef, options).`
265
+ );
266
+ const { layers: s, id: r, label: n, labels: h, ...a } = t, d = s ?? 1, l = {
267
+ ...this.#f.defaults(e.type),
268
+ ...a,
269
+ ...this.#f.overrides(e.type)
270
+ }, u = e.create({ theme: this.#r, layerCount: d }, l), c = h ?? (typeof n == "string" ? [n] : void 0), o = this.#at(r);
271
+ return u.onDataChanged?.(() => this.onDataChanged()), this.#t.push({ id: o, labels: c, renderer: u, visible: !0 }), this.#A = null, this.emit("seriesChange"), this.#S(), o;
272
+ }
273
+ /** Remove a series by ID and clean up its resources. */
274
+ removeSeries(i) {
275
+ const t = this.#t.findIndex((e) => e.id === i);
276
+ t >= 0 && (this.#t[t].renderer.dispose(), this.#t.splice(t, 1), this.#A = null, this.#i.markDirty(), this.emit("seriesChange"), this.#S());
277
+ }
278
+ // --- Markers (point annotations) -----------------------------------------
279
+ /**
280
+ * Pin a point annotation to `time` (X) and `value` (Y), or to a series via
281
+ * `seriesId` (Y snaps to the nearest data point at `time`). Returns the marker
282
+ * id for later {@link updateMarker} / {@link removeMarker}.
283
+ *
284
+ * Markers render on the overlay layer and stay out of the series model — they
285
+ * are excluded from tooltips, the legend, and the Y-range autoscale, and never
286
+ * appear in series queries.
287
+ */
288
+ addMarker(i) {
289
+ const t = `marker_${++ct}`;
290
+ return this.#c.push(this.#tt(t, i)), this.#d.markDirty(), t;
291
+ }
292
+ /** Replace a marker's config in place. No-op when the id is unknown. */
293
+ updateMarker(i, t) {
294
+ const e = this.#c.findIndex((s) => s.id === i);
295
+ e !== -1 && (this.#c[e] = this.#tt(i, t), this.#d.markDirty());
296
+ }
297
+ /** Remove a marker by id. No-op when the id is unknown. */
298
+ removeMarker(i) {
299
+ const t = this.#c.findIndex((e) => e.id === i);
300
+ t !== -1 && (this.#c.splice(t, 1), this.#d.markDirty());
301
+ }
302
+ /** Read-only snapshot of the registered markers (normalized). Mutate via add / update / removeMarker. */
303
+ getMarkers() {
304
+ return this.#c;
305
+ }
306
+ #tt(i, t) {
307
+ return {
308
+ id: i,
309
+ time: g(t.time),
310
+ value: t.value,
311
+ seriesId: t.seriesId,
312
+ shape: t.shape ?? "dot",
313
+ pulse: t.pulse ?? !1,
314
+ label: t.label,
315
+ color: t.color
316
+ };
317
+ }
318
+ /** Resolve a marker's Y value: explicit `value`, else snapped to its series at `time`. Null when neither resolves. */
319
+ #ot(i) {
320
+ if (i.value !== void 0 && Number.isFinite(i.value)) return i.value;
321
+ if (i.seriesId === void 0) return null;
322
+ const t = this.getDataAtTime(i.seriesId, i.time);
323
+ return t ? "close" in t ? t.close : t.value : null;
324
+ }
325
+ /** Resolve a marker's color: explicit `color`, else the series color, else the theme line color. */
326
+ #lt(i) {
327
+ if (i.color !== void 0) return i.color;
328
+ if (i.seriesId !== void 0) {
329
+ const t = this.getSeriesColor(i.seriesId);
330
+ if (t !== null) return t;
331
+ }
332
+ return this.#r.line.color;
333
+ }
334
+ #dt(i) {
335
+ if (this.#c.length === 0) return;
336
+ const t = performance.now() / (H * 2 * Math.PI) % 1;
337
+ for (const e of this.#c) {
338
+ const s = this.#ot(e);
339
+ s !== null && Q({
340
+ scope: i,
341
+ timeScale: this.timeScale,
342
+ yScale: this.yScale,
343
+ theme: this.#r,
344
+ marker: {
345
+ time: e.time,
346
+ value: s,
347
+ color: this.#lt(e),
348
+ shape: e.shape,
349
+ pulse: e.pulse,
350
+ label: e.label
351
+ },
352
+ phase: t
353
+ });
354
+ }
355
+ }
356
+ // --- Time regions (shaded bands) -----------------------------------------
357
+ /**
358
+ * Shade the time interval `[from, to]` with a translucent vertical band —
359
+ * an anomaly window, a maintenance window, a highlighted session. Omit `to`
360
+ * (or pass `'now'`) for an open-ended band that extends to the right edge of
361
+ * the plot — the "still firing" case. Returns the region id for later
362
+ * {@link updateRegion} / {@link removeRegion}.
363
+ *
364
+ * Regions render on the main layer *behind* the series and stay out of the
365
+ * series model — they never affect the Y-range autoscale, tooltips, or the
366
+ * legend.
367
+ */
368
+ addRegion(i) {
369
+ const t = `region_${++mt}`;
370
+ return this.#v.push(this.#it(t, i)), this.#i.markDirty(), t;
371
+ }
372
+ /** Replace a region's config in place. No-op when the id is unknown. */
373
+ updateRegion(i, t) {
374
+ const e = this.#v.findIndex((s) => s.id === i);
375
+ e !== -1 && (this.#v[e] = this.#it(i, t), this.#i.markDirty());
376
+ }
377
+ /** Remove a region by id. No-op when the id is unknown. */
378
+ removeRegion(i) {
379
+ const t = this.#v.findIndex((e) => e.id === i);
380
+ t !== -1 && (this.#v.splice(t, 1), this.#i.markDirty());
381
+ }
382
+ /** Read-only snapshot of the registered regions (normalized). Mutate via add / update / removeRegion. */
383
+ getRegions() {
384
+ return this.#v;
385
+ }
386
+ #it(i, t) {
387
+ const { to: e } = t, s = e === void 0 || e === "now" ? null : g(e);
388
+ return {
389
+ id: i,
390
+ from: g(t.from),
391
+ to: s,
392
+ fill: t.fill,
393
+ color: t.color,
394
+ label: t.label
395
+ };
396
+ }
397
+ #ct(i, t, e) {
398
+ if (this.#v.length !== 0)
399
+ for (const s of this.#v) {
400
+ const { color: r, fill: n } = it(s, this.#r);
401
+ et({
402
+ scope: i,
403
+ timeScale: this.timeScale,
404
+ theme: this.#r,
405
+ region: { from: s.from, to: s.to, fill: n, color: r, label: s.label },
406
+ plotWidth: t,
407
+ plotHeight: e
408
+ });
409
+ }
410
+ }
411
+ // --- Reference lines (thresholds / event boundaries) ---------------------
412
+ /**
413
+ * Draw a reference line across the plot — a horizontal threshold / baseline
414
+ * pinned to a `value`, or a vertical event boundary pinned to a `time`
415
+ * (mutually exclusive). Returns the line id for later {@link updateLine} /
416
+ * {@link removeLine}.
417
+ *
418
+ * Lines render on the overlay layer *above* the series and stay out of the
419
+ * series model — they never affect the Y-range autoscale, tooltips, or the
420
+ * legend.
421
+ */
422
+ addLine(i) {
423
+ const t = `line_${++ut}`;
424
+ return this.#g.push(this.#et(t, i)), this.#d.markDirty(), t;
425
+ }
426
+ /** Replace a reference line's config in place. No-op when the id is unknown. */
427
+ updateLine(i, t) {
428
+ const e = this.#g.findIndex((s) => s.id === i);
429
+ e !== -1 && (this.#g[e] = this.#et(i, t), this.#d.markDirty());
430
+ }
431
+ /** Remove a reference line by id. No-op when the id is unknown. */
432
+ removeLine(i) {
433
+ const t = this.#g.findIndex((e) => e.id === i);
434
+ t !== -1 && (this.#g.splice(t, 1), this.#d.markDirty());
435
+ }
436
+ /** Read-only snapshot of the registered reference lines (normalized). Mutate via add / update / removeLine. */
437
+ getLines() {
438
+ return this.#g;
439
+ }
440
+ #et(i, t) {
441
+ const { time: e, value: s } = t, r = {
442
+ id: i,
443
+ color: t.color,
444
+ label: t.label,
445
+ style: t.style ?? "dashed",
446
+ width: t.width ?? 1
447
+ };
448
+ return e !== void 0 ? { ...r, orientation: "vertical", coord: g(e) } : { ...r, orientation: "horizontal", coord: s ?? Number.NaN };
449
+ }
450
+ #mt(i, t, e) {
451
+ if (this.#g.length !== 0)
452
+ for (const s of this.#g)
453
+ tt({
454
+ scope: i,
455
+ timeScale: this.timeScale,
456
+ yScale: this.yScale,
457
+ theme: this.#r,
458
+ line: {
459
+ orientation: s.orientation,
460
+ coord: s.coord,
461
+ color: s.color ?? this.#r.line.color,
462
+ style: s.style,
463
+ width: s.width,
464
+ label: s.label
465
+ },
466
+ plotWidth: t,
467
+ plotHeight: e
468
+ });
469
+ }
470
+ /**
471
+ * Read-only snapshot of the chart's X / Y viewport animation. The same
472
+ * shape (`xRange`, `yRange`, `animating`) every frame — call this inside
473
+ * the current render pass rather than caching the reference, as the
474
+ * underlying animator mutates between frames.
475
+ */
476
+ getAnimationState() {
477
+ return this.#e.getAnimationState();
478
+ }
479
+ /**
480
+ * Replace all data for a series.
481
+ *
482
+ * - Single-layer series (candlestick, single-layer line/bar, pie): pass `data` directly.
483
+ * - Multi-layer series (line/bar with multiple layers): pass `layerIndex` to target a specific layer.
484
+ *
485
+ * For line/bar accepts `TimePointInput[]` (time may be `Date`); for candlestick accepts `OHLCInput[]`;
486
+ * for pie accepts `PieSliceData[]`. Time fields are normalized internally.
487
+ */
488
+ setSeriesData(i, t, e) {
489
+ const s = this.#t.find((r) => r.id === i);
490
+ s && (this.#Y = !0, s.renderer.setData(t, e));
491
+ }
492
+ /** Append a new data point to the end of a series (real-time tick). */
493
+ appendData(i, t, e) {
494
+ const s = this.#t.find((r) => r.id === i);
495
+ s === void 0 || s.renderer.appendPoint === void 0 || s.renderer.appendPoint(t, e);
496
+ }
497
+ /** Update the last data point of a series in place (e.g. live candle update). */
498
+ updateData(i, t, e) {
499
+ this.#t.find((s) => s.id === i)?.renderer.updateLastPoint?.(t, e);
500
+ }
501
+ /**
502
+ * Keep only the most recent `count` points of a series — drop the oldest
503
+ * tail when the series exceeds the cap. Smooth Y-range chase (no snap):
504
+ * unlike {@link setSeriesData}, this does NOT set the bulk-replace snap
505
+ * flag, so streaming windows can roll without per-tick Y jitter.
506
+ *
507
+ * Idempotent: a no-op when the series is already at or below `count`.
508
+ * Use after {@link appendData} in a rolling-window stream:
509
+ * ```ts
510
+ * chart.appendData('feed', point);
511
+ * chart.keepLast('feed', 100);
512
+ * ```
513
+ */
514
+ keepLast(i, t, e) {
515
+ this.#t.find((s) => s.id === i)?.renderer.keepLast?.(t, e);
516
+ }
517
+ /** Update visual options (color, width, etc.) for an existing series. */
518
+ updateSeriesOptions(i, t) {
519
+ const e = this.#t.find((l) => l.id === i);
520
+ if (!e) return;
521
+ const s = e.labels, r = e.renderer.getLayerColors().slice(), n = this.#f.overrides(e.renderer.kind);
522
+ e.renderer.updateOptions({ ...t, ...n }), "label" in t && typeof t.label == "string" && (e.labels = [t.label]), this.#i.markDirty();
523
+ const h = e.renderer.getLayerColors(), a = r.length !== h.length || r.some((l, u) => l !== h[u]), d = !I(s, e.labels);
524
+ (a || d) && this.#S();
525
+ }
526
+ /**
527
+ * Batch multiple updates: suppress recomputes until `fn` returns. Exceptions
528
+ * inside `fn` still flush the batch so counters don't leak across calls.
529
+ */
530
+ batch(i) {
531
+ this.#w++;
532
+ try {
533
+ i();
534
+ } finally {
535
+ if (--this.#w <= 0) {
536
+ if (this.#w = 0, this.#$)
537
+ this.#$ = !1, this.#T = !1, this.onDataChanged();
538
+ else if (this.#T) {
539
+ this.#T = !1, this.#u = !0;
540
+ const t = performance.now();
541
+ this.#e.onAxisReconfig(t), this.#y(t), this.#i.markDirty();
542
+ }
543
+ this.#O && (this.#O = !1, this.emit("overlayChange"));
544
+ }
545
+ }
546
+ }
547
+ /** Show or hide a series. The series cross-fades over
548
+ * `animations.toggle`; the Y range re-fits on the same schedule
549
+ * (one-shot duration override) so the fade and the axis adjustment
550
+ * finish on the same frame. Hidden series are excluded from Y-range
551
+ * computation immediately so the axis can start moving while the line
552
+ * fades out in parallel.
553
+ */
554
+ setSeriesVisible(i, t) {
555
+ const e = this.#t.find((n) => n.id === i);
556
+ if (!e || e.visible === t) return;
557
+ e.visible = t, this.#S();
558
+ const s = this.#f.toggleMs;
559
+ if (e.renderer.setAlpha?.(t ? 1 : 0, s), this.#i.markDirty(), this.#w > 0) {
560
+ this.#T = !0;
561
+ return;
562
+ }
563
+ this.#u = !0;
564
+ const r = performance.now();
565
+ this.#e.onSeriesVisibilityChanged(r), this.#y(r);
566
+ }
567
+ /**
568
+ * Whether streaming ticks slide the visible range to track the data tail.
569
+ * Flipped off by a pan that pushes the tail off screen; re-engaged when
570
+ * a follow-up pan brings the tail back into the destination window or
571
+ * when {@link fitContent} / {@link setVisibleRange} commits a range
572
+ * containing the tail.
573
+ */
574
+ getAutoScroll() {
575
+ return this.#p;
576
+ }
577
+ isSeriesVisible(i) {
578
+ return this.#t.find((t) => t.id === i)?.visible ?? !0;
579
+ }
580
+ /**
581
+ * Show or hide a specific layer within a multi-layer series.
582
+ *
583
+ * Symmetric with {@link setSeriesVisible}: drives both the renderer's
584
+ * per-layer alpha fade and the engine's Y re-fit through `toggleMs` so the
585
+ * line fade and the axis adjustment finish on the same frame. Hidden layers
586
+ * are excluded from Y-range computation immediately (via the renderer's
587
+ * `getValueRange` reading `store.isVisible()`); the alpha fade keeps the
588
+ * layer's geometry on screen until the engine's Y ease settles.
589
+ */
590
+ setLayerVisible(i, t, e) {
591
+ const s = this.#t.find((h) => h.id === i);
592
+ if (!s || s.renderer.getLayerCount() <= 1 || s.renderer.isLayerVisible(t) === e) return;
593
+ s.renderer.setLayerVisible(t, e), this.#S();
594
+ const r = this.#f.toggleMs;
595
+ if (s.renderer.setLayerAlpha?.(t, e ? 1 : 0, r), this.#i.markDirty(), this.#w > 0) {
596
+ this.#T = !0;
597
+ return;
598
+ }
599
+ this.#u = !0;
600
+ const n = performance.now();
601
+ this.#e.onSeriesVisibilityChanged(n), this.#y(n);
602
+ }
603
+ isLayerVisible(i, t) {
604
+ return this.#t.find((e) => e.id === i)?.renderer.isLayerVisible(t) ?? !0;
605
+ }
606
+ /** Auto-fit the viewport to show all data across every series. */
607
+ fitContent() {
608
+ const { first: i, last: t } = this.getDataBounds();
609
+ if (i === void 0 || t === void 0) return;
610
+ const e = this.#n.size.media.width - this.yAxisWidth;
611
+ this.#M(i, t, e), this.#N({ xEase: !0 });
612
+ }
613
+ getVisibleRange() {
614
+ return this.#o.from === 0 && this.#o.to === 0 ? this.#e.getAnimationState().xRange : this.#o;
615
+ }
616
+ /**
617
+ * Return the full span of registered data, or `null` before any data has
618
+ * arrived. `{ from: dataStart, to: dataEnd }` mirrors the chart's own
619
+ * tracking and is cheaper than recomputing from series stores.
620
+ */
621
+ getDataRange() {
622
+ return this.#k === null || this.#s === null ? null : { from: this.#k, to: this.#s };
623
+ }
624
+ /**
625
+ * Imperatively set the visible time range. Three forms:
626
+ *
627
+ * - `number N` — show the last N bars from the data tail (anchor right).
628
+ * Resolved against current data bounds and data interval; keeps
629
+ * auto-scroll on. No-op if data hasn't loaded yet.
630
+ * - `{ from, to }` — explicit time range. `from`/`to` accept either
631
+ * epoch milliseconds or `Date`. Cancels any in-flight animation and
632
+ * applies immediately. Auto-scroll stays on only if the tail is
633
+ * inside the new range (mirrors pan semantics).
634
+ * - `{ from, bars }` — anchor the visible window at `from` and extend
635
+ * right by `bars` data intervals. Useful for warm-up windows where
636
+ * seed points sit on the left and a stream fills the gap on the
637
+ * right. `from` accepts a `Date` too.
638
+ *
639
+ * Typical use: on mount, zoom to the last N bars while keeping the full
640
+ * buffer available for pan-back history inspection.
641
+ *
642
+ * `opts.gesture` (explicit `{ from, to }` form only) eases X and Y on the
643
+ * same fast, velocity-matched profile a user pan/zoom uses instead of the
644
+ * default programmatic settle (snap X, sticky-Y ease). Use it for
645
+ * multi-chart sync so a synced pane tracks the originating gesture in
646
+ * lockstep rather than lagging behind on the long sticky contract.
647
+ */
648
+ setVisibleRange(i, t) {
649
+ if (typeof i == "number") {
650
+ if (!Number.isInteger(i) || i < 2) return;
651
+ const { first: n, last: h } = this.getDataBounds();
652
+ if (n === void 0 || h === void 0) return;
653
+ const a = Math.max(n, h - (i - 1) * this.#h), d = this.#n.size.media.width - this.yAxisWidth;
654
+ this.#M(a, h, d), this.#N();
655
+ return;
656
+ }
657
+ if ("bars" in i) {
658
+ if (!Number.isInteger(i.bars) || i.bars < 2) return;
659
+ const n = g(i.from), h = n + i.bars * this.#h;
660
+ this.#p = this.#s !== null && this.#s >= n && this.#s <= h, this.#C({ from: n, to: h }, { emitChange: !0 }), this.#L = !0, this.#N();
661
+ return;
662
+ }
663
+ const e = g(i.from), s = g(i.to);
664
+ this.#p = this.#s !== null && this.#s >= e && this.#s <= s, this.#L = !1;
665
+ const r = this.#C({ from: e, to: s }, { emitChange: !0 });
666
+ if (t?.gesture) {
667
+ r && this.#pt();
668
+ return;
669
+ }
670
+ this.#N();
671
+ }
672
+ getYRange() {
673
+ return this.#V;
674
+ }
675
+ getCrosshairPosition() {
676
+ return this.#m;
677
+ }
678
+ /**
679
+ * Programmatically set or clear the crosshair. Same effect as the user
680
+ * hovering over `(timeToX(time), valueToY(y))` — emits `crosshairMove`,
681
+ * marks the overlay layer dirty, and lets `<Crosshair>` / `<Tooltip>`
682
+ * react via their normal store subscriptions.
683
+ *
684
+ * Pass `null` to clear (mirrors the pointer-leave path).
685
+ *
686
+ * `y` is optional. Resolution order when omitted:
687
+ * 1. **current crosshair's y** — preserves whatever the chart already
688
+ * has (real cursor y, or a previously-synthesised value). This is
689
+ * what makes the cross-chart broadcast pattern work: when an echo
690
+ * arrives at the chart that originated the hover, the existing y
691
+ * stays in place and the idempotency check below short-circuits the
692
+ * loop. Without preservation, the echo would overwrite the source's
693
+ * real cursor y with the midpoint default and the tooltip would
694
+ * jitter between cursor and midpoint every mouse-move.
695
+ * 2. **midpoint of the current Y range** — fallback when there is no
696
+ * current crosshair (first programmatic set on a chart that hasn't
697
+ * been hovered yet).
698
+ *
699
+ * Idempotent: a call that produces the same `(time, y)` pair as the
700
+ * current crosshair is a no-op (no emit). Combined with the y-preservation
701
+ * above, this lets N charts broadcast their hover to each other without
702
+ * a feedback-loop guard.
703
+ */
704
+ setCrosshair(i) {
705
+ if (i === null) {
706
+ if (this.#m === null) return;
707
+ this.#m = null, this.#d.markDirty(), this.emit("crosshairMove", null);
708
+ return;
709
+ }
710
+ const { time: t } = i;
711
+ if (!Number.isFinite(t)) return;
712
+ const e = this.#m;
713
+ let s;
714
+ if (i.y !== void 0)
715
+ s = i.y;
716
+ else if (e !== null)
717
+ s = e.y;
718
+ else {
719
+ const h = this.#V;
720
+ s = (h.min + h.max) / 2;
721
+ }
722
+ if (!Number.isFinite(s) || e !== null && e.time === t && e.y === s) return;
723
+ const r = this.timeScale.timeToX(t), n = this.yScale.valueToY(s);
724
+ this.#m = { mediaX: r, mediaY: n, time: t, y: s }, this.#d.markDirty(), this.emit("crosshairMove", this.#m);
725
+ }
726
+ /** Get the last visible value and whether the absolute last point is on screen. */
727
+ getLastValue(i) {
728
+ return B(i, this.#t, this.#e.getAnimationState().xRange);
729
+ }
730
+ /** Get the second-to-last value, useful for computing change. */
731
+ getPreviousClose(i) {
732
+ return F(i, this.#t);
733
+ }
734
+ getLastData(i) {
735
+ const t = this.#t.find((e) => e.id === i);
736
+ return !t || t.renderer.kind === "pie" ? null : t.renderer.getLastDataPoint();
737
+ }
738
+ /** Find the data point closest to the given timestamp within one data interval. */
739
+ getDataAtTime(i, t) {
740
+ const e = this.#t.find((s) => s.id === i);
741
+ return e ? e.renderer.getDataAtTime?.(t, this.#h) ?? null : null;
742
+ }
743
+ /**
744
+ * Get all layers' data at a given time for multi-layer series (Bar/Line
745
+ * with stacking). Each entry carries the owning `layerIndex` and the
746
+ * snapped sample time — callers must not derive layer identity from the
747
+ * array index, because hidden layers are filtered out.
748
+ */
749
+ getLayerSnapshots(i, t) {
750
+ const e = this.#t.find((s) => s.id === i);
751
+ return e?.visible ? e.renderer.getLayerSnapshots?.(t, this.#h) ?? null : null;
752
+ }
753
+ getSeriesIds() {
754
+ return this.#A || (this.#A = this.#t.map((i) => i.id)), this.#A.slice();
755
+ }
756
+ /**
757
+ * True if any visible series is non-pie (line / bar / candlestick). Used
758
+ * to gate crosshair rendering — a pie-only chart has no meaningful x/y
759
+ * coordinate system, so the dashed hairlines would just be visual noise.
760
+ */
761
+ #ut() {
762
+ for (const i of this.#t)
763
+ if (i.visible && i.renderer.kind !== "pie")
764
+ return !0;
765
+ return !1;
766
+ }
767
+ /**
768
+ * Filter `getSeriesIds()` by renderer type. `'pie'` returns pie series,
769
+ * `'time'` returns line/bar/candlestick.
770
+ *
771
+ * - `opts.visibleOnly` — exclude series with `isSeriesVisible=false`; for
772
+ * multi-layer series also exclude when every layer is hidden.
773
+ * - `opts.singleLayerOnly` — exclude series with more than one layer.
774
+ * Useful for YLabel fallback priority (stick to a single line first).
775
+ */
776
+ getSeriesIdsByType(i, t) {
777
+ const e = t?.visibleOnly === !0, s = t?.singleLayerOnly === !0, r = [];
778
+ for (const n of this.#t) {
779
+ const h = n.renderer.kind === "pie";
780
+ if (i === "pie" && !h || i === "time" && h) continue;
781
+ const a = n.renderer.getLayerCount();
782
+ if (!(s && a > 1)) {
783
+ if (e) {
784
+ if (!n.visible) continue;
785
+ if (a > 1) {
786
+ let d = !1;
787
+ for (let l = 0; l < a; l++)
788
+ if (n.renderer.isLayerVisible(l)) {
789
+ d = !0;
790
+ break;
791
+ }
792
+ if (!d) continue;
793
+ }
794
+ }
795
+ r.push(n.id);
796
+ }
797
+ }
798
+ return r;
799
+ }
800
+ /**
801
+ * Cumulative top last-value for stacked series — the point a YLabel badge
802
+ * anchors to on the rendered stack head. Falls back to `getLastValue` for
803
+ * series without stacked concepts (Candlestick, single-layer Line/Bar).
804
+ * Returns `null` for unknown ids or empty series.
805
+ */
806
+ getStackedLastValue(i) {
807
+ return Y(i, this.#t, this.#e.getAnimationState().xRange);
808
+ }
809
+ /**
810
+ * Per-layer last snapshots with each layer's own `time`. Returns `null`
811
+ * for single-layer renderers or when no visible layer has data. Used by
812
+ * overlay components that must display every layer in last-mode even
813
+ * when layers advance at different rates (ragged streams).
814
+ */
815
+ getLayerLastSnapshots(i) {
816
+ return this.#t.find((t) => t.id === i)?.renderer.getLayerLastSnapshots?.() ?? null;
817
+ }
818
+ /**
819
+ * Shallow view of the internal series entries so unit tests can introspect
820
+ * renderer state without exporting a public `getRenderer(id)` (which would
821
+ * leak an implementation detail). Named to discourage use outside of tests.
822
+ *
823
+ * @internal
824
+ */
825
+ listSeriesForTest() {
826
+ return this.#t.map((i) => ({ id: i.id, renderer: i.renderer }));
827
+ }
828
+ /** Get the primary display color for a series. */
829
+ getSeriesColor(i) {
830
+ const t = this.#t.find((e) => e.id === i);
831
+ return t ? t.renderer.getLayerColors()[0] ?? this.#r.line.color : null;
832
+ }
833
+ /** The series' primary name (first layer). Used for single-layer overlays. */
834
+ getSeriesLabel(i) {
835
+ return this.#t.find((t) => t.id === i)?.labels?.[0];
836
+ }
837
+ /** The display name of one layer, or `undefined` when that layer is unnamed. */
838
+ getLayerLabel(i, t) {
839
+ return this.#t.find((e) => e.id === i)?.labels?.[t];
840
+ }
841
+ /**
842
+ * Set every layer's display name at once (index = layer). Drives the tooltip,
843
+ * legend, and info bar. Called by the framework wrappers as the carried
844
+ * per-layer labels in `data` change. No-ops (and skips the overlay bump) when
845
+ * the labels are unchanged, so it's safe to call on every data tick.
846
+ */
847
+ setSeriesLabels(i, t) {
848
+ const e = this.#t.find((s) => s.id === i);
849
+ !e || I(e.labels, t) || (e.labels = t, this.#S());
850
+ }
851
+ /**
852
+ * Set the per-layer color overrides carried in `data` (sparse — `undefined`
853
+ * leaves that layer on the theme base). A {@link ValueColor} per layer: a
854
+ * string (uniform) or a function of value (per bar / per segment). Handed to
855
+ * the renderer, which keeps them separate from the theme base so a theme swap
856
+ * preserves them. Called by the framework wrappers; no-ops when unchanged.
857
+ */
858
+ setLayerColors(i, t) {
859
+ const e = this.#t.find((s) => s.id === i);
860
+ !e || ft(e.colorOverrides, t) || (e.colorOverrides = t, e.renderer.setColorOverrides?.(t), this.#i.markDirty(), this.#S());
861
+ }
862
+ /** Get per-layer colors for a series. Returns null for single-layer non-bar/line series. */
863
+ getSeriesLayers(i) {
864
+ const t = this.#t.find((r) => r.id === i);
865
+ if (!t) return null;
866
+ const e = t.renderer.getLayerCount();
867
+ if (e <= 1) return null;
868
+ const s = t.renderer.getLayerColors();
869
+ return Array.from({ length: e }, (r, n) => ({ color: s[n % s.length] }));
870
+ }
871
+ /** Get all slices with computed colors and percentages. Returns null for series without slice data (e.g. candlestick, line, bar). */
872
+ getSliceInfo(i) {
873
+ return this.#t.find((t) => t.id === i)?.renderer.getSliceInfo?.(this.#r) ?? null;
874
+ }
875
+ /** Get hover info (label/value/percent/color) for the currently hovered element, or null. */
876
+ getHoverInfo(i) {
877
+ return this.#t.find((t) => t.id === i)?.renderer.getHoverInfo?.(this.#r) ?? null;
878
+ }
879
+ /** Apply a new theme and update series colors where appropriate. */
880
+ setTheme(i) {
881
+ const t = this.#r;
882
+ this.#r = i;
883
+ for (const e of this.#t)
884
+ e.renderer.applyTheme(i, t);
885
+ this.#i.markDirty(), this.#S();
886
+ }
887
+ getTheme() {
888
+ return this.#r;
889
+ }
890
+ /** Update axis configuration and re-render. */
891
+ setAxis(i) {
892
+ const t = this.yAxisWidth, e = this.xAxisHeight;
893
+ this.#I = i, this.#a = { min: i.y?.min, max: i.y?.max }, this.#u = !0;
894
+ const s = performance.now();
895
+ this.#e.onAxisReconfig(s), this.#y(s), (this.yAxisWidth !== t || this.xAxisHeight !== e) && this.syncScales(), this.#i.markDirty();
896
+ }
897
+ /**
898
+ * Apply label-density knobs driven by `<YAxis labelCount=… minLabelSpacing=…>`
899
+ * — kept separate from {@link setAxis} so component props don't force a
900
+ * Y-animation reset. Triggers a viewportChange + repaint so static charts
901
+ * react without waiting for pan/zoom.
902
+ */
903
+ setYAxisLabelDensity(i) {
904
+ let t = !1;
905
+ "labelCount" in i && (this.yScale.setLabelCount(i.labelCount ?? null), t = !0), "minLabelSpacing" in i && (this.yScale.setMinSpacing(i.minLabelSpacing ?? null), t = !0), t && (this.syncScales(), this.emit("viewportChange"), this.#i.markDirty());
906
+ }
907
+ /** Label-density knobs for the time axis — mirror of {@link setYAxisLabelDensity}. */
908
+ setTimeAxisLabelDensity(i) {
909
+ let t = !1;
910
+ "labelCount" in i && (this.timeScale.setLabelCount(i.labelCount ?? null), t = !0), "minLabelSpacing" in i && (this.timeScale.setMinSpacing(i.minLabelSpacing ?? null), t = !0), t && (this.syncScales(), this.emit("viewportChange"), this.#i.markDirty());
911
+ }
912
+ getMediaSize() {
913
+ return this.#n.size.media;
914
+ }
915
+ /** Returns layout metrics for the chart area, Y axis, and time axis. */
916
+ getLayout() {
917
+ const i = this.#n.size.media, t = this.yAxisWidth, e = this.xAxisHeight;
918
+ return {
919
+ chartArea: { x: 0, y: 0, width: i.width - t, height: i.height - e },
920
+ yAxisWidth: t,
921
+ xAxisHeight: e
922
+ };
923
+ }
924
+ getDataInterval() {
925
+ return this.#h;
926
+ }
927
+ /**
928
+ * Update viewport padding at runtime. Refits the visible time range to
929
+ * current data bounds **only when horizontal padding (left/right) changes**
930
+ * — vertical padding only affects the Y-range computation, so touching it
931
+ * shouldn't reset the user's zoom / auto-scroll state. This matters when
932
+ * a wrapper re-applies padding reactively (e.g. in response to a Title /
933
+ * InfoBar ResizeObserver).
934
+ */
935
+ setPadding(i) {
936
+ const t = this.#l;
937
+ this.#l = D(i);
938
+ const e = this.#l, s = !C(t.left, e.left) || !C(t.right, e.right), r = t.top !== e.top || t.bottom !== e.bottom;
939
+ if (s) {
940
+ const { first: n, last: h } = this.getDataBounds();
941
+ if (n !== void 0 && h !== void 0) {
942
+ const a = this.#n.size.media.width - this.yAxisWidth;
943
+ this.#M(n, h, a);
944
+ }
945
+ }
946
+ if (s || r) {
947
+ this.#u = !0;
948
+ const n = performance.now();
949
+ this.#e.onAxisReconfig(n), this.#y(n);
950
+ }
951
+ this.syncScales(), this.#i.markDirty(), r && this.emit("viewportChange");
952
+ }
953
+ /** Show or hide the background grid. Takes effect on the next render frame. */
954
+ setGrid(i) {
955
+ this.#E = i.visible, this.#i.markDirty();
956
+ }
957
+ /**
958
+ * Set the visual state for one side of the chart. Typically called in
959
+ * response to the `onEdgeReached` callback:
960
+ * - `loading` while a history fetch is in flight,
961
+ * - `no-data` once the fetch confirmed there's nothing more,
962
+ * - `idle` when the host no longer wants any edge affordance.
963
+ * The state persists until replaced. `has-more` is accepted for API
964
+ * symmetry and currently renders identically to `idle`.
965
+ */
966
+ setEdgeState(i, t) {
967
+ this.#b[i] !== t && (this.#b[i] = t, this.#d.markDirty());
968
+ }
969
+ /** Read the current host-declared state for a given edge. */
970
+ getEdgeState(i) {
971
+ return this.#b[i];
972
+ }
973
+ /**
974
+ * Notify chart that a YLabel overlay is mounted / unmounted. Currently a
975
+ * no-op — placeholder for the right-padding reflow that will adjust the
976
+ * chart area to make room for the badge.
977
+ * TODO: implement reflow.
978
+ */
979
+ setYLabel(i) {
980
+ }
981
+ /**
982
+ * Dispatch a crosshair move to every renderer that supports spatial hover
983
+ * (currently: pie). Any change in hover index schedules a main-layer redraw.
984
+ */
985
+ #ft(i) {
986
+ const t = this.#n.size, e = this.#l, s = {
987
+ top: e.top * t.verticalPixelRatio,
988
+ bottom: e.bottom * t.verticalPixelRatio
989
+ };
990
+ let r = !1;
991
+ for (const n of this.#t) {
992
+ if (!n.renderer.hitTest || !n.renderer.setHoverIndex) continue;
993
+ let h = -1;
994
+ if (i) {
995
+ const a = i.mediaX * t.horizontalPixelRatio, d = i.mediaY * t.verticalPixelRatio;
996
+ h = n.renderer.hitTest(a, d, t.bitmap.width, t.bitmap.height, s);
997
+ }
998
+ n.renderer.setHoverIndex(h) && (r = !0);
999
+ }
1000
+ r && this.#i.markDirty();
1001
+ }
1002
+ /** Tear down the chart: cancel animations, remove listeners, and detach the canvas. */
1003
+ destroy() {
1004
+ for (const i of this.#t) i.renderer.dispose();
1005
+ this.#t = [], this.#c = [], this.#v = [], this.#g = [], this.#A = null, this.#i.destroy(), this.#d.destroy(), this.#W?.destroy(), this.#n.destroy(), this.#F?.destroy(), this.#F = null, this.#B && this.#x?.destroy(), this.#x = null, this.#B = !1, this.removeAllListeners();
1006
+ }
1007
+ /** The attached performance monitor, or `null` when instrumentation is disabled. */
1008
+ getPerfMonitor() {
1009
+ return this.#x;
1010
+ }
1011
+ /** Compute the earliest and latest timestamps across all series. */
1012
+ getDataBounds() {
1013
+ let i, t;
1014
+ for (const e of this.#t) {
1015
+ if (!e.visible || e.renderer.kind === "pie") continue;
1016
+ const s = e.renderer.getTimeBounds();
1017
+ s !== null && ((i === void 0 || s.first < i) && (i = s.first), (t === void 0 || s.last > t) && (t = s.last));
1018
+ }
1019
+ return { first: i, last: t };
1020
+ }
1021
+ /** Set by `setSeriesData`/bulk-replace paths so the next `onDataChanged`
1022
+ * snaps the Y range (instead of easing) and `yScale.getRange()` reflects
1023
+ * the new domain synchronously. Cleared by `onDataChanged` after each run. */
1024
+ #Y = !1;
1025
+ onDataChanged() {
1026
+ if (this.#w > 0) {
1027
+ this.#$ = !0;
1028
+ return;
1029
+ }
1030
+ this.updateDataInterval();
1031
+ const { first: i, last: t } = this.getDataBounds();
1032
+ i !== void 0 && (this.#k = i), t !== void 0 && (this.#P = this.#s, this.#s = t);
1033
+ const e = this.#Y;
1034
+ this.#Y = !1;
1035
+ const s = !this.#u && i !== void 0;
1036
+ e && (this.yScale.tickTracker.reset(), this.timeScale.tickTracker.reset()), this.#K = e;
1037
+ const r = performance.now();
1038
+ e || s ? (this.#D.seed(r), this.#e.onDataReplaced(r)) : this.#e.onPointAppended(r), this.#e.lastXTarget !== null && (this.#R = !0), i !== void 0 && (this.#u = !0), this.#y(r), this.syncScales(), s ? this.#U() : this.#i.markDirty(), this.emit("dataUpdate"), this.#S();
1039
+ }
1040
+ updateDataInterval() {
1041
+ for (const i of this.#t) {
1042
+ if (i.renderer.kind === "pie") continue;
1043
+ const t = i.renderer.sampleTimes(20);
1044
+ if (t.length >= 2) {
1045
+ this.#h = lt(t);
1046
+ break;
1047
+ }
1048
+ }
1049
+ }
1050
+ /** Whether the Y range has been initialized (first snap vs smooth lerp).
1051
+ * Pre-init Y emits are forced to `instant` so the first paint snaps to
1052
+ * data regardless of the caller's requested kind. Also reset by paths
1053
+ * that intentionally invalidate the current Y baseline (axis bound
1054
+ * change, empty viewport). */
1055
+ #u = !1;
1056
+ /** Whether the engine's X slot has received its first emit. Pre-init X
1057
+ * emits are forced to `instant` so first paint snaps to the fitted
1058
+ * range — without this guard the engine would ease from `{0,0}` into
1059
+ * the new target and the very first frame would render with a
1060
+ * zero-width X domain. */
1061
+ #R = !1;
1062
+ /**
1063
+ * One-shot `setVisibleRange` argument applied on the first data arrival.
1064
+ * Captured at chart construction from {@link ChartOptions.viewport.initialRange};
1065
+ * cleared the moment it's consumed so subsequent data swaps don't snap
1066
+ * back to the initial window.
1067
+ */
1068
+ #H;
1069
+ /** Last Y bounds the engine resolved into {@link AnimationState.yRange}.
1070
+ * Used by {@link renderMain} to detect Y movement frame-to-frame so
1071
+ * `viewportChange` only emits when the Y range actually shifted — DOM
1072
+ * axis labels subscribe to that event and re-renders on every tick are
1073
+ * expensive. Initialised to NaN so the first real Y emit always passes
1074
+ * the change check. */
1075
+ #X = Number.NaN;
1076
+ #_ = Number.NaN;
1077
+ /**
1078
+ * Sample the visible data window and return the resolved Y bounds, or
1079
+ * `null` when no series has data inside the X destination range.
1080
+ * Sampling runs against `logicalRange` (the X target, not the animating
1081
+ * current), so Y stays on a stable target while X eases — both dimensions
1082
+ * converge together. Clears {@link #yInited} on empty so the next emit
1083
+ * with valid data lands as an instant snap.
1084
+ */
1085
+ #st(i) {
1086
+ const t = i ?? this.#o, e = this.#a.min !== void 0 && this.#a.min !== "auto" && typeof this.#a.min != "number" || this.#a.max !== void 0 && this.#a.max !== "auto" && typeof this.#a.max != "number" ? [] : null, s = q(t, this.#t, e);
1087
+ if (s === null)
1088
+ return this.#u = !1, null;
1089
+ const r = R(this.#a.min, s.min, s.max, e ?? [], "min"), n = R(this.#a.max, s.max, s.min, e ?? [], "max");
1090
+ return { min: r, max: n };
1091
+ }
1092
+ /**
1093
+ * Engine pull-on-demand X target. Called by the engine's `computeXTarget`
1094
+ * closure during `engine.onPointAppended` / `engine.onDataReplaced`.
1095
+ * Reads `#dataStart` / `#dataEnd` (refreshed by `onDataChanged` just
1096
+ * before the engine signal) plus `#pendingFitToData` flag, then dispatches:
1097
+ *
1098
+ * - **uninit (viewport never committed)** → fit to data, apply any
1099
+ * one-shot `initialVisibleRange`, return the new logical range.
1100
+ * First-paint fit.
1101
+ * - **bulk replace + autoscroll** (`#pendingFitToData`) → fit to data,
1102
+ * return logical. `chart.setSeriesData(...)` lands here.
1103
+ * - **streaming append + autoscroll** → `computeStreamingTarget` (preserves
1104
+ * any pan offset across ticks via `#prevDataEnd`). The engine's X spring
1105
+ * absorbs the new target — velocity carries across retargets so a burst
1106
+ * of ticks doesn't restart easing on each one.
1107
+ * - **autoscroll off** → `null` (no X retarget; user has panned away
1108
+ * from the tail).
1109
+ */
1110
+ #gt() {
1111
+ if (this.#k === null || this.#s === null) return null;
1112
+ const { from: i, to: t } = this.#o, e = i === 0 && t === 0, s = this.#n.size.media.width - this.yAxisWidth;
1113
+ if (e)
1114
+ return this.#M(this.#k, this.#s, s), this.#H !== void 0 && (this.setVisibleRange(this.#H), this.#H = void 0), this.#o;
1115
+ if (!this.#p) return null;
1116
+ if (this.#K)
1117
+ return this.#M(this.#k, this.#s, s), this.#o;
1118
+ const r = j({
1119
+ currentLogical: this.#o,
1120
+ lastTime: this.#s,
1121
+ prevDataEnd: this.#P,
1122
+ dataInterval: this.#h,
1123
+ paddingRight: this.#l.right,
1124
+ chartWidth: s,
1125
+ holdUntilFilled: this.#L
1126
+ });
1127
+ return r.releaseHold && (this.#L = !1), r.reengageAutoScroll && (this.#p = !0), r.newLogical === null ? null : (this.#D.observe(performance.now()), this.#e.setXSettleMs(this.#D.pickSettleMs(this.#f.axis.x.settleMs)), this.#C(r.newLogical, { emitChange: !1, skipValidation: !0 }), this.#P = this.#s, r.newLogical);
1128
+ }
1129
+ /**
1130
+ * Fit the viewport's logical X range around a data span. Mirrors the
1131
+ * old `viewport.fitToData` side effects: clears the warm-up hold,
1132
+ * forces autoscroll on (data tail is in the new window by construction),
1133
+ * commits the new range via `viewport.setRange` (which emits `change`).
1134
+ */
1135
+ #M(i, t, e) {
1136
+ const s = this.#l, r = O({
1137
+ firstTime: i,
1138
+ lastTime: t,
1139
+ dataInterval: this.#h,
1140
+ maxVisibleBars: this.#Q,
1141
+ chartWidth: e,
1142
+ padding: { left: s.left, right: s.right }
1143
+ });
1144
+ this.#L = !1, this.#p = !0, this.#C(r, { emitChange: !0 });
1145
+ }
1146
+ /**
1147
+ * Commit a programmatic zoom — `fitContent` or `setVisibleRange`. Reads
1148
+ * the viewport's just-committed logical range as the X target; pulls
1149
+ * the matching Y target via the engine's `computeYTarget` callback so
1150
+ * the axis re-fits to the new window without the chart having to wire
1151
+ * Y separately.
1152
+ *
1153
+ * - First paint (`#xInited` or `#yInited` not yet set) → both axes snap
1154
+ * so consumers reading `chart.getYRange()` / `chart.getVisibleRange()`
1155
+ * synchronously after mount see the final values.
1156
+ * - Otherwise X snaps and Y eases on the sticky baseline. `fitContent`
1157
+ * passes `xEase: true` so X eases instead — viewport and axis converge
1158
+ * on the same frame.
1159
+ */
1160
+ #N(i) {
1161
+ const t = this.#o;
1162
+ if (t.to <= t.from) return;
1163
+ const e = performance.now();
1164
+ if (!this.#R || !this.#u) {
1165
+ this.#rt(e);
1166
+ return;
1167
+ }
1168
+ this.#e.onProgrammaticZoom({ xTarget: t, xEase: i?.xEase }, e), this.#y(e);
1169
+ }
1170
+ /**
1171
+ * Commit the current logical X to the engine on the *gesture* profile:
1172
+ * X and Y both ease over `animations.axis.{x,y}.gesture` via `onPanZoom`,
1173
+ * exactly the path a user pan/zoom takes. Contrast {@link
1174
+ * #commitProgrammaticZoom}, which snaps X and eases Y on the slower
1175
+ * sticky-Y baseline.
1176
+ *
1177
+ * Used by `setVisibleRange(..., { gesture: true })` for multi-chart sync,
1178
+ * so a synced pane re-fits its Y in lockstep with the originating gesture
1179
+ * instead of lagging on the long sticky contract. First paint still snaps
1180
+ * (same as the programmatic path) so synchronous readers see final values.
1181
+ */
1182
+ #pt() {
1183
+ const i = this.#o;
1184
+ if (i.to <= i.from) return;
1185
+ const t = performance.now();
1186
+ if (!this.#R || !this.#u) {
1187
+ this.#rt(t);
1188
+ return;
1189
+ }
1190
+ this.#e.onPanZoom({ xTarget: i, yAuto: !0 }, t), this.#y(t);
1191
+ }
1192
+ /**
1193
+ * First-paint snap: land both axes on the current logical X immediately
1194
+ * (Y via {@link #computeYTarget}) and flip the init flags so later commits
1195
+ * ease instead of snapping. Shared by the programmatic and gesture zoom
1196
+ * commits — both must snap before the chart has painted once so a
1197
+ * synchronous `getYRange()` / `getVisibleRange()` read sees final values.
1198
+ */
1199
+ #rt(i) {
1200
+ const t = this.#o, e = this.#st(t);
1201
+ this.#R = !0, e !== null && (this.#u = !0), this.#e.snap({ x: t, y: e ?? void 0 }, i), this.#y(i);
1202
+ }
1203
+ /**
1204
+ * Validate and commit a new logical X range. Mirrors the old
1205
+ * `Viewport.applyLogical` contract: silently no-ops on invalid input
1206
+ * (`to <= from`, fewer than 2 bars), then writes `#logical` and (if
1207
+ * `opts.emitChange` is true) wakes the render loop + emits
1208
+ * `viewportChange` for DOM subscribers.
1209
+ *
1210
+ * Streaming-target writes pass `emitChange: false` because the engine
1211
+ * drives the animation: `engine.onWake` already markedDirty, and the
1212
+ * follow-up `renderMain` syncs scales — a redundant `viewportChange`
1213
+ * emit here would just trigger a wasted React re-render per stream tick.
1214
+ *
1215
+ * Returns `true` when the range actually changed (passed validation and
1216
+ * differed from the current logical), `false` on a no-op. Callers that
1217
+ * drive the engine afterwards can skip a redundant retarget on `false` —
1218
+ * see `setVisibleRange`'s gesture path, where a per-frame echo would
1219
+ * otherwise re-arm and stall the in-flight Y ease.
1220
+ */
1221
+ #C(i, t) {
1222
+ const { from: e, to: s } = i;
1223
+ return !Number.isFinite(e) || !Number.isFinite(s) || s <= e || !t.skipValidation && (s - e) / this.#h < 2 || this.#o.from === e && this.#o.to === s ? !1 : (this.#o = { from: e, to: s }, t.emitChange && (this.syncScales(), this.#i?.markDirty(), this.emit("viewportChange")), !0);
1224
+ }
1225
+ /**
1226
+ * Apply pixel-padding to the engine's raw Y target and store the
1227
+ * result in `#yRange`. Mirrors the old `Viewport.setYRange` contract:
1228
+ * symmetric pad top / bottom from `#padding`, suppressed on the side
1229
+ * that has an explicit (fixed) axis bound.
1230
+ */
1231
+ #nt(i, t, e, s, r) {
1232
+ const n = t - i, h = e > 0 ? this.#l.top / e * n : 0, a = e > 0 ? this.#l.bottom / e * n : 0;
1233
+ this.#V = {
1234
+ min: s ? i : i - a,
1235
+ max: r ? t : t + h
1236
+ };
1237
+ }
1238
+ /**
1239
+ * Shift the visible time range by `timeDelta` ms. Hard-clamped so at
1240
+ * least one data point stays on screen — no rubber-band, no overshoot.
1241
+ * A pan that moves the viewport off the live-tail position flips
1242
+ * autoscroll off. Fires `edgeReached` whenever the clamp trims the
1243
+ * gesture (i.e., the user pushed past the boundary), so consumers can
1244
+ * trigger history loads. Forwards the committed target to the engine
1245
+ * as a gesture for visual easing. Public so consumers can drive pan
1246
+ * programmatically.
1247
+ */
1248
+ pan(i, t = this.#z) {
1249
+ t > 0 && (this.#z = t), this.#L = !1;
1250
+ const e = U({
1251
+ currentLogical: this.#o,
1252
+ timeDelta: i,
1253
+ chartWidth: t,
1254
+ dataInterval: this.#h,
1255
+ padding: { left: this.#l.left, right: this.#l.right },
1256
+ dataStart: this.#k,
1257
+ dataEnd: this.#s
1258
+ });
1259
+ e.newLogical !== null && (this.#p = !e.autoScrollOff, this.#C(e.newLogical, { emitChange: !0 }), e.edgeReached !== null && (this.#j[e.edgeReached.side] = e.edgeReached.boundaryTime, this.#q?.(e.edgeReached)), this.#D.pause(), this.#ht());
1260
+ }
1261
+ /**
1262
+ * Zoom around a time anchor. `factor < 1` zooms in, `> 1` zooms out.
1263
+ * In historical mode the window anchors to `centerTime` (cursor-driven).
1264
+ * Zoom-out is hard-capped at the padded data span. Forwards the committed
1265
+ * target to the engine as a gesture for visual easing. Public so consumers
1266
+ * can drive zoom programmatically.
1267
+ *
1268
+ * Sticky-follow: while `#autoScroll` is on, the new window is repositioned
1269
+ * so its right edge sits at `dataEnd + paddingRight` (follow position).
1270
+ * Span (zoom level) from `computeZoom` is preserved; only the position
1271
+ * is locked to the tail. This avoids the next-tick `computeStreamingTarget`
1272
+ * offset clamp (which would otherwise slide X left to the follow position
1273
+ * and produce a visible jump). Cursor-anchored zoom is intentionally
1274
+ * sacrificed in follow-live mode — pan first to inspect history.
1275
+ */
1276
+ zoomAt(i, t, e = this.#z) {
1277
+ e > 0 && (this.#z = e), this.#L = !1;
1278
+ const s = Z({
1279
+ currentLogical: this.#o,
1280
+ centerTime: i,
1281
+ factor: t,
1282
+ chartWidth: e,
1283
+ dataInterval: this.#h,
1284
+ padding: { left: this.#l.left, right: this.#l.right },
1285
+ dataStart: this.#k,
1286
+ dataEnd: this.#s
1287
+ });
1288
+ if (s.newLogical === null) return;
1289
+ let r = s.newLogical;
1290
+ if (this.#p && this.#s !== null) {
1291
+ const n = r.to - r.from, h = T(this.#l.right, n, this.#h, e), a = this.#s + h;
1292
+ r = { from: a - n, to: a }, this.#P = this.#s;
1293
+ }
1294
+ this.#C(r, { emitChange: !0 }), this.#D.pause(), this.#ht();
1295
+ }
1296
+ /**
1297
+ * Common tail for `pan` / `zoomAt`: routes the just-committed `#logical`
1298
+ * to the engine as a gesture so the X spring retargets and the Y spring
1299
+ * eases over `animations.axis.y.gesture`.
1300
+ */
1301
+ #ht() {
1302
+ const i = this.#o;
1303
+ if (i.to <= i.from) return;
1304
+ this.#R = !0, this.#u = !0;
1305
+ const t = performance.now();
1306
+ this.#e.onPanZoom({ xTarget: i, yAuto: !0 }, t), this.#y(t);
1307
+ }
1308
+ /**
1309
+ * Tick the engine at the given wall clock and push the resolved X / Y
1310
+ * ranges into the viewport + scales. Called from every emit path so that
1311
+ * synchronous readers (`chart.getYRange()`, `chart.getVisibleRange()`
1312
+ * immediately after `setSeriesData` / `setSeriesVisible`) see the new
1313
+ * values without waiting for the next RAF — matches the legacy contract
1314
+ * pinned by `data-update-sync`, `visibility-batch`, and the `y-label`
1315
+ * repositioning tests.
1316
+ *
1317
+ * The caller passes the same wall it used as the event's `startWall` so
1318
+ * `effectiveNow === startWall` and the prune step keeps zero-duration
1319
+ * events long enough for the slot processor to snap them.
1320
+ */
1321
+ #y(i) {
1322
+ const t = this.#e.tick(i), e = this.#n.size;
1323
+ if (e.media.width === 0 || e.media.height === 0) return;
1324
+ const s = this.#a.min !== void 0 && this.#a.min !== "auto", r = this.#a.max !== void 0 && this.#a.max !== "auto", n = e.media.height - this.xAxisHeight;
1325
+ this.#nt(t.yRange.min, t.yRange.max, n, s, r), this.#X = t.yRange.min, this.#_ = t.yRange.max, this.syncScales();
1326
+ }
1327
+ /**
1328
+ * Lightweight scale sync: updates timeScale/yScale from current viewport state
1329
+ * without advancing the Y smoothing animation. Called from the viewport 'change'
1330
+ * handler, so DOM axis components always read fresh coordinates on re-render.
1331
+ */
1332
+ syncScales() {
1333
+ const i = this.#n.size;
1334
+ if (i.media.width === 0 || i.media.height === 0) return;
1335
+ const t = i.media.width - this.yAxisWidth, e = i.media.height - this.xAxisHeight;
1336
+ this.timeScale.update(
1337
+ this.#e.getAnimationState().xRange,
1338
+ t,
1339
+ i.horizontalPixelRatio,
1340
+ this.#h
1341
+ ), this.yScale.update(this.#V, e, i.verticalPixelRatio);
1342
+ }
1343
+ /** Expensive: background, grid, all series. Only on data/viewport/resize change. */
1344
+ renderMain(i) {
1345
+ const t = this.#n.size;
1346
+ if (t.media.width === 0 || t.media.height === 0) return;
1347
+ const e = typeof i == "number" ? i : performance.now(), s = this.#e.tick(e);
1348
+ s.animating && this.#i.markDirty();
1349
+ const r = s.yRange.min, n = s.yRange.max, h = r !== this.#X || n !== this.#_;
1350
+ if (h) {
1351
+ const c = this.#a.min !== void 0 && this.#a.min !== "auto", o = this.#a.max !== void 0 && this.#a.max !== "auto", f = t.media.height - this.xAxisHeight;
1352
+ this.#nt(r, n, f, c, o), this.#X = r, this.#_ = n;
1353
+ }
1354
+ if (this.syncScales(), this.#s !== null && !this.#p) {
1355
+ const c = this.#e.lastXTarget ?? this.#o, o = c.to - c.from;
1356
+ if (o > 0) {
1357
+ const f = this.#n.size.media.width - this.yAxisWidth, y = T(this.#l.right, o, this.#h, f), v = this.#h * 0.5;
1358
+ Math.abs(c.to - (this.#s + y)) < v && (this.#p = !0);
1359
+ }
1360
+ }
1361
+ h && this.emit("viewportChange");
1362
+ let a = !1, d = !1;
1363
+ this.#n.useMainLayer((c) => {
1364
+ const { context: o, bitmapSize: f } = c, y = (t.media.width - this.yAxisWidth) * t.horizontalPixelRatio, v = (t.media.height - this.xAxisHeight) * t.verticalPixelRatio;
1365
+ o.clearRect(0, 0, f.width, f.height), o.save(), o.beginPath(), o.rect(0, 0, y, v), o.clip(), this.yScale.tickTracker.setCurrentTicks(this.yScale.niceTickValues()), this.timeScale.tickTracker.setCurrentTicks(this.timeScale.niceTickValues(this.#h).ticks), this.yScale.tickTracker.tick(e), this.timeScale.tickTracker.tick(e);
1366
+ const S = this.yScale.tickTracker.snapshot(), b = this.timeScale.tickTracker.snapshot();
1367
+ a = S.isAnimating, d = b.isAnimating, (S.isAnimating || b.isAnimating || s.animating) && this.emit("tickFrame"), this.#E && K({
1368
+ scope: c,
1369
+ timeScale: this.timeScale,
1370
+ yScale: this.yScale,
1371
+ theme: this.#r,
1372
+ yTicks: S,
1373
+ timeTicks: b
1374
+ }), this.#ct(c, y, v);
1375
+ const k = this.#l, P = { top: k.top, bottom: k.bottom }, w = this.#x;
1376
+ for (const p of this.#t) {
1377
+ const x = p.renderer.getAlpha?.() ?? (p.visible ? 1 : 0);
1378
+ if (x <= 0) continue;
1379
+ const L = {
1380
+ scope: c,
1381
+ timeScale: this.timeScale,
1382
+ yScale: this.yScale,
1383
+ theme: this.#r,
1384
+ dataInterval: this.#h,
1385
+ padding: P,
1386
+ state: s,
1387
+ seriesId: p.id
1388
+ }, A = o.globalAlpha;
1389
+ if (x < 1 && (o.globalAlpha = A * x), w) {
1390
+ const V = performance.now();
1391
+ p.renderer.render(L), w.recordSeries(p.id, performance.now() - V, e);
1392
+ } else
1393
+ p.renderer.render(L);
1394
+ x < 1 && (o.globalAlpha = A);
1395
+ }
1396
+ o.restore();
1397
+ });
1398
+ let l = !1;
1399
+ for (const c of this.#t)
1400
+ if (c.renderer.needsAnimation) {
1401
+ l = !0, this.#i.markDirty();
1402
+ break;
1403
+ }
1404
+ const u = a || d;
1405
+ !s.animating && !l && !u && (this.yScale.tickTracker.markArmed(), this.timeScale.tickTracker.markArmed());
1406
+ }
1407
+ /**
1408
+ * Paint a full frame: the main layer, then the overlay synchronously on
1409
+ * top so both land in the same browser paint. The overlay scheduler's
1410
+ * queued frame (a crosshair move, the pulse re-arm from the previous
1411
+ * frame) is consumed first — the synchronous paint below replaces it, so
1412
+ * the overlay is no longer cleared + repainted twice in the same frame.
1413
+ * `markDirty` calls made *during* `renderOverlay` (pulse, edge spinner)
1414
+ * land after the consume and re-arm cleanly for the next frame.
1415
+ */
1416
+ #U(i) {
1417
+ this.#Z("main", i, () => this.renderMain(i)), this.#d.consume(), this.#Z("overlay", i, () => this.renderOverlay(i));
1418
+ }
1419
+ /** Run a layer's paint, booking duration + draw calls to the perf monitor
1420
+ * when one is attached — keeps overlay time out of the `main` samples. */
1421
+ #Z(i, t, e) {
1422
+ const s = this.#x;
1423
+ if (!s) {
1424
+ e();
1425
+ return;
1426
+ }
1427
+ s.resetDrawCalls(i);
1428
+ const r = performance.now();
1429
+ e(), s.recordFrame(i, performance.now() - r, t ?? r);
1430
+ }
1431
+ /** Cheap overlay: crosshair, nearest-point dots, pulse animation, edge indicator. */
1432
+ renderOverlay(i) {
1433
+ const t = this.#n.size;
1434
+ if (t.media.width === 0 || t.media.height === 0) return;
1435
+ let e = !1;
1436
+ for (const h of this.#t)
1437
+ if (h.visible && h.renderer.overlayNeedsAnimation) {
1438
+ e = !0;
1439
+ break;
1440
+ }
1441
+ this.#c.some((h) => h.pulse) && (e = !0);
1442
+ const s = this.#b.left === "loading" || this.#b.right === "loading", r = this.#b.left !== "idle" || this.#b.right !== "idle", n = !!this.#m || e || r || this.#c.length > 0 || this.#g.length > 0;
1443
+ if (!(!n && !this.#G)) {
1444
+ if (this.#G = n, this.#n.useOverlayLayer((h) => {
1445
+ if (!n) return;
1446
+ const a = (t.media.width - this.yAxisWidth) * t.horizontalPixelRatio, d = (t.media.height - this.xAxisHeight) * t.verticalPixelRatio;
1447
+ if (h.context.save(), h.context.beginPath(), h.context.rect(0, 0, a, d), h.context.clip(), this.#m && this.#ut()) {
1448
+ const o = this.#m.mediaX * t.horizontalPixelRatio, f = this.#m.mediaY * t.verticalPixelRatio;
1449
+ J(h, o, f, this.#r);
1450
+ }
1451
+ const l = this.#l, u = { top: l.top, bottom: l.bottom }, c = this.#e.getAnimationState();
1452
+ for (const o of this.#t)
1453
+ o.visible && o.renderer.drawOverlay?.({
1454
+ scope: h,
1455
+ timeScale: this.timeScale,
1456
+ yScale: this.yScale,
1457
+ theme: this.#r,
1458
+ dataInterval: this.#h,
1459
+ padding: u,
1460
+ crosshair: this.#m,
1461
+ state: c,
1462
+ seriesId: o.id
1463
+ });
1464
+ this.#mt(h, a, d), this.#dt(h), r && E({
1465
+ scope: h,
1466
+ chartMediaHeight: t.media.height - this.xAxisHeight,
1467
+ timeScale: this.timeScale,
1468
+ theme: this.#r,
1469
+ edgeStates: this.#b,
1470
+ resolveBoundary: (o) => $(o, this.#j[o], this.getDataBounds())
1471
+ }), h.context.restore();
1472
+ }), e) {
1473
+ const { from: h, to: a } = this.timeScale.getRange();
1474
+ let d = !1;
1475
+ for (const l of this.#t)
1476
+ if (!(!l.visible || !l.renderer.overlayNeedsAnimation) && (l.renderer.hasOverlayContentInRange?.(h, a) ?? !0)) {
1477
+ d = !0;
1478
+ break;
1479
+ }
1480
+ if (!d) {
1481
+ for (const l of this.#c)
1482
+ if (l.pulse && l.time >= h && l.time <= a) {
1483
+ d = !0;
1484
+ break;
1485
+ }
1486
+ }
1487
+ d && this.#d.markDirty();
1488
+ }
1489
+ s && this.#d.markDirty();
1490
+ }
1491
+ }
1492
+ }
1493
+ export {
1494
+ Ot as ChartInstance
1495
+ };