liveline 0.0.2 → 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);
@@ -236,74 +246,153 @@ function drawGrid(ctx, layout, palette, formatValue, state, dt) {
236
246
  }
237
247
 
238
248
  // src/math/spline.ts
239
- function drawSpline(ctx, pts, tension = 0.15, maxPoints = 300) {
249
+ function drawSpline(ctx, pts) {
240
250
  if (pts.length < 2) return;
241
251
  if (pts.length === 2) {
242
252
  ctx.lineTo(pts[1][0], pts[1][1]);
243
253
  return;
244
254
  }
245
- let sampled = pts;
246
- if (pts.length > maxPoints) {
247
- const step = pts.length / maxPoints;
248
- sampled = [];
249
- for (let i = 0; i < maxPoints; i++) {
250
- sampled.push(pts[Math.round(i * step)]);
255
+ const n = pts.length;
256
+ const delta = new Array(n - 1);
257
+ const h = new Array(n - 1);
258
+ for (let i = 0; i < n - 1; i++) {
259
+ h[i] = pts[i + 1][0] - pts[i][0];
260
+ delta[i] = h[i] === 0 ? 0 : (pts[i + 1][1] - pts[i][1]) / h[i];
261
+ }
262
+ const m = new Array(n);
263
+ m[0] = delta[0];
264
+ m[n - 1] = delta[n - 2];
265
+ for (let i = 1; i < n - 1; i++) {
266
+ if (delta[i - 1] * delta[i] <= 0) {
267
+ m[i] = 0;
268
+ } else {
269
+ m[i] = (delta[i - 1] + delta[i]) / 2;
270
+ }
271
+ }
272
+ for (let i = 0; i < n - 1; i++) {
273
+ if (delta[i] === 0) {
274
+ m[i] = 0;
275
+ m[i + 1] = 0;
276
+ } else {
277
+ const alpha = m[i] / delta[i];
278
+ const beta = m[i + 1] / delta[i];
279
+ const s2 = alpha * alpha + beta * beta;
280
+ if (s2 > 9) {
281
+ const s = 3 / Math.sqrt(s2);
282
+ m[i] = s * alpha * delta[i];
283
+ m[i + 1] = s * beta * delta[i];
284
+ }
251
285
  }
252
- sampled.push(pts[pts.length - 1]);
253
286
  }
254
- for (let i = 0; i < sampled.length - 1; i++) {
255
- const p0 = sampled[Math.max(0, i - 1)];
256
- const p1 = sampled[i];
257
- const p2 = sampled[i + 1];
258
- const p3 = sampled[Math.min(sampled.length - 1, i + 2)];
287
+ for (let i = 0; i < n - 1; i++) {
288
+ const hi = h[i];
259
289
  ctx.bezierCurveTo(
260
- p1[0] + (p2[0] - p0[0]) * tension,
261
- p1[1] + (p2[1] - p0[1]) * tension,
262
- p2[0] - (p3[0] - p1[0]) * tension,
263
- p2[1] - (p3[1] - p1[1]) * tension,
264
- p2[0],
265
- p2[1]
290
+ pts[i][0] + hi / 3,
291
+ pts[i][1] + m[i] * hi / 3,
292
+ pts[i + 1][0] - hi / 3,
293
+ pts[i + 1][1] - m[i + 1] * hi / 3,
294
+ pts[i + 1][0],
295
+ pts[i + 1][1]
266
296
  );
267
297
  }
268
298
  }
269
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
+
270
310
  // src/draw/line.ts
271
- function renderCurve(ctx, layout, palette, pts, showFill, maxSplinePts) {
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) {
272
337
  const { h, pad } = layout;
273
- if (showFill) {
338
+ const baseAlpha = ctx.globalAlpha;
339
+ if (showFill && fillAlpha > 0.01) {
340
+ ctx.globalAlpha = baseAlpha * fillAlpha;
274
341
  const grad = ctx.createLinearGradient(0, pad.top, 0, h - pad.bottom);
275
342
  grad.addColorStop(0, palette.fillTop);
276
343
  grad.addColorStop(1, palette.fillBottom);
277
344
  ctx.beginPath();
278
345
  ctx.moveTo(pts[0][0], h - pad.bottom);
279
346
  ctx.lineTo(pts[0][0], pts[0][1]);
280
- drawSpline(ctx, pts, 0.15, maxSplinePts);
347
+ drawSpline(ctx, pts);
281
348
  ctx.lineTo(pts[pts.length - 1][0], h - pad.bottom);
282
349
  ctx.closePath();
283
350
  ctx.fillStyle = grad;
284
351
  ctx.fill();
285
352
  }
353
+ ctx.globalAlpha = baseAlpha * lineAlpha;
286
354
  ctx.beginPath();
287
355
  ctx.moveTo(pts[0][0], pts[0][1]);
288
- drawSpline(ctx, pts, 0.15, maxSplinePts);
289
- ctx.strokeStyle = palette.line;
356
+ drawSpline(ctx, pts);
357
+ ctx.strokeStyle = strokeColor ?? palette.line;
290
358
  ctx.lineWidth = palette.lineWidth;
291
359
  ctx.lineJoin = "round";
292
360
  ctx.lineCap = "round";
293
361
  ctx.stroke();
362
+ ctx.globalAlpha = baseAlpha;
294
363
  }
295
- function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0) {
296
- 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;
297
366
  const yMin = pad.top;
298
367
  const yMax = h - pad.bottom;
299
368
  const clampY = (y) => Math.max(yMin, Math.min(yMax, y));
300
- const pts = visible.map(
301
- (p, i) => i === visible.length - 1 ? [toX(p.time), clampY(toY(smoothValue))] : [toX(p.time), clampY(toY(p.value))]
302
- );
303
- 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)]);
304
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;
305
395
  const isScrubbing = scrubX !== null;
306
- const maxSplinePts = Math.max(300, Math.ceil(chartW));
307
396
  ctx.save();
308
397
  ctx.beginPath();
309
398
  ctx.rect(pad.left - 1, pad.top, chartW + 2, chartH);
@@ -313,24 +402,26 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
313
402
  ctx.beginPath();
314
403
  ctx.rect(0, 0, scrubX, h);
315
404
  ctx.clip();
316
- renderCurve(ctx, layout, palette, pts, showFill, maxSplinePts);
405
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
317
406
  ctx.restore();
318
407
  ctx.save();
319
408
  ctx.beginPath();
320
409
  ctx.rect(scrubX, 0, layout.w - scrubX, h);
321
410
  ctx.clip();
322
411
  ctx.globalAlpha = 1 - scrubAmount * 0.6;
323
- renderCurve(ctx, layout, palette, pts, showFill, maxSplinePts);
412
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
324
413
  ctx.restore();
325
414
  } else {
326
- renderCurve(ctx, layout, palette, pts, showFill, maxSplinePts);
415
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
327
416
  }
328
417
  ctx.restore();
329
- 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;
330
420
  ctx.setLineDash([4, 4]);
331
421
  ctx.strokeStyle = palette.dashLine;
332
422
  ctx.lineWidth = 1;
333
- 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;
334
425
  ctx.beginPath();
335
426
  ctx.moveTo(pad.left, currentY);
336
427
  ctx.lineTo(layout.w - pad.right, currentY);
@@ -345,27 +436,17 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
345
436
  // src/draw/dot.ts
346
437
  var PULSE_INTERVAL = 1500;
347
438
  var PULSE_DURATION = 900;
348
- function parseColor(color) {
349
- const hex = color.match(/^#([0-9a-f]{3,8})$/i);
350
- if (hex) {
351
- let h = hex[1];
352
- if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
353
- return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
354
- }
355
- const rgb = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
356
- if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
357
- return null;
358
- }
359
439
  function lerpColor(a, b, t) {
360
440
  const r = Math.round(a[0] + (b[0] - a[0]) * t);
361
441
  const g = Math.round(a[1] + (b[1] - a[1]) * t);
362
442
  const bl = Math.round(a[2] + (b[2] - a[2]) * t);
363
443
  return `rgb(${r},${g},${bl})`;
364
444
  }
365
- 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;
366
447
  const dim = scrubAmount * 0.7;
367
448
  if (pulse && dim < 0.3) {
368
- const t = Date.now() % PULSE_INTERVAL / PULSE_DURATION;
449
+ const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
369
450
  if (t < 1) {
370
451
  const radius = 9 + t * 12;
371
452
  const pulseAlpha = 0.35 * (1 - t) * (1 - dim * 3);
@@ -373,13 +454,13 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
373
454
  ctx.arc(x, y, radius, 0, Math.PI * 2);
374
455
  ctx.strokeStyle = palette.line;
375
456
  ctx.lineWidth = 1.5;
376
- ctx.globalAlpha = pulseAlpha;
457
+ ctx.globalAlpha = baseAlpha * pulseAlpha;
377
458
  ctx.stroke();
378
459
  }
379
460
  }
380
- const outerRgb = parseColor(palette.badgeOuterBg) ?? [255, 255, 255];
461
+ const outerRgb = parseColorRgb(palette.badgeOuterBg);
381
462
  ctx.save();
382
- ctx.globalAlpha = 1;
463
+ ctx.globalAlpha = baseAlpha;
383
464
  ctx.shadowColor = palette.badgeOuterShadow;
384
465
  ctx.shadowBlur = 6 * (1 - dim);
385
466
  ctx.shadowOffsetY = 1;
@@ -388,18 +469,19 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
388
469
  ctx.fillStyle = palette.badgeOuterBg;
389
470
  ctx.fill();
390
471
  ctx.restore();
391
- ctx.globalAlpha = 1;
472
+ ctx.globalAlpha = baseAlpha;
392
473
  ctx.beginPath();
393
474
  ctx.arc(x, y, 3.5, 0, Math.PI * 2);
394
475
  if (dim > 0.01) {
395
- const lineRgb = parseColor(palette.line) ?? [100, 100, 255];
476
+ const lineRgb = parseColorRgb(palette.line);
396
477
  ctx.fillStyle = lerpColor(lineRgb, outerRgb, dim);
397
478
  } else {
398
479
  ctx.fillStyle = palette.line;
399
480
  }
400
481
  ctx.fill();
401
482
  }
402
- 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;
403
485
  const upTarget = momentum === "up" ? 1 : 0;
404
486
  const downTarget = momentum === "down" ? 1 : 0;
405
487
  const canFadeInUp = arrows.down < 0.02;
@@ -410,7 +492,7 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
410
492
  if (arrows.down < 0.01) arrows.down = 0;
411
493
  if (arrows.up > 0.99) arrows.up = 1;
412
494
  if (arrows.down > 0.99) arrows.down = 1;
413
- const cycle = Date.now() % 1400 / 1400;
495
+ const cycle = now_ms % 1400 / 1400;
414
496
  const drawChevrons = (dir, opacity) => {
415
497
  if (opacity < 0.01) return;
416
498
  const baseX = x + 19;
@@ -426,7 +508,7 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
426
508
  const localT = cycle - start;
427
509
  const wave = localT >= 0 && localT < dur ? Math.sin(localT / dur * Math.PI) : 0;
428
510
  const pulse = 0.3 + 0.7 * wave;
429
- ctx.globalAlpha = opacity * pulse;
511
+ ctx.globalAlpha = baseAlpha * opacity * pulse;
430
512
  const nudge = dir === -1 ? -3 : 3;
431
513
  const cy = baseY + dir * (i * 8 - 4) + nudge;
432
514
  ctx.beginPath();
@@ -439,13 +521,13 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
439
521
  };
440
522
  drawChevrons(-1, arrows.up);
441
523
  drawChevrons(1, arrows.down);
442
- ctx.globalAlpha = 1;
524
+ ctx.globalAlpha = baseAlpha;
443
525
  }
444
526
 
445
527
  // src/draw/crosshair.ts
446
528
  function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, formatValue, formatTime, scrubOpacity, tooltipY, liveDotX, tooltipOutline) {
447
529
  if (scrubOpacity < 0.01) return;
448
- const { w, h, pad, toY } = layout;
530
+ const { h, pad, toY } = layout;
449
531
  const y = toY(hoverValue);
450
532
  ctx.save();
451
533
  ctx.globalAlpha = scrubOpacity * 0.5;
@@ -477,7 +559,7 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
477
559
  const totalW = valueW + sepW + timeW;
478
560
  let tx = hoverX - totalW / 2;
479
561
  const minX = pad.left + 4;
480
- const dotRightEdge = liveDotX != null ? liveDotX + 7 : w - pad.right;
562
+ const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
481
563
  const maxX = dotRightEdge - totalW;
482
564
  if (tx < minX) tx = minX;
483
565
  if (tx > maxX) tx = maxX;
@@ -552,7 +634,7 @@ function niceTimeInterval(windowSecs) {
552
634
 
553
635
  // src/draw/timeAxis.ts
554
636
  var FADE = 0.08;
555
- function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, formatTime, state, dt) {
637
+ function drawTimeAxis(ctx, layout, palette, windowSecs, targetWindowSecs, formatTime, state, dt) {
556
638
  const { h, pad, leftEdge, rightEdge, toX } = layout;
557
639
  const chartLeft = pad.left;
558
640
  const chartRight = layout.w - pad.right;
@@ -567,9 +649,9 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
567
649
  return fromEdge / fadeZone;
568
650
  };
569
651
  ctx.font = palette.labelFont;
570
- const targetPxPerSec = chartW / _targetWindowSecs;
571
- let interval = niceTimeInterval(_targetWindowSecs);
572
- while (interval * targetPxPerSec < 60 && interval < _targetWindowSecs) {
652
+ const targetPxPerSec = chartW / targetWindowSecs;
653
+ let interval = niceTimeInterval(targetWindowSecs);
654
+ while (interval * targetPxPerSec < 60 && interval < targetWindowSecs) {
573
655
  interval *= 2;
574
656
  }
575
657
  const useLocalDays = interval >= 86400;
@@ -606,6 +688,7 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
606
688
  label.alpha = next;
607
689
  }
608
690
  }
691
+ const baseAlpha = ctx.globalAlpha;
609
692
  const lineY = h - pad.bottom;
610
693
  const tickLen = 5;
611
694
  ctx.strokeStyle = palette.gridLine;
@@ -641,7 +724,7 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
641
724
  }
642
725
  for (const label of drawn) {
643
726
  ctx.save();
644
- ctx.globalAlpha = label.alpha;
727
+ ctx.globalAlpha = baseAlpha * label.alpha;
645
728
  ctx.strokeStyle = palette.gridLine;
646
729
  ctx.lineWidth = 1;
647
730
  ctx.beginPath();
@@ -762,11 +845,12 @@ function drawOrderbook(ctx, layout, palette, orderbook, dt, state, swingMagnitud
762
845
  state.labels[writeIdx++] = l;
763
846
  }
764
847
  state.labels.length = writeIdx;
848
+ const baseAlpha = ctx.globalAlpha;
765
849
  ctx.save();
766
850
  ctx.font = '600 13px "SF Mono", Menlo, monospace';
767
851
  ctx.textAlign = "left";
768
852
  ctx.textBaseline = "middle";
769
- ctx.globalAlpha = 1;
853
+ ctx.globalAlpha = baseAlpha;
770
854
  const outlineColor = `rgb(${bg[0]},${bg[1]},${bg[2]})`;
771
855
  for (let i = 0; i < state.labels.length; i++) {
772
856
  const l = state.labels[i];
@@ -883,18 +967,44 @@ function drawFrame(ctx, layout, palette, opts) {
883
967
  shake.amplitude *= decayRate;
884
968
  if (shake.amplitude < SHAKE_MIN_AMPLITUDE) shake.amplitude = 0;
885
969
  }
886
- 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;
887
979
  drawReferenceLine(ctx, layout, palette, opts.referenceLine);
980
+ ctx.restore();
888
981
  }
889
982
  if (opts.showGrid) {
890
- 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
+ }
891
990
  }
892
- if (opts.orderbookData && opts.orderbookState) {
991
+ if (opts.orderbookData && opts.orderbookState && reveal > 0.01) {
992
+ ctx.save();
993
+ if (reveal < 1) ctx.globalAlpha = reveal;
893
994
  drawOrderbook(ctx, layout, palette, opts.orderbookData, opts.dt, opts.orderbookState, opts.swingMagnitude);
995
+ ctx.restore();
894
996
  }
895
997
  const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
896
- const pts = drawLine(ctx, layout, palette, opts.visible, opts.smoothValue, opts.now, opts.showFill, scrubX, opts.scrubAmount);
897
- 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
+ }
898
1008
  if (pts && pts.length > 0) {
899
1009
  const lastPt = pts[pts.length - 1];
900
1010
  let dotScrub = opts.scrubAmount;
@@ -903,19 +1013,34 @@ function drawFrame(ctx, layout, palette, opts) {
903
1013
  const fadeStart = Math.min(80, layout.chartW * 0.3);
904
1014
  dotScrub = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
905
1015
  }
906
- 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
+ }
907
1024
  if (opts.showMomentum) {
908
- drawArrows(
909
- ctx,
910
- lastPt[0],
911
- lastPt[1],
912
- opts.momentum,
913
- palette,
914
- opts.arrowState,
915
- opts.dt
916
- );
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
+ }
917
1042
  }
918
- if (opts.particleState) {
1043
+ if (opts.particleState && reveal > 0.9) {
919
1044
  const burstIntensity = spawnOnSwing(
920
1045
  opts.particleState,
921
1046
  opts.momentum,
@@ -969,6 +1094,92 @@ function drawFrame(ctx, layout, palette, opts) {
969
1094
  }
970
1095
  }
971
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
+
972
1183
  // src/draw/badge.ts
973
1184
  function badgeSvgPath(pillW, pillH, tailLen, tailSpread) {
974
1185
  const r = pillH / 2;
@@ -1015,6 +1226,11 @@ var VALUE_SNAP_THRESHOLD = 1e-3;
1015
1226
  var ADAPTIVE_SPEED_BOOST = 0.2;
1016
1227
  var MOMENTUM_GREEN = [34, 197, 94];
1017
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;
1018
1234
  function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
1019
1235
  const valGap = Math.abs(value - displayValue);
1020
1236
  const prevRange = displayMax - displayMin || 1;
@@ -1146,12 +1362,14 @@ function updateHoverState(hoverPixelX, pad, w, layout, now, visible, scrubAmount
1146
1362
  lastHover
1147
1363
  };
1148
1364
  }
1149
- function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badgeColor, isWindowTransitioning, noMotion, ctx, dt) {
1150
- 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) {
1151
1367
  badge.container.style.display = "none";
1152
1368
  return badgeY;
1153
1369
  }
1154
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) : "";
1155
1373
  const { w, h, pad } = layout;
1156
1374
  const text = cfg.formatValue(smoothValue);
1157
1375
  badge.text.textContent = text;
@@ -1174,7 +1392,9 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
1174
1392
  badge.svg.setAttribute("height", String(pillH));
1175
1393
  badge.svg.setAttribute("viewBox", `0 0 ${totalW} ${pillH}`);
1176
1394
  badge.path.setAttribute("d", cfg.badgeTail ? badgeSvgPath(pillW, pillH, BADGE_TAIL_LEN, BADGE_TAIL_SPREAD) : badgePillOnly(pillW, pillH));
1177
- 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;
1178
1398
  if (badgeY === null || noMotion) {
1179
1399
  badgeY = targetBadgeY;
1180
1400
  } else {
@@ -1239,12 +1459,20 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1239
1459
  const badgeYRef = useRef(null);
1240
1460
  const reducedMotionRef = useRef(false);
1241
1461
  const sizeRef = useRef({ w: 0, h: 0 });
1462
+ const ctxRef = useRef(null);
1242
1463
  const rafRef = useRef(0);
1243
1464
  const lastFrameRef = useRef(0);
1244
1465
  const badgeRef = useRef(null);
1245
1466
  const hoverXRef = useRef(null);
1246
1467
  const scrubAmountRef = useRef(0);
1247
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);
1248
1476
  useEffect(() => {
1249
1477
  const container = containerRef.current;
1250
1478
  if (!container) return;
@@ -1365,19 +1593,75 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1365
1593
  canvas.style.width = `${w}px`;
1366
1594
  canvas.style.height = `${h}px`;
1367
1595
  }
1368
- 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
+ }
1369
1601
  if (!ctx) {
1370
1602
  rafRef.current = requestAnimationFrame(draw);
1371
1603
  return;
1372
1604
  }
1373
1605
  applyDpr(ctx, dpr, w, h);
1374
1606
  const noMotion = reducedMotionRef.current;
1375
- const points = cfg.data;
1376
- 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();
1377
1660
  if (badgeRef.current) badgeRef.current.container.style.display = "none";
1378
1661
  rafRef.current = requestAnimationFrame(draw);
1379
1662
  return;
1380
1663
  }
1664
+ const effectivePoints = useStash ? lastDataRef.current : points;
1381
1665
  const adaptiveSpeed = computeAdaptiveSpeed(
1382
1666
  cfg.value,
1383
1667
  displayValueRef.current,
@@ -1386,18 +1670,22 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1386
1670
  cfg.lerpSpeed,
1387
1671
  noMotion
1388
1672
  );
1389
- displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, dt);
1390
- const prevRange = displayMaxRef.current - displayMinRef.current || 1;
1391
- if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
1392
- 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
+ }
1393
1681
  }
1394
1682
  const smoothValue = displayValueRef.current;
1395
- const pad = cfg.padding;
1396
1683
  const chartW = w - pad.left - pad.right;
1397
1684
  const needsArrowRoom = cfg.showMomentum;
1398
1685
  const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
1399
1686
  const transition = windowTransitionRef.current;
1400
- 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;
1401
1689
  const windowResult = updateWindowTransition(
1402
1690
  cfg,
1403
1691
  transition,
@@ -1407,7 +1695,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1407
1695
  noMotion,
1408
1696
  now_ms,
1409
1697
  now,
1410
- points,
1698
+ effectivePoints,
1411
1699
  smoothValue,
1412
1700
  buffer
1413
1701
  );
@@ -1416,9 +1704,10 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1416
1704
  const windowTransProgress = windowResult.windowTransProgress;
1417
1705
  const rightEdge = now + windowSecs * buffer;
1418
1706
  const leftEdge = rightEdge - windowSecs;
1707
+ const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
1419
1708
  const visible = [];
1420
- for (const p of points) {
1421
- if (p.time >= leftEdge - 2 && p.time <= rightEdge) {
1709
+ for (const p of effectivePoints) {
1710
+ if (p.time >= leftEdge - 2 && p.time <= filterRight) {
1422
1711
  visible.push(p);
1423
1712
  }
1424
1713
  }
@@ -1427,7 +1716,6 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1427
1716
  rafRef.current = requestAnimationFrame(draw);
1428
1717
  return;
1429
1718
  }
1430
- const chartH = h - pad.top - pad.bottom;
1431
1719
  const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
1432
1720
  const isWindowTransitioning = transition.startMs > 0;
1433
1721
  const rangeResult = updateRange(
@@ -1442,7 +1730,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1442
1730
  transition,
1443
1731
  adaptiveSpeed,
1444
1732
  chartH,
1445
- dt
1733
+ pausedDt
1446
1734
  );
1447
1735
  rangeInitedRef.current = rangeResult.rangeInited;
1448
1736
  targetMinRef.current = rangeResult.targetMin;
@@ -1516,8 +1804,18 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1516
1804
  particleState: cfg.degenOptions ? particleStateRef.current : void 0,
1517
1805
  particleOptions: cfg.degenOptions,
1518
1806
  swingMagnitude,
1519
- shakeState: cfg.degenOptions ? shakeStateRef.current : void 0
1807
+ shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
1808
+ chartReveal,
1809
+ pauseProgress,
1810
+ now_ms
1520
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
+ }
1521
1819
  const badge = badgeRef.current;
1522
1820
  if (badge) {
1523
1821
  badgeYRef.current = updateBadgeDOM(
@@ -1531,8 +1829,13 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1531
1829
  isWindowTransitioning,
1532
1830
  noMotion,
1533
1831
  ctx,
1534
- dt
1832
+ pausedDt,
1833
+ chartReveal
1535
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
+ }
1536
1839
  }
1537
1840
  const valEl = cfg.valueDisplayRef?.current;
1538
1841
  if (valEl) {
@@ -1573,6 +1876,9 @@ function Liveline({
1573
1876
  momentum = true,
1574
1877
  fill = true,
1575
1878
  scrub = true,
1879
+ loading = false,
1880
+ paused = false,
1881
+ emptyText,
1576
1882
  exaggerate = false,
1577
1883
  degen: degenProp,
1578
1884
  badgeTail = true,
@@ -1602,7 +1908,7 @@ function Liveline({
1602
1908
  const windowBarRef = useRef2(null);
1603
1909
  const windowBtnRefs = useRef2(/* @__PURE__ */ new Map());
1604
1910
  const [indicatorStyle, setIndicatorStyle] = useState(null);
1605
- const palette = resolveTheme(color, theme);
1911
+ const palette = useMemo(() => resolveTheme(color, theme), [color, theme]);
1606
1912
  const isDark = theme === "dark";
1607
1913
  const showMomentum = momentum !== false;
1608
1914
  const momentumOverride = typeof momentum === "string" ? momentum : void 0;
@@ -1658,7 +1964,10 @@ function Liveline({
1658
1964
  tooltipOutline,
1659
1965
  valueMomentumColor,
1660
1966
  valueDisplayRef: showValue ? valueDisplayRef : void 0,
1661
- orderbookData: orderbook
1967
+ orderbookData: orderbook,
1968
+ loading,
1969
+ paused,
1970
+ emptyText
1662
1971
  });
1663
1972
  const cursorStyle = scrub ? cursor : "default";
1664
1973
  return /* @__PURE__ */ jsxs(Fragment, { children: [