Source: frontend/js/utils/d3-helpers.js

/**
 * D3.js helper utilities for visualizations
 */

/**
 * Create a responsive SVG element
 * @param {HTMLElement} container - The container element
 * @param {Object} options - Configuration options
 * @returns {Object} D3 selection of the SVG element
 */
export function createResponsiveSVG(container, options = {}) {
  const {
    width = 800,
    height = 600,
    margin = { top: 20, right: 20, bottom: 20, left: 20 },
    onResize = null
  } = options;

  // Create SVG with viewBox for responsive scaling
  const svg = d3.select(container)
    .append('svg')
    .attr('viewBox', `0 0 ${width} ${height}`)
    .attr('preserveAspectRatio', 'xMidYMid meet')
    .classed('d3-svg', true);

  // Handle window resize
  if (onResize) {
    const resizeObserver = new ResizeObserver(entries => {
      if (entries.length > 0) {
        onResize(entries[0].contentRect);
      }
    });
    
    resizeObserver.observe(container);
    
    // Clean up on container removal
    d3.select(container).on('remove', () => {
      resizeObserver.disconnect();
    });
  }

  return svg;
}

/**
 * Create a tooltip
 * @param {Object} options - Tooltip options
 * @returns {Object} Tooltip functions
 */
export function createTooltip(options = {}) {
  const {
    parent = d3.select('body'),
    offsetX = 10,
    offsetY = 10,
    styles = {
      position: 'absolute',
      padding: '8px',
      background: 'rgba(0, 0, 0, 0.8)',
      color: '#fff',
      'border-radius': '4px',
      'pointer-events': 'none',
      'font-size': '12px',
      'z-index': '1000',
      'max-width': '300px',
      'word-wrap': 'break-word'
    }
  } = options;

  const tooltip = parent.append('div')
    .style('opacity', 0)
    .style('position', 'absolute');

  // Apply styles
  Object.entries(styles).forEach(([key, value]) => {
    tooltip.style(key, value);
  });

  /**
   * Show tooltip with content at position [x, y]
   */
  const show = (content, [x, y]) => {
    tooltip
      .style('opacity', 1)
      .html(content)
      .style('left', `${x + offsetX}px`)
      .style('top', `${y + offsetY}px`);
  };

  /**
   * Hide tooltip
   */
  const hide = () => {
    tooltip.style('opacity', 0);
  };

  /**
   * Move tooltip to new position
   */
  const move = ([x, y]) => {
    tooltip
      .style('left', `${x + offsetX}px`)
      .style('top', `${y + offsetY}px`);
  };

  return { show, hide, move, node: tooltip };
}

/**
 * Create a color scale
 * @param {string} scheme - D3 color scheme name
 * @param {Array} domain - Domain values
 * @returns {Function} D3 color scale
 */
export function createColorScale(scheme = 'interpolateViridis', domain = [0, 1]) {
  if (d3[scheme]) {
    return d3.scaleSequential(d3[scheme]).domain(domain);
  }
  return d3.scaleSequential(d3.interpolateViridis).domain(domain);
}

/**
 * Format number with SI prefix
 * @param {number} num - Number to format
 * @returns {string} Formatted string
 */
export function formatSI(num) {
  return d3.format('.3s')(num)
    .replace('G', 'B')
    .replace('M', 'M')
    .replace('K', 'K')
    .replace('B', 'B');
}

/**
 * Debounce function
 * @param {Function} func - Function to debounce
 * @param {number} wait - Wait time in ms
 * @returns {Function} Debounced function
 */
export function debounce(func, wait) {
  let timeout;
  return function(...args) {
    const context = this;
    clearTimeout(timeout);
    timeout = setTimeout(() => func.apply(context, args), wait);
  };
}

/**
 * Add zoom behavior to a D3 selection
 * @param {Object} selection - D3 selection
 * @param {Object} options - Zoom options
 * @returns {Object} Zoom behavior
 */
export function addZoom(selection, options = {}) {
  const {
    scaleExtent = [0.1, 10],
    onZoom = () => {},
    onEnd = () => {}
  } = options;

  const zoom = d3.zoom()
    .scaleExtent(scaleExtent)
    .on('zoom', (event) => {
      selection.attr('transform', event.transform);
      onZoom(event);
    })
    .on('end', onEnd);

  selection.call(zoom);
  return zoom;
}

/**
 * Create a legend for a color scale
 * @param {Object} svg - D3 SVG selection
 * @param {Object} colorScale - D3 color scale
 * @param {Object} options - Legend options
 * @returns {Object} Legend object with update method
 */
export function createLegend(svg, colorScale, options = {}) {
  const {
    title = '',
    width = 300,
    height = 20,
    margin = { top: 0, right: 0, bottom: 0, left: 0 },
    tickFormat = d3.format('.2f'),
    tickValues = null,
    orientation = 'horizontal'
  } = options;

  const defs = svg.append('defs');
  const legendGroup = svg.append('g')
    .attr('class', 'legend')
    .attr('transform', `translate(${margin.left},${margin.top})`);

  // Add gradient
  const gradientId = `gradient-${Math.random().toString(36).substr(2, 9)}`;
  
  const gradient = defs.append('linearGradient')
    .attr('id', gradientId);

  // Update gradient stops based on color scale
  const update = (domain) => {
    const stops = gradient.selectAll('stop')
      .data(d3.range(0, 1.01, 0.1));

    stops.enter()
      .append('stop')
      .merge(stops)
      .attr('offset', d => `${d * 100}%`)
      .attr('stop-color', d => colorScale(d));

    stops.exit().remove();

    // Update axis
    const xScale = d3.scaleLinear()
      .domain(domain || colorScale.domain())
      .range([0, width]);

    const xAxis = d3.axisBottom(xScale)
      .ticks(5)
      .tickFormat(tickFormat);

    if (tickValues) {
      xAxis.tickValues(tickValues);
    }

    let axisGroup = legendGroup.select('.axis');
    if (axisGroup.empty()) {
      axisGroup = legendGroup.append('g')
        .attr('class', 'axis')
        .attr('transform', `translate(0,${height})`);
    }

    axisGroup.call(xAxis);

    // Update gradient rect
    let rect = legendGroup.select('rect');
    if (rect.empty()) {
      rect = legendGroup.append('rect')
        .attr('width', width)
        .attr('height', height);
    }

    rect.attr('fill', `url(#${gradientId})`);

    // Update title
    if (title) {
      let titleElement = legendGroup.select('.legend-title');
      if (titleElement.empty()) {
        titleElement = legendGroup.append('text')
          .attr('class', 'legend-title')
          .attr('y', -10);
      }
      titleElement.text(title);
    }
  };

  // Initial update
  update(colorScale.domain());

  return { update };
}

// Export all functions
export default {
  createResponsiveSVG,
  createTooltip,
  createColorScale,
  formatSI,
  debounce,
  addZoom,
  createLegend
};