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/README.md +81 -2
- package/dist/index.cjs +1893 -297
- package/dist/index.d.cts +45 -3
- package/dist/index.d.ts +45 -3
- package/dist/index.js +1892 -297
- package/package.json +5 -3
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,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 {
|
|
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
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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 =
|
|
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 =
|
|
467
|
+
const outerRgb = parseColorRgb(palette.badgeOuterBg);
|
|
400
468
|
ctx.save();
|
|
401
|
-
ctx.globalAlpha =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 {
|
|
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,
|
|
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 /
|
|
590
|
-
let interval = niceTimeInterval(
|
|
591
|
-
while (interval * targetPxPerSec < 60 && interval <
|
|
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 =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
1395
|
-
if (
|
|
1396
|
-
if (
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
|
|
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
|
|
1416
|
-
const
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
const
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
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
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
const
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
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 (
|
|
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
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
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
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
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
|
-
|
|
2855
|
+
leftEdge,
|
|
2856
|
+
rightEdge,
|
|
2857
|
+
chartW,
|
|
1553
2858
|
dt
|
|
1554
2859
|
);
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
const
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
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
|
|
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
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
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)
|
|
1735
|
-
else
|
|
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
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
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
|
-
|
|
1745
|
-
|
|
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
|
-
|
|
1753
|
-
|
|
1754
|
-
lineHeight: "16px"
|
|
3247
|
+
display: "flex",
|
|
3248
|
+
alignItems: "center"
|
|
1755
3249
|
},
|
|
1756
|
-
children:
|
|
1757
|
-
|
|
1758
|
-
|
|
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
|
};
|