A dotted globe you can scrub through time. Zoom into a place, see what was happening there, and follow the threads connecting it to everywhere else.
The name is the point. Historical dates are ranges, not instants — circa 1200, sometime in the third century — and Circa is built to show that rather than round it away. A record the source dated to a year is displayed as a year, with within a year written underneath it.
Status: early, but real. The globe, the timeline, the detail panel and connection arcs all work, and they are showing 417,968 real events from the UCDP Georeferenced Event Dataset — organised violence worldwide since 1989, under CC BY 4.0. The synthetic fixture it was built against has been retired.
- A globe drawn as a stipple of points masked to real coastlines
- A time slider with a density curve showing where records actually exist
- Playback that moves the window forward, with trails
- A detail panel, opened by clicking a point or zooming close to one, carrying the record's date, location, source, licence, and coverage bounds
Ordered so each step produces something you can look at. Nothing here is pure plumbing — the momentum on a project like this comes from watching the globe get better.
Done
| 0 · Environment | Toolchain, and verifying deck.gl's globe view exists and works. It is still exported as _GlobeView, so the version is pinned. |
| 1 · The globe | Golden-angle point field masked to real coastlines, jittered to kill moiré. Rotates, zooms. |
| 2 · Schema and pipeline | Canonical records, fuzzy time as Julian Day Numbers, source adapters, binary chunks, client loader. |
| 3 · Time | Window slider with a density curve, playback with trails, all of it kept out of React's render path. |
| 4 · Detail | Panel opened by click or by zooming close, carrying date, uncertainty, source, licence, and coverage. Plus a pre-rendered page per place. |
| 5 · Connections | Arcs between places. Every link in the window is drawn; selecting a place lifts its own out of the rest. |
| A real source | 417,968 UCDP events. Detail paged, place ids made URL-safe, and the whole presentation retuned for a corpus four orders of magnitude larger than the fixture. |
| Five sources | UCDP events, and four Correlates of War datasets — wars, militarized disputes, formal alliances, bilateral trade. 420,283 events and 39,600 links. Co-located records draw as one dot sized by count; neighbours merge by zoom and separate as the camera comes in. |
| Followable arcs | The places arcs end at are drawn as rings and can be selected. Hollow, because an event dot claims something happened there and a capital only stands in for a state. |
| Shareable views | The window, the camera and the selection live in the query string, so a find can be sent to someone. A truncated or hand-edited link opens the globe somewhere sensible rather than failing. |
| A 4 MB first load | Down from 11.5 MB. The client place registry holds only the places links point at; the full one is a build input outside public/, and place names ride in the detail files that are fetched on demand anyway. |
| Narration | Every record carries prose, and every narrative says who wrote it — the dataset (673, extracted from the PDFs Correlates of War publishes them in), or Circa, composed from fields. The three voices look different and say which they are. |
| /admin | Unlisted dashboard over a stats file the pipeline writes last: totals, per-source contributions, storage by artifact, and real records shown verbatim. |
| An arc budget | A 2012 window offers 12,377 connections and only a few hundred are readable, so a fixed budget is split evenly between sources and ranked within each — trade is dollars and war is battle deaths, and the two must never be sorted together. What is withheld is counted and stated. |
Next
| 6 · Accounts | Firebase Auth and saved views. The globe stays fully usable signed-out; accounts add persistence, never access. |
| 7 · Deploy | Firebase Hosting. Static export, no server. |
Blocked, not scheduled
| Connections after 2014 | Every Correlates of War dyadic file stops between 2005 and 2018, so the most recent decade has no links at all. Closing it needs an editorial decision rather than an adapter: GDELT is current but machine-coded from news, Wikidata is CC0 but uneven. |
| ACLED | Gated on its own terms, which unlike UCDP's are not plainly permissive. |
Deliberately later
Place-states (what was true somewhere, and when) · deep-linkable routes · curated tours · search · export · mobile polish · a flat-map view for detail the globe cannot show · historical boundaries.
Three that shape the code more than anything else:
- Nothing on screen without a source. Provenance is mandatory at ingest, so every record can be traced back to what published it.
- Uncertainty renders, it does not round. Time is stored as four bounds, not a date. Precision is the degenerate case, not the assumption.
- Absence is not evidence. An empty region usually means nothing was recorded, not that nothing happened. Sources declare what they cover so the app can tell the difference.
Design notes, decisions, and the reasoning behind them live in a separate Obsidian vault alongside this repo.
packages/core canonical schema, time, binary codecs — no I/O, no React
packages/geo build-time generation of the globe's dot field
packages/pipeline source adapters and the build pipeline
apps/web the Next.js app
Dependencies point one way: web → core, geo → core, pipeline → core.
Nothing depends on web, and core depends on nothing.
Exactly one module imports deck.gl (apps/web/src/globe/Globe.tsx), and exactly
one layer of code knows what any given data source looks like (its adapter).
Both boundaries are deliberate.
Requires Node 20+ (developed on 24, which strips TypeScript natively — no build step for the pipeline, and no test framework).
npm installBoth outputs are build artifacts. They are gitignored: regenerate rather than commit.
Source datasets are downloaded rather than committed — they belong to their publishers, and UCDP's alone is 261 MB.
The globe's dot field needs Natural Earth land polygons (public domain) at
packages/geo/.cache/land.geojson:
npm run generate:dotsEvent chunks, detail pages, the density histogram, the place registry and the
manifest. Needs UCDP GED at packages/pipeline/.cache/ucdp/GEDEvent_v26_1.csv,
from https://ucdp.uu.se/downloads/ged/ged261-csv.zip (CC BY 4.0), plus the
Correlates of War files each adapter names in its own error message — run it
and it will tell you exactly what is missing and where to put it:
npm run generate:eventsTo see the render path without land data, generate an unmasked sphere:
npm run generate:dots -- --no-masknpm run devnpm run buildProduces a fully static site in apps/web/out/. There is no server component —
the build output is plain files, and sign-in and per-user data run through
Firebase's client SDK with security rules as the authorization layer.
npm run typecheck
npm testTests use Node's built-in runner and Node 24's native TypeScript stripping, so
there is no test framework and no build step. erasableSyntaxOnly is on in
tsconfig.base.json for the same reason: syntax that emits code (enums,
parameter properties) would typecheck and then fail at runtime.
.claude/skills/ carries a vendored copy of Vercel's React and Next.js
performance guidelines, committed so contributors get the same guidance the
project was written against rather than each needing it installed locally. See
its ATTRIBUTION.md.
Several rules in it are load-bearing here rather than incidental:
- Import from
@deck.gl/coreand@deck.gl/layersdirectly. Thedeck.glumbrella package is a barrel that pulls in every layer pack. - Keep transient values out of React state. The time window changes on every frame of a drag; it is written straight to the DOM and to deck.gl, and React renders only on discrete changes.
- Load the globe with
next/dynamicandssr: false. WebGL has no server-side equivalent.
Not yet chosen for the code.
The data is separate and keeps its own terms regardless: UCDP GED is CC BY 4.0, which requires citation and permits commercial use — the citation is carried in the source manifest and shown in the app. Other datasets are less permissive; ACLED in particular restricts commercial use. Check each before adding an adapter for it.