@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)
|
|
@@ -14641,6 +14641,8 @@
|
|
|
14641
14641
|
return size(originalSize, 0.8);
|
|
14642
14642
|
case 'square':
|
|
14643
14643
|
return size(originalSize, 0.7);
|
|
14644
|
+
case 'tag':
|
|
14645
|
+
return size(originalSize, 1);
|
|
14644
14646
|
}
|
|
14645
14647
|
}
|
|
14646
14648
|
function calculateShapeHeight(barSpacing) {
|
|
@@ -14750,6 +14752,85 @@
|
|
|
14750
14752
|
y >= top && y <= top + squareSize;
|
|
14751
14753
|
}
|
|
14752
14754
|
|
|
14755
|
+
const TAIL_HALF_WIDTH_RATIO = 0.62;
|
|
14756
|
+
/**
|
|
14757
|
+
* Lays the pointer into the edge the path is already travelling along, so the
|
|
14758
|
+
* bubble and its tail are a single outline. Two subpaths would leave a stroked
|
|
14759
|
+
* seam where they meet.
|
|
14760
|
+
*/
|
|
14761
|
+
// eslint-disable-next-line max-params
|
|
14762
|
+
function tailPath(ctx, xCenter, edgeY, tailHeight, halfWidth, up) {
|
|
14763
|
+
// A tip with a small radius reads as drawn rather than as an artefact of the
|
|
14764
|
+
// rasteriser, which a bare triangle does at these sizes.
|
|
14765
|
+
const tipRadius = Math.min(halfWidth / 3, tailHeight / 3);
|
|
14766
|
+
const sign = up ? -1 : 1;
|
|
14767
|
+
const tipY = edgeY + sign * tailHeight;
|
|
14768
|
+
const shoulderY = tipY - sign * tipRadius;
|
|
14769
|
+
// The top edge is travelled left to right, the bottom edge right to left.
|
|
14770
|
+
const entry = up ? -halfWidth : halfWidth;
|
|
14771
|
+
ctx.lineTo(xCenter + entry, edgeY);
|
|
14772
|
+
ctx.lineTo(xCenter + (up ? -tipRadius : tipRadius), shoulderY);
|
|
14773
|
+
ctx.quadraticCurveTo(xCenter, tipY, xCenter + (up ? tipRadius : -tipRadius), shoulderY);
|
|
14774
|
+
ctx.lineTo(xCenter - entry, edgeY);
|
|
14775
|
+
}
|
|
14776
|
+
/**
|
|
14777
|
+
* Draws a rounded speech-bubble tag with an optional pointer towards its anchor.
|
|
14778
|
+
*/
|
|
14779
|
+
function drawTag(ctx, params) {
|
|
14780
|
+
const hpr = params._internal_horizontalPixelRatio;
|
|
14781
|
+
const vpr = params._internal_verticalPixelRatio;
|
|
14782
|
+
const left = Math.round((params._internal_x - params._internal_width / 2) * hpr);
|
|
14783
|
+
const right = Math.round((params._internal_x + params._internal_width / 2) * hpr);
|
|
14784
|
+
const top = Math.round((params._internal_centerY - params._internal_height / 2) * vpr);
|
|
14785
|
+
const bottom = Math.round((params._internal_centerY + params._internal_height / 2) * vpr);
|
|
14786
|
+
const xCenter = Math.round(params._internal_x * hpr);
|
|
14787
|
+
const radius = Math.min(params._internal_radius * hpr, (right - left) / 2, (bottom - top) / 2);
|
|
14788
|
+
const tailHeight = params._internal_tailHeight * vpr;
|
|
14789
|
+
const tailHalfWidth = Math.min(tailHeight * TAIL_HALF_WIDTH_RATIO, Math.max(0, (right - left) / 2 - radius));
|
|
14790
|
+
const hasTail = params._internal_tailDirection !== 'none' && tailHalfWidth > 0 && tailHeight > 0;
|
|
14791
|
+
ctx.save();
|
|
14792
|
+
ctx.beginPath();
|
|
14793
|
+
if (!hasTail) {
|
|
14794
|
+
drawRoundRect(ctx, left, top, right - left, bottom - top, [radius, radius, radius, radius]);
|
|
14795
|
+
}
|
|
14796
|
+
else {
|
|
14797
|
+
ctx.moveTo(left + radius, top);
|
|
14798
|
+
if (params._internal_tailDirection === 'up') {
|
|
14799
|
+
tailPath(ctx, xCenter, top, tailHeight, tailHalfWidth, true);
|
|
14800
|
+
}
|
|
14801
|
+
ctx.lineTo(right - radius, top);
|
|
14802
|
+
ctx.arcTo(right, top, right, top + radius, radius);
|
|
14803
|
+
ctx.lineTo(right, bottom - radius);
|
|
14804
|
+
ctx.arcTo(right, bottom, right - radius, bottom, radius);
|
|
14805
|
+
if (params._internal_tailDirection === 'down') {
|
|
14806
|
+
tailPath(ctx, xCenter, bottom, tailHeight, tailHalfWidth, false);
|
|
14807
|
+
}
|
|
14808
|
+
ctx.lineTo(left + radius, bottom);
|
|
14809
|
+
ctx.arcTo(left, bottom, left, bottom - radius, radius);
|
|
14810
|
+
ctx.lineTo(left, top + radius);
|
|
14811
|
+
ctx.arcTo(left, top, left + radius, top, radius);
|
|
14812
|
+
}
|
|
14813
|
+
ctx.closePath();
|
|
14814
|
+
ctx.fillStyle = params._internal_fillColor;
|
|
14815
|
+
ctx.fill();
|
|
14816
|
+
if (params._internal_borderColor !== undefined && params._internal_borderWidth > 0) {
|
|
14817
|
+
ctx.lineWidth = Math.max(1, Math.round(params._internal_borderWidth * hpr));
|
|
14818
|
+
ctx.strokeStyle = params._internal_borderColor;
|
|
14819
|
+
ctx.stroke();
|
|
14820
|
+
}
|
|
14821
|
+
ctx.restore();
|
|
14822
|
+
}
|
|
14823
|
+
function hitTestTag(centerX, centerY, tag, x, y) {
|
|
14824
|
+
if (tag._internal_width <= 0) {
|
|
14825
|
+
return false;
|
|
14826
|
+
}
|
|
14827
|
+
const halfHeight = tag._internal_height / 2;
|
|
14828
|
+
const top = tag._internal_tailDirection === 'up' ? centerY - halfHeight - tag._internal_tailHeight : centerY - halfHeight;
|
|
14829
|
+
const bottom = tag._internal_tailDirection === 'down' ? centerY + halfHeight + tag._internal_tailHeight : centerY + halfHeight;
|
|
14830
|
+
const left = centerX - tag._internal_width / 2;
|
|
14831
|
+
return x >= left && x <= left + tag._internal_width && y >= top && y <= bottom;
|
|
14832
|
+
}
|
|
14833
|
+
|
|
14753
14834
|
function drawText(ctx, text, x, y, horizontalPixelRatio, verticalPixelRatio) {
|
|
14754
14835
|
ctx.save();
|
|
14755
14836
|
ctx.scale(horizontalPixelRatio, verticalPixelRatio);
|
|
@@ -14766,9 +14847,11 @@
|
|
|
14766
14847
|
constructor() {
|
|
14767
14848
|
this._private__data = null;
|
|
14768
14849
|
this._private__textWidthCache = new TextWidthCache();
|
|
14850
|
+
this._private__tagTextWidthCache = new TextWidthCache();
|
|
14769
14851
|
this._private__fontSize = -1;
|
|
14770
14852
|
this._private__fontFamily = '';
|
|
14771
14853
|
this._private__font = '';
|
|
14854
|
+
this._private__tagFont = '';
|
|
14772
14855
|
this._private__zOrder = 'normal';
|
|
14773
14856
|
}
|
|
14774
14857
|
_internal_setData(data) {
|
|
@@ -14779,7 +14862,11 @@
|
|
|
14779
14862
|
this._private__fontSize = fontSize;
|
|
14780
14863
|
this._private__fontFamily = fontFamily;
|
|
14781
14864
|
this._private__font = makeFont(fontSize, fontFamily);
|
|
14865
|
+
// Tags carry a label rather than an annotation, so they get the heavier
|
|
14866
|
+
// weight — and their own cache, since widths differ per font.
|
|
14867
|
+
this._private__tagFont = makeFont(fontSize, fontFamily, '600');
|
|
14782
14868
|
this._private__textWidthCache._internal_reset();
|
|
14869
|
+
this._private__tagTextWidthCache._internal_reset();
|
|
14783
14870
|
}
|
|
14784
14871
|
this._private__zOrder = zOrder;
|
|
14785
14872
|
}
|
|
@@ -14819,14 +14906,21 @@
|
|
|
14819
14906
|
return;
|
|
14820
14907
|
}
|
|
14821
14908
|
ctx.textBaseline = 'middle';
|
|
14822
|
-
ctx.font = this._private__font;
|
|
14823
14909
|
for (let index = this._private__data._internal_visibleRange.from; index < this._private__data._internal_visibleRange.to; index++) {
|
|
14824
14910
|
const item = this._private__data._internal_items[index];
|
|
14911
|
+
const isTag = item._internal_tag !== undefined;
|
|
14912
|
+
ctx.font = isTag ? this._private__tagFont : this._private__font;
|
|
14825
14913
|
if (item._internal_text !== undefined) {
|
|
14826
|
-
|
|
14914
|
+
const cache = isTag ? this._private__tagTextWidthCache : this._private__textWidthCache;
|
|
14915
|
+
item._internal_text._internal_width = cache._internal_measureText(ctx, item._internal_text._internal_content);
|
|
14827
14916
|
item._internal_text._internal_height = this._private__fontSize;
|
|
14828
14917
|
item._internal_text._internal_x = item._internal_x - item._internal_text._internal_width / 2;
|
|
14829
14918
|
}
|
|
14919
|
+
if (item._internal_tag !== undefined) {
|
|
14920
|
+
const textWidth = item._internal_text !== undefined ? item._internal_text._internal_width : 0;
|
|
14921
|
+
item._internal_tag._internal_width = Math.max(textWidth + item._internal_tag._internal_paddingX * 2, item._internal_tag._internal_height);
|
|
14922
|
+
item._internal_tag._internal_left = item._internal_x - item._internal_tag._internal_width / 2;
|
|
14923
|
+
}
|
|
14830
14924
|
drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio);
|
|
14831
14925
|
}
|
|
14832
14926
|
}
|
|
@@ -14841,12 +14935,40 @@
|
|
|
14841
14935
|
};
|
|
14842
14936
|
}
|
|
14843
14937
|
function drawItem(item, ctx, horizontalPixelRatio, verticalPixelRatio) {
|
|
14938
|
+
if (item._internal_tag !== undefined) {
|
|
14939
|
+
drawTagItem(item, item._internal_tag, ctx, horizontalPixelRatio, verticalPixelRatio);
|
|
14940
|
+
return;
|
|
14941
|
+
}
|
|
14844
14942
|
ctx.fillStyle = item._internal_color;
|
|
14845
14943
|
if (item._internal_text !== undefined) {
|
|
14846
14944
|
drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
|
|
14847
14945
|
}
|
|
14848
14946
|
drawShape(item, ctx, bitmapShapeItemCoordinates(item, horizontalPixelRatio, verticalPixelRatio));
|
|
14849
14947
|
}
|
|
14948
|
+
// eslint-disable-next-line max-params
|
|
14949
|
+
function drawTagItem(item, tag, ctx, horizontalPixelRatio, verticalPixelRatio) {
|
|
14950
|
+
if (item._internal_size === 0) {
|
|
14951
|
+
return;
|
|
14952
|
+
}
|
|
14953
|
+
drawTag(ctx, {
|
|
14954
|
+
_internal_x: item._internal_x,
|
|
14955
|
+
_internal_centerY: item._internal_y,
|
|
14956
|
+
_internal_width: tag._internal_width,
|
|
14957
|
+
_internal_height: tag._internal_height,
|
|
14958
|
+
_internal_tailHeight: tag._internal_tailHeight,
|
|
14959
|
+
_internal_tailDirection: tag._internal_tailDirection,
|
|
14960
|
+
_internal_radius: tag._internal_radius,
|
|
14961
|
+
_internal_fillColor: item._internal_color,
|
|
14962
|
+
_internal_borderColor: item._internal_borderColor,
|
|
14963
|
+
_internal_borderWidth: item._internal_borderWidth ?? 1,
|
|
14964
|
+
_internal_horizontalPixelRatio: horizontalPixelRatio,
|
|
14965
|
+
_internal_verticalPixelRatio: verticalPixelRatio,
|
|
14966
|
+
});
|
|
14967
|
+
if (item._internal_text !== undefined) {
|
|
14968
|
+
ctx.fillStyle = item._internal_textColor ?? '#FFFFFF';
|
|
14969
|
+
drawText(ctx, item._internal_text._internal_content, item._internal_text._internal_x, item._internal_text._internal_y, horizontalPixelRatio, verticalPixelRatio);
|
|
14970
|
+
}
|
|
14971
|
+
}
|
|
14850
14972
|
function drawShape(item, ctx, coordinates) {
|
|
14851
14973
|
if (item._internal_size === 0) {
|
|
14852
14974
|
return;
|
|
@@ -14864,10 +14986,16 @@
|
|
|
14864
14986
|
case 'square':
|
|
14865
14987
|
drawSquare(ctx, coordinates, item._internal_size);
|
|
14866
14988
|
return;
|
|
14989
|
+
case 'tag':
|
|
14990
|
+
// Handled by drawTagItem, which needs both pixel ratios and the text.
|
|
14991
|
+
return;
|
|
14867
14992
|
}
|
|
14868
14993
|
ensureNever(item._internal_shape);
|
|
14869
14994
|
}
|
|
14870
14995
|
function hitTestItem(item, x, y) {
|
|
14996
|
+
if (item._internal_tag !== undefined) {
|
|
14997
|
+
return item._internal_size !== 0 && hitTestTag(item._internal_x, item._internal_y, item._internal_tag, x, y);
|
|
14998
|
+
}
|
|
14871
14999
|
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)) {
|
|
14872
15000
|
return true;
|
|
14873
15001
|
}
|
|
@@ -14886,6 +15014,8 @@
|
|
|
14886
15014
|
return hitTestCircle(item._internal_x, item._internal_y, item._internal_size, x, y);
|
|
14887
15015
|
case 'square':
|
|
14888
15016
|
return hitTestSquare(item._internal_x, item._internal_y, item._internal_size, x, y);
|
|
15017
|
+
case 'tag':
|
|
15018
|
+
return false;
|
|
14889
15019
|
}
|
|
14890
15020
|
}
|
|
14891
15021
|
|
|
@@ -14918,15 +15048,71 @@
|
|
|
14918
15048
|
}
|
|
14919
15049
|
return;
|
|
14920
15050
|
}
|
|
15051
|
+
/**
|
|
15052
|
+
* Lays out a `tag` marker: the bubble carries the text, so it is measured from
|
|
15053
|
+
* the font rather than from the bar spacing, and its pointer is aimed back at
|
|
15054
|
+
* the anchor price.
|
|
15055
|
+
*/
|
|
15056
|
+
// eslint-disable-next-line max-params
|
|
15057
|
+
function fillTagSizeAndY(rendererItem, marker, anchorY, offsets, textHeight, shapeMargin, sizeMultiplier) {
|
|
15058
|
+
const height = Math.round((textHeight + 5 /* Constants.TagPaddingY */ * 2) * sizeMultiplier);
|
|
15059
|
+
const tailHeight = Math.round(7 /* Constants.TagTailHeight */ * sizeMultiplier);
|
|
15060
|
+
const ignoreOffset = isPriceMarker(marker.position);
|
|
15061
|
+
const halfHeight = height / 2;
|
|
15062
|
+
let tailDirection = 'none';
|
|
15063
|
+
let centerY = anchorY;
|
|
15064
|
+
switch (marker.position) {
|
|
15065
|
+
case 'aboveBar':
|
|
15066
|
+
case 'atPriceTop': {
|
|
15067
|
+
const offset = ignoreOffset ? 0 : offsets._internal_aboveBar;
|
|
15068
|
+
tailDirection = 'down';
|
|
15069
|
+
centerY = (anchorY - offset - tailHeight - halfHeight);
|
|
15070
|
+
if (!ignoreOffset) {
|
|
15071
|
+
offsets._internal_aboveBar += height + tailHeight + shapeMargin;
|
|
15072
|
+
}
|
|
15073
|
+
break;
|
|
15074
|
+
}
|
|
15075
|
+
case 'belowBar':
|
|
15076
|
+
case 'atPriceBottom': {
|
|
15077
|
+
const offset = ignoreOffset ? 0 : offsets._internal_belowBar;
|
|
15078
|
+
tailDirection = 'up';
|
|
15079
|
+
centerY = (anchorY + offset + tailHeight + halfHeight);
|
|
15080
|
+
if (!ignoreOffset) {
|
|
15081
|
+
offsets._internal_belowBar += height + tailHeight + shapeMargin;
|
|
15082
|
+
}
|
|
15083
|
+
break;
|
|
15084
|
+
}
|
|
15085
|
+
}
|
|
15086
|
+
const tag = {
|
|
15087
|
+
_internal_height: height,
|
|
15088
|
+
_internal_tailHeight: tailHeight,
|
|
15089
|
+
_internal_tailDirection: tailDirection,
|
|
15090
|
+
_internal_radius: marker.borderRadius ?? 6 /* Constants.TagRadius */,
|
|
15091
|
+
_internal_paddingX: 9 /* Constants.TagPaddingX */ * sizeMultiplier,
|
|
15092
|
+
_internal_left: 0,
|
|
15093
|
+
_internal_width: 0,
|
|
15094
|
+
};
|
|
15095
|
+
rendererItem._internal_size = height;
|
|
15096
|
+
rendererItem._internal_y = centerY;
|
|
15097
|
+
rendererItem._internal_tag = tag;
|
|
15098
|
+
if (rendererItem._internal_text !== undefined) {
|
|
15099
|
+
rendererItem._internal_text._internal_y = centerY;
|
|
15100
|
+
}
|
|
15101
|
+
}
|
|
14921
15102
|
// eslint-disable-next-line max-params, complexity
|
|
14922
|
-
function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart) {
|
|
14923
|
-
const price = getPrice(seriesData, marker,
|
|
15103
|
+
function fillSizeAndY(rendererItem, marker, seriesData, offsets, textHeight, shapeMargin, series, chart, invertScale) {
|
|
15104
|
+
const price = getPrice(seriesData, marker, invertScale);
|
|
14924
15105
|
if (price === undefined) {
|
|
14925
15106
|
return;
|
|
14926
15107
|
}
|
|
14927
15108
|
const ignoreOffset = isPriceMarker(marker.position);
|
|
14928
15109
|
const timeScale = chart.timeScale();
|
|
14929
15110
|
const sizeMultiplier = isNumber(marker.size) ? Math.max(marker.size, 0) : 1;
|
|
15111
|
+
if (marker.shape === 'tag') {
|
|
15112
|
+
const anchorY = ensureNotNull(series.priceToCoordinate(price));
|
|
15113
|
+
fillTagSizeAndY(rendererItem, marker, anchorY, offsets, textHeight, shapeMargin, sizeMultiplier);
|
|
15114
|
+
return;
|
|
15115
|
+
}
|
|
14930
15116
|
const shapeSize = calculateShapeHeight(timeScale.options().barSpacing) * sizeMultiplier;
|
|
14931
15117
|
const halfSize = shapeSize / 2;
|
|
14932
15118
|
rendererItem._internal_size = shapeSize;
|
|
@@ -15033,6 +15219,10 @@
|
|
|
15033
15219
|
_internal_externalId: marker.id,
|
|
15034
15220
|
_internal_internalId: marker._internal_internalId,
|
|
15035
15221
|
_internal_text: undefined,
|
|
15222
|
+
_internal_textColor: marker.textColor,
|
|
15223
|
+
_internal_borderColor: marker.borderColor,
|
|
15224
|
+
_internal_borderWidth: marker.borderWidth,
|
|
15225
|
+
_internal_tag: undefined,
|
|
15036
15226
|
}));
|
|
15037
15227
|
this._private__dataInvalidated = false;
|
|
15038
15228
|
}
|
|
@@ -15043,13 +15233,22 @@
|
|
|
15043
15233
|
return;
|
|
15044
15234
|
}
|
|
15045
15235
|
const visibleBarsRange = new RangeImpl(Math.floor(visibleBars.from), Math.ceil(visibleBars.to));
|
|
15046
|
-
const firstValue = this._private__series.
|
|
15236
|
+
const firstValue = this._private__series.dataByIndex(0, 1 /* MismatchDirection.NearestRight */);
|
|
15047
15237
|
if (firstValue === null) {
|
|
15048
15238
|
return;
|
|
15049
15239
|
}
|
|
15050
15240
|
if (this._private__data._internal_items.length === 0) {
|
|
15051
15241
|
return;
|
|
15052
15242
|
}
|
|
15243
|
+
// This runs inside the hit-test and paint loops, where a throw costs the
|
|
15244
|
+
// rest of the frame.
|
|
15245
|
+
let invertScale = false;
|
|
15246
|
+
try {
|
|
15247
|
+
invertScale = this._private__series.priceScale().options().invertScale;
|
|
15248
|
+
}
|
|
15249
|
+
catch (error) {
|
|
15250
|
+
invertScale = false;
|
|
15251
|
+
}
|
|
15053
15252
|
let prevTimeIndex = NaN;
|
|
15054
15253
|
const shapeMargin$1 = shapeMargin(timeScale.options().barSpacing);
|
|
15055
15254
|
const offsets = {
|
|
@@ -15080,7 +15279,7 @@
|
|
|
15080
15279
|
if (dataAt === null) {
|
|
15081
15280
|
continue;
|
|
15082
15281
|
}
|
|
15083
|
-
fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart);
|
|
15282
|
+
fillSizeAndY(rendererItem, marker, dataAt, offsets, layoutOptions.fontSize, shapeMargin$1, this._private__series, this._private__chart, invertScale);
|
|
15084
15283
|
}
|
|
15085
15284
|
this._private__invalidated = false;
|
|
15086
15285
|
}
|
|
@@ -15238,6 +15437,10 @@
|
|
|
15238
15437
|
text: marker.text,
|
|
15239
15438
|
size: marker.size,
|
|
15240
15439
|
price: marker.price,
|
|
15440
|
+
textColor: marker.textColor,
|
|
15441
|
+
borderColor: marker.borderColor,
|
|
15442
|
+
borderWidth: marker.borderWidth,
|
|
15443
|
+
borderRadius: marker.borderRadius,
|
|
15241
15444
|
_internal_originalTime: marker.time,
|
|
15242
15445
|
};
|
|
15243
15446
|
if (marker.position === 'atPriceTop' ||
|
|
@@ -19539,10 +19742,13 @@
|
|
|
19539
19742
|
constructor(chart, element) {
|
|
19540
19743
|
this._private__lines = new Map();
|
|
19541
19744
|
this._private__hoveredId = null;
|
|
19745
|
+
this._private__hoveredPart = null;
|
|
19542
19746
|
this._private__dragging = null;
|
|
19543
19747
|
this._private__pointerId = null;
|
|
19544
19748
|
this._private__pressOrigin = null;
|
|
19545
19749
|
this._private__pressedLine = null;
|
|
19750
|
+
/** Distance from the pointer to the dragged line when the press landed. */
|
|
19751
|
+
this._private__grabOffset = 0;
|
|
19546
19752
|
/** Latest pointer position in pane coordinates, or null when off the panes. */
|
|
19547
19753
|
this._private__point = null;
|
|
19548
19754
|
this._private__paneIndex = undefined;
|
|
@@ -19596,8 +19802,12 @@
|
|
|
19596
19802
|
if (this._private__dragging !== null) {
|
|
19597
19803
|
return;
|
|
19598
19804
|
}
|
|
19599
|
-
const
|
|
19805
|
+
const hit = this._private__splitHit(param.hoveredObjectId);
|
|
19806
|
+
const hovered = hit === null ? null : this._private__lines.get(hit._internal_id) ?? null;
|
|
19600
19807
|
this._private__setHovered(hovered ? hovered.id() : null);
|
|
19808
|
+
// The part is updated after the id, and separately from it: moving from a
|
|
19809
|
+
// label onto its close button keeps the same line hovered.
|
|
19810
|
+
this._private__setHoveredPart(hovered, hovered === null ? null : hit?._internal_part ?? null);
|
|
19601
19811
|
this._private__applyCursor();
|
|
19602
19812
|
}
|
|
19603
19813
|
_private__onPointerDown(event) {
|
|
@@ -19616,6 +19826,7 @@
|
|
|
19616
19826
|
this._private__pointerId = event.pointerId;
|
|
19617
19827
|
this._private__element.setPointerCapture(event.pointerId);
|
|
19618
19828
|
line.beginDrag();
|
|
19829
|
+
this._private__grabOffset = this._private__measureGrabOffset(line, event);
|
|
19619
19830
|
this._private__applyCursor();
|
|
19620
19831
|
event.preventDefault();
|
|
19621
19832
|
event.stopPropagation();
|
|
@@ -19650,6 +19861,7 @@
|
|
|
19650
19861
|
Math.abs(event.clientY - origin._internal_y) > CLICK_TOLERANCE_PX);
|
|
19651
19862
|
if (dragged !== null) {
|
|
19652
19863
|
this._private__dragging = null;
|
|
19864
|
+
this._private__grabOffset = 0;
|
|
19653
19865
|
if (moved) {
|
|
19654
19866
|
dragged.commitDrag();
|
|
19655
19867
|
}
|
|
@@ -19677,6 +19889,7 @@
|
|
|
19677
19889
|
}
|
|
19678
19890
|
const line = this._private__dragging;
|
|
19679
19891
|
this._private__dragging = null;
|
|
19892
|
+
this._private__grabOffset = 0;
|
|
19680
19893
|
this._private__releasePointer();
|
|
19681
19894
|
line.cancelDrag();
|
|
19682
19895
|
this._private__applyCursor();
|
|
@@ -19701,24 +19914,65 @@
|
|
|
19701
19914
|
if (line === null) {
|
|
19702
19915
|
return null;
|
|
19703
19916
|
}
|
|
19704
|
-
//
|
|
19705
|
-
//
|
|
19706
|
-
//
|
|
19707
|
-
|
|
19708
|
-
|
|
19709
|
-
|
|
19917
|
+
// The pointer event is the gesture's own position, so it is the source of
|
|
19918
|
+
// truth. The crosshair is only a mirror of it: it stops updating as soon
|
|
19919
|
+
// as the pointer leaves the pane canvas — over the price axis, past the
|
|
19920
|
+
// chart's edge — and a drag driven from it freezes there while the user
|
|
19921
|
+
// is still moving.
|
|
19922
|
+
const paneY = this._private__paneY(line, event);
|
|
19923
|
+
return paneY === null ? null : line.priceAt(paneY - this._private__grabOffset);
|
|
19924
|
+
}
|
|
19925
|
+
/** The pointer's position inside the line's pane, in pane coordinates. */
|
|
19926
|
+
_private__paneY(line, event) {
|
|
19710
19927
|
const pane = this._private__chart.panes()[line.paneIndex()];
|
|
19711
19928
|
const paneElement = pane ? pane.getHTMLElement() : null;
|
|
19712
|
-
if (
|
|
19713
|
-
return
|
|
19929
|
+
if (paneElement) {
|
|
19930
|
+
return event.clientY - paneElement.getBoundingClientRect().top;
|
|
19931
|
+
}
|
|
19932
|
+
if (this._private__point !== null && this._private__paneIndex === line.paneIndex()) {
|
|
19933
|
+
return this._private__point._internal_y;
|
|
19934
|
+
}
|
|
19935
|
+
return null;
|
|
19936
|
+
}
|
|
19937
|
+
/**
|
|
19938
|
+
* Keeps the grab point: a line picked up by the edge of its label moves with
|
|
19939
|
+
* the pointer rather than centring itself on it. Ignored past a sane
|
|
19940
|
+
* distance, so a bad reading cannot send the line off on the first move.
|
|
19941
|
+
*/
|
|
19942
|
+
_private__measureGrabOffset(line, event) {
|
|
19943
|
+
if (!line.dragAnchorY) {
|
|
19944
|
+
return 0;
|
|
19945
|
+
}
|
|
19946
|
+
const anchor = line.dragAnchorY();
|
|
19947
|
+
const paneY = this._private__paneY(line, event);
|
|
19948
|
+
if (anchor === null || paneY === null) {
|
|
19949
|
+
return 0;
|
|
19714
19950
|
}
|
|
19715
|
-
|
|
19951
|
+
const offset = paneY - anchor;
|
|
19952
|
+
return Math.abs(offset) > 40 /* Constants.MaxGrabOffsetPx */ ? 0 : offset;
|
|
19716
19953
|
}
|
|
19717
|
-
|
|
19718
|
-
|
|
19954
|
+
/**
|
|
19955
|
+
* Hit ids are either a bare line id or `id#part`, which is how a line with
|
|
19956
|
+
* several targets reports which one was hit.
|
|
19957
|
+
*/
|
|
19958
|
+
_private__splitHit(externalId) {
|
|
19959
|
+
if (typeof externalId !== 'string' || externalId.length === 0) {
|
|
19719
19960
|
return null;
|
|
19720
19961
|
}
|
|
19721
|
-
|
|
19962
|
+
const separator = externalId.indexOf('#');
|
|
19963
|
+
if (separator === -1) {
|
|
19964
|
+
return { _internal_id: externalId, _internal_part: null };
|
|
19965
|
+
}
|
|
19966
|
+
return { _internal_id: externalId.slice(0, separator), _internal_part: externalId.slice(separator + 1) };
|
|
19967
|
+
}
|
|
19968
|
+
_private__setHoveredPart(line, part) {
|
|
19969
|
+
if (this._private__hoveredPart === part) {
|
|
19970
|
+
return;
|
|
19971
|
+
}
|
|
19972
|
+
this._private__hoveredPart = part;
|
|
19973
|
+
if (line && line.setHoveredPart) {
|
|
19974
|
+
line.setHoveredPart(part);
|
|
19975
|
+
}
|
|
19722
19976
|
}
|
|
19723
19977
|
_private__setHovered(id) {
|
|
19724
19978
|
if (this._private__hoveredId === id) {
|
|
@@ -19728,6 +19982,7 @@
|
|
|
19728
19982
|
this._private__lines.get(this._private__hoveredId)?.setHovered(false);
|
|
19729
19983
|
}
|
|
19730
19984
|
this._private__hoveredId = id;
|
|
19985
|
+
this._private__hoveredPart = null;
|
|
19731
19986
|
if (id !== null) {
|
|
19732
19987
|
this._private__lines.get(id)?.setHovered(true);
|
|
19733
19988
|
}
|
|
@@ -20251,6 +20506,862 @@
|
|
|
20251
20506
|
return new InteractiveLineManager(chart, series);
|
|
20252
20507
|
}
|
|
20253
20508
|
|
|
20509
|
+
/**
|
|
20510
|
+
* Fans out labels that land on top of each other.
|
|
20511
|
+
*
|
|
20512
|
+
* Every position line on a chart shares one of these. Two positions opened at
|
|
20513
|
+
* the same price draw their labels at the same coordinate, and the one behind
|
|
20514
|
+
* becomes unreachable — the engine resolves the hit to whichever primitive it
|
|
20515
|
+
* reaches first, and that is always the same one. Nudging each collision down
|
|
20516
|
+
* leaves every label a strip of its own to be grabbed by.
|
|
20517
|
+
*
|
|
20518
|
+
* The line itself never moves: it stays at the price it belongs to, and only
|
|
20519
|
+
* the label is offset.
|
|
20520
|
+
*/
|
|
20521
|
+
class PositionLineLabelLayout {
|
|
20522
|
+
constructor() {
|
|
20523
|
+
/** Registration order. It is what keeps the fan from reshuffling per frame. */
|
|
20524
|
+
this._private__order = [];
|
|
20525
|
+
this._private__claims = new Map();
|
|
20526
|
+
this._private__offsets = new Map();
|
|
20527
|
+
this._private__invalidated = true;
|
|
20528
|
+
}
|
|
20529
|
+
_internal_register(ownerId) {
|
|
20530
|
+
if (!this._private__claims.has(ownerId)) {
|
|
20531
|
+
this._private__order.push(ownerId);
|
|
20532
|
+
this._private__claims.set(ownerId, []);
|
|
20533
|
+
this._private__invalidated = true;
|
|
20534
|
+
}
|
|
20535
|
+
}
|
|
20536
|
+
_internal_unregister(ownerId) {
|
|
20537
|
+
const index = this._private__order.indexOf(ownerId);
|
|
20538
|
+
if (index !== -1) {
|
|
20539
|
+
this._private__order.splice(index, 1);
|
|
20540
|
+
}
|
|
20541
|
+
this._private__claims.delete(ownerId);
|
|
20542
|
+
this._private__invalidated = true;
|
|
20543
|
+
}
|
|
20544
|
+
_internal_isEmpty() {
|
|
20545
|
+
return this._private__order.length === 0;
|
|
20546
|
+
}
|
|
20547
|
+
/**
|
|
20548
|
+
* Publishes where an owner's labels want to sit. Called while each line
|
|
20549
|
+
* rebuilds its renderer data, so a fan can be one frame behind while prices
|
|
20550
|
+
* move — which is invisible, and cheaper than forcing a second pass.
|
|
20551
|
+
*/
|
|
20552
|
+
_internal_setClaims(ownerId, claims) {
|
|
20553
|
+
this._private__claims.set(ownerId, claims);
|
|
20554
|
+
this._private__invalidated = true;
|
|
20555
|
+
}
|
|
20556
|
+
/** How far down a label has to move to clear the ones already placed. */
|
|
20557
|
+
_internal_offsetFor(ownerId, level) {
|
|
20558
|
+
if (this._private__invalidated) {
|
|
20559
|
+
this._private__recalculate();
|
|
20560
|
+
}
|
|
20561
|
+
return this._private__offsets.get(key(ownerId, level)) ?? 0;
|
|
20562
|
+
}
|
|
20563
|
+
_private__recalculate() {
|
|
20564
|
+
const offsets = new Map();
|
|
20565
|
+
const placed = [];
|
|
20566
|
+
for (const ownerId of this._private__order) {
|
|
20567
|
+
for (const claim of this._private__claims.get(ownerId) ?? []) {
|
|
20568
|
+
const gap = minGap(claim._internal_height);
|
|
20569
|
+
const maxOffset = claim._internal_height;
|
|
20570
|
+
// Moved down to the first coordinate that clears everything already
|
|
20571
|
+
// placed, and no further. Two levels whose own prices nearly part
|
|
20572
|
+
// them only need the few pixels they are short of — stepping by a
|
|
20573
|
+
// full gap each time is what made the fan look flung open.
|
|
20574
|
+
let y = claim._internal_y;
|
|
20575
|
+
for (const other of [...placed].sort(byY)) {
|
|
20576
|
+
if (Math.abs(y - other._internal_y) < gap) {
|
|
20577
|
+
y = other._internal_y + gap;
|
|
20578
|
+
}
|
|
20579
|
+
}
|
|
20580
|
+
let offset = y - claim._internal_y;
|
|
20581
|
+
if (offset > maxOffset) {
|
|
20582
|
+
// More labels than there is room to fan. The rest stack, which is
|
|
20583
|
+
// no worse than before and keeps the pile from running off.
|
|
20584
|
+
offset = 0;
|
|
20585
|
+
}
|
|
20586
|
+
placed.push({ _internal_y: claim._internal_y + offset, _internal_height: claim._internal_height });
|
|
20587
|
+
offsets.set(key(ownerId, claim._internal_level), offset);
|
|
20588
|
+
}
|
|
20589
|
+
}
|
|
20590
|
+
this._private__offsets = offsets;
|
|
20591
|
+
this._private__invalidated = false;
|
|
20592
|
+
}
|
|
20593
|
+
}
|
|
20594
|
+
function key(ownerId, level) {
|
|
20595
|
+
return `${ownerId}:${level}`;
|
|
20596
|
+
}
|
|
20597
|
+
/**
|
|
20598
|
+
* The strip a label keeps to itself. Comfortably clickable, and no wider —
|
|
20599
|
+
* every pixel here is a pixel the label sits away from its own line.
|
|
20600
|
+
*/
|
|
20601
|
+
function minGap(height) {
|
|
20602
|
+
return Math.max(9, Math.round(height * 0.4));
|
|
20603
|
+
}
|
|
20604
|
+
function byY(a, b) {
|
|
20605
|
+
return a._internal_y - b._internal_y;
|
|
20606
|
+
}
|
|
20607
|
+
const layouts = new WeakMap();
|
|
20608
|
+
/** The layout is shared by every position line on the same chart. */
|
|
20609
|
+
function acquireLabelLayout(chart, ownerId) {
|
|
20610
|
+
let layout = layouts.get(chart);
|
|
20611
|
+
if (!layout) {
|
|
20612
|
+
layout = new PositionLineLabelLayout();
|
|
20613
|
+
layouts.set(chart, layout);
|
|
20614
|
+
}
|
|
20615
|
+
layout._internal_register(ownerId);
|
|
20616
|
+
return layout;
|
|
20617
|
+
}
|
|
20618
|
+
function releaseLabelLayout(chart, ownerId) {
|
|
20619
|
+
const layout = layouts.get(chart);
|
|
20620
|
+
if (!layout) {
|
|
20621
|
+
return;
|
|
20622
|
+
}
|
|
20623
|
+
layout._internal_unregister(ownerId);
|
|
20624
|
+
if (layout._internal_isEmpty()) {
|
|
20625
|
+
layouts.delete(chart);
|
|
20626
|
+
}
|
|
20627
|
+
}
|
|
20628
|
+
|
|
20629
|
+
const positionLineDefaultColors = {
|
|
20630
|
+
entry: '#2962FF',
|
|
20631
|
+
takeProfit: '#26A17B',
|
|
20632
|
+
stopLoss: '#D9A23B',
|
|
20633
|
+
};
|
|
20634
|
+
const positionLineThemeDefaults = {
|
|
20635
|
+
badgeTextColor: '#FFFFFF',
|
|
20636
|
+
borderRadius: 4,
|
|
20637
|
+
borderWidth: 1,
|
|
20638
|
+
paddingX: 8,
|
|
20639
|
+
paddingY: 6,
|
|
20640
|
+
backgroundOpacity: 1,
|
|
20641
|
+
};
|
|
20642
|
+
const positionLineLevels = ['takeProfit', 'entry', 'stopLoss'];
|
|
20643
|
+
|
|
20644
|
+
class PositionLineRenderer {
|
|
20645
|
+
constructor() {
|
|
20646
|
+
this._private__data = null;
|
|
20647
|
+
this._private__layouts = [];
|
|
20648
|
+
this._private__connectorX = 0;
|
|
20649
|
+
this._private__font = '';
|
|
20650
|
+
this._private__colorParser = new ColorParser([]);
|
|
20651
|
+
this._private__textWidthCache = new TextWidthCache(50);
|
|
20652
|
+
}
|
|
20653
|
+
_internal_setData(data) {
|
|
20654
|
+
this._private__data = data;
|
|
20655
|
+
}
|
|
20656
|
+
_internal_hitTest(x, y) {
|
|
20657
|
+
// The close button sits on top of the label, which sits on top of the
|
|
20658
|
+
// line, so the narrowest target is tested first.
|
|
20659
|
+
for (const layout of this._private__layouts) {
|
|
20660
|
+
if (layout._internal_closable && x >= layout._internal_closeLeft && x <= layout._internal_closeRight &&
|
|
20661
|
+
Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
|
|
20662
|
+
return { _internal_level: layout._internal_level, _internal_part: 'close' };
|
|
20663
|
+
}
|
|
20664
|
+
}
|
|
20665
|
+
for (const layout of this._private__layouts) {
|
|
20666
|
+
if (x >= layout._internal_pillLeft && x <= layout._internal_pillRight && Math.abs(y - layout._internal_labelY) <= layout._internal_height / 2) {
|
|
20667
|
+
return { _internal_level: layout._internal_level, _internal_part: 'body' };
|
|
20668
|
+
}
|
|
20669
|
+
}
|
|
20670
|
+
for (const layout of this._private__layouts) {
|
|
20671
|
+
if (Math.abs(y - layout._internal_y) <= 8 /* Constants.LineHitTolerance */) {
|
|
20672
|
+
return { _internal_level: layout._internal_level, _internal_part: 'line' };
|
|
20673
|
+
}
|
|
20674
|
+
}
|
|
20675
|
+
return null;
|
|
20676
|
+
}
|
|
20677
|
+
draw(target) {
|
|
20678
|
+
target.useBitmapCoordinateSpace((scope) => this._private__drawImpl(scope));
|
|
20679
|
+
}
|
|
20680
|
+
_private__drawImpl(scope) {
|
|
20681
|
+
const data = this._private__data;
|
|
20682
|
+
this._private__layouts = [];
|
|
20683
|
+
if (data === null || data._internal_levels.length === 0) {
|
|
20684
|
+
return;
|
|
20685
|
+
}
|
|
20686
|
+
const { context: ctx, mediaSize } = scope;
|
|
20687
|
+
ctx.save();
|
|
20688
|
+
if (this._private__font !== data._internal_font) {
|
|
20689
|
+
this._private__font = data._internal_font;
|
|
20690
|
+
this._private__textWidthCache._internal_reset();
|
|
20691
|
+
}
|
|
20692
|
+
ctx.font = data._internal_font;
|
|
20693
|
+
ctx.textBaseline = 'middle';
|
|
20694
|
+
this._private__connectorX = Math.max(0, mediaSize.width - data._internal_rightOffset);
|
|
20695
|
+
const lineEnd = data._internal_connectorVisible ? this._private__connectorX : mediaSize.width;
|
|
20696
|
+
for (const level of data._internal_levels) {
|
|
20697
|
+
this._private__layouts.push(this._private__drawLevel(scope, data, level, lineEnd));
|
|
20698
|
+
}
|
|
20699
|
+
if (data._internal_connectorVisible && data._internal_levels.length > 1) {
|
|
20700
|
+
this._private__drawConnector(scope, data);
|
|
20701
|
+
}
|
|
20702
|
+
ctx.restore();
|
|
20703
|
+
}
|
|
20704
|
+
// eslint-disable-next-line max-params
|
|
20705
|
+
_private__drawLevel(scope, data, level, lineEnd) {
|
|
20706
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20707
|
+
const height = data._internal_fontSize + data._internal_paddingY * 2;
|
|
20708
|
+
const badgeWidth = level._internal_badge.length === 0
|
|
20709
|
+
? 0
|
|
20710
|
+
: Math.max(this._private__textWidthCache._internal_measureText(ctx, level._internal_badge) + data._internal_paddingX * 2, height * 0.85 /* Constants.BadgeMinWidthRatio */);
|
|
20711
|
+
const labelWidth = this._private__textWidthCache._internal_measureText(ctx, level._internal_text) + data._internal_paddingX * 2;
|
|
20712
|
+
const closeWidth = level._internal_closable ? height : 0;
|
|
20713
|
+
const pillWidth = badgeWidth + labelWidth + closeWidth;
|
|
20714
|
+
const pillRight = Math.max(pillWidth, lineEnd - 8 /* Constants.LabelGap */);
|
|
20715
|
+
const pillLeft = pillRight - pillWidth;
|
|
20716
|
+
const labelY = level._internal_y + level._internal_labelOffsetY;
|
|
20717
|
+
const top = labelY - height / 2;
|
|
20718
|
+
this._private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd);
|
|
20719
|
+
this._private__drawPill(scope, data, level, { _internal_left: pillLeft, _internal_top: top, _internal_width: pillWidth, _internal_height: height });
|
|
20720
|
+
let cellLeft = pillLeft;
|
|
20721
|
+
if (badgeWidth > 0) {
|
|
20722
|
+
this._private__drawBadge(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: badgeWidth, _internal_height: height });
|
|
20723
|
+
cellLeft += badgeWidth;
|
|
20724
|
+
this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
|
|
20725
|
+
}
|
|
20726
|
+
ctx.save();
|
|
20727
|
+
ctx.scale(hpr, vpr);
|
|
20728
|
+
ctx.textAlign = 'center';
|
|
20729
|
+
ctx.fillStyle = data._internal_textColor ?? level._internal_color;
|
|
20730
|
+
ctx.fillText(level._internal_text, cellLeft + labelWidth / 2, labelY);
|
|
20731
|
+
ctx.restore();
|
|
20732
|
+
cellLeft += labelWidth;
|
|
20733
|
+
if (level._internal_closable) {
|
|
20734
|
+
this._private__drawSeparator(ctx, level._internal_color, cellLeft, top, height, hpr, vpr);
|
|
20735
|
+
this._private__drawCloseButton(scope, data, level, { _internal_left: cellLeft, _internal_top: top, _internal_width: closeWidth, _internal_height: height });
|
|
20736
|
+
}
|
|
20737
|
+
return {
|
|
20738
|
+
_internal_level: level._internal_level,
|
|
20739
|
+
_internal_y: level._internal_y,
|
|
20740
|
+
_internal_labelY: labelY,
|
|
20741
|
+
_internal_height: height,
|
|
20742
|
+
_internal_pillLeft: pillLeft,
|
|
20743
|
+
_internal_pillRight: pillRight,
|
|
20744
|
+
_internal_closeLeft: cellLeft,
|
|
20745
|
+
_internal_closeRight: cellLeft + closeWidth,
|
|
20746
|
+
_internal_closable: level._internal_closable,
|
|
20747
|
+
};
|
|
20748
|
+
}
|
|
20749
|
+
// eslint-disable-next-line max-params
|
|
20750
|
+
_private__drawLineSegments(scope, level, pillLeft, pillRight, lineEnd) {
|
|
20751
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20752
|
+
const lineWidth = Math.max(1, Math.round(level._internal_lineWidth * vpr));
|
|
20753
|
+
const y = Math.round(level._internal_y * vpr) - Math.floor(lineWidth / 2);
|
|
20754
|
+
ctx.save();
|
|
20755
|
+
ctx.strokeStyle = level._internal_color;
|
|
20756
|
+
ctx.lineWidth = lineWidth;
|
|
20757
|
+
setLineStyle(ctx, level._internal_lineStyle);
|
|
20758
|
+
ctx.beginPath();
|
|
20759
|
+
if (level._internal_labelOffsetY !== 0) {
|
|
20760
|
+
// The label has been fanned off its line, so breaking the line for it
|
|
20761
|
+
// would leave a gap with nothing in it.
|
|
20762
|
+
ctx.moveTo(0, y);
|
|
20763
|
+
ctx.lineTo(Math.round(lineEnd * hpr), y);
|
|
20764
|
+
}
|
|
20765
|
+
else {
|
|
20766
|
+
ctx.moveTo(0, y);
|
|
20767
|
+
ctx.lineTo(Math.round(Math.max(0, pillLeft - 8 /* Constants.LabelGap */) * hpr), y);
|
|
20768
|
+
if (lineEnd > pillRight + 8 /* Constants.LabelGap */) {
|
|
20769
|
+
ctx.moveTo(Math.round((pillRight + 8 /* Constants.LabelGap */) * hpr), y);
|
|
20770
|
+
ctx.lineTo(Math.round(lineEnd * hpr), y);
|
|
20771
|
+
}
|
|
20772
|
+
}
|
|
20773
|
+
ctx.stroke();
|
|
20774
|
+
ctx.restore();
|
|
20775
|
+
}
|
|
20776
|
+
// eslint-disable-next-line max-params
|
|
20777
|
+
_private__drawPill(scope, data, level, box) {
|
|
20778
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20779
|
+
const hovered = data._internal_hoveredLevel === level._internal_level;
|
|
20780
|
+
const borderWidth = Math.max(1, Math.round((hovered ? data._internal_borderWidth + 1 : data._internal_borderWidth) * hpr));
|
|
20781
|
+
ctx.save();
|
|
20782
|
+
ctx.setLineDash([]);
|
|
20783
|
+
bitmapRoundRect(ctx, box, data._internal_borderRadius, hpr, vpr);
|
|
20784
|
+
ctx.fillStyle = this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
|
|
20785
|
+
ctx.fill();
|
|
20786
|
+
ctx.lineWidth = borderWidth;
|
|
20787
|
+
ctx.strokeStyle = level._internal_color;
|
|
20788
|
+
ctx.stroke();
|
|
20789
|
+
ctx.restore();
|
|
20790
|
+
}
|
|
20791
|
+
// eslint-disable-next-line max-params
|
|
20792
|
+
_private__drawBadge(scope, data, level, box) {
|
|
20793
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20794
|
+
if (level._internal_badgeFilled) {
|
|
20795
|
+
// The badge shares the label's left corners, so its fill has to stop
|
|
20796
|
+
// short of the border instead of squaring it off.
|
|
20797
|
+
const inset = data._internal_borderWidth;
|
|
20798
|
+
ctx.save();
|
|
20799
|
+
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');
|
|
20800
|
+
ctx.fillStyle = level._internal_color;
|
|
20801
|
+
ctx.fill();
|
|
20802
|
+
ctx.restore();
|
|
20803
|
+
}
|
|
20804
|
+
ctx.save();
|
|
20805
|
+
ctx.scale(hpr, vpr);
|
|
20806
|
+
ctx.textAlign = 'center';
|
|
20807
|
+
ctx.fillStyle = level._internal_badgeFilled ? data._internal_badgeTextColor : level._internal_color;
|
|
20808
|
+
ctx.fillText(level._internal_badge, box._internal_left + box._internal_width / 2, box._internal_top + box._internal_height / 2);
|
|
20809
|
+
ctx.restore();
|
|
20810
|
+
}
|
|
20811
|
+
// eslint-disable-next-line max-params
|
|
20812
|
+
_private__drawSeparator(ctx, color, x, top, height, hpr, vpr) {
|
|
20813
|
+
const width = Math.max(1, Math.floor(hpr));
|
|
20814
|
+
ctx.save();
|
|
20815
|
+
ctx.setLineDash([]);
|
|
20816
|
+
ctx.fillStyle = this._private__colorParser._internal_applyAlpha(color, 0.4 /* Constants.SeparatorAlpha */);
|
|
20817
|
+
ctx.fillRect(Math.round(x * hpr), Math.round(top * vpr), width, Math.round(height * vpr));
|
|
20818
|
+
ctx.restore();
|
|
20819
|
+
}
|
|
20820
|
+
// eslint-disable-next-line max-params
|
|
20821
|
+
_private__drawCloseButton(scope, data, level, box) {
|
|
20822
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20823
|
+
const hovered = data._internal_hoveredLevel === level._internal_level && data._internal_hoveredPart === 'close';
|
|
20824
|
+
if (hovered) {
|
|
20825
|
+
ctx.save();
|
|
20826
|
+
const inset = data._internal_borderWidth;
|
|
20827
|
+
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');
|
|
20828
|
+
ctx.fillStyle = this._private__colorParser._internal_applyAlpha(level._internal_color, 0.25);
|
|
20829
|
+
ctx.fill();
|
|
20830
|
+
ctx.restore();
|
|
20831
|
+
}
|
|
20832
|
+
const centerX = (box._internal_left + box._internal_width / 2) * hpr;
|
|
20833
|
+
const centerY = (box._internal_top + box._internal_height / 2) * vpr;
|
|
20834
|
+
const arm = (7 /* Constants.CloseGlyphSize */ / 2) * hpr;
|
|
20835
|
+
ctx.save();
|
|
20836
|
+
ctx.setLineDash([]);
|
|
20837
|
+
ctx.lineCap = 'round';
|
|
20838
|
+
ctx.lineWidth = Math.max(1, Math.round(1.4 * hpr));
|
|
20839
|
+
ctx.strokeStyle = level._internal_color;
|
|
20840
|
+
ctx.beginPath();
|
|
20841
|
+
ctx.moveTo(centerX - arm, centerY - arm);
|
|
20842
|
+
ctx.lineTo(centerX + arm, centerY + arm);
|
|
20843
|
+
ctx.moveTo(centerX + arm, centerY - arm);
|
|
20844
|
+
ctx.lineTo(centerX - arm, centerY + arm);
|
|
20845
|
+
ctx.stroke();
|
|
20846
|
+
ctx.restore();
|
|
20847
|
+
}
|
|
20848
|
+
_private__drawConnector(scope, data) {
|
|
20849
|
+
const { context: ctx, horizontalPixelRatio: hpr, verticalPixelRatio: vpr } = scope;
|
|
20850
|
+
const entry = data._internal_levels.find((level) => level._internal_level === 'entry') ?? data._internal_levels[0];
|
|
20851
|
+
const ys = data._internal_levels.map((level) => level._internal_y);
|
|
20852
|
+
const x = Math.round(this._private__connectorX * hpr);
|
|
20853
|
+
ctx.save();
|
|
20854
|
+
ctx.setLineDash([]);
|
|
20855
|
+
ctx.strokeStyle = entry._internal_color;
|
|
20856
|
+
ctx.lineWidth = Math.max(1, Math.floor(hpr));
|
|
20857
|
+
ctx.beginPath();
|
|
20858
|
+
ctx.moveTo(x, Math.round(Math.min(...ys) * vpr));
|
|
20859
|
+
ctx.lineTo(x, Math.round(Math.max(...ys) * vpr));
|
|
20860
|
+
ctx.stroke();
|
|
20861
|
+
for (const level of data._internal_levels) {
|
|
20862
|
+
ctx.beginPath();
|
|
20863
|
+
ctx.arc(x, Math.round(level._internal_y * vpr), 4.5 /* Constants.HandleRadius */ * hpr, 0, 2 * Math.PI);
|
|
20864
|
+
ctx.fillStyle = level._internal_level === 'entry'
|
|
20865
|
+
? entry._internal_color
|
|
20866
|
+
: this._private__colorParser._internal_applyAlpha(data._internal_background, data._internal_backgroundOpacity);
|
|
20867
|
+
ctx.fill();
|
|
20868
|
+
ctx.lineWidth = Math.max(1, Math.round(1.5 * hpr));
|
|
20869
|
+
ctx.strokeStyle = level._internal_color;
|
|
20870
|
+
ctx.stroke();
|
|
20871
|
+
}
|
|
20872
|
+
ctx.restore();
|
|
20873
|
+
}
|
|
20874
|
+
}
|
|
20875
|
+
// eslint-disable-next-line max-params
|
|
20876
|
+
function bitmapRoundRect(ctx, box, radius, hpr, vpr, side = 'all') {
|
|
20877
|
+
const left = Math.round(box._internal_left * hpr);
|
|
20878
|
+
const top = Math.round(box._internal_top * vpr);
|
|
20879
|
+
const width = Math.round(box._internal_width * hpr);
|
|
20880
|
+
const height = Math.round(box._internal_height * vpr);
|
|
20881
|
+
const r = Math.min(radius * hpr, width / 2, height / 2);
|
|
20882
|
+
const radii = side === 'left'
|
|
20883
|
+
? [r, 0, 0, r]
|
|
20884
|
+
: side === 'right'
|
|
20885
|
+
? [0, r, r, 0]
|
|
20886
|
+
: [r, r, r, r];
|
|
20887
|
+
drawRoundRect(ctx, left, top, width, height, radii);
|
|
20888
|
+
}
|
|
20889
|
+
|
|
20890
|
+
class PositionLinePaneView {
|
|
20891
|
+
constructor() {
|
|
20892
|
+
this._private__renderer = new PositionLineRenderer();
|
|
20893
|
+
this._private__data = null;
|
|
20894
|
+
}
|
|
20895
|
+
_internal_update(data) {
|
|
20896
|
+
this._private__data = data;
|
|
20897
|
+
}
|
|
20898
|
+
renderer() {
|
|
20899
|
+
if (this._private__data === null) {
|
|
20900
|
+
return null;
|
|
20901
|
+
}
|
|
20902
|
+
this._private__renderer._internal_setData(this._private__data);
|
|
20903
|
+
return this._private__renderer;
|
|
20904
|
+
}
|
|
20905
|
+
zOrder() {
|
|
20906
|
+
return 'top';
|
|
20907
|
+
}
|
|
20908
|
+
_internal_hitTest(x, y) {
|
|
20909
|
+
return this._private__data === null ? null : this._private__renderer._internal_hitTest(x, y);
|
|
20910
|
+
}
|
|
20911
|
+
}
|
|
20912
|
+
|
|
20913
|
+
class PositionLineAxisView {
|
|
20914
|
+
constructor(source) {
|
|
20915
|
+
this._private__source = source;
|
|
20916
|
+
}
|
|
20917
|
+
coordinate() {
|
|
20918
|
+
// A hidden label is pushed far off the axis so it cannot reserve a gap
|
|
20919
|
+
// between the labels that are shown.
|
|
20920
|
+
return this._private__label()?._internal_coordinate ?? -1e3;
|
|
20921
|
+
}
|
|
20922
|
+
text() {
|
|
20923
|
+
return this._private__label()?._internal_text ?? '';
|
|
20924
|
+
}
|
|
20925
|
+
textColor() {
|
|
20926
|
+
return this._private__label()?._internal_textColor ?? '#FFFFFF';
|
|
20927
|
+
}
|
|
20928
|
+
backColor() {
|
|
20929
|
+
return this._private__label()?._internal_color ?? '#000000';
|
|
20930
|
+
}
|
|
20931
|
+
visible() {
|
|
20932
|
+
return this._private__label() !== null;
|
|
20933
|
+
}
|
|
20934
|
+
_private__label() {
|
|
20935
|
+
return this._private__source();
|
|
20936
|
+
}
|
|
20937
|
+
}
|
|
20938
|
+
|
|
20939
|
+
let nextPositionId = 0;
|
|
20940
|
+
const DEFAULT_RIGHT_OFFSET = 56;
|
|
20941
|
+
/**
|
|
20942
|
+
* A group of trading levels — entry, take profit and stop loss — drawn as one
|
|
20943
|
+
* unit: a line per level, a label carrying size and P&L, and a connector that
|
|
20944
|
+
* ties the three together.
|
|
20945
|
+
*
|
|
20946
|
+
* Input is not handled here. Every trading line on a chart shares a single
|
|
20947
|
+
* TradingLineController, which is what guarantees that one gesture moves one
|
|
20948
|
+
* level, and that a position line and a plain draggable price line on the same
|
|
20949
|
+
* chart do not fight over the pointer.
|
|
20950
|
+
*/
|
|
20951
|
+
class PositionLine {
|
|
20952
|
+
constructor(chart, series, options) {
|
|
20953
|
+
this._private__paneView = new PositionLinePaneView();
|
|
20954
|
+
this._private__colorParser = new ColorParser([]);
|
|
20955
|
+
this._private__attached = false;
|
|
20956
|
+
this._private__hoveredLevel = null;
|
|
20957
|
+
this._private__hoveredPart = null;
|
|
20958
|
+
this._private__dragLevel = null;
|
|
20959
|
+
this._private__priceBeforeDrag = null;
|
|
20960
|
+
this._private__chart = chart;
|
|
20961
|
+
this._private__series = series;
|
|
20962
|
+
this._private__id = `pipsend-position-line-${nextPositionId++}`;
|
|
20963
|
+
this._private__options = options;
|
|
20964
|
+
this._private__theme = { ...positionLineThemeDefaults, ...options.theme };
|
|
20965
|
+
this._private__paneViews = [this._private__paneView];
|
|
20966
|
+
this._private__axisViews = positionLineLevels.map((level) => new PositionLineAxisView(() => this._private__axisLabel(level)));
|
|
20967
|
+
this._private__labelLayout = acquireLabelLayout(this._private__chart, this._private__id);
|
|
20968
|
+
this._private__series.attachPrimitive(this);
|
|
20969
|
+
acquireController(this._private__chart)?._internal_register(this);
|
|
20970
|
+
}
|
|
20971
|
+
// ---- ISeriesPrimitive ----
|
|
20972
|
+
attached(param) {
|
|
20973
|
+
this._private__requestUpdate = param.requestUpdate;
|
|
20974
|
+
this._private__attached = true;
|
|
20975
|
+
this.requestUpdate();
|
|
20976
|
+
}
|
|
20977
|
+
detached() {
|
|
20978
|
+
this._private__attached = false;
|
|
20979
|
+
this._private__requestUpdate = undefined;
|
|
20980
|
+
}
|
|
20981
|
+
paneViews() {
|
|
20982
|
+
return this._private__paneViews;
|
|
20983
|
+
}
|
|
20984
|
+
priceAxisViews() {
|
|
20985
|
+
return this._private__axisViews;
|
|
20986
|
+
}
|
|
20987
|
+
updateAllViews() {
|
|
20988
|
+
this._private__paneView._internal_update(this._private__buildRendererData());
|
|
20989
|
+
}
|
|
20990
|
+
hitTest(x, y) {
|
|
20991
|
+
const hit = this._private__paneView._internal_hitTest(x, y);
|
|
20992
|
+
if (hit === null) {
|
|
20993
|
+
return null;
|
|
20994
|
+
}
|
|
20995
|
+
const level = this._private__levelOptions(hit._internal_level);
|
|
20996
|
+
if (level === null) {
|
|
20997
|
+
return null;
|
|
20998
|
+
}
|
|
20999
|
+
return {
|
|
21000
|
+
externalId: `${this._private__id}#${hit._internal_level}:${hit._internal_part}`,
|
|
21001
|
+
zOrder: 'top',
|
|
21002
|
+
cursorStyle: hit._internal_part === 'close'
|
|
21003
|
+
? 'pointer'
|
|
21004
|
+
: (level.draggable ?? true) ? 'ns-resize' : undefined,
|
|
21005
|
+
};
|
|
21006
|
+
}
|
|
21007
|
+
// ---- ControllableLine ----
|
|
21008
|
+
id() {
|
|
21009
|
+
return this._private__id;
|
|
21010
|
+
}
|
|
21011
|
+
paneIndex() {
|
|
21012
|
+
try {
|
|
21013
|
+
return this._private__series.getPane().paneIndex();
|
|
21014
|
+
}
|
|
21015
|
+
catch (error) {
|
|
21016
|
+
// The engine asserts the series belongs to a pane, and that lookup can
|
|
21017
|
+
// come back empty while panes are being rebuilt. The main pane is the
|
|
21018
|
+
// right guess and keeps the gesture alive instead of throwing mid-drag.
|
|
21019
|
+
return 0;
|
|
21020
|
+
}
|
|
21021
|
+
}
|
|
21022
|
+
setHoveredPart(part) {
|
|
21023
|
+
const [level, name] = part === null ? [null, null] : part.split(':');
|
|
21024
|
+
const nextLevel = level ?? null;
|
|
21025
|
+
const nextPart = name ?? null;
|
|
21026
|
+
if (this._private__hoveredLevel === nextLevel && this._private__hoveredPart === nextPart) {
|
|
21027
|
+
return;
|
|
21028
|
+
}
|
|
21029
|
+
this._private__hoveredLevel = nextLevel;
|
|
21030
|
+
this._private__hoveredPart = nextPart;
|
|
21031
|
+
this.requestUpdate();
|
|
21032
|
+
}
|
|
21033
|
+
setHovered(hovered) {
|
|
21034
|
+
if (!hovered) {
|
|
21035
|
+
this.setHoveredPart(null);
|
|
21036
|
+
}
|
|
21037
|
+
}
|
|
21038
|
+
isDraggable() {
|
|
21039
|
+
if (this._private__hoveredLevel === null || this._private__hoveredPart === 'close') {
|
|
21040
|
+
return false;
|
|
21041
|
+
}
|
|
21042
|
+
return this._private__levelOptions(this._private__hoveredLevel)?.draggable ?? true;
|
|
21043
|
+
}
|
|
21044
|
+
isClickable() {
|
|
21045
|
+
if (this._private__hoveredPart === 'close') {
|
|
21046
|
+
return true;
|
|
21047
|
+
}
|
|
21048
|
+
return this._private__options.onClick !== undefined;
|
|
21049
|
+
}
|
|
21050
|
+
priceAt(paneY) {
|
|
21051
|
+
const price = this._private__series.coordinateToPrice(paneY);
|
|
21052
|
+
return price === null ? null : price;
|
|
21053
|
+
}
|
|
21054
|
+
/**
|
|
21055
|
+
* Where the level being dragged actually sits. The controller measures the
|
|
21056
|
+
* pointer against it so the level moves by what the pointer moved, instead
|
|
21057
|
+
* of jumping to sit under it — which matters most when the label has been
|
|
21058
|
+
* fanned away from its own line.
|
|
21059
|
+
*/
|
|
21060
|
+
dragAnchorY() {
|
|
21061
|
+
if (this._private__dragLevel === null) {
|
|
21062
|
+
return null;
|
|
21063
|
+
}
|
|
21064
|
+
const price = this._private__levelOptions(this._private__dragLevel)?.price;
|
|
21065
|
+
if (price === undefined || !isFinite(price)) {
|
|
21066
|
+
return null;
|
|
21067
|
+
}
|
|
21068
|
+
const y = this._private__series.priceToCoordinate(price);
|
|
21069
|
+
return y === null ? null : y;
|
|
21070
|
+
}
|
|
21071
|
+
beginDrag() {
|
|
21072
|
+
this._private__dragLevel = this._private__hoveredLevel;
|
|
21073
|
+
if (this._private__dragLevel === null) {
|
|
21074
|
+
return;
|
|
21075
|
+
}
|
|
21076
|
+
const price = this._private__levelOptions(this._private__dragLevel)?.price ?? null;
|
|
21077
|
+
this._private__priceBeforeDrag = price;
|
|
21078
|
+
if (price !== null && this._private__options.onDragStart) {
|
|
21079
|
+
this._private__options.onDragStart(this._private__dragLevel, price);
|
|
21080
|
+
}
|
|
21081
|
+
}
|
|
21082
|
+
dragTo(price) {
|
|
21083
|
+
if (this._private__dragLevel === null) {
|
|
21084
|
+
return;
|
|
21085
|
+
}
|
|
21086
|
+
this._private__applyPrice(this._private__dragLevel, price);
|
|
21087
|
+
if (this._private__options.onDragMove) {
|
|
21088
|
+
this._private__options.onDragMove(this._private__dragLevel, price);
|
|
21089
|
+
}
|
|
21090
|
+
}
|
|
21091
|
+
commitDrag() {
|
|
21092
|
+
const level = this._private__dragLevel;
|
|
21093
|
+
this._private__dragLevel = null;
|
|
21094
|
+
this._private__priceBeforeDrag = null;
|
|
21095
|
+
if (level === null) {
|
|
21096
|
+
return;
|
|
21097
|
+
}
|
|
21098
|
+
const price = this._private__levelOptions(level)?.price;
|
|
21099
|
+
if (price !== undefined && this._private__options.onDragEnd) {
|
|
21100
|
+
this._private__options.onDragEnd(level, price);
|
|
21101
|
+
}
|
|
21102
|
+
}
|
|
21103
|
+
cancelDrag() {
|
|
21104
|
+
const level = this._private__dragLevel;
|
|
21105
|
+
const original = this._private__priceBeforeDrag;
|
|
21106
|
+
this._private__dragLevel = null;
|
|
21107
|
+
this._private__priceBeforeDrag = null;
|
|
21108
|
+
if (level === null) {
|
|
21109
|
+
return;
|
|
21110
|
+
}
|
|
21111
|
+
// Reported even when the price did not move, so a caller that pauses work
|
|
21112
|
+
// on `onDragStart` always gets the matching end of the gesture.
|
|
21113
|
+
const restored = original !== null && original !== this._private__levelOptions(level)?.price;
|
|
21114
|
+
if (restored) {
|
|
21115
|
+
this._private__applyPrice(level, original);
|
|
21116
|
+
if (this._private__options.onDragMove) {
|
|
21117
|
+
this._private__options.onDragMove(level, original);
|
|
21118
|
+
}
|
|
21119
|
+
}
|
|
21120
|
+
if (this._private__options.onDragCancel) {
|
|
21121
|
+
this._private__options.onDragCancel(level, this._private__levelOptions(level)?.price ?? 0);
|
|
21122
|
+
}
|
|
21123
|
+
}
|
|
21124
|
+
click() {
|
|
21125
|
+
const level = this._private__hoveredLevel;
|
|
21126
|
+
if (level === null) {
|
|
21127
|
+
return;
|
|
21128
|
+
}
|
|
21129
|
+
if (this._private__hoveredPart === 'close') {
|
|
21130
|
+
if (this._private__options.onClose) {
|
|
21131
|
+
this._private__options.onClose(level);
|
|
21132
|
+
}
|
|
21133
|
+
return;
|
|
21134
|
+
}
|
|
21135
|
+
const price = this._private__levelOptions(level)?.price;
|
|
21136
|
+
if (price !== undefined && this._private__options.onClick) {
|
|
21137
|
+
this._private__options.onClick(level, price);
|
|
21138
|
+
}
|
|
21139
|
+
}
|
|
21140
|
+
// ---- public API ----
|
|
21141
|
+
/**
|
|
21142
|
+
* Updates the group. Any subset of options can be passed; the levels given
|
|
21143
|
+
* are merged into the current ones, and a level set to `undefined` is
|
|
21144
|
+
* removed.
|
|
21145
|
+
*/
|
|
21146
|
+
applyOptions(options) {
|
|
21147
|
+
// A level under an active gesture owns its price. Callers feeding the
|
|
21148
|
+
// group from a socket re-send the old price several times a second, and
|
|
21149
|
+
// letting that through would pull the level out from under the cursor.
|
|
21150
|
+
const dragged = this._private__dragLevel;
|
|
21151
|
+
const draggedPrice = dragged === null ? undefined : this._private__levelOptions(dragged)?.price;
|
|
21152
|
+
const merged = { ...this._private__options, ...options };
|
|
21153
|
+
merged.entry = { ...this._private__options.entry, ...options.entry };
|
|
21154
|
+
merged.takeProfit = mergeLevel(this._private__options.takeProfit, options, 'takeProfit');
|
|
21155
|
+
merged.stopLoss = mergeLevel(this._private__options.stopLoss, options, 'stopLoss');
|
|
21156
|
+
this._private__options = merged;
|
|
21157
|
+
if (dragged !== null && draggedPrice !== undefined) {
|
|
21158
|
+
const level = this._private__levelOptions(dragged);
|
|
21159
|
+
if (level !== null) {
|
|
21160
|
+
level.price = draggedPrice;
|
|
21161
|
+
}
|
|
21162
|
+
}
|
|
21163
|
+
if (options.theme) {
|
|
21164
|
+
// Merged onto what is already applied, so two partial theme updates do
|
|
21165
|
+
// not undo each other.
|
|
21166
|
+
this._private__theme = { ...this._private__theme, ...options.theme };
|
|
21167
|
+
}
|
|
21168
|
+
this.requestUpdate();
|
|
21169
|
+
}
|
|
21170
|
+
/**
|
|
21171
|
+
* Moves one level to a new price, recomputing its text when the level has a
|
|
21172
|
+
* `formatText`.
|
|
21173
|
+
*
|
|
21174
|
+
* Ignored for a level the user is currently dragging — the gesture owns the
|
|
21175
|
+
* price until it reports back through
|
|
21176
|
+
* {@link PositionLineOptions.onDragEnd} or
|
|
21177
|
+
* {@link PositionLineOptions.onDragCancel}.
|
|
21178
|
+
*/
|
|
21179
|
+
setPrice(level, price) {
|
|
21180
|
+
if (this._private__dragLevel === level) {
|
|
21181
|
+
return;
|
|
21182
|
+
}
|
|
21183
|
+
this._private__applyPrice(level, price);
|
|
21184
|
+
}
|
|
21185
|
+
/**
|
|
21186
|
+
* Replaces one level's label text. Use this for text that changes on its own
|
|
21187
|
+
* — a ticking P&L on a fixed entry — rather than with the price.
|
|
21188
|
+
*/
|
|
21189
|
+
setText(level, text) {
|
|
21190
|
+
const options = this._private__levelOptions(level);
|
|
21191
|
+
if (options === null) {
|
|
21192
|
+
return;
|
|
21193
|
+
}
|
|
21194
|
+
options.text = text;
|
|
21195
|
+
this.requestUpdate();
|
|
21196
|
+
}
|
|
21197
|
+
/**
|
|
21198
|
+
* The current price of each level that is present.
|
|
21199
|
+
*/
|
|
21200
|
+
prices() {
|
|
21201
|
+
const result = {};
|
|
21202
|
+
for (const level of positionLineLevels) {
|
|
21203
|
+
const options = this._private__levelOptions(level);
|
|
21204
|
+
if (options !== null) {
|
|
21205
|
+
result[level] = options.price;
|
|
21206
|
+
}
|
|
21207
|
+
}
|
|
21208
|
+
return result;
|
|
21209
|
+
}
|
|
21210
|
+
options() {
|
|
21211
|
+
return this._private__options;
|
|
21212
|
+
}
|
|
21213
|
+
/**
|
|
21214
|
+
* Removes the group from the chart and releases its share of the input
|
|
21215
|
+
* controller.
|
|
21216
|
+
*/
|
|
21217
|
+
remove() {
|
|
21218
|
+
releaseController(this._private__chart, this);
|
|
21219
|
+
releaseLabelLayout(this._private__chart, this._private__id);
|
|
21220
|
+
this._private__series.detachPrimitive(this);
|
|
21221
|
+
}
|
|
21222
|
+
requestUpdate() {
|
|
21223
|
+
if (this._private__requestUpdate) {
|
|
21224
|
+
this._private__requestUpdate();
|
|
21225
|
+
}
|
|
21226
|
+
}
|
|
21227
|
+
/** Reads back the label the price axis should draw for a level. */
|
|
21228
|
+
_private__axisLabel(level) {
|
|
21229
|
+
const options = this._private__levelOptions(level);
|
|
21230
|
+
if (options === null || !isFinite(options.price) ||
|
|
21231
|
+
this._private__options.visible === false || options.axisLabelVisible === false) {
|
|
21232
|
+
return null;
|
|
21233
|
+
}
|
|
21234
|
+
const y = this._private__series.priceToCoordinate(options.price);
|
|
21235
|
+
if (y === null) {
|
|
21236
|
+
return null;
|
|
21237
|
+
}
|
|
21238
|
+
const color = options.color ?? positionLineDefaultColors[level];
|
|
21239
|
+
return {
|
|
21240
|
+
_internal_coordinate: y,
|
|
21241
|
+
_internal_text: this._private__series.priceFormatter().format(options.price),
|
|
21242
|
+
_internal_color: color,
|
|
21243
|
+
_internal_textColor: this._private__colorParser._internal_generateContrastColors(color)._internal_foreground,
|
|
21244
|
+
};
|
|
21245
|
+
}
|
|
21246
|
+
_private__levelOptions(level) {
|
|
21247
|
+
const options = level === 'entry' ? this._private__options.entry : this._private__options[level];
|
|
21248
|
+
return options ?? null;
|
|
21249
|
+
}
|
|
21250
|
+
_private__applyPrice(level, price) {
|
|
21251
|
+
const options = this._private__levelOptions(level);
|
|
21252
|
+
if (options === null) {
|
|
21253
|
+
return;
|
|
21254
|
+
}
|
|
21255
|
+
options.price = price;
|
|
21256
|
+
if (options.formatText) {
|
|
21257
|
+
options.text = options.formatText(price);
|
|
21258
|
+
}
|
|
21259
|
+
this.requestUpdate();
|
|
21260
|
+
}
|
|
21261
|
+
_private__buildRendererData() {
|
|
21262
|
+
if (!this._private__attached || this._private__options.visible === false) {
|
|
21263
|
+
return null;
|
|
21264
|
+
}
|
|
21265
|
+
const layout = this._private__chart.options()['layout'];
|
|
21266
|
+
const fontSize = this._private__theme.fontSize ?? layout.fontSize;
|
|
21267
|
+
const labelHeight = fontSize + this._private__theme.paddingY * 2;
|
|
21268
|
+
const levels = [];
|
|
21269
|
+
const claims = [];
|
|
21270
|
+
for (const level of positionLineLevels) {
|
|
21271
|
+
const options = this._private__levelOptions(level);
|
|
21272
|
+
// A level added through a patch can be missing its price until the
|
|
21273
|
+
// caller supplies one; drawing it would put it at NaN.
|
|
21274
|
+
if (options === null || !isFinite(options.price)) {
|
|
21275
|
+
continue;
|
|
21276
|
+
}
|
|
21277
|
+
const y = this._private__series.priceToCoordinate(options.price);
|
|
21278
|
+
if (y === null) {
|
|
21279
|
+
continue;
|
|
21280
|
+
}
|
|
21281
|
+
const isEntry = level === 'entry';
|
|
21282
|
+
claims.push({ _internal_level: level, _internal_y: y, _internal_height: labelHeight });
|
|
21283
|
+
levels.push({
|
|
21284
|
+
_internal_level: level,
|
|
21285
|
+
_internal_y: y,
|
|
21286
|
+
_internal_labelOffsetY: 0,
|
|
21287
|
+
_internal_color: options.color ?? positionLineDefaultColors[level],
|
|
21288
|
+
_internal_text: options.formatText ? options.formatText(options.price) : options.text ?? '',
|
|
21289
|
+
_internal_badge: options.badge ?? '',
|
|
21290
|
+
_internal_badgeFilled: options.badgeFilled ?? isEntry,
|
|
21291
|
+
_internal_lineWidth: options.lineWidth ?? 1,
|
|
21292
|
+
_internal_lineStyle: options.lineStyle ?? (isEntry ? 0 /* LineStyle.Solid */ : 2 /* LineStyle.Dashed */),
|
|
21293
|
+
_internal_closable: options.closable ?? true,
|
|
21294
|
+
_internal_draggable: options.draggable ?? true,
|
|
21295
|
+
});
|
|
21296
|
+
}
|
|
21297
|
+
if (levels.length === 0) {
|
|
21298
|
+
this._private__labelLayout._internal_setClaims(this._private__id, []);
|
|
21299
|
+
return null;
|
|
21300
|
+
}
|
|
21301
|
+
// Published before the offsets are read so every line on the chart —
|
|
21302
|
+
// positions, pending orders, alerts — is fanned against the same set.
|
|
21303
|
+
this._private__labelLayout._internal_setClaims(this._private__id, claims);
|
|
21304
|
+
for (const level of levels) {
|
|
21305
|
+
level._internal_labelOffsetY = this._private__labelLayout._internal_offsetFor(this._private__id, level._internal_level);
|
|
21306
|
+
}
|
|
21307
|
+
return {
|
|
21308
|
+
_internal_levels: levels,
|
|
21309
|
+
_internal_background: this._private__theme.background ?? resolveBackgroundColor(layout.background),
|
|
21310
|
+
_internal_textColor: this._private__theme.textColor,
|
|
21311
|
+
_internal_badgeTextColor: this._private__theme.badgeTextColor,
|
|
21312
|
+
_internal_borderRadius: this._private__theme.borderRadius,
|
|
21313
|
+
_internal_borderWidth: this._private__theme.borderWidth,
|
|
21314
|
+
_internal_paddingX: this._private__theme.paddingX,
|
|
21315
|
+
_internal_paddingY: this._private__theme.paddingY,
|
|
21316
|
+
_internal_backgroundOpacity: this._private__theme.backgroundOpacity,
|
|
21317
|
+
_internal_font: makeFont(fontSize, this._private__theme.fontFamily ?? layout.fontFamily),
|
|
21318
|
+
_internal_fontSize: fontSize,
|
|
21319
|
+
_internal_connectorVisible: this._private__options.connectorVisible !== false && levels.length > 1,
|
|
21320
|
+
_internal_rightOffset: this._private__options.rightOffset ?? DEFAULT_RIGHT_OFFSET,
|
|
21321
|
+
_internal_hoveredLevel: this._private__hoveredLevel,
|
|
21322
|
+
_internal_hoveredPart: this._private__hoveredPart,
|
|
21323
|
+
};
|
|
21324
|
+
}
|
|
21325
|
+
}
|
|
21326
|
+
function mergeLevel(current, options, key) {
|
|
21327
|
+
if (!(key in options)) {
|
|
21328
|
+
return current;
|
|
21329
|
+
}
|
|
21330
|
+
const next = options[key];
|
|
21331
|
+
return next === undefined ? undefined : { ...current, ...next };
|
|
21332
|
+
}
|
|
21333
|
+
/**
|
|
21334
|
+
* Reads the chart's own background so the label sits on a solid surface. The
|
|
21335
|
+
* parameter is typed as the public {@link Background}: the production build
|
|
21336
|
+
* renames the members of structural types it does not see in the public API,
|
|
21337
|
+
* and an anonymous `{ color: string }` here resolved to nothing at runtime.
|
|
21338
|
+
*/
|
|
21339
|
+
function resolveBackgroundColor(background) {
|
|
21340
|
+
return background.type === "solid" /* ColorType.Solid */ ? background.color : background.topColor;
|
|
21341
|
+
}
|
|
21342
|
+
/**
|
|
21343
|
+
* Creates a position line group: entry, take profit and stop loss drawn as one
|
|
21344
|
+
* interactive unit.
|
|
21345
|
+
*
|
|
21346
|
+
* @param series - The series the levels are priced against.
|
|
21347
|
+
* @param chart - The chart the series belongs to.
|
|
21348
|
+
* @param options - The levels and their style.
|
|
21349
|
+
*
|
|
21350
|
+
* @example
|
|
21351
|
+
* ```js
|
|
21352
|
+
* const position = createPositionLine(candleSeries, chart, {
|
|
21353
|
+
* entry: { price: 4306.79, badge: '1', formatText: () => '+ 2.32 USD' },
|
|
21354
|
+
* takeProfit: { price: 4349.58, badge: '1', text: '+ 42.79 USD' },
|
|
21355
|
+
* stopLoss: { price: 4266.06, badge: '1', text: '- 40.73 USD' },
|
|
21356
|
+
* onClose: (level) => console.log('close', level),
|
|
21357
|
+
* onDragEnd: (level, price) => console.log(level, 'moved to', price),
|
|
21358
|
+
* });
|
|
21359
|
+
* ```
|
|
21360
|
+
*/
|
|
21361
|
+
function createPositionLine(series, chart, options) {
|
|
21362
|
+
return new PositionLine(chart, series, options);
|
|
21363
|
+
}
|
|
21364
|
+
|
|
20254
21365
|
/** Hit parts that drag the whole object rather than a single handle. */
|
|
20255
21366
|
const WHOLE_OBJECT_PARTS = new Set([
|
|
20256
21367
|
'line',
|
|
@@ -21130,13 +22241,6 @@
|
|
|
21130
22241
|
// Clean up references
|
|
21131
22242
|
this._private__primitive = null;
|
|
21132
22243
|
this._private__paneView = null;
|
|
21133
|
-
// Force chart update by triggering a resize
|
|
21134
|
-
const chartElement = this._chart.chartElement?.();
|
|
21135
|
-
if (chartElement) {
|
|
21136
|
-
const currentWidth = chartElement.clientWidth;
|
|
21137
|
-
const currentHeight = chartElement.clientHeight;
|
|
21138
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
21139
|
-
}
|
|
21140
22244
|
}
|
|
21141
22245
|
if (this._private__trendOptions.onRemove) {
|
|
21142
22246
|
this._private__trendOptions.onRemove();
|
|
@@ -21409,12 +22513,6 @@
|
|
|
21409
22513
|
this._series.detachPrimitive(this._private__primitive);
|
|
21410
22514
|
this._private__primitive = null;
|
|
21411
22515
|
this._private__paneView = null;
|
|
21412
|
-
const chartElement = this._chart.chartElement?.();
|
|
21413
|
-
if (chartElement) {
|
|
21414
|
-
const currentWidth = chartElement.clientWidth;
|
|
21415
|
-
const currentHeight = chartElement.clientHeight;
|
|
21416
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
21417
|
-
}
|
|
21418
22516
|
}
|
|
21419
22517
|
if (this._private__hLineOptions.onRemove) {
|
|
21420
22518
|
this._private__hLineOptions.onRemove();
|
|
@@ -23253,12 +24351,6 @@
|
|
|
23253
24351
|
this._series.detachPrimitive(this._private__primitive);
|
|
23254
24352
|
this._private__primitive = null;
|
|
23255
24353
|
this._private__paneView = null;
|
|
23256
|
-
const chartElement = this._chart.chartElement?.();
|
|
23257
|
-
if (chartElement) {
|
|
23258
|
-
const currentWidth = chartElement.clientWidth;
|
|
23259
|
-
const currentHeight = chartElement.clientHeight;
|
|
23260
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
23261
|
-
}
|
|
23262
24354
|
}
|
|
23263
24355
|
if (this._private__vLineOptions.onRemove) {
|
|
23264
24356
|
this._private__vLineOptions.onRemove();
|
|
@@ -23911,12 +25003,6 @@
|
|
|
23911
25003
|
this._series.detachPrimitive(this._private__primitive);
|
|
23912
25004
|
this._private__primitive = null;
|
|
23913
25005
|
this._private__paneView = null;
|
|
23914
|
-
const chartElement = this._chart.chartElement?.();
|
|
23915
|
-
if (chartElement) {
|
|
23916
|
-
const currentWidth = chartElement.clientWidth;
|
|
23917
|
-
const currentHeight = chartElement.clientHeight;
|
|
23918
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
23919
|
-
}
|
|
23920
25006
|
}
|
|
23921
25007
|
if (this._private__drawingManager) {
|
|
23922
25008
|
this._private__drawingManager._internal_destroy();
|
|
@@ -23932,12 +25018,6 @@
|
|
|
23932
25018
|
this._series.detachPrimitive(this._private__primitive);
|
|
23933
25019
|
this._private__primitive = null;
|
|
23934
25020
|
this._private__paneView = null;
|
|
23935
|
-
const chartElement = this._chart.chartElement?.();
|
|
23936
|
-
if (chartElement) {
|
|
23937
|
-
const currentWidth = chartElement.clientWidth;
|
|
23938
|
-
const currentHeight = chartElement.clientHeight;
|
|
23939
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
23940
|
-
}
|
|
23941
25021
|
}
|
|
23942
25022
|
if (this._private__fibOptions.onRemove) {
|
|
23943
25023
|
this._private__fibOptions.onRemove();
|
|
@@ -24365,12 +25445,6 @@
|
|
|
24365
25445
|
this._series.detachPrimitive(this._private__primitive);
|
|
24366
25446
|
this._private__primitive = null;
|
|
24367
25447
|
this._private__paneView = null;
|
|
24368
|
-
const chartElement = this._chart.chartElement?.();
|
|
24369
|
-
if (chartElement) {
|
|
24370
|
-
const currentWidth = chartElement.clientWidth;
|
|
24371
|
-
const currentHeight = chartElement.clientHeight;
|
|
24372
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
24373
|
-
}
|
|
24374
25448
|
}
|
|
24375
25449
|
}
|
|
24376
25450
|
}
|
|
@@ -24812,12 +25886,6 @@
|
|
|
24812
25886
|
this._series.detachPrimitive(this._private__primitive);
|
|
24813
25887
|
this._private__primitive = null;
|
|
24814
25888
|
this._private__paneView = null;
|
|
24815
|
-
const chartElement = this._chart.chartElement?.();
|
|
24816
|
-
if (chartElement) {
|
|
24817
|
-
const currentWidth = chartElement.clientWidth;
|
|
24818
|
-
const currentHeight = chartElement.clientHeight;
|
|
24819
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
24820
|
-
}
|
|
24821
25889
|
}
|
|
24822
25890
|
if (this._private__fanOptions.onRemove) {
|
|
24823
25891
|
this._private__fanOptions.onRemove();
|
|
@@ -25358,12 +26426,6 @@
|
|
|
25358
26426
|
this._series.detachPrimitive(this._private__primitive);
|
|
25359
26427
|
this._private__primitive = null;
|
|
25360
26428
|
this._private__paneView = null;
|
|
25361
|
-
const chartElement = this._chart.chartElement?.();
|
|
25362
|
-
if (chartElement) {
|
|
25363
|
-
const currentWidth = chartElement.clientWidth;
|
|
25364
|
-
const currentHeight = chartElement.clientHeight;
|
|
25365
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
25366
|
-
}
|
|
25367
26429
|
}
|
|
25368
26430
|
if (this._private__rectOptions.onRemove) {
|
|
25369
26431
|
this._private__rectOptions.onRemove();
|
|
@@ -25824,12 +26886,6 @@
|
|
|
25824
26886
|
this._series.detachPrimitive(this._private__primitive);
|
|
25825
26887
|
this._private__primitive = null;
|
|
25826
26888
|
this._private__paneView = null;
|
|
25827
|
-
const chartElement = this._chart.chartElement?.();
|
|
25828
|
-
if (chartElement) {
|
|
25829
|
-
const currentWidth = chartElement.clientWidth;
|
|
25830
|
-
const currentHeight = chartElement.clientHeight;
|
|
25831
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
25832
|
-
}
|
|
25833
26889
|
}
|
|
25834
26890
|
if (this._private__circleOptions.onRemove) {
|
|
25835
26891
|
this._private__circleOptions.onRemove();
|
|
@@ -26379,12 +27435,6 @@
|
|
|
26379
27435
|
this._series.detachPrimitive(this._private__primitive);
|
|
26380
27436
|
this._private__primitive = null;
|
|
26381
27437
|
this._private__paneView = null;
|
|
26382
|
-
const chartElement = this._chart.chartElement?.();
|
|
26383
|
-
if (chartElement) {
|
|
26384
|
-
const currentWidth = chartElement.clientWidth;
|
|
26385
|
-
const currentHeight = chartElement.clientHeight;
|
|
26386
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
26387
|
-
}
|
|
26388
27438
|
}
|
|
26389
27439
|
if (this._private__arrowOptions.onRemove) {
|
|
26390
27440
|
this._private__arrowOptions.onRemove();
|
|
@@ -27245,12 +28295,6 @@
|
|
|
27245
28295
|
this._series.detachPrimitive(this._private__primitive);
|
|
27246
28296
|
this._private__primitive = null;
|
|
27247
28297
|
this._private__paneView = null;
|
|
27248
|
-
const chartElement = this._chart.chartElement?.();
|
|
27249
|
-
if (chartElement) {
|
|
27250
|
-
const currentWidth = chartElement.clientWidth;
|
|
27251
|
-
const currentHeight = chartElement.clientHeight;
|
|
27252
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
27253
|
-
}
|
|
27254
28298
|
}
|
|
27255
28299
|
if (this._private__textOptions.onRemove) {
|
|
27256
28300
|
this._private__textOptions.onRemove();
|
|
@@ -27833,12 +28877,6 @@
|
|
|
27833
28877
|
this._series.detachPrimitive(this._private__primitive);
|
|
27834
28878
|
this._private__primitive = null;
|
|
27835
28879
|
this._private__paneView = null;
|
|
27836
|
-
const chartElement = this._chart.chartElement?.();
|
|
27837
|
-
if (chartElement) {
|
|
27838
|
-
const currentWidth = chartElement.clientWidth;
|
|
27839
|
-
const currentHeight = chartElement.clientHeight;
|
|
27840
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
27841
|
-
}
|
|
27842
28880
|
}
|
|
27843
28881
|
if (this._private__drawingManager) {
|
|
27844
28882
|
this._private__drawingManager._internal_destroy();
|
|
@@ -28694,12 +29732,6 @@
|
|
|
28694
29732
|
this._series.detachPrimitive(this._private__primitive);
|
|
28695
29733
|
this._private__primitive = null;
|
|
28696
29734
|
this._private__paneView = null;
|
|
28697
|
-
const chartElement = this._chart.chartElement?.();
|
|
28698
|
-
if (chartElement) {
|
|
28699
|
-
const currentWidth = chartElement.clientWidth;
|
|
28700
|
-
const currentHeight = chartElement.clientHeight;
|
|
28701
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
28702
|
-
}
|
|
28703
29735
|
}
|
|
28704
29736
|
if (this._private__positionOptions.onRemove) {
|
|
28705
29737
|
this._private__positionOptions.onRemove();
|
|
@@ -29042,12 +30074,6 @@
|
|
|
29042
30074
|
this._series.detachPrimitive(this._private__primitive);
|
|
29043
30075
|
this._private__primitive = null;
|
|
29044
30076
|
this._private__paneView = null;
|
|
29045
|
-
const chartElement = this._chart.chartElement?.();
|
|
29046
|
-
if (chartElement) {
|
|
29047
|
-
const currentWidth = chartElement.clientWidth;
|
|
29048
|
-
const currentHeight = chartElement.clientHeight;
|
|
29049
|
-
this._chart.resize(currentWidth, currentHeight);
|
|
29050
|
-
}
|
|
29051
30077
|
}
|
|
29052
30078
|
if (this._private__ganttOptions.onRemove) {
|
|
29053
30079
|
this._private__ganttOptions.onRemove();
|
|
@@ -29058,6 +30084,8 @@
|
|
|
29058
30084
|
return new GanttTool(context, options);
|
|
29059
30085
|
}
|
|
29060
30086
|
|
|
30087
|
+
/** Undo steps kept, each a full snapshot of every drawing. */
|
|
30088
|
+
const UNDO_HISTORY_LIMIT = 100;
|
|
29061
30089
|
/**
|
|
29062
30090
|
* Central manager for all drawing tools - TradingView style
|
|
29063
30091
|
* Handles multiple tools, selection, deletion, and keyboard shortcuts
|
|
@@ -29080,6 +30108,8 @@
|
|
|
29080
30108
|
this._private__pointerDownHandler = null;
|
|
29081
30109
|
this._private__pointerUpHandler = null;
|
|
29082
30110
|
this._private__pointerCancelHandler = null;
|
|
30111
|
+
this._private__lostCaptureHandler = null;
|
|
30112
|
+
this._private__windowPointerUpHandler = null;
|
|
29083
30113
|
this._private__pointerId = null;
|
|
29084
30114
|
/** A press that grabbed a tool already selected it; its click must not undo that. */
|
|
29085
30115
|
this._private__gestureHandled = false;
|
|
@@ -29245,6 +30275,9 @@
|
|
|
29245
30275
|
return;
|
|
29246
30276
|
}
|
|
29247
30277
|
this._private__past.push(this._private__committed);
|
|
30278
|
+
if (this._private__past.length > UNDO_HISTORY_LIMIT) {
|
|
30279
|
+
this._private__past.shift();
|
|
30280
|
+
}
|
|
29248
30281
|
this._private__future = [];
|
|
29249
30282
|
this._private__committed = this.serializeAll();
|
|
29250
30283
|
}
|
|
@@ -29624,8 +30657,12 @@
|
|
|
29624
30657
|
this._private__completionCheck = null;
|
|
29625
30658
|
}
|
|
29626
30659
|
this._private__drawingCompleted._internal_destroy();
|
|
30660
|
+
this._private__selectionChanged._internal_destroy();
|
|
29627
30661
|
this.clearAll();
|
|
29628
30662
|
this._private__detachEventHandlers();
|
|
30663
|
+
this._private__past = [];
|
|
30664
|
+
this._private__future = [];
|
|
30665
|
+
this._private__committed = [];
|
|
29629
30666
|
}
|
|
29630
30667
|
_private__attachEventHandlers() {
|
|
29631
30668
|
// Click handler for selection
|
|
@@ -29645,9 +30682,15 @@
|
|
|
29645
30682
|
this._private__pointerDownHandler = (event) => this._private__handlePointerDown(event);
|
|
29646
30683
|
this._private__pointerUpHandler = (event) => this._private__handlePointerUp(event);
|
|
29647
30684
|
this._private__pointerCancelHandler = (event) => this._private__handlePointerUp(event);
|
|
30685
|
+
this._private__lostCaptureHandler = (event) => this._private__handleLostPointerCapture(event);
|
|
29648
30686
|
chartElement.addEventListener('pointerdown', this._private__pointerDownHandler);
|
|
29649
30687
|
chartElement.addEventListener('pointerup', this._private__pointerUpHandler);
|
|
29650
30688
|
chartElement.addEventListener('pointercancel', this._private__pointerCancelHandler);
|
|
30689
|
+
chartElement.addEventListener('lostpointercapture', this._private__lostCaptureHandler);
|
|
30690
|
+
}
|
|
30691
|
+
if (isRunningOnClientSide) {
|
|
30692
|
+
this._private__windowPointerUpHandler = (event) => this._private__handlePointerUp(event);
|
|
30693
|
+
window.addEventListener('pointerup', this._private__windowPointerUpHandler);
|
|
29651
30694
|
}
|
|
29652
30695
|
// Keyboard handler for shortcuts
|
|
29653
30696
|
if (this._private__options.enableKeyboardShortcuts) {
|
|
@@ -29680,6 +30723,14 @@
|
|
|
29680
30723
|
chartElement.removeEventListener('pointercancel', this._private__pointerCancelHandler);
|
|
29681
30724
|
this._private__pointerCancelHandler = null;
|
|
29682
30725
|
}
|
|
30726
|
+
if (this._private__lostCaptureHandler) {
|
|
30727
|
+
chartElement.removeEventListener('lostpointercapture', this._private__lostCaptureHandler);
|
|
30728
|
+
this._private__lostCaptureHandler = null;
|
|
30729
|
+
}
|
|
30730
|
+
}
|
|
30731
|
+
if (this._private__windowPointerUpHandler) {
|
|
30732
|
+
window.removeEventListener('pointerup', this._private__windowPointerUpHandler);
|
|
30733
|
+
this._private__windowPointerUpHandler = null;
|
|
29683
30734
|
}
|
|
29684
30735
|
if (this._private__keyboardHandler) {
|
|
29685
30736
|
document.removeEventListener('keydown', this._private__keyboardHandler);
|
|
@@ -29805,7 +30856,12 @@
|
|
|
29805
30856
|
this._private__gestureHandled = true;
|
|
29806
30857
|
this._private__draggedToolId = toolId;
|
|
29807
30858
|
this._private__pointerId = event.pointerId;
|
|
29808
|
-
|
|
30859
|
+
try {
|
|
30860
|
+
chartElement.setPointerCapture(event.pointerId);
|
|
30861
|
+
}
|
|
30862
|
+
catch (error) {
|
|
30863
|
+
this._private__pointerId = null;
|
|
30864
|
+
}
|
|
29809
30865
|
tool.startDrag(endpoint, anchor);
|
|
29810
30866
|
// Panning is stopped by disabling it for the gesture, not by preventing
|
|
29811
30867
|
// the event: preventDefault on pointerdown suppresses the compatibility
|
|
@@ -29819,17 +30875,32 @@
|
|
|
29819
30875
|
if (event && this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
|
|
29820
30876
|
return;
|
|
29821
30877
|
}
|
|
29822
|
-
const
|
|
29823
|
-
|
|
29824
|
-
|
|
29825
|
-
}
|
|
29826
|
-
this._private__releasePointer();
|
|
29827
|
-
this._private__enableChartInteractions();
|
|
30878
|
+
const draggedToolId = this._private__draggedToolId;
|
|
30879
|
+
// Cleared first: anything below can throw, and a half-finished release
|
|
30880
|
+
// would leave the manager dragging forever.
|
|
29828
30881
|
this._private__isDragging = false;
|
|
29829
30882
|
this._private__draggedToolId = null;
|
|
30883
|
+
try {
|
|
30884
|
+
const tool = this._private__tools.get(draggedToolId);
|
|
30885
|
+
if (tool) {
|
|
30886
|
+
tool.stopDrag();
|
|
30887
|
+
}
|
|
30888
|
+
}
|
|
30889
|
+
finally {
|
|
30890
|
+
this._private__releasePointer();
|
|
30891
|
+
this._private__enableChartInteractions();
|
|
30892
|
+
}
|
|
29830
30893
|
// A finished drag is one step, not one per frame
|
|
29831
30894
|
this.recordChange();
|
|
29832
30895
|
}
|
|
30896
|
+
/** Ends the drag when capture is lost without a pointerup ever arriving. */
|
|
30897
|
+
_private__handleLostPointerCapture(event) {
|
|
30898
|
+
if (this._private__pointerId !== null && event.pointerId !== this._private__pointerId) {
|
|
30899
|
+
return;
|
|
30900
|
+
}
|
|
30901
|
+
this._private__pointerId = null;
|
|
30902
|
+
this._private__handlePointerUp();
|
|
30903
|
+
}
|
|
29833
30904
|
/**
|
|
29834
30905
|
* Pane element of a tool's series. The engine asserts the series has a pane,
|
|
29835
30906
|
* so the lookup is guarded.
|
|
@@ -29880,9 +30951,11 @@
|
|
|
29880
30951
|
return;
|
|
29881
30952
|
}
|
|
29882
30953
|
const options = this._private__chart.options();
|
|
30954
|
+
// `options()` returns the live object and `applyOptions` merges into it
|
|
30955
|
+
// in place, so a snapshot by reference restores all-false.
|
|
29883
30956
|
this._private__interactionsBeforeDrag = {
|
|
29884
|
-
_internal_handleScroll: options.handleScroll,
|
|
29885
|
-
_internal_handleScale: options.handleScale,
|
|
30957
|
+
_internal_handleScroll: clone(options.handleScroll),
|
|
30958
|
+
_internal_handleScale: clone(options.handleScale),
|
|
29886
30959
|
};
|
|
29887
30960
|
this._private__chart.applyOptions({
|
|
29888
30961
|
handleScroll: false,
|
|
@@ -29968,7 +31041,7 @@
|
|
|
29968
31041
|
* Returns the current version as a string. For example `'1.0.0'`.
|
|
29969
31042
|
*/
|
|
29970
31043
|
function version() {
|
|
29971
|
-
return "2.
|
|
31044
|
+
return "2.3.1";
|
|
29972
31045
|
}
|
|
29973
31046
|
|
|
29974
31047
|
var PipsendChartsModule = /*#__PURE__*/Object.freeze({
|
|
@@ -30004,6 +31077,7 @@
|
|
|
30004
31077
|
PitchforkTool: PitchforkTool,
|
|
30005
31078
|
PolygonTool: PolygonTool,
|
|
30006
31079
|
PositionCalculator: PositionCalculator,
|
|
31080
|
+
PositionLine: PositionLine,
|
|
30007
31081
|
PositionMarkersManager: PositionMarkersManager,
|
|
30008
31082
|
PositionTool: PositionTool,
|
|
30009
31083
|
get PriceLineSource () { return PriceLineSource; },
|
|
@@ -30061,6 +31135,7 @@
|
|
|
30061
31135
|
createParallelChannelTool: createParallelChannelTool,
|
|
30062
31136
|
createPitchforkTool: createPitchforkTool,
|
|
30063
31137
|
createPolygonTool: createPolygonTool,
|
|
31138
|
+
createPositionLine: createPositionLine,
|
|
30064
31139
|
createPositionMarkers: createPositionMarkers,
|
|
30065
31140
|
createPositionTool: createPositionTool,
|
|
30066
31141
|
createRectangleTool: createRectangleTool,
|