Fix anchor positioning when outside visible time range
- Add _getAnchorPixelPosition method that handles anchors outside the visible time range by calculating their position along the line - Update _createAnchor to use robust position calculation - Update _updateFormationPositionsInPlace to use new method Previously, anchors would not update when their time coordinate was outside the visible range (because _chartToPixel returns null). Lines would update correctly via _getInfiniteLineEndpoints, but anchors would stay at stale positions. Now anchors are positioned at the viewport edge when their actual time coordinate is outside the visible range, keeping them on the line. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
d7398569a8
commit
ee199022fa
|
|
@ -316,6 +316,65 @@ class FormationOverlay {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get pixel position for an anchor point, handling cases where the anchor
|
||||
* is outside the visible time range.
|
||||
* @param {Object} anchorPoint - {time, price} - the anchor to position
|
||||
* @param {Object} linePoint1 - {time, price} - first point defining the line
|
||||
* @param {Object} linePoint2 - {time, price} - second point defining the line
|
||||
* @returns {{x: number, y: number}|null}
|
||||
*/
|
||||
_getAnchorPixelPosition(anchorPoint, linePoint1, linePoint2) {
|
||||
// Try direct conversion first
|
||||
const directPixel = this._chartToPixel(anchorPoint.time, anchorPoint.price);
|
||||
if (directPixel) {
|
||||
return directPixel;
|
||||
}
|
||||
|
||||
// Anchor is outside visible time range - calculate position along the line
|
||||
if (!this.chart || !this.candleSeries) return null;
|
||||
|
||||
try {
|
||||
const timeScale = this.chart.timeScale();
|
||||
const visibleRange = timeScale.getVisibleRange();
|
||||
if (!visibleRange) return null;
|
||||
|
||||
const leftTime = visibleRange.from;
|
||||
const rightTime = visibleRange.to;
|
||||
|
||||
// Calculate line slope
|
||||
const dt = linePoint2.time - linePoint1.time;
|
||||
const dp = linePoint2.price - linePoint1.price;
|
||||
|
||||
if (Math.abs(dt) < 1) {
|
||||
// Near-vertical line in time - use center of viewport for x
|
||||
const midTime = (leftTime + rightTime) / 2;
|
||||
return this._chartToPixel(midTime, anchorPoint.price);
|
||||
}
|
||||
|
||||
const slope = dp / dt;
|
||||
|
||||
// Determine if anchor is to the left or right of visible range
|
||||
if (anchorPoint.time < leftTime) {
|
||||
// Anchor is to the left - position at left edge of viewport
|
||||
const priceAtLeft = slope * (leftTime - linePoint1.time) + linePoint1.price;
|
||||
return this._chartToPixel(leftTime, priceAtLeft);
|
||||
} else if (anchorPoint.time > rightTime) {
|
||||
// Anchor is to the right - position at right edge of viewport
|
||||
const priceAtRight = slope * (rightTime - linePoint1.time) + linePoint1.price;
|
||||
return this._chartToPixel(rightTime, priceAtRight);
|
||||
}
|
||||
|
||||
// Anchor time is in range but price conversion failed - shouldn't happen
|
||||
// but fall back to calculating price at anchor time
|
||||
const priceAtAnchor = slope * (anchorPoint.time - linePoint1.time) + linePoint1.price;
|
||||
return this._chartToPixel(anchorPoint.time, priceAtAnchor);
|
||||
} catch (e) {
|
||||
console.warn('FormationOverlay: anchor position calculation failed', e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate infinite line endpoints that extend to viewport edges.
|
||||
* Uses a robust approach that works even when anchor points are outside visible range.
|
||||
|
|
@ -955,16 +1014,16 @@ class FormationOverlay {
|
|||
|
||||
// For channel secondary line: only 1 center anchor (for offset adjustment)
|
||||
if (isChannel && line.isSecondary) {
|
||||
const anchorOffset = this._createAnchor(centerPoint, formation.tbl_key, index, 'channel_offset');
|
||||
const anchorOffset = this._createAnchor(centerPoint, formation.tbl_key, index, 'channel_offset', line.point1, line.point2);
|
||||
if (anchorOffset) {
|
||||
formationGroup.appendChild(anchorOffset);
|
||||
elements.push(anchorOffset);
|
||||
}
|
||||
} else {
|
||||
// Primary line or single line: 3 anchors
|
||||
const anchor1 = this._createAnchor(line.point1, formation.tbl_key, index, 'point1');
|
||||
const anchorCenter = this._createAnchor(centerPoint, formation.tbl_key, index, 'center');
|
||||
const anchor2 = this._createAnchor(line.point2, formation.tbl_key, index, 'point2');
|
||||
const anchor1 = this._createAnchor(line.point1, formation.tbl_key, index, 'point1', line.point1, line.point2);
|
||||
const anchorCenter = this._createAnchor(centerPoint, formation.tbl_key, index, 'center', line.point1, line.point2);
|
||||
const anchor2 = this._createAnchor(line.point2, formation.tbl_key, index, 'point2', line.point1, line.point2);
|
||||
|
||||
if (anchor1) {
|
||||
formationGroup.appendChild(anchor1);
|
||||
|
|
@ -1071,14 +1130,17 @@ class FormationOverlay {
|
|||
|
||||
/**
|
||||
* Create an anchor circle for dragging.
|
||||
* @param {Object} point - {time, price}
|
||||
* @param {Object} point - {time, price} - the anchor point
|
||||
* @param {string} tblKey - Formation tbl_key
|
||||
* @param {number} lineIndex - Index of line in formation
|
||||
* @param {string} pointKey - 'point1', 'point2', 'center', or 'channel_offset'
|
||||
* @param {Object} linePoint1 - {time, price} - first point of the line (for position calculation)
|
||||
* @param {Object} linePoint2 - {time, price} - second point of the line (for position calculation)
|
||||
* @returns {SVGCircleElement|null}
|
||||
*/
|
||||
_createAnchor(point, tblKey, lineIndex, pointKey) {
|
||||
const pixel = this._chartToPixel(point.time, point.price);
|
||||
_createAnchor(point, tblKey, lineIndex, pointKey, linePoint1, linePoint2) {
|
||||
// Use robust position calculation that handles anchors outside visible range
|
||||
const pixel = this._getAnchorPixelPosition(point, linePoint1 || point, linePoint2 || point);
|
||||
if (!pixel) return null;
|
||||
|
||||
const isCenter = pointKey === 'center';
|
||||
|
|
@ -1192,6 +1254,7 @@ class FormationOverlay {
|
|||
});
|
||||
|
||||
// Update anchor positions
|
||||
// Use _getAnchorPixelPosition which handles anchors outside visible range
|
||||
let anchorIdx = 0;
|
||||
lines.forEach((line, index) => {
|
||||
const isChannel = formation.formation_type === 'channel';
|
||||
|
|
@ -1202,7 +1265,7 @@ class FormationOverlay {
|
|||
|
||||
if (isChannel && line.isSecondary) {
|
||||
// Secondary line: only center anchor
|
||||
const pixel = this._chartToPixel(centerPoint.time, centerPoint.price);
|
||||
const pixel = this._getAnchorPixelPosition(centerPoint, line.point1, line.point2);
|
||||
if (pixel && circleElements[anchorIdx]) {
|
||||
circleElements[anchorIdx].setAttribute('cx', pixel.x);
|
||||
circleElements[anchorIdx].setAttribute('cy', pixel.y);
|
||||
|
|
@ -1210,9 +1273,9 @@ class FormationOverlay {
|
|||
}
|
||||
} else {
|
||||
// Primary/single line: 3 anchors (point1, center, point2)
|
||||
const p1Pixel = this._chartToPixel(line.point1.time, line.point1.price);
|
||||
const centerPixel = this._chartToPixel(centerPoint.time, centerPoint.price);
|
||||
const p2Pixel = this._chartToPixel(line.point2.time, line.point2.price);
|
||||
const p1Pixel = this._getAnchorPixelPosition(line.point1, line.point1, line.point2);
|
||||
const centerPixel = this._getAnchorPixelPosition(centerPoint, line.point1, line.point2);
|
||||
const p2Pixel = this._getAnchorPixelPosition(line.point2, line.point1, line.point2);
|
||||
|
||||
if (p1Pixel && circleElements[anchorIdx]) {
|
||||
circleElements[anchorIdx].setAttribute('cx', p1Pixel.x);
|
||||
|
|
|
|||
Loading…
Reference in New Issue