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…
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
<!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>