diff --git a/js/ui/drawing-tools.js b/js/ui/drawing-tools.js index ebcc264..cb29727 100644 --- a/js/ui/drawing-tools.js +++ b/js/ui/drawing-tools.js @@ -29,9 +29,25 @@ export class DrawingManager { init() { const container = this.container; + // Mouse Events container.addEventListener('mousedown', (e) => this.handleMouseDown(e)); window.addEventListener('mousemove', (e) => this.handleMouseMove(e)); window.addEventListener('mouseup', (e) => this.handleMouseUp(e)); + + // Touch Events + container.addEventListener('touchstart', (e) => { + if (this.activeTool || this.selectedDrawing) e.preventDefault(); + this.handleMouseDown(this.touchToMouseEvent(e)); + }, { passive: false }); + + window.addEventListener('touchmove', (e) => { + if (this.activeTool || (this.selectedDrawing && this.isMouseDown)) e.preventDefault(); + this.handleMouseMove(this.touchToMouseEvent(e)); + }, { passive: false }); + + window.addEventListener('touchend', (e) => { + this.handleMouseUp(); + }); window.addEventListener('keydown', (e) => { if ((e.key === 'Delete' || e.key === 'Backspace') && this.selectedDrawing) { @@ -114,6 +130,16 @@ export class DrawingManager { return views; } + touchToMouseEvent(e) { + const touch = e.touches[0] || e.changedTouches[0]; + return { + clientX: touch.clientX, + clientY: touch.clientY, + shiftKey: e.shiftKey, + preventDefault: () => e.preventDefault() + }; + } + updateChartInteractions() { const isInteracting = this.activeTool !== null || this.currentDrawing !== null || (this.selectedDrawing !== null && this.isMouseDown); @@ -377,6 +403,32 @@ export class DrawingManager { if (this.requestUpdate) this.requestUpdate(); } + snapToNearestTime(time) { + if (time === null) return null; + const candleData = this.dashboard.allData.get(this.dashboard.currentInterval) || []; + if (candleData.length === 0) return time; + + // Find nearest timestamp in current data + // Since data is sorted, we can be efficient, but for now a simple find is safest + let nearest = candleData[0].time; + let minDiff = Math.abs(time - nearest); + + // Optimization: check if it's already a match + if (candleData.find(c => c.time === time)) return time; + + for (let i = 1; i < candleData.length; i++) { + const diff = Math.abs(time - candleData[i].time); + if (diff < minDiff) { + minDiff = diff; + nearest = candleData[i].time; + } else if (diff > minDiff) { + // Since it's sorted, once diff starts increasing, we've found the closest + break; + } + } + return nearest; + } + render(target) { target.useMediaCoordinateSpace((scope) => { const ctx = scope.context; @@ -384,18 +436,18 @@ export class DrawingManager { const series = this.series; const allDrawings = [...this.drawings]; if (this.currentDrawing) allDrawings.push(this.currentDrawing); - + allDrawings.forEach(d => { const isSelected = d === this.selectedDrawing; ctx.save(); ctx.strokeStyle = d.color; ctx.lineWidth = isSelected ? 3 : 2; if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; } - + if (d.type === 'trend_line' || d.type === 'ray') { - const x1 = chart.timeScale().timeToCoordinate(d.p1.time); + const x1 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p1.time)); const y1 = series.priceToCoordinate(d.p1.price); - const x2 = chart.timeScale().timeToCoordinate(d.p2.time); + const x2 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p2.time)); const y2 = series.priceToCoordinate(d.p2.price); if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) { ctx.beginPath(); @@ -414,9 +466,9 @@ export class DrawingManager { } } } else if (d.type === 'rectangle') { - const x1 = chart.timeScale().timeToCoordinate(d.p1.time); + const x1 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p1.time)); const y1 = series.priceToCoordinate(d.p1.price); - const x2 = chart.timeScale().timeToCoordinate(d.p2.time); + const x2 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p2.time)); const y2 = series.priceToCoordinate(d.p2.price); if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) { ctx.fillStyle = d.color + '22'; @@ -431,17 +483,23 @@ export class DrawingManager { } else if (d.type === 'horizontal_line') { const y = series.priceToCoordinate(d.price); if (y !== null) { - ctx.beginPath(); ctx.moveTo(0, y); ctx.lineTo(scope.mediaSize.width, y); ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(0, y); + ctx.lineTo(scope.mediaSize.width, y); + ctx.stroke(); } } else if (d.type === 'vertical_line') { - const x = chart.timeScale().timeToCoordinate(d.time); + const x = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.time)); if (x !== null) { - ctx.beginPath(); ctx.moveTo(x, 0); ctx.lineTo(x, scope.mediaSize.height); ctx.stroke(); + ctx.beginPath(); + ctx.moveTo(x, 0); + ctx.lineTo(x, scope.mediaSize.height); + ctx.stroke(); } } else if (d.type === 'fib_retracement') { - const x1 = chart.timeScale().timeToCoordinate(d.p1.time); + const x1 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p1.time)); const y1 = series.priceToCoordinate(d.p1.price); - const x2 = chart.timeScale().timeToCoordinate(d.p2.time); + const x2 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(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]; @@ -465,7 +523,7 @@ export class DrawingManager { } } } else if (d.type === 'arrow') { - const x = chart.timeScale().timeToCoordinate(d.time); + const x = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.time)); const y = series.priceToCoordinate(d.price); if (x !== null && y !== null) { ctx.fillStyle = d.color; ctx.beginPath(); @@ -483,7 +541,7 @@ export class DrawingManager { if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); } } } else if (d.type === 'text') { - const x = chart.timeScale().timeToCoordinate(d.time); + const x = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.time)); const y = series.priceToCoordinate(d.price); if (x !== null && y !== null) { ctx.fillStyle = d.color; @@ -496,12 +554,13 @@ export class DrawingManager { } } } else if (d.type === 'measure') { - const x1 = chart.timeScale().timeToCoordinate(d.p1.time); + const x1 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p1.time)); const y1 = series.priceToCoordinate(d.p1.price); - const x2 = chart.timeScale().timeToCoordinate(d.p2.time); + const x2 = chart.timeScale().timeToCoordinate(this.snapToNearestTime(d.p2.time)); const y2 = series.priceToCoordinate(d.p2.price); - + if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) { + const priceDiff = d.p2.price - d.p1.price; const percentChange = (priceDiff / d.p1.price) * 100; const isUp = priceDiff >= 0; @@ -551,8 +610,8 @@ export class DrawingManager { // 3. Data Calculation // Bar count const candleData = this.dashboard.allData.get(this.dashboard.currentInterval) || []; - const startIdx = candleData.findIndex(c => c.time === d.p1.time); - const endIdx = candleData.findIndex(c => c.time === d.p2.time); + const startIdx = candleData.findIndex(c => c.time === this.snapToNearestTime(d.p1.time)); + const endIdx = candleData.findIndex(c => c.time === this.snapToNearestTime(d.p2.time)); let barCount = 0; let totalVol = 0; if (startIdx !== -1 && endIdx !== -1) { @@ -564,7 +623,8 @@ export class DrawingManager { } } else { // Rough estimate if not in current data - barCount = Math.round((d.p2.time - d.p1.time) / 60); // Assuming 1m? Better use current interval + const intervalSeconds = 60; // Default fallback + barCount = Math.round((d.p2.time - d.p1.time) / intervalSeconds); } // Time duration