Integration Examples
spatial-narrative is designed to fit into your existing data pipeline. Here's how to integrate it with common tools.
Data Ingestion
HTTP APIs (with reqwest)
Fetch data from any API and transform into Events:
use reqwest::blocking::Client;
use spatial_narrative::core::{Event, EventBuilder, Location, Timestamp, NarrativeBuilder};
fn fetch_and_process() -> Result<Narrative, Box<dyn std::error::Error>> {
let client = Client::new();
// Fetch from your data source
let response: Vec<MyApiRecord> = client
.get("https://api.example.com/events")
.send()?
.json()?;
// Transform to Events
let events: Vec<Event> = response
.into_iter()
.filter_map(|r| {
Some(EventBuilder::new()
.location(Location::new(r.lat, r.lon))
.timestamp(Timestamp::parse(&r.date).ok()?)
.text(&r.description)
.build())
})
.collect();
Ok(NarrativeBuilder::new()
.title("API Data")
.events(events)
.build())
}
CSV Files
use csv::Reader;
use spatial_narrative::core::{Event, EventBuilder, Location, Timestamp};
fn load_csv(path: &str) -> Vec<Event> {
let mut rdr = Reader::from_path(path).unwrap();
rdr.deserialize()
.filter_map(|result| {
let record: MyRecord = result.ok()?;
Some(EventBuilder::new()
.location(Location::new(record.lat, record.lon))
.timestamp(Timestamp::parse(&record.date).ok()?)
.text(&record.text)
.build())
})
.collect()
}
JSON Files
use serde_json;
use spatial_narrative::core::{Event, Location, Timestamp};
#[derive(serde::Deserialize)]
struct RawEvent {
lat: f64,
lon: f64,
time: String,
description: String,
}
fn load_json(path: &str) -> Vec<Event> {
let data = std::fs::read_to_string(path).unwrap();
let raw: Vec<RawEvent> = serde_json::from_str(&data).unwrap();
raw.into_iter()
.filter_map(|r| {
Some(Event::new(
Location::new(r.lat, r.lon),
Timestamp::parse(&r.time).ok()?,
&r.description
))
})
.collect()
}
Web Mapping
Leaflet
Export to GeoJSON and display with Leaflet:
use spatial_narrative::io::{GeoJsonFormat, Format};
let geojson = GeoJsonFormat::new().export_str(&narrative)?;
std::fs::write("public/events.geojson", &geojson)?;
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css" />
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
</head>
<body>
<div id="map" style="height: 100vh;"></div>
<script>
const map = L.map('map').setView([40.7128, -74.0060], 10);
L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png', {
attribution: '© OpenStreetMap'
}).addTo(map);
fetch('events.geojson')
.then(res => res.json())
.then(data => {
L.geoJSON(data, {
pointToLayer: (feature, latlng) => {
return L.circleMarker(latlng, {
radius: 8,
fillColor: '#ff7800',
color: '#000',
weight: 1,
fillOpacity: 0.8
});
},
onEachFeature: (feature, layer) => {
layer.bindPopup(`
<strong>${feature.properties.text}</strong><br>
${feature.properties.timestamp}
`);
}
}).addTo(map);
});
</script>
</body>
</html>
Mapbox GL JS
mapboxgl.accessToken = 'your-token';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v11',
center: [-74.0060, 40.7128],
zoom: 10
});
map.on('load', () => {
map.addSource('events', {
type: 'geojson',
data: 'events.geojson'
});
map.addLayer({
id: 'events-layer',
type: 'circle',
source: 'events',
paint: {
'circle-radius': 8,
'circle-color': '#ff7800'
}
});
});
Data Science
Python/Pandas
Export to CSV for pandas:
use spatial_narrative::io::{CsvFormat, Format};
let csv = CsvFormat::new().export_str(&narrative)?;
std::fs::write("events.csv", &csv)?;
import pandas as pd
import geopandas as gpd
# Read CSV
df = pd.read_csv('events.csv', parse_dates=['timestamp'])
# Basic analysis
print(f"Events: {len(df)}")
print(f"Date range: {df['timestamp'].min()} to {df['timestamp'].max()}")
# Convert to GeoDataFrame
gdf = gpd.GeoDataFrame(
df,
geometry=gpd.points_from_xy(df.longitude, df.latitude),
crs="EPSG:4326"
)
# Spatial operations
gdf.plot()
R
library(tidyverse)
library(sf)
# Read CSV
events <- read_csv("events.csv")
# Convert to sf object
events_sf <- st_as_sf(events,
coords = c("longitude", "latitude"),
crs = 4326)
# Plot
ggplot(events_sf) +
geom_sf() +
theme_minimal()
GIS Software
QGIS
- Export to GeoJSON:
let geojson = GeoJsonFormat::new().export_str(&narrative)?;
std::fs::write("events.geojson", &geojson)?;
- In QGIS: Layer → Add Layer → Add Vector Layer
- Select the GeoJSON file
ArcGIS
Export to GeoJSON, then import as feature class.
Graph Visualization
Graphviz
let dot = graph.to_dot();
std::fs::write("graph.dot", &dot)?;
# Render to PNG
dot -Tpng graph.dot -o graph.png
# Render to SVG
dot -Tsvg graph.dot -o graph.svg
# Different layouts
neato -Tpng graph.dot -o graph-neato.png
circo -Tpng graph.dot -o graph-circo.png
D3.js Force Graph
let json = graph.to_json();
std::fs::write("public/graph.json", &json)?;
<script src="https://d3js.org/d3.v7.min.js"></script>
<script>
fetch('graph.json')
.then(res => res.json())
.then(data => {
const svg = d3.select("svg");
const width = +svg.attr("width");
const height = +svg.attr("height");
const simulation = d3.forceSimulation(data.nodes)
.force("link", d3.forceLink(data.edges).id(d => d.id))
.force("charge", d3.forceManyBody().strength(-100))
.force("center", d3.forceCenter(width / 2, height / 2));
// Draw edges
const link = svg.selectAll(".link")
.data(data.edges)
.join("line")
.attr("class", "link");
// Draw nodes
const node = svg.selectAll(".node")
.data(data.nodes)
.join("circle")
.attr("class", "node")
.attr("r", 8);
simulation.on("tick", () => {
link.attr("x1", d => d.source.x)
.attr("y1", d => d.source.y)
.attr("x2", d => d.target.x)
.attr("y2", d => d.target.y);
node.attr("cx", d => d.x)
.attr("cy", d => d.y);
});
});
</script>
Database Integration
SQLite/SQLx
use sqlx::sqlite::SqlitePool;
async fn save_events(pool: &SqlitePool, events: &[Event]) -> Result<()> {
for event in events {
sqlx::query(
"INSERT INTO events (id, lat, lon, timestamp, text) VALUES (?, ?, ?, ?, ?)"
)
.bind(event.id.to_string())
.bind(event.location.lat)
.bind(event.location.lon)
.bind(event.timestamp.to_rfc3339())
.bind(&event.text)
.execute(pool)
.await?;
}
Ok(())
}
async fn load_events(pool: &SqlitePool) -> Result<Vec<Event>> {
let rows = sqlx::query("SELECT * FROM events")
.fetch_all(pool)
.await?;
let events = rows.iter()
.map(|row| Event::new(
Location::new(row.get("lat"), row.get("lon")),
Timestamp::parse(row.get::<String, _>("timestamp")).unwrap(),
row.get("text")
))
.collect();
Ok(events)
}
PostgreSQL with PostGIS
CREATE TABLE events (
id UUID PRIMARY KEY,
location GEOGRAPHY(POINT, 4326),
timestamp TIMESTAMPTZ,
text TEXT,
tags TEXT[]
);
-- Spatial index
CREATE INDEX events_location_idx ON events USING GIST (location);
REST API
Actix-web
use actix_web::{web, App, HttpServer, HttpResponse};
use spatial_narrative::io::{GeoJsonFormat, Format};
async fn get_events(data: web::Data<AppState>) -> HttpResponse {
let geojson = GeoJsonFormat::new()
.export_str(&data.narrative)
.unwrap();
HttpResponse::Ok()
.content_type("application/geo+json")
.body(geojson)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.route("/events", web::get().to(get_events))
})
.bind("127.0.0.1:8080")?
.run()
.await
}
CLI Tools
Process Pipeline
use clap::Parser;
#[derive(Parser)]
struct Args {
#[arg(short, long)]
input: PathBuf,
#[arg(short, long)]
output: PathBuf,
#[arg(long)]
format: String,
}
fn main() -> Result<()> {
let args = Args::parse();
// Load
let input = std::fs::read_to_string(&args.input)?;
let narrative = JsonFormat::new().import_str(&input)?;
// Export
let output = match args.format.as_str() {
"geojson" => GeoJsonFormat::new().export_str(&narrative)?,
"csv" => CsvFormat::new().export_str(&narrative)?,
_ => JsonFormat::new().export_str(&narrative)?,
};
std::fs::write(&args.output, &output)?;
Ok(())
}