From 377271f194274220966f98db6a5c10a84a426692 Mon Sep 17 00:00:00 2001 From: Floke Date: Wed, 4 Feb 2026 14:23:40 +0000 Subject: [PATCH] feat([2fd88f42]): implement tooltip column manager --- heatmap-tool/backend/main.py | 38 +++++++++--- heatmap-tool/frontend/package-lock.json | 61 +++++++++++++++++++ heatmap-tool/frontend/package.json | 2 + heatmap-tool/frontend/src/App.tsx | 43 ++++++++++--- .../frontend/src/components/MapDisplay.tsx | 20 ++++-- 5 files changed, 138 insertions(+), 26 deletions(-) diff --git a/heatmap-tool/backend/main.py b/heatmap-tool/backend/main.py index 0f12cd8c..b4ab4ac4 100644 --- a/heatmap-tool/backend/main.py +++ b/heatmap-tool/backend/main.py @@ -42,14 +42,20 @@ def load_plz_data(): plz_geocoord_df = pd.DataFrame() # --- Pydantic Models --- +class TooltipColumnConfig(BaseModel): + id: str + name: str + visible: bool + class FilterRequest(BaseModel): filters: Dict[str, List[str]] + tooltip_config: List[TooltipColumnConfig] = [] class PlzColumnRequest(BaseModel): plz_column: str # --- API Endpoints --- -@app.get("/") +@app.get("/ ") def read_root(): return {"message": "Heatmap Tool Backend"} @@ -146,15 +152,22 @@ async def get_heatmap_data(request: FilterRequest): plz_grouped = filtered_df.groupby(plz_column_name) plz_counts = plz_grouped.size().reset_index(name='count') - # Collect unique attributes for each PLZ + # Collect unique attributes for each PLZ based on tooltip_config attribute_summaries = {} + if request.tooltip_config: + visible_columns = [col.name for col in request.tooltip_config if col.visible] + ordered_columns = [col.name for col in request.tooltip_config] + else: + # Fallback if no config is provided + visible_columns = [col for col in filtered_df.columns if col != plz_column_name] + ordered_columns = visible_columns + for plz_val, group in plz_grouped: summary = {} - for col in filtered_df.columns: - if col != plz_column_name and col != 'lat' and col != 'lon': # Exclude lat/lon if they somehow exist - unique_attrs = group[col].unique().tolist() - # Limit to top 3 unique values for readability - summary[col] = unique_attrs[:3] + for col_name in visible_columns: + if col_name in group: + unique_attrs = group[col_name].unique().tolist() + summary[col_name] = unique_attrs[:3] attribute_summaries[plz_val] = summary # Convert summaries to a DataFrame for merging @@ -199,16 +212,21 @@ async def get_heatmap_data(request: FilterRequest): # For each record, pick out the attributes that are not 'plz', 'lat', 'lon', 'count' final_heatmap_data = [] for record in heatmap_data: - attrs = {k: v for k, v in record.items() if k not in ['plz', 'lat', 'lon', 'count']} + # Order the attributes based on tooltip_config + ordered_attrs = { + col_name: record.get(col_name) + for col_name in ordered_columns + if col_name in record and record.get(col_name) is not None + } final_heatmap_data.append({ "plz": record['plz'], "lat": record['lat'], "lon": record['lon'], "count": record['count'], - "attributes_summary": attrs + "attributes_summary": ordered_attrs }) - print(f"Generated heatmap data with {len(final_heatmap_data)} PLZ points.") + print(f"Generated heatmap data with {len(final_heatmap_data)} PLZ points, respecting tooltip config.") return final_heatmap_data except Exception as e: diff --git a/heatmap-tool/frontend/package-lock.json b/heatmap-tool/frontend/package-lock.json index 0afff82e..3eac42c7 100644 --- a/heatmap-tool/frontend/package-lock.json +++ b/heatmap-tool/frontend/package-lock.json @@ -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", diff --git a/heatmap-tool/frontend/package.json b/heatmap-tool/frontend/package.json index 917754e3..b82b163d 100644 --- a/heatmap-tool/frontend/package.json +++ b/heatmap-tool/frontend/package.json @@ -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", diff --git a/heatmap-tool/frontend/src/App.tsx b/heatmap-tool/frontend/src/App.tsx index 0bb50d5a..812a544c 100644 --- a/heatmap-tool/frontend/src/App.tsx +++ b/heatmap-tool/frontend/src/App.tsx @@ -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([]); + const [tooltipColumns, setTooltipColumns] = useState([]); 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 ( @@ -113,9 +132,12 @@ function App() { <> handleFilterChange(selectedFilters, tooltipColumns)} isLoading={isLoading} /> + {tooltipColumns.length > 0 && ( + + )}

Map Settings

@@ -151,6 +173,7 @@ function App() { heatmapData={heatmapData} radiusMultiplier={radiusMultiplier} viewMode={viewMode} + tooltipColumns={tooltipColumns} />
diff --git a/heatmap-tool/frontend/src/components/MapDisplay.tsx b/heatmap-tool/frontend/src/components/MapDisplay.tsx index 681416f5..83dd549a 100644 --- a/heatmap-tool/frontend/src/components/MapDisplay.tsx +++ b/heatmap-tool/frontend/src/components/MapDisplay.tsx @@ -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 = ({ heatmapData, radiusMultiplier, viewMode }) => { +const MapDisplay: React.FC = ({ 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 = ({ heatmapData, radiusMultiplier, PLZ: {point.plz}
Count: {point.count} - {point.attributes_summary && Object.entries(point.attributes_summary).map(([attr, values]) => ( -
- {attr}: {values.join(', ')} -
- ))} + {tooltipColumns.map(col => { + if (col.visible && point.attributes_summary && point.attributes_summary[col.name]) { + const values = point.attributes_summary[col.name]; + return ( +
+ {col.name}: {Array.isArray(values) ? values.join(', ') : values} +
+ ); + } + return null; + })}
))}