Heatmaps
Generate spatial density data for visualization.
Overview
The Heatmap struct represents event density across a grid, useful for:
- Visualizing activity hotspots
- Identifying high-density areas
- Creating choropleth maps
Generating Heatmaps
use spatial_narrative::index::{SpatiotemporalIndex, GridSpec, Heatmap};
use spatial_narrative::core::GeoBounds;
// Build index from events
let index = SpatiotemporalIndex::from_iter(
events.iter().cloned(),
|e| &e.location,
|e| &e.timestamp
);
// Define grid
let bounds = GeoBounds::new(40.0, -75.0, 42.0, -73.0);
let grid = GridSpec::new(bounds, 50, 50); // 50x50 cells
// Generate heatmap
let heatmap = index.heatmap(grid);
GridSpec
Configure the heatmap grid:
use spatial_narrative::index::GridSpec;
use spatial_narrative::core::GeoBounds;
let grid = GridSpec::new(
GeoBounds::new(40.0, -75.0, 42.0, -73.0), // bounds
100, // lat_cells (rows)
100, // lon_cells (columns)
);
// Get cell dimensions
let (lat_size, lon_size) = grid.cell_size();
println!("Cell size: {:.4}° lat x {:.4}° lon", lat_size, lon_size);
Accessing Heatmap Data
Raw Counts
// Get count at specific cell
let count = heatmap.get(25, 30); // row 25, column 30
println!("Cell (25, 30): {} events", count);
// Maximum count
let max = heatmap.max_count();
println!("Maximum density: {} events", max);
Normalized Values
// Get normalized value (0.0 to 1.0)
let intensity = heatmap.get_normalized(25, 30);
println!("Cell (25, 30) intensity: {:.2}", intensity);
Cell Centers
// Get the center coordinates of a cell
let (lat, lon) = heatmap.cell_center(25, 30);
println!("Cell center: ({:.4}, {:.4})", lat, lon);
Iterating Over Cells
// Iterate over all cells
for lat_idx in 0..heatmap.grid.lat_cells {
for lon_idx in 0..heatmap.grid.lon_cells {
let count = heatmap.get(lat_idx, lon_idx);
if count > 0 {
let (lat, lon) = heatmap.cell_center(lat_idx, lon_idx);
println!("({:.4}, {:.4}): {} events", lat, lon, count);
}
}
}
Exporting for Visualization
As GeoJSON Grid
use serde_json::json;
let mut features = Vec::new();
for lat_idx in 0..heatmap.grid.lat_cells {
for lon_idx in 0..heatmap.grid.lon_cells {
let count = heatmap.get(lat_idx, lon_idx);
if count > 0 {
let (lat_size, lon_size) = heatmap.grid.cell_size();
let min_lat = heatmap.grid.bounds.min_lat + lat_idx as f64 * lat_size;
let min_lon = heatmap.grid.bounds.min_lon + lon_idx as f64 * lon_size;
features.push(json!({
"type": "Feature",
"properties": {
"count": count,
"intensity": heatmap.get_normalized(lat_idx, lon_idx)
},
"geometry": {
"type": "Polygon",
"coordinates": [[
[min_lon, min_lat],
[min_lon + lon_size, min_lat],
[min_lon + lon_size, min_lat + lat_size],
[min_lon, min_lat + lat_size],
[min_lon, min_lat]
]]
}
}));
}
}
}
let geojson = json!({
"type": "FeatureCollection",
"features": features
});
As CSV
use std::fs::File;
use std::io::Write;
let mut file = File::create("heatmap.csv")?;
writeln!(file, "lat,lon,count,intensity")?;
for lat_idx in 0..heatmap.grid.lat_cells {
for lon_idx in 0..heatmap.grid.lon_cells {
let count = heatmap.get(lat_idx, lon_idx);
let (lat, lon) = heatmap.cell_center(lat_idx, lon_idx);
let intensity = heatmap.get_normalized(lat_idx, lon_idx);
writeln!(file, "{},{},{},{}", lat, lon, count, intensity)?;
}
}
Visualization Integration
Leaflet.heat
// Load heatmap data
const heatData = [];
for (const feature of geojson.features) {
const coords = feature.geometry.coordinates[0][0];
heatData.push([
(coords[1] + feature.geometry.coordinates[0][2][1]) / 2, // center lat
(coords[0] + feature.geometry.coordinates[0][1][0]) / 2, // center lon
feature.properties.intensity // intensity
]);
}
L.heatLayer(heatData, {radius: 25}).addTo(map);
Mapbox GL JS
map.addSource('heatmap', {
type: 'geojson',
data: geojson
});
map.addLayer({
id: 'heat',
type: 'fill',
source: 'heatmap',
paint: {
'fill-color': [
'interpolate',
['linear'],
['get', 'intensity'],
0, 'rgba(0, 0, 255, 0)',
0.5, 'rgba(255, 255, 0, 0.5)',
1, 'rgba(255, 0, 0, 0.8)'
]
}
});
Use Cases
Finding Hotspots
// Find cells with highest activity
let threshold = heatmap.max_count() / 2;
let mut hotspots = Vec::new();
for lat_idx in 0..heatmap.grid.lat_cells {
for lon_idx in 0..heatmap.grid.lon_cells {
if heatmap.get(lat_idx, lon_idx) >= threshold {
hotspots.push(heatmap.cell_center(lat_idx, lon_idx));
}
}
}
println!("Found {} hotspot cells", hotspots.len());
Comparing Time Periods
// Generate heatmaps for different periods
let morning = TimeRange::new(
Timestamp::parse("2024-01-15T06:00:00Z").unwrap(),
Timestamp::parse("2024-01-15T12:00:00Z").unwrap(),
);
let evening = TimeRange::new(
Timestamp::parse("2024-01-15T18:00:00Z").unwrap(),
Timestamp::parse("2024-01-16T00:00:00Z").unwrap(),
);
// Filter and generate separate heatmaps
let morning_events = index.query_temporal(&morning);
let evening_events = index.query_temporal(&evening);
// Compare distributions...