Skip to content

Jonohobs/resource-atlas

Repository files navigation

Resource Atlas

An interactive 3D globe that makes the hidden resource burdens behind AI infrastructure visible — the mines, grids, rivers, fuel chains, and communities underneath the cloud.

Every cloud has a mine, a grid, a river, and a community underneath it.

Resource burden globe with displaced terrain

⚠️ PROTOTYPE — ALL DATA IS SYNTHETIC MOCK. Every number shipped in this repo is a deterministic placeholder generated by scripts/generate-mock-data.mjs. Values are illustrative only and must not be cited as measurements. The schema is built so real public datasets drop in without code changes — see How real data replaces this.


Quick start

npm install          # deps
npm run vendor:geo   # copy Natural Earth geometry into public/geo (also run by gen:data)
npm run gen:data     # (re)generate the synthetic datasets into public/data
npm run dev          # http://localhost:5173

Build for static hosting:

npm run build        # validates data → typechecks → bundles to dist/
npm run preview      # serve the production build

npm run build runs scripts/validate-data.mjs first; the build fails on any dangling reference, bad dataType, or out-of-range normalized value.

Requirements: Node 20+, npm. No backend, no API keys, no database.


What it shows

Four composable visualization modes over the same data (switch via the top tabs; fine-tune with the left-rail layer toggles):

Mode What you see
Heatmap Flat globe coloured by the selected metric (choropleth).
Terrain Moral topography — the globe surface rises and falls with the metric. The hero feature.
Spikes Facilities (mines / refineries / data centres / power plants) as towers, height ∝ capacity.
Flows Resource & value arcs (extraction → processing → AI infra → value capture, plus fuel routes) with travelling pulses.

Click any country, facility, or flow to open the inspector: raw value, normalized value, the exact terrain height it produces, source + real-world equivalent, year, confidence, and a colour-coded data-type badge (measured / estimated / modeled / speculative / mock). The burden × benefit scatter (Analysis) plots the resource-injustice quadrant.

Concentrated metric (extraction) Burden × benefit scatter
Mineral extraction peaks Scatter

Architecture

Stack: Vite + React 19 + TypeScript · @react-three/fiber + drei + raw three · zustand (state) · d3-geo / d3-scale · topojson-client + world-atlas. No charting framework, no backend, no vendor lock-in.

Why custom R3F layers (not three-globe): three-globe extrudes per-country polygon blocks; it cannot displace a single continuous surface. The moral- topography hero needs a watertight displaced sphere, so every layer is a custom, declarative R3F component. This also keeps the path from static JSON → pixels fully transparent and extensible.

Data flow:

public/data/*.json ─┐
                    ├─ loadData.ts ─→ joins (star schema) ─→ zustand store ─→ React UI + globe
public/geo/*.json ──┘                                   │
                                                        └─ countryRaster.ts ─→ equirectangular
                                                              country-ID image (built once)

The terrain technique (src/globe/useTerrainGeometry.ts + src/lib/countryRaster.ts):

  1. Draw every country polygon into an offscreen equirectangular canvas, each filled with a colour encoding its index (d3-geo handles winding, holes, the antimeridian). This is the country-ID rastersample(lat,lon) → countryId in O(1).
  2. Build a subdivided icosphere (IcosahedronGeometry, ~80–130k tris by device tier). Precompute once: for each vertex cache its unit direction + which country it falls in (one raster sample).
  3. Recompute on metric / exaggeration / mode change: pure typed-array writes — position = direction · radius, colour = ramp(normalized) — then recompute normals. A few ms for ~160k vertices; not per-frame work.
  4. Picking raycasts (accelerated by three-mesh-bvh via drei <Bvh>) → hit point → lat/lon → sample the same raster → country.

The feature array index is the country id everywhere (data + raster), and both the generator and the raster iterate the same TopoJSON in the same order — so the join can't drift.

CS notes. The data model is a star schema (dimensions countries / sources; facts kpis / facilities / flows). The raster lookup is an O(1) spatial hash that decouples geometry resolution from country count. State uses external-store subscription with selector equality (zustand) rather than Context's reference-identity propagation, so a slider drag re-renders only the terrain — not every consumer.


Data schema

Static files under public/data/ (swappable / mirrorable). Types: src/types/data.ts.

File Role
countries.json Dimension. id = Natural Earth feature index (the join key); naturalEarthId, centroid, region.
kpis.json Fact: one atomic observation per country × kpiType. Carries value, min/max, precomputed normalized, normalization, direction, dataType, sourceId, modeledOnSourceId.
scores.json Derived composites (burden_score, local_benefit_score), kept separate from raw KPIs with an explicit formula + inputKpiTypes + weights so the opinionated parts are auditable.
facilities.json Mines / refineries / data centres / power plants (lat/lon, capacity).
flows.json Extraction → processing → infra → value-capture + fuel routes.
sources.json The synthetic generator plus the real datasets each metric maps onto.
stories.json Guided narratives over the flows (schema present; guided UI deferred to v0.2).
manifest.json Self-describing dataset list (version, date, license) for mirrors.

KPI categories: water stress · mineral extraction · mineral processing share · grid carbon intensity · AI/data-centre demand · HDI · Gini · (+ derived burden / local-benefit scores).

Terrain displacement formula

Documented and applied identically in code and legend:

normalized   = (value − min) / (max − min)        # precomputed per metric, in the data
peaks-only:    heightOffset = normalized · MAX_DISPLACEMENT · exaggeration
peaks+valleys: heightOffset = (normalized · 2 − 1) · MAX_DISPLACEMENT · exaggeration
finalRadius  = BASE_RADIUS + heightOffset

MAX_DISPLACEMENT and BASE_RADIUS live in src/lib/constants.ts. v0 normalizes every metric linearly against its dataset range — for the concentrated metrics (extraction, processing, compute) this makes a few places tower while the rest stay flat, which is the concentration story. log and percentile strategies are implemented (scripts/generate-mock-data.mjs / src/lib/loadData.ts) for future smooth metrics. Peaks+valleys mode and high exaggeration show an in-UI warning that exaggerated/signed 3D terrain can mislead — heights are normalized, not to scale.


Trust & safety posture

Because every country is populated (the user chose full-world synthetic over a sparse curated set), honesty is enforced structurally:

  • A persistent banner states all data is synthetic — never hidden.
  • A dataType badge on every datum (mock = dashed amber) — the most prominent honesty affordance, never buried.
  • The generator is deterministic and seeded — values are reproducible and auditable, not random per-frame bumps; the globe reads committed values.
  • Curated story countries (DRC cobalt, Chile/Australia lithium, China processing, Ireland/US/Nordic data centres, Gulf fuel, …) are speculative (shaped to real patterns), not exact figures; everything else is mock.
  • Flows and stories are framed as attributed illustrations — no naming-and- blaming specific operators on invented numbers.
  • Derived scores carry their formula + weights so the opinionated composite is inspectable, kept separate from raw observations.

How real data replaces this

Swap one public/data/*.json for a real export with the same shape, set each datum's dataType (measured/estimated/…) and sourceId, and re-run npm run validate:data. No code changes. Each KPI already records the dataset it's designed to be replaced by (modeledOnSourceId):

Field Real-world source
water_stress WRI Aqueduct
mineral_production / reserves USGS Mineral Commodity Summaries, BGS
mineral_processing_share / trade flows UN Comtrade / OEC
grid_carbon_intensity / energy mix Our World in Data
datacentre_demand IEA
HDI UNDP HDR
Gini / poverty / GDP / resource rents World Bank Open Data
facilities / power plants Global Energy Monitor, Open Supply Hub
extraction-conflict cases EJAtlas

Keep raw (kpis.json), derived (scores.json), and geometry (public/geo) conceptually separate. Verify any controversial figure against its primary source before publishing; attribute every claim.


Decentralization / static hosting

  • 100% static — npm run build emits a dist/ you can serve from any static host, a CDN, or a python -m http.server. No backend, no API keys.
  • Data is plain JSON + public-domain TopoJSON — easy to download, mirror, diff, and version. manifest.json makes a mirror self-describing.
  • IPFS-ready: the build is content-addressable as-is. import.meta.env.BASE_URL is respected so it works under a subpath / IPFS gateway. (vite build --base ./ for fully relative asset paths.)
  • Open formats throughout (JSON, TopoJSON); the schema also accommodates CSV / GeoJSON / PMTiles / Parquet pipelines feeding the same JSON contract.

Repo structure

public/
  geo/countries-110m.json   Natural Earth 110m (public domain; vendored)
  data/*.json               synthetic datasets (generated)
scripts/
  generate-mock-data.mjs    seeded full-world synthetic generator
  validate-data.mjs         referential-integrity + enum + range gate (build step)
  vendor-geo.mjs            copy geometry into public/geo
src/
  types/data.ts             schema (single source of truth)
  state/store.ts            zustand store (ui · data · selection)
  lib/                      geo math · country raster · ramps · loader · device tier
  globe/                    terrain hero · base/atmosphere/graticule · spikes · arcs · picking
  components/               TopBar · Controls · Inspector · Legend · Ranking · Scatter
  ui/primitives.tsx         hand-rolled Toggle / Slider / Dropdown

Scripts

Script Purpose
npm run dev Vite dev server
npm run build validate data → tsc -bvite build
npm run gen:data regenerate synthetic datasets (deterministic)
npm run validate:data data integrity gate
npm run vendor:geo copy Natural Earth geometry into public/geo
npm run lint ESLint

License

Code: Apache-2.0 (see LICENSE / NOTICE). Country geometry: Natural Earth, public domain. Shipped data values: synthetic — see public/data/sources.json and the table above for the real datasets they are designed to be replaced by.

About

Prototype 3D globe of AI resource burdens (React Three Fiber)

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors