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:
rob 2026-03-10 22:48:03 -03:00
parent d7398569a8
commit ee199022fa
1 changed files with 74 additions and 11 deletions

View File

@ -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. * Calculate infinite line endpoints that extend to viewport edges.
* Uses a robust approach that works even when anchor points are outside visible range. * 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) // For channel secondary line: only 1 center anchor (for offset adjustment)
if (isChannel && line.isSecondary) { 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) { if (anchorOffset) {
formationGroup.appendChild(anchorOffset); formationGroup.appendChild(anchorOffset);
elements.push(anchorOffset); elements.push(anchorOffset);
} }
} else { } else {
// Primary line or single line: 3 anchors // Primary line or single line: 3 anchors
const anchor1 = this._createAnchor(line.point1, formation.tbl_key, index, 'point1'); 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'); 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'); const anchor2 = this._createAnchor(line.point2, formation.tbl_key, index, 'point2', line.point1, line.point2);
if (anchor1) { if (anchor1) {
formationGroup.appendChild(anchor1); formationGroup.appendChild(anchor1);
@ -1071,14 +1130,17 @@ class FormationOverlay {
/** /**
* Create an anchor circle for dragging. * 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 {string} tblKey - Formation tbl_key
* @param {number} lineIndex - Index of line in formation * @param {number} lineIndex - Index of line in formation
* @param {string} pointKey - 'point1', 'point2', 'center', or 'channel_offset' * @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} * @returns {SVGCircleElement|null}
*/ */
_createAnchor(point, tblKey, lineIndex, pointKey) { _createAnchor(point, tblKey, lineIndex, pointKey, linePoint1, linePoint2) {
const pixel = this._chartToPixel(point.time, point.price); // Use robust position calculation that handles anchors outside visible range
const pixel = this._getAnchorPixelPosition(point, linePoint1 || point, linePoint2 || point);
if (!pixel) return null; if (!pixel) return null;
const isCenter = pointKey === 'center'; const isCenter = pointKey === 'center';
@ -1192,6 +1254,7 @@ class FormationOverlay {
}); });
// Update anchor positions // Update anchor positions
// Use _getAnchorPixelPosition which handles anchors outside visible range
let anchorIdx = 0; let anchorIdx = 0;
lines.forEach((line, index) => { lines.forEach((line, index) => {
const isChannel = formation.formation_type === 'channel'; const isChannel = formation.formation_type === 'channel';
@ -1202,7 +1265,7 @@ class FormationOverlay {
if (isChannel && line.isSecondary) { if (isChannel && line.isSecondary) {
// Secondary line: only center anchor // 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]) { if (pixel && circleElements[anchorIdx]) {
circleElements[anchorIdx].setAttribute('cx', pixel.x); circleElements[anchorIdx].setAttribute('cx', pixel.x);
circleElements[anchorIdx].setAttribute('cy', pixel.y); circleElements[anchorIdx].setAttribute('cy', pixel.y);
@ -1210,9 +1273,9 @@ class FormationOverlay {
} }
} else { } else {
// Primary/single line: 3 anchors (point1, center, point2) // Primary/single line: 3 anchors (point1, center, point2)
const p1Pixel = this._chartToPixel(line.point1.time, line.point1.price); const p1Pixel = this._getAnchorPixelPosition(line.point1, line.point1, line.point2);
const centerPixel = this._chartToPixel(centerPoint.time, centerPoint.price); const centerPixel = this._getAnchorPixelPosition(centerPoint, line.point1, line.point2);
const p2Pixel = this._chartToPixel(line.point2.time, line.point2.price); const p2Pixel = this._getAnchorPixelPosition(line.point2, line.point1, line.point2);
if (p1Pixel && circleElements[anchorIdx]) { if (p1Pixel && circleElements[anchorIdx]) {
circleElements[anchorIdx].setAttribute('cx', p1Pixel.x); circleElements[anchorIdx].setAttribute('cx', p1Pixel.x);