liveline 0.0.3 → 0.0.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,17 +1,23 @@
1
1
  // src/Liveline.tsx
2
- import { useRef as useRef2, useState, useLayoutEffect } from "react";
2
+ import { useRef as useRef2, useState, useLayoutEffect, useMemo } from "react";
3
3
 
4
4
  // src/theme.ts
5
- function hexToRgb(hex) {
6
- const h = hex.replace("#", "");
7
- const n = parseInt(h.length === 3 ? h.split("").map((c) => c + c).join("") : h, 16);
8
- return [n >> 16 & 255, n >> 8 & 255, n & 255];
5
+ function parseColorRgb(color) {
6
+ const hex = color.match(/^#([0-9a-f]{3,8})$/i);
7
+ if (hex) {
8
+ let h = hex[1];
9
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
10
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
11
+ }
12
+ const rgb = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
13
+ if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
14
+ return [128, 128, 128];
9
15
  }
10
16
  function rgba(r, g, b, a) {
11
17
  return `rgba(${r}, ${g}, ${b}, ${a})`;
12
18
  }
13
19
  function resolveTheme(color, mode) {
14
- const [r, g, b] = hexToRgb(color);
20
+ const [r, g, b] = parseColorRgb(color);
15
21
  const isDark = mode === "dark";
16
22
  return {
17
23
  // Line
@@ -97,18 +103,19 @@ function computeRange(visible, currentValue, referenceValue, exaggerate) {
97
103
  // src/math/momentum.ts
98
104
  function detectMomentum(points, lookback = 20) {
99
105
  if (points.length < 5) return "flat";
100
- const recent = points.slice(-lookback);
106
+ const start = Math.max(0, points.length - lookback);
101
107
  let min = Infinity;
102
108
  let max = -Infinity;
103
- for (const p of recent) {
104
- if (p.value < min) min = p.value;
105
- if (p.value > max) max = p.value;
109
+ for (let i = start; i < points.length; i++) {
110
+ const v = points[i].value;
111
+ if (v < min) min = v;
112
+ if (v > max) max = v;
106
113
  }
107
114
  const range = max - min;
108
115
  if (range === 0) return "flat";
109
- const tail = recent.slice(-5);
110
- const first = tail[0].value;
111
- const last = tail[tail.length - 1].value;
116
+ const tailStart = Math.max(start, points.length - 5);
117
+ const first = points[tailStart].value;
118
+ const last = points[points.length - 1].value;
112
119
  const delta = last - first;
113
120
  const threshold = range * 0.12;
114
121
  if (delta > threshold) return "up";
@@ -130,7 +137,9 @@ function interpolateAtTime(points, time) {
130
137
  }
131
138
  const p1 = points[lo];
132
139
  const p2 = points[hi];
133
- const t = (time - p1.time) / (p2.time - p1.time);
140
+ const dt = p2.time - p1.time;
141
+ if (dt === 0) return p1.value;
142
+ const t = (time - p1.time) / dt;
134
143
  return p1.value + (p2.value - p1.value) * t;
135
144
  }
136
145
 
@@ -212,6 +221,7 @@ function drawGrid(ctx, layout, palette, formatValue, state, dt) {
212
221
  state.labels.set(key, target * FADE_IN);
213
222
  }
214
223
  }
224
+ const baseAlpha = ctx.globalAlpha;
215
225
  ctx.setLineDash([1, 3]);
216
226
  ctx.lineWidth = 1;
217
227
  ctx.font = palette.labelFont;
@@ -222,7 +232,7 @@ function drawGrid(ctx, layout, palette, formatValue, state, dt) {
222
232
  const y = toY(val);
223
233
  if (y < pad.top - 10 || y > h - pad.bottom + 10) continue;
224
234
  ctx.save();
225
- ctx.globalAlpha = alpha;
235
+ ctx.globalAlpha = baseAlpha * alpha;
226
236
  ctx.strokeStyle = palette.gridLine;
227
237
  ctx.beginPath();
228
238
  ctx.moveTo(pad.left, y);
@@ -287,10 +297,47 @@ function drawSpline(ctx, pts) {
287
297
  }
288
298
  }
289
299
 
300
+ // src/draw/loadingShape.ts
301
+ var LOADING_AMPLITUDE_RATIO = 0.07;
302
+ var LOADING_SCROLL_SPEED = 1e-3;
303
+ function loadingY(t, centerY, amplitude, scroll) {
304
+ return centerY + amplitude * (Math.sin(t * 9.4 + scroll) * 0.55 + Math.sin(t * 15.7 + scroll * 1.3) * 0.3 + Math.sin(t * 4.2 + scroll * 0.7) * 0.15);
305
+ }
306
+ function loadingBreath(now_ms) {
307
+ return 0.22 + 0.08 * Math.sin(now_ms / 1200 * Math.PI);
308
+ }
309
+
290
310
  // src/draw/line.ts
291
- function renderCurve(ctx, layout, palette, pts, showFill) {
311
+ function parseRgba(color) {
312
+ const hex = color.match(/^#([0-9a-f]{3,8})$/i);
313
+ if (hex) {
314
+ let h = hex[1];
315
+ if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
316
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), 1];
317
+ }
318
+ const rgba2 = color.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)/);
319
+ if (rgba2) return [+rgba2[1], +rgba2[2], +rgba2[3], +rgba2[4]];
320
+ const rgb = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
321
+ if (rgb) return [+rgb[1], +rgb[2], +rgb[3], 1];
322
+ return [128, 128, 128, 1];
323
+ }
324
+ function blendColor(c1, c2, t) {
325
+ if (t <= 0) return c1;
326
+ if (t >= 1) return c2;
327
+ const [r1, g1, b1, a1] = parseRgba(c1);
328
+ const [r2, g2, b2, a2] = parseRgba(c2);
329
+ const r = Math.round(r1 + (r2 - r1) * t);
330
+ const g = Math.round(g1 + (g2 - g1) * t);
331
+ const b = Math.round(b1 + (b2 - b1) * t);
332
+ const a = a1 + (a2 - a1) * t;
333
+ if (a >= 0.995) return `rgb(${r},${g},${b})`;
334
+ return `rgba(${r},${g},${b},${a.toFixed(3)})`;
335
+ }
336
+ function renderCurve(ctx, layout, palette, pts, showFill, lineAlpha = 1, fillAlpha = 1, strokeColor) {
292
337
  const { h, pad } = layout;
293
- if (showFill) {
338
+ const baseAlpha = ctx.globalAlpha;
339
+ if (showFill && fillAlpha > 0.01) {
340
+ ctx.globalAlpha = baseAlpha * fillAlpha;
294
341
  const grad = ctx.createLinearGradient(0, pad.top, 0, h - pad.bottom);
295
342
  grad.addColorStop(0, palette.fillTop);
296
343
  grad.addColorStop(1, palette.fillBottom);
@@ -303,25 +350,48 @@ function renderCurve(ctx, layout, palette, pts, showFill) {
303
350
  ctx.fillStyle = grad;
304
351
  ctx.fill();
305
352
  }
353
+ ctx.globalAlpha = baseAlpha * lineAlpha;
306
354
  ctx.beginPath();
307
355
  ctx.moveTo(pts[0][0], pts[0][1]);
308
356
  drawSpline(ctx, pts);
309
- ctx.strokeStyle = palette.line;
357
+ ctx.strokeStyle = strokeColor ?? palette.line;
310
358
  ctx.lineWidth = palette.lineWidth;
311
359
  ctx.lineJoin = "round";
312
360
  ctx.lineCap = "round";
313
361
  ctx.stroke();
362
+ ctx.globalAlpha = baseAlpha;
314
363
  }
315
- function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0) {
316
- const { w, h, pad, toX, toY, chartW, chartH } = layout;
364
+ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0) {
365
+ const { h, pad, toX, toY, chartW, chartH } = layout;
317
366
  const yMin = pad.top;
318
367
  const yMax = h - pad.bottom;
319
368
  const clampY = (y) => Math.max(yMin, Math.min(yMax, y));
320
- const pts = visible.map(
321
- (p, i) => i === visible.length - 1 ? [toX(p.time), clampY(toY(smoothValue))] : [toX(p.time), clampY(toY(p.value))]
322
- );
323
- pts.push([toX(now), clampY(toY(smoothValue))]);
369
+ const centerY = pad.top + chartH / 2;
370
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
371
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
372
+ const morphY = chartReveal < 1 ? (rawY, x) => {
373
+ const t = Math.max(0, Math.min(1, (x - pad.left) / chartW));
374
+ const baseY = loadingY(t, centerY, amplitude, scroll);
375
+ return baseY + (rawY - baseY) * chartReveal;
376
+ } : (rawY, _x) => rawY;
377
+ const pts = visible.map((p, i) => {
378
+ const x = toX(p.time);
379
+ const y = i === visible.length - 1 ? morphY(clampY(toY(smoothValue)), x) : morphY(clampY(toY(p.value)), x);
380
+ return [x, y];
381
+ });
382
+ const liveTipX = toX(now);
383
+ const fullRightX = pad.left + chartW;
384
+ const tipX = chartReveal < 1 ? liveTipX + (fullRightX - liveTipX) * (1 - chartReveal) : liveTipX;
385
+ pts.push([tipX, morphY(clampY(toY(smoothValue)), tipX)]);
324
386
  if (pts.length < 2) return;
387
+ let lineAlpha = 1;
388
+ let fillAlpha = 1;
389
+ if (chartReveal < 1) {
390
+ const breath = loadingBreath(now_ms);
391
+ lineAlpha = breath + (1 - breath) * chartReveal;
392
+ fillAlpha = chartReveal;
393
+ }
394
+ const strokeColor = chartReveal < 1 ? blendColor(palette.gridLabel, palette.line, Math.min(1, chartReveal * 3)) : void 0;
325
395
  const isScrubbing = scrubX !== null;
326
396
  ctx.save();
327
397
  ctx.beginPath();
@@ -332,24 +402,26 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
332
402
  ctx.beginPath();
333
403
  ctx.rect(0, 0, scrubX, h);
334
404
  ctx.clip();
335
- renderCurve(ctx, layout, palette, pts, showFill);
405
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
336
406
  ctx.restore();
337
407
  ctx.save();
338
408
  ctx.beginPath();
339
409
  ctx.rect(scrubX, 0, layout.w - scrubX, h);
340
410
  ctx.clip();
341
411
  ctx.globalAlpha = 1 - scrubAmount * 0.6;
342
- renderCurve(ctx, layout, palette, pts, showFill);
412
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
343
413
  ctx.restore();
344
414
  } else {
345
- renderCurve(ctx, layout, palette, pts, showFill);
415
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
346
416
  }
347
417
  ctx.restore();
348
- const currentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
418
+ const realCurrentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
419
+ const currentY = chartReveal < 1 ? centerY + (realCurrentY - centerY) * chartReveal : realCurrentY;
349
420
  ctx.setLineDash([4, 4]);
350
421
  ctx.strokeStyle = palette.dashLine;
351
422
  ctx.lineWidth = 1;
352
- if (isScrubbing) ctx.globalAlpha = 1 - scrubAmount * 0.2;
423
+ const dashBase = isScrubbing ? 1 - scrubAmount * 0.2 : 1;
424
+ ctx.globalAlpha = chartReveal < 1 ? dashBase * chartReveal : dashBase;
353
425
  ctx.beginPath();
354
426
  ctx.moveTo(pad.left, currentY);
355
427
  ctx.lineTo(layout.w - pad.right, currentY);
@@ -364,27 +436,17 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
364
436
  // src/draw/dot.ts
365
437
  var PULSE_INTERVAL = 1500;
366
438
  var PULSE_DURATION = 900;
367
- function parseColor(color) {
368
- const hex = color.match(/^#([0-9a-f]{3,8})$/i);
369
- if (hex) {
370
- let h = hex[1];
371
- if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
372
- return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
373
- }
374
- const rgb = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
375
- if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
376
- return null;
377
- }
378
439
  function lerpColor(a, b, t) {
379
440
  const r = Math.round(a[0] + (b[0] - a[0]) * t);
380
441
  const g = Math.round(a[1] + (b[1] - a[1]) * t);
381
442
  const bl = Math.round(a[2] + (b[2] - a[2]) * t);
382
443
  return `rgb(${r},${g},${bl})`;
383
444
  }
384
- function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
445
+ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = performance.now()) {
446
+ const baseAlpha = ctx.globalAlpha;
385
447
  const dim = scrubAmount * 0.7;
386
448
  if (pulse && dim < 0.3) {
387
- const t = Date.now() % PULSE_INTERVAL / PULSE_DURATION;
449
+ const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
388
450
  if (t < 1) {
389
451
  const radius = 9 + t * 12;
390
452
  const pulseAlpha = 0.35 * (1 - t) * (1 - dim * 3);
@@ -392,13 +454,13 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
392
454
  ctx.arc(x, y, radius, 0, Math.PI * 2);
393
455
  ctx.strokeStyle = palette.line;
394
456
  ctx.lineWidth = 1.5;
395
- ctx.globalAlpha = pulseAlpha;
457
+ ctx.globalAlpha = baseAlpha * pulseAlpha;
396
458
  ctx.stroke();
397
459
  }
398
460
  }
399
- const outerRgb = parseColor(palette.badgeOuterBg) ?? [255, 255, 255];
461
+ const outerRgb = parseColorRgb(palette.badgeOuterBg);
400
462
  ctx.save();
401
- ctx.globalAlpha = 1;
463
+ ctx.globalAlpha = baseAlpha;
402
464
  ctx.shadowColor = palette.badgeOuterShadow;
403
465
  ctx.shadowBlur = 6 * (1 - dim);
404
466
  ctx.shadowOffsetY = 1;
@@ -407,18 +469,19 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
407
469
  ctx.fillStyle = palette.badgeOuterBg;
408
470
  ctx.fill();
409
471
  ctx.restore();
410
- ctx.globalAlpha = 1;
472
+ ctx.globalAlpha = baseAlpha;
411
473
  ctx.beginPath();
412
474
  ctx.arc(x, y, 3.5, 0, Math.PI * 2);
413
475
  if (dim > 0.01) {
414
- const lineRgb = parseColor(palette.line) ?? [100, 100, 255];
476
+ const lineRgb = parseColorRgb(palette.line);
415
477
  ctx.fillStyle = lerpColor(lineRgb, outerRgb, dim);
416
478
  } else {
417
479
  ctx.fillStyle = palette.line;
418
480
  }
419
481
  ctx.fill();
420
482
  }
421
- function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
483
+ function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
484
+ const baseAlpha = ctx.globalAlpha;
422
485
  const upTarget = momentum === "up" ? 1 : 0;
423
486
  const downTarget = momentum === "down" ? 1 : 0;
424
487
  const canFadeInUp = arrows.down < 0.02;
@@ -429,7 +492,7 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
429
492
  if (arrows.down < 0.01) arrows.down = 0;
430
493
  if (arrows.up > 0.99) arrows.up = 1;
431
494
  if (arrows.down > 0.99) arrows.down = 1;
432
- const cycle = Date.now() % 1400 / 1400;
495
+ const cycle = now_ms % 1400 / 1400;
433
496
  const drawChevrons = (dir, opacity) => {
434
497
  if (opacity < 0.01) return;
435
498
  const baseX = x + 19;
@@ -445,7 +508,7 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
445
508
  const localT = cycle - start;
446
509
  const wave = localT >= 0 && localT < dur ? Math.sin(localT / dur * Math.PI) : 0;
447
510
  const pulse = 0.3 + 0.7 * wave;
448
- ctx.globalAlpha = opacity * pulse;
511
+ ctx.globalAlpha = baseAlpha * opacity * pulse;
449
512
  const nudge = dir === -1 ? -3 : 3;
450
513
  const cy = baseY + dir * (i * 8 - 4) + nudge;
451
514
  ctx.beginPath();
@@ -458,13 +521,13 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
458
521
  };
459
522
  drawChevrons(-1, arrows.up);
460
523
  drawChevrons(1, arrows.down);
461
- ctx.globalAlpha = 1;
524
+ ctx.globalAlpha = baseAlpha;
462
525
  }
463
526
 
464
527
  // src/draw/crosshair.ts
465
528
  function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, formatValue, formatTime, scrubOpacity, tooltipY, liveDotX, tooltipOutline) {
466
529
  if (scrubOpacity < 0.01) return;
467
- const { w, h, pad, toY } = layout;
530
+ const { h, pad, toY } = layout;
468
531
  const y = toY(hoverValue);
469
532
  ctx.save();
470
533
  ctx.globalAlpha = scrubOpacity * 0.5;
@@ -496,7 +559,7 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
496
559
  const totalW = valueW + sepW + timeW;
497
560
  let tx = hoverX - totalW / 2;
498
561
  const minX = pad.left + 4;
499
- const dotRightEdge = liveDotX != null ? liveDotX + 7 : w - pad.right;
562
+ const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
500
563
  const maxX = dotRightEdge - totalW;
501
564
  if (tx < minX) tx = minX;
502
565
  if (tx > maxX) tx = maxX;
@@ -571,7 +634,7 @@ function niceTimeInterval(windowSecs) {
571
634
 
572
635
  // src/draw/timeAxis.ts
573
636
  var FADE = 0.08;
574
- function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, formatTime, state, dt) {
637
+ function drawTimeAxis(ctx, layout, palette, windowSecs, targetWindowSecs, formatTime, state, dt) {
575
638
  const { h, pad, leftEdge, rightEdge, toX } = layout;
576
639
  const chartLeft = pad.left;
577
640
  const chartRight = layout.w - pad.right;
@@ -586,9 +649,9 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
586
649
  return fromEdge / fadeZone;
587
650
  };
588
651
  ctx.font = palette.labelFont;
589
- const targetPxPerSec = chartW / _targetWindowSecs;
590
- let interval = niceTimeInterval(_targetWindowSecs);
591
- while (interval * targetPxPerSec < 60 && interval < _targetWindowSecs) {
652
+ const targetPxPerSec = chartW / targetWindowSecs;
653
+ let interval = niceTimeInterval(targetWindowSecs);
654
+ while (interval * targetPxPerSec < 60 && interval < targetWindowSecs) {
592
655
  interval *= 2;
593
656
  }
594
657
  const useLocalDays = interval >= 86400;
@@ -625,6 +688,7 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
625
688
  label.alpha = next;
626
689
  }
627
690
  }
691
+ const baseAlpha = ctx.globalAlpha;
628
692
  const lineY = h - pad.bottom;
629
693
  const tickLen = 5;
630
694
  ctx.strokeStyle = palette.gridLine;
@@ -660,7 +724,7 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
660
724
  }
661
725
  for (const label of drawn) {
662
726
  ctx.save();
663
- ctx.globalAlpha = label.alpha;
727
+ ctx.globalAlpha = baseAlpha * label.alpha;
664
728
  ctx.strokeStyle = palette.gridLine;
665
729
  ctx.lineWidth = 1;
666
730
  ctx.beginPath();
@@ -781,11 +845,12 @@ function drawOrderbook(ctx, layout, palette, orderbook, dt, state, swingMagnitud
781
845
  state.labels[writeIdx++] = l;
782
846
  }
783
847
  state.labels.length = writeIdx;
848
+ const baseAlpha = ctx.globalAlpha;
784
849
  ctx.save();
785
850
  ctx.font = '600 13px "SF Mono", Menlo, monospace';
786
851
  ctx.textAlign = "left";
787
852
  ctx.textBaseline = "middle";
788
- ctx.globalAlpha = 1;
853
+ ctx.globalAlpha = baseAlpha;
789
854
  const outlineColor = `rgb(${bg[0]},${bg[1]},${bg[2]})`;
790
855
  for (let i = 0; i < state.labels.length; i++) {
791
856
  const l = state.labels[i];
@@ -902,18 +967,44 @@ function drawFrame(ctx, layout, palette, opts) {
902
967
  shake.amplitude *= decayRate;
903
968
  if (shake.amplitude < SHAKE_MIN_AMPLITUDE) shake.amplitude = 0;
904
969
  }
905
- if (opts.referenceLine) {
970
+ const reveal = opts.chartReveal;
971
+ const pause = opts.pauseProgress;
972
+ const revealRamp = (start, end) => {
973
+ const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
974
+ return t * t * (3 - 2 * t);
975
+ };
976
+ if (opts.referenceLine && reveal > 0.01) {
977
+ ctx.save();
978
+ if (reveal < 1) ctx.globalAlpha = reveal;
906
979
  drawReferenceLine(ctx, layout, palette, opts.referenceLine);
980
+ ctx.restore();
907
981
  }
908
982
  if (opts.showGrid) {
909
- drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
983
+ const gridAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
984
+ if (gridAlpha > 0.01) {
985
+ ctx.save();
986
+ if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
987
+ drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
988
+ ctx.restore();
989
+ }
910
990
  }
911
- if (opts.orderbookData && opts.orderbookState) {
991
+ if (opts.orderbookData && opts.orderbookState && reveal > 0.01) {
992
+ ctx.save();
993
+ if (reveal < 1) ctx.globalAlpha = reveal;
912
994
  drawOrderbook(ctx, layout, palette, opts.orderbookData, opts.dt, opts.orderbookState, opts.swingMagnitude);
995
+ ctx.restore();
913
996
  }
914
997
  const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
915
- const pts = drawLine(ctx, layout, palette, opts.visible, opts.smoothValue, opts.now, opts.showFill, scrubX, opts.scrubAmount);
916
- drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
998
+ const pts = drawLine(ctx, layout, palette, opts.visible, opts.smoothValue, opts.now, opts.showFill, scrubX, opts.scrubAmount, reveal, opts.now_ms);
999
+ {
1000
+ const timeAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1001
+ if (timeAlpha > 0.01) {
1002
+ ctx.save();
1003
+ if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
1004
+ drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1005
+ ctx.restore();
1006
+ }
1007
+ }
917
1008
  if (pts && pts.length > 0) {
918
1009
  const lastPt = pts[pts.length - 1];
919
1010
  let dotScrub = opts.scrubAmount;
@@ -922,19 +1013,34 @@ function drawFrame(ctx, layout, palette, opts) {
922
1013
  const fadeStart = Math.min(80, layout.chartW * 0.3);
923
1014
  dotScrub = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
924
1015
  }
925
- drawDot(ctx, lastPt[0], lastPt[1], palette, opts.showPulse, dotScrub);
1016
+ const dotAlpha = reveal < 0.3 ? 0 : (reveal - 0.3) / 0.7;
1017
+ const showPulse = opts.showPulse && reveal > 0.6 && pause < 0.5;
1018
+ if (dotAlpha > 0.01) {
1019
+ ctx.save();
1020
+ if (dotAlpha < 1) ctx.globalAlpha = dotAlpha;
1021
+ drawDot(ctx, lastPt[0], lastPt[1], palette, showPulse, dotScrub, opts.now_ms);
1022
+ ctx.restore();
1023
+ }
926
1024
  if (opts.showMomentum) {
927
- drawArrows(
928
- ctx,
929
- lastPt[0],
930
- lastPt[1],
931
- opts.momentum,
932
- palette,
933
- opts.arrowState,
934
- opts.dt
935
- );
1025
+ const arrowReveal = reveal < 1 ? revealRamp(0.6, 1) : 1;
1026
+ const arrowAlpha = arrowReveal * (1 - pause);
1027
+ if (arrowAlpha > 0.01) {
1028
+ ctx.save();
1029
+ if (arrowAlpha < 1) ctx.globalAlpha = arrowAlpha;
1030
+ drawArrows(
1031
+ ctx,
1032
+ lastPt[0],
1033
+ lastPt[1],
1034
+ opts.momentum,
1035
+ palette,
1036
+ opts.arrowState,
1037
+ opts.dt,
1038
+ opts.now_ms
1039
+ );
1040
+ ctx.restore();
1041
+ }
936
1042
  }
937
- if (opts.particleState) {
1043
+ if (opts.particleState && reveal > 0.9) {
938
1044
  const burstIntensity = spawnOnSwing(
939
1045
  opts.particleState,
940
1046
  opts.momentum,
@@ -988,6 +1094,92 @@ function drawFrame(ctx, layout, palette, opts) {
988
1094
  }
989
1095
  }
990
1096
 
1097
+ // src/draw/loading.ts
1098
+ function drawLoading(ctx, w, h, pad, palette, now_ms, alpha = 1) {
1099
+ const chartW = w - pad.left - pad.right;
1100
+ const chartH = h - pad.top - pad.bottom;
1101
+ const centerY = pad.top + chartH / 2;
1102
+ const leftX = pad.left;
1103
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1104
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
1105
+ const breath = loadingBreath(now_ms);
1106
+ const numPts = 32;
1107
+ const pts = [];
1108
+ for (let i = 0; i <= numPts; i++) {
1109
+ const t = i / numPts;
1110
+ const x = leftX + t * chartW;
1111
+ const y = loadingY(t, centerY, amplitude, scroll);
1112
+ pts.push([x, y]);
1113
+ }
1114
+ ctx.beginPath();
1115
+ ctx.moveTo(pts[0][0], pts[0][1]);
1116
+ drawSpline(ctx, pts);
1117
+ ctx.strokeStyle = palette.line;
1118
+ ctx.lineWidth = palette.lineWidth;
1119
+ ctx.globalAlpha = breath * alpha;
1120
+ ctx.lineCap = "round";
1121
+ ctx.lineJoin = "round";
1122
+ ctx.stroke();
1123
+ ctx.globalAlpha = 1;
1124
+ }
1125
+
1126
+ // src/draw/empty.ts
1127
+ function drawEmpty(ctx, w, h, pad, palette, alpha = 1, now_ms = 0, skipLine = false, emptyText) {
1128
+ const chartW = w - pad.left - pad.right;
1129
+ const chartH = h - pad.top - pad.bottom;
1130
+ const centerY = pad.top + chartH / 2;
1131
+ const cx = pad.left + chartW / 2;
1132
+ const text = emptyText ?? "No data to display";
1133
+ ctx.font = "400 12px system-ui, -apple-system, sans-serif";
1134
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1135
+ const textW = ctx.measureText(text).width;
1136
+ const gapHalf = textW / 2 + 20;
1137
+ const fadeW = 30;
1138
+ if (!skipLine) {
1139
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
1140
+ const breath = loadingBreath(now_ms);
1141
+ const numPts = 32;
1142
+ const pts = [];
1143
+ for (let i = 0; i <= numPts; i++) {
1144
+ const t = i / numPts;
1145
+ const x = pad.left + t * chartW;
1146
+ const y = loadingY(t, centerY, amplitude, scroll);
1147
+ pts.push([x, y]);
1148
+ }
1149
+ ctx.beginPath();
1150
+ ctx.moveTo(pts[0][0], pts[0][1]);
1151
+ drawSpline(ctx, pts);
1152
+ ctx.strokeStyle = palette.gridLabel;
1153
+ ctx.lineWidth = palette.lineWidth;
1154
+ ctx.globalAlpha = breath * alpha;
1155
+ ctx.lineCap = "round";
1156
+ ctx.lineJoin = "round";
1157
+ ctx.stroke();
1158
+ }
1159
+ ctx.save();
1160
+ ctx.globalCompositeOperation = "destination-out";
1161
+ const gapLeft = cx - gapHalf - fadeW;
1162
+ const gapRight = cx + gapHalf + fadeW;
1163
+ const eraseGrad = ctx.createLinearGradient(gapLeft, 0, gapRight, 0);
1164
+ eraseGrad.addColorStop(0, "rgba(0,0,0,0)");
1165
+ eraseGrad.addColorStop(fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1166
+ eraseGrad.addColorStop(1 - fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1167
+ eraseGrad.addColorStop(1, "rgba(0,0,0,0)");
1168
+ ctx.fillStyle = eraseGrad;
1169
+ ctx.globalAlpha = alpha;
1170
+ const eraseH = amplitude * 2 + palette.lineWidth + 6;
1171
+ ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
1172
+ ctx.restore();
1173
+ ctx.textAlign = "center";
1174
+ ctx.textBaseline = "middle";
1175
+ ctx.globalAlpha = 0.35 * alpha;
1176
+ ctx.fillStyle = palette.gridLabel;
1177
+ ctx.fillText(text, cx, centerY);
1178
+ ctx.globalAlpha = 1;
1179
+ ctx.textAlign = "start";
1180
+ ctx.textBaseline = "alphabetic";
1181
+ }
1182
+
991
1183
  // src/draw/badge.ts
992
1184
  function badgeSvgPath(pillW, pillH, tailLen, tailSpread) {
993
1185
  const r = pillH / 2;
@@ -1034,6 +1226,11 @@ var VALUE_SNAP_THRESHOLD = 1e-3;
1034
1226
  var ADAPTIVE_SPEED_BOOST = 0.2;
1035
1227
  var MOMENTUM_GREEN = [34, 197, 94];
1036
1228
  var MOMENTUM_RED = [239, 68, 68];
1229
+ var CHART_REVEAL_SPEED = 0.14;
1230
+ var PAUSE_PROGRESS_SPEED = 0.12;
1231
+ var PAUSE_CATCHUP_SPEED = 0.08;
1232
+ var PAUSE_CATCHUP_SPEED_FAST = 0.22;
1233
+ var LOADING_ALPHA_SPEED = 0.14;
1037
1234
  function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
1038
1235
  const valGap = Math.abs(value - displayValue);
1039
1236
  const prevRange = displayMax - displayMin || 1;
@@ -1165,12 +1362,14 @@ function updateHoverState(hoverPixelX, pad, w, layout, now, visible, scrubAmount
1165
1362
  lastHover
1166
1363
  };
1167
1364
  }
1168
- function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badgeColor, isWindowTransitioning, noMotion, ctx, dt) {
1169
- if (!cfg.showBadge) {
1365
+ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badgeColor, isWindowTransitioning, noMotion, ctx, dt, chartReveal = 1) {
1366
+ if (!cfg.showBadge || chartReveal < 0.25) {
1170
1367
  badge.container.style.display = "none";
1171
1368
  return badgeY;
1172
1369
  }
1173
1370
  badge.container.style.display = "";
1371
+ const badgeOpacity = chartReveal < 0.5 ? (chartReveal - 0.25) / 0.25 : 1;
1372
+ badge.container.style.opacity = badgeOpacity < 1 ? String(badgeOpacity) : "";
1174
1373
  const { w, h, pad } = layout;
1175
1374
  const text = cfg.formatValue(smoothValue);
1176
1375
  badge.text.textContent = text;
@@ -1193,7 +1392,9 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
1193
1392
  badge.svg.setAttribute("height", String(pillH));
1194
1393
  badge.svg.setAttribute("viewBox", `0 0 ${totalW} ${pillH}`);
1195
1394
  badge.path.setAttribute("d", cfg.badgeTail ? badgeSvgPath(pillW, pillH, BADGE_TAIL_LEN, BADGE_TAIL_SPREAD) : badgePillOnly(pillW, pillH));
1196
- const targetBadgeY = Math.max(pad.top, Math.min(h - pad.bottom, layout.toY(smoothValue)));
1395
+ const centerY = pad.top + layout.chartH / 2;
1396
+ const realTargetY = Math.max(pad.top, Math.min(h - pad.bottom, layout.toY(smoothValue)));
1397
+ const targetBadgeY = chartReveal < 1 ? centerY + (realTargetY - centerY) * chartReveal : realTargetY;
1197
1398
  if (badgeY === null || noMotion) {
1198
1399
  badgeY = targetBadgeY;
1199
1400
  } else {
@@ -1258,12 +1459,20 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1258
1459
  const badgeYRef = useRef(null);
1259
1460
  const reducedMotionRef = useRef(false);
1260
1461
  const sizeRef = useRef({ w: 0, h: 0 });
1462
+ const ctxRef = useRef(null);
1261
1463
  const rafRef = useRef(0);
1262
1464
  const lastFrameRef = useRef(0);
1263
1465
  const badgeRef = useRef(null);
1264
1466
  const hoverXRef = useRef(null);
1265
1467
  const scrubAmountRef = useRef(0);
1266
1468
  const lastHoverRef = useRef(null);
1469
+ const chartRevealRef = useRef(0);
1470
+ const pauseProgressRef = useRef(0);
1471
+ const timeDebtRef = useRef(0);
1472
+ const lastDataRef = useRef([]);
1473
+ const frozenNowRef = useRef(0);
1474
+ const pausedDataRef = useRef(null);
1475
+ const loadingAlphaRef = useRef(config.loading ? 1 : 0);
1267
1476
  useEffect(() => {
1268
1477
  const container = containerRef.current;
1269
1478
  if (!container) return;
@@ -1384,19 +1593,75 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1384
1593
  canvas.style.width = `${w}px`;
1385
1594
  canvas.style.height = `${h}px`;
1386
1595
  }
1387
- const ctx = canvas.getContext("2d");
1596
+ let ctx = ctxRef.current;
1597
+ if (!ctx || ctx.canvas !== canvas) {
1598
+ ctx = canvas.getContext("2d");
1599
+ ctxRef.current = ctx;
1600
+ }
1388
1601
  if (!ctx) {
1389
1602
  rafRef.current = requestAnimationFrame(draw);
1390
1603
  return;
1391
1604
  }
1392
1605
  applyDpr(ctx, dpr, w, h);
1393
1606
  const noMotion = reducedMotionRef.current;
1394
- const points = cfg.data;
1395
- if (points.length < 2) {
1607
+ if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
1608
+ pausedDataRef.current = cfg.data.slice();
1609
+ }
1610
+ if (!cfg.paused) {
1611
+ pausedDataRef.current = null;
1612
+ }
1613
+ const points = pausedDataRef.current ?? cfg.data;
1614
+ const hasData = points.length >= 2;
1615
+ const pad = cfg.padding;
1616
+ const chartH = h - pad.top - pad.bottom;
1617
+ const pauseTarget = cfg.paused ? 1 : 0;
1618
+ pauseProgressRef.current = noMotion ? pauseTarget : lerp(pauseProgressRef.current, pauseTarget, PAUSE_PROGRESS_SPEED, dt);
1619
+ if (pauseProgressRef.current < 5e-3) pauseProgressRef.current = 0;
1620
+ if (pauseProgressRef.current > 0.995) pauseProgressRef.current = 1;
1621
+ const pauseProgress = pauseProgressRef.current;
1622
+ const pausedDt = dt * (1 - pauseProgress);
1623
+ const realDtSec = dt / 1e3;
1624
+ timeDebtRef.current += realDtSec * pauseProgress;
1625
+ if (!cfg.paused && timeDebtRef.current > 1e-3) {
1626
+ const catchUpSpeed = timeDebtRef.current > 10 ? PAUSE_CATCHUP_SPEED_FAST : PAUSE_CATCHUP_SPEED;
1627
+ timeDebtRef.current = lerp(timeDebtRef.current, 0, catchUpSpeed, dt);
1628
+ if (timeDebtRef.current < 0.01) timeDebtRef.current = 0;
1629
+ }
1630
+ const loadingTarget = cfg.loading ? 1 : 0;
1631
+ loadingAlphaRef.current = noMotion ? loadingTarget : lerp(loadingAlphaRef.current, loadingTarget, LOADING_ALPHA_SPEED, dt);
1632
+ if (loadingAlphaRef.current < 0.01) loadingAlphaRef.current = 0;
1633
+ if (loadingAlphaRef.current > 0.99) loadingAlphaRef.current = 1;
1634
+ const loadingAlpha = loadingAlphaRef.current;
1635
+ const revealTarget = !cfg.loading && hasData ? 1 : 0;
1636
+ chartRevealRef.current = noMotion ? revealTarget : lerp(chartRevealRef.current, revealTarget, CHART_REVEAL_SPEED, dt);
1637
+ if (Math.abs(chartRevealRef.current - revealTarget) < 5e-3) {
1638
+ chartRevealRef.current = revealTarget;
1639
+ }
1640
+ const chartReveal = chartRevealRef.current;
1641
+ const useStash = !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
1642
+ if (hasData) {
1643
+ lastDataRef.current = points;
1644
+ }
1645
+ if (!hasData && !useStash) {
1646
+ if (loadingAlpha > 0.01) {
1647
+ drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha);
1648
+ }
1649
+ if (1 - loadingAlpha > 0.01) {
1650
+ drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
1651
+ }
1652
+ ctx.save();
1653
+ ctx.globalCompositeOperation = "destination-out";
1654
+ const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
1655
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
1656
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
1657
+ ctx.fillStyle = fadeGrad;
1658
+ ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
1659
+ ctx.restore();
1396
1660
  if (badgeRef.current) badgeRef.current.container.style.display = "none";
1397
1661
  rafRef.current = requestAnimationFrame(draw);
1398
1662
  return;
1399
1663
  }
1664
+ const effectivePoints = useStash ? lastDataRef.current : points;
1400
1665
  const adaptiveSpeed = computeAdaptiveSpeed(
1401
1666
  cfg.value,
1402
1667
  displayValueRef.current,
@@ -1405,18 +1670,22 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1405
1670
  cfg.lerpSpeed,
1406
1671
  noMotion
1407
1672
  );
1408
- displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, dt);
1409
- const prevRange = displayMaxRef.current - displayMinRef.current || 1;
1410
- if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
1411
- displayValueRef.current = cfg.value;
1673
+ if (!useStash) {
1674
+ displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, pausedDt);
1675
+ if (pauseProgress < 0.5) {
1676
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
1677
+ if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
1678
+ displayValueRef.current = cfg.value;
1679
+ }
1680
+ }
1412
1681
  }
1413
1682
  const smoothValue = displayValueRef.current;
1414
- const pad = cfg.padding;
1415
1683
  const chartW = w - pad.left - pad.right;
1416
1684
  const needsArrowRoom = cfg.showMomentum;
1417
1685
  const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
1418
1686
  const transition = windowTransitionRef.current;
1419
- const now = Date.now() / 1e3;
1687
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
1688
+ const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
1420
1689
  const windowResult = updateWindowTransition(
1421
1690
  cfg,
1422
1691
  transition,
@@ -1426,7 +1695,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1426
1695
  noMotion,
1427
1696
  now_ms,
1428
1697
  now,
1429
- points,
1698
+ effectivePoints,
1430
1699
  smoothValue,
1431
1700
  buffer
1432
1701
  );
@@ -1435,9 +1704,10 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1435
1704
  const windowTransProgress = windowResult.windowTransProgress;
1436
1705
  const rightEdge = now + windowSecs * buffer;
1437
1706
  const leftEdge = rightEdge - windowSecs;
1707
+ const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
1438
1708
  const visible = [];
1439
- for (const p of points) {
1440
- if (p.time >= leftEdge - 2 && p.time <= rightEdge) {
1709
+ for (const p of effectivePoints) {
1710
+ if (p.time >= leftEdge - 2 && p.time <= filterRight) {
1441
1711
  visible.push(p);
1442
1712
  }
1443
1713
  }
@@ -1446,7 +1716,6 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1446
1716
  rafRef.current = requestAnimationFrame(draw);
1447
1717
  return;
1448
1718
  }
1449
- const chartH = h - pad.top - pad.bottom;
1450
1719
  const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
1451
1720
  const isWindowTransitioning = transition.startMs > 0;
1452
1721
  const rangeResult = updateRange(
@@ -1461,7 +1730,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1461
1730
  transition,
1462
1731
  adaptiveSpeed,
1463
1732
  chartH,
1464
- dt
1733
+ pausedDt
1465
1734
  );
1466
1735
  rangeInitedRef.current = rangeResult.rangeInited;
1467
1736
  targetMinRef.current = rangeResult.targetMin;
@@ -1535,8 +1804,18 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1535
1804
  particleState: cfg.degenOptions ? particleStateRef.current : void 0,
1536
1805
  particleOptions: cfg.degenOptions,
1537
1806
  swingMagnitude,
1538
- shakeState: cfg.degenOptions ? shakeStateRef.current : void 0
1807
+ shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
1808
+ chartReveal,
1809
+ pauseProgress,
1810
+ now_ms
1539
1811
  });
1812
+ const bgAlpha = 1 - chartReveal;
1813
+ if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
1814
+ const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
1815
+ if (bgEmptyAlpha > 0.01) {
1816
+ drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
1817
+ }
1818
+ }
1540
1819
  const badge = badgeRef.current;
1541
1820
  if (badge) {
1542
1821
  badgeYRef.current = updateBadgeDOM(
@@ -1550,8 +1829,13 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1550
1829
  isWindowTransitioning,
1551
1830
  noMotion,
1552
1831
  ctx,
1553
- dt
1832
+ pausedDt,
1833
+ chartReveal
1554
1834
  );
1835
+ if (pauseProgress > 0.01 && badge.container.style.display !== "none") {
1836
+ const base = badge.container.style.opacity ? parseFloat(badge.container.style.opacity) : 1;
1837
+ badge.container.style.opacity = String(base * (1 - pauseProgress));
1838
+ }
1555
1839
  }
1556
1840
  const valEl = cfg.valueDisplayRef?.current;
1557
1841
  if (valEl) {
@@ -1592,6 +1876,9 @@ function Liveline({
1592
1876
  momentum = true,
1593
1877
  fill = true,
1594
1878
  scrub = true,
1879
+ loading = false,
1880
+ paused = false,
1881
+ emptyText,
1595
1882
  exaggerate = false,
1596
1883
  degen: degenProp,
1597
1884
  badgeTail = true,
@@ -1621,7 +1908,7 @@ function Liveline({
1621
1908
  const windowBarRef = useRef2(null);
1622
1909
  const windowBtnRefs = useRef2(/* @__PURE__ */ new Map());
1623
1910
  const [indicatorStyle, setIndicatorStyle] = useState(null);
1624
- const palette = resolveTheme(color, theme);
1911
+ const palette = useMemo(() => resolveTheme(color, theme), [color, theme]);
1625
1912
  const isDark = theme === "dark";
1626
1913
  const showMomentum = momentum !== false;
1627
1914
  const momentumOverride = typeof momentum === "string" ? momentum : void 0;
@@ -1677,7 +1964,10 @@ function Liveline({
1677
1964
  tooltipOutline,
1678
1965
  valueMomentumColor,
1679
1966
  valueDisplayRef: showValue ? valueDisplayRef : void 0,
1680
- orderbookData: orderbook
1967
+ orderbookData: orderbook,
1968
+ loading,
1969
+ paused,
1970
+ emptyText
1681
1971
  });
1682
1972
  const cursorStyle = scrub ? cursor : "default";
1683
1973
  return /* @__PURE__ */ jsxs(Fragment, { children: [