hyperprop-charting-library 0.1.62 → 0.1.64

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.
@@ -1065,9 +1065,14 @@ function createChart(element, options = {}) {
1065
1065
  }
1066
1066
  canvas.style.display = "block";
1067
1067
  canvas.style.touchAction = "none";
1068
+ canvas.style.userSelect = "none";
1069
+ canvas.style.setProperty("-webkit-user-select", "none");
1070
+ canvas.style.setProperty("-webkit-touch-callout", "none");
1071
+ canvas.setAttribute("draggable", "false");
1068
1072
  element.innerHTML = "";
1069
1073
  element.appendChild(canvas);
1070
1074
  const margin = { top: 16, right: 72, bottom: 34, left: 12 };
1075
+ let cachedRightMargin = margin.right;
1071
1076
  const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
1072
1077
  const maxVisibleBars = Math.max(minVisibleBars, Math.floor(mergedOptions.maxVisibleBars));
1073
1078
  const maxPanBars = Math.max(0, Math.floor(mergedOptions.maxPanBars));
@@ -1289,14 +1294,34 @@ function createChart(element, options = {}) {
1289
1294
  const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
1290
1295
  return `${integerPart}${decimalPart}`;
1291
1296
  };
1297
+ const getMeasuredLabelWidth = (text, paddingX) => {
1298
+ return Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1299
+ };
1300
+ const getStabilizedNumericLabelWidth = (text, paddingX) => {
1301
+ const measured = getMeasuredLabelWidth(text, paddingX);
1302
+ if (!mergedOptions.stabilizePriceLabels || !/\d/.test(text)) {
1303
+ return measured;
1304
+ }
1305
+ let stableWidth = measured;
1306
+ for (const digit of ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]) {
1307
+ stableWidth = Math.max(stableWidth, getMeasuredLabelWidth(text.replace(/\d/g, digit), paddingX));
1308
+ }
1309
+ return stableWidth;
1310
+ };
1292
1311
  const getPriceLabelWidth = (priceText, paddingX) => {
1293
- const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
1312
+ const measured = getStabilizedNumericLabelWidth(priceText, paddingX);
1294
1313
  if (!mergedOptions.stabilizePriceLabels) {
1295
1314
  return measured;
1296
1315
  }
1297
- const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
1316
+ const templateWidth = getStabilizedNumericLabelWidth(getStabilizedPriceTemplate(), paddingX);
1298
1317
  return Math.max(measured, templateWidth);
1299
1318
  };
1319
+ const getStableLabelWidth = (text, paddingX) => {
1320
+ return getStabilizedNumericLabelWidth(text, paddingX);
1321
+ };
1322
+ const resetRightMarginCache = () => {
1323
+ cachedRightMargin = margin.right;
1324
+ };
1300
1325
  const parseData = (nextData) => {
1301
1326
  const dedupedByTime = /* @__PURE__ */ new Map();
1302
1327
  for (const point of nextData) {
@@ -1822,22 +1847,33 @@ function createChart(element, options = {}) {
1822
1847
  const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
1823
1848
  const estimateRightMargin = () => {
1824
1849
  const paddingX = Math.max(4, labels.labelPaddingX);
1825
- const measure = (text) => Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1826
1850
  let required = margin.right - 1;
1851
+ const includeWidth = (contentWidth) => {
1852
+ if (Number.isFinite(contentWidth)) {
1853
+ required = Math.max(required, contentWidth);
1854
+ }
1855
+ };
1827
1856
  const include = (text) => {
1828
1857
  const normalized = text?.trim();
1829
1858
  if (normalized) {
1830
- required = Math.max(required, measure(normalized));
1859
+ includeWidth(getStableLabelWidth(normalized, paddingX));
1860
+ }
1861
+ };
1862
+ const includePrice = (price) => {
1863
+ if (Number.isFinite(price)) {
1864
+ includeWidth(getPriceLabelWidth(formatPrice(price), paddingX));
1831
1865
  }
1832
1866
  };
1833
1867
  const ticker2 = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1834
1868
  const lastPoint2 = data[data.length - 1];
1835
1869
  if ((ticker2.visible ?? true) && labels.showLastPrice && lastPoint2) {
1836
- include(formatPrice(lastPoint2.c));
1870
+ const tickerPrice2 = ticker2.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint2.c;
1871
+ includePrice(tickerPrice2);
1837
1872
  if (ticker2.labelSubtext) include(String(ticker2.labelSubtext));
1838
1873
  for (const subtext of ticker2.labelSubtexts ?? []) {
1839
1874
  include(String(subtext));
1840
1875
  }
1876
+ if (ticker2.showCountdownInLabel) include("88:88");
1841
1877
  }
1842
1878
  if (labels.showSymbolName) {
1843
1879
  include(labels.symbolName);
@@ -1859,18 +1895,24 @@ function createChart(element, options = {}) {
1859
1895
  for (const line of priceLines) {
1860
1896
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1861
1897
  if (mergedLine.visible && Number.isFinite(mergedLine.price)) {
1862
- include(mergedLine.label ?? formatPrice(mergedLine.price));
1898
+ if (mergedLine.label) {
1899
+ include(mergedLine.label);
1900
+ } else {
1901
+ includePrice(mergedLine.price);
1902
+ }
1863
1903
  }
1864
1904
  }
1865
1905
  for (const line of orderLines) {
1866
1906
  const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
1867
1907
  const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
1868
1908
  if (mergedLine.visible && Number.isFinite(renderPrice)) {
1869
- include(formatPrice(renderPrice));
1909
+ includePrice(renderPrice);
1870
1910
  }
1871
1911
  }
1872
1912
  const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
1873
- return Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
1913
+ const nextRightMargin = Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
1914
+ cachedRightMargin = Math.max(cachedRightMargin, nextRightMargin);
1915
+ return Math.min(maxRightMargin, cachedRightMargin);
1874
1916
  };
1875
1917
  const rightMargin = estimateRightMargin();
1876
1918
  const chartLeft = margin.left;
@@ -3517,6 +3559,9 @@ function createChart(element, options = {}) {
3517
3559
  draw();
3518
3560
  };
3519
3561
  const onPointerDown = (event) => {
3562
+ if (event.pointerType === "touch" || event.pointerType === "pen") {
3563
+ event.preventDefault();
3564
+ }
3520
3565
  const point = getCanvasPoint(event);
3521
3566
  if (event.pointerType === "touch") {
3522
3567
  touchPointers.set(event.pointerId, point);
@@ -3615,12 +3660,29 @@ function createChart(element, options = {}) {
3615
3660
  isDragging = true;
3616
3661
  dragMode = region;
3617
3662
  activePointerId = event.pointerId;
3618
- pointerDownInfo = { pointerId: event.pointerId, x: point.x, y: point.y, region, moved: false };
3663
+ const crosshairDrag = (event.pointerType === "touch" || event.pointerType === "pen") && region === "plot";
3664
+ pointerDownInfo = {
3665
+ pointerId: event.pointerId,
3666
+ pointerType: event.pointerType,
3667
+ x: point.x,
3668
+ y: point.y,
3669
+ region,
3670
+ moved: false,
3671
+ crosshairDrag
3672
+ };
3619
3673
  lastPointerX = point.x;
3620
3674
  lastPointerY = point.y;
3621
3675
  canvas.setPointerCapture(event.pointerId);
3676
+ if (crosshairDrag) {
3677
+ canvas.style.cursor = "crosshair";
3678
+ setCrosshairPoint(point);
3679
+ emitCrosshairMove(point.x, point.y, "plot");
3680
+ }
3622
3681
  };
3623
3682
  const onPointerMove = (event) => {
3683
+ if (event.pointerType === "touch" || event.pointerType === "pen") {
3684
+ event.preventDefault();
3685
+ }
3624
3686
  const point = getCanvasPoint(event);
3625
3687
  if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
3626
3688
  touchPointers.set(event.pointerId, point);
@@ -3643,6 +3705,16 @@ function createChart(element, options = {}) {
3643
3705
  pointerDownInfo.moved = true;
3644
3706
  }
3645
3707
  }
3708
+ if (pointerDownInfo?.crosshairDrag && pointerDownInfo.pointerId === event.pointerId) {
3709
+ if (getHitRegion(point.x, point.y) === "plot") {
3710
+ canvas.style.cursor = "crosshair";
3711
+ setCrosshairPoint(point);
3712
+ emitCrosshairMove(point.x, point.y, "plot");
3713
+ lastPointerX = point.x;
3714
+ lastPointerY = point.y;
3715
+ }
3716
+ return;
3717
+ }
3646
3718
  if (drawingDragState) {
3647
3719
  if (activePointerId !== null && event.pointerId !== activePointerId) {
3648
3720
  return;
@@ -3856,7 +3928,13 @@ function createChart(element, options = {}) {
3856
3928
  activePointerId = null;
3857
3929
  canvas.style.cursor = "default";
3858
3930
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
3859
- if (!pointerDownInfo.moved) {
3931
+ if (pointerDownInfo.crosshairDrag) {
3932
+ const point = getCanvasPoint(event);
3933
+ if (getHitRegion(point.x, point.y) === "plot") {
3934
+ setCrosshairPoint(point);
3935
+ emitCrosshairMove(point.x, point.y, "plot");
3936
+ }
3937
+ } else if (!pointerDownInfo.moved) {
3860
3938
  const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
3861
3939
  chartClickHandler?.({
3862
3940
  x: pointerDownInfo.x,
@@ -3903,6 +3981,7 @@ function createChart(element, options = {}) {
3903
3981
  zoomX(factor, point.x);
3904
3982
  };
3905
3983
  const onDoubleClick = (event) => {
3984
+ event.preventDefault();
3906
3985
  if (!doubleClickEnabled) {
3907
3986
  return;
3908
3987
  }
@@ -3928,6 +4007,9 @@ function createChart(element, options = {}) {
3928
4007
  }
3929
4008
  resetViewport();
3930
4009
  };
4010
+ const onContextMenu = (event) => {
4011
+ event.preventDefault();
4012
+ };
3931
4013
  canvas.addEventListener("pointerdown", onPointerDown);
3932
4014
  canvas.addEventListener("pointermove", onPointerMove);
3933
4015
  canvas.addEventListener("pointerup", endPointerDrag);
@@ -3935,6 +4017,7 @@ function createChart(element, options = {}) {
3935
4017
  canvas.addEventListener("pointerleave", endPointerDrag);
3936
4018
  canvas.addEventListener("wheel", onWheel, { passive: false });
3937
4019
  canvas.addEventListener("dblclick", onDoubleClick);
4020
+ canvas.addEventListener("contextmenu", onContextMenu);
3938
4021
  const updateOptions = (nextOptions) => {
3939
4022
  const wasTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
3940
4023
  const previousWidth = width;
@@ -3943,6 +4026,7 @@ function createChart(element, options = {}) {
3943
4026
  width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
3944
4027
  height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
3945
4028
  mergedOptions = { ...mergedOptions, width, height };
4029
+ resetRightMarginCache();
3946
4030
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
3947
4031
  doubleClickAction = mergedOptions.doubleClickAction;
3948
4032
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
@@ -3969,6 +4053,7 @@ function createChart(element, options = {}) {
3969
4053
  height = nextHeight;
3970
4054
  }
3971
4055
  mergedOptions = { ...mergedOptions, width, height };
4056
+ resetRightMarginCache();
3972
4057
  draw();
3973
4058
  };
3974
4059
  const setData = (nextData) => {
@@ -3985,6 +4070,7 @@ function createChart(element, options = {}) {
3985
4070
  tickerPriceTarget = null;
3986
4071
  smoothedTickerVolume = null;
3987
4072
  tickerVolumeTarget = null;
4073
+ resetRightMarginCache();
3988
4074
  resetYViewport();
3989
4075
  draw();
3990
4076
  return;
@@ -4018,6 +4104,7 @@ function createChart(element, options = {}) {
4018
4104
  ...line,
4019
4105
  id: line.id ?? `line-${index + 1}`
4020
4106
  }));
4107
+ resetRightMarginCache();
4021
4108
  draw();
4022
4109
  };
4023
4110
  const addPriceLine = (line) => {
@@ -4028,6 +4115,7 @@ function createChart(element, options = {}) {
4028
4115
  };
4029
4116
  const removePriceLine = (id) => {
4030
4117
  priceLines = priceLines.filter((line) => line.id !== id);
4118
+ resetRightMarginCache();
4031
4119
  draw();
4032
4120
  };
4033
4121
  const setOrderLines = (lines) => {
@@ -4035,6 +4123,7 @@ function createChart(element, options = {}) {
4035
4123
  ...line,
4036
4124
  id: line.id ?? `order-${index + 1}`
4037
4125
  }));
4126
+ resetRightMarginCache();
4038
4127
  const activeIds = new Set(orderLines.map((line) => line.id));
4039
4128
  for (const id of Array.from(orderWidgetWidthById.keys())) {
4040
4129
  if (!activeIds.has(id)) {
@@ -4062,6 +4151,7 @@ function createChart(element, options = {}) {
4062
4151
  orderLines = orderLines.filter((line) => line.id !== id);
4063
4152
  orderWidgetWidthById.delete(id);
4064
4153
  orderPriceTagWidthById.delete(id);
4154
+ resetRightMarginCache();
4065
4155
  draw();
4066
4156
  };
4067
4157
  const onOrderAction = (handler) => {
@@ -4225,6 +4315,7 @@ function createChart(element, options = {}) {
4225
4315
  canvas.removeEventListener("pointerleave", endPointerDrag);
4226
4316
  canvas.removeEventListener("wheel", onWheel);
4227
4317
  canvas.removeEventListener("dblclick", onDoubleClick);
4318
+ canvas.removeEventListener("contextmenu", onContextMenu);
4228
4319
  element.innerHTML = "";
4229
4320
  };
4230
4321
  draw();
@@ -1041,9 +1041,14 @@ function createChart(element, options = {}) {
1041
1041
  }
1042
1042
  canvas.style.display = "block";
1043
1043
  canvas.style.touchAction = "none";
1044
+ canvas.style.userSelect = "none";
1045
+ canvas.style.setProperty("-webkit-user-select", "none");
1046
+ canvas.style.setProperty("-webkit-touch-callout", "none");
1047
+ canvas.setAttribute("draggable", "false");
1044
1048
  element.innerHTML = "";
1045
1049
  element.appendChild(canvas);
1046
1050
  const margin = { top: 16, right: 72, bottom: 34, left: 12 };
1051
+ let cachedRightMargin = margin.right;
1047
1052
  const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
1048
1053
  const maxVisibleBars = Math.max(minVisibleBars, Math.floor(mergedOptions.maxVisibleBars));
1049
1054
  const maxPanBars = Math.max(0, Math.floor(mergedOptions.maxPanBars));
@@ -1265,14 +1270,34 @@ function createChart(element, options = {}) {
1265
1270
  const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
1266
1271
  return `${integerPart}${decimalPart}`;
1267
1272
  };
1273
+ const getMeasuredLabelWidth = (text, paddingX) => {
1274
+ return Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1275
+ };
1276
+ const getStabilizedNumericLabelWidth = (text, paddingX) => {
1277
+ const measured = getMeasuredLabelWidth(text, paddingX);
1278
+ if (!mergedOptions.stabilizePriceLabels || !/\d/.test(text)) {
1279
+ return measured;
1280
+ }
1281
+ let stableWidth = measured;
1282
+ for (const digit of ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]) {
1283
+ stableWidth = Math.max(stableWidth, getMeasuredLabelWidth(text.replace(/\d/g, digit), paddingX));
1284
+ }
1285
+ return stableWidth;
1286
+ };
1268
1287
  const getPriceLabelWidth = (priceText, paddingX) => {
1269
- const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
1288
+ const measured = getStabilizedNumericLabelWidth(priceText, paddingX);
1270
1289
  if (!mergedOptions.stabilizePriceLabels) {
1271
1290
  return measured;
1272
1291
  }
1273
- const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
1292
+ const templateWidth = getStabilizedNumericLabelWidth(getStabilizedPriceTemplate(), paddingX);
1274
1293
  return Math.max(measured, templateWidth);
1275
1294
  };
1295
+ const getStableLabelWidth = (text, paddingX) => {
1296
+ return getStabilizedNumericLabelWidth(text, paddingX);
1297
+ };
1298
+ const resetRightMarginCache = () => {
1299
+ cachedRightMargin = margin.right;
1300
+ };
1276
1301
  const parseData = (nextData) => {
1277
1302
  const dedupedByTime = /* @__PURE__ */ new Map();
1278
1303
  for (const point of nextData) {
@@ -1798,22 +1823,33 @@ function createChart(element, options = {}) {
1798
1823
  const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
1799
1824
  const estimateRightMargin = () => {
1800
1825
  const paddingX = Math.max(4, labels.labelPaddingX);
1801
- const measure = (text) => Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1802
1826
  let required = margin.right - 1;
1827
+ const includeWidth = (contentWidth) => {
1828
+ if (Number.isFinite(contentWidth)) {
1829
+ required = Math.max(required, contentWidth);
1830
+ }
1831
+ };
1803
1832
  const include = (text) => {
1804
1833
  const normalized = text?.trim();
1805
1834
  if (normalized) {
1806
- required = Math.max(required, measure(normalized));
1835
+ includeWidth(getStableLabelWidth(normalized, paddingX));
1836
+ }
1837
+ };
1838
+ const includePrice = (price) => {
1839
+ if (Number.isFinite(price)) {
1840
+ includeWidth(getPriceLabelWidth(formatPrice(price), paddingX));
1807
1841
  }
1808
1842
  };
1809
1843
  const ticker2 = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1810
1844
  const lastPoint2 = data[data.length - 1];
1811
1845
  if ((ticker2.visible ?? true) && labels.showLastPrice && lastPoint2) {
1812
- include(formatPrice(lastPoint2.c));
1846
+ const tickerPrice2 = ticker2.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint2.c;
1847
+ includePrice(tickerPrice2);
1813
1848
  if (ticker2.labelSubtext) include(String(ticker2.labelSubtext));
1814
1849
  for (const subtext of ticker2.labelSubtexts ?? []) {
1815
1850
  include(String(subtext));
1816
1851
  }
1852
+ if (ticker2.showCountdownInLabel) include("88:88");
1817
1853
  }
1818
1854
  if (labels.showSymbolName) {
1819
1855
  include(labels.symbolName);
@@ -1835,18 +1871,24 @@ function createChart(element, options = {}) {
1835
1871
  for (const line of priceLines) {
1836
1872
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1837
1873
  if (mergedLine.visible && Number.isFinite(mergedLine.price)) {
1838
- include(mergedLine.label ?? formatPrice(mergedLine.price));
1874
+ if (mergedLine.label) {
1875
+ include(mergedLine.label);
1876
+ } else {
1877
+ includePrice(mergedLine.price);
1878
+ }
1839
1879
  }
1840
1880
  }
1841
1881
  for (const line of orderLines) {
1842
1882
  const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
1843
1883
  const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
1844
1884
  if (mergedLine.visible && Number.isFinite(renderPrice)) {
1845
- include(formatPrice(renderPrice));
1885
+ includePrice(renderPrice);
1846
1886
  }
1847
1887
  }
1848
1888
  const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
1849
- return Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
1889
+ const nextRightMargin = Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
1890
+ cachedRightMargin = Math.max(cachedRightMargin, nextRightMargin);
1891
+ return Math.min(maxRightMargin, cachedRightMargin);
1850
1892
  };
1851
1893
  const rightMargin = estimateRightMargin();
1852
1894
  const chartLeft = margin.left;
@@ -3493,6 +3535,9 @@ function createChart(element, options = {}) {
3493
3535
  draw();
3494
3536
  };
3495
3537
  const onPointerDown = (event) => {
3538
+ if (event.pointerType === "touch" || event.pointerType === "pen") {
3539
+ event.preventDefault();
3540
+ }
3496
3541
  const point = getCanvasPoint(event);
3497
3542
  if (event.pointerType === "touch") {
3498
3543
  touchPointers.set(event.pointerId, point);
@@ -3591,12 +3636,29 @@ function createChart(element, options = {}) {
3591
3636
  isDragging = true;
3592
3637
  dragMode = region;
3593
3638
  activePointerId = event.pointerId;
3594
- pointerDownInfo = { pointerId: event.pointerId, x: point.x, y: point.y, region, moved: false };
3639
+ const crosshairDrag = (event.pointerType === "touch" || event.pointerType === "pen") && region === "plot";
3640
+ pointerDownInfo = {
3641
+ pointerId: event.pointerId,
3642
+ pointerType: event.pointerType,
3643
+ x: point.x,
3644
+ y: point.y,
3645
+ region,
3646
+ moved: false,
3647
+ crosshairDrag
3648
+ };
3595
3649
  lastPointerX = point.x;
3596
3650
  lastPointerY = point.y;
3597
3651
  canvas.setPointerCapture(event.pointerId);
3652
+ if (crosshairDrag) {
3653
+ canvas.style.cursor = "crosshair";
3654
+ setCrosshairPoint(point);
3655
+ emitCrosshairMove(point.x, point.y, "plot");
3656
+ }
3598
3657
  };
3599
3658
  const onPointerMove = (event) => {
3659
+ if (event.pointerType === "touch" || event.pointerType === "pen") {
3660
+ event.preventDefault();
3661
+ }
3600
3662
  const point = getCanvasPoint(event);
3601
3663
  if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
3602
3664
  touchPointers.set(event.pointerId, point);
@@ -3619,6 +3681,16 @@ function createChart(element, options = {}) {
3619
3681
  pointerDownInfo.moved = true;
3620
3682
  }
3621
3683
  }
3684
+ if (pointerDownInfo?.crosshairDrag && pointerDownInfo.pointerId === event.pointerId) {
3685
+ if (getHitRegion(point.x, point.y) === "plot") {
3686
+ canvas.style.cursor = "crosshair";
3687
+ setCrosshairPoint(point);
3688
+ emitCrosshairMove(point.x, point.y, "plot");
3689
+ lastPointerX = point.x;
3690
+ lastPointerY = point.y;
3691
+ }
3692
+ return;
3693
+ }
3622
3694
  if (drawingDragState) {
3623
3695
  if (activePointerId !== null && event.pointerId !== activePointerId) {
3624
3696
  return;
@@ -3832,7 +3904,13 @@ function createChart(element, options = {}) {
3832
3904
  activePointerId = null;
3833
3905
  canvas.style.cursor = "default";
3834
3906
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
3835
- if (!pointerDownInfo.moved) {
3907
+ if (pointerDownInfo.crosshairDrag) {
3908
+ const point = getCanvasPoint(event);
3909
+ if (getHitRegion(point.x, point.y) === "plot") {
3910
+ setCrosshairPoint(point);
3911
+ emitCrosshairMove(point.x, point.y, "plot");
3912
+ }
3913
+ } else if (!pointerDownInfo.moved) {
3836
3914
  const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
3837
3915
  chartClickHandler?.({
3838
3916
  x: pointerDownInfo.x,
@@ -3879,6 +3957,7 @@ function createChart(element, options = {}) {
3879
3957
  zoomX(factor, point.x);
3880
3958
  };
3881
3959
  const onDoubleClick = (event) => {
3960
+ event.preventDefault();
3882
3961
  if (!doubleClickEnabled) {
3883
3962
  return;
3884
3963
  }
@@ -3904,6 +3983,9 @@ function createChart(element, options = {}) {
3904
3983
  }
3905
3984
  resetViewport();
3906
3985
  };
3986
+ const onContextMenu = (event) => {
3987
+ event.preventDefault();
3988
+ };
3907
3989
  canvas.addEventListener("pointerdown", onPointerDown);
3908
3990
  canvas.addEventListener("pointermove", onPointerMove);
3909
3991
  canvas.addEventListener("pointerup", endPointerDrag);
@@ -3911,6 +3993,7 @@ function createChart(element, options = {}) {
3911
3993
  canvas.addEventListener("pointerleave", endPointerDrag);
3912
3994
  canvas.addEventListener("wheel", onWheel, { passive: false });
3913
3995
  canvas.addEventListener("dblclick", onDoubleClick);
3996
+ canvas.addEventListener("contextmenu", onContextMenu);
3914
3997
  const updateOptions = (nextOptions) => {
3915
3998
  const wasTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
3916
3999
  const previousWidth = width;
@@ -3919,6 +4002,7 @@ function createChart(element, options = {}) {
3919
4002
  width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
3920
4003
  height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
3921
4004
  mergedOptions = { ...mergedOptions, width, height };
4005
+ resetRightMarginCache();
3922
4006
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
3923
4007
  doubleClickAction = mergedOptions.doubleClickAction;
3924
4008
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
@@ -3945,6 +4029,7 @@ function createChart(element, options = {}) {
3945
4029
  height = nextHeight;
3946
4030
  }
3947
4031
  mergedOptions = { ...mergedOptions, width, height };
4032
+ resetRightMarginCache();
3948
4033
  draw();
3949
4034
  };
3950
4035
  const setData = (nextData) => {
@@ -3961,6 +4046,7 @@ function createChart(element, options = {}) {
3961
4046
  tickerPriceTarget = null;
3962
4047
  smoothedTickerVolume = null;
3963
4048
  tickerVolumeTarget = null;
4049
+ resetRightMarginCache();
3964
4050
  resetYViewport();
3965
4051
  draw();
3966
4052
  return;
@@ -3994,6 +4080,7 @@ function createChart(element, options = {}) {
3994
4080
  ...line,
3995
4081
  id: line.id ?? `line-${index + 1}`
3996
4082
  }));
4083
+ resetRightMarginCache();
3997
4084
  draw();
3998
4085
  };
3999
4086
  const addPriceLine = (line) => {
@@ -4004,6 +4091,7 @@ function createChart(element, options = {}) {
4004
4091
  };
4005
4092
  const removePriceLine = (id) => {
4006
4093
  priceLines = priceLines.filter((line) => line.id !== id);
4094
+ resetRightMarginCache();
4007
4095
  draw();
4008
4096
  };
4009
4097
  const setOrderLines = (lines) => {
@@ -4011,6 +4099,7 @@ function createChart(element, options = {}) {
4011
4099
  ...line,
4012
4100
  id: line.id ?? `order-${index + 1}`
4013
4101
  }));
4102
+ resetRightMarginCache();
4014
4103
  const activeIds = new Set(orderLines.map((line) => line.id));
4015
4104
  for (const id of Array.from(orderWidgetWidthById.keys())) {
4016
4105
  if (!activeIds.has(id)) {
@@ -4038,6 +4127,7 @@ function createChart(element, options = {}) {
4038
4127
  orderLines = orderLines.filter((line) => line.id !== id);
4039
4128
  orderWidgetWidthById.delete(id);
4040
4129
  orderPriceTagWidthById.delete(id);
4130
+ resetRightMarginCache();
4041
4131
  draw();
4042
4132
  };
4043
4133
  const onOrderAction = (handler) => {
@@ -4201,6 +4291,7 @@ function createChart(element, options = {}) {
4201
4291
  canvas.removeEventListener("pointerleave", endPointerDrag);
4202
4292
  canvas.removeEventListener("wheel", onWheel);
4203
4293
  canvas.removeEventListener("dblclick", onDoubleClick);
4294
+ canvas.removeEventListener("contextmenu", onContextMenu);
4204
4295
  element.innerHTML = "";
4205
4296
  };
4206
4297
  draw();
package/dist/index.cjs CHANGED
@@ -1065,9 +1065,14 @@ function createChart(element, options = {}) {
1065
1065
  }
1066
1066
  canvas.style.display = "block";
1067
1067
  canvas.style.touchAction = "none";
1068
+ canvas.style.userSelect = "none";
1069
+ canvas.style.setProperty("-webkit-user-select", "none");
1070
+ canvas.style.setProperty("-webkit-touch-callout", "none");
1071
+ canvas.setAttribute("draggable", "false");
1068
1072
  element.innerHTML = "";
1069
1073
  element.appendChild(canvas);
1070
1074
  const margin = { top: 16, right: 72, bottom: 34, left: 12 };
1075
+ let cachedRightMargin = margin.right;
1071
1076
  const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
1072
1077
  const maxVisibleBars = Math.max(minVisibleBars, Math.floor(mergedOptions.maxVisibleBars));
1073
1078
  const maxPanBars = Math.max(0, Math.floor(mergedOptions.maxPanBars));
@@ -1289,14 +1294,34 @@ function createChart(element, options = {}) {
1289
1294
  const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
1290
1295
  return `${integerPart}${decimalPart}`;
1291
1296
  };
1297
+ const getMeasuredLabelWidth = (text, paddingX) => {
1298
+ return Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1299
+ };
1300
+ const getStabilizedNumericLabelWidth = (text, paddingX) => {
1301
+ const measured = getMeasuredLabelWidth(text, paddingX);
1302
+ if (!mergedOptions.stabilizePriceLabels || !/\d/.test(text)) {
1303
+ return measured;
1304
+ }
1305
+ let stableWidth = measured;
1306
+ for (const digit of ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]) {
1307
+ stableWidth = Math.max(stableWidth, getMeasuredLabelWidth(text.replace(/\d/g, digit), paddingX));
1308
+ }
1309
+ return stableWidth;
1310
+ };
1292
1311
  const getPriceLabelWidth = (priceText, paddingX) => {
1293
- const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
1312
+ const measured = getStabilizedNumericLabelWidth(priceText, paddingX);
1294
1313
  if (!mergedOptions.stabilizePriceLabels) {
1295
1314
  return measured;
1296
1315
  }
1297
- const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
1316
+ const templateWidth = getStabilizedNumericLabelWidth(getStabilizedPriceTemplate(), paddingX);
1298
1317
  return Math.max(measured, templateWidth);
1299
1318
  };
1319
+ const getStableLabelWidth = (text, paddingX) => {
1320
+ return getStabilizedNumericLabelWidth(text, paddingX);
1321
+ };
1322
+ const resetRightMarginCache = () => {
1323
+ cachedRightMargin = margin.right;
1324
+ };
1300
1325
  const parseData = (nextData) => {
1301
1326
  const dedupedByTime = /* @__PURE__ */ new Map();
1302
1327
  for (const point of nextData) {
@@ -1822,22 +1847,33 @@ function createChart(element, options = {}) {
1822
1847
  const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
1823
1848
  const estimateRightMargin = () => {
1824
1849
  const paddingX = Math.max(4, labels.labelPaddingX);
1825
- const measure = (text) => Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1826
1850
  let required = margin.right - 1;
1851
+ const includeWidth = (contentWidth) => {
1852
+ if (Number.isFinite(contentWidth)) {
1853
+ required = Math.max(required, contentWidth);
1854
+ }
1855
+ };
1827
1856
  const include = (text) => {
1828
1857
  const normalized = text?.trim();
1829
1858
  if (normalized) {
1830
- required = Math.max(required, measure(normalized));
1859
+ includeWidth(getStableLabelWidth(normalized, paddingX));
1860
+ }
1861
+ };
1862
+ const includePrice = (price) => {
1863
+ if (Number.isFinite(price)) {
1864
+ includeWidth(getPriceLabelWidth(formatPrice(price), paddingX));
1831
1865
  }
1832
1866
  };
1833
1867
  const ticker2 = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1834
1868
  const lastPoint2 = data[data.length - 1];
1835
1869
  if ((ticker2.visible ?? true) && labels.showLastPrice && lastPoint2) {
1836
- include(formatPrice(lastPoint2.c));
1870
+ const tickerPrice2 = ticker2.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint2.c;
1871
+ includePrice(tickerPrice2);
1837
1872
  if (ticker2.labelSubtext) include(String(ticker2.labelSubtext));
1838
1873
  for (const subtext of ticker2.labelSubtexts ?? []) {
1839
1874
  include(String(subtext));
1840
1875
  }
1876
+ if (ticker2.showCountdownInLabel) include("88:88");
1841
1877
  }
1842
1878
  if (labels.showSymbolName) {
1843
1879
  include(labels.symbolName);
@@ -1859,18 +1895,24 @@ function createChart(element, options = {}) {
1859
1895
  for (const line of priceLines) {
1860
1896
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1861
1897
  if (mergedLine.visible && Number.isFinite(mergedLine.price)) {
1862
- include(mergedLine.label ?? formatPrice(mergedLine.price));
1898
+ if (mergedLine.label) {
1899
+ include(mergedLine.label);
1900
+ } else {
1901
+ includePrice(mergedLine.price);
1902
+ }
1863
1903
  }
1864
1904
  }
1865
1905
  for (const line of orderLines) {
1866
1906
  const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
1867
1907
  const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
1868
1908
  if (mergedLine.visible && Number.isFinite(renderPrice)) {
1869
- include(formatPrice(renderPrice));
1909
+ includePrice(renderPrice);
1870
1910
  }
1871
1911
  }
1872
1912
  const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
1873
- return Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
1913
+ const nextRightMargin = Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
1914
+ cachedRightMargin = Math.max(cachedRightMargin, nextRightMargin);
1915
+ return Math.min(maxRightMargin, cachedRightMargin);
1874
1916
  };
1875
1917
  const rightMargin = estimateRightMargin();
1876
1918
  const chartLeft = margin.left;
@@ -3517,6 +3559,9 @@ function createChart(element, options = {}) {
3517
3559
  draw();
3518
3560
  };
3519
3561
  const onPointerDown = (event) => {
3562
+ if (event.pointerType === "touch" || event.pointerType === "pen") {
3563
+ event.preventDefault();
3564
+ }
3520
3565
  const point = getCanvasPoint(event);
3521
3566
  if (event.pointerType === "touch") {
3522
3567
  touchPointers.set(event.pointerId, point);
@@ -3615,12 +3660,29 @@ function createChart(element, options = {}) {
3615
3660
  isDragging = true;
3616
3661
  dragMode = region;
3617
3662
  activePointerId = event.pointerId;
3618
- pointerDownInfo = { pointerId: event.pointerId, x: point.x, y: point.y, region, moved: false };
3663
+ const crosshairDrag = (event.pointerType === "touch" || event.pointerType === "pen") && region === "plot";
3664
+ pointerDownInfo = {
3665
+ pointerId: event.pointerId,
3666
+ pointerType: event.pointerType,
3667
+ x: point.x,
3668
+ y: point.y,
3669
+ region,
3670
+ moved: false,
3671
+ crosshairDrag
3672
+ };
3619
3673
  lastPointerX = point.x;
3620
3674
  lastPointerY = point.y;
3621
3675
  canvas.setPointerCapture(event.pointerId);
3676
+ if (crosshairDrag) {
3677
+ canvas.style.cursor = "crosshair";
3678
+ setCrosshairPoint(point);
3679
+ emitCrosshairMove(point.x, point.y, "plot");
3680
+ }
3622
3681
  };
3623
3682
  const onPointerMove = (event) => {
3683
+ if (event.pointerType === "touch" || event.pointerType === "pen") {
3684
+ event.preventDefault();
3685
+ }
3624
3686
  const point = getCanvasPoint(event);
3625
3687
  if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
3626
3688
  touchPointers.set(event.pointerId, point);
@@ -3643,6 +3705,16 @@ function createChart(element, options = {}) {
3643
3705
  pointerDownInfo.moved = true;
3644
3706
  }
3645
3707
  }
3708
+ if (pointerDownInfo?.crosshairDrag && pointerDownInfo.pointerId === event.pointerId) {
3709
+ if (getHitRegion(point.x, point.y) === "plot") {
3710
+ canvas.style.cursor = "crosshair";
3711
+ setCrosshairPoint(point);
3712
+ emitCrosshairMove(point.x, point.y, "plot");
3713
+ lastPointerX = point.x;
3714
+ lastPointerY = point.y;
3715
+ }
3716
+ return;
3717
+ }
3646
3718
  if (drawingDragState) {
3647
3719
  if (activePointerId !== null && event.pointerId !== activePointerId) {
3648
3720
  return;
@@ -3856,7 +3928,13 @@ function createChart(element, options = {}) {
3856
3928
  activePointerId = null;
3857
3929
  canvas.style.cursor = "default";
3858
3930
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
3859
- if (!pointerDownInfo.moved) {
3931
+ if (pointerDownInfo.crosshairDrag) {
3932
+ const point = getCanvasPoint(event);
3933
+ if (getHitRegion(point.x, point.y) === "plot") {
3934
+ setCrosshairPoint(point);
3935
+ emitCrosshairMove(point.x, point.y, "plot");
3936
+ }
3937
+ } else if (!pointerDownInfo.moved) {
3860
3938
  const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
3861
3939
  chartClickHandler?.({
3862
3940
  x: pointerDownInfo.x,
@@ -3903,6 +3981,7 @@ function createChart(element, options = {}) {
3903
3981
  zoomX(factor, point.x);
3904
3982
  };
3905
3983
  const onDoubleClick = (event) => {
3984
+ event.preventDefault();
3906
3985
  if (!doubleClickEnabled) {
3907
3986
  return;
3908
3987
  }
@@ -3928,6 +4007,9 @@ function createChart(element, options = {}) {
3928
4007
  }
3929
4008
  resetViewport();
3930
4009
  };
4010
+ const onContextMenu = (event) => {
4011
+ event.preventDefault();
4012
+ };
3931
4013
  canvas.addEventListener("pointerdown", onPointerDown);
3932
4014
  canvas.addEventListener("pointermove", onPointerMove);
3933
4015
  canvas.addEventListener("pointerup", endPointerDrag);
@@ -3935,6 +4017,7 @@ function createChart(element, options = {}) {
3935
4017
  canvas.addEventListener("pointerleave", endPointerDrag);
3936
4018
  canvas.addEventListener("wheel", onWheel, { passive: false });
3937
4019
  canvas.addEventListener("dblclick", onDoubleClick);
4020
+ canvas.addEventListener("contextmenu", onContextMenu);
3938
4021
  const updateOptions = (nextOptions) => {
3939
4022
  const wasTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
3940
4023
  const previousWidth = width;
@@ -3943,6 +4026,7 @@ function createChart(element, options = {}) {
3943
4026
  width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
3944
4027
  height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
3945
4028
  mergedOptions = { ...mergedOptions, width, height };
4029
+ resetRightMarginCache();
3946
4030
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
3947
4031
  doubleClickAction = mergedOptions.doubleClickAction;
3948
4032
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
@@ -3969,6 +4053,7 @@ function createChart(element, options = {}) {
3969
4053
  height = nextHeight;
3970
4054
  }
3971
4055
  mergedOptions = { ...mergedOptions, width, height };
4056
+ resetRightMarginCache();
3972
4057
  draw();
3973
4058
  };
3974
4059
  const setData = (nextData) => {
@@ -3985,6 +4070,7 @@ function createChart(element, options = {}) {
3985
4070
  tickerPriceTarget = null;
3986
4071
  smoothedTickerVolume = null;
3987
4072
  tickerVolumeTarget = null;
4073
+ resetRightMarginCache();
3988
4074
  resetYViewport();
3989
4075
  draw();
3990
4076
  return;
@@ -4018,6 +4104,7 @@ function createChart(element, options = {}) {
4018
4104
  ...line,
4019
4105
  id: line.id ?? `line-${index + 1}`
4020
4106
  }));
4107
+ resetRightMarginCache();
4021
4108
  draw();
4022
4109
  };
4023
4110
  const addPriceLine = (line) => {
@@ -4028,6 +4115,7 @@ function createChart(element, options = {}) {
4028
4115
  };
4029
4116
  const removePriceLine = (id) => {
4030
4117
  priceLines = priceLines.filter((line) => line.id !== id);
4118
+ resetRightMarginCache();
4031
4119
  draw();
4032
4120
  };
4033
4121
  const setOrderLines = (lines) => {
@@ -4035,6 +4123,7 @@ function createChart(element, options = {}) {
4035
4123
  ...line,
4036
4124
  id: line.id ?? `order-${index + 1}`
4037
4125
  }));
4126
+ resetRightMarginCache();
4038
4127
  const activeIds = new Set(orderLines.map((line) => line.id));
4039
4128
  for (const id of Array.from(orderWidgetWidthById.keys())) {
4040
4129
  if (!activeIds.has(id)) {
@@ -4062,6 +4151,7 @@ function createChart(element, options = {}) {
4062
4151
  orderLines = orderLines.filter((line) => line.id !== id);
4063
4152
  orderWidgetWidthById.delete(id);
4064
4153
  orderPriceTagWidthById.delete(id);
4154
+ resetRightMarginCache();
4065
4155
  draw();
4066
4156
  };
4067
4157
  const onOrderAction = (handler) => {
@@ -4225,6 +4315,7 @@ function createChart(element, options = {}) {
4225
4315
  canvas.removeEventListener("pointerleave", endPointerDrag);
4226
4316
  canvas.removeEventListener("wheel", onWheel);
4227
4317
  canvas.removeEventListener("dblclick", onDoubleClick);
4318
+ canvas.removeEventListener("contextmenu", onContextMenu);
4228
4319
  element.innerHTML = "";
4229
4320
  };
4230
4321
  draw();
package/dist/index.js CHANGED
@@ -1041,9 +1041,14 @@ function createChart(element, options = {}) {
1041
1041
  }
1042
1042
  canvas.style.display = "block";
1043
1043
  canvas.style.touchAction = "none";
1044
+ canvas.style.userSelect = "none";
1045
+ canvas.style.setProperty("-webkit-user-select", "none");
1046
+ canvas.style.setProperty("-webkit-touch-callout", "none");
1047
+ canvas.setAttribute("draggable", "false");
1044
1048
  element.innerHTML = "";
1045
1049
  element.appendChild(canvas);
1046
1050
  const margin = { top: 16, right: 72, bottom: 34, left: 12 };
1051
+ let cachedRightMargin = margin.right;
1047
1052
  const minVisibleBars = Math.max(1, Math.floor(mergedOptions.minVisibleBars));
1048
1053
  const maxVisibleBars = Math.max(minVisibleBars, Math.floor(mergedOptions.maxVisibleBars));
1049
1054
  const maxPanBars = Math.max(0, Math.floor(mergedOptions.maxPanBars));
@@ -1265,14 +1270,34 @@ function createChart(element, options = {}) {
1265
1270
  const decimalPart = decimals > 0 ? `.${"8".repeat(decimals)}` : "";
1266
1271
  return `${integerPart}${decimalPart}`;
1267
1272
  };
1273
+ const getMeasuredLabelWidth = (text, paddingX) => {
1274
+ return Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1275
+ };
1276
+ const getStabilizedNumericLabelWidth = (text, paddingX) => {
1277
+ const measured = getMeasuredLabelWidth(text, paddingX);
1278
+ if (!mergedOptions.stabilizePriceLabels || !/\d/.test(text)) {
1279
+ return measured;
1280
+ }
1281
+ let stableWidth = measured;
1282
+ for (const digit of ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]) {
1283
+ stableWidth = Math.max(stableWidth, getMeasuredLabelWidth(text.replace(/\d/g, digit), paddingX));
1284
+ }
1285
+ return stableWidth;
1286
+ };
1268
1287
  const getPriceLabelWidth = (priceText, paddingX) => {
1269
- const measured = Math.ceil(ctx.measureText(priceText).width) + paddingX * 2;
1288
+ const measured = getStabilizedNumericLabelWidth(priceText, paddingX);
1270
1289
  if (!mergedOptions.stabilizePriceLabels) {
1271
1290
  return measured;
1272
1291
  }
1273
- const templateWidth = Math.ceil(ctx.measureText(getStabilizedPriceTemplate()).width) + paddingX * 2;
1292
+ const templateWidth = getStabilizedNumericLabelWidth(getStabilizedPriceTemplate(), paddingX);
1274
1293
  return Math.max(measured, templateWidth);
1275
1294
  };
1295
+ const getStableLabelWidth = (text, paddingX) => {
1296
+ return getStabilizedNumericLabelWidth(text, paddingX);
1297
+ };
1298
+ const resetRightMarginCache = () => {
1299
+ cachedRightMargin = margin.right;
1300
+ };
1276
1301
  const parseData = (nextData) => {
1277
1302
  const dedupedByTime = /* @__PURE__ */ new Map();
1278
1303
  for (const point of nextData) {
@@ -1798,22 +1823,33 @@ function createChart(element, options = {}) {
1798
1823
  const labels = { ...DEFAULT_LABELS_OPTIONS, ...mergedOptions.labels ?? {} };
1799
1824
  const estimateRightMargin = () => {
1800
1825
  const paddingX = Math.max(4, labels.labelPaddingX);
1801
- const measure = (text) => Math.ceil(ctx.measureText(text).width) + paddingX * 2;
1802
1826
  let required = margin.right - 1;
1827
+ const includeWidth = (contentWidth) => {
1828
+ if (Number.isFinite(contentWidth)) {
1829
+ required = Math.max(required, contentWidth);
1830
+ }
1831
+ };
1803
1832
  const include = (text) => {
1804
1833
  const normalized = text?.trim();
1805
1834
  if (normalized) {
1806
- required = Math.max(required, measure(normalized));
1835
+ includeWidth(getStableLabelWidth(normalized, paddingX));
1836
+ }
1837
+ };
1838
+ const includePrice = (price) => {
1839
+ if (Number.isFinite(price)) {
1840
+ includeWidth(getPriceLabelWidth(formatPrice(price), paddingX));
1807
1841
  }
1808
1842
  };
1809
1843
  const ticker2 = mergedOptions.tickerLine ?? DEFAULT_OPTIONS.tickerLine;
1810
1844
  const lastPoint2 = data[data.length - 1];
1811
1845
  if ((ticker2.visible ?? true) && labels.showLastPrice && lastPoint2) {
1812
- include(formatPrice(lastPoint2.c));
1846
+ const tickerPrice2 = ticker2.smoothing && smoothedTickerPrice !== null ? smoothedTickerPrice : lastPoint2.c;
1847
+ includePrice(tickerPrice2);
1813
1848
  if (ticker2.labelSubtext) include(String(ticker2.labelSubtext));
1814
1849
  for (const subtext of ticker2.labelSubtexts ?? []) {
1815
1850
  include(String(subtext));
1816
1851
  }
1852
+ if (ticker2.showCountdownInLabel) include("88:88");
1817
1853
  }
1818
1854
  if (labels.showSymbolName) {
1819
1855
  include(labels.symbolName);
@@ -1835,18 +1871,24 @@ function createChart(element, options = {}) {
1835
1871
  for (const line of priceLines) {
1836
1872
  const mergedLine = { ...DEFAULT_PRICE_LINE_OPTIONS, ...line };
1837
1873
  if (mergedLine.visible && Number.isFinite(mergedLine.price)) {
1838
- include(mergedLine.label ?? formatPrice(mergedLine.price));
1874
+ if (mergedLine.label) {
1875
+ include(mergedLine.label);
1876
+ } else {
1877
+ includePrice(mergedLine.price);
1878
+ }
1839
1879
  }
1840
1880
  }
1841
1881
  for (const line of orderLines) {
1842
1882
  const mergedLine = { ...DEFAULT_ORDER_LINE_OPTIONS, ...line };
1843
1883
  const renderPrice = mergedLine.behavior === "follow" && Number.isFinite(mergedLine.followPrice) ? mergedLine.followPrice : mergedLine.price;
1844
1884
  if (mergedLine.visible && Number.isFinite(renderPrice)) {
1845
- include(formatPrice(renderPrice));
1885
+ includePrice(renderPrice);
1846
1886
  }
1847
1887
  }
1848
1888
  const maxRightMargin = Math.max(margin.right, width - margin.left - 160);
1849
- return Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
1889
+ const nextRightMargin = Math.min(maxRightMargin, Math.max(margin.right, Math.ceil(required + 1)));
1890
+ cachedRightMargin = Math.max(cachedRightMargin, nextRightMargin);
1891
+ return Math.min(maxRightMargin, cachedRightMargin);
1850
1892
  };
1851
1893
  const rightMargin = estimateRightMargin();
1852
1894
  const chartLeft = margin.left;
@@ -3493,6 +3535,9 @@ function createChart(element, options = {}) {
3493
3535
  draw();
3494
3536
  };
3495
3537
  const onPointerDown = (event) => {
3538
+ if (event.pointerType === "touch" || event.pointerType === "pen") {
3539
+ event.preventDefault();
3540
+ }
3496
3541
  const point = getCanvasPoint(event);
3497
3542
  if (event.pointerType === "touch") {
3498
3543
  touchPointers.set(event.pointerId, point);
@@ -3591,12 +3636,29 @@ function createChart(element, options = {}) {
3591
3636
  isDragging = true;
3592
3637
  dragMode = region;
3593
3638
  activePointerId = event.pointerId;
3594
- pointerDownInfo = { pointerId: event.pointerId, x: point.x, y: point.y, region, moved: false };
3639
+ const crosshairDrag = (event.pointerType === "touch" || event.pointerType === "pen") && region === "plot";
3640
+ pointerDownInfo = {
3641
+ pointerId: event.pointerId,
3642
+ pointerType: event.pointerType,
3643
+ x: point.x,
3644
+ y: point.y,
3645
+ region,
3646
+ moved: false,
3647
+ crosshairDrag
3648
+ };
3595
3649
  lastPointerX = point.x;
3596
3650
  lastPointerY = point.y;
3597
3651
  canvas.setPointerCapture(event.pointerId);
3652
+ if (crosshairDrag) {
3653
+ canvas.style.cursor = "crosshair";
3654
+ setCrosshairPoint(point);
3655
+ emitCrosshairMove(point.x, point.y, "plot");
3656
+ }
3598
3657
  };
3599
3658
  const onPointerMove = (event) => {
3659
+ if (event.pointerType === "touch" || event.pointerType === "pen") {
3660
+ event.preventDefault();
3661
+ }
3600
3662
  const point = getCanvasPoint(event);
3601
3663
  if (event.pointerType === "touch" && touchPointers.has(event.pointerId)) {
3602
3664
  touchPointers.set(event.pointerId, point);
@@ -3619,6 +3681,16 @@ function createChart(element, options = {}) {
3619
3681
  pointerDownInfo.moved = true;
3620
3682
  }
3621
3683
  }
3684
+ if (pointerDownInfo?.crosshairDrag && pointerDownInfo.pointerId === event.pointerId) {
3685
+ if (getHitRegion(point.x, point.y) === "plot") {
3686
+ canvas.style.cursor = "crosshair";
3687
+ setCrosshairPoint(point);
3688
+ emitCrosshairMove(point.x, point.y, "plot");
3689
+ lastPointerX = point.x;
3690
+ lastPointerY = point.y;
3691
+ }
3692
+ return;
3693
+ }
3622
3694
  if (drawingDragState) {
3623
3695
  if (activePointerId !== null && event.pointerId !== activePointerId) {
3624
3696
  return;
@@ -3832,7 +3904,13 @@ function createChart(element, options = {}) {
3832
3904
  activePointerId = null;
3833
3905
  canvas.style.cursor = "default";
3834
3906
  if (event && pointerDownInfo && event.pointerId === pointerDownInfo.pointerId) {
3835
- if (!pointerDownInfo.moved) {
3907
+ if (pointerDownInfo.crosshairDrag) {
3908
+ const point = getCanvasPoint(event);
3909
+ if (getHitRegion(point.x, point.y) === "plot") {
3910
+ setCrosshairPoint(point);
3911
+ emitCrosshairMove(point.x, point.y, "plot");
3912
+ }
3913
+ } else if (!pointerDownInfo.moved) {
3836
3914
  const clickPrice = pointerDownInfo.region === "plot" ? roundToPricePrecision(priceFromCanvasY(pointerDownInfo.y)) : void 0;
3837
3915
  chartClickHandler?.({
3838
3916
  x: pointerDownInfo.x,
@@ -3879,6 +3957,7 @@ function createChart(element, options = {}) {
3879
3957
  zoomX(factor, point.x);
3880
3958
  };
3881
3959
  const onDoubleClick = (event) => {
3960
+ event.preventDefault();
3882
3961
  if (!doubleClickEnabled) {
3883
3962
  return;
3884
3963
  }
@@ -3904,6 +3983,9 @@ function createChart(element, options = {}) {
3904
3983
  }
3905
3984
  resetViewport();
3906
3985
  };
3986
+ const onContextMenu = (event) => {
3987
+ event.preventDefault();
3988
+ };
3907
3989
  canvas.addEventListener("pointerdown", onPointerDown);
3908
3990
  canvas.addEventListener("pointermove", onPointerMove);
3909
3991
  canvas.addEventListener("pointerup", endPointerDrag);
@@ -3911,6 +3993,7 @@ function createChart(element, options = {}) {
3911
3993
  canvas.addEventListener("pointerleave", endPointerDrag);
3912
3994
  canvas.addEventListener("wheel", onWheel, { passive: false });
3913
3995
  canvas.addEventListener("dblclick", onDoubleClick);
3996
+ canvas.addEventListener("contextmenu", onContextMenu);
3914
3997
  const updateOptions = (nextOptions) => {
3915
3998
  const wasTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
3916
3999
  const previousWidth = width;
@@ -3919,6 +4002,7 @@ function createChart(element, options = {}) {
3919
4002
  width = nextOptions.width !== void 0 && nextOptions.width > 0 ? mergedOptions.width : previousWidth;
3920
4003
  height = nextOptions.height !== void 0 && nextOptions.height > 0 ? mergedOptions.height : previousHeight;
3921
4004
  mergedOptions = { ...mergedOptions, width, height };
4005
+ resetRightMarginCache();
3922
4006
  doubleClickEnabled = mergedOptions.doubleClickEnabled;
3923
4007
  doubleClickAction = mergedOptions.doubleClickAction;
3924
4008
  const isTickerSmoothingEnabled = mergedOptions.tickerLine?.smoothing ?? false;
@@ -3945,6 +4029,7 @@ function createChart(element, options = {}) {
3945
4029
  height = nextHeight;
3946
4030
  }
3947
4031
  mergedOptions = { ...mergedOptions, width, height };
4032
+ resetRightMarginCache();
3948
4033
  draw();
3949
4034
  };
3950
4035
  const setData = (nextData) => {
@@ -3961,6 +4046,7 @@ function createChart(element, options = {}) {
3961
4046
  tickerPriceTarget = null;
3962
4047
  smoothedTickerVolume = null;
3963
4048
  tickerVolumeTarget = null;
4049
+ resetRightMarginCache();
3964
4050
  resetYViewport();
3965
4051
  draw();
3966
4052
  return;
@@ -3994,6 +4080,7 @@ function createChart(element, options = {}) {
3994
4080
  ...line,
3995
4081
  id: line.id ?? `line-${index + 1}`
3996
4082
  }));
4083
+ resetRightMarginCache();
3997
4084
  draw();
3998
4085
  };
3999
4086
  const addPriceLine = (line) => {
@@ -4004,6 +4091,7 @@ function createChart(element, options = {}) {
4004
4091
  };
4005
4092
  const removePriceLine = (id) => {
4006
4093
  priceLines = priceLines.filter((line) => line.id !== id);
4094
+ resetRightMarginCache();
4007
4095
  draw();
4008
4096
  };
4009
4097
  const setOrderLines = (lines) => {
@@ -4011,6 +4099,7 @@ function createChart(element, options = {}) {
4011
4099
  ...line,
4012
4100
  id: line.id ?? `order-${index + 1}`
4013
4101
  }));
4102
+ resetRightMarginCache();
4014
4103
  const activeIds = new Set(orderLines.map((line) => line.id));
4015
4104
  for (const id of Array.from(orderWidgetWidthById.keys())) {
4016
4105
  if (!activeIds.has(id)) {
@@ -4038,6 +4127,7 @@ function createChart(element, options = {}) {
4038
4127
  orderLines = orderLines.filter((line) => line.id !== id);
4039
4128
  orderWidgetWidthById.delete(id);
4040
4129
  orderPriceTagWidthById.delete(id);
4130
+ resetRightMarginCache();
4041
4131
  draw();
4042
4132
  };
4043
4133
  const onOrderAction = (handler) => {
@@ -4201,6 +4291,7 @@ function createChart(element, options = {}) {
4201
4291
  canvas.removeEventListener("pointerleave", endPointerDrag);
4202
4292
  canvas.removeEventListener("wheel", onWheel);
4203
4293
  canvas.removeEventListener("dblclick", onDoubleClick);
4294
+ canvas.removeEventListener("contextmenu", onContextMenu);
4204
4295
  element.innerHTML = "";
4205
4296
  };
4206
4297
  draw();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hyperprop-charting-library",
3
- "version": "0.1.62",
3
+ "version": "0.1.64",
4
4
  "description": "Lightweight TypeScript charting core",
5
5
  "type": "module",
6
6
  "main": "./dist/hyperprop-charting-library.cjs",