feat: implement professional drawing tools with selection and editing support

This commit is contained in:
DiTus
2026-03-21 11:36:17 +01:00
parent a34f80f841
commit eda151bff5
3 changed files with 546 additions and 376 deletions

View File

@ -149,14 +149,16 @@ function throttle(func, limit) {
}
}
import { DrawingManager } from './drawing-tools.js';
export class TradingDashboard {
constructor() {
constructor() {
this.chart = null;
this.candleSeries = null;
// Load settings from local storage or defaults
this.symbol = localStorage.getItem('winterfail_symbol') || 'BTC';
this.currentInterval = localStorage.getItem('winterfail_interval') || '1d';
this.intervals = INTERVALS;
this.allData = new Map();
this.isLoading = false;
@ -169,14 +171,13 @@ constructor() {
this.avgPriceSeries = null;
this.dailyMAData = new Map(); // timestamp -> { ma44, ma125, price }
this.currentMouseTime = null;
this.drawingItems = []; // Store drawing items for management
this.drawingManager = null;
// Throttled versions of heavy functions
this.throttledOnVisibleRangeChange = throttle(this.onVisibleRangeChange.bind(this), 150);
this.init();
}
async loadDailyMAData() {
try {
// Use 1d interval for this calculation
@ -454,6 +455,14 @@ constructor() {
this.initPriceScaleControls();
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);
}
// Setup price format selector change handler
document.addEventListener("DOMContentLoaded", () => {
const priceSelect = document.getElementById("priceFormatSelect");
@ -523,9 +532,6 @@ constructor() {
}
}
// Drawing Tools
this.initDrawingTools();
// Initialize state from storage
this.scaleState = {
autoScale: localStorage.getItem('winterfail_scale_auto') !== 'false',
@ -583,341 +589,6 @@ constructor() {
});
}
initDrawingTools() {
const btnDrawingTools = document.getElementById('btnDrawingTools');
const drawingToolsPopup = document.getElementById('drawingToolsPopup');
if (btnDrawingTools && drawingToolsPopup) {
btnDrawingTools.addEventListener('click', (e) => {
e.stopPropagation();
drawingToolsPopup.classList.toggle('hidden');
});
document.addEventListener('click', closeDrawingToolsPopup);
document.addEventListener('touchstart', closeDrawingToolsPopup, { passive: true });
function closeDrawingToolsPopup(e) {
const isInside = drawingToolsPopup.contains(e.target) || e.target === btnDrawingTools;
const isDrawingButton = e.target.closest('#btnDrawingTools');
if (!isInside && !isDrawingButton) {
drawingToolsPopup.classList.add('hidden');
}
}
}
let isDrawing = false;
let drawMode = null;
let startPoint = null;
let drawLine = null;
let drawArrow = null;
let drawText = null;
window.activateDrawingTool = (tool) => {
if (tool === 'clear') {
this.clearDrawingTools();
return;
}
drawMode = tool;
isDrawing = true;
const chart = this.chart;
const container = chart.container();
const onMouseDown = (e) => {
e.preventDefault();
const rect = container.getBoundingClientRect();
const x = e.clientX - rect.left;
const y = e.clientY - rect.top;
if (drawMode === 'trend_line' || drawMode === 'horizontal_line' || drawMode === 'vertical_line') {
this.createLine(x, y);
} else if (drawMode === 'arrow_up' || drawMode === 'arrow_down') {
this.createArrow(drawMode === 'arrow_up' ? 'arrowUp' : 'arrowDown', x, y);
} else if (drawMode === 'text') {
this.createText(x, y);
}
};
const onTouchStart = (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = container.getBoundingClientRect();
const x = touch.clientX - rect.left;
const y = touch.clientY - rect.top;
if (drawMode === 'trend_line' || drawMode === 'horizontal_line' || drawMode === 'vertical_line') {
this.createLine(x, y);
} else if (drawMode === 'arrow_up' || drawMode === 'arrow_down') {
this.createArrow(drawMode === 'arrow_up' ? 'arrowUp' : 'arrowDown', x, y);
} else if (drawMode === 'text') {
this.createText(x, y);
}
};
container.addEventListener('mousedown', onMouseDown);
container.addEventListener('touchstart', onTouchStart);
const closeDrawing = () => {
container.removeEventListener('mousedown', onMouseDown);
container.removeEventListener('touchstart', onTouchStart);
document.removeEventListener('click', closeDrawing);
document.removeEventListener('touchstart', closeDrawing);
};
document.addEventListener('click', closeDrawing);
document.addEventListener('touchstart', closeDrawing);
drawingToolsPopup.classList.add('hidden');
};
}
createLine(x, y) {
const chart = this.chart;
const candleSeries = this.candleSeries;
let lineData = {
time: [],
value: []
};
const line = chart.addLineSeries({
color: '#2962ff',
lineWidth: 2,
lineStyle: LightweightCharts.LineStyle.Solid,
crosshairMarkerVisible: true,
crosshairMarkerRadius: 4,
baseIndex: 0,
});
let points = [];
const container = chart.container();
let startTime = null;
let startPrice = null;
const onMouseDown = (e) => {
e.preventDefault();
const rect = container.getBoundingClientRect();
const clientX = e.clientX;
const clientY = e.clientY;
const timeCoord = chart.timeScale().coordinateToTime(x);
const price = chart.priceScale().coordinateToPrice(y);
if (startTime === null) {
startTime = timeCoord;
startPrice = price;
line.setData([{
time: startTime,
value: startPrice
}]);
} else {
line.setData([
{ time: startTime, value: startPrice },
{ time: timeCoord, value: price }
]);
startTime = null;
startPrice = null;
}
};
const onTouchStart = (e) => {
e.preventDefault();
const touch = e.touches[0];
const rect = container.getBoundingClientRect();
const clientX = touch.clientX;
const clientY = touch.clientY;
const timeCoord = chart.timeScale().coordinateToTime(x);
const price = chart.priceScale().coordinateToPrice(y);
if (startTime === null) {
startTime = timeCoord;
startPrice = price;
line.setData([{
time: startTime,
value: startPrice
}]);
} else {
line.setData([
{ time: startTime, value: startPrice },
{ time: timeCoord, value: price }
]);
startTime = null;
startPrice = null;
}
};
container.addEventListener('mousedown', onMouseDown);
container.addEventListener('touchstart', onTouchStart);
const cleanup = () => {
container.removeEventListener('mousedown', onMouseDown);
container.removeEventListener('touchstart', onTouchStart);
};
document.addEventListener('click', cleanup);
document.addEventListener('touchstart', cleanup);
}
createArrow(arrowType, x, y) {
const chart = this.chart;
const container = chart.container();
const markerPrimitive = new SeriesMarkersPrimitive();
this.candleSeries.attachPrimitive(markerPrimitive);
const rect = container.getBoundingClientRect();
const time = chart.timeScale().coordinateToTime(x);
const price = chart.priceScale().coordinateToPrice(y);
const marker = {
time: time,
position: arrowType === 'arrowUp' ? 'belowBar' : 'aboveBar',
color: arrowType === 'arrowUp' ? '#26a69a' : '#ef5350',
shape: arrowType === 'arrowUp' ? 'arrowUp' : 'arrowDown',
text: ''
};
markerPrimitive.setMarkers([marker]);
let isDragging = false;
let startX = 0;
let startY = 0;
let startTime = time;
let startPrice = price;
const onMouseDown = (e) => {
e.preventDefault();
isDragging = true;
startX = e.clientX;
startY = e.clientY;
};
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
const rect = container.getBoundingClientRect();
const timeCoord = chart.timeScale().coordinateToTime(x + dx);
const newPrice = chart.priceScale().coordinateToPrice(y + dy);
const newMarker = {
time: timeCoord,
position: arrowType === 'arrowUp' ? 'belowBar' : 'aboveBar',
color: arrowType === 'arrowUp' ? '#26a69a' : '#ef5350',
shape: arrowType === 'arrowUp' ? 'arrowUp' : 'arrowDown',
text: ''
};
markerPrimitive.setMarkers([newMarker]);
};
const onMouseUp = () => {
isDragging = false;
};
container.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
const cleanup = () => {
container.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('click', cleanup);
document.removeEventListener('touchstart', cleanup);
};
document.addEventListener('click', cleanup);
document.addEventListener('touchstart', cleanup);
}
createText(x, y) {
const chart = this.chart;
const container = chart.container();
const text = prompt('Enter text label:', '');
if (!text) return;
const rect = container.getBoundingClientRect();
const time = chart.timeScale().coordinateToTime(x);
const price = chart.priceScale().coordinateToPrice(y);
const label = chart.addLabel({
text: text,
color: '#ffffff',
fontSize: 11,
fontFamily: 'Inter',
fontWeight: 'bold',
crosshairMarkerVisible: false,
time: time,
price: price
});
let isDragging = false;
let startX = 0;
let startY = 0;
let startTime = time;
let startPrice = price;
const onMouseDown = (e) => {
e.preventDefault();
isDragging = true;
startX = e.clientX;
startY = e.clientY;
};
const onMouseMove = (e) => {
if (!isDragging) return;
const dx = e.clientX - startX;
const dy = e.clientY - startY;
const newTime = chart.timeScale().coordinateToTime(x + dx);
const newPrice = chart.priceScale().coordinateToPrice(y + dy);
label.applyOptions({
time: newTime,
price: newPrice
});
};
const onMouseUp = () => {
isDragging = false;
};
container.addEventListener('mousedown', onMouseDown);
document.addEventListener('mousemove', onMouseMove);
document.addEventListener('mouseup', onMouseUp);
const cleanup = () => {
container.removeEventListener('mousedown', onMouseDown);
document.removeEventListener('mousemove', onMouseMove);
document.removeEventListener('mouseup', onMouseUp);
document.removeEventListener('click', cleanup);
document.removeEventListener('touchstart', cleanup);
};
document.addEventListener('click', cleanup);
document.addEventListener('touchstart', cleanup);
}
clearDrawingTools() {
const allSeries = this.chart.series();
allSeries.forEach(series => {
if (series.type !== 'Candlestick' && series.type !== 'Line' && series.type !== 'Area') {
this.chart.removeSeries(series);
}
});
}
initNavigationControls() {
const chartWrapper = document.getElementById('chartWrapper');
const navLeft = document.getElementById('navLeft');