Skip to content

FIX: translate timedeltas in _to_ordinalf #12863

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions lib/matplotlib/artist.py
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,40 @@ def convert_yunits(self, y):
return y
return ax.yaxis.convert_units(y)

def convert_xunits_delta(self, dx):
"""
Convert *dx* using the unit type of the yaxis.

This is used for APIs where the data is passed as ``(x, dx)``, and
the ``dx`` can be in different units than the ``x``. For instance,
for dates ``x`` is often of type ``datetime``, and ``dx`` of type
``timedelta`` and these are converted differently.

If the artist is not in contained in an Axes or if the yaxis does not
have units, *deltax* itself is returned.
"""
ax = getattr(self, 'axes', None)
if ax is None or ax.xaxis is None:
return dx
return ax.xaxis.convert_units_delta(dx)

def convert_yunits_delta(self, dy):
"""
Convert *dy* using the unit type of the yaxis.

This is used for APIs where the data is passed as ``(y, dy)``, and
the ``dy`` can be in different units than the ``y``. For instance,
for dates ``y`` is often of type ``datetime``, and ``dy`` of type
``timedelta`` and these are converted differently.

If the artist is not in contained in an Axes or if the yaxis does not
have units, *deltay* itself is returned.
"""
ax = getattr(self, 'axes', None)
if ax is None or ax.yaxis is None:
return dy
return ax.yaxis.convert_units_delta(dy)

@property
def axes(self):
"""The `~.axes.Axes` instance the artist resides in, or *None*."""
Expand Down
10 changes: 8 additions & 2 deletions lib/matplotlib/axes/_axes.py
Original file line number Diff line number Diff line change
Expand Up @@ -2526,10 +2526,16 @@ def broken_barh(self, xranges, yrange, **kwargs):
self._process_unit_info(xdata=xdata,
ydata=ydata,
kwargs=kwargs)
xranges = self.convert_xunits(xranges)

xnew = []
for ind in range(len(xranges)):
xr = [[],[]]
xr[0] = self.convert_xunits(xranges[ind][0])
xr[1] = self.convert_xunits_delta(xranges[ind][1])
xnew.append(xr)
yrange = self.convert_yunits(yrange)

col = mcoll.BrokenBarHCollection(xranges, yrange, **kwargs)
col = mcoll.BrokenBarHCollection(xnew, yrange, **kwargs)
self.add_collection(col, autolim=True)
self.autoscale_view()

Expand Down
13 changes: 13 additions & 0 deletions lib/matplotlib/axis.py
Original file line number Diff line number Diff line change
Expand Up @@ -795,6 +795,7 @@ def cla(self):
self.reset_ticks()

self.converter = None
self.converter_delta = None
self.units = None
self.set_units(None)
self.stale = True
Expand Down Expand Up @@ -1490,6 +1491,18 @@ def convert_units(self, x):
ret = self.converter.convert(x, self.units, self)
return ret

def convert_units_delta(self, dx):
# If dx is already a number, doesn't need converting
if (munits.ConversionInterface.is_numlike(dx) or
self.converter is None):
return dx

if hasattr(self.converter, 'convert_delta'):
return self.converter.convert_delta(dx, self.units, self)
else:
return self.converter.convert(dx, self.units, self)


def set_units(self, u):
"""
Set the units for axis.
Expand Down
111 changes: 101 additions & 10 deletions lib/matplotlib/dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,8 +238,21 @@ def _to_ordinalf(dt):
return base


def _td_to_ordinalf(dt):
if isinstance(dt, datetime.timedelta):
base = dt / datetime.timedelta(days=1)
return base


# a version of _to_ordinalf that can operate on numpy arrays
_to_ordinalf_np_vectorized = np.vectorize(_to_ordinalf)
# a version of _to_ordinalf that can operate on numpy arrays
_td_to_ordinalf_np_vectorized = np.vectorize(_td_to_ordinalf)


def _td64_to_ordinalf(d):
print('d', d)
return d / np.timedelta64(1, 'D')


def _dt64_to_ordinalf(d):
Expand Down Expand Up @@ -271,6 +284,16 @@ def _dt64_to_ordinalf(d):
return dt


def _dt64_to_ordinalf_iterable(d):
return np.fromiter((_dt64_to_ordinalf(dd) for dd in d),
float, count=len(d))


def _td64_to_ordinalf_iterable(d):
return np.fromiter((_td64_to_ordinalf(dd) for dd in d),
float, count=len(d))


def _from_ordinalf(x, tz=None):
"""
Convert Gregorian float of the date, preserving hours, minutes,
Expand Down Expand Up @@ -405,22 +428,86 @@ def date2num(d):
Gregorian calendar is assumed; this is not universal practice.
For details see the module docstring.
"""

if hasattr(d, "values"):
# this unpacks pandas series or dataframes...
d = d.values
if not np.iterable(d):
if (isinstance(d, np.datetime64) or (isinstance(d, np.ndarray) and
np.issubdtype(d.dtype, np.datetime64))):
return _dt64_to_ordinalf(d)
return _to_ordinalf(d)

else:
d = np.asarray(d)
if np.issubdtype(d.dtype, np.datetime64):
if not np.iterable(d) and not isinstance(d, np.ndarray):
# single value logic...
if (isinstance(d, np.datetime64) or isinstance(d, np.timedelta64)):
return _dt64_to_ordinalf(d)
if not d.size:
return d
else:
return _to_ordinalf(d)

elif (isinstance(d, np.ndarray) and
(np.issubdtype(d.dtype, np.datetime64) or
np.issubdtype(d.dtype, np.timedelta64))):
# array with all one type of datetime64 object.
return _dt64_to_ordinalf(d)

elif len(d):
# this is a list or tuple...
if (isinstance(d[0], np.datetime64) or
isinstance(d[0], np.timedelta64)):
return _dt64_to_ordinalf_iterable(d)
return _to_ordinalf_np_vectorized(d)
elif hasattr(d, 'size') and not d.size:
# this elif doesn't get tested, but leaving here in case anyone
# needs it.
return d
else:
return []


def timedelta2num(d):
"""
Convert datetime objects to Matplotlib dates.

Parameters
----------
d : `datetime.datetime` or `numpy.datetime64` or sequences of these

Returns
-------
float or sequence of floats
Number of days (fraction part represents hours, minutes, seconds, ms)
since 0001-01-01 00:00:00 UTC, plus one.

Notes
-----
The addition of one here is a historical artifact. Also, note that the
Gregorian calendar is assumed; this is not universal practice.
For details see the module docstring.
"""

if hasattr(d, "values"):
# this unpacks pandas series or dataframes...
d = d.values

if not np.iterable(d) and not isinstance(d, np.ndarray):
# single value logic...
if isinstance(d, np.timedelta64):
return _td64_to_ordinalf(d)
else:
return _td_to_ordinalf(d)

elif (isinstance(d, np.ndarray) and
np.issubdtype(d.dtype, np.timedelta64)):
# array with all one type of datetime64 object.
return _td64_to_ordinalf(d)

elif len(d):
# this is a list or tuple...
if isinstance(d[0], np.timedelta64):
return _td64_to_ordinalf_iterable(d)
return _td_to_ordinalf_np_vectorized(d)
elif hasattr(d, 'size') and not d.size:
# this elif doesn't get tested, but leaving here in case anyone
# needs it.
return d
else:
return []


def julian2num(j):
Expand Down Expand Up @@ -1806,6 +1893,10 @@ def convert(value, unit, axis):
"""
return date2num(value)

@staticmethod
def convert_delta(value, units, axis):
return timedelta2num(value)

@staticmethod
def default_units(x, axis):
"""
Expand Down
33 changes: 33 additions & 0 deletions lib/matplotlib/tests/test_dates.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,3 +680,36 @@ def test_datetime64_in_list():
dt = [np.datetime64('2000-01-01'), np.datetime64('2001-01-01')]
dn = mdates.date2num(dt)
assert np.array_equal(dn, [730120., 730486.])


def test_timedelta():
"""
Test that timedelta objects are properly translated into days.
"""
dt = [datetime.datetime(2000, 1, 1, 0, 0, 0),
datetime.timedelta(days=1, hours=2)]
assert mdates.date2num(dt[1]) == 1 + 2 / 24
# check that mixed lists work....
assert mdates.date2num(dt)[0] == 730120.0
assert mdates.date2num(dt)[1] == 1 + 2 / 24

dt = (np.datetime64('2000-01-01'),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a blank line since a new part of the test starts here.

np.timedelta64(26, 'h'))
assert mdates.date2num(dt[1]) == 1 + 2 / 24
# check that mixed lists work....
assert mdates.date2num(dt)[0] == 730120.0
assert mdates.date2num(dt)[1] == 1 + 2 / 24

dt = [datetime.timedelta(days=1, hours=1),
datetime.timedelta(days=1, hours=2)]
assert mdates.date2num(dt)[0] == 1 + 1 / 24
assert mdates.date2num(dt)[1] == 1 + 2 / 24

dt = (np.timedelta64(25, 'h'),
np.timedelta64(26, 'h'))
assert mdates.date2num(dt)[0] == 1 + 1 / 24
assert mdates.date2num(dt)[1] == 1 + 2 / 24

dt = np.array([25, 26], dtype='timedelta64[h]')
assert mdates.date2num(dt)[0] == 1 + 1 / 24
assert mdates.date2num(dt)[1] == 1 + 2 / 24