@wick-charts/vue 0.4.1 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/README.md +5 -3
  2. package/dist/BarSeries.vue.js +44 -0
  3. package/dist/BarSeries.vue2.js +4 -0
  4. package/dist/CandlestickSeries.vue.js +45 -0
  5. package/dist/CandlestickSeries.vue2.js +4 -0
  6. package/dist/ChartContainer.vue.js +168 -0
  7. package/dist/ChartContainer.vue2.js +4 -0
  8. package/dist/LineSeries.vue.js +44 -0
  9. package/dist/LineSeries.vue2.js +4 -0
  10. package/dist/PieSeries.vue.js +34 -0
  11. package/dist/PieSeries.vue2.js +4 -0
  12. package/dist/composables.js +44 -0
  13. package/dist/context.js +25 -0
  14. package/dist/core/dist/animation/animator.js +72 -0
  15. package/dist/core/dist/animation/config.js +145 -0
  16. package/dist/core/dist/animation/easing.js +4 -0
  17. package/dist/core/dist/animation/scalar-spring.js +57 -0
  18. package/dist/core/dist/animation/spring.js +11 -0
  19. package/dist/core/dist/animation/time.js +19 -0
  20. package/dist/core/dist/animation/viewport-engine.js +174 -0
  21. package/dist/core/dist/animation/visible-range-spring.js +64 -0
  22. package/dist/core/dist/animation/y-range-hermite.js +72 -0
  23. package/dist/core/dist/animation/y-range-snap.js +31 -0
  24. package/dist/core/dist/animation/y-range-spring.js +59 -0
  25. package/dist/core/dist/axis/dom-labels.js +60 -0
  26. package/dist/core/dist/canvas-manager.js +109 -0
  27. package/dist/core/dist/chart/edge-indicators.js +26 -0
  28. package/dist/core/dist/chart/fit-to-data.js +9 -0
  29. package/dist/core/dist/chart/last-value.js +31 -0
  30. package/dist/core/dist/chart/options.js +35 -0
  31. package/dist/core/dist/chart/pan-zoom-math.js +66 -0
  32. package/dist/core/dist/chart/streaming-cadence.js +70 -0
  33. package/dist/core/dist/chart/streaming-target.js +22 -0
  34. package/dist/core/dist/chart/viewport-padding.js +6 -0
  35. package/dist/core/dist/chart/y-target.js +24 -0
  36. package/dist/core/dist/chart.js +1495 -0
  37. package/dist/core/dist/components/crosshair.js +9 -0
  38. package/dist/core/dist/components/edge-indicator.js +36 -0
  39. package/dist/core/dist/components/grid.js +22 -0
  40. package/dist/core/dist/components/marker.js +41 -0
  41. package/dist/core/dist/components/reference-line.js +48 -0
  42. package/dist/core/dist/components/time-region.js +35 -0
  43. package/dist/core/dist/data/decimation.js +54 -0
  44. package/dist/core/dist/data/store.js +76 -0
  45. package/dist/core/dist/data/sync.js +47 -0
  46. package/dist/core/dist/events.js +19 -0
  47. package/dist/core/dist/interactions/handler.js +84 -0
  48. package/dist/core/dist/interactions/pan.js +26 -0
  49. package/dist/core/dist/interactions/zoom.js +21 -0
  50. package/dist/core/dist/navigator/controller.js +374 -0
  51. package/dist/core/dist/navigator/decimate.js +44 -0
  52. package/dist/core/dist/navigator/interactions.js +28 -0
  53. package/dist/core/dist/navigator/render.js +68 -0
  54. package/dist/core/dist/perf/counting-context.js +19 -0
  55. package/dist/core/dist/perf/perf-hud.js +453 -0
  56. package/dist/core/dist/perf/perf-monitor.js +132 -0
  57. package/dist/core/dist/render-scheduler.js +31 -0
  58. package/dist/core/dist/scales/tick-tracker.js +110 -0
  59. package/dist/core/dist/scales/x-scale.js +145 -0
  60. package/dist/core/dist/scales/y-scale.js +156 -0
  61. package/dist/core/dist/series/bar.js +341 -0
  62. package/dist/core/dist/series/base-multi-layer.js +438 -0
  63. package/dist/core/dist/series/candlestick.js +481 -0
  64. package/dist/core/dist/series/definition.js +11 -0
  65. package/dist/core/dist/series/line.js +446 -0
  66. package/dist/core/dist/series/painters/builtins.js +36 -0
  67. package/dist/core/dist/series/painters/canvas-path.js +79 -0
  68. package/dist/core/dist/series/painters/resolve.js +32 -0
  69. package/dist/core/dist/series/pie.js +545 -0
  70. package/dist/core/dist/series/register-builtins.js +11 -0
  71. package/dist/core/dist/series/stack-math.js +17 -0
  72. package/dist/core/dist/snapshots.js +107 -0
  73. package/dist/core/dist/theme/create.js +176 -0
  74. package/dist/core/dist/theme/resolve.js +14 -0
  75. package/dist/core/dist/theme/themes/andromeda.js +33 -0
  76. package/dist/core/dist/theme/themes/ayu-mirage.js +33 -0
  77. package/dist/core/dist/theme/themes/catppuccin.js +33 -0
  78. package/dist/core/dist/theme/themes/dracula.js +35 -0
  79. package/dist/core/dist/theme/themes/github-light.js +34 -0
  80. package/dist/core/dist/theme/themes/gruvbox.js +33 -0
  81. package/dist/core/dist/theme/themes/handwritten.js +101 -0
  82. package/dist/core/dist/theme/themes/high-contrast.js +33 -0
  83. package/dist/core/dist/theme/themes/lavender-mist.js +33 -0
  84. package/dist/core/dist/theme/themes/light-pink.js +34 -0
  85. package/dist/core/dist/theme/themes/material-palenight.js +33 -0
  86. package/dist/core/dist/theme/themes/minimal-light.js +34 -0
  87. package/dist/core/dist/theme/themes/mint-breeze.js +33 -0
  88. package/dist/core/dist/theme/themes/monokai-pro.js +33 -0
  89. package/dist/core/dist/theme/themes/night-owl.js +35 -0
  90. package/dist/core/dist/theme/themes/one-dark-pro.js +33 -0
  91. package/dist/core/dist/theme/themes/panda.js +33 -0
  92. package/dist/core/dist/theme/themes/peach-cream.js +33 -0
  93. package/dist/core/dist/theme/themes/quiet-light.js +37 -0
  94. package/dist/core/dist/theme/themes/rose-pine-dawn.js +33 -0
  95. package/dist/core/dist/theme/themes/sand-dune.js +33 -0
  96. package/dist/core/dist/theme/themes/solarized-light.js +34 -0
  97. package/dist/core/dist/tooltip-position.js +10 -0
  98. package/dist/core/dist/utils/color.js +37 -0
  99. package/dist/core/dist/utils/format.js +29 -0
  100. package/dist/core/dist/utils/math.js +26 -0
  101. package/dist/core/dist/utils/poisoned-data-reporter.js +15 -0
  102. package/dist/core/dist/utils/time.js +41 -0
  103. package/dist/index.d.ts +1096 -63
  104. package/dist/index.js +142 -7754
  105. package/dist/ui/Crosshair.vue.js +53 -0
  106. package/dist/ui/Crosshair.vue2.js +4 -0
  107. package/dist/ui/InfoBar.vue.js +143 -0
  108. package/dist/ui/InfoBar.vue2.js +4 -0
  109. package/dist/ui/Legend.vue.js +212 -0
  110. package/dist/ui/Legend.vue2.js +4 -0
  111. package/dist/ui/Marker.vue.js +47 -0
  112. package/dist/ui/Marker.vue2.js +4 -0
  113. package/dist/ui/Navigator.vue.js +58 -0
  114. package/dist/ui/Navigator.vue2.js +4 -0
  115. package/dist/ui/NumberFlow.vue.js +69 -0
  116. package/dist/ui/NumberFlow.vue2.js +4 -0
  117. package/dist/ui/PieLegend.vue.js +132 -0
  118. package/dist/ui/PieLegend.vue2.js +4 -0
  119. package/dist/ui/PieTooltip.vue.js +135 -0
  120. package/dist/ui/PieTooltip.vue2.js +4 -0
  121. package/dist/ui/ReferenceLine.vue.js +44 -0
  122. package/dist/ui/ReferenceLine.vue2.js +4 -0
  123. package/dist/ui/TimeAxis.vue.js +43 -0
  124. package/dist/ui/TimeAxis.vue2.js +4 -0
  125. package/dist/ui/TimeRegion.vue.js +41 -0
  126. package/dist/ui/TimeRegion.vue2.js +4 -0
  127. package/dist/ui/Title.vue.js +54 -0
  128. package/dist/ui/Title.vue2.js +4 -0
  129. package/dist/ui/Tooltip.vue.js +195 -0
  130. package/dist/ui/Tooltip.vue2.js +4 -0
  131. package/dist/ui/YAxis.vue.js +42 -0
  132. package/dist/ui/YAxis.vue2.js +4 -0
  133. package/dist/ui/YLabel.vue.js +105 -0
  134. package/dist/ui/YLabel.vue2.js +4 -0
  135. package/package.json +2 -2
  136. package/src/BarSeries.vue +12 -13
  137. package/src/CandlestickSeries.vue +17 -6
  138. package/src/LineSeries.vue +12 -13
  139. package/src/PieSeries.vue +2 -1
  140. package/src/composables.ts +12 -2
  141. package/src/index.ts +42 -0
  142. package/src/ui/Crosshair.vue +28 -3
  143. package/src/ui/Legend.vue +1 -2
  144. package/src/ui/Marker.vue +62 -0
  145. package/src/ui/ReferenceLine.vue +58 -0
  146. package/src/ui/TimeRegion.vue +54 -0
@@ -0,0 +1,453 @@
1
+ import { ChartInstance as R } from "../chart.js";
2
+ import { LineSeriesDef as E } from "../series/line.js";
3
+ import { PerfMonitor as g } from "./perf-monitor.js";
4
+ import { createTheme as T } from "../theme/create.js";
5
+ function J(i) {
6
+ const e = i instanceof g;
7
+ return {
8
+ monitor: e ? i : new g(i),
9
+ hud: (t, n) => new D(t, n),
10
+ ownsMonitor: !e
11
+ };
12
+ }
13
+ const P = 100, z = 600, f = 250, F = 1e3 / 60, H = 1e3 / 120, m = 60, b = 4, j = 34, S = 64, L = 24;
14
+ function I(i) {
15
+ let e = 0;
16
+ for (const t of i)
17
+ t > e && (e = t);
18
+ return Math.max(10, Math.ceil(e / 10) * 10);
19
+ }
20
+ function O(i) {
21
+ return -I(i) / 12;
22
+ }
23
+ const B = "#9aa6b6", c = "#5d6877", C = "#e8eef7", x = "#cdd6e2", y = "#3fd68f", V = "#f5c451", $ = "#f4756a", Y = "#5d6877", X = {
24
+ position: "absolute",
25
+ top: "8px",
26
+ right: "8px",
27
+ // Fixed width on purpose: a content-driven box changes width whenever a
28
+ // streamed value gains or loses a digit, which makes the whole panel (and
29
+ // the nested sparkline chart tracking it) visibly twitch.
30
+ width: "216px",
31
+ boxSizing: "border-box",
32
+ padding: "8px 10px 9px",
33
+ background: "linear-gradient(165deg, rgba(22, 26, 36, 0.92), rgba(10, 12, 18, 0.88))",
34
+ border: "1px solid rgba(255, 255, 255, 0.09)",
35
+ borderRadius: "10px",
36
+ boxShadow: "0 10px 28px rgba(0, 0, 0, 0.45), inset 0 1px 0 rgba(255, 255, 255, 0.05)",
37
+ color: B,
38
+ font: "10px/1.5 ui-monospace, SFMono-Regular, Menlo, monospace",
39
+ fontVariantNumeric: "tabular-nums",
40
+ letterSpacing: "0.02em",
41
+ pointerEvents: "none",
42
+ zIndex: "20"
43
+ }, k = "wick-perf-hud-style";
44
+ function G(i) {
45
+ if (i.getElementById(k)) return;
46
+ const e = i.createElement("style");
47
+ e.id = k, e.textContent = `@keyframes wick-perf-pulse {
48
+ 0%, 100% { box-shadow: 0 0 0 0 rgba(63, 214, 143, 0.55); }
49
+ 50% { box-shadow: 0 0 0 4px rgba(63, 214, 143, 0); }
50
+ }`, i.head.appendChild(e);
51
+ }
52
+ function v(i) {
53
+ return i <= H ? y : i <= F ? V : $;
54
+ }
55
+ function A(i) {
56
+ return i >= 1e6 ? `${(i / 1e6).toFixed(1)}M` : i >= 1e4 ? `${(i / 1e3).toFixed(1)}k` : Math.round(i).toLocaleString("en-US");
57
+ }
58
+ function w(i, e, t) {
59
+ return e > t ? e : Math.min(Math.max(i, e), t);
60
+ }
61
+ function M(i) {
62
+ let e = 0;
63
+ for (const t of Object.values(i))
64
+ e += t;
65
+ return e;
66
+ }
67
+ function h(i) {
68
+ const e = i.doc.createElement("span");
69
+ return e.style.textAlign = i.align ?? "right", e.style.color = i.color ?? "", i.perfId && e.setAttribute("data-perf", i.perfId), e;
70
+ }
71
+ class D {
72
+ element;
73
+ unsubscribe;
74
+ updateIntervalMs;
75
+ sampleTimer;
76
+ dot;
77
+ fpsValue;
78
+ fpsSuffix;
79
+ header;
80
+ titleEl;
81
+ body;
82
+ sparkHost;
83
+ sparkMaxTick;
84
+ minimizeButton;
85
+ sparkChart = null;
86
+ sparkSeriesId = null;
87
+ mode = "mini";
88
+ mainCells;
89
+ overlayCells;
90
+ seriesSection;
91
+ seriesGrid;
92
+ heapRow;
93
+ heapValue;
94
+ seriesCells = /* @__PURE__ */ new Map();
95
+ status = null;
96
+ /** Consecutive zero samples streamed into the sparkline since the last live frame. */
97
+ idleStubCount = 0;
98
+ /** Most recent hero render rate, refreshed on every frame; the sample ticker plots it. */
99
+ lastHeroFps = 0;
100
+ /** Current drag offset, applied as a `translate` on the panel. */
101
+ dragX = 0;
102
+ dragY = 0;
103
+ lastFrameAt = 0;
104
+ lastUpdate = 0;
105
+ constructor(e, t, n = P) {
106
+ this.updateIntervalMs = n;
107
+ for (const p of e.querySelectorAll("[data-chart-perf-hud]"))
108
+ p.remove();
109
+ const s = e.ownerDocument;
110
+ G(s), this.element = s.createElement("div"), this.element.setAttribute("data-chart-perf-hud", ""), Object.assign(this.element.style, X), this.element.style.setProperty("backdrop-filter", "blur(10px) saturate(1.3)"), this.element.style.setProperty("-webkit-backdrop-filter", "blur(10px) saturate(1.3)"), this.sparkHost = s.createElement("div"), this.sparkHost.style.overflow = "hidden", this.header = s.createElement("div");
111
+ const r = this.header;
112
+ Object.assign(r.style, {
113
+ position: "relative",
114
+ zIndex: "1",
115
+ display: "flex",
116
+ alignItems: "center",
117
+ gap: "6px",
118
+ pointerEvents: "auto",
119
+ cursor: "grab"
120
+ }), this.dot = s.createElement("span"), Object.assign(this.dot.style, {
121
+ width: "7px",
122
+ height: "7px",
123
+ borderRadius: "50%",
124
+ flexShrink: "0"
125
+ }), this.titleEl = s.createElement("span");
126
+ const o = this.titleEl;
127
+ o.textContent = "PERF", Object.assign(o.style, {
128
+ fontSize: "9px",
129
+ fontWeight: "700",
130
+ letterSpacing: "0.18em",
131
+ color: x
132
+ }), this.minimizeButton = s.createElement("button"), this.minimizeButton.type = "button", this.minimizeButton.textContent = "–", this.minimizeButton.title = "Collapse to FPS square", Object.assign(this.minimizeButton.style, {
133
+ pointerEvents: "auto",
134
+ cursor: "pointer",
135
+ font: "inherit",
136
+ fontSize: "8px",
137
+ lineHeight: "1",
138
+ color: c,
139
+ background: "transparent",
140
+ border: "1px solid rgba(255, 255, 255, 0.14)",
141
+ borderRadius: "4px",
142
+ padding: "2px 5px"
143
+ }), this.minimizeButton.addEventListener("pointerdown", (p) => p.stopPropagation()), this.minimizeButton.addEventListener("click", () => this.setMode("mini"));
144
+ const l = s.createElement("span");
145
+ l.style.flex = "1", this.fpsValue = h({ doc: s, perfId: "hero-fps" }), Object.assign(this.fpsValue.style, {
146
+ fontSize: "16px",
147
+ fontWeight: "700",
148
+ lineHeight: "1",
149
+ color: C
150
+ }), this.fpsValue.textContent = "—", this.fpsSuffix = s.createElement("span"), this.fpsSuffix.textContent = "fps", Object.assign(this.fpsSuffix.style, {
151
+ fontSize: "9px",
152
+ color: c,
153
+ alignSelf: "flex-end"
154
+ }), r.append(this.dot, o, this.minimizeButton, l, this.fpsValue, this.fpsSuffix), this.installDrag(e), this.body = s.createElement("div"), this.body.style.position = "relative", this.body.style.zIndex = "1";
155
+ const a = s.createElement("div");
156
+ Object.assign(a.style, {
157
+ display: "grid",
158
+ gridTemplateColumns: "minmax(44px, 1fr) repeat(3, auto)",
159
+ columnGap: "8px",
160
+ rowGap: "2px",
161
+ marginTop: "7px"
162
+ });
163
+ for (const p of ["", "fps", "ms", "calls/s"]) {
164
+ const u = h({ doc: s, align: p === "" ? "left" : "right", color: c });
165
+ u.textContent = p, u.style.fontSize = "9px", a.appendChild(u);
166
+ }
167
+ this.mainCells = this.appendLayerRow({ doc: s, grid: a, label: "main", perfPrefix: "main" }), this.overlayCells = this.appendLayerRow({ doc: s, grid: a, label: "overlay", perfPrefix: "overlay" }), this.seriesSection = s.createElement("div"), this.seriesSection.style.display = "none", this.seriesSection.style.marginTop = "6px", this.seriesSection.style.paddingTop = "5px", this.seriesSection.style.borderTop = "1px solid rgba(255, 255, 255, 0.07)", this.seriesGrid = s.createElement("div"), Object.assign(this.seriesGrid.style, {
168
+ display: "grid",
169
+ gridTemplateColumns: "minmax(44px, 1fr) auto",
170
+ columnGap: "8px",
171
+ rowGap: "2px"
172
+ }), this.seriesSection.appendChild(this.seriesGrid), this.heapRow = s.createElement("div"), Object.assign(this.heapRow.style, {
173
+ display: "none",
174
+ justifyContent: "space-between",
175
+ marginTop: "6px",
176
+ fontSize: "9px",
177
+ color: c
178
+ });
179
+ const d = s.createElement("span");
180
+ d.textContent = "heap", this.heapValue = h({ doc: s, perfId: "heap" }), this.heapValue.textContent = "—", this.heapRow.append(d, this.heapValue), performance.memory && (this.heapRow.style.display = "flex"), this.sparkMaxTick = s.createElement("span"), this.sparkMaxTick.setAttribute("data-perf", "spark-max"), Object.assign(this.sparkMaxTick.style, {
181
+ position: "absolute",
182
+ display: "none",
183
+ fontSize: "8px",
184
+ lineHeight: "1",
185
+ letterSpacing: "0.05em",
186
+ color: c,
187
+ zIndex: "1"
188
+ }), this.body.append(a, this.seriesSection, this.heapRow), this.element.append(this.sparkHost, r, this.body, this.sparkMaxTick), e.appendChild(this.element), this.applyMode(), this.setStatus("waiting"), this.sampleTimer = setInterval(() => this.onSampleTick(), f), this.unsubscribe = t.onFrame((p) => this.onFrame(p));
189
+ }
190
+ destroy() {
191
+ clearInterval(this.sampleTimer), this.unsubscribe(), this.sparkChart?.destroy(), this.element.remove();
192
+ }
193
+ setMode(e) {
194
+ this.mode !== e && (this.mode = e, this.applyMode());
195
+ }
196
+ /**
197
+ * Apply the panel mode. Mini is a compact square — status dot, hero FPS
198
+ * and the graph as background; a click anywhere expands. Full is the
199
+ * complete stats panel with the `–` header button to collapse back.
200
+ */
201
+ applyMode() {
202
+ const e = this.mode === "mini";
203
+ e ? (Object.assign(this.element.style, {
204
+ width: `${S}px`,
205
+ height: `${S}px`,
206
+ padding: "7px 8px",
207
+ pointerEvents: "auto",
208
+ cursor: "pointer"
209
+ }), this.element.title = "Click to expand") : (Object.assign(this.element.style, {
210
+ width: "216px",
211
+ height: "auto",
212
+ padding: "8px 10px 9px",
213
+ pointerEvents: "none",
214
+ cursor: ""
215
+ }), this.element.title = ""), this.titleEl.style.display = e ? "none" : "", this.minimizeButton.style.display = e ? "none" : "", this.fpsValue.style.fontSize = e ? "13px" : "16px", this.body.style.display = e ? "none" : "", this.header.style.cursor = e ? "pointer" : "grab", this.status === "live" && (this.fpsSuffix.style.display = e ? "none" : ""), this.applySparkLayout();
216
+ }
217
+ /**
218
+ * Place the FPS graph for the current mode. Mini fills the square as a
219
+ * background; full shows a dedicated strip between header and table. The
220
+ * nested chart is created once and then reconfigured in place — tearing it
221
+ * down on a mode switch would wipe the FPS history on every
222
+ * expand/collapse.
223
+ */
224
+ applySparkLayout() {
225
+ if (this.mode === "mini" ? (Object.assign(this.sparkHost.style, {
226
+ position: "absolute",
227
+ inset: "0",
228
+ height: "auto",
229
+ marginTop: "0",
230
+ borderRadius: "inherit",
231
+ opacity: "0.75",
232
+ zIndex: "0"
233
+ }), this.element.insertBefore(this.sparkHost, this.element.firstChild)) : (Object.assign(this.sparkHost.style, {
234
+ position: "relative",
235
+ inset: "auto",
236
+ height: `${j}px`,
237
+ marginTop: "7px",
238
+ borderRadius: "4px",
239
+ opacity: "1",
240
+ zIndex: "0"
241
+ }), this.body.insertBefore(this.sparkHost, this.body.firstChild)), this.sparkChart)
242
+ this.sparkChart.setPadding({ top: this.sparkPaddingTop(), bottom: 4, left: 0, right: 0 });
243
+ else {
244
+ const t = this.createSparkChart(this.sparkHost);
245
+ this.sparkChart = t?.chart ?? null, this.sparkSeriesId = t?.seriesId ?? null;
246
+ }
247
+ const e = this.body.offsetTop + this.sparkHost.offsetTop;
248
+ this.sparkMaxTick.style.top = `${e + 2}px`, this.sparkMaxTick.style.left = "12px", this.sparkMaxTick.style.display = this.sparkChart && this.mode === "full" ? "" : "none";
249
+ }
250
+ /**
251
+ * The sparkline is itself a wick chart: a single-layer area line of the
252
+ * render rate on a chrome-less ChartInstance — no axes, no grid, no
253
+ * interactions. Dogfooding buys the library's own streaming behavior for
254
+ * free: spring tail-scroll on appends, smooth last-value chase, and the
255
+ * sticky-Y contraction when a rate spike leaves the window.
256
+ *
257
+ * Seeding goes through `setSeriesData` on purpose — that is the bulk path
258
+ * whose `onDataChanged` consumes `viewport.initialRange`, pinning the
259
+ * window at full capacity from the first paint (the React Sparkline's
260
+ * "flow" mode). Seeding via `appendData` would leave the viewport fitted
261
+ * to the two seed points and every sample would span half the panel.
262
+ *
263
+ * Two guards keep the measurement honest: the nested chart carries no
264
+ * `perf` config (no recursive HUD, nothing added to the measured monitor),
265
+ * and it draws on its own canvases inside the HUD panel. Construction is
266
+ * best-effort — in environments without a 2D canvas the HUD just drops the
267
+ * sparkline and keeps the numeric panel.
268
+ */
269
+ createSparkChart(e) {
270
+ try {
271
+ const t = performance.now(), n = new R(e, {
272
+ theme: T({ background: "#10141c" }).theme,
273
+ axis: {
274
+ y: { visible: !1, width: 0, min: O, max: I },
275
+ x: { visible: !1, height: 0 }
276
+ },
277
+ // Bottom padding keeps the zero baseline clearly on canvas — at 0 the
278
+ // 1px stroke drowns in the panel's bottom border and rounding.
279
+ padding: {
280
+ top: this.sparkPaddingTop(),
281
+ bottom: 4,
282
+ left: 0,
283
+ right: 0
284
+ },
285
+ grid: { visible: !1 },
286
+ interactive: !1,
287
+ viewport: {
288
+ maxVisibleBars: m,
289
+ initialRange: {
290
+ from: t - m * f,
291
+ to: t
292
+ }
293
+ }
294
+ }), s = n.addSeries(E, {
295
+ strokeWidth: 1,
296
+ curve: "smooth",
297
+ area: { visible: !0 },
298
+ pulse: !1
299
+ });
300
+ n.setLayerColors(s, [y]);
301
+ const r = [];
302
+ for (let o = m + b; o >= 0; o--)
303
+ r.push({ time: t - o * f, value: 0 });
304
+ return n.setSeriesData(s, r), { chart: n, seriesId: s };
305
+ } catch {
306
+ return null;
307
+ }
308
+ }
309
+ /** Keeps the plateau below the FPS number (mini) or the Y tick (strip). */
310
+ sparkPaddingTop() {
311
+ return this.mode === "mini" ? L : 12;
312
+ }
313
+ /** Append one FPS sample to the sparkline chart and roll its window. */
314
+ appendSpark(e) {
315
+ !this.sparkChart || !this.sparkSeriesId || (this.sparkChart.appendData(this.sparkSeriesId, { time: performance.now(), value: e }), this.sparkChart.keepLast(this.sparkSeriesId, m + b));
316
+ }
317
+ /**
318
+ * Drag-to-reposition, attached to the panel itself. In full mode the panel
319
+ * is `pointer-events: none`, so gestures only start from the header (its
320
+ * events bubble up here) and the chart underneath keeps receiving
321
+ * crosshair moves; in mini mode the whole square is the handle. A press
322
+ * that never moves is a click — in mini mode it expands the panel.
323
+ *
324
+ * Movement is applied as a `translate` transform: a `left`/`top` change
325
+ * would re-layout the panel and resize-observe the nested graph chart on
326
+ * every pointer move. Pointer capture keeps the gesture on the panel, so
327
+ * no document-level listeners to clean up.
328
+ */
329
+ installDrag(e) {
330
+ const t = this.element;
331
+ t.style.touchAction = "none", t.style.userSelect = "none";
332
+ let n = 0, s = 0, r = 0, o = 0, l = { minX: 0, maxX: 0, minY: 0, maxY: 0 };
333
+ t.addEventListener("pointerdown", (a) => {
334
+ const d = e.getBoundingClientRect(), p = t.getBoundingClientRect();
335
+ n = a.clientX, s = a.clientY, r = this.dragX, o = this.dragY, l = {
336
+ minX: r + (d.left - p.left),
337
+ maxX: r + (d.right - p.right),
338
+ minY: o + (d.top - p.top),
339
+ maxY: o + (d.bottom - p.bottom)
340
+ }, t.setPointerCapture(a.pointerId), a.preventDefault();
341
+ }), t.addEventListener("pointermove", (a) => {
342
+ t.hasPointerCapture(a.pointerId) && (this.dragX = w(r + (a.clientX - n), l.minX, l.maxX), this.dragY = w(o + (a.clientY - s), l.minY, l.maxY), t.style.transform = `translate(${this.dragX}px, ${this.dragY}px)`);
343
+ }), t.addEventListener("pointerup", (a) => {
344
+ if (!t.hasPointerCapture(a.pointerId)) return;
345
+ t.releasePointerCapture(a.pointerId);
346
+ const d = Math.hypot(a.clientX - n, a.clientY - s);
347
+ this.mode === "mini" && d < 5 && this.setMode("full");
348
+ }), t.addEventListener("pointercancel", (a) => {
349
+ t.hasPointerCapture(a.pointerId) && t.releasePointerCapture(a.pointerId);
350
+ });
351
+ }
352
+ appendLayerRow(e) {
353
+ const { doc: t, grid: n, label: s, perfPrefix: r } = e, o = h({ doc: t, align: "left", color: x });
354
+ o.textContent = s;
355
+ const l = {
356
+ fps: h({ doc: t, perfId: `${r}-fps` }),
357
+ ms: h({ doc: t, perfId: `${r}-ms` }),
358
+ calls: h({ doc: t, perfId: `${r}-calls` })
359
+ };
360
+ return l.fps.textContent = "—", l.ms.textContent = "—", l.calls.textContent = "—", n.append(o, l.fps, l.ms, l.calls), l;
361
+ }
362
+ setStatus(e) {
363
+ if (this.status !== e) {
364
+ if (this.status = e, this.element.setAttribute("data-perf-state", e), e === "live") {
365
+ this.dot.style.background = y, this.dot.style.animation = "wick-perf-pulse 1.6s ease-out infinite", this.fpsValue.style.color = C, this.fpsSuffix.style.display = this.mode === "mini" ? "none" : "", this.body.style.opacity = "1";
366
+ return;
367
+ }
368
+ this.dot.style.background = Y, this.dot.style.animation = "none", this.fpsValue.style.color = c, this.fpsValue.textContent = e === "idle" ? "idle" : "—", this.fpsSuffix.style.display = "none", this.body.style.opacity = "0.6";
369
+ }
370
+ }
371
+ onFrame(e) {
372
+ const t = performance.now();
373
+ this.lastFrameAt = t, this.lastHeroFps = Math.max(e.mainRendersPerSec, e.overlayRendersPerSec), this.setStatus("live"), !(t - this.lastUpdate < this.updateIntervalMs) && (this.lastUpdate = t, this.render(e));
374
+ }
375
+ /**
376
+ * Sample ticker: every {@link SAMPLE_TICK_MS} plot one point of the hero
377
+ * render rate. Doubles as the idle watchdog — once frames stop, the status
378
+ * flips and the plotted value drops to zero, so the line visibly falls to
379
+ * the baseline instead of freezing at the last live reading.
380
+ */
381
+ onSampleTick() {
382
+ if (this.status === "waiting") return;
383
+ const e = performance.now() - this.lastFrameAt >= z;
384
+ e && this.setStatus("idle"), !(e && this.idleStubCount >= m) && (this.idleStubCount = e ? this.idleStubCount + 1 : 0, this.appendSpark(e ? 0 : this.lastHeroFps));
385
+ }
386
+ render(e) {
387
+ const t = Math.max(e.mainRendersPerSec, e.overlayRendersPerSec);
388
+ if (this.fpsValue.textContent = t >= 100 ? String(Math.round(t)) : t.toFixed(1), this.updateLayerRow({
389
+ cells: this.mainCells,
390
+ drawn: e.frameCount.main > 0,
391
+ fps: e.mainRendersPerSec,
392
+ frame: e.mainFrameMs,
393
+ callsPerRender: M(e.drawCalls.main)
394
+ }), this.updateLayerRow({
395
+ cells: this.overlayCells,
396
+ drawn: e.frameCount.overlay > 0,
397
+ fps: e.overlayRendersPerSec,
398
+ frame: e.overlayFrameMs,
399
+ callsPerRender: M(e.drawCalls.overlay)
400
+ }), this.renderSeries(e), this.renderHeap(e), this.sparkChart && this.sparkMaxTick) {
401
+ const { max: n } = this.sparkChart.getYRange();
402
+ this.sparkMaxTick.textContent = n > 0 ? `${Math.round(n)}` : "";
403
+ }
404
+ }
405
+ updateLayerRow(e) {
406
+ const { cells: t, drawn: n, fps: s, frame: r, callsPerRender: o } = e;
407
+ n && (t.fps.textContent = s.toFixed(1), t.ms.textContent = r.last.toFixed(2), t.ms.style.color = v(r.last), t.calls.textContent = A(o * s));
408
+ }
409
+ /**
410
+ * Per-series breakdown of the main pass. Single-series charts give no useful
411
+ * attribution — the row would just echo the main row — so the section only
412
+ * shows when there is more than one series to compare.
413
+ */
414
+ renderSeries(e) {
415
+ const t = Object.keys(e.perSeries);
416
+ if (t.length <= 1) {
417
+ this.seriesSection.style.display = "none";
418
+ return;
419
+ }
420
+ this.seriesSection.style.display = "", this.sameSeriesIds(t) || this.rebuildSeriesRows(t);
421
+ for (const n of t) {
422
+ const s = this.seriesCells.get(n), r = e.perSeries[n];
423
+ !s || !r || (s.ms.textContent = r.last.toFixed(2), s.ms.style.color = v(r.last));
424
+ }
425
+ }
426
+ sameSeriesIds(e) {
427
+ return e.length !== this.seriesCells.size ? !1 : e.every((t) => this.seriesCells.has(t));
428
+ }
429
+ rebuildSeriesRows(e) {
430
+ const t = this.element.ownerDocument;
431
+ this.seriesGrid.replaceChildren(), this.seriesCells = /* @__PURE__ */ new Map();
432
+ for (const n of ["series", "ms"]) {
433
+ const s = h({ doc: t, align: n === "series" ? "left" : "right", color: c });
434
+ s.textContent = n, s.style.fontSize = "9px", this.seriesGrid.appendChild(s);
435
+ }
436
+ for (const n of e) {
437
+ const s = h({ doc: t, align: "left", color: x });
438
+ s.textContent = n, s.style.overflow = "hidden", s.style.textOverflow = "ellipsis", s.style.whiteSpace = "nowrap";
439
+ const r = {
440
+ ms: h({ doc: t, perfId: `series-${n}-ms` })
441
+ };
442
+ this.seriesGrid.append(s, r.ms), this.seriesCells.set(n, r);
443
+ }
444
+ }
445
+ /** The row itself is reserved at construction (no layout pop) — only the value updates here. */
446
+ renderHeap(e) {
447
+ e.heapMb !== null && (this.heapRow.style.display = "flex", this.heapValue.textContent = `${e.heapMb.toFixed(1)} MB`);
448
+ }
449
+ }
450
+ export {
451
+ D as PerfHud,
452
+ J as perfHud
453
+ };
@@ -0,0 +1,132 @@
1
+ import { createCountingContext as m } from "./counting-context.js";
2
+ const S = 5e3, u = 2e3, M = 30;
3
+ function n(s, e, a) {
4
+ let t = 0;
5
+ for (; t < e.length && e[t] < a; ) t++;
6
+ t > 0 && (s.splice(0, t), e.splice(0, t));
7
+ }
8
+ function l(s, e, a) {
9
+ const t = s.length - a;
10
+ t > 0 && (s.splice(0, t), e.splice(0, t));
11
+ }
12
+ function h(s) {
13
+ if (s.length === 0) return { last: 0, p50: 0, p95: 0 };
14
+ const e = [...s].sort((a, t) => a - t);
15
+ return {
16
+ last: s[s.length - 1],
17
+ p50: e[Math.floor(e.length * 0.5)] ?? 0,
18
+ p95: e[Math.floor(e.length * 0.95)] ?? 0
19
+ };
20
+ }
21
+ const c = 300;
22
+ function o(s) {
23
+ if (s.length < 2) return 0;
24
+ let e = s.length - 1;
25
+ for (; e > 0 && s[e] - s[e - 1] <= c; )
26
+ e--;
27
+ const a = s.length - 1 - e, t = s[s.length - 1] - s[e];
28
+ if (a > 0 && t > 0)
29
+ return a * 1e3 / t;
30
+ const r = s[s.length - 1] - s[0];
31
+ return r <= 0 ? 0 : (s.length - 1) * 1e3 / r;
32
+ }
33
+ function p(s) {
34
+ const e = {};
35
+ for (const [a, t] of s) e[a] = t;
36
+ return e;
37
+ }
38
+ class C {
39
+ /** Draw-call tally map for the main layer. Mutated in place by the counting context; cleared at the start of each frame. */
40
+ drawCallsMain = /* @__PURE__ */ new Map();
41
+ /** Draw-call tally map for the overlay layer. Same contract as `drawCallsMain`. */
42
+ drawCallsOverlay = /* @__PURE__ */ new Map();
43
+ mainMs = [];
44
+ overlayMs = [];
45
+ mainStamps = [];
46
+ overlayStamps = [];
47
+ perSeriesMs = /* @__PURE__ */ new Map();
48
+ perSeriesStamps = /* @__PURE__ */ new Map();
49
+ listeners = /* @__PURE__ */ new Set();
50
+ windowMs;
51
+ maxSamples;
52
+ heapInterval;
53
+ heapCounter = 0;
54
+ heapMb = null;
55
+ mainFrameCount = 0;
56
+ overlayFrameCount = 0;
57
+ /** Most recent frame timestamp across either layer — anchor for age-based trimming. */
58
+ lastStamp = 0;
59
+ constructor(e = {}) {
60
+ this.windowMs = e.windowMs ?? S, this.maxSamples = e.maxSamples ?? u, this.heapInterval = e.heapSampleEveryNFrames ?? M, this.heapCounter = this.heapInterval;
61
+ }
62
+ /** Clear the draw-call tally for the given layer. Call at the start of each frame. */
63
+ resetDrawCalls(e) {
64
+ (e === "main" ? this.drawCallsMain : this.drawCallsOverlay).clear();
65
+ }
66
+ /**
67
+ * Wrap a 2D context in a draw-call-counting Proxy feeding this monitor's
68
+ * tally for `kind`. Lives on the monitor (rather than being imported by
69
+ * the chart's canvas manager) so charts without instrumentation carry no
70
+ * perf code at all.
71
+ */
72
+ wrapContext(e, a) {
73
+ return m(e, a === "main" ? this.drawCallsMain : this.drawCallsOverlay);
74
+ }
75
+ /** Record a completed frame's wall-clock duration and emit stats to subscribers. */
76
+ recordFrame(e, a, t) {
77
+ if (this.lastStamp = t, e === "main" ? (this.mainMs.push(a), this.mainStamps.push(t), this.mainFrameCount++, this.sampleHeap()) : (this.overlayMs.push(a), this.overlayStamps.push(t), this.overlayFrameCount++), this.trimAll(t), this.listeners.size > 0) {
78
+ const r = this.getStats();
79
+ for (const i of this.listeners) i(r);
80
+ }
81
+ }
82
+ /** Record one series renderer's time slice inside the main pass. Called from inside `renderMain`, so the enclosing `recordFrame('main', …)` trim takes care of age. */
83
+ recordSeries(e, a, t = this.lastStamp) {
84
+ let r = this.perSeriesMs.get(e), i = this.perSeriesStamps.get(e);
85
+ (!r || !i) && (r = [], i = [], this.perSeriesMs.set(e, r), this.perSeriesStamps.set(e, i)), r.push(a), i.push(t);
86
+ }
87
+ /** Drop samples older than `windowMs` behind the newest recorded stamp, and enforce the hard per-layer cap. */
88
+ trimAll(e) {
89
+ const a = e - this.windowMs;
90
+ n(this.mainMs, this.mainStamps, a), n(this.overlayMs, this.overlayStamps, a), l(this.mainMs, this.mainStamps, this.maxSamples), l(this.overlayMs, this.overlayStamps, this.maxSamples);
91
+ for (const t of this.perSeriesMs.keys()) {
92
+ const r = this.perSeriesMs.get(t), i = this.perSeriesStamps.get(t);
93
+ !r || !i || (n(r, i, a), l(r, i, this.maxSamples));
94
+ }
95
+ }
96
+ /** Subscribe to per-frame stat snapshots. Returns an unsubscribe function. */
97
+ onFrame(e) {
98
+ return this.listeners.add(e), () => {
99
+ this.listeners.delete(e);
100
+ };
101
+ }
102
+ /** Snapshot current stats. Safe to call outside frames. */
103
+ getStats() {
104
+ const e = {};
105
+ for (const [a, t] of this.perSeriesMs) e[a] = h(t);
106
+ return {
107
+ mainRendersPerSec: o(this.mainStamps),
108
+ overlayRendersPerSec: o(this.overlayStamps),
109
+ mainFrameMs: h(this.mainMs),
110
+ overlayFrameMs: h(this.overlayMs),
111
+ frameCount: { main: this.mainFrameCount, overlay: this.overlayFrameCount },
112
+ drawCalls: {
113
+ main: p(this.drawCallsMain),
114
+ overlay: p(this.drawCallsOverlay)
115
+ },
116
+ perSeries: e,
117
+ heapMb: this.heapMb
118
+ };
119
+ }
120
+ destroy() {
121
+ this.listeners.clear(), this.mainMs.length = 0, this.overlayMs.length = 0, this.mainStamps.length = 0, this.overlayStamps.length = 0, this.perSeriesMs.clear(), this.perSeriesStamps.clear(), this.drawCallsMain.clear(), this.drawCallsOverlay.clear(), this.mainFrameCount = 0, this.overlayFrameCount = 0, this.lastStamp = 0, this.heapCounter = 0, this.heapMb = null;
122
+ }
123
+ sampleHeap() {
124
+ if (this.heapCounter++, this.heapCounter < this.heapInterval) return;
125
+ this.heapCounter = 0;
126
+ const e = performance.memory;
127
+ this.heapMb = e ? e.usedJSHeapSize / (1024 * 1024) : null;
128
+ }
129
+ }
130
+ export {
131
+ C as PerfMonitor
132
+ };
@@ -0,0 +1,31 @@
1
+ class e {
2
+ dirty = !1;
3
+ rafId = null;
4
+ callback;
5
+ onVisibilityChange = null;
6
+ constructor(i) {
7
+ this.callback = i, typeof document < "u" && (this.onVisibilityChange = () => {
8
+ !document.hidden && this.dirty && this.rafId === null && (this.rafId = requestAnimationFrame(this.render));
9
+ }, document.addEventListener("visibilitychange", this.onVisibilityChange));
10
+ }
11
+ markDirty() {
12
+ this.dirty || (this.dirty = !0, !(typeof document < "u" && document.hidden) && (this.rafId = requestAnimationFrame(this.render)));
13
+ }
14
+ /**
15
+ * Drop the pending frame without rendering — for callers that just painted
16
+ * the same content synchronously and would otherwise get a duplicate
17
+ * callback later this frame. The next `markDirty` re-arms normally.
18
+ */
19
+ consume() {
20
+ this.rafId !== null && (cancelAnimationFrame(this.rafId), this.rafId = null), this.dirty = !1;
21
+ }
22
+ render = (i) => {
23
+ this.dirty = !1, this.rafId = null, this.callback(i);
24
+ };
25
+ destroy() {
26
+ this.rafId !== null && (cancelAnimationFrame(this.rafId), this.rafId = null), this.dirty = !1, this.onVisibilityChange !== null && document.removeEventListener("visibilitychange", this.onVisibilityChange);
27
+ }
28
+ }
29
+ export {
30
+ e as RenderScheduler
31
+ };