Skip to content

Instantly share code, notes, and snippets.

@stanwmusic
Created February 26, 2026 18:33
Show Gist options
  • Select an option

  • Save stanwmusic/32521befbf0fa6261f63d67d522f4c93 to your computer and use it in GitHub Desktop.

Select an option

Save stanwmusic/32521befbf0fa6261f63d67d522f4c93 to your computer and use it in GitHub Desktop.
Live Earthquake Monitor
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://d3js.org/topojson.v3.min.js"></script>
<svg id="map"></svg>
<div id="info" class="panel">
<h1>Earthquake Monitor</h1>
<h3>www.Stanwilliams.org</h3>&nbsp;
<div class="live"><div class="live-dot"></div>USGS Live Feed</div>
<p style="color:#555;line-height:1.5">
Real-time seismic activity. Ring size indicates magnitude. Color shows depth.
</p>
<div class="scale">
<span>M2</span>
<div class="scale-bar"></div>
<span>M8+</span>
</div>
</div> -->
<div id="stats" class="panel">
<div class="stat-row">
<div class="stat-val" id="count">0</div>
<div class="stat-label">Events</div>
</div>
<div class="stat-row">
<div class="stat-val" id="max-mag">0.0</div>
<div class="stat-label">Max Magnitude</div>
</div>
</div>
<div id="tooltip"></div>
<div id="time" class="panel">
<span id="updated">Loading...</span>
</div>
<div id="controls">
<button class="btn active" data-period="day">24h</button>
<button class="btn" data-period="week">7 Days</button>
<button class="btn" data-period="month">30 Days</button>
</div>

Live Earthquake Monitor

A real-time global earthquake monitor using the USGS GeoJSON feeds. D3 + TopoJSON (Natural Earth projection). Data refresh every 5min. Just for fun. :)

A Pen by Stan Williams on CodePen.

License.

const svg = d3.select("#map");
const tooltip = document.getElementById("tooltip");
let width, height, projection, path;
let landGroup, quakeGroup;
let earthquakes = [];
let currentPeriod = "day";
function getMagColor(mag, depth) {
if (depth < 70) return `rgba(200, 80, 80, ${0.5 + mag * 0.06})`;
if (depth < 300) return `rgba(200, 160, 80, ${0.5 + mag * 0.06})`;
return `rgba(80, 120, 200, ${0.5 + mag * 0.06})`;
}
function init() {
width = window.innerWidth;
height = window.innerHeight;
svg.attr("width", width).attr("height", height);
projection = d3.geoNaturalEarth1()
.scale(width / 5.5)
.translate([width / 2, height / 2]);
path = d3.geoPath().projection(projection);
landGroup = svg.append("g");
quakeGroup = svg.append("g");
// Load world map
d3.json("https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json").then(world => {
const countries = topojson.feature(world, world.objects.countries);
landGroup.selectAll("path")
.data(countries.features)
.enter()
.append("path")
.attr("class", "land")
.attr("d", path);
fetchEarthquakes();
});
}
async function fetchEarthquakes() {
const urls = {
day: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_day.geojson",
week: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/2.5_week.geojson",
month: "https://earthquake.usgs.gov/earthquakes/feed/v1.0/summary/4.5_month.geojson"
};
try {
const response = await fetch(urls[currentPeriod]);
const data = await response.json();
earthquakes = data.features
.map(f => ({
id: f.id,
mag: f.properties.mag,
place: f.properties.place,
time: f.properties.time,
lon: f.geometry.coordinates[0],
lat: f.geometry.coordinates[1],
depth: f.geometry.coordinates[2]
}))
.filter(q => q.mag >= 2.5);
document.getElementById("count").textContent = earthquakes.length;
const maxMag = Math.max(...earthquakes.map(q => q.mag));
document.getElementById("max-mag").textContent = maxMag.toFixed(1);
document.getElementById("updated").textContent = `Updated: ${new Date().toLocaleTimeString()}`;
drawQuakes();
} catch (err) {
console.error("Failed to fetch earthquakes:", err);
earthquakes = generateSampleData();
drawQuakes();
}
}
function generateSampleData() {
const samples = [
{ lat: 35.6, lon: 139.7, mag: 4.2, place: "Near Tokyo, Japan", depth: 35 },
{ lat: 37.4, lon: -122.1, mag: 3.8, place: "San Francisco Bay Area", depth: 12 },
{ lat: -33.4, lon: -70.6, mag: 5.1, place: "Near Santiago, Chile", depth: 45 },
{ lat: 38.9, lon: 43.4, mag: 4.5, place: "Eastern Turkey", depth: 10 },
{ lat: -6.2, lon: 106.8, mag: 4.8, place: "Near Jakarta, Indonesia", depth: 55 },
{ lat: 19.4, lon: -99.1, mag: 3.9, place: "Near Mexico City", depth: 20 },
{ lat: 36.2, lon: 28.0, mag: 4.1, place: "Dodecanese Islands, Greece", depth: 15 },
{ lat: -4.6, lon: 122.5, mag: 5.5, place: "Sulawesi, Indonesia", depth: 100 },
{ lat: 51.5, lon: -178.5, mag: 4.7, place: "Andreanof Islands, Alaska", depth: 30 },
{ lat: -22.9, lon: -68.2, mag: 4.3, place: "Antofagasta, Chile", depth: 110 }
];
return samples.map((s, i) => ({
id: `sample-${i}`,
...s,
time: Date.now() - Math.random() * 24 * 60 * 60 * 1000
}));
}
function drawQuakes() {
quakeGroup.selectAll("*").remove();
// Sort by magnitude (draw smaller first)
const sorted = [...earthquakes].sort((a, b) => a.mag - b.mag);
sorted.forEach((quake, i) => {
const pos = projection([quake.lon, quake.lat]);
if (!pos) return;
const baseRadius = Math.max(3, Math.pow(2, quake.mag) * 0.6);
const color = getMagColor(quake.mag, quake.depth);
const age = (Date.now() - quake.time) / (1000 * 60 * 60);
// Pulsing ring for recent quakes
if (age < 12) {
const g = quakeGroup.append("g")
.attr("transform", `translate(${pos[0]}, ${pos[1]})`);
g.append("circle")
.attr("r", baseRadius)
.attr("fill", "none")
.attr("stroke", color)
.attr("stroke-width", 1.5)
.style("animation", "pulse 2s ease-out infinite")
.style("animation-delay", `${i * 0.1}s`);
}
// Main circle
quakeGroup.append("circle")
.attr("cx", pos[0])
.attr("cy", pos[1])
.attr("r", baseRadius)
.attr("fill", color)
.attr("stroke", "rgba(255,255,255,0.2)")
.attr("stroke-width", 1)
.datum(quake)
.on("mouseenter", function(event, d) {
d3.select(this).attr("stroke", "#fff").attr("stroke-width", 2);
showTooltip(event, d);
})
.on("mouseleave", function() {
d3.select(this).attr("stroke", "rgba(255,255,255,0.2)").attr("stroke-width", 1);
tooltip.style.opacity = "0";
});
// Magnitude label for large quakes
if (quake.mag >= 5) {
quakeGroup.append("text")
.attr("x", pos[0])
.attr("y", pos[1] + baseRadius + 12)
.attr("text-anchor", "middle")
.attr("fill", "#888")
.attr("font-size", "9px")
.text(quake.mag.toFixed(1));
}
});
}
function showTooltip(event, quake) {
const timeAgo = Math.round((Date.now() - quake.time) / (1000 * 60 * 60));
tooltip.innerHTML = `
<h3>M${quake.mag.toFixed(1)} Earthquake</h3>
<div class="row"><span>Location</span><span class="val">${quake.place}</span></div>
<div class="row"><span>Depth</span><span class="val">${quake.depth.toFixed(0)} km</span></div>
<div class="row"><span>Time</span><span class="val">${timeAgo}h ago</span></div>
<div class="row"><span>Coordinates</span><span class="val">${quake.lat.toFixed(2)}, ${quake.lon.toFixed(2)}</span></div>
`;
tooltip.style.opacity = "1";
tooltip.style.left = Math.min(event.pageX + 15, window.innerWidth - 200) + "px";
tooltip.style.top = Math.min(event.pageY + 15, window.innerHeight - 150) + "px";
}
// Controls
document.querySelectorAll(".btn").forEach(btn => {
btn.addEventListener("click", function() {
document.querySelectorAll(".btn").forEach(b => b.classList.remove("active"));
this.classList.add("active");
currentPeriod = this.dataset.period;
fetchEarthquakes();
});
});
window.addEventListener("resize", () => {
width = window.innerWidth;
height = window.innerHeight;
svg.attr("width", width).attr("height", height);
projection.scale(width / 5.5).translate([width / 2, height / 2]);
landGroup.selectAll("path").attr("d", path);
drawQuakes();
});
init();
// Refresh data every 5 minutes
setInterval(fetchEarthquakes, 5 * 60 * 1000);
<script src="https://d3js.org/d3.v7.min.js"></script>
<script src="https://d3js.org/topojson.v3.min.js"></script>
* { margin: 0; padding: 0; box-sizing: border-box; }
html, body {
width: 100%;
height: 100%;
overflow: hidden;
background: #0a0a0a;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
}
#map { width: 100%; height: 100%; }
.land { fill: #151515; stroke: #252525; stroke-width: 0.5px; }
.panel {
position: absolute;
background: rgba(0, 0, 0, 0.9);
border: 1px solid #222;
padding: 14px;
color: #888;
font-size: 10px;
}
/* Earthquake Monitor panel */
#info {
top: 20px;
right: 200px; /* was left 10 px*/
max-width: 220px;
}
#info h1 {
font-size: 10px;
font-weight: 500;
color: #ff0000;
margin-bottom: 8px;
text-transform: uppercase;
letter-spacing: 2px;
}
#info h3 { /* I added this h3 block 2-26-2026 Stanwmusic*/
font-size: 10px;
font-weight: 200;
color: #f00;
margin-bottom: 8px;
text-transform: lowercase;
letter-spacing: 2px;
}
.live {
display: flex;
align-items: center;
gap: 6px;
margin-bottom: 10px;
font-size: 9px;
color: #c44;
}
.live-dot {
width: 6px;
height: 6px;
background: #c44;
border-radius: 50%;
animation: blink 1.5s infinite;
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0.3; }
}
.scale {
display: flex;
align-items: center;
gap: 8px;
margin-top: 10px;
font-size: 9px;
color: #555;
}
.scale-bar {
flex: 1;
height: 4px;
background: linear-gradient(to right, #2a5, #ca4, #c44);
border-radius: 2px;
}
#stats {
top: 20px;
right: 20px;
text-align: right;
}
.stat-val { font-size: 20px; color: #ff0; font-weight: 300; }
.stat-label { font-size: 9px; color: #999; text-transform: uppercase; margin-top: 2px; }
.stat-row { margin-bottom: 12px; }
#tooltip {
position: absolute;
background: rgba(0, 0, 0, 0.95);
border: 1px solid #333;
padding: 12px;
color: #aaa;
font-size: 10px;
pointer-events: none;
opacity: 0;
transition: opacity 0.15s;
z-index: 20;
max-width: 280px;
}
#tooltip h3 { font-size: 11px; font-weight: 500; margin-bottom: 8px; color: #fff; }
#tooltip .row { display: flex; justify-content: space-between; margin: 3px 0; gap: 15px; }
#tooltip .val { color: #666; }
#time {
bottom: 20px;
left: 20px;
}
#controls {
position: absolute;
bottom: 20px;
right: 20px;
display: flex;
gap: 1px;
background: #222;
}
.btn {
background: #111;
border: none;
color: #555;
padding: 8px 14px;
font-size: 9px;
cursor: pointer;
transition: all 0.15s;
text-transform: uppercase;
letter-spacing: 1px;
font-family: inherit;
}
.btn:hover { color: #999; background: #1a1a1a; }
.btn.active { color: #fff; background: #222; }
.quake-pulse {
animation: pulse 2s ease-out infinite;
}
@keyframes pulse {
0% { r: 5; opacity: 0.8; }
100% { r: 30; opacity: 0; }
}
@stanwmusic
Copy link
Author

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment