From 0925e38fc9fbd004fdeece3695b57aa9955167bf Mon Sep 17 00:00:00 2001 From: Ryan May Date: Mon, 14 Sep 2015 10:13:39 -0600 Subject: [PATCH 1/3] Add test for unit conversion. This is catching the problems with pint-like libraries who wrap numpy arrays. --- lib/matplotlib/tests/test_units.py | 57 ++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 lib/matplotlib/tests/test_units.py diff --git a/lib/matplotlib/tests/test_units.py b/lib/matplotlib/tests/test_units.py new file mode 100644 index 000000000000..1112b0f22574 --- /dev/null +++ b/lib/matplotlib/tests/test_units.py @@ -0,0 +1,57 @@ +import matplotlib.pyplot as plt +import matplotlib.units as munits +import numpy as np + +try: + # mock in python 3.3+ + from unittest.mock import MagicMock +except ImportError: + from mock import MagicMock + + +# Tests that the conversion machinery works properly for classes that +# work as a facade over numpy arrays (like pint) +def test_numpy_facade(): + # Basic class that wraps numpy array and has units + class Quantity(object): + def __init__(self, data, units): + self.magnitude = data + self.units = units + + def to(self, new_units): + return Quantity(self.magnitude, new_units) + + def __getattr__(self, attr): + return getattr(self.magnitude, attr) + + def __getitem__(self, item): + return self.magnitude[item] + + # Create an instance of the conversion interface and + # mock so we can check methods called + qc = munits.ConversionInterface() + + def convert(value, unit, axis): + if hasattr(value, 'units'): + return value.to(unit) + else: + return Quantity(value, axis.get_units()).to(unit).magnitude + + qc.convert = MagicMock(side_effect=convert) + qc.axisinfo = MagicMock(return_value=None) + qc.default_units = MagicMock(side_effect=lambda x, a: x.units) + + # Register the class + munits.registry[Quantity] = qc + + # Simple test + t = Quantity(np.linspace(0, 10), 'sec') + d = Quantity(30 * np.linspace(0, 10), 'm/s') + + fig, ax = plt.subplots(1, 1) + l, = plt.plot(t, d) + ax.yaxis.set_units('inch') + + assert qc.convert.called + assert qc.axisinfo.called + assert qc.default_units.called From 1d43e39d21706bf5145cc8f0c60aaa11b59e050f Mon Sep 17 00:00:00 2001 From: Ryan May Date: Mon, 27 Jul 2015 22:24:44 -0500 Subject: [PATCH 2/3] Remove some direct calls to atleast_1d. Even if the argument to atleast_1d is array-compatible and has sufficient dimensions, it is still converted from its original type to a numpy array. This is overly aggressive for our purposes (especially unit support), so replace the direct call with a helper function that only calls atleast_1d if it's actually necessary. --- lib/matplotlib/axes/_base.py | 13 ++++++++----- lib/matplotlib/cbook.py | 15 +++++++++++++++ 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index cc250ab7ac62..57eb3975b4c0 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -15,7 +15,8 @@ import matplotlib from matplotlib import cbook -from matplotlib.cbook import _string_to_bool, iterable, index_of, get_label +from matplotlib.cbook import (_check_1d, _string_to_bool, iterable, + index_of, get_label) from matplotlib import docstring import matplotlib.colors as mcolors import matplotlib.lines as mlines @@ -214,8 +215,10 @@ def _xy_from_xy(self, x, y): if by: y = self.axes.convert_yunits(y) - x = np.atleast_1d(x) # like asanyarray, but converts scalar to array - y = np.atleast_1d(y) + # like asanyarray, but converts scalar to array, and doesn't change + # existing compatible sequences + x = _check_1d(x) + y = _check_1d(y) if x.shape[0] != y.shape[0]: raise ValueError("x and y must have same first dimension") if x.ndim > 2 or y.ndim > 2: @@ -353,8 +356,8 @@ def _plot_args(self, tup, kwargs): kwargs['label'] = get_label(tup[-1], None) if len(tup) == 2: - x = np.atleast_1d(tup[0]) - y = np.atleast_1d(tup[-1]) + x = _check_1d(tup[0]) + y = _check_1d(tup[-1]) else: x, y = index_of(tup[-1]) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 506e34a4fcd2..bc7e4df568b4 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2192,6 +2192,21 @@ def is_math_text(s): return even_dollars +def _check_1d(x): + ''' + Converts a sequence of less than 1 dimension, to an array of 1 + dimension; leaves everything else untouched. + ''' + if not hasattr(x, 'shape') or len(x.shape) < 1: + return np.atleast_1d(x) + else: + try: + x[:, None] + return x + except (IndexError, TypeError): + return np.atleast_1d(x) + + def _reshape_2D(X): """ Converts a non-empty list or an ndarray of two or fewer dimensions From 9b65233f7b6ea18fed6cd205035471840843e4e7 Mon Sep 17 00:00:00 2001 From: Ryan May Date: Mon, 14 Sep 2015 11:10:32 -0600 Subject: [PATCH 3/3] Add test_units to whitelist. --- lib/matplotlib/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 1f95bb1867ee..7f4525fc9277 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -1465,6 +1465,7 @@ def tk_window_focus(): 'matplotlib.tests.test_tightlayout', 'matplotlib.tests.test_transforms', 'matplotlib.tests.test_triangulation', + 'matplotlib.tests.test_units', 'matplotlib.tests.test_widgets', 'matplotlib.tests.test_cycles', 'matplotlib.tests.test_labeled_data_unpacking',