/** * ChartManager - Manages Plotly charts for multi-timeframe visualization */ class ChartManager { constructor(containerId, timeframes) { this.containerId = containerId; this.timeframes = timeframes; this.charts = {}; this.annotations = {}; this.syncedTime = null; console.log('ChartManager initialized with timeframes:', timeframes); } /** * Initialize charts for all timeframes */ initializeCharts(chartData) { console.log('Initializing charts with data:', chartData); this.timeframes.forEach(timeframe => { if (chartData[timeframe]) { this.createChart(timeframe, chartData[timeframe]); } }); // Enable crosshair this.enableCrosshair(); } /** * Create a single chart for a timeframe */ createChart(timeframe, data) { const plotId = `plot-${timeframe}`; const plotElement = document.getElementById(plotId); if (!plotElement) { console.error(`Plot element not found: ${plotId}`); return; } // Create candlestick trace const candlestickTrace = { x: data.timestamps, open: data.open, high: data.high, low: data.low, close: data.close, type: 'candlestick', name: 'Price', increasing: { line: {color: '#10b981', width: 1}, fillcolor: '#10b981' }, decreasing: { line: {color: '#ef4444', width: 1}, fillcolor: '#ef4444' }, xaxis: 'x', yaxis: 'y' }; // Create volume trace with color based on price direction const volumeColors = data.close.map((close, i) => { if (i === 0) return '#3b82f6'; return close >= data.open[i] ? '#10b981' : '#ef4444'; }); const volumeTrace = { x: data.timestamps, y: data.volume, type: 'bar', name: 'Volume', yaxis: 'y2', marker: { color: volumeColors, opacity: 0.3 }, hoverinfo: 'y' }; const layout = { title: '', showlegend: false, xaxis: { rangeslider: {visible: false}, gridcolor: '#374151', color: '#9ca3af', showgrid: true, zeroline: false }, yaxis: { title: { text: 'Price (USD)', font: {size: 10} }, gridcolor: '#374151', color: '#9ca3af', showgrid: true, zeroline: false, domain: [0.3, 1] }, yaxis2: { title: { text: 'Volume', font: {size: 10} }, gridcolor: '#374151', color: '#9ca3af', showgrid: false, zeroline: false, domain: [0, 0.25] }, plot_bgcolor: '#1f2937', paper_bgcolor: '#1f2937', font: {color: '#f8f9fa', size: 11}, margin: {l: 60, r: 20, t: 10, b: 40}, hovermode: 'x unified', dragmode: 'pan' }; const config = { responsive: true, displayModeBar: true, modeBarButtonsToRemove: ['lasso2d', 'select2d', 'autoScale2d'], displaylogo: false, scrollZoom: true }; Plotly.newPlot(plotId, [candlestickTrace, volumeTrace], layout, config); // Store chart reference this.charts[timeframe] = { plotId: plotId, data: data, element: plotElement, annotations: [] }; // Add click handler for annotations plotElement.on('plotly_click', (eventData) => { this.handleChartClick(timeframe, eventData); }); // Add hover handler to update info plotElement.on('plotly_hover', (eventData) => { this.updateChartInfo(timeframe, eventData); }); console.log(`Chart created for ${timeframe} with ${data.timestamps.length} candles`); } /** * Handle chart click for annotation */ handleChartClick(timeframe, eventData) { if (!eventData.points || eventData.points.length === 0) return; const point = eventData.points[0]; const clickData = { timeframe: timeframe, timestamp: point.x, price: point.close || point.y, index: point.pointIndex }; console.log('Chart clicked:', clickData); // Trigger annotation manager if (window.appState && window.appState.annotationManager) { window.appState.annotationManager.handleChartClick(clickData); } } /** * Update charts with new data */ updateCharts(newData) { Object.keys(newData).forEach(timeframe => { if (this.charts[timeframe]) { const plotId = this.charts[timeframe].plotId; Plotly.react(plotId, [ { x: newData[timeframe].timestamps, open: newData[timeframe].open, high: newData[timeframe].high, low: newData[timeframe].low, close: newData[timeframe].close, type: 'candlestick' }, { x: newData[timeframe].timestamps, y: newData[timeframe].volume, type: 'bar', yaxis: 'y2' } ]); } }); } /** * Add annotation to charts */ addAnnotation(annotation) { console.log('Adding annotation to charts:', annotation); // Store annotation this.annotations[annotation.annotation_id] = annotation; // Add markers to relevant timeframe chart const timeframe = annotation.timeframe; if (this.charts[timeframe]) { // TODO: Add visual markers using Plotly annotations this.updateChartAnnotations(timeframe); } } /** * Remove annotation from charts */ removeAnnotation(annotationId) { if (this.annotations[annotationId]) { const annotation = this.annotations[annotationId]; delete this.annotations[annotationId]; // Update chart if (this.charts[annotation.timeframe]) { this.updateChartAnnotations(annotation.timeframe); } } } /** * Update chart annotations */ updateChartAnnotations(timeframe) { const chart = this.charts[timeframe]; if (!chart) return; // Get annotations for this timeframe const timeframeAnnotations = Object.values(this.annotations) .filter(ann => ann.timeframe === timeframe); // Build Plotly annotations and shapes const plotlyAnnotations = []; const plotlyShapes = []; timeframeAnnotations.forEach(ann => { const entryTime = ann.entry.timestamp; const exitTime = ann.exit.timestamp; const entryPrice = ann.entry.price; const exitPrice = ann.exit.price; // Entry marker plotlyAnnotations.push({ x: entryTime, y: entryPrice, text: '▲', showarrow: false, font: { size: 20, color: ann.direction === 'LONG' ? '#10b981' : '#ef4444' }, xanchor: 'center', yanchor: 'bottom' }); // Exit marker plotlyAnnotations.push({ x: exitTime, y: exitPrice, text: '▼', showarrow: false, font: { size: 20, color: ann.direction === 'LONG' ? '#10b981' : '#ef4444' }, xanchor: 'center', yanchor: 'top' }); // P&L label const midTime = new Date((new Date(entryTime).getTime() + new Date(exitTime).getTime()) / 2); const midPrice = (entryPrice + exitPrice) / 2; const pnlColor = ann.profit_loss_pct >= 0 ? '#10b981' : '#ef4444'; plotlyAnnotations.push({ x: midTime, y: midPrice, text: `${ann.profit_loss_pct >= 0 ? '+' : ''}${ann.profit_loss_pct.toFixed(2)}%`, showarrow: true, arrowhead: 0, ax: 0, ay: -40, font: { size: 12, color: pnlColor, family: 'monospace' }, bgcolor: '#1f2937', bordercolor: pnlColor, borderwidth: 1, borderpad: 4 }); // Connecting line plotlyShapes.push({ type: 'line', x0: entryTime, y0: entryPrice, x1: exitTime, y1: exitPrice, line: { color: ann.direction === 'LONG' ? '#10b981' : '#ef4444', width: 2, dash: 'dash' } }); }); // Update chart layout with annotations Plotly.relayout(chart.plotId, { annotations: plotlyAnnotations, shapes: plotlyShapes }); console.log(`Updated ${timeframeAnnotations.length} annotations for ${timeframe}`); } /** * Highlight annotation */ highlightAnnotation(annotationId) { const annotation = this.annotations[annotationId]; if (!annotation) return; const timeframe = annotation.timeframe; const chart = this.charts[timeframe]; if (!chart) return; // Flash the annotation by temporarily changing its color const originalAnnotations = chart.element.layout.annotations || []; const highlightedAnnotations = originalAnnotations.map(ann => { // Create a copy with highlighted color return { ...ann, font: { ...ann.font, color: '#fbbf24' // Yellow highlight } }; }); Plotly.relayout(chart.plotId, {annotations: highlightedAnnotations}); // Restore original colors after 1 second setTimeout(() => { this.updateChartAnnotations(timeframe); }, 1000); console.log('Highlighted annotation:', annotationId); } /** * Edit annotation */ editAnnotation(annotationId) { const annotation = this.annotations[annotationId]; if (!annotation) return; // Remove from charts this.removeAnnotation(annotationId); // Set as pending annotation for editing if (window.appState && window.appState.annotationManager) { window.appState.annotationManager.pendingAnnotation = { annotation_id: annotationId, symbol: annotation.symbol, timeframe: annotation.timeframe, entry: annotation.entry, isEditing: true }; document.getElementById('pending-annotation-status').style.display = 'block'; window.showSuccess('Click on chart to set new exit point'); } } /** * Enable crosshair cursor */ enableCrosshair() { // Crosshair is enabled via hovermode in layout console.log('Crosshair enabled'); } /** * Handle zoom */ handleZoom(zoomFactor) { Object.values(this.charts).forEach(chart => { Plotly.relayout(chart.plotId, { 'xaxis.range[0]': null, 'xaxis.range[1]': null }); }); } /** * Reset zoom */ resetZoom() { Object.values(this.charts).forEach(chart => { Plotly.relayout(chart.plotId, { 'xaxis.autorange': true, 'yaxis.autorange': true }); }); } /** * Synchronize time navigation across charts */ syncTimeNavigation(timestamp) { this.syncedTime = timestamp; // Update all charts to center on this timestamp Object.values(this.charts).forEach(chart => { const data = chart.data; const timestamps = data.timestamps; // Find index closest to target timestamp const targetTime = new Date(timestamp); let closestIndex = 0; let minDiff = Infinity; timestamps.forEach((ts, i) => { const diff = Math.abs(new Date(ts) - targetTime); if (diff < minDiff) { minDiff = diff; closestIndex = i; } }); // Center the view on this index const rangeSize = 100; // Show 100 candles const startIndex = Math.max(0, closestIndex - rangeSize / 2); const endIndex = Math.min(timestamps.length - 1, closestIndex + rangeSize / 2); Plotly.relayout(chart.plotId, { 'xaxis.range': [timestamps[startIndex], timestamps[endIndex]] }); }); console.log('Synced charts to timestamp:', timestamp); } /** * Update chart info display on hover */ updateChartInfo(timeframe, eventData) { if (!eventData.points || eventData.points.length === 0) return; const point = eventData.points[0]; const infoElement = document.getElementById(`info-${timeframe}`); if (infoElement && point.data.type === 'candlestick') { const open = point.data.open[point.pointIndex]; const high = point.data.high[point.pointIndex]; const low = point.data.low[point.pointIndex]; const close = point.data.close[point.pointIndex]; infoElement.textContent = `O: ${open.toFixed(2)} H: ${high.toFixed(2)} L: ${low.toFixed(2)} C: ${close.toFixed(2)}`; } } }