Plotting 2D data

ProPlot adds several new features to matplotlib’s plotting commands using the intermediate PlotAxes subclass. For the most part, these additions represent a superset of matplotlib – if you are not interested, you can use the plotting commands just like you always have. This section documents the features added for 2D plotting commands like contour, pcolor, and imshow.

Standardized arguments

Input arguments passed to 2D plotting commands are now uniformly standardized. For each command, you can optionally omit the x and y coordinates, in which case they are inferred from the data (see xarray and pandas integration). If coordinates are string labels, they are converted to indices and tick labels using FixedLocator and IndexFormatter. Coordinate centers passed to commands like pcolor and pcolormesh are automatically converted to edges using edges or edges2d, and coordinate edges passed to commands like contour and contourf are automatically converted to centers (notice the locations of the rectangle edges in the pcolor plots below). All positional arguments can also be optionally specified as keyword arguments (see the individual command documentation).


By default, when ProPlot selects the default colormap normalization range, it ignores data outside the x or y axis limits if they were previously fixed by set_xlim or set_ylim (or, equivalently, by passing xlim or ylim to proplot.axes.CartesianAxes.format). This can be useful if you wish to restrict the view within a large dataset. To disable this feature, pass inbounds=False to the plotting command or set rc['cmap.inbounds'] to False (see also the rc['axes.inbounds'] setting and the user guide).

import proplot as pplt
import numpy as np

# Sample data
state = np.random.RandomState(51423)
x = y = np.array([-10, -5, 0, 5, 10])
xedges = pplt.edges(x)
yedges = pplt.edges(y)
data = state.rand(y.size, x.size)  # "center" coordinates
lim = (np.min(xedges), np.max(xedges))

with pplt.rc.context({'cmap': 'Grays', 'cmap.levels': 21}):
    # Figure
    fig = pplt.figure(refwidth=2.3, share=False)
    axs = fig.subplots(ncols=2, nrows=2)
        xlabel='xlabel', ylabel='ylabel',
        xlim=lim, ylim=lim, xlocator=5, ylocator=5,
        suptitle='Standardized input demonstration',
        toplabels=('Coordinate centers', 'Coordinate edges'),

    # Plot using both centers and edges as coordinates
    axs[0].pcolormesh(x, y, data)
    axs[1].pcolormesh(xedges, yedges, data)
    axs[2].contourf(x, y, data)
    axs[3].contourf(xedges, yedges, data)
import proplot as pplt
import numpy as np

# Sample data
cmap = 'turku_r'
state = np.random.RandomState(51423)
N = 80
x = y = np.arange(N + 1)
data = 10 + (state.normal(0, 3, size=(N, N))).cumsum(axis=0).cumsum(axis=1)
xlim = ylim = (0, 25)

# Plot the data
fig, axs = pplt.subplots(
    [[0, 1, 1, 0], [2, 2, 3, 3]], wratios=(1.3, 1, 1, 1.3), span=False, refwidth=2.2,
    xlim, *ylim, zorder=3, edgecolor='red', facecolor=pplt.set_alpha('red', 0.2),
for i, ax in enumerate(axs):
    inbounds = i == 1
    title = f'Manual limits inbounds={inbounds}'
    title += ' (default)' if inbounds else ''
        xlim=(None if i == 0 else xlim),
        ylim=(None if i == 0 else ylim),
        title=('Auto axis limits' if i == 0 else title),
    ax.pcolor(x, y, data, cmap=cmap, inbounds=inbounds)
    suptitle='Auto cmap normalization with in-bounds data'

Pandas and xarray integration

The PlotAxes plotting commands are seamlessly integrated with pandas and xarray. If you omit x and y coordinates, the plotting command tries to infer them from the pandas.DataFrame or xarray.DataArray. If you did not explicitly set the x or y axis label or legend or colorbar title, the plotting command tries to retrieve them from the pandas.DataFrame or xarray.DataArray. You can also pass a Dataset, DataFrame, or dict to any plotting command using the data keyword, then pass string keys as the data arguments rather than arrays (for example, ax.contour('x', 'y', 'z', data=dataset) is translated to ax.contour(dataset['x'], dataset['y'], dataset['x'])). Finally, if you pass pint.Quantitys or xarray.DataArrays containing pint.Quantitys to a plotting command, ProPlot will automatically call setup_matplotlib and apply the unit string formatted as rc.unitformat for the default content labels.

These features restore some of the convenience you get with the builtin pandas and xarray plotting functions. They are also optional – installation of pandas and xarray are not required. All of these features can be disabled by setting rc.autoformat to False or by passing autoformat=False to any plotting command.

import xarray as xr
import numpy as np
import pandas as pd

# DataArray
state = np.random.RandomState(51423)
linspace = np.linspace(0, np.pi, 20)
data = 50 * state.normal(1, 0.2, size=(20, 20)) * (
    np.sin(linspace * 2) ** 2
    * np.cos(linspace + np.pi / 2)[:, None] ** 2
lat = xr.DataArray(
    np.linspace(-90, 90, 20),
    attrs={'units': '\N{DEGREE SIGN}N'}
plev = xr.DataArray(
    np.linspace(1000, 0, 20),
    attrs={'long_name': 'pressure', 'units': 'hPa'}
da = xr.DataArray(
    dims=('plev', 'lat'),
    coords={'plev': plev, 'lat': lat},
    attrs={'long_name': 'zonal wind', 'units': 'm/s'}

# DataFrame
data = state.rand(12, 20)
df = pd.DataFrame(
    (data - 0.4).cumsum(axis=0).cumsum(axis=1)[::1, ::-1],
    index=pd.date_range('2000-01', '2000-12', freq='MS')
) = 'temperature (\N{DEGREE SIGN}C)' = 'date' = 'variable (units)'
import proplot as pplt
fig = pplt.figure(refwidth=2.5, share=False)
fig.format(suptitle='Automatic subplot formatting')

# Plot DataArray
cmap = pplt.Colormap('PuBu', left=0.05)
ax = fig.subplot(121)
ax.contourf(da, cmap=cmap, colorbar='t', lw=0.7, ec='k')

# Plot DataFrame
ax = fig.subplot(122)
ax.contourf(df, cmap='YlOrRd', colorbar='t', lw=0.7, ec='k')
ax.format(xtickminor=False, yreverse=True, yformatter='%b', ytickminor=False)

Colormaps and normalizers

It is often useful to create ContinuousColormaps on-the-fly, without explicitly calling the Colormap constructor function. You can do so using the cmap and cmap_kw keywords, available with most PlotAxes 2d plotting commands. For example, to create and apply a monochromatic colormap, you can use cmap='color_name' (see the colormaps section for more info). You can also create on-the-fly “qualitative” DiscreteColormaps by passing lists of colors to the keyword c, color, or colors.

In matplotlib, data values are translated into colormap colors using so-called colormap “normalizers”. A normalizer can be selected from its “registered” name using the Norm constructor function. You can also build a normalizer on-the-fly using the norm and norm_kw keywords, again available with most PlotAxes 2d plotting commands. If you want to work with normalizer classes directly, they are also imported into the top-level namespace (e.g., pplt.LogNorm(...) is allowed). To explicitly set the normalization range, you can pass the usual vmin and vmax keywords to the plotting command. See the next section for more details on colormap normalization.

To apply the default sequential, diverging, cyclic, or qualitative colormap to a plot, pass sequential=True, diverging=True, cyclic=True, or qualitative=True to any plotting command. The default colormaps of each type are rc['cmap.sequential'] = 'fire', rc['cmap.diverging'] = 'negpos', rc['cmap.cyclic'] = 'twilight', and rc['cmap.qualitative'] = 'flatui'. Unless otherwise specified, the sequential colormap is used with the default (linear) normalizer when data is strictly positive or negative, and the diverging colormap is used when the data limits or colormap levels cross zero (see below).

import proplot as pplt
import numpy as np

# Sample data
N = 20
state = np.random.RandomState(51423)
data = 11 ** (0.25 * np.cumsum(state.rand(N, N), axis=0))

# Create figure
pplt.rc['cmap.diverging'] = 'IceFire'
pplt.rc['cmap.sequential'] = 'magma'
gs = pplt.GridSpec(ncols=2, nrows=2)
fig = pplt.figure(refwidth=2.3, span=False)

# Different normalizers
ax = fig.subplot(gs[0, 0])
ax.pcolormesh(data, colorbar='b')
ax.format(title='Default normalizer')
ax = fig.subplot(gs[0, 1])
ax.pcolormesh(data, norm='log', colorbar='b')
ax.format(title='Logarithmic normalizer')

# Different colormaps
ax = fig.subplot(gs[1, 0])
    np.log(data) - 4, colorbar='b',
    diverging=True,  # use the default
ax.format(title='Default colormap')
ax = fig.subplot(gs[1, 1])
    np.log(data) - 4, colorbar='b',
    cmap=('cobalt', 'white', 'violet red'),
    cmap_kw={'space': 'hsl', 'cut': 0.15}
ax.format(title='On-the-fly colormap')

# Format figure
fig.format(xlabel='xlabel', ylabel='ylabel', grid=True)
fig.format(suptitle='On-the-fly colormaps and normalizers')

Distinct colormap levels

In ProPlot, when colormaps are applied to plots, the color levels are “discretized” using DiscreteNorm. This converts data values into colors by first (1) transforming the data using an arbitrary continuous normalizer (e.g., Normalize or LogNorm), then (2) mapping the normalized data to distinct color levels. This is similar to matplotlib’s BoundaryNorm, but more flexible. Distinct levels can help readers discern exact numeric values and tend to reveal qualitative structure in the data. They are especially useful for pcolor and pcolormesh plots, analogous to contourf. By default, distinct levels are disabled for imshow, matshow, spy, hexbin, hist2d, and scatter plots. To explicitly toggle it, pass discrete=False or discrete=True to any plotting command that accepts a cmap argument, or change rc['image.discrete'].

DiscreteNorm also repairs the colormap end-colors by ensuring the following conditions are met (this may seem nitpicky, but it is crucial for plots with very few levels):

  1. All colormaps always span the entire color range, independent of the extend setting.

  2. Cyclic colormaps always have distinct color levels on either end of the colorbar.

The colormap levels used with DiscreteNorm can be configured with the levels, values, or N keywords. If you pass an integer, approximately that many boundaries are automatically generated at “nice” intervals. The keywords vmin, vmax, locator, and locator_kw control how the automatic intervals are chosen. You can also use the positive, negative, and symmetric keywords to ensure that automatically-generated levels are strictly positive, strictly negative, or symmetric about zero (respectively). To generate your own level lists, the arange and edges commands may be useful.

import proplot as pplt
import numpy as np

# Sample data
state = np.random.RandomState(51423)
data = 10 + (state.normal(0, 1, size=(33, 33))).cumsum(axis=0).cumsum(axis=1)

# Figure
fig, axs = pplt.subplots([[1, 1, 2, 2], [0, 3, 3, 0]], ref=3, refwidth=2.3)
axs.format(yformatter='none', suptitle='Distinct vs. smooth colormap levels')

# Pcolor
axs[0].pcolor(data, cmap='spectral_r', norm='div', colorbar='l')
axs[0].set_title('Pcolor plot\nDiscreteNorm enabled (default)')
axs[1].pcolor(data, discrete=False, cmap='spectral_r', norm='div', colorbar='r')
axs[1].set_title('Pcolor plot\nDiscreteNorm disabled')

# Imshow
data = 100 - data
m = axs[2].imshow(data, cmap='thermal', colorbar='b')
axs[2].format(title='Imshow plot\nDiscreteNorm disabled (default)', yformatter='auto')
import proplot as pplt
import numpy as np

# Sample data
state = np.random.RandomState(51423)
data = (20 * (state.rand(20, 20) - 0.4).cumsum(axis=0).cumsum(axis=1)) % 360
levels = pplt.arange(0, 360, 45)

# Figure
gs = pplt.GridSpec(nrows=2, ncols=4, hratios=(1.5, 1))
fig = pplt.figure(refwidth=2.4, right=2)
fig.format(suptitle='DiscreteNorm end-color standardization')

# Cyclic colorbar with distinct end colors
ax = fig.subplot(gs[0, 1:3])
    data, levels=levels, cmap='phase', extend='neither',
    colorbar='b', colorbar_kw={'locator': 90}
ax.format(title='distinct "cyclic" end colors')

# Colorbars with different extend values
for i, extend in enumerate(('min', 'max', 'neither', 'both')):
    ax = fig.subplot(gs[1, i])
        data[:, :10], levels=levels, cmap='oxy',
        extend=extend, colorbar='b', colorbar_kw={'locator': 180}

Auto colormap normalization

By default, colormaps are normalized to span from roughly the minimum data value to the maximum data value. However in the presence of outliers, this is not desirable. ProPlot adds the robust option to change this behavior, inspired by the xarray option of the same name. Passing robust=True to a PlotAxes 2d plotting command will limit the default colormap normalization between the 2nd and 98th data percentiles. This range can be customized by passing an integer to robust (e.g. robust=90 limits the normalization range between the 5th and 95th percentiles) or by passing a 2-tuple to robust (e.g. robust=(0, 90) will limit the normalization range between the data minimum and the 90th percentile). This can be turned on persistently by setting rc['cmap.robust'] to True.

A related xarray feature is the automatic detection of “diverging” datasets. ProPlot automatically applies the default diverging colormap rc['cmap.diverging'] = 'negpos' (rather than the default sequential colormap rc['cmap.sequential'] = 'fire') along with the default continuous normalizer DivergingNorm (see below) if the following conditions are met:

  1. The colormap was not passed, or the colormap was passed but its name matches the name of a known diverging colormap.

  2. If discrete=True (see above) and the discrete colormap levels include at least 2 positive values and 2 negative values.

  3. If discrete=False (see above) and the normalization limits vmin and vmax have opposite signs.

The automatic detection of diverging datasets can be disabled by setting rc['cmap.autodiverging'] to False.

import proplot as pplt
import numpy as np
N = 20
state = np.random.RandomState(51423)
data = N * 2 + (state.rand(N, N) - 0.45).cumsum(axis=0).cumsum(axis=1) * 10
fig, axs = pplt.subplots(nrows=2, ncols=2, refwidth=2)
fig.format(suptitle='Auto normalization demo')

# Auto diverging
pplt.rc['cmap.sequential'] = 'lapaz_r'
pplt.rc['cmap.diverging'] = 'vik'
for i, ax in enumerate(axs[:2]):
    ax.pcolor(data - i * N * 5, colorbar='b')
    ax.format(title='Diverging ' + ('on' if i else 'off'))

# Auto range
pplt.rc['cmap.sequential'] = 'lajolla'
data = data[::-1, :]
data[-1, 0] = 1e3
for i, ax in enumerate(axs[2:]):
    ax.pcolor(data, robust=bool(i), colorbar='b')
    ax.format(title='Robust ' + ('on' if i else 'off'))

Special colormap normalizers

ProPlot includes a few new colormap normalizers. SegmentedNorm provides even color gradations with respect to index for an arbitrary monotonically increasing or decreasing list of levels. This is automatically applied if you pass unevenly spaced levels to a plotting command, or it can be manually applied using e.g. norm='segmented'. This can be useful for datasets with unusual statistical distributions or spanning a wide range of magnitudes.

The DivergingNorm normalizer ensures the colormap midpoint lies on some central data value (usually 0), even if vmin, vmax, or levels are asymmetric with respect to the central value. This is automatically applied if you don’t explicitly specify an unknown or non-diverging colormap and your data contains both negative and positive values, or it can be manually applied using e.g. norm='diverging'. It can also be configured to scale colors “fairly” or “unfairly”:

  • With fair scaling (the default), gradations on either side of the midpoint have equal intensity. If vmin and vmax are not symmetric about zero, the most intense colormap colors on one side of the midpoint will be truncated.

  • With unfair scaling, gradations on either side of the midpoint are warped so that the full range of colormap colors is always traversed. This configuration should be used with care, as it may lead you to misinterpret your data.

The below examples demonstrate how these normalizers affect the interpretation of colormap plots.

import proplot as pplt
import numpy as np

# Sample data
state = np.random.RandomState(51423)
data = 11 ** (2 * state.rand(20, 20).cumsum(axis=0) / 7)

# Linear segmented norm
fig, axs = pplt.subplots(ncols=2, refwidth=2.4)
fig.format(suptitle='Segmented normalizer demo')
ticks = [5, 10, 20, 50, 100, 200, 500, 1000]
for ax, norm in zip(axs, ('linear', 'segmented')):
    m = ax.contourf(
        data, levels=ticks, extend='both',
        cmap='Mako', norm=norm,
        colorbar='b', colorbar_kw={'ticks': ticks},
    ax.format(title=norm.title() + ' normalizer')
import proplot as pplt
import numpy as np

# Sample data
state = np.random.RandomState(51423)
data1 = (state.rand(20, 20) - 0.485).cumsum(axis=1).cumsum(axis=0)
data2 = (state.rand(20, 20) - 0.515).cumsum(axis=0).cumsum(axis=1)

# Figure
fig, axs = pplt.subplots(nrows=2, ncols=2, refwidth=2.2, order='F')
axs.format(suptitle='Diverging normalizer demo')
cmap = pplt.Colormap('DryWet', cut=0.1)

# Diverging norms
i = 0
for data, mode, fair in zip(
    (data1, data2), ('positive', 'negative'), ('fair', 'unfair'),
    for fair in ('fair', 'unfair'):
        norm = pplt.Norm('diverging', fair=(fair == 'fair'))
        ax = axs[i]
        m = ax.contourf(data, cmap=cmap, norm=norm)
        ax.colorbar(m, loc='b')
        ax.format(title=f'{mode.title()}-skewed + {fair} scaling')
        i += 1

Contour and gridbox labels

You can now quickly add labels to contour, contourf, pcolor, pcolormesh, and heatmap, plots by passing labels=True to the plotting command. The label text is colored black or white depending on the luminance of the underlying grid box or filled contour (see the section on colorspaces). Contour labels are drawn with clabel and grid box labels are drawn with text. You can pass keyword arguments to these functions by passing a dictionary to labels_kw, and you can change the label precision using the precision keyword. See the plotting command documentation for details.

import proplot as pplt
import pandas as pd
import numpy as np

# Sample data
state = np.random.RandomState(51423)
data = state.rand(6, 6)
data = pd.DataFrame(data, index=pd.Index(['a', 'b', 'c', 'd', 'e', 'f']))

# Figure
fig, axs = pplt.subplots(
    [[1, 1, 2, 2], [0, 3, 3, 0]],
    refwidth=2.3, share='labels', span=False,
axs.format(xlabel='xlabel', ylabel='ylabel', suptitle='Labels demo')

# Heatmap with labeled boxes
ax = axs[0]
m = ax.heatmap(
    data, cmap='rocket', labels=True,
    precision=2, labels_kw={'weight': 'bold'}
ax.format(title='Heatmap with labels')

# Filled contours with labels
ax = axs[1]
m = ax.contourf(
    data.cumsum(axis=0), labels=True,
    cmap='rocket', labels_kw={'weight': 'bold'}
ax.format(title='Filled contours with labels')

# Line contours with labels
ax = axs[2]
    data.cumsum(axis=1) - 2, labels=True,
    color='gray8', labels_kw={'weight': 'bold'}
ax.format(title='Line contours with labels')

Heatmap plots

The heatmap command can be used to draw “heatmaps” of 2-dimensional data. This is a convenience function equivalent to pcolormesh, except the axes are configured with settings suitable for heatmaps: fixed aspect ratios (ensuring “square” grid boxes), no gridlines, no minor ticks, and major ticks at the center of each box. Among other things, this is useful for displaying covariance and correlation matrices, as shown below. heatmap should generally only be used with CartesianAxes.

import proplot as pplt
import numpy as np
import pandas as pd

# Covariance data
state = np.random.RandomState(51423)
data = state.normal(size=(10, 10)).cumsum(axis=0)
data = (data - data.mean(axis=0)) / data.std(axis=0)
data = (data.T @ data) / data.shape[0]
data[np.tril_indices(data.shape[0], -1)] = np.nan  # fill half with empty boxes
data = pd.DataFrame(data, columns=list('abcdefghij'), index=list('abcdefghij'))

# Covariance matrix plot
fig, ax = pplt.subplots(refwidth=4.5)
m = ax.heatmap(
    data, cmap='ColdHot', vmin=-1, vmax=1, N=100, lw=0.5, ec='k',
    labels=True, precision=2, labels_kw={'weight': 'bold'},
    clip_on=False,  # turn off clipping so box edges are not cut in half
    suptitle='Heatmap demo', title='Table of correlation coefficients',
    xloc='top', yloc='right', yreverse=True, ticklabelweight='bold',
    alpha=0, linewidth=0, tickpad=4,