Compare commits

..

4 Commits

2 changed files with 364 additions and 87 deletions

View File

@ -80,7 +80,7 @@
<div class="flex flex-1 pt-16 overflow-hidden"> <div class="flex flex-1 pt-16 overflow-hidden">
<!-- Main Content --> <!-- Main Content -->
<main class="flex-1 overflow-y-auto pb-20 md:pb-0 no-scrollbar"> <main class="flex-1 flex flex-col overflow-y-auto pb-20 md:pb-0 no-scrollbar">
<!-- Price Statistics --> <!-- Price Statistics -->
<section id="priceHeader" class="grid grid-cols-2 md:grid-cols-4 gap-2 px-4 py-4 border-b border-[#1e293b] bg-[#0d1421]"> <section id="priceHeader" class="grid grid-cols-2 md:grid-cols-4 gap-2 px-4 py-4 border-b border-[#1e293b] bg-[#0d1421]">
<div> <div>
@ -102,33 +102,42 @@
</section> </section>
<!-- Chart Container --> <!-- Chart Container -->
<section class="relative w-full bg-[#0d1421] h-[60vh] md:h-[70vh]" data-purpose="chart-container" id="chartWrapper"> <section class="relative w-full bg-[#0d1421] flex-1 min-h-[50vh]" data-purpose="chart-container" id="chartWrapper">
<div id="chart" class="w-full h-full"></div> <div id="chart" class="w-full h-full"></div>
<!-- Horizontal Drawing Toolbar (Top) --> <!-- Horizontal Drawing Toolbar (Top) -->
<div class="absolute top-2 left-1/2 -translate-x-1/2 flex flex-row gap-1 z-30 bg-[#1a2333]/80 backdrop-blur border border-[#2d3a4f] p-1 rounded-md shadow-xl" id="drawingToolbar"> <div class="absolute top-2 left-1/2 -translate-x-1/2 flex flex-row 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('trend_line', event)" title="Trend Line"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="trend_line" onclick="window.activateDrawingTool('trend_line', event)" title="Trend Line">
<span class="material-symbols-outlined text-sm">call_split</span> <span class="material-symbols-outlined text-sm">call_split</span>
</button> </button>
<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('horizontal_line', event)" title="Horizontal Line"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="horizontal_line" onclick="window.activateDrawingTool('horizontal_line', event)" title="Horizontal Line">
<span class="material-symbols-outlined text-sm">swap_horizontal_circle</span> <span class="material-symbols-outlined text-sm">swap_horizontal_circle</span>
</button> </button>
<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('vertical_line', event)" title="Vertical Line"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="vertical_line" onclick="window.activateDrawingTool('vertical_line', event)" title="Vertical Line">
<span class="material-symbols-outlined text-sm">swap_vert</span> <span class="material-symbols-outlined text-sm">swap_vert</span>
</button> </button>
<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('rectangle', event)" title="Rectangle"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="rectangle" onclick="window.activateDrawingTool('rectangle', event)" title="Rectangle">
<span class="material-symbols-outlined text-sm">crop_square</span> <span class="material-symbols-outlined text-sm">crop_square</span>
</button> </button>
<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('text', event)" title="Text Label"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="text" onclick="window.activateDrawingTool('text', event)" title="Text Label">
<span class="material-symbols-outlined text-sm">text_fields</span> <span class="material-symbols-outlined text-sm">text_fields</span>
</button> </button>
<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('arrow_up', event)" title="Arrow Up"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="arrow_pointer" onclick="window.activateDrawingTool('arrow_pointer', event)" title="Arrow Pointer">
<span class="material-symbols-outlined text-sm">call_made</span>
</button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="arrow_up" onclick="window.activateDrawingTool('arrow_up', event)" title="Arrow Up">
<span class="material-symbols-outlined text-sm">arrow_upward</span> <span class="material-symbols-outlined text-sm">arrow_upward</span>
</button> </button>
<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('arrow_down', event)" title="Arrow Down"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="arrow_down" onclick="window.activateDrawingTool('arrow_down', event)" title="Arrow Down">
<span class="material-symbols-outlined text-sm">arrow_downward</span> <span class="material-symbols-outlined text-sm">arrow_downward</span>
</button> </button>
<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('measure', event)" title="Measure"> <button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="arrow_left" onclick="window.activateDrawingTool('arrow_left', event)" title="Arrow Left">
<span class="material-symbols-outlined text-sm">arrow_back</span>
</button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="arrow_right" onclick="window.activateDrawingTool('arrow_right', event)" title="Arrow Right">
<span class="material-symbols-outlined text-sm">arrow_forward</span>
</button>
<button class="w-8 h-8 text-gray-400 hover:text-white hover:bg-[#2d3a4f] rounded flex items-center justify-center transition-colors" data-tool="measure" onclick="window.activateDrawingTool('measure', event)" title="Measure">
<span class="material-symbols-outlined text-sm">straighten</span> <span class="material-symbols-outlined text-sm">straighten</span>
</button> </button>
<div class="w-[1px] h-full bg-[#2d3a4f] mx-0.5"></div> <div class="w-[1px] h-full bg-[#2d3a4f] mx-0.5"></div>
@ -314,8 +323,13 @@
</div> </div>
</section> </section>
<!-- Draggable Divider -->
<div id="mainChartResizer" class="h-1.5 bg-[#1e293b] hover:bg-blue-500 cursor-row-resize shrink-0 transition-colors z-20 flex items-center justify-center group relative">
<div class="w-10 h-0.5 bg-gray-500 rounded opacity-0 group-hover:opacity-100 transition-opacity"></div>
</div>
<!-- Technical Analysis Section --> <!-- Technical Analysis Section -->
<section class="px-4 py-6 bg-[#0d1421] min-h-[300px]" id="taPanel"> <section class="px-4 py-4 bg-[#0d1421] shrink-0" id="taPanel">
<div class="flex items-center justify-between mb-4"> <div class="flex items-center justify-between mb-4">
<h2 class="text-lg font-bold flex items-center gap-2"> <h2 class="text-lg font-bold flex items-center gap-2">
<span class="material-symbols-outlined text-[#b6c4ff]">analytics</span> <span class="material-symbols-outlined text-[#b6c4ff]">analytics</span>
@ -488,6 +502,56 @@
} }
} }
}; };
// Chart Resizer Logic
document.addEventListener('DOMContentLoaded', () => {
const resizer = document.getElementById('mainChartResizer');
const chartWrapper = document.getElementById('chartWrapper');
let isResizing = false;
let lastDownY = 0;
if (resizer && chartWrapper) {
resizer.addEventListener('mousedown', (e) => {
isResizing = true;
lastDownY = e.clientY;
document.body.style.cursor = 'row-resize';
chartWrapper.style.pointerEvents = 'none'; // Prevent iframe/canvas capturing mouse
});
document.addEventListener('mousemove', (e) => {
if (!isResizing) return;
const deltaY = e.clientY - lastDownY;
lastDownY = e.clientY;
const newHeight = chartWrapper.offsetHeight + deltaY;
// Min 200px, Max windowHeight - 150px
if (newHeight > 200 && newHeight < window.innerHeight - 150) {
chartWrapper.style.flex = 'none';
chartWrapper.style.height = newHeight + 'px';
// Force chart layout recalculation if applicable
if (window.dashboard && window.dashboard.chart) {
const container = document.getElementById('chart');
window.dashboard.chart.applyOptions({
width: container.clientWidth,
height: container.clientHeight
});
}
}
});
document.addEventListener('mouseup', () => {
if (isResizing) {
isResizing = false;
document.body.style.cursor = '';
chartWrapper.style.pointerEvents = 'auto';
}
});
}
});
</script> </script>
<script src="./config.js"></script> <script src="./config.js"></script>

View File

@ -56,6 +56,19 @@ export class DrawingManager {
italic: false, italic: false,
alignVert: 'top', alignVert: 'top',
alignHorz: 'left' alignHorz: 'left'
},
arrow: {
color: '#2962ff',
width: 2,
style: 0,
opacity: 100,
text: '',
textColor: '#2962ff',
fontSize: 14,
bold: false,
italic: false,
alignVert: 'top',
alignHorz: 'left'
} }
}; };
@ -81,7 +94,7 @@ export class DrawingManager {
if (hit) { if (hit) {
this.selectedDrawing = hit.drawing; this.selectedDrawing = hit.drawing;
// Only open panel for supported types // Only open panel for supported types
if (['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(hit.drawing.type)) { if (['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(hit.drawing.type)) {
this.toggleSettingsPanel(true); this.toggleSettingsPanel(true);
} }
this.update(); this.update();
@ -229,7 +242,18 @@ export class DrawingManager {
this.activeTool = tool; this.activeTool = tool;
if (tool === 'cursor') { // Update UI Button states
document.querySelectorAll('#drawingToolbar button[data-tool]').forEach(btn => {
if (btn.dataset.tool === tool) {
btn.classList.add('bg-blue-600/30', 'text-blue-400');
btn.classList.remove('text-gray-400', 'hover:bg-[#2d3a4f]');
} else {
btn.classList.remove('bg-blue-600/30', 'text-blue-400');
btn.classList.add('text-gray-400', 'hover:bg-[#2d3a4f]');
}
});
if (tool === 'cursor' || !tool) {
this.activeTool = null; this.activeTool = null;
document.body.style.cursor = 'default'; document.body.style.cursor = 'default';
} else if (tool === 'clear') { } else if (tool === 'clear') {
@ -237,6 +261,8 @@ export class DrawingManager {
this.selectedDrawing = null; this.selectedDrawing = null;
this.activeTool = null; this.activeTool = null;
this.update(); this.update();
// Instantly clear the 'clear' button state
setTimeout(() => this.setTool(null), 100);
} else { } else {
document.body.style.cursor = 'crosshair'; document.body.style.cursor = 'crosshair';
this.selectedDrawing = null; this.selectedDrawing = null;
@ -391,7 +417,14 @@ export class DrawingManager {
color: defs.color, color: defs.color,
width: defs.width, width: defs.width,
style: defs.style, style: defs.style,
opacity: defs.opacity opacity: defs.opacity,
text: defs.text,
textColor: defs.textColor,
fontSize: defs.fontSize,
bold: defs.bold,
italic: defs.italic,
alignVert: defs.alignVert,
alignHorz: defs.alignHorz
}); });
this.setTool(null); this.setTool(null);
} else if (this.activeTool === 'vertical_line') { } else if (this.activeTool === 'vertical_line') {
@ -402,16 +435,54 @@ export class DrawingManager {
color: defs.color, color: defs.color,
width: defs.width, width: defs.width,
style: defs.style, style: defs.style,
opacity: defs.opacity opacity: defs.opacity,
text: defs.text,
textColor: defs.textColor,
fontSize: defs.fontSize,
bold: defs.bold,
italic: defs.italic,
alignVert: defs.alignVert,
alignHorz: defs.alignHorz
}); });
this.setTool(null); this.setTool(null);
} else if (this.activeTool === 'fib_retracement') { } else if (this.activeTool === 'fib_retracement') {
this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' }; this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' };
} else if (this.activeTool === 'arrow_up' || this.activeTool === 'arrow_down') { } else if (this.activeTool === 'arrow_pointer') {
const defs = this.defaults.arrow;
this.currentDrawing = {
type: 'arrow_pointer', p1, p2,
color: defs.color,
width: defs.width,
style: defs.style,
opacity: defs.opacity,
text: defs.text,
textColor: defs.textColor,
fontSize: defs.fontSize,
bold: defs.bold,
italic: defs.italic,
alignVert: defs.alignVert,
alignHorz: defs.alignHorz
};
} else if (this.activeTool === 'arrow_up' || this.activeTool === 'arrow_down' || this.activeTool === 'arrow_left' || this.activeTool === 'arrow_right') {
let color = '#2962ff'; // Default blue for left/right
if (this.activeTool === 'arrow_up') color = '#26a69a'; // Green
if (this.activeTool === 'arrow_down') color = '#ef5350'; // Red
const defs = this.defaults.arrow;
this.drawings.push({ this.drawings.push({
type: 'arrow', time: pos.time, price: pos.price, type: 'arrow', time: pos.time, price: pos.price,
direction: this.activeTool === 'arrow_up' ? 'up' : 'down', direction: this.activeTool.replace('arrow_', ''),
color: this.activeTool === 'arrow_up' ? '#26a69a' : '#ef5350' color: color,
width: 2, // default thickness for solid arrows
style: defs.style,
opacity: defs.opacity,
text: defs.text,
textColor: defs.textColor,
fontSize: defs.fontSize,
bold: defs.bold,
italic: defs.italic,
alignVert: defs.alignVert,
alignHorz: defs.alignHorz
}); });
this.setTool(null); this.setTool(null);
} else if (this.activeTool === 'text') { } else if (this.activeTool === 'text') {
@ -759,21 +830,57 @@ export class DrawingManager {
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; } if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
if (d.type === 'trend_line' || d.type === 'ray') { if (d.type === 'trend_line' || d.type === 'ray' || d.type === 'arrow_pointer') {
const x1 = this.timeToX(d.p1.time); const x1 = this.timeToX(d.p1.time);
const y1 = series.priceToCoordinate(d.p1.price); const y1 = series.priceToCoordinate(d.p1.price);
const x2 = this.timeToX(d.p2.time); const x2 = this.timeToX(d.p2.time);
const y2 = series.priceToCoordinate(d.p2.price); const y2 = series.priceToCoordinate(d.p2.price);
if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) { if (x1 !== null && y1 !== null && x2 !== null && y2 !== null) {
ctx.beginPath();
ctx.moveTo(x1, y1); if (d.type === 'arrow_pointer') {
if (d.type === 'ray') {
const angle = Math.atan2(y2 - y1, x2 - x1); const angle = Math.atan2(y2 - y1, x2 - x1);
ctx.lineTo(x1 + Math.cos(angle) * 10000, y1 + Math.sin(angle) * 10000); const dist = Math.hypot(x2 - x1, y2 - y1);
ctx.save();
ctx.translate(x1, y1);
ctx.rotate(angle);
// Fat arrow dimensions
const headLen = Math.min(dist * 0.5, 30 + (d.width || 2) * 5);
const headWidth = headLen * 1.2;
const tailStartW = (d.width || 2) * 2;
const tailEndW = headLen * 0.5;
const indent = headLen * 0.2; // Head indentation
ctx.beginPath();
// Top edge of tail
ctx.moveTo(0, -tailStartW/2);
ctx.lineTo(dist - headLen + indent, -tailEndW/2);
// Top edge of head
ctx.lineTo(dist - headLen, -headWidth/2);
// Tip
ctx.lineTo(dist, 0);
// Bottom edge of head
ctx.lineTo(dist - headLen, headWidth/2);
// Bottom edge of tail
ctx.lineTo(dist - headLen + indent, tailEndW/2);
ctx.lineTo(0, tailStartW/2);
ctx.closePath();
ctx.fillStyle = color;
ctx.fill();
ctx.restore();
} else { } else {
ctx.lineTo(x2, y2); ctx.beginPath();
ctx.moveTo(x1, y1);
if (d.type === 'ray') {
const angle = Math.atan2(y2 - y1, x2 - x1);
ctx.lineTo(x1 + Math.cos(angle) * 10000, y1 + Math.sin(angle) * 10000);
} else {
ctx.lineTo(x2, y2);
}
ctx.stroke();
} }
ctx.stroke();
// Render Text if present // Render Text if present
if (d.text) { if (d.text) {
@ -783,13 +890,27 @@ export class DrawingManager {
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : ''); const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
ctx.font = `${fontStyle}${fontSize}px Inter`; ctx.font = `${fontStyle}${fontSize}px Inter`;
ctx.fillStyle = d.textColor || color; ctx.fillStyle = d.textColor || color;
let startX = x1, startY = y1, endX = x2, endY = y2;
if (x1 > x2 || (x1 === x2 && y1 > y2)) {
startX = x2; startY = y2; endX = x1; endY = y1;
}
const angle = Math.atan2(endY - startY, endX - startX);
const length = Math.hypot(endX - startX, endY - startY);
ctx.translate(startX, startY);
ctx.rotate(angle);
ctx.textAlign = d.alignHorz || 'left'; ctx.textAlign = d.alignHorz || 'left';
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom'); ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
const tx = (x1 + x2) / 2; let tx = 0;
const ty = (y1 + y2) / 2; if (d.alignHorz === 'center') tx = length / 2;
else if (d.alignHorz === 'right') tx = length;
const offset = 10; const offset = 10;
const finalTy = d.alignVert === 'top' ? ty - offset : (d.alignVert === 'bottom' ? ty + offset : ty); const finalTy = d.alignVert === 'top' ? -offset : (d.alignVert === 'bottom' ? offset : 0);
ctx.fillText(d.text, tx, finalTy); ctx.fillText(d.text, tx, finalTy);
ctx.restore(); ctx.restore();
@ -797,9 +918,11 @@ export class DrawingManager {
if (isSelected) { if (isSelected) {
ctx.setLineDash([]); ctx.setLineDash([]);
ctx.fillStyle = '#ffffff'; ctx.fillStyle = d.type === 'arrow_pointer' ? 'transparent' : '#ffffff';
ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.strokeStyle = d.type === 'arrow_pointer' ? color : d.color;
ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.lineWidth = 2;
ctx.beginPath(); ctx.arc(x1, y1, 5, 0, Math.PI * 2); if (d.type !== 'arrow_pointer') ctx.fill(); ctx.stroke();
ctx.beginPath(); ctx.arc(x2, y2, 5, 0, Math.PI * 2); if (d.type !== 'arrow_pointer') ctx.fill(); ctx.stroke();
} }
} }
} else if (d.type === 'rectangle') { } else if (d.type === 'rectangle') {
@ -837,41 +960,46 @@ export class DrawingManager {
ctx.beginPath(); ctx.arc(0, y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.arc(0, y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
} }
// Store label position for hit detection (before rendering) // Render Text and store label position
let textX = scope.mediaSize.width / 2;
let textY = y;
let labelPos = null; let labelPos = null;
if (d.text) { if (d.text) {
ctx.save();
ctx.setLineDash([]);
const fontSize = d.fontSize || 14; const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : ''); const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
const font = `${fontStyle}${fontSize}px Inter`; const font = `${fontStyle}${fontSize}px Inter`;
ctx.font = font; ctx.font = font;
ctx.fillStyle = d.textColor || d.color;
const alignHorz = d.alignHorz || 'center';
ctx.textAlign = alignHorz;
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
let textX = scope.mediaSize.width / 2;
if (alignHorz === 'left') textX = 10;
else if (alignHorz === 'right') textX = scope.mediaSize.width - 10;
const offset = 10;
let textY = d.alignVert === 'top' ? y - offset : (d.alignVert === 'bottom' ? y + offset : y);
ctx.fillText(d.text, textX, textY);
const metrics = ctx.measureText(d.text); const metrics = ctx.measureText(d.text);
const labelWidth = metrics.width + 16; const labelWidth = metrics.width + 16;
const labelHeight = 24; const labelHeight = 24;
const defaultLabelX = scope.mediaSize.width / 2 - labelWidth / 2;
const offset = 10; let defaultLabelX = scope.mediaSize.width / 2 - labelWidth / 2;
if (alignHorz === 'left') defaultLabelX = 10;
else if (alignHorz === 'right') defaultLabelX = scope.mediaSize.width - labelWidth - 10;
const defaultLabelY = d.alignVert === 'top' ? y - 40 : (d.alignVert === 'bottom' ? y + 20 : y - 12); const defaultLabelY = d.alignVert === 'top' ? y - 40 : (d.alignVert === 'bottom' ? y + 20 : y - 12);
labelPos = { labelPos = {
x: defaultLabelX + (d.labelOffset?.x || 0), x: defaultLabelX + (d.labelOffset?.x || 0),
y: defaultLabelY + (d.labelOffset?.y || 0), y: defaultLabelY + (d.labelOffset?.y || 0),
width: labelWidth, width: labelWidth,
height: labelHeight height: labelHeight
}; };
textX = scope.mediaSize.width / 2;
textY = d.alignVert === 'top' ? y - offset : (d.alignVert === 'bottom' ? y + offset : y);
}
// Render Text if present
if (d.text) {
ctx.save();
ctx.setLineDash([]);
ctx.font = font;
ctx.fillStyle = d.textColor || d.color;
ctx.textAlign = d.alignHorz || 'center';
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
ctx.fillText(d.text, textX, textY);
ctx.restore(); ctx.restore();
} }
@ -902,39 +1030,48 @@ export class DrawingManager {
ctx.beginPath(); ctx.arc(x, 0, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke(); ctx.beginPath(); ctx.arc(x, 0, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
} }
// Store label position for hit detection (before rendering) // Render Text and store label position
let textX = x;
let textY = 0;
let labelPos = null; let labelPos = null;
if (d.text) { if (d.text) {
ctx.save();
ctx.setLineDash([]);
const fontSize = d.fontSize || 14; const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : ''); const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
const font = `${fontStyle}${fontSize}px Inter`; const font = `${fontStyle}${fontSize}px Inter`;
ctx.font = font; ctx.font = font;
ctx.fillStyle = d.textColor || d.color;
const alignHorz = d.alignHorz || 'center';
ctx.textAlign = alignHorz;
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'bottom' : 'top');
let textX = x;
const offset = 10;
let textY = scope.mediaSize.height / 2; // Middle
if (d.alignVert === 'top') textY = offset;
else if (d.alignVert === 'bottom') textY = scope.mediaSize.height - offset;
ctx.fillText(d.text, textX, textY);
const metrics = ctx.measureText(d.text); const metrics = ctx.measureText(d.text);
const labelWidth = metrics.width + 16; const labelWidth = metrics.width + 16;
const labelHeight = 24; const labelHeight = 24;
const offset = 10;
const defaultLabelY = d.alignVert === 'top' ? -50 : (d.alignVert === 'bottom' ? 20 : -12); let defaultLabelY = scope.mediaSize.height / 2 - labelHeight / 2; // Middle
if (d.alignVert === 'top') defaultLabelY = offset;
else if (d.alignVert === 'bottom') defaultLabelY = scope.mediaSize.height - offset - labelHeight;
let defaultLabelX = x - labelWidth / 2;
if (alignHorz === 'left') defaultLabelX = x - labelWidth;
else if (alignHorz === 'right') defaultLabelX = x;
labelPos = { labelPos = {
x: x - labelWidth / 2 + (d.labelOffset?.x || 0), x: defaultLabelX + (d.labelOffset?.x || 0),
y: defaultLabelY + (d.labelOffset?.y || 0), y: defaultLabelY + (d.labelOffset?.y || 0),
width: labelWidth, width: labelWidth,
height: labelHeight height: labelHeight
}; };
textY = d.alignVert === 'top' ? -offset : (d.alignVert === 'bottom' ? offset : 0);
}
// Render Text if present
if (d.text) {
ctx.save();
ctx.setLineDash([]);
ctx.font = font;
ctx.fillStyle = d.textColor || d.color;
ctx.textAlign = d.alignHorz || 'center';
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
ctx.fillText(d.text, textX, textY);
ctx.restore(); ctx.restore();
} }
@ -975,19 +1112,89 @@ export class DrawingManager {
const x = this.timeToX(d.time); const x = this.timeToX(d.time);
const y = series.priceToCoordinate(d.price); const y = series.priceToCoordinate(d.price);
if (x !== null && y !== null) { if (x !== null && y !== null) {
ctx.fillStyle = d.color; ctx.beginPath(); ctx.fillStyle = d.color;
const size = isSelected ? 15 : 10; ctx.beginPath();
if (d.direction === 'up') { const baseSize = isSelected ? 20 : 15;
ctx.moveTo(x, y); const w = baseSize * 1.5; // width of arrow head
ctx.lineTo(x - size/2, y + size); const h = baseSize * 1.5; // height of arrow head
ctx.lineTo(x + size/2, y + size); const tailW = baseSize * 0.8; // width of tail
} else { const tailH = baseSize * 1.2; // length of tail
ctx.moveTo(x, y);
ctx.lineTo(x - size/2, y - size); ctx.save();
ctx.lineTo(x + size/2, y - size); ctx.translate(x, y);
}
if (d.direction === 'up') ctx.rotate(0);
else if (d.direction === 'right') ctx.rotate(Math.PI / 2);
else if (d.direction === 'down') ctx.rotate(Math.PI);
else if (d.direction === 'left') ctx.rotate(-Math.PI / 2);
// Draw a thick arrow pointing UP from the origin (x,y)
ctx.moveTo(0, -h/2 - tailH/2); // Tip of arrow
ctx.lineTo(w/2, h/2 - tailH/2); // Right corner of head
ctx.lineTo(tailW/2, h/2 - tailH/2); // Inner right corner
ctx.lineTo(tailW/2, h/2 + tailH/2); // Bottom right of tail
ctx.lineTo(-tailW/2, h/2 + tailH/2); // Bottom left of tail
ctx.lineTo(-tailW/2, h/2 - tailH/2); // Inner left corner
ctx.lineTo(-w/2, h/2 - tailH/2); // Left corner of head
ctx.closePath();
ctx.fill(); ctx.fill();
if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 1; ctx.stroke(); } if (isSelected) { ctx.strokeStyle = '#ffffff'; ctx.lineWidth = 2; ctx.stroke(); }
ctx.restore();
// Render Text and store label position
let labelPos = null;
if (d.text) {
ctx.save();
ctx.setLineDash([]);
const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
const font = `${fontStyle}${fontSize}px Inter`;
ctx.font = font;
ctx.fillStyle = d.textColor || d.color;
const alignHorz = d.alignHorz || 'center';
ctx.textAlign = alignHorz;
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'bottom' : 'top');
let textX = x;
let textY = y;
// Default offset logic for arrows if align isn't strictly overriding
const offset = 25;
if (d.direction === 'up') textY += offset;
else if (d.direction === 'down') textY -= offset;
else if (d.direction === 'left') textX += offset;
else if (d.direction === 'right') textX -= offset;
ctx.fillText(d.text, textX, textY);
const metrics = ctx.measureText(d.text);
const labelWidth = metrics.width + 16;
const labelHeight = 24;
let defaultLabelX = textX - labelWidth / 2;
if (alignHorz === 'left') defaultLabelX = textX - labelWidth;
else if (alignHorz === 'right') defaultLabelX = textX;
let defaultLabelY = textY;
if (d.alignVert === 'top') defaultLabelY = textY;
else if (d.alignVert === 'bottom') defaultLabelY = textY - labelHeight;
else defaultLabelY = textY - labelHeight / 2;
labelPos = {
x: defaultLabelX + (d.labelOffset?.x || 0),
y: defaultLabelY + (d.labelOffset?.y || 0),
width: labelWidth,
height: labelHeight
};
ctx.restore();
}
// Store label position for hit detection
if (labelPos) {
d.labelPos = labelPos;
}
} }
} else if (d.type === 'text') { } else if (d.type === 'text') {
const x = this.timeToX(d.time); const x = this.timeToX(d.time);
@ -1164,6 +1371,7 @@ export class DrawingManager {
let startX, startY, initialX, initialY; let startX, startY, initialX, initialY;
header.addEventListener('mousedown', (e) => { header.addEventListener('mousedown', (e) => {
if (e.target.closest('button')) return; // Allow clicking the close button
isDragging = true; isDragging = true;
startX = e.clientX; startX = e.clientX;
startY = e.clientY; startY = e.clientY;
@ -1197,7 +1405,7 @@ export class DrawingManager {
window.switchTLTab = (tab) => { window.switchTLTab = (tab) => {
this.activeTLTab = tab; this.activeTLTab = tab;
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type); const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type);
document.getElementById('tlTabStyle').className = tab === 'style' ? 'flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium' : 'flex-1 py-2 text-center text-gray-400 hover:text-white'; document.getElementById('tlTabStyle').className = tab === 'style' ? 'flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium' : 'flex-1 py-2 text-center text-gray-400 hover:text-white';
@ -1222,11 +1430,11 @@ export class DrawingManager {
window.setTLThickness = (width) => this.applySettings('width', width); window.setTLThickness = (width) => this.applySettings('width', width);
window.setTLStyle = (style) => this.applySettings('style', style); window.setTLStyle = (style) => this.applySettings('style', style);
window.toggleTLBold = () => { window.toggleTLBold = () => {
const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type) ? this.selectedDrawing.bold : this.defaults.trend_line.bold; const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type) ? this.selectedDrawing.bold : this.defaults.trend_line.bold;
this.applySettings('bold', !current); this.applySettings('bold', !current);
}; };
window.toggleTLItalic = () => { window.toggleTLItalic = () => {
const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type) ? this.selectedDrawing.italic : this.defaults.trend_line.italic; const current = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type) ? this.selectedDrawing.italic : this.defaults.trend_line.italic;
this.applySettings('italic', !current); this.applySettings('italic', !current);
}; };
@ -1287,6 +1495,10 @@ export class DrawingManager {
this.applySettings('text', e.target.value); this.applySettings('text', e.target.value);
}); });
document.getElementById('tlTextInput').addEventListener('keydown', (e) => {
e.stopPropagation();
});
document.getElementById('tlAlignVert').addEventListener('change', (e) => { document.getElementById('tlAlignVert').addEventListener('change', (e) => {
this.applySettings('alignVert', e.target.value); this.applySettings('alignVert', e.target.value);
}); });
@ -1344,7 +1556,7 @@ export class DrawingManager {
btn.setAttribute('data-active', parseInt(btn.dataset.style) === settings.style); btn.setAttribute('data-active', parseInt(btn.dataset.style) === settings.style);
}); });
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type); const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type);
const textColorBtn = document.getElementById('tlTextColorBtn'); const textColorBtn = document.getElementById('tlTextColorBtn');
const textPicker = document.getElementById('tlTextColorPicker'); const textPicker = document.getElementById('tlTextColorPicker');
@ -1358,7 +1570,6 @@ export class DrawingManager {
if (isLineWithText) { if (isLineWithText) {
if (textColorBtn && textPicker) { if (textColorBtn && textPicker) {
textColorBtn.style.backgroundColor = settings.textColor || settings.color; textColorBtn.style.backgroundColor = settings.textColor || settings.color;
textPicker.classList.remove('hidden');
} }
fontInput.value = settings.fontSize || 14; fontInput.value = settings.fontSize || 14;
textInput.value = settings.text || ''; textInput.value = settings.text || '';
@ -1374,7 +1585,7 @@ export class DrawingManager {
applySettings(key, value) { applySettings(key, value) {
if (this.selectedDrawing) { if (this.selectedDrawing) {
if (key === 'bold' || key === 'italic' || key === 'fontSize' || key === 'text' || key === 'textColor' || key === 'alignVert' || key === 'alignHorz') { if (key === 'bold' || key === 'italic' || key === 'fontSize' || key === 'text' || key === 'textColor' || key === 'alignVert' || key === 'alignHorz') {
const isLineWithText = ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type); const isLineWithText = ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line', 'arrow', 'arrow_pointer'].includes(this.selectedDrawing.type);
if (!isLineWithText) return; if (!isLineWithText) return;
this.selectedDrawing[key] = value; this.selectedDrawing[key] = value;
} else { } else {
@ -1394,6 +1605,8 @@ export class DrawingManager {
this.defaults.horizontal_line[key] = value; this.defaults.horizontal_line[key] = value;
} else if (drawingType === 'vertical_line') { } else if (drawingType === 'vertical_line') {
this.defaults.vertical_line[key] = value; this.defaults.vertical_line[key] = value;
} else if (drawingType === 'arrow' || drawingType === 'arrow_pointer') {
this.defaults.arrow[key] = value;
} else { } else {
this.defaults.trend_line[key] = value; this.defaults.trend_line[key] = value;
} }