Matplotlib logo

Annotations, Colorbars, and Advanced Layouts


Overview

In this section we explore methods for customizing plots, including the following:

  1. Adding annotations

  2. Rendering equations

  3. Colormap overview

  4. Basic colorbars

  5. Shared colorbars

  6. Custom colorbars

  7. Mosaic subplots

Prerequisites

Concepts

Importance

NumPy Basics

Necessary

Matplotlib Basics

Necessary

  • Time to learn: 30-40 minutes

Imports

Here, we import matplotlib, numpy, and scipy (to generate some sample data)

import matplotlib.pyplot as plt
import numpy as np
import scipy.stats as stats
from matplotlib.colors import LinearSegmentedColormap, ListedColormap, Normalize

Create Some Sample Data

Using scipy.stats, we can create a normal distribution! Notice how nicely centered and normal our distribution is!

mu = 0
variance = 1
sigma = np.sqrt(variance)

x = np.linspace(mu - 3 * sigma, mu + 3 * sigma, 200)
pdf = stats.norm.pdf(x, mu, sigma)

plt.plot(x, pdf);
../../_images/annotations-colorbars-layouts_6_0.png

Adding Annotations

A common part of many people’s workflows is adding annotations, or “a note of explanation or comment added to a text or diagram.”

We can do this using plt.text which takes the inputs of the (x, y) float text position in data coordinates and the text string.

plt.plot(x, pdf)
plt.text(0, 0.05, 'here is some text!');
../../_images/annotations-colorbars-layouts_8_0.png

Rendering Equations

We can even add math text, using Latex syntax. The key is use strings with following format:

r'$some_equation$'

Here is the example equation we use!

\[f(x) = \frac{1}{\mu\sqrt{2\pi}} e^{-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^2}\]

If you are interested in learning more about Latex syntax, check out their official documentation.

Further, if you’re running the notebook interactively (e.g. on Binder) you can double click on the cell to see the latex source for the rendered equation.

plt.plot(x, pdf)

plt.text(
    -1,
    0.05,
    r'$f(x) = \frac{1}{\mu\sqrt{2\pi}}  e^{-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^2}$',
);
../../_images/annotations-colorbars-layouts_11_0.png

We plotted our equation! But it looks pretty small; we can increase the size of the text, and center the equation by using fontsize and ha (horizontal alignment).

This next example also uses latex notation in the legend text.

fstr = r'$f(x) = \frac{1}{\mu\sqrt{2\pi}}  e^{-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^2}$'

plt.plot(x, pdf, label=r'$\mu=0, \,\, \sigma^2 = 1$')
plt.text(0, 0.05, fstr, fontsize=15, ha='center')
plt.legend();
../../_images/annotations-colorbars-layouts_13_0.png

Add a Box Around the Text

One other thing we can add here, for readability, is a box around the text, using bbox.

The bbox argument in plt.text uses a dictionary to create the box! We pass in:

  • a rounded box sytle (boxstyle = 'round')

  • a light grey facecolor (fc = 'lightgrey')

  • a black edgecolor (ec = 'k')

fig = plt.figure(figsize=(10, 8))
plt.plot(x, pdf)

fstr = r'$f(x) = \frac{1}{\mu\sqrt{2\pi}}  e^{-\frac{1}{2}\left(\frac{x-\mu}{\sigma}\right)^2}$'
plt.text(
    0,
    0.05,
    fstr,
    fontsize=18,
    ha='center',
    bbox=dict(boxstyle='round', fc='lightgrey', ec='k'),
)

plt.xticks(fontsize=16)
plt.yticks(fontsize=16)

plt.title("Normal Distribution with SciPy", fontsize=24);
../../_images/annotations-colorbars-layouts_16_0.png

Colormap Overview

Colormaps are a visually appealing way to represent another dimension to your data. They are a matrix of hues and values allowing you to, for example, display hotter temperatures as red and colder temperatures as blue.

Classes of colormaps

Click the dropdown arrow to see examples of colormaps within their respective classes.

1. Sequential: change in lightness and/or saturation of color incrementally. Good for data that has ordering.

Perceptually Sequential

Sequential

Sequential2

Perceptually Sequential

Sequential

Sequential2

2. Diverging: change in lightness and/or saturation of two different colors that meet in the middle at an unsaturated color. Should be used when the data has a natural zero point, such as sea level.

Diverging

Diverging

3. Cyclic: change in lightness of two different colors that meet in the middle and begin and end at an unsaturated color. Should be used for values that naturally wrap around.

Cyclic

Cyclic

4. Qualitative: miscellaneous color. Should not be used for data that has ordering or relationships.

Qualitative

Miscellanous

Miscellanous

Other considerations

There is a lot of info about choosing colormaps that could be its own tutorial. Two important considerations:

  1. Color blind friendly patterns: avoiding colormaps with both red and green can account for the most common form of color blindness. The GeoCAT-examples gallery has a section devoted to picking better colormaps that covers this.

  2. Grayscale conversion: It is not uncommon for plots rendered in color to be printed in black and white, obscuring the usefulness of a chosen colormap

hsv colormap in grayscale

Basic Colorbars

Before we look at a colorbar, let’s generate some fake data using numpy.random

npts = 1000
nbins = 15

x = np.random.normal(size=npts)
y = np.random.normal(size=npts)

Here, we plot a 2D histogram using our fake data, using the default colorbar “viridis”

fig = plt.figure()
ax = plt.gca()

plt.hist2d(x, y, bins=nbins, density=True)
plt.colorbar();
../../_images/annotations-colorbars-layouts_26_0.png

We can change which colorbar to use by passing in cmap = 'colorbar_name'. We can use the magma colorbar instead!

fig = plt.figure()
ax = plt.gca()

plt.hist2d(x, y, bins=nbins, density=True, cmap='magma')
plt.colorbar();
../../_images/annotations-colorbars-layouts_28_0.png

Shared Colorbars

Often times, you are not plotting a single axis. You may wish to share colorbars between different plots! We can share colorbars between two plots using the following:

fig, ax = plt.subplots(nrows=1, ncols=2, constrained_layout=True)

hist1 = ax[0].hist2d(x, y, bins=15, density=True, vmax=0.18)
hist2 = ax[1].hist2d(x, y, bins=30, density=True, vmax=0.18)

fig.colorbar(hist1[3], ax=ax, location='bottom')
<matplotlib.colorbar.Colorbar at 0x7f9596a55070>
../../_images/annotations-colorbars-layouts_30_1.png

You may have noticed the input argument hist1[3] to fig.colorbar. To clarify, hist1 is a tuple returned by hist2d, and hist1[3] returns a matplotlib.collections.QuadMesh that points to the colormap for the first histogram. To make sure that both histograms are using the same colormap with the same range of values, vmax is set to 0.18 for both plots. This ensures that both histograms are using colormaps that represent values from 0 (the default for histograms) to 0.18. Because the same data is used for both plots, it doesn’t matter whether we pass in hist1[3] or hist2[3] to fig.colorbar. Read more at the matplotlib.axes.Axes.hist2d documentation.

Other kinds of plots can share colorbars too. A common use case is filled contour plots with shared colorbars for comparing data. vmin and vmax behave the same way for contourf as they do for hist2d. A downside to using the vmin and vmax kwargs when plotting two different datasets is that while the colormaps may be the same, the dataset with a smaller range of values won’t show the full range of colors as seen below. Thus, it does matter in this particular example which output from contourf is used to make the colorbar.

x2 = y2 = np.arange(-3, 3.01, 0.025)
X2, Y2 = np.meshgrid(x2, y2)
Z = np.sqrt(np.sin(X2) ** 2 + np.sin(Y2) ** 2)
Z2 = np.sqrt(2 * np.cos(X2) ** 2 + 2 * np.cos(Y2) ** 2)

fig, ax = plt.subplots(nrows=1, ncols=2, constrained_layout=True)
c1 = ax[0].contourf(X2, Y2, Z, vmin=0, vmax=2)
c2 = ax[1].contourf(X2, Y2, Z2, vmin=0, vmax=2)
fig.colorbar(c1, ax=ax[0], location='bottom')
fig.colorbar(c2, ax=ax[1], location='bottom')

fig.suptitle('Shared colormaps on data with different ranges')
Text(0.5, 0.98, 'Shared colormaps on data with different ranges')
../../_images/annotations-colorbars-layouts_33_1.png
fig, ax = plt.subplots(nrows=1, ncols=2, constrained_layout=True)
c1 = ax[0].contourf(X2, Y2, Z, vmin=0, vmax=2)
c2 = ax[1].contourf(X2, Y2, Z2, vmin=0, vmax=2)
fig.colorbar(c2, ax=ax, location='bottom')

fig.suptitle('Using the contourf output from the data with a wider range')
Text(0.5, 0.98, 'Using the contourf output from the data with a wider range')
../../_images/annotations-colorbars-layouts_34_1.png

Custom Colorbars

Even with the large collection of prepackaged colorbars, you may find it useful to create your own colorbar.

Below are 2 similar examples of using custom colorbars:

The first has very discrete list of colors called colors, and creates a colormap from this list with the call ListedColormap.

The second used the call LinearSegmentedColormap to create a colormap from interpolating the same list colors.

colors = [
    'white',
    'pink',
    'red',
    'orange',
    'yellow',
    'green',
    'blue',
    'purple',
    'black',
]
ccmap = ListedColormap(colors)
norm = Normalize(vmin=0, vmax=0.18)

fig, ax = plt.subplots(nrows=1, ncols=2, constrained_layout=True)

hist1 = ax[0].hist2d(x, y, bins=15, density=True, cmap=ccmap, norm=norm)
hist2 = ax[1].hist2d(x, y, bins=30, density=True, cmap=ccmap, norm=norm)

cbar = fig.colorbar(hist1[3], ax=ax, location='bottom')
../../_images/annotations-colorbars-layouts_36_0.png
cbcmap = LinearSegmentedColormap.from_list("cbcmap", colors)

fig, ax = plt.subplots(nrows=1, ncols=2, constrained_layout=True)

hist1 = ax[0].hist2d(x, y, bins=15, density=True, cmap=cbcmap, norm=norm)
hist2 = ax[1].hist2d(x, y, bins=30, density=True, cmap=cbcmap, norm=norm)

cbar = fig.colorbar(hist1[3], ax=ax, location='bottom')
../../_images/annotations-colorbars-layouts_37_0.png

The Normalize Class

Note that both plots use the norm kwarg. The Normalize class linearly normalizes data into the [0, 1] interval. This is used to linearly map the colors in the colormap to the data from vmin to vmax. In fact, we used this functionality in the previous histogram exercise! The vmin and vmax kwargs for hist2d are simply passed into the Normalize function. When making a custom colormap, it is best to specify how you want the data normalized.

For non-linear nomalization, check out this Colormap Normalization tutorial.

Mosaic Subplots

One of the recent features added to Matplotlib is subplot_mosaic where you can pass the structure of your figure, and it will generate your subplots automatically!

For example, if we wanted two plots on top, and one of the bottom, we can construct it using the following block of text:

""
AB
CC
""

This corresponds to three axes: A, B, and C with A and B on top of C.

Once we create the subplots, we can access them using the resultant axes dictionary, with the syntax axes_dict['your_axis']. An example of this is given below!

axdict = plt.figure(constrained_layout=True).subplot_mosaic(
    """
    AB
    CC
    """
)

histA = axdict['A'].hist2d(x, y, bins=15, density=True, cmap=cbcmap, norm=norm)
histB = axdict['B'].hist2d(x, y, bins=10, density=True, cmap=cbcmap, norm=norm)
histC = axdict['C'].hist2d(x, y, bins=30, density=True, cmap=cbcmap, norm=norm)
../../_images/annotations-colorbars-layouts_40_0.png

You’ll notice there is not a colorbar plotted by default. When constructing the colorbar, we need to specify:

  • Which plot to use for the colormapping (ex. histA)

  • Which axes to merge colorbars across (ex. [histA, histB])

  • Where to place the colorbar (ex. bottom)

axdict = plt.figure(constrained_layout=True).subplot_mosaic(
    """
    AB
    CC
    """
)

histA = axdict['A'].hist2d(x, y, bins=15, density=True, cmap=cbcmap, norm=norm)
histB = axdict['B'].hist2d(x, y, bins=10, density=True, cmap=cbcmap, norm=norm)
histC = axdict['C'].hist2d(x, y, bins=30, density=True, cmap=cbcmap, norm=norm)

fig.colorbar(histA[3], ax=[axdict['A'], axdict['B']], location='bottom')
fig.colorbar(histC[3], ax=[axdict['C']], location='right');
../../_images/annotations-colorbars-layouts_42_0.png

Summary

  • You can use features in Matplotlib to add annotations, even math, to your plots

  • There are a number of considerations to take into account when choosing your colormap

  • You can create your own colormaps with Matplotlib

  • Various axes in figures can share colorbars