diff --git a/.travis.yml b/.travis.yml index 6a69c3052713..b6277de0be76 100644 --- a/.travis.yml +++ b/.travis.yml @@ -110,7 +110,7 @@ install: # Install nose from a build which has partial # support for python36 and suport for coverage output suppressing pip install git+https://github.com/jenshnielsen/nose.git@matplotlibnose - + pip install pytest # We manually install humor sans using the package from Ubuntu 14.10. Unfortunatly humor sans is not # availible in the Ubuntu version used by Travis but we can manually install the deb from a later # version since is it basically just a .ttf file diff --git a/appveyor.yml b/appveyor.yml index 58d343641fb5..78752beedf87 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -82,7 +82,7 @@ install: # same things as the requirements in ci/conda_recipe/meta.yaml # if conda-forge gets a new pyqt, it might be nice to install it as well to have more backends # https://github.com/conda-forge/conda-forge.github.io/issues/157#issuecomment-223536381 - - cmd: conda create -q -n test-environment python=%PYTHON_VERSION% pip setuptools numpy python-dateutil freetype=2.6 msinttypes "tk=8.5" pyparsing pytz tornado "libpng>=1.6.21,<1.7" "zlib=1.2" "cycler>=0.10" nose mock + - cmd: conda create -q -n test-environment python=%PYTHON_VERSION% pip setuptools numpy python-dateutil freetype=2.6 msinttypes "tk=8.5" pyparsing pytz tornado "libpng>=1.6.21,<1.7" "zlib=1.2" "cycler>=0.10" nose mock pytest - activate test-environment - cmd: echo %PYTHON_VERSION% %TARGET_ARCH% - cmd: IF %PYTHON_VERSION% == 2.7 conda install -q functools32 diff --git a/build_alllocal.cmd b/build_alllocal.cmd deleted file mode 100644 index 9eb9ceadbc68..000000000000 --- a/build_alllocal.cmd +++ /dev/null @@ -1,36 +0,0 @@ -:: This assumes you have installed all the dependencies via conda packages: -:: # create a new environment with the required packages -:: conda create -n "matplotlib_build" python=3.4 numpy python-dateutil pyparsing pytz tornado "cycler>=0.10" tk libpng zlib freetype -:: activate matplotlib_build -:: if you want qt backend, you also have to install pyqt -:: conda install pyqt -:: # this package is only available in the conda-forge channel -:: conda install -c conda-forge msinttypes -:: if you build on py2.7: -:: conda install -c conda-forge functools32 - -set TARGET=bdist_wheel -IF [%1]==[] ( - echo Using default target: %TARGET% -) else ( - set TARGET=%1 - echo Using user supplied target: %TARGET% -) - -IF NOT DEFINED CONDA_PREFIX ( - echo No Conda env activated: you need to create a conda env with the right packages and activate it! - GOTO:eof -) - -:: copy the libs which have "wrong" names -set LIBRARY_LIB=%CONDA_PREFIX%\Library\lib -mkdir lib || cmd /c "exit /b 0" -copy %LIBRARY_LIB%\zlibstatic.lib lib\z.lib -copy %LIBRARY_LIB%\libpng_static.lib lib\png.lib - -:: Make the header files and the rest of the static libs available during the build -:: CONDA_PREFIX is a env variable which is set to the currently active environment path -set MPLBASEDIRLIST=%CONDA_PREFIX%\Library\;. - -:: build the target -python setup.py %TARGET% diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index 276c09605a70..cbccdb695489 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -642,7 +642,7 @@ def __init__(self, axes, pickradius=15): self.offsetText = self._get_offset_text() self.majorTicks = [] self.minorTicks = [] - self.unit_data = [] + self.unit_data = None self.pickradius = pickradius # Initialize here for testing; later add API @@ -695,14 +695,14 @@ def limit_range_for_scale(self, vmin, vmax): @property def unit_data(self): - """Holds data that a ConversionInterface subclass relys on + """Holds data that a ConversionInterface subclass uses to convert between labels and indexes """ return self._unit_data @unit_data.setter - def unit_data(self, data): - self._unit_data = data + def unit_data(self, unit_data): + self._unit_data = unit_data def get_children(self): children = [self.label, self.offsetText] diff --git a/lib/matplotlib/category.py b/lib/matplotlib/category.py index bfac242149c3..4abdf4b86a4b 100644 --- a/lib/matplotlib/category.py +++ b/lib/matplotlib/category.py @@ -9,9 +9,10 @@ import numpy as np -import matplotlib.units as units -import matplotlib.ticker as ticker - +import matplotlib.colors as mcolors +import matplotlib.cbook as cbook +import matplotlib.units as munits +import matplotlib.ticker as mticker # pure hack for numpy 1.6 support from distutils.version import LooseVersion @@ -22,54 +23,148 @@ def to_array(data, maxlen=100): if NP_NEW: return np.array(data, dtype=np.unicode) + if cbook.is_scalar_or_string(data): + data = [data] try: vals = np.array(data, dtype=('|S', maxlen)) except UnicodeEncodeError: - # pure hack + # this yields gibberish vals = np.array([convert_to_string(d) for d in data]) return vals -class StrCategoryConverter(units.ConversionInterface): +class StrCategoryConverter(munits.ConversionInterface): + """Converts categorical (or string) data to numerical values + + Conversion typically happens in the following order: + 1. default_units: + create unit_data category-integer mapping and binds to axis + 2. axis_info: + set ticks/locator and labels/formatter + 3. convert: + map input category data to integers using unit_data + """ @staticmethod def convert(value, unit, axis): - """Uses axis.unit_data map to encode - data as floats """ - vmap = dict(axis.unit_data) + Encode value as floats using axis.unit_data + """ + vmap = dict(zip(axis.unit_data.seq, axis.unit_data.locs)) if isinstance(value, six.string_types): - return vmap[value] + return vmap.get(value, None) vals = to_array(value) - for lab, loc in axis.unit_data: + for lab, loc in vmap.items(): vals[vals == lab] = loc - return vals.astype('float') + return vals.astype('float64') @staticmethod def axisinfo(unit, axis): - seq, locs = zip(*axis.unit_data) - majloc = StrCategoryLocator(locs) - majfmt = StrCategoryFormatter(seq) - return units.AxisInfo(majloc=majloc, majfmt=majfmt) + """ + Return the :class:`~matplotlib.units.AxisInfo` for *unit*. + + *unit* is None + *axis.unit_data* is used to set ticks and labels + """ + majloc = StrCategoryLocator(axis.unit_data.locs) + majfmt = StrCategoryFormatter(axis.unit_data.seq) + return munits.AxisInfo(majloc=majloc, majfmt=majfmt) @staticmethod - def default_units(data, axis): - # the conversion call stack is: - # default_units->axis_info->convert - axis.unit_data = map_categories(data, axis.unit_data) - return None + def default_units(data, axis, sort=True, normed=False): + """ + Create mapping between string categories in *data* + and integers, and store in *axis.unit_data* + """ + if axis and axis.unit_data: + axis.unit_data.update(data, sort) + return axis.unit_data + + unit_data = UnitData(data, sort) + if axis: + axis.unit_data = unit_data + return unit_data -class StrCategoryLocator(ticker.FixedLocator): +class StrCategoryLocator(mticker.FixedLocator): + """ + Ensures that every category has a tick by subclassing + :class:`~matplotlib.ticker.FixedLocator` + """ def __init__(self, locs): - super(StrCategoryLocator, self).__init__(locs, None) + self.locs = locs + self.nbins = None -class StrCategoryFormatter(ticker.FixedFormatter): +class StrCategoryFormatter(mticker.FixedFormatter): + """ + Labels every category by subclassing + :class:`~matplotlib.ticker.FixedFormatter` + """ def __init__(self, seq): - super(StrCategoryFormatter, self).__init__(seq) + self.seq = seq + self.offset_string = '' + + +class CategoryNorm(mcolors.Normalize): + """ + Preserves ordering of discrete values + """ + def __init__(self, data): + """ + *categories* + distinct values for mapping + + Out-of-range values are mapped to np.nan + """ + + self.units = StrCategoryConverter() + self.unit_data = None + self.units.default_units(data, + self, sort=False) + self.loc2seq = dict(zip(self.unit_data.locs, self.unit_data.seq)) + self.vmin = min(self.unit_data.locs) + self.vmax = max(self.unit_data.locs) + + def __call__(self, value, clip=None): + # gonna have to go into imshow and undo casting + value = np.asarray(value, dtype=np.int) + ret = self.units.convert(value, None, self) + # knock out values not in the norm + mask = np.in1d(ret, self.unit_data.locs).reshape(ret.shape) + # normalize ret & locs + ret /= self.vmax + return np.ma.array(ret, mask=~mask) + + def inverse(self, value): + if not cbook.iterable(value): + value = np.asarray(value) + vscaled = np.asarray(value) * self.vmax + return [self.loc2seq[int(vs)] for vs in vscaled] + + +def colors_from_categories(codings): + """ + Helper routine to generate a cmap and a norm from a list + of (color, value) pairs + + Parameters + ---------- + codings : sequence of (key, value) pairs + + Returns + ------- + (cmap, norm) : tuple containing a :class:`Colormap` and a \ + :class:`Normalize` instance + """ + if isinstance(codings, dict): + codings = cbook.sanitize_sequence(codings.items()) + values, colors = zip(*codings) + cmap = mcolors.ListedColormap(list(colors)) + norm = CategoryNorm(list(values)) + return cmap, norm def convert_to_string(value): @@ -77,8 +172,8 @@ def convert_to_string(value): np.array(...,dtype=unicode) for all later versions of numpy""" if isinstance(value, six.string_types): - return value - if np.isfinite(value): + pass + elif np.isfinite(value): value = np.asarray(value, dtype=str)[np.newaxis][0] elif np.isnan(value): value = 'nan' @@ -91,61 +186,53 @@ def convert_to_string(value): return value -def map_categories(data, old_map=None): - """Create mapping between unique categorical - values and numerical identifier. - - Paramters - --------- - data: iterable - sequence of values - old_map: list of tuple, optional - if not `None`, than old_mapping will be updated with new values and - previous mappings will remain unchanged) - sort: bool, optional - sort keys by ASCII value - - Returns - ------- - list of tuple - [(label, ticklocation),...] - - """ - - # code typical missing data in the negative range because - # everything else will always have positive encoding - # question able if it even makes sense +class UnitData(object): + # debatable if it makes sense to special code missing values spdict = {'nan': -1.0, 'inf': -2.0, '-inf': -3.0} - if isinstance(data, six.string_types): - data = [data] - - # will update this post cbook/dict support - strdata = to_array(data) - uniq = np.unique(strdata) - - if old_map: - olabs, okeys = zip(*old_map) - svalue = max(okeys) + 1 - else: - old_map, olabs, okeys = [], [], [] - svalue = 0 - - category_map = old_map[:] - - new_labs = [u for u in uniq if u not in olabs] - missing = [nl for nl in new_labs if nl in spdict.keys()] - - category_map.extend([(m, spdict[m]) for m in missing]) - - new_labs = [nl for nl in new_labs if nl not in missing] - - new_locs = np.arange(svalue, svalue + len(new_labs), dtype='float') - category_map.extend(list(zip(new_labs, new_locs))) - return category_map - + def __init__(self, data, sort=True): + """Create mapping between unique categorical values + and numerical identifier + Paramters + --------- + data: iterable + sequence of values + sort: bool + sort input data, default is True + False preserves input order + """ + self.seq, self.locs = [], [] + self._set_seq_locs(data, 0, sort) + self.sort = sort + + def update(self, new_data, sort=True): + if sort: + self.sort = sort + # so as not to conflict with spdict + value = max(max(self.locs) + 1, 0) + self._set_seq_locs(new_data, value, self.sort) + + def _set_seq_locs(self, data, value, sort): + # magic to make it work under np1.6 + strdata = to_array(data) + + # np.unique makes dateframes work + if sort: + unq = np.unique(strdata) + else: + _, idx = np.unique(strdata, return_index=~sort) + unq = strdata[np.sort(idx)] + + new_s = [d for d in unq if d not in self.seq] + for ns in new_s: + self.seq.append(convert_to_string(ns)) + if ns in UnitData.spdict.keys(): + self.locs.append(UnitData.spdict[ns]) + else: + self.locs.append(value) + value += 1 # Connects the convertor to matplotlib -units.registry[str] = StrCategoryConverter() -units.registry[bytes] = StrCategoryConverter() -units.registry[six.text_type] = StrCategoryConverter() +munits.registry[str] = StrCategoryConverter() +munits.registry[bytes] = StrCategoryConverter() +munits.registry[six.text_type] = StrCategoryConverter() diff --git a/lib/matplotlib/colorbar.py b/lib/matplotlib/colorbar.py index 67cdae563d52..dff73fbc519a 100644 --- a/lib/matplotlib/colorbar.py +++ b/lib/matplotlib/colorbar.py @@ -30,6 +30,7 @@ import matplotlib as mpl import matplotlib.artist as martist +import matplotlib.category as category import matplotlib.cbook as cbook import matplotlib.collections as collections import matplotlib.colors as colors @@ -312,6 +313,8 @@ def __init__(self, ax, cmap=None, if format is None: if isinstance(self.norm, colors.LogNorm): self.formatter = ticker.LogFormatterMathtext() + elif isinstance(self.norm, category.CategoryNorm): + self.formatter = ticker.FixedFormatter(self.norm.unit_data.seq) else: self.formatter = ticker.ScalarFormatter() elif cbook.is_string_like(format): @@ -580,6 +583,8 @@ def _ticker(self): locator = ticker.FixedLocator(b, nbins=10) elif isinstance(self.norm, colors.LogNorm): locator = ticker.LogLocator() + elif isinstance(self.norm, category.CategoryNorm): + locator = ticker.FixedLocator(self.norm.unit_data.locs) else: if mpl.rcParams['_internal.classic_mode']: locator = ticker.MaxNLocator() diff --git a/lib/matplotlib/tests/test_category.py b/lib/matplotlib/tests/test_category.py index 02db774e4ff6..cdf8af1f51a1 100644 --- a/lib/matplotlib/tests/test_category.py +++ b/lib/matplotlib/tests/test_category.py @@ -2,254 +2,254 @@ """Catch all for categorical functions""" from __future__ import (absolute_import, division, print_function, unicode_literals) -import unittest +import pytest import numpy as np import matplotlib.pyplot as plt from matplotlib.testing.decorators import cleanup import matplotlib.category as cat +import unittest -class TestConvertToString(unittest.TestCase): - def setUp(self): - pass - - def test_string(self): - self.assertEqual("abc", cat.convert_to_string("abc")) - def test_unicode(self): - self.assertEqual("Здравствуйте мир", - cat.convert_to_string("Здравствуйте мир")) +class TestConvertToString(object): + testdata = [("abc", "abc"), ("Здравствуйте мир", "Здравствуйте мир"), + ("3.14", 3.14), ("nan", np.nan), + ("inf", np.inf), ("-inf", -np.inf)] + ids = ["string", "unicode", "decimal", "nan", "posinf", "neginf", ] - def test_decimal(self): - self.assertEqual("3.14", cat.convert_to_string(3.14)) + @pytest.mark.parametrize("expected, test", testdata, ids=ids) + def test_convert_to_string(self, expected, test): + assert expected == cat.convert_to_string(test) - def test_nan(self): - self.assertEqual("nan", cat.convert_to_string(np.nan)) - def test_posinf(self): - self.assertEqual("inf", cat.convert_to_string(np.inf)) +class TestUnitData(object): + testdata = [("hello world", ["hello world"], [0]), + ("Здравствуйте мир", ["Здравствуйте мир"], [0]), + (['A', 'A', np.nan, 'B', -np.inf, 3.14, np.inf], + ['-inf', '3.14', 'A', 'B', 'inf', 'nan'], + [-3.0, 0, 1, 2, -2.0, -1.0])] - def test_neginf(self): - self.assertEqual("-inf", cat.convert_to_string(-np.inf)) + ids = ["single", "unicode", "mixed"] + @pytest.mark.parametrize("data, seq, locs", testdata, ids=ids) + def test_unit(self, data, seq, locs): + act = cat.UnitData(data) + assert act.seq == seq + assert act.locs == locs -class TestMapCategories(unittest.TestCase): - def test_map_unicode(self): - act = cat.map_categories("Здравствуйте мир") - exp = [("Здравствуйте мир", 0)] - self.assertListEqual(act, exp) + def test_update_map(self): + data = ['a', 'd'] + oseq = ['a', 'd'] + olocs = [0, 1] - def test_map_data(self): - act = cat.map_categories("hello world") - exp = [("hello world", 0)] - self.assertListEqual(act, exp) + data_update = ['b', 'd', 'e', np.inf] + useq = ['a', 'd', 'b', 'e', 'inf'] + ulocs = [0, 1, 2, 3, -2] - def test_map_data_basic(self): - data = ['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c'] - exp = [('a', 0), ('b', 1), ('c', 2)] - act = cat.map_categories(data) - self.assertListEqual(sorted(act), sorted(exp)) + unitdata = cat.UnitData(data) + assert unitdata.seq == oseq + assert unitdata.locs == olocs - def test_map_data_mixed(self): - data = ['A', 'A', np.nan, 'B', -np.inf, 3.14, np.inf] - exp = [('nan', -1), ('3.14', 0), - ('A', 1), ('B', 2), ('-inf', -3), ('inf', -2)] + unitdata.update(data_update) + assert unitdata.seq == useq + assert unitdata.locs == ulocs - act = cat.map_categories(data) - self.assertListEqual(sorted(act), sorted(exp)) - @unittest.SkipTest - def test_update_map(self): - data = ['b', 'd', 'e', np.inf] - old_map = [('a', 0), ('d', 1)] - exp = [('inf', -2), ('a', 0), ('d', 1), - ('b', 2), ('e', 3)] - act = cat.map_categories(data, old_map) - self.assertListEqual(sorted(act), sorted(exp)) +class FakeAxis(object): + def __init__(self, unit_data): + self.unit_data = unit_data -class FakeAxis(object): - def __init__(self): - self.unit_data = [] +class MockUnitData(object): + def __init__(self, data): + seq, locs = zip(*data) + self.seq = list(seq) + self.locs = list(locs) -class TestStrCategoryConverter(unittest.TestCase): +class TestStrCategoryConverter(object): """Based on the pandas conversion and factorization tests: ref: /pandas/tseries/tests/test_converter.py /pandas/tests/test_algos.py:TestFactorize """ - - def setUp(self): + testdata = [("Здравствуйте мир", [("Здравствуйте мир", 42)], 42), + ("hello world", [("hello world", 42)], 42), + (['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c'], + [('a', 0), ('b', 1), ('c', 2)], + [0, 1, 1, 0, 0, 2, 2, 2]), + (['A', 'A', np.nan, 'B', -np.inf, 3.14, np.inf], + [('nan', -1), ('3.14', 0), ('A', 1), ('B', 2), + ('-inf', 100), ('inf', 200)], + [1, 1, -1, 2, 100, 0, 200])] + ids = ["unicode", "single", "basic", "mixed"] + + @pytest.fixture(autouse=True) + def mock_axis(self, request): self.cc = cat.StrCategoryConverter() - self.axis = FakeAxis() - - def test_convert_unicode(self): - self.axis.unit_data = [("Здравствуйте мир", 42)] - act = self.cc.convert("Здравствуйте мир", None, self.axis) - exp = 42 - self.assertEqual(act, exp) - - def test_convert_single(self): - self.axis.unit_data = [("hello world", 42)] - act = self.cc.convert("hello world", None, self.axis) - exp = 42 - self.assertEqual(act, exp) - - def test_convert_basic(self): - data = ['a', 'b', 'b', 'a', 'a', 'c', 'c', 'c'] - exp = [0, 1, 1, 0, 0, 2, 2, 2] - self.axis.unit_data = [('a', 0), ('b', 1), ('c', 2)] - act = self.cc.convert(data, None, self.axis) - np.testing.assert_array_equal(act, exp) - def test_convert_mixed(self): - data = ['A', 'A', np.nan, 'B', -np.inf, 3.14, np.inf] - exp = [1, 1, -1, 2, 100, 0, 200] - self.axis.unit_data = [('nan', -1), ('3.14', 0), - ('A', 1), ('B', 2), - ('-inf', 100), ('inf', 200)] - act = self.cc.convert(data, None, self.axis) + @pytest.mark.parametrize("data, unitmap, exp", testdata, ids=ids) + def test_convert(self, data, unitmap, exp): + MUD = MockUnitData(unitmap) + axis = FakeAxis(MUD) + act = self.cc.convert(data, None, axis) np.testing.assert_array_equal(act, exp) def test_axisinfo(self): - self.axis.unit_data = [('a', 0)] - ax = self.cc.axisinfo(None, self.axis) - self.assertTrue(isinstance(ax.majloc, cat.StrCategoryLocator)) - self.assertTrue(isinstance(ax.majfmt, cat.StrCategoryFormatter)) + MUD = MockUnitData([(None, None)]) + axis = FakeAxis(MUD) + ax = self.cc.axisinfo(None, axis) + assert isinstance(ax.majloc, cat.StrCategoryLocator) + assert isinstance(ax.majfmt, cat.StrCategoryFormatter) def test_default_units(self): - self.assertEqual(self.cc.default_units(["a"], self.axis), None) + axis = FakeAxis(None) + assert isinstance(self.cc.default_units(["a"], axis), cat.UnitData) -class TestStrCategoryLocator(unittest.TestCase): - def setUp(self): - self.locs = list(range(10)) - +class TestStrCategoryLocator(object): def test_StrCategoryLocator(self): - ticks = cat.StrCategoryLocator(self.locs) - np.testing.assert_equal(ticks.tick_values(None, None), - self.locs) + locs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + ticks = cat.StrCategoryLocator(locs) + np.testing.assert_array_equal(ticks.tick_values(None, None), locs) class TestStrCategoryFormatter(unittest.TestCase): - def setUp(self): - self.seq = ["hello", "world", "hi"] - def test_StrCategoryFormatter(self): - labels = cat.StrCategoryFormatter(self.seq) - self.assertEqual(labels('a', 1), "world") + seq = ["hello", "world", "hi"] + labels = cat.StrCategoryFormatter(seq) + assert labels('a', 1) == "world" + + def test_StrCategoryFormatterUnicode(self): + seq = ["Здравствуйте", "привет"] + labels = cat.StrCategoryFormatter(seq) + assert labels('a', 1) == "привет" + + +class TestCategoryNorm(object): + testdata = [[[205, 302, 205, 101], [0, 1, 0, .5]], + [[205, np.nan, 101, 305], [0, np.nan, .5, 1]], + [[205, 101, 504, 101], [0, .5, np.nan, .5]]] + + ids = ["regular", "nan", "exclude"] + + @pytest.mark.parametrize("data, nmap", testdata, ids=ids) + def test_norm(self, data, nmap): + norm = cat.CategoryNorm([205, 101, 302]) + masked_nmap = np.ma.masked_equal(nmap, np.nan) + assert np.ma.allequal(norm(data), masked_nmap) + + def test_invert(self): + data = [205, 302, 101] + strdata = ['205', '302', '101'] + value = [0, .5, 1] + norm = cat.CategoryNorm(data) + assert norm.inverse(value) == strdata + + +class TestColorsFromCategories(object): + testdata = [[{'101': "blue", '205': "red", '302': "green"}, dict], + [[('205', "red"), ('101', "blue"), ('302', "green")], list]] + ids = ["dict", "tuple"] + + @pytest.mark.parametrize("codings, mtype", testdata, ids=ids) + def test_colors_from_categories(self, codings, mtype): + cmap, norm = cat.colors_from_categories(codings) + assert mtype(zip(norm.unit_data.seq, cmap.colors)) == codings def lt(tl): return [l.get_text() for l in tl] -class TestPlot(unittest.TestCase): - - def setUp(self): +class TestPlot(object): + @pytest.fixture + def data(self): self.d = ['a', 'b', 'c', 'a'] self.dticks = [0, 1, 2] self.dlabels = ['a', 'b', 'c'] - self.dunit_data = [('a', 0), ('b', 1), ('c', 2)] + unitmap = [('a', 0), ('b', 1), ('c', 2)] + self.dunit_data = MockUnitData(unitmap) + @pytest.fixture + def missing_data(self): self.dm = ['here', np.nan, 'here', 'there'] - self.dmticks = [-1, 0, 1] - self.dmlabels = ['nan', 'here', 'there'] - self.dmunit_data = [('nan', -1), ('here', 0), ('there', 1)] + self.dmticks = [0, -1, 1] + self.dmlabels = ['here', 'nan', 'there'] + unitmap = [('here', 0), ('nan', -1), ('there', 1)] + self.dmunit_data = MockUnitData(unitmap) + + def axis_test(self, axis, ticks, labels, unit_data): + np.testing.assert_array_equal(axis.get_majorticklocs(), ticks) + assert lt(axis.get_majorticklabels()) == labels + np.testing.assert_array_equal(axis.unit_data.locs, unit_data.locs) + assert axis.unit_data.seq == unit_data.seq @cleanup def test_plot_unicode(self): - # needs image test - works but - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) + # Image test would fail on numpy 1.6 words = ['Здравствуйте', 'привет'] locs = [0.0, 1.0] + unit_data = MockUnitData(zip(words, locs)) + + fig, ax = plt.subplots() ax.plot(words) fig.canvas.draw() - self.assertListEqual(ax.yaxis.unit_data, - list(zip(words, locs))) - np.testing.assert_array_equal(ax.get_yticks(), locs) - self.assertListEqual(lt(ax.get_yticklabels()), words) + self.axis_test(ax.yaxis, locs, words, unit_data) @cleanup + @pytest.mark.usefixtures("data") def test_plot_1d(self): - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) + fig, ax = plt.subplots() ax.plot(self.d) fig.canvas.draw() - np.testing.assert_array_equal(ax.get_yticks(), self.dticks) - self.assertListEqual(lt(ax.get_yticklabels()), - self.dlabels) - self.assertListEqual(ax.yaxis.unit_data, self.dunit_data) + self.axis_test(ax.yaxis, self.dticks, self.dlabels, self.dunit_data) @cleanup + @pytest.mark.usefixtures("missing_data") def test_plot_1d_missing(self): - - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) + fig, ax = plt.subplots() ax.plot(self.dm) fig.canvas.draw() - np.testing.assert_array_equal(ax.get_yticks(), self.dmticks) - self.assertListEqual(lt(ax.get_yticklabels()), - self.dmlabels) - self.assertListEqual(ax.yaxis.unit_data, self.dmunit_data) + self.axis_test(ax.yaxis, self.dmticks, self.dmlabels, self.dmunit_data) @cleanup + @pytest.mark.usefixtures("data", "missing_data") def test_plot_2d(self): - - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) + fig, ax = plt.subplots() ax.plot(self.dm, self.d) fig.canvas.draw() - np.testing.assert_array_equal(ax.get_xticks(), self.dmticks) - self.assertListEqual(lt(ax.get_xticklabels()), - self.dmlabels) - self.assertListEqual(ax.xaxis.unit_data, self.dmunit_data) - - np.testing.assert_array_equal(ax.get_yticks(), self.dticks) - self.assertListEqual(lt(ax.get_yticklabels()), - self.dlabels) - self.assertListEqual(ax.yaxis.unit_data, self.dunit_data) + self.axis_test(ax.xaxis, self.dmticks, self.dmlabels, self.dmunit_data) + self.axis_test(ax.yaxis, self.dticks, self.dlabels, self.dunit_data) @cleanup + @pytest.mark.usefixtures("data", "missing_data") def test_scatter_2d(self): - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) + fig, ax = plt.subplots() ax.scatter(self.dm, self.d) fig.canvas.draw() - np.testing.assert_array_equal(ax.get_xticks(), self.dmticks) - self.assertListEqual(lt(ax.get_xticklabels()), - self.dmlabels) - self.assertListEqual(ax.xaxis.unit_data, self.dmunit_data) - - np.testing.assert_array_equal(ax.get_yticks(), self.dticks) - self.assertListEqual(lt(ax.get_yticklabels()), - self.dlabels) - self.assertListEqual(ax.yaxis.unit_data, self.dunit_data) + self.axis_test(ax.xaxis, self.dmticks, self.dmlabels, self.dmunit_data) + self.axis_test(ax.yaxis, self.dticks, self.dlabels, self.dunit_data) - @unittest.SkipTest @cleanup def test_plot_update(self): - fig = plt.figure() - ax = fig.add_subplot(1, 1, 1) + fig, ax = plt.subplots() ax.plot(['a', 'b']) ax.plot(['a', 'b', 'd']) ax.plot(['b', 'c', 'd']) fig.canvas.draw() - labels_new = ['a', 'b', 'd', 'c'] - ticks_new = [0, 1, 2, 3] - self.assertListEqual(ax.yaxis.unit_data, - list(zip(labels_new, ticks_new))) - np.testing.assert_array_equal(ax.get_yticks(), ticks_new) - self.assertListEqual(lt(ax.get_yticklabels()), labels_new) + labels = ['a', 'b', 'd', 'c'] + ticks = [0, 1, 2, 3] + unit_data = MockUnitData(list(zip(labels, ticks))) + + self.axis_test(ax.yaxis, ticks, labels, unit_data) diff --git a/tox.ini b/tox.ini index 296cefb56281..f1d3bc8669ca 100644 --- a/tox.ini +++ b/tox.ini @@ -4,7 +4,7 @@ # and then run "tox" from this directory. [tox] -envlist = py26, py27, py31, py32 +envlist = py26, py27, py31, py32, py35 [testenv] changedir = /tmp @@ -15,3 +15,4 @@ deps = nose mock numpy + pytest \ No newline at end of file