Data Flow
This document describes how incident data flows through Sightline from source to display.
Overview
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Config │───▶│ Adapters │───▶│ Context │───▶│ UI │
│ map.config │ │ Fetch data │ │ State mgmt │ │ Rendering │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘App Initialization
Sightline uses a unified initialization flow that loads data and initializes the map in parallel:
┌──────────────────────────────────────────────────────────────────────┐
│ App Initialization │
├──────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ DataProvider│ │ MapEngine │ │
│ │ isLoading │ (parallel) │ mapReady │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────┐ ┌─────────────┐ │
│ │ Fetch data │ │ Mapbox init │ │
│ │ from source │ │ onLoad() │ │
│ └──────┬──────┘ └──────┬──────┘ │
│ │ │ │
│ └──────────────┬────────────────────┘ │
│ ▼ │
│ ┌──────────────────┐ │
│ │ appReady = true │ │
│ │ Fade in content │ │
│ └──────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────────────┘The unified loader displays a single "Initializing..." message while both processes complete:
// page.tsx
const { isLoading } = useData();
const [mapReady, setMapReady] = useState(false);
const appReady = !isLoading && mapReady;
// Loader fades out, content fades in when appReady is true
<MapEngine onMapReady={() => setMapReady(true)} ... />This approach provides:
- Parallel loading — Data fetches while map initializes
- Unified UX — Single loading state instead of sequential messages
- Smooth transition — CSS fade-out/fade-in when ready
See LoaderConfig for customization options.
1. Configuration Loading
On application startup:
// map.config.ts is loaded
const config = await import('@/../map.config').then(m => m.default);
// Config is validated
const validatedConfig = validateConfig(config);
// Merged with defaults
const finalConfig = mergeConfig(validatedConfig);2. Adapter Factory
Adapters are created dynamically via a factory function:
// Factory creates the appropriate adapter based on type
function createAdapter(source: DataSourceConfig): DataAdapter {
switch (source.adapter) {
case 'static':
return new StaticAdapter(source);
case 'supabase':
return new SupabaseAdapter(source);
case 'api':
return new ApiAdapter(source);
default:
throw new Error(`Unknown adapter type: ${source.adapter}`);
}
}Adapter Interface
All adapters implement:
interface DataAdapter {
readonly id: string;
readonly name: string;
getIncidents(filters?: IncidentFilters): Promise<Incident[]>;
getIncidentById(id: string): Promise<Incident | null>;
getFilterOptions(): Promise<FilterOptions>;
clearCache?(): void;
}3. Data Fetching
When the app loads or sources change, adapters are created and fetched in one operation:
// Create adapters and fetch from all enabled sources
const fetchPromises = enabledSources.map(async (source) => {
const adapter = createAdapter(source);
const incidents = await adapter.getIncidents();
// Tag each incident with its source ID for filtering/attribution
return incidents.map((inc) => ({
...inc,
dataSourceId: source.id,
}));
});
// Wait for all to complete (partial failures allowed)
const results = await Promise.allSettled(fetchPromises);
// Combine successful results
const incidents = results
.filter(r => r.status === 'fulfilled')
.flatMap(r => r.value);The dataSourceId tag allows incidents to be filtered by source and enables proper color/attribution in the UI.
Error Handling
// Partial failures are handled gracefully
const failed = results.filter(r => r.status === 'rejected');
const succeeded = results.filter(r => r.status === 'fulfilled');
// Only set error if ALL sources fail
if (failed.length > 0 && succeeded.length === 0) {
setError('All data sources failed to load');
} else if (failed.length > 0) {
// Log warning for partial failures
console.warn(`${failed.length} source(s) failed to load`);
}4. Data Normalization
Raw incidents are transformed to display format using toIncidentDisplay:
function toIncidentDisplay(incident: Incident): IncidentDisplay {
const sensitivity = getIncidentSensitivity(incident);
const showSensitivity = sensitivity && sensitivity !== 'none';
const eventDate = new Date(incident.temporal.date);
const now = new Date();
const ageInDays = Math.floor((now.getTime() - eventDate.getTime()) / (1000 * 60 * 60 * 24));
return {
...incident,
// Extract coordinates for map rendering
coordinates: [incident.location.longitude, incident.location.latitude],
// Add sensitivity display properties (only if set and not 'none')
sensitivityColor: showSensitivity ? LOCATION_SENSITIVITY_COLORS[sensitivity] : undefined,
sensitivityLabel: showSensitivity ? LOCATION_SENSITIVITY_LABELS[sensitivity] : undefined,
// Add formatted date
formattedDate: eventDate.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric',
}),
// Calculate age for filtering/sorting
ageInDays,
};
}5. Context State
Data is stored in React context:
interface DataContextValue {
// Data
incidents: IncidentDisplay[];
filteredIncidents: IncidentDisplay[];
isLoading: boolean;
error: Error | null;
// Data sources
dataSources: DataSourceConfig[];
enabledSourceIds: string[];
toggleSource: (sourceId: string) => void;
// Filters
filters: IncidentFilters;
setFilters: (filters: IncidentFilters | ((prev: IncidentFilters) => IncidentFilters)) => void;
updateFilter: <K extends keyof IncidentFilters>(key: K, value: IncidentFilters[K]) => void;
resetFilters: () => void;
// Filter options (derived from data)
filterOptions: {
countries: string[];
siteTypes: string[];
locationSensitivities: LocationSensitivity[];
incidentTypes: string[];
// ... more options
};
// Selection
selectedIncident: IncidentDisplay | null;
setSelectedIncident: (incident: IncidentDisplay | null) => void;
// Timeline
currentDate: Date | null;
setCurrentDate: (date: Date | null) => void;
dateRange: { min: Date; max: Date } | null;
// Actions
refresh: () => Promise<void>;
}6. Filtering Pipeline
When filters change, the filterIncidents utility is called:
// Filter pipeline in data-context.tsx
const filteredIncidents = useMemo(() => {
// First filter by enabled data sources
const sourceFiltered = incidents.filter(i =>
enabledSourceIds.includes(i.dataSourceId)
);
// Then apply all filters via the filterIncidents utility
return filterIncidents(sourceFiltered, filters, currentDate);
}, [incidents, filters, enabledSourceIds, currentDate]);The filterIncidents utility uses modular filter checks:
// filterIncidents signature
export function filterIncidents(
incidents: IncidentDisplay[],
filters: IncidentFilters,
currentDate: Date | null = null
): IncidentDisplay[]
// Internally uses modular checks:
// - passesLocationFilters(incident, filters)
// - passesClassificationFilters(incident, filters)
// - passesEvidenceFilters(incident, filters)
// - passesInvestigationFilters(incident, filters)
// - passesQuickFilters(incident, filters)
// - passesObjectFilters(incident, filters)
// - passesTimelineFilter(incident, currentDate)7. Component Consumption
Components access data via hooks:
function MapEngine() {
const { filteredIncidents, selectedIncident } = useData();
return (
<Map>
{filteredIncidents.map(incident => (
<Marker key={incident.id} incident={incident} />
))}
</Map>
);
}8. User Interactions
User actions update context state:
// Select an incident
const handleMarkerClick = (incident: IncidentDisplay) => {
setSelectedIncident(incident);
flyTo(incident.location);
};
// Update filters
const handleFilterChange = (key: string, value: any) => {
updateFilter(key, value);
};
// Toggle data source
const handleSourceToggle = (sourceId: string) => {
toggleSource(sourceId);
};Data Flow Diagram
User Action
│
▼
┌─────────────────────────────────────────────────┐
│ DataContext │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Incidents│──▶│ Filters │──▶│ Filtered │ │
│ │ Raw │ │ State │ │ Incidents│ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
└──────────────────────│──────────────────────────┘
│
┌──────────────────┼──────────────────┐
│ │ │
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ Map │ │ Feed │ │Timeline│
│ Engine │ │ │ │ │
└────────┘ └────────┘ └────────┘Performance Optimizations
Memoization
// Expensive computations are memoized
const filteredIncidents = useMemo(() => {
return applyFilters(incidents, filters);
}, [incidents, filters]);
// Callbacks are stable
const updateFilter = useCallback((key, value) => {
setFilters(prev => ({ ...prev, [key]: value }));
}, []);Lazy Loading
// Data is fetched on mount, not import
useEffect(() => {
fetchIncidents();
}, [enabledSources]);Virtualization
For large datasets, the incident feed uses virtualization:
// Only render visible items
<VirtualList
items={filteredIncidents}
itemHeight={80}
renderItem={(incident) => <FeedItem incident={incident} />}
/>Debugging
Enable debug logging:
// In browser console
localStorage.setItem('sightline-debug', 'true');This logs:
- Data fetches and their results
- Filter changes and computed results
- Selection events
- Render cycles