ncplot 0.3.12__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ncplot
3
- Version: 0.3.12
3
+ Version: 0.4.0
4
4
  Summary: Interactive viewing of NetCDF data
5
5
  Home-page: https://github.com/pmlmodelling/ncplot
6
6
  Author: Robert Wilson
@@ -2,6 +2,7 @@ import sys
2
2
  import warnings
3
3
  from threading import Thread
4
4
 
5
+ import glob
5
6
  import time
6
7
  import holoviews as hv
7
8
  import panel as pn
@@ -165,6 +166,172 @@ def in_notebook(out=None):
165
166
  return "ipykernel" in sys.modules
166
167
 
167
168
 
169
+ def _looks_like_time_coord(coord, coord_name):
170
+ """
171
+ Check whether a coordinate is likely to represent time.
172
+ """
173
+
174
+ if coord_name is None:
175
+ return False
176
+
177
+ if coord_name == "time" or coord_name.startswith("time"):
178
+ return True
179
+
180
+ if "time" in coord_name:
181
+ return True
182
+
183
+ if coord.attrs.get("standard_name") == "time":
184
+ return True
185
+
186
+ if coord.attrs.get("axis") == "T":
187
+ return True
188
+
189
+ if np.issubdtype(coord.dtype, np.datetime64):
190
+ return True
191
+
192
+ values = coord.values
193
+ if len(values) > 0:
194
+ first_value = values.ravel()[0]
195
+ if hasattr(first_value, "year") and hasattr(first_value, "month"):
196
+ return True
197
+
198
+ return False
199
+
200
+
201
+ def _find_time_coord(ds):
202
+ """
203
+ Identify the most likely time coordinate name for a dataset.
204
+ """
205
+
206
+ df_dims = get_dims(ds)
207
+ time_name = df_dims.time[0]
208
+
209
+ if time_name is not None:
210
+ return time_name
211
+
212
+ time_candidates = []
213
+ for coord_name in list(ds.coords):
214
+ coord = ds[coord_name]
215
+ if _looks_like_time_coord(coord, coord_name):
216
+ time_candidates.append(coord_name)
217
+
218
+ if len(time_candidates) == 1:
219
+ return time_candidates[0]
220
+
221
+ return None
222
+
223
+
224
+ def _combine_datasets(datasets):
225
+ """
226
+ Combine multiple datasets by time or by variables.
227
+ """
228
+
229
+ if len(datasets) == 1:
230
+ return datasets[0]
231
+
232
+ normalized = []
233
+ for ds in datasets:
234
+ time_name = _find_time_coord(ds)
235
+ if time_name is not None and time_name != "time":
236
+ ds = ds.rename({time_name: "time"})
237
+ normalized.append(ds)
238
+
239
+ try:
240
+ return xr.combine_by_coords(normalized)
241
+ except Exception:
242
+ try:
243
+ return xr.merge(normalized, compat="override")
244
+ except Exception:
245
+ return xr.concat(normalized, dim="time")
246
+
247
+
248
+ def _normalize_time_coord(ds):
249
+ """
250
+ Normalize detected time coordinate names to ``time``.
251
+ """
252
+
253
+ time_name = _find_time_coord(ds)
254
+ if time_name is not None and time_name != "time":
255
+ return ds.rename({time_name: "time"})
256
+
257
+ return ds
258
+
259
+
260
+ def _open_multiple_files(files):
261
+ """
262
+ Open multiple files into one dataset.
263
+
264
+ This uses ``open_mfdataset`` first, then falls back to manual per-file
265
+ opening and combining if xarray cannot infer a valid combine strategy.
266
+ """
267
+
268
+ try:
269
+ return xr.open_mfdataset(
270
+ files,
271
+ combine="by_coords",
272
+ preprocess=_normalize_time_coord,
273
+ )
274
+ except Exception:
275
+ datasets = [xr.open_dataset(ff) for ff in files]
276
+ return _combine_datasets(datasets)
277
+
278
+
279
+ def _load_input_as_dataset(x):
280
+ """
281
+ Load a path, path glob, or sequence of paths into a single dataset.
282
+ """
283
+
284
+ if isinstance(x, (list, tuple, set)):
285
+ files = [os.fspath(ff) for ff in x]
286
+ return _open_multiple_files(files)
287
+
288
+ if isinstance(x, str) and glob.has_magic(x):
289
+ files = sorted(glob.glob(x))
290
+ if len(files) == 0:
291
+ raise FileNotFoundError(f"No files matched pattern {x}")
292
+ return _open_multiple_files(files)
293
+
294
+ return None
295
+
296
+
297
+ def _get_color_limits(ds, vars, kwargs):
298
+ """
299
+ Work out the value range to use for a plot.
300
+
301
+ For time-varying plots, this is evaluated across the full dataset so the
302
+ color scale stays consistent between time steps.
303
+ """
304
+
305
+ if "clim" in kwargs:
306
+ clim_min, clim_max = kwargs["clim"]
307
+ if clim_min < 0 < clim_max:
308
+ v_max = float(max(-clim_min, clim_max))
309
+ return (-v_max, v_max)
310
+
311
+ return (float(clim_min), float(clim_max))
312
+
313
+ data = ds[vars]
314
+ data_min = float(data.min(skipna=True).values)
315
+ data_max = float(data.max(skipna=True).values)
316
+
317
+ if data_min < 0 < data_max:
318
+ v_max = float(max(-data_min, data_max))
319
+ return (-v_max, v_max)
320
+
321
+ return (data_min, data_max)
322
+
323
+
324
+ def _apply_color_limits(plot, ds, vars, autoscale, kwargs):
325
+ """
326
+ Apply a stable color range to a plot when autoscaling is enabled.
327
+ """
328
+
329
+ if autoscale:
330
+ return plot.redim.range(**{vars: _get_color_limits(ds, vars, kwargs)})
331
+
332
+ return plot
333
+
334
+
168
335
  def view(x, vars=None, autoscale=True, out=None, **kwargs):
169
336
  """
170
337
  Plot the contents of a NetCDF out
@@ -209,6 +376,10 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
209
376
  if type(x) is xr.core.dataarray.DataArray:
210
377
  x = x.to_dataset()
211
378
 
379
+ loaded_ds = _load_input_as_dataset(x)
380
+ if loaded_ds is not None:
381
+ x = loaded_ds
382
+
212
383
  if type(x) is xr.core.dataset.Dataset:
213
384
  xr_file = True
214
385
  else:
@@ -495,7 +666,7 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
495
666
  coord_list = [x for x in coord_list if x in list(ds.dims)]
496
667
 
497
668
  coord_df = pd.DataFrame(
498
- {"coord": coord_list, "length": [len(ds.coords[x].values) for x in coord_list]}
669
+ {"coord": coord_list, "coord_length": [len(ds.coords[x].values) for x in coord_list]}
499
670
  )
500
671
 
501
672
  # It's possible there are still 2 time variables in the dimensions which could cause problems...
@@ -515,7 +686,7 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
515
686
  if (len(ds[lon_name].values) > 1) and (len(ds[lat_name].values) > 1):
516
687
  spatial_map = True
517
688
 
518
- if len([x for x in coord_df.length if x > 1]) == 1 and spatial_map is False:
689
+ if len([x for x in coord_df.coord_length if x > 1]) == 1 and spatial_map is False:
519
690
 
520
691
  df = ds.to_dataframe()
521
692
  if nc_vars is not None:
@@ -556,12 +727,11 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
556
727
  return None
557
728
 
558
729
  # heat map where 2 coords have more than 1 value, not a spatial map
559
- if len([x for x in coord_df.length if x > 1]) == 2 and spatial_map is False:
730
+ if len([x for x in coord_df.coord_length if x > 1]) == 2 and spatial_map is False:
560
731
 
561
732
  df = ds.to_dataframe().reset_index()
562
- x_var = coord_df.query("length > 1").reset_index().coord[0]
563
- y_var = coord_df.query("length > 1").reset_index().coord[1]
564
-
733
+ x_var = coord_df.query("coord_length > 1").reset_index().coord[0]
734
+ y_var = coord_df.query("coord_length > 1").reset_index().coord[1]
565
735
  selection = [x for x in df.columns if x in vars or x == x_var or x == y_var]
566
736
 
567
737
  df = df.loc[:, selection].melt([x_var, y_var]).drop_duplicates()
@@ -696,18 +866,18 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
696
866
  return None
697
867
 
698
868
  # heat map where 3 coords have more than 1 value, and one of them is time. Not a spatial map though
699
- if len([x for x in coord_df.length if x > 1]) == 3:
869
+ if len([x for x in coord_df.coord_length if x > 1]) == 3:
700
870
 
701
871
  non_map = True
702
872
 
703
873
  if lon_name is not None and lat_name is not None:
704
874
  if (lon_name is not None) and (lon_name in list(ds.coords)):
705
- lons = int(coord_df.query("coord == @lon_name").length)
875
+ lons = int(coord_df.query("coord == @lon_name").coord_length.values[0])
706
876
  else:
707
877
  lons = 0
708
878
 
709
879
  if (lat_name) is not None and (lat_name in list(ds.coords)):
710
- lats = int(coord_df.query("coord == @lat_name").length)
880
+ lats = int(coord_df.query("coord == @lat_name").coord_length.values[0])
711
881
  else:
712
882
  lats = 0
713
883
 
@@ -725,7 +895,7 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
725
895
 
726
896
  if time_name in coord_list and time_in and non_map:
727
897
 
728
- if coord_df.query("coord == @time_name").length.values > 1:
898
+ if coord_df.query("coord == @time_name").coord_length.values[0] > 1:
729
899
 
730
900
  df = ds.to_dataframe().reset_index()
731
901
  for x in list(ds.coords):
@@ -964,7 +1134,8 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
964
1134
  #responsive=(in_notebook() is False),
965
1135
  responsive = False,
966
1136
  **kwargs,
967
- ).redim.range(**{vars: (-v_max, v_max)})
1137
+ )
1138
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
968
1139
  else:
969
1140
  intplot = ds.hvplot.quadmesh(
970
1141
  lon_name,
@@ -979,6 +1150,7 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
979
1150
  responsive = False,
980
1151
  **kwargs,
981
1152
  )
1153
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
982
1154
  else:
983
1155
  if coastline:
984
1156
  coastline = get_coastline(ds, lon_name, lat_name)
@@ -1003,7 +1175,8 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
1003
1175
  #responsive=(in_notebook() is False),
1004
1176
  responsive = False,
1005
1177
  **kwargs,
1006
- ).redim.range(**{vars: (-v_max, v_max)})
1178
+ )
1179
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
1007
1180
  else:
1008
1181
  intplot = ds.hvplot.image(
1009
1182
  lon_name,
@@ -1055,7 +1228,8 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
1055
1228
  #responsive=(in_notebook() is False),
1056
1229
  responsive = False,
1057
1230
  **kwargs,
1058
- ).redim.range(**{vars: (self_min.values, v_max)})
1231
+ )
1232
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
1059
1233
  else:
1060
1234
  intplot = ds.hvplot.quadmesh(
1061
1235
  lon_name,
@@ -1070,6 +1244,7 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
1070
1244
  responsive = False,
1071
1245
  **kwargs,
1072
1246
  )
1247
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
1073
1248
 
1074
1249
  else:
1075
1250
 
@@ -1096,7 +1271,8 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
1096
1271
  #responsive=(in_notebook() is False),
1097
1272
  responsive = False,
1098
1273
  **kwargs,
1099
- ).redim.range(**{vars: (self_min.values, v_max)})
1274
+ )
1275
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
1100
1276
  else:
1101
1277
  intplot = ds.hvplot.image(
1102
1278
  lon_name,
@@ -1111,6 +1287,7 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
1111
1287
  responsive = False,
1112
1288
  **kwargs,
1113
1289
  )
1290
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
1114
1291
 
1115
1292
  if in_notebook(out):
1116
1293
  if out is None:
@@ -37,7 +37,7 @@ extras_require: dict() = {
37
37
  extras_require["complete"] = ["geoviews"]
38
38
 
39
39
  setup(name='ncplot',
40
- version='0.3.12',
40
+ version='0.4.0',
41
41
  description=DESCRIPTION,
42
42
  long_description=long_description,
43
43
  long_description_content_type='text/markdown',
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes