scitools / nc-time-axis Goto Github PK
View Code? Open in Web Editor NEWProvides support for a cftime axis in matplotlib
Home Page: https://nc-time-axis.readthedocs.io/en/stable/
License: BSD 3-Clause "New" or "Revised" License
Provides support for a cftime axis in matplotlib
Home Page: https://nc-time-axis.readthedocs.io/en/stable/
License: BSD 3-Clause "New" or "Revised" License
Hi!
Looks like cftime
1.5.0 breaks nc-time-axis
... I think it is due to Unidata/cftime#235, which removed cftime.utime
...
With my use case, I get (end part of the traceback):
~/work/xclim/xclim/.tox/doctests/lib/python3.7/site-packages/nc_time_axis/__init__.py in convert(cls, value, unit, axis)
286 '`cftime.datetime`.')
287
--> 288 ut = cftime.utime(cls.standard_unit, calendar=first_value.calendar)
289
290 if isinstance(value, (CalendarDateTime, cftime.datetime)):
AttributeError: module 'cftime' has no attribute 'utime'
This is more of a usage question so apologies if this is the incorrect place to ask this. I'm trying to plot some xarray data but am running into issues with dates that have custom 'days since x' units, e.g. adapting the example from the README this code errors for me:
import random
import matplotlib.pyplot as plt
import xarray as xr
import nc_time_axis
import cftime
dates = xr.cftime_range(start='0001', periods=24, freq='MS', calendar='noleap')
c_d_time = [nc_time_axis.CalendarDateTime(item, "noleap") for item in dates]
temperatures = [round(random.uniform(0, 12), 3) for _ in range(len(c_d_time))]
plt.plot(c_d_time, temperatures)
plt.margins(0.1)
plt.ylim(0, 12)
plt.xlabel("Date")
plt.ylabel("Temperature")
plt.show()
ValueError: invalid year provided in cftime.DatetimeNoLeap(0, 10, 23, 2, 24, 0, 8, 3, 296)
Apologies if I've missed something obvious but I would appreciate a pointer on how to deal with dates like this.
I'm adding full docstrings to the main objects in nc-time-axis and noticed that min_n_ticks
, an optional argument for the NetCDFDateTimeLocator
constructor, is set as an attribute, but never used. We may consider removing it.
CalendarDateTime
served a purpose in the early days of nc-time-axis, but I now think it may not be needed anymore, particularly once #75 is addressed (#80). cftime.datetime
instances now have a meaningful calendar
attribute and their comparisons now check that calendars and datetime components are equal. Should we include a DeprecationWarning
in the next release of nc-time-axis?
As an aside, reading some of the existing tests for CalendarDateTime
makes me a little uneasy now that cftime.datetime
objects have a default calendar of "gregorian"
instead of None
:
nc-time-axis/nc_time_axis/tests/unit/test_NetCDFTimeConverter.py
Lines 116 to 121 in 59c567d
calendar
of the datetime object does not match the calendar of the CalendarDateTime
object.In cftime version 1.3.0 cftime.datetime
objects are now calendar-aware. Eventually this will obviate the need for calendar-specific subclasses, like cftime.DatetimeNoLeap
. nc-time-axis currently does not register cftime.datetime
objects in matplotlib's units registry, so matplotlib does not attempt to convert them. It would be great if -- similar to the subclasses -- we could also directly plot the new calendar-aware base class using nc-time-axis.
E.g. while we can currently do:
import random
import cftime
import matplotlib.pyplot as plt
import nc_time_axis
dt = [cftime.DatetimeNoLeap(2017, 2, day) for day in range(1, 28)]
temperatures = [round(random.uniform(0, 12), 3) for _ in range(len(dt))]
plt.plot(dt, temperatures)
It would be great if we could also do:
dt = [cftime.datetime(2017, 2, day, calendar="noleap") for day in range(1, 28)]
temperatures = [round(random.uniform(0, 12), 3) for _ in range(len(dt))]
plt.plot(dt, temperatures)
This currently results in an error, because matplotlib does not convert the dates.
Hi there,
I am trying to use the matplotlib fill_between
graph type but this fails with the latest version of nc-time-axis
Steps to reproduce the behaviour:
nc-time-axis
homepage for plt.plot
but replace plt.plot(cdt, temperatures)
with plt.fill_between(cdt, temperatures,0)
I get this error...
ValueError: The values must be numbers or instances of "nc_time_axis.CalendarDateTime" or "cftime.datetime".
... but the elements of the cdt
array are of the type nc_time_axis.CalendarDateTime
...
>>> type(cdt[0])
nc_time_axis.CalendarDateTime
Like this example but having datetimes as the x-axis...
> cat /etc/*-release
CentOS Linux release 7.4.1708 (Core)
Cluster Manager v8.0
slave
NAME="CentOS Linux"
VERSION="7 (Core)"
ID="centos"
ID_LIKE="rhel fedora"
VERSION_ID="7"
PRETTY_NAME="CentOS Linux 7 (Core)"
ANSI_COLOR="0;31"
CPE_NAME="cpe:/o:centos:centos:7"
HOME_URL="https://www.centos.org/"
BUG_REPORT_URL="https://bugs.centos.org/"
CENTOS_MANTISBT_PROJECT="CentOS-7"
CENTOS_MANTISBT_PROJECT_VERSION="7"
REDHAT_SUPPORT_PRODUCT="centos"
REDHAT_SUPPORT_PRODUCT_VERSION="7"
CentOS Linux release 7.4.1708 (Core)
CentOS Linux release 7.4.1708 (Core)
python -c "import nc_time_axis; print(nc_time_axis.__version__)"
]> python -c "import nc_time_axis; print(nc_time_axis.__version__)"
1.3.1
Thanks in advance for any tips!
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
/nesi/nobackup/niwa00013/williamsjh/miniconda3/envs/warming/lib/python3.9/site-packages/matplotlib/axis.py in convert_units(self, x)
1499 try:
-> 1500 ret = self.converter.convert(x, self.units, self)
1501 except Exception as e:
/nesi/nobackup/niwa00013/williamsjh/miniconda3/envs/warming/lib/python3.9/site-packages/nc_time_axis/__init__.py in convert(cls, value, unit, axis)
348 if not isinstance(first_value, (CalendarDateTime, cftime.datetime)):
--> 349 raise ValueError(
350 "The values must be numbers or instances of "
ValueError: The values must be numbers or instances of "nc_time_axis.CalendarDateTime" or "cftime.datetime".
The above exception was the direct cause of the following exception:
ConversionError Traceback (most recent call last)
/tmp/ipykernel_13716/2307464752.py in <module>
16 temperatures = [round(random.uniform(0, 12), 3) for _ in range(len(cdt))]
17
---> 18 plt.fill_between(cdt, temperatures,0)
19 plt.margins(0.1)
20 plt.ylim(0, 12)
/nesi/nobackup/niwa00013/williamsjh/miniconda3/envs/warming/lib/python3.9/site-packages/matplotlib/pyplot.py in fill_between(x, y1, y2, where, interpolate, step, data, **kwargs)
2804 x, y1, y2=0, where=None, interpolate=False, step=None, *,
2805 data=None, **kwargs):
-> 2806 return gca().fill_between(
2807 x, y1, y2=y2, where=where, interpolate=interpolate, step=step,
2808 **({"data": data} if data is not None else {}), **kwargs)
/nesi/nobackup/niwa00013/williamsjh/miniconda3/envs/warming/lib/python3.9/site-packages/matplotlib/__init__.py in inner(ax, data, *args, **kwargs)
1359 def inner(ax, *args, data=None, **kwargs):
1360 if data is None:
-> 1361 return func(ax, *map(sanitize_sequence, args), **kwargs)
1362
1363 bound = new_sig.bind(ax, *args, **kwargs)
/nesi/nobackup/niwa00013/williamsjh/miniconda3/envs/warming/lib/python3.9/site-packages/matplotlib/axes/_axes.py in fill_between(self, x, y1, y2, where, interpolate, step, **kwargs)
5384 def fill_between(self, x, y1, y2=0, where=None, interpolate=False,
5385 step=None, **kwargs):
-> 5386 return self._fill_between_x_or_y(
5387 "x", x, y1, y2,
5388 where=where, interpolate=interpolate, step=step, **kwargs)
/nesi/nobackup/niwa00013/williamsjh/miniconda3/envs/warming/lib/python3.9/site-packages/matplotlib/axes/_axes.py in _fill_between_x_or_y(self, ind_dir, ind, dep1, dep2, where, interpolate, step, **kwargs)
5291 # Handle united data, such as dates
5292 ind, dep1, dep2 = map(
-> 5293 ma.masked_invalid, self._process_unit_info(
5294 [(ind_dir, ind), (dep_dir, dep1), (dep_dir, dep2)], kwargs))
5295
/nesi/nobackup/niwa00013/williamsjh/miniconda3/envs/warming/lib/python3.9/site-packages/matplotlib/axes/_base.py in _process_unit_info(self, datasets, kwargs, convert)
2357 if dataset_axis_name == axis_name and data is not None:
2358 axis.update_units(data)
-> 2359 return [axis_map[axis_name].convert_units(data) if convert else data
2360 for axis_name, data in datasets]
2361
/nesi/nobackup/niwa00013/williamsjh/miniconda3/envs/warming/lib/python3.9/site-packages/matplotlib/axes/_base.py in <listcomp>(.0)
2357 if dataset_axis_name == axis_name and data is not None:
2358 axis.update_units(data)
-> 2359 return [axis_map[axis_name].convert_units(data) if convert else data
2360 for axis_name, data in datasets]
2361
/nesi/nobackup/niwa00013/williamsjh/miniconda3/envs/warming/lib/python3.9/site-packages/matplotlib/axis.py in convert_units(self, x)
1500 ret = self.converter.convert(x, self.units, self)
1501 except Exception as e:
-> 1502 raise munits.ConversionError('Failed to convert value(s) to axis '
1503 f'units: {x!r}') from e
1504 return ret
ConversionError: Failed to convert value(s) to axis units: [<CalendarDateTime: datetime=2017-02-01 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-02 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-03 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-04 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-05 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-06 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-07 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-08 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-09 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-10 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-11 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-12 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-13 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-14 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-15 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-16 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-17 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-18 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-19 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-20 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-21 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-22 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-23 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-24 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-25 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-26 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-27 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-28 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-29 00:00:00, calendar={self.calendar}>, <CalendarDateTime: datetime=2017-02-30 00:00:00, calendar={self.calendar}>]
netcdftime has now moved to cftime. We should update our dependency.
Would it be possible to release a new minor version to pypi/conda-forge which includes #50? I would love to have this feature in my normal production environments.
Happy to help with this too!
I just ran into the problem I encountered over at xarray/#3841.
The basic problem is that if one plots a timeseries with cftime dates close to year 0 with the gregorian calendar, the NetCDFTimeDateLocator
seems to create a date that is crossing year 0, which is not defined.
I noticed I never actually opened an issue here, so here it is with a bit simpler example to reproduce the error:
import numpy as np
import matplotlib.pyplot as plt
import nc_time_axis
import cftime
d_time = [cftime.DatetimeGregorian(year=year, month=1, day=1) for year in range(100, 131)]
temperatures = np.random.rand(len(d_time))
plt.plot(d_time, temperatures)
plt.show()
# this works
when I move the years closer to 0:
d_time = [cftime.DatetimeGregorian(year=year, month=1, day=1) for year in range(1, 31)]
temperatures = np.random.rand(len(d_time))
plt.plot(d_time, temperatures)
plt.show()
# this gives the zero error.
I get the following error:
ValueError Traceback (most recent call last)
in
3
4 plt.plot(d_time, temperatures)
----> 5 plt.show()
6 # this gives the zero error.
/scratch/gpfs2/jbusecke/conda_tigressdata/envs/euc_dynamics/lib/python3.8/site-packages/matplotlib/pyplot.py in show(*args, **kw)
270 """
271 global _show
--> 272 return _show(*args, **kw)
273
274
/scratch/gpfs2/jbusecke/conda_tigressdata/envs/euc_dynamics/lib/python3.8/site-packages/ipykernel/pylab/backend_inline.py in show(close, block)
41 display(
42 figure_manager.canvas.figure,
---> 43 metadata=_fetch_figure_metadata(figure_manager.canvas.figure)
44 )
45 finally:
/scratch/gpfs2/jbusecke/conda_tigressdata/envs/euc_dynamics/lib/python3.8/site-packages/ipykernel/pylab/backend_inline.py in _fetch_figure_metadata(fig)
178 if _is_transparent(fig.get_facecolor()):
179 # the background is transparent
--> 180 ticksLight = _is_light([label.get_color()
181 for axes in fig.axes
182 for axis in (axes.xaxis, axes.yaxis)
/scratch/gpfs2/jbusecke/conda_tigressdata/envs/euc_dynamics/lib/python3.8/site-packages/ipykernel/pylab/backend_inline.py in (.0)
181 for axes in fig.axes
182 for axis in (axes.xaxis, axes.yaxis)
--> 183 for label in axis.get_ticklabels()])
184 if ticksLight.size and (ticksLight == ticksLight[0]).all():
185 # there are one or more tick labels, all with the same lightness
/scratch/gpfs2/jbusecke/conda_tigressdata/envs/euc_dynamics/lib/python3.8/site-packages/matplotlib/axis.py in get_ticklabels(self, minor, which)
1318 if minor:
1319 return self.get_minorticklabels()
-> 1320 return self.get_majorticklabels()
1321
1322 def get_majorticklines(self):
/scratch/gpfs2/jbusecke/conda_tigressdata/envs/euc_dynamics/lib/python3.8/site-packages/matplotlib/axis.py in get_majorticklabels(self)
1274 def get_majorticklabels(self):
1275 'Return a list of Text instances for the major ticklabels.'
-> 1276 ticks = self.get_major_ticks()
1277 labels1 = [tick.label1 for tick in ticks if tick.label1.get_visible()]
1278 labels2 = [tick.label2 for tick in ticks if tick.label2.get_visible()]
/scratch/gpfs2/jbusecke/conda_tigressdata/envs/euc_dynamics/lib/python3.8/site-packages/matplotlib/axis.py in get_major_ticks(self, numticks)
1429 'Get the tick instances; grow as necessary.'
1430 if numticks is None:
-> 1431 numticks = len(self.get_majorticklocs())
1432
1433 while len(self.majorTicks) < numticks:
/scratch/gpfs2/jbusecke/conda_tigressdata/envs/euc_dynamics/lib/python3.8/site-packages/matplotlib/axis.py in get_majorticklocs(self)
1346 def get_majorticklocs(self):
1347 """Get the array of major tick locations in data coordinates."""
-> 1348 return self.major.locator()
1349
1350 def get_minorticklocs(self):
/scratch/gpfs2/jbusecke/conda_tigressdata/envs/euc_dynamics/lib/python3.8/site-packages/nc_time_axis/init.py in call(self)
136 def call(self):
137 vmin, vmax = self.axis.get_view_interval()
--> 138 return self.tick_values(vmin, vmax)
139
140 def tick_values(self, vmin, vmax):
/scratch/gpfs2/jbusecke/conda_tigressdata/envs/euc_dynamics/lib/python3.8/site-packages/nc_time_axis/init.py in tick_values(self, vmin, vmax)
192 raise ValueError(msg)
193
--> 194 return utime.date2num(ticks)
195
196
cftime/_cftime.pyx in cftime._cftime.utime.date2num()
cftime/_cftime.pyx in cftime._cftime.JulianDayFromDate()
cftime/_cftime.pyx in cftime._cftime._IntJulianDayFromDate()
ValueError: year zero does not exist in the standard calendar
Id be happy to submit a PR, but I would need some pointers on how to approach this problem best.
My thinking was to check the generated vmin and vmax values here and set anything that is < 1 equal to 1?
But perhaps a solution should be more general, and also allow all negative values? Do we need to make this dependent on the calendar, e.g. do some calendars allow a zero crossing?
This is a nice and useful extension of cftime
and matplolib
!
We spent some time this afternoon with @oliviermarti trying to understand why a time series based on data using a noleap
calendar was plotted (seemingly) correctly with default settings, but was shifted (30 years in our case) when we tried to format the tick labels with set_major_formatter
, following the appropriate examples in the matplotlib gallery. We were lucky to spot this, because matplotlib was not generating any error! I guess matplotlib was happy enough to handle cftime data as if it was regular datetime data
The question now is how to tell our climate data users that they (or most likely their unsuspecting interns) will get horribly wrong plots if they rely on matplolib defaults...
What could work would be to display side by side a matplotlib plot using some dummy data with a cftime+noleap time axis, and a correct plot using nc-time-axis, if somebody can come up with a simple example (I'm not volunteering!)
hey @bjlittle I am trying to install nc-time-axis
the latest version, 1.3.1, since I found an issue in ESMValTool related to it (and we've not had it before, so I am fairly sure it's due to matplotlib 3.4.2 not liking the older nc-time-axis=1.2.0) but nc-time-axis 1.3.1 requires cftime>=1.5 whereas iris=3.0.2 requires cftime<1.3 so I'm shtuck proper ๐
It appears that some matplotlib method pass lists of coordinates to the converting methods. Such an example is plt.axhspan
, that passes [xmin, xmax]
to ax.convert_xunits
and, thus, down to nc_time_axis.NetCDFTimeConverter.convert
. However, the latter tests for np.ndarray
but not for lists, so it raises a ValueError.
MWE:
import cftime
import matplotlib.pyplot as plt
x1 = cftime.DatetimeNoLeap(1990, 1, 1)
x2 = cftime.DatetimeNoLeap(1991, 1, 1)
x3 = cftime.DatetimeNoLeap(1992, 1, 1)
plt.plot([x1, x2, x3], [1, 2, 1])
plt.axvspan(x1, x2, color='red', alpha=0.5)
Result:
---------------------------------------------------------------------------
ValueError Traceback (most recent call last)
~/.conda/envs/xclim-dev/lib/python3.8/site-packages/matplotlib/axis.py in convert_units(self, x)
1519 try:
-> 1520 ret = self.converter.convert(x, self.units, self)
1521 except Exception as e:
~/.conda/envs/xclim-dev/lib/python3.8/site-packages/nc_time_axis/__init__.py in convert(cls, value, unit, axis)
277 if not isinstance(first_value, (CalendarDateTime, cftime.datetime)):
--> 278 raise ValueError('The values must be numbers or instances of '
279 '"nc_time_axis.CalendarDateTime" or '
ValueError: The values must be numbers or instances of "nc_time_axis.CalendarDateTime" or "cftime.datetime".
The above exception was the direct cause of the following exception:
ConversionError Traceback (most recent call last)
<ipython-input-47-fe6cec84569d> in <module>
4
5 plt.plot([x1, x2, x3], [1, 2, 1])
----> 6 plt.axvspan(x1, x2, color='red', alpha=0.5)
~/.conda/envs/xclim-dev/lib/python3.8/site-packages/matplotlib/pyplot.py in axvspan(xmin, xmax, ymin, ymax, **kwargs)
2460 @_copy_docstring_and_deprecators(Axes.axvspan)
2461 def axvspan(xmin, xmax, ymin=0, ymax=1, **kwargs):
-> 2462 return gca().axvspan(xmin, xmax, ymin=ymin, ymax=ymax, **kwargs)
2463
2464
~/.conda/envs/xclim-dev/lib/python3.8/site-packages/matplotlib/axes/_axes.py in axvspan(self, xmin, xmax, ymin, ymax, **kwargs)
1105
1106 # first we need to strip away the units
-> 1107 xmin, xmax = self.convert_xunits([xmin, xmax])
1108 ymin, ymax = self.convert_yunits([ymin, ymax])
1109
~/.conda/envs/xclim-dev/lib/python3.8/site-packages/matplotlib/artist.py in convert_xunits(self, x)
173 if ax is None or ax.xaxis is None:
174 return x
--> 175 return ax.xaxis.convert_units(x)
176
177 def convert_yunits(self, y):
~/.conda/envs/xclim-dev/lib/python3.8/site-packages/matplotlib/axis.py in convert_units(self, x)
1520 ret = self.converter.convert(x, self.units, self)
1521 except Exception as e:
-> 1522 raise munits.ConversionError('Failed to convert value(s) to axis '
1523 f'units: {x!r}') from e
1524 return ret
ConversionError: Failed to convert value(s) to axis units: [cftime.DatetimeNoLeap(1990, 1, 1, 0, 0, 0, 0), cftime.DatetimeNoLeap(1991, 1, 1, 0, 0, 0, 0)]
I hacked this into nc-time-axis (NetCDFTimeConverter.convert
):
if isinstance(value, (list, tuple)):
first_value = value
to make it work. However, I am not an expert of matplotlib and I have no idea if this could be dangerous or if it is appropriate...
Consider the following modification of the basic example
import random
import matplotlib.pyplot as plt
import nc_time_axis
import cftime, numpy as np
calendar = "360_day"
d_time = [cftime.datetime(y,1,1,0,0,0) for y in range(1850, 2001)]
c_d_time = [nc_time_axis.CalendarDateTime(item, calendar) for item in d_time]
temperatures = [round(random.uniform(0, 12), 3) for _ in range(len(c_d_time))]
t0 = nc_time_axis.CalendarDateTime(cftime.datetime(1850,1,1,0,0,0),calendar)
t1 = nc_time_axis.CalendarDateTime(cftime.datetime(2000,1,1,0,0,0),calendar)
plt.plot(c_d_time, temperatures)
plt.margins(0.1)
plt.ylim(0, 12)
plt.xlim(t0,t1)
plt.xlabel("Year")
plt.ylabel("Temperature")
plt.show()
This works, but the tick marks are slightly oddly located. If I try to add ticks with
ticks = [nc_time_axis.CalendarDateTime(cftime.datetime(y,1,1,0,0,0),calendar) for y in range(1850,2001,25)]
plt.xticks(ticks)
I get an error
File "/.../miniconda3/lib/python3.6/site-packages/nc_time_axis/__init__.py", line 270, in convert
raise ValueError('The values must be numbers or instances of '
ValueError: The values must be numbers or instances of "nc_time_axis.CalendarDateTime".
This seems to be because convert checks whether its argument is a numpy array but doesn't handle lists properly.
Using
ticks = nparray([nc_time_axis.CalendarDateTime(cftime.datetime(y,1,1,0,0,0),calendar) for y in range(1850,2001,25)])
plt.xticks(ticks)
fails with
File "/.../miniconda3/lib/python3.6/site-packages/nc_time_axis/__init__.py", line 83, in __call__
format_string = self.pick_format(ndays=self.locator.ndays)
AttributeError: 'NetCDFTimeDateLocator' object has no attribute 'ndays'
It seems that ndays is needed for working out the formatting of tick labels but isn't set when using explicit locations.
At the moment I'm using an ugly workaround that gets the locator and sets the ndays attribute. E.g,
import random
import matplotlib.pyplot as plt
import nc_time_axis
import cftime, numpy as np
calendar = "360_day"
d_time = [cftime.datetime(y,1,1,0,0,0) for y in range(1850, 2001)]
c_d_time = [nc_time_axis.CalendarDateTime(item, calendar) for item in d_time]
temperatures = [round(random.uniform(0, 12), 3) for _ in range(len(c_d_time))]
t0 = nc_time_axis.CalendarDateTime(cftime.datetime(1850,1,1,0,0,0),calendar)
t1 = nc_time_axis.CalendarDateTime(cftime.datetime(2000,1,1,0,0,0),calendar)
ticks = np.array([nc_time_axis.CalendarDateTime(cftime.datetime(y,1,1,0,0,0),calendar) for y in range(1850,2001,25)])
plt.plot(c_d_time, temperatures)
plt.margins(0.1)
plt.ylim(0, 12)
plt.xlim(t0,t1)
axes = plt.gca()
locator = axes.xaxis.get_major_locator()
locator.ndays = 365*150
plt.xticks(ticks)
plt.xlabel("Year")
plt.ylabel("Temperature")
plt.show()
Versions are matplotlib 3.0.2, cftime 1.0.3.4, nc_time_axis 1.1.0.
Currently nc-time-axis is using netcdf version 1.0.2. This was chosen as that is what the users have access to. The latest version of netcdf4 (1.2.4) has had some api changes that will need to be taken care of before we could bump up to the latest version of netcdf4.
SciTools/iris#3016 is blocked b/c we need a new release of nc-time-axis
with #30
Dear awesome nc-time-axis
contributors!
nc-time-axis
is now citable and contains a CITATION.cff file.
If you'd like to be recognised as a citable contributor then please share your ORCID details in a comment in this issue, and I'll add them in batch to the CITATION.cff
. Alternatively, if you want to raise a pull-request yourself instead, then go for it... whatever you prefer ๐
If you don't want to add your details, then that's also totally fine too.
Regardless, thanks for caring and contributing to nc-time-axis
๐ป
Hopefully, I've not missed anyone from the list:
As per https://pypi.org/project/nc-time-axis/1.1.0/:
The author of this package has not provided a project description
It would be great to update the README to markdown at the same time. Similar effort was done in SciTools/cf-units#103.
@LukeC92 was involved in doing a similar thing for iris-grib recently, and may like to have a go at this one too.
We have matplotlib<2 in our requirements.txt, which is being used by conda.
Turns out, that doesn't do exactly what you'd expect...
conda create -n nc_time_axis pip 'matplotlib<2' netcdf4
Fetching package metadata ...........
Solving package specifications: .
Package plan for installation in environment /Users/pelson/miniconda/envs/nc_time_axis:
The following NEW packages will be INSTALLED:
ca-certificates: 2017.7.27.1-0 conda-forge
certifi: 2017.7.27.1-py36_0 conda-forge
curl: 7.54.1-0 conda-forge
cycler: 0.10.0-py36_0 conda-forge
freetype: 2.6.3-1 conda-forge
hdf4: 4.2.12-0 conda-forge
hdf5: 1.8.18-0 conda-forge
jpeg: 9b-0 conda-forge
krb5: 1.14.2-0 conda-forge
libgfortran: 3.0.0-0 conda-forge
libnetcdf: 4.4.1.1-5 conda-forge
libpng: 1.6.28-0 conda-forge
libssh2: 1.8.0-1 conda-forge
matplotlib: 2.0.0rc2-np112py36_2 conda-forge
mkl: 2017.0.3-0 defaults
ncurses: 5.9-10 conda-forge
netcdf4: 1.2.9-py36_1 conda-forge
numpy: 1.12.1-py36_0 defaults
Notice how a version before 2.0.0 has been selected, it's just that it is a 2.0 release candidate. I attribute this to be a bug in conda, but for now, we need to be more explicit and replace 'matplotlib<2' with 'matplotlib 1*'.
The code that chooses the resolution of the ticks and the code that chooses the format of the ticks are sometimes not consistent with each other. This can lead to confusing axis tick labels, e.g. in the following example provided by @klindsay28:
import cftime
import matplotlib.pyplot as plt
import nc_time_axis
time_vals = [cftime.DatetimeNoLeap(1+year, 1+month, 15)
for year in range(3) for month in range(12)]
x_vals = [time_val.year + time_val.dayofyr / 365.0
for time_val in time_vals]
fig, ax = plt.subplots(1, 1)
ax.plot(time_vals, x_vals, "-o")
In this case a monthly resolution is chosen for the ticks, but a tick format of "%Y"
is used, giving the false impression that the ticks represent the beginning of years. I would expect for monthly resolution ticks, a format of "%Y-%m"
would be used. In this example this would result in a plot that looked like this:
If possible it would be great if we could make sure that inconsistencies like the above example could be avoided.
xref: pydata/xarray#4401
I think it is worth putting the rationale here for why we can't handle a calendar-less netcdftime datetime as I'm certain this will come up regularly.
Just FYI, matplotlib version 3.5 deprecates munits.ConversionInterface.is_numlike
. Which you use here:
nc-time-axis/nc_time_axis/__init__.py
Line 433 in 5f2c4dc
See matplotlib/matplotlib#20334 (lib/matplotlib/units.py - L137)
edit: seen in the xarray upstream test warnings.
Historically the time units used in nc-time-axis have been "days since 2000-01-01"
. cftime.num2date
and cftime.date2num
are exact as of cftime version 1.2.1 if units of microseconds are used, since all arithmetic can be done with integers. Therefore it might be beneficial to switch to using microseconds internally within nc-time-axis. Errors are very small if we do not use microseconds, but we might as well be as exact as possible.
Currently NetCDFTimeConverter.convert
promotes scalar CalendarDateTime
or cftime.datetime
objects to arrays of floats:
>>> import cftime
>>> from nc_time_axis import NetCDFTimeConverter
>>> NetCDFTimeConverter.convert(cftime.DatetimeNoLeap(2010, 1, 1), None, None)
array([3650.])
Within matplotlib, this can lead to unusual assignment behavior, which will eventually be deprecated in NumPy (numpy/numpy#16943):
$ python -Wd
>>> import cftime
>>> import matplotlib.pyplot as plt
>>> import nc_time_axis
>>> times = [cftime.DatetimeNoLeap(2000, 1, 1), cftime.DatetimeNoLeap(2000, 1, 2), cftime.DatetimeNoLeap(2000, 1, 3)]
>>> data = range(3)
>>> plt.plot(times, data)
.../matplotlib/transforms.py:943: DeprecationWarning: setting an array element with a sequence. This was supported in some cases where the elements are arrays with a single element. For example `np.array([1, np.array([2])], dtype=int)`. In the future this will raise the same ValueError as `np.array([1, [2]], dtype=int)`.
self._points[:, 0] = interval
.../nc_time_axis/__init__.py:266: DeprecationWarning: `np.object` is a deprecated alias for the builtin `object`. Use `object` by itself, which is identical in behavior, to silence this warning. If you specifically wanted the numpy scalar type, use `np.object_` here.
if value.dtype != np.object:
[<matplotlib.lines.Line2D object at 0x7fc550278c40>]
In the above example, self._points
is np.array([[0., 0.], [1., 1.]])
and interval
is (np.array([0.]), np.array([3650.]))
. If the converter handled scalars without promoting them to arrays, interval
would be (0., 3650.)
, and the assignment would proceed without warning.
xref: pydata/xarray#4265
A few years ago, matplotlib introduced a date formatter that helps to display date information in a more concise manner. It does so by minimizing the amount of duplicate information displayed in the tick labels through the axis offset label. See this example taken from their documentation:
It would be cool if nc-time-axis supported something similar.
Datetime tick labels can often take up a lot of space. This feature in matplotlib does a nice job of minimizing that without losing information.
I think nc-time-axis now requires cftime >= 1.5 (#69). It would be good if that was recorded in the main
branch of the repo.
Hey guys, I just opened an issue on the ESMValTool page and it's related to your package, could you please have a look and advise me what's the solution? Many thanks! Cheers, V
(This may also be a problem with conda rather than nc-time-axis, but I'm posting here as it is the only package for which this problem occurs.)
I have nc-time-axis installed (v1.2.0, build: py_1, channel: conda-forge), mainly to use it with xarray.
However, running import nc_time_axis
results in ModuleNotFoundError: No module named 'nc_time_axis'
.
The conda environment in which nc-time-axis is installed is activated when this error occurs, and has python v3.8.5. OS: linux (hpc)
Is this likely to be a problem with nc-time-axis?
Should/ could allow passing a string to convert
(of the NetCDFTimeConverter
) be beneficial?
I want to set the xlim of a nc-time axis. Is there a more convinent way than to pass cftime._cftime.datetime
?
It seems I need to do:
import numpy as np
import matplotlib.pyplot as plt
import xarray as xr
data = np.random.randn(100)
time = xr.date_range("2000", "2100", freq="A", calendar="noleap")
da = xr.DataArray(data, coords={"time": time})
f, ax = plt.subplots()
da.plot(ax=ax)
ax.set_xlim(None, da.time.sel(time="2050").item())
It would be nice if we could pass a string - similar as we do with sel
in xarray, e.g.:
ax.set_xlim(None, "2050")
Please add additional verbose information in this section e.g., references, screenshots, listings etc
Currently, the max number of ticks is set to 4 (on this line). This is a bit restrictive.
It would be nice if the user had control over the number of ticks, perhaps through the use of some kind of set_max_ticks
method.
there is a potential bug with respect to matplotlib2 in tick locations
the version of mpl is now pinned, pending this fix
https://github.com/SciTools/nc-time-axis/blob/master/nc_time_axis/tests/unit/test_NetCDFTimeDateLocator.py#L99
https://github.com/SciTools/nc-time-axis/blob/master/nc_time_axis/tests/unit/test_NetCDFTimeDateLocator.py#L107
https://github.com/SciTools/nc-time-axis/blob/master/nc_time_axis/tests/unit/test_NetCDFTimeDateLocator.py#L111
ref #22
PR #51 will ensure the tests pass with cftime >= 1.3.0.
cftime 1.3.0 will allow vanilla cftime.datetime instances to be calendar-specific using the calendar __init__
kwarg (the calendar specific sub-classes will no longer be required to instantiate 'calendar-aware' instances).
The nc-time-axis
project needs some basic documentation on readthedocs to help the community understand what the package is and how to use it.
Breath some life back into nc-time-axis
.
To-do checklist:
1.3.0
release discussion1.3.0
release milestone1.3.0
project boardtravis-ci
to cirrus-ci
py37
, py38
, py39
, coverage only for py39
black
, flake8
, and isort
black
flake8
isort
setuptools-scm
and drop versioneer
pre-commit
setup.py
to setup.cfg
conda
and PyPIpytest
README.rst
to README.md
cirrus-ci
, codecov
and pre-commit
post merge in separate prpy37
, py38
, and py39
I am trying to cite this amazing tool in a paper revision and noticed this does not have any citation info?
Would it be possible to mint a DOI via e.g. zenodo?
That way I could cite it properly in the final version of the paper.
We noticed that pypi version of nc-time-axis is still at 1.1.0:
https://pypi.org/project/nc-time-axis/
Would be great to keep that up to date with conda.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.