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