import { useState, useEffect } from 'react'; import axios from 'axios'; import './App.css'; import 'react-leaflet-cluster/dist/assets/MarkerCluster.css'; import 'react-leaflet-cluster/dist/assets/MarkerCluster.Default.css'; import FileUpload from './components/FileUpload'; import FilterPanel from './components/FilterPanel'; import MapDisplay from './components/MapDisplay'; import ErrorBoundary from './components/ErrorBoundary'; import PlzSelector from './components/PlzSelector'; // Define types for our state export interface FilterOptions { [key: string]: string[]; } export interface TooltipColumn { id: string; name: string; visible: boolean; } export interface HeatmapPoint { plz: string; lat: number; lon: number; count: number; attributes_summary?: Record; } export type MapMode = 'points' | 'heatmap'; function App() { const [filters, setFilters] = useState({}); const [heatmapData, setHeatmapData] = useState([]); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const [radiusMultiplier, setRadiusMultiplier] = useState(1); const [viewMode, setViewMode] = useState('points'); const [plzColumnNeeded, setPlzColumnNeeded] = useState(false); const [availableColumns, setAvailableColumns] = useState([]); const [tooltipColumns, setTooltipColumns] = useState([]); const handleUploadSuccess = (response: any) => { setError(null); if (response.plz_column_needed) { setAvailableColumns(response.columns); setPlzColumnNeeded(true); setFilters({}); setHeatmapData([]); setTooltipColumns([]); } else { setPlzColumnNeeded(false); const newFilters = response.filters; setFilters(newFilters); // Initialize tooltip columns based on filters setTooltipColumns(Object.keys(newFilters).map(name => ({ id: name, name, visible: true }))); setHeatmapData([]); // Clear previous heatmap data // Automatically fetch data with no filters on successful upload // Pass initial tooltip config handleFilterChange({}, Object.keys(newFilters).map(name => ({ id: name, name, visible: true }))); } }; const handlePlzColumnSubmit = async (selectedColumn: string) => { setIsLoading(true); setError(null); try { const response = await axios.post('/heatmap/api/set-plz-column', { plz_column: selectedColumn, }); handleUploadSuccess(response.data); // Re-use the success handler } catch (error: any) { if (axios.isAxiosError(error) && error.response) { setError(`Failed to set PLZ column: ${error.response.data.detail || error.message}`); } else { setError(`Failed to set PLZ column: ${error.message}`); } } finally { setIsLoading(false); } }; const handleFilterChange = async (selectedFilters: FilterOptions, currentTooltipConfig: TooltipColumn[]) => { setIsLoading(true); setError(null); try { const response = await axios.post('/heatmap/api/heatmap', { filters: selectedFilters, tooltip_config: currentTooltipConfig, // Pass tooltip config to backend }); setHeatmapData(response.data); } catch (error: any) { if (axios.isAxiosError(error) && error.response) { setError(`Failed to fetch heatmap data: ${error.response.data.detail || error.message}`); } else { setError(`Failed to fetch heatmap data: ${error.message}`); } setHeatmapData([]); // Clear data on error } finally { setIsLoading(false); } }; // Need to re-fetch data when tooltip config changes. // Optimization: In a real app, we might debounce this or add an "Apply" button for sorting. // For now, let's keep it manual via the "Apply Filters" button in the FilterPanel to avoid excessive API calls while dragging. // The FilterPanel calls onFilterChange when "Apply" is clicked, passing both filters and the current tooltipColumns. return (

German PLZ Heatmap Tool

{plzColumnNeeded ? ( ) : ( <> handleFilterChange(selectedFilters, tooltipColumns)} isLoading={isLoading} />

Map Settings

setRadiusMultiplier(parseFloat(e.target.value))} style={{ width: '100%' }} disabled={viewMode === 'heatmap'} />
)}
{isLoading ? (

Loading map data...

) : error ? (

{error}

) : ( )}
); } export default App;