ZUG-BirdNet – Migratory Bird Detection Visualization
A React web application for visualization of bird detections on an interactive map. The app displays migratory bird data from the BirdWeather network as color-coded clusters on a map, featuring temporal animation with day/night filtering.
ZUG-BirdNet
Challenge
ZUG-BirdNet was developed as part of a conservation initiative in the Regensburg region. The goal: visualize bird detection data from the BirdWeather network on an interactive map — with temporal animation, species filtering, and the ability to embed the application as a widget into an existing CMS (Contao).
An initial prototype already existed: simple markers on a map, fetch-based API calls, and basic animation logic. The challenges for the next iteration were clear:
- Data volume: Up to 20,000 detections per time window needed to render performantly.
- Species distinction: Multiple bird species visible simultaneously with clear visual differentiation.
- Temporal navigation: Smooth animation across days, with the option to show only nighttime activity (migratory birds are nocturnal).
- Embeddability: No routing, no runtime external dependencies — a self-contained SPA widget.
Approach
I restructured the application from the ground up based on the existing prototype:
Architecture
Three React context providers form the backbone — ApolloProvider for GraphQL caching, DatesProvider for all temporal logic (date range, animation control, night mode), and MapProvider for the MapLibre GL instance. This separation enables clean boundaries and avoids prop drilling.
Data layer
Instead of raw fetch calls, Apollo Client 4 handles all data fetching. GraphQL queries and fragments live in src/api/, with types auto-generated via @graphql-codegen from the BirdWeather schema. Caching uses cache-and-network for immediate UI response with background updates.
Map rendering
MapLibre GL renders the map in a dark theme (Stadia Maps). Detection data is provided as a GeoJSON source and rendered through declarative layer definitions (src/components/map/mapStyles.ts) as circles with a glow effect.
My Role
I owned the complete overhaul of the application — from architecture to implementation of all new modules. Specifically:
- GraphQL integration: Apollo Client setup (
src/lib/apollo-client.ts), type policies for cache control, query definitions with fragments (src/api/fragments.ts,src/api/queries.ts), the customuseDetectionshook with prefetch logic. - Map logic: Full reimplementation in
src/components/map/Map.tsxwith Supercluster integration, dynamic paint expressions for species colors, info marker system, and layer management. - Timeline & animation:
DatesProviderwith virtual timeline abstraction, night mode calculation via SunCalc, autoplay control. - Astronomical calculations:
getDayPolygon.tsfor the terminator overlay (day/night boundary as GeoJSON polygon). - UI components: SpeciesDropdown with search and availability checking, LayersDropdown for supplementary layers (light pollution, noise mapping), Timeline with throttled slider.
Technical Highlights
Per-Species Clustering
Instead of a single Supercluster index for all detections, clusterUtils.ts creates separate indices per bird species. This enables color-coded clusters without mixing: each species retains its color even in aggregated state. Clusters are colored via a MapLibre match expression:
["match", ["get", "species"],
"grus-grus", "#FF29B4",
"numenius-arquata", "#64BEFF",
"#cccccc"]
Color assignment is persistent: usePersistentColors allocates colors from a fixed palette and only releases them when a species is deselected.
Virtual Timeline with Night Mode
DatesProvider abstracts the time axis as a sequence of TimeSegment objects. In night mode, the component uses SunCalc to calculate sunset and sunrise times for each day in the date range, generating segments that cover only nighttime hours. The slider then operates on virtual minutes that map internally to correct real times. This way, the animation jumps seamlessly from night to night without playing through empty daytime hours.
Dynamic Query Generation
buildAvailableSpeciesQuery.ts generates a GraphQL query at runtime with aliased fields — one field per species — to check the availability of all selected species in the current map viewport in a single API call. This avoids N separate network requests and auto-updates on map pan/zoom (moveend event).
Result
The overhauled application renders migratory bird data performantly and visually compellingly — even with five-digit detection counts. The architecture with clear separation of data, temporal, and map logic makes the codebase maintainable and extensible. The routing-free SPA design enables straightforward third-party integration into the target CMS Contao.
Technically, the project was an intensive exploration of geo-data visualization, WebGL performance, and the interplay of GraphQL caching with animated map layers — an uncommon stack combination that demanded creative solutions at multiple levels.