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.
|
* 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]
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue