natural disasters

DISASTER HISTORY

Search historical earthquakes, storms, volcanoes, wildfires & floods by date range, type, and severity. Powered by NASA EONET and USGS — no signup required.

Data sources USGS — Full earthquake history since 1900. Queries return up to 20,000 results per request. Magnitude, depth, coordinates, and wave data included.
NASA EONET — Storms, volcanoes, wildfires, floods, and 10+ other natural event categories. Coverage from 2010 onwards, real-time satellite sourced.
← back to live map
Developer Reference

Core Algorithm & Standalone Script

Standalone, zero-dependency JavaScript implementation powering this tool. Free to inspect, copy, and build upon.

(function () {
    /* ── CONFIG ────────────────────────────────────────── */
    const USGS_BASE  = 'https://earthquake.usgs.gov/fdsnws/event/1/query';
    const EONET_BASE = 'https://eonet.gsfc.nasa.gov/api/v3/events';

    const CAT_META = {
      earthquakes:  { label: 'Earthquake',   color: '#ff0000', icon: '≋', bg: 'rgba(255,0,0,0.15)' },
      severeStorms: { label: 'Severe Storm', color: '#f5c518', icon: '⌁', bg: 'rgba(245,197,24,0.15)' },
      volcanoes:    { label: 'Volcano',      color: '#9d00ff', icon: '△', bg: 'rgba(157,0,255,0.15)' },
      wildfires:    { label: 'Wildfire',     color: '#ff8000', icon: '⬡', bg: 'rgba(255,128,0,0.15)' },
      floods:       { label: 'Flood',        color: '#4488ff', icon: '≈', bg: 'rgba(68,136,255,0.15)' },
      landslides:   { label: 'Landslide',    color: '#aa8855', icon: '◿', bg: 'rgba(170,136,85,0.15)' },
      seaLakeIce:   { label: 'Sea/Lake Ice', color: '#aaddff', icon: '❄', bg: 'rgba(170,221,255,0.15)' },
      drought:      { label: 'Drought',      color: '#cc9944', icon: '○', bg: 'rgba(204,153,68,0.15)' },
      dustHaze:     { label: 'Dust / Haze',  color: '#bbaa88', icon: '≀', bg: 'rgba(187,170,136,0.15)' },
      tempExtremes: { label: 'Temp Extreme', color: '#ff8855', icon: '◈', bg: 'rgba(255,136,85,0.15)' },
      other:        { label: 'Other',        color: '#888888', icon: '⬤', bg: 'rgba(136,136,136,0.15)' },
    };

    function getMeta(catId) { return CAT_META[catId] || CAT_META.other; }

    /* ── DATE HELPERS ──────────────────────────────────── */
    function toISODate(d) { return d.toISOString().split('T')[0]; }
    function daysAgo(n) {
      const d = new Date();
      d.setDate(d.getDate() - n);
      return d;
    }
    function setDates(start, end) {
      document.getElementById('startDate').value = toISODate(start);
      document.getElementById('endDate').value = toISODate(end);
    }

    /* ── PRESETS ───────────────────────────────────────── */
    const today = new Date();
    document.getElementById('endDate').value = toISODate(today);
    document.getElementById('startDate').value = toISODate(daysAgo(30));

    document.querySelectorAll('.preset-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        const days = parseInt(btn.dataset.days);
        setDates(daysAgo(days), today);
      });
    });

    // Show/hide magnitude field based on event type
    const eventTypeEl = document.getElementById('eventType');
    const magFieldEl  = document.getElementById('magField');
    eventTypeEl.addEventListener('change', () => {
      const isEq = eventTypeEl.value === 'earthquakes' || eventTypeEl.value === 'all';
      magFieldEl.style.opacity = isEq ? '1' : '0.4';
    });

    /* ── FETCH USGS ────────────────────────────────────── */
    async function fetchUSGS(start, end, minMag, limit) {
      const params = new URLSearchParams({
        format: 'geojson',
        starttime: start,
        endtime: end,
        orderby: 'time',
        limit: Math.min(limit, 500),
      });
      if (minMag) params.set('minmagnitude', minMag);

      const res = await fetch(`${USGS_BASE}?${params}`);
      if (!res.ok) throw new Error(`USGS error ${res.status}`);
      const data = await res.json();

      return (data.features || []).map(f => {
        const p = f.properties;
        return {
          id:     f.id,
          title:  p.title || `M${p.mag?.toFixed(1)} Earthquake`,
          catId:  'earthquakes',
          date:   new Date(p.time),
          mag:    p.mag,
          place:  p.place,
          url:    p.url,
          source: 'USGS',
          depth:  f.geometry.coordinates[2],
        };
      });
    }

    /* ── FETCH EONET ───────────────────────────────────── */
    async function fetchEONET(start, end, catId, limit) {
      const params = new URLSearchParams({
        status: 'all',
        start,
        end,
        limit: Math.min(limit, 500),
      });
      if (catId && catId !== 'all' && catId !== 'earthquakes') {
        params.set('category', catId);
      }

      const res = await fetch(`${EONET_BASE}?${params}`);
      if (!res.ok) throw new Error(`EONET error ${res.status}`);
      const data = await res.json();

      return (data.events || []).map(ev => {
        const catData = ev.categories?.[0];
        const geoms = ev.geometry || ev.geometries || [];
        const geom = geoms.find(g => g.type === 'Point') || geoms[0];
        const [lng, lat] = geom?.coordinates || [null, null];
        return {
          id:     ev.id,
          title:  ev.title,
          catId:  catData?.id || 'other',
          date:   new Date(geom?.date || Date.now()),
          mag:    null,
          place:  null,
          url:    ev.link,
          source: 'NASA EONET',
          lat, lng,
        };
      }).filter(e => e.catId !== 'earthquakes'); // avoid duplicates
    }

    /* ── SORT ──────────────────────────────────────────── */
    let currentResults = [];
    let currentSort = 'date-desc';
    let currentPage = 0;
    const PAGE_SIZE = 50;

    function sortEvents(events, sort) {
      const arr = [...events];
      if (sort === 'date-desc') arr.sort((a, b) => b.date - a.date);
      else if (sort === 'date-asc') arr.sort((a, b) => a.date - b.date);
      else if (sort === 'mag-desc') arr.sort((a, b) => (b.mag || 0) - (a.mag || 0));
      return arr;
    }

    document.querySelectorAll('.sort-btn').forEach(btn => {
      btn.addEventListener('click', () => {
        document.querySelectorAll('.sort-btn').forEach(b => b.classList.remove('active'));
        btn.classList.add('active');
        currentSort = btn.dataset.sort;
        currentPage = 0;
        renderResults();
      });
    });

    /* ── RENDER RESULTS ────────────────────────────────── */
    function fmtDate(date) {
      return date.toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' })
        + ' ' + date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) + ' UTC';
    }

    function renderResults() {
      const sorted = sortEvents(currentResults, currentSort);
      const total  = sorted.length;
      const start  = currentPage * PAGE_SIZE;
      const page   = sorted.slice(start, start + PAGE_SIZE);

      document.getElementById('resultsCount').textContent = `${total} events`;

      // Source breakdown
      const usgsCount  = currentResults.filter(e => e.source === 'USGS').length;
      const eonetCount = currentResults.filter(e => e.source === 'NASA EONET').length;
      const splitEl = document.getElementById('sourceSplit');
      splitEl.innerHTML = [
        usgsCount  ? `<div class="source-chip"><div class="source-dot" style="background:#ff2200"></div>${usgsCount} from USGS</div>` : '',
        eonetCount ? `<div class="source-chip"><div class="source-dot" style="background:#f5c518"></div>${eonetCount} from NASA EONET</div>` : '',
      ].join('');

      if (!total) {
        document.getElementById('resultsList').innerHTML = `
          <div class="results-empty">
            <span class="icon">◎</span>
            No events found for the selected criteria.<br>Try expanding the date range or lowering the minimum magnitude.
          </div>`;
        document.getElementById('pagination').style.display = 'none';
        return;
      }

      const html = page.map(ev => {
        const meta = getMeta(ev.catId);
        const magClass = ev.mag >= 7 ? 'big' : ev.mag >= 5.5 ? 'med' : '';
        const magDisplay = ev.mag !== null
          ? `<div class="event-mag ${magClass}">M${ev.mag.toFixed(1)}</div>` : '';
        const depthLine = ev.depth != null
          ? `<span class="event-place">depth ${ev.depth.toFixed(0)} km</span>` : '';
        const srcParam = ev.source === 'USGS' ? 'usgs' : 'eonet';
        const linkEl = `<a class="event-link" href="/disasters/detail.html?id=${encodeURIComponent(ev.id)}&source=${srcParam}">details →</a>`;

        return `
          <div class="event-item">
            <div class="event-icon" style="background:${meta.bg};border:1px solid ${meta.color}33;">
              <span style="color:${meta.color}">${meta.icon}</span>
            </div>
            <div class="event-body">
              <div class="event-title">${ev.title}</div>
              <div class="event-meta">
                <span class="event-type" style="color:${meta.color};border-color:${meta.color}44;background:${meta.bg};">${meta.label}</span>
                ${ev.place ? `<span class="event-place">${ev.place}</span>` : ''}
                ${depthLine}
              </div>
              ${linkEl}
            </div>
            <div class="event-right">
              <div class="event-date">${fmtDate(ev.date)}</div>
              ${magDisplay}
              <div class="event-source">${ev.source}</div>
            </div>
          </div>`;
      }).join('');

      document.getElementById('resultsList').innerHTML = html;

      // Pagination
      const totalPages = Math.ceil(total / PAGE_SIZE);
      const paginationEl = document.getElementById('pagination');
      if (totalPages > 1) {
        paginationEl.style.display = 'flex';
        document.getElementById('pageInfo').textContent =
          `Page ${currentPage + 1} of ${totalPages}`;
        document.getElementById('prevPage').disabled = currentPage === 0;
        document.getElementById('nextPage').disabled = currentPage >= totalPages - 1;
      } else {
        paginationEl.style.display = 'none';
      }
    }

    document.getElementById('prevPage').addEventListener('click', () => {
      if (currentPage > 0) { currentPage--; renderResults(); window.scrollTo({ top: 300, behavior: 'smooth' }); }
    });
    document.getElementById('nextPage').addEventListener('click', () => {
      const max = Math.ceil(currentResults.length / PAGE_SIZE) - 1;
      if (currentPage < max) { currentPage++; renderResults(); window.scrollTo({ top: 300, behavior: 'smooth' }); }
    });

    /* ── SEARCH ────────────────────────────────────────── */
    const searchBtn  = document.getElementById('searchBtn');
    const clearBtn   = document.getElementById('clearBtn');
    const statusEl   = document.getElementById('searchStatus');

    async function doSearch() {
      const startVal  = document.getElementById('startDate').value;
      const endVal    = document.getElementById('endDate').value;
      const catVal    = document.getElementById('eventType').value;
      const magVal    = document.getElementById('minMag').value;
      const limitVal  = parseInt(document.getElementById('limitCount').value);

      if (!startVal || !endVal) {
        statusEl.textContent = 'Please select a date range.';
        statusEl.style.color = 'var(--error)';
        return;
      }
      if (new Date(startVal) > new Date(endVal)) {
        statusEl.textContent = 'Start date must be before end date.';
        statusEl.style.color = 'var(--error)';
        return;
      }

      searchBtn.disabled = true;
      searchBtn.textContent = 'Searching…';
      statusEl.textContent  = '';
      document.getElementById('resultsSection').style.display = 'none';
      currentResults = [];
      currentPage = 0;

      try {
        const fetches = [];

        const needUSGS = catVal === 'all' || catVal === 'earthquakes';
        const needEONET = catVal === 'all' || (catVal !== 'earthquakes');

        if (needUSGS) {
          fetches.push(
            fetchUSGS(startVal, endVal, magVal, limitVal).catch(err => {
              console.warn('USGS fetch failed:', err);
              return [];
            })
          );
        } else {
          fetches.push(Promise.resolve([]));
        }

        if (needEONET) {
          fetches.push(
            fetchEONET(startVal, endVal, catVal, limitVal).catch(err => {
              console.warn('EONET fetch failed:', err);
              return [];
            })
          );
        } else {
          fetches.push(Promise.resolve([]));
        }

        const [usgsEvents, eonetEvents] = await Promise.all(fetches);
        currentResults = [...usgsEvents, ...eonetEvents];

        const titleRange = `${startVal} → ${endVal}`;
        const catLabel = catVal === 'all' ? 'all types' : (CAT_META[catVal]?.label || catVal);
        document.getElementById('resultsTitle').textContent =
          `${catLabel} · ${titleRange}`;

        document.getElementById('resultsSection').style.display = 'block';
        renderResults();
        document.getElementById('resultsSection').scrollIntoView({ behavior: 'smooth', block: 'start' });

      } catch (err) {
        statusEl.textContent = 'Error fetching data. Try again.';
        statusEl.style.color = 'var(--error)';
        console.error(err);
      } finally {
        searchBtn.disabled = false;
        searchBtn.textContent = 'Search disasters';
      }
    }

    searchBtn.addEventListener('click', doSearch);

    clearBtn.addEventListener('click', () => {
      document.getElementById('startDate').value = toISODate(daysAgo(30));
      document.getElementById('endDate').value   = toISODate(today);
      document.getElementById('eventType').value = 'all';
      document.getElementById('minMag').value    = '4.5';
      document.getElementById('limitCount').value = '100';
      document.querySelectorAll('.preset-btn').forEach(b => b.classList.remove('active'));
      statusEl.textContent = '';
      document.getElementById('resultsSection').style.display = 'none';
      currentResults = [];
    });

    // Enter key to search
    document.addEventListener('keydown', e => {
      if (e.key === 'Enter' && !searchBtn.disabled) doSearch();
    });
  })();