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