From 1dfdc298f244bfac85afbf3a697577c21eefda78 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Tue, 27 Nov 2018 15:31:05 -0800 Subject: [PATCH 1/5] FIX: brokenbarh math before units TST: Add test of broken_barh FIX: make bar work with timedeltas TST: check bar and barh work with timedelta DOC: fix private method docstring FIX: revert list comprehension TST: fix test FIX: throw error xr not tw-tuple --- lib/matplotlib/axes/_axes.py | 55 ++++++++++++++++++++++++------- lib/matplotlib/tests/test_axes.py | 35 ++++++++++++++++++++ 2 files changed, 78 insertions(+), 12 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index fd724f55876c..473df00d2759 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2011,6 +2011,26 @@ def step(self, x, y, *args, where='pre', data=None, **kwargs): kwargs['drawstyle'] = 'steps-' + where return self.plot(x, y, *args, data=data, **kwargs) + @staticmethod + def _convert_dx(dx, x0, x, convert): + """ + Small helper to do logic of width conversion flexibly. + + *dx* and *x0* have units, but *x* has already been converted + to unitless. This allows the *dx* to have units that are + different from *x0*, but are still accepted by the ``__add__`` + operator of *x0*. + """ + try: + # attempt to add the width to x0; this works for + # datetime+timedelta, for instance + dx = convert(x0 + dx) - x + except (TypeError, AttributeError): + # but doesn't work for 'string' + float, so just + # see if the converter works on the float. + dx = convert(dx) + return dx + @_preprocess_data() @docstring.dedent_interpd def bar(self, x, height, width=0.8, bottom=None, *, align="center", @@ -2172,23 +2192,25 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", else: raise ValueError('invalid orientation: %s' % orientation) + x, height, width, y, linewidth = np.broadcast_arrays( + # Make args iterable too. + np.atleast_1d(x), height, width, y, linewidth) + # lets do some conversions now since some types cannot be # subtracted uniformly if self.xaxis is not None: + x0 = x x = self.convert_xunits(x) - width = self.convert_xunits(width) + width = self._convert_dx(width, x0, x, self.convert_xunits) if xerr is not None: - xerr = self.convert_xunits(xerr) + xerr = self._convert_dx(xerr, x0, x, self.convert_xunits) if self.yaxis is not None: + y0 = y y = self.convert_yunits(y) - height = self.convert_yunits(height) + height = self._convert_dx(height, y0, y, self.convert_yunits) if yerr is not None: - yerr = self.convert_yunits(yerr) - - x, height, width, y, linewidth = np.broadcast_arrays( - # Make args iterable too. - np.atleast_1d(x), height, width, y, linewidth) + yerr = self._convert_dx(yerr, y0, y, self.convert_yunits) # Now that units have been converted, set the tick locations. if orientation == 'vertical': @@ -2465,10 +2487,19 @@ def broken_barh(self, xranges, yrange, **kwargs): self._process_unit_info(xdata=xdata, ydata=ydata, kwargs=kwargs) - xranges = self.convert_xunits(xranges) - yrange = self.convert_yunits(yrange) - - col = mcoll.BrokenBarHCollection(xranges, yrange, **kwargs) + xranges_conv = [] + for xr in xranges: + if len(xr) != 2: + raise ValueError('each range in xrange must be a sequence ' + 'with two elements (i.e. an Nx2 array)') + # convert the absolute values, not the x and dx... + x_conv = self.convert_xunits(xr[0]) + x1 = self._convert_dx(xr[1], xr[0], x_conv, self.convert_xunits) + xranges_conv.append((x_conv, x1)) + + yrange_conv = self.convert_yunits(yrange) + + col = mcoll.BrokenBarHCollection(xranges_conv, yrange_conv, **kwargs) self.add_collection(col, autolim=True) self.autoscale_view() diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 6e75ce726963..c8e2a00e0b15 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -1498,6 +1498,32 @@ def test_barh_tick_label(): align='center') +def test_bar_timedelta(): + """smoketest that bar can handle width and height in delta units""" + fig, ax = plt.subplots() + ax.bar(datetime.datetime(2018, 1, 1), 1., + width=datetime.timedelta(hours=3)) + ax.bar(datetime.datetime(2018, 1, 1), 1., + xerr=datetime.timedelta(hours=2), + width=datetime.timedelta(hours=3)) + fig, ax = plt.subplots() + ax.barh(datetime.datetime(2018, 1, 1), 1, + height=datetime.timedelta(hours=3)) + ax.barh(datetime.datetime(2018, 1, 1), 1, + height=datetime.timedelta(hours=3), + yerr=datetime.timedelta(hours=2)) + fig, ax = plt.subplots() + ax.barh([datetime.datetime(2018, 1, 1), datetime.datetime(2018, 1, 1)], + np.array([1, 1.5]), + height=datetime.timedelta(hours=3)) + ax.barh([datetime.datetime(2018, 1, 1), datetime.datetime(2018, 1, 1)], + np.array([1, 1.5]), + height=[datetime.timedelta(hours=t) for t in [1, 2]]) + ax.broken_barh([(datetime.datetime(2018, 1, 1), + datetime.timedelta(hours=1))], + (10, 20)) + + @image_comparison(baseline_images=['hist_log'], remove_text=True) def test_hist_log(): @@ -5433,6 +5459,15 @@ def test_broken_barh_empty(): ax.broken_barh([], (.1, .5)) +def test_broken_barh_timedelta(): + """Check that timedelta works as x, dx pair for this method """ + fig, ax = plt.subplots() + pp = ax.broken_barh([(datetime.datetime(2018, 11, 9, 0, 0, 0), + datetime.timedelta(hours=1))], [1, 2]) + assert pp.get_paths()[0].vertices[0, 0] == 737007.0 + assert pp.get_paths()[0].vertices[2, 0] == 737007.0 + 1 / 24 + + def test_pandas_pcolormesh(pd): time = pd.date_range('2000-01-01', periods=10) depth = np.arange(20) From 7c4b000c423c9e136da52d75ebecdcd312156a8f Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Sat, 12 Jan 2019 14:12:50 -0800 Subject: [PATCH 2/5] FIX: make robust to units --- lib/matplotlib/axes/_axes.py | 60 +++++++++++++++++++++++++++--------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 473df00d2759..b87304ae569d 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2012,20 +2012,51 @@ def step(self, x, y, *args, where='pre', data=None, **kwargs): return self.plot(x, y, *args, data=data, **kwargs) @staticmethod - def _convert_dx(dx, x0, x, convert): + def _convert_dx(dx, x0, xconv, convert): """ Small helper to do logic of width conversion flexibly. - *dx* and *x0* have units, but *x* has already been converted - to unitless. This allows the *dx* to have units that are - different from *x0*, but are still accepted by the ``__add__`` - operator of *x0*. + *dx* and *x0* have units, but *xconv* has already been converted + to unitless (and is an ndarray). This allows the *dx* to have units + that are different from *x0*, but are still accepted by the + ``__add__`` operator of *x0*. """ + + # x should be an array... + assert type(xconv) is np.ndarray + + if xconv.size == 0: + # xconv has already been converted, but maybe empty... + return convert(dx) + try: # attempt to add the width to x0; this works for # datetime+timedelta, for instance - dx = convert(x0 + dx) - x - except (TypeError, AttributeError): + + # only use the first element of x and x0. This saves + # having to be sure addition works across the whole + # vector. This is particularly an issue if + # x0 and dx are lists so x0 + dx just concatenates the lists. + # We can't just cast x0 and dx to numpy arrays because that + # removes the units from unit packages like `pint`. + try: + x0 = x0[0] + except (TypeError, IndexError, KeyError): + x0 = x0 + + try: + x = xconv[0] + except (TypeError, IndexError, KeyError): + x = xconv + + delist = False + if not np.iterable(dx): + dx = [dx] + delist = True + dx = [convert(x0 + ddx) - x for ddx in dx] + if delist: + dx = dx[0] + except (TypeError, AttributeError) as e: # but doesn't work for 'string' + float, so just # see if the converter works on the float. dx = convert(dx) @@ -2192,26 +2223,27 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", else: raise ValueError('invalid orientation: %s' % orientation) - x, height, width, y, linewidth = np.broadcast_arrays( - # Make args iterable too. - np.atleast_1d(x), height, width, y, linewidth) # lets do some conversions now since some types cannot be # subtracted uniformly if self.xaxis is not None: x0 = x - x = self.convert_xunits(x) + x = np.asarray(self.convert_xunits(x)) width = self._convert_dx(width, x0, x, self.convert_xunits) if xerr is not None: xerr = self._convert_dx(xerr, x0, x, self.convert_xunits) - if self.yaxis is not None: y0 = y - y = self.convert_yunits(y) + y = np.asarray(self.convert_yunits(y)) height = self._convert_dx(height, y0, y, self.convert_yunits) if yerr is not None: yerr = self._convert_dx(yerr, y0, y, self.convert_yunits) + x, height, width, y, linewidth = np.broadcast_arrays( + # Make args iterable too. + np.atleast_1d(x), height, width, y, linewidth) + + # Now that units have been converted, set the tick locations. if orientation == 'vertical': tick_label_axis = self.xaxis @@ -2493,7 +2525,7 @@ def broken_barh(self, xranges, yrange, **kwargs): raise ValueError('each range in xrange must be a sequence ' 'with two elements (i.e. an Nx2 array)') # convert the absolute values, not the x and dx... - x_conv = self.convert_xunits(xr[0]) + x_conv = np.asarray(self.convert_xunits(xr[0])) x1 = self._convert_dx(xr[1], xr[0], x_conv, self.convert_xunits) xranges_conv.append((x_conv, x1)) From aa6dfaa6c24b12463c34aa61873edcc3566b73d4 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Sat, 12 Jan 2019 14:25:55 -0800 Subject: [PATCH 3/5] FIX: make robust to units --- lib/matplotlib/axes/_axes.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index b87304ae569d..9033e0b5f265 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2038,7 +2038,8 @@ def _convert_dx(dx, x0, xconv, convert): # vector. This is particularly an issue if # x0 and dx are lists so x0 + dx just concatenates the lists. # We can't just cast x0 and dx to numpy arrays because that - # removes the units from unit packages like `pint`. + # removes the units from unit packages like `pint` that + # wrap numpy arrays. try: x0 = x0[0] except (TypeError, IndexError, KeyError): From 6e410db5f7ba2036aef4cf62ac29189406b5abde Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Sat, 12 Jan 2019 14:32:42 -0800 Subject: [PATCH 4/5] FIX: make robust to units --- lib/matplotlib/axes/_axes.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index 9033e0b5f265..d4760b9d2eb1 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2224,7 +2224,6 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", else: raise ValueError('invalid orientation: %s' % orientation) - # lets do some conversions now since some types cannot be # subtracted uniformly if self.xaxis is not None: @@ -2244,7 +2243,6 @@ def bar(self, x, height, width=0.8, bottom=None, *, align="center", # Make args iterable too. np.atleast_1d(x), height, width, y, linewidth) - # Now that units have been converted, set the tick locations. if orientation == 'vertical': tick_label_axis = self.xaxis From f9c43b7457148b9ae511f64554e692ac7655c540 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Sat, 12 Jan 2019 15:35:44 -0800 Subject: [PATCH 5/5] FIX: make robust to units --- lib/matplotlib/axes/_axes.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d4760b9d2eb1..a7164c13cbcb 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2048,7 +2048,7 @@ def _convert_dx(dx, x0, xconv, convert): try: x = xconv[0] except (TypeError, IndexError, KeyError): - x = xconv + x = xconv delist = False if not np.iterable(dx):