Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,6 +1,9 @@
|
|
| 1 |
import gradio as gr
|
| 2 |
import pandas as pd
|
| 3 |
import numpy as np
|
|
|
|
|
|
|
|
|
|
| 4 |
import plotly.graph_objects as go
|
| 5 |
import plotly.express as px
|
| 6 |
import tropycal.tracks as tracks
|
|
@@ -15,9 +18,6 @@ import tempfile
|
|
| 15 |
import csv
|
| 16 |
from collections import defaultdict
|
| 17 |
import filecmp
|
| 18 |
-
import uuid
|
| 19 |
-
import base64
|
| 20 |
-
from io import BytesIO
|
| 21 |
|
| 22 |
# Command-line argument parsing
|
| 23 |
parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
|
|
@@ -32,7 +32,7 @@ iBtrace_uri = 'https://www.ncei.noaa.gov/data/international-best-track-archive-f
|
|
| 32 |
CACHE_FILE = 'ibtracs_cache.pkl'
|
| 33 |
CACHE_EXPIRY_DAYS = 1
|
| 34 |
|
| 35 |
-
# Color
|
| 36 |
color_map = {
|
| 37 |
'C5 Super Typhoon': 'rgb(255, 0, 0)',
|
| 38 |
'C4 Very Strong Typhoon': 'rgb(255, 63, 0)',
|
|
@@ -43,22 +43,22 @@ color_map = {
|
|
| 43 |
'Tropical Depression': 'rgb(173, 216, 230)'
|
| 44 |
}
|
| 45 |
|
| 46 |
-
# Classification standards
|
| 47 |
atlantic_standard = {
|
| 48 |
-
'C5 Super Typhoon': {'wind_speed': 137, 'color': '
|
| 49 |
-
'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': '
|
| 50 |
-
'C3 Strong Typhoon': {'wind_speed': 96, 'color': '
|
| 51 |
-
'C2 Typhoon': {'wind_speed': 83, 'color': '
|
| 52 |
-
'C1 Typhoon': {'wind_speed': 64, 'color': '
|
| 53 |
-
'Tropical Storm': {'wind_speed': 34, 'color': '
|
| 54 |
-
'Tropical Depression': {'wind_speed': 0, 'color': '
|
| 55 |
}
|
| 56 |
|
| 57 |
taiwan_standard = {
|
| 58 |
-
'Strong Typhoon': {'wind_speed': 51.0, 'color': '
|
| 59 |
-
'Medium Typhoon': {'wind_speed': 33.7, 'color': '
|
| 60 |
-
'Mild Typhoon': {'wind_speed': 17.2, 'color': '
|
| 61 |
-
'Tropical Depression': {'wind_speed': 0, 'color': '
|
| 62 |
}
|
| 63 |
|
| 64 |
# Data loading and preprocessing functions
|
|
@@ -212,7 +212,7 @@ oni_long = process_oni_data(oni_data)
|
|
| 212 |
typhoon_max = process_typhoon_data(typhoon_data)
|
| 213 |
merged_data = merge_data(oni_long, typhoon_max)
|
| 214 |
|
| 215 |
-
# Main analysis functions
|
| 216 |
def generate_typhoon_tracks(filtered_data, typhoon_search):
|
| 217 |
fig = go.Figure()
|
| 218 |
for sid in filtered_data['SID'].unique():
|
|
@@ -230,7 +230,11 @@ def generate_typhoon_tracks(filtered_data, typhoon_search):
|
|
| 230 |
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
|
| 231 |
name=f'Matched: {typhoon_search}', line=dict(width=5, color='yellow')
|
| 232 |
))
|
| 233 |
-
fig.update_layout(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 234 |
return fig
|
| 235 |
|
| 236 |
def generate_wind_oni_scatter(filtered_data, typhoon_search):
|
|
@@ -296,7 +300,7 @@ def generate_main_analysis(start_year, start_month, end_year, end_month, enso_ph
|
|
| 296 |
|
| 297 |
return tracks_fig, wind_scatter, pressure_scatter, regression_fig, slopes_text
|
| 298 |
|
| 299 |
-
#
|
| 300 |
def categorize_typhoon_by_standard(wind_speed, standard):
|
| 301 |
if standard == 'taiwan':
|
| 302 |
wind_speed_ms = wind_speed * 0.514444
|
|
@@ -322,9 +326,9 @@ def categorize_typhoon_by_standard(wind_speed, standard):
|
|
| 322 |
return 'Tropical Storm', atlantic_standard['Tropical Storm']['color']
|
| 323 |
return 'Tropical Depression', atlantic_standard['Tropical Depression']['color']
|
| 324 |
|
| 325 |
-
def
|
| 326 |
if not typhoon:
|
| 327 |
-
return
|
| 328 |
|
| 329 |
typhoon_id = typhoon.split('(')[-1].strip(')')
|
| 330 |
storm = ibtracs.get_storm(typhoon_id)
|
|
@@ -335,101 +339,54 @@ def generate_track_animation(year, typhoon, standard):
|
|
| 335 |
lat_padding = max((max_lat - min_lat) * 0.3, 5)
|
| 336 |
lon_padding = max((max_lon - min_lon) * 0.3, 5)
|
| 337 |
|
| 338 |
-
#
|
| 339 |
-
|
| 340 |
-
|
| 341 |
-
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
| 345 |
-
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
| 352 |
-
|
| 353 |
-
)
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
| 363 |
-
|
| 364 |
-
|
| 365 |
-
|
| 366 |
-
|
| 367 |
-
|
| 368 |
-
|
| 369 |
-
|
| 370 |
-
|
| 371 |
-
|
| 372 |
-
|
| 373 |
-
|
| 374 |
-
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
| 382 |
-
|
| 383 |
-
|
| 384 |
-
|
| 385 |
-
|
| 386 |
-
# Convert figure to PNG and encode in base64
|
| 387 |
-
img_bytes = fig.to_image(format="png", width=1000, height=700)
|
| 388 |
-
img_base64 = base64.b64encode(img_bytes).decode('utf-8')
|
| 389 |
-
frames.append(f"data:image/png;base64,{img_base64}")
|
| 390 |
-
dates.append(storm.time[i].strftime('%Y-%m-%d %H:%M'))
|
| 391 |
-
|
| 392 |
-
# JavaScript to handle animation
|
| 393 |
-
js_script = """
|
| 394 |
-
<div style="text-align: center;">
|
| 395 |
-
<img id="animationFrame" style="max-width: 100%; height: 700px;" src="{frames[0]}">
|
| 396 |
-
<p id="dateDisplay" style="font-size: 18px; margin: 10px;">{dates[0]}</p>
|
| 397 |
-
<button id="startBtn" style="margin: 5px;">Start</button>
|
| 398 |
-
<button id="pauseBtn" style="margin: 5px;">Pause</button>
|
| 399 |
-
</div>
|
| 400 |
-
<script>
|
| 401 |
-
const frames = {frames_json};
|
| 402 |
-
const dates = {dates_json};
|
| 403 |
-
let currentFrame = 0;
|
| 404 |
-
let intervalId = null;
|
| 405 |
-
|
| 406 |
-
function updateFrame() {{
|
| 407 |
-
document.getElementById('animationFrame').src = frames[currentFrame];
|
| 408 |
-
document.getElementById('dateDisplay').textContent = dates[currentFrame];
|
| 409 |
-
currentFrame = (currentFrame + 1) % frames.length;
|
| 410 |
-
}}
|
| 411 |
-
|
| 412 |
-
document.getElementById('startBtn').addEventListener('click', function() {{
|
| 413 |
-
if (!intervalId) {{
|
| 414 |
-
intervalId = setInterval(updateFrame, 200); // 200ms per frame
|
| 415 |
-
}}
|
| 416 |
-
}});
|
| 417 |
-
|
| 418 |
-
document.getElementById('pauseBtn').addEventListener('click', function() {{
|
| 419 |
-
if (intervalId) {{
|
| 420 |
-
clearInterval(intervalId);
|
| 421 |
-
intervalId = null;
|
| 422 |
-
}}
|
| 423 |
-
}});
|
| 424 |
-
</script>
|
| 425 |
-
""".format(
|
| 426 |
-
frames_json=str(frames).replace("'", '"'), # JSON-safe string
|
| 427 |
-
dates_json=str(dates).replace("'", '"'),
|
| 428 |
-
frames=frames[0], # Initial frame
|
| 429 |
-
dates=dates[0] # Initial date
|
| 430 |
-
)
|
| 431 |
-
|
| 432 |
-
return js_script
|
| 433 |
|
| 434 |
# Logistic regression functions
|
| 435 |
def perform_wind_regression(start_year, start_month, end_year, end_month):
|
|
@@ -478,7 +435,7 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
|
| 478 |
### Features:
|
| 479 |
- **Track Visualization**: View typhoon tracks by time period and ENSO phase
|
| 480 |
- **Statistical Analysis**: Examine relationships between ONI values and typhoon characteristics
|
| 481 |
-
- **Path Animation**: Watch an animated typhoon path with
|
| 482 |
- **Regression Analysis**: Perform statistical regression on typhoon data
|
| 483 |
|
| 484 |
Select a tab above to begin your analysis.
|
|
@@ -639,15 +596,14 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
|
| 639 |
standard_dropdown = gr.Dropdown(label="Classification Standard", choices=['atlantic', 'taiwan'], value='atlantic')
|
| 640 |
|
| 641 |
animate_btn = gr.Button("Generate Animation")
|
| 642 |
-
|
| 643 |
animation_info = gr.Markdown("""
|
| 644 |
### Animation Instructions
|
| 645 |
1. Select a year and typhoon from the dropdowns
|
| 646 |
2. Choose a classification standard (Atlantic or Taiwan)
|
| 647 |
3. Click "Generate Animation"
|
| 648 |
-
4. Use the
|
| 649 |
-
5. The date
|
| 650 |
-
6. The blue line grows to show the typhoon's path, with markers indicating intensity
|
| 651 |
""")
|
| 652 |
|
| 653 |
def update_typhoon_options(year):
|
|
@@ -658,9 +614,25 @@ with gr.Blocks(title="Typhoon Analysis Dashboard") as demo:
|
|
| 658 |
|
| 659 |
year_dropdown.change(fn=update_typhoon_options, inputs=year_dropdown, outputs=typhoon_dropdown)
|
| 660 |
animate_btn.click(
|
| 661 |
-
fn=
|
| 662 |
inputs=[year_dropdown, typhoon_dropdown, standard_dropdown],
|
| 663 |
-
outputs=
|
| 664 |
)
|
| 665 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 666 |
demo.launch(share=True)
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
import pandas as pd
|
| 3 |
import numpy as np
|
| 4 |
+
import matplotlib.pyplot as plt
|
| 5 |
+
import matplotlib.animation as animation
|
| 6 |
+
from matplotlib.backends.backend_agg import FigureCanvasAgg as FigureCanvas
|
| 7 |
import plotly.graph_objects as go
|
| 8 |
import plotly.express as px
|
| 9 |
import tropycal.tracks as tracks
|
|
|
|
| 18 |
import csv
|
| 19 |
from collections import defaultdict
|
| 20 |
import filecmp
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
# Command-line argument parsing
|
| 23 |
parser = argparse.ArgumentParser(description='Typhoon Analysis Dashboard')
|
|
|
|
| 32 |
CACHE_FILE = 'ibtracs_cache.pkl'
|
| 33 |
CACHE_EXPIRY_DAYS = 1
|
| 34 |
|
| 35 |
+
# Color maps for Plotly (RGB)
|
| 36 |
color_map = {
|
| 37 |
'C5 Super Typhoon': 'rgb(255, 0, 0)',
|
| 38 |
'C4 Very Strong Typhoon': 'rgb(255, 63, 0)',
|
|
|
|
| 43 |
'Tropical Depression': 'rgb(173, 216, 230)'
|
| 44 |
}
|
| 45 |
|
| 46 |
+
# Classification standards for both Plotly and Matplotlib (HEX for Matplotlib)
|
| 47 |
atlantic_standard = {
|
| 48 |
+
'C5 Super Typhoon': {'wind_speed': 137, 'color': '#FF0000'},
|
| 49 |
+
'C4 Very Strong Typhoon': {'wind_speed': 113, 'color': '#FF3F00'},
|
| 50 |
+
'C3 Strong Typhoon': {'wind_speed': 96, 'color': '#FF7F00'},
|
| 51 |
+
'C2 Typhoon': {'wind_speed': 83, 'color': '#FFBF00'},
|
| 52 |
+
'C1 Typhoon': {'wind_speed': 64, 'color': '#FFFF00'},
|
| 53 |
+
'Tropical Storm': {'wind_speed': 34, 'color': '#00FFFF'},
|
| 54 |
+
'Tropical Depression': {'wind_speed': 0, 'color': '#ADD8E6'}
|
| 55 |
}
|
| 56 |
|
| 57 |
taiwan_standard = {
|
| 58 |
+
'Strong Typhoon': {'wind_speed': 51.0, 'color': '#FF0000'},
|
| 59 |
+
'Medium Typhoon': {'wind_speed': 33.7, 'color': '#FF7F00'},
|
| 60 |
+
'Mild Typhoon': {'wind_speed': 17.2, 'color': '#FFFF00'},
|
| 61 |
+
'Tropical Depression': {'wind_speed': 0, 'color': '#ADD8E6'}
|
| 62 |
}
|
| 63 |
|
| 64 |
# Data loading and preprocessing functions
|
|
|
|
| 212 |
typhoon_max = process_typhoon_data(typhoon_data)
|
| 213 |
merged_data = merge_data(oni_long, typhoon_max)
|
| 214 |
|
| 215 |
+
# Main analysis functions (using Plotly)
|
| 216 |
def generate_typhoon_tracks(filtered_data, typhoon_search):
|
| 217 |
fig = go.Figure()
|
| 218 |
for sid in filtered_data['SID'].unique():
|
|
|
|
| 230 |
lon=storm_data['LON'], lat=storm_data['LAT'], mode='lines',
|
| 231 |
name=f'Matched: {typhoon_search}', line=dict(width=5, color='yellow')
|
| 232 |
))
|
| 233 |
+
fig.update_layout(
|
| 234 |
+
title='Typhoon Tracks',
|
| 235 |
+
geo=dict(projection_type='natural earth', showland=True),
|
| 236 |
+
height=700
|
| 237 |
+
)
|
| 238 |
return fig
|
| 239 |
|
| 240 |
def generate_wind_oni_scatter(filtered_data, typhoon_search):
|
|
|
|
| 300 |
|
| 301 |
return tracks_fig, wind_scatter, pressure_scatter, regression_fig, slopes_text
|
| 302 |
|
| 303 |
+
# Video animation function (using Matplotlib)
|
| 304 |
def categorize_typhoon_by_standard(wind_speed, standard):
|
| 305 |
if standard == 'taiwan':
|
| 306 |
wind_speed_ms = wind_speed * 0.514444
|
|
|
|
| 326 |
return 'Tropical Storm', atlantic_standard['Tropical Storm']['color']
|
| 327 |
return 'Tropical Depression', atlantic_standard['Tropical Depression']['color']
|
| 328 |
|
| 329 |
+
def generate_track_video(year, typhoon, standard):
|
| 330 |
if not typhoon:
|
| 331 |
+
return None
|
| 332 |
|
| 333 |
typhoon_id = typhoon.split('(')[-1].strip(')')
|
| 334 |
storm = ibtracs.get_storm(typhoon_id)
|
|
|
|
| 339 |
lat_padding = max((max_lat - min_lat) * 0.3, 5)
|
| 340 |
lon_padding = max((max_lon - min_lon) * 0.3, 5)
|
| 341 |
|
| 342 |
+
# Set up the figure
|
| 343 |
+
fig, ax = plt.subplots(figsize=(10, 7))
|
| 344 |
+
ax.set_xlim(min_lon - lon_padding, max_lon + lon_padding)
|
| 345 |
+
ax.set_ylim(min_lat - lat_padding, max_lat + lat_padding)
|
| 346 |
+
ax.set_xlabel('Longitude')
|
| 347 |
+
ax.set_ylabel('Latitude')
|
| 348 |
+
ax.set_title(f"{year} {storm.name} Typhoon Path")
|
| 349 |
+
ax.grid(True)
|
| 350 |
+
|
| 351 |
+
# Plot background (simple map-like grid)
|
| 352 |
+
ax.plot([], [], 'b-', label='Track') # Placeholder for legend
|
| 353 |
+
|
| 354 |
+
# Legend for categories
|
| 355 |
+
standard_dict = atlantic_standard if standard == 'atlantic' else taiwan_standard
|
| 356 |
+
for cat, details in standard_dict.items():
|
| 357 |
+
ax.plot([], [], 'o', color=details['color'], label=cat)
|
| 358 |
+
ax.legend(loc='upper left', bbox_to_anchor=(1, 1))
|
| 359 |
+
|
| 360 |
+
# Initialize the line and point
|
| 361 |
+
line, = ax.plot([], [], 'b-', linewidth=2)
|
| 362 |
+
point, = ax.plot([], [], 'o', markersize=8)
|
| 363 |
+
date_text = ax.text(0.02, 0.02, '', transform=ax.transAxes, fontsize=12)
|
| 364 |
+
|
| 365 |
+
def init():
|
| 366 |
+
line.set_data([], [])
|
| 367 |
+
point.set_data([], [])
|
| 368 |
+
date_text.set_text('')
|
| 369 |
+
return line, point, date_text
|
| 370 |
+
|
| 371 |
+
def update(frame):
|
| 372 |
+
line.set_data(storm.lon[:frame+1], storm.lat[:frame+1])
|
| 373 |
+
category, color = categorize_typhoon_by_standard(storm.vmax[frame], standard)
|
| 374 |
+
point.set_data([storm.lon[frame]], [storm.lat[frame]])
|
| 375 |
+
point.set_color(color)
|
| 376 |
+
date_text.set_text(storm.time[frame].strftime('%Y-%m-%d %H:%M'))
|
| 377 |
+
return line, point, date_text
|
| 378 |
+
|
| 379 |
+
# Create animation
|
| 380 |
+
ani = animation.FuncAnimation(fig, update, init_func=init, frames=len(storm.time),
|
| 381 |
+
interval=200, blit=True, repeat=True)
|
| 382 |
+
|
| 383 |
+
# Save as video
|
| 384 |
+
temp_file = tempfile.NamedTemporaryFile(delete=False, suffix='.mp4')
|
| 385 |
+
writer = animation.FFMpegWriter(fps=5, bitrate=1800)
|
| 386 |
+
ani.save(temp_file.name, writer=writer)
|
| 387 |
+
plt.close(fig)
|
| 388 |
+
|
| 389 |
+
return temp_file.name
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 390 |
|
| 391 |
# Logistic regression functions
|
| 392 |
def perform_wind_regression(start_year, start_month, end_year, end_month):
|
|
|
|
| 435 |
### Features:
|
| 436 |
- **Track Visualization**: View typhoon tracks by time period and ENSO phase
|
| 437 |
- **Statistical Analysis**: Examine relationships between ONI values and typhoon characteristics
|
| 438 |
+
- **Path Animation**: Watch an animated typhoon path with video controls
|
| 439 |
- **Regression Analysis**: Perform statistical regression on typhoon data
|
| 440 |
|
| 441 |
Select a tab above to begin your analysis.
|
|
|
|
| 596 |
standard_dropdown = gr.Dropdown(label="Classification Standard", choices=['atlantic', 'taiwan'], value='atlantic')
|
| 597 |
|
| 598 |
animate_btn = gr.Button("Generate Animation")
|
| 599 |
+
path_video = gr.Video(label="Typhoon Path Animation", elem_id="path_video")
|
| 600 |
animation_info = gr.Markdown("""
|
| 601 |
### Animation Instructions
|
| 602 |
1. Select a year and typhoon from the dropdowns
|
| 603 |
2. Choose a classification standard (Atlantic or Taiwan)
|
| 604 |
3. Click "Generate Animation"
|
| 605 |
+
4. Use the video player's built-in controls to play, pause, or scrub through the animation
|
| 606 |
+
5. The date is displayed on each frame, and the track grows with intensity markers
|
|
|
|
| 607 |
""")
|
| 608 |
|
| 609 |
def update_typhoon_options(year):
|
|
|
|
| 614 |
|
| 615 |
year_dropdown.change(fn=update_typhoon_options, inputs=year_dropdown, outputs=typhoon_dropdown)
|
| 616 |
animate_btn.click(
|
| 617 |
+
fn=generate_track_video,
|
| 618 |
inputs=[year_dropdown, typhoon_dropdown, standard_dropdown],
|
| 619 |
+
outputs=path_video
|
| 620 |
)
|
| 621 |
|
| 622 |
+
# Custom CSS for better visibility
|
| 623 |
+
gr.HTML("""
|
| 624 |
+
<style>
|
| 625 |
+
#tracks_plot, #path_video {
|
| 626 |
+
height: 700px !important;
|
| 627 |
+
width: 100%;
|
| 628 |
+
}
|
| 629 |
+
.plot-container {
|
| 630 |
+
min-height: 600px;
|
| 631 |
+
}
|
| 632 |
+
.gr-plotly {
|
| 633 |
+
width: 100% !important;
|
| 634 |
+
}
|
| 635 |
+
</style>
|
| 636 |
+
""")
|
| 637 |
+
|
| 638 |
demo.launch(share=True)
|