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 statusintent: Shows detected intentchunk: Streams text incrementallyresult: 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
Memoization:
const MemoizedGeoJSON = React.memo(GeoJSON);Key Management:
key={layer.id + JSON.stringify(layer.style)}Lazy Loading:
- Components loaded on-demand
- Map tiles loaded progressively
Leaflet Optimizations
Circle Markers for Large Datasets:
- Use
L.circleMarkerinstead ofL.divIconfor >500 points - Significantly faster rendering
- Use
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
- API Reference: ../backend/API_ENDPOINTS.md
- Data Flow: ../DATA_FLOW.md