diff --git a/src/static/formation_overlay.js b/src/static/formation_overlay.js index 04ec67e..76afd38 100644 --- a/src/static/formation_overlay.js +++ b/src/static/formation_overlay.js @@ -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);