@vroomchart/react 0.2.0 → 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/dist/index.d.ts +125 -13
- package/dist/index.js +765 -49
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -76,13 +76,17 @@ function overlayToNumeric(o) {
|
|
|
76
76
|
};
|
|
77
77
|
}
|
|
78
78
|
function drawingToSpec(d) {
|
|
79
|
+
const first = d.points[0];
|
|
80
|
+
const last = d.points[d.points.length - 1];
|
|
79
81
|
return {
|
|
80
|
-
aTime:
|
|
81
|
-
aPrice:
|
|
82
|
-
bTime:
|
|
83
|
-
bPrice:
|
|
82
|
+
aTime: first.timeMs,
|
|
83
|
+
aPrice: first.price,
|
|
84
|
+
bTime: last.timeMs,
|
|
85
|
+
bPrice: last.price,
|
|
84
86
|
color: (d.color != null ? parseColor(d.color) : null) ?? 4280902399,
|
|
85
|
-
width: d.width ?? 2
|
|
87
|
+
width: d.width ?? 2,
|
|
88
|
+
kind: d.type === "box" ? 1 : d.type === "pencil" ? 2 : 0,
|
|
89
|
+
...d.type === "pencil" ? { points: d.points.map((p) => ({ timeMs: p.timeMs, price: p.price })) } : {}
|
|
86
90
|
};
|
|
87
91
|
}
|
|
88
92
|
var DEFAULT_BUY_COLOR = 4280723098;
|
|
@@ -122,6 +126,8 @@ function useChartCore(props, loadOpts) {
|
|
|
122
126
|
height: heightProp,
|
|
123
127
|
visibleRange,
|
|
124
128
|
defaultCandleWidth,
|
|
129
|
+
chartType,
|
|
130
|
+
transitionMs,
|
|
125
131
|
theme,
|
|
126
132
|
rsi,
|
|
127
133
|
macd,
|
|
@@ -257,11 +263,103 @@ function useChartCore(props, loadOpts) {
|
|
|
257
263
|
);
|
|
258
264
|
scheduleRender();
|
|
259
265
|
}, [ready, width, height, candles, seriesKey, explicit, startMs, endMs, defaultCandleWidth, themeKey, rsiKey, macdKey, maKey, vwapKey, drawingsKey, liquidityKey, scheduleRender]);
|
|
266
|
+
const morphRafRef = useRef(null);
|
|
267
|
+
const morphFadeRef = useRef(null);
|
|
268
|
+
const morphHandleRef = useRef(null);
|
|
269
|
+
useEffect(() => {
|
|
270
|
+
if (!ready) return;
|
|
271
|
+
const h = handleRef.current;
|
|
272
|
+
if (!h) return;
|
|
273
|
+
const target = chartType === "line" ? 1 : 0;
|
|
274
|
+
if (morphHandleRef.current !== h || morphFadeRef.current == null) {
|
|
275
|
+
morphHandleRef.current = h;
|
|
276
|
+
morphFadeRef.current = target;
|
|
277
|
+
h.setChartType(target);
|
|
278
|
+
scheduleRender();
|
|
279
|
+
return;
|
|
280
|
+
}
|
|
281
|
+
if (morphFadeRef.current === target) return;
|
|
282
|
+
const reduce = !!(typeof window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-reduced-motion: reduce)").matches);
|
|
283
|
+
const dur = Math.max(0, transitionMs ?? 300);
|
|
284
|
+
if (morphRafRef.current != null) {
|
|
285
|
+
cancelAnimationFrame(morphRafRef.current);
|
|
286
|
+
morphRafRef.current = null;
|
|
287
|
+
}
|
|
288
|
+
if (dur === 0) {
|
|
289
|
+
morphFadeRef.current = target;
|
|
290
|
+
h.setChartType(target);
|
|
291
|
+
scheduleRender();
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
const from = morphFadeRef.current;
|
|
295
|
+
const start = performance.now();
|
|
296
|
+
const step = (now) => {
|
|
297
|
+
const p = Math.min(1, (now - start) / dur);
|
|
298
|
+
const e = p * p * (3 - 2 * p);
|
|
299
|
+
const fade = from + (target - from) * e;
|
|
300
|
+
morphFadeRef.current = fade;
|
|
301
|
+
h.setMorph(reduce ? 0 : fade, fade);
|
|
302
|
+
h.present();
|
|
303
|
+
if (p < 1) {
|
|
304
|
+
morphRafRef.current = requestAnimationFrame(step);
|
|
305
|
+
} else {
|
|
306
|
+
morphRafRef.current = null;
|
|
307
|
+
morphFadeRef.current = target;
|
|
308
|
+
h.setChartType(target);
|
|
309
|
+
h.present();
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
morphRafRef.current = requestAnimationFrame(step);
|
|
313
|
+
return () => {
|
|
314
|
+
if (morphRafRef.current != null) {
|
|
315
|
+
cancelAnimationFrame(morphRafRef.current);
|
|
316
|
+
morphRafRef.current = null;
|
|
317
|
+
}
|
|
318
|
+
};
|
|
319
|
+
}, [ready, chartType, transitionMs, scheduleRender]);
|
|
260
320
|
return { containerRef, canvasRef, handleRef, scheduleRender, size: { width, height } };
|
|
261
321
|
}
|
|
262
322
|
|
|
263
323
|
// src/useGestures.ts
|
|
264
|
-
import { useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
324
|
+
import { useCallback as useCallback2, useEffect as useEffect2, useRef as useRef2 } from "react";
|
|
325
|
+
|
|
326
|
+
// src/simplify.ts
|
|
327
|
+
function perpendicularDistance(p, a, b) {
|
|
328
|
+
const dx = b.x - a.x;
|
|
329
|
+
const dy = b.y - a.y;
|
|
330
|
+
if (dx === 0 && dy === 0) return Math.hypot(p.x - a.x, p.y - a.y);
|
|
331
|
+
return Math.abs(dy * p.x - dx * p.y + b.x * a.y - b.y * a.x) / Math.hypot(dx, dy);
|
|
332
|
+
}
|
|
333
|
+
function simplifyIndices(points, epsilon) {
|
|
334
|
+
const n = points.length;
|
|
335
|
+
if (n <= 2) return points.map((_, i) => i);
|
|
336
|
+
if (epsilon <= 0) return points.map((_, i) => i);
|
|
337
|
+
const keep = new Uint8Array(n);
|
|
338
|
+
keep[0] = 1;
|
|
339
|
+
keep[n - 1] = 1;
|
|
340
|
+
const stack = [[0, n - 1]];
|
|
341
|
+
while (stack.length > 0) {
|
|
342
|
+
const [start, end] = stack.pop();
|
|
343
|
+
if (end <= start + 1) continue;
|
|
344
|
+
let farthest = -1;
|
|
345
|
+
let maxDist = -1;
|
|
346
|
+
for (let i = start + 1; i < end; i++) {
|
|
347
|
+
const d = perpendicularDistance(points[i], points[start], points[end]);
|
|
348
|
+
if (d > maxDist) {
|
|
349
|
+
maxDist = d;
|
|
350
|
+
farthest = i;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
if (maxDist <= epsilon || farthest < 0) continue;
|
|
354
|
+
keep[farthest] = 1;
|
|
355
|
+
stack.push([start, farthest], [farthest, end]);
|
|
356
|
+
}
|
|
357
|
+
const out = [];
|
|
358
|
+
for (let i = 0; i < n; i++) if (keep[i]) out.push(i);
|
|
359
|
+
return out;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// src/useGestures.ts
|
|
265
363
|
var MIN_SPAN = 24;
|
|
266
364
|
var AXIS_RATIO = 0.5;
|
|
267
365
|
var LONG_PRESS_MS = 350;
|
|
@@ -270,34 +368,197 @@ var WHEEL_K = 15e-4;
|
|
|
270
368
|
var SEP_HIT = 4;
|
|
271
369
|
var DRAW_COLOR = 4280902399;
|
|
272
370
|
var DRAW_WIDTH = 2;
|
|
273
|
-
var
|
|
371
|
+
var PENCIL_MIN_DIST = 2;
|
|
372
|
+
var PENCIL_EPSILON = 1;
|
|
274
373
|
function drawingId() {
|
|
275
374
|
if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") {
|
|
276
375
|
return crypto.randomUUID();
|
|
277
376
|
}
|
|
278
377
|
return `draw-${Math.random().toString(36).slice(2)}`;
|
|
279
378
|
}
|
|
280
|
-
function
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
379
|
+
function bodyPart(type) {
|
|
380
|
+
return type === "box" ? 4 : type === "pencil" ? 5 : 2;
|
|
381
|
+
}
|
|
382
|
+
function makeDrawing(base, type, pts) {
|
|
383
|
+
if (pts.length < 2) return null;
|
|
384
|
+
const style = {
|
|
385
|
+
...base.color != null ? { color: base.color } : {},
|
|
386
|
+
...base.width != null ? { width: base.width } : {}
|
|
387
|
+
};
|
|
388
|
+
if (type === "pencil") return { id: base.id, type, points: pts, ...style };
|
|
389
|
+
return { id: base.id, type, points: [pts[0], pts[pts.length - 1]], ...style };
|
|
390
|
+
}
|
|
391
|
+
function boxCorners(points) {
|
|
392
|
+
const [a, b] = points;
|
|
393
|
+
return [
|
|
394
|
+
{ timeMs: a.timeMs, price: a.price },
|
|
395
|
+
{ timeMs: b.timeMs, price: a.price },
|
|
396
|
+
{ timeMs: b.timeMs, price: b.price },
|
|
397
|
+
{ timeMs: a.timeMs, price: b.price }
|
|
398
|
+
];
|
|
287
399
|
}
|
|
288
400
|
function useGestures(containerRef, handleRef, scheduleRender, opts) {
|
|
289
401
|
const optsRef = useRef2(opts);
|
|
290
402
|
optsRef.current = opts;
|
|
291
403
|
const drawAnchorRef = useRef2(null);
|
|
292
|
-
const
|
|
293
|
-
const
|
|
404
|
+
const selectedIdRef = useRef2(null);
|
|
405
|
+
const grabRef = useRef2(null);
|
|
406
|
+
const dragLineRef = useRef2(null);
|
|
407
|
+
const pencilRef = useRef2(
|
|
408
|
+
null
|
|
409
|
+
);
|
|
410
|
+
const downHitRef = useRef2(null);
|
|
411
|
+
const lastCoordRef = useRef2(null);
|
|
412
|
+
const selectedTRef = useRef2(0.5);
|
|
413
|
+
const clipboardRef = useRef2(null);
|
|
414
|
+
const applySelection = useCallback2(() => {
|
|
415
|
+
const h = handleRef.current;
|
|
416
|
+
if (!h) return;
|
|
417
|
+
const id = selectedIdRef.current;
|
|
418
|
+
const list = optsRef.current.drawings ?? [];
|
|
419
|
+
let index = -1;
|
|
420
|
+
if (id != null) {
|
|
421
|
+
index = list.findIndex((d) => d.id === id);
|
|
422
|
+
if (index < 0) selectedIdRef.current = null;
|
|
423
|
+
}
|
|
424
|
+
h.setSelectedDrawing(index, index >= 0 ? grabRef.current?.endpoint ?? -1 : -1);
|
|
425
|
+
scheduleRender();
|
|
426
|
+
}, [handleRef, scheduleRender]);
|
|
427
|
+
useEffect2(() => {
|
|
428
|
+
applySelection();
|
|
429
|
+
}, [opts.drawings, applySelection]);
|
|
430
|
+
useEffect2(() => {
|
|
431
|
+
const isEditableTarget = () => {
|
|
432
|
+
const ae = document.activeElement;
|
|
433
|
+
const tag = ae?.tagName;
|
|
434
|
+
return tag === "INPUT" || tag === "TEXTAREA" || !!ae?.isContentEditable;
|
|
435
|
+
};
|
|
436
|
+
const findSelected = () => {
|
|
437
|
+
const id = selectedIdRef.current;
|
|
438
|
+
if (id == null) return null;
|
|
439
|
+
return (optsRef.current.drawings ?? []).find((x) => x.id === id) ?? null;
|
|
440
|
+
};
|
|
441
|
+
const placePaste = (clip) => {
|
|
442
|
+
const h = handleRef.current;
|
|
443
|
+
if (!h) return null;
|
|
444
|
+
const pts = clip.points;
|
|
445
|
+
if (pts.length < 2) return null;
|
|
446
|
+
const a = pts[0];
|
|
447
|
+
const b = pts[pts.length - 1];
|
|
448
|
+
const t = clip.t;
|
|
449
|
+
const pSel = {
|
|
450
|
+
timeMs: a.timeMs + t * (b.timeMs - a.timeMs),
|
|
451
|
+
price: a.price + t * (b.price - a.price)
|
|
452
|
+
};
|
|
453
|
+
const cur = h.getCrosshairInfo();
|
|
454
|
+
const moved = cur != null && (clip.crosshair == null || cur.timeMs !== clip.crosshair.timeMs || cur.price !== clip.crosshair.price);
|
|
455
|
+
if (moved && cur) {
|
|
456
|
+
const dt = cur.timeMs - pSel.timeMs;
|
|
457
|
+
const dp = cur.price - pSel.price;
|
|
458
|
+
return pts.map((q) => ({ timeMs: q.timeMs + dt, price: q.price + dp }));
|
|
459
|
+
}
|
|
460
|
+
const el = containerRef.current;
|
|
461
|
+
const shift = (dp) => pts.map((q) => ({ timeMs: q.timeMs, price: q.price + dp }));
|
|
462
|
+
const prices = pts.map((q) => q.price);
|
|
463
|
+
const span = Math.max(...prices) - Math.min(...prices);
|
|
464
|
+
if (!el) return shift(-(span || 1));
|
|
465
|
+
const rect = el.getBoundingClientRect();
|
|
466
|
+
const m = h.getAxisMetrics();
|
|
467
|
+
const priceBottom = rect.height - m.xAxisHeight - m.indicatorHeight;
|
|
468
|
+
const top = h.coordAt(rect.width / 2, 0);
|
|
469
|
+
const bot = h.coordAt(rect.width / 2, priceBottom);
|
|
470
|
+
if (!top || !bot || priceBottom <= 0 || top.price <= bot.price) return shift(-(span || 1));
|
|
471
|
+
const vTop = top.price;
|
|
472
|
+
const vBot = bot.price;
|
|
473
|
+
const pricePerPx = (vTop - vBot) / priceBottom;
|
|
474
|
+
const GAP_PX = 30;
|
|
475
|
+
const offset = (span / pricePerPx + GAP_PX) * pricePerPx;
|
|
476
|
+
const pMin = Math.min(...prices);
|
|
477
|
+
const pMax = Math.max(...prices);
|
|
478
|
+
const upFits = pMax + offset <= vTop;
|
|
479
|
+
const downFits = pMin - offset >= vBot;
|
|
480
|
+
let dir;
|
|
481
|
+
if (upFits && !downFits) dir = 1;
|
|
482
|
+
else if (downFits && !upFits) dir = -1;
|
|
483
|
+
else if (upFits && downFits) {
|
|
484
|
+
dir = vTop - (pMax + offset) >= pMin - offset - vBot ? 1 : -1;
|
|
485
|
+
} else {
|
|
486
|
+
const upVis = Math.min(pMax + offset, vTop) - Math.max(pMin + offset, vBot);
|
|
487
|
+
const dnVis = Math.min(pMax - offset, vTop) - Math.max(pMin - offset, vBot);
|
|
488
|
+
dir = upVis >= dnVis ? 1 : -1;
|
|
489
|
+
}
|
|
490
|
+
return shift(dir * offset);
|
|
491
|
+
};
|
|
492
|
+
const onKeyDown = (e) => {
|
|
493
|
+
if (isEditableTarget()) return;
|
|
494
|
+
if (drawAnchorRef.current && (e.key === "Escape" || e.key === "Backspace" || e.key === "Delete")) {
|
|
495
|
+
e.preventDefault();
|
|
496
|
+
drawAnchorRef.current = null;
|
|
497
|
+
const h = handleRef.current;
|
|
498
|
+
if (h) {
|
|
499
|
+
h.clearDraft();
|
|
500
|
+
scheduleRender();
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (e.key === "Backspace" || e.key === "Delete") {
|
|
505
|
+
const id = selectedIdRef.current;
|
|
506
|
+
if (id == null) return;
|
|
507
|
+
e.preventDefault();
|
|
508
|
+
optsRef.current.onDrawingDelete?.(id);
|
|
509
|
+
selectedIdRef.current = null;
|
|
510
|
+
grabRef.current = null;
|
|
511
|
+
dragLineRef.current = null;
|
|
512
|
+
const h = handleRef.current;
|
|
513
|
+
if (h) {
|
|
514
|
+
h.setSelectedDrawing(-1, -1);
|
|
515
|
+
scheduleRender();
|
|
516
|
+
}
|
|
517
|
+
return;
|
|
518
|
+
}
|
|
519
|
+
if (!(e.metaKey || e.ctrlKey)) return;
|
|
520
|
+
const key = e.key.toLowerCase();
|
|
521
|
+
if (key === "c") {
|
|
522
|
+
if (window.getSelection()?.toString()) return;
|
|
523
|
+
const d = findSelected();
|
|
524
|
+
if (!d) return;
|
|
525
|
+
e.preventDefault();
|
|
526
|
+
const info = handleRef.current?.getCrosshairInfo() ?? null;
|
|
527
|
+
clipboardRef.current = {
|
|
528
|
+
type: d.type,
|
|
529
|
+
points: d.points.map((q) => ({ timeMs: q.timeMs, price: q.price })),
|
|
530
|
+
color: d.color,
|
|
531
|
+
width: d.width,
|
|
532
|
+
t: selectedTRef.current,
|
|
533
|
+
crosshair: info ? { timeMs: info.timeMs, price: info.price } : null
|
|
534
|
+
};
|
|
535
|
+
} else if (key === "v") {
|
|
536
|
+
const clip = clipboardRef.current;
|
|
537
|
+
if (!clip) return;
|
|
538
|
+
const pts = placePaste(clip);
|
|
539
|
+
if (!pts) return;
|
|
540
|
+
const id = drawingId();
|
|
541
|
+
const copy = makeDrawing(
|
|
542
|
+
{ id, color: clip.color, width: clip.width },
|
|
543
|
+
clip.type,
|
|
544
|
+
pts
|
|
545
|
+
);
|
|
546
|
+
if (!copy) return;
|
|
547
|
+
e.preventDefault();
|
|
548
|
+
optsRef.current.onDrawingComplete?.(copy);
|
|
549
|
+
selectedIdRef.current = id;
|
|
550
|
+
selectedTRef.current = clip.t;
|
|
551
|
+
}
|
|
552
|
+
};
|
|
553
|
+
window.addEventListener("keydown", onKeyDown);
|
|
554
|
+
return () => window.removeEventListener("keydown", onKeyDown);
|
|
555
|
+
}, [containerRef, handleRef, scheduleRender]);
|
|
294
556
|
const localCrosshairActiveRef = useRef2(false);
|
|
295
557
|
useEffect2(() => {
|
|
296
|
-
const active = opts.mode === "draw" && opts.tool === "line";
|
|
558
|
+
const active = opts.mode === "draw" && (opts.tool === "line" || opts.tool === "box" || opts.tool === "pencil");
|
|
297
559
|
if (active) return;
|
|
298
560
|
drawAnchorRef.current = null;
|
|
299
|
-
|
|
300
|
-
drawSelectedPxRef.current = null;
|
|
561
|
+
pencilRef.current = null;
|
|
301
562
|
const h = handleRef.current;
|
|
302
563
|
if (h) {
|
|
303
564
|
h.clearDraft();
|
|
@@ -325,11 +586,46 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
|
|
|
325
586
|
let downX = 0;
|
|
326
587
|
let downY = 0;
|
|
327
588
|
let moved = false;
|
|
589
|
+
let shiftHeld = false;
|
|
590
|
+
let lastX = 0;
|
|
591
|
+
let lastY = 0;
|
|
328
592
|
const pinch = { spanX: 1, spanY: 1, ratioX: 1, ratioY: 1, enableX: false, enableY: false, active: false };
|
|
329
593
|
const rel = (e) => {
|
|
330
594
|
const r = el.getBoundingClientRect();
|
|
331
595
|
return { x: e.clientX - r.left, y: e.clientY - r.top };
|
|
332
596
|
};
|
|
597
|
+
const snapToLine = (fixed, x, y) => {
|
|
598
|
+
const h = handleRef.current;
|
|
599
|
+
if (!h) return null;
|
|
600
|
+
const free = h.coordAt(x, y);
|
|
601
|
+
if (!shiftHeld || !free) return free;
|
|
602
|
+
const f = h.project(fixed.timeMs, fixed.price);
|
|
603
|
+
if (!f) return free;
|
|
604
|
+
const dx = x - f.x;
|
|
605
|
+
const dy = y - f.y;
|
|
606
|
+
if (dx === 0 && dy === 0) return free;
|
|
607
|
+
const step = Math.PI / 4;
|
|
608
|
+
const ang = Math.round(Math.atan2(dy, dx) / step) * step;
|
|
609
|
+
const ux = Math.cos(ang);
|
|
610
|
+
const uy = Math.sin(ang);
|
|
611
|
+
const len = dx * ux + dy * uy;
|
|
612
|
+
return h.coordAt(f.x + len * ux, f.y + len * uy) ?? free;
|
|
613
|
+
};
|
|
614
|
+
const snapToSquare = (fixed, x, y) => {
|
|
615
|
+
const h = handleRef.current;
|
|
616
|
+
if (!h) return null;
|
|
617
|
+
const free = h.coordAt(x, y);
|
|
618
|
+
if (!shiftHeld || !free) return free;
|
|
619
|
+
const f = h.project(fixed.timeMs, fixed.price);
|
|
620
|
+
if (!f) return free;
|
|
621
|
+
const dx = x - f.x;
|
|
622
|
+
const dy = y - f.y;
|
|
623
|
+
const side = Math.max(Math.abs(dx), Math.abs(dy));
|
|
624
|
+
if (side === 0) return free;
|
|
625
|
+
const nx = f.x + (dx < 0 ? -side : side);
|
|
626
|
+
const ny = f.y + (dy < 0 ? -side : side);
|
|
627
|
+
return h.coordAt(nx, ny) ?? free;
|
|
628
|
+
};
|
|
333
629
|
const regionAt = (x, y) => {
|
|
334
630
|
const h = handleRef.current;
|
|
335
631
|
const r = el.getBoundingClientRect();
|
|
@@ -396,61 +692,128 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
|
|
|
396
692
|
scheduleRender();
|
|
397
693
|
reportCrosshair("hide");
|
|
398
694
|
};
|
|
399
|
-
const drawActive = () => optsRef.current.mode === "draw" && optsRef.current.tool === "line";
|
|
695
|
+
const drawActive = () => optsRef.current.mode === "draw" && (optsRef.current.tool === "line" || optsRef.current.tool === "box" || optsRef.current.tool === "pencil");
|
|
696
|
+
const drawIsBox = () => optsRef.current.tool === "box";
|
|
697
|
+
const drawIsPencil = () => optsRef.current.tool === "pencil";
|
|
698
|
+
const snapMoving = (isBox, fixed, x, y) => isBox ? snapToSquare(fixed, x, y) : snapToLine(fixed, x, y);
|
|
400
699
|
const updateGuideline = (x, y) => {
|
|
401
700
|
const h = handleRef.current;
|
|
402
701
|
if (!h) return;
|
|
403
|
-
if (!drawAnchorRef.current || drawSelectedPxRef.current) return;
|
|
404
|
-
const c = h.coordAt(x, y);
|
|
405
|
-
if (!c) return;
|
|
406
702
|
const a = drawAnchorRef.current;
|
|
407
|
-
|
|
703
|
+
if (!a) return;
|
|
704
|
+
const isBox = drawIsBox();
|
|
705
|
+
const c = snapMoving(isBox, a, x, y);
|
|
706
|
+
if (!c) return;
|
|
707
|
+
h.setDraft(a.timeMs, a.price, true, c.timeMs, c.price, true, DRAW_COLOR, DRAW_WIDTH, isBox ? 1 : 0);
|
|
408
708
|
scheduleRender();
|
|
409
709
|
};
|
|
410
710
|
const handleDrawClick = (x, y) => {
|
|
411
711
|
const h = handleRef.current;
|
|
412
712
|
if (!h) return;
|
|
413
|
-
const
|
|
414
|
-
const sel = drawSelectedPxRef.current;
|
|
415
|
-
if (sel) {
|
|
416
|
-
if (distToSegment(x, y, sel.ax, sel.ay, sel.bx, sel.by) <= DRAW_HIT) return;
|
|
417
|
-
drawAnchorRef.current = null;
|
|
418
|
-
drawAnchorPxRef.current = null;
|
|
419
|
-
drawSelectedPxRef.current = null;
|
|
420
|
-
h.clearDraft();
|
|
421
|
-
scheduleRender();
|
|
422
|
-
el.style.cursor = "";
|
|
423
|
-
o.onRequestMode?.("pan");
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
const coord = h.coordAt(x, y);
|
|
427
|
-
if (!coord) return;
|
|
713
|
+
const isBox = drawIsBox();
|
|
428
714
|
if (!drawAnchorRef.current) {
|
|
715
|
+
const coord = h.coordAt(x, y);
|
|
716
|
+
if (!coord) return;
|
|
429
717
|
drawAnchorRef.current = coord;
|
|
430
|
-
|
|
431
|
-
h.setDraft(coord.timeMs, coord.price, false, 0, 0, true, DRAW_COLOR, DRAW_WIDTH);
|
|
718
|
+
h.setDraft(coord.timeMs, coord.price, false, 0, 0, true, DRAW_COLOR, DRAW_WIDTH, isBox ? 1 : 0);
|
|
432
719
|
scheduleRender();
|
|
433
720
|
} else {
|
|
434
721
|
const a = drawAnchorRef.current;
|
|
435
|
-
const
|
|
436
|
-
|
|
722
|
+
const coord = snapMoving(isBox, a, x, y);
|
|
723
|
+
if (!coord) return;
|
|
724
|
+
optsRef.current.onDrawingComplete?.({
|
|
437
725
|
id: drawingId(),
|
|
438
|
-
type: "line",
|
|
726
|
+
type: isBox ? "box" : "line",
|
|
439
727
|
points: [
|
|
440
728
|
{ timeMs: a.timeMs, price: a.price },
|
|
441
729
|
{ timeMs: coord.timeMs, price: coord.price }
|
|
442
730
|
]
|
|
443
731
|
});
|
|
444
|
-
|
|
445
|
-
h.
|
|
732
|
+
drawAnchorRef.current = null;
|
|
733
|
+
h.clearDraft();
|
|
446
734
|
scheduleRender();
|
|
447
735
|
}
|
|
448
736
|
};
|
|
737
|
+
const pencilStart = (x, y) => {
|
|
738
|
+
const h = handleRef.current;
|
|
739
|
+
if (!h) return;
|
|
740
|
+
const c = h.coordAt(x, y);
|
|
741
|
+
if (!c) return;
|
|
742
|
+
pencilRef.current = { pts: [c], px: [{ x, y }] };
|
|
743
|
+
h.startDraftStroke(DRAW_COLOR, DRAW_WIDTH);
|
|
744
|
+
h.appendDraftPoint(c.timeMs, c.price);
|
|
745
|
+
scheduleRender();
|
|
746
|
+
};
|
|
747
|
+
const pencilMove = (x, y) => {
|
|
748
|
+
const h = handleRef.current;
|
|
749
|
+
const s = pencilRef.current;
|
|
750
|
+
if (!h || !s) return;
|
|
751
|
+
const last = s.px[s.px.length - 1];
|
|
752
|
+
if (Math.hypot(x - last.x, y - last.y) < PENCIL_MIN_DIST) return;
|
|
753
|
+
const c = h.coordAt(x, y);
|
|
754
|
+
if (!c) return;
|
|
755
|
+
s.pts.push(c);
|
|
756
|
+
s.px.push({ x, y });
|
|
757
|
+
h.appendDraftPoint(c.timeMs, c.price);
|
|
758
|
+
scheduleRender();
|
|
759
|
+
};
|
|
760
|
+
const priceDecimals = () => {
|
|
761
|
+
const h = handleRef.current;
|
|
762
|
+
const el2 = containerRef.current;
|
|
763
|
+
if (!h || !el2) return 6;
|
|
764
|
+
const rect = el2.getBoundingClientRect();
|
|
765
|
+
const m = h.getAxisMetrics();
|
|
766
|
+
const priceBottom = rect.height - m.xAxisHeight - m.indicatorHeight;
|
|
767
|
+
if (priceBottom <= 0) return 6;
|
|
768
|
+
const top = h.coordAt(rect.width / 2, 0);
|
|
769
|
+
const bot = h.coordAt(rect.width / 2, priceBottom);
|
|
770
|
+
if (!top || !bot || top.price <= bot.price) return 6;
|
|
771
|
+
const pricePerPx = (top.price - bot.price) / priceBottom;
|
|
772
|
+
const d = Math.ceil(-Math.log10(pricePerPx / 100));
|
|
773
|
+
return Math.max(0, Math.min(12, Number.isFinite(d) ? d : 6));
|
|
774
|
+
};
|
|
775
|
+
const roundPoints = (pts) => {
|
|
776
|
+
const dp = priceDecimals();
|
|
777
|
+
return pts.map((p) => ({
|
|
778
|
+
timeMs: Math.round(p.timeMs),
|
|
779
|
+
price: Number(p.price.toFixed(dp))
|
|
780
|
+
}));
|
|
781
|
+
};
|
|
782
|
+
const pencilEnd = () => {
|
|
783
|
+
const h = handleRef.current;
|
|
784
|
+
const s = pencilRef.current;
|
|
785
|
+
pencilRef.current = null;
|
|
786
|
+
if (!h) return;
|
|
787
|
+
h.clearDraft();
|
|
788
|
+
scheduleRender();
|
|
789
|
+
if (!s || s.pts.length < 2) return;
|
|
790
|
+
const keep = simplifyIndices(s.px, PENCIL_EPSILON);
|
|
791
|
+
if (keep.length < 2) return;
|
|
792
|
+
const stroke = makeDrawing(
|
|
793
|
+
{ id: drawingId() },
|
|
794
|
+
"pencil",
|
|
795
|
+
roundPoints(keep.map((i) => s.pts[i]))
|
|
796
|
+
);
|
|
797
|
+
if (stroke) optsRef.current.onDrawingComplete?.(stroke);
|
|
798
|
+
};
|
|
799
|
+
const moveGrab = (x, y) => {
|
|
800
|
+
const h = handleRef.current;
|
|
801
|
+
const g = grabRef.current;
|
|
802
|
+
if (!h || !g) return;
|
|
803
|
+
const c = snapMoving(g.isBox, g.fixed, x, y);
|
|
804
|
+
if (!c) return;
|
|
805
|
+
lastCoordRef.current = c;
|
|
806
|
+
h.moveDrawingEndpoint(g.index, g.endpoint, c.timeMs, c.price);
|
|
807
|
+
scheduleRender();
|
|
808
|
+
};
|
|
449
809
|
const onPointerDown = (e) => {
|
|
450
810
|
const h = handleRef.current;
|
|
451
811
|
if (!h) return;
|
|
452
812
|
if (e.pointerType === "mouse" && e.button !== 0) return;
|
|
453
813
|
const { x, y } = rel(e);
|
|
814
|
+
shiftHeld = e.shiftKey;
|
|
815
|
+
lastX = x;
|
|
816
|
+
lastY = y;
|
|
454
817
|
el.setPointerCapture(e.pointerId);
|
|
455
818
|
pointers.set(e.pointerId, { x, y });
|
|
456
819
|
if (pointers.size === 2) {
|
|
@@ -472,6 +835,66 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
|
|
|
472
835
|
downY = y;
|
|
473
836
|
moved = false;
|
|
474
837
|
panMode = regionAt(x, y);
|
|
838
|
+
if (drawIsPencil() && drawActive() && panMode === "chart") {
|
|
839
|
+
pencilStart(x, y);
|
|
840
|
+
return;
|
|
841
|
+
}
|
|
842
|
+
downHitRef.current = null;
|
|
843
|
+
if (!drawActive() && panMode === "chart") {
|
|
844
|
+
const hit = h.hitTestDrawing(x, y);
|
|
845
|
+
const gd = hit ? (optsRef.current.drawings ?? [])[hit.index] : void 0;
|
|
846
|
+
if (hit && gd && gd.type === "box" && hit.part <= 3) {
|
|
847
|
+
const corners = boxCorners(gd.points);
|
|
848
|
+
const grabbed = corners[hit.part];
|
|
849
|
+
const diagonal = corners[(hit.part + 2) % 4];
|
|
850
|
+
h.moveDrawingEndpoint(hit.index, 0, grabbed.timeMs, grabbed.price);
|
|
851
|
+
h.moveDrawingEndpoint(hit.index, 1, diagonal.timeMs, diagonal.price);
|
|
852
|
+
grabRef.current = { index: hit.index, endpoint: 0, fixed: diagonal, isBox: true };
|
|
853
|
+
lastCoordRef.current = grabbed;
|
|
854
|
+
applySelection();
|
|
855
|
+
return;
|
|
856
|
+
}
|
|
857
|
+
if (hit && gd && gd.type === "line" && hit.part <= 1) {
|
|
858
|
+
grabRef.current = {
|
|
859
|
+
index: hit.index,
|
|
860
|
+
endpoint: hit.part,
|
|
861
|
+
fixed: gd.points[hit.part === 0 ? 1 : 0],
|
|
862
|
+
// the other end stays put
|
|
863
|
+
isBox: false
|
|
864
|
+
};
|
|
865
|
+
lastCoordRef.current = null;
|
|
866
|
+
applySelection();
|
|
867
|
+
return;
|
|
868
|
+
}
|
|
869
|
+
const isBody = hit != null && gd != null && hit.part === bodyPart(gd.type);
|
|
870
|
+
if (hit && isBody) {
|
|
871
|
+
const list = optsRef.current.drawings ?? [];
|
|
872
|
+
const selIdx = selectedIdRef.current ? list.findIndex((d) => d.id === selectedIdRef.current) : -1;
|
|
873
|
+
if (hit.index === selIdx && selIdx >= 0) {
|
|
874
|
+
const start = h.coordAt(x, y);
|
|
875
|
+
const d = list[selIdx];
|
|
876
|
+
if (start && d) {
|
|
877
|
+
const first = d.points[0];
|
|
878
|
+
const last = d.points[d.points.length - 1];
|
|
879
|
+
dragLineRef.current = {
|
|
880
|
+
index: selIdx,
|
|
881
|
+
startTime: start.timeMs,
|
|
882
|
+
startPrice: start.price,
|
|
883
|
+
a0: first,
|
|
884
|
+
b0: last,
|
|
885
|
+
lastA: first,
|
|
886
|
+
lastB: last,
|
|
887
|
+
// A stroke can't be restated cheaply each frame, so it keeps its
|
|
888
|
+
// original points and is nudged with relative translate calls.
|
|
889
|
+
...d.type === "pencil" ? { points0: d.points, appliedTime: 0, appliedPrice: 0 } : {}
|
|
890
|
+
};
|
|
891
|
+
el.style.cursor = "move";
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
downHitRef.current = hit;
|
|
897
|
+
}
|
|
475
898
|
if (e.pointerType !== "mouse" && panMode === "chart" && !drawActive()) {
|
|
476
899
|
longPressTimer = setTimeout(() => {
|
|
477
900
|
longPressTimer = null;
|
|
@@ -483,6 +906,9 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
|
|
|
483
906
|
const h = handleRef.current;
|
|
484
907
|
if (!h) return;
|
|
485
908
|
const { x, y } = rel(e);
|
|
909
|
+
shiftHeld = e.shiftKey;
|
|
910
|
+
lastX = x;
|
|
911
|
+
lastY = y;
|
|
486
912
|
if (!pointers.has(e.pointerId)) {
|
|
487
913
|
if (e.pointerType === "mouse" && pointers.size === 0) {
|
|
488
914
|
if (drawActive()) {
|
|
@@ -504,6 +930,35 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
|
|
|
504
930
|
const dx = x - prev.x;
|
|
505
931
|
const dy = y - prev.y;
|
|
506
932
|
pointers.set(e.pointerId, { x, y });
|
|
933
|
+
if (pencilRef.current) {
|
|
934
|
+
pencilMove(x, y);
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (grabRef.current) {
|
|
938
|
+
moveGrab(x, y);
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
if (dragLineRef.current) {
|
|
942
|
+
const g = dragLineRef.current;
|
|
943
|
+
const c = h.coordAt(x, y);
|
|
944
|
+
if (c) {
|
|
945
|
+
const dT = c.timeMs - g.startTime;
|
|
946
|
+
const dP = c.price - g.startPrice;
|
|
947
|
+
if (g.points0) {
|
|
948
|
+
h.translateDrawing(g.index, dT - (g.appliedTime ?? 0), dP - (g.appliedPrice ?? 0));
|
|
949
|
+
g.appliedTime = dT;
|
|
950
|
+
g.appliedPrice = dP;
|
|
951
|
+
} else {
|
|
952
|
+
g.lastA = { timeMs: g.a0.timeMs + dT, price: g.a0.price + dP };
|
|
953
|
+
g.lastB = { timeMs: g.b0.timeMs + dT, price: g.b0.price + dP };
|
|
954
|
+
h.moveDrawingEndpoint(g.index, 0, g.lastA.timeMs, g.lastA.price);
|
|
955
|
+
h.moveDrawingEndpoint(g.index, 1, g.lastB.timeMs, g.lastB.price);
|
|
956
|
+
}
|
|
957
|
+
if (!moved && Math.hypot(x - downX, y - downY) > MOVE_THRESH) moved = true;
|
|
958
|
+
scheduleRender();
|
|
959
|
+
}
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
507
962
|
if (pointers.size >= 2 && pinch.active && !drawActive()) {
|
|
508
963
|
const pts = [...pointers.values()];
|
|
509
964
|
const focalX = (pts[0].x + pts[1].x) / 2;
|
|
@@ -552,15 +1007,75 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
|
|
|
552
1007
|
scheduleRender();
|
|
553
1008
|
};
|
|
554
1009
|
const endPointer = (e) => {
|
|
1010
|
+
shiftHeld = e.shiftKey;
|
|
555
1011
|
const had = pointers.delete(e.pointerId);
|
|
556
1012
|
clearLongPress();
|
|
557
1013
|
if (pointers.size < 2) pinch.active = false;
|
|
558
1014
|
if (!had) return;
|
|
1015
|
+
if (pencilRef.current && pointers.size === 0) {
|
|
1016
|
+
pencilEnd();
|
|
1017
|
+
el.style.cursor = "crosshair";
|
|
1018
|
+
return;
|
|
1019
|
+
}
|
|
1020
|
+
if (grabRef.current && pointers.size === 0) {
|
|
1021
|
+
const g = grabRef.current;
|
|
1022
|
+
grabRef.current = null;
|
|
1023
|
+
const list = optsRef.current.drawings ?? [];
|
|
1024
|
+
const d = list[g.index];
|
|
1025
|
+
const c = lastCoordRef.current;
|
|
1026
|
+
if (d && c) {
|
|
1027
|
+
const movedPt = { timeMs: c.timeMs, price: c.price };
|
|
1028
|
+
const pts = g.isBox ? [movedPt, g.fixed] : [d.points[0], d.points[1]];
|
|
1029
|
+
if (!g.isBox) pts[g.endpoint] = movedPt;
|
|
1030
|
+
const next = makeDrawing({ id: d.id, color: d.color, width: d.width }, d.type, pts);
|
|
1031
|
+
if (next) optsRef.current.onDrawingChange?.(next);
|
|
1032
|
+
}
|
|
1033
|
+
applySelection();
|
|
1034
|
+
el.style.cursor = "";
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
if (dragLineRef.current && pointers.size === 0) {
|
|
1038
|
+
const g = dragLineRef.current;
|
|
1039
|
+
dragLineRef.current = null;
|
|
1040
|
+
if (moved) {
|
|
1041
|
+
const list = optsRef.current.drawings ?? [];
|
|
1042
|
+
const d = list[g.index];
|
|
1043
|
+
if (d) {
|
|
1044
|
+
const pts = g.points0 ? roundPoints(
|
|
1045
|
+
g.points0.map((p) => ({
|
|
1046
|
+
timeMs: p.timeMs + (g.appliedTime ?? 0),
|
|
1047
|
+
price: p.price + (g.appliedPrice ?? 0)
|
|
1048
|
+
}))
|
|
1049
|
+
) : [g.lastA, g.lastB];
|
|
1050
|
+
const next = makeDrawing({ id: d.id, color: d.color, width: d.width }, d.type, pts);
|
|
1051
|
+
if (next) optsRef.current.onDrawingChange?.(next);
|
|
1052
|
+
}
|
|
1053
|
+
}
|
|
1054
|
+
applySelection();
|
|
1055
|
+
el.style.cursor = "";
|
|
1056
|
+
return;
|
|
1057
|
+
}
|
|
559
1058
|
if (drawActive() && pointers.size === 0) {
|
|
560
1059
|
if (!moved && panMode === "chart") handleDrawClick(downX, downY);
|
|
561
1060
|
else el.style.cursor = "crosshair";
|
|
562
1061
|
return;
|
|
563
1062
|
}
|
|
1063
|
+
if (pointers.size === 0 && !moved && panMode === "chart" && crosshairSource !== "press") {
|
|
1064
|
+
const hit = downHitRef.current;
|
|
1065
|
+
const list = optsRef.current.drawings ?? [];
|
|
1066
|
+
const hitDrawing = hit ? list[hit.index] : void 0;
|
|
1067
|
+
const isBodyHit = hit != null && hitDrawing != null && hit.part === bodyPart(hitDrawing.type);
|
|
1068
|
+
if (hit && isBodyHit && hitDrawing) {
|
|
1069
|
+
selectedTRef.current = hit.t;
|
|
1070
|
+
if (selectedIdRef.current !== hitDrawing.id) {
|
|
1071
|
+
selectedIdRef.current = hitDrawing.id;
|
|
1072
|
+
applySelection();
|
|
1073
|
+
}
|
|
1074
|
+
} else if (!hit && selectedIdRef.current != null) {
|
|
1075
|
+
selectedIdRef.current = null;
|
|
1076
|
+
applySelection();
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
564
1079
|
if (crosshairSource === "press" && pointers.size === 0) {
|
|
565
1080
|
hideCrosshair();
|
|
566
1081
|
} else if (moved && (panMode === "chart" || panMode === "indicator")) {
|
|
@@ -591,12 +1106,22 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
|
|
|
591
1106
|
}
|
|
592
1107
|
scheduleRender();
|
|
593
1108
|
};
|
|
1109
|
+
const onShiftKey = (e) => {
|
|
1110
|
+
if (e.key !== "Shift") return;
|
|
1111
|
+
const held = e.type === "keydown";
|
|
1112
|
+
if (held === shiftHeld) return;
|
|
1113
|
+
shiftHeld = held;
|
|
1114
|
+
if (drawAnchorRef.current) updateGuideline(lastX, lastY);
|
|
1115
|
+
else if (grabRef.current) moveGrab(lastX, lastY);
|
|
1116
|
+
};
|
|
594
1117
|
el.addEventListener("pointerdown", onPointerDown);
|
|
595
1118
|
el.addEventListener("pointermove", onPointerMove);
|
|
596
1119
|
el.addEventListener("pointerup", endPointer);
|
|
597
1120
|
el.addEventListener("pointercancel", endPointer);
|
|
598
1121
|
el.addEventListener("pointerleave", onPointerLeave);
|
|
599
1122
|
el.addEventListener("wheel", onWheel, { passive: false });
|
|
1123
|
+
window.addEventListener("keydown", onShiftKey);
|
|
1124
|
+
window.addEventListener("keyup", onShiftKey);
|
|
600
1125
|
return () => {
|
|
601
1126
|
clearLongPress();
|
|
602
1127
|
el.removeEventListener("pointerdown", onPointerDown);
|
|
@@ -605,10 +1130,187 @@ function useGestures(containerRef, handleRef, scheduleRender, opts) {
|
|
|
605
1130
|
el.removeEventListener("pointercancel", endPointer);
|
|
606
1131
|
el.removeEventListener("pointerleave", onPointerLeave);
|
|
607
1132
|
el.removeEventListener("wheel", onWheel);
|
|
1133
|
+
window.removeEventListener("keydown", onShiftKey);
|
|
1134
|
+
window.removeEventListener("keyup", onShiftKey);
|
|
608
1135
|
};
|
|
609
1136
|
}, [containerRef, handleRef, scheduleRender]);
|
|
610
1137
|
}
|
|
611
1138
|
|
|
1139
|
+
// src/useManagedDrawings.ts
|
|
1140
|
+
import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState as useState2 } from "react";
|
|
1141
|
+
|
|
1142
|
+
// src/drawingStorage.ts
|
|
1143
|
+
var DRAWINGS_VERSION = 1;
|
|
1144
|
+
var KNOWN_TYPES = /* @__PURE__ */ new Set(["line", "box", "pencil"]);
|
|
1145
|
+
var FIXED_POINT_TYPES = /* @__PURE__ */ new Set(["line", "box"]);
|
|
1146
|
+
function isPoint(p) {
|
|
1147
|
+
if (p == null || typeof p !== "object") return false;
|
|
1148
|
+
const { timeMs, price } = p;
|
|
1149
|
+
return typeof timeMs === "number" && Number.isFinite(timeMs) && typeof price === "number" && Number.isFinite(price);
|
|
1150
|
+
}
|
|
1151
|
+
function isValidDrawing(d) {
|
|
1152
|
+
if (d == null || typeof d !== "object") return false;
|
|
1153
|
+
const { type, points } = d;
|
|
1154
|
+
if (typeof type !== "string" || !KNOWN_TYPES.has(type)) {
|
|
1155
|
+
return false;
|
|
1156
|
+
}
|
|
1157
|
+
if (!Array.isArray(points)) return false;
|
|
1158
|
+
if (FIXED_POINT_TYPES.has(type) ? points.length !== 2 : points.length < 2) {
|
|
1159
|
+
return false;
|
|
1160
|
+
}
|
|
1161
|
+
return points.every(isPoint);
|
|
1162
|
+
}
|
|
1163
|
+
var MIGRATIONS = {};
|
|
1164
|
+
function serializeDrawings(drawings) {
|
|
1165
|
+
const env = { v: DRAWINGS_VERSION, drawings };
|
|
1166
|
+
return JSON.stringify(env);
|
|
1167
|
+
}
|
|
1168
|
+
function deserializeDrawings(raw) {
|
|
1169
|
+
if (raw == null || raw === "") return [];
|
|
1170
|
+
let parsed;
|
|
1171
|
+
try {
|
|
1172
|
+
parsed = JSON.parse(raw);
|
|
1173
|
+
} catch {
|
|
1174
|
+
return [];
|
|
1175
|
+
}
|
|
1176
|
+
let env;
|
|
1177
|
+
if (Array.isArray(parsed)) {
|
|
1178
|
+
env = { v: 0, drawings: parsed };
|
|
1179
|
+
} else if (parsed != null && typeof parsed === "object") {
|
|
1180
|
+
const obj = parsed;
|
|
1181
|
+
env = {
|
|
1182
|
+
v: typeof obj.v === "number" ? obj.v : 0,
|
|
1183
|
+
drawings: Array.isArray(obj.drawings) ? obj.drawings : []
|
|
1184
|
+
};
|
|
1185
|
+
} else {
|
|
1186
|
+
return [];
|
|
1187
|
+
}
|
|
1188
|
+
let guard = 0;
|
|
1189
|
+
while (env.v < DRAWINGS_VERSION && guard++ < 100) {
|
|
1190
|
+
const up = MIGRATIONS[env.v];
|
|
1191
|
+
if (!up) break;
|
|
1192
|
+
env = up(env);
|
|
1193
|
+
}
|
|
1194
|
+
return (Array.isArray(env.drawings) ? env.drawings : []).filter(isValidDrawing);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
// src/useManagedDrawings.ts
|
|
1198
|
+
var SAVE_DEBOUNCE_MS = 400;
|
|
1199
|
+
function useManagedDrawings(seriesKey, store) {
|
|
1200
|
+
const [drawings, setDrawings] = useState2([]);
|
|
1201
|
+
const drawingsRef = useRef3([]);
|
|
1202
|
+
const storeRef = useRef3(store);
|
|
1203
|
+
storeRef.current = store;
|
|
1204
|
+
const loadedKeyRef = useRef3(void 0);
|
|
1205
|
+
const loadTokenRef = useRef3(0);
|
|
1206
|
+
const saveTimerRef = useRef3(null);
|
|
1207
|
+
const warnedRef = useRef3(false);
|
|
1208
|
+
const setBoth = (list) => {
|
|
1209
|
+
drawingsRef.current = list;
|
|
1210
|
+
setDrawings(list);
|
|
1211
|
+
};
|
|
1212
|
+
const flushSave = useCallback3((key, list) => {
|
|
1213
|
+
if (saveTimerRef.current != null) {
|
|
1214
|
+
clearTimeout(saveTimerRef.current);
|
|
1215
|
+
saveTimerRef.current = null;
|
|
1216
|
+
}
|
|
1217
|
+
const s = storeRef.current;
|
|
1218
|
+
if (!s || key == null) return;
|
|
1219
|
+
try {
|
|
1220
|
+
const r = s.save(key, serializeDrawings(list));
|
|
1221
|
+
if (r && typeof r.catch === "function") {
|
|
1222
|
+
r.catch(() => {
|
|
1223
|
+
});
|
|
1224
|
+
}
|
|
1225
|
+
} catch {
|
|
1226
|
+
}
|
|
1227
|
+
}, []);
|
|
1228
|
+
const scheduleSave = useCallback3(() => {
|
|
1229
|
+
const key = seriesKey;
|
|
1230
|
+
if (!storeRef.current || key == null) return;
|
|
1231
|
+
if (saveTimerRef.current != null) clearTimeout(saveTimerRef.current);
|
|
1232
|
+
saveTimerRef.current = setTimeout(() => {
|
|
1233
|
+
saveTimerRef.current = null;
|
|
1234
|
+
flushSave(key, drawingsRef.current);
|
|
1235
|
+
}, SAVE_DEBOUNCE_MS);
|
|
1236
|
+
}, [seriesKey, flushSave]);
|
|
1237
|
+
useEffect3(() => {
|
|
1238
|
+
const s = storeRef.current;
|
|
1239
|
+
if (!s) return;
|
|
1240
|
+
if (seriesKey == null) {
|
|
1241
|
+
if (!warnedRef.current && typeof console !== "undefined") {
|
|
1242
|
+
console.warn(
|
|
1243
|
+
"[vroom] drawingStore is set but seriesKey is missing \u2014 drawings will render but not persist."
|
|
1244
|
+
);
|
|
1245
|
+
warnedRef.current = true;
|
|
1246
|
+
}
|
|
1247
|
+
setBoth([]);
|
|
1248
|
+
loadedKeyRef.current = void 0;
|
|
1249
|
+
return;
|
|
1250
|
+
}
|
|
1251
|
+
const token = ++loadTokenRef.current;
|
|
1252
|
+
const result = s.load(seriesKey);
|
|
1253
|
+
if (result && typeof result.then === "function") {
|
|
1254
|
+
setBoth([]);
|
|
1255
|
+
loadedKeyRef.current = void 0;
|
|
1256
|
+
result.then((raw) => {
|
|
1257
|
+
if (token !== loadTokenRef.current) return;
|
|
1258
|
+
loadedKeyRef.current = seriesKey;
|
|
1259
|
+
setBoth(deserializeDrawings(raw));
|
|
1260
|
+
}).catch(() => {
|
|
1261
|
+
if (token !== loadTokenRef.current) return;
|
|
1262
|
+
loadedKeyRef.current = seriesKey;
|
|
1263
|
+
setBoth([]);
|
|
1264
|
+
});
|
|
1265
|
+
} else {
|
|
1266
|
+
loadedKeyRef.current = seriesKey;
|
|
1267
|
+
setBoth(deserializeDrawings(result));
|
|
1268
|
+
}
|
|
1269
|
+
return () => {
|
|
1270
|
+
if (loadedKeyRef.current === seriesKey) {
|
|
1271
|
+
flushSave(seriesKey, drawingsRef.current);
|
|
1272
|
+
} else if (saveTimerRef.current != null) {
|
|
1273
|
+
clearTimeout(saveTimerRef.current);
|
|
1274
|
+
saveTimerRef.current = null;
|
|
1275
|
+
}
|
|
1276
|
+
};
|
|
1277
|
+
}, [seriesKey, flushSave]);
|
|
1278
|
+
const onDrawingComplete = useCallback3(
|
|
1279
|
+
(d) => {
|
|
1280
|
+
setDrawings((p) => {
|
|
1281
|
+
const next = [...p, d];
|
|
1282
|
+
drawingsRef.current = next;
|
|
1283
|
+
return next;
|
|
1284
|
+
});
|
|
1285
|
+
scheduleSave();
|
|
1286
|
+
},
|
|
1287
|
+
[scheduleSave]
|
|
1288
|
+
);
|
|
1289
|
+
const onDrawingChange = useCallback3(
|
|
1290
|
+
(d) => {
|
|
1291
|
+
setDrawings((p) => {
|
|
1292
|
+
const next = p.map((x) => x.id === d.id ? d : x);
|
|
1293
|
+
drawingsRef.current = next;
|
|
1294
|
+
return next;
|
|
1295
|
+
});
|
|
1296
|
+
scheduleSave();
|
|
1297
|
+
},
|
|
1298
|
+
[scheduleSave]
|
|
1299
|
+
);
|
|
1300
|
+
const onDrawingDelete = useCallback3(
|
|
1301
|
+
(id) => {
|
|
1302
|
+
setDrawings((p) => {
|
|
1303
|
+
const next = p.filter((x) => x.id !== id);
|
|
1304
|
+
drawingsRef.current = next;
|
|
1305
|
+
return next;
|
|
1306
|
+
});
|
|
1307
|
+
scheduleSave();
|
|
1308
|
+
},
|
|
1309
|
+
[scheduleSave]
|
|
1310
|
+
);
|
|
1311
|
+
return { drawings, onDrawingComplete, onDrawingChange, onDrawingDelete };
|
|
1312
|
+
}
|
|
1313
|
+
|
|
612
1314
|
// src/VroomChart.tsx
|
|
613
1315
|
import { jsx } from "react/jsx-runtime";
|
|
614
1316
|
var FILL = { position: "relative", width: "100%", height: "100%" };
|
|
@@ -628,13 +1330,21 @@ function VroomChart(props) {
|
|
|
628
1330
|
crosshairOverride,
|
|
629
1331
|
mode,
|
|
630
1332
|
tool,
|
|
1333
|
+
drawings,
|
|
631
1334
|
onCrosshair,
|
|
632
1335
|
onViewportChange,
|
|
633
1336
|
onDrawingComplete,
|
|
634
|
-
|
|
1337
|
+
onDrawingChange,
|
|
1338
|
+
onDrawingDelete,
|
|
1339
|
+
onModeChange,
|
|
1340
|
+
drawingStore,
|
|
1341
|
+
seriesKey
|
|
635
1342
|
} = props;
|
|
1343
|
+
const managed = useManagedDrawings(seriesKey, drawingStore);
|
|
1344
|
+
const stored = drawingStore != null;
|
|
1345
|
+
const effectiveDrawings = stored ? managed.drawings : drawings;
|
|
636
1346
|
const { containerRef, canvasRef, handleRef, scheduleRender } = useChartCore(
|
|
637
|
-
props,
|
|
1347
|
+
stored ? { ...props, drawings: effectiveDrawings } : props,
|
|
638
1348
|
wasm ? { wasm } : void 0
|
|
639
1349
|
);
|
|
640
1350
|
useGestures(containerRef, handleRef, scheduleRender, {
|
|
@@ -642,9 +1352,12 @@ function VroomChart(props) {
|
|
|
642
1352
|
crosshairOverride,
|
|
643
1353
|
mode,
|
|
644
1354
|
tool,
|
|
1355
|
+
drawings: effectiveDrawings,
|
|
645
1356
|
onCrosshair,
|
|
646
1357
|
onViewportChange,
|
|
647
|
-
onDrawingComplete,
|
|
1358
|
+
onDrawingComplete: stored ? managed.onDrawingComplete : onDrawingComplete,
|
|
1359
|
+
onDrawingChange: stored ? managed.onDrawingChange : onDrawingChange,
|
|
1360
|
+
onDrawingDelete: stored ? managed.onDrawingDelete : onDrawingDelete,
|
|
648
1361
|
onRequestMode: onModeChange
|
|
649
1362
|
});
|
|
650
1363
|
const rootStyle = {
|
|
@@ -656,9 +1369,12 @@ function VroomChart(props) {
|
|
|
656
1369
|
return /* @__PURE__ */ jsx("div", { ref: containerRef, className, style: rootStyle, children: /* @__PURE__ */ jsx("canvas", { ref: canvasRef, style: CANVAS_STYLE }) });
|
|
657
1370
|
}
|
|
658
1371
|
export {
|
|
1372
|
+
DRAWINGS_VERSION,
|
|
659
1373
|
VroomChart,
|
|
660
1374
|
classifyTransition,
|
|
1375
|
+
deserializeDrawings,
|
|
661
1376
|
inferStepMs,
|
|
1377
|
+
serializeDrawings,
|
|
662
1378
|
timeframeWindow
|
|
663
1379
|
};
|
|
664
1380
|
//# sourceMappingURL=index.js.map
|