---
format: typebulb/v1
name: Earthquake Depths
---

**code.tsx**

```tsx
import React, { useEffect, useRef, useState } from "react"
import { createRoot } from "react-dom/client"
import Globe from "globe.gl"
import * as THREE from "three"

interface Quake { lat: number; lng: number; depth: number; mag: number; place: string }

const BLUE_MARBLE = "https://unpkg.com/three-globe@2.31.1/example/img/earth-blue-marble.jpg"
const EARTH_RADIUS_KM = 6371

// depth color ramp, warm shallow → cool deep (seismology convention)
const STOPS: [number, [number, number, number]][] = [
  [0, [255, 75, 75]],
  [70, [255, 165, 60]],
  [150, [255, 225, 85]],
  [300, [110, 215, 115]],
  [500, [80, 145, 255]],
  [700, [165, 90, 255]],
]

function depthColor(d: number): [number, number, number] {
  const c = Math.max(0, Math.min(700, d))
  for (let i = 1; i < STOPS.length; i++) {
    const [d1, c1] = STOPS[i]
    if (c <= d1) {
      const [d0, c0] = STOPS[i - 1]
      const t = (c - d0) / (d1 - d0)
      return [0, 1, 2].map(k => (c0[k] + (c1[k] - c0[k]) * t) / 255) as [number, number, number]
    }
  }
  return [165 / 255, 90 / 255, 255 / 255]
}

const LEGEND_GRADIENT = `linear-gradient(to right, ${STOPS.map(([d, [r, g, b]]) => `rgb(${r},${g},${b}) ${(d / 700) * 100}%`).join(", ")})`

const VERT = `
  attribute float aSize;
  attribute vec3 aColor;
  varying vec3 vColor;
  varying float vFade;
  void main() {
    vColor = aColor;
    // dim points beyond the visible horizon (facing < cos of the limb angle)
    float facing = dot(normalize(position), normalize(cameraPosition));
    float horizon = 100.0 / length(cameraPosition);
    vFade = mix(0.18, 1.0, smoothstep(horizon - 0.4, horizon, facing));
    vec4 mv = modelViewMatrix * vec4(position, 1.0);
    gl_PointSize = aSize * (300.0 / -mv.z);
    gl_Position = projectionMatrix * mv;
  }
`
const FRAG = `
  varying vec3 vColor;
  varying float vFade;
  void main() {
    float d = length(gl_PointCoord - 0.5);
    if (d > 0.5) discard;
    gl_FragColor = vec4(vColor, smoothstep(0.5, 0.3, d) * 0.9 * vFade);
  }
`

function App() {
  const ref = useRef<HTMLDivElement>(null)
  const worldRef = useRef<any>(null)
  const geomRef = useRef<THREE.BufferGeometry | undefined>(undefined)
  const quakesRef = useRef<Quake[]>([])
  const depthScaleRef = useRef(3)
  const [status, setStatus] = useState<"loading" | "ready" | "error">("loading")
  const [stats, setStats] = useState<{ count: number; max?: Quake }>({ count: 0 })
  const [depthScale, setDepthScale] = useState(3)

  const applyDepth = (scale: number) => {
    const world = worldRef.current, geom = geomRef.current
    if (!world || !geom) return
    const pos = geom.getAttribute("position") as THREE.BufferAttribute
    quakesRef.current.forEach((q, i) => {
      const { x, y, z } = world.getCoords(q.lat, q.lng, -(q.depth * scale) / EARTH_RADIUS_KM)
      pos.setXYZ(i, x, y, z)
    })
    pos.needsUpdate = true
  }

  useEffect(() => {
    depthScaleRef.current = depthScale
    applyDepth(depthScale)
  }, [depthScale])

  useEffect(() => {
    const el = ref.current!
    let disposed = false
    const world: any = (Globe as any)()(el)
      .backgroundColor("rgba(0,0,0,0)")
      .globeImageUrl(BLUE_MARBLE)
      .showAtmosphere(true)
      .atmosphereColor("#4895ef")
      .atmosphereAltitude(0.12)
      .showGraticules(true)
      .width(el.clientWidth)
      .height(el.clientHeight)
    worldRef.current = world

    const globeMat = world.globeMaterial()
    globeMat.transparent = true
    globeMat.opacity = 0.5

    world.controls().autoRotate = true
    world.controls().autoRotateSpeed = 0.35

    // fit globe + atmosphere glow (1.12 radii) to 92% of the smaller viewport dimension
    const fitAltitude = () => {
      const halfFov = (world.camera().fov / 2) * Math.PI / 180
      const aspect = el.clientWidth / el.clientHeight
      return 1.12 / (0.92 * Math.tan(halfFov) * Math.min(1, aspect)) - 1
    }
    world.pointOfView({ lat: -15, lng: 170, altitude: fitAltitude() }, 0)

    const onResize = () => {
      world.width(el.clientWidth).height(el.clientHeight)
      world.pointOfView({ altitude: fitAltitude() })
    }
    window.addEventListener("resize", onResize)

    const geom = new THREE.BufferGeometry()
    const mat = new THREE.ShaderMaterial({
      vertexShader: VERT,
      fragmentShader: FRAG,
      transparent: true,
      depthWrite: false,
      depthTest: false, // interior points must not be occluded by the globe surface
    })
    const points = new THREE.Points(geom, mat)
    points.renderOrder = 3
    world.scene().add(points)

    ;(async () => {
      try {
        const start = new Date(Date.now() - 365 * 864e5).toISOString().slice(0, 10)
        const res = await fetch(
          `https://earthquake.usgs.gov/fdsnws/event/1/query?format=geojson&starttime=${start}&minmagnitude=4.5&limit=20000`
        )
        if (!res.ok) throw new Error("feed " + res.status)
        const json = await res.json()
        if (disposed) return
        const quakes: Quake[] = json.features
          .map((f: any) => ({
            lat: f.geometry?.coordinates?.[1],
            lng: f.geometry?.coordinates?.[0],
            depth: Math.max(0, f.geometry?.coordinates?.[2] ?? 0),
            mag: f.properties?.mag,
            place: f.properties?.place,
          }))
          .filter((q: Quake) => typeof q.mag === "number" && typeof q.lat === "number")

        const n = quakes.length
        const colors = new Float32Array(n * 3)
        const sizes = new Float32Array(n)
        quakes.forEach((q, i) => {
          colors.set(depthColor(q.depth), i * 3)
          sizes[i] = (2.2 + (Math.max(4.5, q.mag) - 4.5) * 2) * devicePixelRatio
        })
        geom.setAttribute("position", new THREE.BufferAttribute(new Float32Array(n * 3), 3))
        geom.setAttribute("aColor", new THREE.BufferAttribute(colors, 3))
        geom.setAttribute("aSize", new THREE.BufferAttribute(sizes, 1))
        geomRef.current = geom
        quakesRef.current = quakes
        applyDepth(depthScaleRef.current)

        const max = quakes.reduce<Quake | undefined>((a, b) => (b.mag > (a?.mag ?? -99) ? b : a), undefined)
        setStats({ count: n, max })
        setStatus("ready")
      } catch {
        if (!disposed) setStatus("error")
      }
    })()

    return () => {
      disposed = true
      window.removeEventListener("resize", onResize)
      geom.dispose()
      mat.dispose()
      try { world._destructor?.() } catch { /* best-effort */ }
    }
  }, [])

  return (
    <div className="wrap">
      <div className="hud">
        <div className="title">Earthquake depths</div>
        <div className="meta">
          {status === "loading" && "fetching a year of USGS data…"}
          {status === "ready" && (
            <>
              {stats.count.toLocaleString()} quakes · M 4.5+ · past 12 months
              {stats.max && <> · strongest <b>M {stats.max.mag.toFixed(1)}</b> at {Math.round(stats.max.depth)} km</>}
            </>
          )}
          {status === "error" && "couldn't reach the USGS archive"}
        </div>
        <div className="controls">
          <label>
            depth ×{depthScale}{depthScale === 1 && " (true scale)"}
            <input
              type="range" min={1} max={8} step={0.5} value={depthScale}
              onChange={e => setDepthScale(Number(e.target.value))}
            />
          </label>
          <div className="legend">
            <span>0 km</span>
            <div className="ramp" style={{ background: LEGEND_GRADIENT }} />
            <span>700 km</span>
          </div>
        </div>
      </div>
      <div className="globe" ref={ref} />
    </div>
  )
}

createRoot(document.getElementById("root")!).render(<App />)
```
**styles.css**

```css
* { box-sizing: border-box; }
html, body, #root { margin: 0; height: 100%; }
.wrap {
  position: relative;
  width: 100%;
  height: 100dvh;
  min-height: 420px;
  overflow: hidden;
  background: radial-gradient(circle at 50% 40%, #0a1430 0%, #04070e 75%);
  font: 13px system-ui, -apple-system, sans-serif;
  color: #e7ecf6;
}
.globe { position: absolute; inset: 0; }
.hud {
  position: absolute;
  top: 16px;
  left: 18px;
  z-index: 2;
  pointer-events: none;
  text-shadow: 0 1px 6px rgba(0, 0, 0, 0.85);
}
.title { font-size: 16px; font-weight: 600; letter-spacing: 0.2px; }
.meta { margin-top: 3px; font-size: 12px; opacity: 0.8; }
.meta b { color: #ff9d5c; font-weight: 600; }
.controls { margin-top: 10px; pointer-events: auto; }
.controls label { display: flex; flex-direction: column; gap: 3px; font-size: 12px; opacity: 0.9; }
.controls input { width: 160px; accent-color: #4895ef; }
.legend { display: flex; align-items: center; gap: 6px; margin-top: 8px; font-size: 11px; opacity: 0.85; }
.ramp { width: 120px; height: 8px; border-radius: 4px; }
```
**index.html**

```html
<div id="root"></div>
```
**config.json**

```json
{
  "description": "A translucent 3D Earth with a year of earthquakes plotted at their true depth beneath the surface.",
  "dependencies": {
    "react": "^19.2.7",
    "react-dom": "^19.2.7",
    "globe.gl": "^2.34.0",
    "three": "^0.180.0"
  }
}
```