import sys
sys.path.insert(0, "..")
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from shapely.geometry import Polygon
from src.datasets.gdacs import (
get_active_cyclones,
get_event_detail,
get_timeline,
get_impact_by_country,
load_ibtracs_lookup,
match_gdacs_to_ibtracs,
wind_radii_polygon,
)
# -- Plotting helpers (inline, not in src module) --
_INTENSITY_BREAKS = [
(17.5, "#5ebaff", "TD"),
(25.0, "#00faf4", "TS"),
(33.0, "#ffffcc", "Cat 1"),
(43.0, "#ffe775", "Cat 2"),
(50.0, "#ffc140", "Cat 3"),
(58.0, "#ff8f20", "Cat 4"),
(999, "#ff6060", "Cat 5"),
]
def _wind_color(wind_ms):
if np.isnan(wind_ms):
return "#cccccc"
for threshold, color, _ in _INTENSITY_BREAKS:
if wind_ms < threshold:
return color
return "#ff6060"
def plot_track_interactive(timeline, event_name="", show_radii=True, radii_interval=4):
df = timeline.dropna(subset=["latitude", "longitude"]).copy()
df["color"] = df["wind_speed"].apply(_wind_color)
df["wind_kmh"] = (df["wind_speed"] * 3.6).round(1)
df["is_actual"] = df["actual"].astype(str).str.lower() == "true"
fig = make_subplots(
rows=2, cols=1, row_heights=[0.7, 0.3],
specs=[[{"type": "map"}], [{"type": "xy"}]],
vertical_spacing=0.05,
)
# Track line
fig.add_trace(
go.Scattermap(
lat=df["latitude"], lon=df["longitude"],
mode="lines", line={"width": 2, "color": "gray"},
showlegend=False, hoverinfo="skip",
), row=1, col=1,
)
# Advisory points
for _, row in df.iterrows():
status = row.get("storm_status", "")
hover = (
f"<b>{row.get('name', '')}</b><br>"
f"Advisory {int(row['advisory_number'])}<br>"
f"{row['advisory_datetime']}<br>"
f"Wind: {row['wind_kmh']} km/h ({row['wind_speed']:.1f} m/s)<br>"
f"Status: {status}<br>"
f"Pop 39kt: {row['pop39']:,.0f}<br>"
f"Pop 74kt: {row['pop74']:,.0f}"
)
fig.add_trace(
go.Scattermap(
lat=[row["latitude"]], lon=[row["longitude"]],
mode="markers",
marker={"size": 10, "color": row["color"],
"opacity": 1.0 if row["is_actual"] else 0.5},
text=hover, hoverinfo="text", showlegend=False,
), row=1, col=1,
)
# Wind radii
if show_radii:
radii_config = {
"34kt": ("rgba(51,136,255,{a})", 0.10),
"50kt": ("rgba(255,136,0,{a})", 0.15),
"64kt": ("rgba(255,0,0,{a})", 0.20),
}
for idx, row in df.iterrows():
if idx % radii_interval != 0:
continue
for kt, (color, opacity) in radii_config.items():
ne = row.get(f"windrad_nm_{kt}_ne", 0) or 0
se = row.get(f"windrad_nm_{kt}_se", 0) or 0
sw = row.get(f"windrad_nm_{kt}_sw", 0) or 0
nw = row.get(f"windrad_nm_{kt}_nw", 0) or 0
poly = wind_radii_polygon(
row["latitude"], row["longitude"], ne, se, sw, nw
)
if poly is None:
continue
lons, lats = poly.exterior.xy
fig.add_trace(
go.Scattermap(
lat=list(lats), lon=list(lons),
mode="lines", fill="toself",
fillcolor=color.format(a=opacity),
line={"width": 0.5, "color": color.format(a=1.0)},
showlegend=False, hoverinfo="skip",
), row=1, col=1,
)
# Population subplot
fig.add_trace(
go.Scatter(
x=df["advisory_datetime"], y=df["pop39"],
fill="tozeroy", fillcolor="rgba(51,136,255,0.2)",
line={"color": "#3388ff", "width": 1.5}, name="Pop >= 39 kt",
), row=2, col=1,
)
fig.add_trace(
go.Scatter(
x=df["advisory_datetime"], y=df["pop74"],
fill="tozeroy", fillcolor="rgba(255,0,0,0.2)",
line={"color": "#ff0000", "width": 1.5}, name="Pop >= 74 kt",
), row=2, col=1,
)
fig.update_layout(
title=f"GDACS Track: {event_name}" if event_name else "GDACS Storm Track",
map={"style": "carto-positron",
"center": {"lat": df["latitude"].mean(), "lon": df["longitude"].mean()},
"zoom": 3},
height=900, margin={"l": 40, "r": 20, "t": 60, "b": 40},
legend={"yanchor": "bottom", "y": 0.02, "x": 0.01},
)
fig.update_yaxes(title_text="Population Exposed", row=2, col=1)
fig.update_xaxes(title_text="Advisory Time", row=2, col=1)
return fig