revert([2fd88f42]): remove zoom-adaptive legend due to critical errors
This commit is contained in:
51
heatmap-tool/frontend/src/components/ErrorBoundary.tsx
Normal file
51
heatmap-tool/frontend/src/components/ErrorBoundary.tsx
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// src/components/ErrorBoundary.tsx
|
||||||
|
import React, { Component, ErrorInfo, ReactNode } from 'react';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
children: ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface State {
|
||||||
|
hasError: boolean;
|
||||||
|
error: Error | null;
|
||||||
|
errorInfo: ErrorInfo | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ErrorBoundary extends Component<Props, State> {
|
||||||
|
public state: State = {
|
||||||
|
hasError: false,
|
||||||
|
error: null,
|
||||||
|
errorInfo: null,
|
||||||
|
};
|
||||||
|
|
||||||
|
public static getDerivedStateFromError(_: Error): State {
|
||||||
|
// Update state so the next render will show the fallback UI.
|
||||||
|
return { hasError: true, error: _, errorInfo: null };
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentDidCatch(error: Error, errorInfo: ErrorInfo) {
|
||||||
|
// You can also log the error to an error reporting service
|
||||||
|
console.error("Uncaught error:", error, errorInfo);
|
||||||
|
this.setState({ error: error, errorInfo: errorInfo });
|
||||||
|
}
|
||||||
|
|
||||||
|
public render() {
|
||||||
|
if (this.state.hasError) {
|
||||||
|
// You can render any custom fallback UI
|
||||||
|
return (
|
||||||
|
<div style={{ padding: '20px', color: 'red' }}>
|
||||||
|
<h2>Something went wrong.</h2>
|
||||||
|
<details style={{ whiteSpace: 'pre-wrap' }}>
|
||||||
|
{this.state.error && this.state.error.toString()}
|
||||||
|
<br />
|
||||||
|
{this.state.errorInfo?.componentStack}
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.props.children;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default ErrorBoundary;
|
||||||
68
heatmap-tool/frontend/src/components/Legend.tsx
Normal file
68
heatmap-tool/frontend/src/components/Legend.tsx
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// src/components/Legend.tsx
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
interface LegendProps {
|
||||||
|
getColor: (count: number) => string;
|
||||||
|
maxCount: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Legend: React.FC<LegendProps> = ({ getColor, maxCount }) => {
|
||||||
|
// Dynamically generate grades based on the maxCount of the current dataset
|
||||||
|
const grades = [
|
||||||
|
1,
|
||||||
|
Math.round(maxCount / 5),
|
||||||
|
Math.round(maxCount / 2.5),
|
||||||
|
Math.round(maxCount / 1.5),
|
||||||
|
maxCount
|
||||||
|
];
|
||||||
|
|
||||||
|
// Remove duplicates and filter out zero or invalid numbers, then sort
|
||||||
|
const uniqueGrades = [...new Set(grades)].filter(g => g > 0).sort((a,b) => a-b);
|
||||||
|
if (uniqueGrades.length > 1 && uniqueGrades[1] <= 1) {
|
||||||
|
uniqueGrades.shift(); // remove the '1' if the next step is too close
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<style>{`
|
||||||
|
.legend {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 20px;
|
||||||
|
right: 20px;
|
||||||
|
padding: 10px;
|
||||||
|
background: rgba(255, 255, 255, 0.8);
|
||||||
|
color: #333;
|
||||||
|
border-radius: 5px;
|
||||||
|
box-shadow: 0 0 15px rgba(0,0,0,0.2);
|
||||||
|
z-index: 1000; /* Ensure it's on top of the map */
|
||||||
|
}
|
||||||
|
.legend h4 {
|
||||||
|
margin: 0 0 5px 0;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.legend i {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
float: left;
|
||||||
|
margin-right: 8px;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
<div className="legend">
|
||||||
|
<h4>Count</h4>
|
||||||
|
{uniqueGrades.map((grade, index) => {
|
||||||
|
const from = grade;
|
||||||
|
const to = uniqueGrades[index + 1];
|
||||||
|
return (
|
||||||
|
<div key={from}>
|
||||||
|
<i style={{ background: getColor(from) }}></i>
|
||||||
|
{from}{to ? `–${to - 1}` : '+'}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Legend;
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
// src/components/MapBoundsManager.tsx
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { useMap } from 'react-leaflet';
|
|
||||||
import L from 'leaflet';
|
|
||||||
import type { HeatmapPoint } from '../App';
|
|
||||||
|
|
||||||
interface MapBoundsManagerProps {
|
|
||||||
data: HeatmapPoint[];
|
|
||||||
}
|
|
||||||
|
|
||||||
const MapBoundsManager: React.FC<MapBoundsManagerProps> = ({ data }) => {
|
|
||||||
const map = useMap();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (data && data.length > 0) {
|
|
||||||
const bounds = L.latLngBounds(data.map(p => [p.lat, p.lon]));
|
|
||||||
if (bounds.isValid()) {
|
|
||||||
map.fitBounds(bounds, { padding: [50, 50] });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [data, map]);
|
|
||||||
|
|
||||||
return null; // This component does not render anything
|
|
||||||
};
|
|
||||||
|
|
||||||
export default MapBoundsManager;
|
|
||||||
@@ -1,15 +1,12 @@
|
|||||||
// src/components/MapDisplay.tsx
|
// src/components/MapDisplay.tsx
|
||||||
import React, { useState, useEffect, useCallback } from 'react';
|
import React from 'react';
|
||||||
import { MapContainer, TileLayer, CircleMarker, Tooltip, useMapEvents } from 'react-leaflet';
|
import { MapContainer, TileLayer, CircleMarker, Tooltip } from 'react-leaflet';
|
||||||
import { HeatmapLayer } from 'react-leaflet-heatmap-layer-v3';
|
import { HeatmapLayer } from 'react-leaflet-heatmap-layer-v3';
|
||||||
import 'leaflet/dist/leaflet.css';
|
import 'leaflet/dist/leaflet.css';
|
||||||
import 'leaflet.heat';
|
import 'leaflet.heat';
|
||||||
import type { HeatmapPoint, MapMode } from '../App';
|
import type { HeatmapPoint, MapMode } from '../App';
|
||||||
import MarkerClusterGroup from 'react-leaflet-cluster';
|
import MarkerClusterGroup from 'react-leaflet-cluster';
|
||||||
import Legend from './Legend';
|
import Legend from './Legend';
|
||||||
import MapBoundsManager from './MapBoundsManager';
|
|
||||||
import { LatLngBounds } from 'leaflet';
|
|
||||||
|
|
||||||
|
|
||||||
interface MapDisplayProps {
|
interface MapDisplayProps {
|
||||||
heatmapData: HeatmapPoint[];
|
heatmapData: HeatmapPoint[];
|
||||||
@@ -17,48 +14,21 @@ interface MapDisplayProps {
|
|||||||
viewMode: MapMode;
|
viewMode: MapMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
// This component listens to map events and calls a callback when the view changes
|
|
||||||
const DynamicLegendHandler = ({ onBoundsChange }: { onBoundsChange: (bounds: LatLngBounds) => void }) => {
|
|
||||||
const map = useMapEvents({
|
|
||||||
zoomend: () => onBoundsChange(map.getBounds()),
|
|
||||||
moveend: () => onBoundsChange(map.getBounds()),
|
|
||||||
load: () => onBoundsChange(map.getBounds()), // Handle initial load
|
|
||||||
});
|
|
||||||
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier, viewMode }) => {
|
const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier, viewMode }) => {
|
||||||
const germanyCenter: [number, number] = [51.1657, 10.4515];
|
const germanyCenter: [number, number] = [51.1657, 10.4515];
|
||||||
|
const maxCount = Math.max(...heatmapData.map(p => p.count), 1);
|
||||||
const [visibleData, setVisibleData] = useState<HeatmapPoint[]>(heatmapData);
|
|
||||||
const dynamicMaxCount = Math.max(...visibleData.map(p => p.count), 1);
|
|
||||||
|
|
||||||
const calculateRadius = (count: number) => {
|
const calculateRadius = (count: number) => {
|
||||||
return 3 + Math.log(count + 1) * 5 * radiusMultiplier;
|
return 3 + Math.log(count + 1) * 5 * radiusMultiplier;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getColor = (count: number) => {
|
const getColor = (count: number) => {
|
||||||
const ratio = count / dynamicMaxCount;
|
const ratio = count / maxCount;
|
||||||
if (ratio > 0.8) return '#d73027'; // Red
|
if (ratio > 0.8) return '#d73027'; // Red
|
||||||
if (ratio > 0.5) return '#fdae61'; // Orange
|
if (ratio > 0.5) return '#fdae61'; // Orange
|
||||||
if (ratio > 0.2) return '#fee08b'; // Yellow
|
if (ratio > 0.2) return '#fee08b'; // Yellow
|
||||||
return '#66bd63'; // Green
|
return '#66bd63'; // Green
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateVisibleData = useCallback((bounds: LatLngBounds) => {
|
|
||||||
const visible = heatmapData.filter(p =>
|
|
||||||
bounds.contains([p.lat, p.lon])
|
|
||||||
);
|
|
||||||
setVisibleData(visible.length > 0 ? visible : heatmapData); // Fallback to all data if none are visible
|
|
||||||
}, [heatmapData]);
|
|
||||||
|
|
||||||
// Reset visible data when heatmapData changes
|
|
||||||
useEffect(() => {
|
|
||||||
setVisibleData(heatmapData);
|
|
||||||
}, [heatmapData]);
|
|
||||||
|
|
||||||
|
|
||||||
const renderPoints = () => (
|
const renderPoints = () => (
|
||||||
<MarkerClusterGroup>
|
<MarkerClusterGroup>
|
||||||
@@ -95,7 +65,7 @@ const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier,
|
|||||||
intensityExtractor={(p: HeatmapPoint) => p.count}
|
intensityExtractor={(p: HeatmapPoint) => p.count}
|
||||||
radius={25}
|
radius={25}
|
||||||
blur={20}
|
blur={20}
|
||||||
max={dynamicMaxCount * 0.1}
|
max={maxCount * 0.1} // Adjust max intensity for better visualization
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -103,6 +73,7 @@ const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier,
|
|||||||
return (
|
return (
|
||||||
<div style={{ textAlign: 'center', paddingTop: '50px' }}>
|
<div style={{ textAlign: 'center', paddingTop: '50px' }}>
|
||||||
<p>No data to display on the map.</p>
|
<p>No data to display on the map.</p>
|
||||||
|
<p>Upload a file and apply filters to see the heatmap.</p>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -113,12 +84,10 @@ const MapDisplay: React.FC<MapDisplayProps> = ({ heatmapData, radiusMultiplier,
|
|||||||
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
|
||||||
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
attribution='© <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
|
||||||
/>
|
/>
|
||||||
<MapBoundsManager data={heatmapData} />
|
|
||||||
<DynamicLegendHandler onBoundsChange={updateVisibleData} />
|
|
||||||
{viewMode === 'points' ? renderPoints() : renderHeatmap()}
|
{viewMode === 'points' ? renderPoints() : renderHeatmap()}
|
||||||
{viewMode === 'points' && <Legend getColor={getColor} maxCount={dynamicMaxCount} />}
|
{viewMode === 'points' && <Legend getColor={getColor} maxCount={maxCount} />}
|
||||||
</MapContainer>
|
</MapContainer>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default MapDisplay;
|
export default MapDisplay;
|
||||||
|
|||||||
Reference in New Issue
Block a user