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.
@@ -189,8 +189,12 @@ var DEFAULT_OPTIONS = {
189
189
  tickSize: 0,
190
190
  candleColorMode: "openClose",
191
191
  candleColorEpsilon: -1,
192
- autoScaleSmoothing: 0.16,
192
+ // 0 = deterministic scale (lightweight-charts behavior): the y-range is a
193
+ // pure function of the visible data, so it can never drift or "breathe".
194
+ // Values > 0 re-enable eased contraction for hosts that prefer it.
195
+ autoScaleSmoothing: 0,
193
196
  autoScaleIgnoreLatestCandle: true,
197
+ kineticScroll: { touch: true, mouse: false },
194
198
  pinOutOfRangeLines: false,
195
199
  doubleClickEnabled: true,
196
200
  doubleClickAction: "reset",
@@ -409,6 +413,60 @@ var computeStdDevSeries = (data, length, source) => {
409
413
  }
410
414
  return result;
411
415
  };
416
+ var VWAP_SESSION_ANCHOR_MS = 22 * 36e5;
417
+ var vwapSessionDayOf = (ms) => Math.floor((ms - VWAP_SESSION_ANCHOR_MS) / 864e5);
418
+ var computeVwapSeries = (data, band) => {
419
+ const result = new Array(data.length).fill(null);
420
+ let session = Number.NaN;
421
+ let cumPv = 0;
422
+ let cumPv2 = 0;
423
+ let cumV = 0;
424
+ for (let i = 0; i < data.length; i += 1) {
425
+ const point = data[i];
426
+ if (!point) continue;
427
+ const day = vwapSessionDayOf(point.time.getTime());
428
+ if (day !== session) {
429
+ session = day;
430
+ cumPv = 0;
431
+ cumPv2 = 0;
432
+ cumV = 0;
433
+ }
434
+ const typical = (point.h + point.l + point.c) / 3;
435
+ const volume = Math.max(0, point.v ?? 0);
436
+ cumPv += typical * volume;
437
+ cumPv2 += typical * typical * volume;
438
+ cumV += volume;
439
+ if (cumV <= 0) continue;
440
+ const vwap = cumPv / cumV;
441
+ if (band === "vwap") {
442
+ result[i] = vwap;
443
+ } else {
444
+ const variance = Math.max(0, cumPv2 / cumV - vwap * vwap);
445
+ result[i] = Math.sqrt(variance);
446
+ }
447
+ }
448
+ for (let i = 1; i < data.length; i += 1) {
449
+ const point = data[i];
450
+ const prev = data[i - 1];
451
+ const next = data[i + 1];
452
+ if (!point || !prev || !next) continue;
453
+ const day = vwapSessionDayOf(point.time.getTime());
454
+ if (day !== vwapSessionDayOf(prev.time.getTime()) && day === vwapSessionDayOf(next.time.getTime())) {
455
+ result[i] = null;
456
+ }
457
+ }
458
+ return result;
459
+ };
460
+ var computeBollingerSeries = (data, length, multiplier, source, band) => {
461
+ const basis = computeSmaSeries(data, length, source);
462
+ if (band === "basis") return basis;
463
+ const dev = computeStdDevSeries(data, length, source);
464
+ return basis.map((value, idx) => {
465
+ const sigma = dev[idx];
466
+ if (value == null || sigma == null) return null;
467
+ return band === "upper" ? value + multiplier * sigma : value - multiplier * sigma;
468
+ });
469
+ };
412
470
  var computeRsiSeries = (data, length) => {
413
471
  const result = new Array(data.length).fill(null);
414
472
  if (data.length < 2) return result;
@@ -535,6 +593,59 @@ var drawOverlaySeries = (ctx, renderContext, values, color, width) => {
535
593
  }
536
594
  ctx.restore();
537
595
  };
596
+ var rangeOfSeries = (seriesList, startIndex, endIndex, skipIndex) => {
597
+ let min = Number.POSITIVE_INFINITY;
598
+ let max = Number.NEGATIVE_INFINITY;
599
+ for (const series of seriesList) {
600
+ for (let idx = startIndex; idx <= endIndex; idx += 1) {
601
+ if (idx === skipIndex) continue;
602
+ const value = series[idx];
603
+ if (value == null || !Number.isFinite(value)) continue;
604
+ if (value < min) min = value;
605
+ if (value > max) max = value;
606
+ }
607
+ }
608
+ return min <= max ? { min, max } : null;
609
+ };
610
+ var maAutoscaleRange = (cacheKeyPrefix, compute, defaultLength) => (data, startIndex, endIndex, inputs, skipIndex) => {
611
+ const length = clampIndicatorLength(inputs.length, defaultLength);
612
+ const source = inputs.source ?? "close";
613
+ const series = withCachedSeries(`${cacheKeyPrefix}|${length}|${source}`, data, () => compute(data, length, source));
614
+ return rangeOfSeries([series], startIndex, endIndex, skipIndex);
615
+ };
616
+ var fillBetweenSeries = (ctx, renderContext, upper, lower, color, opacity) => {
617
+ if (!renderContext.yFromPrice || opacity <= 0) return;
618
+ const yFromPrice = renderContext.yFromPrice;
619
+ ctx.save();
620
+ ctx.globalAlpha = Math.min(1, Math.max(0, opacity));
621
+ ctx.fillStyle = color;
622
+ let runStart = -1;
623
+ const flushRun = (endIndexExclusive) => {
624
+ if (runStart < 0) return;
625
+ ctx.beginPath();
626
+ for (let i = runStart; i < endIndexExclusive; i += 1) {
627
+ const x = renderContext.xFromIndex(i);
628
+ const y = yFromPrice(upper[i]);
629
+ if (i === runStart) ctx.moveTo(x, y);
630
+ else ctx.lineTo(x, y);
631
+ }
632
+ for (let i = endIndexExclusive - 1; i >= runStart; i -= 1) {
633
+ ctx.lineTo(renderContext.xFromIndex(i), yFromPrice(lower[i]));
634
+ }
635
+ ctx.closePath();
636
+ ctx.fill();
637
+ runStart = -1;
638
+ };
639
+ for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
640
+ const up = upper[index];
641
+ const lo = lower[index];
642
+ const valid = Number.isFinite(up ?? Number.NaN) && Number.isFinite(lo ?? Number.NaN);
643
+ if (valid && runStart < 0) runStart = index;
644
+ if (!valid) flushRun(index);
645
+ }
646
+ flushRun(renderContext.endIndex + 1);
647
+ ctx.restore();
648
+ };
538
649
  var drawSeparateSeries = (ctx, renderContext, values, color, width, minOverride, maxOverride, guideLines, options = {}) => {
539
650
  const visible = [];
540
651
  for (let index = renderContext.startIndex; index <= renderContext.endIndex; index += 1) {
@@ -719,7 +830,8 @@ var BUILTIN_SMA_INDICATOR = {
719
830
  () => computeSmaSeries(renderContext.data, length, inputs.source ?? "close")
720
831
  );
721
832
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#60a5fa", Number(inputs.width) || 2);
722
- }
833
+ },
834
+ getAutoscaleRange: maAutoscaleRange("sma", computeSmaSeries, 20)
723
835
  };
724
836
  var BUILTIN_EMA_INDICATOR = {
725
837
  id: "ema",
@@ -734,7 +846,8 @@ var BUILTIN_EMA_INDICATOR = {
734
846
  () => computeEmaSeries(renderContext.data, length, inputs.source ?? "close")
735
847
  );
736
848
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
737
- }
849
+ },
850
+ getAutoscaleRange: maAutoscaleRange("ema", computeEmaSeries, 20)
738
851
  };
739
852
  var BUILTIN_WMA_INDICATOR = {
740
853
  id: "wma",
@@ -749,7 +862,8 @@ var BUILTIN_WMA_INDICATOR = {
749
862
  () => computeWmaSeries(renderContext.data, length, inputs.source ?? "close")
750
863
  );
751
864
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#a78bfa", Number(inputs.width) || 2);
752
- }
865
+ },
866
+ getAutoscaleRange: maAutoscaleRange("wma", computeWmaSeries, 20)
753
867
  };
754
868
  var BUILTIN_VWMA_INDICATOR = {
755
869
  id: "vwma",
@@ -764,7 +878,8 @@ var BUILTIN_VWMA_INDICATOR = {
764
878
  () => computeVwmaSeries(renderContext.data, length, inputs.source ?? "close")
765
879
  );
766
880
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#ef4444", Number(inputs.width) || 2);
767
- }
881
+ },
882
+ getAutoscaleRange: maAutoscaleRange("vwma", computeVwmaSeries, 20)
768
883
  };
769
884
  var BUILTIN_RMA_INDICATOR = {
770
885
  id: "rma",
@@ -779,7 +894,8 @@ var BUILTIN_RMA_INDICATOR = {
779
894
  () => computeRmaSeries(renderContext.data, length, inputs.source ?? "close")
780
895
  );
781
896
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#22c55e", Number(inputs.width) || 2);
782
- }
897
+ },
898
+ getAutoscaleRange: maAutoscaleRange("rma", computeRmaSeries, 14)
783
899
  };
784
900
  var BUILTIN_HMA_INDICATOR = {
785
901
  id: "hma",
@@ -794,6 +910,127 @@ var BUILTIN_HMA_INDICATOR = {
794
910
  () => computeHmaSeries(renderContext.data, length, inputs.source ?? "close")
795
911
  );
796
912
  drawOverlaySeries(ctx, renderContext, values, inputs.color ?? "#14b8a6", Number(inputs.width) || 2);
913
+ },
914
+ getAutoscaleRange: maAutoscaleRange("hma", computeHmaSeries, 21)
915
+ };
916
+ var BUILTIN_VWAP_INDICATOR = {
917
+ id: "vwap",
918
+ name: "VWAP",
919
+ pane: "overlay",
920
+ defaultInputs: {
921
+ color: "#f59e0b",
922
+ width: 2,
923
+ showBands: false,
924
+ bandMultiplier: 1,
925
+ bandColor: "#94a3b8",
926
+ bandFillOpacity: 0.06
927
+ },
928
+ draw: (ctx, renderContext, inputs) => {
929
+ const vwap = withCachedSeries(
930
+ "vwap|line",
931
+ renderContext.data,
932
+ () => computeVwapSeries(renderContext.data, "vwap")
933
+ );
934
+ if (inputs.showBands) {
935
+ const sigma = withCachedSeries(
936
+ "vwap|sigma",
937
+ renderContext.data,
938
+ () => computeVwapSeries(renderContext.data, "sigma")
939
+ );
940
+ const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
941
+ const upper = vwap.map((value, idx) => {
942
+ const s = sigma[idx];
943
+ return value == null || s == null ? null : value + multiplier * s;
944
+ });
945
+ const lower = vwap.map((value, idx) => {
946
+ const s = sigma[idx];
947
+ return value == null || s == null ? null : value - multiplier * s;
948
+ });
949
+ const bandColor = inputs.bandColor ?? "#94a3b8";
950
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.bandFillOpacity) || 0.06);
951
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, 1);
952
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, 1);
953
+ }
954
+ drawOverlaySeries(ctx, renderContext, vwap, inputs.color ?? "#f59e0b", Number(inputs.width) || 2);
955
+ },
956
+ getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
957
+ const vwap = withCachedSeries("vwap|line", data, () => computeVwapSeries(data, "vwap"));
958
+ if (!inputs.showBands) {
959
+ return rangeOfSeries([vwap], startIndex, endIndex, skipIndex);
960
+ }
961
+ const sigma = withCachedSeries("vwap|sigma", data, () => computeVwapSeries(data, "sigma"));
962
+ const multiplier = Math.max(0.1, Number(inputs.bandMultiplier) || 1);
963
+ let min = Number.POSITIVE_INFINITY;
964
+ let max = Number.NEGATIVE_INFINITY;
965
+ for (let idx = startIndex; idx <= endIndex; idx += 1) {
966
+ if (idx === skipIndex) continue;
967
+ const value = vwap[idx];
968
+ const s = sigma[idx];
969
+ if (value == null || s == null) continue;
970
+ const lo = value - multiplier * s;
971
+ const hi = value + multiplier * s;
972
+ if (lo < min) min = lo;
973
+ if (hi > max) max = hi;
974
+ }
975
+ return min <= max ? { min, max } : null;
976
+ }
977
+ };
978
+ var BUILTIN_BOLLINGER_INDICATOR = {
979
+ id: "bollinger",
980
+ name: "BB",
981
+ pane: "overlay",
982
+ // Basis is pink so it stays distinguishable from the (orange) VWAP line
983
+ // when both overlays are active.
984
+ defaultInputs: {
985
+ length: 20,
986
+ multiplier: 2,
987
+ source: "close",
988
+ basisColor: "#ec4899",
989
+ bandColor: "#3b82f6",
990
+ width: 1.5,
991
+ fillOpacity: 0.05
992
+ },
993
+ draw: (ctx, renderContext, inputs) => {
994
+ const length = clampIndicatorLength(inputs.length, 20);
995
+ const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
996
+ const source = inputs.source ?? "close";
997
+ const basis = withCachedSeries(
998
+ `bb|basis|${length}|${source}`,
999
+ renderContext.data,
1000
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "basis")
1001
+ );
1002
+ const upper = withCachedSeries(
1003
+ `bb|upper|${length}|${multiplier}|${source}`,
1004
+ renderContext.data,
1005
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "upper")
1006
+ );
1007
+ const lower = withCachedSeries(
1008
+ `bb|lower|${length}|${multiplier}|${source}`,
1009
+ renderContext.data,
1010
+ () => computeBollingerSeries(renderContext.data, length, multiplier, source, "lower")
1011
+ );
1012
+ const bandColor = inputs.bandColor ?? "#3b82f6";
1013
+ const width = Number(inputs.width) || 1.5;
1014
+ fillBetweenSeries(ctx, renderContext, upper, lower, bandColor, Number(inputs.fillOpacity) || 0.05);
1015
+ drawOverlaySeries(ctx, renderContext, upper, bandColor, width);
1016
+ drawOverlaySeries(ctx, renderContext, lower, bandColor, width);
1017
+ drawOverlaySeries(ctx, renderContext, basis, inputs.basisColor ?? "#ec4899", width);
1018
+ },
1019
+ getAutoscaleRange: (data, startIndex, endIndex, inputs, skipIndex) => {
1020
+ const length = clampIndicatorLength(inputs.length, 20);
1021
+ const multiplier = Math.max(0.1, Number(inputs.multiplier) || 2);
1022
+ const source = inputs.source ?? "close";
1023
+ const upper = withCachedSeries(
1024
+ `bb|upper|${length}|${multiplier}|${source}`,
1025
+ data,
1026
+ () => computeBollingerSeries(data, length, multiplier, source, "upper")
1027
+ );
1028
+ const lower = withCachedSeries(
1029
+ `bb|lower|${length}|${multiplier}|${source}`,
1030
+ data,
1031
+ () => computeBollingerSeries(data, length, multiplier, source, "lower")
1032
+ );
1033
+ return rangeOfSeries([upper, lower], startIndex, endIndex, skipIndex);
797
1034
  }
798
1035
  };
799
1036
  var BUILTIN_STDDEV_INDICATOR = {
@@ -879,11 +1116,27 @@ var BUILTIN_INDICATORS = [
879
1116
  BUILTIN_VWMA_INDICATOR,
880
1117
  BUILTIN_RMA_INDICATOR,
881
1118
  BUILTIN_HMA_INDICATOR,
1119
+ BUILTIN_VWAP_INDICATOR,
1120
+ BUILTIN_BOLLINGER_INDICATOR,
882
1121
  BUILTIN_STDDEV_INDICATOR,
883
1122
  BUILTIN_ATR_INDICATOR
884
1123
  ];
885
1124
  function createChart(element, options = {}) {
886
1125
  let mergedOptions = mergeChartOptions(DEFAULT_OPTIONS, options);
1126
+ let resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1127
+ let resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
1128
+ let resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
1129
+ let resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
1130
+ let resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1131
+ let resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
1132
+ const refreshResolvedOptions = () => {
1133
+ resolvedAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1134
+ resolvedXAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
1135
+ resolvedYAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
1136
+ resolvedLabels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
1137
+ resolvedGrid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
1138
+ resolvedCrosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
1139
+ };
887
1140
  let width = mergedOptions.width;
888
1141
  let height = mergedOptions.height;
889
1142
  let data = [];
@@ -920,8 +1173,11 @@ function createChart(element, options = {}) {
920
1173
  pane: indicator.pane ?? plugin?.pane ?? "overlay",
921
1174
  ...indicator.paneHeightRatio === void 0 ? {} : { paneHeightRatio: indicator.paneHeightRatio },
922
1175
  zIndex: Math.round(Number(indicator.zIndex) || 0),
923
- excludeFromAutoscale: indicator.excludeFromAutoscale ?? true,
924
- overlayScaleWeight: Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0.25)),
1176
+ // Overlay series participate in autoscale by default (lightweight-charts
1177
+ // semantics): bands and MAs expand the price range instead of clipping
1178
+ // at the chart edge. Hosts can still opt out per indicator.
1179
+ excludeFromAutoscale: indicator.excludeFromAutoscale ?? false,
1180
+ overlayScaleWeight: indicator.overlayScaleWeight === void 0 ? 1 : Math.min(1, Math.max(0, Number(indicator.overlayScaleWeight) || 0)),
925
1181
  inputs: {
926
1182
  ...defaults,
927
1183
  ...indicator.inputs ?? {}
@@ -1123,6 +1379,9 @@ function createChart(element, options = {}) {
1123
1379
  canvas.setAttribute("draggable", "false");
1124
1380
  element.innerHTML = "";
1125
1381
  element.appendChild(canvas);
1382
+ const baseCanvas = document.createElement("canvas");
1383
+ const baseCtx = baseCanvas.getContext("2d");
1384
+ let overlayLayout = null;
1126
1385
  const margin = { top: 16, right: 72, bottom: 26, left: 12 };
1127
1386
  let cachedRightMargin = margin.right;
1128
1387
  const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
@@ -1162,6 +1421,20 @@ function createChart(element, options = {}) {
1162
1421
  const clamp = (value, min, max) => {
1163
1422
  return Math.min(max, Math.max(min, value));
1164
1423
  };
1424
+ const textWidthCache = /* @__PURE__ */ new Map();
1425
+ const measureTextWidth = (text) => {
1426
+ const key = `${ctx.font}\0${text}`;
1427
+ const cached = textWidthCache.get(key);
1428
+ if (cached !== void 0) {
1429
+ return cached;
1430
+ }
1431
+ const width2 = ctx.measureText(text).width;
1432
+ if (textWidthCache.size >= 4096) {
1433
+ textWidthCache.clear();
1434
+ }
1435
+ textWidthCache.set(key, width2);
1436
+ return width2;
1437
+ };
1165
1438
  const resetLabelSlots = (enabled) => {
1166
1439
  noOverlappingLineLabels = enabled;
1167
1440
  rightAxisLabelSlots = [];
@@ -1364,7 +1637,7 @@ function createChart(element, options = {}) {
1364
1637
  return `${integerPart}${decimalPart}`;
1365
1638
  };
1366
1639
  const getMeasuredLabelWidth = (text, paddingX) => {
1367
- return Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1640
+ return Math.ceil(measureTextWidth(text)) + paddingX * 2;
1368
1641
  };
1369
1642
  const getStabilizedNumericLabelWidth = (text, paddingX) => {
1370
1643
  const measured = getMeasuredLabelWidth(text, paddingX);
@@ -1605,11 +1878,11 @@ function createChart(element, options = {}) {
1605
1878
  return;
1606
1879
  }
1607
1880
  const labelText = mergedLine.label ?? formatPrice(mergedLine.price);
1608
- const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1881
+ const axis = resolvedAxis;
1609
1882
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1610
1883
  const labelPaddingX = 8;
1611
1884
  const labelHeight = 20;
1612
- const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(ctx.measureText(labelText).width) + labelPaddingX * 2;
1885
+ const measuredLabelWidth = mergedLine.label === void 0 ? getPriceLabelWidth(labelText, labelPaddingX) : Math.ceil(measureTextWidth(labelText)) + labelPaddingX * 2;
1613
1886
  const labelWidth = getRightAxisLabelWidth(chartRight, measuredLabelWidth);
1614
1887
  const labelX = getRightAxisLabelX(chartRight);
1615
1888
  const labelY = placeRightAxisLabel(lineY - labelHeight / 2, labelHeight, chartTop, chartBottom - labelHeight);
@@ -1704,7 +1977,7 @@ function createChart(element, options = {}) {
1704
1977
  const closeAction = mergedLine.type === "market" ? "close" : "cancel";
1705
1978
  const showCloseButton = mergedLine.showCloseButton;
1706
1979
  const actionButtonText = mergedLine.actionButtonText;
1707
- const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1980
+ const axis = resolvedAxis;
1708
1981
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1709
1982
  const legacyButton = actionButtonText ? [
1710
1983
  {
@@ -1726,7 +1999,7 @@ function createChart(element, options = {}) {
1726
1999
  const actionButtonMetrics = actionButtons.map((button) => {
1727
2000
  const paddingX = Math.max(2, button.paddingX ?? mergedLine.actionButtonPaddingX);
1728
2001
  const minWidth = Math.max(16, button.minWidth ?? mergedLine.actionButtonMinWidth);
1729
- const width2 = Math.max(minWidth, Math.ceil(ctx.measureText(button.text).width) + paddingX * 2);
2002
+ const width2 = Math.max(minWidth, Math.ceil(measureTextWidth(button.text)) + paddingX * 2);
1730
2003
  return { button, width: width2 };
1731
2004
  });
1732
2005
  const actionButtonInnerGap = actionButtonMetrics.length > 1 ? Math.max(0, mergedLine.actionButtonsInnerGap) : 0;
@@ -1734,8 +2007,8 @@ function createChart(element, options = {}) {
1734
2007
  const actionButtonsGap = actionButtonMetrics.length > 0 ? Math.max(0, mergedLine.actionButtonsGroupGap) : 0;
1735
2008
  const segmentPaddingX = 8;
1736
2009
  const labelHeight = 22;
1737
- const qtyWidth = qtyText ? Math.ceil(ctx.measureText(qtyText).width) + segmentPaddingX * 2 : 0;
1738
- const centerMeasuredWidth = Math.ceil(ctx.measureText(centerText).width) + segmentPaddingX * 2;
2010
+ const qtyWidth = qtyText ? Math.ceil(measureTextWidth(qtyText)) + segmentPaddingX * 2 : 0;
2011
+ const centerMeasuredWidth = Math.ceil(measureTextWidth(centerText)) + segmentPaddingX * 2;
1739
2012
  const centerWidth = mergedLine.id === void 0 ? centerMeasuredWidth : Math.max(centerMeasuredWidth, orderWidgetWidthById.get(mergedLine.id) ?? 0);
1740
2013
  if (mergedLine.id) {
1741
2014
  orderWidgetWidthById.set(mergedLine.id, centerWidth);
@@ -1743,7 +2016,7 @@ function createChart(element, options = {}) {
1743
2016
  const closeWidth = showCloseButton ? 24 : 0;
1744
2017
  const mainWidgetWidth = qtyWidth + centerWidth + closeWidth;
1745
2018
  const totalWidth = mainWidgetWidth + actionButtonsGap + actionButtonsTotalWidth;
1746
- const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
2019
+ const crosshair = resolvedCrosshair;
1747
2020
  const crosshairActionInset = crosshair.visible && crosshair.showPriceLabel && crosshair.showPriceActionButton ? Math.max(
1748
2021
  0,
1749
2022
  Math.round(crosshair.priceActionButtonGap) + (clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4)) + 10) - 4
@@ -1924,20 +2197,29 @@ function createChart(element, options = {}) {
1924
2197
  }
1925
2198
  }
1926
2199
  crosshairPoint = point;
1927
- scheduleDraw();
2200
+ scheduleDraw({ overlayOnly: true });
1928
2201
  };
1929
2202
  let drawRafId = null;
1930
2203
  let pendingDrawUpdateAutoScale = false;
2204
+ let pendingDrawOverlayOnly = true;
1931
2205
  const scheduleDraw = (options2 = {}) => {
1932
- pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || (options2.updateAutoScale ?? true);
2206
+ const overlayOnly = options2.overlayOnly ?? false;
2207
+ pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
2208
+ pendingDrawUpdateAutoScale = pendingDrawUpdateAutoScale || !overlayOnly && (options2.updateAutoScale ?? true);
1933
2209
  if (drawRafId !== null) {
1934
2210
  return;
1935
2211
  }
1936
2212
  drawRafId = requestAnimationFrame(() => {
1937
2213
  drawRafId = null;
1938
2214
  const updateAutoScale = pendingDrawUpdateAutoScale;
2215
+ const flushOverlayOnly = pendingDrawOverlayOnly;
1939
2216
  pendingDrawUpdateAutoScale = false;
1940
- draw({ updateAutoScale });
2217
+ pendingDrawOverlayOnly = true;
2218
+ if (flushOverlayOnly) {
2219
+ drawOverlayOnly();
2220
+ } else {
2221
+ draw({ updateAutoScale });
2222
+ }
1941
2223
  });
1942
2224
  };
1943
2225
  const draw = (options2 = {}) => {
@@ -1951,15 +2233,15 @@ function createChart(element, options = {}) {
1951
2233
  canvas.width = Math.floor(width * pixelRatio);
1952
2234
  canvas.height = Math.floor(height * pixelRatio);
1953
2235
  ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
1954
- const axis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.axis ?? {} };
1955
- const xAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.xAxis ?? {} };
1956
- const yAxis = { ...DEFAULT_AXIS_OPTIONS, ...mergedOptions.yAxis ?? {} };
2236
+ const axis = resolvedAxis;
2237
+ const xAxis = resolvedXAxis;
2238
+ const yAxis = resolvedYAxis;
1957
2239
  const xAxisFontSize = Math.max(8, xAxis.fontSize);
1958
2240
  const yAxisFontSize = Math.max(8, yAxis.fontSize);
1959
2241
  ctx.font = `${Math.max(8, axis.fontSize)}px ${mergedOptions.fontFamily}`;
1960
2242
  ctx.fillStyle = mergedOptions.backgroundColor;
1961
2243
  ctx.fillRect(0, 0, width, height);
1962
- const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
2244
+ const labels = resolvedLabels;
1963
2245
  const estimateRightMargin = () => {
1964
2246
  const paddingX = Math.max(4, labels.labelPaddingX);
1965
2247
  let required = margin.right - 1;
@@ -2072,6 +2354,7 @@ function createChart(element, options = {}) {
2072
2354
  yMax: 0
2073
2355
  };
2074
2356
  drawText("Load OHLC data to render chart", chartLeft + 10, chartTop + 20, "left", "middle", "#64748b");
2357
+ overlayLayout = null;
2075
2358
  return;
2076
2359
  }
2077
2360
  clampXViewport();
@@ -2079,62 +2362,39 @@ function createChart(element, options = {}) {
2079
2362
  const xEnd = xStart + xSpan;
2080
2363
  const startIndex = Math.max(0, Math.floor(xStart));
2081
2364
  const endIndex = Math.min(data.length - 1, Math.ceil(xEnd) - 1);
2082
- const visibleData = data.slice(startIndex, endIndex + 1);
2083
- let priceSource = visibleData.length > 0 ? visibleData : data;
2084
- if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1) {
2085
- const latestIndex = data.length - 1;
2086
- const filtered = priceSource.filter((_, offset) => startIndex + offset !== latestIndex);
2087
- if (filtered.length > 0) {
2088
- priceSource = filtered;
2089
- } else {
2090
- const fallbackWindow = 120;
2091
- const fallbackStart = Math.max(0, latestIndex - fallbackWindow);
2092
- const fallback = data.slice(fallbackStart, latestIndex);
2093
- if (fallback.length > 0) {
2094
- priceSource = fallback;
2095
- }
2365
+ const skipLatestIndex = mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 ? data.length - 1 : -1;
2366
+ const scanCandleRange = (from, to) => {
2367
+ let min = Number.POSITIVE_INFINITY;
2368
+ let max = Number.NEGATIVE_INFINITY;
2369
+ for (let idx = from; idx <= to; idx += 1) {
2370
+ if (idx === skipLatestIndex) continue;
2371
+ const point = data[idx];
2372
+ if (!point) continue;
2373
+ if (point.l < min) min = point.l;
2374
+ if (point.h > max) max = point.h;
2096
2375
  }
2376
+ return min <= max ? { min, max } : null;
2377
+ };
2378
+ let candleRange = scanCandleRange(startIndex, endIndex);
2379
+ if (!candleRange) {
2380
+ const latestIndex = data.length - 1;
2381
+ candleRange = scanCandleRange(Math.max(0, latestIndex - 120), latestIndex - 1) ?? scanCandleRange(0, latestIndex) ?? { min: data[latestIndex].l, max: data[latestIndex].h };
2097
2382
  }
2098
- let minPrice = Math.min(...priceSource.map((point) => point.l));
2099
- let maxPrice = Math.max(...priceSource.map((point) => point.h));
2383
+ let minPrice = candleRange.min;
2384
+ let maxPrice = candleRange.max;
2100
2385
  if (overlayIndicatorsForScale.length > 0) {
2101
- for (const { indicator } of overlayIndicatorsForScale) {
2386
+ for (const { indicator, plugin } of overlayIndicatorsForScale) {
2102
2387
  if (indicator.excludeFromAutoscale) {
2103
2388
  continue;
2104
2389
  }
2105
- const type = indicator.type;
2106
- const inputs = indicator.inputs;
2107
- const source = inputs.source ?? "close";
2108
- const length = clampIndicatorLength(inputs.length ?? 14, 14);
2109
- let series = null;
2110
- if (type === "sma") series = withCachedSeries(`sma|${length}|${source}`, data, () => computeSmaSeries(data, length, source));
2111
- if (type === "ema") series = withCachedSeries(`ema|${length}|${source}`, data, () => computeEmaSeries(data, length, source));
2112
- if (type === "wma") series = withCachedSeries(`wma|${length}|${source}`, data, () => computeWmaSeries(data, length, source));
2113
- if (type === "vwma") series = withCachedSeries(`vwma|${length}|${source}`, data, () => computeVwmaSeries(data, length, source));
2114
- if (type === "rma") series = withCachedSeries(`rma|${length}|${source}`, data, () => computeRmaSeries(data, length, source));
2115
- if (type === "hma") series = withCachedSeries(`hma|${length}|${source}`, data, () => computeHmaSeries(data, length, source));
2116
- if (!series) {
2390
+ const range = plugin.getAutoscaleRange?.(data, startIndex, endIndex, indicator.inputs, skipLatestIndex);
2391
+ if (!range || !Number.isFinite(range.min) || !Number.isFinite(range.max)) {
2117
2392
  continue;
2118
2393
  }
2119
- const visibleValues = [];
2120
- for (let idx = startIndex; idx <= endIndex; idx += 1) {
2121
- if (mergedOptions.autoScaleIgnoreLatestCandle && data.length > 1 && idx === data.length - 1) {
2122
- continue;
2123
- }
2124
- const value = series[idx];
2125
- if (Number.isFinite(value ?? Number.NaN)) {
2126
- visibleValues.push(value);
2127
- }
2128
- }
2129
- if (visibleValues.length === 0) {
2130
- continue;
2131
- }
2132
- const seriesMin = Math.min(...visibleValues);
2133
- const seriesMax = Math.max(...visibleValues);
2134
2394
  const weight = Math.min(1, Math.max(0, indicator.overlayScaleWeight));
2135
2395
  const currentMid = (minPrice + maxPrice) / 2;
2136
- const weightedMin = currentMid + (seriesMin - currentMid) * weight;
2137
- const weightedMax = currentMid + (seriesMax - currentMid) * weight;
2396
+ const weightedMin = currentMid + (range.min - currentMid) * weight;
2397
+ const weightedMax = currentMid + (range.max - currentMid) * weight;
2138
2398
  minPrice = Math.min(minPrice, weightedMin);
2139
2399
  maxPrice = Math.max(maxPrice, weightedMax);
2140
2400
  }
@@ -2209,7 +2469,7 @@ function createChart(element, options = {}) {
2209
2469
  ctx.save();
2210
2470
  const prevFont = ctx.font;
2211
2471
  ctx.font = `500 12px ${mergedOptions.fontFamily}`;
2212
- const textWidth = ctx.measureText(labelText).width;
2472
+ const textWidth = measureTextWidth(labelText);
2213
2473
  const paddingX = 6;
2214
2474
  const bgWidth = textWidth + paddingX * 2;
2215
2475
  const bgHeight = 18;
@@ -2375,7 +2635,7 @@ function createChart(element, options = {}) {
2375
2635
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2376
2636
  levelLines.forEach((level, index) => {
2377
2637
  const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2378
- const textWidth = ctx.measureText(labelText).width;
2638
+ const textWidth = measureTextWidth(labelText);
2379
2639
  const padding = 4;
2380
2640
  const bgX = lineLeft + 4;
2381
2641
  const bgY = level.y - 9;
@@ -2448,7 +2708,7 @@ function createChart(element, options = {}) {
2448
2708
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2449
2709
  levelLines.forEach((level, index) => {
2450
2710
  const labelText = `${level.ratio} (${formatPrice(level.price)})`;
2451
- const textWidth = ctx.measureText(labelText).width;
2711
+ const textWidth = measureTextWidth(labelText);
2452
2712
  const padding = 4;
2453
2713
  const bgX = chartLeft + 4;
2454
2714
  const bgY = level.y - 9;
@@ -2591,8 +2851,8 @@ function createChart(element, options = {}) {
2591
2851
  const padding = 6;
2592
2852
  const lineH = 14;
2593
2853
  const isDigit = (ch) => ch >= "0" && ch <= "9";
2594
- const digitW = ctx.measureText("0").width;
2595
- const charAdvance = (ch) => isDigit(ch) ? digitW : ctx.measureText(ch).width;
2854
+ const digitW = measureTextWidth("0");
2855
+ const charAdvance = (ch) => isDigit(ch) ? digitW : measureTextWidth(ch);
2596
2856
  const lineWidth = (line) => {
2597
2857
  let w = 0;
2598
2858
  for (const ch of line) w += charAdvance(ch);
@@ -2615,11 +2875,11 @@ function createChart(element, options = {}) {
2615
2875
  const y = startY + lineIndex * lineH;
2616
2876
  for (const ch of line) {
2617
2877
  if (isDigit(ch)) {
2618
- ctx.fillText(ch, x + (digitW - ctx.measureText(ch).width) / 2, y);
2878
+ ctx.fillText(ch, x + (digitW - measureTextWidth(ch)) / 2, y);
2619
2879
  x += digitW;
2620
2880
  } else {
2621
2881
  ctx.fillText(ch, x, y);
2622
- x += ctx.measureText(ch).width;
2882
+ x += measureTextWidth(ch);
2623
2883
  }
2624
2884
  }
2625
2885
  });
@@ -2731,15 +2991,31 @@ function createChart(element, options = {}) {
2731
2991
  const pct = diff / base * 100;
2732
2992
  const ticks = tick > 0 ? Math.round(diff / tick) : 0;
2733
2993
  const signed = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
2734
- 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)}%`)})`;
2994
+ const labelLines = [
2995
+ 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)}%`)})`
2996
+ ];
2997
+ const barSpan = Math.abs(Math.round(p1.index) - Math.round(p0.index));
2998
+ if (barSpan > 0) {
2999
+ const t0 = getTimeForIndex(Math.round(p0.index));
3000
+ const t1 = getTimeForIndex(Math.round(p1.index));
3001
+ const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
3002
+ labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
3003
+ }
3004
+ const pointValue = Number(drawing.pointValue);
3005
+ if (Number.isFinite(pointValue) && pointValue > 0) {
3006
+ const dollars = diff * pointValue;
3007
+ const money = Math.abs(dollars).toLocaleString("en-US", { minimumFractionDigits: 2, maximumFractionDigits: 2 });
3008
+ labelLines.push(`${signed(dollars, `$${money}`)} / contract`);
3009
+ }
2735
3010
  const prevFont = ctx.font;
2736
3011
  ctx.font = `500 11px ${mergedOptions.fontFamily}`;
2737
3012
  const padding = 6;
2738
- const textW = ctx.measureText(labelText).width;
3013
+ const lineHeight = 15;
3014
+ const textW = Math.max(...labelLines.map((line) => measureTextWidth(line)));
2739
3015
  const pillW = textW + padding * 2;
2740
- const pillH = 18;
3016
+ const pillH = labelLines.length * lineHeight + padding;
2741
3017
  const pillX = midX - pillW / 2;
2742
- const pillY = botY + 6;
3018
+ const pillY = botY + 6 + pillH <= fullChartBottom - 2 ? botY + 6 : Math.max(chartTop + 2, topY - 6 - pillH);
2743
3019
  ctx.fillStyle = mergedOptions.backgroundColor;
2744
3020
  fillRoundedRect(pillX, pillY, pillW, pillH, 4);
2745
3021
  ctx.save();
@@ -2750,7 +3026,9 @@ function createChart(element, options = {}) {
2750
3026
  ctx.fillStyle = drawing.color;
2751
3027
  ctx.textAlign = "center";
2752
3028
  ctx.textBaseline = "middle";
2753
- ctx.fillText(labelText, midX, pillY + pillH / 2);
3029
+ labelLines.forEach((line, lineIndex) => {
3030
+ ctx.fillText(line, midX, pillY + padding / 2 + lineIndex * lineHeight + lineHeight / 2);
3031
+ });
2754
3032
  ctx.font = prevFont;
2755
3033
  if (drawing.label) {
2756
3034
  drawDrawingLabel(drawing.label, midX, topY - 4, drawing.color);
@@ -2769,7 +3047,7 @@ function createChart(element, options = {}) {
2769
3047
  ctx.textBaseline = "middle";
2770
3048
  const lines = (drawing.label ?? "").split("\n");
2771
3049
  const lineH = Math.round(fontSize * 1.35);
2772
- const textW = Math.max(1, ...lines.map((line) => ctx.measureText(line).width));
3050
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
2773
3051
  const padX = isNote ? 8 : 2;
2774
3052
  const padY = isNote ? 6 : 1;
2775
3053
  const blockW = textW + padX * 2;
@@ -2851,47 +3129,114 @@ function createChart(element, options = {}) {
2851
3129
  Math.max(1, candleSpacing - 1),
2852
3130
  Math.max(candleMinWidth, Math.floor(candleSpacing * candleBodyWidthRatio))
2853
3131
  );
2854
- const grid = { ...DEFAULT_GRID_OPTIONS, ...mergedOptions.grid ?? {} };
3132
+ const grid = resolvedGrid;
2855
3133
  const gridOpacity = clamp(grid.opacity, 0, 1);
2856
3134
  const yTickCountInput = grid.yTickCount ?? grid.horizontalTickCount;
2857
3135
  const yTicks = Math.max(1, Math.floor(yTickCountInput));
2858
3136
  const gridColor = grid.color ?? mergedOptions.gridColor;
3137
+ const priceTicks = [];
3138
+ {
3139
+ const targetStep = yRange / Math.max(1, yTicks);
3140
+ let step;
3141
+ if (targetStep > 0 && Number.isFinite(targetStep)) {
3142
+ const base = Math.pow(10, Math.floor(Math.log10(targetStep)));
3143
+ const frac = targetStep / base;
3144
+ step = (frac <= 1 ? 1 : frac <= 2 ? 2 : frac <= 2.5 ? 2.5 : frac <= 5 ? 5 : 10) * base;
3145
+ const instrumentTick = getConfiguredTickSize();
3146
+ if (instrumentTick > 0 && step < instrumentTick * 1e6) {
3147
+ step = Math.max(instrumentTick, Math.round(step / instrumentTick) * instrumentTick);
3148
+ }
3149
+ } else {
3150
+ step = 1;
3151
+ }
3152
+ const firstTick = Math.ceil(yMin / step) * step;
3153
+ for (let i = 0; ; i += 1) {
3154
+ const price = firstTick + i * step;
3155
+ if (price > yMax + step * 1e-6) break;
3156
+ priceTicks.push(price);
3157
+ if (priceTicks.length > 200) break;
3158
+ }
3159
+ }
2859
3160
  if (grid.horizontalLines) {
2860
3161
  ctx.save();
2861
3162
  ctx.globalAlpha = gridOpacity;
2862
- for (let tick = 0; tick <= yTicks; tick += 1) {
2863
- const ratio = tick / yTicks;
2864
- const price = yMin + yRange * ratio;
3163
+ ctx.strokeStyle = gridColor;
3164
+ ctx.lineWidth = 1;
3165
+ ctx.beginPath();
3166
+ for (const price of priceTicks) {
2865
3167
  const y = yFromPrice(price);
2866
- ctx.strokeStyle = gridColor;
2867
- ctx.lineWidth = 1;
2868
- ctx.beginPath();
2869
3168
  ctx.moveTo(crisp(chartLeft), crisp(y));
2870
3169
  ctx.lineTo(crisp(chartRight), crisp(y));
2871
- ctx.stroke();
2872
3170
  }
3171
+ ctx.stroke();
2873
3172
  ctx.restore();
2874
3173
  }
2875
- const minLabelSpacingPx = Math.max(72, candleSpacing * 6);
2876
- const autoLabelCount = Math.max(2, Math.floor(chartWidth / minLabelSpacingPx));
2877
- const xTickCountInput = grid.xTickCount ?? autoLabelCount;
2878
- const xTickCount = Math.max(2, Math.floor(xTickCountInput));
2879
- const rawStep = xSpan / xTickCount;
2880
- const xStep = Math.max(1, Math.ceil(rawStep));
2881
- const visibleTickStart = Math.floor(xStart);
3174
+ const minLabelSpacingPx = Math.max(60, Math.min(120, chartWidth / Math.max(2, Math.floor(grid.xTickCount ?? 8))));
3175
+ const visibleTickStart = Math.max(0, Math.floor(xStart));
2882
3176
  const visibleTickEnd = Math.ceil(xEnd) - 1;
2883
- const tickStartIndex = Math.ceil(visibleTickStart / xStep) * xStep;
2884
- if (grid.verticalLines) {
3177
+ const timeTicks = [];
3178
+ {
3179
+ const weightBuckets = [[], [], [], [], [], [], [], [], [], []];
3180
+ let prevTime = visibleTickStart > 0 ? getTimeForIndex(visibleTickStart - 1) : null;
3181
+ for (let index = visibleTickStart; index <= visibleTickEnd; index += 1) {
3182
+ const time = getTimeForIndex(index);
3183
+ if (!time) {
3184
+ prevTime = null;
3185
+ continue;
3186
+ }
3187
+ let weight = 0;
3188
+ if (!prevTime) {
3189
+ weight = 7;
3190
+ } else {
3191
+ if (prevTime.getFullYear() !== time.getFullYear()) weight = 9;
3192
+ else if (prevTime.getMonth() !== time.getMonth()) weight = 8;
3193
+ else if (prevTime.getDate() !== time.getDate()) weight = 7;
3194
+ else {
3195
+ const prevMinutes = prevTime.getHours() * 60 + prevTime.getMinutes();
3196
+ const curMinutes = time.getHours() * 60 + time.getMinutes();
3197
+ const crossed = (span) => Math.floor(prevMinutes / span) !== Math.floor(curMinutes / span);
3198
+ if (crossed(360)) weight = 6;
3199
+ else if (crossed(60)) weight = 5;
3200
+ else if (crossed(30)) weight = 4;
3201
+ else if (crossed(15)) weight = 3;
3202
+ else if (crossed(5)) weight = 2;
3203
+ else weight = 1;
3204
+ }
3205
+ }
3206
+ weightBuckets[weight].push(index);
3207
+ prevTime = time;
3208
+ }
3209
+ const placedXs = [];
3210
+ const maxMarks = Math.floor(chartWidth / minLabelSpacingPx) + 2;
3211
+ for (let weight = 9; weight >= 1 && timeTicks.length < maxMarks; weight -= 1) {
3212
+ for (const index of weightBuckets[weight]) {
3213
+ const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
3214
+ let fits = true;
3215
+ for (const placedX of placedXs) {
3216
+ if (Math.abs(placedX - x) < minLabelSpacingPx) {
3217
+ fits = false;
3218
+ break;
3219
+ }
3220
+ }
3221
+ if (!fits) continue;
3222
+ placedXs.push(x);
3223
+ timeTicks.push({ index, weight, x });
3224
+ if (timeTicks.length >= maxMarks) break;
3225
+ }
3226
+ }
3227
+ timeTicks.sort((a, b) => a.index - b.index);
3228
+ }
3229
+ if (grid.verticalLines && timeTicks.length > 0) {
2885
3230
  ctx.save();
2886
3231
  ctx.globalAlpha = gridOpacity;
2887
- for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
2888
- const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
2889
- ctx.strokeStyle = gridColor;
2890
- ctx.beginPath();
2891
- ctx.moveTo(crisp(x), crisp(chartTop));
2892
- ctx.lineTo(crisp(x), crisp(fullChartBottom));
2893
- ctx.stroke();
3232
+ ctx.strokeStyle = gridColor;
3233
+ ctx.lineWidth = 1;
3234
+ ctx.beginPath();
3235
+ for (const tick of timeTicks) {
3236
+ ctx.moveTo(crisp(tick.x), crisp(chartTop));
3237
+ ctx.lineTo(crisp(tick.x), crisp(fullChartBottom));
2894
3238
  }
3239
+ ctx.stroke();
2895
3240
  ctx.restore();
2896
3241
  }
2897
3242
  if (grid.sessionSeparators && data.length > 1) {
@@ -2929,38 +3274,45 @@ function createChart(element, options = {}) {
2929
3274
  }
2930
3275
  return data[index]?.v;
2931
3276
  };
2932
- for (let index = startIndex; index <= endIndex; index += 1) {
2933
- const point = data[index];
2934
- if (!point) {
2935
- continue;
3277
+ {
3278
+ const upWicks = new Path2D();
3279
+ const downWicks = new Path2D();
3280
+ const upBodies = new Path2D();
3281
+ const downBodies = new Path2D();
3282
+ for (let index = startIndex; index <= endIndex; index += 1) {
3283
+ const point = data[index];
3284
+ if (!point) {
3285
+ continue;
3286
+ }
3287
+ const isLastCandle = useSmoothedCandle && index === lastDataIndex;
3288
+ const actualDirection = point.c >= point.o ? "up" : "down";
3289
+ const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
3290
+ const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
3291
+ const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
3292
+ const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
3293
+ const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
3294
+ const isUp = direction === "up";
3295
+ const roundedCenterX = Math.round(centerX);
3296
+ const wicks = isUp ? upWicks : downWicks;
3297
+ wicks.moveTo(roundedCenterX + 0.5, crisp(yFromPrice(displayHigh)));
3298
+ wicks.lineTo(roundedCenterX + 0.5, crisp(yFromPrice(displayLow)));
3299
+ const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
3300
+ const openYPx = Math.round(yFromPrice(point.o));
3301
+ const closeYPx = Math.round(yFromPrice(displayClose));
3302
+ const bodyIsUp = displayClose >= point.o;
3303
+ const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
3304
+ const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
3305
+ (isUp ? upBodies : downBodies).rect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
2936
3306
  }
2937
- const isLastCandle = useSmoothedCandle && index === lastDataIndex;
2938
- const actualDirection = point.c >= point.o ? "up" : "down";
2939
- const displayClose = isLastCandle ? actualDirection === "up" ? Math.max(point.o, smoothedTickerPrice) : Math.min(point.o, smoothedTickerPrice) : point.c;
2940
- const displayHigh = isLastCandle ? actualDirection === "up" ? Math.max(point.h, displayClose) : point.h : point.h;
2941
- const displayLow = isLastCandle ? actualDirection === "up" ? point.l : Math.min(point.l, displayClose) : point.l;
2942
- const centerX = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
2943
- const openY = yFromPrice(point.o);
2944
- const closeY = yFromPrice(displayClose);
2945
- const highY = yFromPrice(displayHigh);
2946
- const lowY = yFromPrice(displayLow);
2947
- const direction = isLastCandle ? actualDirection : getCandleDirectionByIndex(index);
2948
- const candleColor = direction === "up" ? mergedOptions.upColor : mergedOptions.downColor;
2949
- const roundedCenterX = Math.round(centerX);
2950
- ctx.strokeStyle = candleColor;
2951
3307
  ctx.lineWidth = candleWickWidth;
2952
- ctx.beginPath();
2953
- ctx.moveTo(roundedCenterX + 0.5, crisp(highY));
2954
- ctx.lineTo(roundedCenterX + 0.5, crisp(lowY));
2955
- ctx.stroke();
2956
- const bodyLeft = roundedCenterX - Math.floor(bodyWidth / 2);
2957
- const openYPx = Math.round(openY);
2958
- const closeYPx = Math.round(closeY);
2959
- const bodyIsUp = displayClose >= point.o;
2960
- const bodyTop = bodyIsUp ? Math.min(closeYPx, openYPx - 1) : openYPx;
2961
- const bodyBottom = bodyIsUp ? openYPx : Math.max(closeYPx, openYPx + 1);
2962
- ctx.fillStyle = candleColor;
2963
- ctx.fillRect(bodyLeft, bodyTop, bodyWidth, bodyBottom - bodyTop);
3308
+ ctx.strokeStyle = mergedOptions.upColor;
3309
+ ctx.stroke(upWicks);
3310
+ ctx.fillStyle = mergedOptions.upColor;
3311
+ ctx.fill(upBodies);
3312
+ ctx.strokeStyle = mergedOptions.downColor;
3313
+ ctx.stroke(downWicks);
3314
+ ctx.fillStyle = mergedOptions.downColor;
3315
+ ctx.fill(downBodies);
2964
3316
  }
2965
3317
  const activeOverlayIndicators = indicators.filter((indicator) => indicator.visible).map((indicator) => ({ indicator, plugin: indicatorRegistry.get(indicator.type) })).filter(
2966
3318
  (value) => value.plugin !== void 0 && (value.indicator.pane ?? value.plugin.pane ?? "overlay") === "overlay"
@@ -3078,56 +3430,6 @@ function createChart(element, options = {}) {
3078
3430
  ctx.textAlign = prevAlign;
3079
3431
  ctx.textBaseline = prevBaseline;
3080
3432
  }
3081
- const crosshair = { ...DEFAULT_CROSSHAIR_OPTIONS, ...mergedOptions.crosshair ?? {} };
3082
- if (crosshair.visible && crosshairPoint) {
3083
- const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
3084
- const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
3085
- if (crosshair.mode === "dot") {
3086
- ctx.save();
3087
- ctx.fillStyle = crosshair.color;
3088
- ctx.beginPath();
3089
- ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
3090
- ctx.fill();
3091
- ctx.restore();
3092
- } else {
3093
- ctx.save();
3094
- ctx.strokeStyle = crosshair.color;
3095
- ctx.lineWidth = Math.max(1, crosshair.width);
3096
- applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
3097
- if (crosshair.showVertical) {
3098
- ctx.beginPath();
3099
- ctx.moveTo(crisp(cx), crisp(chartTop));
3100
- ctx.lineTo(crisp(cx), crisp(chartBottom));
3101
- ctx.stroke();
3102
- }
3103
- if (crosshair.showHorizontal) {
3104
- ctx.beginPath();
3105
- ctx.moveTo(crisp(chartLeft), crisp(cy));
3106
- ctx.lineTo(crisp(chartRight), crisp(cy));
3107
- ctx.stroke();
3108
- }
3109
- ctx.restore();
3110
- }
3111
- if (crosshair.sideHintLeft || crosshair.sideHintRight) {
3112
- ctx.save();
3113
- ctx.font = `600 11px ${mergedOptions.fontFamily}`;
3114
- ctx.textBaseline = "middle";
3115
- ctx.setLineDash([]);
3116
- const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
3117
- const hintGap = 8;
3118
- if (crosshair.sideHintLeft) {
3119
- ctx.fillStyle = crosshair.sideHintLeftColor;
3120
- ctx.textAlign = "right";
3121
- ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
3122
- }
3123
- if (crosshair.sideHintRight) {
3124
- ctx.fillStyle = crosshair.sideHintRightColor;
3125
- ctx.textAlign = "left";
3126
- ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
3127
- }
3128
- ctx.restore();
3129
- }
3130
- }
3131
3433
  ctx.restore();
3132
3434
  const positionAxisGutter = Math.max(0, width - chartRight);
3133
3435
  if (positionAxisGutter > 0) {
@@ -3240,7 +3542,7 @@ function createChart(element, options = {}) {
3240
3542
  const text = label.text ?? formatPaneValue(label.value);
3241
3543
  const labelPaddingX = 7;
3242
3544
  const labelHeight = Math.max(16, yAxisFontSize + 8);
3243
- const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(ctx.measureText(text).width) + labelPaddingX * 2);
3545
+ const labelWidth = getRightAxisLabelWidth(chartRight, Math.ceil(measureTextWidth(text)) + labelPaddingX * 2);
3244
3546
  const labelX = getRightAxisLabelX(chartRight);
3245
3547
  const labelY = clamp(yFromPaneValue(label.value) - labelHeight / 2, paneTop + 2, paneBottom - labelHeight - 2);
3246
3548
  ctx.fillStyle = label.backgroundColor ?? label.color ?? labels.backgroundColor;
@@ -3252,18 +3554,6 @@ function createChart(element, options = {}) {
3252
3554
  }
3253
3555
  });
3254
3556
  }
3255
- if (crosshair.visible && crosshairPoint && crosshair.showVertical) {
3256
- const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
3257
- ctx.save();
3258
- ctx.strokeStyle = crosshair.color;
3259
- ctx.lineWidth = Math.max(1, crosshair.width);
3260
- applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
3261
- ctx.beginPath();
3262
- ctx.moveTo(crisp(cx), crisp(chartTop));
3263
- ctx.lineTo(crisp(cx), crisp(fullChartBottom));
3264
- ctx.stroke();
3265
- ctx.restore();
3266
- }
3267
3557
  ctx.strokeStyle = axis.lineColor;
3268
3558
  ctx.lineWidth = Math.max(1, axis.lineWidth);
3269
3559
  ctx.beginPath();
@@ -3273,13 +3563,13 @@ function createChart(element, options = {}) {
3273
3563
  ctx.lineTo(crisp(chartRight), crisp(fullChartBottom));
3274
3564
  ctx.stroke();
3275
3565
  const priceScaleTickLabelInset = yAxisFontSize / 2 + 3;
3276
- for (let tick = 0; tick <= yTicks; tick += 1) {
3277
- const ratio = tick / yTicks;
3278
- const price = yMin + yRange * ratio;
3279
- const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
3566
+ {
3280
3567
  const prevFont = ctx.font;
3281
3568
  ctx.font = `${yAxisFontSize}px ${mergedOptions.fontFamily}`;
3282
- drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
3569
+ for (const price of priceTicks) {
3570
+ const y = clamp(yFromPrice(price), chartTop + priceScaleTickLabelInset, chartBottom - priceScaleTickLabelInset);
3571
+ drawText(formatPrice(price), chartRight + 6, y, "left", "middle", yAxis.textColor);
3572
+ }
3283
3573
  ctx.font = prevFont;
3284
3574
  }
3285
3575
  resetLabelSlots(labels.noOverlapping);
@@ -3388,9 +3678,15 @@ function createChart(element, options = {}) {
3388
3678
  });
3389
3679
  }
3390
3680
  }
3391
- if (labels.showHighLow && visibleData.length > 0) {
3392
- const visibleHigh = Math.max(...visibleData.map((point) => point.h));
3393
- const visibleLow = Math.min(...visibleData.map((point) => point.l));
3681
+ if (labels.showHighLow && endIndex >= startIndex) {
3682
+ let visibleHigh = Number.NEGATIVE_INFINITY;
3683
+ let visibleLow = Number.POSITIVE_INFINITY;
3684
+ for (let idx = startIndex; idx <= endIndex; idx += 1) {
3685
+ const point = data[idx];
3686
+ if (!point) continue;
3687
+ if (point.h > visibleHigh) visibleHigh = point.h;
3688
+ if (point.l < visibleLow) visibleLow = point.l;
3689
+ }
3394
3690
  addPriceAxisLabel({
3395
3691
  text: `H ${formatPrice(visibleHigh)}`,
3396
3692
  price: visibleHigh,
@@ -3443,12 +3739,12 @@ function createChart(element, options = {}) {
3443
3739
  const subtextFontSize = label.subtextFontSize !== void 0 && label.subtextFontSize > 0 ? Math.max(8, Math.round(label.subtextFontSize)) : priceLabelFontSize;
3444
3740
  const subtextLineGap = 5;
3445
3741
  const labelHeight = baseLabelHeight + (subtexts.length > 0 ? subtexts.length * subtextFontSize + subtexts.length * subtextLineGap : 0);
3446
- const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(ctx.measureText(label.text).width) + labelPaddingX * 2;
3742
+ const primaryWidth = label.text === formatPrice(label.price) ? getPriceLabelWidth(label.text, labelPaddingX) : Math.ceil(measureTextWidth(label.text)) + labelPaddingX * 2;
3447
3743
  let subtextWidth = 0;
3448
3744
  if (subtexts.length > 0) {
3449
3745
  const baseFont = ctx.font;
3450
3746
  ctx.font = `${subtextFontSize}px ${mergedOptions.fontFamily}`;
3451
- subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(ctx.measureText(subtext).width) + labelPaddingX * 2));
3747
+ subtextWidth = Math.max(...subtexts.map((subtext) => Math.ceil(measureTextWidth(subtext)) + labelPaddingX * 2));
3452
3748
  ctx.font = baseFont;
3453
3749
  }
3454
3750
  const labelTextWidth = Math.max(primaryWidth, subtextWidth);
@@ -3526,8 +3822,9 @@ function createChart(element, options = {}) {
3526
3822
  }
3527
3823
  return typeof value === "string" && !/^#(?:[0-9a-fA-F]{3}){1,2}$/.test(value.trim());
3528
3824
  };
3825
+ const LEGEND_EXCLUDED_INPUT_KEYS = /* @__PURE__ */ new Set(["width", "bandMultiplier", "bandFillOpacity", "fillOpacity"]);
3529
3826
  const labelEntries = activeOverlayIndicators.map(({ indicator, plugin }) => {
3530
- const inputValues = Object.entries(indicator.inputs).filter(([, value]) => isLegendInputValue(value)).slice(0, 2).map(([, value]) => String(value));
3827
+ 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));
3531
3828
  if (labels.showIndicatorNames && labels.showIndicatorValues && inputValues.length > 0) {
3532
3829
  return `${plugin.name} ${inputValues.join(" ")}`;
3533
3830
  }
@@ -3551,21 +3848,15 @@ function createChart(element, options = {}) {
3551
3848
  ctx.font = prevFont;
3552
3849
  }
3553
3850
  }
3554
- const axisStepMs = getTimeStepMs();
3555
- const axisIntraday = axisStepMs > 0 && axisStepMs < 24 * 60 * 60 * 1e3;
3556
- let prevAxisTickTime = null;
3557
- for (let index = tickStartIndex; index <= visibleTickEnd; index += xStep) {
3558
- const tickTime = getTimeForIndex(index);
3559
- if (!tickTime) {
3560
- continue;
3561
- }
3562
- const x = chartLeft + (index + 0.5 - xStart) / xSpan * chartWidth;
3563
- const isNewDay = !prevAxisTickTime || prevAxisTickTime.getDate() !== tickTime.getDate() || prevAxisTickTime.getMonth() !== tickTime.getMonth() || prevAxisTickTime.getFullYear() !== tickTime.getFullYear();
3564
- const timeLabel = axisIntraday && !isNewDay ? tickTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false }) : tickTime.toLocaleDateString(void 0, { month: "short", day: "numeric" });
3565
- prevAxisTickTime = tickTime;
3851
+ {
3566
3852
  const prevFont = ctx.font;
3567
3853
  ctx.font = `${xAxisFontSize}px ${mergedOptions.fontFamily}`;
3568
- drawText(timeLabel, x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
3854
+ for (const tick of timeTicks) {
3855
+ const tickTime = getTimeForIndex(tick.index);
3856
+ if (!tickTime) continue;
3857
+ const timeLabel = tick.weight >= 9 ? String(tickTime.getFullYear()) : tick.weight === 8 ? tickTime.toLocaleDateString(void 0, { month: "short" }) : tick.weight === 7 ? tickTime.toLocaleDateString(void 0, { month: "short", day: "numeric" }) : tickTime.toLocaleTimeString(void 0, { hour: "2-digit", minute: "2-digit", hour12: false });
3858
+ drawText(timeLabel, tick.x, (fullChartBottom + height) / 2, "center", "middle", xAxis.textColor);
3859
+ }
3569
3860
  ctx.font = prevFont;
3570
3861
  }
3571
3862
  if (labels.visible && labels.showCountdownToBarClose && lastPoint) {
@@ -3578,138 +3869,230 @@ function createChart(element, options = {}) {
3578
3869
  drawText(countdownText, chartRight + 6, (fullChartBottom + height) / 2, "left", "middle", xAxis.textColor);
3579
3870
  ctx.font = prevFont;
3580
3871
  }
3581
- if (crosshair.visible && crosshairPoint) {
3582
- const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
3583
- const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
3584
- const labelPaddingX = 8;
3585
- const labelHeight = 20;
3586
- const labelRadius = Math.max(0, crosshair.labelBorderRadius);
3587
- const labelBackground = crosshair.labelBackgroundColor;
3588
- const labelTextColor = crosshair.labelTextColor;
3589
- const labelBorderColor = crosshair.labelBorderColor;
3590
- const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
3591
- const labelBorderStyle = crosshair.labelBorderStyle;
3592
- const strokeCrosshairLabel = (x, y, widthValue, heightValue = labelHeight) => {
3593
- if (labelBorderWidth <= 0) {
3594
- return;
3595
- }
3596
- ctx.save();
3597
- ctx.strokeStyle = labelBorderColor;
3598
- ctx.lineWidth = labelBorderWidth;
3599
- applyDashPattern(
3600
- labelBorderStyle,
3601
- dashPatterns.borderDotted,
3602
- dashPatterns.borderDashed
3603
- );
3604
- strokeRoundedRect(Math.round(x), Math.round(y), widthValue, heightValue, labelRadius);
3605
- ctx.restore();
3606
- };
3607
- if (crosshair.showPriceLabel) {
3608
- const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
3609
- const priceText = formatPrice(hoverPrice);
3610
- const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
3611
- const priceX = getRightAxisLabelX(chartRight);
3612
- const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
3872
+ if (baseCtx) {
3873
+ if (baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height) {
3874
+ baseCanvas.width = canvas.width;
3875
+ baseCanvas.height = canvas.height;
3876
+ }
3877
+ baseCtx.setTransform(1, 0, 0, 1, 0, 0);
3878
+ baseCtx.clearRect(0, 0, baseCanvas.width, baseCanvas.height);
3879
+ baseCtx.drawImage(canvas, 0, 0);
3880
+ }
3881
+ overlayLayout = {
3882
+ chartLeft,
3883
+ chartTop,
3884
+ chartRight,
3885
+ chartBottom,
3886
+ chartWidth,
3887
+ chartHeight,
3888
+ fullChartBottom,
3889
+ yMin,
3890
+ yRange,
3891
+ xStart,
3892
+ xSpan
3893
+ };
3894
+ paintCrosshairOverlay();
3895
+ };
3896
+ const paintCrosshairOverlay = () => {
3897
+ crosshairPriceActionRegion = null;
3898
+ const layout = overlayLayout;
3899
+ const crosshair = resolvedCrosshair;
3900
+ if (!layout || !crosshair.visible || !crosshairPoint) {
3901
+ return;
3902
+ }
3903
+ const { chartLeft, chartTop, chartRight, chartBottom, chartWidth, chartHeight, fullChartBottom, yMin, yRange, xStart, xSpan: xSpan2 } = layout;
3904
+ const cx = clamp(crosshairPoint.x, chartLeft, chartRight);
3905
+ const cy = clamp(crosshairPoint.y, chartTop, chartBottom);
3906
+ ctx.save();
3907
+ ctx.beginPath();
3908
+ ctx.rect(chartLeft, chartTop, chartWidth, Math.max(0, chartBottom - chartTop));
3909
+ ctx.clip();
3910
+ if (crosshair.mode === "dot") {
3911
+ ctx.fillStyle = crosshair.color;
3912
+ ctx.beginPath();
3913
+ ctx.arc(cx, cy, Math.max(1, crosshair.dotRadius), 0, Math.PI * 2);
3914
+ ctx.fill();
3915
+ } else {
3916
+ ctx.strokeStyle = crosshair.color;
3917
+ ctx.lineWidth = Math.max(1, crosshair.width);
3918
+ applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
3919
+ if (crosshair.showHorizontal) {
3920
+ ctx.beginPath();
3921
+ ctx.moveTo(crisp(chartLeft), crisp(cy));
3922
+ ctx.lineTo(crisp(chartRight), crisp(cy));
3923
+ ctx.stroke();
3924
+ }
3925
+ }
3926
+ if (crosshair.sideHintLeft || crosshair.sideHintRight) {
3927
+ ctx.save();
3928
+ ctx.font = `600 11px ${mergedOptions.fontFamily}`;
3929
+ ctx.textBaseline = "middle";
3930
+ ctx.setLineDash([]);
3931
+ const hintY = clamp(cy - 14, chartTop + 8, chartBottom - 8);
3932
+ const hintGap = 8;
3933
+ if (crosshair.sideHintLeft) {
3934
+ ctx.fillStyle = crosshair.sideHintLeftColor;
3935
+ ctx.textAlign = "right";
3936
+ ctx.fillText(crosshair.sideHintLeft, cx - hintGap, hintY);
3937
+ }
3938
+ if (crosshair.sideHintRight) {
3939
+ ctx.fillStyle = crosshair.sideHintRightColor;
3940
+ ctx.textAlign = "left";
3941
+ ctx.fillText(crosshair.sideHintRight, cx + hintGap, hintY);
3942
+ }
3943
+ ctx.restore();
3944
+ }
3945
+ ctx.restore();
3946
+ if (crosshair.showVertical) {
3947
+ ctx.save();
3948
+ ctx.strokeStyle = crosshair.color;
3949
+ ctx.lineWidth = Math.max(1, crosshair.width);
3950
+ applyDashPattern(crosshair.style, dashPatterns.dotted, dashPatterns.dashed);
3951
+ ctx.beginPath();
3952
+ ctx.moveTo(crisp(cx), crisp(chartTop));
3953
+ ctx.lineTo(crisp(cx), crisp(fullChartBottom));
3954
+ ctx.stroke();
3955
+ ctx.restore();
3956
+ }
3957
+ const labelPaddingX = 8;
3958
+ const labelHeight = 20;
3959
+ const labelRadius = Math.max(0, crosshair.labelBorderRadius);
3960
+ const labelBackground = crosshair.labelBackgroundColor;
3961
+ const labelTextColor = crosshair.labelTextColor;
3962
+ const labelBorderColor = crosshair.labelBorderColor;
3963
+ const labelBorderWidth = Math.max(0, crosshair.labelBorderWidth);
3964
+ const labelBorderStyle = crosshair.labelBorderStyle;
3965
+ const strokeCrosshairLabel = (x, y, widthValue, heightValue = labelHeight) => {
3966
+ if (labelBorderWidth <= 0) {
3967
+ return;
3968
+ }
3969
+ ctx.save();
3970
+ ctx.strokeStyle = labelBorderColor;
3971
+ ctx.lineWidth = labelBorderWidth;
3972
+ applyDashPattern(
3973
+ labelBorderStyle,
3974
+ dashPatterns.borderDotted,
3975
+ dashPatterns.borderDashed
3976
+ );
3977
+ strokeRoundedRect(Math.round(x), Math.round(y), widthValue, heightValue, labelRadius);
3978
+ ctx.restore();
3979
+ };
3980
+ if (crosshair.showPriceLabel) {
3981
+ const hoverPrice = yMin + (chartBottom - cy) / chartHeight * yRange;
3982
+ const priceText = formatPrice(hoverPrice);
3983
+ const priceWidth = getRightAxisLabelWidth(chartRight, getPriceLabelWidth(priceText, labelPaddingX));
3984
+ const priceX = getRightAxisLabelX(chartRight);
3985
+ const priceY = clamp(cy - labelHeight / 2, chartTop, chartBottom - labelHeight);
3986
+ ctx.fillStyle = labelBackground;
3987
+ fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
3988
+ strokeCrosshairLabel(priceX, priceY, priceWidth);
3989
+ drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
3990
+ if (crosshair.showPriceActionButton) {
3991
+ const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
3992
+ const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
3993
+ const containerPaddingX = 5;
3994
+ const containerWidth = buttonSize + containerPaddingX * 2;
3995
+ const containerX = priceX - buttonGap - containerWidth;
3996
+ const containerY = priceY;
3997
+ const buttonX = containerX + containerPaddingX;
3998
+ const buttonY = priceY + (labelHeight - buttonSize) / 2;
3999
+ const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
4000
+ const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
4001
+ const buttonCenterX = buttonX + buttonSize / 2;
4002
+ const buttonCenterY = buttonY + buttonSize / 2;
4003
+ const containerRadius = labelRadius;
3613
4004
  ctx.fillStyle = labelBackground;
3614
- fillRoundedRect(Math.round(priceX), Math.round(priceY), priceWidth, labelHeight, labelRadius);
3615
- strokeCrosshairLabel(priceX, priceY, priceWidth);
3616
- drawText(priceText, priceX + labelPaddingX, priceY + labelHeight / 2, "left", "middle", labelTextColor);
3617
- if (crosshair.showPriceActionButton) {
3618
- const buttonSize = clamp(Math.round(crosshair.priceActionButtonSize), 12, Math.max(12, labelHeight - 4));
3619
- const buttonGap = Math.max(0, Math.round(crosshair.priceActionButtonGap));
3620
- const containerPaddingX = 5;
3621
- const containerWidth = buttonSize + containerPaddingX * 2;
3622
- const containerX = priceX - buttonGap - containerWidth;
3623
- const containerY = priceY;
3624
- const buttonX = containerX + containerPaddingX;
3625
- const buttonY = priceY + (labelHeight - buttonSize) / 2;
3626
- const buttonRadius = crosshair.priceActionButtonRounded ? clamp(Math.round(crosshair.priceActionButtonBorderRadius), 0, buttonSize / 2) : 0;
3627
- const buttonBorderWidth = Math.max(1, Math.round(labelBorderWidth || 1));
3628
- const buttonCenterX = buttonX + buttonSize / 2;
3629
- const buttonCenterY = buttonY + buttonSize / 2;
3630
- const containerRadius = labelRadius;
3631
- ctx.fillStyle = labelBackground;
3632
- fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
3633
- if (labelBorderWidth > 0) {
3634
- ctx.save();
3635
- ctx.strokeStyle = labelBorderColor;
3636
- ctx.lineWidth = labelBorderWidth;
3637
- ctx.setLineDash([]);
3638
- strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
3639
- ctx.restore();
3640
- }
3641
- if (buttonBorderWidth > 0) {
3642
- ctx.save();
3643
- ctx.strokeStyle = labelBorderColor;
3644
- ctx.lineWidth = buttonBorderWidth;
3645
- ctx.setLineDash([]);
3646
- if (crosshair.priceActionButtonRounded) {
3647
- ctx.beginPath();
3648
- ctx.arc(
3649
- buttonCenterX,
3650
- buttonCenterY,
3651
- Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
3652
- 0,
3653
- Math.PI * 2
3654
- );
3655
- ctx.stroke();
3656
- } else {
3657
- strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
3658
- }
3659
- ctx.restore();
3660
- }
3661
- if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
3662
- const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
3663
- const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
3664
- ctx.save();
3665
- ctx.strokeStyle = labelTextColor;
3666
- ctx.lineWidth = plusThickness;
3667
- ctx.lineCap = "round";
3668
- ctx.setLineDash([]);
4005
+ fillRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
4006
+ if (labelBorderWidth > 0) {
4007
+ ctx.save();
4008
+ ctx.strokeStyle = labelBorderColor;
4009
+ ctx.lineWidth = labelBorderWidth;
4010
+ ctx.setLineDash([]);
4011
+ strokeRoundedRect(Math.round(containerX), Math.round(containerY), containerWidth, labelHeight, containerRadius);
4012
+ ctx.restore();
4013
+ }
4014
+ if (buttonBorderWidth > 0) {
4015
+ ctx.save();
4016
+ ctx.strokeStyle = labelBorderColor;
4017
+ ctx.lineWidth = buttonBorderWidth;
4018
+ ctx.setLineDash([]);
4019
+ if (crosshair.priceActionButtonRounded) {
3669
4020
  ctx.beginPath();
3670
- ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
3671
- ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
3672
- ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
3673
- ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
3674
- ctx.stroke();
3675
- ctx.restore();
3676
- } else {
3677
- drawText(
3678
- crosshair.priceActionButtonText,
4021
+ ctx.arc(
3679
4022
  buttonCenterX,
3680
4023
  buttonCenterY,
3681
- "center",
3682
- "middle",
3683
- labelTextColor
4024
+ Math.max(1, buttonSize / 2 - buttonBorderWidth / 2),
4025
+ 0,
4026
+ Math.PI * 2
3684
4027
  );
4028
+ ctx.stroke();
4029
+ } else {
4030
+ strokeRoundedRect(Math.round(buttonX), Math.round(buttonY), buttonSize, buttonSize, buttonRadius);
3685
4031
  }
3686
- crosshairPriceActionRegion = {
3687
- x: containerX,
3688
- y: containerY,
3689
- width: containerWidth,
3690
- height: labelHeight,
3691
- price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
3692
- };
4032
+ ctx.restore();
3693
4033
  }
3694
- }
3695
- if (crosshair.showTimeLabel) {
3696
- const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
3697
- const hoverIndex = Math.round(xStart + ratio * xSpan - 0.5);
3698
- const hoverTime = getTimeForIndex(hoverIndex);
3699
- if (hoverTime) {
3700
- const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
3701
- const timeWidth = Math.ceil(ctx.measureText(timeText).width) + labelPaddingX * 2;
3702
- const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
3703
- const timeY = fullChartBottom + 1;
3704
- const timeHeight = Math.max(labelHeight, Math.floor(height - timeY));
3705
- ctx.fillStyle = labelBackground;
3706
- fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, timeHeight, labelRadius);
3707
- strokeCrosshairLabel(timeX, timeY, timeWidth, timeHeight);
3708
- drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
4034
+ if (crosshair.priceActionButtonIcon !== "text" && crosshair.priceActionButtonText === "+") {
4035
+ const plusHalf = Math.max(3, Math.round(buttonSize * 0.22));
4036
+ const plusThickness = crosshair.priceActionButtonIcon === "plusThin" ? Math.max(1, Math.round(buttonSize * 0.07)) : Math.max(1, Math.round(buttonSize * 0.1));
4037
+ ctx.save();
4038
+ ctx.strokeStyle = labelTextColor;
4039
+ ctx.lineWidth = plusThickness;
4040
+ ctx.lineCap = "round";
4041
+ ctx.setLineDash([]);
4042
+ ctx.beginPath();
4043
+ ctx.moveTo(buttonCenterX - plusHalf, buttonCenterY);
4044
+ ctx.lineTo(buttonCenterX + plusHalf, buttonCenterY);
4045
+ ctx.moveTo(buttonCenterX, buttonCenterY - plusHalf);
4046
+ ctx.lineTo(buttonCenterX, buttonCenterY + plusHalf);
4047
+ ctx.stroke();
4048
+ ctx.restore();
4049
+ } else {
4050
+ drawText(
4051
+ crosshair.priceActionButtonText,
4052
+ buttonCenterX,
4053
+ buttonCenterY,
4054
+ "center",
4055
+ "middle",
4056
+ labelTextColor
4057
+ );
3709
4058
  }
4059
+ crosshairPriceActionRegion = {
4060
+ x: containerX,
4061
+ y: containerY,
4062
+ width: containerWidth,
4063
+ height: labelHeight,
4064
+ price: Number(hoverPrice.toFixed(mergedOptions.priceDecimals))
4065
+ };
4066
+ }
4067
+ }
4068
+ if (crosshair.showTimeLabel) {
4069
+ const ratio = clamp((cx - chartLeft) / chartWidth, 0, 1);
4070
+ const hoverIndex = Math.round(xStart + ratio * xSpan2 - 0.5);
4071
+ const hoverTime = getTimeForIndex(hoverIndex);
4072
+ if (hoverTime) {
4073
+ const timeText = formatHoverTimeLabel(hoverTime, crosshair.timeLabelFormat);
4074
+ const timeWidth = Math.ceil(measureTextWidth(timeText)) + labelPaddingX * 2;
4075
+ const timeX = clamp(cx - timeWidth / 2, chartLeft, chartRight - timeWidth);
4076
+ const timeY = fullChartBottom + 1;
4077
+ const timeHeight = Math.max(labelHeight, Math.floor(height - timeY));
4078
+ ctx.fillStyle = labelBackground;
4079
+ fillRoundedRect(Math.round(timeX), Math.round(timeY), timeWidth, timeHeight, labelRadius);
4080
+ strokeCrosshairLabel(timeX, timeY, timeWidth, timeHeight);
4081
+ drawText(timeText, timeX + labelPaddingX, timeY + timeHeight / 2, "left", "middle", labelTextColor);
3710
4082
  }
3711
4083
  }
3712
4084
  };
4085
+ const drawOverlayOnly = () => {
4086
+ if (!baseCtx || !overlayLayout || baseCanvas.width !== canvas.width || baseCanvas.height !== canvas.height || baseCanvas.width === 0) {
4087
+ draw({ updateAutoScale: false });
4088
+ return;
4089
+ }
4090
+ const pixelRatio = getPixelRatio();
4091
+ ctx.setTransform(1, 0, 0, 1, 0, 0);
4092
+ ctx.drawImage(baseCanvas, 0, 0);
4093
+ ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
4094
+ paintCrosshairOverlay();
4095
+ };
3713
4096
  const zoomX = (factor, anchorX) => {
3714
4097
  if (!drawState || data.length === 0) {
3715
4098
  return;
@@ -3799,6 +4182,59 @@ function createChart(element, options = {}) {
3799
4182
  }
3800
4183
  scheduleDraw();
3801
4184
  };
4185
+ let kineticRafId = null;
4186
+ const panVelocitySamples = [];
4187
+ const cancelKineticPan = () => {
4188
+ if (kineticRafId !== null) {
4189
+ cancelAnimationFrame(kineticRafId);
4190
+ kineticRafId = null;
4191
+ }
4192
+ };
4193
+ const recordPanVelocitySample = (x) => {
4194
+ const now = performance.now();
4195
+ panVelocitySamples.push({ t: now, x });
4196
+ while (panVelocitySamples.length > 0 && now - panVelocitySamples[0].t > 120) {
4197
+ panVelocitySamples.shift();
4198
+ }
4199
+ };
4200
+ const startKineticPan = (initialVelocityPxPerMs) => {
4201
+ cancelKineticPan();
4202
+ let velocity = initialVelocityPxPerMs;
4203
+ let lastTime = performance.now();
4204
+ const stepFn = (now) => {
4205
+ kineticRafId = null;
4206
+ const dt = Math.min(64, Math.max(1, now - lastTime));
4207
+ lastTime = now;
4208
+ const centerBefore = xCenter;
4209
+ pan(velocity * dt, 0, true, false);
4210
+ velocity *= Math.exp(-dt / 325);
4211
+ if (Math.abs(velocity) < 0.02 || xCenter === centerBefore) {
4212
+ return;
4213
+ }
4214
+ kineticRafId = requestAnimationFrame(stepFn);
4215
+ };
4216
+ kineticRafId = requestAnimationFrame(stepFn);
4217
+ };
4218
+ const maybeStartKineticPan = (pointerType) => {
4219
+ const config = mergedOptions.kineticScroll ?? DEFAULT_OPTIONS.kineticScroll;
4220
+ const enabled = pointerType === "touch" || pointerType === "pen" ? config?.touch !== false : config?.mouse === true;
4221
+ if (!enabled || panVelocitySamples.length < 2) {
4222
+ panVelocitySamples.length = 0;
4223
+ return;
4224
+ }
4225
+ const first = panVelocitySamples[0];
4226
+ const last = panVelocitySamples[panVelocitySamples.length - 1];
4227
+ panVelocitySamples.length = 0;
4228
+ const elapsed = last.t - first.t;
4229
+ if (elapsed <= 0 || performance.now() - last.t > 100) {
4230
+ return;
4231
+ }
4232
+ const velocity = (last.x - first.x) / elapsed;
4233
+ if (Math.abs(velocity) < 0.1) {
4234
+ return;
4235
+ }
4236
+ startKineticPan(clamp(velocity, -4, 4));
4237
+ };
3802
4238
  const resetYViewport = () => {
3803
4239
  yMinOverride = null;
3804
4240
  yMaxOverride = null;
@@ -3858,6 +4294,7 @@ function createChart(element, options = {}) {
3858
4294
  scheduleDraw();
3859
4295
  };
3860
4296
  const resetViewport = () => {
4297
+ cancelKineticPan();
3861
4298
  fitXViewport();
3862
4299
  resetYViewport();
3863
4300
  updateFollowLatest(true);
@@ -3867,6 +4304,7 @@ function createChart(element, options = {}) {
3867
4304
  const isFollowingLatest = () => followLatest;
3868
4305
  const setFollowingLatest = (follow) => {
3869
4306
  if (follow && data.length > 0) {
4307
+ cancelKineticPan();
3870
4308
  xCenter = data.length - xSpan / 2 + rightEdgePaddingBars;
3871
4309
  clampXViewport();
3872
4310
  updateFollowLatest(true);
@@ -4236,7 +4674,7 @@ function createChart(element, options = {}) {
4236
4674
  const prevFont = ctx.font;
4237
4675
  ctx.font = `500 ${fontSize}px ${mergedOptions.fontFamily}`;
4238
4676
  const lines = (drawing.label ?? "").split("\n");
4239
- const textW = Math.max(1, ...lines.map((line) => ctx.measureText(line).width));
4677
+ const textW = Math.max(1, ...lines.map((line) => measureTextWidth(line)));
4240
4678
  ctx.font = prevFont;
4241
4679
  const isNote = drawing.type === "note";
4242
4680
  const padX = isNote ? 8 : 2;
@@ -4505,7 +4943,9 @@ function createChart(element, options = {}) {
4505
4943
  points: [point, normalizeDrawingPoint(point.index + width2, point.price - priceOffset)],
4506
4944
  color: defaults.color ?? "#2962ff",
4507
4945
  style: defaults.style ?? "solid",
4508
- width: defaults.width ?? 1
4946
+ width: defaults.width ?? 1,
4947
+ // pointValue enables the dollar-P&L line in the measure label.
4948
+ ...defaults.pointValue === void 0 ? {} : { pointValue: defaults.pointValue }
4509
4949
  })
4510
4950
  );
4511
4951
  emitDrawingsChange();
@@ -4707,16 +5147,24 @@ function createChart(element, options = {}) {
4707
5147
  emitViewportChange();
4708
5148
  scheduleDraw();
4709
5149
  };
5150
+ const capturePointer = (pointerId) => {
5151
+ try {
5152
+ canvas.setPointerCapture(pointerId);
5153
+ } catch {
5154
+ }
5155
+ };
4710
5156
  const onPointerDown = (event) => {
4711
5157
  if (event.pointerType === "touch" || event.pointerType === "pen") {
4712
5158
  event.preventDefault();
4713
5159
  }
5160
+ cancelKineticPan();
5161
+ panVelocitySamples.length = 0;
4714
5162
  magnetModifierActive = event.metaKey || event.ctrlKey;
4715
5163
  shiftKeyActive = event.shiftKey;
4716
5164
  const point = getCanvasPoint(event);
4717
5165
  if (event.pointerType === "touch") {
4718
5166
  touchPointers.set(event.pointerId, point);
4719
- canvas.setPointerCapture(event.pointerId);
5167
+ capturePointer(event.pointerId);
4720
5168
  const touchPair = getTouchPair();
4721
5169
  if (touchPair) {
4722
5170
  beginPinchZoom(touchPair[0], touchPair[1]);
@@ -4746,7 +5194,7 @@ function createChart(element, options = {}) {
4746
5194
  lastPrice: startPrice,
4747
5195
  moved: false
4748
5196
  };
4749
- canvas.setPointerCapture(event.pointerId);
5197
+ capturePointer(event.pointerId);
4750
5198
  canvas.style.cursor = "ns-resize";
4751
5199
  setCrosshairPoint(null);
4752
5200
  return;
@@ -4767,7 +5215,7 @@ function createChart(element, options = {}) {
4767
5215
  startPrice: orderDragRegion.price,
4768
5216
  lastPrice: orderDragRegion.price
4769
5217
  };
4770
- canvas.setPointerCapture(event.pointerId);
5218
+ capturePointer(event.pointerId);
4771
5219
  canvas.style.cursor = "ns-resize";
4772
5220
  setCrosshairPoint(null);
4773
5221
  return;
@@ -4799,7 +5247,7 @@ function createChart(element, options = {}) {
4799
5247
  startPoints: drawingHit.drawing.points.map((drawingPoint) => ({ ...drawingPoint }))
4800
5248
  };
4801
5249
  activePointerId = event.pointerId;
4802
- canvas.setPointerCapture(event.pointerId);
5250
+ capturePointer(event.pointerId);
4803
5251
  }
4804
5252
  }
4805
5253
  setCrosshairPoint(null);
@@ -4827,7 +5275,7 @@ function createChart(element, options = {}) {
4827
5275
  };
4828
5276
  lastPointerX = point.x;
4829
5277
  lastPointerY = point.y;
4830
- canvas.setPointerCapture(event.pointerId);
5278
+ capturePointer(event.pointerId);
4831
5279
  if (crosshairDrag) {
4832
5280
  canvas.style.cursor = "crosshair";
4833
5281
  setCrosshairPoint(point);
@@ -5013,6 +5461,7 @@ function createChart(element, options = {}) {
5013
5461
  const deltaY = point.y - lastPointerY;
5014
5462
  if (dragMode === "plot") {
5015
5463
  canvas.style.cursor = "grabbing";
5464
+ recordPanVelocitySample(point.x);
5016
5465
  pan(deltaX, deltaY, true, true);
5017
5466
  setCrosshairPoint(null);
5018
5467
  } else if (dragMode === "x-axis") {
@@ -5086,10 +5535,16 @@ function createChart(element, options = {}) {
5086
5535
  drawingDragState = null;
5087
5536
  emitDrawingsChange();
5088
5537
  }
5538
+ const endedPlotDrag = isDragging && dragMode === "plot";
5089
5539
  isDragging = false;
5090
5540
  dragMode = null;
5091
5541
  activePointerId = null;
5092
5542
  canvas.style.cursor = "default";
5543
+ if (endedPlotDrag) {
5544
+ maybeStartKineticPan(event?.pointerType);
5545
+ } else {
5546
+ panVelocitySamples.length = 0;
5547
+ }
5093
5548
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
5094
5549
  if (pointerDownInfo.crosshairDrag) {
5095
5550
  const point = getCanvasPoint(event);
@@ -5127,6 +5582,7 @@ function createChart(element, options = {}) {
5127
5582
  if (!drawState) {
5128
5583
  return;
5129
5584
  }
5585
+ cancelKineticPan();
5130
5586
  const point = getCanvasPoint(event);
5131
5587
  const region = getHitRegion(point.x, point.y);
5132
5588
  if (region === "outside") {
@@ -5226,6 +5682,7 @@ function createChart(element, options = {}) {
5226
5682
  width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
5227
5683
  height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
5228
5684
  mergedOptions = { ...mergedOptions, width, height };
5685
+ refreshResolvedOptions();
5229
5686
  resetRightMarginCache();
5230
5687
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
5231
5688
  doubleClickAction = mergedOptions.doubleClickAction;
@@ -5570,6 +6027,7 @@ function createChart(element, options = {}) {
5570
6027
  drawingHoverHandler = handler;
5571
6028
  };
5572
6029
  const destroy = () => {
6030
+ cancelKineticPan();
5573
6031
  if (smoothingRafId !== null) {
5574
6032
  cancelAnimationFrame(smoothingRafId);
5575
6033
  smoothingRafId = null;