From 4d57690bbf478b91c34dd7aa58f75d4a7def5782 Mon Sep 17 00:00:00 2001 From: hannah Date: Thu, 8 Feb 2018 00:30:40 -0500 Subject: [PATCH] addressing documentation comments + more use of units --- .appveyor.yml | 2 +- .travis.yml | 2 +- doc/api/next_api_changes/2018-02-10-HA.rst | 10 ++ lib/matplotlib/axes/_axes.py | 2 +- lib/matplotlib/axis.py | 11 +- lib/matplotlib/category.py | 123 +++++++++++++-------- lib/matplotlib/tests/test_category.py | 36 +++--- 7 files changed, 118 insertions(+), 68 deletions(-) create mode 100644 doc/api/next_api_changes/2018-02-10-HA.rst diff --git a/.appveyor.yml b/.appveyor.yml index c8d6e22627f3..afd1faa72756 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -66,7 +66,7 @@ install: - activate test-environment - echo %PYTHON_VERSION% %TARGET_ARCH% # pytest-cov>=2.3.1 due to https://github.com/pytest-dev/pytest-cov/issues/124 - - pip install -q "pytest!=3.3.0" "pytest-cov>=2.3.1" pytest-rerunfailures pytest-timeout pytest-xdist + - pip install -q "pytest!=3.3.0,>=3.2.0" "pytest-cov>=2.3.1" pytest-rerunfailures pytest-timeout pytest-xdist # Apply patch to `subprocess` on Python versions > 2 and < 3.6.3 # https://github.com/matplotlib/matplotlib/issues/9176 diff --git a/.travis.yml b/.travis.yml index b85cf4e14797..4973ace7027b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -52,7 +52,7 @@ env: - NUMPY=numpy - PANDAS= - PYPARSING=pyparsing - - PYTEST=pytest!=3.3.0 + - PYTEST='pytest!=3.3.0,>=3.2.0' - PYTEST_COV=pytest-cov - PYTEST_PEP8= - SPHINX=sphinx diff --git a/doc/api/next_api_changes/2018-02-10-HA.rst b/doc/api/next_api_changes/2018-02-10-HA.rst new file mode 100644 index 000000000000..6483d8c8345b --- /dev/null +++ b/doc/api/next_api_changes/2018-02-10-HA.rst @@ -0,0 +1,10 @@ +Deprecated `Axis.unt_data` +`````````````````````````` + +Use `Axis.units` (which has long existed) instead. + +Only accept string-like for Categorical input +````````````````````````````````````````````` + +Do not accept mixed string / float / int input, only +strings are valid categoricals. diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index f028a95df663..a09a76bd7fb5 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -6442,7 +6442,7 @@ def hist(self, x, bins=None, range=None, density=None, weights=None, if normed is not None: warnings.warn("The 'normed' kwarg is deprecated, and has been " "replaced by the 'density' kwarg.") - + # basic input validation input_empty = np.size(x) == 0 # Massage 'x' for processing. diff --git a/lib/matplotlib/axis.py b/lib/matplotlib/axis.py index f113b095d334..70ec488673cc 100644 --- a/lib/matplotlib/axis.py +++ b/lib/matplotlib/axis.py @@ -720,9 +720,6 @@ def __init__(self, axes, pickradius=15): self.labelpad = rcParams['axes.labelpad'] self.offsetText = self._get_offset_text() - self.majorTicks = [] - self.minorTicks = [] - self.pickradius = pickradius # Initialize here for testing; later add API @@ -780,14 +777,14 @@ def limit_range_for_scale(self, vmin, vmax): return self._scale.limit_range_for_scale(vmin, vmax, self.get_minpos()) @property - @cbook.deprecated("2.1.1") + @cbook.deprecated("2.2.0") def unit_data(self): - return self._units + return self.units @unit_data.setter - @cbook.deprecated("2.1.1") + @cbook.deprecated("2.2.0") def unit_data(self, unit_data): - self.set_units = unit_data + self.set_units(unit_data) def get_children(self): children = [self.label, self.offsetText] diff --git a/lib/matplotlib/category.py b/lib/matplotlib/category.py index 1dd7c8aa469d..326d817df1ba 100644 --- a/lib/matplotlib/category.py +++ b/lib/matplotlib/category.py @@ -1,15 +1,19 @@ # -*- coding: utf-8 -*- """ -catch all for categorical functions +StrCategorical module for facilitating natively plotting String/Text data. +This module contains the conversion mechanism (a monotonic mapping from +strings to integers), tick locator and formatter, and the class:`.UnitData` +object that creates and stores the string to integer mapping. """ from __future__ import (absolute_import, division, print_function, unicode_literals) -from collections import Iterable, OrderedDict +from collections import OrderedDict import itertools import six + import numpy as np import matplotlib.units as units @@ -22,31 +26,26 @@ (bytes, six.text_type, np.str_, np.bytes_))) -def to_str(value): - """Helper function to turn values to strings. - """ - # Note: This function is only used by StrCategoryFormatter - if LooseVersion(np.__version__) < LooseVersion('1.7.0'): - if (isinstance(value, (six.text_type, np.unicode))): - value = value.encode('utf-8', 'ignore').decode('utf-8') - if isinstance(value, (np.bytes_, six.binary_type)): - value = value.decode(encoding='utf-8') - elif not isinstance(value, (np.str_, six.string_types)): - value = str(value) - return value - - class StrCategoryConverter(units.ConversionInterface): @staticmethod def convert(value, unit, axis): - """Uses axis.units to encode string data as floats + """Converts strings in value to floats using + mapping information store in the unit object Parameters ---------- - value: string, iterable - value or list of values to plot - unit: - axis: + value : string or iterable + value or list of values to be converted + unit : :class:`.UnitData` + object string unit information for value + axis : :class:`~matplotlib.Axis.axis` + axis on which the converted value is plotted + + Returns + ------- + mapped_ value : float or ndarray[float] + + .. note:: axis is not used in this function """ # dtype = object preserves numerical pass throughs values = np.atleast_1d(np.array(value, dtype=object)) @@ -57,9 +56,9 @@ def convert(value, unit, axis): return np.asarray(values, dtype=float) # force an update so it also does type checking - axis.units.update(values) + unit.update(values) - str2idx = np.vectorize(axis.units._mapping.__getitem__, + str2idx = np.vectorize(unit._mapping.__getitem__, otypes=[float]) mapped_value = str2idx(values) @@ -67,16 +66,43 @@ def convert(value, unit, axis): @staticmethod def axisinfo(unit, axis): - """Sets the axis ticks and labels + """Sets the default axis ticks and labels + + Parameters + --------- + unit : :class:`.UnitData` + object string unit information for value + axis : :class:`~matplotlib.Axis.axis` + axis for which information is being set + + Returns + ------- + :class:~matplotlib.units.AxisInfo~ + Information to support default tick labeling + + .. note: axis is not used """ # locator and formatter take mapping dict because # args need to be pass by reference for updates - majloc = StrCategoryLocator(axis.units) - majfmt = StrCategoryFormatter(axis.units) + majloc = StrCategoryLocator(unit._mapping) + majfmt = StrCategoryFormatter(unit._mapping) return units.AxisInfo(majloc=majloc, majfmt=majfmt) @staticmethod - def default_units(data=None, axis=None): + def default_units(data, axis): + """ Sets and updates the :class:`~matplotlib.Axis.axis~ units + + Parameters + ---------- + data : string or iterable of strings + axis : :class:`~matplotlib.Axis.axis` + axis on which the data is plotted + + Returns + ------- + class:~.UnitData~ + object storing string to integer mapping + """ # the conversion call stack is supposed to be # default_units->axis_info->convert if axis.units is None: @@ -88,17 +114,17 @@ def default_units(data=None, axis=None): class StrCategoryLocator(ticker.Locator): """tick at every integer mapping of the string data""" - def __init__(self, units): + def __init__(self, units_mapping): """ Parameters ----------- units: dict - (string, integer) mapping + string:integer mapping """ - self._units = units + self._units = units_mapping def __call__(self): - return list(self._units._mapping.values()) + return list(self._units.values()) def tick_values(self, vmin, vmax): return self() @@ -106,21 +132,35 @@ def tick_values(self, vmin, vmax): class StrCategoryFormatter(ticker.Formatter): """String representation of the data at every tick""" - def __init__(self, units): + def __init__(self, units_mapping): """ Parameters ---------- units: dict - (string, integer) mapping + string:integer mapping """ - self._units = units + self._units = units_mapping def __call__(self, x, pos=None): if pos is None: return "" - r_mapping = {v: to_str(k) for k, v in self._units._mapping.items()} + r_mapping = {v: StrCategoryFormatter._text(k) + for k, v in self._units.items()} return r_mapping.get(int(np.round(x)), '') + @staticmethod + def _text(value): + """Converts text values into `utf-8` or `ascii` strings + """ + if LooseVersion(np.__version__) < LooseVersion('1.7.0'): + if (isinstance(value, (six.text_type, np.unicode))): + value = value.encode('utf-8', 'ignore').decode('utf-8') + if isinstance(value, (np.bytes_, six.binary_type)): + value = value.decode(encoding='utf-8') + elif not isinstance(value, (np.str_, six.string_types)): + value = str(value) + return value + class UnitData(object): def __init__(self, data=None): @@ -130,11 +170,10 @@ def __init__(self, data=None): data: iterable sequence of string values """ - if data is None: - data = () self._mapping = OrderedDict() self._counter = itertools.count(start=0) - self.update(data) + if data is not None: + self.update(data) def update(self, data): """Maps new values to integer identifiers. @@ -149,13 +188,9 @@ def update(self, data): TypeError If the value in data is not a string, unicode, bytes type """ + data = np.atleast_1d(np.array(data, dtype=object)) - if (isinstance(data, VALID_TYPES) or - not isinstance(data, Iterable)): - data = [data] - - unsorted_unique = OrderedDict.fromkeys(data) - for val in unsorted_unique: + for val in OrderedDict.fromkeys(data): if not isinstance(val, VALID_TYPES): raise TypeError("{val!r} is not a string".format(val=val)) if val not in self._mapping: diff --git a/lib/matplotlib/tests/test_category.py b/lib/matplotlib/tests/test_category.py index de8eb49d7a75..40f9d078ec5e 100644 --- a/lib/matplotlib/tests/test_category.py +++ b/lib/matplotlib/tests/test_category.py @@ -9,6 +9,9 @@ import matplotlib.pyplot as plt import matplotlib.category as cat +# Python2/3 text handling +_to_str = cat.StrCategoryFormatter._text + class TestUnitData(object): test_cases = [('single', (["hello world"], [0])), @@ -86,53 +89,57 @@ class TestStrCategoryConverter(object): @pytest.fixture(autouse=True) def mock_axis(self, request): self.cc = cat.StrCategoryConverter() - # self.unit should be probably be replaced with real mock unit + # self.unit should be probably be replaced with real mock unit self.unit = cat.UnitData() self.ax = FakeAxis(self.unit) @pytest.mark.parametrize("vals", values, ids=ids) def test_convert(self, vals): - np.testing.assert_allclose(self.cc.convert(vals, None, self.ax), + np.testing.assert_allclose(self.cc.convert(vals, self.ax.units, + self.ax), range(len(vals))) @pytest.mark.parametrize("value", ["hi", "мир"], ids=["ascii", "unicode"]) def test_convert_one_string(self, value): - assert self.cc.convert(value, None, self.ax) == 0 + assert self.cc.convert(value, self.unit, self.ax) == 0 def test_convert_one_number(self): - actual = self.cc.convert(0.0, None, self.ax) + actual = self.cc.convert(0.0, self.unit, self.ax) np.testing.assert_allclose(actual, np.array([0.])) def test_convert_float_array(self): data = np.array([1, 2, 3], dtype=float) - actual = self.cc.convert(data, None, self.ax) + actual = self.cc.convert(data, self.unit, self.ax) np.testing.assert_allclose(actual, np.array([1., 2., 3.])) @pytest.mark.parametrize("fvals", fvalues, ids=fids) def test_convert_fail(self, fvals): with pytest.raises(TypeError): - self.cc.convert(fvals, None, self.ax) + self.cc.convert(fvals, self.unit, self.ax) def test_axisinfo(self): - axis = self.cc.axisinfo(None, self.ax) + axis = self.cc.axisinfo(self.unit, self.ax) assert isinstance(axis.majloc, cat.StrCategoryLocator) assert isinstance(axis.majfmt, cat.StrCategoryFormatter) def test_default_units(self): assert isinstance(self.cc.default_units(["a"], self.ax), cat.UnitData) + @pytest.fixture def ax(): return plt.figure().subplots() + PLOT_LIST = [Axes.scatter, Axes.plot, Axes.bar] PLOT_IDS = ["scatter", "plot", "bar"] + class TestStrCategoryLocator(object): def test_StrCategoryLocator(self): locs = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] unit = cat.UnitData([str(j) for j in locs]) - ticks = cat.StrCategoryLocator(unit) + ticks = cat.StrCategoryLocator(unit._mapping) np.testing.assert_array_equal(ticks.tick_values(None, None), locs) @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) @@ -150,16 +157,16 @@ class TestStrCategoryFormatter(object): @pytest.mark.parametrize("ydata", cases, ids=ids) def test_StrCategoryFormatter(self, ax, ydata): unit = cat.UnitData(ydata) - labels = cat.StrCategoryFormatter(unit) + labels = cat.StrCategoryFormatter(unit._mapping) for i, d in enumerate(ydata): - assert labels(i, i) == d + assert labels(i, i) == _to_str(d) @pytest.mark.parametrize("ydata", cases, ids=ids) @pytest.mark.parametrize("plotter", PLOT_LIST, ids=PLOT_IDS) def test_StrCategoryFormatterPlot(self, ax, ydata, plotter): plotter(ax, range(len(ydata)), ydata) for i, d in enumerate(ydata): - assert ax.yaxis.major.formatter(i, i) == d + assert ax.yaxis.major.formatter(i, i) == _to_str(d) assert ax.yaxis.major.formatter(i+1, i+1) == "" assert ax.yaxis.major.formatter(0, None) == "" @@ -168,7 +175,7 @@ def axis_test(axis, labels): ticks = list(range(len(labels))) np.testing.assert_array_equal(axis.get_majorticklocs(), ticks) graph_labels = [axis.major.formatter(i, i) for i in ticks] - assert graph_labels == [cat.to_str(l) for l in labels] + assert graph_labels == [_to_str(l) for l in labels] assert list(axis.units._mapping.keys()) == [l for l in labels] assert list(axis.units._mapping.values()) == ticks @@ -254,17 +261,18 @@ def test_update_plot(self, ax, plotter): PLOT_BROKEN_LIST = [Axes.scatter, pytest.param(Axes.plot, marks=pytest.mark.xfail), pytest.param(Axes.bar, marks=pytest.mark.xfail)] + PLOT_BROKEN_IDS = ["scatter", "plot", "bar"] @pytest.mark.parametrize("plotter", PLOT_BROKEN_LIST, ids=PLOT_BROKEN_IDS) @pytest.mark.parametrize("xdata", fvalues, ids=fids) - def test_plot_failures(self, ax, plotter, xdata): + def test_mixed_type_exception(self, ax, plotter, xdata): with pytest.raises(TypeError): plotter(ax, xdata, [1, 2]) @pytest.mark.parametrize("plotter", PLOT_BROKEN_LIST, ids=PLOT_BROKEN_IDS) @pytest.mark.parametrize("xdata", fvalues, ids=fids) - def test_plot_failures_update(self, ax, plotter, xdata): + def test_mixed_type_update_exception(self, ax, plotter, xdata): with pytest.raises(TypeError): plotter(ax, [0, 3], [1, 3]) plotter(ax, xdata, [1, 2])