Source: frontend/js/components/vsom/SOMGrid/SOMGrid.js

import log from 'loglevel';
import * as d3 from 'd3';
import { BaseVisualization } from '../BaseVisualization.js';
import { createResponsiveSVG, createTooltip, createColorScale } from '../../../utils/d3-helpers.js';

/**
 * SOM Grid Visualization Component
 * Displays a 2D grid of SOM nodes with optional U-Matrix visualization
 */
export class SOMGrid extends BaseVisualization {
  constructor(container, options = {}) {
    const defaultOptions = {
      nodeSize: 20,
      padding: 2,
      colorScheme: 'interpolateViridis',
      showUMatrix: true,
      showLabels: false,
      showGrid: true,
      ...options
    };

    super(container, defaultOptions);
    
    // Visualization state
    this.nodes = [];
    this.links = [];
    this.simulation = null;
    this.zoom = null;
    this.colorScale = null;
    this.svgElements = {
      nodes: null,
      links: null,
      labels: null,
      uMatrix: null
    };
    
    // Bind methods
    this.handleNodeClick = this.handleNodeClick.bind(this);
    this.handleNodeHover = this.handleNodeHover.bind(this);
  }

  /**
   * Initialize the visualization
   * @param {Object} data - Initial data
   */
  async init(data) {
    this.logger.debug('Initializing SOM Grid visualization');
    await super.init(data);
    
    // Set up the visualization
    this.setupScales();
    this.setupZoom();
    this.renderGrid();
    
    if (data) {
      this.update(data);
    }
    
    return this;
  }

  /**
   * Set up D3 scales
   * @private
   */
  setupScales() {
    this.logger.debug('Setting up scales');
    this.colorScale = createColorScale([0, 1], this.options.colorScheme);
  }

  /**
   * Set up zoom behavior
   * @private
   */
  setupZoom() {
    this.logger.debug('Setting up zoom behavior');
    
    const container = d3.select(this.container);
    
    this.zoom = d3.zoom()
      .scaleExtent([0.1, 8])
      .on('zoom', (event) => {
        container.select('.nodes-group')
          .attr('transform', event.transform);
      });
    
    container.call(this.zoom);
  }

  /**
   * Render the SOM grid
   * @private
   */
  renderGrid() {
    this.logger.debug('Rendering SOM grid');
    
    const { svg } = this;
    
    // Clear existing content
    svg.selectAll('*').remove();
    
    // Add groups for different layers
    const defs = svg.append('defs');
    
    // Add pattern for grid
    if (this.options.showGrid) {
      const gridPattern = defs.append('pattern')
        .attr('id', 'grid-pattern')
        .attr('width', this.options.nodeSize + this.options.padding)
        .attr('height', this.options.nodeSize + this.options.padding)
        .attr('patternUnits', 'userSpaceOnUse');
      
      gridPattern.append('rect')
        .attr('width', this.options.nodeSize + this.options.padding)
        .attr('height', this.options.nodeSize + this.options.padding)
        .attr('fill', 'none')
        .attr('stroke', '#eee')
        .attr('stroke-width', 0.5);
    }
    
    // Create groups for different layers
    const layers = {
      uMatrix: svg.append('g').attr('class', 'u-matrix-layer'),
      links: svg.append('g').attr('class', 'links-layer'),
      nodes: svg.append('g').attr('class', 'nodes-group')
    };
    
    this.svgElements = { ...this.svgElements, ...layers };
    
    // Add background grid
    if (this.options.showGrid) {
      svg.insert('rect', ':first-child')
        .attr('width', '100%')
        .attr('height', '100%')
        .attr('fill', 'url(#grid-pattern)');
    }
  }

  /**
   * Update the visualization with new data
   * @param {Object} data - New data to visualize
   */
  update(data) {
    this.logger.debug('Updating SOM Grid visualization', data);
    
    if (!this.initialized) {
      this.logger.warn('Visualization not initialized, calling init() first');
      this.init(data);
      return;
    }
    
    this.processData(data);
    this.updateNodes();
    this.updateLinks();
    
    if (this.options.showUMatrix) {
      this.updateUMatrix();
    }
    
    if (this.options.showLabels) {
      this.updateLabels();
    }
    
    return this;
  }

  /**
   * Process input data into visualization format
   * @param {Object} data - Input data
   * @private
   */
  processData(data) {
    this.logger.debug('Processing data', data);
    
    // Process nodes
    this.nodes = data.nodes.map((node, i) => ({
      id: node.id || `node-${i}`,
      x: node.x,
      y: node.y,
      weight: node.weight || 0,
      activation: node.activation || 0,
      bmuCount: node.bmuCount || 0,
      metadata: node.metadata || {}
    }));
    
    // Process links (if any)
    this.links = [];
    
    // Calculate grid dimensions if not provided
    if (!this.options.width || !this.options.height) {
      const xExtent = d3.extent(this.nodes, d => d.x);
      const yExtent = d3.extent(this.nodes, d => d.y);
      
      this.options.width = (xExtent[1] - xExtent[0] + 1) * (this.options.nodeSize + this.options.padding);
      this.options.height = (yExtent[1] - yExtent[0] + 1) * (this.options.nodeSize + this.options.padding);
      
      this.logger.debug('Calculated grid dimensions', {
        width: this.options.width,
        height: this.options.height
      });
    }
  }

  /**
   * Update node visualization
   * @private
   */
  updateNodes() {
    this.logger.debug('Updating nodes');
    
    const { nodes } = this.svgElements;
    const nodeSize = this.options.nodeSize;
    
    // Bind data
    const nodeSelection = nodes.selectAll('.node')
      .data(this.nodes, d => d.id);
    
    // Enter
    const nodeEnter = nodeSelection.enter()
      .append('rect')
      .attr('class', 'node')
      .attr('width', nodeSize)
      .attr('height', nodeSize)
      .attr('rx', 2)
      .attr('ry', 2)
      .style('fill', d => this.colorScale(d.activation))
      .style('stroke', '#fff')
      .style('stroke-width', 1)
      .style('cursor', 'pointer')
      .on('click', (event, d) => this.handleNodeClick(event, d))
      .on('mouseover', (event, d) => this.handleNodeHover(event, d, true))
      .on('mouseout', (event, d) => this.handleNodeHover(event, d, false));
    
    // Update
    nodeSelection.merge(nodeEnter)
      .transition()
      .duration(300)
      .attr('x', d => d.x * (nodeSize + this.options.padding))
      .attr('y', d => d.y * (nodeSize + this.options.padding))
      .style('fill', d => this.colorScale(d.activation));
    
    // Exit
    nodeSelection.exit()
      .transition()
      .duration(200)
      .style('opacity', 0)
      .remove();
    
    // Add tooltips
    addTooltip(nodeSelection, d => `
      <div><strong>Node (${d.x}, ${d.y})</strong></div>
      <div>Activation: ${d.activation.toFixed(3)}</div>
      <div>BMU Count: ${d.bmuCount}</div>
      <div>Weight: ${d.weight.toFixed(3)}</div>
    `);
  }

  /**
   * Update links between nodes
   * @private
   */
  updateLinks() {
    // Implement link visualization if needed
    this.logger.debug('Updating links');
  }

  /**
   * Update U-Matrix visualization
   * @private
   */
  updateUMatrix() {
    this.logger.debug('Updating U-Matrix');
    
    // Implement U-Matrix visualization
    // This would show distances between neighboring nodes
  }

  /**
   * Update node labels
   * @private
   */
  updateLabels() {
    this.logger.debug('Updating labels');
    
    // Add or update labels if enabled
  }

  /**
   * Handle node click events
   * @param {Event} event - DOM event
   * @param {Object} node - Clicked node data
   */
  handleNodeClick(event, node) {
    this.logger.debug('Node clicked', node);
    
    // Emit event or call callback
    if (typeof this.options.onNodeClick === 'function') {
      this.options.onNodeClick(node, event);
    }
  }

  /**
   * Handle node hover events
   * @param {Event} event - DOM event
   * @param {Object} node - Hovered node data
   * @param {boolean} isHovered - Whether the node is being hovered
   */
  handleNodeHover(event, node, isHovered) {
    if (isHovered) {
      d3.select(event.currentTarget)
        .style('stroke', '#333')
        .style('stroke-width', 2);
      
      if (typeof this.options.onNodeHover === 'function') {
        this.options.onNodeHover(node, true, event);
      }
    } else {
      d3.select(event.currentTarget)
        .style('stroke', '#fff')
        .style('stroke-width', 1);
      
      if (typeof this.options.onNodeHover === 'function') {
        this.options.onNodeHover(node, false, event);
      }
    }
  }

  /**
   * Handle window resize
   */
  handleResize() {
    super.handleResize();
    this.logger.debug('Handling resize in SOMGrid');
    
    // Recalculate dimensions and update visualization
    if (this.initialized) {
      this.renderGrid();
      this.update({
        nodes: this.nodes,
        links: this.links
      });
    }
  }

  /**
   * Clean up resources
   */
  destroy() {
    this.logger.debug('Destroying SOM Grid visualization');
    
    // Clean up event listeners, timeouts, etc.
    if (this.simulation) {
      this.simulation.stop();
    }
    
    super.destroy();
  }
}