From a79a42d5f766cb20a8ceb98373c4e07e3bb5604e Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Wed, 21 Nov 2018 08:51:01 -0800 Subject: [PATCH 1/2] FIX: translate timedeltas in _to_ordinalf FIX: translate timedelta64 in _dt64_to_ordinalf --- lib/matplotlib/dates.py | 48 +++++++++++++++++++++++------- lib/matplotlib/tests/test_dates.py | 33 ++++++++++++++++++++ 2 files changed, 71 insertions(+), 10 deletions(-) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index c9eb3a1ca230..e087353e8003 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -222,6 +222,10 @@ def _to_ordinalf(dt): dt = dt.astimezone(UTC) tzi = UTC + if isinstance(dt, datetime.timedelta): + base = dt / datetime.timedelta(days=1) + return base + base = float(dt.toordinal()) # If it's sufficiently datetime-like, it will have a `date()` method @@ -251,6 +255,11 @@ def _dt64_to_ordinalf(d): because we do times compared to ``0001-01-01T00:00:00`` (plus one day). """ + if (isinstance(d, np.timedelta64) or + (isinstance(d, np.ndarray) and + np.issubdtype(d.dtype, np.timedelta64))): + return d / np.timedelta64(1, 'D') + # the "extra" ensures that we at least allow the dynamic range out to # seconds. That should get out to +/-2e11 years. # NOTE: First cast truncates; second cast back is for NumPy 1.10. @@ -271,6 +280,11 @@ 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 _from_ordinalf(x, tz=None): """ Convert Gregorian float of the date, preserving hours, minutes, @@ -405,22 +419,36 @@ 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 julian2num(j): diff --git a/lib/matplotlib/tests/test_dates.py b/lib/matplotlib/tests/test_dates.py index 35e005346335..10ba781609b0 100644 --- a/lib/matplotlib/tests/test_dates.py +++ b/lib/matplotlib/tests/test_dates.py @@ -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'), + 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 From b12584caa54b4d4e279c072727a0bbfde9321e0b Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 27 Nov 2018 13:55:37 -0800 Subject: [PATCH 2/2] ENH: change to convert_delta approach --- lib/matplotlib/artist.py | 34 +++++++++++++++ lib/matplotlib/axes/_axes.py | 10 ++++- lib/matplotlib/axis.py | 13 ++++++ lib/matplotlib/dates.py | 81 ++++++++++++++++++++++++++++++++---- 4 files changed, 127 insertions(+), 11 deletions(-) diff --git a/lib/matplotlib/artist.py b/lib/matplotlib/artist.py index 7fb017f262ad..f0ce9aae2f28 100644 --- a/lib/matplotlib/artist.py +++ b/lib/matplotlib/artist.py @@ -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*.""" diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 9037674780e2..de99ea918617 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -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() diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 4f174b0da45e..34cf2ad3e1ca 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -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 @@ -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. diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index e087353e8003..93d91ce8c666 100644 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -222,10 +222,6 @@ def _to_ordinalf(dt): dt = dt.astimezone(UTC) tzi = UTC - if isinstance(dt, datetime.timedelta): - base = dt / datetime.timedelta(days=1) - return base - base = float(dt.toordinal()) # If it's sufficiently datetime-like, it will have a `date()` method @@ -242,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): @@ -255,11 +264,6 @@ def _dt64_to_ordinalf(d): because we do times compared to ``0001-01-01T00:00:00`` (plus one day). """ - if (isinstance(d, np.timedelta64) or - (isinstance(d, np.ndarray) and - np.issubdtype(d.dtype, np.timedelta64))): - return d / np.timedelta64(1, 'D') - # the "extra" ensures that we at least allow the dynamic range out to # seconds. That should get out to +/-2e11 years. # NOTE: First cast truncates; second cast back is for NumPy 1.10. @@ -285,6 +289,11 @@ def _dt64_to_ordinalf_iterable(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, @@ -451,6 +460,56 @@ def date2num(d): 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): """ Convert a Julian date (or sequence) to a Matplotlib date (or sequence). @@ -1834,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): """