liveline 0.0.2 → 0.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -1
- package/dist/index.cjs +426 -117
- package/dist/index.d.cts +4 -1
- package/dist/index.d.ts +4 -1
- package/dist/index.js +427 -118
- 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);
|
|
@@ -236,74 +246,153 @@ function drawGrid(ctx, layout, palette, formatValue, state, dt) {
|
|
|
236
246
|
}
|
|
237
247
|
|
|
238
248
|
// src/math/spline.ts
|
|
239
|
-
function drawSpline(ctx, pts
|
|
249
|
+
function drawSpline(ctx, pts) {
|
|
240
250
|
if (pts.length < 2) return;
|
|
241
251
|
if (pts.length === 2) {
|
|
242
252
|
ctx.lineTo(pts[1][0], pts[1][1]);
|
|
243
253
|
return;
|
|
244
254
|
}
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
255
|
+
const n = pts.length;
|
|
256
|
+
const delta = new Array(n - 1);
|
|
257
|
+
const h = new Array(n - 1);
|
|
258
|
+
for (let i = 0; i < n - 1; i++) {
|
|
259
|
+
h[i] = pts[i + 1][0] - pts[i][0];
|
|
260
|
+
delta[i] = h[i] === 0 ? 0 : (pts[i + 1][1] - pts[i][1]) / h[i];
|
|
261
|
+
}
|
|
262
|
+
const m = new Array(n);
|
|
263
|
+
m[0] = delta[0];
|
|
264
|
+
m[n - 1] = delta[n - 2];
|
|
265
|
+
for (let i = 1; i < n - 1; i++) {
|
|
266
|
+
if (delta[i - 1] * delta[i] <= 0) {
|
|
267
|
+
m[i] = 0;
|
|
268
|
+
} else {
|
|
269
|
+
m[i] = (delta[i - 1] + delta[i]) / 2;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
for (let i = 0; i < n - 1; i++) {
|
|
273
|
+
if (delta[i] === 0) {
|
|
274
|
+
m[i] = 0;
|
|
275
|
+
m[i + 1] = 0;
|
|
276
|
+
} else {
|
|
277
|
+
const alpha = m[i] / delta[i];
|
|
278
|
+
const beta = m[i + 1] / delta[i];
|
|
279
|
+
const s2 = alpha * alpha + beta * beta;
|
|
280
|
+
if (s2 > 9) {
|
|
281
|
+
const s = 3 / Math.sqrt(s2);
|
|
282
|
+
m[i] = s * alpha * delta[i];
|
|
283
|
+
m[i + 1] = s * beta * delta[i];
|
|
284
|
+
}
|
|
251
285
|
}
|
|
252
|
-
sampled.push(pts[pts.length - 1]);
|
|
253
286
|
}
|
|
254
|
-
for (let i = 0; i <
|
|
255
|
-
const
|
|
256
|
-
const p1 = sampled[i];
|
|
257
|
-
const p2 = sampled[i + 1];
|
|
258
|
-
const p3 = sampled[Math.min(sampled.length - 1, i + 2)];
|
|
287
|
+
for (let i = 0; i < n - 1; i++) {
|
|
288
|
+
const hi = h[i];
|
|
259
289
|
ctx.bezierCurveTo(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
290
|
+
pts[i][0] + hi / 3,
|
|
291
|
+
pts[i][1] + m[i] * hi / 3,
|
|
292
|
+
pts[i + 1][0] - hi / 3,
|
|
293
|
+
pts[i + 1][1] - m[i + 1] * hi / 3,
|
|
294
|
+
pts[i + 1][0],
|
|
295
|
+
pts[i + 1][1]
|
|
266
296
|
);
|
|
267
297
|
}
|
|
268
298
|
}
|
|
269
299
|
|
|
300
|
+
// src/draw/loadingShape.ts
|
|
301
|
+
var LOADING_AMPLITUDE_RATIO = 0.07;
|
|
302
|
+
var LOADING_SCROLL_SPEED = 1e-3;
|
|
303
|
+
function loadingY(t, centerY, amplitude, scroll) {
|
|
304
|
+
return centerY + amplitude * (Math.sin(t * 9.4 + scroll) * 0.55 + Math.sin(t * 15.7 + scroll * 1.3) * 0.3 + Math.sin(t * 4.2 + scroll * 0.7) * 0.15);
|
|
305
|
+
}
|
|
306
|
+
function loadingBreath(now_ms) {
|
|
307
|
+
return 0.22 + 0.08 * Math.sin(now_ms / 1200 * Math.PI);
|
|
308
|
+
}
|
|
309
|
+
|
|
270
310
|
// src/draw/line.ts
|
|
271
|
-
function
|
|
311
|
+
function parseRgba(color) {
|
|
312
|
+
const hex = color.match(/^#([0-9a-f]{3,8})$/i);
|
|
313
|
+
if (hex) {
|
|
314
|
+
let h = hex[1];
|
|
315
|
+
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
316
|
+
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16), 1];
|
|
317
|
+
}
|
|
318
|
+
const rgba2 = color.match(/rgba\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*,\s*([\d.]+)/);
|
|
319
|
+
if (rgba2) return [+rgba2[1], +rgba2[2], +rgba2[3], +rgba2[4]];
|
|
320
|
+
const rgb = color.match(/rgb\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
321
|
+
if (rgb) return [+rgb[1], +rgb[2], +rgb[3], 1];
|
|
322
|
+
return [128, 128, 128, 1];
|
|
323
|
+
}
|
|
324
|
+
function blendColor(c1, c2, t) {
|
|
325
|
+
if (t <= 0) return c1;
|
|
326
|
+
if (t >= 1) return c2;
|
|
327
|
+
const [r1, g1, b1, a1] = parseRgba(c1);
|
|
328
|
+
const [r2, g2, b2, a2] = parseRgba(c2);
|
|
329
|
+
const r = Math.round(r1 + (r2 - r1) * t);
|
|
330
|
+
const g = Math.round(g1 + (g2 - g1) * t);
|
|
331
|
+
const b = Math.round(b1 + (b2 - b1) * t);
|
|
332
|
+
const a = a1 + (a2 - a1) * t;
|
|
333
|
+
if (a >= 0.995) return `rgb(${r},${g},${b})`;
|
|
334
|
+
return `rgba(${r},${g},${b},${a.toFixed(3)})`;
|
|
335
|
+
}
|
|
336
|
+
function renderCurve(ctx, layout, palette, pts, showFill, lineAlpha = 1, fillAlpha = 1, strokeColor) {
|
|
272
337
|
const { h, pad } = layout;
|
|
273
|
-
|
|
338
|
+
const baseAlpha = ctx.globalAlpha;
|
|
339
|
+
if (showFill && fillAlpha > 0.01) {
|
|
340
|
+
ctx.globalAlpha = baseAlpha * fillAlpha;
|
|
274
341
|
const grad = ctx.createLinearGradient(0, pad.top, 0, h - pad.bottom);
|
|
275
342
|
grad.addColorStop(0, palette.fillTop);
|
|
276
343
|
grad.addColorStop(1, palette.fillBottom);
|
|
277
344
|
ctx.beginPath();
|
|
278
345
|
ctx.moveTo(pts[0][0], h - pad.bottom);
|
|
279
346
|
ctx.lineTo(pts[0][0], pts[0][1]);
|
|
280
|
-
drawSpline(ctx, pts
|
|
347
|
+
drawSpline(ctx, pts);
|
|
281
348
|
ctx.lineTo(pts[pts.length - 1][0], h - pad.bottom);
|
|
282
349
|
ctx.closePath();
|
|
283
350
|
ctx.fillStyle = grad;
|
|
284
351
|
ctx.fill();
|
|
285
352
|
}
|
|
353
|
+
ctx.globalAlpha = baseAlpha * lineAlpha;
|
|
286
354
|
ctx.beginPath();
|
|
287
355
|
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
288
|
-
drawSpline(ctx, pts
|
|
289
|
-
ctx.strokeStyle = palette.line;
|
|
356
|
+
drawSpline(ctx, pts);
|
|
357
|
+
ctx.strokeStyle = strokeColor ?? palette.line;
|
|
290
358
|
ctx.lineWidth = palette.lineWidth;
|
|
291
359
|
ctx.lineJoin = "round";
|
|
292
360
|
ctx.lineCap = "round";
|
|
293
361
|
ctx.stroke();
|
|
362
|
+
ctx.globalAlpha = baseAlpha;
|
|
294
363
|
}
|
|
295
|
-
function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0) {
|
|
296
|
-
const {
|
|
364
|
+
function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scrubX, scrubAmount = 0, chartReveal = 1, now_ms = 0) {
|
|
365
|
+
const { h, pad, toX, toY, chartW, chartH } = layout;
|
|
297
366
|
const yMin = pad.top;
|
|
298
367
|
const yMax = h - pad.bottom;
|
|
299
368
|
const clampY = (y) => Math.max(yMin, Math.min(yMax, y));
|
|
300
|
-
const
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
369
|
+
const centerY = pad.top + chartH / 2;
|
|
370
|
+
const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
|
|
371
|
+
const scroll = now_ms * LOADING_SCROLL_SPEED;
|
|
372
|
+
const morphY = chartReveal < 1 ? (rawY, x) => {
|
|
373
|
+
const t = Math.max(0, Math.min(1, (x - pad.left) / chartW));
|
|
374
|
+
const baseY = loadingY(t, centerY, amplitude, scroll);
|
|
375
|
+
return baseY + (rawY - baseY) * chartReveal;
|
|
376
|
+
} : (rawY, _x) => rawY;
|
|
377
|
+
const pts = visible.map((p, i) => {
|
|
378
|
+
const x = toX(p.time);
|
|
379
|
+
const y = i === visible.length - 1 ? morphY(clampY(toY(smoothValue)), x) : morphY(clampY(toY(p.value)), x);
|
|
380
|
+
return [x, y];
|
|
381
|
+
});
|
|
382
|
+
const liveTipX = toX(now);
|
|
383
|
+
const fullRightX = pad.left + chartW;
|
|
384
|
+
const tipX = chartReveal < 1 ? liveTipX + (fullRightX - liveTipX) * (1 - chartReveal) : liveTipX;
|
|
385
|
+
pts.push([tipX, morphY(clampY(toY(smoothValue)), tipX)]);
|
|
304
386
|
if (pts.length < 2) return;
|
|
387
|
+
let lineAlpha = 1;
|
|
388
|
+
let fillAlpha = 1;
|
|
389
|
+
if (chartReveal < 1) {
|
|
390
|
+
const breath = loadingBreath(now_ms);
|
|
391
|
+
lineAlpha = breath + (1 - breath) * chartReveal;
|
|
392
|
+
fillAlpha = chartReveal;
|
|
393
|
+
}
|
|
394
|
+
const strokeColor = chartReveal < 1 ? blendColor(palette.gridLabel, palette.line, Math.min(1, chartReveal * 3)) : void 0;
|
|
305
395
|
const isScrubbing = scrubX !== null;
|
|
306
|
-
const maxSplinePts = Math.max(300, Math.ceil(chartW));
|
|
307
396
|
ctx.save();
|
|
308
397
|
ctx.beginPath();
|
|
309
398
|
ctx.rect(pad.left - 1, pad.top, chartW + 2, chartH);
|
|
@@ -313,24 +402,26 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
|
|
|
313
402
|
ctx.beginPath();
|
|
314
403
|
ctx.rect(0, 0, scrubX, h);
|
|
315
404
|
ctx.clip();
|
|
316
|
-
renderCurve(ctx, layout, palette, pts, showFill,
|
|
405
|
+
renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
|
|
317
406
|
ctx.restore();
|
|
318
407
|
ctx.save();
|
|
319
408
|
ctx.beginPath();
|
|
320
409
|
ctx.rect(scrubX, 0, layout.w - scrubX, h);
|
|
321
410
|
ctx.clip();
|
|
322
411
|
ctx.globalAlpha = 1 - scrubAmount * 0.6;
|
|
323
|
-
renderCurve(ctx, layout, palette, pts, showFill,
|
|
412
|
+
renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
|
|
324
413
|
ctx.restore();
|
|
325
414
|
} else {
|
|
326
|
-
renderCurve(ctx, layout, palette, pts, showFill,
|
|
415
|
+
renderCurve(ctx, layout, palette, pts, showFill, lineAlpha, fillAlpha, strokeColor);
|
|
327
416
|
}
|
|
328
417
|
ctx.restore();
|
|
329
|
-
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;
|
|
330
420
|
ctx.setLineDash([4, 4]);
|
|
331
421
|
ctx.strokeStyle = palette.dashLine;
|
|
332
422
|
ctx.lineWidth = 1;
|
|
333
|
-
|
|
423
|
+
const dashBase = isScrubbing ? 1 - scrubAmount * 0.2 : 1;
|
|
424
|
+
ctx.globalAlpha = chartReveal < 1 ? dashBase * chartReveal : dashBase;
|
|
334
425
|
ctx.beginPath();
|
|
335
426
|
ctx.moveTo(pad.left, currentY);
|
|
336
427
|
ctx.lineTo(layout.w - pad.right, currentY);
|
|
@@ -345,27 +436,17 @@ function drawLine(ctx, layout, palette, visible, smoothValue, now, showFill, scr
|
|
|
345
436
|
// src/draw/dot.ts
|
|
346
437
|
var PULSE_INTERVAL = 1500;
|
|
347
438
|
var PULSE_DURATION = 900;
|
|
348
|
-
function parseColor(color) {
|
|
349
|
-
const hex = color.match(/^#([0-9a-f]{3,8})$/i);
|
|
350
|
-
if (hex) {
|
|
351
|
-
let h = hex[1];
|
|
352
|
-
if (h.length === 3) h = h[0] + h[0] + h[1] + h[1] + h[2] + h[2];
|
|
353
|
-
return [parseInt(h.slice(0, 2), 16), parseInt(h.slice(2, 4), 16), parseInt(h.slice(4, 6), 16)];
|
|
354
|
-
}
|
|
355
|
-
const rgb = color.match(/rgba?\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)/);
|
|
356
|
-
if (rgb) return [+rgb[1], +rgb[2], +rgb[3]];
|
|
357
|
-
return null;
|
|
358
|
-
}
|
|
359
439
|
function lerpColor(a, b, t) {
|
|
360
440
|
const r = Math.round(a[0] + (b[0] - a[0]) * t);
|
|
361
441
|
const g = Math.round(a[1] + (b[1] - a[1]) * t);
|
|
362
442
|
const bl = Math.round(a[2] + (b[2] - a[2]) * t);
|
|
363
443
|
return `rgb(${r},${g},${bl})`;
|
|
364
444
|
}
|
|
365
|
-
function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
|
|
445
|
+
function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0, now_ms = performance.now()) {
|
|
446
|
+
const baseAlpha = ctx.globalAlpha;
|
|
366
447
|
const dim = scrubAmount * 0.7;
|
|
367
448
|
if (pulse && dim < 0.3) {
|
|
368
|
-
const t =
|
|
449
|
+
const t = now_ms % PULSE_INTERVAL / PULSE_DURATION;
|
|
369
450
|
if (t < 1) {
|
|
370
451
|
const radius = 9 + t * 12;
|
|
371
452
|
const pulseAlpha = 0.35 * (1 - t) * (1 - dim * 3);
|
|
@@ -373,13 +454,13 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
|
|
|
373
454
|
ctx.arc(x, y, radius, 0, Math.PI * 2);
|
|
374
455
|
ctx.strokeStyle = palette.line;
|
|
375
456
|
ctx.lineWidth = 1.5;
|
|
376
|
-
ctx.globalAlpha = pulseAlpha;
|
|
457
|
+
ctx.globalAlpha = baseAlpha * pulseAlpha;
|
|
377
458
|
ctx.stroke();
|
|
378
459
|
}
|
|
379
460
|
}
|
|
380
|
-
const outerRgb =
|
|
461
|
+
const outerRgb = parseColorRgb(palette.badgeOuterBg);
|
|
381
462
|
ctx.save();
|
|
382
|
-
ctx.globalAlpha =
|
|
463
|
+
ctx.globalAlpha = baseAlpha;
|
|
383
464
|
ctx.shadowColor = palette.badgeOuterShadow;
|
|
384
465
|
ctx.shadowBlur = 6 * (1 - dim);
|
|
385
466
|
ctx.shadowOffsetY = 1;
|
|
@@ -388,18 +469,19 @@ function drawDot(ctx, x, y, palette, pulse = true, scrubAmount = 0) {
|
|
|
388
469
|
ctx.fillStyle = palette.badgeOuterBg;
|
|
389
470
|
ctx.fill();
|
|
390
471
|
ctx.restore();
|
|
391
|
-
ctx.globalAlpha =
|
|
472
|
+
ctx.globalAlpha = baseAlpha;
|
|
392
473
|
ctx.beginPath();
|
|
393
474
|
ctx.arc(x, y, 3.5, 0, Math.PI * 2);
|
|
394
475
|
if (dim > 0.01) {
|
|
395
|
-
const lineRgb =
|
|
476
|
+
const lineRgb = parseColorRgb(palette.line);
|
|
396
477
|
ctx.fillStyle = lerpColor(lineRgb, outerRgb, dim);
|
|
397
478
|
} else {
|
|
398
479
|
ctx.fillStyle = palette.line;
|
|
399
480
|
}
|
|
400
481
|
ctx.fill();
|
|
401
482
|
}
|
|
402
|
-
function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
|
|
483
|
+
function drawArrows(ctx, x, y, momentum, palette, arrows, dt, now_ms = performance.now()) {
|
|
484
|
+
const baseAlpha = ctx.globalAlpha;
|
|
403
485
|
const upTarget = momentum === "up" ? 1 : 0;
|
|
404
486
|
const downTarget = momentum === "down" ? 1 : 0;
|
|
405
487
|
const canFadeInUp = arrows.down < 0.02;
|
|
@@ -410,7 +492,7 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
|
|
|
410
492
|
if (arrows.down < 0.01) arrows.down = 0;
|
|
411
493
|
if (arrows.up > 0.99) arrows.up = 1;
|
|
412
494
|
if (arrows.down > 0.99) arrows.down = 1;
|
|
413
|
-
const cycle =
|
|
495
|
+
const cycle = now_ms % 1400 / 1400;
|
|
414
496
|
const drawChevrons = (dir, opacity) => {
|
|
415
497
|
if (opacity < 0.01) return;
|
|
416
498
|
const baseX = x + 19;
|
|
@@ -426,7 +508,7 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
|
|
|
426
508
|
const localT = cycle - start;
|
|
427
509
|
const wave = localT >= 0 && localT < dur ? Math.sin(localT / dur * Math.PI) : 0;
|
|
428
510
|
const pulse = 0.3 + 0.7 * wave;
|
|
429
|
-
ctx.globalAlpha = opacity * pulse;
|
|
511
|
+
ctx.globalAlpha = baseAlpha * opacity * pulse;
|
|
430
512
|
const nudge = dir === -1 ? -3 : 3;
|
|
431
513
|
const cy = baseY + dir * (i * 8 - 4) + nudge;
|
|
432
514
|
ctx.beginPath();
|
|
@@ -439,13 +521,13 @@ function drawArrows(ctx, x, y, momentum, palette, arrows, dt) {
|
|
|
439
521
|
};
|
|
440
522
|
drawChevrons(-1, arrows.up);
|
|
441
523
|
drawChevrons(1, arrows.down);
|
|
442
|
-
ctx.globalAlpha =
|
|
524
|
+
ctx.globalAlpha = baseAlpha;
|
|
443
525
|
}
|
|
444
526
|
|
|
445
527
|
// src/draw/crosshair.ts
|
|
446
528
|
function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, formatValue, formatTime, scrubOpacity, tooltipY, liveDotX, tooltipOutline) {
|
|
447
529
|
if (scrubOpacity < 0.01) return;
|
|
448
|
-
const {
|
|
530
|
+
const { h, pad, toY } = layout;
|
|
449
531
|
const y = toY(hoverValue);
|
|
450
532
|
ctx.save();
|
|
451
533
|
ctx.globalAlpha = scrubOpacity * 0.5;
|
|
@@ -477,7 +559,7 @@ function drawCrosshair(ctx, layout, palette, hoverX, hoverValue, hoverTime, form
|
|
|
477
559
|
const totalW = valueW + sepW + timeW;
|
|
478
560
|
let tx = hoverX - totalW / 2;
|
|
479
561
|
const minX = pad.left + 4;
|
|
480
|
-
const dotRightEdge = liveDotX != null ? liveDotX + 7 : w - pad.right;
|
|
562
|
+
const dotRightEdge = liveDotX != null ? liveDotX + 7 : layout.w - pad.right;
|
|
481
563
|
const maxX = dotRightEdge - totalW;
|
|
482
564
|
if (tx < minX) tx = minX;
|
|
483
565
|
if (tx > maxX) tx = maxX;
|
|
@@ -552,7 +634,7 @@ function niceTimeInterval(windowSecs) {
|
|
|
552
634
|
|
|
553
635
|
// src/draw/timeAxis.ts
|
|
554
636
|
var FADE = 0.08;
|
|
555
|
-
function drawTimeAxis(ctx, layout, palette, windowSecs,
|
|
637
|
+
function drawTimeAxis(ctx, layout, palette, windowSecs, targetWindowSecs, formatTime, state, dt) {
|
|
556
638
|
const { h, pad, leftEdge, rightEdge, toX } = layout;
|
|
557
639
|
const chartLeft = pad.left;
|
|
558
640
|
const chartRight = layout.w - pad.right;
|
|
@@ -567,9 +649,9 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
|
|
|
567
649
|
return fromEdge / fadeZone;
|
|
568
650
|
};
|
|
569
651
|
ctx.font = palette.labelFont;
|
|
570
|
-
const targetPxPerSec = chartW /
|
|
571
|
-
let interval = niceTimeInterval(
|
|
572
|
-
while (interval * targetPxPerSec < 60 && interval <
|
|
652
|
+
const targetPxPerSec = chartW / targetWindowSecs;
|
|
653
|
+
let interval = niceTimeInterval(targetWindowSecs);
|
|
654
|
+
while (interval * targetPxPerSec < 60 && interval < targetWindowSecs) {
|
|
573
655
|
interval *= 2;
|
|
574
656
|
}
|
|
575
657
|
const useLocalDays = interval >= 86400;
|
|
@@ -606,6 +688,7 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
|
|
|
606
688
|
label.alpha = next;
|
|
607
689
|
}
|
|
608
690
|
}
|
|
691
|
+
const baseAlpha = ctx.globalAlpha;
|
|
609
692
|
const lineY = h - pad.bottom;
|
|
610
693
|
const tickLen = 5;
|
|
611
694
|
ctx.strokeStyle = palette.gridLine;
|
|
@@ -641,7 +724,7 @@ function drawTimeAxis(ctx, layout, palette, windowSecs, _targetWindowSecs, forma
|
|
|
641
724
|
}
|
|
642
725
|
for (const label of drawn) {
|
|
643
726
|
ctx.save();
|
|
644
|
-
ctx.globalAlpha = label.alpha;
|
|
727
|
+
ctx.globalAlpha = baseAlpha * label.alpha;
|
|
645
728
|
ctx.strokeStyle = palette.gridLine;
|
|
646
729
|
ctx.lineWidth = 1;
|
|
647
730
|
ctx.beginPath();
|
|
@@ -762,11 +845,12 @@ function drawOrderbook(ctx, layout, palette, orderbook, dt, state, swingMagnitud
|
|
|
762
845
|
state.labels[writeIdx++] = l;
|
|
763
846
|
}
|
|
764
847
|
state.labels.length = writeIdx;
|
|
848
|
+
const baseAlpha = ctx.globalAlpha;
|
|
765
849
|
ctx.save();
|
|
766
850
|
ctx.font = '600 13px "SF Mono", Menlo, monospace';
|
|
767
851
|
ctx.textAlign = "left";
|
|
768
852
|
ctx.textBaseline = "middle";
|
|
769
|
-
ctx.globalAlpha =
|
|
853
|
+
ctx.globalAlpha = baseAlpha;
|
|
770
854
|
const outlineColor = `rgb(${bg[0]},${bg[1]},${bg[2]})`;
|
|
771
855
|
for (let i = 0; i < state.labels.length; i++) {
|
|
772
856
|
const l = state.labels[i];
|
|
@@ -883,18 +967,44 @@ function drawFrame(ctx, layout, palette, opts) {
|
|
|
883
967
|
shake.amplitude *= decayRate;
|
|
884
968
|
if (shake.amplitude < SHAKE_MIN_AMPLITUDE) shake.amplitude = 0;
|
|
885
969
|
}
|
|
886
|
-
|
|
970
|
+
const reveal = opts.chartReveal;
|
|
971
|
+
const pause = opts.pauseProgress;
|
|
972
|
+
const revealRamp = (start, end) => {
|
|
973
|
+
const t = Math.max(0, Math.min(1, (reveal - start) / (end - start)));
|
|
974
|
+
return t * t * (3 - 2 * t);
|
|
975
|
+
};
|
|
976
|
+
if (opts.referenceLine && reveal > 0.01) {
|
|
977
|
+
ctx.save();
|
|
978
|
+
if (reveal < 1) ctx.globalAlpha = reveal;
|
|
887
979
|
drawReferenceLine(ctx, layout, palette, opts.referenceLine);
|
|
980
|
+
ctx.restore();
|
|
888
981
|
}
|
|
889
982
|
if (opts.showGrid) {
|
|
890
|
-
|
|
983
|
+
const gridAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
|
|
984
|
+
if (gridAlpha > 0.01) {
|
|
985
|
+
ctx.save();
|
|
986
|
+
if (gridAlpha < 1) ctx.globalAlpha = gridAlpha;
|
|
987
|
+
drawGrid(ctx, layout, palette, opts.formatValue, opts.gridState, opts.dt);
|
|
988
|
+
ctx.restore();
|
|
989
|
+
}
|
|
891
990
|
}
|
|
892
|
-
if (opts.orderbookData && opts.orderbookState) {
|
|
991
|
+
if (opts.orderbookData && opts.orderbookState && reveal > 0.01) {
|
|
992
|
+
ctx.save();
|
|
993
|
+
if (reveal < 1) ctx.globalAlpha = reveal;
|
|
893
994
|
drawOrderbook(ctx, layout, palette, opts.orderbookData, opts.dt, opts.orderbookState, opts.swingMagnitude);
|
|
995
|
+
ctx.restore();
|
|
894
996
|
}
|
|
895
997
|
const scrubX = opts.scrubAmount > 0.05 ? opts.hoverX : null;
|
|
896
|
-
const pts = drawLine(ctx, layout, palette, opts.visible, opts.smoothValue, opts.now, opts.showFill, scrubX, opts.scrubAmount);
|
|
897
|
-
|
|
998
|
+
const pts = drawLine(ctx, layout, palette, opts.visible, opts.smoothValue, opts.now, opts.showFill, scrubX, opts.scrubAmount, reveal, opts.now_ms);
|
|
999
|
+
{
|
|
1000
|
+
const timeAlpha = reveal < 1 ? revealRamp(0.15, 0.7) : 1;
|
|
1001
|
+
if (timeAlpha > 0.01) {
|
|
1002
|
+
ctx.save();
|
|
1003
|
+
if (timeAlpha < 1) ctx.globalAlpha = timeAlpha;
|
|
1004
|
+
drawTimeAxis(ctx, layout, palette, opts.windowSecs, opts.targetWindowSecs, opts.formatTime, opts.timeAxisState, opts.dt);
|
|
1005
|
+
ctx.restore();
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
898
1008
|
if (pts && pts.length > 0) {
|
|
899
1009
|
const lastPt = pts[pts.length - 1];
|
|
900
1010
|
let dotScrub = opts.scrubAmount;
|
|
@@ -903,19 +1013,34 @@ function drawFrame(ctx, layout, palette, opts) {
|
|
|
903
1013
|
const fadeStart = Math.min(80, layout.chartW * 0.3);
|
|
904
1014
|
dotScrub = distToLive < CROSSHAIR_FADE_MIN_PX ? 0 : distToLive >= fadeStart ? opts.scrubAmount : (distToLive - CROSSHAIR_FADE_MIN_PX) / (fadeStart - CROSSHAIR_FADE_MIN_PX) * opts.scrubAmount;
|
|
905
1015
|
}
|
|
906
|
-
|
|
1016
|
+
const dotAlpha = reveal < 0.3 ? 0 : (reveal - 0.3) / 0.7;
|
|
1017
|
+
const showPulse = opts.showPulse && reveal > 0.6 && pause < 0.5;
|
|
1018
|
+
if (dotAlpha > 0.01) {
|
|
1019
|
+
ctx.save();
|
|
1020
|
+
if (dotAlpha < 1) ctx.globalAlpha = dotAlpha;
|
|
1021
|
+
drawDot(ctx, lastPt[0], lastPt[1], palette, showPulse, dotScrub, opts.now_ms);
|
|
1022
|
+
ctx.restore();
|
|
1023
|
+
}
|
|
907
1024
|
if (opts.showMomentum) {
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
1025
|
+
const arrowReveal = reveal < 1 ? revealRamp(0.6, 1) : 1;
|
|
1026
|
+
const arrowAlpha = arrowReveal * (1 - pause);
|
|
1027
|
+
if (arrowAlpha > 0.01) {
|
|
1028
|
+
ctx.save();
|
|
1029
|
+
if (arrowAlpha < 1) ctx.globalAlpha = arrowAlpha;
|
|
1030
|
+
drawArrows(
|
|
1031
|
+
ctx,
|
|
1032
|
+
lastPt[0],
|
|
1033
|
+
lastPt[1],
|
|
1034
|
+
opts.momentum,
|
|
1035
|
+
palette,
|
|
1036
|
+
opts.arrowState,
|
|
1037
|
+
opts.dt,
|
|
1038
|
+
opts.now_ms
|
|
1039
|
+
);
|
|
1040
|
+
ctx.restore();
|
|
1041
|
+
}
|
|
917
1042
|
}
|
|
918
|
-
if (opts.particleState) {
|
|
1043
|
+
if (opts.particleState && reveal > 0.9) {
|
|
919
1044
|
const burstIntensity = spawnOnSwing(
|
|
920
1045
|
opts.particleState,
|
|
921
1046
|
opts.momentum,
|
|
@@ -969,6 +1094,92 @@ function drawFrame(ctx, layout, palette, opts) {
|
|
|
969
1094
|
}
|
|
970
1095
|
}
|
|
971
1096
|
|
|
1097
|
+
// src/draw/loading.ts
|
|
1098
|
+
function drawLoading(ctx, w, h, pad, palette, now_ms, alpha = 1) {
|
|
1099
|
+
const chartW = w - pad.left - pad.right;
|
|
1100
|
+
const chartH = h - pad.top - pad.bottom;
|
|
1101
|
+
const centerY = pad.top + chartH / 2;
|
|
1102
|
+
const leftX = pad.left;
|
|
1103
|
+
const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
|
|
1104
|
+
const scroll = now_ms * LOADING_SCROLL_SPEED;
|
|
1105
|
+
const breath = loadingBreath(now_ms);
|
|
1106
|
+
const numPts = 32;
|
|
1107
|
+
const pts = [];
|
|
1108
|
+
for (let i = 0; i <= numPts; i++) {
|
|
1109
|
+
const t = i / numPts;
|
|
1110
|
+
const x = leftX + t * chartW;
|
|
1111
|
+
const y = loadingY(t, centerY, amplitude, scroll);
|
|
1112
|
+
pts.push([x, y]);
|
|
1113
|
+
}
|
|
1114
|
+
ctx.beginPath();
|
|
1115
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
1116
|
+
drawSpline(ctx, pts);
|
|
1117
|
+
ctx.strokeStyle = palette.line;
|
|
1118
|
+
ctx.lineWidth = palette.lineWidth;
|
|
1119
|
+
ctx.globalAlpha = breath * alpha;
|
|
1120
|
+
ctx.lineCap = "round";
|
|
1121
|
+
ctx.lineJoin = "round";
|
|
1122
|
+
ctx.stroke();
|
|
1123
|
+
ctx.globalAlpha = 1;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
// src/draw/empty.ts
|
|
1127
|
+
function drawEmpty(ctx, w, h, pad, palette, alpha = 1, now_ms = 0, skipLine = false, emptyText) {
|
|
1128
|
+
const chartW = w - pad.left - pad.right;
|
|
1129
|
+
const chartH = h - pad.top - pad.bottom;
|
|
1130
|
+
const centerY = pad.top + chartH / 2;
|
|
1131
|
+
const cx = pad.left + chartW / 2;
|
|
1132
|
+
const text = emptyText ?? "No data to display";
|
|
1133
|
+
ctx.font = "400 12px system-ui, -apple-system, sans-serif";
|
|
1134
|
+
const amplitude = chartH * LOADING_AMPLITUDE_RATIO;
|
|
1135
|
+
const textW = ctx.measureText(text).width;
|
|
1136
|
+
const gapHalf = textW / 2 + 20;
|
|
1137
|
+
const fadeW = 30;
|
|
1138
|
+
if (!skipLine) {
|
|
1139
|
+
const scroll = now_ms * LOADING_SCROLL_SPEED;
|
|
1140
|
+
const breath = loadingBreath(now_ms);
|
|
1141
|
+
const numPts = 32;
|
|
1142
|
+
const pts = [];
|
|
1143
|
+
for (let i = 0; i <= numPts; i++) {
|
|
1144
|
+
const t = i / numPts;
|
|
1145
|
+
const x = pad.left + t * chartW;
|
|
1146
|
+
const y = loadingY(t, centerY, amplitude, scroll);
|
|
1147
|
+
pts.push([x, y]);
|
|
1148
|
+
}
|
|
1149
|
+
ctx.beginPath();
|
|
1150
|
+
ctx.moveTo(pts[0][0], pts[0][1]);
|
|
1151
|
+
drawSpline(ctx, pts);
|
|
1152
|
+
ctx.strokeStyle = palette.gridLabel;
|
|
1153
|
+
ctx.lineWidth = palette.lineWidth;
|
|
1154
|
+
ctx.globalAlpha = breath * alpha;
|
|
1155
|
+
ctx.lineCap = "round";
|
|
1156
|
+
ctx.lineJoin = "round";
|
|
1157
|
+
ctx.stroke();
|
|
1158
|
+
}
|
|
1159
|
+
ctx.save();
|
|
1160
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
1161
|
+
const gapLeft = cx - gapHalf - fadeW;
|
|
1162
|
+
const gapRight = cx + gapHalf + fadeW;
|
|
1163
|
+
const eraseGrad = ctx.createLinearGradient(gapLeft, 0, gapRight, 0);
|
|
1164
|
+
eraseGrad.addColorStop(0, "rgba(0,0,0,0)");
|
|
1165
|
+
eraseGrad.addColorStop(fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
|
|
1166
|
+
eraseGrad.addColorStop(1 - fadeW / (gapRight - gapLeft), "rgba(0,0,0,1)");
|
|
1167
|
+
eraseGrad.addColorStop(1, "rgba(0,0,0,0)");
|
|
1168
|
+
ctx.fillStyle = eraseGrad;
|
|
1169
|
+
ctx.globalAlpha = alpha;
|
|
1170
|
+
const eraseH = amplitude * 2 + palette.lineWidth + 6;
|
|
1171
|
+
ctx.fillRect(gapLeft, centerY - eraseH / 2, gapRight - gapLeft, eraseH);
|
|
1172
|
+
ctx.restore();
|
|
1173
|
+
ctx.textAlign = "center";
|
|
1174
|
+
ctx.textBaseline = "middle";
|
|
1175
|
+
ctx.globalAlpha = 0.35 * alpha;
|
|
1176
|
+
ctx.fillStyle = palette.gridLabel;
|
|
1177
|
+
ctx.fillText(text, cx, centerY);
|
|
1178
|
+
ctx.globalAlpha = 1;
|
|
1179
|
+
ctx.textAlign = "start";
|
|
1180
|
+
ctx.textBaseline = "alphabetic";
|
|
1181
|
+
}
|
|
1182
|
+
|
|
972
1183
|
// src/draw/badge.ts
|
|
973
1184
|
function badgeSvgPath(pillW, pillH, tailLen, tailSpread) {
|
|
974
1185
|
const r = pillH / 2;
|
|
@@ -1015,6 +1226,11 @@ var VALUE_SNAP_THRESHOLD = 1e-3;
|
|
|
1015
1226
|
var ADAPTIVE_SPEED_BOOST = 0.2;
|
|
1016
1227
|
var MOMENTUM_GREEN = [34, 197, 94];
|
|
1017
1228
|
var MOMENTUM_RED = [239, 68, 68];
|
|
1229
|
+
var CHART_REVEAL_SPEED = 0.14;
|
|
1230
|
+
var PAUSE_PROGRESS_SPEED = 0.12;
|
|
1231
|
+
var PAUSE_CATCHUP_SPEED = 0.08;
|
|
1232
|
+
var PAUSE_CATCHUP_SPEED_FAST = 0.22;
|
|
1233
|
+
var LOADING_ALPHA_SPEED = 0.14;
|
|
1018
1234
|
function computeAdaptiveSpeed(value, displayValue, displayMin, displayMax, lerpSpeed, noMotion) {
|
|
1019
1235
|
const valGap = Math.abs(value - displayValue);
|
|
1020
1236
|
const prevRange = displayMax - displayMin || 1;
|
|
@@ -1146,12 +1362,14 @@ function updateHoverState(hoverPixelX, pad, w, layout, now, visible, scrubAmount
|
|
|
1146
1362
|
lastHover
|
|
1147
1363
|
};
|
|
1148
1364
|
}
|
|
1149
|
-
function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badgeColor, isWindowTransitioning, noMotion, ctx, dt) {
|
|
1150
|
-
if (!cfg.showBadge) {
|
|
1365
|
+
function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badgeColor, isWindowTransitioning, noMotion, ctx, dt, chartReveal = 1) {
|
|
1366
|
+
if (!cfg.showBadge || chartReveal < 0.25) {
|
|
1151
1367
|
badge.container.style.display = "none";
|
|
1152
1368
|
return badgeY;
|
|
1153
1369
|
}
|
|
1154
1370
|
badge.container.style.display = "";
|
|
1371
|
+
const badgeOpacity = chartReveal < 0.5 ? (chartReveal - 0.25) / 0.25 : 1;
|
|
1372
|
+
badge.container.style.opacity = badgeOpacity < 1 ? String(badgeOpacity) : "";
|
|
1155
1373
|
const { w, h, pad } = layout;
|
|
1156
1374
|
const text = cfg.formatValue(smoothValue);
|
|
1157
1375
|
badge.text.textContent = text;
|
|
@@ -1174,7 +1392,9 @@ function updateBadgeDOM(badge, cfg, smoothValue, layout, momentum, badgeY, badge
|
|
|
1174
1392
|
badge.svg.setAttribute("height", String(pillH));
|
|
1175
1393
|
badge.svg.setAttribute("viewBox", `0 0 ${totalW} ${pillH}`);
|
|
1176
1394
|
badge.path.setAttribute("d", cfg.badgeTail ? badgeSvgPath(pillW, pillH, BADGE_TAIL_LEN, BADGE_TAIL_SPREAD) : badgePillOnly(pillW, pillH));
|
|
1177
|
-
const
|
|
1395
|
+
const centerY = pad.top + layout.chartH / 2;
|
|
1396
|
+
const realTargetY = Math.max(pad.top, Math.min(h - pad.bottom, layout.toY(smoothValue)));
|
|
1397
|
+
const targetBadgeY = chartReveal < 1 ? centerY + (realTargetY - centerY) * chartReveal : realTargetY;
|
|
1178
1398
|
if (badgeY === null || noMotion) {
|
|
1179
1399
|
badgeY = targetBadgeY;
|
|
1180
1400
|
} else {
|
|
@@ -1239,12 +1459,20 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1239
1459
|
const badgeYRef = useRef(null);
|
|
1240
1460
|
const reducedMotionRef = useRef(false);
|
|
1241
1461
|
const sizeRef = useRef({ w: 0, h: 0 });
|
|
1462
|
+
const ctxRef = useRef(null);
|
|
1242
1463
|
const rafRef = useRef(0);
|
|
1243
1464
|
const lastFrameRef = useRef(0);
|
|
1244
1465
|
const badgeRef = useRef(null);
|
|
1245
1466
|
const hoverXRef = useRef(null);
|
|
1246
1467
|
const scrubAmountRef = useRef(0);
|
|
1247
1468
|
const lastHoverRef = useRef(null);
|
|
1469
|
+
const chartRevealRef = useRef(0);
|
|
1470
|
+
const pauseProgressRef = useRef(0);
|
|
1471
|
+
const timeDebtRef = useRef(0);
|
|
1472
|
+
const lastDataRef = useRef([]);
|
|
1473
|
+
const frozenNowRef = useRef(0);
|
|
1474
|
+
const pausedDataRef = useRef(null);
|
|
1475
|
+
const loadingAlphaRef = useRef(config.loading ? 1 : 0);
|
|
1248
1476
|
useEffect(() => {
|
|
1249
1477
|
const container = containerRef.current;
|
|
1250
1478
|
if (!container) return;
|
|
@@ -1365,19 +1593,75 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1365
1593
|
canvas.style.width = `${w}px`;
|
|
1366
1594
|
canvas.style.height = `${h}px`;
|
|
1367
1595
|
}
|
|
1368
|
-
|
|
1596
|
+
let ctx = ctxRef.current;
|
|
1597
|
+
if (!ctx || ctx.canvas !== canvas) {
|
|
1598
|
+
ctx = canvas.getContext("2d");
|
|
1599
|
+
ctxRef.current = ctx;
|
|
1600
|
+
}
|
|
1369
1601
|
if (!ctx) {
|
|
1370
1602
|
rafRef.current = requestAnimationFrame(draw);
|
|
1371
1603
|
return;
|
|
1372
1604
|
}
|
|
1373
1605
|
applyDpr(ctx, dpr, w, h);
|
|
1374
1606
|
const noMotion = reducedMotionRef.current;
|
|
1375
|
-
|
|
1376
|
-
|
|
1607
|
+
if (cfg.paused && pausedDataRef.current === null && cfg.data.length >= 2) {
|
|
1608
|
+
pausedDataRef.current = cfg.data.slice();
|
|
1609
|
+
}
|
|
1610
|
+
if (!cfg.paused) {
|
|
1611
|
+
pausedDataRef.current = null;
|
|
1612
|
+
}
|
|
1613
|
+
const points = pausedDataRef.current ?? cfg.data;
|
|
1614
|
+
const hasData = points.length >= 2;
|
|
1615
|
+
const pad = cfg.padding;
|
|
1616
|
+
const chartH = h - pad.top - pad.bottom;
|
|
1617
|
+
const pauseTarget = cfg.paused ? 1 : 0;
|
|
1618
|
+
pauseProgressRef.current = noMotion ? pauseTarget : lerp(pauseProgressRef.current, pauseTarget, PAUSE_PROGRESS_SPEED, dt);
|
|
1619
|
+
if (pauseProgressRef.current < 5e-3) pauseProgressRef.current = 0;
|
|
1620
|
+
if (pauseProgressRef.current > 0.995) pauseProgressRef.current = 1;
|
|
1621
|
+
const pauseProgress = pauseProgressRef.current;
|
|
1622
|
+
const pausedDt = dt * (1 - pauseProgress);
|
|
1623
|
+
const realDtSec = dt / 1e3;
|
|
1624
|
+
timeDebtRef.current += realDtSec * pauseProgress;
|
|
1625
|
+
if (!cfg.paused && timeDebtRef.current > 1e-3) {
|
|
1626
|
+
const catchUpSpeed = timeDebtRef.current > 10 ? PAUSE_CATCHUP_SPEED_FAST : PAUSE_CATCHUP_SPEED;
|
|
1627
|
+
timeDebtRef.current = lerp(timeDebtRef.current, 0, catchUpSpeed, dt);
|
|
1628
|
+
if (timeDebtRef.current < 0.01) timeDebtRef.current = 0;
|
|
1629
|
+
}
|
|
1630
|
+
const loadingTarget = cfg.loading ? 1 : 0;
|
|
1631
|
+
loadingAlphaRef.current = noMotion ? loadingTarget : lerp(loadingAlphaRef.current, loadingTarget, LOADING_ALPHA_SPEED, dt);
|
|
1632
|
+
if (loadingAlphaRef.current < 0.01) loadingAlphaRef.current = 0;
|
|
1633
|
+
if (loadingAlphaRef.current > 0.99) loadingAlphaRef.current = 1;
|
|
1634
|
+
const loadingAlpha = loadingAlphaRef.current;
|
|
1635
|
+
const revealTarget = !cfg.loading && hasData ? 1 : 0;
|
|
1636
|
+
chartRevealRef.current = noMotion ? revealTarget : lerp(chartRevealRef.current, revealTarget, CHART_REVEAL_SPEED, dt);
|
|
1637
|
+
if (Math.abs(chartRevealRef.current - revealTarget) < 5e-3) {
|
|
1638
|
+
chartRevealRef.current = revealTarget;
|
|
1639
|
+
}
|
|
1640
|
+
const chartReveal = chartRevealRef.current;
|
|
1641
|
+
const useStash = !hasData && chartReveal > 5e-3 && lastDataRef.current.length >= 2;
|
|
1642
|
+
if (hasData) {
|
|
1643
|
+
lastDataRef.current = points;
|
|
1644
|
+
}
|
|
1645
|
+
if (!hasData && !useStash) {
|
|
1646
|
+
if (loadingAlpha > 0.01) {
|
|
1647
|
+
drawLoading(ctx, w, h, pad, cfg.palette, now_ms, loadingAlpha);
|
|
1648
|
+
}
|
|
1649
|
+
if (1 - loadingAlpha > 0.01) {
|
|
1650
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, 1 - loadingAlpha, now_ms, false, cfg.emptyText);
|
|
1651
|
+
}
|
|
1652
|
+
ctx.save();
|
|
1653
|
+
ctx.globalCompositeOperation = "destination-out";
|
|
1654
|
+
const fadeGrad = ctx.createLinearGradient(pad.left, 0, pad.left + FADE_EDGE_WIDTH, 0);
|
|
1655
|
+
fadeGrad.addColorStop(0, "rgba(0, 0, 0, 1)");
|
|
1656
|
+
fadeGrad.addColorStop(1, "rgba(0, 0, 0, 0)");
|
|
1657
|
+
ctx.fillStyle = fadeGrad;
|
|
1658
|
+
ctx.fillRect(0, 0, pad.left + FADE_EDGE_WIDTH, h);
|
|
1659
|
+
ctx.restore();
|
|
1377
1660
|
if (badgeRef.current) badgeRef.current.container.style.display = "none";
|
|
1378
1661
|
rafRef.current = requestAnimationFrame(draw);
|
|
1379
1662
|
return;
|
|
1380
1663
|
}
|
|
1664
|
+
const effectivePoints = useStash ? lastDataRef.current : points;
|
|
1381
1665
|
const adaptiveSpeed = computeAdaptiveSpeed(
|
|
1382
1666
|
cfg.value,
|
|
1383
1667
|
displayValueRef.current,
|
|
@@ -1386,18 +1670,22 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1386
1670
|
cfg.lerpSpeed,
|
|
1387
1671
|
noMotion
|
|
1388
1672
|
);
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1673
|
+
if (!useStash) {
|
|
1674
|
+
displayValueRef.current = lerp(displayValueRef.current, cfg.value, adaptiveSpeed, pausedDt);
|
|
1675
|
+
if (pauseProgress < 0.5) {
|
|
1676
|
+
const prevRange = displayMaxRef.current - displayMinRef.current || 1;
|
|
1677
|
+
if (Math.abs(displayValueRef.current - cfg.value) < prevRange * VALUE_SNAP_THRESHOLD) {
|
|
1678
|
+
displayValueRef.current = cfg.value;
|
|
1679
|
+
}
|
|
1680
|
+
}
|
|
1393
1681
|
}
|
|
1394
1682
|
const smoothValue = displayValueRef.current;
|
|
1395
|
-
const pad = cfg.padding;
|
|
1396
1683
|
const chartW = w - pad.left - pad.right;
|
|
1397
1684
|
const needsArrowRoom = cfg.showMomentum;
|
|
1398
1685
|
const buffer = needsArrowRoom ? Math.max(WINDOW_BUFFER, 37 / Math.max(chartW, 1)) : WINDOW_BUFFER;
|
|
1399
1686
|
const transition = windowTransitionRef.current;
|
|
1400
|
-
|
|
1687
|
+
if (hasData) frozenNowRef.current = Date.now() / 1e3 - timeDebtRef.current;
|
|
1688
|
+
const now = useStash ? frozenNowRef.current : Date.now() / 1e3 - timeDebtRef.current;
|
|
1401
1689
|
const windowResult = updateWindowTransition(
|
|
1402
1690
|
cfg,
|
|
1403
1691
|
transition,
|
|
@@ -1407,7 +1695,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1407
1695
|
noMotion,
|
|
1408
1696
|
now_ms,
|
|
1409
1697
|
now,
|
|
1410
|
-
|
|
1698
|
+
effectivePoints,
|
|
1411
1699
|
smoothValue,
|
|
1412
1700
|
buffer
|
|
1413
1701
|
);
|
|
@@ -1416,9 +1704,10 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1416
1704
|
const windowTransProgress = windowResult.windowTransProgress;
|
|
1417
1705
|
const rightEdge = now + windowSecs * buffer;
|
|
1418
1706
|
const leftEdge = rightEdge - windowSecs;
|
|
1707
|
+
const filterRight = rightEdge - (rightEdge - now) * pauseProgress;
|
|
1419
1708
|
const visible = [];
|
|
1420
|
-
for (const p of
|
|
1421
|
-
if (p.time >= leftEdge - 2 && p.time <=
|
|
1709
|
+
for (const p of effectivePoints) {
|
|
1710
|
+
if (p.time >= leftEdge - 2 && p.time <= filterRight) {
|
|
1422
1711
|
visible.push(p);
|
|
1423
1712
|
}
|
|
1424
1713
|
}
|
|
@@ -1427,7 +1716,6 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1427
1716
|
rafRef.current = requestAnimationFrame(draw);
|
|
1428
1717
|
return;
|
|
1429
1718
|
}
|
|
1430
|
-
const chartH = h - pad.top - pad.bottom;
|
|
1431
1719
|
const computedRange = computeRange(visible, smoothValue, cfg.referenceLine?.value, cfg.exaggerate);
|
|
1432
1720
|
const isWindowTransitioning = transition.startMs > 0;
|
|
1433
1721
|
const rangeResult = updateRange(
|
|
@@ -1442,7 +1730,7 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1442
1730
|
transition,
|
|
1443
1731
|
adaptiveSpeed,
|
|
1444
1732
|
chartH,
|
|
1445
|
-
|
|
1733
|
+
pausedDt
|
|
1446
1734
|
);
|
|
1447
1735
|
rangeInitedRef.current = rangeResult.rangeInited;
|
|
1448
1736
|
targetMinRef.current = rangeResult.targetMin;
|
|
@@ -1516,8 +1804,18 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1516
1804
|
particleState: cfg.degenOptions ? particleStateRef.current : void 0,
|
|
1517
1805
|
particleOptions: cfg.degenOptions,
|
|
1518
1806
|
swingMagnitude,
|
|
1519
|
-
shakeState: cfg.degenOptions ? shakeStateRef.current : void 0
|
|
1807
|
+
shakeState: cfg.degenOptions ? shakeStateRef.current : void 0,
|
|
1808
|
+
chartReveal,
|
|
1809
|
+
pauseProgress,
|
|
1810
|
+
now_ms
|
|
1520
1811
|
});
|
|
1812
|
+
const bgAlpha = 1 - chartReveal;
|
|
1813
|
+
if (bgAlpha > 0.01 && revealTarget === 0 && !cfg.loading) {
|
|
1814
|
+
const bgEmptyAlpha = (1 - loadingAlpha) * bgAlpha;
|
|
1815
|
+
if (bgEmptyAlpha > 0.01) {
|
|
1816
|
+
drawEmpty(ctx, w, h, pad, cfg.palette, bgEmptyAlpha, now_ms, true, cfg.emptyText);
|
|
1817
|
+
}
|
|
1818
|
+
}
|
|
1521
1819
|
const badge = badgeRef.current;
|
|
1522
1820
|
if (badge) {
|
|
1523
1821
|
badgeYRef.current = updateBadgeDOM(
|
|
@@ -1531,8 +1829,13 @@ function useLivelineEngine(canvasRef, containerRef, config) {
|
|
|
1531
1829
|
isWindowTransitioning,
|
|
1532
1830
|
noMotion,
|
|
1533
1831
|
ctx,
|
|
1534
|
-
|
|
1832
|
+
pausedDt,
|
|
1833
|
+
chartReveal
|
|
1535
1834
|
);
|
|
1835
|
+
if (pauseProgress > 0.01 && badge.container.style.display !== "none") {
|
|
1836
|
+
const base = badge.container.style.opacity ? parseFloat(badge.container.style.opacity) : 1;
|
|
1837
|
+
badge.container.style.opacity = String(base * (1 - pauseProgress));
|
|
1838
|
+
}
|
|
1536
1839
|
}
|
|
1537
1840
|
const valEl = cfg.valueDisplayRef?.current;
|
|
1538
1841
|
if (valEl) {
|
|
@@ -1573,6 +1876,9 @@ function Liveline({
|
|
|
1573
1876
|
momentum = true,
|
|
1574
1877
|
fill = true,
|
|
1575
1878
|
scrub = true,
|
|
1879
|
+
loading = false,
|
|
1880
|
+
paused = false,
|
|
1881
|
+
emptyText,
|
|
1576
1882
|
exaggerate = false,
|
|
1577
1883
|
degen: degenProp,
|
|
1578
1884
|
badgeTail = true,
|
|
@@ -1602,7 +1908,7 @@ function Liveline({
|
|
|
1602
1908
|
const windowBarRef = useRef2(null);
|
|
1603
1909
|
const windowBtnRefs = useRef2(/* @__PURE__ */ new Map());
|
|
1604
1910
|
const [indicatorStyle, setIndicatorStyle] = useState(null);
|
|
1605
|
-
const palette = resolveTheme(color, theme);
|
|
1911
|
+
const palette = useMemo(() => resolveTheme(color, theme), [color, theme]);
|
|
1606
1912
|
const isDark = theme === "dark";
|
|
1607
1913
|
const showMomentum = momentum !== false;
|
|
1608
1914
|
const momentumOverride = typeof momentum === "string" ? momentum : void 0;
|
|
@@ -1658,7 +1964,10 @@ function Liveline({
|
|
|
1658
1964
|
tooltipOutline,
|
|
1659
1965
|
valueMomentumColor,
|
|
1660
1966
|
valueDisplayRef: showValue ? valueDisplayRef : void 0,
|
|
1661
|
-
orderbookData: orderbook
|
|
1967
|
+
orderbookData: orderbook,
|
|
1968
|
+
loading,
|
|
1969
|
+
paused,
|
|
1970
|
+
emptyText
|
|
1662
1971
|
});
|
|
1663
1972
|
const cursorStyle = scrub ? cursor : "default";
|
|
1664
1973
|
return /* @__PURE__ */ jsxs(Fragment, { children: [
|