feat([2fd88f42]): initial setup of heatmap tool
This commit is contained in:
70
heatmap-tool/frontend/src/components/FileUpload.tsx
Normal file
70
heatmap-tool/frontend/src/components/FileUpload.tsx
Normal 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;
|
||||
75
heatmap-tool/frontend/src/components/FilterPanel.tsx
Normal file
75
heatmap-tool/frontend/src/components/FilterPanel.tsx
Normal 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;
|
||||
67
heatmap-tool/frontend/src/components/MapDisplay.tsx
Normal file
67
heatmap-tool/frontend/src/components/MapDisplay.tsx
Normal 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='© <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;
|
||||
Reference in New Issue
Block a user