ncplot 0.3.13__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.13
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:
@@ -963,7 +1134,8 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
963
1134
  #responsive=(in_notebook() is False),
964
1135
  responsive = False,
965
1136
  **kwargs,
966
- ).redim.range(**{vars: (-v_max, v_max)})
1137
+ )
1138
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
967
1139
  else:
968
1140
  intplot = ds.hvplot.quadmesh(
969
1141
  lon_name,
@@ -978,6 +1150,7 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
978
1150
  responsive = False,
979
1151
  **kwargs,
980
1152
  )
1153
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
981
1154
  else:
982
1155
  if coastline:
983
1156
  coastline = get_coastline(ds, lon_name, lat_name)
@@ -1002,7 +1175,8 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
1002
1175
  #responsive=(in_notebook() is False),
1003
1176
  responsive = False,
1004
1177
  **kwargs,
1005
- ).redim.range(**{vars: (-v_max, v_max)})
1178
+ )
1179
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
1006
1180
  else:
1007
1181
  intplot = ds.hvplot.image(
1008
1182
  lon_name,
@@ -1054,7 +1228,8 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
1054
1228
  #responsive=(in_notebook() is False),
1055
1229
  responsive = False,
1056
1230
  **kwargs,
1057
- ).redim.range(**{vars: (self_min.values, v_max)})
1231
+ )
1232
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
1058
1233
  else:
1059
1234
  intplot = ds.hvplot.quadmesh(
1060
1235
  lon_name,
@@ -1069,6 +1244,7 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
1069
1244
  responsive = False,
1070
1245
  **kwargs,
1071
1246
  )
1247
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
1072
1248
 
1073
1249
  else:
1074
1250
 
@@ -1095,7 +1271,8 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
1095
1271
  #responsive=(in_notebook() is False),
1096
1272
  responsive = False,
1097
1273
  **kwargs,
1098
- ).redim.range(**{vars: (self_min.values, v_max)})
1274
+ )
1275
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
1099
1276
  else:
1100
1277
  intplot = ds.hvplot.image(
1101
1278
  lon_name,
@@ -1110,6 +1287,7 @@ def view(x, vars=None, autoscale=True, out=None, **kwargs):
1110
1287
  responsive = False,
1111
1288
  **kwargs,
1112
1289
  )
1290
+ intplot = _apply_color_limits(intplot, ds, vars, autoscale, kwargs)
1113
1291
 
1114
1292
  if in_notebook(out):
1115
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.13',
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