Files
winterfail/js/ui/drawing-tools.js

1408 lines
63 KiB
JavaScript

/**
* Drawing Tools for Lightweight Charts
* Implements professional trading chart drawing primitives
*/
export class DrawingManager {
constructor(dashboard, container) {
this.dashboard = dashboard;
this.chart = dashboard.chart;
this.series = dashboard.candleSeries;
this.container = container;
this.drawings = [];
this.activeTool = null;
this.currentDrawing = null;
this.selectedDrawing = null;
this.dragMode = null;
this.isMouseDown = false;
// Default Settings
this.defaults = {
trend_line: {
color: '#2962ff',
width: 2,
style: 0, // 0: Solid, 1: Dashed, 2: Dotted
opacity: 100,
text: '',
textColor: '#2962ff',
fontSize: 14,
bold: false,
italic: false,
alignVert: 'top',
alignHorz: 'left'
},
horizontal_line: {
color: '#2962ff',
width: 2,
style: 0,
opacity: 100,
text: '',
textColor: '#2962ff',
fontSize: 14,
bold: false,
italic: false,
alignVert: 'top',
alignHorz: 'left'
},
vertical_line: {
color: '#2962ff',
width: 2,
style: 0,
opacity: 100,
text: '',
textColor: '#2962ff',
fontSize: 14,
bold: false,
italic: false,
alignVert: 'top',
alignHorz: 'left'
}
};
// Dragging offsets
this.dragStartPos = null;
this.startP1 = null;
this.startP2 = null;
this.startPrice = null;
this.startTime = null;
this.init();
this.initSettingsPanel();
}
init() {
const container = this.container;
// Mouse Events
container.addEventListener('mousedown', (e) => this.handleMouseDown(e));
container.addEventListener('dblclick', (e) => {
const pos = this.getMousePos(e);
const hit = this.findHit(pos.x, pos.y);
if (hit) {
this.selectedDrawing = hit.drawing;
// Only open panel for supported types
if (['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(hit.drawing.type)) {
this.toggleSettingsPanel(true);
}
this.update();
}
});
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) {
this.drawings = this.drawings.filter(d => d !== this.selectedDrawing);
this.selectedDrawing = null;
this.update();
}
});
this.drawingPrimitive = {
attached: (param) => {
this.requestUpdate = param.requestUpdate;
},
detached: () => {
this.requestUpdate = undefined;
},
updateAllViews: () => {},
paneViews: () => [{
renderer: () => ({
draw: (target) => this.render(target)
})
}],
priceAxisViews: () => this.getPriceAxisViews(),
timeAxisViews: () => this.getTimeAxisViews()
};
this.series.attachPrimitive(this.drawingPrimitive);
}
getPriceAxisViews() {
const views = [];
const drawingsToShow = [];
if (this.currentDrawing && this.currentDrawing.type === 'measure') drawingsToShow.push(this.currentDrawing);
if (this.selectedDrawing && this.selectedDrawing.type === 'measure') drawingsToShow.push(this.selectedDrawing);
drawingsToShow.forEach(d => {
const isUp = d.p2.price >= d.p1.price;
const color = isUp ? '#26a69a' : '#ef5350';
[d.p1.price, d.p2.price].forEach(price => {
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;
}
getTimeAxisViews() {
const views = [];
const drawingsToShow = [];
if (this.currentDrawing && this.currentDrawing.type === 'measure') drawingsToShow.push(this.currentDrawing);
if (this.selectedDrawing && this.selectedDrawing.type === 'measure') drawingsToShow.push(this.selectedDrawing);
drawingsToShow.forEach(d => {
const isUp = d.p2.price >= d.p1.price;
const color = isUp ? '#26a69a' : '#ef5350';
[d.p1.time, d.p2.time].forEach(time => {
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;
}
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);
this.chart.applyOptions({
handleScroll: {
mouseWheel: !isInteracting,
pressedMouseMove: !isInteracting,
horzTouchDrag: !isInteracting,
vertTouchDrag: !isInteracting,
},
handleScale: {
axisPressedMouseMove: !isInteracting,
mouseWheel: !isInteracting,
pinch: !isInteracting,
}
});
}
setTool(tool, event) {
// Shift + Click on trend_line button opens settings
if (tool === 'trend_line' && event && event.shiftKey) {
this.toggleSettingsPanel(true);
return;
}
this.activeTool = tool;
if (tool === 'cursor') {
this.activeTool = null;
document.body.style.cursor = 'default';
} else if (tool === 'clear') {
this.drawings = [];
this.selectedDrawing = null;
this.activeTool = null;
this.update();
} else {
document.body.style.cursor = 'crosshair';
this.selectedDrawing = null;
}
this.updateChartInteractions();
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 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 };
}
handleMouseDown(e) {
const pos = this.getMousePos(e);
this.isMouseDown = true;
this.dragStartPos = pos;
// Shift + Click shortcut for Measurement Tool
if (e.shiftKey && !this.activeTool) {
this.setTool('measure');
this.currentDrawing = {
type: 'measure',
p1: { time: pos.time, price: pos.price },
p2: { time: pos.time, price: pos.price },
color: '#26a69a'
};
this.update();
return;
}
// Click-Move-Click logic for Measurement Tool
if (this.activeTool === 'measure') {
if (this.currentDrawing) {
// 2nd Click: Finish
this.drawings.push(this.currentDrawing);
this.currentDrawing = null;
this.setTool(null);
this.isMouseDown = false;
this.update();
return;
} else {
// 1st Click: Start
this.currentDrawing = {
type: 'measure',
p1: { time: pos.time, price: pos.price },
p2: { time: pos.time, price: pos.price },
color: '#26a69a',
labelOffset: { x: 0, y: 0 }
};
this.update();
return;
}
}
if (!this.activeTool) {
const hit = this.findHit(pos.x, pos.y);
if (hit) {
this.selectedDrawing = hit.drawing;
this.dragMode = hit.part;
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();
this.update();
return;
} else {
this.selectedDrawing = null;
this.dragMode = null;
}
}
if (!this.activeTool || pos.time === null || pos.price === null) {
this.update();
return;
}
const color = this.defaults.trend_line.color;
const p1 = { time: pos.time, price: pos.price };
const p2 = { time: pos.time, price: pos.price };
const defs = this.defaults.trend_line;
if (this.activeTool === 'trend_line' || this.activeTool === 'ray' || this.activeTool === 'rectangle') {
this.currentDrawing = {
type: this.activeTool, 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 === 'horizontal_line') {
const defs = this.defaults.horizontal_line;
this.drawings.push({
type: 'horizontal_line',
price: pos.price,
color: defs.color,
width: defs.width,
style: defs.style,
opacity: defs.opacity
});
this.setTool(null);
} else if (this.activeTool === 'vertical_line') {
const defs = this.defaults.vertical_line;
this.drawings.push({
type: 'vertical_line',
time: pos.time,
color: defs.color,
width: defs.width,
style: defs.style,
opacity: defs.opacity
});
this.setTool(null);
} else if (this.activeTool === 'fib_retracement') {
this.currentDrawing = { type: 'fib_retracement', p1, p2, color: '#fb8c00' };
} else if (this.activeTool === 'arrow_up' || this.activeTool === 'arrow_down') {
this.drawings.push({
type: 'arrow', time: pos.time, price: pos.price,
direction: this.activeTool === 'arrow_up' ? 'up' : 'down',
color: this.activeTool === 'arrow_up' ? '#26a69a' : '#ef5350'
});
this.setTool(null);
} else if (this.activeTool === 'text') {
const text = prompt('Enter text:');
if (text) this.drawings.push({ type: 'text', time: pos.time, price: pos.price, text, color: '#ffffff' });
this.setTool(null);
}
this.update();
}
handleMouseMove(e) {
const pos = this.getMousePos(e);
// Hover cursor logic
if (!this.isMouseDown && !this.activeTool) {
const hit = this.findHit(pos.x, pos.y);
if (hit) {
this.container.style.cursor = hit.part === 'label' ? 'move' : 'pointer';
} else {
this.container.style.cursor = '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 };
this.update();
}
return;
}
if (!this.isMouseDown) return;
if (this.selectedDrawing && this.dragMode) {
if (pos.time === null || pos.price === null) return;
const d = this.selectedDrawing;
// Safety check for dragStartPos
if (!this.dragStartPos) {
this.dragStartPos = pos;
return;
}
if (this.dragMode === 'label') {
// Free movement of ONLY the label field
const dx = pos.x - this.dragStartPos.x;
const dy = pos.y - this.dragStartPos.y;
if (!d.labelOffset) d.labelOffset = { x: 0, y: 0 };
d.labelOffset.x += dx;
d.labelOffset.y += dy;
this.dragStartPos = pos;
this.update();
return; // Critical: exit here so we don't move measurement points
}
if (this.dragMode === 'label') {
const dx = pos.x - this.dragStartPos.x;
const dy = pos.y - this.dragStartPos.y;
if (!d.labelOffset) d.labelOffset = { x: 0, y: 0 };
d.labelOffset.x += dx;
d.labelOffset.y += dy;
this.dragStartPos = pos;
this.update();
return;
}
if (this.dragMode === 'p1') {
d.p1 = { time: pos.time, price: pos.price };
} else if (this.dragMode === 'p2') {
d.p2 = { time: pos.time, price: pos.price };
} else if (this.dragMode === 'all') {
let timeDiff = pos.time - this.dragStartPos.time;
const priceDiff = pos.price - this.dragStartPos.price;
// If timeDiff is NaN (e.g., dragged into whitespace on high TF), estimate it
if (isNaN(timeDiff) || pos.time === null) {
const timeScale = this.chart.timeScale();
const startLogical = timeScale.coordinateToLogical(this.dragStartPos.x);
const currentLogical = timeScale.coordinateToLogical(pos.x);
if (startLogical !== null && currentLogical !== null) {
const logicalDiff = currentLogical - startLogical;
timeDiff = logicalDiff * this.getIntervalSeconds();
}
}
if (!isNaN(timeDiff) && !isNaN(priceDiff)) {
if (d.p1 && d.p2) {
d.p1.time = this.startP1.time + timeDiff;
d.p1.price = this.startP1.price + priceDiff;
d.p2.time = this.startP2.time + timeDiff;
d.p2.price = this.startP2.price + priceDiff;
} else if (d.time !== undefined && d.price !== undefined) {
d.time = this.startTime + timeDiff;
d.price = this.startPrice + priceDiff;
} else if (d.price !== undefined) {
d.price = this.startPrice + priceDiff;
} else if (d.time !== undefined) {
d.time = this.startTime + timeDiff;
}
}
} else if (this.dragMode === 'price') {
d.price = pos.price;
} else if (this.dragMode === 'time') {
d.time = pos.time;
}
this.update();
return;
}
if (this.currentDrawing && pos.time !== null) {
this.currentDrawing.p2 = { time: pos.time, price: pos.price };
this.update();
}
}
handleMouseUp() {
if (this.currentDrawing && this.currentDrawing.type === 'measure') {
this.isMouseDown = false;
this.updateChartInteractions();
this.update();
return;
}
if (this.currentDrawing) {
this.drawings.push(this.currentDrawing);
this.currentDrawing = null;
this.setTool(null);
}
this.dragMode = null;
this.isMouseDown = false;
this.updateChartInteractions();
this.update();
}
findHit(x, y) {
const threshold = 10;
for (let i = this.drawings.length - 1; i >= 0; i--) {
const d = this.drawings[i];
// Check for label hit first (highest z-order)
if (d.type === 'measure' && d.labelPos) {
if (x >= d.labelPos.x && x <= d.labelPos.x + d.labelPos.width &&
y >= d.labelPos.y && y <= d.labelPos.y + d.labelPos.height) {
return { drawing: d, part: 'label' };
}
}
if (d.p1 && d.p2) {
const x1 = this.timeToX(d.p1.time);
const y1 = this.series.priceToCoordinate(d.p1.price);
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;
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' || 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 });
if (dist < threshold) return { drawing: d, part: 'all' };
}
} else if (d.price !== undefined && d.type === 'horizontal_line') {
const dy = this.series.priceToCoordinate(d.price);
if (dy !== null && Math.abs(y - dy) < threshold) {
if (d.text && d.labelPos) {
if (x >= d.labelPos.x && x <= d.labelPos.x + d.labelPos.width &&
y >= d.labelPos.y && y <= d.labelPos.y + d.labelPos.height) {
return { drawing: d, part: 'label' };
}
}
return { drawing: d, part: 'all' };
}
} else if (d.time !== undefined && d.type === 'vertical_line') {
const dx = this.timeToX(d.time);
if (dx !== null && Math.abs(x - dx) < threshold) {
if (d.text && d.labelPos) {
if (x >= d.labelPos.x && x <= d.labelPos.x + d.labelPos.width &&
y >= d.labelPos.y && y <= d.labelPos.y + d.labelPos.height) {
return { drawing: d, part: 'label' };
}
}
return { drawing: d, part: 'all' };
}
} else if (d.time !== undefined && d.price !== undefined) {
const dx = this.timeToX(d.time);
const dy = this.series.priceToCoordinate(d.price);
if (dx !== null && dy !== null && Math.hypot(x - dx, y - dy) < threshold) return { drawing: d, part: 'all' };
}
}
return null;
}
distToSegment(p, v, w) {
const l2 = Math.pow(v.x - w.x, 2) + Math.pow(v.y - w.y, 2);
if (l2 === 0) return Math.hypot(p.x - v.x, p.y - v.y);
let t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2;
t = Math.max(0, Math.min(1, t));
return Math.hypot(p.x - (v.x + t * (w.x - v.x)), p.y - (v.y + t * (w.y - v.y)));
}
update() {
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;
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
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;
}
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);
}
hexToRgba(hex, opacity) {
let r = 0, g = 0, b = 0;
if (hex.length === 4) {
r = parseInt(hex[1] + hex[1], 16);
g = parseInt(hex[2] + hex[2], 16);
b = parseInt(hex[3] + hex[3], 16);
} else if (hex.length === 7) {
r = parseInt(hex.slice(1, 3), 16);
g = parseInt(hex.slice(3, 5), 16);
b = parseInt(hex.slice(5, 7), 16);
}
return `rgba(${r}, ${g}, ${b}, ${opacity / 100})`;
}
render(target) {
target.useMediaCoordinateSpace((scope) => {
const ctx = scope.context;
const chart = this.chart;
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();
// Base Style
const color = d.opacity !== undefined ? this.hexToRgba(d.color, d.opacity) : d.color;
ctx.strokeStyle = color;
ctx.lineWidth = d.width || (isSelected ? 3 : 2);
if (d.style === 1) ctx.setLineDash([10, 5]); // Dashed
else if (d.style === 2) ctx.setLineDash([2, 2]); // Dotted
else ctx.setLineDash([]); // Solid
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
if (d.type === 'trend_line' || d.type === 'ray') {
const x1 = this.timeToX(d.p1.time);
const y1 = series.priceToCoordinate(d.p1.price);
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();
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();
// Render Text if present
if (d.text) {
ctx.save();
ctx.setLineDash([]); // Text should not be dashed
const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
ctx.font = `${fontStyle}${fontSize}px Inter`;
ctx.fillStyle = d.textColor || color;
ctx.textAlign = d.alignHorz || 'left';
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
const tx = (x1 + x2) / 2;
const ty = (y1 + y2) / 2;
const offset = 10;
const finalTy = d.alignVert === 'top' ? ty - offset : (d.alignVert === 'bottom' ? ty + offset : ty);
ctx.fillText(d.text, tx, finalTy);
ctx.restore();
}
if (isSelected) {
ctx.setLineDash([]);
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();
}
}
} else if (d.type === 'rectangle') {
const x1 = this.timeToX(d.p1.time);
const y1 = series.priceToCoordinate(d.p1.price);
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';
ctx.fillRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
ctx.strokeRect(Math.min(x1, x2), Math.min(y1, y2), Math.abs(x2 - x1), Math.abs(y2 - y1));
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();
}
}
} else if (d.type === 'horizontal_line') {
const y = series.priceToCoordinate(d.price);
if (y !== null) {
ctx.save();
ctx.strokeStyle = d.opacity !== undefined ? this.hexToRgba(d.color, d.opacity) : d.color;
ctx.lineWidth = d.width || 2;
if (d.style === 1) ctx.setLineDash([10, 5]);
else if (d.style === 2) ctx.setLineDash([2, 2]);
else ctx.setLineDash([]);
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
ctx.beginPath();
ctx.moveTo(-1000, y);
ctx.lineTo(scope.mediaSize.width + 1000, y);
ctx.stroke();
if (isSelected) {
ctx.setLineDash([]);
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(0, y, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
}
// Render Text if present
if (d.text) {
ctx.save();
ctx.setLineDash([]);
const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
ctx.font = `${fontStyle}${fontSize}px Inter`;
ctx.fillStyle = d.textColor || d.color;
const tx = scope.mediaSize.width / 2;
const ty = y;
const offset = 10;
const finalTy = d.alignVert === 'top' ? ty - offset : (d.alignVert === 'bottom' ? ty + offset : ty);
ctx.textAlign = d.alignHorz || 'center';
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
ctx.fillText(d.text, tx, finalTy);
ctx.restore();
}
// Store label position for hit detection
if (d.text) {
ctx.save();
ctx.setLineDash([]);
const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
ctx.font = `${fontStyle}${fontSize}px Inter`;
const metrics = ctx.measureText(d.text);
const labelWidth = metrics.width + 16;
const labelHeight = 24;
const defaultLabelX = scope.mediaSize.width / 2 - labelWidth / 2;
const defaultLabelY = d.alignVert === 'top' ? y - 40 : (d.alignVert === 'bottom' ? y + 20 : y - 12);
d.labelPos = {
x: defaultLabelX + (d.labelOffset?.x || 0),
y: defaultLabelY + (d.labelOffset?.y || 0),
width: labelWidth,
height: labelHeight
};
ctx.restore();
}
ctx.restore();
}
} else if (d.type === 'vertical_line') {
const x = this.timeToX(d.time);
if (x !== null) {
ctx.save();
ctx.strokeStyle = d.opacity !== undefined ? this.hexToRgba(d.color, d.opacity) : d.color;
ctx.lineWidth = d.width || 2;
if (d.style === 1) ctx.setLineDash([10, 5]);
else if (d.style === 2) ctx.setLineDash([2, 2]);
else ctx.setLineDash([]);
if (isSelected) { ctx.shadowBlur = 5; ctx.shadowColor = d.color; }
ctx.beginPath();
ctx.moveTo(x, -1000);
ctx.lineTo(x, scope.mediaSize.height + 1000);
ctx.stroke();
if (isSelected) {
ctx.setLineDash([]);
ctx.fillStyle = '#ffffff';
ctx.beginPath(); ctx.arc(x, 0, 5, 0, Math.PI * 2); ctx.fill(); ctx.stroke();
}
// Render Text if present
if (d.text) {
ctx.save();
ctx.setLineDash([]);
const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
ctx.font = `${fontStyle}${fontSize}px Inter`;
ctx.fillStyle = d.textColor || d.color;
const tx = x;
const ty = 0;
const offset = 10;
const finalTy = d.alignVert === 'top' ? ty - offset : (d.alignVert === 'bottom' ? ty + offset : ty);
ctx.textAlign = d.alignHorz || 'center';
ctx.textBaseline = d.alignVert === 'middle' ? 'middle' : (d.alignVert === 'bottom' ? 'top' : 'bottom');
ctx.fillText(d.text, tx, finalTy);
ctx.restore();
}
// Store label position for hit detection
if (d.text) {
ctx.save();
ctx.setLineDash([]);
const fontSize = d.fontSize || 14;
const fontStyle = (d.bold ? 'bold ' : '') + (d.italic ? 'italic ' : '');
ctx.font = `${fontStyle}${fontSize}px Inter`;
const metrics = ctx.measureText(d.text);
const labelWidth = metrics.width + 16;
const labelHeight = 24;
const defaultLabelX = x - labelWidth / 2;
const defaultLabelY = d.alignVert === 'top' ? -50 : (d.alignVert === 'bottom' ? 20 : -12);
d.labelPos = {
x: defaultLabelX + (d.labelOffset?.x || 0),
y: defaultLabelY + (d.labelOffset?.y || 0),
width: labelWidth,
height: labelHeight
};
ctx.restore();
}
ctx.restore();
}
} else if (d.type === 'fib_retracement') {
const x1 = this.timeToX(d.p1.time);
const y1 = series.priceToCoordinate(d.p1.price);
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];
const range = d.p2.price - d.p1.price;
ctx.setLineDash([5, 5]); ctx.lineWidth = 1;
levels.forEach(level => {
const price = d.p1.price + (range * level);
const y = series.priceToCoordinate(price);
if (y !== null) {
ctx.beginPath(); ctx.moveTo(Math.min(x1, x2), y); ctx.lineTo(Math.max(x1, x2), y); ctx.stroke();
ctx.fillStyle = d.color; ctx.font = '10px Inter';
ctx.fillText(`${(level * 100).toFixed(1)}% (${price.toFixed(2)})`, Math.max(x1, x2) + 5, y + 3);
}
});
ctx.setLineDash([]); ctx.strokeStyle = d.color + '44';
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
if (isSelected) {
ctx.fillStyle = '#ffffff'; ctx.strokeStyle = d.color; ctx.lineWidth = 2;
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();
}
}
} else if (d.type === 'arrow') {
const x = this.timeToX(d.time);
const y = series.priceToCoordinate(d.price);
if (x !== null && y !== null) {
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);
} else {
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(); }
}
} else if (d.type === 'text') {
const x = this.timeToX(d.time);
const y = series.priceToCoordinate(d.price);
if (x !== null && y !== null) {
ctx.fillStyle = d.color;
ctx.font = isSelected ? 'bold 14px Inter' : '12px Inter';
ctx.fillText(d.text, x, y);
if (isSelected) {
const metrics = ctx.measureText(d.text);
ctx.strokeStyle = d.color;
ctx.strokeRect(x - 2, y - 12, metrics.width + 4, 16);
}
}
} else if (d.type === 'measure') {
const x1 = this.timeToX(d.p1.time);
const y1 = series.priceToCoordinate(d.p1.price);
const x2 = this.timeToX(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;
const color = isUp ? '#26a69a' : '#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));
// 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 === 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) {
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
const intervalSec = this.getIntervalSeconds();
barCount = Math.round((d.p2.time - d.p1.time) / intervalSec);
}
// Time duration
const diffSec = Math.abs(d.p2.time - d.p1.time);
let timeStr = "";
if (diffSec >= 86400) {
const days = Math.floor(diffSec / 86400);
const hours = Math.floor((diffSec % 86400) / 3600);
const mins = Math.floor((diffSec % 3600) / 60);
timeStr = `${days}d${hours > 0 ? ` ${hours}h` : ''}${mins > 0 ? ` ${mins}m` : ''}`;
} else {
const hours = Math.floor(diffSec / 3600);
const mins = Math.floor((diffSec % 3600) / 60);
timeStr = `${hours}h${mins > 0 ? ` ${mins}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 midX = (x1 + x2) / 2;
const topY = Math.min(y1, y2);
const bottomY = Math.max(y1, y2);
const defaultLabelX = midX - labelWidth / 2;
const defaultLabelY = isUp ? topY - labelHeight - 10 : bottomY + 10;
const labelX = defaultLabelX + (d.labelOffset?.x || 0);
const labelY = defaultLabelY + (d.labelOffset?.y || 0);
// Store label position for hit detection
d.labelPos = { x: labelX, y: labelY, width: labelWidth, height: labelHeight };
// 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.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();
}
}
}
ctx.restore();
});
});
}
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();
}
initSettingsPanel() {
const panel = document.getElementById('trendlineSettingsPanel');
const header = document.getElementById('tlPanelHeader');
this.activeTLTab = 'style';
// Dragging Logic
let isDragging = false;
let startX, startY, initialX, initialY;
header.addEventListener('mousedown', (e) => {
isDragging = true;
startX = e.clientX;
startY = e.clientY;
const rect = panel.getBoundingClientRect();
initialX = rect.left;
initialY = rect.top;
panel.classList.remove('left-1/2', '-translate-x-1/2', 'top-14');
panel.style.left = initialX + 'px';
panel.style.top = initialY + 'px';
panel.style.margin = '0';
e.preventDefault();
});
window.addEventListener('mousemove', (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
panel.style.left = (initialX + dx) + 'px';
panel.style.top = (initialY + dy) + 'px';
});
window.addEventListener('mouseup', () => {
isDragging = false;
});
// Global functions for HTML event handlers
window.toggleTLSettings = (show) => this.toggleSettingsPanel(show);
window.switchTLTab = (tab) => {
this.activeTLTab = tab;
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].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';
const textTab = document.getElementById('tlTabText');
if (isLineWithText) {
textTab.className = tab === 'text' ? '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';
textTab.style.display = 'block';
} else {
textTab.style.display = 'none';
}
document.getElementById('tlContentStyle').className = tab === 'style' ? 'block' : 'hidden';
document.getElementById('tlContentText').className = tab === 'text' ? 'block' : 'hidden';
const textPicker = document.getElementById('tlTextColorPicker');
if (textPicker) textPicker.classList.add('hidden');
this.updateSettingsPanelUI();
};
window.setTLThickness = (width) => this.applySettings('width', width);
window.setTLStyle = (style) => this.applySettings('style', style);
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;
this.applySettings('bold', !current);
};
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;
this.applySettings('italic', !current);
};
// Initialize Color Grids
const colors = [
'#ffffff', '#e0e0e0', '#bdbdbd', '#9e9e9e', '#757575', '#616161', '#424242', '#212121', '#000000',
'#ef5350', '#ff9800', '#ffeb3b', '#66bb6a', '#009688', '#00bcd4', '#2962ff', '#673ab7', '#9c27b0', '#e91e63',
'#ffcdd2', '#ffe0b2', '#fff9c4', '#c8e6c9', '#b2dfdb', '#b2ebf2', '#bbdefb', '#d1c4e9', '#e1bee7', '#f8bbd0',
'#ef9a9a', '#ffcc80', '#fff59d', '#a5d6a7', '#80cbc4', '#80deea', '#90caf9', '#b39ddb', '#ce93d8', '#f48fb1',
'#e57373', '#ffb74d', '#fff176', '#81c784', '#4db6ac', '#4dd0e1', '#64b5f6', '#9575cd', '#ba68c8', '#f06292',
'#f44336', '#fb8c00', '#fdd835', '#4caf50', '#00897b', '#00acc1', '#1e88e5', '#5e35b1', '#8e24aa', '#d81b60',
'#c62828', '#ef6c00', '#f9a825', '#2e7d32', '#00695c', '#00838f', '#1565c0', '#4527a0', '#6a1b9a', '#ad1457',
'#b71c1c', '#e65100', '#f57f17', '#1b5e20', '#004d40', '#006064', '#0d47a1', '#311b92', '#4a148c', '#880e4f'
];
const styleGrid = document.getElementById('tlColorGrid');
const textGrid = document.getElementById('tlTextColorGrid');
const populateGrid = (gridEl, key, closePopupEl = null) => {
if (!gridEl) return;
gridEl.innerHTML = '';
colors.forEach(color => {
const btn = document.createElement('div');
btn.className = 'color-swatch';
btn.style.backgroundColor = color;
btn.onclick = (e) => {
e.stopPropagation();
this.applySettings(key, color);
if (closePopupEl) closePopupEl.classList.add('hidden');
};
gridEl.appendChild(btn);
});
};
populateGrid(styleGrid, 'color');
populateGrid(textGrid, 'textColor', document.getElementById('tlTextColorPicker'));
const textColorBtn = document.getElementById('tlTextColorBtn');
const textPicker = document.getElementById('tlTextColorPicker');
if (textColorBtn && textPicker) {
textColorBtn.onclick = (e) => {
e.stopPropagation();
textPicker.classList.toggle('hidden');
};
}
// Bind Inputs
document.getElementById('tlOpacityInput').addEventListener('input', (e) => {
document.getElementById('tlOpacityValue').textContent = e.target.value + '%';
this.applySettings('opacity', parseInt(e.target.value));
});
document.getElementById('tlFontSize').addEventListener('change', (e) => {
this.applySettings('fontSize', parseInt(e.target.value));
});
document.getElementById('tlTextInput').addEventListener('input', (e) => {
this.applySettings('text', e.target.value);
});
document.getElementById('tlAlignVert').addEventListener('change', (e) => {
this.applySettings('alignVert', e.target.value);
});
document.getElementById('tlAlignHorz').addEventListener('change', (e) => {
this.applySettings('alignHorz', e.target.value);
});
// Close on click outside
document.addEventListener('mousedown', (e) => {
const panel = document.getElementById('trendlineSettingsPanel');
const toolbar = document.getElementById('drawingToolbar');
const textPicker = document.getElementById('tlTextColorPicker');
if (textPicker && !textPicker.classList.contains('hidden') && !textPicker.contains(e.target) && e.target !== document.getElementById('tlTextColorBtn')) {
textPicker.classList.add('hidden');
}
if (panel && !panel.classList.contains('hidden')) {
if (!panel.contains(e.target) && !toolbar.contains(e.target)) {
panel.classList.add('hidden');
}
}
});
}
toggleSettingsPanel(show = true) {
const panel = document.getElementById('trendlineSettingsPanel');
if (!panel) return;
if (show) {
panel.classList.remove('hidden');
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle', 'horizontal_line', 'vertical_line'].includes(this.selectedDrawing.type);
if (!isLineWithText) {
this.activeTLTab = 'style';
document.getElementById('tlTabStyle').className = 'flex-1 py-2 text-center text-blue-500 border-b-2 border-blue-500 font-medium';
document.getElementById('tlTabText').className = 'flex-1 py-2 text-center text-gray-400 hover:text-white';
document.getElementById('tlContentStyle').className = 'block';
document.getElementById('tlContentText').className = 'hidden';
}
this.updateSettingsPanelUI();
} else {
panel.classList.add('hidden');
}
}
updateSettingsPanelUI() {
const settings = this.selectedDrawing || this.defaults[this.selectedDrawing ? this.selectedDrawing.type : 'trend_line'];
document.querySelectorAll('#tlColorGrid .color-swatch').forEach(el => {
if (el.style.backgroundColor === settings.color) el.classList.add('active');
else el.classList.remove('active');
});
document.getElementById('tlOpacityInput').value = settings.opacity !== undefined ? settings.opacity : 100;
document.getElementById('tlOpacityValue').textContent = (settings.opacity !== undefined ? settings.opacity : 100) + '%';
document.querySelectorAll('.tl-thickness-btn').forEach(btn => {
btn.setAttribute('data-active', parseInt(btn.dataset.thickness) === settings.width);
});
document.querySelectorAll('.tl-style-btn').forEach(btn => {
btn.setAttribute('data-active', parseInt(btn.dataset.style) === settings.style);
});
const isLineWithText = this.selectedDrawing && ['trend_line', 'ray', 'rectangle'].includes(this.selectedDrawing.type);
const textColorBtn = document.getElementById('tlTextColorBtn');
const textPicker = document.getElementById('tlTextColorPicker');
const fontInput = document.getElementById('tlFontSize');
const textInput = document.getElementById('tlTextInput');
const alignVert = document.getElementById('tlAlignVert');
const alignHorz = document.getElementById('tlAlignHorz');
const boldBtn = document.getElementById('tlBoldBtn');
const italicBtn = document.getElementById('tlItalicBtn');
if (isLineWithText) {
if (textColorBtn && textPicker) {
textColorBtn.style.backgroundColor = settings.textColor || settings.color;
textPicker.classList.remove('hidden');
}
fontInput.value = settings.fontSize || 14;
textInput.value = settings.text || '';
boldBtn.setAttribute('data-active', !!settings.bold);
italicBtn.setAttribute('data-active', !!settings.italic);
alignVert.value = settings.alignVert || 'top';
alignHorz.value = settings.alignHorz || 'left';
} else {
if (textPicker) textPicker.classList.add('hidden');
}
}
applySettings(key, value) {
if (this.selectedDrawing) {
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);
if (!isLineWithText) return;
this.selectedDrawing[key] = value;
} else {
this.selectedDrawing[key] = value;
}
this.update();
} else {
const drawingType = this.selectedDrawing ? this.selectedDrawing.type : 'trend_line';
if (key === 'bold' || key === 'italic' || key === 'fontSize' || key === 'text' || key === 'textColor' || key === 'alignVert' || key === 'alignHorz') {
if (drawingType === 'trend_line' || drawingType === 'ray' || drawingType === 'rectangle') {
this.defaults.trend_line[key] = value;
}
} else {
if (drawingType === 'trend_line' || drawingType === 'ray' || drawingType === 'rectangle') {
this.defaults.trend_line[key] = value;
} else if (drawingType === 'horizontal_line') {
this.defaults.horizontal_line[key] = value;
} else if (drawingType === 'vertical_line') {
this.defaults.vertical_line[key] = value;
} else {
this.defaults.trend_line[key] = value;
}
}
}
this.updateSettingsPanelUI();
}
}