@wick-charts/vue 0.4.2 → 0.5.1

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