2d plotting

ProPlot adds new features to various Axes plotting methods thanks to a set of wrapper functions. These features are a strict superset of the existing matplotlib API – if you want, you can use plotting commands exactly as you always have.

Standardized input

The standardize_2d wrapper is used to standardize the positional arguments for several different plotting functions. It allows you to optionally omit x and y coordinates, in which case they are inffered from the data array; guesses graticule edges for pcolor and pcolormesh plots; and optionally enforces global data coverage when plotting in map projections.

Pandas and xarray

The standardize_2d wrapper also supports integration with pandas DataFrames and xarray DataArrays. When you pass a DataFrame or DataArray to any plotting command, the x-axis label, y-axis label, legend label, colorbar label, and/or title are configured from the metadata. This restores some of the convenience you get with the builtin pandas and xarray plotting functions. This feature is optional; installation of pandas and xarray are not required.

import xarray as xr
import numpy as np
import pandas as pd
from string import ascii_lowercase

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

# DataFrame
data = state.rand(20, 20)
df = pd.DataFrame(
df.name = 'temporal data'
df.index.name = 'index'
df.columns.name = 'time (days)'
import proplot as plot
f, axs = plot.subplots(nrows=2, axwidth=2.2, share=0)
axs.format(collabels=['Automatic subplot formatting'])

# Plot DataArray
    da, cmap='Greens', cmap_kw={'left': 0.05}, colorbar='l', linewidth=0.7, color='gray7'

# Plot DataFrame
    df, cmap='Blues', colorbar='r', linewidth=0.7, color='gray7'
/home/docs/checkouts/readthedocs.org/user_builds/proplot/conda/v0.4.2/lib/python3.7/site-packages/proplot/utils.py:105: ProPlotWarning: Rebuilding font cache.

Discrete colormap levels

cmap_changer uses the BinNorm “meta normalizer” for all plotting commands involving colormaps. The BinNorm normalizer first transforms data using any arbitrary continuous normalizer, e.g. LogNorm, then maps the normalized data to discrete colormap levels.

This permits distinct levels even for commands like pcolor and pcolormesh. Distinct levels can help the reader discern exact numeric values and tends to reveal qualitative structure in the figure. Distinct levels are also critical for users that would prefer contours, but have complex 2D coordinate matrices that trip up the contouring algorithm.

BinNorm also fixes your 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.

import proplot as plot
import numpy as np

# Pcolor plot with and without distinct levels
f, axs = plot.subplots(ncols=2, axwidth=2)
state = np.random.RandomState(51423)
data = (state.normal(0, 1, size=(33, 33))).cumsum(axis=0).cumsum(axis=1)
axs.format(suptitle='Pcolor with levels demo')
for ax, n, mode, side in zip(axs, (200, 10), ('Ambiguous', 'Discernible'), 'lr'):
    ax.pcolor(data, cmap='spectral', N=n, symmetric=True, colorbar=side)
    ax.format(title=f'{mode} level boundaries', yformatter='null')
import proplot as plot
import numpy as np
f, axs = plot.subplots(
    [[0, 0, 1, 1, 0, 0], [2, 3, 3, 4, 4, 5]],
    wratios=(1.5, 0.5, 1, 1, 0.5, 1.5), axwidth=1.7, ref=1, right='2em'
axs.format(suptitle='Demo of colorbar color-range standardization')
levels = plot.arange(0, 360, 45)
state = np.random.RandomState(51423)
data = (20*(state.rand(20, 20) - 0.4).cumsum(axis=0).cumsum(axis=1)) % 360

# Cyclic colorbar with distinct end colors
ax = axs[0]
    data, levels=levels, cmap='phase', extend='neither',
    colorbar='b', colorbar_kw={'locator': 90}
ax.format(title='cyclic colormap\nwith distinct end colors')

# Colorbars with different extend values
for ax, extend in zip(axs[1:], ('min', 'max', 'neither', 'both')):
        data[:, :10], levels=levels, cmap='oxy',
        extend=extend, colorbar='b', colorbar_kw={'locator': 90}

Colormap normalization

To change the colormap normalizer in ProPlot, just pass a string name or Normalize instance with the norm keyword arg to any command wrapped by cmap_changer. norm and norm_kw are interpreted by the Norm constructor.

ProPlot also introduces the following new Normalize classes:

  • LinearSegmentedNorm provides even color gradations with respect to index for arbitrary monotonically increasing level lists. This is applied by default if you pass unevenly spaced levels to a plotting command.

  • MidpointNorm is similar to matplotlib’s DivergingNorm. It warps your values so that 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.

import proplot as plot
import numpy as np

# Linear segmented norm
state = np.random.RandomState(51423)
data = 10**(2*state.rand(20, 20).cumsum(axis=0)/7)
f, axs = plot.subplots(ncols=2, axwidth=2.5, aspect=1.5)
ticks = [5, 10, 20, 50, 100, 200, 500, 1000]
for i, (norm, title) in enumerate(zip(('linear', 'segments'), ('Linear normalizer', 'LinearSegmentedNorm'))):
    m = axs[i].contourf(
        data, levels=ticks, extend='both',
        cmap='Mako', norm=norm, colorbar='b'
axs.format(suptitle='Level normalizers demo')

# Midpoint norm
data1 = (state.rand(20, 20) - 0.43).cumsum(axis=0)
data2 = (state.rand(20, 20) - 0.57).cumsum(axis=0)
f, axs = plot.subplots(ncols=2, axwidth=2.5, aspect=1.5)
cmap = plot.Colormap('DryWet', cut=0.1)
axs.format(suptitle='Midpoint normalizer demo')
for ax, data, mode in zip(axs, (data1, data2), ('positive', 'negative')):
    m = ax.contourf(data, norm='midpoint', cmap=cmap)
    ax.colorbar(m, loc='b', locator=1, minorlocator=0.25)
    ax.format(title=f'Skewed {mode} data')

Pcolor and contour labels

Thanks to cmap_changer, you can now add labels to heatmap, pcolor, pcolormesh, contour, and contourf plots by simply passing labels=True. ProPlot draws contour labels with clabel or grid box labels with text. Label colors are automatically chosen based on the luminance of the underlying box or contour color. The label text objects can be changed with the labels_kw dictionary keyword arg and the precision keyword arg. See cmap_changer for details.

import proplot as plot
import pandas as pd
import numpy as np
f, axs = plot.subplots(
    [[1, 1, 2, 2], [0, 3, 3, 0]],
    axwidth=2, share=1, span=False, hratios=(1, 0.9)
state = np.random.RandomState(51423)
data = state.rand(6, 6)
data = pd.DataFrame(data, index=pd.Index(['a', 'b', 'c', 'd', 'e', 'f']))
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 plot 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 contour plot with labels')

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

Heatmap plots

The new heatmap command calls pcolormesh and applies default formatting that is suitable for heatmaps – that is, no gridlines, no minor ticks, and major ticks at the center of each box. Among other things, this is useful for displaying covariance matrices. See the below example.

import proplot as plot
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
f, ax = plot.subplots(axwidth=4)
m = ax.heatmap(
    data, cmap='ColdHot', vmin=-1, vmax=1, N=100,
    lw=0.5, edgecolor='k', labels=True, labels_kw={'weight': 'bold'},
    clip_on=False, # turn off clipping so box edges are not cut in half
    suptitle='Heatmap demo', title='Pseudo covariance matrix', alpha=0, linewidth=0,
    xloc='top', yloc='right', yreverse=True, ticklabelweight='bold',
    ytickmajorpad=4,  # the ytick.major.pad rc setting; adds extra space