Fix drawing tools: allow future/whitespace drawing and improve TF scaling precision
This commit is contained in:
@ -87,13 +87,16 @@ export class DrawingManager {
|
||||
const isUp = d.p2.price >= d.p1.price;
|
||||
const color = isUp ? '#26a69a' : '#ef5350';
|
||||
[d.p1.price, d.p2.price].forEach(price => {
|
||||
views.push({
|
||||
coordinate: () => this.series.priceToCoordinate(price),
|
||||
text: () => price.toFixed(2),
|
||||
textColor: () => '#ffffff',
|
||||
backColor: () => color,
|
||||
visible: () => true,
|
||||
});
|
||||
const coord = this.series.priceToCoordinate(price);
|
||||
if (coord !== null) {
|
||||
views.push({
|
||||
coordinate: () => coord,
|
||||
text: () => price.toFixed(2),
|
||||
textColor: () => '#ffffff',
|
||||
backColor: () => color,
|
||||
visible: () => true,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return views;
|
||||
@ -109,22 +112,25 @@ export class DrawingManager {
|
||||
const isUp = d.p2.price >= d.p1.price;
|
||||
const color = isUp ? '#26a69a' : '#ef5350';
|
||||
[d.p1.time, d.p2.time].forEach(time => {
|
||||
views.push({
|
||||
coordinate: () => this.chart.timeScale().timeToCoordinate(time),
|
||||
text: () => {
|
||||
const date = new Date(time * 1000);
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const month = months[date.getMonth()];
|
||||
const year = date.getFullYear().toString().slice(-2);
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const mins = date.getMinutes().toString().padStart(2, '0');
|
||||
return `${day} ${month} '${year} ${hours}:${mins}`;
|
||||
},
|
||||
textColor: () => '#ffffff',
|
||||
backColor: () => color,
|
||||
visible: () => true,
|
||||
});
|
||||
const coord = this.timeToX(time);
|
||||
if (coord !== null) {
|
||||
views.push({
|
||||
coordinate: () => coord,
|
||||
text: () => {
|
||||
const date = new Date(time * 1000);
|
||||
const day = date.getDate().toString().padStart(2, '0');
|
||||
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
||||
const month = months[date.getMonth()];
|
||||
const year = date.getFullYear().toString().slice(-2);
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const mins = date.getMinutes().toString().padStart(2, '0');
|
||||
return `${day} ${month} '${year} ${hours}:${mins}`;
|
||||
},
|
||||
textColor: () => '#ffffff',
|
||||
backColor: () => color,
|
||||
visible: () => true,
|
||||
});
|
||||
}
|
||||
});
|
||||
});
|
||||
return views;
|
||||
@ -178,11 +184,51 @@ export class DrawingManager {
|
||||
this.update();
|
||||
}
|
||||
|
||||
getIntervalSeconds() {
|
||||
const interval = this.dashboard.currentInterval;
|
||||
const unit = interval.slice(-1); // Keep case to distinguish 'm' and 'M'
|
||||
const value = parseInt(interval.slice(0, -1));
|
||||
|
||||
switch (unit) {
|
||||
case 'm': return value * 60;
|
||||
case 'h': return value * 3600;
|
||||
case 'd': return value * 86400;
|
||||
case 'w': return value * 604800;
|
||||
case 'M': return value * 2592000;
|
||||
default: return 60;
|
||||
}
|
||||
}
|
||||
|
||||
getMousePos(e) {
|
||||
const rect = this.container.getBoundingClientRect();
|
||||
const x = e.clientX - rect.left;
|
||||
const y = e.clientY - rect.top;
|
||||
const time = this.chart.timeScale().coordinateToTime(x);
|
||||
|
||||
const timeScale = this.chart.timeScale();
|
||||
const candleData = this.dashboard.allData.get(this.dashboard.currentInterval) || [];
|
||||
let time = timeScale.coordinateToTime(x);
|
||||
|
||||
// Handle whitespace / future drawing
|
||||
if (candleData.length > 0) {
|
||||
const logical = timeScale.coordinateToLogical(x);
|
||||
const lastIdx = candleData.length - 1;
|
||||
|
||||
// If coordinateToTime returns null OR logical index > last index (future)
|
||||
if (logical !== null && (time === null || logical > lastIdx)) {
|
||||
const lastCandle = candleData[lastIdx];
|
||||
const intervalSec = this.getIntervalSeconds();
|
||||
const offset = logical - lastIdx;
|
||||
|
||||
// Calculate time based on logical offset from the last candle
|
||||
time = lastCandle.time + Math.round(offset * intervalSec);
|
||||
} else if (logical !== null && logical < 0) {
|
||||
// Handle past whitespace (if any)
|
||||
const firstCandle = candleData[0];
|
||||
const intervalSec = this.getIntervalSeconds();
|
||||
time = firstCandle.time + Math.round(logical * intervalSec);
|
||||
}
|
||||
}
|
||||
|
||||
const price = this.series.coordinateToPrice(y);
|
||||
return { x, y, time, price };
|
||||
}
|
||||
@ -412,9 +458,9 @@ export class DrawingManager {
|
||||
}
|
||||
|
||||
if (d.p1 && d.p2) {
|
||||
const x1 = this.chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p1.time));
|
||||
const x1 = this.timeToX(d.p1.time);
|
||||
const y1 = this.series.priceToCoordinate(d.p1.price);
|
||||
const x2 = this.chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p2.time));
|
||||
const x2 = this.timeToX(d.p2.time);
|
||||
const y2 = this.series.priceToCoordinate(d.p2.price);
|
||||
|
||||
if (x1 === null || y1 === null || x2 === null || y2 === null) continue;
|
||||
@ -430,14 +476,14 @@ export class DrawingManager {
|
||||
}
|
||||
} else if (d.price !== undefined && d.type === 'horizontal_line') {
|
||||
const dy = this.series.priceToCoordinate(d.price);
|
||||
if (Math.abs(y - dy) < threshold) return { drawing: d, part: 'price' };
|
||||
if (dy !== null && Math.abs(y - dy) < threshold) return { drawing: d, part: 'price' };
|
||||
} else if (d.time !== undefined && d.type === 'vertical_line') {
|
||||
const dx = this.chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.time));
|
||||
if (Math.abs(x - dx) < threshold) return { drawing: d, part: 'time' };
|
||||
const dx = this.timeToX(d.time);
|
||||
if (dx !== null && Math.abs(x - dx) < threshold) return { drawing: d, part: 'time' };
|
||||
} else if (d.time !== undefined && d.price !== undefined) {
|
||||
const dx = this.chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.time));
|
||||
const dx = this.timeToX(d.time);
|
||||
const dy = this.series.priceToCoordinate(d.price);
|
||||
if (Math.hypot(x - dx, y - dy) < threshold) return { drawing: d, part: 'all' };
|
||||
if (dx !== null && dy !== null && Math.hypot(x - dx, y - dy) < threshold) return { drawing: d, part: 'all' };
|
||||
}
|
||||
}
|
||||
return null;
|
||||
@ -460,8 +506,25 @@ export class DrawingManager {
|
||||
const candleData = this.dashboard.allData.get(this.dashboard.currentInterval) || [];
|
||||
if (candleData.length === 0) return time;
|
||||
|
||||
const firstCandle = candleData[0];
|
||||
const lastCandle = candleData[candleData.length - 1];
|
||||
|
||||
// Handle future whitespace
|
||||
if (time > lastCandle.time) {
|
||||
const intervalSec = this.getIntervalSeconds();
|
||||
const offset = Math.round((time - lastCandle.time) / intervalSec);
|
||||
return lastCandle.time + (offset * intervalSec);
|
||||
}
|
||||
|
||||
// Handle past whitespace
|
||||
if (time < firstCandle.time) {
|
||||
const intervalSec = this.getIntervalSeconds();
|
||||
const offset = Math.round((firstCandle.time - time) / intervalSec);
|
||||
return firstCandle.time - (offset * intervalSec);
|
||||
}
|
||||
|
||||
// Find nearest timestamp in current data
|
||||
// Since data is sorted, we can be efficient, but for now a simple find is safest
|
||||
// Since data is sorted, we can be efficient
|
||||
let nearest = candleData[0].time;
|
||||
let minDiff = Math.abs(time - nearest);
|
||||
|
||||
@ -481,6 +544,66 @@ export class DrawingManager {
|
||||
return nearest;
|
||||
}
|
||||
|
||||
timeToX(time) {
|
||||
if (time === null) return null;
|
||||
const timeScale = this.chart.timeScale();
|
||||
const candleData = this.dashboard.allData.get(this.dashboard.currentInterval) || [];
|
||||
if (candleData.length === 0) return timeScale.timeToCoordinate(time);
|
||||
|
||||
// Native check (exact match)
|
||||
const nativeCoord = timeScale.timeToCoordinate(time);
|
||||
if (nativeCoord !== null) return nativeCoord;
|
||||
|
||||
const firstCandle = candleData[0];
|
||||
const lastCandle = candleData[candleData.length - 1];
|
||||
const intervalSec = this.getIntervalSeconds();
|
||||
|
||||
// 1. Whitespace: Future
|
||||
if (time > lastCandle.time) {
|
||||
const lastIdx = candleData.length - 1;
|
||||
const offset = (time - lastCandle.time) / intervalSec;
|
||||
return timeScale.logicalToCoordinate(lastIdx + offset);
|
||||
}
|
||||
|
||||
// 2. Whitespace: Past
|
||||
if (time < firstCandle.time) {
|
||||
const offset = (time - firstCandle.time) / intervalSec;
|
||||
return timeScale.logicalToCoordinate(offset);
|
||||
}
|
||||
|
||||
// 3. Inside Data Range: Find the two candles it falls between
|
||||
// Binary search for efficiency
|
||||
let left = 0;
|
||||
let right = candleData.length - 1;
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
if (candleData[mid].time === time) return timeScale.timeToCoordinate(time); // Should have been caught by nativeCoord
|
||||
if (candleData[mid].time < time) left = mid + 1;
|
||||
else right = mid - 1;
|
||||
}
|
||||
|
||||
// It falls between candleData[right] and candleData[left]
|
||||
const c1 = candleData[right];
|
||||
const c2 = candleData[left];
|
||||
|
||||
if (!c1 || !c2) return null; // Should not happen given bounds check above
|
||||
|
||||
const x1 = timeScale.timeToCoordinate(c1.time);
|
||||
const x2 = timeScale.timeToCoordinate(c2.time);
|
||||
|
||||
if (x1 === null || x2 === null) {
|
||||
// If native coordinates fail (e.g. off-screen), fallback to logical interpolation
|
||||
const logical1 = right;
|
||||
const logical2 = left;
|
||||
const ratio = (time - c1.time) / (c2.time - c1.time);
|
||||
return timeScale.logicalToCoordinate(logical1 + ratio * (logical2 - logical1));
|
||||
}
|
||||
|
||||
// Linear interpolation between the two candle coordinates
|
||||
const ratio = (time - c1.time) / (c2.time - c1.time);
|
||||
return x1 + ratio * (x2 - x1);
|
||||
}
|
||||
|
||||
render(target) {
|
||||
target.useMediaCoordinateSpace((scope) => {
|
||||
const ctx = scope.context;
|
||||
@ -497,9 +620,9 @@ export class DrawingManager {
|
||||
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
|
||||
|
||||
if (d.type === 'trend_line' || d.type === 'ray') {
|
||||
const x1 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p1.time));
|
||||
const x1 = this.timeToX(d.p1.time);
|
||||
const y1 = series.priceToCoordinate(d.p1.price);
|
||||
const x2 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p2.time));
|
||||
const x2 = this.timeToX(d.p2.time);
|
||||
const y2 = series.priceToCoordinate(d.p2.price);
|
||||
if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
|
||||
ctx.beginPath();
|
||||
@ -518,9 +641,9 @@ export class DrawingManager {
|
||||
}
|
||||
}
|
||||
} else if (d.type === 'rectangle') {
|
||||
const x1 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p1.time));
|
||||
const x1 = this.timeToX(d.p1.time);
|
||||
const y1 = series.priceToCoordinate(d.p1.price);
|
||||
const x2 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p2.time));
|
||||
const x2 = this.timeToX(d.p2.time);
|
||||
const y2 = series.priceToCoordinate(d.p2.price);
|
||||
if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
|
||||
ctx.fillStyle = d.color + '22';
|
||||
@ -536,22 +659,22 @@ export class DrawingManager {
|
||||
const y = series.priceToCoordinate(d.price);
|
||||
if (y !== null) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(0, y);
|
||||
ctx.lineTo(scope.mediaSize.width, y);
|
||||
ctx.moveTo(-1000, y);
|
||||
ctx.lineTo(scope.mediaSize.width + 1000, y);
|
||||
ctx.stroke();
|
||||
}
|
||||
} else if (d.type === 'vertical_line') {
|
||||
const x = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.time));
|
||||
const x = this.timeToX(d.time);
|
||||
if (x !== null) {
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x, 0);
|
||||
ctx.lineTo(x, scope.mediaSize.height);
|
||||
ctx.moveTo(x, -1000);
|
||||
ctx.lineTo(x, scope.mediaSize.height + 1000);
|
||||
ctx.stroke();
|
||||
}
|
||||
} else if (d.type === 'fib_retracement') {
|
||||
const x1 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p1.time));
|
||||
const x1 = this.timeToX(d.p1.time);
|
||||
const y1 = series.priceToCoordinate(d.p1.price);
|
||||
const x2 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p2.time));
|
||||
const x2 = this.timeToX(d.p2.time);
|
||||
const y2 = series.priceToCoordinate(d.p2.price);
|
||||
if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
|
||||
const levels = [0, 0.236, 0.382, 0.5, 0.618, 0.786, 1];
|
||||
@ -575,7 +698,7 @@ export class DrawingManager {
|
||||
}
|
||||
}
|
||||
} else if (d.type === 'arrow') {
|
||||
const x = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.time));
|
||||
const x = this.timeToX(d.time);
|
||||
const y = series.priceToCoordinate(d.price);
|
||||
if (x !== null && y !== null) {
|
||||
ctx.fillStyle = d.color; ctx.beginPath();
|
||||
@ -593,7 +716,7 @@ export class DrawingManager {
|
||||
if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); }
|
||||
}
|
||||
} else if (d.type === 'text') {
|
||||
const x = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.time));
|
||||
const x = this.timeToX(d.time);
|
||||
const y = series.priceToCoordinate(d.price);
|
||||
if (x !== null && y !== null) {
|
||||
ctx.fillStyle = d.color;
|
||||
@ -606,9 +729,9 @@ export class DrawingManager {
|
||||
}
|
||||
}
|
||||
} else if (d.type === 'measure') {
|
||||
const x1 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p1.time));
|
||||
const x1 = this.timeToX(d.p1.time);
|
||||
const y1 = series.priceToCoordinate(d.p1.price);
|
||||
const x2 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p2.time));
|
||||
const x2 = this.timeToX(d.p2.time);
|
||||
const y2 = series.priceToCoordinate(d.p2.price);
|
||||
|
||||
if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
|
||||
@ -675,8 +798,8 @@ export class DrawingManager {
|
||||
}
|
||||
} else {
|
||||
// Rough estimate if not in current data
|
||||
const intervalSeconds = 60; // Default fallback
|
||||
barCount = Math.round((d.p2.time - d.p1.time) / intervalSeconds);
|
||||
const intervalSec = this.getIntervalSeconds();
|
||||
barCount = Math.round((d.p2.time - d.p1.time) / intervalSec);
|
||||
}
|
||||
|
||||
// Time duration
|
||||
@ -708,7 +831,6 @@ export class DrawingManager {
|
||||
|
||||
// Natural position: Centered horizontally at midpoint,
|
||||
// vertically offset from the top/bottom edge.
|
||||
// This makes the label follow the box edges correctly during resize.
|
||||
const midX = (x1 + x2) / 2;
|
||||
const topY = Math.min(y1, y2);
|
||||
const bottomY = Math.max(y1, y2);
|
||||
|
||||
Reference in New Issue
Block a user