UNPKG

trace-chart

Version:

A lightweight, fully customizable financial charting library for global markets

1,422 lines (1,193 loc) 256 kB
/*! * trace-chart v0.0.1 * A lightweight, fully customizable financial charting library for global markets * (c) 2025 Shobhit Srivastava * Released under the Apache 2.0 License */ (function (global, factory) { typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() : typeof define === 'function' && define.amd ? define(factory) : (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.TraceChart = factory()); })(this, (function () { 'use strict'; /** * ChartEventManager.js - Centralized Event Management System * * @author Shobhit Srivastava * @license Apache 2.0 * * Centralizes all mouse event handling for the chart to eliminate conflicts * between drawing system, interaction detection, and ECharts native events. * * ARCHITECTURE: * - Single event handlers per event type * - Mode-based event routing * - Proper cleanup and state management * - Performance optimization through event consolidation */ class ChartEventManager { constructor(chartEngine) { this.chartEngine = chartEngine; this.chart = chartEngine.chart; this.zr = this.chart.getZr(); // Event state tracking this.mouseState = { isDown: false, startPoint: null, currentPoint: null, dragDistance: 0, button: null // Track which button was pressed }; // Event routing this.eventMode = 'chart'; // 'chart', 'drawing', 'drawingEdit' // Performance optimization this.lastMoveTime = 0; this.moveThrottleMs = 16; // ~60fps max this.setupMasterHandlers(); } /** * Setup single master event handlers * Replaces all scattered zr.on() calls throughout the codebase */ setupMasterHandlers = () => { console.log('Trace: Setting up centralized event handlers'); // Clear any existing handlers first this.cleanup(); // Master event handlers - one per event type this.zr.on('mousedown', this.handleMouseDown); this.zr.on('mousemove', this.handleMouseMove); this.zr.on('mouseup', this.handleMouseUp); this.zr.on('mouseleave', this.handleMouseLeave); this.zr.on('click', this.handleClick); this.zr.on('contextmenu', this.handleContextMenu); // Global fallback for edge cases document.addEventListener('mouseup', this.handleGlobalMouseUp); console.log('Trace: Centralized event handlers setup complete'); } /** * Master mousedown handler - routes to appropriate subsystem */ handleMouseDown = (params) => { // Update mouse state this.mouseState.isDown = true; this.mouseState.startPoint = [params.offsetX, params.offsetY]; this.mouseState.currentPoint = [params.offsetX, params.offsetY]; this.mouseState.dragDistance = 0; this.mouseState.button = params.event?.button || 0; // Route based on current mode switch (this.eventMode) { case 'drawing': this.handleDrawingMouseDown(params); break; case 'drawingEdit': this.handleDrawingEditMouseDown(params); break; case 'chart': default: this.handleChartMouseDown(params); break; } } /** * Master mousemove handler with performance throttling */ handleMouseMove = (params) => { // Performance throttling - limit to ~60fps const now = Date.now(); if (now - this.lastMoveTime < this.moveThrottleMs) { return; } this.lastMoveTime = now; // Update mouse state if (this.mouseState.isDown && this.mouseState.startPoint) { this.mouseState.currentPoint = [params.offsetX, params.offsetY]; this.mouseState.dragDistance = Math.sqrt( Math.pow(params.offsetX - this.mouseState.startPoint[0], 2) + Math.pow(params.offsetY - this.mouseState.startPoint[1], 2) ); } // Route based on current mode and state if (this.mouseState.isDown) { // Mouse is down - this is a drag operation switch (this.eventMode) { case 'drawing': this.handleDrawingMouseMove(params); break; case 'drawingEdit': this.handleDrawingEditMouseMove(params); break; case 'chart': default: this.handleChartMouseMove(params); break; } } else { // Mouse is up - this is a hover operation this.handleMouseHover(params); } } /** * Master mouseup handler */ handleMouseUp = (params) => { const wasDown = this.mouseState.isDown; if (wasDown) { // This was a drag operation ending switch (this.eventMode) { case 'drawing': this.handleDrawingMouseUp(params); break; case 'drawingEdit': this.handleDrawingEditMouseUp(params); break; case 'chart': default: this.handleChartMouseUp(params); break; } } // Reset mouse state this.resetMouseState(); } /** * Handle mouse leaving chart area */ handleMouseLeave = () => { const wasDown = this.mouseState.isDown; if (wasDown) { // Treat as mouseup this.handleMouseUp({ offsetX: this.mouseState.currentPoint?.[0] || 0, offsetY: this.mouseState.currentPoint?.[1] || 0 }); } this.resetMouseState(); } /** * Handle click events */ handleClick = (params) => { // Route based on mode switch (this.eventMode) { case 'drawing': // Handle single-click drawings (horizontal/vertical lines) if (this.chartEngine.drawingMode === 'horizontalLine' || this.chartEngine.drawingMode === 'verticalLine') { this.handleDrawingClick(params); } break; case 'drawingEdit': // Handle drawing selection/deselection this.handleDrawingEditClick(params); break; } } /** * Handle context menu (right-click) */ handleContextMenu = (params) => { console.log(`Trace: ContextMenu - Mode: ${this.eventMode}`); // Prevent default context menu params.event?.preventDefault(); if (this.eventMode === 'drawingEdit') { const point = [params.offsetX, params.offsetY]; const drawing = this.chartEngine.getDrawingAtPoint(point); if (drawing) { this.chartEngine.showDrawingContextMenu(drawing, params.event.clientX, params.event.clientY); } else { this.chartEngine.hideDrawingContextMenu(); } } } /** * Reset mouse state */ resetMouseState = () => { this.mouseState.isDown = false; this.mouseState.startPoint = null; this.mouseState.currentPoint = null; this.mouseState.dragDistance = 0; this.mouseState.button = null; } /** * Global mouseup fallback */ handleGlobalMouseUp = (event) => { if (this.mouseState.isDown) { console.log('Trace: Global mouseup - cleaning up mouse state'); // Calculate relative position if possible let offsetX = 0, offsetY = 0; if (this.chart && this.chart.getDom()) { const rect = this.chart.getDom().getBoundingClientRect(); offsetX = event.clientX - rect.left; offsetY = event.clientY - rect.top; } this.handleMouseUp({ offsetX, offsetY }); } } // ============================================================================= // CHART INTERACTION HANDLERS // ============================================================================= /** * Handle chart-specific mouse interactions */ handleChartMouseDown = (params) => { // Start interaction tracking for performance if (this.mouseState.button === 0) { // Left button only this.chartEngine.startUserInteraction('drag'); } } handleChartMouseMove = (params) => { // Chart drag in progress - ECharts handles the actual dragging // We just maintain interaction state for performance } handleChartMouseUp = (params) => { // End interaction tracking this.chartEngine.endUserInteraction(); } handleMouseHover = (params) => { const point = [params.offsetX, params.offsetY]; const hoveredDrawing = this.chartEngine.getDrawingAtPoint(point); // Update cursor and hover state this.chartEngine.updateCursorForHover(hoveredDrawing); this.chartEngine.hoveredDrawing = hoveredDrawing; // Simple mode switching based on hover if (hoveredDrawing) { // Mouse is over a drawing - switch to drawingEdit mode if (this.eventMode !== 'drawingEdit' && this.eventMode !== 'drawing') { this.setEventMode('drawingEdit'); } } else { // Mouse is not over any drawing - switch back to chart mode if (this.eventMode === 'drawingEdit') { this.setEventMode('chart'); } } } // ============================================================================= // DRAWING SYSTEM HANDLERS // ============================================================================= /** * Drawing-specific event handlers */ handleDrawingMouseDown = (params) => { if (!this.chartEngine.drawingMode || !this.chartEngine.processedData.length) return; if (this.mouseState.button !== 0) return; // Left button only // SKIP mousedown/up flow for single-click drawings if (this.chartEngine.drawingMode === 'horizontalLine' || this.chartEngine.drawingMode === 'verticalLine') { return; // Let these be handled by click event only } const point = [params.offsetX, params.offsetY]; const dataPoint = this.chartEngine.pixelToData(point); this.chartEngine.isDrawing = true; this.chartEngine.drawingStartPoint = dataPoint; this.chartEngine.currentDrawing = { id: `drawing_${Date.now()}`, type: this.chartEngine.drawingMode, symbol: this.chartEngine.currentSymbolObject.token, tradingsymbol: this.chartEngine.currentSymbolObject.tradingsymbol, start: dataPoint, end: dataPoint, style: this.chartEngine.drawingStyles[this.chartEngine.drawingMode] || this.chartEngine.drawingStyles.line, created: new Date().toISOString() }; console.log('Trace: Drawing started:', this.chartEngine.currentDrawing.type); } handleDrawingMouseMove = (params) => { if (!this.chartEngine.isDrawing || !this.chartEngine.currentDrawing) return; const point = [params.offsetX, params.offsetY]; const dataPoint = this.chartEngine.pixelToData(point); this.chartEngine.currentDrawing.end = dataPoint; this.chartEngine.updateDrawingPreview(); } handleDrawingMouseUp = (params) => { if (!this.chartEngine.isDrawing || !this.chartEngine.currentDrawing) return; const point = [params.offsetX, params.offsetY]; const dataPoint = this.chartEngine.pixelToData(point); this.chartEngine.currentDrawing.end = dataPoint; // Validate drawing size for drag-based drawings if (['line', 'rectangle', 'circle'].includes(this.chartEngine.currentDrawing.type)) { if (this.mouseState.dragDistance < 5) { console.log('Trace: Drawing too small, cancelling'); this.cancelDrawing(); return; } } // Complete drawing const drawings = this.chartEngine.getCurrentDrawings(); drawings.push(this.chartEngine.currentDrawing); console.log('Trace: Drawing completed:', this.chartEngine.currentDrawing.type); this.finishDrawing(); this.chartEngine.updateDrawings(); } handleDrawingClick = (params) => { // Handle single-click drawings (horizontal/vertical lines) const point = [params.offsetX, params.offsetY]; const dataPoint = this.chartEngine.pixelToData(point); const drawing = { id: `drawing_${Date.now()}`, type: this.chartEngine.drawingMode, symbol: this.chartEngine.currentSymbolObject.token, tradingsymbol: this.chartEngine.currentSymbolObject.tradingsymbol, start: dataPoint, end: dataPoint, style: this.chartEngine.drawingStyles[this.chartEngine.drawingMode], created: new Date().toISOString() }; const drawings = this.chartEngine.getCurrentDrawings(); drawings.push(drawing); this.finishDrawing(); this.chartEngine.updateDrawings(); } // ============================================================================= // DRAWING EDIT HANDLERS // ============================================================================= /** * Drawing edit mode handlers */ handleDrawingEditMouseDown = (params) => { if (this.mouseState.button !== 0) return; // Left button only const point = [params.offsetX, params.offsetY]; const drawing = this.chartEngine.getDrawingAtPoint(point); if (drawing && ['rectangle', 'circle'].includes(drawing.type)) { this.chartEngine.isDraggingDrawing = true; this.chartEngine.selectedDrawing = drawing; this.chartEngine.originalDrawingPosition = { start: { ...drawing.start }, end: { ...drawing.end } }; const startPixel = this.chartEngine.chart.convertToPixel({ gridIndex: 0 }, [drawing.start.dataIndex, drawing.start.price]); this.chartEngine.drawingDragOffset = { x: point[0] - startPixel[0], y: point[1] - startPixel[1] }; // Show drag overlay const overlay = document.getElementById('drawingDragOverlay'); if (overlay) { overlay.style.display = 'block'; } } } handleDrawingEditMouseMove = (params) => { if (!this.chartEngine.isDraggingDrawing) return; const point = [params.offsetX, params.offsetY]; this.chartEngine.dragDrawing(point); } handleDrawingEditMouseUp = (params) => { if (this.chartEngine.isDraggingDrawing) { this.chartEngine.stopDraggingDrawing(); } } handleDrawingEditClick = (params) => { // Handle drawing selection/deselection const point = [params.offsetX, params.offsetY]; const drawing = this.chartEngine.getDrawingAtPoint(point); if (drawing) { console.log('Trace: Drawing selected:', drawing.id); this.chartEngine.selectedDrawing = drawing; } else { console.log('Trace: Drawing deselected'); this.chartEngine.selectedDrawing = null; } } // ============================================================================= // DRAWING HELPERS // ============================================================================= /** * Cancel current drawing operation */ cancelDrawing = () => { this.chartEngine.isDrawing = false; this.chartEngine.currentDrawing = null; this.chartEngine.drawingStartPoint = null; this.chartEngine.updateDrawings(); } /** * Finish current drawing operation */ finishDrawing = () => { this.chartEngine.isDrawing = false; this.chartEngine.currentDrawing = null; this.chartEngine.drawingStartPoint = null; setTimeout(() => { this.chartEngine.setDrawingMode(null); }, 200); // Allow UI to update before clearing // Clear drawing tool UI document.querySelectorAll('[data-drawing]').forEach(d => { d.classList.remove('active'); }); } // ============================================================================= // MODE MANAGEMENT // ============================================================================= /** * Switch event handling mode * @param {string} mode - 'chart', 'drawing', 'drawingEdit' */ setEventMode = (mode) => { if (this.eventMode === mode) return; console.log(`Trace: Event mode changing from ${this.eventMode} to ${mode}`); // Cleanup current mode state this.resetMouseState(); // Cancel any active operations if (this.chartEngine.isDrawing) { this.cancelDrawing(); } if (this.chartEngine.isDraggingDrawing) { this.chartEngine.stopDraggingDrawing(); } // Set new mode const oldMode = this.eventMode; this.eventMode = mode; // Update chart interaction settings this.updateChartInteraction(); // Update cursor this.updateCursor(); console.log(`Trace: Event mode changed from ${oldMode} to ${mode}`); } /** * Update chart interaction settings based on current mode */ updateChartInteraction = () => { if (!this.chart) return; try { const option = this.chart.getOption(); const hasVolume = this.chartEngine.activeIndicators.has('volume') || this.chartEngine.settings.volumeEnabled; const hasOscillators = Array.from(this.chartEngine.activeIndicators.values()).some(indicator => indicator.getGridIndex && indicator.getGridIndex(hasVolume) > 1 ); let xAxisIndices = [0]; if (hasVolume) xAxisIndices.push(1); if (hasOscillators) xAxisIndices.push(hasVolume ? 2 : 1); // Enable/disable chart interaction based on mode const interactionEnabled = this.eventMode === 'chart'; this.chart.setOption({ dataZoom: [{ type: 'inside', disabled: !interactionEnabled, xAxisIndex: xAxisIndices, start: option.dataZoom?.[0]?.start || 70, end: option.dataZoom?.[0]?.end || 100, zoomLock: !interactionEnabled, moveOnMouseMove: interactionEnabled, moveOnMouseWheel: interactionEnabled, preventDefaultMouseMove: !interactionEnabled }] }); console.log(`Trace: Chart interaction ${interactionEnabled ? 'enabled' : 'disabled'} for mode ${this.eventMode}`); } catch (error) { console.error('Trace: Error updating chart interaction:', error); } } /** * Update cursor based on current mode */ updateCursor = () => { const chartDom = document.getElementById('traceChart'); if (!chartDom) return; switch (this.eventMode) { case 'chart': chartDom.style.cursor = 'default'; break; case 'drawing': chartDom.style.cursor = 'crosshair'; break; case 'drawingEdit': chartDom.style.cursor = 'default'; break; } } // ============================================================================= // PERFORMANCE MONITORING // ============================================================================= /** * Get performance statistics */ getPerformanceStats = () => { return { eventMode: this.eventMode, mouseState: { ...this.mouseState }, throttleRate: this.moveThrottleMs, lastMoveTime: this.lastMoveTime }; } /** * Update performance settings */ setPerformanceSettings = (settings) => { if (settings.throttleMs) { this.moveThrottleMs = Math.max(8, Math.min(50, settings.throttleMs)); // 8-50ms range console.log(`Trace: Mouse move throttle set to ${this.moveThrottleMs}ms`); } } // ============================================================================= // CLEANUP // ============================================================================= /** * Cleanup all event handlers and state */ cleanup = () => { console.log('Trace: Cleaning up event manager'); if (this.zr) { this.zr.off('mousedown', this.handleMouseDown); this.zr.off('mousemove', this.handleMouseMove); this.zr.off('mouseup', this.handleMouseUp); this.zr.off('mouseleave', this.handleMouseLeave); this.zr.off('click', this.handleClick); this.zr.off('contextmenu', this.handleContextMenu); } document.removeEventListener('mouseup', this.handleGlobalMouseUp); this.resetMouseState(); this.eventMode = 'chart'; console.log('Trace: Event manager cleanup complete'); } } /** * TraceChartEngine - Core Chart Engine * * @author Shobhit Srivastava * @license Apache 2.0 * * Main chart engine handling chart rendering, indicators, drawings, and interactions. * Preserves all original functionality while being modularized for library use. */ class TraceChartEngine { constructor(dependencies = {}) { const { config = null, quoteFeed = null, demoGenerator = null, marketHours = null, } = dependencies; // Inject dependencies immediately this.config = config; this.quoteFeed = quoteFeed; this.demoGenerator = demoGenerator; this.marketHours = marketHours; // Chart instance and data this.chart = null; this.rawData = []; this.processedData = []; this.currentSymbolObject = null; this.currentTimeframe = '1D'; this.currentChartType = 'candlestick'; // Technical indicators this.indicatorFactory = dependencies.indicatorFactory; this.activeIndicators = new Map(); // indicatorId -> indicator instance // Update and polling this.updateInterval = null; this.activeFilter = 'all'; this.isUpdating = false; this.statusInterval = null; // API and fallback settings this.apiEnabled = false; this.fallbackToDemo = true; this.lastTickTimestamp = null; // Drawing tools state this.drawingMode = null; this.drawingsPerSymbol = new Map(); this.currentDrawing = null; this.isDrawing = false; this.drawingStartPoint = null; // Drawing styles configuration this.drawingStyles = { line: { stroke: 'rgb(153, 102, 255)', lineWidth: 2 }, rectangle: { stroke: 'rgba(0, 137, 208, 1)', lineWidth: 2, fill: 'rgba(0, 137, 208, 0.1)' }, horizontalLine: { stroke: 'rgb(254, 213, 28)', lineWidth: 1, lineDash: [5, 5] }, verticalLine: { stroke: 'rgb(22, 127, 57)', lineWidth: 1, lineDash: [5, 5] } }; // Drawing interaction state this.hoveredDrawing = null; this.selectedDrawing = null; this.isDraggingDrawing = false; this.drawingDragOffset = null; this.contextMenuDrawing = null; // Chart interaction modes this.interactionMode = 'chart'; this.chartHandlers = null; this.eventManager = null; // LTP line configuration for native ECharts implementation // Uses markLine and y-axis formatting instead of custom graphics this.ltpLine = { price: null, // Current LTP price enabled: this.config?.ltpLine?.enabled ?? true, style: { // Native markLine style configuration stroke: this.config?.ltpLine?.style?.stroke || '#5B9A5D', lineWidth: this.config?.ltpLine?.style?.lineWidth || 1, lineDash: this.config?.ltpLine?.style?.lineDash || [4, 2], opacity: this.config?.ltpLine?.style?.opacity || 0.85 }, label: { // Native y-axis label highlighting configuration enabled: this.config?.ltpLine?.label?.enabled ?? true, backgroundColor: this.config?.ltpLine?.label?.backgroundColor || '#5B9A5D', textColor: this.config?.ltpLine?.label?.textColor || '#ffffff', fontSize: this.config?.ltpLine?.label?.fontSize || 11, fontWeight: this.config?.ltpLine?.label?.fontWeight || 'bold' } }; // Interaction state tracking this.userInteraction = { isActive: false, // Is user currently interacting? type: null, // 'drag', 'zoom', 'drawing', null startTime: null, // When interaction started pausedUpdates: [] // Queue of updates to apply after interaction }; // Chart appearance settings - merge with config if available this.settings = { theme: 'dark', backgroundColor: this.config?.theme?.backgroundColor || '#181818', gridColor: this.config?.theme?.gridColor || '#404040', bullishColor: this.config?.theme?.bullishColor || '#5B9A5D', bearishColor: this.config?.theme?.bearishColor || '#E25F5B', crosshairEnabled: this.config?.features?.crosshairEnabled ?? true, tooltipEnabled: this.config?.features?.tooltipEnabled ?? true, volumeEnabled: this.config?.features?.volumeEnabled ?? true, crosshairColor: this.config?.theme?.crosshairColor || '#BBBBBB', textColor: this.config?.theme?.textColor || '#BBBBBB' }; console.log('Trace: Starting initialization with dependencies...'); // Only initialize if we have the necessary dependencies if (this.config) { this.init(); } else { console.warn('Trace: TraceChartEngine created without config - delaying initialization'); } } /** * Start user interaction - pause all updates * @param {string} type - Type of interaction ('drag', 'zoom', 'drawing') */ startUserInteraction = (type = 'drag') => { if (this.userInteraction.isActive) { // Already active, just update type if different if (this.userInteraction.type !== type) { console.log(`Trace: Interaction type changed from ${this.userInteraction.type} to ${type}`); this.userInteraction.type = type; } return; } this.userInteraction.isActive = true; this.userInteraction.type = type; this.userInteraction.startTime = Date.now(); this.userInteraction.pausedUpdates = []; console.log(`Trace: User interaction started (${type}) - pausing updates`); // Clear any pending animation frames if (this.chartUpdateTimeout) { clearTimeout(this.chartUpdateTimeout); this.chartUpdateTimeout = null; } } /** * End user interaction - resume updates * @param {number} delay - Delay before resuming updates (default: 100ms) */ endUserInteraction = () => { if (!this.userInteraction.isActive) return; // Not active const duration = Date.now() - this.userInteraction.startTime; console.log(`Trace: User interaction ended (${this.userInteraction.type}) after ${duration}ms - resuming updates`); this.userInteraction.isActive = false; this.userInteraction.type = null; this.userInteraction.startTime = null; // Apply any queued updates immediately (no delay) this.applyQueuedUpdates(); } /** * Apply updates that were queued during interaction */ applyQueuedUpdates = () => { if (this.userInteraction.pausedUpdates.length === 0) { console.log('Trace: No queued updates to apply'); return; } console.log(`Trace: Applying ${this.userInteraction.pausedUpdates.length} queued updates`); // Analyze queued updates for optimal batching let hasChartUpdate = false; let hasLTPUpdate = false; let latestLTPPrice = null; let updateCount = { chart: 0, ltp: 0, indicators: 0 }; this.userInteraction.pausedUpdates.forEach(update => { updateCount[update.type] = (updateCount[update.type] || 0) + 1; switch (update.type) { case 'chart': hasChartUpdate = true; break; case 'ltp': hasLTPUpdate = true; latestLTPPrice = update.price; // Only keep latest price break; case 'indicators': hasChartUpdate = true; // Indicators are updated as part of chart update break; } }); console.log('Trace: Update summary:', updateCount); // Apply batched updates efficiently if (hasChartUpdate) { console.log('Trace: Applying batched chart update'); this.updateChart(); } // LTP updates are now part of chart update (via markLine) if (hasLTPUpdate && latestLTPPrice && this.ltpLine.enabled) { this.ltpLine.price = latestLTPPrice; } // Clear queue this.userInteraction.pausedUpdates = []; console.log('Trace: All queued updates applied and cleared'); } /** * Queue an update to be applied after interaction ends * @param {string} type - Update type ('chart', 'ltp', 'indicators') * @param {Object} data - Update data */ queueUpdate = (type, data = {}) => { if (!this.userInteraction.isActive) { // Not in interaction mode, apply immediately return false; } // Add to queue (limit queue size to prevent memory issues) this.userInteraction.pausedUpdates.push({ type, timestamp: Date.now(), ...data }); // Keep only last 10 updates to prevent memory buildup if (this.userInteraction.pausedUpdates.length > 10) { this.userInteraction.pausedUpdates = this.userInteraction.pausedUpdates.slice(-10); } return true; // Update was queued } /** * Enable/disable LTP line display * @param {boolean} enabled - Whether to show LTP line */ setLTPLineEnabled = (enabled) => { this.ltpLine.enabled = enabled; console.log('Trace: LTP line', enabled ? 'enabled' : 'disabled'); // Just trigger chart update - markLine will be included/excluded automatically if (this.processedData.length > 0) { this.updateChart(); } } /** * Configure LTP line styling * @param {Object} style - Style configuration */ setLTPLineStyle = (style) => { this.ltpLine.style = { ...this.ltpLine.style, ...style }; console.log('Trace: LTP line style updated:', style); // Update chart to apply new markLine styles if (this.ltpLine.price && this.ltpLine.enabled) { this.updateChart(); } } /** * Configure LTP label styling * @param {Object} labelConfig - Label configuration */ setLTPLabelStyle = (labelConfig) => { this.ltpLine.label = { ...this.ltpLine.label, ...labelConfig }; console.log('Trace: LTP label style updated:', labelConfig); // Update chart to apply new y-axis label formatting if (this.ltpLine.price && this.ltpLine.enabled) { this.updateChart(); } } /** * Update LTP line using native ECharts markLine * Called on every tick to maintain real-time price line * */ updateLTPLine = (newPrice) => { // Skip update if user is interacting if (this.userInteraction.isActive) { this.queueUpdate('ltp', { price: newPrice }); return; } if (!this.chart || !newPrice || !this.ltpLine.enabled) { return; } try { // Just store the price - markLine will be updated in next chart render const oldPrice = this.ltpLine.price; this.ltpLine.price = newPrice; console.log(`Trace: LTP updated from ${oldPrice} to ${newPrice} (using native markLine)`); // The markLine will be automatically updated in the next chart update cycle } catch (error) { console.error('Trace: Error updating LTP line:', error); // Graceful fallback - continue without LTP line this.ltpLine.enabled = false; } } // Get current symbol's drawings getCurrentDrawings = () => { if (!this.currentSymbolObject) return []; const token = this.currentSymbolObject.token; if (!this.drawingsPerSymbol.has(token)) { this.drawingsPerSymbol.set(token, []); } return this.drawingsPerSymbol.get(token); } setDrawingMode = (mode) => { this.drawingMode = mode; if (mode) { this.setInteractionMode('drawing'); } else { // If coming from drawingEdit, reset to chart mode if (this.interactionMode !== 'chart') { this.setInteractionMode('chart'); } else { this.setInteractionMode('drawingEdit'); } } } initDrawingSystem = () => { this.setInteractionMode('drawingEdit'); this.chart.getDom().addEventListener('contextmenu', (e) => { e.preventDefault(); if (this.interactionMode !== 'drawingEdit') return; const rect = this.chart.getDom().getBoundingClientRect(); const point = [e.clientX - rect.left, e.clientY - rect.top]; const drawing = this.getDrawingAtPoint(point); if (drawing) { this.showDrawingContextMenu(drawing, e.clientX, e.clientY); } else { this.hideDrawingContextMenu(); } }); document.addEventListener('click', () => { this.hideDrawingContextMenu(); }); } getFallbackDataPoint = () => { const lastIndex = this.processedData.length - 1; return { dataIndex: lastIndex, timestamp: this.processedData[lastIndex].timestamp, price: this.processedData[lastIndex].close }; } pixelToData = (pixelPoint) => { try { const pointInGrid = this.chart.convertFromPixel({ gridIndex: 0 }, pixelPoint); if (!pointInGrid) { console.error('Could not convert pixel to data coordinates'); return this.getFallbackDataPoint(); } const dataIndex = Math.round(pointInGrid[0]); const clampedIndex = Math.max(0, Math.min(dataIndex, this.processedData.length - 1)); const price = pointInGrid[1]; return { dataIndex: clampedIndex, timestamp: this.processedData[clampedIndex].timestamp, price: price }; } catch (error) { console.error('Error in pixelToData:', error); return this.getFallbackDataPoint(); } } updateDrawingPreview = () => { if (!this.currentDrawing) return; const existingDrawings = this.buildAllDrawingGraphics(); const tempGraphic = this.buildDrawingGraphic(this.currentDrawing); if (tempGraphic) { tempGraphic.id = 'drawing_preview'; tempGraphic.style = { ...tempGraphic.style, opacity: 0.7 }; this.chart.setOption({ graphic: [...existingDrawings, tempGraphic] }); } } refreshChartAndDrawings = () => { this.updateChart(); this.updateDrawings(); } buildDrawingGraphic = (drawing) => { try { const startIndex = drawing.start.dataIndex; const endIndex = drawing.end.dataIndex; if (startIndex < 0 || startIndex >= this.processedData.length) { console.error('Invalid start index:', startIndex); return null; } if (endIndex < 0 || endIndex >= this.processedData.length) { console.error('Invalid end index:', endIndex); return null; } const startPixel = this.chart.convertToPixel({ gridIndex: 0 }, [startIndex, drawing.start.price]); const endPixel = this.chart.convertToPixel({ gridIndex: 0 }, [endIndex, drawing.end.price]); const option = this.chart.getOption(); const grid = option.grid[0]; const chartDom = this.chart.getDom(); const rect = chartDom.getBoundingClientRect(); const gridLeft = parseFloat(grid.left.replace('%', '')) * rect.width / 100; const gridRight = parseFloat(grid.right.replace('%', '')) * rect.width / 100; const gridTop = parseFloat(grid.top.replace('%', '')) * rect.height / 100; const gridBottom = parseFloat(grid.bottom.replace('%', '')) * rect.height / 100; const gridWidth = rect.width - gridLeft - gridRight; const gridHeight = rect.height - gridTop - gridBottom; const defaultStyles = this.drawingStyles[drawing.type] || this.drawingStyles.line; const style = drawing.style || defaultStyles; switch (drawing.type) { case 'line': return { type: 'line', id: drawing.id, shape: { x1: startPixel[0], y1: startPixel[1], x2: endPixel[0], y2: endPixel[1] }, style: { stroke: style.stroke || '#00ff00', lineWidth: style.lineWidth || 2 }, z: 100 }; case 'rectangle': return { type: 'rect', id: drawing.id, shape: { x: Math.min(startPixel[0], endPixel[0]), y: Math.min(startPixel[1], endPixel[1]), width: Math.abs(endPixel[0] - startPixel[0]), height: Math.abs(endPixel[1] - startPixel[1]) }, style: { stroke: style.stroke || '#00ff00', lineWidth: style.lineWidth || 2, fill: style.fill || 'rgba(0, 255, 0, 0.1)' }, z: 100 }; case 'circle': const radius = Math.sqrt( Math.pow(endPixel[0] - startPixel[0], 2) + Math.pow(endPixel[1] - startPixel[1], 2) ); return { type: 'circle', id: drawing.id, shape: { cx: startPixel[0], cy: startPixel[1], r: radius }, style: { stroke: style.stroke || 'rgb(153, 102, 255)', lineWidth: style.lineWidth || 2, fill: style.fill || 'rgba(153, 102, 255, 0.1)' }, z: 100 }; case 'horizontalLine': return { type: 'line', id: drawing.id, shape: { x1: gridLeft, y1: startPixel[1], x2: gridLeft + gridWidth, y2: startPixel[1] }, style: { stroke: style.stroke || '#ffff00', lineWidth: style.lineWidth || 2, lineDash: style.lineDash || [5, 5] }, z: 100 }; case 'verticalLine': return { type: 'line', id: drawing.id, shape: { x1: startPixel[0], y1: gridTop, x2: startPixel[0], y2: gridTop + gridHeight }, style: { stroke: style.stroke || '#ffff00', lineWidth: style.lineWidth || 2, lineDash: style.lineDash || [5, 5] }, z: 100 }; default: console.warn('Unknown drawing type:', drawing.type); return null; } } catch (error) { console.error('Error building drawing graphic:', error, drawing); return null; } } buildAllDrawingGraphics = () => { const drawings = this.getCurrentDrawings(); return drawings .map(drawing => this.buildDrawingGraphic(drawing)) .filter(graphic => graphic !== null); } clearDrawings = () => { if (!this.currentSymbolObject) return; const token = this.currentSymbolObject.token; this.drawingsPerSymbol.set(token, []); this.updateDrawings(); console.log('Trace: Cleared all drawings for', this.currentSymbolObject.tradingsymbol); } getDrawingInfo = () => { const allDrawings = []; this.drawingsPerSymbol.forEach((drawings, token) => { drawings.forEach(drawing => { allDrawings.push({ symbol: drawing.tradingsymbol || token, type: drawing.type, created: drawing.created }); }); }); return allDrawings; } exportDrawings = () => { const data = { version: 1, drawings: Object.fromEntries(this.drawingsPerSymbol), exported: new Date().toISOString() }; return JSON.stringify(data); } importDrawings = (jsonData) => { try { const data = JSON.parse(jsonData); if (data.version !== 1) { throw new Error('Unsupported drawing data version'); } this.drawingsPerSymbol = new Map(Object.entries(data.drawings)); this.updateChart(); console.log('Imported drawings for', this.drawingsPerSymbol.size, 'symbols'); return true; } catch (error) { console.error('Failed to import drawings:', error); return false; } } /** * Initialize chart engine - sets up DOM, loads data, and starts real-time updates * Main initialization flow for the entire chart system */ init = async () => { try { console.log('Trace: Initializing with config:', this.config ? 'Available' : 'Missing'); await this.waitForDOM(); let params = await window.chartUtils.parseQueryParam(window.location.href); window.chartUtils.setGlobals(params); window.chartGlobalVars.stateAPI = window.parent?.stateAPI || window.chartGlobalVars.stateAPI; this.setDefaultSymbol(); this.initChart(); await this.loadHistoricalData(); this.updateChart(); this.setupEventListeners(); this.startRealTimeUpdates(); this.initializeUIState(); document.getElementById('loadingIndicator').style.display = 'none'; this.updateStatus('success'); console.log('Trace: Initialization complete'); } catch (error) { console.error('Trace: Error:', error); this.showError(`Failed to initialize chart: ${error.message}`); } } /** * Set default symbol using instrument manager or fallback to demo data */ setDefaultSymbol = () => { if (window?.chartGlobalVars.stateAPI?.instrumentManager) { const defaultSymbol = window?.chartGlobalVars.stateAPI?.instrumentManager.get('RELIANCE', 'NSE'); if (defaultSymbol) { defaultSymbol.token ??= defaultSymbol.instrumentToken || 'RELIANCE_NSE'; this.currentSymbolObject = defaultSymbol; console.log('Trace: Set default symbol:', defaultSymbol.tradingsymbol, 'with basePrice:', defaultSymbol.basePrice); return; } } // Fallback to demo data if instrument manager not available if (this.demoGenerator) { const defaultDemoSymbol = this.demoGenerator.getAllSymbols()[0]; if (defaultDemoSymbol) { this.currentSymbolObject = defaultDemoSymbol; console.log('Trace: Set fallback demo symbol:', defaultDemoSymbol.tradingsymbol, 'with basePrice:', defaultDemoSymbol.basePrice); return; } } console.warn('Trace: No symbol source available, using hardcoded fallback'); // Last resort hardcoded fallback this.currentSymbolObject = { token: 'RELIANCE_NSE', tradingsymbol: 'RELIANCE', exchange: 'NSE', segment: 'NSE', basePrice: 2450 }; } waitForDOM = () => { return new Promise((resolve) => { if (document.getElementById('traceChart')) { resolve(); } else { setTimeout(() => this.waitForDOM().then(resolve), 100); } }); } getGridConfig = () => { const hasVolume = this.activeIndicators.has('volume') || this.settings.volumeEnabled; // Pass actual hasVolume state to getGridIndex (was false) const hasOscillators = Array.from(this.activeIndicators.values()).some(indicator => indicator.getGridIndex && indicator.getGridIndex(hasVolume) > 1 ); if (hasOscillators && hasVolume) { return [ { left: '3%', right: '3%', top: '10%', bottom: '35%', containLabel: true }, // Main chart { left: '3%', right: '3%', top: '70%', bottom: '25%', containLabel: true }, // Volume { left: '3%', right: '3%', top: '80%', bottom: '5%', containLabel: true } // Oscillators ]; } else if (hasOscillators && !hasVolume) { return [ { left: '3%', right: '3%', top: '10%', bottom: '25%', containLabel: true }, // Main chart { left: '3%', right: '3%', top: '80%', bottom: '5%', containLabel: true } // Oscillators ]; } else if (!hasOscillators && hasVolume) { return [ { left: '3%', right: '3%', top: '10%', bottom: '25%', containLabel: true }, // Main chart { left: '3%', right: '3%', top: '80%', bottom: '5%', containLabel: true } // Volume ]; } else { return [ { left: '3%', right: '3%', top: '10%', bottom: '5%', containLabel: true } // Main cha