/** * 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 with pivot bounds */ initializeCharts(chartData, pivotBounds = null) { console.log('Initializing charts with data:', chartData); console.log('Pivot bounds:', pivotBounds); // Use requestAnimationFrame to batch chart creation let index = 0; const createNextChart = () => { if (index < this.timeframes.length) { const timeframe = this.timeframes[index]; if (chartData[timeframe]) { this.createChart(timeframe, chartData[timeframe], pivotBounds); } index++; requestAnimationFrame(createNextChart); } else { // Enable crosshair after all charts are created this.enableCrosshair(); } }; requestAnimationFrame(createNextChart); } /** * Create a single chart for a timeframe */ createChart(timeframe, data, pivotBounds = null) { 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', // Performance optimizations autosize: true, staticPlot: false }; const config = { responsive: true, displayModeBar: true, modeBarButtonsToRemove: ['lasso2d', 'select2d', 'autoScale2d'], displaylogo: false, scrollZoom: true, // Performance optimizations doubleClick: false, showAxisDragHandles: false, showAxisRangeEntryBoxes: false }; // Prepare chart data with pivot bounds const chartData = [candlestickTrace, volumeTrace]; // Add pivot markers from chart data const shapes = []; const annotations = []; const pivotDots = { x: [], y: [], text: [], marker: { color: [], size: [], symbol: [] }, mode: 'markers', hoverinfo: 'text', showlegend: false }; if (data.pivot_markers && Object.keys(data.pivot_markers).length > 0) { const xMin = data.timestamps[0]; const xMax = data.timestamps[data.timestamps.length - 1]; // Process each timestamp that has pivot markers Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => { // Process high pivots if (pivots.highs && pivots.highs.length > 0) { pivots.highs.forEach(pivot => { const color = this._getPivotColor(pivot.level, 'high'); // Draw dot on the pivot candle (above the high) pivotDots.x.push(timestamp); pivotDots.y.push(pivot.price); pivotDots.text.push(`L${pivot.level} High Pivot
Price: $${pivot.price.toFixed(2)}
Strength: ${(pivot.strength * 100).toFixed(0)}%`); pivotDots.marker.color.push(color); pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level)); pivotDots.marker.symbol.push('triangle-down'); // Draw horizontal line ONLY for last pivot of this level if (pivot.is_last) { shapes.push({ type: 'line', x0: xMin, y0: pivot.price, x1: xMax, y1: pivot.price, line: { color: color, width: 1, dash: 'dash' }, layer: 'below' }); // Add label for the level annotations.push({ x: xMax, y: pivot.price, text: `L${pivot.level}H`, showarrow: false, xanchor: 'left', font: { size: 9, color: color }, bgcolor: '#1f2937', borderpad: 2 }); } }); } // Process low pivots if (pivots.lows && pivots.lows.length > 0) { pivots.lows.forEach(pivot => { const color = this._getPivotColor(pivot.level, 'low'); // Draw dot on the pivot candle (below the low) pivotDots.x.push(timestamp); pivotDots.y.push(pivot.price); pivotDots.text.push(`L${pivot.level} Low Pivot
Price: $${pivot.price.toFixed(2)}
Strength: ${(pivot.strength * 100).toFixed(0)}%`); pivotDots.marker.color.push(color); pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level)); pivotDots.marker.symbol.push('triangle-up'); // Draw horizontal line ONLY for last pivot of this level if (pivot.is_last) { shapes.push({ type: 'line', x0: xMin, y0: pivot.price, x1: xMax, y1: pivot.price, line: { color: color, width: 1, dash: 'dash' }, layer: 'below' }); // Add label for the level annotations.push({ x: xMax, y: pivot.price, text: `L${pivot.level}L`, showarrow: false, xanchor: 'left', font: { size: 9, color: color }, bgcolor: '#1f2937', borderpad: 2 }); } }); } }); // Add pivot dots trace if we have any if (pivotDots.x.length > 0) { chartData.push(pivotDots); } console.log(`Added ${shapes.length} pivot levels to ${timeframe} chart`); } // Add shapes and annotations to layout if (shapes.length > 0) { layout.shapes = shapes; } if (annotations.length > 0) { layout.annotations = annotations; } // Use Plotly.react for better performance on updates Plotly.newPlot(plotId, chartData, layout, config).then(() => { // Optimize rendering after initial plot plotElement._fullLayout._replotting = false; }); // Store chart reference this.charts[timeframe] = { plotId: plotId, data: data, element: plotElement, annotations: [] }; // Add click handler for chart and annotations plotElement.on('plotly_click', (eventData) => { // Check if this is an annotation click by looking at the clicked element if (eventData.points && eventData.points.length > 0) { const point = eventData.points[0]; // If it's a shape click (annotation line), try to find the annotation if (point.data && point.data.name && point.data.name.startsWith('line_')) { const annotationId = point.data.name.replace('line_', ''); console.log('Line annotation clicked:', annotationId); this.handleAnnotationClick(annotationId, 'edit'); return; // Don't process as regular chart click } } // Regular chart click for annotation marking this.handleChartClick(timeframe, eventData); }); // Add click handler for annotations plotElement.on('plotly_clickannotation', (eventData) => { console.log('=== plotly_clickannotation event fired ==='); console.log('Event data:', eventData); console.log('Annotation:', eventData.annotation); const annotationName = eventData.annotation.name; console.log('Annotation name:', annotationName); if (annotationName) { const parts = annotationName.split('_'); const action = parts[0]; // 'entry', 'exit', or 'delete' const annotationId = parts[1]; console.log(`Parsed - Action: ${action}, ID: ${annotationId}`); if (action === 'delete') { this.handleAnnotationClick(annotationId, 'delete'); } else { this.handleAnnotationClick(annotationId, 'edit'); } } else { console.log('No annotation name found in click event'); } }); // 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]; // Get the actual price from candlestick data let price; if (point.data.type === 'candlestick') { // For candlestick, use close price price = point.data.close[point.pointIndex]; } else if (point.data.type === 'bar') { // Skip volume bar clicks return; } else { price = point.y; } const clickData = { timeframe: timeframe, timestamp: point.x, price: price, 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 including pivot levels */ updateCharts(newData, pivotBounds = null) { Object.keys(newData).forEach(timeframe => { if (this.charts[timeframe]) { const plotId = this.charts[timeframe].plotId; const data = newData[timeframe]; // Create volume colors const volumeColors = data.close.map((close, i) => { if (i === 0) return '#3b82f6'; return close >= data.open[i] ? '#10b981' : '#ef4444'; }); // Prepare chart data const chartData = [ { 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' } }, { x: data.timestamps, y: data.volume, type: 'bar', yaxis: 'y2', name: 'Volume', marker: { color: volumeColors, opacity: 0.3 }, hoverinfo: 'y' } ]; // Add pivot markers from chart data const shapes = []; const annotations = []; const pivotDots = { x: [], y: [], text: [], marker: { color: [], size: [], symbol: [] }, mode: 'markers', hoverinfo: 'text', showlegend: false }; if (data.pivot_markers && Object.keys(data.pivot_markers).length > 0) { const xMin = data.timestamps[0]; const xMax = data.timestamps[data.timestamps.length - 1]; // Process each timestamp that has pivot markers Object.entries(data.pivot_markers).forEach(([timestamp, pivots]) => { // Process high pivots if (pivots.highs && pivots.highs.length > 0) { pivots.highs.forEach(pivot => { const color = this._getPivotColor(pivot.level, 'high'); // Draw dot on the pivot candle pivotDots.x.push(timestamp); pivotDots.y.push(pivot.price); pivotDots.text.push(`L${pivot.level} High Pivot
Price: $${pivot.price.toFixed(2)}
Strength: ${(pivot.strength * 100).toFixed(0)}%`); pivotDots.marker.color.push(color); pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level)); pivotDots.marker.symbol.push('triangle-down'); // Draw horizontal line ONLY for last pivot if (pivot.is_last) { shapes.push({ type: 'line', x0: xMin, y0: pivot.price, x1: xMax, y1: pivot.price, line: { color: color, width: 1, dash: 'dash' }, layer: 'below' }); annotations.push({ x: xMax, y: pivot.price, text: `L${pivot.level}H`, showarrow: false, xanchor: 'left', font: { size: 9, color: color }, bgcolor: '#1f2937', borderpad: 2 }); } }); } // Process low pivots if (pivots.lows && pivots.lows.length > 0) { pivots.lows.forEach(pivot => { const color = this._getPivotColor(pivot.level, 'low'); // Draw dot on the pivot candle pivotDots.x.push(timestamp); pivotDots.y.push(pivot.price); pivotDots.text.push(`L${pivot.level} Low Pivot
Price: $${pivot.price.toFixed(2)}
Strength: ${(pivot.strength * 100).toFixed(0)}%`); pivotDots.marker.color.push(color); pivotDots.marker.size.push(this._getPivotMarkerSize(pivot.level)); pivotDots.marker.symbol.push('triangle-up'); // Draw horizontal line ONLY for last pivot if (pivot.is_last) { shapes.push({ type: 'line', x0: xMin, y0: pivot.price, x1: xMax, y1: pivot.price, line: { color: color, width: 1, dash: 'dash' }, layer: 'below' }); annotations.push({ x: xMax, y: pivot.price, text: `L${pivot.level}L`, showarrow: false, xanchor: 'left', font: { size: 9, color: color }, bgcolor: '#1f2937', borderpad: 2 }); } }); } }); // Add pivot dots trace if we have any if (pivotDots.x.length > 0) { chartData.push(pivotDots); } } // Use Plotly.react for efficient updates const update = { shapes: shapes, annotations: annotations }; Plotly.react(plotId, chartData, update); } }); } /** * 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 (clickable) plotlyAnnotations.push({ x: entryTime, y: entryPrice, text: '▲', showarrow: false, font: { size: 20, color: ann.direction === 'LONG' ? '#10b981' : '#ef4444' }, xanchor: 'center', yanchor: 'bottom', captureevents: true, name: `entry_${ann.annotation_id}` }); // Exit marker (clickable) plotlyAnnotations.push({ x: exitTime, y: exitPrice, text: '▼', showarrow: false, font: { size: 20, color: ann.direction === 'LONG' ? '#10b981' : '#ef4444' }, xanchor: 'center', yanchor: 'top', captureevents: true, name: `exit_${ann.annotation_id}` }); // P&L label with delete button 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, captureevents: true, name: `delete_${ann.annotation_id}` }); // Connecting line (clickable for selection) plotlyShapes.push({ type: 'line', x0: entryTime, y0: entryPrice, x1: exitTime, y1: exitPrice, line: { color: ann.direction === 'LONG' ? '#10b981' : '#ef4444', width: 2, dash: 'dash' }, name: `line_${ann.annotation_id}` }); }); // Get existing pivot annotations (they have specific names like L1H, L2L) const existingLayout = chart.element.layout || {}; const existingAnnotations = existingLayout.annotations || []; const pivotAnnotations = existingAnnotations.filter(ann => ann.text && ann.text.match(/^L\d+[HL]$/) ); // Merge pivot annotations with trade annotations const allAnnotations = [...pivotAnnotations, ...plotlyAnnotations]; // Get existing pivot shapes const existingShapes = existingLayout.shapes || []; const pivotShapes = existingShapes.filter(shape => shape.layer === 'below' && shape.line && shape.line.dash === 'dash' ); // Merge pivot shapes with trade annotation shapes const allShapes = [...pivotShapes, ...plotlyShapes]; // Update chart layout with merged annotations and shapes Plotly.relayout(chart.plotId, { annotations: allAnnotations, shapes: allShapes }); console.log(`Updated ${timeframeAnnotations.length} trade annotations for ${timeframe} (preserved ${pivotAnnotations.length} pivot annotations)`); } /** * Handle annotation click for editing/deleting */ handleAnnotationClick(annotationId, action) { console.log(`=== handleAnnotationClick called ===`); console.log(` Action: ${action}`); console.log(` Annotation ID: ${annotationId}`); console.log(` window.deleteAnnotation type: ${typeof window.deleteAnnotation}`); if (action === 'delete') { console.log('Delete action confirmed, showing confirm dialog...'); if (confirm('Delete this annotation?')) { console.log('User confirmed deletion'); if (window.deleteAnnotation) { console.log('Calling window.deleteAnnotation...'); window.deleteAnnotation(annotationId); } else { console.error('window.deleteAnnotation is not available!'); alert('Delete function not available. Please refresh the page.'); } } else { console.log('User cancelled deletion'); } } else if (action === 'edit') { console.log('Edit action'); if (window.appState && window.appState.chartManager) { window.appState.chartManager.editAnnotation(annotationId); } } } /** * 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 - allows moving entry/exit points */ editAnnotation(annotationId) { const annotation = this.annotations[annotationId]; if (!annotation) return; // Create a better edit dialog using Bootstrap modal this.showEditDialog(annotationId, annotation); } /** * Show edit dialog for annotation */ showEditDialog(annotationId, annotation) { // Create modal HTML const modalHtml = ` `; // Remove existing modal if any const existingModal = document.getElementById('editAnnotationModal'); if (existingModal) { existingModal.remove(); } // Add modal to page document.body.insertAdjacentHTML('beforeend', modalHtml); // Show modal const modal = new bootstrap.Modal(document.getElementById('editAnnotationModal')); modal.show(); // Add event listeners document.getElementById('edit-entry-btn').addEventListener('click', () => { modal.hide(); this.startEditMode(annotationId, annotation, 'entry'); }); document.getElementById('edit-exit-btn').addEventListener('click', () => { modal.hide(); this.startEditMode(annotationId, annotation, 'exit'); }); document.getElementById('delete-annotation-btn').addEventListener('click', () => { modal.hide(); this.handleAnnotationClick(annotationId, 'delete'); }); // Clean up modal when hidden document.getElementById('editAnnotationModal').addEventListener('hidden.bs.modal', () => { document.getElementById('editAnnotationModal').remove(); }); } /** * Start edit mode for annotation */ startEditMode(annotationId, annotation, editMode) { const message = editMode === 'entry' ? 'Click on chart to set new entry point' : 'Click on chart to set new exit point'; window.showSuccess(message); // Store annotation for editing if (window.appState && window.appState.annotationManager) { window.appState.annotationManager.editingAnnotation = { annotation_id: annotationId, original: annotation, editMode: editMode }; // Remove current annotation from display this.removeAnnotation(annotationId); // Show reference marker const chart = this.charts[annotation.timeframe]; if (chart) { const referencePoint = editMode === 'entry' ? annotation.exit : annotation.entry; const markerText = editMode === 'entry' ? '▼ (exit)' : '▲ (entry)'; const arrowDirection = editMode === 'entry' ? 40 : -40; Plotly.relayout(chart.plotId, { annotations: [{ x: referencePoint.timestamp, y: referencePoint.price, text: markerText, showarrow: true, arrowhead: 2, ax: 0, ay: arrowDirection, font: { size: 14, color: '#9ca3af' }, bgcolor: 'rgba(31, 41, 55, 0.8)', borderpad: 4 }] }); } } } /** * Clear all annotations from charts */ clearAllAnnotations() { console.log('Clearing all annotations from charts'); // Clear from memory this.annotations = {}; // Update all charts Object.keys(this.charts).forEach(timeframe => { this.updateChartAnnotations(timeframe); }); console.log('All annotations cleared from charts'); } /** * Update chart layout when charts are minimized/maximized */ updateChartLayout() { const chartContainer = document.getElementById('chart-container'); const visibleCharts = document.querySelectorAll('.timeframe-chart:not(.minimized)'); const minimizedCharts = document.querySelectorAll('.timeframe-chart.minimized'); // Remove scroll if all charts are visible or if some are minimized if (minimizedCharts.length > 0) { chartContainer.classList.add('no-scroll'); // Calculate available height for visible charts const containerHeight = chartContainer.clientHeight; const headerHeight = 50; // Approximate header height const availableHeight = containerHeight - (visibleCharts.length * headerHeight); const chartHeight = Math.max(200, availableHeight / visibleCharts.length); // Update visible chart heights visibleCharts.forEach(chart => { const plotElement = chart.querySelector('.chart-plot'); if (plotElement) { plotElement.style.height = `${chartHeight}px`; // Trigger Plotly resize const plotId = plotElement.id; if (plotId) { Plotly.Plots.resize(plotId); } } }); } else { chartContainer.classList.remove('no-scroll'); // Reset to default heights visibleCharts.forEach(chart => { const plotElement = chart.querySelector('.chart-plot'); if (plotElement) { plotElement.style.height = '300px'; // Trigger Plotly resize const plotId = plotElement.id; if (plotId) { Plotly.Plots.resize(plotId); } } }); } console.log(`Updated chart layout: ${visibleCharts.length} visible, ${minimizedCharts.length} minimized`); } /** * Get color for pivot level */ _getPivotColor(level, type) { // Different colors for different levels const highColors = ['#dc3545', '#ff6b6b', '#ff8787', '#ffa8a8', '#ffc9c9']; const lowColors = ['#28a745', '#51cf66', '#69db7c', '#8ce99a', '#b2f2bb']; const colors = type === 'high' ? highColors : lowColors; return colors[Math.min(level - 1, colors.length - 1)]; } /** * Get marker size for pivot level * L1 = smallest (6px), L5 = largest (14px) */ _getPivotMarkerSize(level) { const sizes = [6, 8, 10, 12, 14]; // L1 to L5 return sizes[Math.min(level - 1, sizes.length - 1)]; } /** * 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)}`; } } }