liveline 0.0.4 → 0.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +60 -2
- package/dist/index.cjs +2267 -339
- package/dist/index.d.cts +52 -3
- package/dist/index.d.ts +52 -3
- package/dist/index.js +2266 -339
- package/package.json +16 -11
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/Liveline.tsx
|
|
2
|
-
import { useRef as useRef2, useState, useLayoutEffect, useMemo } from "react";
|
|
2
|
+
import { useRef as useRef2, useState, useLayoutEffect, useMemo, useCallback as useCallback2 } from "react";
|
|
3
3
|
|
|
4
4
|
// src/theme.ts
|
|
5
5
|
function parseColorRgb(color) {
|
|
@@ -61,6 +61,33 @@ function resolveTheme(color, mode) {
|
|
|
61
61
|
badgeFont: '500 11px "SF Mono", Menlo, monospace'
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
+
var SERIES_COLORS = [
|
|
65
|
+
"#3b82f6",
|
|
66
|
+
// blue
|
|
67
|
+
"#ef4444",
|
|
68
|
+
// red
|
|
69
|
+
"#22c55e",
|
|
70
|
+
// green
|
|
71
|
+
"#f59e0b",
|
|
72
|
+
// amber
|
|
73
|
+
"#8b5cf6",
|
|
74
|
+
// violet
|
|
75
|
+
"#ec4899",
|
|
76
|
+
// pink
|
|
77
|
+
"#06b6d4",
|
|
78
|
+
// cyan
|
|
79
|
+
"#f97316"
|
|
80
|
+
// orange
|
|
81
|
+
];
|
|
82
|
+
function resolveSeriesPalettes(series, mode) {
|
|
83
|
+
const map = /* @__PURE__ */ new Map();
|
|
84
|
+
for (let i = 0; i < series.length; i++) {
|
|
85
|
+
const s = series[i];
|
|
86
|
+
const color = s.color || SERIES_COLORS[i % SERIES_COLORS.length];
|
|
87
|
+
map.set(s.id, resolveTheme(color, mode));
|
|
88
|
+
}
|
|
89
|
+
return map;
|
|
90
|
+
}
|
|
64
91
|
|
|
65
92
|
// src/useLivelineEngine.ts
|
|
66
93
|
import { useRef, useEffect, useCallback } from "react";
|
|
@@ -361,8 +388,9 @@ function renderCurve(ctx, layout, palette, pts, showFill, lineAlpha = 1, fillAlp
|
|
|
361
388
|
ctx.stroke();
|
|
362
389
|
ctx.globalAlpha = baseAlpha;
|
|
363
390
|
}
|
|
364
|
-
function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0) {
|
|
391
|
+
function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0, colorBlend = 1, skipDashLine = false, fillScale = 1) {
|
|
365
392
|
const { h, pad, toX, toY, chartW, chartH } = layout;
|
|
393
|
+
const incomingAlpha = ctx.globalAlpha;
|
|
366
394
|
const yMin = pad.top;
|
|
367
395
|
const yMax = h - pad.bottom;
|
|
368
396
|
const clampY = (y) => Math.max(yMin, Math.min(yMax, y));
|
|
@@ -371,8 +399,10 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
|
|
|
371
399
|
const scroll = now_ms * LOADING_SCROLL_SPEED;
|
|
372
400
|
const morphY = chartReveal < 1 ? (rawY, x) => {
|
|
373
401
|
const t = Math.max(0, Math.min(1, (x - pad.left) / chartW));
|
|
402
|
+
const centerDist = Math.abs(t - 0.5) * 2;
|
|
403
|
+
const localReveal = Math.max(0, Math.min(1, (chartReveal - centerDist * 0.4) / 0.6));
|
|
374
404
|
const baseY = loadingY(t, centerY, amplitude, scroll);
|
|
375
|
-
return baseY + (rawY - baseY) *
|
|
405
|
+
return baseY + (rawY - baseY) * localReveal;
|
|
376
406
|
} : (rawY, _x) => rawY;
|
|
377
407
|
const pts = visible.map((p, i) => {
|
|
378
408
|
const x = toX(p.time);
|
|
@@ -385,13 +415,14 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
|
|
|
385
415
|
pts.push([tipX, morphY(clampY(toY(smoothValue)), tipX)]);
|
|
386
416
|
if (pts.length < 2) return;
|
|
387
417
|
let lineAlpha = 1;
|
|
388
|
-
let fillAlpha =
|
|
418
|
+
let fillAlpha = fillScale;
|
|
389
419
|
if (chartReveal < 1) {
|
|
390
420
|
const breath = loadingBreath(now_ms);
|
|
391
421
|
lineAlpha = breath + (1 - breath) * chartReveal;
|
|
392
|
-
fillAlpha = chartReveal;
|
|
422
|
+
fillAlpha = chartReveal * fillScale;
|
|
393
423
|
}
|
|
394
|
-
const
|
|
424
|
+
const colorT = Math.min(1, chartReveal * 3) * colorBlend;
|
|
425
|
+
const strokeColor = chartReveal < 1 || colorBlend < 1 ? blendColor(palette.gridLabel, palette.line, colorT) : void 0;
|
|
395
426
|
const isScrubbing = scrubX !== null;
|
|
396
427
|
ctx.save();
|
|
397
428
|
ctx.beginPath();
|
|
@@ -408,26 +439,28 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
|
|
|
408
439
|
ctx.beginPath();
|
|
409
440
|
ctx.rect(scrubX, 0, layout.w - scrubX, h);
|
|
410
441
|
ctx.clip();
|
|
411
|
-
ctx.globalAlpha = 1 - scrubAmount * 0.6;
|
|
442
|
+
ctx.globalAlpha = incomingAlpha * (1 - scrubAmount * 0.6);
|
|
412
443
|
renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
|
|
413
444
|
ctx.restore();
|
|
414
445
|
} else {
|
|
415
446
|
renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
|
|
416
447
|
}
|
|
417
448
|
ctx.restore();
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
449
|
+
if (!skipDashLine) {
|
|
450
|
+
const realCurrentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
|
|
451
|
+
const currentY = chartReveal < 1 ? centerY + (realCurrentY - centerY) * chartReveal : realCurrentY;
|
|
452
|
+
ctx.setLineDash([4, 4]);
|
|
453
|
+
ctx.strokeStyle = palette.dashLine;
|
|
454
|
+
ctx.lineWidth = 1;
|
|
455
|
+
const dashBase = isScrubbing ? 1 - scrubAmount * 0.2 : 1;
|
|
456
|
+
ctx.globalAlpha = incomingAlpha * (chartReveal < 1 ? dashBase * chartReveal : dashBase);
|
|
457
|
+
ctx.beginPath();
|
|
458
|
+
ctx.moveTo(pad.left, currentY);
|
|
459
|
+
ctx.lineTo(layout.w - pad.right, currentY);
|
|
460
|
+
ctx.stroke();
|
|
461
|
+
ctx.setLineDash([]);
|
|
462
|
+
}
|
|
463
|
+
ctx.globalAlpha = incomingAlpha;
|
|
431
464
|
const last = pts[pts.length - 1];
|
|
432
465
|
last[1] = Math.max(10, Math.min(h - 10, last[1]));
|
|
433
466
|
return pts;
|
|
@@ -480,6 +513,33 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = per
|
|
|
480
513
|
}
|
|
481
514
|
ctx.fill();
|
|
482
515
|
}
|
|
516
|
+
function drawMultiDot(ctx, x, y, color, pulse = true, now_ms = performance.now(), radius = 3) {
|
|
517
|
+
const baseAlpha = ctx.globalAlpha;
|
|
518
|
+
if (pulse) {
|
|
519
|
+
const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
|
|
520
|
+
if (t < 1) {
|
|
521
|
+
const ringRadius = 9 + t * 10;
|
|
522
|
+
const pulseAlpha = 0.3 * (1 - t);
|
|
523
|
+
ctx.beginPath();
|
|
524
|
+
ctx.arc(x, y, ringRadius, 0, Math.PI * 2);
|
|
525
|
+
ctx.strokeStyle = color;
|
|
526
|
+
ctx.lineWidth = 1.5;
|
|
527
|
+
ctx.globalAlpha = baseAlpha * pulseAlpha;
|
|
528
|
+
ctx.stroke();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
ctx.globalAlpha = baseAlpha;
|
|
532
|
+
ctx.beginPath();
|
|
533
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
534
|
+
ctx.fillStyle = color;
|
|
535
|
+
ctx.fill();
|
|
536
|
+
}
|
|
537
|
+
function drawSimpleDot(ctx, x, y, color, radius = 3) {
|
|
538
|
+
ctx.beginPath();
|
|
539
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
540
|
+
ctx.fillStyle = color;
|
|
541
|
+
ctx.fill();
|
|
542
|
+
}
|
|
483
543
|
function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
|
|
484
544
|
const baseAlpha = ctx.globalAlpha;
|
|
485
545
|
const upTarget = momentum === "up" ? 1 : 0;
|
|
@@ -578,6 +638,90 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
|
|
|
578
638
|
ctx.fillText(separator + timeText, tx + valueW, ty);
|
|
579
639
|
ctx.restore();
|
|
580
640
|
}
|
|
641
|
+
function drawMultiCrosshair(ctx, layout, palette, hoverX, hoverTime, entries, formatValue, formatTime, scrubOpacity, tooltipY, tooltipOutline, liveDotX) {
|
|
642
|
+
if (scrubOpacity < 0.01 || entries.length === 0) return;
|
|
643
|
+
const { h, pad, toY } = layout;
|
|
644
|
+
ctx.save();
|
|
645
|
+
ctx.globalAlpha = scrubOpacity * 0.5;
|
|
646
|
+
ctx.strokeStyle = palette.crosshairLine;
|
|
647
|
+
ctx.lineWidth = 1;
|
|
648
|
+
ctx.beginPath();
|
|
649
|
+
ctx.moveTo(hoverX, pad.top);
|
|
650
|
+
ctx.lineTo(hoverX, h - pad.bottom);
|
|
651
|
+
ctx.stroke();
|
|
652
|
+
ctx.restore();
|
|
653
|
+
const dotRadius = 4 * Math.min(scrubOpacity * 3, 1);
|
|
654
|
+
if (dotRadius > 0.5) {
|
|
655
|
+
ctx.globalAlpha = 1;
|
|
656
|
+
for (const entry of entries) {
|
|
657
|
+
const y = toY(entry.value);
|
|
658
|
+
ctx.beginPath();
|
|
659
|
+
ctx.arc(hoverX, y, dotRadius, 0, Math.PI * 2);
|
|
660
|
+
ctx.fillStyle = entry.color;
|
|
661
|
+
ctx.fill();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (scrubOpacity < 0.1 || layout.w < 300) return;
|
|
665
|
+
ctx.save();
|
|
666
|
+
ctx.globalAlpha = scrubOpacity;
|
|
667
|
+
ctx.font = '400 13px "SF Mono", Menlo, monospace';
|
|
668
|
+
ctx.textAlign = "left";
|
|
669
|
+
const timeText = formatTime(hoverTime);
|
|
670
|
+
const sep = " \xB7 ";
|
|
671
|
+
const dotInline = " ";
|
|
672
|
+
const segments = [
|
|
673
|
+
{ text: timeText, color: palette.gridLabel }
|
|
674
|
+
];
|
|
675
|
+
for (const e of entries) {
|
|
676
|
+
segments.push({ text: sep, color: palette.gridLabel });
|
|
677
|
+
segments.push({ text: dotInline, color: e.color, isDot: true });
|
|
678
|
+
const label = e.label ? `${e.label} ` : "";
|
|
679
|
+
if (label) segments.push({ text: label, color: palette.gridLabel });
|
|
680
|
+
segments.push({ text: formatValue(e.value), color: palette.tooltipText });
|
|
681
|
+
}
|
|
682
|
+
let totalW = 0;
|
|
683
|
+
const segWidths = [];
|
|
684
|
+
for (const seg of segments) {
|
|
685
|
+
const w = seg.isDot ? 12 : ctx.measureText(seg.text).width;
|
|
686
|
+
segWidths.push(w);
|
|
687
|
+
totalW += w;
|
|
688
|
+
}
|
|
689
|
+
let tx = hoverX - totalW / 2;
|
|
690
|
+
const minX = pad.left + 4;
|
|
691
|
+
const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
|
|
692
|
+
const maxX = dotRightEdge - totalW;
|
|
693
|
+
if (tx < minX) tx = minX;
|
|
694
|
+
if (tx > maxX) tx = maxX;
|
|
695
|
+
const ty = pad.top + (tooltipY ?? 14) + 10;
|
|
696
|
+
if (tooltipOutline !== false) {
|
|
697
|
+
ctx.strokeStyle = palette.tooltipBg;
|
|
698
|
+
ctx.lineWidth = 3;
|
|
699
|
+
ctx.lineJoin = "round";
|
|
700
|
+
let ox2 = tx;
|
|
701
|
+
for (let i = 0; i < segments.length; i++) {
|
|
702
|
+
const seg = segments[i];
|
|
703
|
+
if (!seg.isDot) {
|
|
704
|
+
ctx.strokeText(seg.text, ox2, ty);
|
|
705
|
+
}
|
|
706
|
+
ox2 += segWidths[i];
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
let ox = tx;
|
|
710
|
+
for (let i = 0; i < segments.length; i++) {
|
|
711
|
+
const seg = segments[i];
|
|
712
|
+
if (seg.isDot) {
|
|
713
|
+
ctx.beginPath();
|
|
714
|
+
ctx.arc(ox + 4, ty - 4, 3.5, 0, Math.PI * 2);
|
|
715
|
+
ctx.fillStyle = seg.color;
|
|
716
|
+
ctx.fill();
|
|
717
|
+
} else {
|
|
718
|
+
ctx.fillStyle = seg.color;
|
|
719
|
+
ctx.fillText(seg.text, ox, ty);
|
|
720
|
+
}
|
|
721
|
+
ox += segWidths[i];
|
|
722
|
+
}
|
|
723
|
+
ctx.restore();
|
|
724
|
+
}
|
|
581
725
|
|
|
582
726
|
// src/draw/referenceLine.ts
|
|
583
727
|
function drawReferenceLine(ctx, layout, palette, ref) {
|
|
@@ -944,6 +1088,337 @@ function drawParticles(ctx, state, dt) {
|
|
|
944
1088
|
ctx.restore();
|
|
945
1089
|
}
|
|
946
1090
|
|
|
1091
|
+
// src/draw/candlestick.ts
|
|
1092
|
+
var BULL = "#22c55e";
|
|
1093
|
+
var BEAR = "#ef4444";
|
|
1094
|
+
var BULL_RGB = [34, 197, 94];
|
|
1095
|
+
var BEAR_RGB = [239, 68, 68];
|
|
1096
|
+
function blendColor2(t) {
|
|
1097
|
+
const r = Math.round(BEAR_RGB[0] + (BULL_RGB[0] - BEAR_RGB[0]) * t);
|
|
1098
|
+
const g = Math.round(BEAR_RGB[1] + (BULL_RGB[1] - BEAR_RGB[1]) * t);
|
|
1099
|
+
const b = Math.round(BEAR_RGB[2] + (BULL_RGB[2] - BEAR_RGB[2]) * t);
|
|
1100
|
+
return `rgb(${r},${g},${b})`;
|
|
1101
|
+
}
|
|
1102
|
+
function parseRgb(color) {
|
|
1103
|
+
const hex = color.match(/^#([0-9a-f]{6})$/i);
|
|
1104
|
+
if (hex) {
|
|
1105
|
+
const h = hex[1];
|
|
1106
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
1107
|
+
}
|
|
1108
|
+
const rgb = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
1109
|
+
if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
|
|
1110
|
+
return [128, 128, 128];
|
|
1111
|
+
}
|
|
1112
|
+
function blendToAccent(candleColor, accentColor, t) {
|
|
1113
|
+
if (t <= 0) return candleColor;
|
|
1114
|
+
if (t >= 1) return accentColor;
|
|
1115
|
+
const [r1, g1, b1] = parseRgb(candleColor);
|
|
1116
|
+
const [r2, g2, b2] = parseRgb(accentColor);
|
|
1117
|
+
const r = Math.round(r1 + (r2 - r1) * t);
|
|
1118
|
+
const g = Math.round(g1 + (g2 - g1) * t);
|
|
1119
|
+
const b = Math.round(b1 + (b2 - b1) * t);
|
|
1120
|
+
return `rgb(${r},${g},${b})`;
|
|
1121
|
+
}
|
|
1122
|
+
function candleDims(layout, candleWidthSecs) {
|
|
1123
|
+
const pxPerSec = layout.chartW / (layout.rightEdge - layout.leftEdge);
|
|
1124
|
+
const candlePxW = candleWidthSecs * pxPerSec;
|
|
1125
|
+
const bodyW = Math.max(1, candlePxW * 0.7);
|
|
1126
|
+
const wickW = Math.max(0.8, Math.min(2, bodyW * 0.15));
|
|
1127
|
+
const radius = bodyW > 6 ? 1.5 : 0;
|
|
1128
|
+
return { bodyW, wickW, radius };
|
|
1129
|
+
}
|
|
1130
|
+
function roundedRect(ctx, x, y, w, h, r) {
|
|
1131
|
+
if (r <= 0 || h < r * 2) {
|
|
1132
|
+
ctx.rect(x, y, w, h);
|
|
1133
|
+
return;
|
|
1134
|
+
}
|
|
1135
|
+
ctx.moveTo(x + r, y);
|
|
1136
|
+
ctx.lineTo(x + w - r, y);
|
|
1137
|
+
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
1138
|
+
ctx.lineTo(x + w, y + h - r);
|
|
1139
|
+
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
1140
|
+
ctx.lineTo(x + r, y + h);
|
|
1141
|
+
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
1142
|
+
ctx.lineTo(x, y + r);
|
|
1143
|
+
ctx.arcTo(x, y, x + r, y, r);
|
|
1144
|
+
ctx.closePath();
|
|
1145
|
+
}
|
|
1146
|
+
function drawCandlesticks(ctx, layout, candles, candleWidthSecs, liveTime, now_ms, scrubX, scrubDim, liveAlpha = 1, liveBullBlend = -1, accentColor, accentBlend = 0) {
|
|
1147
|
+
if (candles.length === 0) return;
|
|
1148
|
+
const { toX, toY } = layout;
|
|
1149
|
+
const { bodyW, wickW, radius } = candleDims(layout, candleWidthSecs);
|
|
1150
|
+
const halfBody = bodyW / 2;
|
|
1151
|
+
const padL = layout.pad.left;
|
|
1152
|
+
const padR = layout.pad.left + layout.chartW;
|
|
1153
|
+
const livePulse = 0.12 + Math.sin(now_ms * 4e-3) * 0.08;
|
|
1154
|
+
for (const c of candles) {
|
|
1155
|
+
const cx = toX(c.time + candleWidthSecs / 2);
|
|
1156
|
+
if (cx + halfBody < padL || cx - halfBody > padR) continue;
|
|
1157
|
+
const isBull = c.close >= c.open;
|
|
1158
|
+
const isLive = c.time === liveTime;
|
|
1159
|
+
let color = isLive && liveBullBlend >= 0 ? blendColor2(liveBullBlend) : isBull ? BULL : BEAR;
|
|
1160
|
+
if (accentColor && accentBlend > 0.01) {
|
|
1161
|
+
color = blendToAccent(color, accentColor, accentBlend);
|
|
1162
|
+
}
|
|
1163
|
+
let candleAlpha = isLive ? liveAlpha : 1;
|
|
1164
|
+
if (scrubDim > 0.01 && scrubX > 0) {
|
|
1165
|
+
const dist = cx - scrubX;
|
|
1166
|
+
if (dist > 0) {
|
|
1167
|
+
const fadeZone = bodyW * 1.5;
|
|
1168
|
+
const dimT = Math.min(dist / fadeZone, 1);
|
|
1169
|
+
candleAlpha *= 1 - scrubDim * 0.5 * dimT;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
const baseAlpha = ctx.globalAlpha;
|
|
1173
|
+
ctx.globalAlpha = baseAlpha * candleAlpha;
|
|
1174
|
+
const bodyTop = toY(Math.max(c.open, c.close));
|
|
1175
|
+
const bodyBottom = toY(Math.min(c.open, c.close));
|
|
1176
|
+
const bodyH = Math.max(1, bodyBottom - bodyTop);
|
|
1177
|
+
const wickTop = toY(c.high);
|
|
1178
|
+
const wickBottom = toY(c.low);
|
|
1179
|
+
ctx.lineCap = "round";
|
|
1180
|
+
ctx.strokeStyle = color;
|
|
1181
|
+
if (bodyTop - wickTop > 0.5) {
|
|
1182
|
+
ctx.beginPath();
|
|
1183
|
+
ctx.moveTo(cx, bodyTop);
|
|
1184
|
+
ctx.lineTo(cx, wickTop);
|
|
1185
|
+
ctx.lineWidth = wickW;
|
|
1186
|
+
ctx.stroke();
|
|
1187
|
+
}
|
|
1188
|
+
if (wickBottom - bodyBottom > 0.5) {
|
|
1189
|
+
ctx.beginPath();
|
|
1190
|
+
ctx.moveTo(cx, bodyBottom);
|
|
1191
|
+
ctx.lineTo(cx, wickBottom);
|
|
1192
|
+
ctx.lineWidth = wickW;
|
|
1193
|
+
ctx.stroke();
|
|
1194
|
+
}
|
|
1195
|
+
ctx.fillStyle = color;
|
|
1196
|
+
ctx.beginPath();
|
|
1197
|
+
roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
|
|
1198
|
+
ctx.fill();
|
|
1199
|
+
if (isLive) {
|
|
1200
|
+
ctx.save();
|
|
1201
|
+
ctx.globalAlpha = baseAlpha * candleAlpha * livePulse;
|
|
1202
|
+
ctx.shadowColor = color;
|
|
1203
|
+
ctx.shadowBlur = 8;
|
|
1204
|
+
ctx.fillStyle = color;
|
|
1205
|
+
ctx.beginPath();
|
|
1206
|
+
roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
|
|
1207
|
+
ctx.fill();
|
|
1208
|
+
ctx.restore();
|
|
1209
|
+
}
|
|
1210
|
+
ctx.globalAlpha = baseAlpha;
|
|
1211
|
+
}
|
|
1212
|
+
}
|
|
1213
|
+
function drawClosePrice(ctx, layout, palette, liveCandle, scrubDim, bullBlend = -1) {
|
|
1214
|
+
const y = layout.toY(liveCandle.close);
|
|
1215
|
+
if (y < layout.pad.top || y > layout.h - layout.pad.bottom) return;
|
|
1216
|
+
const isBull = liveCandle.close >= liveCandle.open;
|
|
1217
|
+
const color = bullBlend >= 0 ? blendColor2(bullBlend) : isBull ? BULL : BEAR;
|
|
1218
|
+
const baseAlpha = ctx.globalAlpha;
|
|
1219
|
+
ctx.save();
|
|
1220
|
+
ctx.setLineDash([4, 4]);
|
|
1221
|
+
ctx.strokeStyle = color;
|
|
1222
|
+
ctx.lineWidth = 1;
|
|
1223
|
+
ctx.globalAlpha = baseAlpha * (1 - scrubDim * 0.3) * 0.4;
|
|
1224
|
+
ctx.beginPath();
|
|
1225
|
+
ctx.moveTo(layout.pad.left, y);
|
|
1226
|
+
ctx.lineTo(layout.w - layout.pad.right, y);
|
|
1227
|
+
ctx.stroke();
|
|
1228
|
+
ctx.setLineDash([]);
|
|
1229
|
+
ctx.restore();
|
|
1230
|
+
}
|
|
1231
|
+
function drawCandleCrosshair(ctx, layout, palette, hoverX, candle, hoverTime, formatValue, formatTime, opacity) {
|
|
1232
|
+
if (opacity < 0.01) return;
|
|
1233
|
+
const { h, pad } = layout;
|
|
1234
|
+
ctx.save();
|
|
1235
|
+
ctx.globalAlpha = opacity * 0.5;
|
|
1236
|
+
ctx.strokeStyle = palette.crosshairLine;
|
|
1237
|
+
ctx.lineWidth = 1;
|
|
1238
|
+
ctx.beginPath();
|
|
1239
|
+
ctx.moveTo(hoverX, pad.top);
|
|
1240
|
+
ctx.lineTo(hoverX, h - pad.bottom);
|
|
1241
|
+
ctx.stroke();
|
|
1242
|
+
ctx.restore();
|
|
1243
|
+
if (opacity < 0.1 || layout.w < 200) return;
|
|
1244
|
+
const isBull = candle.close >= candle.open;
|
|
1245
|
+
const valueColor = isBull ? BULL : BEAR;
|
|
1246
|
+
const cl = formatValue(candle.close);
|
|
1247
|
+
const time = formatTime(hoverTime);
|
|
1248
|
+
ctx.save();
|
|
1249
|
+
ctx.globalAlpha = opacity;
|
|
1250
|
+
ctx.font = '400 13px "SF Mono", Menlo, monospace';
|
|
1251
|
+
ctx.textAlign = "left";
|
|
1252
|
+
let parts;
|
|
1253
|
+
if (layout.w >= 400) {
|
|
1254
|
+
const o = formatValue(candle.open);
|
|
1255
|
+
const hi = formatValue(candle.high);
|
|
1256
|
+
const lo = formatValue(candle.low);
|
|
1257
|
+
parts = [
|
|
1258
|
+
{ text: "O ", color: palette.gridLabel },
|
|
1259
|
+
{ text: o, color: valueColor },
|
|
1260
|
+
{ text: " H ", color: palette.gridLabel },
|
|
1261
|
+
{ text: hi, color: valueColor },
|
|
1262
|
+
{ text: " L ", color: palette.gridLabel },
|
|
1263
|
+
{ text: lo, color: valueColor },
|
|
1264
|
+
{ text: " C ", color: palette.gridLabel },
|
|
1265
|
+
{ text: cl, color: valueColor },
|
|
1266
|
+
{ text: " \xB7 ", color: palette.gridLabel },
|
|
1267
|
+
{ text: time, color: palette.gridLabel }
|
|
1268
|
+
];
|
|
1269
|
+
} else {
|
|
1270
|
+
parts = [
|
|
1271
|
+
{ text: "C ", color: palette.gridLabel },
|
|
1272
|
+
{ text: cl, color: valueColor },
|
|
1273
|
+
{ text: " \xB7 ", color: palette.gridLabel },
|
|
1274
|
+
{ text: time, color: palette.gridLabel }
|
|
1275
|
+
];
|
|
1276
|
+
}
|
|
1277
|
+
let totalW = 0;
|
|
1278
|
+
const widths = [];
|
|
1279
|
+
for (const p of parts) {
|
|
1280
|
+
const w = ctx.measureText(p.text).width;
|
|
1281
|
+
widths.push(w);
|
|
1282
|
+
totalW += w;
|
|
1283
|
+
}
|
|
1284
|
+
let tx = hoverX - totalW / 2;
|
|
1285
|
+
const minX = pad.left + 4;
|
|
1286
|
+
const maxX = layout.w - pad.right - totalW;
|
|
1287
|
+
if (tx < minX) tx = minX;
|
|
1288
|
+
if (tx > maxX) tx = maxX;
|
|
1289
|
+
const ty = pad.top + 24;
|
|
1290
|
+
ctx.strokeStyle = palette.tooltipBg;
|
|
1291
|
+
ctx.lineWidth = 3;
|
|
1292
|
+
ctx.lineJoin = "round";
|
|
1293
|
+
let cx = tx;
|
|
1294
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1295
|
+
ctx.strokeText(parts[i].text, cx, ty);
|
|
1296
|
+
cx += widths[i];
|
|
1297
|
+
}
|
|
1298
|
+
cx = tx;
|
|
1299
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1300
|
+
ctx.fillStyle = parts[i].color;
|
|
1301
|
+
ctx.fillText(parts[i].text, cx, ty);
|
|
1302
|
+
cx += widths[i];
|
|
1303
|
+
}
|
|
1304
|
+
ctx.restore();
|
|
1305
|
+
}
|
|
1306
|
+
function drawLineModeCrosshair(ctx, layout, palette, hoverX, value, hoverTime, formatValue, formatTime, opacity) {
|
|
1307
|
+
if (opacity < 0.01) return;
|
|
1308
|
+
const { h, pad } = layout;
|
|
1309
|
+
const y = layout.toY(value);
|
|
1310
|
+
ctx.save();
|
|
1311
|
+
ctx.globalAlpha = opacity * 0.5;
|
|
1312
|
+
ctx.strokeStyle = palette.crosshairLine;
|
|
1313
|
+
ctx.lineWidth = 1;
|
|
1314
|
+
ctx.beginPath();
|
|
1315
|
+
ctx.moveTo(hoverX, pad.top);
|
|
1316
|
+
ctx.lineTo(hoverX, h - pad.bottom);
|
|
1317
|
+
ctx.stroke();
|
|
1318
|
+
ctx.globalAlpha = opacity * 0.3;
|
|
1319
|
+
ctx.beginPath();
|
|
1320
|
+
ctx.moveTo(pad.left, y);
|
|
1321
|
+
ctx.lineTo(layout.w - pad.right, y);
|
|
1322
|
+
ctx.stroke();
|
|
1323
|
+
ctx.restore();
|
|
1324
|
+
if (opacity < 0.1 || layout.w < 200) return;
|
|
1325
|
+
const val = formatValue(value);
|
|
1326
|
+
const time = formatTime(hoverTime);
|
|
1327
|
+
ctx.save();
|
|
1328
|
+
ctx.globalAlpha = opacity;
|
|
1329
|
+
ctx.font = '400 13px "SF Mono", Menlo, monospace';
|
|
1330
|
+
ctx.textAlign = "left";
|
|
1331
|
+
const parts = [
|
|
1332
|
+
{ text: val, color: palette.line },
|
|
1333
|
+
{ text: " \xB7 ", color: palette.gridLabel },
|
|
1334
|
+
{ text: time, color: palette.gridLabel }
|
|
1335
|
+
];
|
|
1336
|
+
let totalW = 0;
|
|
1337
|
+
const widths = [];
|
|
1338
|
+
for (const p of parts) {
|
|
1339
|
+
const w = ctx.measureText(p.text).width;
|
|
1340
|
+
widths.push(w);
|
|
1341
|
+
totalW += w;
|
|
1342
|
+
}
|
|
1343
|
+
let tx = hoverX - totalW / 2;
|
|
1344
|
+
const minX = pad.left + 4;
|
|
1345
|
+
const maxX = layout.w - pad.right - totalW;
|
|
1346
|
+
if (tx < minX) tx = minX;
|
|
1347
|
+
if (tx > maxX) tx = maxX;
|
|
1348
|
+
const ty = pad.top + 24;
|
|
1349
|
+
ctx.strokeStyle = palette.tooltipBg;
|
|
1350
|
+
ctx.lineWidth = 3;
|
|
1351
|
+
ctx.lineJoin = "round";
|
|
1352
|
+
let lx = tx;
|
|
1353
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1354
|
+
ctx.strokeText(parts[i].text, lx, ty);
|
|
1355
|
+
lx += widths[i];
|
|
1356
|
+
}
|
|
1357
|
+
lx = tx;
|
|
1358
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1359
|
+
ctx.fillStyle = parts[i].color;
|
|
1360
|
+
ctx.fillText(parts[i].text, lx, ty);
|
|
1361
|
+
lx += widths[i];
|
|
1362
|
+
}
|
|
1363
|
+
ctx.restore();
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
// src/draw/empty.ts
|
|
1367
|
+
function drawEmpty(ctx, w, h, pad, palette, alpha = 1, now_ms = 0, skipLine = false, emptyText) {
|
|
1368
|
+
const chartW = w - pad.left - pad.right;
|
|
1369
|
+
const chartH = h - pad.top - pad.bottom;
|
|
1370
|
+
const centerY = pad.top + chartH / 2;
|
|
1371
|
+
const cx = pad.left + chartW / 2;
|
|
1372
|
+
const text = emptyText ?? "No data to display";
|
|
1373
|
+
const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
|
|
1374
|
+
ctx.save();
|
|
1375
|
+
ctx.font = "400 12px system-ui, -apple-system, sans-serif";
|
|
1376
|
+
const textW = ctx.measureText(text).width;
|
|
1377
|
+
const gapHalf = textW / 2 + 20;
|
|
1378
|
+
const fadeW = 30;
|
|
1379
|
+
if (!skipLine) {
|
|
1380
|
+
const scroll = now_ms * LOADING_SCROLL_SPEED;
|
|
1381
|
+
const breath = loadingBreath(now_ms);
|
|
1382
|
+
const numPts = 32;
|
|
1383
|
+
const pts = [];
|
|
1384
|
+
for (let i = 0; i <= numPts; i++) {
|
|
1385
|
+
const t = i / numPts;
|
|
1386
|
+
const x = pad.left + t * chartW;
|
|
1387
|
+
const y = loadingY(t, centerY, amplitude, scroll);
|
|
1388
|
+
pts.push([x, y]);
|
|
1389
|
+
}
|
|
1390
|
+
ctx.beginPath();
|
|
1391
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
1392
|
+
drawSpline(ctx, pts);
|
|
1393
|
+
ctx.strokeStyle = palette.gridLabel;
|
|
1394
|
+
ctx.lineWidth = palette.lineWidth;
|
|
1395
|
+
ctx.globalAlpha = breath * alpha;
|
|
1396
|
+
ctx.lineCap = "round";
|
|
1397
|
+
ctx.lineJoin = "round";
|
|
1398
|
+
ctx.stroke();
|
|
1399
|
+
}
|
|
1400
|
+
ctx.save();
|
|
1401
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
1402
|
+
const gapLeft = cx - gapHalf - fadeW;
|
|
1403
|
+
const gapRight = cx + gapHalf + fadeW;
|
|
1404
|
+
const eraseGrad = ctx.createLinearGradient(gapLeft, 0, gapRight, 0);
|
|
1405
|
+
eraseGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
1406
|
+
eraseGrad.addColorStop(fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
|
|
1407
|
+
eraseGrad.addColorStop(1 - fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
|
|
1408
|
+
eraseGrad.addColorStop(1, "rgba(0,0,0,0)");
|
|
1409
|
+
ctx.fillStyle = eraseGrad;
|
|
1410
|
+
ctx.globalAlpha = alpha;
|
|
1411
|
+
const eraseH = amplitude * 2 + palette.lineWidth + 6;
|
|
1412
|
+
ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
|
|
1413
|
+
ctx.restore();
|
|
1414
|
+
ctx.textAlign = "center";
|
|
1415
|
+
ctx.textBaseline = "middle";
|
|
1416
|
+
ctx.globalAlpha = 0.35 * alpha;
|
|
1417
|
+
ctx.fillStyle = palette.gridLabel;
|
|
1418
|
+
ctx.fillText(text, cx, centerY);
|
|
1419
|
+
ctx.restore();
|
|
1420
|
+
}
|
|
1421
|
+
|
|
947
1422
|
// src/draw/index.ts
|
|
948
1423
|
var SHAKE_DECAY_RATE = 2e-3;
|
|
949
1424
|
var SHAKE_MIN_AMPLITUDE = 0.2;
|
|
@@ -1093,91 +1568,352 @@ function drawFrame(ctx, layout, palette, opts) {
|
|
|
1093
1568
|
ctx.restore();
|
|
1094
1569
|
}
|
|
1095
1570
|
}
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
const
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
for (let i = 0; i <= numPts; i++) {
|
|
1109
|
-
const t = i / numPts;
|
|
1110
|
-
const x = leftX + t * chartW;
|
|
1111
|
-
const y = loadingY(t, centerY, amplitude, scroll);
|
|
1112
|
-
pts.push([x, y]);
|
|
1571
|
+
function drawMultiFrame(ctx, layout, opts) {
|
|
1572
|
+
const palette = opts.primaryPalette;
|
|
1573
|
+
const reveal = opts.chartReveal;
|
|
1574
|
+
const revealRamp = (start, end) => {
|
|
1575
|
+
const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
|
|
1576
|
+
return t * t * (3 - 2 * t);
|
|
1577
|
+
};
|
|
1578
|
+
if (opts.referenceLine && reveal > 0.01) {
|
|
1579
|
+
ctx.save();
|
|
1580
|
+
if (reveal < 1) ctx.globalAlpha = reveal;
|
|
1581
|
+
drawReferenceLine(ctx, layout, palette, opts.referenceLine);
|
|
1582
|
+
ctx.restore();
|
|
1113
1583
|
}
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1584
|
+
if (opts.showGrid) {
|
|
1585
|
+
const gridAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
|
|
1586
|
+
if (gridAlpha > 0.01) {
|
|
1587
|
+
ctx.save();
|
|
1588
|
+
if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
|
|
1589
|
+
drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
|
|
1590
|
+
ctx.restore();
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
|
|
1594
|
+
const allPts = [];
|
|
1595
|
+
for (let si = 0; si < opts.series.length; si++) {
|
|
1596
|
+
const s = opts.series[si];
|
|
1597
|
+
const seriesAlpha = s.alpha ?? 1;
|
|
1598
|
+
const secondaryFade = si > 0 && reveal < 1 ? Math.min(1, reveal * 2) : 1;
|
|
1599
|
+
const combinedAlpha = secondaryFade * seriesAlpha;
|
|
1600
|
+
if (combinedAlpha < 0.01) continue;
|
|
1601
|
+
ctx.save();
|
|
1602
|
+
if (combinedAlpha < 1) ctx.globalAlpha = combinedAlpha;
|
|
1603
|
+
const pts = drawLine(
|
|
1604
|
+
ctx,
|
|
1605
|
+
layout,
|
|
1606
|
+
s.palette,
|
|
1607
|
+
s.visible,
|
|
1608
|
+
s.smoothValue,
|
|
1609
|
+
opts.now,
|
|
1610
|
+
false,
|
|
1611
|
+
// no fill
|
|
1612
|
+
scrubX,
|
|
1613
|
+
opts.scrubAmount,
|
|
1614
|
+
reveal,
|
|
1615
|
+
opts.now_ms
|
|
1616
|
+
);
|
|
1617
|
+
ctx.restore();
|
|
1618
|
+
if (pts && pts.length > 0) {
|
|
1619
|
+
allPts.push({ pts, palette: s.palette, label: s.label, alpha: seriesAlpha });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
{
|
|
1623
|
+
const timeAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
|
|
1624
|
+
if (timeAlpha > 0.01) {
|
|
1625
|
+
ctx.save();
|
|
1626
|
+
if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
|
|
1627
|
+
drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
|
|
1628
|
+
ctx.restore();
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (reveal > 0.3 && allPts.length > 0) {
|
|
1632
|
+
const dotAlpha = (reveal - 0.3) / 0.7;
|
|
1633
|
+
const showPulse = opts.showPulse && reveal > 0.6 && opts.pauseProgress < 0.5;
|
|
1634
|
+
for (const entry of allPts) {
|
|
1635
|
+
if (entry.alpha < 0.01) continue;
|
|
1636
|
+
const lastPt = entry.pts[entry.pts.length - 1];
|
|
1637
|
+
ctx.save();
|
|
1638
|
+
ctx.globalAlpha = dotAlpha * entry.alpha;
|
|
1639
|
+
if (showPulse && entry.alpha > 0.5) {
|
|
1640
|
+
drawMultiDot(ctx, lastPt[0], lastPt[1], entry.palette.line, true, opts.now_ms, 3);
|
|
1641
|
+
} else {
|
|
1642
|
+
drawSimpleDot(ctx, lastPt[0], lastPt[1], entry.palette.line, 3);
|
|
1643
|
+
}
|
|
1644
|
+
if (entry.label) {
|
|
1645
|
+
ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
|
|
1646
|
+
ctx.textAlign = "left";
|
|
1647
|
+
ctx.fillStyle = entry.palette.line;
|
|
1648
|
+
ctx.fillText(entry.label, lastPt[0] + 6, lastPt[1] + 3.5);
|
|
1649
|
+
}
|
|
1650
|
+
ctx.restore();
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
ctx.save();
|
|
1654
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
1655
|
+
const fadeGrad = ctx.createLinearGradient(layout.pad.left, 0, layout.pad.left + FADE_EDGE_WIDTH, 0);
|
|
1656
|
+
fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
|
|
1657
|
+
fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
1658
|
+
ctx.fillStyle = fadeGrad;
|
|
1659
|
+
ctx.fillRect(0, 0, layout.pad.left + FADE_EDGE_WIDTH, layout.h);
|
|
1660
|
+
ctx.restore();
|
|
1661
|
+
if (opts.hoverX !== null && opts.hoverTime !== null && opts.hoverEntries.length > 0 && allPts.length > 0 && opts.scrubAmount > 0.01) {
|
|
1662
|
+
let maxLiveDotX = 0;
|
|
1663
|
+
for (const entry of allPts) {
|
|
1664
|
+
if (entry.alpha < 0.01) continue;
|
|
1665
|
+
const lastX = entry.pts[entry.pts.length - 1][0];
|
|
1666
|
+
if (lastX > maxLiveDotX) maxLiveDotX = lastX;
|
|
1667
|
+
}
|
|
1668
|
+
const distToLive = maxLiveDotX - opts.hoverX;
|
|
1669
|
+
const fadeStart = Math.min(80, layout.chartW * 0.3);
|
|
1670
|
+
const scrubOpacity = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
|
|
1671
|
+
if (scrubOpacity > 0.01) {
|
|
1672
|
+
drawMultiCrosshair(
|
|
1673
|
+
ctx,
|
|
1674
|
+
layout,
|
|
1675
|
+
palette,
|
|
1676
|
+
opts.hoverX,
|
|
1677
|
+
opts.hoverTime,
|
|
1678
|
+
opts.hoverEntries,
|
|
1679
|
+
opts.formatValue,
|
|
1680
|
+
opts.formatTime,
|
|
1681
|
+
scrubOpacity,
|
|
1682
|
+
opts.tooltipY,
|
|
1683
|
+
opts.tooltipOutline,
|
|
1684
|
+
maxLiveDotX
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1689
|
+
function drawCandleFrame(ctx, layout, palette, opts) {
|
|
1690
|
+
const { w, h, pad, chartW, chartH } = layout;
|
|
1691
|
+
const reveal = opts.chartReveal;
|
|
1692
|
+
const fullLineMode = opts.lineModeProg >= 0.99;
|
|
1693
|
+
const revealLine = fullLineMode ? 1 - reveal : (1 - reveal) * (1 - reveal) * (1 - reveal);
|
|
1694
|
+
const lp = Math.max(opts.lineModeProg, revealLine);
|
|
1695
|
+
const colorBlend = lp > 1e-3 ? opts.lineModeProg / lp : 1;
|
|
1696
|
+
const revealRamp = (start, end) => {
|
|
1697
|
+
const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
|
|
1698
|
+
return t * t * (3 - 2 * t);
|
|
1699
|
+
};
|
|
1700
|
+
const gridAlpha = revealRamp(0.25, 0.6);
|
|
1701
|
+
if (opts.showGrid && gridAlpha > 0.01) {
|
|
1702
|
+
ctx.save();
|
|
1703
|
+
if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
|
|
1704
|
+
drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
|
|
1705
|
+
ctx.restore();
|
|
1706
|
+
}
|
|
1707
|
+
let linePts;
|
|
1708
|
+
if (lp > 0.01 && opts.lineVisible.length >= 2) {
|
|
1709
|
+
const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
|
|
1710
|
+
ctx.save();
|
|
1711
|
+
ctx.globalAlpha = lp;
|
|
1712
|
+
linePts = drawLine(
|
|
1713
|
+
ctx,
|
|
1714
|
+
layout,
|
|
1715
|
+
palette,
|
|
1716
|
+
opts.lineVisible,
|
|
1717
|
+
opts.lineSmoothValue,
|
|
1718
|
+
opts.now,
|
|
1719
|
+
opts.lineModeProg > 0.01,
|
|
1720
|
+
scrubX,
|
|
1721
|
+
opts.scrubAmount,
|
|
1722
|
+
opts.chartReveal,
|
|
1723
|
+
opts.now_ms,
|
|
1724
|
+
colorBlend,
|
|
1725
|
+
!fullLineMode,
|
|
1726
|
+
opts.lineModeProg
|
|
1727
|
+
// fillScale — fill fades smoothly with line mode transition
|
|
1728
|
+
);
|
|
1729
|
+
ctx.restore();
|
|
1730
|
+
}
|
|
1731
|
+
const closeAlpha = revealRamp(0.4, 0.8);
|
|
1732
|
+
const closeSource = opts.closePriceCandle ?? opts.liveCandle;
|
|
1733
|
+
if (closeSource && closeAlpha > 0.01) {
|
|
1734
|
+
if (lp < 0.99) {
|
|
1735
|
+
ctx.save();
|
|
1736
|
+
ctx.globalAlpha = closeAlpha * (1 - lp);
|
|
1737
|
+
drawClosePrice(ctx, layout, palette, closeSource, opts.scrubAmount, opts.liveBullBlend);
|
|
1738
|
+
ctx.restore();
|
|
1739
|
+
}
|
|
1740
|
+
if (lp > 0.01 && !fullLineMode) {
|
|
1741
|
+
const dashY = layout.toY(closeSource.close);
|
|
1742
|
+
if (dashY >= pad.top && dashY <= h - pad.bottom) {
|
|
1743
|
+
ctx.save();
|
|
1744
|
+
ctx.setLineDash([4, 4]);
|
|
1745
|
+
ctx.strokeStyle = palette.dashLine;
|
|
1746
|
+
ctx.lineWidth = 1;
|
|
1747
|
+
ctx.globalAlpha = closeAlpha * lp * (1 - opts.scrubAmount * 0.2);
|
|
1748
|
+
ctx.beginPath();
|
|
1749
|
+
ctx.moveTo(pad.left, dashY);
|
|
1750
|
+
ctx.lineTo(w - pad.right, dashY);
|
|
1751
|
+
ctx.stroke();
|
|
1752
|
+
ctx.setLineDash([]);
|
|
1753
|
+
ctx.restore();
|
|
1754
|
+
}
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
const candleAlpha = opts.chartReveal * (1 - lp);
|
|
1758
|
+
if (candleAlpha > 0.01) {
|
|
1759
|
+
const ohlcScale = reveal * reveal * (3 - 2 * reveal);
|
|
1760
|
+
const collapseC = (c) => ohlcScale >= 0.99 ? c : {
|
|
1761
|
+
time: c.time,
|
|
1762
|
+
open: c.close + (c.open - c.close) * ohlcScale,
|
|
1763
|
+
high: c.close + (c.high - c.close) * ohlcScale,
|
|
1764
|
+
low: c.close + (c.low - c.close) * ohlcScale,
|
|
1765
|
+
close: c.close
|
|
1766
|
+
};
|
|
1767
|
+
const revealCandles = ohlcScale < 0.99 ? opts.candles.map(collapseC) : opts.candles;
|
|
1768
|
+
const revealOld = ohlcScale < 0.99 && opts.oldCandles.length > 0 ? opts.oldCandles.map(collapseC) : opts.oldCandles;
|
|
1769
|
+
ctx.save();
|
|
1770
|
+
ctx.beginPath();
|
|
1771
|
+
ctx.rect(pad.left - 1, pad.top, chartW + 2, chartH);
|
|
1772
|
+
ctx.clip();
|
|
1773
|
+
const accentCol = lp > 0.01 ? palette.line : void 0;
|
|
1774
|
+
if (opts.morphT >= 0 && revealOld.length > 0) {
|
|
1775
|
+
ctx.globalAlpha = (1 - opts.morphT) * candleAlpha;
|
|
1776
|
+
drawCandlesticks(
|
|
1777
|
+
ctx,
|
|
1778
|
+
layout,
|
|
1779
|
+
revealOld,
|
|
1780
|
+
opts.oldWidth,
|
|
1781
|
+
-1,
|
|
1782
|
+
opts.now_ms,
|
|
1783
|
+
opts.hoverX ?? 0,
|
|
1784
|
+
opts.scrubAmount,
|
|
1785
|
+
1,
|
|
1786
|
+
-1,
|
|
1787
|
+
accentCol,
|
|
1788
|
+
lp
|
|
1789
|
+
);
|
|
1790
|
+
ctx.globalAlpha = opts.morphT * candleAlpha;
|
|
1791
|
+
drawCandlesticks(
|
|
1792
|
+
ctx,
|
|
1793
|
+
layout,
|
|
1794
|
+
revealCandles,
|
|
1795
|
+
opts.displayCandleWidth,
|
|
1796
|
+
opts.liveCandle?.time ?? -1,
|
|
1797
|
+
opts.now_ms,
|
|
1798
|
+
opts.hoverX ?? 0,
|
|
1799
|
+
opts.scrubAmount,
|
|
1800
|
+
opts.liveBirthAlpha,
|
|
1801
|
+
opts.liveBullBlend,
|
|
1802
|
+
accentCol,
|
|
1803
|
+
lp
|
|
1804
|
+
);
|
|
1805
|
+
ctx.globalAlpha = 1;
|
|
1806
|
+
} else {
|
|
1807
|
+
if (candleAlpha < 1) ctx.globalAlpha = candleAlpha;
|
|
1808
|
+
drawCandlesticks(
|
|
1809
|
+
ctx,
|
|
1810
|
+
layout,
|
|
1811
|
+
revealCandles,
|
|
1812
|
+
opts.displayCandleWidth,
|
|
1813
|
+
opts.liveCandle?.time ?? -1,
|
|
1814
|
+
opts.now_ms,
|
|
1815
|
+
opts.hoverX ?? 0,
|
|
1816
|
+
opts.scrubAmount,
|
|
1817
|
+
opts.liveBirthAlpha,
|
|
1818
|
+
opts.liveBullBlend,
|
|
1819
|
+
accentCol,
|
|
1820
|
+
lp
|
|
1821
|
+
);
|
|
1822
|
+
}
|
|
1823
|
+
ctx.restore();
|
|
1824
|
+
}
|
|
1825
|
+
if (lp > 0.5 && linePts && linePts.length > 0 && reveal > 0.3) {
|
|
1826
|
+
const lastPt = linePts[linePts.length - 1];
|
|
1827
|
+
const dotAlpha = (lp - 0.5) * 2 * ((reveal - 0.3) / 0.7);
|
|
1828
|
+
const showPulse = lp > 0.8 && reveal > 0.6;
|
|
1829
|
+
if (dotAlpha > 0.01) {
|
|
1830
|
+
ctx.save();
|
|
1831
|
+
ctx.globalAlpha = dotAlpha;
|
|
1832
|
+
drawDot(ctx, lastPt[0], lastPt[1], palette, showPulse, opts.scrubAmount, opts.now_ms);
|
|
1833
|
+
ctx.restore();
|
|
1834
|
+
}
|
|
1835
|
+
}
|
|
1836
|
+
const timeAlpha = revealRamp(0.25, 0.6);
|
|
1837
|
+
if (timeAlpha > 0.01) {
|
|
1838
|
+
ctx.save();
|
|
1839
|
+
if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
|
|
1840
|
+
drawTimeAxis(ctx, layout, palette, opts.targetWindowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
|
|
1841
|
+
ctx.restore();
|
|
1842
|
+
}
|
|
1843
|
+
ctx.save();
|
|
1844
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
1845
|
+
const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
|
|
1846
|
+
fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
|
|
1847
|
+
fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
1848
|
+
ctx.fillStyle = fadeGrad;
|
|
1849
|
+
ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
|
|
1850
|
+
ctx.restore();
|
|
1851
|
+
if (opts.showEmptyOverlay) {
|
|
1852
|
+
const bgAlpha = 1 - opts.chartReveal;
|
|
1853
|
+
if (bgAlpha > 0.01) {
|
|
1854
|
+
const bgEmptyAlpha = (1 - opts.loadingAlpha) * bgAlpha;
|
|
1855
|
+
if (bgEmptyAlpha > 0.01) {
|
|
1856
|
+
drawEmpty(ctx, w, h, pad, palette, bgEmptyAlpha, opts.now_ms, true, opts.emptyText);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
}
|
|
1860
|
+
if (opts.chartReveal > 0.7 && opts.hoveredCandle && opts.hoverX !== null && opts.scrubAmount > 0.01) {
|
|
1861
|
+
if (opts.lineModeProg > 0.5) {
|
|
1862
|
+
drawLineModeCrosshair(
|
|
1863
|
+
ctx,
|
|
1864
|
+
layout,
|
|
1865
|
+
palette,
|
|
1866
|
+
opts.hoverX,
|
|
1867
|
+
opts.hoveredCandle.close,
|
|
1868
|
+
opts.hoverTime ?? 0,
|
|
1869
|
+
opts.formatValue,
|
|
1870
|
+
opts.formatTime,
|
|
1871
|
+
opts.scrubAmount
|
|
1872
|
+
);
|
|
1873
|
+
} else {
|
|
1874
|
+
drawCandleCrosshair(
|
|
1875
|
+
ctx,
|
|
1876
|
+
layout,
|
|
1877
|
+
palette,
|
|
1878
|
+
opts.hoverX,
|
|
1879
|
+
opts.hoveredCandle,
|
|
1880
|
+
opts.hoverTime ?? 0,
|
|
1881
|
+
opts.formatValue,
|
|
1882
|
+
opts.formatTime,
|
|
1883
|
+
opts.scrubAmount
|
|
1884
|
+
);
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
|
|
1889
|
+
// src/draw/loading.ts
|
|
1890
|
+
function drawLoading(ctx, w, h, pad, palette, now_ms, alpha = 1, strokeColor) {
|
|
1128
1891
|
const chartW = w - pad.left - pad.right;
|
|
1129
1892
|
const chartH = h - pad.top - pad.bottom;
|
|
1130
1893
|
const centerY = pad.top + chartH / 2;
|
|
1131
|
-
const
|
|
1132
|
-
const text = emptyText ?? "No data to display";
|
|
1133
|
-
ctx.font = "400 12px system-ui, -apple-system, sans-serif";
|
|
1894
|
+
const leftX = pad.left;
|
|
1134
1895
|
const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
|
|
1135
|
-
const
|
|
1136
|
-
const
|
|
1137
|
-
const
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
const
|
|
1141
|
-
const
|
|
1142
|
-
const
|
|
1143
|
-
|
|
1144
|
-
const t = i / numPts;
|
|
1145
|
-
const x = pad.left + t * chartW;
|
|
1146
|
-
const y = loadingY(t, centerY, amplitude, scroll);
|
|
1147
|
-
pts.push([x, y]);
|
|
1148
|
-
}
|
|
1149
|
-
ctx.beginPath();
|
|
1150
|
-
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
1151
|
-
drawSpline(ctx, pts);
|
|
1152
|
-
ctx.strokeStyle = palette.gridLabel;
|
|
1153
|
-
ctx.lineWidth = palette.lineWidth;
|
|
1154
|
-
ctx.globalAlpha = breath * alpha;
|
|
1155
|
-
ctx.lineCap = "round";
|
|
1156
|
-
ctx.lineJoin = "round";
|
|
1157
|
-
ctx.stroke();
|
|
1896
|
+
const scroll = now_ms * LOADING_SCROLL_SPEED;
|
|
1897
|
+
const breath = loadingBreath(now_ms);
|
|
1898
|
+
const numPts = 32;
|
|
1899
|
+
const pts = [];
|
|
1900
|
+
for (let i = 0; i <= numPts; i++) {
|
|
1901
|
+
const t = i / numPts;
|
|
1902
|
+
const x = leftX + t * chartW;
|
|
1903
|
+
const y = loadingY(t, centerY, amplitude, scroll);
|
|
1904
|
+
pts.push([x, y]);
|
|
1158
1905
|
}
|
|
1159
1906
|
ctx.save();
|
|
1160
|
-
ctx.
|
|
1161
|
-
|
|
1162
|
-
|
|
1163
|
-
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1167
|
-
|
|
1168
|
-
ctx.
|
|
1169
|
-
ctx.globalAlpha = alpha;
|
|
1170
|
-
const eraseH = amplitude * 2 + palette.lineWidth + 6;
|
|
1171
|
-
ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
|
|
1907
|
+
ctx.beginPath();
|
|
1908
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
1909
|
+
drawSpline(ctx, pts);
|
|
1910
|
+
ctx.strokeStyle = strokeColor ?? palette.line;
|
|
1911
|
+
ctx.lineWidth = palette.lineWidth;
|
|
1912
|
+
ctx.globalAlpha = breath * alpha;
|
|
1913
|
+
ctx.lineCap = "round";
|
|
1914
|
+
ctx.lineJoin = "round";
|
|
1915
|
+
ctx.stroke();
|
|
1172
1916
|
ctx.restore();
|
|
1173
|
-
ctx.textAlign = "center";
|
|
1174
|
-
ctx.textBaseline = "middle";
|
|
1175
|
-
ctx.globalAlpha = 0.35 * alpha;
|
|
1176
|
-
ctx.fillStyle = palette.gridLabel;
|
|
1177
|
-
ctx.fillText(text, cx, centerY);
|
|
1178
|
-
ctx.globalAlpha = 1;
|
|
1179
|
-
ctx.textAlign = "start";
|
|
1180
|
-
ctx.textBaseline = "alphabetic";
|
|
1181
1917
|
}
|
|
1182
1918
|
|
|
1183
1919
|
// src/draw/badge.ts
|
|
@@ -1227,10 +1963,23 @@ var ADAPTIVE_SPEED_BOOST = 0.2;
|
|
|
1227
1963
|
var MOMENTUM_GREEN = [34, 197, 94];
|
|
1228
1964
|
var MOMENTUM_RED = [239, 68, 68];
|
|
1229
1965
|
var CHART_REVEAL_SPEED = 0.14;
|
|
1966
|
+
var CHART_REVEAL_SPEED_FWD = 0.09;
|
|
1230
1967
|
var PAUSE_PROGRESS_SPEED = 0.12;
|
|
1231
1968
|
var PAUSE_CATCHUP_SPEED = 0.08;
|
|
1232
1969
|
var PAUSE_CATCHUP_SPEED_FAST = 0.22;
|
|
1233
1970
|
var LOADING_ALPHA_SPEED = 0.14;
|
|
1971
|
+
var SERIES_TOGGLE_SPEED = 0.1;
|
|
1972
|
+
var CANDLE_LERP_SPEED = 0.25;
|
|
1973
|
+
var CANDLE_WIDTH_TRANS_MS = 300;
|
|
1974
|
+
var LINE_MORPH_MS = 500;
|
|
1975
|
+
var CLOSE_LINE_LERP_SPEED = 0.25;
|
|
1976
|
+
var LINE_DENSITY_MS = 350;
|
|
1977
|
+
var LINE_LERP_BASE = 0.08;
|
|
1978
|
+
var LINE_ADAPTIVE_BOOST = 0.2;
|
|
1979
|
+
var LINE_SNAP_THRESHOLD = 1e-3;
|
|
1980
|
+
var RANGE_LERP_SPEED = 0.15;
|
|
1981
|
+
var RANGE_ADAPTIVE_BOOST = 0.2;
|
|
1982
|
+
var CANDLE_BUFFER = 0.05;
|
|
1234
1983
|
function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
|
|
1235
1984
|
const valGap = Math.abs(value - displayValue);
|
|
1236
1985
|
const prevRange = displayMax - displayMin || 1;
|
|
@@ -1430,10 +2179,121 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
|
|
|
1430
2179
|
}
|
|
1431
2180
|
return badgeY;
|
|
1432
2181
|
}
|
|
2182
|
+
function computeCandleRange(candles) {
|
|
2183
|
+
let min = Infinity;
|
|
2184
|
+
let max = -Infinity;
|
|
2185
|
+
for (const c of candles) {
|
|
2186
|
+
if (c.low < min) min = c.low;
|
|
2187
|
+
if (c.high > max) max = c.high;
|
|
2188
|
+
}
|
|
2189
|
+
if (!isFinite(min) || !isFinite(max)) return { min: 99, max: 101 };
|
|
2190
|
+
const range = max - min;
|
|
2191
|
+
const margin = range * 0.12;
|
|
2192
|
+
const minRange = range * 0.1 || 0.4;
|
|
2193
|
+
if (range < minRange) {
|
|
2194
|
+
const mid = (min + max) / 2;
|
|
2195
|
+
return { min: mid - minRange / 2, max: mid + minRange / 2 };
|
|
2196
|
+
}
|
|
2197
|
+
return { min: min - margin, max: max + margin };
|
|
2198
|
+
}
|
|
2199
|
+
function candleAtX(candles, hoverX, candleWidth, layout) {
|
|
2200
|
+
const time = layout.leftEdge + (hoverX - layout.pad.left) / layout.chartW * (layout.rightEdge - layout.leftEdge);
|
|
2201
|
+
let lo = 0;
|
|
2202
|
+
let hi = candles.length - 1;
|
|
2203
|
+
while (lo <= hi) {
|
|
2204
|
+
const mid = lo + hi >> 1;
|
|
2205
|
+
const c = candles[mid];
|
|
2206
|
+
if (time < c.time) hi = mid - 1;
|
|
2207
|
+
else if (time >= c.time + candleWidth) lo = mid + 1;
|
|
2208
|
+
else return c;
|
|
2209
|
+
}
|
|
2210
|
+
return null;
|
|
2211
|
+
}
|
|
2212
|
+
function updateCandleRange(computedRange, rangeInited, displayMin, displayMax, isTransitioning, windowTransProgress, wt, chartH, dt) {
|
|
2213
|
+
if (!rangeInited) {
|
|
2214
|
+
return {
|
|
2215
|
+
minVal: computedRange.min,
|
|
2216
|
+
maxVal: computedRange.max,
|
|
2217
|
+
valRange: computedRange.max - computedRange.min || 1e-3,
|
|
2218
|
+
displayMin: computedRange.min,
|
|
2219
|
+
displayMax: computedRange.max,
|
|
2220
|
+
rangeInited: true
|
|
2221
|
+
};
|
|
2222
|
+
}
|
|
2223
|
+
if (isTransitioning) {
|
|
2224
|
+
displayMin = wt.rangeFromMin + (wt.rangeToMin - wt.rangeFromMin) * windowTransProgress;
|
|
2225
|
+
displayMax = wt.rangeFromMax + (wt.rangeToMax - wt.rangeFromMax) * windowTransProgress;
|
|
2226
|
+
} else {
|
|
2227
|
+
const curRange = displayMax - displayMin || 1;
|
|
2228
|
+
const gapMin = Math.abs(displayMin - computedRange.min);
|
|
2229
|
+
const gapMax = Math.abs(displayMax - computedRange.max);
|
|
2230
|
+
const gapRatio = Math.min((gapMin + gapMax) / curRange, 1);
|
|
2231
|
+
const speed = RANGE_LERP_SPEED + (1 - gapRatio) * RANGE_ADAPTIVE_BOOST;
|
|
2232
|
+
displayMin = lerp(displayMin, computedRange.min, speed, dt);
|
|
2233
|
+
displayMax = lerp(displayMax, computedRange.max, speed, dt);
|
|
2234
|
+
const pxThreshold = 0.5 * curRange / chartH || 1e-3;
|
|
2235
|
+
if (Math.abs(displayMin - computedRange.min) < pxThreshold) displayMin = computedRange.min;
|
|
2236
|
+
if (Math.abs(displayMax - computedRange.max) < pxThreshold) displayMax = computedRange.max;
|
|
2237
|
+
}
|
|
2238
|
+
return {
|
|
2239
|
+
minVal: displayMin,
|
|
2240
|
+
maxVal: displayMax,
|
|
2241
|
+
valRange: displayMax - displayMin || 1e-3,
|
|
2242
|
+
displayMin,
|
|
2243
|
+
displayMax,
|
|
2244
|
+
rangeInited: true
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
function updateCandleWindowTransition(targetWindowSecs, wt, displayWindow, displayMin, displayMax, now_ms, now, candles, liveCandle, candleWidth, buffer) {
|
|
2248
|
+
if (wt.to !== targetWindowSecs) {
|
|
2249
|
+
wt.from = displayWindow;
|
|
2250
|
+
wt.to = targetWindowSecs;
|
|
2251
|
+
wt.startMs = now_ms;
|
|
2252
|
+
wt.rangeFromMin = displayMin;
|
|
2253
|
+
wt.rangeFromMax = displayMax;
|
|
2254
|
+
const targetRightEdge = now + targetWindowSecs * buffer;
|
|
2255
|
+
const targetLeftEdge = targetRightEdge - targetWindowSecs;
|
|
2256
|
+
const targetVisible = [];
|
|
2257
|
+
for (const c of candles) {
|
|
2258
|
+
if (c.time + candleWidth >= targetLeftEdge && c.time <= targetRightEdge) {
|
|
2259
|
+
targetVisible.push(c);
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
if (liveCandle && liveCandle.time + candleWidth >= targetLeftEdge && liveCandle.time <= targetRightEdge) {
|
|
2263
|
+
targetVisible.push(liveCandle);
|
|
2264
|
+
}
|
|
2265
|
+
if (targetVisible.length > 0) {
|
|
2266
|
+
const tr = computeCandleRange(targetVisible);
|
|
2267
|
+
wt.rangeToMin = tr.min;
|
|
2268
|
+
wt.rangeToMax = tr.max;
|
|
2269
|
+
}
|
|
2270
|
+
}
|
|
2271
|
+
let windowTransProgress = 0;
|
|
2272
|
+
let resultWindow;
|
|
2273
|
+
if (wt.startMs === 0) {
|
|
2274
|
+
resultWindow = targetWindowSecs;
|
|
2275
|
+
} else {
|
|
2276
|
+
const elapsed = now_ms - wt.startMs;
|
|
2277
|
+
const t = Math.min(elapsed / WINDOW_TRANSITION_MS, 1);
|
|
2278
|
+
const eased = (1 - Math.cos(t * Math.PI)) / 2;
|
|
2279
|
+
windowTransProgress = eased;
|
|
2280
|
+
const logFrom = Math.log(wt.from);
|
|
2281
|
+
const logTo = Math.log(wt.to);
|
|
2282
|
+
resultWindow = Math.exp(logFrom + (logTo - logFrom) * eased);
|
|
2283
|
+
if (t >= 1) {
|
|
2284
|
+
resultWindow = targetWindowSecs;
|
|
2285
|
+
wt.startMs = 0;
|
|
2286
|
+
windowTransProgress = 0;
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
return { windowSecs: resultWindow, windowTransProgress };
|
|
2290
|
+
}
|
|
1433
2291
|
function useLivelineEngine(canvasRef, containerRef, config) {
|
|
1434
2292
|
const configRef = useRef(config);
|
|
1435
2293
|
configRef.current = config;
|
|
1436
2294
|
const displayValueRef = useRef(config.value);
|
|
2295
|
+
const displayValuesRef = useRef(/* @__PURE__ */ new Map());
|
|
2296
|
+
const seriesAlphaRef = useRef(/* @__PURE__ */ new Map());
|
|
1437
2297
|
const displayMinRef = useRef(0);
|
|
1438
2298
|
const displayMaxRef = useRef(0);
|
|
1439
2299
|
const targetMinRef = useRef(0);
|
|
@@ -1466,13 +2326,49 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1466
2326
|
const hoverXRef = useRef(null);
|
|
1467
2327
|
const scrubAmountRef = useRef(0);
|
|
1468
2328
|
const lastHoverRef = useRef(null);
|
|
2329
|
+
const lastHoverEntriesRef = useRef([]);
|
|
1469
2330
|
const chartRevealRef = useRef(0);
|
|
1470
2331
|
const pauseProgressRef = useRef(0);
|
|
1471
2332
|
const timeDebtRef = useRef(0);
|
|
1472
2333
|
const lastDataRef = useRef([]);
|
|
2334
|
+
const lastMultiSeriesRef = useRef([]);
|
|
1473
2335
|
const frozenNowRef = useRef(0);
|
|
1474
2336
|
const pausedDataRef = useRef(null);
|
|
2337
|
+
const pausedMultiDataRef = useRef(null);
|
|
1475
2338
|
const loadingAlphaRef = useRef(config.loading ? 1 : 0);
|
|
2339
|
+
const displayCandleRef = useRef(null);
|
|
2340
|
+
const liveBirthAlphaRef = useRef(1);
|
|
2341
|
+
const liveBullRef = useRef(0.5);
|
|
2342
|
+
const lineSmoothCloseRef = useRef(0);
|
|
2343
|
+
const lineSmoothInitedRef = useRef(false);
|
|
2344
|
+
const closeLineSmoothRef = useRef(0);
|
|
2345
|
+
const closeLineSmoothInitedRef = useRef(false);
|
|
2346
|
+
const lineModeProgRef = useRef(0);
|
|
2347
|
+
const lineModeTransRef = useRef({ startMs: 0, from: 0, to: 0 });
|
|
2348
|
+
const lineDensityProgRef = useRef(0);
|
|
2349
|
+
const lineDensityTransRef = useRef({ startMs: 0, from: 0, to: 0 });
|
|
2350
|
+
const lineTickSmoothRef = useRef(0);
|
|
2351
|
+
const lineTickSmoothInitedRef = useRef(false);
|
|
2352
|
+
const candleWidthTransRef = useRef({
|
|
2353
|
+
fromWidth: config.candleWidth ?? 1,
|
|
2354
|
+
toWidth: config.candleWidth ?? 1,
|
|
2355
|
+
startMs: 0,
|
|
2356
|
+
rangeFromMin: 0,
|
|
2357
|
+
rangeFromMax: 0,
|
|
2358
|
+
rangeToMin: 0,
|
|
2359
|
+
rangeToMax: 0,
|
|
2360
|
+
oldCandles: [],
|
|
2361
|
+
oldWidth: config.candleWidth ?? 1
|
|
2362
|
+
});
|
|
2363
|
+
const prevCandleDataRef = useRef({ candles: [], width: config.candleWidth ?? 1 });
|
|
2364
|
+
const pausedCandlesRef = useRef(null);
|
|
2365
|
+
const pausedLiveRef = useRef(null);
|
|
2366
|
+
const pausedLineDataRef = useRef(null);
|
|
2367
|
+
const pausedLineValueRef = useRef(null);
|
|
2368
|
+
const lastCandlesRef = useRef([]);
|
|
2369
|
+
const lastLiveRef = useRef(null);
|
|
2370
|
+
const lastLineDataStashRef = useRef([]);
|
|
2371
|
+
const lastLineValueStashRef = useRef(void 0);
|
|
1476
2372
|
useEffect(() => {
|
|
1477
2373
|
const container = containerRef.current;
|
|
1478
2374
|
if (!container) return;
|
|
@@ -1604,14 +2500,43 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1604
2500
|
}
|
|
1605
2501
|
applyDpr(ctx, dpr, w, h);
|
|
1606
2502
|
const noMotion = reducedMotionRef.current;
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
2503
|
+
const isCandle = cfg.mode === "candle";
|
|
2504
|
+
if (isCandle) {
|
|
2505
|
+
if (cfg.paused && pausedCandlesRef.current === null && (cfg.candles?.length ?? 0) > 0) {
|
|
2506
|
+
pausedCandlesRef.current = cfg.candles.slice();
|
|
2507
|
+
pausedLiveRef.current = cfg.liveCandle ?? null;
|
|
2508
|
+
pausedLineDataRef.current = cfg.lineData?.slice() ?? null;
|
|
2509
|
+
pausedLineValueRef.current = cfg.lineValue ?? null;
|
|
2510
|
+
}
|
|
2511
|
+
if (!cfg.paused) {
|
|
2512
|
+
pausedCandlesRef.current = null;
|
|
2513
|
+
pausedLiveRef.current = null;
|
|
2514
|
+
pausedLineDataRef.current = null;
|
|
2515
|
+
pausedLineValueRef.current = null;
|
|
2516
|
+
}
|
|
2517
|
+
} else if (cfg.isMultiSeries && cfg.multiSeries) {
|
|
2518
|
+
if (cfg.paused && pausedMultiDataRef.current === null) {
|
|
2519
|
+
const snap = /* @__PURE__ */ new Map();
|
|
2520
|
+
for (const s of cfg.multiSeries) {
|
|
2521
|
+
if (s.data.length >= 2) snap.set(s.id, { data: s.data.slice(), value: s.value });
|
|
2522
|
+
}
|
|
2523
|
+
if (snap.size > 0) pausedMultiDataRef.current = snap;
|
|
2524
|
+
}
|
|
2525
|
+
if (!cfg.paused) {
|
|
2526
|
+
pausedMultiDataRef.current = null;
|
|
2527
|
+
}
|
|
2528
|
+
} else {
|
|
2529
|
+
if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
|
|
2530
|
+
pausedDataRef.current = cfg.data.slice();
|
|
2531
|
+
}
|
|
2532
|
+
if (!cfg.paused) {
|
|
2533
|
+
pausedDataRef.current = null;
|
|
2534
|
+
}
|
|
1612
2535
|
}
|
|
1613
|
-
const points = pausedDataRef.current ?? cfg.data;
|
|
1614
|
-
const
|
|
2536
|
+
const points = isCandle ? [] : pausedDataRef.current ?? cfg.data;
|
|
2537
|
+
const effectiveCandles = isCandle ? pausedCandlesRef.current ?? (cfg.candles ?? []) : [];
|
|
2538
|
+
const hasMultiData = cfg.isMultiSeries && cfg.multiSeries ? cfg.multiSeries.some((s) => s.data.length >= 2) : false;
|
|
2539
|
+
const hasData = isCandle ? effectiveCandles.length >= 2 : hasMultiData || points.length >= 2;
|
|
1615
2540
|
const pad = cfg.padding;
|
|
1616
2541
|
const chartH = h - pad.top - pad.bottom;
|
|
1617
2542
|
const pauseTarget = cfg.paused ? 1 : 0;
|
|
@@ -1633,18 +2558,62 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1633
2558
|
if (loadingAlphaRef.current > 0.99) loadingAlphaRef.current = 1;
|
|
1634
2559
|
const loadingAlpha = loadingAlphaRef.current;
|
|
1635
2560
|
const revealTarget = !cfg.loading && hasData ? 1 : 0;
|
|
1636
|
-
chartRevealRef.current = noMotion ? revealTarget : lerp(
|
|
2561
|
+
chartRevealRef.current = noMotion ? revealTarget : lerp(
|
|
2562
|
+
chartRevealRef.current,
|
|
2563
|
+
revealTarget,
|
|
2564
|
+
revealTarget === 1 ? CHART_REVEAL_SPEED_FWD : CHART_REVEAL_SPEED,
|
|
2565
|
+
dt
|
|
2566
|
+
);
|
|
1637
2567
|
if (Math.abs(chartRevealRef.current - revealTarget) < 5e-3) {
|
|
1638
2568
|
chartRevealRef.current = revealTarget;
|
|
1639
2569
|
}
|
|
1640
2570
|
const chartReveal = chartRevealRef.current;
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
2571
|
+
if (chartReveal < 0.01) {
|
|
2572
|
+
rangeInitedRef.current = false;
|
|
2573
|
+
}
|
|
2574
|
+
let useStash;
|
|
2575
|
+
let useMultiStash = false;
|
|
2576
|
+
if (isCandle) {
|
|
2577
|
+
useStash = !hasData && chartReveal > 5e-3 && lastCandlesRef.current.length > 0;
|
|
2578
|
+
} else {
|
|
2579
|
+
useMultiStash = !hasData && chartReveal > 5e-3 && lastMultiSeriesRef.current.length > 0;
|
|
2580
|
+
if (hasMultiData && cfg.multiSeries) {
|
|
2581
|
+
lastMultiSeriesRef.current = cfg.multiSeries.map((s) => ({
|
|
2582
|
+
id: s.id,
|
|
2583
|
+
data: s.data.slice(),
|
|
2584
|
+
value: s.value,
|
|
2585
|
+
palette: s.palette,
|
|
2586
|
+
label: s.label
|
|
2587
|
+
}));
|
|
2588
|
+
}
|
|
2589
|
+
if (hasData && !cfg.isMultiSeries) lastMultiSeriesRef.current = [];
|
|
2590
|
+
useStash = !useMultiStash && !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
|
|
2591
|
+
if (hasData && !cfg.isMultiSeries) lastDataRef.current = points;
|
|
1644
2592
|
}
|
|
1645
|
-
if (
|
|
2593
|
+
if (isCandle) {
|
|
2594
|
+
const lmt = lineModeTransRef.current;
|
|
2595
|
+
const lineModeTarget = cfg.lineMode ? 1 : 0;
|
|
2596
|
+
if (lmt.to !== lineModeTarget) {
|
|
2597
|
+
lmt.from = lineModeProgRef.current;
|
|
2598
|
+
lmt.to = lineModeTarget;
|
|
2599
|
+
lmt.startMs = now_ms;
|
|
2600
|
+
}
|
|
2601
|
+
if (lmt.startMs > 0) {
|
|
2602
|
+
const elapsed = now_ms - lmt.startMs;
|
|
2603
|
+
const t = Math.min(elapsed / LINE_MORPH_MS, 1);
|
|
2604
|
+
lineModeProgRef.current = lmt.from + (lmt.to - lmt.from) * ((1 - Math.cos(t * Math.PI)) / 2);
|
|
2605
|
+
if (t >= 1) {
|
|
2606
|
+
lineModeProgRef.current = lmt.to;
|
|
2607
|
+
lmt.startMs = 0;
|
|
2608
|
+
}
|
|
2609
|
+
} else {
|
|
2610
|
+
lineModeProgRef.current = lmt.to;
|
|
2611
|
+
}
|
|
2612
|
+
}
|
|
2613
|
+
if (!hasData && !useStash && !useMultiStash) {
|
|
2614
|
+
const loadingColor = isCandle || cfg.isMultiSeries || lastMultiSeriesRef.current.length > 0 ? cfg.palette.gridLabel : void 0;
|
|
1646
2615
|
if (loadingAlpha > 0.01) {
|
|
1647
|
-
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha);
|
|
2616
|
+
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, loadingColor);
|
|
1648
2617
|
}
|
|
1649
2618
|
if (1 - loadingAlpha > 0.01) {
|
|
1650
2619
|
drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
|
|
@@ -1661,190 +2630,840 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1661
2630
|
rafRef.current = requestAnimationFrame(draw);
|
|
1662
2631
|
return;
|
|
1663
2632
|
}
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
2633
|
+
if (isCandle) {
|
|
2634
|
+
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
2635
|
+
const now = hasData || chartReveal < 5e-3 ? Date.now() / 1e3 - timeDebtRef.current : frozenNowRef.current;
|
|
2636
|
+
const rawLive = pausedCandlesRef.current ? pausedLiveRef.current ?? void 0 : cfg.liveCandle;
|
|
2637
|
+
let effectiveLineData = pausedLineDataRef.current ?? cfg.lineData;
|
|
2638
|
+
let effectiveLineValue = pausedLineValueRef.current ?? cfg.lineValue;
|
|
2639
|
+
if (hasData && effectiveLineData && effectiveLineData.length > 0) {
|
|
2640
|
+
lastLineDataStashRef.current = effectiveLineData;
|
|
2641
|
+
lastLineValueStashRef.current = effectiveLineValue;
|
|
2642
|
+
}
|
|
2643
|
+
if (useStash && lastLineDataStashRef.current.length > 0) {
|
|
2644
|
+
effectiveLineData = lastLineDataStashRef.current;
|
|
2645
|
+
effectiveLineValue = lastLineValueStashRef.current;
|
|
2646
|
+
}
|
|
2647
|
+
const candleWidthSecs = cfg.candleWidth ?? 1;
|
|
2648
|
+
const cwt = candleWidthTransRef.current;
|
|
2649
|
+
let morphT = -1;
|
|
2650
|
+
let displayCandleWidth;
|
|
2651
|
+
if (cwt.startMs > 0) {
|
|
2652
|
+
const elapsed = now_ms - cwt.startMs;
|
|
2653
|
+
const t = Math.min(elapsed / CANDLE_WIDTH_TRANS_MS, 1);
|
|
2654
|
+
morphT = (1 - Math.cos(t * Math.PI)) / 2;
|
|
2655
|
+
displayCandleWidth = Math.exp(
|
|
2656
|
+
Math.log(cwt.fromWidth) + (Math.log(cwt.toWidth) - Math.log(cwt.fromWidth)) * morphT
|
|
2657
|
+
);
|
|
2658
|
+
if (t >= 1) {
|
|
2659
|
+
displayCandleWidth = cwt.toWidth;
|
|
2660
|
+
cwt.startMs = 0;
|
|
2661
|
+
morphT = -1;
|
|
2662
|
+
}
|
|
2663
|
+
} else {
|
|
2664
|
+
displayCandleWidth = cwt.toWidth;
|
|
2665
|
+
}
|
|
2666
|
+
if (candleWidthSecs !== cwt.toWidth) {
|
|
2667
|
+
cwt.oldCandles = prevCandleDataRef.current.candles;
|
|
2668
|
+
cwt.oldWidth = prevCandleDataRef.current.width;
|
|
2669
|
+
cwt.fromWidth = displayCandleWidth;
|
|
2670
|
+
cwt.toWidth = candleWidthSecs;
|
|
2671
|
+
cwt.startMs = now_ms;
|
|
2672
|
+
morphT = 0;
|
|
2673
|
+
cwt.rangeFromMin = displayMinRef.current;
|
|
2674
|
+
cwt.rangeFromMax = displayMaxRef.current;
|
|
2675
|
+
const curWindow = displayWindowRef.current;
|
|
2676
|
+
const re = now + curWindow * CANDLE_BUFFER;
|
|
2677
|
+
const le = re - curWindow;
|
|
2678
|
+
const targetVis = [];
|
|
2679
|
+
for (const c of effectiveCandles) {
|
|
2680
|
+
if (c.time + candleWidthSecs >= le && c.time <= re) targetVis.push(c);
|
|
2681
|
+
}
|
|
2682
|
+
if (rawLive) targetVis.push(rawLive);
|
|
2683
|
+
if (targetVis.length > 0) {
|
|
2684
|
+
const tr = computeCandleRange(targetVis);
|
|
2685
|
+
cwt.rangeToMin = tr.min;
|
|
2686
|
+
cwt.rangeToMax = tr.max;
|
|
2687
|
+
} else {
|
|
2688
|
+
cwt.rangeToMin = displayMinRef.current;
|
|
2689
|
+
cwt.rangeToMax = displayMaxRef.current;
|
|
2690
|
+
}
|
|
2691
|
+
}
|
|
2692
|
+
prevCandleDataRef.current = { candles: cfg.candles ?? [], width: candleWidthSecs };
|
|
2693
|
+
const lineModeProg = lineModeProgRef.current;
|
|
2694
|
+
const ldt = lineDensityTransRef.current;
|
|
2695
|
+
const hasTickData = effectiveLineData && effectiveLineData.length > 0;
|
|
2696
|
+
const densityTarget = cfg.lineMode && lineModeProg >= 0.3 && hasTickData ? 1 : 0;
|
|
2697
|
+
if (ldt.to !== densityTarget) {
|
|
2698
|
+
ldt.from = lineDensityProgRef.current;
|
|
2699
|
+
ldt.to = densityTarget;
|
|
2700
|
+
ldt.startMs = now_ms;
|
|
2701
|
+
}
|
|
2702
|
+
let lineDensityProg;
|
|
2703
|
+
if (ldt.startMs > 0) {
|
|
2704
|
+
const elapsed = now_ms - ldt.startMs;
|
|
2705
|
+
const t = Math.min(elapsed / LINE_DENSITY_MS, 1);
|
|
2706
|
+
lineDensityProg = ldt.from + (ldt.to - ldt.from) * (1 - (1 - t) * (1 - t));
|
|
2707
|
+
if (t >= 1) {
|
|
2708
|
+
lineDensityProg = ldt.to;
|
|
2709
|
+
ldt.startMs = 0;
|
|
2710
|
+
}
|
|
2711
|
+
} else {
|
|
2712
|
+
lineDensityProg = ldt.to;
|
|
2713
|
+
}
|
|
2714
|
+
lineDensityProgRef.current = lineDensityProg;
|
|
2715
|
+
const transition = windowTransitionRef.current;
|
|
2716
|
+
const windowResult = updateCandleWindowTransition(
|
|
2717
|
+
cfg.windowSecs,
|
|
2718
|
+
transition,
|
|
2719
|
+
displayWindowRef.current,
|
|
2720
|
+
displayMinRef.current,
|
|
2721
|
+
displayMaxRef.current,
|
|
2722
|
+
now_ms,
|
|
2723
|
+
now,
|
|
2724
|
+
effectiveCandles,
|
|
2725
|
+
rawLive,
|
|
2726
|
+
candleWidthSecs,
|
|
2727
|
+
CANDLE_BUFFER
|
|
2728
|
+
);
|
|
2729
|
+
displayWindowRef.current = windowResult.windowSecs;
|
|
2730
|
+
const windowSecs = windowResult.windowSecs;
|
|
2731
|
+
const windowTransProgress = windowResult.windowTransProgress;
|
|
2732
|
+
const isWindowTransitioning = transition.startMs > 0;
|
|
2733
|
+
const rightEdge = now + windowSecs * CANDLE_BUFFER;
|
|
2734
|
+
const leftEdge = rightEdge - windowSecs;
|
|
2735
|
+
let smoothLive;
|
|
2736
|
+
if (rawLive) {
|
|
2737
|
+
const prev = displayCandleRef.current;
|
|
2738
|
+
if (!prev || prev.time !== rawLive.time) {
|
|
2739
|
+
displayCandleRef.current = {
|
|
2740
|
+
time: rawLive.time,
|
|
2741
|
+
open: rawLive.open,
|
|
2742
|
+
high: rawLive.open,
|
|
2743
|
+
low: rawLive.open,
|
|
2744
|
+
close: rawLive.open
|
|
2745
|
+
};
|
|
2746
|
+
liveBirthAlphaRef.current = 0;
|
|
2747
|
+
} else {
|
|
2748
|
+
const dc2 = displayCandleRef.current;
|
|
2749
|
+
dc2.open = lerp(dc2.open, rawLive.open, CANDLE_LERP_SPEED, pausedDt);
|
|
2750
|
+
dc2.high = lerp(dc2.high, rawLive.high, CANDLE_LERP_SPEED, pausedDt);
|
|
2751
|
+
dc2.low = lerp(dc2.low, rawLive.low, CANDLE_LERP_SPEED, pausedDt);
|
|
2752
|
+
dc2.close = lerp(dc2.close, rawLive.close, CANDLE_LERP_SPEED, pausedDt);
|
|
2753
|
+
}
|
|
2754
|
+
liveBirthAlphaRef.current = lerp(liveBirthAlphaRef.current, 1, 0.2, pausedDt);
|
|
2755
|
+
if (liveBirthAlphaRef.current > 0.99) liveBirthAlphaRef.current = 1;
|
|
2756
|
+
const dc = displayCandleRef.current;
|
|
2757
|
+
const bullTarget = dc.close >= dc.open ? 1 : 0;
|
|
2758
|
+
liveBullRef.current = lerp(liveBullRef.current, bullTarget, 0.12, pausedDt);
|
|
2759
|
+
if (liveBullRef.current > 0.99) liveBullRef.current = 1;
|
|
2760
|
+
if (liveBullRef.current < 0.01) liveBullRef.current = 0;
|
|
2761
|
+
smoothLive = dc;
|
|
2762
|
+
} else {
|
|
2763
|
+
displayCandleRef.current = null;
|
|
2764
|
+
liveBirthAlphaRef.current = 1;
|
|
2765
|
+
liveBullRef.current = 0.5;
|
|
2766
|
+
}
|
|
2767
|
+
if (rawLive) {
|
|
2768
|
+
if (!closeLineSmoothInitedRef.current) {
|
|
2769
|
+
closeLineSmoothRef.current = rawLive.close;
|
|
2770
|
+
closeLineSmoothInitedRef.current = true;
|
|
2771
|
+
} else {
|
|
2772
|
+
closeLineSmoothRef.current = lerp(closeLineSmoothRef.current, rawLive.close, CLOSE_LINE_LERP_SPEED, pausedDt);
|
|
2773
|
+
const gap = Math.abs(closeLineSmoothRef.current - rawLive.close);
|
|
2774
|
+
const range = displayMaxRef.current - displayMinRef.current || 1;
|
|
2775
|
+
if (gap < range * 5e-4) closeLineSmoothRef.current = rawLive.close;
|
|
2776
|
+
}
|
|
2777
|
+
} else if (!useStash) {
|
|
2778
|
+
closeLineSmoothInitedRef.current = false;
|
|
2779
|
+
}
|
|
2780
|
+
if (rawLive) {
|
|
2781
|
+
if (!lineSmoothInitedRef.current) {
|
|
2782
|
+
lineSmoothCloseRef.current = rawLive.close;
|
|
2783
|
+
lineSmoothInitedRef.current = true;
|
|
2784
|
+
} else {
|
|
2785
|
+
const valGap = Math.abs(rawLive.close - lineSmoothCloseRef.current);
|
|
2786
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
2787
|
+
const gapRatio = Math.min(valGap / prevRange, 1);
|
|
2788
|
+
const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
|
|
2789
|
+
lineSmoothCloseRef.current = lerp(lineSmoothCloseRef.current, rawLive.close, adaptiveSpeed, pausedDt);
|
|
2790
|
+
if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineSmoothCloseRef.current = rawLive.close;
|
|
2791
|
+
}
|
|
2792
|
+
} else if (!useStash) {
|
|
2793
|
+
lineSmoothInitedRef.current = false;
|
|
2794
|
+
}
|
|
2795
|
+
if (effectiveLineValue !== void 0 && hasTickData) {
|
|
2796
|
+
if (!lineTickSmoothInitedRef.current) {
|
|
2797
|
+
lineTickSmoothRef.current = effectiveLineValue;
|
|
2798
|
+
lineTickSmoothInitedRef.current = true;
|
|
2799
|
+
} else {
|
|
2800
|
+
const valGap = Math.abs(effectiveLineValue - lineTickSmoothRef.current);
|
|
2801
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
2802
|
+
const gapRatio = Math.min(valGap / prevRange, 1);
|
|
2803
|
+
const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
|
|
2804
|
+
lineTickSmoothRef.current = lerp(lineTickSmoothRef.current, effectiveLineValue, adaptiveSpeed, pausedDt);
|
|
2805
|
+
if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineTickSmoothRef.current = effectiveLineValue;
|
|
2806
|
+
}
|
|
2807
|
+
} else if (!useStash) {
|
|
2808
|
+
lineTickSmoothInitedRef.current = false;
|
|
2809
|
+
}
|
|
2810
|
+
const visible = [];
|
|
2811
|
+
for (const c of effectiveCandles) {
|
|
2812
|
+
if (c.time + candleWidthSecs >= leftEdge && c.time <= rightEdge) visible.push(c);
|
|
2813
|
+
}
|
|
2814
|
+
if (smoothLive && smoothLive.time + displayCandleWidth >= leftEdge && smoothLive.time <= rightEdge) {
|
|
2815
|
+
visible.push(smoothLive);
|
|
2816
|
+
}
|
|
2817
|
+
let oldVisible = [];
|
|
2818
|
+
if (morphT >= 0 && cwt.oldCandles.length > 0) {
|
|
2819
|
+
for (const c of cwt.oldCandles) {
|
|
2820
|
+
if (c.time + cwt.oldWidth >= leftEdge && c.time <= rightEdge) oldVisible.push(c);
|
|
2821
|
+
}
|
|
2822
|
+
}
|
|
2823
|
+
if (hasData) {
|
|
2824
|
+
lastCandlesRef.current = visible;
|
|
2825
|
+
lastLiveRef.current = smoothLive ?? null;
|
|
2826
|
+
}
|
|
2827
|
+
const effectiveVisible = useStash ? lastCandlesRef.current : visible;
|
|
2828
|
+
const effectiveLive = useStash ? lastLiveRef.current ?? void 0 : smoothLive;
|
|
2829
|
+
const chartW = w - pad.left - pad.right;
|
|
2830
|
+
const computed = effectiveVisible.length > 0 ? computeCandleRange(effectiveVisible) : { min: displayMinRef.current, max: displayMaxRef.current };
|
|
2831
|
+
const rangeResult = updateCandleRange(
|
|
2832
|
+
computed,
|
|
2833
|
+
rangeInitedRef.current,
|
|
2834
|
+
displayMinRef.current,
|
|
2835
|
+
displayMaxRef.current,
|
|
2836
|
+
isWindowTransitioning,
|
|
2837
|
+
windowTransProgress,
|
|
2838
|
+
transition,
|
|
2839
|
+
chartH,
|
|
2840
|
+
pausedDt
|
|
2841
|
+
);
|
|
2842
|
+
if (morphT >= 0) {
|
|
2843
|
+
rangeResult.displayMin = cwt.rangeFromMin + (cwt.rangeToMin - cwt.rangeFromMin) * morphT;
|
|
2844
|
+
rangeResult.displayMax = cwt.rangeFromMax + (cwt.rangeToMax - cwt.rangeFromMax) * morphT;
|
|
2845
|
+
rangeResult.minVal = rangeResult.displayMin;
|
|
2846
|
+
rangeResult.maxVal = rangeResult.displayMax;
|
|
2847
|
+
rangeResult.valRange = rangeResult.displayMax - rangeResult.displayMin || 1e-3;
|
|
2848
|
+
}
|
|
2849
|
+
rangeInitedRef.current = rangeResult.rangeInited;
|
|
2850
|
+
displayMinRef.current = rangeResult.displayMin;
|
|
2851
|
+
displayMaxRef.current = rangeResult.displayMax;
|
|
2852
|
+
const { minVal, maxVal, valRange } = rangeResult;
|
|
2853
|
+
const layout = {
|
|
2854
|
+
w,
|
|
2855
|
+
h,
|
|
2856
|
+
pad,
|
|
2857
|
+
chartW,
|
|
2858
|
+
chartH,
|
|
2859
|
+
leftEdge,
|
|
2860
|
+
rightEdge,
|
|
2861
|
+
minVal,
|
|
2862
|
+
maxVal,
|
|
2863
|
+
valRange,
|
|
2864
|
+
toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
|
|
2865
|
+
toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
|
|
2866
|
+
};
|
|
2867
|
+
const hoverPx = hoverXRef.current;
|
|
2868
|
+
let hoveredCandle = null;
|
|
2869
|
+
let isActiveHover = false;
|
|
2870
|
+
if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
|
|
2871
|
+
hoveredCandle = candleAtX(effectiveVisible, hoverPx, displayCandleWidth, layout);
|
|
2872
|
+
if (hoveredCandle) isActiveHover = true;
|
|
2873
|
+
}
|
|
2874
|
+
const scrubTarget = isActiveHover ? 1 : 0;
|
|
2875
|
+
scrubAmountRef.current = lerp(scrubAmountRef.current, scrubTarget, 0.12, dt);
|
|
2876
|
+
if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
|
|
2877
|
+
if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
|
|
2878
|
+
const scrubAmount = scrubAmountRef.current;
|
|
2879
|
+
let drawHoverX = hoverPx;
|
|
2880
|
+
let drawHoverTime = 0;
|
|
2881
|
+
let drawHoverCandle = hoveredCandle;
|
|
2882
|
+
if (!isActiveHover && scrubAmount > 0 && lastHoverRef.current) {
|
|
2883
|
+
drawHoverX = lastHoverRef.current.x;
|
|
2884
|
+
drawHoverTime = lastHoverRef.current.time;
|
|
2885
|
+
drawHoverCandle = candleAtX(effectiveVisible, lastHoverRef.current.x, displayCandleWidth, layout);
|
|
2886
|
+
} else if (isActiveHover && hoverPx !== null) {
|
|
2887
|
+
drawHoverTime = layout.leftEdge + (hoverPx - pad.left) / chartW * (layout.rightEdge - layout.leftEdge);
|
|
2888
|
+
lastHoverRef.current = { x: hoverPx, value: hoveredCandle?.close ?? 0, time: drawHoverTime };
|
|
2889
|
+
}
|
|
2890
|
+
let drawCandles = effectiveVisible;
|
|
2891
|
+
let drawOldCandles = oldVisible;
|
|
2892
|
+
let drawLive = effectiveLive;
|
|
2893
|
+
if (lineModeProg > 0.01 && drawLive && lineSmoothInitedRef.current) {
|
|
2894
|
+
const blended = drawLive.close + (lineSmoothCloseRef.current - drawLive.close) * lineModeProg;
|
|
2895
|
+
drawLive = { ...drawLive, close: blended };
|
|
2896
|
+
const li = drawCandles.length - 1;
|
|
2897
|
+
if (li >= 0 && drawCandles[li].time === drawLive.time) {
|
|
2898
|
+
drawCandles = drawCandles.slice();
|
|
2899
|
+
drawCandles[li] = { ...drawCandles[li], close: blended };
|
|
2900
|
+
}
|
|
2901
|
+
}
|
|
2902
|
+
if (lineModeProg > 0.01 && lineModeProg < 0.99) {
|
|
2903
|
+
const collapseOHLC = (c) => {
|
|
2904
|
+
const inv = 1 - lineModeProg;
|
|
2905
|
+
return {
|
|
2906
|
+
time: c.time,
|
|
2907
|
+
open: c.close + (c.open - c.close) * inv,
|
|
2908
|
+
high: c.close + (c.high - c.close) * inv,
|
|
2909
|
+
low: c.close + (c.low - c.close) * inv,
|
|
2910
|
+
close: c.close
|
|
2911
|
+
};
|
|
2912
|
+
};
|
|
2913
|
+
drawCandles = drawCandles.map(collapseOHLC);
|
|
2914
|
+
if (drawOldCandles.length > 0) drawOldCandles = drawOldCandles.map(collapseOHLC);
|
|
2915
|
+
if (drawLive) drawLive = collapseOHLC(drawLive);
|
|
2916
|
+
}
|
|
2917
|
+
let lineVisible;
|
|
2918
|
+
let lineSmoothValue;
|
|
2919
|
+
if (effectiveLineData && effectiveLineData.length > 0 && (lineDensityProg > 0.01 || lineModeProg > 0.05)) {
|
|
2920
|
+
const closeRefs = [];
|
|
2921
|
+
for (const c of drawCandles) {
|
|
2922
|
+
closeRefs.push({ t: c.time + displayCandleWidth / 2, v: c.close });
|
|
2923
|
+
}
|
|
2924
|
+
if (drawLive) closeRefs.push({ t: now, v: drawLive.close });
|
|
2925
|
+
lineVisible = [];
|
|
2926
|
+
let refIdx = 0;
|
|
2927
|
+
for (const pt of effectiveLineData) {
|
|
2928
|
+
if (pt.time < leftEdge || pt.time > rightEdge) continue;
|
|
2929
|
+
while (refIdx < closeRefs.length - 2 && closeRefs[refIdx + 1].t < pt.time) refIdx++;
|
|
2930
|
+
let interpClose;
|
|
2931
|
+
if (closeRefs.length === 0) {
|
|
2932
|
+
interpClose = pt.value;
|
|
2933
|
+
} else if (closeRefs.length === 1 || pt.time <= closeRefs[0].t) {
|
|
2934
|
+
interpClose = closeRefs[0].v;
|
|
2935
|
+
} else if (refIdx >= closeRefs.length - 1) {
|
|
2936
|
+
interpClose = closeRefs[closeRefs.length - 1].v;
|
|
2937
|
+
} else {
|
|
2938
|
+
const a = closeRefs[refIdx];
|
|
2939
|
+
const b = closeRefs[refIdx + 1];
|
|
2940
|
+
const span = b.t - a.t;
|
|
2941
|
+
const frac = span > 0 ? Math.max(0, Math.min(1, (pt.time - a.t) / span)) : 0;
|
|
2942
|
+
interpClose = a.v + (b.v - a.v) * frac;
|
|
2943
|
+
}
|
|
2944
|
+
const blended = interpClose + (pt.value - interpClose) * lineDensityProg;
|
|
2945
|
+
lineVisible.push({ time: pt.time, value: blended });
|
|
2946
|
+
}
|
|
2947
|
+
const smoothTick = lineTickSmoothInitedRef.current ? lineTickSmoothRef.current : effectiveLineValue ?? effectiveLineData[effectiveLineData.length - 1].value;
|
|
2948
|
+
lineSmoothValue = lineSmoothCloseRef.current + (smoothTick - lineSmoothCloseRef.current) * lineDensityProg;
|
|
2949
|
+
} else {
|
|
2950
|
+
lineVisible = drawCandles.map((c) => ({
|
|
2951
|
+
time: c.time + displayCandleWidth / 2,
|
|
2952
|
+
value: c.close
|
|
2953
|
+
}));
|
|
2954
|
+
lineSmoothValue = lineSmoothInitedRef.current ? lineSmoothCloseRef.current : drawLive?.close ?? drawCandles[drawCandles.length - 1]?.close ?? 0;
|
|
2955
|
+
}
|
|
2956
|
+
if (chartReveal < 1 && lineVisible.length >= 2) {
|
|
2957
|
+
const firstTime = lineVisible[0].time;
|
|
2958
|
+
const windowSpan = rightEdge - leftEdge;
|
|
2959
|
+
if (firstTime - leftEdge > windowSpan * 0.05) {
|
|
2960
|
+
const firstVal = lineVisible[0].value;
|
|
2961
|
+
const step = windowSpan / 32;
|
|
2962
|
+
const padded = [];
|
|
2963
|
+
for (let t = leftEdge; t < firstTime - step * 0.5; t += step) {
|
|
2964
|
+
padded.push({ time: t, value: firstVal });
|
|
2965
|
+
}
|
|
2966
|
+
lineVisible = [...padded, ...lineVisible];
|
|
2967
|
+
}
|
|
2968
|
+
}
|
|
2969
|
+
drawCandleFrame(ctx, layout, cfg.palette, {
|
|
2970
|
+
candles: drawCandles,
|
|
2971
|
+
displayCandleWidth,
|
|
2972
|
+
oldCandles: drawOldCandles,
|
|
2973
|
+
oldWidth: cwt.oldWidth,
|
|
2974
|
+
morphT,
|
|
2975
|
+
liveCandle: drawLive,
|
|
2976
|
+
closePriceCandle: closeLineSmoothInitedRef.current && rawLive ? { ...rawLive, close: closeLineSmoothRef.current } : rawLive,
|
|
2977
|
+
liveTime: effectiveLive?.time ?? -1,
|
|
2978
|
+
liveBirthAlpha: liveBirthAlphaRef.current,
|
|
2979
|
+
liveBullBlend: liveBullRef.current,
|
|
2980
|
+
lineModeProg,
|
|
2981
|
+
chartReveal,
|
|
2982
|
+
now_ms,
|
|
2983
|
+
now,
|
|
2984
|
+
pauseProgress,
|
|
2985
|
+
showGrid: cfg.showGrid,
|
|
2986
|
+
scrubAmount,
|
|
2987
|
+
hoverX: drawHoverX,
|
|
2988
|
+
hoverValue: drawHoverCandle?.close ?? null,
|
|
2989
|
+
hoverTime: drawHoverTime,
|
|
2990
|
+
hoveredCandle: drawHoverCandle,
|
|
2991
|
+
formatValue: cfg.formatValue,
|
|
2992
|
+
formatTime: cfg.formatTime,
|
|
2993
|
+
gridState: gridStateRef.current,
|
|
2994
|
+
timeAxisState: timeAxisStateRef.current,
|
|
2995
|
+
dt: pausedDt,
|
|
2996
|
+
targetWindowSecs: cfg.windowSecs,
|
|
2997
|
+
tooltipY: cfg.tooltipY,
|
|
2998
|
+
tooltipOutline: cfg.tooltipOutline,
|
|
2999
|
+
lineVisible,
|
|
3000
|
+
lineSmoothValue,
|
|
3001
|
+
emptyText: cfg.emptyText,
|
|
3002
|
+
loadingAlpha,
|
|
3003
|
+
// Show empty overlay when not loading AND loadingAlpha has fully
|
|
3004
|
+
// decayed. This prevents the gradient gap from flashing during
|
|
3005
|
+
// loading→live (where loadingAlpha starts at ~1), while still
|
|
3006
|
+
// allowing smooth fade-out during empty→live (loadingAlpha is 0).
|
|
3007
|
+
showEmptyOverlay: !(cfg.loading ?? false) && loadingAlpha < 0.01
|
|
3008
|
+
});
|
|
3009
|
+
if (badgeRef.current) {
|
|
3010
|
+
if (lineModeProg > 0.5 && cfg.showBadge) {
|
|
3011
|
+
const momentum = detectMomentum(lineVisible);
|
|
3012
|
+
badgeYRef.current = updateBadgeDOM(
|
|
3013
|
+
badgeRef.current,
|
|
3014
|
+
cfg,
|
|
3015
|
+
lineSmoothValue,
|
|
3016
|
+
layout,
|
|
3017
|
+
momentum,
|
|
3018
|
+
badgeYRef.current,
|
|
3019
|
+
badgeColorRef.current,
|
|
3020
|
+
isWindowTransitioning,
|
|
3021
|
+
noMotion,
|
|
3022
|
+
ctx,
|
|
3023
|
+
pausedDt,
|
|
3024
|
+
chartReveal
|
|
3025
|
+
);
|
|
3026
|
+
const badgeFade = (lineModeProg - 0.5) * 2;
|
|
3027
|
+
if (badgeRef.current.container.style.display !== "none") {
|
|
3028
|
+
const base = badgeRef.current.container.style.opacity ? parseFloat(badgeRef.current.container.style.opacity) : 1;
|
|
3029
|
+
badgeRef.current.container.style.opacity = String(
|
|
3030
|
+
base * badgeFade * (1 - pauseProgress)
|
|
3031
|
+
);
|
|
3032
|
+
}
|
|
3033
|
+
} else {
|
|
3034
|
+
badgeRef.current.container.style.display = "none";
|
|
3035
|
+
}
|
|
3036
|
+
}
|
|
3037
|
+
} else if (cfg.isMultiSeries && cfg.multiSeries && cfg.multiSeries.length > 0 || useMultiStash) {
|
|
3038
|
+
const effectiveMultiSeries = useMultiStash ? lastMultiSeriesRef.current : cfg.multiSeries;
|
|
3039
|
+
let labelReserve = 0;
|
|
3040
|
+
if (effectiveMultiSeries.some((s) => s.label)) {
|
|
3041
|
+
ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
|
|
3042
|
+
let maxLabelW = 0;
|
|
3043
|
+
for (const s of effectiveMultiSeries) {
|
|
3044
|
+
if (s.label) {
|
|
3045
|
+
const lw = ctx.measureText(s.label).width;
|
|
3046
|
+
if (lw > maxLabelW) maxLabelW = lw;
|
|
3047
|
+
}
|
|
3048
|
+
}
|
|
3049
|
+
labelReserve = Math.max(0, maxLabelW - 2) * chartReveal;
|
|
3050
|
+
}
|
|
3051
|
+
const chartW = w - pad.left - pad.right - labelReserve;
|
|
3052
|
+
const buffer = WINDOW_BUFFER;
|
|
3053
|
+
if (!useMultiStash) {
|
|
3054
|
+
const currentIds = new Set(effectiveMultiSeries.map((s) => s.id));
|
|
3055
|
+
for (const key of displayValuesRef.current.keys()) {
|
|
3056
|
+
if (!currentIds.has(key)) displayValuesRef.current.delete(key);
|
|
3057
|
+
}
|
|
3058
|
+
}
|
|
3059
|
+
const firstSeries = effectiveMultiSeries[0];
|
|
3060
|
+
const transition = windowTransitionRef.current;
|
|
3061
|
+
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
3062
|
+
const now = useMultiStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
3063
|
+
const smoothValues = /* @__PURE__ */ new Map();
|
|
3064
|
+
for (const s of effectiveMultiSeries) {
|
|
3065
|
+
let dv = displayValuesRef.current.get(s.id);
|
|
3066
|
+
if (dv === void 0) dv = s.value;
|
|
3067
|
+
if (!useMultiStash) {
|
|
3068
|
+
const adaptiveSpeed2 = computeAdaptiveSpeed(
|
|
3069
|
+
s.value,
|
|
3070
|
+
dv,
|
|
3071
|
+
displayMinRef.current,
|
|
3072
|
+
displayMaxRef.current,
|
|
3073
|
+
cfg.lerpSpeed,
|
|
3074
|
+
noMotion
|
|
3075
|
+
);
|
|
3076
|
+
dv = lerp(dv, s.value, adaptiveSpeed2, pausedDt);
|
|
3077
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
3078
|
+
if (Math.abs(dv - s.value) < prevRange * VALUE_SNAP_THRESHOLD) dv = s.value;
|
|
3079
|
+
displayValuesRef.current.set(s.id, dv);
|
|
3080
|
+
}
|
|
3081
|
+
smoothValues.set(s.id, dv);
|
|
3082
|
+
}
|
|
3083
|
+
const hiddenIds = cfg.hiddenSeriesIds;
|
|
3084
|
+
const seriesAlphas = seriesAlphaRef.current;
|
|
3085
|
+
for (const s of effectiveMultiSeries) {
|
|
3086
|
+
let alpha = seriesAlphas.get(s.id) ?? 1;
|
|
3087
|
+
const target = hiddenIds?.has(s.id) ? 0 : 1;
|
|
3088
|
+
alpha = noMotion ? target : lerp(alpha, target, SERIES_TOGGLE_SPEED, pausedDt);
|
|
3089
|
+
if (alpha < 0.01) alpha = 0;
|
|
3090
|
+
if (alpha > 0.99) alpha = 1;
|
|
3091
|
+
seriesAlphas.set(s.id, alpha);
|
|
3092
|
+
}
|
|
3093
|
+
const firstData = pausedMultiDataRef.current?.get(firstSeries.id)?.data ?? firstSeries.data;
|
|
3094
|
+
const windowResult = updateWindowTransition(
|
|
3095
|
+
cfg,
|
|
3096
|
+
transition,
|
|
3097
|
+
displayWindowRef.current,
|
|
3098
|
+
displayMinRef.current,
|
|
3099
|
+
displayMaxRef.current,
|
|
3100
|
+
noMotion,
|
|
3101
|
+
now_ms,
|
|
3102
|
+
now,
|
|
3103
|
+
firstData,
|
|
3104
|
+
smoothValues.get(firstSeries.id) ?? firstSeries.value,
|
|
3105
|
+
buffer
|
|
3106
|
+
);
|
|
3107
|
+
if (transition.startMs > 0 && effectiveMultiSeries.length > 1) {
|
|
3108
|
+
const targetRightEdge = now + cfg.windowSecs * buffer;
|
|
3109
|
+
const targetLeftEdge = targetRightEdge - cfg.windowSecs;
|
|
3110
|
+
let unionMin = Infinity;
|
|
3111
|
+
let unionMax = -Infinity;
|
|
3112
|
+
for (const s of effectiveMultiSeries) {
|
|
3113
|
+
const sData = pausedMultiDataRef.current?.get(s.id)?.data ?? s.data;
|
|
3114
|
+
const sv = smoothValues.get(s.id) ?? s.value;
|
|
3115
|
+
const targetVisible = [];
|
|
3116
|
+
for (const p of sData) {
|
|
3117
|
+
if (p.time >= targetLeftEdge - 2 && p.time <= targetRightEdge) targetVisible.push(p);
|
|
3118
|
+
}
|
|
3119
|
+
if (targetVisible.length > 0) {
|
|
3120
|
+
const range = computeRange(targetVisible, sv, cfg.referenceLine?.value, cfg.exaggerate);
|
|
3121
|
+
if (range.min < unionMin) unionMin = range.min;
|
|
3122
|
+
if (range.max > unionMax) unionMax = range.max;
|
|
3123
|
+
}
|
|
3124
|
+
}
|
|
3125
|
+
if (isFinite(unionMin) && isFinite(unionMax)) {
|
|
3126
|
+
transition.rangeToMin = unionMin;
|
|
3127
|
+
transition.rangeToMax = unionMax;
|
|
3128
|
+
}
|
|
3129
|
+
}
|
|
3130
|
+
displayWindowRef.current = windowResult.windowSecs;
|
|
3131
|
+
const windowSecs = windowResult.windowSecs;
|
|
3132
|
+
const windowTransProgress = windowResult.windowTransProgress;
|
|
3133
|
+
const isWindowTransitioning = transition.startMs > 0;
|
|
3134
|
+
const rightEdge = now + windowSecs * buffer;
|
|
3135
|
+
const leftEdge = rightEdge - windowSecs;
|
|
3136
|
+
const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
|
|
3137
|
+
const seriesEntries = [];
|
|
3138
|
+
let globalMin = Infinity;
|
|
3139
|
+
let globalMax = -Infinity;
|
|
3140
|
+
for (const s of effectiveMultiSeries) {
|
|
3141
|
+
const snap = pausedMultiDataRef.current?.get(s.id);
|
|
3142
|
+
const seriesData = snap?.data ?? s.data;
|
|
3143
|
+
const visible = [];
|
|
3144
|
+
for (const p of seriesData) {
|
|
3145
|
+
if (p.time >= leftEdge - 2 && p.time <= filterRight) visible.push(p);
|
|
3146
|
+
}
|
|
3147
|
+
const sv = smoothValues.get(s.id) ?? s.value;
|
|
3148
|
+
const alpha = seriesAlphas.get(s.id) ?? 1;
|
|
3149
|
+
if (visible.length >= 2) {
|
|
3150
|
+
if (alpha > 0.01) {
|
|
3151
|
+
const range = computeRange(visible, sv, cfg.referenceLine?.value, cfg.exaggerate);
|
|
3152
|
+
if (range.min < globalMin) globalMin = range.min;
|
|
3153
|
+
if (range.max > globalMax) globalMax = range.max;
|
|
3154
|
+
}
|
|
3155
|
+
seriesEntries.push({ visible, smoothValue: sv, palette: s.palette, label: s.label, alpha });
|
|
3156
|
+
}
|
|
3157
|
+
}
|
|
3158
|
+
if (seriesEntries.length === 0) {
|
|
3159
|
+
if (loadingAlpha > 0.01) {
|
|
3160
|
+
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, cfg.palette.gridLabel);
|
|
3161
|
+
}
|
|
3162
|
+
if (1 - loadingAlpha > 0.01) {
|
|
3163
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
|
|
3164
|
+
}
|
|
3165
|
+
ctx.save();
|
|
3166
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
3167
|
+
const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
|
|
3168
|
+
fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
|
|
3169
|
+
fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
3170
|
+
ctx.fillStyle = fadeGrad;
|
|
3171
|
+
ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
|
|
3172
|
+
ctx.restore();
|
|
3173
|
+
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
3174
|
+
rafRef.current = requestAnimationFrame(draw);
|
|
3175
|
+
return;
|
|
3176
|
+
}
|
|
3177
|
+
const computedRange = { min: isFinite(globalMin) ? globalMin : 0, max: isFinite(globalMax) ? globalMax : 1 };
|
|
3178
|
+
const adaptiveSpeed = cfg.lerpSpeed + ADAPTIVE_SPEED_BOOST * 0.5;
|
|
3179
|
+
const rangeResult = updateRange(
|
|
3180
|
+
computedRange,
|
|
3181
|
+
rangeInitedRef.current,
|
|
3182
|
+
targetMinRef.current,
|
|
3183
|
+
targetMaxRef.current,
|
|
3184
|
+
displayMinRef.current,
|
|
3185
|
+
displayMaxRef.current,
|
|
3186
|
+
isWindowTransitioning,
|
|
3187
|
+
windowTransProgress,
|
|
3188
|
+
transition,
|
|
3189
|
+
adaptiveSpeed,
|
|
3190
|
+
chartH,
|
|
3191
|
+
pausedDt
|
|
3192
|
+
);
|
|
3193
|
+
rangeInitedRef.current = rangeResult.rangeInited;
|
|
3194
|
+
targetMinRef.current = rangeResult.targetMin;
|
|
3195
|
+
targetMaxRef.current = rangeResult.targetMax;
|
|
3196
|
+
displayMinRef.current = rangeResult.displayMin;
|
|
3197
|
+
displayMaxRef.current = rangeResult.displayMax;
|
|
3198
|
+
const { minVal, maxVal, valRange } = rangeResult;
|
|
3199
|
+
const layout = {
|
|
3200
|
+
w,
|
|
3201
|
+
h,
|
|
3202
|
+
pad,
|
|
3203
|
+
chartW,
|
|
3204
|
+
chartH,
|
|
3205
|
+
leftEdge,
|
|
3206
|
+
rightEdge,
|
|
3207
|
+
minVal,
|
|
3208
|
+
maxVal,
|
|
3209
|
+
valRange,
|
|
3210
|
+
toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
|
|
3211
|
+
toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
|
|
3212
|
+
};
|
|
3213
|
+
const hoverPx = hoverXRef.current;
|
|
3214
|
+
let drawHoverX = null;
|
|
3215
|
+
let drawHoverTime = null;
|
|
3216
|
+
let isActiveHover = false;
|
|
3217
|
+
let hoverEntries = [];
|
|
3218
|
+
if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
|
|
3219
|
+
const maxHoverX = layout.toX(now);
|
|
3220
|
+
const clampedX = Math.min(hoverPx, maxHoverX);
|
|
3221
|
+
const t = leftEdge + (clampedX - pad.left) / chartW * (rightEdge - leftEdge);
|
|
3222
|
+
drawHoverX = clampedX;
|
|
3223
|
+
drawHoverTime = t;
|
|
3224
|
+
isActiveHover = true;
|
|
3225
|
+
for (const entry of seriesEntries) {
|
|
3226
|
+
if ((entry.alpha ?? 1) < 0.5) continue;
|
|
3227
|
+
const v = interpolateAtTime(entry.visible, t);
|
|
3228
|
+
if (v !== null) {
|
|
3229
|
+
hoverEntries.push({ color: entry.palette.line, label: entry.label ?? "", value: v });
|
|
3230
|
+
}
|
|
3231
|
+
}
|
|
3232
|
+
lastHoverRef.current = { x: clampedX, value: hoverEntries[0]?.value ?? 0, time: t };
|
|
3233
|
+
lastHoverEntriesRef.current = hoverEntries;
|
|
3234
|
+
cfg.onHover?.({ time: t, value: hoverEntries[0]?.value ?? 0, x: clampedX, y: layout.toY(hoverEntries[0]?.value ?? 0) });
|
|
3235
|
+
}
|
|
3236
|
+
const scrubTarget = isActiveHover ? 1 : 0;
|
|
3237
|
+
if (noMotion) {
|
|
3238
|
+
scrubAmountRef.current = scrubTarget;
|
|
3239
|
+
} else {
|
|
3240
|
+
scrubAmountRef.current += (scrubTarget - scrubAmountRef.current) * SCRUB_LERP_SPEED;
|
|
3241
|
+
if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
|
|
3242
|
+
if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
|
|
3243
|
+
}
|
|
3244
|
+
if (!isActiveHover && scrubAmountRef.current > 0 && lastHoverRef.current) {
|
|
3245
|
+
drawHoverX = lastHoverRef.current.x;
|
|
3246
|
+
drawHoverTime = lastHoverRef.current.time;
|
|
3247
|
+
hoverEntries = lastHoverEntriesRef.current;
|
|
3248
|
+
}
|
|
3249
|
+
drawMultiFrame(ctx, layout, {
|
|
3250
|
+
series: seriesEntries,
|
|
3251
|
+
now,
|
|
3252
|
+
showGrid: cfg.showGrid,
|
|
3253
|
+
showPulse: cfg.showPulse,
|
|
3254
|
+
referenceLine: cfg.referenceLine,
|
|
3255
|
+
hoverX: drawHoverX,
|
|
3256
|
+
hoverTime: drawHoverTime,
|
|
3257
|
+
hoverEntries,
|
|
3258
|
+
scrubAmount: scrubAmountRef.current,
|
|
3259
|
+
windowSecs,
|
|
3260
|
+
formatValue: cfg.formatValue,
|
|
3261
|
+
formatTime: cfg.formatTime,
|
|
3262
|
+
gridState: gridStateRef.current,
|
|
3263
|
+
timeAxisState: timeAxisStateRef.current,
|
|
3264
|
+
dt,
|
|
3265
|
+
targetWindowSecs: cfg.windowSecs,
|
|
3266
|
+
tooltipY: cfg.tooltipY,
|
|
3267
|
+
tooltipOutline: cfg.tooltipOutline,
|
|
3268
|
+
chartReveal,
|
|
3269
|
+
pauseProgress,
|
|
3270
|
+
now_ms,
|
|
3271
|
+
primaryPalette: cfg.palette
|
|
3272
|
+
});
|
|
3273
|
+
const bgAlpha = 1 - chartReveal;
|
|
3274
|
+
if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
|
|
3275
|
+
const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
|
|
3276
|
+
if (bgEmptyAlpha > 0.01) {
|
|
3277
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
|
|
1679
3278
|
}
|
|
1680
3279
|
}
|
|
1681
|
-
}
|
|
1682
|
-
const smoothValue = displayValueRef.current;
|
|
1683
|
-
const chartW = w - pad.left - pad.right;
|
|
1684
|
-
const needsArrowRoom = cfg.showMomentum;
|
|
1685
|
-
const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
|
|
1686
|
-
const transition = windowTransitionRef.current;
|
|
1687
|
-
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
1688
|
-
const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
1689
|
-
const windowResult = updateWindowTransition(
|
|
1690
|
-
cfg,
|
|
1691
|
-
transition,
|
|
1692
|
-
displayWindowRef.current,
|
|
1693
|
-
displayMinRef.current,
|
|
1694
|
-
displayMaxRef.current,
|
|
1695
|
-
noMotion,
|
|
1696
|
-
now_ms,
|
|
1697
|
-
now,
|
|
1698
|
-
effectivePoints,
|
|
1699
|
-
smoothValue,
|
|
1700
|
-
buffer
|
|
1701
|
-
);
|
|
1702
|
-
displayWindowRef.current = windowResult.windowSecs;
|
|
1703
|
-
const windowSecs = windowResult.windowSecs;
|
|
1704
|
-
const windowTransProgress = windowResult.windowTransProgress;
|
|
1705
|
-
const rightEdge = now + windowSecs * buffer;
|
|
1706
|
-
const leftEdge = rightEdge - windowSecs;
|
|
1707
|
-
const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
|
|
1708
|
-
const visible = [];
|
|
1709
|
-
for (const p of effectivePoints) {
|
|
1710
|
-
if (p.time >= leftEdge - 2 && p.time <= filterRight) {
|
|
1711
|
-
visible.push(p);
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
if (visible.length < 2) {
|
|
1715
3280
|
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
);
|
|
1735
|
-
rangeInitedRef.current = rangeResult.rangeInited;
|
|
1736
|
-
targetMinRef.current = rangeResult.targetMin;
|
|
1737
|
-
targetMaxRef.current = rangeResult.targetMax;
|
|
1738
|
-
displayMinRef.current = rangeResult.displayMin;
|
|
1739
|
-
displayMaxRef.current = rangeResult.displayMax;
|
|
1740
|
-
const { minVal, maxVal, valRange } = rangeResult;
|
|
1741
|
-
const layout = {
|
|
1742
|
-
w,
|
|
1743
|
-
h,
|
|
1744
|
-
pad,
|
|
1745
|
-
chartW,
|
|
1746
|
-
chartH,
|
|
1747
|
-
leftEdge,
|
|
1748
|
-
rightEdge,
|
|
1749
|
-
minVal,
|
|
1750
|
-
maxVal,
|
|
1751
|
-
valRange,
|
|
1752
|
-
toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
|
|
1753
|
-
toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
|
|
1754
|
-
};
|
|
1755
|
-
const momentum = cfg.momentumOverride ?? detectMomentum(visible);
|
|
1756
|
-
const hoverResult = updateHoverState(
|
|
1757
|
-
hoverXRef.current,
|
|
1758
|
-
pad,
|
|
1759
|
-
w,
|
|
1760
|
-
layout,
|
|
1761
|
-
now,
|
|
1762
|
-
visible,
|
|
1763
|
-
scrubAmountRef.current,
|
|
1764
|
-
lastHoverRef.current,
|
|
1765
|
-
cfg,
|
|
1766
|
-
noMotion,
|
|
1767
|
-
leftEdge,
|
|
1768
|
-
rightEdge,
|
|
1769
|
-
chartW,
|
|
1770
|
-
dt
|
|
1771
|
-
);
|
|
1772
|
-
scrubAmountRef.current = hoverResult.scrubAmount;
|
|
1773
|
-
lastHoverRef.current = hoverResult.lastHover;
|
|
1774
|
-
const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
|
|
1775
|
-
const lookback = Math.min(5, visible.length - 1);
|
|
1776
|
-
const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
|
|
1777
|
-
const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
|
|
1778
|
-
drawFrame(ctx, layout, cfg.palette, {
|
|
1779
|
-
visible,
|
|
1780
|
-
smoothValue,
|
|
1781
|
-
now,
|
|
1782
|
-
momentum,
|
|
1783
|
-
arrowState: arrowStateRef.current,
|
|
1784
|
-
showGrid: cfg.showGrid,
|
|
1785
|
-
showMomentum: cfg.showMomentum,
|
|
1786
|
-
showPulse: cfg.showPulse,
|
|
1787
|
-
showFill: cfg.showFill,
|
|
1788
|
-
referenceLine: cfg.referenceLine,
|
|
1789
|
-
hoverX: drawHoverX,
|
|
1790
|
-
hoverValue: drawHoverValue,
|
|
1791
|
-
hoverTime: drawHoverTime,
|
|
1792
|
-
scrubAmount: scrubAmountRef.current,
|
|
1793
|
-
windowSecs,
|
|
1794
|
-
formatValue: cfg.formatValue,
|
|
1795
|
-
formatTime: cfg.formatTime,
|
|
1796
|
-
gridState: gridStateRef.current,
|
|
1797
|
-
timeAxisState: timeAxisStateRef.current,
|
|
1798
|
-
dt,
|
|
1799
|
-
targetWindowSecs: cfg.windowSecs,
|
|
1800
|
-
tooltipY: cfg.tooltipY,
|
|
1801
|
-
tooltipOutline: cfg.tooltipOutline,
|
|
1802
|
-
orderbookData: cfg.orderbookData,
|
|
1803
|
-
orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
|
|
1804
|
-
particleState: cfg.degenOptions ? particleStateRef.current : void 0,
|
|
1805
|
-
particleOptions: cfg.degenOptions,
|
|
1806
|
-
swingMagnitude,
|
|
1807
|
-
shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
|
|
1808
|
-
chartReveal,
|
|
1809
|
-
pauseProgress,
|
|
1810
|
-
now_ms
|
|
1811
|
-
});
|
|
1812
|
-
const bgAlpha = 1 - chartReveal;
|
|
1813
|
-
if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
|
|
1814
|
-
const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
|
|
1815
|
-
if (bgEmptyAlpha > 0.01) {
|
|
1816
|
-
drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
|
|
3281
|
+
} else {
|
|
3282
|
+
const effectivePoints = useStash ? lastDataRef.current : points;
|
|
3283
|
+
const adaptiveSpeed = computeAdaptiveSpeed(
|
|
3284
|
+
cfg.value,
|
|
3285
|
+
displayValueRef.current,
|
|
3286
|
+
displayMinRef.current,
|
|
3287
|
+
displayMaxRef.current,
|
|
3288
|
+
cfg.lerpSpeed,
|
|
3289
|
+
noMotion
|
|
3290
|
+
);
|
|
3291
|
+
if (!useStash) {
|
|
3292
|
+
displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, pausedDt);
|
|
3293
|
+
if (pauseProgress < 0.5) {
|
|
3294
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
3295
|
+
if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
|
|
3296
|
+
displayValueRef.current = cfg.value;
|
|
3297
|
+
}
|
|
3298
|
+
}
|
|
1817
3299
|
}
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
3300
|
+
const smoothValue = displayValueRef.current;
|
|
3301
|
+
const chartW = w - pad.left - pad.right;
|
|
3302
|
+
const needsArrowRoom = cfg.showMomentum;
|
|
3303
|
+
const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
|
|
3304
|
+
const transition = windowTransitionRef.current;
|
|
3305
|
+
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
3306
|
+
const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
3307
|
+
const windowResult = updateWindowTransition(
|
|
1823
3308
|
cfg,
|
|
3309
|
+
transition,
|
|
3310
|
+
displayWindowRef.current,
|
|
3311
|
+
displayMinRef.current,
|
|
3312
|
+
displayMaxRef.current,
|
|
3313
|
+
noMotion,
|
|
3314
|
+
now_ms,
|
|
3315
|
+
now,
|
|
3316
|
+
effectivePoints,
|
|
1824
3317
|
smoothValue,
|
|
1825
|
-
|
|
1826
|
-
|
|
1827
|
-
|
|
1828
|
-
|
|
3318
|
+
buffer
|
|
3319
|
+
);
|
|
3320
|
+
displayWindowRef.current = windowResult.windowSecs;
|
|
3321
|
+
const windowSecs = windowResult.windowSecs;
|
|
3322
|
+
const windowTransProgress = windowResult.windowTransProgress;
|
|
3323
|
+
const rightEdge = now + windowSecs * buffer;
|
|
3324
|
+
const leftEdge = rightEdge - windowSecs;
|
|
3325
|
+
const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
|
|
3326
|
+
const visible = [];
|
|
3327
|
+
for (const p of effectivePoints) {
|
|
3328
|
+
if (p.time >= leftEdge - 2 && p.time <= filterRight) {
|
|
3329
|
+
visible.push(p);
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
if (visible.length < 2) {
|
|
3333
|
+
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
3334
|
+
rafRef.current = requestAnimationFrame(draw);
|
|
3335
|
+
return;
|
|
3336
|
+
}
|
|
3337
|
+
const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
|
|
3338
|
+
const isWindowTransitioning = transition.startMs > 0;
|
|
3339
|
+
const rangeResult = updateRange(
|
|
3340
|
+
computedRange,
|
|
3341
|
+
rangeInitedRef.current,
|
|
3342
|
+
targetMinRef.current,
|
|
3343
|
+
targetMaxRef.current,
|
|
3344
|
+
displayMinRef.current,
|
|
3345
|
+
displayMaxRef.current,
|
|
1829
3346
|
isWindowTransitioning,
|
|
3347
|
+
windowTransProgress,
|
|
3348
|
+
transition,
|
|
3349
|
+
adaptiveSpeed,
|
|
3350
|
+
chartH,
|
|
3351
|
+
pausedDt
|
|
3352
|
+
);
|
|
3353
|
+
rangeInitedRef.current = rangeResult.rangeInited;
|
|
3354
|
+
targetMinRef.current = rangeResult.targetMin;
|
|
3355
|
+
targetMaxRef.current = rangeResult.targetMax;
|
|
3356
|
+
displayMinRef.current = rangeResult.displayMin;
|
|
3357
|
+
displayMaxRef.current = rangeResult.displayMax;
|
|
3358
|
+
const { minVal, maxVal, valRange } = rangeResult;
|
|
3359
|
+
const layout = {
|
|
3360
|
+
w,
|
|
3361
|
+
h,
|
|
3362
|
+
pad,
|
|
3363
|
+
chartW,
|
|
3364
|
+
chartH,
|
|
3365
|
+
leftEdge,
|
|
3366
|
+
rightEdge,
|
|
3367
|
+
minVal,
|
|
3368
|
+
maxVal,
|
|
3369
|
+
valRange,
|
|
3370
|
+
toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
|
|
3371
|
+
toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
|
|
3372
|
+
};
|
|
3373
|
+
const momentum = cfg.momentumOverride ?? detectMomentum(visible);
|
|
3374
|
+
const hoverResult = updateHoverState(
|
|
3375
|
+
hoverXRef.current,
|
|
3376
|
+
pad,
|
|
3377
|
+
w,
|
|
3378
|
+
layout,
|
|
3379
|
+
now,
|
|
3380
|
+
visible,
|
|
3381
|
+
scrubAmountRef.current,
|
|
3382
|
+
lastHoverRef.current,
|
|
3383
|
+
cfg,
|
|
1830
3384
|
noMotion,
|
|
1831
|
-
|
|
1832
|
-
|
|
1833
|
-
|
|
3385
|
+
leftEdge,
|
|
3386
|
+
rightEdge,
|
|
3387
|
+
chartW,
|
|
3388
|
+
dt
|
|
1834
3389
|
);
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
3390
|
+
scrubAmountRef.current = hoverResult.scrubAmount;
|
|
3391
|
+
lastHoverRef.current = hoverResult.lastHover;
|
|
3392
|
+
const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
|
|
3393
|
+
const lookback = Math.min(5, visible.length - 1);
|
|
3394
|
+
const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
|
|
3395
|
+
const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
|
|
3396
|
+
drawFrame(ctx, layout, cfg.palette, {
|
|
3397
|
+
visible,
|
|
3398
|
+
smoothValue,
|
|
3399
|
+
now,
|
|
3400
|
+
momentum,
|
|
3401
|
+
arrowState: arrowStateRef.current,
|
|
3402
|
+
showGrid: cfg.showGrid,
|
|
3403
|
+
showMomentum: cfg.showMomentum,
|
|
3404
|
+
showPulse: cfg.showPulse,
|
|
3405
|
+
showFill: cfg.showFill,
|
|
3406
|
+
referenceLine: cfg.referenceLine,
|
|
3407
|
+
hoverX: drawHoverX,
|
|
3408
|
+
hoverValue: drawHoverValue,
|
|
3409
|
+
hoverTime: drawHoverTime,
|
|
3410
|
+
scrubAmount: scrubAmountRef.current,
|
|
3411
|
+
windowSecs,
|
|
3412
|
+
formatValue: cfg.formatValue,
|
|
3413
|
+
formatTime: cfg.formatTime,
|
|
3414
|
+
gridState: gridStateRef.current,
|
|
3415
|
+
timeAxisState: timeAxisStateRef.current,
|
|
3416
|
+
dt,
|
|
3417
|
+
targetWindowSecs: cfg.windowSecs,
|
|
3418
|
+
tooltipY: cfg.tooltipY,
|
|
3419
|
+
tooltipOutline: cfg.tooltipOutline,
|
|
3420
|
+
orderbookData: cfg.orderbookData,
|
|
3421
|
+
orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
|
|
3422
|
+
particleState: cfg.degenOptions ? particleStateRef.current : void 0,
|
|
3423
|
+
particleOptions: cfg.degenOptions,
|
|
3424
|
+
swingMagnitude,
|
|
3425
|
+
shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
|
|
3426
|
+
chartReveal,
|
|
3427
|
+
pauseProgress,
|
|
3428
|
+
now_ms
|
|
3429
|
+
});
|
|
3430
|
+
const bgAlpha = 1 - chartReveal;
|
|
3431
|
+
if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
|
|
3432
|
+
const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
|
|
3433
|
+
if (bgEmptyAlpha > 0.01) {
|
|
3434
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
|
|
3435
|
+
}
|
|
1838
3436
|
}
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
3437
|
+
const badge = badgeRef.current;
|
|
3438
|
+
if (badge) {
|
|
3439
|
+
badgeYRef.current = updateBadgeDOM(
|
|
3440
|
+
badge,
|
|
3441
|
+
cfg,
|
|
3442
|
+
smoothValue,
|
|
3443
|
+
layout,
|
|
3444
|
+
momentum,
|
|
3445
|
+
badgeYRef.current,
|
|
3446
|
+
badgeColorRef.current,
|
|
3447
|
+
isWindowTransitioning,
|
|
3448
|
+
noMotion,
|
|
3449
|
+
ctx,
|
|
3450
|
+
pausedDt,
|
|
3451
|
+
chartReveal
|
|
3452
|
+
);
|
|
3453
|
+
if (pauseProgress > 0.01 && badge.container.style.display !== "none") {
|
|
3454
|
+
const base = badge.container.style.opacity ? parseFloat(badge.container.style.opacity) : 1;
|
|
3455
|
+
badge.container.style.opacity = String(base * (1 - pauseProgress));
|
|
3456
|
+
}
|
|
3457
|
+
}
|
|
3458
|
+
const valEl = cfg.valueDisplayRef?.current;
|
|
3459
|
+
if (valEl) {
|
|
3460
|
+
const displayVal = cfg.valueMomentumColor ? Math.abs(smoothValue) : smoothValue;
|
|
3461
|
+
valEl.textContent = cfg.formatValue(displayVal);
|
|
3462
|
+
if (cfg.valueMomentumColor) {
|
|
3463
|
+
const mc = momentum === "up" ? "#22c55e" : momentum === "down" ? "#ef4444" : "";
|
|
3464
|
+
if (mc) valEl.style.color = mc;
|
|
3465
|
+
else valEl.style.removeProperty("color");
|
|
3466
|
+
}
|
|
1848
3467
|
}
|
|
1849
3468
|
}
|
|
1850
3469
|
rafRef.current = requestAnimationFrame(draw);
|
|
@@ -1868,6 +3487,7 @@ var defaultFormatTime = (t) => {
|
|
|
1868
3487
|
function Liveline({
|
|
1869
3488
|
data,
|
|
1870
3489
|
value,
|
|
3490
|
+
series: seriesProp,
|
|
1871
3491
|
theme = "dark",
|
|
1872
3492
|
color = "#3b82f6",
|
|
1873
3493
|
window: windowSecs = 30,
|
|
@@ -1899,6 +3519,16 @@ function Liveline({
|
|
|
1899
3519
|
onHover,
|
|
1900
3520
|
cursor = "crosshair",
|
|
1901
3521
|
pulse = true,
|
|
3522
|
+
mode = "line",
|
|
3523
|
+
candles,
|
|
3524
|
+
candleWidth,
|
|
3525
|
+
liveCandle,
|
|
3526
|
+
lineMode,
|
|
3527
|
+
lineData,
|
|
3528
|
+
lineValue,
|
|
3529
|
+
onModeChange,
|
|
3530
|
+
onSeriesToggle,
|
|
3531
|
+
seriesToggleCompact = false,
|
|
1902
3532
|
className,
|
|
1903
3533
|
style
|
|
1904
3534
|
}) {
|
|
@@ -1908,8 +3538,30 @@ function Liveline({
|
|
|
1908
3538
|
const windowBarRef = useRef2(null);
|
|
1909
3539
|
const windowBtnRefs = useRef2(/* @__PURE__ */ new Map());
|
|
1910
3540
|
const [indicatorStyle, setIndicatorStyle] = useState(null);
|
|
3541
|
+
const modeBarRef = useRef2(null);
|
|
3542
|
+
const modeBtnRefs = useRef2(/* @__PURE__ */ new Map());
|
|
3543
|
+
const [modeIndicatorStyle, setModeIndicatorStyle] = useState(null);
|
|
3544
|
+
const [hiddenSeries, setHiddenSeries] = useState(/* @__PURE__ */ new Set());
|
|
3545
|
+
const lastSeriesPropRef = useRef2(seriesProp);
|
|
3546
|
+
if (seriesProp && seriesProp.length > 0) lastSeriesPropRef.current = seriesProp;
|
|
1911
3547
|
const palette = useMemo(() => resolveTheme(color, theme), [color, theme]);
|
|
1912
3548
|
const isDark = theme === "dark";
|
|
3549
|
+
const isMultiSeries = seriesProp != null && seriesProp.length > 0;
|
|
3550
|
+
const showSeriesToggle = (lastSeriesPropRef.current?.length ?? 0) > 1;
|
|
3551
|
+
const seriesPalettes = useMemo(() => {
|
|
3552
|
+
if (!seriesProp || seriesProp.length === 0) return null;
|
|
3553
|
+
return resolveSeriesPalettes(seriesProp, theme);
|
|
3554
|
+
}, [seriesProp, theme]);
|
|
3555
|
+
const multiSeries = useMemo(() => {
|
|
3556
|
+
if (!seriesProp || !seriesPalettes) return void 0;
|
|
3557
|
+
return seriesProp.map((s, i) => ({
|
|
3558
|
+
id: s.id,
|
|
3559
|
+
data: s.data,
|
|
3560
|
+
value: s.value,
|
|
3561
|
+
palette: seriesPalettes.get(s.id) ?? resolveTheme(s.color || SERIES_COLORS[i % SERIES_COLORS.length], theme),
|
|
3562
|
+
label: s.label
|
|
3563
|
+
}));
|
|
3564
|
+
}, [seriesProp, seriesPalettes, theme]);
|
|
1913
3565
|
const showMomentum = momentum !== false;
|
|
1914
3566
|
const momentumOverride = typeof momentum === "string" ? momentum : void 0;
|
|
1915
3567
|
const pad = {
|
|
@@ -1937,6 +3589,36 @@ function Liveline({
|
|
|
1937
3589
|
});
|
|
1938
3590
|
}
|
|
1939
3591
|
}, [activeWindowSecs, windows]);
|
|
3592
|
+
const activeMode = lineMode ? "line" : "candle";
|
|
3593
|
+
useLayoutEffect(() => {
|
|
3594
|
+
if (!onModeChange) return;
|
|
3595
|
+
const btn = modeBtnRefs.current.get(activeMode);
|
|
3596
|
+
const bar = modeBarRef.current;
|
|
3597
|
+
if (btn && bar) {
|
|
3598
|
+
const barRect = bar.getBoundingClientRect();
|
|
3599
|
+
const btnRect = btn.getBoundingClientRect();
|
|
3600
|
+
setModeIndicatorStyle({
|
|
3601
|
+
left: btnRect.left - barRect.left,
|
|
3602
|
+
width: btnRect.width
|
|
3603
|
+
});
|
|
3604
|
+
}
|
|
3605
|
+
}, [activeMode, onModeChange]);
|
|
3606
|
+
const handleSeriesToggle = useCallback2((id) => {
|
|
3607
|
+
setHiddenSeries((prev) => {
|
|
3608
|
+
const next = new Set(prev);
|
|
3609
|
+
if (next.has(id)) {
|
|
3610
|
+
next.delete(id);
|
|
3611
|
+
onSeriesToggle?.(id, true);
|
|
3612
|
+
} else {
|
|
3613
|
+
const totalSeries = seriesProp?.length ?? 0;
|
|
3614
|
+
const visibleCount = totalSeries - next.size;
|
|
3615
|
+
if (visibleCount <= 1) return prev;
|
|
3616
|
+
next.add(id);
|
|
3617
|
+
onSeriesToggle?.(id, false);
|
|
3618
|
+
}
|
|
3619
|
+
return next;
|
|
3620
|
+
});
|
|
3621
|
+
}, [seriesProp?.length, onSeriesToggle]);
|
|
1940
3622
|
const ws = windowStyle ?? "default";
|
|
1941
3623
|
useLivelineEngine(canvasRef, containerRef, {
|
|
1942
3624
|
data,
|
|
@@ -1945,10 +3627,10 @@ function Liveline({
|
|
|
1945
3627
|
windowSecs: effectiveWindowSecs,
|
|
1946
3628
|
lerpSpeed,
|
|
1947
3629
|
showGrid: grid,
|
|
1948
|
-
showBadge: badge,
|
|
1949
|
-
showMomentum,
|
|
3630
|
+
showBadge: isMultiSeries ? false : badge,
|
|
3631
|
+
showMomentum: isMultiSeries ? false : showMomentum,
|
|
1950
3632
|
momentumOverride,
|
|
1951
|
-
showFill: fill,
|
|
3633
|
+
showFill: isMultiSeries ? false : fill,
|
|
1952
3634
|
referenceLine,
|
|
1953
3635
|
formatValue,
|
|
1954
3636
|
formatTime,
|
|
@@ -1957,7 +3639,7 @@ function Liveline({
|
|
|
1957
3639
|
showPulse: pulse,
|
|
1958
3640
|
scrub,
|
|
1959
3641
|
exaggerate,
|
|
1960
|
-
degenOptions,
|
|
3642
|
+
degenOptions: isMultiSeries ? void 0 : degenOptions,
|
|
1961
3643
|
badgeTail,
|
|
1962
3644
|
badgeVariant,
|
|
1963
3645
|
tooltipY,
|
|
@@ -1967,9 +3649,21 @@ function Liveline({
|
|
|
1967
3649
|
orderbookData: orderbook,
|
|
1968
3650
|
loading,
|
|
1969
3651
|
paused,
|
|
1970
|
-
emptyText
|
|
3652
|
+
emptyText,
|
|
3653
|
+
mode,
|
|
3654
|
+
candles,
|
|
3655
|
+
candleWidth,
|
|
3656
|
+
liveCandle,
|
|
3657
|
+
lineMode,
|
|
3658
|
+
lineData,
|
|
3659
|
+
lineValue,
|
|
3660
|
+
multiSeries,
|
|
3661
|
+
isMultiSeries,
|
|
3662
|
+
hiddenSeriesIds: hiddenSeries
|
|
1971
3663
|
});
|
|
1972
3664
|
const cursorStyle = scrub ? cursor : "default";
|
|
3665
|
+
const activeColor = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)";
|
|
3666
|
+
const inactiveColor = isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.22)";
|
|
1973
3667
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
1974
3668
|
showValue && /* @__PURE__ */ jsx(
|
|
1975
3669
|
"span",
|
|
@@ -1989,68 +3683,244 @@ function Liveline({
|
|
|
1989
3683
|
}
|
|
1990
3684
|
}
|
|
1991
3685
|
),
|
|
1992
|
-
windows && windows.length > 0 && /* @__PURE__ */ jsxs(
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2011
|
-
|
|
2012
|
-
|
|
2013
|
-
|
|
2014
|
-
|
|
2015
|
-
|
|
2016
|
-
|
|
2017
|
-
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
3686
|
+
(windows && windows.length > 0 || onModeChange || showSeriesToggle) && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
|
|
3687
|
+
windows && windows.length > 0 && /* @__PURE__ */ jsxs(
|
|
3688
|
+
"div",
|
|
3689
|
+
{
|
|
3690
|
+
ref: windowBarRef,
|
|
3691
|
+
style: {
|
|
3692
|
+
position: "relative",
|
|
3693
|
+
display: "inline-flex",
|
|
3694
|
+
gap: ws === "text" ? 4 : 2,
|
|
3695
|
+
background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3696
|
+
borderRadius: ws === "rounded" ? 999 : 6,
|
|
3697
|
+
padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
|
|
3698
|
+
},
|
|
3699
|
+
children: [
|
|
3700
|
+
ws !== "text" && indicatorStyle && /* @__PURE__ */ jsx("div", { style: {
|
|
3701
|
+
position: "absolute",
|
|
3702
|
+
top: ws === "rounded" ? 3 : 2,
|
|
3703
|
+
left: indicatorStyle.left,
|
|
3704
|
+
width: indicatorStyle.width,
|
|
3705
|
+
height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
|
|
3706
|
+
background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
|
|
3707
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3708
|
+
transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
3709
|
+
pointerEvents: "none"
|
|
3710
|
+
} }),
|
|
3711
|
+
windows.map((w) => {
|
|
3712
|
+
const isActive = w.secs === activeWindowSecs;
|
|
3713
|
+
return /* @__PURE__ */ jsx(
|
|
3714
|
+
"button",
|
|
3715
|
+
{
|
|
3716
|
+
ref: (el) => {
|
|
3717
|
+
if (el) windowBtnRefs.current.set(w.secs, el);
|
|
3718
|
+
else windowBtnRefs.current.delete(w.secs);
|
|
3719
|
+
},
|
|
3720
|
+
onClick: () => {
|
|
3721
|
+
setActiveWindowSecs(w.secs);
|
|
3722
|
+
onWindowChange?.(w.secs);
|
|
3723
|
+
},
|
|
3724
|
+
style: {
|
|
3725
|
+
position: "relative",
|
|
3726
|
+
zIndex: 1,
|
|
3727
|
+
fontSize: 11,
|
|
3728
|
+
padding: ws === "text" ? "2px 6px" : "3px 10px",
|
|
3729
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3730
|
+
border: "none",
|
|
3731
|
+
cursor: "pointer",
|
|
3732
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
3733
|
+
fontWeight: isActive ? 600 : 400,
|
|
3734
|
+
background: "transparent",
|
|
3735
|
+
color: isActive ? activeColor : inactiveColor,
|
|
3736
|
+
transition: "color 0.2s, background 0.15s",
|
|
3737
|
+
lineHeight: "16px"
|
|
3738
|
+
},
|
|
3739
|
+
children: w.label
|
|
3740
|
+
},
|
|
3741
|
+
w.secs
|
|
3742
|
+
);
|
|
3743
|
+
})
|
|
3744
|
+
]
|
|
3745
|
+
}
|
|
3746
|
+
),
|
|
3747
|
+
onModeChange && /* @__PURE__ */ jsxs(
|
|
3748
|
+
"div",
|
|
3749
|
+
{
|
|
3750
|
+
ref: modeBarRef,
|
|
3751
|
+
style: {
|
|
3752
|
+
position: "relative",
|
|
3753
|
+
display: "inline-flex",
|
|
3754
|
+
gap: ws === "text" ? 4 : 2,
|
|
3755
|
+
background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3756
|
+
borderRadius: ws === "rounded" ? 999 : 6,
|
|
3757
|
+
padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
|
|
3758
|
+
},
|
|
3759
|
+
children: [
|
|
3760
|
+
ws !== "text" && modeIndicatorStyle && /* @__PURE__ */ jsx("div", { style: {
|
|
3761
|
+
position: "absolute",
|
|
3762
|
+
top: ws === "rounded" ? 3 : 2,
|
|
3763
|
+
left: modeIndicatorStyle.left,
|
|
3764
|
+
width: modeIndicatorStyle.width,
|
|
3765
|
+
height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
|
|
3766
|
+
background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
|
|
3767
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3768
|
+
transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
3769
|
+
pointerEvents: "none"
|
|
3770
|
+
} }),
|
|
3771
|
+
/* @__PURE__ */ jsx(
|
|
2021
3772
|
"button",
|
|
2022
3773
|
{
|
|
2023
3774
|
ref: (el) => {
|
|
2024
|
-
if (el)
|
|
2025
|
-
else
|
|
3775
|
+
if (el) modeBtnRefs.current.set("line", el);
|
|
3776
|
+
else modeBtnRefs.current.delete("line");
|
|
2026
3777
|
},
|
|
2027
|
-
onClick: () =>
|
|
2028
|
-
|
|
2029
|
-
|
|
3778
|
+
onClick: () => onModeChange("line"),
|
|
3779
|
+
style: {
|
|
3780
|
+
position: "relative",
|
|
3781
|
+
zIndex: 1,
|
|
3782
|
+
padding: "5px 7px",
|
|
3783
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3784
|
+
border: "none",
|
|
3785
|
+
cursor: "pointer",
|
|
3786
|
+
background: "transparent",
|
|
3787
|
+
display: "flex",
|
|
3788
|
+
alignItems: "center"
|
|
2030
3789
|
},
|
|
3790
|
+
children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
|
|
3791
|
+
"path",
|
|
3792
|
+
{
|
|
3793
|
+
d: "M1 8.5C2.5 8.5 3 4 5.5 4S7.5 7 8.5 7C9.5 7 10 3.5 11 3.5",
|
|
3794
|
+
stroke: activeMode === "line" ? activeColor : inactiveColor,
|
|
3795
|
+
strokeWidth: activeMode === "line" ? 1.5 : 1.2,
|
|
3796
|
+
strokeLinecap: "round",
|
|
3797
|
+
fill: "none"
|
|
3798
|
+
}
|
|
3799
|
+
) })
|
|
3800
|
+
}
|
|
3801
|
+
),
|
|
3802
|
+
/* @__PURE__ */ jsx(
|
|
3803
|
+
"button",
|
|
3804
|
+
{
|
|
3805
|
+
ref: (el) => {
|
|
3806
|
+
if (el) modeBtnRefs.current.set("candle", el);
|
|
3807
|
+
else modeBtnRefs.current.delete("candle");
|
|
3808
|
+
},
|
|
3809
|
+
onClick: () => onModeChange("candle"),
|
|
2031
3810
|
style: {
|
|
2032
3811
|
position: "relative",
|
|
2033
3812
|
zIndex: 1,
|
|
2034
|
-
|
|
2035
|
-
padding: ws === "text" ? "2px 6px" : "3px 10px",
|
|
3813
|
+
padding: "5px 7px",
|
|
2036
3814
|
borderRadius: ws === "rounded" ? 999 : 4,
|
|
2037
3815
|
border: "none",
|
|
2038
3816
|
cursor: "pointer",
|
|
2039
|
-
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
2040
|
-
fontWeight: isActive ? 600 : 400,
|
|
2041
3817
|
background: "transparent",
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
lineHeight: "16px"
|
|
3818
|
+
display: "flex",
|
|
3819
|
+
alignItems: "center"
|
|
2045
3820
|
},
|
|
2046
|
-
children:
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
3821
|
+
children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: [
|
|
3822
|
+
/* @__PURE__ */ jsx(
|
|
3823
|
+
"line",
|
|
3824
|
+
{
|
|
3825
|
+
x1: "3.5",
|
|
3826
|
+
y1: "1",
|
|
3827
|
+
x2: "3.5",
|
|
3828
|
+
y2: "11",
|
|
3829
|
+
stroke: activeMode === "candle" ? activeColor : inactiveColor,
|
|
3830
|
+
strokeWidth: "1"
|
|
3831
|
+
}
|
|
3832
|
+
),
|
|
3833
|
+
/* @__PURE__ */ jsx(
|
|
3834
|
+
"rect",
|
|
3835
|
+
{
|
|
3836
|
+
x: "2",
|
|
3837
|
+
y: "3",
|
|
3838
|
+
width: "3",
|
|
3839
|
+
height: "5",
|
|
3840
|
+
rx: "0.5",
|
|
3841
|
+
fill: activeMode === "candle" ? activeColor : inactiveColor
|
|
3842
|
+
}
|
|
3843
|
+
),
|
|
3844
|
+
/* @__PURE__ */ jsx(
|
|
3845
|
+
"line",
|
|
3846
|
+
{
|
|
3847
|
+
x1: "8.5",
|
|
3848
|
+
y1: "2",
|
|
3849
|
+
x2: "8.5",
|
|
3850
|
+
y2: "10",
|
|
3851
|
+
stroke: activeMode === "candle" ? activeColor : inactiveColor,
|
|
3852
|
+
strokeWidth: "1"
|
|
3853
|
+
}
|
|
3854
|
+
),
|
|
3855
|
+
/* @__PURE__ */ jsx(
|
|
3856
|
+
"rect",
|
|
3857
|
+
{
|
|
3858
|
+
x: "7",
|
|
3859
|
+
y: "4",
|
|
3860
|
+
width: "3",
|
|
3861
|
+
height: "4",
|
|
3862
|
+
rx: "0.5",
|
|
3863
|
+
fill: activeMode === "candle" ? activeColor : inactiveColor
|
|
3864
|
+
}
|
|
3865
|
+
)
|
|
3866
|
+
] })
|
|
3867
|
+
}
|
|
3868
|
+
)
|
|
3869
|
+
]
|
|
3870
|
+
}
|
|
3871
|
+
),
|
|
3872
|
+
showSeriesToggle && /* @__PURE__ */ jsx("div", { style: {
|
|
3873
|
+
display: "inline-flex",
|
|
3874
|
+
gap: ws === "text" ? 4 : 2,
|
|
3875
|
+
background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3876
|
+
borderRadius: ws === "rounded" ? 999 : 6,
|
|
3877
|
+
padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
|
|
3878
|
+
opacity: isMultiSeries ? 1 : 0,
|
|
3879
|
+
transition: "opacity 0.4s",
|
|
3880
|
+
pointerEvents: isMultiSeries ? "auto" : "none"
|
|
3881
|
+
}, children: (lastSeriesPropRef.current ?? []).map((s, si) => {
|
|
3882
|
+
const isHidden = hiddenSeries.has(s.id);
|
|
3883
|
+
const seriesColor = s.color || SERIES_COLORS[si % SERIES_COLORS.length];
|
|
3884
|
+
return /* @__PURE__ */ jsxs(
|
|
3885
|
+
"button",
|
|
3886
|
+
{
|
|
3887
|
+
onClick: () => handleSeriesToggle(s.id),
|
|
3888
|
+
style: {
|
|
3889
|
+
position: "relative",
|
|
3890
|
+
zIndex: 1,
|
|
3891
|
+
fontSize: 11,
|
|
3892
|
+
padding: seriesToggleCompact ? ws === "text" ? "2px 4px" : "5px 7px" : ws === "text" ? "2px 6px" : "3px 8px",
|
|
3893
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3894
|
+
border: "none",
|
|
3895
|
+
cursor: "pointer",
|
|
3896
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
3897
|
+
fontWeight: 500,
|
|
3898
|
+
background: isHidden ? "transparent" : ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
|
|
3899
|
+
color: isHidden ? inactiveColor : activeColor,
|
|
3900
|
+
opacity: isHidden ? 0.4 : 1,
|
|
3901
|
+
transition: "opacity 0.2s, background 0.15s, color 0.2s",
|
|
3902
|
+
lineHeight: "16px",
|
|
3903
|
+
display: "flex",
|
|
3904
|
+
alignItems: "center",
|
|
3905
|
+
gap: seriesToggleCompact ? 0 : 4
|
|
3906
|
+
},
|
|
3907
|
+
children: [
|
|
3908
|
+
/* @__PURE__ */ jsx("span", { style: {
|
|
3909
|
+
width: seriesToggleCompact ? 8 : 6,
|
|
3910
|
+
height: seriesToggleCompact ? 8 : 6,
|
|
3911
|
+
borderRadius: "50%",
|
|
3912
|
+
background: seriesColor,
|
|
3913
|
+
flexShrink: 0,
|
|
3914
|
+
opacity: isHidden ? 0.4 : 1,
|
|
3915
|
+
transition: "opacity 0.2s"
|
|
3916
|
+
} }),
|
|
3917
|
+
!seriesToggleCompact && (s.label ?? s.id)
|
|
3918
|
+
]
|
|
3919
|
+
},
|
|
3920
|
+
s.id
|
|
3921
|
+
);
|
|
3922
|
+
}) })
|
|
3923
|
+
] }),
|
|
2054
3924
|
/* @__PURE__ */ jsx(
|
|
2055
3925
|
"div",
|
|
2056
3926
|
{
|
|
@@ -2073,6 +3943,63 @@ function Liveline({
|
|
|
2073
3943
|
)
|
|
2074
3944
|
] });
|
|
2075
3945
|
}
|
|
3946
|
+
|
|
3947
|
+
// src/LivelineTransition.tsx
|
|
3948
|
+
import { useState as useState2, useEffect as useEffect2, useRef as useRef3 } from "react";
|
|
3949
|
+
import { jsx as jsx2 } from "react/jsx-runtime";
|
|
3950
|
+
function LivelineTransition({
|
|
3951
|
+
active,
|
|
3952
|
+
children,
|
|
3953
|
+
duration = 300,
|
|
3954
|
+
className,
|
|
3955
|
+
style
|
|
3956
|
+
}) {
|
|
3957
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
3958
|
+
const [mounted, setMounted] = useState2(() => /* @__PURE__ */ new Set([active]));
|
|
3959
|
+
const [visible, setVisible] = useState2(active);
|
|
3960
|
+
const prevRef = useRef3(active);
|
|
3961
|
+
useEffect2(() => {
|
|
3962
|
+
if (active === prevRef.current) return () => {
|
|
3963
|
+
};
|
|
3964
|
+
const oldKey = prevRef.current;
|
|
3965
|
+
prevRef.current = active;
|
|
3966
|
+
setMounted((prev) => /* @__PURE__ */ new Set([...prev, active]));
|
|
3967
|
+
let raf1 = requestAnimationFrame(() => {
|
|
3968
|
+
raf1 = requestAnimationFrame(() => setVisible(active));
|
|
3969
|
+
});
|
|
3970
|
+
const timer = setTimeout(() => {
|
|
3971
|
+
setMounted((prev) => {
|
|
3972
|
+
const next = new Set(prev);
|
|
3973
|
+
next.delete(oldKey);
|
|
3974
|
+
return next;
|
|
3975
|
+
});
|
|
3976
|
+
}, duration + 50);
|
|
3977
|
+
return () => {
|
|
3978
|
+
cancelAnimationFrame(raf1);
|
|
3979
|
+
clearTimeout(timer);
|
|
3980
|
+
};
|
|
3981
|
+
}, [active, duration]);
|
|
3982
|
+
return /* @__PURE__ */ jsx2("div", { className, style: { position: "relative", width: "100%", height: "100%", ...style }, children: childArray.map((child) => {
|
|
3983
|
+
const key = String(child.key ?? "");
|
|
3984
|
+
if (!mounted.has(key)) return null;
|
|
3985
|
+
const isActive = key === visible;
|
|
3986
|
+
return /* @__PURE__ */ jsx2(
|
|
3987
|
+
"div",
|
|
3988
|
+
{
|
|
3989
|
+
style: {
|
|
3990
|
+
position: "absolute",
|
|
3991
|
+
inset: 0,
|
|
3992
|
+
opacity: isActive ? 1 : 0,
|
|
3993
|
+
transition: `opacity ${duration}ms ease`,
|
|
3994
|
+
pointerEvents: isActive ? "auto" : "none"
|
|
3995
|
+
},
|
|
3996
|
+
children: child
|
|
3997
|
+
},
|
|
3998
|
+
key
|
|
3999
|
+
);
|
|
4000
|
+
}) });
|
|
4001
|
+
}
|
|
2076
4002
|
export {
|
|
2077
|
-
Liveline
|
|
4003
|
+
Liveline,
|
|
4004
|
+
LivelineTransition
|
|
2078
4005
|
};
|