feat([2fd88f42]): implement smart PLZ column selection

This commit is contained in:
2026-02-04 14:18:23 +00:00
parent 56397f80dc
commit 3a4f7ae790
3 changed files with 110 additions and 43 deletions

View File

@@ -45,6 +45,9 @@ def load_plz_data():
class FilterRequest(BaseModel):
filters: Dict[str, List[str]]
class PlzColumnRequest(BaseModel):
plz_column: str
# --- API Endpoints ---
@app.get("/")
def read_root():
@@ -61,7 +64,7 @@ async def upload_file(file: UploadFile = File(...)):
contents = await file.read()
df = pd.read_excel(io.BytesIO(contents), dtype=str) # Read all as string to be safe
df.fillna('N/A', inplace=True)
df_storage = df # Store dataframe temporarily
# --- PLZ Column Detection ---
temp_plz_col = None
@@ -71,30 +74,53 @@ async def upload_file(file: UploadFile = File(...)):
break
if not temp_plz_col:
raise HTTPException(status_code=400, detail="No column with 'PLZ' found in the file.")
print("PLZ column not found automatically. Asking user for selection.")
return {"plz_column_needed": True, "columns": list(df.columns)}
# If we found a column, proceed as before
plz_column_name = temp_plz_col
# Normalize PLZ data
df[plz_column_name] = df[plz_column_name].str.strip().str.zfill(5)
df_storage = df # Update storage with normalized PLZ
# --- Dynamic Filter Detection ---
filters = {}
for col in df.columns:
if col != plz_column_name:
unique_values = df[col].unique().tolist()
filters[col] = sorted(unique_values)
df_storage = df
print(f"Successfully processed file. Found PLZ column: '{plz_column_name}'. Detected {len(filters)} filterable columns.")
return {"filename": file.filename, "filters": filters, "plz_column": plz_column_name}
print(f"Successfully processed file. Found PLZ column: '{plz_column_name}'.")
return {"plz_column_needed": False, "filters": filters, "plz_column": plz_column_name}
except Exception as e:
print(f"ERROR processing file: {e}")
raise HTTPException(status_code=500, detail=f"An error occurred while processing the file: {e}")
@app.post("/api/set-plz-column")
async def set_plz_column(request: PlzColumnRequest):
global df_storage, plz_column_name
print(f"--- Received request to set PLZ column to: {request.plz_column} ---")
if df_storage is None:
raise HTTPException(status_code=400, detail="No data available. Please upload a file first.")
plz_column_name = request.plz_column
if plz_column_name not in df_storage.columns:
raise HTTPException(status_code=400, detail=f"Column '{plz_column_name}' not found in the uploaded file.")
# Normalize PLZ data
df_storage[plz_column_name] = df_storage[plz_column_name].str.strip().str.zfill(5)
# --- Dynamic Filter Detection ---
filters = {}
for col in df_storage.columns:
if col != plz_column_name:
unique_values = df_storage[col].unique().tolist()
filters[col] = sorted(unique_values)
print(f"Successfully set PLZ column. Detected {len(filters)} filterable columns.")
return {"plz_column_needed": False, "filters": filters, "plz_column": plz_column_name}
@app.post("/api/heatmap")
async def get_heatmap_data(request: FilterRequest):
global df_storage, plz_column_name, plz_geocoord_df

View File

@@ -30,13 +30,44 @@ function App() {
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 handleUploadSuccess = (newFilters: FilterOptions) => {
setFilters(newFilters);
setHeatmapData([]); // Clear previous heatmap data
const handleUploadSuccess = (response: any) => {
setError(null);
// Automatically fetch data with no filters on successful upload
handleFilterChange({});
if (response.plz_column_needed) {
setAvailableColumns(response.columns);
setPlzColumnNeeded(true);
setFilters({});
setHeatmapData([]);
} else {
setPlzColumnNeeded(false);
setFilters(response.filters);
setHeatmapData([]); // Clear previous heatmap data
// Automatically fetch data with no filters on successful upload
handleFilterChange({});
}
};
const handlePlzColumnSubmit = async (selectedColumn: string) => {
setIsLoading(true);
setError(null);
try {
const response = await axios.post('/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) => {
@@ -72,36 +103,46 @@ function App() {
setIsLoading={setIsLoading}
setError={setError}
/>
<FilterPanel
filters={filters}
onFilterChange={handleFilterChange}
{plzColumnNeeded ? (
<PlzSelector
columns={availableColumns}
onSubmit={handlePlzColumnSubmit}
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>
) : (
<>
<FilterPanel
filters={filters}
onFilterChange={handleFilterChange}
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>}

View File

@@ -44,7 +44,7 @@ const FileUpload: React.FC<FileUploadProps> = ({ onUploadSuccess, setIsLoading,
'Content-Type': 'multipart/form-data',
},
});
onUploadSuccess(response.data.filters);
onUploadSuccess(response.data);
} catch (error: any) {
if (axios.isAxiosError(error) && error.response) {
setError(`Upload failed: ${error.response.data.detail || error.message}`);