SilentSkies — Project Story

What Inspired This

The starting point was a number: 3 billion. That's how many birds have disappeared from North America alone since 1970, according to a 2019 study in Science. It gets cited everywhere, but it never quite lands. You read it, you feel vaguely bad, and then you move on.

I kept thinking about why that is. Part of it is that "3 billion" has no geography. It doesn't tell you whether the birds that used to show up in your garden are among them. It doesn't show you where the loss is happening fastest, or whether it's slowing down, or whether anything is actually working.

The other thing is that most conservation tools are built for researchers. If you're not already in that world, you bounce off the interface in about thirty seconds.

I wanted to build something that made the data feel real to a normal person — not just visible, but visceral.


How I Built It

The core idea was a time slider on a globe. Drag it backward and watch the bird observations thin out. That's it. Everything else grew from that.

Data layer. All occurrence data comes from GBIF (Global Biodiversity Information Facility), which is free and requires no API key. It turned out that GBIF already contains most of eBird's observations — over 150 million bird records — so I didn't need a second source for the core feature. I built a custom hook (useGBIF) that queries the occurrence API by species, year, and optionally month, then clusters the results into heatmap points for the globe.

The globe. I used react-globe.gl, which wraps Three.js into a React component. Getting the heatmap density right took more work than expected. Raw occurrence counts are noisy — some species are simply better-studied than others, so areas with more birdwatchers show more dots regardless of actual population. I normalized by applying a search-effort proxy: rather than showing raw counts, the point opacity scales with density relative to the regional average for that year.

Risk scoring. For each species in my watch list I compute a simplified decline trend using year-over-year change, smoothed with a three-year rolling window. The score \(R\) is defined as:

$$R = 1 - \frac{\bar{n}_{recent}}{\bar{n}_{baseline}}$$

where \(\bar{n}_{recent}\) is the mean annual observation count over the last five years and \(\bar{n}_{baseline}\) is the mean over the five years at the start of the dataset. A score near \(1.0\) means the population has nearly disappeared from the record. This isn't a perfect ecological model — observation effort varies, and GBIF coverage in some regions is thin — but it's honest, reproducible, and grounded in the actual numbers.

AI analysis layer. When you click "Analyze" on a species, I construct a structured prompt that injects the computed trend stats, regional occurrence data, and IUCN conservation status directly into the context. The model (Claude, via the OpenAI-compatible SDK) is instructed to explain likely drivers of decline for that specific species in that region, compare local to global patterns, and flag low confidence when data is sparse. Critically, it's told not to invent statistics. The underlying numbers are displayed alongside the AI output so you can see exactly what it worked from — the point was to make the AI legible, not magical.

Call to action. The last panel generates concrete, region-specific steps: native plants, pesticide alternatives, local conservation groups. This was deliberately kept simple. The goal was to close the gap between "this is alarming" and "here's what I do on Tuesday."

Frontend. React 19 with TypeScript, Vite, and vanilla CSS. No component library — I wanted full control over the visual design, particularly the globe controls and the animated time slider.


Challenges

The data is messier than it looks. GBIF occurrence records include research-grade observations, casual sightings, museum specimens, and automated sensor logs — all mixed together. A spike in records for a given region might mean the population recovered, or it might mean a birdwatching club started submitting data that year. I had to be careful about what claims I let the AI make based on that.

Migration and seasonality skew everything. A migratory species observed in Norway in July and Tunisia in January isn't "two populations" — it's the same bird at different times. Filtering by month helped, but it also reduced the dataset significantly for some species. I ended up adding a monthly sub-slider so users can explore this themselves, rather than hiding the complexity.

Globe performance. Rendering thousands of points as individual meshes tanks the framerate fast. I switched to a single geometry with instanced rendering, which brought things back to 60fps even with the full GBIF response loaded.

Getting the AI to stay honest. Early prompts produced confident-sounding analysis even for species with only a handful of observations in a region. I added an explicit instruction to state confidence based on sample size, and I surface the raw \(n\) count in the UI so users can make their own judgment.


What I Learned

The technical stuff — Three.js instancing, GBIF's query structure, grounded prompting — was mostly tractable. The harder problem was design.

The version of this app that shows everything is useless. There are roughly 10,000 bird species on Earth; GBIF has records for most of them. Showing all of them at once produces an undifferentiated green smear on the globe. I settled on a curated watch list of about 100 well-studied migratory species, with a browser overlay where users can swap in whatever they care about. That constraint — picking 100 species instead of 10,000 — made the whole thing work.

The other thing I learned is that the time slider carries the demo. You can describe the bird population decline in words for as long as you want and people will nod politely. But drag the slider backward twenty years on the Barn Swallow and the dots visibly shrink. That's the moment where it stops being a statistic.


What's Next

The risk model is intentionally simple. A proper treatment would correct for search effort using GBIF's own effort metadata, weight observations by data quality grade, and account for range shifts driven by climate change (some species showing up in new areas aren't recovering — they're retreating northward). There's also a lot more that could be done with the AI layer: multi-species analysis, year-over-year change explanation, and cross-referencing with land-use change data.

But the core thing works. You can open it, pick a bird, and watch what's happened to it over the last two and a half decades. That was the goal.

Share this project:

Updates

Submission history