From 4445ab245d63f2baeaee9c2ba3af175d9682a98f Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Thu, 8 Jul 2021 17:33:22 -0600 Subject: [PATCH 01/11] Nicer default font sizes --- docs/_static/custom.css | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/_static/custom.css b/docs/_static/custom.css index 8d11dfcc1..cebfb7159 100644 --- a/docs/_static/custom.css +++ b/docs/_static/custom.css @@ -112,8 +112,16 @@ h5{ h6{ font-size: 100%; } +.rst-content dl:not(.docutils) dt, +.rst-content dl:not(.docutils) .field-list>dt { + font-size: 100%; /* default is 90% */ +} .wy-menu-vertical header, .wy-menu-vertical p.caption { - font-size: 90%; /* default is 85% */ + font-size: 100%; /* default is 85% */ + padding: 0 1.4em; /* fix alignment */ +} +.wy-menu-vertical a { + padding: 0.4em 1.6em /* mostly unchanged */ } /* RST content background color */ @@ -298,8 +306,8 @@ code, /* Parameters and Returns header colors */ .rst-content dl:not(.docutils) .field-list>dt { - font-weight: bold; color: var(--main-color); + font-weight: bold; background: var(--block-bg-color); border-left-color: var(--accent-bg-color); /* padding: 0; */ /* remove accents, decided against this */ From 00d5a862115c838f07a9e820378eb5506833b305 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Thu, 8 Jul 2021 19:49:09 -0600 Subject: [PATCH 02/11] Fix outdated links and cleanup api docs --- WHATSNEW.rst | 17 ++++++++++++++--- docs/axis.py | 6 +++--- docs/basics.py | 4 ++-- docs/configuration.rst | 2 +- docs/cycles.py | 2 +- docs/fonts.py | 2 +- docs/insets_panels.py | 2 +- docs/projections.py | 4 ++-- docs/subplots.py | 4 ++-- docs/why.rst | 16 ++++++++-------- proplot/axes/base.py | 19 ++++++++----------- proplot/axes/cartesian.py | 8 ++++---- proplot/axes/geo.py | 19 +++++++++---------- proplot/axes/plot.py | 14 +++++++------- proplot/axes/polar.py | 3 +-- proplot/colors.py | 3 +-- proplot/config.py | 2 +- proplot/constructor.py | 11 +++++------ proplot/demos.py | 16 ++++++++-------- proplot/figure.py | 6 +++--- proplot/internals/rcsetup.py | 6 +++--- proplot/scale.py | 33 +++++++++++++++------------------ proplot/ui.py | 5 +++-- 23 files changed, 103 insertions(+), 101 deletions(-) diff --git a/WHATSNEW.rst b/WHATSNEW.rst index a9d81ddc1..c8d6edda8 100644 --- a/WHATSNEW.rst +++ b/WHATSNEW.rst @@ -112,8 +112,8 @@ ProPlot v0.7.0 (2021-07-##) * When using ``medians=True`` or ``means=True`` with `indicate_error` plot simple error bars by default instead of bars and "boxes" (:commit:`4e30f415`). Only plot "boxes" with central "markers" by default for violin plots (:commit:`13b45ccd`). -* `legend_wrapper` no longer returns the background patch generated for centered-row - legends (:pr:`254`). This is consistent with `colorbar_wrapper` not returning +* `legend_extras` no longer returns the background patch generated for centered-row + legends (:pr:`254`). This is consistent with `colorbar_extras` not returning background patches generated for inset colorbars. Until proplot adds new subclasses, it makes more sense if these functions only return `~matplotlib.legend.Legend` and `~matplotlib.colorbar.Colorbar` instances. @@ -129,8 +129,16 @@ ProPlot v0.7.0 (2021-07-##) * Add `wequal`, `hequal`, and `equal` options to still use automatic spacing but force the tight layout algorithm to make spacings equal (:pr:`215`, :issue:`64`) by `Zachary Moon`_. -* Add baseline support for "3D" `~matplotlib.mpl_toolkits.mplot3d.Axes3D` axes +* Add minimal support for "3D" `~matplotlib.mpl_toolkits.mplot3d.Axes3D` axes (:issue:`249`). Example usage: ``fig.subplots(proj='3d')``. +* Add `~proplot.axes.Axes.plotx` and `~proplot.axes.Axes.scatterx` commands that + interpret plotting args as ``(y, x)`` rather than ``(x, y)``, analogous to + `~proplot.axes.Axes.areax` (:commit:`###`). +* Add support for `~proplot.axes.indicate_error` *horizontal* error bars and + shading for line and scatter plots (:commit:`###`). +* Add support for ``ax.plot_command('x_key', 'y_key', data=dataset)`` for + virtually all plotting commands using `standardize_1d` and `standardize_2d` + (:commit:`###`). This was an existing `~matplotlib.axes.Axes.plot` feature. * Allow updating axes fonts that use scalings like ``'small'`` and ``'large'`` by passing ``fontsize=N`` to `format` (:issue:`212`). * Interpret fontsize-relative legend rc params like ``legend.borderpad`` @@ -184,6 +192,9 @@ ProPlot v0.7.0 (2021-07-##) ``'a'`` or ``'A'`` in string, and only replace that one (:issue:`201`). * Allow passing e.g. ``barstds=3`` or ``barpctiles=90`` to request error bars denoting +/-3 standard deviations and 5-95 percentile range (:commit:`4e30f415`). +* Add singular `indicate_error` keywords `barstd`, `barpctile`, etc. as + alternatives to `barstds`, `barpctiles`, etc. (:commit:`81151a58`). + Also prefer them in the documentation. * Allow passing ``means=True`` to `boxplot` to toggle mean line (:commit:`4e30f415`). * Allow setting the mean and median boxplot linestyle with diff --git a/docs/axis.py b/docs/axis.py index 896badfef..44cb51ec3 100644 --- a/docs/axis.py +++ b/docs/axis.py @@ -31,7 +31,7 @@ # -------------- # # `Tick locators\ -# `__ +# `__ # are used to automatically select sensible tick locations # based on the axis data limits. In ProPlot, you can change the tick locator # using the `~proplot.axes.Axes.format` keyword arguments `xlocator`, @@ -126,7 +126,7 @@ # ----------- # # `Tick formatters\ -# `__ +# `__ # are used to convert floating point numbers to # nicely-formatted tick labels. In ProPlot, you can change the tick formatter # using the `~proplot.axes.Axes.format` keyword arguments `xformatter` and @@ -493,7 +493,7 @@ # In the below examples, we generate dual axes with each of these three methods. 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 `__. +# `symlog scale `__. # %% import proplot as plot diff --git a/docs/basics.py b/docs/basics.py index 5607b3101..0233013f1 100644 --- a/docs/basics.py +++ b/docs/basics.py @@ -153,7 +153,7 @@ # -------------- # # Matplotlib has -# `two different interfaces `__: +# `two different interfaces `__: # an object-oriented interface and a MATLAB-style `~matplotlib.pyplot` interface # (which uses the object-oriented interface internally). Plotting with ProPlot is # just like plotting with matplotlib's *object-oriented* interface. Proplot builds @@ -321,7 +321,7 @@ # :ref:`ProPlot settings `. `~proplot.config.rc` also # provides a ``style`` parameter that can be used to switch between # `matplotlib stylesheets\ -# `__. +# `__. # See the :ref:`configuration section ` for details. # # To modify a setting for just one subplot, you can pass it to the diff --git a/docs/configuration.rst b/docs/configuration.rst index b1a389610..86340f1f4 100644 --- a/docs/configuration.rst +++ b/docs/configuration.rst @@ -98,7 +98,7 @@ The .proplotrc file When you install ProPlot for the first time, a ``.proplotrc`` file is generated and placed in your home directory. This is just like the `matplotlibrc file\ -`__, +`__, but for changing both ProPlot *and* matplotlib settings. The syntax is basically the same as the ``matplotlibrc`` syntax. diff --git a/docs/cycles.py b/docs/cycles.py index 9bd70c560..be5399613 100644 --- a/docs/cycles.py +++ b/docs/cycles.py @@ -26,7 +26,7 @@ # instances so that they can be `used with categorical data\ # `__. # Much more commonly, we build `property cycles\ -# `__ +# `__ # from the `~proplot.colors.ListedColormap` colors using the # `~proplot.constructor.Cycle` constructor function or by # :ref:`drawing samples ` from continuous colormaps. diff --git a/docs/fonts.py b/docs/fonts.py index fde94b5ba..036cfe36c 100644 --- a/docs/fonts.py +++ b/docs/fonts.py @@ -32,7 +32,7 @@ # # Matplotlib provides a `~matplotlib.font_manager` module for working with # system fonts and classifies fonts into `five font families\ -# `__: +# `__: # :rcraw:`font.serif` :rcraw:`font.sans-serif`, :rcraw:`font.monospace`, # :rcraw:`font.cursive`, and :rcraw:`font.fantasy`. The default font family # is sans-serif, because sans-serif fonts are generally more suitable for diff --git a/docs/insets_panels.py b/docs/insets_panels.py index 8f303bb8f..1dbe8546d 100644 --- a/docs/insets_panels.py +++ b/docs/insets_panels.py @@ -115,7 +115,7 @@ # ---------- # # `Inset axes\ -# `__ +# `__ # can be generated with the `~proplot.axes.Axes.inset` or # `~proplot.axes.Axes.inset_axes` command. By default, inset axes # have the same projection as the parent axes, but you can also request diff --git a/docs/projections.py b/docs/projections.py index e67679e5c..fe8aaae39 100644 --- a/docs/projections.py +++ b/docs/projections.py @@ -15,7 +15,7 @@ # %% [raw] raw_mimetype="text/restructuredtext" # -# .. _polar: https://matplotlib.org/3.1.0/gallery/pie_and_polar_charts/polar_demo.html +# .. _polar: https://matplotlib.org/stable/gallery/pie_and_polar_charts/polar_demo.html # # .. _cartopy: https://scitools.org.uk/cartopy/docs/latest/ # @@ -241,7 +241,7 @@ # `~mpl_toolkits.basemap.addcyclic`. # # Geographic features can be drawn underneath data or on top of data by changing the -# corresponding `zorder `__ +# corresponding `zorder `__ # setting. For example, to draw land patches on top of all plotted content as # a "land mask," use ``ax.format(land=True, landzorder=4)`` or set :rcraw:`land.zorder` # to ``True``. See the :ref:`next section ` for details. diff --git a/docs/subplots.py b/docs/subplots.py index 982fd261a..f27140b37 100644 --- a/docs/subplots.py +++ b/docs/subplots.py @@ -63,7 +63,7 @@ # `nrows` arguments), the arguments `refaspect`, `refwidth`, and `refheight` # apply to every subplot in the figure -- not just the reference subplot. # * When the reference subplot `aspect ratio\ -# `__ +# `__ # has been fixed (e.g., using ``ax.set_aspect(1)``) or is set to ``'equal'`` # (as with :ref:`geographic projections ` and # `~matplotlib.axes.Axes.imshow` images), the fixed aspect ratio is used @@ -157,7 +157,7 @@ # subplot rows and columns and the figure edge to accommodate labels. # It can be disabled by passing ``tight=False`` to `~proplot.ui.subplots`. # While matplotlib has `its own tight layout algorithm -# `__, +# `__, # ProPlot's algorithm may change the figure size to accommodate the correct spacing # and permits variable spacing between subsequent subplot rows and columns (see the # new `~proplot.gridspec.GridSpec` class for details). diff --git a/docs/why.rst b/docs/why.rst index 1178b6acd..9420cfbd0 100644 --- a/docs/why.rst +++ b/docs/why.rst @@ -142,11 +142,11 @@ seconds, typing out these extra class names and import statements can be a major Parts of matplotlib's interface were actually designed with this in mind. `Backend classes `__, -`native axes projections `__, -`axis scales `__, -`box styles `__, -`arrow styles `__, -and `arc styles `__ +`native axes projections `__, +`axis scales `__, +`box styles `__, +`arrow styles `__, +and `arc styles `__ are referenced with "registered" string names, as are `basemap projections `__. So, why not "register" everything else? @@ -208,7 +208,7 @@ figure, despite the fact that... number of subplot tiles in the figure. Also, while matplotlib's `tight layout algorithm -`__ +`__ helps to avoid tweaking the *spacing*, the algorithm cannot apply different amounts of spacing between different subplot row and column boundaries. @@ -218,7 +218,7 @@ In ProPlot, you can specify the physical dimensions of a *reference subplot* instead of the figure by passing `refwidth`, `refheight`, and/or `refaspect` to `~proplot.figure.Figure`. The default behavior is ``refaspect=1`` and ``refwidth=2`` (inches). If the `aspect ratio mode -`__ +`__ for the reference subplot is set to ``'equal'``, as with :ref:`geographic and polar ` plots and `~matplotlib.axes.Axes.imshow` plots, the *imposed* aspect ratio will be used instead. @@ -331,7 +331,7 @@ difficult to draw "inset" colorbars in matplotlib. .. The matplotlib example for `~matplotlib.figure.Figure` legends is `not pretty - `__. + `__. .. Drawing colorbars and legends is pretty clumsy in matplotlib -- especially diff --git a/proplot/axes/base.py b/proplot/axes/base.py index eba7efd13..d74326eb9 100644 --- a/proplot/axes/base.py +++ b/proplot/axes/base.py @@ -89,8 +89,7 @@ ---------- bounds : list of float The bounds for the inset axes, listed as ``(x, y, width, height)``. -transform : {'data', 'axes', 'figure'} or \ -`~matplotlib.transforms.Transform`, optional +transform : {'data', 'axes', 'figure'} or `~matplotlib.transforms.Transform`, optional The transform used to interpret `bounds`. Can be a `~matplotlib.transforms.Transform` object or a string representing the `~matplotlib.axes.Axes.transData`, @@ -113,7 +112,7 @@ Whether to use `~mpl_toolkits.basemap.Basemap` or `~cartopy.crs.Projection` for map projections. Default is ``False``. zorder : float, optional - The `zorder `__ + The `zorder `__ of the axes, should be greater than the zorder of elements in the parent axes. Default is ``4``. zoom : bool, optional @@ -728,8 +727,8 @@ def format( lower right inside axes ``'lower right'``, ``'lr'`` ======================== ============================ - ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, \ -lrtitle : str, optional + ltitle, ctitle, rtitle, ultitle, uctitle, urtitle, lltitle, lctitle, lrtitle \ +: str, optional Axes titles in specific positions. Works as an alternative to ``ax.format(title='title', titleloc='loc')`` and lets you specify multiple title-like labels in a single subplot. @@ -749,8 +748,7 @@ def format( titleabove : bool, optional Whether to try to put outer titles and a-b-c labels above panels, colorbars, or legends that are above the axes. Default is :rc:`title.above`. - leftlabels, toplabels, rightlabels, bottomlabels : list of str, \ -optional + leftlabels, toplabels, rightlabels, bottomlabels : list of str, optional Labels for the subplots lying along the left, top, right, and bottom edges of the figure. The length of each list must match the number of subplots along the corresponding edge. @@ -1184,9 +1182,8 @@ def indicate_inset_zoom( capstyle : {'butt', 'round', 'projecting'} The cap style for the zoom lines and box outline. zorder : float, optional - The `zorder \ -`__ - of the axes, should be greater than the zorder of + The `zorder `__ + of the zoom lines. Should be greater than the zorder of elements in the parent axes. Default is ``3.5``. Other parameters @@ -1295,7 +1292,7 @@ def parametric( ------- `~matplotlib.collections.LineCollection` The parametric line. See `this matplotlib example \ -`__. +`__. """ # NOTE: The 'extras' wrapper handles input before ingestion by other wrapper # functions. *This* method is analogous to a native matplotlib method. diff --git a/proplot/axes/cartesian.py b/proplot/axes/cartesian.py index 34fb9347c..b641acf94 100644 --- a/proplot/axes/cartesian.py +++ b/proplot/axes/cartesian.py @@ -563,13 +563,13 @@ def format( xscale_kw, yscale_kw : dict-like, optional The *x* and *y* axis scale settings. Passed to `~proplot.scale.Scale`. - xspineloc, yspineloc : {'both', 'bottom', 'top', 'left', 'right', \ -'neither', 'center', 'zero'}, optional + xspineloc, yspineloc \ +: {'both', 'bottom', 'top', 'left', 'right', 'neither', 'center', 'zero'}, optional The *x* and *y* axis spine locations. xloc, yloc : optional Aliases for `xspineloc`, `yspineloc`. - xtickloc, ytickloc : {'both', 'bottom', 'top', 'left', 'right', \ -'neither'}, optional + xtickloc, ytickloc \ +: {'both', 'bottom', 'top', 'left', 'right', 'neither'}, optional Which *x* and *y* axis spines should have major and minor tick marks. xtickminor, ytickminor : bool, optional diff --git a/proplot/axes/geo.py b/proplot/axes/geo.py index c7b674c2d..0ae54ea00 100644 --- a/proplot/axes/geo.py +++ b/proplot/axes/geo.py @@ -308,8 +308,8 @@ def format( longridminor, latgridminor : bool, optional Whether to draw "minor" longitude and latitude lines. Default is :rc:`gridminor`. Use `gridminor` to toggle both. - lonlocator, latlocator : str, float, list of float, or \ -`~matplotlib.ticker.Locator`, optional + lonlocator, latlocator \ +: str, float, list of float, or `~matplotlib.ticker.Locator`, optional Used to determine the longitude and latitude gridline locations. Passed to the `~proplot.constructor.Locator` constructor. Can be string, float, list of float, or `matplotlib.ticker.Locator` instance. @@ -330,8 +330,8 @@ def format( lonminorlocator, latminorlocator, lonminorlines, latminorlines : optional As with `lonlocator` and `latlocator` but for the "minor" gridlines. The defaults are :rc:`grid.lonminorstep` and :rc:`grid.latminorstep`. - lonminorlocator_kw, latminorlocator_kw, lonminorlines_kw, latminorlines_kw : \ -optional + lonminorlocator_kw, latminorlocator_kw, lonminorlines_kw, latminorlines_kw \ +: optional As with `lonlocator_kw` and `latlocator_kw` but for the "minor" gridlines. latmax : float, optional The maximum absolute latitude for longitude and latitude gridlines. @@ -399,8 +399,7 @@ def format( *For cartopy axes only.* The number of interpolation steps used to draw gridlines. Default is :rc:`grid.nsteps`. - land, ocean, coast, rivers, lakes, borders, innerborders : bool, \ -optional + land, ocean, coast, rivers, lakes, borders, innerborders : bool, optional Toggles various geographic features. These are actually the :rcraw:`land`, :rcraw:`ocean`, :rcraw:`coast`, :rcraw:`rivers`, :rcraw:`lakes`, :rcraw:`borders`, and :rcraw:`innerborders` @@ -1185,16 +1184,16 @@ def projection(self, map_projection): plot = wrap._apply_wrappers( GeoAxesBase.plot, wrap.default_transform, - wrap._plot_extras, wrap.standardize_1d, + wrap._plot_extras, wrap.indicate_error, wrap.apply_cycle, ) scatter = wrap._apply_wrappers( GeoAxesBase.scatter, wrap.default_transform, - wrap.scatter_extras, wrap.standardize_1d, + wrap.scatter_extras, wrap.indicate_error, wrap.apply_cycle, ) @@ -1558,8 +1557,8 @@ def projection(self, map_projection): maxes.Axes.plot, wrap._basemap_norecurse, wrap.default_latlon, - wrap._plot_extras, wrap.standardize_1d, + wrap._plot_extras, wrap.indicate_error, wrap.apply_cycle, wrap._basemap_redirect, @@ -1568,8 +1567,8 @@ def projection(self, map_projection): maxes.Axes.scatter, wrap._basemap_norecurse, wrap.default_latlon, - wrap.scatter_extras, wrap.standardize_1d, + wrap.scatter_extras, wrap.indicate_error, wrap.apply_cycle, wrap._basemap_redirect, diff --git a/proplot/axes/plot.py b/proplot/axes/plot.py index b6f031817..af2bb1eb8 100644 --- a/proplot/axes/plot.py +++ b/proplot/axes/plot.py @@ -232,7 +232,7 @@ 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 @@ -2049,8 +2049,8 @@ def boxplot_extras( meanls, medianls, meanlinestyle, medianlinestyle : line style-spec, optional The line style for the mean and median lines drawn horizontally across the box. - boxcolor, capcolor, whiskercolor, fliercolor, meancolor, mediancolor : \ -color-spec, list, optional + boxcolor, capcolor, whiskercolor, fliercolor, meancolor, mediancolor \ +: color-spec, list, optional The color of various boxplot components. If a list, it should be the same length as the number of objects. These are shorthands so you don't have to pass e.g. a ``boxprops`` dictionary. @@ -2166,7 +2166,7 @@ def violinplot_extras( """ Adds convenient keyword arguments and changes the default violinplot style to match `this matplotlib example \ -`__. +`__. It is also no longer possible to show minima and maxima with whiskers -- while this is useful for `~matplotlib.axes.Axes.boxplot`\\ s it is redundant for `~matplotlib.axes.Axes.violinplot`\\ s. @@ -2360,8 +2360,8 @@ def text_extras( The *x* and *y* coordinates for the text. text : str The text string. - transform : {{'data', 'axes', 'figure'}} \ -or `~matplotlib.transforms.Transform`, optional + 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 corresponding to `~matplotlib.axes.Axes.transData`, `~matplotlib.axes.Axes.transAxes`, @@ -3286,7 +3286,7 @@ def apply_cmap( # plots by calling contour() after contourf() if 'linewidths' or # 'linestyles' are explicitly passed, but do not want to disable the # native matplotlib feature for manually coloring filled contours. - # https://matplotlib.org/3.1.1/api/_as_gen/matplotlib.axes.Axes.contourf + # https://matplotlib.org/stable/api/_as_gen/matplotlib.axes.Axes.contourf add_contours = contourf and (linewidths is not None or linestyles is not None) use_cmap = colors is None or (not contour and (not contourf or add_contours)) if not use_cmap: diff --git a/proplot/axes/polar.py b/proplot/axes/polar.py index 3c9cfc753..1b10f2692 100644 --- a/proplot/axes/polar.py +++ b/proplot/axes/polar.py @@ -67,8 +67,7 @@ def format( The radial origin. theta0 : {'N', 'NW', 'W', 'SW', 'S', 'SE', 'E', 'NE'} The zero azimuth location. - thetadir : {1, -1, 'anticlockwise', 'counterclockwise', 'clockwise'}, \ -optional + thetadir : {1, -1, 'anticlockwise', 'counterclockwise', 'clockwise'}, optional The positive azimuth direction. Clockwise corresponds to ``-1`` and anticlockwise corresponds to ``1``. Default is ``1``. thetamin, thetamax : float, optional diff --git a/proplot/colors.py b/proplot/colors.py index 85c400a1d..45fecf228 100644 --- a/proplot/colors.py +++ b/proplot/colors.py @@ -1620,8 +1620,7 @@ def copy( ---------- name : str The name of the new colormap. Default is ``self.name + '_copy'``. - segmentdata, N, alpha, clip, cyclic, gamma, gamma1, gamma2, space : \ -optional + segmentdata, N, alpha, clip, cyclic, gamma, gamma1, gamma2, space : optional See `PerceptuallyUniformColormap`. If not provided, these are copied from the current colormap. """ diff --git a/proplot/config.py b/proplot/config.py index 09c3a1501..5245c98d0 100644 --- a/proplot/config.py +++ b/proplot/config.py @@ -1023,7 +1023,7 @@ def save(self, path=None, user=True, comment=None, backup=True, description=Fals '# Use this file to change the default proplot and matplotlib settings', '# The syntax is identical to matplotlibrc syntax. For details see:', '# https://proplot.readthedocs.io/en/latest/configuration.html', - '# https://matplotlib.org/3.1.1/tutorials/introductory/customizing', + '# https://matplotlib.org/stable/tutorials/introductory/customizing.html', # noqa: E501 '#--------------------------------------------------------------------', *rc_user, # includes blank line '# ProPlot settings', diff --git a/proplot/constructor.py b/proplot/constructor.py index 61aefd1ef..50adf5896 100644 --- a/proplot/constructor.py +++ b/proplot/constructor.py @@ -673,11 +673,11 @@ def Cycle( will be filled to match the length of the longest list. See `~matplotlib.axes.Axes.set_prop_cycle` for more info on cyclers. Also see the `line style reference \ -`__, +`__, the `marker reference \ -`__, +`__, and the `custom dashes reference \ -`__. +`__. Other parameters ---------------- @@ -771,9 +771,8 @@ 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.axes.apply_cmap`. See - `this tutorial \ -`__ + method wrapped by `~proplot.axes.apply_cmap`. See `this tutorial \ +`__ for more info. Parameters diff --git a/proplot/demos.py b/proplot/demos.py index 21203cd0e..8ed96d0b1 100644 --- a/proplot/demos.py +++ b/proplot/demos.py @@ -287,7 +287,7 @@ def show_channels( Show how arbitrary colormap(s) vary with respect to the hue, chroma, luminance, HSL saturation, and HPL saturation channels, and optionally the red, blue and green channels. Adapted from `this example \ -`__. +`__. Parameters ---------- @@ -665,7 +665,7 @@ def show_cmaps(*args, **kwargs): """ Generate a table of the registered colormaps or the input colormaps categorized by source. Adapted from `this example \ -`__. +`__. Parameters ---------- @@ -712,7 +712,7 @@ def show_cycles(*args, **kwargs): """ Generate a table of registered color cycles or the input color cycles categorized by source. Adapted from `this example \ -`__. +`__. Parameters ---------- @@ -762,14 +762,14 @@ def show_fonts( The font name(s). If none are provided and the `family` keyword argument was not provided, the *available* :rcraw:`font.sans-serif` fonts and the fonts in your ``.proplot/fonts`` folder are shown. - family : {'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', \ -'tex-gyre'}, optional + family \ +: {'serif', 'sans-serif', 'monospace', 'cursive', 'fantasy', 'tex-gyre'}, optional If provided, the *available* fonts in the corresponding families are shown. The fonts belonging to these families are listed under the :rcraw:`font.serif`, :rcraw:`font.sans-serif`, :rcraw:`font.monospace`, - :rcraw:`font.cursive`, and :rcraw:`font.fantasy` settings. The special - family ``'tex-gyre'`` draws the `TeX Gyre \ -`__ fonts. + :rcraw:`font.cursive`, and :rcraw:`font.fantasy` settings. The + family ``'tex-gyre'`` draws the + `TeX Gyre `__ fonts. text : str, optional The sample text. The default sample text includes the Latin letters, Greek letters, Arabic numerals, and some simple mathematical symbols. diff --git a/proplot/figure.py b/proplot/figure.py index 7ae9a373f..ebb06ea97 100644 --- a/proplot/figure.py +++ b/proplot/figure.py @@ -206,15 +206,15 @@ def __init__( your figure will have 1 ylabel instead of 9. alignx, aligny, align : bool or {0, 1}, optional Whether to `align axis labels \ -`__ +`__ for the *x* axis, *y* axis, or both axes. Only has an effect when `spanx`, `spany`, or `span` are ``False``. Default is :rc:`subplots.align`. mathtext_fallback : bool or str, optional Figure-specific application of the :rc:`mathtext.fallback` property. If ``True`` or string, unavailable glyphs are replaced with a glyph from a fallback font (Computer Modern by default). Otherwise, they are replaced - with the "¤" dummy character. See `mathtext \ -`__ + with the "¤" dummy character. See this `mathtext tutorial \ +`__ for details. gridspec_kw, subplots_kw, subplots_orig_kw Keywords used for initializing the main gridspec, for initializing diff --git a/proplot/internals/rcsetup.py b/proplot/internals/rcsetup.py index 5aab71cf5..befd2db7a 100644 --- a/proplot/internals/rcsetup.py +++ b/proplot/internals/rcsetup.py @@ -244,14 +244,14 @@ _addendum_units = ' Interpreted by `~proplot.utils.units`.' _addendum_fonts = ( ' (see `this list of valid font sizes ' - '`__).' + '`__).' ) _rc_proplot = { # Stylesheet 'style': ( None, 'The default matplotlib `stylesheet ' - '`__ ' # noqa: E501 + '`__ ' # noqa: E501 'name. If ``None``, a custom proplot style is used. ' "If ``'default'``, the default matplotlib style is used." ), @@ -769,7 +769,7 @@ 'subplots.align': ( False, 'Whether to align axis labels during draw. See `aligning labels ' - '`__.' # noqa: E501 + '`__.' # noqa: E501 ), 'subplots.innerpad': ( '1em', diff --git a/proplot/scale.py b/proplot/scale.py index 5e5b9dbaf..02c9ef232 100644 --- a/proplot/scale.py +++ b/proplot/scale.py @@ -236,10 +236,9 @@ def __init__(self, **kwargs): of the base. For example, ``subs=(1, 2, 5)`` draws ticks on 1, 2, 5, 10, 20, 50, 100, 200, 500, etc. The default is ``subs=numpy.arange(1, 10)``. - basex, basey, linthreshx, linthreshy, linscalex, linscaley, \ -subsx, subsy - Aliases for the above keywords. These used to be conditional - on the *name* of the axis. + basex, basey, linthreshx, linthreshy, linscalex, linscaley, subsx, subsy + Aliases for the above keywords. These keywords used to be + conditional on the name of the axis. """ keys = ('base', 'linthresh', 'linscale', 'subs') super().__init__(**_parse_logscale_args(*keys, **kwargs)) @@ -263,16 +262,16 @@ def __init__( """ Parameters ---------- - arg : function, (function, function), or \ -`~matplotlib.scale.ScaleBase` + arg : function, (function, function), or `~matplotlib.scale.ScaleBase` The transform used to translate units from the parent axis to the secondary axis. Input can be as follows: * A single function that accepts a number and returns some transformation of that number. If you do not provide the - inverse, the function must be - `linear `__ or \ -`involutory `__. + inverse, the function must be `linear \ +`__ + or `involutory \ +`__. For example, to convert Kelvin to Celsius, use ``ax.dual%(x)s(lambda x: x - 273.15)``. To convert kilometers to meters, use ``ax.dual%(x)s(lambda x: x*1e3)``. @@ -298,8 +297,7 @@ def __init__( The default major and minor locator. By default these are borrowed from `transform`. If `transform` is not an axis scale, they are the same as `~matplotlib.scale.LinearScale`. - major_formatter, minor_formatter : `~matplotlib.ticker.Formatter`, \ -optional + major_formatter, minor_formatter : `~matplotlib.ticker.Formatter`, optional 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`. @@ -578,10 +576,9 @@ def transform_non_affine(self, a): class MercatorLatitudeScale(_Scale, mscale.ScaleBase): """ - Axis scale that transforms coordinates as with latitude in the - `Mercator projection `__. - Adapted from `this matplotlib example \ -`__. + Axis scale that is linear in the `Mercator projection latitude \ +`__. Adapted from `this example \ +`__. The scale function is as follows: .. math:: @@ -669,9 +666,9 @@ def transform_non_affine(self, a): class SineLatitudeScale(_Scale, mscale.ScaleBase): r""" - Axis scale that is linear in the *sine* of *x*. The axis limits are - constrained to fall between ``-90`` and ``+90`` degrees. The scale - function is as follows: + Axis scale that is linear in the sine transformation of *x*. The axis + limits are constrained to fall between ``-90`` and ``+90`` degrees. + The scale function is as follows: .. math:: diff --git a/proplot/ui.py b/proplot/ui.py index 76e88ebc3..1b0698404 100644 --- a/proplot/ui.py +++ b/proplot/ui.py @@ -283,8 +283,9 @@ def subplots( Passed to `~proplot.gridspec.GridSpec`, denotes the width of padding between the subplots and figure edge. Units are interpreted by `~proplot.utils.units`. By default, these are determined by the "tight layout" algorithm. - proj, projection : str, `cartopy.crs.Projection`, `~mpl_toolkits.basemap.Basemap`, \ -list thereof, or dict thereof, optional + proj, projection \ +: str, `cartopy.crs.Projection`, `~mpl_toolkits.basemap.Basemap`, list thereof, \ +or dict thereof, optional The map projection specification(s). If ``'cartesian'`` (the default), a `~proplot.axes.CartesianAxes` is created. If ``'polar'``, a `~proplot.axes.PolarAxes` is created. Otherwise, the argument is From a0d4adbd6e253e021ec47714a4e82215a2abdd37 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Thu, 8 Jul 2021 19:57:13 -0600 Subject: [PATCH 03/11] More sensible order for 1D plot examples and API docs --- docs/1dplots.py | 354 ++++++++++++++++++++++--------------------- proplot/axes/plot.py | 6 +- 2 files changed, 181 insertions(+), 179 deletions(-) diff --git a/docs/1dplots.py b/docs/1dplots.py index a0b940091..2e463664b 100644 --- a/docs/1dplots.py +++ b/docs/1dplots.py @@ -195,81 +195,126 @@ # %% [raw] raw_mimetype="text/restructuredtext" -# .. _ug_errorbars: +# .. _ug_lines: # -# Shading and error bars -# ---------------------- +# Line plots +# ---------- # -# 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`. +# The `~matplotlib.axes.Axes.plot` command is wrapped by +# `~proplot.axes.apply_cycle` and `~proplot.axes.standardize_1d`. +# Its behavior is the same -- ProPlot simply tries to expand +# the flexibility of this command to the rest of the 1D plotting commands. +# The new `~proplot.axes.Axes.plotx` command can be used just like +# `~matplotlib.axes.Axes.plotx`, except a single argument is interpreted +# as *x* coordinates (with default *y* coordinates inferred from the data), +# and multiple arguments are interpreted as *y* and *x* coordinates (in that order). +# This is analogous to `~matplotlib.axes.Axes.barh` and +# `~matplotlib.axes.Axes.fill_betweenx`. # -# If you pass 2D arrays to these methods with ``mean=True`` or ``median=True``, -# the means or medians of each column are drawn as points, lines, or bars, and -# *error bars* or *shading* is drawn to represent the spread of the distribution -# for each column. You can also specify the error bounds *manually* with the -# `bardata`, `boxdata`, `shadedata`, and `fadedata` keywords. -# `~proplot.axes.indicate_error` can draw thin error bars with optional whiskers, -# thick "boxes" overlayed on top of these bars (think of these as miniature boxplots), -# and up to 2 layers of shading. See `~proplot.axes.indicate_error` for details. +# As with the other 1D plotting commands, `~matplotlib.axes.Axes.step`, +# `~matplotlib.axes.Axes.hlines`, `~matplotlib.axes.Axes.vlines`, and +# `~matplotlib.axes.Axes.stem` are wrapped by `~proplot.axes.standardize_1d`. +# `~proplot.axes.Axes.step` now use the property cycle, just like +# `~matplotlib.axes.Axes.plot`. `~matplotlib.axes.Axes.vlines` and +# `~matplotlib.axes.Axes.hlines` are also wrapped by `~proplot.axes.vlines_extras` +# and `~proplot.axes.hlines_extras`, which can use +# different colors for "negative" and "positive" lines using ``negpos=True`` +# (the default colors are :rc:`negcolor` and :rc:`poscolor`). + +# %% +import proplot as plot +import numpy as np +state = np.random.RandomState(51423) +fig, axs = plot.subplots(ncols=2, nrows=2, share=0) +axs.format(suptitle='Line plots demo', xlabel='xlabel', ylabel='ylabel') + +# Step +ax = axs[0] +data = state.rand(20, 4).cumsum(axis=1).cumsum(axis=0) +cycle = ('blue7', 'gray5', 'red7', 'gray5') +ax.step(data, cycle=cycle, labels=list('ABCD'), legend='ul', legend_kw={'ncol': 2}) +ax.format(title='Step plot') + +# Stems +ax = axs[1] +data = state.rand(20) +ax.stem(data, linefmt='k-') +ax.format(title='Stem plot') + +# Vertical lines +gray = 'gray7' +data = state.rand(20) - 0.5 +ax = axs[2] +ax.area(data, color=gray, alpha=0.2) +ax.vlines(data, negpos=True, linewidth=2) +ax.format(title='Vertical lines') + +# Horizontal lines +ax = axs[3] +ax.areax(data, color=gray, alpha=0.2) +ax.hlines(data, negpos=True, linewidth=2) +ax.format(title='Horizontal lines') + +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_scatter: +# +# Scatter plots +# ------------- +# +# The `~matplotlib.axes.Axes.scatter` command is wrapped by +# `~proplot.axes.scatter_extras`, `~proplot.axes.apply_cycle`, and +# `~proplot.axes.standardize_1d`. This means that +# `~matplotlib.axes.Axes.scatter` now accepts 2D *y* coordinates and permits +# omitting *x* coordinates, just like `~matplotlib.axes.Axes.plot`. +# `~matplotlib.axes.Axes.scatter` now also accepts keywords that look like +# `~matplotlib.axes.Axes.plot` keywords (e.g., `color` instead of `c` and +# `markersize` instead of `s`). This way, `~matplotlib.axes.Axes.scatter` can +# optionally be used simply to "plot markers, not lines" without changing the +# input arguments relative to `~matplotlib.axes.Axes.plot`. +# +# Just like `~matplotlib.axes.Axes.plot`, the property cycle is used +# with `~matplotlib.axes.Axes.scatter` plots by default. It can be changed +# using the `cycle` keyword argument, and it can include properties like `marker` +# and `markersize`. The colormap `cmap` and normalizer `norm` used with the +# optional `c` color array are now passed through the `~proplot.constructor.Colormap` +# and `~proplot.constructor.Norm` constructor functions, and the the `s` marker +# size array can now be conveniently scaled using the arguments `smin` and `smax` +# (analogous to `vmin` and `vmax` used for colors). # %% import proplot as plot import numpy as np import pandas as pd -plot.rc['title.loc'] = 'uc' # Sample data state = np.random.RandomState(51423) -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' +x = (state.rand(20) - 0).cumsum() +data = (state.rand(20, 4) - 0.5).cumsum(axis=0) +data = pd.DataFrame(data, columns=pd.Index(['a', 'b', 'c', 'd'], name='label')) # Figure -fig, axs = plot.subplots( - nrows=3, refaspect=1.5, refwidth=4, - share=0, hratios=(2, 1, 1) -) -axs.format(suptitle='Indicating error bounds') -axs[1:].format(xlabel='column number', xticks=1, xgrid=False) +fig, axs = plot.subplots(ncols=2, share=1) +axs.format(suptitle='Scatter plot demo') -# Medians and percentile ranges +# Scatter plot with property cycler ax = axs[0] -obj = ax.barh( - data, color='light red', legend=True, - median=True, barpctile=90, boxpctile=True, - # median=True, barpctile=(5, 95), boxpctile=(25, 75) # equivalent +ax.format(title='With property cycle') +obj = ax.scatter( + x, data, legend='ul', cycle='Set2', legend_kw={'ncols': 2}, + cycle_kw={'marker': ['x', 'o', 'x', 'o'], 'markersize': [5, 10, 20, 30]} ) -ax.format(title='Column statistics') -ax.format(ylabel='column number', title='Bar plot', ygrid=False) -# Means and standard deviation range +# Scatter plot with colormap ax = axs[1] -ax.scatter( - data, color='denim', marker='x', markersize=8**2, linewidth=0.8, legend='ll', - label='mean', shadelabel=True, - mean=True, shadestd=1, - # mean=True, shadestd=(-1, 1) # equivalent -) -ax.format(title='Marker plot') - -# User-defined error bars -ax = axs[2] -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( - means, - shadedata=shadedata, fadedata=fadedata, - label='mean', shadelabel='50% CI', fadelabel='90% CI', - color='ocean blue', barzorder=0, boxmarker=False, legend='ll', +ax.format(title='With colormap') +data = state.rand(2, 100) +obj = ax.scatter( + *data, color=data.sum(axis=0), size=state.rand(100), smin=3, smax=30, + marker='o', cmap='dark red', cmap_kw={'fade': 90}, vmin=0, vmax=2, + colorbar='lr', colorbar_kw={'label': 'label', 'locator': 0.5}, ) -ax.format(title='Line plot') -plot.rc.reset() +axs.format(xlabel='xlabel', ylabel='ylabel') # %% [raw] raw_mimetype="text/restructuredtext" @@ -392,6 +437,84 @@ axs[1].format(title='Area plot') +# %% [raw] raw_mimetype="text/restructuredtext" +# .. _ug_errorbars: +# +# Shading and error bars +# ---------------------- +# +# 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 ``mean=True`` or ``median=True``, +# the means or medians of each column are drawn as points, lines, or bars, and +# *error bars* or *shading* is drawn to represent the spread of the distribution +# for each column. You can also specify the error bounds *manually* with the +# `bardata`, `boxdata`, `shadedata`, and `fadedata` keywords. +# `~proplot.axes.indicate_error` can draw thin error bars with optional whiskers, +# thick "boxes" overlayed on top of these bars (think of these as miniature boxplots), +# and up to 2 layers of shading. See `~proplot.axes.indicate_error` for details. + + +# %% +import proplot as plot +import numpy as np +import pandas as pd +plot.rc['title.loc'] = 'uc' + +# Sample data +state = np.random.RandomState(51423) +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' + +# Figure +fig, axs = plot.subplots( + nrows=3, refaspect=1.5, refwidth=4, + share=0, hratios=(2, 1, 1) +) +axs.format(suptitle='Indicating error bounds') +axs[1:].format(xlabel='column number', xticks=1, xgrid=False) + +# Medians and percentile ranges +ax = axs[0] +obj = ax.barh( + data, color='light red', legend=True, + median=True, barpctile=90, boxpctile=True, + # median=True, barpctile=(5, 95), boxpctile=(25, 75) # equivalent +) +ax.format(title='Column statistics') +ax.format(ylabel='column number', title='Bar plot', ygrid=False) + +# Means and standard deviation range +ax = axs[1] +ax.scatter( + data, color='denim', marker='x', markersize=8**2, linewidth=0.8, legend='ll', + label='mean', shadelabel=True, + mean=True, shadestd=1, + # mean=True, shadestd=(-1, 1) # equivalent +) +ax.format(title='Marker plot') + +# User-defined error bars +ax = axs[2] +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( + means, + shadedata=shadedata, fadedata=fadedata, + label='mean', shadelabel='50% CI', fadelabel='90% CI', + color='ocean blue', barzorder=0, boxmarker=False, legend='ll', +) +ax.format(title='Line plot') +plot.rc.reset() + + # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_boxplots: # @@ -448,127 +571,6 @@ ax.format(title='Multiple colors', titleloc='uc', ymargin=0.15) -# %% [raw] raw_mimetype="text/restructuredtext" -# .. _ug_lines: -# -# Line plots -# ---------- -# -# The `~matplotlib.axes.Axes.plot` command is wrapped by -# `~proplot.axes.apply_cycle` and `~proplot.axes.standardize_1d`. -# But in general, its behavior is the same -- ProPlot simply tries to expand -# the flexibility of this command to the rest of the 1D plotting commands. -# The new `~proplot.axes.Axes.plotx` command can be used just like -# `~matplotlib.axes.Axes.plotx`, except a single argument is interpreted -# as *x* coordinates (with default *y* coordinates inferred from the data), -# and multiple arguments are interpreted as *y* and *x* coordinates (in that order). -# This is analogous to `~matplotlib.axes.Axes.barh` and -# `~matplotlib.axes.Axes.fill_betweenx`. -# -# As with the other 1D plotting commands, `~matplotlib.axes.Axes.step`, -# `~matplotlib.axes.Axes.hlines`, `~matplotlib.axes.Axes.vlines`, and -# `~matplotlib.axes.Axes.stem` are wrapped by `~proplot.axes.standardize_1d`. -# `~proplot.axes.Axes.step` now use the property cycle, just like -# `~matplotlib.axes.Axes.plot`. `~matplotlib.axes.Axes.vlines` and -# `~matplotlib.axes.Axes.hlines` are also wrapped by `~proplot.axes.vlines_extras` -# and `~proplot.axes.hlines_extras`, which can use -# different colors for "negative" and "positive" lines using ``negpos=True`` -# (the default colors are :rc:`negcolor` and :rc:`poscolor`). - -# %% -import proplot as plot -import numpy as np -state = np.random.RandomState(51423) -fig, axs = plot.subplots(ncols=2, nrows=2, share=0) -axs.format(suptitle='Line plots demo', xlabel='xlabel', ylabel='ylabel') - -# Step -ax = axs[0] -data = state.rand(20, 4).cumsum(axis=1).cumsum(axis=0) -cycle = ('blue7', 'gray5', 'red7', 'gray5') -ax.step(data, cycle=cycle, labels=list('ABCD'), legend='ul', legend_kw={'ncol': 2}) -ax.format(title='Step plot') - -# Stems -ax = axs[1] -data = state.rand(20) -ax.stem(data, linefmt='k-') -ax.format(title='Stem plot') - -# Vertical lines -gray = 'gray7' -data = state.rand(20) - 0.5 -ax = axs[2] -ax.area(data, color=gray, alpha=0.2) -ax.vlines(data, negpos=True, linewidth=2) -ax.format(title='Vertical lines') - -# Horizontal lines -ax = axs[3] -ax.areax(data, color=gray, alpha=0.2) -ax.hlines(data, negpos=True, linewidth=2) -ax.format(title='Horizontal lines') - -# %% [raw] raw_mimetype="text/restructuredtext" -# .. _ug_scatter: -# -# Scatter plots -# ------------- -# -# The `~matplotlib.axes.Axes.scatter` command is wrapped by -# `~proplot.axes.scatter_extras`, `~proplot.axes.apply_cycle`, and -# `~proplot.axes.standardize_1d`. This means that -# `~matplotlib.axes.Axes.scatter` now accepts 2D *y* coordinates and permits -# omitting *x* coordinates, just like `~matplotlib.axes.Axes.plot`. -# `~matplotlib.axes.Axes.scatter` now also accepts keywords that look like -# `~matplotlib.axes.Axes.plot` keywords (e.g., `color` instead of `c` and -# `markersize` instead of `s`). This way, `~matplotlib.axes.Axes.scatter` can -# optionally be used simply to "plot markers, not lines" without changing the -# input arguments relative to `~matplotlib.axes.Axes.plot`. -# -# Just like `~matplotlib.axes.Axes.plot`, the property cycle is used -# with `~matplotlib.axes.Axes.scatter` plots by default. It can be changed -# using the `cycle` keyword argument, and it can include properties like `marker` -# and `markersize`. The colormap `cmap` and normalizer `norm` used with the -# optional `c` color array are now passed through the `~proplot.constructor.Colormap` -# and `~proplot.constructor.Norm` constructor functions, and the the `s` marker -# size array can now be conveniently scaled using the arguments `smin` and `smax` -# (analogous to `vmin` and `vmax` used for colors). - -# %% -import proplot as plot -import numpy as np -import pandas as pd - -# Sample data -state = np.random.RandomState(51423) -x = (state.rand(20) - 0).cumsum() -data = (state.rand(20, 4) - 0.5).cumsum(axis=0) -data = pd.DataFrame(data, columns=pd.Index(['a', 'b', 'c', 'd'], name='label')) - -# Figure -fig, axs = plot.subplots(ncols=2, share=1) -axs.format(suptitle='Scatter plot demo') - -# Scatter plot with property cycler -ax = axs[0] -ax.format(title='With property cycle') -obj = ax.scatter( - x, data, legend='ul', cycle='Set2', legend_kw={'ncols': 2}, - cycle_kw={'marker': ['x', 'o', 'x', 'o'], 'markersize': [5, 10, 20, 30]} -) - -# Scatter plot with colormap -ax = axs[1] -ax.format(title='With colormap') -data = state.rand(2, 100) -obj = ax.scatter( - *data, color=data.sum(axis=0), size=state.rand(100), smin=3, smax=30, - marker='o', cmap='dark red', cmap_kw={'fade': 90}, vmin=0, vmax=2, - colorbar='lr', colorbar_kw={'label': 'label', 'locator': 0.5}, -) -axs.format(xlabel='xlabel', ylabel='ylabel') - # %% [raw] raw_mimetype="text/restructuredtext" # .. _ug_parametric: # diff --git a/proplot/axes/plot.py b/proplot/axes/plot.py index af2bb1eb8..f62a85364 100644 --- a/proplot/axes/plot.py +++ b/proplot/axes/plot.py @@ -57,15 +57,15 @@ 'colorbar_extras', 'legend_extras', 'text_extras', + 'vlines_extras', + 'hlines_extras', + 'scatter_extras', 'bar_extras', 'barh_extras', 'fill_between_extras', 'fill_betweenx_extras', 'boxplot_extras', 'violinplot_extras', - 'vlines_extras', - 'hlines_extras', - 'scatter_extras', ] From 87fa3a15d8a0b1516fdb0c20b49710b39c7b635e Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Thu, 8 Jul 2021 20:01:10 -0600 Subject: [PATCH 04/11] Always surround docstrings with newlines --- proplot/internals/docstring.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/proplot/internals/docstring.py b/proplot/internals/docstring.py index b8697f68a..bf703bf47 100644 --- a/proplot/internals/docstring.py +++ b/proplot/internals/docstring.py @@ -9,10 +9,12 @@ def add_snippets(func): - """Decorator that dedents docstrings with `inspect.getdoc` and adds + """ + Decorator that dedents docstrings with `inspect.getdoc` and adds un-indented snippets from the global `snippets` dictionary. This function uses ``%(name)s`` substitution rather than `str.format` substitution so - that the `snippets` keys can be invalid variable names.""" + that the `snippets` keys can be invalid variable names. + """ func.__doc__ = inspect.getdoc(func) if func.__doc__: func.__doc__ %= {key: value.strip() for key, value in snippets.items()} From 3ec681e7a6767cf3b4fb47c583370852dba08948 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Thu, 8 Jul 2021 20:02:13 -0600 Subject: [PATCH 05/11] Nitpicky style consistency --- proplot/internals/warnings.py | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/proplot/internals/warnings.py b/proplot/internals/warnings.py index a393a4fb1..ac6f3a689 100644 --- a/proplot/internals/warnings.py +++ b/proplot/internals/warnings.py @@ -40,14 +40,12 @@ def getter(self): f'{type(self).__name__}.{property} instead.' ) return getattr(self, '_' + property) - - def setter(self, value): + def setter(self, value): # noqa: E306 _warn_proplot( f'set_{property}() was deprecated in {version}. The property is ' f'now read-only.' ) return - getter.__name__ = f'get_{property}' setter.__name__ = f'set_{property}' @@ -61,7 +59,6 @@ def _rename_objs(version, **kwargs): """ wrappers = [] for old_name, func_or_class in kwargs.items(): - def wrapper(*args, old_name=old_name, func_or_class=func_or_class, **kwargs): new_name = func_or_class.__name__ _warn_proplot( @@ -69,10 +66,8 @@ def wrapper(*args, old_name=old_name, func_or_class=func_or_class, **kwargs): f'removed in the next major release. Please use {new_name!r} instead.' ) return func_or_class(*args, **kwargs) - wrapper.__name__ = old_name wrappers.append(wrapper) - if len(wrappers) == 1: return wrappers[0] else: From 3b1cbafb050f1a4d36829c677a90b007226dbdfe Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Fri, 9 Jul 2021 04:22:17 -0600 Subject: [PATCH 06/11] Major 1d wrapper refactor, add (plot|scatter)x, add 'data' key --- proplot/axes/base.py | 204 ++++-- proplot/axes/plot.py | 1430 +++++++++++++++++++++--------------------- proplot/demos.py | 9 +- 3 files changed, 872 insertions(+), 771 deletions(-) diff --git a/proplot/axes/base.py b/proplot/axes/base.py index d74326eb9..68024e428 100644 --- a/proplot/axes/base.py +++ b/proplot/axes/base.py @@ -993,7 +993,9 @@ def area(self, *args, **kwargs): See also -------- matplotlib.axes.Axes.fill_between + proplot.axes.standardize_1d proplot.axes.fill_between_extras + proplot.axes.apply_cycle """ # NOTE: *Cannot* assign area = axes.Axes.fill_between because the # wrapper won't be applied and for some reason it messes up @@ -1007,7 +1009,9 @@ def areax(self, *args, **kwargs): See also -------- matplotlib.axes.Axes.fill_betweenx + proplot.axes.standardize_1d proplot.axes.fill_betweenx_extras + proplot.axes.apply_cycle """ return self.fill_betweenx(*args, **kwargs) @@ -1018,7 +1022,9 @@ def boxes(self, *args, **kwargs): See also -------- matplotlib.axes.Axes.boxplot + proplot.axes.standardize_1d proplot.axes.boxplot_extras + proplot.axes.apply_cycle """ return self.boxplot(*args, **kwargs) @@ -1247,11 +1253,32 @@ def panel_axes(self, side, **kwargs): side = self._loc_translate(side, 'panel') return self.figure._add_axes_panel(self, side, **kwargs) + def plotx(self, *args, **kwargs): + """ + As with `~matplotlib.axes.Axes.plot` but interpret a single + positional argument as *x* and multiple positional arguments + as *y* and *x* (in that order). + + Parameters + ---------- + *args, **kwargs + Passed to `~matplotlib.axes.Axes.plot`. + + See also + -------- + matplotlib.axes.Axes.plot + proplot.axes.standardize_1d + proplot.axes.indicate_error + proplot.axes.apply_cycle + """ + # NOTE: Arguments are standardized once we reach this block + x, y, *args = args + return super().plot(y, x, *args, **kwargs) + + @docstring.add_snippets def parametric( - self, *args, values=None, - cmap=None, norm=None, interp=0, - scalex=True, scaley=True, - **kwargs + self, x, y, values=None, cmap=None, norm=None, *, + interp=0, scalex=True, scaley=True, **kwargs ): """ Draw a line whose color changes as a function of the parametric @@ -1263,17 +1290,9 @@ def parametric( ---------- *args : (y,), (x, y), or (x, y, values) The coordinates. If `x` is not provided, it is inferred from `y`. - values : list of float - The parametric values used to map points on the line to colors - in the colormap. This can also be passed as a third positional argument. - cmap : colormap spec, optional - The colormap specifier, passed to `~proplot.constructor.Colormap`. - cmap_kw : dict, optional - Keyword arguments passed to `~proplot.constructor.Colormap`. - norm : normalizer spec, optional - The normalizer, passed to `~proplot.constructor.Norm`. - norm_kw : dict, optional - Keyword arguments passed to `~proplot.constructor.Norm`. + The parametric coordinate can be indicated as a third positional + argument or with the `values` or `levels` keywords. + %(axes.cmap_norm)s interp : int, optional If greater than ``0``, we interpolate to additional points between the `values` coordinates. The number corresponds to the @@ -1293,12 +1312,43 @@ def parametric( `~matplotlib.collections.LineCollection` The parametric line. See `this matplotlib example \ `__. - """ + + See also + -------- + matplotlib.axes.Axes.plot + proplot.axes.standardize_1d + proplot.axes.apply_cmap + """ + # Parse input + # NOTE: Input *x* and *y* will have been standardized by _standardize_1d + if values is None: + raise ValueError('Values must be provided.') + values = wrap._to_ndarray(values) + ndim = tuple(_.ndim for _ in (x, y, values)) + size = tuple(_.size for _ in (x, y, values)) + if any(_ != 1 for _ in ndim): + raise ValueError(f'Input coordinates must be 1D. Instead got dimensions {ndim}.') # noqa: E501 + if any(_ != size[0] for _ in size): + raise ValueError(f'Input coordinates must have identical size. Instead got sizes {size}.') # noqa: E501 + + # Interpolate values to allow for smooth gradations between values + # (interp=False) or color switchover halfway between points + # (interp=True). Then optionally interpolate the colormap values. # NOTE: The 'extras' wrapper handles input before ingestion by other wrapper # functions. *This* method is analogous to a native matplotlib method. + if interp > 0: + x_orig, y_orig, v_orig = x, y, values + x, y, values = [], [], [] + for j in range(x_orig.shape[0] - 1): + idx = slice(None) + if j + 1 < x_orig.shape[0] - 1: + idx = slice(None, -1) + x.extend(np.linspace(x_orig[j], x_orig[j + 1], interp + 2)[idx].flat) + y.extend(np.linspace(y_orig[j], y_orig[j + 1], interp + 2)[idx].flat) + values.extend(np.linspace(v_orig[j], v_orig[j + 1], interp + 2)[idx].flat) # noqa: E501 + x, y, values = np.array(x), np.array(y), np.array(values) + # Get coordinates and values for points to the 'left' and 'right' of joints - x, y = args # standardized by parametric wrapper - interp # avoid U100 unused argument error (arg is handled by wrapper) coords = [] levels = edges(values) for i in range(y.shape[0]): @@ -1330,8 +1380,30 @@ def parametric( self.autoscale_view(scalex=scalex, scaley=scaley) hs.values = values hs.levels = levels # needed for other functions + return hs + def scatterx(self, *args, **kwargs): + """ + As with `~matplotlib.axes.Axes.scatter` but interpret a single + positional argument as *x* and multiple positional arguments + as *y* and *x* (in that order). + + Parameters + ---------- + *args, **kwargs + Passed to `~matplotlib.axes.Axes.scatter`. + + See also + -------- + proplot.axes.standardize_1d + matplotlib.axes.Axes.scatter + proplot.axes.scatterx_extras + """ + # NOTE: Arguments are standardized once we reach this block + x, y, *args = args + return super().scatter(y, x, *args, **kwargs) + def violins(self, *args, **kwargs): """ Shorthand for `~matplotlib.axes.Axes.violinplot`. @@ -1339,7 +1411,10 @@ def violins(self, *args, **kwargs): See also -------- matplotlib.axes.Axes.violinplot + proplot.axes.standardize_1d proplot.axes.violinplot_extras + proplot.axes.indicate_error + proplot.axes.apply_cycle """ return self.violinplot(*args, **kwargs) @@ -1805,28 +1880,56 @@ def number(self, num): # Apply 1D plotting command wrappers plot = wrap._apply_wrappers( maxes.Axes.plot, + wrap.standardize_1d, wrap._plot_extras, + wrap.indicate_error, + wrap.apply_cycle, + ) + plotx = wrap._apply_wrappers( + plotx, wrap.standardize_1d, + wrap._plotx_extras, wrap.indicate_error, wrap.apply_cycle, ) + step = wrap._apply_wrappers( + maxes.Axes.step, + wrap.standardize_1d, + wrap.apply_cycle, + ) + stem = wrap._apply_wrappers( + maxes.Axes.stem, + wrap.standardize_1d, + wrap._stem_extras, # TODO check this + ) + vlines = wrap._apply_wrappers( + maxes.Axes.vlines, + wrap.standardize_1d, + wrap.vlines_extras, # TODO check this + ) + hlines = wrap._apply_wrappers( + maxes.Axes.hlines, + wrap.standardize_1d, + wrap.hlines_extras, # TODO check this + ) scatter = wrap._apply_wrappers( maxes.Axes.scatter, - wrap.scatter_extras, wrap.standardize_1d, + wrap.scatter_extras, wrap.indicate_error, wrap.apply_cycle, ) - hist = wrap._apply_wrappers( - maxes.Axes.hist, - wrap._hist_extras, + scatterx = wrap._apply_wrappers( + scatterx, wrap.standardize_1d, + wrap.scatterx_extras, + wrap.indicate_error, wrap.apply_cycle, ) bar = wrap._apply_wrappers( maxes.Axes.bar, - wrap.bar_extras, wrap.standardize_1d, + wrap.bar_extras, wrap.indicate_error, wrap.apply_cycle, ) @@ -1834,60 +1937,45 @@ def number(self, num): maxes.Axes.barh, wrap.barh_extras, ) - boxplot = wrap._apply_wrappers( - maxes.Axes.boxplot, - wrap.boxplot_extras, - wrap.standardize_1d, - wrap.apply_cycle, - ) - violinplot = wrap._apply_wrappers( - maxes.Axes.violinplot, - wrap.violinplot_extras, + hist = wrap._apply_wrappers( + maxes.Axes.hist, wrap.standardize_1d, - wrap.indicate_error, + wrap._hist_extras, wrap.apply_cycle, ) fill_between = wrap._apply_wrappers( maxes.Axes.fill_between, - wrap.fill_between_extras, wrap.standardize_1d, + wrap.fill_between_extras, wrap.apply_cycle, ) fill_betweenx = wrap._apply_wrappers( maxes.Axes.fill_betweenx, - wrap.fill_betweenx_extras, wrap.standardize_1d, + wrap.fill_betweenx_extras, wrap.apply_cycle, ) - pie = wrap._apply_wrappers( - maxes.Axes.pie, + boxplot = wrap._apply_wrappers( + maxes.Axes.boxplot, wrap.standardize_1d, + wrap.boxplot_extras, wrap.apply_cycle, - ) - step = wrap._apply_wrappers( - maxes.Axes.step, + violinplot = wrap._apply_wrappers( + maxes.Axes.violinplot, wrap.standardize_1d, + wrap.violinplot_extras, + wrap.indicate_error, wrap.apply_cycle, ) - stem = wrap._apply_wrappers( - maxes.Axes.stem, - wrap._stem_extras, # TODO check this - wrap.standardize_1d, - ) - hlines = wrap._apply_wrappers( - maxes.Axes.hlines, - wrap.hlines_extras, # TODO check this - wrap.standardize_1d, - ) - vlines = wrap._apply_wrappers( - maxes.Axes.vlines, - wrap.vlines_extras, # TODO check this + pie = wrap._apply_wrappers( + maxes.Axes.pie, wrap.standardize_1d, + wrap.apply_cycle, + ) parametric = wrap._apply_wrappers( parametric, - wrap._parametric_extras, wrap.standardize_1d, wrap.apply_cmap, ) @@ -1949,10 +2037,6 @@ def number(self, num): maxes.Axes.tricontourf, wrap.apply_cmap, ) - hist2d = wrap._apply_wrappers( - maxes.Axes.hist2d, - wrap.apply_cmap, - ) spy = wrap._apply_wrappers( maxes.Axes.spy, wrap.apply_cmap, @@ -1965,3 +2049,7 @@ def number(self, num): maxes.Axes.matshow, wrap.apply_cmap, ) + hist2d = wrap._apply_wrappers( + maxes.Axes.hist2d, + wrap.apply_cmap, + ) diff --git a/proplot/axes/plot.py b/proplot/axes/plot.py index f62a85364..1308106b0 100644 --- a/proplot/axes/plot.py +++ b/proplot/axes/plot.py @@ -4,11 +4,19 @@ methods. "Wrapped" `~matplotlib.axes.Axes` methods accept the additional arguments documented in the wrapper function. """ +# NOTE: Two possible workflows are 1) make horizontal functions use wrapped +# vertical functions, then flip things around inside apply_cycle or by +# creating undocumented 'plot', 'scatter', etc. methods in Axes that flip +# arguments around by reading a 'orientation' key or 2) make separately +# wrapped chains of horizontal functions and vertical functions whose 'extra' +# wrappers jointly refer to a hidden helper function and create documented +# 'plotx', 'scatterx', etc. that flip arguments around before sending to +# superclass 'plot', 'scatter', etc. Opted for the latter approach. import functools import inspect import re import sys -from numbers import Integral, Real +from numbers import Integral import matplotlib.artist as martist import matplotlib.axes as maxes @@ -17,7 +25,9 @@ import matplotlib.colors as mcolors import matplotlib.container as mcontainer import matplotlib.contour as mcontour +import matplotlib.font_manager as mfonts import matplotlib.legend as mlegend +import matplotlib.lines as mlines import matplotlib.patches as mpatches import matplotlib.patheffects as mpatheffects import matplotlib.text as mtext @@ -69,85 +79,48 @@ ] -# Consistent keywords for styling apply_cmap-overridden plots -# TODO: Deprecate linewidth and linestyle interpretation. Think these -# already have flexible interpretation for all plotting funcs. +# Positional args that can be passed as out-of-order keywords. Used by standardize_1d +# NOTE: The 'barh' interpretation represent a breaking change from default +# (y, width, height, left) behavior. Want to have consistent interpretation +# of vertical or horizontal bar 'width' with 'width' key or 3rd positional arg. +# Interal hist() func uses positional arguments when calling bar() so this is fine. +POSITIONAL_ARGS_TRANSLATE = { + 'fill_between': ('x', 'y1', 'y2'), + 'fill_betweenx': ('y', 'x1', 'x2'), + 'vlines': ('x', 'ymin', 'ymax'), + 'hlines': ('y', 'xmin', 'xmax'), + 'bar': ('x', 'height', 'width', 'bottom'), + 'barh': ('y', 'height', 'width', 'left'), +} + +# Consistent keywords for cmap plots. Used by apply_cmap to pass correct plural +# or singular form to matplotlib function. STYLE_ARGS_TRANSLATE = { - 'contour': { - 'colors': 'colors', - 'linewidths': 'linewidths', - 'linestyles': 'linestyles', - }, - 'tricontour': { - 'colors': 'colors', - 'linewidths': 'linewidths', - 'linestyles': 'linestyles', - }, - 'pcolor': { - 'colors': 'edgecolors', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle', - }, - 'pcolormesh': { - 'colors': 'edgecolors', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle', - }, - 'pcolorfast': { - 'colors': 'edgecolors', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle', - }, - 'tripcolor': { - 'colors': 'edgecolors', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle', - }, - 'parametric': { - 'colors': 'color', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle', - }, - 'hexbin': { - 'colors': 'edgecolors', - 'linewidths': 'linewidths', - 'linestyles': 'linestyles', - }, - 'hist2d': { - 'colors': 'edgecolors', - 'linewidths': 'linewidths', - 'linestyles': 'linestyles', - }, - 'barbs': { - 'colors': 'barbcolor', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle', - }, - 'quiver': { # NOTE: linewidth/linestyle apply to *arrow outline* - 'colors': 'color', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle', - }, - 'streamplot': { - 'colors': 'color', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle', - }, - 'spy': { - 'colors': 'color', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle', - }, - 'matshow': { - 'colors': 'color', - 'linewidths': 'linewidth', - 'linestyles': 'linestyle', - }, - 'imshow': None, + 'contour': ('colors', 'linewidths', 'linestyles'), + 'tricontour': ('colors', 'linewidths', 'linestyles'), + 'pcolor': ('edgecolors', 'linewidth', 'linestyle'), + 'pcolormesh': ('edgecolors', 'linewidth', 'linestyle'), + 'pcolorfast': ('edgecolors', 'linewidth', 'linestyle'), + 'tripcolor': ('edgecolors', 'linewidth', 'linestyle'), + 'parametric': ('color', 'linewidth', 'linestyle'), + 'hexbin': ('edgecolors', 'linewidths', 'linestyles'), + 'hist2d': ('edgecolors', 'linewidths', 'linestyles'), + 'barbs': ('barbcolor', 'linewidth', 'linestyle'), + 'quiver': ('color', 'linewidth', 'linestyle'), # applied to arrow *outline* + 'streamplot': ('color', 'linewidth', 'linestyle'), + 'spy': ('color', 'linewidth', 'linestyle'), + 'matshow': ('color', 'linewidth', 'linestyle'), } -docstring.snippets['standardize.autoformat'] = """ +docstring.snippets['axes.autoformat'] = """ +data : dict-like, optional + A dict-like dataset container (e.g., `~pandas.DataFrame` or `~xarray.DataArray`). + If passed, positional arguments must be valid `data` keys and the arrays used for + plotting are retrieved with ``data[key]``. This is a native `matplotlib feature \ +`__ + previously restricted to just `~matplotlib.axes.Axes.plot` + and `~matplotlib.axes.Axes.scatter`. autoformat : bool, optional Whether *x* axis labels, *y* axis labels, axis formatters, axes titles, legend labels, and colorbar labels are automatically configured when @@ -155,7 +128,7 @@ to the plotting command. Default is :rc:`autoformat`. """ -docstring.snippets['axes.apply_cmap'] = """ +docstring.snippets['axes.cmap_norm'] = """ cmap : colormap spec, optional The colormap specifer, passed to the `~proplot.constructor.Colormap` constructor. @@ -170,32 +143,32 @@ extend : {{'neither', 'min', 'max', 'both'}}, optional Whether to assign unique colors to out-of-bounds data and draw "extensions" (triangles, by default) on the colorbar. -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. -N, levels : 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. +""" + +docstring.snippets['axes.levels_values'] = """ +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 level edges 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. -positive : bool, optional - If ``True``, automatically generated levels are positive with a minimum at zero. -negative : bool, optional - If ``True``, automatically generated levels are negative with a maximum at zero. -nozero : bool, optional - If ``True``, ``0`` is removed from the level list. + The number of level centers or a list of level centers. If the former, + `locator` is used to generate this many level centers at "nice" intervals. + If the latter, levels are inferred using `~proplot.utils.edges`. + This will override any `levels` input. +""" + +docstring.snippets['axes.vmin_vmax'] = """ +vmin, vmax : float, optional + Used to determine level locations if `levels` or `values` 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` are not provided, the + minimum and maximum data values are used. +""" + +docstring.snippets['axes.auto_levels'] = """ 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 @@ -203,10 +176,106 @@ `~matplotlib.ticker.MaxNLocator` with ``levels`` integer levels. locator_kw : dict-like, optional Passed to `~proplot.constructor.Locator`. +symmetric : bool, optional + If ``True``, automatically generated levels are symmetric + about zero. +positive : bool, optional + If ``True``, automatically generated levels are positive + with a minimum at zero. +negative : bool, optional + If ``True``, automatically generated levels are negative + with a maximum at zero. +nozero : bool, optional + If ``True``, ``0`` is removed from the level list. This is + mainly useful for `~matplotlib.axes.Axes.contour` plots. +""" + +_lines_docstring = """ +Plot {orientation} lines with flexible positional arguments and optionally +use different colors for "negative" and "positive" lines. + +Important +--------- +This function wraps `~matplotlib.axes.Axes.{prefix}lines`. + +Parameters +---------- +color, colors : color-spec or list thereof, optional + The line color(s). +linestyle, linestyles : linestyle-spec or list thereof, optional + The line style(s). +lw, linewidth, linewidths : linewidth-spec or list thereof, optional + The line width(s). +negpos : bool, optional + Whether to color lines greater than zero with `poscolor` and lines less + than zero with `negcolor`. +negcolor, poscolor : color-spec, optional + Colors to use for the negative and positive lines. Ignored if `negpos` + is ``False``. Defaults are :rc:`negcolor` and :rc:`poscolor`. + +See also +-------- +standardize_1d +apply_cycle +""" +docstring.snippets['axes.vlines'] = _lines_docstring.format( + prefix='v', orientation='vertical', +) +docstring.snippets['axes.hlines'] = _lines_docstring.format( + prefix='h', orientation='horizontal', +) + +_scatter_docstring = """ +Support `apply_cmap` features and support keywords that are more +consistent with `~{package}.axes.Axes.plot{suffix}` keywords. + +Important +--------- +This function wraps `~{package}.axes.Axes.scatter{suffix}`. + +Parameters +---------- +s, size, markersize : float or list of float, optional + The marker size(s). The units are optionally scaled by + `smin` and `smax`. +smin, smax : float, optional + The minimum and maximum marker size in units ``points^2`` used to scale + `s`. If not provided, the marker sizes are equivalent to the values in `s`. +c, color, markercolor : color-spec or list thereof, or array, optional + The marker fill color(s). If this is an array of scalar values, colors + will be generated using the colormap `cmap` and normalizer `norm`. +%(axes.vmin_vmax)s +%(axes.cmap_norm)s +%(axes.levels_values)s +%(axes.auto_levels)s +lw, linewidth, linewidths, markeredgewidth, markeredgewidths \ +: float or list thereof, optional + The marker edge width. +edgecolors, markeredgecolor, markeredgecolors \ +: color-spec or list thereof, optional + The marker edge color. + +Other parameters +---------------- +**kwargs + Passed to `~{package}.axes.Axes.scatter{suffix}`. + +See also +-------- +{package}.axes.Axes.bar{suffix} +standardize_1d +indicate_error +apply_cycle """ +docstring.snippets['axes.scatter'] = _scatter_docstring.format( + suffix='', package='matplotlib', +) % docstring.snippets +docstring.snippets['axes.scatterx'] = _scatter_docstring.format( + suffix='', package='proplot', +) % docstring.snippets _fill_between_docstring = """ -Supports overlaying and stacking successive columns of data, and permits +Support overlaying and stacking successive columns of data, and permits using different colors for "negative" and "positive" regions. Important @@ -232,7 +301,7 @@ 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 @@ -245,6 +314,7 @@ See also -------- +matplotlib.axes.Axes.fill_between{suffix} proplot.axes.Axes.area{suffix} standardize_1d apply_cycle @@ -257,7 +327,7 @@ ) _bar_docstring = """ -Supports grouping and stacking successive columns of data, and changes +Support grouping and stacking successive columns of data, and changes the default bar style. Important @@ -297,6 +367,7 @@ See also -------- +matplotlib.axes.Axes.bar{suffix} standardize_1d indicate_error apply_cycle @@ -308,15 +379,6 @@ x='y', height='right', bottom='left', suffix='h', ) -docstring.snippets['axes.lines'] = """ -negpos : bool, optional - Whether to color lines greater than zero with `poscolor` and lines less - than zero with `negcolor`. -negcolor, poscolor : color-spec, optional - Colors to use for the negative and positive lines. Ignored if `negpos` - is ``False``. Defaults are :rc:`negcolor` and :rc:`poscolor`. -""" - def _load_objects(): """ @@ -362,13 +424,6 @@ def _to_arraylike(data): return data -def _to_indexer(data): - """ - Return indexible attribute of array-like type. - """ - return getattr(data, 'iloc', data) - - def _to_ndarray(data): """ Convert arbitrary input to ndarray cleanly. Returns a masked @@ -377,7 +432,26 @@ def _to_ndarray(data): return np.atleast_1d(getattr(data, 'values', data)) -def default_latlon(self, *args, latlon=True, _method=None, **kwargs): +def _mask_array(mask, *args): + """ + Apply the mask to the input arrays. Values matching ``False`` are + set to `np.nan`. + """ + invalid = ~mask # True if invalid + args_masked = [] + for arg in args: + if arg.shape != invalid.shape: + raise ValueError('Shape mismatch between mask and array.') + arg_masked = arg.astype(np.float64) + if arg.size == 1 and invalid.item(): + arg_masked = np.nan + elif arg.size > 1: + arg_masked[invalid] = np.nan + args_masked.append(arg_masked) + return args_masked[0] if len(args_masked) == 1 else args_masked + + +def default_latlon(self, *args, latlon=True, **kwargs): """ Makes ``latlon=True`` the default for basemap plots. This means you no longer have to pass ``latlon=True`` if your data @@ -387,10 +461,11 @@ def default_latlon(self, *args, latlon=True, _method=None, **kwargs): --------- This function wraps {methods} for `~proplot.axes.BasemapAxes`. """ - return _method(self, *args, latlon=latlon, **kwargs) + method = kwargs.pop('_method') + return method(self, *args, latlon=latlon, **kwargs) -def default_transform(self, *args, transform=None, _method=None, **kwargs): +def default_transform(self, *args, transform=None, **kwargs): """ Makes ``transform=cartopy.crs.PlateCarree()`` the default for cartopy plots. This means you no longer have to @@ -404,35 +479,54 @@ def default_transform(self, *args, transform=None, _method=None, **kwargs): # Apply default transform # TODO: Do some cartopy methods reset backgroundpatch or outlinepatch? # Deleted comment reported this issue + method = kwargs.pop('_method') if transform is None: transform = PlateCarree() - result = _method(self, *args, transform=transform, **kwargs) - return result + return method(self, *args, transform=transform, **kwargs) -def _basemap_redirect(self, *args, _method=None, **kwargs): +def _basemap_redirect(self, *args, **kwargs): """ Docorator that calls the basemap version of the function of the same name. This must be applied as the innermost decorator. """ - name = _method.__name__ + method = kwargs.pop('_method') + name = method.__name__ if getattr(self, 'name', None) == 'proplot_basemap': return getattr(self.projection, name)(*args, ax=self, **kwargs) else: - return _method(self, *args, **kwargs) + return method(self, *args, **kwargs) -def _basemap_norecurse(self, *args, _method=None, **kwargs): +def _basemap_norecurse(self, *args, **kwargs): """ Decorator to prevent recursion in basemap method overrides. See `this post https://stackoverflow.com/a/37675810/4970632`__. """ - name = _method.__name__ - if getattr(_method, '_called_from_basemap', None): + method = kwargs.pop('_method') + name = method.__name__ + if getattr(method, '_called_from_basemap', None): return getattr(maxes.Axes, name)(self, *args, **kwargs) else: - with _state_context(_method, _called_from_basemap=True): - return _method(self, *args, **kwargs) + with _state_context(method, _called_from_basemap=True): + return method(self, *args, **kwargs) + + +def _get_data(data, *args): + """ + Try to convert positional `key` arguments to `data[key]`. If argument is string + it could be a valid positional argument like `fmt` so do not raise error. + """ + args = list(args) + for i, arg in enumerate(args): + if isinstance(arg, str): + try: + array = data[arg] + except KeyError: + pass + else: + args[i] = array + return args def _get_label(obj): @@ -459,8 +553,8 @@ def _get_labels(data, axis=0, always=True): # and column coordinates representing individual series. if axis not in (0, 1): raise ValueError(f'Invalid axis {axis}.') - _load_objects() labels = None + _load_objects() if isinstance(data, ndarray): if not always: pass @@ -473,15 +567,24 @@ def _get_labels(data, axis=0, always=True): elif isinstance(data, DataArray): if axis < data.ndim: labels = data.coords[data.dims[axis]] - elif always: + elif not always: + pass + else: labels = np.array([0]) # Pandas object elif axis == 0 and isinstance(data, (DataFrame, Series)): labels = data.index elif axis == 1 and isinstance(data, (DataFrame,)): labels = data.columns - elif axis == 1 and isinstance(data, (Series, Index)) and always: - labels = np.array([0]) + elif axis == 1 and isinstance(data, (Series, Index)): + if not always: + pass + else: + labels = np.array([0]) + # Everything else + # NOTE: We ensure data is at least 1D in _to_arraylike so this covers everything + else: + raise ValueError(f'Unrecognized array type {type(data)}.') return labels @@ -493,8 +596,8 @@ def _get_title(data, units=True): preferring the former, and append `(units)` if `units` is ``True``. If no names are available but units are available we just use the units string. """ - _load_objects() title = None + _load_objects() if isinstance(data, ndarray): pass # Xarray object with possible long_name, standard_name, and units attributes. @@ -519,6 +622,18 @@ def _get_title(data, units=True): return title +def _get_vert(**kwargs): + """ + Return ``True`` if ``vert`` is present or ``orientation`` is ``'vertical'``. + """ + vert = kwargs.pop('vert', None) + orientation = kwargs.pop('orientation', None) + if orientation and orientation not in ('horizontal', 'vertical'): + raise ValueError("Orientation must be either 'horizontal' or 'vertical'.") + vert = _not_none(vert=vert, orientation=(orientation == 'vertical'), default=True) + return vert, kwargs + + def _parse_string_coords(*args, which='x', **kwargs): """ Convert string arrays and lists to index coordinates. @@ -557,6 +672,7 @@ def _auto_format_1d( scatter = name in ('scatter',) hist = name in ('hist',) box = name in ('boxplot', 'violinplot') + pie = name in ('pie',) nocycle = name in ('stem', 'hlines', 'vlines', 'hexbin', 'parametric') # WARNING labels = _not_none( label=label, @@ -597,9 +713,11 @@ def _auto_format_1d( kw_format[sx + 'label'] = title # Handle string-type coordinates - if not hist: + if pie: + labels = _not_none(labels, x) # possibly inferred from DataFrame rows + elif not hist: x, kw_format = _parse_string_coords(x, which=sx, **kw_format) - if not hist and not box: + if not hist and not box and not pie: *ys, kw_format = _parse_string_coords(*ys, which=sy, **kw_format) if not scatter and x.ndim == 1 and x.size > 1 and _to_ndarray(x)[1] < _to_ndarray(x)[0]: # noqa: E501 kw_format[sx + 'reverse'] = True # auto reverse @@ -609,9 +727,11 @@ def _auto_format_1d( self.format(**kw_format) # Default legend or colorbar labels and title + # NOTE: Current idea is we only want default legend labels if this is + # an object with 'title' metadata and/or the 'coordinates' are string # WARNING: This will fail for any funcs not wrapped by apply_cycle. Keep # the 'nocycle' list updated until 'wrapper' functions disbanded. - if labels is None and not nocycle and autoformat: + if labels is None: labels = _get_labels(ys[0] if vert else x, axis=1) title = _get_title(labels) if not title and not any(isinstance(_, str) for _ in labels): @@ -635,7 +755,7 @@ def _auto_format_1d( @docstring.add_snippets -def standardize_1d(self, *args, autoformat=None, _method=None, **kwargs): +def standardize_1d(self, *args, data=None, autoformat=None, **kwargs): """ Interpret positional arguments for the "1D" plotting methods so usage is consistent. Positional arguments are standardized as follows: @@ -653,35 +773,44 @@ def standardize_1d(self, *args, autoformat=None, _method=None, **kwargs): Parameters ---------- - %(standardize.autoformat)s + %(axes.autoformat)s See also -------- apply_cycle indicate_error """ - name = _method.__name__ - fill = name in ('fill_between', 'fill_betweenx') + method = kwargs.pop('_method') + name = method.__name__ box = name in ('boxplot', 'violinplot') + twocoords = name in ('vlines', 'hlines', 'fill_between', 'fill_betweenx') + allowempty = name in ('plot', 'plotx', 'fill') autoformat = _not_none(autoformat, rc['autoformat']) - _load_objects() + + # Find and translate input args + args = list(args) + keys = POSITIONAL_ARGS_TRANSLATE.get(name, {}) + for idx, key in enumerate(keys): + if key in kwargs: + args.insert(idx, kwargs.pop(key)) + if data is not None: + args = _get_data(data, *args) if not args: - return _method(self, *args, **kwargs) + if allowempty: + return [] # match matplotlib behavior + else: + raise TypeError('Positional arguments are required.') # Parse input args # WARNING: This will temporarily add pseudo-x coordinates to hist, boxplot, and # pie but they are ignored when we reach apply_cycle. if len(args) == 1: x = None - y, *args = args - elif len(args) <= 4: # max signature is x, y1, y2, color - x, y, *args = args + ys, args = args[:1], args[1:] # single y + elif twocoords: + x, ys, args = args[0], args[1:3], args[3:] else: - raise ValueError(f'Expected 1-4 positional arguments, got {len(args)}.') - if fill and len(args) >= 1: - *ys, args = y, args[0], args[1:] - else: - ys = (y,) + x, ys, args = args[0], args[1:2], args[2:] ys = tuple(_to_arraylike(y) for y in ys) # Automatic formatting and coordinates @@ -701,7 +830,7 @@ def standardize_1d(self, *args, autoformat=None, _method=None, **kwargs): # Call function if box: kwargs.setdefault('positions', x) # *this* is how 'x' is passed to boxplot - return _method(self, x, *ys, *args, **kwargs) + return method(self, x, *ys, *args, **kwargs) def _auto_format_2d(self, x, y, *Zs, order='C', autoformat=False, **kwargs): @@ -713,20 +842,12 @@ def _auto_format_2d(self, x, y, *Zs, order='C', autoformat=False, **kwargs): projection = hasattr(self, 'projection') if x is None and y is None: Z = Zs[0] - if isinstance(Z, ndarray): - x = np.arange(Z.shape[-1]) # in case 1D - y = np.zeros(Z.shape) if Z.ndim == 1 else np.arange(Z.shape[0]) - elif isinstance(Z, DataArray): - x = Z.coords[Z.dims[-1]] # in case 1D - y = np.zeros(Z.shape) if Z.ndim == 1 else Z.coords[Z.dims[0]] - elif isinstance(Z, DataFrame): - x, y = Z.columns, Z.index - elif isinstance(Z, Series): # e.g. barbs or quiver - x, y = Z.index, np.zeros(Z.shape) - elif isinstance(Z, Index): # e.g. barbs or quiver - x, y = np.arange(Z.size), np.zeros(Z.shape) - else: # snould be unreachable due to _to_arraylike - raise ValueError(f'Unrecognized array type {type(Z)}.') + if _to_arraylike(Z).ndim == 1: + x = _get_labels(Z, axis=0) + y = np.zeros(Z.shape) # default barb() and quiver() behavior in matplotlib + else: + x = _get_labels(Z, axis=1) + y = _get_labels(Z, axis=0) if order == 'F': x, y = y, x @@ -992,7 +1113,7 @@ def _fix_basemap(x, y, *Zs, globe=False, projection=None): @docstring.add_snippets def standardize_2d( - self, *args, autoformat=None, order='C', globe=False, _method=None, **kwargs + self, *args, data=None, autoformat=None, order='C', globe=False, **kwargs ): """ Interpret positional arguments for the "2D" plotting methods so usage is @@ -1012,7 +1133,7 @@ def standardize_2d( Parameters ---------- - %(standardize.autoformat)s + %(axes.autoformat)s order : {{'C', 'F'}}, optional If ``'C'``, arrays should be shaped ``(y, x)``. If ``'F'``, arrays should be shaped ``(x, y)``. Default is ``'C'``. @@ -1033,13 +1154,17 @@ def standardize_2d( -------- apply_cmap """ - name = _method.__name__ + method = kwargs.pop('_method') + name = method.__name__ pcolor = name in ('pcolor', 'pcolormesh', 'pcolorfast') allow_1d = name in ('barbs', 'quiver') # these also allow 1D data autoformat = _not_none(autoformat, rc['autoformat']) - _load_objects() + + # Find and translate input args + if data is not None: + args = _get_data(data, *args) if not args: - return _method(self, *args, **kwargs) + raise TypeError('Positional arguments are required.') # Parse input args if len(args) > 5: @@ -1087,36 +1212,7 @@ def standardize_2d( kwargs['latlon'] = False # Call function - return _method(self, x, y, *Zs, **kwargs) - - -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, range_) in zip( - ('bar', 'box'), (bars, boxes), (barstd, boxstd), (barrange, boxrange), - ): - if b is not None or std is not None or range_ 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 release. ' - f"Please use '{prefix}stds' or '{prefix}pctiles' instead." - ) - if range_ is None and b: # means 'use the default range' - range_ = b - if std: - kwargs.setdefault(prefix + 'stds', range_) - else: - kwargs.setdefault(prefix + 'pctiles', range_) - return func(*args, **kwargs) - return wrapper + return method(self, x, y, *Zs, **kwargs) def _get_error_data( @@ -1134,21 +1230,25 @@ def _get_error_data( stds = stds_default elif stds is False or stds is None: stds = None - elif isinstance(stds, Real): - stds = sorted((-stds, stds)) - elif not np.iterable(stds) or len(stds) != 2: - raise ValueError('Expected scalar or length-2 tuple stdev specification.') + else: + stds = np.atleast_1d(stds) + if stds.size == 1: + stds = sorted((-stds, stds)) + elif stds.size != 2: + raise ValueError('Expected scalar or length-2 stdev specification.') # Parse pctiles arguments if pctiles is True: pctiles = pctiles_default elif pctiles is False or pctiles is None: pctiles = None - elif isinstance(pctiles, Real): - delta = (100 - pctiles) / 2.0 - pctiles = sorted((delta, 100 - delta)) - elif not np.iterable(pctiles) or len(pctiles) != 2: - raise ValueError('Expected length-2 tuple pctiles specification.') + else: + pctiles = np.atleast_1d(pctiles) + if pctiles.size == 1: + delta = (100 - pctiles) / 2.0 + pctiles = sorted((delta, 100 - delta)) + elif pctiles.size != 2: + raise ValueError('Expected scalar or length-2 pctiles specification.') # Incompatible settings if stds is not None and pctiles is not None: @@ -1216,7 +1316,6 @@ def _get_error_data( return err, label -@_deprecate_add_errorbars def indicate_error( self, *args, mean=None, means=None, median=None, medians=None, @@ -1229,7 +1328,7 @@ def indicate_error( shadelabel=False, fadelabel=False, 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, - _method=None, **kwargs + **kwargs ): """ Adds support for drawing error bars and error shading on-the-fly. Includes @@ -1322,7 +1421,8 @@ def indicate_error( h, err1, err2, ... The original plot object and the error bar or shading objects. """ - name = _method.__name__ + method = kwargs.pop('_method') + name = method.__name__ bar = name in ('bar',) violin = name in ('violinplot',) plot = name in ('plot', 'scatter') @@ -1365,13 +1465,13 @@ def indicate_error( # NOTE: Should not use plot() 'linewidth' for bar elements # NOTE: violinplot_extras passes some invalid keyword args with expectation # that indicate_error pops them and uses them for error bars. - method = kwargs.pop if violin else kwargs.get if bar else lambda *args: None + getter = kwargs.pop if violin else kwargs.get if bar else lambda *args: None boxmarker = _not_none(boxmarker, True if violin else False) capsize = _not_none(capsize, 3.0) - linewidth = _not_none(method('linewidth', None), method('lw', None), 1.0) + linewidth = _not_none(getter('linewidth', None), getter('lw', None), 1.0) barlinewidth = _not_none(barlinewidth=barlinewidth, barlw=barlw, default=linewidth) boxlinewidth = _not_none(boxlinewidth=boxlinewidth, boxlw=boxlw, default=4 * barlinewidth) # noqa: E501 - edgecolor = _not_none(method('edgecolor', None), 'k') + edgecolor = _not_none(getter('edgecolor', None), 'k') barcolor = _not_none(barcolor, edgecolor) boxcolor = _not_none(boxcolor, barcolor) shadecolor_infer = shadecolor is None @@ -1380,17 +1480,17 @@ def indicate_error( fadecolor = _not_none(fadecolor, shadecolor) # Draw dark and light shading - method = kwargs.pop if plot else kwargs.get - vert = method('vert', method('orientation', 'vertical') == 'vertical') + getter = kwargs.pop if plot else kwargs.get + vert = getter('vert', getter('orientation', 'vertical') == 'vertical') eobjs = [] - method = self.fill_between if vert else self.fill_betweenx + fill = self.fill_between if vert else self.fill_betweenx if fade: edata, label = _get_error_data( data, y, errdata=fadedata, stds=fadestds, pctiles=fadepctiles, stds_default=(-3, 3), pctiles_default=(0, 100), absolute=True, reduced=means or medians, label=fadelabel, ) - eobj = method( + eobj = fill( x, *edata, linewidth=0, label=label, color=fadecolor, alpha=fadealpha, zorder=fadezorder, ) @@ -1401,7 +1501,7 @@ def indicate_error( stds_default=(-2, 2), pctiles_default=(10, 90), absolute=True, reduced=means or medians, label=shadelabel, ) - eobj = method( + eobj = fill( x, *edata, linewidth=0, label=label, color=shadecolor, alpha=shadealpha, zorder=shadezorder, ) @@ -1443,7 +1543,7 @@ def indicate_error( # the shading. Never want legend entries for error bars. xy = (x, data) if name == 'violinplot' else (x, y) kwargs.setdefault('_errobjs', eobjs[:int(shade + fade)]) - res = obj = _method(self, *xy, *args, **kwargs) + res = obj = method(self, *xy, *args, **kwargs) # Apply inferrred colors to objects i = 0 @@ -1471,13 +1571,11 @@ def indicate_error( return res -def _plot_extras(self, *args, cmap=None, values=None, _method=None, **kwargs): +def _apply_plot(self, *args, cmap=None, values=None, **kwargs): """ - Adds the option `orientation` to change the default orientation of the - lines. See also `~proplot.axes.Axes.plotx`. + Apply horizontal or vertical lines. """ - if len(args) > 3: # e.g. with fmt string - raise ValueError(f'Expected 1-3 positional args, got {len(args)}.') + # Deprecated functionality if cmap is not None: warnings._warn_proplot( 'Drawing "parametric" plots with ax.plot(x, y, values=values, cmap=cmap) ' @@ -1486,72 +1584,58 @@ def _plot_extras(self, *args, cmap=None, values=None, _method=None, **kwargs): ) return self.parametric(*args, cmap=cmap, values=values, **kwargs) - # Call function - result = _method(self, *args, values=values, **kwargs) + # Plot line(s) + method = kwargs.pop('_method') + name = method.__name__ + sx = 'y' if 'x' in name else 'x' # i.e. plotx + objs = [] + while args: + # Support e.g. x1, y1, fmt, x2, y2, fmt2 input + # NOTE: Copied from _process_plot_var_args.__call__ to avoid relying + # on public API. ProPlot already supports passing extra positional + # arguments beyond x, y so can feed (x1, y1, fmt) through wrappers. + # Instead represent (x2, y2, fmt, ...) as successive calls to plot(). + iargs, args = args[:2], args[2:] + if args and isinstance(args[0], str): + iargs.append(args[0]) + args = args[1:] - # 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. - # for obj in result: - # xdata = obj.get_xdata() - # obj.sticky_edges.x.append(self.convert_xunits(min(xdata))) - # obj.sticky_edges.x.append(self.convert_yunits(max(xdata))) + # Call function + iobjs = method(self, *iargs, values=values, **kwargs) - return result + # Add sticky edges + # NOTE: Skip edges when error bars present or caps are flush against axes edge + if all(isinstance(obj, mlines.Line2D) for obj in iobjs): + for obj in iobjs: + data = getattr(obj, 'get_' + sx + 'data')() + convert = getattr(self, 'convert_' + sx + 'units') + edges = getattr(obj.sticky_edges, sx) + edges.append(convert(min(data))) + edges.append(convert(max(data))) + objs.extend(iobjs) -def _parametric_extras(self, *args, interp=0, _method=None, **kwargs): + return objs + + +def _plot_extras(self, *args, **kwargs): """ - Calls `~proplot.axes.Axes.parametric` and optionally interpolates values before - they get passed to `apply_cmap` and the colormap boundaries are drawn. Full - documentation is on public axes method. + Pre-processing for `plot`. """ - # Parse input arguments - # WARNING: This is separated from parametric because its task is analogous to - # the other wrappers -- standardizing arguments before ingestion by other - # functions. So far this only works for 1D *x* and *y* coordinates. - if len(args) == 3: - x, y, values = args - elif 'values' in kwargs: - values = kwargs.pop('values') - if len(args) == 1: - y = args[0] - x = np.arange(y.shape[-1]) - elif len(args) == 2: - x, y = args - else: - raise ValueError(f'Expected 1-3 positional arguments, got {len(args)}.') - else: - raise ValueError('Missing required keyword argument "values".') - x = _to_arraylike(x) - y = _to_arraylike(y) - values = _to_arraylike(values) - if any(_.ndim != 1 or _.size != x.size for _ in (x, y, values)): - raise ValueError( - f'x {x.shape}, y {y.shape}, and values {values.shape} ' - 'must be 1-dimensional and have the same size.' - ) + return _apply_plot(self, *args, **kwargs) - # Interpolate values to allow for smooth gradations between values - # (interp=False) or color switchover halfway between points - # (interp=True). Then optionally interpolate the colormap values. - if interp > 0: - x_orig, y_orig, v_orig = x, y, values - x, y, values = [], [], [] - for j in range(x_orig.shape[0] - 1): - idx = slice(None) - if j + 1 < x_orig.shape[0] - 1: - idx = slice(None, -1) - x.extend(np.linspace(x_orig[j], x_orig[j + 1], interp + 2)[idx].flat) - y.extend(np.linspace(y_orig[j], y_orig[j + 1], interp + 2)[idx].flat) - values.extend(np.linspace(v_orig[j], v_orig[j + 1], interp + 2)[idx].flat) - x, y, values = np.array(x), np.array(y), np.array(values) - # Call main function - return _method(self, x, y, values=values, **kwargs) +def _plotx_extras(self, *args, **kwargs): + """ + Pre-processing for `plotx`. + """ + # NOTE: The 'horizontal' orientation will be inferred by downstream + # wrappers using the function name. + return _apply_plot(self, *args, **kwargs) def _stem_extras( - self, *args, linefmt=None, basefmt=None, markerfmt=None, _method=None, **kwargs + self, *args, linefmt=None, basefmt=None, markerfmt=None, **kwargs ): """ Make `use_line_collection` the default to suppress warning message. @@ -1561,10 +1645,10 @@ def _stem_extras( # like 'r' or cycle colors like 'C0'. Cannot use full color names. # NOTE: Matplotlib defaults try to make a 'reddish' color the base and 'bluish' # color the stems. To make this more robust we temporarily replace the cycler - # with a negcolor/poscolor cycler, otherwise try to point default colors to the - # blush 'C0' and reddish 'C1' from the new default 'colorblind' cycler. + # with a negcolor/poscolor cycler. + method = kwargs.pop('_method') fmts = (linefmt, basefmt, markerfmt) - if not any(isinstance(fmt, str) and re.match(r'\AC[0-9]', fmt) for fmt in fmts): + if not any(isinstance(_, str) and re.match(r'\AC[0-9]', _) for _ in fmts): cycle = constructor.Cycle((rc['negcolor'], rc['poscolor']), name='_neg_pos') context = rc.context({'axes.prop_cycle': cycle}) else: @@ -1577,177 +1661,111 @@ def _stem_extras( kwargs['markerfmt'] = _not_none(markerfmt, linefmt[:-1] + 'o') kwargs.setdefault('use_line_collection', True) try: - return _method(self, *args, **kwargs) + return method(self, *args, **kwargs) except TypeError: - kwargs.pop('use_line_collection') # old version - return _method(self, *args, **kwargs) - - -def _mask_lines(y1, y2, mask): - """ - Apply the mask based on the input filter. - """ - y1_masked = y1.copy() - y2_masked = y2.copy() - if mask.size == 1: - if mask.item(): - y1_masked = y2_masked = np.nan - else: - if y1.size > 1: - _to_indexer(y1_masked)[mask] = np.nan - if y2.size > 1: - _to_indexer(y2_masked)[mask] = np.nan - return y1_masked, y2_masked + del kwargs['use_line_collection'] # older version + return method(self, *args, **kwargs) def _apply_lines( - self, *args, negpos=False, negcolor=None, poscolor=None, _method=None, **kwargs + self, *args, + color=None, colors=None, + linestyle=None, linestyles=None, + lw=None, linewidth=None, linewidths=None, + negpos=False, negcolor=None, poscolor=None, + **kwargs ): """ - Parse lines arguments. Support automatic *x* coordinates and default - "minima" at zero. + Apply hlines or vlines command. Support default "minima" at zero. """ - # Parse positional arguments - # Use default "base" position of zero as with bar and fill_between - x = 'x' if _method.__name__ == 'vlines' else 'y' - y = 'y' if x == 'x' else 'x' + # Parse input arguments + method = kwargs.pop('_method') + name = method.__name__ args = list(args) - if x in kwargs: - args.insert(0, kwargs.pop(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(f'Expected 1-3 positional arguments, got {len(args)}.') + colors = _not_none(color=color, colors=colors) + linestyles = _not_none(linestyle=linestyle, linestyles=linestyles) + linewidths = _not_none(lw=lw, linewidth=linewidth, linewidths=linewidths) + if len(args) > 3: + raise ValueError(f'Expected 1-3 positional args, got {len(args)}.') + if len(args) == 2: # empty possible + args.insert(1, np.array([0.0])) # default base # Support "negative" and "positive" lines - x, y1, y2 = args - if not negpos or kwargs.get('color', None) is None: + x, y1, y2, *args = args # standardized + if not negpos: # Plot basic lines - return _method(self, x, y1, y2, **kwargs) + if colors is not None: + kwargs['colors'] = colors + result = method(self, x, y1, y2, *args, **kwargs) + objs = (result,) else: # Plot negative and positive colors - y1 = _to_arraylike(y1) - y2 = _to_arraylike(y2) - y1neg, y2neg = _mask_lines(_to_ndarray(y2) >= _to_ndarray(y1), y1, y2) + if colors is not None: + warnings._warn_proplot( + f'{name}() argument color={colors!r} is incompatible with ' + 'negpos=True. Ignoring.' + ) + y1neg, y2neg = _mask_array(y2 < y1, y1, y2) color = _not_none(negcolor, rc['negcolor']) - negobj = _method(self, x, y1neg, y2neg, color=color, **kwargs) - y1pos, y2pos = _mask_lines(_to_ndarray(y2) < _to_ndarray(y1), y1, y2) + negobj = method(self, x, y1neg, y2neg, color=color, **kwargs) + y1pos, y2pos = _mask_array(y2 >= y1, y1, y2) color = _not_none(poscolor, rc['poscolor']) - posobj = _method(self, x, y1pos, y2pos, color=color, **kwargs) - return (negobj, posobj) + posobj = method(self, x, y1pos, y2pos, color=color, **kwargs) + objs = result = (negobj, posobj) + # Apply formatting unavailable in matplotlib + for obj in objs: + obj.set_linewidth(linewidths) # LineCollection setters + obj.set_linestyle(linestyles) -@docstring.add_snippets -def hlines_extras(self, *args, _method=None, **kwargs): - """ - Plot horizontal lines with flexible positional arguments and optionally - use different colors for "negative" and "positive" lines. + return result - Important - --------- - This function wraps {methods} - Parameters - ---------- - %(axes.lines)s - - See also - -------- - standardize_1d +@docstring.add_snippets +def vlines_extras(self, *args, **kwargs): """ - return _apply_lines(self, *args, _method=_method, **kwargs) + %(axes.vlines)s + """ + return _apply_lines(self, *args, **kwargs) @docstring.add_snippets -def vlines_extras(self, *args, _method=None, **kwargs): +def hlines_extras(self, *args, **kwargs): """ - Plot vertical lines with flexible positional arguments and optionally - use different colors for "negative" and "positive" lines. - - Important - --------- - This function wraps {methods} - - Parameters - ---------- - %(axes.lines)s - - See also - -------- - standardize_1d + %(axes.hlines)s """ - return _apply_lines(self, *args, _method=_method, **kwargs) + # NOTE: The 'horizontal' orientation will be inferred by downstream + # wrappers using the function name. + return _apply_lines(self, *args, **kwargs) -@docstring.add_snippets -def scatter_extras( +def _apply_scatter( self, *args, - s=None, size=None, markersize=None, - c=None, color=None, markercolor=None, smin=None, smax=None, + s=None, c=None, + color=None, markercolor=None, vmin=None, vmax=None, + size=None, markersize=None, smin=None, smax=None, cmap=None, cmap_kw=None, norm=None, norm_kw=None, - vmin=None, vmax=None, extend='neither', N=None, levels=None, values=None, + extend='neither', levels=None, N=None, values=None, symmetric=False, locator=None, locator_kw=None, lw=None, linewidth=None, linewidths=None, markeredgewidth=None, markeredgewidths=None, edgecolor=None, edgecolors=None, markeredgecolor=None, markeredgecolors=None, - _method=None, **kwargs + **kwargs ): """ - Adds keyword arguments to `~matplotlib.axes.Axes.scatter` that are more - consistent with the `~matplotlib.axes.Axes.plot` keyword arguments and - supports `apply_cmap` features. - - Important - --------- - This function wraps {methods} - - Parameters - ---------- - s, size, markersize : float or list of float, optional - The marker size(s). The units are optionally scaled by - `smin` and `smax`. - smin, smax : float, optional - The minimum and maximum marker size in units ``points^2`` used to scale - `s`. If not provided, the marker sizes are equivalent to the values in `s`. - c, color, markercolor : color-spec or list thereof, or array, optional - The marker fill color(s). If this is an array of scalar values, colors - will be generated using the colormap `cmap` and normalizer `norm`. - %(axes.apply_cmap)s - lw, linewidth, linewidths, markeredgewidth, markeredgewidths : \ -float or list thereof, optional - The marker edge width. - edgecolors, markeredgecolor, markeredgecolors : \ -color-spec or list thereof, optional - The marker edge color. - - Other parameters - ---------------- - **kwargs - Passed to `~matplotlib.axes.Axes.scatter`. - - See also - -------- - standardize_1d - indicate_error - apply_cycle + Apply scatter or scatterx markers. Permit up to 4 positional arguments + including `s` and `c`. """ # Manage input arguments - # NOTE: Parse 1d must come before this + method = kwargs.pop('_method') if len(args) > 4: raise ValueError(f'Expected 1-4 positional arguments, got {len(args)}.') args = list(args) if len(args) == 4: - c = args.pop(1) + c = _not_none(c_positional=args.pop(-1), c=c) if len(args) == 3: - s = args.pop(0) + s = _not_none(s_positional=args.pop(-1), s=s) # Apply some aliases for keyword arguments c = _not_none(c=c, color=color, markercolor=markercolor) @@ -1776,15 +1794,15 @@ def scatter_extras( carray = carray.ravel() norm, cmap, _, ticks = _build_discrete_norm( carray, # sample data for getting suitable levels - N=N, levels=levels, values=values, + levels=levels, N=N, values=values, cmap=cmap, norm=norm, norm_kw=norm_kw, vmin=vmin, vmax=vmax, extend=extend, symmetric=symmetric, locator=locator, locator_kw=locator_kw, ) # Fix 2D arguments but still support scatter(x_vector, y_2d) usage + # NOTE: numpy.ravel() preserves masked arrays # NOTE: Since we are flattening vectors the coordinate metadata is meaningless, # so converting to ndarray and stripping metadata is no problem. - # NOTE: numpy.ravel() preserves masked arrays if len(args) == 2 and all(_to_ndarray(arg).squeeze().ndim > 1 for arg in args): args = tuple(np.ravel(arg) for arg in args) @@ -1796,7 +1814,9 @@ def scatter_extras( if smax is None: smax = smax_true s = smin + (smax - smin) * (np.array(s) - smin_true) / (smax_true - smin_true) - obj = objs = _method( + + # Call function + obj = objs = method( self, *args, c=c, s=s, cmap=cmap, norm=norm, linewidths=lw, edgecolors=ec, **kwargs ) @@ -1805,81 +1825,94 @@ def scatter_extras( for iobj in objs: iobj._colorbar_extend = extend iobj._colorbar_ticks = ticks + return obj +@docstring.add_snippets +def scatter_extras(self, *args, **kwargs): + """ + %(axes.scatter)s + """ + return _apply_scatter(self, *args, **kwargs) + + +@docstring.add_snippets +def scatterx_extras(self, *args, **kwargs): + """ + %(axes.scatterx)s + """ + # NOTE: The 'horizontal' orientation will be inferred by downstream + # wrappers using the function name. + return _apply_scatter(self, *args, **kwargs) + + def _apply_fill_between( - self, *args, negcolor=None, poscolor=None, negpos=None, - lw=None, linewidth=None, stack=None, stacked=None, where=None, - _method=None, **kwargs + self, *args, where=None, negpos=None, negcolor=None, poscolor=None, + lw=None, linewidth=None, color=None, facecolor=None, + stack=None, stacked=None, **kwargs ): """ - Helper function that powers `fill_between` and `fill_betweenx`. + Apply `fill_between` or `fill_betweenx` shading. Permit up to 4 + positional arguments including `where`. """ - # Parse input arguments as follows: - # * Permit using 'x', 'y1', and 'y2' or 'y', 'x1', and 'x2' as keyword - # arguments. - # * When negpos is True, use fill_between(x, y1=0, y2) as the default - # instead of fill_between(x, y1, y2=0). - name = _method.__name__ + # Parse input arguments + method = kwargs.pop('_method') + name = method.__name__ sx = 'y' if 'x' in name else 'x' # i.e. fill_betweenx sy = 'x' if sx == 'y' else 'y' - stack = _not_none(stack=stack, stacked=stacked) args = list(args) - if sx in kwargs: # keyword 'x' - args.insert(0, kwargs.pop(sx)) - if len(args) == 1: - args.insert(0, np.arange(len(args[0]))) - for yi in (sy + '1', sy + '2'): - if yi in kwargs: # keyword 'y' - args.append(kwargs.pop(yi)) - if len(args) == 2: - args.append(0) - elif len(args) == 3 and stack: # ignore argument 3 down-the-line + stack = _not_none(stack=stack, stacked=stacked) + color = _not_none(color=color, facecolor=facecolor) + linewidth = _not_none(lw=lw, linewidth=linewidth, default=0) + if len(args) > 4: + raise ValueError(f'Expected 1-4 positional args, got {len(args)}.') + if len(args) == 4: + where = _not_none(where_positional=args.pop(3), where=where) + if len(args) == 3 and stack: warnings._warn_proplot( f'{name}() cannot have three positional arguments with stack=True. ' - 'Ignoring third argument.' + 'Ignoring second argument.' ) - elif len(args) != 3: - raise ValueError(f'Expected 2-3 positional args, got {len(args)}.') + if len(args) == 2: # empty possible + args.insert(1, np.array([0.0])) # default base # Draw patches with default edge width zero - # TODO: Test 'negpos' with 3 arguments (i.e. zero location is not at zero) x, y1, y2 = args - x = _to_arraylike(x) - y1 = _to_arraylike(y1) - y2 = _to_arraylike(y2) - kwargs.update({'linewidth': _not_none(lw=lw, linewidth=linewidth, default=0)}) - if not negpos or kwargs.get('color') is not None: + kwargs['linewidth'] = linewidth + if not negpos: # Plot basic patches - kwargs.update({'stack': stack, 'where': where}) - result = _method(self, x, y1, y2, **kwargs) + kwargs.update({'where': where, 'stack': stack}) + if color is not None: + kwargs['color'] = color + result = method(self, x, y1, y2, **kwargs) objs = (result,) else: # Plot negative and positive patches - kwargs.setdefault('interpolate', True) - message = f'{name}() argument {{}}={{!r}} is incompatible with negpos=True. Ignoring.' # noqa: E501 - if where is not None: - warnings._warn_proplot(message.format('where', where)) - if stack: - warnings._warn_proplot(message.format('stack', stack)) if y1.ndim > 1 or y2.ndim > 1: raise ValueError(f'{name}() arguments with negpos=True must be 1D.') - negcolor = _not_none(negcolor, rc['negcolor']) - poscolor = _not_none(poscolor, rc['poscolor']) - obj1 = _method(self, x, y1, y2, where=(y1 < y2), color=negcolor, **kwargs) - obj2 = _method(self, x, y1, y2, where=(y1 >= y2), color=poscolor, **kwargs) - result = objs = (obj1, obj2) # may be tuple of tuples due to apply_cycle + kwargs.setdefault('interpolate', True) + for key, arg in (('where', where), ('stack', stack), ('color', color)): + if arg is not None: + warnings._warn_proplot( + f'{name}() argument {key}={arg!r} is incompatible with ' + 'negpos=True. Ignoring.' + ) + color = _not_none(negcolor, rc['negcolor']) + negobj = method(self, x, y1, y2, where=(y2 < y1), color=color, **kwargs) + color = _not_none(poscolor, rc['poscolor']) + posobj = method(self, x, y1, y2, where=(y2 >= y1), color=color, **kwargs) + result = objs = (posobj, negobj) # may be tuple of tuples due to apply_cycle # 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: Could also retrieve data from PolyCollection but that's tricky. - xsides = (np.min(_to_ndarray(x)), np.max(_to_ndarray(x))) + # NOTE: Standardize function guarantees ndarray input by now + xsides = (np.min(x), np.max(x)) ysides = [] - if y1.size == 1: - ysides.append(_to_ndarray(y1).item()) - if y2.size == 1: - ysides.append(_to_ndarray(y2).item()) + for y in (y1, y2): + if y.size == 1: + ysides.append(y.item()) objs = tuple(obj for _ in objs for obj in (_ if isinstance(_, tuple) else (_,))) for obj in objs: for s, sides in zip((sx, sy), (xsides, ysides)): @@ -1891,116 +1924,131 @@ def _apply_fill_between( @docstring.add_snippets -def fill_between_extras(self, *args, _method=None, **kwargs): +def fill_between_extras(self, *args, **kwargs): """ %(axes.fill_between)s """ - return _apply_fill_between(self, *args, _method=_method, **kwargs) + return _apply_fill_between(self, *args, **kwargs) @docstring.add_snippets -def fill_betweenx_extras(self, *args, _method=None, **kwargs): +def fill_betweenx_extras(self, *args, **kwargs): """ %(axes.fill_betweenx)s """ - return _apply_fill_between(self, *args, _method=_method, **kwargs) + # NOTE: The 'horizontal' orientation will be inferred by downstream + # wrappers using the function name. + return _apply_fill_between(self, *args, **kwargs) -def _hist_extras(self, x, bins=None, _method=None, **kwargs): +def _hist_extras(self, *args, **kwargs): """ - Forces `bar_extras` to interpret `width` as literal rather than relative - to step size and enforces all arguments after `bins` are keyword-only. + Force `bar_extras` to interpret `width` as literal rather than relative. """ + method = kwargs.pop('_method') with _state_context(self, _absolute_bar_width=True): - return _method(self, x, bins=bins, **kwargs) + return method(self, *args, **kwargs) -@docstring.add_snippets -def bar_extras( - self, x=None, height=None, width=0.8, bottom=None, *, - vert=None, orientation='vertical', stack=None, stacked=None, - lw=None, linewidth=None, edgecolor='black', # default edge color black - negpos=False, negcolor=None, poscolor=None, - _method=None, **kwargs -): +def _convert_bar_width(x, width=1, ncols=1): """ - %(axes.bar)s + Convert bar plot widths from relative to coordinate spacing. Relative + widths are much more convenient for users. """ - # Parse arguments - # WARNING: Implementation is really weird... we flip around arguments for horizontal - # plots only to flip them back in apply_cycle when iterating through columns. - if vert is not None: - orientation = 'vertical' if vert else 'horizontal' - if orientation == 'horizontal': - x, bottom, width, height = bottom, x, height, width + # WARNING: This will fail for non-numeric non-datetime64 singleton + # datatypes but this is good enough for vast majority of 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): + # Avoid integer timedelta truncation + x_step = x_step.astype('timedelta64[ns]') + return width * x_step / ncols + +def _apply_bar( + self, *args, stack=None, stacked=None, + lw=None, linewidth=None, color=None, facecolor=None, edgecolor='black', + negpos=False, negcolor=None, poscolor=None, **kwargs +): + """ + Apply bar or barh command. Support default "minima" at zero. + """ # Parse args # TODO: Stacked feature is implemented in `apply_cycle`, but makes more - # sense do document here; figure out way to move it here? - if kwargs.get('left', None) is not None: - warnings._warn_proplot('bar() keyword "left" is deprecated. Use "x" instead.') - x = kwargs.pop('left') - if x is None and height is None: - raise ValueError('bar() requires at least 1 positional argument, got 0.') - elif height is None: - x, height = None, x - args = (x, height) + # sense do document here. Figure out way to move it here? + method = kwargs.pop('_method') + name = method.__name__ stack = _not_none(stack=stack, stacked=stacked) + color = _not_none(color=color, facecolor=facecolor) linewidth = _not_none(lw=lw, linewidth=linewidth, default=rc['patch.linewidth']) - kwargs.update({'width': width, 'bottom': bottom, 'orientation': orientation}) kwargs.update({'linewidth': linewidth, 'edgecolor': edgecolor}) - - # Call func - # NOTE: This *must* also be wrapped by apply_cycle, which ultimately - # permutes back the x/bottom args for horizontal bars! Need to clean up. - if not negpos or kwargs.get('color') is not None: + if len(args) > 4: + raise ValueError(f'Expected 1-4 positional args, got {len(args)}.') + if len(args) == 4 and stack: + warnings._warn_proplot( + f'{name}() cannot have four positional arguments with stack=True. ' + 'Ignoring fourth argument.' # i.e. ignore default 'bottom' + ) + if len(args) == 2: + args.append(np.array([0.8])) # default width + if len(args) == 3: + args.append(np.array([0.0])) # default base + + # Call func after converting bar width + x, h, w, b = args + if not stack and not getattr(self, '_absolute_bar_width', None): + ncols = 1 if h.ndim == 1 else h.shape[1] + w = _convert_bar_width(x, w, ncols=ncols) + if not negpos: # Draw simple bars kwargs['stack'] = stack - return _method(self, *args, **kwargs) + if color is not None: + kwargs['color'] = color + return method(self, x, h, w, b, **kwargs) else: # Draw negative and positive bars - stack = kwargs.pop('stack', None) - message = 'bar() argument {}={!r} is incompatible with negpos=True. Ignoring.' - if stack: - warnings._warn_proplot(message.format('stack', stack)) - height = _to_ndarray(height) - if height.ndim > 1: - raise ValueError('bar() heights with negpos=True must be 1D.') - height1 = height.astype(np.float64) # always copies by default - height1[height >= 0] = np.nan - negcolor = _not_none(negcolor, rc['negcolor']) - obj1 = _method(self, x, height1, color=negcolor, **kwargs) - height2 = height.astype(np.float64) - height2[height < 0] = np.nan - poscolor = _not_none(poscolor, rc['poscolor']) - obj2 = _method(self, x, height2, color=poscolor, **kwargs) - return (obj1, obj2) + for key, arg in (('stack', stack), ('color', color)): + if arg is not None: + warnings._warn_proplot( + f'{name}() argument {key}={arg!r} is incompatible with ' + 'negpos=True. Ignoring.' + ) + if x.ndim > 1 or h.ndim > 1: + raise ValueError(f'{name}() arguments with negpos=True must be 1D.') + hneg = _mask_array(h < b, h) + color = _not_none(negcolor, rc['negcolor']) + negobj = method(self, x, hneg, w, b, color=color, **kwargs) + hpos = _mask_array(h >= b, h) + color = _not_none(poscolor, rc['poscolor']) + posobj = method(self, x, hpos, w, b, color=color, **kwargs) + return (negobj, posobj) + + +@docstring.add_snippets +def bar_extras(self, *args, **kwargs): + """ + %(axes.bar)s + """ + return _apply_bar(self, *args, **kwargs) @docstring.add_snippets -def barh_extras(self, y=None, right=None, width=0.8, left=None, _method=None, **kwargs): +def barh_extras(self, *args, **kwargs): """ %(axes.barh)s """ - # NOTE: Also the second positional argument is called 'right' so that 'width' - # always means the width of *bars*. - # NOTE: This reverses the input arguemnts by setting y-->bottom, left-->x, - # width-->height, height-->width. Arguments then passed through standardize_1d - # indicate_error and reversed back by apply_cycle. - # 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! - _method # avoid U100 error - height = _not_none(height=kwargs.pop('height', None), 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, width=right, height=height, bottom=y, **kwargs) + return _apply_bar(self, *args, **kwargs) def boxplot_extras( self, *args, - orientation=None, means=None, + mean=None, means=None, fill=True, fillcolor=None, fillalpha=None, lw=None, linewidth=None, color=None, edgecolor=None, @@ -2012,14 +2060,14 @@ def boxplot_extras( mediancolor=None, medianlw=None, medianlinewidth=None, meanls=None, meanlinestyle=None, medianls=None, medianlinestyle=None, marker=None, markersize=None, - _method=None, **kwargs + **kwargs ): """ - Adds convenient keyword arguments and changes the default boxplot style. + Support convenient keyword arguments and change the default boxplot style. Important --------- - This function wraps {methods} + This function wraps `~matplotlib.axes.Axes.boxplot`. Parameters ---------- @@ -2030,7 +2078,7 @@ def boxplot_extras( orientation : {{None, 'vertical', 'horizontal'}}, optional Alternative to the native `vert` keyword arg. Added for consistency with `~matplotlib.axes.Axes.bar`. - means : bool, optional + mean, means : bool, optional If ``True``, this passes ``showmeans=True`` and ``meanline=True`` to `~matplotlib.axes.Axes.boxplot`. fill : bool, optional @@ -2066,16 +2114,21 @@ def boxplot_extras( Other parameters ---------------- **kwargs - Passed to the matplotlib plotting method. + Passed to `~matplotlib.axes.Axes.boxplot`. See also -------- + matplotlib.axes.Axes.boxplot proplot.axes.Axes.boxes standardize_1d indicate_error apply_cycle """ # Parse keyword args + # NOTE: For some reason native violinplot() uses _get_lines for + # property cycler. We dot he same here. + vert, kwargs = _get_vert(**kwargs) # interpret 'orientation' and 'vert' + method = kwargs.pop('_method') fill = fill is True or fillcolor is not None or fillalpha is not None fillalpha = _not_none(fillalpha, default=0.7) if fill and fillcolor is None: @@ -2091,19 +2144,12 @@ def boxplot_extras( medianlinewidth = _not_none(medianlw=medianlw, medianlinewidth=medianlinewidth) meanlinestyle = _not_none(meanls=meanls, meanlinestyle=meanlinestyle) medianlinestyle = _not_none(medianls=medianls, medianlinestyle=medianlinestyle) - if means or kwargs.get('showmeans'): + means = _not_none(mean=mean, means=means, showmeans=kwargs.get('showmeans')) + if means: kwargs['showmeans'] = kwargs['meanline'] = True - if orientation == 'horizontal': - kwargs['vert'] = False - elif orientation == 'vertical': - kwargs['vert'] = True - elif orientation is not None: - raise ValueError("Orientation must be 'horizontal' or 'vertical', got {orientation!r}.") # noqa: E501 # Call function - if len(args) > 2: - raise ValueError(f'Expected 1-2 positional args, got {len(args)}.') - obj = _method(self, *args, **kwargs) + obj = method(self, *args, vert=vert, **kwargs) if not args: return obj @@ -2157,11 +2203,8 @@ def boxplot_extras( def violinplot_extras( - self, *args, orientation=None, - fillcolor=None, fillalpha=None, - lw=None, linewidth=None, - color=None, edgecolor=None, - _method=None, **kwargs + self, *args, fillcolor=None, fillalpha=None, + lw=None, linewidth=None, color=None, edgecolor=None, **kwargs ): """ Adds convenient keyword arguments and changes the default violinplot style @@ -2173,7 +2216,7 @@ def violinplot_extras( Important --------- - This function wraps {methods} + This function wraps `~matplotlib.axes.Axes.violinplot`. Parameters ---------- @@ -2203,35 +2246,27 @@ def violinplot_extras( See also -------- + matplotlib.axes.Axes.violinplot proplot.axes.Axes.violins standardize_1d indicate_error apply_cycle """ # Parse keyword args - # NOTE: Some of these are caught by indicate_error for drawing error bars - if orientation == 'horizontal': - kwargs['vert'] = False - elif orientation == 'vertical': - kwargs['vert'] = True - elif orientation is not None: - raise ValueError("Orientation must be 'horizontal' or 'vertical', got {orientation!r}.") # noqa: E501 + vert, kwargs = _get_vert(**kwargs) # interpret 'orientation' and 'vert' + method = kwargs.pop('_method') color = _not_none(color=color, edgecolor=edgecolor, default='black') linewidth = _not_none(lw=lw, linewidth=linewidth, default=0.8) fillalpha = _not_none(fillalpha, default=0.7) - if kwargs.pop('showextrema', None): - warnings._warn_proplot('Ignoring showextrema=True.') - if 'showmeans' in kwargs: # native argument overridden by proplot handling - kwargs.setdefault('mean', kwargs.pop('showmeans')) - if 'showmedians' in kwargs: # native argument overridden by proplot handling - kwargs.setdefault('median', kwargs.pop('showmedians')) kwargs.update({'showmeans': False, 'showmedians': False, 'showextrema': False}) kwargs.setdefault('capsize', 0) # caps are redundant for violin plots + kwargs.setdefault('means', kwargs.pop('showmeans', None)) # for indicate_error + kwargs.setdefault('medians', kwargs.pop('showmedians', None)) + if kwargs.pop('showextrema', None): + warnings._warn_proplot('Ignoring showextrema=True.') # Call function - if len(args) > 2: - raise ValueError(f'Expected 1-2 positional args, got {len(args)}.') - obj = result = _method(self, *args, linewidth=linewidth, **kwargs) + obj = result = method(self, *args, vert=vert, linewidth=linewidth, **kwargs) if not args: return result @@ -2340,11 +2375,10 @@ def _update_text(self, props): def text_extras( - self, x=0, y=0, text='', transform='data', + self, x=0, y=0, s='', transform='data', *, family=None, fontfamily=None, fontname=None, fontsize=None, size=None, border=False, bordercolor='w', borderwidth=2, borderinvert=False, - bbox=False, bboxcolor='w', bboxstyle='round', bboxalpha=0.5, bboxpad=None, - _method=None, **kwargs + bbox=False, bboxcolor='w', bboxstyle='round', bboxalpha=0.5, bboxpad=None, **kwargs ): """ Enables specifying `tranform` with a string name and adds a feature for @@ -2358,7 +2392,7 @@ def text_extras( ---------- x, y : float The *x* and *y* coordinates for the text. - text : str + s : str The text string. transform \ : {{'data', 'axes', 'figure'}} or `~matplotlib.transforms.Transform`, optional @@ -2398,6 +2432,10 @@ def text_extras( ---------------- **kwargs Passed to `~matplotlib.axes.Axes.text`. + + See also + -------- + matplotlib.axes.Axes.text """ # Parse input args # NOTE: Previously issued warning if fontname did not match any of names @@ -2407,12 +2445,13 @@ def text_extras( # NOTE: Do not emit warning if user supplied conflicting properties # because matplotlib has like 100 conflicting text properties for which # it doesn't emit warnings. Prefer not to fix all of them. + method = kwargs.pop('_method') fontsize = _not_none(fontsize, size) fontfamily = _not_none(fontname, fontfamily, family) if fontsize is not None: - try: - rc._scale_font(fontsize) # *validate* but do not translate - except KeyError: + if fontsize in mfonts.font_scalings: + fontsize = rc._scale_font(fontsize) + else: fontsize = units(fontsize, 'pt') kwargs['fontsize'] = fontsize if fontfamily is not None: @@ -2425,7 +2464,7 @@ def text_extras( # Apply monkey patch to text object # TODO: Why only support this here, and not in arbitrary places throughout # rest of matplotlib API? Units engine needs better implementation. - obj = _method(self, x, y, text, transform=transform, **kwargs) + obj = method(self, x, y, s, transform=transform, **kwargs) obj.update = _update_text.__get__(obj) obj.update({ 'border': border, @@ -2441,27 +2480,6 @@ def text_extras( return obj -def _convert_bar_width(x, width=1, ncols=1): - """ - Convert bar plot widths from relative to coordinate spacing. Relative - widths are much more convenient for users. - """ - # WARNING: This will fail for non-numeric non-datetime64 singleton - # datatypes but this is good enough for vast majority of 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): - # Avoid integer timedelta truncation - x_step = x_step.astype('timedelta64[ns]') - return width * x_step / ncols - - def _iter_objs_labels(objs): """ Retrieve the (object, label) pairs for objects with actual labels @@ -2550,7 +2568,7 @@ def apply_cycle( label=None, labels=None, values=None, legend=None, legend_kw=None, colorbar=None, colorbar_kw=None, - _errobjs=None, _method=None, **kwargs + **kwargs ): """ Adds features for controlling colors in the property cycler and drawing @@ -2587,7 +2605,7 @@ def apply_cycle( default location is used. Valid locations are described in `~proplot.axes.Axes.legend`. legend_kw : dict-like, optional - Ignored if `legend` is ``None``. Extra keyword args for our call + Ignored if `legend` is ``None``. Extra keyword args for the call to `~proplot.axes.Axes.legend`. colorbar : bool, int, or str, optional If not ``None``, this is a location specifying where to draw an *inset* @@ -2595,7 +2613,7 @@ def apply_cycle( default location is used. Valid locations are described in `~proplot.axes.Axes.colorbar`. colorbar_kw : dict-like, optional - Ignored if `colorbar` is ``None``. Extra keyword args for our call + Ignored if `colorbar` is ``None``. Extra keyword args for the call to `~proplot.axes.Axes.colorbar`. Other parameters @@ -2610,9 +2628,11 @@ def apply_cycle( proplot.constructor.Cycle proplot.constructor.Colors """ - # NOTE: Requires standardize_1d wrapper before reaching this. Also note - # that the 'x' coordinates are sometimes ignored below. - name = _method.__name__ + # Parse input arguments + # NOTE: Requires standardize_1d wrapper before reaching this. + method = kwargs.pop('_method') + errobjs = kwargs.pop('_errobjs') + name = method.__name__ plot = name in ('plot',) scatter = name in ('scatter',) fill = name in ('fill_between', 'fill_betweenx') @@ -2621,34 +2641,17 @@ def apply_cycle( pie = name in ('pie',) box = name in ('boxplot', 'violinplot') violin = name in ('violinplot',) - if not args: - return _method(self, *args, **kwargs) - x, y, *args = args - ys = (y,) - if fill and len(args) >= 1: - *ys, args = y, args[0], args[1:] - ncols = 1 if pie or box or y.ndim == 1 else y.shape[1] - - # Parse keyword args - # NOTE: Already pull out labels/values from legend_kw/colorbar_kw in _auto_format_1d cycle_kw = cycle_kw or {} legend_kw = legend_kw or {} colorbar_kw = colorbar_kw or {} labels = _not_none(label=label, values=values, labels=labels) - if pie: # add x coordinates as default pie chart labels - labels = _not_none(labels, x) # TODO: move to pie wrapper? - # Bar plot width and origin - barh = stack = False - if bar or fill: + # Special cases + # TODO: Support stacking vlines/hlines because it would be really easy + x, y, *args = args + stack = False + if fill or bar: stack = kwargs.pop('stack', False) - if bar: - barh = kwargs.get('orientation', None) == 'horizontal' - width = kwargs.pop('width', 0.8) # 'width' for bar *and* barh (see bar_extras) - if not stack and not getattr(self, '_absolute_bar_width', None): - width = _convert_bar_width(x, width, ncols) - kwargs['height' if barh else 'width'] = width - kwargs.setdefault('x' if barh else 'bottom', 0) # Update the property cycler apply_manually = {} @@ -2659,18 +2662,14 @@ def apply_cycle( cycle = constructor.Cycle(*cycle_args, **cycle_kw) apply_manually = _update_cycle(self, cycle, scatter=scatter, **kwargs) - # Handle legend labels. Several scenarios: - # 1. Always prefer input labels - # 2. Always add labels if this is a *named* dimension. - # 3. Even if not *named* dimension add labels if labels are string - # Then convert labels to string because e.g. scatter() applies default label - # if input is False-ey. So numeric '0' would be overridden. + # Handle legend labels + ncols = 1 if pie or box or y.ndim == 1 else y.shape[1] if pie or box: # Functions handle multiple labels on their own # NOTE: Using boxplot() without this will overwrite labels previously # set along the x-axis by _auto_format_1d. if not violin and labels is not None: - kwargs['labels'] = _to_ndarray(labels) # error raised down the line + kwargs['labels'] = _to_ndarray(labels) else: # Check and standardize labels # NOTE: Must convert to ndarray or can get singleton DataArrays @@ -2688,42 +2687,45 @@ def apply_cycle( for i in range(ncols): # Property cycling for scatter plots # NOTE: See comments in _update_cycle - kw = kwargs.copy() + ikwargs = kwargs.copy() if apply_manually: props = next(self._get_lines.prop_cycler) for prop, key in apply_manually.items(): - kw[key] = props[prop] + ikwargs[key] = props[prop] # The x coordinates for bar plots - ix, iy, *_ = x, *ys # samples + ix, iy, iargs = x, y, args if bar and stack and iy.ndim > 1: - kw['x' if barh else 'bottom'] = _to_indexer(iy)[:, :i].sum(axis=1) + args[2] = iy[:, :i].sum(axis=1) # 4th positional arg is 'bototm' if bar and not stack: - offset = width * (i - 0.5 * (ncols - 1)) + offset = args[1] * (i - 0.5 * (ncols - 1)) # 3rd positional arg is 'width' ix = x + offset # The y coordinates and labels # WARNING: If stack=True then we always *ignore* second argument passed to # fill_between. Warning should be issued by fill_between_extras in this case. - if pie or box: # only ever have one y value, cannot have legend labels - iys = ys[:1] - elif fill and stack: # ignore argument 3 as warned in _apply_fill_between - iys = tuple(iy if iy.ndim == 1 else _to_indexer(iy)[:, :ii].sum(axis=1) for ii in (i, i + 1)) # noqa: E501 - kw['label'] = labels[i] or None + if pie or box: # only ever have one y value + pass + elif fill and stack: # ignore argument 2 as warned in _apply_fill_between + iy, *iargs = ( + args[0] if args[0].ndim == 1 else args[0][:, :ii].sum(axis=1) + for ii in (i, i + 1) + ) + ikwargs['label'] = labels[i] or None else: - iys = tuple(iy if iy.ndim == 1 else _to_indexer(iy)[:, i] for iy in ys) - kw['label'] = labels[i] or None + iy, *iargs = ( # careful to support e.g. hist() bins positional arg + arg if _to_arraylike(arg).ndim == 1 else arg[:, i] + for arg in (y, *args) + ) + ikwargs['label'] = labels[i] or None # Call function for relevant column - # TODO: Why pull result out of singleton list? - iargs = () - if barh: # special case, use kwargs only! - kw.update({'bottom': ix, 'width': iys[0]}) # always single y vector - elif pie or hist or box: - iargs = iys - else: # has x-coordinates, and maybe more than one y - iargs = ix, *iys - obj = _method(self, *iargs, *args, **kw) + # NOTE: Should have already applied 'x' coordinates with keywords + # or as labels by this point for funcs where we omit them. + if pie or box or hist: + obj = method(self, iy, *iargs, **ikwargs) + else: + obj = method(self, ix, iy, *iargs, **ikwargs) if isinstance(obj, (list, tuple)) and len(obj) == 1: obj = obj[0] objs.append(obj) @@ -2740,11 +2742,11 @@ def apply_cycle( # NOTE: If error objects have separate label, allocate separate legend entry. # If they do not, try to combine with current legend entry. if legend: - if not isinstance(_errobjs, (list, tuple)): - _errobjs = (_errobjs,) - eobjs = [obj for obj in _errobjs if obj and not _get_label(obj)] + if not isinstance(errobjs, (list, tuple)): + errobjs = (errobjs,) + eobjs = [obj for obj in errobjs if obj and not _get_label(obj)] hobjs = [(*eobjs[::-1], *objs)] if eobjs else objs.copy() - hobjs.extend(obj for obj in _errobjs if obj and _get_label(obj)) + hobjs.extend(obj for obj in errobjs if obj and _get_label(obj)) try: hobjs, labels = list(zip(*_iter_objs_labels(hobjs))) except ValueError: @@ -2764,8 +2766,8 @@ def apply_cycle( def _auto_levels_locator( - *args, N=None, norm=None, norm_kw=None, - vmin=None, vmax=None, extend='neither', locator=None, locator_kw=None, + *args, N=None, norm=None, norm_kw=None, extend='neither', + vmin=None, vmax=None, locator=None, locator_kw=None, symmetric=False, positive=False, negative=False, nozero=False, ): """ @@ -2781,10 +2783,10 @@ def _auto_levels_locator( norm, norm_kw Passed to `~proplot.constructor.Norm`. Used to determine suitable level locations if `locator` is not passed. - vmin, vmax : float, optional - The data limits. extend : str, optional The extend setting. + vmin, vmax : float, optional + The data limits. locator, locator_kw Passed to `~proplot.constructor.Locator`. Used to determine suitable level locations. @@ -2897,9 +2899,8 @@ def _auto_levels_locator( def _build_discrete_norm( - data=None, N=None, levels=None, values=None, - cmap=None, norm=None, norm_kw=None, vmin=None, vmax=None, extend='neither', - minlength=2, + data=None, cmap=None, norm=None, norm_kw=None, extend='neither', + levels=None, N=None, values=None, vmin=None, vmax=None, minlength=2, **kwargs, ): """ @@ -2911,16 +2912,16 @@ def _build_discrete_norm( ---------- data : ndarray, optional The data. - levels, values : ndarray, optional - The explicit boundaries. - norm, norm_kw - Passed to `~proplot.constructor.Norm` and then to `DiscreteNorm`. - vmin, vmax : float, optional - The minimum and maximum values for the normalizer. cmap : `matplotlib.colors.Colormap`, optional The colormap. Passed to `DiscreteNorm`. + norm, norm_kw + Passed to `~proplot.constructor.Norm` and then to `DiscreteNorm`. extend : str, optional The extend setting. + levels, N, values : ndarray, optional + The explicit boundaries. + vmin, vmax : float, optional + The minimum and maximum values for the normalizer. minlength : int, optional The minimum length for level lists. **kwargs @@ -3048,14 +3049,14 @@ def _build_discrete_norm( return norm, cmap, levels, locator -def _fix_white_lines(obj): +def _fix_white_lines(obj, linewidth=0.3): """ Fix white lines between between filled contours and mesh and fix issues with colormaps that are transparent. """ # See: https://github.com/jklymak/contourfIssues # See: https://stackoverflow.com/q/15003353/4970632 - # 0.4pt is thick enough to hide lines but thin enough to not add "dots" + # 0.3pt is thick enough to hide lines but thin enough to not add "dots" # in corner of pcolor plots so good compromise. cmap = obj.cmap if not cmap._isinit: @@ -3069,13 +3070,13 @@ def _fix_white_lines(obj): if isinstance(obj, mcontour.ContourSet): for contour in obj.collections: contour.set_edgecolor(edgecolor) - contour.set_linewidth(0.4) + contour.set_linewidth(linewidth) contour.set_linestyle('-') # Pcolor fixes # NOTE: This ignores AxesImage and PcolorImage sometimes returned by pcolorfast elif isinstance(obj, (mcollections.PolyCollection, mcollections.QuadMesh)): if hasattr(obj, 'set_linewidth'): # not always true for pcolorfast - obj.set_linewidth(0.4) + obj.set_linewidth(linewidth) if hasattr(obj, 'set_edgecolor'): # not always true for pcolorfast obj.set_edgecolor(edgecolor) @@ -3163,15 +3164,15 @@ def _labels_pcolor(self, obj, fmt=None, **kwargs): def apply_cmap( self, *args, cmap=None, cmap_kw=None, norm=None, norm_kw=None, - vmin=None, vmax=None, extend='neither', N=None, levels=None, values=None, + extend='neither', levels=None, N=None, values=None, + vmin=None, vmax=None, locator=None, locator_kw=None, symmetric=False, positive=False, negative=False, nozero=False, - locator=None, locator_kw=None, edgefix=None, labels=False, labels_kw=None, fmt=None, precision=2, colorbar=False, colorbar_kw=None, lw=None, linewidth=None, linewidths=None, ls=None, linestyle=None, linestyles=None, color=None, colors=None, edgecolor=None, edgecolors=None, - _method=None, **kwargs + **kwargs ): """ Adds several new keyword args and features for specifying the colormap, @@ -3184,7 +3185,10 @@ def apply_cmap( Parameters ---------- - %(axes.apply_cmap)s + %(axes.cmap_norm)s + %(axes.levels_values)s + %(axes.vmin_vmax)s + %(axes.auto_levels)s edgefix : bool, optional Whether to fix the the `white-lines-between-filled-contours \ `__ @@ -3219,7 +3223,7 @@ def apply_cmap( default location is used. Valid locations are described in `~proplot.axes.Axes.colorbar`. colorbar_kw : dict-like, optional - Ignored if `colorbar` is ``None``. Extra keyword args for our call + Ignored if `colorbar` is ``None``. Extra keyword args for the call to `~proplot.axes.Axes.colorbar`. Other parameters @@ -3248,7 +3252,8 @@ def apply_cmap( proplot.constructor.Norm proplot.colors.DiscreteNorm """ - name = _method.__name__ + method = kwargs.pop('_method') + name = method.__name__ contour = name in ('contour', 'tricontour') contourf = name in ('contourf', 'tricontourf') pcolor = name in ('pcolor', 'pcolormesh', 'pcolorfast') @@ -3256,7 +3261,7 @@ def apply_cmap( parametric = name in ('parametric',) no_discrete_norm = hexbin # TODO: this should be global setting!!! if not args: - return _method(self, *args, **kwargs) + return method(self, *args, **kwargs) sample = args[-1] # used for labels # Parse keyword args @@ -3316,18 +3321,18 @@ def apply_cmap( # Translate standardized keyword arguments back into the keyword args # accepted by native matplotlib methods. Also disable edgefix if user want # to customize the "edges". - style_kw = STYLE_ARGS_TRANSLATE.get(name, None) - for key, value in ( + styles = STYLE_ARGS_TRANSLATE.get(name, None) + for idx, (key, value) in enumerate(( ('colors', colors), ('linewidths', linewidths), ('linestyles', linestyles) - ): + )): if value is None or add_contours: continue - if not style_kw or key not in style_kw: # no known conversion table + if not styles: # no known conversion table, e.g. for imshow() plots raise TypeError(f'{name}() got an unexpected keyword argument {key!r}') edgefix = False # disable edgefix when specifying borders! - kwargs[style_kw[key]] = value + kwargs[styles[idx]] = value # Build colormap normalizer and update keyword args # NOTE: Standard algorithm for obtaining default levels does not work @@ -3365,7 +3370,7 @@ def apply_cmap( # Call function and possibly add solid contours between filled ones or # fix common "white lines" issues with vector graphic output - obj = _method(self, *args, **kwargs) + obj = method(self, *args, **kwargs) if not isinstance(obj, tuple): # hist2d obj._colorbar_extend = extend # used by proplot colorbar obj._colorbar_ticks = ticks # used by proplot colorbar @@ -3530,8 +3535,8 @@ def colorbar_extras( Parameters ---------- - mappable : mappable, list of plot handles, list of color-spec, \ -or colormap-spec + mappable \ +: mappable, list of plot handles, list of color-spec, or colormap-spec There are four options here: 1. A mappable object. Basically, any object with a ``get_cmap`` method, @@ -3623,8 +3628,9 @@ def colorbar_extras( See also -------- - proplot.axes.Axes.colorbar + matplotlib.figure.Figure.colorbar proplot.figure.Figure.colorbar + proplot.axes.Axes.colorbar """ # NOTE: There is a weird problem with colorbars when simultaneously # passing levels and norm object to a mappable; fixed by passing vmin/vmax @@ -3885,7 +3891,9 @@ def _iter_legend_children(children): yield obj -def _multiple_legend(self, pairs, loc=None, ncol=None, order=None, **kwargs): +def _multiple_legend( + self, pairs, *, fontsize, loc=None, ncol=None, order=None, **kwargs +): """ Draw "legend" with centered rows by creating separate legends for each row. The label spacing/border spacing will be exactly replicated. @@ -3914,8 +3922,6 @@ def _multiple_legend(self, pairs, loc=None, ncol=None, order=None, **kwargs): # NOTE: Empirical testing shows spacing fudge factor necessary to # exactly replicate the spacing of standard aligned legends. width, height = self.get_size_inches() - fontsize = kwargs.get('fontsize', None) or rc['legend.fontsize'] - fontsize = rc._scale_font(fontsize) spacing = kwargs.get('labelspacing', None) or rc['legend.labelspacing'] if pairs: interval = 1 / len(pairs) # split up axes @@ -4111,9 +4117,10 @@ def legend_extras( The legend title. The `label` keyword is also accepted, for consistency with `colorbar`. fontsize, fontweight, fontcolor : optional - The font size, weight, and color for legend text. - color, lw, linewidth, marker, linestyle, dashes, markersize : \ -property-spec, optional + The font size, weight, and color for the legend text. The default + font size is :rcraw:`legend.fontsize`. + color, lw, linewidth, marker, linestyle, dashes, markersize \ +: property-spec, optional Properties used to override the legend handles. For example, for a legend describing variations in line style ignoring variations in color, you might want to use ``color='k'``. For now this does not include `facecolor`, @@ -4127,8 +4134,10 @@ def legend_extras( See also -------- - proplot.axes.Axes.legend + matplotlib.figure.Figure.legend + matplotlib.axes.Axes.legend proplot.figure.Figure.legend + proplot.axes.Axes.legend """ # Parse input args # TODO: Legend entries for colormap or scatterplot objects! Idea is we @@ -4157,8 +4166,13 @@ def legend_extras( kwargs['title'] = title if frameon is not None: kwargs['frameon'] = frameon - if fontsize is not None: + fontsize = kwargs.get('fontsize', None) or rc['legend.fontsize'] + if fontsize is None: + pass + elif fontsize in mfonts.font_scalings: kwargs['fontsize'] = rc._scale_font(fontsize) + else: + kwargs['fontsize'] = units(fontsize, 'pt') # Handle and text properties that are applied after-the-fact # NOTE: Set solid_capstyle to 'butt' so line does not extend past error bounds @@ -4296,7 +4310,7 @@ def legend_extras( obj.update(kw_text) else: for key, value in kw_handle.items(): - getattr(obj, f'set_{key}', lambda value: None)(value) + getattr(obj, 'set_' + key, lambda value: None)(value) # Append attributes and return, and set clip property!!! This is critical # for tight bounding box calcs! @@ -4322,8 +4336,8 @@ def _apply_wrappers(method, *args): # NOTE: Must assign fucn and method as keywords to avoid overwriting # by look scope and associated recursion errors. method = functools.wraps(method)( - lambda self, *args, func=func, method=method, **kwargs: - func(self, *args, _method=method, **kwargs) + lambda self, *args, _func=func, _method=method, **kwargs: + _func(self, *args, _method=_method, **kwargs) ) # List wrapped methods in the driver function docstring diff --git a/proplot/demos.py b/proplot/demos.py index 8ed96d0b1..89831a7e3 100644 --- a/proplot/demos.py +++ b/proplot/demos.py @@ -244,7 +244,7 @@ def _draw_bars( cmapdict.pop(cat) # Draw figure - naxs = len(cmapdict) + sum(map(len, cmapdict.values())) + naxs = 2 * len(cmapdict) + sum(map(len, cmapdict.values())) fig, axs = ui.subplots( nrows=naxs, refwidth=length, refheight=width, share=0, hspace=0.03, @@ -257,11 +257,10 @@ def _draw_bars( iax += 1 if imap + nheads + nbars > naxs: break - ax = axs[iax] if imap == 0: # allocate this axes for title - iax += 1 - ax.set_visible(False) - ax = axs[iax] + iax += 2 + axs[iax - 2:iax].set_visible(False) + ax = axs[iax] cmap = database[name] if N is not None: cmap = cmap.copy(N=N) From 23d4cc9744146495a33a8ca93e00f982f9abd4f9 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Fri, 9 Jul 2021 14:17:18 -0600 Subject: [PATCH 07/11] Fix issues with show_(cmaps|cycles) due to padding fix (#221) --- proplot/demos.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/proplot/demos.py b/proplot/demos.py index 89831a7e3..bad3b1819 100644 --- a/proplot/demos.py +++ b/proplot/demos.py @@ -244,10 +244,11 @@ def _draw_bars( cmapdict.pop(cat) # Draw figure + # Allocate two colorbar widths for each title of sections naxs = 2 * len(cmapdict) + sum(map(len, cmapdict.values())) fig, axs = ui.subplots( nrows=naxs, refwidth=length, refheight=width, - share=0, hspace=0.03, + share=0, hspace='2pt', top='-1em', ) iax = -1 nheads = nbars = 0 # for deciding which axes to plot in @@ -327,8 +328,8 @@ def show_channels( array += [np.array([4, 4, 5, 5, 6, 6]) + 2 * int(saturation)] labels += ('Red', 'Green', 'Blue') fig, axs = ui.subplots( - array=array, span=False, share=1, - refwidth=refwidth, innerpad='1em', + array=array, refwidth=refwidth, wratios=(1.5, 1, 1, 1, 1, 1.5), + share=1, span=False, innerpad='1em', ) # Iterate through colormaps mc = ms = mp = 0 From bbcffa89b13a2fce7b06eca297997f82ea954983 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Fri, 9 Jul 2021 14:18:35 -0600 Subject: [PATCH 08/11] Fix recently introduced bug where fail to convert twilight_shifted --- proplot/colors.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/proplot/colors.py b/proplot/colors.py index 45fecf228..737beb7c7 100644 --- a/proplot/colors.py +++ b/proplot/colors.py @@ -22,13 +22,6 @@ from .internals import _not_none, docstring, warnings from .utils import to_rgb, to_rgba, to_xyz, to_xyza -if hasattr(mcm, '_cmap_registry'): - _cmap_database_attr = '_cmap_registry' -else: - _cmap_database_attr = 'cmap_d' -_cmap_database = getattr(mcm, _cmap_database_attr) - - __all__ = [ 'ListedColormap', 'LinearSegmentedColormap', @@ -2564,12 +2557,16 @@ def _sanitize_key(self, key, mirror=True): mcolors.colorConverter.colors = _map # Replace colormap database with custom database +# WARNING: Skip over the matplotlib native duplicate entries with +# suffixes '_r' and '_shifted'. +_cmap_database_attr = '_cmap_registry' if hasattr(mcm, '_cmap_registry') else 'cmap_d' +_cmap_database = getattr(mcm, _cmap_database_attr) if mcm.get_cmap is not _get_cmap: mcm.get_cmap = _get_cmap if not isinstance(_cmap_database, ColormapDatabase): _cmap_database = { key: value for key, value in _cmap_database.items() - if key[-2:] != '_r' and key[-2:] != '_s' + if key[-2:] != '_r' and key[-8:] != '_shifted' } _cmap_database = ColormapDatabase(_cmap_database) setattr(mcm, _cmap_database_attr, _cmap_database) From d083438c718404c3afd9be0c349c8216fa4c68c1 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Fri, 9 Jul 2021 14:19:21 -0600 Subject: [PATCH 09/11] Fix issue where we overwrite mcm.get_cmap with useless docstring --- proplot/colors.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/proplot/colors.py b/proplot/colors.py index 737beb7c7..e1ed3280f 100644 --- a/proplot/colors.py +++ b/proplot/colors.py @@ -2366,9 +2366,21 @@ def __getitem__(self, key): def _get_cmap(name=None, lut=None): """ - Monkey patch for matplotlib `~matplotlib.get_cmap`. Permits case-insensitive - search of monkey-patched colormap database (which was broken in v3.2.0). + Return the registered colormap instance. + + Parameters + ---------- + name : `matplotlib.colors.Colormap` or str or None, optional + If a `~matplotlib.colors.Colormap` instance, it will be returned. Otherwise, + the name of the registered colormap will be looked up and resampled by `lut`. + If ``None``, the default colormap :rc:`image.cmap` is returned. + lut : int or None, optional + If `name` is not already a `~matplotlib.colors.Colormap` instance + and `lut` is not None, the colormap will be resampled to have `lut` + entries in the lookup table. """ + # Monkey patch for matplotlib `~matplotlib.get_cmap`. Permits case-insensitive + # search of monkey-patched colormap database (which was broken in v3.2.0). if name is None: name = rcParams['image.cmap'] if isinstance(name, mcolors.Colormap): From 2ebb682f6ae74065d26d8aa55d64e40d3b6d4b3c Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Fri, 9 Jul 2021 14:19:55 -0600 Subject: [PATCH 10/11] Fix minor bugs after refactor --- proplot/axes/plot.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/proplot/axes/plot.py b/proplot/axes/plot.py index 1308106b0..eeda3a23e 100644 --- a/proplot/axes/plot.py +++ b/proplot/axes/plot.py @@ -1681,10 +1681,10 @@ def _apply_lines( # Parse input arguments method = kwargs.pop('_method') name = method.__name__ - args = list(args) colors = _not_none(color=color, colors=colors) linestyles = _not_none(linestyle=linestyle, linestyles=linestyles) linewidths = _not_none(lw=lw, linewidth=linewidth, linewidths=linewidths) + args = list(args) if len(args) > 3: raise ValueError(f'Expected 1-3 positional args, got {len(args)}.') if len(args) == 2: # empty possible @@ -1759,9 +1759,9 @@ def _apply_scatter( """ # Manage input arguments method = kwargs.pop('_method') + args = list(args) if len(args) > 4: raise ValueError(f'Expected 1-4 positional arguments, got {len(args)}.') - args = list(args) if len(args) == 4: c = _not_none(c_positional=args.pop(-1), c=c) if len(args) == 3: @@ -1861,10 +1861,10 @@ def _apply_fill_between( name = method.__name__ sx = 'y' if 'x' in name else 'x' # i.e. fill_betweenx sy = 'x' if sx == 'y' else 'y' - args = list(args) stack = _not_none(stack=stack, stacked=stacked) color = _not_none(color=color, facecolor=facecolor) linewidth = _not_none(lw=lw, linewidth=linewidth, default=0) + args = list(args) if len(args) > 4: raise ValueError(f'Expected 1-4 positional args, got {len(args)}.') if len(args) == 4: @@ -1988,6 +1988,7 @@ def _apply_bar( color = _not_none(color=color, facecolor=facecolor) linewidth = _not_none(lw=lw, linewidth=linewidth, default=rc['patch.linewidth']) kwargs.update({'linewidth': linewidth, 'edgecolor': edgecolor}) + args = list(args) if len(args) > 4: raise ValueError(f'Expected 1-4 positional args, got {len(args)}.') if len(args) == 4 and stack: @@ -2631,7 +2632,7 @@ def apply_cycle( # Parse input arguments # NOTE: Requires standardize_1d wrapper before reaching this. method = kwargs.pop('_method') - errobjs = kwargs.pop('_errobjs') + errobjs = kwargs.pop('_errobjs', None) name = method.__name__ plot = name in ('plot',) scatter = name in ('scatter',) From e18a46a8ccd1e1fc9fa92d65ae933691fe185560 Mon Sep 17 00:00:00 2001 From: Luke Davis Date: Fri, 9 Jul 2021 14:58:54 -0600 Subject: [PATCH 11/11] Final bugfixes after 1dplots refactor --- proplot/axes/base.py | 9 +- proplot/axes/plot.py | 478 +++++++++++++++++++++++++------------------ 2 files changed, 280 insertions(+), 207 deletions(-) diff --git a/proplot/axes/base.py b/proplot/axes/base.py index 68024e428..5bf4dc1ed 100644 --- a/proplot/axes/base.py +++ b/proplot/axes/base.py @@ -1900,17 +1900,17 @@ def number(self, num): stem = wrap._apply_wrappers( maxes.Axes.stem, wrap.standardize_1d, - wrap._stem_extras, # TODO check this + wrap._stem_extras, ) vlines = wrap._apply_wrappers( maxes.Axes.vlines, wrap.standardize_1d, - wrap.vlines_extras, # TODO check this + wrap.vlines_extras, ) hlines = wrap._apply_wrappers( maxes.Axes.hlines, wrap.standardize_1d, - wrap.hlines_extras, # TODO check this + wrap.hlines_extras, ) scatter = wrap._apply_wrappers( maxes.Axes.scatter, @@ -1935,7 +1935,10 @@ def number(self, num): ) barh = wrap._apply_wrappers( maxes.Axes.barh, + wrap.standardize_1d, wrap.barh_extras, + wrap.indicate_error, + wrap.apply_cycle, ) hist = wrap._apply_wrappers( maxes.Axes.hist, diff --git a/proplot/axes/plot.py b/proplot/axes/plot.py index eeda3a23e..b4ddcc53e 100644 --- a/proplot/axes/plot.py +++ b/proplot/axes/plot.py @@ -43,7 +43,7 @@ from ..internals import ic # noqa: F401 from ..internals import ( _dummy_context, - _flexible_getattr, + _getattr_flexible, _not_none, _state_context, docstring, @@ -84,13 +84,20 @@ # (y, width, height, left) behavior. Want to have consistent interpretation # of vertical or horizontal bar 'width' with 'width' key or 3rd positional arg. # Interal hist() func uses positional arguments when calling bar() so this is fine. -POSITIONAL_ARGS_TRANSLATE = { +KEYWORD_TO_POSITIONAL_INSERT = { 'fill_between': ('x', 'y1', 'y2'), 'fill_betweenx': ('y', 'x1', 'x2'), 'vlines': ('x', 'ymin', 'ymax'), 'hlines': ('y', 'xmin', 'xmax'), - 'bar': ('x', 'height', 'width', 'bottom'), - 'barh': ('y', 'height', 'width', 'left'), + 'bar': ('x', 'height'), + 'barh': ('y', 'height'), + 'parametric': ('x', 'y', 'values'), + 'boxplot': ('positions',), # use as x-coordinates during wrapper processing + 'violinplot': ('positions',), +} +KEYWORD_TO_POSITIONAL_APPEND = { + 'bar': ('width', 'bottom'), + 'barh': ('width', 'left'), } # Consistent keywords for cmap plots. Used by apply_cmap to pass correct plural @@ -146,7 +153,9 @@ """ docstring.snippets['axes.levels_values'] = """ -levels, N : int or list of float, optional +N + Shorthand for `levels`. +levels : 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 level edges at "nice" intervals. If the latter, the levels should be monotonically increasing or @@ -200,18 +209,26 @@ Parameters ---------- -color, colors : color-spec or list thereof, optional - The line color(s). -linestyle, linestyles : linestyle-spec or list thereof, optional - The line style(s). -lw, linewidth, linewidths : linewidth-spec or list thereof, optional - The line width(s). +*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 + draw lines between these points. If `{y}1` or `{y}2` are 2D, this + function is called with each column. The default value for `{y}2` is ``0``. +stack, 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 color lines greater than zero with `poscolor` and lines less than zero with `negcolor`. negcolor, poscolor : color-spec, optional Colors to use for the negative and positive lines. Ignored if `negpos` is ``False``. Defaults are :rc:`negcolor` and :rc:`poscolor`. +color, colors : color-spec or list thereof, optional + The line color(s). +linestyle, linestyles : linestyle-spec or list thereof, optional + The line style(s). +lw, linewidth, linewidths : linewidth-spec or list thereof, optional + The line width(s). See also -------- @@ -219,10 +236,10 @@ apply_cycle """ docstring.snippets['axes.vlines'] = _lines_docstring.format( - prefix='v', orientation='vertical', + x='x', y='y', prefix='v', orientation='vertical', ) docstring.snippets['axes.hlines'] = _lines_docstring.format( - prefix='h', orientation='horizontal', + x='y', y='x', prefix='h', orientation='horizontal', ) _scatter_docstring = """ @@ -235,6 +252,9 @@ Parameters ---------- +*args : {y} or {x}, {y} + The input *{x}* or *{x}* and *{y}* coordinates. If only *{y}* is provided, + *{x}* will be inferred from *{y}*. s, size, markersize : float or list of float, optional The marker size(s). The units are optionally scaled by `smin` and `smax`. @@ -268,10 +288,10 @@ apply_cycle """ docstring.snippets['axes.scatter'] = _scatter_docstring.format( - suffix='', package='matplotlib', + x='x', y='y', suffix='', package='matplotlib', ) % docstring.snippets docstring.snippets['axes.scatterx'] = _scatter_docstring.format( - suffix='', package='proplot', + x='y', y='x', suffix='', package='proplot', ) % docstring.snippets _fill_between_docstring = """ @@ -286,9 +306,10 @@ 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``. + 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 these points. If `{y}1` or `{y}2` are 2D, this function + is called with each column. The default value for `{y}2` is ``0``. stack, 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. @@ -336,16 +357,13 @@ Parameters ---------- -{x}, {height}, width, {bottom} : float or list of float, optional +{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))``. Note that the units - for `width` are now *relative*. -orientation : {{None, 'vertical', 'horizontal'}}, optional - The orientation of the bars. If ``'horizontal'``, bars are drawn horizontally - rather than vertically. This is the default for `~matplotlib.axes.Axes.barh`. -vert : bool, optional - Alternative to the `orientation` keyword arg. If ``False``, bars are drawn - horizontally. Added for consistency with `~matplotlib.axes.Axes.boxplot`. + they are set to ``np.arange(0, len(height))``. The units for width + are *relative* by default. +absolute_width : bool, optional + Whether to make the units for width *absolute*. This restores + the default matplotlib behavior. stack, stacked : bool, optional Whether to stack columns of the input array or plot the bars side-by-side in groups. @@ -373,10 +391,10 @@ apply_cycle """ docstring.snippets['axes.bar'] = _bar_docstring.format( - x='x', height='height', bottom='bottom', suffix='', + x='x', bottom='bottom', suffix='', ) docstring.snippets['axes.barh'] = _bar_docstring.format( - x='y', height='right', bottom='left', suffix='h', + x='y', bottom='left', suffix='h', ) @@ -440,11 +458,13 @@ def _mask_array(mask, *args): invalid = ~mask # True if invalid args_masked = [] for arg in args: - if arg.shape != invalid.shape: + if arg.size > 1 and arg.shape != invalid.shape: raise ValueError('Shape mismatch between mask and array.') arg_masked = arg.astype(np.float64) - if arg.size == 1 and invalid.item(): - arg_masked = np.nan + if arg.size == 1: + pass + elif invalid.size == 1: + arg_masked = np.nan if invalid.item() else arg_masked elif arg.size > 1: arg_masked[invalid] = np.nan args_masked.append(arg_masked) @@ -498,18 +518,17 @@ def _basemap_redirect(self, *args, **kwargs): return method(self, *args, **kwargs) -def _basemap_norecurse(self, *args, **kwargs): +def _basemap_norecurse(self, *args, called_from_basemap=False, **kwargs): """ Decorator to prevent recursion in basemap method overrides. See `this post https://stackoverflow.com/a/37675810/4970632`__. """ method = kwargs.pop('_method') name = method.__name__ - if getattr(method, '_called_from_basemap', None): + if called_from_basemap: return getattr(maxes.Axes, name)(self, *args, **kwargs) else: - with _state_context(method, _called_from_basemap=True): - return method(self, *args, **kwargs) + return method(self, *args, called_from_basemap=True, **kwargs) def _get_data(data, *args): @@ -622,18 +641,6 @@ def _get_title(data, units=True): return title -def _get_vert(**kwargs): - """ - Return ``True`` if ``vert`` is present or ``orientation`` is ``'vertical'``. - """ - vert = kwargs.pop('vert', None) - orientation = kwargs.pop('orientation', None) - if orientation and orientation not in ('horizontal', 'vertical'): - raise ValueError("Orientation must be either 'horizontal' or 'vertical'.") - vert = _not_none(vert=vert, orientation=(orientation == 'vertical'), default=True) - return vert, kwargs - - def _parse_string_coords(*args, which='x', **kwargs): """ Convert string arrays and lists to index coordinates. @@ -658,7 +665,7 @@ def _parse_string_coords(*args, which='x', **kwargs): def _auto_format_1d( - self, x, *ys, name='plot', vert=True, autoformat=False, + self, x, *ys, name='plot', autoformat=False, label=None, values=None, labels=None, **kwargs ): """ @@ -666,14 +673,16 @@ def _auto_format_1d( formatting. Also update the keyword arguments. """ # Parse input - vert = kwargs.get('vert', kwargs.get('orientation', 'vertical') == 'vertical') projection = hasattr(self, 'projection') parametric = name in ('parametric',) scatter = name in ('scatter',) hist = name in ('hist',) box = name in ('boxplot', 'violinplot') pie = name in ('pie',) - nocycle = name in ('stem', 'hlines', 'vlines', 'hexbin', 'parametric') # WARNING + vert = kwargs.get('vert', True) and kwargs.get('orientation', None) != 'horizontal' + vert = vert and name not in ('plotx', 'scatterx', 'fill_betweenx', 'barh') + stem = name in ('stem',) + nocycle = name in ('stem', 'hexbin', 'hist2d', 'parametric') labels = _not_none( label=label, values=values, @@ -682,21 +691,44 @@ def _auto_format_1d( legend_kw_labels=kwargs.get('legend_kw', {}).pop('labels', None), ) - # The x coords or histogram labels + # The inferred x coords # NOTE: Where columns represent distributions, like for box and violin plots or # where we use 'means' or 'medians', columns coords (axis 1) are 'x' coords. - # Otherwise, columns represent e.g. lines, and row coords (axis 0) are 'x' coords. + # Otherwise, columns represent e.g. lines, and row coords (axis 0) are 'x' coords reduce = any(kwargs.get(s) for s in ('mean', 'means', 'median', 'medians')) - sx = int(box or hist or reduce or False) - if hist: - if labels is None: - labels = _get_labels(ys[0], axis=sx, always=False) # hist labels - else: - if x is None: - x = _get_labels(ys[0], axis=sx) # infer from rows or columns - x = _to_arraylike(x) + xaxis = int(box or reduce or False) + if x is None and not hist: + x = _get_labels(ys[0], axis=xaxis) # infer from rows or columns + + # Default legend or colorbar labels and title. We want default legend + # labels if this is an object with 'title' metadata and/or coords are string + # WARNING: Confusing terminology differences here -- for box and violin plots + # 'labels' refer to indices along x axis. Get interpreted that way down the line. + if autoformat and not stem: + # Dictionaries used to supply title to on-the-fly legend or colorbar + colorbar_kw = kwargs.setdefault('colorbar_kw', {}) + if not nocycle: + legend_kw = kwargs.setdefault('legend_kw', {}) - # The labels and XY axis settings + # The inferred labels or title + always = pie or box + if labels is not None: + title = _get_title(labels) + elif reduce or not always and not any(y.ndim > 1 for y in ys): + title = None + else: + labels = _get_labels(ys[0], axis=1, always=always) + title = _get_title(labels) # e.g. if labels is a Series + if not title and labels is not None and any(isinstance(_, str) for _ in labels): # noqa: E501 + labels = None + + # Apply the title + if title: + colorbar_kw.setdefault('title', title) + if not nocycle: + legend_kw.setdefault('title', title) + + # The basic x and y settings if not projection: # Apply label # NOTE: Do not overwrite existing labels! @@ -713,39 +745,23 @@ def _auto_format_1d( kw_format[sx + 'label'] = title # Handle string-type coordinates - if pie: - labels = _not_none(labels, x) # possibly inferred from DataFrame rows - elif not hist: + if not pie and not hist: x, kw_format = _parse_string_coords(x, which=sx, **kw_format) if not hist and not box and not pie: *ys, kw_format = _parse_string_coords(*ys, which=sy, **kw_format) - if not scatter and x.ndim == 1 and x.size > 1 and _to_ndarray(x)[1] < _to_ndarray(x)[0]: # noqa: E501 + if not hist and not scatter and x.ndim == 1 and x.size > 1 and x[1] < x[0]: kw_format[sx + 'reverse'] = True # auto reverse # Appply if kw_format: self.format(**kw_format) - # Default legend or colorbar labels and title - # NOTE: Current idea is we only want default legend labels if this is - # an object with 'title' metadata and/or the 'coordinates' are string - # WARNING: This will fail for any funcs not wrapped by apply_cycle. Keep - # the 'nocycle' list updated until 'wrapper' functions disbanded. - if labels is None: - labels = _get_labels(ys[0] if vert else x, axis=1) - title = _get_title(labels) - if not title and not any(isinstance(_, str) for _ in labels): - labels = None - else: - kwargs.setdefault('legend_kw', {}) - kwargs.setdefault('colorbar_kw', {}) - kwargs['legend_kw'].setdefault('title', title) - kwargs['colorbar_kw'].setdefault('label', title) - # Finally strip metadata # WARNING: Most methods that accept 2D arrays use columns of data, but when # pandas DataFrame specifically is passed to hist, boxplot, or violinplot, rows # of data assumed! Converting to ndarray necessary. + if labels is None and (pie or box): + labels = x if labels is not None: if not nocycle: kwargs['labels'] = _to_ndarray(labels) @@ -782,14 +798,18 @@ def standardize_1d(self, *args, data=None, autoformat=None, **kwargs): """ method = kwargs.pop('_method') name = method.__name__ + bar = name in ('bar', 'barh') box = name in ('boxplot', 'violinplot') + hist = name in ('hist',) + parametric = name in ('parametric',) + onecoord = name in ('hist',) twocoords = name in ('vlines', 'hlines', 'fill_between', 'fill_betweenx') - allowempty = name in ('plot', 'plotx', 'fill') + allowempty = name in ('fill', 'plot', 'plotx',) autoformat = _not_none(autoformat, rc['autoformat']) # Find and translate input args args = list(args) - keys = POSITIONAL_ARGS_TRANSLATE.get(name, {}) + keys = KEYWORD_TO_POSITIONAL_INSERT.get(name, {}) for idx, key in enumerate(keys): if key in kwargs: args.insert(idx, kwargs.pop(key)) @@ -801,19 +821,49 @@ def standardize_1d(self, *args, data=None, autoformat=None, **kwargs): else: raise TypeError('Positional arguments are required.') - # Parse input args - # WARNING: This will temporarily add pseudo-x coordinates to hist, boxplot, and - # pie but they are ignored when we reach apply_cycle. - if len(args) == 1: - x = None - ys, args = args[:1], args[1:] # single y + # Translate between 'orientation' and 'vert' for flexibility + # NOTE: Users should only pass these to hist, boxplot, or violinplot. To change + # the bar plot orientation users should use 'bar' and 'barh'. Internally, + # matplotlib has a single central bar function whose behavior is configured + # by the 'orientation' key, so critical not to strip the argument here. + vert = kwargs.pop('vert', None) + orientation = kwargs.pop('orientation', None) + if orientation is not None: + vert = _not_none(vert=vert, orientation=(orientation == 'vertical')) + if orientation not in (None, 'horizontal', 'vertical'): + raise ValueError("Orientation must be either 'horizontal' or 'vertical'.") + if vert is None: + pass + elif box: + kwargs['vert'] = vert + elif bar or hist: + kwargs['orientation'] = 'vertical' if vert else 'horizontal' # used internally + else: + raise TypeError("Unexpected keyword argument(s) 'vert' and 'orientation'.") + + # Parse positional args + if parametric and len(args) == 3: # allow positional values + kwargs['values'] = args.pop(2) + if onecoord or len(args) == 1: # allow hist() positional bins + x, ys, args = None, args[:1], args[1:] elif twocoords: x, ys, args = args[0], args[1:3], args[3:] else: x, ys, args = args[0], args[1:2], args[2:] + if x is not None: + x = _to_arraylike(x) ys = tuple(_to_arraylike(y) for y in ys) + # Append remaining positional args + # NOTE: This is currently just used for bar and barh. More convenient to pass + # 'width' as positional so that matplotlib native 'barh' sees it as 'height'. + keys = KEYWORD_TO_POSITIONAL_APPEND.get(name, {}) + for key in keys: + if key in kwargs: + args.append(kwargs.pop(key)) + # Automatic formatting and coordinates + # NOTE: For 'hist' the 'x' coordinate remains None then is ignored in apply_cycle. x, *ys, kwargs = _auto_format_1d( self, x, *ys, name=name, autoformat=autoformat, **kwargs ) @@ -1124,8 +1174,9 @@ def standardize_2d( try to infer them from the metadata. Otherwise, ``np.arange(0, data.shape[0])`` and ``np.arange(0, data.shape[1])`` are used. * For ``pcolor`` and ``pcolormesh``, coordinate *edges* are calculated - if *centers* were provided. For all other methods, coordinate *centers* - are calculated if *edges* were provided. + if *centers* were provided. This uses the `~proplot.utils.edges` and + `~propot.utils.edges2d` functions. For all other methods, coordinate + *centers* are calculated if *edges* were provided. Important --------- @@ -1153,6 +1204,8 @@ def standardize_2d( See also -------- apply_cmap + proplot.utils.edges + proplot.utils.edges2d """ method = kwargs.pop('_method') name = method.__name__ @@ -1233,7 +1286,7 @@ def _get_error_data( else: stds = np.atleast_1d(stds) if stds.size == 1: - stds = sorted((-stds, stds)) + stds = sorted((-stds.item(), stds.item())) elif stds.size != 2: raise ValueError('Expected scalar or length-2 stdev specification.') @@ -1245,7 +1298,7 @@ def _get_error_data( else: pctiles = np.atleast_1d(pctiles) if pctiles.size == 1: - delta = (100 - pctiles) / 2.0 + delta = (100 - pctiles.item()) / 2.0 pctiles = sorted((delta, 100 - delta)) elif pctiles.size != 2: raise ValueError('Expected scalar or length-2 pctiles specification.') @@ -1353,12 +1406,6 @@ def indicate_error( Whether to plot the medians of each column in the input data. If no other arguments specified, this also sets ``barstd=True`` (and ``boxstd=True`` for violin plots). - vert : bool, optional - If ``False``, error data is drawn horizontally rather than vertially. Set - automatically by methods like `bar`, `barh`, `area`, and `areax`. - orientation : {{None, 'vertical', 'horizontal'}}, optional - Alternative to the `vert` keyword arg. If ``'horizontal'``, error data is - drawn horizontally rather than vertically. barstd, barstds : float, (float, float), or bool, optional Standard deviation multiples for *thin error bars* with optional whiskers (i.e. caps). If scalar, then +/- that number is used. If ``True``, the @@ -1424,8 +1471,9 @@ def indicate_error( method = kwargs.pop('_method') name = method.__name__ bar = name in ('bar',) - violin = name in ('violinplot',) + flip = name in ('barh', 'plotx', 'scatterx') or kwargs.get('vert') is False plot = name in ('plot', 'scatter') + violin = name in ('violinplot',) means = _not_none(mean=mean, means=means) medians = _not_none(median=median, medians=medians) barstds = _not_none(barstd=barstd, barstds=barstds) @@ -1447,8 +1495,8 @@ def indicate_error( # TODO: Permit 3D array with error dimension coming first # NOTE: Previously went to great pains to preserve metadata but now retrieval # of default legend handles moved to _auto_format_1d so can strip. - x, data, *args = args - y = data + x, y, *args = args + data = y if means or medians: if data.ndim != 2: raise ValueError(f'Expected 2D array for means=True. Got {data.ndim}D.') @@ -1481,9 +1529,8 @@ def indicate_error( # Draw dark and light shading getter = kwargs.pop if plot else kwargs.get - vert = getter('vert', getter('orientation', 'vertical') == 'vertical') eobjs = [] - fill = self.fill_between if vert else self.fill_betweenx + fill = self.fill_betweenx if flip else self.fill_between if fade: edata, label = _get_error_data( data, y, errdata=fadedata, stds=fadestds, pctiles=fadepctiles, @@ -1508,8 +1555,8 @@ def indicate_error( eobjs.append(eobj) # Draw thin error bars and thick error boxes - sy = 'y' if vert else 'x' # yerr - ex, ey = (x, y) if vert else (y, x) + sy = 'x' if flip else 'y' # yerr + ex, ey = (y, x) if flip else (x, y) if boxes: edata, _ = _get_error_data( data, y, errdata=boxdata, stds=boxstds, pctiles=boxpctiles, @@ -1541,7 +1588,7 @@ def indicate_error( # 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) + xy = (x, data) if violin else (x, y) kwargs.setdefault('_errobjs', eobjs[:int(shade + fade)]) res = obj = method(self, *xy, *args, **kwargs) @@ -1579,7 +1626,7 @@ def _apply_plot(self, *args, cmap=None, values=None, **kwargs): if cmap is not None: warnings._warn_proplot( 'Drawing "parametric" plots with ax.plot(x, y, values=values, cmap=cmap) ' - 'is deprecated and will be removed in a future release. Please use ' + 'is deprecated and will be removed in the next major release. Please use ' 'ax.parametric(x, y, values, cmap=cmap) instead.' ) return self.parametric(*args, cmap=cmap, values=values, **kwargs) @@ -1589,6 +1636,7 @@ def _apply_plot(self, *args, cmap=None, values=None, **kwargs): name = method.__name__ sx = 'y' if 'x' in name else 'x' # i.e. plotx objs = [] + args = list(args) while args: # Support e.g. x1, y1, fmt, x2, y2, fmt2 input # NOTE: Copied from _process_plot_var_args.__call__ to avoid relying @@ -1605,9 +1653,12 @@ def _apply_plot(self, *args, cmap=None, values=None, **kwargs): # Add sticky edges # NOTE: Skip edges when error bars present or caps are flush against axes edge - if all(isinstance(obj, mlines.Line2D) for obj in iobjs): + lines = all(isinstance(obj, mlines.Line2D) for obj in iobjs) + if lines and not getattr(self, '_no_sticky_edges', False): for obj in iobjs: data = getattr(obj, 'get_' + sx + 'data')() + if not data.size: + continue convert = getattr(self, 'convert_' + sx + 'units') edges = getattr(obj.sticky_edges, sx) edges.append(convert(min(data))) @@ -1667,12 +1718,26 @@ def _stem_extras( return method(self, *args, **kwargs) +def _check_negpos(name, **kwargs): + """ + Issue warnings if we are ignoring arguments for "negpos" plot. + """ + for key, arg in kwargs.items(): + if arg is None: + continue + warnings._warn_proplot( + f'{name}() argument {key}={arg!r} is incompatible with ' + 'negpos=True. Ignoring.' + ) + + def _apply_lines( self, *args, + stack=None, stacked=None, + negpos=False, negcolor=None, poscolor=None, color=None, colors=None, linestyle=None, linestyles=None, lw=None, linewidth=None, linewidths=None, - negpos=False, negcolor=None, poscolor=None, **kwargs ): """ @@ -1681,12 +1746,18 @@ def _apply_lines( # Parse input arguments method = kwargs.pop('_method') name = method.__name__ + stack = _not_none(stack=stack, stacked=stacked) colors = _not_none(color=color, colors=colors) linestyles = _not_none(linestyle=linestyle, linestyles=linestyles) linewidths = _not_none(lw=lw, linewidth=linewidth, linewidths=linewidths) args = list(args) if len(args) > 3: raise ValueError(f'Expected 1-3 positional args, got {len(args)}.') + if len(args) == 3 and stack: + warnings._warn_proplot( + f'{name}() cannot have three positional arguments with stack=True. ' + 'Ignoring second argument.' + ) if len(args) == 2: # empty possible args.insert(1, np.array([0.0])) # default base @@ -1694,17 +1765,14 @@ def _apply_lines( x, y1, y2, *args = args # standardized if not negpos: # Plot basic lines + kwargs['stack'] = stack if colors is not None: kwargs['colors'] = colors result = method(self, x, y1, y2, *args, **kwargs) objs = (result,) else: # Plot negative and positive colors - if colors is not None: - warnings._warn_proplot( - f'{name}() argument color={colors!r} is incompatible with ' - 'negpos=True. Ignoring.' - ) + _check_negpos(name, stack=stack, colors=colors) y1neg, y2neg = _mask_array(y2 < y1, y1, y2) color = _not_none(negcolor, rc['negcolor']) negobj = method(self, x, y1neg, y2neg, color=color, **kwargs) @@ -1715,8 +1783,10 @@ def _apply_lines( # Apply formatting unavailable in matplotlib for obj in objs: - obj.set_linewidth(linewidths) # LineCollection setters - obj.set_linestyle(linestyles) + if linewidths is not None: + obj.set_linewidth(linewidths) # LineCollection setters + if linestyles is not None: + obj.set_linestyle(linestyles) return result @@ -1782,6 +1852,7 @@ def _apply_scatter( # Get colormap cmap_kw = cmap_kw or {} if cmap is not None: + cmap_kw.setdefault('luminance', 90) # matches to_listed behavior cmap = constructor.Colormap(cmap, **cmap_kw) # Get normalizer and levels @@ -1892,33 +1963,29 @@ def _apply_fill_between( if y1.ndim > 1 or y2.ndim > 1: raise ValueError(f'{name}() arguments with negpos=True must be 1D.') kwargs.setdefault('interpolate', True) - for key, arg in (('where', where), ('stack', stack), ('color', color)): - if arg is not None: - warnings._warn_proplot( - f'{name}() argument {key}={arg!r} is incompatible with ' - 'negpos=True. Ignoring.' - ) + _check_negpos(name, where=where, stack=stack, color=color) color = _not_none(negcolor, rc['negcolor']) - negobj = method(self, x, y1, y2, where=(y2 < y1), color=color, **kwargs) + negobj = method(self, x, y1, y2, where=(y2 < y1), facecolor=color, **kwargs) color = _not_none(poscolor, rc['poscolor']) - posobj = method(self, x, y1, y2, where=(y2 >= y1), color=color, **kwargs) + posobj = method(self, x, y1, y2, where=(y2 >= y1), facecolor=color, **kwargs) result = objs = (posobj, negobj) # may be tuple of tuples due to apply_cycle # 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: Could also retrieve data from PolyCollection but that's tricky. # NOTE: Standardize function guarantees ndarray input by now - xsides = (np.min(x), np.max(x)) - ysides = [] - for y in (y1, y2): - if y.size == 1: - ysides.append(y.item()) - objs = tuple(obj for _ in objs for obj in (_ if isinstance(_, tuple) else (_,))) - for obj in objs: - for s, sides in zip((sx, sy), (xsides, ysides)): - convert = getattr(self, 'convert_' + s + 'units') - edges = getattr(obj.sticky_edges, s) - edges.extend(convert(sides)) + if not getattr(self, '_no_sticky_edges', False): + xsides = (np.min(x), np.max(x)) + ysides = [] + for y in (y1, y2): + if y.size == 1: + ysides.append(y.item()) + objs = tuple(obj for _ in objs for obj in (_ if isinstance(_, tuple) else (_,))) + for obj in objs: + for s, sides in zip((sx, sy), (xsides, ysides)): + convert = getattr(self, 'convert_' + s + 'units') + edges = getattr(obj.sticky_edges, s) + edges.extend(convert(sides)) return result @@ -1941,15 +2008,6 @@ def fill_betweenx_extras(self, *args, **kwargs): return _apply_fill_between(self, *args, **kwargs) -def _hist_extras(self, *args, **kwargs): - """ - Force `bar_extras` to interpret `width` as literal rather than relative. - """ - method = kwargs.pop('_method') - with _state_context(self, _absolute_bar_width=True): - return method(self, *args, **kwargs) - - def _convert_bar_width(x, width=1, ncols=1): """ Convert bar plot widths from relative to coordinate spacing. Relative @@ -1974,7 +2032,7 @@ def _convert_bar_width(x, width=1, ncols=1): def _apply_bar( self, *args, stack=None, stacked=None, lw=None, linewidth=None, color=None, facecolor=None, edgecolor='black', - negpos=False, negcolor=None, poscolor=None, **kwargs + negpos=False, negcolor=None, poscolor=None, absolute_width=False, **kwargs ): """ Apply bar or barh command. Support default "minima" at zero. @@ -2003,8 +2061,10 @@ def _apply_bar( # Call func after converting bar width x, h, w, b = args - if not stack and not getattr(self, '_absolute_bar_width', None): - ncols = 1 if h.ndim == 1 else h.shape[1] + reduce = any(kwargs.get(s) for s in ('mean', 'means', 'median', 'medians')) + absolute_width = absolute_width or getattr(self, '_bar_absolute_width', False) + if not stack and not absolute_width: + ncols = 1 if reduce or h.ndim == 1 else h.shape[1] w = _convert_bar_width(x, w, ncols=ncols) if not negpos: # Draw simple bars @@ -2014,20 +2074,15 @@ def _apply_bar( return method(self, x, h, w, b, **kwargs) else: # Draw negative and positive bars - for key, arg in (('stack', stack), ('color', color)): - if arg is not None: - warnings._warn_proplot( - f'{name}() argument {key}={arg!r} is incompatible with ' - 'negpos=True. Ignoring.' - ) + _check_negpos(name, stack=stack, color=color) if x.ndim > 1 or h.ndim > 1: raise ValueError(f'{name}() arguments with negpos=True must be 1D.') hneg = _mask_array(h < b, h) color = _not_none(negcolor, rc['negcolor']) - negobj = method(self, x, hneg, w, b, color=color, **kwargs) + negobj = method(self, x, hneg, w, b, facecolor=color, **kwargs) hpos = _mask_array(h >= b, h) color = _not_none(poscolor, rc['poscolor']) - posobj = method(self, x, hpos, w, b, color=color, **kwargs) + posobj = method(self, x, hpos, w, b, facecolor=color, **kwargs) return (negobj, posobj) @@ -2075,10 +2130,11 @@ def boxplot_extras( *args : 1D or 2D ndarray The data array. vert : bool, optional - If ``False``, box plots are drawn horizontally. + If ``False``, box plots are drawn horizontally. Otherwise, box plots + are drawn vertically. orientation : {{None, 'vertical', 'horizontal'}}, optional Alternative to the native `vert` keyword arg. Added for - consistency with `~matplotlib.axes.Axes.bar`. + consistency with the rest of matplotlib. mean, means : bool, optional If ``True``, this passes ``showmeans=True`` and ``meanline=True`` to `~matplotlib.axes.Axes.boxplot`. @@ -2128,7 +2184,6 @@ def boxplot_extras( # Parse keyword args # NOTE: For some reason native violinplot() uses _get_lines for # property cycler. We dot he same here. - vert, kwargs = _get_vert(**kwargs) # interpret 'orientation' and 'vert' method = kwargs.pop('_method') fill = fill is True or fillcolor is not None or fillalpha is not None fillalpha = _not_none(fillalpha, default=0.7) @@ -2150,7 +2205,7 @@ def boxplot_extras( kwargs['showmeans'] = kwargs['meanline'] = True # Call function - obj = method(self, *args, vert=vert, **kwargs) + obj = method(self, *args, **kwargs) if not args: return obj @@ -2224,10 +2279,11 @@ def violinplot_extras( *args : 1D or 2D ndarray The data array. vert : bool, optional - If ``False``, box plots are drawn horizontally. + If ``False``, violin plots are drawn horizontally. Otherwise, + violin plots are drawn vertically. orientation : {{None, 'vertical', 'horizontal'}}, optional Alternative to the native `vert` keyword arg. - Added for consistency with `~matplotlib.axes.Axes.bar`. + Added for consistency with the rest of matplotlib. fillcolor : color-spec, list, optional The violin plot fill color. Default is the next color cycler color. If a list, it should be the same length as the number of objects. @@ -2254,12 +2310,10 @@ def violinplot_extras( apply_cycle """ # Parse keyword args - vert, kwargs = _get_vert(**kwargs) # interpret 'orientation' and 'vert' method = kwargs.pop('_method') color = _not_none(color=color, edgecolor=edgecolor, default='black') linewidth = _not_none(lw=lw, linewidth=linewidth, default=0.8) fillalpha = _not_none(fillalpha, default=0.7) - kwargs.update({'showmeans': False, 'showmedians': False, 'showextrema': False}) kwargs.setdefault('capsize', 0) # caps are redundant for violin plots kwargs.setdefault('means', kwargs.pop('showmeans', None)) # for indicate_error kwargs.setdefault('medians', kwargs.pop('showmedians', None)) @@ -2267,7 +2321,8 @@ def violinplot_extras( warnings._warn_proplot('Ignoring showextrema=True.') # Call function - obj = result = method(self, *args, vert=vert, linewidth=linewidth, **kwargs) + kwargs.update({'showmeans': False, 'showmedians': False, 'showextrema': False}) + obj = result = method(self, *args, linewidth=linewidth, **kwargs) if not args: return result @@ -2637,11 +2692,12 @@ def apply_cycle( plot = name in ('plot',) scatter = name in ('scatter',) fill = name in ('fill_between', 'fill_betweenx') - hist = name in ('hist',) - bar = name in ('bar',) - pie = name in ('pie',) + bar = name in ('bar', 'barh') + lines = name in ('vlines', 'hlines') box = name in ('boxplot', 'violinplot') violin = name in ('violinplot',) + hist = name in ('hist',) + pie = name in ('pie',) cycle_kw = cycle_kw or {} legend_kw = legend_kw or {} colorbar_kw = colorbar_kw or {} @@ -2651,8 +2707,14 @@ def apply_cycle( # TODO: Support stacking vlines/hlines because it would be really easy x, y, *args = args stack = False - if fill or bar: + if lines or fill or bar: stack = kwargs.pop('stack', False) + elif stack in kwargs: + raise TypeError(f"{name}() got unexpected keyword argument 'stack'.") + if fill or lines: + ncols = max(1 if iy.ndim == 1 else iy.shape[1] for iy in (y, args[0])) + else: + ncols = 1 if pie or box or y.ndim == 1 else y.shape[1] # Update the property cycler apply_manually = {} @@ -2664,7 +2726,6 @@ def apply_cycle( apply_manually = _update_cycle(self, cycle, scatter=scatter, **kwargs) # Handle legend labels - ncols = 1 if pie or box or y.ndim == 1 else y.shape[1] if pie or box: # Functions handle multiple labels on their own # NOTE: Using boxplot() without this will overwrite labels previously @@ -2695,38 +2756,41 @@ def apply_cycle( ikwargs[key] = props[prop] # The x coordinates for bar plots - ix, iy, iargs = x, y, args - if bar and stack and iy.ndim > 1: - args[2] = iy[:, :i].sum(axis=1) # 4th positional arg is 'bototm' + ix, iy, iargs = x, y, args.copy() + offset = 0 if bar and not stack: - offset = args[1] * (i - 0.5 * (ncols - 1)) # 3rd positional arg is 'width' + offset = iargs[0] * (i - 0.5 * (ncols - 1)) # 3rd positional arg is 'width' ix = x + offset + # The y coordinates for stacked plots + # NOTE: If stack=True then we always *ignore* second argument passed + # to area or lines. Warning should be issued by 'extras' wrappers. + if stack and ncols > 1: + if bar: + iargs[1] = iy[:, :i].sum(axis=1) # the new 'bottom' + else: + iy = iargs[0] # for vlines, hlines, area, arex + ys = (iy if iy.ndim == 1 else iy[:, :j].sum(axis=1) for j in (i, i + 1)) + iy, iargs[0] = ys + # The y coordinates and labels - # WARNING: If stack=True then we always *ignore* second argument passed to - # fill_between. Warning should be issued by fill_between_extras in this case. - if pie or box: # only ever have one y value - pass - elif fill and stack: # ignore argument 2 as warned in _apply_fill_between + # NOTE: Support 1D x, 2D y1, and 2D y2 input + if not pie and not box: # only ever have one y value iy, *iargs = ( - args[0] if args[0].ndim == 1 else args[0][:, :ii].sum(axis=1) - for ii in (i, i + 1) - ) - ikwargs['label'] = labels[i] or None - else: - iy, *iargs = ( # careful to support e.g. hist() bins positional arg - arg if _to_arraylike(arg).ndim == 1 else arg[:, i] - for arg in (y, *args) + arg if not isinstance(arg, np.ndarray) or arg.ndim == 1 + else arg[:, i] for arg in (iy, *iargs) ) ikwargs['label'] = labels[i] or None # Call function for relevant column # NOTE: Should have already applied 'x' coordinates with keywords - # or as labels by this point for funcs where we omit them. - if pie or box or hist: - obj = method(self, iy, *iargs, **ikwargs) - else: - obj = method(self, ix, iy, *iargs, **ikwargs) + # or as labels by this point for funcs where we omit them. Also note + # hist() does not pass kwargs to bar() so need this funky state context. + with _state_context(self, _bar_absolute_width=True, _no_sticky_edges=True): + if pie or box or hist: + obj = method(self, iy, *iargs, **ikwargs) + else: + obj = method(self, ix, iy, *iargs, **ikwargs) if isinstance(obj, (list, tuple)) and len(obj) == 1: obj = obj[0] objs.append(obj) @@ -2821,8 +2885,8 @@ def _auto_levels_locator( elif isinstance(norm, mcolors.LogNorm): level_locator = tick_locator = mticker.LogLocator(**locator_kw) elif isinstance(norm, mcolors.SymLogNorm): - locator_kw.setdefault('base', _flexible_getattr(norm, 'base', 10)) - locator_kw.setdefault('linthresh', _flexible_getattr(norm, 'linthresh', 1)) + locator_kw.setdefault('base', _getattr_flexible(norm, 'base', 10)) + locator_kw.setdefault('linthresh', _getattr_flexible(norm, 'linthresh', 1)) level_locator = tick_locator = mticker.SymmetricalLogLocator(**locator_kw) else: nbins = N * 2 if positive or negative else N @@ -3059,6 +3123,8 @@ def _fix_white_lines(obj, linewidth=0.3): # See: https://stackoverflow.com/q/15003353/4970632 # 0.3pt is thick enough to hide lines but thin enough to not add "dots" # in corner of pcolor plots so good compromise. + if not hasattr(obj, 'cmap'): + return cmap = obj.cmap if not cmap._isinit: cmap._init() @@ -3101,10 +3167,10 @@ def _labels_contour(self, obj, *args, fmt=None, **kwargs): # Draw hidden additional contour for filled contour labels colors = None - if obj.filled: # guard against changes? - obj = self.contour(*args, levels=obj.levels, linewidths=0) + if _getattr_flexible(obj, 'filled'): # guard against changes? lums = [to_xyz(obj.cmap(obj.norm(level)), 'hcl')[2] for level in obj.levels] colors = ['w' if lum < 50 else 'k' for lum in lums] + obj = self.contour(*args, levels=obj.levels, linewidths=0) kwargs.setdefault('colors', colors) # Draw labels @@ -3252,6 +3318,7 @@ def apply_cmap( proplot.constructor.Colormap proplot.constructor.Norm proplot.colors.DiscreteNorm + proplot.utils.edges """ method = kwargs.pop('_method') name = method.__name__ @@ -3259,6 +3326,7 @@ def apply_cmap( contourf = name in ('contourf', 'tricontourf') pcolor = name in ('pcolor', 'pcolormesh', 'pcolorfast') hexbin = name in ('hexbin',) + hist2d = name in ('hist2d',) parametric = name in ('parametric',) no_discrete_norm = hexbin # TODO: this should be global setting!!! if not args: @@ -3398,18 +3466,18 @@ def apply_cmap( # Optionally add colorbar if colorbar: - loc = self._loc_translate(colorbar, 'colorbar') + m = obj + if hist2d: + m = obj[-1] if parametric and values is not None: colorbar_kw.setdefault('values', values) - if loc != 'fill': - colorbar_kw.setdefault('loc', loc) - self.colorbar(obj, **colorbar_kw) + self.colorbar(m, loc=colorbar, **colorbar_kw) return obj def _generate_mappable( - mappable, values=None, *, orientation='horizontal', + mappable, values=None, *, orientation=None, locator=None, formatter=None, norm=None, norm_kw=None, rotation=None, ): """ @@ -3420,6 +3488,7 @@ def _generate_mappable( # A colormap instance # TODO: Pass remaining arguments through Colormap()? This is really # niche usage so maybe not necessary. + orientation = _not_none(orientation, 'horizontal') if isinstance(mappable, mcolors.Colormap): # NOTE: 'Values' makes no sense if this is just a colormap. Just # use unique color for every segmentdata / colors color. @@ -3520,7 +3589,7 @@ def colorbar_extras( locator_kw=None, minorlocator_kw=None, formatter=None, ticklabels=None, formatter_kw=None, rotation=None, norm=None, norm_kw=None, # normalizer to use when passing colors/lines - orientation='horizontal', + orientation=None, edgecolor=None, linewidth=None, labelsize=None, labelweight=None, labelcolor=None, ticklabelsize=None, ticklabelweight=None, ticklabelcolor=None, @@ -3659,6 +3728,7 @@ def colorbar_extras( # Colorbar kwargs grid = _not_none(grid, rc['colorbar.grid']) + orientation = _not_none(orientation, 'horizontal') kwargs.update({ 'cax': self, 'use_gridspec': True, @@ -4335,7 +4405,7 @@ def _apply_wrappers(method, *args): for func in args[::-1]: # Apply wrapper # NOTE: Must assign fucn and method as keywords to avoid overwriting - # by look scope and associated recursion errors. + # by loop scope and associated recursion errors. method = functools.wraps(method)( lambda self, *args, _func=func, _method=method, **kwargs: _func(self, *args, _method=_method, **kwargs)