@pipsend/charts 2.2.1 → 2.3.1
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/README.md +28 -0
- package/dist/pipsend-charts.development.mjs +1189 -116
- package/dist/pipsend-charts.production.mjs +2 -2
- package/dist/pipsend-charts.standalone.development.js +1190 -115
- package/dist/pipsend-charts.standalone.development.mjs +1189 -116
- package/dist/pipsend-charts.standalone.production.js +2 -2
- package/dist/pipsend-charts.standalone.production.mjs +2 -2
- package/dist/typings.d.ts +348 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/*!
|
|
2
2
|
* @license
|
|
3
|
-
* Pipsend Charts v2.
|
|
3
|
+
* Pipsend Charts v2.3.1
|
|
4
4
|
* Copyright (c) 2026 Pipsend
|
|
5
5
|
* Licensed under MIT License
|
|
6
6
|
* Built on TradingView Lightweight Charts™ (Apache 2.0)
|
|
@@ -14245,6 +14245,8 @@ function shapeSize(shape, originalSize) {
|
|
|
14245
14245
|
return size(originalSize, 0.8);
|
|
14246
14246
|
case 'square':
|
|
14247
14247
|
return size(originalSize, 0.7);
|
|
14248
|
+
case 'tag':
|
|
14249
|
+
return size(originalSize, 1);
|
|
14248
14250
|
}
|
|
14249
14251
|
}
|
|
14250
14252
|
function calculateShapeHeight(barSpacing) {
|
|
@@ -14354,6 +14356,85 @@ function hitTestSquare(centerX, centerY, size, x, y) {
|
|
|
14354
14356
|
y >= top && y <= top + squareSize;
|
|
14355
14357
|
}
|
|
14356
14358
|
|
|
14359
|
+
const TAIL_HALF_WIDTH_RATIO = 0.62;
|
|
14360
|
+
/**
|
|
14361
|
+
* Lays the pointer into the edge the path is already travelling along, so the
|
|
14362
|
+
* bubble and its tail are a single outline. Two subpaths would leave a stroked
|
|
14363
|
+
* seam where they meet.
|
|
14364
|
+
*/
|
|
14365
|
+
// eslint-disable-next-line max-params
|
|
14366
|
+
function tailPath(ctx, xCenter, edgeY, tailHeight, halfWidth, up) {
|
|
14367
|
+
// A tip with a small radius reads as drawn rather than as an artefact of the
|
|
14368
|
+
// rasteriser, which a bare triangle does at these sizes.
|
|
14369
|
+
const tipRadius = Math.min(halfWidth / 3, tailHeight / 3);
|
|
14370
|
+
const sign = up ? -1 : 1;
|
|
14371
|
+
const tipY = edgeY + sign * tailHeight;
|
|
14372
|
+
const shoulderY = tipY - sign * tipRadius;
|
|
14373
|
+
// The top edge is travelled left to right, the bottom edge right to left.
|
|
14374
|
+
const entry = up ? -halfWidth : halfWidth;
|
|
14375
|
+
ctx.lineTo(xCenter + entry, edgeY);
|
|
14376
|
+
ctx.lineTo(xCenter + (up ? -tipRadius : tipRadius), shoulderY);
|
|
14377
|
+
ctx.quadraticCurveTo(xCenter, tipY, xCenter + (up ? tipRadius : -tipRadius), shoulderY);
|
|
14378
|
+
ctx.lineTo(xCenter - entry, edgeY);
|
|
14379
|
+
}
|
|
14380
|
+
/**
|
|
14381
|
+
* Draws a rounded speech-bubble tag with an optional pointer towards its anchor.
|
|
14382
|
+
*/
|
|
14383
|
+
function drawTag(ctx, params) {
|
|
14384
|
+
const hpr = params._internal_horizontalPixelRatio;
|
|
14385
|
+
const vpr = params._internal_verticalPixelRatio;
|
|
14386
|
+
const left = Math.round((params._internal_x - params._internal_width / 2) * hpr);
|
|
14387
|
+
const right = Math.round((params._internal_x + params._internal_width / 2) * hpr);
|
|
14388
|
+
const top = Math.round((params._internal_centerY - params._internal_height / 2) * vpr);
|
|
14389
|
+
const bottom = Math.round((params._internal_centerY + params._internal_height / 2) * vpr);
|
|
14390
|
+
const xCenter = Math.round(params._internal_x * hpr);
|
|
14391
|
+
const radius = Math.min(params._internal_radius * hpr, (right - left) / 2, (bottom - top) / 2);
|
|
14392
|
+
const tailHeight = params._internal_tailHeight * vpr;
|
|
14393
|
+
const tailHalfWidth = Math.min(tailHeight * TAIL_HALF_WIDTH_RATIO, Math.max(0, (right - left) / 2 - radius));
|
|
14394
|
+
const hasTail = params._internal_tailDirection !== 'none' && tailHalfWidth > 0 && tailHeight > 0;
|
|
14395
|
+
ctx.save();
|
|
14396
|
+
ctx.beginPath();
|
|
14397
|
+
if (!hasTail) {
|
|
14398
|
+
drawRoundRect(ctx, left, top, right - left, bottom - top, [radius, radius, radius, radius]);
|
|
14399
|
+
}
|
|
14400
|
+
else {
|
|
14401
|
+
ctx.moveTo(left + radius, top);
|
|
14402
|
+
if (params._internal_tailDirection === 'up') {
|
|
14403
|
+
tailPath(ctx, xCenter, top, tailHeight, tailHalfWidth, true);
|
|
14404
|
+
}
|
|
14405
|
+
ctx.lineTo(right - radius, top);
|
|
14406
|
+
ctx.arcTo(right, top, right, top + radius, radius);
|
|
14407
|
+
ctx.lineTo(right, bottom - radius);
|
|
14408
|
+
ctx.arcTo(right, bottom, right - radius, bottom, radius);
|
|
14409
|
+
if (params._internal_tailDirection === 'down') {
|
|
14410
|
+
tailPath(ctx, xCenter, bottom, tailHeight, tailHalfWidth, false);
|
|
14411
|
+
}
|
|
14412
|
+
ctx.lineTo(left + radius, bottom);
|
|
14413
|
+
ctx.arcTo(left, bottom, left, bottom - radius, radius);
|
|
14414
|
+
ctx.lineTo(left, top + radius);
|
|
14415
|
+
ctx.arcTo(left, top, left + radius, top, radius);
|
|
14416
|
+
}
|
|
14417
|
+
ctx.closePath();
|
|
14418
|
+
ctx.fillStyle = params._internal_fillColor;
|
|
14419
|
+
ctx.fill();
|
|
14420
|
+
if (params._internal_borderColor !== undefined && params._internal_borderWidth > 0) {
|
|
14421
|
+
ctx.lineWidth = Math.max(1, Math.round(params._internal_borderWidth * hpr));
|
|
14422
|
+
ctx.strokeStyle = params._internal_borderColor;
|
|
14423
|
+
ctx.stroke();
|
|
14424
|
+
}
|
|
14425
|
+
ctx.restore();
|
|
14426
|
+
}
|
|
14427
|
+
function hitTestTag(centerX, centerY, tag, x, y) {
|
|
14428
|
+
if (tag._internal_width <= 0) {
|
|
14429
|
+
return false;
|
|
14430
|
+
}
|
|
14431
|
+
const halfHeight = tag._internal_height / 2;
|
|
14432
|
+
const top = tag._internal_tailDirection === 'up' ? centerY - halfHeight - tag._internal_tailHeight : centerY - halfHeight;
|
|
14433
|
+
const bottom = tag._internal_tailDirection === 'down' ? centerY + halfHeight + tag._internal_tailHeight : centerY + halfHeight;
|
|
14434
|
+
const left = centerX - tag._internal_width / 2;
|
|
14435
|
+
return x >= left && x <= left + tag._internal_width && y >= top && y <= bottom;
|
|
14436
|
+
}
|
|
14437
|
+
|
|
14357
14438
|
function drawText(ctx, text, x, y, horizontalPixelRatio, verticalPixelRatio) {
|
|
14358
14439
|
ctx.save();
|
|
14359
14440
|
ctx.scale(horizontalPixelRatio, verticalPixelRatio);
|
|
@@ -14370,9 +14451,11 @@ class SeriesMarkersRenderer {
|
|
|
14370
14451
|
constructor() {
|
|
14371
14452
|
this._private__data = null;
|
|
14372
14453
|
this._private__textWidthCache = new TextWidthCache();
|
|
14454
|
+
this._private__tagTextWidthCache = new TextWidthCache();
|
|
14373
14455
|
this._private__fontSize = -1;
|
|
14374
14456
|
this._private__fontFamily = '';
|
|
14375
14457
|
this._private__font = '';
|
|
14458
|
+
this._private__tagFont = '';
|
|
14376
14459
|
this._private__zOrder = 'normal';
|
|
14377
14460
|
}
|
|
14378
14461
|
_internal_setData(data) {
|
|
@@ -14383,7 +14466,11 @@ class SeriesMarkersRenderer {
|
|
|
14383
14466
|
this._private__fontSize = fontSize;
|
|
14384
14467
|
this._private__fontFamily = fontFamily;
|
|
14385
14468
|
this._private__font = makeFont(fontSize, fontFamily);
|
|
14469
|
+
// Tags carry a label rather than an annotation, so they get the heavier
|
|
14470
|
+
// weight — and their own cache, since widths differ per font.
|
|
14471
|
+
this._private__tagFont = makeFont(fontSize, fontFamily, '600');
|
|
14386
14472
|
this._private__textWidthCache._internal_reset();
|
|
14473
|
+
this._private__tagTextWidthCache._internal_reset();
|
|
14387
14474
|
}
|
|
14388
14475
|
this._private__zOrder = zOrder;
|
|
14389
14476
|
}
|
|
@@ -14423,14 +14510,21 @@ class SeriesMarkersRenderer {
|
|
|
14423
14510
|
return;
|
|
14424
14511
|
}
|
|
14425
14512
|
ctx.textBaseline = 'middle';
|
|
14426
|
-
ctx.font = this._private__font;
|
|
14427
14513
|
for (let index = this._private__data._internal_visibleRange.from; index < this._private__data._internal_visibleRange.to; index++) {
|
|
14428
14514
|
const item = this._private__data._internal_items[index];
|
|
14515
|
+
const isTag = item._internal_tag !== undefined;
|
|
14516
|
+
ctx.font = isTag ? this._private__tagFont : this._private__font;
|
|
14429
14517
|
if (item._internal_text !== undefined) {
|
|
14430
|
-
|
|
14518
|
+
const cache = isTag ? this._private__tagTextWidthCache : this._private__textWidthCache;
|
|
14519
|
+
item._internal_text._internal_width = cache._internal_measureText(ctx, item._internal_text._internal_content);
|
|
14431
14520
|
item._internal_text._internal_height = this._private__fontSize;
|
|
14432
14521
|
item._internal_text._internal_x = item._internal_x - item._internal_text._internal_width / 2;
|
|
14433
14522
|
}
|
|
14523
|
+
if (item._internal_tag !== undefined) {
|
|
14524
|
+
const textWidth = item._internal_text !== undefined ? item._internal_text._internal_width : 0;
|
|
14525
|
+
item._internal_tag._internal_width = Math.max(textWidth + item._internal_tag._internal_paddingX * 2, item._internal_tag._internal_height);
|
|
14526
|
+
item._internal_tag._internal_left = item._internal_x - item._internal_tag._internal_width / 2;
|
|
14527
|
+
}
|
|
14434
14528
|
drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio);
|
|
14435
14529
|
}
|
|
14436
14530
|
}
|
|
@@ -14445,12 +14539,40 @@ function bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRat
|
|
|
14445
14539
|
};
|
|
14446
14540
|
}
|
|
14447
14541
|
function drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio) {
|
|
14542
|
+
if (item._internal_tag !== undefined) {
|
|
14543
|
+
drawTagItem(item, item._internal_tag, ctx, horizontalPixelRatio, verticalPixelRatio);
|
|
14544
|
+
return;
|
|
14545
|
+
}
|
|
14448
14546
|
ctx.fillStyle = item._internal_color;
|
|
14449
14547
|
if (item._internal_text !== undefined) {
|
|
14450
14548
|
drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
|
|
14451
14549
|
}
|
|
14452
14550
|
drawShape(item, ctx, bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRatio));
|
|
14453
14551
|
}
|
|
14552
|
+
// eslint-disable-next-line max-params
|
|
14553
|
+
function drawTagItem(item, tag, ctx, horizontalPixelRatio, verticalPixelRatio) {
|
|
14554
|
+
if (item._internal_size === 0) {
|
|
14555
|
+
return;
|
|
14556
|
+
}
|
|
14557
|
+
drawTag(ctx, {
|
|
14558
|
+
_internal_x: item._internal_x,
|
|
14559
|
+
_internal_centerY: item._internal_y,
|
|
14560
|
+
_internal_width: tag._internal_width,
|
|
14561
|
+
_internal_height: tag._internal_height,
|
|
14562
|
+
_internal_tailHeight: tag._internal_tailHeight,
|
|
14563
|
+
_internal_tailDirection: tag._internal_tailDirection,
|
|
14564
|
+
_internal_radius: tag._internal_radius,
|
|
14565
|
+
_internal_fillColor: item._internal_color,
|
|
14566
|
+
_internal_borderColor: item._internal_borderColor,
|
|
14567
|
+
_internal_borderWidth: item._internal_borderWidth ?? 1,
|
|
14568
|
+
_internal_horizontalPixelRatio: horizontalPixelRatio,
|
|
14569
|
+
_internal_verticalPixelRatio: verticalPixelRatio,
|
|
14570
|
+
});
|
|
14571
|
+
if (item._internal_text !== undefined) {
|
|
14572
|
+
ctx.fillStyle = item._internal_textColor ?? '#FFFFFF';
|
|
14573
|
+
drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
|
|
14574
|
+
}
|
|
14575
|
+
}
|
|
14454
14576
|
function drawShape(item, ctx, coordinates) {
|
|
14455
14577
|
if (item._internal_size === 0) {
|
|
14456
14578
|
return;
|
|
@@ -14468,10 +14590,16 @@ function drawShape(item, ctx, coordinates) {
|
|
|
14468
14590
|
case 'square':
|
|
14469
14591
|
drawSquare(ctx, coordinates, item._internal_size);
|
|
14470
14592
|
return;
|
|
14593
|
+
case 'tag':
|
|
14594
|
+
// Handled by drawTagItem, which needs both pixel ratios and the text.
|
|
14595
|
+
return;
|
|
14471
14596
|
}
|
|
14472
14597
|
ensureNever(item._internal_shape);
|
|
14473
14598
|
}
|
|
14474
14599
|
function hitTestItem(item, x, y) {
|
|
14600
|
+
if (item._internal_tag !== undefined) {
|
|
14601
|
+
return item._internal_size !== 0 && hitTestTag(item._internal_x, item._internal_y, item._internal_tag, x, y);
|
|
14602
|
+
}
|
|
14475
14603
|
if (item._internal_text !== undefined && hitTestText(item._internal_text._internal_x, item._internal_text._internal_y, item._internal_text._internal_width, item._internal_text._internal_height, x, y)) {
|
|
14476
14604
|
return true;
|
|
14477
14605
|
}
|
|
@@ -14490,6 +14618,8 @@ function hitTestShape(item, x, y) {
|
|
|
14490
14618
|
return hitTestCircle(item._internal_x, item._internal_y, item._internal_size, x, y);
|
|
14491
14619
|
case 'square':
|
|
14492
14620
|
return hitTestSquare(item._internal_x, item._internal_y, item._internal_size, x, y);
|
|
14621
|
+
case 'tag':
|
|
14622
|
+
return false;
|
|
14493
14623
|
}
|
|
14494
14624
|
}
|
|
14495
14625
|
|
|
@@ -14522,15 +14652,71 @@ function getPrice(seriesData, marker, isInverted) {
|
|
|
14522
14652
|
}
|
|
14523
14653
|
return;
|
|
14524
14654
|
}
|
|
14655
|
+
/**
|
|
14656
|
+
* Lays out a `tag` marker: the bubble carries the text, so it is measured from
|
|
14657
|
+
* the font rather than from the bar spacing, and its pointer is aimed back at
|
|
14658
|
+
* the anchor price.
|
|
14659
|
+
*/
|
|
14660
|
+
// eslint-disable-next-line max-params
|
|
14661
|
+
function fillTagSizeAndY(rendererItem, marker, anchorY, offsets, textHeight, shapeMargin, sizeMultiplier) {
|
|
14662
|
+
const height = Math.round((textHeight + 5 /* Constants.TagPaddingY */ * 2) * sizeMultiplier);
|
|
14663
|
+
const tailHeight = Math.round(7 /* Constants.TagTailHeight */ * sizeMultiplier);
|
|
14664
|
+
const ignoreOffset = isPriceMarker(marker.position);
|
|
14665
|
+
const halfHeight = height / 2;
|
|
14666
|
+
let tailDirection = 'none';
|
|
14667
|
+
let centerY = anchorY;
|
|
14668
|
+
switch (marker.position) {
|
|
14669
|
+
case 'aboveBar':
|
|
14670
|
+
case 'atPriceTop': {
|
|
14671
|
+
const offset = ignoreOffset ? 0 : offsets._internal_aboveBar;
|
|
14672
|
+
tailDirection = 'down';
|
|
14673
|
+
centerY = (anchorY - offset - tailHeight - halfHeight);
|
|
14674
|
+
if (!ignoreOffset) {
|
|
14675
|
+
offsets._internal_aboveBar += height + tailHeight + shapeMargin;
|
|
14676
|
+
}
|
|
14677
|
+
break;
|
|
14678
|
+
}
|
|
14679
|
+
case 'belowBar':
|
|
14680
|
+
case 'atPriceBottom': {
|
|
14681
|
+
const offset = ignoreOffset ? 0 : offsets._internal_belowBar;
|
|
14682
|
+
tailDirection = 'up';
|
|
14683
|
+
centerY = (anchorY + offset + tailHeight + halfHeight);
|
|
14684
|
+
if (!ignoreOffset) {
|
|
14685
|
+
offsets._internal_belowBar += height + tailHeight + shapeMargin;
|
|
14686
|
+
}
|
|
14687
|
+
break;
|
|
14688
|
+
}
|
|
14689
|
+
}
|
|
14690
|
+
const tag = {
|
|
14691
|
+
_internal_height: height,
|
|
14692
|
+
_internal_tailHeight: tailHeight,
|
|
14693
|
+
_internal_tailDirection: tailDirection,
|
|
14694
|
+
_internal_radius: marker.borderRadius ?? 6 /* Constants.TagRadius */,
|
|
14695
|
+
_internal_paddingX: 9 /* Constants.TagPaddingX */ * sizeMultiplier,
|
|
14696
|
+
_internal_left: 0,
|
|
14697
|
+
_internal_width: 0,
|
|
14698
|
+
};
|
|
14699
|
+
rendererItem._internal_size = height;
|
|
14700
|
+
rendererItem._internal_y = centerY;
|
|
14701
|
+
rendererItem._internal_tag = tag;
|
|
14702
|
+
if (rendererItem._internal_text !== undefined) {
|
|
14703
|
+
rendererItem._internal_text._internal_y = centerY;
|
|
14704
|
+
}
|
|
14705
|
+
}
|
|
14525
14706
|
// eslint-disable-next-line max-params, complexity
|
|
14526
|
-
function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart) {
|
|
14527
|
-
const price = getPrice(seriesData, marker,
|
|
14707
|
+
function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart, invertScale) {
|
|
14708
|
+
const price = getPrice(seriesData, marker, invertScale);
|
|
14528
14709
|
if (price === undefined) {
|
|
14529
14710
|
return;
|
|
14530
14711
|
}
|
|
14531
14712
|
const ignoreOffset = isPriceMarker(marker.position);
|
|
14532
14713
|
const timeScale = chart.timeScale();
|
|
14533
14714
|
const sizeMultiplier = isNumber(marker.size) ? Math.max(marker.size, 0) : 1;
|
|
14715
|
+
if (marker.shape === 'tag') {
|
|
14716
|
+
const anchorY = ensureNotNull(series.priceToCoordinate(price));
|
|
14717
|
+
fillTagSizeAndY(rendererItem, marker, anchorY, offsets, textHeight, shapeMargin, sizeMultiplier);
|
|
14718
|
+
return;
|
|
14719
|
+
}
|
|
14534
14720
|
const shapeSize = calculateShapeHeight(timeScale.options().barSpacing) * sizeMultiplier;
|
|
14535
14721
|
const halfSize = shapeSize / 2;
|
|
14536
14722
|
rendererItem._internal_size = shapeSize;
|
|
@@ -14637,6 +14823,10 @@ class SeriesMarkersPaneView {
|
|
|
14637
14823
|
_internal_externalId: marker.id,
|
|
14638
14824
|
_internal_internalId: marker._internal_internalId,
|
|
14639
14825
|
_internal_text: undefined,
|
|
14826
|
+
_internal_textColor: marker.textColor,
|
|
14827
|
+
_internal_borderColor: marker.borderColor,
|
|
14828
|
+
_internal_borderWidth: marker.borderWidth,
|
|
14829
|
+
_internal_tag: undefined,
|
|
14640
14830
|
}));
|
|
14641
14831
|
this._private__dataInvalidated = false;
|
|
14642
14832
|
}
|
|
@@ -14647,13 +14837,22 @@ class SeriesMarkersPaneView {
|
|
|
14647
14837
|
return;
|
|
14648
14838
|
}
|
|
14649
14839
|
const visibleBarsRange = new RangeImpl(Math.floor(visibleBars.from), Math.ceil(visibleBars.to));
|
|
14650
|
-
const firstValue = this._private__series.
|
|
14840
|
+
const firstValue = this._private__series.dataByIndex(0, 1 /* MismatchDirection.NearestRight */);
|
|
14651
14841
|
if (firstValue === null) {
|
|
14652
14842
|
return;
|
|
14653
14843
|
}
|
|
14654
14844
|
if (this._private__data._internal_items.length === 0) {
|
|
14655
14845
|
return;
|
|
14656
14846
|
}
|
|
14847
|
+
// This runs inside the hit-test and paint loops, where a throw costs the
|
|
14848
|
+
// rest of the frame.
|
|
14849
|
+
let invertScale = false;
|
|
14850
|
+
try {
|
|
14851
|
+
invertScale = this._private__series.priceScale().options().invertScale;
|
|
14852
|
+
}
|
|
14853
|
+
catch (error) {
|
|
14854
|
+
invertScale = false;
|
|
14855
|
+
}
|
|
14657
14856
|
let prevTimeIndex = NaN;
|
|
14658
14857
|
const shapeMargin$1 = shapeMargin(timeScale.options().barSpacing);
|
|
14659
14858
|
const offsets = {
|
|
@@ -14684,7 +14883,7 @@ class SeriesMarkersPaneView {
|
|
|
14684
14883
|
if (dataAt === null) {
|
|
14685
14884
|
continue;
|
|
14686
14885
|
}
|
|
14687
|
-
fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart);
|
|
14886
|
+
fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart, invertScale);
|
|
14688
14887
|
}
|
|
14689
14888
|
this._private__invalidated = false;
|
|
14690
14889
|
}
|
|
@@ -14842,6 +15041,10 @@ class SeriesMarkersPrimitive {
|
|
|
14842
15041
|
text: marker.text,
|
|
14843
15042
|
size: marker.size,
|
|
14844
15043
|
price: marker.price,
|
|
15044
|
+
textColor: marker.textColor,
|
|
15045
|
+
borderColor: marker.borderColor,
|
|
15046
|
+
borderWidth: marker.borderWidth,
|
|
15047
|
+
borderRadius: marker.borderRadius,
|
|
14845
15048
|
_internal_originalTime: marker.time,
|
|
14846
15049
|
};
|
|
14847
15050
|
if (marker.position === 'atPriceTop' ||
|
|
@@ -19143,10 +19346,13 @@ class TradingLineController {
|
|
|
19143
19346
|
constructor(chart, element) {
|
|
19144
19347
|
this._private__lines = new Map();
|
|
19145
19348
|
this._private__hoveredId = null;
|
|
19349
|
+
this._private__hoveredPart = null;
|
|
19146
19350
|
this._private__dragging = null;
|
|
19147
19351
|
this._private__pointerId = null;
|
|
19148
19352
|
this._private__pressOrigin = null;
|
|
19149
19353
|
this._private__pressedLine = null;
|
|
19354
|
+
/** Distance from the pointer to the dragged line when the press landed. */
|
|
19355
|
+
this._private__grabOffset = 0;
|
|
19150
19356
|
/** Latest pointer position in pane coordinates, or null when off the panes. */
|
|
19151
19357
|
this._private__point = null;
|
|
19152
19358
|
this._private__paneIndex = undefined;
|
|
@@ -19200,8 +19406,12 @@ class TradingLineController {
|
|
|
19200
19406
|
if (this._private__dragging !== null) {
|
|
19201
19407
|
return;
|
|
19202
19408
|
}
|
|
19203
|
-
const
|
|
19409
|
+
const hit = this._private__splitHit(param.hoveredObjectId);
|
|
19410
|
+
const hovered = hit === null ? null : this._private__lines.get(hit._internal_id) ?? null;
|
|
19204
19411
|
this._private__setHovered(hovered ? hovered.id() : null);
|
|
19412
|
+
// The part is updated after the id, and separately from it: moving from a
|
|
19413
|
+
// label onto its close button keeps the same line hovered.
|
|
19414
|
+
this._private__setHoveredPart(hovered, hovered === null ? null : hit?._internal_part ?? null);
|
|
19205
19415
|
this._private__applyCursor();
|
|
19206
19416
|
}
|
|
19207
19417
|
_private__onPointerDown(event) {
|
|
@@ -19220,6 +19430,7 @@ class TradingLineController {
|
|
|
19220
19430
|
this._private__pointerId = event.pointerId;
|
|
19221
19431
|
this._private__element.setPointerCapture(event.pointerId);
|
|
19222
19432
|
line.beginDrag();
|
|
19433
|
+
this._private__grabOffset = this._private__measureGrabOffset(line, event);
|
|
19223
19434
|
this._private__applyCursor();
|
|
19224
19435
|
event.preventDefault();
|
|
19225
19436
|
event.stopPropagation();
|
|
@@ -19254,6 +19465,7 @@ class TradingLineController {
|
|
|
19254
19465
|
Math.abs(event.clientY - origin._internal_y) > CLICK_TOLERANCE_PX);
|
|
19255
19466
|
if (dragged !== null) {
|
|
19256
19467
|
this._private__dragging = null;
|
|
19468
|
+
this._private__grabOffset = 0;
|
|
19257
19469
|
if (moved) {
|
|
19258
19470
|
dragged.commitDrag();
|
|
19259
19471
|
}
|
|
@@ -19281,6 +19493,7 @@ class TradingLineController {
|
|
|
19281
19493
|
}
|
|
19282
19494
|
const line = this._private__dragging;
|
|
19283
19495
|
this._private__dragging = null;
|
|
19496
|
+
this._private__grabOffset = 0;
|
|
19284
19497
|
this._private__releasePointer();
|
|
19285
19498
|
line.cancelDrag();
|
|
19286
19499
|
this._private__applyCursor();
|
|
@@ -19305,24 +19518,65 @@ class TradingLineController {
|
|
|
19305
19518
|
if (line === null) {
|
|
19306
19519
|
return null;
|
|
19307
19520
|
}
|
|
19308
|
-
//
|
|
19309
|
-
//
|
|
19310
|
-
//
|
|
19311
|
-
|
|
19312
|
-
|
|
19313
|
-
|
|
19521
|
+
// The pointer event is the gesture's own position, so it is the source of
|
|
19522
|
+
// truth. The crosshair is only a mirror of it: it stops updating as soon
|
|
19523
|
+
// as the pointer leaves the pane canvas — over the price axis, past the
|
|
19524
|
+
// chart's edge — and a drag driven from it freezes there while the user
|
|
19525
|
+
// is still moving.
|
|
19526
|
+
const paneY = this._private__paneY(line, event);
|
|
19527
|
+
return paneY === null ? null : line.priceAt(paneY - this._private__grabOffset);
|
|
19528
|
+
}
|
|
19529
|
+
/** The pointer's position inside the line's pane, in pane coordinates. */
|
|
19530
|
+
_private__paneY(line, event) {
|
|
19314
19531
|
const pane = this._private__chart.panes()[line.paneIndex()];
|
|
19315
19532
|
const paneElement = pane ? pane.getHTMLElement() : null;
|
|
19316
|
-
if (
|
|
19317
|
-
return
|
|
19533
|
+
if (paneElement) {
|
|
19534
|
+
return event.clientY - paneElement.getBoundingClientRect().top;
|
|
19535
|
+
}
|
|
19536
|
+
if (this._private__point !== null && this._private__paneIndex === line.paneIndex()) {
|
|
19537
|
+
return this._private__point._internal_y;
|
|
19538
|
+
}
|
|
19539
|
+
return null;
|
|
19540
|
+
}
|
|
19541
|
+
/**
|
|
19542
|
+
* Keeps the grab point: a line picked up by the edge of its label moves with
|
|
19543
|
+
* the pointer rather than centring itself on it. Ignored past a sane
|
|
19544
|
+
* distance, so a bad reading cannot send the line off on the first move.
|
|
19545
|
+
*/
|
|
19546
|
+
_private__measureGrabOffset(line, event) {
|
|
19547
|
+
if (!line.dragAnchorY) {
|
|
19548
|
+
return 0;
|
|
19549
|
+
}
|
|
19550
|
+
const anchor = line.dragAnchorY();
|
|
19551
|
+
const paneY = this._private__paneY(line, event);
|
|
19552
|
+
if (anchor === null || paneY === null) {
|
|
19553
|
+
return 0;
|
|
19318
19554
|
}
|
|
19319
|
-
|
|
19555
|
+
const offset = paneY - anchor;
|
|
19556
|
+
return Math.abs(offset) > 40 /* Constants.MaxGrabOffsetPx */ ? 0 : offset;
|
|
19320
19557
|
}
|
|
19321
|
-
|
|
19322
|
-
|
|
19558
|
+
/**
|
|
19559
|
+
* Hit ids are either a bare line id or `id#part`, which is how a line with
|
|
19560
|
+
* several targets reports which one was hit.
|
|
19561
|
+
*/
|
|
19562
|
+
_private__splitHit(externalId) {
|
|
19563
|
+
if (typeof externalId !== 'string' || externalId.length === 0) {
|
|
19323
19564
|
return null;
|
|
19324
19565
|
}
|
|
19325
|
-
|
|
19566
|
+
const separator = externalId.indexOf('#');
|
|
19567
|
+
if (separator === -1) {
|
|
19568
|
+
return { _internal_id: externalId, _internal_part: null };
|
|
19569
|
+
}
|
|
19570
|
+
return { _internal_id: externalId.slice(0, separator), _internal_part: externalId.slice(separator + 1) };
|
|
19571
|
+
}
|
|
19572
|
+
_private__setHoveredPart(line, part) {
|
|
19573
|
+
if (this._private__hoveredPart === part) {
|
|
19574
|
+
return;
|
|
19575
|
+
}
|
|
19576
|
+
this._private__hoveredPart = part;
|
|
19577
|
+
if (line && line.setHoveredPart) {
|
|
19578
|
+
line.setHoveredPart(part);
|
|
19579
|
+
}
|
|
19326
19580
|
}
|
|
19327
19581
|
_private__setHovered(id) {
|
|
19328
19582
|
if (this._private__hoveredId === id) {
|
|
@@ -19332,6 +19586,7 @@ class TradingLineController {
|
|
|
19332
19586
|
this._private__lines.get(this._private__hoveredId)?.setHovered(false);
|
|
19333
19587
|
}
|
|
19334
19588
|
this._private__hoveredId = id;
|
|
19589
|
+
this._private__hoveredPart = null;
|
|
19335
19590
|
if (id !== null) {
|
|
19336
19591
|
this._private__lines.get(id)?.setHovered(true);
|
|
19337
19592
|
}
|
|
@@ -19855,6 +20110,862 @@ function createInteractiveLineManager(chart, series) {
|
|
|
19855
20110
|
return new InteractiveLineManager(chart, series);
|
|
19856
20111
|
}
|
|
19857
20112
|
|
|
20113
|
+
/**
|
|
20114
|
+
* Fans out labels that land on top of each other.
|
|
20115
|
+
*
|
|
20116
|
+
* Every position line on a chart shares one of these. Two positions opened at
|
|
20117
|
+
* the same price draw their labels at the same coordinate, and the one behind
|
|
20118
|
+
* becomes unreachable — the engine resolves the hit to whichever primitive it
|
|
20119
|
+
* reaches first, and that is always the same one. Nudging each collision down
|
|
20120
|
+
* leaves every label a strip of its own to be grabbed by.
|
|
20121
|
+
*
|
|
20122
|
+
* The line itself never moves: it stays at the price it belongs to, and only
|
|
20123
|
+
* the label is offset.
|
|
20124
|
+
*/
|
|
20125
|
+
class PositionLineLabelLayout {
|
|
20126
|
+
constructor() {
|
|
20127
|
+
/** Registration order. It is what keeps the fan from reshuffling per frame. */
|
|
20128
|
+
this._private__order = [];
|
|
20129
|
+
this._private__claims = new Map();
|
|
20130
|
+
this._private__offsets = new Map();
|
|
20131
|
+
this._private__invalidated = true;
|
|
20132
|
+
}
|
|
20133
|
+
_internal_register(ownerId) {
|
|
20134
|
+
if (!this._private__claims.has(ownerId)) {
|
|
20135
|
+
this._private__order.push(ownerId);
|
|
20136
|
+
this._private__claims.set(ownerId, []);
|
|
20137
|
+
this._private__invalidated = true;
|
|
20138
|
+
}
|
|
20139
|
+
}
|
|
20140
|
+
_internal_unregister(ownerId) {
|
|
20141
|
+
const index = this._private__order.indexOf(ownerId);
|
|
20142
|
+
if (index !== -1) {
|
|
20143
|
+
this._private__order.splice(index, 1);
|
|
20144
|
+
}
|
|
20145
|
+
this._private__claims.delete(ownerId);
|
|
20146
|
+
this._private__invalidated = true;
|
|
20147
|
+
}
|
|
20148
|
+
_internal_isEmpty() {
|
|
20149
|
+
return this._private__order.length === 0;
|
|
20150
|
+
}
|
|
20151
|
+
/**
|
|
20152
|
+
* Publishes where an owner's labels want to sit. Called while each line
|
|
20153
|
+
* rebuilds its renderer data, so a fan can be one frame behind while prices
|
|
20154
|
+
* move — which is invisible, and cheaper than forcing a second pass.
|
|
20155
|
+
*/
|
|
20156
|
+
_internal_setClaims(ownerId, claims) {
|
|
20157
|
+
this._private__claims.set(ownerId, claims);
|
|
20158
|
+
this._private__invalidated = true;
|
|
20159
|
+
}
|
|
20160
|
+
/** How far down a label has to move to clear the ones already placed. */
|
|
20161
|
+
_internal_offsetFor(ownerId, level) {
|
|
20162
|
+
if (this._private__invalidated) {
|
|
20163
|
+
this._private__recalculate();
|
|
20164
|
+
}
|
|
20165
|
+
return this._private__offsets.get(key(ownerId, level)) ?? 0;
|
|
20166
|
+
}
|
|
20167
|
+
_private__recalculate() {
|
|
20168
|
+
const offsets = new Map();
|
|
20169
|
+
const placed = [];
|
|
20170
|
+
for (const ownerId of this._private__order) {
|
|
20171
|
+
for (const claim of this._private__claims.get(ownerId) ?? []) {
|
|
20172
|
+
const gap = minGap(claim._internal_height);
|
|
20173
|
+
const maxOffset = claim._internal_height;
|
|
20174
|
+
// Moved down to the first coordinate that clears everything already
|
|
20175
|
+
// placed, and no further. Two levels whose own prices nearly part
|
|
20176
|
+
// them only need the few pixels they are short of — stepping by a
|
|
20177
|
+
// full gap each time is what made the fan look flung open.
|
|
20178
|
+
let y = claim._internal_y;
|
|
20179
|
+
for (const other of [...placed].sort(byY)) {
|
|
20180
|
+
if (Math.abs(y - other._internal_y) < gap) {
|
|
20181
|
+
y = other._internal_y + gap;
|
|
20182
|
+
}
|
|
20183
|
+
}
|
|
20184
|
+
let offset = y - claim._internal_y;
|
|
20185
|
+
if (offset > maxOffset) {
|
|
20186
|
+
// More labels than there is room to fan. The rest stack, which is
|
|
20187
|
+
// no worse than before and keeps the pile from running off.
|
|
20188
|
+
offset = 0;
|
|
20189
|
+
}
|
|
20190
|
+
placed.push({ _internal_y: claim._internal_y + offset, _internal_height: claim._internal_height });
|
|
20191
|
+
offsets.set(key(ownerId, claim._internal_level), offset);
|
|
20192
|
+
}
|
|
20193
|
+
}
|
|
20194
|
+
this._private__offsets = offsets;
|
|
20195
|
+
this._private__invalidated = false;
|
|
20196
|
+
}
|
|
20197
|
+
}
|
|
20198
|
+
function key(ownerId, level) {
|
|
20199
|
+
return `${ownerId}:${level}`;
|
|
20200
|
+
}
|
|
20201
|
+
/**
|
|
20202
|
+
* The strip a label keeps to itself. Comfortably clickable, and no wider —
|
|
20203
|
+
* every pixel here is a pixel the label sits away from its own line.
|
|
20204
|
+
*/
|
|
20205
|
+
function minGap(height) {
|
|
20206
|
+
return Math.max(9, Math.round(height * 0.4));
|
|
20207
|
+
}
|
|
20208
|
+
function byY(a, b) {
|
|
20209
|
+
return a._internal_y - b._internal_y;
|
|
20210
|
+
}
|
|
20211
|
+
const layouts = new WeakMap();
|
|
20212
|
+
/** The layout is shared by every position line on the same chart. */
|
|
20213
|
+
function acquireLabelLayout(chart, ownerId) {
|
|
20214
|
+
let layout = layouts.get(chart);
|
|
20215
|
+
if (!layout) {
|
|
20216
|
+
layout = new PositionLineLabelLayout();
|
|
20217
|
+
layouts.set(chart, layout);
|
|
20218
|
+
}
|
|
20219
|
+
layout._internal_register(ownerId);
|
|
20220
|
+
return layout;
|
|
20221
|
+
}
|
|
20222
|
+
function releaseLabelLayout(chart, ownerId) {
|
|
20223
|
+
const layout = layouts.get(chart);
|
|
20224
|
+
if (!layout) {
|
|
20225
|
+
return;
|
|
20226
|
+
}
|
|
20227
|
+
layout._internal_unregister(ownerId);
|
|
20228
|
+
if (layout._internal_isEmpty()) {
|
|
20229
|
+
layouts.delete(chart);
|
|
20230
|
+
}
|
|
20231
|
+
}
|
|
20232
|
+
|
|
20233
|
+
const positionLineDefaultColors = {
|
|
20234
|
+
entry: '#2962FF',
|
|
20235
|
+
takeProfit: '#26A17B',
|
|
20236
|
+
stopLoss: '#D9A23B',
|
|
20237
|
+
};
|
|
20238
|
+
const positionLineThemeDefaults = {
|
|
20239
|
+
badgeTextColor: '#FFFFFF',
|
|
20240
|
+
borderRadius: 4,
|
|
20241
|
+
borderWidth: 1,
|
|
20242
|
+
paddingX: 8,
|
|
20243
|
+
paddingY: 6,
|
|
20244
|
+
backgroundOpacity: 1,
|
|
20245
|
+
};
|
|
20246
|
+
const positionLineLevels = ['takeProfit', 'entry', 'stopLoss'];
|
|
20247
|
+
|
|
20248
|
+
class PositionLineRenderer {
|
|
20249
|
+
constructor() {
|
|
20250
|
+
this._private__data = null;
|
|
20251
|
+
this._private__layouts = [];
|
|
20252
|
+
this._private__connectorX = 0;
|
|
20253
|
+
this._private__font = '';
|
|
20254
|
+
this._private__colorParser = new ColorParser([]);
|
|
20255
|
+
this._private__textWidthCache = new TextWidthCache(50);
|
|
20256
|
+
}
|
|
20257
|
+
_internal_setData(data) {
|
|
20258
|
+
this._private__data = data;
|
|
20259
|
+
}
|
|
20260
|
+
_internal_hitTest(x, y) {
|
|
20261
|
+
// The close button sits on top of the label, which sits on top of the
|
|
20262
|
+
// line, so the narrowest target is tested first.
|
|
20263
|
+
for (const layout of this._private__layouts) {
|
|
20264
|
+
if (layout._internal_closable && x >= layout._internal_closeLeft && x <= layout._internal_closeRight &&
|
|
20265
|
+
Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
|
|
20266
|
+
return { _internal_level: layout._internal_level, _internal_part: 'close' };
|
|
20267
|
+
}
|
|
20268
|
+
}
|
|
20269
|
+
for (const layout of this._private__layouts) {
|
|
20270
|
+
if (x >= layout._internal_pillLeft && x <= layout._internal_pillRight && Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
|
|
20271
|
+
return { _internal_level: layout._internal_level, _internal_part: 'body' };
|
|
20272
|
+
}
|
|
20273
|
+
}
|
|
20274
|
+
for (const layout of this._private__layouts) {
|
|
20275
|
+
if (Math.abs(y - layout._internal_y) <= 8 /* Constants.LineHitTolerance */) {
|
|
20276
|
+
return { _internal_level: layout._internal_level, _internal_part: 'line' };
|
|
20277
|
+
}
|
|
20278
|
+
}
|
|
20279
|
+
return null;
|
|
20280
|
+
}
|
|
20281
|
+
draw(target) {
|
|
20282
|
+
target.useBitmapCoordinateSpace((scope) => this._private__drawImpl(scope));
|
|
20283
|
+
}
|
|
20284
|
+
_private__drawImpl(scope) {
|
|
20285
|
+
const data = this._private__data;
|
|
20286
|
+
this._private__layouts = [];
|
|
20287
|
+
if (data === null || data._internal_levels.length === 0) {
|
|
20288
|
+
return;
|
|
20289
|
+
}
|
|
20290
|
+
const { context: ctx, mediaSize } = scope;
|
|
20291
|
+
ctx.save();
|
|
20292
|
+
if (this._private__font !== data._internal_font) {
|
|
20293
|
+
this._private__font = data._internal_font;
|
|
20294
|
+
this._private__textWidthCache._internal_reset();
|
|
20295
|
+
}
|
|
20296
|
+
ctx.font = data._internal_font;
|
|
20297
|
+
ctx.textBaseline = 'middle';
|
|
20298
|
+
this._private__connectorX = Math.max(0, mediaSize.width - data._internal_rightOffset);
|
|
20299
|
+
const lineEnd = data._internal_connectorVisible ? this._private__connectorX : mediaSize.width;
|
|
20300
|
+
for (const level of data._internal_levels) {
|
|
20301
|
+
this._private__layouts.push(this._private__drawLevel(scope, data, level, lineEnd));
|
|
20302
|
+
}
|
|
20303
|
+
if (data._internal_connectorVisible && data._internal_levels.length > 1) {
|
|
20304
|
+
this._private__drawConnector(scope, data);
|
|
20305
|
+
}
|
|
20306
|
+
ctx.restore();
|
|
20307
|
+
}
|
|
20308
|
+
// eslint-disable-next-line max-params
|
|
20309
|
+
_private__drawLevel(scope, data, level, lineEnd) {
|
|
20310
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20311
|
+
const height = data._internal_fontSize + data._internal_paddingY * 2;
|
|
20312
|
+
const badgeWidth = level._internal_badge.length === 0
|
|
20313
|
+
? 0
|
|
20314
|
+
: Math.max(this._private__textWidthCache._internal_measureText(ctx, level._internal_badge) + data._internal_paddingX * 2, height * 0.85 /* Constants.BadgeMinWidthRatio */);
|
|
20315
|
+
const labelWidth = this._private__textWidthCache._internal_measureText(ctx, level._internal_text) + data._internal_paddingX * 2;
|
|
20316
|
+
const closeWidth = level._internal_closable ? height : 0;
|
|
20317
|
+
const pillWidth = badgeWidth + labelWidth + closeWidth;
|
|
20318
|
+
const pillRight = Math.max(pillWidth, lineEnd - 8 /* Constants.LabelGap */);
|
|
20319
|
+
const pillLeft = pillRight - pillWidth;
|
|
20320
|
+
const labelY = level._internal_y + level._internal_labelOffsetY;
|
|
20321
|
+
const top = labelY - height / 2;
|
|
20322
|
+
this._private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd);
|
|
20323
|
+
this._private__drawPill(scope, data, level, { _internal_left: pillLeft, _internal_top: top, _internal_width: pillWidth, _internal_height: height });
|
|
20324
|
+
let cellLeft = pillLeft;
|
|
20325
|
+
if (badgeWidth > 0) {
|
|
20326
|
+
this._private__drawBadge(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: badgeWidth, _internal_height: height });
|
|
20327
|
+
cellLeft += badgeWidth;
|
|
20328
|
+
this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
|
|
20329
|
+
}
|
|
20330
|
+
ctx.save();
|
|
20331
|
+
ctx.scale(hpr, vpr);
|
|
20332
|
+
ctx.textAlign = 'center';
|
|
20333
|
+
ctx.fillStyle = data._internal_textColor ?? level._internal_color;
|
|
20334
|
+
ctx.fillText(level._internal_text, cellLeft + labelWidth / 2, labelY);
|
|
20335
|
+
ctx.restore();
|
|
20336
|
+
cellLeft += labelWidth;
|
|
20337
|
+
if (level._internal_closable) {
|
|
20338
|
+
this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
|
|
20339
|
+
this._private__drawCloseButton(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: closeWidth, _internal_height: height });
|
|
20340
|
+
}
|
|
20341
|
+
return {
|
|
20342
|
+
_internal_level: level._internal_level,
|
|
20343
|
+
_internal_y: level._internal_y,
|
|
20344
|
+
_internal_labelY: labelY,
|
|
20345
|
+
_internal_height: height,
|
|
20346
|
+
_internal_pillLeft: pillLeft,
|
|
20347
|
+
_internal_pillRight: pillRight,
|
|
20348
|
+
_internal_closeLeft: cellLeft,
|
|
20349
|
+
_internal_closeRight: cellLeft + closeWidth,
|
|
20350
|
+
_internal_closable: level._internal_closable,
|
|
20351
|
+
};
|
|
20352
|
+
}
|
|
20353
|
+
// eslint-disable-next-line max-params
|
|
20354
|
+
_private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd) {
|
|
20355
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20356
|
+
const lineWidth = Math.max(1, Math.round(level._internal_lineWidth * vpr));
|
|
20357
|
+
const y = Math.round(level._internal_y * vpr) - Math.floor(lineWidth / 2);
|
|
20358
|
+
ctx.save();
|
|
20359
|
+
ctx.strokeStyle = level._internal_color;
|
|
20360
|
+
ctx.lineWidth = lineWidth;
|
|
20361
|
+
setLineStyle(ctx, level._internal_lineStyle);
|
|
20362
|
+
ctx.beginPath();
|
|
20363
|
+
if (level._internal_labelOffsetY !== 0) {
|
|
20364
|
+
// The label has been fanned off its line, so breaking the line for it
|
|
20365
|
+
// would leave a gap with nothing in it.
|
|
20366
|
+
ctx.moveTo(0, y);
|
|
20367
|
+
ctx.lineTo(Math.round(lineEnd * hpr), y);
|
|
20368
|
+
}
|
|
20369
|
+
else {
|
|
20370
|
+
ctx.moveTo(0, y);
|
|
20371
|
+
ctx.lineTo(Math.round(Math.max(0, pillLeft - 8 /* Constants.LabelGap */) * hpr), y);
|
|
20372
|
+
if (lineEnd > pillRight + 8 /* Constants.LabelGap */) {
|
|
20373
|
+
ctx.moveTo(Math.round((pillRight + 8 /* Constants.LabelGap */) * hpr), y);
|
|
20374
|
+
ctx.lineTo(Math.round(lineEnd * hpr), y);
|
|
20375
|
+
}
|
|
20376
|
+
}
|
|
20377
|
+
ctx.stroke();
|
|
20378
|
+
ctx.restore();
|
|
20379
|
+
}
|
|
20380
|
+
// eslint-disable-next-line max-params
|
|
20381
|
+
_private__drawPill(scope, data, level, box) {
|
|
20382
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20383
|
+
const hovered = data._internal_hoveredLevel === level._internal_level;
|
|
20384
|
+
const borderWidth = Math.max(1, Math.round((hovered ? data._internal_borderWidth + 1 : data._internal_borderWidth) * hpr));
|
|
20385
|
+
ctx.save();
|
|
20386
|
+
ctx.setLineDash([]);
|
|
20387
|
+
bitmapRoundRect(ctx, box, data._internal_borderRadius, hpr, vpr);
|
|
20388
|
+
ctx.fillStyle = this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
|
|
20389
|
+
ctx.fill();
|
|
20390
|
+
ctx.lineWidth = borderWidth;
|
|
20391
|
+
ctx.strokeStyle = level._internal_color;
|
|
20392
|
+
ctx.stroke();
|
|
20393
|
+
ctx.restore();
|
|
20394
|
+
}
|
|
20395
|
+
// eslint-disable-next-line max-params
|
|
20396
|
+
_private__drawBadge(scope, data, level, box) {
|
|
20397
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20398
|
+
if (level._internal_badgeFilled) {
|
|
20399
|
+
// The badge shares the label's left corners, so its fill has to stop
|
|
20400
|
+
// short of the border instead of squaring it off.
|
|
20401
|
+
const inset = data._internal_borderWidth;
|
|
20402
|
+
ctx.save();
|
|
20403
|
+
bitmapRoundRect(ctx, { _internal_left: box._internal_left + inset, _internal_top: box._internal_top + inset, _internal_width: box._internal_width - inset, _internal_height: box._internal_height - inset * 2 }, Math.max(0, data._internal_borderRadius - inset), hpr, vpr, 'left');
|
|
20404
|
+
ctx.fillStyle = level._internal_color;
|
|
20405
|
+
ctx.fill();
|
|
20406
|
+
ctx.restore();
|
|
20407
|
+
}
|
|
20408
|
+
ctx.save();
|
|
20409
|
+
ctx.scale(hpr, vpr);
|
|
20410
|
+
ctx.textAlign = 'center';
|
|
20411
|
+
ctx.fillStyle = level._internal_badgeFilled ? data._internal_badgeTextColor : level._internal_color;
|
|
20412
|
+
ctx.fillText(level._internal_badge, box._internal_left + box._internal_width / 2, box._internal_top + box._internal_height / 2);
|
|
20413
|
+
ctx.restore();
|
|
20414
|
+
}
|
|
20415
|
+
// eslint-disable-next-line max-params
|
|
20416
|
+
_private__drawSeparator(ctx, color, x, top, height, hpr, vpr) {
|
|
20417
|
+
const width = Math.max(1, Math.floor(hpr));
|
|
20418
|
+
ctx.save();
|
|
20419
|
+
ctx.setLineDash([]);
|
|
20420
|
+
ctx.fillStyle = this._private__colorParser._internal_applyAlpha(color, 0.4 /* Constants.SeparatorAlpha */);
|
|
20421
|
+
ctx.fillRect(Math.round(x * hpr), Math.round(top * vpr), width, Math.round(height * vpr));
|
|
20422
|
+
ctx.restore();
|
|
20423
|
+
}
|
|
20424
|
+
// eslint-disable-next-line max-params
|
|
20425
|
+
_private__drawCloseButton(scope, data, level, box) {
|
|
20426
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20427
|
+
const hovered = data._internal_hoveredLevel === level._internal_level && data._internal_hoveredPart === 'close';
|
|
20428
|
+
if (hovered) {
|
|
20429
|
+
ctx.save();
|
|
20430
|
+
const inset = data._internal_borderWidth;
|
|
20431
|
+
bitmapRoundRect(ctx, { _internal_left: box._internal_left, _internal_top: box._internal_top + inset, _internal_width: box._internal_width - inset, _internal_height: box._internal_height - inset * 2 }, Math.max(0, data._internal_borderRadius - inset), hpr, vpr, 'right');
|
|
20432
|
+
ctx.fillStyle = this._private__colorParser._internal_applyAlpha(level._internal_color, 0.25);
|
|
20433
|
+
ctx.fill();
|
|
20434
|
+
ctx.restore();
|
|
20435
|
+
}
|
|
20436
|
+
const centerX = (box._internal_left + box._internal_width / 2) * hpr;
|
|
20437
|
+
const centerY = (box._internal_top + box._internal_height / 2) * vpr;
|
|
20438
|
+
const arm = (7 /* Constants.CloseGlyphSize */ / 2) * hpr;
|
|
20439
|
+
ctx.save();
|
|
20440
|
+
ctx.setLineDash([]);
|
|
20441
|
+
ctx.lineCap = 'round';
|
|
20442
|
+
ctx.lineWidth = Math.max(1, Math.round(1.4 * hpr));
|
|
20443
|
+
ctx.strokeStyle = level._internal_color;
|
|
20444
|
+
ctx.beginPath();
|
|
20445
|
+
ctx.moveTo(centerX - arm, centerY - arm);
|
|
20446
|
+
ctx.lineTo(centerX + arm, centerY + arm);
|
|
20447
|
+
ctx.moveTo(centerX + arm, centerY - arm);
|
|
20448
|
+
ctx.lineTo(centerX - arm, centerY + arm);
|
|
20449
|
+
ctx.stroke();
|
|
20450
|
+
ctx.restore();
|
|
20451
|
+
}
|
|
20452
|
+
_private__drawConnector(scope, data) {
|
|
20453
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20454
|
+
const entry = data._internal_levels.find((level) => level._internal_level === 'entry') ?? data._internal_levels[0];
|
|
20455
|
+
const ys = data._internal_levels.map((level) => level._internal_y);
|
|
20456
|
+
const x = Math.round(this._private__connectorX * hpr);
|
|
20457
|
+
ctx.save();
|
|
20458
|
+
ctx.setLineDash([]);
|
|
20459
|
+
ctx.strokeStyle = entry._internal_color;
|
|
20460
|
+
ctx.lineWidth = Math.max(1, Math.floor(hpr));
|
|
20461
|
+
ctx.beginPath();
|
|
20462
|
+
ctx.moveTo(x, Math.round(Math.min(...ys) * vpr));
|
|
20463
|
+
ctx.lineTo(x, Math.round(Math.max(...ys) * vpr));
|
|
20464
|
+
ctx.stroke();
|
|
20465
|
+
for (const level of data._internal_levels) {
|
|
20466
|
+
ctx.beginPath();
|
|
20467
|
+
ctx.arc(x, Math.round(level._internal_y * vpr), 4.5 /* Constants.HandleRadius */ * hpr, 0, 2 * Math.PI);
|
|
20468
|
+
ctx.fillStyle = level._internal_level === 'entry'
|
|
20469
|
+
? entry._internal_color
|
|
20470
|
+
: this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
|
|
20471
|
+
ctx.fill();
|
|
20472
|
+
ctx.lineWidth = Math.max(1, Math.round(1.5 * hpr));
|
|
20473
|
+
ctx.strokeStyle = level._internal_color;
|
|
20474
|
+
ctx.stroke();
|
|
20475
|
+
}
|
|
20476
|
+
ctx.restore();
|
|
20477
|
+
}
|
|
20478
|
+
}
|
|
20479
|
+
// eslint-disable-next-line max-params
|
|
20480
|
+
function bitmapRoundRect(ctx, box, radius, hpr, vpr, side = 'all') {
|
|
20481
|
+
const left = Math.round(box._internal_left * hpr);
|
|
20482
|
+
const top = Math.round(box._internal_top * vpr);
|
|
20483
|
+
const width = Math.round(box._internal_width * hpr);
|
|
20484
|
+
const height = Math.round(box._internal_height * vpr);
|
|
20485
|
+
const r = Math.min(radius * hpr, width / 2, height / 2);
|
|
20486
|
+
const radii = side === 'left'
|
|
20487
|
+
? [r, 0, 0, r]
|
|
20488
|
+
: side === 'right'
|
|
20489
|
+
? [0, r, r, 0]
|
|
20490
|
+
: [r, r, r, r];
|
|
20491
|
+
drawRoundRect(ctx, left, top, width, height, radii);
|
|
20492
|
+
}
|
|
20493
|
+
|
|
20494
|
+
class PositionLinePaneView {
|
|
20495
|
+
constructor() {
|
|
20496
|
+
this._private__renderer = new PositionLineRenderer();
|
|
20497
|
+
this._private__data = null;
|
|
20498
|
+
}
|
|
20499
|
+
_internal_update(data) {
|
|
20500
|
+
this._private__data = data;
|
|
20501
|
+
}
|
|
20502
|
+
renderer() {
|
|
20503
|
+
if (this._private__data === null) {
|
|
20504
|
+
return null;
|
|
20505
|
+
}
|
|
20506
|
+
this._private__renderer._internal_setData(this._private__data);
|
|
20507
|
+
return this._private__renderer;
|
|
20508
|
+
}
|
|
20509
|
+
zOrder() {
|
|
20510
|
+
return 'top';
|
|
20511
|
+
}
|
|
20512
|
+
_internal_hitTest(x, y) {
|
|
20513
|
+
return this._private__data === null ? null : this._private__renderer._internal_hitTest(x, y);
|
|
20514
|
+
}
|
|
20515
|
+
}
|
|
20516
|
+
|
|
20517
|
+
class PositionLineAxisView {
|
|
20518
|
+
constructor(source) {
|
|
20519
|
+
this._private__source = source;
|
|
20520
|
+
}
|
|
20521
|
+
coordinate() {
|
|
20522
|
+
// A hidden label is pushed far off the axis so it cannot reserve a gap
|
|
20523
|
+
// between the labels that are shown.
|
|
20524
|
+
return this._private__label()?._internal_coordinate ?? -1e3;
|
|
20525
|
+
}
|
|
20526
|
+
text() {
|
|
20527
|
+
return this._private__label()?._internal_text ?? '';
|
|
20528
|
+
}
|
|
20529
|
+
textColor() {
|
|
20530
|
+
return this._private__label()?._internal_textColor ?? '#FFFFFF';
|
|
20531
|
+
}
|
|
20532
|
+
backColor() {
|
|
20533
|
+
return this._private__label()?._internal_color ?? '#000000';
|
|
20534
|
+
}
|
|
20535
|
+
visible() {
|
|
20536
|
+
return this._private__label() !== null;
|
|
20537
|
+
}
|
|
20538
|
+
_private__label() {
|
|
20539
|
+
return this._private__source();
|
|
20540
|
+
}
|
|
20541
|
+
}
|
|
20542
|
+
|
|
20543
|
+
let nextPositionId = 0;
|
|
20544
|
+
const DEFAULT_RIGHT_OFFSET = 56;
|
|
20545
|
+
/**
|
|
20546
|
+
* A group of trading levels — entry, take profit and stop loss — drawn as one
|
|
20547
|
+
* unit: a line per level, a label carrying size and P&L, and a connector that
|
|
20548
|
+
* ties the three together.
|
|
20549
|
+
*
|
|
20550
|
+
* Input is not handled here. Every trading line on a chart shares a single
|
|
20551
|
+
* TradingLineController, which is what guarantees that one gesture moves one
|
|
20552
|
+
* level, and that a position line and a plain draggable price line on the same
|
|
20553
|
+
* chart do not fight over the pointer.
|
|
20554
|
+
*/
|
|
20555
|
+
class PositionLine {
|
|
20556
|
+
constructor(chart, series, options) {
|
|
20557
|
+
this._private__paneView = new PositionLinePaneView();
|
|
20558
|
+
this._private__colorParser = new ColorParser([]);
|
|
20559
|
+
this._private__attached = false;
|
|
20560
|
+
this._private__hoveredLevel = null;
|
|
20561
|
+
this._private__hoveredPart = null;
|
|
20562
|
+
this._private__dragLevel = null;
|
|
20563
|
+
this._private__priceBeforeDrag = null;
|
|
20564
|
+
this._private__chart = chart;
|
|
20565
|
+
this._private__series = series;
|
|
20566
|
+
this._private__id = `pipsend-position-line-${nextPositionId++}`;
|
|
20567
|
+
this._private__options = options;
|
|
20568
|
+
this._private__theme = { ...positionLineThemeDefaults, ...options.theme };
|
|
20569
|
+
this._private__paneViews = [this._private__paneView];
|
|
20570
|
+
this._private__axisViews = positionLineLevels.map((level) => new PositionLineAxisView(() => this._private__axisLabel(level)));
|
|
20571
|
+
this._private__labelLayout = acquireLabelLayout(this._private__chart, this._private__id);
|
|
20572
|
+
this._private__series.attachPrimitive(this);
|
|
20573
|
+
acquireController(this._private__chart)?._internal_register(this);
|
|
20574
|
+
}
|
|
20575
|
+
// ---- ISeriesPrimitive ----
|
|
20576
|
+
attached(param) {
|
|
20577
|
+
this._private__requestUpdate = param.requestUpdate;
|
|
20578
|
+
this._private__attached = true;
|
|
20579
|
+
this.requestUpdate();
|
|
20580
|
+
}
|
|
20581
|
+
detached() {
|
|
20582
|
+
this._private__attached = false;
|
|
20583
|
+
this._private__requestUpdate = undefined;
|
|
20584
|
+
}
|
|
20585
|
+
paneViews() {
|
|
20586
|
+
return this._private__paneViews;
|
|
20587
|
+
}
|
|
20588
|
+
priceAxisViews() {
|
|
20589
|
+
return this._private__axisViews;
|
|
20590
|
+
}
|
|
20591
|
+
updateAllViews() {
|
|
20592
|
+
this._private__paneView._internal_update(this._private__buildRendererData());
|
|
20593
|
+
}
|
|
20594
|
+
hitTest(x, y) {
|
|
20595
|
+
const hit = this._private__paneView._internal_hitTest(x, y);
|
|
20596
|
+
if (hit === null) {
|
|
20597
|
+
return null;
|
|
20598
|
+
}
|
|
20599
|
+
const level = this._private__levelOptions(hit._internal_level);
|
|
20600
|
+
if (level === null) {
|
|
20601
|
+
return null;
|
|
20602
|
+
}
|
|
20603
|
+
return {
|
|
20604
|
+
externalId: `${this._private__id}#${hit._internal_level}:${hit._internal_part}`,
|
|
20605
|
+
zOrder: 'top',
|
|
20606
|
+
cursorStyle: hit._internal_part === 'close'
|
|
20607
|
+
? 'pointer'
|
|
20608
|
+
: (level.draggable ?? true) ? 'ns-resize' : undefined,
|
|
20609
|
+
};
|
|
20610
|
+
}
|
|
20611
|
+
// ---- ControllableLine ----
|
|
20612
|
+
id() {
|
|
20613
|
+
return this._private__id;
|
|
20614
|
+
}
|
|
20615
|
+
paneIndex() {
|
|
20616
|
+
try {
|
|
20617
|
+
return this._private__series.getPane().paneIndex();
|
|
20618
|
+
}
|
|
20619
|
+
catch (error) {
|
|
20620
|
+
// The engine asserts the series belongs to a pane, and that lookup can
|
|
20621
|
+
// come back empty while panes are being rebuilt. The main pane is the
|
|
20622
|
+
// right guess and keeps the gesture alive instead of throwing mid-drag.
|
|
20623
|
+
return 0;
|
|
20624
|
+
}
|
|
20625
|
+
}
|
|
20626
|
+
setHoveredPart(part) {
|
|
20627
|
+
const [level, name] = part === null ? [null, null] : part.split(':');
|
|
20628
|
+
const nextLevel = level ?? null;
|
|
20629
|
+
const nextPart = name ?? null;
|
|
20630
|
+
if (this._private__hoveredLevel === nextLevel && this._private__hoveredPart === nextPart) {
|
|
20631
|
+
return;
|
|
20632
|
+
}
|
|
20633
|
+
this._private__hoveredLevel = nextLevel;
|
|
20634
|
+
this._private__hoveredPart = nextPart;
|
|
20635
|
+
this.requestUpdate();
|
|
20636
|
+
}
|
|
20637
|
+
setHovered(hovered) {
|
|
20638
|
+
if (!hovered) {
|
|
20639
|
+
this.setHoveredPart(null);
|
|
20640
|
+
}
|
|
20641
|
+
}
|
|
20642
|
+
isDraggable() {
|
|
20643
|
+
if (this._private__hoveredLevel === null || this._private__hoveredPart === 'close') {
|
|
20644
|
+
return false;
|
|
20645
|
+
}
|
|
20646
|
+
return this._private__levelOptions(this._private__hoveredLevel)?.draggable ?? true;
|
|
20647
|
+
}
|
|
20648
|
+
isClickable() {
|
|
20649
|
+
if (this._private__hoveredPart === 'close') {
|
|
20650
|
+
return true;
|
|
20651
|
+
}
|
|
20652
|
+
return this._private__options.onClick !== undefined;
|
|
20653
|
+
}
|
|
20654
|
+
priceAt(paneY) {
|
|
20655
|
+
const price = this._private__series.coordinateToPrice(paneY);
|
|
20656
|
+
return price === null ? null : price;
|
|
20657
|
+
}
|
|
20658
|
+
/**
|
|
20659
|
+
* Where the level being dragged actually sits. The controller measures the
|
|
20660
|
+
* pointer against it so the level moves by what the pointer moved, instead
|
|
20661
|
+
* of jumping to sit under it — which matters most when the label has been
|
|
20662
|
+
* fanned away from its own line.
|
|
20663
|
+
*/
|
|
20664
|
+
dragAnchorY() {
|
|
20665
|
+
if (this._private__dragLevel === null) {
|
|
20666
|
+
return null;
|
|
20667
|
+
}
|
|
20668
|
+
const price = this._private__levelOptions(this._private__dragLevel)?.price;
|
|
20669
|
+
if (price === undefined || !isFinite(price)) {
|
|
20670
|
+
return null;
|
|
20671
|
+
}
|
|
20672
|
+
const y = this._private__series.priceToCoordinate(price);
|
|
20673
|
+
return y === null ? null : y;
|
|
20674
|
+
}
|
|
20675
|
+
beginDrag() {
|
|
20676
|
+
this._private__dragLevel = this._private__hoveredLevel;
|
|
20677
|
+
if (this._private__dragLevel === null) {
|
|
20678
|
+
return;
|
|
20679
|
+
}
|
|
20680
|
+
const price = this._private__levelOptions(this._private__dragLevel)?.price ?? null;
|
|
20681
|
+
this._private__priceBeforeDrag = price;
|
|
20682
|
+
if (price !== null && this._private__options.onDragStart) {
|
|
20683
|
+
this._private__options.onDragStart(this._private__dragLevel, price);
|
|
20684
|
+
}
|
|
20685
|
+
}
|
|
20686
|
+
dragTo(price) {
|
|
20687
|
+
if (this._private__dragLevel === null) {
|
|
20688
|
+
return;
|
|
20689
|
+
}
|
|
20690
|
+
this._private__applyPrice(this._private__dragLevel, price);
|
|
20691
|
+
if (this._private__options.onDragMove) {
|
|
20692
|
+
this._private__options.onDragMove(this._private__dragLevel, price);
|
|
20693
|
+
}
|
|
20694
|
+
}
|
|
20695
|
+
commitDrag() {
|
|
20696
|
+
const level = this._private__dragLevel;
|
|
20697
|
+
this._private__dragLevel = null;
|
|
20698
|
+
this._private__priceBeforeDrag = null;
|
|
20699
|
+
if (level === null) {
|
|
20700
|
+
return;
|
|
20701
|
+
}
|
|
20702
|
+
const price = this._private__levelOptions(level)?.price;
|
|
20703
|
+
if (price !== undefined && this._private__options.onDragEnd) {
|
|
20704
|
+
this._private__options.onDragEnd(level, price);
|
|
20705
|
+
}
|
|
20706
|
+
}
|
|
20707
|
+
cancelDrag() {
|
|
20708
|
+
const level = this._private__dragLevel;
|
|
20709
|
+
const original = this._private__priceBeforeDrag;
|
|
20710
|
+
this._private__dragLevel = null;
|
|
20711
|
+
this._private__priceBeforeDrag = null;
|
|
20712
|
+
if (level === null) {
|
|
20713
|
+
return;
|
|
20714
|
+
}
|
|
20715
|
+
// Reported even when the price did not move, so a caller that pauses work
|
|
20716
|
+
// on `onDragStart` always gets the matching end of the gesture.
|
|
20717
|
+
const restored = original !== null && original !== this._private__levelOptions(level)?.price;
|
|
20718
|
+
if (restored) {
|
|
20719
|
+
this._private__applyPrice(level, original);
|
|
20720
|
+
if (this._private__options.onDragMove) {
|
|
20721
|
+
this._private__options.onDragMove(level, original);
|
|
20722
|
+
}
|
|
20723
|
+
}
|
|
20724
|
+
if (this._private__options.onDragCancel) {
|
|
20725
|
+
this._private__options.onDragCancel(level, this._private__levelOptions(level)?.price ?? 0);
|
|
20726
|
+
}
|
|
20727
|
+
}
|
|
20728
|
+
click() {
|
|
20729
|
+
const level = this._private__hoveredLevel;
|
|
20730
|
+
if (level === null) {
|
|
20731
|
+
return;
|
|
20732
|
+
}
|
|
20733
|
+
if (this._private__hoveredPart === 'close') {
|
|
20734
|
+
if (this._private__options.onClose) {
|
|
20735
|
+
this._private__options.onClose(level);
|
|
20736
|
+
}
|
|
20737
|
+
return;
|
|
20738
|
+
}
|
|
20739
|
+
const price = this._private__levelOptions(level)?.price;
|
|
20740
|
+
if (price !== undefined && this._private__options.onClick) {
|
|
20741
|
+
this._private__options.onClick(level, price);
|
|
20742
|
+
}
|
|
20743
|
+
}
|
|
20744
|
+
// ---- public API ----
|
|
20745
|
+
/**
|
|
20746
|
+
* Updates the group. Any subset of options can be passed; the levels given
|
|
20747
|
+
* are merged into the current ones, and a level set to `undefined` is
|
|
20748
|
+
* removed.
|
|
20749
|
+
*/
|
|
20750
|
+
applyOptions(options) {
|
|
20751
|
+
// A level under an active gesture owns its price. Callers feeding the
|
|
20752
|
+
// group from a socket re-send the old price several times a second, and
|
|
20753
|
+
// letting that through would pull the level out from under the cursor.
|
|
20754
|
+
const dragged = this._private__dragLevel;
|
|
20755
|
+
const draggedPrice = dragged === null ? undefined : this._private__levelOptions(dragged)?.price;
|
|
20756
|
+
const merged = { ...this._private__options, ...options };
|
|
20757
|
+
merged.entry = { ...this._private__options.entry, ...options.entry };
|
|
20758
|
+
merged.takeProfit = mergeLevel(this._private__options.takeProfit, options, 'takeProfit');
|
|
20759
|
+
merged.stopLoss = mergeLevel(this._private__options.stopLoss, options, 'stopLoss');
|
|
20760
|
+
this._private__options = merged;
|
|
20761
|
+
if (dragged !== null && draggedPrice !== undefined) {
|
|
20762
|
+
const level = this._private__levelOptions(dragged);
|
|
20763
|
+
if (level !== null) {
|
|
20764
|
+
level.price = draggedPrice;
|
|
20765
|
+
}
|
|
20766
|
+
}
|
|
20767
|
+
if (options.theme) {
|
|
20768
|
+
// Merged onto what is already applied, so two partial theme updates do
|
|
20769
|
+
// not undo each other.
|
|
20770
|
+
this._private__theme = { ...this._private__theme, ...options.theme };
|
|
20771
|
+
}
|
|
20772
|
+
this.requestUpdate();
|
|
20773
|
+
}
|
|
20774
|
+
/**
|
|
20775
|
+
* Moves one level to a new price, recomputing its text when the level has a
|
|
20776
|
+
* `formatText`.
|
|
20777
|
+
*
|
|
20778
|
+
* Ignored for a level the user is currently dragging — the gesture owns the
|
|
20779
|
+
* price until it reports back through
|
|
20780
|
+
* {@link PositionLineOptions.onDragEnd} or
|
|
20781
|
+
* {@link PositionLineOptions.onDragCancel}.
|
|
20782
|
+
*/
|
|
20783
|
+
setPrice(level, price) {
|
|
20784
|
+
if (this._private__dragLevel === level) {
|
|
20785
|
+
return;
|
|
20786
|
+
}
|
|
20787
|
+
this._private__applyPrice(level, price);
|
|
20788
|
+
}
|
|
20789
|
+
/**
|
|
20790
|
+
* Replaces one level's label text. Use this for text that changes on its own
|
|
20791
|
+
* — a ticking P&L on a fixed entry — rather than with the price.
|
|
20792
|
+
*/
|
|
20793
|
+
setText(level, text) {
|
|
20794
|
+
const options = this._private__levelOptions(level);
|
|
20795
|
+
if (options === null) {
|
|
20796
|
+
return;
|
|
20797
|
+
}
|
|
20798
|
+
options.text = text;
|
|
20799
|
+
this.requestUpdate();
|
|
20800
|
+
}
|
|
20801
|
+
/**
|
|
20802
|
+
* The current price of each level that is present.
|
|
20803
|
+
*/
|
|
20804
|
+
prices() {
|
|
20805
|
+
const result = {};
|
|
20806
|
+
for (const level of positionLineLevels) {
|
|
20807
|
+
const options = this._private__levelOptions(level);
|
|
20808
|
+
if (options !== null) {
|
|
20809
|
+
result[level] = options.price;
|
|
20810
|
+
}
|
|
20811
|
+
}
|
|
20812
|
+
return result;
|
|
20813
|
+
}
|
|
20814
|
+
options() {
|
|
20815
|
+
return this._private__options;
|
|
20816
|
+
}
|
|
20817
|
+
/**
|
|
20818
|
+
* Removes the group from the chart and releases its share of the input
|
|
20819
|
+
* controller.
|
|
20820
|
+
*/
|
|
20821
|
+
remove() {
|
|
20822
|
+
releaseController(this._private__chart, this);
|
|
20823
|
+
releaseLabelLayout(this._private__chart, this._private__id);
|
|
20824
|
+
this._private__series.detachPrimitive(this);
|
|
20825
|
+
}
|
|
20826
|
+
requestUpdate() {
|
|
20827
|
+
if (this._private__requestUpdate) {
|
|
20828
|
+
this._private__requestUpdate();
|
|
20829
|
+
}
|
|
20830
|
+
}
|
|
20831
|
+
/** Reads back the label the price axis should draw for a level. */
|
|
20832
|
+
_private__axisLabel(level) {
|
|
20833
|
+
const options = this._private__levelOptions(level);
|
|
20834
|
+
if (options === null || !isFinite(options.price) ||
|
|
20835
|
+
this._private__options.visible === false || options.axisLabelVisible === false) {
|
|
20836
|
+
return null;
|
|
20837
|
+
}
|
|
20838
|
+
const y = this._private__series.priceToCoordinate(options.price);
|
|
20839
|
+
if (y === null) {
|
|
20840
|
+
return null;
|
|
20841
|
+
}
|
|
20842
|
+
const color = options.color ?? positionLineDefaultColors[level];
|
|
20843
|
+
return {
|
|
20844
|
+
_internal_coordinate: y,
|
|
20845
|
+
_internal_text: this._private__series.priceFormatter().format(options.price),
|
|
20846
|
+
_internal_color: color,
|
|
20847
|
+
_internal_textColor: this._private__colorParser._internal_generateContrastColors(color)._internal_foreground,
|
|
20848
|
+
};
|
|
20849
|
+
}
|
|
20850
|
+
_private__levelOptions(level) {
|
|
20851
|
+
const options = level === 'entry' ? this._private__options.entry : this._private__options[level];
|
|
20852
|
+
return options ?? null;
|
|
20853
|
+
}
|
|
20854
|
+
_private__applyPrice(level, price) {
|
|
20855
|
+
const options = this._private__levelOptions(level);
|
|
20856
|
+
if (options === null) {
|
|
20857
|
+
return;
|
|
20858
|
+
}
|
|
20859
|
+
options.price = price;
|
|
20860
|
+
if (options.formatText) {
|
|
20861
|
+
options.text = options.formatText(price);
|
|
20862
|
+
}
|
|
20863
|
+
this.requestUpdate();
|
|
20864
|
+
}
|
|
20865
|
+
_private__buildRendererData() {
|
|
20866
|
+
if (!this._private__attached || this._private__options.visible === false) {
|
|
20867
|
+
return null;
|
|
20868
|
+
}
|
|
20869
|
+
const layout = this._private__chart.options()['layout'];
|
|
20870
|
+
const fontSize = this._private__theme.fontSize ?? layout.fontSize;
|
|
20871
|
+
const labelHeight = fontSize + this._private__theme.paddingY * 2;
|
|
20872
|
+
const levels = [];
|
|
20873
|
+
const claims = [];
|
|
20874
|
+
for (const level of positionLineLevels) {
|
|
20875
|
+
const options = this._private__levelOptions(level);
|
|
20876
|
+
// A level added through a patch can be missing its price until the
|
|
20877
|
+
// caller supplies one; drawing it would put it at NaN.
|
|
20878
|
+
if (options === null || !isFinite(options.price)) {
|
|
20879
|
+
continue;
|
|
20880
|
+
}
|
|
20881
|
+
const y = this._private__series.priceToCoordinate(options.price);
|
|
20882
|
+
if (y === null) {
|
|
20883
|
+
continue;
|
|
20884
|
+
}
|
|
20885
|
+
const isEntry = level === 'entry';
|
|
20886
|
+
claims.push({ _internal_level: level, _internal_y: y, _internal_height: labelHeight });
|
|
20887
|
+
levels.push({
|
|
20888
|
+
_internal_level: level,
|
|
20889
|
+
_internal_y: y,
|
|
20890
|
+
_internal_labelOffsetY: 0,
|
|
20891
|
+
_internal_color: options.color ?? positionLineDefaultColors[level],
|
|
20892
|
+
_internal_text: options.formatText ? options.formatText(options.price) : options.text ?? '',
|
|
20893
|
+
_internal_badge: options.badge ?? '',
|
|
20894
|
+
_internal_badgeFilled: options.badgeFilled ?? isEntry,
|
|
20895
|
+
_internal_lineWidth: options.lineWidth ?? 1,
|
|
20896
|
+
_internal_lineStyle: options.lineStyle ?? (isEntry ? 0 /* LineStyle.Solid */ : 2 /* LineStyle.Dashed */),
|
|
20897
|
+
_internal_closable: options.closable ?? true,
|
|
20898
|
+
_internal_draggable: options.draggable ?? true,
|
|
20899
|
+
});
|
|
20900
|
+
}
|
|
20901
|
+
if (levels.length === 0) {
|
|
20902
|
+
this._private__labelLayout._internal_setClaims(this._private__id, []);
|
|
20903
|
+
return null;
|
|
20904
|
+
}
|
|
20905
|
+
// Published before the offsets are read so every line on the chart —
|
|
20906
|
+
// positions, pending orders, alerts — is fanned against the same set.
|
|
20907
|
+
this._private__labelLayout._internal_setClaims(this._private__id, claims);
|
|
20908
|
+
for (const level of levels) {
|
|
20909
|
+
level._internal_labelOffsetY = this._private__labelLayout._internal_offsetFor(this._private__id, level._internal_level);
|
|
20910
|
+
}
|
|
20911
|
+
return {
|
|
20912
|
+
_internal_levels: levels,
|
|
20913
|
+
_internal_background: this._private__theme.background ?? resolveBackgroundColor(layout.background),
|
|
20914
|
+
_internal_textColor: this._private__theme.textColor,
|
|
20915
|
+
_internal_badgeTextColor: this._private__theme.badgeTextColor,
|
|
20916
|
+
_internal_borderRadius: this._private__theme.borderRadius,
|
|
20917
|
+
_internal_borderWidth: this._private__theme.borderWidth,
|
|
20918
|
+
_internal_paddingX: this._private__theme.paddingX,
|
|
20919
|
+
_internal_paddingY: this._private__theme.paddingY,
|
|
20920
|
+
_internal_backgroundOpacity: this._private__theme.backgroundOpacity,
|
|
20921
|
+
_internal_font: makeFont(fontSize, this._private__theme.fontFamily ?? layout.fontFamily),
|
|
20922
|
+
_internal_fontSize: fontSize,
|
|
20923
|
+
_internal_connectorVisible: this._private__options.connectorVisible !== false && levels.length > 1,
|
|
20924
|
+
_internal_rightOffset: this._private__options.rightOffset ?? DEFAULT_RIGHT_OFFSET,
|
|
20925
|
+
_internal_hoveredLevel: this._private__hoveredLevel,
|
|
20926
|
+
_internal_hoveredPart: this._private__hoveredPart,
|
|
20927
|
+
};
|
|
20928
|
+
}
|
|
20929
|
+
}
|
|
20930
|
+
function mergeLevel(current, options, key) {
|
|
20931
|
+
if (!(key in options)) {
|
|
20932
|
+
return current;
|
|
20933
|
+
}
|
|
20934
|
+
const next = options[key];
|
|
20935
|
+
return next === undefined ? undefined : { ...current, ...next };
|
|
20936
|
+
}
|
|
20937
|
+
/**
|
|
20938
|
+
* Reads the chart's own background so the label sits on a solid surface. The
|
|
20939
|
+
* parameter is typed as the public {@link Background}: the production build
|
|
20940
|
+
* renames the members of structural types it does not see in the public API,
|
|
20941
|
+
* and an anonymous `{ color: string }` here resolved to nothing at runtime.
|
|
20942
|
+
*/
|
|
20943
|
+
function resolveBackgroundColor(background) {
|
|
20944
|
+
return background.type === "solid" /* ColorType.Solid */ ? background.color : background.topColor;
|
|
20945
|
+
}
|
|
20946
|
+
/**
|
|
20947
|
+
* Creates a position line group: entry, take profit and stop loss drawn as one
|
|
20948
|
+
* interactive unit.
|
|
20949
|
+
*
|
|
20950
|
+
* @param series - The series the levels are priced against.
|
|
20951
|
+
* @param chart - The chart the series belongs to.
|
|
20952
|
+
* @param options - The levels and their style.
|
|
20953
|
+
*
|
|
20954
|
+
* @example
|
|
20955
|
+
* ```js
|
|
20956
|
+
* const position = createPositionLine(candleSeries, chart, {
|
|
20957
|
+
* entry: { price: 4306.79, badge: '1', formatText: () => '+ 2.32 USD' },
|
|
20958
|
+
* takeProfit: { price: 4349.58, badge: '1', text: '+ 42.79 USD' },
|
|
20959
|
+
* stopLoss: { price: 4266.06, badge: '1', text: '- 40.73 USD' },
|
|
20960
|
+
* onClose: (level) => console.log('close', level),
|
|
20961
|
+
* onDragEnd: (level, price) => console.log(level, 'moved to', price),
|
|
20962
|
+
* });
|
|
20963
|
+
* ```
|
|
20964
|
+
*/
|
|
20965
|
+
function createPositionLine(series, chart, options) {
|
|
20966
|
+
return new PositionLine(chart, series, options);
|
|
20967
|
+
}
|
|
20968
|
+
|
|
19858
20969
|
/** Hit parts that drag the whole object rather than a single handle. */
|
|
19859
20970
|
const WHOLE_OBJECT_PARTS = new Set([
|
|
19860
20971
|
'line',
|
|
@@ -20734,13 +21845,6 @@ class TrendLineTool extends DrawingToolBase {
|
|
|
20734
21845
|
// Clean up references
|
|
20735
21846
|
this._private__primitive = null;
|
|
20736
21847
|
this._private__paneView = null;
|
|
20737
|
-
// Force chart update by triggering a resize
|
|
20738
|
-
const chartElement = this._chart.chartElement?.();
|
|
20739
|
-
if (chartElement) {
|
|
20740
|
-
const currentWidth = chartElement.clientWidth;
|
|
20741
|
-
const currentHeight = chartElement.clientHeight;
|
|
20742
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
20743
|
-
}
|
|
20744
21848
|
}
|
|
20745
21849
|
if (this._private__trendOptions.onRemove) {
|
|
20746
21850
|
this._private__trendOptions.onRemove();
|
|
@@ -21013,12 +22117,6 @@ class HorizontalLineTool extends DrawingToolBase {
|
|
|
21013
22117
|
this._series.detachPrimitive(this._private__primitive);
|
|
21014
22118
|
this._private__primitive = null;
|
|
21015
22119
|
this._private__paneView = null;
|
|
21016
|
-
const chartElement = this._chart.chartElement?.();
|
|
21017
|
-
if (chartElement) {
|
|
21018
|
-
const currentWidth = chartElement.clientWidth;
|
|
21019
|
-
const currentHeight = chartElement.clientHeight;
|
|
21020
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
21021
|
-
}
|
|
21022
22120
|
}
|
|
21023
22121
|
if (this._private__hLineOptions.onRemove) {
|
|
21024
22122
|
this._private__hLineOptions.onRemove();
|
|
@@ -22857,12 +23955,6 @@ class VerticalLineTool extends DrawingToolBase {
|
|
|
22857
23955
|
this._series.detachPrimitive(this._private__primitive);
|
|
22858
23956
|
this._private__primitive = null;
|
|
22859
23957
|
this._private__paneView = null;
|
|
22860
|
-
const chartElement = this._chart.chartElement?.();
|
|
22861
|
-
if (chartElement) {
|
|
22862
|
-
const currentWidth = chartElement.clientWidth;
|
|
22863
|
-
const currentHeight = chartElement.clientHeight;
|
|
22864
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
22865
|
-
}
|
|
22866
23958
|
}
|
|
22867
23959
|
if (this._private__vLineOptions.onRemove) {
|
|
22868
23960
|
this._private__vLineOptions.onRemove();
|
|
@@ -23515,12 +24607,6 @@ class FibonacciTool extends DrawingToolBase {
|
|
|
23515
24607
|
this._series.detachPrimitive(this._private__primitive);
|
|
23516
24608
|
this._private__primitive = null;
|
|
23517
24609
|
this._private__paneView = null;
|
|
23518
|
-
const chartElement = this._chart.chartElement?.();
|
|
23519
|
-
if (chartElement) {
|
|
23520
|
-
const currentWidth = chartElement.clientWidth;
|
|
23521
|
-
const currentHeight = chartElement.clientHeight;
|
|
23522
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
23523
|
-
}
|
|
23524
24610
|
}
|
|
23525
24611
|
if (this._private__drawingManager) {
|
|
23526
24612
|
this._private__drawingManager._internal_destroy();
|
|
@@ -23536,12 +24622,6 @@ class FibonacciTool extends DrawingToolBase {
|
|
|
23536
24622
|
this._series.detachPrimitive(this._private__primitive);
|
|
23537
24623
|
this._private__primitive = null;
|
|
23538
24624
|
this._private__paneView = null;
|
|
23539
|
-
const chartElement = this._chart.chartElement?.();
|
|
23540
|
-
if (chartElement) {
|
|
23541
|
-
const currentWidth = chartElement.clientWidth;
|
|
23542
|
-
const currentHeight = chartElement.clientHeight;
|
|
23543
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
23544
|
-
}
|
|
23545
24625
|
}
|
|
23546
24626
|
if (this._private__fibOptions.onRemove) {
|
|
23547
24627
|
this._private__fibOptions.onRemove();
|
|
@@ -23969,12 +25049,6 @@ class FibonacciExtensionTool extends DrawingToolBase {
|
|
|
23969
25049
|
this._series.detachPrimitive(this._private__primitive);
|
|
23970
25050
|
this._private__primitive = null;
|
|
23971
25051
|
this._private__paneView = null;
|
|
23972
|
-
const chartElement = this._chart.chartElement?.();
|
|
23973
|
-
if (chartElement) {
|
|
23974
|
-
const currentWidth = chartElement.clientWidth;
|
|
23975
|
-
const currentHeight = chartElement.clientHeight;
|
|
23976
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
23977
|
-
}
|
|
23978
25052
|
}
|
|
23979
25053
|
}
|
|
23980
25054
|
}
|
|
@@ -24416,12 +25490,6 @@ class FibonacciFanTool extends DrawingToolBase {
|
|
|
24416
25490
|
this._series.detachPrimitive(this._private__primitive);
|
|
24417
25491
|
this._private__primitive = null;
|
|
24418
25492
|
this._private__paneView = null;
|
|
24419
|
-
const chartElement = this._chart.chartElement?.();
|
|
24420
|
-
if (chartElement) {
|
|
24421
|
-
const currentWidth = chartElement.clientWidth;
|
|
24422
|
-
const currentHeight = chartElement.clientHeight;
|
|
24423
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
24424
|
-
}
|
|
24425
25493
|
}
|
|
24426
25494
|
if (this._private__fanOptions.onRemove) {
|
|
24427
25495
|
this._private__fanOptions.onRemove();
|
|
@@ -24962,12 +26030,6 @@ class RectangleTool extends DrawingToolBase {
|
|
|
24962
26030
|
this._series.detachPrimitive(this._private__primitive);
|
|
24963
26031
|
this._private__primitive = null;
|
|
24964
26032
|
this._private__paneView = null;
|
|
24965
|
-
const chartElement = this._chart.chartElement?.();
|
|
24966
|
-
if (chartElement) {
|
|
24967
|
-
const currentWidth = chartElement.clientWidth;
|
|
24968
|
-
const currentHeight = chartElement.clientHeight;
|
|
24969
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
24970
|
-
}
|
|
24971
26033
|
}
|
|
24972
26034
|
if (this._private__rectOptions.onRemove) {
|
|
24973
26035
|
this._private__rectOptions.onRemove();
|
|
@@ -25428,12 +26490,6 @@ class CircleTool extends DrawingToolBase {
|
|
|
25428
26490
|
this._series.detachPrimitive(this._private__primitive);
|
|
25429
26491
|
this._private__primitive = null;
|
|
25430
26492
|
this._private__paneView = null;
|
|
25431
|
-
const chartElement = this._chart.chartElement?.();
|
|
25432
|
-
if (chartElement) {
|
|
25433
|
-
const currentWidth = chartElement.clientWidth;
|
|
25434
|
-
const currentHeight = chartElement.clientHeight;
|
|
25435
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
25436
|
-
}
|
|
25437
26493
|
}
|
|
25438
26494
|
if (this._private__circleOptions.onRemove) {
|
|
25439
26495
|
this._private__circleOptions.onRemove();
|
|
@@ -25983,12 +27039,6 @@ class ArrowTool extends DrawingToolBase {
|
|
|
25983
27039
|
this._series.detachPrimitive(this._private__primitive);
|
|
25984
27040
|
this._private__primitive = null;
|
|
25985
27041
|
this._private__paneView = null;
|
|
25986
|
-
const chartElement = this._chart.chartElement?.();
|
|
25987
|
-
if (chartElement) {
|
|
25988
|
-
const currentWidth = chartElement.clientWidth;
|
|
25989
|
-
const currentHeight = chartElement.clientHeight;
|
|
25990
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
25991
|
-
}
|
|
25992
27042
|
}
|
|
25993
27043
|
if (this._private__arrowOptions.onRemove) {
|
|
25994
27044
|
this._private__arrowOptions.onRemove();
|
|
@@ -26849,12 +27899,6 @@ class TextTool extends DrawingToolBase {
|
|
|
26849
27899
|
this._series.detachPrimitive(this._private__primitive);
|
|
26850
27900
|
this._private__primitive = null;
|
|
26851
27901
|
this._private__paneView = null;
|
|
26852
|
-
const chartElement = this._chart.chartElement?.();
|
|
26853
|
-
if (chartElement) {
|
|
26854
|
-
const currentWidth = chartElement.clientWidth;
|
|
26855
|
-
const currentHeight = chartElement.clientHeight;
|
|
26856
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
26857
|
-
}
|
|
26858
27902
|
}
|
|
26859
27903
|
if (this._private__textOptions.onRemove) {
|
|
26860
27904
|
this._private__textOptions.onRemove();
|
|
@@ -27437,12 +28481,6 @@ class RulerTool extends DrawingToolBase {
|
|
|
27437
28481
|
this._series.detachPrimitive(this._private__primitive);
|
|
27438
28482
|
this._private__primitive = null;
|
|
27439
28483
|
this._private__paneView = null;
|
|
27440
|
-
const chartElement = this._chart.chartElement?.();
|
|
27441
|
-
if (chartElement) {
|
|
27442
|
-
const currentWidth = chartElement.clientWidth;
|
|
27443
|
-
const currentHeight = chartElement.clientHeight;
|
|
27444
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
27445
|
-
}
|
|
27446
28484
|
}
|
|
27447
28485
|
if (this._private__drawingManager) {
|
|
27448
28486
|
this._private__drawingManager._internal_destroy();
|
|
@@ -28298,12 +29336,6 @@ class PositionTool extends DrawingToolBase {
|
|
|
28298
29336
|
this._series.detachPrimitive(this._private__primitive);
|
|
28299
29337
|
this._private__primitive = null;
|
|
28300
29338
|
this._private__paneView = null;
|
|
28301
|
-
const chartElement = this._chart.chartElement?.();
|
|
28302
|
-
if (chartElement) {
|
|
28303
|
-
const currentWidth = chartElement.clientWidth;
|
|
28304
|
-
const currentHeight = chartElement.clientHeight;
|
|
28305
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
28306
|
-
}
|
|
28307
29339
|
}
|
|
28308
29340
|
if (this._private__positionOptions.onRemove) {
|
|
28309
29341
|
this._private__positionOptions.onRemove();
|
|
@@ -28646,12 +29678,6 @@ class GanttTool extends DrawingToolBase {
|
|
|
28646
29678
|
this._series.detachPrimitive(this._private__primitive);
|
|
28647
29679
|
this._private__primitive = null;
|
|
28648
29680
|
this._private__paneView = null;
|
|
28649
|
-
const chartElement = this._chart.chartElement?.();
|
|
28650
|
-
if (chartElement) {
|
|
28651
|
-
const currentWidth = chartElement.clientWidth;
|
|
28652
|
-
const currentHeight = chartElement.clientHeight;
|
|
28653
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
28654
|
-
}
|
|
28655
29681
|
}
|
|
28656
29682
|
if (this._private__ganttOptions.onRemove) {
|
|
28657
29683
|
this._private__ganttOptions.onRemove();
|
|
@@ -28662,6 +29688,8 @@ function createGanttTool(context, options) {
|
|
|
28662
29688
|
return new GanttTool(context, options);
|
|
28663
29689
|
}
|
|
28664
29690
|
|
|
29691
|
+
/** Undo steps kept, each a full snapshot of every drawing. */
|
|
29692
|
+
const UNDO_HISTORY_LIMIT = 100;
|
|
28665
29693
|
/**
|
|
28666
29694
|
* Central manager for all drawing tools - TradingView style
|
|
28667
29695
|
* Handles multiple tools, selection, deletion, and keyboard shortcuts
|
|
@@ -28684,6 +29712,8 @@ class DrawingToolsManager {
|
|
|
28684
29712
|
this._private__pointerDownHandler = null;
|
|
28685
29713
|
this._private__pointerUpHandler = null;
|
|
28686
29714
|
this._private__pointerCancelHandler = null;
|
|
29715
|
+
this._private__lostCaptureHandler = null;
|
|
29716
|
+
this._private__windowPointerUpHandler = null;
|
|
28687
29717
|
this._private__pointerId = null;
|
|
28688
29718
|
/** A press that grabbed a tool already selected it; its click must not undo that. */
|
|
28689
29719
|
this._private__gestureHandled = false;
|
|
@@ -28849,6 +29879,9 @@ class DrawingToolsManager {
|
|
|
28849
29879
|
return;
|
|
28850
29880
|
}
|
|
28851
29881
|
this._private__past.push(this._private__committed);
|
|
29882
|
+
if (this._private__past.length > UNDO_HISTORY_LIMIT) {
|
|
29883
|
+
this._private__past.shift();
|
|
29884
|
+
}
|
|
28852
29885
|
this._private__future = [];
|
|
28853
29886
|
this._private__committed = this.serializeAll();
|
|
28854
29887
|
}
|
|
@@ -29228,8 +30261,12 @@ class DrawingToolsManager {
|
|
|
29228
30261
|
this._private__completionCheck = null;
|
|
29229
30262
|
}
|
|
29230
30263
|
this._private__drawingCompleted._internal_destroy();
|
|
30264
|
+
this._private__selectionChanged._internal_destroy();
|
|
29231
30265
|
this.clearAll();
|
|
29232
30266
|
this._private__detachEventHandlers();
|
|
30267
|
+
this._private__past = [];
|
|
30268
|
+
this._private__future = [];
|
|
30269
|
+
this._private__committed = [];
|
|
29233
30270
|
}
|
|
29234
30271
|
_private__attachEventHandlers() {
|
|
29235
30272
|
// Click handler for selection
|
|
@@ -29249,9 +30286,15 @@ class DrawingToolsManager {
|
|
|
29249
30286
|
this._private__pointerDownHandler = (event) => this._private__handlePointerDown(event);
|
|
29250
30287
|
this._private__pointerUpHandler = (event) => this._private__handlePointerUp(event);
|
|
29251
30288
|
this._private__pointerCancelHandler = (event) => this._private__handlePointerUp(event);
|
|
30289
|
+
this._private__lostCaptureHandler = (event) => this._private__handleLostPointerCapture(event);
|
|
29252
30290
|
chartElement.addEventListener('pointerdown', this._private__pointerDownHandler);
|
|
29253
30291
|
chartElement.addEventListener('pointerup', this._private__pointerUpHandler);
|
|
29254
30292
|
chartElement.addEventListener('pointercancel', this._private__pointerCancelHandler);
|
|
30293
|
+
chartElement.addEventListener('lostpointercapture', this._private__lostCaptureHandler);
|
|
30294
|
+
}
|
|
30295
|
+
if (isRunningOnClientSide) {
|
|
30296
|
+
this._private__windowPointerUpHandler = (event) => this._private__handlePointerUp(event);
|
|
30297
|
+
window.addEventListener('pointerup', this._private__windowPointerUpHandler);
|
|
29255
30298
|
}
|
|
29256
30299
|
// Keyboard handler for shortcuts
|
|
29257
30300
|
if (this._private__options.enableKeyboardShortcuts) {
|
|
@@ -29284,6 +30327,14 @@ class DrawingToolsManager {
|
|
|
29284
30327
|
chartElement.removeEventListener('pointercancel', this._private__pointerCancelHandler);
|
|
29285
30328
|
this._private__pointerCancelHandler = null;
|
|
29286
30329
|
}
|
|
30330
|
+
if (this._private__lostCaptureHandler) {
|
|
30331
|
+
chartElement.removeEventListener('lostpointercapture', this._private__lostCaptureHandler);
|
|
30332
|
+
this._private__lostCaptureHandler = null;
|
|
30333
|
+
}
|
|
30334
|
+
}
|
|
30335
|
+
if (this._private__windowPointerUpHandler) {
|
|
30336
|
+
window.removeEventListener('pointerup', this._private__windowPointerUpHandler);
|
|
30337
|
+
this._private__windowPointerUpHandler = null;
|
|
29287
30338
|
}
|
|
29288
30339
|
if (this._private__keyboardHandler) {
|
|
29289
30340
|
document.removeEventListener('keydown', this._private__keyboardHandler);
|
|
@@ -29409,7 +30460,12 @@ class DrawingToolsManager {
|
|
|
29409
30460
|
this._private__gestureHandled = true;
|
|
29410
30461
|
this._private__draggedToolId = toolId;
|
|
29411
30462
|
this._private__pointerId = event.pointerId;
|
|
29412
|
-
|
|
30463
|
+
try {
|
|
30464
|
+
chartElement.setPointerCapture(event.pointerId);
|
|
30465
|
+
}
|
|
30466
|
+
catch (error) {
|
|
30467
|
+
this._private__pointerId = null;
|
|
30468
|
+
}
|
|
29413
30469
|
tool.startDrag(endpoint, anchor);
|
|
29414
30470
|
// Panning is stopped by disabling it for the gesture, not by preventing
|
|
29415
30471
|
// the event: preventDefault on pointerdown suppresses the compatibility
|
|
@@ -29423,17 +30479,32 @@ class DrawingToolsManager {
|
|
|
29423
30479
|
if (event && this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
|
|
29424
30480
|
return;
|
|
29425
30481
|
}
|
|
29426
|
-
const
|
|
29427
|
-
|
|
29428
|
-
|
|
29429
|
-
}
|
|
29430
|
-
this._private__releasePointer();
|
|
29431
|
-
this._private__enableChartInteractions();
|
|
30482
|
+
const draggedToolId = this._private__draggedToolId;
|
|
30483
|
+
// Cleared first: anything below can throw, and a half-finished release
|
|
30484
|
+
// would leave the manager dragging forever.
|
|
29432
30485
|
this._private__isDragging = false;
|
|
29433
30486
|
this._private__draggedToolId = null;
|
|
30487
|
+
try {
|
|
30488
|
+
const tool = this._private__tools.get(draggedToolId);
|
|
30489
|
+
if (tool) {
|
|
30490
|
+
tool.stopDrag();
|
|
30491
|
+
}
|
|
30492
|
+
}
|
|
30493
|
+
finally {
|
|
30494
|
+
this._private__releasePointer();
|
|
30495
|
+
this._private__enableChartInteractions();
|
|
30496
|
+
}
|
|
29434
30497
|
// A finished drag is one step, not one per frame
|
|
29435
30498
|
this.recordChange();
|
|
29436
30499
|
}
|
|
30500
|
+
/** Ends the drag when capture is lost without a pointerup ever arriving. */
|
|
30501
|
+
_private__handleLostPointerCapture(event) {
|
|
30502
|
+
if (this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
|
|
30503
|
+
return;
|
|
30504
|
+
}
|
|
30505
|
+
this._private__pointerId = null;
|
|
30506
|
+
this._private__handlePointerUp();
|
|
30507
|
+
}
|
|
29437
30508
|
/**
|
|
29438
30509
|
* Pane element of a tool's series. The engine asserts the series has a pane,
|
|
29439
30510
|
* so the lookup is guarded.
|
|
@@ -29484,9 +30555,11 @@ class DrawingToolsManager {
|
|
|
29484
30555
|
return;
|
|
29485
30556
|
}
|
|
29486
30557
|
const options = this._private__chart.options();
|
|
30558
|
+
// `options()` returns the live object and `applyOptions` merges into it
|
|
30559
|
+
// in place, so a snapshot by reference restores all-false.
|
|
29487
30560
|
this._private__interactionsBeforeDrag = {
|
|
29488
|
-
_internal_handleScroll: options.handleScroll,
|
|
29489
|
-
_internal_handleScale: options.handleScale,
|
|
30561
|
+
_internal_handleScroll: clone(options.handleScroll),
|
|
30562
|
+
_internal_handleScale: clone(options.handleScale),
|
|
29490
30563
|
};
|
|
29491
30564
|
this._private__chart.applyOptions({
|
|
29492
30565
|
handleScroll: false,
|
|
@@ -29572,7 +30645,7 @@ const customSeriesDefaultOptions = {
|
|
|
29572
30645
|
* Returns the current version as a string. For example `'1.0.0'`.
|
|
29573
30646
|
*/
|
|
29574
30647
|
function version() {
|
|
29575
|
-
return "2.
|
|
30648
|
+
return "2.3.1";
|
|
29576
30649
|
}
|
|
29577
30650
|
|
|
29578
|
-
export { areaSeries as AreaSeries, ArrowTool, barSeries as BarSeries, baselineSeries as BaselineSeries, BrushTool, candlestickSeries as CandlestickSeries, CircleTool, ColorType, CrossLineTool, CrosshairMode, DEFAULT_SESSIONS, DraggablePriceLine, DrawingToolsManager, EXTENDED_FIBONACCI_RATIOS, FibonacciCalculator, FibonacciExtensionTool, FibonacciFanTool, FibonacciTool, GanttTool, histogramSeries as HistogramSeries, HorizontalLineTool, InteractiveLineManager, LastPriceAnimationMode, lineSeries as LineSeries, LineStyle, LineType, MismatchDirection, ParallelChannelTool, PitchforkTool, PolygonTool, PositionCalculator, PositionMarkersManager, PositionTool, PriceLineSource, PriceScaleMode, RectangleTool, RulerCalculator, RulerTool, STANDARD_FIBONACCI_RATIOS, TextTool, TickMarkType, TrackingModeExitMode, TradingSessionsIndicator, TrendLineTool, VerticalLineTool, VolumeProfileIndicator, applyADX, applyATR, applyBollingerBands, applyCCI, applyDonchianChannels, applyEMA, applyHeikinAshi, applyIchimoku, applyKeltnerChannels, applyMACD, applyMFI, applyOBV, applyParabolicSAR, applyPivotPoints, applyRSI, applySMA, applyStochastic, applySuperTrend, applyVWAP, applyVolume, applyWMA, applyWilliamsR, calculateHeikinAshi, calculatePipConfig, coordinateFromTime, createArrowTool, createBrushTool, createChart, createChartEx, createCircleTool, createCrossLineTool, createFibonacciExtensionTool, createFibonacciFanTool, createFibonacciTool, createGanttTool, createHorizontalLineTool, createImageWatermark, createInteractiveLineManager, createOptionsChart, createParallelChannelTool, createPitchforkTool, createPolygonTool, createPositionMarkers, createPositionTool, createRectangleTool, createRulerTool, createSeriesMarkers, createTextTool, createTextWatermark, createTradingLine, createTradingSessionsIndicator, createTrendLineTool, createUpDownMarkers, createVerticalLineTool, createVolumeProfileIndicator, createYieldCurveChart, customSeriesDefaultOptions, defaultHorzScaleBehavior, getPipConfig, hideSeriesPaint, isBusinessDay, isUTCTimestamp, setSeriesVolumes, setVolumeProfileData, timeFromCoordinate, version };
|
|
30651
|
+
export { areaSeries as AreaSeries, ArrowTool, barSeries as BarSeries, baselineSeries as BaselineSeries, BrushTool, candlestickSeries as CandlestickSeries, CircleTool, ColorType, CrossLineTool, CrosshairMode, DEFAULT_SESSIONS, DraggablePriceLine, DrawingToolsManager, EXTENDED_FIBONACCI_RATIOS, FibonacciCalculator, FibonacciExtensionTool, FibonacciFanTool, FibonacciTool, GanttTool, histogramSeries as HistogramSeries, HorizontalLineTool, InteractiveLineManager, LastPriceAnimationMode, lineSeries as LineSeries, LineStyle, LineType, MismatchDirection, ParallelChannelTool, PitchforkTool, PolygonTool, PositionCalculator, PositionLine, PositionMarkersManager, PositionTool, PriceLineSource, PriceScaleMode, RectangleTool, RulerCalculator, RulerTool, STANDARD_FIBONACCI_RATIOS, TextTool, TickMarkType, TrackingModeExitMode, TradingSessionsIndicator, TrendLineTool, VerticalLineTool, VolumeProfileIndicator, applyADX, applyATR, applyBollingerBands, applyCCI, applyDonchianChannels, applyEMA, applyHeikinAshi, applyIchimoku, applyKeltnerChannels, applyMACD, applyMFI, applyOBV, applyParabolicSAR, applyPivotPoints, applyRSI, applySMA, applyStochastic, applySuperTrend, applyVWAP, applyVolume, applyWMA, applyWilliamsR, calculateHeikinAshi, calculatePipConfig, coordinateFromTime, createArrowTool, createBrushTool, createChart, createChartEx, createCircleTool, createCrossLineTool, createFibonacciExtensionTool, createFibonacciFanTool, createFibonacciTool, createGanttTool, createHorizontalLineTool, createImageWatermark, createInteractiveLineManager, createOptionsChart, createParallelChannelTool, createPitchforkTool, createPolygonTool, createPositionLine, createPositionMarkers, createPositionTool, createRectangleTool, createRulerTool, createSeriesMarkers, createTextTool, createTextWatermark, createTradingLine, createTradingSessionsIndicator, createTrendLineTool, createUpDownMarkers, createVerticalLineTool, createVolumeProfileIndicator, createYieldCurveChart, customSeriesDefaultOptions, defaultHorzScaleBehavior, getPipConfig, hideSeriesPaint, isBusinessDay, isUTCTimestamp, setSeriesVolumes, setVolumeProfileData, timeFromCoordinate, version };
|