Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .DS_Store
Binary file not shown.
4,948 changes: 4,948 additions & 0 deletions data/earthquake.json

Large diffs are not rendered by default.

1,182 changes: 1,182 additions & 0 deletions data/plate_boundaries.geojson

Large diffs are not rendered by default.

48,060 changes: 48,060 additions & 0 deletions data/worldcities.csv

Large diffs are not rendered by default.

105 changes: 105 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Global M6+ Earthquake Dashboard (1990–2023)</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@400;600;700&display=swap" rel="stylesheet">
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
>
<link rel="stylesheet" href="./styles.css">
</head>
<body>
<header class="app-header">
<div class="header-text">
<h1>Global M6+ Earthquakes</h1>
<p>Major earthquakes from 1990&ndash;2023 with proximity to seismic belts.</p>
</div>
</header>

<main class="app-main">
<section class="map-section">
<div id="map" class="map"></div>
<div id="map-legend" class="map-legend"></div>
</section>

<aside class="sidebar" aria-label="Earthquake filters and results">
<section class="panel-section">
<h2>Filters</h2>
<fieldset class="field">
<legend class="field-label">Magnitude</legend>
<label class="checkbox">
<input type="checkbox" name="magnitude" value="6-6.9" checked>
<span>6.0&ndash;6.9</span>
</label>
<label class="checkbox">
<input type="checkbox" name="magnitude" value="7-7.9" checked>
<span>7.0&ndash;7.9</span>
</label>
<label class="checkbox">
<input type="checkbox" name="magnitude" value="8+" checked>
<span>&ge; 8.0</span>
</label>
</fieldset>
<div class="field range-pair dual-range">
<label for="year-start" class="field-label">Year range</label>
<div class="range-track">
<input id="year-start" class="range-thumb range-thumb-start" type="range" min="1990" max="2023" value="1990" step="1" aria-label="Start year">
<input id="year-end" class="range-thumb range-thumb-end" type="range" min="1990" max="2023" value="2023" step="1" aria-label="End year">
</div>
<div class="range-values">
<span id="year-start-value">1990</span>
<span>&ndash;</span>
<span id="year-end-value">2023</span>
</div>
</div>
</section>

<section class="panel-section">
<h2>Find a Place</h2>
<label class="field" for="city-input">
<span class="field-label">Search city</span>
<input id="city-input" type="search" name="city" autocomplete="off" placeholder="e.g. Tokyo" aria-describedby="city-help">
</label>
<p id="city-help" class="field-help">Start typing to see matching cities.</p>
<div class="field search-actions">
<button id="use-location" type="button" class="button button-secondary">Use my location</button>
<button id="clear-search" type="button" class="button button-tertiary">Clear</button>
</div>
<div class="field">
<label for="radius-input" class="field-label">Search radius <span id="radius-value">100 km</span></label>
<input id="radius-input" type="range" min="50" max="300" step="10" value="100">
</div>
</section>

<section class="panel-section">
<h2>Risk Summary</h2>
<div id="risk-summary" class="summary-card" role="status" aria-live="polite">
<p>Select a city or use your location to see nearby earthquakes.</p>
</div>
</section>

</aside>
</main>

<footer class="app-footer">
<p>
Earthquakes: <a href="https://www.kaggle.com/datasets/alessandrolobello/the-ultimate-earthquake-dataset-from-1990-2023/data" target="_blank" rel="noopener noreferrer">Kaggle M6+ dataset (1990&ndash;2023)</a> ·
Seismic belts: <a href="https://earthquake.usgs.gov/learn/plate-boundaries.kmz" target="_blank" rel="noopener noreferrer">USGS plate boundaries</a> ·
Global cities: <a href="https://www.kaggle.com/datasets/juanmah/world-cities" target="_blank" rel="noopener noreferrer">Kaggle world cities</a> ·
Basemap: Mapbox · Prepared for CPLN6920.
</p>
</footer>

<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
></script>
<script src="https://cdn.jsdelivr.net/npm/@turf/turf@6/turf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/fuse.js@6.6.2/dist/fuse.min.js"></script>
<script type="module" src="./js/main.js"></script>
</body>
</html>
94 changes: 94 additions & 0 deletions js/data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import { parseCSV } from "./utils.js";

function normalizeTaiwanLabel(value) {
if (typeof value !== "string" || value.length === 0) {
return value;
}
if (!value.includes("Taiwan")) {
return value;
}
return value.replace(/Taiwan(?!, China)/g, "Taiwan, China");
}

const earthquakeUrl = new URL("../data/earthquake.json", import.meta.url);
const beltsUrl = new URL("../data/plate_boundaries.geojson", import.meta.url);
const citiesUrl = new URL("../data/worldcities.csv", import.meta.url);

function normalizeEarthquake(feature, index) {
const { properties, geometry } = feature;
const coords = geometry && geometry.coordinates ? geometry.coordinates : [0, 0];
const dateISO = properties.date || properties.time || null;
const dateObj = dateISO ? new Date(dateISO) : null;
const year = dateObj && !Number.isNaN(dateObj.getTime()) ? dateObj.getUTCFullYear() : null;
const id = properties.ID ?? properties.id ?? `eq-${index}`;
const place = normalizeTaiwanLabel(properties.place || "Unknown location");
const state = normalizeTaiwanLabel(properties.state || properties.country || "");
return {
type: "Feature",
geometry: {
type: "Point",
coordinates: coords,
},
properties: {
...properties,
id,
mag: Number(properties.magnitudo ?? properties.mag ?? properties.magnitude ?? null),
depth: Number(properties.depth ?? null),
hasTsunami: Number(properties.tsunami ?? 0) === 1,
significance: Number(properties.significance ?? properties.sig ?? 0),
place,
state,
dateISO,
year,
latitude: coords[1],
longitude: coords[0],
},
};
}

export async function loadEarthquakes() {
const response = await fetch(earthquakeUrl);
const data = await response.json();
const features = data.features.map((feature, index) => normalizeEarthquake(feature, index));
const byId = new Map(features.map((f) => [f.properties.id, f]));
return {
collection: { ...data, features },
features,
byId,
};
}

export async function loadBelts() {
const response = await fetch(beltsUrl);
const belts = await response.json();
return belts;
}

export async function loadCities() {
const response = await fetch(citiesUrl);
const text = await response.text();
const rows = parseCSV(text);
return rows
.map((row) => {
const lat = Number(row.lat ?? row.latitude ?? row.Latitude);
const lng = Number(row.lng ?? row.longitude ?? row.Longitude);
if (!Number.isFinite(lat) || !Number.isFinite(lng)) return null;
const population = Number(row.population ?? 0);
const city = normalizeTaiwanLabel(row.city_ascii || row.city || "");
const admin = normalizeTaiwanLabel(row.admin_name || "");
const country = normalizeTaiwanLabel(row.country || "");
const displayName = [city, admin && admin !== city ? admin : null, country]
.filter(Boolean)
.join(", ");
return {
city,
country,
admin,
lat,
lng,
population,
value: displayName,
};
})
.filter(Boolean);
}
112 changes: 112 additions & 0 deletions js/filters.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
export function createFilterState() {
return {
radius: 100,
magnitudes: new Set(["6-6.9", "7-7.9", "8+"]),
yearStart: 1990,
yearEnd: 2023,
};
}

export function bindFilterControls(state, onChange) {
const radiusInput = document.getElementById("radius-input");
const radiusValue = document.getElementById("radius-value");
const magnitudeInputs = document.querySelectorAll('input[name="magnitude"]');
const yearStartInput = document.getElementById("year-start");
const yearEndInput = document.getElementById("year-end");
const yearStartValue = document.getElementById("year-start-value");
const yearEndValue = document.getElementById("year-end-value");
const rangeTrack = document.querySelector(".range-track");

const notify = () => {
if (typeof onChange === "function") {
onChange({
...state,
magnitudes: new Set(state.magnitudes),
});
}
};

const updateRadius = (value) => {
state.radius = Number(value);
radiusValue.textContent = `${state.radius} km`;
};

const updateYears = () => {
state.yearStart = Number(yearStartInput.value);
state.yearEnd = Number(yearEndInput.value);
if (state.yearStart > state.yearEnd) {
if (document.activeElement === yearStartInput) {
state.yearEnd = state.yearStart;
yearEndInput.value = String(state.yearEnd);
} else {
state.yearStart = state.yearEnd;
yearStartInput.value = String(state.yearStart);
}
}
yearStartValue.textContent = state.yearStart;
yearEndValue.textContent = state.yearEnd;
updateRangeTrack();
};

const updateRangeTrack = () => {
if (!rangeTrack) return;
rangeTrack.style.background = "transparent";
};

if (radiusInput) {
updateRadius(radiusInput.value);
radiusInput.addEventListener("input", (evt) => {
updateRadius(evt.target.value);
});
radiusInput.addEventListener("change", () => notify());
}

if (magnitudeInputs.length) {
magnitudeInputs.forEach((input) => {
input.addEventListener("change", (evt) => {
const value = evt.target.value;
if (evt.target.checked) {
state.magnitudes.add(value);
} else {
state.magnitudes.delete(value);
}
notify();
});
});
}

if (yearStartInput && yearEndInput) {
updateYears();
yearStartInput.addEventListener("input", () => {
updateYears();
});
yearEndInput.addEventListener("input", () => {
updateYears();
});
yearStartInput.addEventListener("change", () => notify());
yearEndInput.addEventListener("change", () => notify());
}

// Emit initial state
notify();
updateRangeTrack();

return {
getState: () => ({
...state,
magnitudes: new Set(state.magnitudes),
}),
setRadius(value) {
radiusInput.value = value;
updateRadius(value);
notify();
},
setYearRange(start, end) {
yearStartInput.value = start;
yearEndInput.value = end;
updateYears();
notify();
updateRangeTrack();
},
};
}
Loading