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.cjs
CHANGED
|
@@ -20,7 +20,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
|
|
|
20
20
|
// src/index.ts
|
|
21
21
|
var index_exports = {};
|
|
22
22
|
__export(index_exports, {
|
|
23
|
-
Liveline: () => Liveline
|
|
23
|
+
Liveline: () => Liveline,
|
|
24
|
+
LivelineTransition: () => LivelineTransition
|
|
24
25
|
});
|
|
25
26
|
module.exports = __toCommonJS(index_exports);
|
|
26
27
|
|
|
@@ -87,6 +88,33 @@ function resolveTheme(color, mode) {
|
|
|
87
88
|
badgeFont: '500 11px "SF Mono", Menlo, monospace'
|
|
88
89
|
};
|
|
89
90
|
}
|
|
91
|
+
var SERIES_COLORS = [
|
|
92
|
+
"#3b82f6",
|
|
93
|
+
// blue
|
|
94
|
+
"#ef4444",
|
|
95
|
+
// red
|
|
96
|
+
"#22c55e",
|
|
97
|
+
// green
|
|
98
|
+
"#f59e0b",
|
|
99
|
+
// amber
|
|
100
|
+
"#8b5cf6",
|
|
101
|
+
// violet
|
|
102
|
+
"#ec4899",
|
|
103
|
+
// pink
|
|
104
|
+
"#06b6d4",
|
|
105
|
+
// cyan
|
|
106
|
+
"#f97316"
|
|
107
|
+
// orange
|
|
108
|
+
];
|
|
109
|
+
function resolveSeriesPalettes(series, mode) {
|
|
110
|
+
const map = /* @__PURE__ */ new Map();
|
|
111
|
+
for (let i = 0; i < series.length; i++) {
|
|
112
|
+
const s = series[i];
|
|
113
|
+
const color = s.color || SERIES_COLORS[i % SERIES_COLORS.length];
|
|
114
|
+
map.set(s.id, resolveTheme(color, mode));
|
|
115
|
+
}
|
|
116
|
+
return map;
|
|
117
|
+
}
|
|
90
118
|
|
|
91
119
|
// src/useLivelineEngine.ts
|
|
92
120
|
var import_react = require("react");
|
|
@@ -387,8 +415,9 @@ function renderCurve(ctx, layout, palette, pts, showFill, lineAlpha = 1, fillAlp
|
|
|
387
415
|
ctx.stroke();
|
|
388
416
|
ctx.globalAlpha = baseAlpha;
|
|
389
417
|
}
|
|
390
|
-
function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0) {
|
|
418
|
+
function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0, colorBlend = 1, skipDashLine = false, fillScale = 1) {
|
|
391
419
|
const { h, pad, toX, toY, chartW, chartH } = layout;
|
|
420
|
+
const incomingAlpha = ctx.globalAlpha;
|
|
392
421
|
const yMin = pad.top;
|
|
393
422
|
const yMax = h - pad.bottom;
|
|
394
423
|
const clampY = (y) => Math.max(yMin, Math.min(yMax, y));
|
|
@@ -397,8 +426,10 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
|
|
|
397
426
|
const scroll = now_ms * LOADING_SCROLL_SPEED;
|
|
398
427
|
const morphY = chartReveal < 1 ? (rawY, x) => {
|
|
399
428
|
const t = Math.max(0, Math.min(1, (x - pad.left) / chartW));
|
|
429
|
+
const centerDist = Math.abs(t - 0.5) * 2;
|
|
430
|
+
const localReveal = Math.max(0, Math.min(1, (chartReveal - centerDist * 0.4) / 0.6));
|
|
400
431
|
const baseY = loadingY(t, centerY, amplitude, scroll);
|
|
401
|
-
return baseY + (rawY - baseY) *
|
|
432
|
+
return baseY + (rawY - baseY) * localReveal;
|
|
402
433
|
} : (rawY, _x) => rawY;
|
|
403
434
|
const pts = visible.map((p, i) => {
|
|
404
435
|
const x = toX(p.time);
|
|
@@ -411,13 +442,14 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
|
|
|
411
442
|
pts.push([tipX, morphY(clampY(toY(smoothValue)), tipX)]);
|
|
412
443
|
if (pts.length < 2) return;
|
|
413
444
|
let lineAlpha = 1;
|
|
414
|
-
let fillAlpha =
|
|
445
|
+
let fillAlpha = fillScale;
|
|
415
446
|
if (chartReveal < 1) {
|
|
416
447
|
const breath = loadingBreath(now_ms);
|
|
417
448
|
lineAlpha = breath + (1 - breath) * chartReveal;
|
|
418
|
-
fillAlpha = chartReveal;
|
|
449
|
+
fillAlpha = chartReveal * fillScale;
|
|
419
450
|
}
|
|
420
|
-
const
|
|
451
|
+
const colorT = Math.min(1, chartReveal * 3) * colorBlend;
|
|
452
|
+
const strokeColor = chartReveal < 1 || colorBlend < 1 ? blendColor(palette.gridLabel, palette.line, colorT) : void 0;
|
|
421
453
|
const isScrubbing = scrubX !== null;
|
|
422
454
|
ctx.save();
|
|
423
455
|
ctx.beginPath();
|
|
@@ -434,26 +466,28 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
|
|
|
434
466
|
ctx.beginPath();
|
|
435
467
|
ctx.rect(scrubX, 0, layout.w - scrubX, h);
|
|
436
468
|
ctx.clip();
|
|
437
|
-
ctx.globalAlpha = 1 - scrubAmount * 0.6;
|
|
469
|
+
ctx.globalAlpha = incomingAlpha * (1 - scrubAmount * 0.6);
|
|
438
470
|
renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
|
|
439
471
|
ctx.restore();
|
|
440
472
|
} else {
|
|
441
473
|
renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
|
|
442
474
|
}
|
|
443
475
|
ctx.restore();
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
476
|
+
if (!skipDashLine) {
|
|
477
|
+
const realCurrentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
|
|
478
|
+
const currentY = chartReveal < 1 ? centerY + (realCurrentY - centerY) * chartReveal : realCurrentY;
|
|
479
|
+
ctx.setLineDash([4, 4]);
|
|
480
|
+
ctx.strokeStyle = palette.dashLine;
|
|
481
|
+
ctx.lineWidth = 1;
|
|
482
|
+
const dashBase = isScrubbing ? 1 - scrubAmount * 0.2 : 1;
|
|
483
|
+
ctx.globalAlpha = incomingAlpha * (chartReveal < 1 ? dashBase * chartReveal : dashBase);
|
|
484
|
+
ctx.beginPath();
|
|
485
|
+
ctx.moveTo(pad.left, currentY);
|
|
486
|
+
ctx.lineTo(layout.w - pad.right, currentY);
|
|
487
|
+
ctx.stroke();
|
|
488
|
+
ctx.setLineDash([]);
|
|
489
|
+
}
|
|
490
|
+
ctx.globalAlpha = incomingAlpha;
|
|
457
491
|
const last = pts[pts.length - 1];
|
|
458
492
|
last[1] = Math.max(10, Math.min(h - 10, last[1]));
|
|
459
493
|
return pts;
|
|
@@ -506,6 +540,33 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = per
|
|
|
506
540
|
}
|
|
507
541
|
ctx.fill();
|
|
508
542
|
}
|
|
543
|
+
function drawMultiDot(ctx, x, y, color, pulse = true, now_ms = performance.now(), radius = 3) {
|
|
544
|
+
const baseAlpha = ctx.globalAlpha;
|
|
545
|
+
if (pulse) {
|
|
546
|
+
const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
|
|
547
|
+
if (t < 1) {
|
|
548
|
+
const ringRadius = 9 + t * 10;
|
|
549
|
+
const pulseAlpha = 0.3 * (1 - t);
|
|
550
|
+
ctx.beginPath();
|
|
551
|
+
ctx.arc(x, y, ringRadius, 0, Math.PI * 2);
|
|
552
|
+
ctx.strokeStyle = color;
|
|
553
|
+
ctx.lineWidth = 1.5;
|
|
554
|
+
ctx.globalAlpha = baseAlpha * pulseAlpha;
|
|
555
|
+
ctx.stroke();
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
ctx.globalAlpha = baseAlpha;
|
|
559
|
+
ctx.beginPath();
|
|
560
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
561
|
+
ctx.fillStyle = color;
|
|
562
|
+
ctx.fill();
|
|
563
|
+
}
|
|
564
|
+
function drawSimpleDot(ctx, x, y, color, radius = 3) {
|
|
565
|
+
ctx.beginPath();
|
|
566
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
567
|
+
ctx.fillStyle = color;
|
|
568
|
+
ctx.fill();
|
|
569
|
+
}
|
|
509
570
|
function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
|
|
510
571
|
const baseAlpha = ctx.globalAlpha;
|
|
511
572
|
const upTarget = momentum === "up" ? 1 : 0;
|
|
@@ -604,6 +665,90 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
|
|
|
604
665
|
ctx.fillText(separator + timeText, tx + valueW, ty);
|
|
605
666
|
ctx.restore();
|
|
606
667
|
}
|
|
668
|
+
function drawMultiCrosshair(ctx, layout, palette, hoverX, hoverTime, entries, formatValue, formatTime, scrubOpacity, tooltipY, tooltipOutline, liveDotX) {
|
|
669
|
+
if (scrubOpacity < 0.01 || entries.length === 0) return;
|
|
670
|
+
const { h, pad, toY } = layout;
|
|
671
|
+
ctx.save();
|
|
672
|
+
ctx.globalAlpha = scrubOpacity * 0.5;
|
|
673
|
+
ctx.strokeStyle = palette.crosshairLine;
|
|
674
|
+
ctx.lineWidth = 1;
|
|
675
|
+
ctx.beginPath();
|
|
676
|
+
ctx.moveTo(hoverX, pad.top);
|
|
677
|
+
ctx.lineTo(hoverX, h - pad.bottom);
|
|
678
|
+
ctx.stroke();
|
|
679
|
+
ctx.restore();
|
|
680
|
+
const dotRadius = 4 * Math.min(scrubOpacity * 3, 1);
|
|
681
|
+
if (dotRadius > 0.5) {
|
|
682
|
+
ctx.globalAlpha = 1;
|
|
683
|
+
for (const entry of entries) {
|
|
684
|
+
const y = toY(entry.value);
|
|
685
|
+
ctx.beginPath();
|
|
686
|
+
ctx.arc(hoverX, y, dotRadius, 0, Math.PI * 2);
|
|
687
|
+
ctx.fillStyle = entry.color;
|
|
688
|
+
ctx.fill();
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
if (scrubOpacity < 0.1 || layout.w < 300) return;
|
|
692
|
+
ctx.save();
|
|
693
|
+
ctx.globalAlpha = scrubOpacity;
|
|
694
|
+
ctx.font = '400 13px "SF Mono", Menlo, monospace';
|
|
695
|
+
ctx.textAlign = "left";
|
|
696
|
+
const timeText = formatTime(hoverTime);
|
|
697
|
+
const sep = " \xB7 ";
|
|
698
|
+
const dotInline = " ";
|
|
699
|
+
const segments = [
|
|
700
|
+
{ text: timeText, color: palette.gridLabel }
|
|
701
|
+
];
|
|
702
|
+
for (const e of entries) {
|
|
703
|
+
segments.push({ text: sep, color: palette.gridLabel });
|
|
704
|
+
segments.push({ text: dotInline, color: e.color, isDot: true });
|
|
705
|
+
const label = e.label ? `${e.label} ` : "";
|
|
706
|
+
if (label) segments.push({ text: label, color: palette.gridLabel });
|
|
707
|
+
segments.push({ text: formatValue(e.value), color: palette.tooltipText });
|
|
708
|
+
}
|
|
709
|
+
let totalW = 0;
|
|
710
|
+
const segWidths = [];
|
|
711
|
+
for (const seg of segments) {
|
|
712
|
+
const w = seg.isDot ? 12 : ctx.measureText(seg.text).width;
|
|
713
|
+
segWidths.push(w);
|
|
714
|
+
totalW += w;
|
|
715
|
+
}
|
|
716
|
+
let tx = hoverX - totalW / 2;
|
|
717
|
+
const minX = pad.left + 4;
|
|
718
|
+
const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
|
|
719
|
+
const maxX = dotRightEdge - totalW;
|
|
720
|
+
if (tx < minX) tx = minX;
|
|
721
|
+
if (tx > maxX) tx = maxX;
|
|
722
|
+
const ty = pad.top + (tooltipY ?? 14) + 10;
|
|
723
|
+
if (tooltipOutline !== false) {
|
|
724
|
+
ctx.strokeStyle = palette.tooltipBg;
|
|
725
|
+
ctx.lineWidth = 3;
|
|
726
|
+
ctx.lineJoin = "round";
|
|
727
|
+
let ox2 = tx;
|
|
728
|
+
for (let i = 0; i < segments.length; i++) {
|
|
729
|
+
const seg = segments[i];
|
|
730
|
+
if (!seg.isDot) {
|
|
731
|
+
ctx.strokeText(seg.text, ox2, ty);
|
|
732
|
+
}
|
|
733
|
+
ox2 += segWidths[i];
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
let ox = tx;
|
|
737
|
+
for (let i = 0; i < segments.length; i++) {
|
|
738
|
+
const seg = segments[i];
|
|
739
|
+
if (seg.isDot) {
|
|
740
|
+
ctx.beginPath();
|
|
741
|
+
ctx.arc(ox + 4, ty - 4, 3.5, 0, Math.PI * 2);
|
|
742
|
+
ctx.fillStyle = seg.color;
|
|
743
|
+
ctx.fill();
|
|
744
|
+
} else {
|
|
745
|
+
ctx.fillStyle = seg.color;
|
|
746
|
+
ctx.fillText(seg.text, ox, ty);
|
|
747
|
+
}
|
|
748
|
+
ox += segWidths[i];
|
|
749
|
+
}
|
|
750
|
+
ctx.restore();
|
|
751
|
+
}
|
|
607
752
|
|
|
608
753
|
// src/draw/referenceLine.ts
|
|
609
754
|
function drawReferenceLine(ctx, layout, palette, ref) {
|
|
@@ -970,6 +1115,337 @@ function drawParticles(ctx, state, dt) {
|
|
|
970
1115
|
ctx.restore();
|
|
971
1116
|
}
|
|
972
1117
|
|
|
1118
|
+
// src/draw/candlestick.ts
|
|
1119
|
+
var BULL = "#22c55e";
|
|
1120
|
+
var BEAR = "#ef4444";
|
|
1121
|
+
var BULL_RGB = [34, 197, 94];
|
|
1122
|
+
var BEAR_RGB = [239, 68, 68];
|
|
1123
|
+
function blendColor2(t) {
|
|
1124
|
+
const r = Math.round(BEAR_RGB[0] + (BULL_RGB[0] - BEAR_RGB[0]) * t);
|
|
1125
|
+
const g = Math.round(BEAR_RGB[1] + (BULL_RGB[1] - BEAR_RGB[1]) * t);
|
|
1126
|
+
const b = Math.round(BEAR_RGB[2] + (BULL_RGB[2] - BEAR_RGB[2]) * t);
|
|
1127
|
+
return `rgb(${r},${g},${b})`;
|
|
1128
|
+
}
|
|
1129
|
+
function parseRgb(color) {
|
|
1130
|
+
const hex = color.match(/^#([0-9a-f]{6})$/i);
|
|
1131
|
+
if (hex) {
|
|
1132
|
+
const h = hex[1];
|
|
1133
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
1134
|
+
}
|
|
1135
|
+
const rgb = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
1136
|
+
if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
|
|
1137
|
+
return [128, 128, 128];
|
|
1138
|
+
}
|
|
1139
|
+
function blendToAccent(candleColor, accentColor, t) {
|
|
1140
|
+
if (t <= 0) return candleColor;
|
|
1141
|
+
if (t >= 1) return accentColor;
|
|
1142
|
+
const [r1, g1, b1] = parseRgb(candleColor);
|
|
1143
|
+
const [r2, g2, b2] = parseRgb(accentColor);
|
|
1144
|
+
const r = Math.round(r1 + (r2 - r1) * t);
|
|
1145
|
+
const g = Math.round(g1 + (g2 - g1) * t);
|
|
1146
|
+
const b = Math.round(b1 + (b2 - b1) * t);
|
|
1147
|
+
return `rgb(${r},${g},${b})`;
|
|
1148
|
+
}
|
|
1149
|
+
function candleDims(layout, candleWidthSecs) {
|
|
1150
|
+
const pxPerSec = layout.chartW / (layout.rightEdge - layout.leftEdge);
|
|
1151
|
+
const candlePxW = candleWidthSecs * pxPerSec;
|
|
1152
|
+
const bodyW = Math.max(1, candlePxW * 0.7);
|
|
1153
|
+
const wickW = Math.max(0.8, Math.min(2, bodyW * 0.15));
|
|
1154
|
+
const radius = bodyW > 6 ? 1.5 : 0;
|
|
1155
|
+
return { bodyW, wickW, radius };
|
|
1156
|
+
}
|
|
1157
|
+
function roundedRect(ctx, x, y, w, h, r) {
|
|
1158
|
+
if (r <= 0 || h < r * 2) {
|
|
1159
|
+
ctx.rect(x, y, w, h);
|
|
1160
|
+
return;
|
|
1161
|
+
}
|
|
1162
|
+
ctx.moveTo(x + r, y);
|
|
1163
|
+
ctx.lineTo(x + w - r, y);
|
|
1164
|
+
ctx.arcTo(x + w, y, x + w, y + r, r);
|
|
1165
|
+
ctx.lineTo(x + w, y + h - r);
|
|
1166
|
+
ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
|
|
1167
|
+
ctx.lineTo(x + r, y + h);
|
|
1168
|
+
ctx.arcTo(x, y + h, x, y + h - r, r);
|
|
1169
|
+
ctx.lineTo(x, y + r);
|
|
1170
|
+
ctx.arcTo(x, y, x + r, y, r);
|
|
1171
|
+
ctx.closePath();
|
|
1172
|
+
}
|
|
1173
|
+
function drawCandlesticks(ctx, layout, candles, candleWidthSecs, liveTime, now_ms, scrubX, scrubDim, liveAlpha = 1, liveBullBlend = -1, accentColor, accentBlend = 0) {
|
|
1174
|
+
if (candles.length === 0) return;
|
|
1175
|
+
const { toX, toY } = layout;
|
|
1176
|
+
const { bodyW, wickW, radius } = candleDims(layout, candleWidthSecs);
|
|
1177
|
+
const halfBody = bodyW / 2;
|
|
1178
|
+
const padL = layout.pad.left;
|
|
1179
|
+
const padR = layout.pad.left + layout.chartW;
|
|
1180
|
+
const livePulse = 0.12 + Math.sin(now_ms * 4e-3) * 0.08;
|
|
1181
|
+
for (const c of candles) {
|
|
1182
|
+
const cx = toX(c.time + candleWidthSecs / 2);
|
|
1183
|
+
if (cx + halfBody < padL || cx - halfBody > padR) continue;
|
|
1184
|
+
const isBull = c.close >= c.open;
|
|
1185
|
+
const isLive = c.time === liveTime;
|
|
1186
|
+
let color = isLive && liveBullBlend >= 0 ? blendColor2(liveBullBlend) : isBull ? BULL : BEAR;
|
|
1187
|
+
if (accentColor && accentBlend > 0.01) {
|
|
1188
|
+
color = blendToAccent(color, accentColor, accentBlend);
|
|
1189
|
+
}
|
|
1190
|
+
let candleAlpha = isLive ? liveAlpha : 1;
|
|
1191
|
+
if (scrubDim > 0.01 && scrubX > 0) {
|
|
1192
|
+
const dist = cx - scrubX;
|
|
1193
|
+
if (dist > 0) {
|
|
1194
|
+
const fadeZone = bodyW * 1.5;
|
|
1195
|
+
const dimT = Math.min(dist / fadeZone, 1);
|
|
1196
|
+
candleAlpha *= 1 - scrubDim * 0.5 * dimT;
|
|
1197
|
+
}
|
|
1198
|
+
}
|
|
1199
|
+
const baseAlpha = ctx.globalAlpha;
|
|
1200
|
+
ctx.globalAlpha = baseAlpha * candleAlpha;
|
|
1201
|
+
const bodyTop = toY(Math.max(c.open, c.close));
|
|
1202
|
+
const bodyBottom = toY(Math.min(c.open, c.close));
|
|
1203
|
+
const bodyH = Math.max(1, bodyBottom - bodyTop);
|
|
1204
|
+
const wickTop = toY(c.high);
|
|
1205
|
+
const wickBottom = toY(c.low);
|
|
1206
|
+
ctx.lineCap = "round";
|
|
1207
|
+
ctx.strokeStyle = color;
|
|
1208
|
+
if (bodyTop - wickTop > 0.5) {
|
|
1209
|
+
ctx.beginPath();
|
|
1210
|
+
ctx.moveTo(cx, bodyTop);
|
|
1211
|
+
ctx.lineTo(cx, wickTop);
|
|
1212
|
+
ctx.lineWidth = wickW;
|
|
1213
|
+
ctx.stroke();
|
|
1214
|
+
}
|
|
1215
|
+
if (wickBottom - bodyBottom > 0.5) {
|
|
1216
|
+
ctx.beginPath();
|
|
1217
|
+
ctx.moveTo(cx, bodyBottom);
|
|
1218
|
+
ctx.lineTo(cx, wickBottom);
|
|
1219
|
+
ctx.lineWidth = wickW;
|
|
1220
|
+
ctx.stroke();
|
|
1221
|
+
}
|
|
1222
|
+
ctx.fillStyle = color;
|
|
1223
|
+
ctx.beginPath();
|
|
1224
|
+
roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
|
|
1225
|
+
ctx.fill();
|
|
1226
|
+
if (isLive) {
|
|
1227
|
+
ctx.save();
|
|
1228
|
+
ctx.globalAlpha = baseAlpha * candleAlpha * livePulse;
|
|
1229
|
+
ctx.shadowColor = color;
|
|
1230
|
+
ctx.shadowBlur = 8;
|
|
1231
|
+
ctx.fillStyle = color;
|
|
1232
|
+
ctx.beginPath();
|
|
1233
|
+
roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
|
|
1234
|
+
ctx.fill();
|
|
1235
|
+
ctx.restore();
|
|
1236
|
+
}
|
|
1237
|
+
ctx.globalAlpha = baseAlpha;
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
function drawClosePrice(ctx, layout, palette, liveCandle, scrubDim, bullBlend = -1) {
|
|
1241
|
+
const y = layout.toY(liveCandle.close);
|
|
1242
|
+
if (y < layout.pad.top || y > layout.h - layout.pad.bottom) return;
|
|
1243
|
+
const isBull = liveCandle.close >= liveCandle.open;
|
|
1244
|
+
const color = bullBlend >= 0 ? blendColor2(bullBlend) : isBull ? BULL : BEAR;
|
|
1245
|
+
const baseAlpha = ctx.globalAlpha;
|
|
1246
|
+
ctx.save();
|
|
1247
|
+
ctx.setLineDash([4, 4]);
|
|
1248
|
+
ctx.strokeStyle = color;
|
|
1249
|
+
ctx.lineWidth = 1;
|
|
1250
|
+
ctx.globalAlpha = baseAlpha * (1 - scrubDim * 0.3) * 0.4;
|
|
1251
|
+
ctx.beginPath();
|
|
1252
|
+
ctx.moveTo(layout.pad.left, y);
|
|
1253
|
+
ctx.lineTo(layout.w - layout.pad.right, y);
|
|
1254
|
+
ctx.stroke();
|
|
1255
|
+
ctx.setLineDash([]);
|
|
1256
|
+
ctx.restore();
|
|
1257
|
+
}
|
|
1258
|
+
function drawCandleCrosshair(ctx, layout, palette, hoverX, candle, hoverTime, formatValue, formatTime, opacity) {
|
|
1259
|
+
if (opacity < 0.01) return;
|
|
1260
|
+
const { h, pad } = layout;
|
|
1261
|
+
ctx.save();
|
|
1262
|
+
ctx.globalAlpha = opacity * 0.5;
|
|
1263
|
+
ctx.strokeStyle = palette.crosshairLine;
|
|
1264
|
+
ctx.lineWidth = 1;
|
|
1265
|
+
ctx.beginPath();
|
|
1266
|
+
ctx.moveTo(hoverX, pad.top);
|
|
1267
|
+
ctx.lineTo(hoverX, h - pad.bottom);
|
|
1268
|
+
ctx.stroke();
|
|
1269
|
+
ctx.restore();
|
|
1270
|
+
if (opacity < 0.1 || layout.w < 200) return;
|
|
1271
|
+
const isBull = candle.close >= candle.open;
|
|
1272
|
+
const valueColor = isBull ? BULL : BEAR;
|
|
1273
|
+
const cl = formatValue(candle.close);
|
|
1274
|
+
const time = formatTime(hoverTime);
|
|
1275
|
+
ctx.save();
|
|
1276
|
+
ctx.globalAlpha = opacity;
|
|
1277
|
+
ctx.font = '400 13px "SF Mono", Menlo, monospace';
|
|
1278
|
+
ctx.textAlign = "left";
|
|
1279
|
+
let parts;
|
|
1280
|
+
if (layout.w >= 400) {
|
|
1281
|
+
const o = formatValue(candle.open);
|
|
1282
|
+
const hi = formatValue(candle.high);
|
|
1283
|
+
const lo = formatValue(candle.low);
|
|
1284
|
+
parts = [
|
|
1285
|
+
{ text: "O ", color: palette.gridLabel },
|
|
1286
|
+
{ text: o, color: valueColor },
|
|
1287
|
+
{ text: " H ", color: palette.gridLabel },
|
|
1288
|
+
{ text: hi, color: valueColor },
|
|
1289
|
+
{ text: " L ", color: palette.gridLabel },
|
|
1290
|
+
{ text: lo, color: valueColor },
|
|
1291
|
+
{ text: " C ", color: palette.gridLabel },
|
|
1292
|
+
{ text: cl, color: valueColor },
|
|
1293
|
+
{ text: " \xB7 ", color: palette.gridLabel },
|
|
1294
|
+
{ text: time, color: palette.gridLabel }
|
|
1295
|
+
];
|
|
1296
|
+
} else {
|
|
1297
|
+
parts = [
|
|
1298
|
+
{ text: "C ", color: palette.gridLabel },
|
|
1299
|
+
{ text: cl, color: valueColor },
|
|
1300
|
+
{ text: " \xB7 ", color: palette.gridLabel },
|
|
1301
|
+
{ text: time, color: palette.gridLabel }
|
|
1302
|
+
];
|
|
1303
|
+
}
|
|
1304
|
+
let totalW = 0;
|
|
1305
|
+
const widths = [];
|
|
1306
|
+
for (const p of parts) {
|
|
1307
|
+
const w = ctx.measureText(p.text).width;
|
|
1308
|
+
widths.push(w);
|
|
1309
|
+
totalW += w;
|
|
1310
|
+
}
|
|
1311
|
+
let tx = hoverX - totalW / 2;
|
|
1312
|
+
const minX = pad.left + 4;
|
|
1313
|
+
const maxX = layout.w - pad.right - totalW;
|
|
1314
|
+
if (tx < minX) tx = minX;
|
|
1315
|
+
if (tx > maxX) tx = maxX;
|
|
1316
|
+
const ty = pad.top + 24;
|
|
1317
|
+
ctx.strokeStyle = palette.tooltipBg;
|
|
1318
|
+
ctx.lineWidth = 3;
|
|
1319
|
+
ctx.lineJoin = "round";
|
|
1320
|
+
let cx = tx;
|
|
1321
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1322
|
+
ctx.strokeText(parts[i].text, cx, ty);
|
|
1323
|
+
cx += widths[i];
|
|
1324
|
+
}
|
|
1325
|
+
cx = tx;
|
|
1326
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1327
|
+
ctx.fillStyle = parts[i].color;
|
|
1328
|
+
ctx.fillText(parts[i].text, cx, ty);
|
|
1329
|
+
cx += widths[i];
|
|
1330
|
+
}
|
|
1331
|
+
ctx.restore();
|
|
1332
|
+
}
|
|
1333
|
+
function drawLineModeCrosshair(ctx, layout, palette, hoverX, value, hoverTime, formatValue, formatTime, opacity) {
|
|
1334
|
+
if (opacity < 0.01) return;
|
|
1335
|
+
const { h, pad } = layout;
|
|
1336
|
+
const y = layout.toY(value);
|
|
1337
|
+
ctx.save();
|
|
1338
|
+
ctx.globalAlpha = opacity * 0.5;
|
|
1339
|
+
ctx.strokeStyle = palette.crosshairLine;
|
|
1340
|
+
ctx.lineWidth = 1;
|
|
1341
|
+
ctx.beginPath();
|
|
1342
|
+
ctx.moveTo(hoverX, pad.top);
|
|
1343
|
+
ctx.lineTo(hoverX, h - pad.bottom);
|
|
1344
|
+
ctx.stroke();
|
|
1345
|
+
ctx.globalAlpha = opacity * 0.3;
|
|
1346
|
+
ctx.beginPath();
|
|
1347
|
+
ctx.moveTo(pad.left, y);
|
|
1348
|
+
ctx.lineTo(layout.w - pad.right, y);
|
|
1349
|
+
ctx.stroke();
|
|
1350
|
+
ctx.restore();
|
|
1351
|
+
if (opacity < 0.1 || layout.w < 200) return;
|
|
1352
|
+
const val = formatValue(value);
|
|
1353
|
+
const time = formatTime(hoverTime);
|
|
1354
|
+
ctx.save();
|
|
1355
|
+
ctx.globalAlpha = opacity;
|
|
1356
|
+
ctx.font = '400 13px "SF Mono", Menlo, monospace';
|
|
1357
|
+
ctx.textAlign = "left";
|
|
1358
|
+
const parts = [
|
|
1359
|
+
{ text: val, color: palette.line },
|
|
1360
|
+
{ text: " \xB7 ", color: palette.gridLabel },
|
|
1361
|
+
{ text: time, color: palette.gridLabel }
|
|
1362
|
+
];
|
|
1363
|
+
let totalW = 0;
|
|
1364
|
+
const widths = [];
|
|
1365
|
+
for (const p of parts) {
|
|
1366
|
+
const w = ctx.measureText(p.text).width;
|
|
1367
|
+
widths.push(w);
|
|
1368
|
+
totalW += w;
|
|
1369
|
+
}
|
|
1370
|
+
let tx = hoverX - totalW / 2;
|
|
1371
|
+
const minX = pad.left + 4;
|
|
1372
|
+
const maxX = layout.w - pad.right - totalW;
|
|
1373
|
+
if (tx < minX) tx = minX;
|
|
1374
|
+
if (tx > maxX) tx = maxX;
|
|
1375
|
+
const ty = pad.top + 24;
|
|
1376
|
+
ctx.strokeStyle = palette.tooltipBg;
|
|
1377
|
+
ctx.lineWidth = 3;
|
|
1378
|
+
ctx.lineJoin = "round";
|
|
1379
|
+
let lx = tx;
|
|
1380
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1381
|
+
ctx.strokeText(parts[i].text, lx, ty);
|
|
1382
|
+
lx += widths[i];
|
|
1383
|
+
}
|
|
1384
|
+
lx = tx;
|
|
1385
|
+
for (let i = 0; i < parts.length; i++) {
|
|
1386
|
+
ctx.fillStyle = parts[i].color;
|
|
1387
|
+
ctx.fillText(parts[i].text, lx, ty);
|
|
1388
|
+
lx += widths[i];
|
|
1389
|
+
}
|
|
1390
|
+
ctx.restore();
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1393
|
+
// src/draw/empty.ts
|
|
1394
|
+
function drawEmpty(ctx, w, h, pad, palette, alpha = 1, now_ms = 0, skipLine = false, emptyText) {
|
|
1395
|
+
const chartW = w - pad.left - pad.right;
|
|
1396
|
+
const chartH = h - pad.top - pad.bottom;
|
|
1397
|
+
const centerY = pad.top + chartH / 2;
|
|
1398
|
+
const cx = pad.left + chartW / 2;
|
|
1399
|
+
const text = emptyText ?? "No data to display";
|
|
1400
|
+
const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
|
|
1401
|
+
ctx.save();
|
|
1402
|
+
ctx.font = "400 12px system-ui, -apple-system, sans-serif";
|
|
1403
|
+
const textW = ctx.measureText(text).width;
|
|
1404
|
+
const gapHalf = textW / 2 + 20;
|
|
1405
|
+
const fadeW = 30;
|
|
1406
|
+
if (!skipLine) {
|
|
1407
|
+
const scroll = now_ms * LOADING_SCROLL_SPEED;
|
|
1408
|
+
const breath = loadingBreath(now_ms);
|
|
1409
|
+
const numPts = 32;
|
|
1410
|
+
const pts = [];
|
|
1411
|
+
for (let i = 0; i <= numPts; i++) {
|
|
1412
|
+
const t = i / numPts;
|
|
1413
|
+
const x = pad.left + t * chartW;
|
|
1414
|
+
const y = loadingY(t, centerY, amplitude, scroll);
|
|
1415
|
+
pts.push([x, y]);
|
|
1416
|
+
}
|
|
1417
|
+
ctx.beginPath();
|
|
1418
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
1419
|
+
drawSpline(ctx, pts);
|
|
1420
|
+
ctx.strokeStyle = palette.gridLabel;
|
|
1421
|
+
ctx.lineWidth = palette.lineWidth;
|
|
1422
|
+
ctx.globalAlpha = breath * alpha;
|
|
1423
|
+
ctx.lineCap = "round";
|
|
1424
|
+
ctx.lineJoin = "round";
|
|
1425
|
+
ctx.stroke();
|
|
1426
|
+
}
|
|
1427
|
+
ctx.save();
|
|
1428
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
1429
|
+
const gapLeft = cx - gapHalf - fadeW;
|
|
1430
|
+
const gapRight = cx + gapHalf + fadeW;
|
|
1431
|
+
const eraseGrad = ctx.createLinearGradient(gapLeft, 0, gapRight, 0);
|
|
1432
|
+
eraseGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
1433
|
+
eraseGrad.addColorStop(fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
|
|
1434
|
+
eraseGrad.addColorStop(1 - fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
|
|
1435
|
+
eraseGrad.addColorStop(1, "rgba(0,0,0,0)");
|
|
1436
|
+
ctx.fillStyle = eraseGrad;
|
|
1437
|
+
ctx.globalAlpha = alpha;
|
|
1438
|
+
const eraseH = amplitude * 2 + palette.lineWidth + 6;
|
|
1439
|
+
ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
|
|
1440
|
+
ctx.restore();
|
|
1441
|
+
ctx.textAlign = "center";
|
|
1442
|
+
ctx.textBaseline = "middle";
|
|
1443
|
+
ctx.globalAlpha = 0.35 * alpha;
|
|
1444
|
+
ctx.fillStyle = palette.gridLabel;
|
|
1445
|
+
ctx.fillText(text, cx, centerY);
|
|
1446
|
+
ctx.restore();
|
|
1447
|
+
}
|
|
1448
|
+
|
|
973
1449
|
// src/draw/index.ts
|
|
974
1450
|
var SHAKE_DECAY_RATE = 2e-3;
|
|
975
1451
|
var SHAKE_MIN_AMPLITUDE = 0.2;
|
|
@@ -1119,91 +1595,352 @@ function drawFrame(ctx, layout, palette, opts) {
|
|
|
1119
1595
|
ctx.restore();
|
|
1120
1596
|
}
|
|
1121
1597
|
}
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
const
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
for (let i = 0; i <= numPts; i++) {
|
|
1135
|
-
const t = i / numPts;
|
|
1136
|
-
const x = leftX + t * chartW;
|
|
1137
|
-
const y = loadingY(t, centerY, amplitude, scroll);
|
|
1138
|
-
pts.push([x, y]);
|
|
1598
|
+
function drawMultiFrame(ctx, layout, opts) {
|
|
1599
|
+
const palette = opts.primaryPalette;
|
|
1600
|
+
const reveal = opts.chartReveal;
|
|
1601
|
+
const revealRamp = (start, end) => {
|
|
1602
|
+
const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
|
|
1603
|
+
return t * t * (3 - 2 * t);
|
|
1604
|
+
};
|
|
1605
|
+
if (opts.referenceLine && reveal > 0.01) {
|
|
1606
|
+
ctx.save();
|
|
1607
|
+
if (reveal < 1) ctx.globalAlpha = reveal;
|
|
1608
|
+
drawReferenceLine(ctx, layout, palette, opts.referenceLine);
|
|
1609
|
+
ctx.restore();
|
|
1139
1610
|
}
|
|
1140
|
-
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1152
|
-
|
|
1153
|
-
|
|
1611
|
+
if (opts.showGrid) {
|
|
1612
|
+
const gridAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
|
|
1613
|
+
if (gridAlpha > 0.01) {
|
|
1614
|
+
ctx.save();
|
|
1615
|
+
if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
|
|
1616
|
+
drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
|
|
1617
|
+
ctx.restore();
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
|
|
1621
|
+
const allPts = [];
|
|
1622
|
+
for (let si = 0; si < opts.series.length; si++) {
|
|
1623
|
+
const s = opts.series[si];
|
|
1624
|
+
const seriesAlpha = s.alpha ?? 1;
|
|
1625
|
+
const secondaryFade = si > 0 && reveal < 1 ? Math.min(1, reveal * 2) : 1;
|
|
1626
|
+
const combinedAlpha = secondaryFade * seriesAlpha;
|
|
1627
|
+
if (combinedAlpha < 0.01) continue;
|
|
1628
|
+
ctx.save();
|
|
1629
|
+
if (combinedAlpha < 1) ctx.globalAlpha = combinedAlpha;
|
|
1630
|
+
const pts = drawLine(
|
|
1631
|
+
ctx,
|
|
1632
|
+
layout,
|
|
1633
|
+
s.palette,
|
|
1634
|
+
s.visible,
|
|
1635
|
+
s.smoothValue,
|
|
1636
|
+
opts.now,
|
|
1637
|
+
false,
|
|
1638
|
+
// no fill
|
|
1639
|
+
scrubX,
|
|
1640
|
+
opts.scrubAmount,
|
|
1641
|
+
reveal,
|
|
1642
|
+
opts.now_ms
|
|
1643
|
+
);
|
|
1644
|
+
ctx.restore();
|
|
1645
|
+
if (pts && pts.length > 0) {
|
|
1646
|
+
allPts.push({ pts, palette: s.palette, label: s.label, alpha: seriesAlpha });
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1649
|
+
{
|
|
1650
|
+
const timeAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
|
|
1651
|
+
if (timeAlpha > 0.01) {
|
|
1652
|
+
ctx.save();
|
|
1653
|
+
if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
|
|
1654
|
+
drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
|
|
1655
|
+
ctx.restore();
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
if (reveal > 0.3 && allPts.length > 0) {
|
|
1659
|
+
const dotAlpha = (reveal - 0.3) / 0.7;
|
|
1660
|
+
const showPulse = opts.showPulse && reveal > 0.6 && opts.pauseProgress < 0.5;
|
|
1661
|
+
for (const entry of allPts) {
|
|
1662
|
+
if (entry.alpha < 0.01) continue;
|
|
1663
|
+
const lastPt = entry.pts[entry.pts.length - 1];
|
|
1664
|
+
ctx.save();
|
|
1665
|
+
ctx.globalAlpha = dotAlpha * entry.alpha;
|
|
1666
|
+
if (showPulse && entry.alpha > 0.5) {
|
|
1667
|
+
drawMultiDot(ctx, lastPt[0], lastPt[1], entry.palette.line, true, opts.now_ms, 3);
|
|
1668
|
+
} else {
|
|
1669
|
+
drawSimpleDot(ctx, lastPt[0], lastPt[1], entry.palette.line, 3);
|
|
1670
|
+
}
|
|
1671
|
+
if (entry.label) {
|
|
1672
|
+
ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
|
|
1673
|
+
ctx.textAlign = "left";
|
|
1674
|
+
ctx.fillStyle = entry.palette.line;
|
|
1675
|
+
ctx.fillText(entry.label, lastPt[0] + 6, lastPt[1] + 3.5);
|
|
1676
|
+
}
|
|
1677
|
+
ctx.restore();
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
ctx.save();
|
|
1681
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
1682
|
+
const fadeGrad = ctx.createLinearGradient(layout.pad.left, 0, layout.pad.left + FADE_EDGE_WIDTH, 0);
|
|
1683
|
+
fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
|
|
1684
|
+
fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
1685
|
+
ctx.fillStyle = fadeGrad;
|
|
1686
|
+
ctx.fillRect(0, 0, layout.pad.left + FADE_EDGE_WIDTH, layout.h);
|
|
1687
|
+
ctx.restore();
|
|
1688
|
+
if (opts.hoverX !== null && opts.hoverTime !== null && opts.hoverEntries.length > 0 && allPts.length > 0 && opts.scrubAmount > 0.01) {
|
|
1689
|
+
let maxLiveDotX = 0;
|
|
1690
|
+
for (const entry of allPts) {
|
|
1691
|
+
if (entry.alpha < 0.01) continue;
|
|
1692
|
+
const lastX = entry.pts[entry.pts.length - 1][0];
|
|
1693
|
+
if (lastX > maxLiveDotX) maxLiveDotX = lastX;
|
|
1694
|
+
}
|
|
1695
|
+
const distToLive = maxLiveDotX - opts.hoverX;
|
|
1696
|
+
const fadeStart = Math.min(80, layout.chartW * 0.3);
|
|
1697
|
+
const scrubOpacity = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
|
|
1698
|
+
if (scrubOpacity > 0.01) {
|
|
1699
|
+
drawMultiCrosshair(
|
|
1700
|
+
ctx,
|
|
1701
|
+
layout,
|
|
1702
|
+
palette,
|
|
1703
|
+
opts.hoverX,
|
|
1704
|
+
opts.hoverTime,
|
|
1705
|
+
opts.hoverEntries,
|
|
1706
|
+
opts.formatValue,
|
|
1707
|
+
opts.formatTime,
|
|
1708
|
+
scrubOpacity,
|
|
1709
|
+
opts.tooltipY,
|
|
1710
|
+
opts.tooltipOutline,
|
|
1711
|
+
maxLiveDotX
|
|
1712
|
+
);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
}
|
|
1716
|
+
function drawCandleFrame(ctx, layout, palette, opts) {
|
|
1717
|
+
const { w, h, pad, chartW, chartH } = layout;
|
|
1718
|
+
const reveal = opts.chartReveal;
|
|
1719
|
+
const fullLineMode = opts.lineModeProg >= 0.99;
|
|
1720
|
+
const revealLine = fullLineMode ? 1 - reveal : (1 - reveal) * (1 - reveal) * (1 - reveal);
|
|
1721
|
+
const lp = Math.max(opts.lineModeProg, revealLine);
|
|
1722
|
+
const colorBlend = lp > 1e-3 ? opts.lineModeProg / lp : 1;
|
|
1723
|
+
const revealRamp = (start, end) => {
|
|
1724
|
+
const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
|
|
1725
|
+
return t * t * (3 - 2 * t);
|
|
1726
|
+
};
|
|
1727
|
+
const gridAlpha = revealRamp(0.25, 0.6);
|
|
1728
|
+
if (opts.showGrid && gridAlpha > 0.01) {
|
|
1729
|
+
ctx.save();
|
|
1730
|
+
if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
|
|
1731
|
+
drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
|
|
1732
|
+
ctx.restore();
|
|
1733
|
+
}
|
|
1734
|
+
let linePts;
|
|
1735
|
+
if (lp > 0.01 && opts.lineVisible.length >= 2) {
|
|
1736
|
+
const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
|
|
1737
|
+
ctx.save();
|
|
1738
|
+
ctx.globalAlpha = lp;
|
|
1739
|
+
linePts = drawLine(
|
|
1740
|
+
ctx,
|
|
1741
|
+
layout,
|
|
1742
|
+
palette,
|
|
1743
|
+
opts.lineVisible,
|
|
1744
|
+
opts.lineSmoothValue,
|
|
1745
|
+
opts.now,
|
|
1746
|
+
opts.lineModeProg > 0.01,
|
|
1747
|
+
scrubX,
|
|
1748
|
+
opts.scrubAmount,
|
|
1749
|
+
opts.chartReveal,
|
|
1750
|
+
opts.now_ms,
|
|
1751
|
+
colorBlend,
|
|
1752
|
+
!fullLineMode,
|
|
1753
|
+
opts.lineModeProg
|
|
1754
|
+
// fillScale — fill fades smoothly with line mode transition
|
|
1755
|
+
);
|
|
1756
|
+
ctx.restore();
|
|
1757
|
+
}
|
|
1758
|
+
const closeAlpha = revealRamp(0.4, 0.8);
|
|
1759
|
+
const closeSource = opts.closePriceCandle ?? opts.liveCandle;
|
|
1760
|
+
if (closeSource && closeAlpha > 0.01) {
|
|
1761
|
+
if (lp < 0.99) {
|
|
1762
|
+
ctx.save();
|
|
1763
|
+
ctx.globalAlpha = closeAlpha * (1 - lp);
|
|
1764
|
+
drawClosePrice(ctx, layout, palette, closeSource, opts.scrubAmount, opts.liveBullBlend);
|
|
1765
|
+
ctx.restore();
|
|
1766
|
+
}
|
|
1767
|
+
if (lp > 0.01 && !fullLineMode) {
|
|
1768
|
+
const dashY = layout.toY(closeSource.close);
|
|
1769
|
+
if (dashY >= pad.top && dashY <= h - pad.bottom) {
|
|
1770
|
+
ctx.save();
|
|
1771
|
+
ctx.setLineDash([4, 4]);
|
|
1772
|
+
ctx.strokeStyle = palette.dashLine;
|
|
1773
|
+
ctx.lineWidth = 1;
|
|
1774
|
+
ctx.globalAlpha = closeAlpha * lp * (1 - opts.scrubAmount * 0.2);
|
|
1775
|
+
ctx.beginPath();
|
|
1776
|
+
ctx.moveTo(pad.left, dashY);
|
|
1777
|
+
ctx.lineTo(w - pad.right, dashY);
|
|
1778
|
+
ctx.stroke();
|
|
1779
|
+
ctx.setLineDash([]);
|
|
1780
|
+
ctx.restore();
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
const candleAlpha = opts.chartReveal * (1 - lp);
|
|
1785
|
+
if (candleAlpha > 0.01) {
|
|
1786
|
+
const ohlcScale = reveal * reveal * (3 - 2 * reveal);
|
|
1787
|
+
const collapseC = (c) => ohlcScale >= 0.99 ? c : {
|
|
1788
|
+
time: c.time,
|
|
1789
|
+
open: c.close + (c.open - c.close) * ohlcScale,
|
|
1790
|
+
high: c.close + (c.high - c.close) * ohlcScale,
|
|
1791
|
+
low: c.close + (c.low - c.close) * ohlcScale,
|
|
1792
|
+
close: c.close
|
|
1793
|
+
};
|
|
1794
|
+
const revealCandles = ohlcScale < 0.99 ? opts.candles.map(collapseC) : opts.candles;
|
|
1795
|
+
const revealOld = ohlcScale < 0.99 && opts.oldCandles.length > 0 ? opts.oldCandles.map(collapseC) : opts.oldCandles;
|
|
1796
|
+
ctx.save();
|
|
1797
|
+
ctx.beginPath();
|
|
1798
|
+
ctx.rect(pad.left - 1, pad.top, chartW + 2, chartH);
|
|
1799
|
+
ctx.clip();
|
|
1800
|
+
const accentCol = lp > 0.01 ? palette.line : void 0;
|
|
1801
|
+
if (opts.morphT >= 0 && revealOld.length > 0) {
|
|
1802
|
+
ctx.globalAlpha = (1 - opts.morphT) * candleAlpha;
|
|
1803
|
+
drawCandlesticks(
|
|
1804
|
+
ctx,
|
|
1805
|
+
layout,
|
|
1806
|
+
revealOld,
|
|
1807
|
+
opts.oldWidth,
|
|
1808
|
+
-1,
|
|
1809
|
+
opts.now_ms,
|
|
1810
|
+
opts.hoverX ?? 0,
|
|
1811
|
+
opts.scrubAmount,
|
|
1812
|
+
1,
|
|
1813
|
+
-1,
|
|
1814
|
+
accentCol,
|
|
1815
|
+
lp
|
|
1816
|
+
);
|
|
1817
|
+
ctx.globalAlpha = opts.morphT * candleAlpha;
|
|
1818
|
+
drawCandlesticks(
|
|
1819
|
+
ctx,
|
|
1820
|
+
layout,
|
|
1821
|
+
revealCandles,
|
|
1822
|
+
opts.displayCandleWidth,
|
|
1823
|
+
opts.liveCandle?.time ?? -1,
|
|
1824
|
+
opts.now_ms,
|
|
1825
|
+
opts.hoverX ?? 0,
|
|
1826
|
+
opts.scrubAmount,
|
|
1827
|
+
opts.liveBirthAlpha,
|
|
1828
|
+
opts.liveBullBlend,
|
|
1829
|
+
accentCol,
|
|
1830
|
+
lp
|
|
1831
|
+
);
|
|
1832
|
+
ctx.globalAlpha = 1;
|
|
1833
|
+
} else {
|
|
1834
|
+
if (candleAlpha < 1) ctx.globalAlpha = candleAlpha;
|
|
1835
|
+
drawCandlesticks(
|
|
1836
|
+
ctx,
|
|
1837
|
+
layout,
|
|
1838
|
+
revealCandles,
|
|
1839
|
+
opts.displayCandleWidth,
|
|
1840
|
+
opts.liveCandle?.time ?? -1,
|
|
1841
|
+
opts.now_ms,
|
|
1842
|
+
opts.hoverX ?? 0,
|
|
1843
|
+
opts.scrubAmount,
|
|
1844
|
+
opts.liveBirthAlpha,
|
|
1845
|
+
opts.liveBullBlend,
|
|
1846
|
+
accentCol,
|
|
1847
|
+
lp
|
|
1848
|
+
);
|
|
1849
|
+
}
|
|
1850
|
+
ctx.restore();
|
|
1851
|
+
}
|
|
1852
|
+
if (lp > 0.5 && linePts && linePts.length > 0 && reveal > 0.3) {
|
|
1853
|
+
const lastPt = linePts[linePts.length - 1];
|
|
1854
|
+
const dotAlpha = (lp - 0.5) * 2 * ((reveal - 0.3) / 0.7);
|
|
1855
|
+
const showPulse = lp > 0.8 && reveal > 0.6;
|
|
1856
|
+
if (dotAlpha > 0.01) {
|
|
1857
|
+
ctx.save();
|
|
1858
|
+
ctx.globalAlpha = dotAlpha;
|
|
1859
|
+
drawDot(ctx, lastPt[0], lastPt[1], palette, showPulse, opts.scrubAmount, opts.now_ms);
|
|
1860
|
+
ctx.restore();
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
const timeAlpha = revealRamp(0.25, 0.6);
|
|
1864
|
+
if (timeAlpha > 0.01) {
|
|
1865
|
+
ctx.save();
|
|
1866
|
+
if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
|
|
1867
|
+
drawTimeAxis(ctx, layout, palette, opts.targetWindowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
|
|
1868
|
+
ctx.restore();
|
|
1869
|
+
}
|
|
1870
|
+
ctx.save();
|
|
1871
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
1872
|
+
const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
|
|
1873
|
+
fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
|
|
1874
|
+
fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
1875
|
+
ctx.fillStyle = fadeGrad;
|
|
1876
|
+
ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
|
|
1877
|
+
ctx.restore();
|
|
1878
|
+
if (opts.showEmptyOverlay) {
|
|
1879
|
+
const bgAlpha = 1 - opts.chartReveal;
|
|
1880
|
+
if (bgAlpha > 0.01) {
|
|
1881
|
+
const bgEmptyAlpha = (1 - opts.loadingAlpha) * bgAlpha;
|
|
1882
|
+
if (bgEmptyAlpha > 0.01) {
|
|
1883
|
+
drawEmpty(ctx, w, h, pad, palette, bgEmptyAlpha, opts.now_ms, true, opts.emptyText);
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
if (opts.chartReveal > 0.7 && opts.hoveredCandle && opts.hoverX !== null && opts.scrubAmount > 0.01) {
|
|
1888
|
+
if (opts.lineModeProg > 0.5) {
|
|
1889
|
+
drawLineModeCrosshair(
|
|
1890
|
+
ctx,
|
|
1891
|
+
layout,
|
|
1892
|
+
palette,
|
|
1893
|
+
opts.hoverX,
|
|
1894
|
+
opts.hoveredCandle.close,
|
|
1895
|
+
opts.hoverTime ?? 0,
|
|
1896
|
+
opts.formatValue,
|
|
1897
|
+
opts.formatTime,
|
|
1898
|
+
opts.scrubAmount
|
|
1899
|
+
);
|
|
1900
|
+
} else {
|
|
1901
|
+
drawCandleCrosshair(
|
|
1902
|
+
ctx,
|
|
1903
|
+
layout,
|
|
1904
|
+
palette,
|
|
1905
|
+
opts.hoverX,
|
|
1906
|
+
opts.hoveredCandle,
|
|
1907
|
+
opts.hoverTime ?? 0,
|
|
1908
|
+
opts.formatValue,
|
|
1909
|
+
opts.formatTime,
|
|
1910
|
+
opts.scrubAmount
|
|
1911
|
+
);
|
|
1912
|
+
}
|
|
1913
|
+
}
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
// src/draw/loading.ts
|
|
1917
|
+
function drawLoading(ctx, w, h, pad, palette, now_ms, alpha = 1, strokeColor) {
|
|
1154
1918
|
const chartW = w - pad.left - pad.right;
|
|
1155
1919
|
const chartH = h - pad.top - pad.bottom;
|
|
1156
1920
|
const centerY = pad.top + chartH / 2;
|
|
1157
|
-
const
|
|
1158
|
-
const text = emptyText ?? "No data to display";
|
|
1159
|
-
ctx.font = "400 12px system-ui, -apple-system, sans-serif";
|
|
1921
|
+
const leftX = pad.left;
|
|
1160
1922
|
const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
|
|
1161
|
-
const
|
|
1162
|
-
const
|
|
1163
|
-
const
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
const
|
|
1167
|
-
const
|
|
1168
|
-
const
|
|
1169
|
-
|
|
1170
|
-
const t = i / numPts;
|
|
1171
|
-
const x = pad.left + t * chartW;
|
|
1172
|
-
const y = loadingY(t, centerY, amplitude, scroll);
|
|
1173
|
-
pts.push([x, y]);
|
|
1174
|
-
}
|
|
1175
|
-
ctx.beginPath();
|
|
1176
|
-
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
1177
|
-
drawSpline(ctx, pts);
|
|
1178
|
-
ctx.strokeStyle = palette.gridLabel;
|
|
1179
|
-
ctx.lineWidth = palette.lineWidth;
|
|
1180
|
-
ctx.globalAlpha = breath * alpha;
|
|
1181
|
-
ctx.lineCap = "round";
|
|
1182
|
-
ctx.lineJoin = "round";
|
|
1183
|
-
ctx.stroke();
|
|
1923
|
+
const scroll = now_ms * LOADING_SCROLL_SPEED;
|
|
1924
|
+
const breath = loadingBreath(now_ms);
|
|
1925
|
+
const numPts = 32;
|
|
1926
|
+
const pts = [];
|
|
1927
|
+
for (let i = 0; i <= numPts; i++) {
|
|
1928
|
+
const t = i / numPts;
|
|
1929
|
+
const x = leftX + t * chartW;
|
|
1930
|
+
const y = loadingY(t, centerY, amplitude, scroll);
|
|
1931
|
+
pts.push([x, y]);
|
|
1184
1932
|
}
|
|
1185
1933
|
ctx.save();
|
|
1186
|
-
ctx.
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
ctx.
|
|
1195
|
-
ctx.globalAlpha = alpha;
|
|
1196
|
-
const eraseH = amplitude * 2 + palette.lineWidth + 6;
|
|
1197
|
-
ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
|
|
1934
|
+
ctx.beginPath();
|
|
1935
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
1936
|
+
drawSpline(ctx, pts);
|
|
1937
|
+
ctx.strokeStyle = strokeColor ?? palette.line;
|
|
1938
|
+
ctx.lineWidth = palette.lineWidth;
|
|
1939
|
+
ctx.globalAlpha = breath * alpha;
|
|
1940
|
+
ctx.lineCap = "round";
|
|
1941
|
+
ctx.lineJoin = "round";
|
|
1942
|
+
ctx.stroke();
|
|
1198
1943
|
ctx.restore();
|
|
1199
|
-
ctx.textAlign = "center";
|
|
1200
|
-
ctx.textBaseline = "middle";
|
|
1201
|
-
ctx.globalAlpha = 0.35 * alpha;
|
|
1202
|
-
ctx.fillStyle = palette.gridLabel;
|
|
1203
|
-
ctx.fillText(text, cx, centerY);
|
|
1204
|
-
ctx.globalAlpha = 1;
|
|
1205
|
-
ctx.textAlign = "start";
|
|
1206
|
-
ctx.textBaseline = "alphabetic";
|
|
1207
1944
|
}
|
|
1208
1945
|
|
|
1209
1946
|
// src/draw/badge.ts
|
|
@@ -1253,10 +1990,23 @@ var ADAPTIVE_SPEED_BOOST = 0.2;
|
|
|
1253
1990
|
var MOMENTUM_GREEN = [34, 197, 94];
|
|
1254
1991
|
var MOMENTUM_RED = [239, 68, 68];
|
|
1255
1992
|
var CHART_REVEAL_SPEED = 0.14;
|
|
1993
|
+
var CHART_REVEAL_SPEED_FWD = 0.09;
|
|
1256
1994
|
var PAUSE_PROGRESS_SPEED = 0.12;
|
|
1257
1995
|
var PAUSE_CATCHUP_SPEED = 0.08;
|
|
1258
1996
|
var PAUSE_CATCHUP_SPEED_FAST = 0.22;
|
|
1259
1997
|
var LOADING_ALPHA_SPEED = 0.14;
|
|
1998
|
+
var SERIES_TOGGLE_SPEED = 0.1;
|
|
1999
|
+
var CANDLE_LERP_SPEED = 0.25;
|
|
2000
|
+
var CANDLE_WIDTH_TRANS_MS = 300;
|
|
2001
|
+
var LINE_MORPH_MS = 500;
|
|
2002
|
+
var CLOSE_LINE_LERP_SPEED = 0.25;
|
|
2003
|
+
var LINE_DENSITY_MS = 350;
|
|
2004
|
+
var LINE_LERP_BASE = 0.08;
|
|
2005
|
+
var LINE_ADAPTIVE_BOOST = 0.2;
|
|
2006
|
+
var LINE_SNAP_THRESHOLD = 1e-3;
|
|
2007
|
+
var RANGE_LERP_SPEED = 0.15;
|
|
2008
|
+
var RANGE_ADAPTIVE_BOOST = 0.2;
|
|
2009
|
+
var CANDLE_BUFFER = 0.05;
|
|
1260
2010
|
function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
|
|
1261
2011
|
const valGap = Math.abs(value - displayValue);
|
|
1262
2012
|
const prevRange = displayMax - displayMin || 1;
|
|
@@ -1456,10 +2206,121 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
|
|
|
1456
2206
|
}
|
|
1457
2207
|
return badgeY;
|
|
1458
2208
|
}
|
|
2209
|
+
function computeCandleRange(candles) {
|
|
2210
|
+
let min = Infinity;
|
|
2211
|
+
let max = -Infinity;
|
|
2212
|
+
for (const c of candles) {
|
|
2213
|
+
if (c.low < min) min = c.low;
|
|
2214
|
+
if (c.high > max) max = c.high;
|
|
2215
|
+
}
|
|
2216
|
+
if (!isFinite(min) || !isFinite(max)) return { min: 99, max: 101 };
|
|
2217
|
+
const range = max - min;
|
|
2218
|
+
const margin = range * 0.12;
|
|
2219
|
+
const minRange = range * 0.1 || 0.4;
|
|
2220
|
+
if (range < minRange) {
|
|
2221
|
+
const mid = (min + max) / 2;
|
|
2222
|
+
return { min: mid - minRange / 2, max: mid + minRange / 2 };
|
|
2223
|
+
}
|
|
2224
|
+
return { min: min - margin, max: max + margin };
|
|
2225
|
+
}
|
|
2226
|
+
function candleAtX(candles, hoverX, candleWidth, layout) {
|
|
2227
|
+
const time = layout.leftEdge + (hoverX - layout.pad.left) / layout.chartW * (layout.rightEdge - layout.leftEdge);
|
|
2228
|
+
let lo = 0;
|
|
2229
|
+
let hi = candles.length - 1;
|
|
2230
|
+
while (lo <= hi) {
|
|
2231
|
+
const mid = lo + hi >> 1;
|
|
2232
|
+
const c = candles[mid];
|
|
2233
|
+
if (time < c.time) hi = mid - 1;
|
|
2234
|
+
else if (time >= c.time + candleWidth) lo = mid + 1;
|
|
2235
|
+
else return c;
|
|
2236
|
+
}
|
|
2237
|
+
return null;
|
|
2238
|
+
}
|
|
2239
|
+
function updateCandleRange(computedRange, rangeInited, displayMin, displayMax, isTransitioning, windowTransProgress, wt, chartH, dt) {
|
|
2240
|
+
if (!rangeInited) {
|
|
2241
|
+
return {
|
|
2242
|
+
minVal: computedRange.min,
|
|
2243
|
+
maxVal: computedRange.max,
|
|
2244
|
+
valRange: computedRange.max - computedRange.min || 1e-3,
|
|
2245
|
+
displayMin: computedRange.min,
|
|
2246
|
+
displayMax: computedRange.max,
|
|
2247
|
+
rangeInited: true
|
|
2248
|
+
};
|
|
2249
|
+
}
|
|
2250
|
+
if (isTransitioning) {
|
|
2251
|
+
displayMin = wt.rangeFromMin + (wt.rangeToMin - wt.rangeFromMin) * windowTransProgress;
|
|
2252
|
+
displayMax = wt.rangeFromMax + (wt.rangeToMax - wt.rangeFromMax) * windowTransProgress;
|
|
2253
|
+
} else {
|
|
2254
|
+
const curRange = displayMax - displayMin || 1;
|
|
2255
|
+
const gapMin = Math.abs(displayMin - computedRange.min);
|
|
2256
|
+
const gapMax = Math.abs(displayMax - computedRange.max);
|
|
2257
|
+
const gapRatio = Math.min((gapMin + gapMax) / curRange, 1);
|
|
2258
|
+
const speed = RANGE_LERP_SPEED + (1 - gapRatio) * RANGE_ADAPTIVE_BOOST;
|
|
2259
|
+
displayMin = lerp(displayMin, computedRange.min, speed, dt);
|
|
2260
|
+
displayMax = lerp(displayMax, computedRange.max, speed, dt);
|
|
2261
|
+
const pxThreshold = 0.5 * curRange / chartH || 1e-3;
|
|
2262
|
+
if (Math.abs(displayMin - computedRange.min) < pxThreshold) displayMin = computedRange.min;
|
|
2263
|
+
if (Math.abs(displayMax - computedRange.max) < pxThreshold) displayMax = computedRange.max;
|
|
2264
|
+
}
|
|
2265
|
+
return {
|
|
2266
|
+
minVal: displayMin,
|
|
2267
|
+
maxVal: displayMax,
|
|
2268
|
+
valRange: displayMax - displayMin || 1e-3,
|
|
2269
|
+
displayMin,
|
|
2270
|
+
displayMax,
|
|
2271
|
+
rangeInited: true
|
|
2272
|
+
};
|
|
2273
|
+
}
|
|
2274
|
+
function updateCandleWindowTransition(targetWindowSecs, wt, displayWindow, displayMin, displayMax, now_ms, now, candles, liveCandle, candleWidth, buffer) {
|
|
2275
|
+
if (wt.to !== targetWindowSecs) {
|
|
2276
|
+
wt.from = displayWindow;
|
|
2277
|
+
wt.to = targetWindowSecs;
|
|
2278
|
+
wt.startMs = now_ms;
|
|
2279
|
+
wt.rangeFromMin = displayMin;
|
|
2280
|
+
wt.rangeFromMax = displayMax;
|
|
2281
|
+
const targetRightEdge = now + targetWindowSecs * buffer;
|
|
2282
|
+
const targetLeftEdge = targetRightEdge - targetWindowSecs;
|
|
2283
|
+
const targetVisible = [];
|
|
2284
|
+
for (const c of candles) {
|
|
2285
|
+
if (c.time + candleWidth >= targetLeftEdge && c.time <= targetRightEdge) {
|
|
2286
|
+
targetVisible.push(c);
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2289
|
+
if (liveCandle && liveCandle.time + candleWidth >= targetLeftEdge && liveCandle.time <= targetRightEdge) {
|
|
2290
|
+
targetVisible.push(liveCandle);
|
|
2291
|
+
}
|
|
2292
|
+
if (targetVisible.length > 0) {
|
|
2293
|
+
const tr = computeCandleRange(targetVisible);
|
|
2294
|
+
wt.rangeToMin = tr.min;
|
|
2295
|
+
wt.rangeToMax = tr.max;
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
let windowTransProgress = 0;
|
|
2299
|
+
let resultWindow;
|
|
2300
|
+
if (wt.startMs === 0) {
|
|
2301
|
+
resultWindow = targetWindowSecs;
|
|
2302
|
+
} else {
|
|
2303
|
+
const elapsed = now_ms - wt.startMs;
|
|
2304
|
+
const t = Math.min(elapsed / WINDOW_TRANSITION_MS, 1);
|
|
2305
|
+
const eased = (1 - Math.cos(t * Math.PI)) / 2;
|
|
2306
|
+
windowTransProgress = eased;
|
|
2307
|
+
const logFrom = Math.log(wt.from);
|
|
2308
|
+
const logTo = Math.log(wt.to);
|
|
2309
|
+
resultWindow = Math.exp(logFrom + (logTo - logFrom) * eased);
|
|
2310
|
+
if (t >= 1) {
|
|
2311
|
+
resultWindow = targetWindowSecs;
|
|
2312
|
+
wt.startMs = 0;
|
|
2313
|
+
windowTransProgress = 0;
|
|
2314
|
+
}
|
|
2315
|
+
}
|
|
2316
|
+
return { windowSecs: resultWindow, windowTransProgress };
|
|
2317
|
+
}
|
|
1459
2318
|
function useLivelineEngine(canvasRef, containerRef, config) {
|
|
1460
2319
|
const configRef = (0, import_react.useRef)(config);
|
|
1461
2320
|
configRef.current = config;
|
|
1462
2321
|
const displayValueRef = (0, import_react.useRef)(config.value);
|
|
2322
|
+
const displayValuesRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
2323
|
+
const seriesAlphaRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
1463
2324
|
const displayMinRef = (0, import_react.useRef)(0);
|
|
1464
2325
|
const displayMaxRef = (0, import_react.useRef)(0);
|
|
1465
2326
|
const targetMinRef = (0, import_react.useRef)(0);
|
|
@@ -1492,13 +2353,49 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1492
2353
|
const hoverXRef = (0, import_react.useRef)(null);
|
|
1493
2354
|
const scrubAmountRef = (0, import_react.useRef)(0);
|
|
1494
2355
|
const lastHoverRef = (0, import_react.useRef)(null);
|
|
2356
|
+
const lastHoverEntriesRef = (0, import_react.useRef)([]);
|
|
1495
2357
|
const chartRevealRef = (0, import_react.useRef)(0);
|
|
1496
2358
|
const pauseProgressRef = (0, import_react.useRef)(0);
|
|
1497
2359
|
const timeDebtRef = (0, import_react.useRef)(0);
|
|
1498
2360
|
const lastDataRef = (0, import_react.useRef)([]);
|
|
2361
|
+
const lastMultiSeriesRef = (0, import_react.useRef)([]);
|
|
1499
2362
|
const frozenNowRef = (0, import_react.useRef)(0);
|
|
1500
2363
|
const pausedDataRef = (0, import_react.useRef)(null);
|
|
2364
|
+
const pausedMultiDataRef = (0, import_react.useRef)(null);
|
|
1501
2365
|
const loadingAlphaRef = (0, import_react.useRef)(config.loading ? 1 : 0);
|
|
2366
|
+
const displayCandleRef = (0, import_react.useRef)(null);
|
|
2367
|
+
const liveBirthAlphaRef = (0, import_react.useRef)(1);
|
|
2368
|
+
const liveBullRef = (0, import_react.useRef)(0.5);
|
|
2369
|
+
const lineSmoothCloseRef = (0, import_react.useRef)(0);
|
|
2370
|
+
const lineSmoothInitedRef = (0, import_react.useRef)(false);
|
|
2371
|
+
const closeLineSmoothRef = (0, import_react.useRef)(0);
|
|
2372
|
+
const closeLineSmoothInitedRef = (0, import_react.useRef)(false);
|
|
2373
|
+
const lineModeProgRef = (0, import_react.useRef)(0);
|
|
2374
|
+
const lineModeTransRef = (0, import_react.useRef)({ startMs: 0, from: 0, to: 0 });
|
|
2375
|
+
const lineDensityProgRef = (0, import_react.useRef)(0);
|
|
2376
|
+
const lineDensityTransRef = (0, import_react.useRef)({ startMs: 0, from: 0, to: 0 });
|
|
2377
|
+
const lineTickSmoothRef = (0, import_react.useRef)(0);
|
|
2378
|
+
const lineTickSmoothInitedRef = (0, import_react.useRef)(false);
|
|
2379
|
+
const candleWidthTransRef = (0, import_react.useRef)({
|
|
2380
|
+
fromWidth: config.candleWidth ?? 1,
|
|
2381
|
+
toWidth: config.candleWidth ?? 1,
|
|
2382
|
+
startMs: 0,
|
|
2383
|
+
rangeFromMin: 0,
|
|
2384
|
+
rangeFromMax: 0,
|
|
2385
|
+
rangeToMin: 0,
|
|
2386
|
+
rangeToMax: 0,
|
|
2387
|
+
oldCandles: [],
|
|
2388
|
+
oldWidth: config.candleWidth ?? 1
|
|
2389
|
+
});
|
|
2390
|
+
const prevCandleDataRef = (0, import_react.useRef)({ candles: [], width: config.candleWidth ?? 1 });
|
|
2391
|
+
const pausedCandlesRef = (0, import_react.useRef)(null);
|
|
2392
|
+
const pausedLiveRef = (0, import_react.useRef)(null);
|
|
2393
|
+
const pausedLineDataRef = (0, import_react.useRef)(null);
|
|
2394
|
+
const pausedLineValueRef = (0, import_react.useRef)(null);
|
|
2395
|
+
const lastCandlesRef = (0, import_react.useRef)([]);
|
|
2396
|
+
const lastLiveRef = (0, import_react.useRef)(null);
|
|
2397
|
+
const lastLineDataStashRef = (0, import_react.useRef)([]);
|
|
2398
|
+
const lastLineValueStashRef = (0, import_react.useRef)(void 0);
|
|
1502
2399
|
(0, import_react.useEffect)(() => {
|
|
1503
2400
|
const container = containerRef.current;
|
|
1504
2401
|
if (!container) return;
|
|
@@ -1630,14 +2527,43 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1630
2527
|
}
|
|
1631
2528
|
applyDpr(ctx, dpr, w, h);
|
|
1632
2529
|
const noMotion = reducedMotionRef.current;
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
2530
|
+
const isCandle = cfg.mode === "candle";
|
|
2531
|
+
if (isCandle) {
|
|
2532
|
+
if (cfg.paused && pausedCandlesRef.current === null && (cfg.candles?.length ?? 0) > 0) {
|
|
2533
|
+
pausedCandlesRef.current = cfg.candles.slice();
|
|
2534
|
+
pausedLiveRef.current = cfg.liveCandle ?? null;
|
|
2535
|
+
pausedLineDataRef.current = cfg.lineData?.slice() ?? null;
|
|
2536
|
+
pausedLineValueRef.current = cfg.lineValue ?? null;
|
|
2537
|
+
}
|
|
2538
|
+
if (!cfg.paused) {
|
|
2539
|
+
pausedCandlesRef.current = null;
|
|
2540
|
+
pausedLiveRef.current = null;
|
|
2541
|
+
pausedLineDataRef.current = null;
|
|
2542
|
+
pausedLineValueRef.current = null;
|
|
2543
|
+
}
|
|
2544
|
+
} else if (cfg.isMultiSeries && cfg.multiSeries) {
|
|
2545
|
+
if (cfg.paused && pausedMultiDataRef.current === null) {
|
|
2546
|
+
const snap = /* @__PURE__ */ new Map();
|
|
2547
|
+
for (const s of cfg.multiSeries) {
|
|
2548
|
+
if (s.data.length >= 2) snap.set(s.id, { data: s.data.slice(), value: s.value });
|
|
2549
|
+
}
|
|
2550
|
+
if (snap.size > 0) pausedMultiDataRef.current = snap;
|
|
2551
|
+
}
|
|
2552
|
+
if (!cfg.paused) {
|
|
2553
|
+
pausedMultiDataRef.current = null;
|
|
2554
|
+
}
|
|
2555
|
+
} else {
|
|
2556
|
+
if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
|
|
2557
|
+
pausedDataRef.current = cfg.data.slice();
|
|
2558
|
+
}
|
|
2559
|
+
if (!cfg.paused) {
|
|
2560
|
+
pausedDataRef.current = null;
|
|
2561
|
+
}
|
|
1638
2562
|
}
|
|
1639
|
-
const points = pausedDataRef.current ?? cfg.data;
|
|
1640
|
-
const
|
|
2563
|
+
const points = isCandle ? [] : pausedDataRef.current ?? cfg.data;
|
|
2564
|
+
const effectiveCandles = isCandle ? pausedCandlesRef.current ?? (cfg.candles ?? []) : [];
|
|
2565
|
+
const hasMultiData = cfg.isMultiSeries && cfg.multiSeries ? cfg.multiSeries.some((s) => s.data.length >= 2) : false;
|
|
2566
|
+
const hasData = isCandle ? effectiveCandles.length >= 2 : hasMultiData || points.length >= 2;
|
|
1641
2567
|
const pad = cfg.padding;
|
|
1642
2568
|
const chartH = h - pad.top - pad.bottom;
|
|
1643
2569
|
const pauseTarget = cfg.paused ? 1 : 0;
|
|
@@ -1659,18 +2585,62 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1659
2585
|
if (loadingAlphaRef.current > 0.99) loadingAlphaRef.current = 1;
|
|
1660
2586
|
const loadingAlpha = loadingAlphaRef.current;
|
|
1661
2587
|
const revealTarget = !cfg.loading && hasData ? 1 : 0;
|
|
1662
|
-
chartRevealRef.current = noMotion ? revealTarget : lerp(
|
|
2588
|
+
chartRevealRef.current = noMotion ? revealTarget : lerp(
|
|
2589
|
+
chartRevealRef.current,
|
|
2590
|
+
revealTarget,
|
|
2591
|
+
revealTarget === 1 ? CHART_REVEAL_SPEED_FWD : CHART_REVEAL_SPEED,
|
|
2592
|
+
dt
|
|
2593
|
+
);
|
|
1663
2594
|
if (Math.abs(chartRevealRef.current - revealTarget) < 5e-3) {
|
|
1664
2595
|
chartRevealRef.current = revealTarget;
|
|
1665
2596
|
}
|
|
1666
2597
|
const chartReveal = chartRevealRef.current;
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
2598
|
+
if (chartReveal < 0.01) {
|
|
2599
|
+
rangeInitedRef.current = false;
|
|
2600
|
+
}
|
|
2601
|
+
let useStash;
|
|
2602
|
+
let useMultiStash = false;
|
|
2603
|
+
if (isCandle) {
|
|
2604
|
+
useStash = !hasData && chartReveal > 5e-3 && lastCandlesRef.current.length > 0;
|
|
2605
|
+
} else {
|
|
2606
|
+
useMultiStash = !hasData && chartReveal > 5e-3 && lastMultiSeriesRef.current.length > 0;
|
|
2607
|
+
if (hasMultiData && cfg.multiSeries) {
|
|
2608
|
+
lastMultiSeriesRef.current = cfg.multiSeries.map((s) => ({
|
|
2609
|
+
id: s.id,
|
|
2610
|
+
data: s.data.slice(),
|
|
2611
|
+
value: s.value,
|
|
2612
|
+
palette: s.palette,
|
|
2613
|
+
label: s.label
|
|
2614
|
+
}));
|
|
2615
|
+
}
|
|
2616
|
+
if (hasData && !cfg.isMultiSeries) lastMultiSeriesRef.current = [];
|
|
2617
|
+
useStash = !useMultiStash && !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
|
|
2618
|
+
if (hasData && !cfg.isMultiSeries) lastDataRef.current = points;
|
|
1670
2619
|
}
|
|
1671
|
-
if (
|
|
2620
|
+
if (isCandle) {
|
|
2621
|
+
const lmt = lineModeTransRef.current;
|
|
2622
|
+
const lineModeTarget = cfg.lineMode ? 1 : 0;
|
|
2623
|
+
if (lmt.to !== lineModeTarget) {
|
|
2624
|
+
lmt.from = lineModeProgRef.current;
|
|
2625
|
+
lmt.to = lineModeTarget;
|
|
2626
|
+
lmt.startMs = now_ms;
|
|
2627
|
+
}
|
|
2628
|
+
if (lmt.startMs > 0) {
|
|
2629
|
+
const elapsed = now_ms - lmt.startMs;
|
|
2630
|
+
const t = Math.min(elapsed / LINE_MORPH_MS, 1);
|
|
2631
|
+
lineModeProgRef.current = lmt.from + (lmt.to - lmt.from) * ((1 - Math.cos(t * Math.PI)) / 2);
|
|
2632
|
+
if (t >= 1) {
|
|
2633
|
+
lineModeProgRef.current = lmt.to;
|
|
2634
|
+
lmt.startMs = 0;
|
|
2635
|
+
}
|
|
2636
|
+
} else {
|
|
2637
|
+
lineModeProgRef.current = lmt.to;
|
|
2638
|
+
}
|
|
2639
|
+
}
|
|
2640
|
+
if (!hasData && !useStash && !useMultiStash) {
|
|
2641
|
+
const loadingColor = isCandle || cfg.isMultiSeries || lastMultiSeriesRef.current.length > 0 ? cfg.palette.gridLabel : void 0;
|
|
1672
2642
|
if (loadingAlpha > 0.01) {
|
|
1673
|
-
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha);
|
|
2643
|
+
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, loadingColor);
|
|
1674
2644
|
}
|
|
1675
2645
|
if (1 - loadingAlpha > 0.01) {
|
|
1676
2646
|
drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
|
|
@@ -1687,190 +2657,840 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1687
2657
|
rafRef.current = requestAnimationFrame(draw);
|
|
1688
2658
|
return;
|
|
1689
2659
|
}
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
2660
|
+
if (isCandle) {
|
|
2661
|
+
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
2662
|
+
const now = hasData || chartReveal < 5e-3 ? Date.now() / 1e3 - timeDebtRef.current : frozenNowRef.current;
|
|
2663
|
+
const rawLive = pausedCandlesRef.current ? pausedLiveRef.current ?? void 0 : cfg.liveCandle;
|
|
2664
|
+
let effectiveLineData = pausedLineDataRef.current ?? cfg.lineData;
|
|
2665
|
+
let effectiveLineValue = pausedLineValueRef.current ?? cfg.lineValue;
|
|
2666
|
+
if (hasData && effectiveLineData && effectiveLineData.length > 0) {
|
|
2667
|
+
lastLineDataStashRef.current = effectiveLineData;
|
|
2668
|
+
lastLineValueStashRef.current = effectiveLineValue;
|
|
2669
|
+
}
|
|
2670
|
+
if (useStash && lastLineDataStashRef.current.length > 0) {
|
|
2671
|
+
effectiveLineData = lastLineDataStashRef.current;
|
|
2672
|
+
effectiveLineValue = lastLineValueStashRef.current;
|
|
2673
|
+
}
|
|
2674
|
+
const candleWidthSecs = cfg.candleWidth ?? 1;
|
|
2675
|
+
const cwt = candleWidthTransRef.current;
|
|
2676
|
+
let morphT = -1;
|
|
2677
|
+
let displayCandleWidth;
|
|
2678
|
+
if (cwt.startMs > 0) {
|
|
2679
|
+
const elapsed = now_ms - cwt.startMs;
|
|
2680
|
+
const t = Math.min(elapsed / CANDLE_WIDTH_TRANS_MS, 1);
|
|
2681
|
+
morphT = (1 - Math.cos(t * Math.PI)) / 2;
|
|
2682
|
+
displayCandleWidth = Math.exp(
|
|
2683
|
+
Math.log(cwt.fromWidth) + (Math.log(cwt.toWidth) - Math.log(cwt.fromWidth)) * morphT
|
|
2684
|
+
);
|
|
2685
|
+
if (t >= 1) {
|
|
2686
|
+
displayCandleWidth = cwt.toWidth;
|
|
2687
|
+
cwt.startMs = 0;
|
|
2688
|
+
morphT = -1;
|
|
2689
|
+
}
|
|
2690
|
+
} else {
|
|
2691
|
+
displayCandleWidth = cwt.toWidth;
|
|
2692
|
+
}
|
|
2693
|
+
if (candleWidthSecs !== cwt.toWidth) {
|
|
2694
|
+
cwt.oldCandles = prevCandleDataRef.current.candles;
|
|
2695
|
+
cwt.oldWidth = prevCandleDataRef.current.width;
|
|
2696
|
+
cwt.fromWidth = displayCandleWidth;
|
|
2697
|
+
cwt.toWidth = candleWidthSecs;
|
|
2698
|
+
cwt.startMs = now_ms;
|
|
2699
|
+
morphT = 0;
|
|
2700
|
+
cwt.rangeFromMin = displayMinRef.current;
|
|
2701
|
+
cwt.rangeFromMax = displayMaxRef.current;
|
|
2702
|
+
const curWindow = displayWindowRef.current;
|
|
2703
|
+
const re = now + curWindow * CANDLE_BUFFER;
|
|
2704
|
+
const le = re - curWindow;
|
|
2705
|
+
const targetVis = [];
|
|
2706
|
+
for (const c of effectiveCandles) {
|
|
2707
|
+
if (c.time + candleWidthSecs >= le && c.time <= re) targetVis.push(c);
|
|
2708
|
+
}
|
|
2709
|
+
if (rawLive) targetVis.push(rawLive);
|
|
2710
|
+
if (targetVis.length > 0) {
|
|
2711
|
+
const tr = computeCandleRange(targetVis);
|
|
2712
|
+
cwt.rangeToMin = tr.min;
|
|
2713
|
+
cwt.rangeToMax = tr.max;
|
|
2714
|
+
} else {
|
|
2715
|
+
cwt.rangeToMin = displayMinRef.current;
|
|
2716
|
+
cwt.rangeToMax = displayMaxRef.current;
|
|
2717
|
+
}
|
|
2718
|
+
}
|
|
2719
|
+
prevCandleDataRef.current = { candles: cfg.candles ?? [], width: candleWidthSecs };
|
|
2720
|
+
const lineModeProg = lineModeProgRef.current;
|
|
2721
|
+
const ldt = lineDensityTransRef.current;
|
|
2722
|
+
const hasTickData = effectiveLineData && effectiveLineData.length > 0;
|
|
2723
|
+
const densityTarget = cfg.lineMode && lineModeProg >= 0.3 && hasTickData ? 1 : 0;
|
|
2724
|
+
if (ldt.to !== densityTarget) {
|
|
2725
|
+
ldt.from = lineDensityProgRef.current;
|
|
2726
|
+
ldt.to = densityTarget;
|
|
2727
|
+
ldt.startMs = now_ms;
|
|
2728
|
+
}
|
|
2729
|
+
let lineDensityProg;
|
|
2730
|
+
if (ldt.startMs > 0) {
|
|
2731
|
+
const elapsed = now_ms - ldt.startMs;
|
|
2732
|
+
const t = Math.min(elapsed / LINE_DENSITY_MS, 1);
|
|
2733
|
+
lineDensityProg = ldt.from + (ldt.to - ldt.from) * (1 - (1 - t) * (1 - t));
|
|
2734
|
+
if (t >= 1) {
|
|
2735
|
+
lineDensityProg = ldt.to;
|
|
2736
|
+
ldt.startMs = 0;
|
|
2737
|
+
}
|
|
2738
|
+
} else {
|
|
2739
|
+
lineDensityProg = ldt.to;
|
|
2740
|
+
}
|
|
2741
|
+
lineDensityProgRef.current = lineDensityProg;
|
|
2742
|
+
const transition = windowTransitionRef.current;
|
|
2743
|
+
const windowResult = updateCandleWindowTransition(
|
|
2744
|
+
cfg.windowSecs,
|
|
2745
|
+
transition,
|
|
2746
|
+
displayWindowRef.current,
|
|
2747
|
+
displayMinRef.current,
|
|
2748
|
+
displayMaxRef.current,
|
|
2749
|
+
now_ms,
|
|
2750
|
+
now,
|
|
2751
|
+
effectiveCandles,
|
|
2752
|
+
rawLive,
|
|
2753
|
+
candleWidthSecs,
|
|
2754
|
+
CANDLE_BUFFER
|
|
2755
|
+
);
|
|
2756
|
+
displayWindowRef.current = windowResult.windowSecs;
|
|
2757
|
+
const windowSecs = windowResult.windowSecs;
|
|
2758
|
+
const windowTransProgress = windowResult.windowTransProgress;
|
|
2759
|
+
const isWindowTransitioning = transition.startMs > 0;
|
|
2760
|
+
const rightEdge = now + windowSecs * CANDLE_BUFFER;
|
|
2761
|
+
const leftEdge = rightEdge - windowSecs;
|
|
2762
|
+
let smoothLive;
|
|
2763
|
+
if (rawLive) {
|
|
2764
|
+
const prev = displayCandleRef.current;
|
|
2765
|
+
if (!prev || prev.time !== rawLive.time) {
|
|
2766
|
+
displayCandleRef.current = {
|
|
2767
|
+
time: rawLive.time,
|
|
2768
|
+
open: rawLive.open,
|
|
2769
|
+
high: rawLive.open,
|
|
2770
|
+
low: rawLive.open,
|
|
2771
|
+
close: rawLive.open
|
|
2772
|
+
};
|
|
2773
|
+
liveBirthAlphaRef.current = 0;
|
|
2774
|
+
} else {
|
|
2775
|
+
const dc2 = displayCandleRef.current;
|
|
2776
|
+
dc2.open = lerp(dc2.open, rawLive.open, CANDLE_LERP_SPEED, pausedDt);
|
|
2777
|
+
dc2.high = lerp(dc2.high, rawLive.high, CANDLE_LERP_SPEED, pausedDt);
|
|
2778
|
+
dc2.low = lerp(dc2.low, rawLive.low, CANDLE_LERP_SPEED, pausedDt);
|
|
2779
|
+
dc2.close = lerp(dc2.close, rawLive.close, CANDLE_LERP_SPEED, pausedDt);
|
|
2780
|
+
}
|
|
2781
|
+
liveBirthAlphaRef.current = lerp(liveBirthAlphaRef.current, 1, 0.2, pausedDt);
|
|
2782
|
+
if (liveBirthAlphaRef.current > 0.99) liveBirthAlphaRef.current = 1;
|
|
2783
|
+
const dc = displayCandleRef.current;
|
|
2784
|
+
const bullTarget = dc.close >= dc.open ? 1 : 0;
|
|
2785
|
+
liveBullRef.current = lerp(liveBullRef.current, bullTarget, 0.12, pausedDt);
|
|
2786
|
+
if (liveBullRef.current > 0.99) liveBullRef.current = 1;
|
|
2787
|
+
if (liveBullRef.current < 0.01) liveBullRef.current = 0;
|
|
2788
|
+
smoothLive = dc;
|
|
2789
|
+
} else {
|
|
2790
|
+
displayCandleRef.current = null;
|
|
2791
|
+
liveBirthAlphaRef.current = 1;
|
|
2792
|
+
liveBullRef.current = 0.5;
|
|
2793
|
+
}
|
|
2794
|
+
if (rawLive) {
|
|
2795
|
+
if (!closeLineSmoothInitedRef.current) {
|
|
2796
|
+
closeLineSmoothRef.current = rawLive.close;
|
|
2797
|
+
closeLineSmoothInitedRef.current = true;
|
|
2798
|
+
} else {
|
|
2799
|
+
closeLineSmoothRef.current = lerp(closeLineSmoothRef.current, rawLive.close, CLOSE_LINE_LERP_SPEED, pausedDt);
|
|
2800
|
+
const gap = Math.abs(closeLineSmoothRef.current - rawLive.close);
|
|
2801
|
+
const range = displayMaxRef.current - displayMinRef.current || 1;
|
|
2802
|
+
if (gap < range * 5e-4) closeLineSmoothRef.current = rawLive.close;
|
|
2803
|
+
}
|
|
2804
|
+
} else if (!useStash) {
|
|
2805
|
+
closeLineSmoothInitedRef.current = false;
|
|
2806
|
+
}
|
|
2807
|
+
if (rawLive) {
|
|
2808
|
+
if (!lineSmoothInitedRef.current) {
|
|
2809
|
+
lineSmoothCloseRef.current = rawLive.close;
|
|
2810
|
+
lineSmoothInitedRef.current = true;
|
|
2811
|
+
} else {
|
|
2812
|
+
const valGap = Math.abs(rawLive.close - lineSmoothCloseRef.current);
|
|
2813
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
2814
|
+
const gapRatio = Math.min(valGap / prevRange, 1);
|
|
2815
|
+
const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
|
|
2816
|
+
lineSmoothCloseRef.current = lerp(lineSmoothCloseRef.current, rawLive.close, adaptiveSpeed, pausedDt);
|
|
2817
|
+
if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineSmoothCloseRef.current = rawLive.close;
|
|
2818
|
+
}
|
|
2819
|
+
} else if (!useStash) {
|
|
2820
|
+
lineSmoothInitedRef.current = false;
|
|
2821
|
+
}
|
|
2822
|
+
if (effectiveLineValue !== void 0 && hasTickData) {
|
|
2823
|
+
if (!lineTickSmoothInitedRef.current) {
|
|
2824
|
+
lineTickSmoothRef.current = effectiveLineValue;
|
|
2825
|
+
lineTickSmoothInitedRef.current = true;
|
|
2826
|
+
} else {
|
|
2827
|
+
const valGap = Math.abs(effectiveLineValue - lineTickSmoothRef.current);
|
|
2828
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
2829
|
+
const gapRatio = Math.min(valGap / prevRange, 1);
|
|
2830
|
+
const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
|
|
2831
|
+
lineTickSmoothRef.current = lerp(lineTickSmoothRef.current, effectiveLineValue, adaptiveSpeed, pausedDt);
|
|
2832
|
+
if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineTickSmoothRef.current = effectiveLineValue;
|
|
2833
|
+
}
|
|
2834
|
+
} else if (!useStash) {
|
|
2835
|
+
lineTickSmoothInitedRef.current = false;
|
|
2836
|
+
}
|
|
2837
|
+
const visible = [];
|
|
2838
|
+
for (const c of effectiveCandles) {
|
|
2839
|
+
if (c.time + candleWidthSecs >= leftEdge && c.time <= rightEdge) visible.push(c);
|
|
2840
|
+
}
|
|
2841
|
+
if (smoothLive && smoothLive.time + displayCandleWidth >= leftEdge && smoothLive.time <= rightEdge) {
|
|
2842
|
+
visible.push(smoothLive);
|
|
2843
|
+
}
|
|
2844
|
+
let oldVisible = [];
|
|
2845
|
+
if (morphT >= 0 && cwt.oldCandles.length > 0) {
|
|
2846
|
+
for (const c of cwt.oldCandles) {
|
|
2847
|
+
if (c.time + cwt.oldWidth >= leftEdge && c.time <= rightEdge) oldVisible.push(c);
|
|
2848
|
+
}
|
|
2849
|
+
}
|
|
2850
|
+
if (hasData) {
|
|
2851
|
+
lastCandlesRef.current = visible;
|
|
2852
|
+
lastLiveRef.current = smoothLive ?? null;
|
|
2853
|
+
}
|
|
2854
|
+
const effectiveVisible = useStash ? lastCandlesRef.current : visible;
|
|
2855
|
+
const effectiveLive = useStash ? lastLiveRef.current ?? void 0 : smoothLive;
|
|
2856
|
+
const chartW = w - pad.left - pad.right;
|
|
2857
|
+
const computed = effectiveVisible.length > 0 ? computeCandleRange(effectiveVisible) : { min: displayMinRef.current, max: displayMaxRef.current };
|
|
2858
|
+
const rangeResult = updateCandleRange(
|
|
2859
|
+
computed,
|
|
2860
|
+
rangeInitedRef.current,
|
|
2861
|
+
displayMinRef.current,
|
|
2862
|
+
displayMaxRef.current,
|
|
2863
|
+
isWindowTransitioning,
|
|
2864
|
+
windowTransProgress,
|
|
2865
|
+
transition,
|
|
2866
|
+
chartH,
|
|
2867
|
+
pausedDt
|
|
2868
|
+
);
|
|
2869
|
+
if (morphT >= 0) {
|
|
2870
|
+
rangeResult.displayMin = cwt.rangeFromMin + (cwt.rangeToMin - cwt.rangeFromMin) * morphT;
|
|
2871
|
+
rangeResult.displayMax = cwt.rangeFromMax + (cwt.rangeToMax - cwt.rangeFromMax) * morphT;
|
|
2872
|
+
rangeResult.minVal = rangeResult.displayMin;
|
|
2873
|
+
rangeResult.maxVal = rangeResult.displayMax;
|
|
2874
|
+
rangeResult.valRange = rangeResult.displayMax - rangeResult.displayMin || 1e-3;
|
|
2875
|
+
}
|
|
2876
|
+
rangeInitedRef.current = rangeResult.rangeInited;
|
|
2877
|
+
displayMinRef.current = rangeResult.displayMin;
|
|
2878
|
+
displayMaxRef.current = rangeResult.displayMax;
|
|
2879
|
+
const { minVal, maxVal, valRange } = rangeResult;
|
|
2880
|
+
const layout = {
|
|
2881
|
+
w,
|
|
2882
|
+
h,
|
|
2883
|
+
pad,
|
|
2884
|
+
chartW,
|
|
2885
|
+
chartH,
|
|
2886
|
+
leftEdge,
|
|
2887
|
+
rightEdge,
|
|
2888
|
+
minVal,
|
|
2889
|
+
maxVal,
|
|
2890
|
+
valRange,
|
|
2891
|
+
toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
|
|
2892
|
+
toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
|
|
2893
|
+
};
|
|
2894
|
+
const hoverPx = hoverXRef.current;
|
|
2895
|
+
let hoveredCandle = null;
|
|
2896
|
+
let isActiveHover = false;
|
|
2897
|
+
if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
|
|
2898
|
+
hoveredCandle = candleAtX(effectiveVisible, hoverPx, displayCandleWidth, layout);
|
|
2899
|
+
if (hoveredCandle) isActiveHover = true;
|
|
2900
|
+
}
|
|
2901
|
+
const scrubTarget = isActiveHover ? 1 : 0;
|
|
2902
|
+
scrubAmountRef.current = lerp(scrubAmountRef.current, scrubTarget, 0.12, dt);
|
|
2903
|
+
if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
|
|
2904
|
+
if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
|
|
2905
|
+
const scrubAmount = scrubAmountRef.current;
|
|
2906
|
+
let drawHoverX = hoverPx;
|
|
2907
|
+
let drawHoverTime = 0;
|
|
2908
|
+
let drawHoverCandle = hoveredCandle;
|
|
2909
|
+
if (!isActiveHover && scrubAmount > 0 && lastHoverRef.current) {
|
|
2910
|
+
drawHoverX = lastHoverRef.current.x;
|
|
2911
|
+
drawHoverTime = lastHoverRef.current.time;
|
|
2912
|
+
drawHoverCandle = candleAtX(effectiveVisible, lastHoverRef.current.x, displayCandleWidth, layout);
|
|
2913
|
+
} else if (isActiveHover && hoverPx !== null) {
|
|
2914
|
+
drawHoverTime = layout.leftEdge + (hoverPx - pad.left) / chartW * (layout.rightEdge - layout.leftEdge);
|
|
2915
|
+
lastHoverRef.current = { x: hoverPx, value: hoveredCandle?.close ?? 0, time: drawHoverTime };
|
|
2916
|
+
}
|
|
2917
|
+
let drawCandles = effectiveVisible;
|
|
2918
|
+
let drawOldCandles = oldVisible;
|
|
2919
|
+
let drawLive = effectiveLive;
|
|
2920
|
+
if (lineModeProg > 0.01 && drawLive && lineSmoothInitedRef.current) {
|
|
2921
|
+
const blended = drawLive.close + (lineSmoothCloseRef.current - drawLive.close) * lineModeProg;
|
|
2922
|
+
drawLive = { ...drawLive, close: blended };
|
|
2923
|
+
const li = drawCandles.length - 1;
|
|
2924
|
+
if (li >= 0 && drawCandles[li].time === drawLive.time) {
|
|
2925
|
+
drawCandles = drawCandles.slice();
|
|
2926
|
+
drawCandles[li] = { ...drawCandles[li], close: blended };
|
|
2927
|
+
}
|
|
2928
|
+
}
|
|
2929
|
+
if (lineModeProg > 0.01 && lineModeProg < 0.99) {
|
|
2930
|
+
const collapseOHLC = (c) => {
|
|
2931
|
+
const inv = 1 - lineModeProg;
|
|
2932
|
+
return {
|
|
2933
|
+
time: c.time,
|
|
2934
|
+
open: c.close + (c.open - c.close) * inv,
|
|
2935
|
+
high: c.close + (c.high - c.close) * inv,
|
|
2936
|
+
low: c.close + (c.low - c.close) * inv,
|
|
2937
|
+
close: c.close
|
|
2938
|
+
};
|
|
2939
|
+
};
|
|
2940
|
+
drawCandles = drawCandles.map(collapseOHLC);
|
|
2941
|
+
if (drawOldCandles.length > 0) drawOldCandles = drawOldCandles.map(collapseOHLC);
|
|
2942
|
+
if (drawLive) drawLive = collapseOHLC(drawLive);
|
|
2943
|
+
}
|
|
2944
|
+
let lineVisible;
|
|
2945
|
+
let lineSmoothValue;
|
|
2946
|
+
if (effectiveLineData && effectiveLineData.length > 0 && (lineDensityProg > 0.01 || lineModeProg > 0.05)) {
|
|
2947
|
+
const closeRefs = [];
|
|
2948
|
+
for (const c of drawCandles) {
|
|
2949
|
+
closeRefs.push({ t: c.time + displayCandleWidth / 2, v: c.close });
|
|
2950
|
+
}
|
|
2951
|
+
if (drawLive) closeRefs.push({ t: now, v: drawLive.close });
|
|
2952
|
+
lineVisible = [];
|
|
2953
|
+
let refIdx = 0;
|
|
2954
|
+
for (const pt of effectiveLineData) {
|
|
2955
|
+
if (pt.time < leftEdge || pt.time > rightEdge) continue;
|
|
2956
|
+
while (refIdx < closeRefs.length - 2 && closeRefs[refIdx + 1].t < pt.time) refIdx++;
|
|
2957
|
+
let interpClose;
|
|
2958
|
+
if (closeRefs.length === 0) {
|
|
2959
|
+
interpClose = pt.value;
|
|
2960
|
+
} else if (closeRefs.length === 1 || pt.time <= closeRefs[0].t) {
|
|
2961
|
+
interpClose = closeRefs[0].v;
|
|
2962
|
+
} else if (refIdx >= closeRefs.length - 1) {
|
|
2963
|
+
interpClose = closeRefs[closeRefs.length - 1].v;
|
|
2964
|
+
} else {
|
|
2965
|
+
const a = closeRefs[refIdx];
|
|
2966
|
+
const b = closeRefs[refIdx + 1];
|
|
2967
|
+
const span = b.t - a.t;
|
|
2968
|
+
const frac = span > 0 ? Math.max(0, Math.min(1, (pt.time - a.t) / span)) : 0;
|
|
2969
|
+
interpClose = a.v + (b.v - a.v) * frac;
|
|
2970
|
+
}
|
|
2971
|
+
const blended = interpClose + (pt.value - interpClose) * lineDensityProg;
|
|
2972
|
+
lineVisible.push({ time: pt.time, value: blended });
|
|
2973
|
+
}
|
|
2974
|
+
const smoothTick = lineTickSmoothInitedRef.current ? lineTickSmoothRef.current : effectiveLineValue ?? effectiveLineData[effectiveLineData.length - 1].value;
|
|
2975
|
+
lineSmoothValue = lineSmoothCloseRef.current + (smoothTick - lineSmoothCloseRef.current) * lineDensityProg;
|
|
2976
|
+
} else {
|
|
2977
|
+
lineVisible = drawCandles.map((c) => ({
|
|
2978
|
+
time: c.time + displayCandleWidth / 2,
|
|
2979
|
+
value: c.close
|
|
2980
|
+
}));
|
|
2981
|
+
lineSmoothValue = lineSmoothInitedRef.current ? lineSmoothCloseRef.current : drawLive?.close ?? drawCandles[drawCandles.length - 1]?.close ?? 0;
|
|
2982
|
+
}
|
|
2983
|
+
if (chartReveal < 1 && lineVisible.length >= 2) {
|
|
2984
|
+
const firstTime = lineVisible[0].time;
|
|
2985
|
+
const windowSpan = rightEdge - leftEdge;
|
|
2986
|
+
if (firstTime - leftEdge > windowSpan * 0.05) {
|
|
2987
|
+
const firstVal = lineVisible[0].value;
|
|
2988
|
+
const step = windowSpan / 32;
|
|
2989
|
+
const padded = [];
|
|
2990
|
+
for (let t = leftEdge; t < firstTime - step * 0.5; t += step) {
|
|
2991
|
+
padded.push({ time: t, value: firstVal });
|
|
2992
|
+
}
|
|
2993
|
+
lineVisible = [...padded, ...lineVisible];
|
|
2994
|
+
}
|
|
2995
|
+
}
|
|
2996
|
+
drawCandleFrame(ctx, layout, cfg.palette, {
|
|
2997
|
+
candles: drawCandles,
|
|
2998
|
+
displayCandleWidth,
|
|
2999
|
+
oldCandles: drawOldCandles,
|
|
3000
|
+
oldWidth: cwt.oldWidth,
|
|
3001
|
+
morphT,
|
|
3002
|
+
liveCandle: drawLive,
|
|
3003
|
+
closePriceCandle: closeLineSmoothInitedRef.current && rawLive ? { ...rawLive, close: closeLineSmoothRef.current } : rawLive,
|
|
3004
|
+
liveTime: effectiveLive?.time ?? -1,
|
|
3005
|
+
liveBirthAlpha: liveBirthAlphaRef.current,
|
|
3006
|
+
liveBullBlend: liveBullRef.current,
|
|
3007
|
+
lineModeProg,
|
|
3008
|
+
chartReveal,
|
|
3009
|
+
now_ms,
|
|
3010
|
+
now,
|
|
3011
|
+
pauseProgress,
|
|
3012
|
+
showGrid: cfg.showGrid,
|
|
3013
|
+
scrubAmount,
|
|
3014
|
+
hoverX: drawHoverX,
|
|
3015
|
+
hoverValue: drawHoverCandle?.close ?? null,
|
|
3016
|
+
hoverTime: drawHoverTime,
|
|
3017
|
+
hoveredCandle: drawHoverCandle,
|
|
3018
|
+
formatValue: cfg.formatValue,
|
|
3019
|
+
formatTime: cfg.formatTime,
|
|
3020
|
+
gridState: gridStateRef.current,
|
|
3021
|
+
timeAxisState: timeAxisStateRef.current,
|
|
3022
|
+
dt: pausedDt,
|
|
3023
|
+
targetWindowSecs: cfg.windowSecs,
|
|
3024
|
+
tooltipY: cfg.tooltipY,
|
|
3025
|
+
tooltipOutline: cfg.tooltipOutline,
|
|
3026
|
+
lineVisible,
|
|
3027
|
+
lineSmoothValue,
|
|
3028
|
+
emptyText: cfg.emptyText,
|
|
3029
|
+
loadingAlpha,
|
|
3030
|
+
// Show empty overlay when not loading AND loadingAlpha has fully
|
|
3031
|
+
// decayed. This prevents the gradient gap from flashing during
|
|
3032
|
+
// loading→live (where loadingAlpha starts at ~1), while still
|
|
3033
|
+
// allowing smooth fade-out during empty→live (loadingAlpha is 0).
|
|
3034
|
+
showEmptyOverlay: !(cfg.loading ?? false) && loadingAlpha < 0.01
|
|
3035
|
+
});
|
|
3036
|
+
if (badgeRef.current) {
|
|
3037
|
+
if (lineModeProg > 0.5 && cfg.showBadge) {
|
|
3038
|
+
const momentum = detectMomentum(lineVisible);
|
|
3039
|
+
badgeYRef.current = updateBadgeDOM(
|
|
3040
|
+
badgeRef.current,
|
|
3041
|
+
cfg,
|
|
3042
|
+
lineSmoothValue,
|
|
3043
|
+
layout,
|
|
3044
|
+
momentum,
|
|
3045
|
+
badgeYRef.current,
|
|
3046
|
+
badgeColorRef.current,
|
|
3047
|
+
isWindowTransitioning,
|
|
3048
|
+
noMotion,
|
|
3049
|
+
ctx,
|
|
3050
|
+
pausedDt,
|
|
3051
|
+
chartReveal
|
|
3052
|
+
);
|
|
3053
|
+
const badgeFade = (lineModeProg - 0.5) * 2;
|
|
3054
|
+
if (badgeRef.current.container.style.display !== "none") {
|
|
3055
|
+
const base = badgeRef.current.container.style.opacity ? parseFloat(badgeRef.current.container.style.opacity) : 1;
|
|
3056
|
+
badgeRef.current.container.style.opacity = String(
|
|
3057
|
+
base * badgeFade * (1 - pauseProgress)
|
|
3058
|
+
);
|
|
3059
|
+
}
|
|
3060
|
+
} else {
|
|
3061
|
+
badgeRef.current.container.style.display = "none";
|
|
3062
|
+
}
|
|
3063
|
+
}
|
|
3064
|
+
} else if (cfg.isMultiSeries && cfg.multiSeries && cfg.multiSeries.length > 0 || useMultiStash) {
|
|
3065
|
+
const effectiveMultiSeries = useMultiStash ? lastMultiSeriesRef.current : cfg.multiSeries;
|
|
3066
|
+
let labelReserve = 0;
|
|
3067
|
+
if (effectiveMultiSeries.some((s) => s.label)) {
|
|
3068
|
+
ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
|
|
3069
|
+
let maxLabelW = 0;
|
|
3070
|
+
for (const s of effectiveMultiSeries) {
|
|
3071
|
+
if (s.label) {
|
|
3072
|
+
const lw = ctx.measureText(s.label).width;
|
|
3073
|
+
if (lw > maxLabelW) maxLabelW = lw;
|
|
3074
|
+
}
|
|
3075
|
+
}
|
|
3076
|
+
labelReserve = Math.max(0, maxLabelW - 2) * chartReveal;
|
|
3077
|
+
}
|
|
3078
|
+
const chartW = w - pad.left - pad.right - labelReserve;
|
|
3079
|
+
const buffer = WINDOW_BUFFER;
|
|
3080
|
+
if (!useMultiStash) {
|
|
3081
|
+
const currentIds = new Set(effectiveMultiSeries.map((s) => s.id));
|
|
3082
|
+
for (const key of displayValuesRef.current.keys()) {
|
|
3083
|
+
if (!currentIds.has(key)) displayValuesRef.current.delete(key);
|
|
3084
|
+
}
|
|
3085
|
+
}
|
|
3086
|
+
const firstSeries = effectiveMultiSeries[0];
|
|
3087
|
+
const transition = windowTransitionRef.current;
|
|
3088
|
+
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
3089
|
+
const now = useMultiStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
3090
|
+
const smoothValues = /* @__PURE__ */ new Map();
|
|
3091
|
+
for (const s of effectiveMultiSeries) {
|
|
3092
|
+
let dv = displayValuesRef.current.get(s.id);
|
|
3093
|
+
if (dv === void 0) dv = s.value;
|
|
3094
|
+
if (!useMultiStash) {
|
|
3095
|
+
const adaptiveSpeed2 = computeAdaptiveSpeed(
|
|
3096
|
+
s.value,
|
|
3097
|
+
dv,
|
|
3098
|
+
displayMinRef.current,
|
|
3099
|
+
displayMaxRef.current,
|
|
3100
|
+
cfg.lerpSpeed,
|
|
3101
|
+
noMotion
|
|
3102
|
+
);
|
|
3103
|
+
dv = lerp(dv, s.value, adaptiveSpeed2, pausedDt);
|
|
3104
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
3105
|
+
if (Math.abs(dv - s.value) < prevRange * VALUE_SNAP_THRESHOLD) dv = s.value;
|
|
3106
|
+
displayValuesRef.current.set(s.id, dv);
|
|
3107
|
+
}
|
|
3108
|
+
smoothValues.set(s.id, dv);
|
|
3109
|
+
}
|
|
3110
|
+
const hiddenIds = cfg.hiddenSeriesIds;
|
|
3111
|
+
const seriesAlphas = seriesAlphaRef.current;
|
|
3112
|
+
for (const s of effectiveMultiSeries) {
|
|
3113
|
+
let alpha = seriesAlphas.get(s.id) ?? 1;
|
|
3114
|
+
const target = hiddenIds?.has(s.id) ? 0 : 1;
|
|
3115
|
+
alpha = noMotion ? target : lerp(alpha, target, SERIES_TOGGLE_SPEED, pausedDt);
|
|
3116
|
+
if (alpha < 0.01) alpha = 0;
|
|
3117
|
+
if (alpha > 0.99) alpha = 1;
|
|
3118
|
+
seriesAlphas.set(s.id, alpha);
|
|
3119
|
+
}
|
|
3120
|
+
const firstData = pausedMultiDataRef.current?.get(firstSeries.id)?.data ?? firstSeries.data;
|
|
3121
|
+
const windowResult = updateWindowTransition(
|
|
3122
|
+
cfg,
|
|
3123
|
+
transition,
|
|
3124
|
+
displayWindowRef.current,
|
|
3125
|
+
displayMinRef.current,
|
|
3126
|
+
displayMaxRef.current,
|
|
3127
|
+
noMotion,
|
|
3128
|
+
now_ms,
|
|
3129
|
+
now,
|
|
3130
|
+
firstData,
|
|
3131
|
+
smoothValues.get(firstSeries.id) ?? firstSeries.value,
|
|
3132
|
+
buffer
|
|
3133
|
+
);
|
|
3134
|
+
if (transition.startMs > 0 && effectiveMultiSeries.length > 1) {
|
|
3135
|
+
const targetRightEdge = now + cfg.windowSecs * buffer;
|
|
3136
|
+
const targetLeftEdge = targetRightEdge - cfg.windowSecs;
|
|
3137
|
+
let unionMin = Infinity;
|
|
3138
|
+
let unionMax = -Infinity;
|
|
3139
|
+
for (const s of effectiveMultiSeries) {
|
|
3140
|
+
const sData = pausedMultiDataRef.current?.get(s.id)?.data ?? s.data;
|
|
3141
|
+
const sv = smoothValues.get(s.id) ?? s.value;
|
|
3142
|
+
const targetVisible = [];
|
|
3143
|
+
for (const p of sData) {
|
|
3144
|
+
if (p.time >= targetLeftEdge - 2 && p.time <= targetRightEdge) targetVisible.push(p);
|
|
3145
|
+
}
|
|
3146
|
+
if (targetVisible.length > 0) {
|
|
3147
|
+
const range = computeRange(targetVisible, sv, cfg.referenceLine?.value, cfg.exaggerate);
|
|
3148
|
+
if (range.min < unionMin) unionMin = range.min;
|
|
3149
|
+
if (range.max > unionMax) unionMax = range.max;
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
if (isFinite(unionMin) && isFinite(unionMax)) {
|
|
3153
|
+
transition.rangeToMin = unionMin;
|
|
3154
|
+
transition.rangeToMax = unionMax;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
displayWindowRef.current = windowResult.windowSecs;
|
|
3158
|
+
const windowSecs = windowResult.windowSecs;
|
|
3159
|
+
const windowTransProgress = windowResult.windowTransProgress;
|
|
3160
|
+
const isWindowTransitioning = transition.startMs > 0;
|
|
3161
|
+
const rightEdge = now + windowSecs * buffer;
|
|
3162
|
+
const leftEdge = rightEdge - windowSecs;
|
|
3163
|
+
const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
|
|
3164
|
+
const seriesEntries = [];
|
|
3165
|
+
let globalMin = Infinity;
|
|
3166
|
+
let globalMax = -Infinity;
|
|
3167
|
+
for (const s of effectiveMultiSeries) {
|
|
3168
|
+
const snap = pausedMultiDataRef.current?.get(s.id);
|
|
3169
|
+
const seriesData = snap?.data ?? s.data;
|
|
3170
|
+
const visible = [];
|
|
3171
|
+
for (const p of seriesData) {
|
|
3172
|
+
if (p.time >= leftEdge - 2 && p.time <= filterRight) visible.push(p);
|
|
3173
|
+
}
|
|
3174
|
+
const sv = smoothValues.get(s.id) ?? s.value;
|
|
3175
|
+
const alpha = seriesAlphas.get(s.id) ?? 1;
|
|
3176
|
+
if (visible.length >= 2) {
|
|
3177
|
+
if (alpha > 0.01) {
|
|
3178
|
+
const range = computeRange(visible, sv, cfg.referenceLine?.value, cfg.exaggerate);
|
|
3179
|
+
if (range.min < globalMin) globalMin = range.min;
|
|
3180
|
+
if (range.max > globalMax) globalMax = range.max;
|
|
3181
|
+
}
|
|
3182
|
+
seriesEntries.push({ visible, smoothValue: sv, palette: s.palette, label: s.label, alpha });
|
|
3183
|
+
}
|
|
3184
|
+
}
|
|
3185
|
+
if (seriesEntries.length === 0) {
|
|
3186
|
+
if (loadingAlpha > 0.01) {
|
|
3187
|
+
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, cfg.palette.gridLabel);
|
|
3188
|
+
}
|
|
3189
|
+
if (1 - loadingAlpha > 0.01) {
|
|
3190
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
|
|
3191
|
+
}
|
|
3192
|
+
ctx.save();
|
|
3193
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
3194
|
+
const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
|
|
3195
|
+
fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
|
|
3196
|
+
fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
3197
|
+
ctx.fillStyle = fadeGrad;
|
|
3198
|
+
ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
|
|
3199
|
+
ctx.restore();
|
|
3200
|
+
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
3201
|
+
rafRef.current = requestAnimationFrame(draw);
|
|
3202
|
+
return;
|
|
3203
|
+
}
|
|
3204
|
+
const computedRange = { min: isFinite(globalMin) ? globalMin : 0, max: isFinite(globalMax) ? globalMax : 1 };
|
|
3205
|
+
const adaptiveSpeed = cfg.lerpSpeed + ADAPTIVE_SPEED_BOOST * 0.5;
|
|
3206
|
+
const rangeResult = updateRange(
|
|
3207
|
+
computedRange,
|
|
3208
|
+
rangeInitedRef.current,
|
|
3209
|
+
targetMinRef.current,
|
|
3210
|
+
targetMaxRef.current,
|
|
3211
|
+
displayMinRef.current,
|
|
3212
|
+
displayMaxRef.current,
|
|
3213
|
+
isWindowTransitioning,
|
|
3214
|
+
windowTransProgress,
|
|
3215
|
+
transition,
|
|
3216
|
+
adaptiveSpeed,
|
|
3217
|
+
chartH,
|
|
3218
|
+
pausedDt
|
|
3219
|
+
);
|
|
3220
|
+
rangeInitedRef.current = rangeResult.rangeInited;
|
|
3221
|
+
targetMinRef.current = rangeResult.targetMin;
|
|
3222
|
+
targetMaxRef.current = rangeResult.targetMax;
|
|
3223
|
+
displayMinRef.current = rangeResult.displayMin;
|
|
3224
|
+
displayMaxRef.current = rangeResult.displayMax;
|
|
3225
|
+
const { minVal, maxVal, valRange } = rangeResult;
|
|
3226
|
+
const layout = {
|
|
3227
|
+
w,
|
|
3228
|
+
h,
|
|
3229
|
+
pad,
|
|
3230
|
+
chartW,
|
|
3231
|
+
chartH,
|
|
3232
|
+
leftEdge,
|
|
3233
|
+
rightEdge,
|
|
3234
|
+
minVal,
|
|
3235
|
+
maxVal,
|
|
3236
|
+
valRange,
|
|
3237
|
+
toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
|
|
3238
|
+
toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
|
|
3239
|
+
};
|
|
3240
|
+
const hoverPx = hoverXRef.current;
|
|
3241
|
+
let drawHoverX = null;
|
|
3242
|
+
let drawHoverTime = null;
|
|
3243
|
+
let isActiveHover = false;
|
|
3244
|
+
let hoverEntries = [];
|
|
3245
|
+
if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
|
|
3246
|
+
const maxHoverX = layout.toX(now);
|
|
3247
|
+
const clampedX = Math.min(hoverPx, maxHoverX);
|
|
3248
|
+
const t = leftEdge + (clampedX - pad.left) / chartW * (rightEdge - leftEdge);
|
|
3249
|
+
drawHoverX = clampedX;
|
|
3250
|
+
drawHoverTime = t;
|
|
3251
|
+
isActiveHover = true;
|
|
3252
|
+
for (const entry of seriesEntries) {
|
|
3253
|
+
if ((entry.alpha ?? 1) < 0.5) continue;
|
|
3254
|
+
const v = interpolateAtTime(entry.visible, t);
|
|
3255
|
+
if (v !== null) {
|
|
3256
|
+
hoverEntries.push({ color: entry.palette.line, label: entry.label ?? "", value: v });
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
lastHoverRef.current = { x: clampedX, value: hoverEntries[0]?.value ?? 0, time: t };
|
|
3260
|
+
lastHoverEntriesRef.current = hoverEntries;
|
|
3261
|
+
cfg.onHover?.({ time: t, value: hoverEntries[0]?.value ?? 0, x: clampedX, y: layout.toY(hoverEntries[0]?.value ?? 0) });
|
|
3262
|
+
}
|
|
3263
|
+
const scrubTarget = isActiveHover ? 1 : 0;
|
|
3264
|
+
if (noMotion) {
|
|
3265
|
+
scrubAmountRef.current = scrubTarget;
|
|
3266
|
+
} else {
|
|
3267
|
+
scrubAmountRef.current += (scrubTarget - scrubAmountRef.current) * SCRUB_LERP_SPEED;
|
|
3268
|
+
if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
|
|
3269
|
+
if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
|
|
3270
|
+
}
|
|
3271
|
+
if (!isActiveHover && scrubAmountRef.current > 0 && lastHoverRef.current) {
|
|
3272
|
+
drawHoverX = lastHoverRef.current.x;
|
|
3273
|
+
drawHoverTime = lastHoverRef.current.time;
|
|
3274
|
+
hoverEntries = lastHoverEntriesRef.current;
|
|
3275
|
+
}
|
|
3276
|
+
drawMultiFrame(ctx, layout, {
|
|
3277
|
+
series: seriesEntries,
|
|
3278
|
+
now,
|
|
3279
|
+
showGrid: cfg.showGrid,
|
|
3280
|
+
showPulse: cfg.showPulse,
|
|
3281
|
+
referenceLine: cfg.referenceLine,
|
|
3282
|
+
hoverX: drawHoverX,
|
|
3283
|
+
hoverTime: drawHoverTime,
|
|
3284
|
+
hoverEntries,
|
|
3285
|
+
scrubAmount: scrubAmountRef.current,
|
|
3286
|
+
windowSecs,
|
|
3287
|
+
formatValue: cfg.formatValue,
|
|
3288
|
+
formatTime: cfg.formatTime,
|
|
3289
|
+
gridState: gridStateRef.current,
|
|
3290
|
+
timeAxisState: timeAxisStateRef.current,
|
|
3291
|
+
dt,
|
|
3292
|
+
targetWindowSecs: cfg.windowSecs,
|
|
3293
|
+
tooltipY: cfg.tooltipY,
|
|
3294
|
+
tooltipOutline: cfg.tooltipOutline,
|
|
3295
|
+
chartReveal,
|
|
3296
|
+
pauseProgress,
|
|
3297
|
+
now_ms,
|
|
3298
|
+
primaryPalette: cfg.palette
|
|
3299
|
+
});
|
|
3300
|
+
const bgAlpha = 1 - chartReveal;
|
|
3301
|
+
if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
|
|
3302
|
+
const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
|
|
3303
|
+
if (bgEmptyAlpha > 0.01) {
|
|
3304
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
|
|
1705
3305
|
}
|
|
1706
3306
|
}
|
|
1707
|
-
}
|
|
1708
|
-
const smoothValue = displayValueRef.current;
|
|
1709
|
-
const chartW = w - pad.left - pad.right;
|
|
1710
|
-
const needsArrowRoom = cfg.showMomentum;
|
|
1711
|
-
const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
|
|
1712
|
-
const transition = windowTransitionRef.current;
|
|
1713
|
-
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
1714
|
-
const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
1715
|
-
const windowResult = updateWindowTransition(
|
|
1716
|
-
cfg,
|
|
1717
|
-
transition,
|
|
1718
|
-
displayWindowRef.current,
|
|
1719
|
-
displayMinRef.current,
|
|
1720
|
-
displayMaxRef.current,
|
|
1721
|
-
noMotion,
|
|
1722
|
-
now_ms,
|
|
1723
|
-
now,
|
|
1724
|
-
effectivePoints,
|
|
1725
|
-
smoothValue,
|
|
1726
|
-
buffer
|
|
1727
|
-
);
|
|
1728
|
-
displayWindowRef.current = windowResult.windowSecs;
|
|
1729
|
-
const windowSecs = windowResult.windowSecs;
|
|
1730
|
-
const windowTransProgress = windowResult.windowTransProgress;
|
|
1731
|
-
const rightEdge = now + windowSecs * buffer;
|
|
1732
|
-
const leftEdge = rightEdge - windowSecs;
|
|
1733
|
-
const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
|
|
1734
|
-
const visible = [];
|
|
1735
|
-
for (const p of effectivePoints) {
|
|
1736
|
-
if (p.time >= leftEdge - 2 && p.time <= filterRight) {
|
|
1737
|
-
visible.push(p);
|
|
1738
|
-
}
|
|
1739
|
-
}
|
|
1740
|
-
if (visible.length < 2) {
|
|
1741
3307
|
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
|
|
1746
|
-
|
|
1747
|
-
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
);
|
|
1761
|
-
rangeInitedRef.current = rangeResult.rangeInited;
|
|
1762
|
-
targetMinRef.current = rangeResult.targetMin;
|
|
1763
|
-
targetMaxRef.current = rangeResult.targetMax;
|
|
1764
|
-
displayMinRef.current = rangeResult.displayMin;
|
|
1765
|
-
displayMaxRef.current = rangeResult.displayMax;
|
|
1766
|
-
const { minVal, maxVal, valRange } = rangeResult;
|
|
1767
|
-
const layout = {
|
|
1768
|
-
w,
|
|
1769
|
-
h,
|
|
1770
|
-
pad,
|
|
1771
|
-
chartW,
|
|
1772
|
-
chartH,
|
|
1773
|
-
leftEdge,
|
|
1774
|
-
rightEdge,
|
|
1775
|
-
minVal,
|
|
1776
|
-
maxVal,
|
|
1777
|
-
valRange,
|
|
1778
|
-
toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
|
|
1779
|
-
toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
|
|
1780
|
-
};
|
|
1781
|
-
const momentum = cfg.momentumOverride ?? detectMomentum(visible);
|
|
1782
|
-
const hoverResult = updateHoverState(
|
|
1783
|
-
hoverXRef.current,
|
|
1784
|
-
pad,
|
|
1785
|
-
w,
|
|
1786
|
-
layout,
|
|
1787
|
-
now,
|
|
1788
|
-
visible,
|
|
1789
|
-
scrubAmountRef.current,
|
|
1790
|
-
lastHoverRef.current,
|
|
1791
|
-
cfg,
|
|
1792
|
-
noMotion,
|
|
1793
|
-
leftEdge,
|
|
1794
|
-
rightEdge,
|
|
1795
|
-
chartW,
|
|
1796
|
-
dt
|
|
1797
|
-
);
|
|
1798
|
-
scrubAmountRef.current = hoverResult.scrubAmount;
|
|
1799
|
-
lastHoverRef.current = hoverResult.lastHover;
|
|
1800
|
-
const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
|
|
1801
|
-
const lookback = Math.min(5, visible.length - 1);
|
|
1802
|
-
const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
|
|
1803
|
-
const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
|
|
1804
|
-
drawFrame(ctx, layout, cfg.palette, {
|
|
1805
|
-
visible,
|
|
1806
|
-
smoothValue,
|
|
1807
|
-
now,
|
|
1808
|
-
momentum,
|
|
1809
|
-
arrowState: arrowStateRef.current,
|
|
1810
|
-
showGrid: cfg.showGrid,
|
|
1811
|
-
showMomentum: cfg.showMomentum,
|
|
1812
|
-
showPulse: cfg.showPulse,
|
|
1813
|
-
showFill: cfg.showFill,
|
|
1814
|
-
referenceLine: cfg.referenceLine,
|
|
1815
|
-
hoverX: drawHoverX,
|
|
1816
|
-
hoverValue: drawHoverValue,
|
|
1817
|
-
hoverTime: drawHoverTime,
|
|
1818
|
-
scrubAmount: scrubAmountRef.current,
|
|
1819
|
-
windowSecs,
|
|
1820
|
-
formatValue: cfg.formatValue,
|
|
1821
|
-
formatTime: cfg.formatTime,
|
|
1822
|
-
gridState: gridStateRef.current,
|
|
1823
|
-
timeAxisState: timeAxisStateRef.current,
|
|
1824
|
-
dt,
|
|
1825
|
-
targetWindowSecs: cfg.windowSecs,
|
|
1826
|
-
tooltipY: cfg.tooltipY,
|
|
1827
|
-
tooltipOutline: cfg.tooltipOutline,
|
|
1828
|
-
orderbookData: cfg.orderbookData,
|
|
1829
|
-
orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
|
|
1830
|
-
particleState: cfg.degenOptions ? particleStateRef.current : void 0,
|
|
1831
|
-
particleOptions: cfg.degenOptions,
|
|
1832
|
-
swingMagnitude,
|
|
1833
|
-
shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
|
|
1834
|
-
chartReveal,
|
|
1835
|
-
pauseProgress,
|
|
1836
|
-
now_ms
|
|
1837
|
-
});
|
|
1838
|
-
const bgAlpha = 1 - chartReveal;
|
|
1839
|
-
if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
|
|
1840
|
-
const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
|
|
1841
|
-
if (bgEmptyAlpha > 0.01) {
|
|
1842
|
-
drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
|
|
3308
|
+
} else {
|
|
3309
|
+
const effectivePoints = useStash ? lastDataRef.current : points;
|
|
3310
|
+
const adaptiveSpeed = computeAdaptiveSpeed(
|
|
3311
|
+
cfg.value,
|
|
3312
|
+
displayValueRef.current,
|
|
3313
|
+
displayMinRef.current,
|
|
3314
|
+
displayMaxRef.current,
|
|
3315
|
+
cfg.lerpSpeed,
|
|
3316
|
+
noMotion
|
|
3317
|
+
);
|
|
3318
|
+
if (!useStash) {
|
|
3319
|
+
displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, pausedDt);
|
|
3320
|
+
if (pauseProgress < 0.5) {
|
|
3321
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
3322
|
+
if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
|
|
3323
|
+
displayValueRef.current = cfg.value;
|
|
3324
|
+
}
|
|
3325
|
+
}
|
|
1843
3326
|
}
|
|
1844
|
-
|
|
1845
|
-
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
3327
|
+
const smoothValue = displayValueRef.current;
|
|
3328
|
+
const chartW = w - pad.left - pad.right;
|
|
3329
|
+
const needsArrowRoom = cfg.showMomentum;
|
|
3330
|
+
const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
|
|
3331
|
+
const transition = windowTransitionRef.current;
|
|
3332
|
+
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
3333
|
+
const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
3334
|
+
const windowResult = updateWindowTransition(
|
|
1849
3335
|
cfg,
|
|
3336
|
+
transition,
|
|
3337
|
+
displayWindowRef.current,
|
|
3338
|
+
displayMinRef.current,
|
|
3339
|
+
displayMaxRef.current,
|
|
3340
|
+
noMotion,
|
|
3341
|
+
now_ms,
|
|
3342
|
+
now,
|
|
3343
|
+
effectivePoints,
|
|
1850
3344
|
smoothValue,
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
3345
|
+
buffer
|
|
3346
|
+
);
|
|
3347
|
+
displayWindowRef.current = windowResult.windowSecs;
|
|
3348
|
+
const windowSecs = windowResult.windowSecs;
|
|
3349
|
+
const windowTransProgress = windowResult.windowTransProgress;
|
|
3350
|
+
const rightEdge = now + windowSecs * buffer;
|
|
3351
|
+
const leftEdge = rightEdge - windowSecs;
|
|
3352
|
+
const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
|
|
3353
|
+
const visible = [];
|
|
3354
|
+
for (const p of effectivePoints) {
|
|
3355
|
+
if (p.time >= leftEdge - 2 && p.time <= filterRight) {
|
|
3356
|
+
visible.push(p);
|
|
3357
|
+
}
|
|
3358
|
+
}
|
|
3359
|
+
if (visible.length < 2) {
|
|
3360
|
+
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
3361
|
+
rafRef.current = requestAnimationFrame(draw);
|
|
3362
|
+
return;
|
|
3363
|
+
}
|
|
3364
|
+
const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
|
|
3365
|
+
const isWindowTransitioning = transition.startMs > 0;
|
|
3366
|
+
const rangeResult = updateRange(
|
|
3367
|
+
computedRange,
|
|
3368
|
+
rangeInitedRef.current,
|
|
3369
|
+
targetMinRef.current,
|
|
3370
|
+
targetMaxRef.current,
|
|
3371
|
+
displayMinRef.current,
|
|
3372
|
+
displayMaxRef.current,
|
|
1855
3373
|
isWindowTransitioning,
|
|
3374
|
+
windowTransProgress,
|
|
3375
|
+
transition,
|
|
3376
|
+
adaptiveSpeed,
|
|
3377
|
+
chartH,
|
|
3378
|
+
pausedDt
|
|
3379
|
+
);
|
|
3380
|
+
rangeInitedRef.current = rangeResult.rangeInited;
|
|
3381
|
+
targetMinRef.current = rangeResult.targetMin;
|
|
3382
|
+
targetMaxRef.current = rangeResult.targetMax;
|
|
3383
|
+
displayMinRef.current = rangeResult.displayMin;
|
|
3384
|
+
displayMaxRef.current = rangeResult.displayMax;
|
|
3385
|
+
const { minVal, maxVal, valRange } = rangeResult;
|
|
3386
|
+
const layout = {
|
|
3387
|
+
w,
|
|
3388
|
+
h,
|
|
3389
|
+
pad,
|
|
3390
|
+
chartW,
|
|
3391
|
+
chartH,
|
|
3392
|
+
leftEdge,
|
|
3393
|
+
rightEdge,
|
|
3394
|
+
minVal,
|
|
3395
|
+
maxVal,
|
|
3396
|
+
valRange,
|
|
3397
|
+
toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
|
|
3398
|
+
toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
|
|
3399
|
+
};
|
|
3400
|
+
const momentum = cfg.momentumOverride ?? detectMomentum(visible);
|
|
3401
|
+
const hoverResult = updateHoverState(
|
|
3402
|
+
hoverXRef.current,
|
|
3403
|
+
pad,
|
|
3404
|
+
w,
|
|
3405
|
+
layout,
|
|
3406
|
+
now,
|
|
3407
|
+
visible,
|
|
3408
|
+
scrubAmountRef.current,
|
|
3409
|
+
lastHoverRef.current,
|
|
3410
|
+
cfg,
|
|
1856
3411
|
noMotion,
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
3412
|
+
leftEdge,
|
|
3413
|
+
rightEdge,
|
|
3414
|
+
chartW,
|
|
3415
|
+
dt
|
|
1860
3416
|
);
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
3417
|
+
scrubAmountRef.current = hoverResult.scrubAmount;
|
|
3418
|
+
lastHoverRef.current = hoverResult.lastHover;
|
|
3419
|
+
const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
|
|
3420
|
+
const lookback = Math.min(5, visible.length - 1);
|
|
3421
|
+
const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
|
|
3422
|
+
const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
|
|
3423
|
+
drawFrame(ctx, layout, cfg.palette, {
|
|
3424
|
+
visible,
|
|
3425
|
+
smoothValue,
|
|
3426
|
+
now,
|
|
3427
|
+
momentum,
|
|
3428
|
+
arrowState: arrowStateRef.current,
|
|
3429
|
+
showGrid: cfg.showGrid,
|
|
3430
|
+
showMomentum: cfg.showMomentum,
|
|
3431
|
+
showPulse: cfg.showPulse,
|
|
3432
|
+
showFill: cfg.showFill,
|
|
3433
|
+
referenceLine: cfg.referenceLine,
|
|
3434
|
+
hoverX: drawHoverX,
|
|
3435
|
+
hoverValue: drawHoverValue,
|
|
3436
|
+
hoverTime: drawHoverTime,
|
|
3437
|
+
scrubAmount: scrubAmountRef.current,
|
|
3438
|
+
windowSecs,
|
|
3439
|
+
formatValue: cfg.formatValue,
|
|
3440
|
+
formatTime: cfg.formatTime,
|
|
3441
|
+
gridState: gridStateRef.current,
|
|
3442
|
+
timeAxisState: timeAxisStateRef.current,
|
|
3443
|
+
dt,
|
|
3444
|
+
targetWindowSecs: cfg.windowSecs,
|
|
3445
|
+
tooltipY: cfg.tooltipY,
|
|
3446
|
+
tooltipOutline: cfg.tooltipOutline,
|
|
3447
|
+
orderbookData: cfg.orderbookData,
|
|
3448
|
+
orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
|
|
3449
|
+
particleState: cfg.degenOptions ? particleStateRef.current : void 0,
|
|
3450
|
+
particleOptions: cfg.degenOptions,
|
|
3451
|
+
swingMagnitude,
|
|
3452
|
+
shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
|
|
3453
|
+
chartReveal,
|
|
3454
|
+
pauseProgress,
|
|
3455
|
+
now_ms
|
|
3456
|
+
});
|
|
3457
|
+
const bgAlpha = 1 - chartReveal;
|
|
3458
|
+
if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
|
|
3459
|
+
const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
|
|
3460
|
+
if (bgEmptyAlpha > 0.01) {
|
|
3461
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
|
|
3462
|
+
}
|
|
1864
3463
|
}
|
|
1865
|
-
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
|
|
1873
|
-
|
|
3464
|
+
const badge = badgeRef.current;
|
|
3465
|
+
if (badge) {
|
|
3466
|
+
badgeYRef.current = updateBadgeDOM(
|
|
3467
|
+
badge,
|
|
3468
|
+
cfg,
|
|
3469
|
+
smoothValue,
|
|
3470
|
+
layout,
|
|
3471
|
+
momentum,
|
|
3472
|
+
badgeYRef.current,
|
|
3473
|
+
badgeColorRef.current,
|
|
3474
|
+
isWindowTransitioning,
|
|
3475
|
+
noMotion,
|
|
3476
|
+
ctx,
|
|
3477
|
+
pausedDt,
|
|
3478
|
+
chartReveal
|
|
3479
|
+
);
|
|
3480
|
+
if (pauseProgress > 0.01 && badge.container.style.display !== "none") {
|
|
3481
|
+
const base = badge.container.style.opacity ? parseFloat(badge.container.style.opacity) : 1;
|
|
3482
|
+
badge.container.style.opacity = String(base * (1 - pauseProgress));
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
const valEl = cfg.valueDisplayRef?.current;
|
|
3486
|
+
if (valEl) {
|
|
3487
|
+
const displayVal = cfg.valueMomentumColor ? Math.abs(smoothValue) : smoothValue;
|
|
3488
|
+
valEl.textContent = cfg.formatValue(displayVal);
|
|
3489
|
+
if (cfg.valueMomentumColor) {
|
|
3490
|
+
const mc = momentum === "up" ? "#22c55e" : momentum === "down" ? "#ef4444" : "";
|
|
3491
|
+
if (mc) valEl.style.color = mc;
|
|
3492
|
+
else valEl.style.removeProperty("color");
|
|
3493
|
+
}
|
|
1874
3494
|
}
|
|
1875
3495
|
}
|
|
1876
3496
|
rafRef.current = requestAnimationFrame(draw);
|
|
@@ -1894,6 +3514,7 @@ var defaultFormatTime = (t) => {
|
|
|
1894
3514
|
function Liveline({
|
|
1895
3515
|
data,
|
|
1896
3516
|
value,
|
|
3517
|
+
series: seriesProp,
|
|
1897
3518
|
theme = "dark",
|
|
1898
3519
|
color = "#3b82f6",
|
|
1899
3520
|
window: windowSecs = 30,
|
|
@@ -1925,6 +3546,16 @@ function Liveline({
|
|
|
1925
3546
|
onHover,
|
|
1926
3547
|
cursor = "crosshair",
|
|
1927
3548
|
pulse = true,
|
|
3549
|
+
mode = "line",
|
|
3550
|
+
candles,
|
|
3551
|
+
candleWidth,
|
|
3552
|
+
liveCandle,
|
|
3553
|
+
lineMode,
|
|
3554
|
+
lineData,
|
|
3555
|
+
lineValue,
|
|
3556
|
+
onModeChange,
|
|
3557
|
+
onSeriesToggle,
|
|
3558
|
+
seriesToggleCompact = false,
|
|
1928
3559
|
className,
|
|
1929
3560
|
style
|
|
1930
3561
|
}) {
|
|
@@ -1934,8 +3565,30 @@ function Liveline({
|
|
|
1934
3565
|
const windowBarRef = (0, import_react2.useRef)(null);
|
|
1935
3566
|
const windowBtnRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
|
|
1936
3567
|
const [indicatorStyle, setIndicatorStyle] = (0, import_react2.useState)(null);
|
|
3568
|
+
const modeBarRef = (0, import_react2.useRef)(null);
|
|
3569
|
+
const modeBtnRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
|
|
3570
|
+
const [modeIndicatorStyle, setModeIndicatorStyle] = (0, import_react2.useState)(null);
|
|
3571
|
+
const [hiddenSeries, setHiddenSeries] = (0, import_react2.useState)(/* @__PURE__ */ new Set());
|
|
3572
|
+
const lastSeriesPropRef = (0, import_react2.useRef)(seriesProp);
|
|
3573
|
+
if (seriesProp && seriesProp.length > 0) lastSeriesPropRef.current = seriesProp;
|
|
1937
3574
|
const palette = (0, import_react2.useMemo)(() => resolveTheme(color, theme), [color, theme]);
|
|
1938
3575
|
const isDark = theme === "dark";
|
|
3576
|
+
const isMultiSeries = seriesProp != null && seriesProp.length > 0;
|
|
3577
|
+
const showSeriesToggle = (lastSeriesPropRef.current?.length ?? 0) > 1;
|
|
3578
|
+
const seriesPalettes = (0, import_react2.useMemo)(() => {
|
|
3579
|
+
if (!seriesProp || seriesProp.length === 0) return null;
|
|
3580
|
+
return resolveSeriesPalettes(seriesProp, theme);
|
|
3581
|
+
}, [seriesProp, theme]);
|
|
3582
|
+
const multiSeries = (0, import_react2.useMemo)(() => {
|
|
3583
|
+
if (!seriesProp || !seriesPalettes) return void 0;
|
|
3584
|
+
return seriesProp.map((s, i) => ({
|
|
3585
|
+
id: s.id,
|
|
3586
|
+
data: s.data,
|
|
3587
|
+
value: s.value,
|
|
3588
|
+
palette: seriesPalettes.get(s.id) ?? resolveTheme(s.color || SERIES_COLORS[i % SERIES_COLORS.length], theme),
|
|
3589
|
+
label: s.label
|
|
3590
|
+
}));
|
|
3591
|
+
}, [seriesProp, seriesPalettes, theme]);
|
|
1939
3592
|
const showMomentum = momentum !== false;
|
|
1940
3593
|
const momentumOverride = typeof momentum === "string" ? momentum : void 0;
|
|
1941
3594
|
const pad = {
|
|
@@ -1963,6 +3616,36 @@ function Liveline({
|
|
|
1963
3616
|
});
|
|
1964
3617
|
}
|
|
1965
3618
|
}, [activeWindowSecs, windows]);
|
|
3619
|
+
const activeMode = lineMode ? "line" : "candle";
|
|
3620
|
+
(0, import_react2.useLayoutEffect)(() => {
|
|
3621
|
+
if (!onModeChange) return;
|
|
3622
|
+
const btn = modeBtnRefs.current.get(activeMode);
|
|
3623
|
+
const bar = modeBarRef.current;
|
|
3624
|
+
if (btn && bar) {
|
|
3625
|
+
const barRect = bar.getBoundingClientRect();
|
|
3626
|
+
const btnRect = btn.getBoundingClientRect();
|
|
3627
|
+
setModeIndicatorStyle({
|
|
3628
|
+
left: btnRect.left - barRect.left,
|
|
3629
|
+
width: btnRect.width
|
|
3630
|
+
});
|
|
3631
|
+
}
|
|
3632
|
+
}, [activeMode, onModeChange]);
|
|
3633
|
+
const handleSeriesToggle = (0, import_react2.useCallback)((id) => {
|
|
3634
|
+
setHiddenSeries((prev) => {
|
|
3635
|
+
const next = new Set(prev);
|
|
3636
|
+
if (next.has(id)) {
|
|
3637
|
+
next.delete(id);
|
|
3638
|
+
onSeriesToggle?.(id, true);
|
|
3639
|
+
} else {
|
|
3640
|
+
const totalSeries = seriesProp?.length ?? 0;
|
|
3641
|
+
const visibleCount = totalSeries - next.size;
|
|
3642
|
+
if (visibleCount <= 1) return prev;
|
|
3643
|
+
next.add(id);
|
|
3644
|
+
onSeriesToggle?.(id, false);
|
|
3645
|
+
}
|
|
3646
|
+
return next;
|
|
3647
|
+
});
|
|
3648
|
+
}, [seriesProp?.length, onSeriesToggle]);
|
|
1966
3649
|
const ws = windowStyle ?? "default";
|
|
1967
3650
|
useLivelineEngine(canvasRef, containerRef, {
|
|
1968
3651
|
data,
|
|
@@ -1971,10 +3654,10 @@ function Liveline({
|
|
|
1971
3654
|
windowSecs: effectiveWindowSecs,
|
|
1972
3655
|
lerpSpeed,
|
|
1973
3656
|
showGrid: grid,
|
|
1974
|
-
showBadge: badge,
|
|
1975
|
-
showMomentum,
|
|
3657
|
+
showBadge: isMultiSeries ? false : badge,
|
|
3658
|
+
showMomentum: isMultiSeries ? false : showMomentum,
|
|
1976
3659
|
momentumOverride,
|
|
1977
|
-
showFill: fill,
|
|
3660
|
+
showFill: isMultiSeries ? false : fill,
|
|
1978
3661
|
referenceLine,
|
|
1979
3662
|
formatValue,
|
|
1980
3663
|
formatTime,
|
|
@@ -1983,7 +3666,7 @@ function Liveline({
|
|
|
1983
3666
|
showPulse: pulse,
|
|
1984
3667
|
scrub,
|
|
1985
3668
|
exaggerate,
|
|
1986
|
-
degenOptions,
|
|
3669
|
+
degenOptions: isMultiSeries ? void 0 : degenOptions,
|
|
1987
3670
|
badgeTail,
|
|
1988
3671
|
badgeVariant,
|
|
1989
3672
|
tooltipY,
|
|
@@ -1993,9 +3676,21 @@ function Liveline({
|
|
|
1993
3676
|
orderbookData: orderbook,
|
|
1994
3677
|
loading,
|
|
1995
3678
|
paused,
|
|
1996
|
-
emptyText
|
|
3679
|
+
emptyText,
|
|
3680
|
+
mode,
|
|
3681
|
+
candles,
|
|
3682
|
+
candleWidth,
|
|
3683
|
+
liveCandle,
|
|
3684
|
+
lineMode,
|
|
3685
|
+
lineData,
|
|
3686
|
+
lineValue,
|
|
3687
|
+
multiSeries,
|
|
3688
|
+
isMultiSeries,
|
|
3689
|
+
hiddenSeriesIds: hiddenSeries
|
|
1997
3690
|
});
|
|
1998
3691
|
const cursorStyle = scrub ? cursor : "default";
|
|
3692
|
+
const activeColor = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)";
|
|
3693
|
+
const inactiveColor = isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.22)";
|
|
1999
3694
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
|
|
2000
3695
|
showValue && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2001
3696
|
"span",
|
|
@@ -2015,68 +3710,244 @@ function Liveline({
|
|
|
2015
3710
|
}
|
|
2016
3711
|
}
|
|
2017
3712
|
),
|
|
2018
|
-
windows && windows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
2022
|
-
|
|
2023
|
-
|
|
2024
|
-
|
|
2025
|
-
|
|
2026
|
-
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
3713
|
+
(windows && windows.length > 0 || onModeChange || showSeriesToggle) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
|
|
3714
|
+
windows && windows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3715
|
+
"div",
|
|
3716
|
+
{
|
|
3717
|
+
ref: windowBarRef,
|
|
3718
|
+
style: {
|
|
3719
|
+
position: "relative",
|
|
3720
|
+
display: "inline-flex",
|
|
3721
|
+
gap: ws === "text" ? 4 : 2,
|
|
3722
|
+
background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3723
|
+
borderRadius: ws === "rounded" ? 999 : 6,
|
|
3724
|
+
padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
|
|
3725
|
+
},
|
|
3726
|
+
children: [
|
|
3727
|
+
ws !== "text" && indicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
|
|
3728
|
+
position: "absolute",
|
|
3729
|
+
top: ws === "rounded" ? 3 : 2,
|
|
3730
|
+
left: indicatorStyle.left,
|
|
3731
|
+
width: indicatorStyle.width,
|
|
3732
|
+
height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
|
|
3733
|
+
background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
|
|
3734
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3735
|
+
transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
3736
|
+
pointerEvents: "none"
|
|
3737
|
+
} }),
|
|
3738
|
+
windows.map((w) => {
|
|
3739
|
+
const isActive = w.secs === activeWindowSecs;
|
|
3740
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3741
|
+
"button",
|
|
3742
|
+
{
|
|
3743
|
+
ref: (el) => {
|
|
3744
|
+
if (el) windowBtnRefs.current.set(w.secs, el);
|
|
3745
|
+
else windowBtnRefs.current.delete(w.secs);
|
|
3746
|
+
},
|
|
3747
|
+
onClick: () => {
|
|
3748
|
+
setActiveWindowSecs(w.secs);
|
|
3749
|
+
onWindowChange?.(w.secs);
|
|
3750
|
+
},
|
|
3751
|
+
style: {
|
|
3752
|
+
position: "relative",
|
|
3753
|
+
zIndex: 1,
|
|
3754
|
+
fontSize: 11,
|
|
3755
|
+
padding: ws === "text" ? "2px 6px" : "3px 10px",
|
|
3756
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3757
|
+
border: "none",
|
|
3758
|
+
cursor: "pointer",
|
|
3759
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
3760
|
+
fontWeight: isActive ? 600 : 400,
|
|
3761
|
+
background: "transparent",
|
|
3762
|
+
color: isActive ? activeColor : inactiveColor,
|
|
3763
|
+
transition: "color 0.2s, background 0.15s",
|
|
3764
|
+
lineHeight: "16px"
|
|
3765
|
+
},
|
|
3766
|
+
children: w.label
|
|
3767
|
+
},
|
|
3768
|
+
w.secs
|
|
3769
|
+
);
|
|
3770
|
+
})
|
|
3771
|
+
]
|
|
3772
|
+
}
|
|
3773
|
+
),
|
|
3774
|
+
onModeChange && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3775
|
+
"div",
|
|
3776
|
+
{
|
|
3777
|
+
ref: modeBarRef,
|
|
3778
|
+
style: {
|
|
3779
|
+
position: "relative",
|
|
3780
|
+
display: "inline-flex",
|
|
3781
|
+
gap: ws === "text" ? 4 : 2,
|
|
3782
|
+
background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3783
|
+
borderRadius: ws === "rounded" ? 999 : 6,
|
|
3784
|
+
padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
|
|
3785
|
+
},
|
|
3786
|
+
children: [
|
|
3787
|
+
ws !== "text" && modeIndicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
|
|
3788
|
+
position: "absolute",
|
|
3789
|
+
top: ws === "rounded" ? 3 : 2,
|
|
3790
|
+
left: modeIndicatorStyle.left,
|
|
3791
|
+
width: modeIndicatorStyle.width,
|
|
3792
|
+
height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
|
|
3793
|
+
background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
|
|
3794
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3795
|
+
transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
3796
|
+
pointerEvents: "none"
|
|
3797
|
+
} }),
|
|
3798
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2047
3799
|
"button",
|
|
2048
3800
|
{
|
|
2049
3801
|
ref: (el) => {
|
|
2050
|
-
if (el)
|
|
2051
|
-
else
|
|
3802
|
+
if (el) modeBtnRefs.current.set("line", el);
|
|
3803
|
+
else modeBtnRefs.current.delete("line");
|
|
2052
3804
|
},
|
|
2053
|
-
onClick: () =>
|
|
2054
|
-
|
|
2055
|
-
|
|
3805
|
+
onClick: () => onModeChange("line"),
|
|
3806
|
+
style: {
|
|
3807
|
+
position: "relative",
|
|
3808
|
+
zIndex: 1,
|
|
3809
|
+
padding: "5px 7px",
|
|
3810
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3811
|
+
border: "none",
|
|
3812
|
+
cursor: "pointer",
|
|
3813
|
+
background: "transparent",
|
|
3814
|
+
display: "flex",
|
|
3815
|
+
alignItems: "center"
|
|
2056
3816
|
},
|
|
3817
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3818
|
+
"path",
|
|
3819
|
+
{
|
|
3820
|
+
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",
|
|
3821
|
+
stroke: activeMode === "line" ? activeColor : inactiveColor,
|
|
3822
|
+
strokeWidth: activeMode === "line" ? 1.5 : 1.2,
|
|
3823
|
+
strokeLinecap: "round",
|
|
3824
|
+
fill: "none"
|
|
3825
|
+
}
|
|
3826
|
+
) })
|
|
3827
|
+
}
|
|
3828
|
+
),
|
|
3829
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3830
|
+
"button",
|
|
3831
|
+
{
|
|
3832
|
+
ref: (el) => {
|
|
3833
|
+
if (el) modeBtnRefs.current.set("candle", el);
|
|
3834
|
+
else modeBtnRefs.current.delete("candle");
|
|
3835
|
+
},
|
|
3836
|
+
onClick: () => onModeChange("candle"),
|
|
2057
3837
|
style: {
|
|
2058
3838
|
position: "relative",
|
|
2059
3839
|
zIndex: 1,
|
|
2060
|
-
|
|
2061
|
-
padding: ws === "text" ? "2px 6px" : "3px 10px",
|
|
3840
|
+
padding: "5px 7px",
|
|
2062
3841
|
borderRadius: ws === "rounded" ? 999 : 4,
|
|
2063
3842
|
border: "none",
|
|
2064
3843
|
cursor: "pointer",
|
|
2065
|
-
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
2066
|
-
fontWeight: isActive ? 600 : 400,
|
|
2067
3844
|
background: "transparent",
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
lineHeight: "16px"
|
|
3845
|
+
display: "flex",
|
|
3846
|
+
alignItems: "center"
|
|
2071
3847
|
},
|
|
2072
|
-
children:
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
3848
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: [
|
|
3849
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3850
|
+
"line",
|
|
3851
|
+
{
|
|
3852
|
+
x1: "3.5",
|
|
3853
|
+
y1: "1",
|
|
3854
|
+
x2: "3.5",
|
|
3855
|
+
y2: "11",
|
|
3856
|
+
stroke: activeMode === "candle" ? activeColor : inactiveColor,
|
|
3857
|
+
strokeWidth: "1"
|
|
3858
|
+
}
|
|
3859
|
+
),
|
|
3860
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3861
|
+
"rect",
|
|
3862
|
+
{
|
|
3863
|
+
x: "2",
|
|
3864
|
+
y: "3",
|
|
3865
|
+
width: "3",
|
|
3866
|
+
height: "5",
|
|
3867
|
+
rx: "0.5",
|
|
3868
|
+
fill: activeMode === "candle" ? activeColor : inactiveColor
|
|
3869
|
+
}
|
|
3870
|
+
),
|
|
3871
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3872
|
+
"line",
|
|
3873
|
+
{
|
|
3874
|
+
x1: "8.5",
|
|
3875
|
+
y1: "2",
|
|
3876
|
+
x2: "8.5",
|
|
3877
|
+
y2: "10",
|
|
3878
|
+
stroke: activeMode === "candle" ? activeColor : inactiveColor,
|
|
3879
|
+
strokeWidth: "1"
|
|
3880
|
+
}
|
|
3881
|
+
),
|
|
3882
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3883
|
+
"rect",
|
|
3884
|
+
{
|
|
3885
|
+
x: "7",
|
|
3886
|
+
y: "4",
|
|
3887
|
+
width: "3",
|
|
3888
|
+
height: "4",
|
|
3889
|
+
rx: "0.5",
|
|
3890
|
+
fill: activeMode === "candle" ? activeColor : inactiveColor
|
|
3891
|
+
}
|
|
3892
|
+
)
|
|
3893
|
+
] })
|
|
3894
|
+
}
|
|
3895
|
+
)
|
|
3896
|
+
]
|
|
3897
|
+
}
|
|
3898
|
+
),
|
|
3899
|
+
showSeriesToggle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
|
|
3900
|
+
display: "inline-flex",
|
|
3901
|
+
gap: ws === "text" ? 4 : 2,
|
|
3902
|
+
background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3903
|
+
borderRadius: ws === "rounded" ? 999 : 6,
|
|
3904
|
+
padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
|
|
3905
|
+
opacity: isMultiSeries ? 1 : 0,
|
|
3906
|
+
transition: "opacity 0.4s",
|
|
3907
|
+
pointerEvents: isMultiSeries ? "auto" : "none"
|
|
3908
|
+
}, children: (lastSeriesPropRef.current ?? []).map((s, si) => {
|
|
3909
|
+
const isHidden = hiddenSeries.has(s.id);
|
|
3910
|
+
const seriesColor = s.color || SERIES_COLORS[si % SERIES_COLORS.length];
|
|
3911
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3912
|
+
"button",
|
|
3913
|
+
{
|
|
3914
|
+
onClick: () => handleSeriesToggle(s.id),
|
|
3915
|
+
style: {
|
|
3916
|
+
position: "relative",
|
|
3917
|
+
zIndex: 1,
|
|
3918
|
+
fontSize: 11,
|
|
3919
|
+
padding: seriesToggleCompact ? ws === "text" ? "2px 4px" : "5px 7px" : ws === "text" ? "2px 6px" : "3px 8px",
|
|
3920
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3921
|
+
border: "none",
|
|
3922
|
+
cursor: "pointer",
|
|
3923
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
3924
|
+
fontWeight: 500,
|
|
3925
|
+
background: isHidden ? "transparent" : ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
|
|
3926
|
+
color: isHidden ? inactiveColor : activeColor,
|
|
3927
|
+
opacity: isHidden ? 0.4 : 1,
|
|
3928
|
+
transition: "opacity 0.2s, background 0.15s, color 0.2s",
|
|
3929
|
+
lineHeight: "16px",
|
|
3930
|
+
display: "flex",
|
|
3931
|
+
alignItems: "center",
|
|
3932
|
+
gap: seriesToggleCompact ? 0 : 4
|
|
3933
|
+
},
|
|
3934
|
+
children: [
|
|
3935
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
|
|
3936
|
+
width: seriesToggleCompact ? 8 : 6,
|
|
3937
|
+
height: seriesToggleCompact ? 8 : 6,
|
|
3938
|
+
borderRadius: "50%",
|
|
3939
|
+
background: seriesColor,
|
|
3940
|
+
flexShrink: 0,
|
|
3941
|
+
opacity: isHidden ? 0.4 : 1,
|
|
3942
|
+
transition: "opacity 0.2s"
|
|
3943
|
+
} }),
|
|
3944
|
+
!seriesToggleCompact && (s.label ?? s.id)
|
|
3945
|
+
]
|
|
3946
|
+
},
|
|
3947
|
+
s.id
|
|
3948
|
+
);
|
|
3949
|
+
}) })
|
|
3950
|
+
] }),
|
|
2080
3951
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
2081
3952
|
"div",
|
|
2082
3953
|
{
|
|
@@ -2099,7 +3970,64 @@ function Liveline({
|
|
|
2099
3970
|
)
|
|
2100
3971
|
] });
|
|
2101
3972
|
}
|
|
3973
|
+
|
|
3974
|
+
// src/LivelineTransition.tsx
|
|
3975
|
+
var import_react3 = require("react");
|
|
3976
|
+
var import_jsx_runtime2 = require("react/jsx-runtime");
|
|
3977
|
+
function LivelineTransition({
|
|
3978
|
+
active,
|
|
3979
|
+
children,
|
|
3980
|
+
duration = 300,
|
|
3981
|
+
className,
|
|
3982
|
+
style
|
|
3983
|
+
}) {
|
|
3984
|
+
const childArray = Array.isArray(children) ? children : [children];
|
|
3985
|
+
const [mounted, setMounted] = (0, import_react3.useState)(() => /* @__PURE__ */ new Set([active]));
|
|
3986
|
+
const [visible, setVisible] = (0, import_react3.useState)(active);
|
|
3987
|
+
const prevRef = (0, import_react3.useRef)(active);
|
|
3988
|
+
(0, import_react3.useEffect)(() => {
|
|
3989
|
+
if (active === prevRef.current) return () => {
|
|
3990
|
+
};
|
|
3991
|
+
const oldKey = prevRef.current;
|
|
3992
|
+
prevRef.current = active;
|
|
3993
|
+
setMounted((prev) => /* @__PURE__ */ new Set([...prev, active]));
|
|
3994
|
+
let raf1 = requestAnimationFrame(() => {
|
|
3995
|
+
raf1 = requestAnimationFrame(() => setVisible(active));
|
|
3996
|
+
});
|
|
3997
|
+
const timer = setTimeout(() => {
|
|
3998
|
+
setMounted((prev) => {
|
|
3999
|
+
const next = new Set(prev);
|
|
4000
|
+
next.delete(oldKey);
|
|
4001
|
+
return next;
|
|
4002
|
+
});
|
|
4003
|
+
}, duration + 50);
|
|
4004
|
+
return () => {
|
|
4005
|
+
cancelAnimationFrame(raf1);
|
|
4006
|
+
clearTimeout(timer);
|
|
4007
|
+
};
|
|
4008
|
+
}, [active, duration]);
|
|
4009
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { className, style: { position: "relative", width: "100%", height: "100%", ...style }, children: childArray.map((child) => {
|
|
4010
|
+
const key = String(child.key ?? "");
|
|
4011
|
+
if (!mounted.has(key)) return null;
|
|
4012
|
+
const isActive = key === visible;
|
|
4013
|
+
return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
|
|
4014
|
+
"div",
|
|
4015
|
+
{
|
|
4016
|
+
style: {
|
|
4017
|
+
position: "absolute",
|
|
4018
|
+
inset: 0,
|
|
4019
|
+
opacity: isActive ? 1 : 0,
|
|
4020
|
+
transition: `opacity ${duration}ms ease`,
|
|
4021
|
+
pointerEvents: isActive ? "auto" : "none"
|
|
4022
|
+
},
|
|
4023
|
+
children: child
|
|
4024
|
+
},
|
|
4025
|
+
key
|
|
4026
|
+
);
|
|
4027
|
+
}) });
|
|
4028
|
+
}
|
|
2102
4029
|
// Annotate the CommonJS export names for ESM import in node:
|
|
2103
4030
|
0 && (module.exports = {
|
|
2104
|
-
Liveline
|
|
4031
|
+
Liveline,
|
|
4032
|
+
LivelineTransition
|
|
2105
4033
|
});
|