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:
parent
d7c8d42905
commit
2d01ee07eb
|
|
@ -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]
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue