style: refine measurement tool to match professional style and fix rendering bugs
This commit is contained in:
@ -105,9 +105,6 @@
|
||||
<section class="relative w-full bg-[#0d1421] h-[60vh] md:h-[70vh]" data-purpose="chart-container" id="chartWrapper">
|
||||
<div id="chart" class="w-full h-full"></div>
|
||||
|
||||
<!-- Drawing Layer Overlay -->
|
||||
<canvas id="drawingLayer" class="absolute inset-0 pointer-events-none z-20 w-full h-full"></canvas>
|
||||
|
||||
<!-- Vertical Drawing Toolbar (Left) -->
|
||||
<div class="absolute left-2 top-1/2 -translate-y-1/2 flex flex-col gap-1 z-30 bg-[#1a2333]/80 backdrop-blur border border-[#2d3a4f] p-1 rounded-md shadow-xl" id="drawingToolbar">
|
||||
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" onclick="window.activateDrawingTool('cursor')" title="Cursor">
|
||||
|
||||
@ -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", () => {
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user