Source: renderer/managers/VisualyzerManager.js

/**
 * Visualyzer Manager - Interactive Graph Visualization
 * Uses D3.js for force-directed graph rendering
 */

class VisualyzerManager {
  constructor() {
    this.currentGraph = null;
    this.parsedData = null;
    this.svg = null;
    this.simulation = null;
    this.selectedNode = null;
    this.expandedNodes = new Set();
    this.visibleNodes = new Set();
    this.visibleEdges = new Set();
    
    // Color mapping for node types
    this.typeColors = {
      'container': { fill: '#ffa657', stroke: '#f0883e', label: 'Container' },
      'F': { fill: '#58a6ff', stroke: '#1f6feb', label: 'Function' },
      'O': { fill: '#56d364', stroke: '#238636', label: 'Object' },
      'default': { fill: '#8b949e', stroke: '#484f58', label: 'Other' }
    };
    
    this.initializeElements();
    this.setupEventListeners();
  }

  /**
   * Get node color based on type
   * @param {Object} node - Node data
   * @returns {Object} Color object with fill and stroke
   */
  getNodeColor(node) {
    if (node.isContainer) {
      return this.typeColors['container'];
    }
    if (node.type && this.typeColors[node.type]) {
      return this.typeColors[node.type];
    }
    return this.typeColors['default'];
  }

  /**
   * Initialize DOM elements
   */
  initializeElements() {
    this.dropzone = document.querySelector('.visualyzer-dropzone');
    this.canvas = document.querySelector('.visualyzer-canvas');
    this.controls = document.querySelector('.visualyzer-controls');
    this.info = document.querySelector('.visualyzer-info');
    this.filename = document.querySelector('.visualyzer-filename');
    this.stats = document.querySelector('.visualyzer-stats');
  }

  /**
   * Setup event listeners
   */
  setupEventListeners() {
    // Drag and drop
    this.dropzone.addEventListener('dragover', (e) => {
      e.preventDefault();
      this.dropzone.classList.add('dragover');
    });

    this.dropzone.addEventListener('dragleave', () => {
      this.dropzone.classList.remove('dragover');
    });

    this.dropzone.addEventListener('drop', (e) => {
      e.preventDefault();
      this.dropzone.classList.remove('dragover');
      
      const file = e.dataTransfer.files[0];
      if (file && file.name.endsWith('.dot')) {
        this.handleFile(file);
      } else {
        alert('Please drop a .dot file');
      }
    });

    // File input
    const fileInput = document.createElement('input');
    fileInput.type = 'file';
    fileInput.accept = '.dot';
    fileInput.style.display = 'none';
    fileInput.addEventListener('change', (e) => {
      const file = e.target.files[0];
      if (file) {
        this.handleFile(file);
      }
    });
    document.body.appendChild(fileInput);

    this.dropzone.addEventListener('click', () => {
      fileInput.click();
    });
  }

  /**
   * Handle dropped or selected file
   * @param {File} file - DOT file
   * @returns {Promise<void>}
   */
  async handleFile(file) {
    try {
      const content = await this.readFile(file);
      await this.renderGraph(content, file.name);
    } catch (error) {
      this.showError(`Error rendering graph: ${error.message}`);
      console.error('Error:', error);
    }
  }

  /**
   * Read file content
   * @param {File} file - File to read
   * @returns {Promise<string>} File content
   */
  readFile(file) {
    return new Promise((resolve, reject) => {
      const reader = new FileReader();
      reader.onload = (e) => resolve(e.target.result);
      reader.onerror = (e) => reject(new Error('Failed to read file'));
      reader.readAsText(file);
    });
  }

  /**
   * Parse DOT content into graph data
   * @param {string} dotContent - DOT file content
   * @returns {Object} Parsed graph data
   */
  parseDotContent(dotContent) {
    const nodes = new Map();
    const edges = [];
    
    // Extract node definitions with labels
    const nodeRegex = /(\w+)\s*\[label="({[^"]+}|[^"]+)"\]/g;
    let match;
    
    while ((match = nodeRegex.exec(dotContent)) !== null) {
      const nodeId = match[1];
      const label = match[2];
      
      // Parse label
      const parsedLabel = this.parseLabel(label);
      
      // Create main node
      nodes.set(nodeId, {
        id: nodeId,
        label: parsedLabel.title,
        isContainer: parsedLabel.items.length > 0,
        type: null,
        address: null
      });
      
      // Create child nodes from fields
      parsedLabel.items.forEach((field, index) => {
        const childId = `${nodeId}_field_${index}`;
        nodes.set(childId, {
          id: childId,
          label: field.name,
          isContainer: false,
          type: field.address,
          address: field.value
        });
        
        // Create edge from parent to child
        edges.push({
          source: nodeId,
          target: childId
        });
      });
    }
    
    // Extract explicit edges (connections)
    const edgeRegex = /(\w+)\s*->\s*(\w+)/g;
    while ((match = edgeRegex.exec(dotContent)) !== null) {
      edges.push({
        source: match[1],
        target: match[2]
      });
    }
    
    return {
      nodes: Array.from(nodes.values()),
      edges: edges
    };
  }

  /**
   * Parse label into structured data
   * @param {string} label - Node label
   * @returns {Object} Parsed label data
   */
  /**
   * Parse node label text
   * @param {string} label - Label text to parse
   * @returns {Object} Parsed label with title and fields
   */
  parseLabel(label) {
    // Check if it's a record-style label
    if (label.startsWith('{') && label.endsWith('}')) {
      return this.parseRecordLabel(label);
    }
    
    // Simple label
    return {
      title: label,
      items: []
    };
  }

  /**
   * Parse record-style label into structured data
   * @param {string} label - Record label like "{Title|{field1|value1}|{field2|value2}}"
   * @returns {Object} Parsed label data
   */
  /**
   * Parse record-style label into structured fields
   * @param {string} label - Record label text
   * @returns {Object} Object with title and fields array
   */
  parseRecordLabel(label) {
    // Remove outer braces
    let content = label.slice(1, -1);
    
    // Split by top-level pipes (not inside braces)
    const parts = [];
    let depth = 0;
    let current = '';
    
    for (let i = 0; i < content.length; i++) {
      const char = content[i];
      
      if (char === '\\' && i + 1 < content.length) {
        // Skip escaped character
        current += content[i + 1];
        i++;
        continue;
      }
      
      if (char === '{') depth++;
      else if (char === '}') depth--;
      else if (char === '|' && depth === 0) {
        parts.push(current);
        current = '';
        continue;
      }
      
      current += char;
    }
    if (current) parts.push(current);
    
    const title = parts[0] || 'Unknown';
    const items = [];
    
    // Parse remaining parts as field records
    for (let i = 1; i < parts.length; i++) {
      const part = parts[i].trim();
      if (part.startsWith('{') && part.endsWith('}')) {
        const fieldContent = part.slice(1, -1);
        const fieldParts = fieldContent.split('|').map(p => p.trim());
        
        items.push({
          name: fieldParts[0] || '',
          address: fieldParts[1] || '',
          value: fieldParts[2] || ''
        });
      }
    }
    
    return { title, items };
  }

  /**
   * Render interactive graph using D3.js
   * @param {string} dotContent - DOT file content
   * @param {string} filename - File name
   */
  async renderGraph(dotContent, filename) {
    try {
      // Check if D3 is loaded
      if (typeof d3 === 'undefined') {
        throw new Error('D3.js library not loaded. Please check your internet connection.');
      }

      // Basic validation
      const trimmedContent = dotContent.trim();
      if (!trimmedContent) {
        throw new Error('DOT file is empty');
      }

      // Parse DOT content
      const graphData = this.parseDotContent(dotContent);
      
      if (graphData.nodes.length === 0) {
        throw new Error('No nodes found in DOT file');
      }
      
      this.parsedData = graphData;
      this.currentGraph = dotContent;
      
      // Create D3 force-directed graph
      this.createForceGraph(graphData, filename);
      
      // Show controls and info
      this.dropzone.style.display = 'none';
      this.controls.style.display = 'flex';
      this.info.style.display = 'flex';
      
      // Update info
      this.filename.textContent = filename;
      this.stats.textContent = `${graphData.nodes.length} nodes, ${graphData.edges.length} connections`;
      
    } catch (error) {
      throw error;
    }
  }

  /**
   * Create force-directed graph with D3.js
   * @param {Object} data - Graph data
   * @param {string} filename - File name
   */
  /**
   * Create D3.js force-directed graph visualization
   * @param {Object} data - Graph data with nodes and edges
   * @param {string} filename - Name of the DOT file
   * @returns {void}
   */
  createForceGraph(data, filename) {
    // Clear canvas
    this.canvas.innerHTML = '';
    
    // Store full graph data
    this.fullGraphData = data;
    
    // Find root nodes (nodes with no incoming edges)
    const childNodeIds = new Set(data.edges.map(e => e.target));
    const rootNodes = data.nodes.filter(n => !childNodeIds.has(n.id));
    
    // If no clear root, use first few nodes
    if (rootNodes.length === 0) {
      rootNodes.push(...data.nodes.slice(0, Math.min(3, data.nodes.length)));
    }
    
    // Initialize visible nodes with roots
    this.visibleNodes.clear();
    rootNodes.forEach(n => this.visibleNodes.add(n.id));
    
    // Get canvas dimensions
    const width = this.canvas.clientWidth;
    const height = this.canvas.clientHeight;
    
    // Create SVG
    const svg = d3.select(this.canvas)
      .append('svg')
      .attr('width', width)
      .attr('height', height)
      .attr('viewBox', [0, 0, width, height]);
    
    // Add zoom behavior
    const g = svg.append('g');
    
    const zoom = d3.zoom()
      .scaleExtent([0.1, 4])
      .on('zoom', (event) => {
        g.attr('transform', event.transform);
      });
    
    svg.call(zoom);
    
    // Store zoom and svg references
    this.zoom = zoom;
    this.svg = svg;
    
    // Create arrow markers for edges
    svg.append('defs').append('marker')
      .attr('id', 'arrowhead')
      .attr('viewBox', '-0 -5 10 10')
      .attr('refX', 25)
      .attr('refY', 0)
      .attr('orient', 'auto')
      .attr('markerWidth', 8)
      .attr('markerHeight', 8)
      .append('svg:path')
      .attr('d', 'M 0,-5 L 10 ,0 L 0,5')
      .attr('fill', '#848d97');
    
    // Store references for updates
    this.g = g;
    this.width = width;
    this.height = height;
    
    // Create legend
    this.createLegend(svg);
    
    // Initial render with only visible nodes
    this.updateGraph();
  }

  /**
   * Create legend for node types
   * @param {Object} svg - SVG element
   */
  /**
   * Create legend showing node type colors
   * @param {Object} svg - D3.js SVG selection
   * @returns {void}
   */
  createLegend(svg) {
    const legend = svg.append('g')
      .attr('class', 'legend')
      .attr('transform', `translate(20, ${this.height - 120})`);
    
    // Background
    legend.append('rect')
      .attr('x', 0)
      .attr('y', 0)
      .attr('width', 120)
      .attr('height', 100)
      .attr('fill', '#161b22')
      .attr('stroke', '#30363d')
      .attr('stroke-width', 1)
      .attr('rx', 4);
    
    // Title
    legend.append('text')
      .attr('x', 10)
      .attr('y', 18)
      .attr('fill', '#f0f6fc')
      .attr('font-size', '12px')
      .attr('font-weight', 'bold')
      .text('Node Types');
    
    // Legend items
    const items = [
      { type: 'container', label: 'Container' },
      { type: 'F', label: 'Function' },
      { type: 'O', label: 'Object' },
      { type: 'default', label: 'Other' }
    ];
    
    items.forEach((item, index) => {
      const yPos = 35 + index * 20;
      const colors = this.typeColors[item.type];
      
      // Circle
      legend.append('circle')
        .attr('cx', 15)
        .attr('cy', yPos)
        .attr('r', 6)
        .attr('fill', colors.fill)
        .attr('stroke', colors.stroke)
        .attr('stroke-width', 1.5);
      
      // Label
      legend.append('text')
        .attr('x', 28)
        .attr('y', yPos + 4)
        .attr('fill', '#c9d1d9')
        .attr('font-size', '11px')
        .text(item.label);
    });
  }

  /**
   * Update graph to show only visible nodes and edges
   */
  /**
   * Update graph visualization with current visible nodes and edges
   * @returns {void}
   */
  updateGraph() {
    const data = this.fullGraphData;
    
    // Filter visible nodes
    const visibleNodesData = data.nodes.filter(n => this.visibleNodes.has(n.id));
    
    // Filter visible edges (both source and target must be visible)
    const visibleEdgesData = data.edges.filter(e => 
      this.visibleNodes.has(e.source.id || e.source) && 
      this.visibleNodes.has(e.target.id || e.target)
    );
    
    // Create or update force simulation
    if (this.simulation) {
      this.simulation.stop();
    }
    
    this.simulation = d3.forceSimulation(visibleNodesData)
      .force('link', d3.forceLink(visibleEdgesData)
        .id(d => d.id)
        .distance(150))
      .force('charge', d3.forceManyBody().strength(-400))
      .force('center', d3.forceCenter(this.width / 2, this.height / 2))
      .force('collision', d3.forceCollide().radius(40));
    
    // Update edges
    const link = this.g.selectAll('line')
      .data(visibleEdgesData, d => `${d.source.id || d.source}-${d.target.id || d.target}`);
    
    link.exit().remove();
    
    const linkEnter = link.enter().append('line')
      .attr('stroke', '#848d97')
      .attr('stroke-width', 2)
      .attr('marker-end', 'url(#arrowhead)')
      .attr('opacity', 0);
    
    const linkAll = linkEnter.merge(link);
    
    linkAll.transition()
      .duration(300)
      .attr('opacity', 1);
    
    // Update nodes
    const node = this.g.selectAll('g.node')
      .data(visibleNodesData, d => d.id);
    
    node.exit().remove();
    
    const nodeEnter = node.enter().append('g')
      .attr('class', 'node')
      .call(d3.drag()
        .on('start', (event, d) => this.dragStarted(event, d, this.simulation))
        .on('drag', (event, d) => this.dragged(event, d))
        .on('end', (event, d) => this.dragEnded(event, d, this.simulation)));
    
    // Add circles to new nodes
    nodeEnter.append('circle')
      .attr('r', 0)
      .attr('fill', d => this.getNodeColor(d).fill)
      .attr('stroke', d => this.getNodeColor(d).stroke)
      .attr('stroke-width', 2);
    
    // Add labels to new nodes
    nodeEnter.append('text')
      .text(d => d.label.length > 20 ? d.label.substring(0, 17) + '...' : d.label)
      .attr('x', 0)
      .attr('y', 35)
      .attr('text-anchor', 'middle')
      .attr('fill', '#f0f6fc')
      .attr('font-size', '12px')
      .style('pointer-events', 'none')
      .attr('opacity', 0);
    
    // Add expand indicator for nodes with children
    nodeEnter.each((d, i, nodes) => {
      const hasChildren = this.hasUnexpandedChildren(d);
      
      if (hasChildren) {
        // Green + for expandable nodes
        d3.select(nodes[i]).append('circle')
          .attr('class', 'expand-indicator')
          .attr('r', 6)
          .attr('cx', 18)
          .attr('cy', -18)
          .attr('fill', '#238636')
          .attr('stroke', '#2ea043')
          .attr('stroke-width', 1)
          .attr('opacity', 0);
        
        d3.select(nodes[i]).append('text')
          .attr('class', 'expand-icon')
          .attr('x', 18)
          .attr('y', -14)
          .attr('text-anchor', 'middle')
          .attr('fill', 'white')
          .attr('font-size', '10px')
          .attr('font-weight', 'bold')
          .style('pointer-events', 'none')
          .text('+')
          .attr('opacity', 0);
      }
    });
    
    const nodeAll = nodeEnter.merge(node);
    
    // Animate circles
    nodeAll.select('circle:not(.expand-indicator)')
      .transition()
      .duration(300)
      .attr('r', d => this.expandedNodes.has(d.id) ? 24 : 20)
      .attr('fill', d => {
        if (this.expandedNodes.has(d.id)) return '#a371f7'; // Purple when expanded
        return this.getNodeColor(d).fill;
      })
      .attr('stroke', d => {
        if (this.expandedNodes.has(d.id)) return '#8957e5';
        return this.getNodeColor(d).stroke;
      });
    
    // Animate labels
    nodeAll.select('text:not(.expand-icon)')
      .transition()
      .duration(300)
      .attr('opacity', 1);
    
    // Update expand indicators
    nodeAll.each((d, i, nodes) => {
      const hasChildren = this.hasUnexpandedChildren(d);
      const nodeGroup = d3.select(nodes[i]);
      
      if (hasChildren) {
        nodeGroup.select('.expand-indicator')
          .transition()
          .duration(300)
          .attr('opacity', 1);
        
        nodeGroup.select('.expand-icon')
          .transition()
          .duration(300)
          .attr('opacity', 1);
      } else {
        // No children, remove indicators
        nodeGroup.select('.expand-indicator').remove();
        nodeGroup.select('.expand-icon').remove();
      }
    });
    
    // Add click handler to nodes
    nodeAll.on('click', (event, d) => {
      event.stopPropagation();
      this.toggleNodeExpansion(d);
    });
    
    // Add hover effects
    nodeAll.on('mouseenter', function(event, d) {
      if (!d._expanded) {
        const baseColor = d3.select(this).select('circle:not(.expand-indicator)').attr('fill');
        d3.select(this).select('circle:not(.expand-indicator)')
          .attr('r', 24);
      }
    });
    
    nodeAll.on('mouseleave', function(event, d) {
      if (!d._expanded) {
        d3.select(this).select('circle:not(.expand-indicator)')
          .attr('r', 20);
      }
    });
    
    // Update positions on simulation tick
    this.simulation.on('tick', () => {
      linkAll
        .attr('x1', d => d.source.x)
        .attr('y1', d => d.source.y)
        .attr('x2', d => d.target.x)
        .attr('y2', d => d.target.y);
      
      nodeAll.attr('transform', d => `translate(${d.x},${d.y})`);
    });
  }

  /**
   * Check if node has unexpanded children
   * @param {Object} node - Node to check
   * @returns {boolean} Has unexpanded children
   */
  /**
   * Check if node has children that haven't been expanded
   * @param {Object} node - Node to check
   * @returns {boolean} True if node has unexpanded children
   */
  hasUnexpandedChildren(node) {
    const children = this.fullGraphData.edges
      .filter(e => (e.source.id || e.source) === node.id)
      .map(e => e.target.id || e.target);
    
    return children.some(childId => !this.visibleNodes.has(childId));
  }

  /**
   * Toggle node expansion
   * @param {Object} node - Node to toggle
   */
  /**
   * Toggle node expansion state (expand/collapse children)
   * @param {Object} node - Node to toggle
   * @returns {void}
   */
  toggleNodeExpansion(node) {
    const hasChildren = this.hasUnexpandedChildren(node);
    
    // If node has children, expand/collapse
    if (hasChildren || this.expandedNodes.has(node.id)) {
      if (this.expandedNodes.has(node.id)) {
        // Collapse: remove children
        this.collapseNode(node);
      } else {
        // Expand: show children
        this.expandNode(node);
      }
      
      // Update the graph
      this.updateGraph();
    } 
    // If node is a leaf (no children), show its metadata
    else if (node.type || node.address) {
      this.showNodeMetadata(node);
    }
    
    // Update stats
    const visibleCount = this.visibleNodes.size;
    const totalCount = this.fullGraphData.nodes.length;
    this.stats.textContent = `Showing ${visibleCount} of ${totalCount} nodes`;
  }

  /**
   * Expand node to show its children
   * @param {Object} node - Node to expand
   */
  /**
   * Expand node to show its children
   * @param {Object} node - Node to expand
   * @returns {void}
   */
  expandNode(node) {
    this.expandedNodes.add(node.id);
    node._expanded = true;
    
    // Find all children
    const children = this.fullGraphData.edges
      .filter(e => (e.source.id || e.source) === node.id)
      .map(e => e.target.id || e.target);
    
    // Add children to visible nodes
    children.forEach(childId => this.visibleNodes.add(childId));
  }

  /**
   * Collapse node to hide its children
   * @param {Object} node - Node to collapse
   */
  /**
   * Collapse node and hide its children recursively
   * @param {Object} node - Node to collapse
   * @returns {void}
   */
  collapseNode(node) {
    this.expandedNodes.delete(node.id);
    node._expanded = false;
    
    // Find all descendants (recursive)
    const toRemove = new Set();
    const findDescendants = (nodeId) => {
      const children = this.fullGraphData.edges
        .filter(e => (e.source.id || e.source) === nodeId)
        .map(e => e.target.id || e.target);
      
      children.forEach(childId => {
        toRemove.add(childId);
        if (this.expandedNodes.has(childId)) {
          findDescendants(childId);
        }
      });
    };
    
    findDescendants(node.id);
    
    // Remove descendants from visible nodes
    toRemove.forEach(nodeId => {
      this.visibleNodes.delete(nodeId);
      this.expandedNodes.delete(nodeId);
    });
  }

  /**
   * Show node metadata (type and address) as overlay
   * @param {Object} node - Node data
   */
  /**
   * Display node metadata in the info panel
   * @param {Object} node - Node whose metadata to display
   * @returns {void}
   */
  showNodeMetadata(node) {
    // Remove existing tooltip
    const existingTooltip = this.canvas.querySelector('.node-metadata-tooltip');
    if (existingTooltip) {
      existingTooltip.remove();
    }
    
    // Create tooltip
    const tooltip = document.createElement('div');
    tooltip.className = 'node-metadata-tooltip';
    
    const title = document.createElement('div');
    title.className = 'tooltip-title';
    title.textContent = node.label;
    tooltip.appendChild(title);
    
    if (node.type) {
      const typeDiv = document.createElement('div');
      typeDiv.className = 'tooltip-field';
      typeDiv.innerHTML = `<span class="tooltip-label">Type:</span> <span class="tooltip-value">${node.type}</span>`;
      tooltip.appendChild(typeDiv);
    }
    
    if (node.address) {
      const addrDiv = document.createElement('div');
      addrDiv.className = 'tooltip-field';
      addrDiv.innerHTML = `<span class="tooltip-label">Address:</span> <span class="tooltip-value">${node.address}</span>`;
      tooltip.appendChild(addrDiv);
    }
    
    this.canvas.appendChild(tooltip);
    
    // Auto-dismiss after 5 seconds
    setTimeout(() => {
      if (tooltip.parentNode) {
        tooltip.remove();
      }
    }, 5000);
  }

  /**
   * Drag event handlers for D3
   */
  /**
   * Handle drag start event for node
   * @param {Object} event - D3 drag event
   * @param {Object} d - Node data
   * @param {Object} simulation - D3 simulation
   * @returns {void}
   */
  dragStarted(event, d, simulation) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  }

  /**
   * Handle drag event for node
   * @param {Object} event - D3 drag event
   * @param {Object} d - Node data
   * @returns {void}
   */
  dragged(event, d) {
    d.fx = event.x;
    d.fy = event.y;
  }

  /**
   * Handle drag end event for node
   * @param {Object} event - D3 drag event
   * @param {Object} d - Node data
   * @param {Object} simulation - D3 simulation
   * @returns {void}
   */
  dragEnded(event, d, simulation) {
    if (!event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
  }

  /**
   * Zoom controls
   */
  /**
   * Zoom in on the graph
   * @returns {void}
   */
  zoomIn() {
    if (!this.svg || !this.zoom) return;
    this.svg.transition().duration(300).call(
      this.zoom.scaleBy,
      1.3
    );
  }

  /**
   * Zoom out on the graph
   * @returns {void}
   */
  zoomOut() {
    if (!this.svg || !this.zoom) return;
    this.svg.transition().duration(300).call(
      this.zoom.scaleBy,
      0.7
    );
  }

  /**
   * Reset zoom to default level
   * @returns {void}
   */
  resetZoom() {
    if (!this.svg || !this.zoom) return;
    this.svg.transition().duration(300).call(
      this.zoom.transform,
      d3.zoomIdentity
    );
  }

  /**
   * Clear current visualization
   */
  /**
   * Clear the graph and return to dropzone
   * @returns {void}
   */
  clear() {
    this.canvas.innerHTML = '';
    this.currentGraph = null;
    this.parsedData = null;
    this.fullGraphData = null;
    this.svg = null;
    this.zoom = null;
    this.expandedNodes.clear();
    this.visibleNodes.clear();
    this.visibleEdges.clear();
    
    if (this.simulation) {
      this.simulation.stop();
      this.simulation = null;
    }
    
    // Show dropzone again
    if (this.dropzone) {
      this.dropzone.style.display = 'flex';
    }
    
    // Hide info panel
    if (this.info) {
      this.info.style.display = 'none';
      this.info.classList.remove('error');
    }
  }

  /**
   * Show error message
   * @param {string} message - Error message
   */
  /**
   * Display error message to user
   * @param {string} message - Error message to display
   * @returns {void}
   */
  showError(message) {
    this.info.classList.add('error');
    this.stats.textContent = message;
  }
}

module.exports = VisualyzerManager;