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.
- {ncplot-0.3.12 → ncplot-0.4.0}/PKG-INFO +1 -1
- {ncplot-0.3.12 → ncplot-0.4.0}/ncplot/plot.py +191 -14
- {ncplot-0.3.12 → ncplot-0.4.0}/setup.py +1 -1
- {ncplot-0.3.12 → ncplot-0.4.0}/LICENSE +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/MANIFEST.in +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/README.md +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/ncplot/__init__.py +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/ncplot/command_line.py +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/ncplot/deprecated.py +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/ncplot/utils.py +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/ncplot/xarray.py +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/ncplot.egg-info/SOURCES.txt +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/pyproject.toml +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/requirements.txt +0 -0
- {ncplot-0.3.12 → ncplot-0.4.0}/setup.cfg +0 -0
|
@@ -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, "
|
|
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.
|
|
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.
|
|
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("
|
|
563
|
-
y_var = coord_df.query("
|
|
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.
|
|
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").
|
|
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").
|
|
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").
|
|
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
|
-
)
|
|
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
|
-
)
|
|
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
|
-
)
|
|
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
|
-
)
|
|
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:
|
|
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
|
|
File without changes
|