feat: implement hotspot visualization on map

- Add HotspotSeverity type and HotspotWithSeverity interface
- Implement getHotspots() method in analyticsService with severity calculation
- Add hotspot layer to MapView component with color-coded circles
- Create hotspot markers with severity icons and report count badges
- Add detailed hotspot popups showing report count, severity score, radius, and dates
- Implement hotspot toggle control in MapFilters with loading state
- Add hotspot severity legend to MapFilters
- Integrate hotspot data fetching in MapPage with sample fallback data
- Color-code hotspot boundaries: yellow (low), orange (medium/high), red (critical)
- Display hotspot statistics in interactive popups

Implements task 12: Create hotspot visualization
Requirements: 3.2, 3.3, 3.5
This commit is contained in:
2025-11-16 19:01:26 +00:00
parent 82635559db
commit 40a96142ba
6 changed files with 371 additions and 6 deletions

View File

@@ -1,6 +1,7 @@
import { useState } from 'react';
import { useState, useEffect } from 'react';
import { MapView, MapFilters } from '@/components/map';
import type { Report, FilterState } from '@/types';
import { analyticsService } from '@/services/analyticsService';
import type { Report, FilterState, HotspotWithSeverity } from '@/types';
/**
* Map page - Interactive map displaying litter reports and cleanup events
@@ -10,6 +11,9 @@ export function MapPage() {
const [filters, setFilters] = useState<FilterState>({});
const [showClusters, setShowClusters] = useState(true);
const [showHeatmap, setShowHeatmap] = useState(false);
const [showHotspots, setShowHotspots] = useState(false);
const [hotspots, setHotspots] = useState<HotspotWithSeverity[]>([]);
const [isLoadingHotspots, setIsLoadingHotspots] = useState(false);
// Sample data for demonstration (will be replaced with API data in later tasks)
const sampleReports: Report[] = [
@@ -124,6 +128,63 @@ export function MapPage() {
setShowHeatmap(!showHeatmap);
};
const handleToggleHotspots = () => {
setShowHotspots(!showHotspots);
};
// Fetch hotspots when hotspot view is enabled
useEffect(() => {
if (showHotspots && hotspots.length === 0) {
setIsLoadingHotspots(true);
analyticsService.getHotspots()
.then((data) => {
setHotspots(data);
})
.catch((error) => {
console.error('Failed to fetch hotspots:', error);
// Use sample hotspot data for demonstration
setHotspots([
{
id: 'hotspot1',
latitude: 53.3498,
longitude: -6.2603,
radius: 500,
reportCount: 15,
severityScore: 0.75,
lastReportDate: new Date('2024-01-20'),
calculatedAt: new Date('2024-01-21'),
severity: 'high',
},
{
id: 'hotspot2',
latitude: 51.8969,
longitude: -8.4863,
radius: 450,
reportCount: 8,
severityScore: 0.45,
lastReportDate: new Date('2024-01-19'),
calculatedAt: new Date('2024-01-21'),
severity: 'medium',
},
{
id: 'hotspot3',
latitude: 53.2707,
longitude: -9.0568,
radius: 600,
reportCount: 22,
severityScore: 0.88,
lastReportDate: new Date('2024-01-21'),
calculatedAt: new Date('2024-01-21'),
severity: 'critical',
},
]);
})
.finally(() => {
setIsLoadingHotspots(false);
});
}
}, [showHotspots]);
return (
<div className="h-screen flex flex-col">
<div className="bg-white border-b border-gray-200 px-4 py-4 sm:px-6 lg:px-8">
@@ -137,9 +198,11 @@ export function MapPage() {
<div className="h-full bg-white rounded-lg shadow-lg overflow-hidden relative">
<MapView
reports={sampleReports}
hotspots={hotspots}
filters={filters}
showClusters={showClusters}
showHeatmap={showHeatmap}
showHotspots={showHotspots}
onMarkerClick={handleMarkerClick}
onMapClick={handleMapClick}
/>
@@ -151,6 +214,9 @@ export function MapPage() {
showClusters={showClusters}
onToggleHeatmap={handleToggleHeatmap}
showHeatmap={showHeatmap}
onToggleHotspots={handleToggleHotspots}
showHotspots={showHotspots}
isLoadingHotspots={isLoadingHotspots}
/>
</div>
</div>