feat([2fd88f42]): implement tooltip column manager

This commit is contained in:
2026-02-04 14:23:40 +00:00
parent f0713ff5b3
commit 73ef663f22
5 changed files with 138 additions and 26 deletions

View File

@@ -8,6 +8,8 @@
"name": "frontend",
"version": "0.0.0",
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"axios": "^1.13.4",
"leaflet": "^1.9.4",
"leaflet.heat": "^0.2.0",
@@ -316,6 +318,59 @@
"node": ">=6.9.0"
}
},
"node_modules/@dnd-kit/accessibility": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/accessibility/-/accessibility-3.1.1.tgz",
"integrity": "sha512-2P+YgaXF+gRsIihwwY1gCsQSYnu9Zyj2py8kY5fFvUM1qm2WA2u639R6YNVfU4GWr+ZM5mqEsfHZZLoRONbemw==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/core": {
"version": "6.3.1",
"resolved": "https://registry.npmjs.org/@dnd-kit/core/-/core-6.3.1.tgz",
"integrity": "sha512-xkGBRQQab4RLwgXxoqETICr6S5JlogafbhNsidmrkVv2YRs5MLwpjoF2qpiGjQt8S9AoxtIV603s0GIUpY5eYQ==",
"license": "MIT",
"dependencies": {
"@dnd-kit/accessibility": "^3.1.1",
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0",
"react-dom": ">=16.8.0"
}
},
"node_modules/@dnd-kit/sortable": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/@dnd-kit/sortable/-/sortable-10.0.0.tgz",
"integrity": "sha512-+xqhmIIzvAYMGfBYYnbKuNicfSsk4RksY2XdmJhT+HAC01nix6fHCztU68jooFiMUB01Ky3F0FyOvhG/BZrWkg==",
"license": "MIT",
"dependencies": {
"@dnd-kit/utilities": "^3.2.2",
"tslib": "^2.0.0"
},
"peerDependencies": {
"@dnd-kit/core": "^6.3.0",
"react": ">=16.8.0"
}
},
"node_modules/@dnd-kit/utilities": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/@dnd-kit/utilities/-/utilities-3.2.2.tgz",
"integrity": "sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==",
"license": "MIT",
"dependencies": {
"tslib": "^2.0.0"
},
"peerDependencies": {
"react": ">=16.8.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.2.tgz",
@@ -3405,6 +3460,12 @@
"typescript": ">=4.8.4"
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"license": "0BSD"
},
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",

View File

@@ -10,6 +10,8 @@
"preview": "vite preview"
},
"dependencies": {
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"axios": "^1.13.4",
"leaflet": "^1.9.4",
"leaflet.heat": "^0.2.0",

View File

@@ -1,4 +1,4 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import axios from 'axios';
import './App.css';
import 'react-leaflet-cluster/dist/assets/MarkerCluster.css';
@@ -7,6 +7,8 @@ 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';
import TooltipManager, { TooltipColumn } from './components/TooltipManager';
// Define types for our state
export interface FilterOptions {
@@ -33,6 +35,7 @@ function App() {
const [plzColumnNeeded, setPlzColumnNeeded] = useState(false);
const [availableColumns, setAvailableColumns] = useState<string[]>([]);
const [tooltipColumns, setTooltipColumns] = useState<TooltipColumn[]>([]);
const handleUploadSuccess = (response: any) => {
@@ -42,12 +45,17 @@ function App() {
setPlzColumnNeeded(true);
setFilters({});
setHeatmapData([]);
setTooltipColumns([]);
} else {
setPlzColumnNeeded(false);
setFilters(response.filters);
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
handleFilterChange({});
// Pass initial tooltip config
handleFilterChange({}, Object.keys(newFilters).map(name => ({ id: name, name, visible: true })));
}
};
@@ -70,25 +78,36 @@ function App() {
}
};
const handleFilterChange = async (selectedFilters: FilterOptions) => {
const handleFilterChange = async (selectedFilters: FilterOptions, currentTooltipConfig: TooltipColumn[]) => {
setIsLoading(true);
setError(null);
try {
const response = await axios.post('/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}`);
}
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
useEffect(() => {
if (Object.keys(filters).length > 0) {
// This assumes you want to re-fetch with the *last applied* filters, which is complex to track.
// For simplicity, we won't re-fetch automatically on tooltip change for now,
// but the config will be applied on the *next* "Apply Filters" click.
}
}, [tooltipColumns]);
return (
<ErrorBoundary>
@@ -113,9 +132,12 @@ function App() {
<>
<FilterPanel
filters={filters}
onFilterChange={handleFilterChange}
onFilterChange={(selectedFilters) => handleFilterChange(selectedFilters, tooltipColumns)}
isLoading={isLoading}
/>
{tooltipColumns.length > 0 && (
<TooltipManager columns={tooltipColumns} setColumns={setTooltipColumns} />
)}
<div className="map-controls" style={{ marginTop: '20px', paddingTop: '20px', borderTop: '1px solid #555' }}>
<h3>Map Settings</h3>
<div className="toggle-switch" style={{ marginBottom: '15px' }}>
@@ -151,6 +173,7 @@ function App() {
heatmapData={heatmapData}
radiusMultiplier={radiusMultiplier}
viewMode={viewMode}
tooltipColumns={tooltipColumns}
/>
</div>
</main>

View File

@@ -6,14 +6,16 @@ import 'leaflet/dist/leaflet.css';
import 'leaflet.heat';
import type { HeatmapPoint, MapMode } from '../App';
import MarkerClusterGroup from 'react-leaflet-cluster';
import { TooltipColumn } from './TooltipManager';
interface MapDisplayProps {
heatmapData: HeatmapPoint[];
radiusMultiplier: number;
viewMode: MapMode;
tooltipColumns: TooltipColumn[];
}
const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier, viewMode }) => {
const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier, viewMode, tooltipColumns }) => {
const germanyCenter: [number, number] = [51.1657, 10.4515];
const maxCount = Math.max(...heatmapData.map(p => p.count), 1);
@@ -45,11 +47,17 @@ const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier,
<Tooltip>
PLZ: {point.plz} <br />
Count: {point.count}
{point.attributes_summary && Object.entries(point.attributes_summary).map(([attr, values]) => (
<div key={attr}>
<strong>{attr}:</strong> {values.join(', ')}
</div>
))}
{tooltipColumns.map(col => {
if (col.visible && point.attributes_summary && point.attributes_summary[col.name]) {
const values = point.attributes_summary[col.name];
return (
<div key={col.id}>
<strong>{col.name}:</strong> {Array.isArray(values) ? values.join(', ') : values}
</div>
);
}
return null;
})}
</Tooltip>
</CircleMarker>
))}