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