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/hyperprop-charting-library.cjs +204 -3
- package/dist/hyperprop-charting-library.d.ts +17 -1
- package/dist/hyperprop-charting-library.js +204 -3
- package/dist/index.cjs +204 -3
- package/dist/index.d.cts +17 -1
- package/dist/index.d.ts +17 -1
- package/dist/index.js +204 -3
- package/docs/API.md +18 -1
- package/package.json +1 -1
|
@@ -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, ${
|
|
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
|
|
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,
|
|
@@ -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;
|
|
@@ -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, ${
|
|
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
|
|
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,
|