Spaces:
Running
Running
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)
|