Seismic Globe pulls live earthquake data from the USGS and maps it onto an interactive 3D globe in real time. Click any earthquake and the globe rotates to face it — preserving your zoom level. Filter by magnitude. Watch the Ring of Fire trace itself. Every marker, boundary, camera movement, and atmospheric shader is hand-engineered. No map library. No 3D plugin. No shortcuts.
A list of coordinates is not a visualisation. The design work is deciding what a user understands at a glance.
The central UX question for any data visualisation is: what does the user need to understand first? For Seismic Globe, the answer was geography and scale. Where are earthquakes happening, and how serious are they? Everything else is secondary. That hierarchy drove every visual decision from the marker encoding to the camera behavior.
Markers are sized by magnitude and colored by depth — shallow quakes in red, intermediate in orange, deep in yellow. A user who has never heard of the Richter scale can still immediately see that the cluster of large red circles along the Pacific Rim is more significant than the scattered small ones in the Atlantic. The data speaks before the legend is read.
Clicking a marker rotates the globe to face it. The interaction was designed to preserve the user's current zoom level — only the direction changes. This sounds like a small detail, but it's the difference between an interface that respects the user's context and one that constantly resets it. If you've zoomed in to study Japan's seismic activity and click a specific event, you shouldn't be snapped back to a default view. You should land exactly where you intended to look.
The tectonic plate boundaries are rendered at radius 1.002 — just proud of the Earth surface — so they read as overlaid information rather than part of the terrain. That 0.2% clearance is an information design decision: the globe is the data, the lines are the context.
Technical implementation follows ↓
Every choice was deliberate. Here's the rationale.
| Rendering | React Three Fiber + Three.js R3F lets you compose a Three.js scene declaratively inside React — the 3D world and the 2D HUD live in the same component tree and share state without a bespoke event bridge. The Canvas runs full-screen behind everything; HTML overlays sit on top via R3F's Html component. |
| Markers | THREE.InstancedMesh Rendering 1,000 earthquake markers as individual meshes means 1,000 draw calls per frame. InstancedMesh collapses them into a single draw call — per-instance color and scale are encoded in typed arrays. Frame rate stays locked regardless of event count. |
| Atmosphere | Custom GLSL Fresnel shader The blue atmospheric rim is a custom ShaderMaterial using a Fresnel term — glow intensifies at grazing angles relative to the camera, mimicking real atmospheric scattering. No post-processing pass; it runs as a separate transparent sphere around the Earth mesh. |
| Tectonic lines | GeoJSON → BufferGeometry Real tectonic plate boundary data is parsed at runtime and projected onto the globe surface using the same geoToCartesian utility as the markers — each coordinate pair becomes a point on a THREE.Line at radius 1.002, just proud of the Earth surface. |
| Camera | OrbitControls + custom flyTo Clicking a marker triggers a smooth eased animation that repositions the camera to face that event. The fly preserves the user's current zoom distance — only the direction changes — so zooming in then clicking a marker doesn't reset the view. |
| Data | React Query + USGS GeoJSON API React Query handles stale-while-revalidate, background refetch, and deduplication. The USGS feed re-fetches every 5 minutes. A single types/earthquake.ts file types the full GeoJSON surface — coordinate tuples, nullable fields, alert levels — so the compiler catches bad data before it reaches the renderer. |
The globe was straightforward. The seams between canvas and DOM were the work.
A Next.js app is a document. A Three.js scene is a WebGL canvas. Getting them to share state, respond to the same events, and render in the correct layer order requires deliberate architecture. The HUD panels — live event count, selected earthquake card, filter controls — needed to read and write the same state the 3D markers were reacting to, with no prop-drilling through the Canvas boundary that R3F enforces.
Zustand acts as the shared membrane. The Canvas and all its 3D children sit in their own render tree; the HTML overlay sits in a sibling div. Both read from and write to the same globeStore — selected earthquake, magnitude filter, time window — with no awareness of each other. The store is the only bridge.
// useGlobeCamera.ts — zoom-preserving fly-to
const flyTo = useCallback((lat: number, lng: number) => {
// preserve current zoom — only change direction
const currentDistance = controls.object.position.length()
const endPos = geoToCartesian(lat, lng, currentDistance)
// eased animation over 1400ms
const ease = t < 0.5 ? 2*t*t : -1 + (4 - 2*t) * t
controls.object.position.lerpVectors(start, endPos, ease)
}, [])