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
|
@@ -189,8 +189,13 @@ 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 },
|
|
198
|
+
timeStepMs: 0,
|
|
194
199
|
pinOutOfRangeLines: false,
|
|
195
200
|
doubleClickEnabled: true,
|
|
196
201
|
doubleClickAction: "reset",
|
|
@@ -589,6 +594,26 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
|
|
|
589
594
|
}
|
|
590
595
|
ctx.restore();
|
|
591
596
|
};
|
|
597
|
+
var rangeOfSeries = (seriesList, startIndex, endIndex, skipIndex) => {
|
|
598
|
+
let min = Number.POSITIVE_INFINITY;
|
|
599
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
600
|
+
for (const series of seriesList) {
|
|
601
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
602
|
+
if (idx === skipIndex) continue;
|
|
603
|
+
const value = series[idx];
|
|
604
|
+
if (value == null || !Number.isFinite(value)) continue;
|
|
605
|
+
if (value < min) min = value;
|
|
606
|
+
if (value > max) max = value;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return min <= max ? { min, max } : null;
|
|
610
|
+
};
|
|
611
|
+
var maAutoscaleRange = (cacheKeyPrefix, compute, defaultLength) => (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
612
|
+
const length = clampIndicatorLength(inputs.length, defaultLength);
|
|
613
|
+
const source = inputs.source ?? "close";
|
|
614
|
+
const series = withCachedSeries(`${cacheKeyPrefix}|${length}|${source}`, data, () => compute(data, length, source));
|
|
615
|
+
return rangeOfSeries([series], startIndex, endIndex, skipIndex);
|
|
616
|
+
};
|
|
592
617
|
var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
|
|
593
618
|
if (!renderContext.yFromPrice || opacity <= 0) return;
|
|
594
619
|
const yFromPrice = renderContext.yFromPrice;
|
|
@@ -806,7 +831,8 @@ var BUILTIN_SMA_INDICATOR = {
|
|
|
806
831
|
() => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
807
832
|
);
|
|
808
833
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
|
|
809
|
-
}
|
|
834
|
+
},
|
|
835
|
+
getAutoscaleRange: maAutoscaleRange("sma", computeSmaSeries, 20)
|
|
810
836
|
};
|
|
811
837
|
var BUILTIN_EMA_INDICATOR = {
|
|
812
838
|
id: "ema",
|
|
@@ -821,7 +847,8 @@ var BUILTIN_EMA_INDICATOR = {
|
|
|
821
847
|
() => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
822
848
|
);
|
|
823
849
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
824
|
-
}
|
|
850
|
+
},
|
|
851
|
+
getAutoscaleRange: maAutoscaleRange("ema", computeEmaSeries, 20)
|
|
825
852
|
};
|
|
826
853
|
var BUILTIN_WMA_INDICATOR = {
|
|
827
854
|
id: "wma",
|
|
@@ -836,7 +863,8 @@ var BUILTIN_WMA_INDICATOR = {
|
|
|
836
863
|
() => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
837
864
|
);
|
|
838
865
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
|
|
839
|
-
}
|
|
866
|
+
},
|
|
867
|
+
getAutoscaleRange: maAutoscaleRange("wma", computeWmaSeries, 20)
|
|
840
868
|
};
|
|
841
869
|
var BUILTIN_VWMA_INDICATOR = {
|
|
842
870
|
id: "vwma",
|
|
@@ -851,7 +879,8 @@ var BUILTIN_VWMA_INDICATOR = {
|
|
|
851
879
|
() => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
852
880
|
);
|
|
853
881
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
|
|
854
|
-
}
|
|
882
|
+
},
|
|
883
|
+
getAutoscaleRange: maAutoscaleRange("vwma", computeVwmaSeries, 20)
|
|
855
884
|
};
|
|
856
885
|
var BUILTIN_RMA_INDICATOR = {
|
|
857
886
|
id: "rma",
|
|
@@ -866,7 +895,8 @@ var BUILTIN_RMA_INDICATOR = {
|
|
|
866
895
|
() => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
867
896
|
);
|
|
868
897
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
|
|
869
|
-
}
|
|
898
|
+
},
|
|
899
|
+
getAutoscaleRange: maAutoscaleRange("rma", computeRmaSeries, 14)
|
|
870
900
|
};
|
|
871
901
|
var BUILTIN_HMA_INDICATOR = {
|
|
872
902
|
id: "hma",
|
|
@@ -881,7 +911,8 @@ var BUILTIN_HMA_INDICATOR = {
|
|
|
881
911
|
() => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
|
|
882
912
|
);
|
|
883
913
|
drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
|
|
884
|
-
}
|
|
914
|
+
},
|
|
915
|
+
getAutoscaleRange: maAutoscaleRange("hma", computeHmaSeries, 21)
|
|
885
916
|
};
|
|
886
917
|
var BUILTIN_VWAP_INDICATOR = {
|
|
887
918
|
id: "vwap",
|
|
@@ -922,6 +953,27 @@ var BUILTIN_VWAP_INDICATOR = {
|
|
|
922
953
|
drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
|
|
923
954
|
}
|
|
924
955
|
drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
|
|
956
|
+
},
|
|
957
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
958
|
+
const vwap = withCachedSeries("vwap|line", data, () => computeVwapSeries(data, "vwap"));
|
|
959
|
+
if (!inputs.showBands) {
|
|
960
|
+
return rangeOfSeries([vwap], startIndex, endIndex, skipIndex);
|
|
961
|
+
}
|
|
962
|
+
const sigma = withCachedSeries("vwap|sigma", data, () => computeVwapSeries(data, "sigma"));
|
|
963
|
+
const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
|
|
964
|
+
let min = Number.POSITIVE_INFINITY;
|
|
965
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
966
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
967
|
+
if (idx === skipIndex) continue;
|
|
968
|
+
const value = vwap[idx];
|
|
969
|
+
const s = sigma[idx];
|
|
970
|
+
if (value == null || s == null) continue;
|
|
971
|
+
const lo = value - multiplier * s;
|
|
972
|
+
const hi = value + multiplier * s;
|
|
973
|
+
if (lo < min) min = lo;
|
|
974
|
+
if (hi > max) max = hi;
|
|
975
|
+
}
|
|
976
|
+
return min <= max ? { min, max } : null;
|
|
925
977
|
}
|
|
926
978
|
};
|
|
927
979
|
var BUILTIN_BOLLINGER_INDICATOR = {
|
|
@@ -963,7 +1015,23 @@ var BUILTIN_BOLLINGER_INDICATOR = {
|
|
|
963
1015
|
fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
|
|
964
1016
|
drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
|
|
965
1017
|
drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
|
|
966
|
-
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#
|
|
1018
|
+
drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#ec4899", width);
|
|
1019
|
+
},
|
|
1020
|
+
getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
|
|
1021
|
+
const length = clampIndicatorLength(inputs.length, 20);
|
|
1022
|
+
const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
|
|
1023
|
+
const source = inputs.source ?? "close";
|
|
1024
|
+
const upper = withCachedSeries(
|
|
1025
|
+
`bb|upper|${length}|${multiplier}|${source}`,
|
|
1026
|
+
data,
|
|
1027
|
+
() => computeBollingerSeries(data, length, multiplier, source, "upper")
|
|
1028
|
+
);
|
|
1029
|
+
const lower = withCachedSeries(
|
|
1030
|
+
`bb|lower|${length}|${multiplier}|${source}`,
|
|
1031
|
+
data,
|
|
1032
|
+
() => computeBollingerSeries(data, length, multiplier, source, "lower")
|
|
1033
|
+
);
|
|
1034
|
+
return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
|
|
967
1035
|
}
|
|
968
1036
|
};
|
|
969
1037
|
var BUILTIN_STDDEV_INDICATOR = {
|
|
@@ -1056,6 +1124,20 @@ var BUILTIN_INDICATORS = [
|
|
|
1056
1124
|
];
|
|
1057
1125
|
function createChart(element, options = {}) {
|
|
1058
1126
|
let mergedOptions = mergeChartOptions(DEFAULT_OPTIONS, options);
|
|
1127
|
+
let resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1128
|
+
let resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1129
|
+
let resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1130
|
+
let resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1131
|
+
let resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
|
|
1132
|
+
let resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1133
|
+
const refreshResolvedOptions = () => {
|
|
1134
|
+
resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
|
|
1135
|
+
resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
|
|
1136
|
+
resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
|
|
1137
|
+
resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
|
|
1138
|
+
resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
|
|
1139
|
+
resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
|
|
1140
|
+
};
|
|
1059
1141
|
let width = mergedOptions.width;
|
|
1060
1142
|
let height = mergedOptions.height;
|
|
1061
1143
|
let data = [];
|
|
@@ -1092,8 +1174,11 @@ function createChart(element, options = {}) {
|
|
|
1092
1174
|
pane: indicator.pane ?? plugin?.pane ?? "overlay",
|
|
1093
1175
|
...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
|
|
1094
1176
|
zIndex: Math.round(Number(indicator.zIndex) || 0),
|
|
1095
|
-
|
|
1096
|
-
|
|
1177
|
+
// Overlay series participate in autoscale by default (lightweight-charts
|
|
1178
|
+
// semantics): bands and MAs expand the price range instead of clipping
|
|
1179
|
+
// at the chart edge. Hosts can still opt out per indicator.
|
|
1180
|
+
excludeFromAutoscale: indicator.excludeFromAutoscale ?? false,
|
|
1181
|
+
overlayScaleWeight: indicator.overlayScaleWeight === void 0 ? 1 : Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0)),
|
|
1097
1182
|
inputs: {
|
|
1098
1183
|
...defaults,
|
|
1099
1184
|
...indicator.inputs ?? {}
|
|
@@ -1295,6 +1380,9 @@ function createChart(element, options = {}) {
|
|
|
1295
1380
|
canvas.setAttribute("draggable", "false");
|
|
1296
1381
|
element.innerHTML = "";
|
|
1297
1382
|
element.appendChild(canvas);
|
|
1383
|
+
const baseCanvas = document.createElement("canvas");
|
|
1384
|
+
const baseCtx = baseCanvas.getContext("2d");
|
|
1385
|
+
let overlayLayout = null;
|
|
1298
1386
|
const margin = { top: 16, right: 72, bottom: 26, left: 12 };
|
|
1299
1387
|
let cachedRightMargin = margin.right;
|
|
1300
1388
|
const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
|
|
@@ -1334,6 +1422,20 @@ function createChart(element, options = {}) {
|
|
|
1334
1422
|
const clamp = (value, min, max) => {
|
|
1335
1423
|
return Math.min(max, Math.max(min, value));
|
|
1336
1424
|
};
|
|
1425
|
+
const textWidthCache = /* @__PURE__ */ new Map();
|
|
1426
|
+
const measureTextWidth = (text) => {
|
|
1427
|
+
const key = `${ctx.font}\0${text}`;
|
|
1428
|
+
const cached = textWidthCache.get(key);
|
|
1429
|
+
if (cached !== void 0) {
|
|
1430
|
+
return cached;
|
|
1431
|
+
}
|
|
1432
|
+
const width2 = ctx.measureText(text).width;
|
|
1433
|
+
if (textWidthCache.size >= 4096) {
|
|
1434
|
+
textWidthCache.clear();
|
|
1435
|
+
}
|
|
1436
|
+
textWidthCache.set(key, width2);
|
|
1437
|
+
return width2;
|
|
1438
|
+
};
|
|
1337
1439
|
const resetLabelSlots = (enabled) => {
|
|
1338
1440
|
noOverlappingLineLabels = enabled;
|
|
1339
1441
|
rightAxisLabelSlots = [];
|
|
@@ -1536,7 +1638,7 @@ function createChart(element, options = {}) {
|
|
|
1536
1638
|
return `${integerPart}${decimalPart}`;
|
|
1537
1639
|
};
|
|
1538
1640
|
const getMeasuredLabelWidth = (text, paddingX) => {
|
|
1539
|
-
return Math.ceil(
|
|
1641
|
+
return Math.ceil(measureTextWidth(text)) + paddingX * 2;
|
|
1540
1642
|
};
|
|
1541
1643
|
const getStabilizedNumericLabelWidth = (text, paddingX) => {
|
|
1542
1644
|
const measured = getMeasuredLabelWidth(text, paddingX);
|
|
@@ -1593,11 +1695,16 @@ function createChart(element, options = {}) {
|
|
|
1593
1695
|
return Array.from(dedupedByTime.values()).sort((a, b) => a.time.getTime() - b.time.getTime());
|
|
1594
1696
|
};
|
|
1595
1697
|
const getTimeStepMs = () => {
|
|
1698
|
+
const configured = Number(mergedOptions.timeStepMs);
|
|
1699
|
+
if (Number.isFinite(configured) && configured > 0) {
|
|
1700
|
+
return configured;
|
|
1701
|
+
}
|
|
1596
1702
|
if (data.length < 2) {
|
|
1597
1703
|
return 24 * 60 * 60 * 1e3;
|
|
1598
1704
|
}
|
|
1599
1705
|
const deltas = [];
|
|
1600
|
-
|
|
1706
|
+
const first = Math.max(1, data.length - 40);
|
|
1707
|
+
for (let index = first; index < data.length; index += 1) {
|
|
1601
1708
|
const previous = data[index - 1];
|
|
1602
1709
|
const current = data[index];
|
|
1603
1710
|
if (!previous || !current) {
|
|
@@ -1777,11 +1884,11 @@ function createChart(element, options = {}) {
|
|
|
1777
1884
|
return;
|
|
1778
1885
|
}
|
|
1779
1886
|
const labelText = mergedLine.label ?? formatPrice(mergedLine.price);
|
|
1780
|
-
const axis =
|
|
1887
|
+
const axis = resolvedAxis;
|
|
1781
1888
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1782
1889
|
const labelPaddingX = 8;
|
|
1783
1890
|
const labelHeight = 20;
|
|
1784
|
-
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(
|
|
1891
|
+
const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(measureTextWidth(labelText)) + labelPaddingX * 2;
|
|
1785
1892
|
const labelWidth = getRightAxisLabelWidth(chartRight, measuredLabelWidth);
|
|
1786
1893
|
const labelX = getRightAxisLabelX(chartRight);
|
|
1787
1894
|
const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
|
|
@@ -1876,7 +1983,7 @@ function createChart(element, options = {}) {
|
|
|
1876
1983
|
const closeAction = mergedLine.type === "market" ? "close" : "cancel";
|
|
1877
1984
|
const showCloseButton = mergedLine.showCloseButton;
|
|
1878
1985
|
const actionButtonText = mergedLine.actionButtonText;
|
|
1879
|
-
const axis =
|
|
1986
|
+
const axis = resolvedAxis;
|
|
1880
1987
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
1881
1988
|
const legacyButton = actionButtonText ? [
|
|
1882
1989
|
{
|
|
@@ -1898,7 +2005,7 @@ function createChart(element, options = {}) {
|
|
|
1898
2005
|
const actionButtonMetrics = actionButtons.map((button) => {
|
|
1899
2006
|
const paddingX = Math.max(2, button.paddingX ?? mergedLine.actionButtonPaddingX);
|
|
1900
2007
|
const minWidth = Math.max(16, button.minWidth ?? mergedLine.actionButtonMinWidth);
|
|
1901
|
-
const width2 = Math.max(minWidth, Math.ceil(
|
|
2008
|
+
const width2 = Math.max(minWidth, Math.ceil(measureTextWidth(button.text)) + paddingX * 2);
|
|
1902
2009
|
return { button, width: width2 };
|
|
1903
2010
|
});
|
|
1904
2011
|
const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
|
|
@@ -1906,8 +2013,8 @@ function createChart(element, options = {}) {
|
|
|
1906
2013
|
const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
|
|
1907
2014
|
const segmentPaddingX = 8;
|
|
1908
2015
|
const labelHeight = 22;
|
|
1909
|
-
const qtyWidth = qtyText ? Math.ceil(
|
|
1910
|
-
const centerMeasuredWidth = Math.ceil(
|
|
2016
|
+
const qtyWidth = qtyText ? Math.ceil(measureTextWidth(qtyText)) + segmentPaddingX * 2 : 0;
|
|
2017
|
+
const centerMeasuredWidth = Math.ceil(measureTextWidth(centerText)) + segmentPaddingX * 2;
|
|
1911
2018
|
const centerWidth = mergedLine.id === void 0 ? centerMeasuredWidth : Math.max(centerMeasuredWidth, orderWidgetWidthById.get(mergedLine.id) ?? 0);
|
|
1912
2019
|
if (mergedLine.id) {
|
|
1913
2020
|
orderWidgetWidthById.set(mergedLine.id, centerWidth);
|
|
@@ -1915,7 +2022,7 @@ function createChart(element, options = {}) {
|
|
|
1915
2022
|
const closeWidth = showCloseButton ? 24 : 0;
|
|
1916
2023
|
const mainWidgetWidth = qtyWidth + centerWidth + closeWidth;
|
|
1917
2024
|
const totalWidth = mainWidgetWidth + actionButtonsGap + actionButtonsTotalWidth;
|
|
1918
|
-
const crosshair =
|
|
2025
|
+
const crosshair = resolvedCrosshair;
|
|
1919
2026
|
const crosshairActionInset = crosshair.visible && crosshair.showPriceLabel && crosshair.showPriceActionButton ? Math.max(
|
|
1920
2027
|
0,
|
|
1921
2028
|
Math.round(crosshair.priceActionButtonGap) + (clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4)) + 10) - 4
|
|
@@ -2096,20 +2203,29 @@ function createChart(element, options = {}) {
|
|
|
2096
2203
|
}
|
|
2097
2204
|
}
|
|
2098
2205
|
crosshairPoint = point;
|
|
2099
|
-
scheduleDraw();
|
|
2206
|
+
scheduleDraw({ overlayOnly: true });
|
|
2100
2207
|
};
|
|
2101
2208
|
let drawRafId = null;
|
|
2102
2209
|
let pendingDrawUpdateAutoScale = false;
|
|
2210
|
+
let pendingDrawOverlayOnly = true;
|
|
2103
2211
|
const scheduleDraw = (options2 = {}) => {
|
|
2104
|
-
|
|
2212
|
+
const overlayOnly = options2.overlayOnly ?? false;
|
|
2213
|
+
pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
|
|
2214
|
+
pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
|
|
2105
2215
|
if (drawRafId !== null) {
|
|
2106
2216
|
return;
|
|
2107
2217
|
}
|
|
2108
2218
|
drawRafId = requestAnimationFrame(() => {
|
|
2109
2219
|
drawRafId = null;
|
|
2110
2220
|
const updateAutoScale = pendingDrawUpdateAutoScale;
|
|
2221
|
+
const flushOverlayOnly = pendingDrawOverlayOnly;
|
|
2111
2222
|
pendingDrawUpdateAutoScale = false;
|
|
2112
|
-
|
|
2223
|
+
pendingDrawOverlayOnly = true;
|
|
2224
|
+
if (flushOverlayOnly) {
|
|
2225
|
+
drawOverlayOnly();
|
|
2226
|
+
} else {
|
|
2227
|
+
draw({ updateAutoScale });
|
|
2228
|
+
}
|
|
2113
2229
|
});
|
|
2114
2230
|
};
|
|
2115
2231
|
const draw = (options2 = {}) => {
|
|
@@ -2123,15 +2239,15 @@ function createChart(element, options = {}) {
|
|
|
2123
2239
|
canvas.width = Math.floor(width * pixelRatio);
|
|
2124
2240
|
canvas.height = Math.floor(height * pixelRatio);
|
|
2125
2241
|
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
2126
|
-
const axis =
|
|
2127
|
-
const xAxis =
|
|
2128
|
-
const yAxis =
|
|
2242
|
+
const axis = resolvedAxis;
|
|
2243
|
+
const xAxis = resolvedXAxis;
|
|
2244
|
+
const yAxis = resolvedYAxis;
|
|
2129
2245
|
const xAxisFontSize = Math.max(8, xAxis.fontSize);
|
|
2130
2246
|
const yAxisFontSize = Math.max(8, yAxis.fontSize);
|
|
2131
2247
|
ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
|
|
2132
2248
|
ctx.fillStyle = mergedOptions.backgroundColor;
|
|
2133
2249
|
ctx.fillRect(0, 0, width, height);
|
|
2134
|
-
const labels =
|
|
2250
|
+
const labels = resolvedLabels;
|
|
2135
2251
|
const estimateRightMargin = () => {
|
|
2136
2252
|
const paddingX = Math.max(4, labels.labelPaddingX);
|
|
2137
2253
|
let required = margin.right - 1;
|
|
@@ -2244,6 +2360,7 @@ function createChart(element, options = {}) {
|
|
|
2244
2360
|
yMax: 0
|
|
2245
2361
|
};
|
|
2246
2362
|
drawText("Load OHLC data to render chart", chartLeft + 10, chartTop + 20, "left", "middle", "#64748b");
|
|
2363
|
+
overlayLayout = null;
|
|
2247
2364
|
return;
|
|
2248
2365
|
}
|
|
2249
2366
|
clampXViewport();
|
|
@@ -2251,62 +2368,39 @@ function createChart(element, options = {}) {
|
|
|
2251
2368
|
const xEnd = xStart + xSpan;
|
|
2252
2369
|
const startIndex = Math.max(0, Math.floor(xStart));
|
|
2253
2370
|
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
|
-
}
|
|
2371
|
+
const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
|
|
2372
|
+
const scanCandleRange = (from, to) => {
|
|
2373
|
+
let min = Number.POSITIVE_INFINITY;
|
|
2374
|
+
let max = Number.NEGATIVE_INFINITY;
|
|
2375
|
+
for (let idx = from; idx <= to; idx += 1) {
|
|
2376
|
+
if (idx === skipLatestIndex) continue;
|
|
2377
|
+
const point = data[idx];
|
|
2378
|
+
if (!point) continue;
|
|
2379
|
+
if (point.l < min) min = point.l;
|
|
2380
|
+
if (point.h > max) max = point.h;
|
|
2268
2381
|
}
|
|
2382
|
+
return min <= max ? { min, max } : null;
|
|
2383
|
+
};
|
|
2384
|
+
let candleRange = scanCandleRange(startIndex, endIndex);
|
|
2385
|
+
if (!candleRange) {
|
|
2386
|
+
const latestIndex = data.length - 1;
|
|
2387
|
+
candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? { min: data[latestIndex].l, max: data[latestIndex].h };
|
|
2269
2388
|
}
|
|
2270
|
-
let minPrice =
|
|
2271
|
-
let maxPrice =
|
|
2389
|
+
let minPrice = candleRange.min;
|
|
2390
|
+
let maxPrice = candleRange.max;
|
|
2272
2391
|
if (overlayIndicatorsForScale.length > 0) {
|
|
2273
|
-
for (const { indicator } of overlayIndicatorsForScale) {
|
|
2392
|
+
for (const { indicator, plugin } of overlayIndicatorsForScale) {
|
|
2274
2393
|
if (indicator.excludeFromAutoscale) {
|
|
2275
2394
|
continue;
|
|
2276
2395
|
}
|
|
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) {
|
|
2396
|
+
const range = plugin.getAutoscaleRange?.(data, startIndex, endIndex, indicator.inputs, skipLatestIndex);
|
|
2397
|
+
if (!range || !Number.isFinite(range.min) || !Number.isFinite(range.max)) {
|
|
2289
2398
|
continue;
|
|
2290
2399
|
}
|
|
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) {
|
|
2302
|
-
continue;
|
|
2303
|
-
}
|
|
2304
|
-
const seriesMin = Math.min(...visibleValues);
|
|
2305
|
-
const seriesMax = Math.max(...visibleValues);
|
|
2306
2400
|
const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
|
|
2307
2401
|
const currentMid = (minPrice + maxPrice) / 2;
|
|
2308
|
-
const weightedMin = currentMid + (
|
|
2309
|
-
const weightedMax = currentMid + (
|
|
2402
|
+
const weightedMin = currentMid + (range.min - currentMid) * weight;
|
|
2403
|
+
const weightedMax = currentMid + (range.max - currentMid) * weight;
|
|
2310
2404
|
minPrice = Math.min(minPrice, weightedMin);
|
|
2311
2405
|
maxPrice = Math.max(maxPrice, weightedMax);
|
|
2312
2406
|
}
|
|
@@ -2381,7 +2475,7 @@ function createChart(element, options = {}) {
|
|
|
2381
2475
|
ctx.save();
|
|
2382
2476
|
const prevFont = ctx.font;
|
|
2383
2477
|
ctx.font = `500 12px ${mergedOptions.fontFamily}`;
|
|
2384
|
-
const textWidth =
|
|
2478
|
+
const textWidth = measureTextWidth(labelText);
|
|
2385
2479
|
const paddingX = 6;
|
|
2386
2480
|
const bgWidth = textWidth + paddingX * 2;
|
|
2387
2481
|
const bgHeight = 18;
|
|
@@ -2547,7 +2641,7 @@ function createChart(element, options = {}) {
|
|
|
2547
2641
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2548
2642
|
levelLines.forEach((level, index) => {
|
|
2549
2643
|
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2550
|
-
const textWidth =
|
|
2644
|
+
const textWidth = measureTextWidth(labelText);
|
|
2551
2645
|
const padding = 4;
|
|
2552
2646
|
const bgX = lineLeft + 4;
|
|
2553
2647
|
const bgY = level.y - 9;
|
|
@@ -2620,7 +2714,7 @@ function createChart(element, options = {}) {
|
|
|
2620
2714
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2621
2715
|
levelLines.forEach((level, index) => {
|
|
2622
2716
|
const labelText = `${level.ratio} (${formatPrice(level.price)})`;
|
|
2623
|
-
const textWidth =
|
|
2717
|
+
const textWidth = measureTextWidth(labelText);
|
|
2624
2718
|
const padding = 4;
|
|
2625
2719
|
const bgX = chartLeft + 4;
|
|
2626
2720
|
const bgY = level.y - 9;
|
|
@@ -2763,8 +2857,8 @@ function createChart(element, options = {}) {
|
|
|
2763
2857
|
const padding = 6;
|
|
2764
2858
|
const lineH = 14;
|
|
2765
2859
|
const isDigit = (ch) => ch >= "0" && ch <= "9";
|
|
2766
|
-
const digitW =
|
|
2767
|
-
const charAdvance = (ch) => isDigit(ch) ? digitW :
|
|
2860
|
+
const digitW = measureTextWidth("0");
|
|
2861
|
+
const charAdvance = (ch) => isDigit(ch) ? digitW : measureTextWidth(ch);
|
|
2768
2862
|
const lineWidth = (line) => {
|
|
2769
2863
|
let w = 0;
|
|
2770
2864
|
for (const ch of line) w += charAdvance(ch);
|
|
@@ -2787,11 +2881,11 @@ function createChart(element, options = {}) {
|
|
|
2787
2881
|
const y = startY + lineIndex * lineH;
|
|
2788
2882
|
for (const ch of line) {
|
|
2789
2883
|
if (isDigit(ch)) {
|
|
2790
|
-
ctx.fillText(ch, x + (digitW -
|
|
2884
|
+
ctx.fillText(ch, x + (digitW - measureTextWidth(ch)) / 2, y);
|
|
2791
2885
|
x += digitW;
|
|
2792
2886
|
} else {
|
|
2793
2887
|
ctx.fillText(ch, x, y);
|
|
2794
|
-
x +=
|
|
2888
|
+
x += measureTextWidth(ch);
|
|
2795
2889
|
}
|
|
2796
2890
|
}
|
|
2797
2891
|
});
|
|
@@ -2923,7 +3017,7 @@ function createChart(element, options = {}) {
|
|
|
2923
3017
|
ctx.font = `500 11px ${mergedOptions.fontFamily}`;
|
|
2924
3018
|
const padding = 6;
|
|
2925
3019
|
const lineHeight = 15;
|
|
2926
|
-
const textW = Math.max(...labelLines.map((line) =>
|
|
3020
|
+
const textW = Math.max(...labelLines.map((line) => measureTextWidth(line)));
|
|
2927
3021
|
const pillW = textW + padding * 2;
|
|
2928
3022
|
const pillH = labelLines.length * lineHeight + padding;
|
|
2929
3023
|
const pillX = midX - pillW / 2;
|
|
@@ -2959,7 +3053,7 @@ function createChart(element, options = {}) {
|
|
|
2959
3053
|
ctx.textBaseline = "middle";
|
|
2960
3054
|
const lines = (drawing.label ?? "").split("\n");
|
|
2961
3055
|
const lineH = Math.round(fontSize * 1.35);
|
|
2962
|
-
const textW = Math.max(1, ...lines.map((line) =>
|
|
3056
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
2963
3057
|
const padX = isNote ? 8 : 2;
|
|
2964
3058
|
const padY = isNote ? 6 : 1;
|
|
2965
3059
|
const blockW = textW + padX * 2;
|
|
@@ -3041,47 +3135,114 @@ function createChart(element, options = {}) {
|
|
|
3041
3135
|
Math.max(1, candleSpacing - 1),
|
|
3042
3136
|
Math.max(candleMinWidth, Math.floor(candleSpacing * candleBodyWidthRatio))
|
|
3043
3137
|
);
|
|
3044
|
-
const grid =
|
|
3138
|
+
const grid = resolvedGrid;
|
|
3045
3139
|
const gridOpacity = clamp(grid.opacity, 0, 1);
|
|
3046
3140
|
const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
|
|
3047
3141
|
const yTicks = Math.max(1, Math.floor(yTickCountInput));
|
|
3048
3142
|
const gridColor = grid.color ?? mergedOptions.gridColor;
|
|
3143
|
+
const priceTicks = [];
|
|
3144
|
+
{
|
|
3145
|
+
const targetStep = yRange / Math.max(1, yTicks);
|
|
3146
|
+
let step;
|
|
3147
|
+
if (targetStep > 0 && Number.isFinite(targetStep)) {
|
|
3148
|
+
const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
|
|
3149
|
+
const frac = targetStep / base;
|
|
3150
|
+
step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
|
|
3151
|
+
const instrumentTick = getConfiguredTickSize();
|
|
3152
|
+
if (instrumentTick > 0 && step < instrumentTick * 1e6) {
|
|
3153
|
+
step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
|
|
3154
|
+
}
|
|
3155
|
+
} else {
|
|
3156
|
+
step = 1;
|
|
3157
|
+
}
|
|
3158
|
+
const firstTick = Math.ceil(yMin / step) * step;
|
|
3159
|
+
for (let i = 0; ; i += 1) {
|
|
3160
|
+
const price = firstTick + i * step;
|
|
3161
|
+
if (price > yMax + step * 1e-6) break;
|
|
3162
|
+
priceTicks.push(price);
|
|
3163
|
+
if (priceTicks.length > 200) break;
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3049
3166
|
if (grid.horizontalLines) {
|
|
3050
3167
|
ctx.save();
|
|
3051
3168
|
ctx.globalAlpha = gridOpacity;
|
|
3052
|
-
|
|
3053
|
-
|
|
3054
|
-
|
|
3169
|
+
ctx.strokeStyle = gridColor;
|
|
3170
|
+
ctx.lineWidth = 1;
|
|
3171
|
+
ctx.beginPath();
|
|
3172
|
+
for (const price of priceTicks) {
|
|
3055
3173
|
const y = yFromPrice(price);
|
|
3056
|
-
ctx.strokeStyle = gridColor;
|
|
3057
|
-
ctx.lineWidth = 1;
|
|
3058
|
-
ctx.beginPath();
|
|
3059
3174
|
ctx.moveTo(crisp(chartLeft), crisp(y));
|
|
3060
3175
|
ctx.lineTo(crisp(chartRight), crisp(y));
|
|
3061
|
-
ctx.stroke();
|
|
3062
3176
|
}
|
|
3177
|
+
ctx.stroke();
|
|
3063
3178
|
ctx.restore();
|
|
3064
3179
|
}
|
|
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);
|
|
3180
|
+
const minLabelSpacingPx = Math.max(60, Math.min(120, chartWidth / Math.max(2, Math.floor(grid.xTickCount ?? 8))));
|
|
3181
|
+
const visibleTickStart = Math.max(0, Math.floor(xStart));
|
|
3072
3182
|
const visibleTickEnd = Math.ceil(xEnd) - 1;
|
|
3073
|
-
const
|
|
3074
|
-
|
|
3183
|
+
const timeTicks = [];
|
|
3184
|
+
{
|
|
3185
|
+
const weightBuckets = [[], [], [], [], [], [], [], [], [], []];
|
|
3186
|
+
let prevTime = visibleTickStart > 0 ? getTimeForIndex(visibleTickStart - 1) : null;
|
|
3187
|
+
for (let index = visibleTickStart; index <= visibleTickEnd; index += 1) {
|
|
3188
|
+
const time = getTimeForIndex(index);
|
|
3189
|
+
if (!time) {
|
|
3190
|
+
prevTime = null;
|
|
3191
|
+
continue;
|
|
3192
|
+
}
|
|
3193
|
+
let weight = 0;
|
|
3194
|
+
if (!prevTime) {
|
|
3195
|
+
weight = 7;
|
|
3196
|
+
} else {
|
|
3197
|
+
if (prevTime.getFullYear() !== time.getFullYear()) weight = 9;
|
|
3198
|
+
else if (prevTime.getMonth() !== time.getMonth()) weight = 8;
|
|
3199
|
+
else if (prevTime.getDate() !== time.getDate()) weight = 7;
|
|
3200
|
+
else {
|
|
3201
|
+
const prevMinutes = prevTime.getHours() * 60 + prevTime.getMinutes();
|
|
3202
|
+
const curMinutes = time.getHours() * 60 + time.getMinutes();
|
|
3203
|
+
const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
|
|
3204
|
+
if (crossed(360)) weight = 6;
|
|
3205
|
+
else if (crossed(60)) weight = 5;
|
|
3206
|
+
else if (crossed(30)) weight = 4;
|
|
3207
|
+
else if (crossed(15)) weight = 3;
|
|
3208
|
+
else if (crossed(5)) weight = 2;
|
|
3209
|
+
else weight = 1;
|
|
3210
|
+
}
|
|
3211
|
+
}
|
|
3212
|
+
weightBuckets[weight].push(index);
|
|
3213
|
+
prevTime = time;
|
|
3214
|
+
}
|
|
3215
|
+
const placedXs = [];
|
|
3216
|
+
const maxMarks = Math.floor(chartWidth / minLabelSpacingPx) + 2;
|
|
3217
|
+
for (let weight = 9; weight >= 1 && timeTicks.length < maxMarks; weight -= 1) {
|
|
3218
|
+
for (const index of weightBuckets[weight]) {
|
|
3219
|
+
const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3220
|
+
let fits = true;
|
|
3221
|
+
for (const placedX of placedXs) {
|
|
3222
|
+
if (Math.abs(placedX - x) < minLabelSpacingPx) {
|
|
3223
|
+
fits = false;
|
|
3224
|
+
break;
|
|
3225
|
+
}
|
|
3226
|
+
}
|
|
3227
|
+
if (!fits) continue;
|
|
3228
|
+
placedXs.push(x);
|
|
3229
|
+
timeTicks.push({ index, weight, x });
|
|
3230
|
+
if (timeTicks.length >= maxMarks) break;
|
|
3231
|
+
}
|
|
3232
|
+
}
|
|
3233
|
+
timeTicks.sort((a, b) => a.index - b.index);
|
|
3234
|
+
}
|
|
3235
|
+
if (grid.verticalLines && timeTicks.length > 0) {
|
|
3075
3236
|
ctx.save();
|
|
3076
3237
|
ctx.globalAlpha = gridOpacity;
|
|
3077
|
-
|
|
3078
|
-
|
|
3079
|
-
|
|
3080
|
-
|
|
3081
|
-
ctx.moveTo(crisp(x), crisp(chartTop));
|
|
3082
|
-
ctx.lineTo(crisp(x), crisp(fullChartBottom));
|
|
3083
|
-
ctx.stroke();
|
|
3238
|
+
ctx.strokeStyle = gridColor;
|
|
3239
|
+
ctx.lineWidth = 1;
|
|
3240
|
+
ctx.beginPath();
|
|
3241
|
+
for (const tick of timeTicks) {
|
|
3242
|
+
ctx.moveTo(crisp(tick.x), crisp(chartTop));
|
|
3243
|
+
ctx.lineTo(crisp(tick.x), crisp(fullChartBottom));
|
|
3084
3244
|
}
|
|
3245
|
+
ctx.stroke();
|
|
3085
3246
|
ctx.restore();
|
|
3086
3247
|
}
|
|
3087
3248
|
if (grid.sessionSeparators && data.length > 1) {
|
|
@@ -3119,38 +3280,45 @@ function createChart(element, options = {}) {
|
|
|
3119
3280
|
}
|
|
3120
3281
|
return data[index]?.v;
|
|
3121
3282
|
};
|
|
3122
|
-
|
|
3123
|
-
const
|
|
3124
|
-
|
|
3125
|
-
|
|
3283
|
+
{
|
|
3284
|
+
const upWicks = new Path2D();
|
|
3285
|
+
const downWicks = new Path2D();
|
|
3286
|
+
const upBodies = new Path2D();
|
|
3287
|
+
const downBodies = new Path2D();
|
|
3288
|
+
for (let index = startIndex; index <= endIndex; index += 1) {
|
|
3289
|
+
const point = data[index];
|
|
3290
|
+
if (!point) {
|
|
3291
|
+
continue;
|
|
3292
|
+
}
|
|
3293
|
+
const isLastCandle = useSmoothedCandle && index === lastDataIndex;
|
|
3294
|
+
const actualDirection = point.c >= point.o ? "up" : "down";
|
|
3295
|
+
const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
|
|
3296
|
+
const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
|
|
3297
|
+
const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
|
|
3298
|
+
const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
|
|
3299
|
+
const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
|
|
3300
|
+
const isUp = direction === "up";
|
|
3301
|
+
const roundedCenterX = Math.round(centerX);
|
|
3302
|
+
const wicks = isUp ? upWicks : downWicks;
|
|
3303
|
+
wicks.moveTo(roundedCenterX + 0.5, crisp(yFromPrice(displayHigh)));
|
|
3304
|
+
wicks.lineTo(roundedCenterX + 0.5, crisp(yFromPrice(displayLow)));
|
|
3305
|
+
const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
|
|
3306
|
+
const openYPx = Math.round(yFromPrice(point.o));
|
|
3307
|
+
const closeYPx = Math.round(yFromPrice(displayClose));
|
|
3308
|
+
const bodyIsUp = displayClose >= point.o;
|
|
3309
|
+
const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
|
|
3310
|
+
const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
|
|
3311
|
+
(isUp ? upBodies : downBodies).rect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
|
|
3126
3312
|
}
|
|
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
3313
|
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);
|
|
3314
|
+
ctx.strokeStyle = mergedOptions.upColor;
|
|
3315
|
+
ctx.stroke(upWicks);
|
|
3316
|
+
ctx.fillStyle = mergedOptions.upColor;
|
|
3317
|
+
ctx.fill(upBodies);
|
|
3318
|
+
ctx.strokeStyle = mergedOptions.downColor;
|
|
3319
|
+
ctx.stroke(downWicks);
|
|
3320
|
+
ctx.fillStyle = mergedOptions.downColor;
|
|
3321
|
+
ctx.fill(downBodies);
|
|
3154
3322
|
}
|
|
3155
3323
|
const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
|
|
3156
3324
|
(value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
|
|
@@ -3268,56 +3436,6 @@ function createChart(element, options = {}) {
|
|
|
3268
3436
|
ctx.textAlign = prevAlign;
|
|
3269
3437
|
ctx.textBaseline = prevBaseline;
|
|
3270
3438
|
}
|
|
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
3439
|
ctx.restore();
|
|
3322
3440
|
const positionAxisGutter = Math.max(0, width - chartRight);
|
|
3323
3441
|
if (positionAxisGutter > 0) {
|
|
@@ -3430,7 +3548,7 @@ function createChart(element, options = {}) {
|
|
|
3430
3548
|
const text = label.text ?? formatPaneValue(label.value);
|
|
3431
3549
|
const labelPaddingX = 7;
|
|
3432
3550
|
const labelHeight = Math.max(16, yAxisFontSize + 8);
|
|
3433
|
-
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(
|
|
3551
|
+
const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(measureTextWidth(text)) + labelPaddingX * 2);
|
|
3434
3552
|
const labelX = getRightAxisLabelX(chartRight);
|
|
3435
3553
|
const labelY = clamp(yFromPaneValue(label.value) - labelHeight / 2, paneTop + 2, paneBottom - labelHeight - 2);
|
|
3436
3554
|
ctx.fillStyle = label.backgroundColor ?? label.color ?? labels.backgroundColor;
|
|
@@ -3442,18 +3560,6 @@ function createChart(element, options = {}) {
|
|
|
3442
3560
|
}
|
|
3443
3561
|
});
|
|
3444
3562
|
}
|
|
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
3563
|
ctx.strokeStyle = axis.lineColor;
|
|
3458
3564
|
ctx.lineWidth = Math.max(1, axis.lineWidth);
|
|
3459
3565
|
ctx.beginPath();
|
|
@@ -3463,13 +3569,13 @@ function createChart(element, options = {}) {
|
|
|
3463
3569
|
ctx.lineTo(crisp(chartRight), crisp(fullChartBottom));
|
|
3464
3570
|
ctx.stroke();
|
|
3465
3571
|
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);
|
|
3572
|
+
{
|
|
3470
3573
|
const prevFont = ctx.font;
|
|
3471
3574
|
ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3472
|
-
|
|
3575
|
+
for (const price of priceTicks) {
|
|
3576
|
+
const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
|
|
3577
|
+
drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
|
|
3578
|
+
}
|
|
3473
3579
|
ctx.font = prevFont;
|
|
3474
3580
|
}
|
|
3475
3581
|
resetLabelSlots(labels.noOverlapping);
|
|
@@ -3507,8 +3613,8 @@ function createChart(element, options = {}) {
|
|
|
3507
3613
|
return null;
|
|
3508
3614
|
}
|
|
3509
3615
|
const stepMs = getTimeStepMs();
|
|
3510
|
-
const
|
|
3511
|
-
const countdownMs =
|
|
3616
|
+
const elapsedMs = Date.now() - last.time.getTime();
|
|
3617
|
+
const countdownMs = elapsedMs >= 0 ? stepMs - elapsedMs % stepMs : Math.min(stepMs, stepMs - elapsedMs);
|
|
3512
3618
|
return formatDuration(countdownMs);
|
|
3513
3619
|
};
|
|
3514
3620
|
const ticker = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
|
|
@@ -3578,9 +3684,15 @@ function createChart(element, options = {}) {
|
|
|
3578
3684
|
});
|
|
3579
3685
|
}
|
|
3580
3686
|
}
|
|
3581
|
-
if (labels.showHighLow &&
|
|
3582
|
-
|
|
3583
|
-
|
|
3687
|
+
if (labels.showHighLow && endIndex >= startIndex) {
|
|
3688
|
+
let visibleHigh = Number.NEGATIVE_INFINITY;
|
|
3689
|
+
let visibleLow = Number.POSITIVE_INFINITY;
|
|
3690
|
+
for (let idx = startIndex; idx <= endIndex; idx += 1) {
|
|
3691
|
+
const point = data[idx];
|
|
3692
|
+
if (!point) continue;
|
|
3693
|
+
if (point.h > visibleHigh) visibleHigh = point.h;
|
|
3694
|
+
if (point.l < visibleLow) visibleLow = point.l;
|
|
3695
|
+
}
|
|
3584
3696
|
addPriceAxisLabel({
|
|
3585
3697
|
text: `H ${formatPrice(visibleHigh)}`,
|
|
3586
3698
|
price: visibleHigh,
|
|
@@ -3633,12 +3745,12 @@ function createChart(element, options = {}) {
|
|
|
3633
3745
|
const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
|
|
3634
3746
|
const subtextLineGap = 5;
|
|
3635
3747
|
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(
|
|
3748
|
+
const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(measureTextWidth(label.text)) + labelPaddingX * 2;
|
|
3637
3749
|
let subtextWidth = 0;
|
|
3638
3750
|
if (subtexts.length > 0) {
|
|
3639
3751
|
const baseFont = ctx.font;
|
|
3640
3752
|
ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
|
|
3641
|
-
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(
|
|
3753
|
+
subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(measureTextWidth(subtext)) + labelPaddingX * 2));
|
|
3642
3754
|
ctx.font = baseFont;
|
|
3643
3755
|
}
|
|
3644
3756
|
const labelTextWidth = Math.max(primaryWidth, subtextWidth);
|
|
@@ -3742,164 +3854,250 @@ function createChart(element, options = {}) {
|
|
|
3742
3854
|
ctx.font = prevFont;
|
|
3743
3855
|
}
|
|
3744
3856
|
}
|
|
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;
|
|
3857
|
+
{
|
|
3757
3858
|
const prevFont = ctx.font;
|
|
3758
3859
|
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3759
|
-
|
|
3860
|
+
for (const tick of timeTicks) {
|
|
3861
|
+
const tickTime = getTimeForIndex(tick.index);
|
|
3862
|
+
if (!tickTime) continue;
|
|
3863
|
+
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 });
|
|
3864
|
+
drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
|
|
3865
|
+
}
|
|
3760
3866
|
ctx.font = prevFont;
|
|
3761
3867
|
}
|
|
3762
3868
|
if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
|
|
3763
3869
|
const stepMs = getTimeStepMs();
|
|
3764
|
-
const
|
|
3765
|
-
const countdownMs =
|
|
3870
|
+
const elapsedMs = Date.now() - lastPoint.time.getTime();
|
|
3871
|
+
const countdownMs = elapsedMs >= 0 ? stepMs - elapsedMs % stepMs : Math.min(stepMs, stepMs - elapsedMs);
|
|
3766
3872
|
const countdownText = formatDuration(countdownMs);
|
|
3767
3873
|
const prevFont = ctx.font;
|
|
3768
3874
|
ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
|
|
3769
3875
|
drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
|
|
3770
3876
|
ctx.font = prevFont;
|
|
3771
3877
|
}
|
|
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
|
-
|
|
3878
|
+
if (baseCtx) {
|
|
3879
|
+
if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
|
|
3880
|
+
baseCanvas.width = canvas.width;
|
|
3881
|
+
baseCanvas.height = canvas.height;
|
|
3882
|
+
}
|
|
3883
|
+
baseCtx.setTransform(1, 0, 0, 1, 0, 0);
|
|
3884
|
+
baseCtx.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
|
|
3885
|
+
baseCtx.drawImage(canvas, 0, 0);
|
|
3886
|
+
}
|
|
3887
|
+
overlayLayout = {
|
|
3888
|
+
chartLeft,
|
|
3889
|
+
chartTop,
|
|
3890
|
+
chartRight,
|
|
3891
|
+
chartBottom,
|
|
3892
|
+
chartWidth,
|
|
3893
|
+
chartHeight,
|
|
3894
|
+
fullChartBottom,
|
|
3895
|
+
yMin,
|
|
3896
|
+
yRange,
|
|
3897
|
+
xStart,
|
|
3898
|
+
xSpan
|
|
3899
|
+
};
|
|
3900
|
+
paintCrosshairOverlay();
|
|
3901
|
+
};
|
|
3902
|
+
const paintCrosshairOverlay = () => {
|
|
3903
|
+
crosshairPriceActionRegion = null;
|
|
3904
|
+
const layout = overlayLayout;
|
|
3905
|
+
const crosshair = resolvedCrosshair;
|
|
3906
|
+
if (!layout || !crosshair.visible || !crosshairPoint) {
|
|
3907
|
+
return;
|
|
3908
|
+
}
|
|
3909
|
+
const { chartLeft, chartTop, chartRight, chartBottom, chartWidth, chartHeight, fullChartBottom, yMin, yRange, xStart, xSpan: xSpan2 } = layout;
|
|
3910
|
+
const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
|
|
3911
|
+
const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
|
|
3912
|
+
ctx.save();
|
|
3913
|
+
ctx.beginPath();
|
|
3914
|
+
ctx.rect(chartLeft, chartTop, chartWidth, Math.max(0, chartBottom - chartTop));
|
|
3915
|
+
ctx.clip();
|
|
3916
|
+
if (crosshair.mode === "dot") {
|
|
3917
|
+
ctx.fillStyle = crosshair.color;
|
|
3918
|
+
ctx.beginPath();
|
|
3919
|
+
ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
|
|
3920
|
+
ctx.fill();
|
|
3921
|
+
} else {
|
|
3922
|
+
ctx.strokeStyle = crosshair.color;
|
|
3923
|
+
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3924
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3925
|
+
if (crosshair.showHorizontal) {
|
|
3926
|
+
ctx.beginPath();
|
|
3927
|
+
ctx.moveTo(crisp(chartLeft), crisp(cy));
|
|
3928
|
+
ctx.lineTo(crisp(chartRight), crisp(cy));
|
|
3929
|
+
ctx.stroke();
|
|
3930
|
+
}
|
|
3931
|
+
}
|
|
3932
|
+
if (crosshair.sideHintLeft || crosshair.sideHintRight) {
|
|
3933
|
+
ctx.save();
|
|
3934
|
+
ctx.font = `600 11px ${mergedOptions.fontFamily}`;
|
|
3935
|
+
ctx.textBaseline = "middle";
|
|
3936
|
+
ctx.setLineDash([]);
|
|
3937
|
+
const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
|
|
3938
|
+
const hintGap = 8;
|
|
3939
|
+
if (crosshair.sideHintLeft) {
|
|
3940
|
+
ctx.fillStyle = crosshair.sideHintLeftColor;
|
|
3941
|
+
ctx.textAlign = "right";
|
|
3942
|
+
ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
|
|
3943
|
+
}
|
|
3944
|
+
if (crosshair.sideHintRight) {
|
|
3945
|
+
ctx.fillStyle = crosshair.sideHintRightColor;
|
|
3946
|
+
ctx.textAlign = "left";
|
|
3947
|
+
ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
|
|
3948
|
+
}
|
|
3949
|
+
ctx.restore();
|
|
3950
|
+
}
|
|
3951
|
+
ctx.restore();
|
|
3952
|
+
if (crosshair.showVertical) {
|
|
3953
|
+
ctx.save();
|
|
3954
|
+
ctx.strokeStyle = crosshair.color;
|
|
3955
|
+
ctx.lineWidth = Math.max(1, crosshair.width);
|
|
3956
|
+
applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
|
|
3957
|
+
ctx.beginPath();
|
|
3958
|
+
ctx.moveTo(crisp(cx), crisp(chartTop));
|
|
3959
|
+
ctx.lineTo(crisp(cx), crisp(fullChartBottom));
|
|
3960
|
+
ctx.stroke();
|
|
3961
|
+
ctx.restore();
|
|
3962
|
+
}
|
|
3963
|
+
const labelPaddingX = 8;
|
|
3964
|
+
const labelHeight = 20;
|
|
3965
|
+
const labelRadius = Math.max(0, crosshair.labelBorderRadius);
|
|
3966
|
+
const labelBackground = crosshair.labelBackgroundColor;
|
|
3967
|
+
const labelTextColor = crosshair.labelTextColor;
|
|
3968
|
+
const labelBorderColor = crosshair.labelBorderColor;
|
|
3969
|
+
const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
|
|
3970
|
+
const labelBorderStyle = crosshair.labelBorderStyle;
|
|
3971
|
+
const strokeCrosshairLabel = (x, y, widthValue, heightValue = labelHeight) => {
|
|
3972
|
+
if (labelBorderWidth <= 0) {
|
|
3973
|
+
return;
|
|
3974
|
+
}
|
|
3975
|
+
ctx.save();
|
|
3976
|
+
ctx.strokeStyle = labelBorderColor;
|
|
3977
|
+
ctx.lineWidth = labelBorderWidth;
|
|
3978
|
+
applyDashPattern(
|
|
3979
|
+
labelBorderStyle,
|
|
3980
|
+
dashPatterns.borderDotted,
|
|
3981
|
+
dashPatterns.borderDashed
|
|
3982
|
+
);
|
|
3983
|
+
strokeRoundedRect(Math.round(x), Math.round(y), widthValue, heightValue, labelRadius);
|
|
3984
|
+
ctx.restore();
|
|
3985
|
+
};
|
|
3986
|
+
if (crosshair.showPriceLabel) {
|
|
3987
|
+
const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
|
|
3988
|
+
const priceText = formatPrice(hoverPrice);
|
|
3989
|
+
const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
|
|
3990
|
+
const priceX = getRightAxisLabelX(chartRight);
|
|
3991
|
+
const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
|
|
3992
|
+
ctx.fillStyle = labelBackground;
|
|
3993
|
+
fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
|
|
3994
|
+
strokeCrosshairLabel(priceX, priceY, priceWidth);
|
|
3995
|
+
drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
|
|
3996
|
+
if (crosshair.showPriceActionButton) {
|
|
3997
|
+
const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
|
|
3998
|
+
const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
|
|
3999
|
+
const containerPaddingX = 5;
|
|
4000
|
+
const containerWidth = buttonSize + containerPaddingX * 2;
|
|
4001
|
+
const containerX = priceX - buttonGap - containerWidth;
|
|
4002
|
+
const containerY = priceY;
|
|
4003
|
+
const buttonX = containerX + containerPaddingX;
|
|
4004
|
+
const buttonY = priceY + (labelHeight - buttonSize) / 2;
|
|
4005
|
+
const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
|
|
4006
|
+
const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
|
|
4007
|
+
const buttonCenterX = buttonX + buttonSize / 2;
|
|
4008
|
+
const buttonCenterY = buttonY + buttonSize / 2;
|
|
4009
|
+
const containerRadius = labelRadius;
|
|
3804
4010
|
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([]);
|
|
4011
|
+
fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
4012
|
+
if (labelBorderWidth > 0) {
|
|
4013
|
+
ctx.save();
|
|
4014
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4015
|
+
ctx.lineWidth = labelBorderWidth;
|
|
4016
|
+
ctx.setLineDash([]);
|
|
4017
|
+
strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
|
|
4018
|
+
ctx.restore();
|
|
4019
|
+
}
|
|
4020
|
+
if (buttonBorderWidth > 0) {
|
|
4021
|
+
ctx.save();
|
|
4022
|
+
ctx.strokeStyle = labelBorderColor;
|
|
4023
|
+
ctx.lineWidth = buttonBorderWidth;
|
|
4024
|
+
ctx.setLineDash([]);
|
|
4025
|
+
if (crosshair.priceActionButtonRounded) {
|
|
3860
4026
|
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,
|
|
4027
|
+
ctx.arc(
|
|
3870
4028
|
buttonCenterX,
|
|
3871
4029
|
buttonCenterY,
|
|
3872
|
-
|
|
3873
|
-
|
|
3874
|
-
|
|
4030
|
+
Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
|
|
4031
|
+
0,
|
|
4032
|
+
Math.PI * 2
|
|
3875
4033
|
);
|
|
4034
|
+
ctx.stroke();
|
|
4035
|
+
} else {
|
|
4036
|
+
strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
|
|
3876
4037
|
}
|
|
3877
|
-
|
|
3878
|
-
x: containerX,
|
|
3879
|
-
y: containerY,
|
|
3880
|
-
width: containerWidth,
|
|
3881
|
-
height: labelHeight,
|
|
3882
|
-
price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
|
|
3883
|
-
};
|
|
4038
|
+
ctx.restore();
|
|
3884
4039
|
}
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
ctx.
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
4040
|
+
if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
|
|
4041
|
+
const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
|
|
4042
|
+
const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
|
|
4043
|
+
ctx.save();
|
|
4044
|
+
ctx.strokeStyle = labelTextColor;
|
|
4045
|
+
ctx.lineWidth = plusThickness;
|
|
4046
|
+
ctx.lineCap = "round";
|
|
4047
|
+
ctx.setLineDash([]);
|
|
4048
|
+
ctx.beginPath();
|
|
4049
|
+
ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
|
|
4050
|
+
ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
|
|
4051
|
+
ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
|
|
4052
|
+
ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
|
|
4053
|
+
ctx.stroke();
|
|
4054
|
+
ctx.restore();
|
|
4055
|
+
} else {
|
|
4056
|
+
drawText(
|
|
4057
|
+
crosshair.priceActionButtonText,
|
|
4058
|
+
buttonCenterX,
|
|
4059
|
+
buttonCenterY,
|
|
4060
|
+
"center",
|
|
4061
|
+
"middle",
|
|
4062
|
+
labelTextColor
|
|
4063
|
+
);
|
|
3900
4064
|
}
|
|
4065
|
+
crosshairPriceActionRegion = {
|
|
4066
|
+
x: containerX,
|
|
4067
|
+
y: containerY,
|
|
4068
|
+
width: containerWidth,
|
|
4069
|
+
height: labelHeight,
|
|
4070
|
+
price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
|
|
4071
|
+
};
|
|
3901
4072
|
}
|
|
3902
4073
|
}
|
|
4074
|
+
if (crosshair.showTimeLabel) {
|
|
4075
|
+
const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
|
|
4076
|
+
const hoverIndex = Math.round(xStart + ratio * xSpan2 - 0.5);
|
|
4077
|
+
const hoverTime = getTimeForIndex(hoverIndex);
|
|
4078
|
+
if (hoverTime) {
|
|
4079
|
+
const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
|
|
4080
|
+
const timeWidth = Math.ceil(measureTextWidth(timeText)) + labelPaddingX * 2;
|
|
4081
|
+
const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
|
|
4082
|
+
const timeY = fullChartBottom + 1;
|
|
4083
|
+
const timeHeight = Math.max(labelHeight, Math.floor(height - timeY));
|
|
4084
|
+
ctx.fillStyle = labelBackground;
|
|
4085
|
+
fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, timeHeight, labelRadius);
|
|
4086
|
+
strokeCrosshairLabel(timeX, timeY, timeWidth, timeHeight);
|
|
4087
|
+
drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
|
|
4088
|
+
}
|
|
4089
|
+
}
|
|
4090
|
+
};
|
|
4091
|
+
const drawOverlayOnly = () => {
|
|
4092
|
+
if (!baseCtx || !overlayLayout || baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height || baseCanvas.width === 0) {
|
|
4093
|
+
draw({ updateAutoScale: false });
|
|
4094
|
+
return;
|
|
4095
|
+
}
|
|
4096
|
+
const pixelRatio = getPixelRatio();
|
|
4097
|
+
ctx.setTransform(1, 0, 0, 1, 0, 0);
|
|
4098
|
+
ctx.drawImage(baseCanvas, 0, 0);
|
|
4099
|
+
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
|
4100
|
+
paintCrosshairOverlay();
|
|
3903
4101
|
};
|
|
3904
4102
|
const zoomX = (factor, anchorX) => {
|
|
3905
4103
|
if (!drawState || data.length === 0) {
|
|
@@ -3990,6 +4188,59 @@ function createChart(element, options = {}) {
|
|
|
3990
4188
|
}
|
|
3991
4189
|
scheduleDraw();
|
|
3992
4190
|
};
|
|
4191
|
+
let kineticRafId = null;
|
|
4192
|
+
const panVelocitySamples = [];
|
|
4193
|
+
const cancelKineticPan = () => {
|
|
4194
|
+
if (kineticRafId !== null) {
|
|
4195
|
+
cancelAnimationFrame(kineticRafId);
|
|
4196
|
+
kineticRafId = null;
|
|
4197
|
+
}
|
|
4198
|
+
};
|
|
4199
|
+
const recordPanVelocitySample = (x) => {
|
|
4200
|
+
const now = performance.now();
|
|
4201
|
+
panVelocitySamples.push({ t: now, x });
|
|
4202
|
+
while (panVelocitySamples.length > 0 && now - panVelocitySamples[0].t > 120) {
|
|
4203
|
+
panVelocitySamples.shift();
|
|
4204
|
+
}
|
|
4205
|
+
};
|
|
4206
|
+
const startKineticPan = (initialVelocityPxPerMs) => {
|
|
4207
|
+
cancelKineticPan();
|
|
4208
|
+
let velocity = initialVelocityPxPerMs;
|
|
4209
|
+
let lastTime = performance.now();
|
|
4210
|
+
const stepFn = (now) => {
|
|
4211
|
+
kineticRafId = null;
|
|
4212
|
+
const dt = Math.min(64, Math.max(1, now - lastTime));
|
|
4213
|
+
lastTime = now;
|
|
4214
|
+
const centerBefore = xCenter;
|
|
4215
|
+
pan(velocity * dt, 0, true, false);
|
|
4216
|
+
velocity *= Math.exp(-dt / 325);
|
|
4217
|
+
if (Math.abs(velocity) < 0.02 || xCenter === centerBefore) {
|
|
4218
|
+
return;
|
|
4219
|
+
}
|
|
4220
|
+
kineticRafId = requestAnimationFrame(stepFn);
|
|
4221
|
+
};
|
|
4222
|
+
kineticRafId = requestAnimationFrame(stepFn);
|
|
4223
|
+
};
|
|
4224
|
+
const maybeStartKineticPan = (pointerType) => {
|
|
4225
|
+
const config = mergedOptions.kineticScroll ?? DEFAULT_OPTIONS.kineticScroll;
|
|
4226
|
+
const enabled = pointerType === "touch" || pointerType === "pen" ? config?.touch !== false : config?.mouse === true;
|
|
4227
|
+
if (!enabled || panVelocitySamples.length < 2) {
|
|
4228
|
+
panVelocitySamples.length = 0;
|
|
4229
|
+
return;
|
|
4230
|
+
}
|
|
4231
|
+
const first = panVelocitySamples[0];
|
|
4232
|
+
const last = panVelocitySamples[panVelocitySamples.length - 1];
|
|
4233
|
+
panVelocitySamples.length = 0;
|
|
4234
|
+
const elapsed = last.t - first.t;
|
|
4235
|
+
if (elapsed <= 0 || performance.now() - last.t > 100) {
|
|
4236
|
+
return;
|
|
4237
|
+
}
|
|
4238
|
+
const velocity = (last.x - first.x) / elapsed;
|
|
4239
|
+
if (Math.abs(velocity) < 0.1) {
|
|
4240
|
+
return;
|
|
4241
|
+
}
|
|
4242
|
+
startKineticPan(clamp(velocity, -4, 4));
|
|
4243
|
+
};
|
|
3993
4244
|
const resetYViewport = () => {
|
|
3994
4245
|
yMinOverride = null;
|
|
3995
4246
|
yMaxOverride = null;
|
|
@@ -4049,6 +4300,7 @@ function createChart(element, options = {}) {
|
|
|
4049
4300
|
scheduleDraw();
|
|
4050
4301
|
};
|
|
4051
4302
|
const resetViewport = () => {
|
|
4303
|
+
cancelKineticPan();
|
|
4052
4304
|
fitXViewport();
|
|
4053
4305
|
resetYViewport();
|
|
4054
4306
|
updateFollowLatest(true);
|
|
@@ -4058,6 +4310,7 @@ function createChart(element, options = {}) {
|
|
|
4058
4310
|
const isFollowingLatest = () => followLatest;
|
|
4059
4311
|
const setFollowingLatest = (follow) => {
|
|
4060
4312
|
if (follow && data.length > 0) {
|
|
4313
|
+
cancelKineticPan();
|
|
4061
4314
|
xCenter = data.length - xSpan / 2 + rightEdgePaddingBars;
|
|
4062
4315
|
clampXViewport();
|
|
4063
4316
|
updateFollowLatest(true);
|
|
@@ -4427,7 +4680,7 @@ function createChart(element, options = {}) {
|
|
|
4427
4680
|
const prevFont = ctx.font;
|
|
4428
4681
|
ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
|
|
4429
4682
|
const lines = (drawing.label ?? "").split("\n");
|
|
4430
|
-
const textW = Math.max(1, ...lines.map((line) =>
|
|
4683
|
+
const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
|
|
4431
4684
|
ctx.font = prevFont;
|
|
4432
4685
|
const isNote = drawing.type === "note";
|
|
4433
4686
|
const padX = isNote ? 8 : 2;
|
|
@@ -4900,16 +5153,24 @@ function createChart(element, options = {}) {
|
|
|
4900
5153
|
emitViewportChange();
|
|
4901
5154
|
scheduleDraw();
|
|
4902
5155
|
};
|
|
5156
|
+
const capturePointer = (pointerId) => {
|
|
5157
|
+
try {
|
|
5158
|
+
canvas.setPointerCapture(pointerId);
|
|
5159
|
+
} catch {
|
|
5160
|
+
}
|
|
5161
|
+
};
|
|
4903
5162
|
const onPointerDown = (event) => {
|
|
4904
5163
|
if (event.pointerType === "touch" || event.pointerType === "pen") {
|
|
4905
5164
|
event.preventDefault();
|
|
4906
5165
|
}
|
|
5166
|
+
cancelKineticPan();
|
|
5167
|
+
panVelocitySamples.length = 0;
|
|
4907
5168
|
magnetModifierActive = event.metaKey || event.ctrlKey;
|
|
4908
5169
|
shiftKeyActive = event.shiftKey;
|
|
4909
5170
|
const point = getCanvasPoint(event);
|
|
4910
5171
|
if (event.pointerType === "touch") {
|
|
4911
5172
|
touchPointers.set(event.pointerId, point);
|
|
4912
|
-
|
|
5173
|
+
capturePointer(event.pointerId);
|
|
4913
5174
|
const touchPair = getTouchPair();
|
|
4914
5175
|
if (touchPair) {
|
|
4915
5176
|
beginPinchZoom(touchPair[0], touchPair[1]);
|
|
@@ -4939,7 +5200,7 @@ function createChart(element, options = {}) {
|
|
|
4939
5200
|
lastPrice: startPrice,
|
|
4940
5201
|
moved: false
|
|
4941
5202
|
};
|
|
4942
|
-
|
|
5203
|
+
capturePointer(event.pointerId);
|
|
4943
5204
|
canvas.style.cursor = "ns-resize";
|
|
4944
5205
|
setCrosshairPoint(null);
|
|
4945
5206
|
return;
|
|
@@ -4960,7 +5221,7 @@ function createChart(element, options = {}) {
|
|
|
4960
5221
|
startPrice: orderDragRegion.price,
|
|
4961
5222
|
lastPrice: orderDragRegion.price
|
|
4962
5223
|
};
|
|
4963
|
-
|
|
5224
|
+
capturePointer(event.pointerId);
|
|
4964
5225
|
canvas.style.cursor = "ns-resize";
|
|
4965
5226
|
setCrosshairPoint(null);
|
|
4966
5227
|
return;
|
|
@@ -4992,7 +5253,7 @@ function createChart(element, options = {}) {
|
|
|
4992
5253
|
startPoints: drawingHit.drawing.points.map((drawingPoint) => ({ ...drawingPoint }))
|
|
4993
5254
|
};
|
|
4994
5255
|
activePointerId = event.pointerId;
|
|
4995
|
-
|
|
5256
|
+
capturePointer(event.pointerId);
|
|
4996
5257
|
}
|
|
4997
5258
|
}
|
|
4998
5259
|
setCrosshairPoint(null);
|
|
@@ -5020,7 +5281,7 @@ function createChart(element, options = {}) {
|
|
|
5020
5281
|
};
|
|
5021
5282
|
lastPointerX = point.x;
|
|
5022
5283
|
lastPointerY = point.y;
|
|
5023
|
-
|
|
5284
|
+
capturePointer(event.pointerId);
|
|
5024
5285
|
if (crosshairDrag) {
|
|
5025
5286
|
canvas.style.cursor = "crosshair";
|
|
5026
5287
|
setCrosshairPoint(point);
|
|
@@ -5206,6 +5467,7 @@ function createChart(element, options = {}) {
|
|
|
5206
5467
|
const deltaY = point.y - lastPointerY;
|
|
5207
5468
|
if (dragMode === "plot") {
|
|
5208
5469
|
canvas.style.cursor = "grabbing";
|
|
5470
|
+
recordPanVelocitySample(point.x);
|
|
5209
5471
|
pan(deltaX, deltaY, true, true);
|
|
5210
5472
|
setCrosshairPoint(null);
|
|
5211
5473
|
} else if (dragMode === "x-axis") {
|
|
@@ -5279,10 +5541,16 @@ function createChart(element, options = {}) {
|
|
|
5279
5541
|
drawingDragState = null;
|
|
5280
5542
|
emitDrawingsChange();
|
|
5281
5543
|
}
|
|
5544
|
+
const endedPlotDrag = isDragging && dragMode === "plot";
|
|
5282
5545
|
isDragging = false;
|
|
5283
5546
|
dragMode = null;
|
|
5284
5547
|
activePointerId = null;
|
|
5285
5548
|
canvas.style.cursor = "default";
|
|
5549
|
+
if (endedPlotDrag) {
|
|
5550
|
+
maybeStartKineticPan(event?.pointerType);
|
|
5551
|
+
} else {
|
|
5552
|
+
panVelocitySamples.length = 0;
|
|
5553
|
+
}
|
|
5286
5554
|
if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
|
|
5287
5555
|
if (pointerDownInfo.crosshairDrag) {
|
|
5288
5556
|
const point = getCanvasPoint(event);
|
|
@@ -5320,6 +5588,7 @@ function createChart(element, options = {}) {
|
|
|
5320
5588
|
if (!drawState) {
|
|
5321
5589
|
return;
|
|
5322
5590
|
}
|
|
5591
|
+
cancelKineticPan();
|
|
5323
5592
|
const point = getCanvasPoint(event);
|
|
5324
5593
|
const region = getHitRegion(point.x, point.y);
|
|
5325
5594
|
if (region === "outside") {
|
|
@@ -5419,6 +5688,7 @@ function createChart(element, options = {}) {
|
|
|
5419
5688
|
width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
|
|
5420
5689
|
height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
|
|
5421
5690
|
mergedOptions = { ...mergedOptions, width, height };
|
|
5691
|
+
refreshResolvedOptions();
|
|
5422
5692
|
resetRightMarginCache();
|
|
5423
5693
|
doubleClickEnabled = mergedOptions.doubleClickEnabled;
|
|
5424
5694
|
doubleClickAction = mergedOptions.doubleClickAction;
|
|
@@ -5763,6 +6033,7 @@ function createChart(element, options = {}) {
|
|
|
5763
6033
|
drawingHoverHandler = handler;
|
|
5764
6034
|
};
|
|
5765
6035
|
const destroy = () => {
|
|
6036
|
+
cancelKineticPan();
|
|
5766
6037
|
if (smoothingRafId !== null) {
|
|
5767
6038
|
cancelAnimationFrame(smoothingRafId);
|
|
5768
6039
|
smoothingRafId = null;
|