From 1d692dcedaed07134d3f6ee6a1e28607418ab8e8 Mon Sep 17 00:00:00 2001 From: Spencer Clark Date: Wed, 6 Mar 2019 21:35:01 -0500 Subject: [PATCH] More improvements to documentation (#28) * Documentation updates * Add what's new page --- doc/conf.py | 3 +- doc/examples.rst | 26 +++++++---- doc/index.rst | 2 + doc/whats-new.rst | 14 ++++++ doc/why-faceted.rst | 108 ++++++++++++++++++++++++-------------------- faceted/faceted.py | 2 +- setup.py | 57 +++++++++++++++++------ 7 files changed, 137 insertions(+), 75 deletions(-) create mode 100644 doc/whats-new.rst diff --git a/doc/conf.py b/doc/conf.py index 9ae8b11..1d2fdf4 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -77,7 +77,7 @@ source_suffix = '.rst' # The encoding of source files. -#source_encoding = 'utf-8-sig' +# source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' @@ -314,6 +314,7 @@ intersphinx_mapping = { 'python': ('https://docs.python.org/3/', None), 'numpy': ('https://docs.scipy.org/doc/numpy/', None), + 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), 'matplotlib': ('https://matplotlib.org/', None), 'mpl_toolkits': ('https://matplotlib.org/', None) } diff --git a/doc/examples.rst b/doc/examples.rst index 6941a0a..cc3f7f0 100644 --- a/doc/examples.rst +++ b/doc/examples.rst @@ -1,16 +1,20 @@ Using faceted ============= -``faceted`` is quite flexible. Here are a couple of examples illustrating the -different features. Using it in many ways resembles using ``plt.subplots``. +:py:meth:`faceted.faceted` is quite flexible. Here are a couple of examples +illustrating the different features. Using it in many ways resembles using +:py:meth:`matplotlib.pyplot.subplots`. .. ipython:: python :okwarning: import matplotlib.pyplot as plt import xarray as xr + from matplotlib import ticker from faceted import faceted + tick_locator = ticker.MaxNLocator(nbins=3) + ds = xr.tutorial.load_dataset('rasm').isel(x=slice(30, 37), y=-1, time=slice(0, 11)) temp = ds.Tair @@ -21,6 +25,7 @@ different features. Using it in many ways resembles using ``plt.subplots``. ax.set_title('{:0.2f}'.format(temp.xc.isel(x=i).item())) ax.set_xlabel('Time') ax.set_ylabel('Temperature [C]') + ax.tick_params(axis='x', labelrotation=45) @savefig example_tair_base.png fig.show() @@ -39,13 +44,14 @@ width. .. ipython:: python :okwarning: - fig, axes = faceted(2, 3, width=8, left_pad=0.75, bottom_pad=0.75, + fig, axes = faceted(2, 3, width=8, left_pad=0.75, bottom_pad=0.9, internal_pad=(0.33, 0.66)) for i, ax in enumerate(axes): temp.isel(x=i).plot(ax=ax, marker='o', ls='none') ax.set_title('{:0.2f}'.format(temp.xc.isel(x=i).item())) ax.set_xlabel('Time') ax.set_ylabel('Temperature [C]') + ax.tick_params(axis='x', labelrotation=45) @savefig example_tair_padding.png fig.show() @@ -69,7 +75,7 @@ from the time mean at each location in the lower row instead. mean = (ds.Tair * time_weights).sum('time') / time_weights.where(np.isfinite(ds.Tair)).sum('time') anomaly = ds.Tair - mean - fig, axes = faceted(2, 3, width=8, left_pad=0.75, bottom_pad=0.75, + fig, axes = faceted(2, 3, width=8, left_pad=0.75, bottom_pad=0.9, internal_pad=(0.33, 0.66), sharey='row') for i, ax in enumerate(axes[:3]): temp.isel(x=i).plot(ax=ax, marker='o', ls='none') @@ -82,6 +88,7 @@ from the time mean at each location in the lower row instead. ax.set_title('{:0.2f}'.format(temp.xc.isel(x=i).item())) ax.set_xlabel('Time') ax.set_ylabel('Anomaly [C]') + ax.tick_params(axis='x', labelrotation=45) @savefig example_tair_share_axes.png fig.show() @@ -90,13 +97,13 @@ Colorbar modes and locations ---------------------------- Let's say we are plotting 2D data in the form of pcolormesh plots that require -a colorbar. ``faceted`` comes with a number of options for placing and sizing +a colorbar. :py:meth:`faceted.faceted` comes with a number of options for placing and sizing colorbars in a paneled figure. We can add a colorbar to a figure by modifying the ``cbar_mode`` argument; by default it is set to ``None``, meaning no colorbar, as in the plots above. For all of the examples here, we'll just plot a time series of maps. Since the xarray tutorial data is geographic in nature, -we'll also use this opportunity to show how to use ``cartopy`` with -``faceted``. +we'll also use this opportunity to show how to use :py:mod:`cartopy` with +:py:meth:`faceted.faceted`. Single colorbar ############### @@ -178,12 +185,11 @@ Colorbars for each panel ######################## One more common use case is a colorbar for each panel. This can be done by -specifying ``cbar_mode='each'`` as an argument in the call to ``faceted``. +specifying ``cbar_mode='each'`` as an argument in the call to :py:meth:`faceted.faceted`. .. ipython:: python :okwarning: - from matplotlib import ticker tick_locator = ticker.MaxNLocator(nbins=3) aspect = 75. / 180. @@ -212,7 +218,7 @@ specifying ``cbar_mode='each'`` as an argument in the call to ``faceted``. Parameter defintions -------------------- -A full summary of the meanings of the different arguments to ``faceted`` can be +A full summary of the meanings of the different arguments to :py:meth:`faceted.faceted` can be found here. Parameters controlling figure and axes dimensions diff --git a/doc/index.rst b/doc/index.rst index 1024752..60bc64d 100644 --- a/doc/index.rst +++ b/doc/index.rst @@ -47,6 +47,7 @@ Currently the only option for installing ``faceted`` is from source:: Documentation ------------- +* :doc:`whats-new` * :doc:`why-faceted` * :doc:`examples` * :doc:`api` @@ -55,6 +56,7 @@ Documentation :maxdepth: 1 :hidden: + whats-new why-faceted examples api diff --git a/doc/whats-new.rst b/doc/whats-new.rst new file mode 100644 index 0000000..4045d09 --- /dev/null +++ b/doc/whats-new.rst @@ -0,0 +1,14 @@ +.. _whats-new: + +########## +What's New +########## + +.. _whats-new.0.1: + +v0.1 (unreleased) +================= + +- Initial release. Note the name has changed since the development version from + ``facets`` to ``faceted``. +- Default ``cbar_short_side_pad`` is now 0.0 instead of 0.5 inches. diff --git a/doc/why-faceted.rst b/doc/why-faceted.rst index 0ccd88d..faa02d4 100644 --- a/doc/why-faceted.rst +++ b/doc/why-faceted.rst @@ -2,8 +2,10 @@ Why faceted? ============ At first glance it might seem we are re-inventing the wheel here. If you just -google for "matplotlib subplots with shared colorbar" you'll find a -StackOverflow question with numerous answers with varying levels of +google for "matplotlib subplots with shared colorbar" you'll find `a +StackOverflow question +`_ +with numerous answers with varying levels of complexity (some in fact are quite elegant). It might be tempting to go with one of these solutions, e.g. @@ -27,10 +29,10 @@ one of these solutions, e.g. This looks ok, but things become a bit more challenging when we'd like to have a more control over the spacing and size of elements in the figure. -``matplotlib`` is super-flexible in that it is indeed *possible* to do this, -but if your starting point for creating paneled figures is ``plt.subplots``, -which it so often is for many of us, your options for exerting this type of -control are somewhat tricky to use. +:py:mod:`matplotlib` is super-flexible in that it is indeed *possible* to do this, +but if your starting point for creating paneled figures is +:py:meth:`matplotlib.pyplot.subplots`, which it so often is for many of us, +your options for exerting this type of control are somewhat tricky to use. Let's take the example above and start to impose some contraints: @@ -41,10 +43,10 @@ Let's take the example above and start to impose some contraints: an inch. This thickness *should not* depend on the overall dimensions of the figure. - The data we are plotting is geographic in nature; we really should be using - ``cartopy``, which will require that the panels have a strict aspect ratio, + :py:mod:`cartopy`, which will require that the panels have a strict aspect ratio, related to the extent of the domain in latitude-longitude space. Currently the aspect ratio is set dynamically based on the total figure size and - ``matplotlib`` defaults for between-plot spacing and outer padding. + :py:mod:`matplotlib` defaults for between-plot spacing and outer padding. One by one we'll go through these illustrating how much complexity this adds to our code just to produce a simple figure. @@ -54,8 +56,8 @@ Fixing the between-plot spacing As soon as we try to assign a certain amount of physical space to a plot element, we need to do some algebra. This is because to change the panel -spacing after a call to ``plt.subplots``, we need to use -``plt.subplots_adjust``, which takes parameters representing an amount of +spacing after a call to :py:meth:`matplotlib.pyplot.subplots`, we need to use +:py:meth:`matplotlib.pyplot.subplots_adjust`, which takes parameters representing an amount of *relative* space, meaning expressed as a fraction of a plot element, be it the whole figure or a single panel. @@ -63,7 +65,7 @@ To help set up the problem, let's define some variables. First, let's say that we have :math:`m` rows of :math:`n` panels each; in our example :math:`m = 1` and :math:`n = 3`. Then let's say that we would like to introduce an internal pad, :math:`p_{internal}`, representing the spacing -between the axes in inches. In order to use ``plt.subplots_adjust``, we need +between the axes in inches. In order to use :py:meth:`matplotlib.pyplot.subplots_adjust`, we need to determine the amount of relative space :math:`p_{internal}` represents. In the context of the ``wspace`` parameter, the parameter that controls the spacing between panels, we need to determine the ratio of the width of the @@ -75,14 +77,14 @@ a figure of width :math:`w`, with outer left and right paddings of w_{panel} = \frac{w - p_{left} - p_{right} - (n - 1) p_{internal}}{n}. -Therefore the value we pass to ``wspace`` in ``plt.subplots_adjust`` is: +Therefore the value we pass to ``wspace`` in :py:meth:`matplotlib.pyplot.subplots_adjust` is: .. math:: \texttt{wspace} = \frac{p_{internal}}{w_{panel}}. Finally, since in this process we needed to fix the left and right pads of the -figure, we need to specify those in ``plt.subplots_adjust`` too; note these are +figure, we need to specify those in :py:meth:`matplotlib.pyplot.subplots_adjust` too; note these are defined relative to the full figure width rather than the width of single panel: .. math:: @@ -142,22 +144,24 @@ colorbar and the panels, :math:`p_{cbar}`, a thickness for the colorbar, w_cbar = 0.125 h = 4. -The top and bottom pads need to be passed to ``plt.subplots_adjust`` and they +The top and bottom pads need to be passed to +:py:meth:`matplotlib.pyplot.subplots_adjust` and they follow similar conventions to the left and right pads, i.e. they are defined in terms of length relative to the overall height of the figure. The size of the colorbar is controlled differently; we control its size when we construct it -using ``plt.colorbar``, using the ``fraction``, ``pad``, and ``aspect`` -arguments. ``fraction`` dictates the fraction of the height of the colorbar -would take with respect to the height of a single panel in the *original* -figure; ``pad`` dictates the fraction of a single panel in the *original* -figure the padding between the colorbar and panels would take; and ``aspect`` -sets the ratio of the width of the long part of the colorbar to its thickness. -Note that since we call ``plt.subplots_adjust`` before calling -``plt.colorbar``, the panel height in the original figure is determined in part -by our imposed :math:`p_{top}` and :math:`p_{bottom}`. In this case since we -are only using a single row of panels, we do not need to worry about the -between panel spacing in this dimension, but we'll include the -:math:`p_{internal}` term to keep things general: +using :py:meth:`matplotlib.pyplot.colorbar`, using the ``fraction``, ``pad``, +and ``aspect`` arguments. ``fraction`` dictates the fraction of the height of +the colorbar would take with respect to the height of a single panel in the +*original* figure; ``pad`` dictates the fraction of a single panel in the +*original* figure the padding between the colorbar and panels would take; and +``aspect`` sets the ratio of the width of the long part of the colorbar to its +thickness. Note that since we call :py:meth:`matplotlib.pyplot.subplots_adjust` +before calling :py:meth:`matplotlib.pyplot.colorbar`, the panel height in the +original figure is determined in part by our imposed :math:`p_{top}` and +:math:`p_{bottom}`. In this case since we are only using a single row of +panels, we do not need to worry about the between panel spacing in this +dimension, but we'll include the :math:`p_{internal}` term to keep things +general: .. math:: @@ -202,11 +206,13 @@ Holding panels at a fixed aspect ratio -------------------------------------- Things are starting to look much better, but there's still more work to do. -Let's introduce ``cartopy`` to the mix. Adding a ``cartopy`` projection turns +Let's introduce :py:mod:`cartopy` to the mix. Adding a :py:mod:`cartopy` +projection turns out to fix the aspect ratio of the panels in the figure, regardless of the figure size. We'll want to address this additional constraint by adjusting our value for the total height of the figure, because the panel height will now by -completely determined by the panel width. In a ``PlateCarree`` projection, the +completely determined by the panel width. In a +:py:class:`cartopy.crs.PlateCarree` projection, the aspect ratio will be determined by the ratio of the latitudinal extent of the map divided by the longitudinal extent. In this case it will be :math:`\texttt{aspect} = \frac{75}{360}`. :math:`h_{panel}` will now be @@ -225,12 +231,13 @@ elements: h = m h_{panel} + (m - 1) p_{internal} + p_{bottom} + p_{top} + p_{cbar} + w_{cbar}. As a result of the height values changing, we'll need to update the ``bottom`` and -``top`` parameters for ``plt.subplots_adjust`` as well as the colorbar size -parameters: +``top`` parameters for :py:meth:`matplotlib.pyplot.subplots_adjust` as well as +the colorbar size parameters: .. ipython:: python a = 75. / 360. + p_cbar = 0.25 h_panel = a * w_panel h = p_bottom + p_top + h_panel + p_cbar + w_cbar h_panel_original = h - p_top - p_bottom @@ -264,24 +271,26 @@ parameters: As examples go, this one was actually fairly simple; we only had one row of panels, rather than multiple, and we only had one colorbar. Taking the -``plt.subplots`` approach was remarkably complicated. Admittedly, it would be -*slightly* more straightforward to use the ``AxesGrid`` framework to do this, -but other problems remain with that approach; e.g. using ``AxesGrid`` with +:py:meth:`matplotlib.pyplot.subplots` approach was remarkably complicated. +Admittedly, it would be +*slightly* more straightforward to use the :py:class:`mpl_toolkits.axes_grid1.AxesGrid` framework to do this, +but other problems remain with that approach; e.g. using :py:class:`mpl_toolkits.axes_grid1.AxesGrid` with cartopy is not ideal due to axes sharing issues (`SciTools/cartopy#939 `_), and colorbars drawn using -``AxesGrid`` are drawn using an outdated colorbar class in ``matplotlib``, +:py:class:`mpl_toolkits.axes_grid1.AxesGrid` are drawn using an outdated colorbar class in :py:mod:`matplotlib`, which is different than the one used by default (`matplotlib/matplotlib#9778 -`_). In ``faceted`` we use -``AxesGrid`` to aid in the placing the axes and colorbars, but we do not use -the axes generated by it. Instead we create our own, which are modern and have -working axes-sharing capabilities. In so doing we create a -``plt.subplots``-like interface, which is slightly more intuitive to use than -``AxesGrid``. +`_). In +:py:meth:`faceted.faceted` we use :py:class:`mpl_toolkits.axes_grid1.AxesGrid` to aid in the placing the axes +and colorbars (some math is still required to determine the figure height), but +we do not use the axes generated by it. Instead we create our own, +which are modern and have working axes-sharing capabilities. In so doing we +create a :py:meth:`matplotlib.pyplot.subplots`-like interface, which is +slightly more intuitive to use than :py:class:`mpl_toolkits.axes_grid1.AxesGrid`. How would you do this in faceted? --------------------------------- -In ``faceted`` this becomes much simpler; there is no need to do any algebra +In :py:meth:`faceted.faceted` this becomes much simpler; there is no need to do any algebra or post-hoc adjustment of the axes placement; everything gets handled in the top-level function. @@ -313,11 +322,14 @@ top-level function. What can't you do in faceted? ----------------------------- -The main thing that ``faceted`` cannot do is create a constrained set of axes +The main thing that :py:meth:`faceted.faceted` cannot do is create a +constrained set of axes that have varying size, or varying properties. For more complex figure -construction tasks we recommend using a more fundamental ``matplotlib`` -approach, either using ``AxesGrid``, ``GridSpec``, or ``ConstrainedLayout``. -The main reason for creating ``faceted`` was that these other tools were *too* -flexible at the expense of simplicity. For a large percentage of the use -cases, they are not required, but for the remaining percentage they are indeed -quite useful. +construction tasks we recommend using a more fundamental :py:mod:`matplotlib` +approach, either using :py:class:`mpl_toolkits.axes_grid1.AxesGrid`, +:py:class:`matplotlib.GridSpec`, or `Constrained Layout +`_. The +main reason for creating :py:meth:`faceted.faceted` was that these other tools +were *too* flexible at the expense of simplicity. For a large percentage of +the use cases, they are not required, but for the remaining percentage they are +indeed quite useful. diff --git a/faceted/faceted.py b/faceted/faceted.py index 1eea6b0..66a177a 100644 --- a/faceted/faceted.py +++ b/faceted/faceted.py @@ -8,7 +8,7 @@ def faceted(rows, cols, width=8., aspect=0.618, top_pad=0.25, bottom_pad=0.25, left_pad=0.25, right_pad=0.25, internal_pad=0.33, - cbar_mode=None, cbar_short_side_pad=0.5, cbar_pad=0.5, + cbar_mode=None, cbar_short_side_pad=0.0, cbar_pad=0.5, cbar_size=0.125, cbar_location='right', sharex='all', sharey='all', axes_kwargs={}): """Create figure and tiled axes objects with precise attributes. diff --git a/setup.py b/setup.py index b679f9b..9e39bd7 100644 --- a/setup.py +++ b/setup.py @@ -3,22 +3,34 @@ import versioneer LONG_DESCRIPTION = """ -This is a module that I use in practice to produce both single and multi-panel -figures for presentations and manuscripts. The reason I have gone through the -trouble to write something like this is that I am particular about a few -things: +The purpose of this module is to make it easy to produce single-or-multi-panel +figures in matplotlib with strict dimensional constraints. For example, perhaps +you would like to make a figure that fits exactly within a column of a +manuscript without any scaling, and you would like the panels to be as large as +possible, but retain a fixed aspect ratio (height divided by width). Maybe some +(or all) of your panels require an accompanying colorbar. With out of the box +matplotlib tools this is actually somewhat tricky. -- I want tight, but easy, control over the space in between the panels of my -figures in real space (not relative space). -- I want tight control over the aspect ratio of the panels of my figure (e.g. -when plotting maps), but still work within a strict dimensional constraint over -the entire figure (e.g. I want to make a figure to fit in a column of a -manuscript). -- I want to make sure that the colorbars in all of my figures have the same -thickness throughout my presentations or manuscripts; unfortunately it is hard -to control this using the default matplotlib tools (you can set the thickness -in relative space, but you need to be careful about what that means within the -context of your figure size). +Internally, this module uses the flexible [`matplotlib` `AxesGrid` toolkit](https://matplotlib.org/2.0.2/mpl_toolkits/axes_grid/users/overview.html#axes-grid1), +with some additional logic to enable making these kinds of +dimensionally-constrained +panel plots with precise padding and colorbar size(s). + +Another project with a similar motivation is [panel-plots]( +https://github.com/ajdawson/panel-plots); however it does not have support +for adding colorbars to a dimensionally-constrained figure. One part of the +implementation there that inspired part of what is done here is the ability +to add user-settable padding to the edges of the figure (to add space for +axes ticks, ticklabels, and labels). This eliminates the need for using +`bbox_inches='tight'` when saving the figure, and enables you +to make sure that your figures are *exactly* the dimensions you need for your +use. + +Important links +--------------- +- HTML documentation: https://faceted.readthedocs.io/en/latest/ +- Issue tracker: https://github.com/spencerkclark/faceted/issues +- Source code: https://github.com/spencerkclark/faceted """ @@ -28,10 +40,25 @@ cmdclass=versioneer.get_cmdclass(), packages=setuptools.find_packages(), author='Spencer K. Clark', + author_email='spencerkclark@gmail.com', description='Precisely spaced subplots', long_description=LONG_DESCRIPTION, install_requires=[ 'matplotlib >= 1.5', 'numpy >= 1.7', + ], + keywords='matplotlib cartopy multi-panel plots colorbars', + url='https://github.com/spencerkclark/faceted', + classifiers=[ + 'Development Status :: 4 - Beta', + 'Intended Audience :: Science/Research', + 'License :: OSI Approved :: MIT License', + 'Natural Language :: English', + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'Programming Language :: Python :: 3.5', + 'Programming Language :: Python :: 3.6', + 'Programming Language :: Python :: 3.7', + 'Topic :: Scientific/Engineering' ] )