import React, { useState, useRef, useEffect } from 'react'; import Plotly from 'plotly.js-dist'; const EarthquakeApp = () => { const [startDate, setStartDate] = useState('2025-08-20'); const [endDate, setEndDate] = useState('2025-08-21'); const [earthquakeData, setEarthquakeData] = useState([]); const [loading, setLoading] = useState(false); const [error, setError] = useState(''); const [isPlaying, setIsPlaying] = useState(false); const [currentIndex, setCurrentIndex] = useState(0); const [showPlates, setShowPlates] = useState(true); const [animationSpeed, setAnimationSpeed] = useState(1); // 1x, 2x, 4x, 8x const plotRef = useRef(null); const animationRef = useRef(null); const fetchData = async () => { setLoading(true); setError(''); try { const response = await fetch('/api/proxy', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ start_date: startDate, end_date: endDate }) }); if (!response.ok) { const errorData = await response.json(); throw new Error(errorData.detail || `HTTP error! status: ${response.status}`); } const data = await response.json(); const sortedData = (data.data || []).sort((a, b) => new Date(a.full_time) - new Date(b.full_time) ); setEarthquakeData(sortedData); setCurrentIndex(0); setIsPlaying(false); if (sortedData.length === 0) { setError('No earthquake data found for the selected date range'); } } catch (err) { setError(`Failed to fetch data: ${err.message}`); } finally { setLoading(false); } }; const initMap = async () => { if (!plotRef.current) return; const traces = [ { type: 'scattergeo', mode: 'markers', lat: [], lon: [], text: [], marker: { size: [], color: 'red', opacity: 0.7, line: { color: 'darkred', width: 1 } }, name: 'Earthquakes' }, { type: 'scattergeo', mode: 'lines', lat: [], lon: [], line: { color: 'blue', width: 2, dash: 'dash' }, name: 'Tectonic Plates', visible: showPlates } ]; const layout = { title: { text: 'Earthquake Animation - World Map', font: { size: 18, color: '#333' }, x: 0.5 }, geo: { projection: { type: 'natural earth' }, showland: true, landcolor: 'lightgray', showocean: true, oceancolor: 'lightblue', coastlinecolor: 'gray', showframe: false }, height: 600, margin: { l: 40, r: 40, t: 60, b: 40 }, showlegend: true, legend: { x: 0, y: 1.2, bgcolor: 'rgba(255,255,255,0.9)', } }; await Plotly.newPlot(plotRef.current, traces, layout, { responsive: true, displayModeBar: true, modeBarButtonsToRemove: ['pan2d', 'select2d', 'lasso2d', 'autoScale2d'] }); // { responsive: true } it ensures that the map resizes correctly if (showPlates) await loadPlates(); }; const loadPlates = async () => { try { const response = await fetch('https://raw.githubusercontent.com/fraxen/tectonicplates/master/GeoJSON/PB2002_boundaries.json'); const plateData = await response.json(); const [lats, lons] = [[], []]; plateData.features.forEach(feature => { const processCoords = (coords) => { // coords is an array of coordinates [[lon, lat], [lon, lat], ...] coords.forEach(coord => { lons.push(coord[0]); lats.push(coord[1]); }); lons.push(null); lats.push(null); // After finishing one line (or segment), we push null to separate line segments in Plotly. // In Plotly, arrays of coordinates with null values indicate a break in the line. }; if (feature.geometry.type === 'LineString') { processCoords(feature.geometry.coordinates); } else if (feature.geometry.type === 'MultiLineString') { feature.geometry.coordinates.forEach(processCoords); } }); Plotly.restyle(plotRef.current, { lat: [lats], lon: [lons] }, [1]); //[1] tells Plotly to update the second trace (tectonic plates) } catch (error) { console.error('Failed to load plates:', error); } }; const updateMap = (index) => { if (!plotRef.current || !earthquakeData.length) return; const current = earthquakeData.slice(0, index + 1); const update = { lat: [current.map(eq => eq.latitude)], // Loops over an array and returns a new array with the results of the callback function (forEach does not return a new element) lon: [current.map(eq => eq.longitude)], text: [current.map(eq => `Mag: ${eq.mag}
Location: ${eq.place}
Time: ${eq.full_time}
Depth: ${eq.depth} km`)], 'marker.size': [current.map(eq => Math.max(4, eq.mag * 3))] }; Plotly.restyle(plotRef.current, update, [0]); }; const animate = () => { if (!earthquakeData.length || isPlaying) return; setIsPlaying(true); setCurrentIndex(0); }; const stop = () => { setIsPlaying(false); if (animationRef.current) { clearTimeout(animationRef.current); animationRef.current = null; } // Update map to show only earthquakes up to current index updateMap(currentIndex); }; const reset = () => { stop(); setCurrentIndex(0); updateMap(-1); }; const togglePlates = () => { setShowPlates(!showPlates); if (plotRef.current) { Plotly.restyle(plotRef.current, { visible: !showPlates }, [1]); // visible: it shows or hide the tectonic plates if (!showPlates && (!plotRef.current.data[1].lat || plotRef.current.data[1].lat.length === 0)) { loadPlates(); } } }; useEffect(() => { initMap(); return () => animationRef.current && clearTimeout(animationRef.current); }, []); // Initialize map and clear animation on unmount useEffect(() => { // shows the first earthquake on the map if (earthquakeData.length > 0 && !isPlaying) updateMap(currentIndex); }, [earthquakeData, currentIndex]); useEffect(() => { if (isPlaying && currentIndex < earthquakeData.length) { // Group earthquakes by hour const currentTime = new Date(earthquakeData[currentIndex]?.full_time); const currentHour = new Date(currentTime.getFullYear(), currentTime.getMonth(), currentTime.getDate(), currentTime.getHours()); // Find all earthquakes in the same hour let nextIndex = currentIndex; while (nextIndex < earthquakeData.length) { const eqTime = new Date(earthquakeData[nextIndex].full_time); const eqHour = new Date(eqTime.getFullYear(), eqTime.getMonth(), eqTime.getDate(), eqTime.getHours()); if (eqHour.getTime() === currentHour.getTime()) { nextIndex++; } else { break; } } updateMap(nextIndex - 1); if (nextIndex < earthquakeData.length) { animationRef.current = setTimeout(() => { setCurrentIndex(nextIndex); }, 500 / animationSpeed); } else { setIsPlaying(false); } } }, [currentIndex, isPlaying, earthquakeData, animationSpeed]); const styles = { container: { minHeight: '100vh', backgroundColor: '#f8f9fa', padding: '20px', font: 'Avenir sans-serif', // fontFamily: 'Inter, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif' }, card: { backgroundColor: 'white', padding: '24px', borderRadius: '12px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', marginBottom: '24px', maxWidth: '1000px', margin: '0 auto 24px auto', border: '1px solid #e9ecef' }, plotCard: { backgroundColor: 'white', padding: '24px', borderRadius: '12px', boxShadow: '0 4px 6px rgba(0,0,0,0.1)', margin: '0 auto', maxWidth: '1000px', border: '1px solid #e9ecef' }, input: { padding: '10px 12px', border: '2px solid #e9ecef', borderRadius: '6px', marginRight: '12px', fontSize: '14px', transition: 'border-color 0.2s', outline: 'none' }, btn: { padding: '10px 18px', border: 'none', borderRadius: '6px', cursor: 'pointer', marginRight: '12px', color: 'white', fontSize: '14px', fontWeight: '500', transition: 'all 0.2s', boxShadow: '0 2px 4px rgba(0,0,0,0.1)' }, error: { marginTop: '16px', padding: '12px 16px', backgroundColor: '#f8d7da', color: '#721c24', border: '1px solid #f5c6cb', borderRadius: '6px', fontSize: '14px' }, timeDisplay: { backgroundColor: '#f8f9fa', border: '2px solid #dee2e6', padding: '12px 20px', borderRadius: '8px', fontSize: '16px', fontWeight: '500', color: '#495057', boxShadow: '0 2px 4px rgba(0,0,0,0.05)' } }; return (

Dynamic Earthquake Dashboard

Date Range

setStartDate(e.target.value)} style={styles.input} /> setEndDate(e.target.value)} style={styles.input} />
{error &&
{error}
}
{earthquakeData.length > 0 && (

Controls

Showing: {Math.min(currentIndex + 1, earthquakeData.length)} of {earthquakeData.length} earthquakes found
)}
Current Earthquake Time:
{earthquakeData[currentIndex]?.full_time || 'No data available'}
{/* If you want React to "store" a DOM element inside a ref, you must attach it with the ref attribute. */}
); }; export default EarthquakeApp;