The smokesignal.events web application
at main 110 lines 3.2 kB view raw
1/** 2 * Location Heatmap Feature 3 * 4 * Renders the location heatmap on the location page showing event distribution. 5 * Uses MapLibre GL for map rendering and H3 for hexagon visualization. 6 */ 7 8import type { Feature, Polygon } from 'geojson' 9import type { H3Bucket } from '../../types' 10import { 11 loadMapLibraries, 12 createMap, 13 h3ToGeoJsonFeature, 14 addHexagonLayers, 15 updateHexagonSource, 16 calculateBounds, 17 heatmapGradientColor, 18 toMapCenter, 19} from './map-utils' 20 21export async function initLocationHeatmap(): Promise<void> { 22 const mapContainer = document.getElementById('location-heatmap') 23 if (!mapContainer) return 24 25 // Skip if already initialized or currently initializing 26 if ( 27 mapContainer.dataset.mapInitialized === 'true' || 28 mapContainer.dataset.mapInitializing === 'true' 29 ) 30 return 31 mapContainer.dataset.mapInitializing = 'true' 32 33 try { 34 // Parse data from data attributes 35 const centerLat = parseFloat(mapContainer.dataset.centerLat ?? '0') 36 const centerLon = parseFloat(mapContainer.dataset.centerLon ?? '0') 37 const centerCell = mapContainer.dataset.centerCell ?? '' 38 const geoBucketsData = mapContainer.dataset.geoBuckets 39 40 if (!geoBucketsData) return 41 42 let geoBuckets: H3Bucket[] 43 try { 44 geoBuckets = JSON.parse(geoBucketsData) 45 } catch (e) { 46 console.error('Failed to parse geo buckets:', e) 47 return 48 } 49 50 // Lazy load MapLibre and H3 51 const { maplibregl, h3 } = await loadMapLibraries() 52 53 // Create non-interactive map 54 const map = createMap(maplibregl, { 55 container: 'location-heatmap', 56 center: toMapCenter(centerLat, centerLon), 57 zoom: 9, 58 interactive: false, 59 }) 60 61 map.on('load', () => { 62 // Add hexagon layers 63 addHexagonLayers(map) 64 65 // Only draw hexes with events 66 if (geoBuckets && geoBuckets.length > 0) { 67 const counts = geoBuckets.map((b) => b.doc_count ?? 0) 68 const minCount = Math.min(...counts) 69 const maxCount = Math.max(...counts) 70 71 const features: Feature<Polygon>[] = [] 72 73 geoBuckets.forEach((bucket) => { 74 try { 75 const cellIndex = bucket.key 76 const count = bucket.doc_count ?? 0 77 const isCenter = cellIndex === centerCell 78 const color = heatmapGradientColor(count, minCount, maxCount) 79 80 features.push( 81 h3ToGeoJsonFeature(h3, cellIndex, { 82 fillColor: color, 83 fillOpacity: 0.5, 84 strokeColor: isCenter ? '#1a1a1a' : color, 85 strokeWidth: isCenter ? 3 : 2, 86 }) 87 ) 88 } catch (e) { 89 console.warn('Failed to draw hex:', bucket.key, e) 90 } 91 }) 92 93 // Update the source with features 94 updateHexagonSource(map, features) 95 96 // Fit bounds to show all hexes 97 const bounds = calculateBounds(features) 98 if (bounds) { 99 map.fitBounds(bounds, { padding: 10 }) 100 } 101 } 102 }) 103 104 mapContainer.dataset.mapInitialized = 'true' 105 } catch (err) { 106 console.error('Failed to initialize location heatmap:', err) 107 } finally { 108 mapContainer.dataset.mapInitializing = 'false' 109 } 110}