feat([2fd88f42]): initial setup of heatmap tool

This commit is contained in:
2026-02-04 11:30:47 +00:00
parent 3bf0c4f3b3
commit c5c3de12e7
24 changed files with 4433 additions and 920 deletions

View File

@@ -0,0 +1,70 @@
// src/components/FileUpload.tsx
import React, { useState } from 'react';
import axios from 'axios';
interface FilterOptions {
[key: string]: string[];
}
interface FileUploadProps {
onUploadSuccess: (filters: FilterOptions) => void;
setIsLoading: (isLoading: boolean) => void;
setError: (error: string | null) => void;
}
const FileUpload: React.FC<FileUploadProps> = ({ onUploadSuccess, setIsLoading, setError }) => {
const [selectedFile, setSelectedFile] = useState<File | null>(null);
const handleFileChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files) {
setSelectedFile(event.target.files[0]);
}
};
const handleSubmit = async (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
if (!selectedFile) {
setError("Please select a file first.");
return;
}
if (!selectedFile.name.endsWith('.xlsx')) {
setError("Invalid file type. Please upload an .xlsx file.");
return;
}
const formData = new FormData();
formData.append('file', selectedFile);
setIsLoading(true);
setError(null);
try {
const response = await axios.post('http://localhost:8000/api/upload', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
onUploadSuccess(response.data.filters);
} catch (error: any) {
if (axios.isAxiosError(error) && error.response) {
setError(`Upload failed: ${error.response.data.detail || error.message}`);
} else {
setError(`Upload failed: ${error.message}`);
}
} finally {
setIsLoading(false);
}
};
return (
<div className="file-upload">
<h3>Upload XLSX File</h3>
<form onSubmit={handleSubmit}>
<input type="file" accept=".xlsx" onChange={handleFileChange} />
<button type="submit" disabled={!selectedFile}>Upload</button>
</form>
</div>
);
};
export default FileUpload;

View File

@@ -0,0 +1,75 @@
// src/components/FilterPanel.tsx
import React, { useState, useEffect } from 'react';
interface FilterOptions {
[key: string]: string[];
}
interface FilterPanelProps {
filters: FilterOptions;
onFilterChange: (selectedFilters: FilterOptions) => void;
isLoading: boolean;
}
const FilterPanel: React.FC<FilterPanelProps> = ({ filters, onFilterChange, isLoading }) => {
const [selectedFilters, setSelectedFilters] = useState<FilterOptions>({});
// Reset selected filters when a new file is uploaded (filters prop changes)
useEffect(() => {
setSelectedFilters({});
}, [filters]);
const handleSelectionChange = (category: string, values: string[]) => {
setSelectedFilters(prev => ({
...prev,
[category]: values,
}));
};
const handleApplyClick = () => {
onFilterChange(selectedFilters);
}
const handleResetClick = () => {
setSelectedFilters({});
onFilterChange({});
}
if (Object.keys(filters).length === 0) {
return <div>Upload a file to see filter options.</div>;
}
return (
<div className="filter-panel">
<h3>Filters</h3>
{Object.entries(filters).map(([category, options]) => (
<div key={category} className="filter-group">
<label>{category}</label>
<select
multiple
value={selectedFilters[category] || []}
onChange={(e) =>
handleSelectionChange(category, Array.from(e.target.selectedOptions, option => option.value))
}
>
{options.map(option => (
<option key={option} value={option}>
{option}
</option>
))}
</select>
</div>
))}
<div className="filter-buttons">
<button onClick={handleApplyClick} disabled={isLoading}>
{isLoading ? 'Loading...' : 'Apply Filters'}
</button>
<button onClick={handleResetClick} disabled={isLoading}>
Reset Filters
</button>
</div>
</div>
);
};
export default FilterPanel;

View File

@@ -0,0 +1,67 @@
// src/components/MapDisplay.tsx
import React from 'react';
import { MapContainer, TileLayer, CircleMarker, Tooltip } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import { HeatmapPoint } from '../App'; // Assuming type is exported from App.tsx
interface MapDisplayProps {
heatmapData: HeatmapPoint[];
}
const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData }) => {
const germanyCenter: [number, number] = [51.1657, 10.4515];
// Simple scaling function for marker radius
const calculateRadius = (count: number) => {
return 5 + Math.log(count + 1) * 5;
};
// Simple color scaling function
const getColor = (count: number, maxCount: number) => {
const ratio = count / maxCount;
if (ratio > 0.8) return 'red';
if (ratio > 0.5) return 'orange';
if (ratio > 0.2) return 'yellow';
return 'green';
}
const maxCount = Math.max(...heatmapData.map(p => p.count), 1);
if (heatmapData.length === 0) {
return (
<div style={{ textAlign: 'center', paddingTop: '50px' }}>
<p>No data to display on the map.</p>
<p>Upload a file and apply filters to see the heatmap.</p>
</div>
);
}
return (
<MapContainer center={germanyCenter} zoom={6} style={{ height: '100%', width: '100%' }}>
<TileLayer
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
/>
{heatmapData.map((point, idx) => (
<CircleMarker
key={idx}
center={[point.lat, point.lon]}
radius={calculateRadius(point.count)}
pathOptions={{
color: getColor(point.count, maxCount),
fillColor: getColor(point.count, maxCount),
fillOpacity: 0.7
}}
>
<Tooltip>
PLZ: {point.plz} <br />
Count: {point.count}
</Tooltip>
</CircleMarker>
))}
</MapContainer>
);
};
export default MapDisplay;