Why UltraPlot?
Matplotlib is an extremely versatile 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.
UltraPlot’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. UltraPlot 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.
It seems like there should be a more unified, straightforward way to change
settings without sacrificing the advantages of object-oriented design.
Changes
UltraPlot includes the format() command to resolve this.
Think of this as an expanded and thoroughly documented version of the
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 format()
and 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 ultraplot as uplt
fig, axs = uplt.subplots(ncols=2)
axs.format(color='gray', linewidth=1)
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.edgecolor': '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')
Links
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 frustrating.
Parts of matplotlib’s interface were 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?
Changes
In UltraPlot, 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.
uplt.MultipleLocator(...) or uplt.LogNorm(...) rather than
relying on constructor functions.
The below table lists the constructor functions and the keyword arguments that use them.
Links
For more on axes projections, see this page.
For more on axis locators, see this page.
For more on axis formatters, see this page.
For more on axis scales, see this page.
For more on datetime locators and formatters, see this page.
For more on colormaps, see this page.
For more on normalizers, see this page.
For more on color cycles, see this page.
Automatic dimensions and spacing
Limitation
Matplotlib plots tend to require “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…
The subplot aspect ratio is generally more relevant than the figure aspect ratio. A default aspect ratio of
1is desirable for most plots, and the aspect ratio must be held fixed for geographic and polar projections and mostimshow()plots.The subplot width and height control the “apparent” size of lines, markers, text, and other plotted content. If the figure size is fixed, adding more subplots will decrease the average subplot size and increase the “apparent” sizes. If the subplot size is fixed instead, this can be avoided.
Matplotlib also includes “tight layout”
and “constrained layout”
algorithms that can help users avoid having to tweak
GridSpec spacing parameters like left, bottom, and wspace.
However, these algorithms are disabled by default and somewhat cumbersome to configure.
They also cannot apply different amounts of spacing between different subplot row and
column boundaries.
Changes
By default, UltraPlot fixes the physical dimensions of a reference subplot rather
than the figure. The reference subplot dimensions are controlled with the refwidth,
refheight, and refaspect Figure keywords, with a default
behavior of refaspect=1 and refwidth=2.5 (inches). If the data aspect ratio
of the reference subplot is fixed (as with geographic,
polar, imshow(), and
heatmap() plots) then this is used instead of refaspect.
Alternatively, you can independently specify the width or height of the figure
with the figwidth and figheight parameters. If only one is specified, the
other is adjusted to preserve subplot aspect ratios. This is very often useful
when preparing figures for submission to a publication. To request figure
dimensions suitable for submission to a specific publication,
use the journal keyword.
By default, UltraPlot also uses its own tight layout algorithm –
preventing text labels from overlapping with subplots. This algorithm works with the
GridSpec subclass rather than GridSpec, which
provides the following advantages:
The
GridSpecsubclass interprets spacing parameters with font size-relative units rather than figure size-relative units. This is more consistent with the tight layoutpadarguments (which, like matplotlib, are specified in font size-relative units) and obviates the need to adjust spaces when the figure size or font size changes.The
GridSpecsubclass permits variable spacing between rows and columns, and the tight layout algorithm takes this into account. Variable spacing is critical for making outer colorbars and legends and axes panels without “stealing space” from the parent subplot – these objects usually need to be spaced closer to their parents than other subplots.You can override particular spacing parameters and leave the tight layout algorithm to adjust the unspecified spacing parameters. For example, passing
right=1toadd_subplots()fixes the right margin at 1 font size-width while the others are adjusted automatically.Only one
GridSpecis permitted per figure, considerably simplifying the tight layout algorithm calculations. This restriction is enforced by requiring successiveadd_subplot()calls to imply the same geometry and include only subplot specs generated from the sameGridSpec.
Links
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.
Changes
UltraPlot makes it easier to work with multiple subplots and create clear, concise figures.
Axis tick labels and axis labels are automatically shared and aligned between subplot in the same
GridSpecrow or column. This is controlled by thesharex,sharey,spanx,spany,alignx, andalignyfigure keywords.The figure
colorbar()andlegend`()commands can easily draw colorbars and legends intended to reference more than one subplot in arbitrary contiguous rows and columns. See the next section for details.A-b-c labels can be added to subplots simply using the
rc.abcsetting – for example,uplt.rc['abc'] = 'A.'oraxs.format(abc='A.'). This is possible becauseadd_subplot()assigns a uniquenumber()to every new subplot.The
format()command can easily format multiple subplots at once or add colorbars, legends, panels, twin axes, or inset axes to multiple subplots at once. ASubplotGridis returned bysubplots(), and can be indexed like a list or a 2D array.The
panel_axes()(shorthandpanel()) commands draw thin panels along the edges of subplots. This can be useful for plotting 1D summary statistics alongside 2D plots. You can also add twin axes and panel axes to several subplots at once usingSubplotGridcommands.
Links
Simpler colorbars and legends
Limitation
In matplotlib, it can be difficult to draw legend()s
along the outside of subplots. Generally, you need to position the legend
manually and tweak the spacing to make room for the legend.
Also, colorbar()s 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”).
Changes
UltraPlot includes a simple framework for drawing colorbars and legends that reference individual subplots and multiple contiguous subplots.
To draw a colorbar or legend on the outside of a specific subplot, pass an “outer” location (e.g.
loc='l'orloc='left') tocolorbar()orlegend().To draw a colorbar or legend on the inside of a specific subplot, pass an “inner” location (e.g.
loc='ur'orloc='upper right') tocolorbar()orlegend().To draw a colorbar or legend along the edge of the figure, use
colorbar()andlegend. Thecol,row, andspankeywords control whichGridSpecrows and columns are spanned by the colorbar or legend.
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.
Links
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.
Changes
UltraPlot 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 things easier.
The following features are relevant for “1D” PlotAxes commands
like line() (equivalent to plot())
and scatter():
The treatment of data arguments passed to the 1D
PlotAxescommands is standardized. This makes them more flexible and arguably more intuitive to use than their matplotlib counterparts.The
cyclekeyword is interpreted by theCycleconstructor function and applies property cyclers on-the-fly. This permits succinct and flexible property cycler declaration.The
legendandcolorbarkeywords draw on-the-fly legends and colorbars using the result of thePlotAxescommand. Note that colorbars can be drawn from lists of artists.The default
ylim(xlim) in the presence of a fixedxlim(ylim) is now adjusted to exclude out-of-bounds data. This can be useful when “zooming in” on a dependent variable axis but can be disabled by settingrc['axes.inbounds']toFalseor passinginbounds=FalsetoPlotAxescommands.The
bar()andbarh()commands accept 2D arrays and can stack or group successive columns. Likewise, thearea()andareax()commands (shorthands forfill_between()andfill_betweenx()) accept 2D arrays and can stack or overlay successive columns.The
bar(),barh(),vlines(),hlines(),area(), andareax()commands accept anegposkeyword argument that assigns different colors to “negative” and “positive” regions.The
linex()andscatterx()commands are just likeline()andscatter(), but positional arguments are interpreted as x coordinates or (y, x) pairs. There are also the related commandsstemx(),stepx(),boxh()(shorthand forboxploth()), andviolinh()(shorthand forviolinploth()).The
line(),linex(),scatter(),scatterx(),bar(), andbarh()commands can draw vertical or horizontal error bars or “shading” using a variety of keyword arguments. This is often more convenient than working directly witherrorbar()orfill_between().The
parametric()command draws clean-looking parametric lines by encoding the parametric coordinate using colormap colors rather than text annotations.
The following features are relevant for “2D” PlotAxes commands
like pcolor() and contour():
The treatment of data arguments passed to the 2D
PlotAxescommands is standardized. This makes them more flexible and arguably more intuitive to use than their matplotlib counterparts.The
cmapandnormkeyword arguments are interpreted by theColormapandNormconstructor functions. This permits succinct and flexible colormap and normalizer application.The
colorbarkeyword draws on-the-fly colorbars using the result of the plotting command. Note that “inset” colorbars can also be drawn, analogous to “inset” legends.The
contour(),contourf(),pcolormesh(), andpcolor()commands all accept alabelskeyword. 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
vminandvmaxused to normalize colormaps now excludes data outside the x and y axis boundsxlimandylimif they were explicitly fixed. This can be disabled by settingrc['cmap.inbounds']toFalseor by passinginbounds=FalsetoPlotAxescommands.The
DiscreteNormnormalizer is paired with most colormaps by default. It can easily divide colormaps into distinct levels, similar to contour plots. This can be disabled by settingrc['cmap.discrete']toFalseor by passingdiscrete=FalsetoPlotAxescommands.The
DivergingNormnormalizer is perfect for data with a natural midpoint and offers both “fair” and “unfair” scaling. TheSegmentedNormnormalizer can generate uneven color gradations useful for unusual data distributions.The
heatmap()command invokespcolormesh()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 likecontour()are automatically translated to “centers”. In matplotlib,pcolorsimply truncates and offsets the data when it receives centers.Commands like
pcolor(),contourf()andcolorbar()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 passingedgefix=FalsetoPlotAxescommands.
Links
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.
Changes
UltraPlot 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
backend='basemap' to axes-creation commands or by setting rc['geo.backend'] to
'basemap'. To create a geographic plot, simply pass the PROJ
name to an axes-creation command, e.g. fig, ax = uplt.subplots(proj='pcarree')
or fig.add_subplot(proj='pcarree'). Alternatively, use the
Proj constructor function to quickly generate
a Projection or Basemap instance.
Requesting geographic projections creates a GeoAxes
with unified support for cartopy and basemap features via the
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 format() and format().
The GeoAxes subclass also makes longitude-latitude coordinates
the “default” coordinate system by passing transform=ccrs.PlateCarree()
or latlon=True to PlotAxes 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 PlotAxes commands
like contour() and pcolormesh().
Links
Pandas and xarray integration
Limitation
Scientific data is commonly stored in array-like containers
that include metadata – namely, DataArrays, DataFrames,
and Series. When matplotlib receives these objects, it ignores
the associated metadata. To create plots that are labeled with the metadata,
you must use the plot(), plot(),
and 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.
Changes
UltraPlot reproduces many of the plot(),
plot(), and plot()
features directly on the PlotAxes commands.
This includes grouped or stacked bar plots
and layered or stacked area plots from two-dimensional
input data, auto-detection of diverging datasets for
application of diverging colormaps and normalizers, and
on-the-fly colorbars and legends using colorbar
and legend keywords.
UltraPlot also handles metadata associated with DataArray, DataFrame,
Series, and Quantity objects. When a plotting command receives these
objects, it updates the axis tick labels, axis labels, subplot title, and
colorbar and legend labels from the metadata. For Quantity arrays (including
Quantity those stored inside DataArray containers), a unit string
is generated from the pint.Unit according to the rc.unitformat setting
(note UltraPlot also automatically calls setup_matplotlib()
whenever a Quantity is used for x and y coordinates and removes the
units from z coordinates to avoid the stripped-units warning message).
These features can be disabled by setting rc.autoformat to False
or passing autoformat=False to any plotting command.
Links
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
that would be nice to have in one place.
Matplotlib also “registers” the X11/CSS4 color names, but these are relatively
limited. The more numerous and arguably more intuitive XKCD color survey
names can only be accessed with the 'xkcd:' prefix. As with colormaps, there
are also external projects with useful color names like open color.
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.
However in our opinion, it is not very aesthetically pleasing. It is also
difficult to switch to other fonts on limited systems or systems with fonts
stored in incompatible file formats (see below).
Changes
UltraPlot adds new colormaps, colors, and fonts to help you make more aesthetically pleasing figures.
UltraPlot adds colormaps from the seaborn, cmocean, SciVisColor, and Scientific Colour Maps projects. It also defines a few default perceptually uniform colormaps and includes a
PerceptualColormapclass for generating new ones. A table of colormap and color cycles can be shown usingshow_cmaps()andshow_cycles(). Colormaps like'jet'can still be accessed, but this is discouraged.UltraPlot adds colors from the open color project and adds XKCD color survey names without the
'xkcd:'prefix after filtering them to exclude perceptually-similar colors and normalizing the naming pattern to make them more self-consistent. Old X11/CSS4 colors can still be accessed, but this is discouraged. A table of color names can be shown usingshow_colors().UltraPlot comes packaged with several additional sans-serif fonts and the entire TeX Gyre font series. TeX Gyre consists of open-source fonts designed to resemble more popular, commonly-used fonts like Helvetica and Century. They are used as the new default serif, sans-serif, monospace, cursive, and “fantasy” fonts, and they are available on all workstations. A table of font names can be shown using
show_fonts().
Links
Manipulating colormaps
Limitation
In matplotlib, colormaps are implemented with the
LinearSegmentedColormap class (representing “smooth”
color gradations) and the ListedColormap class (representing
“categorical” color sets). They are somewhat cumbersome to modify or create from
scratch. Meanwhile, property cycles used for individual plot elements are implemented
with the Cycler class. They are easier to modify but they cannot be
“registered” by name like colormaps.
The seaborn package includes “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’s colormap and property cycle constructs.
Changes
UltraPlot tries to make it easy to manipulate colormaps and property cycles.
All colormaps in UltraPlot are replaced with the
ContinuousColormapandDiscreteColormapsubclasses ofLinearSegmentedColormapandListedColormap. These classes include several useful features leveraged by the constructor functionsColormapandCycle.The
Colormapfunction can merge, truncate, and modify existing colormaps or generate brand new colormaps. It can also create newPerceptualColormaps – a type ofContinuousColormapwith linear transitions in the perceptually uniform-like hue, saturation, and luminance channels rather then the red, blue, and green channels.The
Cyclefunction can make property cycles from scratch or retrieve “registered” color cycles from their associatedDiscreteColormapinstances. It can also make property cycles by splitting up the colors from registered or on-the-flyContinuousColormaps andPerceptualColormaps.
UltraPlot 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.
Links
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.
Changes
UltraPlot uses physical units for the GridSpec keywords
left, right, top, bottom, wspace, hspace, pad, outerpad, and
innerpad. The default unit (assumed when a numeric argument is passed) is
em-widths. Em-widths are
particularly appropriate for this context, as plot text can be a useful “ruler”
when figuring out the amount of space you need. UltraPlot also permits arbitrary
string units for these keywords, for the Figure keywords
figsize, figwidth, figheight, refwidth, and refheight, and in a
few other places. This is powered by the physical units engine units().
Acceptable units include inches, centimeters, millimeters,
pixels, points, and picas (a table of acceptable
units is found here). Note the units() engine
also translates rc settings assigned to rc_matplotlib() and
rc_UltraPlot, e.g. rc['subplots.refwidth'],
rc['legend.columnspacing'], and rc['axes.labelpad'].
Links
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.
Changes
In UltraPlot, you can use the rc object to change both native
matplotlib settings (found in rc_matplotlib) and added UltraPlot
settings (found in rc_UltraPlot). Assigned settings are always
validated, and “meta” settings like meta.edgecolor, meta.linewidth, and
font.smallsize can be used to update many settings all at once. Settings can
be changed with uplt.rc.key = value, uplt.rc[key] = value,
uplt.rc.update(key=value), using format(), or using
context(). Settings that have changed during the
python session can be saved to a file with save()
(see changed()), and settings can be loaded from
files with load().
Links
Loading stuff
Limitation
Matplotlib rcParams can be changed persistently by placing
ref: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.
Changes
UltraPlot settings can be changed persistently by editing the default ultraplotrc
file in the location given by user_file() (this is
usually $HOME/.ultraplot/ultraplotrc) or by adding loose ultraplotrc files to
either the current directory or an arbitrary parent directory. Adding files to
parent directories can be useful when working in projects with lots of subfolders.
UltraPlot also automatically registers colormaps, color cycles, colors, and font
files stored in subfolders named cmaps, cycles, colors, and fonts
in the location given by user_folder() (this is usually
$HOME/.ultraplot), as well as loose subfolders named ultraplot_cmaps,
ultraplot_cycles, ultraplot_colors, and ultraplot_fonts in the current
directory or an arbitrary parent directory. You can save colormaps and color cycles to
user_folder() simply by passing save=True to
Colormap and Cycle. To re-register
these files during an active python session, or to register arbitrary input arguments,
you can use register_cmaps(), register_cycles(),
register_colors(), or register_fonts().