Nipun Claude commited on
Commit
3aaaab0
Β·
1 Parent(s): aeedf73

Add awesome NetCDF Explorer Gradio app

Browse files

- Interactive NetCDF file visualization with multiple plot types
- 2D heatmaps with customizable colormaps and dimension slicing
- Time series analysis with spatial aggregation options
- Vertical profile plots for atmospheric/oceanic data
- Comprehensive metadata analysis and variable exploration
- Modern Gradio interface with responsive controls
- Support for complex multi-dimensional NetCDF datasets

πŸ€– Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

Files changed (2) hide show
  1. app.py +445 -0
  2. requirements.txt +10 -0
app.py ADDED
@@ -0,0 +1,445 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import xarray as xr
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import matplotlib.patches as patches
6
+ import plotly.express as px
7
+ import plotly.graph_objects as go
8
+ from plotly.subplots import make_subplots
9
+ import pandas as pd
10
+ import tempfile
11
+ import os
12
+ from typing import Optional, Tuple, Dict, Any
13
+
14
+ # Set matplotlib backend
15
+ plt.switch_backend('Agg')
16
+
17
+ def analyze_netcdf(file_path: str) -> Tuple[str, Dict[str, Any]]:
18
+ """Analyze NetCDF file and extract metadata."""
19
+ try:
20
+ ds = xr.open_dataset(file_path)
21
+
22
+ # Basic info
23
+ info = {
24
+ 'dimensions': dict(ds.dims),
25
+ 'variables': list(ds.data_vars.keys()),
26
+ 'coordinates': list(ds.coords.keys()),
27
+ 'attrs': dict(ds.attrs),
28
+ 'data_vars_info': {}
29
+ }
30
+
31
+ # Detailed variable information
32
+ for var in ds.data_vars:
33
+ var_info = {
34
+ 'shape': ds[var].shape,
35
+ 'dtype': str(ds[var].dtype),
36
+ 'dims': ds[var].dims,
37
+ 'attrs': dict(ds[var].attrs),
38
+ 'min': float(ds[var].min().values) if ds[var].size > 0 else None,
39
+ 'max': float(ds[var].max().values) if ds[var].size > 0 else None,
40
+ 'mean': float(ds[var].mean().values) if ds[var].size > 0 else None
41
+ }
42
+ info['data_vars_info'][var] = var_info
43
+
44
+ # Generate summary text
45
+ summary = f"""
46
+ ## Dataset Overview
47
+ - **Dimensions**: {len(ds.dims)} ({', '.join([f"{k}: {v}" for k, v in ds.dims.items()])})
48
+ - **Variables**: {len(ds.data_vars)} data variables, {len(ds.coords)} coordinates
49
+ - **Global Attributes**: {len(ds.attrs)} attributes
50
+
51
+ ### Variables:
52
+ """
53
+ for var, var_info in info['data_vars_info'].items():
54
+ summary += f"- **{var}**: {var_info['shape']} ({var_info['dtype']})"
55
+ if var_info['min'] is not None:
56
+ summary += f" [{var_info['min']:.2f} to {var_info['max']:.2f}]"
57
+ summary += "\n"
58
+
59
+ ds.close()
60
+ return summary, info
61
+
62
+ except Exception as e:
63
+ return f"Error analyzing file: {str(e)}", {}
64
+
65
+ def create_2d_plot(file_path: str, variable: str, time_idx: int = 0, level_idx: int = 0,
66
+ colormap: str = "viridis") -> go.Figure:
67
+ """Create 2D visualization of NetCDF data."""
68
+ try:
69
+ ds = xr.open_dataset(file_path)
70
+
71
+ if variable not in ds.data_vars:
72
+ raise ValueError(f"Variable '{variable}' not found in dataset")
73
+
74
+ data_var = ds[variable]
75
+
76
+ # Handle different dimensional data
77
+ if len(data_var.dims) >= 2:
78
+ # Find spatial dimensions (usually lat/lon or x/y)
79
+ spatial_dims = []
80
+ for dim in data_var.dims:
81
+ if any(name in dim.lower() for name in ['lat', 'lon', 'x', 'y']):
82
+ spatial_dims.append(dim)
83
+
84
+ if len(spatial_dims) >= 2:
85
+ # Use the last two spatial dimensions
86
+ dim1, dim2 = spatial_dims[-2:]
87
+
88
+ # Select subset based on other dimensions
89
+ data_subset = data_var
90
+ for dim in data_var.dims:
91
+ if dim not in [dim1, dim2]:
92
+ if 'time' in dim.lower():
93
+ data_subset = data_subset.isel({dim: min(time_idx, data_var.sizes[dim]-1)})
94
+ elif any(name in dim.lower() for name in ['level', 'depth', 'height']):
95
+ data_subset = data_subset.isel({dim: min(level_idx, data_var.sizes[dim]-1)})
96
+ else:
97
+ data_subset = data_subset.isel({dim: 0})
98
+ else:
99
+ # Use first two dimensions
100
+ dims = list(data_var.dims)
101
+ if len(dims) >= 2:
102
+ data_subset = data_var.isel({dim: 0 for dim in dims[2:]})
103
+ else:
104
+ data_subset = data_var
105
+ dim1, dim2 = dims[:2]
106
+ else:
107
+ raise ValueError("Data must have at least 2 dimensions for 2D plotting")
108
+
109
+ # Create the plot
110
+ fig = go.Figure(data=go.Heatmap(
111
+ z=data_subset.values,
112
+ x=data_subset.coords[dim2].values if dim2 in data_subset.coords else None,
113
+ y=data_subset.coords[dim1].values if dim1 in data_subset.coords else None,
114
+ colorscale=colormap,
115
+ colorbar=dict(title=data_var.attrs.get('units', 'Value'))
116
+ ))
117
+
118
+ fig.update_layout(
119
+ title=f"{variable} - {data_var.attrs.get('long_name', variable)}",
120
+ xaxis_title=dim2,
121
+ yaxis_title=dim1,
122
+ height=600,
123
+ width=800
124
+ )
125
+
126
+ ds.close()
127
+ return fig
128
+
129
+ except Exception as e:
130
+ # Return empty figure with error message
131
+ fig = go.Figure()
132
+ fig.add_annotation(
133
+ text=f"Error creating plot: {str(e)}",
134
+ x=0.5, y=0.5,
135
+ xref="paper", yref="paper",
136
+ showarrow=False,
137
+ font=dict(size=16, color="red")
138
+ )
139
+ return fig
140
+
141
+ def create_time_series(file_path: str, variable: str, method: str = "mean") -> go.Figure:
142
+ """Create time series plot by aggregating spatial dimensions."""
143
+ try:
144
+ ds = xr.open_dataset(file_path)
145
+
146
+ if variable not in ds.data_vars:
147
+ raise ValueError(f"Variable '{variable}' not found in dataset")
148
+
149
+ data_var = ds[variable]
150
+
151
+ # Find time dimension
152
+ time_dim = None
153
+ for dim in data_var.dims:
154
+ if 'time' in dim.lower():
155
+ time_dim = dim
156
+ break
157
+
158
+ if time_dim is None:
159
+ raise ValueError("No time dimension found in the data")
160
+
161
+ # Aggregate spatial dimensions
162
+ spatial_dims = [dim for dim in data_var.dims if dim != time_dim]
163
+
164
+ if method == "mean":
165
+ time_series = data_var.mean(dim=spatial_dims)
166
+ elif method == "max":
167
+ time_series = data_var.max(dim=spatial_dims)
168
+ elif method == "min":
169
+ time_series = data_var.min(dim=spatial_dims)
170
+ else:
171
+ time_series = data_var.mean(dim=spatial_dims)
172
+
173
+ fig = go.Figure(data=go.Scatter(
174
+ x=time_series.coords[time_dim].values,
175
+ y=time_series.values,
176
+ mode='lines+markers',
177
+ name=f"{method.title()} {variable}"
178
+ ))
179
+
180
+ fig.update_layout(
181
+ title=f"Time Series: {method.title()} {variable}",
182
+ xaxis_title="Time",
183
+ yaxis_title=f"{variable} ({data_var.attrs.get('units', 'Value')})",
184
+ height=400
185
+ )
186
+
187
+ ds.close()
188
+ return fig
189
+
190
+ except Exception as e:
191
+ fig = go.Figure()
192
+ fig.add_annotation(
193
+ text=f"Error creating time series: {str(e)}",
194
+ x=0.5, y=0.5,
195
+ xref="paper", yref="paper",
196
+ showarrow=False,
197
+ font=dict(size=16, color="red")
198
+ )
199
+ return fig
200
+
201
+ def create_vertical_profile(file_path: str, variable: str, time_idx: int = 0) -> go.Figure:
202
+ """Create vertical profile plot."""
203
+ try:
204
+ ds = xr.open_dataset(file_path)
205
+
206
+ if variable not in ds.data_vars:
207
+ raise ValueError(f"Variable '{variable}' not found in dataset")
208
+
209
+ data_var = ds[variable]
210
+
211
+ # Find vertical dimension
212
+ vertical_dim = None
213
+ for dim in data_var.dims:
214
+ if any(name in dim.lower() for name in ['level', 'depth', 'height', 'pressure']):
215
+ vertical_dim = dim
216
+ break
217
+
218
+ if vertical_dim is None:
219
+ raise ValueError("No vertical dimension found in the data")
220
+
221
+ # Average over horizontal dimensions, select time
222
+ dims_to_avg = []
223
+ for dim in data_var.dims:
224
+ if dim != vertical_dim:
225
+ if 'time' in dim.lower():
226
+ data_var = data_var.isel({dim: min(time_idx, data_var.sizes[dim]-1)})
227
+ else:
228
+ dims_to_avg.append(dim)
229
+
230
+ if dims_to_avg:
231
+ profile = data_var.mean(dim=dims_to_avg)
232
+ else:
233
+ profile = data_var
234
+
235
+ fig = go.Figure(data=go.Scatter(
236
+ x=profile.values,
237
+ y=profile.coords[vertical_dim].values,
238
+ mode='lines+markers',
239
+ name=variable
240
+ ))
241
+
242
+ fig.update_layout(
243
+ title=f"Vertical Profile: {variable}",
244
+ xaxis_title=f"{variable} ({data_var.attrs.get('units', 'Value')})",
245
+ yaxis_title=vertical_dim,
246
+ height=500
247
+ )
248
+
249
+ ds.close()
250
+ return fig
251
+
252
+ except Exception as e:
253
+ fig = go.Figure()
254
+ fig.add_annotation(
255
+ text=f"Error creating profile: {str(e)}",
256
+ x=0.5, y=0.5,
257
+ xref="paper", yref="paper",
258
+ showarrow=False,
259
+ font=dict(size=16, color="red")
260
+ )
261
+ return fig
262
+
263
+ def process_netcdf_file(file):
264
+ """Process uploaded NetCDF file and return analysis."""
265
+ if file is None:
266
+ return "Please upload a NetCDF file.", {}, [], []
267
+
268
+ try:
269
+ # Save uploaded file temporarily
270
+ with tempfile.NamedTemporaryFile(delete=False, suffix='.nc') as tmp_file:
271
+ tmp_file.write(file.read())
272
+ tmp_path = tmp_file.name
273
+
274
+ # Analyze the file
275
+ summary, info = analyze_netcdf(tmp_path)
276
+
277
+ # Get variable options
278
+ variable_options = list(info.get('data_vars_info', {}).keys())
279
+
280
+ # Get dimension options for slicing
281
+ dimensions = info.get('dimensions', {})
282
+
283
+ return summary, tmp_path, variable_options, list(dimensions.keys())
284
+
285
+ except Exception as e:
286
+ return f"Error processing file: {str(e)}", "", [], []
287
+
288
+ def update_plot(file_path: str, variable: str, plot_type: str, time_idx: int,
289
+ level_idx: int, colormap: str, aggregation_method: str):
290
+ """Update plot based on user selections."""
291
+ if not file_path or not variable:
292
+ return go.Figure()
293
+
294
+ try:
295
+ if plot_type == "2D Heatmap":
296
+ return create_2d_plot(file_path, variable, time_idx, level_idx, colormap)
297
+ elif plot_type == "Time Series":
298
+ return create_time_series(file_path, variable, aggregation_method)
299
+ elif plot_type == "Vertical Profile":
300
+ return create_vertical_profile(file_path, variable, time_idx)
301
+ else:
302
+ return go.Figure()
303
+ except Exception as e:
304
+ fig = go.Figure()
305
+ fig.add_annotation(
306
+ text=f"Error: {str(e)}",
307
+ x=0.5, y=0.5,
308
+ xref="paper", yref="paper",
309
+ showarrow=False
310
+ )
311
+ return fig
312
+
313
+ # Create Gradio interface
314
+ with gr.Blocks(title="NetCDF Explorer 🌍", theme=gr.themes.Soft()) as app:
315
+ gr.Markdown("""
316
+ # 🌍 NetCDF Explorer
317
+
318
+ Upload and explore NetCDF (.nc) files with interactive visualizations!
319
+
320
+ **Features:**
321
+ - πŸ“Š Interactive 2D heatmaps
322
+ - πŸ“ˆ Time series analysis
323
+ - πŸ“‰ Vertical profiles
324
+ - 🎨 Customizable colormaps
325
+ - πŸ“‹ Comprehensive metadata analysis
326
+ """)
327
+
328
+ # File upload section
329
+ with gr.Row():
330
+ file_upload = gr.File(
331
+ label="Upload NetCDF File (.nc)",
332
+ file_types=[".nc", ".netcdf"],
333
+ type="binary"
334
+ )
335
+
336
+ # File analysis section
337
+ with gr.Row():
338
+ file_info = gr.Markdown("Upload a file to see its structure and metadata.")
339
+
340
+ # Control panel
341
+ with gr.Row():
342
+ with gr.Column(scale=1):
343
+ variable_dropdown = gr.Dropdown(
344
+ label="Select Variable",
345
+ choices=[],
346
+ interactive=True
347
+ )
348
+
349
+ plot_type = gr.Radio(
350
+ label="Plot Type",
351
+ choices=["2D Heatmap", "Time Series", "Vertical Profile"],
352
+ value="2D Heatmap"
353
+ )
354
+
355
+ colormap_dropdown = gr.Dropdown(
356
+ label="Colormap",
357
+ choices=["viridis", "plasma", "inferno", "magma", "cividis",
358
+ "Blues", "Reds", "RdYlBu", "RdBu", "coolwarm"],
359
+ value="viridis"
360
+ )
361
+
362
+ aggregation_method = gr.Radio(
363
+ label="Time Series Aggregation",
364
+ choices=["mean", "max", "min"],
365
+ value="mean",
366
+ visible=False
367
+ )
368
+
369
+ with gr.Column(scale=1):
370
+ time_slider = gr.Slider(
371
+ label="Time Index",
372
+ minimum=0,
373
+ maximum=100,
374
+ value=0,
375
+ step=1
376
+ )
377
+
378
+ level_slider = gr.Slider(
379
+ label="Level Index",
380
+ minimum=0,
381
+ maximum=100,
382
+ value=0,
383
+ step=1
384
+ )
385
+
386
+ update_btn = gr.Button("Update Plot", variant="primary")
387
+
388
+ # Plot display
389
+ plot_display = gr.Plot(label="Visualization")
390
+
391
+ # Hidden state to store file path
392
+ file_path_state = gr.State("")
393
+
394
+ # Event handlers
395
+ def on_file_upload(file):
396
+ summary, tmp_path, variables, dimensions = process_netcdf_file(file)
397
+
398
+ # Update UI components
399
+ updates = [
400
+ gr.update(value=summary), # file_info
401
+ gr.update(choices=variables, value=variables[0] if variables else None), # variable_dropdown
402
+ gr.update(value=tmp_path), # file_path_state
403
+ ]
404
+
405
+ return updates
406
+
407
+ def on_plot_type_change(plot_type_val):
408
+ if plot_type_val == "Time Series":
409
+ return gr.update(visible=True)
410
+ else:
411
+ return gr.update(visible=False)
412
+
413
+ def on_update_plot(file_path, variable, plot_type_val, time_idx, level_idx, colormap, agg_method):
414
+ return update_plot(file_path, variable, plot_type_val, int(time_idx), int(level_idx), colormap, agg_method)
415
+
416
+ # Connect event handlers
417
+ file_upload.upload(
418
+ fn=on_file_upload,
419
+ inputs=[file_upload],
420
+ outputs=[file_info, variable_dropdown, file_path_state]
421
+ )
422
+
423
+ plot_type.change(
424
+ fn=on_plot_type_change,
425
+ inputs=[plot_type],
426
+ outputs=[aggregation_method]
427
+ )
428
+
429
+ update_btn.click(
430
+ fn=on_update_plot,
431
+ inputs=[file_path_state, variable_dropdown, plot_type, time_slider,
432
+ level_slider, colormap_dropdown, aggregation_method],
433
+ outputs=[plot_display]
434
+ )
435
+
436
+ # Auto-update on variable change
437
+ variable_dropdown.change(
438
+ fn=on_update_plot,
439
+ inputs=[file_path_state, variable_dropdown, plot_type, time_slider,
440
+ level_slider, colormap_dropdown, aggregation_method],
441
+ outputs=[plot_display]
442
+ )
443
+
444
+ if __name__ == "__main__":
445
+ app.launch()
requirements.txt ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ gradio==5.42.0
2
+ xarray>=2023.1.0
3
+ netcdf4>=1.6.0
4
+ numpy>=1.21.0
5
+ matplotlib>=3.5.0
6
+ plotly>=5.10.0
7
+ pandas>=1.5.0
8
+ scipy>=1.9.0
9
+ h5netcdf>=1.0.0
10
+ dask>=2022.1.0