@wick-charts/vue 0.4.2 → 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.
- package/README.md +5 -3
- package/dist/BarSeries.vue.js +44 -0
- package/dist/BarSeries.vue2.js +4 -0
- package/dist/CandlestickSeries.vue.js +45 -0
- package/dist/CandlestickSeries.vue2.js +4 -0
- package/dist/ChartContainer.vue.js +168 -0
- package/dist/ChartContainer.vue2.js +4 -0
- package/dist/LineSeries.vue.js +44 -0
- package/dist/LineSeries.vue2.js +4 -0
- package/dist/PieSeries.vue.js +34 -0
- package/dist/PieSeries.vue2.js +4 -0
- package/dist/composables.js +44 -0
- package/dist/context.js +25 -0
- package/dist/core/dist/animation/animator.js +72 -0
- package/dist/core/dist/animation/config.js +145 -0
- package/dist/core/dist/animation/easing.js +4 -0
- package/dist/core/dist/animation/scalar-spring.js +57 -0
- package/dist/core/dist/animation/spring.js +11 -0
- package/dist/core/dist/animation/time.js +19 -0
- package/dist/core/dist/animation/viewport-engine.js +174 -0
- package/dist/core/dist/animation/visible-range-spring.js +64 -0
- package/dist/core/dist/animation/y-range-hermite.js +72 -0
- package/dist/core/dist/animation/y-range-snap.js +31 -0
- package/dist/core/dist/animation/y-range-spring.js +59 -0
- package/dist/core/dist/axis/dom-labels.js +60 -0
- package/dist/core/dist/canvas-manager.js +109 -0
- package/dist/core/dist/chart/edge-indicators.js +26 -0
- package/dist/core/dist/chart/fit-to-data.js +9 -0
- package/dist/core/dist/chart/last-value.js +31 -0
- package/dist/core/dist/chart/options.js +35 -0
- package/dist/core/dist/chart/pan-zoom-math.js +66 -0
- package/dist/core/dist/chart/streaming-cadence.js +70 -0
- package/dist/core/dist/chart/streaming-target.js +22 -0
- package/dist/core/dist/chart/viewport-padding.js +6 -0
- package/dist/core/dist/chart/y-target.js +24 -0
- package/dist/core/dist/chart.js +1495 -0
- package/dist/core/dist/components/crosshair.js +9 -0
- package/dist/core/dist/components/edge-indicator.js +36 -0
- package/dist/core/dist/components/grid.js +22 -0
- package/dist/core/dist/components/marker.js +41 -0
- package/dist/core/dist/components/reference-line.js +48 -0
- package/dist/core/dist/components/time-region.js +35 -0
- package/dist/core/dist/data/decimation.js +54 -0
- package/dist/core/dist/data/store.js +76 -0
- package/dist/core/dist/data/sync.js +47 -0
- package/dist/core/dist/events.js +19 -0
- package/dist/core/dist/interactions/handler.js +84 -0
- package/dist/core/dist/interactions/pan.js +26 -0
- package/dist/core/dist/interactions/zoom.js +21 -0
- package/dist/core/dist/navigator/controller.js +374 -0
- package/dist/core/dist/navigator/decimate.js +44 -0
- package/dist/core/dist/navigator/interactions.js +28 -0
- package/dist/core/dist/navigator/render.js +68 -0
- package/dist/core/dist/perf/counting-context.js +19 -0
- package/dist/core/dist/perf/perf-hud.js +453 -0
- package/dist/core/dist/perf/perf-monitor.js +132 -0
- package/dist/core/dist/render-scheduler.js +31 -0
- package/dist/core/dist/scales/tick-tracker.js +110 -0
- package/dist/core/dist/scales/x-scale.js +145 -0
- package/dist/core/dist/scales/y-scale.js +156 -0
- package/dist/core/dist/series/bar.js +341 -0
- package/dist/core/dist/series/base-multi-layer.js +438 -0
- package/dist/core/dist/series/candlestick.js +481 -0
- package/dist/core/dist/series/definition.js +11 -0
- package/dist/core/dist/series/line.js +446 -0
- package/dist/core/dist/series/painters/builtins.js +36 -0
- package/dist/core/dist/series/painters/canvas-path.js +79 -0
- package/dist/core/dist/series/painters/resolve.js +32 -0
- package/dist/core/dist/series/pie.js +545 -0
- package/dist/core/dist/series/register-builtins.js +11 -0
- package/dist/core/dist/series/stack-math.js +17 -0
- package/dist/core/dist/snapshots.js +107 -0
- package/dist/core/dist/theme/create.js +176 -0
- package/dist/core/dist/theme/resolve.js +14 -0
- package/dist/core/dist/theme/themes/andromeda.js +33 -0
- package/dist/core/dist/theme/themes/ayu-mirage.js +33 -0
- package/dist/core/dist/theme/themes/catppuccin.js +33 -0
- package/dist/core/dist/theme/themes/dracula.js +35 -0
- package/dist/core/dist/theme/themes/github-light.js +34 -0
- package/dist/core/dist/theme/themes/gruvbox.js +33 -0
- package/dist/core/dist/theme/themes/handwritten.js +101 -0
- package/dist/core/dist/theme/themes/high-contrast.js +33 -0
- package/dist/core/dist/theme/themes/lavender-mist.js +33 -0
- package/dist/core/dist/theme/themes/light-pink.js +34 -0
- package/dist/core/dist/theme/themes/material-palenight.js +33 -0
- package/dist/core/dist/theme/themes/minimal-light.js +34 -0
- package/dist/core/dist/theme/themes/mint-breeze.js +33 -0
- package/dist/core/dist/theme/themes/monokai-pro.js +33 -0
- package/dist/core/dist/theme/themes/night-owl.js +35 -0
- package/dist/core/dist/theme/themes/one-dark-pro.js +33 -0
- package/dist/core/dist/theme/themes/panda.js +33 -0
- package/dist/core/dist/theme/themes/peach-cream.js +33 -0
- package/dist/core/dist/theme/themes/quiet-light.js +37 -0
- package/dist/core/dist/theme/themes/rose-pine-dawn.js +33 -0
- package/dist/core/dist/theme/themes/sand-dune.js +33 -0
- package/dist/core/dist/theme/themes/solarized-light.js +34 -0
- package/dist/core/dist/tooltip-position.js +10 -0
- package/dist/core/dist/utils/color.js +37 -0
- package/dist/core/dist/utils/format.js +29 -0
- package/dist/core/dist/utils/math.js +26 -0
- package/dist/core/dist/utils/poisoned-data-reporter.js +15 -0
- package/dist/core/dist/utils/time.js +41 -0
- package/dist/index.d.ts +827 -63
- package/dist/index.js +142 -8025
- package/dist/ui/Crosshair.vue.js +53 -0
- package/dist/ui/Crosshair.vue2.js +4 -0
- package/dist/ui/InfoBar.vue.js +143 -0
- package/dist/ui/InfoBar.vue2.js +4 -0
- package/dist/ui/Legend.vue.js +212 -0
- package/dist/ui/Legend.vue2.js +4 -0
- package/dist/ui/Marker.vue.js +47 -0
- package/dist/ui/Marker.vue2.js +4 -0
- package/dist/ui/Navigator.vue.js +58 -0
- package/dist/ui/Navigator.vue2.js +4 -0
- package/dist/ui/NumberFlow.vue.js +69 -0
- package/dist/ui/NumberFlow.vue2.js +4 -0
- package/dist/ui/PieLegend.vue.js +132 -0
- package/dist/ui/PieLegend.vue2.js +4 -0
- package/dist/ui/PieTooltip.vue.js +135 -0
- package/dist/ui/PieTooltip.vue2.js +4 -0
- package/dist/ui/ReferenceLine.vue.js +44 -0
- package/dist/ui/ReferenceLine.vue2.js +4 -0
- package/dist/ui/TimeAxis.vue.js +43 -0
- package/dist/ui/TimeAxis.vue2.js +4 -0
- package/dist/ui/TimeRegion.vue.js +41 -0
- package/dist/ui/TimeRegion.vue2.js +4 -0
- package/dist/ui/Title.vue.js +54 -0
- package/dist/ui/Title.vue2.js +4 -0
- package/dist/ui/Tooltip.vue.js +195 -0
- package/dist/ui/Tooltip.vue2.js +4 -0
- package/dist/ui/YAxis.vue.js +42 -0
- package/dist/ui/YAxis.vue2.js +4 -0
- package/dist/ui/YLabel.vue.js +105 -0
- package/dist/ui/YLabel.vue2.js +4 -0
- package/package.json +2 -2
- package/src/BarSeries.vue +12 -13
- package/src/CandlestickSeries.vue +17 -6
- package/src/LineSeries.vue +12 -13
- package/src/PieSeries.vue +2 -1
- package/src/index.ts +23 -0
- package/src/ui/Legend.vue +1 -2
- package/src/ui/Marker.vue +62 -0
- package/src/ui/ReferenceLine.vue +58 -0
- 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
|
+
};
|