euler314 commited on
Commit
3c5b4c7
verified
1 Parent(s): 4a85449

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +304 -109
app.py CHANGED
@@ -1145,7 +1145,8 @@ def calculate_track_uncertainty(track_points):
1145
 
1146
  def create_genesis_animation(prediction_data, enable_animation=True):
1147
  """
1148
- Create animation showing daily genesis potential and storm development
 
1149
  """
1150
  try:
1151
  if 'daily_gpi_maps' not in prediction_data or not prediction_data['daily_gpi_maps']:
@@ -1155,105 +1156,262 @@ def create_genesis_animation(prediction_data, enable_animation=True):
1155
  storm_predictions = prediction_data.get('storm_predictions', [])
1156
  month = prediction_data['month']
1157
  oni_value = prediction_data['oni_value']
 
1158
 
 
1159
  fig = go.Figure()
1160
 
1161
  if enable_animation and len(daily_maps) > 1:
1162
- # Create animated version
1163
  frames = []
1164
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1165
  for day_idx, day_data in enumerate(daily_maps):
1166
  day = day_data['day']
 
 
 
1167
  gpi_field = day_data['gpi_field']
1168
  lat_range = day_data['lat_range']
1169
  lon_range = day_data['lon_range']
1170
 
1171
- frame_data = []
1172
-
1173
- # Add GPI contour
1174
  frame_data.append(
1175
- go.Contour(
1176
- z=gpi_field,
1177
- x=lon_range,
1178
- y=lat_range,
1179
- colorscale='Viridis',
1180
- showscale=True,
1181
- colorbar=dict(title="Genesis Potential Index"),
1182
- name=f'GPI Day {day}',
 
 
 
 
 
 
 
 
 
 
 
1183
  hovertemplate=(
1184
- 'Longitude: %{x:.1f}掳E<br>'
1185
- 'Latitude: %{y:.1f}掳N<br>'
1186
- 'GPI: %{z:.2f}<br>'
1187
- f'Day: {day}<br>'
 
1188
  '<extra></extra>'
1189
  )
1190
  )
1191
  )
1192
 
1193
- # Add storm tracks up to current day
1194
- for storm in storm_predictions:
1195
  track = storm['track']
1196
- # Only show track points up to current day
 
 
1197
  current_track = [pt for pt in track if pt['day'] <= day]
1198
 
1199
- if current_track:
1200
  lats = [pt['lat'] for pt in current_track]
1201
  lons = [pt['lon'] for pt in current_track]
1202
  intensities = [pt['intensity'] for pt in current_track]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1203
 
1204
- # Storm track
1205
  frame_data.append(
1206
- go.Scatter(
1207
- x=lons,
1208
- y=lats,
1209
- mode='lines+markers',
1210
- line=dict(color='red', width=3),
1211
  marker=dict(
1212
- size=[max(4, int/10) for int in intensities],
1213
- color='red'
 
 
 
1214
  ),
1215
- name=f'Storm {storm["storm_id"]}',
 
 
1216
  hovertemplate=(
1217
- f'Storm {storm["storm_id"]}<br>'
1218
- 'Longitude: %{x:.1f}掳E<br>'
1219
- 'Latitude: %{y:.1f}掳N<br>'
 
 
 
 
1220
  '<extra></extra>'
1221
- ),
1222
- showlegend=(day_idx == 0)
1223
  )
1224
  )
1225
 
1226
- # Current position marker
1227
- if current_track:
1228
- current_pos = current_track[-1]
 
 
 
 
 
 
 
 
 
 
1229
  frame_data.append(
1230
- go.Scatter(
1231
- x=[current_pos['lon']],
1232
- y=[current_pos['lat']],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1233
  mode='markers',
1234
  marker=dict(
1235
- size=15,
1236
- color='yellow',
1237
  symbol='star',
1238
  line=dict(width=2, color='black')
1239
  ),
1240
- name=f'Storm {storm["storm_id"]} Current',
1241
- showlegend=False,
 
1242
  hovertemplate=(
1243
- f'Storm {storm["storm_id"]} - Day {day:.1f}<br>'
1244
- f'Intensity: {current_pos["intensity"]:.0f} kt<br>'
1245
- f'Category: {current_pos["category"]}<br>'
1246
- f'Pressure: {current_pos["pressure"]:.0f} hPa<br>'
1247
  '<extra></extra>'
1248
  )
1249
  )
1250
  )
1251
 
 
1252
  frames.append(go.Frame(
1253
  data=frame_data,
1254
  name=str(day),
1255
  layout=go.Layout(
1256
- title=f"Genesis Potential & Storm Development - Month {month}, Day {day}<br>ONI: {oni_value:.2f}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1257
  )
1258
  ))
1259
 
@@ -1262,30 +1420,46 @@ def create_genesis_animation(prediction_data, enable_animation=True):
1262
  # Set initial frame
1263
  if frames:
1264
  fig.add_traces(frames[0].data)
 
1265
 
1266
- # Add animation controls
1267
  fig.update_layout(
1268
  updatemenus=[{
1269
  "buttons": [
1270
  {
1271
- "args": [None, {"frame": {"duration": 1000, "redraw": True},
1272
- "fromcurrent": True, "transition": {"duration": 300}}],
1273
- "label": "鈻讹笍 Play",
 
 
 
1274
  "method": "animate"
1275
  },
1276
  {
1277
- "args": [[None], {"frame": {"duration": 0, "redraw": True},
1278
- "mode": "immediate", "transition": {"duration": 0}}],
 
 
 
1279
  "label": "鈴革笍 Pause",
1280
  "method": "animate"
 
 
 
 
 
 
 
 
 
1281
  }
1282
  ],
1283
  "direction": "left",
1284
- "pad": {"r": 10, "t": 87},
1285
  "showactive": False,
1286
  "type": "buttons",
1287
  "x": 0.1,
1288
- "xanchor": "right",
1289
  "y": 0,
1290
  "yanchor": "top"
1291
  }],
@@ -1294,8 +1468,8 @@ def create_genesis_animation(prediction_data, enable_animation=True):
1294
  "yanchor": "top",
1295
  "xanchor": "left",
1296
  "currentvalue": {
1297
- "font": {"size": 16},
1298
- "prefix": "Day: ",
1299
  "visible": True,
1300
  "xanchor": "right"
1301
  },
@@ -1306,39 +1480,41 @@ def create_genesis_animation(prediction_data, enable_animation=True):
1306
  "y": 0,
1307
  "steps": [
1308
  {
1309
- "args": [[str(day_data['day'])], {"frame": {"duration": 300, "redraw": True},
1310
- "mode": "immediate", "transition": {"duration": 300}}],
1311
- "label": f"D{day_data['day']}",
 
 
 
1312
  "method": "animate"
1313
  }
1314
- for day_data in daily_maps[::max(1, len(daily_maps)//15)]
1315
  ]
1316
  }]
1317
  )
1318
 
1319
  else:
1320
- # Static version showing final day
1321
  final_day = daily_maps[-1]
1322
  gpi_field = final_day['gpi_field']
1323
  lat_range = final_day['lat_range']
1324
  lon_range = final_day['lon_range']
1325
 
1326
- # Add GPI contour
1327
  fig.add_trace(
1328
- go.Contour(
1329
- z=gpi_field,
1330
- x=lon_range,
1331
- y=lat_range,
1332
- colorscale='Viridis',
1333
- showscale=True,
1334
- colorbar=dict(title="Genesis Potential Index"),
1335
- name='Genesis Potential',
1336
- hovertemplate=(
1337
- 'Longitude: %{x:.1f}掳E<br>'
1338
- 'Latitude: %{y:.1f}掳N<br>'
1339
- 'GPI: %{z:.2f}<br>'
1340
- '<extra></extra>'
1341
- )
1342
  )
1343
  )
1344
 
@@ -1350,37 +1526,32 @@ def create_genesis_animation(prediction_data, enable_animation=True):
1350
  lons = [pt['lon'] for pt in track]
1351
  intensities = [pt['intensity'] for pt in track]
1352
 
 
1353
  fig.add_trace(
1354
- go.Scatter(
1355
- x=lons,
1356
- y=lats,
1357
  mode='lines+markers',
1358
  line=dict(color='red', width=3),
1359
  marker=dict(
1360
- size=[max(4, int/10) for int in intensities],
1361
  color=intensities,
1362
  colorscale='Reds',
1363
  showscale=False
1364
  ),
1365
- name=f'Storm {storm["storm_id"]}',
1366
- hovertemplate=(
1367
- f'Storm {storm["storm_id"]}<br>'
1368
- 'Longitude: %{x:.1f}掳E<br>'
1369
- 'Latitude: %{y:.1f}掳N<br>'
1370
- '<extra></extra>'
1371
- )
1372
  )
1373
  )
1374
 
1375
  # Genesis marker
1376
  fig.add_trace(
1377
- go.Scatter(
1378
- x=[lons[0]],
1379
- y=[lats[0]],
1380
  mode='markers',
1381
  marker=dict(
1382
- size=20,
1383
- color='yellow',
1384
  symbol='star',
1385
  line=dict(width=2, color='black')
1386
  ),
@@ -1389,25 +1560,49 @@ def create_genesis_animation(prediction_data, enable_animation=True):
1389
  )
1390
  )
1391
 
1392
- # Update layout
1393
  fig.update_layout(
1394
- title=f"Typhoon Genesis Prediction - Month {month} (ONI: {oni_value:.2f})" +
1395
- (" - Animated" if enable_animation else ""),
1396
- xaxis_title="Longitude (掳E)",
1397
- yaxis_title="Latitude (掳N)",
1398
- width=1000,
1399
- height=700,
1400
- showlegend=True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1401
  )
1402
 
1403
- # Set map bounds
1404
- fig.update_xaxes(range=[110, 180])
1405
- fig.update_yaxes(range=[5, 35])
1406
-
1407
  return fig
1408
 
1409
  except Exception as e:
1410
- logging.error(f"Error creating genesis animation: {e}")
 
 
1411
  return create_error_plot(f"Animation error: {str(e)}")
1412
 
1413
  def create_error_plot(error_message):
 
1145
 
1146
  def create_genesis_animation(prediction_data, enable_animation=True):
1147
  """
1148
+ Create professional typhoon track animation showing daily genesis potential and storm development
1149
+ Following NHC/JTWC visualization standards with proper geographic map and time controls
1150
  """
1151
  try:
1152
  if 'daily_gpi_maps' not in prediction_data or not prediction_data['daily_gpi_maps']:
 
1156
  storm_predictions = prediction_data.get('storm_predictions', [])
1157
  month = prediction_data['month']
1158
  oni_value = prediction_data['oni_value']
1159
+ year = prediction_data.get('year', 2025)
1160
 
1161
+ # Create base figure with geographic map
1162
  fig = go.Figure()
1163
 
1164
  if enable_animation and len(daily_maps) > 1:
1165
+ # PROFESSIONAL ANIMATION VERSION
1166
  frames = []
1167
 
1168
+ # Get all storm track data for proper bounds
1169
+ all_lats, all_lons = [], []
1170
+ for storm in storm_predictions:
1171
+ track = storm.get('track', [])
1172
+ if track:
1173
+ all_lats.extend([pt['lat'] for pt in track])
1174
+ all_lons.extend([pt['lon'] for pt in track])
1175
+
1176
+ # Set map bounds (Western Pacific focus)
1177
+ map_bounds = {
1178
+ 'lat_min': min(5, min(all_lats) - 5) if all_lats else 5,
1179
+ 'lat_max': max(35, max(all_lats) + 5) if all_lats else 35,
1180
+ 'lon_min': min(110, min(all_lons) - 10) if all_lons else 110,
1181
+ 'lon_max': max(180, max(all_lons) + 10) if all_lons else 180
1182
+ }
1183
+
1184
+ # Create frames for each day
1185
  for day_idx, day_data in enumerate(daily_maps):
1186
  day = day_data['day']
1187
+ frame_data = []
1188
+
1189
+ # 1. GENESIS POTENTIAL BACKGROUND (GPI Contours)
1190
  gpi_field = day_data['gpi_field']
1191
  lat_range = day_data['lat_range']
1192
  lon_range = day_data['lon_range']
1193
 
 
 
 
1194
  frame_data.append(
1195
+ go.Scattergeo(
1196
+ lat=np.repeat(lat_range, len(lon_range)),
1197
+ lon=np.tile(lon_range, len(lat_range)),
1198
+ mode='markers',
1199
+ marker=dict(
1200
+ size=6,
1201
+ color=gpi_field.flatten(),
1202
+ colorscale='Viridis',
1203
+ cmin=0,
1204
+ cmax=3,
1205
+ opacity=0.6,
1206
+ showscale=(day_idx == 0),
1207
+ colorbar=dict(
1208
+ title="Genesis<br>Potential<br>Index",
1209
+ titleside="right"
1210
+ ) if day_idx == 0 else None
1211
+ ),
1212
+ name='Genesis Potential',
1213
+ showlegend=(day_idx == 0),
1214
  hovertemplate=(
1215
+ 'Genesis Potential<br>'
1216
+ 'Lat: %{lat:.1f}掳N<br>'
1217
+ 'Lon: %{lon:.1f}掳E<br>'
1218
+ 'GPI: %{marker.color:.2f}<br>'
1219
+ f'Day {day} of {month:02d}/{year}<br>'
1220
  '<extra></extra>'
1221
  )
1222
  )
1223
  )
1224
 
1225
+ # 2. STORM TRACKS (Progressive reveal like NHC)
1226
+ for storm_idx, storm in enumerate(storm_predictions):
1227
  track = storm['track']
1228
+ storm_id = storm['storm_id']
1229
+
1230
+ # Get track points up to current day
1231
  current_track = [pt for pt in track if pt['day'] <= day]
1232
 
1233
+ if current_track and len(current_track) > 0:
1234
  lats = [pt['lat'] for pt in current_track]
1235
  lons = [pt['lon'] for pt in current_track]
1236
  intensities = [pt['intensity'] for pt in current_track]
1237
+ categories = [pt['category'] for pt in current_track]
1238
+
1239
+ # Get colors based on intensity (Saffir-Simpson-like scale)
1240
+ colors = []
1241
+ for intensity in intensities:
1242
+ if intensity < 34:
1243
+ colors.append('#80808080') # Gray - TD
1244
+ elif intensity < 64:
1245
+ colors.append('#0000FF80') # Blue - TS
1246
+ elif intensity < 83:
1247
+ colors.append('#00FFFF80') # Cyan - C1
1248
+ elif intensity < 96:
1249
+ colors.append('#00FF0080') # Green - C2
1250
+ elif intensity < 113:
1251
+ colors.append('#FFFF0080') # Yellow - C3
1252
+ elif intensity < 137:
1253
+ colors.append('#FFA50080') # Orange - C4
1254
+ else:
1255
+ colors.append('#FF000080') # Red - C5
1256
+
1257
+ # 3. HISTORICAL TRACK (Past movement)
1258
+ if len(current_track) > 1:
1259
+ frame_data.append(
1260
+ go.Scattergeo(
1261
+ lat=lats[:-1], # All except current position
1262
+ lon=lons[:-1],
1263
+ mode='lines+markers',
1264
+ line=dict(
1265
+ color='rgba(100,100,100,0.7)',
1266
+ width=3
1267
+ ),
1268
+ marker=dict(
1269
+ size=[max(6, int/8) for int in intensities[:-1]],
1270
+ color=colors[:-1],
1271
+ line=dict(width=1, color='white')
1272
+ ),
1273
+ name=f'Storm {storm_id} Track',
1274
+ showlegend=(storm_idx == 0 and day_idx == 0),
1275
+ legendgroup=f'storm_{storm_id}',
1276
+ hovertemplate=(
1277
+ f'<b>Storm {storm_id} History</b><br>'
1278
+ 'Lat: %{lat:.1f}掳N<br>'
1279
+ 'Lon: %{lon:.1f}掳E<br>'
1280
+ '<extra></extra>'
1281
+ )
1282
+ )
1283
+ )
1284
+
1285
+ # 4. CURRENT POSITION (Big marker like operational forecasts)
1286
+ current_pos = current_track[-1]
1287
+ current_intensity = current_pos['intensity']
1288
+ current_category = current_pos['category']
1289
+
1290
+ # Get current position color
1291
+ if current_intensity < 34:
1292
+ current_color = '#808080' # Gray - TD
1293
+ elif current_intensity < 64:
1294
+ current_color = '#0000FF' # Blue - TS
1295
+ elif current_intensity < 83:
1296
+ current_color = '#00FFFF' # Cyan - C1
1297
+ elif current_intensity < 96:
1298
+ current_color = '#00FF00' # Green - C2
1299
+ elif current_intensity < 113:
1300
+ current_color = '#FFFF00' # Yellow - C3
1301
+ elif current_intensity < 137:
1302
+ current_color = '#FFA500' # Orange - C4
1303
+ else:
1304
+ current_color = '#FF0000' # Red - C5
1305
 
 
1306
  frame_data.append(
1307
+ go.Scattergeo(
1308
+ lat=[current_pos['lat']],
1309
+ lon=[current_pos['lon']],
1310
+ mode='markers',
 
1311
  marker=dict(
1312
+ size=max(15, current_intensity/4),
1313
+ color=current_color,
1314
+ symbol='circle',
1315
+ line=dict(width=3, color='white'),
1316
+ opacity=1.0
1317
  ),
1318
+ name=f'Storm {storm_id} Current',
1319
+ showlegend=(storm_idx == 0 and day_idx == 0),
1320
+ legendgroup=f'storm_{storm_id}',
1321
  hovertemplate=(
1322
+ f'<b>STORM {storm_id}</b><br>'
1323
+ f'Day {day:.1f} of {month:02d}/{year}<br>'
1324
+ f'Position: %{{lat:.1f}}掳N, %{{lon:.1f}}掳E<br>'
1325
+ f'Intensity: {current_intensity:.0f} kt<br>'
1326
+ f'Category: {current_category}<br>'
1327
+ f'Pressure: {current_pos["pressure"]:.0f} hPa<br>'
1328
+ f'Uncertainty: 卤{current_pos["position_uncertainty"]:.1f}掳<br>'
1329
  '<extra></extra>'
1330
+ )
 
1331
  )
1332
  )
1333
 
1334
+ # 5. UNCERTAINTY CONE (Growing with time like NHC)
1335
+ if len(current_track) > 1:
1336
+ uncertainty = current_pos['position_uncertainty']
1337
+ center_lat = current_pos['lat']
1338
+ center_lon = current_pos['lon']
1339
+
1340
+ # Create uncertainty circle
1341
+ circle_lats = []
1342
+ circle_lons = []
1343
+ for angle in np.linspace(0, 2*np.pi, 20):
1344
+ circle_lats.append(center_lat + uncertainty * np.cos(angle))
1345
+ circle_lons.append(center_lon + uncertainty * np.sin(angle))
1346
+
1347
  frame_data.append(
1348
+ go.Scattergeo(
1349
+ lat=circle_lats,
1350
+ lon=circle_lons,
1351
+ mode='lines',
1352
+ line=dict(
1353
+ color='rgba(255,0,0,0.3)',
1354
+ width=2,
1355
+ dash='dash'
1356
+ ),
1357
+ fill='toself',
1358
+ fillcolor='rgba(255,0,0,0.1)',
1359
+ name=f'Storm {storm_id} Uncertainty',
1360
+ showlegend=(storm_idx == 0 and day_idx == 0),
1361
+ legendgroup=f'storm_{storm_id}',
1362
+ hoverinfo='skip'
1363
+ )
1364
+ )
1365
+
1366
+ # 6. GENESIS MARKER (Show where storm formed)
1367
+ if len(current_track) >= 1:
1368
+ genesis_pos = track[0] # First position
1369
+ frame_data.append(
1370
+ go.Scattergeo(
1371
+ lat=[genesis_pos['lat']],
1372
+ lon=[genesis_pos['lon']],
1373
  mode='markers',
1374
  marker=dict(
1375
+ size=12,
1376
+ color='gold',
1377
  symbol='star',
1378
  line=dict(width=2, color='black')
1379
  ),
1380
+ name=f'Storm {storm_id} Genesis',
1381
+ showlegend=(storm_idx == 0 and day_idx == 0),
1382
+ legendgroup=f'storm_{storm_id}',
1383
  hovertemplate=(
1384
+ f'<b>GENESIS - Storm {storm_id}</b><br>'
1385
+ f'Day {genesis_pos["day"]:.1f}<br>'
1386
+ f'Location: %{{lat:.1f}}掳N, %{{lon:.1f}}掳E<br>'
1387
+ f'Initial Intensity: {genesis_pos["intensity"]:.0f} kt<br>'
1388
  '<extra></extra>'
1389
  )
1390
  )
1391
  )
1392
 
1393
+ # Create frame
1394
  frames.append(go.Frame(
1395
  data=frame_data,
1396
  name=str(day),
1397
  layout=go.Layout(
1398
+ title=f"Typhoon Development Forecast - Day {day}, {month:02d}/{year} | ONI: {oni_value:.2f}",
1399
+ geo=dict(
1400
+ projection_type="natural earth",
1401
+ showland=True,
1402
+ landcolor="lightgray",
1403
+ showocean=True,
1404
+ oceancolor="lightblue",
1405
+ showcoastlines=True,
1406
+ coastlinecolor="darkgray",
1407
+ center=dict(
1408
+ lat=(map_bounds['lat_min'] + map_bounds['lat_max']) / 2,
1409
+ lon=(map_bounds['lon_min'] + map_bounds['lon_max']) / 2
1410
+ ),
1411
+ lonaxis_range=[map_bounds['lon_min'], map_bounds['lon_max']],
1412
+ lataxis_range=[map_bounds['lat_min'], map_bounds['lat_max']],
1413
+ resolution=50
1414
+ )
1415
  )
1416
  ))
1417
 
 
1420
  # Set initial frame
1421
  if frames:
1422
  fig.add_traces(frames[0].data)
1423
+ fig.update_layout(frames[0].layout)
1424
 
1425
+ # PROFESSIONAL ANIMATION CONTROLS (like NHC website)
1426
  fig.update_layout(
1427
  updatemenus=[{
1428
  "buttons": [
1429
  {
1430
+ "args": [None, {
1431
+ "frame": {"duration": 800, "redraw": True},
1432
+ "fromcurrent": True,
1433
+ "transition": {"duration": 300, "easing": "cubic-in-out"}
1434
+ }],
1435
+ "label": "鈻讹笍 Play Animation",
1436
  "method": "animate"
1437
  },
1438
  {
1439
+ "args": [[None], {
1440
+ "frame": {"duration": 0, "redraw": True},
1441
+ "mode": "immediate",
1442
+ "transition": {"duration": 0}
1443
+ }],
1444
  "label": "鈴革笍 Pause",
1445
  "method": "animate"
1446
+ },
1447
+ {
1448
+ "args": [[frames[0].name], {
1449
+ "frame": {"duration": 0, "redraw": True},
1450
+ "mode": "immediate",
1451
+ "transition": {"duration": 0}
1452
+ }],
1453
+ "label": "鈴笍 Reset",
1454
+ "method": "animate"
1455
  }
1456
  ],
1457
  "direction": "left",
1458
+ "pad": {"r": 10, "t": 120},
1459
  "showactive": False,
1460
  "type": "buttons",
1461
  "x": 0.1,
1462
+ "xanchor": "right",
1463
  "y": 0,
1464
  "yanchor": "top"
1465
  }],
 
1468
  "yanchor": "top",
1469
  "xanchor": "left",
1470
  "currentvalue": {
1471
+ "font": {"size": 16, "color": "darkblue"},
1472
+ "prefix": f"{month:02d}/{year} Day: ",
1473
  "visible": True,
1474
  "xanchor": "right"
1475
  },
 
1480
  "y": 0,
1481
  "steps": [
1482
  {
1483
+ "args": [[str(day_data['day'])], {
1484
+ "frame": {"duration": 300, "redraw": True},
1485
+ "mode": "immediate",
1486
+ "transition": {"duration": 300}
1487
+ }],
1488
+ "label": f"Day {day_data['day']}",
1489
  "method": "animate"
1490
  }
1491
+ for day_data in daily_maps
1492
  ]
1493
  }]
1494
  )
1495
 
1496
  else:
1497
+ # STATIC VERSION (Final state overview)
1498
  final_day = daily_maps[-1]
1499
  gpi_field = final_day['gpi_field']
1500
  lat_range = final_day['lat_range']
1501
  lon_range = final_day['lon_range']
1502
 
1503
+ # Add GPI background
1504
  fig.add_trace(
1505
+ go.Scattergeo(
1506
+ lat=np.repeat(lat_range, len(lon_range)),
1507
+ lon=np.tile(lon_range, len(lat_range)),
1508
+ mode='markers',
1509
+ marker=dict(
1510
+ size=6,
1511
+ color=gpi_field.flatten(),
1512
+ colorscale='Viridis',
1513
+ opacity=0.6,
1514
+ showscale=True,
1515
+ colorbar=dict(title="Genesis Potential Index")
1516
+ ),
1517
+ name='Genesis Potential'
 
1518
  )
1519
  )
1520
 
 
1526
  lons = [pt['lon'] for pt in track]
1527
  intensities = [pt['intensity'] for pt in track]
1528
 
1529
+ # Complete track
1530
  fig.add_trace(
1531
+ go.Scattergeo(
1532
+ lat=lats,
1533
+ lon=lons,
1534
  mode='lines+markers',
1535
  line=dict(color='red', width=3),
1536
  marker=dict(
1537
+ size=[max(6, int/8) for int in intensities],
1538
  color=intensities,
1539
  colorscale='Reds',
1540
  showscale=False
1541
  ),
1542
+ name=f'Storm {storm["storm_id"]} Complete Track'
 
 
 
 
 
 
1543
  )
1544
  )
1545
 
1546
  # Genesis marker
1547
  fig.add_trace(
1548
+ go.Scattergeo(
1549
+ lat=[lats[0]],
1550
+ lon=[lons[0]],
1551
  mode='markers',
1552
  marker=dict(
1553
+ size=15,
1554
+ color='gold',
1555
  symbol='star',
1556
  line=dict(width=2, color='black')
1557
  ),
 
1560
  )
1561
  )
1562
 
1563
+ # FINAL LAYOUT (Professional meteorological style)
1564
  fig.update_layout(
1565
+ title={
1566
+ 'text': f"馃寠 Typhoon Genesis & Development Forecast<br><sub>{month:02d}/{year} | ONI: {oni_value:.2f} | " +
1567
+ ("Interactive Animation" if enable_animation else "Monthly Summary") + "</sub>",
1568
+ 'x': 0.5,
1569
+ 'font': {'size': 18}
1570
+ },
1571
+ geo=dict(
1572
+ projection_type="natural earth",
1573
+ showland=True,
1574
+ landcolor="lightgray",
1575
+ showocean=True,
1576
+ oceancolor="lightblue",
1577
+ showcoastlines=True,
1578
+ coastlinecolor="darkgray",
1579
+ showlakes=True,
1580
+ lakecolor="lightblue",
1581
+ showcountries=True,
1582
+ countrycolor="gray",
1583
+ resolution=50,
1584
+ center=dict(lat=20, lon=140),
1585
+ lonaxis_range=[110, 180],
1586
+ lataxis_range=[5, 35]
1587
+ ),
1588
+ width=1200,
1589
+ height=800,
1590
+ showlegend=True,
1591
+ legend=dict(
1592
+ x=0.02,
1593
+ y=0.98,
1594
+ bgcolor="rgba(255,255,255,0.8)",
1595
+ bordercolor="gray",
1596
+ borderwidth=1
1597
+ )
1598
  )
1599
 
 
 
 
 
1600
  return fig
1601
 
1602
  except Exception as e:
1603
+ logging.error(f"Error creating professional genesis animation: {e}")
1604
+ import traceback
1605
+ traceback.print_exc()
1606
  return create_error_plot(f"Animation error: {str(e)}")
1607
 
1608
  def create_error_plot(error_message):