diff --git a/index.html b/index.html index 457fb97..9773739 100644 --- a/index.html +++ b/index.html @@ -105,9 +105,6 @@ - - - diff --git a/js/ui/chart.js b/js/ui/chart.js index a765f77..1696605 100644 --- a/js/ui/chart.js +++ b/js/ui/chart.js @@ -457,11 +457,8 @@ export class TradingDashboard { this.initNavigationControls(); // Initialize Drawing Manager - const drawingLayer = document.getElementById('drawingLayer'); - if (drawingLayer) { - this.drawingManager = new DrawingManager(this.chart, this.candleSeries, drawingLayer, chartContainer); - window.activateDrawingTool = (tool) => this.drawingManager.setTool(tool); - } + this.drawingManager = new DrawingManager(this, chartContainer); + window.activateDrawingTool = (tool) => this.drawingManager.setTool(tool); // Setup price format selector change handler document.addEventListener("DOMContentLoaded", () => { diff --git a/js/ui/drawing-tools.js b/js/ui/drawing-tools.js index 46c2c23..8aa684b 100644 --- a/js/ui/drawing-tools.js +++ b/js/ui/drawing-tools.js @@ -4,12 +4,11 @@ */ export class DrawingManager { - constructor(chart, series, canvas, container) { - this.chart = chart; - this.series = series; - this.canvas = canvas; + constructor(dashboard, container) { + this.dashboard = dashboard; + this.chart = dashboard.chart; + this.series = dashboard.candleSeries; this.container = container; - this.ctx = canvas.getContext('2d'); this.drawings = []; this.activeTool = null; this.currentDrawing = null; @@ -61,8 +60,6 @@ export class DrawingManager { } updateChartInteractions() { - // Disable chart interactions if a tool is active, if we are currently drawing, - // or if we are dragging an existing object. const isInteracting = this.activeTool !== null || this.currentDrawing !== null || (this.selectedDrawing !== null && this.isMouseDown); this.chart.applyOptions({ @@ -156,14 +153,13 @@ export class DrawingManager { this.selectedDrawing = hit.drawing; this.dragMode = hit.part; - // Store initial state for offset movement const d = this.selectedDrawing; if (d.p1) this.startP1 = { ...d.p1 }; if (d.p2) this.startP2 = { ...d.p2 }; if (d.price !== undefined) this.startPrice = d.price; if (d.time !== undefined) this.startTime = d.time; - this.updateChartInteractions(); // Freeze chart for dragging + this.updateChartInteractions(); this.update(); return; } else { @@ -191,8 +187,6 @@ export class DrawingManager { this.setTool(null); } else if (this.activeTool === 'fib_retracement') { this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' }; - } else if (this.activeTool === 'measure') { - this.currentDrawing = { type: 'measure', p1, p2, color }; } else if (this.activeTool === 'arrow_up' || this.activeTool === 'arrow_down') { this.drawings.push({ type: 'arrow', time: pos.time, price: pos.price, @@ -215,7 +209,6 @@ export class DrawingManager { this.container.style.cursor = hit ? 'pointer' : 'default'; } - // Track measurement even if mouse is up (Click-Move-Click) if (this.currentDrawing && this.currentDrawing.type === 'measure') { if (pos.time !== null) { this.currentDrawing.p2 = { time: pos.time, price: pos.price }; @@ -265,7 +258,6 @@ export class DrawingManager { } handleMouseUp() { - // Special case for measure tool (Click-Move-Click) if (this.currentDrawing && this.currentDrawing.type === 'measure') { this.isMouseDown = false; this.updateChartInteractions(); @@ -280,7 +272,7 @@ export class DrawingManager { } this.dragMode = null; this.isMouseDown = false; - this.updateChartInteractions(); // Ensure interactions are restored (panning/zooming) + this.updateChartInteractions(); this.update(); } @@ -297,7 +289,7 @@ export class DrawingManager { if (Math.hypot(x - x1, y - y1) < threshold) return { drawing: d, part: 'p1' }; if (Math.hypot(x - x2, y - y2) < threshold) return { drawing: d, part: 'p2' }; - if (d.type === 'rectangle') { + if (d.type === 'rectangle' || d.type === 'measure') { if (x >= Math.min(x1, x2) && x <= Math.max(x1, x2) && y >= Math.min(y1, y2) && y <= Math.max(y1, y2)) return { drawing: d, part: 'all' }; } else { const dist = this.distToSegment({ x, y }, { x: x1, y: y1 }, { x: x2, y: y2 }); @@ -424,9 +416,13 @@ export class DrawingManager { ctx.fillStyle = d.color; ctx.beginPath(); const size = isSelected ? 15 : 10; if (d.direction === 'up') { - ctx.moveTo(x, y); ctx.lineTo(x - size/2, y + size); ctx.lineTo(x + size/2, y + size); + ctx.moveTo(x, y); + ctx.lineTo(x - size/2, y + size); + ctx.lineTo(x + size/2, y + size); } else { - ctx.moveTo(x, y); ctx.lineTo(x - size/2, y - size); ctx.lineTo(x + size/2, y - size); + ctx.moveTo(x, y); + ctx.lineTo(x - size/2, y - size); + ctx.lineTo(x + size/2, y - size); } ctx.fill(); if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); } @@ -449,30 +445,110 @@ export class DrawingManager { const y1 = series.priceToCoordinate(d.p1.price); const x2 = chart.timeScale().timeToCoordinate(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 isPositive = priceDiff >= 0; - const measureColor = isPositive ? '#26a69a' : '#ef5350'; - const timeSpanSeconds = Math.abs(d.p2.time - d.p1.time); - const days = (timeSpanSeconds / 86400).toFixed(1); - ctx.fillStyle = measureColor + '22'; + const isUp = priceDiff >= 0; + const color = isUp ? '#2962ff' : '#ef5350'; + + // 1. Draw Measurement Area + ctx.fillStyle = color + '33'; ctx.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1)); - ctx.strokeStyle = measureColor; - ctx.lineWidth = isSelected ? 2 : 1; - ctx.strokeRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1)); - ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); - const text = [`${priceDiff.toFixed(2)} (${percentChange.toFixed(2)}%)`, `${days} days`]; - const padding = 5, lineHeight = 14, boxWidth = 100, boxHeight = text.length * lineHeight + padding * 2; - const boxX = x2 + 10, boxY = y2 - boxHeight / 2; - ctx.fillStyle = '#1a2333'; ctx.fillRect(boxX, boxY, boxWidth, boxHeight); - ctx.strokeStyle = measureColor; ctx.strokeRect(boxX, boxY, boxWidth, boxHeight); - ctx.fillStyle = '#ffffff'; ctx.font = '11px Inter'; - text.forEach((line, i) => { ctx.fillText(line, boxX + padding, boxY + padding + 10 + (i * lineHeight)); }); + + // 2. Draw Crosshairs & Arrows + ctx.strokeStyle = color; + ctx.lineWidth = 1; + + // Horizontal line + ctx.beginPath(); + ctx.moveTo(Math.min(x1, x2), (y1 + y2) / 2); + ctx.lineTo(Math.max(x1, x2), (y1 + y2) / 2); + ctx.stroke(); + + // Vertical line + ctx.beginPath(); + ctx.moveTo((x1 + x2) / 2, Math.min(y1, y2)); + ctx.lineTo((x1 + x2) / 2, Math.max(y1, y2)); + ctx.stroke(); + + // Draw Arrows at ends of crosshairs + const drawArrow = (x, y, angle) => { + ctx.save(); + ctx.translate(x, y); + ctx.rotate(angle); + ctx.beginPath(); + ctx.moveTo(-6, -4); + ctx.lineTo(0, 0); + ctx.lineTo(-6, 4); + ctx.stroke(); + ctx.restore(); + }; + + // Time arrow (horizontal) + if (x2 > x1) drawArrow(x2, (y1 + y2) / 2, 0); + else drawArrow(x2, (y1 + y2) / 2, Math.PI); + + // Price arrow (vertical) + if (y2 < y1) drawArrow((x1 + x2) / 2, y2, -Math.PI/2); + else drawArrow((x1 + x2) / 2, y2, Math.PI/2); + + // 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); + let barCount = 0; + let totalVol = 0; + if (startIdx !== -1 && endIdx !== -1) { + barCount = endIdx - startIdx; + const minIdx = Math.min(startIdx, endIdx); + const maxIdx = Math.max(startIdx, endIdx); + for (let i = minIdx; i <= maxIdx; i++) { + totalVol += candleData[i].volume || 0; + } + } else { + // Rough estimate if not in current data + barCount = Math.round((d.p2.time - d.p1.time) / 60); // Assuming 1m? Better use current interval + } + + // Time duration + const diffSec = Math.abs(d.p2.time - d.p1.time); + const h = Math.floor(diffSec / 3600); + const m = Math.floor((diffSec % 3600) / 60); + const timeStr = `${h}h${m > 0 ? ` ${m}m` : ''}`; + + const volStr = totalVol > 1000 ? `${(totalVol/1000).toFixed(3)}K` : totalVol.toFixed(0); + + // 4. Draw Label Box + const labelLines = [ + `${priceDiff.toFixed(2)} (${percentChange.toFixed(2)}%) , ${Math.round(priceDiff * 100)}`, + `${barCount} bars, ${timeStr}`, + `Vol ${volStr}` + ]; + + ctx.font = '500 12px Inter'; + const labelWidth = Math.max(...labelLines.map(l => ctx.measureText(l).width)) + 24; + const labelHeight = 65; + const labelX = x2 - labelWidth / 2; + const labelY = isUp ? y2 - labelHeight - 10 : y2 + 10; + + // Solid Box + ctx.fillStyle = color; + this.roundRect(ctx, labelX, labelY, labelWidth, labelHeight, 4); + ctx.fill(); + + // Text + ctx.fillStyle = '#ffffff'; + ctx.textAlign = 'center'; + labelLines.forEach((line, i) => { + ctx.fillText(line, labelX + labelWidth/2, labelY + 20 + (i * 18)); + }); + if (isSelected) { - ctx.fillStyle = '#ffffff'; - ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); - ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); + ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2; + ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.stroke(); + ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.stroke(); } } } @@ -480,4 +556,18 @@ export class DrawingManager { }); }); } + + roundRect(ctx, x, y, width, height, radius) { + ctx.beginPath(); + ctx.moveTo(x + radius, y); + ctx.lineTo(x + width - radius, y); + ctx.quadraticCurveTo(x + width, y, x + width, y + radius); + ctx.lineTo(x + width, y + height - radius); + ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height); + ctx.lineTo(x + radius, y + height); + ctx.quadraticCurveTo(x, y + height, x, y + height - radius); + ctx.lineTo(x, y + radius); + ctx.quadraticCurveTo(x, y, x + radius, y); + ctx.closePath(); + } }