@vroomchart/core-wasm 0.1.0 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/vroom_core.mjs +1 -1
- package/assets/vroom_core.wasm +0 -0
- package/dist/index.d.ts +22 -11
- package/dist/index.js +95 -460
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/assets/vroom_core.wasm
CHANGED
|
Binary file
|
package/dist/index.d.ts
CHANGED
|
@@ -20,6 +20,16 @@ type CrosshairCandle = {
|
|
|
20
20
|
close: number;
|
|
21
21
|
volume: number;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* What the crosshair currently snaps to. `timeMs` is the snapped slot's
|
|
25
|
+
* period-start time — set even in the empty space ahead of the most recent
|
|
26
|
+
* candle. `candle` holds the OHLCV when a real candle sits at that slot, and is
|
|
27
|
+
* null when the crosshair is parked on a future candle-aligned slot.
|
|
28
|
+
*/
|
|
29
|
+
type CrosshairInfo = {
|
|
30
|
+
timeMs: number;
|
|
31
|
+
candle: CrosshairCandle | null;
|
|
32
|
+
};
|
|
23
33
|
/** Axis region sizes in CSS px, for hit-testing gestures on the JS side. */
|
|
24
34
|
type AxisMetrics = {
|
|
25
35
|
yAxisWidth: number;
|
|
@@ -70,6 +80,12 @@ interface VroomChartHandle {
|
|
|
70
80
|
clearCrosshair(): void;
|
|
71
81
|
/** OHLCV of the candle the crosshair snaps to, or null when inactive. */
|
|
72
82
|
getCrosshairCandle(): CrosshairCandle | null;
|
|
83
|
+
/**
|
|
84
|
+
* The slot the crosshair snaps to (real candle or future candle-aligned
|
|
85
|
+
* slot), or null when inactive. Unlike getCrosshairCandle this reports a
|
|
86
|
+
* timeMs even in the empty space ahead of the most recent candle.
|
|
87
|
+
*/
|
|
88
|
+
getCrosshairInfo(): CrosshairInfo | null;
|
|
73
89
|
setRSI(enabled: boolean, period: number, upperBand: number, lowerBand: number, maEnabled: boolean, maPeriod: number): void;
|
|
74
90
|
setMACD(enabled: boolean, fast: number, slow: number, signal: number): void;
|
|
75
91
|
setOverlays(overlays: OverlaySpec[]): void;
|
|
@@ -138,7 +154,7 @@ type VroomTheme = {
|
|
|
138
154
|
declare const BYTES_PER_CANDLE = 48;
|
|
139
155
|
/** Serialize candles into the packed little-endian buffer the core expects. */
|
|
140
156
|
declare function packCandles(candles: Candle[]): ArrayBuffer;
|
|
141
|
-
/** Decode the packed buffer back to candles (
|
|
157
|
+
/** Decode the packed buffer back to candles (inverse of packCandles). */
|
|
142
158
|
declare function unpackCandles(buf: ArrayBuffer): Candle[];
|
|
143
159
|
|
|
144
160
|
declare const COLOR_KEYS: Record<keyof VroomTheme, ColorKey>;
|
|
@@ -160,24 +176,19 @@ type LoadVroomOptions = {
|
|
|
160
176
|
/**
|
|
161
177
|
* Override where the Skia-WASM core is loaded from. By default the core uses
|
|
162
178
|
* the WASM build bundled in this package (turnkey, no asset hosting needed);
|
|
163
|
-
* pass `wasm` to load it from your own URLs instead.
|
|
164
|
-
* Canvas2D stub is used unless `fallbackToStub: false`.
|
|
179
|
+
* pass `wasm` to load it from your own URLs instead.
|
|
165
180
|
*/
|
|
166
181
|
wasm?: WasmConfig;
|
|
167
|
-
/** When wasm loading fails, fall back to the stub (default true). */
|
|
168
|
-
fallbackToStub?: boolean;
|
|
169
|
-
/** Force the Canvas2D stub instead of the WASM core (tests / SSR). */
|
|
170
|
-
forceStub?: boolean;
|
|
171
182
|
};
|
|
172
183
|
/**
|
|
173
184
|
* Load the chart core. Cached: repeated calls share one module instance.
|
|
174
185
|
*
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
*
|
|
186
|
+
* Resolves to the Skia-WASM core bundled in this package, or the core at the
|
|
187
|
+
* URLs in `opts.wasm`. The returned promise rejects if the WASM module fails to
|
|
188
|
+
* load (and the cache is cleared so a later call can retry).
|
|
178
189
|
*/
|
|
179
190
|
declare function loadVroom(opts?: LoadVroomOptions): Promise<VroomModule>;
|
|
180
191
|
/** Test/HMR helper: drop the cached module so the next loadVroom() re-creates it. */
|
|
181
192
|
declare function resetVroomForTesting(): void;
|
|
182
193
|
|
|
183
|
-
export { type AxisMetrics, BYTES_PER_CANDLE, COLOR_KEYS, ColorKey, type CrosshairCandle, type LoadVroomOptions, type OverlaySpec, type VroomChartHandle, type VroomModule, type WasmConfig, applyTheme, argbToCss, loadVroom, packCandles, parseColor, resetVroomForTesting, unpackCandles };
|
|
194
|
+
export { type AxisMetrics, BYTES_PER_CANDLE, COLOR_KEYS, ColorKey, type CrosshairCandle, type CrosshairInfo, type LoadVroomOptions, type OverlaySpec, type VroomChartHandle, type VroomModule, type WasmConfig, applyTheme, argbToCss, loadVroom, packCandles, parseColor, resetVroomForTesting, unpackCandles };
|
package/dist/index.js
CHANGED
|
@@ -2,455 +2,6 @@ var __defProp = Object.defineProperty;
|
|
|
2
2
|
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
3
|
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
4
4
|
|
|
5
|
-
// src/handle.ts
|
|
6
|
-
var ColorKey = /* @__PURE__ */ ((ColorKey2) => {
|
|
7
|
-
ColorKey2[ColorKey2["Background"] = 0] = "Background";
|
|
8
|
-
ColorKey2[ColorKey2["Bull"] = 1] = "Bull";
|
|
9
|
-
ColorKey2[ColorKey2["Bear"] = 2] = "Bear";
|
|
10
|
-
ColorKey2[ColorKey2["Wick"] = 3] = "Wick";
|
|
11
|
-
ColorKey2[ColorKey2["Grid"] = 4] = "Grid";
|
|
12
|
-
ColorKey2[ColorKey2["AxisText"] = 5] = "AxisText";
|
|
13
|
-
ColorKey2[ColorKey2["Crosshair"] = 6] = "Crosshair";
|
|
14
|
-
ColorKey2[ColorKey2["TooltipBg"] = 7] = "TooltipBg";
|
|
15
|
-
ColorKey2[ColorKey2["TooltipText"] = 8] = "TooltipText";
|
|
16
|
-
ColorKey2[ColorKey2["CrosshairTarget"] = 9] = "CrosshairTarget";
|
|
17
|
-
return ColorKey2;
|
|
18
|
-
})(ColorKey || {});
|
|
19
|
-
|
|
20
|
-
// src/color.ts
|
|
21
|
-
var COLOR_KEYS = {
|
|
22
|
-
background: 0 /* Background */,
|
|
23
|
-
bull: 1 /* Bull */,
|
|
24
|
-
bear: 2 /* Bear */,
|
|
25
|
-
grid: 4 /* Grid */,
|
|
26
|
-
axisText: 5 /* AxisText */,
|
|
27
|
-
crosshair: 6 /* Crosshair */,
|
|
28
|
-
crosshairTarget: 9 /* CrosshairTarget */
|
|
29
|
-
};
|
|
30
|
-
function parseColor(value) {
|
|
31
|
-
if (typeof value === "number") {
|
|
32
|
-
return Number.isFinite(value) ? value >>> 0 : null;
|
|
33
|
-
}
|
|
34
|
-
let s = value.trim();
|
|
35
|
-
if (s.startsWith("#")) s = s.slice(1);
|
|
36
|
-
if (s.length === 6) s = `ff${s}`;
|
|
37
|
-
if (s.length !== 8 || !/^[0-9a-fA-F]{8}$/.test(s)) return null;
|
|
38
|
-
return parseInt(s, 16) >>> 0;
|
|
39
|
-
}
|
|
40
|
-
function applyTheme(handle, theme) {
|
|
41
|
-
Object.keys(COLOR_KEYS).forEach((field) => {
|
|
42
|
-
const value = theme[field];
|
|
43
|
-
if (value == null) return;
|
|
44
|
-
const argb = parseColor(value);
|
|
45
|
-
if (argb == null) return;
|
|
46
|
-
handle.setColor(COLOR_KEYS[field], argb);
|
|
47
|
-
});
|
|
48
|
-
}
|
|
49
|
-
function argbToCss(argb) {
|
|
50
|
-
const a = (argb >>> 24 & 255) / 255;
|
|
51
|
-
const r = argb >>> 16 & 255;
|
|
52
|
-
const g = argb >>> 8 & 255;
|
|
53
|
-
const b = argb & 255;
|
|
54
|
-
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
// src/packCandles.ts
|
|
58
|
-
var BYTES_PER_CANDLE = 48;
|
|
59
|
-
function packCandles(candles) {
|
|
60
|
-
const buf = new ArrayBuffer(candles.length * BYTES_PER_CANDLE);
|
|
61
|
-
const view = new DataView(buf);
|
|
62
|
-
for (let i = 0; i < candles.length; i++) {
|
|
63
|
-
const c = candles[i];
|
|
64
|
-
const off = i * BYTES_PER_CANDLE;
|
|
65
|
-
view.setBigInt64(off, BigInt(Math.trunc(c.timeMs)), true);
|
|
66
|
-
view.setFloat64(off + 8, c.open, true);
|
|
67
|
-
view.setFloat64(off + 16, c.high, true);
|
|
68
|
-
view.setFloat64(off + 24, c.low, true);
|
|
69
|
-
view.setFloat64(off + 32, c.close, true);
|
|
70
|
-
view.setFloat64(off + 40, c.volume, true);
|
|
71
|
-
}
|
|
72
|
-
return buf;
|
|
73
|
-
}
|
|
74
|
-
function unpackCandles(buf) {
|
|
75
|
-
const view = new DataView(buf);
|
|
76
|
-
const n = Math.floor(buf.byteLength / BYTES_PER_CANDLE);
|
|
77
|
-
const out = new Array(n);
|
|
78
|
-
for (let i = 0; i < n; i++) {
|
|
79
|
-
const off = i * BYTES_PER_CANDLE;
|
|
80
|
-
out[i] = {
|
|
81
|
-
timeMs: Number(view.getBigInt64(off, true)),
|
|
82
|
-
open: view.getFloat64(off + 8, true),
|
|
83
|
-
high: view.getFloat64(off + 16, true),
|
|
84
|
-
low: view.getFloat64(off + 24, true),
|
|
85
|
-
close: view.getFloat64(off + 32, true),
|
|
86
|
-
volume: view.getFloat64(off + 40, true)
|
|
87
|
-
};
|
|
88
|
-
}
|
|
89
|
-
return out;
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
// src/stub/stubModule.ts
|
|
93
|
-
var DEFAULT_VISIBLE = 80;
|
|
94
|
-
var Y_AXIS_W = 58;
|
|
95
|
-
var X_AXIS_H = 22;
|
|
96
|
-
var RIGHT_PAD_CANDLES = 4;
|
|
97
|
-
var DEFAULT_COLORS = {
|
|
98
|
-
[0 /* Background */]: 4279046423,
|
|
99
|
-
[1 /* Bull */]: 4280723098,
|
|
100
|
-
[2 /* Bear */]: 4293874512,
|
|
101
|
-
[3 /* Wick */]: 4286086022,
|
|
102
|
-
[4 /* Grid */]: 4280231472,
|
|
103
|
-
[5 /* AxisText */]: 4287337630,
|
|
104
|
-
[6 /* Crosshair */]: 4291416537,
|
|
105
|
-
[9 /* CrosshairTarget */]: 4291416537
|
|
106
|
-
};
|
|
107
|
-
function median(xs) {
|
|
108
|
-
if (xs.length === 0) return 0;
|
|
109
|
-
const s = [...xs].sort((a, b) => a - b);
|
|
110
|
-
const mid = s.length >> 1;
|
|
111
|
-
return s.length % 2 ? s[mid] : (s[mid - 1] + s[mid]) / 2;
|
|
112
|
-
}
|
|
113
|
-
function niceStep(raw) {
|
|
114
|
-
if (raw <= 0 || !Number.isFinite(raw)) return 1;
|
|
115
|
-
const mag = Math.pow(10, Math.floor(Math.log10(raw)));
|
|
116
|
-
const norm = raw / mag;
|
|
117
|
-
const step = norm <= 1 ? 1 : norm <= 2 ? 2 : norm <= 5 ? 5 : 10;
|
|
118
|
-
return step * mag;
|
|
119
|
-
}
|
|
120
|
-
var StubChart = class {
|
|
121
|
-
constructor(canvas) {
|
|
122
|
-
__publicField(this, "canvas", canvas);
|
|
123
|
-
__publicField(this, "ctx");
|
|
124
|
-
__publicField(this, "candles", []);
|
|
125
|
-
__publicField(this, "durationMs", 6e4);
|
|
126
|
-
__publicField(this, "wCss", 0);
|
|
127
|
-
__publicField(this, "hCss", 0);
|
|
128
|
-
__publicField(this, "dpr", 1);
|
|
129
|
-
__publicField(this, "visStart", 0);
|
|
130
|
-
__publicField(this, "visEnd", 0);
|
|
131
|
-
__publicField(this, "priceMin", 0);
|
|
132
|
-
__publicField(this, "priceMax", 1);
|
|
133
|
-
__publicField(this, "rangeInit", false);
|
|
134
|
-
__publicField(this, "crosshair", false);
|
|
135
|
-
__publicField(this, "chX", 0);
|
|
136
|
-
__publicField(this, "chY", 0);
|
|
137
|
-
__publicField(this, "colors", { ...DEFAULT_COLORS });
|
|
138
|
-
const ctx = canvas.getContext("2d");
|
|
139
|
-
if (!ctx) throw new Error("VroomChart stub: 2D canvas context unavailable");
|
|
140
|
-
this.ctx = ctx;
|
|
141
|
-
}
|
|
142
|
-
// --- geometry helpers ---------------------------------------------------
|
|
143
|
-
get plotW() {
|
|
144
|
-
return Math.max(0, this.wCss - Y_AXIS_W);
|
|
145
|
-
}
|
|
146
|
-
get plotH() {
|
|
147
|
-
return Math.max(0, this.hCss - X_AXIS_H);
|
|
148
|
-
}
|
|
149
|
-
xOf(timeMs) {
|
|
150
|
-
const span = this.visEnd - this.visStart || 1;
|
|
151
|
-
return (timeMs - this.visStart) / span * this.plotW;
|
|
152
|
-
}
|
|
153
|
-
yOf(price) {
|
|
154
|
-
const span = this.priceMax - this.priceMin || 1;
|
|
155
|
-
return (this.priceMax - price) / span * this.plotH;
|
|
156
|
-
}
|
|
157
|
-
color(key) {
|
|
158
|
-
return argbToCss(this.colors[key] ?? DEFAULT_COLORS[key] ?? 4278190080);
|
|
159
|
-
}
|
|
160
|
-
// --- data ---------------------------------------------------------------
|
|
161
|
-
setCandles(packed) {
|
|
162
|
-
this.candles = unpackCandles(packed);
|
|
163
|
-
if (this.candles.length >= 2) {
|
|
164
|
-
const diffs = [];
|
|
165
|
-
for (let i = 1; i < this.candles.length; i++) {
|
|
166
|
-
diffs.push(this.candles[i].timeMs - this.candles[i - 1].timeMs);
|
|
167
|
-
}
|
|
168
|
-
const m = median(diffs);
|
|
169
|
-
if (m > 0) this.durationMs = m;
|
|
170
|
-
}
|
|
171
|
-
if (!this.rangeInit) this.resetWindow();
|
|
172
|
-
this.recomputePriceBounds();
|
|
173
|
-
}
|
|
174
|
-
resetWindow() {
|
|
175
|
-
const n = this.candles.length;
|
|
176
|
-
if (n === 0) {
|
|
177
|
-
this.visStart = 0;
|
|
178
|
-
this.visEnd = 1;
|
|
179
|
-
return;
|
|
180
|
-
}
|
|
181
|
-
const last = this.candles[n - 1].timeMs;
|
|
182
|
-
this.visEnd = last + this.durationMs * RIGHT_PAD_CANDLES;
|
|
183
|
-
this.visStart = this.visEnd - this.durationMs * (DEFAULT_VISIBLE + RIGHT_PAD_CANDLES);
|
|
184
|
-
this.rangeInit = true;
|
|
185
|
-
}
|
|
186
|
-
recomputePriceBounds() {
|
|
187
|
-
let lo = Infinity;
|
|
188
|
-
let hi = -Infinity;
|
|
189
|
-
for (const c of this.candles) {
|
|
190
|
-
if (c.timeMs < this.visStart || c.timeMs > this.visEnd) continue;
|
|
191
|
-
if (c.low < lo) lo = c.low;
|
|
192
|
-
if (c.high > hi) hi = c.high;
|
|
193
|
-
}
|
|
194
|
-
if (!Number.isFinite(lo) || !Number.isFinite(hi) || hi <= lo) {
|
|
195
|
-
if (this.priceMax > this.priceMin) return;
|
|
196
|
-
lo = 0;
|
|
197
|
-
hi = 1;
|
|
198
|
-
}
|
|
199
|
-
const pad = (hi - lo) * 0.08;
|
|
200
|
-
this.priceMin = lo - pad;
|
|
201
|
-
this.priceMax = hi + pad;
|
|
202
|
-
}
|
|
203
|
-
// --- sizing -------------------------------------------------------------
|
|
204
|
-
setSize(width, height, dpr) {
|
|
205
|
-
this.wCss = width;
|
|
206
|
-
this.hCss = height;
|
|
207
|
-
this.dpr = dpr || 1;
|
|
208
|
-
this.canvas.width = Math.max(1, Math.round(width * this.dpr));
|
|
209
|
-
this.canvas.height = Math.max(1, Math.round(height * this.dpr));
|
|
210
|
-
this.ctx.setTransform(this.dpr, 0, 0, this.dpr, 0, 0);
|
|
211
|
-
}
|
|
212
|
-
setColor(key, argb) {
|
|
213
|
-
this.colors[key] = argb >>> 0;
|
|
214
|
-
}
|
|
215
|
-
setVisibleRange(startMs, endMs) {
|
|
216
|
-
if (startMs === 0 && endMs === 0) {
|
|
217
|
-
this.rangeInit = false;
|
|
218
|
-
this.resetWindow();
|
|
219
|
-
} else {
|
|
220
|
-
this.visStart = startMs;
|
|
221
|
-
this.visEnd = endMs;
|
|
222
|
-
this.rangeInit = true;
|
|
223
|
-
}
|
|
224
|
-
this.recomputePriceBounds();
|
|
225
|
-
}
|
|
226
|
-
// --- viewport mutators --------------------------------------------------
|
|
227
|
-
pan(dx, dy) {
|
|
228
|
-
this.translate(dx, dy);
|
|
229
|
-
}
|
|
230
|
-
translate(dx, dy) {
|
|
231
|
-
const span = this.visEnd - this.visStart;
|
|
232
|
-
const dt = dx / (this.plotW || 1) * span;
|
|
233
|
-
this.visStart -= dt;
|
|
234
|
-
this.visEnd -= dt;
|
|
235
|
-
if (dy) {
|
|
236
|
-
const pspan = this.priceMax - this.priceMin;
|
|
237
|
-
const dp = dy / (this.plotH || 1) * pspan;
|
|
238
|
-
this.priceMin += dp;
|
|
239
|
-
this.priceMax += dp;
|
|
240
|
-
}
|
|
241
|
-
}
|
|
242
|
-
zoom(scaleX, scaleY, fx, fy) {
|
|
243
|
-
if (scaleX !== 1) {
|
|
244
|
-
const span = this.visEnd - this.visStart;
|
|
245
|
-
const focalT = this.visStart + fx / (this.plotW || 1) * span;
|
|
246
|
-
const newSpan = span / scaleX;
|
|
247
|
-
this.visStart = focalT - fx / (this.plotW || 1) * newSpan;
|
|
248
|
-
this.visEnd = this.visStart + newSpan;
|
|
249
|
-
}
|
|
250
|
-
if (scaleY !== 1) {
|
|
251
|
-
const pspan = this.priceMax - this.priceMin;
|
|
252
|
-
const focalP = this.priceMax - fy / (this.plotH || 1) * pspan;
|
|
253
|
-
const newSpan = pspan / scaleY;
|
|
254
|
-
this.priceMax = focalP + fy / (this.plotH || 1) * newSpan;
|
|
255
|
-
this.priceMin = this.priceMax - newSpan;
|
|
256
|
-
}
|
|
257
|
-
}
|
|
258
|
-
scalePriceAxis(dy) {
|
|
259
|
-
const factor = Math.max(0.1, 1 + dy / (this.plotH || 1));
|
|
260
|
-
const center = (this.priceMin + this.priceMax) / 2;
|
|
261
|
-
const half = (this.priceMax - this.priceMin) / 2 * factor;
|
|
262
|
-
this.priceMin = center - half;
|
|
263
|
-
this.priceMax = center + half;
|
|
264
|
-
}
|
|
265
|
-
scaleTimeAxis(dx) {
|
|
266
|
-
const factor = Math.max(0.1, 1 + dx / (this.plotW || 1));
|
|
267
|
-
const span = (this.visEnd - this.visStart) * factor;
|
|
268
|
-
this.visStart = this.visEnd - span;
|
|
269
|
-
}
|
|
270
|
-
getAxisMetrics() {
|
|
271
|
-
return { yAxisWidth: Y_AXIS_W, xAxisHeight: X_AXIS_H, indicatorHeight: 0 };
|
|
272
|
-
}
|
|
273
|
-
// --- crosshair ----------------------------------------------------------
|
|
274
|
-
setCrosshair(x, y) {
|
|
275
|
-
this.crosshair = true;
|
|
276
|
-
this.chX = x;
|
|
277
|
-
this.chY = y;
|
|
278
|
-
}
|
|
279
|
-
clearCrosshair() {
|
|
280
|
-
this.crosshair = false;
|
|
281
|
-
}
|
|
282
|
-
snapIndex() {
|
|
283
|
-
if (this.candles.length === 0) return -1;
|
|
284
|
-
const span = this.visEnd - this.visStart || 1;
|
|
285
|
-
const t = this.visStart + this.chX / (this.plotW || 1) * span;
|
|
286
|
-
let best = -1;
|
|
287
|
-
let bestD = Infinity;
|
|
288
|
-
for (let i = 0; i < this.candles.length; i++) {
|
|
289
|
-
const d = Math.abs(this.candles[i].timeMs - t);
|
|
290
|
-
if (d < bestD) {
|
|
291
|
-
bestD = d;
|
|
292
|
-
best = i;
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
return best;
|
|
296
|
-
}
|
|
297
|
-
getCrosshairCandle() {
|
|
298
|
-
if (!this.crosshair) return null;
|
|
299
|
-
const i = this.snapIndex();
|
|
300
|
-
if (i < 0) return null;
|
|
301
|
-
const c = this.candles[i];
|
|
302
|
-
return { ...c };
|
|
303
|
-
}
|
|
304
|
-
// --- indicators (accepted, not drawn in the stub) -----------------------
|
|
305
|
-
setRSI() {
|
|
306
|
-
}
|
|
307
|
-
setMACD() {
|
|
308
|
-
}
|
|
309
|
-
setOverlays(_overlays) {
|
|
310
|
-
}
|
|
311
|
-
setVWAP() {
|
|
312
|
-
}
|
|
313
|
-
isAnimating() {
|
|
314
|
-
return false;
|
|
315
|
-
}
|
|
316
|
-
// --- rendering ----------------------------------------------------------
|
|
317
|
-
present() {
|
|
318
|
-
const ctx = this.ctx;
|
|
319
|
-
const w = this.wCss;
|
|
320
|
-
const h = this.hCss;
|
|
321
|
-
if (w <= 0 || h <= 0) return;
|
|
322
|
-
ctx.clearRect(0, 0, w, h);
|
|
323
|
-
ctx.fillStyle = this.color(0 /* Background */);
|
|
324
|
-
ctx.fillRect(0, 0, w, h);
|
|
325
|
-
this.drawPriceAxis();
|
|
326
|
-
this.drawTimeAxis();
|
|
327
|
-
this.drawCandles();
|
|
328
|
-
if (this.crosshair) this.drawCrosshair();
|
|
329
|
-
}
|
|
330
|
-
priceTicks() {
|
|
331
|
-
const span = this.priceMax - this.priceMin;
|
|
332
|
-
if (span <= 0) return [];
|
|
333
|
-
const step = niceStep(span / 5);
|
|
334
|
-
const first = Math.ceil(this.priceMin / step) * step;
|
|
335
|
-
const out = [];
|
|
336
|
-
for (let p = first; p <= this.priceMax; p += step) out.push(p);
|
|
337
|
-
return out;
|
|
338
|
-
}
|
|
339
|
-
fmtPrice(p) {
|
|
340
|
-
const span = this.priceMax - this.priceMin;
|
|
341
|
-
const decimals = span < 1 ? 4 : span < 100 ? 2 : 0;
|
|
342
|
-
return p.toFixed(decimals);
|
|
343
|
-
}
|
|
344
|
-
drawPriceAxis() {
|
|
345
|
-
const ctx = this.ctx;
|
|
346
|
-
const plotW = this.plotW;
|
|
347
|
-
ctx.lineWidth = 1;
|
|
348
|
-
ctx.strokeStyle = this.color(4 /* Grid */);
|
|
349
|
-
ctx.fillStyle = this.color(5 /* AxisText */);
|
|
350
|
-
ctx.font = '11px -apple-system, system-ui, "Segoe UI", sans-serif';
|
|
351
|
-
ctx.textAlign = "left";
|
|
352
|
-
ctx.textBaseline = "middle";
|
|
353
|
-
for (const p of this.priceTicks()) {
|
|
354
|
-
const y = this.yOf(p);
|
|
355
|
-
if (y < 0 || y > this.plotH) continue;
|
|
356
|
-
ctx.beginPath();
|
|
357
|
-
ctx.moveTo(0, Math.round(y) + 0.5);
|
|
358
|
-
ctx.lineTo(plotW, Math.round(y) + 0.5);
|
|
359
|
-
ctx.stroke();
|
|
360
|
-
ctx.fillText(this.fmtPrice(p), plotW + 6, y);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
fmtTime(timeMs) {
|
|
364
|
-
const d = new Date(timeMs);
|
|
365
|
-
const span = this.visEnd - this.visStart;
|
|
366
|
-
const twoDays = 2 * 24 * 3600 * 1e3;
|
|
367
|
-
const pad = (n) => String(n).padStart(2, "0");
|
|
368
|
-
if (span < twoDays) return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
|
|
369
|
-
return `${d.getMonth() + 1}/${d.getDate()}`;
|
|
370
|
-
}
|
|
371
|
-
drawTimeAxis() {
|
|
372
|
-
const ctx = this.ctx;
|
|
373
|
-
const span = this.visEnd - this.visStart;
|
|
374
|
-
if (span <= 0) return;
|
|
375
|
-
const targetPx = 80;
|
|
376
|
-
const stepMs = niceStep(span / this.plotW * targetPx);
|
|
377
|
-
const first = Math.ceil(this.visStart / stepMs) * stepMs;
|
|
378
|
-
ctx.fillStyle = this.color(5 /* AxisText */);
|
|
379
|
-
ctx.font = '11px -apple-system, system-ui, "Segoe UI", sans-serif';
|
|
380
|
-
ctx.textAlign = "center";
|
|
381
|
-
ctx.textBaseline = "middle";
|
|
382
|
-
const y = this.plotH + X_AXIS_H / 2;
|
|
383
|
-
for (let t = first; t <= this.visEnd; t += stepMs) {
|
|
384
|
-
const x = this.xOf(t);
|
|
385
|
-
if (x < 0 || x > this.plotW) continue;
|
|
386
|
-
ctx.fillText(this.fmtTime(t), x, y);
|
|
387
|
-
}
|
|
388
|
-
}
|
|
389
|
-
drawCandles() {
|
|
390
|
-
const ctx = this.ctx;
|
|
391
|
-
const span = this.visEnd - this.visStart || 1;
|
|
392
|
-
const slotW = this.durationMs / span * this.plotW;
|
|
393
|
-
const bodyW = Math.max(1, slotW * 0.7);
|
|
394
|
-
const bull = this.color(1 /* Bull */);
|
|
395
|
-
const bear = this.color(2 /* Bear */);
|
|
396
|
-
const wick = this.color(3 /* Wick */);
|
|
397
|
-
for (const c of this.candles) {
|
|
398
|
-
const x = this.xOf(c.timeMs);
|
|
399
|
-
if (x < -slotW || x > this.plotW + slotW) continue;
|
|
400
|
-
const up = c.close >= c.open;
|
|
401
|
-
const yHigh = this.yOf(c.high);
|
|
402
|
-
const yLow = this.yOf(c.low);
|
|
403
|
-
const yOpen = this.yOf(c.open);
|
|
404
|
-
const yClose = this.yOf(c.close);
|
|
405
|
-
ctx.strokeStyle = wick;
|
|
406
|
-
ctx.lineWidth = 1;
|
|
407
|
-
ctx.beginPath();
|
|
408
|
-
ctx.moveTo(Math.round(x) + 0.5, yHigh);
|
|
409
|
-
ctx.lineTo(Math.round(x) + 0.5, yLow);
|
|
410
|
-
ctx.stroke();
|
|
411
|
-
ctx.fillStyle = up ? bull : bear;
|
|
412
|
-
const top = Math.min(yOpen, yClose);
|
|
413
|
-
const bodyH = Math.max(1, Math.abs(yClose - yOpen));
|
|
414
|
-
ctx.fillRect(x - bodyW / 2, top, bodyW, bodyH);
|
|
415
|
-
}
|
|
416
|
-
}
|
|
417
|
-
drawCrosshair() {
|
|
418
|
-
const ctx = this.ctx;
|
|
419
|
-
const i = this.snapIndex();
|
|
420
|
-
if (i < 0) return;
|
|
421
|
-
const c = this.candles[i];
|
|
422
|
-
const x = this.xOf(c.timeMs);
|
|
423
|
-
const y = Math.max(0, Math.min(this.plotH, this.chY));
|
|
424
|
-
ctx.save();
|
|
425
|
-
ctx.strokeStyle = this.color(6 /* Crosshair */);
|
|
426
|
-
ctx.lineWidth = 1;
|
|
427
|
-
ctx.setLineDash([4, 4]);
|
|
428
|
-
ctx.beginPath();
|
|
429
|
-
ctx.moveTo(Math.round(x) + 0.5, 0);
|
|
430
|
-
ctx.lineTo(Math.round(x) + 0.5, this.plotH);
|
|
431
|
-
ctx.moveTo(0, Math.round(y) + 0.5);
|
|
432
|
-
ctx.lineTo(this.plotW, Math.round(y) + 0.5);
|
|
433
|
-
ctx.stroke();
|
|
434
|
-
ctx.setLineDash([]);
|
|
435
|
-
const ty = this.yOf(c.close);
|
|
436
|
-
ctx.strokeStyle = this.color(9 /* CrosshairTarget */);
|
|
437
|
-
ctx.beginPath();
|
|
438
|
-
ctx.arc(x, ty, 3.5, 0, Math.PI * 2);
|
|
439
|
-
ctx.stroke();
|
|
440
|
-
ctx.restore();
|
|
441
|
-
}
|
|
442
|
-
destroy() {
|
|
443
|
-
this.candles = [];
|
|
444
|
-
}
|
|
445
|
-
};
|
|
446
|
-
function createStubModule() {
|
|
447
|
-
return {
|
|
448
|
-
create(canvas) {
|
|
449
|
-
return new StubChart(canvas);
|
|
450
|
-
}
|
|
451
|
-
};
|
|
452
|
-
}
|
|
453
|
-
|
|
454
5
|
// src/wasm/adapter.ts
|
|
455
6
|
var uid = 0;
|
|
456
7
|
var WasmHandle = class {
|
|
@@ -496,6 +47,9 @@ var WasmHandle = class {
|
|
|
496
47
|
getCrosshairCandle() {
|
|
497
48
|
return this.wc.getCrosshairCandle();
|
|
498
49
|
}
|
|
50
|
+
getCrosshairInfo() {
|
|
51
|
+
return this.wc.getCrosshairInfo();
|
|
52
|
+
}
|
|
499
53
|
setRSI(enabled, period, upperBand, lowerBand, maEnabled, maPeriod) {
|
|
500
54
|
this.wc.setRSI(enabled, period, upperBand, lowerBand, maEnabled, maPeriod);
|
|
501
55
|
}
|
|
@@ -555,6 +109,93 @@ async function loadWasmCore(cfg) {
|
|
|
555
109
|
return makeWasmModule(instance, fontBytes);
|
|
556
110
|
}
|
|
557
111
|
|
|
112
|
+
// src/handle.ts
|
|
113
|
+
var ColorKey = /* @__PURE__ */ ((ColorKey2) => {
|
|
114
|
+
ColorKey2[ColorKey2["Background"] = 0] = "Background";
|
|
115
|
+
ColorKey2[ColorKey2["Bull"] = 1] = "Bull";
|
|
116
|
+
ColorKey2[ColorKey2["Bear"] = 2] = "Bear";
|
|
117
|
+
ColorKey2[ColorKey2["Wick"] = 3] = "Wick";
|
|
118
|
+
ColorKey2[ColorKey2["Grid"] = 4] = "Grid";
|
|
119
|
+
ColorKey2[ColorKey2["AxisText"] = 5] = "AxisText";
|
|
120
|
+
ColorKey2[ColorKey2["Crosshair"] = 6] = "Crosshair";
|
|
121
|
+
ColorKey2[ColorKey2["TooltipBg"] = 7] = "TooltipBg";
|
|
122
|
+
ColorKey2[ColorKey2["TooltipText"] = 8] = "TooltipText";
|
|
123
|
+
ColorKey2[ColorKey2["CrosshairTarget"] = 9] = "CrosshairTarget";
|
|
124
|
+
return ColorKey2;
|
|
125
|
+
})(ColorKey || {});
|
|
126
|
+
|
|
127
|
+
// src/packCandles.ts
|
|
128
|
+
var BYTES_PER_CANDLE = 48;
|
|
129
|
+
function packCandles(candles) {
|
|
130
|
+
const buf = new ArrayBuffer(candles.length * BYTES_PER_CANDLE);
|
|
131
|
+
const view = new DataView(buf);
|
|
132
|
+
for (let i = 0; i < candles.length; i++) {
|
|
133
|
+
const c = candles[i];
|
|
134
|
+
const off = i * BYTES_PER_CANDLE;
|
|
135
|
+
view.setBigInt64(off, BigInt(Math.trunc(c.timeMs)), true);
|
|
136
|
+
view.setFloat64(off + 8, c.open, true);
|
|
137
|
+
view.setFloat64(off + 16, c.high, true);
|
|
138
|
+
view.setFloat64(off + 24, c.low, true);
|
|
139
|
+
view.setFloat64(off + 32, c.close, true);
|
|
140
|
+
view.setFloat64(off + 40, c.volume, true);
|
|
141
|
+
}
|
|
142
|
+
return buf;
|
|
143
|
+
}
|
|
144
|
+
function unpackCandles(buf) {
|
|
145
|
+
const view = new DataView(buf);
|
|
146
|
+
const n = Math.floor(buf.byteLength / BYTES_PER_CANDLE);
|
|
147
|
+
const out = new Array(n);
|
|
148
|
+
for (let i = 0; i < n; i++) {
|
|
149
|
+
const off = i * BYTES_PER_CANDLE;
|
|
150
|
+
out[i] = {
|
|
151
|
+
timeMs: Number(view.getBigInt64(off, true)),
|
|
152
|
+
open: view.getFloat64(off + 8, true),
|
|
153
|
+
high: view.getFloat64(off + 16, true),
|
|
154
|
+
low: view.getFloat64(off + 24, true),
|
|
155
|
+
close: view.getFloat64(off + 32, true),
|
|
156
|
+
volume: view.getFloat64(off + 40, true)
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
return out;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// src/color.ts
|
|
163
|
+
var COLOR_KEYS = {
|
|
164
|
+
background: 0 /* Background */,
|
|
165
|
+
bull: 1 /* Bull */,
|
|
166
|
+
bear: 2 /* Bear */,
|
|
167
|
+
grid: 4 /* Grid */,
|
|
168
|
+
axisText: 5 /* AxisText */,
|
|
169
|
+
crosshair: 6 /* Crosshair */,
|
|
170
|
+
crosshairTarget: 9 /* CrosshairTarget */
|
|
171
|
+
};
|
|
172
|
+
function parseColor(value) {
|
|
173
|
+
if (typeof value === "number") {
|
|
174
|
+
return Number.isFinite(value) ? value >>> 0 : null;
|
|
175
|
+
}
|
|
176
|
+
let s = value.trim();
|
|
177
|
+
if (s.startsWith("#")) s = s.slice(1);
|
|
178
|
+
if (s.length === 6) s = `ff${s}`;
|
|
179
|
+
if (s.length !== 8 || !/^[0-9a-fA-F]{8}$/.test(s)) return null;
|
|
180
|
+
return parseInt(s, 16) >>> 0;
|
|
181
|
+
}
|
|
182
|
+
function applyTheme(handle, theme) {
|
|
183
|
+
Object.keys(COLOR_KEYS).forEach((field) => {
|
|
184
|
+
const value = theme[field];
|
|
185
|
+
if (value == null) return;
|
|
186
|
+
const argb = parseColor(value);
|
|
187
|
+
if (argb == null) return;
|
|
188
|
+
handle.setColor(COLOR_KEYS[field], argb);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
function argbToCss(argb) {
|
|
192
|
+
const a = (argb >>> 24 & 255) / 255;
|
|
193
|
+
const r = argb >>> 16 & 255;
|
|
194
|
+
const g = argb >>> 8 & 255;
|
|
195
|
+
const b = argb & 255;
|
|
196
|
+
return `rgba(${r}, ${g}, ${b}, ${a})`;
|
|
197
|
+
}
|
|
198
|
+
|
|
558
199
|
// src/index.ts
|
|
559
200
|
function bundledWasmConfig() {
|
|
560
201
|
return {
|
|
@@ -566,17 +207,11 @@ function bundledWasmConfig() {
|
|
|
566
207
|
var modulePromise = null;
|
|
567
208
|
function loadVroom(opts) {
|
|
568
209
|
if (modulePromise) return modulePromise;
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
modulePromise = loadWasmCore(cfg).catch((err) => {
|
|
575
|
-
if (!fallback) throw err;
|
|
576
|
-
console.warn("[vroom] Skia-WASM core failed to load; using Canvas2D stub.", err);
|
|
577
|
-
return createStubModule();
|
|
578
|
-
});
|
|
579
|
-
}
|
|
210
|
+
const cfg = opts?.wasm ?? bundledWasmConfig();
|
|
211
|
+
modulePromise = loadWasmCore(cfg).catch((err) => {
|
|
212
|
+
modulePromise = null;
|
|
213
|
+
throw err;
|
|
214
|
+
});
|
|
580
215
|
return modulePromise;
|
|
581
216
|
}
|
|
582
217
|
function resetVroomForTesting() {
|