liveline 0.0.3 → 0.0.5

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,52 @@ 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, colorBlend = 1, skipDashLine = false, fillScale = 1) {
365
+ const { h, pad, toX, toY, chartW, chartH } = layout;
366
+ const incomingAlpha = ctx.globalAlpha;
317
367
  const yMin = pad.top;
318
368
  const yMax = h - pad.bottom;
319
369
  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))]);
370
+ const centerY = pad.top + chartH / 2;
371
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
372
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
373
+ const morphY = chartReveal < 1 ? (rawY, x) => {
374
+ const t = Math.max(0, Math.min(1, (x - pad.left) / chartW));
375
+ const centerDist = Math.abs(t - 0.5) * 2;
376
+ const localReveal = Math.max(0, Math.min(1, (chartReveal - centerDist * 0.4) / 0.6));
377
+ const baseY = loadingY(t, centerY, amplitude, scroll);
378
+ return baseY + (rawY - baseY) * localReveal;
379
+ } : (rawY, _x) => rawY;
380
+ const pts = visible.map((p, i) => {
381
+ const x = toX(p.time);
382
+ const y = i === visible.length - 1 ? morphY(clampY(toY(smoothValue)), x) : morphY(clampY(toY(p.value)), x);
383
+ return [x, y];
384
+ });
385
+ const liveTipX = toX(now);
386
+ const fullRightX = pad.left + chartW;
387
+ const tipX = chartReveal < 1 ? liveTipX + (fullRightX - liveTipX) * (1 - chartReveal) : liveTipX;
388
+ pts.push([tipX, morphY(clampY(toY(smoothValue)), tipX)]);
324
389
  if (pts.length < 2) return;
390
+ let lineAlpha = 1;
391
+ let fillAlpha = fillScale;
392
+ if (chartReveal < 1) {
393
+ const breath = loadingBreath(now_ms);
394
+ lineAlpha = breath + (1 - breath) * chartReveal;
395
+ fillAlpha = chartReveal * fillScale;
396
+ }
397
+ const colorT = Math.min(1, chartReveal * 3) * colorBlend;
398
+ const strokeColor = chartReveal < 1 || colorBlend < 1 ? blendColor(palette.gridLabel, palette.line, colorT) : void 0;
325
399
  const isScrubbing = scrubX !== null;
326
400
  ctx.save();
327
401
  ctx.beginPath();
@@ -332,30 +406,34 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
332
406
  ctx.beginPath();
333
407
  ctx.rect(0, 0, scrubX, h);
334
408
  ctx.clip();
335
- renderCurve(ctx, layout, palette, pts, showFill);
409
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
336
410
  ctx.restore();
337
411
  ctx.save();
338
412
  ctx.beginPath();
339
413
  ctx.rect(scrubX, 0, layout.w - scrubX, h);
340
414
  ctx.clip();
341
- ctx.globalAlpha = 1 - scrubAmount * 0.6;
342
- renderCurve(ctx, layout, palette, pts, showFill);
415
+ ctx.globalAlpha = incomingAlpha * (1 - scrubAmount * 0.6);
416
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
343
417
  ctx.restore();
344
418
  } else {
345
- renderCurve(ctx, layout, palette, pts, showFill);
419
+ renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
346
420
  }
347
421
  ctx.restore();
348
- const currentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
349
- ctx.setLineDash([4, 4]);
350
- ctx.strokeStyle = palette.dashLine;
351
- ctx.lineWidth = 1;
352
- if (isScrubbing) ctx.globalAlpha = 1 - scrubAmount * 0.2;
353
- ctx.beginPath();
354
- ctx.moveTo(pad.left, currentY);
355
- ctx.lineTo(layout.w - pad.right, currentY);
356
- ctx.stroke();
357
- ctx.setLineDash([]);
358
- ctx.globalAlpha = 1;
422
+ if (!skipDashLine) {
423
+ const realCurrentY = Math.max(pad.top, Math.min(h - pad.bottom, toY(smoothValue)));
424
+ const currentY = chartReveal < 1 ? centerY + (realCurrentY - centerY) * chartReveal : realCurrentY;
425
+ ctx.setLineDash([4, 4]);
426
+ ctx.strokeStyle = palette.dashLine;
427
+ ctx.lineWidth = 1;
428
+ const dashBase = isScrubbing ? 1 - scrubAmount * 0.2 : 1;
429
+ ctx.globalAlpha = incomingAlpha * (chartReveal < 1 ? dashBase * chartReveal : dashBase);
430
+ ctx.beginPath();
431
+ ctx.moveTo(pad.left, currentY);
432
+ ctx.lineTo(layout.w - pad.right, currentY);
433
+ ctx.stroke();
434
+ ctx.setLineDash([]);
435
+ }
436
+ ctx.globalAlpha = incomingAlpha;
359
437
  const last = pts[pts.length - 1];
360
438
  last[1] = Math.max(10, Math.min(h - 10, last[1]));
361
439
  return pts;
@@ -364,27 +442,17 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
364
442
  // src/draw/dot.ts
365
443
  var PULSE_INTERVAL = 1500;
366
444
  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
445
  function lerpColor(a, b, t) {
379
446
  const r = Math.round(a[0] + (b[0] - a[0]) * t);
380
447
  const g = Math.round(a[1] + (b[1] - a[1]) * t);
381
448
  const bl = Math.round(a[2] + (b[2] - a[2]) * t);
382
449
  return `rgb(${r},${g},${bl})`;
383
450
  }
384
- function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
451
+ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = performance.now()) {
452
+ const baseAlpha = ctx.globalAlpha;
385
453
  const dim = scrubAmount * 0.7;
386
454
  if (pulse && dim < 0.3) {
387
- const t = Date.now() % PULSE_INTERVAL / PULSE_DURATION;
455
+ const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
388
456
  if (t < 1) {
389
457
  const radius = 9 + t * 12;
390
458
  const pulseAlpha = 0.35 * (1 - t) * (1 - dim * 3);
@@ -392,13 +460,13 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
392
460
  ctx.arc(x, y, radius, 0, Math.PI * 2);
393
461
  ctx.strokeStyle = palette.line;
394
462
  ctx.lineWidth = 1.5;
395
- ctx.globalAlpha = pulseAlpha;
463
+ ctx.globalAlpha = baseAlpha * pulseAlpha;
396
464
  ctx.stroke();
397
465
  }
398
466
  }
399
- const outerRgb = parseColor(palette.badgeOuterBg) ?? [255, 255, 255];
467
+ const outerRgb = parseColorRgb(palette.badgeOuterBg);
400
468
  ctx.save();
401
- ctx.globalAlpha = 1;
469
+ ctx.globalAlpha = baseAlpha;
402
470
  ctx.shadowColor = palette.badgeOuterShadow;
403
471
  ctx.shadowBlur = 6 * (1 - dim);
404
472
  ctx.shadowOffsetY = 1;
@@ -407,18 +475,19 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
407
475
  ctx.fillStyle = palette.badgeOuterBg;
408
476
  ctx.fill();
409
477
  ctx.restore();
410
- ctx.globalAlpha = 1;
478
+ ctx.globalAlpha = baseAlpha;
411
479
  ctx.beginPath();
412
480
  ctx.arc(x, y, 3.5, 0, Math.PI * 2);
413
481
  if (dim > 0.01) {
414
- const lineRgb = parseColor(palette.line) ?? [100, 100, 255];
482
+ const lineRgb = parseColorRgb(palette.line);
415
483
  ctx.fillStyle = lerpColor(lineRgb, outerRgb, dim);
416
484
  } else {
417
485
  ctx.fillStyle = palette.line;
418
486
  }
419
487
  ctx.fill();
420
488
  }
421
- function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
489
+ function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
490
+ const baseAlpha = ctx.globalAlpha;
422
491
  const upTarget = momentum === "up" ? 1 : 0;
423
492
  const downTarget = momentum === "down" ? 1 : 0;
424
493
  const canFadeInUp = arrows.down < 0.02;
@@ -429,7 +498,7 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
429
498
  if (arrows.down < 0.01) arrows.down = 0;
430
499
  if (arrows.up > 0.99) arrows.up = 1;
431
500
  if (arrows.down > 0.99) arrows.down = 1;
432
- const cycle = Date.now() % 1400 / 1400;
501
+ const cycle = now_ms % 1400 / 1400;
433
502
  const drawChevrons = (dir, opacity) => {
434
503
  if (opacity < 0.01) return;
435
504
  const baseX = x + 19;
@@ -445,7 +514,7 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
445
514
  const localT = cycle - start;
446
515
  const wave = localT >= 0 && localT < dur ? Math.sin(localT / dur * Math.PI) : 0;
447
516
  const pulse = 0.3 + 0.7 * wave;
448
- ctx.globalAlpha = opacity * pulse;
517
+ ctx.globalAlpha = baseAlpha * opacity * pulse;
449
518
  const nudge = dir === -1 ? -3 : 3;
450
519
  const cy = baseY + dir * (i * 8 - 4) + nudge;
451
520
  ctx.beginPath();
@@ -458,13 +527,13 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
458
527
  };
459
528
  drawChevrons(-1, arrows.up);
460
529
  drawChevrons(1, arrows.down);
461
- ctx.globalAlpha = 1;
530
+ ctx.globalAlpha = baseAlpha;
462
531
  }
463
532
 
464
533
  // src/draw/crosshair.ts
465
534
  function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, formatValue, formatTime, scrubOpacity, tooltipY, liveDotX, tooltipOutline) {
466
535
  if (scrubOpacity < 0.01) return;
467
- const { w, h, pad, toY } = layout;
536
+ const { h, pad, toY } = layout;
468
537
  const y = toY(hoverValue);
469
538
  ctx.save();
470
539
  ctx.globalAlpha = scrubOpacity * 0.5;
@@ -496,7 +565,7 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
496
565
  const totalW = valueW + sepW + timeW;
497
566
  let tx = hoverX - totalW / 2;
498
567
  const minX = pad.left + 4;
499
- const dotRightEdge = liveDotX != null ? liveDotX + 7 : w - pad.right;
568
+ const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
500
569
  const maxX = dotRightEdge - totalW;
501
570
  if (tx < minX) tx = minX;
502
571
  if (tx > maxX) tx = maxX;
@@ -571,7 +640,7 @@ function niceTimeInterval(windowSecs) {
571
640
 
572
641
  // src/draw/timeAxis.ts
573
642
  var FADE = 0.08;
574
- function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, formatTime, state, dt) {
643
+ function drawTimeAxis(ctx, layout, palette, windowSecs, targetWindowSecs, formatTime, state, dt) {
575
644
  const { h, pad, leftEdge, rightEdge, toX } = layout;
576
645
  const chartLeft = pad.left;
577
646
  const chartRight = layout.w - pad.right;
@@ -586,9 +655,9 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
586
655
  return fromEdge / fadeZone;
587
656
  };
588
657
  ctx.font = palette.labelFont;
589
- const targetPxPerSec = chartW / _targetWindowSecs;
590
- let interval = niceTimeInterval(_targetWindowSecs);
591
- while (interval * targetPxPerSec < 60 && interval < _targetWindowSecs) {
658
+ const targetPxPerSec = chartW / targetWindowSecs;
659
+ let interval = niceTimeInterval(targetWindowSecs);
660
+ while (interval * targetPxPerSec < 60 && interval < targetWindowSecs) {
592
661
  interval *= 2;
593
662
  }
594
663
  const useLocalDays = interval >= 86400;
@@ -625,6 +694,7 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
625
694
  label.alpha = next;
626
695
  }
627
696
  }
697
+ const baseAlpha = ctx.globalAlpha;
628
698
  const lineY = h - pad.bottom;
629
699
  const tickLen = 5;
630
700
  ctx.strokeStyle = palette.gridLine;
@@ -660,7 +730,7 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
660
730
  }
661
731
  for (const label of drawn) {
662
732
  ctx.save();
663
- ctx.globalAlpha = label.alpha;
733
+ ctx.globalAlpha = baseAlpha * label.alpha;
664
734
  ctx.strokeStyle = palette.gridLine;
665
735
  ctx.lineWidth = 1;
666
736
  ctx.beginPath();
@@ -781,11 +851,12 @@ function drawOrderbook(ctx, layout, palette, orderbook, dt, state, swingMagnitud
781
851
  state.labels[writeIdx++] = l;
782
852
  }
783
853
  state.labels.length = writeIdx;
854
+ const baseAlpha = ctx.globalAlpha;
784
855
  ctx.save();
785
856
  ctx.font = '600 13px "SF Mono", Menlo, monospace';
786
857
  ctx.textAlign = "left";
787
858
  ctx.textBaseline = "middle";
788
- ctx.globalAlpha = 1;
859
+ ctx.globalAlpha = baseAlpha;
789
860
  const outlineColor = `rgb(${bg[0]},${bg[1]},${bg[2]})`;
790
861
  for (let i = 0; i < state.labels.length; i++) {
791
862
  const l = state.labels[i];
@@ -879,6 +950,337 @@ function drawParticles(ctx, state, dt) {
879
950
  ctx.restore();
880
951
  }
881
952
 
953
+ // src/draw/candlestick.ts
954
+ var BULL = "#22c55e";
955
+ var BEAR = "#ef4444";
956
+ var BULL_RGB = [34, 197, 94];
957
+ var BEAR_RGB = [239, 68, 68];
958
+ function blendColor2(t) {
959
+ const r = Math.round(BEAR_RGB[0] + (BULL_RGB[0] - BEAR_RGB[0]) * t);
960
+ const g = Math.round(BEAR_RGB[1] + (BULL_RGB[1] - BEAR_RGB[1]) * t);
961
+ const b = Math.round(BEAR_RGB[2] + (BULL_RGB[2] - BEAR_RGB[2]) * t);
962
+ return `rgb(${r},${g},${b})`;
963
+ }
964
+ function parseRgb(color) {
965
+ const hex = color.match(/^#([0-9a-f]{6})$/i);
966
+ if (hex) {
967
+ const h = hex[1];
968
+ return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
969
+ }
970
+ const rgb = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
971
+ if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
972
+ return [128, 128, 128];
973
+ }
974
+ function blendToAccent(candleColor, accentColor, t) {
975
+ if (t <= 0) return candleColor;
976
+ if (t >= 1) return accentColor;
977
+ const [r1, g1, b1] = parseRgb(candleColor);
978
+ const [r2, g2, b2] = parseRgb(accentColor);
979
+ const r = Math.round(r1 + (r2 - r1) * t);
980
+ const g = Math.round(g1 + (g2 - g1) * t);
981
+ const b = Math.round(b1 + (b2 - b1) * t);
982
+ return `rgb(${r},${g},${b})`;
983
+ }
984
+ function candleDims(layout, candleWidthSecs) {
985
+ const pxPerSec = layout.chartW / (layout.rightEdge - layout.leftEdge);
986
+ const candlePxW = candleWidthSecs * pxPerSec;
987
+ const bodyW = Math.max(1, candlePxW * 0.7);
988
+ const wickW = Math.max(0.8, Math.min(2, bodyW * 0.15));
989
+ const radius = bodyW > 6 ? 1.5 : 0;
990
+ return { bodyW, wickW, radius };
991
+ }
992
+ function roundedRect(ctx, x, y, w, h, r) {
993
+ if (r <= 0 || h < r * 2) {
994
+ ctx.rect(x, y, w, h);
995
+ return;
996
+ }
997
+ ctx.moveTo(x + r, y);
998
+ ctx.lineTo(x + w - r, y);
999
+ ctx.arcTo(x + w, y, x + w, y + r, r);
1000
+ ctx.lineTo(x + w, y + h - r);
1001
+ ctx.arcTo(x + w, y + h, x + w - r, y + h, r);
1002
+ ctx.lineTo(x + r, y + h);
1003
+ ctx.arcTo(x, y + h, x, y + h - r, r);
1004
+ ctx.lineTo(x, y + r);
1005
+ ctx.arcTo(x, y, x + r, y, r);
1006
+ ctx.closePath();
1007
+ }
1008
+ function drawCandlesticks(ctx, layout, candles, candleWidthSecs, liveTime, now_ms, scrubX, scrubDim, liveAlpha = 1, liveBullBlend = -1, accentColor, accentBlend = 0) {
1009
+ if (candles.length === 0) return;
1010
+ const { toX, toY } = layout;
1011
+ const { bodyW, wickW, radius } = candleDims(layout, candleWidthSecs);
1012
+ const halfBody = bodyW / 2;
1013
+ const padL = layout.pad.left;
1014
+ const padR = layout.pad.left + layout.chartW;
1015
+ const livePulse = 0.12 + Math.sin(now_ms * 4e-3) * 0.08;
1016
+ for (const c of candles) {
1017
+ const cx = toX(c.time + candleWidthSecs / 2);
1018
+ if (cx + halfBody < padL || cx - halfBody > padR) continue;
1019
+ const isBull = c.close >= c.open;
1020
+ const isLive = c.time === liveTime;
1021
+ let color = isLive && liveBullBlend >= 0 ? blendColor2(liveBullBlend) : isBull ? BULL : BEAR;
1022
+ if (accentColor && accentBlend > 0.01) {
1023
+ color = blendToAccent(color, accentColor, accentBlend);
1024
+ }
1025
+ let candleAlpha = isLive ? liveAlpha : 1;
1026
+ if (scrubDim > 0.01 && scrubX > 0) {
1027
+ const dist = cx - scrubX;
1028
+ if (dist > 0) {
1029
+ const fadeZone = bodyW * 1.5;
1030
+ const dimT = Math.min(dist / fadeZone, 1);
1031
+ candleAlpha *= 1 - scrubDim * 0.5 * dimT;
1032
+ }
1033
+ }
1034
+ const baseAlpha = ctx.globalAlpha;
1035
+ ctx.globalAlpha = baseAlpha * candleAlpha;
1036
+ const bodyTop = toY(Math.max(c.open, c.close));
1037
+ const bodyBottom = toY(Math.min(c.open, c.close));
1038
+ const bodyH = Math.max(1, bodyBottom - bodyTop);
1039
+ const wickTop = toY(c.high);
1040
+ const wickBottom = toY(c.low);
1041
+ ctx.lineCap = "round";
1042
+ ctx.strokeStyle = color;
1043
+ if (bodyTop - wickTop > 0.5) {
1044
+ ctx.beginPath();
1045
+ ctx.moveTo(cx, bodyTop);
1046
+ ctx.lineTo(cx, wickTop);
1047
+ ctx.lineWidth = wickW;
1048
+ ctx.stroke();
1049
+ }
1050
+ if (wickBottom - bodyBottom > 0.5) {
1051
+ ctx.beginPath();
1052
+ ctx.moveTo(cx, bodyBottom);
1053
+ ctx.lineTo(cx, wickBottom);
1054
+ ctx.lineWidth = wickW;
1055
+ ctx.stroke();
1056
+ }
1057
+ ctx.fillStyle = color;
1058
+ ctx.beginPath();
1059
+ roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
1060
+ ctx.fill();
1061
+ if (isLive) {
1062
+ ctx.save();
1063
+ ctx.globalAlpha = baseAlpha * candleAlpha * livePulse;
1064
+ ctx.shadowColor = color;
1065
+ ctx.shadowBlur = 8;
1066
+ ctx.fillStyle = color;
1067
+ ctx.beginPath();
1068
+ roundedRect(ctx, cx - halfBody, bodyTop, bodyW, bodyH, radius);
1069
+ ctx.fill();
1070
+ ctx.restore();
1071
+ }
1072
+ ctx.globalAlpha = baseAlpha;
1073
+ }
1074
+ }
1075
+ function drawClosePrice(ctx, layout, palette, liveCandle, scrubDim, bullBlend = -1) {
1076
+ const y = layout.toY(liveCandle.close);
1077
+ if (y < layout.pad.top || y > layout.h - layout.pad.bottom) return;
1078
+ const isBull = liveCandle.close >= liveCandle.open;
1079
+ const color = bullBlend >= 0 ? blendColor2(bullBlend) : isBull ? BULL : BEAR;
1080
+ const baseAlpha = ctx.globalAlpha;
1081
+ ctx.save();
1082
+ ctx.setLineDash([4, 4]);
1083
+ ctx.strokeStyle = color;
1084
+ ctx.lineWidth = 1;
1085
+ ctx.globalAlpha = baseAlpha * (1 - scrubDim * 0.3) * 0.4;
1086
+ ctx.beginPath();
1087
+ ctx.moveTo(layout.pad.left, y);
1088
+ ctx.lineTo(layout.w - layout.pad.right, y);
1089
+ ctx.stroke();
1090
+ ctx.setLineDash([]);
1091
+ ctx.restore();
1092
+ }
1093
+ function drawCandleCrosshair(ctx, layout, palette, hoverX, candle, hoverTime, formatValue, formatTime, opacity) {
1094
+ if (opacity < 0.01) return;
1095
+ const { h, pad } = layout;
1096
+ ctx.save();
1097
+ ctx.globalAlpha = opacity * 0.5;
1098
+ ctx.strokeStyle = palette.crosshairLine;
1099
+ ctx.lineWidth = 1;
1100
+ ctx.beginPath();
1101
+ ctx.moveTo(hoverX, pad.top);
1102
+ ctx.lineTo(hoverX, h - pad.bottom);
1103
+ ctx.stroke();
1104
+ ctx.restore();
1105
+ if (opacity < 0.1 || layout.w < 200) return;
1106
+ const isBull = candle.close >= candle.open;
1107
+ const valueColor = isBull ? BULL : BEAR;
1108
+ const cl = formatValue(candle.close);
1109
+ const time = formatTime(hoverTime);
1110
+ ctx.save();
1111
+ ctx.globalAlpha = opacity;
1112
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
1113
+ ctx.textAlign = "left";
1114
+ let parts;
1115
+ if (layout.w >= 400) {
1116
+ const o = formatValue(candle.open);
1117
+ const hi = formatValue(candle.high);
1118
+ const lo = formatValue(candle.low);
1119
+ parts = [
1120
+ { text: "O ", color: palette.gridLabel },
1121
+ { text: o, color: valueColor },
1122
+ { text: " H ", color: palette.gridLabel },
1123
+ { text: hi, color: valueColor },
1124
+ { text: " L ", color: palette.gridLabel },
1125
+ { text: lo, color: valueColor },
1126
+ { text: " C ", color: palette.gridLabel },
1127
+ { text: cl, color: valueColor },
1128
+ { text: " \xB7 ", color: palette.gridLabel },
1129
+ { text: time, color: palette.gridLabel }
1130
+ ];
1131
+ } else {
1132
+ parts = [
1133
+ { text: "C ", color: palette.gridLabel },
1134
+ { text: cl, color: valueColor },
1135
+ { text: " \xB7 ", color: palette.gridLabel },
1136
+ { text: time, color: palette.gridLabel }
1137
+ ];
1138
+ }
1139
+ let totalW = 0;
1140
+ const widths = [];
1141
+ for (const p of parts) {
1142
+ const w = ctx.measureText(p.text).width;
1143
+ widths.push(w);
1144
+ totalW += w;
1145
+ }
1146
+ let tx = hoverX - totalW / 2;
1147
+ const minX = pad.left + 4;
1148
+ const maxX = layout.w - pad.right - totalW;
1149
+ if (tx < minX) tx = minX;
1150
+ if (tx > maxX) tx = maxX;
1151
+ const ty = pad.top + 24;
1152
+ ctx.strokeStyle = palette.tooltipBg;
1153
+ ctx.lineWidth = 3;
1154
+ ctx.lineJoin = "round";
1155
+ let cx = tx;
1156
+ for (let i = 0; i < parts.length; i++) {
1157
+ ctx.strokeText(parts[i].text, cx, ty);
1158
+ cx += widths[i];
1159
+ }
1160
+ cx = tx;
1161
+ for (let i = 0; i < parts.length; i++) {
1162
+ ctx.fillStyle = parts[i].color;
1163
+ ctx.fillText(parts[i].text, cx, ty);
1164
+ cx += widths[i];
1165
+ }
1166
+ ctx.restore();
1167
+ }
1168
+ function drawLineModeCrosshair(ctx, layout, palette, hoverX, value, hoverTime, formatValue, formatTime, opacity) {
1169
+ if (opacity < 0.01) return;
1170
+ const { h, pad } = layout;
1171
+ const y = layout.toY(value);
1172
+ ctx.save();
1173
+ ctx.globalAlpha = opacity * 0.5;
1174
+ ctx.strokeStyle = palette.crosshairLine;
1175
+ ctx.lineWidth = 1;
1176
+ ctx.beginPath();
1177
+ ctx.moveTo(hoverX, pad.top);
1178
+ ctx.lineTo(hoverX, h - pad.bottom);
1179
+ ctx.stroke();
1180
+ ctx.globalAlpha = opacity * 0.3;
1181
+ ctx.beginPath();
1182
+ ctx.moveTo(pad.left, y);
1183
+ ctx.lineTo(layout.w - pad.right, y);
1184
+ ctx.stroke();
1185
+ ctx.restore();
1186
+ if (opacity < 0.1 || layout.w < 200) return;
1187
+ const val = formatValue(value);
1188
+ const time = formatTime(hoverTime);
1189
+ ctx.save();
1190
+ ctx.globalAlpha = opacity;
1191
+ ctx.font = '400 13px "SF Mono", Menlo, monospace';
1192
+ ctx.textAlign = "left";
1193
+ const parts = [
1194
+ { text: val, color: palette.line },
1195
+ { text: " \xB7 ", color: palette.gridLabel },
1196
+ { text: time, color: palette.gridLabel }
1197
+ ];
1198
+ let totalW = 0;
1199
+ const widths = [];
1200
+ for (const p of parts) {
1201
+ const w = ctx.measureText(p.text).width;
1202
+ widths.push(w);
1203
+ totalW += w;
1204
+ }
1205
+ let tx = hoverX - totalW / 2;
1206
+ const minX = pad.left + 4;
1207
+ const maxX = layout.w - pad.right - totalW;
1208
+ if (tx < minX) tx = minX;
1209
+ if (tx > maxX) tx = maxX;
1210
+ const ty = pad.top + 24;
1211
+ ctx.strokeStyle = palette.tooltipBg;
1212
+ ctx.lineWidth = 3;
1213
+ ctx.lineJoin = "round";
1214
+ let lx = tx;
1215
+ for (let i = 0; i < parts.length; i++) {
1216
+ ctx.strokeText(parts[i].text, lx, ty);
1217
+ lx += widths[i];
1218
+ }
1219
+ lx = tx;
1220
+ for (let i = 0; i < parts.length; i++) {
1221
+ ctx.fillStyle = parts[i].color;
1222
+ ctx.fillText(parts[i].text, lx, ty);
1223
+ lx += widths[i];
1224
+ }
1225
+ ctx.restore();
1226
+ }
1227
+
1228
+ // src/draw/empty.ts
1229
+ function drawEmpty(ctx, w, h, pad, palette, alpha = 1, now_ms = 0, skipLine = false, emptyText) {
1230
+ const chartW = w - pad.left - pad.right;
1231
+ const chartH = h - pad.top - pad.bottom;
1232
+ const centerY = pad.top + chartH / 2;
1233
+ const cx = pad.left + chartW / 2;
1234
+ const text = emptyText ?? "No data to display";
1235
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1236
+ ctx.save();
1237
+ ctx.font = "400 12px system-ui, -apple-system, sans-serif";
1238
+ const textW = ctx.measureText(text).width;
1239
+ const gapHalf = textW / 2 + 20;
1240
+ const fadeW = 30;
1241
+ if (!skipLine) {
1242
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
1243
+ const breath = loadingBreath(now_ms);
1244
+ const numPts = 32;
1245
+ const pts = [];
1246
+ for (let i = 0; i <= numPts; i++) {
1247
+ const t = i / numPts;
1248
+ const x = pad.left + t * chartW;
1249
+ const y = loadingY(t, centerY, amplitude, scroll);
1250
+ pts.push([x, y]);
1251
+ }
1252
+ ctx.beginPath();
1253
+ ctx.moveTo(pts[0][0], pts[0][1]);
1254
+ drawSpline(ctx, pts);
1255
+ ctx.strokeStyle = palette.gridLabel;
1256
+ ctx.lineWidth = palette.lineWidth;
1257
+ ctx.globalAlpha = breath * alpha;
1258
+ ctx.lineCap = "round";
1259
+ ctx.lineJoin = "round";
1260
+ ctx.stroke();
1261
+ }
1262
+ ctx.save();
1263
+ ctx.globalCompositeOperation = "destination-out";
1264
+ const gapLeft = cx - gapHalf - fadeW;
1265
+ const gapRight = cx + gapHalf + fadeW;
1266
+ const eraseGrad = ctx.createLinearGradient(gapLeft, 0, gapRight, 0);
1267
+ eraseGrad.addColorStop(0, "rgba(0,0,0,0)");
1268
+ eraseGrad.addColorStop(fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1269
+ eraseGrad.addColorStop(1 - fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
1270
+ eraseGrad.addColorStop(1, "rgba(0,0,0,0)");
1271
+ ctx.fillStyle = eraseGrad;
1272
+ ctx.globalAlpha = alpha;
1273
+ const eraseH = amplitude * 2 + palette.lineWidth + 6;
1274
+ ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
1275
+ ctx.restore();
1276
+ ctx.textAlign = "center";
1277
+ ctx.textBaseline = "middle";
1278
+ ctx.globalAlpha = 0.35 * alpha;
1279
+ ctx.fillStyle = palette.gridLabel;
1280
+ ctx.fillText(text, cx, centerY);
1281
+ ctx.restore();
1282
+ }
1283
+
882
1284
  // src/draw/index.ts
883
1285
  var SHAKE_DECAY_RATE = 2e-3;
884
1286
  var SHAKE_MIN_AMPLITUDE = 0.2;
@@ -902,18 +1304,44 @@ function drawFrame(ctx, layout, palette, opts) {
902
1304
  shake.amplitude *= decayRate;
903
1305
  if (shake.amplitude < SHAKE_MIN_AMPLITUDE) shake.amplitude = 0;
904
1306
  }
905
- if (opts.referenceLine) {
1307
+ const reveal = opts.chartReveal;
1308
+ const pause = opts.pauseProgress;
1309
+ const revealRamp = (start, end) => {
1310
+ const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
1311
+ return t * t * (3 - 2 * t);
1312
+ };
1313
+ if (opts.referenceLine && reveal > 0.01) {
1314
+ ctx.save();
1315
+ if (reveal < 1) ctx.globalAlpha = reveal;
906
1316
  drawReferenceLine(ctx, layout, palette, opts.referenceLine);
1317
+ ctx.restore();
907
1318
  }
908
1319
  if (opts.showGrid) {
909
- drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1320
+ const gridAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1321
+ if (gridAlpha > 0.01) {
1322
+ ctx.save();
1323
+ if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
1324
+ drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1325
+ ctx.restore();
1326
+ }
910
1327
  }
911
- if (opts.orderbookData && opts.orderbookState) {
1328
+ if (opts.orderbookData && opts.orderbookState && reveal > 0.01) {
1329
+ ctx.save();
1330
+ if (reveal < 1) ctx.globalAlpha = reveal;
912
1331
  drawOrderbook(ctx, layout, palette, opts.orderbookData, opts.dt, opts.orderbookState, opts.swingMagnitude);
1332
+ ctx.restore();
913
1333
  }
914
1334
  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);
1335
+ const pts = drawLine(ctx, layout, palette, opts.visible, opts.smoothValue, opts.now, opts.showFill, scrubX, opts.scrubAmount, reveal, opts.now_ms);
1336
+ {
1337
+ const timeAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
1338
+ if (timeAlpha > 0.01) {
1339
+ ctx.save();
1340
+ if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
1341
+ drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1342
+ ctx.restore();
1343
+ }
1344
+ }
917
1345
  if (pts && pts.length > 0) {
918
1346
  const lastPt = pts[pts.length - 1];
919
1347
  let dotScrub = opts.scrubAmount;
@@ -922,19 +1350,34 @@ function drawFrame(ctx, layout, palette, opts) {
922
1350
  const fadeStart = Math.min(80, layout.chartW * 0.3);
923
1351
  dotScrub = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
924
1352
  }
925
- drawDot(ctx, lastPt[0], lastPt[1], palette, opts.showPulse, dotScrub);
1353
+ const dotAlpha = reveal < 0.3 ? 0 : (reveal - 0.3) / 0.7;
1354
+ const showPulse = opts.showPulse && reveal > 0.6 && pause < 0.5;
1355
+ if (dotAlpha > 0.01) {
1356
+ ctx.save();
1357
+ if (dotAlpha < 1) ctx.globalAlpha = dotAlpha;
1358
+ drawDot(ctx, lastPt[0], lastPt[1], palette, showPulse, dotScrub, opts.now_ms);
1359
+ ctx.restore();
1360
+ }
926
1361
  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
- );
1362
+ const arrowReveal = reveal < 1 ? revealRamp(0.6, 1) : 1;
1363
+ const arrowAlpha = arrowReveal * (1 - pause);
1364
+ if (arrowAlpha > 0.01) {
1365
+ ctx.save();
1366
+ if (arrowAlpha < 1) ctx.globalAlpha = arrowAlpha;
1367
+ drawArrows(
1368
+ ctx,
1369
+ lastPt[0],
1370
+ lastPt[1],
1371
+ opts.momentum,
1372
+ palette,
1373
+ opts.arrowState,
1374
+ opts.dt,
1375
+ opts.now_ms
1376
+ );
1377
+ ctx.restore();
1378
+ }
936
1379
  }
937
- if (opts.particleState) {
1380
+ if (opts.particleState && reveal > 0.9) {
938
1381
  const burstIntensity = spawnOnSwing(
939
1382
  opts.particleState,
940
1383
  opts.momentum,
@@ -987,6 +1430,235 @@ function drawFrame(ctx, layout, palette, opts) {
987
1430
  ctx.restore();
988
1431
  }
989
1432
  }
1433
+ function drawCandleFrame(ctx, layout, palette, opts) {
1434
+ const { w, h, pad, chartW, chartH } = layout;
1435
+ const reveal = opts.chartReveal;
1436
+ const fullLineMode = opts.lineModeProg >= 0.99;
1437
+ const revealLine = fullLineMode ? 1 - reveal : (1 - reveal) * (1 - reveal) * (1 - reveal);
1438
+ const lp = Math.max(opts.lineModeProg, revealLine);
1439
+ const colorBlend = lp > 1e-3 ? opts.lineModeProg / lp : 1;
1440
+ const revealRamp = (start, end) => {
1441
+ const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
1442
+ return t * t * (3 - 2 * t);
1443
+ };
1444
+ const gridAlpha = revealRamp(0.25, 0.6);
1445
+ if (opts.showGrid && gridAlpha > 0.01) {
1446
+ ctx.save();
1447
+ if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
1448
+ drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
1449
+ ctx.restore();
1450
+ }
1451
+ let linePts;
1452
+ if (lp > 0.01 && opts.lineVisible.length >= 2) {
1453
+ const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
1454
+ ctx.save();
1455
+ ctx.globalAlpha = lp;
1456
+ linePts = drawLine(
1457
+ ctx,
1458
+ layout,
1459
+ palette,
1460
+ opts.lineVisible,
1461
+ opts.lineSmoothValue,
1462
+ opts.now,
1463
+ opts.lineModeProg > 0.01,
1464
+ scrubX,
1465
+ opts.scrubAmount,
1466
+ opts.chartReveal,
1467
+ opts.now_ms,
1468
+ colorBlend,
1469
+ !fullLineMode,
1470
+ opts.lineModeProg
1471
+ // fillScale — fill fades smoothly with line mode transition
1472
+ );
1473
+ ctx.restore();
1474
+ }
1475
+ const closeAlpha = revealRamp(0.4, 0.8);
1476
+ const closeSource = opts.closePriceCandle ?? opts.liveCandle;
1477
+ if (closeSource && closeAlpha > 0.01) {
1478
+ if (lp < 0.99) {
1479
+ ctx.save();
1480
+ ctx.globalAlpha = closeAlpha * (1 - lp);
1481
+ drawClosePrice(ctx, layout, palette, closeSource, opts.scrubAmount, opts.liveBullBlend);
1482
+ ctx.restore();
1483
+ }
1484
+ if (lp > 0.01 && !fullLineMode) {
1485
+ const dashY = layout.toY(closeSource.close);
1486
+ if (dashY >= pad.top && dashY <= h - pad.bottom) {
1487
+ ctx.save();
1488
+ ctx.setLineDash([4, 4]);
1489
+ ctx.strokeStyle = palette.dashLine;
1490
+ ctx.lineWidth = 1;
1491
+ ctx.globalAlpha = closeAlpha * lp * (1 - opts.scrubAmount * 0.2);
1492
+ ctx.beginPath();
1493
+ ctx.moveTo(pad.left, dashY);
1494
+ ctx.lineTo(w - pad.right, dashY);
1495
+ ctx.stroke();
1496
+ ctx.setLineDash([]);
1497
+ ctx.restore();
1498
+ }
1499
+ }
1500
+ }
1501
+ const candleAlpha = opts.chartReveal * (1 - lp);
1502
+ if (candleAlpha > 0.01) {
1503
+ const ohlcScale = reveal * reveal * (3 - 2 * reveal);
1504
+ const collapseC = (c) => ohlcScale >= 0.99 ? c : {
1505
+ time: c.time,
1506
+ open: c.close + (c.open - c.close) * ohlcScale,
1507
+ high: c.close + (c.high - c.close) * ohlcScale,
1508
+ low: c.close + (c.low - c.close) * ohlcScale,
1509
+ close: c.close
1510
+ };
1511
+ const revealCandles = ohlcScale < 0.99 ? opts.candles.map(collapseC) : opts.candles;
1512
+ const revealOld = ohlcScale < 0.99 && opts.oldCandles.length > 0 ? opts.oldCandles.map(collapseC) : opts.oldCandles;
1513
+ ctx.save();
1514
+ ctx.beginPath();
1515
+ ctx.rect(pad.left - 1, pad.top, chartW + 2, chartH);
1516
+ ctx.clip();
1517
+ const accentCol = lp > 0.01 ? palette.line : void 0;
1518
+ if (opts.morphT >= 0 && revealOld.length > 0) {
1519
+ ctx.globalAlpha = (1 - opts.morphT) * candleAlpha;
1520
+ drawCandlesticks(
1521
+ ctx,
1522
+ layout,
1523
+ revealOld,
1524
+ opts.oldWidth,
1525
+ -1,
1526
+ opts.now_ms,
1527
+ opts.hoverX ?? 0,
1528
+ opts.scrubAmount,
1529
+ 1,
1530
+ -1,
1531
+ accentCol,
1532
+ lp
1533
+ );
1534
+ ctx.globalAlpha = opts.morphT * candleAlpha;
1535
+ drawCandlesticks(
1536
+ ctx,
1537
+ layout,
1538
+ revealCandles,
1539
+ opts.displayCandleWidth,
1540
+ opts.liveCandle?.time ?? -1,
1541
+ opts.now_ms,
1542
+ opts.hoverX ?? 0,
1543
+ opts.scrubAmount,
1544
+ opts.liveBirthAlpha,
1545
+ opts.liveBullBlend,
1546
+ accentCol,
1547
+ lp
1548
+ );
1549
+ ctx.globalAlpha = 1;
1550
+ } else {
1551
+ if (candleAlpha < 1) ctx.globalAlpha = candleAlpha;
1552
+ drawCandlesticks(
1553
+ ctx,
1554
+ layout,
1555
+ revealCandles,
1556
+ opts.displayCandleWidth,
1557
+ opts.liveCandle?.time ?? -1,
1558
+ opts.now_ms,
1559
+ opts.hoverX ?? 0,
1560
+ opts.scrubAmount,
1561
+ opts.liveBirthAlpha,
1562
+ opts.liveBullBlend,
1563
+ accentCol,
1564
+ lp
1565
+ );
1566
+ }
1567
+ ctx.restore();
1568
+ }
1569
+ if (lp > 0.5 && linePts && linePts.length > 0 && reveal > 0.3) {
1570
+ const lastPt = linePts[linePts.length - 1];
1571
+ const dotAlpha = (lp - 0.5) * 2 * ((reveal - 0.3) / 0.7);
1572
+ const showPulse = lp > 0.8 && reveal > 0.6;
1573
+ if (dotAlpha > 0.01) {
1574
+ ctx.save();
1575
+ ctx.globalAlpha = dotAlpha;
1576
+ drawDot(ctx, lastPt[0], lastPt[1], palette, showPulse, opts.scrubAmount, opts.now_ms);
1577
+ ctx.restore();
1578
+ }
1579
+ }
1580
+ const timeAlpha = revealRamp(0.25, 0.6);
1581
+ if (timeAlpha > 0.01) {
1582
+ ctx.save();
1583
+ if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
1584
+ drawTimeAxis(ctx, layout, palette, opts.targetWindowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
1585
+ ctx.restore();
1586
+ }
1587
+ ctx.save();
1588
+ ctx.globalCompositeOperation = "destination-out";
1589
+ const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
1590
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
1591
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
1592
+ ctx.fillStyle = fadeGrad;
1593
+ ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
1594
+ ctx.restore();
1595
+ if (opts.showEmptyOverlay) {
1596
+ const bgAlpha = 1 - opts.chartReveal;
1597
+ if (bgAlpha > 0.01) {
1598
+ const bgEmptyAlpha = (1 - opts.loadingAlpha) * bgAlpha;
1599
+ if (bgEmptyAlpha > 0.01) {
1600
+ drawEmpty(ctx, w, h, pad, palette, bgEmptyAlpha, opts.now_ms, true, opts.emptyText);
1601
+ }
1602
+ }
1603
+ }
1604
+ if (opts.chartReveal > 0.7 && opts.hoveredCandle && opts.hoverX !== null && opts.scrubAmount > 0.01) {
1605
+ if (opts.lineModeProg > 0.5) {
1606
+ drawLineModeCrosshair(
1607
+ ctx,
1608
+ layout,
1609
+ palette,
1610
+ opts.hoverX,
1611
+ opts.hoveredCandle.close,
1612
+ opts.hoverTime ?? 0,
1613
+ opts.formatValue,
1614
+ opts.formatTime,
1615
+ opts.scrubAmount
1616
+ );
1617
+ } else {
1618
+ drawCandleCrosshair(
1619
+ ctx,
1620
+ layout,
1621
+ palette,
1622
+ opts.hoverX,
1623
+ opts.hoveredCandle,
1624
+ opts.hoverTime ?? 0,
1625
+ opts.formatValue,
1626
+ opts.formatTime,
1627
+ opts.scrubAmount
1628
+ );
1629
+ }
1630
+ }
1631
+ }
1632
+
1633
+ // src/draw/loading.ts
1634
+ function drawLoading(ctx, w, h, pad, palette, now_ms, alpha = 1, strokeColor) {
1635
+ const chartW = w - pad.left - pad.right;
1636
+ const chartH = h - pad.top - pad.bottom;
1637
+ const centerY = pad.top + chartH / 2;
1638
+ const leftX = pad.left;
1639
+ const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
1640
+ const scroll = now_ms * LOADING_SCROLL_SPEED;
1641
+ const breath = loadingBreath(now_ms);
1642
+ const numPts = 32;
1643
+ const pts = [];
1644
+ for (let i = 0; i <= numPts; i++) {
1645
+ const t = i / numPts;
1646
+ const x = leftX + t * chartW;
1647
+ const y = loadingY(t, centerY, amplitude, scroll);
1648
+ pts.push([x, y]);
1649
+ }
1650
+ ctx.save();
1651
+ ctx.beginPath();
1652
+ ctx.moveTo(pts[0][0], pts[0][1]);
1653
+ drawSpline(ctx, pts);
1654
+ ctx.strokeStyle = strokeColor ?? palette.line;
1655
+ ctx.lineWidth = palette.lineWidth;
1656
+ ctx.globalAlpha = breath * alpha;
1657
+ ctx.lineCap = "round";
1658
+ ctx.lineJoin = "round";
1659
+ ctx.stroke();
1660
+ ctx.restore();
1661
+ }
990
1662
 
991
1663
  // src/draw/badge.ts
992
1664
  function badgeSvgPath(pillW, pillH, tailLen, tailSpread) {
@@ -1034,6 +1706,23 @@ var VALUE_SNAP_THRESHOLD = 1e-3;
1034
1706
  var ADAPTIVE_SPEED_BOOST = 0.2;
1035
1707
  var MOMENTUM_GREEN = [34, 197, 94];
1036
1708
  var MOMENTUM_RED = [239, 68, 68];
1709
+ var CHART_REVEAL_SPEED = 0.14;
1710
+ var CHART_REVEAL_SPEED_FWD = 0.09;
1711
+ var PAUSE_PROGRESS_SPEED = 0.12;
1712
+ var PAUSE_CATCHUP_SPEED = 0.08;
1713
+ var PAUSE_CATCHUP_SPEED_FAST = 0.22;
1714
+ var LOADING_ALPHA_SPEED = 0.14;
1715
+ var CANDLE_LERP_SPEED = 0.25;
1716
+ var CANDLE_WIDTH_TRANS_MS = 300;
1717
+ var LINE_MORPH_MS = 500;
1718
+ var CLOSE_LINE_LERP_SPEED = 0.25;
1719
+ var LINE_DENSITY_MS = 350;
1720
+ var LINE_LERP_BASE = 0.08;
1721
+ var LINE_ADAPTIVE_BOOST = 0.2;
1722
+ var LINE_SNAP_THRESHOLD = 1e-3;
1723
+ var RANGE_LERP_SPEED = 0.15;
1724
+ var RANGE_ADAPTIVE_BOOST = 0.2;
1725
+ var CANDLE_BUFFER = 0.05;
1037
1726
  function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
1038
1727
  const valGap = Math.abs(value - displayValue);
1039
1728
  const prevRange = displayMax - displayMin || 1;
@@ -1165,12 +1854,14 @@ function updateHoverState(hoverPixelX, pad, w, layout, now, visible, scrubAmount
1165
1854
  lastHover
1166
1855
  };
1167
1856
  }
1168
- function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badgeColor, isWindowTransitioning, noMotion, ctx, dt) {
1169
- if (!cfg.showBadge) {
1857
+ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badgeColor, isWindowTransitioning, noMotion, ctx, dt, chartReveal = 1) {
1858
+ if (!cfg.showBadge || chartReveal < 0.25) {
1170
1859
  badge.container.style.display = "none";
1171
1860
  return badgeY;
1172
1861
  }
1173
1862
  badge.container.style.display = "";
1863
+ const badgeOpacity = chartReveal < 0.5 ? (chartReveal - 0.25) / 0.25 : 1;
1864
+ badge.container.style.opacity = badgeOpacity < 1 ? String(badgeOpacity) : "";
1174
1865
  const { w, h, pad } = layout;
1175
1866
  const text = cfg.formatValue(smoothValue);
1176
1867
  badge.text.textContent = text;
@@ -1193,7 +1884,9 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
1193
1884
  badge.svg.setAttribute("height", String(pillH));
1194
1885
  badge.svg.setAttribute("viewBox", `0 0 ${totalW} ${pillH}`);
1195
1886
  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)));
1887
+ const centerY = pad.top + layout.chartH / 2;
1888
+ const realTargetY = Math.max(pad.top, Math.min(h - pad.bottom, layout.toY(smoothValue)));
1889
+ const targetBadgeY = chartReveal < 1 ? centerY + (realTargetY - centerY) * chartReveal : realTargetY;
1197
1890
  if (badgeY === null || noMotion) {
1198
1891
  badgeY = targetBadgeY;
1199
1892
  } else {
@@ -1229,6 +1922,115 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
1229
1922
  }
1230
1923
  return badgeY;
1231
1924
  }
1925
+ function computeCandleRange(candles) {
1926
+ let min = Infinity;
1927
+ let max = -Infinity;
1928
+ for (const c of candles) {
1929
+ if (c.low < min) min = c.low;
1930
+ if (c.high > max) max = c.high;
1931
+ }
1932
+ if (!isFinite(min) || !isFinite(max)) return { min: 99, max: 101 };
1933
+ const range = max - min;
1934
+ const margin = range * 0.12;
1935
+ const minRange = range * 0.1 || 0.4;
1936
+ if (range < minRange) {
1937
+ const mid = (min + max) / 2;
1938
+ return { min: mid - minRange / 2, max: mid + minRange / 2 };
1939
+ }
1940
+ return { min: min - margin, max: max + margin };
1941
+ }
1942
+ function candleAtX(candles, hoverX, candleWidth, layout) {
1943
+ const time = layout.leftEdge + (hoverX - layout.pad.left) / layout.chartW * (layout.rightEdge - layout.leftEdge);
1944
+ let lo = 0;
1945
+ let hi = candles.length - 1;
1946
+ while (lo <= hi) {
1947
+ const mid = lo + hi >> 1;
1948
+ const c = candles[mid];
1949
+ if (time < c.time) hi = mid - 1;
1950
+ else if (time >= c.time + candleWidth) lo = mid + 1;
1951
+ else return c;
1952
+ }
1953
+ return null;
1954
+ }
1955
+ function updateCandleRange(computedRange, rangeInited, displayMin, displayMax, isTransitioning, windowTransProgress, wt, chartH, dt) {
1956
+ if (!rangeInited) {
1957
+ return {
1958
+ minVal: computedRange.min,
1959
+ maxVal: computedRange.max,
1960
+ valRange: computedRange.max - computedRange.min || 1e-3,
1961
+ displayMin: computedRange.min,
1962
+ displayMax: computedRange.max,
1963
+ rangeInited: true
1964
+ };
1965
+ }
1966
+ if (isTransitioning) {
1967
+ displayMin = wt.rangeFromMin + (wt.rangeToMin - wt.rangeFromMin) * windowTransProgress;
1968
+ displayMax = wt.rangeFromMax + (wt.rangeToMax - wt.rangeFromMax) * windowTransProgress;
1969
+ } else {
1970
+ const curRange = displayMax - displayMin || 1;
1971
+ const gapMin = Math.abs(displayMin - computedRange.min);
1972
+ const gapMax = Math.abs(displayMax - computedRange.max);
1973
+ const gapRatio = Math.min((gapMin + gapMax) / curRange, 1);
1974
+ const speed = RANGE_LERP_SPEED + (1 - gapRatio) * RANGE_ADAPTIVE_BOOST;
1975
+ displayMin = lerp(displayMin, computedRange.min, speed, dt);
1976
+ displayMax = lerp(displayMax, computedRange.max, speed, dt);
1977
+ const pxThreshold = 0.5 * curRange / chartH || 1e-3;
1978
+ if (Math.abs(displayMin - computedRange.min) < pxThreshold) displayMin = computedRange.min;
1979
+ if (Math.abs(displayMax - computedRange.max) < pxThreshold) displayMax = computedRange.max;
1980
+ }
1981
+ return {
1982
+ minVal: displayMin,
1983
+ maxVal: displayMax,
1984
+ valRange: displayMax - displayMin || 1e-3,
1985
+ displayMin,
1986
+ displayMax,
1987
+ rangeInited: true
1988
+ };
1989
+ }
1990
+ function updateCandleWindowTransition(targetWindowSecs, wt, displayWindow, displayMin, displayMax, now_ms, now, candles, liveCandle, candleWidth, buffer) {
1991
+ if (wt.to !== targetWindowSecs) {
1992
+ wt.from = displayWindow;
1993
+ wt.to = targetWindowSecs;
1994
+ wt.startMs = now_ms;
1995
+ wt.rangeFromMin = displayMin;
1996
+ wt.rangeFromMax = displayMax;
1997
+ const targetRightEdge = now + targetWindowSecs * buffer;
1998
+ const targetLeftEdge = targetRightEdge - targetWindowSecs;
1999
+ const targetVisible = [];
2000
+ for (const c of candles) {
2001
+ if (c.time + candleWidth >= targetLeftEdge && c.time <= targetRightEdge) {
2002
+ targetVisible.push(c);
2003
+ }
2004
+ }
2005
+ if (liveCandle && liveCandle.time + candleWidth >= targetLeftEdge && liveCandle.time <= targetRightEdge) {
2006
+ targetVisible.push(liveCandle);
2007
+ }
2008
+ if (targetVisible.length > 0) {
2009
+ const tr = computeCandleRange(targetVisible);
2010
+ wt.rangeToMin = tr.min;
2011
+ wt.rangeToMax = tr.max;
2012
+ }
2013
+ }
2014
+ let windowTransProgress = 0;
2015
+ let resultWindow;
2016
+ if (wt.startMs === 0) {
2017
+ resultWindow = targetWindowSecs;
2018
+ } else {
2019
+ const elapsed = now_ms - wt.startMs;
2020
+ const t = Math.min(elapsed / WINDOW_TRANSITION_MS, 1);
2021
+ const eased = (1 - Math.cos(t * Math.PI)) / 2;
2022
+ windowTransProgress = eased;
2023
+ const logFrom = Math.log(wt.from);
2024
+ const logTo = Math.log(wt.to);
2025
+ resultWindow = Math.exp(logFrom + (logTo - logFrom) * eased);
2026
+ if (t >= 1) {
2027
+ resultWindow = targetWindowSecs;
2028
+ wt.startMs = 0;
2029
+ windowTransProgress = 0;
2030
+ }
2031
+ }
2032
+ return { windowSecs: resultWindow, windowTransProgress };
2033
+ }
1232
2034
  function useLivelineEngine(canvasRef, containerRef, config) {
1233
2035
  const configRef = useRef(config);
1234
2036
  configRef.current = config;
@@ -1258,12 +2060,53 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1258
2060
  const badgeYRef = useRef(null);
1259
2061
  const reducedMotionRef = useRef(false);
1260
2062
  const sizeRef = useRef({ w: 0, h: 0 });
2063
+ const ctxRef = useRef(null);
1261
2064
  const rafRef = useRef(0);
1262
2065
  const lastFrameRef = useRef(0);
1263
2066
  const badgeRef = useRef(null);
1264
2067
  const hoverXRef = useRef(null);
1265
2068
  const scrubAmountRef = useRef(0);
1266
2069
  const lastHoverRef = useRef(null);
2070
+ const chartRevealRef = useRef(0);
2071
+ const pauseProgressRef = useRef(0);
2072
+ const timeDebtRef = useRef(0);
2073
+ const lastDataRef = useRef([]);
2074
+ const frozenNowRef = useRef(0);
2075
+ const pausedDataRef = useRef(null);
2076
+ const loadingAlphaRef = useRef(config.loading ? 1 : 0);
2077
+ const displayCandleRef = useRef(null);
2078
+ const liveBirthAlphaRef = useRef(1);
2079
+ const liveBullRef = useRef(0.5);
2080
+ const lineSmoothCloseRef = useRef(0);
2081
+ const lineSmoothInitedRef = useRef(false);
2082
+ const closeLineSmoothRef = useRef(0);
2083
+ const closeLineSmoothInitedRef = useRef(false);
2084
+ const lineModeProgRef = useRef(0);
2085
+ const lineModeTransRef = useRef({ startMs: 0, from: 0, to: 0 });
2086
+ const lineDensityProgRef = useRef(0);
2087
+ const lineDensityTransRef = useRef({ startMs: 0, from: 0, to: 0 });
2088
+ const lineTickSmoothRef = useRef(0);
2089
+ const lineTickSmoothInitedRef = useRef(false);
2090
+ const candleWidthTransRef = useRef({
2091
+ fromWidth: config.candleWidth ?? 1,
2092
+ toWidth: config.candleWidth ?? 1,
2093
+ startMs: 0,
2094
+ rangeFromMin: 0,
2095
+ rangeFromMax: 0,
2096
+ rangeToMin: 0,
2097
+ rangeToMax: 0,
2098
+ oldCandles: [],
2099
+ oldWidth: config.candleWidth ?? 1
2100
+ });
2101
+ const prevCandleDataRef = useRef({ candles: [], width: config.candleWidth ?? 1 });
2102
+ const pausedCandlesRef = useRef(null);
2103
+ const pausedLiveRef = useRef(null);
2104
+ const pausedLineDataRef = useRef(null);
2105
+ const pausedLineValueRef = useRef(null);
2106
+ const lastCandlesRef = useRef([]);
2107
+ const lastLiveRef = useRef(null);
2108
+ const lastLineDataStashRef = useRef([]);
2109
+ const lastLineValueStashRef = useRef(void 0);
1267
2110
  useEffect(() => {
1268
2111
  const container = containerRef.current;
1269
2112
  if (!container) return;
@@ -1384,183 +2227,713 @@ function useLivelineEngine(canvasRef, containerRef, config) {
1384
2227
  canvas.style.width = `${w}px`;
1385
2228
  canvas.style.height = `${h}px`;
1386
2229
  }
1387
- const ctx = canvas.getContext("2d");
2230
+ let ctx = ctxRef.current;
2231
+ if (!ctx || ctx.canvas !== canvas) {
2232
+ ctx = canvas.getContext("2d");
2233
+ ctxRef.current = ctx;
2234
+ }
1388
2235
  if (!ctx) {
1389
2236
  rafRef.current = requestAnimationFrame(draw);
1390
2237
  return;
1391
2238
  }
1392
2239
  applyDpr(ctx, dpr, w, h);
1393
2240
  const noMotion = reducedMotionRef.current;
1394
- const points = cfg.data;
1395
- if (points.length < 2) {
1396
- if (badgeRef.current) badgeRef.current.container.style.display = "none";
1397
- rafRef.current = requestAnimationFrame(draw);
1398
- return;
1399
- }
1400
- const adaptiveSpeed = computeAdaptiveSpeed(
1401
- cfg.value,
1402
- displayValueRef.current,
1403
- displayMinRef.current,
1404
- displayMaxRef.current,
1405
- cfg.lerpSpeed,
1406
- noMotion
1407
- );
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;
2241
+ const isCandle = cfg.mode === "candle";
2242
+ if (isCandle) {
2243
+ if (cfg.paused && pausedCandlesRef.current === null && (cfg.candles?.length ?? 0) > 0) {
2244
+ pausedCandlesRef.current = cfg.candles.slice();
2245
+ pausedLiveRef.current = cfg.liveCandle ?? null;
2246
+ pausedLineDataRef.current = cfg.lineData?.slice() ?? null;
2247
+ pausedLineValueRef.current = cfg.lineValue ?? null;
2248
+ }
2249
+ if (!cfg.paused) {
2250
+ pausedCandlesRef.current = null;
2251
+ pausedLiveRef.current = null;
2252
+ pausedLineDataRef.current = null;
2253
+ pausedLineValueRef.current = null;
2254
+ }
2255
+ } else {
2256
+ if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
2257
+ pausedDataRef.current = cfg.data.slice();
2258
+ }
2259
+ if (!cfg.paused) {
2260
+ pausedDataRef.current = null;
2261
+ }
1412
2262
  }
1413
- const smoothValue = displayValueRef.current;
2263
+ const points = isCandle ? [] : pausedDataRef.current ?? cfg.data;
2264
+ const effectiveCandles = isCandle ? pausedCandlesRef.current ?? (cfg.candles ?? []) : [];
2265
+ const hasData = isCandle ? effectiveCandles.length >= 2 : points.length >= 2;
1414
2266
  const pad = cfg.padding;
1415
- const chartW = w - pad.left - pad.right;
1416
- const needsArrowRoom = cfg.showMomentum;
1417
- const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
1418
- const transition = windowTransitionRef.current;
1419
- const now = Date.now() / 1e3;
1420
- const windowResult = updateWindowTransition(
1421
- cfg,
1422
- transition,
1423
- displayWindowRef.current,
1424
- displayMinRef.current,
1425
- displayMaxRef.current,
1426
- noMotion,
1427
- now_ms,
1428
- now,
1429
- points,
1430
- smoothValue,
1431
- buffer
2267
+ const chartH = h - pad.top - pad.bottom;
2268
+ const pauseTarget = cfg.paused ? 1 : 0;
2269
+ pauseProgressRef.current = noMotion ? pauseTarget : lerp(pauseProgressRef.current, pauseTarget, PAUSE_PROGRESS_SPEED, dt);
2270
+ if (pauseProgressRef.current < 5e-3) pauseProgressRef.current = 0;
2271
+ if (pauseProgressRef.current > 0.995) pauseProgressRef.current = 1;
2272
+ const pauseProgress = pauseProgressRef.current;
2273
+ const pausedDt = dt * (1 - pauseProgress);
2274
+ const realDtSec = dt / 1e3;
2275
+ timeDebtRef.current += realDtSec * pauseProgress;
2276
+ if (!cfg.paused && timeDebtRef.current > 1e-3) {
2277
+ const catchUpSpeed = timeDebtRef.current > 10 ? PAUSE_CATCHUP_SPEED_FAST : PAUSE_CATCHUP_SPEED;
2278
+ timeDebtRef.current = lerp(timeDebtRef.current, 0, catchUpSpeed, dt);
2279
+ if (timeDebtRef.current < 0.01) timeDebtRef.current = 0;
2280
+ }
2281
+ const loadingTarget = cfg.loading ? 1 : 0;
2282
+ loadingAlphaRef.current = noMotion ? loadingTarget : lerp(loadingAlphaRef.current, loadingTarget, LOADING_ALPHA_SPEED, dt);
2283
+ if (loadingAlphaRef.current < 0.01) loadingAlphaRef.current = 0;
2284
+ if (loadingAlphaRef.current > 0.99) loadingAlphaRef.current = 1;
2285
+ const loadingAlpha = loadingAlphaRef.current;
2286
+ const revealTarget = !cfg.loading && hasData ? 1 : 0;
2287
+ chartRevealRef.current = noMotion ? revealTarget : lerp(
2288
+ chartRevealRef.current,
2289
+ revealTarget,
2290
+ revealTarget === 1 ? CHART_REVEAL_SPEED_FWD : CHART_REVEAL_SPEED,
2291
+ dt
1432
2292
  );
1433
- displayWindowRef.current = windowResult.windowSecs;
1434
- const windowSecs = windowResult.windowSecs;
1435
- const windowTransProgress = windowResult.windowTransProgress;
1436
- const rightEdge = now + windowSecs * buffer;
1437
- const leftEdge = rightEdge - windowSecs;
1438
- const visible = [];
1439
- for (const p of points) {
1440
- if (p.time >= leftEdge - 2 && p.time <= rightEdge) {
1441
- visible.push(p);
2293
+ if (Math.abs(chartRevealRef.current - revealTarget) < 5e-3) {
2294
+ chartRevealRef.current = revealTarget;
2295
+ }
2296
+ const chartReveal = chartRevealRef.current;
2297
+ if (chartReveal < 0.01) {
2298
+ rangeInitedRef.current = false;
2299
+ }
2300
+ let useStash;
2301
+ if (isCandle) {
2302
+ useStash = !hasData && chartReveal > 5e-3 && lastCandlesRef.current.length > 0;
2303
+ } else {
2304
+ useStash = !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
2305
+ if (hasData) lastDataRef.current = points;
2306
+ }
2307
+ if (isCandle) {
2308
+ const lmt = lineModeTransRef.current;
2309
+ const lineModeTarget = cfg.lineMode ? 1 : 0;
2310
+ if (lmt.to !== lineModeTarget) {
2311
+ lmt.from = lineModeProgRef.current;
2312
+ lmt.to = lineModeTarget;
2313
+ lmt.startMs = now_ms;
2314
+ }
2315
+ if (lmt.startMs > 0) {
2316
+ const elapsed = now_ms - lmt.startMs;
2317
+ const t = Math.min(elapsed / LINE_MORPH_MS, 1);
2318
+ lineModeProgRef.current = lmt.from + (lmt.to - lmt.from) * ((1 - Math.cos(t * Math.PI)) / 2);
2319
+ if (t >= 1) {
2320
+ lineModeProgRef.current = lmt.to;
2321
+ lmt.startMs = 0;
2322
+ }
2323
+ } else {
2324
+ lineModeProgRef.current = lmt.to;
1442
2325
  }
1443
2326
  }
1444
- if (visible.length < 2) {
2327
+ if (!hasData && !useStash) {
2328
+ const loadingColor = isCandle ? cfg.palette.gridLabel : void 0;
2329
+ if (loadingAlpha > 0.01) {
2330
+ drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha, loadingColor);
2331
+ }
2332
+ if (1 - loadingAlpha > 0.01) {
2333
+ drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
2334
+ }
2335
+ ctx.save();
2336
+ ctx.globalCompositeOperation = "destination-out";
2337
+ const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
2338
+ fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
2339
+ fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
2340
+ ctx.fillStyle = fadeGrad;
2341
+ ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
2342
+ ctx.restore();
1445
2343
  if (badgeRef.current) badgeRef.current.container.style.display = "none";
1446
2344
  rafRef.current = requestAnimationFrame(draw);
1447
2345
  return;
1448
2346
  }
1449
- const chartH = h - pad.top - pad.bottom;
1450
- const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
1451
- const isWindowTransitioning = transition.startMs > 0;
1452
- const rangeResult = updateRange(
1453
- computedRange,
1454
- rangeInitedRef.current,
1455
- targetMinRef.current,
1456
- targetMaxRef.current,
1457
- displayMinRef.current,
1458
- displayMaxRef.current,
1459
- isWindowTransitioning,
1460
- windowTransProgress,
1461
- transition,
1462
- adaptiveSpeed,
1463
- chartH,
1464
- dt
1465
- );
1466
- rangeInitedRef.current = rangeResult.rangeInited;
1467
- targetMinRef.current = rangeResult.targetMin;
1468
- targetMaxRef.current = rangeResult.targetMax;
1469
- displayMinRef.current = rangeResult.displayMin;
1470
- displayMaxRef.current = rangeResult.displayMax;
1471
- const { minVal, maxVal, valRange } = rangeResult;
1472
- const layout = {
1473
- w,
1474
- h,
1475
- pad,
1476
- chartW,
1477
- chartH,
1478
- leftEdge,
1479
- rightEdge,
1480
- minVal,
1481
- maxVal,
1482
- valRange,
1483
- toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
1484
- toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
1485
- };
1486
- const momentum = cfg.momentumOverride ?? detectMomentum(visible);
1487
- const hoverResult = updateHoverState(
1488
- hoverXRef.current,
1489
- pad,
1490
- w,
1491
- layout,
1492
- now,
1493
- visible,
1494
- scrubAmountRef.current,
1495
- lastHoverRef.current,
1496
- cfg,
1497
- noMotion,
1498
- leftEdge,
1499
- rightEdge,
1500
- chartW,
1501
- dt
1502
- );
1503
- scrubAmountRef.current = hoverResult.scrubAmount;
1504
- lastHoverRef.current = hoverResult.lastHover;
1505
- const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
1506
- const lookback = Math.min(5, visible.length - 1);
1507
- const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
1508
- const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
1509
- drawFrame(ctx, layout, cfg.palette, {
1510
- visible,
1511
- smoothValue,
1512
- now,
1513
- momentum,
1514
- arrowState: arrowStateRef.current,
1515
- showGrid: cfg.showGrid,
1516
- showMomentum: cfg.showMomentum,
1517
- showPulse: cfg.showPulse,
1518
- showFill: cfg.showFill,
1519
- referenceLine: cfg.referenceLine,
1520
- hoverX: drawHoverX,
1521
- hoverValue: drawHoverValue,
1522
- hoverTime: drawHoverTime,
1523
- scrubAmount: scrubAmountRef.current,
1524
- windowSecs,
1525
- formatValue: cfg.formatValue,
1526
- formatTime: cfg.formatTime,
1527
- gridState: gridStateRef.current,
1528
- timeAxisState: timeAxisStateRef.current,
1529
- dt,
1530
- targetWindowSecs: cfg.windowSecs,
1531
- tooltipY: cfg.tooltipY,
1532
- tooltipOutline: cfg.tooltipOutline,
1533
- orderbookData: cfg.orderbookData,
1534
- orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
1535
- particleState: cfg.degenOptions ? particleStateRef.current : void 0,
1536
- particleOptions: cfg.degenOptions,
1537
- swingMagnitude,
1538
- shakeState: cfg.degenOptions ? shakeStateRef.current : void 0
1539
- });
1540
- const badge = badgeRef.current;
1541
- if (badge) {
1542
- badgeYRef.current = updateBadgeDOM(
1543
- badge,
2347
+ if (isCandle) {
2348
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
2349
+ const now = hasData || chartReveal < 5e-3 ? Date.now() / 1e3 - timeDebtRef.current : frozenNowRef.current;
2350
+ const rawLive = pausedCandlesRef.current ? pausedLiveRef.current ?? void 0 : cfg.liveCandle;
2351
+ let effectiveLineData = pausedLineDataRef.current ?? cfg.lineData;
2352
+ let effectiveLineValue = pausedLineValueRef.current ?? cfg.lineValue;
2353
+ if (hasData && effectiveLineData && effectiveLineData.length > 0) {
2354
+ lastLineDataStashRef.current = effectiveLineData;
2355
+ lastLineValueStashRef.current = effectiveLineValue;
2356
+ }
2357
+ if (useStash && lastLineDataStashRef.current.length > 0) {
2358
+ effectiveLineData = lastLineDataStashRef.current;
2359
+ effectiveLineValue = lastLineValueStashRef.current;
2360
+ }
2361
+ const candleWidthSecs = cfg.candleWidth ?? 1;
2362
+ const cwt = candleWidthTransRef.current;
2363
+ let morphT = -1;
2364
+ let displayCandleWidth;
2365
+ if (cwt.startMs > 0) {
2366
+ const elapsed = now_ms - cwt.startMs;
2367
+ const t = Math.min(elapsed / CANDLE_WIDTH_TRANS_MS, 1);
2368
+ morphT = (1 - Math.cos(t * Math.PI)) / 2;
2369
+ displayCandleWidth = Math.exp(
2370
+ Math.log(cwt.fromWidth) + (Math.log(cwt.toWidth) - Math.log(cwt.fromWidth)) * morphT
2371
+ );
2372
+ if (t >= 1) {
2373
+ displayCandleWidth = cwt.toWidth;
2374
+ cwt.startMs = 0;
2375
+ morphT = -1;
2376
+ }
2377
+ } else {
2378
+ displayCandleWidth = cwt.toWidth;
2379
+ }
2380
+ if (candleWidthSecs !== cwt.toWidth) {
2381
+ cwt.oldCandles = prevCandleDataRef.current.candles;
2382
+ cwt.oldWidth = prevCandleDataRef.current.width;
2383
+ cwt.fromWidth = displayCandleWidth;
2384
+ cwt.toWidth = candleWidthSecs;
2385
+ cwt.startMs = now_ms;
2386
+ morphT = 0;
2387
+ cwt.rangeFromMin = displayMinRef.current;
2388
+ cwt.rangeFromMax = displayMaxRef.current;
2389
+ const curWindow = displayWindowRef.current;
2390
+ const re = now + curWindow * CANDLE_BUFFER;
2391
+ const le = re - curWindow;
2392
+ const targetVis = [];
2393
+ for (const c of effectiveCandles) {
2394
+ if (c.time + candleWidthSecs >= le && c.time <= re) targetVis.push(c);
2395
+ }
2396
+ if (rawLive) targetVis.push(rawLive);
2397
+ if (targetVis.length > 0) {
2398
+ const tr = computeCandleRange(targetVis);
2399
+ cwt.rangeToMin = tr.min;
2400
+ cwt.rangeToMax = tr.max;
2401
+ } else {
2402
+ cwt.rangeToMin = displayMinRef.current;
2403
+ cwt.rangeToMax = displayMaxRef.current;
2404
+ }
2405
+ }
2406
+ prevCandleDataRef.current = { candles: cfg.candles ?? [], width: candleWidthSecs };
2407
+ const lineModeProg = lineModeProgRef.current;
2408
+ const ldt = lineDensityTransRef.current;
2409
+ const hasTickData = effectiveLineData && effectiveLineData.length > 0;
2410
+ const densityTarget = cfg.lineMode && lineModeProg >= 0.3 && hasTickData ? 1 : 0;
2411
+ if (ldt.to !== densityTarget) {
2412
+ ldt.from = lineDensityProgRef.current;
2413
+ ldt.to = densityTarget;
2414
+ ldt.startMs = now_ms;
2415
+ }
2416
+ let lineDensityProg;
2417
+ if (ldt.startMs > 0) {
2418
+ const elapsed = now_ms - ldt.startMs;
2419
+ const t = Math.min(elapsed / LINE_DENSITY_MS, 1);
2420
+ lineDensityProg = ldt.from + (ldt.to - ldt.from) * (1 - (1 - t) * (1 - t));
2421
+ if (t >= 1) {
2422
+ lineDensityProg = ldt.to;
2423
+ ldt.startMs = 0;
2424
+ }
2425
+ } else {
2426
+ lineDensityProg = ldt.to;
2427
+ }
2428
+ lineDensityProgRef.current = lineDensityProg;
2429
+ const transition = windowTransitionRef.current;
2430
+ const windowResult = updateCandleWindowTransition(
2431
+ cfg.windowSecs,
2432
+ transition,
2433
+ displayWindowRef.current,
2434
+ displayMinRef.current,
2435
+ displayMaxRef.current,
2436
+ now_ms,
2437
+ now,
2438
+ effectiveCandles,
2439
+ rawLive,
2440
+ candleWidthSecs,
2441
+ CANDLE_BUFFER
2442
+ );
2443
+ displayWindowRef.current = windowResult.windowSecs;
2444
+ const windowSecs = windowResult.windowSecs;
2445
+ const windowTransProgress = windowResult.windowTransProgress;
2446
+ const isWindowTransitioning = transition.startMs > 0;
2447
+ const rightEdge = now + windowSecs * CANDLE_BUFFER;
2448
+ const leftEdge = rightEdge - windowSecs;
2449
+ let smoothLive;
2450
+ if (rawLive) {
2451
+ const prev = displayCandleRef.current;
2452
+ if (!prev || prev.time !== rawLive.time) {
2453
+ displayCandleRef.current = {
2454
+ time: rawLive.time,
2455
+ open: rawLive.open,
2456
+ high: rawLive.open,
2457
+ low: rawLive.open,
2458
+ close: rawLive.open
2459
+ };
2460
+ liveBirthAlphaRef.current = 0;
2461
+ } else {
2462
+ const dc2 = displayCandleRef.current;
2463
+ dc2.open = lerp(dc2.open, rawLive.open, CANDLE_LERP_SPEED, pausedDt);
2464
+ dc2.high = lerp(dc2.high, rawLive.high, CANDLE_LERP_SPEED, pausedDt);
2465
+ dc2.low = lerp(dc2.low, rawLive.low, CANDLE_LERP_SPEED, pausedDt);
2466
+ dc2.close = lerp(dc2.close, rawLive.close, CANDLE_LERP_SPEED, pausedDt);
2467
+ }
2468
+ liveBirthAlphaRef.current = lerp(liveBirthAlphaRef.current, 1, 0.2, pausedDt);
2469
+ if (liveBirthAlphaRef.current > 0.99) liveBirthAlphaRef.current = 1;
2470
+ const dc = displayCandleRef.current;
2471
+ const bullTarget = dc.close >= dc.open ? 1 : 0;
2472
+ liveBullRef.current = lerp(liveBullRef.current, bullTarget, 0.12, pausedDt);
2473
+ if (liveBullRef.current > 0.99) liveBullRef.current = 1;
2474
+ if (liveBullRef.current < 0.01) liveBullRef.current = 0;
2475
+ smoothLive = dc;
2476
+ } else {
2477
+ displayCandleRef.current = null;
2478
+ liveBirthAlphaRef.current = 1;
2479
+ liveBullRef.current = 0.5;
2480
+ }
2481
+ if (rawLive) {
2482
+ if (!closeLineSmoothInitedRef.current) {
2483
+ closeLineSmoothRef.current = rawLive.close;
2484
+ closeLineSmoothInitedRef.current = true;
2485
+ } else {
2486
+ closeLineSmoothRef.current = lerp(closeLineSmoothRef.current, rawLive.close, CLOSE_LINE_LERP_SPEED, pausedDt);
2487
+ const gap = Math.abs(closeLineSmoothRef.current - rawLive.close);
2488
+ const range = displayMaxRef.current - displayMinRef.current || 1;
2489
+ if (gap < range * 5e-4) closeLineSmoothRef.current = rawLive.close;
2490
+ }
2491
+ } else if (!useStash) {
2492
+ closeLineSmoothInitedRef.current = false;
2493
+ }
2494
+ if (rawLive) {
2495
+ if (!lineSmoothInitedRef.current) {
2496
+ lineSmoothCloseRef.current = rawLive.close;
2497
+ lineSmoothInitedRef.current = true;
2498
+ } else {
2499
+ const valGap = Math.abs(rawLive.close - lineSmoothCloseRef.current);
2500
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
2501
+ const gapRatio = Math.min(valGap / prevRange, 1);
2502
+ const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
2503
+ lineSmoothCloseRef.current = lerp(lineSmoothCloseRef.current, rawLive.close, adaptiveSpeed, pausedDt);
2504
+ if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineSmoothCloseRef.current = rawLive.close;
2505
+ }
2506
+ } else if (!useStash) {
2507
+ lineSmoothInitedRef.current = false;
2508
+ }
2509
+ if (effectiveLineValue !== void 0 && hasTickData) {
2510
+ if (!lineTickSmoothInitedRef.current) {
2511
+ lineTickSmoothRef.current = effectiveLineValue;
2512
+ lineTickSmoothInitedRef.current = true;
2513
+ } else {
2514
+ const valGap = Math.abs(effectiveLineValue - lineTickSmoothRef.current);
2515
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
2516
+ const gapRatio = Math.min(valGap / prevRange, 1);
2517
+ const adaptiveSpeed = LINE_LERP_BASE + (1 - gapRatio) * LINE_ADAPTIVE_BOOST;
2518
+ lineTickSmoothRef.current = lerp(lineTickSmoothRef.current, effectiveLineValue, adaptiveSpeed, pausedDt);
2519
+ if (valGap < prevRange * LINE_SNAP_THRESHOLD) lineTickSmoothRef.current = effectiveLineValue;
2520
+ }
2521
+ } else if (!useStash) {
2522
+ lineTickSmoothInitedRef.current = false;
2523
+ }
2524
+ const visible = [];
2525
+ for (const c of effectiveCandles) {
2526
+ if (c.time + candleWidthSecs >= leftEdge && c.time <= rightEdge) visible.push(c);
2527
+ }
2528
+ if (smoothLive && smoothLive.time + displayCandleWidth >= leftEdge && smoothLive.time <= rightEdge) {
2529
+ visible.push(smoothLive);
2530
+ }
2531
+ let oldVisible = [];
2532
+ if (morphT >= 0 && cwt.oldCandles.length > 0) {
2533
+ for (const c of cwt.oldCandles) {
2534
+ if (c.time + cwt.oldWidth >= leftEdge && c.time <= rightEdge) oldVisible.push(c);
2535
+ }
2536
+ }
2537
+ if (hasData) {
2538
+ lastCandlesRef.current = visible;
2539
+ lastLiveRef.current = smoothLive ?? null;
2540
+ }
2541
+ const effectiveVisible = useStash ? lastCandlesRef.current : visible;
2542
+ const effectiveLive = useStash ? lastLiveRef.current ?? void 0 : smoothLive;
2543
+ const chartW = w - pad.left - pad.right;
2544
+ const computed = effectiveVisible.length > 0 ? computeCandleRange(effectiveVisible) : { min: displayMinRef.current, max: displayMaxRef.current };
2545
+ const rangeResult = updateCandleRange(
2546
+ computed,
2547
+ rangeInitedRef.current,
2548
+ displayMinRef.current,
2549
+ displayMaxRef.current,
2550
+ isWindowTransitioning,
2551
+ windowTransProgress,
2552
+ transition,
2553
+ chartH,
2554
+ pausedDt
2555
+ );
2556
+ if (morphT >= 0) {
2557
+ rangeResult.displayMin = cwt.rangeFromMin + (cwt.rangeToMin - cwt.rangeFromMin) * morphT;
2558
+ rangeResult.displayMax = cwt.rangeFromMax + (cwt.rangeToMax - cwt.rangeFromMax) * morphT;
2559
+ rangeResult.minVal = rangeResult.displayMin;
2560
+ rangeResult.maxVal = rangeResult.displayMax;
2561
+ rangeResult.valRange = rangeResult.displayMax - rangeResult.displayMin || 1e-3;
2562
+ }
2563
+ rangeInitedRef.current = rangeResult.rangeInited;
2564
+ displayMinRef.current = rangeResult.displayMin;
2565
+ displayMaxRef.current = rangeResult.displayMax;
2566
+ const { minVal, maxVal, valRange } = rangeResult;
2567
+ const layout = {
2568
+ w,
2569
+ h,
2570
+ pad,
2571
+ chartW,
2572
+ chartH,
2573
+ leftEdge,
2574
+ rightEdge,
2575
+ minVal,
2576
+ maxVal,
2577
+ valRange,
2578
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
2579
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
2580
+ };
2581
+ const hoverPx = hoverXRef.current;
2582
+ let hoveredCandle = null;
2583
+ let isActiveHover = false;
2584
+ if (hoverPx !== null && hoverPx >= pad.left && hoverPx <= w - pad.right) {
2585
+ hoveredCandle = candleAtX(effectiveVisible, hoverPx, displayCandleWidth, layout);
2586
+ if (hoveredCandle) isActiveHover = true;
2587
+ }
2588
+ const scrubTarget = isActiveHover ? 1 : 0;
2589
+ scrubAmountRef.current = lerp(scrubAmountRef.current, scrubTarget, 0.12, dt);
2590
+ if (scrubAmountRef.current < 0.01) scrubAmountRef.current = 0;
2591
+ if (scrubAmountRef.current > 0.99) scrubAmountRef.current = 1;
2592
+ const scrubAmount = scrubAmountRef.current;
2593
+ let drawHoverX = hoverPx;
2594
+ let drawHoverTime = 0;
2595
+ let drawHoverCandle = hoveredCandle;
2596
+ if (!isActiveHover && scrubAmount > 0 && lastHoverRef.current) {
2597
+ drawHoverX = lastHoverRef.current.x;
2598
+ drawHoverTime = lastHoverRef.current.time;
2599
+ drawHoverCandle = candleAtX(effectiveVisible, lastHoverRef.current.x, displayCandleWidth, layout);
2600
+ } else if (isActiveHover && hoverPx !== null) {
2601
+ drawHoverTime = layout.leftEdge + (hoverPx - pad.left) / chartW * (layout.rightEdge - layout.leftEdge);
2602
+ lastHoverRef.current = { x: hoverPx, value: hoveredCandle?.close ?? 0, time: drawHoverTime };
2603
+ }
2604
+ let drawCandles = effectiveVisible;
2605
+ let drawOldCandles = oldVisible;
2606
+ let drawLive = effectiveLive;
2607
+ if (lineModeProg > 0.01 && drawLive && lineSmoothInitedRef.current) {
2608
+ const blended = drawLive.close + (lineSmoothCloseRef.current - drawLive.close) * lineModeProg;
2609
+ drawLive = { ...drawLive, close: blended };
2610
+ const li = drawCandles.length - 1;
2611
+ if (li >= 0 && drawCandles[li].time === drawLive.time) {
2612
+ drawCandles = drawCandles.slice();
2613
+ drawCandles[li] = { ...drawCandles[li], close: blended };
2614
+ }
2615
+ }
2616
+ if (lineModeProg > 0.01 && lineModeProg < 0.99) {
2617
+ const collapseOHLC = (c) => {
2618
+ const inv = 1 - lineModeProg;
2619
+ return {
2620
+ time: c.time,
2621
+ open: c.close + (c.open - c.close) * inv,
2622
+ high: c.close + (c.high - c.close) * inv,
2623
+ low: c.close + (c.low - c.close) * inv,
2624
+ close: c.close
2625
+ };
2626
+ };
2627
+ drawCandles = drawCandles.map(collapseOHLC);
2628
+ if (drawOldCandles.length > 0) drawOldCandles = drawOldCandles.map(collapseOHLC);
2629
+ if (drawLive) drawLive = collapseOHLC(drawLive);
2630
+ }
2631
+ let lineVisible;
2632
+ let lineSmoothValue;
2633
+ if (effectiveLineData && effectiveLineData.length > 0 && (lineDensityProg > 0.01 || lineModeProg > 0.05)) {
2634
+ const closeRefs = [];
2635
+ for (const c of drawCandles) {
2636
+ closeRefs.push({ t: c.time + displayCandleWidth / 2, v: c.close });
2637
+ }
2638
+ if (drawLive) closeRefs.push({ t: now, v: drawLive.close });
2639
+ lineVisible = [];
2640
+ let refIdx = 0;
2641
+ for (const pt of effectiveLineData) {
2642
+ if (pt.time < leftEdge || pt.time > rightEdge) continue;
2643
+ while (refIdx < closeRefs.length - 2 && closeRefs[refIdx + 1].t < pt.time) refIdx++;
2644
+ let interpClose;
2645
+ if (closeRefs.length === 0) {
2646
+ interpClose = pt.value;
2647
+ } else if (closeRefs.length === 1 || pt.time <= closeRefs[0].t) {
2648
+ interpClose = closeRefs[0].v;
2649
+ } else if (refIdx >= closeRefs.length - 1) {
2650
+ interpClose = closeRefs[closeRefs.length - 1].v;
2651
+ } else {
2652
+ const a = closeRefs[refIdx];
2653
+ const b = closeRefs[refIdx + 1];
2654
+ const span = b.t - a.t;
2655
+ const frac = span > 0 ? Math.max(0, Math.min(1, (pt.time - a.t) / span)) : 0;
2656
+ interpClose = a.v + (b.v - a.v) * frac;
2657
+ }
2658
+ const blended = interpClose + (pt.value - interpClose) * lineDensityProg;
2659
+ lineVisible.push({ time: pt.time, value: blended });
2660
+ }
2661
+ const smoothTick = lineTickSmoothInitedRef.current ? lineTickSmoothRef.current : effectiveLineValue ?? effectiveLineData[effectiveLineData.length - 1].value;
2662
+ lineSmoothValue = lineSmoothCloseRef.current + (smoothTick - lineSmoothCloseRef.current) * lineDensityProg;
2663
+ } else {
2664
+ lineVisible = drawCandles.map((c) => ({
2665
+ time: c.time + displayCandleWidth / 2,
2666
+ value: c.close
2667
+ }));
2668
+ lineSmoothValue = lineSmoothInitedRef.current ? lineSmoothCloseRef.current : drawLive?.close ?? drawCandles[drawCandles.length - 1]?.close ?? 0;
2669
+ }
2670
+ if (chartReveal < 1 && lineVisible.length >= 2) {
2671
+ const firstTime = lineVisible[0].time;
2672
+ const windowSpan = rightEdge - leftEdge;
2673
+ if (firstTime - leftEdge > windowSpan * 0.05) {
2674
+ const firstVal = lineVisible[0].value;
2675
+ const step = windowSpan / 32;
2676
+ const padded = [];
2677
+ for (let t = leftEdge; t < firstTime - step * 0.5; t += step) {
2678
+ padded.push({ time: t, value: firstVal });
2679
+ }
2680
+ lineVisible = [...padded, ...lineVisible];
2681
+ }
2682
+ }
2683
+ drawCandleFrame(ctx, layout, cfg.palette, {
2684
+ candles: drawCandles,
2685
+ displayCandleWidth,
2686
+ oldCandles: drawOldCandles,
2687
+ oldWidth: cwt.oldWidth,
2688
+ morphT,
2689
+ liveCandle: drawLive,
2690
+ closePriceCandle: closeLineSmoothInitedRef.current && rawLive ? { ...rawLive, close: closeLineSmoothRef.current } : rawLive,
2691
+ liveTime: effectiveLive?.time ?? -1,
2692
+ liveBirthAlpha: liveBirthAlphaRef.current,
2693
+ liveBullBlend: liveBullRef.current,
2694
+ lineModeProg,
2695
+ chartReveal,
2696
+ now_ms,
2697
+ now,
2698
+ pauseProgress,
2699
+ showGrid: cfg.showGrid,
2700
+ scrubAmount,
2701
+ hoverX: drawHoverX,
2702
+ hoverValue: drawHoverCandle?.close ?? null,
2703
+ hoverTime: drawHoverTime,
2704
+ hoveredCandle: drawHoverCandle,
2705
+ formatValue: cfg.formatValue,
2706
+ formatTime: cfg.formatTime,
2707
+ gridState: gridStateRef.current,
2708
+ timeAxisState: timeAxisStateRef.current,
2709
+ dt: pausedDt,
2710
+ targetWindowSecs: cfg.windowSecs,
2711
+ tooltipY: cfg.tooltipY,
2712
+ tooltipOutline: cfg.tooltipOutline,
2713
+ lineVisible,
2714
+ lineSmoothValue,
2715
+ emptyText: cfg.emptyText,
2716
+ loadingAlpha,
2717
+ // Show empty overlay when not loading AND loadingAlpha has fully
2718
+ // decayed. This prevents the gradient gap from flashing during
2719
+ // loading→live (where loadingAlpha starts at ~1), while still
2720
+ // allowing smooth fade-out during empty→live (loadingAlpha is 0).
2721
+ showEmptyOverlay: !(cfg.loading ?? false) && loadingAlpha < 0.01
2722
+ });
2723
+ if (badgeRef.current) {
2724
+ if (lineModeProg > 0.5 && cfg.showBadge) {
2725
+ const momentum = detectMomentum(lineVisible);
2726
+ badgeYRef.current = updateBadgeDOM(
2727
+ badgeRef.current,
2728
+ cfg,
2729
+ lineSmoothValue,
2730
+ layout,
2731
+ momentum,
2732
+ badgeYRef.current,
2733
+ badgeColorRef.current,
2734
+ isWindowTransitioning,
2735
+ noMotion,
2736
+ ctx,
2737
+ pausedDt,
2738
+ chartReveal
2739
+ );
2740
+ const badgeFade = (lineModeProg - 0.5) * 2;
2741
+ if (badgeRef.current.container.style.display !== "none") {
2742
+ const base = badgeRef.current.container.style.opacity ? parseFloat(badgeRef.current.container.style.opacity) : 1;
2743
+ badgeRef.current.container.style.opacity = String(
2744
+ base * badgeFade * (1 - pauseProgress)
2745
+ );
2746
+ }
2747
+ } else {
2748
+ badgeRef.current.container.style.display = "none";
2749
+ }
2750
+ }
2751
+ } else {
2752
+ const effectivePoints = useStash ? lastDataRef.current : points;
2753
+ const adaptiveSpeed = computeAdaptiveSpeed(
2754
+ cfg.value,
2755
+ displayValueRef.current,
2756
+ displayMinRef.current,
2757
+ displayMaxRef.current,
2758
+ cfg.lerpSpeed,
2759
+ noMotion
2760
+ );
2761
+ if (!useStash) {
2762
+ displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, pausedDt);
2763
+ if (pauseProgress < 0.5) {
2764
+ const prevRange = displayMaxRef.current - displayMinRef.current || 1;
2765
+ if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
2766
+ displayValueRef.current = cfg.value;
2767
+ }
2768
+ }
2769
+ }
2770
+ const smoothValue = displayValueRef.current;
2771
+ const chartW = w - pad.left - pad.right;
2772
+ const needsArrowRoom = cfg.showMomentum;
2773
+ const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
2774
+ const transition = windowTransitionRef.current;
2775
+ if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
2776
+ const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
2777
+ const windowResult = updateWindowTransition(
1544
2778
  cfg,
2779
+ transition,
2780
+ displayWindowRef.current,
2781
+ displayMinRef.current,
2782
+ displayMaxRef.current,
2783
+ noMotion,
2784
+ now_ms,
2785
+ now,
2786
+ effectivePoints,
1545
2787
  smoothValue,
1546
- layout,
1547
- momentum,
1548
- badgeYRef.current,
1549
- badgeColorRef.current,
2788
+ buffer
2789
+ );
2790
+ displayWindowRef.current = windowResult.windowSecs;
2791
+ const windowSecs = windowResult.windowSecs;
2792
+ const windowTransProgress = windowResult.windowTransProgress;
2793
+ const rightEdge = now + windowSecs * buffer;
2794
+ const leftEdge = rightEdge - windowSecs;
2795
+ const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
2796
+ const visible = [];
2797
+ for (const p of effectivePoints) {
2798
+ if (p.time >= leftEdge - 2 && p.time <= filterRight) {
2799
+ visible.push(p);
2800
+ }
2801
+ }
2802
+ if (visible.length < 2) {
2803
+ if (badgeRef.current) badgeRef.current.container.style.display = "none";
2804
+ rafRef.current = requestAnimationFrame(draw);
2805
+ return;
2806
+ }
2807
+ const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
2808
+ const isWindowTransitioning = transition.startMs > 0;
2809
+ const rangeResult = updateRange(
2810
+ computedRange,
2811
+ rangeInitedRef.current,
2812
+ targetMinRef.current,
2813
+ targetMaxRef.current,
2814
+ displayMinRef.current,
2815
+ displayMaxRef.current,
1550
2816
  isWindowTransitioning,
2817
+ windowTransProgress,
2818
+ transition,
2819
+ adaptiveSpeed,
2820
+ chartH,
2821
+ pausedDt
2822
+ );
2823
+ rangeInitedRef.current = rangeResult.rangeInited;
2824
+ targetMinRef.current = rangeResult.targetMin;
2825
+ targetMaxRef.current = rangeResult.targetMax;
2826
+ displayMinRef.current = rangeResult.displayMin;
2827
+ displayMaxRef.current = rangeResult.displayMax;
2828
+ const { minVal, maxVal, valRange } = rangeResult;
2829
+ const layout = {
2830
+ w,
2831
+ h,
2832
+ pad,
2833
+ chartW,
2834
+ chartH,
2835
+ leftEdge,
2836
+ rightEdge,
2837
+ minVal,
2838
+ maxVal,
2839
+ valRange,
2840
+ toX: (t) => pad.left + (t - leftEdge) / (rightEdge - leftEdge) * chartW,
2841
+ toY: (v) => pad.top + (1 - (v - minVal) / valRange) * chartH
2842
+ };
2843
+ const momentum = cfg.momentumOverride ?? detectMomentum(visible);
2844
+ const hoverResult = updateHoverState(
2845
+ hoverXRef.current,
2846
+ pad,
2847
+ w,
2848
+ layout,
2849
+ now,
2850
+ visible,
2851
+ scrubAmountRef.current,
2852
+ lastHoverRef.current,
2853
+ cfg,
1551
2854
  noMotion,
1552
- ctx,
2855
+ leftEdge,
2856
+ rightEdge,
2857
+ chartW,
1553
2858
  dt
1554
2859
  );
1555
- }
1556
- const valEl = cfg.valueDisplayRef?.current;
1557
- if (valEl) {
1558
- const displayVal = cfg.valueMomentumColor ? Math.abs(smoothValue) : smoothValue;
1559
- valEl.textContent = cfg.formatValue(displayVal);
1560
- if (cfg.valueMomentumColor) {
1561
- const mc = momentum === "up" ? "#22c55e" : momentum === "down" ? "#ef4444" : "";
1562
- if (mc) valEl.style.color = mc;
1563
- else valEl.style.removeProperty("color");
2860
+ scrubAmountRef.current = hoverResult.scrubAmount;
2861
+ lastHoverRef.current = hoverResult.lastHover;
2862
+ const { hoverX: drawHoverX, hoverValue: drawHoverValue, hoverTime: drawHoverTime } = hoverResult;
2863
+ const lookback = Math.min(5, visible.length - 1);
2864
+ const recentDelta = lookback > 0 ? Math.abs(visible[visible.length - 1].value - visible[visible.length - 1 - lookback].value) : 0;
2865
+ const swingMagnitude = valRange > 0 ? Math.min(recentDelta / valRange, 1) : 0;
2866
+ drawFrame(ctx, layout, cfg.palette, {
2867
+ visible,
2868
+ smoothValue,
2869
+ now,
2870
+ momentum,
2871
+ arrowState: arrowStateRef.current,
2872
+ showGrid: cfg.showGrid,
2873
+ showMomentum: cfg.showMomentum,
2874
+ showPulse: cfg.showPulse,
2875
+ showFill: cfg.showFill,
2876
+ referenceLine: cfg.referenceLine,
2877
+ hoverX: drawHoverX,
2878
+ hoverValue: drawHoverValue,
2879
+ hoverTime: drawHoverTime,
2880
+ scrubAmount: scrubAmountRef.current,
2881
+ windowSecs,
2882
+ formatValue: cfg.formatValue,
2883
+ formatTime: cfg.formatTime,
2884
+ gridState: gridStateRef.current,
2885
+ timeAxisState: timeAxisStateRef.current,
2886
+ dt,
2887
+ targetWindowSecs: cfg.windowSecs,
2888
+ tooltipY: cfg.tooltipY,
2889
+ tooltipOutline: cfg.tooltipOutline,
2890
+ orderbookData: cfg.orderbookData,
2891
+ orderbookState: cfg.orderbookData ? orderbookStateRef.current : void 0,
2892
+ particleState: cfg.degenOptions ? particleStateRef.current : void 0,
2893
+ particleOptions: cfg.degenOptions,
2894
+ swingMagnitude,
2895
+ shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
2896
+ chartReveal,
2897
+ pauseProgress,
2898
+ now_ms
2899
+ });
2900
+ const bgAlpha = 1 - chartReveal;
2901
+ if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
2902
+ const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
2903
+ if (bgEmptyAlpha > 0.01) {
2904
+ drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
2905
+ }
2906
+ }
2907
+ const badge = badgeRef.current;
2908
+ if (badge) {
2909
+ badgeYRef.current = updateBadgeDOM(
2910
+ badge,
2911
+ cfg,
2912
+ smoothValue,
2913
+ layout,
2914
+ momentum,
2915
+ badgeYRef.current,
2916
+ badgeColorRef.current,
2917
+ isWindowTransitioning,
2918
+ noMotion,
2919
+ ctx,
2920
+ pausedDt,
2921
+ chartReveal
2922
+ );
2923
+ if (pauseProgress > 0.01 && badge.container.style.display !== "none") {
2924
+ const base = badge.container.style.opacity ? parseFloat(badge.container.style.opacity) : 1;
2925
+ badge.container.style.opacity = String(base * (1 - pauseProgress));
2926
+ }
2927
+ }
2928
+ const valEl = cfg.valueDisplayRef?.current;
2929
+ if (valEl) {
2930
+ const displayVal = cfg.valueMomentumColor ? Math.abs(smoothValue) : smoothValue;
2931
+ valEl.textContent = cfg.formatValue(displayVal);
2932
+ if (cfg.valueMomentumColor) {
2933
+ const mc = momentum === "up" ? "#22c55e" : momentum === "down" ? "#ef4444" : "";
2934
+ if (mc) valEl.style.color = mc;
2935
+ else valEl.style.removeProperty("color");
2936
+ }
1564
2937
  }
1565
2938
  }
1566
2939
  rafRef.current = requestAnimationFrame(draw);
@@ -1592,6 +2965,9 @@ function Liveline({
1592
2965
  momentum = true,
1593
2966
  fill = true,
1594
2967
  scrub = true,
2968
+ loading = false,
2969
+ paused = false,
2970
+ emptyText,
1595
2971
  exaggerate = false,
1596
2972
  degen: degenProp,
1597
2973
  badgeTail = true,
@@ -1612,6 +2988,14 @@ function Liveline({
1612
2988
  onHover,
1613
2989
  cursor = "crosshair",
1614
2990
  pulse = true,
2991
+ mode = "line",
2992
+ candles,
2993
+ candleWidth,
2994
+ liveCandle,
2995
+ lineMode,
2996
+ lineData,
2997
+ lineValue,
2998
+ onModeChange,
1615
2999
  className,
1616
3000
  style
1617
3001
  }) {
@@ -1621,7 +3005,10 @@ function Liveline({
1621
3005
  const windowBarRef = useRef2(null);
1622
3006
  const windowBtnRefs = useRef2(/* @__PURE__ */ new Map());
1623
3007
  const [indicatorStyle, setIndicatorStyle] = useState(null);
1624
- const palette = resolveTheme(color, theme);
3008
+ const modeBarRef = useRef2(null);
3009
+ const modeBtnRefs = useRef2(/* @__PURE__ */ new Map());
3010
+ const [modeIndicatorStyle, setModeIndicatorStyle] = useState(null);
3011
+ const palette = useMemo(() => resolveTheme(color, theme), [color, theme]);
1625
3012
  const isDark = theme === "dark";
1626
3013
  const showMomentum = momentum !== false;
1627
3014
  const momentumOverride = typeof momentum === "string" ? momentum : void 0;
@@ -1650,6 +3037,20 @@ function Liveline({
1650
3037
  });
1651
3038
  }
1652
3039
  }, [activeWindowSecs, windows]);
3040
+ const activeMode = lineMode ? "line" : "candle";
3041
+ useLayoutEffect(() => {
3042
+ if (!onModeChange) return;
3043
+ const btn = modeBtnRefs.current.get(activeMode);
3044
+ const bar = modeBarRef.current;
3045
+ if (btn && bar) {
3046
+ const barRect = bar.getBoundingClientRect();
3047
+ const btnRect = btn.getBoundingClientRect();
3048
+ setModeIndicatorStyle({
3049
+ left: btnRect.left - barRect.left,
3050
+ width: btnRect.width
3051
+ });
3052
+ }
3053
+ }, [activeMode, onModeChange]);
1653
3054
  const ws = windowStyle ?? "default";
1654
3055
  useLivelineEngine(canvasRef, containerRef, {
1655
3056
  data,
@@ -1677,9 +3078,21 @@ function Liveline({
1677
3078
  tooltipOutline,
1678
3079
  valueMomentumColor,
1679
3080
  valueDisplayRef: showValue ? valueDisplayRef : void 0,
1680
- orderbookData: orderbook
3081
+ orderbookData: orderbook,
3082
+ loading,
3083
+ paused,
3084
+ emptyText,
3085
+ mode,
3086
+ candles,
3087
+ candleWidth,
3088
+ liveCandle,
3089
+ lineMode,
3090
+ lineData,
3091
+ lineValue
1681
3092
  });
1682
3093
  const cursorStyle = scrub ? cursor : "default";
3094
+ const activeColor = isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)";
3095
+ const inactiveColor = isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.22)";
1683
3096
  return /* @__PURE__ */ jsxs(Fragment, { children: [
1684
3097
  showValue && /* @__PURE__ */ jsx(
1685
3098
  "span",
@@ -1699,68 +3112,193 @@ function Liveline({
1699
3112
  }
1700
3113
  }
1701
3114
  ),
1702
- windows && windows.length > 0 && /* @__PURE__ */ jsxs(
1703
- "div",
1704
- {
1705
- ref: windowBarRef,
1706
- style: {
1707
- position: "relative",
1708
- display: "inline-flex",
1709
- gap: ws === "text" ? 4 : 2,
1710
- background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
1711
- borderRadius: ws === "rounded" ? 999 : 6,
1712
- padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2,
1713
- marginBottom: 6,
1714
- marginLeft: pad.left
1715
- },
1716
- children: [
1717
- ws !== "text" && indicatorStyle && /* @__PURE__ */ jsx("div", { style: {
1718
- position: "absolute",
1719
- top: ws === "rounded" ? 3 : 2,
1720
- left: indicatorStyle.left,
1721
- width: indicatorStyle.width,
1722
- height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
1723
- background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
1724
- borderRadius: ws === "rounded" ? 999 : 4,
1725
- transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
1726
- pointerEvents: "none"
1727
- } }),
1728
- windows.map((w) => {
1729
- const isActive = w.secs === activeWindowSecs;
1730
- return /* @__PURE__ */ jsx(
3115
+ (windows && windows.length > 0 || onModeChange) && /* @__PURE__ */ jsxs("div", { style: { display: "flex", alignItems: "center", gap: 6, marginBottom: 6, marginLeft: pad.left }, children: [
3116
+ windows && windows.length > 0 && /* @__PURE__ */ jsxs(
3117
+ "div",
3118
+ {
3119
+ ref: windowBarRef,
3120
+ style: {
3121
+ position: "relative",
3122
+ display: "inline-flex",
3123
+ gap: ws === "text" ? 4 : 2,
3124
+ background: ws === "text" ? "transparent" : isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3125
+ borderRadius: ws === "rounded" ? 999 : 6,
3126
+ padding: ws === "text" ? 0 : ws === "rounded" ? 3 : 2
3127
+ },
3128
+ children: [
3129
+ ws !== "text" && indicatorStyle && /* @__PURE__ */ jsx("div", { style: {
3130
+ position: "absolute",
3131
+ top: ws === "rounded" ? 3 : 2,
3132
+ left: indicatorStyle.left,
3133
+ width: indicatorStyle.width,
3134
+ height: ws === "rounded" ? "calc(100% - 6px)" : "calc(100% - 4px)",
3135
+ background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3136
+ borderRadius: ws === "rounded" ? 999 : 4,
3137
+ transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
3138
+ pointerEvents: "none"
3139
+ } }),
3140
+ windows.map((w) => {
3141
+ const isActive = w.secs === activeWindowSecs;
3142
+ return /* @__PURE__ */ jsx(
3143
+ "button",
3144
+ {
3145
+ ref: (el) => {
3146
+ if (el) windowBtnRefs.current.set(w.secs, el);
3147
+ else windowBtnRefs.current.delete(w.secs);
3148
+ },
3149
+ onClick: () => {
3150
+ setActiveWindowSecs(w.secs);
3151
+ onWindowChange?.(w.secs);
3152
+ },
3153
+ style: {
3154
+ position: "relative",
3155
+ zIndex: 1,
3156
+ fontSize: 11,
3157
+ padding: ws === "text" ? "2px 6px" : "3px 10px",
3158
+ borderRadius: ws === "rounded" ? 999 : 4,
3159
+ border: "none",
3160
+ cursor: "pointer",
3161
+ fontFamily: "system-ui, -apple-system, sans-serif",
3162
+ fontWeight: isActive ? 600 : 400,
3163
+ background: "transparent",
3164
+ color: isActive ? activeColor : inactiveColor,
3165
+ transition: "color 0.2s, background 0.15s",
3166
+ lineHeight: "16px"
3167
+ },
3168
+ children: w.label
3169
+ },
3170
+ w.secs
3171
+ );
3172
+ })
3173
+ ]
3174
+ }
3175
+ ),
3176
+ onModeChange && /* @__PURE__ */ jsxs(
3177
+ "div",
3178
+ {
3179
+ ref: modeBarRef,
3180
+ style: {
3181
+ position: "relative",
3182
+ display: "inline-flex",
3183
+ gap: 2,
3184
+ background: isDark ? "rgba(255,255,255,0.03)" : "rgba(0,0,0,0.02)",
3185
+ borderRadius: 6,
3186
+ padding: 2
3187
+ },
3188
+ children: [
3189
+ modeIndicatorStyle && /* @__PURE__ */ jsx("div", { style: {
3190
+ position: "absolute",
3191
+ top: 2,
3192
+ left: modeIndicatorStyle.left,
3193
+ width: modeIndicatorStyle.width,
3194
+ height: "calc(100% - 4px)",
3195
+ background: isDark ? "rgba(255,255,255,0.06)" : "rgba(0,0,0,0.035)",
3196
+ borderRadius: 4,
3197
+ transition: "left 0.25s cubic-bezier(0.4, 0, 0.2, 1), width 0.25s cubic-bezier(0.4, 0, 0.2, 1)",
3198
+ pointerEvents: "none"
3199
+ } }),
3200
+ /* @__PURE__ */ jsx(
1731
3201
  "button",
1732
3202
  {
1733
3203
  ref: (el) => {
1734
- if (el) windowBtnRefs.current.set(w.secs, el);
1735
- else windowBtnRefs.current.delete(w.secs);
3204
+ if (el) modeBtnRefs.current.set("line", el);
3205
+ else modeBtnRefs.current.delete("line");
3206
+ },
3207
+ onClick: () => onModeChange("line"),
3208
+ style: {
3209
+ position: "relative",
3210
+ zIndex: 1,
3211
+ padding: "5px 7px",
3212
+ borderRadius: 4,
3213
+ border: "none",
3214
+ cursor: "pointer",
3215
+ background: "transparent",
3216
+ display: "flex",
3217
+ alignItems: "center"
1736
3218
  },
1737
- onClick: () => {
1738
- setActiveWindowSecs(w.secs);
1739
- onWindowChange?.(w.secs);
3219
+ children: /* @__PURE__ */ jsx("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: /* @__PURE__ */ jsx(
3220
+ "path",
3221
+ {
3222
+ d: "M1 8.5C2.5 8.5 3 4 5.5 4S7.5 7 8.5 7C9.5 7 10 3.5 11 3.5",
3223
+ stroke: activeMode === "line" ? activeColor : inactiveColor,
3224
+ strokeWidth: activeMode === "line" ? 1.5 : 1.2,
3225
+ strokeLinecap: "round",
3226
+ fill: "none"
3227
+ }
3228
+ ) })
3229
+ }
3230
+ ),
3231
+ /* @__PURE__ */ jsx(
3232
+ "button",
3233
+ {
3234
+ ref: (el) => {
3235
+ if (el) modeBtnRefs.current.set("candle", el);
3236
+ else modeBtnRefs.current.delete("candle");
1740
3237
  },
3238
+ onClick: () => onModeChange("candle"),
1741
3239
  style: {
1742
3240
  position: "relative",
1743
3241
  zIndex: 1,
1744
- fontSize: 11,
1745
- padding: ws === "text" ? "2px 6px" : "3px 10px",
1746
- borderRadius: ws === "rounded" ? 999 : 4,
3242
+ padding: "5px 7px",
3243
+ borderRadius: 4,
1747
3244
  border: "none",
1748
3245
  cursor: "pointer",
1749
- fontFamily: "system-ui, -apple-system, sans-serif",
1750
- fontWeight: isActive ? 600 : 400,
1751
3246
  background: "transparent",
1752
- color: isActive ? isDark ? "rgba(255,255,255,0.7)" : "rgba(0,0,0,0.55)" : isDark ? "rgba(255,255,255,0.25)" : "rgba(0,0,0,0.22)",
1753
- transition: "color 0.2s, background 0.15s",
1754
- lineHeight: "16px"
3247
+ display: "flex",
3248
+ alignItems: "center"
1755
3249
  },
1756
- children: w.label
1757
- },
1758
- w.secs
1759
- );
1760
- })
1761
- ]
1762
- }
1763
- ),
3250
+ children: /* @__PURE__ */ jsxs("svg", { width: "12", height: "12", viewBox: "0 0 12 12", fill: "none", children: [
3251
+ /* @__PURE__ */ jsx(
3252
+ "line",
3253
+ {
3254
+ x1: "3.5",
3255
+ y1: "1",
3256
+ x2: "3.5",
3257
+ y2: "11",
3258
+ stroke: activeMode === "candle" ? activeColor : inactiveColor,
3259
+ strokeWidth: "1"
3260
+ }
3261
+ ),
3262
+ /* @__PURE__ */ jsx(
3263
+ "rect",
3264
+ {
3265
+ x: "2",
3266
+ y: "3",
3267
+ width: "3",
3268
+ height: "5",
3269
+ rx: "0.5",
3270
+ fill: activeMode === "candle" ? activeColor : inactiveColor
3271
+ }
3272
+ ),
3273
+ /* @__PURE__ */ jsx(
3274
+ "line",
3275
+ {
3276
+ x1: "8.5",
3277
+ y1: "2",
3278
+ x2: "8.5",
3279
+ y2: "10",
3280
+ stroke: activeMode === "candle" ? activeColor : inactiveColor,
3281
+ strokeWidth: "1"
3282
+ }
3283
+ ),
3284
+ /* @__PURE__ */ jsx(
3285
+ "rect",
3286
+ {
3287
+ x: "7",
3288
+ y: "4",
3289
+ width: "3",
3290
+ height: "4",
3291
+ rx: "0.5",
3292
+ fill: activeMode === "candle" ? activeColor : inactiveColor
3293
+ }
3294
+ )
3295
+ ] })
3296
+ }
3297
+ )
3298
+ ]
3299
+ }
3300
+ )
3301
+ ] }),
1764
3302
  /* @__PURE__ */ jsx(
1765
3303
  "div",
1766
3304
  {
@@ -1783,6 +3321,63 @@ function Liveline({
1783
3321
  )
1784
3322
  ] });
1785
3323
  }
3324
+
3325
+ // src/LivelineTransition.tsx
3326
+ import { useState as useState2, useEffect as useEffect2, useRef as useRef3 } from "react";
3327
+ import { jsx as jsx2 } from "react/jsx-runtime";
3328
+ function LivelineTransition({
3329
+ active,
3330
+ children,
3331
+ duration = 300,
3332
+ className,
3333
+ style
3334
+ }) {
3335
+ const childArray = Array.isArray(children) ? children : [children];
3336
+ const [mounted, setMounted] = useState2(() => /* @__PURE__ */ new Set([active]));
3337
+ const [visible, setVisible] = useState2(active);
3338
+ const prevRef = useRef3(active);
3339
+ useEffect2(() => {
3340
+ if (active === prevRef.current) return () => {
3341
+ };
3342
+ const oldKey = prevRef.current;
3343
+ prevRef.current = active;
3344
+ setMounted((prev) => /* @__PURE__ */ new Set([...prev, active]));
3345
+ let raf1 = requestAnimationFrame(() => {
3346
+ raf1 = requestAnimationFrame(() => setVisible(active));
3347
+ });
3348
+ const timer = setTimeout(() => {
3349
+ setMounted((prev) => {
3350
+ const next = new Set(prev);
3351
+ next.delete(oldKey);
3352
+ return next;
3353
+ });
3354
+ }, duration + 50);
3355
+ return () => {
3356
+ cancelAnimationFrame(raf1);
3357
+ clearTimeout(timer);
3358
+ };
3359
+ }, [active, duration]);
3360
+ return /* @__PURE__ */ jsx2("div", { className, style: { position: "relative", width: "100%", height: "100%", ...style }, children: childArray.map((child) => {
3361
+ const key = String(child.key ?? "");
3362
+ if (!mounted.has(key)) return null;
3363
+ const isActive = key === visible;
3364
+ return /* @__PURE__ */ jsx2(
3365
+ "div",
3366
+ {
3367
+ style: {
3368
+ position: "absolute",
3369
+ inset: 0,
3370
+ opacity: isActive ? 1 : 0,
3371
+ transition: `opacity ${duration}ms ease`,
3372
+ pointerEvents: isActive ? "auto" : "none"
3373
+ },
3374
+ children: child
3375
+ },
3376
+ key
3377
+ );
3378
+ }) });
3379
+ }
1786
3380
  export {
1787
- Liveline
3381
+ Liveline,
3382
+ LivelineTransition
1788
3383
  };