Source code for seismoalert.visualizer

"""Visualization utilities for earthquake data."""

from __future__ import annotations

from pathlib import Path

import folium
import matplotlib
import matplotlib.pyplot as plt
import numpy as np

from seismoalert.models import EarthquakeCatalog

matplotlib.use("Agg")


def _magnitude_to_color(mag: float) -> str:
    """Map magnitude to a color string."""
    if mag >= 7.0:
        return "red"
    if mag >= 5.0:
        return "orange"
    if mag >= 3.0:
        return "yellow"
    return "green"


def _magnitude_to_radius(mag: float) -> float:
    """Map magnitude to a circle radius in pixels."""
    return max(3.0, mag ** 2)


[docs] def create_earthquake_map( catalog: EarthquakeCatalog, output_path: str | Path = "earthquakes.html", ) -> Path: """Create an interactive Folium map of earthquake locations. Each earthquake is represented as a circle marker with size proportional to magnitude and color indicating severity. Args: catalog: Earthquake catalog to visualize. output_path: Path for the output HTML file. Returns: Path to the saved HTML file. """ output_path = Path(output_path) # Center map on mean coordinates, or world center if empty if len(catalog) > 0: center_lat = np.mean([eq.latitude for eq in catalog]) center_lon = np.mean([eq.longitude for eq in catalog]) zoom = 3 else: center_lat, center_lon = 0.0, 0.0 zoom = 2 m = folium.Map(location=[center_lat, center_lon], zoom_start=zoom) for eq in catalog: popup_text = ( f"<b>M{eq.magnitude:.1f}</b> - {eq.place}<br>" f"Depth: {eq.depth:.1f} km<br>" f"Time: {eq.time.strftime('%Y-%m-%d %H:%M:%S UTC')}" ) folium.CircleMarker( location=[eq.latitude, eq.longitude], radius=_magnitude_to_radius(eq.magnitude), color=_magnitude_to_color(eq.magnitude), fill=True, fill_opacity=0.7, popup=folium.Popup(popup_text, max_width=300), ).add_to(m) m.save(str(output_path)) return output_path
[docs] def plot_magnitude_time( catalog: EarthquakeCatalog, output_path: str | Path = "magnitude_time.png", ) -> Path: """Plot earthquake magnitude vs. time. Args: catalog: Earthquake catalog to plot. output_path: Path for the output image file. Returns: Path to the saved image file. """ output_path = Path(output_path) sorted_cat = catalog.sort_by_time() times = [eq.time for eq in sorted_cat] mags = [eq.magnitude for eq in sorted_cat] fig, ax = plt.subplots(figsize=(12, 5)) ax.scatter(times, mags, s=10, alpha=0.6, c=mags, cmap="YlOrRd", edgecolors="none") ax.set_xlabel("Time") ax.set_ylabel("Magnitude") ax.set_title("Earthquake Magnitude vs. Time") ax.grid(True, alpha=0.3) fig.tight_layout() fig.savefig(str(output_path), dpi=150) plt.close(fig) return output_path
[docs] def plot_gutenberg_richter( catalog: EarthquakeCatalog, a_value: float, b_value: float, output_path: str | Path = "gutenberg_richter.png", mc: float | None = None, ) -> Path: """Plot the Gutenberg-Richter frequency-magnitude distribution. Shows observed cumulative event counts, the fitted G-R line, and optionally the magnitude of completeness (Mc). Args: catalog: Earthquake catalog to plot. a_value: G-R a-value (productivity). b_value: G-R b-value (slope). output_path: Path for the output image file. mc: Magnitude of completeness. Drawn as a vertical dashed line if provided. Returns: Path to the saved image file. """ output_path = Path(output_path) mags = sorted(catalog.magnitudes) # Cumulative counts (N >= M) unique_mags = sorted(set(mags)) cum_counts = [ sum(1 for m in mags if m >= mag) for mag in unique_mags ] # G-R fit line mag_range = np.linspace(min(mags), max(mags), 100) gr_line = 10 ** (a_value - b_value * mag_range) fig, ax = plt.subplots(figsize=(8, 6)) ax.semilogy( unique_mags, cum_counts, "ko", markersize=5, label="Observed" ) ax.semilogy( mag_range, gr_line, "r-", linewidth=2, label=f"G-R fit (a={a_value:.2f}, b={b_value:.2f})", ) if mc is not None: ax.axvline( x=mc, color="blue", linestyle="--", linewidth=1.5, label=f"Mc = {mc:.1f}", ) ax.set_xlabel("Magnitude") ax.set_ylabel("Cumulative Number (N ≥ M)") ax.set_title("Gutenberg-Richter Distribution") ax.legend() ax.grid(True, alpha=0.3) fig.tight_layout() fig.savefig(str(output_path), dpi=150) plt.close(fig) return output_path