Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -107,8 +107,9 @@ class TyphoonAnalyzer:
|
|
| 107 |
raise
|
| 108 |
|
| 109 |
print("All required data files are ready")
|
| 110 |
-
|
| 111 |
def load_initial_data(self):
|
|
|
|
| 112 |
print("Loading initial data...")
|
| 113 |
self.update_oni_data()
|
| 114 |
self.oni_df = self.fetch_oni_data_from_csv()
|
|
@@ -120,261 +121,102 @@ class TyphoonAnalyzer:
|
|
| 120 |
self.merged_data = self.merge_data()
|
| 121 |
print("Initial data loading complete")
|
| 122 |
|
| 123 |
-
def fetch_oni_data_from_csv(self):
|
| 124 |
-
"""Load ONI data from CSV"""
|
| 125 |
-
df = pd.read_csv(ONI_DATA_PATH)
|
| 126 |
-
df = df.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
|
| 127 |
-
|
| 128 |
-
# Convert month numbers to month names
|
| 129 |
-
month_map = {
|
| 130 |
-
'01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr',
|
| 131 |
-
'05': 'May', '06': 'Jun', '07': 'Jul', '08': 'Aug',
|
| 132 |
-
'09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec'
|
| 133 |
-
}
|
| 134 |
-
df['Month'] = df['Month'].map(month_map)
|
| 135 |
-
|
| 136 |
-
# Now create the date
|
| 137 |
-
df['Date'] = pd.to_datetime(df['Year'].astype(str) + df['Month'], format='%Y%b')
|
| 138 |
-
return df.set_index('Date')
|
| 139 |
-
|
| 140 |
-
def should_update_oni(self):
|
| 141 |
-
today = datetime.now()
|
| 142 |
-
return (today.day == 1 or today.day == 15 or
|
| 143 |
-
today.day == (today.replace(day=1, month=today.month%12+1) - timedelta(days=1)).day)
|
| 144 |
def convert_typhoondata(self, input_file, output_file):
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
for row in reader:
|
| 156 |
-
if not row: # Skip blank lines
|
| 157 |
-
continue
|
| 158 |
-
|
| 159 |
-
sid = row[0]
|
| 160 |
-
iso_time = row[6]
|
| 161 |
-
sid_data[sid].append((row, iso_time))
|
| 162 |
-
|
| 163 |
-
with open(output_file, 'w', newline='') as outfile:
|
| 164 |
-
fieldnames = ['SID', 'ISO_TIME', 'LAT', 'LON', 'SEASON', 'NAME',
|
| 165 |
-
'WMO_WIND', 'WMO_PRES', 'USA_WIND', 'USA_PRES',
|
| 166 |
-
'START_DATE', 'END_DATE']
|
| 167 |
-
writer = csv.DictWriter(outfile, fieldnames=fieldnames)
|
| 168 |
-
writer.writeheader()
|
| 169 |
-
|
| 170 |
-
for sid, data in sid_data.items():
|
| 171 |
-
start_date = min(data, key=lambda x: x[1])[1]
|
| 172 |
-
end_date = max(data, key=lambda x: x[1])[1]
|
| 173 |
-
|
| 174 |
-
for row, iso_time in data:
|
| 175 |
-
writer.writerow({
|
| 176 |
-
'SID': row[0],
|
| 177 |
-
'ISO_TIME': iso_time,
|
| 178 |
-
'LAT': row[8],
|
| 179 |
-
'LON': row[9],
|
| 180 |
-
'SEASON': row[1],
|
| 181 |
-
'NAME': row[5],
|
| 182 |
-
'WMO_WIND': row[10].strip() or ' ',
|
| 183 |
-
'WMO_PRES': row[11].strip() or ' ',
|
| 184 |
-
'USA_WIND': row[23].strip() or ' ',
|
| 185 |
-
'USA_PRES': row[24].strip() or ' ',
|
| 186 |
-
'START_DATE': start_date,
|
| 187 |
-
'END_DATE': end_date
|
| 188 |
-
})
|
| 189 |
-
def update_oni_data(self):
|
| 190 |
-
if not self.should_update_oni():
|
| 191 |
-
return
|
| 192 |
-
|
| 193 |
-
url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
|
| 194 |
-
temp_file = os.path.join(DATA_PATH, "temp_oni.ascii.txt")
|
| 195 |
-
|
| 196 |
-
try:
|
| 197 |
-
response = requests.get(url)
|
| 198 |
-
response.raise_for_status()
|
| 199 |
-
with open(temp_file, 'wb') as f:
|
| 200 |
-
f.write(response.content)
|
| 201 |
-
self.convert_oni_ascii_to_csv(temp_file, ONI_DATA_PATH)
|
| 202 |
-
self.last_oni_update = date.today()
|
| 203 |
-
except Exception as e:
|
| 204 |
-
print(f"Error updating ONI data: {e}")
|
| 205 |
-
finally:
|
| 206 |
-
if os.path.exists(temp_file):
|
| 207 |
-
os.remove(temp_file)
|
| 208 |
-
def create_wind_analysis(self, data):
|
| 209 |
-
"""Create wind speed analysis plot"""
|
| 210 |
-
fig = px.scatter(data,
|
| 211 |
-
x='ONI',
|
| 212 |
-
y='USA_WIND',
|
| 213 |
-
color='Category',
|
| 214 |
-
color_discrete_map=COLOR_MAP,
|
| 215 |
-
title='Wind Speed vs ONI Index',
|
| 216 |
-
labels={
|
| 217 |
-
'ONI': 'Oceanic Niño Index',
|
| 218 |
-
'USA_WIND': 'Maximum Wind Speed (kt)'
|
| 219 |
-
},
|
| 220 |
-
hover_data=['NAME', 'ISO_TIME', 'Category']
|
| 221 |
-
)
|
| 222 |
-
|
| 223 |
-
# Add regression line
|
| 224 |
-
x = data['ONI']
|
| 225 |
-
y = data['USA_WIND']
|
| 226 |
-
slope, intercept = np.polyfit(x, y, 1)
|
| 227 |
-
fig.add_trace(
|
| 228 |
-
go.Scatter(
|
| 229 |
-
x=x,
|
| 230 |
-
y=slope * x + intercept,
|
| 231 |
-
mode='lines',
|
| 232 |
-
name=f'Regression (slope={slope:.2f})',
|
| 233 |
-
line=dict(color='black', dash='dash')
|
| 234 |
-
)
|
| 235 |
-
)
|
| 236 |
-
|
| 237 |
-
return fig
|
| 238 |
-
def create_typhoon_animation(self, year, typhoon_id):
|
| 239 |
-
"""Create animated visualization of typhoon path"""
|
| 240 |
-
# Create default empty figure
|
| 241 |
-
empty_fig = go.Figure()
|
| 242 |
-
empty_fig.update_layout(
|
| 243 |
-
title="No Data Available",
|
| 244 |
-
showlegend=False,
|
| 245 |
-
geo=dict(
|
| 246 |
-
projection_type='mercator',
|
| 247 |
-
showland=True,
|
| 248 |
-
showcoastlines=True,
|
| 249 |
-
landcolor='rgb(243, 243, 243)',
|
| 250 |
-
countrycolor='rgb(204, 204, 204)',
|
| 251 |
-
coastlinecolor='rgb(214, 214, 214)',
|
| 252 |
-
showocean=True,
|
| 253 |
-
oceancolor='rgb(230, 250, 255)',
|
| 254 |
-
lataxis=dict(range=[0, 50]),
|
| 255 |
-
lonaxis=dict(range=[100, 180]),
|
| 256 |
-
center=dict(lat=20, lon=140)
|
| 257 |
-
)
|
| 258 |
-
)
|
| 259 |
-
|
| 260 |
-
# Input validation
|
| 261 |
-
if not typhoon_id:
|
| 262 |
-
return empty_fig, "Please select a typhoon"
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 268 |
|
| 269 |
-
|
| 270 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 271 |
|
| 272 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 273 |
|
| 274 |
-
#
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
showlegend=True,
|
| 278 |
-
geo=dict(
|
| 279 |
-
projection_type='mercator',
|
| 280 |
-
showland=True,
|
| 281 |
-
showcoastlines=True,
|
| 282 |
-
landcolor='rgb(243, 243, 243)',
|
| 283 |
-
countrycolor='rgb(204, 204, 204)',
|
| 284 |
-
coastlinecolor='rgb(214, 214, 214)',
|
| 285 |
-
showocean=True,
|
| 286 |
-
oceancolor='rgb(230, 250, 255)',
|
| 287 |
-
lataxis=dict(range=[0, 50]),
|
| 288 |
-
lonaxis=dict(range=[100, 180]),
|
| 289 |
-
center=dict(lat=20, lon=140)
|
| 290 |
-
)
|
| 291 |
-
)
|
| 292 |
-
|
| 293 |
-
# Create animation frames
|
| 294 |
-
frames = []
|
| 295 |
-
for i in range(len(storm_data)):
|
| 296 |
-
frame = go.Frame(
|
| 297 |
-
data=[
|
| 298 |
-
go.Scattergeo(
|
| 299 |
-
lon=storm_data['LON'].iloc[:i+1],
|
| 300 |
-
lat=storm_data['LAT'].iloc[:i+1],
|
| 301 |
-
mode='lines+markers',
|
| 302 |
-
line=dict(width=2, color='red'),
|
| 303 |
-
marker=dict(size=8, color='red'),
|
| 304 |
-
name='Path',
|
| 305 |
-
hovertemplate=(
|
| 306 |
-
f"Time: {storm_data['ISO_TIME'].iloc[i]:%Y-%m-%d %H:%M}<br>" +
|
| 307 |
-
f"Wind: {storm_data['USA_WIND'].iloc[i]:.1f} kt<br>" +
|
| 308 |
-
f"Pressure: {storm_data['WMO_PRES'].iloc[i]:.1f} hPa<br>" +
|
| 309 |
-
f"Lat: {storm_data['LAT'].iloc[i]:.2f}°N<br>" +
|
| 310 |
-
f"Lon: {storm_data['LON'].iloc[i]:.2f}°E"
|
| 311 |
-
)
|
| 312 |
-
)
|
| 313 |
-
],
|
| 314 |
-
name=f'frame{i}'
|
| 315 |
-
)
|
| 316 |
-
frames.append(frame)
|
| 317 |
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
}
|
| 337 |
-
],
|
| 338 |
-
'type': 'buttons',
|
| 339 |
-
'showactive': False,
|
| 340 |
-
'x': 0.1,
|
| 341 |
-
'y': 0,
|
| 342 |
-
'xanchor': 'right',
|
| 343 |
-
'yanchor': 'top'
|
| 344 |
-
}]
|
| 345 |
-
)
|
| 346 |
-
|
| 347 |
-
# Add initial data
|
| 348 |
-
fig.add_trace(
|
| 349 |
-
go.Scattergeo(
|
| 350 |
-
lon=[storm_data['LON'].iloc[0]],
|
| 351 |
-
lat=[storm_data['LAT'].iloc[0]],
|
| 352 |
-
mode='markers',
|
| 353 |
-
marker=dict(size=8, color='red'),
|
| 354 |
-
name='Start Point',
|
| 355 |
-
showlegend=True
|
| 356 |
-
)
|
| 357 |
-
)
|
| 358 |
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
- Maximum Wind Speed: {storm_data['USA_WIND'].max():.1f} kt
|
| 365 |
-
- Minimum Pressure: {storm_data['WMO_PRES'].min():.1f} hPa
|
| 366 |
-
- Duration: {(storm_data['ISO_TIME'].iloc[-1] - storm_data['ISO_TIME'].iloc[0]).total_seconds() / 3600:.1f} hours
|
| 367 |
-
"""
|
| 368 |
|
| 369 |
-
return fig, info_text
|
| 370 |
-
|
| 371 |
def convert_oni_ascii_to_csv(self, input_file, output_file):
|
|
|
|
| 372 |
data = defaultdict(lambda: [''] * 12)
|
| 373 |
season_to_month = {
|
| 374 |
'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
|
| 375 |
'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11
|
| 376 |
}
|
| 377 |
-
|
| 378 |
with open(input_file, 'r') as f:
|
| 379 |
next(f) # Skip header
|
| 380 |
for line in f:
|
|
@@ -394,6 +236,7 @@ class TyphoonAnalyzer:
|
|
| 394 |
writer.writerow([year] + data[year])
|
| 395 |
|
| 396 |
def load_ibtracs_data(self):
|
|
|
|
| 397 |
if os.path.exists(CACHE_FILE):
|
| 398 |
cache_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))
|
| 399 |
if datetime.now() - cache_time < timedelta(days=CACHE_EXPIRY_DAYS):
|
|
@@ -402,24 +245,25 @@ class TyphoonAnalyzer:
|
|
| 402 |
|
| 403 |
if os.path.exists(LOCAL_iBtrace_PATH):
|
| 404 |
ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs',
|
| 405 |
-
|
| 406 |
else:
|
| 407 |
response = requests.get(iBtrace_uri)
|
| 408 |
response.raise_for_status()
|
| 409 |
with open(LOCAL_iBtrace_PATH, 'w') as f:
|
| 410 |
f.write(response.text)
|
| 411 |
ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs',
|
| 412 |
-
|
| 413 |
|
| 414 |
with open(CACHE_FILE, 'wb') as f:
|
| 415 |
pickle.dump(ibtracs, f)
|
| 416 |
return ibtracs
|
| 417 |
|
| 418 |
def update_typhoon_data(self):
|
|
|
|
| 419 |
try:
|
| 420 |
response = requests.head(iBtrace_uri)
|
| 421 |
remote_modified = datetime.strptime(response.headers['Last-Modified'],
|
| 422 |
-
|
| 423 |
local_modified = (datetime.fromtimestamp(os.path.getmtime(LOCAL_iBtrace_PATH))
|
| 424 |
if os.path.exists(LOCAL_iBtrace_PATH) else datetime.min)
|
| 425 |
|
|
@@ -428,10 +272,12 @@ class TyphoonAnalyzer:
|
|
| 428 |
response.raise_for_status()
|
| 429 |
with open(LOCAL_iBtrace_PATH, 'w') as f:
|
| 430 |
f.write(response.text)
|
|
|
|
| 431 |
except Exception as e:
|
| 432 |
print(f"Error updating typhoon data: {e}")
|
| 433 |
|
| 434 |
def load_data(self):
|
|
|
|
| 435 |
oni_data = pd.read_csv(ONI_DATA_PATH)
|
| 436 |
typhoon_data = pd.read_csv(TYPHOON_DATA_PATH, low_memory=False)
|
| 437 |
typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
|
|
@@ -439,7 +285,7 @@ class TyphoonAnalyzer:
|
|
| 439 |
|
| 440 |
def process_oni_data(self, oni_data):
|
| 441 |
"""Process ONI data"""
|
| 442 |
-
oni_long =
|
| 443 |
|
| 444 |
# Create a mapping for month numbers
|
| 445 |
month_map = {
|
|
@@ -454,6 +300,7 @@ class TyphoonAnalyzer:
|
|
| 454 |
return oni_long
|
| 455 |
|
| 456 |
def process_typhoon_data(self, typhoon_data):
|
|
|
|
| 457 |
typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
|
| 458 |
typhoon_data['WMO_PRES'] = pd.to_numeric(typhoon_data['WMO_PRES'], errors='coerce')
|
| 459 |
typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
|
|
@@ -473,9 +320,13 @@ class TyphoonAnalyzer:
|
|
| 473 |
return typhoon_max
|
| 474 |
|
| 475 |
def merge_data(self):
|
|
|
|
| 476 |
return pd.merge(self.typhoon_max, self.oni_long, on=['Year', 'Month'])
|
| 477 |
|
| 478 |
def categorize_typhoon(self, wind_speed):
|
|
|
|
|
|
|
|
|
|
| 479 |
if wind_speed >= 137:
|
| 480 |
return 'C5 Super Typhoon'
|
| 481 |
elif wind_speed >= 113:
|
|
@@ -492,6 +343,7 @@ class TyphoonAnalyzer:
|
|
| 492 |
return 'Tropical Depression'
|
| 493 |
|
| 494 |
def analyze_typhoon(self, start_year, start_month, end_year, end_month, enso_value='all'):
|
|
|
|
| 495 |
start_date = datetime(start_year, start_month, 1)
|
| 496 |
end_date = datetime(end_year, end_month, 28)
|
| 497 |
|
|
@@ -511,7 +363,6 @@ class TyphoonAnalyzer:
|
|
| 511 |
'tracks': self.create_tracks_plot(filtered_data),
|
| 512 |
'wind': self.create_wind_analysis(filtered_data),
|
| 513 |
'pressure': self.create_pressure_analysis(filtered_data),
|
| 514 |
-
'clusters': self.create_cluster_analysis(filtered_data, 5),
|
| 515 |
'stats': self.generate_statistics(filtered_data)
|
| 516 |
}
|
| 517 |
|
|
@@ -586,6 +437,96 @@ class TyphoonAnalyzer:
|
|
| 586 |
|
| 587 |
return fig
|
| 588 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 589 |
def analyze_clusters(self, year, n_clusters):
|
| 590 |
"""Analyze typhoon clusters for a specific year"""
|
| 591 |
year_data = self.typhoon_data[self.typhoon_data['SEASON'] == year]
|
|
@@ -669,369 +610,321 @@ class TyphoonAnalyzer:
|
|
| 669 |
stats_text += f"- Cluster {i+1}: {cluster_counts[i]} typhoons\n"
|
| 670 |
|
| 671 |
return fig, stats_text
|
|
|
|
| 672 |
def get_typhoons_for_year(self, year):
|
| 673 |
"""Get list of typhoons for a specific year"""
|
| 674 |
-
|
| 675 |
-
|
| 676 |
-
|
| 677 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
|
| 679 |
-
def create_typhoon_animation(self, year,
|
| 680 |
"""Create animated visualization of typhoon path"""
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 686 |
geo=dict(
|
| 687 |
-
projection_type='
|
| 688 |
showland=True,
|
| 689 |
-
showcoastlines=True,
|
| 690 |
landcolor='rgb(243, 243, 243)',
|
| 691 |
countrycolor='rgb(204, 204, 204)',
|
| 692 |
-
coastlinecolor='rgb(
|
| 693 |
showocean=True,
|
| 694 |
oceancolor='rgb(230, 250, 255)',
|
| 695 |
lataxis=dict(range=[0, 50]),
|
| 696 |
lonaxis=dict(range=[100, 180]),
|
| 697 |
-
center=dict(lat=20, lon=140)
|
| 698 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 699 |
)
|
| 700 |
|
| 701 |
-
#
|
| 702 |
-
|
| 703 |
-
|
|
|
|
| 704 |
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 710 |
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
storm_name = storm_data['NAME'].values[0] if len(storm_data) > 0 else "Unknown"
|
| 715 |
-
|
| 716 |
-
fig = go.Figure()
|
| 717 |
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
mode='lines+markers',
|
| 746 |
-
line=dict(width=2, color='red'),
|
| 747 |
-
marker=dict(size=8, color='red'),
|
| 748 |
-
name='Path',
|
| 749 |
-
hovertemplate=(
|
| 750 |
-
f"Time: {pd.to_datetime(storm_data['ISO_TIME'].values[i]).strftime('%Y-%m-%d %H:%M')}<br>" +
|
| 751 |
-
f"Wind: {storm_data['USA_WIND'].values[i]:.1f} kt<br>" +
|
| 752 |
-
f"Pressure: {storm_data['WMO_PRES'].values[i]:.1f} hPa<br>" +
|
| 753 |
-
f"Lat: {storm_data['LAT'].values[i]:.2f}°N<br>" +
|
| 754 |
-
f"Lon: {storm_data['LON'].values[i]:.2f}°E"
|
| 755 |
-
)
|
| 756 |
-
)
|
| 757 |
-
],
|
| 758 |
-
name=f'frame{i}'
|
| 759 |
-
)
|
| 760 |
-
frames.append(frame)
|
| 761 |
-
|
| 762 |
-
fig.frames = frames
|
| 763 |
-
|
| 764 |
-
# Add animation controls
|
| 765 |
-
fig.update_layout(
|
| 766 |
-
updatemenus=[{
|
| 767 |
-
'buttons': [
|
| 768 |
-
{
|
| 769 |
-
'args': [None, {'frame': {'duration': 100, 'redraw': True},
|
| 770 |
-
'fromcurrent': True}],
|
| 771 |
-
'label': 'Play',
|
| 772 |
-
'method': 'animate'
|
| 773 |
-
},
|
| 774 |
-
{
|
| 775 |
-
'args': [[None], {'frame': {'duration': 0, 'redraw': True},
|
| 776 |
-
'mode': 'immediate',
|
| 777 |
-
'transition': {'duration': 0}}],
|
| 778 |
-
'label': 'Pause',
|
| 779 |
-
'method': 'animate'
|
| 780 |
-
}
|
| 781 |
-
],
|
| 782 |
-
'type': 'buttons',
|
| 783 |
-
'showactive': False,
|
| 784 |
-
'x': 0.1,
|
| 785 |
-
'y': 0,
|
| 786 |
-
'xanchor': 'right',
|
| 787 |
-
'yanchor': 'top'
|
| 788 |
-
}]
|
| 789 |
-
)
|
| 790 |
-
|
| 791 |
-
# Add initial data
|
| 792 |
-
fig.add_trace(
|
| 793 |
-
go.Scattergeo(
|
| 794 |
-
lon=[storm_data['LON'].values[0]],
|
| 795 |
-
lat=[storm_data['LAT'].values[0]],
|
| 796 |
-
mode='markers',
|
| 797 |
-
marker=dict(size=8, color='red'),
|
| 798 |
-
name='Start Point',
|
| 799 |
-
showlegend=True
|
| 800 |
-
)
|
| 801 |
)
|
|
|
|
| 802 |
|
| 803 |
-
|
| 804 |
-
end_time = pd.to_datetime(storm_data['ISO_TIME'].values[-1])
|
| 805 |
-
duration = (end_time - start_time).total_seconds() / 3600
|
| 806 |
-
|
| 807 |
-
info_text = f"""
|
| 808 |
-
### Typhoon Information
|
| 809 |
-
- Name: {storm_name}
|
| 810 |
-
- Start Date: {start_time.strftime('%Y-%m-%d %H:%M')}
|
| 811 |
-
- End Date: {end_time.strftime('%Y-%m-%d %H:%M')}
|
| 812 |
-
- Maximum Wind Speed: {storm_data['USA_WIND'].max():.1f} kt
|
| 813 |
-
- Minimum Pressure: {storm_data['WMO_PRES'].min():.1f} hPa
|
| 814 |
-
- Duration: {duration:.1f} hours
|
| 815 |
-
"""
|
| 816 |
-
|
| 817 |
-
return fig, info_text
|
| 818 |
-
|
| 819 |
-
except Exception as e:
|
| 820 |
-
print(f"Error in create_typhoon_animation: {str(e)}")
|
| 821 |
-
return empty_fig, f"Error processing typhoon data: {str(e)}"
|
| 822 |
-
|
| 823 |
-
return fig, info_text
|
| 824 |
-
def create_pressure_analysis(self, data):
|
| 825 |
-
fig = px.scatter(data,
|
| 826 |
-
x='ONI',
|
| 827 |
-
y='WMO_PRES',
|
| 828 |
-
color='Category',
|
| 829 |
-
color_discrete_map=COLOR_MAP,
|
| 830 |
-
title='Pressure vs ONI Index',
|
| 831 |
-
labels={
|
| 832 |
-
'ONI': 'Oceanic Niño Index',
|
| 833 |
-
'WMO_PRES': 'Minimum Pressure (hPa)'
|
| 834 |
-
},
|
| 835 |
-
hover_data=['NAME', 'ISO_TIME']
|
| 836 |
-
)
|
| 837 |
|
| 838 |
-
# Add
|
| 839 |
-
x = data['ONI']
|
| 840 |
-
y = data['WMO_PRES']
|
| 841 |
-
slope, intercept = np.polyfit(x, y, 1)
|
| 842 |
fig.add_trace(
|
| 843 |
-
go.
|
| 844 |
-
|
| 845 |
-
|
| 846 |
mode='lines',
|
| 847 |
-
|
| 848 |
-
|
|
|
|
| 849 |
)
|
| 850 |
)
|
| 851 |
-
|
| 852 |
-
return fig
|
| 853 |
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
if not routes:
|
| 867 |
-
return go.Figure()
|
| 868 |
-
|
| 869 |
-
# Perform clustering
|
| 870 |
-
routes_array = np.array(routes)
|
| 871 |
-
routes_reshaped = routes_array.reshape(routes_array.shape[0], -1)
|
| 872 |
-
kmeans = KMeans(n_clusters=n_clusters, random_state=42)
|
| 873 |
-
clusters = kmeans.fit_predict(routes_reshaped)
|
| 874 |
|
| 875 |
-
#
|
| 876 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 877 |
|
| 878 |
-
|
| 879 |
-
for route, cluster_id in zip(routes, clusters):
|
| 880 |
-
fig.add_trace(go.Scattergeo(
|
| 881 |
-
lon=route[:, 0],
|
| 882 |
-
lat=route[:, 1],
|
| 883 |
-
mode='lines',
|
| 884 |
-
line=dict(width=1, color=f'hsl({cluster_id * 360/n_clusters}, 50%, 50%)'),
|
| 885 |
-
showlegend=False
|
| 886 |
-
))
|
| 887 |
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
| 898 |
|
| 899 |
-
fig
|
| 900 |
-
title='Typhoon Route Clusters',
|
| 901 |
-
showlegend=True,
|
| 902 |
-
geo=dict(
|
| 903 |
-
projection_type='mercator',
|
| 904 |
-
showland=True,
|
| 905 |
-
showcoastlines=True,
|
| 906 |
-
landcolor='rgb(243, 243, 243)',
|
| 907 |
-
countrycolor='rgb(204, 204, 204)',
|
| 908 |
-
coastlinecolor='rgb(214, 214, 214)',
|
| 909 |
-
lataxis=dict(range=[0, 50]),
|
| 910 |
-
lonaxis=dict(range=[100, 180]),
|
| 911 |
-
)
|
| 912 |
-
)
|
| 913 |
-
|
| 914 |
-
return fig
|
| 915 |
-
def get_typhoons_for_year(self, year):
|
| 916 |
-
"""Get list of typhoons for a specific year"""
|
| 917 |
-
year_data = self.typhoon_data[self.typhoon_data['SEASON'] == year]
|
| 918 |
-
unique_typhoons = year_data.groupby('SID').first().reset_index()
|
| 919 |
-
return [
|
| 920 |
-
{'label': f"{row['NAME']} ({row['ISO_TIME'].strftime('%Y-%m-%d')})",
|
| 921 |
-
'value': row['SID']}
|
| 922 |
-
for _, row in unique_typhoons.iterrows()
|
| 923 |
-
]
|
| 924 |
|
| 925 |
-
def
|
| 926 |
-
"""
|
| 927 |
-
if not
|
| 928 |
-
return
|
| 929 |
-
|
| 930 |
-
storm_data = self.typhoon_data[self.typhoon_data['SID'] == typhoon_id]
|
| 931 |
-
storm_data = storm_data.sort_values('ISO_TIME')
|
| 932 |
|
| 933 |
-
#
|
| 934 |
-
|
| 935 |
|
| 936 |
-
#
|
| 937 |
-
|
|
|
|
| 938 |
|
| 939 |
-
|
| 940 |
-
|
| 941 |
-
|
| 942 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 943 |
fig = go.Figure()
|
| 944 |
|
| 945 |
fig.update_layout(
|
| 946 |
-
title=f"
|
| 947 |
-
showlegend=True,
|
| 948 |
geo=dict(
|
| 949 |
-
projection_type='
|
| 950 |
showland=True,
|
| 951 |
-
showcoastlines=True,
|
| 952 |
landcolor='rgb(243, 243, 243)',
|
| 953 |
countrycolor='rgb(204, 204, 204)',
|
| 954 |
-
coastlinecolor='rgb(
|
| 955 |
showocean=True,
|
| 956 |
oceancolor='rgb(230, 250, 255)',
|
| 957 |
lataxis=dict(range=[0, 50]),
|
| 958 |
lonaxis=dict(range=[100, 180]),
|
| 959 |
-
center=dict(lat=20, lon=140)
|
| 960 |
)
|
| 961 |
)
|
| 962 |
|
| 963 |
-
#
|
| 964 |
-
|
| 965 |
-
lon=storm_data['LON'],
|
| 966 |
-
lat=storm_data['LAT'],
|
| 967 |
-
mode='lines+markers',
|
| 968 |
-
line=dict(width=2, color='red'),
|
| 969 |
-
marker=dict(
|
| 970 |
-
size=8,
|
| 971 |
-
color=storm_data['USA_WIND'],
|
| 972 |
-
colorscale='Viridis',
|
| 973 |
-
showscale=True,
|
| 974 |
-
colorbar=dict(title='Wind Speed (kt)')
|
| 975 |
-
),
|
| 976 |
-
text=[f"Time: {time:%Y-%m-%d %H:%M}<br>Wind: {wind:.1f} kt<br>Pressure: {pres:.1f} hPa"
|
| 977 |
-
for time, wind, pres in zip(storm_data['ISO_TIME'],
|
| 978 |
-
storm_data['USA_WIND'],
|
| 979 |
-
storm_data['WMO_PRES'])],
|
| 980 |
-
hoverinfo='text'
|
| 981 |
-
))
|
| 982 |
|
| 983 |
-
|
| 984 |
-
|
| 985 |
-
|
| 986 |
-
|
| 987 |
-
|
| 988 |
-
|
| 989 |
-
|
| 990 |
-
|
| 991 |
-
|
| 992 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 993 |
|
| 994 |
-
|
| 995 |
-
|
| 996 |
-
|
| 997 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 998 |
|
| 999 |
-
|
| 1000 |
-
|
| 1001 |
-
- Minimum Pressure: {min_pressure:.1f} hPa
|
| 1002 |
-
- Maximum Category: {self.categorize_typhoon(max_wind)}
|
| 1003 |
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
|
| 1009 |
-
|
| 1010 |
-
stats = {
|
| 1011 |
-
'total_typhoons': len(data['SID'].unique()),
|
| 1012 |
-
'avg_wind': data['USA_WIND'].mean(),
|
| 1013 |
-
'max_wind': data['USA_WIND'].max(),
|
| 1014 |
-
'avg_pressure': data['WMO_PRES'].mean(),
|
| 1015 |
-
'min_pressure': data['WMO_PRES'].min(),
|
| 1016 |
-
'oni_correlation_wind': data['ONI'].corr(data['USA_WIND']),
|
| 1017 |
-
'oni_correlation_pressure': data['ONI'].corr(data['WMO_PRES']),
|
| 1018 |
-
'category_counts': data['Category'].value_counts().to_dict()
|
| 1019 |
-
}
|
| 1020 |
|
| 1021 |
-
return
|
| 1022 |
-
### Statistical Summary
|
| 1023 |
-
|
| 1024 |
-
- Total Typhoons: {stats['total_typhoons']}
|
| 1025 |
-
- Average Wind Speed: {stats['avg_wind']:.2f} kt
|
| 1026 |
-
- Maximum Wind Speed: {stats['max_wind']:.2f} kt
|
| 1027 |
-
- Average Pressure: {stats['avg_pressure']:.2f} hPa
|
| 1028 |
-
- Minimum Pressure: {stats['min_pressure']:.2f} hPa
|
| 1029 |
-
- ONI-Wind Speed Correlation: {stats['oni_correlation_wind']:.3f}
|
| 1030 |
-
- ONI-Pressure Correlation: {stats['oni_correlation_pressure']:.3f}
|
| 1031 |
|
| 1032 |
-
|
| 1033 |
-
|
| 1034 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1035 |
|
| 1036 |
def create_interface():
|
| 1037 |
analyzer = TyphoonAnalyzer()
|
|
@@ -1058,31 +951,21 @@ def create_interface():
|
|
| 1058 |
|
| 1059 |
analyze_btn = gr.Button("Analyze")
|
| 1060 |
|
| 1061 |
-
|
| 1062 |
-
tracks_plot = gr.Plot()
|
| 1063 |
-
with gr.Row():
|
| 1064 |
-
wind_plot = gr.Plot()
|
| 1065 |
-
pressure_plot = gr.Plot()
|
| 1066 |
|
| 1067 |
-
stats_text = gr.Markdown()
|
| 1068 |
-
|
| 1069 |
-
# Clustering Analysis Tab
|
| 1070 |
-
with gr.Tab("Clustering Analysis"):
|
| 1071 |
with gr.Row():
|
| 1072 |
-
|
| 1073 |
-
|
| 1074 |
|
| 1075 |
-
|
| 1076 |
-
cluster_plot = gr.Plot()
|
| 1077 |
-
cluster_stats = gr.Markdown()
|
| 1078 |
|
| 1079 |
-
# Animation Tab
|
| 1080 |
with gr.Tab("Typhoon Animation"):
|
| 1081 |
with gr.Row():
|
| 1082 |
animation_year = gr.Slider(
|
| 1083 |
-
minimum=
|
| 1084 |
-
maximum=2024,
|
| 1085 |
-
value=
|
| 1086 |
step=1,
|
| 1087 |
label="Select Year"
|
| 1088 |
)
|
|
@@ -1093,31 +976,27 @@ def create_interface():
|
|
| 1093 |
label="Select Typhoon",
|
| 1094 |
interactive=True
|
| 1095 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1096 |
|
| 1097 |
-
animation_btn = gr.Button("
|
| 1098 |
-
animation_plot = gr.Plot()
|
| 1099 |
animation_info = gr.Markdown()
|
| 1100 |
|
| 1101 |
# Search Tab
|
| 1102 |
with gr.Tab("Typhoon Search"):
|
| 1103 |
with gr.Row():
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
maximum=2024,
|
| 1107 |
-
value=2024,
|
| 1108 |
-
step=1,
|
| 1109 |
-
label="Select Year"
|
| 1110 |
-
)
|
| 1111 |
-
|
| 1112 |
-
with gr.Row():
|
| 1113 |
-
search_typhoon = gr.Dropdown(
|
| 1114 |
-
choices=[],
|
| 1115 |
-
label="Select Typhoon",
|
| 1116 |
-
interactive=True
|
| 1117 |
-
)
|
| 1118 |
|
| 1119 |
-
|
| 1120 |
-
search_plot = gr.Plot()
|
| 1121 |
search_info = gr.Markdown()
|
| 1122 |
|
| 1123 |
# Event handlers
|
|
@@ -1130,9 +1009,6 @@ def create_interface():
|
|
| 1130 |
results['stats']
|
| 1131 |
]
|
| 1132 |
|
| 1133 |
-
def cluster_callback(year, n_clusters):
|
| 1134 |
-
return analyzer.analyze_clusters(year, n_clusters)
|
| 1135 |
-
|
| 1136 |
def update_typhoon_choices(year):
|
| 1137 |
typhoons = analyzer.get_typhoons_for_year(year)
|
| 1138 |
return gr.update(choices=typhoons, value=None)
|
|
@@ -1144,13 +1020,6 @@ def create_interface():
|
|
| 1144 |
outputs=[tracks_plot, wind_plot, pressure_plot, stats_text]
|
| 1145 |
)
|
| 1146 |
|
| 1147 |
-
# Connect events for clustering
|
| 1148 |
-
cluster_btn.click(
|
| 1149 |
-
cluster_callback,
|
| 1150 |
-
inputs=[cluster_year, n_clusters],
|
| 1151 |
-
outputs=[cluster_plot, cluster_stats]
|
| 1152 |
-
)
|
| 1153 |
-
|
| 1154 |
# Connect events for Animation tab
|
| 1155 |
animation_year.change(
|
| 1156 |
update_typhoon_choices,
|
|
@@ -1160,21 +1029,15 @@ def create_interface():
|
|
| 1160 |
|
| 1161 |
animation_btn.click(
|
| 1162 |
analyzer.create_typhoon_animation,
|
| 1163 |
-
inputs=[animation_year, animation_typhoon],
|
| 1164 |
outputs=[animation_plot, animation_info]
|
| 1165 |
)
|
| 1166 |
|
| 1167 |
# Connect events for Search tab
|
| 1168 |
-
search_year.change(
|
| 1169 |
-
update_typhoon_choices,
|
| 1170 |
-
inputs=[search_year],
|
| 1171 |
-
outputs=[search_typhoon]
|
| 1172 |
-
)
|
| 1173 |
-
|
| 1174 |
search_btn.click(
|
| 1175 |
-
analyzer.
|
| 1176 |
-
inputs=[
|
| 1177 |
-
outputs=[
|
| 1178 |
)
|
| 1179 |
|
| 1180 |
return demo
|
|
|
|
| 107 |
raise
|
| 108 |
|
| 109 |
print("All required data files are ready")
|
| 110 |
+
|
| 111 |
def load_initial_data(self):
|
| 112 |
+
"""Initialize all required data"""
|
| 113 |
print("Loading initial data...")
|
| 114 |
self.update_oni_data()
|
| 115 |
self.oni_df = self.fetch_oni_data_from_csv()
|
|
|
|
| 121 |
self.merged_data = self.merge_data()
|
| 122 |
print("Initial data loading complete")
|
| 123 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
def convert_typhoondata(self, input_file, output_file):
|
| 125 |
+
"""Convert IBTrACS data to processed format"""
|
| 126 |
+
print(f"Converting typhoon data from {input_file} to {output_file}")
|
| 127 |
+
with open(input_file, 'r') as infile:
|
| 128 |
+
# Skip the header lines
|
| 129 |
+
next(infile)
|
| 130 |
+
next(infile)
|
| 131 |
+
|
| 132 |
+
reader = csv.reader(infile)
|
| 133 |
+
sid_data = defaultdict(list)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 134 |
|
| 135 |
+
for row in reader:
|
| 136 |
+
if not row: # Skip blank lines
|
| 137 |
+
continue
|
| 138 |
+
|
| 139 |
+
sid = row[0]
|
| 140 |
+
iso_time = row[6]
|
| 141 |
+
sid_data[sid].append((row, iso_time))
|
| 142 |
+
|
| 143 |
+
with open(output_file, 'w', newline='') as outfile:
|
| 144 |
+
fieldnames = ['SID', 'ISO_TIME', 'LAT', 'LON', 'SEASON', 'NAME',
|
| 145 |
+
'WMO_WIND', 'WMO_PRES', 'USA_WIND', 'USA_PRES',
|
| 146 |
+
'START_DATE', 'END_DATE']
|
| 147 |
+
writer = csv.DictWriter(outfile, fieldnames=fieldnames)
|
| 148 |
+
writer.writeheader()
|
| 149 |
|
| 150 |
+
for sid, data in sid_data.items():
|
| 151 |
+
start_date = min(data, key=lambda x: x[1])[1]
|
| 152 |
+
end_date = max(data, key=lambda x: x[1])[1]
|
| 153 |
+
|
| 154 |
+
for row, iso_time in data:
|
| 155 |
+
writer.writerow({
|
| 156 |
+
'SID': row[0],
|
| 157 |
+
'ISO_TIME': iso_time,
|
| 158 |
+
'LAT': row[8],
|
| 159 |
+
'LON': row[9],
|
| 160 |
+
'SEASON': row[1],
|
| 161 |
+
'NAME': row[5],
|
| 162 |
+
'WMO_WIND': row[10].strip() or ' ',
|
| 163 |
+
'WMO_PRES': row[11].strip() or ' ',
|
| 164 |
+
'USA_WIND': row[23].strip() or ' ',
|
| 165 |
+
'USA_PRES': row[24].strip() or ' ',
|
| 166 |
+
'START_DATE': start_date,
|
| 167 |
+
'END_DATE': end_date
|
| 168 |
+
})
|
| 169 |
+
|
| 170 |
+
def fetch_oni_data_from_csv(self):
|
| 171 |
+
"""Load ONI data from CSV"""
|
| 172 |
+
df = pd.read_csv(ONI_DATA_PATH)
|
| 173 |
+
df = df.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
|
| 174 |
|
| 175 |
+
# Convert month numbers to month names
|
| 176 |
+
month_map = {
|
| 177 |
+
'01': 'Jan', '02': 'Feb', '03': 'Mar', '04': 'Apr',
|
| 178 |
+
'05': 'May', '06': 'Jun', '07': 'Jul', '08': 'Aug',
|
| 179 |
+
'09': 'Sep', '10': 'Oct', '11': 'Nov', '12': 'Dec'
|
| 180 |
+
}
|
| 181 |
+
df['Month'] = df['Month'].map(month_map)
|
| 182 |
|
| 183 |
+
# Now create the date
|
| 184 |
+
df['Date'] = pd.to_datetime(df['Year'].astype(str) + df['Month'], format='%Y%b')
|
| 185 |
+
return df.set_index('Date')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 186 |
|
| 187 |
+
def update_oni_data(self):
|
| 188 |
+
"""Update ONI data from NOAA"""
|
| 189 |
+
if not self._should_update_oni():
|
| 190 |
+
return
|
| 191 |
+
|
| 192 |
+
url = "https://www.cpc.ncep.noaa.gov/data/indices/oni.ascii.txt"
|
| 193 |
+
with tempfile.NamedTemporaryFile(delete=False) as temp_file:
|
| 194 |
+
try:
|
| 195 |
+
response = requests.get(url)
|
| 196 |
+
response.raise_for_status()
|
| 197 |
+
temp_file.write(response.content)
|
| 198 |
+
self.convert_oni_ascii_to_csv(temp_file.name, ONI_DATA_PATH)
|
| 199 |
+
self.last_oni_update = date.today()
|
| 200 |
+
except Exception as e:
|
| 201 |
+
print(f"Error updating ONI data: {e}")
|
| 202 |
+
finally:
|
| 203 |
+
if os.path.exists(temp_file.name):
|
| 204 |
+
os.remove(temp_file.name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
|
| 206 |
+
def _should_update_oni(self):
|
| 207 |
+
"""Check if ONI data should be updated"""
|
| 208 |
+
today = datetime.now()
|
| 209 |
+
return (today.day in [1, 15] or
|
| 210 |
+
today.day == (today.replace(day=1, month=today.month%12+1) - timedelta(days=1)).day)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
|
|
|
|
|
|
| 212 |
def convert_oni_ascii_to_csv(self, input_file, output_file):
|
| 213 |
+
"""Convert ONI ASCII data to CSV format"""
|
| 214 |
data = defaultdict(lambda: [''] * 12)
|
| 215 |
season_to_month = {
|
| 216 |
'DJF': 12, 'JFM': 1, 'FMA': 2, 'MAM': 3, 'AMJ': 4, 'MJJ': 5,
|
| 217 |
'JJA': 6, 'JAS': 7, 'ASO': 8, 'SON': 9, 'OND': 10, 'NDJ': 11
|
| 218 |
}
|
| 219 |
+
|
| 220 |
with open(input_file, 'r') as f:
|
| 221 |
next(f) # Skip header
|
| 222 |
for line in f:
|
|
|
|
| 236 |
writer.writerow([year] + data[year])
|
| 237 |
|
| 238 |
def load_ibtracs_data(self):
|
| 239 |
+
"""Load IBTrACS data with caching"""
|
| 240 |
if os.path.exists(CACHE_FILE):
|
| 241 |
cache_time = datetime.fromtimestamp(os.path.getmtime(CACHE_FILE))
|
| 242 |
if datetime.now() - cache_time < timedelta(days=CACHE_EXPIRY_DAYS):
|
|
|
|
| 245 |
|
| 246 |
if os.path.exists(LOCAL_iBtrace_PATH):
|
| 247 |
ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs',
|
| 248 |
+
ibtracs_url=LOCAL_iBtrace_PATH)
|
| 249 |
else:
|
| 250 |
response = requests.get(iBtrace_uri)
|
| 251 |
response.raise_for_status()
|
| 252 |
with open(LOCAL_iBtrace_PATH, 'w') as f:
|
| 253 |
f.write(response.text)
|
| 254 |
ibtracs = tracks.TrackDataset(basin='west_pacific', source='ibtracs',
|
| 255 |
+
ibtracs_url=LOCAL_iBtrace_PATH)
|
| 256 |
|
| 257 |
with open(CACHE_FILE, 'wb') as f:
|
| 258 |
pickle.dump(ibtracs, f)
|
| 259 |
return ibtracs
|
| 260 |
|
| 261 |
def update_typhoon_data(self):
|
| 262 |
+
"""Update typhoon data from IBTrACS"""
|
| 263 |
try:
|
| 264 |
response = requests.head(iBtrace_uri)
|
| 265 |
remote_modified = datetime.strptime(response.headers['Last-Modified'],
|
| 266 |
+
'%a, %d %b %Y %H:%M:%S GMT')
|
| 267 |
local_modified = (datetime.fromtimestamp(os.path.getmtime(LOCAL_iBtrace_PATH))
|
| 268 |
if os.path.exists(LOCAL_iBtrace_PATH) else datetime.min)
|
| 269 |
|
|
|
|
| 272 |
response.raise_for_status()
|
| 273 |
with open(LOCAL_iBtrace_PATH, 'w') as f:
|
| 274 |
f.write(response.text)
|
| 275 |
+
print("Typhoon data updated successfully")
|
| 276 |
except Exception as e:
|
| 277 |
print(f"Error updating typhoon data: {e}")
|
| 278 |
|
| 279 |
def load_data(self):
|
| 280 |
+
"""Load ONI and typhoon data"""
|
| 281 |
oni_data = pd.read_csv(ONI_DATA_PATH)
|
| 282 |
typhoon_data = pd.read_csv(TYPHOON_DATA_PATH, low_memory=False)
|
| 283 |
typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
|
|
|
|
| 285 |
|
| 286 |
def process_oni_data(self, oni_data):
|
| 287 |
"""Process ONI data"""
|
| 288 |
+
oni_long = oni_data.melt(id_vars=['Year'], var_name='Month', value_name='ONI')
|
| 289 |
|
| 290 |
# Create a mapping for month numbers
|
| 291 |
month_map = {
|
|
|
|
| 300 |
return oni_long
|
| 301 |
|
| 302 |
def process_typhoon_data(self, typhoon_data):
|
| 303 |
+
"""Process typhoon data"""
|
| 304 |
typhoon_data['USA_WIND'] = pd.to_numeric(typhoon_data['USA_WIND'], errors='coerce')
|
| 305 |
typhoon_data['WMO_PRES'] = pd.to_numeric(typhoon_data['WMO_PRES'], errors='coerce')
|
| 306 |
typhoon_data['ISO_TIME'] = pd.to_datetime(typhoon_data['ISO_TIME'])
|
|
|
|
| 320 |
return typhoon_max
|
| 321 |
|
| 322 |
def merge_data(self):
|
| 323 |
+
"""Merge ONI and typhoon data"""
|
| 324 |
return pd.merge(self.typhoon_max, self.oni_long, on=['Year', 'Month'])
|
| 325 |
|
| 326 |
def categorize_typhoon(self, wind_speed):
|
| 327 |
+
"""Categorize typhoon based on wind speed"""
|
| 328 |
+
if np.isnan(wind_speed):
|
| 329 |
+
return 'Unknown'
|
| 330 |
if wind_speed >= 137:
|
| 331 |
return 'C5 Super Typhoon'
|
| 332 |
elif wind_speed >= 113:
|
|
|
|
| 343 |
return 'Tropical Depression'
|
| 344 |
|
| 345 |
def analyze_typhoon(self, start_year, start_month, end_year, end_month, enso_value='all'):
|
| 346 |
+
"""Main analysis function"""
|
| 347 |
start_date = datetime(start_year, start_month, 1)
|
| 348 |
end_date = datetime(end_year, end_month, 28)
|
| 349 |
|
|
|
|
| 363 |
'tracks': self.create_tracks_plot(filtered_data),
|
| 364 |
'wind': self.create_wind_analysis(filtered_data),
|
| 365 |
'pressure': self.create_pressure_analysis(filtered_data),
|
|
|
|
| 366 |
'stats': self.generate_statistics(filtered_data)
|
| 367 |
}
|
| 368 |
|
|
|
|
| 437 |
|
| 438 |
return fig
|
| 439 |
|
| 440 |
+
def create_wind_analysis(self, data):
|
| 441 |
+
"""Create wind speed analysis plot"""
|
| 442 |
+
fig = px.scatter(data,
|
| 443 |
+
x='ONI',
|
| 444 |
+
y='USA_WIND',
|
| 445 |
+
color='Category',
|
| 446 |
+
color_discrete_map=COLOR_MAP,
|
| 447 |
+
title='Wind Speed vs ONI Index',
|
| 448 |
+
labels={
|
| 449 |
+
'ONI': 'Oceanic Niño Index',
|
| 450 |
+
'USA_WIND': 'Maximum Wind Speed (kt)'
|
| 451 |
+
},
|
| 452 |
+
hover_data=['NAME', 'ISO_TIME', 'Category']
|
| 453 |
+
)
|
| 454 |
+
|
| 455 |
+
# Add regression line
|
| 456 |
+
x = data['ONI']
|
| 457 |
+
y = data['USA_WIND']
|
| 458 |
+
slope, intercept = np.polyfit(x, y, 1)
|
| 459 |
+
fig.add_trace(
|
| 460 |
+
go.Scatter(
|
| 461 |
+
x=x,
|
| 462 |
+
y=slope * x + intercept,
|
| 463 |
+
mode='lines',
|
| 464 |
+
name=f'Regression (slope={slope:.2f})',
|
| 465 |
+
line=dict(color='black', dash='dash')
|
| 466 |
+
)
|
| 467 |
+
)
|
| 468 |
+
|
| 469 |
+
return fig
|
| 470 |
+
|
| 471 |
+
def create_pressure_analysis(self, data):
|
| 472 |
+
"""Create pressure analysis plot"""
|
| 473 |
+
fig = px.scatter(data,
|
| 474 |
+
x='ONI',
|
| 475 |
+
y='WMO_PRES',
|
| 476 |
+
color='Category',
|
| 477 |
+
color_discrete_map=COLOR_MAP,
|
| 478 |
+
title='Pressure vs ONI Index',
|
| 479 |
+
labels={
|
| 480 |
+
'ONI': 'Oceanic Niño Index',
|
| 481 |
+
'WMO_PRES': 'Minimum Pressure (hPa)'
|
| 482 |
+
},
|
| 483 |
+
hover_data=['NAME', 'ISO_TIME', 'Category']
|
| 484 |
+
)
|
| 485 |
+
|
| 486 |
+
# Add regression line
|
| 487 |
+
x = data['ONI']
|
| 488 |
+
y = data['WMO_PRES']
|
| 489 |
+
slope, intercept = np.polyfit(x, y, 1)
|
| 490 |
+
fig.add_trace(
|
| 491 |
+
go.Scatter(
|
| 492 |
+
x=x,
|
| 493 |
+
y=slope * x + intercept,
|
| 494 |
+
mode='lines',
|
| 495 |
+
name=f'Regression (slope={slope:.2f})',
|
| 496 |
+
line=dict(color='black', dash='dash')
|
| 497 |
+
)
|
| 498 |
+
)
|
| 499 |
+
|
| 500 |
+
return fig
|
| 501 |
+
|
| 502 |
+
def generate_statistics(self, data):
|
| 503 |
+
"""Generate statistical summary"""
|
| 504 |
+
stats = {
|
| 505 |
+
'total_typhoons': len(data['SID'].unique()),
|
| 506 |
+
'avg_wind': data['USA_WIND'].mean(),
|
| 507 |
+
'max_wind': data['USA_WIND'].max(),
|
| 508 |
+
'avg_pressure': data['WMO_PRES'].mean(),
|
| 509 |
+
'min_pressure': data['WMO_PRES'].min(),
|
| 510 |
+
'oni_correlation_wind': data['ONI'].corr(data['USA_WIND']),
|
| 511 |
+
'oni_correlation_pressure': data['ONI'].corr(data['WMO_PRES']),
|
| 512 |
+
'category_counts': data['Category'].value_counts().to_dict()
|
| 513 |
+
}
|
| 514 |
+
|
| 515 |
+
return f"""
|
| 516 |
+
### Statistical Summary
|
| 517 |
+
|
| 518 |
+
- Total Typhoons: {stats['total_typhoons']}
|
| 519 |
+
- Average Wind Speed: {stats['avg_wind']:.2f} kt
|
| 520 |
+
- Maximum Wind Speed: {stats['max_wind']:.2f} kt
|
| 521 |
+
- Average Pressure: {stats['avg_pressure']:.2f} hPa
|
| 522 |
+
- Minimum Pressure: {stats['min_pressure']:.2f} hPa
|
| 523 |
+
- ONI-Wind Speed Correlation: {stats['oni_correlation_wind']:.3f}
|
| 524 |
+
- ONI-Pressure Correlation: {stats['oni_correlation_pressure']:.3f}
|
| 525 |
+
|
| 526 |
+
### Category Distribution
|
| 527 |
+
{chr(10).join(f'- {cat}: {count}' for cat, count in stats['category_counts'].items())}
|
| 528 |
+
"""
|
| 529 |
+
|
| 530 |
def analyze_clusters(self, year, n_clusters):
|
| 531 |
"""Analyze typhoon clusters for a specific year"""
|
| 532 |
year_data = self.typhoon_data[self.typhoon_data['SEASON'] == year]
|
|
|
|
| 610 |
stats_text += f"- Cluster {i+1}: {cluster_counts[i]} typhoons\n"
|
| 611 |
|
| 612 |
return fig, stats_text
|
| 613 |
+
|
| 614 |
def get_typhoons_for_year(self, year):
|
| 615 |
"""Get list of typhoons for a specific year"""
|
| 616 |
+
try:
|
| 617 |
+
season = self.ibtracs.get_season(year)
|
| 618 |
+
storm_summary = season.summary()
|
| 619 |
+
|
| 620 |
+
typhoon_options = []
|
| 621 |
+
for i in range(storm_summary['season_storms']):
|
| 622 |
+
storm_id = storm_summary['id'][i]
|
| 623 |
+
storm_name = storm_summary['name'][i]
|
| 624 |
+
typhoon_options.append({'label': f"{storm_name} ({storm_id})", 'value': storm_id})
|
| 625 |
+
|
| 626 |
+
return typhoon_options
|
| 627 |
+
except Exception as e:
|
| 628 |
+
print(f"Error getting typhoons for year {year}: {str(e)}")
|
| 629 |
+
return []
|
| 630 |
|
| 631 |
+
def create_typhoon_animation(self, year, storm_id, standard='atlantic'):
|
| 632 |
"""Create animated visualization of typhoon path"""
|
| 633 |
+
if not storm_id:
|
| 634 |
+
return go.Figure(), "Please select a typhoon"
|
| 635 |
+
|
| 636 |
+
storm = self.ibtracs.get_storm(storm_id)
|
| 637 |
+
|
| 638 |
+
fig = go.Figure()
|
| 639 |
+
|
| 640 |
+
# Base map setup with correct scaling
|
| 641 |
+
fig.update_layout(
|
| 642 |
+
title=f"{year} - {storm.name} Typhoon Path",
|
| 643 |
geo=dict(
|
| 644 |
+
projection_type='natural earth',
|
| 645 |
showland=True,
|
|
|
|
| 646 |
landcolor='rgb(243, 243, 243)',
|
| 647 |
countrycolor='rgb(204, 204, 204)',
|
| 648 |
+
coastlinecolor='rgb(100, 100, 100)',
|
| 649 |
showocean=True,
|
| 650 |
oceancolor='rgb(230, 250, 255)',
|
| 651 |
lataxis=dict(range=[0, 50]),
|
| 652 |
lonaxis=dict(range=[100, 180]),
|
| 653 |
+
center=dict(lat=20, lon=140),
|
| 654 |
+
),
|
| 655 |
+
updatemenus=[{
|
| 656 |
+
"buttons": [
|
| 657 |
+
{
|
| 658 |
+
"args": [None, {"frame": {"duration": 100, "redraw": True},
|
| 659 |
+
"fromcurrent": True,
|
| 660 |
+
"transition": {"duration": 0}}],
|
| 661 |
+
"label": "Play",
|
| 662 |
+
"method": "animate"
|
| 663 |
+
},
|
| 664 |
+
{
|
| 665 |
+
"args": [[None], {"frame": {"duration": 0, "redraw": True},
|
| 666 |
+
"mode": "immediate",
|
| 667 |
+
"transition": {"duration": 0}}],
|
| 668 |
+
"label": "Pause",
|
| 669 |
+
"method": "animate"
|
| 670 |
+
}
|
| 671 |
+
],
|
| 672 |
+
"direction": "left",
|
| 673 |
+
"pad": {"r": 10, "t": 87},
|
| 674 |
+
"showactive": False,
|
| 675 |
+
"type": "buttons",
|
| 676 |
+
"x": 0.1,
|
| 677 |
+
"xanchor": "right",
|
| 678 |
+
"y": 0,
|
| 679 |
+
"yanchor": "top"
|
| 680 |
+
}]
|
| 681 |
)
|
| 682 |
|
| 683 |
+
# Create animation frames
|
| 684 |
+
frames = []
|
| 685 |
+
for i in range(len(storm.time)):
|
| 686 |
+
category, color = self.categorize_typhoon_by_standard(storm.vmax[i], standard)
|
| 687 |
|
| 688 |
+
# Get extra radius data if available
|
| 689 |
+
radius_info = ""
|
| 690 |
+
if hasattr(storm, 'dict'):
|
| 691 |
+
r34_ne = storm.dict.get('USA_R34_NE', [None])[i] if 'USA_R34_NE' in storm.dict else None
|
| 692 |
+
r34_se = storm.dict.get('USA_R34_SE', [None])[i] if 'USA_R34_SE' in storm.dict else None
|
| 693 |
+
r34_sw = storm.dict.get('USA_R34_SW', [None])[i] if 'USA_R34_SW' in storm.dict else None
|
| 694 |
+
r34_nw = storm.dict.get('USA_R34_NW', [None])[i] if 'USA_R34_NW' in storm.dict else None
|
| 695 |
+
rmw = storm.dict.get('USA_RMW', [None])[i] if 'USA_RMW' in storm.dict else None
|
| 696 |
+
eye = storm.dict.get('USA_EYE', [None])[i] if 'USA_EYE' in storm.dict else None
|
| 697 |
|
| 698 |
+
if any([r34_ne, r34_se, r34_sw, r34_nw, rmw, eye]):
|
| 699 |
+
radius_info = f"<br>R34: NE={r34_ne}, SE={r34_se}, SW={r34_sw}, NW={r34_nw}<br>"
|
| 700 |
+
radius_info += f"RMW: {rmw}<br>Eye Diameter: {eye}"
|
|
|
|
|
|
|
|
|
|
| 701 |
|
| 702 |
+
frame = go.Frame(
|
| 703 |
+
data=[
|
| 704 |
+
go.Scattergeo(
|
| 705 |
+
lon=storm.lon[:i+1],
|
| 706 |
+
lat=storm.lat[:i+1],
|
| 707 |
+
mode='lines',
|
| 708 |
+
line=dict(width=2, color='blue'),
|
| 709 |
+
name='Path Traveled',
|
| 710 |
+
showlegend=False,
|
| 711 |
+
),
|
| 712 |
+
go.Scattergeo(
|
| 713 |
+
lon=[storm.lon[i]],
|
| 714 |
+
lat=[storm.lat[i]],
|
| 715 |
+
mode='markers+text',
|
| 716 |
+
marker=dict(size=10, color=color, symbol='star'),
|
| 717 |
+
text=category,
|
| 718 |
+
textposition="top center",
|
| 719 |
+
textfont=dict(size=12, color=color),
|
| 720 |
+
name='Current Location',
|
| 721 |
+
hovertemplate=(
|
| 722 |
+
f"{storm.time[i].strftime('%Y-%m-%d %H:%M')}<br>"
|
| 723 |
+
f"Category: {category}<br>"
|
| 724 |
+
f"Wind Speed: {storm.vmax[i]:.1f} kt<br>"
|
| 725 |
+
f"{radius_info}"
|
| 726 |
+
),
|
| 727 |
+
),
|
| 728 |
+
],name=f"frame{i}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 729 |
)
|
| 730 |
+
frames.append(frame)
|
| 731 |
|
| 732 |
+
fig.frames = frames
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 733 |
|
| 734 |
+
# Add initial track and starting point
|
|
|
|
|
|
|
|
|
|
| 735 |
fig.add_trace(
|
| 736 |
+
go.Scattergeo(
|
| 737 |
+
lon=storm.lon,
|
| 738 |
+
lat=storm.lat,
|
| 739 |
mode='lines',
|
| 740 |
+
line=dict(width=2, color='gray'),
|
| 741 |
+
name='Complete Path',
|
| 742 |
+
showlegend=True,
|
| 743 |
)
|
| 744 |
)
|
|
|
|
|
|
|
| 745 |
|
| 746 |
+
fig.add_trace(
|
| 747 |
+
go.Scattergeo(
|
| 748 |
+
lon=[storm.lon[0]],
|
| 749 |
+
lat=[storm.lat[0]],
|
| 750 |
+
mode='markers',
|
| 751 |
+
marker=dict(size=10, color='green', symbol='star'),
|
| 752 |
+
name='Starting Point',
|
| 753 |
+
text=storm.time[0].strftime('%Y-%m-%d %H:%M'),
|
| 754 |
+
hoverinfo='text+name',
|
| 755 |
+
)
|
| 756 |
+
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 757 |
|
| 758 |
+
# Add slider for frame selection
|
| 759 |
+
sliders = [{
|
| 760 |
+
"active": 0,
|
| 761 |
+
"yanchor": "top",
|
| 762 |
+
"xanchor": "left",
|
| 763 |
+
"currentvalue": {
|
| 764 |
+
"font": {"size": 20},
|
| 765 |
+
"prefix": "Time: ",
|
| 766 |
+
"visible": True,
|
| 767 |
+
"xanchor": "right"
|
| 768 |
+
},
|
| 769 |
+
"transition": {"duration": 100, "easing": "cubic-in-out"},
|
| 770 |
+
"pad": {"b": 10, "t": 50},
|
| 771 |
+
"len": 0.9,
|
| 772 |
+
"x": 0.1,
|
| 773 |
+
"y": 0,
|
| 774 |
+
"steps": [
|
| 775 |
+
{
|
| 776 |
+
"args": [[f"frame{k}"],
|
| 777 |
+
{"frame": {"duration": 100, "redraw": True},
|
| 778 |
+
"mode": "immediate",
|
| 779 |
+
"transition": {"duration": 0}}
|
| 780 |
+
],
|
| 781 |
+
"label": storm.time[k].strftime('%Y-%m-%d %H:%M'),
|
| 782 |
+
"method": "animate"
|
| 783 |
+
}
|
| 784 |
+
for k in range(len(storm.time))
|
| 785 |
+
]
|
| 786 |
+
}]
|
| 787 |
|
| 788 |
+
fig.update_layout(sliders=sliders)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 789 |
|
| 790 |
+
info_text = f"""
|
| 791 |
+
### Typhoon Information
|
| 792 |
+
- **Name:** {storm.name}
|
| 793 |
+
- **Start Date:** {storm.time[0].strftime('%Y-%m-%d %H:%M')}
|
| 794 |
+
- **End Date:** {storm.time[-1].strftime('%Y-%m-%d %H:%M')}
|
| 795 |
+
- **Duration:** {(storm.time[-1] - storm.time[0]).total_seconds() / 3600:.1f} hours
|
| 796 |
+
- **Maximum Wind Speed:** {max(storm.vmax):.1f} kt
|
| 797 |
+
- **Minimum Pressure:** {min(storm.mslp):.1f} hPa
|
| 798 |
+
- **Peak Category:** {self.categorize_typhoon_by_standard(max(storm.vmax), standard)[0]}
|
| 799 |
+
"""
|
| 800 |
|
| 801 |
+
return fig, info_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 802 |
|
| 803 |
+
def search_typhoons(self, query):
|
| 804 |
+
"""Search for typhoons by name"""
|
| 805 |
+
if not query:
|
| 806 |
+
return go.Figure(), "Please enter a typhoon name to search"
|
|
|
|
|
|
|
|
|
|
| 807 |
|
| 808 |
+
# Find all typhoons matching the query
|
| 809 |
+
matching_storms = []
|
| 810 |
|
| 811 |
+
# Limit search to last 30 years to improve performance
|
| 812 |
+
current_year = datetime.now().year
|
| 813 |
+
start_year = current_year - 30
|
| 814 |
|
| 815 |
+
for year in range(start_year, current_year + 1):
|
| 816 |
+
try:
|
| 817 |
+
season = self.ibtracs.get_season(year)
|
| 818 |
+
for storm_id in season.summary()['id']:
|
| 819 |
+
storm = self.ibtracs.get_storm(storm_id)
|
| 820 |
+
if query.lower() in storm.name.lower():
|
| 821 |
+
matching_storms.append((year, storm))
|
| 822 |
+
except Exception as e:
|
| 823 |
+
print(f"Error searching year {year}: {str(e)}")
|
| 824 |
+
continue
|
| 825 |
+
|
| 826 |
+
if not matching_storms:
|
| 827 |
+
return go.Figure(), "No typhoons found matching your search"
|
| 828 |
+
|
| 829 |
+
# Create visualization of all matching typhoons
|
| 830 |
fig = go.Figure()
|
| 831 |
|
| 832 |
fig.update_layout(
|
| 833 |
+
title=f"Typhoons Matching: '{query}'",
|
|
|
|
| 834 |
geo=dict(
|
| 835 |
+
projection_type='natural earth',
|
| 836 |
showland=True,
|
|
|
|
| 837 |
landcolor='rgb(243, 243, 243)',
|
| 838 |
countrycolor='rgb(204, 204, 204)',
|
| 839 |
+
coastlinecolor='rgb(100, 100, 100)',
|
| 840 |
showocean=True,
|
| 841 |
oceancolor='rgb(230, 250, 255)',
|
| 842 |
lataxis=dict(range=[0, 50]),
|
| 843 |
lonaxis=dict(range=[100, 180]),
|
| 844 |
+
center=dict(lat=20, lon=140),
|
| 845 |
)
|
| 846 |
)
|
| 847 |
|
| 848 |
+
# Plot each matching storm with a different color
|
| 849 |
+
colors = px.colors.qualitative.Plotly
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 850 |
|
| 851 |
+
for i, (year, storm) in enumerate(matching_storms):
|
| 852 |
+
color = colors[i % len(colors)]
|
| 853 |
+
|
| 854 |
+
fig.add_trace(go.Scattergeo(
|
| 855 |
+
lon=storm.lon,
|
| 856 |
+
lat=storm.lat,
|
| 857 |
+
mode='lines',
|
| 858 |
+
line=dict(width=3, color=color),
|
| 859 |
+
name=f"{storm.name} ({year})",
|
| 860 |
+
hovertemplate=(
|
| 861 |
+
f"Name: {storm.name}<br>"
|
| 862 |
+
f"Year: {year}<br>"
|
| 863 |
+
f"Max Wind: {max(storm.vmax):.1f} kt<br>"
|
| 864 |
+
f"Min Pressure: {min(storm.mslp):.1f} hPa<br>"
|
| 865 |
+
f"Position: %{lat:.2f}°N, %{lon:.2f}°E"
|
| 866 |
+
)
|
| 867 |
+
))
|
| 868 |
|
| 869 |
+
# Add starting points
|
| 870 |
+
for i, (year, storm) in enumerate(matching_storms):
|
| 871 |
+
color = colors[i % len(colors)]
|
| 872 |
+
|
| 873 |
+
fig.add_trace(go.Scattergeo(
|
| 874 |
+
lon=[storm.lon[0]],
|
| 875 |
+
lat=[storm.lat[0]],
|
| 876 |
+
mode='markers',
|
| 877 |
+
marker=dict(size=10, color=color, symbol='circle'),
|
| 878 |
+
name=f"Start: {storm.name} ({year})",
|
| 879 |
+
showlegend=False,
|
| 880 |
+
hoverinfo='name'
|
| 881 |
+
))
|
| 882 |
|
| 883 |
+
# Create information text
|
| 884 |
+
info_text = f"### Found {len(matching_storms)} typhoons matching '{query}':\n\n"
|
|
|
|
|
|
|
| 885 |
|
| 886 |
+
for year, storm in matching_storms:
|
| 887 |
+
info_text += f"- **{storm.name} ({year})**\n"
|
| 888 |
+
info_text += f" - Time: {storm.time[0].strftime('%Y-%m-%d')} to {storm.time[-1].strftime('%Y-%m-%d')}\n"
|
| 889 |
+
info_text += f" - Max Wind: {max(storm.vmax):.1f} kt\n"
|
| 890 |
+
info_text += f" - Min Pressure: {min(storm.mslp):.1f} hPa\n"
|
| 891 |
+
info_text += f" - Category: {self.categorize_typhoon_by_standard(max(storm.vmax))[0]}\n\n"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 892 |
|
| 893 |
+
return fig, info_text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 894 |
|
| 895 |
+
def categorize_typhoon_by_standard(self, wind_speed, standard='atlantic'):
|
| 896 |
+
"""
|
| 897 |
+
Categorize typhoon based on wind speed and chosen standard
|
| 898 |
+
wind_speed is in knots
|
| 899 |
+
"""
|
| 900 |
+
if standard == 'taiwan':
|
| 901 |
+
# Convert knots to m/s for Taiwan standard
|
| 902 |
+
wind_speed_ms = wind_speed * 0.514444
|
| 903 |
+
|
| 904 |
+
if wind_speed_ms >= 51.0:
|
| 905 |
+
return 'Strong Typhoon', 'rgb(255, 0, 0)'
|
| 906 |
+
elif wind_speed_ms >= 33.7:
|
| 907 |
+
return 'Medium Typhoon', 'rgb(255, 127, 0)'
|
| 908 |
+
elif wind_speed_ms >= 17.2:
|
| 909 |
+
return 'Mild Typhoon', 'rgb(255, 255, 0)'
|
| 910 |
+
else:
|
| 911 |
+
return 'Tropical Depression', 'rgb(173, 216, 230)'
|
| 912 |
+
else:
|
| 913 |
+
# Atlantic standard uses knots
|
| 914 |
+
if wind_speed >= 137:
|
| 915 |
+
return 'C5 Super Typhoon', 'rgb(255, 0, 0)'
|
| 916 |
+
elif wind_speed >= 113:
|
| 917 |
+
return 'C4 Very Strong Typhoon', 'rgb(255, 63, 0)'
|
| 918 |
+
elif wind_speed >= 96:
|
| 919 |
+
return 'C3 Strong Typhoon', 'rgb(255, 127, 0)'
|
| 920 |
+
elif wind_speed >= 83:
|
| 921 |
+
return 'C2 Typhoon', 'rgb(255, 191, 0)'
|
| 922 |
+
elif wind_speed >= 64:
|
| 923 |
+
return 'C1 Typhoon', 'rgb(255, 255, 0)'
|
| 924 |
+
elif wind_speed >= 34:
|
| 925 |
+
return 'Tropical Storm', 'rgb(0, 255, 255)'
|
| 926 |
+
else:
|
| 927 |
+
return 'Tropical Depression', 'rgb(173, 216, 230)'
|
| 928 |
|
| 929 |
def create_interface():
|
| 930 |
analyzer = TyphoonAnalyzer()
|
|
|
|
| 951 |
|
| 952 |
analyze_btn = gr.Button("Analyze")
|
| 953 |
|
| 954 |
+
tracks_plot = gr.Plot(label="Typhoon Tracks")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 955 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 956 |
with gr.Row():
|
| 957 |
+
wind_plot = gr.Plot(label="Wind Speed Analysis")
|
| 958 |
+
pressure_plot = gr.Plot(label="Pressure Analysis")
|
| 959 |
|
| 960 |
+
stats_text = gr.Markdown()
|
|
|
|
|
|
|
| 961 |
|
| 962 |
+
# Typhoon Animation Tab
|
| 963 |
with gr.Tab("Typhoon Animation"):
|
| 964 |
with gr.Row():
|
| 965 |
animation_year = gr.Slider(
|
| 966 |
+
minimum=1950,
|
| 967 |
+
maximum=2024,
|
| 968 |
+
value=2020,
|
| 969 |
step=1,
|
| 970 |
label="Select Year"
|
| 971 |
)
|
|
|
|
| 976 |
label="Select Typhoon",
|
| 977 |
interactive=True
|
| 978 |
)
|
| 979 |
+
|
| 980 |
+
standard_dropdown = gr.Dropdown(
|
| 981 |
+
choices=[
|
| 982 |
+
{"label": "Atlantic Standard", "value": "atlantic"},
|
| 983 |
+
{"label": "Taiwan Standard", "value": "taiwan"}
|
| 984 |
+
],
|
| 985 |
+
value="atlantic",
|
| 986 |
+
label="Classification Standard"
|
| 987 |
+
)
|
| 988 |
|
| 989 |
+
animation_btn = gr.Button("Show Typhoon Path", variant="primary")
|
| 990 |
+
animation_plot = gr.Plot(label="Typhoon Path Animation")
|
| 991 |
animation_info = gr.Markdown()
|
| 992 |
|
| 993 |
# Search Tab
|
| 994 |
with gr.Tab("Typhoon Search"):
|
| 995 |
with gr.Row():
|
| 996 |
+
search_input = gr.Textbox(label="Search Typhoon Name")
|
| 997 |
+
search_btn = gr.Button("Search Typhoons", variant="primary")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 998 |
|
| 999 |
+
search_results = gr.Plot(label="Search Results")
|
|
|
|
| 1000 |
search_info = gr.Markdown()
|
| 1001 |
|
| 1002 |
# Event handlers
|
|
|
|
| 1009 |
results['stats']
|
| 1010 |
]
|
| 1011 |
|
|
|
|
|
|
|
|
|
|
| 1012 |
def update_typhoon_choices(year):
|
| 1013 |
typhoons = analyzer.get_typhoons_for_year(year)
|
| 1014 |
return gr.update(choices=typhoons, value=None)
|
|
|
|
| 1020 |
outputs=[tracks_plot, wind_plot, pressure_plot, stats_text]
|
| 1021 |
)
|
| 1022 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1023 |
# Connect events for Animation tab
|
| 1024 |
animation_year.change(
|
| 1025 |
update_typhoon_choices,
|
|
|
|
| 1029 |
|
| 1030 |
animation_btn.click(
|
| 1031 |
analyzer.create_typhoon_animation,
|
| 1032 |
+
inputs=[animation_year, animation_typhoon, standard_dropdown],
|
| 1033 |
outputs=[animation_plot, animation_info]
|
| 1034 |
)
|
| 1035 |
|
| 1036 |
# Connect events for Search tab
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1037 |
search_btn.click(
|
| 1038 |
+
analyzer.search_typhoons,
|
| 1039 |
+
inputs=[search_input],
|
| 1040 |
+
outputs=[search_results, search_info]
|
| 1041 |
)
|
| 1042 |
|
| 1043 |
return demo
|