diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a4437a297..a2107e788 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,4 +10,4 @@ repos: - id: end-of-file-fixer - id: trailing-whitespace - id: flake8 - args: ['--max-line-length=88', '--ignore=W503,E402'] + args: ['--max-line-length=88', '--ignore=W503,E402,E741'] diff --git a/.travis.yml b/.travis.yml index ff7650c45..c2ae03537 100644 --- a/.travis.yml +++ b/.travis.yml @@ -38,7 +38,7 @@ install: - which python script: - - flake8 proplot docs --max-line-length=88 --ignore=W503,E402 + - flake8 proplot docs --max-line-length=88 --ignore=W503,E402,E741 - pushd docs - make html - popd diff --git a/CHANGELOG.rst b/CHANGELOG.rst index d826ecb36..25089d71a 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -115,6 +115,8 @@ 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`). - Support `cartopy 0.18 `__ locators, formatters, deprecations, and new labelling features (:pr:`158`). - Add :rcraw:`geogrid.labelpad` and :rcraw:`geogrid.rotatelabels` settings diff --git a/docs/1dplots.py b/docs/1dplots.py index dd52f4678..e26a3eac9 100644 --- a/docs/1dplots.py +++ b/docs/1dplots.py @@ -40,7 +40,7 @@ # It is often desirable to use different `property cycles # `__ # for different axes or different plot elements. To enable this, the -# `~proplot.wrappers.cycle_changer` adds the `cycle` and `cycle_kw` to the 1D +# `~proplot.axes.cycle_changer` adds the `cycle` and `cycle_kw` to the 1D # plotting methods. These arguments are passed to the # `~proplot.constructor.Cycle` constructor function, and the resulting property # cycle is used to style the input data. ProPlot iterates through property @@ -76,9 +76,9 @@ # Standardized arguments # ---------------------- # -# The `~proplot.wrappers.standardize_1d` wrapper is used to standardize +# The `~proplot.axes.standardize_1d` wrapper is used to standardize # positional arguments across all 1D plotting methods. -# `~proplot.wrappers.standardize_1d` allows you to optionally omit *x* +# `~proplot.axes.standardize_1d` allows you to optionally omit *x* # coordinates, in which case they are inferred from the data. It also permits # passing 2D *y* coordinate arrays to any plotting method, in which case the # plotting method is called for each column of the array. @@ -100,7 +100,7 @@ # Plot by passing both x and y coordinates ax = axs[0] ax.area(x, -1 * y / N, stacked=True) - ax.bar(x, y, linewidth=0, alpha=1, width=0.8 * (x[1] - x[0])) + ax.bar(x, y, linewidth=0, alpha=1, width=0.8) ax.plot(x, y + 1, linewidth=2) ax.scatter(x, y + 2, marker='s', markersize=5**2) ax.format(title='Manual x coordinates') @@ -122,7 +122,7 @@ # Pandas and xarray integration # ----------------------------- # -# The `~proplot.wrappers.standardize_1d` wrapper integrates 1D plotting +# The `~proplot.axes.standardize_1d` wrapper integrates 1D plotting # methods with pandas `~pandas.DataFrame`\ s and xarray `~xarray.DataArray`\ s. # When you pass a DataFrame or DataArray to any plotting command, the x-axis # label, y-axis label, legend label, colorbar label, and/or title are @@ -185,7 +185,7 @@ # Adding error bars # ----------------- # -# The `~proplot.wrappers.add_errorbars` wrapper lets you draw error bars +# The `~proplot.axes.add_errorbars` wrapper lets you draw error bars # 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`. @@ -193,12 +193,12 @@ # 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.wrappers.add_errorbars` lets you draw both thin error +# 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.wrappers.add_errorbars` for details. +# `~proplot.axes.add_errorbars` for details. # %% import proplot as plot @@ -254,8 +254,8 @@ # ------------------------ # # The `~matplotlib.axes.Axes.bar` and `~matplotlib.axes.Axes.barh` methods -# are wrapped by `~proplot.wrappers.bar_wrapper`, -# `~proplot.wrappers.cycle_changer`, and `~proplot.wrappers.standardize_1d`. +# are wrapped by `~proplot.axes.bar_wrapper`, +# `~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` @@ -266,8 +266,8 @@ # `~proplot.axes.Axes.areax` methods. These are alises for # `~matplotlib.axes.Axes.fill_between` and # `~matplotlib.axes.Axes.fill_betweenx`, which are now wrapped by -# `~proplot.wrappers.fill_between_wrapper` and -# `~proplot.wrappers.fill_betweenx_wrapper`. You can now *stack* or *overlay* +# `~proplot.axes.fill_between_wrapper` and +# `~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 @@ -350,9 +350,9 @@ # -------------------------- # # The `~matplotlib.axes.Axes.boxplot` and `~matplotlib.axes.Axes.violinplot` -# methods are now wrapped with `~proplot.wrappers.boxplot_wrapper`, -# `~proplot.wrappers.violinplot_wrapper`, `~proplot.wrappers.cycle_changer`, -# and `~proplot.wrappers.standardize_1d`. These wrappers add some useful +# methods are now wrapped with `~proplot.axes.boxplot_wrapper`, +# `~proplot.axes.violinplot_wrapper`, `~proplot.axes.cycle_changer`, +# and `~proplot.axes.standardize_1d`. These wrappers add some useful # options and apply aesthetically pleasing default settings. They also # automatically apply axis labels based on the `~pandas.DataFrame` column # labels or the input *x* coordinate labels. @@ -446,7 +446,7 @@ xlim=(-1, 1), ylim=(-1, 1), title='Step gradations', xlabel='cosine angle', ylabel='sine angle' ) -ax.colorbar(m, loc='b', maxn=10, label=f'parametric coordinate') +ax.colorbar(m, loc='b', maxn=10, label='parametric coordinate') # %% [raw] raw_mimetype="text/restructuredtext" @@ -456,8 +456,8 @@ # ------------- # # The `~matplotlib.axes.Axes.scatter` method is now wrapped by -# `~proplot.wrappers.scatter_wrapper`, `~proplot.wrappers.cycle_changer`, and -# `~proplot.wrappers.standardize_1d`. This means that +# `~proplot.axes.scatter_wrapper`, `~proplot.axes.cycle_changer`, and +# `~proplot.axes.standardize_1d`. This means that # `~matplotlib.axes.Axes.scatter` now accepts 2D arrays, just like # `~matplotlib.axes.Axes.plot`. Also, successive calls to # `~matplotlib.axes.Axes.scatter` now use the property cycler properties diff --git a/docs/2dplots.py b/docs/2dplots.py index dc884e04e..a465d4873 100644 --- a/docs/2dplots.py +++ b/docs/2dplots.py @@ -39,14 +39,14 @@ # # It is often desirable to create ProPlot colormaps on-the-fly, without # explicitly using the `~proplot.constructor.Colormap` constructor function. -# To enable this, the `~proplot.wrappers.cmap_changer` wrapper adds the +# To enable this, the `~proplot.axes.cmap_changer` wrapper adds the # `cmap` and `cmap_kw` arguments to every 2D plotting method. These # arguments are passed to the `~proplot.constructor.Colormap` constructor # function, and the resulting colormap is used for the input data. For # example, to create and apply a monochromatic colormap, you can simply use # ``cmap='color name'``. -# The `~proplot.wrappers.cmap_changer` wrapper also +# The `~proplot.axes.cmap_changer` wrapper also # adds the `norm` and `norm_kw` arguments. They are passed to the # `~proplot.constructor.Norm` constructor function, and the resulting # normalizer is used for the input data. For more information on colormaps @@ -80,7 +80,7 @@ # Discrete colormap levels # ------------------------ # -# The `~proplot.wrappers.cmap_changer` wrapper also applies the +# The `~proplot.axes.cmap_changer` wrapper also applies the # `~proplot.colors.DiscreteNorm` normalizer to every colormap plot. # `~proplot.colors.DiscreteNorm` converts data values to colormap colors by (1) # transforming data using an arbitrary *continuous* normalizer (e.g. @@ -212,7 +212,7 @@ # Standardized arguments # ---------------------- # -# The `~proplot.wrappers.standardize_2d` wrapper is used to standardize +# The `~proplot.axes.standardize_2d` wrapper is used to standardize # positional arguments across all 2D plotting methods. Among other things, # it guesses coordinate *edges* for `~matplotlib.axes.Axes.pcolor` and # `~matplotlib.axes.Axes.pcolormesh` plots when you supply coordinate @@ -255,7 +255,7 @@ # Pandas and xarray integration # ----------------------------- # -# The `~proplot.wrappers.standardize_2d` wrapper also integrates 2D +# The `~proplot.axes.standardize_2d` wrapper also integrates 2D # plotting methods with pandas `~pandas.DataFrame`\ s and xarray # `~xarray.DataArray`\ s. When you pass a DataFrame or DataArray to any # plotting command, the x-axis label, y-axis label, legend label, colorbar @@ -272,19 +272,20 @@ # DataArray state = np.random.RandomState(51423) -data = 50 * ( - np.sin(np.linspace(0, 2 * np.pi, 20) + 0) ** 2 - * np.cos(np.linspace(0, np.pi, 20) + np.pi / 2)[:, None] ** 2 +linspace = np.linspace(0, np.pi, 20) +data = 50 * state.normal(1, 0.2, size=(20, 20)) * ( + np.sin(linspace * 2) ** 2 + * np.cos(linspace + np.pi / 2)[:, None] ** 2 ) lat = xr.DataArray( np.linspace(-90, 90, 20), dims=('lat',), - attrs={'units': 'degN'} + attrs={'units': 'deg_north'} ) plev = xr.DataArray( np.linspace(1000, 0, 20), dims=('plev',), - attrs={'long_name': 'pressure', 'units': 'hPa'} + attrs={'long_name': 'pressure', 'units': 'mb'} ) da = xr.DataArray( data, @@ -295,14 +296,14 @@ ) # DataFrame -data = state.rand(20, 20) +data = state.rand(12, 20) df = pd.DataFrame( - data.cumsum(axis=0).cumsum(axis=1), - index=[*'JFMAMJJASONDJFMAMJJA'] + (data - 0.4).cumsum(axis=0).cumsum(axis=1), + index=list('JFMAMJJASOND'), ) df.name = 'temporal data' -df.index.name = 'index' -df.columns.name = 'time (days)' +df.index.name = 'month' +df.columns.name = 'variable (units)' # %% import proplot as plot @@ -311,13 +312,13 @@ # Plot DataArray axs[0].contourf( - da, cmap='RdPu', cmap_kw={'left': 0.05}, colorbar='l', lw=0.7, color='gray7' + da, cmap='RdPu', cmap_kw={'left': 0.05}, colorbar='l', lw=0.7, color='k' ) axs[0].format(yreverse=True) # Plot DataFrame axs[1].contourf( - df, cmap='YlOrRd', colorbar='r', linewidth=0.7, color='gray7' + df, cmap='YlOrRd', colorbar='r', linewidth=0.7, color='k' ) axs[1].format(xtickminor=False) @@ -328,19 +329,19 @@ # Contour and gridbox labels # -------------------------- # -# The `~proplot.wrappers.cmap_changer` wrapper also allows you to quickly add +# The `~proplot.axes.cmap_changer` wrapper also allows you to quickly add # *labels* to `~proplot.axes.Axes.heatmap`, `~matplotlib.axes.Axes.pcolor`, # `~matplotlib.axes.Axes.pcolormesh`, `~matplotlib.axes.Axes.contour`, and # `~matplotlib.axes.Axes.contourf` plots by simply using ``labels=True``. # Critically, the label text is colored black or white depending on the # luminance of the underlying grid box or filled contour. # -# `~proplot.wrappers.cmap_changer` draws contour labels with +# `~proplot.axes.cmap_changer` draws contour labels with # `~matplotlib.axes.Axes.clabel` and grid box labels with # `~matplotlib.axes.Axes.text`. You can pass keyword arguments to these # functions using the `labels_kw` dictionary keyword argument, and change the # label precision with the `precision` keyword argument. See -# `~proplot.wrappers.cmap_changer` for details. +# `~proplot.axes.cmap_changer` for details. # %% import proplot as plot diff --git a/docs/api.rst b/docs/api.rst index d9e7b262e..2061e875d 100644 --- a/docs/api.rst +++ b/docs/api.rst @@ -28,7 +28,7 @@ Axes classes .. automodsumm:: proplot.axes :toctree: api - :skip: ProjAxes, XYAxes + :classes-only: Constructor functions @@ -52,13 +52,15 @@ Configuration tools Plotting wrappers ================= -.. automodule:: proplot.wrappers +.. automodule:: proplot.axes.plot -.. automodsumm:: proplot.wrappers +.. automodsumm:: proplot.axes :toctree: api + :functions-only: + :skip: ProjAxes, XYAxes -Show functions +Demo functions ============== .. automodule:: proplot.demos diff --git a/docs/axis.py b/docs/axis.py index 8087aec51..86dac5458 100644 --- a/docs/axis.py +++ b/docs/axis.py @@ -269,8 +269,8 @@ # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_scales: # -# Axis scales -# ----------- +# Changing the axis scale +# ----------------------- # # "Axis scales" like ``'linear'`` and ``'log'`` control the *x* and *y* axis # coordinate system. To change the axis scale, simply pass e.g. @@ -448,8 +448,7 @@ # In the latter case, the scale's transforms are used for the forward and # inverse functions, and the scale's default locators and formatters are used # for the default `~proplot.scale.FuncScale` locators and formatters. -# -# Notably, the "parent" axis scale is now *arbitrary* -- in the first example +# Note that the "parent" axis scale is now arbitrary -- in the first example # shown below, we create a `~proplot.axes.CartesianAxes.dualx` axis for an # axis scaled by the ``'symlog'`` scale. diff --git a/docs/colorbars_legends.py b/docs/colorbars_legends.py index 7e6571e91..3beb49df2 100644 --- a/docs/colorbars_legends.py +++ b/docs/colorbars_legends.py @@ -43,8 +43,8 @@ # `~matplotlib.axes.Axes.contourf`). To draw a legend or colorbar-legend in # one go, pass a location (e.g. ``legend='r'`` or ``colorbar='r'``) to # methods that accept a `cycle` argument (e.g. `~matplotlib.axes.Axes.plot`). -# This feature is powered by the `~proplot.wrappers.cmap_changer` and -# `~proplot.wrappers.cycle_changer` wrappers. +# This feature is powered by the `~proplot.axes.cmap_changer` and +# `~proplot.axes.cycle_changer` wrappers. # # Finally, just like matplotlib "inset" legends, ProPlot also supports # "inset" *colorbars*. To draw an inset colorbar, pass an inset location to @@ -185,7 +185,7 @@ # # The `~proplot.figure.Figure` `~proplot.figure.Figure.colorbar` and # `~proplot.axes.Axes` `~proplot.axes.Axes.colorbar` methods are wrapped by -# `~proplot.wrappers.colorbar_wrapper`, which adds several new features. +# `~proplot.axes.colorbar_wrapper`, which adds several new features. # # You can now draw colorbars from *lists of colors* or *lists of artists* by # passing a list instead of a mappable object. Colorbar minor ticks are now @@ -243,7 +243,7 @@ # # The `~proplot.figure.Figure` `~proplot.figure.Figure.legend` and # `~proplot.axes.Axes` `~proplot.axes.Axes.legend` methods are wrapped by -# `~proplot.wrappers.legend_wrapper`, which adds several new features. +# `~proplot.axes.legend_wrapper`, which adds several new features. # # You can draw legends with *centered legend rows*, either by passing # ``center=True`` or by passing *list of lists* of plot handles. This is diff --git a/docs/colormaps.py b/docs/colormaps.py index ff60159dd..b0818ecdc 100644 --- a/docs/colormaps.py +++ b/docs/colormaps.py @@ -139,7 +139,7 @@ # your convenience, most of these features can be accessed via the # `~proplot.constructor.Colormap` constructor function. Note that every # plotting command that accepts a `cmap` keyword passes it through this -# function (see `~proplot.wrappers.cmap_changer`). +# function (see `~proplot.axes.cmap_changer`). # # To make `~proplot.colors.PerceptuallyUniformColormap`\ s from scratch, you # have the following three options: diff --git a/docs/conf.py b/docs/conf.py index 0e4550e87..4d2906510 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -74,7 +74,7 @@ 'sphinx_copybutton', 'sphinx_automodapi.automodapi', # see: https://github.com/lukelbd/sphinx-automodapi/tree/proplot-mods # noqa 'nbsphinx', - ] +] extlinks = { 'issue': ('https://github.com/lukelbd/proplot/issues/%s', 'GH#'), diff --git a/docs/cycles.py b/docs/cycles.py index 177e1d60a..1047d4590 100644 --- a/docs/cycles.py +++ b/docs/cycles.py @@ -58,7 +58,7 @@ # commands like `~matplotlib.axes.Axes.plot` and # `~matplotlib.axes.Axes.scatter` now accept a `cycle` keyword arg, which is # passed to `~proplot.constructor.Cycle` (see -# `~proplot.wrappers.cycle_changer`). To save your color cycle data and use +# `~proplot.axes.cycle_changer`). To save your color cycle data and use # it every time ProPlot is imported, simply pass ``save=True`` to # `~proplot.constructor.Cycle`. If you want to change the global property # cycler, pass a *name* to the :rcraw:`cycle` setting or pass the result of diff --git a/docs/projections.py b/docs/projections.py index 002f81b0a..d1feea054 100644 --- a/docs/projections.py +++ b/docs/projections.py @@ -181,8 +181,8 @@ # # The below example demonstrates how to plot geographic data with ProPlot. # It is mostly the same as cartopy, but with some new features powered by the -# `~proplot.wrappers.standardize_2d`, `~proplot.wrappers.default_transform`, -# and `~proplot.wrappers.default_latlon` wrappers. +# `~proplot.axes.standardize_2d`, `~proplot.axes.default_transform`, +# and `~proplot.axes.default_latlon` wrappers. # # * For both basemap and cartopy projections, you can pass ``globe=True`` to # 2D plotting commands to ensure global data coverage. diff --git a/docs/why.rst b/docs/why.rst index 0d7069c97..971a1c7f8 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -441,10 +441,10 @@ designed to make your life easier. .. All positional arguments for 1D plotting methods are standardized by - `~proplot.wrappers.standardize_1d`. All positional arguments for 2D - plotting methods are standardized by `~proplot.wrappers.standardize_2d`. - See :ref:`1D plotting wrappers` and :ref:`2D plotting wrappers` for - details. + `~proplot.axes.standardize_1d`. All positional arguments for 2D + plotting methods are standardized by `~proplot.axes.standardize_2d`. + See :ref:`1D plotting methods <1d_plots>` and :ref:`2D plotting methods <2d_plots>` + for details. .. _why_xarray_pandas: diff --git a/proplot/__init__.py b/proplot/__init__.py index 5c46bb33a..24a285d7f 100644 --- a/proplot/__init__.py +++ b/proplot/__init__.py @@ -15,7 +15,6 @@ from .scale import * # noqa: F401 F403 from .gridspec import * # noqa: F401 F403 from .constructor import * # noqa: F401 F403 - from .wrappers import * # noqa: F401 F403 from .axes import * # noqa: F401 F403 from .figure import * # noqa: F401 F403 from .ui import * # noqa: F401 F403 diff --git a/proplot/axes/__init__.py b/proplot/axes/__init__.py index 1d321bb8a..64bf55893 100644 --- a/proplot/axes/__init__.py +++ b/proplot/axes/__init__.py @@ -2,11 +2,14 @@ """ The axes classes used for all ProPlot figures. """ +from . import plot +from .plot import * # noqa: F401, F403 from .base import Axes # noqa: F401 -from ..internals import warnings from .cartesian import CartesianAxes from .polar import PolarAxes -from .geo import GeoAxes, BasemapAxes, CartopyAxes # noqa: F401 +from .geo import GeoAxes # noqa: F401 +from .geo import BasemapAxes, CartopyAxes +from ..internals import warnings XYAxes = warnings._rename_obj('XYAxes', CartesianAxes) ProjAxes = warnings._rename_obj('ProjAxes', GeoAxes) @@ -22,3 +25,4 @@ 'GeoAxes', 'CartopyAxes', 'BasemapAxes', 'ProjAxes', 'XYAxes', # deprecated ] +__all__.extend(plot.__all__) diff --git a/proplot/axes/base.py b/proplot/axes/base.py index 693229aa6..cac524327 100644 --- a/proplot/axes/base.py +++ b/proplot/axes/base.py @@ -9,12 +9,7 @@ import matplotlib.patches as mpatches import matplotlib.transforms as mtransforms import matplotlib.collections as mcollections -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 ..wrappers import ( +from .plot import ( _get_transform, _add_errorbars, _bar_wrapper, _barh_wrapper, _boxplot_wrapper, _cmap_changer, _cycle_changer, @@ -24,6 +19,11 @@ _text_wrapper, _violinplot_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 __all__ = ['Axes'] @@ -329,7 +329,7 @@ def _range_gridspec(self, x): Return the column or row gridspec range for the axes. """ if not hasattr(self, 'get_subplotspec'): - raise RuntimeError(f'Axes is not a subplot.') + raise RuntimeError('Axes is not a subplot.') ss = self.get_subplotspec() if hasattr(ss, 'get_active_rows_columns'): func = ss.get_active_rows_columns @@ -952,7 +952,7 @@ def colorbar( ): """ Add an *inset* colorbar or *outer* colorbar along the outside edge of - the axes. See `~proplot.wrappers.colorbar_wrapper` for details. + the axes. See `~proplot.axes.colorbar_wrapper` for details. Parameters ---------- @@ -1008,7 +1008,7 @@ def colorbar( Other parameters ---------------- *args, **kwargs - Passed to `~proplot.wrappers.colorbar_wrapper`. + Passed to `~proplot.axes.colorbar_wrapper`. """ # TODO: add option to pad inset away from axes edge! # TODO: get "best" colorbar location from legend algorithm. @@ -1051,7 +1051,7 @@ def colorbar( subplotspec = gridspec[1] # Draw colorbar axes - with self.figure._authorize_add_subplot(): + with self.figure._context_authorize_add_subplot(): ax = self.figure.add_subplot(subplotspec, projection='cartesian') # noqa: E501 self.add_child_axes(ax) @@ -1180,7 +1180,7 @@ def colorbar( ticklocation = kwargs.pop('ticklocation', None) or ticklocation if ticklocation is not None and ticklocation != 'bottom': warnings._warn_proplot( - f'Inset colorbars can only have ticks on the bottom.' + 'Inset colorbars can only have ticks on the bottom.' ) kwargs.update({ 'orientation': 'horizontal', 'ticklocation': 'bottom' @@ -1194,7 +1194,7 @@ def colorbar( def legend(self, *args, loc=None, width=None, space=None, **kwargs): """ Add an *inset* legend or *outer* legend along the edge of the axes. - See `~proplot.wrappers.legend_wrapper` for details. + See `~proplot.axes.legend_wrapper` for details. Parameters ---------- @@ -1234,7 +1234,7 @@ def legend(self, *args, loc=None, width=None, space=None, **kwargs): Other parameters ---------------- *args, **kwargs - Passed to `~proplot.wrappers.legend_wrapper`. + Passed to `~proplot.axes.legend_wrapper`. """ if loc != 'fill': loc = self._loc_translate(loc, 'legend') diff --git a/proplot/axes/cartesian.py b/proplot/axes/cartesian.py index cf821a59b..1033998a5 100644 --- a/proplot/axes/cartesian.py +++ b/proplot/axes/cartesian.py @@ -1124,7 +1124,7 @@ def altx(self, **kwargs): # See https://github.com/matplotlib/matplotlib/blob/master/lib/matplotlib/axes/_subplots.py # noqa if self._altx_child or self._altx_parent: raise RuntimeError('No more than *two* twin axes are allowed.') - with self.figure._authorize_add_subplot(): + with self.figure._context_authorize_add_subplot(): ax = self._make_twin_axes(sharey=self, projection='cartesian') ax.set_autoscaley_on(self.get_autoscaley_on()) ax.grid(False) @@ -1145,7 +1145,7 @@ def alty(self, **kwargs): # Docstring is programatically assigned below if self._alty_child or self._alty_parent: raise RuntimeError('No more than *two* twin axes are allowed.') - with self.figure._authorize_add_subplot(): + with self.figure._context_authorize_add_subplot(): ax = self._make_twin_axes(sharex=self, projection='cartesian') ax.set_autoscalex_on(self.get_autoscalex_on()) ax.grid(False) diff --git a/proplot/axes/geo.py b/proplot/axes/geo.py index beb33ef75..79fa66155 100644 --- a/proplot/axes/geo.py +++ b/proplot/axes/geo.py @@ -8,12 +8,7 @@ import matplotlib.path as mpath import matplotlib.ticker as mticker from . import base -from .. import crs as pcrs -from ..utils import arange -from ..config import rc -from ..internals import ic # noqa: F401 -from ..internals import warnings, _version, _version_cartopy, _not_none -from ..wrappers import ( +from .plot import ( _add_errorbars, _norecurse, _redirect, _plot_wrapper, _scatter_wrapper, _fill_between_wrapper, _fill_betweenx_wrapper, @@ -22,6 +17,11 @@ _standardize_1d, _standardize_2d, _text_wrapper, ) +from .. import crs as pcrs +from ..utils import arange +from ..config import rc +from ..internals import ic # noqa: F401 +from ..internals import warnings, _version, _version_cartopy, _not_none try: from cartopy.mpl.geoaxes import GeoAxes as GeoAxesCartopy except ModuleNotFoundError: @@ -760,7 +760,7 @@ def projection(self): def projection(self, map_projection): import cartopy.crs as ccrs if not isinstance(map_projection, ccrs.CRS): - raise ValueError(f'Projection must be a cartopy.crs.CRS instance.') + raise ValueError('Projection must be a cartopy.crs.CRS instance.') self._map_projection = map_projection # Wrapped methods @@ -1074,7 +1074,7 @@ def projection(self): def projection(self, map_projection): import mpl_toolkits.basemap as mbasemap if not isinstance(map_projection, mbasemap.Basemap): - raise ValueError(f'Projection must be a basemap.Basemap instance.') + raise ValueError('Projection must be a basemap.Basemap instance.') self._map_projection = map_projection # Wrapped methods diff --git a/proplot/wrappers.py b/proplot/axes/plot.py similarity index 93% rename from proplot/wrappers.py rename to proplot/axes/plot.py index f990bc245..5dfde3686 100644 --- a/proplot/wrappers.py +++ b/proplot/axes/plot.py @@ -1,9 +1,9 @@ #!/usr/bin/env python3 """ -Wrapper functions used to add functionality to various `~proplot.axes.Axes` -plotting methods. "Wrapped" plotting methods accept the additional keyword -arguments documented by the wrapper function. In a future version, these -features will be documented on the individual plotting methods. +The plotting wrappers that add functionality to various `~matplotlib.axes.Axes` +methods. "Wrapped" `~matplotlib.axes.Axes` methods accept the additional keyword +arguments documented by the wrapper function. In a future version, these features will +be documented on the individual `~proplot.axes.Axes` methods themselves. """ import sys import numpy as np @@ -21,12 +21,12 @@ import matplotlib.legend as mlegend import matplotlib.cm as mcm from numbers import Number -from . import constructor -from . import colors as pcolors -from .config import rc -from .utils import edges, edges2d, units, to_xyz, to_rgb -from .internals import ic # noqa: F401 -from .internals import docstring, warnings, _version, _version_cartopy, _not_none +from .. import constructor +from .. import colors as pcolors +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 try: from cartopy.crs import PlateCarree except ModuleNotFoundError: @@ -55,6 +55,129 @@ ] +docstring.snippets['cmap_changer.params'] = """ +cmap : colormap spec, optional + The colormap specifer, passed to the `~proplot.constructor.Colormap` + constructor. +cmap_kw : dict-like, optional + Passed to `~proplot.constructor.Colormap`. +norm : normalizer spec, optional + The colormap normalizer, used to warp data before passing it + to `~proplot.colors.DiscreteNorm`. This is passed to the + `~proplot.constructor.Norm` constructor. +norm_kw : dict-like, optional + Passed to `~proplot.constructor.Norm`. +vmin, vmax : float, optional + Used to determine level locations if `levels` is an integer. Actual + levels may not fall exactly on `vmin` and `vmax`, but the minimum + level will be no smaller than `vmin` and the maximum level will be + no larger than `vmax`. If `vmin` or `vmax` is not provided, the + minimum and maximum data values are used. +levels, N : int or list of float, optional + The number of level edges, or a list of level edges. If the former, + `locator` is used to generate this many levels at "nice" intervals. + If the latter, the levels should be monotonically increasing or + decreasing (note that decreasing levels will only work with ``pcolor`` + plots, not ``contour`` plots). Default is :rc:`image.levels`. + Note this means you can now discretize your colormap colors in a + ``pcolor`` plot just like with ``contourf``. +values : int or list of float, optional + The number of level centers, or a list of level centers. If provided, + levels are inferred using `~proplot.utils.edges`. This will override + any `levels` input. +symmetric : bool, optional + If ``True``, automatically generated levels are symmetric about zero. +locator : locator-spec, optional + The locator used to determine level locations if `levels` or `values` + is an integer and `vmin` and `vmax` were not provided. Passed to the + `~proplot.constructor.Locator` constructor. Default is + `~matplotlib.ticker.MaxNLocator` with ``levels`` integer levels. +locator_kw : dict-like, optional + Passed to `~proplot.constructor.Locator`. +""" + +_area_docstring = """ +Supports overlaying and stacking successive columns of data, and permits +using different colors for "negative" and "positive" regions. + +Note +---- +This function wraps `~matplotlib.axes.Axes.fill_between{suffix}` and +`~proplot.axes.Axes.area{suffix}`. + +Parameters +---------- +*args : ({y}1,), ({x}, {y}1), or ({x}, {y}1, {y}2) + The *{x}* and *{y}* coordinates. If `{x}` is not provided, it will be + inferred from `{y}1`. If `{y}1` and `{y}2` are provided, this function + will shade between respective columns of the arrays. The default value + for `{y}2` is ``0``. +stacked : bool, optional + Whether to "stack" successive columns of the `{y}1` array. If this is + ``True`` and `{y}2` was provided, it will be ignored. +negpos : bool, optional + Whether to shade where `{y}1` is greater than `{y}2` with the color + `poscolor`, and where `{y}1` is less than `{y}2` with the color + `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``. +where : ndarray, optional + Boolean ndarray mask for points you want to shade. See `this example \ +`__. + +Other parameters +---------------- +**kwargs + Passed to `~matplotlib.axes.Axes.fill_between`. +""" +docstring.snippets['axes.fill_between'] = _area_docstring.format( + x='x', y='y', suffix='', +) +docstring.snippets['axes.fill_betweenx'] = _area_docstring.format( + x='y', y='x', suffix='x', +) + +_bar_docstring = """ +Supports grouping and stacking successive columns of data, and changes +the default bar style. + +Note +---- +This function wraps `~matplotlib.axes.Axes.bar{suffix}`. + +Parameters +---------- +{x}, {height}, {width}, {bottom} : float or list of float, optional + The dimensions of the bars. If the *{x}* coordinates are not provided, + they are set to ``np.arange(0, len(height))``. +orientation : {{'vertical', 'horizontal'}}, optional + The orientation of the bars. +vert : bool, optional + Alternative to the `orientation` keyword arg. If ``False``, horizontal + bars are drawn. This is for consistency with + `~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. +lw, linewidth : float, optional + The edge width for the bar patches. + +Other parameters +---------------- +**kwargs + Passed to `~matplotlib.axes.Axes.bar{suffix}`. +""" +docstring.snippets['axes.bar'] = _bar_docstring.format( + x='x', height='height', width='width', bottom='bottom', suffix='', +) +docstring.snippets['axes.barh'] = _bar_docstring.format( + x='y', height='width', width='height', bottom='left', suffix='h', +) + + def _load_objects(): """ Delay loading expensive modules. We just want to detect if *input @@ -128,7 +251,7 @@ def _is_string(data): return len(data) and isinstance(_to_ndarray(data).flat[0], str) -def _to_array(data): +def _to_arraylike(data): """ Convert list of lists to array-like type. """ @@ -162,7 +285,7 @@ def default_latlon(self, func, *args, latlon=True, **kwargs): Note ---- - This function wraps %(methods)s for `~proplot.axes.BasemapAxes`. + This function wraps {methods} for `~proplot.axes.BasemapAxes`. """ return func(self, *args, latlon=latlon, **kwargs) @@ -176,7 +299,7 @@ def default_transform(self, func, *args, transform=None, **kwargs): Note ---- - This function wraps %(methods)s for `~proplot.axes.CartopyAxes`. + This function wraps {methods} for `~proplot.axes.CartopyAxes`. """ # Apply default transform # TODO: Do some cartopy methods reset backgroundpatch or outlinepatch? @@ -195,7 +318,7 @@ def default_crs(self, func, *args, crs=None, **kwargs): Note ---- - This function wraps %(methods)s for `~proplot.axes.CartopyAxes`. + This function wraps {methods} for `~proplot.axes.CartopyAxes`. """ # Apply default crs name = func.__name__ @@ -284,7 +407,7 @@ def standardize_1d(self, func, *args, **kwargs): Note ---- - This function wraps the 1d plotting methods: %(methods)s. + This function wraps the 1d plotting methods: {methods} """ # Sanitize input # TODO: Add exceptions for methods other than 'hist'? @@ -311,7 +434,7 @@ def standardize_1d(self, func, *args, **kwargs): ys, args = (y, args[0]), args[1:] else: ys = (y,) - ys = [_to_array(y) for y in ys] + ys = [_to_arraylike(y) for y in ys] # Auto x coords y = ys[0] # test the first y input @@ -321,7 +444,7 @@ def standardize_1d(self, func, *args, **kwargs): or any(kwargs.get(s, None) for s in ('means', 'medians')) ) x, _ = _axis_labels_title(y, axis=axis) - x = _to_array(x) + x = _to_arraylike(x) if x.ndim != 1: raise ValueError( f'x coordinates must be 1-dimensional, but got {x.ndim}.' @@ -476,7 +599,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): Parameters ---------- - order : {'C', 'F'}, optional + order : {{'C', 'F'}}, optional If ``'C'``, arrays should be shaped as ``(y, x)``. If ``'F'``, arrays should be shaped as ``(x, y)``. Default is ``'C'``. globe : bool, optional @@ -498,7 +621,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): Note ---- - This function wraps the 2d plotting methods: %(methods)s. + This function wraps the 2d plotting methods: {methods} """ # Sanitize input name = func.__name__ @@ -514,7 +637,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): # Ensure DataArray, DataFrame or ndarray Zs = [] for Z in args: - Z = _to_array(Z) + Z = _to_arraylike(Z) if Z.ndim != 2: raise ValueError(f'Z must be 2-dimensional, got shape {Z.shape}.') Zs.append(Z) @@ -526,10 +649,12 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): # Retrieve coordinates if x is None and y is None: Z = Zs[0] - if order == 'C': # TODO: check order stuff works + if order == 'C': idx, idy = 1, 0 else: idx, idy = 0, 1 + # x = np.arange(Z.shape[idx]) + # y = np.arange(Z.shape[idy]) if isinstance(Z, ndarray): x = np.arange(Z.shape[idx]) y = np.arange(Z.shape[idy]) @@ -544,8 +669,19 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): x = Z.index y = Z.columns + # Optionally re-order + # TODO: Double check this + if order == 'F': + x, y = x.T, y.T # in case they are 2-dimensional + Zs = tuple(Z.T for Z in Zs) + elif order != 'C': + raise ValueError( + f'Invalid order {order!r}. Choose from ' + '"C" (row-major, default) and "F" (column-major).' + ) + # Check coordinates - x, y = _to_array(x), _to_array(y) + x, y = _to_arraylike(x), _to_arraylike(y) if x.ndim != y.ndim: raise ValueError( f'x coordinates are {x.ndim}-dimensional, ' @@ -568,7 +704,7 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): kw['xlocator'] = mticker.FixedLocator(xi) kw['xformatter'] = mticker.IndexFormatter(x) kw['xminorlocator'] = mticker.NullLocator() - if _is_string(x): + if _is_string(y): yi = np.arange(len(y)) kw['ylocator'] = mticker.FixedLocator(yi) kw['yformatter'] = mticker.IndexFormatter(y) @@ -627,17 +763,6 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): f'Z borders {tuple(i+1 for i in Z.shape)}.' ) - # Optionally re-order - # TODO: Double check this - if order == 'F': - x, y = x.T, y.T # in case they are 2-dimensional - Zs = (Z.T for Z in Zs) - elif order != 'C': - raise ValueError( - f'Invalid order {order!r}. Choose from ' - '"C" (row-major, default) and "F" (column-major).' - ) - # Enforce centers else: # Get centers given edges. If 2d, don't raise error, let matplotlib @@ -670,17 +795,6 @@ def standardize_2d(self, func, *args, order='C', globe=False, **kwargs): f'or Z borders {tuple(i+1 for i in Z.shape)}.' ) - # Optionally re-order - # TODO: Double check this - if order == 'F': - x, y = x.T, y.T # in case they are 2-dimensional - Zs = (Z.T for Z in Zs) - elif order != 'C': - raise ValueError( - f'Invalid order {order!r}. Choose from ' - '"C" (row-major, default) and "F" (column-major).' - ) - # Cartopy projection axes if ( getattr(self, 'name', '') == 'cartopy' @@ -808,7 +922,7 @@ def add_errorbars( Note ---- - This function wraps the 1d plotting methods: %(methods)s. + This function wraps the 1d plotting methods: {methods} Parameters ---------- @@ -968,12 +1082,14 @@ def _plot_wrapper_deprecated( return self.parametric(*args, cmap=cmap, values=values, **kwargs) +@docstring.add_snippets def scatter_wrapper( self, func, *args, s=None, size=None, markersize=None, - c=None, color=None, markercolor=None, - smin=None, smax=None, - cmap=None, cmap_kw=None, vmin=None, vmax=None, norm=None, norm_kw=None, + c=None, color=None, markercolor=None, smin=None, smax=None, + cmap=None, cmap_kw=None, norm=None, norm_kw=None, + vmin=None, vmax=None, N=None, levels=None, values=None, + symmetric=False, locator=None, locator_kw=None, lw=None, linewidth=None, linewidths=None, markeredgewidth=None, markeredgewidths=None, edgecolor=None, edgecolors=None, @@ -982,14 +1098,12 @@ def scatter_wrapper( ): """ Adds keyword arguments to `~matplotlib.axes.Axes.scatter` that are more - consistent with the `~matplotlib.axes.Axes.plot` keyword arguments, and - interpret the `cmap` and `norm` keyword arguments with - `~proplot.constructor.Colormap` and `~proplot.constructor.Norm` like - in `cmap_changer`. + consistent with the `~matplotlib.axes.Axes.plot` keyword arguments and + supports `cmap_changer` features. Note ---- - This function wraps %(methods)s. + This function wraps {methods} Parameters ---------- @@ -1003,20 +1117,7 @@ def scatter_wrapper( The marker fill color(s). If this is an array of scalar values, the colors will be generated by passing the values through the `norm` normalizer and drawing from the `cmap` colormap. - cmap : colormap-spec, optional - The colormap specifer, passed to the `~proplot.constructor.Colormap` - constructor. - cmap_kw : dict-like, optional - Passed to `~proplot.constructor.Colormap`. - vmin, vmax : float, optional - Used to generate a `norm` for scaling the `c` array. These are the - values corresponding to the leftmost and rightmost colors in the - colormap. Defaults are the minimum and maximum values of the `c` array. - norm : normalizer spec, optional - The colormap normalizer, passed to the `~proplot.constructor.Norm` - constructor. - norm_kw : dict, optional - Passed to `~proplot.constructor.Norm`. + %(cmap_changer.params)s lw, linewidth, linewidths, markeredgewidth, markeredgewidths : \ float or list thereof, optional The marker edge width. @@ -1040,14 +1141,6 @@ def scatter_wrapper( if len(args) == 3: s = args.pop(0) - # Format cmap and norm - cmap_kw = cmap_kw or {} - norm_kw = norm_kw or {} - if cmap is not None: - cmap = constructor.Colormap(cmap, **cmap_kw) - if norm is not None: - norm = constructor.Norm(norm, **norm_kw) - # Apply some aliases for keyword arguments c = _not_none(c=c, color=color, markercolor=markercolor) s = _not_none(s=s, size=size, markersize=markersize) @@ -1060,6 +1153,29 @@ def scatter_wrapper( markeredgecolor=markeredgecolor, markeredgecolors=markeredgecolors, ) + # Get colormap + cmap_kw = cmap_kw or {} + if cmap is not None: + cmap = constructor.Colormap(cmap, **cmap_kw) + + # Get normalizer and levels + # NOTE: If the length of the c array != + ticks = None + carray = np.atleast_1d(c) + if ( + np.issubdtype(carray.dtype, np.number) + and not (carray.ndim == 2 and carray.shape[1] in (3, 4)) + ): + carray = carray.ravel() + norm, cmap, _, ticks = _build_discrete_norm( + carray, # sample data for getting suitable levels + N=N, levels=levels, values=values, + norm=norm, norm_kw=norm_kw, + locator=locator, locator_kw=locator_kw, + cmap=cmap, vmin=vmin, vmax=vmax, extend='neither', + symmetric=symmetric, + ) + # Fix 2D arguments but still support scatter(x_vector, y_2d) usage # NOTE: Since we are flattening vectors the coordinate metadata is meaningless, # so converting to ndarray and stripping metadata is no problem. @@ -1078,56 +1194,13 @@ def scatter_wrapper( smin + (smax - smin) * (np.array(s) - smin_true) / (smax_true - smin_true) ) - return func( - self, *args, c=c, s=s, - cmap=cmap, vmin=vmin, vmax=vmax, - norm=norm, linewidths=lw, edgecolors=ec, - **kwargs + obj = func( + self, *args, c=c, s=s, cmap=cmap, norm=norm, + linewidths=lw, edgecolors=ec, **kwargs ) - - -_area_docstring = """ -Supports overlaying and stacking successive columns of data, and permits -using different colors for "negative" and "positive" regions. - -Note ----- -This function wraps `~matplotlib.axes.Axes.fill_between{suffix}` and -`~proplot.axes.Axes.area{suffix}`. - -Parameters ----------- -*args : ({y}1,), ({x}, {y}1), or ({x}, {y}1, {y}2) - The *{x}* and *{y}* coordinates. If `{x}` is not provided, it will be - inferred from `{y}1`. If `{y}1` and `{y}2` are provided, this function - will shade between respective columns of the arrays. The default value - for `{y}2` is ``0``. -stacked : bool, optional - Whether to "stack" successive columns of the `{y}1` array. If this is - ``True`` and `{y}2` was provided, it will be ignored. -negpos : bool, optional - Whether to shade where `{y}1` is greater than `{y}2` with the color - `poscolor`, and where `{y}1` is less than `{y}2` with the color - `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``. -where : ndarray, optional - Boolean ndarray mask for points you want to shade. See `this example \ -`__. - -Other parameters ----------------- -**kwargs - Passed to `~matplotlib.axes.Axes.fill_between`. -""" -docstring.snippets['axes.fill_between'] = _area_docstring.format( - x='x', y='y', suffix='', -) -docstring.snippets['axes.fill_betweenx'] = _area_docstring.format( - x='y', y='x', suffix='x', -) + if ticks is not None: + obj.ticks = ticks + return obj def _fill_between_apply( @@ -1212,51 +1285,12 @@ def hist_wrapper(self, func, x, bins=None, **kwargs): Note ---- - This function wraps %(methods)s. + This function wraps {methods} """ kwargs.setdefault('linewidth', 0) return func(self, x, bins=bins, **kwargs) -_bar_docstring = """ -Supports grouping and stacking successive columns of data, and changes -the default bar style. - -Note ----- -This function wraps `~matplotlib.axes.Axes.bar{suffix}`. - -Parameters ----------- -{x}, {height}, {width}, {bottom} : float or list of float, optional - The dimensions of the bars. If the *{x}* coordinates are not provided, - they are set to ``np.arange(0, len(height))``. -orientation : {{'vertical', 'horizontal'}}, optional - The orientation of the bars. -vert : bool, optional - Alternative to the `orientation` keyword arg. If ``False``, horizontal - bars are drawn. This is for consistency with - `~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. -lw, linewidth : float, optional - The edge width for the bar patches. - -Other parameters ----------------- -**kwargs - Passed to `~matplotlib.axes.Axes.bar{suffix}`. -""" -docstring.snippets['axes.bar'] = _bar_docstring.format( - x='x', height='height', width='width', bottom='bottom', suffix='', -) -docstring.snippets['axes.barh'] = _bar_docstring.format( - x='y', height='width', width='height', bottom='left', suffix='h', -) - - @docstring.add_snippets def bar_wrapper( self, func, x=None, height=None, width=0.8, bottom=None, *, left=None, @@ -1267,8 +1301,11 @@ def bar_wrapper( """ %(axes.bar)s """ + # Parse arguments + # WARNING: Implementation is really weird... we flip around arguments for horizontal + # plots only to flip them back in cycle_changer when iterating through columns. if vert is not None: - orientation = ('vertical' if vert else 'horizontal') + orientation = 'vertical' if vert else 'horizontal' if orientation == 'horizontal': x, bottom = bottom, x width, height = height, width @@ -1278,12 +1315,12 @@ def bar_wrapper( # sense do document here; figure out way to move it here? if left is not None: warnings._warn_proplot( - f'The "left" keyword with bar() is deprecated. Use "x" instead.' + 'The "left" keyword with bar() is deprecated. Use "x" instead.' ) x = left if x is None and height is None: raise ValueError( - f'bar() requires at least 1 positional argument, got 0.' + 'bar() requires at least 1 positional argument, got 0.' ) elif height is None: x, height = None, x @@ -1316,7 +1353,7 @@ def barh_wrapper( kwargs.setdefault('orientation', 'horizontal') if y is None and width is None: raise ValueError( - f'barh() requires at least 1 positional argument, got 0.' + 'barh() requires at least 1 positional argument, got 0.' ) return self.bar(x=left, height=height, width=width, bottom=y, **kwargs) @@ -1339,7 +1376,7 @@ def boxplot_wrapper( Note ---- - This function wraps %(methods)s. + This function wraps {methods} Parameters ---------- @@ -1355,7 +1392,7 @@ def boxplot_wrapper( The opacity of the boxes. Default is ``1``. lw, linewidth : float, optional The linewidth of all objects. - orientation : {None, 'horizontal', 'vertical'}, optional + orientation : {{None, 'horizontal', 'vertical'}}, optional Alternative to the native `vert` keyword arg. Controls orientation. marker : marker-spec, optional Marker style for the 'fliers', i.e. outliers. @@ -1444,7 +1481,7 @@ def violinplot_wrapper( Note ---- - This function wraps %(methods)s. + This function wraps {methods} Parameters ---------- @@ -1458,7 +1495,7 @@ def violinplot_wrapper( The violin plot fill color. Default is the next color cycler color. fillalpha : float, optional The opacity of the violins. Default is ``1``. - orientation : {None, 'horizontal', 'vertical'}, optional + orientation : {{None, 'horizontal', 'vertical'}}, optional Alternative to the native `vert` keyword arg. Controls orientation. boxrange, barrange : (float, float), optional Percentile ranges for the thick and thin central bars. The defaults @@ -1484,7 +1521,7 @@ def violinplot_wrapper( # Sanitize input lw = _not_none(lw=lw, linewidth=linewidth) if kwargs.pop('showextrema', None): - warnings._warn_proplot(f'Ignoring showextrema=True.') + warnings._warn_proplot('Ignoring showextrema=True.') if 'showmeans' in kwargs: kwargs.setdefault('means', kwargs.pop('showmeans')) if 'showmedians' in kwargs: @@ -1575,7 +1612,7 @@ def text_wrapper( Note ---- - This function wraps %(methods)s. + This function wraps {methods} Parameters ---------- @@ -1583,7 +1620,7 @@ def text_wrapper( The *x* and *y* coordinates for the text. text : str The text string. - transform : {'data', 'axes', 'figure'} or \ + transform : {{'data', 'axes', 'figure'}} or \ `~matplotlib.transforms.Transform`, optional The transform used to interpret `x` and `y`. Can be a `~matplotlib.transforms.Transform` object or a string representing the @@ -1660,7 +1697,7 @@ def cycle_changer( Note ---- This function wraps every method that uses the property cycler: - %(methods)s. + {methods} This wrapper also *standardizes acceptable input* -- these methods now all accept 2d arrays holding columns of data, and *x*-coordinates are always @@ -1711,23 +1748,29 @@ def cycle_changer( proplot.constructor.Cycle proplot.constructor.Colors """ - # Parse input - cycle_kw = cycle_kw or {} - legend_kw = legend_kw or {} - colorbar_kw = colorbar_kw or {} - - # Test input + # Parse input args # NOTE: Requires standardize_1d wrapper before reaching this. Also note # that the 'x' coordinates are sometimes ignored below. name = func.__name__ if not args: return func(self, *args, **kwargs) - barh = name == 'bar' and kwargs.get('orientation', None) == 'horizontal' x, y, *args = args - if len(args) >= 1 and 'fill_between' in name: + ys = (y,) + if len(args) >= 1 and name in ('fill_between', 'fill_betweenx'): ys, args = (y, args[0]), args[1:] - else: - ys = (y,) + barh = stacked = False + if name in ('pie',): # add x coordinates as default pie chart labels + kwargs['labels'] = _not_none(labels, x) # TODO: move to pie wrapper? + if name in ('bar', 'fill_between', 'fill_betweenx'): + stacked = kwargs.pop('stacked', False) + if name in ('bar',): + barh = kwargs.get('orientation', None) == 'horizontal' + width = kwargs.pop('width', 0.8) # 'width' for bar *and* barh (see bar_wrapper) + bottom = 'x' if barh else 'bottom' + kwargs.setdefault(bottom, 0) # 'x' required even though 'y' isn't for bar plots + cycle_kw = cycle_kw or {} + legend_kw = legend_kw or {} + colorbar_kw = colorbar_kw or {} # Determine and temporarily set cycler # NOTE: Axes cycle has no getter, only set_prop_cycle, which sets a @@ -1804,13 +1847,25 @@ def cycle_changer( if labels is None or isinstance(labels, str): labels = [labels] * ncols - # Handle stacked bar plots - stacked = kwargs.pop('stacked', False) + # Get step size for bar plots + # WARNING: This will fail for non-numeric non-datetime64 singleton + # datatypes but this is good enough for vast majority of most cases. if name in ('bar',): - width = kwargs.pop('width', 0.8) - kwargs['height' if barh else 'width'] = ( - width if stacked else width / ncols - ) + if not stacked: + x_test = np.atleast_1d(_to_ndarray(x)) + if len(x_test) >= 2: + x_step = x_test[1:] - x_test[:-1] + x_step = np.concatenate((x_step, x_step[-1:])) + elif x_test.dtype == np.datetime64: + x_step = np.timedelta64(1, 'D') + else: + x_step = np.array(0.5) + if np.issubdtype(x_test.dtype, np.datetime64): + # Avoid integer timedelta truncation + x_step = x_step.astype('timedelta64[ns]') + width = width * x_step / ncols + key = 'height' if barh else 'width' + kwargs[key] = width # Plot susccessive columns objs = [] @@ -1830,31 +1885,12 @@ def cycle_changer( key = 'edgecolors' kw[key] = value - # Add x coordinates as pi chart labels by default - if name in ('pie',): - kw['labels'] = _not_none(labels, x) # TODO: move to pie wrapper? - - # Step size for grouped bar plots - # WARNING: This will fail for non-numeric non-datetime64 singleton - # datatypes but this is good enough for vast majority of most cases. - x_test = np.atleast_1d(_to_ndarray(x)) - if len(x_test) >= 2: - x_step = x_test[1:] - x_test[:-1] - x_step = np.concatenate((x_step, x_step[-1:])) - elif x_test.dtype == np.datetime64: - x_step = np.timedelta64(1, 'D') - else: - x_step = np.array(0.5) - if np.issubdtype(x_test.dtype, np.datetime64): - x_step = x_step.astype('timedelta64[ns]') # avoid int timedelta truncation - # Get x coordinates for bar plot x_col, y_first = x, ys[0] # samples if name in ('bar',): # adjust if not stacked: - scale = i - 0.5 * (ncols - 1) # offset from true coordinate - scale = width * scale / ncols - x_col = x + x_step * scale + offset = width * (i - 0.5 * (ncols - 1)) + x_col = x + offset elif stacked and y_first.ndim > 1: key = 'x' if barh else 'bottom' kw[key] = _to_indexer(y_first)[:, :i].sum(axis=1) @@ -1869,7 +1905,7 @@ def cycle_changer( # WARNING: If stacked=True then we always *ignore* second # argument passed to fill_between. Warning should be issued # by fill_between_wrapper in this case. - if stacked and 'fill_between' in name: + if stacked and name in ('fill_between', 'fill_betweenx'): ys_col = tuple( y_first if y_first.ndim == 1 else _to_indexer(y_first)[:, :ii].sum(axis=1) @@ -1903,15 +1939,12 @@ def cycle_changer( # Build coordinate arguments x_ys_col = () - if barh: # special, use kwargs only! + if barh: # special case, use kwargs only! kw.update({'bottom': x_col, 'width': ys_col[0]}) - kw.setdefault('x', kwargs.get('bottom', 0)) # required elif name in ('pie', 'hist', 'boxplot', 'violinplot'): x_ys_col = ys_col else: # has x-coordinates, and maybe more than one y x_ys_col = (x_col, *ys_col) - - # Call plotting function obj = func(self, *x_ys_col, *args, **kw) if isinstance(obj, (list, tuple)) and len(obj) == 1: obj = obj[0] @@ -1958,7 +1991,7 @@ def cycle_changer( def _build_discrete_norm( - data=None, levels=None, values=None, + data=None, N=None, levels=None, values=None, norm=None, norm_kw=None, locator=None, locator_kw=None, cmap=None, vmin=None, vmax=None, extend='neither', symmetric=False, minlength=2, @@ -1986,6 +2019,18 @@ def _build_discrete_norm( ticks : `numpy.ndarray` or `matplotlib.locator.Locator` The axis locator or the tick location candidates. """ + # Parse flexible keyword args + norm_kw = norm_kw or {} + locator_kw = locator_kw or {} + levels = _not_none( + N=N, levels=levels, norm_kw_levels=norm_kw.pop('levels', None), + default=rc['image.levels'] + ) + vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop('vmin', None)) + vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop('vmax', None)) + if norm == 'segments': # TODO: remove + norm = 'segmented' + # NOTE: Matplotlib colorbar algorithm *cannot* handle descending levels # so this function reverses them and adds special attribute to the # normalizer. Then colorbar_wrapper reads this attribute and flips the @@ -2003,13 +2048,7 @@ def _build_discrete_norm( ) # Get level edges from level centers - from .config import rc ticks = None - levels = _not_none(levels, rc['image.levels']) - norm_kw = norm_kw or {} - locator_kw = locator_kw or {} - if norm == 'segments': # TODO: remove - norm = 'segmented' if isinstance(values, Number): levels = np.atleast_1d(values)[0] + 1 elif np.iterable(values) and len(values) == 1: @@ -2162,10 +2201,12 @@ def _build_discrete_norm( return norm, cmap, levels, ticks +@warnings._rename_kwargs(centers='values') +@docstring.add_snippets def cmap_changer( - self, func, *args, cmap=None, cmap_kw=None, - extend='neither', norm=None, norm_kw=None, - N=None, levels=None, values=None, centers=None, vmin=None, vmax=None, + self, func, *args, extend='neither', + cmap=None, cmap_kw=None, norm=None, norm_kw=None, + vmin=None, vmax=None, N=None, levels=None, values=None, symmetric=False, locator=None, locator_kw=None, edgefix=None, labels=False, labels_kw=None, fmt=None, precision=2, colorbar=False, colorbar_kw=None, @@ -2182,57 +2223,14 @@ def cmap_changer( Note ---- This function wraps every method that take a `cmap` argument: - %(methods)s. + {methods} Parameters ---------- - cmap : colormap spec, optional - The colormap specifer, passed to the `~proplot.constructor.Colormap` - constructor. - cmap_kw : dict-like, optional - Passed to `~proplot.constructor.Colormap`. - norm : normalizer spec, optional - The colormap normalizer, used to warp data before passing it - to `~proplot.colors.DiscreteNorm`. This is passed to the - `~proplot.constructor.Norm` constructor. - norm_kw : dict-like, optional - Passed to `~proplot.constructor.Norm`. - extend : {'neither', 'min', 'max', 'both'}, optional + extend : {{'neither', 'min', 'max', 'both'}}, optional Where to assign unique colors to out-of-bounds data and draw "extensions" (triangles, by default) on the colorbar. - levels, N : int or list of float, optional - The number of level edges, or a list of level edges. If the former, - `locator` is used to generate this many levels at "nice" intervals. - If the latter, the levels should be monotonically increasing or - decreasing (note that decreasing levels will only work with ``pcolor`` - plots, not ``contour`` plots). Default is :rc:`image.levels`. - - Since this function also wraps `~matplotlib.axes.Axes.pcolor` and - `~matplotlib.axes.Axes.pcolormesh`, this means they now - accept the `levels` keyword arg. You can now discretize your - colors in a ``pcolor`` plot just like with ``contourf``. - values, centers : int or list of float, optional - The number of level centers, or a list of level centers. If provided, - levels are inferred using `~proplot.utils.edges`. This will override - any `levels` input. - symmetric : bool, optional - If ``True``, auto-generated levels are symmetric about zero. - vmin, vmax : float, optional - Used to determine level locations if `levels` is an integer. Actual - levels may not fall exactly on `vmin` and `vmax`, but the minimum - level will be no smaller than `vmin` and the maximum level will be - no larger than `vmax`. - - If `vmin` or `vmax` is not provided, the minimum and maximum data - values are used. - locator : locator-spec, optional - The locator used to determine level locations if `levels` or `values` - is an integer and `vmin` and `vmax` were not provided. Passed to the - `~proplot.constructor.Locator` constructor. Default is - `~matplotlib.ticker.MaxNLocator` with ``levels`` or ``values+1`` - integer levels. - locator_kw : dict-like, optional - Passed to `~proplot.constructor.Locator`. + %(cmap_changer.params)s edgefix : bool, optional Whether to fix the the `white-lines-between-filled-contours \ `__ @@ -2325,19 +2323,12 @@ def cmap_changer( # Flexible user input Z_sample = args[-1] - vmin = _not_none(vmin=vmin, norm_kw_vmin=norm_kw.pop('vmin', None)) - vmax = _not_none(vmax=vmax, norm_kw_vmax=norm_kw.pop('vmax', None)) - values = _not_none(values=values, centers=centers) edgefix = _not_none(edgefix, rc['image.edgefix']) linewidths = _not_none(lw=lw, linewidth=linewidth, linewidths=linewidths) linestyles = _not_none(ls=ls, linestyle=linestyle, linestyles=linestyles) colors = _not_none( color=color, colors=colors, edgecolor=edgecolor, edgecolors=edgecolors, ) - levels = _not_none( - N=N, levels=levels, norm_kw_levels=norm_kw.pop('levels', None), - default=rc['image.levels'] - ) # Get colormap, but do not use cmap when 'colors' are passed to contour() # or to contourf() -- the latter only when 'linewidths' and 'linestyles' @@ -2405,7 +2396,7 @@ def cmap_changer( if cmap is not None and name not in ('hexbin',): norm, cmap, levels, ticks = _build_discrete_norm( Z_sample, # sample data for getting suitable levels - levels=levels, values=values, + N=N, levels=levels, values=values, norm=norm, norm_kw=norm_kw, locator=locator, locator_kw=locator_kw, cmap=cmap, vmin=vmin, vmax=vmax, extend=extend, @@ -2769,7 +2760,7 @@ def legend_wrapper( ) if overridden: warnings._warn_proplot( - f'Ignoring user input properties ' + 'Ignoring user input properties ' + ', '.join(map(repr, overridden)) + ' for centered-row legend.' ) @@ -2789,7 +2780,7 @@ def legend_wrapper( ymin, ymax = None, None if order == 'F': raise NotImplementedError( - f'When center=True, ProPlot vertically stacks successive ' + 'When center=True, ProPlot vertically stacks successive ' 'single-row legends. Column-major (order="F") ordering ' 'is un-supported.' ) @@ -3022,7 +3013,7 @@ def colorbar_wrapper( The font size, weight, and color for colorbar label text. ticklabelsize, ticklabelweight, ticklabelcolor : optional The font size, weight, and color for colorbar tick labels. - orientation : {'horizontal', 'vertical'}, optional + orientation : {{'horizontal', 'vertical'}}, optional The colorbar orientation. You should not have to explicitly set this. Other parameters @@ -3353,6 +3344,7 @@ def _redirect(func): be applied on the base axes class, not the basemap axes. """ name = func.__name__ + @functools.wraps(func) def _wrapper(self, *args, **kwargs): if getattr(self, 'name', '') == 'basemap': @@ -3370,6 +3362,7 @@ def _norecurse(func): """ name = func.__name__ func._has_recurred = False + @functools.wraps(func) def _wrapper(self, *args, **kwargs): if func._has_recurred: @@ -3412,7 +3405,7 @@ def _wrapper(self, *args, **kwargs): # Prevents us from having to both explicitly apply decorators in # axes.py and explicitly list functions *again* in this file docstring = driver._docstring_orig - if '%(methods)s' in docstring: + if '{methods}' in docstring: if name in proplot_methods: link = f'`~proplot.axes.Axes.{name}`' elif name in cartopy_methods: @@ -3427,7 +3420,7 @@ def _wrapper(self, *args, **kwargs): + ',' * int(len(methods) > 2) # Oxford comma bitches + ' and ' * int(len(methods) > 1) + methods[-1]) - driver.__doc__ = docstring % {'methods': string} + driver.__doc__ = docstring.format(methods=string) return _wrapper return decorator diff --git a/proplot/axes/polar.py b/proplot/axes/polar.py index 7f31d2f9e..aa9231650 100644 --- a/proplot/axes/polar.py +++ b/proplot/axes/polar.py @@ -17,9 +17,8 @@ class PolarAxes(base.Axes, mproj.PolarAxes): """ - Axes subclass for plotting in polar coordinates. - Adds the `~PolarAxes.format` method and overrides several existing - methods. + Axes subclass for plotting in polar coordinates. Adds the `~PolarAxes.format` + method and overrides several existing methods. """ #: The registered projection name. name = 'polar2' diff --git a/proplot/colors.py b/proplot/colors.py index c6c9ed379..a01c29bdd 100644 --- a/proplot/colors.py +++ b/proplot/colors.py @@ -1097,7 +1097,7 @@ def to_listed(self, samples=10, **kwargs): if isinstance(samples, Integral): samples = np.linspace(0, 1, samples) elif not np.iterable(samples): - raise TypeError(f'Samples must be integer or iterable.') + raise TypeError('Samples must be integer or iterable.') samples = np.asarray(samples) colors = self(samples) kwargs.setdefault('name', self.name) @@ -1818,7 +1818,7 @@ def __init__( if not norm: norm = mcolors.Normalize() elif isinstance(norm, mcolors.BoundaryNorm): - raise ValueError(f'Normalizer cannot be instance of BoundaryNorm.') + raise ValueError('Normalizer cannot be instance of BoundaryNorm.') elif not isinstance(norm, mcolors.Normalize): raise ValueError('Normalizer must be instance of Normalize.') extend = extend or 'neither' diff --git a/proplot/constructor.py b/proplot/constructor.py index ecc170f81..e3c473f11 100644 --- a/proplot/constructor.py +++ b/proplot/constructor.py @@ -264,7 +264,7 @@ def _mod_colormap(cmap, *, cut, left, right, shift, reverse, samples): if isinstance(cmap, pcolors.ListedColormap): if cut is not None: warnings._warn_proplot( - f"Invalid argument 'cut' for ListedColormap. Ignoring." + "Invalid argument 'cut' for ListedColormap. Ignoring." ) cmap = cmap.truncate(left=left, right=right) else: @@ -304,7 +304,7 @@ def Colormap( `~proplot.colors.ListedColormap`. Used to interpret the `cmap` and `cmap_kw` arguments when passed to any plotting method wrapped by - `~proplot.wrappers.cmap_changer`. + `~proplot.axes.cmap_changer`. Parameters ---------- @@ -415,7 +415,7 @@ def Colormap( # how to make colormaps cyclic. if not args: raise ValueError( - f'Colormap() requires at least one positional argument.' + 'Colormap() requires at least one positional argument.' ) if listmode not in ('listed', 'linear', 'perceptual'): raise ValueError( @@ -579,7 +579,7 @@ def Cycle( """ Generate and merge `~cycler.Cycler` instances in a variety of ways. Used to interpret the `cycle` and `cycle_kw` arguments when passed to - any plotting method wrapped by `~proplot.wrappers.cycle_changer`. + any plotting method wrapped by `~proplot.axes.cycle_changer`. If you just want a list of colors instead of a `~cycler.Cycler` instance, use the `Colors` function. If you want a `~cycler.Cycler` instance that @@ -723,7 +723,7 @@ def Norm(norm, *args, **kwargs): """ Return an arbitrary `~matplotlib.colors.Normalize` instance. Used to interpret the `norm` and `norm_kw` arguments when passed to any plotting - method wrapped by `~proplot.wrappers.cmap_changer`. See + method wrapped by `~proplot.axes.cmap_changer`. See `this tutorial \ `__ for more info. @@ -790,7 +790,7 @@ def Locator(locator, *args, **kwargs): `yminorlocator_kw` arguments when passed to `~proplot.axes.CartesianAxes.format`, and the `locator`, `locator_kw`, `minorlocator`, and `minorlocator_kw` arguments when passed to colorbar - methods wrapped by `~proplot.wrappers.colorbar_wrapper`. + methods wrapped by `~proplot.axes.colorbar_wrapper`. Parameters ---------- @@ -894,7 +894,7 @@ def Formatter(formatter, *args, date=False, index=False, **kwargs): `yformatter_kw` arguments when passed to `~proplot.axes.CartesianAxes.format`, and the `formatter` and `formatter_kw` arguments when passed to colorbar methods wrapped by - `~proplot.wrappers.colorbar_wrapper`. + `~proplot.axes.colorbar_wrapper`. Parameters ---------- @@ -1305,7 +1305,7 @@ def Proj(name, basemap=None, **kwargs): import mpl_toolkits.basemap as mbasemap if _version_mpl >= _version('3.3'): raise RuntimeError( - f'Basemap is no longer maintained and is incompatible with ' + 'Basemap is no longer maintained and is incompatible with ' 'matplotlib >= 3.3. Please use cartopy as your cartographic ' 'plotting backend or downgrade to matplotlib <=3.2.' ) @@ -1333,7 +1333,7 @@ def Proj(name, basemap=None, **kwargs): kwproj.pop('central_latitude', None) if 'boundinglat' in kwproj: raise ValueError( - f'"boundinglat" must be passed to the ax.format() command ' + '"boundinglat" must be passed to the ax.format() command ' 'for cartopy axes.' ) if crs is None: diff --git a/proplot/demos.py b/proplot/demos.py index 5afa13037..8beefd968 100644 --- a/proplot/demos.py +++ b/proplot/demos.py @@ -279,7 +279,7 @@ def show_channels( """ # Figure and plot if not args: - raise ValueError(f'At least one positional argument required.') + raise ValueError('At least one positional argument required.') array = [[1, 1, 2, 2, 3, 3]] labels = ('Hue', 'Chroma', 'Luminance') if saturation: diff --git a/proplot/figure.py b/proplot/figure.py index 7f1298496..ac35ffeba 100644 --- a/proplot/figure.py +++ b/proplot/figure.py @@ -12,7 +12,7 @@ from .config import rc from .utils import units from .internals import ic # noqa: F401 -from .internals import warnings, _not_none, _set_state +from .internals import warnings, _not_none, _dummy_context, _set_state __all__ = ['Figure'] @@ -77,11 +77,13 @@ def _preprocess(self, *args, **kwargs): self.draw() # Bail out if we are already pre-processing - # The return value for macosx _draw is the renderer, for qt draw is + # NOTE: The _is_autoresizing check necessary when inserting new gridspec + # rows or columns with the qt backend. + # NOTE: Return value for macosx _draw is the renderer, for qt draw is # nothing, and for print_figure is some figure object, but this block # has never been invoked when calling print_figure. renderer = fig._get_renderer() # any renderer will do for now - if fig._is_preprocessing: + if fig._is_autoresizing or fig._is_preprocessing: if method == '_draw': # macosx backend return renderer else: @@ -230,6 +232,8 @@ def __init__( **kwargs Passed to `matplotlib.figure.Figure`. """ # noqa + # Initialize first, because need to provide fully initialized figure + # as argument to gridspec, because matplotlib tight_layout does that tight_layout = kwargs.pop('tight_layout', None) constrained_layout = kwargs.pop('constrained_layout', None) if tight_layout or constrained_layout: @@ -237,14 +241,11 @@ def __init__( f'Ignoring tight_layout={tight_layout} and ' f'contrained_layout={constrained_layout}. ProPlot uses its ' 'own tight layout algorithm, activated by default or with ' - 'tight=True.' + 'plot.subplots(tight=True).' ) - - # Initialize first, because need to provide fully initialized figure - # as argument to gridspec, because matplotlib tight_layout does that self._authorized_add_subplot = False self._is_preprocessing = False - self._is_resizing = False + self._is_autoresizing = False super().__init__(**kwargs) # Axes sharing and spanning settings @@ -253,9 +254,9 @@ def __init__( spanx = _not_none(spanx, span, 0 if sharex == 0 else None, rc['span']) spany = _not_none(spany, span, 0 if sharey == 0 else None, rc['span']) if spanx and (alignx or align): - warnings._warn_proplot(f'"alignx" has no effect when spanx=True.') + warnings._warn_proplot('"alignx" has no effect when spanx=True.') if spany and (aligny or align): - warnings._warn_proplot(f'"aligny" has no effect when spany=True.') + warnings._warn_proplot('"aligny" has no effect when spany=True.') alignx = _not_none(alignx, align, rc['align']) aligny = _not_none(aligny, align, rc['align']) self.set_alignx(alignx) @@ -330,7 +331,7 @@ def _add_axes_panel(self, ax, side, filled=False, **kwargs): idx2 += 1 # Draw and setup panel - with self._authorize_add_subplot(): + with self._context_authorize_add_subplot(): pax = self.add_subplot(gridspec[idx1, idx2], projection='cartesian') # noqa: E501 pgrid.append(pax) pax._panel_side = side @@ -439,7 +440,7 @@ def _add_figure_panel( ) # Draw and setup panel - with self._authorize_add_subplot(): + with self._context_authorize_add_subplot(): pax = self.add_subplot(gridspec[idx1, idx2], projection='cartesian') # noqa: E501 pgrid = getattr(self, '_' + side + '_panels') pgrid.append(pax) @@ -482,8 +483,8 @@ def _align_labels_axis(self, b=True): grp.join(axs[0], ax) elif align: warnings._warn_proplot( - f'Aligning *x* and *y* axis labels required ' - f'matplotlib >=3.1.0' + 'Aligning *x* and *y* axis labels required ' + 'matplotlib >=3.1.0' ) if not span: continue @@ -611,19 +612,19 @@ def _align_labels_figure(self, renderer): } suptitle.update(kw) - def _authorize_add_subplot(self): + def _context_authorize_add_subplot(self): """ Prevent warning message when adding subplots one-by-one. Used internally. """ return _set_state(self, _authorized_add_subplot=True) - def _context_resizing(self): + def _context_autoresizing(self): """ Ensure backend calls to `~matplotlib.figure.Figure.set_size_inches` during pre-processing are not interpreted as *manual* resizing. """ - return _set_state(self, _is_resizing=True) + return _set_state(self, _is_autoresizing=True) def _context_preprocessing(self): """ @@ -753,30 +754,24 @@ def _insert_row_column( spaces = subplots_kw[w + 'space'] spaces_orig = subplots_orig_kw[w + 'space'] - # Slot already exists + # Adjust space, ratio, and panel indicator arrays slot_type = 'f' if figure else side[0] slot_exists = idx not in (-1, len(panels)) and panels[idx] == slot_type - if slot_exists: # already exists! + if slot_exists: + # Slot already exists if spaces_orig[idx_space] is None: spaces_orig[idx_space] = units(space_orig) spaces[idx_space] = _not_none(spaces_orig[idx_space], space) - # Make room for new panel slot else: - # Modify basic geometry + # Modify basic geometry and insert new slot idx += idx_offset idx_space += idx_offset subplots_kw[ncols] += 1 - # Original space, ratio array, space array, panel toggles spaces_orig.insert(idx_space, space_orig) spaces.insert(idx_space, space) ratios.insert(idx, ratio) panels.insert(idx, slot_type) - # Reference ax location array - # TODO: For now do not need to increment, but need to double - # check algorithm for fixing axes aspect! - # ref = subplots_kw[x + 'ref'] - # ref[:] = [val + 1 if val >= idx else val for val in ref] # Update figure figsize, gridspec_kw, _ = pgridspec._calc_geometry(**subplots_kw) @@ -788,7 +783,9 @@ def _insert_row_column( else: # Make new gridspec gridspec = pgridspec.GridSpec(self, **gridspec_kw) + self._gridspec_main.figure = None self._gridspec_main = gridspec + # Reassign subplotspecs to all axes and update positions for ax in self._iter_axes(hidden=True, children=True): # Get old index @@ -800,26 +797,24 @@ def _insert_row_column( else: inserts = (idx, idx, None, None) subplotspec = ax.get_subplotspec() - igridspec = subplotspec.get_gridspec() - topmost = subplotspec.get_topmost_subplotspec() + gridspec_ss = subplotspec.get_gridspec() + subplotspec_top = subplotspec.get_topmost_subplotspec() # Apply new subplotspec - _, _, *coords = topmost.get_active_rows_columns() + _, _, *coords = subplotspec_top.get_active_rows_columns() for i in range(4): if inserts[i] is not None and coords[i] >= inserts[i]: coords[i] += 1 row1, row2, col1, col2 = coords subplotspec_new = gridspec[row1:row2 + 1, col1:col2 + 1] - if topmost is subplotspec: + if subplotspec_top is subplotspec: ax.set_subplotspec(subplotspec_new) - elif topmost is igridspec._subplot_spec: - igridspec._subplot_spec = subplotspec_new + elif subplotspec_top is gridspec_ss._subplot_spec: + gridspec_ss._subplot_spec = subplotspec_new else: raise ValueError( - f'Unexpected GridSpecFromSubplotSpec nesting.' + 'Unexpected GridSpecFromSubplotSpec nesting.' ) - - # Update parent or child position ax.update_params() ax.set_position(ax.figbox) @@ -1094,7 +1089,7 @@ def colorbar( Other parameters ---------------- *args, **kwargs - Passed to `~proplot.wrappers.colorbar_wrapper`. + Passed to `~proplot.axes.colorbar_wrapper`. """ ax = kwargs.pop('ax', None) cax = kwargs.pop('cax', None) @@ -1175,7 +1170,7 @@ def legend( Other parameters ---------------- *args, **kwargs - Passed to `~proplot.wrappers.legend_wrapper`. + Passed to `~proplot.axes.legend_wrapper`. """ ax = kwargs.pop('ax', None) # Generate axes panel @@ -1205,9 +1200,8 @@ def set_canvas(self, canvas): # `~matplotlib.backend_bases.FigureCanvasBase.draw_idle` and # `~matplotlib.backend_bases.FigureCanvasBase.print_figure` # methods. The latter is called by save() and by the inline backend. - # See `_canvas_preprocessor` for details.""" + # See `_canvas_preprocessor` for details. # TODO: Concatenate docstrings. - # TODO: Figure out matplotlib>=3.3 bug with macos backend. # NOTE: Cannot use draw_idle() because it causes complications for qt5 # backend (wrong figure size). if callable(getattr(canvas, '_draw', None)): # for macos backend @@ -1230,6 +1224,9 @@ def set_size_inches(self, w, h=None, forward=True, auto=False): # renderer calls set_size_inches, size may be effectively the same, but # slightly changed due to roundoff error! Therefore, always compare to # *both* get_size_inches() and the truncated bbox dimensions times dpi. + # NOTE: If we fail to detect 'manual' resize as manual, not only will + # result be incorrect, but qt backend will crash because it detects a + # recursive size change, since preprocessor size will differ. if h is None: width, height = w else: @@ -1241,20 +1238,18 @@ def set_size_inches(self, w, h=None, forward=True, auto=False): width_true, height_true = self.get_size_inches() width_trunc = int(self.bbox.width) / self.dpi height_trunc = int(self.bbox.height) / self.dpi - if auto: # internal resizing not associated with any draws - with self._context_resizing(): - super().set_size_inches(width, height, forward=forward) - else: # manual resizing on behalf of user - if ( - ( - width not in (width_true, width_trunc) - or height not in (height_true, height_trunc) - ) - and not self._is_resizing - and not self.canvas._is_idle_drawing # standard - and not getattr(self.canvas, '_draw_pending', None) # pyqt5 - ): - self._subplots_kw.update(width=width, height=height) + if ( + ( + width not in (width_true, width_trunc) + or height not in (height_true, height_trunc) + ) + and not auto + and not self._is_autoresizing + and not getattr(self.canvas, '_is_idle_drawing', None) # standard + ): + self._subplots_kw.update(width=width, height=height) + context = self._context_autoresizing if auto else _dummy_context + with context(): super().set_size_inches(width, height, forward=forward) def get_alignx(self): diff --git a/proplot/ui.py b/proplot/ui.py index f6efa6c7f..58579dca6 100644 --- a/proplot/ui.py +++ b/proplot/ui.py @@ -486,7 +486,7 @@ def subplots( y0, y1 = yrange[idx, 0], yrange[idx, 1] # Draw subplot subplotspec = gridspec[y0:y1 + 1, x0:x1 + 1] - with fig._authorize_add_subplot(): + with fig._context_authorize_add_subplot(): axs[idx] = fig.add_subplot( subplotspec, number=num, main=True, **axes_kw[num] diff --git a/proplot/utils.py b/proplot/utils.py index 78680285f..bb0d1647c 100644 --- a/proplot/utils.py +++ b/proplot/utils.py @@ -93,7 +93,7 @@ def edges(Z, axis=-1): you supply centers to `~matplotlib.axes.Axes.pcolor` or `~matplotlib.axes.Axes.pcolormesh`. It is also used to calculate colormap level boundaries when you supply centers to plotting methods wrapped by - `~proplot.wrappers.cmap_changer`. + `~proplot.axes.cmap_changer`. Parameters ----------