Skip to content

Add batch region comparison UI to interactive map frontend #22

@comnam90

Description

@comnam90

Batch Region Comparison UI for Interactive Map

Overview

Extend the interactive Leaflet.js map with a dedicated UI component that allows users to select multiple regions (by clicking/checkboxes) and display a detailed side-by-side comparison table. This closes the gap between the backend /api/v1/regions/compare endpoint and the frontend user experience.

Scope

Included:

  • Toggle-able comparison panel (off by default, activated by user action)
  • Select multiple regions via map markers or comparison list
  • Display side-by-side service availability table
  • Highlight common services across selected regions
  • Export comparison as CSV
  • Clear/reset selection easily
  • Responsive design for mobile and desktop

Explicitly Excluded:

  • Sharing comparison results via URL (phase 2)
  • Saving comparisons to browser storage (phase 2)
  • Advanced analytics or scoring of regions
  • Custom comparison formulas or weighting

Technical Requirements

UI/UX Flow

  1. Activation: User clicks "Compare Regions" button in info panel or clicks marker with Ctrl/Cmd key held
  2. Selection: Regions are added to comparison list (max 5-10 regions to keep table readable)
  3. Display: Comparison panel slides in from right side showing:
    • Selected region names (with remove button for each)
    • Service availability matrix (rows = services, columns = regions)
    • Color-coded cells: green (available), gray (unavailable), yellow (partial/tiered)
    • Service type indicator (boolean vs. tiered)
  4. Actions:
    • Add/remove regions
    • Export to CSV
    • Clear all selections
    • Sort by service or region

Frontend Component Structure

<!-- layouts/index.html updates -->

<!-- Comparison Panel (hidden by default) -->
<div id="comparisonPanel" class="fixed right-0 top-0 h-full w-96 bg-white shadow-lg 
     transform translate-x-full transition-transform duration-300 z-40 overflow-y-auto">
  
  <!-- Header -->
  <div class="p-4 border-b flex justify-between items-center sticky top-0 bg-white">
    <h2 class="text-lg font-bold">Region Comparison</h2>
    <button id="closeComparisonBtn" class="text-gray-500 hover:text-gray-700">×</button>
  </div>
  
  <!-- Selected Regions List -->
  <div id="selectedRegionsList" class="p-4 border-b">
    <div class="space-y-2" id="regionTags"></div>
    <button id="clearSelectionBtn" class="mt-2 w-full py-2 text-sm text-red-600 
            hover:bg-red-50 rounded">Clear All</button>
  </div>
  
  <!-- Comparison Table -->
  <div id="comparisonTable" class="p-4">
    <div class="overflow-x-auto">
      <table class="w-full text-sm border-collapse" id="serviceMatrix"></table>
    </div>
  </div>
  
  <!-- Export Button -->
  <div class="p-4 border-t sticky bottom-0 bg-white">
    <button id="exportCsvBtn" class="w-full py-2 bg-blue-600 text-white rounded 
            hover:bg-blue-700">Export as CSV</button>
  </div>
</div>

<!-- "Compare Regions" button added to info panel -->

Implementation Plan

Step 1: Add Comparison State Management

// In layouts/index.html <script> section
const comparisonState = {
  selectedRegions: [],
  maxRegions: 6,
  
  addRegion(region) {
    if (this.selectedRegions.length >= this.maxRegions) {
      alert(`Maximum ${this.maxRegions} regions allowed for comparison`)
      return
    }
    if (!this.selectedRegions.find(r => r.id === region.id)) {
      this.selectedRegions.push(region)
      this.render()
    }
  },
  
  removeRegion(regionId) {
    this.selectedRegions = this.selectedRegions.filter(r => r.id !== regionId)
    this.render()
  },
  
  clear() {
    this.selectedRegions = []
    this.render()
  }
}

Step 2: API Integration Function

// Fetch comparison data from backend
async function fetchComparison(regionIds) {
  const ids = regionIds.join(',')
  const res = await fetch(`/api/v1/regions/compare?ids=${ids}`)
  if (!res.ok) throw new Error(`API error: ${res.statusText}`)
  return res.json()
}

Step 3: Comparison Panel Rendering

async function renderComparisonPanel() {
  const panel = document.getElementById('comparisonPanel')
  
  if (comparisonState.selectedRegions.length === 0) {
    panel.classList.add('translate-x-full')
    return
  }
  
  // Fetch comparison data
  const regionIds = comparisonState.selectedRegions.map(r => r.id)
  const comparisonData = await fetchComparison(regionIds)
  
  // Render selected regions tags
  const tagContainer = document.getElementById('regionTags')
  tagContainer.innerHTML = comparisonState.selectedRegions.map(r => `
    <div class="flex justify-between items-center bg-gray-100 p-2 rounded">
      <span class="font-medium">${r.name}</span>
      <button class="text-red-500 hover:text-red-700" 
              onclick="comparisonState.removeRegion('${r.id}')">✕</button>
    </div>
  `).join('')
  
  // Render comparison table
  const table = document.getElementById('serviceMatrix')
  table.innerHTML = buildComparisonMatrix(comparisonData)
  
  // Show panel
  panel.classList.remove('translate-x-full')
}

function buildComparisonMatrix(comparisonData) {
  // Build HTML table with service rows and region columns
  // Color-code cells based on availability
  let html = '<thead><tr><th>Service</th>'
  
  comparisonData.regions.forEach(region => {
    html += `<th>${region.name}</th>`
  })
  html += '</tr></thead><tbody>'
  
  comparisonData.services.forEach(service => {
    html += `<tr><td class="font-medium">${service.name}</td>`
    
    service.regionStatus.forEach(status => {
      const bgColor = status.available ? 'bg-green-100' : 'bg-gray-100'
      const text = status.available ? '✓' : '—'
      html += `<td class="${bgColor} text-center p-2">${text}</td>`
    })
    
    html += '</tr>'
  })
  
  html += '</tbody>'
  return html
}

Step 4: CSV Export Function

function exportComparisonAsCSV() {
  const rows = []
  const regionNames = comparisonState.selectedRegions.map(r => r.name)
  
  // Header row
  rows.push(['Service', ...regionNames].join(','))
  
  // Service rows (simplified; use actual table data)
  const table = document.getElementById('serviceMatrix')
  const tableRows = table.querySelectorAll('tbody tr')
  tableRows.forEach(row => {
    const cells = row.querySelectorAll('td')
    const rowData = Array.from(cells).map(c => c.textContent.trim())
    rows.push(rowData.join(','))
  })
  
  // Download CSV
  const csv = rows.join('\n')
  const blob = new Blob([csv], { type: 'text/csv' })
  const url = window.URL.createObjectURL(blob)
  const a = document.createElement('a')
  a.href = url
  a.download = `vdc-comparison-${Date.now()}.csv`
  a.click()
}

Step 5: Event Handlers

// Toggle comparison mode on marker click
function setupMarkerClickHandler(marker, region) {
  marker.on('click', function(e) {
    if (e.originalEvent.ctrlKey || e.originalEvent.metaKey) {
      // Ctrl/Cmd + click = add to comparison
      comparisonState.addRegion(region)
    } else {
      // Regular click = show info
      marker.openPopup()
    }
  })
}

// Button event handlers
document.getElementById('closeComparisonBtn')?.addEventListener('click', () => {
  document.getElementById('comparisonPanel').classList.add('translate-x-full')
})

document.getElementById('clearSelectionBtn')?.addEventListener('click', () => {
  comparisonState.clear()
})

document.getElementById('exportCsvBtn')?.addEventListener('click', () => {
  exportComparisonAsCSV()
})

// "Compare Regions" button in info panel
document.getElementById('compareRegionsBtn')?.addEventListener('click', () => {
  // Activate comparison mode; user then selects regions
  document.getElementById('comparisonPanel').classList.remove('translate-x-full')
})

Acceptance Criteria

  • Comparison panel slides in/out smoothly with Tailwind transitions
  • Users can add regions via Ctrl/Cmd + click on markers
  • Maximum 6 regions can be selected for comparison
  • Comparison table displays all services (rows) and selected regions (columns)
  • Green cells = service available, gray cells = unavailable
  • CSV export includes all comparison data with correct headers
  • "Clear All" button removes all selected regions
  • Comparison panel is responsive on mobile (slides up instead of right, adjusted width)
  • No page reload or API errors occur during normal usage
  • Panel closes when user navigates away or manually closes it
  • Selected regions list shows region name and remove button for each

Priority

User Impact: 4/5 (extends popular comparison API to frontend)
Strategic Alignment: 4/5 (improves UX for infrastructure planning)
Feasibility: 4/5 (leverages existing backend)
Overall Score: 8.0 (second highest priority)

Justification: The comparison API already exists on the backend but isn't exposed in the UI. This is a quick win that surfaces an existing feature to users and provides significant value for multi-region deployment planning.

Dependencies

  • Blocks: None
  • Blocked by: None (depends on existing /api/v1/regions/compare endpoint)

Implementation Size

  • Estimated effort: Medium (4-5 days)
  • Complexity: Medium (state management + API integration + responsive design)
  • Testing effort: Medium (integration tests, responsive design testing)

Implementation Order

  1. Add comparison state management
  2. Create comparison panel HTML/CSS (Tailwind)
  3. Implement API fetch and data processing
  4. Build comparison matrix rendering
  5. Add event handlers and user interactions
  6. CSV export functionality
  7. Mobile responsiveness testing

Additional Notes

  • Consider debouncing comparison panel renders if many regions selected
  • Could add filtering to comparison table (e.g., "show only vdc_vault services")
  • Phase 2: Add URL sharing so users can send comparisons to colleagues
  • Phase 2: Persist selected regions to localStorage for resume on return

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions