diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 25089d71a..ec96349ef 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -71,6 +71,9 @@ ProPlot v0.6.0 (2020-##-##) There are quite a lot of deprecations for this release. +- Rename `add_errorbars` to `~proplot.axes.plot.indicate_error` and rename + various keyword args (:pr:`166`, :commit:`d8c50a8d`). +- Remove ``'rgbcycle'`` setting (:commit:`6653b7f0`). - Deprecate support for "parametric" plots inside `~matplotlib.axes.Axes.plot`, instead use `~proplot.axes.Axes.parametric` (:commit:`64210bce`). - Change `~proplot.utils.units` ``units`` keyword argument to more natural @@ -115,10 +118,25 @@ There are quite a lot of deprecations for this release. .. rubric:: Features -- Support building a colormap and `DiscreteNorm` inside `~matplotlib.axes.Axes.scatter`, - just like `contourf` and `pcolormesh` (:pr:`162`). +- Add options to `~proplot.axes.plot.indicate_error` for adding *shading* + to arbitrary plots (:pr:`166`, :commit:`d8c50a8d`). Also support automatic legend + entries for shading and ensure `indicate_error` preserves metadata. +- Wrap ``pcolorfast`` just like ``pcolor`` and ``pcolormesh`` are + wrapped (:commit:`50a262dd`). +- Add ``negpos`` feature to `~proplot.axes.plot.bar_wrapper` and new + :rcraw:`negcolor` and :rcraw:`poscolor` rc keyword arguments (:commit:`ab4d6746`). +- Increase default line width from ``0.6`` to ``0.8`` to match matplotlib defaults + (:commit:`f801852b`; try to avoid frivolously changing defaults). +- Change default resolution for geographic features from ``'lo'`` to ``'med'`` + (:commit:`f801852b`). +- Change default line style for geographic gridlines from ``':'`` to ``'-'`` + and match style from primary gridlines (:commit:`f801852b`). +- Support cartopy inline meridian and parallel gridlines and support + changing the gridline padding (:commit:`###`). - Support `cartopy 0.18 `__ locators, formatters, deprecations, and new labelling features (:pr:`158`). +- Support building a colormap and `DiscreteNorm` inside `~matplotlib.axes.Axes.scatter`, + just like `contourf` and `pcolormesh` (:pr:`162`). - Add :rcraw:`geogrid.labelpad` and :rcraw:`geogrid.rotatelabels` settings for cartopy gridline labels (:pr:`158`). - Support more `~proplot.ticker.AutoFormatter` features on @@ -161,6 +179,8 @@ There are quite a lot of deprecations for this release. .. rubric:: Bug fixes +- Fix various issues with axis label sharing and axis sharing for + twinned axes and panel axes (:pr:`164`). - Fix issue drawing bar plots with datetime *x* axes (:pr:`156`). - Fix issue where `~proplot.ticker.AutoFormatter` tools were not locale-aware, i.e. use comma as decimal point sometimes (:commit:`c7636296`). @@ -206,9 +226,8 @@ There are quite a lot of deprecations for this release. sensible behavior. - Turn some private `~proplot.config` functions into static methods (:commit:`6121de03`). +- Remove "smart bounds" feature from `FuncScale` (:commit:`9ac149ea`). - Clean up axes iterators (:commit:`c8a0768a`). -- Clean up locator and formatter sharing stuff (:commit:`###`). -- Configure label sharing at draw-time rather than label-time (:commit:`###`). .. rubric:: Documentation @@ -245,7 +264,6 @@ ProPlot v0.5.0 (2020-02-10) .. rubric:: Bug fixes -- Fix issue where colormaps cannot have "dot" in name (:commit:`###`). - Fix issue where `~proplot.styletools.show_cmaps` and `~proplot.styletools.show_cycles` colormap names were messed up (:commit:`13045599`) @@ -256,9 +274,9 @@ ProPlot v0.5.0 (2020-02-10) `~proplot.wrappers.colorbar_wrapper` were sometimes ignored (:commit:`fd4f8d5f`). - Permit passing *lists of colors* to manually shade line contours and filled - contours in `~proplot.wrappers.cmap_changer` (:commit:`###`). + contours in `~proplot.wrappers.cmap_changer`. - Prevent formatting rightmost meridian label as ``1e-10`` on cartopy map - projections (:commit:`37fdd1eb]`). + projections (:commit:`37fdd1eb`). - Support CF-time axes by fixing bug in `~proplot.wrappers.standardize_1d` and `~proplot.wrappers.standardize_2d` (:issue:`103`, :pr:`121`). - Redirect to the "default" location when using ``legend=True`` and diff --git a/docs/1dplots.py b/docs/1dplots.py index 5f9bdb8a5..1c9b0eef6 100644 --- a/docs/1dplots.py +++ b/docs/1dplots.py @@ -94,7 +94,7 @@ fig, axs = plot.subplots(ncols=2, share=False) x = np.linspace(-5, 5, N) y = state.rand(N, 5) - axs.format(xlabel='xlabel', ylabel='ylabel', ymargin=0.05) + axs.format(xlabel='xlabel', ylabel='ylabel') axs.format(suptitle='Standardized arguments demonstration') # Plot by passing both x and y coordinates @@ -182,66 +182,70 @@ # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_errorbars: # -# Adding error bars -# ----------------- +# Error bars and shading +# ---------------------- # -# The `~proplot.axes.add_errorbars` wrapper lets you draw error bars -# on-the-fly by passing certain keyword arguments to +# The `~proplot.axes.indicate_error` wrapper lets you draw error bars +# and error shading on-the-fly by passing certain keyword arguments to # `~matplotlib.axes.Axes.plot`, `~matplotlib.axes.Axes.scatter`, # `~matplotlib.axes.Axes.bar`, or `~matplotlib.axes.Axes.barh`. # # If you pass 2D arrays to these methods with ``means=True`` or # ``medians=True``, the means or medians of each column are drawn as points, -# lines, or bars, and error bars are drawn to represent the spread in each -# column. `~proplot.axes.add_errorbars` lets you draw both thin error -# "bars" with optional whiskers, and thick error "boxes" overlayed on top of -# these bars (this can be used to represent different percentil ranges). -# Instead of using 2D arrays, you can also pass error bar coordinates -# *manually* with the `bardata` and `boxdata` keyword arguments. See -# `~proplot.axes.add_errorbars` for details. +# lines, or bars, and error bars or shading is drawn to represent the spread in +# each column. `~proplot.axes.indicate_error` lets you draw thin error +# bars with optional whiskers, thick "boxes" overlayed on top of these bars +# (think of this as a miniature boxplot), or up to 2 intervals of shading. +# Instead of using 2D arrays, you can also pass the error bounds +# *manually* with the `bardata`, `boxdata`, `shadedata`, and `fadedata` keyword +# arguments. See `~proplot.axes.indicate_error` for details. -# %% import proplot as plot import numpy as np import pandas as pd plot.rc['title.loc'] = 'uc' -plot.rc['axes.ymargin'] = plot.rc['axes.xmargin'] = 0.05 + +# Generate sample data state = np.random.RandomState(51423) -data = ( - state.rand(20, 8).cumsum(axis=0).cumsum(axis=1)[:, ::-1] - + 20 * state.normal(size=(20, 8)) + 30 -) +data = state.rand(20, 8).cumsum(axis=0).cumsum(axis=1)[:, ::-1] +data = data + 20 * state.normal(size=(20, 8)) + 30 +data = pd.DataFrame(data, columns=np.arange(0, 16, 2)) +data.name = 'variable' + +# Generate figure fig, axs = plot.subplots( nrows=3, aspect=1.5, axwidth=4, share=0, hratios=(2, 1, 1) ) -axs.format(suptitle='Error bars with various plotting commands') +axs.format(suptitle='Indicating error bounds with various plotting commands') axs[1:].format(xlabel='column number', xticks=1, xgrid=False) -# Asking add_errorbars to calculate bars +# Automatically calculate medians and display default percentile range ax = axs[0] -obj = ax.barh(data, color='red orange', means=True) +obj = ax.barh( + data, color='light red', legend=True, + medians=True, barpctiles=True, boxpctiles=True +) ax.format(title='Column statistics') ax.format(ylabel='column number', title='Bar plot', ygrid=False) -# Showing a standard deviation range instead of percentile range +# Automatically calculate means and display requested standard deviation range ax = axs[1] ax.scatter( - data, color='k', marker='_', markersize=50, - medians=True, barstd=True, boxes=False, capsize=2, - barcolor='gray6', barrange=(-1, 1), barzorder=0, barlw=1, + data, color='denim', marker='x', markersize=8**2, linewidth=0.8, + means=True, shadestds=(-1, 1), legend='ll', ) ax.format(title='Scatter plot') -# Supplying error bar data manually +# Manually supply error bar data and legend labels ax = axs[2] -boxdata = np.percentile(data, (25, 75), axis=0) -bardata = np.percentile(data, (5, 95), axis=0) +means = data.mean(axis=0) +means.name = data.name +shadedata = np.percentile(data, (25, 75), axis=0) # dark shading +fadedata = np.percentile(data, (5, 95), axis=0) # light shading ax.plot( - data.mean(axis=0), boxes=True, - edgecolor='k', color='gray9', - boxdata=boxdata, bardata=bardata, barzorder=0, - boxcolor='gray7', barcolor='gray7', boxmarker=False, + means, shadedata=shadedata, fadedata=fadedata, + color='ocean blue', barzorder=0, boxmarker=False, legend='ll', ) ax.format(title='Line plot') plot.rc.reset() @@ -258,9 +262,10 @@ # `~proplot.axes.cycle_changer`, and `~proplot.axes.standardize_1d`. # You can now *group* or *stack* columns of data by passing 2D arrays to # `~matplotlib.axes.Axes.bar` or `~matplotlib.axes.Axes.barh`, just like in -# `pandas`. Also, `~matplotlib.axes.Axes.bar` and `~matplotlib.axes.Axes.barh` -# now employ "default" *x* coordinates if you failed to provide them -# explicitly, just like `~matplotlib.axes.Axes.plot`. +# `pandas`, or use different colors for negative and positive bars by +# passing ``negpos=True``. Also, `~matplotlib.axes.Axes.bar` and +# `~matplotlib.axes.Axes.barh` now employ "default" *x* coordinates if you +# failed to provide them explicitly. # # To make filled "area" plots, use the new `~proplot.axes.Axes.area` and # `~proplot.axes.Axes.areax` methods. These are alises for @@ -270,7 +275,7 @@ # `~proplot.axes.fill_betweenx_wrapper`. You can now *stack* or *overlay* # columns of data by passing 2D arrays to `~proplot.axes.Axes.area` and # `~proplot.axes.Axes.areax`, just like in `pandas`. You can also now draw -# area plots that *change color* when the fill boundaries cross each other by +# area plots that change color when the fill boundaries cross each other by # passing ``negpos=True`` to `~matplotlib.axes.Axes.fill_between`. The most # common use case for this is highlighting negative and positive areas with # different colors, as shown below. @@ -280,7 +285,6 @@ import numpy as np import pandas as pd plot.rc.titleloc = 'uc' -plot.rc.margin = 0.05 fig, axs = plot.subplots(nrows=2, aspect=2, axwidth=5, share=0, hratios=(3, 2)) state = np.random.RandomState(51423) data = state.rand(5, 5).cumsum(axis=0).cumsum(axis=1)[:, ::-1] @@ -313,20 +317,21 @@ # %% import proplot as plot import numpy as np -plot.rc.margin = 0 -fig, axs = plot.subplots(array=[[1, 2], [3, 3]], hratios=(1, 0.8), share=0) -axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Area plot demo') +fig, axs = plot.subplots(array=[[1, 2], [3, 3]], hratios=(1, 1.5), share=0) +axs.format(grid=False, xlabel='xlabel', ylabel='ylabel', suptitle='Area plot demo') state = np.random.RandomState(51423) data = state.rand(5, 3).cumsum(axis=0) cycle = ('gray3', 'gray5', 'gray7') -# Overlaid and stacked area patches +# Overlaid area patches ax = axs[0] ax.area( - np.arange(5), data, data + state.rand(5)[:, None], cycle=cycle, alpha=0.5, + np.arange(5), data, data + state.rand(5)[:, None], cycle=cycle, alpha=0.7, legend='uc', legend_kw={'center': True, 'ncols': 2, 'labels': ['z', 'y', 'qqqq']}, ) ax.format(title='Fill between columns') + +# Stacked area patches ax = axs[1] ax.area( np.arange(5), data, stacked=True, cycle=cycle, alpha=0.8, @@ -334,12 +339,17 @@ ) ax.format(title='Stack between columns') -# Positive and negative color area patches +# Positive and negative color bars and area patches ax = axs[2] -data = 5 * (state.rand(20) - 0.5) -ax.area(data, negpos=True, negcolor='blue7', poscolor='red7') -ax.format(title='Positive and negative colors', xlabel='xlabel', ylabel='ylabel') -axs.format(grid=False) +data = 4 * (state.rand(20) - 0.5) +ax.bar(data, bottom=-2, width=1, edgecolor='none', negpos=True) +ax.area(data + 2, y2=2, negpos=True) +for offset in (-2, 2): + ax.axhline(offset, color='k', linewidth=1, linestyle='--') +ax.format( + xmargin=0, xlabel='xlabel', ylabel='ylabel', + title='Positive and negative colors demo', titleweight='bold', +) plot.rc.reset() @@ -361,18 +371,19 @@ import proplot as plot import numpy as np import pandas as pd + +# Generate sample data N = 500 state = np.random.RandomState(51423) -fig, axs = plot.subplots(ncols=2, axwidth=2.5) data = state.normal(size=(N, 5)) + 2 * (state.rand(N, 5) - 0.5) * np.arange(5) data = pd.DataFrame( data, columns=pd.Index(['a', 'b', 'c', 'd', 'e'], name='xlabel') ) -axs.format( - ymargin=0.1, xmargin=0.1, grid=False, - suptitle='Boxes and violins demo' -) + +# Generate figure +fig, axs = plot.subplots(ncols=2, axwidth=2.5) +axs.format(grid=False, suptitle='Boxes and violins demo') # Box plots ax = axs[0] @@ -428,7 +439,6 @@ x, y, c, cmap=cmap, lw=7, interp=5, capstyle='round', joinstyle='round' ) ax.format(xlabel='xlabel', ylabel='ylabel', title='Line with smooth gradations') -ax.format(xmargin=0.05, ymargin=0.05) ax.colorbar(m, loc='b', label='parametric coordinate', locator=5) # Parametric line with stepped gradations @@ -463,12 +473,6 @@ # `~matplotlib.axes.Axes.scatter` now optionally accepts keywords that look # like `~matplotlib.axes.Axes.plot` keywords (e.g. `color` instead of `c` and # `markersize` instead of `s`). -# -# We are also considering supporting 2D array input and property cycle -# iteration for more obscure matplotlib plotting commands like -# `~matplotlib.axes.Axes.stem`, `~matplotlib.axes.Axes.step`, -# `~matplotlib.axes.Axes.vlines`, and `~matplotlib.axes.Axes.hlines`. Stay -# tuned. # %% import proplot as plot diff --git a/docs/basics.py b/docs/basics.py index f49c7ce6b..5babac057 100644 --- a/docs/basics.py +++ b/docs/basics.py @@ -200,7 +200,7 @@ xlabel='x-axis', ylabel='y-axis', xscale='log', xlim=(1, 10), xticks=1, - ylim=(-2, 2), yticks=plot.arange(-2, 2), + ymargin=0.05, yticks=plot.arange(-2, 2), yticklabels=('a', 'bb', 'c', 'dd', 'e'), ytickloc='both', yticklabelloc='both', xtickdir='inout', xtickminor=False, ygridminor=True, diff --git a/docs/configuration.rst b/docs/configuration.rst index 6601fd6c9..52eb112d2 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -89,9 +89,10 @@ Key Description ``linewidth`` Thickness of axes spines and major tick lines. ``lut`` The number of colors to put in the colormap lookup table. ``margin`` The margin of space between axes edges and objects plotted inside the axes, if ``xlim`` and ``ylim`` are unset. +``negcolor`` The color for negative bars and shaded areas when using ``negpos=True``. ``ocean`` Boolean, toggles ocean patches on and off. +``poscolor`` The color for positive bars and shaded areas when using ``negpos=True``. ``reso`` Resolution of geographic features, one of ``'lo'``, ``'med'``, or ``'hi'`` -``rgbcycle`` If ``True``, and ``colorblind`` is the current cycle, this registers the ``colorblind`` colors as ``'r'``, ``'b'``, ``'g'``, etc., like in `seaborn `__. ``rivers`` Boolean, toggles river lines on and off. ``share`` The axis sharing level, one of ``0``, ``1``, ``2``, or ``3``. See `~proplot.ui.subplots` for details. ``small`` Font size for legend text, tick labels, axis labels, and text generated with `~matplotlib.axes.Axes.text`. diff --git a/docs/projections.py b/docs/projections.py index d1feea054..208d679c0 100644 --- a/docs/projections.py +++ b/docs/projections.py @@ -220,12 +220,6 @@ fig.colorbar(m, loc='b', span=i + 1, label='values', extendsize='1.7em') else: ax.pcolor(lon, lat, data, cmap=cmap, globe=globe, extend='both') - if globe: - continue - xi = offset + np.linspace(0, 360, 20) - for cmd in (np.sin, np.cos): - yi = cmd(xi * np.pi / 180) * 60 - ax.plot(xi, yi, color='k', lw=0, marker='o') axs.format( suptitle=titles[globe], collabels=['Cartopy example', 'Basemap example'], diff --git a/docs/sphinxext/custom_roles.py b/docs/sphinxext/custom_roles.py index d64fe89dc..a24d2f0e9 100644 --- a/docs/sphinxext/custom_roles.py +++ b/docs/sphinxext/custom_roles.py @@ -1,6 +1,6 @@ from docutils import nodes from os.path import sep -from proplot.internals import defaults +from proplot.internals import rcsetup def get_nodes(rawtext, text, inliner): @@ -20,7 +20,7 @@ def get_nodes(rawtext, text, inliner): def rc_role(name, rawtext, text, lineno, inliner, options={}, content=[]): # noqa: U100 node_list = get_nodes(rawtext, text, inliner) try: - default = defaults._get_default_param(text) + default = rcsetup._get_default_param(text) except KeyError: pass else: diff --git a/docs/why.rst b/docs/why.rst index 971a1c7f8..20c68affc 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -400,8 +400,8 @@ designed to make your life easier. `~matplotlib.axes.Axes.fill_betweenx`, you will be able to use different colors for positive/negative bars. * All :ref:`1D plotting methods ` can be used to :ref:`add error - bars ` using various specialized keyword arguments. You no - longer have to work with the `~matplotlib.axes.Axes.errorbar` method + bars or error shading ` using various specialized keyword arguments. + You no longer have to work with the `~matplotlib.axes.Axes.errorbar` method directly. They also accept a :ref:`"cycle" keyword argument ` interpreted by `~proplot.constructor.Cycle` and optional diff --git a/proplot/axes/base.py b/proplot/axes/base.py index 027808f35..ef6cc1938 100644 --- a/proplot/axes/base.py +++ b/proplot/axes/base.py @@ -11,19 +11,20 @@ import matplotlib.collections as mcollections from .plot import ( _get_transform, - _add_errorbars, _bar_wrapper, _barh_wrapper, _boxplot_wrapper, + _bar_wrapper, _barh_wrapper, _boxplot_wrapper, _cmap_changer, _cycle_changer, - _fill_between_wrapper, _fill_betweenx_wrapper, - _hist_wrapper, _parametric_wrapper, _plot_wrapper, _scatter_wrapper, + _fill_between_wrapper, _fill_betweenx_wrapper, _hlines_wrapper, + _hist_wrapper, _indicate_error, + _parametric_wrapper, _plot_wrapper, _scatter_wrapper, _stem_wrapper, _standardize_1d, _standardize_2d, - _text_wrapper, _violinplot_wrapper, + _text_wrapper, _violinplot_wrapper, _vlines_wrapper, colorbar_wrapper, legend_wrapper, ) from .. import gridspec as pgridspec from ..config import rc from ..utils import units, edges from ..internals import ic # noqa: F401 -from ..internals import defaults, warnings, _not_none +from ..internals import rcsetup, warnings, _not_none __all__ = ['Axes'] @@ -362,7 +363,10 @@ def _loc_translate(self, loc, mode=None, allow_manual=True): try: loc = loc_translate[loc] except KeyError: - raise KeyError(f'Invalid {mode} location {loc!r}.') + raise KeyError( + f'Invalid {mode} location {loc!r}. Options are: ' + + ', '.join(map(repr, loc_translate)) + '.' + ) elif ( allow_manual and mode == 'legend' @@ -611,7 +615,7 @@ def _parse_format(rc_kw=None, rc_mode=None, **kwargs): rc_kw = rc_kw or {} rc_mode = _not_none(rc_mode, 2) for key, value in kwargs.items(): - key_fixed = defaults._rc_nodots.get(key, None) + key_fixed = rcsetup._rc_nodots.get(key, None) if key_fixed is None: kw[key] = value else: @@ -1639,13 +1643,13 @@ def number(self, num): text = _text_wrapper( maxes.Axes.text ) - plot = _plot_wrapper(_standardize_1d(_add_errorbars(_cycle_changer( + plot = _plot_wrapper(_standardize_1d(_indicate_error(_cycle_changer( maxes.Axes.plot )))) - scatter = _scatter_wrapper(_standardize_1d(_add_errorbars(_cycle_changer( + scatter = _scatter_wrapper(_standardize_1d(_indicate_error(_cycle_changer( maxes.Axes.scatter )))) - bar = _bar_wrapper(_standardize_1d(_add_errorbars(_cycle_changer( + bar = _bar_wrapper(_standardize_1d(_indicate_error(_cycle_changer( maxes.Axes.bar )))) barh = _barh_wrapper( @@ -1657,7 +1661,7 @@ def number(self, num): boxplot = _boxplot_wrapper(_standardize_1d(_cycle_changer( maxes.Axes.boxplot ))) - violinplot = _violinplot_wrapper(_standardize_1d(_add_errorbars( + violinplot = _violinplot_wrapper(_standardize_1d(_indicate_error( _cycle_changer(maxes.Axes.violinplot) ))) fill_between = _fill_between_wrapper(_standardize_1d(_cycle_changer( @@ -1671,13 +1675,21 @@ def number(self, num): pie = _standardize_1d(_cycle_changer( maxes.Axes.pie )) - stem = _standardize_1d(_cycle_changer( - maxes.Axes.stem - )) step = _standardize_1d(_cycle_changer( maxes.Axes.step )) + # Wrapped by standardizer + stem = _standardize_1d(_stem_wrapper( + maxes.Axes.stem + )) + hlines = _standardize_1d(_hlines_wrapper( + maxes.Axes.hlines + )) + vlines = _standardize_1d(_vlines_wrapper( + maxes.Axes.vlines + )) + # Wrapped by cmap wrapper and standardized # Also support redirecting to Basemap methods hexbin = _standardize_1d(_cmap_changer( @@ -1695,6 +1707,9 @@ def number(self, num): pcolormesh = _standardize_2d(_cmap_changer( maxes.Axes.pcolormesh )) + pcolorfast = _standardize_2d(_cmap_changer( + maxes.Axes.pcolorfast # WARNING: not available in cartopy and basemap + )) quiver = _standardize_2d(_cmap_changer( maxes.Axes.quiver )) diff --git a/proplot/axes/cartesian.py b/proplot/axes/cartesian.py index be29d2332..ded51df39 100644 --- a/proplot/axes/cartesian.py +++ b/proplot/axes/cartesian.py @@ -12,7 +12,7 @@ from ..config import rc from ..utils import units from ..internals import ic # noqa: F401 -from ..internals import defaults, docstring, warnings, _not_none +from ..internals import rcsetup, docstring, warnings, _not_none __all__ = ['CartesianAxes'] @@ -149,7 +149,7 @@ def _parse_alt(x, kwargs): # while is more elegant and consistent with e.g. colorbar() syntax # but latter is more consistent and easier to use when refactoring. kw_out[key] = value - elif key in defaults._rc_nodots: + elif key in rcsetup._rc_nodots: kw_out[key] = value else: kw_bad[key] = value diff --git a/proplot/axes/geo.py b/proplot/axes/geo.py index 7e7826d5e..e347fc90a 100644 --- a/proplot/axes/geo.py +++ b/proplot/axes/geo.py @@ -9,12 +9,11 @@ import matplotlib.ticker as mticker from . import base from .plot import ( - _add_errorbars, _norecurse, _redirect, - _plot_wrapper, _scatter_wrapper, - _fill_between_wrapper, _fill_betweenx_wrapper, + _basemap_norecurse, _basemap_redirect, _cmap_changer, _cycle_changer, _default_crs, _default_latlon, _default_transform, - _cmap_changer, _cycle_changer, - _standardize_1d, _standardize_2d, + _fill_between_wrapper, _fill_betweenx_wrapper, + _indicate_error, _plot_wrapper, + _scatter_wrapper, _standardize_1d, _standardize_2d, _text_wrapper, ) from .. import crs as pcrs @@ -770,10 +769,10 @@ def projection(self, map_projection): GeoAxesCartopy.text ) plot = _default_transform(_plot_wrapper(_standardize_1d( - _add_errorbars(_cycle_changer(GeoAxesCartopy.plot)) + _indicate_error(_cycle_changer(GeoAxesCartopy.plot)) ))) scatter = _default_transform(_scatter_wrapper(_standardize_1d( - _add_errorbars(_cycle_changer(GeoAxesCartopy.scatter)) + _indicate_error(_cycle_changer(GeoAxesCartopy.scatter)) ))) fill_between = _fill_between_wrapper(_standardize_1d(_cycle_changer( GeoAxesCartopy.fill_between @@ -1078,36 +1077,36 @@ def projection(self, map_projection): self._map_projection = map_projection # Wrapped methods - plot = _norecurse(_default_latlon(_plot_wrapper(_standardize_1d( - _add_errorbars(_cycle_changer(_redirect(maxes.Axes.plot))) + plot = _basemap_norecurse(_default_latlon(_plot_wrapper(_standardize_1d( + _indicate_error(_cycle_changer(_basemap_redirect(maxes.Axes.plot))) )))) - scatter = _norecurse(_default_latlon(_scatter_wrapper(_standardize_1d( - _add_errorbars(_cycle_changer(_redirect(maxes.Axes.scatter))) + scatter = _basemap_norecurse(_default_latlon(_scatter_wrapper(_standardize_1d( + _indicate_error(_cycle_changer(_basemap_redirect(maxes.Axes.scatter))) )))) - contour = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.contour) + contour = _basemap_norecurse(_default_latlon(_standardize_2d(_cmap_changer( + _basemap_redirect(maxes.Axes.contour) )))) - contourf = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.contourf) + contourf = _basemap_norecurse(_default_latlon(_standardize_2d(_cmap_changer( + _basemap_redirect(maxes.Axes.contourf) )))) - pcolor = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.pcolor) + pcolor = _basemap_norecurse(_default_latlon(_standardize_2d(_cmap_changer( + _basemap_redirect(maxes.Axes.pcolor) )))) - pcolormesh = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.pcolormesh) + pcolormesh = _basemap_norecurse(_default_latlon(_standardize_2d(_cmap_changer( + _basemap_redirect(maxes.Axes.pcolormesh) )))) - quiver = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.quiver) + quiver = _basemap_norecurse(_default_latlon(_standardize_2d(_cmap_changer( + _basemap_redirect(maxes.Axes.quiver) )))) - streamplot = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.streamplot) + streamplot = _basemap_norecurse(_default_latlon(_standardize_2d(_cmap_changer( + _basemap_redirect(maxes.Axes.streamplot) )))) - barbs = _norecurse(_default_latlon(_standardize_2d(_cmap_changer( - _redirect(maxes.Axes.barbs) + barbs = _basemap_norecurse(_default_latlon(_standardize_2d(_cmap_changer( + _basemap_redirect(maxes.Axes.barbs) )))) - hexbin = _norecurse(_standardize_1d(_cmap_changer( - _redirect(maxes.Axes.hexbin) + hexbin = _basemap_norecurse(_standardize_1d(_cmap_changer( + _basemap_redirect(maxes.Axes.hexbin) ))) - imshow = _norecurse(_cmap_changer( - _redirect(maxes.Axes.imshow) + imshow = _basemap_norecurse(_cmap_changer( + _basemap_redirect(maxes.Axes.imshow) )) diff --git a/proplot/axes/plot.py b/proplot/axes/plot.py index 7d7f9d3c3..7c606220c 100644 --- a/proplot/axes/plot.py +++ b/proplot/axes/plot.py @@ -26,14 +26,14 @@ from ..utils import edges, edges2d, units, to_xyz, to_rgb from ..config import rc from ..internals import ic # noqa: F401 -from ..internals import docstring, warnings, _version, _version_cartopy, _not_none +from ..internals import docstring, warnings, _version, _version_cartopy +from ..internals import _not_none, _set_state try: from cartopy.crs import PlateCarree except ModuleNotFoundError: PlateCarree = object __all__ = [ - 'add_errorbars', 'bar_wrapper', 'barh_wrapper', 'boxplot_wrapper', @@ -46,6 +46,7 @@ 'fill_between_wrapper', 'fill_betweenx_wrapper', 'hist_wrapper', + 'indicate_error', 'legend_wrapper', 'scatter_wrapper', 'standardize_1d', @@ -54,7 +55,6 @@ 'violinplot_wrapper', ] - docstring.snippets['cmap_changer.params'] = """ cmap : colormap spec, optional The colormap specifer, passed to the `~proplot.constructor.Colormap` @@ -121,11 +121,15 @@ `negcolor`. For example, to shade positive values red and negative values blue, use ``ax.fill_between{suffix}({x}, {y}, negpos=True)``. negcolor, poscolor : color-spec, optional - Colors to use for the negative and positive values. Ignored if `negpos` - is ``False``. + Colors to use for the negative and positive shaded regions. Ignored if `negpos` + is ``False``. Defaults are :rc:`negcolor` and :rc:`poscolor`. where : ndarray, optional Boolean ndarray mask for points you want to shade. See `this example \ `__. +lw, linewidth : float, optional + The edge width for the area patches. +edgecolor : color-spec, optional + The edge color for the area patches. Other parameters ---------------- @@ -160,10 +164,16 @@ `~matplotlib.axes.Axes.boxplot` and `~matplotlib.axes.Axes.violinplot`. stacked : bool, optional Whether to stack columns of input data, or plot the bars side-by-side. -edgecolor : color-spec, optional - The edge color for the bar patches. +negpos : bool, optional + Whether to shade bars greater than zero with `poscolor` and bars less + than zero with `negcolor`. +negcolor, poscolor : color-spec, optional + Colors to use for the negative and positive bars. Ignored if `negpos` + is ``False``. Defaults are :rc:`negcolor` and :rc:`poscolor`. lw, linewidth : float, optional The edge width for the bar patches. +edgecolor : color-spec, optional + The edge color for the bar patches. Other parameters ---------------- @@ -219,6 +229,11 @@ def _load_objects(): 'linewidths': 'linewidth', 'linestyles': 'linestyle', }, + 'pcolorfast': { + 'colors': 'edgecolors', + 'linewidths': 'linewidth', + 'linestyles': 'linestyle', + }, 'tripcolor': { 'colors': 'edgecolors', 'linewidths': 'linewidth', @@ -344,9 +359,8 @@ def default_crs(self, func, *args, crs=None, **kwargs): def _axis_labels_title(data, axis=None, units=True): """ - Get data and label for pandas or xarray objects or their coordinates - along axis `axis`. If `units` is ``True`` also look for units on xarray - data arrays. + Get data and label for pandas or xarray objects or their coordinates along axis + `axis`. If `units` is ``True`` also look for units on xarray data arrays. """ label = '' _load_objects() @@ -371,12 +385,14 @@ def _axis_labels_title(data, axis=None, units=True): # Pandas object with name attribute # if not label and isinstance(data, DataFrame) and data.columns.size == 1: elif isinstance(data, (DataFrame, Series, Index)): - if axis == 0 and isinstance(data, (DataFrame, Series)): + if axis == 0 and isinstance(data, Index): + pass + elif axis == 0 and isinstance(data, (DataFrame, Series)): data = data.index elif axis == 1 and isinstance(data, DataFrame): data = data.columns - elif axis is not None: - data = np.arange(len(data)) # e.g. for Index + elif axis == 1 and isinstance(data, (Series, Index)): + data = np.array([data.name]) # treat series name as the "column" data # DataFrame has no native name attribute but user can add one: # https://github.com/pandas-dev/pandas/issues/447 label = getattr(data, 'name', '') or '' @@ -709,6 +725,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): kw['ylocator'] = mticker.FixedLocator(yi) kw['yformatter'] = mticker.IndexFormatter(y) kw['yminorlocator'] = mticker.NullLocator() + # Handle labels if 'autoformat' is on if self.figure._auto_format: for key, xy in zip(('xlabel', 'ylabel'), (x, y)): @@ -722,6 +739,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): x = xi if yi is not None: y = yi + # Handle figure titles if self.figure._auto_format: _, colorbar_label = _axis_labels_title(Zs[0], units=True) @@ -732,7 +750,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): self.format(**kw) # Enforce edges - if name in ('pcolor', 'pcolormesh'): + if name in ('pcolor', 'pcolormesh', 'pcolorfast'): # Get centers or raise error. If 2d, don't raise error, but don't fix # either, because matplotlib pcolor just trims last column and row. xlen, ylen = x.shape[-1], y.shape[0] @@ -875,45 +893,128 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): return func(self, x, y, *Zs, colorbar_kw=colorbar_kw, **kwargs) -def _errorbar_values(data, idata, bardata=None, barrange=None, barstd=False): +def _get_error_data( + data, y, errdata=None, stds=None, pctiles=False, + stds_default=None, pctiles_default=None, + means_or_medians=True, absolute=False, +): """ Return values that can be passed to the `~matplotlib.axes.Axes.errorbar` `xerr` and `yerr` keyword args. """ - if bardata is not None: - err = np.array(bardata) - if err.ndim == 1: - err = err[:, None] - if err.ndim != 2 or err.shape[0] != 2 \ - or err.shape[1] != idata.shape[-1]: + # Parse arguments + if stds in (1, True): + stds = stds_default + elif stds in (0, False): + stds = None + if pctiles in (1, True): + pctiles = pctiles_default + elif pctiles in (0, False): + pctiles = None + + # Incompatible settings + if stds is not None and pctiles is not None: + warnings._warn_proplot( + 'You passed both a standard deviation range and a percentile range for ' + 'drawing error indicators. Using the former.' + ) + pctiles = None + if not means_or_medians and (stds is not None or pctiles is not None): + raise ValueError( + 'To automatically compute standard deviations or percentiles on columns ' + 'of data you must pass means=True or medians=True.' + ) + if means_or_medians and errdata is not None: + stds = pctiles = None + warnings._warn_proplot( + 'You explicitly provided the error bounds but also requested ' + 'automatically calculating means or medians on data columns. ' + 'It may make more sense to use the "stds" or "pctiles" keyword args ' + 'and have *proplot* calculate the error bounds.' + ) + + # Compute error data in format that can be passed to matplotlib.axes.Axes.errorbar() + # NOTE: Include option to pass symmetric deviation from central points + y = _to_ndarray(y) + data = _to_ndarray(data) + if errdata is not None: + label = 'error range' + err = _to_ndarray(errdata) + if ( + err.ndim not in (1, 2) + or err.shape[-1] != y.shape[-1] + or err.ndim == 2 and err.shape[0] != 2 + ): raise ValueError( - f'bardata must have shape (2, {idata.shape[-1]}), ' - f'but got {err.shape}.' + f'errdata must have shape (2, {y.shape[-1]}), but got {err.shape}.' ) - elif barstd: - err = np.array(idata) + \ - np.std(data, axis=0)[None, :] * np.array(barrange)[:, None] + if err.ndim == 1: + abserr = err + err = np.empty((2, err.size)) + err[:, 0] = y - abserr # translated back to absolute deviations below + err[:, 1] = y + abserr + elif stds is not None: + label = fr'{stds[1]}$\sigma$ range' + err = y + np.std(data, axis=0)[None, :] * np.asarray(stds)[:, None] + elif pctiles is not None: + label = f'{pctiles[0]}-{pctiles[1]} percentile range' + err = np.percentile(data, pctiles, axis=0) else: - err = np.percentile(data, barrange, axis=0) - err = err - np.array(idata) - err[0, :] *= -1 # array now represents error bar sizes - return err + raise ValueError('You must provide error bounds.') + if not absolute: + err = err - y + err[0, :] *= -1 # absolute deviations from central points + + # Return data with default legend entry + return err, label -def add_errorbars( +def _deprecate_add_errorbars(func): + """ + Translate old-style keyword arguments to new-style in way that is too complex + for _rename_kwargs. Use a decorator to avoid call signature pollution. + """ + @functools.wraps(func) + def _wrapper( + *args, + bars=None, boxes=None, barstd=None, boxstd=None, barrange=None, boxrange=None, + **kwargs + ): + for (prefix, b, std, span) in zip( + ('bar', 'box'), (bars, boxes), (barstd, boxstd), (barrange, boxrange), + ): + if b is not None or std is not None or span is not None: + warnings._warn_proplot( + f'Keyword args "{prefix}s", "{prefix}std", and "{prefix}range" ' + 'are deprecated and will be removed in a future version. ' + f'Please use "{prefix}stds" or "{prefix}pctiles" instead.' + ) + if span is None and b: # means 'use the default range' + span = b + if std: + kwargs.setdefault(prefix + 'stds', span) + else: + kwargs.setdefault(prefix + 'pctiles', span) + return func(*args, **kwargs) + return _wrapper + + +@_deprecate_add_errorbars +def indicate_error( self, func, *args, medians=False, means=False, - boxes=None, bars=None, - boxdata=None, bardata=None, - boxstd=False, barstd=False, + boxdata=None, bardata=None, shadedata=None, fadedata=None, + boxstds=None, barstds=None, shadestds=None, fadestds=None, + boxpctiles=None, barpctiles=None, shadepctiles=None, fadepctiles=None, boxmarker=True, boxmarkercolor='white', - boxrange=(25, 75), barrange=(5, 95), boxcolor=None, barcolor=None, - boxlw=None, barlw=None, capsize=None, - boxzorder=3, barzorder=3, + boxcolor=None, barcolor=None, shadecolor=None, fadecolor=None, + shadelabel=None, fadelabel=None, shadealpha=0.4, fadealpha=0.2, + boxlinewidth=None, boxlw=None, barlinewidth=None, barlw=None, capsize=None, + boxzorder=2.5, barzorder=2.5, shadezorder=1.5, fadezorder=1.5, **kwargs ): """ - Adds support for drawing error bars on-the-fly. + Adds support for drawing error bars and error shading on-the-fly. Includes options for interpreting columns of data as *samples*, representing the mean or median of each sample with lines, points, or bars, and drawing error bars representing percentile ranges or standard @@ -928,141 +1029,230 @@ def add_errorbars( ---------- *args The input data. - bars : bool, optional - Toggles *thin* error bars with optional "whiskers" (i.e. caps). Default - is ``True`` when `means` is ``True``, `medians` is ``True``, or - `bardata` is not ``None``. - boxes : bool, optional - Toggles *thick* boxplot-like error bars with a marker inside - representing the mean or median. Default is ``True`` when `means` is - ``True``, `medians` is ``True``, or `boxdata` is not ``None``. means : bool, optional Whether to plot the means of each column in the input data. medians : bool, optional Whether to plot the medians of each column in the input data. - bardata, boxdata : 2xN ndarray, optional - Arrays that manually specify the thin and thick error bar coordinates. - The first row contains lower bounds, and the second row contains - upper bounds. Columns correspond to points in the dataset. - barstd, boxstd : bool, optional - Whether `barrange` and `boxrange` refer to multiples of the standard - deviation, or percentile ranges. Default is ``False``. - barrange : (float, float), optional - Percentile ranges or standard deviation multiples for drawing thin - error bars. The defaults are ``(-3, 3)`` (i.e. +/-3 standard - deviations) when `barstd` is ``True``, and ``(0, 100)`` (i.e. the full - data range) when `barstd` is ``False``. - boxrange : (float, float), optional - Percentile ranges or standard deviation multiples for drawing thick - error bars. The defaults are ``(-1, 1)`` (i.e. +/-1 standard deviation) - when `boxstd` is ``True``, and ``(25, 75)`` (i.e. the interquartile - range) when `boxstd` is ``False``. - barcolor, boxcolor : color-spec, optional - Colors for the thick and thin error bars. Default is ``'k'``. - barlw, boxlw : float, optional - Line widths for the thin and thick error bars, in points. Default - `barlw` is ``0.7`` and default `boxlw` is ``4 * barlw``. + barstds : (float, float) or bool, optional + Standard deviation multiples for *thin error bars* with optional whiskers + (i.e. caps). If ``True``, the default standard deviation multiples ``(-3, 3)`` + are used. This argument is only valid if `means` or `medians` is ``True``. + barpctiles : (float, float) or bool, optional + As with `barstds`, but instead using *percentiles* for the error bars. + The percentiles are calculated with `numpy.percentile`. If ``True``, the + default percentiles ``(0, 100)`` are used. + bardata : 2 x N array or 1D array, optional + If shape is 2 x N these are the lower and upper bounds for the thin error bars. + If array is 1D these are the absolute, symmetric deviations from the central + points. This should be used if `means` and `medians` are both ``False`` (i.e. + you did not provide dataset columns from which statistical properties can be + calculated automatically). + boxstds, boxpctiles, boxdata : optional + As with `barstds`, `barpctiles`, and `bardata`, but for *thicker error bars* + representing a smaller interval than thick error bars. If `boxstds` is + ``True``, the default standard deviation multiples ``(-1, 1)`` are used. + If `boxpctiles` is ``True``, the default percentile multiples ``(25, 75)`` + (i.e. the interquartile range) are used. When boxes and bars are combined, this + has the effect of drawing miniature box-and-whisker plots. + shadestds, shadepctiles, shadedata : optional + As with `barstds`, `barpctiles`, and `bardata`, but using *shading* to indicate + the error range. If `shadestds` is ``True``, the default standard deviation + multiples ``(-2, 2)`` are used. If `shadepctiles` is ``True``, the default + percentile multiples ``(5, 95)`` are used. Shading is generally useful for + `~matplotlib.axes.Axes.plot` plots and not `~matplotlib.axes.Axes.bar` plots. + fadestds, fadepctiles, fadedata : optional + As with `shadestds`, `shadepctiles`, and `shadedata`, but for an additional, + more faded, *secondary* shaded region. + barcolor, boxcolor, shadecolor, fadecolor : color-spec, optional + Colors for the different error indicators. For error bars, the default is + ``'k'``. For shading, the default behavior is to inherit color from the + primary `~matplotlib.artist.Artist`. + shadelabel, fadelabel : str, optional + Labels for the shaded regions, to be used in legends. There is no option + to change the "error bar" labels because error bars should not appear in a + legend -- they should instead be described in the figure caption. + barlinewidth, boxlinewidth, barlw, boxlw : float, optional + Line widths for the thin and thick error bars, in points. The defaults + are ``barlw=0.8`` and ``boxlw=4 * barlw``. boxmarker : bool, optional - Whether to draw a small marker in the middle of the box denoting - the mean or median position. Ignored if `boxes` is ``False``. - Default is ``True``. + Whether to draw a small marker in the middle of the box denoting the mean or + median position. Ignored if `boxes` is ``False``. Default is ``True``. boxmarkercolor : color-spec, optional Color for the `boxmarker` marker. Default is ``'w'``. capsize : float, optional - The cap size for thin error bars, in points. - barzorder, boxzorder : float, optional - The "zorder" for the thin and thick error bars. - lw, linewidth : float, optional - If passed, this is used for the default `barlw`. - edgecolor : float, optional - If passed, this is used for the default `barcolor` and `boxcolor`. + The cap size for thin error bars in points. + barzorder, boxzorder, shadezorder, fadezorder : float, optional + The "zorder" for the thin error bars, thick error bars, and shading. + + Returns + ------- + h, errobj1, errobj2, ... + The original plotting handle and the error bar objects. """ name = func.__name__ - x, y, *args = args - # Sensible defaults - if boxdata is not None: - bars = _not_none(bars, True) - if bardata is not None: - boxes = _not_none(boxes, True) - if boxdata is not None or bardata is not None: - # e.g. if boxdata passed but bardata not passed, use bars=False - bars = _not_none(bars, False) - boxes = _not_none(boxes, False) + x, data, *args = args + x = _to_arraylike(x) + data = _to_arraylike(data) # Get means or medians for plotting - iy = y - if (means or medians): - bars = _not_none(bars, True) - boxes = _not_none(boxes, True) - if y.ndim != 2: + # NOTE: We can *only* use pctiles and stds if one of these was true + # TODO: Add support for 3D arrays. + y = data + bars = any(_ is not None for _ in (barstds, barpctiles, bardata)) + boxes = any(_ is not None for _ in (boxstds, boxpctiles, boxdata)) + shading = any(_ is not None for _ in (shadestds, shadepctiles, shadedata)) + fading = any(_ is not None for _ in (fadestds, fadepctiles, fadedata)) + if means or medians: + # Take means or medians while preserving metadata for legends + # NOTE: Permit 3d array with error dimension coming first + if not (bars or boxes or shading or fading): + bars = boxes = True # toggle these on + barstds = boxstds = True # error bars and boxes with default stdev ranges + if data.ndim != 2: raise ValueError( - f'Need 2d data array for means=True or medians=True, ' - f'got {y.ndim}d array.' + f'Need 2D data array for means=True or medians=True, ' + f'got {data.ndim}D array.' ) + keep = {} + if DataArray is not ndarray and isinstance(data, DataArray): + keep['keep_attrs'] = True if means: - iy = np.mean(y, axis=0) + y = data.mean(axis=0, **keep) elif medians: - iy = np.percentile(y, 50, axis=0) - - # Call function, accounting for different signatures of plot and violinplot - get = kwargs.pop if name == 'violinplot' else kwargs.get - lw = _not_none(get('lw', None), get('linewidth', None), 0.7) - get = kwargs.pop if name != 'bar' else kwargs.get - edgecolor = _not_none(get('edgecolor', None), 'k') - if name == 'violinplot': - xy = (x, y) # full data - else: - xy = (x, iy) # just the stats - obj = func(self, *xy, *args, **kwargs) - if not boxes and not bars: - return obj - - # Account for horizontal bar plots - if 'vert' in kwargs: - orientation = 'vertical' if kwargs['vert'] else 'horizontal' - else: - orientation = kwargs.get('orientation', 'vertical') - if orientation == 'horizontal': - axis = 'x' # xerr - xy = (iy, x) - else: - axis = 'y' # yerr - xy = (x, iy) - - # Defaults settings - barlw = _not_none(barlw, lw) - boxlw = _not_none(boxlw, 4 * barlw) - capsize = _not_none(capsize, 3) + if hasattr(data, 'quantile'): # DataFrame and DataArray + y = data.quantile(0.5, axis=0, **keep) + if Series is not ndarray and isinstance(y, Series): + y.name = '' # do not set name to quantile number + else: + y = np.percentile(data, 50, axis=0, **keep) + if getattr(data, 'name', '') and not getattr(y, 'name', ''): + y.name = data.name # copy DataFrame name to Series name + + # Infer width of error elements + # NOTE: violinplot_wrapper passes some invalid keyword args with expectation + # that indicate_error wrapper pops them and uses them for error bars. + lw = None + if name == 'bar': + lw = _not_none(kwargs.get('linewidth', None), kwargs.get('lw', None)) + elif name == 'violinplot': + lw = _not_none(kwargs.pop('linewidth', None), kwargs.pop('lw', None)) + lw = _not_none(lw, 0.8) + barlw = _not_none(barlinewidth=barlinewidth, barlw=barlw, default=lw) + boxlw = _not_none(boxlinewidth=boxlinewidth, boxlw=boxlw, default=4 * barlw) + capsize = _not_none(capsize, 3.0) + + # Infer color for error bars + edgecolor = None + if name == 'bar': + edgecolor = kwargs.get('edgecolor', None) + elif name == 'violinplot': + edgecolor = kwargs.pop('edgecolor', None) + edgecolor = _not_none(edgecolor, 'k') barcolor = _not_none(barcolor, edgecolor) - boxcolor = _not_none(boxcolor, edgecolor) + boxcolor = _not_none(boxcolor, barcolor) + + # Infer color for shading + shadecolor_infer = shadecolor is None + shadecolor = _not_none( + shadecolor, kwargs.get('color', None), kwargs.get('facecolor', None), edgecolor + ) + fadecolor_infer = fadecolor is None + fadecolor = _not_none(fadecolor, shadecolor) + + # Draw dark and light shading + vert = kwargs.get('vert', kwargs.get('orientation', 'vertical') == 'vertical') + axis = 'y' if vert else 'x' # yerr + errargs = (x, y) if vert else (y, x) + errobjs = [] + means_or_medians = means or medians + if fading: + err, label = _get_error_data( + data, y, fadedata, fadestds, fadepctiles, + stds_default=(-3, 3), pctiles_default=(0, 100), absolute=True, + means_or_medians=means_or_medians, + ) + errfunc = self.fill_between if vert else self.fill_betweenx + errobj = errfunc( + x, *err, linewidth=0, color=fadecolor, + alpha=fadealpha, zorder=fadezorder, + ) + errobj.set_label(_not_none(fadelabel, label)) + errobjs.append(errobj) + if shading: + err, label = _get_error_data( + data, y, shadedata, shadestds, shadepctiles, + stds_default=(-2, 2), pctiles_default=(10, 90), absolute=True, + means_or_medians=means_or_medians, + ) + errfunc = self.fill_between if vert else self.fill_betweenx + errobj = errfunc( + x, *err, linewidth=0, color=shadecolor, + alpha=shadealpha, zorder=shadezorder, + ) + errobj.set_label(_not_none(shadelabel, label)) + errobjs.append(errobj) - # Draw boxes and bars + # Draw thin error bars and thick error boxes if boxes: - default = (-1, 1) if barstd else (25, 75) - boxrange = _not_none(boxrange, default) - err = _errorbar_values(y, iy, boxdata, boxrange, boxstd) + err, label = _get_error_data( + data, y, boxdata, boxstds, boxpctiles, + stds_default=(-1, 1), pctiles_default=(25, 75), + means_or_medians=means_or_medians, + ) if boxmarker: - self.scatter( - *xy, marker='o', color=boxmarkercolor, - s=boxlw, zorder=5 - ) - self.errorbar(*xy, **{ - axis + 'err': err, 'capsize': 0, 'zorder': boxzorder, - 'color': boxcolor, 'linestyle': 'none', 'linewidth': boxlw - }) + self.scatter(*errargs, s=boxlw, marker='o', color=boxmarkercolor, zorder=5) + errkw = {axis + 'err': err} + errobj = self.errorbar( + *errargs, color=boxcolor, linewidth=boxlw, linestyle='none', + capsize=0, zorder=boxzorder, **errkw, + ) + errobjs.append(errobj) if bars: # now impossible to make thin bar width different from cap width! - default = (-3, 3) if barstd else (0, 100) - barrange = _not_none(barrange, default) - err = _errorbar_values(y, iy, bardata, barrange, barstd) - self.errorbar(*xy, **{ - axis + 'err': err, 'capsize': capsize, 'zorder': barzorder, - 'color': barcolor, 'linewidth': barlw, 'linestyle': 'none', - 'markeredgecolor': barcolor, 'markeredgewidth': barlw - }) - return obj + err, label = _get_error_data( + data, y, bardata, barstds, barpctiles, + stds_default=(-3, 3), pctiles_default=(0, 100), + means_or_medians=means_or_medians, + ) + errkw = {axis + 'err': err} + errobj = self.errorbar( + *errargs, color=barcolor, linewidth=barlw, linestyle='none', + markeredgecolor=barcolor, markeredgewidth=barlw, + capsize=capsize, zorder=barzorder, **errkw + ) + errobjs.append(errobj) + # Call main function + # NOTE: Provide error objects for inclusion in legend, but *only* provide + # the shading. Never want legend entries for error bars. + xy = (x, data) if name == 'violinplot' else (x, y) + kwargs.setdefault('errobjs', errobjs[:int(shading + fading)]) + result = obj = func(self, *xy, *args, **kwargs) + + # Apply inferrred colors to objects + if type(result) in (tuple, list): # avoid BarContainer + obj = result[0] + i = 0 + for b, infer in zip((fading, shading), (fadecolor_infer, shadecolor_infer)): + if b and infer: + color = getattr( + obj, 'get_facecolor', getattr(obj, 'get_color', lambda: None) + )() + if color is not None: + errobjs[i].set_facecolor(color) + i += 1 + + # Return objects + # NOTE: This should not affect internal + if errobjs: + if type(result) in (tuple, list): # e.g. result of plot + return (*result, *errobjs) + else: + return (result, *errobjs) + else: + return result -def _parametric_wrapper_temporary(self, func, *args, interp=0, **kwargs): + +def _parametric_wrapper_private(self, func, *args, interp=0, **kwargs): """ Calls `~proplot.axes.Axes.parametric` and optionally interpolates values before they get passed to `cmap_changer` and the colormap boundaries are drawn. @@ -1112,7 +1302,7 @@ def _parametric_wrapper_temporary(self, func, *args, interp=0, **kwargs): return func(self, x, y, values=values, **kwargs) -def _plot_wrapper_deprecated( +def _plot_wrapper_private( self, func, *args, cmap=None, values=None, **kwargs ): """ @@ -1121,16 +1311,26 @@ def _plot_wrapper_deprecated( """ if len(args) > 3: # e.g. with fmt string raise ValueError(f'Expected 1-3 positional args, got {len(args)}.') - if cmap is None: - return func(self, *args, values=values, **kwargs) - else: + if cmap is not None: warnings._warn_proplot( - 'Drawing "parametric" plots with ax.plot(..., cmap=cmap, values=' - 'values) is deprecated and will be removed in a future version. ' - 'Please use ax.parametric(..., cmap=cmap, values=values) instead.' + 'Drawing "parametric" plots with ax.plot(x, y, values=values, cmap=cmap) ' + 'is deprecated and will be removed in a future version. Please use ' + 'ax.parametric(x, y, values, cmap=cmap) instead.' ) return self.parametric(*args, cmap=cmap, values=values, **kwargs) + # Draw lines + # Add sticky edges? No because there is no way to check whether "dependent variable" + # is x or y axis like with area/areax and bar/barh. Better to always have margin. + result = func(self, *args, values=values, **kwargs) + # for objs in result: + # if not isinstance(objs, tuple): + # objs = (objs,) + # for obj in objs: + # xdata = obj.get_xdata() + # obj.sticky_edges.x.extend((np.min(xdata), np.max(xdata))) + return result + @docstring.add_snippets def scatter_wrapper( @@ -1253,9 +1453,63 @@ def scatter_wrapper( return obj +def _stem_wrapper_private(self, func, *args, **kwargs): + """ + Make `use_line_collection` the default to suppress annoying warning message. + """ + kwargs.setdefault('use_line_collection', True) + kwargs.setdefault('linefmt', 'C0-') + kwargs.setdefault('basefmt', kwargs['linefmt']) # use *same* for base + try: + return func(self, *args, **kwargs) + except TypeError: + kwargs.pop('use_line_collection') # old version + return func(self, *args, **kwargs) + + +def _parse_lines_args(x, args, kwargs): + """ + Parse lines arguments. Support automatic *x* coordinates and default + "minima" at zero. + """ + args = list(args) + if x in kwargs: + args.insert(0, kwargs.pop(x)) + y = 'y' if x == 'x' else 'x' + for suffix in ('min', 'max'): + key = y + suffix + if key in kwargs: + args.append(kwargs.pop(key)) + if len(args) == 1: + x = np.arange(len(np.atleast_1d(args[0]))) + args.insert(0, x) + if len(args) == 2: + args.insert(1, 0.0) + elif len(args) != 3: + raise TypeError('lines() requires 1 to 3 positional arguments.') + return args, kwargs + + +def _hlines_wrapper_private(self, func, *args, **kwargs): + """ + Parse hlines arguments. + """ + args, kwargs = _parse_lines_args('x', args, kwargs) + return func(self, *args, **kwargs) + + +def _vlines_wrapper_private(self, func, *args, **kwargs): + """ + Parse vlines arguments. + """ + args, kwargs = _parse_lines_args('y', args, kwargs) + return func(self, *args, **kwargs) + + def _fill_between_apply( self, func, *args, - negcolor='blue', poscolor='red', negpos=False, + negcolor=None, poscolor=None, negpos=False, + lw=None, linewidth=None, **kwargs ): """ @@ -1268,15 +1522,16 @@ def _fill_between_apply( # make the default fill_between(x, y1=0, y2). x = 'y' if 'x' in func.__name__ else 'x' y = 'x' if x == 'y' else 'y' - if x in kwargs: - args = (kwargs.pop(x), *args) - for y in (y + '1', y + '2'): - if y in kwargs: - args = (*args, kwargs.pop(y)) + args = list(args) + if x in kwargs: # keyword 'x' + args.insert(0, kwargs.pop(x)) if len(args) == 1: - args = (np.arange(len(args[0])), *args) + args.insert(0, np.arange(len(args[0]))) + for yi in (y + '1', y + '2'): + if yi in kwargs: # keyword 'y' + args.append(kwargs.pop(yi)) if len(args) == 2: - args = (*args, 0) + args.append(0) elif len(args) == 3: if kwargs.get('stacked', False): warnings._warn_proplot( @@ -1286,31 +1541,55 @@ def _fill_between_apply( else: raise ValueError(f'Expected 2-3 positional args, got {len(args)}.') + # Modify default properties + # Set default edge width for patches to zero + kwargs['linewidth'] = _not_none(lw=lw, linewidth=linewidth, default=0) + # Draw patches - x, y1, y2 = args + xv, y1, y2 = args if negpos: - # Get zero points - objs = [] - kwargs.setdefault('interpolate', True) + # Plot negative and positive patches + name = func.__name__ + message = name + ' argument {}={!r} is incompatible with negpos=True. Ignoring.' where = kwargs.pop('where', None) if where is not None: - warnings._warn_proplot( - f'{func.__name__} cannot have "where" argument ' - f'with negpos=True. Ignoring where={where!r}.' - ) - - # Plot negative and positive patches - for i in range(2): - kw = kwargs.copy() - kw.setdefault('color', negcolor if i == 0 else poscolor) - where = y1 < y2 if i == 0 else y1 >= y2 - obj = func(self, x, y1, y2, where=where, **kw) - objs.append(obj) - return tuple(objs) + warnings._warn_proplot(message.format('where', where)) + stacked = kwargs.pop('stacked', None) + if stacked: + warnings._warn_proplot(message.format('stacked', stacked)) + kwargs.setdefault('interpolate', True) + if np.asarray(y1).ndim > 1 or np.asarray(y2).ndim > 2: + raise ValueError(f'{name} arguments with negpos=True must be 1D.') + where1 = y1 < y2 + where2 = y1 >= y2 + negcolor = _not_none(negcolor, rc['negcolor']) + poscolor = _not_none(poscolor, rc['poscolor']) + obj1 = func(self, xv, y1, y2, where=where1, **{'color': negcolor, **kwargs}) + obj2 = func(self, xv, y1, y2, where=where2, **{'color': poscolor, **kwargs}) + result = objs = (obj1, obj2) # may be tuple of tuples due to cycle_changer else: # Plot basic patches - return func(self, x, y1, y2, **kwargs) + result = func(self, xv, y1, y2, **kwargs) + objs = (result,) + + # Add sticky edges in x-direction, and sticky edges in y-direction *only* + # if one of the y limits is scalar. This should satisfy most users. + # NOTE: Current private API self._request_autoscale_view is very recent but + xsides = (np.min(xv), np.max(xv)) + ysides = [] + if np.isscalar(y1): + ysides.append(np.asarray(y1).item()) + if np.isscalar(y2): + ysides.append(np.asarray(y2).item()) + for iobjs in objs: + if not isinstance(iobjs, tuple): + iobjs = (iobjs,) + for obj in iobjs: + getattr(obj.sticky_edges, x).extend(xsides) + getattr(obj.sticky_edges, y).extend(ysides) + + return result @docstring.add_snippets @@ -1345,7 +1624,8 @@ def hist_wrapper(self, func, x, bins=None, **kwargs): def bar_wrapper( self, func, x=None, height=None, width=0.8, bottom=None, *, left=None, vert=None, orientation='vertical', stacked=False, - lw=None, linewidth=0.7, edgecolor='k', + lw=None, linewidth=0.7, edgecolor='black', + negpos=False, negcolor=None, poscolor=None, **kwargs ): """ @@ -1364,32 +1644,51 @@ def bar_wrapper( # TODO: Stacked feature is implemented in `cycle_changer`, but makes more # sense do document here; figure out way to move it here? if left is not None: - warnings._warn_proplot( - 'The "left" keyword with bar() is deprecated. Use "x" instead.' - ) + warnings._warn_proplot('bar() keyword "left" is deprecated. Use "x" instead.') x = left if x is None and height is None: - raise ValueError( - 'bar() requires at least 1 positional argument, got 0.' - ) + raise ValueError('bar() requires at least 1 positional argument, got 0.') elif height is None: x, height = None, x + args = (x, height) + linewidth = _not_none(lw=lw, linewidth=linewidth) + kwargs.update({ + 'width': width, 'bottom': bottom, 'stacked': stacked, + 'orientation': orientation, 'linewidth': linewidth, 'edgecolor': edgecolor, + }) # Call func - # TODO: This *must* also be wrapped by cycle_changer, which ultimately + # NOTE: This *must* also be wrapped by cycle_changer, which ultimately # permutes back the x/bottom args for horizontal bars! Need to clean up. - lw = _not_none(lw=lw, linewidth=linewidth) - return func( - self, x, height, width=width, bottom=bottom, - linewidth=lw, edgecolor=edgecolor, - stacked=stacked, orientation=orientation, - **kwargs - ) + if negpos: + # Draw negative and positive bars + # NOTE: cycle_changer makes bar widths *relative* to step size between + # x coordinates to cannot just omit data. Instead make some height nan. + message = 'bar() argument {}={!r} is incompatible with negpos=True. Ignoring.' + stacked = kwargs.pop('stacked', None) + if stacked: + warnings._warn_proplot(message.format('stacked', stacked)) + height = np.asarray(height) + if height.ndim > 1: + raise ValueError('bar() heights with negpos=True must be 1D.') + height1 = height.copy().astype(np.float64) + height1[height >= 0] = np.nan + height2 = height.copy().astype(np.float64) + height2[height < 0] = np.nan + negcolor = _not_none(negcolor, rc['negcolor']) + poscolor = _not_none(poscolor, rc['poscolor']) + obj1 = func(self, x, height1, **{'color': negcolor, **kwargs}) + obj2 = func(self, x, height2, **{'color': poscolor, **kwargs}) + result = (obj1, obj2) + else: + # Draw simple bars + result = func(self, *args, **kwargs) + return result -@docstring.add_snippets # noqa: U100 +@docstring.add_snippets def barh_wrapper( - self, func, y=None, width=None, height=0.8, left=None, **kwargs + self, func, y=None, right=None, width=None, left=None, height=None, **kwargs ): """ %(axes.barh)s @@ -1397,22 +1696,23 @@ def barh_wrapper( # Converts y-->bottom, left-->x, width-->height, height-->width. # Convert back to (x, bottom, width, height) so we can pass stuff # through cycle_changer. + # NOTE: ProPlot calls second positional argument 'right' so that 'width' + # means the width of *bars*. # NOTE: You *must* do juggling of barh keyword order --> bar keyword order # --> barh keyword order, because horizontal hist passes arguments to bar # directly and will not use a 'barh' method with overridden argument order! func # avoid U100 error + height = _not_none(height=height, width=width, default=0.8) kwargs.setdefault('orientation', 'horizontal') if y is None and width is None: - raise ValueError( - 'barh() requires at least 1 positional argument, got 0.' - ) - return self.bar(x=left, height=height, width=width, bottom=y, **kwargs) + raise ValueError('barh() requires at least 1 positional argument, got 0.') + return self.bar(x=left, width=right, height=height, bottom=y, **kwargs) def boxplot_wrapper( self, func, *args, color='k', fill=True, fillcolor=None, fillalpha=0.7, - lw=None, linewidth=0.7, orientation=None, + lw=None, linewidth=None, orientation=None, marker=None, markersize=None, boxcolor=None, boxlw=None, capcolor=None, caplw=None, @@ -1479,7 +1779,7 @@ def boxplot_wrapper( # Modify results # TODO: Pass props keyword args instead? Maybe does not matter. - lw = _not_none(lw=lw, linewidth=linewidth) + lw = _not_none(lw=lw, linewidth=linewidth, default=0.8) if fillcolor is None: cycler = next(self._get_lines.prop_cycler) fillcolor = cycler.get('color', None) @@ -1518,7 +1818,7 @@ def boxplot_wrapper( def violinplot_wrapper( self, func, *args, - lw=None, linewidth=0.7, fillcolor=None, edgecolor='k', + lw=None, linewidth=None, fillcolor=None, edgecolor='black', fillalpha=0.7, orientation=None, **kwargs ): @@ -1541,7 +1841,7 @@ def violinplot_wrapper( lw, linewidth : float, optional The linewidth of the line objects. Default is ``1``. edgecolor : color-spec, optional - The edge color for the violin patches. Default is ``'k'``. + The edge color for the violin patches. Default is ``'black'``. fillcolor : color-spec, optional The violin plot fill color. Default is the next color cycler color. fillalpha : float, optional @@ -1570,7 +1870,7 @@ def violinplot_wrapper( ) # Sanitize input - lw = _not_none(lw=lw, linewidth=linewidth) + lw = _not_none(lw=lw, linewidth=linewidth, default=0.8) if kwargs.pop('showextrema', None): warnings._warn_proplot('Ignoring showextrema=True.') if 'showmeans' in kwargs: @@ -1578,22 +1878,23 @@ def violinplot_wrapper( if 'showmedians' in kwargs: kwargs.setdefault('medians', kwargs.pop('showmedians')) kwargs.setdefault('capsize', 0) - obj = func( + result = obj = func( self, *args, - showmeans=False, showmedians=False, showextrema=False, - edgecolor=edgecolor, lw=lw, **kwargs + showmeans=False, showmedians=False, showextrema=False, lw=lw, **kwargs ) if not args: - return obj + return result # Modify body settings + if isinstance(result, (list, tuple)): + obj = result[0] for artist in obj['bodies']: artist.set_alpha(fillalpha) artist.set_edgecolor(edgecolor) artist.set_linewidths(lw) if fillcolor is not None: artist.set_facecolor(fillcolor) - return obj + return result def _get_transform(self, transform): @@ -1739,6 +2040,7 @@ def cycle_changer( label=None, labels=None, values=None, legend=None, legend_kw=None, colorbar=None, colorbar_kw=None, + errobjs=None, **kwargs ): """ @@ -1787,6 +2089,9 @@ def cycle_changer( colorbar_kw : dict-like, optional Ignored if `colorbar` is ``None``. Extra keyword args for our call to `~proplot.axes.Axes.colorbar`. + errobjs : `~matplotlib.artist.Artist` or list thereof, optional + Error bar objects to add to the legend. This is used internally and + should not be necessary for users. See `indicate_error`. Other parameters ---------------- @@ -1866,12 +2171,12 @@ def cycle_changer( # Custom property cycler additions # NOTE: By default matplotlib uses _get_patches_for_fill.get_next_color - # for scatter properties! So we simultaneously iterate through the - # _get_lines property cycler and apply them. - apply = set() # which keys to apply from property cycler - if name == 'scatter': + # for scatter next scatter color, but cannot get anything else! We simultaneously + # iterate through the _get_lines property cycler and apply relevant properties. + apply_from_cycler = set() # which keys to apply from property cycler + if name in ('scatter',): # Figure out which props should be updated - keys = {*self._get_lines._prop_keys} - {'color', 'linestyle', 'dashes'} + prop_keys = set(self._get_lines._prop_keys) - {'color', 'linestyle', 'dashes'} for key, prop in ( ('markersize', 's'), ('linewidth', 'linewidths'), @@ -1881,8 +2186,8 @@ def cycle_changer( ('marker', 'marker'), ): prop = kwargs.get(prop, None) - if key in keys and prop is None: - apply.add(key) + if key in prop_keys and prop is None: # if key in cycler and property unset + apply_from_cycler.add(key) # Handle legend labels and # WARNING: Most methods that accept 2d arrays use columns of data, but when @@ -1924,9 +2229,9 @@ def cycle_changer( for i in range(ncols): # Prop cycle properties kw = kwargs.copy() - if apply: + if apply_from_cycler: props = next(self._get_lines.prop_cycler) - for key in apply: + for key in apply_from_cycler: value = props[key] if key in ('size', 'markersize'): key = 's' @@ -2017,11 +2322,14 @@ def cycle_changer( # Add legend if legend: - # Add handles + # Add handles and error objects loc = self._loc_translate(legend, 'legend', allow_manual=False) if loc not in self._auto_legend: self._auto_legend[loc] = ([], {}) self._auto_legend[loc][0].extend(objs) + if type(errobjs) not in (list, tuple): + errobjs = (errobjs,) + self._auto_legend[loc][0].extend(obj for obj in errobjs if obj is not None) # Add keywords if loc != 'fill': legend_kw.setdefault('loc', loc) @@ -2488,9 +2796,7 @@ def cmap_changer( cobj = obj colors = None if name in ('contourf', 'tricontourf'): - lums = [ - to_xyz(cmap(norm(level)), 'hcl')[2] for level in levels - ] + lums = [to_xyz(cmap(norm(level)), 'hcl')[2] for level in levels] cobj = self.contour(*args, levels=levels, linewidths=0) colors = ['w' if lum < 50 else 'k' for lum in lums] text_kw = {} @@ -2503,13 +2809,13 @@ def cmap_changer( labels_kw.setdefault('colors', colors) labels_kw.setdefault('inline_spacing', 3) labels_kw.setdefault('fontsize', rc['small']) - labs = self.clabel(cobj, fmt=fmt, **labels_kw) + labs = cobj.clabel(fmt=fmt, **labels_kw) for lab in labs: lab.update(text_kw) # Label each box manually # See: https://stackoverflow.com/a/20998634/4970632 - elif name in ('pcolor', 'pcolormesh'): + elif name in ('pcolor', 'pcolormesh', 'pcolorfast'): # Populate the _facecolors attribute, which is initially filled # with just a single color obj.update_scalarmappable() @@ -2549,13 +2855,13 @@ def cmap_changer( # corner of pcolor plots. *Never* use this when colormap has opacity. # See: https://stackoverflow.com/q/15003353/4970632 if edgefix and name in ( - 'pcolor', 'pcolormesh', 'tripcolor', 'contourf', 'tricontourf' + 'pcolor', 'pcolormesh', 'pcolorfast', 'tripcolor', 'contourf', 'tricontourf' ): cmap = obj.get_cmap() if not cmap._isinit: cmap._init() if all(cmap._lut[:-1, 3] == 1): # skip for cmaps with transparency - if name in ('pcolor', 'pcolormesh', 'tripcolor'): + if name in ('pcolor', 'pcolormesh', 'pcolorfast', 'tripcolor'): obj.set_edgecolor('face') obj.set_linewidth(0.4) else: @@ -3388,11 +3694,10 @@ def colorbar_wrapper( return cb -def _redirect(func): +def _basemap_redirect(func): """ Docorator that calls the basemap version of the function of the - same name. This must be applied as innermost decorator, which means it must - be applied on the base axes class, not the basemap axes. + same name. This must be applied as the innermost decorator. """ name = func.__name__ @@ -3406,30 +3711,26 @@ def _wrapper(self, *args, **kwargs): return _wrapper -def _norecurse(func): +def _basemap_norecurse(func): """ Decorator to prevent recursion in basemap method overrides. See `this post https://stackoverflow.com/a/37675810/4970632`__. """ name = func.__name__ - func._has_recurred = False + func._called_from_basemap = False @functools.wraps(func) def _wrapper(self, *args, **kwargs): - if func._has_recurred: - # Return the *original* version of the matplotlib method - func._has_recurred = False + if func._called_from_basemap: result = getattr(maxes.Axes, name)(self, *args, **kwargs) else: - # Return the version we have wrapped - func._has_recurred = True - result = func(self, *args, **kwargs) - func._has_recurred = False # cleanup, in case recursion never occurred + with _set_state(func, _called_from_basemap=True): + result = func(self, *args, **kwargs) return result return _wrapper -def _wrapper_decorator(driver): +def _generate_decorator(driver): """ Generate generic wrapper decorator and dynamically modify the docstring to list methods wrapped by this function. Also set `__doc__` to ``None`` so @@ -3476,24 +3777,26 @@ def _wrapper(self, *args, **kwargs): return decorator -# Auto generated decorators. Each wrapper internally calls -# func(self, ...) somewhere. -_add_errorbars = _wrapper_decorator(add_errorbars) -_bar_wrapper = _wrapper_decorator(bar_wrapper) -_barh_wrapper = _wrapper_decorator(barh_wrapper) -_default_latlon = _wrapper_decorator(default_latlon) -_boxplot_wrapper = _wrapper_decorator(boxplot_wrapper) -_default_crs = _wrapper_decorator(default_crs) -_default_transform = _wrapper_decorator(default_transform) -_cmap_changer = _wrapper_decorator(cmap_changer) -_cycle_changer = _wrapper_decorator(cycle_changer) -_fill_between_wrapper = _wrapper_decorator(fill_between_wrapper) -_fill_betweenx_wrapper = _wrapper_decorator(fill_betweenx_wrapper) -_hist_wrapper = _wrapper_decorator(hist_wrapper) -_parametric_wrapper = _wrapper_decorator(_parametric_wrapper_temporary) -_plot_wrapper = _wrapper_decorator(_plot_wrapper_deprecated) -_scatter_wrapper = _wrapper_decorator(scatter_wrapper) -_standardize_1d = _wrapper_decorator(standardize_1d) -_standardize_2d = _wrapper_decorator(standardize_2d) -_text_wrapper = _wrapper_decorator(text_wrapper) -_violinplot_wrapper = _wrapper_decorator(violinplot_wrapper) +# Auto generated decorators. Each wrapper internally calls func(self, ...) somewhere. +_bar_wrapper = _generate_decorator(bar_wrapper) +_barh_wrapper = _generate_decorator(barh_wrapper) +_default_latlon = _generate_decorator(default_latlon) +_boxplot_wrapper = _generate_decorator(boxplot_wrapper) +_default_crs = _generate_decorator(default_crs) +_default_transform = _generate_decorator(default_transform) +_cmap_changer = _generate_decorator(cmap_changer) +_cycle_changer = _generate_decorator(cycle_changer) +_fill_between_wrapper = _generate_decorator(fill_between_wrapper) +_fill_betweenx_wrapper = _generate_decorator(fill_betweenx_wrapper) +_hist_wrapper = _generate_decorator(hist_wrapper) +_hlines_wrapper = _generate_decorator(_hlines_wrapper_private) +_indicate_error = _generate_decorator(indicate_error) +_parametric_wrapper = _generate_decorator(_parametric_wrapper_private) +_plot_wrapper = _generate_decorator(_plot_wrapper_private) +_scatter_wrapper = _generate_decorator(scatter_wrapper) +_standardize_1d = _generate_decorator(standardize_1d) +_standardize_2d = _generate_decorator(standardize_2d) +_stem_wrapper = _generate_decorator(_stem_wrapper_private) +_text_wrapper = _generate_decorator(text_wrapper) +_violinplot_wrapper = _generate_decorator(violinplot_wrapper) +_vlines_wrapper = _generate_decorator(_vlines_wrapper_private) diff --git a/proplot/config.py b/proplot/config.py index cf9922056..9d0f91367 100644 --- a/proplot/config.py +++ b/proplot/config.py @@ -24,7 +24,7 @@ from . import colors as pcolors from .utils import units, to_xyz from .internals import ic # noqa: F401 -from .internals import defaults, docstring, timers, warnings, _not_none +from .internals import rcsetup, docstring, timers, warnings, _not_none try: from IPython import get_ipython except ImportError: @@ -47,8 +47,8 @@ def get_ipython(): # Dictionaries used to track custom proplot settings _Context = namedtuple('Context', ('mode', 'kwargs', 'rc_new', 'rc_old')) rc_params = mpl.rcParams # PEP8 4 lyfe -rc_quick = defaults._rc_quick_default.copy() -rc_added = defaults._rc_added_default.copy() +rc_quick = rcsetup._rc_quick_default.copy() +rc_added = rcsetup._rc_added_default.copy() # Misc constants # TODO: Use explicit validators for specific settings like matplotlib. @@ -232,11 +232,11 @@ def _tabulate(rcdict): # TODO: Never set rc_parents = { child: parent - for parent, children in defaults._rc_children.items() + for parent, children in rcsetup._rc_children.items() for child in children } - rc_added_filled = defaults._rc_added_default.copy() - for key, value in defaults._rc_added_default.items(): + rc_added_filled = rcsetup._rc_added_default.copy() + for key, value in rcsetup._rc_added_default.items(): if value is None: try: parent = rc_parents[key] @@ -245,10 +245,10 @@ def _tabulate(rcdict): f'rc_added param {key!r} has default value of None ' 'but has no rcParmsShort parent!' ) - if parent in defaults._rc_quick_default: - value = defaults._rc_quick_default[parent] - elif parent in defaults._rc_params_default: - value = defaults._rc_params_default[parent] + if parent in rcsetup._rc_quick_default: + value = rcsetup._rc_quick_default[parent] + elif parent in rcsetup._rc_params_default: + value = rcsetup._rc_params_default[parent] else: value = rc_params[parent] rc_added_filled[key] = value @@ -263,13 +263,13 @@ def _tabulate(rcdict): # https://matplotlib.org/3.1.1/tutorials/introductory/customizing.html #--------------------------------------------------------------------- # ProPlot quick settings -{_tabulate(defaults._rc_quick_default)} +{_tabulate(rcsetup._rc_quick_default)} # ProPlot added settings {_tabulate(rc_added_filled)} # Matplotlib settings -{_tabulate(defaults._rc_params_default)} +{_tabulate(rcsetup._rc_params_default)} """.strip()) @@ -450,7 +450,7 @@ def _get_param_dicts(self, key, value): kw_added = {} # custom properties that global setting applies to kw_quick = {} # short name properties key = self._sanitize_key(key) - children = defaults._rc_children.get(key, ()) + children = rcsetup._rc_children.get(key, ()) # Permit arbitary units for builtin matplotlib params # See: https://matplotlib.org/users/customizing.html, props matching @@ -462,6 +462,20 @@ def _get_param_dicts(self, key, value): except KeyError: value = units(value, 'pt') + # Deprecations + if key in rcsetup._rc_removed: + version = rcsetup._rc_removed[key] + warnings._warn_proplot( + f'rc setting {key!r} was removed in version {version}.' + ) + return {}, {}, {} + if key in rcsetup._rc_renamed: + key_new, version = rcsetup._rc_renamed[key] + warnings._warn_proplot( + f'rc setting {key!r} was renamed to {key_new} in version {version}.' + ) + key = key_new + # Special key: configure inline backend if key == 'inlinefmt': config_inline_backend(value) @@ -472,12 +486,13 @@ def _get_param_dicts(self, key, value): kw_params, kw_added = _get_style_dicts(value, infer=True) # Cycler - elif key in ('cycle', 'rgbcycle'): - colors = _update_color_cycle(key, value) + elif key == 'cycle': + colors = _get_cycle_colors(value) kw_params['patch.facecolor'] = colors[0] kw_params['axes.prop_cycle'] = cycler.cycler('color', colors) # Zero linewidth almost always means zero tick length + # TODO: Document this feature elif key == 'linewidth' and value == 0: _, ikw_added, ikw_params = self._get_param_dicts('ticklen', 0) kw_added.update(ikw_added) @@ -571,7 +586,7 @@ def _get_param_dicts(self, key, value): elif key in rc_params: kw_params[key] = value else: - raise KeyError(f'Invalid key {key!r}.') + raise KeyError(f'Invalid rc key {key!r}.') # Update linked settings for key in children: @@ -614,7 +629,7 @@ def _sanitize_key(key): if not isinstance(key, str): raise KeyError(f'Invalid key {key!r}. Must be string.') if '.' not in key and key not in rc_quick: # speedup - key = defaults._rc_nodots.get(key, key) + key = rcsetup._rc_nodots.get(key, key) return key.lower() @staticmethod @@ -703,10 +718,10 @@ def category(self, cat, *, trimcat=True, context=False): context mode dictionaries is omitted from the output dictionary. See `~rc_configurator.context`. """ - if cat not in defaults._rc_categories: + if cat not in rcsetup._rc_categories: raise ValueError( f'Invalid rc category {cat!r}. Valid categories are ' - ', '.join(map(repr, defaults._rc_categories)) + '.' + ', '.join(map(repr, rcsetup._rc_categories)) + '.' ) kw = {} mode = 0 if not context else None @@ -911,9 +926,9 @@ def reset(self, local=True, user=True, default=True): # should be for user convenience only and shold not be used internally. if default: rc_params.update(_get_style_dicts('original', infer=False)) - rc_params.update(defaults._rc_params_default) # proplot changes - rc_added.update(defaults._rc_added_default) # proplot custom params - rc_quick.update(defaults._rc_quick_default) # proplot quick params + rc_params.update(rcsetup._rc_params_default) # proplot changes + rc_added.update(rcsetup._rc_added_default) # proplot custom params + rc_quick.update(rcsetup._rc_quick_default) # proplot quick params for dict_ in (rc_quick, rc_added): for key, value in dict_.items(): _, kw_added, kw_params = self._get_param_dicts(key, value) @@ -944,47 +959,6 @@ def values(self): yield self[key] -def _update_color_cycle(key, value): - """ - Update the color cycle. - """ - if key == 'rgbcycle': - cycle, rgbcycle = rc_quick['cycle'], value - else: - cycle, rgbcycle = value, rc_quick['rgbcycle'] - try: - colors = pcolors._cmap_database[cycle].colors - except (KeyError, AttributeError): - return {}, {}, {} - cycles = sorted( - name for name, cmap in pcolors._cmap_database.items() - if isinstance(cmap, pcolors.ListedColormap) - ) - raise ValueError( - f'Invalid cycle name {cycle!r}. Options are: ' - + ', '.join(map(repr, cycles)) + '.' - ) - if rgbcycle and cycle.lower() == 'colorblind': - regcolors = colors + [(0.1, 0.1, 0.1)] - elif mcolors.to_rgb('r') != (1.0, 0.0, 0.0): # reset - regcolors = [ - (0.0, 0.0, 1.0), - (1.0, 0.0, 0.0), - (0.0, 1.0, 0.0), - (0.75, 0.75, 0.0), - (0.75, 0.75, 0.0), - (0.0, 0.75, 0.75), - (0.0, 0.0, 0.0) - ] - else: - regcolors = [] # no reset necessary - for code, color in zip('brgmyck', regcolors): - rgb = mcolors.to_rgb(color) - mcolors.colorConverter.colors[code] = rgb - mcolors.colorConverter.cache[code] = rgb - return colors - - def config_inline_backend(fmt=None): """ Set up the `ipython inline backend \ @@ -1030,6 +1004,24 @@ def config_inline_backend(fmt=None): ipython.magic("config InlineBackend.print_figure_kwargs = {'bbox_inches': None}") +def _get_cycle_colors(cycle): + """ + Update the color cycle. + """ + try: + colors = pcolors._cmap_database[cycle].colors + except (KeyError, AttributeError): + cycles = sorted( + name for name, cmap in pcolors._cmap_database.items() + if isinstance(cmap, pcolors.ListedColormap) + ) + raise ValueError( + f'Invalid cycle name {cycle!r}. Options are: ' + + ', '.join(map(repr, cycles)) + '.' + ) + return colors + + def _get_default_dict(): """ Get the default rc parameters dictionary with deprecated parameters filtered. @@ -1105,7 +1097,7 @@ def _get_style_dicts(style, infer=False): # Apply "pseudo" default properties. Pretend some proplot settings are part of # the matplotlib specification so they propagate to other styles. kw_params['font.family'] = 'sans-serif' - kw_params['font.sans-serif'] = defaults._rc_params_default['font.sans-serif'] + kw_params['font.sans-serif'] = rcsetup._rc_params_default['font.sans-serif'] # Apply user input style(s) one by one # NOTE: Always use proplot fonts if style does not explicitly set them. diff --git a/proplot/demos.py b/proplot/demos.py index 8beefd968..fd10b038e 100644 --- a/proplot/demos.py +++ b/proplot/demos.py @@ -12,7 +12,7 @@ from .utils import to_rgb, to_xyz from .config import rc, _get_data_paths, BASE_COLORS, XKCD_COLORS, OPEN_COLORS from .internals import ic # noqa: F401 -from .internals import docstring, _not_none +from .internals import docstring, warnings, _not_none __all__ = [ 'show_cmaps', @@ -201,7 +201,7 @@ def _draw_bars( + ', '.join(map(repr, source)) + '.' ) for cat in (*cmapdict,): - if cat not in categories: + if cat not in categories and cat != unknown: cmapdict.pop(cat) # Draw figure @@ -223,7 +223,11 @@ def _draw_bars( iax += 1 ax.set_visible(False) ax = axs[iax] - cmap = pcolors._cmap_database[name] + try: + cmap = pcolors._cmap_database[name] + except KeyError: + warnings._warn_proplot(f'Colormap {name!r} not found or unregistered.') + continue if N is not None: cmap = cmap.copy(N=N) ax.colorbar( diff --git a/proplot/internals/__init__.py b/proplot/internals/__init__.py index 567249a37..292a8dac5 100644 --- a/proplot/internals/__init__.py +++ b/proplot/internals/__init__.py @@ -2,7 +2,7 @@ """ Utilities used internally by proplot. """ -from . import defaults, docstring, timers, warnings # noqa: F401 +from . import rcsetup, docstring, timers, warnings # noqa: F401 try: # print debugging from icecream import ic except ImportError: # graceful fallback if IceCream isn't installed diff --git a/proplot/internals/defaults.py b/proplot/internals/rcsetup.py similarity index 77% rename from proplot/internals/defaults.py rename to proplot/internals/rcsetup.py index 53604f60b..f2a1175b7 100644 --- a/proplot/internals/defaults.py +++ b/proplot/internals/rcsetup.py @@ -7,6 +7,11 @@ from matplotlib import rcParamsDefault as _rc_matplotlib_default +# Deprecated settings +# TODO: Add 'openrgb' setting for renaming shorthand color names to +_rc_removed = {'rgbcycle': '0.6'} # {key: version} dictionary +_rc_renamed = {} # {old_key: (new_key, version)} dictionary + # Quick settings # TODO: This is currently a hodgepodge of "real" settings and meta-settings. # Should give the real settings "real" longer names in _rc_added_default. For @@ -21,7 +26,7 @@ 'borders': False, 'cmap': 'fire', 'coast': False, - 'color': 'k', + 'color': 'black', 'cycle': 'colorblind', 'facecolor': 'w', 'fontname': 'sans-serif', @@ -34,12 +39,13 @@ 'lakes': False, 'land': False, 'large': 10, - 'linewidth': 0.6, + 'linewidth': 0.8, 'lut': 256, - 'margin': 0.0, + 'margin': 0.05, + 'negcolor': 'blue7', 'ocean': False, - 'reso': 'lo', - 'rgbcycle': False, + 'poscolor': 'red7', + 'reso': 'med', 'rivers': False, 'share': 3, 'small': 9, @@ -61,7 +67,7 @@ _rc_added_default = { 'abc.border': True, 'abc.borderwidth': 1.5, - 'abc.color': 'k', + 'abc.color': 'black', 'abc.loc': 'l', # left side above the axes 'abc.size': None, # = large 'abc.style': 'a', @@ -71,13 +77,13 @@ 'axes.formatter.zerotrim': True, 'axes.geogrid': True, 'axes.gridminor': True, - 'borders.color': 'k', - 'borders.linewidth': 0.6, - 'bottomlabel.color': 'k', + 'borders.color': 'black', + 'borders.linewidth': 0.8, + 'bottomlabel.color': 'black', 'bottomlabel.size': None, # = large 'bottomlabel.weight': 'bold', - 'coast.color': 'k', - 'coast.linewidth': 0.6, + 'coast.color': 'black', + 'coast.linewidth': 0.8, 'colorbar.extend': '1.3em', 'colorbar.framealpha': 0.8, 'colorbar.frameon': True, @@ -93,15 +99,15 @@ 'geoaxes.facealpha': None, # = alpha 'geoaxes.facecolor': None, # = facecolor 'geoaxes.linewidth': None, # = linewidth - 'geogrid.alpha': 0.5, - 'geogrid.color': 'k', + 'geogrid.alpha': 0.25, + 'geogrid.color': 'black', 'geogrid.labelpad': 5, # use cartopy default 'geogrid.labels': False, 'geogrid.labelsize': None, # = small 'geogrid.latmax': 90, 'geogrid.latstep': 20, - 'geogrid.linestyle': ':', - 'geogrid.linewidth': 1.0, + 'geogrid.linestyle': '-', + 'geogrid.linewidth': 0.8, 'geogrid.lonstep': 30, 'geogrid.rotatelabels': True, # False limits projections where labels are available 'gridminor.alpha': None, # = grid.alpha @@ -110,25 +116,25 @@ 'gridminor.linewidth': None, # = grid.linewidth x gridratio 'image.edgefix': True, 'image.levels': 11, - 'innerborders.color': 'k', - 'innerborders.linewidth': 0.6, + 'innerborders.color': 'black', + 'innerborders.linewidth': 0.8, 'lakes.color': 'w', - 'land.color': 'k', - 'leftlabel.color': 'k', + 'land.color': 'black', + 'leftlabel.color': 'black', 'leftlabel.size': None, # = large 'leftlabel.weight': 'bold', 'ocean.color': 'w', - 'rightlabel.color': 'k', + 'rightlabel.color': 'black', 'rightlabel.size': None, # = large 'rightlabel.weight': 'bold', - 'rivers.color': 'k', - 'rivers.linewidth': 0.6, + 'rivers.color': 'black', + 'rivers.linewidth': 0.8, 'subplots.axpad': '1em', 'subplots.axwidth': '18em', 'subplots.pad': '0.5em', 'subplots.panelpad': '0.5em', 'subplots.panelwidth': '4em', - 'suptitle.color': 'k', + 'suptitle.color': 'black', 'suptitle.size': None, # = large 'suptitle.weight': 'bold', 'tick.labelcolor': None, # = color @@ -136,29 +142,31 @@ 'tick.labelweight': 'normal', 'title.border': True, 'title.borderwidth': 1.5, - 'title.color': 'k', + 'title.color': 'black', 'title.loc': 'c', # centered above the axes 'title.pad': 3.0, # copy 'title.size': None, # = large 'title.weight': 'normal', - 'toplabel.color': 'k', + 'toplabel.color': 'black', 'toplabel.size': None, # = large 'toplabel.weight': 'bold', } # ProPlot overrides of matplotlib default style -# TODO: Allow users to override with custom stylesheets and .matplotlibrc files. +# NOTE: Hard to say what best value for 'margin' is. 0 is bad for bar plots and scatter +# plots, 0.05 is good for line plot in y direction but not x direction. +# NOTE: Settings bounds is same as setting limits, except bounds get +# overridden after next plot: https://stackoverflow.com/a/11467349/4970632 +# NOTE: Some of these parameters are the same as matplotlib defaults but want +# to enforce some critical settings on top of user or system matplotlibrc files _rc_params_default = { - 'axes.grid': True, - 'axes.labelpad': 3.0, - 'axes.titlepad': 3.0, + 'axes.grid': True, # enable lightweight transparent grid by default + 'axes.labelpad': 3.0, # more compact + 'axes.titlepad': 3.0, # more compact 'axes.titleweight': 'normal', - 'axes.xmargin': 0.0, - 'axes.ymargin': 0.0, 'figure.autolayout': False, - 'figure.facecolor': '#f2f2f2', - 'figure.max_open_warning': 0, - 'figure.titleweight': 'bold', + 'figure.facecolor': '#f2f2f2', # similar to MATLAB interface + 'figure.titleweight': 'bold', # differentiate from axes titles 'font.serif': ( 'TeX Gyre Schola', # Century lookalike 'TeX Gyre Bonum', # Bookman lookalike @@ -241,34 +249,29 @@ 'xkcd', 'fantasy' ), - 'grid.alpha': 0.1, - 'grid.color': 'k', + 'grid.alpha': 0.1, # lightweight unobtrusive gridlines + 'grid.color': 'black', # lightweight unobtrusive gridlines 'grid.linestyle': '-', - 'grid.linewidth': 0.6, - 'hatch.color': 'k', - 'hatch.linewidth': 0.6, - 'legend.borderaxespad': 0, - 'legend.borderpad': 0.5, - 'legend.columnspacing': 1.0, - 'legend.fancybox': False, - 'legend.framealpha': 0.8, - 'legend.frameon': True, - 'legend.handlelength': 1.5, + 'grid.linewidth': 0.8, + 'hatch.color': 'black', + 'hatch.linewidth': 0.8, + 'lines.linestyle': '-', + 'lines.linewidth': 1.5, + 'lines.markersize': 6.0, + 'legend.borderaxespad': 0, # looks sleeker flush against edge + 'legend.borderpad': 0.5, # a bit more space + 'legend.columnspacing': 1.5, # more compact + 'legend.fancybox': False, # looks modern without curvy box 'legend.handletextpad': 0.5, - 'legend.labelspacing': 0.5, - 'lines.linewidth': 1.3, - 'lines.markersize': 3.0, 'mathtext.fontset': 'custom', 'mathtext.default': 'regular', - 'savefig.bbox': 'standard', - 'savefig.directory': '', - 'savefig.dpi': 300, - 'savefig.facecolor': 'white', - 'savefig.format': 'pdf', - 'savefig.pad_inches': 0.0, + 'savefig.bbox': 'standard', # use custom tight layout + 'savefig.directory': '', # current directory + 'savefig.dpi': 300, # low dpi to improve performance, high dpi when it matters + 'savefig.facecolor': 'white', # different from figure.facecolor + 'savefig.format': 'pdf', # most users use bitmap when vector graphics are better 'savefig.transparent': True, - 'text.usetex': False, - 'xtick.minor.visible': True, + 'xtick.minor.visible': True, # enable minor ticks by default 'ytick.minor.visible': True, } diff --git a/proplot/internals/warnings.py b/proplot/internals/warnings.py index af4198d33..07331a255 100644 --- a/proplot/internals/warnings.py +++ b/proplot/internals/warnings.py @@ -50,10 +50,11 @@ def obj(*args, **kwargs): return obj -def _rename_kwargs(version=None, **kwargs_rename): +def _rename_kwargs(version=None, ignore=False, **kwargs_rename): """ Emit a basic deprecation warning after removing or renaming function - keyword arguments. + keyword arguments. Each key should be an old keyword, and each arguments + should be the new keyword or a tuple of new keyword options. """ version = 'a future version' if version is None else f'version {version}' @@ -62,20 +63,20 @@ def decorator(func_orig): def func(*args, **kwargs): for key_old, key_new in kwargs_rename.items(): if key_old in kwargs: - if key_new is None: + if ignore or not isinstance(key_new, str): del kwargs[key_old] - _warn_proplot( - f'Ignoring keyword arg {key_old!r}. This argument ' - 'is deprecated. Using it will raise an error ' - 'in {version}.' - ) + message = f'Ignoring deprecated keyword arg {key_old!r}.' else: kwargs[key_new] = kwargs.pop(key_old) - _warn_proplot( - f'Keyword arg {key_old!r} is deprecated and will be ' - f'removed in {version}. Please use {key_new!r} ' - 'instead.' + message = ( + f'Keyword arg {key_old!r} is deprecated and will ' + f'be removed in {version}.' ) + if isinstance(key_new, str): + alternative = repr(key_new) + else: + alternative = ', '.join(map(repr, key_new)) + _warn_proplot(f'{message} Please use {alternative} instead.') return func_orig(*args, **kwargs) return func return decorator diff --git a/proplot/scale.py b/proplot/scale.py index 5d706f4e7..fef5fe5b6 100644 --- a/proplot/scale.py +++ b/proplot/scale.py @@ -257,7 +257,6 @@ def __init__( self, arg, invert=False, parent_scale=None, major_locator=None, minor_locator=None, major_formatter=None, minor_formatter=None, - smart_bounds=None, ): """ Parameters @@ -304,11 +303,6 @@ def __init__( The default major and minor formatter. By default these are borrowed from `transform`. If `transform` is not an axis scale, they are the same as `~matplotlib.scale.LinearScale`. - smart_bounds : bool, optional - Whether "smart bounds" are enabled by default. If not ``None``, - this is passed to `~matplotlib.axis.Axis.set_smart_bounds` when - `~matplotlib.scale.ScaleBase.set_default_locators_and_formatters` - is called. By default these are borrowed from `transform`. """ # NOTE: We permit *arbitrary* parent axis scales. If the parent is # non-linear, we use *its* default locators and formatters. Assumption @@ -363,7 +357,6 @@ def __init__( # Transform and default stuff self.functions = (forward, inverse) self._transform = functransform - self._default_smart_bounds = smart_bounds self._default_major_locator = major_locator self._default_minor_locator = minor_locator self._default_major_formatter = major_formatter @@ -378,8 +371,7 @@ def __init__( if isinstance(scale, mscale.LinearScale): continue for key in ( - 'smart_bounds', 'major_locator', 'minor_locator', - 'major_formatter', 'minor_formatter' + 'major_locator', 'minor_locator', 'major_formatter', 'minor_formatter' ): key = '_default_' + key attr = getattr(scale, key)