diff --git a/.gitignore b/.gitignore index 341741a83fdd..16e1f0b996aa 100644 --- a/.gitignore +++ b/.gitignore @@ -13,6 +13,7 @@ .project .pydevproject .swp +.idea # Compiled source # ################### diff --git a/.travis.yml b/.travis.yml index 2c9279bd83fb..9dc579d4fb31 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,6 +26,7 @@ env: - secure: "dfjNqGKzQG5bu3FnDNwLG8H/C4QoieFo4PfFmZPdM2RY7WIzukwKFNT6kiDfOrpwt+2bR7FhzjOGlDECGtlGOtYPN8XuXGjhcP4a4IfakdbDfF+D3NPIpf5VlE6776k0VpvcZBTMYJKNFIMc7QPkOwjvNJ2aXyfe3hBuGlKJzQU=" - BUILD_DOCS=false - NUMPY=numpy + - PANDAS= - NPROC=2 - TEST_ARGS=--no-pep8 @@ -36,7 +37,7 @@ matrix: - python: 2.6 env: NUMPY=numpy==1.6 MOCK=mock==1.0.1 - python: 2.7 - env: MOCK=mock + env: MOCK=mock PANDAS=pandas - python: 3.3 - python: 3.4 - python: 2.7 @@ -62,7 +63,7 @@ install: pip install --upgrade setuptools # Install only from travis wheelhouse - if [ -z "$PRE" ]; then - wheelhouse_pip_install python-dateutil $NUMPY pyparsing pillow sphinx!=1.3.0; + wheelhouse_pip_install python-dateutil $NUMPY $PANDAS pyparsing pillow sphinx!=1.3.0; else pip install $PRE python-dateutil $NUMPY pyparsing pillow sphinx!=1.3.0; fi diff --git a/boilerplate.py b/boilerplate.py index b0fce8ac0113..67fb7001146e 100644 --- a/boilerplate.py +++ b/boilerplate.py @@ -8,6 +8,7 @@ itself, whereas the generatable content must be edited via templates in this file. +This file is python 3 only due to the use of `inspect` """ # We did try to do the wrapping the smart way, # with callable functions and new.function, but could never get the @@ -209,7 +210,12 @@ def format_value(value): mappable = '' # Get argspec of wrapped function - args, varargs, varkw, defaults = inspect.getargspec(getattr(Axes, func)) + base_func = getattr(Axes, func) + has_data = 'data' in inspect.signature(base_func).parameters + work_func = inspect.unwrap(base_func) + + args, varargs, varkw, defaults = inspect.getargspec(work_func) + args.pop(0) # remove 'self' argument if defaults is None: defaults = () @@ -222,6 +228,15 @@ def format_value(value): def_edited.append(val) defaults = tuple(def_edited) + # Add a data keyword argument if needed (fmt is PLOT_TEMPLATE) and + # possible (if *args is used, we can't just add a data + # argument in front of it since it would gobble one of the + # arguments the user means to pass via *args) + # This needs to be done here so that it goes into call + if not varargs and fmt is PLOT_TEMPLATE and has_data: + args.append('data') + defaults = defaults + (None,) + # How to call the wrapped function call = [] for i, arg in enumerate(args): @@ -230,6 +245,14 @@ def format_value(value): else: call.append('%s=%s' % (arg, arg)) + # remove the data keyword as it was needed above to go into the + # call but should go after `hold` in the signature. + # This is janky as all get out, but hopefully boilerplate will + # be retired soon. + if not varargs and fmt is PLOT_TEMPLATE and has_data: + args.pop() + defaults = defaults[:-1] + if varargs is not None: call.append('*' + varargs) if varkw is not None: @@ -249,6 +272,9 @@ def format_value(value): elif fmt is PLOT_TEMPLATE: args.append('hold') defaults = defaults + (None,) + if has_data: + args.append('data') + defaults = defaults + (None,) sethold = '' # Now we can build the argspec for defining the wrapper diff --git a/doc/users/whats_new/2015-07-30_unpack_labeled_data.rst b/doc/users/whats_new/2015-07-30_unpack_labeled_data.rst new file mode 100644 index 000000000000..04d5e5985563 --- /dev/null +++ b/doc/users/whats_new/2015-07-30_unpack_labeled_data.rst @@ -0,0 +1,36 @@ +Working with labeled data like pandas DataFrames +------------------------------------------------ +Plot methods which take arrays as inputs can now also work with labeled data +and unpack such data. + +This means that the following two examples produce the same plot: + +Example :: + + df = pandas.DataFrame({"var1":[1,2,3,4,5,6], "var2":[1,2,3,4,5,6]}) + plt.plot(df["var1"], df["var2"]) + + +Example :: + + plt.plot("var1", "var2", data=df) + +This works for most plotting methods, which expect arrays/sequences as +inputs. ``data`` can be anything which supports ``__getitem__`` +(``dict``, ``pandas.DataFrame``, ``h5py``, ...) to access ``array`` like +values with string keys. + +In addition to this, some other changes were made, which makes working with +labeled data (ex ``pandas.Series``) easier: + +* For plotting methods with ``label`` keyword argument, one of the + data inputs is designated as the label source. If the user does not + supply a ``label`` that value object will be introspected for a + label, currently by looking for a ``name`` attribute. If the value + object does not have a ``name`` attribute but was specified by as a + key into the ``data`` kwarg, then the key is used. In the above + examples, this results in an implicit ``label="var2"`` for both + cases. + +* ``plot()`` now uses the index of a ``Series`` instead of + ``np.arange(len(y))``, if no ``x`` argument is supplied. diff --git a/lib/matplotlib/__init__.py b/lib/matplotlib/__init__.py index 5b7675e7cfe8..281c7ec83dfe 100644 --- a/lib/matplotlib/__init__.py +++ b/lib/matplotlib/__init__.py @@ -108,6 +108,7 @@ from itertools import chain import io +import inspect import locale import os import re @@ -115,10 +116,10 @@ import warnings import contextlib import distutils.sysconfig - +import functools # cbook must import matplotlib only within function # definitions, so it is safe to import from it here. -from matplotlib.cbook import is_string_like, mplDeprecation +from matplotlib.cbook import is_string_like, mplDeprecation, dedent, get_label from matplotlib.compat import subprocess from matplotlib.rcsetup import (defaultParams, validate_backend, @@ -1457,6 +1458,7 @@ def tk_window_focus(): 'matplotlib.tests.test_triangulation', 'matplotlib.tests.test_widgets', 'matplotlib.tests.test_cycles', + 'matplotlib.tests.test_labeled_data_unpacking', 'matplotlib.sphinxext.tests.test_tinypages', 'mpl_toolkits.tests.test_mplot3d', 'mpl_toolkits.tests.test_axes_grid1', @@ -1520,6 +1522,297 @@ def test(verbosity=1): test.__test__ = False # nose: this function is not a test + +def _replacer(data, key): + # if key isn't a string don't bother + if not isinstance(key, six.string_types): + return key + # try to use __getitem__ + try: + return data[key] + # key does not exist, silently fall back to key + except KeyError: + return key + + +_DATA_DOC_APPENDIX = """ + +Notes +----- + +In addition to the above described arguments, this function can take a +**data** keyword argument. If such a **data** argument is given, the +following arguments are replaced by **data[]**: + +{replaced} +""" + + +def unpack_labeled_data(replace_names=None, replace_all_args=False, + label_namer=None, positional_parameter_names=None): + """ + A decorator to add a 'data' kwarg to any a function. The signature + of the input function must include the ax argument at the first position :: + + def foo(ax, *args, **kwargs) + + so this is suitable for use with Axes methods. + + Parameters + ---------- + replace_names : list of strings, optional, default: None + The list of parameter names which arguments should be replaced by + `data[name]`. If None, all arguments are replaced if they are + included in `data`. + replace_all_args : bool, default: False + If True, all arguments in *args get replaced, even if they are not + in replace_names. + label_namer : string, optional, default: None + The name of the parameter which argument should be used as label, if + label is not set. If None, the label keyword argument is not set. + positional_parameter_names : list of strings or callable, optional + The full list of positional parameter names (excluding an explicit + `ax`/'self' argument at the first place and including all possible + positional parameter in `*args`), in the right order. Can also include + all other keyword parameter. Only needed if the wrapped function does + contain `*args` and (replace_names is not None or replace_all_args is + False). If it is a callable, it will be called with the actual + tuple of *args and the data and should return a list like + above. + NOTE: callables should only be used when the names and order of *args + can only be determined at runtime. Please use list of names + when the order and names of *args is clear before runtime! + """ + if replace_names is not None: + replace_names = set(replace_names) + + def param(func): + new_sig = None + python_has_signature = major >= 3 and minor1 >= 3 + python_has_wrapped = major >= 3 and minor1 >= 2 + + # if in a legacy version of python and IPython is already imported + # try to use their back-ported signature + if not python_has_signature and 'IPython' in sys.modules: + try: + import IPython.utils.signatures + signature = IPython.utils.signatures.signature + Parameter = IPython.utils.signatures.Parameter + except ImportError: + pass + else: + python_has_signature = True + else: + if python_has_signature: + signature = inspect.signature + Parameter = inspect.Parameter + + if not python_has_signature: + arg_spec = inspect.getargspec(func) + _arg_names = arg_spec.args + _has_varargs = arg_spec.varargs is not None + _has_varkwargs = arg_spec.keywords is not None + else: + sig = signature(func) + _has_varargs = False + _has_varkwargs = False + _arg_names = [] + params = list(sig.parameters.values()) + for p in params: + if p.kind is Parameter.VAR_POSITIONAL: + _has_varargs = True + elif p.kind is Parameter.VAR_KEYWORD: + _has_varkwargs = True + else: + _arg_names.append(p.name) + data_param = Parameter('data', + Parameter.KEYWORD_ONLY, + default=None) + if _has_varkwargs: + params.insert(-1, data_param) + else: + params.append(data_param) + new_sig = sig.replace(parameters=params) + # Import-time check: do we have enough information to replace *args? + arg_names_at_runtime = False + # there can't be any positional arguments behind *args and no + # positional args can end up in **kwargs, so only *varargs make + # problems. + # http://stupidpythonideas.blogspot.de/2013/08/arguments-and-parameters.html + if not _has_varargs: + # all args are "named", so no problem + # remove the first "ax" / self arg + arg_names = _arg_names[1:] + else: + # Here we have "unnamed" variables and we need a way to determine + # whether to replace a arg or not + if replace_names is None: + # all argnames should be replaced + arg_names = None + elif len(replace_names) == 0: + # No argnames should be replaced + arg_names = [] + elif len(_arg_names) > 1 and (positional_parameter_names is None): + # we got no manual parameter names but more than an 'ax' ... + if len(set(replace_names) - set(_arg_names[1:])) == 0: + # all to be replaced arguments are in the list + arg_names = _arg_names[1:] + else: + msg = ("Got unknown 'replace_names' and wrapped function " + "'%s' uses '*args', need " + "'positional_parameter_names'!") + raise AssertionError(msg % func.__name__) + else: + if positional_parameter_names is not None: + if callable(positional_parameter_names): + # determined by the function at runtime + arg_names_at_runtime = True + # so that we don't compute the label_pos at import time + arg_names = [] + else: + arg_names = positional_parameter_names + else: + if replace_all_args: + arg_names = [] + else: + msg = ("Got 'replace_names' and wrapped function " + "'%s' uses *args, need " + "'positional_parameter_names' or " + "'replace_all_args'!") + raise AssertionError(msg % func.__name__) + + # compute the possible label_namer and label position in positional + # arguments + label_pos = 9999 # bigger than all "possible" argument lists + label_namer_pos = 9999 # bigger than all "possible" argument lists + if (label_namer and # we actually want a label here ... + arg_names and # and we can determine a label in *args ... + (label_namer in arg_names)): # and it is in *args + label_namer_pos = arg_names.index(label_namer) + if "label" in arg_names: + label_pos = arg_names.index("label") + + # Check the case we know a label_namer but we can't find it the + # arg_names... Unfortunately the label_namer can be in **kwargs, + # which we can't detect here and which results in a non-set label + # which might surprise the user :-( + if label_namer and not arg_names_at_runtime and not _has_varkwargs: + if not arg_names: + msg = ("label_namer '%s' can't be found as the parameter " + "without 'positional_parameter_names'.") + raise AssertionError(msg % label_namer) + elif label_namer not in arg_names: + msg = ("label_namer '%s' can't be found in the parameter " + "names (known argnames: %s).") + raise AssertionError(msg % (label_namer, arg_names)) + else: + # this is the case when the name is in arg_names + pass + + @functools.wraps(func) + def inner(ax, *args, **kwargs): + # this is needed because we want to change these values if + # arg_names_at_runtime==True, but python does not allow assigning + # to a variable in a outer scope. So use some new local ones and + # set them to the already computed values. + _label_pos = label_pos + _label_namer_pos = label_namer_pos + _arg_names = arg_names + + label = None + + data = kwargs.pop('data', None) + if data is not None: + if arg_names_at_runtime: + # update the information about replace names and + # label position + _arg_names = positional_parameter_names(args, data) + if (label_namer and # we actually want a label here ... + _arg_names and # and we can find a label in *args + (label_namer in _arg_names)): # and it is in *args + _label_namer_pos = _arg_names.index(label_namer) + if "label" in _arg_names: + _label_pos = arg_names.index("label") + + # save the current label_namer value so that it can be used as + # a label + if _label_namer_pos < len(args): + label = args[_label_namer_pos] + else: + label = kwargs.get(label_namer, None) + # ensure a string, as label can't be anything else + if not isinstance(label, six.string_types): + label = None + + if (replace_names is None) or (replace_all_args is True): + # all should be replaced + args = tuple(_replacer(data, a) for + j, a in enumerate(args)) + else: + # An arg is replaced if the arg_name of that position is + # in replace_names ... + if len(_arg_names) < len(args): + raise RuntimeError( + "Got more args than function expects") + args = tuple(_replacer(data, a) + if _arg_names[j] in replace_names else a + for j, a in enumerate(args)) + + if replace_names is None: + # replace all kwargs ... + kwargs = dict((k, _replacer(data, v)) + for k, v in six.iteritems(kwargs)) + else: + # ... or only if a kwarg of that name is in replace_names + kwargs = dict((k, _replacer(data, v) + if k in replace_names else v) + for k, v in six.iteritems(kwargs)) + + # replace the label if this func "wants" a label arg and the user + # didn't set one. Note: if the user puts in "label=None", it does + # *NOT* get replaced! + user_supplied_label = ( + (len(args) >= _label_pos) or # label is included in args + ('label' in kwargs) # ... or in kwargs + ) + if (label_namer and not user_supplied_label): + if _label_namer_pos < len(args): + kwargs['label'] = get_label(args[_label_namer_pos], label) + elif label_namer in kwargs: + kwargs['label'] = get_label(kwargs[label_namer], label) + else: + import warnings + msg = ("Tried to set a label via parameter '%s' in " + "func '%s' but couldn't find such an argument. \n" + "(This is a programming error, please report to " + "the matplotlib list!)") + warnings.warn(msg % (label_namer, func.__name__), + RuntimeWarning, stacklevel=2) + return func(ax, *args, **kwargs) + pre_doc = inner.__doc__ + if pre_doc is None: + pre_doc = '' + else: + pre_doc = dedent(pre_doc) + _repl = "" + if replace_names is None: + _repl = "* All positional and all keyword arguments." + else: + if len(replace_names) != 0: + _repl = "* All arguments with the following names: '{names}'." + if replace_all_args: + _repl += "\n* All positional arguments." + _repl = _repl.format(names="', '".join(replace_names)) + inner.__doc__ = (pre_doc + + _DATA_DOC_APPENDIX.format(replaced=_repl)) + if not python_has_wrapped: + inner.__wrapped__ = func + if new_sig is not None: + inner.__signature__ = new_sig + return inner + return param + + verbose.report('matplotlib version %s' % __version__) verbose.report('verbose.level %s' % verbose.level) verbose.report('interactive is %s' % is_interactive()) diff --git a/lib/matplotlib/axes/_axes.py b/lib/matplotlib/axes/_axes.py index bd5f08584c65..67acaae161a5 100644 --- a/lib/matplotlib/axes/_axes.py +++ b/lib/matplotlib/axes/_axes.py @@ -11,6 +11,7 @@ from numpy import ma import matplotlib +from matplotlib import unpack_labeled_data import matplotlib.cbook as cbook from matplotlib.cbook import mplDeprecation, STEP_LOOKUP_MAP @@ -39,6 +40,7 @@ from matplotlib.axes._base import _AxesBase from matplotlib.axes._base import _process_plot_format + rcParams = matplotlib.rcParams iterable = cbook.iterable @@ -46,9 +48,39 @@ is_sequence_of_strings = cbook.is_sequence_of_strings +def _plot_args_replacer(args, data): + if len(args) == 1: + return ["y"] + elif len(args) == 2: + # this can be two cases: x,y or y,c + if not args[1] in data: + # this is not in data, so just assume that it is something which + # will not get replaced (color spec or array like). + return ["y", "c"] + # it's data, but could be a color code like 'ro' or 'b--' + # -> warn the user in that case... + try: + _process_plot_format(args[1]) + except ValueError: + pass + else: + msg = "Second argument is ambiguous: could be a color spec " \ + "but is in data. Using as data.\nEither rename the " \ + "entry in data or use three arguments to plot." + warnings.warn(msg, RuntimeWarning, stacklevel=3) + return ["x", "y"] + elif len(args) == 3: + return ["x", "y", "c"] + else: + raise ValueError("Using arbitrary long args with data is not " + "supported due to ambiguity of arguments.\nUse " + "multiple plotting calls instead.") + + # The axes module contains all the wrappers to plotting functions. # All the other methods should go in the _AxesBase class. + class Axes(_AxesBase): """ The :class:`Axes` contains most of the figure elements: @@ -901,6 +933,7 @@ def axvspan(self, xmin, xmax, ymin=0, ymax=1, **kwargs): self.autoscale_view(scaley=False) return p + @unpack_labeled_data(replace_names=['y', 'xmin', 'xmax'], label_namer="y") @docstring.dedent def hlines(self, y, xmin, xmax, colors='k', linestyles='solid', label='', **kwargs): @@ -979,6 +1012,8 @@ def hlines(self, y, xmin, xmax, colors='k', linestyles='solid', return coll + @unpack_labeled_data(replace_names=["x", "ymin", "ymax", "colors"], + label_namer="x") @docstring.dedent_interpd def vlines(self, x, ymin, ymax, colors='k', linestyles='solid', label='', **kwargs): @@ -1059,6 +1094,10 @@ def vlines(self, x, ymin, ymax, colors='k', linestyles='solid', return coll + @unpack_labeled_data(replace_names=["positions", "lineoffsets", + "linelengths", "linewidths", + "colors", "linestyles"], + label_namer=None) @docstring.dedent_interpd def eventplot(self, positions, orientation='horizontal', lineoffsets=1, linelengths=1, linewidths=None, colors=None, @@ -1240,7 +1279,11 @@ def eventplot(self, positions, orientation='horizontal', lineoffsets=1, return colls - #### Basic plotting + # ### Basic plotting + # The label_naming happens in `matplotlib.axes._base._plot_args` + @unpack_labeled_data(replace_names=["x", "y"], + positional_parameter_names=_plot_args_replacer, + label_namer=None) @docstring.dedent_interpd def plot(self, *args, **kwargs): """ @@ -1258,8 +1301,14 @@ def plot(self, *args, **kwargs): If *x* and/or *y* is 2-dimensional, then the corresponding columns will be plotted. - An arbitrary number of *x*, *y*, *fmt* groups can be - specified, as in:: + If used with labeled data, make sure that the color spec is not + included as an element in data, as otherwise the last case + ``plot("v","r", data={"v":..., "r":...)`` + can be interpreted as the first case which would do ``plot(v, r)`` + using the default line style and color. + + If not used with labeled data (i.e., without a data argument), + an arbitrary number of *x*, *y*, *fmt* groups can be specified, as in:: a.plot(x1, y1, 'g^', x2, y2, 'g-') @@ -1385,6 +1434,7 @@ def plot(self, *args, **kwargs): self.autoscale_view(scalex=scalex, scaley=scaley) return lines + @unpack_labeled_data(replace_names=["x", "y"], label_namer="y") @docstring.dedent_interpd def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False, **kwargs): @@ -1458,6 +1508,7 @@ def plot_date(self, x, y, fmt='o', tz=None, xdate=True, ydate=False, return ret + # @unpack_labeled_data() # let 'plot' do the unpacking.. @docstring.dedent_interpd def loglog(self, *args, **kwargs): """ @@ -1519,6 +1570,7 @@ def loglog(self, *args, **kwargs): return l + # @unpack_labeled_data() # let 'plot' do the unpacking.. @docstring.dedent_interpd def semilogx(self, *args, **kwargs): """ @@ -1571,6 +1623,7 @@ def semilogx(self, *args, **kwargs): self._hold = b # restore the hold return l + # @unpack_labeled_data() # let 'plot' do the unpacking.. @docstring.dedent_interpd def semilogy(self, *args, **kwargs): """ @@ -1623,6 +1676,7 @@ def semilogy(self, *args, **kwargs): return l + @unpack_labeled_data(replace_names=["x"], label_namer="x") @docstring.dedent_interpd def acorr(self, x, **kwargs): """ @@ -1684,6 +1738,7 @@ def acorr(self, x, **kwargs): """ return self.xcorr(x, x, **kwargs) + @unpack_labeled_data(replace_names=["x", "y"], label_namer="y") @docstring.dedent_interpd def xcorr(self, x, y, normed=True, detrend=mlab.detrend_none, usevlines=True, maxlags=10, **kwargs): @@ -1773,6 +1828,7 @@ def xcorr(self, x, y, normed=True, detrend=mlab.detrend_none, #### Specialized plotting + @unpack_labeled_data(replace_names=["x", "y"], label_namer="y") def step(self, x, y, *args, **kwargs): """ Make a step plot. @@ -1810,6 +1866,11 @@ def step(self, x, y, *args, **kwargs): return self.plot(x, y, *args, **kwargs) + @unpack_labeled_data(replace_names=["left", "height", "width", "bottom", + "color", "edgecolor", "linewidth", + "tick_label", "xerr", "yerr", + "ecolor"], + label_namer=None) @docstring.dedent_interpd def bar(self, left, height, width=0.8, bottom=None, **kwargs): """ @@ -2236,6 +2297,7 @@ def barh(self, bottom, width, height=0.8, left=None, **kwargs): bottom=bottom, orientation='horizontal', **kwargs) return patches + @unpack_labeled_data(label_namer=None) @docstring.dedent_interpd def broken_barh(self, xranges, yrange, **kwargs): """ @@ -2288,6 +2350,7 @@ def broken_barh(self, xranges, yrange, **kwargs): return col + @unpack_labeled_data(replace_all_args=True, label_namer=None) def stem(self, *args, **kwargs): """ Create a stem plot. @@ -2375,6 +2438,8 @@ def stem(self, *args, **kwargs): return stem_container + @unpack_labeled_data(replace_names=['x', 'explode', 'labels', 'colors'], + label_namer=None) def pie(self, x, explode=None, labels=None, colors=None, autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1, startangle=None, radius=None, counterclock=True, @@ -2594,6 +2659,8 @@ def pie(self, x, explode=None, labels=None, colors=None, else: return slices, texts, autotexts + @unpack_labeled_data(replace_names=["x", "y", "xerr", "yerr"], + label_namer="y") @docstring.dedent_interpd def errorbar(self, x, y, yerr=None, xerr=None, fmt='', ecolor=None, elinewidth=None, capsize=None, @@ -2971,6 +3038,7 @@ def xywhere(xs, ys, mask): return errorbar_container # (l0, caplines, barcols) + @unpack_labeled_data(label_namer=None) def boxplot(self, x, notch=None, sym=None, vert=None, whis=None, positions=None, widths=None, patch_artist=None, bootstrap=None, usermedians=None, conf_intervals=None, @@ -3640,6 +3708,10 @@ def dopatch(xs, ys, **kwargs): return dict(whiskers=whiskers, caps=caps, boxes=boxes, medians=medians, fliers=fliers, means=means) + @unpack_labeled_data(replace_names=["x", "y", "s", "linewidths", + "edgecolors", "c", 'facecolor', + 'facecolors', 'color'], + label_namer="y") @docstring.dedent_interpd def scatter(self, x, y, s=20, c=None, marker='o', cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, @@ -3847,6 +3919,7 @@ def scatter(self, x, y, s=20, c=None, marker='o', cmap=None, norm=None, return collection + @unpack_labeled_data(replace_names=["x", "y"], label_namer="y") @docstring.dedent_interpd def hexbin(self, x, y, C=None, gridsize=100, bins=None, xscale='linear', yscale='linear', extent=None, @@ -4347,6 +4420,8 @@ def quiverkey(self, *args, **kw): return qk quiverkey.__doc__ = mquiver.QuiverKey.quiverkey_doc + # args can by a combination if X, Y, U, V, C and all should be replaced + @unpack_labeled_data(replace_all_args=True, label_namer=None) def quiver(self, *args, **kw): if not self._hold: self.cla() @@ -4357,10 +4432,14 @@ def quiver(self, *args, **kw): return q quiver.__doc__ = mquiver.Quiver.quiver_doc + # args can by either Y or y1,y2,... and all should be replaced + @unpack_labeled_data(replace_all_args=True, label_namer=None) def stackplot(self, x, *args, **kwargs): return mstack.stackplot(self, x, *args, **kwargs) stackplot.__doc__ = mstack.stackplot.__doc__ + @unpack_labeled_data(replace_names=["x", "y", "u", "v", "start_points"], + label_namer=None) def streamplot(self, x, y, u, v, density=1, linewidth=None, color=None, cmap=None, norm=None, arrowsize=1, arrowstyle='-|>', minlength=0.1, transform=None, zorder=1, start_points=None): @@ -4381,6 +4460,8 @@ def streamplot(self, x, y, u, v, density=1, linewidth=None, color=None, return stream_container streamplot.__doc__ = mstream.streamplot.__doc__ + # args can be some combination of X, Y, U, V, C and all should be replaced + @unpack_labeled_data(replace_all_args=True, label_namer=None) @docstring.dedent_interpd def barbs(self, *args, **kw): """ @@ -4397,6 +4478,8 @@ def barbs(self, *args, **kw): self.autoscale_view() return b + @unpack_labeled_data(replace_names=["x", "y"], label_namer=None, + positional_parameter_names=["x", "y", "c"]) @docstring.dedent_interpd def fill(self, *args, **kwargs): """ @@ -4448,6 +4531,8 @@ def fill(self, *args, **kwargs): self.autoscale_view() return patches + @unpack_labeled_data(replace_names=["x", "y1", "y2", "where"], + label_namer=None) @docstring.dedent_interpd def fill_between(self, x, y1, y2=0, where=None, interpolate=False, step=None, @@ -4601,6 +4686,8 @@ def get_interp_point(ind): self.autoscale_view() return collection + @unpack_labeled_data(replace_names=["y", "x1", "x2", "where"], + label_namer=None) @docstring.dedent_interpd def fill_betweenx(self, y, x1, x2=0, where=None, step=None, **kwargs): @@ -4725,7 +4812,7 @@ def fill_betweenx(self, y, x1, x2=0, where=None, return collection #### plotting z(x,y): imshow, pcolor and relatives, contour - + @unpack_labeled_data(label_namer=None) @docstring.dedent_interpd def imshow(self, X, cmap=None, norm=None, aspect=None, interpolation=None, alpha=None, vmin=None, vmax=None, @@ -4930,6 +5017,7 @@ def _pcolorargs(funcname, *args, **kw): C = C[:Ny - 1, :Nx - 1] return X, Y, C + @unpack_labeled_data(label_namer=None) @docstring.dedent_interpd def pcolor(self, *args, **kwargs): """ @@ -5206,6 +5294,7 @@ def pcolor(self, *args, **kwargs): self.add_collection(collection, autolim=False) return collection + @unpack_labeled_data(label_namer=None) @docstring.dedent_interpd def pcolormesh(self, *args, **kwargs): """ @@ -5354,6 +5443,7 @@ def pcolormesh(self, *args, **kwargs): self.add_collection(collection, autolim=False) return collection + @unpack_labeled_data(label_namer=None) @docstring.dedent_interpd def pcolorfast(self, *args, **kwargs): """ @@ -5541,6 +5631,7 @@ def pcolorfast(self, *args, **kwargs): self.autoscale_view(tight=True) return ret + @unpack_labeled_data() def contour(self, *args, **kwargs): if not self._hold: self.cla() @@ -5548,6 +5639,7 @@ def contour(self, *args, **kwargs): return mcontour.QuadContourSet(self, *args, **kwargs) contour.__doc__ = mcontour.QuadContourSet.contour_doc + @unpack_labeled_data() def contourf(self, *args, **kwargs): if not self._hold: self.cla() @@ -5588,6 +5680,7 @@ def table(self, **kwargs): #### Data analysis + @unpack_labeled_data(replace_names=["x", 'weights'], label_namer="x") @docstring.dedent_interpd def hist(self, x, bins=10, range=None, normed=False, weights=None, cumulative=False, bottom=None, histtype='bar', align='mid', @@ -6134,6 +6227,7 @@ def hist(self, x, bins=10, range=None, normed=False, weights=None, else: return n, bins, cbook.silent_list('Lists of Patches', patches) + @unpack_labeled_data(replace_names=["x", "y", "weights"], label_namer=None) @docstring.dedent_interpd def hist2d(self, x, y, bins=10, range=None, normed=False, weights=None, cmin=None, cmax=None, **kwargs): @@ -6227,6 +6321,7 @@ def hist2d(self, x, y, bins=10, range=None, normed=False, weights=None, return h, xedges, yedges, pc + @unpack_labeled_data(replace_names=["x"], label_namer=None) @docstring.dedent_interpd def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, @@ -6351,6 +6446,7 @@ def psd(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, else: return pxx, freqs, line + @unpack_labeled_data(replace_names=["x", "y"], label_namer="y") @docstring.dedent_interpd def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, @@ -6462,6 +6558,7 @@ def csd(self, x, y, NFFT=None, Fs=None, Fc=None, detrend=None, else: return pxy, freqs, line + @unpack_labeled_data(replace_names=["x"], label_namer=None) @docstring.dedent_interpd def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, scale=None, @@ -6561,6 +6658,7 @@ def magnitude_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] + @unpack_labeled_data(replace_names=["x"], label_namer=None) @docstring.dedent_interpd def angle_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, **kwargs): @@ -6638,6 +6736,7 @@ def angle_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] + @unpack_labeled_data(replace_names=["x"], label_namer=None) @docstring.dedent_interpd def phase_spectrum(self, x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, **kwargs): @@ -6715,6 +6814,7 @@ def phase_spectrum(self, x, Fs=None, Fc=None, window=None, return spec, freqs, lines[0] + @unpack_labeled_data(replace_names=["x", "y"], label_namer=None) @docstring.dedent_interpd def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, window=mlab.window_hanning, noverlap=0, pad_to=None, @@ -6782,6 +6882,7 @@ def cohere(self, x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, return cxy, freqs + @unpack_labeled_data(replace_names=["x"], label_namer=None) @docstring.dedent_interpd def specgram(self, x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, @@ -7099,6 +7200,7 @@ def matshow(self, Z, **kwargs): integer=True)) return im + @unpack_labeled_data(replace_names=["dataset"], label_namer=None) def violinplot(self, dataset, positions=None, vert=True, widths=0.5, showmeans=False, showextrema=True, showmedians=False, points=100, bw_method=None): diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 77620aec1117..c1ff5ddc0427 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -15,7 +15,7 @@ import matplotlib from matplotlib import cbook -from matplotlib.cbook import _string_to_bool +from matplotlib.cbook import _string_to_bool, iterable, index_of, get_label from matplotlib import docstring import matplotlib.colors as mcolors import matplotlib.lines as mlines @@ -31,7 +31,7 @@ import matplotlib.image as mimage from matplotlib.offsetbox import OffsetBox from matplotlib.artist import allow_rasterization -from matplotlib.cbook import iterable +from matplotlib.cbook import iterable, index_of from matplotlib.rcsetup import cycler rcParams = matplotlib.rcParams @@ -349,12 +349,14 @@ def _plot_args(self, tup, kwargs): if v is not None: kw[k] = v - y = np.atleast_1d(tup[-1]) + if 'label' not in kwargs or kwargs['label'] is None: + kwargs['label'] = get_label(tup[-1], None) if len(tup) == 2: x = np.atleast_1d(tup[0]) + y = np.atleast_1d(tup[-1]) else: - x = np.arange(y.shape[0], dtype=float) + x, y = index_of(tup[-1]) x, y = self._xy_from_xy(x, y) diff --git a/lib/matplotlib/cbook.py b/lib/matplotlib/cbook.py index 00070bc5fac0..506e34a4fcd2 100644 --- a/lib/matplotlib/cbook.py +++ b/lib/matplotlib/cbook.py @@ -2487,6 +2487,41 @@ def pts_to_midstep(x, *args): 'step-post': pts_to_poststep, 'step-mid': pts_to_midstep} + +def index_of(y): + """ + A helper function to get the index of an input to plot + against if x values are not explicitly given. + + Tries to get `y.index` (works if this is a pd.Series), if that + fails, return np.arange(y.shape[0]). + + This will be extended in the future to deal with more types of + labeled data. + + Parameters + ---------- + y : scalar or array-like + The proposed y-value + + Returns + ------- + x, y : ndarray + The x and y values to plot. + """ + try: + return y.index.values, y.values + except AttributeError: + y = np.atleast_1d(y) + return np.arange(y.shape[0], dtype=float), y + + +def get_label(y, default_name): + try: + return y.name + except AttributeError: + return default_name + # Numpy > 1.6.x deprecates putmask in favor of the new copyto. # So long as we support versions 1.6.x and less, we need the # following local version of putmask. We choose to make a diff --git a/lib/matplotlib/pyplot.py b/lib/matplotlib/pyplot.py index 39d4c3858c61..a84e1cd90875 100644 --- a/lib/matplotlib/pyplot.py +++ b/lib/matplotlib/pyplot.py @@ -2508,7 +2508,7 @@ def spy(Z, precision=0, marker=None, markersize=None, aspect='equal', hold=None, # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.acorr) -def acorr(x, hold=None, **kwargs): +def acorr(x, hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2516,7 +2516,7 @@ def acorr(x, hold=None, **kwargs): if hold is not None: ax.hold(hold) try: - ret = ax.acorr(x, **kwargs) + ret = ax.acorr(x, data=data, **kwargs) finally: ax.hold(washold) @@ -2526,7 +2526,7 @@ def acorr(x, hold=None, **kwargs): # changes will be lost @_autogen_docstring(Axes.angle_spectrum) def angle_spectrum(x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, - hold=None, **kwargs): + hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2535,7 +2535,7 @@ def angle_spectrum(x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, ax.hold(hold) try: ret = ax.angle_spectrum(x, Fs=Fs, Fc=Fc, window=window, pad_to=pad_to, - sides=sides, **kwargs) + sides=sides, data=data, **kwargs) finally: ax.hold(washold) @@ -2629,7 +2629,7 @@ def axvspan(xmin, xmax, ymin=0, ymax=1, hold=None, **kwargs): # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.bar) -def bar(left, height, width=0.8, bottom=None, hold=None, **kwargs): +def bar(left, height, width=0.8, bottom=None, hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2637,7 +2637,8 @@ def bar(left, height, width=0.8, bottom=None, hold=None, **kwargs): if hold is not None: ax.hold(hold) try: - ret = ax.bar(left, height, width=width, bottom=bottom, **kwargs) + ret = ax.bar(left, height, width=width, bottom=bottom, data=data, + **kwargs) finally: ax.hold(washold) @@ -2663,7 +2664,7 @@ def barh(bottom, width, height=0.8, left=None, hold=None, **kwargs): # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.broken_barh) -def broken_barh(xranges, yrange, hold=None, **kwargs): +def broken_barh(xranges, yrange, hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2671,7 +2672,7 @@ def broken_barh(xranges, yrange, hold=None, **kwargs): if hold is not None: ax.hold(hold) try: - ret = ax.broken_barh(xranges, yrange, **kwargs) + ret = ax.broken_barh(xranges, yrange, data=data, **kwargs) finally: ax.hold(washold) @@ -2685,7 +2686,7 @@ def boxplot(x, notch=None, sym=None, vert=None, whis=None, positions=None, conf_intervals=None, meanline=None, showmeans=None, showcaps=None, showbox=None, showfliers=None, boxprops=None, labels=None, flierprops=None, medianprops=None, meanprops=None, capprops=None, - whiskerprops=None, manage_xticks=True, hold=None): + whiskerprops=None, manage_xticks=True, hold=None, data=None): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2703,7 +2704,8 @@ def boxplot(x, notch=None, sym=None, vert=None, whis=None, positions=None, boxprops=boxprops, labels=labels, flierprops=flierprops, medianprops=medianprops, meanprops=meanprops, capprops=capprops, - whiskerprops=whiskerprops, manage_xticks=manage_xticks) + whiskerprops=whiskerprops, + manage_xticks=manage_xticks, data=data) finally: ax.hold(washold) @@ -2714,7 +2716,7 @@ def boxplot(x, notch=None, sym=None, vert=None, whis=None, positions=None, @_autogen_docstring(Axes.cohere) def cohere(x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, window=mlab.window_hanning, noverlap=0, pad_to=None, sides='default', - scale_by_freq=None, hold=None, **kwargs): + scale_by_freq=None, hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2724,7 +2726,8 @@ def cohere(x, y, NFFT=256, Fs=2, Fc=0, detrend=mlab.detrend_none, try: ret = ax.cohere(x, y, NFFT=NFFT, Fs=Fs, Fc=Fc, detrend=detrend, window=window, noverlap=noverlap, pad_to=pad_to, - sides=sides, scale_by_freq=scale_by_freq, **kwargs) + sides=sides, scale_by_freq=scale_by_freq, data=data, + **kwargs) finally: ax.hold(washold) @@ -2786,7 +2789,7 @@ def contourf(*args, **kwargs): @_autogen_docstring(Axes.csd) def csd(x, y, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, - return_line=None, hold=None, **kwargs): + return_line=None, hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2797,7 +2800,7 @@ def csd(x, y, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, ret = ax.csd(x, y, NFFT=NFFT, Fs=Fs, Fc=Fc, detrend=detrend, window=window, noverlap=noverlap, pad_to=pad_to, sides=sides, scale_by_freq=scale_by_freq, - return_line=return_line, **kwargs) + return_line=return_line, data=data, **kwargs) finally: ax.hold(washold) @@ -2809,7 +2812,7 @@ def csd(x, y, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, def errorbar(x, y, yerr=None, xerr=None, fmt='', ecolor=None, elinewidth=None, capsize=None, barsabove=False, lolims=False, uplims=False, xlolims=False, xuplims=False, errorevery=1, capthick=None, - hold=None, **kwargs): + hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2821,7 +2824,8 @@ def errorbar(x, y, yerr=None, xerr=None, fmt='', ecolor=None, elinewidth=None, elinewidth=elinewidth, capsize=capsize, barsabove=barsabove, lolims=lolims, uplims=uplims, xlolims=xlolims, xuplims=xuplims, - errorevery=errorevery, capthick=capthick, **kwargs) + errorevery=errorevery, capthick=capthick, data=data, + **kwargs) finally: ax.hold(washold) @@ -2832,7 +2836,7 @@ def errorbar(x, y, yerr=None, xerr=None, fmt='', ecolor=None, elinewidth=None, @_autogen_docstring(Axes.eventplot) def eventplot(positions, orientation='horizontal', lineoffsets=1, linelengths=1, linewidths=None, colors=None, linestyles='solid', hold=None, - **kwargs): + data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2843,7 +2847,7 @@ def eventplot(positions, orientation='horizontal', lineoffsets=1, linelengths=1, ret = ax.eventplot(positions, orientation=orientation, lineoffsets=lineoffsets, linelengths=linelengths, linewidths=linewidths, colors=colors, - linestyles=linestyles, **kwargs) + linestyles=linestyles, data=data, **kwargs) finally: ax.hold(washold) @@ -2869,7 +2873,8 @@ def fill(*args, **kwargs): # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.fill_between) -def fill_between(x, y1, y2=0, where=None, interpolate=False, hold=None, **kwargs): +def fill_between(x, y1, y2=0, where=None, interpolate=False, step=None, + hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2878,7 +2883,8 @@ def fill_between(x, y1, y2=0, where=None, interpolate=False, hold=None, **kwargs ax.hold(hold) try: ret = ax.fill_between(x, y1, y2=y2, where=where, - interpolate=interpolate, **kwargs) + interpolate=interpolate, step=step, data=data, + **kwargs) finally: ax.hold(washold) @@ -2887,7 +2893,8 @@ def fill_between(x, y1, y2=0, where=None, interpolate=False, hold=None, **kwargs # This function was autogenerated by boilerplate.py. Do not edit as # changes will be lost @_autogen_docstring(Axes.fill_betweenx) -def fill_betweenx(y, x1, x2=0, where=None, hold=None, **kwargs): +def fill_betweenx(y, x1, x2=0, where=None, step=None, hold=None, data=None, + **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2895,7 +2902,8 @@ def fill_betweenx(y, x1, x2=0, where=None, hold=None, **kwargs): if hold is not None: ax.hold(hold) try: - ret = ax.fill_betweenx(y, x1, x2=x2, where=where, **kwargs) + ret = ax.fill_betweenx(y, x1, x2=x2, where=where, step=step, data=data, + **kwargs) finally: ax.hold(washold) @@ -2908,7 +2916,7 @@ def hexbin(x, y, C=None, gridsize=100, bins=None, xscale='linear', yscale='linear', extent=None, cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, edgecolors='none', reduce_C_function=np.mean, mincnt=None, marginals=False, hold=None, - **kwargs): + data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2921,7 +2929,7 @@ def hexbin(x, y, C=None, gridsize=100, bins=None, xscale='linear', vmin=vmin, vmax=vmax, alpha=alpha, linewidths=linewidths, edgecolors=edgecolors, reduce_C_function=reduce_C_function, mincnt=mincnt, - marginals=marginals, **kwargs) + marginals=marginals, data=data, **kwargs) finally: ax.hold(washold) sci(ret) @@ -2933,7 +2941,7 @@ def hexbin(x, y, C=None, gridsize=100, bins=None, xscale='linear', def hist(x, bins=10, range=None, normed=False, weights=None, cumulative=False, bottom=None, histtype='bar', align='mid', orientation='vertical', rwidth=None, log=False, color=None, label=None, stacked=False, - hold=None, **kwargs): + hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2945,7 +2953,7 @@ def hist(x, bins=10, range=None, normed=False, weights=None, cumulative=False, weights=weights, cumulative=cumulative, bottom=bottom, histtype=histtype, align=align, orientation=orientation, rwidth=rwidth, log=log, color=color, label=label, - stacked=stacked, **kwargs) + stacked=stacked, data=data, **kwargs) finally: ax.hold(washold) @@ -2955,7 +2963,7 @@ def hist(x, bins=10, range=None, normed=False, weights=None, cumulative=False, # changes will be lost @_autogen_docstring(Axes.hist2d) def hist2d(x, y, bins=10, range=None, normed=False, weights=None, cmin=None, - cmax=None, hold=None, **kwargs): + cmax=None, hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2964,7 +2972,8 @@ def hist2d(x, y, bins=10, range=None, normed=False, weights=None, cmin=None, ax.hold(hold) try: ret = ax.hist2d(x, y, bins=bins, range=range, normed=normed, - weights=weights, cmin=cmin, cmax=cmax, **kwargs) + weights=weights, cmin=cmin, cmax=cmax, data=data, + **kwargs) finally: ax.hold(washold) sci(ret[-1]) @@ -2974,7 +2983,7 @@ def hist2d(x, y, bins=10, range=None, normed=False, weights=None, cmin=None, # changes will be lost @_autogen_docstring(Axes.hlines) def hlines(y, xmin, xmax, colors='k', linestyles='solid', label='', hold=None, - **kwargs): + data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -2983,7 +2992,7 @@ def hlines(y, xmin, xmax, colors='k', linestyles='solid', label='', hold=None, ax.hold(hold) try: ret = ax.hlines(y, xmin, xmax, colors=colors, linestyles=linestyles, - label=label, **kwargs) + label=label, data=data, **kwargs) finally: ax.hold(washold) @@ -2995,7 +3004,7 @@ def hlines(y, xmin, xmax, colors='k', linestyles='solid', label='', hold=None, def imshow(X, cmap=None, norm=None, aspect=None, interpolation=None, alpha=None, vmin=None, vmax=None, origin=None, extent=None, shape=None, filternorm=1, filterrad=4.0, imlim=None, resample=None, url=None, - hold=None, **kwargs): + hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3007,7 +3016,8 @@ def imshow(X, cmap=None, norm=None, aspect=None, interpolation=None, alpha=None, interpolation=interpolation, alpha=alpha, vmin=vmin, vmax=vmax, origin=origin, extent=extent, shape=shape, filternorm=filternorm, filterrad=filterrad, - imlim=imlim, resample=resample, url=url, **kwargs) + imlim=imlim, resample=resample, url=url, data=data, + **kwargs) finally: ax.hold(washold) sci(ret) @@ -3034,7 +3044,7 @@ def loglog(*args, **kwargs): # changes will be lost @_autogen_docstring(Axes.magnitude_spectrum) def magnitude_spectrum(x, Fs=None, Fc=None, window=None, pad_to=None, - sides=None, scale=None, hold=None, **kwargs): + sides=None, scale=None, hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3044,7 +3054,7 @@ def magnitude_spectrum(x, Fs=None, Fc=None, window=None, pad_to=None, try: ret = ax.magnitude_spectrum(x, Fs=Fs, Fc=Fc, window=window, pad_to=pad_to, sides=sides, scale=scale, - **kwargs) + data=data, **kwargs) finally: ax.hold(washold) @@ -3088,7 +3098,7 @@ def pcolormesh(*args, **kwargs): # changes will be lost @_autogen_docstring(Axes.phase_spectrum) def phase_spectrum(x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, - hold=None, **kwargs): + hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3097,7 +3107,7 @@ def phase_spectrum(x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, ax.hold(hold) try: ret = ax.phase_spectrum(x, Fs=Fs, Fc=Fc, window=window, pad_to=pad_to, - sides=sides, **kwargs) + sides=sides, data=data, **kwargs) finally: ax.hold(washold) @@ -3109,7 +3119,7 @@ def phase_spectrum(x, Fs=None, Fc=None, window=None, pad_to=None, sides=None, def pie(x, explode=None, labels=None, colors=None, autopct=None, pctdistance=0.6, shadow=False, labeldistance=1.1, startangle=None, radius=None, counterclock=True, wedgeprops=None, textprops=None, - center=(0, 0), frame=False, hold=None): + center=(0, 0), frame=False, hold=None, data=None): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3122,7 +3132,7 @@ def pie(x, explode=None, labels=None, colors=None, autopct=None, labeldistance=labeldistance, startangle=startangle, radius=radius, counterclock=counterclock, wedgeprops=wedgeprops, textprops=textprops, center=center, - frame=frame) + frame=frame, data=data) finally: ax.hold(washold) @@ -3149,7 +3159,7 @@ def plot(*args, **kwargs): # changes will be lost @_autogen_docstring(Axes.plot_date) def plot_date(x, y, fmt='o', tz=None, xdate=True, ydate=False, hold=None, - **kwargs): + data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3158,7 +3168,7 @@ def plot_date(x, y, fmt='o', tz=None, xdate=True, ydate=False, hold=None, ax.hold(hold) try: ret = ax.plot_date(x, y, fmt=fmt, tz=tz, xdate=xdate, ydate=ydate, - **kwargs) + data=data, **kwargs) finally: ax.hold(washold) @@ -3169,7 +3179,7 @@ def plot_date(x, y, fmt='o', tz=None, xdate=True, ydate=False, hold=None, @_autogen_docstring(Axes.psd) def psd(x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, pad_to=None, sides=None, scale_by_freq=None, - return_line=None, hold=None, **kwargs): + return_line=None, hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3180,7 +3190,7 @@ def psd(x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, ret = ax.psd(x, NFFT=NFFT, Fs=Fs, Fc=Fc, detrend=detrend, window=window, noverlap=noverlap, pad_to=pad_to, sides=sides, scale_by_freq=scale_by_freq, - return_line=return_line, **kwargs) + return_line=return_line, data=data, **kwargs) finally: ax.hold(washold) @@ -3225,7 +3235,7 @@ def quiverkey(*args, **kw): @_autogen_docstring(Axes.scatter) def scatter(x, y, s=20, c=None, marker='o', cmap=None, norm=None, vmin=None, vmax=None, alpha=None, linewidths=None, verts=None, edgecolors=None, - hold=None, **kwargs): + hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3236,7 +3246,7 @@ def scatter(x, y, s=20, c=None, marker='o', cmap=None, norm=None, vmin=None, ret = ax.scatter(x, y, s=s, c=c, marker=marker, cmap=cmap, norm=norm, vmin=vmin, vmax=vmax, alpha=alpha, linewidths=linewidths, verts=verts, - edgecolors=edgecolors, **kwargs) + edgecolors=edgecolors, data=data, **kwargs) finally: ax.hold(washold) sci(ret) @@ -3282,7 +3292,7 @@ def semilogy(*args, **kwargs): def specgram(x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, noverlap=None, cmap=None, xextent=None, pad_to=None, sides=None, scale_by_freq=None, mode=None, scale=None, vmin=None, vmax=None, - hold=None, **kwargs): + hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3294,7 +3304,7 @@ def specgram(x, NFFT=None, Fs=None, Fc=None, detrend=None, window=None, window=window, noverlap=noverlap, cmap=cmap, xextent=xextent, pad_to=pad_to, sides=sides, scale_by_freq=scale_by_freq, mode=mode, scale=scale, - vmin=vmin, vmax=vmax, **kwargs) + vmin=vmin, vmax=vmax, data=data, **kwargs) finally: ax.hold(washold) sci(ret[-1]) @@ -3356,7 +3366,7 @@ def step(x, y, *args, **kwargs): @_autogen_docstring(Axes.streamplot) def streamplot(x, y, u, v, density=1, linewidth=None, color=None, cmap=None, norm=None, arrowsize=1, arrowstyle='-|>', minlength=0.1, - transform=None, zorder=1, start_points=None, hold=None): + transform=None, zorder=1, start_points=None, hold=None, data=None): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3368,7 +3378,7 @@ def streamplot(x, y, u, v, density=1, linewidth=None, color=None, cmap=None, color=color, cmap=cmap, norm=norm, arrowsize=arrowsize, arrowstyle=arrowstyle, minlength=minlength, transform=transform, - zorder=zorder, start_points=start_points) + zorder=zorder, start_points=start_points, data=data) finally: ax.hold(washold) sci(ret.lines) @@ -3447,7 +3457,7 @@ def triplot(*args, **kwargs): @_autogen_docstring(Axes.violinplot) def violinplot(dataset, positions=None, vert=True, widths=0.5, showmeans=False, showextrema=True, showmedians=False, points=100, bw_method=None, - hold=None): + hold=None, data=None): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3458,7 +3468,7 @@ def violinplot(dataset, positions=None, vert=True, widths=0.5, showmeans=False, ret = ax.violinplot(dataset, positions=positions, vert=vert, widths=widths, showmeans=showmeans, showextrema=showextrema, showmedians=showmedians, - points=points, bw_method=bw_method) + points=points, bw_method=bw_method, data=data) finally: ax.hold(washold) @@ -3468,7 +3478,7 @@ def violinplot(dataset, positions=None, vert=True, widths=0.5, showmeans=False, # changes will be lost @_autogen_docstring(Axes.vlines) def vlines(x, ymin, ymax, colors='k', linestyles='solid', label='', hold=None, - **kwargs): + data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3477,7 +3487,7 @@ def vlines(x, ymin, ymax, colors='k', linestyles='solid', label='', hold=None, ax.hold(hold) try: ret = ax.vlines(x, ymin, ymax, colors=colors, linestyles=linestyles, - label=label, **kwargs) + label=label, data=data, **kwargs) finally: ax.hold(washold) @@ -3487,7 +3497,7 @@ def vlines(x, ymin, ymax, colors='k', linestyles='solid', label='', hold=None, # changes will be lost @_autogen_docstring(Axes.xcorr) def xcorr(x, y, normed=True, detrend=mlab.detrend_none, usevlines=True, - maxlags=10, hold=None, **kwargs): + maxlags=10, hold=None, data=None, **kwargs): ax = gca() # allow callers to override the hold state by passing hold=True|False washold = ax.ishold() @@ -3496,7 +3506,8 @@ def xcorr(x, y, normed=True, detrend=mlab.detrend_none, usevlines=True, ax.hold(hold) try: ret = ax.xcorr(x, y, normed=normed, detrend=detrend, - usevlines=usevlines, maxlags=maxlags, **kwargs) + usevlines=usevlines, maxlags=maxlags, data=data, + **kwargs) finally: ax.hold(washold) diff --git a/lib/matplotlib/rcsetup.py b/lib/matplotlib/rcsetup.py index 2699311323ec..ccae09cf7773 100644 --- a/lib/matplotlib/rcsetup.py +++ b/lib/matplotlib/rcsetup.py @@ -1003,7 +1003,7 @@ def validate_cycler(s): 'xtick.minor.pad': [4, validate_float], # distance to label in points 'xtick.color': ['k', validate_color], # color of the xtick labels 'xtick.minor.visible': [False, validate_bool], # visiablility of the x axis minor ticks - + # fontsize of the xtick labels 'xtick.labelsize': ['medium', validate_fontsize], 'xtick.direction': ['in', six.text_type], # direction of xticks diff --git a/lib/matplotlib/testing/__init__.py b/lib/matplotlib/testing/__init__.py index 800d82e7ee00..6addb0e53f76 100644 --- a/lib/matplotlib/testing/__init__.py +++ b/lib/matplotlib/testing/__init__.py @@ -1,2 +1,72 @@ from __future__ import (absolute_import, division, print_function, unicode_literals) + +import warnings +from contextlib import contextmanager + +from matplotlib.cbook import is_string_like, iterable + + +def _is_list_like(obj): + """Returns whether the obj is iterable and not a string""" + return not is_string_like(obj) and iterable(obj) + + +# stolen from pandas +@contextmanager +def assert_produces_warning(expected_warning=Warning, filter_level="always", + clear=None): + """ + Context manager for running code that expects to raise (or not raise) + warnings. Checks that code raises the expected warning and only the + expected warning. Pass ``False`` or ``None`` to check that it does *not* + raise a warning. Defaults to ``exception.Warning``, baseclass of all + Warnings. (basically a wrapper around ``warnings.catch_warnings``). + + >>> import warnings + >>> with assert_produces_warning(): + ... warnings.warn(UserWarning()) + ... + >>> with assert_produces_warning(False): + ... warnings.warn(RuntimeWarning()) + ... + Traceback (most recent call last): + ... + AssertionError: Caused unexpected warning(s): ['RuntimeWarning']. + >>> with assert_produces_warning(UserWarning): + ... warnings.warn(RuntimeWarning()) + Traceback (most recent call last): + ... + AssertionError: Did not see expected warning of class 'UserWarning'. + + ..warn:: This is *not* thread-safe. + """ + with warnings.catch_warnings(record=True) as w: + + if clear is not None: + # make sure that we are clearning these warnings + # if they have happened before + # to guarantee that we will catch them + if not _is_list_like(clear): + clear = [clear] + for m in clear: + try: + m.__warningregistry__.clear() + except: + pass + + saw_warning = False + warnings.simplefilter(filter_level) + yield w + extra_warnings = [] + for actual_warning in w: + if (expected_warning and issubclass(actual_warning.category, + expected_warning)): + saw_warning = True + else: + extra_warnings.append(actual_warning.category.__name__) + if expected_warning: + assert saw_warning, ("Did not see expected warning of class %r." + % expected_warning.__name__) + assert not extra_warnings, ("Caused unexpected warning(s): %r." + % extra_warnings) diff --git a/lib/matplotlib/tests/test_axes.py b/lib/matplotlib/tests/test_axes.py index e5c222183915..e33d751984b0 100644 --- a/lib/matplotlib/tests/test_axes.py +++ b/lib/matplotlib/tests/test_axes.py @@ -22,6 +22,10 @@ import warnings from matplotlib.cbook import IgnoredKeywordWarning +# Note: Some test cases are run twice: once normally and once with labeled data +# These two must be defined in the same test function or need to have +# different baseline images to prevent race conditions when nose runs +# the tests with multiple threads. @image_comparison(baseline_images=['formatter_ticker_001', 'formatter_ticker_002', @@ -274,7 +278,7 @@ def test_fill_units(): fig.autofmt_xdate() -@image_comparison(baseline_images=['single_point']) +@image_comparison(baseline_images=['single_point', 'single_point']) def test_single_point(): # Issue #1796: don't let lines.marker affect the grid matplotlib.rcParams['lines.marker'] = 'o' @@ -287,6 +291,16 @@ def test_single_point(): plt.subplot(212) plt.plot([1], [1], 'o') + # Reuse testcase from above for a labeled data test + data = {'a':[0], 'b':[1]} + + fig = plt.figure() + plt.subplot(211) + plt.plot('a', 'a', 'o', data=data) + + plt.subplot(212) + plt.plot('b','b', 'o', data=data) + @image_comparison(baseline_images=['single_date']) def test_single_date(): @@ -482,7 +496,7 @@ def test_axhspan_epoch(): ax.set_ylim(t0 - 5.0*dt, tf + 5.0*dt) -@image_comparison(baseline_images=['hexbin_extent'], +@image_comparison(baseline_images=['hexbin_extent', 'hexbin_extent'], remove_text=True, extensions=['png']) def test_hexbin_extent(): # this test exposes sf bug 2856228 @@ -495,6 +509,14 @@ def test_hexbin_extent(): ax.hexbin(x, y, extent=[.1, .3, .6, .7]) + # Reuse testcase from above for a labeled data test + data = {"x": x, "y": y} + + fig = plt.figure() + ax = fig.add_subplot(111) + ax.hexbin("x", "y", extent=[.1, .3, .6, .7], data=data) + + @image_comparison(baseline_images=['hexbin_empty'], remove_text=True, extensions=['png']) def test_hexbin_empty(): @@ -575,7 +597,7 @@ def test_nonfinite_limits(): ax.plot(x, y) -@image_comparison(baseline_images=['imshow'], +@image_comparison(baseline_images=['imshow', 'imshow'], remove_text=True) def test_imshow(): # Create a NxN image @@ -591,6 +613,12 @@ def test_imshow(): ax.imshow(r) + # Reuse testcase from above for a labeled data test + data={"r": r} + fig = plt.figure() + ax = fig.add_subplot(111) + ax.imshow("r", data=data) + @image_comparison(baseline_images=['imshow_clip']) def test_imshow_clip(): @@ -1011,13 +1039,21 @@ def test_marker_edges(): ax.plot(x+0.2, np.sin(x), 'y.', ms=30.0, mew=2, mec='b') -@image_comparison(baseline_images=['bar_tick_label_single'], +@image_comparison(baseline_images=['bar_tick_label_single', + 'bar_tick_label_single'], extensions=['png']) def test_bar_tick_label_single(): # From 2516: plot bar with array of string labels for x axis ax = plt.gca() ax.bar(0, 1 , tick_label='a') + # Reuse testcase from above for a labeled data test + data = {"a": 0, "b": 1} + fig = plt.figure() + ax = fig.add_subplot(111) + ax = plt.gca() + ax.bar("a", "b" , tick_label='a', data=data) + @image_comparison(baseline_images=['bar_tick_label_multiple'], extensions=['png']) @@ -1147,7 +1183,7 @@ def test_contour_colorbar(): cbar.add_lines(cs2, erase=False) -@image_comparison(baseline_images=['hist2d']) +@image_comparison(baseline_images=['hist2d', 'hist2d']) def test_hist2d(): np.random.seed(0) # make it not symetric in case we switch x and y axis @@ -1157,6 +1193,12 @@ def test_hist2d(): ax = fig.add_subplot(111) ax.hist2d(x, y, bins=10) + # Reuse testcase from above for a labeled data test + data = {"x": x, "y": y} + fig = plt.figure() + ax = fig.add_subplot(111) + ax.hist2d("x", "y", bins=10, data=data) + @image_comparison(baseline_images=['hist2d_transpose']) def test_hist2d_transpose(): @@ -1170,11 +1212,17 @@ def test_hist2d_transpose(): ax.hist2d(x, y, bins=10) -@image_comparison(baseline_images=['scatter']) +@image_comparison(baseline_images=['scatter', 'scatter']) def test_scatter_plot(): ax = plt.axes() - ax.scatter([3, 4, 2, 6], [2, 5, 2, 3], - c=['r', 'y', 'b', 'lime'], s=[24, 15, 19, 29]) + data = {"x": [3, 4, 2, 6], "y": [2, 5, 2, 3], "c": ['r', 'y', 'b', 'lime'], + "s": [24, 15, 19, 29]} + + ax.scatter(data["x"], data["y"], c=data["c"], s=data["s"]) + + # Reuse testcase from above for a labeled data test + ax = plt.axes() + ax.scatter("x", "y", c="c", s="s", data=data) @image_comparison(baseline_images=['scatter_marker'], remove_text=True, @@ -1252,7 +1300,8 @@ def test_log_scales(): ax.set_xscale('log', basex=9.0) -@image_comparison(baseline_images=['stackplot_test_image']) +@image_comparison(baseline_images=['stackplot_test_image', + 'stackplot_test_image']) def test_stackplot(): fig = plt.figure() x = np.linspace(0, 10, 10) @@ -1264,6 +1313,14 @@ def test_stackplot(): ax.set_xlim((0, 10)) ax.set_ylim((0, 70)) + # Reuse testcase from above for a labeled data test + data={"x": x, "y1": y1, "y2": y2, "y3": y3} + fig = plt.figure() + ax = fig.add_subplot(1, 1, 1) + ax.stackplot("x", "y1", "y2", "y3", data=data) + ax.set_xlim((0, 10)) + ax.set_ylim((0, 70)) + @image_comparison(baseline_images=['stackplot_test_baseline'], remove_text=True) @@ -1646,7 +1703,7 @@ def test_bxp_bad_positions(): assert_raises(ValueError, ax.bxp, logstats, positions=[2, 3]) -@image_comparison(baseline_images=['boxplot']) +@image_comparison(baseline_images=['boxplot', 'boxplot']) def test_boxplot(): x = np.linspace(-7, 7, 140) x = np.hstack([-25, x, 25]) @@ -1655,6 +1712,12 @@ def test_boxplot(): ax.boxplot([x, x], bootstrap=10000, notch=1) ax.set_ylim((-30, 30)) + # Reuse testcase from above for a labeled data test + data={"x": [x, x]} + fig, ax = plt.subplots() + ax.boxplot("x", bootstrap=10000, notch=1, data=data) + ax.set_ylim((-30, 30)) + @image_comparison(baseline_images=['boxplot_sym2'], remove_text=True, extensions=['png']) @@ -1834,7 +1897,8 @@ def test_boxplot_mod_artist_after_plotting(): obj.set_color('green') -@image_comparison(baseline_images=['violinplot_vert_baseline'], +@image_comparison(baseline_images=['violinplot_vert_baseline', + 'violinplot_vert_baseline'], extensions=['png']) def test_vert_violinplot_baseline(): # First 9 digits of frac(sqrt(2)) @@ -1844,6 +1908,13 @@ def test_vert_violinplot_baseline(): ax.violinplot(data, positions=range(4), showmeans=0, showextrema=0, showmedians=0) + # Reuse testcase from above for a labeled data test + data = {"d": data} + fig, ax = plt.subplots() + ax = plt.axes() + ax.violinplot("d", positions=range(4), showmeans=0, showextrema=0, + showmedians=0, data=data) + @image_comparison(baseline_images=['violinplot_vert_showmeans'], extensions=['png']) @@ -2021,7 +2092,8 @@ def test_manage_xticks(): assert_array_equal(old_xlim, new_xlim) -@image_comparison(baseline_images=['errorbar_basic', 'errorbar_mixed']) +@image_comparison(baseline_images=['errorbar_basic', 'errorbar_mixed', + 'errorbar_basic']) def test_errorbar(): x = np.arange(0.1, 4, 0.5) y = np.exp(-x) @@ -2065,6 +2137,15 @@ def test_errorbar(): fig.suptitle('Variable errorbars') + + # Reuse te first testcase from above for a labeled data test + data = {"x": x, "y": y} + fig = plt.figure() + ax = fig.gca() + ax.errorbar("x", "y", xerr=0.2, yerr=0.4, data=data) + ax.set_title("Simplest errorbars, 0.2 in x, 0.4 in y") + + @cleanup def test_errorbar_shape(): fig = plt.figure() @@ -2129,7 +2210,8 @@ def test_errorbar_limits(): ax.set_title('Errorbar upper and lower limits') -@image_comparison(baseline_images=['hist_stacked_stepfilled']) +@image_comparison(baseline_images=['hist_stacked_stepfilled', + 'hist_stacked_stepfilled']) def test_hist_stacked_stepfilled(): # make some data d1 = np.linspace(1, 3, 20) @@ -2138,6 +2220,12 @@ def test_hist_stacked_stepfilled(): ax = fig.add_subplot(111) ax.hist((d1, d2), histtype="stepfilled", stacked=True) + # Reuse testcase from above for a labeled data test + data = {"x": (d1, d2)} + fig = plt.figure() + ax = fig.add_subplot(111) + ax.hist("x", histtype="stepfilled", stacked=True, data=data) + @image_comparison(baseline_images=['hist_offset']) def test_hist_offset(): @@ -2386,7 +2474,7 @@ def test_alpha(): markersize=20, lw=10) -@image_comparison(baseline_images=['eventplot'], remove_text=True) +@image_comparison(baseline_images=['eventplot', 'eventplot'], remove_text=True) def test_eventplot(): ''' test that eventplot produces the correct output @@ -2423,6 +2511,15 @@ def test_eventplot(): num_collections = len(colls) np.testing.assert_equal(num_collections, num_datasets) + # Reuse testcase from above for a labeled data test + data = {"pos": data, "c": colors, "lo": lineoffsets, "ll": linelengths} + fig = plt.figure() + axobj = fig.add_subplot(111) + colls = axobj.eventplot("pos", colors="c", lineoffsets="lo", + linelengths="ll", data=data) + num_collections = len(colls) + np.testing.assert_equal(num_collections, num_datasets) + @image_comparison(baseline_images=['test_eventplot_defaults'], extensions=['png'], remove_text=True) def test_eventplot_defaults(): @@ -2543,7 +2640,8 @@ def test_eb_line_zorder(): ax.set_title("errorbar zorder test") -@image_comparison(baseline_images=['step_linestyle'], remove_text=True) +@image_comparison(baseline_images=['step_linestyle', 'step_linestyle'], + remove_text=True) def test_step_linestyle(): x = y = np.arange(10) @@ -2560,6 +2658,18 @@ def test_step_linestyle(): ax.set_xlim([-1, 5]) ax.set_ylim([-1, 7]) + # Reuse testcase from above for a labeled data test + data = {"x": x, "y": y, "y1": y+1, "y2": y+2} + fig, ax_lst = plt.subplots(2, 2) + ax_lst = ax_lst.flatten() + ln_styles = ['-', '--', '-.', ':'] + for ax, ls in zip(ax_lst, ln_styles): + ax.step("x", "y", lw=5, linestyle=ls, where='pre', data=data) + ax.step("x", "y1", lw=5, linestyle=ls, where='mid', data=data) + ax.step("x", "y2", lw=5, linestyle=ls, where='post', data=data) + ax.set_xlim([-1, 5]) + ax.set_ylim([-1, 7]) + @image_comparison(baseline_images=['mixed_collection'], remove_text=True) def test_mixed_collection(): @@ -3501,8 +3611,8 @@ def make_patch_spines_invisible(ax): host.tick_params(axis='x', **tkw) -@image_comparison(baseline_images=['twin_spines_on_top'], extensions=['png'], - remove_text=True) +@image_comparison(baseline_images=['twin_spines_on_top', 'twin_spines_on_top'], + extensions=['png'], remove_text=True) def test_twin_spines_on_top(): matplotlib.rcParams['axes.linewidth'] = 48.0 matplotlib.rcParams['lines.linewidth'] = 48.0 @@ -3521,6 +3631,16 @@ def test_twin_spines_on_top(): ax2.plot(data[0], data[1]/1E3, color='#7FC97F') ax2.fill_between(data[0], data[1]/1E3, color='#7FC97F', alpha=.5) + # Reuse testcase from above for a labeled data test + data = {"x": data[0], "y": data[1]/1E3} + fig = plt.figure() + ax1 = fig.add_subplot(1, 1, 1) + ax2 = ax1.twinx() + ax1.plot("x", "y", color='#BEAED4', data=data) + ax1.fill_between("x", "y", color='#BEAED4', alpha=.8, data=data) + ax2.plot("x", "y", color='#7FC97F', data=data) + ax2.fill_between("x", "y", color='#7FC97F', alpha=.5, data=data) + @cleanup def test_rcparam_grid_minor(): @@ -3606,7 +3726,9 @@ def test_text_labelsize(): ax.tick_params(direction='out') -@image_comparison(baseline_images=['pie_linewidth_0'], extensions=['png']) +@image_comparison(baseline_images=['pie_linewidth_0', 'pie_linewidth_0', + 'pie_linewidth_0'], + extensions=['png']) def test_pie_linewidth_0(): # The slices will be ordered and plotted counter-clockwise. labels = 'Frogs', 'Hogs', 'Dogs', 'Logs' @@ -3620,6 +3742,23 @@ def test_pie_linewidth_0(): # Set aspect ratio to be equal so that pie is drawn as a circle. plt.axis('equal') + # Reuse testcase from above for a labeled data test + data = {"l": labels, "s": sizes, "c": colors, "ex": explode} + fig = plt.figure() + ax = fig.gca() + ax.pie("s", explode="ex", labels="l", colors="c", + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 0}, data=data) + ax.axis('equal') + + # And again to test the pyplot functions which should also be able to be + # called with a data kwarg + plt.figure() + plt.pie("s", explode="ex", labels="l", colors="c", + autopct='%1.1f%%', shadow=True, startangle=90, + wedgeprops={'linewidth': 0}, data=data) + plt.axis('equal') + @image_comparison(baseline_images=['pie_center_radius'], extensions=['png']) def test_pie_center_radius(): diff --git a/lib/matplotlib/tests/test_labeled_data_unpacking.py b/lib/matplotlib/tests/test_labeled_data_unpacking.py new file mode 100644 index 000000000000..e8baf8235e4f --- /dev/null +++ b/lib/matplotlib/tests/test_labeled_data_unpacking.py @@ -0,0 +1,422 @@ +from __future__ import (absolute_import, division, print_function) + +from nose.tools import (assert_raises, assert_equal) +from nose.plugins.skip import SkipTest + +try: + # 3.2+ versions + from nose.tools import assert_regex, assert_not_regex +except ImportError: + try: + # 2.7 versions + from nose.tools import assert_regexp_matches, assert_not_regexp_matches + assert_regex = assert_regexp_matches + assert_not_regex = assert_not_regexp_matches + except ImportError: + # 2.6 versions + def noop(txt, regex): + raise SkipTest("No assert for regex matching in py2.6") + assert_regex = noop + assert_not_regex = noop + +from ..testing import assert_produces_warning + +from .. import unpack_labeled_data + + +# Notes on testing the plotting functions itself +# * the individual decorated plotting functions are tested in 'test_axes.py' +# * that pyplot functions accept a data kwarg is only tested in +# test_axes.test_pie_linewidth_0 + + +# these two get used in multiple tests, so define them here +@unpack_labeled_data(replace_names=["x", "y"], label_namer="y") +def plot_func(ax, x, y, ls="x", label=None, w="xyz"): + return ("x: %s, y: %s, ls: %s, w: %s, label: %s" % ( + list(x), list(y), ls, w, label)) + + +@unpack_labeled_data(replace_names=["x", "y"], label_namer="y", + positional_parameter_names=["x", "y", "ls", "label", "w"]) +def plot_func_varags(ax, *args, **kwargs): + all_args = [None, None, "x", None, "xyz"] + for i, v in enumerate(args): + all_args[i] = v + for i, k in enumerate(["x", "y", "ls", "label", "w"]): + if k in kwargs: + all_args[i] = kwargs[k] + x, y, ls, label, w = all_args + return ("x: %s, y: %s, ls: %s, w: %s, label: %s" % ( + list(x), list(y), ls, w, label)) + + +all_funcs = [plot_func, plot_func_varags] + + +def test_compiletime_checks(): + """test decorator invocations -> no replacements""" + + def func(ax, x, y): pass + + def func_args(ax, x, y, *args): pass + + def func_kwargs(ax, x, y, **kwargs): pass + + def func_no_ax_args(*args, **kwargs): pass + + # this is ok + unpack_labeled_data(replace_names=["x", "y"])(func) + unpack_labeled_data(replace_names=["x", "y"])(func_kwargs) + # this has "enough" information to do all the replaces + unpack_labeled_data(replace_names=["x", "y"])(func_args) + + # no positional_parameter_names but needed due to replaces + def f(): + # z is unknown + unpack_labeled_data(replace_names=["x", "y", "z"])(func_args) + + assert_raises(AssertionError, f) + + def f(): + unpack_labeled_data(replace_names=["x", "y"])(func_no_ax_args) + + assert_raises(AssertionError, f) + + # no replacements at all -> all ok... + unpack_labeled_data(replace_names=[], label_namer=None)(func) + unpack_labeled_data(replace_names=[], label_namer=None)(func_args) + unpack_labeled_data(replace_names=[], label_namer=None)(func_kwargs) + unpack_labeled_data(replace_names=[], label_namer=None)(func_no_ax_args) + + # label namer is unknown + def f(): + unpack_labeled_data(label_namer="z")(func) + + assert_raises(AssertionError, f) + + def f(): + unpack_labeled_data(label_namer="z")(func_args) + + assert_raises(AssertionError, f) + # but "ok-ish", if func has kwargs -> will show up at runtime :-( + unpack_labeled_data(label_namer="z")(func_kwargs) + unpack_labeled_data(label_namer="z")(func_no_ax_args) + + +def test_label_problems_at_runtime(): + """Tests for behaviour which would actually be nice to get rid of.""" + + @unpack_labeled_data(label_namer="z") + def func(*args, **kwargs): + pass + + def f(): + func(None, x="a", y="b") + + # This is a programming mistake: the parameter which should add the + # label is not present in the function call. Unfortunately this was masked + # due to the **kwargs useage + # This would be nice to handle as a compiletime check (see above...) + with assert_produces_warning(RuntimeWarning): + f() + + def real_func(x, y): + pass + + @unpack_labeled_data(label_namer="x") + def func(*args, **kwargs): + real_func(**kwargs) + + def f(): + func(None, x="a", y="b") + + # This sets a label although the function can't handle it. + assert_raises(TypeError, f) + + +def test_function_call_without_data(): + """test without data -> no replacements""" + for func in all_funcs: + assert_equal(func(None, "x", "y"), + "x: ['x'], y: ['y'], ls: x, w: xyz, label: None") + assert_equal(func(None, x="x", y="y"), + "x: ['x'], y: ['y'], ls: x, w: xyz, label: None") + assert_equal(func(None, "x", "y", label=""), + "x: ['x'], y: ['y'], ls: x, w: xyz, label: ") + assert_equal(func(None, "x", "y", label="text"), + "x: ['x'], y: ['y'], ls: x, w: xyz, label: text") + assert_equal(func(None, x="x", y="y", label=""), + "x: ['x'], y: ['y'], ls: x, w: xyz, label: ") + assert_equal(func(None, x="x", y="y", label="text"), + "x: ['x'], y: ['y'], ls: x, w: xyz, label: text") + + +def test_function_call_with_dict_data(): + """Test with dict data -> label comes from the value of 'x' parameter """ + data = {"a": [1, 2], "b": [8, 9], "w": "NOT"} + for func in all_funcs: + assert_equal(func(None, "a", "b", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b") + assert_equal(func(None, x="a", y="b", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b") + assert_equal(func(None, "a", "b", label="", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal(func(None, "a", "b", label="text", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + assert_equal(func(None, x="a", y="b", label="", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal(func(None, x="a", y="b", label="text", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + + +def test_function_call_with_dict_data_not_in_data(): + "test for the case that one var is not in data -> half replaces, half kept" + data = {"a": [1, 2], "w": "NOT"} + for func in all_funcs: + assert_equal(func(None, "a", "b", data=data), + "x: [1, 2], y: ['b'], ls: x, w: xyz, label: b") + assert_equal(func(None, x="a", y="b", data=data), + "x: [1, 2], y: ['b'], ls: x, w: xyz, label: b") + assert_equal(func(None, "a", "b", label="", data=data), + "x: [1, 2], y: ['b'], ls: x, w: xyz, label: ") + assert_equal(func(None, "a", "b", label="text", data=data), + "x: [1, 2], y: ['b'], ls: x, w: xyz, label: text") + assert_equal(func(None, x="a", y="b", label="", data=data), + "x: [1, 2], y: ['b'], ls: x, w: xyz, label: ") + assert_equal(func(None, x="a", y="b", label="text", data=data), + "x: [1, 2], y: ['b'], ls: x, w: xyz, label: text") + + +def test_function_call_with_pandas_data(): + """test with pandas dataframe -> label comes from data["col"].name """ + try: + import pandas as pd + except ImportError: + raise SkipTest("Pandas not installed") + + data = pd.DataFrame({"a": [1, 2], "b": [8, 9], "w": ["NOT", "NOT"]}) + + for func in all_funcs: + assert_equal(func(None, "a", "b", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b") + assert_equal(func(None, x="a", y="b", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b") + assert_equal(func(None, "a", "b", label="", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal(func(None, "a", "b", label="text", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + assert_equal(func(None, x="a", y="b", label="", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal(func(None, x="a", y="b", label="text", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + + +def test_function_call_replace_all(): + """Test without a "replace_names" argument, all vars should be replaced""" + data = {"a": [1, 2], "b": [8, 9], "x": "xyz"} + + @unpack_labeled_data(label_namer="y") + def func_replace_all(ax, x, y, ls="x", label=None, w="NOT"): + return "x: %s, y: %s, ls: %s, w: %s, label: %s" % ( + list(x), list(y), ls, w, label) + + assert_equal(func_replace_all(None, "a", "b", w="x", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b") + assert_equal(func_replace_all(None, x="a", y="b", w="x", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b") + assert_equal(func_replace_all(None, "a", "b", w="x", label="", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal( + func_replace_all(None, "a", "b", w="x", label="text", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + assert_equal( + func_replace_all(None, x="a", y="b", w="x", label="", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal( + func_replace_all(None, x="a", y="b", w="x", label="text", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + + @unpack_labeled_data(label_namer="y") + def func_varags_replace_all(ax, *args, **kwargs): + all_args = [None, None, "x", None, "xyz"] + for i, v in enumerate(args): + all_args[i] = v + for i, k in enumerate(["x", "y", "ls", "label", "w"]): + if k in kwargs: + all_args[i] = kwargs[k] + x, y, ls, label, w = all_args + return "x: %s, y: %s, ls: %s, w: %s, label: %s" % ( + list(x), list(y), ls, w, label) + + # in the first case, we can't get a "y" argument, + # as we don't know the names of the *args + assert_equal(func_varags_replace_all(None, x="a", y="b", w="x", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b") + assert_equal( + func_varags_replace_all(None, "a", "b", w="x", label="", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal( + func_varags_replace_all(None, "a", "b", w="x", label="text", + data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + assert_equal( + func_varags_replace_all(None, x="a", y="b", w="x", label="", + data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal( + func_varags_replace_all(None, x="a", y="b", w="x", label="text", + data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + + with assert_produces_warning(): + assert_equal(func_varags_replace_all(None, "a", "b", w="x", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: None") + + +def test_no_label_replacements(): + """Test with "label_namer=None" -> no label replacement at all""" + + @unpack_labeled_data(replace_names=["x", "y"], label_namer=None) + def func_no_label(ax, x, y, ls="x", label=None, w="xyz"): + return "x: %s, y: %s, ls: %s, w: %s, label: %s" % ( + list(x), list(y), ls, w, label) + + data = {"a": [1, 2], "b": [8, 9], "w": "NOT"} + assert_equal(func_no_label(None, "a", "b", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: None") + assert_equal(func_no_label(None, x="a", y="b", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: None") + assert_equal(func_no_label(None, "a", "b", label="", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal(func_no_label(None, "a", "b", label="text", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + + +def test_more_args_than_pos_parameter(): + @unpack_labeled_data(replace_names=["x", "y"], label_namer="y") + def func(ax, x, y, z=1): + pass + + data = {"a": [1, 2], "b": [8, 9], "w": "NOT"} + + def f(): + func(None, "a", "b", "z", "z", data=data) + + assert_raises(RuntimeError, f) + + +def test_function_call_with_replace_all_args(): + """Test with a "replace_all_args" argument, all *args should be replaced""" + data = {"a": [1, 2], "b": [8, 9], "x": "xyz"} + + def funcy(ax, *args, **kwargs): + all_args = [None, None, "x", None, "NOT"] + for i, v in enumerate(args): + all_args[i] = v + for i, k in enumerate(["x", "y", "ls", "label", "w"]): + if k in kwargs: + all_args[i] = kwargs[k] + x, y, ls, label, w = all_args + return "x: %s, y: %s, ls: %s, w: %s, label: %s" % ( + list(x), list(y), ls, w, label) + + func = unpack_labeled_data(replace_all_args=True, replace_names=["w"], + label_namer="y")(funcy) + + assert_equal(func(None, "a", "b", w="x", label="", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal(func(None, "a", "b", w="x", label="text", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + + func2 = unpack_labeled_data(replace_all_args=True, replace_names=["w"], + label_namer="y", + positional_parameter_names=["x", "y", "ls", + "label", "w"])( + funcy) + + assert_equal(func2(None, "a", "b", w="x", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: b") + assert_equal(func2(None, "a", "b", w="x", label="", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: ") + assert_equal(func2(None, "a", "b", w="x", label="text", data=data), + "x: [1, 2], y: [8, 9], ls: x, w: xyz, label: text") + + +def test_docstring_addition(): + @unpack_labeled_data() + def funcy(ax, *args, **kwargs): + """Funcy does nothing""" + pass + + assert_regex(funcy.__doc__, + r".*All positional and all keyword arguments\.") + assert_not_regex(funcy.__doc__, r".*All positional arguments\.") + assert_not_regex(funcy.__doc__, + r".*All arguments with the following names: .*") + + @unpack_labeled_data(replace_all_args=True, replace_names=[]) + def funcy(ax, x, y, z, bar=None): + """Funcy does nothing""" + pass + + assert_regex(funcy.__doc__, r".*All positional arguments\.") + assert_not_regex(funcy.__doc__, + r".*All positional and all keyword arguments\.") + assert_not_regex(funcy.__doc__, + r".*All arguments with the following names: .*") + + @unpack_labeled_data(replace_all_args=True, replace_names=["bar"]) + def funcy(ax, x, y, z, bar=None): + """Funcy does nothing""" + pass + + assert_regex(funcy.__doc__, r".*All positional arguments\.") + assert_regex(funcy.__doc__, + r".*All arguments with the following names: 'bar'\.") + assert_not_regex(funcy.__doc__, + r".*All positional and all keyword arguments\.") + + @unpack_labeled_data(replace_names=["x", "bar"]) + def funcy(ax, x, y, z, bar=None): + """Funcy does nothing""" + pass + + # lists can print in any order, so test for both x,bar and bar,x + assert_regex(funcy.__doc__, + r".*All arguments with the following names: '.*', '.*'\.") + assert_regex(funcy.__doc__, r".*'x'.*") + assert_regex(funcy.__doc__, r".*'bar'.*") + assert_not_regex(funcy.__doc__, + r".*All positional and all keyword arguments\.") + assert_not_regex(funcy.__doc__, r".*All positional arguments\.") + + +def test_positional_parameter_names_as_function(): + # Also test the _plot_arg_replacer for plot... + from matplotlib.axes._axes import _plot_args_replacer + + @unpack_labeled_data(replace_names=["x", "y"], + positional_parameter_names=_plot_args_replacer) + def funcy(ax, *args, **kwargs): + return "{args} | {kwargs}".format(args=args, kwargs=kwargs) + + # the normal case... + data = {"x": "X", "y1": "Y"} + assert_equal(funcy(None, "x", "y1", data=data), + "('X', 'Y') | {}") + assert_equal(funcy(None, "x", "y1", "c", data=data), + "('X', 'Y', 'c') | {}") + + # no arbitrary long args with data + def f(): + assert_equal(funcy(None, "x", "y", "c", "x", "y", "x", "y", data=data), + "('X', 'Y', 'c', 'X', 'Y', 'X', 'Y') | {}") + assert_raises(ValueError, f) + + # In the two arg case, if a valid color spec is in data, we warn but use + # it as data... + data = {"x": "X", "y": "Y", "ro": "!!"} + with assert_produces_warning(RuntimeWarning): + assert_equal(funcy(None, "y", "ro", data=data), + "('Y', '!!') | {}")