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.cjs
CHANGED
|
@@ -88,6 +88,33 @@ function resolveTheme(color, mode) {
|
|
|
88
88
|
badgeFont: '500 11px "SF Mono", Menlo, monospace'
|
|
89
89
|
};
|
|
90
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
|
+
}
|
|
91
118
|
|
|
92
119
|
// src/useLivelineEngine.ts
|
|
93
120
|
var import_react = require("react");
|
|
@@ -513,6 +540,33 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = per
|
|
|
513
540
|
}
|
|
514
541
|
ctx.fill();
|
|
515
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
|
+
}
|
|
516
570
|
function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
|
|
517
571
|
const baseAlpha = ctx.globalAlpha;
|
|
518
572
|
const upTarget = momentum === "up" ? 1 : 0;
|
|
@@ -611,6 +665,90 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
|
|
|
611
665
|
ctx.fillText(separator + timeText, tx + valueW, ty);
|
|
612
666
|
ctx.restore();
|
|
613
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
|
+
}
|
|
614
752
|
|
|
615
753
|
// src/draw/referenceLine.ts
|
|
616
754
|
function drawReferenceLine(ctx, layout, palette, ref) {
|
|
@@ -1457,6 +1595,124 @@ function drawFrame(ctx, layout, palette, opts) {
|
|
|
1457
1595
|
ctx.restore();
|
|
1458
1596
|
}
|
|
1459
1597
|
}
|
|
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();
|
|
1610
|
+
}
|
|
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
|
+
}
|
|
1460
1716
|
function drawCandleFrame(ctx, layout, palette, opts) {
|
|
1461
1717
|
const { w, h, pad, chartW, chartH } = layout;
|
|
1462
1718
|
const reveal = opts.chartReveal;
|
|
@@ -1729,6 +1985,7 @@ var BADGE_Y_LERP_TRANSITIONING = 0.5;
|
|
|
1729
1985
|
var MOMENTUM_COLOR_LERP = 0.12;
|
|
1730
1986
|
var WINDOW_TRANSITION_MS = 750;
|
|
1731
1987
|
var WINDOW_BUFFER = 0.05;
|
|
1988
|
+
var WINDOW_BUFFER_NO_BADGE = 0.015;
|
|
1732
1989
|
var VALUE_SNAP_THRESHOLD = 1e-3;
|
|
1733
1990
|
var ADAPTIVE_SPEED_BOOST = 0.2;
|
|
1734
1991
|
var MOMENTUM_GREEN = [34, 197, 94];
|
|
@@ -1739,6 +1996,7 @@ var PAUSE_PROGRESS_SPEED = 0.12;
|
|
|
1739
1996
|
var PAUSE_CATCHUP_SPEED = 0.08;
|
|
1740
1997
|
var PAUSE_CATCHUP_SPEED_FAST = 0.22;
|
|
1741
1998
|
var LOADING_ALPHA_SPEED = 0.14;
|
|
1999
|
+
var SERIES_TOGGLE_SPEED = 0.1;
|
|
1742
2000
|
var CANDLE_LERP_SPEED = 0.25;
|
|
1743
2001
|
var CANDLE_WIDTH_TRANS_MS = 300;
|
|
1744
2002
|
var LINE_MORPH_MS = 500;
|
|
@@ -1749,7 +2007,7 @@ var LINE_ADAPTIVE_BOOST = 0.2;
|
|
|
1749
2007
|
var LINE_SNAP_THRESHOLD = 1e-3;
|
|
1750
2008
|
var RANGE_LERP_SPEED = 0.15;
|
|
1751
2009
|
var RANGE_ADAPTIVE_BOOST = 0.2;
|
|
1752
|
-
var
|
|
2010
|
+
var CANDLE_BUFFER_NO_BADGE = 0.015;
|
|
1753
2011
|
function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
|
|
1754
2012
|
const valGap = Math.abs(value - displayValue);
|
|
1755
2013
|
const prevRange = displayMax - displayMin || 1;
|
|
@@ -2062,6 +2320,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2062
2320
|
const configRef = (0, import_react.useRef)(config);
|
|
2063
2321
|
configRef.current = config;
|
|
2064
2322
|
const displayValueRef = (0, import_react.useRef)(config.value);
|
|
2323
|
+
const displayValuesRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
2324
|
+
const seriesAlphaRef = (0, import_react.useRef)(/* @__PURE__ */ new Map());
|
|
2065
2325
|
const displayMinRef = (0, import_react.useRef)(0);
|
|
2066
2326
|
const displayMaxRef = (0, import_react.useRef)(0);
|
|
2067
2327
|
const targetMinRef = (0, import_react.useRef)(0);
|
|
@@ -2094,12 +2354,15 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2094
2354
|
const hoverXRef = (0, import_react.useRef)(null);
|
|
2095
2355
|
const scrubAmountRef = (0, import_react.useRef)(0);
|
|
2096
2356
|
const lastHoverRef = (0, import_react.useRef)(null);
|
|
2357
|
+
const lastHoverEntriesRef = (0, import_react.useRef)([]);
|
|
2097
2358
|
const chartRevealRef = (0, import_react.useRef)(0);
|
|
2098
2359
|
const pauseProgressRef = (0, import_react.useRef)(0);
|
|
2099
2360
|
const timeDebtRef = (0, import_react.useRef)(0);
|
|
2100
2361
|
const lastDataRef = (0, import_react.useRef)([]);
|
|
2362
|
+
const lastMultiSeriesRef = (0, import_react.useRef)([]);
|
|
2101
2363
|
const frozenNowRef = (0, import_react.useRef)(0);
|
|
2102
2364
|
const pausedDataRef = (0, import_react.useRef)(null);
|
|
2365
|
+
const pausedMultiDataRef = (0, import_react.useRef)(null);
|
|
2103
2366
|
const loadingAlphaRef = (0, import_react.useRef)(config.loading ? 1 : 0);
|
|
2104
2367
|
const displayCandleRef = (0, import_react.useRef)(null);
|
|
2105
2368
|
const liveBirthAlphaRef = (0, import_react.useRef)(1);
|
|
@@ -2279,6 +2542,17 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2279
2542
|
pausedLineDataRef.current = null;
|
|
2280
2543
|
pausedLineValueRef.current = null;
|
|
2281
2544
|
}
|
|
2545
|
+
} else if (cfg.isMultiSeries && cfg.multiSeries) {
|
|
2546
|
+
if (cfg.paused && pausedMultiDataRef.current === null) {
|
|
2547
|
+
const snap = /* @__PURE__ */ new Map();
|
|
2548
|
+
for (const s of cfg.multiSeries) {
|
|
2549
|
+
if (s.data.length >= 2) snap.set(s.id, { data: s.data.slice(), value: s.value });
|
|
2550
|
+
}
|
|
2551
|
+
if (snap.size > 0) pausedMultiDataRef.current = snap;
|
|
2552
|
+
}
|
|
2553
|
+
if (!cfg.paused) {
|
|
2554
|
+
pausedMultiDataRef.current = null;
|
|
2555
|
+
}
|
|
2282
2556
|
} else {
|
|
2283
2557
|
if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
|
|
2284
2558
|
pausedDataRef.current = cfg.data.slice();
|
|
@@ -2289,7 +2563,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2289
2563
|
}
|
|
2290
2564
|
const points = isCandle ? [] : pausedDataRef.current ?? cfg.data;
|
|
2291
2565
|
const effectiveCandles = isCandle ? pausedCandlesRef.current ?? (cfg.candles ?? []) : [];
|
|
2292
|
-
const
|
|
2566
|
+
const hasMultiData = cfg.isMultiSeries && cfg.multiSeries ? cfg.multiSeries.some((s) => s.data.length >= 2) : false;
|
|
2567
|
+
const hasData = isCandle ? effectiveCandles.length >= 2 : hasMultiData || points.length >= 2;
|
|
2293
2568
|
const pad = cfg.padding;
|
|
2294
2569
|
const chartH = h - pad.top - pad.bottom;
|
|
2295
2570
|
const pauseTarget = cfg.paused ? 1 : 0;
|
|
@@ -2325,11 +2600,23 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2325
2600
|
rangeInitedRef.current = false;
|
|
2326
2601
|
}
|
|
2327
2602
|
let useStash;
|
|
2603
|
+
let useMultiStash = false;
|
|
2328
2604
|
if (isCandle) {
|
|
2329
2605
|
useStash = !hasData && chartReveal > 5e-3 && lastCandlesRef.current.length > 0;
|
|
2330
2606
|
} else {
|
|
2331
|
-
|
|
2332
|
-
if (
|
|
2607
|
+
useMultiStash = !hasData && chartReveal > 5e-3 && lastMultiSeriesRef.current.length > 0;
|
|
2608
|
+
if (hasMultiData && cfg.multiSeries) {
|
|
2609
|
+
lastMultiSeriesRef.current = cfg.multiSeries.map((s) => ({
|
|
2610
|
+
id: s.id,
|
|
2611
|
+
data: s.data.slice(),
|
|
2612
|
+
value: s.value,
|
|
2613
|
+
palette: s.palette,
|
|
2614
|
+
label: s.label
|
|
2615
|
+
}));
|
|
2616
|
+
}
|
|
2617
|
+
if (hasData && !cfg.isMultiSeries) lastMultiSeriesRef.current = [];
|
|
2618
|
+
useStash = !useMultiStash && !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
|
|
2619
|
+
if (hasData && !cfg.isMultiSeries) lastDataRef.current = points;
|
|
2333
2620
|
}
|
|
2334
2621
|
if (isCandle) {
|
|
2335
2622
|
const lmt = lineModeTransRef.current;
|
|
@@ -2351,8 +2638,8 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2351
2638
|
lineModeProgRef.current = lmt.to;
|
|
2352
2639
|
}
|
|
2353
2640
|
}
|
|
2354
|
-
if (!hasData && !useStash) {
|
|
2355
|
-
const loadingColor = isCandle ? cfg.palette.gridLabel : void 0;
|
|
2641
|
+
if (!hasData && !useStash && !useMultiStash) {
|
|
2642
|
+
const loadingColor = isCandle || cfg.isMultiSeries || lastMultiSeriesRef.current.length > 0 ? cfg.palette.gridLabel : void 0;
|
|
2356
2643
|
if (loadingAlpha > 0.01) {
|
|
2357
2644
|
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, loadingColor);
|
|
2358
2645
|
}
|
|
@@ -2372,6 +2659,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2372
2659
|
return;
|
|
2373
2660
|
}
|
|
2374
2661
|
if (isCandle) {
|
|
2662
|
+
const candleBuffer = CANDLE_BUFFER_NO_BADGE;
|
|
2375
2663
|
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
2376
2664
|
const now = hasData || chartReveal < 5e-3 ? Date.now() / 1e3 - timeDebtRef.current : frozenNowRef.current;
|
|
2377
2665
|
const rawLive = pausedCandlesRef.current ? pausedLiveRef.current ?? void 0 : cfg.liveCandle;
|
|
@@ -2414,7 +2702,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2414
2702
|
cwt.rangeFromMin = displayMinRef.current;
|
|
2415
2703
|
cwt.rangeFromMax = displayMaxRef.current;
|
|
2416
2704
|
const curWindow = displayWindowRef.current;
|
|
2417
|
-
const re = now + curWindow *
|
|
2705
|
+
const re = now + curWindow * candleBuffer;
|
|
2418
2706
|
const le = re - curWindow;
|
|
2419
2707
|
const targetVis = [];
|
|
2420
2708
|
for (const c of effectiveCandles) {
|
|
@@ -2465,13 +2753,13 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2465
2753
|
effectiveCandles,
|
|
2466
2754
|
rawLive,
|
|
2467
2755
|
candleWidthSecs,
|
|
2468
|
-
|
|
2756
|
+
candleBuffer
|
|
2469
2757
|
);
|
|
2470
2758
|
displayWindowRef.current = windowResult.windowSecs;
|
|
2471
2759
|
const windowSecs = windowResult.windowSecs;
|
|
2472
2760
|
const windowTransProgress = windowResult.windowTransProgress;
|
|
2473
2761
|
const isWindowTransitioning = transition.startMs > 0;
|
|
2474
|
-
const rightEdge = now + windowSecs *
|
|
2762
|
+
const rightEdge = now + windowSecs * candleBuffer;
|
|
2475
2763
|
const leftEdge = rightEdge - windowSecs;
|
|
2476
2764
|
let smoothLive;
|
|
2477
2765
|
if (rawLive) {
|
|
@@ -2775,6 +3063,250 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2775
3063
|
badgeRef.current.container.style.display = "none";
|
|
2776
3064
|
}
|
|
2777
3065
|
}
|
|
3066
|
+
} else if (cfg.isMultiSeries && cfg.multiSeries && cfg.multiSeries.length > 0 || useMultiStash) {
|
|
3067
|
+
const effectiveMultiSeries = useMultiStash ? lastMultiSeriesRef.current : cfg.multiSeries;
|
|
3068
|
+
let labelReserve = 0;
|
|
3069
|
+
if (effectiveMultiSeries.some((s) => s.label)) {
|
|
3070
|
+
ctx.font = '600 10px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif';
|
|
3071
|
+
let maxLabelW = 0;
|
|
3072
|
+
for (const s of effectiveMultiSeries) {
|
|
3073
|
+
if (s.label) {
|
|
3074
|
+
const lw = ctx.measureText(s.label).width;
|
|
3075
|
+
if (lw > maxLabelW) maxLabelW = lw;
|
|
3076
|
+
}
|
|
3077
|
+
}
|
|
3078
|
+
labelReserve = Math.max(0, maxLabelW - 2) * chartReveal;
|
|
3079
|
+
}
|
|
3080
|
+
const chartW = w - pad.left - pad.right - labelReserve;
|
|
3081
|
+
const buffer = cfg.showBadge ? WINDOW_BUFFER : WINDOW_BUFFER_NO_BADGE;
|
|
3082
|
+
if (!useMultiStash) {
|
|
3083
|
+
const currentIds = new Set(effectiveMultiSeries.map((s) => s.id));
|
|
3084
|
+
for (const key of displayValuesRef.current.keys()) {
|
|
3085
|
+
if (!currentIds.has(key)) displayValuesRef.current.delete(key);
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
const firstSeries = effectiveMultiSeries[0];
|
|
3089
|
+
const transition = windowTransitionRef.current;
|
|
3090
|
+
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
3091
|
+
const now = useMultiStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
3092
|
+
const smoothValues = /* @__PURE__ */ new Map();
|
|
3093
|
+
for (const s of effectiveMultiSeries) {
|
|
3094
|
+
let dv = displayValuesRef.current.get(s.id);
|
|
3095
|
+
if (dv === void 0) dv = s.value;
|
|
3096
|
+
if (!useMultiStash) {
|
|
3097
|
+
const adaptiveSpeed2 = computeAdaptiveSpeed(
|
|
3098
|
+
s.value,
|
|
3099
|
+
dv,
|
|
3100
|
+
displayMinRef.current,
|
|
3101
|
+
displayMaxRef.current,
|
|
3102
|
+
cfg.lerpSpeed,
|
|
3103
|
+
noMotion
|
|
3104
|
+
);
|
|
3105
|
+
dv = lerp(dv, s.value, adaptiveSpeed2, pausedDt);
|
|
3106
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
3107
|
+
if (Math.abs(dv - s.value) < prevRange * VALUE_SNAP_THRESHOLD) dv = s.value;
|
|
3108
|
+
displayValuesRef.current.set(s.id, dv);
|
|
3109
|
+
}
|
|
3110
|
+
smoothValues.set(s.id, dv);
|
|
3111
|
+
}
|
|
3112
|
+
const hiddenIds = cfg.hiddenSeriesIds;
|
|
3113
|
+
const seriesAlphas = seriesAlphaRef.current;
|
|
3114
|
+
for (const s of effectiveMultiSeries) {
|
|
3115
|
+
let alpha = seriesAlphas.get(s.id) ?? 1;
|
|
3116
|
+
const target = hiddenIds?.has(s.id) ? 0 : 1;
|
|
3117
|
+
alpha = noMotion ? target : lerp(alpha, target, SERIES_TOGGLE_SPEED, pausedDt);
|
|
3118
|
+
if (alpha < 0.01) alpha = 0;
|
|
3119
|
+
if (alpha > 0.99) alpha = 1;
|
|
3120
|
+
seriesAlphas.set(s.id, alpha);
|
|
3121
|
+
}
|
|
3122
|
+
const firstData = pausedMultiDataRef.current?.get(firstSeries.id)?.data ?? firstSeries.data;
|
|
3123
|
+
const windowResult = updateWindowTransition(
|
|
3124
|
+
cfg,
|
|
3125
|
+
transition,
|
|
3126
|
+
displayWindowRef.current,
|
|
3127
|
+
displayMinRef.current,
|
|
3128
|
+
displayMaxRef.current,
|
|
3129
|
+
noMotion,
|
|
3130
|
+
now_ms,
|
|
3131
|
+
now,
|
|
3132
|
+
firstData,
|
|
3133
|
+
smoothValues.get(firstSeries.id) ?? firstSeries.value,
|
|
3134
|
+
buffer
|
|
3135
|
+
);
|
|
3136
|
+
if (transition.startMs > 0 && effectiveMultiSeries.length > 1) {
|
|
3137
|
+
const targetRightEdge = now + cfg.windowSecs * buffer;
|
|
3138
|
+
const targetLeftEdge = targetRightEdge - cfg.windowSecs;
|
|
3139
|
+
let unionMin = Infinity;
|
|
3140
|
+
let unionMax = -Infinity;
|
|
3141
|
+
for (const s of effectiveMultiSeries) {
|
|
3142
|
+
const sData = pausedMultiDataRef.current?.get(s.id)?.data ?? s.data;
|
|
3143
|
+
const sv = smoothValues.get(s.id) ?? s.value;
|
|
3144
|
+
const targetVisible = [];
|
|
3145
|
+
for (const p of sData) {
|
|
3146
|
+
if (p.time >= targetLeftEdge - 2 && p.time <= targetRightEdge) targetVisible.push(p);
|
|
3147
|
+
}
|
|
3148
|
+
if (targetVisible.length > 0) {
|
|
3149
|
+
const range = computeRange(targetVisible, sv, cfg.referenceLine?.value, cfg.exaggerate);
|
|
3150
|
+
if (range.min < unionMin) unionMin = range.min;
|
|
3151
|
+
if (range.max > unionMax) unionMax = range.max;
|
|
3152
|
+
}
|
|
3153
|
+
}
|
|
3154
|
+
if (isFinite(unionMin) && isFinite(unionMax)) {
|
|
3155
|
+
transition.rangeToMin = unionMin;
|
|
3156
|
+
transition.rangeToMax = unionMax;
|
|
3157
|
+
}
|
|
3158
|
+
}
|
|
3159
|
+
displayWindowRef.current = windowResult.windowSecs;
|
|
3160
|
+
const windowSecs = windowResult.windowSecs;
|
|
3161
|
+
const windowTransProgress = windowResult.windowTransProgress;
|
|
3162
|
+
const isWindowTransitioning = transition.startMs > 0;
|
|
3163
|
+
const rightEdge = now + windowSecs * buffer;
|
|
3164
|
+
const leftEdge = rightEdge - windowSecs;
|
|
3165
|
+
const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
|
|
3166
|
+
const seriesEntries = [];
|
|
3167
|
+
let globalMin = Infinity;
|
|
3168
|
+
let globalMax = -Infinity;
|
|
3169
|
+
for (const s of effectiveMultiSeries) {
|
|
3170
|
+
const snap = pausedMultiDataRef.current?.get(s.id);
|
|
3171
|
+
const seriesData = snap?.data ?? s.data;
|
|
3172
|
+
const visible = [];
|
|
3173
|
+
for (const p of seriesData) {
|
|
3174
|
+
if (p.time >= leftEdge - 2 && p.time <= filterRight) visible.push(p);
|
|
3175
|
+
}
|
|
3176
|
+
const sv = smoothValues.get(s.id) ?? s.value;
|
|
3177
|
+
const alpha = seriesAlphas.get(s.id) ?? 1;
|
|
3178
|
+
if (visible.length >= 2) {
|
|
3179
|
+
if (alpha > 0.01) {
|
|
3180
|
+
const range = computeRange(visible, sv, cfg.referenceLine?.value, cfg.exaggerate);
|
|
3181
|
+
if (range.min < globalMin) globalMin = range.min;
|
|
3182
|
+
if (range.max > globalMax) globalMax = range.max;
|
|
3183
|
+
}
|
|
3184
|
+
seriesEntries.push({ visible, smoothValue: sv, palette: s.palette, label: s.label, alpha });
|
|
3185
|
+
}
|
|
3186
|
+
}
|
|
3187
|
+
if (seriesEntries.length === 0) {
|
|
3188
|
+
if (loadingAlpha > 0.01) {
|
|
3189
|
+
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, cfg.palette.gridLabel);
|
|
3190
|
+
}
|
|
3191
|
+
if (1 - loadingAlpha > 0.01) {
|
|
3192
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
|
|
3193
|
+
}
|
|
3194
|
+
ctx.save();
|
|
3195
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
3196
|
+
const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
|
|
3197
|
+
fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
|
|
3198
|
+
fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
3199
|
+
ctx.fillStyle = fadeGrad;
|
|
3200
|
+
ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
|
|
3201
|
+
ctx.restore();
|
|
3202
|
+
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
3203
|
+
rafRef.current = requestAnimationFrame(draw);
|
|
3204
|
+
return;
|
|
3205
|
+
}
|
|
3206
|
+
const computedRange = { min: isFinite(globalMin) ? globalMin : 0, max: isFinite(globalMax) ? globalMax : 1 };
|
|
3207
|
+
const adaptiveSpeed = cfg.lerpSpeed + ADAPTIVE_SPEED_BOOST * 0.5;
|
|
3208
|
+
const rangeResult = updateRange(
|
|
3209
|
+
computedRange,
|
|
3210
|
+
rangeInitedRef.current,
|
|
3211
|
+
targetMinRef.current,
|
|
3212
|
+
targetMaxRef.current,
|
|
3213
|
+
displayMinRef.current,
|
|
3214
|
+
displayMaxRef.current,
|
|
3215
|
+
isWindowTransitioning,
|
|
3216
|
+
windowTransProgress,
|
|
3217
|
+
transition,
|
|
3218
|
+
adaptiveSpeed,
|
|
3219
|
+
chartH,
|
|
3220
|
+
pausedDt
|
|
3221
|
+
);
|
|
3222
|
+
rangeInitedRef.current = rangeResult.rangeInited;
|
|
3223
|
+
targetMinRef.current = rangeResult.targetMin;
|
|
3224
|
+
targetMaxRef.current = rangeResult.targetMax;
|
|
3225
|
+
displayMinRef.current = rangeResult.displayMin;
|
|
3226
|
+
displayMaxRef.current = rangeResult.displayMax;
|
|
3227
|
+
const { minVal, maxVal, valRange } = rangeResult;
|
|
3228
|
+
const layout = {
|
|
3229
|
+
w,
|
|
3230
|
+
h,
|
|
3231
|
+
pad,
|
|
3232
|
+
chartW,
|
|
3233
|
+
chartH,
|
|
3234
|
+
leftEdge,
|
|
3235
|
+
rightEdge,
|
|
3236
|
+
minVal,
|
|
3237
|
+
maxVal,
|
|
3238
|
+
valRange,
|
|
3239
|
+
toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
|
|
3240
|
+
toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
|
|
3241
|
+
};
|
|
3242
|
+
const hoverPx = hoverXRef.current;
|
|
3243
|
+
let drawHoverX = null;
|
|
3244
|
+
let drawHoverTime = null;
|
|
3245
|
+
let isActiveHover = false;
|
|
3246
|
+
let hoverEntries = [];
|
|
3247
|
+
if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
|
|
3248
|
+
const maxHoverX = layout.toX(now);
|
|
3249
|
+
const clampedX = Math.min(hoverPx, maxHoverX);
|
|
3250
|
+
const t = leftEdge + (clampedX - pad.left) / chartW * (rightEdge - leftEdge);
|
|
3251
|
+
drawHoverX = clampedX;
|
|
3252
|
+
drawHoverTime = t;
|
|
3253
|
+
isActiveHover = true;
|
|
3254
|
+
for (const entry of seriesEntries) {
|
|
3255
|
+
if ((entry.alpha ?? 1) < 0.5) continue;
|
|
3256
|
+
const v = interpolateAtTime(entry.visible, t);
|
|
3257
|
+
if (v !== null) {
|
|
3258
|
+
hoverEntries.push({ color: entry.palette.line, label: entry.label ?? "", value: v });
|
|
3259
|
+
}
|
|
3260
|
+
}
|
|
3261
|
+
lastHoverRef.current = { x: clampedX, value: hoverEntries[0]?.value ?? 0, time: t };
|
|
3262
|
+
lastHoverEntriesRef.current = hoverEntries;
|
|
3263
|
+
cfg.onHover?.({ time: t, value: hoverEntries[0]?.value ?? 0, x: clampedX, y: layout.toY(hoverEntries[0]?.value ?? 0) });
|
|
3264
|
+
}
|
|
3265
|
+
const scrubTarget = isActiveHover ? 1 : 0;
|
|
3266
|
+
if (noMotion) {
|
|
3267
|
+
scrubAmountRef.current = scrubTarget;
|
|
3268
|
+
} else {
|
|
3269
|
+
scrubAmountRef.current += (scrubTarget - scrubAmountRef.current) * SCRUB_LERP_SPEED;
|
|
3270
|
+
if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
|
|
3271
|
+
if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
|
|
3272
|
+
}
|
|
3273
|
+
if (!isActiveHover && scrubAmountRef.current > 0 && lastHoverRef.current) {
|
|
3274
|
+
drawHoverX = lastHoverRef.current.x;
|
|
3275
|
+
drawHoverTime = lastHoverRef.current.time;
|
|
3276
|
+
hoverEntries = lastHoverEntriesRef.current;
|
|
3277
|
+
}
|
|
3278
|
+
drawMultiFrame(ctx, layout, {
|
|
3279
|
+
series: seriesEntries,
|
|
3280
|
+
now,
|
|
3281
|
+
showGrid: cfg.showGrid,
|
|
3282
|
+
showPulse: cfg.showPulse,
|
|
3283
|
+
referenceLine: cfg.referenceLine,
|
|
3284
|
+
hoverX: drawHoverX,
|
|
3285
|
+
hoverTime: drawHoverTime,
|
|
3286
|
+
hoverEntries,
|
|
3287
|
+
scrubAmount: scrubAmountRef.current,
|
|
3288
|
+
windowSecs,
|
|
3289
|
+
formatValue: cfg.formatValue,
|
|
3290
|
+
formatTime: cfg.formatTime,
|
|
3291
|
+
gridState: gridStateRef.current,
|
|
3292
|
+
timeAxisState: timeAxisStateRef.current,
|
|
3293
|
+
dt,
|
|
3294
|
+
targetWindowSecs: cfg.windowSecs,
|
|
3295
|
+
tooltipY: cfg.tooltipY,
|
|
3296
|
+
tooltipOutline: cfg.tooltipOutline,
|
|
3297
|
+
chartReveal,
|
|
3298
|
+
pauseProgress,
|
|
3299
|
+
now_ms,
|
|
3300
|
+
primaryPalette: cfg.palette
|
|
3301
|
+
});
|
|
3302
|
+
const bgAlpha = 1 - chartReveal;
|
|
3303
|
+
if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
|
|
3304
|
+
const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
|
|
3305
|
+
if (bgEmptyAlpha > 0.01) {
|
|
3306
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
|
|
3307
|
+
}
|
|
3308
|
+
}
|
|
3309
|
+
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
2778
3310
|
} else {
|
|
2779
3311
|
const effectivePoints = useStash ? lastDataRef.current : points;
|
|
2780
3312
|
const adaptiveSpeed = computeAdaptiveSpeed(
|
|
@@ -2796,8 +3328,9 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
2796
3328
|
}
|
|
2797
3329
|
const smoothValue = displayValueRef.current;
|
|
2798
3330
|
const chartW = w - pad.left - pad.right;
|
|
2799
|
-
const
|
|
2800
|
-
const
|
|
3331
|
+
const baseBuffer = cfg.showBadge ? WINDOW_BUFFER : WINDOW_BUFFER_NO_BADGE;
|
|
3332
|
+
const needsArrowRoom = cfg.showMomentum && cfg.showBadge;
|
|
3333
|
+
const buffer = needsArrowRoom ? Math.max(baseBuffer, 37 / Math.max(chartW, 1)) : baseBuffer;
|
|
2801
3334
|
const transition = windowTransitionRef.current;
|
|
2802
3335
|
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
2803
3336
|
const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
@@ -2984,6 +3517,7 @@ var defaultFormatTime = (t) => {
|
|
|
2984
3517
|
function Liveline({
|
|
2985
3518
|
data,
|
|
2986
3519
|
value,
|
|
3520
|
+
series: seriesProp,
|
|
2987
3521
|
theme = "dark",
|
|
2988
3522
|
color = "#3b82f6",
|
|
2989
3523
|
window: windowSecs = 30,
|
|
@@ -3023,6 +3557,9 @@ function Liveline({
|
|
|
3023
3557
|
lineData,
|
|
3024
3558
|
lineValue,
|
|
3025
3559
|
onModeChange,
|
|
3560
|
+
onSeriesToggle,
|
|
3561
|
+
seriesToggleCompact = false,
|
|
3562
|
+
lineWidth,
|
|
3026
3563
|
className,
|
|
3027
3564
|
style
|
|
3028
3565
|
}) {
|
|
@@ -3035,13 +3572,37 @@ function Liveline({
|
|
|
3035
3572
|
const modeBarRef = (0, import_react2.useRef)(null);
|
|
3036
3573
|
const modeBtnRefs = (0, import_react2.useRef)(/* @__PURE__ */ new Map());
|
|
3037
3574
|
const [modeIndicatorStyle, setModeIndicatorStyle] = (0, import_react2.useState)(null);
|
|
3038
|
-
const
|
|
3575
|
+
const [hiddenSeries, setHiddenSeries] = (0, import_react2.useState)(/* @__PURE__ */ new Set());
|
|
3576
|
+
const lastSeriesPropRef = (0, import_react2.useRef)(seriesProp);
|
|
3577
|
+
if (seriesProp && seriesProp.length > 0) lastSeriesPropRef.current = seriesProp;
|
|
3578
|
+
const palette = (0, import_react2.useMemo)(() => {
|
|
3579
|
+
const p = resolveTheme(color, theme);
|
|
3580
|
+
if (lineWidth != null) p.lineWidth = lineWidth;
|
|
3581
|
+
return p;
|
|
3582
|
+
}, [color, theme, lineWidth]);
|
|
3039
3583
|
const isDark = theme === "dark";
|
|
3584
|
+
const isMultiSeries = seriesProp != null && seriesProp.length > 0;
|
|
3585
|
+
const showSeriesToggle = (lastSeriesPropRef.current?.length ?? 0) > 1;
|
|
3586
|
+
const seriesPalettes = (0, import_react2.useMemo)(() => {
|
|
3587
|
+
if (!seriesProp || seriesProp.length === 0) return null;
|
|
3588
|
+
return resolveSeriesPalettes(seriesProp, theme);
|
|
3589
|
+
}, [seriesProp, theme]);
|
|
3590
|
+
const multiSeries = (0, import_react2.useMemo)(() => {
|
|
3591
|
+
if (!seriesProp || !seriesPalettes) return void 0;
|
|
3592
|
+
return seriesProp.map((s, i) => ({
|
|
3593
|
+
id: s.id,
|
|
3594
|
+
data: s.data,
|
|
3595
|
+
value: s.value,
|
|
3596
|
+
palette: seriesPalettes.get(s.id) ?? resolveTheme(s.color || SERIES_COLORS[i % SERIES_COLORS.length], theme),
|
|
3597
|
+
label: s.label
|
|
3598
|
+
}));
|
|
3599
|
+
}, [seriesProp, seriesPalettes, theme]);
|
|
3040
3600
|
const showMomentum = momentum !== false;
|
|
3041
3601
|
const momentumOverride = typeof momentum === "string" ? momentum : void 0;
|
|
3602
|
+
const defaultRight = badge ? 80 : grid ? 54 : 12;
|
|
3042
3603
|
const pad = {
|
|
3043
3604
|
top: paddingOverride?.top ?? 12,
|
|
3044
|
-
right: paddingOverride?.right ??
|
|
3605
|
+
right: paddingOverride?.right ?? defaultRight,
|
|
3045
3606
|
bottom: paddingOverride?.bottom ?? 28,
|
|
3046
3607
|
left: paddingOverride?.left ?? 12
|
|
3047
3608
|
};
|
|
@@ -3078,6 +3639,22 @@ function Liveline({
|
|
|
3078
3639
|
});
|
|
3079
3640
|
}
|
|
3080
3641
|
}, [activeMode, onModeChange]);
|
|
3642
|
+
const handleSeriesToggle = (0, import_react2.useCallback)((id) => {
|
|
3643
|
+
setHiddenSeries((prev) => {
|
|
3644
|
+
const next = new Set(prev);
|
|
3645
|
+
if (next.has(id)) {
|
|
3646
|
+
next.delete(id);
|
|
3647
|
+
onSeriesToggle?.(id, true);
|
|
3648
|
+
} else {
|
|
3649
|
+
const totalSeries = seriesProp?.length ?? 0;
|
|
3650
|
+
const visibleCount = totalSeries - next.size;
|
|
3651
|
+
if (visibleCount <= 1) return prev;
|
|
3652
|
+
next.add(id);
|
|
3653
|
+
onSeriesToggle?.(id, false);
|
|
3654
|
+
}
|
|
3655
|
+
return next;
|
|
3656
|
+
});
|
|
3657
|
+
}, [seriesProp?.length, onSeriesToggle]);
|
|
3081
3658
|
const ws = windowStyle ?? "default";
|
|
3082
3659
|
useLivelineEngine(canvasRef, containerRef, {
|
|
3083
3660
|
data,
|
|
@@ -3086,10 +3663,10 @@ function Liveline({
|
|
|
3086
3663
|
windowSecs: effectiveWindowSecs,
|
|
3087
3664
|
lerpSpeed,
|
|
3088
3665
|
showGrid: grid,
|
|
3089
|
-
showBadge: badge,
|
|
3090
|
-
showMomentum,
|
|
3666
|
+
showBadge: isMultiSeries ? false : badge,
|
|
3667
|
+
showMomentum: isMultiSeries ? false : showMomentum,
|
|
3091
3668
|
momentumOverride,
|
|
3092
|
-
showFill: fill,
|
|
3669
|
+
showFill: isMultiSeries ? false : fill,
|
|
3093
3670
|
referenceLine,
|
|
3094
3671
|
formatValue,
|
|
3095
3672
|
formatTime,
|
|
@@ -3098,7 +3675,7 @@ function Liveline({
|
|
|
3098
3675
|
showPulse: pulse,
|
|
3099
3676
|
scrub,
|
|
3100
3677
|
exaggerate,
|
|
3101
|
-
degenOptions,
|
|
3678
|
+
degenOptions: isMultiSeries ? void 0 : degenOptions,
|
|
3102
3679
|
badgeTail,
|
|
3103
3680
|
badgeVariant,
|
|
3104
3681
|
tooltipY,
|
|
@@ -3115,7 +3692,10 @@ function Liveline({
|
|
|
3115
3692
|
liveCandle,
|
|
3116
3693
|
lineMode,
|
|
3117
3694
|
lineData,
|
|
3118
|
-
lineValue
|
|
3695
|
+
lineValue,
|
|
3696
|
+
multiSeries,
|
|
3697
|
+
isMultiSeries,
|
|
3698
|
+
hiddenSeriesIds: hiddenSeries
|
|
3119
3699
|
});
|
|
3120
3700
|
const cursorStyle = scrub ? cursor : "default";
|
|
3121
3701
|
const activeColor = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)";
|
|
@@ -3139,7 +3719,7 @@ function Liveline({
|
|
|
3139
3719
|
}
|
|
3140
3720
|
}
|
|
3141
3721
|
),
|
|
3142
|
-
(windows && windows.length > 0 || onModeChange) && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
|
|
3722
|
+
(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: [
|
|
3143
3723
|
windows && windows.length > 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3144
3724
|
"div",
|
|
3145
3725
|
{
|
|
@@ -3207,20 +3787,20 @@ function Liveline({
|
|
|
3207
3787
|
style: {
|
|
3208
3788
|
position: "relative",
|
|
3209
3789
|
display: "inline-flex",
|
|
3210
|
-
gap: 2,
|
|
3211
|
-
background: isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3212
|
-
borderRadius: 6,
|
|
3213
|
-
padding: 2
|
|
3790
|
+
gap: ws === "text" ? 4 : 2,
|
|
3791
|
+
background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3792
|
+
borderRadius: ws === "rounded" ? 999 : 6,
|
|
3793
|
+
padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
|
|
3214
3794
|
},
|
|
3215
3795
|
children: [
|
|
3216
|
-
modeIndicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
|
|
3796
|
+
ws !== "text" && modeIndicatorStyle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
|
|
3217
3797
|
position: "absolute",
|
|
3218
|
-
top: 2,
|
|
3798
|
+
top: ws === "rounded" ? 3 : 2,
|
|
3219
3799
|
left: modeIndicatorStyle.left,
|
|
3220
3800
|
width: modeIndicatorStyle.width,
|
|
3221
|
-
height: "calc(100% - 4px)",
|
|
3801
|
+
height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
|
|
3222
3802
|
background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
|
|
3223
|
-
borderRadius: 4,
|
|
3803
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3224
3804
|
transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
|
|
3225
3805
|
pointerEvents: "none"
|
|
3226
3806
|
} }),
|
|
@@ -3236,7 +3816,7 @@ function Liveline({
|
|
|
3236
3816
|
position: "relative",
|
|
3237
3817
|
zIndex: 1,
|
|
3238
3818
|
padding: "5px 7px",
|
|
3239
|
-
borderRadius: 4,
|
|
3819
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3240
3820
|
border: "none",
|
|
3241
3821
|
cursor: "pointer",
|
|
3242
3822
|
background: "transparent",
|
|
@@ -3267,7 +3847,7 @@ function Liveline({
|
|
|
3267
3847
|
position: "relative",
|
|
3268
3848
|
zIndex: 1,
|
|
3269
3849
|
padding: "5px 7px",
|
|
3270
|
-
borderRadius: 4,
|
|
3850
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3271
3851
|
border: "none",
|
|
3272
3852
|
cursor: "pointer",
|
|
3273
3853
|
background: "transparent",
|
|
@@ -3324,7 +3904,58 @@ function Liveline({
|
|
|
3324
3904
|
)
|
|
3325
3905
|
]
|
|
3326
3906
|
}
|
|
3327
|
-
)
|
|
3907
|
+
),
|
|
3908
|
+
showSeriesToggle && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { style: {
|
|
3909
|
+
display: "inline-flex",
|
|
3910
|
+
gap: ws === "text" ? 4 : 2,
|
|
3911
|
+
background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
|
|
3912
|
+
borderRadius: ws === "rounded" ? 999 : 6,
|
|
3913
|
+
padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
|
|
3914
|
+
opacity: isMultiSeries ? 1 : 0,
|
|
3915
|
+
transition: "opacity 0.4s",
|
|
3916
|
+
pointerEvents: isMultiSeries ? "auto" : "none"
|
|
3917
|
+
}, children: (lastSeriesPropRef.current ?? []).map((s, si) => {
|
|
3918
|
+
const isHidden = hiddenSeries.has(s.id);
|
|
3919
|
+
const seriesColor = s.color || SERIES_COLORS[si % SERIES_COLORS.length];
|
|
3920
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
3921
|
+
"button",
|
|
3922
|
+
{
|
|
3923
|
+
onClick: () => handleSeriesToggle(s.id),
|
|
3924
|
+
style: {
|
|
3925
|
+
position: "relative",
|
|
3926
|
+
zIndex: 1,
|
|
3927
|
+
fontSize: 11,
|
|
3928
|
+
padding: seriesToggleCompact ? ws === "text" ? "2px 4px" : "5px 7px" : ws === "text" ? "2px 6px" : "3px 8px",
|
|
3929
|
+
borderRadius: ws === "rounded" ? 999 : 4,
|
|
3930
|
+
border: "none",
|
|
3931
|
+
cursor: "pointer",
|
|
3932
|
+
fontFamily: "system-ui, -apple-system, sans-serif",
|
|
3933
|
+
fontWeight: 500,
|
|
3934
|
+
background: isHidden ? "transparent" : ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
|
|
3935
|
+
color: isHidden ? inactiveColor : activeColor,
|
|
3936
|
+
opacity: isHidden ? 0.4 : 1,
|
|
3937
|
+
transition: "opacity 0.2s, background 0.15s, color 0.2s",
|
|
3938
|
+
lineHeight: "16px",
|
|
3939
|
+
display: "flex",
|
|
3940
|
+
alignItems: "center",
|
|
3941
|
+
gap: seriesToggleCompact ? 0 : 4
|
|
3942
|
+
},
|
|
3943
|
+
children: [
|
|
3944
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { style: {
|
|
3945
|
+
width: seriesToggleCompact ? 8 : 6,
|
|
3946
|
+
height: seriesToggleCompact ? 8 : 6,
|
|
3947
|
+
borderRadius: "50%",
|
|
3948
|
+
background: seriesColor,
|
|
3949
|
+
flexShrink: 0,
|
|
3950
|
+
opacity: isHidden ? 0.4 : 1,
|
|
3951
|
+
transition: "opacity 0.2s"
|
|
3952
|
+
} }),
|
|
3953
|
+
!seriesToggleCompact && (s.label ?? s.id)
|
|
3954
|
+
]
|
|
3955
|
+
},
|
|
3956
|
+
s.id
|
|
3957
|
+
);
|
|
3958
|
+
}) })
|
|
3328
3959
|
] }),
|
|
3329
3960
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
3330
3961
|
"div",
|