Files
Brancheneinstufung2/heatmap-tool/frontend/src/App.tsx
Floke a3cec85798 [2fd88f42] 1. Kartendarstellung (Neutralisierung):
1. Kartendarstellung (Neutralisierung):
       * Die TileLayer-URL in heatmap-tool/frontend/src/components/MapDisplay.tsx wurde auf eine neutrale CARTO light_all-Kachelansicht umgestellt und die Quellenangabe entsprechend angepasst.
2026-02-11 15:36:06 +00:00

193 lines
6.9 KiB
TypeScript

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<string, string[]>;
}
export type MapMode = 'points' | 'heatmap';
function App() {
const [filters, setFilters] = useState<FilterOptions>({});
const [heatmapData, setHeatmapData] = useState<HeatmapPoint[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [radiusMultiplier, setRadiusMultiplier] = useState(1);
const [viewMode, setViewMode] = useState<MapMode>('points');
const [plzColumnNeeded, setPlzColumnNeeded] = useState(false);
const [availableColumns, setAvailableColumns] = useState<string[]>([]);
const [tooltipColumns, setTooltipColumns] = useState<TooltipColumn[]>([]);
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 (
<ErrorBoundary>
<div className="App">
<header className="App-header">
<h1>German PLZ Heatmap Tool</h1>
</header>
<main className="App-main">
<div className="control-panel">
<FileUpload
onUploadSuccess={handleUploadSuccess}
setIsLoading={setIsLoading}
setError={setError}
/>
{plzColumnNeeded ? (
<PlzSelector
columns={availableColumns}
onSubmit={handlePlzColumnSubmit}
isLoading={isLoading}
/>
) : (
<>
<FilterPanel
filters={filters}
tooltipColumns={tooltipColumns}
setTooltipColumns={setTooltipColumns}
onFilterChange={(selectedFilters) => handleFilterChange(selectedFilters, tooltipColumns)}
isLoading={isLoading}
/>
<div className="map-controls" style={{ marginTop: '20px', paddingTop: '20px', borderTop: '1px solid #555' }}>
<h3>Map Settings</h3>
<div className="toggle-switch" style={{ marginBottom: '15px' }}>
<label>
<input type="radio" value="points" checked={viewMode === 'points'} onChange={() => setViewMode('points')} />
Points
</label>
<label>
<input type="radio" value="heatmap" checked={viewMode === 'heatmap'} onChange={() => setViewMode('heatmap')} />
Heatmap
</label>
</div>
<label htmlFor="radius-slider">Marker Size: {radiusMultiplier.toFixed(1)}x</label>
<input
type="range"
id="radius-slider"
min="0.1"
max="5"
step="0.1"
value={radiusMultiplier}
onChange={(e) => setRadiusMultiplier(parseFloat(e.target.value))}
style={{ width: '100%' }}
disabled={viewMode === 'heatmap'}
/>
</div>
</>
)}
</div>
<div className="map-container">
{isLoading ? (
<p>Loading map data...</p>
) : error ? (
<p className="error">{error}</p>
) : (
<MapDisplay
heatmapData={heatmapData}
radiusMultiplier={radiusMultiplier}
viewMode={viewMode}
tooltipColumns={tooltipColumns}
/>
)}
</div>
</main>
</div>
</ErrorBoundary>
);
}
export default App;