From 2f73778aba90f330ef05c698c43e453af30bd8d4 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 24 Nov 2015 15:12:35 -0500 Subject: [PATCH 01/10] FIX: pandas indexing error pd.Series prefer indexing via searching the index to positional indexing. This method will get the first element of any iterable. It will advance a generater, but they did not work previously anyway. Closes #5550 --- lib/matplotlib/dates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 04c5da0f0996..3422c2b1fbe0 100755 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -1561,7 +1561,7 @@ def default_units(x, axis): x = x.ravel() try: - x = x[0] + x = next(iter(x)) except (TypeError, IndexError): pass From ab6d338c876e46cf9effc888c78f600473b1ac88 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 24 Nov 2015 15:24:00 -0500 Subject: [PATCH 02/10] TST: add test of bad pandas indexing --- lib/matplotlib/tests/test_axes.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index bd13f2960b52..8d7e3895bdd9 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -7,6 +7,7 @@ import io from nose.tools import assert_equal, assert_raises, assert_false, assert_true +from nose.plugins.skip import SkipTest import datetime @@ -4183,6 +4184,23 @@ def test_broken_barh_empty(): ax.broken_barh([], (.1, .5)) +@cleanup +def test_pandas_indexing_dates(): + try: + import pandas as pd + except ImportError: + raise SkipTest("Pandas not installed") + + dates = np.arange('2005-02', '2005-03', dtype='datetime64[D]') + values = np.sin(np.array(range(len(dates)))) + df = pd.DataFrame({'dates': dates, 'values': values}) + + ax = plt.gca() + + without_zero_index = df[np.array(df.index) % 2 == 1].copy() + ax.plot('dates', 'values', data=without_zero_index) + + if __name__ == '__main__': import nose import sys From af9d6eb9fe59bbc4e5933a0354c5f240fc3a87a0 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 12 Dec 2015 21:41:37 -0500 Subject: [PATCH 03/10] MNT: catch correct exceptions using `next(iter(obj))` can raise `TypeError` if obj is not iterable and `StopIteration` if len(obj) == 0. As we are no longer explictily indexing into `obj` it should not raise an `IndexError` --- lib/matplotlib/dates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 3422c2b1fbe0..28655e33486d 100755 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -1562,7 +1562,7 @@ def default_units(x, axis): try: x = next(iter(x)) - except (TypeError, IndexError): + except (TypeError, StopIteration): pass try: From 8495a1bd9f4f2385be27fdf52a1f7f1e533106ed Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Sat, 5 Dec 2015 14:56:32 -0500 Subject: [PATCH 04/10] MNT: remove redundant imports Both iterable and index_of are imported at the top of this import block --- lib/matplotlib/axes/_base.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index d46b083a78f0..ce723588ceb8 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -32,7 +32,7 @@ import matplotlib.image as mimage from matplotlib.offsetbox import OffsetBox from matplotlib.artist import allow_rasterization -from matplotlib.cbook import iterable, index_of + from matplotlib.rcsetup import cycler rcParams = matplotlib.rcParams From e48f7e2f1c4dfc331e3dbcd7e6b68d204590e6e6 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 21 Dec 2015 22:22:18 -0500 Subject: [PATCH 05/10] STY: minor pep8 white space --- 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 771acc38ed16..c1f9efcbe752 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -5985,7 +5985,7 @@ def hist(self, x, bins=10, range=None, normed=False, weights=None, xmax = max(xmax, xi.max()) bin_range = (xmin, xmax) - #hist_kwargs = dict(range=range, normed=bool(normed)) + # hist_kwargs = dict(range=range, normed=bool(normed)) # We will handle the normed kwarg within mpl until we # get to the point of requiring numpy >= 1.5. hist_kwargs = dict(range=bin_range) From 9c37e3ffffb67dd28c768a02672a9a10ceb48eff Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 21 Dec 2015 22:53:02 -0500 Subject: [PATCH 06/10] FIX: non-indexed pd.Series into hist closes #5557 --- lib/matplotlib/axes/_axes.py | 88 ++++++++++++++++++++---------------- 1 file changed, 49 insertions(+), 39 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index c1f9efcbe752..a63c5be8ff50 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -5875,6 +5875,40 @@ def hist(self, x, bins=10, range=None, normed=False, weights=None, .. plot:: mpl_examples/statistics/histogram_demo_features.py """ + def _normalize_input(inp, ename='input'): + """Normalize 1 or 2d input into list of np.ndarray or + a single 2D np.ndarray. + + Parameters + ---------- + inp : iterable + ename : str, optional + Name to use is ValueError if can not be normalized + + """ + if isinstance(x, np.ndarray) or not iterable(next(iter(inp))): + # TODO: support masked arrays; + inp = np.asarray(inp) + if inp.ndim == 2: + # 2-D input with columns as datasets; switch to rows + inp = inp.T + elif inp.ndim == 1: + # new view, single row + inp = inp.reshape(1, inp.shape[0]) + else: + raise ValueError( + "{ename} must be 1D or 2D".format(ename=ename)) + if inp.shape[1] < inp.shape[0]: + warnings.warn( + '2D hist input should be nsamples x nvariables;\n ' + 'this looks transposed ' + '(shape is %d x %d)' % inp.shape[::-1]) + else: + # multiple hist with data of different length + inp = [np.asarray(xi) for xi in inp] + + return inp + if not self._hold: self.cla() @@ -5918,28 +5952,26 @@ def hist(self, x, bins=10, range=None, normed=False, weights=None, input_empty = len(flat) == 0 # Massage 'x' for processing. - # NOTE: Be sure any changes here is also done below to 'weights' if input_empty: x = np.array([[]]) - elif isinstance(x, np.ndarray) or not iterable(x[0]): - # TODO: support masked arrays; - x = np.asarray(x) - if x.ndim == 2: - x = x.T # 2-D input with columns as datasets; switch to rows - elif x.ndim == 1: - x = x.reshape(1, x.shape[0]) # new view, single row - else: - raise ValueError("x must be 1D or 2D") - if x.shape[1] < x.shape[0]: - warnings.warn( - '2D hist input should be nsamples x nvariables;\n ' - 'this looks transposed (shape is %d x %d)' % x.shape[::-1]) else: - # multiple hist with data of different length - x = [np.asarray(xi) for xi in x] - + x = _normalize_input(x, 'x') nx = len(x) # number of datasets + # We need to do to 'weights' what was done to 'x' + if weights is not None: + w = _normalize_input(weights, 'weights') + else: + w = [None]*nx + + if len(w) != nx: + raise ValueError('weights should have the same shape as x') + + for xi, wi in zip(x, w): + if wi is not None and len(wi) != len(xi): + raise ValueError( + 'weights should have the same shape as x') + if color is None and 'color' in self._get_lines._prop_keys: color = [six.next(self._get_lines.prop_cycler)['color'] for i in xrange(nx)] @@ -5948,28 +5980,6 @@ def hist(self, x, bins=10, range=None, normed=False, weights=None, if len(color) != nx: raise ValueError("color kwarg must have one color per dataset") - # We need to do to 'weights' what was done to 'x' - if weights is not None: - if isinstance(weights, np.ndarray) or not iterable(weights[0]): - w = np.array(weights) - if w.ndim == 2: - w = w.T - elif w.ndim == 1: - w.shape = (1, w.shape[0]) - else: - raise ValueError("weights must be 1D or 2D") - else: - w = [np.asarray(wi) for wi in weights] - - if len(w) != nx: - raise ValueError('weights should have the same shape as x') - for i in xrange(nx): - if len(w[i]) != len(x[i]): - raise ValueError( - 'weights should have the same shape as x') - else: - w = [None]*nx - # Save the datalimits for the same reason: _saved_bounds = self.dataLim.bounds From f959cbee82489a51a6c65afefd9e325b5e5591c4 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 21 Dec 2015 23:03:05 -0500 Subject: [PATCH 07/10] TST: test hist with no 0 index pd.Series --- lib/matplotlib/tests/test_axes.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index 8d7e3895bdd9..5b6ad3f1c479 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -4201,6 +4201,19 @@ def test_pandas_indexing_dates(): ax.plot('dates', 'values', data=without_zero_index) +@cleanup +def test_pandas_indexing_hist(): + try: + import pandas as pd + except ImportError: + raise SkipTest("Pandas not installed") + + ser_1 = pd.Series(data=[1, 2, 2, 3, 3, 4, 4, 4, 4, 5]) + ser_2 = ser_1.iloc[1:] + fig, axes = plt.subplots() + axes.hist(ser_2) + + if __name__ == '__main__': import nose import sys From bb7691b6978512ea117726e540c1f548a1cec266 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Mon, 28 Dec 2015 12:02:51 -0500 Subject: [PATCH 08/10] DOC: fix typo / clarify docstring --- 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 a63c5be8ff50..a713d72bcfd8 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -5883,7 +5883,7 @@ def _normalize_input(inp, ename='input'): ---------- inp : iterable ename : str, optional - Name to use is ValueError if can not be normalized + Name to use in ValueError if `inp` can not be normalized """ if isinstance(x, np.ndarray) or not iterable(next(iter(inp))): From 1ba3a5196aa6622086f1e792e24d9929dfa62284 Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 29 Dec 2015 15:25:11 -0500 Subject: [PATCH 09/10] MNT: do not need to use six.next All versions of python we support have built in `next` --- lib/matplotlib/axes/_axes.py | 8 ++++---- lib/matplotlib/backends/backend_pdf.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index a713d72bcfd8..d8d84b0b134d 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2331,11 +2331,11 @@ def broken_barh(self, xranges, yrange, **kwargs): """ # process the unit information if len(xranges): - xdata = six.next(iter(xranges)) + xdata = next(iter(xranges)) else: xdata = None if len(yrange): - ydata = six.next(iter(yrange)) + ydata = next(iter(yrange)) else: ydata = None self._process_unit_info(xdata=xdata, @@ -3016,7 +3016,7 @@ def xywhere(xs, ys, mask): if ecolor is None: if l0 is None and 'color' in self._get_lines._prop_keys: - ecolor = six.next(self._get_lines.prop_cycler)['color'] + ecolor = next(self._get_lines.prop_cycler)['color'] else: ecolor = l0.get_color() @@ -5973,7 +5973,7 @@ def _normalize_input(inp, ename='input'): 'weights should have the same shape as x') if color is None and 'color' in self._get_lines._prop_keys: - color = [six.next(self._get_lines.prop_cycler)['color'] + color = [next(self._get_lines.prop_cycler)['color'] for i in xrange(nx)] else: color = mcolors.colorConverter.to_rgba_array(color) diff --git a/lib/matplotlib/backends/backend_pdf.py b/lib/matplotlib/backends/backend_pdf.py index 7e048b757c51..56975900a0da 100644 --- a/lib/matplotlib/backends/backend_pdf.py +++ b/lib/matplotlib/backends/backend_pdf.py @@ -1846,7 +1846,7 @@ def draw_tex(self, gc, x, y, s, prop, angle, ismath='TeX!', mtext=None): fontsize = prop.get_size_in_points() dvifile = texmanager.make_dvi(s, fontsize) dvi = dviread.Dvi(dvifile, 72) - page = six.next(iter(dvi)) + page = next(iter(dvi)) dvi.close() # Gather font information and do some setup for combining From bdaaf594a64744ffa088c586c7898a1b3bcc99aa Mon Sep 17 00:00:00 2001 From: Thomas A Caswell Date: Tue, 29 Dec 2015 15:33:54 -0500 Subject: [PATCH 10/10] MNT: raise exception if generators passed in - moved next(iter(obj)) logic into a cbook function --- lib/matplotlib/axes/_axes.py | 7 ++++--- lib/matplotlib/cbook.py | 8 ++++++++ lib/matplotlib/dates.py | 2 +- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index d8d84b0b134d..9a4976585cff 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -2331,11 +2331,11 @@ def broken_barh(self, xranges, yrange, **kwargs): """ # process the unit information if len(xranges): - xdata = next(iter(xranges)) + xdata = cbook.safe_first_element(xranges) else: xdata = None if len(yrange): - ydata = next(iter(yrange)) + ydata = cbook.safe_first_element(yrange) else: ydata = None self._process_unit_info(xdata=xdata, @@ -5886,7 +5886,8 @@ def _normalize_input(inp, ename='input'): Name to use in ValueError if `inp` can not be normalized """ - if isinstance(x, np.ndarray) or not iterable(next(iter(inp))): + if (isinstance(x, np.ndarray) or + not iterable(cbook.safe_first_element(inp))): # TODO: support masked arrays; inp = np.asarray(inp) if inp.ndim == 2: diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index da870a7f421e..69741ceff04c 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -12,6 +12,7 @@ from matplotlib.externals import six from matplotlib.externals.six.moves import xrange, zip from itertools import repeat +import collections import datetime import errno @@ -2536,6 +2537,13 @@ def index_of(y): return np.arange(y.shape[0], dtype=float), y +def safe_first_element(obj): + if isinstance(obj, collections.Iterator): + raise RuntimeError("matplotlib does not support generators " + "as input") + return next(iter(obj)) + + def get_label(y, default_name): try: return y.name diff --git a/lib/matplotlib/dates.py b/lib/matplotlib/dates.py index 28655e33486d..79196c8d4293 100755 --- a/lib/matplotlib/dates.py +++ b/lib/matplotlib/dates.py @@ -1561,7 +1561,7 @@ def default_units(x, axis): x = x.ravel() try: - x = next(iter(x)) + x = cbook.safe_first_element(x) except (TypeError, StopIteration): pass