← All projects

Imperium

An interactive, animated historical map covering 3,000+ years of political history — from 5000 BCE to 2026 CE. Watch empires rise and fall in real time, click any territory to explore it, and jump to any year in history.

In development
React 18 Vite 7 deck.gl MapLibre GL JS Node.js 24 Express SQLite Python GDAL Zustand Tailwind v4

Overview

Imperium came from a simple question: what did the political map of the world look like at any given moment in history? The existing tools were either paywalled, visually outdated, or covered only a narrow slice of time. The goal was something that felt as immediate as Google Maps but spanned all of recorded history.

The project is genuinely full stack — a Python data pipeline that ingests and normalises four independent historical GIS datasets into 5,427 yearly GeoJSON snapshots, a Node.js/Express API serving those files alongside a SQLite full-text search index, and a React client that renders them on a GPU-accelerated deck.gl map with smooth animated transitions, a playback timeline, guided tours, and a right-click place-history lookup.

Role

Solo — data pipeline, backend, frontend, map rendering

Status

In active development

Year

2025 – present

Architecture

The client fetches one GeoJSON file per year from the server and caches the last 50 in an LRU cache, using binary search over the sorted year list so timeline scrubbing never hits the network twice for the same year. The server exposes a small REST API for search, entity metadata, area history, and point-in-polygon history queries — all backed by a SQLite index built by the pipeline.

Client

  • React 18 + Zustand (3 stores)
  • deck.gl — 3-layer map (ghost, cultural, political)
  • MapLibre GL JS base tiles
  • 50-entry LRU snapshot cache
  • Binary search over year list
  • PWA + Workbox service worker

Server

  • Node.js 24 + Express
  • SQLite via node:sqlite (built-in)
  • FTS5 full-text search
  • 5,427 static GeoJSON snapshots
  • Entity metadata + area history API
  • Point-in-polygon history (ray-cast)

Data Pipeline

  • Python 3.x scripts (numbered sequence)
  • GDAL + mapshaper (geometry)
  • 4 source datasets merged by year
  • Wikidata bulk enrichment (1,672 slugs)
  • Capital coords via Wikidata QID → P36
  • Succession links (65 transitions)

Key features

Animated map layers

Three deck.gl layers compose the map: a political layer with 600ms fill-colour transitions and dashed borders for approximate ancient data; a translucent cultural layer for nomadic and cultural regions; and a ghost layer that flashes dark-red when a territory disappears year-to-year. Newly appeared territories flash gold for 600ms. Globe mode switches the projection to a 3D sphere view.

Timeline & playback

Scrub through any year from 5000 BCE to 2026 CE via a timeline slider or by typing a year directly. Four playback speeds (slow / normal / fast / ultra). Era jumper teleports to major historical periods. Keyboard shortcuts: Space to play/pause, ←/→ to step year by year, Ctrl+K for search.

Entity panel & place history

Click any territory to open a slide-in panel with a Wikipedia thumbnail and extract, date range, capital (rendered as a gold ★ on the map), an SVG sparkline of territory size over time, and preceded_by / succeeded_by navigation. Right-click anywhere on the map to see every polity that ever controlled that point, ray-cast across sampled snapshots.

Guided tours, leaderboard & search

Three narrated auto-advancing tours (Rise of Rome, Mongol Conquest, Age of Decolonization), each with pause/play, step dots, and a progress bar. A live leaderboard shows the top-5 largest empires for the current year, computed client-side using the Shoelace formula. Ctrl+K opens SQLite FTS5-powered full-text search. Shareable URLs encode the current year (?year=1453).

Data pipeline

Four independent historical GIS datasets covering different time ranges and regions are ingested, normalised, and merged into a unified yearly snapshot format by a numbered sequence of Python scripts.

Data sources

  • CShapes 2.0 — 1886–2019, annual, ~200 polities/year
  • Cliopatria / Seshat — 3400 BCE–1885 CE, worldwide
  • Aourednik Basemaps — cultural regions, 500 BCE–1885 CE
  • AWMC Classical Antiquity — Achaemenid, Macedonian, Hasmonean, Herodian kingdoms

Pipeline steps

  • 01–03 — Shapefile → GeoJSON, schema normalisation, source merge
  • 07–08 — Extend to 2026, manual post-2019 border corrections
  • 09 — Succession links (preceded_by / succeeded_by, 65 entries)
  • 10–11 — Wikidata enrichment: 1,672 Wikipedia slugs + capital coordinates
  • 05 — SQLite FTS5 search index build

Challenges

  • Merging four datasets that use different coordinate reference systems, naming conventions, and entity schemas — while preserving year-level granularity and avoiding gaps in the timeline — required careful upfront data modelling and a deterministic merge priority (CShapes > Cliopatria > Aourednik).
  • Serving 5,427 GeoJSON files efficiently without a CDN meant designing a caching strategy that worked well for both the common case (timeline scrubbing in one era) and cold loads. The 50-entry LRU cache with binary search over the year list keeps network requests minimal for normal use.
  • Rendering smooth 600ms colour transitions and ghost/flash animations on deck.gl while maintaining 60fps with potentially hundreds of polygons required careful layer composition and knowing when to let WebGL handle interpolation versus doing it in JS.
  • Ancient borders are inherently approximate — the data marks them as such, and the map needs to communicate that visually (dashed borders, lower opacity) without making the UI feel broken or untrustworthy.

What I learned

  • Working with geodata end-to-end — GDAL, mapshaper, GeoJSON, coordinate reference systems, and point-in-polygon algorithms — was entirely new territory. Understanding spatial data structures made the map rendering decisions much clearer.
  • SQLite's FTS5 full-text search is remarkably capable for this use case — fast, zero-dependency, and trivially embedded via the Node 24 built-in. Not every search problem needs a search service.
  • deck.gl's layer model forces you to think declaratively about what state drives what visual output. Building the ghost and flash layers taught me how to compose GPU-rendered layers cleanly without fighting the library.
  • Designing a data pipeline as a numbered, reproducible script sequence — rather than one monolithic processor — made debugging and re-running individual steps dramatically easier as the data grew.

What's next

A historical cities overlay showing major cities sized by population, with a toggle button, is the most visually compelling next feature. On the data side, disputed territories (Kosovo, Crimea, Western Sahara, Taiwan) need a dedicated correction script. Wikidata enrichment for peak area and peak population would make the entity panel significantly more informative. For deployment, the plan is a Dockerfile with Cloudflare R2 serving the 5,427 GeoJSON snapshots as static assets behind a CDN, which would remove the main server load bottleneck.