hyperprop-charting-library 0.1.134 → 0.1.135

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -2970,8 +2970,11 @@ function createChart(element, options = {}) {
2970
2970
  );
2971
2971
  let activeDrawingTool = null;
2972
2972
  let draftDrawing = null;
2973
+ let measureState = null;
2973
2974
  let drawingDragState = null;
2974
2975
  let drawingsChangeHandler = null;
2976
+ let drawingToolChangeHandler = null;
2977
+ let lastNotifiedDrawingTool = null;
2975
2978
  let drawingSelectHandler = null;
2976
2979
  let drawingDoubleClickHandler = null;
2977
2980
  let drawingEditTextHandler = null;
@@ -3564,6 +3567,13 @@ function createChart(element, options = {}) {
3564
3567
  points: draftDrawing.points.map((point) => reanchorDrawingPoint(point))
3565
3568
  };
3566
3569
  }
3570
+ if (measureState) {
3571
+ measureState = {
3572
+ ...measureState,
3573
+ start: reanchorDrawingPoint(measureState.start),
3574
+ end: reanchorDrawingPoint(measureState.end)
3575
+ };
3576
+ }
3567
3577
  };
3568
3578
  const getCandleDirectionByIndex = (index) => {
3569
3579
  const point = data[index];
@@ -3665,6 +3675,22 @@ function createChart(element, options = {}) {
3665
3675
  const pad = (value) => String(value).padStart(2, "0");
3666
3676
  return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
3667
3677
  };
3678
+ const formatTimeSpan = (ms) => {
3679
+ const totalMinutes = Math.max(0, Math.round(ms / 6e4));
3680
+ if (totalMinutes < 1) {
3681
+ return `${Math.max(1, Math.round(ms / 1e3))}s`;
3682
+ }
3683
+ const days = Math.floor(totalMinutes / 1440);
3684
+ const hours = Math.floor(totalMinutes % 1440 / 60);
3685
+ const minutes = totalMinutes % 60;
3686
+ if (days > 0) {
3687
+ return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
3688
+ }
3689
+ if (hours > 0) {
3690
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
3691
+ }
3692
+ return `${minutes}m`;
3693
+ };
3668
3694
  const getRightAxisLabelX = (chartRight) => chartRight + 1;
3669
3695
  const getRightAxisLabelWidth = (chartRight, contentWidth) => {
3670
3696
  const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
@@ -4033,6 +4059,10 @@ function createChart(element, options = {}) {
4033
4059
  let pendingDrawUpdateAutoScale = false;
4034
4060
  let pendingDrawOverlayOnly = true;
4035
4061
  const scheduleDraw = (options2 = {}) => {
4062
+ if (activeDrawingTool !== lastNotifiedDrawingTool) {
4063
+ lastNotifiedDrawingTool = activeDrawingTool;
4064
+ drawingToolChangeHandler?.(activeDrawingTool);
4065
+ }
4036
4066
  armCountdownHeartbeat();
4037
4067
  const overlayOnly = options2.overlayOnly ?? false;
4038
4068
  pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
@@ -4845,7 +4875,7 @@ function createChart(element, options = {}) {
4845
4875
  const t0 = getTimeForIndex(Math.round(p0.index));
4846
4876
  const t1 = getTimeForIndex(Math.round(p1.index));
4847
4877
  const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
4848
- labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
4878
+ labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatTimeSpan(spanMs)}` : `${barSpan} bars`);
4849
4879
  }
4850
4880
  const pointValue = Number(drawing.pointValue);
4851
4881
  if (Number.isFinite(pointValue) && pointValue > 0) {
@@ -5322,6 +5352,96 @@ function createChart(element, options = {}) {
5322
5352
  if (draftDrawing) {
5323
5353
  drawDrawing(draftDrawing, true);
5324
5354
  }
5355
+ if (measureState) {
5356
+ const mStart = measureState.start;
5357
+ const mEnd = measureState.end;
5358
+ const mx0 = xFromDrawingPoint(mStart);
5359
+ const mx1 = xFromDrawingPoint(mEnd);
5360
+ const my0 = yFromPrice(mStart.price);
5361
+ const my1 = yFromPrice(mEnd.price);
5362
+ const mLeft = Math.min(mx0, mx1);
5363
+ const mRight = Math.max(mx0, mx1);
5364
+ const mTop = Math.min(my0, my1);
5365
+ const mBottom = Math.max(my0, my1);
5366
+ const mUp = mEnd.price >= mStart.price;
5367
+ const mColor = mUp ? "#2962ff" : "#f23645";
5368
+ ctx.save();
5369
+ ctx.fillStyle = hexToRgba(mColor, 0.12);
5370
+ ctx.fillRect(mLeft, mTop, Math.max(1, mRight - mLeft), Math.max(1, mBottom - mTop));
5371
+ ctx.strokeStyle = mColor;
5372
+ ctx.lineWidth = 1;
5373
+ ctx.setLineDash([]);
5374
+ const mArrowHead = 5;
5375
+ const mMidX = (mLeft + mRight) / 2;
5376
+ if (Math.abs(my1 - my0) > mArrowHead * 2) {
5377
+ ctx.beginPath();
5378
+ ctx.moveTo(crisp(mMidX), crisp(my0));
5379
+ ctx.lineTo(crisp(mMidX), crisp(my1));
5380
+ ctx.stroke();
5381
+ const dirY = my1 > my0 ? 1 : -1;
5382
+ ctx.beginPath();
5383
+ ctx.moveTo(mMidX, my1);
5384
+ ctx.lineTo(mMidX - mArrowHead, my1 - dirY * mArrowHead);
5385
+ ctx.moveTo(mMidX, my1);
5386
+ ctx.lineTo(mMidX + mArrowHead, my1 - dirY * mArrowHead);
5387
+ ctx.stroke();
5388
+ }
5389
+ const mMidY = (mTop + mBottom) / 2;
5390
+ if (Math.abs(mx1 - mx0) > mArrowHead * 2) {
5391
+ ctx.beginPath();
5392
+ ctx.moveTo(crisp(mx0), crisp(mMidY));
5393
+ ctx.lineTo(crisp(mx1), crisp(mMidY));
5394
+ ctx.stroke();
5395
+ const dirX = mx1 > mx0 ? 1 : -1;
5396
+ ctx.beginPath();
5397
+ ctx.moveTo(mx1, mMidY);
5398
+ ctx.lineTo(mx1 - dirX * mArrowHead, mMidY - mArrowHead);
5399
+ ctx.moveTo(mx1, mMidY);
5400
+ ctx.lineTo(mx1 - dirX * mArrowHead, mMidY + mArrowHead);
5401
+ ctx.stroke();
5402
+ }
5403
+ const mDiff = mEnd.price - mStart.price;
5404
+ const mBase = Math.abs(mStart.price) > 0 ? Math.abs(mStart.price) : 1;
5405
+ const mPct = mDiff / mBase * 100;
5406
+ const mTick = getConfiguredTickSize();
5407
+ const mTicks = mTick > 0 ? Math.round(mDiff / mTick) : 0;
5408
+ const mSigned = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
5409
+ const mLines = [
5410
+ mTick > 0 ? `${mSigned(mDiff, formatPrice(Math.abs(mDiff)))} (${mSigned(mPct, `${Math.abs(mPct).toFixed(2)}%`)}) ${mSigned(mTicks, String(Math.abs(mTicks)))}` : `${mSigned(mDiff, formatPrice(Math.abs(mDiff)))} (${mSigned(mPct, `${Math.abs(mPct).toFixed(2)}%`)})`
5411
+ ];
5412
+ const mBars = Math.round(mEnd.index) - Math.round(mStart.index);
5413
+ const mT0 = getTimeForIndex(Math.round(mStart.index));
5414
+ const mT1 = getTimeForIndex(Math.round(mEnd.index));
5415
+ const mSpanMs = mT0 && mT1 ? Math.abs(mT1.getTime() - mT0.getTime()) : 0;
5416
+ mLines.push(
5417
+ mSpanMs > 0 ? `${mSigned(mBars, String(Math.abs(mBars)))} bars, ${formatTimeSpan(mSpanMs)}` : `${mSigned(mBars, String(Math.abs(mBars)))} bars`
5418
+ );
5419
+ const prevMeasureFont = ctx.font;
5420
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
5421
+ const mPad = 7;
5422
+ const mLineHeight = 15;
5423
+ const mTextW = Math.max(...mLines.map((line) => measureTextWidth(line)));
5424
+ const mPillW = mTextW + mPad * 2;
5425
+ const mPillH = mLines.length * mLineHeight + mPad;
5426
+ const mPillX = clamp(mMidX - mPillW / 2, chartLeft + 2, chartLeft + chartWidth - mPillW - 2);
5427
+ let mPillY = mUp ? mTop - 8 - mPillH : mBottom + 8;
5428
+ if (mPillY < chartTop + 2) {
5429
+ mPillY = mBottom + 8;
5430
+ }
5431
+ if (mPillY + mPillH > fullChartBottom - 2) {
5432
+ mPillY = Math.max(chartTop + 2, mTop - 8 - mPillH);
5433
+ }
5434
+ ctx.fillStyle = mColor;
5435
+ fillRoundedRect(mPillX, mPillY, mPillW, mPillH, 4);
5436
+ ctx.fillStyle = "#ffffff";
5437
+ ctx.textAlign = "center";
5438
+ ctx.textBaseline = "middle";
5439
+ mLines.forEach((line, lineIndex) => {
5440
+ ctx.fillText(line, mPillX + mPillW / 2, mPillY + mPad / 2 + lineIndex * mLineHeight + mLineHeight / 2);
5441
+ });
5442
+ ctx.font = prevMeasureFont;
5443
+ ctx.restore();
5444
+ }
5325
5445
  if (tradeMarkers.length > 0 && data.length > 0) {
5326
5446
  const visibleStart = Math.floor(xStart) - 1;
5327
5447
  const visibleEnd = Math.ceil(xStart + xSpan) + 1;
@@ -7320,6 +7440,19 @@ function createChart(element, options = {}) {
7320
7440
  return;
7321
7441
  }
7322
7442
  }
7443
+ if (measureState) {
7444
+ if (measureState.dragging) {
7445
+ measureState = { ...measureState, dragging: false };
7446
+ if (activeDrawingTool === "measure") {
7447
+ activeDrawingTool = null;
7448
+ canvas.style.cursor = "default";
7449
+ }
7450
+ scheduleDraw();
7451
+ return;
7452
+ }
7453
+ measureState = null;
7454
+ scheduleDraw();
7455
+ }
7323
7456
  const drawingToolCapturesPointer = activeDrawingTool !== null || draftDrawing !== null;
7324
7457
  if (!drawingToolCapturesPointer) {
7325
7458
  const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
@@ -7394,6 +7527,18 @@ function createChart(element, options = {}) {
7394
7527
  if (region === "outside") {
7395
7528
  return;
7396
7529
  }
7530
+ if (region === "plot" && (activeDrawingTool === "measure" || activeDrawingTool === null && draftDrawing === null && event.shiftKey && event.pointerType === "mouse")) {
7531
+ const measurePoint = drawingPointFromCanvas(point.x, point.y);
7532
+ if (measurePoint) {
7533
+ measureState = { start: measurePoint, end: measurePoint, dragging: true };
7534
+ activePointerId = event.pointerId;
7535
+ capturePointer(event.pointerId);
7536
+ setCrosshairPoint(null);
7537
+ canvas.style.cursor = "crosshair";
7538
+ scheduleDraw();
7539
+ return;
7540
+ }
7541
+ }
7397
7542
  if (region === "plot" && !activeDrawingTool) {
7398
7543
  const drawingHit = getDrawingHit(point.x, point.y);
7399
7544
  if (drawingHit) {
@@ -7489,6 +7634,19 @@ function createChart(element, options = {}) {
7489
7634
  }
7490
7635
  return;
7491
7636
  }
7637
+ if (measureState?.dragging) {
7638
+ if (activePointerId !== null && event.pointerId !== activePointerId) {
7639
+ return;
7640
+ }
7641
+ const nextMeasurePoint = drawingPointFromCanvas(point.x, point.y);
7642
+ if (nextMeasurePoint) {
7643
+ measureState = { ...measureState, end: nextMeasurePoint };
7644
+ setCrosshairPoint(null);
7645
+ canvas.style.cursor = "crosshair";
7646
+ scheduleDraw();
7647
+ }
7648
+ return;
7649
+ }
7492
7650
  if (drawingDragState) {
7493
7651
  if (activePointerId !== null && event.pointerId !== activePointerId) {
7494
7652
  return;
@@ -7719,6 +7877,24 @@ function createChart(element, options = {}) {
7719
7877
  if (event && activePointerId !== null && event.pointerId !== activePointerId) {
7720
7878
  return;
7721
7879
  }
7880
+ if (measureState?.dragging) {
7881
+ const movedMeasure = measureState.start.index !== measureState.end.index || measureState.start.price !== measureState.end.price;
7882
+ if (movedMeasure) {
7883
+ measureState = { ...measureState, dragging: false };
7884
+ if (activeDrawingTool === "measure") {
7885
+ activeDrawingTool = null;
7886
+ }
7887
+ canvas.style.cursor = "default";
7888
+ } else {
7889
+ canvas.style.cursor = "crosshair";
7890
+ }
7891
+ isDragging = false;
7892
+ dragMode = null;
7893
+ activePointerId = null;
7894
+ pointerDownInfo = null;
7895
+ scheduleDraw();
7896
+ return;
7897
+ }
7722
7898
  if (orderDragState) {
7723
7899
  const moved = orderDragState.lastPrice !== orderDragState.startPrice;
7724
7900
  const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
@@ -7899,6 +8075,15 @@ function createChart(element, options = {}) {
7899
8075
  canvas.addEventListener("dblclick", onDoubleClick);
7900
8076
  canvas.addEventListener("contextmenu", onContextMenu);
7901
8077
  const onModifierKeyChange = (event) => {
8078
+ if (event.key === "Escape" && event.type === "keydown" && measureState) {
8079
+ measureState = null;
8080
+ if (activeDrawingTool === "measure") {
8081
+ activeDrawingTool = null;
8082
+ canvas.style.cursor = "default";
8083
+ }
8084
+ scheduleDraw();
8085
+ return;
8086
+ }
7902
8087
  const active = event.metaKey || event.ctrlKey;
7903
8088
  const shift = event.shiftKey;
7904
8089
  const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
@@ -8211,17 +8396,29 @@ function createChart(element, options = {}) {
8211
8396
  const setActiveDrawingTool = (tool) => {
8212
8397
  activeDrawingTool = tool;
8213
8398
  draftDrawing = null;
8399
+ measureState = null;
8214
8400
  canvas.style.cursor = tool ? "crosshair" : "default";
8215
8401
  scheduleDraw();
8216
8402
  };
8217
8403
  const getActiveDrawingTool = () => activeDrawingTool;
8218
8404
  const cancelDrawing = () => {
8405
+ let cancelled = false;
8219
8406
  if (draftDrawing) {
8220
8407
  draftDrawing = null;
8408
+ cancelled = true;
8409
+ }
8410
+ if (measureState) {
8411
+ measureState = null;
8412
+ if (activeDrawingTool === "measure") {
8413
+ activeDrawingTool = null;
8414
+ canvas.style.cursor = "default";
8415
+ }
8416
+ cancelled = true;
8417
+ }
8418
+ if (cancelled) {
8221
8419
  scheduleDraw();
8222
- return true;
8223
8420
  }
8224
- return false;
8421
+ return cancelled;
8225
8422
  };
8226
8423
  const setTradeMarkers = (markers) => {
8227
8424
  tradeMarkers = Array.isArray(markers) ? markers.slice() : [];
@@ -8273,6 +8470,9 @@ function createChart(element, options = {}) {
8273
8470
  const onDrawingsChange = (handler) => {
8274
8471
  drawingsChangeHandler = handler;
8275
8472
  };
8473
+ const onActiveDrawingToolChange = (handler) => {
8474
+ drawingToolChangeHandler = handler;
8475
+ };
8276
8476
  const onDrawingSelect = (handler) => {
8277
8477
  drawingSelectHandler = handler;
8278
8478
  };
@@ -8359,6 +8559,7 @@ function createChart(element, options = {}) {
8359
8559
  removeDrawing,
8360
8560
  clearDrawings,
8361
8561
  onDrawingsChange,
8562
+ onActiveDrawingToolChange,
8362
8563
  onDrawingSelect,
8363
8564
  onDrawingDoubleClick,
8364
8565
  onDrawingEditText,
package/dist/index.d.cts CHANGED
@@ -90,7 +90,15 @@ interface ChartOptions {
90
90
  drawings?: DrawingObjectOptions[];
91
91
  }
92
92
  type IndicatorPane = "overlay" | "separate";
93
- type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "rectangle" | "text" | "note";
93
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "rectangle" | "text" | "note"
94
+ /**
95
+ * TradingView-style ruler. Unlike every other tool it is transient: the
96
+ * measurement overlay is never added to the drawings list (no
97
+ * `onDrawingsChange`), and it clears on the next click, on Escape, or when
98
+ * the tool changes. Also reachable without arming the tool via Shift+drag
99
+ * on the plot.
100
+ */
101
+ | "measure";
94
102
  interface DrawingPoint {
95
103
  index: number;
96
104
  price: number;
@@ -552,6 +560,14 @@ interface ChartInstance {
552
560
  removeDrawing: (id: string) => void;
553
561
  clearDrawings: () => void;
554
562
  onDrawingsChange: (handler: ((drawings: DrawingObjectOptions[]) => void) | null) => void;
563
+ /**
564
+ * Fires whenever the active drawing tool changes — including when the chart
565
+ * auto-reverts to the cursor after a tool completes (every drawing tool does
566
+ * this once its shape is placed; the transient `measure` tool does it after
567
+ * every measurement). Lets host toolbars stay in sync without polling
568
+ * `getActiveDrawingTool()`.
569
+ */
570
+ onActiveDrawingToolChange: (handler: ((tool: DrawingToolType | null) => void) | null) => void;
555
571
  onDrawingSelect: (handler: ((event: DrawingSelectEvent) => void) | null) => void;
556
572
  onDrawingDoubleClick: (handler: ((event: DrawingSelectEvent) => void) | null) => void;
557
573
  onDrawingEditText: (handler: ((event: DrawingSelectEvent) => void) | null) => void;
package/dist/index.d.ts CHANGED
@@ -90,7 +90,15 @@ interface ChartOptions {
90
90
  drawings?: DrawingObjectOptions[];
91
91
  }
92
92
  type IndicatorPane = "overlay" | "separate";
93
- type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "rectangle" | "text" | "note";
93
+ type DrawingToolType = "horizontal-line" | "vertical-line" | "trendline" | "ray" | "fib-retracement" | "fib-extension" | "long-position" | "short-position" | "price-range" | "rectangle" | "text" | "note"
94
+ /**
95
+ * TradingView-style ruler. Unlike every other tool it is transient: the
96
+ * measurement overlay is never added to the drawings list (no
97
+ * `onDrawingsChange`), and it clears on the next click, on Escape, or when
98
+ * the tool changes. Also reachable without arming the tool via Shift+drag
99
+ * on the plot.
100
+ */
101
+ | "measure";
94
102
  interface DrawingPoint {
95
103
  index: number;
96
104
  price: number;
@@ -552,6 +560,14 @@ interface ChartInstance {
552
560
  removeDrawing: (id: string) => void;
553
561
  clearDrawings: () => void;
554
562
  onDrawingsChange: (handler: ((drawings: DrawingObjectOptions[]) => void) | null) => void;
563
+ /**
564
+ * Fires whenever the active drawing tool changes — including when the chart
565
+ * auto-reverts to the cursor after a tool completes (every drawing tool does
566
+ * this once its shape is placed; the transient `measure` tool does it after
567
+ * every measurement). Lets host toolbars stay in sync without polling
568
+ * `getActiveDrawingTool()`.
569
+ */
570
+ onActiveDrawingToolChange: (handler: ((tool: DrawingToolType | null) => void) | null) => void;
555
571
  onDrawingSelect: (handler: ((event: DrawingSelectEvent) => void) | null) => void;
556
572
  onDrawingDoubleClick: (handler: ((event: DrawingSelectEvent) => void) | null) => void;
557
573
  onDrawingEditText: (handler: ((event: DrawingSelectEvent) => void) | null) => void;
package/dist/index.js CHANGED
@@ -2940,8 +2940,11 @@ function createChart(element, options = {}) {
2940
2940
  );
2941
2941
  let activeDrawingTool = null;
2942
2942
  let draftDrawing = null;
2943
+ let measureState = null;
2943
2944
  let drawingDragState = null;
2944
2945
  let drawingsChangeHandler = null;
2946
+ let drawingToolChangeHandler = null;
2947
+ let lastNotifiedDrawingTool = null;
2945
2948
  let drawingSelectHandler = null;
2946
2949
  let drawingDoubleClickHandler = null;
2947
2950
  let drawingEditTextHandler = null;
@@ -3534,6 +3537,13 @@ function createChart(element, options = {}) {
3534
3537
  points: draftDrawing.points.map((point) => reanchorDrawingPoint(point))
3535
3538
  };
3536
3539
  }
3540
+ if (measureState) {
3541
+ measureState = {
3542
+ ...measureState,
3543
+ start: reanchorDrawingPoint(measureState.start),
3544
+ end: reanchorDrawingPoint(measureState.end)
3545
+ };
3546
+ }
3537
3547
  };
3538
3548
  const getCandleDirectionByIndex = (index) => {
3539
3549
  const point = data[index];
@@ -3635,6 +3645,22 @@ function createChart(element, options = {}) {
3635
3645
  const pad = (value) => String(value).padStart(2, "0");
3636
3646
  return hours > 0 ? `${hours}:${pad(minutes)}:${pad(seconds)}` : `${pad(minutes)}:${pad(seconds)}`;
3637
3647
  };
3648
+ const formatTimeSpan = (ms) => {
3649
+ const totalMinutes = Math.max(0, Math.round(ms / 6e4));
3650
+ if (totalMinutes < 1) {
3651
+ return `${Math.max(1, Math.round(ms / 1e3))}s`;
3652
+ }
3653
+ const days = Math.floor(totalMinutes / 1440);
3654
+ const hours = Math.floor(totalMinutes % 1440 / 60);
3655
+ const minutes = totalMinutes % 60;
3656
+ if (days > 0) {
3657
+ return hours > 0 ? `${days}d ${hours}h` : `${days}d`;
3658
+ }
3659
+ if (hours > 0) {
3660
+ return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`;
3661
+ }
3662
+ return `${minutes}m`;
3663
+ };
3638
3664
  const getRightAxisLabelX = (chartRight) => chartRight + 1;
3639
3665
  const getRightAxisLabelWidth = (chartRight, contentWidth) => {
3640
3666
  const scaleWidth = Math.max(0, Math.floor(width - getRightAxisLabelX(chartRight)));
@@ -4003,6 +4029,10 @@ function createChart(element, options = {}) {
4003
4029
  let pendingDrawUpdateAutoScale = false;
4004
4030
  let pendingDrawOverlayOnly = true;
4005
4031
  const scheduleDraw = (options2 = {}) => {
4032
+ if (activeDrawingTool !== lastNotifiedDrawingTool) {
4033
+ lastNotifiedDrawingTool = activeDrawingTool;
4034
+ drawingToolChangeHandler?.(activeDrawingTool);
4035
+ }
4006
4036
  armCountdownHeartbeat();
4007
4037
  const overlayOnly = options2.overlayOnly ?? false;
4008
4038
  pendingDrawOverlayOnly = pendingDrawOverlayOnly && overlayOnly;
@@ -4815,7 +4845,7 @@ function createChart(element, options = {}) {
4815
4845
  const t0 = getTimeForIndex(Math.round(p0.index));
4816
4846
  const t1 = getTimeForIndex(Math.round(p1.index));
4817
4847
  const spanMs = t0 && t1 ? Math.abs(t1.getTime() - t0.getTime()) : 0;
4818
- labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatDuration(spanMs)}` : `${barSpan} bars`);
4848
+ labelLines.push(spanMs > 0 ? `${barSpan} bars, ${formatTimeSpan(spanMs)}` : `${barSpan} bars`);
4819
4849
  }
4820
4850
  const pointValue = Number(drawing.pointValue);
4821
4851
  if (Number.isFinite(pointValue) && pointValue > 0) {
@@ -5292,6 +5322,96 @@ function createChart(element, options = {}) {
5292
5322
  if (draftDrawing) {
5293
5323
  drawDrawing(draftDrawing, true);
5294
5324
  }
5325
+ if (measureState) {
5326
+ const mStart = measureState.start;
5327
+ const mEnd = measureState.end;
5328
+ const mx0 = xFromDrawingPoint(mStart);
5329
+ const mx1 = xFromDrawingPoint(mEnd);
5330
+ const my0 = yFromPrice(mStart.price);
5331
+ const my1 = yFromPrice(mEnd.price);
5332
+ const mLeft = Math.min(mx0, mx1);
5333
+ const mRight = Math.max(mx0, mx1);
5334
+ const mTop = Math.min(my0, my1);
5335
+ const mBottom = Math.max(my0, my1);
5336
+ const mUp = mEnd.price >= mStart.price;
5337
+ const mColor = mUp ? "#2962ff" : "#f23645";
5338
+ ctx.save();
5339
+ ctx.fillStyle = hexToRgba(mColor, 0.12);
5340
+ ctx.fillRect(mLeft, mTop, Math.max(1, mRight - mLeft), Math.max(1, mBottom - mTop));
5341
+ ctx.strokeStyle = mColor;
5342
+ ctx.lineWidth = 1;
5343
+ ctx.setLineDash([]);
5344
+ const mArrowHead = 5;
5345
+ const mMidX = (mLeft + mRight) / 2;
5346
+ if (Math.abs(my1 - my0) > mArrowHead * 2) {
5347
+ ctx.beginPath();
5348
+ ctx.moveTo(crisp(mMidX), crisp(my0));
5349
+ ctx.lineTo(crisp(mMidX), crisp(my1));
5350
+ ctx.stroke();
5351
+ const dirY = my1 > my0 ? 1 : -1;
5352
+ ctx.beginPath();
5353
+ ctx.moveTo(mMidX, my1);
5354
+ ctx.lineTo(mMidX - mArrowHead, my1 - dirY * mArrowHead);
5355
+ ctx.moveTo(mMidX, my1);
5356
+ ctx.lineTo(mMidX + mArrowHead, my1 - dirY * mArrowHead);
5357
+ ctx.stroke();
5358
+ }
5359
+ const mMidY = (mTop + mBottom) / 2;
5360
+ if (Math.abs(mx1 - mx0) > mArrowHead * 2) {
5361
+ ctx.beginPath();
5362
+ ctx.moveTo(crisp(mx0), crisp(mMidY));
5363
+ ctx.lineTo(crisp(mx1), crisp(mMidY));
5364
+ ctx.stroke();
5365
+ const dirX = mx1 > mx0 ? 1 : -1;
5366
+ ctx.beginPath();
5367
+ ctx.moveTo(mx1, mMidY);
5368
+ ctx.lineTo(mx1 - dirX * mArrowHead, mMidY - mArrowHead);
5369
+ ctx.moveTo(mx1, mMidY);
5370
+ ctx.lineTo(mx1 - dirX * mArrowHead, mMidY + mArrowHead);
5371
+ ctx.stroke();
5372
+ }
5373
+ const mDiff = mEnd.price - mStart.price;
5374
+ const mBase = Math.abs(mStart.price) > 0 ? Math.abs(mStart.price) : 1;
5375
+ const mPct = mDiff / mBase * 100;
5376
+ const mTick = getConfiguredTickSize();
5377
+ const mTicks = mTick > 0 ? Math.round(mDiff / mTick) : 0;
5378
+ const mSigned = (value, text) => `${value < 0 ? "\u2212" : value > 0 ? "+" : ""}${text}`;
5379
+ const mLines = [
5380
+ mTick > 0 ? `${mSigned(mDiff, formatPrice(Math.abs(mDiff)))} (${mSigned(mPct, `${Math.abs(mPct).toFixed(2)}%`)}) ${mSigned(mTicks, String(Math.abs(mTicks)))}` : `${mSigned(mDiff, formatPrice(Math.abs(mDiff)))} (${mSigned(mPct, `${Math.abs(mPct).toFixed(2)}%`)})`
5381
+ ];
5382
+ const mBars = Math.round(mEnd.index) - Math.round(mStart.index);
5383
+ const mT0 = getTimeForIndex(Math.round(mStart.index));
5384
+ const mT1 = getTimeForIndex(Math.round(mEnd.index));
5385
+ const mSpanMs = mT0 && mT1 ? Math.abs(mT1.getTime() - mT0.getTime()) : 0;
5386
+ mLines.push(
5387
+ mSpanMs > 0 ? `${mSigned(mBars, String(Math.abs(mBars)))} bars, ${formatTimeSpan(mSpanMs)}` : `${mSigned(mBars, String(Math.abs(mBars)))} bars`
5388
+ );
5389
+ const prevMeasureFont = ctx.font;
5390
+ ctx.font = `500 11px ${mergedOptions.fontFamily}`;
5391
+ const mPad = 7;
5392
+ const mLineHeight = 15;
5393
+ const mTextW = Math.max(...mLines.map((line) => measureTextWidth(line)));
5394
+ const mPillW = mTextW + mPad * 2;
5395
+ const mPillH = mLines.length * mLineHeight + mPad;
5396
+ const mPillX = clamp(mMidX - mPillW / 2, chartLeft + 2, chartLeft + chartWidth - mPillW - 2);
5397
+ let mPillY = mUp ? mTop - 8 - mPillH : mBottom + 8;
5398
+ if (mPillY < chartTop + 2) {
5399
+ mPillY = mBottom + 8;
5400
+ }
5401
+ if (mPillY + mPillH > fullChartBottom - 2) {
5402
+ mPillY = Math.max(chartTop + 2, mTop - 8 - mPillH);
5403
+ }
5404
+ ctx.fillStyle = mColor;
5405
+ fillRoundedRect(mPillX, mPillY, mPillW, mPillH, 4);
5406
+ ctx.fillStyle = "#ffffff";
5407
+ ctx.textAlign = "center";
5408
+ ctx.textBaseline = "middle";
5409
+ mLines.forEach((line, lineIndex) => {
5410
+ ctx.fillText(line, mPillX + mPillW / 2, mPillY + mPad / 2 + lineIndex * mLineHeight + mLineHeight / 2);
5411
+ });
5412
+ ctx.font = prevMeasureFont;
5413
+ ctx.restore();
5414
+ }
5295
5415
  if (tradeMarkers.length > 0 && data.length > 0) {
5296
5416
  const visibleStart = Math.floor(xStart) - 1;
5297
5417
  const visibleEnd = Math.ceil(xStart + xSpan) + 1;
@@ -7290,6 +7410,19 @@ function createChart(element, options = {}) {
7290
7410
  return;
7291
7411
  }
7292
7412
  }
7413
+ if (measureState) {
7414
+ if (measureState.dragging) {
7415
+ measureState = { ...measureState, dragging: false };
7416
+ if (activeDrawingTool === "measure") {
7417
+ activeDrawingTool = null;
7418
+ canvas.style.cursor = "default";
7419
+ }
7420
+ scheduleDraw();
7421
+ return;
7422
+ }
7423
+ measureState = null;
7424
+ scheduleDraw();
7425
+ }
7293
7426
  const drawingToolCapturesPointer = activeDrawingTool !== null || draftDrawing !== null;
7294
7427
  if (!drawingToolCapturesPointer) {
7295
7428
  const crosshairButtonRegion = getCrosshairPriceActionRegion(point.x, point.y);
@@ -7364,6 +7497,18 @@ function createChart(element, options = {}) {
7364
7497
  if (region === "outside") {
7365
7498
  return;
7366
7499
  }
7500
+ if (region === "plot" && (activeDrawingTool === "measure" || activeDrawingTool === null && draftDrawing === null && event.shiftKey && event.pointerType === "mouse")) {
7501
+ const measurePoint = drawingPointFromCanvas(point.x, point.y);
7502
+ if (measurePoint) {
7503
+ measureState = { start: measurePoint, end: measurePoint, dragging: true };
7504
+ activePointerId = event.pointerId;
7505
+ capturePointer(event.pointerId);
7506
+ setCrosshairPoint(null);
7507
+ canvas.style.cursor = "crosshair";
7508
+ scheduleDraw();
7509
+ return;
7510
+ }
7511
+ }
7367
7512
  if (region === "plot" && !activeDrawingTool) {
7368
7513
  const drawingHit = getDrawingHit(point.x, point.y);
7369
7514
  if (drawingHit) {
@@ -7459,6 +7604,19 @@ function createChart(element, options = {}) {
7459
7604
  }
7460
7605
  return;
7461
7606
  }
7607
+ if (measureState?.dragging) {
7608
+ if (activePointerId !== null && event.pointerId !== activePointerId) {
7609
+ return;
7610
+ }
7611
+ const nextMeasurePoint = drawingPointFromCanvas(point.x, point.y);
7612
+ if (nextMeasurePoint) {
7613
+ measureState = { ...measureState, end: nextMeasurePoint };
7614
+ setCrosshairPoint(null);
7615
+ canvas.style.cursor = "crosshair";
7616
+ scheduleDraw();
7617
+ }
7618
+ return;
7619
+ }
7462
7620
  if (drawingDragState) {
7463
7621
  if (activePointerId !== null && event.pointerId !== activePointerId) {
7464
7622
  return;
@@ -7689,6 +7847,24 @@ function createChart(element, options = {}) {
7689
7847
  if (event && activePointerId !== null && event.pointerId !== activePointerId) {
7690
7848
  return;
7691
7849
  }
7850
+ if (measureState?.dragging) {
7851
+ const movedMeasure = measureState.start.index !== measureState.end.index || measureState.start.price !== measureState.end.price;
7852
+ if (movedMeasure) {
7853
+ measureState = { ...measureState, dragging: false };
7854
+ if (activeDrawingTool === "measure") {
7855
+ activeDrawingTool = null;
7856
+ }
7857
+ canvas.style.cursor = "default";
7858
+ } else {
7859
+ canvas.style.cursor = "crosshair";
7860
+ }
7861
+ isDragging = false;
7862
+ dragMode = null;
7863
+ activePointerId = null;
7864
+ pointerDownInfo = null;
7865
+ scheduleDraw();
7866
+ return;
7867
+ }
7692
7868
  if (orderDragState) {
7693
7869
  const moved = orderDragState.lastPrice !== orderDragState.startPrice;
7694
7870
  const finalLine = orderLines.find((line) => line.id === orderDragState?.orderId);
@@ -7869,6 +8045,15 @@ function createChart(element, options = {}) {
7869
8045
  canvas.addEventListener("dblclick", onDoubleClick);
7870
8046
  canvas.addEventListener("contextmenu", onContextMenu);
7871
8047
  const onModifierKeyChange = (event) => {
8048
+ if (event.key === "Escape" && event.type === "keydown" && measureState) {
8049
+ measureState = null;
8050
+ if (activeDrawingTool === "measure") {
8051
+ activeDrawingTool = null;
8052
+ canvas.style.cursor = "default";
8053
+ }
8054
+ scheduleDraw();
8055
+ return;
8056
+ }
7872
8057
  const active = event.metaKey || event.ctrlKey;
7873
8058
  const shift = event.shiftKey;
7874
8059
  const changed = active !== magnetModifierActive || shift !== shiftKeyActive;
@@ -8181,17 +8366,29 @@ function createChart(element, options = {}) {
8181
8366
  const setActiveDrawingTool = (tool) => {
8182
8367
  activeDrawingTool = tool;
8183
8368
  draftDrawing = null;
8369
+ measureState = null;
8184
8370
  canvas.style.cursor = tool ? "crosshair" : "default";
8185
8371
  scheduleDraw();
8186
8372
  };
8187
8373
  const getActiveDrawingTool = () => activeDrawingTool;
8188
8374
  const cancelDrawing = () => {
8375
+ let cancelled = false;
8189
8376
  if (draftDrawing) {
8190
8377
  draftDrawing = null;
8378
+ cancelled = true;
8379
+ }
8380
+ if (measureState) {
8381
+ measureState = null;
8382
+ if (activeDrawingTool === "measure") {
8383
+ activeDrawingTool = null;
8384
+ canvas.style.cursor = "default";
8385
+ }
8386
+ cancelled = true;
8387
+ }
8388
+ if (cancelled) {
8191
8389
  scheduleDraw();
8192
- return true;
8193
8390
  }
8194
- return false;
8391
+ return cancelled;
8195
8392
  };
8196
8393
  const setTradeMarkers = (markers) => {
8197
8394
  tradeMarkers = Array.isArray(markers) ? markers.slice() : [];
@@ -8243,6 +8440,9 @@ function createChart(element, options = {}) {
8243
8440
  const onDrawingsChange = (handler) => {
8244
8441
  drawingsChangeHandler = handler;
8245
8442
  };
8443
+ const onActiveDrawingToolChange = (handler) => {
8444
+ drawingToolChangeHandler = handler;
8445
+ };
8246
8446
  const onDrawingSelect = (handler) => {
8247
8447
  drawingSelectHandler = handler;
8248
8448
  };
@@ -8329,6 +8529,7 @@ function createChart(element, options = {}) {
8329
8529
  removeDrawing,
8330
8530
  clearDrawings,
8331
8531
  onDrawingsChange,
8532
+ onActiveDrawingToolChange,
8332
8533
  onDrawingSelect,
8333
8534
  onDrawingDoubleClick,
8334
8535
  onDrawingEditText,