Skip to content

Time series plot fails with non-standard calendars #288

Open
@pgf

Description

@pgf

It seems UltraPlot doesn't handle non-standard calendars when dealing with DataArray time series.
xarray makes use of cftime instead of numpy.datetime64 for non-standard calendars (see xarray docs, Time series data

import numpy            as np
import xarray           as xr
import ultraplot        as uplt

# Time axis centered at mid-month
# Standard calendar
time1=xr.date_range("2000-01", periods=120, freq="MS")
time2=xr.date_range("2000-02", periods=120, freq="MS")
time=time1+0.5*(time2-time1)
# Non-standard calendar (uses cftime)
time1=xr.date_range("2000-01", periods=120, freq="MS", calendar="noleap")
time2=xr.date_range("2000-02", periods=120, freq="MS", calendar="noleap")
time_noleap=time1+0.5*(time2-time1)

da=xr.DataArray(data=np.sin(np.arange(0.,2*np.pi,np.pi/60.)),
               dims=("time",),
               coords={"time":time,},
               attrs={"long_name":"low freq signal", "units":"normalized"})

da_noleap=xr.DataArray(data=np.sin(2.*np.arange(0.,2*np.pi,np.pi/60.)),
               dims=("time",),
               coords={"time":time_noleap,},
               attrs={"long_name":"high freq signal", "units":"normalized"})

fig,axs = uplt.subplots(ncols=2)
axs.format(title=("Standard calendar", "Non-standard calendar"))
axs[0].plot(da)
axs[1].plot(da_noleap)

Result:

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[1], line 28
     26 axs.format(title=("Standard calendar", "Non-standard calendar"))
     27 axs[0].plot(da)
---> 28 axs[1].plot(da_noleap)

File [~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/ultraplot/internals/inputs.py:365](http://localhost:8888/lab/tree/tmp/~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/ultraplot/internals/inputs.py#line=364), in _preprocess_or_redirect.<locals>._decorator.<locals>._preprocess_or_redirect(self, *args, **kwargs)
    363     kwargs["color"] = color
    364 # Call main function
--> 365 return func(self, *args, **kwargs)

File [~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/ultraplot/axes/plot.py:3673](http://localhost:8888/lab/tree/tmp/~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/ultraplot/axes/plot.py#line=3672), in PlotAxes.plot(self, *args, **kwargs)
   3669 """
   3670 %(plot.plot)s
   3671 """
   3672 kwargs = _parse_vert(default_vert=True, **kwargs)
-> 3673 return self._apply_plot(*args, **kwargs)

File [~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/ultraplot/axes/plot.py:3390](http://localhost:8888/lab/tree/tmp/~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/ultraplot/axes/plot.py#line=3389), in PlotAxes._apply_plot(self, vert, *pairs, **kwargs)
   3388 if fmt is not None:  # x1, y1, fmt1, x2, y2, fm2... style input
   3389     a.append(fmt)
-> 3390 (obj,) = self._call_native("plot", *a, **kw)
   3391 self._inbounds_xylim(extents, x, y)
   3392 objs.append((*eb, *es, obj) if eb or es else obj)

File [~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/ultraplot/axes/plot.py:1531](http://localhost:8888/lab/tree/tmp/~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/ultraplot/axes/plot.py#line=1530), in PlotAxes._call_native(self, name, *args, **kwargs)
   1529         obj = getattr(self.projection, name)(*args, ax=self, **kwargs)
   1530     else:
-> 1531         obj = getattr(super(), name)(*args, **kwargs)
   1532 return obj

File [~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/axes/_axes.py:1779](http://localhost:8888/lab/tree/tmp/~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/axes/_axes.py#line=1778), in Axes.plot(self, scalex, scaley, data, *args, **kwargs)
   1777 lines = [*self._get_lines(self, *args, data=data, **kwargs)]
   1778 for line in lines:
-> 1779     self.add_line(line)
   1780 if scalex:
   1781     self._request_autoscale_view("x")

File [~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/axes/_base.py:2407](http://localhost:8888/lab/tree/tmp/~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/axes/_base.py#line=2406), in _AxesBase.add_line(self, line)
   2404 if line.get_clip_path() is None:
   2405     line.set_clip_path(self.patch)
-> 2407 self._update_line_limits(line)
   2408 if not line.get_label():
   2409     line.set_label(f'_child{len(self._children)}')


File [~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/axes/_base.py:2430](http://localhost:8888/lab/tree/tmp/~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/axes/_base.py#line=2429), in _AxesBase._update_line_limits(self, line)
   2426 def _update_line_limits(self, line):
   2427     """
   2428     Figures out the data limit of the given line, updating `.Axes.dataLim`.
   2429     """
-> 2430     path = line.get_path()
   2431     if path.vertices.size == 0:
   2432         return

File [~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/lines.py:1052](http://localhost:8888/lab/tree/tmp/~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/lines.py#line=1051), in Line2D.get_path(self)
   1050 """Return the `~matplotlib.path.Path` associated with this line."""
   1051 if self._invalidy or self._invalidx:
-> 1052     self.recache()
   1053 return self._path

File [~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/lines.py:689](http://localhost:8888/lab/tree/tmp/~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/lines.py#line=688), in Line2D.recache(self, always)
    687 if always or self._invalidx:
    688     xconv = self.convert_xunits(self._xorig)
--> 689     x = _to_unmasked_float_array(xconv).ravel()
    690 else:
    691     x = self._x

File [~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/cbook.py:1355](http://localhost:8888/lab/tree/tmp/~/Library/local/miniforge3/envs/py3cf/lib/python3.13/site-packages/matplotlib/cbook.py#line=1354), in _to_unmasked_float_array(x)
   1353     return np.ma.asarray(x, float).filled(np.nan)
   1354 else:
-> 1355     return np.asarray(x, float)

TypeError: float() argument must be a string or a real number, not 'cftime._cftime.DatetimeNoLeap'

Image

Setup:

cftime                              1.6.4            py313h93df234_1          conda-forge
numpy                               2.3.0            py313h41a2e72_0          conda-forge
python                              3.13.1           h4f43103_102_cp313       conda-forge
matplotlib                          3.10.3           py313h39782a4_0          conda-forge
ultraplot                           1.57.1           pyhd8ed1ab_0             conda-forge
xarray                              2025.6.1         pyhd8ed1ab_1             conda-forge

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions