Fix formation line rendering for points outside visible range

The _getInfiniteLineEndpoints method was returning null when anchor
points were outside the visible time range, causing lines to not
render. Fixed by calculating line intersections with viewport edges
in chart coordinates first, then converting to pixels.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
rob 2026-03-10 21:55:06 -03:00
parent d7c8d42905
commit 2d01ee07eb
1 changed files with 92 additions and 45 deletions

View File

@ -321,87 +321,134 @@ class FormationOverlay {
/** /**
* Calculate infinite line endpoints that extend to viewport edges. * Calculate infinite line endpoints that extend to viewport edges.
* Handles anchor points outside the visible range by calculating in chart coordinates first.
* @param {Object} point1 - {time, price} * @param {Object} point1 - {time, price}
* @param {Object} point2 - {time, price} * @param {Object} point2 - {time, price}
* @returns {{start: {x, y}, end: {x, y}}|null} * @returns {{start: {x, y}, end: {x, y}}|null}
*/ */
_getInfiniteLineEndpoints(point1, point2) { _getInfiniteLineEndpoints(point1, point2) {
if (!this.svg) return null; if (!this.svg || !this.chart || !this.candleSeries) return null;
const width = this.svg.clientWidth || this.container.clientWidth; const width = this.svg.clientWidth || this.container.clientWidth;
const height = this.svg.clientHeight || this.container.clientHeight; const height = this.svg.clientHeight || this.container.clientHeight;
// Convert anchor points to pixels // Get visible range in chart coordinates
const p1 = this._chartToPixel(point1.time, point1.price); const timeScale = this.chart.timeScale();
const p2 = this._chartToPixel(point2.time, point2.price); const visibleRange = timeScale.getVisibleRange();
if (!visibleRange) return null;
if (!p1 || !p2) return null; const visiblePriceRange = this.candleSeries.priceScale().getVisiblePriceRange?.();
// Calculate line parameters // Get viewport boundaries in chart coordinates
const dx = p2.x - p1.x; const leftTime = visibleRange.from;
const dy = p2.y - p1.y; const rightTime = visibleRange.to;
// Handle vertical line // Estimate price range from top/bottom of viewport
if (Math.abs(dx) < 0.001) { const topPrice = this.candleSeries.coordinateToPrice(0);
const bottomPrice = this.candleSeries.coordinateToPrice(height);
if (topPrice === null || bottomPrice === null) return null;
// Calculate line slope in chart coordinates (price per time unit)
const dt = point2.time - point1.time;
const dp = point2.price - point1.price;
// Handle vertical line (same time, different prices) - shouldn't happen normally
if (Math.abs(dt) < 1) {
const x = timeScale.timeToCoordinate(point1.time);
if (x === null) return null;
return { return {
start: { x: p1.x, y: 0 }, start: { x: x, y: 0 },
end: { x: p1.x, y: height } end: { x: x, y: height }
}; };
} }
// Handle horizontal line // Handle horizontal line (same price) - common case
if (Math.abs(dy) < 0.001) { if (Math.abs(dp) < 0.001) {
const y = this.candleSeries.priceToCoordinate(point1.price);
if (y === null) return null;
return { return {
start: { x: 0, y: p1.y }, start: { x: 0, y: y },
end: { x: width, y: p1.y } end: { x: width, y: y }
}; };
} }
// Calculate slope // Calculate price/time slope
const slope = dy / dx; const slope = dp / dt; // price change per time unit
// Calculate y-intercept (y = mx + b => b = y - mx) // Find prices at viewport time edges using line equation: price = slope * (time - t1) + p1
const b = p1.y - slope * p1.x; const priceAtLeft = slope * (leftTime - point1.time) + point1.price;
const priceAtRight = slope * (rightTime - point1.time) + point1.price;
// Find intersections with viewport edges // Find times at viewport price edges (top and bottom)
const intersections = []; // time = (price - p1) / slope + t1
const timeAtTop = (topPrice - point1.price) / slope + point1.time;
const timeAtBottom = (bottomPrice - point1.price) / slope + point1.time;
// Left edge (x = 0) // Collect valid intersection points in chart coordinates
const yLeft = b; const chartIntersections = [];
if (yLeft >= 0 && yLeft <= height) {
intersections.push({ x: 0, y: yLeft }); // Left edge (time = leftTime)
if (priceAtLeft >= Math.min(topPrice, bottomPrice) && priceAtLeft <= Math.max(topPrice, bottomPrice)) {
chartIntersections.push({ time: leftTime, price: priceAtLeft });
} }
// Right edge (x = width) // Right edge (time = rightTime)
const yRight = slope * width + b; if (priceAtRight >= Math.min(topPrice, bottomPrice) && priceAtRight <= Math.max(topPrice, bottomPrice)) {
if (yRight >= 0 && yRight <= height) { chartIntersections.push({ time: rightTime, price: priceAtRight });
intersections.push({ x: width, y: yRight });
} }
// Top edge (y = 0) // Top edge (price = topPrice)
const xTop = -b / slope; if (timeAtTop >= leftTime && timeAtTop <= rightTime) {
if (xTop >= 0 && xTop <= width) { chartIntersections.push({ time: timeAtTop, price: topPrice });
intersections.push({ x: xTop, y: 0 });
} }
// Bottom edge (y = height) // Bottom edge (price = bottomPrice)
const xBottom = (height - b) / slope; if (timeAtBottom >= leftTime && timeAtBottom <= rightTime) {
if (xBottom >= 0 && xBottom <= width) { chartIntersections.push({ time: timeAtBottom, price: bottomPrice });
intersections.push({ x: xBottom, y: height });
} }
// We need exactly 2 intersections // Remove duplicates (can happen at corners)
if (intersections.length < 2) { const uniqueIntersections = [];
// Fallback: just use the anchor points for (const pt of chartIntersections) {
return { start: p1, end: p2 }; const isDupe = uniqueIntersections.some(
existing => Math.abs(existing.time - pt.time) < 1 && Math.abs(existing.price - pt.price) < 0.01
);
if (!isDupe) {
uniqueIntersections.push(pt);
}
}
// We need at least 2 intersections
if (uniqueIntersections.length < 2) {
// Line doesn't cross visible viewport - try using anchor points directly
const p1 = this._chartToPixel(point1.time, point1.price);
const p2 = this._chartToPixel(point2.time, point2.price);
if (p1 && p2) {
return { start: p1, end: p2 };
}
return null;
}
// Convert chart intersections to pixel coordinates
const pixelIntersections = [];
for (const pt of uniqueIntersections) {
const pixel = this._chartToPixel(pt.time, pt.price);
if (pixel) {
pixelIntersections.push(pixel);
}
}
if (pixelIntersections.length < 2) {
return null;
} }
// Sort by x to get consistent start/end // Sort by x to get consistent start/end
intersections.sort((a, b) => a.x - b.x); pixelIntersections.sort((a, b) => a.x - b.x);
return { return {
start: intersections[0], start: pixelIntersections[0],
end: intersections[intersections.length - 1] end: pixelIntersections[pixelIntersections.length - 1]
}; };
} }