← Examples
USGS live feed · countries basemap · join

The planet's last 30 days of earthquakes

Every magnitude-4.5-or-greater earthquake of the past month, pulled live from the USGS feed and plotted on a mapjson world basemap. Circle size is magnitude; colour is depth, from shallow (red) to deep (blue).

fetching live quakes from USGS…

depth:  shallow
deep (700 km)size = magnitude

API Calls

https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=low
https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_month.geojson

Code

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <title>30 Days of Earthquakes</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <style>
    body { margin: 0; background: #070c15; }
    #map-wrap svg { width: 100%; height: auto; display: block; }
  </style>
</head>
<body>
  <p class="live" id="live" style="font:12px ui-monospace,monospace;color:#8aa0b8;padding:12px 16px;margin:0">fetching live quakes from USGS…</p>
  <div id="map-wrap"></div>

  <script src="https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js"></script>
  <script>
  const W = 1200, H = 620;
  const svg = d3.select("#map-wrap").append("svg").attr("viewBox", `0 0 ${W} ${H}`).style("cursor","grab");
  const tt = d3.select("body").append("div").style("position","fixed").style("pointer-events","none").style("opacity",0).style("background","#0b1220").style("color","#eaf2ff").style("font","11px ui-monospace,monospace").style("padding","5px 9px").style("border","1px solid #24405f").style("border-radius","4px").style("z-index",10);
  const projection = d3.geoNaturalEarth1().rotate([-11, 0]);
  const path = d3.geoPath(projection);
  const defs = svg.append("defs");
  const gl = defs.append("filter").attr("id","g").attr("x","-60%").attr("y","-60%").attr("width","220%").attr("height","220%");
  gl.append("feGaussianBlur").attr("stdDeviation","1.6").attr("result","b");
  const mm = gl.append("feMerge"); mm.append("feMergeNode").attr("in","b"); mm.append("feMergeNode").attr("in","SourceGraphic");
  const rMag = d3.scaleSqrt().domain([4.5,8]).range([1.6,15]).clamp(true);
  const cDepth = d3.scaleSequential(t => d3.interpolateTurbo(1 - t)).domain([0,300]).clamp(true);
  // one zoomable group holds land + quakes; borders use non-scaling strokes and dot radii are
  // counter-scaled by k, so zooming into a dense cluster (Japan, Indonesia) spreads the dots apart
  const gZoom = svg.append("g");
  const gLand = gZoom.append("g");
  const gQuakes = gZoom.append("g").attr("filter","url(#g)");
  const zoom = d3.zoom().scaleExtent([1,14]).extent([[0,0],[W,H]]).translateExtent([[0,0],[W,H]])
    .on("zoom", (e) => {
      gZoom.attr("transform", e.transform);
      gQuakes.selectAll("circle").attr("r", d=>rMag(d.properties.mag)/e.transform.k).attr("stroke-width", 0.5/e.transform.k);
    });
  svg.call(zoom);
  Promise.all([
    d3.json("https://api.mapjson.com/v1/geo?layer=countries&filter=world&detail=low&format=geojson"),
    d3.json("https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_month.geojson"),
  ]).then(([world, eq]) => {
    projection.fitExtent([[6,6],[W-6,H-6]], world);
    gLand.selectAll("path").data(world.features).join("path").attr("d", path)
      .attr("fill","#16202e").attr("stroke","#243346").attr("stroke-width",0.4).attr("vector-effect","non-scaling-stroke");
    const quakes = eq.features.slice().sort((a,b)=>(a.properties.mag||0)-(b.properties.mag||0));
    gQuakes.selectAll("circle").data(quakes).join("circle")
      .attr("cx", d=>projection(d.geometry.coordinates)[0]).attr("cy", d=>projection(d.geometry.coordinates)[1])
      .attr("r", d=>rMag(d.properties.mag)).attr("fill", d=>cDepth(d.geometry.coordinates[2]))
      .attr("fill-opacity",0.72).attr("stroke", d=>cDepth(d.geometry.coordinates[2])).attr("stroke-opacity",0.9).attr("stroke-width",0.5)
      .style("cursor","pointer")
      .on("mousemove",(e,d)=>{tt.style("opacity",1).style("left",(e.clientX+12)+"px").style("top",(e.clientY-8)+"px")
        .html(`<b>M ${d.properties.mag}</b> · ${d.geometry.coordinates[2]} km deep<br>${d.properties.place||""}`);})
      .on("mouseout",()=>tt.style("opacity",0));
    const live = document.getElementById("live");
    if (live) live.innerHTML = `<b>${quakes.length}</b> quakes M4.5+ in the last 30 days · live from USGS as of ${new Date(eq.metadata.generated).toLocaleString()} · drag to pan, scroll to zoom`;
  });
  </script>
</body>
</html>