Why ProPlot?

Matplotlib is an extremely powerful plotting package used by scientists and engineers far and wide. However, matplotlib can be cumbersome or repetitive for users who…

  • Make highly complex figures with many subplots.

  • Want to finely tune their annotations and aesthetics.

  • Need to make new figures nearly every day.

ProPlot’s core mission is to provide a smoother plotting experience for matplotlib’s most demanding users. We accomplish this by expanding upon matplotlib’s object-oriented interface. ProPlot makes changes that would be hard to justify or difficult to incorporate into matplotlib itself, owing to differing design choices and backwards compatibility considerations.

This page enumerates these changes and explains how they address the limitations of matplotlib’s default interface. To start using these features, see the usage introduction and the user guide.

Less typing, more plotting

Limitation

Matplotlib users often need to change lots of plot settings all at once. With the default interface, this requires calling a series of one-liner setter methods.

This workflow is quite verbose – it tends to require “boilerplate code” that gets copied and pasted a hundred times. It can also be confusing – it is often unclear whether properties are applied from an Axes setter (e.g. set_xlabel and set_xticks), an XAxis or YAxis setter (e.g. set_major_locator and set_major_formatter), a Spine setter (e.g. set_bounds), or a “bulk” property setter (e.g. tick_params), or whether one must dig into the figure architecture and apply settings to several different objects. While this is in the spirit of object-oriented design, it seems like there should be a more unified, straightforward way to change settings for day-to-day matplotlib usage.

Solution

ProPlot introduces the proplot.axes.Axes.format command to resolve this. Think of this as an expanded and thoroughly documented version of the matplotlib.artist.Artist.update command. format can modify things like axis labels and titles and apply new “rc” settings to existing axes. It also integrates with various constructor functions to help keep things succinct. Further, the proplot.figure.Figure.format and proplot.figure.SubplotGrid.format commands can be used to format several subplots at once.

Together, these features significantly reduce the amount of code needed to create highly customized figures. As an example, it is trivial to see that…

import proplot as pplt
fig, axs = pplt.subplots(ncols=2)
axs.format(linewidth=1, color='gray')
axs.format(xlim=(0, 100), xticks=10, xtickminor=True, xlabel='foo', ylabel='bar')

is much more succinct than…

import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
import matplotlib as mpl
with mpl.rc_context(rc={'axes.linewidth': 1, 'axes.color': 'gray'}):
    fig, axs = plt.subplots(ncols=2, sharey=True)
    axs[0].set_ylabel('bar', color='gray')
    for ax in axs:
        ax.set_xlim(0, 100)
        ax.xaxis.set_major_locator(mticker.MultipleLocator(10))
        ax.tick_params(width=1, color='gray', labelcolor='gray')
        ax.tick_params(axis='x', which='minor', bottom=True)
        ax.set_xlabel('foo', color='gray')

Class constructor functions

Limitation

Matplotlib and cartopy define several classes with verbose names like MultipleLocator, FormatStrFormatter, and LambertAzimuthalEqualArea. They also keep them out of the top-level package namespace. Since plotting code has a half life of about 30 seconds, typing out these extra class names and import statements can be a major drag.

Parts of matplotlib’s interface were actually designed with this in mind. Backend classes, native axes projections, axis scales, colormaps, box styles, arrow styles, and arc styles are referenced with “registered” string names, as are basemap projections. So, why not “register” everything else?

Solution

In ProPlot, tick locators, tick formatters, axis scales, property cycles, colormaps, normalizers, and cartopy projections are all “registered”. This is accomplished by defining “constructor functions” and passing various keyword arguments through these functions.

The constructor functions also accept intuitive inputs alongside “registered” names. For example, a scalar passed to Locator returns a MultipleLocator, a lists of strings passed to Formatter returns a FixedFormatter, and Cycle and Colormap accept colormap names, individual colors, and lists of colors. Passing the relevant class instance to a constructor function simply returns it, and all the registered classes are available in the top-level namespace – so class instances can be directly created with e.g. pplt.MultipleLocator(...) or pplt.LogNorm(...) rather than relying on constructor functions.

For details, see the user guide sections on Cartesian plots, color cycles and colormaps. The below table lists the constructor functions and the keyword arguments that use them. Note that set_xscale and set_yscale accept instances of ScaleBase thanks to a patch applied by ProPlot.

Function

Return type

Used by

Keyword argument(s)

Proj

Projection or Basemap

subplots

proj=

Locator

Locator

format and colorbar

locator=, xlocator=, ylocator=, minorlocator=, xminorlocator=, yminorlocator=, ticks=, xticks=, yticks=, minorticks=, xminorticks=, yminorticks=

Formatter

Formatter

format and colorbar

formatter=, xformatter=, yformatter=, ticklabels=, xticklabels=, yticklabels=

Scale

ScaleBase

format

xscale=, yscale=

Colormap

Colormap

2D plotting commands

cmap=

Norm

Normalize

2D plotting commands

norm=

Cycle

Cycler

1D plotting commands

cycle=

Automatic dimensions and spacing

Limitation

Matplotlib plots tend to require lots of “tweaking” when you have more than one subplot in the figure. This is partly because you must specify the physical dimensions of the figure, despite the fact that…

  1. The subplot aspect ratio is generally more relevant than the figure aspect ratio. An aspect ratio of 1 is desirable for most plots, and the aspect ratio must be held fixed for geographic and polar projections and most imshow plots.

  2. The physical width and height of the subplot controls the “evident” thickness of text, lines, and other content plotted inside the subplot. The effect of the figure size on this “evident” thickness depends on the number of subplot tiles in the figure.

Also, while matplotlib’s tight layout algorithm can help you avoid tweaking the spacing, the algorithm cannot apply different amounts of spacing between different subplot row and column boundaries.

Solution

In ProPlot, you can specify the physical dimensions of a reference subplot by passing refwidth, refheight, and/or refaspect to figure or subplots. The dimensions of the figure are calculated automatically. 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 geographic, polar, imshow, and heatmap plots, the or data aspect ratio is used instead.

You can also independently specify the width or height of the figure with the figwidth and figheight parameters. If only one is specified, the other will be calculated to preserve subplot aspect ratios. You can also select a figwidth and/or figheight suitable for submission to various publications using the journal parameter.

ProPlot also uses its own tight layout algorithm to automatically determine the left, right, bottom, top, wspace, and hspace spacing parameters. This algorithm has the following advantages:

See the user guide for details.

Working with multiple subplots

Limitation

When working with multiple subplots in matplotlib, the path of least resistance often leads to redundant figure elements. Namely…

  • Repeated axis tick labels.

  • Repeated axis labels.

  • Repeated colorbars.

  • Repeated legends.

These sorts of redundancies are very common even in publications, where they waste valuable page space. It is also generally necessary to add “a-b-c” labels to figures with multiple subplots before submitting them to publications, but matplotlib has no built-in way of doing this.

Solution

ProPlot makes it easier to work with multiple subplots and create clear, concise figures.

Simpler colorbars and legends

Limitation

In matplotlib, it can be difficult to draw legends along the outside of subplots. Generally, you need to position the legend manually and tweak the spacing to make room for the legend.

Also, colorbars drawn along the outside of subplots with e.g. fig.colorbar(..., ax=ax) need to “steal” space from the parent subplot. This can cause asymmetry in figures with more than one subplot. It is also generally difficult to draw “inset” colorbars in matplotlib and to generate outer colorbars with consistent widths (i.e., not too “skinny” or “fat”).

Solution

ProPlot includes a simple framework for drawing colorbars and legends that reference individual subplots and multiple contiguous subplots.

Since GridSpec permits variable spacing between subplot rows and columns, “outer” colorbars and legends do not alter subplot spacing or add whitespace. This is critical e.g. if you have a colorbar between columns 1 and 2 but nothing between columns 2 and 3. Also, Figure and Axes colorbar widths are now specified in physical units rather than relative units, which makes colorbar thickness independent of subplot size and easier to get just right.

There are also several useful new colorbar and legend features described in the user guide.

Improved plotting commands

Limitation

A few common plotting tasks take a lot of work using matplotlib alone. The seaborn, xarray, and pandas packages offer improvements, but it would be nice to have this functionality built right into matplotlib’s interface.

Solution

ProPlot uses the PlotAxes subclass to add various seaborn, xarray, and pandas features to existing matplotlib plotting commands along with several additional features designed to make your life easier.

The following features are relevant for the 1D plotting commands like line (equivalent to plot) and scatter:

The following features are relevant for the 2D plotting commands like pcolor and contour:

  • The cmap and norm keyword arguments are interpreted by the Colormap and Norm constructor functions. This permits succinct and flexible colormap and normalizer application.

  • The colorbar keyword draws on-the-fly colorbars using the result of the plotting command. Note that “inset” colorbars can also be drawn, analogous to “inset” legends (see colorbar).

  • The contour, contourf, pcolormesh, and pcolor commands all accept a labels keyword. This draws contour and grid box labels on-the-fly. Labels are automatically colored black or white according to the luminance of the underlying grid box or filled contour.

  • The default vmin and vmax used to normalize colormaps now excludes data outside the x and y axis bounds xlim and ylim if they were explicitly fixed. This can be disabled by setting rc['image.inbounds'] to False or by passing inbounds=False to plot commands.

  • The DiscreteNorm normalizer is paired with most colormaps by default. It can easily divide colormaps into distinct levels, similar to contour plots. This can be disabled by setting rc['image.discrete'] to False or by passing discrete=False to plot commands.

  • The DivergingNorm normalizer is perfect for data with a natural midpoint and offers both “fair” and “unfair” scaling. The SegmentedNorm normalizer can generate uneven color gradations useful for unusual data distributions.

  • The heatmap command invokes pcolormesh then applies an equal axes apect ratio, adds ticks to the center of each gridbox, and disables minor ticks and gridlines. This can be convenient for things like covariance matrices.

  • Coordinate centers passed to commands like pcolor are automatically translated to “edges”, and coordinate edges passed to commands like contour are automatically translated to “centers”. In matplotlib, pcolor simply truncates the data when it receives centers.

  • Commands like pcolor, contourf and colorbar automatically fix an irritating issue where saved vector graphics appear to have thin white lines between filled contours, grid boxes, and colorbar segments. This can be disabled by passing fixedges=False to plot commands.

Cartopy and basemap integration

Limitation

There are two widely-used engines for working with geographic data in matplotlib: cartopy and basemap. Using cartopy tends to be verbose and involve boilerplate code, while using basemap requires plotting with a separate Basemap object rather than the Axes. They both require separate import statements and extra lines of code to configure the projection.

Furthermore, when you use cartopy and basemap plotting commands, “map projection” coordinates are the default coordinate system rather than longitude-latitude coordinates. This choice is confusing for many users, since the vast majority of geophysical data are stored with longitude-latitude (i.e., “Plate Carrée”) coordinates.

Solution

ProPlot can succinctly create detailed geographic plots using either cartopy or basemap as “backends”. By default, cartopy is used, but basemap can be used by passing basemap=True to axes-creation commands or by setting rc.basemap to True. To create a geographic plot, simply pass the PROJ name to an axes-creation command, e.g. fig, ax = pplt.subplots(proj='pcarree') or fig.add_subplot(proj='pcarree'). Alternatively, use the Proj constructor function to quickly generate a cartopy.crs.Projection or Basemap instance.

Requesting geographic projections creates a proplot.axes.GeoAxes with unified support for cartopy and basemap features via the proplot.axes.GeoAxes.format command. This lets you quickly modify geographic plot features like latitude and longitude gridlines, gridline labels, continents, coastlines, and political boundaries. The syntax is conveniently analogous to the syntax used for proplot.axes.CartesianAxes.format and proplot.axes.PolarAxes.format.

The GeoAxes subclass also makes longitude-latitude coordinates the “default” coordinate system by passing transform=ccrs.PlateCarree() or latlon=True to plotting commands (depending on whether cartopy or basemap is the backend). And to enforce global coverage over the poles and across longitude seams, you can pass globe=True to 2D plotting commands like contour and pcolormesh.

See the user guide for details.

Pandas and xarray integration

Limitation

Scientific data is commonly stored in array-like containers that include metadata – namely, xarray.DataArrays, pandas.DataFrames, and pandas.Series. When matplotlib receives these objects, it simply ignores the associated metadata. To create plots that are labeled with the metadata, you must use the xarray.DataArray.plot, pandas.DataFrame.plot, and pandas.Series.plot commands instead.

This approach is fine for quick plots, but not ideal for complex ones. It requires learning a different syntax from matplotlib, and tends to encourage using the pyplot interface rather than the object-oriented interface. The plot commands also include features that would be useful additions to matplotlib in their own right, without requiring special containers and a separate interface.

Solution

ProPlot reproduces many of the xarray.DataArray.plot, pandas.DataFrame.plot, and pandas.Series.plot features on the Axes plotting commands themselves. Passing a DataArray, DataFrame, or Series through any plotting command updates the axis tick labels, axis labels, subplot title, and colorbar and legend labels from the metadata. This feature can be disabled by setting rc.autoformat to False or passing autoformat=False to any plotting command.

ProPlot also supports pint.Quantity positional arguments by auto-calling setup_matplotlib when a pint.Quantity is detected and by extracting magnitudes from z coordinates (e.g., the data passed to contour) to avoid the stripped-units warning message. It also adds a unit string formatted with rc.unitformat as the default x and y axis label when rc.autoformat is enabled and supports DataArray containers with pint.Quantity arrays.

Finally, as described above, ProPlot implements features that were originally only available from the xarray.DataArray.plot, pandas.DataFrame.plot, and pandas.Series.plot commands – like grouped bar plots, layered area plots, and on-the-fly colorbars and legends – directly within the Axes plotting commands.

Aesthetic colors and fonts

Limitation

A common problem with scientific visualizations is the use of “misleading” colormaps like 'jet'. These colormaps have jarring jumps in hue, saturation, and luminance that can trick the human eye into seeing non-existing patterns. It is important to use “perceptually uniform” colormaps instead. Matplotlib comes packaged with a few of its own, plus the ColorBrewer colormap series, but external projects offer a larger variety of aesthetically pleasing “perceptually uniform” colormaps.

Matplotlib also “registers” the X11/CSS4 color names, but these are relatively limited. The more intuitive and more numerous XKCD color survey names can be accessed with the 'xkcd:' prefix, but this is cumbersome, and external projects like open color offer even more useful names.

Finally, matplotlib comes packaged with DejaVu Sans as the default font. This font is open source and include glyphs for a huge variety of characters, but unfortunately (in our opinion) it is not very aesthetically pleasing. It can also be difficult to change the default matplotlib font.

Solution

ProPlot adds new colormaps, colors, and fonts to help you make more aesthetically pleasing figures.

For details on adding new colormaps, colors, and fonts, see the .proplot folder section.

Manipulating colormaps and cycles

Limitation

In matplotlib, colormaps are implemented with the LinearSegmentedColormap class (representing “smooth” color gradations) and the ListedColormap class (representing “categorical” color sets). They are generally cumbersome to modify or create from scratch. Meanwhile, property cycles used for individual plot elements are implemented with the Cycler class. They are also cumbersome to modify and they cannot be “registered” by name like colormaps.

The seaborn package introduces “color palettes” to make working with colormaps and property cycles easier, but it would be nice to have similar features integrated more closely with matplotlib.

Solution

In ProPlot, it is easy to manipulate colormaps and property cycles.

ProPlot also makes all colormap and color cycle names case-insensitive, and colormaps are automatically reversed or cyclically shifted 180 degrees if you append '_r' or '_s' to any colormap name. These features are powered by ColormapDatabase, which replaces matplotlib’s native colormap database.

Physical units engine

Limitation

Matplotlib uses figure-relative units for the margins left, right, bottom, and top, and axes-relative units for the column and row spacing wspace and hspace. Relative units tend to require “tinkering” with numbers until you find the right one. And since they are relative, if you decide to change your figure size or add a subplot, they will have to be readjusted.

Matplotlib also requires users to set the figure size figsize in inches. This may be confusing for users outside of the United States.

Solution

ProPlot introduces the physical units engine units for interpreting figsize, figwidth, figheight, refwidth, refheight, left, right, top, bottom, wspace, hspace, and keyword arguments in a few other places. Acceptable units include inches, centimeters, millimeters, pixels, points, picas, and em-heights (a table of acceptable units is found here). Em-heights are particularly useful, as the figure text can be a useful “ruler” when figuring out the amount of space you need. The units function also translates rc settings assigned to rc_matplotlib and rc_proplot, e.g. rc['axes.labelpad'], rc['legend.handlelength'], and rc['subplot.refwidth']. See the user guide for details.

Flexible global settings

Limitation

In matplotlib, there are several rcParams that would be useful to set all at once, like spine and label colors. It might also be useful to change these settings for individual subplots rather than globally.

Solution

In ProPlot, you can use the rc object to change both native matplotlib settings (found in rc_matplotlib) and added ProPlot settings (found in rc_proplot). Assigned settings are always validated, and special settings like meta.edgecolor, meta.linewidth, and font.smallsize can be used to update many settings all at once. Settings can be changed with pplt.rc.key = value, pplt.rc[key] = value, pplt.rc.update(key=value), using proplot.axes.Axes.format, or using proplot.config.Configurator.context. Settings that have changed during the python session can be saved to a file with proplot.config.Configurator.save (see changed), and settings can be loaded from files with proplot.config.Configurator.load. See the user guide for details.

Loading saved settings

Limitation

Matplotlib rcParams can be changed persistently by placing matplotlibrc files in the same directory as your python script. But it can be difficult to design and store your own colormaps and color cycles for future use. It is also difficult to get matplotlib to use custom .ttf and .otf font files, which may be desirable when you are working on Linux servers with limited font selections.

Solution

ProPlot settings can be changed persistently by editing the default proplotrc file in the location given by user_file (this is usually $HOME/.proplot/proplotrc) or by adding proplotrc files to either the current directory or any parent directory. Adding files to parent directories can be useful when working in projects with lots of subfolders. See the user guide for details.

ProPlot also automatically registers colormaps, color cycles, colors, and font files stored in the cmaps, cycles, colors, and fonts folders in the location given by user_folder (this is usually $HOME/.proplot). You can save colormaps and color cycles to these folders simply by passing save=True to Colormap and Cycle. To explicitly register objects stored in these folders, or to register arbitrary input arguments, you can use register_cmaps, register_cycles, register_colors, or register_fonts.