GeoQuery / docs /frontend /COMPONENTS.md
GerardCB's picture
Deploy to Spaces (Final Clean)
4851501

Frontend Component Architecture

Overview of GeoQuery's React components and their interactions.


Component Hierarchy

App (Next.js Layout)
└── Main Page
    β”œβ”€β”€ ChatPanel
    β”‚   β”œβ”€β”€ MessageList
    β”‚   β”œβ”€β”€ InputForm
    β”‚   └── CitationLinks
    β”œβ”€β”€ MapViewer
    β”‚   β”œβ”€β”€ LeafletMap
    β”‚   β”œβ”€β”€ LayerControl
    β”‚   β”‚   β”œβ”€β”€ SortableLayerItem (draggable)
    β”‚   β”‚   └── LayerSettings
    β”‚   └── EmptyState
    └── DataExplorer
        β”œβ”€β”€ ResultsTable
        └── ExportButton

Core Components

ChatPanel (components/ChatPanel.tsx)

Main conversational interface with streaming support.

Props:

interface ChatPanelProps {
  onLayerAdd: (layer: MapLayer) => void;
}

State:

const [messages, setMessages] = useState<Message[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [currentThought, setCurrentThought] = useState<string>('');

Key Features:

  • SSE (Server-Sent Events) for streaming
  • Real-time thought process display
  • Markdown rendering
  • Citation link generation
  • Layer reference chips

SSE Implementation:

const response = await fetch('/api/chat', {
  method: 'POST',
  headers: {'Content-Type': 'application/json'},
  body: JSON.stringify({message, history})
});

const reader = response.body.getReader();
const decoder = new TextDecoder();

while (true) {
  const {value, done} = await reader.read();
  if (done) break;
  
  const chunk = decoder.decode(value);
  // Process SSE events...
}

Event Handling:

  • status: Updates loading status
  • intent: Shows detected intent
  • chunk: Streams text incrementally
  • result: Adds map layer and displays data

MapViewer (components/MapViewer.tsx)

Interactive Leaflet map with layer management.

Props:

interface MapViewerProps {
  layers: MapLayer[];
  onLayerUpdate: (id: string, updates: Partial<MapLayer>) => void;
  onLayerRemove: (id: string) => void;
  onLayerReorder: (fromIndex: number, toIndex: number) => void;
}

Layer Model:

interface MapLayer {
  id: string;
  name: string;
  data: GeoJSON.FeatureCollection;
  visible: boolean;
  style: {
    color: string;
    fillColor: string;
    fillOpacity: number;
    weight: number;
  };
  choropleth?: {
    enabled: boolean;
    column: string;
    palette: string;
    scale: 'linear' | 'log';
    min?: number;
    max?: number;
  };
  pointMarker?: {
    icon: string;
    style: 'icon' | 'circle';
    color: string;
    size: number;
  };
}

Key Features:

  • Layer visibility toggle
  • Style customization (color, opacity, weight)
  • Drag-and-drop layer reordering
  • Auto-fit bounds to new layers
  • Choropleth visualization
  • Point rendering modes (icon vs circle)

Point Rendering Logic:

const pointToLayer = (feature: any, latlng: L.LatLng) => {
  if (layer.pointMarker?.style === "circle") {
    // Simple circle for large datasets
    return L.circleMarker(latlng, {
      radius: 5,
      fillColor: layer.pointMarker.color,
      color: '#ffffff',
      weight: 2,
      opacity: 1,
      fillOpacity: 0.7
    });
  }
  
  // Emoji icon for POI
  return L.marker(latlng, {
    icon: L.divIcon({
      html: `<div style="font-size: 32px">${layer.pointMarker.icon}</div>`,
      className: 'custom-emoji-marker',
      iconSize: [32, 32]
    })
  });
};

Choropleth Implementation:

// Calculate color based on value
const fillColor = getChoroplethColor(
  feature.properties[choropleth.column],
  choropleth.min,
  choropleth.max,
  choropleth.palette,
  choropleth.scale
);

DataExplorer (components/DataExplorer.tsx)

Tabular data view with export capabilities.

Props:

interface DataExplorerProps {
  data: any[];
  visible: boolean;
}

Features:

  • Responsive table
  • Column sorting
  • CSV export
  • Pagination for large datasets

Sub-Components

SortableLayerItem

Draggable layer control item using @dnd-kit.

Features:

  • Drag handle with visual feedback
  • Layer visibility toggle
  • Settings expansion
  • Style controls (color pickers, sliders)
  • Remove layer button

Drag-and-Drop:

const {
  attributes,
  listeners,
  setNodeRef,
  transform,
  transition
} = useSortable({ id: layer.id });

const style = {
  transform: CSS.Transform.toString(transform),
  transition
};

AutoFitBounds

Utility component that auto-zooms map to new layers.

function AutoFitBounds({ layers }: { layers: MapLayer[] }) {
  const map = useMap();
  
  useEffect(() => {
    if (layers.length > prevLayersLength.current) {
      const latestLayer = layers[layers.length - 1];
      const bounds = L.geoJSON(latestLayer.data).getBounds();
      if (bounds.isValid()) {
        map.fitBounds(bounds, { padding: [50, 50] });
      }
    }
  }, [layers]);
  
  return null;
}

State Management

Global State (Main Page)

const [layers, setLayers] = useState<MapLayer[]>([]);

const handleLayerAdd = (geojson: GeoJSON.FeatureCollection) => {
  const newLayer: MapLayer = {
    id: geojson.properties.layer_id,
    name: geojson.properties.layer_name,
    data: geojson,
    visible: true,
    style: geojson.properties.style,
    choropleth: geojson.properties.choropleth,
    pointMarker: geojson.properties.pointMarker
  };
  
  setLayers(prev => [...prev, newLayer]);
};

Layer Updates

const handleLayerUpdate = (id: string, updates: Partial<MapLayer>) => {
  setLayers(prev => prev.map(layer =>
    layer.id === id ? { ...layer, ...updates } : layer
  ));
};

Styling & Theming

Global Styles (app/globals.css)

  • Tailwind CSS for utility-first styling
  • Custom CSS variables for theming
  • Leaflet overrides for custom markers

Color Palettes

const COLOR_PALETTES = {
  viridis: ['#440154', '#482878', ... '#fde725'],
  blues: ['#f7fbff', ... '#08306b'],
  reds: ['#fff5f0', ... '#67000d']
};

Performance Optimizations

React Optimizations

  1. Memoization:

    const MemoizedGeoJSON = React.memo(GeoJSON);
    
  2. Key Management:

    key={layer.id + JSON.stringify(layer.style)}
    
  3. Lazy Loading:

    • Components loaded on-demand
    • Map tiles loaded progressively

Leaflet Optimizations

  1. Circle Markers for Large Datasets:

    • Use L.circleMarker instead of L.divIcon for >500 points
    • Significantly faster rendering
  2. Layer Virtualization:

    • Only render visible layers
    • Remove offscreen features

Responsive Design

Breakpoints

  • Mobile (<640px): Stacked layout
  • Tablet (640-1024px): Sidebar collapses
  • Desktop (>1024px): Full sidebar

Mobile Adaptations

  • Layer legend becomes bottom sheet
  • Map controls repositioned
  • Touch-friendly drag handles

Accessibility

  • Keyboard Navigation: Tab through controls
  • Screen Readers: ARIA labels on interactive elements
  • Color Contrast: WCAG AA compliant
  • Focus Indicators: Visible focus states

Icons & Assets

  • Lucide React: Icon library for UI elements
  • Leaflet Markers: Default and custom markers
  • Emoji Icons: Unicode emojis for POI markers

Next Steps