hyperprop-charting-library 0.1.119 → 0.1.121

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -215,8 +215,12 @@ var DEFAULT_OPTIONS = {
215
215
  tickSize: 0,
216
216
  candleColorMode: "openClose",
217
217
  candleColorEpsilon: -1,
218
- autoScaleSmoothing: 0.16,
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 },
220
224
  pinOutOfRangeLines: false,
221
225
  doubleClickEnabled: true,
222
226
  doubleClickAction: "reset",
@@ -435,6 +439,60 @@ var computeStdDevSeries = (data, length, source) => {
435
439
  }
436
440
  return result;
437
441
  };
442
+ var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
443
+ var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
444
+ var computeVwapSeries = (data, band) => {
445
+ const result = new Array(data.length).fill(null);
446
+ let session = Number.NaN;
447
+ let cumPv = 0;
448
+ let cumPv2 = 0;
449
+ let cumV = 0;
450
+ for (let i = 0; i < data.length; i += 1) {
451
+ const point = data[i];
452
+ if (!point) continue;
453
+ const day = vwapSessionDayOf(point.time.getTime());
454
+ if (day !== session) {
455
+ session = day;
456
+ cumPv = 0;
457
+ cumPv2 = 0;
458
+ cumV = 0;
459
+ }
460
+ const typical = (point.h + point.l + point.c) / 3;
461
+ const volume = Math.max(0, point.v ?? 0);
462
+ cumPv += typical * volume;
463
+ cumPv2 += typical * typical * volume;
464
+ cumV += volume;
465
+ if (cumV <= 0) continue;
466
+ const vwap = cumPv / cumV;
467
+ if (band === "vwap") {
468
+ result[i] = vwap;
469
+ } else {
470
+ const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
471
+ result[i] = Math.sqrt(variance);
472
+ }
473
+ }
474
+ for (let i = 1; i < data.length; i += 1) {
475
+ const point = data[i];
476
+ const prev = data[i - 1];
477
+ const next = data[i + 1];
478
+ if (!point || !prev || !next) continue;
479
+ const day = vwapSessionDayOf(point.time.getTime());
480
+ if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
481
+ result[i] = null;
482
+ }
483
+ }
484
+ return result;
485
+ };
486
+ var computeBollingerSeries = (data, length, multiplier, source, band) => {
487
+ const basis = computeSmaSeries(data, length, source);
488
+ if (band === "basis") return basis;
489
+ const dev = computeStdDevSeries(data, length, source);
490
+ return basis.map((value, idx) => {
491
+ const sigma = dev[idx];
492
+ if (value == null || sigma == null) return null;
493
+ return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
494
+ });
495
+ };
438
496
  var computeRsiSeries = (data, length) => {
439
497
  const result = new Array(data.length).fill(null);
440
498
  if (data.length < 2) return result;
@@ -561,6 +619,59 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
561
619
  }
562
620
  ctx.restore();
563
621
  };
622
+ var rangeOfSeries = (seriesList, startIndex, endIndex, skipIndex) => {
623
+ let min = Number.POSITIVE_INFINITY;
624
+ let max = Number.NEGATIVE_INFINITY;
625
+ for (const series of seriesList) {
626
+ for (let idx = startIndex; idx <= endIndex; idx += 1) {
627
+ if (idx === skipIndex) continue;
628
+ const value = series[idx];
629
+ if (value == null || !Number.isFinite(value)) continue;
630
+ if (value < min) min = value;
631
+ if (value > max) max = value;
632
+ }
633
+ }
634
+ return min <= max ? { min, max } : null;
635
+ };
636
+ var maAutoscaleRange = (cacheKeyPrefix, compute, defaultLength) => (data, startIndex, endIndex, inputs, skipIndex) => {
637
+ const length = clampIndicatorLength(inputs.length, defaultLength);
638
+ const source = inputs.source ?? "close";
639
+ const series = withCachedSeries(`${cacheKeyPrefix}|${length}|${source}`, data, () => compute(data, length, source));
640
+ return rangeOfSeries([series], startIndex, endIndex, skipIndex);
641
+ };
642
+ var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
643
+ if (!renderContext.yFromPrice || opacity <= 0) return;
644
+ const yFromPrice = renderContext.yFromPrice;
645
+ ctx.save();
646
+ ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
647
+ ctx.fillStyle = color;
648
+ let runStart = -1;
649
+ const flushRun = (endIndexExclusive) => {
650
+ if (runStart < 0) return;
651
+ ctx.beginPath();
652
+ for (let i = runStart; i < endIndexExclusive; i += 1) {
653
+ const x = renderContext.xFromIndex(i);
654
+ const y = yFromPrice(upper[i]);
655
+ if (i === runStart) ctx.moveTo(x, y);
656
+ else ctx.lineTo(x, y);
657
+ }
658
+ for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
659
+ ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
660
+ }
661
+ ctx.closePath();
662
+ ctx.fill();
663
+ runStart = -1;
664
+ };
665
+ for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
666
+ const up = upper[index];
667
+ const lo = lower[index];
668
+ const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
669
+ if (valid && runStart < 0) runStart = index;
670
+ if (!valid) flushRun(index);
671
+ }
672
+ flushRun(renderContext.endIndex + 1);
673
+ ctx.restore();
674
+ };
564
675
  var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
565
676
  const visible = [];
566
677
  for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
@@ -745,7 +856,8 @@ var BUILTIN_SMA_INDICATOR = {
745
856
  () => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
746
857
  );
747
858
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
748
- }
859
+ },
860
+ getAutoscaleRange: maAutoscaleRange("sma", computeSmaSeries, 20)
749
861
  };
750
862
  var BUILTIN_EMA_INDICATOR = {
751
863
  id: "ema",
@@ -760,7 +872,8 @@ var BUILTIN_EMA_INDICATOR = {
760
872
  () => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
761
873
  );
762
874
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
763
- }
875
+ },
876
+ getAutoscaleRange: maAutoscaleRange("ema", computeEmaSeries, 20)
764
877
  };
765
878
  var BUILTIN_WMA_INDICATOR = {
766
879
  id: "wma",
@@ -775,7 +888,8 @@ var BUILTIN_WMA_INDICATOR = {
775
888
  () => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
776
889
  );
777
890
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
778
- }
891
+ },
892
+ getAutoscaleRange: maAutoscaleRange("wma", computeWmaSeries, 20)
779
893
  };
780
894
  var BUILTIN_VWMA_INDICATOR = {
781
895
  id: "vwma",
@@ -790,7 +904,8 @@ var BUILTIN_VWMA_INDICATOR = {
790
904
  () => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
791
905
  );
792
906
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
793
- }
907
+ },
908
+ getAutoscaleRange: maAutoscaleRange("vwma", computeVwmaSeries, 20)
794
909
  };
795
910
  var BUILTIN_RMA_INDICATOR = {
796
911
  id: "rma",
@@ -805,7 +920,8 @@ var BUILTIN_RMA_INDICATOR = {
805
920
  () => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
806
921
  );
807
922
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
808
- }
923
+ },
924
+ getAutoscaleRange: maAutoscaleRange("rma", computeRmaSeries, 14)
809
925
  };
810
926
  var BUILTIN_HMA_INDICATOR = {
811
927
  id: "hma",
@@ -820,6 +936,127 @@ var BUILTIN_HMA_INDICATOR = {
820
936
  () => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
821
937
  );
822
938
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
939
+ },
940
+ getAutoscaleRange: maAutoscaleRange("hma", computeHmaSeries, 21)
941
+ };
942
+ var BUILTIN_VWAP_INDICATOR = {
943
+ id: "vwap",
944
+ name: "VWAP",
945
+ pane: "overlay",
946
+ defaultInputs: {
947
+ color: "#f59e0b",
948
+ width: 2,
949
+ showBands: false,
950
+ bandMultiplier: 1,
951
+ bandColor: "#94a3b8",
952
+ bandFillOpacity: 0.06
953
+ },
954
+ draw: (ctx, renderContext, inputs) => {
955
+ const vwap = withCachedSeries(
956
+ "vwap|line",
957
+ renderContext.data,
958
+ () => computeVwapSeries(renderContext.data, "vwap")
959
+ );
960
+ if (inputs.showBands) {
961
+ const sigma = withCachedSeries(
962
+ "vwap|sigma",
963
+ renderContext.data,
964
+ () => computeVwapSeries(renderContext.data, "sigma")
965
+ );
966
+ const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
967
+ const upper = vwap.map((value, idx) => {
968
+ const s = sigma[idx];
969
+ return value == null || s == null ? null : value + multiplier * s;
970
+ });
971
+ const lower = vwap.map((value, idx) => {
972
+ const s = sigma[idx];
973
+ return value == null || s == null ? null : value - multiplier * s;
974
+ });
975
+ const bandColor = inputs.bandColor ?? "#94a3b8";
976
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
977
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
978
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
979
+ }
980
+ drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
981
+ },
982
+ getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
983
+ const vwap = withCachedSeries("vwap|line", data, () => computeVwapSeries(data, "vwap"));
984
+ if (!inputs.showBands) {
985
+ return rangeOfSeries([vwap], startIndex, endIndex, skipIndex);
986
+ }
987
+ const sigma = withCachedSeries("vwap|sigma", data, () => computeVwapSeries(data, "sigma"));
988
+ const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
989
+ let min = Number.POSITIVE_INFINITY;
990
+ let max = Number.NEGATIVE_INFINITY;
991
+ for (let idx = startIndex; idx <= endIndex; idx += 1) {
992
+ if (idx === skipIndex) continue;
993
+ const value = vwap[idx];
994
+ const s = sigma[idx];
995
+ if (value == null || s == null) continue;
996
+ const lo = value - multiplier * s;
997
+ const hi = value + multiplier * s;
998
+ if (lo < min) min = lo;
999
+ if (hi > max) max = hi;
1000
+ }
1001
+ return min <= max ? { min, max } : null;
1002
+ }
1003
+ };
1004
+ var BUILTIN_BOLLINGER_INDICATOR = {
1005
+ id: "bollinger",
1006
+ name: "BB",
1007
+ pane: "overlay",
1008
+ // Basis is pink so it stays distinguishable from the (orange) VWAP line
1009
+ // when both overlays are active.
1010
+ defaultInputs: {
1011
+ length: 20,
1012
+ multiplier: 2,
1013
+ source: "close",
1014
+ basisColor: "#ec4899",
1015
+ bandColor: "#3b82f6",
1016
+ width: 1.5,
1017
+ fillOpacity: 0.05
1018
+ },
1019
+ draw: (ctx, renderContext, inputs) => {
1020
+ const length = clampIndicatorLength(inputs.length, 20);
1021
+ const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
1022
+ const source = inputs.source ?? "close";
1023
+ const basis = withCachedSeries(
1024
+ `bb|basis|${length}|${source}`,
1025
+ renderContext.data,
1026
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
1027
+ );
1028
+ const upper = withCachedSeries(
1029
+ `bb|upper|${length}|${multiplier}|${source}`,
1030
+ renderContext.data,
1031
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
1032
+ );
1033
+ const lower = withCachedSeries(
1034
+ `bb|lower|${length}|${multiplier}|${source}`,
1035
+ renderContext.data,
1036
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
1037
+ );
1038
+ const bandColor = inputs.bandColor ?? "#3b82f6";
1039
+ const width = Number(inputs.width) || 1.5;
1040
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
1041
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
1042
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
1043
+ drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#ec4899", width);
1044
+ },
1045
+ getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
1046
+ const length = clampIndicatorLength(inputs.length, 20);
1047
+ const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
1048
+ const source = inputs.source ?? "close";
1049
+ const upper = withCachedSeries(
1050
+ `bb|upper|${length}|${multiplier}|${source}`,
1051
+ data,
1052
+ () => computeBollingerSeries(data, length, multiplier, source, "upper")
1053
+ );
1054
+ const lower = withCachedSeries(
1055
+ `bb|lower|${length}|${multiplier}|${source}`,
1056
+ data,
1057
+ () => computeBollingerSeries(data, length, multiplier, source, "lower")
1058
+ );
1059
+ return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
823
1060
  }
824
1061
  };
825
1062
  var BUILTIN_STDDEV_INDICATOR = {
@@ -905,11 +1142,27 @@ var BUILTIN_INDICATORS = [
905
1142
  BUILTIN_VWMA_INDICATOR,
906
1143
  BUILTIN_RMA_INDICATOR,
907
1144
  BUILTIN_HMA_INDICATOR,
1145
+ BUILTIN_VWAP_INDICATOR,
1146
+ BUILTIN_BOLLINGER_INDICATOR,
908
1147
  BUILTIN_STDDEV_INDICATOR,
909
1148
  BUILTIN_ATR_INDICATOR
910
1149
  ];
911
1150
  function createChart(element, options = {}) {
912
1151
  let mergedOptions = mergeChartOptions(DEFAULT_OPTIONS, options);
1152
+ let resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1153
+ let resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
1154
+ let resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
1155
+ let resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
1156
+ let resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1157
+ let resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
1158
+ const refreshResolvedOptions = () => {
1159
+ resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1160
+ resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
1161
+ resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
1162
+ resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
1163
+ resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1164
+ resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
1165
+ };
913
1166
  let width = mergedOptions.width;
914
1167
  let height = mergedOptions.height;
915
1168
  let data = [];
@@ -946,8 +1199,11 @@ function createChart(element, options = {}) {
946
1199
  pane: indicator.pane ?? plugin?.pane ?? "overlay",
947
1200
  ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
948
1201
  zIndex: Math.round(Number(indicator.zIndex) || 0),
949
- excludeFromAutoscale: indicator.excludeFromAutoscale ?? true,
950
- overlayScaleWeight: Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0.25)),
1202
+ // Overlay series participate in autoscale by default (lightweight-charts
1203
+ // semantics): bands and MAs expand the price range instead of clipping
1204
+ // at the chart edge. Hosts can still opt out per indicator.
1205
+ excludeFromAutoscale: indicator.excludeFromAutoscale ?? false,
1206
+ overlayScaleWeight: indicator.overlayScaleWeight === void 0 ? 1 : Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0)),
951
1207
  inputs: {
952
1208
  ...defaults,
953
1209
  ...indicator.inputs ?? {}
@@ -1149,6 +1405,9 @@ function createChart(element, options = {}) {
1149
1405
  canvas.setAttribute("draggable", "false");
1150
1406
  element.innerHTML = "";
1151
1407
  element.appendChild(canvas);
1408
+ const baseCanvas = document.createElement("canvas");
1409
+ const baseCtx = baseCanvas.getContext("2d");
1410
+ let overlayLayout = null;
1152
1411
  const margin = { top: 16, right: 72, bottom: 26, left: 12 };
1153
1412
  let cachedRightMargin = margin.right;
1154
1413
  const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
@@ -1188,6 +1447,20 @@ function createChart(element, options = {}) {
1188
1447
  const clamp = (value, min, max) => {
1189
1448
  return Math.min(max, Math.max(min, value));
1190
1449
  };
1450
+ const textWidthCache = /* @__PURE__ */ new Map();
1451
+ const measureTextWidth = (text) => {
1452
+ const key = `${ctx.font}\0${text}`;
1453
+ const cached = textWidthCache.get(key);
1454
+ if (cached !== void 0) {
1455
+ return cached;
1456
+ }
1457
+ const width2 = ctx.measureText(text).width;
1458
+ if (textWidthCache.size >= 4096) {
1459
+ textWidthCache.clear();
1460
+ }
1461
+ textWidthCache.set(key, width2);
1462
+ return width2;
1463
+ };
1191
1464
  const resetLabelSlots = (enabled) => {
1192
1465
  noOverlappingLineLabels = enabled;
1193
1466
  rightAxisLabelSlots = [];
@@ -1390,7 +1663,7 @@ function createChart(element, options = {}) {
1390
1663
  return `${integerPart}${decimalPart}`;
1391
1664
  };
1392
1665
  const getMeasuredLabelWidth = (text, paddingX) => {
1393
- return Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1666
+ return Math.ceil(measureTextWidth(text)) + paddingX * 2;
1394
1667
  };
1395
1668
  const getStabilizedNumericLabelWidth = (text, paddingX) => {
1396
1669
  const measured = getMeasuredLabelWidth(text, paddingX);
@@ -1631,11 +1904,11 @@ function createChart(element, options = {}) {
1631
1904
  return;
1632
1905
  }
1633
1906
  const labelText = mergedLine.label ?? formatPrice(mergedLine.price);
1634
- const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1907
+ const axis = resolvedAxis;
1635
1908
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1636
1909
  const labelPaddingX = 8;
1637
1910
  const labelHeight = 20;
1638
- const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
1911
+ const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(measureTextWidth(labelText)) + labelPaddingX * 2;
1639
1912
  const labelWidth = getRightAxisLabelWidth(chartRight, measuredLabelWidth);
1640
1913
  const labelX = getRightAxisLabelX(chartRight);
1641
1914
  const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
@@ -1730,7 +2003,7 @@ function createChart(element, options = {}) {
1730
2003
  const closeAction = mergedLine.type === "market" ? "close" : "cancel";
1731
2004
  const showCloseButton = mergedLine.showCloseButton;
1732
2005
  const actionButtonText = mergedLine.actionButtonText;
1733
- const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
2006
+ const axis = resolvedAxis;
1734
2007
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1735
2008
  const legacyButton = actionButtonText ? [
1736
2009
  {
@@ -1752,7 +2025,7 @@ function createChart(element, options = {}) {
1752
2025
  const actionButtonMetrics = actionButtons.map((button) => {
1753
2026
  const paddingX = Math.max(2, button.paddingX ?? mergedLine.actionButtonPaddingX);
1754
2027
  const minWidth = Math.max(16, button.minWidth ?? mergedLine.actionButtonMinWidth);
1755
- const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
2028
+ const width2 = Math.max(minWidth, Math.ceil(measureTextWidth(button.text)) + paddingX * 2);
1756
2029
  return { button, width: width2 };
1757
2030
  });
1758
2031
  const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
@@ -1760,8 +2033,8 @@ function createChart(element, options = {}) {
1760
2033
  const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
1761
2034
  const segmentPaddingX = 8;
1762
2035
  const labelHeight = 22;
1763
- const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
1764
- const centerMeasuredWidth = Math.ceil(ctx.measureText(centerText).width) + segmentPaddingX * 2;
2036
+ const qtyWidth = qtyText ? Math.ceil(measureTextWidth(qtyText)) + segmentPaddingX * 2 : 0;
2037
+ const centerMeasuredWidth = Math.ceil(measureTextWidth(centerText)) + segmentPaddingX * 2;
1765
2038
  const centerWidth = mergedLine.id === void 0 ? centerMeasuredWidth : Math.max(centerMeasuredWidth, orderWidgetWidthById.get(mergedLine.id) ?? 0);
1766
2039
  if (mergedLine.id) {
1767
2040
  orderWidgetWidthById.set(mergedLine.id, centerWidth);
@@ -1769,7 +2042,7 @@ function createChart(element, options = {}) {
1769
2042
  const closeWidth = showCloseButton ? 24 : 0;
1770
2043
  const mainWidgetWidth = qtyWidth + centerWidth + closeWidth;
1771
2044
  const totalWidth = mainWidgetWidth + actionButtonsGap + actionButtonsTotalWidth;
1772
- const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
2045
+ const crosshair = resolvedCrosshair;
1773
2046
  const crosshairActionInset = crosshair.visible && crosshair.showPriceLabel && crosshair.showPriceActionButton ? Math.max(
1774
2047
  0,
1775
2048
  Math.round(crosshair.priceActionButtonGap) + (clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4)) + 10) - 4
@@ -1950,20 +2223,29 @@ function createChart(element, options = {}) {
1950
2223
  }
1951
2224
  }
1952
2225
  crosshairPoint = point;
1953
- scheduleDraw();
2226
+ scheduleDraw({ overlayOnly: true });
1954
2227
  };
1955
2228
  let drawRafId = null;
1956
2229
  let pendingDrawUpdateAutoScale = false;
2230
+ let pendingDrawOverlayOnly = true;
1957
2231
  const scheduleDraw = (options2 = {}) => {
1958
- pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || (options2.updateAutoScale ?? true);
2232
+ const overlayOnly = options2.overlayOnly ?? false;
2233
+ pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
2234
+ pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
1959
2235
  if (drawRafId !== null) {
1960
2236
  return;
1961
2237
  }
1962
2238
  drawRafId = requestAnimationFrame(() => {
1963
2239
  drawRafId = null;
1964
2240
  const updateAutoScale = pendingDrawUpdateAutoScale;
2241
+ const flushOverlayOnly = pendingDrawOverlayOnly;
1965
2242
  pendingDrawUpdateAutoScale = false;
1966
- draw({ updateAutoScale });
2243
+ pendingDrawOverlayOnly = true;
2244
+ if (flushOverlayOnly) {
2245
+ drawOverlayOnly();
2246
+ } else {
2247
+ draw({ updateAutoScale });
2248
+ }
1967
2249
  });
1968
2250
  };
1969
2251
  const draw = (options2 = {}) => {
@@ -1977,15 +2259,15 @@ function createChart(element, options = {}) {
1977
2259
  canvas.width = Math.floor(width * pixelRatio);
1978
2260
  canvas.height = Math.floor(height * pixelRatio);
1979
2261
  ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1980
- const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1981
- const xAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
1982
- const yAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
2262
+ const axis = resolvedAxis;
2263
+ const xAxis = resolvedXAxis;
2264
+ const yAxis = resolvedYAxis;
1983
2265
  const xAxisFontSize = Math.max(8, xAxis.fontSize);
1984
2266
  const yAxisFontSize = Math.max(8, yAxis.fontSize);
1985
2267
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1986
2268
  ctx.fillStyle = mergedOptions.backgroundColor;
1987
2269
  ctx.fillRect(0, 0, width, height);
1988
- const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2270
+ const labels = resolvedLabels;
1989
2271
  const estimateRightMargin = () => {
1990
2272
  const paddingX = Math.max(4, labels.labelPaddingX);
1991
2273
  let required = margin.right - 1;
@@ -2098,6 +2380,7 @@ function createChart(element, options = {}) {
2098
2380
  yMax: 0
2099
2381
  };
2100
2382
  drawText("Load OHLC data to render chart", chartLeft + 10, chartTop + 20, "left", "middle", "#64748b");
2383
+ overlayLayout = null;
2101
2384
  return;
2102
2385
  }
2103
2386
  clampXViewport();
@@ -2105,62 +2388,39 @@ function createChart(element, options = {}) {
2105
2388
  const xEnd = xStart + xSpan;
2106
2389
  const startIndex = Math.max(0, Math.floor(xStart));
2107
2390
  const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
2108
- const visibleData = data.slice(startIndex, endIndex + 1);
2109
- let priceSource = visibleData.length > 0 ? visibleData : data;
2110
- if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1) {
2111
- const latestIndex = data.length - 1;
2112
- const filtered = priceSource.filter((_, offset) => startIndex + offset !== latestIndex);
2113
- if (filtered.length > 0) {
2114
- priceSource = filtered;
2115
- } else {
2116
- const fallbackWindow = 120;
2117
- const fallbackStart = Math.max(0, latestIndex - fallbackWindow);
2118
- const fallback = data.slice(fallbackStart, latestIndex);
2119
- if (fallback.length > 0) {
2120
- priceSource = fallback;
2121
- }
2391
+ const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
2392
+ const scanCandleRange = (from, to) => {
2393
+ let min = Number.POSITIVE_INFINITY;
2394
+ let max = Number.NEGATIVE_INFINITY;
2395
+ for (let idx = from; idx <= to; idx += 1) {
2396
+ if (idx === skipLatestIndex) continue;
2397
+ const point = data[idx];
2398
+ if (!point) continue;
2399
+ if (point.l < min) min = point.l;
2400
+ if (point.h > max) max = point.h;
2122
2401
  }
2402
+ return min <= max ? { min, max } : null;
2403
+ };
2404
+ let candleRange = scanCandleRange(startIndex, endIndex);
2405
+ if (!candleRange) {
2406
+ const latestIndex = data.length - 1;
2407
+ candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? { min: data[latestIndex].l, max: data[latestIndex].h };
2123
2408
  }
2124
- let minPrice = Math.min(...priceSource.map((point) => point.l));
2125
- let maxPrice = Math.max(...priceSource.map((point) => point.h));
2409
+ let minPrice = candleRange.min;
2410
+ let maxPrice = candleRange.max;
2126
2411
  if (overlayIndicatorsForScale.length > 0) {
2127
- for (const { indicator } of overlayIndicatorsForScale) {
2412
+ for (const { indicator, plugin } of overlayIndicatorsForScale) {
2128
2413
  if (indicator.excludeFromAutoscale) {
2129
2414
  continue;
2130
2415
  }
2131
- const type = indicator.type;
2132
- const inputs = indicator.inputs;
2133
- const source = inputs.source ?? "close";
2134
- const length = clampIndicatorLength(inputs.length ?? 14, 14);
2135
- let series = null;
2136
- if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
2137
- if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
2138
- if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
2139
- if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
2140
- if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
2141
- if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
2142
- if (!series) {
2416
+ const range = plugin.getAutoscaleRange?.(data, startIndex, endIndex, indicator.inputs, skipLatestIndex);
2417
+ if (!range || !Number.isFinite(range.min) || !Number.isFinite(range.max)) {
2143
2418
  continue;
2144
2419
  }
2145
- const visibleValues = [];
2146
- for (let idx = startIndex; idx <= endIndex; idx += 1) {
2147
- if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 && idx === data.length - 1) {
2148
- continue;
2149
- }
2150
- const value = series[idx];
2151
- if (Number.isFinite(value ?? Number.NaN)) {
2152
- visibleValues.push(value);
2153
- }
2154
- }
2155
- if (visibleValues.length === 0) {
2156
- continue;
2157
- }
2158
- const seriesMin = Math.min(...visibleValues);
2159
- const seriesMax = Math.max(...visibleValues);
2160
2420
  const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
2161
2421
  const currentMid = (minPrice + maxPrice) / 2;
2162
- const weightedMin = currentMid + (seriesMin - currentMid) * weight;
2163
- const weightedMax = currentMid + (seriesMax - currentMid) * weight;
2422
+ const weightedMin = currentMid + (range.min - currentMid) * weight;
2423
+ const weightedMax = currentMid + (range.max - currentMid) * weight;
2164
2424
  minPrice = Math.min(minPrice, weightedMin);
2165
2425
  maxPrice = Math.max(maxPrice, weightedMax);
2166
2426
  }
@@ -2235,7 +2495,7 @@ function createChart(element, options = {}) {
2235
2495
  ctx.save();
2236
2496
  const prevFont = ctx.font;
2237
2497
  ctx.font = `500 12px ${mergedOptions.fontFamily}`;
2238
- const textWidth = ctx.measureText(labelText).width;
2498
+ const textWidth = measureTextWidth(labelText);
2239
2499
  const paddingX = 6;
2240
2500
  const bgWidth = textWidth + paddingX * 2;
2241
2501
  const bgHeight = 18;
@@ -2401,7 +2661,7 @@ function createChart(element, options = {}) {
2401
2661
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2402
2662
  levelLines.forEach((level, index) => {
2403
2663
  const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2404
- const textWidth = ctx.measureText(labelText).width;
2664
+ const textWidth = measureTextWidth(labelText);
2405
2665
  const padding = 4;
2406
2666
  const bgX = lineLeft + 4;
2407
2667
  const bgY = level.y - 9;
@@ -2474,7 +2734,7 @@ function createChart(element, options = {}) {
2474
2734
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2475
2735
  levelLines.forEach((level, index) => {
2476
2736
  const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2477
- const textWidth = ctx.measureText(labelText).width;
2737
+ const textWidth = measureTextWidth(labelText);
2478
2738
  const padding = 4;
2479
2739
  const bgX = chartLeft + 4;
2480
2740
  const bgY = level.y - 9;
@@ -2617,8 +2877,8 @@ function createChart(element, options = {}) {
2617
2877
  const padding = 6;
2618
2878
  const lineH = 14;
2619
2879
  const isDigit = (ch) => ch >= "0" && ch <= "9";
2620
- const digitW = ctx.measureText("0").width;
2621
- const charAdvance = (ch) => isDigit(ch) ? digitW : ctx.measureText(ch).width;
2880
+ const digitW = measureTextWidth("0");
2881
+ const charAdvance = (ch) => isDigit(ch) ? digitW : measureTextWidth(ch);
2622
2882
  const lineWidth = (line) => {
2623
2883
  let w = 0;
2624
2884
  for (const ch of line) w += charAdvance(ch);
@@ -2641,11 +2901,11 @@ function createChart(element, options = {}) {
2641
2901
  const y = startY + lineIndex * lineH;
2642
2902
  for (const ch of line) {
2643
2903
  if (isDigit(ch)) {
2644
- ctx.fillText(ch, x + (digitW - ctx.measureText(ch).width) / 2, y);
2904
+ ctx.fillText(ch, x + (digitW - measureTextWidth(ch)) / 2, y);
2645
2905
  x += digitW;
2646
2906
  } else {
2647
2907
  ctx.fillText(ch, x, y);
2648
- x += ctx.measureText(ch).width;
2908
+ x += measureTextWidth(ch);
2649
2909
  }
2650
2910
  }
2651
2911
  });
@@ -2757,15 +3017,31 @@ function createChart(element, options = {}) {
2757
3017
  const pct = diff / base * 100;
2758
3018
  const ticks = tick > 0 ? Math.round(diff / tick) : 0;
2759
3019
  const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
2760
- const labelText = tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`;
3020
+ const labelLines = [
3021
+ tick > 0 ? `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)}) ${signed(ticks, String(Math.abs(ticks)))}` : `${signed(diff, formatPrice(Math.abs(diff)))} (${signed(pct, `${Math.abs(pct).toFixed(2)}%`)})`
3022
+ ];
3023
+ const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
3024
+ if (barSpan > 0) {
3025
+ const t0 = getTimeForIndex(Math.round(p0.index));
3026
+ const t1 = getTimeForIndex(Math.round(p1.index));
3027
+ const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
3028
+ labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
3029
+ }
3030
+ const pointValue = Number(drawing.pointValue);
3031
+ if (Number.isFinite(pointValue) && pointValue > 0) {
3032
+ const dollars = diff * pointValue;
3033
+ const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
3034
+ labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
3035
+ }
2761
3036
  const prevFont = ctx.font;
2762
3037
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2763
3038
  const padding = 6;
2764
- const textW = ctx.measureText(labelText).width;
3039
+ const lineHeight = 15;
3040
+ const textW = Math.max(...labelLines.map((line) => measureTextWidth(line)));
2765
3041
  const pillW = textW + padding * 2;
2766
- const pillH = 18;
3042
+ const pillH = labelLines.length * lineHeight + padding;
2767
3043
  const pillX = midX - pillW / 2;
2768
- const pillY = botY + 6;
3044
+ const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
2769
3045
  ctx.fillStyle = mergedOptions.backgroundColor;
2770
3046
  fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2771
3047
  ctx.save();
@@ -2776,7 +3052,9 @@ function createChart(element, options = {}) {
2776
3052
  ctx.fillStyle = drawing.color;
2777
3053
  ctx.textAlign = "center";
2778
3054
  ctx.textBaseline = "middle";
2779
- ctx.fillText(labelText, midX, pillY + pillH / 2);
3055
+ labelLines.forEach((line, lineIndex) => {
3056
+ ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
3057
+ });
2780
3058
  ctx.font = prevFont;
2781
3059
  if (drawing.label) {
2782
3060
  drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
@@ -2795,7 +3073,7 @@ function createChart(element, options = {}) {
2795
3073
  ctx.textBaseline = "middle";
2796
3074
  const lines = (drawing.label ?? "").split("\n");
2797
3075
  const lineH = Math.round(fontSize * 1.35);
2798
- const textW = Math.max(1, ...lines.map((line) => ctx.measureText(line).width));
3076
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
2799
3077
  const padX = isNote ? 8 : 2;
2800
3078
  const padY = isNote ? 6 : 1;
2801
3079
  const blockW = textW + padX * 2;
@@ -2877,47 +3155,114 @@ function createChart(element, options = {}) {
2877
3155
  Math.max(1, candleSpacing - 1),
2878
3156
  Math.max(candleMinWidth, Math.floor(candleSpacing * candleBodyWidthRatio))
2879
3157
  );
2880
- const grid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
3158
+ const grid = resolvedGrid;
2881
3159
  const gridOpacity = clamp(grid.opacity, 0, 1);
2882
3160
  const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
2883
3161
  const yTicks = Math.max(1, Math.floor(yTickCountInput));
2884
3162
  const gridColor = grid.color ?? mergedOptions.gridColor;
3163
+ const priceTicks = [];
3164
+ {
3165
+ const targetStep = yRange / Math.max(1, yTicks);
3166
+ let step;
3167
+ if (targetStep > 0 && Number.isFinite(targetStep)) {
3168
+ const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
3169
+ const frac = targetStep / base;
3170
+ step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
3171
+ const instrumentTick = getConfiguredTickSize();
3172
+ if (instrumentTick > 0 && step < instrumentTick * 1e6) {
3173
+ step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
3174
+ }
3175
+ } else {
3176
+ step = 1;
3177
+ }
3178
+ const firstTick = Math.ceil(yMin / step) * step;
3179
+ for (let i = 0; ; i += 1) {
3180
+ const price = firstTick + i * step;
3181
+ if (price > yMax + step * 1e-6) break;
3182
+ priceTicks.push(price);
3183
+ if (priceTicks.length > 200) break;
3184
+ }
3185
+ }
2885
3186
  if (grid.horizontalLines) {
2886
3187
  ctx.save();
2887
3188
  ctx.globalAlpha = gridOpacity;
2888
- for (let tick = 0; tick <= yTicks; tick += 1) {
2889
- const ratio = tick / yTicks;
2890
- const price = yMin + yRange * ratio;
3189
+ ctx.strokeStyle = gridColor;
3190
+ ctx.lineWidth = 1;
3191
+ ctx.beginPath();
3192
+ for (const price of priceTicks) {
2891
3193
  const y = yFromPrice(price);
2892
- ctx.strokeStyle = gridColor;
2893
- ctx.lineWidth = 1;
2894
- ctx.beginPath();
2895
3194
  ctx.moveTo(crisp(chartLeft), crisp(y));
2896
3195
  ctx.lineTo(crisp(chartRight), crisp(y));
2897
- ctx.stroke();
2898
3196
  }
3197
+ ctx.stroke();
2899
3198
  ctx.restore();
2900
3199
  }
2901
- const minLabelSpacingPx = Math.max(72, candleSpacing * 6);
2902
- const autoLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
2903
- const xTickCountInput = grid.xTickCount ?? autoLabelCount;
2904
- const xTickCount = Math.max(2, Math.floor(xTickCountInput));
2905
- const rawStep = xSpan / xTickCount;
2906
- const xStep = Math.max(1, Math.ceil(rawStep));
2907
- const visibleTickStart = Math.floor(xStart);
3200
+ const minLabelSpacingPx = Math.max(60, Math.min(120, chartWidth / Math.max(2, Math.floor(grid.xTickCount ?? 8))));
3201
+ const visibleTickStart = Math.max(0, Math.floor(xStart));
2908
3202
  const visibleTickEnd = Math.ceil(xEnd) - 1;
2909
- const tickStartIndex = Math.ceil(visibleTickStart / xStep) * xStep;
2910
- if (grid.verticalLines) {
3203
+ const timeTicks = [];
3204
+ {
3205
+ const weightBuckets = [[], [], [], [], [], [], [], [], [], []];
3206
+ let prevTime = visibleTickStart > 0 ? getTimeForIndex(visibleTickStart - 1) : null;
3207
+ for (let index = visibleTickStart; index <= visibleTickEnd; index += 1) {
3208
+ const time = getTimeForIndex(index);
3209
+ if (!time) {
3210
+ prevTime = null;
3211
+ continue;
3212
+ }
3213
+ let weight = 0;
3214
+ if (!prevTime) {
3215
+ weight = 7;
3216
+ } else {
3217
+ if (prevTime.getFullYear() !== time.getFullYear()) weight = 9;
3218
+ else if (prevTime.getMonth() !== time.getMonth()) weight = 8;
3219
+ else if (prevTime.getDate() !== time.getDate()) weight = 7;
3220
+ else {
3221
+ const prevMinutes = prevTime.getHours() * 60 + prevTime.getMinutes();
3222
+ const curMinutes = time.getHours() * 60 + time.getMinutes();
3223
+ const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
3224
+ if (crossed(360)) weight = 6;
3225
+ else if (crossed(60)) weight = 5;
3226
+ else if (crossed(30)) weight = 4;
3227
+ else if (crossed(15)) weight = 3;
3228
+ else if (crossed(5)) weight = 2;
3229
+ else weight = 1;
3230
+ }
3231
+ }
3232
+ weightBuckets[weight].push(index);
3233
+ prevTime = time;
3234
+ }
3235
+ const placedXs = [];
3236
+ const maxMarks = Math.floor(chartWidth / minLabelSpacingPx) + 2;
3237
+ for (let weight = 9; weight >= 1 && timeTicks.length < maxMarks; weight -= 1) {
3238
+ for (const index of weightBuckets[weight]) {
3239
+ const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
3240
+ let fits = true;
3241
+ for (const placedX of placedXs) {
3242
+ if (Math.abs(placedX - x) < minLabelSpacingPx) {
3243
+ fits = false;
3244
+ break;
3245
+ }
3246
+ }
3247
+ if (!fits) continue;
3248
+ placedXs.push(x);
3249
+ timeTicks.push({ index, weight, x });
3250
+ if (timeTicks.length >= maxMarks) break;
3251
+ }
3252
+ }
3253
+ timeTicks.sort((a, b) => a.index - b.index);
3254
+ }
3255
+ if (grid.verticalLines && timeTicks.length > 0) {
2911
3256
  ctx.save();
2912
3257
  ctx.globalAlpha = gridOpacity;
2913
- for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
2914
- const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
2915
- ctx.strokeStyle = gridColor;
2916
- ctx.beginPath();
2917
- ctx.moveTo(crisp(x), crisp(chartTop));
2918
- ctx.lineTo(crisp(x), crisp(fullChartBottom));
2919
- ctx.stroke();
3258
+ ctx.strokeStyle = gridColor;
3259
+ ctx.lineWidth = 1;
3260
+ ctx.beginPath();
3261
+ for (const tick of timeTicks) {
3262
+ ctx.moveTo(crisp(tick.x), crisp(chartTop));
3263
+ ctx.lineTo(crisp(tick.x), crisp(fullChartBottom));
2920
3264
  }
3265
+ ctx.stroke();
2921
3266
  ctx.restore();
2922
3267
  }
2923
3268
  if (grid.sessionSeparators && data.length > 1) {
@@ -2955,38 +3300,45 @@ function createChart(element, options = {}) {
2955
3300
  }
2956
3301
  return data[index]?.v;
2957
3302
  };
2958
- for (let index = startIndex; index <= endIndex; index += 1) {
2959
- const point = data[index];
2960
- if (!point) {
2961
- continue;
3303
+ {
3304
+ const upWicks = new Path2D();
3305
+ const downWicks = new Path2D();
3306
+ const upBodies = new Path2D();
3307
+ const downBodies = new Path2D();
3308
+ for (let index = startIndex; index <= endIndex; index += 1) {
3309
+ const point = data[index];
3310
+ if (!point) {
3311
+ continue;
3312
+ }
3313
+ const isLastCandle = useSmoothedCandle && index === lastDataIndex;
3314
+ const actualDirection = point.c >= point.o ? "up" : "down";
3315
+ const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
3316
+ const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
3317
+ const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
3318
+ const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
3319
+ const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
3320
+ const isUp = direction === "up";
3321
+ const roundedCenterX = Math.round(centerX);
3322
+ const wicks = isUp ? upWicks : downWicks;
3323
+ wicks.moveTo(roundedCenterX + 0.5, crisp(yFromPrice(displayHigh)));
3324
+ wicks.lineTo(roundedCenterX + 0.5, crisp(yFromPrice(displayLow)));
3325
+ const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
3326
+ const openYPx = Math.round(yFromPrice(point.o));
3327
+ const closeYPx = Math.round(yFromPrice(displayClose));
3328
+ const bodyIsUp = displayClose >= point.o;
3329
+ const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
3330
+ const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
3331
+ (isUp ? upBodies : downBodies).rect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
2962
3332
  }
2963
- const isLastCandle = useSmoothedCandle && index === lastDataIndex;
2964
- const actualDirection = point.c >= point.o ? "up" : "down";
2965
- const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
2966
- const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
2967
- const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
2968
- const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
2969
- const openY = yFromPrice(point.o);
2970
- const closeY = yFromPrice(displayClose);
2971
- const highY = yFromPrice(displayHigh);
2972
- const lowY = yFromPrice(displayLow);
2973
- const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
2974
- const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
2975
- const roundedCenterX = Math.round(centerX);
2976
- ctx.strokeStyle = candleColor;
2977
3333
  ctx.lineWidth = candleWickWidth;
2978
- ctx.beginPath();
2979
- ctx.moveTo(roundedCenterX + 0.5, crisp(highY));
2980
- ctx.lineTo(roundedCenterX + 0.5, crisp(lowY));
2981
- ctx.stroke();
2982
- const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
2983
- const openYPx = Math.round(openY);
2984
- const closeYPx = Math.round(closeY);
2985
- const bodyIsUp = displayClose >= point.o;
2986
- const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
2987
- const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
2988
- ctx.fillStyle = candleColor;
2989
- ctx.fillRect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
3334
+ ctx.strokeStyle = mergedOptions.upColor;
3335
+ ctx.stroke(upWicks);
3336
+ ctx.fillStyle = mergedOptions.upColor;
3337
+ ctx.fill(upBodies);
3338
+ ctx.strokeStyle = mergedOptions.downColor;
3339
+ ctx.stroke(downWicks);
3340
+ ctx.fillStyle = mergedOptions.downColor;
3341
+ ctx.fill(downBodies);
2990
3342
  }
2991
3343
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
2992
3344
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
@@ -3104,56 +3456,6 @@ function createChart(element, options = {}) {
3104
3456
  ctx.textAlign = prevAlign;
3105
3457
  ctx.textBaseline = prevBaseline;
3106
3458
  }
3107
- const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
3108
- if (crosshair.visible && crosshairPoint) {
3109
- const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
3110
- const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
3111
- if (crosshair.mode === "dot") {
3112
- ctx.save();
3113
- ctx.fillStyle = crosshair.color;
3114
- ctx.beginPath();
3115
- ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
3116
- ctx.fill();
3117
- ctx.restore();
3118
- } else {
3119
- ctx.save();
3120
- ctx.strokeStyle = crosshair.color;
3121
- ctx.lineWidth = Math.max(1, crosshair.width);
3122
- applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
3123
- if (crosshair.showVertical) {
3124
- ctx.beginPath();
3125
- ctx.moveTo(crisp(cx), crisp(chartTop));
3126
- ctx.lineTo(crisp(cx), crisp(chartBottom));
3127
- ctx.stroke();
3128
- }
3129
- if (crosshair.showHorizontal) {
3130
- ctx.beginPath();
3131
- ctx.moveTo(crisp(chartLeft), crisp(cy));
3132
- ctx.lineTo(crisp(chartRight), crisp(cy));
3133
- ctx.stroke();
3134
- }
3135
- ctx.restore();
3136
- }
3137
- if (crosshair.sideHintLeft || crosshair.sideHintRight) {
3138
- ctx.save();
3139
- ctx.font = `600 11px ${mergedOptions.fontFamily}`;
3140
- ctx.textBaseline = "middle";
3141
- ctx.setLineDash([]);
3142
- const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
3143
- const hintGap = 8;
3144
- if (crosshair.sideHintLeft) {
3145
- ctx.fillStyle = crosshair.sideHintLeftColor;
3146
- ctx.textAlign = "right";
3147
- ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
3148
- }
3149
- if (crosshair.sideHintRight) {
3150
- ctx.fillStyle = crosshair.sideHintRightColor;
3151
- ctx.textAlign = "left";
3152
- ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
3153
- }
3154
- ctx.restore();
3155
- }
3156
- }
3157
3459
  ctx.restore();
3158
3460
  const positionAxisGutter = Math.max(0, width - chartRight);
3159
3461
  if (positionAxisGutter > 0) {
@@ -3266,7 +3568,7 @@ function createChart(element, options = {}) {
3266
3568
  const text = label.text ?? formatPaneValue(label.value);
3267
3569
  const labelPaddingX = 7;
3268
3570
  const labelHeight = Math.max(16, yAxisFontSize + 8);
3269
- const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(ctx.measureText(text).width) + labelPaddingX * 2);
3571
+ const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(measureTextWidth(text)) + labelPaddingX * 2);
3270
3572
  const labelX = getRightAxisLabelX(chartRight);
3271
3573
  const labelY = clamp(yFromPaneValue(label.value) - labelHeight / 2, paneTop + 2, paneBottom - labelHeight - 2);
3272
3574
  ctx.fillStyle = label.backgroundColor ?? label.color ?? labels.backgroundColor;
@@ -3278,18 +3580,6 @@ function createChart(element, options = {}) {
3278
3580
  }
3279
3581
  });
3280
3582
  }
3281
- if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
3282
- const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
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
- ctx.beginPath();
3288
- ctx.moveTo(crisp(cx), crisp(chartTop));
3289
- ctx.lineTo(crisp(cx), crisp(fullChartBottom));
3290
- ctx.stroke();
3291
- ctx.restore();
3292
- }
3293
3583
  ctx.strokeStyle = axis.lineColor;
3294
3584
  ctx.lineWidth = Math.max(1, axis.lineWidth);
3295
3585
  ctx.beginPath();
@@ -3299,13 +3589,13 @@ function createChart(element, options = {}) {
3299
3589
  ctx.lineTo(crisp(chartRight), crisp(fullChartBottom));
3300
3590
  ctx.stroke();
3301
3591
  const priceScaleTickLabelInset = yAxisFontSize / 2 + 3;
3302
- for (let tick = 0; tick <= yTicks; tick += 1) {
3303
- const ratio = tick / yTicks;
3304
- const price = yMin + yRange * ratio;
3305
- const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
3592
+ {
3306
3593
  const prevFont = ctx.font;
3307
3594
  ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
3308
- drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
3595
+ for (const price of priceTicks) {
3596
+ const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
3597
+ drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
3598
+ }
3309
3599
  ctx.font = prevFont;
3310
3600
  }
3311
3601
  resetLabelSlots(labels.noOverlapping);
@@ -3414,9 +3704,15 @@ function createChart(element, options = {}) {
3414
3704
  });
3415
3705
  }
3416
3706
  }
3417
- if (labels.showHighLow && visibleData.length > 0) {
3418
- const visibleHigh = Math.max(...visibleData.map((point) => point.h));
3419
- const visibleLow = Math.min(...visibleData.map((point) => point.l));
3707
+ if (labels.showHighLow && endIndex >= startIndex) {
3708
+ let visibleHigh = Number.NEGATIVE_INFINITY;
3709
+ let visibleLow = Number.POSITIVE_INFINITY;
3710
+ for (let idx = startIndex; idx <= endIndex; idx += 1) {
3711
+ const point = data[idx];
3712
+ if (!point) continue;
3713
+ if (point.h > visibleHigh) visibleHigh = point.h;
3714
+ if (point.l < visibleLow) visibleLow = point.l;
3715
+ }
3420
3716
  addPriceAxisLabel({
3421
3717
  text: `H ${formatPrice(visibleHigh)}`,
3422
3718
  price: visibleHigh,
@@ -3469,12 +3765,12 @@ function createChart(element, options = {}) {
3469
3765
  const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
3470
3766
  const subtextLineGap = 5;
3471
3767
  const labelHeight = baseLabelHeight + (subtexts.length > 0 ? subtexts.length * subtextFontSize + subtexts.length * subtextLineGap : 0);
3472
- const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
3768
+ const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(measureTextWidth(label.text)) + labelPaddingX * 2;
3473
3769
  let subtextWidth = 0;
3474
3770
  if (subtexts.length > 0) {
3475
3771
  const baseFont = ctx.font;
3476
3772
  ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
3477
- subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(ctx.measureText(subtext).width) + labelPaddingX * 2));
3773
+ subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(measureTextWidth(subtext)) + labelPaddingX * 2));
3478
3774
  ctx.font = baseFont;
3479
3775
  }
3480
3776
  const labelTextWidth = Math.max(primaryWidth, subtextWidth);
@@ -3552,8 +3848,9 @@ function createChart(element, options = {}) {
3552
3848
  }
3553
3849
  return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
3554
3850
  };
3851
+ const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
3555
3852
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
3556
- const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3853
+ const inputValues = Object.entries(indicator.inputs).filter(([key, value]) => !LEGEND_EXCLUDED_INPUT_KEYS.has(key) && typeof value !== "boolean" && isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3557
3854
  if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
3558
3855
  return `${plugin.name} ${inputValues.join(" ")}`;
3559
3856
  }
@@ -3577,21 +3874,15 @@ function createChart(element, options = {}) {
3577
3874
  ctx.font = prevFont;
3578
3875
  }
3579
3876
  }
3580
- const axisStepMs = getTimeStepMs();
3581
- const axisIntraday = axisStepMs > 0 && axisStepMs < 24 * 60 * 60 * 1e3;
3582
- let prevAxisTickTime = null;
3583
- for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
3584
- const tickTime = getTimeForIndex(index);
3585
- if (!tickTime) {
3586
- continue;
3587
- }
3588
- const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
3589
- const isNewDay = !prevAxisTickTime || prevAxisTickTime.getDate() !== tickTime.getDate() || prevAxisTickTime.getMonth() !== tickTime.getMonth() || prevAxisTickTime.getFullYear() !== tickTime.getFullYear();
3590
- const timeLabel = axisIntraday && !isNewDay ? tickTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false }) : tickTime.toLocaleDateString(void 0, { month: "short", day: "numeric" });
3591
- prevAxisTickTime = tickTime;
3877
+ {
3592
3878
  const prevFont = ctx.font;
3593
3879
  ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
3594
- drawText(timeLabel, x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
3880
+ for (const tick of timeTicks) {
3881
+ const tickTime = getTimeForIndex(tick.index);
3882
+ if (!tickTime) continue;
3883
+ 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 });
3884
+ drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
3885
+ }
3595
3886
  ctx.font = prevFont;
3596
3887
  }
3597
3888
  if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
@@ -3604,138 +3895,230 @@ function createChart(element, options = {}) {
3604
3895
  drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
3605
3896
  ctx.font = prevFont;
3606
3897
  }
3607
- if (crosshair.visible && crosshairPoint) {
3608
- const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
3609
- const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
3610
- const labelPaddingX = 8;
3611
- const labelHeight = 20;
3612
- const labelRadius = Math.max(0, crosshair.labelBorderRadius);
3613
- const labelBackground = crosshair.labelBackgroundColor;
3614
- const labelTextColor = crosshair.labelTextColor;
3615
- const labelBorderColor = crosshair.labelBorderColor;
3616
- const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
3617
- const labelBorderStyle = crosshair.labelBorderStyle;
3618
- const strokeCrosshairLabel = (x, y, widthValue, heightValue = labelHeight) => {
3619
- if (labelBorderWidth <= 0) {
3620
- return;
3621
- }
3622
- ctx.save();
3623
- ctx.strokeStyle = labelBorderColor;
3624
- ctx.lineWidth = labelBorderWidth;
3625
- applyDashPattern(
3626
- labelBorderStyle,
3627
- dashPatterns.borderDotted,
3628
- dashPatterns.borderDashed
3629
- );
3630
- strokeRoundedRect(Math.round(x), Math.round(y), widthValue, heightValue, labelRadius);
3631
- ctx.restore();
3632
- };
3633
- if (crosshair.showPriceLabel) {
3634
- const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
3635
- const priceText = formatPrice(hoverPrice);
3636
- const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
3637
- const priceX = getRightAxisLabelX(chartRight);
3638
- const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
3898
+ if (baseCtx) {
3899
+ if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
3900
+ baseCanvas.width = canvas.width;
3901
+ baseCanvas.height = canvas.height;
3902
+ }
3903
+ baseCtx.setTransform(1, 0, 0, 1, 0, 0);
3904
+ baseCtx.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
3905
+ baseCtx.drawImage(canvas, 0, 0);
3906
+ }
3907
+ overlayLayout = {
3908
+ chartLeft,
3909
+ chartTop,
3910
+ chartRight,
3911
+ chartBottom,
3912
+ chartWidth,
3913
+ chartHeight,
3914
+ fullChartBottom,
3915
+ yMin,
3916
+ yRange,
3917
+ xStart,
3918
+ xSpan
3919
+ };
3920
+ paintCrosshairOverlay();
3921
+ };
3922
+ const paintCrosshairOverlay = () => {
3923
+ crosshairPriceActionRegion = null;
3924
+ const layout = overlayLayout;
3925
+ const crosshair = resolvedCrosshair;
3926
+ if (!layout || !crosshair.visible || !crosshairPoint) {
3927
+ return;
3928
+ }
3929
+ const { chartLeft, chartTop, chartRight, chartBottom, chartWidth, chartHeight, fullChartBottom, yMin, yRange, xStart, xSpan: xSpan2 } = layout;
3930
+ const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
3931
+ const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
3932
+ ctx.save();
3933
+ ctx.beginPath();
3934
+ ctx.rect(chartLeft, chartTop, chartWidth, Math.max(0, chartBottom - chartTop));
3935
+ ctx.clip();
3936
+ if (crosshair.mode === "dot") {
3937
+ ctx.fillStyle = crosshair.color;
3938
+ ctx.beginPath();
3939
+ ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
3940
+ ctx.fill();
3941
+ } else {
3942
+ ctx.strokeStyle = crosshair.color;
3943
+ ctx.lineWidth = Math.max(1, crosshair.width);
3944
+ applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
3945
+ if (crosshair.showHorizontal) {
3946
+ ctx.beginPath();
3947
+ ctx.moveTo(crisp(chartLeft), crisp(cy));
3948
+ ctx.lineTo(crisp(chartRight), crisp(cy));
3949
+ ctx.stroke();
3950
+ }
3951
+ }
3952
+ if (crosshair.sideHintLeft || crosshair.sideHintRight) {
3953
+ ctx.save();
3954
+ ctx.font = `600 11px ${mergedOptions.fontFamily}`;
3955
+ ctx.textBaseline = "middle";
3956
+ ctx.setLineDash([]);
3957
+ const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
3958
+ const hintGap = 8;
3959
+ if (crosshair.sideHintLeft) {
3960
+ ctx.fillStyle = crosshair.sideHintLeftColor;
3961
+ ctx.textAlign = "right";
3962
+ ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
3963
+ }
3964
+ if (crosshair.sideHintRight) {
3965
+ ctx.fillStyle = crosshair.sideHintRightColor;
3966
+ ctx.textAlign = "left";
3967
+ ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
3968
+ }
3969
+ ctx.restore();
3970
+ }
3971
+ ctx.restore();
3972
+ if (crosshair.showVertical) {
3973
+ ctx.save();
3974
+ ctx.strokeStyle = crosshair.color;
3975
+ ctx.lineWidth = Math.max(1, crosshair.width);
3976
+ applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
3977
+ ctx.beginPath();
3978
+ ctx.moveTo(crisp(cx), crisp(chartTop));
3979
+ ctx.lineTo(crisp(cx), crisp(fullChartBottom));
3980
+ ctx.stroke();
3981
+ ctx.restore();
3982
+ }
3983
+ const labelPaddingX = 8;
3984
+ const labelHeight = 20;
3985
+ const labelRadius = Math.max(0, crosshair.labelBorderRadius);
3986
+ const labelBackground = crosshair.labelBackgroundColor;
3987
+ const labelTextColor = crosshair.labelTextColor;
3988
+ const labelBorderColor = crosshair.labelBorderColor;
3989
+ const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
3990
+ const labelBorderStyle = crosshair.labelBorderStyle;
3991
+ const strokeCrosshairLabel = (x, y, widthValue, heightValue = labelHeight) => {
3992
+ if (labelBorderWidth <= 0) {
3993
+ return;
3994
+ }
3995
+ ctx.save();
3996
+ ctx.strokeStyle = labelBorderColor;
3997
+ ctx.lineWidth = labelBorderWidth;
3998
+ applyDashPattern(
3999
+ labelBorderStyle,
4000
+ dashPatterns.borderDotted,
4001
+ dashPatterns.borderDashed
4002
+ );
4003
+ strokeRoundedRect(Math.round(x), Math.round(y), widthValue, heightValue, labelRadius);
4004
+ ctx.restore();
4005
+ };
4006
+ if (crosshair.showPriceLabel) {
4007
+ const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
4008
+ const priceText = formatPrice(hoverPrice);
4009
+ const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
4010
+ const priceX = getRightAxisLabelX(chartRight);
4011
+ const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
4012
+ ctx.fillStyle = labelBackground;
4013
+ fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
4014
+ strokeCrosshairLabel(priceX, priceY, priceWidth);
4015
+ drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
4016
+ if (crosshair.showPriceActionButton) {
4017
+ const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
4018
+ const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
4019
+ const containerPaddingX = 5;
4020
+ const containerWidth = buttonSize + containerPaddingX * 2;
4021
+ const containerX = priceX - buttonGap - containerWidth;
4022
+ const containerY = priceY;
4023
+ const buttonX = containerX + containerPaddingX;
4024
+ const buttonY = priceY + (labelHeight - buttonSize) / 2;
4025
+ const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
4026
+ const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
4027
+ const buttonCenterX = buttonX + buttonSize / 2;
4028
+ const buttonCenterY = buttonY + buttonSize / 2;
4029
+ const containerRadius = labelRadius;
3639
4030
  ctx.fillStyle = labelBackground;
3640
- fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
3641
- strokeCrosshairLabel(priceX, priceY, priceWidth);
3642
- drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
3643
- if (crosshair.showPriceActionButton) {
3644
- const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
3645
- const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
3646
- const containerPaddingX = 5;
3647
- const containerWidth = buttonSize + containerPaddingX * 2;
3648
- const containerX = priceX - buttonGap - containerWidth;
3649
- const containerY = priceY;
3650
- const buttonX = containerX + containerPaddingX;
3651
- const buttonY = priceY + (labelHeight - buttonSize) / 2;
3652
- const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
3653
- const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
3654
- const buttonCenterX = buttonX + buttonSize / 2;
3655
- const buttonCenterY = buttonY + buttonSize / 2;
3656
- const containerRadius = labelRadius;
3657
- ctx.fillStyle = labelBackground;
3658
- fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
3659
- if (labelBorderWidth > 0) {
3660
- ctx.save();
3661
- ctx.strokeStyle = labelBorderColor;
3662
- ctx.lineWidth = labelBorderWidth;
3663
- ctx.setLineDash([]);
3664
- strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
3665
- ctx.restore();
3666
- }
3667
- if (buttonBorderWidth > 0) {
3668
- ctx.save();
3669
- ctx.strokeStyle = labelBorderColor;
3670
- ctx.lineWidth = buttonBorderWidth;
3671
- ctx.setLineDash([]);
3672
- if (crosshair.priceActionButtonRounded) {
3673
- ctx.beginPath();
3674
- ctx.arc(
3675
- buttonCenterX,
3676
- buttonCenterY,
3677
- Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
3678
- 0,
3679
- Math.PI * 2
3680
- );
3681
- ctx.stroke();
3682
- } else {
3683
- strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
3684
- }
3685
- ctx.restore();
3686
- }
3687
- if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
3688
- const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
3689
- const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
3690
- ctx.save();
3691
- ctx.strokeStyle = labelTextColor;
3692
- ctx.lineWidth = plusThickness;
3693
- ctx.lineCap = "round";
3694
- ctx.setLineDash([]);
4031
+ fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
4032
+ if (labelBorderWidth > 0) {
4033
+ ctx.save();
4034
+ ctx.strokeStyle = labelBorderColor;
4035
+ ctx.lineWidth = labelBorderWidth;
4036
+ ctx.setLineDash([]);
4037
+ strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
4038
+ ctx.restore();
4039
+ }
4040
+ if (buttonBorderWidth > 0) {
4041
+ ctx.save();
4042
+ ctx.strokeStyle = labelBorderColor;
4043
+ ctx.lineWidth = buttonBorderWidth;
4044
+ ctx.setLineDash([]);
4045
+ if (crosshair.priceActionButtonRounded) {
3695
4046
  ctx.beginPath();
3696
- ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
3697
- ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
3698
- ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
3699
- ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
3700
- ctx.stroke();
3701
- ctx.restore();
3702
- } else {
3703
- drawText(
3704
- crosshair.priceActionButtonText,
4047
+ ctx.arc(
3705
4048
  buttonCenterX,
3706
4049
  buttonCenterY,
3707
- "center",
3708
- "middle",
3709
- labelTextColor
4050
+ Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
4051
+ 0,
4052
+ Math.PI * 2
3710
4053
  );
4054
+ ctx.stroke();
4055
+ } else {
4056
+ strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
3711
4057
  }
3712
- crosshairPriceActionRegion = {
3713
- x: containerX,
3714
- y: containerY,
3715
- width: containerWidth,
3716
- height: labelHeight,
3717
- price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
3718
- };
4058
+ ctx.restore();
3719
4059
  }
3720
- }
3721
- if (crosshair.showTimeLabel) {
3722
- const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
3723
- const hoverIndex = Math.round(xStart + ratio * xSpan - 0.5);
3724
- const hoverTime = getTimeForIndex(hoverIndex);
3725
- if (hoverTime) {
3726
- const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
3727
- const timeWidth = Math.ceil(ctx.measureText(timeText).width) + labelPaddingX * 2;
3728
- const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
3729
- const timeY = fullChartBottom + 1;
3730
- const timeHeight = Math.max(labelHeight, Math.floor(height - timeY));
3731
- ctx.fillStyle = labelBackground;
3732
- fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, timeHeight, labelRadius);
3733
- strokeCrosshairLabel(timeX, timeY, timeWidth, timeHeight);
3734
- drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
4060
+ if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
4061
+ const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
4062
+ const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
4063
+ ctx.save();
4064
+ ctx.strokeStyle = labelTextColor;
4065
+ ctx.lineWidth = plusThickness;
4066
+ ctx.lineCap = "round";
4067
+ ctx.setLineDash([]);
4068
+ ctx.beginPath();
4069
+ ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
4070
+ ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
4071
+ ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
4072
+ ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
4073
+ ctx.stroke();
4074
+ ctx.restore();
4075
+ } else {
4076
+ drawText(
4077
+ crosshair.priceActionButtonText,
4078
+ buttonCenterX,
4079
+ buttonCenterY,
4080
+ "center",
4081
+ "middle",
4082
+ labelTextColor
4083
+ );
3735
4084
  }
4085
+ crosshairPriceActionRegion = {
4086
+ x: containerX,
4087
+ y: containerY,
4088
+ width: containerWidth,
4089
+ height: labelHeight,
4090
+ price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
4091
+ };
4092
+ }
4093
+ }
4094
+ if (crosshair.showTimeLabel) {
4095
+ const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
4096
+ const hoverIndex = Math.round(xStart + ratio * xSpan2 - 0.5);
4097
+ const hoverTime = getTimeForIndex(hoverIndex);
4098
+ if (hoverTime) {
4099
+ const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
4100
+ const timeWidth = Math.ceil(measureTextWidth(timeText)) + labelPaddingX * 2;
4101
+ const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
4102
+ const timeY = fullChartBottom + 1;
4103
+ const timeHeight = Math.max(labelHeight, Math.floor(height - timeY));
4104
+ ctx.fillStyle = labelBackground;
4105
+ fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, timeHeight, labelRadius);
4106
+ strokeCrosshairLabel(timeX, timeY, timeWidth, timeHeight);
4107
+ drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
3736
4108
  }
3737
4109
  }
3738
4110
  };
4111
+ const drawOverlayOnly = () => {
4112
+ if (!baseCtx || !overlayLayout || baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height || baseCanvas.width === 0) {
4113
+ draw({ updateAutoScale: false });
4114
+ return;
4115
+ }
4116
+ const pixelRatio = getPixelRatio();
4117
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
4118
+ ctx.drawImage(baseCanvas, 0, 0);
4119
+ ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
4120
+ paintCrosshairOverlay();
4121
+ };
3739
4122
  const zoomX = (factor, anchorX) => {
3740
4123
  if (!drawState || data.length === 0) {
3741
4124
  return;
@@ -3825,6 +4208,59 @@ function createChart(element, options = {}) {
3825
4208
  }
3826
4209
  scheduleDraw();
3827
4210
  };
4211
+ let kineticRafId = null;
4212
+ const panVelocitySamples = [];
4213
+ const cancelKineticPan = () => {
4214
+ if (kineticRafId !== null) {
4215
+ cancelAnimationFrame(kineticRafId);
4216
+ kineticRafId = null;
4217
+ }
4218
+ };
4219
+ const recordPanVelocitySample = (x) => {
4220
+ const now = performance.now();
4221
+ panVelocitySamples.push({ t: now, x });
4222
+ while (panVelocitySamples.length > 0 && now - panVelocitySamples[0].t > 120) {
4223
+ panVelocitySamples.shift();
4224
+ }
4225
+ };
4226
+ const startKineticPan = (initialVelocityPxPerMs) => {
4227
+ cancelKineticPan();
4228
+ let velocity = initialVelocityPxPerMs;
4229
+ let lastTime = performance.now();
4230
+ const stepFn = (now) => {
4231
+ kineticRafId = null;
4232
+ const dt = Math.min(64, Math.max(1, now - lastTime));
4233
+ lastTime = now;
4234
+ const centerBefore = xCenter;
4235
+ pan(velocity * dt, 0, true, false);
4236
+ velocity *= Math.exp(-dt / 325);
4237
+ if (Math.abs(velocity) < 0.02 || xCenter === centerBefore) {
4238
+ return;
4239
+ }
4240
+ kineticRafId = requestAnimationFrame(stepFn);
4241
+ };
4242
+ kineticRafId = requestAnimationFrame(stepFn);
4243
+ };
4244
+ const maybeStartKineticPan = (pointerType) => {
4245
+ const config = mergedOptions.kineticScroll ?? DEFAULT_OPTIONS.kineticScroll;
4246
+ const enabled = pointerType === "touch" || pointerType === "pen" ? config?.touch !== false : config?.mouse === true;
4247
+ if (!enabled || panVelocitySamples.length < 2) {
4248
+ panVelocitySamples.length = 0;
4249
+ return;
4250
+ }
4251
+ const first = panVelocitySamples[0];
4252
+ const last = panVelocitySamples[panVelocitySamples.length - 1];
4253
+ panVelocitySamples.length = 0;
4254
+ const elapsed = last.t - first.t;
4255
+ if (elapsed <= 0 || performance.now() - last.t > 100) {
4256
+ return;
4257
+ }
4258
+ const velocity = (last.x - first.x) / elapsed;
4259
+ if (Math.abs(velocity) < 0.1) {
4260
+ return;
4261
+ }
4262
+ startKineticPan(clamp(velocity, -4, 4));
4263
+ };
3828
4264
  const resetYViewport = () => {
3829
4265
  yMinOverride = null;
3830
4266
  yMaxOverride = null;
@@ -3884,6 +4320,7 @@ function createChart(element, options = {}) {
3884
4320
  scheduleDraw();
3885
4321
  };
3886
4322
  const resetViewport = () => {
4323
+ cancelKineticPan();
3887
4324
  fitXViewport();
3888
4325
  resetYViewport();
3889
4326
  updateFollowLatest(true);
@@ -3893,6 +4330,7 @@ function createChart(element, options = {}) {
3893
4330
  const isFollowingLatest = () => followLatest;
3894
4331
  const setFollowingLatest = (follow) => {
3895
4332
  if (follow && data.length > 0) {
4333
+ cancelKineticPan();
3896
4334
  xCenter = data.length - xSpan / 2 + rightEdgePaddingBars;
3897
4335
  clampXViewport();
3898
4336
  updateFollowLatest(true);
@@ -4262,7 +4700,7 @@ function createChart(element, options = {}) {
4262
4700
  const prevFont = ctx.font;
4263
4701
  ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
4264
4702
  const lines = (drawing.label ?? "").split("\n");
4265
- const textW = Math.max(1, ...lines.map((line) => ctx.measureText(line).width));
4703
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
4266
4704
  ctx.font = prevFont;
4267
4705
  const isNote = drawing.type === "note";
4268
4706
  const padX = isNote ? 8 : 2;
@@ -4531,7 +4969,9 @@ function createChart(element, options = {}) {
4531
4969
  points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
4532
4970
  color: defaults.color ?? "#2962ff",
4533
4971
  style: defaults.style ?? "solid",
4534
- width: defaults.width ?? 1
4972
+ width: defaults.width ?? 1,
4973
+ // pointValue enables the dollar-P&L line in the measure label.
4974
+ ...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
4535
4975
  })
4536
4976
  );
4537
4977
  emitDrawingsChange();
@@ -4733,16 +5173,24 @@ function createChart(element, options = {}) {
4733
5173
  emitViewportChange();
4734
5174
  scheduleDraw();
4735
5175
  };
5176
+ const capturePointer = (pointerId) => {
5177
+ try {
5178
+ canvas.setPointerCapture(pointerId);
5179
+ } catch {
5180
+ }
5181
+ };
4736
5182
  const onPointerDown = (event) => {
4737
5183
  if (event.pointerType === "touch" || event.pointerType === "pen") {
4738
5184
  event.preventDefault();
4739
5185
  }
5186
+ cancelKineticPan();
5187
+ panVelocitySamples.length = 0;
4740
5188
  magnetModifierActive = event.metaKey || event.ctrlKey;
4741
5189
  shiftKeyActive = event.shiftKey;
4742
5190
  const point = getCanvasPoint(event);
4743
5191
  if (event.pointerType === "touch") {
4744
5192
  touchPointers.set(event.pointerId, point);
4745
- canvas.setPointerCapture(event.pointerId);
5193
+ capturePointer(event.pointerId);
4746
5194
  const touchPair = getTouchPair();
4747
5195
  if (touchPair) {
4748
5196
  beginPinchZoom(touchPair[0], touchPair[1]);
@@ -4772,7 +5220,7 @@ function createChart(element, options = {}) {
4772
5220
  lastPrice: startPrice,
4773
5221
  moved: false
4774
5222
  };
4775
- canvas.setPointerCapture(event.pointerId);
5223
+ capturePointer(event.pointerId);
4776
5224
  canvas.style.cursor = "ns-resize";
4777
5225
  setCrosshairPoint(null);
4778
5226
  return;
@@ -4793,7 +5241,7 @@ function createChart(element, options = {}) {
4793
5241
  startPrice: orderDragRegion.price,
4794
5242
  lastPrice: orderDragRegion.price
4795
5243
  };
4796
- canvas.setPointerCapture(event.pointerId);
5244
+ capturePointer(event.pointerId);
4797
5245
  canvas.style.cursor = "ns-resize";
4798
5246
  setCrosshairPoint(null);
4799
5247
  return;
@@ -4825,7 +5273,7 @@ function createChart(element, options = {}) {
4825
5273
  startPoints: drawingHit.drawing.points.map((drawingPoint) => ({ ...drawingPoint }))
4826
5274
  };
4827
5275
  activePointerId = event.pointerId;
4828
- canvas.setPointerCapture(event.pointerId);
5276
+ capturePointer(event.pointerId);
4829
5277
  }
4830
5278
  }
4831
5279
  setCrosshairPoint(null);
@@ -4853,7 +5301,7 @@ function createChart(element, options = {}) {
4853
5301
  };
4854
5302
  lastPointerX = point.x;
4855
5303
  lastPointerY = point.y;
4856
- canvas.setPointerCapture(event.pointerId);
5304
+ capturePointer(event.pointerId);
4857
5305
  if (crosshairDrag) {
4858
5306
  canvas.style.cursor = "crosshair";
4859
5307
  setCrosshairPoint(point);
@@ -5039,6 +5487,7 @@ function createChart(element, options = {}) {
5039
5487
  const deltaY = point.y - lastPointerY;
5040
5488
  if (dragMode === "plot") {
5041
5489
  canvas.style.cursor = "grabbing";
5490
+ recordPanVelocitySample(point.x);
5042
5491
  pan(deltaX, deltaY, true, true);
5043
5492
  setCrosshairPoint(null);
5044
5493
  } else if (dragMode === "x-axis") {
@@ -5112,10 +5561,16 @@ function createChart(element, options = {}) {
5112
5561
  drawingDragState = null;
5113
5562
  emitDrawingsChange();
5114
5563
  }
5564
+ const endedPlotDrag = isDragging && dragMode === "plot";
5115
5565
  isDragging = false;
5116
5566
  dragMode = null;
5117
5567
  activePointerId = null;
5118
5568
  canvas.style.cursor = "default";
5569
+ if (endedPlotDrag) {
5570
+ maybeStartKineticPan(event?.pointerType);
5571
+ } else {
5572
+ panVelocitySamples.length = 0;
5573
+ }
5119
5574
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
5120
5575
  if (pointerDownInfo.crosshairDrag) {
5121
5576
  const point = getCanvasPoint(event);
@@ -5153,6 +5608,7 @@ function createChart(element, options = {}) {
5153
5608
  if (!drawState) {
5154
5609
  return;
5155
5610
  }
5611
+ cancelKineticPan();
5156
5612
  const point = getCanvasPoint(event);
5157
5613
  const region = getHitRegion(point.x, point.y);
5158
5614
  if (region === "outside") {
@@ -5252,6 +5708,7 @@ function createChart(element, options = {}) {
5252
5708
  width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
5253
5709
  height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
5254
5710
  mergedOptions = { ...mergedOptions, width, height };
5711
+ refreshResolvedOptions();
5255
5712
  resetRightMarginCache();
5256
5713
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
5257
5714
  doubleClickAction = mergedOptions.doubleClickAction;
@@ -5596,6 +6053,7 @@ function createChart(element, options = {}) {
5596
6053
  drawingHoverHandler = handler;
5597
6054
  };
5598
6055
  const destroy = () => {
6056
+ cancelKineticPan();
5599
6057
  if (smoothingRafId !== null) {
5600
6058
  cancelAnimationFrame(smoothingRafId);
5601
6059
  smoothingRafId = null;