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**:
```typescript
interface ChatPanelProps {
onLayerAdd: (layer: MapLayer) => void;
}
```
**State**:
```typescript
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**:
```typescript
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**:
```typescript
interface MapViewerProps {
layers: MapLayer[];
onLayerUpdate: (id: string, updates: Partial<MapLayer>) => void;
onLayerRemove: (id: string) => void;
onLayerReorder: (fromIndex: number, toIndex: number) => void;
}
```
**Layer Model**:
```typescript
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**:
```typescript
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**:
```typescript
// 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**:
```typescript
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**:
```typescript
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.
```typescript
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)
```typescript
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
```typescript
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
```typescript
const COLOR_PALETTES = {
viridis: ['#440154', '#482878', ... '#fde725'],
blues: ['#f7fbff', ... '#08306b'],
reds: ['#fff5f0', ... '#67000d']
};
```
---
## Performance Optimizations
### React Optimizations
1. **Memoization**:
```typescript
const MemoizedGeoJSON = React.memo(GeoJSON);
```
2. **Key Management**:
```typescript
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
- **Backend Services**: [../backend/CORE_SERVICES.md](../backend/CORE_SERVICES.md)
- **API Reference**: [../backend/API_ENDPOINTS.md](../backend/API_ENDPOINTS.md)
- **Data Flow**: [../DATA_FLOW.md](../DATA_FLOW.md)