aditya-me13 commited on
Commit
868635f
Β·
1 Parent(s): 6258aac

Fix plot aspect ratios and implement gallery delete functionality

Browse files

- Fixed interactive plot horizontal stretching issues
- Made static and interactive plots pixel-wise for visual consistency
- Resolved data orientation mirroring with latitude coordinate handling
- Adjusted plot proportions to match between static and interactive
- Added comprehensive gallery functionality with delete buttons
- Enhanced error handling and user feedback
- Removed unused README_HF.md file

README_HF.md DELETED
@@ -1,27 +0,0 @@
1
- # CAMS Air Pollution Dashboard
2
-
3
- A comprehensive web application for visualizing atmospheric composition data from the Copernicus Atmosphere Monitoring Service (CAMS).
4
-
5
- ## Features
6
-
7
- - 🌍 Interactive air pollution maps for India
8
- - πŸ“Š Multiple pollutant visualization (PM2.5, PM10, NO2, O3, CO, etc.)
9
- - 🎨 Various color themes for data visualization
10
- - πŸ“ Support for NetCDF file uploads
11
- - 🌐 Direct CAMS data download integration
12
- - πŸ“ˆ Statistical analysis and hover information
13
-
14
- ## Usage
15
-
16
- 1. Upload your own NetCDF files or download CAMS data for specific dates
17
- 2. Select variables, pressure levels, and time points
18
- 3. Generate static or interactive pollution maps
19
- 4. Explore air quality data with detailed statistics
20
-
21
- ## Data Source
22
-
23
- This application uses data from the Copernicus Atmosphere Monitoring Service (CAMS), which provides global atmospheric composition forecasts and analyses.
24
-
25
- ## License
26
-
27
- This project is open source and available under the MIT License.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app.py CHANGED
@@ -497,6 +497,161 @@ def serve_plot(filename):
497
  return redirect(url_for('index'))
498
 
499
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
500
  @app.route('/cleanup')
501
  def cleanup():
502
  """Clean up old files"""
 
497
  return redirect(url_for('index'))
498
 
499
 
500
+ @app.route('/gallery')
501
+ def gallery():
502
+ """Display gallery of all saved plots"""
503
+ try:
504
+ plots_dir = Path('plots')
505
+ plots_dir.mkdir(exist_ok=True)
506
+
507
+ # Get all plot files
508
+ plot_files = []
509
+
510
+ # Static plots (PNG/JPG)
511
+ for ext in ['*.png', '*.jpg', '*.jpeg']:
512
+ for plot_file in plots_dir.glob(ext):
513
+ if plot_file.is_file():
514
+ # Parse filename to extract metadata
515
+ filename = plot_file.name
516
+ file_info = {
517
+ 'filename': filename,
518
+ 'path': str(plot_file),
519
+ 'type': 'static',
520
+ 'size': plot_file.stat().st_size,
521
+ 'created': datetime.fromtimestamp(plot_file.stat().st_mtime),
522
+ 'extension': plot_file.suffix.lower()
523
+ }
524
+
525
+ # Try to parse metadata from filename
526
+ try:
527
+ # Example: PM2_5_India_viridis_20200824_1200.png
528
+ name_parts = filename.replace(plot_file.suffix, '').split('_')
529
+ if len(name_parts) >= 3:
530
+ file_info['variable'] = name_parts[0].replace('_', '.')
531
+ file_info['region'] = name_parts[1] if len(name_parts) > 1 else 'Unknown'
532
+ file_info['theme'] = name_parts[-2] if len(name_parts) > 2 else 'Unknown'
533
+ except:
534
+ file_info['variable'] = 'Unknown'
535
+ file_info['region'] = 'Unknown'
536
+ file_info['theme'] = 'Unknown'
537
+
538
+ plot_files.append(file_info)
539
+
540
+ # Interactive plots (HTML)
541
+ for plot_file in plots_dir.glob('*.html'):
542
+ if plot_file.is_file():
543
+ filename = plot_file.name
544
+ file_info = {
545
+ 'filename': filename,
546
+ 'path': str(plot_file),
547
+ 'type': 'interactive',
548
+ 'size': plot_file.stat().st_size,
549
+ 'created': datetime.fromtimestamp(plot_file.stat().st_mtime),
550
+ 'extension': '.html'
551
+ }
552
+
553
+ # Try to parse metadata from filename
554
+ try:
555
+ name_parts = filename.replace('.html', '').split('_')
556
+ if len(name_parts) >= 3:
557
+ file_info['variable'] = name_parts[0].replace('_', '.')
558
+ file_info['region'] = name_parts[1] if len(name_parts) > 1 else 'Unknown'
559
+ file_info['theme'] = name_parts[-2] if len(name_parts) > 2 else 'Unknown'
560
+ except:
561
+ file_info['variable'] = 'Unknown'
562
+ file_info['region'] = 'Unknown'
563
+ file_info['theme'] = 'Unknown'
564
+
565
+ plot_files.append(file_info)
566
+
567
+ # Sort by creation time (newest first)
568
+ plot_files.sort(key=lambda x: x['created'], reverse=True)
569
+
570
+ # Group by type for display
571
+ static_plots = [p for p in plot_files if p['type'] == 'static']
572
+ interactive_plots = [p for p in plot_files if p['type'] == 'interactive']
573
+
574
+ return render_template('gallery.html',
575
+ static_plots=static_plots,
576
+ interactive_plots=interactive_plots,
577
+ total_plots=len(plot_files))
578
+
579
+ except Exception as e:
580
+ flash(f'Error loading gallery: {str(e)}', 'error')
581
+ return redirect(url_for('index'))
582
+
583
+
584
+ @app.route('/view_interactive/<filename>')
585
+ def view_interactive_plot(filename):
586
+ """View an interactive plot from the gallery"""
587
+ try:
588
+ plot_path = Path('plots') / filename
589
+ if not plot_path.exists() or not filename.endswith('.html'):
590
+ flash('Interactive plot not found', 'error')
591
+ return redirect(url_for('gallery'))
592
+
593
+ # Read the HTML content
594
+ with open(plot_path, 'r', encoding='utf-8') as f:
595
+ html_content = f.read()
596
+
597
+ # Create plot info from filename
598
+ plot_info = {
599
+ 'variable': 'Unknown',
600
+ 'generated_time': datetime.fromtimestamp(plot_path.stat().st_mtime).strftime('%Y-%m-%d %H:%M:%S'),
601
+ 'is_interactive': True,
602
+ 'filename': filename
603
+ }
604
+
605
+ return render_template('view_interactive.html',
606
+ plot_html=html_content,
607
+ plot_info=plot_info)
608
+
609
+ except Exception as e:
610
+ flash(f'Error viewing plot: {str(e)}', 'error')
611
+ return redirect(url_for('gallery'))
612
+
613
+
614
+ @app.route('/delete_plot/<filename>', methods=['DELETE'])
615
+ def delete_plot(filename):
616
+ """Delete a specific plot file"""
617
+ print(f"πŸ—‘οΈ DELETE request received for: {filename}") # Debug logging
618
+ try:
619
+ # Security check: ensure filename is safe
620
+ if not filename or '..' in filename or '/' in filename:
621
+ print(f"❌ Invalid filename: {filename}")
622
+ return jsonify({'success': False, 'error': 'Invalid filename'}), 400
623
+
624
+ plot_path = Path('plots') / filename
625
+ print(f"πŸ” Looking for file at: {plot_path}")
626
+
627
+ # Check if file exists
628
+ if not plot_path.exists():
629
+ print(f"❌ File not found: {plot_path}")
630
+ return jsonify({'success': False, 'error': 'File not found'}), 404
631
+
632
+ # Check if it's a valid plot file
633
+ allowed_extensions = ['.png', '.jpg', '.jpeg', '.html']
634
+ if plot_path.suffix.lower() not in allowed_extensions:
635
+ print(f"❌ Invalid file type: {filename}")
636
+ return jsonify({'success': False, 'error': 'Invalid file type'}), 400
637
+
638
+ # Delete the file
639
+ plot_path.unlink()
640
+ print(f"βœ… Successfully deleted: {filename}")
641
+
642
+ return jsonify({
643
+ 'success': True,
644
+ 'message': f'Plot {filename} deleted successfully'
645
+ })
646
+
647
+ except Exception as e:
648
+ print(f"πŸ’₯ Error deleting file {filename}: {str(e)}")
649
+ return jsonify({
650
+ 'success': False,
651
+ 'error': f'Failed to delete plot: {str(e)}'
652
+ }), 500
653
+
654
+
655
  @app.route('/cleanup')
656
  def cleanup():
657
  """Clean up old files"""
data_processor.py CHANGED
@@ -307,9 +307,12 @@ class NetCDFProcessor:
307
  lats = dataset[coords['lat']].values
308
  lons = dataset[coords['lon']].values
309
 
 
 
 
310
  # Convert units if necessary
311
  original_units = getattr(dataset[variable_name], 'units', '')
312
- data_values = self._convert_units(data_array.values, original_units, var_info['units'])
313
 
314
  metadata = {
315
  'variable_name': variable_name,
@@ -356,6 +359,64 @@ class NetCDFProcessor:
356
 
357
  return data_converted
358
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
  def get_available_times(self, variable_name):
360
  """Get available time steps for a variable"""
361
  if variable_name not in self.detected_variables:
 
307
  lats = dataset[coords['lat']].values
308
  lons = dataset[coords['lon']].values
309
 
310
+ # Crop data to India region for better performance
311
+ data_values, lats, lons = self._crop_to_india(data_array.values, lats, lons)
312
+
313
  # Convert units if necessary
314
  original_units = getattr(dataset[variable_name], 'units', '')
315
+ data_values = self._convert_units(data_values, original_units, var_info['units'])
316
 
317
  metadata = {
318
  'variable_name': variable_name,
 
359
 
360
  return data_converted
361
 
362
+ def _crop_to_india(self, data_values, lats, lons):
363
+ """
364
+ Crop data to India region to improve performance and focus visualization
365
+
366
+ Parameters:
367
+ data_values (np.ndarray): 2D data array (lat, lon)
368
+ lats (np.ndarray): Latitude values
369
+ lons (np.ndarray): Longitude values
370
+
371
+ Returns:
372
+ tuple: (cropped_data, cropped_lats, cropped_lons)
373
+ """
374
+ from constants import INDIA_BOUNDS
375
+
376
+ # Define extended India bounds with some buffer
377
+ lat_min = INDIA_BOUNDS['lat_min'] - 2 # 6-2 = 4Β°N
378
+ lat_max = INDIA_BOUNDS['lat_max'] + 2 # 38+2 = 40Β°N
379
+ lon_min = INDIA_BOUNDS['lon_min'] - 2 # 68-2 = 66Β°E
380
+ lon_max = INDIA_BOUNDS['lon_max'] + 2 # 97+2 = 99Β°E
381
+
382
+ print(f"Original data shape: {data_values.shape}")
383
+ print(f"Original lat range: {lats.min():.2f} to {lats.max():.2f}")
384
+ print(f"Original lon range: {lons.min():.2f} to {lons.max():.2f}")
385
+
386
+ # Find indices within India bounds
387
+ lat_mask = (lats >= lat_min) & (lats <= lat_max)
388
+ lon_mask = (lons >= lon_min) & (lons <= lon_max)
389
+
390
+ # Get the indices
391
+ lat_indices = np.where(lat_mask)[0]
392
+ lon_indices = np.where(lon_mask)[0]
393
+
394
+ if len(lat_indices) == 0 or len(lon_indices) == 0:
395
+ print("Warning: No data found in India region, using full dataset")
396
+ return data_values, lats, lons
397
+
398
+ # Get the min and max indices to define the crop region
399
+ lat_start, lat_end = lat_indices[0], lat_indices[-1] + 1
400
+ lon_start, lon_end = lon_indices[0], lon_indices[-1] + 1
401
+
402
+ # Crop the data
403
+ if len(data_values.shape) == 2: # (lat, lon)
404
+ cropped_data = data_values[lat_start:lat_end, lon_start:lon_end]
405
+ else:
406
+ print(f"Warning: Unexpected data shape {data_values.shape}, cropping first two dimensions")
407
+ cropped_data = data_values[lat_start:lat_end, lon_start:lon_end]
408
+
409
+ # Crop coordinates
410
+ cropped_lats = lats[lat_start:lat_end]
411
+ cropped_lons = lons[lon_start:lon_end]
412
+
413
+ print(f"Cropped data shape: {cropped_data.shape}")
414
+ print(f"Cropped lat range: {cropped_lats.min():.2f} to {cropped_lats.max():.2f}")
415
+ print(f"Cropped lon range: {cropped_lons.min():.2f} to {cropped_lons.max():.2f}")
416
+ print(f"Data reduction: {(1 - cropped_data.size / data_values.size) * 100:.1f}%")
417
+
418
+ return cropped_data, cropped_lats, cropped_lons
419
+
420
  def get_available_times(self, variable_name):
421
  """Get available time steps for a variable"""
422
  if variable_name not in self.detected_variables:
interactive_plot_generator.py CHANGED
@@ -179,10 +179,12 @@ class InteractiveIndiaMapPlotter:
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',
@@ -253,8 +255,8 @@ class InteractiveIndiaMapPlotter:
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
@@ -393,7 +395,7 @@ class InteractiveIndiaMapPlotter:
393
 
394
  try:
395
  # Save as static PNG with high quality
396
- fig.write_image(str(plot_path), format='png', width=1200, height=800, scale=2)
397
  print(f"Static PNG plot saved: {plot_path}")
398
  return str(plot_path)
399
  except Exception as e:
 
179
  range=lat_range,
180
  showgrid=True,
181
  gridcolor='rgba(128, 128, 128, 0.3)',
182
+ zeroline=False,
183
+ scaleanchor="x",
184
+ scaleratio=1 # Simplified to match static plot aspect ratio
185
  ),
186
+ width=1400,
187
+ height=1000,
188
  plot_bgcolor='white',
189
  # Enable zoom, pan and other interactive features
190
  dragmode='zoom',
 
255
  'toImageButtonOptions': {
256
  'format': 'png',
257
  'filename': f'india_pollution_map_{datetime.now().strftime("%Y%m%d_%H%M%S")}',
258
+ 'height': 1000,
259
+ 'width': 1400,
260
  'scale': 2
261
  },
262
  'responsive': True
 
395
 
396
  try:
397
  # Save as static PNG with high quality
398
+ fig.write_image(str(plot_path), format='png', width=1400, height=1000, scale=2)
399
  print(f"Static PNG plot saved: {plot_path}")
400
  return str(plot_path)
401
  except Exception as e:
plot_generator.py CHANGED
@@ -63,8 +63,8 @@ class IndiaMapPlotter:
63
  print(f"Warning: Color theme '{color_theme}' not found, using 'viridis'")
64
  color_theme = 'viridis'
65
 
66
- # Create figure and axes
67
- fig = plt.figure(figsize=(14, 10))
68
  ax = fig.add_subplot(1, 1, 1)
69
 
70
  # Set map extent
@@ -73,7 +73,7 @@ class IndiaMapPlotter:
73
 
74
  # --- KEY CHANGE: PLOT ORDER & ZORDER ---
75
 
76
- # 1. Plot the pollution data in the background (lower zorder)
77
  if lons.ndim == 1 and lats.ndim == 1:
78
  lon_grid, lat_grid = np.meshgrid(lons, lats)
79
  else:
@@ -90,10 +90,23 @@ class IndiaMapPlotter:
90
  if vmax <= vmin:
91
  vmax = vmin + 1.0
92
 
93
- levels = np.linspace(vmin, vmax, 25)
94
- contour = ax.contourf(lon_grid, lat_grid, data_values,
95
- levels=levels, cmap=color_theme, extend='max',
96
- zorder=1)
 
 
 
 
 
 
 
 
 
 
 
 
 
97
 
98
  # Auto-adjust bounds if INDIA_BOUNDS is too small or wrong
99
  xmin, ymin, xmax, ymax = self.india_map.total_bounds
@@ -107,7 +120,7 @@ class IndiaMapPlotter:
107
  linewidth=0.8, zorder=2) # <-- CHANGED: Set zorder=2 (foreground)
108
 
109
  # Add colorbar
110
- cbar = plt.colorbar(contour, ax=ax, shrink=0.6, pad=0.02, aspect=30)
111
  cbar_label = f"{display_name}" + (f" ({units})" if units else "")
112
  cbar.set_label(cbar_label, fontsize=12, labelpad=15)
113
 
 
63
  print(f"Warning: Color theme '{color_theme}' not found, using 'viridis'")
64
  color_theme = 'viridis'
65
 
66
+ # Create figure and axes - match interactive plot proportions (1400x1000 = 1.4:1 ratio)
67
+ fig = plt.figure(figsize=(16, 10)) # Wider to match interactive plot
68
  ax = fig.add_subplot(1, 1, 1)
69
 
70
  # Set map extent
 
73
 
74
  # --- KEY CHANGE: PLOT ORDER & ZORDER ---
75
 
76
+ # 1. Plot the pollution data in the background (lower zorder) - pixel-wise like interactive plots
77
  if lons.ndim == 1 and lats.ndim == 1:
78
  lon_grid, lat_grid = np.meshgrid(lons, lats)
79
  else:
 
90
  if vmax <= vmin:
91
  vmax = vmin + 1.0
92
 
93
+ # Use imshow for pixel-wise display - matches interactive plot orientation
94
+ extent = [lons.min(), lons.max(), lats.min(), lats.max()]
95
+
96
+ # Handle latitude order for proper orientation
97
+ # NetCDF files often have descending latitudes, but imshow with origin='lower' expects ascending
98
+ lat_ascending = lats[0] < lats[-1] if len(lats) > 1 else True
99
+
100
+ if lat_ascending:
101
+ # Lats are ascending (good for origin='lower')
102
+ plot_data = data_values
103
+ else:
104
+ # Lats are descending, flip to match origin='lower'
105
+ plot_data = np.flipud(data_values)
106
+
107
+ im = ax.imshow(plot_data, cmap=color_theme, vmin=vmin, vmax=vmax,
108
+ extent=extent, origin='lower', aspect='auto', # Changed to 'auto' to match interactive plot
109
+ interpolation='nearest', zorder=1)
110
 
111
  # Auto-adjust bounds if INDIA_BOUNDS is too small or wrong
112
  xmin, ymin, xmax, ymax = self.india_map.total_bounds
 
120
  linewidth=0.8, zorder=2) # <-- CHANGED: Set zorder=2 (foreground)
121
 
122
  # Add colorbar
123
+ cbar = plt.colorbar(im, ax=ax, shrink=0.6, pad=0.02, aspect=30)
124
  cbar_label = f"{display_name}" + (f" ({units})" if units else "")
125
  cbar.set_label(cbar_label, fontsize=12, labelpad=15)
126
 
templates/gallery.html ADDED
@@ -0,0 +1,464 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>Plot Gallery - CAMS Air Pollution Dashboard</title>
7
+ <style>
8
+ * {
9
+ margin: 0;
10
+ padding: 0;
11
+ box-sizing: border-box;
12
+ }
13
+
14
+ body {
15
+ font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
16
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
17
+ min-height: 100vh;
18
+ padding: 20px;
19
+ }
20
+
21
+ .container {
22
+ max-width: 1400px;
23
+ margin: 0 auto;
24
+ }
25
+
26
+ .header {
27
+ background: rgba(255, 255, 255, 0.95);
28
+ border-radius: 15px;
29
+ padding: 20px;
30
+ margin-bottom: 20px;
31
+ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
32
+ backdrop-filter: blur(4px);
33
+ border: 1px solid rgba(255, 255, 255, 0.18);
34
+ }
35
+
36
+ .header h1 {
37
+ color: #2c3e50;
38
+ text-align: center;
39
+ margin-bottom: 10px;
40
+ }
41
+
42
+ .controls {
43
+ display: flex;
44
+ gap: 15px;
45
+ justify-content: center;
46
+ margin-bottom: 20px;
47
+ flex-wrap: wrap;
48
+ }
49
+
50
+ .btn {
51
+ background: linear-gradient(45deg, #3498db, #2980b9);
52
+ color: white;
53
+ padding: 12px 24px;
54
+ border: none;
55
+ border-radius: 25px;
56
+ text-decoration: none;
57
+ font-weight: 600;
58
+ transition: all 0.3s ease;
59
+ cursor: pointer;
60
+ font-size: 14px;
61
+ }
62
+
63
+ .btn:hover {
64
+ transform: translateY(-2px);
65
+ box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4);
66
+ }
67
+
68
+ .btn-secondary {
69
+ background: linear-gradient(45deg, #95a5a6, #7f8c8d);
70
+ }
71
+
72
+ .btn-danger {
73
+ background: linear-gradient(45deg, #e74c3c, #c0392b);
74
+ }
75
+
76
+ .section {
77
+ background: rgba(255, 255, 255, 0.95);
78
+ border-radius: 15px;
79
+ padding: 20px;
80
+ margin-bottom: 20px;
81
+ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
82
+ backdrop-filter: blur(4px);
83
+ border: 1px solid rgba(255, 255, 255, 0.18);
84
+ }
85
+
86
+ .section h2 {
87
+ color: #2c3e50;
88
+ margin-bottom: 20px;
89
+ display: flex;
90
+ align-items: center;
91
+ gap: 10px;
92
+ }
93
+
94
+ .plots-grid {
95
+ display: grid;
96
+ grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
97
+ gap: 20px;
98
+ }
99
+
100
+ .plot-card {
101
+ background: white;
102
+ border-radius: 10px;
103
+ padding: 15px;
104
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
105
+ transition: all 0.3s ease;
106
+ border: 2px solid transparent;
107
+ }
108
+
109
+ .plot-card:hover {
110
+ transform: translateY(-5px);
111
+ box-shadow: 0 8px 25px rgba(0, 0, 0, 0.15);
112
+ border-color: #3498db;
113
+ }
114
+
115
+ .plot-preview {
116
+ width: 100%;
117
+ height: 200px;
118
+ background: #f8f9fa;
119
+ border-radius: 8px;
120
+ margin-bottom: 15px;
121
+ display: flex;
122
+ align-items: center;
123
+ justify-content: center;
124
+ overflow: hidden;
125
+ position: relative;
126
+ }
127
+
128
+ .plot-preview img {
129
+ max-width: 100%;
130
+ max-height: 100%;
131
+ object-fit: cover;
132
+ border-radius: 8px;
133
+ }
134
+
135
+ .plot-preview.interactive {
136
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
137
+ color: white;
138
+ font-size: 48px;
139
+ }
140
+
141
+ .plot-info {
142
+ color: #2c3e50;
143
+ }
144
+
145
+ .plot-info h3 {
146
+ margin-bottom: 10px;
147
+ color: #34495e;
148
+ font-size: 16px;
149
+ }
150
+
151
+ .plot-meta {
152
+ display: grid;
153
+ grid-template-columns: 1fr 1fr;
154
+ gap: 8px;
155
+ margin-bottom: 15px;
156
+ font-size: 12px;
157
+ color: #7f8c8d;
158
+ }
159
+
160
+ .plot-meta span {
161
+ background: #ecf0f1;
162
+ padding: 4px 8px;
163
+ border-radius: 4px;
164
+ }
165
+
166
+ .plot-actions {
167
+ display: flex;
168
+ gap: 8px;
169
+ justify-content: space-between;
170
+ }
171
+
172
+ .btn-small {
173
+ padding: 6px 12px;
174
+ font-size: 12px;
175
+ border-radius: 15px;
176
+ text-decoration: none;
177
+ font-weight: 500;
178
+ }
179
+
180
+ .btn-view {
181
+ background: linear-gradient(45deg, #27ae60, #219a52);
182
+ color: white;
183
+ }
184
+
185
+ .btn-download {
186
+ background: linear-gradient(45deg, #f39c12, #e67e22);
187
+ color: white;
188
+ }
189
+
190
+ .btn-delete {
191
+ background: linear-gradient(45deg, #e74c3c, #c0392b);
192
+ color: white;
193
+ }
194
+
195
+ .stats {
196
+ display: grid;
197
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
198
+ gap: 15px;
199
+ margin-bottom: 20px;
200
+ }
201
+
202
+ .stat-card {
203
+ background: rgba(52, 152, 219, 0.1);
204
+ padding: 15px;
205
+ border-radius: 10px;
206
+ text-align: center;
207
+ border-left: 4px solid #3498db;
208
+ }
209
+
210
+ .stat-number {
211
+ font-size: 24px;
212
+ font-weight: bold;
213
+ color: #2980b9;
214
+ }
215
+
216
+ .stat-label {
217
+ color: #7f8c8d;
218
+ font-size: 12px;
219
+ margin-top: 5px;
220
+ }
221
+
222
+ .empty-state {
223
+ text-align: center;
224
+ padding: 40px;
225
+ color: #7f8c8d;
226
+ }
227
+
228
+ .empty-state h3 {
229
+ margin-bottom: 10px;
230
+ color: #95a5a6;
231
+ }
232
+
233
+ @media (max-width: 768px) {
234
+ .plots-grid {
235
+ grid-template-columns: 1fr;
236
+ }
237
+
238
+ .controls {
239
+ justify-content: center;
240
+ }
241
+
242
+ .btn {
243
+ padding: 10px 20px;
244
+ font-size: 12px;
245
+ }
246
+ }
247
+ </style>
248
+ </head>
249
+ <body>
250
+ <div class="container">
251
+ <div class="header">
252
+ <h1>πŸ“Š Plot Gallery</h1>
253
+ <p style="text-align: center; color: #7f8c8d; margin-top: 10px;">
254
+ View and manage all your saved pollution maps
255
+ </p>
256
+ </div>
257
+
258
+ <div class="controls">
259
+ <a href="{{ url_for('index') }}" class="btn btn-secondary">← Back to Dashboard</a>
260
+ <a href="{{ url_for('cleanup') }}" class="btn btn-danger"
261
+ onclick="return confirm('This will delete plots older than 24 hours. Continue?')">
262
+ 🧹 Clean Old Plots
263
+ </a>
264
+ </div>
265
+
266
+ <!-- Statistics -->
267
+ <div class="section">
268
+ <div class="stats">
269
+ <div class="stat-card">
270
+ <div class="stat-number">{{ total_plots }}</div>
271
+ <div class="stat-label">Total Plots</div>
272
+ </div>
273
+ <div class="stat-card">
274
+ <div class="stat-number">{{ static_plots|length }}</div>
275
+ <div class="stat-label">Static Plots</div>
276
+ </div>
277
+ <div class="stat-card">
278
+ <div class="stat-number">{{ interactive_plots|length }}</div>
279
+ <div class="stat-label">Interactive Plots</div>
280
+ </div>
281
+ </div>
282
+ </div>
283
+
284
+ <!-- Interactive Plots Section -->
285
+ <div class="section">
286
+ <h2>🎯 Interactive Plots</h2>
287
+ {% if interactive_plots %}
288
+ <div class="plots-grid">
289
+ {% for plot in interactive_plots %}
290
+ <div class="plot-card">
291
+ <div class="plot-preview interactive">
292
+ 🌍
293
+ </div>
294
+ <div class="plot-info">
295
+ <h3>{{ plot.variable|title }} - {{ plot.region|title }}</h3>
296
+ <div class="plot-meta">
297
+ <span>πŸ“… {{ plot.created.strftime('%Y-%m-%d %H:%M') }}</span>
298
+ <span>🎨 {{ plot.theme|title }}</span>
299
+ <span>πŸ“ {{ "%.1f"|format(plot.size/1024) }} KB</span>
300
+ <span>🌐 Interactive</span>
301
+ </div>
302
+ <div class="plot-actions">
303
+ <a href="{{ url_for('view_interactive_plot', filename=plot.filename) }}"
304
+ class="btn-small btn-view">πŸ‘οΈ View</a>
305
+ <a href="{{ url_for('serve_plot', filename=plot.filename) }}"
306
+ class="btn-small btn-download" download>πŸ’Ύ Download</a>
307
+ <button onclick="deletePlot('{{ plot.filename }}')"
308
+ class="btn-small btn-delete">πŸ—‘οΈ Delete</button>
309
+ </div>
310
+ </div>
311
+ </div>
312
+ {% endfor %}
313
+ </div>
314
+ {% else %}
315
+ <div class="empty-state">
316
+ <h3>No Interactive Plots Yet</h3>
317
+ <p>Create your first interactive plot from the dashboard!</p>
318
+ </div>
319
+ {% endif %}
320
+ </div>
321
+
322
+ <!-- Static Plots Section -->
323
+ <div class="section">
324
+ <h2>πŸ“Š Static Plots</h2>
325
+ {% if static_plots %}
326
+ <div class="plots-grid">
327
+ {% for plot in static_plots %}
328
+ <div class="plot-card">
329
+ <div class="plot-preview">
330
+ <img src="{{ url_for('serve_plot', filename=plot.filename) }}"
331
+ alt="{{ plot.variable }} plot"
332
+ onerror="this.style.display='none'; this.parentElement.innerHTML='<div style=\'color: #e74c3c; text-align: center;\'>πŸ“·<br>Preview unavailable</div>'">
333
+ </div>
334
+ <div class="plot-info">
335
+ <h3>{{ plot.variable|title }} - {{ plot.region|title }}</h3>
336
+ <div class="plot-meta">
337
+ <span>πŸ“… {{ plot.created.strftime('%Y-%m-%d %H:%M') }}</span>
338
+ <span>🎨 {{ plot.theme|title }}</span>
339
+ <span>πŸ“ {{ "%.1f"|format(plot.size/1024) }} KB</span>
340
+ <span>πŸ–ΌοΈ {{ plot.extension.upper() }}</span>
341
+ </div>
342
+ <div class="plot-actions">
343
+ <a href="{{ url_for('serve_plot', filename=plot.filename) }}"
344
+ class="btn-small btn-view" target="_blank">πŸ‘οΈ View</a>
345
+ <a href="{{ url_for('serve_plot', filename=plot.filename) }}"
346
+ class="btn-small btn-download" download>πŸ’Ύ Download</a>
347
+ <button onclick="deletePlot('{{ plot.filename }}')"
348
+ class="btn-small btn-delete">πŸ—‘οΈ Delete</button>
349
+ </div>
350
+ </div>
351
+ </div>
352
+ {% endfor %}
353
+ </div>
354
+ {% else %}
355
+ <div class="empty-state">
356
+ <h3>No Static Plots Yet</h3>
357
+ <p>Generate your first static plot from the dashboard!</p>
358
+ </div>
359
+ {% endif %}
360
+ </div>
361
+ </div>
362
+
363
+ <script>
364
+ // Add loading indicators for actions
365
+ document.addEventListener('DOMContentLoaded', function() {
366
+ const viewButtons = document.querySelectorAll('.btn-view');
367
+ viewButtons.forEach(button => {
368
+ button.addEventListener('click', function() {
369
+ const originalText = this.textContent;
370
+ this.textContent = '⏳ Loading...';
371
+
372
+ // Reset after a delay if still on page
373
+ setTimeout(() => {
374
+ this.textContent = originalText;
375
+ }, 3000);
376
+ });
377
+ });
378
+
379
+ const downloadButtons = document.querySelectorAll('.btn-download');
380
+ downloadButtons.forEach(button => {
381
+ button.addEventListener('click', function() {
382
+ const originalText = this.textContent;
383
+ this.textContent = 'πŸ“₯ Downloading...';
384
+
385
+ setTimeout(() => {
386
+ this.textContent = originalText;
387
+ }, 2000);
388
+ });
389
+ });
390
+
391
+ // Delete plot functionality - fixed to work properly with onclick
392
+ window.deletePlot = function(filename) {
393
+ if (confirm(`Are you sure you want to delete "${filename}"? This action cannot be undone.`)) {
394
+ // Find the button that was clicked by searching for the button with this filename
395
+ const buttons = document.querySelectorAll('.btn-delete');
396
+ let button = null;
397
+
398
+ buttons.forEach(btn => {
399
+ if (btn.getAttribute('onclick').includes(filename)) {
400
+ button = btn;
401
+ }
402
+ });
403
+
404
+ if (!button) {
405
+ alert('Error: Could not find the delete button');
406
+ return;
407
+ }
408
+
409
+ const originalText = button.textContent;
410
+ button.textContent = 'πŸ—‘οΈ Deleting...';
411
+ button.disabled = true;
412
+
413
+ fetch(`/delete_plot/${filename}`, {
414
+ method: 'DELETE',
415
+ headers: {
416
+ 'Content-Type': 'application/json',
417
+ }
418
+ })
419
+ .then(response => {
420
+ console.log('Delete response status:', response.status);
421
+ if (!response.ok) {
422
+ throw new Error(`HTTP error! status: ${response.status}`);
423
+ }
424
+ return response.json();
425
+ })
426
+ .then(data => {
427
+ console.log('Delete response data:', data);
428
+ if (data.success) {
429
+ // Remove the plot card from the DOM
430
+ button.closest('.plot-card').remove();
431
+
432
+ // Show success message
433
+ const successMsg = document.createElement('div');
434
+ successMsg.style.cssText = `
435
+ position: fixed; top: 20px; right: 20px; z-index: 10000;
436
+ background: #27ae60; color: white; padding: 15px 25px;
437
+ border-radius: 8px; font-weight: bold; box-shadow: 0 4px 12px rgba(0,0,0,0.3);
438
+ `;
439
+ successMsg.textContent = `βœ… ${filename} deleted successfully`;
440
+ document.body.appendChild(successMsg);
441
+
442
+ setTimeout(() => {
443
+ successMsg.remove();
444
+ // Reload page to update statistics
445
+ window.location.reload();
446
+ }, 2000);
447
+ } else {
448
+ alert(`Failed to delete plot: ${data.error}`);
449
+ button.textContent = originalText;
450
+ button.disabled = false;
451
+ }
452
+ })
453
+ .catch(error => {
454
+ console.error('Error:', error);
455
+ alert(`An error occurred while deleting the plot: ${error.message}`);
456
+ button.textContent = originalText;
457
+ button.disabled = false;
458
+ });
459
+ }
460
+ }
461
+ });
462
+ </script>
463
+ </body>
464
+ </html>
templates/index.html CHANGED
@@ -155,6 +155,12 @@
155
  <div class="container">
156
  <h1>🌍 CAMS Air Pollution Visualization</h1>
157
 
 
 
 
 
 
 
158
  {% with messages = get_flashed_messages(with_categories=true) %}
159
  {% if messages %}
160
  {% for category, message in messages %}
 
155
  <div class="container">
156
  <h1>🌍 CAMS Air Pollution Visualization</h1>
157
 
158
+ <div style="text-align: center; margin-bottom: 20px;">
159
+ <a href="{{ url_for('gallery') }}" class="btn" style="font-size: 14px; padding: 8px 16px; margin-right: 10px;">
160
+ πŸ“Š View Plot Gallery
161
+ </a>
162
+ </div>
163
+
164
  {% with messages = get_flashed_messages(with_categories=true) %}
165
  {% if messages %}
166
  {% for category, message in messages %}
templates/view_interactive.html ADDED
@@ -0,0 +1,233 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>View Interactive Plot - 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
+ padding: 20px;
20
+ }
21
+
22
+ .container {
23
+ max-width: 1400px;
24
+ margin: 0 auto;
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
+ text-align: center;
36
+ }
37
+
38
+ .header h1 {
39
+ color: #2c3e50;
40
+ margin-bottom: 10px;
41
+ }
42
+
43
+ .controls {
44
+ display: flex;
45
+ gap: 15px;
46
+ justify-content: center;
47
+ margin-bottom: 20px;
48
+ flex-wrap: wrap;
49
+ }
50
+
51
+ .btn {
52
+ background: linear-gradient(45deg, #3498db, #2980b9);
53
+ color: white;
54
+ padding: 12px 24px;
55
+ border: none;
56
+ border-radius: 25px;
57
+ text-decoration: none;
58
+ font-weight: 600;
59
+ transition: all 0.3s ease;
60
+ cursor: pointer;
61
+ font-size: 14px;
62
+ }
63
+
64
+ .btn:hover {
65
+ transform: translateY(-2px);
66
+ box-shadow: 0 8px 25px rgba(52, 152, 219, 0.4);
67
+ }
68
+
69
+ .btn-secondary {
70
+ background: linear-gradient(45deg, #95a5a6, #7f8c8d);
71
+ }
72
+
73
+ .btn-download {
74
+ background: linear-gradient(45deg, #27ae60, #219a52);
75
+ }
76
+
77
+ .plot-container {
78
+ background: rgba(255, 255, 255, 0.95);
79
+ border-radius: 15px;
80
+ padding: 20px;
81
+ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
82
+ backdrop-filter: blur(4px);
83
+ border: 1px solid rgba(255, 255, 255, 0.18);
84
+ }
85
+
86
+ .plot-content {
87
+ border-radius: 10px;
88
+ overflow: hidden;
89
+ box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
90
+ }
91
+
92
+ .plot-info-section {
93
+ background: rgba(255, 255, 255, 0.95);
94
+ border-radius: 15px;
95
+ padding: 20px;
96
+ margin-top: 20px;
97
+ box-shadow: 0 8px 32px rgba(31, 38, 135, 0.37);
98
+ backdrop-filter: blur(4px);
99
+ border: 1px solid rgba(255, 255, 255, 0.18);
100
+ }
101
+
102
+ .info-grid {
103
+ display: grid;
104
+ grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
105
+ gap: 15px;
106
+ }
107
+
108
+ .info-item {
109
+ background: rgba(52, 73, 94, 0.1);
110
+ padding: 15px;
111
+ border-radius: 10px;
112
+ border-left: 4px solid #3498db;
113
+ }
114
+
115
+ .info-item h3 {
116
+ color: #2c3e50;
117
+ margin-bottom: 5px;
118
+ font-size: 14px;
119
+ font-weight: 600;
120
+ }
121
+
122
+ .info-item p {
123
+ color: #34495e;
124
+ font-size: 16px;
125
+ font-weight: 500;
126
+ }
127
+
128
+ @media (max-width: 768px) {
129
+ .container {
130
+ padding: 10px;
131
+ }
132
+
133
+ .controls {
134
+ justify-content: center;
135
+ }
136
+
137
+ .btn {
138
+ padding: 10px 20px;
139
+ font-size: 12px;
140
+ }
141
+ }
142
+ </style>
143
+ </head>
144
+ <body>
145
+ <div class="container">
146
+ <div class="header">
147
+ <h1>🌍 Interactive Plot Viewer</h1>
148
+ <p style="color: #7f8c8d; margin-top: 10px;">
149
+ Viewing saved interactive plot: {{ plot_info.filename }}
150
+ </p>
151
+ </div>
152
+
153
+ <div class="controls">
154
+ <a href="{{ url_for('gallery') }}" class="btn btn-secondary">← Back to Gallery</a>
155
+ <a href="{{ url_for('index') }}" class="btn btn-secondary">🏠 Dashboard</a>
156
+ <a href="{{ url_for('serve_plot', filename=plot_info.filename) }}"
157
+ class="btn btn-download" download>πŸ“₯ Download HTML</a>
158
+ </div>
159
+
160
+ <div class="plot-container">
161
+ <div class="plot-content">
162
+ {{ plot_html|safe }}
163
+ </div>
164
+ </div>
165
+
166
+ <div class="plot-info-section">
167
+ <h2 style="color: #2c3e50; margin-bottom: 20px;">πŸ“Š Plot Information</h2>
168
+
169
+ <div class="info-grid">
170
+ <div class="info-item">
171
+ <h3>πŸ§ͺ Variable</h3>
172
+ <p>{{ plot_info.variable }}</p>
173
+ </div>
174
+
175
+ <div class="info-item">
176
+ <h3>⏰ Generated</h3>
177
+ <p>{{ plot_info.generated_time }}</p>
178
+ </div>
179
+
180
+ <div class="info-item">
181
+ <h3>πŸ“ Filename</h3>
182
+ <p>{{ plot_info.filename }}</p>
183
+ </div>
184
+
185
+ <div class="info-item">
186
+ <h3>🎯 Type</h3>
187
+ <p>Interactive Plot</p>
188
+ </div>
189
+ </div>
190
+ </div>
191
+ </div>
192
+
193
+ <script>
194
+ // Enhanced interactivity for saved plots
195
+ document.addEventListener('DOMContentLoaded', function() {
196
+ console.log('Viewing saved interactive plot');
197
+
198
+ // Check if Plotly is loaded
199
+ if (typeof Plotly === 'undefined') {
200
+ console.error('Plotly is not loaded!');
201
+ return;
202
+ }
203
+
204
+ // Find the plotly div
205
+ const plotDiv = document.querySelector('.plotly-graph-div');
206
+
207
+ if (plotDiv) {
208
+ console.log('Found Plotly plot');
209
+
210
+ // Add responsive behavior
211
+ window.addEventListener('resize', function() {
212
+ Plotly.Plots.resize(plotDiv);
213
+ });
214
+ }
215
+
216
+ // Add loading indicators for download
217
+ const downloadBtn = document.querySelector('.btn-download');
218
+ if (downloadBtn) {
219
+ downloadBtn.addEventListener('click', function() {
220
+ const originalText = this.textContent;
221
+ this.textContent = '⏳ Preparing download...';
222
+ this.style.pointerEvents = 'none';
223
+
224
+ setTimeout(() => {
225
+ this.textContent = originalText;
226
+ this.style.pointerEvents = 'auto';
227
+ }, 2000);
228
+ });
229
+ }
230
+ });
231
+ </script>
232
+ </body>
233
+ </html>