hyperprop-charting-library 0.1.141 → 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 +256 -73
- package/dist/hyperprop-charting-library.d.ts +72 -8
- package/dist/hyperprop-charting-library.js +256 -73
- package/dist/index.cjs +256 -73
- package/dist/index.d.cts +72 -8
- package/dist/index.d.ts +72 -8
- package/dist/index.js +256 -73
- package/docs/API.md +92 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -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;
|
|
@@ -2625,44 +2644,6 @@ var compileScriptIndicator = (definition) => {
|
|
|
2625
2644
|
|
|
2626
2645
|
// src/themes.ts
|
|
2627
2646
|
var CHART_THEMES = {
|
|
2628
|
-
dark: {
|
|
2629
|
-
name: "dark",
|
|
2630
|
-
background: "#0f0f0f",
|
|
2631
|
-
upColor: "#089981",
|
|
2632
|
-
downColor: "#f23645",
|
|
2633
|
-
lineColor: "#2962ff",
|
|
2634
|
-
gridColor: "#3c3c3c",
|
|
2635
|
-
gridOpacity: 0.5,
|
|
2636
|
-
axisTextColor: "#dbdbdb",
|
|
2637
|
-
axisLineColor: "#424242",
|
|
2638
|
-
crosshairColor: "#9598a1",
|
|
2639
|
-
crosshairLabelBackground: "#3c3c3c",
|
|
2640
|
-
crosshairLabelText: "#dbdbdb",
|
|
2641
|
-
crosshairLabelBorder: "#5d5d5d",
|
|
2642
|
-
watermarkColor: "rgba(219,219,219,0.06)",
|
|
2643
|
-
indicatorTextColor: "#b2b5be",
|
|
2644
|
-
labelTextColor: "#ffffff",
|
|
2645
|
-
labelBackground: "#1a1a1a"
|
|
2646
|
-
},
|
|
2647
|
-
light: {
|
|
2648
|
-
name: "light",
|
|
2649
|
-
background: "#ffffff",
|
|
2650
|
-
upColor: "#089981",
|
|
2651
|
-
downColor: "#f23645",
|
|
2652
|
-
lineColor: "#2962ff",
|
|
2653
|
-
gridColor: "#d5d5d5",
|
|
2654
|
-
gridOpacity: 0.9,
|
|
2655
|
-
axisTextColor: "#0f0f0f",
|
|
2656
|
-
axisLineColor: "#c3c3c3",
|
|
2657
|
-
crosshairColor: "#787b86",
|
|
2658
|
-
crosshairLabelBackground: "#4b4b4b",
|
|
2659
|
-
crosshairLabelText: "#ffffff",
|
|
2660
|
-
crosshairLabelBorder: "#878787",
|
|
2661
|
-
watermarkColor: "rgba(15,15,15,0.06)",
|
|
2662
|
-
indicatorTextColor: "#4b4b4b",
|
|
2663
|
-
labelTextColor: "#ffffff",
|
|
2664
|
-
labelBackground: "#f0f3fa"
|
|
2665
|
-
},
|
|
2666
2647
|
midnight: {
|
|
2667
2648
|
name: "midnight",
|
|
2668
2649
|
background: "#171717",
|
|
@@ -2682,6 +2663,63 @@ var CHART_THEMES = {
|
|
|
2682
2663
|
labelTextColor: "#dbeafe",
|
|
2683
2664
|
labelBackground: "#0b1220"
|
|
2684
2665
|
},
|
|
2666
|
+
light: {
|
|
2667
|
+
name: "light",
|
|
2668
|
+
background: "#ffffff",
|
|
2669
|
+
upColor: "#22ab94",
|
|
2670
|
+
downColor: "#f23645",
|
|
2671
|
+
lineColor: "#2563eb",
|
|
2672
|
+
gridColor: "#e0e3eb",
|
|
2673
|
+
gridOpacity: 0.9,
|
|
2674
|
+
axisTextColor: "#1f2430",
|
|
2675
|
+
axisLineColor: "#c8ccd6",
|
|
2676
|
+
crosshairColor: "#6b7280",
|
|
2677
|
+
crosshairLabelBackground: "#2a2e39",
|
|
2678
|
+
crosshairLabelText: "#ffffff",
|
|
2679
|
+
crosshairLabelBorder: "#6b7280",
|
|
2680
|
+
watermarkColor: "rgba(19,23,34,0.06)",
|
|
2681
|
+
indicatorTextColor: "#4b5563",
|
|
2682
|
+
labelTextColor: "#ffffff",
|
|
2683
|
+
labelBackground: "#f1f3f8"
|
|
2684
|
+
},
|
|
2685
|
+
brand: {
|
|
2686
|
+
name: "brand",
|
|
2687
|
+
background: "#0b1220",
|
|
2688
|
+
upColor: "#22ab94",
|
|
2689
|
+
downColor: "#f23645",
|
|
2690
|
+
lineColor: "#253cff",
|
|
2691
|
+
gridColor: "#1b2740",
|
|
2692
|
+
gridOpacity: 0.6,
|
|
2693
|
+
axisTextColor: "#c3cee8",
|
|
2694
|
+
axisLineColor: "#34405c",
|
|
2695
|
+
crosshairColor: "#6e85ff",
|
|
2696
|
+
crosshairLabelBackground: "#253cff",
|
|
2697
|
+
crosshairLabelText: "#ffffff",
|
|
2698
|
+
crosshairLabelBorder: "#6e85ff",
|
|
2699
|
+
watermarkColor: "rgba(37,60,255,0.08)",
|
|
2700
|
+
indicatorTextColor: "#a9b8dc",
|
|
2701
|
+
labelTextColor: "#ffffff",
|
|
2702
|
+
labelBackground: "#101a2e"
|
|
2703
|
+
},
|
|
2704
|
+
dark: {
|
|
2705
|
+
name: "dark",
|
|
2706
|
+
background: "#0f0f0f",
|
|
2707
|
+
upColor: "#089981",
|
|
2708
|
+
downColor: "#f23645",
|
|
2709
|
+
lineColor: "#2962ff",
|
|
2710
|
+
gridColor: "#3c3c3c",
|
|
2711
|
+
gridOpacity: 0.5,
|
|
2712
|
+
axisTextColor: "#dbdbdb",
|
|
2713
|
+
axisLineColor: "#424242",
|
|
2714
|
+
crosshairColor: "#9598a1",
|
|
2715
|
+
crosshairLabelBackground: "#3c3c3c",
|
|
2716
|
+
crosshairLabelText: "#dbdbdb",
|
|
2717
|
+
crosshairLabelBorder: "#5d5d5d",
|
|
2718
|
+
watermarkColor: "rgba(219,219,219,0.06)",
|
|
2719
|
+
indicatorTextColor: "#b2b5be",
|
|
2720
|
+
labelTextColor: "#ffffff",
|
|
2721
|
+
labelBackground: "#1a1a1a"
|
|
2722
|
+
},
|
|
2685
2723
|
"high-contrast": {
|
|
2686
2724
|
name: "high-contrast",
|
|
2687
2725
|
background: "#000000",
|
|
@@ -2703,7 +2741,7 @@ var CHART_THEMES = {
|
|
|
2703
2741
|
}
|
|
2704
2742
|
};
|
|
2705
2743
|
var CHART_THEME_NAMES = Object.keys(CHART_THEMES);
|
|
2706
|
-
var resolveTheme = (theme) => typeof theme === "string" ? CHART_THEMES[theme] ?? CHART_THEMES.
|
|
2744
|
+
var resolveTheme = (theme) => typeof theme === "string" ? CHART_THEMES[theme] ?? CHART_THEMES.midnight : theme;
|
|
2707
2745
|
|
|
2708
2746
|
// src/options.ts
|
|
2709
2747
|
var DEFAULT_GRID_OPTIONS = {
|
|
@@ -2905,7 +2943,23 @@ var DEFAULT_OPTIONS = {
|
|
|
2905
2943
|
accessibility: { label: "Price chart", description: "", focusable: true },
|
|
2906
2944
|
downsampling: { enabled: true, thresholdPx: 1.5 },
|
|
2907
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
|
+
},
|
|
2908
2960
|
datafeedOptions: { prefetchThresholdBars: 150, cooldownMs: 1200, chunkBars: 1500 },
|
|
2961
|
+
animation: { viewportTransitions: true, durationMs: 260 },
|
|
2962
|
+
indicatorUpdate: { liveThrottleMs: 80 },
|
|
2909
2963
|
crosshair: DEFAULT_CROSSHAIR_OPTIONS,
|
|
2910
2964
|
grid: DEFAULT_GRID_OPTIONS,
|
|
2911
2965
|
watermark: DEFAULT_WATERMARK_OPTIONS,
|
|
@@ -2964,6 +3018,18 @@ var mergeChartOptions = (baseOptions, options = {}) => ({
|
|
|
2964
3018
|
...baseOptions.touch,
|
|
2965
3019
|
...options.touch ?? {}
|
|
2966
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
|
+
},
|
|
2967
3033
|
datafeedOptions: {
|
|
2968
3034
|
...baseOptions.datafeedOptions,
|
|
2969
3035
|
...options.datafeedOptions ?? {}
|
|
@@ -3223,6 +3289,7 @@ function createChart(element, options = {}) {
|
|
|
3223
3289
|
let crosshairPoint = null;
|
|
3224
3290
|
let doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
3225
3291
|
let doubleClickAction = mergedOptions.doubleClickAction;
|
|
3292
|
+
setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
|
|
3226
3293
|
let noOverlappingLineLabels = DEFAULT_LABELS_OPTIONS.noOverlapping;
|
|
3227
3294
|
let rightAxisLabelSlots = [];
|
|
3228
3295
|
let plotLabelSlots = [];
|
|
@@ -3464,21 +3531,69 @@ function createChart(element, options = {}) {
|
|
|
3464
3531
|
Math.min(highCenter, count + maxPanBars)
|
|
3465
3532
|
);
|
|
3466
3533
|
};
|
|
3467
|
-
|
|
3468
|
-
|
|
3469
|
-
if (
|
|
3470
|
-
|
|
3471
|
-
|
|
3472
|
-
|
|
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;
|
|
3473
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 };
|
|
3474
3586
|
const maxSpan = Math.min(maxVisibleBars, Math.max(minVisibleBars, count + maxPanBars * 2));
|
|
3475
|
-
const
|
|
3476
|
-
|
|
3477
|
-
|
|
3478
|
-
|
|
3479
|
-
}
|
|
3480
|
-
|
|
3481
|
-
|
|
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;
|
|
3482
3597
|
clampXViewport();
|
|
3483
3598
|
};
|
|
3484
3599
|
const getYBounds = () => {
|
|
@@ -6875,6 +6990,73 @@ function createChart(element, options = {}) {
|
|
|
6875
6990
|
for (const orderLine of orderLines) {
|
|
6876
6991
|
drawOrderLine(orderLine, yFromPrice, chartLeft, chartTop, chartRight, chartBottom);
|
|
6877
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
|
+
}
|
|
6878
7060
|
if (labels.visible && (labels.showIndicatorNames || labels.showIndicatorValues)) {
|
|
6879
7061
|
const isLegendInputValue = (value) => {
|
|
6880
7062
|
if (typeof value === "number" || typeof value === "boolean") {
|
|
@@ -6903,7 +7085,7 @@ function createChart(element, options = {}) {
|
|
|
6903
7085
|
const isRight = position === "top-right" || position === "bottom-right";
|
|
6904
7086
|
const isBottom = position === "bottom-left" || position === "bottom-right";
|
|
6905
7087
|
const legendX = isRight ? chartRight - offsetX : chartLeft + offsetX;
|
|
6906
|
-
const legendY = isBottom ? chartBottom - offsetY : chartTop + offsetY;
|
|
7088
|
+
const legendY = isBottom ? chartBottom - offsetY : Math.max(chartTop + offsetY, dataLineBottom);
|
|
6907
7089
|
drawText(legendText, legendX, legendY, isRight ? "right" : "left", isBottom ? "bottom" : "top", labels.indicatorTextColor);
|
|
6908
7090
|
ctx.font = prevFont;
|
|
6909
7091
|
}
|
|
@@ -7444,27 +7626,24 @@ function createChart(element, options = {}) {
|
|
|
7444
7626
|
scheduleDraw();
|
|
7445
7627
|
};
|
|
7446
7628
|
const fitContent = () => {
|
|
7447
|
-
|
|
7448
|
-
updateFollowLatest(true);
|
|
7449
|
-
emitViewportChange();
|
|
7450
|
-
scheduleDraw();
|
|
7629
|
+
const target = getFitXTarget();
|
|
7630
|
+
animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
|
|
7451
7631
|
};
|
|
7452
7632
|
const resetViewport = () => {
|
|
7453
7633
|
cancelKineticPan();
|
|
7454
|
-
|
|
7634
|
+
const target = getFitXTarget();
|
|
7455
7635
|
resetYViewport();
|
|
7456
|
-
updateFollowLatest(true);
|
|
7457
|
-
emitViewportChange();
|
|
7458
|
-
scheduleDraw();
|
|
7636
|
+
animateViewportTo(target.center, target.span, () => updateFollowLatest(true));
|
|
7459
7637
|
};
|
|
7460
7638
|
const isFollowingLatest = () => followLatest;
|
|
7461
7639
|
const setFollowingLatest = (follow) => {
|
|
7462
7640
|
if (follow && data.length > 0) {
|
|
7463
7641
|
cancelKineticPan();
|
|
7464
|
-
|
|
7465
|
-
|
|
7466
|
-
|
|
7467
|
-
|
|
7642
|
+
animateViewportTo(
|
|
7643
|
+
data.length - xSpan / 2 + rightEdgePaddingBars,
|
|
7644
|
+
xSpan,
|
|
7645
|
+
() => updateFollowLatest(true)
|
|
7646
|
+
);
|
|
7468
7647
|
} else {
|
|
7469
7648
|
updateFollowLatest(follow);
|
|
7470
7649
|
}
|
|
@@ -8747,6 +8926,7 @@ function createChart(element, options = {}) {
|
|
|
8747
8926
|
canvas.focus({ preventScroll: true });
|
|
8748
8927
|
}
|
|
8749
8928
|
cancelKineticPan();
|
|
8929
|
+
cancelViewportTween();
|
|
8750
8930
|
panVelocitySamples.length = 0;
|
|
8751
8931
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
8752
8932
|
shiftKeyActive = event.shiftKey;
|
|
@@ -9411,6 +9591,7 @@ function createChart(element, options = {}) {
|
|
|
9411
9591
|
return;
|
|
9412
9592
|
}
|
|
9413
9593
|
cancelKineticPan();
|
|
9594
|
+
cancelViewportTween();
|
|
9414
9595
|
const point = getCanvasPoint(event);
|
|
9415
9596
|
const region = getHitRegion(point.x, point.y);
|
|
9416
9597
|
if (region === "outside") {
|
|
@@ -9688,6 +9869,7 @@ function createChart(element, options = {}) {
|
|
|
9688
9869
|
resetRightMarginCache();
|
|
9689
9870
|
doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
9690
9871
|
doubleClickAction = mergedOptions.doubleClickAction;
|
|
9872
|
+
setIndicatorLiveThrottleMs(mergedOptions.indicatorUpdate?.liveThrottleMs ?? 80);
|
|
9691
9873
|
applyAccessibilityAttributes();
|
|
9692
9874
|
const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
|
|
9693
9875
|
if (!isTickerSmoothingEnabled) {
|
|
@@ -10166,6 +10348,7 @@ function createChart(element, options = {}) {
|
|
|
10166
10348
|
return canvas.toDataURL(options2.type ?? "image/png", options2.quality);
|
|
10167
10349
|
};
|
|
10168
10350
|
const destroy = () => {
|
|
10351
|
+
cancelViewportTween();
|
|
10169
10352
|
cancelLongPressTimer();
|
|
10170
10353
|
datafeedUnsubscribe?.();
|
|
10171
10354
|
datafeedUnsubscribe = null;
|
package/docs/API.md
CHANGED
|
@@ -568,6 +568,7 @@ an up measurement, red for down.
|
|
|
568
568
|
- `cancelDrawing(): boolean` — abort an in-progress multi-click drawing (between the first click and the final point, e.g. a half-drawn trendline/ray/fib). Returns `true` if a draft was cancelled. Wire it to Escape.
|
|
569
569
|
- Hold **Shift** while drawing or dragging an endpoint of a `trendline`/`ray`/`arrow` to constrain it to the nearest 0/45/90° angle (e.g. a perfectly horizontal line).
|
|
570
570
|
- `setMagnetMode(mode): void` / `getMagnetMode()` — magnet/snap for all drawing tools. `"none"` off, `"weak"` snaps to a candle's OHLC only when the cursor is near a value, `"strong"` always snaps to the nearest OHLC of the bar under the cursor. Holding Cmd/Ctrl while drawing/dragging forces strong snapping regardless of the mode.
|
|
571
|
+
- `setTheme(theme: ChartThemeName | ChartTheme): void` / `getTheme(): string | null` — apply a whole palette at once; see "Themes" below
|
|
571
572
|
- `setDatafeed(datafeed: ChartDatafeed | null): Promise<void>` — attach a pull-based data source with lazy history; see "Datafeed" below
|
|
572
573
|
- `onLongPress(handler: ((event: ChartLongPressEvent) => void) | null): void` — touch press-and-hold inspection
|
|
573
574
|
- `setCompareSeries(series: CompareSeriesOptions[]): void` / `addCompareSeries(series)` / `removeCompareSeries(id)` / `getCompareSeries()` — overlay other instruments on the main pane; see "Compare overlays" below
|
|
@@ -613,6 +614,97 @@ link.click();
|
|
|
613
614
|
|
|
614
615
|
---
|
|
615
616
|
|
|
617
|
+
## In-chart data line
|
|
618
|
+
|
|
619
|
+
The symbol/OHLC line TradingView draws in the top-left of the plot. Hosts have
|
|
620
|
+
been building this in DOM and re-rendering it on every crosshair move; drawn
|
|
621
|
+
in-canvas it just follows the frame, inherits the theme's colors, and costs
|
|
622
|
+
nothing per pointer move.
|
|
623
|
+
|
|
624
|
+
```ts
|
|
625
|
+
createChart(el, {
|
|
626
|
+
dataLine: {
|
|
627
|
+
visible: true,
|
|
628
|
+
symbol: "NQU6",
|
|
629
|
+
interval: "1h",
|
|
630
|
+
exchange: "CME",
|
|
631
|
+
showVolume: true,
|
|
632
|
+
statusColor: marketOpen ? "#22c55e" : "#ef4444",
|
|
633
|
+
details: [
|
|
634
|
+
{ label: "Session", value: "RTH" },
|
|
635
|
+
{ value: "Closed", color: "#fff", background: "rgba(242,54,69,0.85)" },
|
|
636
|
+
],
|
|
637
|
+
},
|
|
638
|
+
});
|
|
639
|
+
```
|
|
640
|
+
|
|
641
|
+
Values track the hovered bar and fall back to the latest one, the change is
|
|
642
|
+
colored by direction, and the indicator legend moves down to sit under it.
|
|
643
|
+
Detail chips are dropped rather than clipped when the row runs out of width.
|
|
644
|
+
Off by default so hosts with their own overlay don't suddenly render two.
|
|
645
|
+
|
|
646
|
+
## Viewport transitions
|
|
647
|
+
|
|
648
|
+
The discrete jumps — `resetViewport()`, `fitContent()`,
|
|
649
|
+
`setFollowingLatest(true)` and the `Home`/`End`/`R` shortcuts — ease into place
|
|
650
|
+
over `animation.durationMs` (default 260) instead of teleporting, so it stays
|
|
651
|
+
obvious which way the chart moved. Zoom interpolates geometrically, so each
|
|
652
|
+
frame changes scale by the same ratio rather than appearing to accelerate.
|
|
653
|
+
|
|
654
|
+
Continuous gestures (drag, wheel, pinch) are never animated — they already
|
|
655
|
+
track the input — and any pointer interaction cancels a transition in flight.
|
|
656
|
+
The whole thing is skipped when the OS asks for reduced motion, or with
|
|
657
|
+
`animation: { viewportTransitions: false }`.
|
|
658
|
+
|
|
659
|
+
## Indicator recomputation
|
|
660
|
+
|
|
661
|
+
Indicator series are memoised per (indicator, inputs, series fingerprint). A
|
|
662
|
+
live tick changes the fingerprint, which used to mean every indicator
|
|
663
|
+
recomputed its whole series on every animation frame — measured at 50k bars
|
|
664
|
+
with 10 indicators, that was ~11ms of the frame budget and dropped the chart to
|
|
665
|
+
51fps.
|
|
666
|
+
|
|
667
|
+
While *only the forming bar* is ticking, recomputation is now throttled to
|
|
668
|
+
`indicatorUpdate.liveThrottleMs` (default 80ms). The values are always exact —
|
|
669
|
+
this changes how often the live bar's output refreshes, never how it is
|
|
670
|
+
computed — and anything structural (a bar closing, history prepending, inputs
|
|
671
|
+
or length changing) recomputes immediately, so closed bars are never stale.
|
|
672
|
+
Same benchmark after: 8.4ms and 118fps. Set `liveThrottleMs: 0` to recompute on
|
|
673
|
+
every frame.
|
|
674
|
+
|
|
675
|
+
## Themes
|
|
676
|
+
|
|
677
|
+
A theme is the chart's whole palette as one object, so hosts stop setting a
|
|
678
|
+
dozen colors individually and re-pushing them on every light/dark toggle.
|
|
679
|
+
|
|
680
|
+
```ts
|
|
681
|
+
import { CHART_THEMES, createChart } from "hyperprop-charting-library";
|
|
682
|
+
|
|
683
|
+
chart.setTheme("light"); // built-in preset
|
|
684
|
+
chart.setTheme({ ...CHART_THEMES.dark, upColor: "#00c853" }); // tweak one
|
|
685
|
+
chart.getTheme(); // "light" | null when hand-set
|
|
686
|
+
```
|
|
687
|
+
|
|
688
|
+
Five presets ship in `CHART_THEMES`:
|
|
689
|
+
|
|
690
|
+
| Preset | Surface | Candles | For |
|
|
691
|
+
| --- | --- | --- | --- |
|
|
692
|
+
| `midnight` | `#171717` | `#22ab94` / `#f23645` | the default dark look |
|
|
693
|
+
| `light` | `#ffffff` | `#22ab94` / `#f23645` | same candles on a white surface |
|
|
694
|
+
| `brand` | `#0b1220` | `#22ab94` / `#f23645` | Hyperprop blue accents (`#253cff`) |
|
|
695
|
+
| `dark` | `#0f0f0f` | `#089981` / `#f23645` | neutral near-black, no tint |
|
|
696
|
+
| `high-contrast` | `#000000` | `#00e5a0` / `#ff4d5e` | projectors, low vision |
|
|
697
|
+
|
|
698
|
+
`setTheme` covers background, candles, the line/area series, grid (color and
|
|
699
|
+
opacity), both axes, the crosshair and its labels, the watermark, and
|
|
700
|
+
indicator legend text. It deliberately does **not** touch order-line colors or
|
|
701
|
+
drawing defaults — those are trading state, not chrome, so switching looks
|
|
702
|
+
never changes what your orders look like mid-session.
|
|
703
|
+
|
|
704
|
+
`getTheme()` returns the active preset name, or `null` once any themed color
|
|
705
|
+
is set by hand. Unrelated `updateOptions` calls (label toggles, sizing, ticker
|
|
706
|
+
line) don't clear it, so it stays a reliable "am I on a preset" signal.
|
|
707
|
+
|
|
616
708
|
## Datafeed (optional, pull-based)
|
|
617
709
|
|
|
618
710
|
By default the host pushes bars in with `setData`/`upsertBar`. Attach a
|