liveline 0.0.5 → 0.0.7
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 +38 -2
- package/dist/index.cjs +661 -30
- package/dist/index.d.cts +13 -2
- package/dist/index.d.ts +13 -2
- package/dist/index.js +662 -31
- package/package.json +5 -3
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/Liveline.tsx
|
|
2
|
-
import { useRef as useRef2, useState, useLayoutEffect, useMemo } from "react";
|
|
2
|
+
import { useRef as useRef2, useState, useLayoutEffect, useMemo, useCallback as useCallback2 } from "react";
|
|
3
3
|
|
|
4
4
|
// src/theme.ts
|
|
5
5
|
function parseColorRgb(color) {
|
|
@@ -61,6 +61,33 @@ function resolveTheme(color, mode) {
|
|
|
61
61
|
badgeFont: '500 11px "SF Mono", Menlo, monospace'
|
|
62
62
|
};
|
|
63
63
|
}
|
|
64
|
+
var SERIES_COLORS = [
|
|
65
|
+
"#3b82f6",
|
|
66
|
+
// blue
|
|
67
|
+
"#ef4444",
|
|
68
|
+
// red
|
|
69
|
+
"#22c55e",
|
|
70
|
+
// green
|
|
71
|
+
"#f59e0b",
|
|
72
|
+
// amber
|
|
73
|
+
"#8b5cf6",
|
|
74
|
+
// violet
|
|
75
|
+
"#ec4899",
|
|
76
|
+
// pink
|
|
77
|
+
"#06b6d4",
|
|
78
|
+
// cyan
|
|
79
|
+
"#f97316"
|
|
80
|
+
// orange
|
|
81
|
+
];
|
|
82
|
+
function resolveSeriesPalettes(series, mode) {
|
|
83
|
+
const map = /* @__PURE__ */ new Map();
|
|
84
|
+
for (let i = 0; i < series.length; i++) {
|
|
85
|
+
const s = series[i];
|
|
86
|
+
const color = s.color || SERIES_COLORS[i % SERIES_COLORS.length];
|
|
87
|
+
map.set(s.id, resolveTheme(color, mode));
|
|
88
|
+
}
|
|
89
|
+
return map;
|
|
90
|
+
}
|
|
64
91
|
|
|
65
92
|
// src/useLivelineEngine.ts
|
|
66
93
|
import { useRef, useEffect, useCallback } from "react";
|
|
@@ -486,6 +513,33 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = per
|
|
|
486
513
|
}
|
|
487
514
|
ctx.fill();
|
|
488
515
|
}
|
|
516
|
+
function drawMultiDot(ctx, x, y, color, pulse = true, now_ms = performance.now(), radius = 3) {
|
|
517
|
+
const baseAlpha = ctx.globalAlpha;
|
|
518
|
+
if (pulse) {
|
|
519
|
+
const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
|
|
520
|
+
if (t < 1) {
|
|
521
|
+
const ringRadius = 9 + t * 10;
|
|
522
|
+
const pulseAlpha = 0.3 * (1 - t);
|
|
523
|
+
ctx.beginPath();
|
|
524
|
+
ctx.arc(x, y, ringRadius, 0, Math.PI * 2);
|
|
525
|
+
ctx.strokeStyle = color;
|
|
526
|
+
ctx.lineWidth = 1.5;
|
|
527
|
+
ctx.globalAlpha = baseAlpha * pulseAlpha;
|
|
528
|
+
ctx.stroke();
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
ctx.globalAlpha = baseAlpha;
|
|
532
|
+
ctx.beginPath();
|
|
533
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
534
|
+
ctx.fillStyle = color;
|
|
535
|
+
ctx.fill();
|
|
536
|
+
}
|
|
537
|
+
function drawSimpleDot(ctx, x, y, color, radius = 3) {
|
|
538
|
+
ctx.beginPath();
|
|
539
|
+
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
540
|
+
ctx.fillStyle = color;
|
|
541
|
+
ctx.fill();
|
|
542
|
+
}
|
|
489
543
|
function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
|
|
490
544
|
const baseAlpha = ctx.globalAlpha;
|
|
491
545
|
const upTarget = momentum === "up" ? 1 : 0;
|
|
@@ -584,6 +638,90 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
|
|
|
584
638
|
ctx.fillText(separator + timeText, tx + valueW, ty);
|
|
585
639
|
ctx.restore();
|
|
586
640
|
}
|
|
641
|
+
function drawMultiCrosshair(ctx, layout, palette, hoverX, hoverTime, entries, formatValue, formatTime, scrubOpacity, tooltipY, tooltipOutline, liveDotX) {
|
|
642
|
+
if (scrubOpacity < 0.01 || entries.length === 0) return;
|
|
643
|
+
const { h, pad, toY } = layout;
|
|
644
|
+
ctx.save();
|
|
645
|
+
ctx.globalAlpha = scrubOpacity * 0.5;
|
|
646
|
+
ctx.strokeStyle = palette.crosshairLine;
|
|
647
|
+
ctx.lineWidth = 1;
|
|
648
|
+
ctx.beginPath();
|
|
649
|
+
ctx.moveTo(hoverX, pad.top);
|
|
650
|
+
ctx.lineTo(hoverX, h - pad.bottom);
|
|
651
|
+
ctx.stroke();
|
|
652
|
+
ctx.restore();
|
|
653
|
+
const dotRadius = 4 * Math.min(scrubOpacity * 3, 1);
|
|
654
|
+
if (dotRadius > 0.5) {
|
|
655
|
+
ctx.globalAlpha = 1;
|
|
656
|
+
for (const entry of entries) {
|
|
657
|
+
const y = toY(entry.value);
|
|
658
|
+
ctx.beginPath();
|
|
659
|
+
ctx.arc(hoverX, y, dotRadius, 0, Math.PI * 2);
|
|
660
|
+
ctx.fillStyle = entry.color;
|
|
661
|
+
ctx.fill();
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (scrubOpacity < 0.1 || layout.w < 300) return;
|
|
665
|
+
ctx.save();
|
|
666
|
+
ctx.globalAlpha = scrubOpacity;
|
|
667
|
+
ctx.font = '400 13px "SF Mono", Menlo, monospace';
|
|
668
|
+
ctx.textAlign = "left";
|
|
669
|
+
const timeText = formatTime(hoverTime);
|
|
670
|
+
const sep = " \xB7 ";
|
|
671
|
+
const dotInline = " ";
|
|
672
|
+
const segments = [
|
|
673
|
+
{ text: timeText, color: palette.gridLabel }
|
|
674
|
+
];
|
|
675
|
+
for (const e of entries) {
|
|
676
|
+
segments.push({ text: sep, color: palette.gridLabel });
|
|
677
|
+
segments.push({ text: dotInline, color: e.color, isDot: true });
|
|
678
|
+
const label = e.label ? `${e.label} ` : "";
|
|
679
|
+
if (label) segments.push({ text: label, color: palette.gridLabel });
|
|
680
|
+
segments.push({ text: formatValue(e.value), color: palette.tooltipText });
|
|
681
|
+
}
|
|
682
|
+
let totalW = 0;
|
|
683
|
+
const segWidths = [];
|
|
684
|
+
for (const seg of segments) {
|
|
685
|
+
const w = seg.isDot ? 12 : ctx.measureText(seg.text).width;
|
|
686
|
+
segWidths.push(w);
|
|
687
|
+
totalW += w;
|
|
688
|
+
}
|
|
689
|
+
let tx = hoverX - totalW / 2;
|
|
690
|
+
const minX = pad.left + 4;
|
|
691
|
+
const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
|
|
692
|
+
const maxX = dotRightEdge - totalW;
|
|
693
|
+
if (tx < minX) tx = minX;
|
|
694
|
+
if (tx > maxX) tx = maxX;
|
|
695
|
+
const ty = pad.top + (tooltipY ?? 14) + 10;
|
|
696
|
+
if (tooltipOutline !== false) {
|
|
697
|
+
ctx.strokeStyle = palette.tooltipBg;
|
|
698
|
+
ctx.lineWidth = 3;
|
|
699
|
+
ctx.lineJoin = "round";
|
|
700
|
+
let ox2 = tx;
|
|
701
|
+
for (let i = 0; i < segments.length; i++) {
|
|
702
|
+
const seg = segments[i];
|
|
703
|
+
if (!seg.isDot) {
|
|
704
|
+
ctx.strokeText(seg.text, ox2, ty);
|
|
705
|
+
}
|
|
706
|
+
ox2 += segWidths[i];
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
let ox = tx;
|
|
710
|
+
for (let i = 0; i < segments.length; i++) {
|
|
711
|
+
const seg = segments[i];
|
|
712
|
+
if (seg.isDot) {
|
|
713
|
+
ctx.beginPath();
|
|
714
|
+
ctx.arc(ox + 4, ty - 4, 3.5, 0, Math.PI * 2);
|
|
715
|
+
ctx.fillStyle = seg.color;
|
|
716
|
+
ctx.fill();
|
|
717
|
+
} else {
|
|
718
|
+
ctx.fillStyle = seg.color;
|
|
719
|
+
ctx.fillText(seg.text, ox, ty);
|
|
720
|
+
}
|
|
721
|
+
ox += segWidths[i];
|
|
722
|
+
}
|
|
723
|
+
ctx.restore();
|
|
724
|
+
}
|
|
587
725
|
|
|
588
726
|
// src/draw/referenceLine.ts
|
|
589
727
|
function drawReferenceLine(ctx, layout, palette, ref) {
|
|
@@ -1430,6 +1568,124 @@ function drawFrame(ctx, layout, palette, opts) {
|
|
|
1430
1568
|
ctx.restore();
|
|
1431
1569
|
}
|
|
1432
1570
|
}
|
|
1571
|
+
function drawMultiFrame(ctx, layout, opts) {
|
|
1572
|
+
const palette = opts.primaryPalette;
|
|
1573
|
+
const reveal = opts.chartReveal;
|
|
1574
|
+
const revealRamp = (start, end) => {
|
|
1575
|
+
const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
|
|
1576
|
+
return t * t * (3 - 2 * t);
|
|
1577
|
+
};
|
|
1578
|
+
if (opts.referenceLine && reveal > 0.01) {
|
|
1579
|
+
ctx.save();
|
|
1580
|
+
if (reveal < 1) ctx.globalAlpha = reveal;
|
|
1581
|
+
drawReferenceLine(ctx, layout, palette, opts.referenceLine);
|
|
1582
|
+
ctx.restore();
|
|
1583
|
+
}
|
|
1584
|
+
if (opts.showGrid) {
|
|
1585
|
+
const gridAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
|
|
1586
|
+
if (gridAlpha > 0.01) {
|
|
1587
|
+
ctx.save();
|
|
1588
|
+
if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
|
|
1589
|
+
drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
|
|
1590
|
+
ctx.restore();
|
|
1591
|
+
}
|
|
1592
|
+
}
|
|
1593
|
+
const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
|
|
1594
|
+
const allPts = [];
|
|
1595
|
+
for (let si = 0; si < opts.series.length; si++) {
|
|
1596
|
+
const s = opts.series[si];
|
|
1597
|
+
const seriesAlpha = s.alpha ?? 1;
|
|
1598
|
+
const secondaryFade = si > 0 && reveal < 1 ? Math.min(1, reveal * 2) : 1;
|
|
1599
|
+
const combinedAlpha = secondaryFade * seriesAlpha;
|
|
1600
|
+
if (combinedAlpha < 0.01) continue;
|
|
1601
|
+
ctx.save();
|
|
1602
|
+
if (combinedAlpha < 1) ctx.globalAlpha = combinedAlpha;
|
|
1603
|
+
const pts = drawLine(
|
|
1604
|
+
ctx,
|
|
1605
|
+
layout,
|
|
1606
|
+
s.palette,
|
|
1607
|
+
s.visible,
|
|
1608
|
+
s.smoothValue,
|
|
1609
|
+
opts.now,
|
|
1610
|
+
false,
|
|
1611
|
+
// no fill
|
|
1612
|
+
scrubX,
|
|
1613
|
+
opts.scrubAmount,
|
|
1614
|
+
reveal,
|
|
1615
|
+
opts.now_ms
|
|
1616
|
+
);
|
|
1617
|
+
ctx.restore();
|
|
1618
|
+
if (pts && pts.length > 0) {
|
|
1619
|
+
allPts.push({ pts, palette: s.palette, label: s.label, alpha: seriesAlpha });
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1622
|
+
{
|
|
1623
|
+
const timeAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
|
|
1624
|
+
if (timeAlpha > 0.01) {
|
|
1625
|
+
ctx.save();
|
|
1626
|
+
if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
|
|
1627
|
+
drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
|
|
1628
|
+
ctx.restore();
|
|
1629
|
+
}
|
|
1630
|
+
}
|
|
1631
|
+
if (reveal > 0.3 && allPts.length > 0) {
|
|
1632
|
+
const dotAlpha = (reveal - 0.3) / 0.7;
|
|
1633
|
+
const showPulse = opts.showPulse && reveal > 0.6 && opts.pauseProgress < 0.5;
|
|
1634
|
+
for (const entry of allPts) {
|
|
1635
|
+
if (entry.alpha < 0.01) continue;
|
|
1636
|
+
const lastPt = entry.pts[entry.pts.length - 1];
|
|
1637
|
+
ctx.save();
|
|
1638
|
+
ctx.globalAlpha = dotAlpha * entry.alpha;
|
|
1639
|
+
if (showPulse && entry.alpha > 0.5) {
|
|
1640
|
+
drawMultiDot(ctx, lastPt[0], lastPt[1], entry.palette.line, true, opts.now_ms, 3);
|
|
1641
|
+
} else {
|
|
1642
|
+
drawSimpleDot(ctx, lastPt[0], lastPt[1], entry.palette.line, 3);
|
|
1643
|
+
}
|
|
1644
|
+
if (entry.label) {
|
|
1645
|
+
ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
|
|
1646
|
+
ctx.textAlign = "left";
|
|
1647
|
+
ctx.fillStyle = entry.palette.line;
|
|
1648
|
+
ctx.fillText(entry.label, lastPt[0] + 6, lastPt[1] + 3.5);
|
|
1649
|
+
}
|
|
1650
|
+
ctx.restore();
|
|
1651
|
+
}
|
|
1652
|
+
}
|
|
1653
|
+
ctx.save();
|
|
1654
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
1655
|
+
const fadeGrad = ctx.createLinearGradient(layout.pad.left, 0, layout.pad.left + FADE_EDGE_WIDTH, 0);
|
|
1656
|
+
fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
|
|
1657
|
+
fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
1658
|
+
ctx.fillStyle = fadeGrad;
|
|
1659
|
+
ctx.fillRect(0, 0, layout.pad.left + FADE_EDGE_WIDTH, layout.h);
|
|
1660
|
+
ctx.restore();
|
|
1661
|
+
if (opts.hoverX !== null && opts.hoverTime !== null && opts.hoverEntries.length > 0 && allPts.length > 0 && opts.scrubAmount > 0.01) {
|
|
1662
|
+
let maxLiveDotX = 0;
|
|
1663
|
+
for (const entry of allPts) {
|
|
1664
|
+
if (entry.alpha < 0.01) continue;
|
|
1665
|
+
const lastX = entry.pts[entry.pts.length - 1][0];
|
|
1666
|
+
if (lastX > maxLiveDotX) maxLiveDotX = lastX;
|
|
1667
|
+
}
|
|
1668
|
+
const distToLive = maxLiveDotX - opts.hoverX;
|
|
1669
|
+
const fadeStart = Math.min(80, layout.chartW * 0.3);
|
|
1670
|
+
const scrubOpacity = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
|
|
1671
|
+
if (scrubOpacity > 0.01) {
|
|
1672
|
+
drawMultiCrosshair(
|
|
1673
|
+
ctx,
|
|
1674
|
+
layout,
|
|
1675
|
+
palette,
|
|
1676
|
+
opts.hoverX,
|
|
1677
|
+
opts.hoverTime,
|
|
1678
|
+
opts.hoverEntries,
|
|
1679
|
+
opts.formatValue,
|
|
1680
|
+
opts.formatTime,
|
|
1681
|
+
scrubOpacity,
|
|
1682
|
+
opts.tooltipY,
|
|
1683
|
+
opts.tooltipOutline,
|
|
1684
|
+
maxLiveDotX
|
|
1685
|
+
);
|
|
1686
|
+
}
|
|
1687
|
+
}
|
|
1688
|
+
}
|
|
1433
1689
|
function drawCandleFrame(ctx, layout, palette, opts) {
|
|
1434
1690
|
const { w, h, pad, chartW, chartH } = layout;
|
|
1435
1691
|
const reveal = opts.chartReveal;
|
|
@@ -1702,6 +1958,7 @@ var BADGE_Y_LERP_TRANSITIONING = 0.5;
|
|
|
1702
1958
|
var MOMENTUM_COLOR_LERP = 0.12;
|
|
1703
1959
|
var WINDOW_TRANSITION_MS = 750;
|
|
1704
1960
|
var WINDOW_BUFFER = 0.05;
|
|
1961
|
+
var WINDOW_BUFFER_NO_BADGE = 0.015;
|
|
1705
1962
|
var VALUE_SNAP_THRESHOLD = 1e-3;
|
|
1706
1963
|
var ADAPTIVE_SPEED_BOOST = 0.2;
|
|
1707
1964
|
var MOMENTUM_GREEN = [34, 197, 94];
|
|
@@ -1712,6 +1969,7 @@ var PAUSE_PROGRESS_SPEED = 0.12;
|
|
|
1712
1969
|
var PAUSE_CATCHUP_SPEED = 0.08;
|
|
1713
1970
|
var PAUSE_CATCHUP_SPEED_FAST = 0.22;
|
|
1714
1971
|
var LOADING_ALPHA_SPEED = 0.14;
|
|
1972
|
+
var SERIES_TOGGLE_SPEED = 0.1;
|
|
1715
1973
|
var CANDLE_LERP_SPEED = 0.25;
|
|
1716
1974
|
var CANDLE_WIDTH_TRANS_MS = 300;
|
|
1717
1975
|
var LINE_MORPH_MS = 500;
|
|
@@ -1722,7 +1980,7 @@ var LINE_ADAPTIVE_BOOST = 0.2;
|
|
|
1722
1980
|
var LINE_SNAP_THRESHOLD = 1e-3;
|
|
1723
1981
|
var RANGE_LERP_SPEED = 0.15;
|
|
1724
1982
|
var RANGE_ADAPTIVE_BOOST = 0.2;
|
|
1725
|
-
var
|
|
1983
|
+
var CANDLE_BUFFER_NO_BADGE = 0.015;
|
|
1726
1984
|
function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
|
|
1727
1985
|
const valGap = Math.abs(value - displayValue);
|
|
1728
1986
|
const prevRange = displayMax - displayMin || 1;
|
|
@@ -2035,6 +2293,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2035
2293
|
const configRef = useRef(config);
|
|
2036
2294
|
configRef.current = config;
|
|
2037
2295
|
const displayValueRef = useRef(config.value);
|
|
2296
|
+
const displayValuesRef = useRef(/* @__PURE__ */ new Map());
|
|
2297
|
+
const seriesAlphaRef = useRef(/* @__PURE__ */ new Map());
|
|
2038
2298
|
const displayMinRef = useRef(0);
|
|
2039
2299
|
const displayMaxRef = useRef(0);
|
|
2040
2300
|
const targetMinRef = useRef(0);
|
|
@@ -2067,12 +2327,15 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2067
2327
|
const hoverXRef = useRef(null);
|
|
2068
2328
|
const scrubAmountRef = useRef(0);
|
|
2069
2329
|
const lastHoverRef = useRef(null);
|
|
2330
|
+
const lastHoverEntriesRef = useRef([]);
|
|
2070
2331
|
const chartRevealRef = useRef(0);
|
|
2071
2332
|
const pauseProgressRef = useRef(0);
|
|
2072
2333
|
const timeDebtRef = useRef(0);
|
|
2073
2334
|
const lastDataRef = useRef([]);
|
|
2335
|
+
const lastMultiSeriesRef = useRef([]);
|
|
2074
2336
|
const frozenNowRef = useRef(0);
|
|
2075
2337
|
const pausedDataRef = useRef(null);
|
|
2338
|
+
const pausedMultiDataRef = useRef(null);
|
|
2076
2339
|
const loadingAlphaRef = useRef(config.loading ? 1 : 0);
|
|
2077
2340
|
const displayCandleRef = useRef(null);
|
|
2078
2341
|
const liveBirthAlphaRef = useRef(1);
|
|
@@ -2252,6 +2515,17 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2252
2515
|
pausedLineDataRef.current = null;
|
|
2253
2516
|
pausedLineValueRef.current = null;
|
|
2254
2517
|
}
|
|
2518
|
+
} else if (cfg.isMultiSeries && cfg.multiSeries) {
|
|
2519
|
+
if (cfg.paused && pausedMultiDataRef.current === null) {
|
|
2520
|
+
const snap = /* @__PURE__ */ new Map();
|
|
2521
|
+
for (const s of cfg.multiSeries) {
|
|
2522
|
+
if (s.data.length >= 2) snap.set(s.id, { data: s.data.slice(), value: s.value });
|
|
2523
|
+
}
|
|
2524
|
+
if (snap.size > 0) pausedMultiDataRef.current = snap;
|
|
2525
|
+
}
|
|
2526
|
+
if (!cfg.paused) {
|
|
2527
|
+
pausedMultiDataRef.current = null;
|
|
2528
|
+
}
|
|
2255
2529
|
} else {
|
|
2256
2530
|
if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
|
|
2257
2531
|
pausedDataRef.current = cfg.data.slice();
|
|
@@ -2262,7 +2536,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2262
2536
|
}
|
|
2263
2537
|
const points = isCandle ? [] : pausedDataRef.current ?? cfg.data;
|
|
2264
2538
|
const effectiveCandles = isCandle ? pausedCandlesRef.current ?? (cfg.candles ?? []) : [];
|
|
2265
|
-
const
|
|
2539
|
+
const hasMultiData = cfg.isMultiSeries && cfg.multiSeries ? cfg.multiSeries.some((s) => s.data.length >= 2) : false;
|
|
2540
|
+
const hasData = isCandle ? effectiveCandles.length >= 2 : hasMultiData || points.length >= 2;
|
|
2266
2541
|
const pad = cfg.padding;
|
|
2267
2542
|
const chartH = h - pad.top - pad.bottom;
|
|
2268
2543
|
const pauseTarget = cfg.paused ? 1 : 0;
|
|
@@ -2298,11 +2573,23 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2298
2573
|
rangeInitedRef.current = false;
|
|
2299
2574
|
}
|
|
2300
2575
|
let useStash;
|
|
2576
|
+
let useMultiStash = false;
|
|
2301
2577
|
if (isCandle) {
|
|
2302
2578
|
useStash = !hasData && chartReveal > 5e-3 && lastCandlesRef.current.length > 0;
|
|
2303
2579
|
} else {
|
|
2304
|
-
|
|
2305
|
-
if (
|
|
2580
|
+
useMultiStash = !hasData && chartReveal > 5e-3 && lastMultiSeriesRef.current.length > 0;
|
|
2581
|
+
if (hasMultiData && cfg.multiSeries) {
|
|
2582
|
+
lastMultiSeriesRef.current = cfg.multiSeries.map((s) => ({
|
|
2583
|
+
id: s.id,
|
|
2584
|
+
data: s.data.slice(),
|
|
2585
|
+
value: s.value,
|
|
2586
|
+
palette: s.palette,
|
|
2587
|
+
label: s.label
|
|
2588
|
+
}));
|
|
2589
|
+
}
|
|
2590
|
+
if (hasData && !cfg.isMultiSeries) lastMultiSeriesRef.current = [];
|
|
2591
|
+
useStash = !useMultiStash && !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
|
|
2592
|
+
if (hasData && !cfg.isMultiSeries) lastDataRef.current = points;
|
|
2306
2593
|
}
|
|
2307
2594
|
if (isCandle) {
|
|
2308
2595
|
const lmt = lineModeTransRef.current;
|
|
@@ -2324,8 +2611,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2324
2611
|
lineModeProgRef.current = lmt.to;
|
|
2325
2612
|
}
|
|
2326
2613
|
}
|
|
2327
|
-
if (!hasData && !useStash) {
|
|
2328
|
-
const loadingColor = isCandle ? cfg.palette.gridLabel : void 0;
|
|
2614
|
+
if (!hasData && !useStash && !useMultiStash) {
|
|
2615
|
+
const loadingColor = isCandle || cfg.isMultiSeries || lastMultiSeriesRef.current.length > 0 ? cfg.palette.gridLabel : void 0;
|
|
2329
2616
|
if (loadingAlpha > 0.01) {
|
|
2330
2617
|
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, loadingColor);
|
|
2331
2618
|
}
|
|
@@ -2345,6 +2632,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2345
2632
|
return;
|
|
2346
2633
|
}
|
|
2347
2634
|
if (isCandle) {
|
|
2635
|
+
const candleBuffer = CANDLE_BUFFER_NO_BADGE;
|
|
2348
2636
|
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
2349
2637
|
const now = hasData || chartReveal < 5e-3 ? Date.now() / 1e3 - timeDebtRef.current : frozenNowRef.current;
|
|
2350
2638
|
const rawLive = pausedCandlesRef.current ? pausedLiveRef.current ?? void 0 : cfg.liveCandle;
|
|
@@ -2387,7 +2675,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2387
2675
|
cwt.rangeFromMin = displayMinRef.current;
|
|
2388
2676
|
cwt.rangeFromMax = displayMaxRef.current;
|
|
2389
2677
|
const curWindow = displayWindowRef.current;
|
|
2390
|
-
const re = now + curWindow *
|
|
2678
|
+
const re = now + curWindow * candleBuffer;
|
|
2391
2679
|
const le = re - curWindow;
|
|
2392
2680
|
const targetVis = [];
|
|
2393
2681
|
for (const c of effectiveCandles) {
|
|
@@ -2438,13 +2726,13 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2438
2726
|
effectiveCandles,
|
|
2439
2727
|
rawLive,
|
|
2440
2728
|
candleWidthSecs,
|
|
2441
|
-
|
|
2729
|
+
candleBuffer
|
|
2442
2730
|
);
|
|
2443
2731
|
displayWindowRef.current = windowResult.windowSecs;
|
|
2444
2732
|
const windowSecs = windowResult.windowSecs;
|
|
2445
2733
|
const windowTransProgress = windowResult.windowTransProgress;
|
|
2446
2734
|
const isWindowTransitioning = transition.startMs > 0;
|
|
2447
|
-
const rightEdge = now + windowSecs *
|
|
2735
|
+
const rightEdge = now + windowSecs * candleBuffer;
|
|
2448
2736
|
const leftEdge = rightEdge - windowSecs;
|
|
2449
2737
|
let smoothLive;
|
|
2450
2738
|
if (rawLive) {
|
|
@@ -2748,6 +3036,250 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2748
3036
|
badgeRef.current.container.style.display = "none";
|
|
2749
3037
|
}
|
|
2750
3038
|
}
|
|
3039
|
+
} else if (cfg.isMultiSeries && cfg.multiSeries && cfg.multiSeries.length > 0 || useMultiStash) {
|
|
3040
|
+
const effectiveMultiSeries = useMultiStash ? lastMultiSeriesRef.current : cfg.multiSeries;
|
|
3041
|
+
let labelReserve = 0;
|
|
3042
|
+
if (effectiveMultiSeries.some((s) => s.label)) {
|
|
3043
|
+
ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
|
|
3044
|
+
let maxLabelW = 0;
|
|
3045
|
+
for (const s of effectiveMultiSeries) {
|
|
3046
|
+
if (s.label) {
|
|
3047
|
+
const lw = ctx.measureText(s.label).width;
|
|
3048
|
+
if (lw > maxLabelW) maxLabelW = lw;
|
|
3049
|
+
}
|
|
3050
|
+
}
|
|
3051
|
+
labelReserve = Math.max(0, maxLabelW - 2) * chartReveal;
|
|
3052
|
+
}
|
|
3053
|
+
const chartW = w - pad.left - pad.right - labelReserve;
|
|
3054
|
+
const buffer = cfg.showBadge ? WINDOW_BUFFER : WINDOW_BUFFER_NO_BADGE;
|
|
3055
|
+
if (!useMultiStash) {
|
|
3056
|
+
const currentIds = new Set(effectiveMultiSeries.map((s) => s.id));
|
|
3057
|
+
for (const key of displayValuesRef.current.keys()) {
|
|
3058
|
+
if (!currentIds.has(key)) displayValuesRef.current.delete(key);
|
|
3059
|
+
}
|
|
3060
|
+
}
|
|
3061
|
+
const firstSeries = effectiveMultiSeries[0];
|
|
3062
|
+
const transition = windowTransitionRef.current;
|
|
3063
|
+
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
3064
|
+
const now = useMultiStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
3065
|
+
const smoothValues = /* @__PURE__ */ new Map();
|
|
3066
|
+
for (const s of effectiveMultiSeries) {
|
|
3067
|
+
let dv = displayValuesRef.current.get(s.id);
|
|
3068
|
+
if (dv === void 0) dv = s.value;
|
|
3069
|
+
if (!useMultiStash) {
|
|
3070
|
+
const adaptiveSpeed2 = computeAdaptiveSpeed(
|
|
3071
|
+
s.value,
|
|
3072
|
+
dv,
|
|
3073
|
+
displayMinRef.current,
|
|
3074
|
+
displayMaxRef.current,
|
|
3075
|
+
cfg.lerpSpeed,
|
|
3076
|
+
noMotion
|
|
3077
|
+
);
|
|
3078
|
+
dv = lerp(dv, s.value, adaptiveSpeed2, pausedDt);
|
|
3079
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
3080
|
+
if (Math.abs(dv - s.value) < prevRange * VALUE_SNAP_THRESHOLD) dv = s.value;
|
|
3081
|
+
displayValuesRef.current.set(s.id, dv);
|
|
3082
|
+
}
|
|
3083
|
+
smoothValues.set(s.id, dv);
|
|
3084
|
+
}
|
|
3085
|
+
const hiddenIds = cfg.hiddenSeriesIds;
|
|
3086
|
+
const seriesAlphas = seriesAlphaRef.current;
|
|
3087
|
+
for (const s of effectiveMultiSeries) {
|
|
3088
|
+
let alpha = seriesAlphas.get(s.id) ?? 1;
|
|
3089
|
+
const target = hiddenIds?.has(s.id) ? 0 : 1;
|
|
3090
|
+
alpha = noMotion ? target : lerp(alpha, target, SERIES_TOGGLE_SPEED, pausedDt);
|
|
3091
|
+
if (alpha < 0.01) alpha = 0;
|
|
3092
|
+
if (alpha > 0.99) alpha = 1;
|
|
3093
|
+
seriesAlphas.set(s.id, alpha);
|
|
3094
|
+
}
|
|
3095
|
+
const firstData = pausedMultiDataRef.current?.get(firstSeries.id)?.data ?? firstSeries.data;
|
|
3096
|
+
const windowResult = updateWindowTransition(
|
|
3097
|
+
cfg,
|
|
3098
|
+
transition,
|
|
3099
|
+
displayWindowRef.current,
|
|
3100
|
+
displayMinRef.current,
|
|
3101
|
+
displayMaxRef.current,
|
|
3102
|
+
noMotion,
|
|
3103
|
+
now_ms,
|
|
3104
|
+
now,
|
|
3105
|
+
firstData,
|
|
3106
|
+
smoothValues.get(firstSeries.id) ?? firstSeries.value,
|
|
3107
|
+
buffer
|
|
3108
|
+
);
|
|
3109
|
+
if (transition.startMs > 0 && effectiveMultiSeries.length > 1) {
|
|
3110
|
+
const targetRightEdge = now + cfg.windowSecs * buffer;
|
|
3111
|
+
const targetLeftEdge = targetRightEdge - cfg.windowSecs;
|
|
3112
|
+
let unionMin = Infinity;
|
|
3113
|
+
let unionMax = -Infinity;
|
|
3114
|
+
for (const s of effectiveMultiSeries) {
|
|
3115
|
+
const sData = pausedMultiDataRef.current?.get(s.id)?.data ?? s.data;
|
|
3116
|
+
const sv = smoothValues.get(s.id) ?? s.value;
|
|
3117
|
+
const targetVisible = [];
|
|
3118
|
+
for (const p of sData) {
|
|
3119
|
+
if (p.time >= targetLeftEdge - 2 && p.time <= targetRightEdge) targetVisible.push(p);
|
|
3120
|
+
}
|
|
3121
|
+
if (targetVisible.length > 0) {
|
|
3122
|
+
const range = computeRange(targetVisible, sv, cfg.referenceLine?.value, cfg.exaggerate);
|
|
3123
|
+
if (range.min < unionMin) unionMin = range.min;
|
|
3124
|
+
if (range.max > unionMax) unionMax = range.max;
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
if (isFinite(unionMin) && isFinite(unionMax)) {
|
|
3128
|
+
transition.rangeToMin = unionMin;
|
|
3129
|
+
transition.rangeToMax = unionMax;
|
|
3130
|
+
}
|
|
3131
|
+
}
|
|
3132
|
+
displayWindowRef.current = windowResult.windowSecs;
|
|
3133
|
+
const windowSecs = windowResult.windowSecs;
|
|
3134
|
+
const windowTransProgress = windowResult.windowTransProgress;
|
|
3135
|
+
const isWindowTransitioning = transition.startMs > 0;
|
|
3136
|
+
const rightEdge = now + windowSecs * buffer;
|
|
3137
|
+
const leftEdge = rightEdge - windowSecs;
|
|
3138
|
+
const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
|
|
3139
|
+
const seriesEntries = [];
|
|
3140
|
+
let globalMin = Infinity;
|
|
3141
|
+
let globalMax = -Infinity;
|
|
3142
|
+
for (const s of effectiveMultiSeries) {
|
|
3143
|
+
const snap = pausedMultiDataRef.current?.get(s.id);
|
|
3144
|
+
const seriesData = snap?.data ?? s.data;
|
|
3145
|
+
const visible = [];
|
|
3146
|
+
for (const p of seriesData) {
|
|
3147
|
+
if (p.time >= leftEdge - 2 && p.time <= filterRight) visible.push(p);
|
|
3148
|
+
}
|
|
3149
|
+
const sv = smoothValues.get(s.id) ?? s.value;
|
|
3150
|
+
const alpha = seriesAlphas.get(s.id) ?? 1;
|
|
3151
|
+
if (visible.length >= 2) {
|
|
3152
|
+
if (alpha > 0.01) {
|
|
3153
|
+
const range = computeRange(visible, sv, cfg.referenceLine?.value, cfg.exaggerate);
|
|
3154
|
+
if (range.min < globalMin) globalMin = range.min;
|
|
3155
|
+
if (range.max > globalMax) globalMax = range.max;
|
|
3156
|
+
}
|
|
3157
|
+
seriesEntries.push({ visible, smoothValue: sv, palette: s.palette, label: s.label, alpha });
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
if (seriesEntries.length === 0) {
|
|
3161
|
+
if (loadingAlpha > 0.01) {
|
|
3162
|
+
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, cfg.palette.gridLabel);
|
|
3163
|
+
}
|
|
3164
|
+
if (1 - loadingAlpha > 0.01) {
|
|
3165
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
|
|
3166
|
+
}
|
|
3167
|
+
ctx.save();
|
|
3168
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
3169
|
+
const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
|
|
3170
|
+
fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
|
|
3171
|
+
fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
3172
|
+
ctx.fillStyle = fadeGrad;
|
|
3173
|
+
ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
|
|
3174
|
+
ctx.restore();
|
|
3175
|
+
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
3176
|
+
rafRef.current = requestAnimationFrame(draw);
|
|
3177
|
+
return;
|
|
3178
|
+
}
|
|
3179
|
+
const computedRange = { min: isFinite(globalMin) ? globalMin : 0, max: isFinite(globalMax) ? globalMax : 1 };
|
|
3180
|
+
const adaptiveSpeed = cfg.lerpSpeed + ADAPTIVE_SPEED_BOOST * 0.5;
|
|
3181
|
+
const rangeResult = updateRange(
|
|
3182
|
+
computedRange,
|
|
3183
|
+
rangeInitedRef.current,
|
|
3184
|
+
targetMinRef.current,
|
|
3185
|
+
targetMaxRef.current,
|
|
3186
|
+
displayMinRef.current,
|
|
3187
|
+
displayMaxRef.current,
|
|
3188
|
+
isWindowTransitioning,
|
|
3189
|
+
windowTransProgress,
|
|
3190
|
+
transition,
|
|
3191
|
+
adaptiveSpeed,
|
|
3192
|
+
chartH,
|
|
3193
|
+
pausedDt
|
|
3194
|
+
);
|
|
3195
|
+
rangeInitedRef.current = rangeResult.rangeInited;
|
|
3196
|
+
targetMinRef.current = rangeResult.targetMin;
|
|
3197
|
+
targetMaxRef.current = rangeResult.targetMax;
|
|
3198
|
+
displayMinRef.current = rangeResult.displayMin;
|
|
3199
|
+
displayMaxRef.current = rangeResult.displayMax;
|
|
3200
|
+
const { minVal, maxVal, valRange } = rangeResult;
|
|
3201
|
+
const layout = {
|
|
3202
|
+
w,
|
|
3203
|
+
h,
|
|
3204
|
+
pad,
|
|
3205
|
+
chartW,
|
|
3206
|
+
chartH,
|
|
3207
|
+
leftEdge,
|
|
3208
|
+
rightEdge,
|
|
3209
|
+
minVal,
|
|
3210
|
+
maxVal,
|
|
3211
|
+
valRange,
|
|
3212
|
+
toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
|
|
3213
|
+
toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
|
|
3214
|
+
};
|
|
3215
|
+
const hoverPx = hoverXRef.current;
|
|
3216
|
+
let drawHoverX = null;
|
|
3217
|
+
let drawHoverTime = null;
|
|
3218
|
+
let isActiveHover = false;
|
|
3219
|
+
let hoverEntries = [];
|
|
3220
|
+
if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
|
|
3221
|
+
const maxHoverX = layout.toX(now);
|
|
3222
|
+
const clampedX = Math.min(hoverPx, maxHoverX);
|
|
3223
|
+
const t = leftEdge + (clampedX - pad.left) / chartW * (rightEdge - leftEdge);
|
|
3224
|
+
drawHoverX = clampedX;
|
|
3225
|
+
drawHoverTime = t;
|
|
3226
|
+
isActiveHover = true;
|
|
3227
|
+
for (const entry of seriesEntries) {
|
|
3228
|
+
if ((entry.alpha ?? 1) < 0.5) continue;
|
|
3229
|
+
const v = interpolateAtTime(entry.visible, t);
|
|
3230
|
+
if (v !== null) {
|
|
3231
|
+
hoverEntries.push({ color: entry.palette.line, label: entry.label ?? "", value: v });
|
|
3232
|
+
}
|
|
3233
|
+
}
|
|
3234
|
+
lastHoverRef.current = { x: clampedX, value: hoverEntries[0]?.value ?? 0, time: t };
|
|
3235
|
+
lastHoverEntriesRef.current = hoverEntries;
|
|
3236
|
+
cfg.onHover?.({ time: t, value: hoverEntries[0]?.value ?? 0, x: clampedX, y: layout.toY(hoverEntries[0]?.value ?? 0) });
|
|
3237
|
+
}
|
|
3238
|
+
const scrubTarget = isActiveHover ? 1 : 0;
|
|
3239
|
+
if (noMotion) {
|
|
3240
|
+
scrubAmountRef.current = scrubTarget;
|
|
3241
|
+
} else {
|
|
3242
|
+
scrubAmountRef.current += (scrubTarget - scrubAmountRef.current) * SCRUB_LERP_SPEED;
|
|
3243
|
+
if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
|
|
3244
|
+
if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
|
|
3245
|
+
}
|
|
3246
|
+
if (!isActiveHover && scrubAmountRef.current > 0 && lastHoverRef.current) {
|
|
3247
|
+
drawHoverX = lastHoverRef.current.x;
|
|
3248
|
+
drawHoverTime = lastHoverRef.current.time;
|
|
3249
|
+
hoverEntries = lastHoverEntriesRef.current;
|
|
3250
|
+
}
|
|
3251
|
+
drawMultiFrame(ctx, layout, {
|
|
3252
|
+
series: seriesEntries,
|
|
3253
|
+
now,
|
|
3254
|
+
showGrid: cfg.showGrid,
|
|
3255
|
+
showPulse: cfg.showPulse,
|
|
3256
|
+
referenceLine: cfg.referenceLine,
|
|
3257
|
+
hoverX: drawHoverX,
|
|
3258
|
+
hoverTime: drawHoverTime,
|
|
3259
|
+
hoverEntries,
|
|
3260
|
+
scrubAmount: scrubAmountRef.current,
|
|
3261
|
+
windowSecs,
|
|
3262
|
+
formatValue: cfg.formatValue,
|
|
3263
|
+
formatTime: cfg.formatTime,
|
|
3264
|
+
gridState: gridStateRef.current,
|
|
3265
|
+
timeAxisState: timeAxisStateRef.current,
|
|
3266
|
+
dt,
|
|
3267
|
+
targetWindowSecs: cfg.windowSecs,
|
|
3268
|
+
tooltipY: cfg.tooltipY,
|
|
3269
|
+
tooltipOutline: cfg.tooltipOutline,
|
|
3270
|
+
chartReveal,
|
|
3271
|
+
pauseProgress,
|
|
3272
|
+
now_ms,
|
|
3273
|
+
primaryPalette: cfg.palette
|
|
3274
|
+
});
|
|
3275
|
+
const bgAlpha = 1 - chartReveal;
|
|
3276
|
+
if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
|
|
3277
|
+
const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
|
|
3278
|
+
if (bgEmptyAlpha > 0.01) {
|
|
3279
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
|
|
3280
|
+
}
|
|
3281
|
+
}
|
|
3282
|
+
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
2751
3283
|
} else {
|
|
2752
3284
|
const effectivePoints = useStash ? lastDataRef.current : points;
|
|
2753
3285
|
const adaptiveSpeed = computeAdaptiveSpeed(
|
|
@@ -2769,8 +3301,9 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2769
3301
|
}
|
|
2770
3302
|
const smoothValue = displayValueRef.current;
|
|
2771
3303
|
const chartW = w - pad.left - pad.right;
|
|
2772
|
-
const
|
|
2773
|
-
const
|
|
3304
|
+
const baseBuffer = cfg.showBadge ? WINDOW_BUFFER : WINDOW_BUFFER_NO_BADGE;
|
|
3305
|
+
const needsArrowRoom = cfg.showMomentum && cfg.showBadge;
|
|
3306
|
+
const buffer = needsArrowRoom ? Math.max(baseBuffer, 37 / Math.max(chartW, 1)) : baseBuffer;
|
|
2774
3307
|
const transition = windowTransitionRef.current;
|
|
2775
3308
|
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
2776
3309
|
const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
@@ -2957,6 +3490,7 @@ var defaultFormatTime = (t) => {
|
|
|
2957
3490
|
function Liveline({
|
|
2958
3491
|
data,
|
|
2959
3492
|
value,
|
|
3493
|
+
series: seriesProp,
|
|
2960
3494
|
theme = "dark",
|
|
2961
3495
|
color = "#3b82f6",
|
|
2962
3496
|
window: windowSecs = 30,
|
|
@@ -2996,6 +3530,9 @@ function Liveline({
|
|
|
2996
3530
|
lineData,
|
|
2997
3531
|
lineValue,
|
|
2998
3532
|
onModeChange,
|
|
3533
|
+
onSeriesToggle,
|
|
3534
|
+
seriesToggleCompact = false,
|
|
3535
|
+
lineWidth,
|
|
2999
3536
|
className,
|
|
3000
3537
|
style
|
|
3001
3538
|
}) {
|
|
@@ -3008,13 +3545,37 @@ function Liveline({
|
|
|
3008
3545
|
const modeBarRef = useRef2(null);
|
|
3009
3546
|
const modeBtnRefs = useRef2(/* @__PURE__ */ new Map());
|
|
3010
3547
|
const [modeIndicatorStyle, setModeIndicatorStyle] = useState(null);
|
|
3011
|
-
const
|
|
3548
|
+
const [hiddenSeries, setHiddenSeries] = useState(/* @__PURE__ */ new Set());
|
|
3549
|
+
const lastSeriesPropRef = useRef2(seriesProp);
|
|
3550
|
+
if (seriesProp && seriesProp.length > 0) lastSeriesPropRef.current = seriesProp;
|
|
3551
|
+
const palette = useMemo(() => {
|
|
3552
|
+
const p = resolveTheme(color, theme);
|
|
3553
|
+
if (lineWidth != null) p.lineWidth = lineWidth;
|
|
3554
|
+
return p;
|
|
3555
|
+
}, [color, theme, lineWidth]);
|
|
3012
3556
|
const isDark = theme === "dark";
|
|
3557
|
+
const isMultiSeries = seriesProp != null && seriesProp.length > 0;
|
|
3558
|
+
const showSeriesToggle = (lastSeriesPropRef.current?.length ?? 0) > 1;
|
|
3559
|
+
const seriesPalettes = useMemo(() => {
|
|
3560
|
+
if (!seriesProp || seriesProp.length === 0) return null;
|
|
3561
|
+
return resolveSeriesPalettes(seriesProp, theme);
|
|
3562
|
+
}, [seriesProp, theme]);
|
|
3563
|
+
const multiSeries = useMemo(() => {
|
|
3564
|
+
if (!seriesProp || !seriesPalettes) return void 0;
|
|
3565
|
+
return seriesProp.map((s, i) => ({
|
|
3566
|
+
id: s.id,
|
|
3567
|
+
data: s.data,
|
|
3568
|
+
value: s.value,
|
|
3569
|
+
palette: seriesPalettes.get(s.id) ?? resolveTheme(s.color || SERIES_COLORS[i % SERIES_COLORS.length], theme),
|
|
3570
|
+
label: s.label
|
|
3571
|
+
}));
|
|
3572
|
+
}, [seriesProp, seriesPalettes, theme]);
|
|
3013
3573
|
const showMomentum = momentum !== false;
|
|
3014
3574
|
const momentumOverride = typeof momentum === "string" ? momentum : void 0;
|
|
3575
|
+
const defaultRight = badge ? 80 : grid ? 54 : 12;
|
|
3015
3576
|
const pad = {
|
|
3016
3577
|
top: paddingOverride?.top ?? 12,
|
|
3017
|
-
right: paddingOverride?.right ??
|
|
3578
|
+
right: paddingOverride?.right ?? defaultRight,
|
|
3018
3579
|
bottom: paddingOverride?.bottom ?? 28,
|
|
3019
3580
|
left: paddingOverride?.left ?? 12
|
|
3020
3581
|
};
|
|
@@ -3051,6 +3612,22 @@ function Liveline({
|
|
|
3051
3612
|
});
|
|
3052
3613
|
}
|
|
3053
3614
|
}, [activeMode, onModeChange]);
|
|
3615
|
+
const handleSeriesToggle = useCallback2((id) => {
|
|
3616
|
+
setHiddenSeries((prev) => {
|
|
3617
|
+
const next = new Set(prev);
|
|
3618
|
+
if (next.has(id)) {
|
|
3619
|
+
next.delete(id);
|
|
3620
|
+
onSeriesToggle?.(id, true);
|
|
3621
|
+
} else {
|
|
3622
|
+
const totalSeries = seriesProp?.length ?? 0;
|
|
3623
|
+
const visibleCount = totalSeries - next.size;
|
|
3624
|
+
if (visibleCount <= 1) return prev;
|
|
3625
|
+
next.add(id);
|
|
3626
|
+
onSeriesToggle?.(id, false);
|
|
3627
|
+
}
|
|
3628
|
+
return next;
|
|
3629
|
+
});
|
|
3630
|
+
}, [seriesProp?.length, onSeriesToggle]);
|
|
3054
3631
|
const ws = windowStyle ?? "default";
|
|
3055
3632
|
useLivelineEngine(canvasRef, containerRef, {
|
|
3056
3633
|
data,
|
|
@@ -3059,10 +3636,10 @@ function Liveline({
|
|
|
3059
3636
|
windowSecs: effectiveWindowSecs,
|
|
3060
3637
|
lerpSpeed,
|
|
3061
3638
|
showGrid: grid,
|
|
3062
|
-
showBadge: badge,
|
|
3063
|
-
showMomentum,
|
|
3639
|
+
showBadge: isMultiSeries ? false : badge,
|
|
3640
|
+
showMomentum: isMultiSeries ? false : showMomentum,
|
|
3064
3641
|
momentumOverride,
|
|
3065
|
-
showFill: fill,
|
|
3642
|
+
showFill: isMultiSeries ? false : fill,
|
|
3066
3643
|
referenceLine,
|
|
3067
3644
|
formatValue,
|
|
3068
3645
|
formatTime,
|
|
@@ -3071,7 +3648,7 @@ function Liveline({
|
|
|
3071
3648
|
showPulse: pulse,
|
|
3072
3649
|
scrub,
|
|
3073
3650
|
exaggerate,
|
|
3074
|
-
degenOptions,
|
|
3651
|
+
degenOptions: isMultiSeries ? void 0 : degenOptions,
|
|
3075
3652
|
badgeTail,
|
|
3076
3653
|
badgeVariant,
|
|
3077
3654
|
tooltipY,
|
|
@@ -3088,7 +3665,10 @@ function Liveline({
|
|
|
3088
3665
|
liveCandle,
|
|
3089
3666
|
lineMode,
|
|
3090
3667
|
lineData,
|
|
3091
|
-
lineValue
|
|
3668
|
+
lineValue,
|
|
3669
|
+
multiSeries,
|
|
3670
|
+
isMultiSeries,
|
|
3671
|
+
hiddenSeriesIds: hiddenSeries
|
|
3092
3672
|
});
|
|
3093
3673
|
const cursorStyle = scrub ? cursor : "default";
|
|
3094
3674
|
const activeColor = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)";
|
|
@@ -3112,7 +3692,7 @@ function Liveline({
|
|
|
3112
3692
|
}
|
|
3113
3693
|
}
|
|
3114
3694
|
),
|
|
3115
|
-
(windows && windows.length > 0 || onModeChange) && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
|
|
3695
|
+
(windows && windows.length > 0 || onModeChange || showSeriesToggle) && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
|
|
3116
3696
|
windows && windows.length > 0 && /* @__PURE__ */ jsxs(
|
|
3117
3697
|
"div",
|
|
3118
3698
|
{
|
|
@@ -3180,20 +3760,20 @@ function Liveline({
|
|
|
3180
3760
|
style: {
|
|
3181
3761
|
position: "relative",
|
|
3182
3762
|
display: "inline-flex",
|
|
3183
|
-
gap: 2,
|
|
3184
|
-
background: isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3185
|
-
borderRadius: 6,
|
|
3186
|
-
padding: 2
|
|
3763
|
+
gap: ws === "text" ? 4 : 2,
|
|
3764
|
+
background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3765
|
+
borderRadius: ws === "rounded" ? 999 : 6,
|
|
3766
|
+
padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
|
|
3187
3767
|
},
|
|
3188
3768
|
children: [
|
|
3189
|
-
modeIndicatorStyle && /* @__PURE__ */ jsx("div", { style: {
|
|
3769
|
+
ws !== "text" && modeIndicatorStyle && /* @__PURE__ */ jsx("div", { style: {
|
|
3190
3770
|
position: "absolute",
|
|
3191
|
-
top: 2,
|
|
3771
|
+
top: ws === "rounded" ? 3 : 2,
|
|
3192
3772
|
left: modeIndicatorStyle.left,
|
|
3193
3773
|
width: modeIndicatorStyle.width,
|
|
3194
|
-
height: "calc(100% - 4px)",
|
|
3774
|
+
height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
|
|
3195
3775
|
background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
|
|
3196
|
-
borderRadius: 4,
|
|
3776
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3197
3777
|
transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
3198
3778
|
pointerEvents: "none"
|
|
3199
3779
|
} }),
|
|
@@ -3209,7 +3789,7 @@ function Liveline({
|
|
|
3209
3789
|
position: "relative",
|
|
3210
3790
|
zIndex: 1,
|
|
3211
3791
|
padding: "5px 7px",
|
|
3212
|
-
borderRadius: 4,
|
|
3792
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3213
3793
|
border: "none",
|
|
3214
3794
|
cursor: "pointer",
|
|
3215
3795
|
background: "transparent",
|
|
@@ -3240,7 +3820,7 @@ function Liveline({
|
|
|
3240
3820
|
position: "relative",
|
|
3241
3821
|
zIndex: 1,
|
|
3242
3822
|
padding: "5px 7px",
|
|
3243
|
-
borderRadius: 4,
|
|
3823
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3244
3824
|
border: "none",
|
|
3245
3825
|
cursor: "pointer",
|
|
3246
3826
|
background: "transparent",
|
|
@@ -3297,7 +3877,58 @@ function Liveline({
|
|
|
3297
3877
|
)
|
|
3298
3878
|
]
|
|
3299
3879
|
}
|
|
3300
|
-
)
|
|
3880
|
+
),
|
|
3881
|
+
showSeriesToggle && /* @__PURE__ */ jsx("div", { style: {
|
|
3882
|
+
display: "inline-flex",
|
|
3883
|
+
gap: ws === "text" ? 4 : 2,
|
|
3884
|
+
background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3885
|
+
borderRadius: ws === "rounded" ? 999 : 6,
|
|
3886
|
+
padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
|
|
3887
|
+
opacity: isMultiSeries ? 1 : 0,
|
|
3888
|
+
transition: "opacity 0.4s",
|
|
3889
|
+
pointerEvents: isMultiSeries ? "auto" : "none"
|
|
3890
|
+
}, children: (lastSeriesPropRef.current ?? []).map((s, si) => {
|
|
3891
|
+
const isHidden = hiddenSeries.has(s.id);
|
|
3892
|
+
const seriesColor = s.color || SERIES_COLORS[si % SERIES_COLORS.length];
|
|
3893
|
+
return /* @__PURE__ */ jsxs(
|
|
3894
|
+
"button",
|
|
3895
|
+
{
|
|
3896
|
+
onClick: () => handleSeriesToggle(s.id),
|
|
3897
|
+
style: {
|
|
3898
|
+
position: "relative",
|
|
3899
|
+
zIndex: 1,
|
|
3900
|
+
fontSize: 11,
|
|
3901
|
+
padding: seriesToggleCompact ? ws === "text" ? "2px 4px" : "5px 7px" : ws === "text" ? "2px 6px" : "3px 8px",
|
|
3902
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3903
|
+
border: "none",
|
|
3904
|
+
cursor: "pointer",
|
|
3905
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
3906
|
+
fontWeight: 500,
|
|
3907
|
+
background: isHidden ? "transparent" : ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
|
|
3908
|
+
color: isHidden ? inactiveColor : activeColor,
|
|
3909
|
+
opacity: isHidden ? 0.4 : 1,
|
|
3910
|
+
transition: "opacity 0.2s, background 0.15s, color 0.2s",
|
|
3911
|
+
lineHeight: "16px",
|
|
3912
|
+
display: "flex",
|
|
3913
|
+
alignItems: "center",
|
|
3914
|
+
gap: seriesToggleCompact ? 0 : 4
|
|
3915
|
+
},
|
|
3916
|
+
children: [
|
|
3917
|
+
/* @__PURE__ */ jsx("span", { style: {
|
|
3918
|
+
width: seriesToggleCompact ? 8 : 6,
|
|
3919
|
+
height: seriesToggleCompact ? 8 : 6,
|
|
3920
|
+
borderRadius: "50%",
|
|
3921
|
+
background: seriesColor,
|
|
3922
|
+
flexShrink: 0,
|
|
3923
|
+
opacity: isHidden ? 0.4 : 1,
|
|
3924
|
+
transition: "opacity 0.2s"
|
|
3925
|
+
} }),
|
|
3926
|
+
!seriesToggleCompact && (s.label ?? s.id)
|
|
3927
|
+
]
|
|
3928
|
+
},
|
|
3929
|
+
s.id
|
|
3930
|
+
);
|
|
3931
|
+
}) })
|
|
3301
3932
|
] }),
|
|
3302
3933
|
/* @__PURE__ */ jsx(
|
|
3303
3934
|
"div",
|