hyperprop-charting-library 0.1.120 → 0.1.122
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 +623 -352
- package/dist/hyperprop-charting-library.d.ts +33 -0
- package/dist/hyperprop-charting-library.js +623 -352
- package/dist/index.cjs +623 -352
- package/dist/index.d.cts +33 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.js +623 -352
- package/package.json +1 -1
|
@@ -215,8 +215,13 @@ var DEFAULT_OPTIONS = {
|
|
|
215
215
|
tickSize: 0,
|
|
216
216
|
candleColorMode: "openClose",
|
|
217
217
|
candleColorEpsilon: -1,
|
|
218
|
-
|
|
218
|
+
// 0 = deterministic scale (lightweight-charts behavior): the y-range is a
|
|
219
|
+
// pure function of the visible data, so it can never drift or "breathe".
|
|
220
|
+
// Values > 0 re-enable eased contraction for hosts that prefer it.
|
|
221
|
+
autoScaleSmoothing: 0,
|
|
219
222
|
autoScaleIgnoreLatestCandle: true,
|
|
223
|
+
kineticScroll: { touch: true, mouse: false },
|
|
224
|
+
timeStepMs: 0,
|
|
220
225
|
pinOutOfRangeLines: false,
|
|
221
226
|
doubleClickEnabled: true,
|
|
222
227
|
doubleClickAction: "reset",
|
|
@@ -615,6 +620,26 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
615
620
|
}
|
|
616
621
|
ctx.restore();
|
|
617
622
|
};
|
|
623
|
+
var rangeOfSeries = (seriesList, startIndex, endIndex, skipIndex) => {
|
|
624
|
+
let min = Number.POSITIVE_INFINITY;
|
|
625
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
626
|
+
for (const series of seriesList) {
|
|
627
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
628
|
+
if (idx === skipIndex) continue;
|
|
629
|
+
const value = series[idx];
|
|
630
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
631
|
+
if (value < min) min = value;
|
|
632
|
+
if (value > max) max = value;
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
return min <= max ? { min, max } : null;
|
|
636
|
+
};
|
|
637
|
+
var maAutoscaleRange = (cacheKeyPrefix, compute, defaultLength) => (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
638
|
+
const length = clampIndicatorLength(inputs.length, defaultLength);
|
|
639
|
+
const source = inputs.source ?? "close";
|
|
640
|
+
const series = withCachedSeries(`${cacheKeyPrefix}|${length}|${source}`, data, () => compute(data, length, source));
|
|
641
|
+
return rangeOfSeries([series], startIndex, endIndex, skipIndex);
|
|
642
|
+
};
|
|
618
643
|
var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
|
|
619
644
|
if (!renderContext.yFromPrice || opacity <= 0) return;
|
|
620
645
|
const yFromPrice = renderContext.yFromPrice;
|
|
@@ -832,7 +857,8 @@ var BUILTIN_SMA_INDICATOR = {
|
|
|
832
857
|
() => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
833
858
|
);
|
|
834
859
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
|
|
835
|
-
}
|
|
860
|
+
},
|
|
861
|
+
getAutoscaleRange: maAutoscaleRange("sma", computeSmaSeries, 20)
|
|
836
862
|
};
|
|
837
863
|
var BUILTIN_EMA_INDICATOR = {
|
|
838
864
|
id: "ema",
|
|
@@ -847,7 +873,8 @@ var BUILTIN_EMA_INDICATOR = {
|
|
|
847
873
|
() => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
848
874
|
);
|
|
849
875
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
850
|
-
}
|
|
876
|
+
},
|
|
877
|
+
getAutoscaleRange: maAutoscaleRange("ema", computeEmaSeries, 20)
|
|
851
878
|
};
|
|
852
879
|
var BUILTIN_WMA_INDICATOR = {
|
|
853
880
|
id: "wma",
|
|
@@ -862,7 +889,8 @@ var BUILTIN_WMA_INDICATOR = {
|
|
|
862
889
|
() => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
863
890
|
);
|
|
864
891
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
|
|
865
|
-
}
|
|
892
|
+
},
|
|
893
|
+
getAutoscaleRange: maAutoscaleRange("wma", computeWmaSeries, 20)
|
|
866
894
|
};
|
|
867
895
|
var BUILTIN_VWMA_INDICATOR = {
|
|
868
896
|
id: "vwma",
|
|
@@ -877,7 +905,8 @@ var BUILTIN_VWMA_INDICATOR = {
|
|
|
877
905
|
() => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
878
906
|
);
|
|
879
907
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
|
|
880
|
-
}
|
|
908
|
+
},
|
|
909
|
+
getAutoscaleRange: maAutoscaleRange("vwma", computeVwmaSeries, 20)
|
|
881
910
|
};
|
|
882
911
|
var BUILTIN_RMA_INDICATOR = {
|
|
883
912
|
id: "rma",
|
|
@@ -892,7 +921,8 @@ var BUILTIN_RMA_INDICATOR = {
|
|
|
892
921
|
() => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
893
922
|
);
|
|
894
923
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
|
|
895
|
-
}
|
|
924
|
+
},
|
|
925
|
+
getAutoscaleRange: maAutoscaleRange("rma", computeRmaSeries, 14)
|
|
896
926
|
};
|
|
897
927
|
var BUILTIN_HMA_INDICATOR = {
|
|
898
928
|
id: "hma",
|
|
@@ -907,7 +937,8 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
907
937
|
() => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
908
938
|
);
|
|
909
939
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
910
|
-
}
|
|
940
|
+
},
|
|
941
|
+
getAutoscaleRange: maAutoscaleRange("hma", computeHmaSeries, 21)
|
|
911
942
|
};
|
|
912
943
|
var BUILTIN_VWAP_INDICATOR = {
|
|
913
944
|
id: "vwap",
|
|
@@ -948,6 +979,27 @@ var BUILTIN_VWAP_INDICATOR = {
|
|
|
948
979
|
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
949
980
|
}
|
|
950
981
|
drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
982
|
+
},
|
|
983
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
984
|
+
const vwap = withCachedSeries("vwap|line", data, () => computeVwapSeries(data, "vwap"));
|
|
985
|
+
if (!inputs.showBands) {
|
|
986
|
+
return rangeOfSeries([vwap], startIndex, endIndex, skipIndex);
|
|
987
|
+
}
|
|
988
|
+
const sigma = withCachedSeries("vwap|sigma", data, () => computeVwapSeries(data, "sigma"));
|
|
989
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
990
|
+
let min = Number.POSITIVE_INFINITY;
|
|
991
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
992
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
993
|
+
if (idx === skipIndex) continue;
|
|
994
|
+
const value = vwap[idx];
|
|
995
|
+
const s = sigma[idx];
|
|
996
|
+
if (value == null || s == null) continue;
|
|
997
|
+
const lo = value - multiplier * s;
|
|
998
|
+
const hi = value + multiplier * s;
|
|
999
|
+
if (lo < min) min = lo;
|
|
1000
|
+
if (hi > max) max = hi;
|
|
1001
|
+
}
|
|
1002
|
+
return min <= max ? { min, max } : null;
|
|
951
1003
|
}
|
|
952
1004
|
};
|
|
953
1005
|
var BUILTIN_BOLLINGER_INDICATOR = {
|
|
@@ -989,7 +1041,23 @@ var BUILTIN_BOLLINGER_INDICATOR = {
|
|
|
989
1041
|
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
|
|
990
1042
|
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
991
1043
|
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
992
|
-
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#
|
|
1044
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#ec4899", width);
|
|
1045
|
+
},
|
|
1046
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
1047
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
1048
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
1049
|
+
const source = inputs.source ?? "close";
|
|
1050
|
+
const upper = withCachedSeries(
|
|
1051
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
1052
|
+
data,
|
|
1053
|
+
() => computeBollingerSeries(data, length, multiplier, source, "upper")
|
|
1054
|
+
);
|
|
1055
|
+
const lower = withCachedSeries(
|
|
1056
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
1057
|
+
data,
|
|
1058
|
+
() => computeBollingerSeries(data, length, multiplier, source, "lower")
|
|
1059
|
+
);
|
|
1060
|
+
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
993
1061
|
}
|
|
994
1062
|
};
|
|
995
1063
|
var BUILTIN_STDDEV_INDICATOR = {
|
|
@@ -1082,6 +1150,20 @@ var BUILTIN_INDICATORS = [
|
|
|
1082
1150
|
];
|
|
1083
1151
|
function createChart(element, options = {}) {
|
|
1084
1152
|
let mergedOptions = mergeChartOptions(DEFAULT_OPTIONS, options);
|
|
1153
|
+
let resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1154
|
+
let resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1155
|
+
let resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1156
|
+
let resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1157
|
+
let resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
|
|
1158
|
+
let resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1159
|
+
const refreshResolvedOptions = () => {
|
|
1160
|
+
resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1161
|
+
resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1162
|
+
resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1163
|
+
resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1164
|
+
resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
|
|
1165
|
+
resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1166
|
+
};
|
|
1085
1167
|
let width = mergedOptions.width;
|
|
1086
1168
|
let height = mergedOptions.height;
|
|
1087
1169
|
let data = [];
|
|
@@ -1118,8 +1200,11 @@ function createChart(element, options = {}) {
|
|
|
1118
1200
|
pane: indicator.pane ?? plugin?.pane ?? "overlay",
|
|
1119
1201
|
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
1120
1202
|
zIndex: Math.round(Number(indicator.zIndex) || 0),
|
|
1121
|
-
|
|
1122
|
-
|
|
1203
|
+
// Overlay series participate in autoscale by default (lightweight-charts
|
|
1204
|
+
// semantics): bands and MAs expand the price range instead of clipping
|
|
1205
|
+
// at the chart edge. Hosts can still opt out per indicator.
|
|
1206
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale ?? false,
|
|
1207
|
+
overlayScaleWeight: indicator.overlayScaleWeight === void 0 ? 1 : Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0)),
|
|
1123
1208
|
inputs: {
|
|
1124
1209
|
...defaults,
|
|
1125
1210
|
...indicator.inputs ?? {}
|
|
@@ -1321,6 +1406,9 @@ function createChart(element, options = {}) {
|
|
|
1321
1406
|
canvas.setAttribute("draggable", "false");
|
|
1322
1407
|
element.innerHTML = "";
|
|
1323
1408
|
element.appendChild(canvas);
|
|
1409
|
+
const baseCanvas = document.createElement("canvas");
|
|
1410
|
+
const baseCtx = baseCanvas.getContext("2d");
|
|
1411
|
+
let overlayLayout = null;
|
|
1324
1412
|
const margin = { top: 16, right: 72, bottom: 26, left: 12 };
|
|
1325
1413
|
let cachedRightMargin = margin.right;
|
|
1326
1414
|
const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
|
|
@@ -1360,6 +1448,20 @@ function createChart(element, options = {}) {
|
|
|
1360
1448
|
const clamp = (value, min, max) => {
|
|
1361
1449
|
return Math.min(max, Math.max(min, value));
|
|
1362
1450
|
};
|
|
1451
|
+
const textWidthCache = /* @__PURE__ */ new Map();
|
|
1452
|
+
const measureTextWidth = (text) => {
|
|
1453
|
+
const key = `${ctx.font}\0${text}`;
|
|
1454
|
+
const cached = textWidthCache.get(key);
|
|
1455
|
+
if (cached !== void 0) {
|
|
1456
|
+
return cached;
|
|
1457
|
+
}
|
|
1458
|
+
const width2 = ctx.measureText(text).width;
|
|
1459
|
+
if (textWidthCache.size >= 4096) {
|
|
1460
|
+
textWidthCache.clear();
|
|
1461
|
+
}
|
|
1462
|
+
textWidthCache.set(key, width2);
|
|
1463
|
+
return width2;
|
|
1464
|
+
};
|
|
1363
1465
|
const resetLabelSlots = (enabled) => {
|
|
1364
1466
|
noOverlappingLineLabels = enabled;
|
|
1365
1467
|
rightAxisLabelSlots = [];
|
|
@@ -1562,7 +1664,7 @@ function createChart(element, options = {}) {
|
|
|
1562
1664
|
return `${integerPart}${decimalPart}`;
|
|
1563
1665
|
};
|
|
1564
1666
|
const getMeasuredLabelWidth = (text, paddingX) => {
|
|
1565
|
-
return Math.ceil(
|
|
1667
|
+
return Math.ceil(measureTextWidth(text)) + paddingX * 2;
|
|
1566
1668
|
};
|
|
1567
1669
|
const getStabilizedNumericLabelWidth = (text, paddingX) => {
|
|
1568
1670
|
const measured = getMeasuredLabelWidth(text, paddingX);
|
|
@@ -1619,11 +1721,16 @@ function createChart(element, options = {}) {
|
|
|
1619
1721
|
return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
|
|
1620
1722
|
};
|
|
1621
1723
|
const getTimeStepMs = () => {
|
|
1724
|
+
const configured = Number(mergedOptions.timeStepMs);
|
|
1725
|
+
if (Number.isFinite(configured) && configured > 0) {
|
|
1726
|
+
return configured;
|
|
1727
|
+
}
|
|
1622
1728
|
if (data.length < 2) {
|
|
1623
1729
|
return 24 * 60 * 60 * 1e3;
|
|
1624
1730
|
}
|
|
1625
1731
|
const deltas = [];
|
|
1626
|
-
|
|
1732
|
+
const first = Math.max(1, data.length - 40);
|
|
1733
|
+
for (let index = first; index < data.length; index += 1) {
|
|
1627
1734
|
const previous = data[index - 1];
|
|
1628
1735
|
const current = data[index];
|
|
1629
1736
|
if (!previous || !current) {
|
|
@@ -1803,11 +1910,11 @@ function createChart(element, options = {}) {
|
|
|
1803
1910
|
return;
|
|
1804
1911
|
}
|
|
1805
1912
|
const labelText = mergedLine.label ?? formatPrice(mergedLine.price);
|
|
1806
|
-
const axis =
|
|
1913
|
+
const axis = resolvedAxis;
|
|
1807
1914
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1808
1915
|
const labelPaddingX = 8;
|
|
1809
1916
|
const labelHeight = 20;
|
|
1810
|
-
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(
|
|
1917
|
+
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(measureTextWidth(labelText)) + labelPaddingX * 2;
|
|
1811
1918
|
const labelWidth = getRightAxisLabelWidth(chartRight, measuredLabelWidth);
|
|
1812
1919
|
const labelX = getRightAxisLabelX(chartRight);
|
|
1813
1920
|
const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
|
|
@@ -1902,7 +2009,7 @@ function createChart(element, options = {}) {
|
|
|
1902
2009
|
const closeAction = mergedLine.type === "market" ? "close" : "cancel";
|
|
1903
2010
|
const showCloseButton = mergedLine.showCloseButton;
|
|
1904
2011
|
const actionButtonText = mergedLine.actionButtonText;
|
|
1905
|
-
const axis =
|
|
2012
|
+
const axis = resolvedAxis;
|
|
1906
2013
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1907
2014
|
const legacyButton = actionButtonText ? [
|
|
1908
2015
|
{
|
|
@@ -1924,7 +2031,7 @@ function createChart(element, options = {}) {
|
|
|
1924
2031
|
const actionButtonMetrics = actionButtons.map((button) => {
|
|
1925
2032
|
const paddingX = Math.max(2, button.paddingX ?? mergedLine.actionButtonPaddingX);
|
|
1926
2033
|
const minWidth = Math.max(16, button.minWidth ?? mergedLine.actionButtonMinWidth);
|
|
1927
|
-
const width2 = Math.max(minWidth, Math.ceil(
|
|
2034
|
+
const width2 = Math.max(minWidth, Math.ceil(measureTextWidth(button.text)) + paddingX * 2);
|
|
1928
2035
|
return { button, width: width2 };
|
|
1929
2036
|
});
|
|
1930
2037
|
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
@@ -1932,8 +2039,8 @@ function createChart(element, options = {}) {
|
|
|
1932
2039
|
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
1933
2040
|
const segmentPaddingX = 8;
|
|
1934
2041
|
const labelHeight = 22;
|
|
1935
|
-
const qtyWidth = qtyText ? Math.ceil(
|
|
1936
|
-
const centerMeasuredWidth = Math.ceil(
|
|
2042
|
+
const qtyWidth = qtyText ? Math.ceil(measureTextWidth(qtyText)) + segmentPaddingX * 2 : 0;
|
|
2043
|
+
const centerMeasuredWidth = Math.ceil(measureTextWidth(centerText)) + segmentPaddingX * 2;
|
|
1937
2044
|
const centerWidth = mergedLine.id === void 0 ? centerMeasuredWidth : Math.max(centerMeasuredWidth, orderWidgetWidthById.get(mergedLine.id) ?? 0);
|
|
1938
2045
|
if (mergedLine.id) {
|
|
1939
2046
|
orderWidgetWidthById.set(mergedLine.id, centerWidth);
|
|
@@ -1941,7 +2048,7 @@ function createChart(element, options = {}) {
|
|
|
1941
2048
|
const closeWidth = showCloseButton ? 24 : 0;
|
|
1942
2049
|
const mainWidgetWidth = qtyWidth + centerWidth + closeWidth;
|
|
1943
2050
|
const totalWidth = mainWidgetWidth + actionButtonsGap + actionButtonsTotalWidth;
|
|
1944
|
-
const crosshair =
|
|
2051
|
+
const crosshair = resolvedCrosshair;
|
|
1945
2052
|
const crosshairActionInset = crosshair.visible && crosshair.showPriceLabel && crosshair.showPriceActionButton ? Math.max(
|
|
1946
2053
|
0,
|
|
1947
2054
|
Math.round(crosshair.priceActionButtonGap) + (clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4)) + 10) - 4
|
|
@@ -2122,20 +2229,29 @@ function createChart(element, options = {}) {
|
|
|
2122
2229
|
}
|
|
2123
2230
|
}
|
|
2124
2231
|
crosshairPoint = point;
|
|
2125
|
-
scheduleDraw();
|
|
2232
|
+
scheduleDraw({ overlayOnly: true });
|
|
2126
2233
|
};
|
|
2127
2234
|
let drawRafId = null;
|
|
2128
2235
|
let pendingDrawUpdateAutoScale = false;
|
|
2236
|
+
let pendingDrawOverlayOnly = true;
|
|
2129
2237
|
const scheduleDraw = (options2 = {}) => {
|
|
2130
|
-
|
|
2238
|
+
const overlayOnly = options2.overlayOnly ?? false;
|
|
2239
|
+
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
2240
|
+
pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
|
|
2131
2241
|
if (drawRafId !== null) {
|
|
2132
2242
|
return;
|
|
2133
2243
|
}
|
|
2134
2244
|
drawRafId = requestAnimationFrame(() => {
|
|
2135
2245
|
drawRafId = null;
|
|
2136
2246
|
const updateAutoScale = pendingDrawUpdateAutoScale;
|
|
2247
|
+
const flushOverlayOnly = pendingDrawOverlayOnly;
|
|
2137
2248
|
pendingDrawUpdateAutoScale = false;
|
|
2138
|
-
|
|
2249
|
+
pendingDrawOverlayOnly = true;
|
|
2250
|
+
if (flushOverlayOnly) {
|
|
2251
|
+
drawOverlayOnly();
|
|
2252
|
+
} else {
|
|
2253
|
+
draw({ updateAutoScale });
|
|
2254
|
+
}
|
|
2139
2255
|
});
|
|
2140
2256
|
};
|
|
2141
2257
|
const draw = (options2 = {}) => {
|
|
@@ -2149,15 +2265,15 @@ function createChart(element, options = {}) {
|
|
|
2149
2265
|
canvas.width = Math.floor(width * pixelRatio);
|
|
2150
2266
|
canvas.height = Math.floor(height * pixelRatio);
|
|
2151
2267
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
2152
|
-
const axis =
|
|
2153
|
-
const xAxis =
|
|
2154
|
-
const yAxis =
|
|
2268
|
+
const axis = resolvedAxis;
|
|
2269
|
+
const xAxis = resolvedXAxis;
|
|
2270
|
+
const yAxis = resolvedYAxis;
|
|
2155
2271
|
const xAxisFontSize = Math.max(8, xAxis.fontSize);
|
|
2156
2272
|
const yAxisFontSize = Math.max(8, yAxis.fontSize);
|
|
2157
2273
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2158
2274
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2159
2275
|
ctx.fillRect(0, 0, width, height);
|
|
2160
|
-
const labels =
|
|
2276
|
+
const labels = resolvedLabels;
|
|
2161
2277
|
const estimateRightMargin = () => {
|
|
2162
2278
|
const paddingX = Math.max(4, labels.labelPaddingX);
|
|
2163
2279
|
let required = margin.right - 1;
|
|
@@ -2270,6 +2386,7 @@ function createChart(element, options = {}) {
|
|
|
2270
2386
|
yMax: 0
|
|
2271
2387
|
};
|
|
2272
2388
|
drawText("Load OHLC data to render chart", chartLeft + 10, chartTop + 20, "left", "middle", "#64748b");
|
|
2389
|
+
overlayLayout = null;
|
|
2273
2390
|
return;
|
|
2274
2391
|
}
|
|
2275
2392
|
clampXViewport();
|
|
@@ -2277,62 +2394,39 @@ function createChart(element, options = {}) {
|
|
|
2277
2394
|
const xEnd = xStart + xSpan;
|
|
2278
2395
|
const startIndex = Math.max(0, Math.floor(xStart));
|
|
2279
2396
|
const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
|
|
2280
|
-
const
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
const fallback = data.slice(fallbackStart, latestIndex);
|
|
2291
|
-
if (fallback.length > 0) {
|
|
2292
|
-
priceSource = fallback;
|
|
2293
|
-
}
|
|
2397
|
+
const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
|
|
2398
|
+
const scanCandleRange = (from, to) => {
|
|
2399
|
+
let min = Number.POSITIVE_INFINITY;
|
|
2400
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
2401
|
+
for (let idx = from; idx <= to; idx += 1) {
|
|
2402
|
+
if (idx === skipLatestIndex) continue;
|
|
2403
|
+
const point = data[idx];
|
|
2404
|
+
if (!point) continue;
|
|
2405
|
+
if (point.l < min) min = point.l;
|
|
2406
|
+
if (point.h > max) max = point.h;
|
|
2294
2407
|
}
|
|
2408
|
+
return min <= max ? { min, max } : null;
|
|
2409
|
+
};
|
|
2410
|
+
let candleRange = scanCandleRange(startIndex, endIndex);
|
|
2411
|
+
if (!candleRange) {
|
|
2412
|
+
const latestIndex = data.length - 1;
|
|
2413
|
+
candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? { min: data[latestIndex].l, max: data[latestIndex].h };
|
|
2295
2414
|
}
|
|
2296
|
-
let minPrice =
|
|
2297
|
-
let maxPrice =
|
|
2415
|
+
let minPrice = candleRange.min;
|
|
2416
|
+
let maxPrice = candleRange.max;
|
|
2298
2417
|
if (overlayIndicatorsForScale.length > 0) {
|
|
2299
|
-
for (const { indicator } of overlayIndicatorsForScale) {
|
|
2418
|
+
for (const { indicator, plugin } of overlayIndicatorsForScale) {
|
|
2300
2419
|
if (indicator.excludeFromAutoscale) {
|
|
2301
2420
|
continue;
|
|
2302
2421
|
}
|
|
2303
|
-
const
|
|
2304
|
-
|
|
2305
|
-
const source = inputs.source ?? "close";
|
|
2306
|
-
const length = clampIndicatorLength(inputs.length ?? 14, 14);
|
|
2307
|
-
let series = null;
|
|
2308
|
-
if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
|
|
2309
|
-
if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
|
|
2310
|
-
if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
|
|
2311
|
-
if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
|
|
2312
|
-
if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
|
|
2313
|
-
if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
|
|
2314
|
-
if (!series) {
|
|
2422
|
+
const range = plugin.getAutoscaleRange?.(data, startIndex, endIndex, indicator.inputs, skipLatestIndex);
|
|
2423
|
+
if (!range || !Number.isFinite(range.min) || !Number.isFinite(range.max)) {
|
|
2315
2424
|
continue;
|
|
2316
2425
|
}
|
|
2317
|
-
const visibleValues = [];
|
|
2318
|
-
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
2319
|
-
if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 && idx === data.length - 1) {
|
|
2320
|
-
continue;
|
|
2321
|
-
}
|
|
2322
|
-
const value = series[idx];
|
|
2323
|
-
if (Number.isFinite(value ?? Number.NaN)) {
|
|
2324
|
-
visibleValues.push(value);
|
|
2325
|
-
}
|
|
2326
|
-
}
|
|
2327
|
-
if (visibleValues.length === 0) {
|
|
2328
|
-
continue;
|
|
2329
|
-
}
|
|
2330
|
-
const seriesMin = Math.min(...visibleValues);
|
|
2331
|
-
const seriesMax = Math.max(...visibleValues);
|
|
2332
2426
|
const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
|
|
2333
2427
|
const currentMid = (minPrice + maxPrice) / 2;
|
|
2334
|
-
const weightedMin = currentMid + (
|
|
2335
|
-
const weightedMax = currentMid + (
|
|
2428
|
+
const weightedMin = currentMid + (range.min - currentMid) * weight;
|
|
2429
|
+
const weightedMax = currentMid + (range.max - currentMid) * weight;
|
|
2336
2430
|
minPrice = Math.min(minPrice, weightedMin);
|
|
2337
2431
|
maxPrice = Math.max(maxPrice, weightedMax);
|
|
2338
2432
|
}
|
|
@@ -2407,7 +2501,7 @@ function createChart(element, options = {}) {
|
|
|
2407
2501
|
ctx.save();
|
|
2408
2502
|
const prevFont = ctx.font;
|
|
2409
2503
|
ctx.font = `500 12px ${mergedOptions.fontFamily}`;
|
|
2410
|
-
const textWidth =
|
|
2504
|
+
const textWidth = measureTextWidth(labelText);
|
|
2411
2505
|
const paddingX = 6;
|
|
2412
2506
|
const bgWidth = textWidth + paddingX * 2;
|
|
2413
2507
|
const bgHeight = 18;
|
|
@@ -2573,7 +2667,7 @@ function createChart(element, options = {}) {
|
|
|
2573
2667
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2574
2668
|
levelLines.forEach((level, index) => {
|
|
2575
2669
|
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2576
|
-
const textWidth =
|
|
2670
|
+
const textWidth = measureTextWidth(labelText);
|
|
2577
2671
|
const padding = 4;
|
|
2578
2672
|
const bgX = lineLeft + 4;
|
|
2579
2673
|
const bgY = level.y - 9;
|
|
@@ -2646,7 +2740,7 @@ function createChart(element, options = {}) {
|
|
|
2646
2740
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2647
2741
|
levelLines.forEach((level, index) => {
|
|
2648
2742
|
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2649
|
-
const textWidth =
|
|
2743
|
+
const textWidth = measureTextWidth(labelText);
|
|
2650
2744
|
const padding = 4;
|
|
2651
2745
|
const bgX = chartLeft + 4;
|
|
2652
2746
|
const bgY = level.y - 9;
|
|
@@ -2789,8 +2883,8 @@ function createChart(element, options = {}) {
|
|
|
2789
2883
|
const padding = 6;
|
|
2790
2884
|
const lineH = 14;
|
|
2791
2885
|
const isDigit = (ch) => ch >= "0" && ch <= "9";
|
|
2792
|
-
const digitW =
|
|
2793
|
-
const charAdvance = (ch) => isDigit(ch) ? digitW :
|
|
2886
|
+
const digitW = measureTextWidth("0");
|
|
2887
|
+
const charAdvance = (ch) => isDigit(ch) ? digitW : measureTextWidth(ch);
|
|
2794
2888
|
const lineWidth = (line) => {
|
|
2795
2889
|
let w = 0;
|
|
2796
2890
|
for (const ch of line) w += charAdvance(ch);
|
|
@@ -2813,11 +2907,11 @@ function createChart(element, options = {}) {
|
|
|
2813
2907
|
const y = startY + lineIndex * lineH;
|
|
2814
2908
|
for (const ch of line) {
|
|
2815
2909
|
if (isDigit(ch)) {
|
|
2816
|
-
ctx.fillText(ch, x + (digitW -
|
|
2910
|
+
ctx.fillText(ch, x + (digitW - measureTextWidth(ch)) / 2, y);
|
|
2817
2911
|
x += digitW;
|
|
2818
2912
|
} else {
|
|
2819
2913
|
ctx.fillText(ch, x, y);
|
|
2820
|
-
x +=
|
|
2914
|
+
x += measureTextWidth(ch);
|
|
2821
2915
|
}
|
|
2822
2916
|
}
|
|
2823
2917
|
});
|
|
@@ -2949,7 +3043,7 @@ function createChart(element, options = {}) {
|
|
|
2949
3043
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2950
3044
|
const padding = 6;
|
|
2951
3045
|
const lineHeight = 15;
|
|
2952
|
-
const textW = Math.max(...labelLines.map((line) =>
|
|
3046
|
+
const textW = Math.max(...labelLines.map((line) => measureTextWidth(line)));
|
|
2953
3047
|
const pillW = textW + padding * 2;
|
|
2954
3048
|
const pillH = labelLines.length * lineHeight + padding;
|
|
2955
3049
|
const pillX = midX - pillW / 2;
|
|
@@ -2985,7 +3079,7 @@ function createChart(element, options = {}) {
|
|
|
2985
3079
|
ctx.textBaseline = "middle";
|
|
2986
3080
|
const lines = (drawing.label ?? "").split("\n");
|
|
2987
3081
|
const lineH = Math.round(fontSize * 1.35);
|
|
2988
|
-
const textW = Math.max(1, ...lines.map((line) =>
|
|
3082
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
2989
3083
|
const padX = isNote ? 8 : 2;
|
|
2990
3084
|
const padY = isNote ? 6 : 1;
|
|
2991
3085
|
const blockW = textW + padX * 2;
|
|
@@ -3067,47 +3161,114 @@ function createChart(element, options = {}) {
|
|
|
3067
3161
|
Math.max(1, candleSpacing - 1),
|
|
3068
3162
|
Math.max(candleMinWidth, Math.floor(candleSpacing * candleBodyWidthRatio))
|
|
3069
3163
|
);
|
|
3070
|
-
const grid =
|
|
3164
|
+
const grid = resolvedGrid;
|
|
3071
3165
|
const gridOpacity = clamp(grid.opacity, 0, 1);
|
|
3072
3166
|
const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
|
|
3073
3167
|
const yTicks = Math.max(1, Math.floor(yTickCountInput));
|
|
3074
3168
|
const gridColor = grid.color ?? mergedOptions.gridColor;
|
|
3169
|
+
const priceTicks = [];
|
|
3170
|
+
{
|
|
3171
|
+
const targetStep = yRange / Math.max(1, yTicks);
|
|
3172
|
+
let step;
|
|
3173
|
+
if (targetStep > 0 && Number.isFinite(targetStep)) {
|
|
3174
|
+
const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
|
|
3175
|
+
const frac = targetStep / base;
|
|
3176
|
+
step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
|
|
3177
|
+
const instrumentTick = getConfiguredTickSize();
|
|
3178
|
+
if (instrumentTick > 0 && step < instrumentTick * 1e6) {
|
|
3179
|
+
step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
|
|
3180
|
+
}
|
|
3181
|
+
} else {
|
|
3182
|
+
step = 1;
|
|
3183
|
+
}
|
|
3184
|
+
const firstTick = Math.ceil(yMin / step) * step;
|
|
3185
|
+
for (let i = 0; ; i += 1) {
|
|
3186
|
+
const price = firstTick + i * step;
|
|
3187
|
+
if (price > yMax + step * 1e-6) break;
|
|
3188
|
+
priceTicks.push(price);
|
|
3189
|
+
if (priceTicks.length > 200) break;
|
|
3190
|
+
}
|
|
3191
|
+
}
|
|
3075
3192
|
if (grid.horizontalLines) {
|
|
3076
3193
|
ctx.save();
|
|
3077
3194
|
ctx.globalAlpha = gridOpacity;
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3195
|
+
ctx.strokeStyle = gridColor;
|
|
3196
|
+
ctx.lineWidth = 1;
|
|
3197
|
+
ctx.beginPath();
|
|
3198
|
+
for (const price of priceTicks) {
|
|
3081
3199
|
const y = yFromPrice(price);
|
|
3082
|
-
ctx.strokeStyle = gridColor;
|
|
3083
|
-
ctx.lineWidth = 1;
|
|
3084
|
-
ctx.beginPath();
|
|
3085
3200
|
ctx.moveTo(crisp(chartLeft), crisp(y));
|
|
3086
3201
|
ctx.lineTo(crisp(chartRight), crisp(y));
|
|
3087
|
-
ctx.stroke();
|
|
3088
3202
|
}
|
|
3203
|
+
ctx.stroke();
|
|
3089
3204
|
ctx.restore();
|
|
3090
3205
|
}
|
|
3091
|
-
const minLabelSpacingPx = Math.max(
|
|
3092
|
-
const
|
|
3093
|
-
const xTickCountInput = grid.xTickCount ?? autoLabelCount;
|
|
3094
|
-
const xTickCount = Math.max(2, Math.floor(xTickCountInput));
|
|
3095
|
-
const rawStep = xSpan / xTickCount;
|
|
3096
|
-
const xStep = Math.max(1, Math.ceil(rawStep));
|
|
3097
|
-
const visibleTickStart = Math.floor(xStart);
|
|
3206
|
+
const minLabelSpacingPx = Math.max(60, Math.min(120, chartWidth / Math.max(2, Math.floor(grid.xTickCount ?? 8))));
|
|
3207
|
+
const visibleTickStart = Math.max(0, Math.floor(xStart));
|
|
3098
3208
|
const visibleTickEnd = Math.ceil(xEnd) - 1;
|
|
3099
|
-
const
|
|
3100
|
-
|
|
3209
|
+
const timeTicks = [];
|
|
3210
|
+
{
|
|
3211
|
+
const weightBuckets = [[], [], [], [], [], [], [], [], [], []];
|
|
3212
|
+
let prevTime = visibleTickStart > 0 ? getTimeForIndex(visibleTickStart - 1) : null;
|
|
3213
|
+
for (let index = visibleTickStart; index <= visibleTickEnd; index += 1) {
|
|
3214
|
+
const time = getTimeForIndex(index);
|
|
3215
|
+
if (!time) {
|
|
3216
|
+
prevTime = null;
|
|
3217
|
+
continue;
|
|
3218
|
+
}
|
|
3219
|
+
let weight = 0;
|
|
3220
|
+
if (!prevTime) {
|
|
3221
|
+
weight = 7;
|
|
3222
|
+
} else {
|
|
3223
|
+
if (prevTime.getFullYear() !== time.getFullYear()) weight = 9;
|
|
3224
|
+
else if (prevTime.getMonth() !== time.getMonth()) weight = 8;
|
|
3225
|
+
else if (prevTime.getDate() !== time.getDate()) weight = 7;
|
|
3226
|
+
else {
|
|
3227
|
+
const prevMinutes = prevTime.getHours() * 60 + prevTime.getMinutes();
|
|
3228
|
+
const curMinutes = time.getHours() * 60 + time.getMinutes();
|
|
3229
|
+
const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
|
|
3230
|
+
if (crossed(360)) weight = 6;
|
|
3231
|
+
else if (crossed(60)) weight = 5;
|
|
3232
|
+
else if (crossed(30)) weight = 4;
|
|
3233
|
+
else if (crossed(15)) weight = 3;
|
|
3234
|
+
else if (crossed(5)) weight = 2;
|
|
3235
|
+
else weight = 1;
|
|
3236
|
+
}
|
|
3237
|
+
}
|
|
3238
|
+
weightBuckets[weight].push(index);
|
|
3239
|
+
prevTime = time;
|
|
3240
|
+
}
|
|
3241
|
+
const placedXs = [];
|
|
3242
|
+
const maxMarks = Math.floor(chartWidth / minLabelSpacingPx) + 2;
|
|
3243
|
+
for (let weight = 9; weight >= 1 && timeTicks.length < maxMarks; weight -= 1) {
|
|
3244
|
+
for (const index of weightBuckets[weight]) {
|
|
3245
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3246
|
+
let fits = true;
|
|
3247
|
+
for (const placedX of placedXs) {
|
|
3248
|
+
if (Math.abs(placedX - x) < minLabelSpacingPx) {
|
|
3249
|
+
fits = false;
|
|
3250
|
+
break;
|
|
3251
|
+
}
|
|
3252
|
+
}
|
|
3253
|
+
if (!fits) continue;
|
|
3254
|
+
placedXs.push(x);
|
|
3255
|
+
timeTicks.push({ index, weight, x });
|
|
3256
|
+
if (timeTicks.length >= maxMarks) break;
|
|
3257
|
+
}
|
|
3258
|
+
}
|
|
3259
|
+
timeTicks.sort((a, b) => a.index - b.index);
|
|
3260
|
+
}
|
|
3261
|
+
if (grid.verticalLines && timeTicks.length > 0) {
|
|
3101
3262
|
ctx.save();
|
|
3102
3263
|
ctx.globalAlpha = gridOpacity;
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
3108
|
-
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
3109
|
-
ctx.stroke();
|
|
3264
|
+
ctx.strokeStyle = gridColor;
|
|
3265
|
+
ctx.lineWidth = 1;
|
|
3266
|
+
ctx.beginPath();
|
|
3267
|
+
for (const tick of timeTicks) {
|
|
3268
|
+
ctx.moveTo(crisp(tick.x), crisp(chartTop));
|
|
3269
|
+
ctx.lineTo(crisp(tick.x), crisp(fullChartBottom));
|
|
3110
3270
|
}
|
|
3271
|
+
ctx.stroke();
|
|
3111
3272
|
ctx.restore();
|
|
3112
3273
|
}
|
|
3113
3274
|
if (grid.sessionSeparators && data.length > 1) {
|
|
@@ -3145,38 +3306,45 @@ function createChart(element, options = {}) {
|
|
|
3145
3306
|
}
|
|
3146
3307
|
return data[index]?.v;
|
|
3147
3308
|
};
|
|
3148
|
-
|
|
3149
|
-
const
|
|
3150
|
-
|
|
3151
|
-
|
|
3309
|
+
{
|
|
3310
|
+
const upWicks = new Path2D();
|
|
3311
|
+
const downWicks = new Path2D();
|
|
3312
|
+
const upBodies = new Path2D();
|
|
3313
|
+
const downBodies = new Path2D();
|
|
3314
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
3315
|
+
const point = data[index];
|
|
3316
|
+
if (!point) {
|
|
3317
|
+
continue;
|
|
3318
|
+
}
|
|
3319
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
3320
|
+
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
3321
|
+
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
3322
|
+
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
3323
|
+
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
3324
|
+
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3325
|
+
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
3326
|
+
const isUp = direction === "up";
|
|
3327
|
+
const roundedCenterX = Math.round(centerX);
|
|
3328
|
+
const wicks = isUp ? upWicks : downWicks;
|
|
3329
|
+
wicks.moveTo(roundedCenterX + 0.5, crisp(yFromPrice(displayHigh)));
|
|
3330
|
+
wicks.lineTo(roundedCenterX + 0.5, crisp(yFromPrice(displayLow)));
|
|
3331
|
+
const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
|
|
3332
|
+
const openYPx = Math.round(yFromPrice(point.o));
|
|
3333
|
+
const closeYPx = Math.round(yFromPrice(displayClose));
|
|
3334
|
+
const bodyIsUp = displayClose >= point.o;
|
|
3335
|
+
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
3336
|
+
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
3337
|
+
(isUp ? upBodies : downBodies).rect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
3152
3338
|
}
|
|
3153
|
-
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
3154
|
-
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
3155
|
-
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
3156
|
-
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
3157
|
-
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
3158
|
-
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3159
|
-
const openY = yFromPrice(point.o);
|
|
3160
|
-
const closeY = yFromPrice(displayClose);
|
|
3161
|
-
const highY = yFromPrice(displayHigh);
|
|
3162
|
-
const lowY = yFromPrice(displayLow);
|
|
3163
|
-
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
3164
|
-
const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
|
|
3165
|
-
const roundedCenterX = Math.round(centerX);
|
|
3166
|
-
ctx.strokeStyle = candleColor;
|
|
3167
3339
|
ctx.lineWidth = candleWickWidth;
|
|
3168
|
-
ctx.
|
|
3169
|
-
ctx.
|
|
3170
|
-
ctx.
|
|
3171
|
-
ctx.
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
|
|
3175
|
-
|
|
3176
|
-
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
3177
|
-
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
3178
|
-
ctx.fillStyle = candleColor;
|
|
3179
|
-
ctx.fillRect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
3340
|
+
ctx.strokeStyle = mergedOptions.upColor;
|
|
3341
|
+
ctx.stroke(upWicks);
|
|
3342
|
+
ctx.fillStyle = mergedOptions.upColor;
|
|
3343
|
+
ctx.fill(upBodies);
|
|
3344
|
+
ctx.strokeStyle = mergedOptions.downColor;
|
|
3345
|
+
ctx.stroke(downWicks);
|
|
3346
|
+
ctx.fillStyle = mergedOptions.downColor;
|
|
3347
|
+
ctx.fill(downBodies);
|
|
3180
3348
|
}
|
|
3181
3349
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
3182
3350
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -3294,56 +3462,6 @@ function createChart(element, options = {}) {
|
|
|
3294
3462
|
ctx.textAlign = prevAlign;
|
|
3295
3463
|
ctx.textBaseline = prevBaseline;
|
|
3296
3464
|
}
|
|
3297
|
-
const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
3298
|
-
if (crosshair.visible && crosshairPoint) {
|
|
3299
|
-
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3300
|
-
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
3301
|
-
if (crosshair.mode === "dot") {
|
|
3302
|
-
ctx.save();
|
|
3303
|
-
ctx.fillStyle = crosshair.color;
|
|
3304
|
-
ctx.beginPath();
|
|
3305
|
-
ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
|
|
3306
|
-
ctx.fill();
|
|
3307
|
-
ctx.restore();
|
|
3308
|
-
} else {
|
|
3309
|
-
ctx.save();
|
|
3310
|
-
ctx.strokeStyle = crosshair.color;
|
|
3311
|
-
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3312
|
-
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3313
|
-
if (crosshair.showVertical) {
|
|
3314
|
-
ctx.beginPath();
|
|
3315
|
-
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3316
|
-
ctx.lineTo(crisp(cx), crisp(chartBottom));
|
|
3317
|
-
ctx.stroke();
|
|
3318
|
-
}
|
|
3319
|
-
if (crosshair.showHorizontal) {
|
|
3320
|
-
ctx.beginPath();
|
|
3321
|
-
ctx.moveTo(crisp(chartLeft), crisp(cy));
|
|
3322
|
-
ctx.lineTo(crisp(chartRight), crisp(cy));
|
|
3323
|
-
ctx.stroke();
|
|
3324
|
-
}
|
|
3325
|
-
ctx.restore();
|
|
3326
|
-
}
|
|
3327
|
-
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3328
|
-
ctx.save();
|
|
3329
|
-
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3330
|
-
ctx.textBaseline = "middle";
|
|
3331
|
-
ctx.setLineDash([]);
|
|
3332
|
-
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3333
|
-
const hintGap = 8;
|
|
3334
|
-
if (crosshair.sideHintLeft) {
|
|
3335
|
-
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3336
|
-
ctx.textAlign = "right";
|
|
3337
|
-
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3338
|
-
}
|
|
3339
|
-
if (crosshair.sideHintRight) {
|
|
3340
|
-
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3341
|
-
ctx.textAlign = "left";
|
|
3342
|
-
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3343
|
-
}
|
|
3344
|
-
ctx.restore();
|
|
3345
|
-
}
|
|
3346
|
-
}
|
|
3347
3465
|
ctx.restore();
|
|
3348
3466
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
3349
3467
|
if (positionAxisGutter > 0) {
|
|
@@ -3456,7 +3574,7 @@ function createChart(element, options = {}) {
|
|
|
3456
3574
|
const text = label.text ?? formatPaneValue(label.value);
|
|
3457
3575
|
const labelPaddingX = 7;
|
|
3458
3576
|
const labelHeight = Math.max(16, yAxisFontSize + 8);
|
|
3459
|
-
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(
|
|
3577
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(measureTextWidth(text)) + labelPaddingX * 2);
|
|
3460
3578
|
const labelX = getRightAxisLabelX(chartRight);
|
|
3461
3579
|
const labelY = clamp(yFromPaneValue(label.value) - labelHeight / 2, paneTop + 2, paneBottom - labelHeight - 2);
|
|
3462
3580
|
ctx.fillStyle = label.backgroundColor ?? label.color ?? labels.backgroundColor;
|
|
@@ -3468,18 +3586,6 @@ function createChart(element, options = {}) {
|
|
|
3468
3586
|
}
|
|
3469
3587
|
});
|
|
3470
3588
|
}
|
|
3471
|
-
if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
|
|
3472
|
-
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3473
|
-
ctx.save();
|
|
3474
|
-
ctx.strokeStyle = crosshair.color;
|
|
3475
|
-
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3476
|
-
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3477
|
-
ctx.beginPath();
|
|
3478
|
-
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3479
|
-
ctx.lineTo(crisp(cx), crisp(fullChartBottom));
|
|
3480
|
-
ctx.stroke();
|
|
3481
|
-
ctx.restore();
|
|
3482
|
-
}
|
|
3483
3589
|
ctx.strokeStyle = axis.lineColor;
|
|
3484
3590
|
ctx.lineWidth = Math.max(1, axis.lineWidth);
|
|
3485
3591
|
ctx.beginPath();
|
|
@@ -3489,13 +3595,13 @@ function createChart(element, options = {}) {
|
|
|
3489
3595
|
ctx.lineTo(crisp(chartRight), crisp(fullChartBottom));
|
|
3490
3596
|
ctx.stroke();
|
|
3491
3597
|
const priceScaleTickLabelInset = yAxisFontSize / 2 + 3;
|
|
3492
|
-
|
|
3493
|
-
const ratio = tick / yTicks;
|
|
3494
|
-
const price = yMin + yRange * ratio;
|
|
3495
|
-
const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
|
|
3598
|
+
{
|
|
3496
3599
|
const prevFont = ctx.font;
|
|
3497
3600
|
ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3498
|
-
|
|
3601
|
+
for (const price of priceTicks) {
|
|
3602
|
+
const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
|
|
3603
|
+
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
3604
|
+
}
|
|
3499
3605
|
ctx.font = prevFont;
|
|
3500
3606
|
}
|
|
3501
3607
|
resetLabelSlots(labels.noOverlapping);
|
|
@@ -3533,8 +3639,8 @@ function createChart(element, options = {}) {
|
|
|
3533
3639
|
return null;
|
|
3534
3640
|
}
|
|
3535
3641
|
const stepMs = getTimeStepMs();
|
|
3536
|
-
const
|
|
3537
|
-
const countdownMs =
|
|
3642
|
+
const elapsedMs = Date.now() - last.time.getTime();
|
|
3643
|
+
const countdownMs = elapsedMs >= 0 ? stepMs - elapsedMs % stepMs : Math.min(stepMs, stepMs - elapsedMs);
|
|
3538
3644
|
return formatDuration(countdownMs);
|
|
3539
3645
|
};
|
|
3540
3646
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
@@ -3604,9 +3710,15 @@ function createChart(element, options = {}) {
|
|
|
3604
3710
|
});
|
|
3605
3711
|
}
|
|
3606
3712
|
}
|
|
3607
|
-
if (labels.showHighLow &&
|
|
3608
|
-
|
|
3609
|
-
|
|
3713
|
+
if (labels.showHighLow && endIndex >= startIndex) {
|
|
3714
|
+
let visibleHigh = Number.NEGATIVE_INFINITY;
|
|
3715
|
+
let visibleLow = Number.POSITIVE_INFINITY;
|
|
3716
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
3717
|
+
const point = data[idx];
|
|
3718
|
+
if (!point) continue;
|
|
3719
|
+
if (point.h > visibleHigh) visibleHigh = point.h;
|
|
3720
|
+
if (point.l < visibleLow) visibleLow = point.l;
|
|
3721
|
+
}
|
|
3610
3722
|
addPriceAxisLabel({
|
|
3611
3723
|
text: `H ${formatPrice(visibleHigh)}`,
|
|
3612
3724
|
price: visibleHigh,
|
|
@@ -3659,12 +3771,12 @@ function createChart(element, options = {}) {
|
|
|
3659
3771
|
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
3660
3772
|
const subtextLineGap = 5;
|
|
3661
3773
|
const labelHeight = baseLabelHeight + (subtexts.length > 0 ? subtexts.length * subtextFontSize + subtexts.length * subtextLineGap : 0);
|
|
3662
|
-
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(
|
|
3774
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(measureTextWidth(label.text)) + labelPaddingX * 2;
|
|
3663
3775
|
let subtextWidth = 0;
|
|
3664
3776
|
if (subtexts.length > 0) {
|
|
3665
3777
|
const baseFont = ctx.font;
|
|
3666
3778
|
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
3667
|
-
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(
|
|
3779
|
+
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(measureTextWidth(subtext)) + labelPaddingX * 2));
|
|
3668
3780
|
ctx.font = baseFont;
|
|
3669
3781
|
}
|
|
3670
3782
|
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
@@ -3768,164 +3880,250 @@ function createChart(element, options = {}) {
|
|
|
3768
3880
|
ctx.font = prevFont;
|
|
3769
3881
|
}
|
|
3770
3882
|
}
|
|
3771
|
-
|
|
3772
|
-
const axisIntraday = axisStepMs > 0 && axisStepMs < 24 * 60 * 60 * 1e3;
|
|
3773
|
-
let prevAxisTickTime = null;
|
|
3774
|
-
for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
|
|
3775
|
-
const tickTime = getTimeForIndex(index);
|
|
3776
|
-
if (!tickTime) {
|
|
3777
|
-
continue;
|
|
3778
|
-
}
|
|
3779
|
-
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3780
|
-
const isNewDay = !prevAxisTickTime || prevAxisTickTime.getDate() !== tickTime.getDate() || prevAxisTickTime.getMonth() !== tickTime.getMonth() || prevAxisTickTime.getFullYear() !== tickTime.getFullYear();
|
|
3781
|
-
const timeLabel = axisIntraday && !isNewDay ? tickTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false }) : tickTime.toLocaleDateString(void 0, { month: "short", day: "numeric" });
|
|
3782
|
-
prevAxisTickTime = tickTime;
|
|
3883
|
+
{
|
|
3783
3884
|
const prevFont = ctx.font;
|
|
3784
3885
|
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3785
|
-
|
|
3886
|
+
for (const tick of timeTicks) {
|
|
3887
|
+
const tickTime = getTimeForIndex(tick.index);
|
|
3888
|
+
if (!tickTime) continue;
|
|
3889
|
+
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 });
|
|
3890
|
+
drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
|
|
3891
|
+
}
|
|
3786
3892
|
ctx.font = prevFont;
|
|
3787
3893
|
}
|
|
3788
3894
|
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
3789
3895
|
const stepMs = getTimeStepMs();
|
|
3790
|
-
const
|
|
3791
|
-
const countdownMs =
|
|
3896
|
+
const elapsedMs = Date.now() - lastPoint.time.getTime();
|
|
3897
|
+
const countdownMs = elapsedMs >= 0 ? stepMs - elapsedMs % stepMs : Math.min(stepMs, stepMs - elapsedMs);
|
|
3792
3898
|
const countdownText = formatDuration(countdownMs);
|
|
3793
3899
|
const prevFont = ctx.font;
|
|
3794
3900
|
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3795
3901
|
drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
|
|
3796
3902
|
ctx.font = prevFont;
|
|
3797
3903
|
}
|
|
3798
|
-
if (
|
|
3799
|
-
|
|
3800
|
-
|
|
3801
|
-
|
|
3802
|
-
|
|
3803
|
-
|
|
3804
|
-
|
|
3805
|
-
|
|
3806
|
-
|
|
3807
|
-
|
|
3808
|
-
|
|
3809
|
-
|
|
3810
|
-
|
|
3811
|
-
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
3815
|
-
|
|
3816
|
-
|
|
3817
|
-
|
|
3818
|
-
|
|
3819
|
-
|
|
3820
|
-
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3904
|
+
if (baseCtx) {
|
|
3905
|
+
if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
|
|
3906
|
+
baseCanvas.width = canvas.width;
|
|
3907
|
+
baseCanvas.height = canvas.height;
|
|
3908
|
+
}
|
|
3909
|
+
baseCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
3910
|
+
baseCtx.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
|
|
3911
|
+
baseCtx.drawImage(canvas, 0, 0);
|
|
3912
|
+
}
|
|
3913
|
+
overlayLayout = {
|
|
3914
|
+
chartLeft,
|
|
3915
|
+
chartTop,
|
|
3916
|
+
chartRight,
|
|
3917
|
+
chartBottom,
|
|
3918
|
+
chartWidth,
|
|
3919
|
+
chartHeight,
|
|
3920
|
+
fullChartBottom,
|
|
3921
|
+
yMin,
|
|
3922
|
+
yRange,
|
|
3923
|
+
xStart,
|
|
3924
|
+
xSpan
|
|
3925
|
+
};
|
|
3926
|
+
paintCrosshairOverlay();
|
|
3927
|
+
};
|
|
3928
|
+
const paintCrosshairOverlay = () => {
|
|
3929
|
+
crosshairPriceActionRegion = null;
|
|
3930
|
+
const layout = overlayLayout;
|
|
3931
|
+
const crosshair = resolvedCrosshair;
|
|
3932
|
+
if (!layout || !crosshair.visible || !crosshairPoint) {
|
|
3933
|
+
return;
|
|
3934
|
+
}
|
|
3935
|
+
const { chartLeft, chartTop, chartRight, chartBottom, chartWidth, chartHeight, fullChartBottom, yMin, yRange, xStart, xSpan: xSpan2 } = layout;
|
|
3936
|
+
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3937
|
+
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
3938
|
+
ctx.save();
|
|
3939
|
+
ctx.beginPath();
|
|
3940
|
+
ctx.rect(chartLeft, chartTop, chartWidth, Math.max(0, chartBottom - chartTop));
|
|
3941
|
+
ctx.clip();
|
|
3942
|
+
if (crosshair.mode === "dot") {
|
|
3943
|
+
ctx.fillStyle = crosshair.color;
|
|
3944
|
+
ctx.beginPath();
|
|
3945
|
+
ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
|
|
3946
|
+
ctx.fill();
|
|
3947
|
+
} else {
|
|
3948
|
+
ctx.strokeStyle = crosshair.color;
|
|
3949
|
+
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3950
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3951
|
+
if (crosshair.showHorizontal) {
|
|
3952
|
+
ctx.beginPath();
|
|
3953
|
+
ctx.moveTo(crisp(chartLeft), crisp(cy));
|
|
3954
|
+
ctx.lineTo(crisp(chartRight), crisp(cy));
|
|
3955
|
+
ctx.stroke();
|
|
3956
|
+
}
|
|
3957
|
+
}
|
|
3958
|
+
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3959
|
+
ctx.save();
|
|
3960
|
+
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3961
|
+
ctx.textBaseline = "middle";
|
|
3962
|
+
ctx.setLineDash([]);
|
|
3963
|
+
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3964
|
+
const hintGap = 8;
|
|
3965
|
+
if (crosshair.sideHintLeft) {
|
|
3966
|
+
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3967
|
+
ctx.textAlign = "right";
|
|
3968
|
+
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3969
|
+
}
|
|
3970
|
+
if (crosshair.sideHintRight) {
|
|
3971
|
+
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3972
|
+
ctx.textAlign = "left";
|
|
3973
|
+
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3974
|
+
}
|
|
3975
|
+
ctx.restore();
|
|
3976
|
+
}
|
|
3977
|
+
ctx.restore();
|
|
3978
|
+
if (crosshair.showVertical) {
|
|
3979
|
+
ctx.save();
|
|
3980
|
+
ctx.strokeStyle = crosshair.color;
|
|
3981
|
+
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3982
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3983
|
+
ctx.beginPath();
|
|
3984
|
+
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3985
|
+
ctx.lineTo(crisp(cx), crisp(fullChartBottom));
|
|
3986
|
+
ctx.stroke();
|
|
3987
|
+
ctx.restore();
|
|
3988
|
+
}
|
|
3989
|
+
const labelPaddingX = 8;
|
|
3990
|
+
const labelHeight = 20;
|
|
3991
|
+
const labelRadius = Math.max(0, crosshair.labelBorderRadius);
|
|
3992
|
+
const labelBackground = crosshair.labelBackgroundColor;
|
|
3993
|
+
const labelTextColor = crosshair.labelTextColor;
|
|
3994
|
+
const labelBorderColor = crosshair.labelBorderColor;
|
|
3995
|
+
const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
|
|
3996
|
+
const labelBorderStyle = crosshair.labelBorderStyle;
|
|
3997
|
+
const strokeCrosshairLabel = (x, y, widthValue, heightValue = labelHeight) => {
|
|
3998
|
+
if (labelBorderWidth <= 0) {
|
|
3999
|
+
return;
|
|
4000
|
+
}
|
|
4001
|
+
ctx.save();
|
|
4002
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4003
|
+
ctx.lineWidth = labelBorderWidth;
|
|
4004
|
+
applyDashPattern(
|
|
4005
|
+
labelBorderStyle,
|
|
4006
|
+
dashPatterns.borderDotted,
|
|
4007
|
+
dashPatterns.borderDashed
|
|
4008
|
+
);
|
|
4009
|
+
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, heightValue, labelRadius);
|
|
4010
|
+
ctx.restore();
|
|
4011
|
+
};
|
|
4012
|
+
if (crosshair.showPriceLabel) {
|
|
4013
|
+
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
4014
|
+
const priceText = formatPrice(hoverPrice);
|
|
4015
|
+
const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
|
|
4016
|
+
const priceX = getRightAxisLabelX(chartRight);
|
|
4017
|
+
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
4018
|
+
ctx.fillStyle = labelBackground;
|
|
4019
|
+
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
|
|
4020
|
+
strokeCrosshairLabel(priceX, priceY, priceWidth);
|
|
4021
|
+
drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
|
|
4022
|
+
if (crosshair.showPriceActionButton) {
|
|
4023
|
+
const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
|
|
4024
|
+
const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
|
|
4025
|
+
const containerPaddingX = 5;
|
|
4026
|
+
const containerWidth = buttonSize + containerPaddingX * 2;
|
|
4027
|
+
const containerX = priceX - buttonGap - containerWidth;
|
|
4028
|
+
const containerY = priceY;
|
|
4029
|
+
const buttonX = containerX + containerPaddingX;
|
|
4030
|
+
const buttonY = priceY + (labelHeight - buttonSize) / 2;
|
|
4031
|
+
const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
|
|
4032
|
+
const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
|
|
4033
|
+
const buttonCenterX = buttonX + buttonSize / 2;
|
|
4034
|
+
const buttonCenterY = buttonY + buttonSize / 2;
|
|
4035
|
+
const containerRadius = labelRadius;
|
|
3830
4036
|
ctx.fillStyle = labelBackground;
|
|
3831
|
-
fillRoundedRect(Math.round(
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
3843
|
-
|
|
3844
|
-
|
|
3845
|
-
|
|
3846
|
-
const buttonCenterY = buttonY + buttonSize / 2;
|
|
3847
|
-
const containerRadius = labelRadius;
|
|
3848
|
-
ctx.fillStyle = labelBackground;
|
|
3849
|
-
fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
3850
|
-
if (labelBorderWidth > 0) {
|
|
3851
|
-
ctx.save();
|
|
3852
|
-
ctx.strokeStyle = labelBorderColor;
|
|
3853
|
-
ctx.lineWidth = labelBorderWidth;
|
|
3854
|
-
ctx.setLineDash([]);
|
|
3855
|
-
strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
3856
|
-
ctx.restore();
|
|
3857
|
-
}
|
|
3858
|
-
if (buttonBorderWidth > 0) {
|
|
3859
|
-
ctx.save();
|
|
3860
|
-
ctx.strokeStyle = labelBorderColor;
|
|
3861
|
-
ctx.lineWidth = buttonBorderWidth;
|
|
3862
|
-
ctx.setLineDash([]);
|
|
3863
|
-
if (crosshair.priceActionButtonRounded) {
|
|
3864
|
-
ctx.beginPath();
|
|
3865
|
-
ctx.arc(
|
|
3866
|
-
buttonCenterX,
|
|
3867
|
-
buttonCenterY,
|
|
3868
|
-
Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
|
|
3869
|
-
0,
|
|
3870
|
-
Math.PI * 2
|
|
3871
|
-
);
|
|
3872
|
-
ctx.stroke();
|
|
3873
|
-
} else {
|
|
3874
|
-
strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
|
|
3875
|
-
}
|
|
3876
|
-
ctx.restore();
|
|
3877
|
-
}
|
|
3878
|
-
if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
|
|
3879
|
-
const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
|
|
3880
|
-
const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
|
|
3881
|
-
ctx.save();
|
|
3882
|
-
ctx.strokeStyle = labelTextColor;
|
|
3883
|
-
ctx.lineWidth = plusThickness;
|
|
3884
|
-
ctx.lineCap = "round";
|
|
3885
|
-
ctx.setLineDash([]);
|
|
4037
|
+
fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
4038
|
+
if (labelBorderWidth > 0) {
|
|
4039
|
+
ctx.save();
|
|
4040
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4041
|
+
ctx.lineWidth = labelBorderWidth;
|
|
4042
|
+
ctx.setLineDash([]);
|
|
4043
|
+
strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
4044
|
+
ctx.restore();
|
|
4045
|
+
}
|
|
4046
|
+
if (buttonBorderWidth > 0) {
|
|
4047
|
+
ctx.save();
|
|
4048
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4049
|
+
ctx.lineWidth = buttonBorderWidth;
|
|
4050
|
+
ctx.setLineDash([]);
|
|
4051
|
+
if (crosshair.priceActionButtonRounded) {
|
|
3886
4052
|
ctx.beginPath();
|
|
3887
|
-
ctx.
|
|
3888
|
-
ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
|
|
3889
|
-
ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
|
|
3890
|
-
ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
|
|
3891
|
-
ctx.stroke();
|
|
3892
|
-
ctx.restore();
|
|
3893
|
-
} else {
|
|
3894
|
-
drawText(
|
|
3895
|
-
crosshair.priceActionButtonText,
|
|
4053
|
+
ctx.arc(
|
|
3896
4054
|
buttonCenterX,
|
|
3897
4055
|
buttonCenterY,
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
4056
|
+
Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
|
|
4057
|
+
0,
|
|
4058
|
+
Math.PI * 2
|
|
3901
4059
|
);
|
|
4060
|
+
ctx.stroke();
|
|
4061
|
+
} else {
|
|
4062
|
+
strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
|
|
3902
4063
|
}
|
|
3903
|
-
|
|
3904
|
-
x: containerX,
|
|
3905
|
-
y: containerY,
|
|
3906
|
-
width: containerWidth,
|
|
3907
|
-
height: labelHeight,
|
|
3908
|
-
price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
|
|
3909
|
-
};
|
|
4064
|
+
ctx.restore();
|
|
3910
4065
|
}
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
|
|
3918
|
-
|
|
3919
|
-
|
|
3920
|
-
|
|
3921
|
-
|
|
3922
|
-
ctx.
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
4066
|
+
if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
|
|
4067
|
+
const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
|
|
4068
|
+
const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
|
|
4069
|
+
ctx.save();
|
|
4070
|
+
ctx.strokeStyle = labelTextColor;
|
|
4071
|
+
ctx.lineWidth = plusThickness;
|
|
4072
|
+
ctx.lineCap = "round";
|
|
4073
|
+
ctx.setLineDash([]);
|
|
4074
|
+
ctx.beginPath();
|
|
4075
|
+
ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
|
|
4076
|
+
ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
|
|
4077
|
+
ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
|
|
4078
|
+
ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
|
|
4079
|
+
ctx.stroke();
|
|
4080
|
+
ctx.restore();
|
|
4081
|
+
} else {
|
|
4082
|
+
drawText(
|
|
4083
|
+
crosshair.priceActionButtonText,
|
|
4084
|
+
buttonCenterX,
|
|
4085
|
+
buttonCenterY,
|
|
4086
|
+
"center",
|
|
4087
|
+
"middle",
|
|
4088
|
+
labelTextColor
|
|
4089
|
+
);
|
|
3926
4090
|
}
|
|
4091
|
+
crosshairPriceActionRegion = {
|
|
4092
|
+
x: containerX,
|
|
4093
|
+
y: containerY,
|
|
4094
|
+
width: containerWidth,
|
|
4095
|
+
height: labelHeight,
|
|
4096
|
+
price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
|
|
4097
|
+
};
|
|
3927
4098
|
}
|
|
3928
4099
|
}
|
|
4100
|
+
if (crosshair.showTimeLabel) {
|
|
4101
|
+
const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
|
|
4102
|
+
const hoverIndex = Math.round(xStart + ratio * xSpan2 - 0.5);
|
|
4103
|
+
const hoverTime = getTimeForIndex(hoverIndex);
|
|
4104
|
+
if (hoverTime) {
|
|
4105
|
+
const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
|
|
4106
|
+
const timeWidth = Math.ceil(measureTextWidth(timeText)) + labelPaddingX * 2;
|
|
4107
|
+
const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
|
|
4108
|
+
const timeY = fullChartBottom + 1;
|
|
4109
|
+
const timeHeight = Math.max(labelHeight, Math.floor(height - timeY));
|
|
4110
|
+
ctx.fillStyle = labelBackground;
|
|
4111
|
+
fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, timeHeight, labelRadius);
|
|
4112
|
+
strokeCrosshairLabel(timeX, timeY, timeWidth, timeHeight);
|
|
4113
|
+
drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
|
|
4114
|
+
}
|
|
4115
|
+
}
|
|
4116
|
+
};
|
|
4117
|
+
const drawOverlayOnly = () => {
|
|
4118
|
+
if (!baseCtx || !overlayLayout || baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height || baseCanvas.width === 0) {
|
|
4119
|
+
draw({ updateAutoScale: false });
|
|
4120
|
+
return;
|
|
4121
|
+
}
|
|
4122
|
+
const pixelRatio = getPixelRatio();
|
|
4123
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4124
|
+
ctx.drawImage(baseCanvas, 0, 0);
|
|
4125
|
+
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
4126
|
+
paintCrosshairOverlay();
|
|
3929
4127
|
};
|
|
3930
4128
|
const zoomX = (factor, anchorX) => {
|
|
3931
4129
|
if (!drawState || data.length === 0) {
|
|
@@ -4016,6 +4214,59 @@ function createChart(element, options = {}) {
|
|
|
4016
4214
|
}
|
|
4017
4215
|
scheduleDraw();
|
|
4018
4216
|
};
|
|
4217
|
+
let kineticRafId = null;
|
|
4218
|
+
const panVelocitySamples = [];
|
|
4219
|
+
const cancelKineticPan = () => {
|
|
4220
|
+
if (kineticRafId !== null) {
|
|
4221
|
+
cancelAnimationFrame(kineticRafId);
|
|
4222
|
+
kineticRafId = null;
|
|
4223
|
+
}
|
|
4224
|
+
};
|
|
4225
|
+
const recordPanVelocitySample = (x) => {
|
|
4226
|
+
const now = performance.now();
|
|
4227
|
+
panVelocitySamples.push({ t: now, x });
|
|
4228
|
+
while (panVelocitySamples.length > 0 && now - panVelocitySamples[0].t > 120) {
|
|
4229
|
+
panVelocitySamples.shift();
|
|
4230
|
+
}
|
|
4231
|
+
};
|
|
4232
|
+
const startKineticPan = (initialVelocityPxPerMs) => {
|
|
4233
|
+
cancelKineticPan();
|
|
4234
|
+
let velocity = initialVelocityPxPerMs;
|
|
4235
|
+
let lastTime = performance.now();
|
|
4236
|
+
const stepFn = (now) => {
|
|
4237
|
+
kineticRafId = null;
|
|
4238
|
+
const dt = Math.min(64, Math.max(1, now - lastTime));
|
|
4239
|
+
lastTime = now;
|
|
4240
|
+
const centerBefore = xCenter;
|
|
4241
|
+
pan(velocity * dt, 0, true, false);
|
|
4242
|
+
velocity *= Math.exp(-dt / 325);
|
|
4243
|
+
if (Math.abs(velocity) < 0.02 || xCenter === centerBefore) {
|
|
4244
|
+
return;
|
|
4245
|
+
}
|
|
4246
|
+
kineticRafId = requestAnimationFrame(stepFn);
|
|
4247
|
+
};
|
|
4248
|
+
kineticRafId = requestAnimationFrame(stepFn);
|
|
4249
|
+
};
|
|
4250
|
+
const maybeStartKineticPan = (pointerType) => {
|
|
4251
|
+
const config = mergedOptions.kineticScroll ?? DEFAULT_OPTIONS.kineticScroll;
|
|
4252
|
+
const enabled = pointerType === "touch" || pointerType === "pen" ? config?.touch !== false : config?.mouse === true;
|
|
4253
|
+
if (!enabled || panVelocitySamples.length < 2) {
|
|
4254
|
+
panVelocitySamples.length = 0;
|
|
4255
|
+
return;
|
|
4256
|
+
}
|
|
4257
|
+
const first = panVelocitySamples[0];
|
|
4258
|
+
const last = panVelocitySamples[panVelocitySamples.length - 1];
|
|
4259
|
+
panVelocitySamples.length = 0;
|
|
4260
|
+
const elapsed = last.t - first.t;
|
|
4261
|
+
if (elapsed <= 0 || performance.now() - last.t > 100) {
|
|
4262
|
+
return;
|
|
4263
|
+
}
|
|
4264
|
+
const velocity = (last.x - first.x) / elapsed;
|
|
4265
|
+
if (Math.abs(velocity) < 0.1) {
|
|
4266
|
+
return;
|
|
4267
|
+
}
|
|
4268
|
+
startKineticPan(clamp(velocity, -4, 4));
|
|
4269
|
+
};
|
|
4019
4270
|
const resetYViewport = () => {
|
|
4020
4271
|
yMinOverride = null;
|
|
4021
4272
|
yMaxOverride = null;
|
|
@@ -4075,6 +4326,7 @@ function createChart(element, options = {}) {
|
|
|
4075
4326
|
scheduleDraw();
|
|
4076
4327
|
};
|
|
4077
4328
|
const resetViewport = () => {
|
|
4329
|
+
cancelKineticPan();
|
|
4078
4330
|
fitXViewport();
|
|
4079
4331
|
resetYViewport();
|
|
4080
4332
|
updateFollowLatest(true);
|
|
@@ -4084,6 +4336,7 @@ function createChart(element, options = {}) {
|
|
|
4084
4336
|
const isFollowingLatest = () => followLatest;
|
|
4085
4337
|
const setFollowingLatest = (follow) => {
|
|
4086
4338
|
if (follow && data.length > 0) {
|
|
4339
|
+
cancelKineticPan();
|
|
4087
4340
|
xCenter = data.length - xSpan / 2 + rightEdgePaddingBars;
|
|
4088
4341
|
clampXViewport();
|
|
4089
4342
|
updateFollowLatest(true);
|
|
@@ -4453,7 +4706,7 @@ function createChart(element, options = {}) {
|
|
|
4453
4706
|
const prevFont = ctx.font;
|
|
4454
4707
|
ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
|
|
4455
4708
|
const lines = (drawing.label ?? "").split("\n");
|
|
4456
|
-
const textW = Math.max(1, ...lines.map((line) =>
|
|
4709
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
4457
4710
|
ctx.font = prevFont;
|
|
4458
4711
|
const isNote = drawing.type === "note";
|
|
4459
4712
|
const padX = isNote ? 8 : 2;
|
|
@@ -4926,16 +5179,24 @@ function createChart(element, options = {}) {
|
|
|
4926
5179
|
emitViewportChange();
|
|
4927
5180
|
scheduleDraw();
|
|
4928
5181
|
};
|
|
5182
|
+
const capturePointer = (pointerId) => {
|
|
5183
|
+
try {
|
|
5184
|
+
canvas.setPointerCapture(pointerId);
|
|
5185
|
+
} catch {
|
|
5186
|
+
}
|
|
5187
|
+
};
|
|
4929
5188
|
const onPointerDown = (event) => {
|
|
4930
5189
|
if (event.pointerType === "touch" || event.pointerType === "pen") {
|
|
4931
5190
|
event.preventDefault();
|
|
4932
5191
|
}
|
|
5192
|
+
cancelKineticPan();
|
|
5193
|
+
panVelocitySamples.length = 0;
|
|
4933
5194
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4934
5195
|
shiftKeyActive = event.shiftKey;
|
|
4935
5196
|
const point = getCanvasPoint(event);
|
|
4936
5197
|
if (event.pointerType === "touch") {
|
|
4937
5198
|
touchPointers.set(event.pointerId, point);
|
|
4938
|
-
|
|
5199
|
+
capturePointer(event.pointerId);
|
|
4939
5200
|
const touchPair = getTouchPair();
|
|
4940
5201
|
if (touchPair) {
|
|
4941
5202
|
beginPinchZoom(touchPair[0], touchPair[1]);
|
|
@@ -4965,7 +5226,7 @@ function createChart(element, options = {}) {
|
|
|
4965
5226
|
lastPrice: startPrice,
|
|
4966
5227
|
moved: false
|
|
4967
5228
|
};
|
|
4968
|
-
|
|
5229
|
+
capturePointer(event.pointerId);
|
|
4969
5230
|
canvas.style.cursor = "ns-resize";
|
|
4970
5231
|
setCrosshairPoint(null);
|
|
4971
5232
|
return;
|
|
@@ -4986,7 +5247,7 @@ function createChart(element, options = {}) {
|
|
|
4986
5247
|
startPrice: orderDragRegion.price,
|
|
4987
5248
|
lastPrice: orderDragRegion.price
|
|
4988
5249
|
};
|
|
4989
|
-
|
|
5250
|
+
capturePointer(event.pointerId);
|
|
4990
5251
|
canvas.style.cursor = "ns-resize";
|
|
4991
5252
|
setCrosshairPoint(null);
|
|
4992
5253
|
return;
|
|
@@ -5018,7 +5279,7 @@ function createChart(element, options = {}) {
|
|
|
5018
5279
|
startPoints: drawingHit.drawing.points.map((drawingPoint) => ({ ...drawingPoint }))
|
|
5019
5280
|
};
|
|
5020
5281
|
activePointerId = event.pointerId;
|
|
5021
|
-
|
|
5282
|
+
capturePointer(event.pointerId);
|
|
5022
5283
|
}
|
|
5023
5284
|
}
|
|
5024
5285
|
setCrosshairPoint(null);
|
|
@@ -5046,7 +5307,7 @@ function createChart(element, options = {}) {
|
|
|
5046
5307
|
};
|
|
5047
5308
|
lastPointerX = point.x;
|
|
5048
5309
|
lastPointerY = point.y;
|
|
5049
|
-
|
|
5310
|
+
capturePointer(event.pointerId);
|
|
5050
5311
|
if (crosshairDrag) {
|
|
5051
5312
|
canvas.style.cursor = "crosshair";
|
|
5052
5313
|
setCrosshairPoint(point);
|
|
@@ -5232,6 +5493,7 @@ function createChart(element, options = {}) {
|
|
|
5232
5493
|
const deltaY = point.y - lastPointerY;
|
|
5233
5494
|
if (dragMode === "plot") {
|
|
5234
5495
|
canvas.style.cursor = "grabbing";
|
|
5496
|
+
recordPanVelocitySample(point.x);
|
|
5235
5497
|
pan(deltaX, deltaY, true, true);
|
|
5236
5498
|
setCrosshairPoint(null);
|
|
5237
5499
|
} else if (dragMode === "x-axis") {
|
|
@@ -5305,10 +5567,16 @@ function createChart(element, options = {}) {
|
|
|
5305
5567
|
drawingDragState = null;
|
|
5306
5568
|
emitDrawingsChange();
|
|
5307
5569
|
}
|
|
5570
|
+
const endedPlotDrag = isDragging && dragMode === "plot";
|
|
5308
5571
|
isDragging = false;
|
|
5309
5572
|
dragMode = null;
|
|
5310
5573
|
activePointerId = null;
|
|
5311
5574
|
canvas.style.cursor = "default";
|
|
5575
|
+
if (endedPlotDrag) {
|
|
5576
|
+
maybeStartKineticPan(event?.pointerType);
|
|
5577
|
+
} else {
|
|
5578
|
+
panVelocitySamples.length = 0;
|
|
5579
|
+
}
|
|
5312
5580
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
5313
5581
|
if (pointerDownInfo.crosshairDrag) {
|
|
5314
5582
|
const point = getCanvasPoint(event);
|
|
@@ -5346,6 +5614,7 @@ function createChart(element, options = {}) {
|
|
|
5346
5614
|
if (!drawState) {
|
|
5347
5615
|
return;
|
|
5348
5616
|
}
|
|
5617
|
+
cancelKineticPan();
|
|
5349
5618
|
const point = getCanvasPoint(event);
|
|
5350
5619
|
const region = getHitRegion(point.x, point.y);
|
|
5351
5620
|
if (region === "outside") {
|
|
@@ -5445,6 +5714,7 @@ function createChart(element, options = {}) {
|
|
|
5445
5714
|
width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
|
|
5446
5715
|
height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
|
|
5447
5716
|
mergedOptions = { ...mergedOptions, width, height };
|
|
5717
|
+
refreshResolvedOptions();
|
|
5448
5718
|
resetRightMarginCache();
|
|
5449
5719
|
doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
5450
5720
|
doubleClickAction = mergedOptions.doubleClickAction;
|
|
@@ -5789,6 +6059,7 @@ function createChart(element, options = {}) {
|
|
|
5789
6059
|
drawingHoverHandler = handler;
|
|
5790
6060
|
};
|
|
5791
6061
|
const destroy = () => {
|
|
6062
|
+
cancelKineticPan();
|
|
5792
6063
|
if (smoothingRafId !== null) {
|
|
5793
6064
|
cancelAnimationFrame(smoothingRafId);
|
|
5794
6065
|
smoothingRafId = null;
|