hyperprop-charting-library 0.1.142 → 0.1.143
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/hyperprop-charting-library.cjs +198 -34
- package/dist/hyperprop-charting-library.d.ts +65 -1
- package/dist/hyperprop-charting-library.js +198 -34
- package/dist/index.cjs +198 -34
- package/dist/index.d.cts +65 -1
- package/dist/index.d.ts +65 -1
- package/dist/index.js +198 -34
- package/docs/API.md +58 -0
- package/package.json +1 -1
|
@@ -335,26 +335,45 @@ var getSeriesFingerprint = (data) => {
|
|
|
335
335
|
var withCachedSeries = (key, data, compute) => {
|
|
336
336
|
const fingerprint = getSeriesFingerprint(data);
|
|
337
337
|
const existing = builtInSeriesCache.get(key);
|
|
338
|
-
|
|
339
|
-
|
|
338
|
+
const shape = getSeriesShape(data);
|
|
339
|
+
if (existing) {
|
|
340
|
+
const unchanged = existing.fingerprint === fingerprint;
|
|
341
|
+
const liveTickWithinThrottle = !unchanged && indicatorLiveThrottleMs > 0 && existing.shape === shape && Date.now() - existing.computedAt < indicatorLiveThrottleMs;
|
|
342
|
+
if (unchanged || liveTickWithinThrottle) {
|
|
343
|
+
return existing.values;
|
|
344
|
+
}
|
|
340
345
|
}
|
|
341
346
|
const values = compute();
|
|
342
|
-
builtInSeriesCache.set(key, { fingerprint, values });
|
|
347
|
+
builtInSeriesCache.set(key, { fingerprint, values, shape, computedAt: Date.now() });
|
|
343
348
|
return values;
|
|
344
349
|
};
|
|
345
350
|
var builtInComputationCache = /* @__PURE__ */ new Map();
|
|
351
|
+
var indicatorLiveThrottleMs = 80;
|
|
352
|
+
var setIndicatorLiveThrottleMs = (ms) => {
|
|
353
|
+
indicatorLiveThrottleMs = Math.max(0, ms);
|
|
354
|
+
};
|
|
355
|
+
var getSeriesShape = (data) => {
|
|
356
|
+
const length = data.length;
|
|
357
|
+
const last = length > 0 ? data[length - 1] : void 0;
|
|
358
|
+
return last ? `${length}|${last.time.getTime()}` : "empty";
|
|
359
|
+
};
|
|
346
360
|
var COMPUTATION_CACHE_MAX_ENTRIES = 128;
|
|
347
361
|
var withCachedComputation = (key, data, compute) => {
|
|
348
362
|
const fingerprint = getSeriesFingerprint(data);
|
|
349
363
|
const existing = builtInComputationCache.get(key);
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
364
|
+
const shape = getSeriesShape(data);
|
|
365
|
+
if (existing) {
|
|
366
|
+
const unchanged = existing.fingerprint === fingerprint;
|
|
367
|
+
const liveTickWithinThrottle = !unchanged && indicatorLiveThrottleMs > 0 && existing.shape === shape && Date.now() - existing.computedAt < indicatorLiveThrottleMs;
|
|
368
|
+
if (unchanged || liveTickWithinThrottle) {
|
|
369
|
+
builtInComputationCache.delete(key);
|
|
370
|
+
builtInComputationCache.set(key, existing);
|
|
371
|
+
return existing.value;
|
|
372
|
+
}
|
|
354
373
|
}
|
|
355
374
|
const value = compute();
|
|
356
375
|
builtInComputationCache.delete(key);
|
|
357
|
-
builtInComputationCache.set(key, { fingerprint, value });
|
|
376
|
+
builtInComputationCache.set(key, { fingerprint, value, shape, computedAt: Date.now() });
|
|
358
377
|
while (builtInComputationCache.size > COMPUTATION_CACHE_MAX_ENTRIES) {
|
|
359
378
|
const oldest = builtInComputationCache.keys().next().value;
|
|
360
379
|
if (oldest === void 0) break;
|
|
@@ -2924,7 +2943,23 @@ var DEFAULT_OPTIONS = {
|
|
|
2924
2943
|
accessibility: { label: "Price chart", description: "", focusable: true },
|
|
2925
2944
|
downsampling: { enabled: true, thresholdPx: 1.5 },
|
|
2926
2945
|
touch: { hitToleranceScale: 2.2, longPressTooltip: true, longPressMs: 400 },
|
|
2946
|
+
dataLine: {
|
|
2947
|
+
visible: false,
|
|
2948
|
+
symbol: "",
|
|
2949
|
+
interval: "",
|
|
2950
|
+
exchange: "",
|
|
2951
|
+
showOhlc: true,
|
|
2952
|
+
showChange: true,
|
|
2953
|
+
showVolume: false,
|
|
2954
|
+
details: [],
|
|
2955
|
+
statusColor: "",
|
|
2956
|
+
fontSize: 0,
|
|
2957
|
+
symbolColor: "",
|
|
2958
|
+
textColor: ""
|
|
2959
|
+
},
|
|
2927
2960
|
datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
|
|
2961
|
+
animation: { viewportTransitions: true, durationMs: 260 },
|
|
2962
|
+
indicatorUpdate: { liveThrottleMs: 80 },
|
|
2928
2963
|
crosshair: DEFAULT_CROSSHAIR_OPTIONS,
|
|
2929
2964
|
grid: DEFAULT_GRID_OPTIONS,
|
|
2930
2965
|
watermark: DEFAULT_WATERMARK_OPTIONS,
|
|
@@ -2983,6 +3018,18 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
|
|
|
2983
3018
|
...baseOptions.touch,
|
|
2984
3019
|
...options.touch ?? {}
|
|
2985
3020
|
},
|
|
3021
|
+
dataLine: {
|
|
3022
|
+
...baseOptions.dataLine,
|
|
3023
|
+
...options.dataLine ?? {}
|
|
3024
|
+
},
|
|
3025
|
+
animation: {
|
|
3026
|
+
...baseOptions.animation,
|
|
3027
|
+
...options.animation ?? {}
|
|
3028
|
+
},
|
|
3029
|
+
indicatorUpdate: {
|
|
3030
|
+
...baseOptions.indicatorUpdate,
|
|
3031
|
+
...options.indicatorUpdate ?? {}
|
|
3032
|
+
},
|
|
2986
3033
|
datafeedOptions: {
|
|
2987
3034
|
...baseOptions.datafeedOptions,
|
|
2988
3035
|
...options.datafeedOptions ?? {}
|
|
@@ -3242,6 +3289,7 @@ function createChart(element, options = {}) {
|
|
|
3242
3289
|
let crosshairPoint = null;
|
|
3243
3290
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
3244
3291
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
3292
|
+
setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
|
|
3245
3293
|
let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
|
|
3246
3294
|
let rightAxisLabelSlots = [];
|
|
3247
3295
|
let plotLabelSlots = [];
|
|
@@ -3483,21 +3531,69 @@ function createChart(element, options = {}) {
|
|
|
3483
3531
|
Math.min(highCenter, count + maxPanBars)
|
|
3484
3532
|
);
|
|
3485
3533
|
};
|
|
3486
|
-
|
|
3487
|
-
|
|
3488
|
-
if (
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3534
|
+
let viewportTweenRaf = null;
|
|
3535
|
+
const cancelViewportTween = () => {
|
|
3536
|
+
if (viewportTweenRaf !== null) {
|
|
3537
|
+
cancelAnimationFrame(viewportTweenRaf);
|
|
3538
|
+
viewportTweenRaf = null;
|
|
3539
|
+
}
|
|
3540
|
+
};
|
|
3541
|
+
const prefersReducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3542
|
+
const animateViewportTo = (targetCenter, targetSpan, onComplete) => {
|
|
3543
|
+
const animation = mergedOptions.animation ?? DEFAULT_OPTIONS.animation;
|
|
3544
|
+
const duration = Math.max(0, animation?.durationMs ?? 260);
|
|
3545
|
+
const startCenter = xCenter;
|
|
3546
|
+
const startSpan = xSpan;
|
|
3547
|
+
const centerDelta = targetCenter - startCenter;
|
|
3548
|
+
const spanRatio = startSpan > 0 ? targetSpan / startSpan : 1;
|
|
3549
|
+
const negligible = Math.abs(centerDelta) < 0.5 && Math.abs(spanRatio - 1) < 0.01;
|
|
3550
|
+
if (animation?.viewportTransitions === false || duration === 0 || negligible || prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
|
|
3551
|
+
xCenter = targetCenter;
|
|
3552
|
+
xSpan = targetSpan;
|
|
3553
|
+
clampXViewport();
|
|
3554
|
+
onComplete?.();
|
|
3555
|
+
emitViewportChange();
|
|
3556
|
+
scheduleDraw();
|
|
3557
|
+
return false;
|
|
3492
3558
|
}
|
|
3559
|
+
cancelViewportTween();
|
|
3560
|
+
const startedAt = performance.now();
|
|
3561
|
+
const step = (now) => {
|
|
3562
|
+
const t = clamp((now - startedAt) / duration, 0, 1);
|
|
3563
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
3564
|
+
xCenter = startCenter + centerDelta * eased;
|
|
3565
|
+
xSpan = startSpan * Math.pow(spanRatio, eased);
|
|
3566
|
+
clampXViewport();
|
|
3567
|
+
scheduleDraw();
|
|
3568
|
+
if (t < 1) {
|
|
3569
|
+
viewportTweenRaf = requestAnimationFrame(step);
|
|
3570
|
+
return;
|
|
3571
|
+
}
|
|
3572
|
+
viewportTweenRaf = null;
|
|
3573
|
+
xCenter = targetCenter;
|
|
3574
|
+
xSpan = targetSpan;
|
|
3575
|
+
clampXViewport();
|
|
3576
|
+
onComplete?.();
|
|
3577
|
+
emitViewportChange();
|
|
3578
|
+
scheduleDraw();
|
|
3579
|
+
};
|
|
3580
|
+
viewportTweenRaf = requestAnimationFrame(step);
|
|
3581
|
+
return true;
|
|
3582
|
+
};
|
|
3583
|
+
const getFitXTarget = () => {
|
|
3584
|
+
const count = data.length;
|
|
3585
|
+
if (count === 0) return { center: 0, span: 60 };
|
|
3493
3586
|
const maxSpan = Math.min(maxVisibleBars, Math.max(minVisibleBars, count + maxPanBars * 2));
|
|
3494
|
-
const
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
}
|
|
3499
|
-
|
|
3500
|
-
|
|
3587
|
+
const span = clamp(Math.floor(mergedOptions.initialVisibleBars), minVisibleBars, maxSpan);
|
|
3588
|
+
return {
|
|
3589
|
+
center: mergedOptions.initialViewport === "center" ? count / 2 : count - span / 2 + rightEdgePaddingBars,
|
|
3590
|
+
span
|
|
3591
|
+
};
|
|
3592
|
+
};
|
|
3593
|
+
const fitXViewport = () => {
|
|
3594
|
+
const target = getFitXTarget();
|
|
3595
|
+
xSpan = target.span;
|
|
3596
|
+
xCenter = target.center;
|
|
3501
3597
|
clampXViewport();
|
|
3502
3598
|
};
|
|
3503
3599
|
const getYBounds = () => {
|
|
@@ -6894,6 +6990,73 @@ function createChart(element, options = {}) {
|
|
|
6894
6990
|
for (const orderLine of orderLines) {
|
|
6895
6991
|
drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
6896
6992
|
}
|
|
6993
|
+
let dataLineBottom = chartTop;
|
|
6994
|
+
const dataLine = mergedOptions.dataLine;
|
|
6995
|
+
if (dataLine?.visible && data.length > 0) {
|
|
6996
|
+
const hoverIndex = crosshairPoint && getHitRegion(crosshairPoint.x, crosshairPoint.y) === "plot" ? indexFromCanvasX(crosshairPoint.x) : null;
|
|
6997
|
+
const barIndex = hoverIndex !== null && hoverIndex >= 0 && hoverIndex < data.length ? hoverIndex : data.length - 1;
|
|
6998
|
+
const bar = data[barIndex];
|
|
6999
|
+
if (bar) {
|
|
7000
|
+
const prevFont = ctx.font;
|
|
7001
|
+
const fontSize = Math.max(9, dataLine.fontSize || axis.fontSize);
|
|
7002
|
+
ctx.font = `${fontSize}px ${mergedOptions.fontFamily}`;
|
|
7003
|
+
ctx.textBaseline = "top";
|
|
7004
|
+
ctx.textAlign = "left";
|
|
7005
|
+
const symbolColor = dataLine.symbolColor || axis.textColor;
|
|
7006
|
+
const textColor = dataLine.textColor || labels.indicatorTextColor;
|
|
7007
|
+
const changeAbs = bar.c - bar.o;
|
|
7008
|
+
const changeColor = changeAbs >= 0 ? mergedOptions.upColor : mergedOptions.downColor;
|
|
7009
|
+
const rowY = chartTop + 6;
|
|
7010
|
+
let cursorX = chartLeft + 10;
|
|
7011
|
+
if (dataLine.statusColor) {
|
|
7012
|
+
const dotRadius = Math.max(2.5, fontSize / 4);
|
|
7013
|
+
ctx.fillStyle = dataLine.statusColor;
|
|
7014
|
+
ctx.beginPath();
|
|
7015
|
+
ctx.arc(cursorX + dotRadius, rowY + fontSize / 2, dotRadius, 0, Math.PI * 2);
|
|
7016
|
+
ctx.fill();
|
|
7017
|
+
cursorX += dotRadius * 2 + 6;
|
|
7018
|
+
}
|
|
7019
|
+
const headline = [dataLine.symbol, dataLine.interval, dataLine.exchange].filter((part) => part && part.length > 0).join(" \xB7 ");
|
|
7020
|
+
if (headline) {
|
|
7021
|
+
ctx.fillStyle = symbolColor;
|
|
7022
|
+
ctx.fillText(headline, cursorX, rowY);
|
|
7023
|
+
cursorX += measureTextWidth(headline) + 10;
|
|
7024
|
+
}
|
|
7025
|
+
if (dataLine.showOhlc !== false) {
|
|
7026
|
+
const ohlc = `O${formatPrice(bar.o)} H${formatPrice(bar.h)} L${formatPrice(bar.l)} C${formatPrice(bar.c)}`;
|
|
7027
|
+
ctx.fillStyle = changeColor;
|
|
7028
|
+
ctx.fillText(ohlc, cursorX, rowY);
|
|
7029
|
+
cursorX += measureTextWidth(ohlc) + 8;
|
|
7030
|
+
}
|
|
7031
|
+
if (dataLine.showChange !== false) {
|
|
7032
|
+
const pct = bar.o !== 0 ? changeAbs / bar.o * 100 : 0;
|
|
7033
|
+
const changeText = `${changeAbs >= 0 ? "+" : ""}${formatPrice(changeAbs)} (${changeAbs >= 0 ? "+" : ""}${pct.toFixed(2)}%)`;
|
|
7034
|
+
ctx.fillStyle = changeColor;
|
|
7035
|
+
ctx.fillText(changeText, cursorX, rowY);
|
|
7036
|
+
cursorX += measureTextWidth(changeText) + 10;
|
|
7037
|
+
}
|
|
7038
|
+
if (dataLine.showVolume && bar.v !== void 0) {
|
|
7039
|
+
const volumeText = `Vol ${bar.v >= 1e6 ? `${(bar.v / 1e6).toFixed(2)}M` : bar.v >= 1e3 ? `${(bar.v / 1e3).toFixed(1)}K` : String(Math.round(bar.v))}`;
|
|
7040
|
+
ctx.fillStyle = textColor;
|
|
7041
|
+
ctx.fillText(volumeText, cursorX, rowY);
|
|
7042
|
+
cursorX += measureTextWidth(volumeText) + 10;
|
|
7043
|
+
}
|
|
7044
|
+
for (const detail of dataLine.details ?? []) {
|
|
7045
|
+
const text = detail.label ? `${detail.label} ${detail.value}` : detail.value;
|
|
7046
|
+
const width2 = measureTextWidth(text);
|
|
7047
|
+
if (cursorX + width2 > chartRight - 4) break;
|
|
7048
|
+
if (detail.background) {
|
|
7049
|
+
ctx.fillStyle = detail.background;
|
|
7050
|
+
fillRoundedRect(cursorX - 4, rowY - 2, width2 + 8, fontSize + 4, 3);
|
|
7051
|
+
}
|
|
7052
|
+
ctx.fillStyle = detail.color || textColor;
|
|
7053
|
+
ctx.fillText(text, cursorX, rowY);
|
|
7054
|
+
cursorX += width2 + 12;
|
|
7055
|
+
}
|
|
7056
|
+
dataLineBottom = rowY + fontSize + 4;
|
|
7057
|
+
ctx.font = prevFont;
|
|
7058
|
+
}
|
|
7059
|
+
}
|
|
6897
7060
|
if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
|
|
6898
7061
|
const isLegendInputValue = (value) => {
|
|
6899
7062
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
@@ -6922,7 +7085,7 @@ function createChart(element, options = {}) {
|
|
|
6922
7085
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
6923
7086
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
6924
7087
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
6925
|
-
const legendY = isBottom ? chartBottom - offsetY : chartTop + offsetY;
|
|
7088
|
+
const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
|
|
6926
7089
|
drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
|
|
6927
7090
|
ctx.font = prevFont;
|
|
6928
7091
|
}
|
|
@@ -7463,27 +7626,24 @@ function createChart(element, options = {}) {
|
|
|
7463
7626
|
scheduleDraw();
|
|
7464
7627
|
};
|
|
7465
7628
|
const fitContent = () => {
|
|
7466
|
-
|
|
7467
|
-
updateFollowLatest(true);
|
|
7468
|
-
emitViewportChange();
|
|
7469
|
-
scheduleDraw();
|
|
7629
|
+
const target = getFitXTarget();
|
|
7630
|
+
animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
|
|
7470
7631
|
};
|
|
7471
7632
|
const resetViewport = () => {
|
|
7472
7633
|
cancelKineticPan();
|
|
7473
|
-
|
|
7634
|
+
const target = getFitXTarget();
|
|
7474
7635
|
resetYViewport();
|
|
7475
|
-
updateFollowLatest(true);
|
|
7476
|
-
emitViewportChange();
|
|
7477
|
-
scheduleDraw();
|
|
7636
|
+
animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
|
|
7478
7637
|
};
|
|
7479
7638
|
const isFollowingLatest = () => followLatest;
|
|
7480
7639
|
const setFollowingLatest = (follow) => {
|
|
7481
7640
|
if (follow && data.length > 0) {
|
|
7482
7641
|
cancelKineticPan();
|
|
7483
|
-
|
|
7484
|
-
|
|
7485
|
-
|
|
7486
|
-
|
|
7642
|
+
animateViewportTo(
|
|
7643
|
+
data.length - xSpan / 2 + rightEdgePaddingBars,
|
|
7644
|
+
xSpan,
|
|
7645
|
+
() => updateFollowLatest(true)
|
|
7646
|
+
);
|
|
7487
7647
|
} else {
|
|
7488
7648
|
updateFollowLatest(follow);
|
|
7489
7649
|
}
|
|
@@ -8766,6 +8926,7 @@ function createChart(element, options = {}) {
|
|
|
8766
8926
|
canvas.focus({ preventScroll: true });
|
|
8767
8927
|
}
|
|
8768
8928
|
cancelKineticPan();
|
|
8929
|
+
cancelViewportTween();
|
|
8769
8930
|
panVelocitySamples.length = 0;
|
|
8770
8931
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
8771
8932
|
shiftKeyActive = event.shiftKey;
|
|
@@ -9430,6 +9591,7 @@ function createChart(element, options = {}) {
|
|
|
9430
9591
|
return;
|
|
9431
9592
|
}
|
|
9432
9593
|
cancelKineticPan();
|
|
9594
|
+
cancelViewportTween();
|
|
9433
9595
|
const point = getCanvasPoint(event);
|
|
9434
9596
|
const region = getHitRegion(point.x, point.y);
|
|
9435
9597
|
if (region === "outside") {
|
|
@@ -9707,6 +9869,7 @@ function createChart(element, options = {}) {
|
|
|
9707
9869
|
resetRightMarginCache();
|
|
9708
9870
|
doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
9709
9871
|
doubleClickAction = mergedOptions.doubleClickAction;
|
|
9872
|
+
setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
|
|
9710
9873
|
applyAccessibilityAttributes();
|
|
9711
9874
|
const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
|
|
9712
9875
|
if (!isTickerSmoothingEnabled) {
|
|
@@ -10185,6 +10348,7 @@ function createChart(element, options = {}) {
|
|
|
10185
10348
|
return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
|
|
10186
10349
|
};
|
|
10187
10350
|
const destroy = () => {
|
|
10351
|
+
cancelViewportTween();
|
|
10188
10352
|
cancelLongPressTimer();
|
|
10189
10353
|
datafeedUnsubscribe?.();
|
|
10190
10354
|
datafeedUnsubscribe = null;
|
package/dist/index.cjs
CHANGED
|
@@ -369,26 +369,45 @@ var getSeriesFingerprint = (data) => {
|
|
|
369
369
|
var withCachedSeries = (key, data, compute) => {
|
|
370
370
|
const fingerprint = getSeriesFingerprint(data);
|
|
371
371
|
const existing = builtInSeriesCache.get(key);
|
|
372
|
-
|
|
373
|
-
|
|
372
|
+
const shape = getSeriesShape(data);
|
|
373
|
+
if (existing) {
|
|
374
|
+
const unchanged = existing.fingerprint === fingerprint;
|
|
375
|
+
const liveTickWithinThrottle = !unchanged && indicatorLiveThrottleMs > 0 && existing.shape === shape && Date.now() - existing.computedAt < indicatorLiveThrottleMs;
|
|
376
|
+
if (unchanged || liveTickWithinThrottle) {
|
|
377
|
+
return existing.values;
|
|
378
|
+
}
|
|
374
379
|
}
|
|
375
380
|
const values = compute();
|
|
376
|
-
builtInSeriesCache.set(key, { fingerprint, values });
|
|
381
|
+
builtInSeriesCache.set(key, { fingerprint, values, shape, computedAt: Date.now() });
|
|
377
382
|
return values;
|
|
378
383
|
};
|
|
379
384
|
var builtInComputationCache = /* @__PURE__ */ new Map();
|
|
385
|
+
var indicatorLiveThrottleMs = 80;
|
|
386
|
+
var setIndicatorLiveThrottleMs = (ms) => {
|
|
387
|
+
indicatorLiveThrottleMs = Math.max(0, ms);
|
|
388
|
+
};
|
|
389
|
+
var getSeriesShape = (data) => {
|
|
390
|
+
const length = data.length;
|
|
391
|
+
const last = length > 0 ? data[length - 1] : void 0;
|
|
392
|
+
return last ? `${length}|${last.time.getTime()}` : "empty";
|
|
393
|
+
};
|
|
380
394
|
var COMPUTATION_CACHE_MAX_ENTRIES = 128;
|
|
381
395
|
var withCachedComputation = (key, data, compute) => {
|
|
382
396
|
const fingerprint = getSeriesFingerprint(data);
|
|
383
397
|
const existing = builtInComputationCache.get(key);
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
398
|
+
const shape = getSeriesShape(data);
|
|
399
|
+
if (existing) {
|
|
400
|
+
const unchanged = existing.fingerprint === fingerprint;
|
|
401
|
+
const liveTickWithinThrottle = !unchanged && indicatorLiveThrottleMs > 0 && existing.shape === shape && Date.now() - existing.computedAt < indicatorLiveThrottleMs;
|
|
402
|
+
if (unchanged || liveTickWithinThrottle) {
|
|
403
|
+
builtInComputationCache.delete(key);
|
|
404
|
+
builtInComputationCache.set(key, existing);
|
|
405
|
+
return existing.value;
|
|
406
|
+
}
|
|
388
407
|
}
|
|
389
408
|
const value = compute();
|
|
390
409
|
builtInComputationCache.delete(key);
|
|
391
|
-
builtInComputationCache.set(key, { fingerprint, value });
|
|
410
|
+
builtInComputationCache.set(key, { fingerprint, value, shape, computedAt: Date.now() });
|
|
392
411
|
while (builtInComputationCache.size > COMPUTATION_CACHE_MAX_ENTRIES) {
|
|
393
412
|
const oldest = builtInComputationCache.keys().next().value;
|
|
394
413
|
if (oldest === void 0) break;
|
|
@@ -2958,7 +2977,23 @@ var DEFAULT_OPTIONS = {
|
|
|
2958
2977
|
accessibility: { label: "Price chart", description: "", focusable: true },
|
|
2959
2978
|
downsampling: { enabled: true, thresholdPx: 1.5 },
|
|
2960
2979
|
touch: { hitToleranceScale: 2.2, longPressTooltip: true, longPressMs: 400 },
|
|
2980
|
+
dataLine: {
|
|
2981
|
+
visible: false,
|
|
2982
|
+
symbol: "",
|
|
2983
|
+
interval: "",
|
|
2984
|
+
exchange: "",
|
|
2985
|
+
showOhlc: true,
|
|
2986
|
+
showChange: true,
|
|
2987
|
+
showVolume: false,
|
|
2988
|
+
details: [],
|
|
2989
|
+
statusColor: "",
|
|
2990
|
+
fontSize: 0,
|
|
2991
|
+
symbolColor: "",
|
|
2992
|
+
textColor: ""
|
|
2993
|
+
},
|
|
2961
2994
|
datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
|
|
2995
|
+
animation: { viewportTransitions: true, durationMs: 260 },
|
|
2996
|
+
indicatorUpdate: { liveThrottleMs: 80 },
|
|
2962
2997
|
crosshair: DEFAULT_CROSSHAIR_OPTIONS,
|
|
2963
2998
|
grid: DEFAULT_GRID_OPTIONS,
|
|
2964
2999
|
watermark: DEFAULT_WATERMARK_OPTIONS,
|
|
@@ -3017,6 +3052,18 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
|
|
|
3017
3052
|
...baseOptions.touch,
|
|
3018
3053
|
...options.touch ?? {}
|
|
3019
3054
|
},
|
|
3055
|
+
dataLine: {
|
|
3056
|
+
...baseOptions.dataLine,
|
|
3057
|
+
...options.dataLine ?? {}
|
|
3058
|
+
},
|
|
3059
|
+
animation: {
|
|
3060
|
+
...baseOptions.animation,
|
|
3061
|
+
...options.animation ?? {}
|
|
3062
|
+
},
|
|
3063
|
+
indicatorUpdate: {
|
|
3064
|
+
...baseOptions.indicatorUpdate,
|
|
3065
|
+
...options.indicatorUpdate ?? {}
|
|
3066
|
+
},
|
|
3020
3067
|
datafeedOptions: {
|
|
3021
3068
|
...baseOptions.datafeedOptions,
|
|
3022
3069
|
...options.datafeedOptions ?? {}
|
|
@@ -3276,6 +3323,7 @@ function createChart(element, options = {}) {
|
|
|
3276
3323
|
let crosshairPoint = null;
|
|
3277
3324
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
3278
3325
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
3326
|
+
setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
|
|
3279
3327
|
let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
|
|
3280
3328
|
let rightAxisLabelSlots = [];
|
|
3281
3329
|
let plotLabelSlots = [];
|
|
@@ -3517,21 +3565,69 @@ function createChart(element, options = {}) {
|
|
|
3517
3565
|
Math.min(highCenter, count + maxPanBars)
|
|
3518
3566
|
);
|
|
3519
3567
|
};
|
|
3520
|
-
|
|
3521
|
-
|
|
3522
|
-
if (
|
|
3523
|
-
|
|
3524
|
-
|
|
3525
|
-
|
|
3568
|
+
let viewportTweenRaf = null;
|
|
3569
|
+
const cancelViewportTween = () => {
|
|
3570
|
+
if (viewportTweenRaf !== null) {
|
|
3571
|
+
cancelAnimationFrame(viewportTweenRaf);
|
|
3572
|
+
viewportTweenRaf = null;
|
|
3573
|
+
}
|
|
3574
|
+
};
|
|
3575
|
+
const prefersReducedMotion = () => typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
3576
|
+
const animateViewportTo = (targetCenter, targetSpan, onComplete) => {
|
|
3577
|
+
const animation = mergedOptions.animation ?? DEFAULT_OPTIONS.animation;
|
|
3578
|
+
const duration = Math.max(0, animation?.durationMs ?? 260);
|
|
3579
|
+
const startCenter = xCenter;
|
|
3580
|
+
const startSpan = xSpan;
|
|
3581
|
+
const centerDelta = targetCenter - startCenter;
|
|
3582
|
+
const spanRatio = startSpan > 0 ? targetSpan / startSpan : 1;
|
|
3583
|
+
const negligible = Math.abs(centerDelta) < 0.5 && Math.abs(spanRatio - 1) < 0.01;
|
|
3584
|
+
if (animation?.viewportTransitions === false || duration === 0 || negligible || prefersReducedMotion() || typeof requestAnimationFrame !== "function") {
|
|
3585
|
+
xCenter = targetCenter;
|
|
3586
|
+
xSpan = targetSpan;
|
|
3587
|
+
clampXViewport();
|
|
3588
|
+
onComplete?.();
|
|
3589
|
+
emitViewportChange();
|
|
3590
|
+
scheduleDraw();
|
|
3591
|
+
return false;
|
|
3526
3592
|
}
|
|
3593
|
+
cancelViewportTween();
|
|
3594
|
+
const startedAt = performance.now();
|
|
3595
|
+
const step = (now) => {
|
|
3596
|
+
const t = clamp((now - startedAt) / duration, 0, 1);
|
|
3597
|
+
const eased = 1 - Math.pow(1 - t, 3);
|
|
3598
|
+
xCenter = startCenter + centerDelta * eased;
|
|
3599
|
+
xSpan = startSpan * Math.pow(spanRatio, eased);
|
|
3600
|
+
clampXViewport();
|
|
3601
|
+
scheduleDraw();
|
|
3602
|
+
if (t < 1) {
|
|
3603
|
+
viewportTweenRaf = requestAnimationFrame(step);
|
|
3604
|
+
return;
|
|
3605
|
+
}
|
|
3606
|
+
viewportTweenRaf = null;
|
|
3607
|
+
xCenter = targetCenter;
|
|
3608
|
+
xSpan = targetSpan;
|
|
3609
|
+
clampXViewport();
|
|
3610
|
+
onComplete?.();
|
|
3611
|
+
emitViewportChange();
|
|
3612
|
+
scheduleDraw();
|
|
3613
|
+
};
|
|
3614
|
+
viewportTweenRaf = requestAnimationFrame(step);
|
|
3615
|
+
return true;
|
|
3616
|
+
};
|
|
3617
|
+
const getFitXTarget = () => {
|
|
3618
|
+
const count = data.length;
|
|
3619
|
+
if (count === 0) return { center: 0, span: 60 };
|
|
3527
3620
|
const maxSpan = Math.min(maxVisibleBars, Math.max(minVisibleBars, count + maxPanBars * 2));
|
|
3528
|
-
const
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
}
|
|
3533
|
-
|
|
3534
|
-
|
|
3621
|
+
const span = clamp(Math.floor(mergedOptions.initialVisibleBars), minVisibleBars, maxSpan);
|
|
3622
|
+
return {
|
|
3623
|
+
center: mergedOptions.initialViewport === "center" ? count / 2 : count - span / 2 + rightEdgePaddingBars,
|
|
3624
|
+
span
|
|
3625
|
+
};
|
|
3626
|
+
};
|
|
3627
|
+
const fitXViewport = () => {
|
|
3628
|
+
const target = getFitXTarget();
|
|
3629
|
+
xSpan = target.span;
|
|
3630
|
+
xCenter = target.center;
|
|
3535
3631
|
clampXViewport();
|
|
3536
3632
|
};
|
|
3537
3633
|
const getYBounds = () => {
|
|
@@ -6928,6 +7024,73 @@ function createChart(element, options = {}) {
|
|
|
6928
7024
|
for (const orderLine of orderLines) {
|
|
6929
7025
|
drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
6930
7026
|
}
|
|
7027
|
+
let dataLineBottom = chartTop;
|
|
7028
|
+
const dataLine = mergedOptions.dataLine;
|
|
7029
|
+
if (dataLine?.visible && data.length > 0) {
|
|
7030
|
+
const hoverIndex = crosshairPoint && getHitRegion(crosshairPoint.x, crosshairPoint.y) === "plot" ? indexFromCanvasX(crosshairPoint.x) : null;
|
|
7031
|
+
const barIndex = hoverIndex !== null && hoverIndex >= 0 && hoverIndex < data.length ? hoverIndex : data.length - 1;
|
|
7032
|
+
const bar = data[barIndex];
|
|
7033
|
+
if (bar) {
|
|
7034
|
+
const prevFont = ctx.font;
|
|
7035
|
+
const fontSize = Math.max(9, dataLine.fontSize || axis.fontSize);
|
|
7036
|
+
ctx.font = `${fontSize}px ${mergedOptions.fontFamily}`;
|
|
7037
|
+
ctx.textBaseline = "top";
|
|
7038
|
+
ctx.textAlign = "left";
|
|
7039
|
+
const symbolColor = dataLine.symbolColor || axis.textColor;
|
|
7040
|
+
const textColor = dataLine.textColor || labels.indicatorTextColor;
|
|
7041
|
+
const changeAbs = bar.c - bar.o;
|
|
7042
|
+
const changeColor = changeAbs >= 0 ? mergedOptions.upColor : mergedOptions.downColor;
|
|
7043
|
+
const rowY = chartTop + 6;
|
|
7044
|
+
let cursorX = chartLeft + 10;
|
|
7045
|
+
if (dataLine.statusColor) {
|
|
7046
|
+
const dotRadius = Math.max(2.5, fontSize / 4);
|
|
7047
|
+
ctx.fillStyle = dataLine.statusColor;
|
|
7048
|
+
ctx.beginPath();
|
|
7049
|
+
ctx.arc(cursorX + dotRadius, rowY + fontSize / 2, dotRadius, 0, Math.PI * 2);
|
|
7050
|
+
ctx.fill();
|
|
7051
|
+
cursorX += dotRadius * 2 + 6;
|
|
7052
|
+
}
|
|
7053
|
+
const headline = [dataLine.symbol, dataLine.interval, dataLine.exchange].filter((part) => part && part.length > 0).join(" \xB7 ");
|
|
7054
|
+
if (headline) {
|
|
7055
|
+
ctx.fillStyle = symbolColor;
|
|
7056
|
+
ctx.fillText(headline, cursorX, rowY);
|
|
7057
|
+
cursorX += measureTextWidth(headline) + 10;
|
|
7058
|
+
}
|
|
7059
|
+
if (dataLine.showOhlc !== false) {
|
|
7060
|
+
const ohlc = `O${formatPrice(bar.o)} H${formatPrice(bar.h)} L${formatPrice(bar.l)} C${formatPrice(bar.c)}`;
|
|
7061
|
+
ctx.fillStyle = changeColor;
|
|
7062
|
+
ctx.fillText(ohlc, cursorX, rowY);
|
|
7063
|
+
cursorX += measureTextWidth(ohlc) + 8;
|
|
7064
|
+
}
|
|
7065
|
+
if (dataLine.showChange !== false) {
|
|
7066
|
+
const pct = bar.o !== 0 ? changeAbs / bar.o * 100 : 0;
|
|
7067
|
+
const changeText = `${changeAbs >= 0 ? "+" : ""}${formatPrice(changeAbs)} (${changeAbs >= 0 ? "+" : ""}${pct.toFixed(2)}%)`;
|
|
7068
|
+
ctx.fillStyle = changeColor;
|
|
7069
|
+
ctx.fillText(changeText, cursorX, rowY);
|
|
7070
|
+
cursorX += measureTextWidth(changeText) + 10;
|
|
7071
|
+
}
|
|
7072
|
+
if (dataLine.showVolume && bar.v !== void 0) {
|
|
7073
|
+
const volumeText = `Vol ${bar.v >= 1e6 ? `${(bar.v / 1e6).toFixed(2)}M` : bar.v >= 1e3 ? `${(bar.v / 1e3).toFixed(1)}K` : String(Math.round(bar.v))}`;
|
|
7074
|
+
ctx.fillStyle = textColor;
|
|
7075
|
+
ctx.fillText(volumeText, cursorX, rowY);
|
|
7076
|
+
cursorX += measureTextWidth(volumeText) + 10;
|
|
7077
|
+
}
|
|
7078
|
+
for (const detail of dataLine.details ?? []) {
|
|
7079
|
+
const text = detail.label ? `${detail.label} ${detail.value}` : detail.value;
|
|
7080
|
+
const width2 = measureTextWidth(text);
|
|
7081
|
+
if (cursorX + width2 > chartRight - 4) break;
|
|
7082
|
+
if (detail.background) {
|
|
7083
|
+
ctx.fillStyle = detail.background;
|
|
7084
|
+
fillRoundedRect(cursorX - 4, rowY - 2, width2 + 8, fontSize + 4, 3);
|
|
7085
|
+
}
|
|
7086
|
+
ctx.fillStyle = detail.color || textColor;
|
|
7087
|
+
ctx.fillText(text, cursorX, rowY);
|
|
7088
|
+
cursorX += width2 + 12;
|
|
7089
|
+
}
|
|
7090
|
+
dataLineBottom = rowY + fontSize + 4;
|
|
7091
|
+
ctx.font = prevFont;
|
|
7092
|
+
}
|
|
7093
|
+
}
|
|
6931
7094
|
if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
|
|
6932
7095
|
const isLegendInputValue = (value) => {
|
|
6933
7096
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
@@ -6956,7 +7119,7 @@ function createChart(element, options = {}) {
|
|
|
6956
7119
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
6957
7120
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
6958
7121
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
6959
|
-
const legendY = isBottom ? chartBottom - offsetY : chartTop + offsetY;
|
|
7122
|
+
const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
|
|
6960
7123
|
drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
|
|
6961
7124
|
ctx.font = prevFont;
|
|
6962
7125
|
}
|
|
@@ -7497,27 +7660,24 @@ function createChart(element, options = {}) {
|
|
|
7497
7660
|
scheduleDraw();
|
|
7498
7661
|
};
|
|
7499
7662
|
const fitContent = () => {
|
|
7500
|
-
|
|
7501
|
-
updateFollowLatest(true);
|
|
7502
|
-
emitViewportChange();
|
|
7503
|
-
scheduleDraw();
|
|
7663
|
+
const target = getFitXTarget();
|
|
7664
|
+
animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
|
|
7504
7665
|
};
|
|
7505
7666
|
const resetViewport = () => {
|
|
7506
7667
|
cancelKineticPan();
|
|
7507
|
-
|
|
7668
|
+
const target = getFitXTarget();
|
|
7508
7669
|
resetYViewport();
|
|
7509
|
-
updateFollowLatest(true);
|
|
7510
|
-
emitViewportChange();
|
|
7511
|
-
scheduleDraw();
|
|
7670
|
+
animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
|
|
7512
7671
|
};
|
|
7513
7672
|
const isFollowingLatest = () => followLatest;
|
|
7514
7673
|
const setFollowingLatest = (follow) => {
|
|
7515
7674
|
if (follow && data.length > 0) {
|
|
7516
7675
|
cancelKineticPan();
|
|
7517
|
-
|
|
7518
|
-
|
|
7519
|
-
|
|
7520
|
-
|
|
7676
|
+
animateViewportTo(
|
|
7677
|
+
data.length - xSpan / 2 + rightEdgePaddingBars,
|
|
7678
|
+
xSpan,
|
|
7679
|
+
() => updateFollowLatest(true)
|
|
7680
|
+
);
|
|
7521
7681
|
} else {
|
|
7522
7682
|
updateFollowLatest(follow);
|
|
7523
7683
|
}
|
|
@@ -8800,6 +8960,7 @@ function createChart(element, options = {}) {
|
|
|
8800
8960
|
canvas.focus({ preventScroll: true });
|
|
8801
8961
|
}
|
|
8802
8962
|
cancelKineticPan();
|
|
8963
|
+
cancelViewportTween();
|
|
8803
8964
|
panVelocitySamples.length = 0;
|
|
8804
8965
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
8805
8966
|
shiftKeyActive = event.shiftKey;
|
|
@@ -9464,6 +9625,7 @@ function createChart(element, options = {}) {
|
|
|
9464
9625
|
return;
|
|
9465
9626
|
}
|
|
9466
9627
|
cancelKineticPan();
|
|
9628
|
+
cancelViewportTween();
|
|
9467
9629
|
const point = getCanvasPoint(event);
|
|
9468
9630
|
const region = getHitRegion(point.x, point.y);
|
|
9469
9631
|
if (region === "outside") {
|
|
@@ -9741,6 +9903,7 @@ function createChart(element, options = {}) {
|
|
|
9741
9903
|
resetRightMarginCache();
|
|
9742
9904
|
doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
9743
9905
|
doubleClickAction = mergedOptions.doubleClickAction;
|
|
9906
|
+
setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
|
|
9744
9907
|
applyAccessibilityAttributes();
|
|
9745
9908
|
const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
|
|
9746
9909
|
if (!isTickerSmoothingEnabled) {
|
|
@@ -10219,6 +10382,7 @@ function createChart(element, options = {}) {
|
|
|
10219
10382
|
return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
|
|
10220
10383
|
};
|
|
10221
10384
|
const destroy = () => {
|
|
10385
|
+
cancelViewportTween();
|
|
10222
10386
|
cancelLongPressTimer();
|
|
10223
10387
|
datafeedUnsubscribe?.();
|
|
10224
10388
|
datafeedUnsubscribe = null;
|