hyperprop-charting-library 0.1.120 → 0.1.121
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 +612 -347
- package/dist/hyperprop-charting-library.d.ts +25 -0
- package/dist/hyperprop-charting-library.js +612 -347
- package/dist/index.cjs +612 -347
- package/dist/index.d.cts +25 -0
- package/dist/index.d.ts +25 -0
- package/dist/index.js +612 -347
- package/package.json +1 -1
|
@@ -189,8 +189,12 @@ var DEFAULT_OPTIONS = {
|
|
|
189
189
|
tickSize: 0,
|
|
190
190
|
candleColorMode: "openClose",
|
|
191
191
|
candleColorEpsilon: -1,
|
|
192
|
-
|
|
192
|
+
// 0 = deterministic scale (lightweight-charts behavior): the y-range is a
|
|
193
|
+
// pure function of the visible data, so it can never drift or "breathe".
|
|
194
|
+
// Values > 0 re-enable eased contraction for hosts that prefer it.
|
|
195
|
+
autoScaleSmoothing: 0,
|
|
193
196
|
autoScaleIgnoreLatestCandle: true,
|
|
197
|
+
kineticScroll: { touch: true, mouse: false },
|
|
194
198
|
pinOutOfRangeLines: false,
|
|
195
199
|
doubleClickEnabled: true,
|
|
196
200
|
doubleClickAction: "reset",
|
|
@@ -589,6 +593,26 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
589
593
|
}
|
|
590
594
|
ctx.restore();
|
|
591
595
|
};
|
|
596
|
+
var rangeOfSeries = (seriesList, startIndex, endIndex, skipIndex) => {
|
|
597
|
+
let min = Number.POSITIVE_INFINITY;
|
|
598
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
599
|
+
for (const series of seriesList) {
|
|
600
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
601
|
+
if (idx === skipIndex) continue;
|
|
602
|
+
const value = series[idx];
|
|
603
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
604
|
+
if (value < min) min = value;
|
|
605
|
+
if (value > max) max = value;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return min <= max ? { min, max } : null;
|
|
609
|
+
};
|
|
610
|
+
var maAutoscaleRange = (cacheKeyPrefix, compute, defaultLength) => (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
611
|
+
const length = clampIndicatorLength(inputs.length, defaultLength);
|
|
612
|
+
const source = inputs.source ?? "close";
|
|
613
|
+
const series = withCachedSeries(`${cacheKeyPrefix}|${length}|${source}`, data, () => compute(data, length, source));
|
|
614
|
+
return rangeOfSeries([series], startIndex, endIndex, skipIndex);
|
|
615
|
+
};
|
|
592
616
|
var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
|
|
593
617
|
if (!renderContext.yFromPrice || opacity <= 0) return;
|
|
594
618
|
const yFromPrice = renderContext.yFromPrice;
|
|
@@ -806,7 +830,8 @@ var BUILTIN_SMA_INDICATOR = {
|
|
|
806
830
|
() => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
807
831
|
);
|
|
808
832
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
|
|
809
|
-
}
|
|
833
|
+
},
|
|
834
|
+
getAutoscaleRange: maAutoscaleRange("sma", computeSmaSeries, 20)
|
|
810
835
|
};
|
|
811
836
|
var BUILTIN_EMA_INDICATOR = {
|
|
812
837
|
id: "ema",
|
|
@@ -821,7 +846,8 @@ var BUILTIN_EMA_INDICATOR = {
|
|
|
821
846
|
() => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
822
847
|
);
|
|
823
848
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
824
|
-
}
|
|
849
|
+
},
|
|
850
|
+
getAutoscaleRange: maAutoscaleRange("ema", computeEmaSeries, 20)
|
|
825
851
|
};
|
|
826
852
|
var BUILTIN_WMA_INDICATOR = {
|
|
827
853
|
id: "wma",
|
|
@@ -836,7 +862,8 @@ var BUILTIN_WMA_INDICATOR = {
|
|
|
836
862
|
() => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
837
863
|
);
|
|
838
864
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
|
|
839
|
-
}
|
|
865
|
+
},
|
|
866
|
+
getAutoscaleRange: maAutoscaleRange("wma", computeWmaSeries, 20)
|
|
840
867
|
};
|
|
841
868
|
var BUILTIN_VWMA_INDICATOR = {
|
|
842
869
|
id: "vwma",
|
|
@@ -851,7 +878,8 @@ var BUILTIN_VWMA_INDICATOR = {
|
|
|
851
878
|
() => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
852
879
|
);
|
|
853
880
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
|
|
854
|
-
}
|
|
881
|
+
},
|
|
882
|
+
getAutoscaleRange: maAutoscaleRange("vwma", computeVwmaSeries, 20)
|
|
855
883
|
};
|
|
856
884
|
var BUILTIN_RMA_INDICATOR = {
|
|
857
885
|
id: "rma",
|
|
@@ -866,7 +894,8 @@ var BUILTIN_RMA_INDICATOR = {
|
|
|
866
894
|
() => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
867
895
|
);
|
|
868
896
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
|
|
869
|
-
}
|
|
897
|
+
},
|
|
898
|
+
getAutoscaleRange: maAutoscaleRange("rma", computeRmaSeries, 14)
|
|
870
899
|
};
|
|
871
900
|
var BUILTIN_HMA_INDICATOR = {
|
|
872
901
|
id: "hma",
|
|
@@ -881,7 +910,8 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
881
910
|
() => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
882
911
|
);
|
|
883
912
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
884
|
-
}
|
|
913
|
+
},
|
|
914
|
+
getAutoscaleRange: maAutoscaleRange("hma", computeHmaSeries, 21)
|
|
885
915
|
};
|
|
886
916
|
var BUILTIN_VWAP_INDICATOR = {
|
|
887
917
|
id: "vwap",
|
|
@@ -922,6 +952,27 @@ var BUILTIN_VWAP_INDICATOR = {
|
|
|
922
952
|
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
923
953
|
}
|
|
924
954
|
drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
955
|
+
},
|
|
956
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
957
|
+
const vwap = withCachedSeries("vwap|line", data, () => computeVwapSeries(data, "vwap"));
|
|
958
|
+
if (!inputs.showBands) {
|
|
959
|
+
return rangeOfSeries([vwap], startIndex, endIndex, skipIndex);
|
|
960
|
+
}
|
|
961
|
+
const sigma = withCachedSeries("vwap|sigma", data, () => computeVwapSeries(data, "sigma"));
|
|
962
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
963
|
+
let min = Number.POSITIVE_INFINITY;
|
|
964
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
965
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
966
|
+
if (idx === skipIndex) continue;
|
|
967
|
+
const value = vwap[idx];
|
|
968
|
+
const s = sigma[idx];
|
|
969
|
+
if (value == null || s == null) continue;
|
|
970
|
+
const lo = value - multiplier * s;
|
|
971
|
+
const hi = value + multiplier * s;
|
|
972
|
+
if (lo < min) min = lo;
|
|
973
|
+
if (hi > max) max = hi;
|
|
974
|
+
}
|
|
975
|
+
return min <= max ? { min, max } : null;
|
|
925
976
|
}
|
|
926
977
|
};
|
|
927
978
|
var BUILTIN_BOLLINGER_INDICATOR = {
|
|
@@ -963,7 +1014,23 @@ var BUILTIN_BOLLINGER_INDICATOR = {
|
|
|
963
1014
|
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
|
|
964
1015
|
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
965
1016
|
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
966
|
-
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#
|
|
1017
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#ec4899", width);
|
|
1018
|
+
},
|
|
1019
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
1020
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
1021
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
1022
|
+
const source = inputs.source ?? "close";
|
|
1023
|
+
const upper = withCachedSeries(
|
|
1024
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
1025
|
+
data,
|
|
1026
|
+
() => computeBollingerSeries(data, length, multiplier, source, "upper")
|
|
1027
|
+
);
|
|
1028
|
+
const lower = withCachedSeries(
|
|
1029
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
1030
|
+
data,
|
|
1031
|
+
() => computeBollingerSeries(data, length, multiplier, source, "lower")
|
|
1032
|
+
);
|
|
1033
|
+
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
967
1034
|
}
|
|
968
1035
|
};
|
|
969
1036
|
var BUILTIN_STDDEV_INDICATOR = {
|
|
@@ -1056,6 +1123,20 @@ var BUILTIN_INDICATORS = [
|
|
|
1056
1123
|
];
|
|
1057
1124
|
function createChart(element, options = {}) {
|
|
1058
1125
|
let mergedOptions = mergeChartOptions(DEFAULT_OPTIONS, options);
|
|
1126
|
+
let resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1127
|
+
let resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1128
|
+
let resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1129
|
+
let resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1130
|
+
let resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
|
|
1131
|
+
let resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1132
|
+
const refreshResolvedOptions = () => {
|
|
1133
|
+
resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1134
|
+
resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1135
|
+
resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1136
|
+
resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1137
|
+
resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
|
|
1138
|
+
resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1139
|
+
};
|
|
1059
1140
|
let width = mergedOptions.width;
|
|
1060
1141
|
let height = mergedOptions.height;
|
|
1061
1142
|
let data = [];
|
|
@@ -1092,8 +1173,11 @@ function createChart(element, options = {}) {
|
|
|
1092
1173
|
pane: indicator.pane ?? plugin?.pane ?? "overlay",
|
|
1093
1174
|
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
1094
1175
|
zIndex: Math.round(Number(indicator.zIndex) || 0),
|
|
1095
|
-
|
|
1096
|
-
|
|
1176
|
+
// Overlay series participate in autoscale by default (lightweight-charts
|
|
1177
|
+
// semantics): bands and MAs expand the price range instead of clipping
|
|
1178
|
+
// at the chart edge. Hosts can still opt out per indicator.
|
|
1179
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale ?? false,
|
|
1180
|
+
overlayScaleWeight: indicator.overlayScaleWeight === void 0 ? 1 : Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0)),
|
|
1097
1181
|
inputs: {
|
|
1098
1182
|
...defaults,
|
|
1099
1183
|
...indicator.inputs ?? {}
|
|
@@ -1295,6 +1379,9 @@ function createChart(element, options = {}) {
|
|
|
1295
1379
|
canvas.setAttribute("draggable", "false");
|
|
1296
1380
|
element.innerHTML = "";
|
|
1297
1381
|
element.appendChild(canvas);
|
|
1382
|
+
const baseCanvas = document.createElement("canvas");
|
|
1383
|
+
const baseCtx = baseCanvas.getContext("2d");
|
|
1384
|
+
let overlayLayout = null;
|
|
1298
1385
|
const margin = { top: 16, right: 72, bottom: 26, left: 12 };
|
|
1299
1386
|
let cachedRightMargin = margin.right;
|
|
1300
1387
|
const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
|
|
@@ -1334,6 +1421,20 @@ function createChart(element, options = {}) {
|
|
|
1334
1421
|
const clamp = (value, min, max) => {
|
|
1335
1422
|
return Math.min(max, Math.max(min, value));
|
|
1336
1423
|
};
|
|
1424
|
+
const textWidthCache = /* @__PURE__ */ new Map();
|
|
1425
|
+
const measureTextWidth = (text) => {
|
|
1426
|
+
const key = `${ctx.font}\0${text}`;
|
|
1427
|
+
const cached = textWidthCache.get(key);
|
|
1428
|
+
if (cached !== void 0) {
|
|
1429
|
+
return cached;
|
|
1430
|
+
}
|
|
1431
|
+
const width2 = ctx.measureText(text).width;
|
|
1432
|
+
if (textWidthCache.size >= 4096) {
|
|
1433
|
+
textWidthCache.clear();
|
|
1434
|
+
}
|
|
1435
|
+
textWidthCache.set(key, width2);
|
|
1436
|
+
return width2;
|
|
1437
|
+
};
|
|
1337
1438
|
const resetLabelSlots = (enabled) => {
|
|
1338
1439
|
noOverlappingLineLabels = enabled;
|
|
1339
1440
|
rightAxisLabelSlots = [];
|
|
@@ -1536,7 +1637,7 @@ function createChart(element, options = {}) {
|
|
|
1536
1637
|
return `${integerPart}${decimalPart}`;
|
|
1537
1638
|
};
|
|
1538
1639
|
const getMeasuredLabelWidth = (text, paddingX) => {
|
|
1539
|
-
return Math.ceil(
|
|
1640
|
+
return Math.ceil(measureTextWidth(text)) + paddingX * 2;
|
|
1540
1641
|
};
|
|
1541
1642
|
const getStabilizedNumericLabelWidth = (text, paddingX) => {
|
|
1542
1643
|
const measured = getMeasuredLabelWidth(text, paddingX);
|
|
@@ -1777,11 +1878,11 @@ function createChart(element, options = {}) {
|
|
|
1777
1878
|
return;
|
|
1778
1879
|
}
|
|
1779
1880
|
const labelText = mergedLine.label ?? formatPrice(mergedLine.price);
|
|
1780
|
-
const axis =
|
|
1881
|
+
const axis = resolvedAxis;
|
|
1781
1882
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1782
1883
|
const labelPaddingX = 8;
|
|
1783
1884
|
const labelHeight = 20;
|
|
1784
|
-
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(
|
|
1885
|
+
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(measureTextWidth(labelText)) + labelPaddingX * 2;
|
|
1785
1886
|
const labelWidth = getRightAxisLabelWidth(chartRight, measuredLabelWidth);
|
|
1786
1887
|
const labelX = getRightAxisLabelX(chartRight);
|
|
1787
1888
|
const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
|
|
@@ -1876,7 +1977,7 @@ function createChart(element, options = {}) {
|
|
|
1876
1977
|
const closeAction = mergedLine.type === "market" ? "close" : "cancel";
|
|
1877
1978
|
const showCloseButton = mergedLine.showCloseButton;
|
|
1878
1979
|
const actionButtonText = mergedLine.actionButtonText;
|
|
1879
|
-
const axis =
|
|
1980
|
+
const axis = resolvedAxis;
|
|
1880
1981
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1881
1982
|
const legacyButton = actionButtonText ? [
|
|
1882
1983
|
{
|
|
@@ -1898,7 +1999,7 @@ function createChart(element, options = {}) {
|
|
|
1898
1999
|
const actionButtonMetrics = actionButtons.map((button) => {
|
|
1899
2000
|
const paddingX = Math.max(2, button.paddingX ?? mergedLine.actionButtonPaddingX);
|
|
1900
2001
|
const minWidth = Math.max(16, button.minWidth ?? mergedLine.actionButtonMinWidth);
|
|
1901
|
-
const width2 = Math.max(minWidth, Math.ceil(
|
|
2002
|
+
const width2 = Math.max(minWidth, Math.ceil(measureTextWidth(button.text)) + paddingX * 2);
|
|
1902
2003
|
return { button, width: width2 };
|
|
1903
2004
|
});
|
|
1904
2005
|
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
@@ -1906,8 +2007,8 @@ function createChart(element, options = {}) {
|
|
|
1906
2007
|
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
1907
2008
|
const segmentPaddingX = 8;
|
|
1908
2009
|
const labelHeight = 22;
|
|
1909
|
-
const qtyWidth = qtyText ? Math.ceil(
|
|
1910
|
-
const centerMeasuredWidth = Math.ceil(
|
|
2010
|
+
const qtyWidth = qtyText ? Math.ceil(measureTextWidth(qtyText)) + segmentPaddingX * 2 : 0;
|
|
2011
|
+
const centerMeasuredWidth = Math.ceil(measureTextWidth(centerText)) + segmentPaddingX * 2;
|
|
1911
2012
|
const centerWidth = mergedLine.id === void 0 ? centerMeasuredWidth : Math.max(centerMeasuredWidth, orderWidgetWidthById.get(mergedLine.id) ?? 0);
|
|
1912
2013
|
if (mergedLine.id) {
|
|
1913
2014
|
orderWidgetWidthById.set(mergedLine.id, centerWidth);
|
|
@@ -1915,7 +2016,7 @@ function createChart(element, options = {}) {
|
|
|
1915
2016
|
const closeWidth = showCloseButton ? 24 : 0;
|
|
1916
2017
|
const mainWidgetWidth = qtyWidth + centerWidth + closeWidth;
|
|
1917
2018
|
const totalWidth = mainWidgetWidth + actionButtonsGap + actionButtonsTotalWidth;
|
|
1918
|
-
const crosshair =
|
|
2019
|
+
const crosshair = resolvedCrosshair;
|
|
1919
2020
|
const crosshairActionInset = crosshair.visible && crosshair.showPriceLabel && crosshair.showPriceActionButton ? Math.max(
|
|
1920
2021
|
0,
|
|
1921
2022
|
Math.round(crosshair.priceActionButtonGap) + (clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4)) + 10) - 4
|
|
@@ -2096,20 +2197,29 @@ function createChart(element, options = {}) {
|
|
|
2096
2197
|
}
|
|
2097
2198
|
}
|
|
2098
2199
|
crosshairPoint = point;
|
|
2099
|
-
scheduleDraw();
|
|
2200
|
+
scheduleDraw({ overlayOnly: true });
|
|
2100
2201
|
};
|
|
2101
2202
|
let drawRafId = null;
|
|
2102
2203
|
let pendingDrawUpdateAutoScale = false;
|
|
2204
|
+
let pendingDrawOverlayOnly = true;
|
|
2103
2205
|
const scheduleDraw = (options2 = {}) => {
|
|
2104
|
-
|
|
2206
|
+
const overlayOnly = options2.overlayOnly ?? false;
|
|
2207
|
+
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
2208
|
+
pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
|
|
2105
2209
|
if (drawRafId !== null) {
|
|
2106
2210
|
return;
|
|
2107
2211
|
}
|
|
2108
2212
|
drawRafId = requestAnimationFrame(() => {
|
|
2109
2213
|
drawRafId = null;
|
|
2110
2214
|
const updateAutoScale = pendingDrawUpdateAutoScale;
|
|
2215
|
+
const flushOverlayOnly = pendingDrawOverlayOnly;
|
|
2111
2216
|
pendingDrawUpdateAutoScale = false;
|
|
2112
|
-
|
|
2217
|
+
pendingDrawOverlayOnly = true;
|
|
2218
|
+
if (flushOverlayOnly) {
|
|
2219
|
+
drawOverlayOnly();
|
|
2220
|
+
} else {
|
|
2221
|
+
draw({ updateAutoScale });
|
|
2222
|
+
}
|
|
2113
2223
|
});
|
|
2114
2224
|
};
|
|
2115
2225
|
const draw = (options2 = {}) => {
|
|
@@ -2123,15 +2233,15 @@ function createChart(element, options = {}) {
|
|
|
2123
2233
|
canvas.width = Math.floor(width * pixelRatio);
|
|
2124
2234
|
canvas.height = Math.floor(height * pixelRatio);
|
|
2125
2235
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
2126
|
-
const axis =
|
|
2127
|
-
const xAxis =
|
|
2128
|
-
const yAxis =
|
|
2236
|
+
const axis = resolvedAxis;
|
|
2237
|
+
const xAxis = resolvedXAxis;
|
|
2238
|
+
const yAxis = resolvedYAxis;
|
|
2129
2239
|
const xAxisFontSize = Math.max(8, xAxis.fontSize);
|
|
2130
2240
|
const yAxisFontSize = Math.max(8, yAxis.fontSize);
|
|
2131
2241
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2132
2242
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2133
2243
|
ctx.fillRect(0, 0, width, height);
|
|
2134
|
-
const labels =
|
|
2244
|
+
const labels = resolvedLabels;
|
|
2135
2245
|
const estimateRightMargin = () => {
|
|
2136
2246
|
const paddingX = Math.max(4, labels.labelPaddingX);
|
|
2137
2247
|
let required = margin.right - 1;
|
|
@@ -2244,6 +2354,7 @@ function createChart(element, options = {}) {
|
|
|
2244
2354
|
yMax: 0
|
|
2245
2355
|
};
|
|
2246
2356
|
drawText("Load OHLC data to render chart", chartLeft + 10, chartTop + 20, "left", "middle", "#64748b");
|
|
2357
|
+
overlayLayout = null;
|
|
2247
2358
|
return;
|
|
2248
2359
|
}
|
|
2249
2360
|
clampXViewport();
|
|
@@ -2251,62 +2362,39 @@ function createChart(element, options = {}) {
|
|
|
2251
2362
|
const xEnd = xStart + xSpan;
|
|
2252
2363
|
const startIndex = Math.max(0, Math.floor(xStart));
|
|
2253
2364
|
const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
|
|
2254
|
-
const
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
const fallback = data.slice(fallbackStart, latestIndex);
|
|
2265
|
-
if (fallback.length > 0) {
|
|
2266
|
-
priceSource = fallback;
|
|
2267
|
-
}
|
|
2365
|
+
const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
|
|
2366
|
+
const scanCandleRange = (from, to) => {
|
|
2367
|
+
let min = Number.POSITIVE_INFINITY;
|
|
2368
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
2369
|
+
for (let idx = from; idx <= to; idx += 1) {
|
|
2370
|
+
if (idx === skipLatestIndex) continue;
|
|
2371
|
+
const point = data[idx];
|
|
2372
|
+
if (!point) continue;
|
|
2373
|
+
if (point.l < min) min = point.l;
|
|
2374
|
+
if (point.h > max) max = point.h;
|
|
2268
2375
|
}
|
|
2376
|
+
return min <= max ? { min, max } : null;
|
|
2377
|
+
};
|
|
2378
|
+
let candleRange = scanCandleRange(startIndex, endIndex);
|
|
2379
|
+
if (!candleRange) {
|
|
2380
|
+
const latestIndex = data.length - 1;
|
|
2381
|
+
candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? { min: data[latestIndex].l, max: data[latestIndex].h };
|
|
2269
2382
|
}
|
|
2270
|
-
let minPrice =
|
|
2271
|
-
let maxPrice =
|
|
2383
|
+
let minPrice = candleRange.min;
|
|
2384
|
+
let maxPrice = candleRange.max;
|
|
2272
2385
|
if (overlayIndicatorsForScale.length > 0) {
|
|
2273
|
-
for (const { indicator } of overlayIndicatorsForScale) {
|
|
2386
|
+
for (const { indicator, plugin } of overlayIndicatorsForScale) {
|
|
2274
2387
|
if (indicator.excludeFromAutoscale) {
|
|
2275
2388
|
continue;
|
|
2276
2389
|
}
|
|
2277
|
-
const
|
|
2278
|
-
|
|
2279
|
-
const source = inputs.source ?? "close";
|
|
2280
|
-
const length = clampIndicatorLength(inputs.length ?? 14, 14);
|
|
2281
|
-
let series = null;
|
|
2282
|
-
if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
|
|
2283
|
-
if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
|
|
2284
|
-
if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
|
|
2285
|
-
if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
|
|
2286
|
-
if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
|
|
2287
|
-
if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
|
|
2288
|
-
if (!series) {
|
|
2289
|
-
continue;
|
|
2290
|
-
}
|
|
2291
|
-
const visibleValues = [];
|
|
2292
|
-
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
2293
|
-
if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 && idx === data.length - 1) {
|
|
2294
|
-
continue;
|
|
2295
|
-
}
|
|
2296
|
-
const value = series[idx];
|
|
2297
|
-
if (Number.isFinite(value ?? Number.NaN)) {
|
|
2298
|
-
visibleValues.push(value);
|
|
2299
|
-
}
|
|
2300
|
-
}
|
|
2301
|
-
if (visibleValues.length === 0) {
|
|
2390
|
+
const range = plugin.getAutoscaleRange?.(data, startIndex, endIndex, indicator.inputs, skipLatestIndex);
|
|
2391
|
+
if (!range || !Number.isFinite(range.min) || !Number.isFinite(range.max)) {
|
|
2302
2392
|
continue;
|
|
2303
2393
|
}
|
|
2304
|
-
const seriesMin = Math.min(...visibleValues);
|
|
2305
|
-
const seriesMax = Math.max(...visibleValues);
|
|
2306
2394
|
const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
|
|
2307
2395
|
const currentMid = (minPrice + maxPrice) / 2;
|
|
2308
|
-
const weightedMin = currentMid + (
|
|
2309
|
-
const weightedMax = currentMid + (
|
|
2396
|
+
const weightedMin = currentMid + (range.min - currentMid) * weight;
|
|
2397
|
+
const weightedMax = currentMid + (range.max - currentMid) * weight;
|
|
2310
2398
|
minPrice = Math.min(minPrice, weightedMin);
|
|
2311
2399
|
maxPrice = Math.max(maxPrice, weightedMax);
|
|
2312
2400
|
}
|
|
@@ -2381,7 +2469,7 @@ function createChart(element, options = {}) {
|
|
|
2381
2469
|
ctx.save();
|
|
2382
2470
|
const prevFont = ctx.font;
|
|
2383
2471
|
ctx.font = `500 12px ${mergedOptions.fontFamily}`;
|
|
2384
|
-
const textWidth =
|
|
2472
|
+
const textWidth = measureTextWidth(labelText);
|
|
2385
2473
|
const paddingX = 6;
|
|
2386
2474
|
const bgWidth = textWidth + paddingX * 2;
|
|
2387
2475
|
const bgHeight = 18;
|
|
@@ -2547,7 +2635,7 @@ function createChart(element, options = {}) {
|
|
|
2547
2635
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2548
2636
|
levelLines.forEach((level, index) => {
|
|
2549
2637
|
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2550
|
-
const textWidth =
|
|
2638
|
+
const textWidth = measureTextWidth(labelText);
|
|
2551
2639
|
const padding = 4;
|
|
2552
2640
|
const bgX = lineLeft + 4;
|
|
2553
2641
|
const bgY = level.y - 9;
|
|
@@ -2620,7 +2708,7 @@ function createChart(element, options = {}) {
|
|
|
2620
2708
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2621
2709
|
levelLines.forEach((level, index) => {
|
|
2622
2710
|
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2623
|
-
const textWidth =
|
|
2711
|
+
const textWidth = measureTextWidth(labelText);
|
|
2624
2712
|
const padding = 4;
|
|
2625
2713
|
const bgX = chartLeft + 4;
|
|
2626
2714
|
const bgY = level.y - 9;
|
|
@@ -2763,8 +2851,8 @@ function createChart(element, options = {}) {
|
|
|
2763
2851
|
const padding = 6;
|
|
2764
2852
|
const lineH = 14;
|
|
2765
2853
|
const isDigit = (ch) => ch >= "0" && ch <= "9";
|
|
2766
|
-
const digitW =
|
|
2767
|
-
const charAdvance = (ch) => isDigit(ch) ? digitW :
|
|
2854
|
+
const digitW = measureTextWidth("0");
|
|
2855
|
+
const charAdvance = (ch) => isDigit(ch) ? digitW : measureTextWidth(ch);
|
|
2768
2856
|
const lineWidth = (line) => {
|
|
2769
2857
|
let w = 0;
|
|
2770
2858
|
for (const ch of line) w += charAdvance(ch);
|
|
@@ -2787,11 +2875,11 @@ function createChart(element, options = {}) {
|
|
|
2787
2875
|
const y = startY + lineIndex * lineH;
|
|
2788
2876
|
for (const ch of line) {
|
|
2789
2877
|
if (isDigit(ch)) {
|
|
2790
|
-
ctx.fillText(ch, x + (digitW -
|
|
2878
|
+
ctx.fillText(ch, x + (digitW - measureTextWidth(ch)) / 2, y);
|
|
2791
2879
|
x += digitW;
|
|
2792
2880
|
} else {
|
|
2793
2881
|
ctx.fillText(ch, x, y);
|
|
2794
|
-
x +=
|
|
2882
|
+
x += measureTextWidth(ch);
|
|
2795
2883
|
}
|
|
2796
2884
|
}
|
|
2797
2885
|
});
|
|
@@ -2923,7 +3011,7 @@ function createChart(element, options = {}) {
|
|
|
2923
3011
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2924
3012
|
const padding = 6;
|
|
2925
3013
|
const lineHeight = 15;
|
|
2926
|
-
const textW = Math.max(...labelLines.map((line) =>
|
|
3014
|
+
const textW = Math.max(...labelLines.map((line) => measureTextWidth(line)));
|
|
2927
3015
|
const pillW = textW + padding * 2;
|
|
2928
3016
|
const pillH = labelLines.length * lineHeight + padding;
|
|
2929
3017
|
const pillX = midX - pillW / 2;
|
|
@@ -2959,7 +3047,7 @@ function createChart(element, options = {}) {
|
|
|
2959
3047
|
ctx.textBaseline = "middle";
|
|
2960
3048
|
const lines = (drawing.label ?? "").split("\n");
|
|
2961
3049
|
const lineH = Math.round(fontSize * 1.35);
|
|
2962
|
-
const textW = Math.max(1, ...lines.map((line) =>
|
|
3050
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
2963
3051
|
const padX = isNote ? 8 : 2;
|
|
2964
3052
|
const padY = isNote ? 6 : 1;
|
|
2965
3053
|
const blockW = textW + padX * 2;
|
|
@@ -3041,47 +3129,114 @@ function createChart(element, options = {}) {
|
|
|
3041
3129
|
Math.max(1, candleSpacing - 1),
|
|
3042
3130
|
Math.max(candleMinWidth, Math.floor(candleSpacing * candleBodyWidthRatio))
|
|
3043
3131
|
);
|
|
3044
|
-
const grid =
|
|
3132
|
+
const grid = resolvedGrid;
|
|
3045
3133
|
const gridOpacity = clamp(grid.opacity, 0, 1);
|
|
3046
3134
|
const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
|
|
3047
3135
|
const yTicks = Math.max(1, Math.floor(yTickCountInput));
|
|
3048
3136
|
const gridColor = grid.color ?? mergedOptions.gridColor;
|
|
3137
|
+
const priceTicks = [];
|
|
3138
|
+
{
|
|
3139
|
+
const targetStep = yRange / Math.max(1, yTicks);
|
|
3140
|
+
let step;
|
|
3141
|
+
if (targetStep > 0 && Number.isFinite(targetStep)) {
|
|
3142
|
+
const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
|
|
3143
|
+
const frac = targetStep / base;
|
|
3144
|
+
step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
|
|
3145
|
+
const instrumentTick = getConfiguredTickSize();
|
|
3146
|
+
if (instrumentTick > 0 && step < instrumentTick * 1e6) {
|
|
3147
|
+
step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
|
|
3148
|
+
}
|
|
3149
|
+
} else {
|
|
3150
|
+
step = 1;
|
|
3151
|
+
}
|
|
3152
|
+
const firstTick = Math.ceil(yMin / step) * step;
|
|
3153
|
+
for (let i = 0; ; i += 1) {
|
|
3154
|
+
const price = firstTick + i * step;
|
|
3155
|
+
if (price > yMax + step * 1e-6) break;
|
|
3156
|
+
priceTicks.push(price);
|
|
3157
|
+
if (priceTicks.length > 200) break;
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3049
3160
|
if (grid.horizontalLines) {
|
|
3050
3161
|
ctx.save();
|
|
3051
3162
|
ctx.globalAlpha = gridOpacity;
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3163
|
+
ctx.strokeStyle = gridColor;
|
|
3164
|
+
ctx.lineWidth = 1;
|
|
3165
|
+
ctx.beginPath();
|
|
3166
|
+
for (const price of priceTicks) {
|
|
3055
3167
|
const y = yFromPrice(price);
|
|
3056
|
-
ctx.strokeStyle = gridColor;
|
|
3057
|
-
ctx.lineWidth = 1;
|
|
3058
|
-
ctx.beginPath();
|
|
3059
3168
|
ctx.moveTo(crisp(chartLeft), crisp(y));
|
|
3060
3169
|
ctx.lineTo(crisp(chartRight), crisp(y));
|
|
3061
|
-
ctx.stroke();
|
|
3062
3170
|
}
|
|
3171
|
+
ctx.stroke();
|
|
3063
3172
|
ctx.restore();
|
|
3064
3173
|
}
|
|
3065
|
-
const minLabelSpacingPx = Math.max(
|
|
3066
|
-
const
|
|
3067
|
-
const xTickCountInput = grid.xTickCount ?? autoLabelCount;
|
|
3068
|
-
const xTickCount = Math.max(2, Math.floor(xTickCountInput));
|
|
3069
|
-
const rawStep = xSpan / xTickCount;
|
|
3070
|
-
const xStep = Math.max(1, Math.ceil(rawStep));
|
|
3071
|
-
const visibleTickStart = Math.floor(xStart);
|
|
3174
|
+
const minLabelSpacingPx = Math.max(60, Math.min(120, chartWidth / Math.max(2, Math.floor(grid.xTickCount ?? 8))));
|
|
3175
|
+
const visibleTickStart = Math.max(0, Math.floor(xStart));
|
|
3072
3176
|
const visibleTickEnd = Math.ceil(xEnd) - 1;
|
|
3073
|
-
const
|
|
3074
|
-
|
|
3177
|
+
const timeTicks = [];
|
|
3178
|
+
{
|
|
3179
|
+
const weightBuckets = [[], [], [], [], [], [], [], [], [], []];
|
|
3180
|
+
let prevTime = visibleTickStart > 0 ? getTimeForIndex(visibleTickStart - 1) : null;
|
|
3181
|
+
for (let index = visibleTickStart; index <= visibleTickEnd; index += 1) {
|
|
3182
|
+
const time = getTimeForIndex(index);
|
|
3183
|
+
if (!time) {
|
|
3184
|
+
prevTime = null;
|
|
3185
|
+
continue;
|
|
3186
|
+
}
|
|
3187
|
+
let weight = 0;
|
|
3188
|
+
if (!prevTime) {
|
|
3189
|
+
weight = 7;
|
|
3190
|
+
} else {
|
|
3191
|
+
if (prevTime.getFullYear() !== time.getFullYear()) weight = 9;
|
|
3192
|
+
else if (prevTime.getMonth() !== time.getMonth()) weight = 8;
|
|
3193
|
+
else if (prevTime.getDate() !== time.getDate()) weight = 7;
|
|
3194
|
+
else {
|
|
3195
|
+
const prevMinutes = prevTime.getHours() * 60 + prevTime.getMinutes();
|
|
3196
|
+
const curMinutes = time.getHours() * 60 + time.getMinutes();
|
|
3197
|
+
const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
|
|
3198
|
+
if (crossed(360)) weight = 6;
|
|
3199
|
+
else if (crossed(60)) weight = 5;
|
|
3200
|
+
else if (crossed(30)) weight = 4;
|
|
3201
|
+
else if (crossed(15)) weight = 3;
|
|
3202
|
+
else if (crossed(5)) weight = 2;
|
|
3203
|
+
else weight = 1;
|
|
3204
|
+
}
|
|
3205
|
+
}
|
|
3206
|
+
weightBuckets[weight].push(index);
|
|
3207
|
+
prevTime = time;
|
|
3208
|
+
}
|
|
3209
|
+
const placedXs = [];
|
|
3210
|
+
const maxMarks = Math.floor(chartWidth / minLabelSpacingPx) + 2;
|
|
3211
|
+
for (let weight = 9; weight >= 1 && timeTicks.length < maxMarks; weight -= 1) {
|
|
3212
|
+
for (const index of weightBuckets[weight]) {
|
|
3213
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3214
|
+
let fits = true;
|
|
3215
|
+
for (const placedX of placedXs) {
|
|
3216
|
+
if (Math.abs(placedX - x) < minLabelSpacingPx) {
|
|
3217
|
+
fits = false;
|
|
3218
|
+
break;
|
|
3219
|
+
}
|
|
3220
|
+
}
|
|
3221
|
+
if (!fits) continue;
|
|
3222
|
+
placedXs.push(x);
|
|
3223
|
+
timeTicks.push({ index, weight, x });
|
|
3224
|
+
if (timeTicks.length >= maxMarks) break;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
timeTicks.sort((a, b) => a.index - b.index);
|
|
3228
|
+
}
|
|
3229
|
+
if (grid.verticalLines && timeTicks.length > 0) {
|
|
3075
3230
|
ctx.save();
|
|
3076
3231
|
ctx.globalAlpha = gridOpacity;
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
3082
|
-
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
3083
|
-
ctx.stroke();
|
|
3232
|
+
ctx.strokeStyle = gridColor;
|
|
3233
|
+
ctx.lineWidth = 1;
|
|
3234
|
+
ctx.beginPath();
|
|
3235
|
+
for (const tick of timeTicks) {
|
|
3236
|
+
ctx.moveTo(crisp(tick.x), crisp(chartTop));
|
|
3237
|
+
ctx.lineTo(crisp(tick.x), crisp(fullChartBottom));
|
|
3084
3238
|
}
|
|
3239
|
+
ctx.stroke();
|
|
3085
3240
|
ctx.restore();
|
|
3086
3241
|
}
|
|
3087
3242
|
if (grid.sessionSeparators && data.length > 1) {
|
|
@@ -3119,38 +3274,45 @@ function createChart(element, options = {}) {
|
|
|
3119
3274
|
}
|
|
3120
3275
|
return data[index]?.v;
|
|
3121
3276
|
};
|
|
3122
|
-
|
|
3123
|
-
const
|
|
3124
|
-
|
|
3125
|
-
|
|
3277
|
+
{
|
|
3278
|
+
const upWicks = new Path2D();
|
|
3279
|
+
const downWicks = new Path2D();
|
|
3280
|
+
const upBodies = new Path2D();
|
|
3281
|
+
const downBodies = new Path2D();
|
|
3282
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
3283
|
+
const point = data[index];
|
|
3284
|
+
if (!point) {
|
|
3285
|
+
continue;
|
|
3286
|
+
}
|
|
3287
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
3288
|
+
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
3289
|
+
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
3290
|
+
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
3291
|
+
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
3292
|
+
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3293
|
+
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
3294
|
+
const isUp = direction === "up";
|
|
3295
|
+
const roundedCenterX = Math.round(centerX);
|
|
3296
|
+
const wicks = isUp ? upWicks : downWicks;
|
|
3297
|
+
wicks.moveTo(roundedCenterX + 0.5, crisp(yFromPrice(displayHigh)));
|
|
3298
|
+
wicks.lineTo(roundedCenterX + 0.5, crisp(yFromPrice(displayLow)));
|
|
3299
|
+
const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
|
|
3300
|
+
const openYPx = Math.round(yFromPrice(point.o));
|
|
3301
|
+
const closeYPx = Math.round(yFromPrice(displayClose));
|
|
3302
|
+
const bodyIsUp = displayClose >= point.o;
|
|
3303
|
+
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
3304
|
+
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
3305
|
+
(isUp ? upBodies : downBodies).rect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
3126
3306
|
}
|
|
3127
|
-
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
3128
|
-
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
3129
|
-
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
3130
|
-
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
3131
|
-
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
3132
|
-
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3133
|
-
const openY = yFromPrice(point.o);
|
|
3134
|
-
const closeY = yFromPrice(displayClose);
|
|
3135
|
-
const highY = yFromPrice(displayHigh);
|
|
3136
|
-
const lowY = yFromPrice(displayLow);
|
|
3137
|
-
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
3138
|
-
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
3139
|
-
const roundedCenterX = Math.round(centerX);
|
|
3140
|
-
ctx.strokeStyle = candleColor;
|
|
3141
3307
|
ctx.lineWidth = candleWickWidth;
|
|
3142
|
-
ctx.
|
|
3143
|
-
ctx.
|
|
3144
|
-
ctx.
|
|
3145
|
-
ctx.
|
|
3146
|
-
|
|
3147
|
-
|
|
3148
|
-
|
|
3149
|
-
|
|
3150
|
-
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
3151
|
-
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
3152
|
-
ctx.fillStyle = candleColor;
|
|
3153
|
-
ctx.fillRect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
3308
|
+
ctx.strokeStyle = mergedOptions.upColor;
|
|
3309
|
+
ctx.stroke(upWicks);
|
|
3310
|
+
ctx.fillStyle = mergedOptions.upColor;
|
|
3311
|
+
ctx.fill(upBodies);
|
|
3312
|
+
ctx.strokeStyle = mergedOptions.downColor;
|
|
3313
|
+
ctx.stroke(downWicks);
|
|
3314
|
+
ctx.fillStyle = mergedOptions.downColor;
|
|
3315
|
+
ctx.fill(downBodies);
|
|
3154
3316
|
}
|
|
3155
3317
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
3156
3318
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -3268,56 +3430,6 @@ function createChart(element, options = {}) {
|
|
|
3268
3430
|
ctx.textAlign = prevAlign;
|
|
3269
3431
|
ctx.textBaseline = prevBaseline;
|
|
3270
3432
|
}
|
|
3271
|
-
const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
3272
|
-
if (crosshair.visible && crosshairPoint) {
|
|
3273
|
-
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3274
|
-
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
3275
|
-
if (crosshair.mode === "dot") {
|
|
3276
|
-
ctx.save();
|
|
3277
|
-
ctx.fillStyle = crosshair.color;
|
|
3278
|
-
ctx.beginPath();
|
|
3279
|
-
ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
|
|
3280
|
-
ctx.fill();
|
|
3281
|
-
ctx.restore();
|
|
3282
|
-
} else {
|
|
3283
|
-
ctx.save();
|
|
3284
|
-
ctx.strokeStyle = crosshair.color;
|
|
3285
|
-
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3286
|
-
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3287
|
-
if (crosshair.showVertical) {
|
|
3288
|
-
ctx.beginPath();
|
|
3289
|
-
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3290
|
-
ctx.lineTo(crisp(cx), crisp(chartBottom));
|
|
3291
|
-
ctx.stroke();
|
|
3292
|
-
}
|
|
3293
|
-
if (crosshair.showHorizontal) {
|
|
3294
|
-
ctx.beginPath();
|
|
3295
|
-
ctx.moveTo(crisp(chartLeft), crisp(cy));
|
|
3296
|
-
ctx.lineTo(crisp(chartRight), crisp(cy));
|
|
3297
|
-
ctx.stroke();
|
|
3298
|
-
}
|
|
3299
|
-
ctx.restore();
|
|
3300
|
-
}
|
|
3301
|
-
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3302
|
-
ctx.save();
|
|
3303
|
-
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3304
|
-
ctx.textBaseline = "middle";
|
|
3305
|
-
ctx.setLineDash([]);
|
|
3306
|
-
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3307
|
-
const hintGap = 8;
|
|
3308
|
-
if (crosshair.sideHintLeft) {
|
|
3309
|
-
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3310
|
-
ctx.textAlign = "right";
|
|
3311
|
-
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3312
|
-
}
|
|
3313
|
-
if (crosshair.sideHintRight) {
|
|
3314
|
-
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3315
|
-
ctx.textAlign = "left";
|
|
3316
|
-
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3317
|
-
}
|
|
3318
|
-
ctx.restore();
|
|
3319
|
-
}
|
|
3320
|
-
}
|
|
3321
3433
|
ctx.restore();
|
|
3322
3434
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
3323
3435
|
if (positionAxisGutter > 0) {
|
|
@@ -3430,7 +3542,7 @@ function createChart(element, options = {}) {
|
|
|
3430
3542
|
const text = label.text ?? formatPaneValue(label.value);
|
|
3431
3543
|
const labelPaddingX = 7;
|
|
3432
3544
|
const labelHeight = Math.max(16, yAxisFontSize + 8);
|
|
3433
|
-
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(
|
|
3545
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(measureTextWidth(text)) + labelPaddingX * 2);
|
|
3434
3546
|
const labelX = getRightAxisLabelX(chartRight);
|
|
3435
3547
|
const labelY = clamp(yFromPaneValue(label.value) - labelHeight / 2, paneTop + 2, paneBottom - labelHeight - 2);
|
|
3436
3548
|
ctx.fillStyle = label.backgroundColor ?? label.color ?? labels.backgroundColor;
|
|
@@ -3442,18 +3554,6 @@ function createChart(element, options = {}) {
|
|
|
3442
3554
|
}
|
|
3443
3555
|
});
|
|
3444
3556
|
}
|
|
3445
|
-
if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
|
|
3446
|
-
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3447
|
-
ctx.save();
|
|
3448
|
-
ctx.strokeStyle = crosshair.color;
|
|
3449
|
-
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3450
|
-
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3451
|
-
ctx.beginPath();
|
|
3452
|
-
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3453
|
-
ctx.lineTo(crisp(cx), crisp(fullChartBottom));
|
|
3454
|
-
ctx.stroke();
|
|
3455
|
-
ctx.restore();
|
|
3456
|
-
}
|
|
3457
3557
|
ctx.strokeStyle = axis.lineColor;
|
|
3458
3558
|
ctx.lineWidth = Math.max(1, axis.lineWidth);
|
|
3459
3559
|
ctx.beginPath();
|
|
@@ -3463,13 +3563,13 @@ function createChart(element, options = {}) {
|
|
|
3463
3563
|
ctx.lineTo(crisp(chartRight), crisp(fullChartBottom));
|
|
3464
3564
|
ctx.stroke();
|
|
3465
3565
|
const priceScaleTickLabelInset = yAxisFontSize / 2 + 3;
|
|
3466
|
-
|
|
3467
|
-
const ratio = tick / yTicks;
|
|
3468
|
-
const price = yMin + yRange * ratio;
|
|
3469
|
-
const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
|
|
3566
|
+
{
|
|
3470
3567
|
const prevFont = ctx.font;
|
|
3471
3568
|
ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3472
|
-
|
|
3569
|
+
for (const price of priceTicks) {
|
|
3570
|
+
const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
|
|
3571
|
+
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
3572
|
+
}
|
|
3473
3573
|
ctx.font = prevFont;
|
|
3474
3574
|
}
|
|
3475
3575
|
resetLabelSlots(labels.noOverlapping);
|
|
@@ -3578,9 +3678,15 @@ function createChart(element, options = {}) {
|
|
|
3578
3678
|
});
|
|
3579
3679
|
}
|
|
3580
3680
|
}
|
|
3581
|
-
if (labels.showHighLow &&
|
|
3582
|
-
|
|
3583
|
-
|
|
3681
|
+
if (labels.showHighLow && endIndex >= startIndex) {
|
|
3682
|
+
let visibleHigh = Number.NEGATIVE_INFINITY;
|
|
3683
|
+
let visibleLow = Number.POSITIVE_INFINITY;
|
|
3684
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
3685
|
+
const point = data[idx];
|
|
3686
|
+
if (!point) continue;
|
|
3687
|
+
if (point.h > visibleHigh) visibleHigh = point.h;
|
|
3688
|
+
if (point.l < visibleLow) visibleLow = point.l;
|
|
3689
|
+
}
|
|
3584
3690
|
addPriceAxisLabel({
|
|
3585
3691
|
text: `H ${formatPrice(visibleHigh)}`,
|
|
3586
3692
|
price: visibleHigh,
|
|
@@ -3633,12 +3739,12 @@ function createChart(element, options = {}) {
|
|
|
3633
3739
|
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
3634
3740
|
const subtextLineGap = 5;
|
|
3635
3741
|
const labelHeight = baseLabelHeight + (subtexts.length > 0 ? subtexts.length * subtextFontSize + subtexts.length * subtextLineGap : 0);
|
|
3636
|
-
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(
|
|
3742
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(measureTextWidth(label.text)) + labelPaddingX * 2;
|
|
3637
3743
|
let subtextWidth = 0;
|
|
3638
3744
|
if (subtexts.length > 0) {
|
|
3639
3745
|
const baseFont = ctx.font;
|
|
3640
3746
|
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
3641
|
-
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(
|
|
3747
|
+
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(measureTextWidth(subtext)) + labelPaddingX * 2));
|
|
3642
3748
|
ctx.font = baseFont;
|
|
3643
3749
|
}
|
|
3644
3750
|
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
@@ -3742,21 +3848,15 @@ function createChart(element, options = {}) {
|
|
|
3742
3848
|
ctx.font = prevFont;
|
|
3743
3849
|
}
|
|
3744
3850
|
}
|
|
3745
|
-
|
|
3746
|
-
const axisIntraday = axisStepMs > 0 && axisStepMs < 24 * 60 * 60 * 1e3;
|
|
3747
|
-
let prevAxisTickTime = null;
|
|
3748
|
-
for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
|
|
3749
|
-
const tickTime = getTimeForIndex(index);
|
|
3750
|
-
if (!tickTime) {
|
|
3751
|
-
continue;
|
|
3752
|
-
}
|
|
3753
|
-
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3754
|
-
const isNewDay = !prevAxisTickTime || prevAxisTickTime.getDate() !== tickTime.getDate() || prevAxisTickTime.getMonth() !== tickTime.getMonth() || prevAxisTickTime.getFullYear() !== tickTime.getFullYear();
|
|
3755
|
-
const timeLabel = axisIntraday && !isNewDay ? tickTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false }) : tickTime.toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
3756
|
-
prevAxisTickTime = tickTime;
|
|
3851
|
+
{
|
|
3757
3852
|
const prevFont = ctx.font;
|
|
3758
3853
|
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3759
|
-
|
|
3854
|
+
for (const tick of timeTicks) {
|
|
3855
|
+
const tickTime = getTimeForIndex(tick.index);
|
|
3856
|
+
if (!tickTime) continue;
|
|
3857
|
+
const timeLabel = tick.weight >= 9 ? String(tickTime.getFullYear()) : tick.weight === 8 ? tickTime.toLocaleDateString(void 0, { month: "short" }) : tick.weight === 7 ? tickTime.toLocaleDateString(void 0, { month: "short", day: "numeric" }) : tickTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false });
|
|
3858
|
+
drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
|
|
3859
|
+
}
|
|
3760
3860
|
ctx.font = prevFont;
|
|
3761
3861
|
}
|
|
3762
3862
|
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
@@ -3769,137 +3869,229 @@ function createChart(element, options = {}) {
|
|
|
3769
3869
|
drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
|
|
3770
3870
|
ctx.font = prevFont;
|
|
3771
3871
|
}
|
|
3772
|
-
if (
|
|
3773
|
-
|
|
3774
|
-
|
|
3775
|
-
|
|
3776
|
-
|
|
3777
|
-
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
|
|
3797
|
-
|
|
3798
|
-
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3872
|
+
if (baseCtx) {
|
|
3873
|
+
if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
|
|
3874
|
+
baseCanvas.width = canvas.width;
|
|
3875
|
+
baseCanvas.height = canvas.height;
|
|
3876
|
+
}
|
|
3877
|
+
baseCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
3878
|
+
baseCtx.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
|
|
3879
|
+
baseCtx.drawImage(canvas, 0, 0);
|
|
3880
|
+
}
|
|
3881
|
+
overlayLayout = {
|
|
3882
|
+
chartLeft,
|
|
3883
|
+
chartTop,
|
|
3884
|
+
chartRight,
|
|
3885
|
+
chartBottom,
|
|
3886
|
+
chartWidth,
|
|
3887
|
+
chartHeight,
|
|
3888
|
+
fullChartBottom,
|
|
3889
|
+
yMin,
|
|
3890
|
+
yRange,
|
|
3891
|
+
xStart,
|
|
3892
|
+
xSpan
|
|
3893
|
+
};
|
|
3894
|
+
paintCrosshairOverlay();
|
|
3895
|
+
};
|
|
3896
|
+
const paintCrosshairOverlay = () => {
|
|
3897
|
+
crosshairPriceActionRegion = null;
|
|
3898
|
+
const layout = overlayLayout;
|
|
3899
|
+
const crosshair = resolvedCrosshair;
|
|
3900
|
+
if (!layout || !crosshair.visible || !crosshairPoint) {
|
|
3901
|
+
return;
|
|
3902
|
+
}
|
|
3903
|
+
const { chartLeft, chartTop, chartRight, chartBottom, chartWidth, chartHeight, fullChartBottom, yMin, yRange, xStart, xSpan: xSpan2 } = layout;
|
|
3904
|
+
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3905
|
+
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
3906
|
+
ctx.save();
|
|
3907
|
+
ctx.beginPath();
|
|
3908
|
+
ctx.rect(chartLeft, chartTop, chartWidth, Math.max(0, chartBottom - chartTop));
|
|
3909
|
+
ctx.clip();
|
|
3910
|
+
if (crosshair.mode === "dot") {
|
|
3911
|
+
ctx.fillStyle = crosshair.color;
|
|
3912
|
+
ctx.beginPath();
|
|
3913
|
+
ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
|
|
3914
|
+
ctx.fill();
|
|
3915
|
+
} else {
|
|
3916
|
+
ctx.strokeStyle = crosshair.color;
|
|
3917
|
+
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3918
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3919
|
+
if (crosshair.showHorizontal) {
|
|
3920
|
+
ctx.beginPath();
|
|
3921
|
+
ctx.moveTo(crisp(chartLeft), crisp(cy));
|
|
3922
|
+
ctx.lineTo(crisp(chartRight), crisp(cy));
|
|
3923
|
+
ctx.stroke();
|
|
3924
|
+
}
|
|
3925
|
+
}
|
|
3926
|
+
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3927
|
+
ctx.save();
|
|
3928
|
+
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3929
|
+
ctx.textBaseline = "middle";
|
|
3930
|
+
ctx.setLineDash([]);
|
|
3931
|
+
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3932
|
+
const hintGap = 8;
|
|
3933
|
+
if (crosshair.sideHintLeft) {
|
|
3934
|
+
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3935
|
+
ctx.textAlign = "right";
|
|
3936
|
+
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3937
|
+
}
|
|
3938
|
+
if (crosshair.sideHintRight) {
|
|
3939
|
+
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3940
|
+
ctx.textAlign = "left";
|
|
3941
|
+
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3942
|
+
}
|
|
3943
|
+
ctx.restore();
|
|
3944
|
+
}
|
|
3945
|
+
ctx.restore();
|
|
3946
|
+
if (crosshair.showVertical) {
|
|
3947
|
+
ctx.save();
|
|
3948
|
+
ctx.strokeStyle = crosshair.color;
|
|
3949
|
+
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3950
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3951
|
+
ctx.beginPath();
|
|
3952
|
+
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3953
|
+
ctx.lineTo(crisp(cx), crisp(fullChartBottom));
|
|
3954
|
+
ctx.stroke();
|
|
3955
|
+
ctx.restore();
|
|
3956
|
+
}
|
|
3957
|
+
const labelPaddingX = 8;
|
|
3958
|
+
const labelHeight = 20;
|
|
3959
|
+
const labelRadius = Math.max(0, crosshair.labelBorderRadius);
|
|
3960
|
+
const labelBackground = crosshair.labelBackgroundColor;
|
|
3961
|
+
const labelTextColor = crosshair.labelTextColor;
|
|
3962
|
+
const labelBorderColor = crosshair.labelBorderColor;
|
|
3963
|
+
const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
|
|
3964
|
+
const labelBorderStyle = crosshair.labelBorderStyle;
|
|
3965
|
+
const strokeCrosshairLabel = (x, y, widthValue, heightValue = labelHeight) => {
|
|
3966
|
+
if (labelBorderWidth <= 0) {
|
|
3967
|
+
return;
|
|
3968
|
+
}
|
|
3969
|
+
ctx.save();
|
|
3970
|
+
ctx.strokeStyle = labelBorderColor;
|
|
3971
|
+
ctx.lineWidth = labelBorderWidth;
|
|
3972
|
+
applyDashPattern(
|
|
3973
|
+
labelBorderStyle,
|
|
3974
|
+
dashPatterns.borderDotted,
|
|
3975
|
+
dashPatterns.borderDashed
|
|
3976
|
+
);
|
|
3977
|
+
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, heightValue, labelRadius);
|
|
3978
|
+
ctx.restore();
|
|
3979
|
+
};
|
|
3980
|
+
if (crosshair.showPriceLabel) {
|
|
3981
|
+
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
3982
|
+
const priceText = formatPrice(hoverPrice);
|
|
3983
|
+
const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
|
|
3984
|
+
const priceX = getRightAxisLabelX(chartRight);
|
|
3985
|
+
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
3986
|
+
ctx.fillStyle = labelBackground;
|
|
3987
|
+
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
|
|
3988
|
+
strokeCrosshairLabel(priceX, priceY, priceWidth);
|
|
3989
|
+
drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
|
|
3990
|
+
if (crosshair.showPriceActionButton) {
|
|
3991
|
+
const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
|
|
3992
|
+
const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
|
|
3993
|
+
const containerPaddingX = 5;
|
|
3994
|
+
const containerWidth = buttonSize + containerPaddingX * 2;
|
|
3995
|
+
const containerX = priceX - buttonGap - containerWidth;
|
|
3996
|
+
const containerY = priceY;
|
|
3997
|
+
const buttonX = containerX + containerPaddingX;
|
|
3998
|
+
const buttonY = priceY + (labelHeight - buttonSize) / 2;
|
|
3999
|
+
const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
|
|
4000
|
+
const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
|
|
4001
|
+
const buttonCenterX = buttonX + buttonSize / 2;
|
|
4002
|
+
const buttonCenterY = buttonY + buttonSize / 2;
|
|
4003
|
+
const containerRadius = labelRadius;
|
|
3804
4004
|
ctx.fillStyle = labelBackground;
|
|
3805
|
-
fillRoundedRect(Math.round(
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
const buttonCenterY = buttonY + buttonSize / 2;
|
|
3821
|
-
const containerRadius = labelRadius;
|
|
3822
|
-
ctx.fillStyle = labelBackground;
|
|
3823
|
-
fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
3824
|
-
if (labelBorderWidth > 0) {
|
|
3825
|
-
ctx.save();
|
|
3826
|
-
ctx.strokeStyle = labelBorderColor;
|
|
3827
|
-
ctx.lineWidth = labelBorderWidth;
|
|
3828
|
-
ctx.setLineDash([]);
|
|
3829
|
-
strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
3830
|
-
ctx.restore();
|
|
3831
|
-
}
|
|
3832
|
-
if (buttonBorderWidth > 0) {
|
|
3833
|
-
ctx.save();
|
|
3834
|
-
ctx.strokeStyle = labelBorderColor;
|
|
3835
|
-
ctx.lineWidth = buttonBorderWidth;
|
|
3836
|
-
ctx.setLineDash([]);
|
|
3837
|
-
if (crosshair.priceActionButtonRounded) {
|
|
3838
|
-
ctx.beginPath();
|
|
3839
|
-
ctx.arc(
|
|
3840
|
-
buttonCenterX,
|
|
3841
|
-
buttonCenterY,
|
|
3842
|
-
Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
|
|
3843
|
-
0,
|
|
3844
|
-
Math.PI * 2
|
|
3845
|
-
);
|
|
3846
|
-
ctx.stroke();
|
|
3847
|
-
} else {
|
|
3848
|
-
strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
|
|
3849
|
-
}
|
|
3850
|
-
ctx.restore();
|
|
3851
|
-
}
|
|
3852
|
-
if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
|
|
3853
|
-
const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
|
|
3854
|
-
const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
|
|
3855
|
-
ctx.save();
|
|
3856
|
-
ctx.strokeStyle = labelTextColor;
|
|
3857
|
-
ctx.lineWidth = plusThickness;
|
|
3858
|
-
ctx.lineCap = "round";
|
|
3859
|
-
ctx.setLineDash([]);
|
|
4005
|
+
fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
4006
|
+
if (labelBorderWidth > 0) {
|
|
4007
|
+
ctx.save();
|
|
4008
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4009
|
+
ctx.lineWidth = labelBorderWidth;
|
|
4010
|
+
ctx.setLineDash([]);
|
|
4011
|
+
strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
4012
|
+
ctx.restore();
|
|
4013
|
+
}
|
|
4014
|
+
if (buttonBorderWidth > 0) {
|
|
4015
|
+
ctx.save();
|
|
4016
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4017
|
+
ctx.lineWidth = buttonBorderWidth;
|
|
4018
|
+
ctx.setLineDash([]);
|
|
4019
|
+
if (crosshair.priceActionButtonRounded) {
|
|
3860
4020
|
ctx.beginPath();
|
|
3861
|
-
ctx.
|
|
3862
|
-
ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
|
|
3863
|
-
ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
|
|
3864
|
-
ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
|
|
3865
|
-
ctx.stroke();
|
|
3866
|
-
ctx.restore();
|
|
3867
|
-
} else {
|
|
3868
|
-
drawText(
|
|
3869
|
-
crosshair.priceActionButtonText,
|
|
4021
|
+
ctx.arc(
|
|
3870
4022
|
buttonCenterX,
|
|
3871
4023
|
buttonCenterY,
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
4024
|
+
Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
|
|
4025
|
+
0,
|
|
4026
|
+
Math.PI * 2
|
|
3875
4027
|
);
|
|
4028
|
+
ctx.stroke();
|
|
4029
|
+
} else {
|
|
4030
|
+
strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
|
|
3876
4031
|
}
|
|
3877
|
-
|
|
3878
|
-
x: containerX,
|
|
3879
|
-
y: containerY,
|
|
3880
|
-
width: containerWidth,
|
|
3881
|
-
height: labelHeight,
|
|
3882
|
-
price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
|
|
3883
|
-
};
|
|
4032
|
+
ctx.restore();
|
|
3884
4033
|
}
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
ctx.
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
4034
|
+
if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
|
|
4035
|
+
const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
|
|
4036
|
+
const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
|
|
4037
|
+
ctx.save();
|
|
4038
|
+
ctx.strokeStyle = labelTextColor;
|
|
4039
|
+
ctx.lineWidth = plusThickness;
|
|
4040
|
+
ctx.lineCap = "round";
|
|
4041
|
+
ctx.setLineDash([]);
|
|
4042
|
+
ctx.beginPath();
|
|
4043
|
+
ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
|
|
4044
|
+
ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
|
|
4045
|
+
ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
|
|
4046
|
+
ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
|
|
4047
|
+
ctx.stroke();
|
|
4048
|
+
ctx.restore();
|
|
4049
|
+
} else {
|
|
4050
|
+
drawText(
|
|
4051
|
+
crosshair.priceActionButtonText,
|
|
4052
|
+
buttonCenterX,
|
|
4053
|
+
buttonCenterY,
|
|
4054
|
+
"center",
|
|
4055
|
+
"middle",
|
|
4056
|
+
labelTextColor
|
|
4057
|
+
);
|
|
3900
4058
|
}
|
|
4059
|
+
crosshairPriceActionRegion = {
|
|
4060
|
+
x: containerX,
|
|
4061
|
+
y: containerY,
|
|
4062
|
+
width: containerWidth,
|
|
4063
|
+
height: labelHeight,
|
|
4064
|
+
price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
|
|
4065
|
+
};
|
|
3901
4066
|
}
|
|
3902
4067
|
}
|
|
4068
|
+
if (crosshair.showTimeLabel) {
|
|
4069
|
+
const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
|
|
4070
|
+
const hoverIndex = Math.round(xStart + ratio * xSpan2 - 0.5);
|
|
4071
|
+
const hoverTime = getTimeForIndex(hoverIndex);
|
|
4072
|
+
if (hoverTime) {
|
|
4073
|
+
const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
|
|
4074
|
+
const timeWidth = Math.ceil(measureTextWidth(timeText)) + labelPaddingX * 2;
|
|
4075
|
+
const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
|
|
4076
|
+
const timeY = fullChartBottom + 1;
|
|
4077
|
+
const timeHeight = Math.max(labelHeight, Math.floor(height - timeY));
|
|
4078
|
+
ctx.fillStyle = labelBackground;
|
|
4079
|
+
fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, timeHeight, labelRadius);
|
|
4080
|
+
strokeCrosshairLabel(timeX, timeY, timeWidth, timeHeight);
|
|
4081
|
+
drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
|
|
4082
|
+
}
|
|
4083
|
+
}
|
|
4084
|
+
};
|
|
4085
|
+
const drawOverlayOnly = () => {
|
|
4086
|
+
if (!baseCtx || !overlayLayout || baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height || baseCanvas.width === 0) {
|
|
4087
|
+
draw({ updateAutoScale: false });
|
|
4088
|
+
return;
|
|
4089
|
+
}
|
|
4090
|
+
const pixelRatio = getPixelRatio();
|
|
4091
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4092
|
+
ctx.drawImage(baseCanvas, 0, 0);
|
|
4093
|
+
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
4094
|
+
paintCrosshairOverlay();
|
|
3903
4095
|
};
|
|
3904
4096
|
const zoomX = (factor, anchorX) => {
|
|
3905
4097
|
if (!drawState || data.length === 0) {
|
|
@@ -3990,6 +4182,59 @@ function createChart(element, options = {}) {
|
|
|
3990
4182
|
}
|
|
3991
4183
|
scheduleDraw();
|
|
3992
4184
|
};
|
|
4185
|
+
let kineticRafId = null;
|
|
4186
|
+
const panVelocitySamples = [];
|
|
4187
|
+
const cancelKineticPan = () => {
|
|
4188
|
+
if (kineticRafId !== null) {
|
|
4189
|
+
cancelAnimationFrame(kineticRafId);
|
|
4190
|
+
kineticRafId = null;
|
|
4191
|
+
}
|
|
4192
|
+
};
|
|
4193
|
+
const recordPanVelocitySample = (x) => {
|
|
4194
|
+
const now = performance.now();
|
|
4195
|
+
panVelocitySamples.push({ t: now, x });
|
|
4196
|
+
while (panVelocitySamples.length > 0 && now - panVelocitySamples[0].t > 120) {
|
|
4197
|
+
panVelocitySamples.shift();
|
|
4198
|
+
}
|
|
4199
|
+
};
|
|
4200
|
+
const startKineticPan = (initialVelocityPxPerMs) => {
|
|
4201
|
+
cancelKineticPan();
|
|
4202
|
+
let velocity = initialVelocityPxPerMs;
|
|
4203
|
+
let lastTime = performance.now();
|
|
4204
|
+
const stepFn = (now) => {
|
|
4205
|
+
kineticRafId = null;
|
|
4206
|
+
const dt = Math.min(64, Math.max(1, now - lastTime));
|
|
4207
|
+
lastTime = now;
|
|
4208
|
+
const centerBefore = xCenter;
|
|
4209
|
+
pan(velocity * dt, 0, true, false);
|
|
4210
|
+
velocity *= Math.exp(-dt / 325);
|
|
4211
|
+
if (Math.abs(velocity) < 0.02 || xCenter === centerBefore) {
|
|
4212
|
+
return;
|
|
4213
|
+
}
|
|
4214
|
+
kineticRafId = requestAnimationFrame(stepFn);
|
|
4215
|
+
};
|
|
4216
|
+
kineticRafId = requestAnimationFrame(stepFn);
|
|
4217
|
+
};
|
|
4218
|
+
const maybeStartKineticPan = (pointerType) => {
|
|
4219
|
+
const config = mergedOptions.kineticScroll ?? DEFAULT_OPTIONS.kineticScroll;
|
|
4220
|
+
const enabled = pointerType === "touch" || pointerType === "pen" ? config?.touch !== false : config?.mouse === true;
|
|
4221
|
+
if (!enabled || panVelocitySamples.length < 2) {
|
|
4222
|
+
panVelocitySamples.length = 0;
|
|
4223
|
+
return;
|
|
4224
|
+
}
|
|
4225
|
+
const first = panVelocitySamples[0];
|
|
4226
|
+
const last = panVelocitySamples[panVelocitySamples.length - 1];
|
|
4227
|
+
panVelocitySamples.length = 0;
|
|
4228
|
+
const elapsed = last.t - first.t;
|
|
4229
|
+
if (elapsed <= 0 || performance.now() - last.t > 100) {
|
|
4230
|
+
return;
|
|
4231
|
+
}
|
|
4232
|
+
const velocity = (last.x - first.x) / elapsed;
|
|
4233
|
+
if (Math.abs(velocity) < 0.1) {
|
|
4234
|
+
return;
|
|
4235
|
+
}
|
|
4236
|
+
startKineticPan(clamp(velocity, -4, 4));
|
|
4237
|
+
};
|
|
3993
4238
|
const resetYViewport = () => {
|
|
3994
4239
|
yMinOverride = null;
|
|
3995
4240
|
yMaxOverride = null;
|
|
@@ -4049,6 +4294,7 @@ function createChart(element, options = {}) {
|
|
|
4049
4294
|
scheduleDraw();
|
|
4050
4295
|
};
|
|
4051
4296
|
const resetViewport = () => {
|
|
4297
|
+
cancelKineticPan();
|
|
4052
4298
|
fitXViewport();
|
|
4053
4299
|
resetYViewport();
|
|
4054
4300
|
updateFollowLatest(true);
|
|
@@ -4058,6 +4304,7 @@ function createChart(element, options = {}) {
|
|
|
4058
4304
|
const isFollowingLatest = () => followLatest;
|
|
4059
4305
|
const setFollowingLatest = (follow) => {
|
|
4060
4306
|
if (follow && data.length > 0) {
|
|
4307
|
+
cancelKineticPan();
|
|
4061
4308
|
xCenter = data.length - xSpan / 2 + rightEdgePaddingBars;
|
|
4062
4309
|
clampXViewport();
|
|
4063
4310
|
updateFollowLatest(true);
|
|
@@ -4427,7 +4674,7 @@ function createChart(element, options = {}) {
|
|
|
4427
4674
|
const prevFont = ctx.font;
|
|
4428
4675
|
ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
|
|
4429
4676
|
const lines = (drawing.label ?? "").split("\n");
|
|
4430
|
-
const textW = Math.max(1, ...lines.map((line) =>
|
|
4677
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
4431
4678
|
ctx.font = prevFont;
|
|
4432
4679
|
const isNote = drawing.type === "note";
|
|
4433
4680
|
const padX = isNote ? 8 : 2;
|
|
@@ -4900,16 +5147,24 @@ function createChart(element, options = {}) {
|
|
|
4900
5147
|
emitViewportChange();
|
|
4901
5148
|
scheduleDraw();
|
|
4902
5149
|
};
|
|
5150
|
+
const capturePointer = (pointerId) => {
|
|
5151
|
+
try {
|
|
5152
|
+
canvas.setPointerCapture(pointerId);
|
|
5153
|
+
} catch {
|
|
5154
|
+
}
|
|
5155
|
+
};
|
|
4903
5156
|
const onPointerDown = (event) => {
|
|
4904
5157
|
if (event.pointerType === "touch" || event.pointerType === "pen") {
|
|
4905
5158
|
event.preventDefault();
|
|
4906
5159
|
}
|
|
5160
|
+
cancelKineticPan();
|
|
5161
|
+
panVelocitySamples.length = 0;
|
|
4907
5162
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4908
5163
|
shiftKeyActive = event.shiftKey;
|
|
4909
5164
|
const point = getCanvasPoint(event);
|
|
4910
5165
|
if (event.pointerType === "touch") {
|
|
4911
5166
|
touchPointers.set(event.pointerId, point);
|
|
4912
|
-
|
|
5167
|
+
capturePointer(event.pointerId);
|
|
4913
5168
|
const touchPair = getTouchPair();
|
|
4914
5169
|
if (touchPair) {
|
|
4915
5170
|
beginPinchZoom(touchPair[0], touchPair[1]);
|
|
@@ -4939,7 +5194,7 @@ function createChart(element, options = {}) {
|
|
|
4939
5194
|
lastPrice: startPrice,
|
|
4940
5195
|
moved: false
|
|
4941
5196
|
};
|
|
4942
|
-
|
|
5197
|
+
capturePointer(event.pointerId);
|
|
4943
5198
|
canvas.style.cursor = "ns-resize";
|
|
4944
5199
|
setCrosshairPoint(null);
|
|
4945
5200
|
return;
|
|
@@ -4960,7 +5215,7 @@ function createChart(element, options = {}) {
|
|
|
4960
5215
|
startPrice: orderDragRegion.price,
|
|
4961
5216
|
lastPrice: orderDragRegion.price
|
|
4962
5217
|
};
|
|
4963
|
-
|
|
5218
|
+
capturePointer(event.pointerId);
|
|
4964
5219
|
canvas.style.cursor = "ns-resize";
|
|
4965
5220
|
setCrosshairPoint(null);
|
|
4966
5221
|
return;
|
|
@@ -4992,7 +5247,7 @@ function createChart(element, options = {}) {
|
|
|
4992
5247
|
startPoints: drawingHit.drawing.points.map((drawingPoint) => ({ ...drawingPoint }))
|
|
4993
5248
|
};
|
|
4994
5249
|
activePointerId = event.pointerId;
|
|
4995
|
-
|
|
5250
|
+
capturePointer(event.pointerId);
|
|
4996
5251
|
}
|
|
4997
5252
|
}
|
|
4998
5253
|
setCrosshairPoint(null);
|
|
@@ -5020,7 +5275,7 @@ function createChart(element, options = {}) {
|
|
|
5020
5275
|
};
|
|
5021
5276
|
lastPointerX = point.x;
|
|
5022
5277
|
lastPointerY = point.y;
|
|
5023
|
-
|
|
5278
|
+
capturePointer(event.pointerId);
|
|
5024
5279
|
if (crosshairDrag) {
|
|
5025
5280
|
canvas.style.cursor = "crosshair";
|
|
5026
5281
|
setCrosshairPoint(point);
|
|
@@ -5206,6 +5461,7 @@ function createChart(element, options = {}) {
|
|
|
5206
5461
|
const deltaY = point.y - lastPointerY;
|
|
5207
5462
|
if (dragMode === "plot") {
|
|
5208
5463
|
canvas.style.cursor = "grabbing";
|
|
5464
|
+
recordPanVelocitySample(point.x);
|
|
5209
5465
|
pan(deltaX, deltaY, true, true);
|
|
5210
5466
|
setCrosshairPoint(null);
|
|
5211
5467
|
} else if (dragMode === "x-axis") {
|
|
@@ -5279,10 +5535,16 @@ function createChart(element, options = {}) {
|
|
|
5279
5535
|
drawingDragState = null;
|
|
5280
5536
|
emitDrawingsChange();
|
|
5281
5537
|
}
|
|
5538
|
+
const endedPlotDrag = isDragging && dragMode === "plot";
|
|
5282
5539
|
isDragging = false;
|
|
5283
5540
|
dragMode = null;
|
|
5284
5541
|
activePointerId = null;
|
|
5285
5542
|
canvas.style.cursor = "default";
|
|
5543
|
+
if (endedPlotDrag) {
|
|
5544
|
+
maybeStartKineticPan(event?.pointerType);
|
|
5545
|
+
} else {
|
|
5546
|
+
panVelocitySamples.length = 0;
|
|
5547
|
+
}
|
|
5286
5548
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
5287
5549
|
if (pointerDownInfo.crosshairDrag) {
|
|
5288
5550
|
const point = getCanvasPoint(event);
|
|
@@ -5320,6 +5582,7 @@ function createChart(element, options = {}) {
|
|
|
5320
5582
|
if (!drawState) {
|
|
5321
5583
|
return;
|
|
5322
5584
|
}
|
|
5585
|
+
cancelKineticPan();
|
|
5323
5586
|
const point = getCanvasPoint(event);
|
|
5324
5587
|
const region = getHitRegion(point.x, point.y);
|
|
5325
5588
|
if (region === "outside") {
|
|
@@ -5419,6 +5682,7 @@ function createChart(element, options = {}) {
|
|
|
5419
5682
|
width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
|
|
5420
5683
|
height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
|
|
5421
5684
|
mergedOptions = { ...mergedOptions, width, height };
|
|
5685
|
+
refreshResolvedOptions();
|
|
5422
5686
|
resetRightMarginCache();
|
|
5423
5687
|
doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
5424
5688
|
doubleClickAction = mergedOptions.doubleClickAction;
|
|
@@ -5763,6 +6027,7 @@ function createChart(element, options = {}) {
|
|
|
5763
6027
|
drawingHoverHandler = handler;
|
|
5764
6028
|
};
|
|
5765
6029
|
const destroy = () => {
|
|
6030
|
+
cancelKineticPan();
|
|
5766
6031
|
if (smoothingRafId !== null) {
|
|
5767
6032
|
cancelAnimationFrame(smoothingRafId);
|
|
5768
6033
|
smoothingRafId = null;
|