aditya-me13 commited on
Commit
e86c10c
Β·
1 Parent(s): f89b28b

FEATURE: Implement fully interactive pollution maps with zoom, pan and coordinate display

Browse files

- Replace static JPG output with interactive HTML plots using Plotly
- Add comprehensive zoom, pan, and hover functionality with exact coordinates
- Implement PNG download capability via toolbar camera icon
- Create new interactive_plot.html template with user instructions
- Add drawing tools, annotations, and responsive behavior
- Support both HTML and PNG file serving
- Maintain shapefile boundary integration with interactive features
- Add detailed plot information display and statistics

app.py CHANGED
@@ -431,8 +431,8 @@ def visualize_interactive():
431
  pressure_level=pressure_level_val
432
  )
433
 
434
- # Generate interactive plot (saved as JPG)
435
- plot_path = interactive_plotter.create_india_map(
436
  data_values,
437
  metadata,
438
  color_theme=color_theme,
@@ -441,9 +441,7 @@ def visualize_interactive():
441
 
442
  processor.close()
443
 
444
- if plot_path:
445
- plot_filename = Path(plot_path).name
446
-
447
  # Prepare metadata for display
448
  plot_info = {
449
  'variable': metadata.get('display_name', 'Unknown Variable'),
@@ -457,11 +455,13 @@ def visualize_interactive():
457
  'max': float(f"{data_values.max():.3f}") if hasattr(data_values, 'max') and not data_values.max() is None else 0,
458
  'mean': float(f"{data_values.mean():.3f}") if hasattr(data_values, 'mean') and not data_values.mean() is None else 0
459
  },
460
- 'is_interactive': True
 
 
461
  }
462
 
463
- return render_template('plot.html',
464
- plot_filename=plot_filename,
465
  plot_info=plot_info)
466
  else:
467
  flash('Error generating interactive plot', 'error')
@@ -479,11 +479,16 @@ def serve_plot(filename):
479
  plot_path = Path('plots') / filename
480
  if plot_path.exists():
481
  # Determine mimetype based on file extension
482
- if filename.lower().endswith('.jpg') or filename.lower().endswith('.jpeg'):
 
 
483
  mimetype = 'image/jpeg'
484
- else:
 
485
  mimetype = 'image/png'
486
- return send_file(str(plot_path), mimetype=mimetype)
 
 
487
  else:
488
  flash('Plot not found', 'error')
489
  return redirect(url_for('index'))
 
431
  pressure_level=pressure_level_val
432
  )
433
 
434
+ # Generate interactive plot
435
+ result = interactive_plotter.create_india_map(
436
  data_values,
437
  metadata,
438
  color_theme=color_theme,
 
441
 
442
  processor.close()
443
 
444
+ if result and result.get('html_content'):
 
 
445
  # Prepare metadata for display
446
  plot_info = {
447
  'variable': metadata.get('display_name', 'Unknown Variable'),
 
455
  'max': float(f"{data_values.max():.3f}") if hasattr(data_values, 'max') and not data_values.max() is None else 0,
456
  'mean': float(f"{data_values.mean():.3f}") if hasattr(data_values, 'mean') and not data_values.mean() is None else 0
457
  },
458
+ 'is_interactive': True,
459
+ 'html_path': result.get('html_path'),
460
+ 'png_path': result.get('png_path')
461
  }
462
 
463
+ return render_template('interactive_plot.html',
464
+ plot_html=result['html_content'],
465
  plot_info=plot_info)
466
  else:
467
  flash('Error generating interactive plot', 'error')
 
479
  plot_path = Path('plots') / filename
480
  if plot_path.exists():
481
  # Determine mimetype based on file extension
482
+ if filename.lower().endswith('.html'):
483
+ return send_file(str(plot_path), mimetype='text/html', as_attachment=True)
484
+ elif filename.lower().endswith('.jpg') or filename.lower().endswith('.jpeg'):
485
  mimetype = 'image/jpeg'
486
+ return send_file(str(plot_path), mimetype=mimetype)
487
+ elif filename.lower().endswith('.png'):
488
  mimetype = 'image/png'
489
+ return send_file(str(plot_path), mimetype=mimetype)
490
+ else:
491
+ return send_file(str(plot_path), as_attachment=True)
492
  else:
493
  flash('Plot not found', 'error')
494
  return redirect(url_for('index'))
interactive_plot_generator.py CHANGED
@@ -8,6 +8,7 @@ import geopandas as gpd
8
  from pathlib import Path
9
  from datetime import datetime
10
  from constants import INDIA_BOUNDS, COLOR_THEMES
 
11
  import warnings
12
  warnings.filterwarnings('ignore')
13
 
@@ -43,11 +44,14 @@ class InteractiveIndiaMapPlotter:
43
  data_values (np.ndarray): 2D array of pollution data
44
  metadata (dict): Metadata containing lats, lons, variable info, etc.
45
  color_theme (str): Color theme name from COLOR_THEMES
46
- save_plot (bool): Whether to save the plot as JPG
47
  custom_title (str): Custom title for the plot
48
 
49
  Returns:
50
- str: Path to saved plot file
 
 
 
51
  """
52
  try:
53
  # Extract metadata
@@ -155,7 +159,7 @@ class InteractiveIndiaMapPlotter:
155
  lon_range = [INDIA_BOUNDS['lon_min'], INDIA_BOUNDS['lon_max']]
156
  lat_range = [INDIA_BOUNDS['lat_min'], INDIA_BOUNDS['lat_max']]
157
 
158
- # Update layout
159
  fig.update_layout(
160
  title=dict(
161
  text=title,
@@ -167,17 +171,29 @@ class InteractiveIndiaMapPlotter:
167
  title='Longitude',
168
  range=lon_range,
169
  showgrid=True,
170
- gridcolor='rgba(128, 128, 128, 0.3)'
 
171
  ),
172
  yaxis=dict(
173
  title='Latitude',
174
  range=lat_range,
175
  showgrid=True,
176
- gridcolor='rgba(128, 128, 128, 0.3)'
 
177
  ),
178
- width=1400,
179
- height=1000,
180
  plot_bgcolor='white',
 
 
 
 
 
 
 
 
 
 
181
  annotations=[
182
  # Statistics box
183
  dict(
@@ -204,15 +220,67 @@ class InteractiveIndiaMapPlotter:
204
  borderwidth=1,
205
  borderpad=8,
206
  font=dict(size=10)
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  )
208
  ]
209
  )
210
 
211
- plot_path = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  if save_plot:
213
- plot_path = self._save_plot(fig, var_name, display_name, pressure_level, color_theme, time_stamp)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
214
 
215
- return plot_path
216
 
217
  except Exception as e:
218
  raise Exception(f"Error creating interactive map: {str(e)}")
@@ -279,8 +347,8 @@ class InteractiveIndiaMapPlotter:
279
  stats_lines = [f"{name}: {format_number(val)}{units_str}" for name, val in stats.items()]
280
  return "\n".join(stats_lines)
281
 
282
- def _save_plot(self, fig, var_name, display_name, pressure_level, color_theme, time_stamp):
283
- """Save the plot as JPG"""
284
  safe_display_name = display_name.replace('/', '_').replace(' ', '_').replace('β‚‚', '2').replace('₃', '3').replace('.', '_')
285
  safe_time_stamp = time_stamp.replace('-', '').replace(':', '').replace(' ', '_')
286
 
@@ -288,16 +356,38 @@ class InteractiveIndiaMapPlotter:
288
  if pressure_level:
289
  filename_parts.append(f"{int(pressure_level)}hPa")
290
  filename_parts.extend([color_theme, safe_time_stamp])
291
- filename = "_".join(filename_parts) + ".jpg"
292
 
293
  plot_path = self.plots_dir / filename
294
 
295
- # Save as static JPG with high quality
296
- fig.write_image(str(plot_path), format='jpg', width=1400, height=1000, scale=2)
297
- print(f"Interactive plot saved as JPG: {plot_path}")
298
 
299
  return str(plot_path)
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
  def list_available_themes(self):
302
  """List available color themes"""
303
  return COLOR_THEMES
@@ -333,8 +423,11 @@ def test_interactive_plot_generator():
333
  plotter = InteractiveIndiaMapPlotter(shapefile_path=shapefile_path)
334
 
335
  try:
336
- plot_path = plotter.create_india_map(data, metadata, color_theme='YlOrRd')
337
- print(f"βœ… Test interactive plot created successfully: {plot_path}")
 
 
 
338
  return True
339
  except Exception as e:
340
  print(f"❌ Test failed: {str(e)}")
 
8
  from pathlib import Path
9
  from datetime import datetime
10
  from constants import INDIA_BOUNDS, COLOR_THEMES
11
+ import plotly.io as pio
12
  import warnings
13
  warnings.filterwarnings('ignore')
14
 
 
44
  data_values (np.ndarray): 2D array of pollution data
45
  metadata (dict): Metadata containing lats, lons, variable info, etc.
46
  color_theme (str): Color theme name from COLOR_THEMES
47
+ save_plot (bool): Whether to save the plot as HTML and PNG
48
  custom_title (str): Custom title for the plot
49
 
50
  Returns:
51
+ dict: Dictionary containing paths to saved files and HTML content
52
+ - 'html_path': Path to interactive HTML file
53
+ - 'png_path': Path to static PNG file
54
+ - 'html_content': HTML content for embedding
55
  """
56
  try:
57
  # Extract metadata
 
159
  lon_range = [INDIA_BOUNDS['lon_min'], INDIA_BOUNDS['lon_max']]
160
  lat_range = [INDIA_BOUNDS['lat_min'], INDIA_BOUNDS['lat_max']]
161
 
162
+ # Update layout for better interactivity
163
  fig.update_layout(
164
  title=dict(
165
  text=title,
 
171
  title='Longitude',
172
  range=lon_range,
173
  showgrid=True,
174
+ gridcolor='rgba(128, 128, 128, 0.3)',
175
+ zeroline=False
176
  ),
177
  yaxis=dict(
178
  title='Latitude',
179
  range=lat_range,
180
  showgrid=True,
181
+ gridcolor='rgba(128, 128, 128, 0.3)',
182
+ zeroline=False
183
  ),
184
+ width=1200,
185
+ height=800,
186
  plot_bgcolor='white',
187
+ # Enable zoom, pan and other interactive features
188
+ dragmode='zoom',
189
+ showlegend=False,
190
+ hovermode='closest',
191
+ # Add modebar with download options
192
+ modebar=dict(
193
+ bgcolor='rgba(255, 255, 255, 0.8)',
194
+ activecolor='rgb(0, 123, 255)',
195
+ orientation='h'
196
+ ),
197
  annotations=[
198
  # Statistics box
199
  dict(
 
220
  borderwidth=1,
221
  borderpad=8,
222
  font=dict(size=10)
223
+ ),
224
+ # Instructions
225
+ dict(
226
+ text='πŸ” Zoom: Mouse wheel or zoom tool | πŸ“ Hover: Show coordinates & values | πŸ“₯ Download: Camera icon',
227
+ xref='paper', yref='paper',
228
+ x=0.5, y=0.02,
229
+ xanchor='center', yanchor='bottom',
230
+ showarrow=False,
231
+ bgcolor='rgba(173, 216, 230, 0.8)',
232
+ bordercolor='steelblue',
233
+ borderwidth=1,
234
+ borderpad=8,
235
+ font=dict(size=10, color='darkblue')
236
  )
237
  ]
238
  )
239
 
240
+ # Configure the figure for better interactivity and downloads
241
+ config = {
242
+ 'displayModeBar': True,
243
+ 'displaylogo': False,
244
+ 'modeBarButtonsToAdd': [
245
+ 'drawline',
246
+ 'drawopenpath',
247
+ 'drawclosedpath',
248
+ 'drawcircle',
249
+ 'drawrect',
250
+ 'eraseshape'
251
+ ],
252
+ 'modeBarButtonsToRemove': ['lasso2d', 'select2d'],
253
+ 'toImageButtonOptions': {
254
+ 'format': 'png',
255
+ 'filename': f'india_pollution_map_{datetime.now().strftime("%Y%m%d_%H%M%S")}',
256
+ 'height': 800,
257
+ 'width': 1200,
258
+ 'scale': 2
259
+ },
260
+ 'responsive': True
261
+ }
262
+
263
+ # Save files if requested
264
+ result = {'html_content': None, 'html_path': None, 'png_path': None}
265
+
266
  if save_plot:
267
+ # Generate HTML content for embedding
268
+ html_content = pio.to_html(fig, config=config, include_plotlyjs='cdn', div_id='interactive-plot')
269
+ result['html_content'] = html_content
270
+
271
+ # Save as HTML file
272
+ html_path = self._save_html_plot(fig, var_name, display_name, pressure_level, color_theme, time_stamp, config)
273
+ result['html_path'] = html_path
274
+
275
+ # Save as PNG for fallback
276
+ png_path = self._save_png_plot(fig, var_name, display_name, pressure_level, color_theme, time_stamp)
277
+ result['png_path'] = png_path
278
+ else:
279
+ # Just return HTML content for display
280
+ html_content = pio.to_html(fig, config=config, include_plotlyjs='cdn')
281
+ result['html_content'] = html_content
282
 
283
+ return result
284
 
285
  except Exception as e:
286
  raise Exception(f"Error creating interactive map: {str(e)}")
 
347
  stats_lines = [f"{name}: {format_number(val)}{units_str}" for name, val in stats.items()]
348
  return "\n".join(stats_lines)
349
 
350
+ def _save_html_plot(self, fig, var_name, display_name, pressure_level, color_theme, time_stamp, config):
351
+ """Save the interactive plot as HTML"""
352
  safe_display_name = display_name.replace('/', '_').replace(' ', '_').replace('β‚‚', '2').replace('₃', '3').replace('.', '_')
353
  safe_time_stamp = time_stamp.replace('-', '').replace(':', '').replace(' ', '_')
354
 
 
356
  if pressure_level:
357
  filename_parts.append(f"{int(pressure_level)}hPa")
358
  filename_parts.extend([color_theme, safe_time_stamp])
359
+ filename = "_".join(filename_parts) + ".html"
360
 
361
  plot_path = self.plots_dir / filename
362
 
363
+ # Save as interactive HTML
364
+ fig.write_html(str(plot_path), config=config, include_plotlyjs='cdn')
365
+ print(f"Interactive HTML plot saved: {plot_path}")
366
 
367
  return str(plot_path)
368
 
369
+ def _save_png_plot(self, fig, var_name, display_name, pressure_level, color_theme, time_stamp):
370
+ """Save the plot as PNG for download/fallback"""
371
+ safe_display_name = display_name.replace('/', '_').replace(' ', '_').replace('β‚‚', '2').replace('₃', '3').replace('.', '_')
372
+ safe_time_stamp = time_stamp.replace('-', '').replace(':', '').replace(' ', '_')
373
+
374
+ filename_parts = [f"{safe_display_name}_India_static"]
375
+ if pressure_level:
376
+ filename_parts.append(f"{int(pressure_level)}hPa")
377
+ filename_parts.extend([color_theme, safe_time_stamp])
378
+ filename = "_".join(filename_parts) + ".png"
379
+
380
+ plot_path = self.plots_dir / filename
381
+
382
+ try:
383
+ # Save as static PNG with high quality
384
+ fig.write_image(str(plot_path), format='png', width=1200, height=800, scale=2)
385
+ print(f"Static PNG plot saved: {plot_path}")
386
+ return str(plot_path)
387
+ except Exception as e:
388
+ print(f"Warning: Could not save PNG: {e}")
389
+ return None
390
+
391
  def list_available_themes(self):
392
  """List available color themes"""
393
  return COLOR_THEMES
 
423
  plotter = InteractiveIndiaMapPlotter(shapefile_path=shapefile_path)
424
 
425
  try:
426
+ result = plotter.create_india_map(data, metadata, color_theme='YlOrRd')
427
+ if result.get('html_path'):
428
+ print(f"βœ… Test interactive HTML plot created successfully: {result['html_path']}")
429
+ if result.get('png_path'):
430
+ print(f"βœ… Test static PNG plot created successfully: {result['png_path']}")
431
  return True
432
  except Exception as e:
433
  print(f"❌ Test failed: {str(e)}")
templates/interactive_plot.html ADDED
@@ -0,0 +1,299 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Interactive Air Pollution Map - India CAMS Dashboard</title>
7
+ <script src="https://cdn.plot.ly/plotly-latest.min.js"></script>
8
+ <style>
9
+ * {
10
+ margin: 0;
11
+ padding: 0;
12
+ box-sizing: border-box;
13
+ }
14
+
15
+ body {
16
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
17
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
18
+ min-height: 100vh;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1400px;
23
+ margin: 0 auto;
24
+ padding: 20px;
25
+ }
26
+
27
+ .header {
28
+ background: rgba(255, 255, 255, 0.95);
29
+ border-radius: 15px;
30
+ padding: 20px;
31
+ margin-bottom: 20px;
32
+ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
33
+ backdrop-filter: blur(4px);
34
+ border: 1px solid rgba(255, 255, 255, 0.18);
35
+ }
36
+
37
+ .header h1 {
38
+ color: #2c3e50;
39
+ margin-bottom: 10px;
40
+ text-align: center;
41
+ }
42
+
43
+ .plot-container {
44
+ background: rgba(255, 255, 255, 0.95);
45
+ border-radius: 15px;
46
+ padding: 20px;
47
+ margin-bottom: 20px;
48
+ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
49
+ backdrop-filter: blur(4px);
50
+ border: 1px solid rgba(255, 255, 255, 0.18);
51
+ }
52
+
53
+ .plot-info {
54
+ background: rgba(255, 255, 255, 0.95);
55
+ border-radius: 15px;
56
+ padding: 20px;
57
+ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
58
+ backdrop-filter: blur(4px);
59
+ border: 1px solid rgba(255, 255, 255, 0.18);
60
+ }
61
+
62
+ .info-grid {
63
+ display: grid;
64
+ grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
65
+ gap: 15px;
66
+ margin-bottom: 20px;
67
+ }
68
+
69
+ .info-item {
70
+ background: rgba(52, 73, 94, 0.1);
71
+ padding: 15px;
72
+ border-radius: 10px;
73
+ border-left: 4px solid #3498db;
74
+ }
75
+
76
+ .info-item h3 {
77
+ color: #2c3e50;
78
+ margin-bottom: 5px;
79
+ font-size: 14px;
80
+ font-weight: 600;
81
+ }
82
+
83
+ .info-item p {
84
+ color: #34495e;
85
+ font-size: 16px;
86
+ font-weight: 500;
87
+ }
88
+
89
+ .controls {
90
+ display: flex;
91
+ gap: 15px;
92
+ flex-wrap: wrap;
93
+ margin-bottom: 20px;
94
+ }
95
+
96
+ .btn {
97
+ background: linear-gradient(45deg, #3498db, #2980b9);
98
+ color: white;
99
+ padding: 12px 24px;
100
+ border: none;
101
+ border-radius: 25px;
102
+ text-decoration: none;
103
+ font-weight: 600;
104
+ transition: all 0.3s ease;
105
+ cursor: pointer;
106
+ font-size: 14px;
107
+ }
108
+
109
+ .btn:hover {
110
+ transform: translateY(-2px);
111
+ box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4);
112
+ }
113
+
114
+ .btn-download {
115
+ background: linear-gradient(45deg, #27ae60, #219a52);
116
+ }
117
+
118
+ .btn-download:hover {
119
+ box-shadow: 0 8px 25px rgba(39, 174, 96, 0.4);
120
+ }
121
+
122
+ .btn-back {
123
+ background: linear-gradient(45deg, #95a5a6, #7f8c8d);
124
+ }
125
+
126
+ .btn-back:hover {
127
+ box-shadow: 0 8px 25px rgba(149, 165, 166, 0.4);
128
+ }
129
+
130
+ .instructions {
131
+ background: rgba(241, 196, 15, 0.1);
132
+ border: 2px solid rgba(241, 196, 15, 0.3);
133
+ border-radius: 10px;
134
+ padding: 15px;
135
+ margin-bottom: 20px;
136
+ }
137
+
138
+ .instructions h3 {
139
+ color: #f39c12;
140
+ margin-bottom: 10px;
141
+ }
142
+
143
+ .instructions ul {
144
+ color: #34495e;
145
+ padding-left: 20px;
146
+ }
147
+
148
+ .instructions li {
149
+ margin-bottom: 5px;
150
+ }
151
+
152
+ .interactive-plot {
153
+ border-radius: 10px;
154
+ overflow: hidden;
155
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
156
+ }
157
+
158
+ @media (max-width: 768px) {
159
+ .container {
160
+ padding: 10px;
161
+ }
162
+
163
+ .controls {
164
+ justify-content: center;
165
+ }
166
+
167
+ .btn {
168
+ padding: 10px 20px;
169
+ font-size: 12px;
170
+ }
171
+ }
172
+ </style>
173
+ </head>
174
+ <body>
175
+ <div class="container">
176
+ <div class="header">
177
+ <h1>🌍 Interactive Air Pollution Map</h1>
178
+ <p style="text-align: center; color: #7f8c8d; margin-top: 10px;">
179
+ Hover over the map to see exact coordinates and pollution values. Use the toolbar to zoom, pan, and download.
180
+ </p>
181
+ </div>
182
+
183
+ <div class="instructions">
184
+ <h3>οΏ½οΏ½ How to Use This Interactive Map:</h3>
185
+ <ul>
186
+ <li><strong>πŸ–±οΈ Hover:</strong> Move your mouse over any point to see exact coordinates, pollution values, and location data</li>
187
+ <li><strong>πŸ” Zoom:</strong> Use mouse wheel, zoom buttons in toolbar, or draw a rectangle to zoom to specific area</li>
188
+ <li><strong>πŸ“ Pan:</strong> Click and drag to move around the map</li>
189
+ <li><strong>πŸ“₯ Download:</strong> Click the camera icon in the toolbar to download as PNG image</li>
190
+ <li><strong>πŸ”„ Reset:</strong> Double-click anywhere to reset zoom to original view</li>
191
+ <li><strong>✏️ Annotate:</strong> Use drawing tools in the toolbar to add lines, shapes, and annotations</li>
192
+ </ul>
193
+ </div>
194
+
195
+ <div class="controls">
196
+ <a href="{{ url_for('index') }}" class="btn btn-back">← Back to Dashboard</a>
197
+ {% if plot_info.png_path %}
198
+ <a href="{{ url_for('serve_plot', filename=plot_info.png_path.split('/')[-1]) }}"
199
+ class="btn btn-download" download>πŸ“₯ Download PNG</a>
200
+ {% endif %}
201
+ {% if plot_info.html_path %}
202
+ <a href="{{ url_for('serve_plot', filename=plot_info.html_path.split('/')[-1]) }}"
203
+ class="btn btn-download" download>πŸ“„ Download HTML</a>
204
+ {% endif %}
205
+ </div>
206
+
207
+ <div class="plot-container">
208
+ <div class="interactive-plot">
209
+ {{ plot_html|safe }}
210
+ </div>
211
+ </div>
212
+
213
+ <div class="plot-info">
214
+ <h2 style="color: #2c3e50; margin-bottom: 20px;">πŸ“Š Plot Information</h2>
215
+
216
+ <div class="info-grid">
217
+ <div class="info-item">
218
+ <h3>πŸ§ͺ Variable</h3>
219
+ <p>{{ plot_info.variable }}</p>
220
+ </div>
221
+
222
+ {% if plot_info.units %}
223
+ <div class="info-item">
224
+ <h3>πŸ“ Units</h3>
225
+ <p>{{ plot_info.units }}</p>
226
+ </div>
227
+ {% endif %}
228
+
229
+ {% if plot_info.pressure_level %}
230
+ <div class="info-item">
231
+ <h3>🌑️ Pressure Level</h3>
232
+ <p>{{ plot_info.pressure_level }} hPa</p>
233
+ </div>
234
+ {% endif %}
235
+
236
+ <div class="info-item">
237
+ <h3>🎨 Color Theme</h3>
238
+ <p>{{ plot_info.color_theme }}</p>
239
+ </div>
240
+
241
+ <div class="info-item">
242
+ <h3>πŸ“ Data Shape</h3>
243
+ <p>{{ plot_info.shape }}</p>
244
+ </div>
245
+
246
+ <div class="info-item">
247
+ <h3>⏰ Generated</h3>
248
+ <p>{{ plot_info.generated_time }}</p>
249
+ </div>
250
+ </div>
251
+
252
+ <div class="info-grid">
253
+ <div class="info-item">
254
+ <h3>πŸ“Š Minimum Value</h3>
255
+ <p>{{ "%.3f"|format(plot_info.data_range.min) }}{% if plot_info.units %} {{ plot_info.units }}{% endif %}</p>
256
+ </div>
257
+
258
+ <div class="info-item">
259
+ <h3>πŸ“Š Maximum Value</h3>
260
+ <p>{{ "%.3f"|format(plot_info.data_range.max) }}{% if plot_info.units %} {{ plot_info.units }}{% endif %}</p>
261
+ </div>
262
+
263
+ <div class="info-item">
264
+ <h3>πŸ“Š Average Value</h3>
265
+ <p>{{ "%.3f"|format(plot_info.data_range.mean) }}{% if plot_info.units %} {{ plot_info.units }}{% endif %}</p>
266
+ </div>
267
+ </div>
268
+ </div>
269
+ </div>
270
+
271
+ <script>
272
+ // Additional interactivity enhancements
273
+ document.addEventListener('DOMContentLoaded', function() {
274
+ // Add responsive behavior
275
+ const plotDiv = document.getElementById('interactive-plot');
276
+ if (plotDiv) {
277
+ window.addEventListener('resize', function() {
278
+ Plotly.Plots.resize(plotDiv);
279
+ });
280
+ }
281
+
282
+ // Add loading indicator for downloads
283
+ const downloadButtons = document.querySelectorAll('.btn-download');
284
+ downloadButtons.forEach(button => {
285
+ button.addEventListener('click', function() {
286
+ const originalText = this.textContent;
287
+ this.textContent = '⏳ Preparing download...';
288
+ this.style.pointerEvents = 'none';
289
+
290
+ setTimeout(() => {
291
+ this.textContent = originalText;
292
+ this.style.pointerEvents = 'auto';
293
+ }, 2000);
294
+ });
295
+ });
296
+ });
297
+ </script>
298
+ </body>
299
+ </html>