Giter Club home page Giter Club logo

faceted's Introduction

faceted

Build Status codecov Documentation Status PyPI black

Figures with precise control over overall width, overall height, plot aspect ratio, between-plot spacing, and colorbar dimensions.

Description

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.

readme-example.png

Internally, this module uses the flexible matplotlib AxesGrid toolkit, 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; 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.

I intend to keep the scope of this project quite limited. I want the results it produces to remain extremely easy to understand and control. For more complicated figure layouts, e.g. multiple panels with different sizes and aspect ratios, and a more magical approach to setting figure boundary padding and between-panel spacing, a library worth checking out is proplot. The "smart tight layout" feature it provides is an impressive automated method of solving some of the same problems addressed by this library.

For information on how to use faceted, see the documentation: https://faceted.readthedocs.io/en/latest/.

Installation

You can install faceted either from PyPI:

$ pip install faceted

or directly from source:

$ git clone https://github.com/spencerkclark/faceted.git
$ cd faceted
$ pip install -e .

faceted's People

Contributors

spencerkclark avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar

faceted's Issues

Incorrect plot area aspect ratio when using cbar_mode='single' and cbar_location='left' or 'bottom'

This logic was inserted because of the comment in the code.

https://github.com/spencerkclark/facets/blob/20705e9e314fdd7cb02974be5bdb1b77aa5e6260/facets/facets.py#L92-L98

But this fix is not totally correct. Adding a correction to the cbar_pad used throughout WidthConstrainedAxesGrid causes problems downstream when computing the widths and heights of the panels. The corrected cbar_pad should only be passed to AxesGrid and the WidthConstrainedAxesGrid attribute should remain unchanged.

saved figures are cropped; bounding box issue?

If I understand correctly, faceted should eliminate the need to save with bbox_inches='tight', as the contents of the figure should automatically be sized to fit within the specified dimensions (determined by any two of width, height, aspect).

When I save figures created with faceted, however, the result looks cropped and does not contain all of the content. Here is a minimal example:

import faceted
import numpy as np

# Data for plotting
t = np.arange(0.0, 2.0, 0.01)
s = 1 + np.sin(2 * np.pi * t)

fig, axis = faceted.faceted(1,1, width=3.0, height=3.0)

axis[0].plot(t, s)

axis[0].set(xlabel='time (s)', ylabel='voltage (mV)',
              title='About as simple as it gets, folks')
axis[0].grid()

fig.savefig("test.png", dpi=100)

yields the attached plot:

test

Is this a bug or am I misunderstanding the intended functionality of faceted? Thank you!

Version number: faceted (0.2.1) and matplotlib (3.5.1),

Add support for other types of constrained figures

Currently only width is enabled as a constraint. While for me this has been the most common use case, there are at least two other conceivable use-cases:

  • A height-constrained figure with a fixed aspect ratio (and flexible width)
  • A width and height constrained figure, with a flexible aspect ratio.

Internally I think it makes sense to create separate classes for these other types of constraints; we'll have to think a little more carefully how we might want to expose them in the interface. It could be something as simple as which keyword arguments one specifies (out of width, height, and aspect).

Use AxesGrid internally?

The AxesGrid framework offers much of the functionality implemented here and is already in matplotlib. It does not offer an equivalent to the cbar_short_side_pad parameter, but past that I think everything implemented here would be possible with AxesGrid (when wrapped correctly).

AxesGrid is not perfect, particularly with sharing axes and Cartopy, but it is probably better to work with what matplotlib has designed specifically for this purpose than rolling our own solution.

What I would be interested in adding on top of AxesGrid:

  • The ability to set the colorbar thickness in inches [this is simple; it's just the ratio of the desired colorbar thickness to the figure width or height (depending on its location)] Edit: this is already possible in AxesGrid by spacing a float value for the cbar_size argument.
  • The ability to set the figure size in inches
  • The ability to set the aspect ratio of each of the panels in the figure
  • The ability to set the outer padding of the figure in inches (we can use the rect parameter to set the extent of the inner panel objects in the overall figure).

Choosing the figure size will require some calculation if we want precise control over the aspect ratio of the panels. While AxesGrid has an aspect parameter, it only takes values of True or False (one cannot set a decimal value for instance). When True it just takes the aspect ratio of the axes limits (so if the scale of the axes values differs a lot, this is a problem. That said, it works pretty well for cartopy, because that is what we want in that case). Ultimately though, I think that is much less work than we currently do here.

With what we've explored in #9, perhaps it might even be possible to hack together a workaround for the sharing axes issues with cartopy.

As a reference here is a link to the documentation of ImageGrid, which is the same as AxesGrid: https://matplotlib.org/2.0.2/mpl_toolkits/axes_grid/users/overview.html#axes-grid1

Convenience function for single axis figure

I use this in various Notebooks as a convenience function to get a single-axis figure but still using faceted's kwargs (see code below). Any interest in including it or something like it in faceted proper? Another alternative would be for faceted to return a single ax instance rather than a length-1 array in the case of a single-paneled plot.

Basically this is to avoid having to do

fig, axarr = faceted(1, 1, width=4)
ax = axarr[0]

and instead simply do

fig, ax = facet_ax()
def facet_ax(width=4, cbar_mode=None, **kwargs):
    """Use faceted to create single panel figure."""
    if cbar_mode is None:
        fig, axarr = faceted(1, 1, width=width, **kwargs)
        return fig, axarr[0]
    else:
        fig, axarr, cax = faceted(1, 1, width=width,
                                  cbar_mode=cbar_mode, **kwargs)
        return fig, axarr[0], cax

Re-include default aspect argument?

Hi @spencerkclark!

I recently created a fresh conda environment for the first time in quite a few months, and evidently faceted has been updated since then w/ what appears to be a breaking change. aspect no longer is set to the golden ratio by default, and instead one must specify at least two of height, width, and aspect.

I know complaining about breaking changes at such an early stage of a package is a bit of a nuisance, but still I'm wondering if it would be worth reintroducing the default aspect=0.618 setting in the future. Not just for the sake of my legacy code...a really nice feature of faceted for me was being able to just do fig, ax = faceted_ax() or e.g. fig, axarr = faceted(1, 2). I think that simplicity of call signature is something worth retaining in the future. Did the generalizing you've done make that no longer appropriate?

I can workaround this obviously with a simple wrapper to faceted and faceted_ax, but wanted to get your take for the sake of faceted more broadly. Thanks!

Enable use of alternative axes classes

This is important in particular to support plotting maps using cartopy. See this example for how this can be accomplished using AxesGrid.

Note that because AxesGrid uses an alternative (presumably older) version of Axes objects (mpl_toolkits.axes_grid1.axes_divider.LocatableAxes), which have a slightly different API than modern matplotlib.axes.Axes objects, some options on AxesGrid break when used with cartopy.mpl.geoaxes.GeoAxes objects (which are based on modern Axes objects). An important example of this is the label_mode option; see this StackOverflow question for a concrete example.

I'll note here that specifying we want to use modern matplotlib.axes.Axes objects for an axes_class in AxesGrid also leads to a similar error when using label_mode='L':

import matplotlib.pyplot as plt

from matplotlib.axes import Axes
from mpl_toolkits.axes_grid1 import AxesGrid

fig = plt.figure()

axgrid = AxesGrid(fig, 111, axes_class=(Axes, {}),
                  nrows_ncols=(2, 2), 
                  axes_pad=0.3,
                  label_mode='L')
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-12-a699e48a074a> in <module>()
      5                   nrows_ncols=(2, 2),
      6                   axes_pad=0.3,
----> 7                   label_mode='L')

//anaconda/envs/research/lib/python2.7/site-packages/mpl_toolkits/axes_grid1/axes_grid.pyc in __init__(self, fig, rect, nrows_ncols, ngrids, direction, axes_pad, add_all, share_all, aspect, label_mode, cbar_mode, cbar_location, cbar_pad, cbar_size, cbar_set_cax, axes_class)
    613                     ax.cax = cax
    614 
--> 615         self.set_label_mode(label_mode)
    616 
    617     def _update_locators(self):

//anaconda/envs/research/lib/python2.7/site-packages/mpl_toolkits/axes_grid1/axes_grid.pyc in set_label_mode(self, mode)
    380             # left-most axes
    381             for ax in self.axes_column[0][:-1]:
--> 382                 _tick_only(ax, bottom_on=True, left_on=False)
    383             # lower-left axes
    384             ax = self.axes_column[0][-1]

//anaconda/envs/research/lib/python2.7/site-packages/mpl_toolkits/axes_grid1/axes_grid.pyc in _tick_only(ax, bottom_on, left_on)
     31     # ax.xaxis.label.set_visible(bottom_off)
     32     # ax.yaxis.label.set_visible(left_off)
---> 33     ax.axis["bottom"].toggle(ticklabels=bottom_off, label=bottom_off)
     34     ax.axis["left"].toggle(ticklabels=left_off, label=left_off)
     35 

TypeError: 'instancemethod' object has no attribute '__getitem__'

Change cbar_short_side_pad to cbar_length

cbar_short_side_pad is a mouthful of an argument name; it is also somewhat of an odd measure (denoting the distance between the ends of the colorbar and the edges of the plot area or figure area depending on the cbar_mode). In an offline conversation with @nathanieltarshish, it was determined that it may make more sense (when paired with cbar_thickness) to allow the user to specify the length of the colorbar.

It would also be nice if one could specify this measure in either absolute or relative space. For the colorbar thickness (there called cbar_size), this is enabled in AxesGrid by allowing the user to specify either a float (for specifying a measure in absolute space) or string of the form '5%' (for specifying a measure in relative space). We could adopt a similar convention here.

Add support for cbar_mode='edge'

This is the final other colorbar mode that AxesGrid supports. Depending on the cbar_location specified, it either means adding colorbars to end of rows or columns of panels. For example for a plot with 2 rows by 3 columns, if one specified cbar_location='right' and cbar_mode='edge' there would be two colorbars (one at the right end of each row). If one specified cbar_location='bottom' and cbar_mode='edge' there would be three colorbars (one at the bottom end of each column).

Sharing axes

Sharing axes is a really convenient feature of matplotlib. It would be handy if this was enabled in facets. It's not necessarily trivial, but I think it is possible to enable axes sharing on existing axes.

Background

Maybe there are some properties of the axes that might be missing, but simply automatically making inner ticklabels invisible, and sharing tick positions and axes limits would go a long way. In doing some searching, I found this StackOverflow question / answer, which is helpful for sharing axes limits.

Then in doing some reading of the matplotlib source code, namely the following lines: https://github.com/matplotlib/matplotlib/blob/a8ae66ea275b402e3461d69b19d049fc94a34b68/lib/matplotlib/figure.py#L1214-L1226, it seems it shouldn't be too hard to share ticks across axes while keeping some ticklabels visible and others invisible.

Proof of concept

The following is a modified version of the first example in the README (note this works!):

rows, cols = 2, 3
fig, axes = facets(rows, cols, width=8., aspect=0.6,
                   internal_pad=0.2, top_pad=0.5,
                   bottom_pad=0.5, left_pad=0.5, 
                   right_pad=0.5)

x = np.linspace(0., 6. * np.pi)
y = np.sin(x)

# Share ticks and limits of all x and y axes
# Make inner ticklabels invisible
# This could all be done in the facets function
ax_ref = axes[0]
axes = np.reshape(axes, (rows, cols))
for ax in axes.flatten():
    ax.xaxis.major = ax_ref.xaxis.major
    ax.xaxis.minor = ax_ref.xaxis.minor
    ax.yaxis.major = ax_ref.yaxis.major
    ax.yaxis.minor = ax_ref.yaxis.minor
    ax.get_shared_x_axes().join(ax_ref, ax)
    ax.get_shared_y_axes().join(ax_ref, ax)
    
for ax in axes[:-1, :].flatten():
    ax.xaxis.set_tick_params(which='both', 
                             labelbottom=False, labeltop=False)
    
for ax in axes[:, 1:].flatten():
    ax.yaxis.set_tick_params(which='both', 
                             labelbottom=False, labeltop=False)
    
axes = axes.flatten()

# Besides calling facets, this is all the user would need to see
for ax in axes:
    ax.plot(x, y)

axes[0].set_ylim(-2, 2)
axes[0].set_yticks(np.arange(-2, 2.1))
axes[0].set_xticks(np.arange(0., 18.1, 6.))
axes[0].set_xlim([0, 30])

Further considerations

matplotlib has several options for sharing axes in the matplotlib.pyplot.subplots command:

sharex, sharey : bool or {‘none’, ‘all’, ‘row’, ‘col’}, default: False

Controls sharing of properties among x (sharex) or y (sharey) axes:

  • True or ‘all’: x- or y-axis will be shared among all subplots.
  • False or ‘none’: each subplot x- or y-axis will be independent.
  • ‘row’: each subplot row will share an x- or y-axis.
  • ‘col’: each subplot column will share an x- or y-axis.

When subplots have a shared x-axis along a column, only the x tick labels of the bottom subplot are visible. Similarly, when subplots have a shared y-axis along a row, only the y tick labels of the first column subplot are visible.

It would be good to support all of those options from facets via sharex and sharey keyword arguments. (I think the example above would be accomplished through sharex='all' and sharey='all').

Wrap AxesGrid to add new features / fix problem areas

We have identified a number of areas of AxesGrid that we would like to improve upon:

  • More share axes modes (#9)
  • More control over colorbar sizes and axes type (this was implemented in #11)
  • Use of modern matplotlib Axes objects by default, support for alternative Axes types, and support for different label modes using those different Axes types (#12)

Sharing axes and label modes are somewhat linked. It might be good to have them work like they do in plt.subplots (i.e. for axes that are shared, only draw tick labels for panels on the left or bottom edge; for axes that are not shared, draw tick labels for every panel).

It would be useful for #15 to create a separate class that wraps AxesGrid, which implements these changes.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.