Matplotlib logo

Annotations, Colorbars, and Advanced Layouts


Overview

In this section we explore methods for customizing plots. The following topics will be covered:

  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 the matplotlib.pyplot interface and numpy, in addition to the scipy statistics package (scipy.stats) for generating 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

By using scipy.stats, the Scipy statistics package described above, we can easily create a data array containing a normal distribution. We can plot these data points to confirm that the correct distribution was generated. The generated sample data will then be used later in this section. The code and sample plot for this data generation are as follows:

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/1b032d9a2feeffd58f4e3df5e582f2f3fc2710e05e1adbc47eb699b687e891d0.png

Adding Annotations

A common part of many people’s workflows is adding annotations. A rough definition of ‘annotation’ is ‘a note of explanation or comment added to text or a diagram’.

We can add an annotation to a plot using plt.text. This method takes the x and y data coordinates at which to draw the annotation (as floating-point values), and the string containing the annotation text.

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

Rendering Equations

We can also add annotations with equation formatting, by using LaTeX syntax. The key is to use strings in the following format:

r'$some_equation$'

Let’s run an example that renders the following equation as an annotation:

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

The next code block and plot demonstrate rendering this equation as an annotation.

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

Furthermore, if the code is being executed in a Jupyter notebook run 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/1d51c6d30cd93059141d8982c4f6bce9d3c7c7f9b234e1b359521763688676d8.png

As you can see, the equation was correctly rendered in the plot above. However, the equation appears quite small. We can increase the size of the text using the fontsize keyword argument, and center the equation using the ha (horizontal alignment) keyword argument.

The following example illustrates the use of these keyword arguments, as well as creating a legend containing LaTeX notation:

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/470f6f8431715056f1a7646b6b78ab6751eccb25f37d471854e689b222a44d63.png

Add a Box Around the Text

To improve readability, we can also add a box around the equation text. This is done using bbox.

bbox is a keyword argument in plt.text that creates a box around text. It takes a dictionary that specifies options, behaving like additional keyword arguments inside of the bbox argument. In this case, we use the following dictionary keys:

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

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

  • a black edgecolor (ec = 'k')

This example demonstrates the correct use of bbox:

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/bc39ec80fee868b87317c071ae9c36dff1c3d39c703a0422d5cf2d7760de77eb.png

Colormap Overview

Colormaps are a visually appealing method of looking at visualized data in a new and different way. They associate specific values with hues, using color to ease rapid understanding of plotted data; for example, displaying hotter temperatures as red and colder temperatures as blue.

Classes of colormaps

There are four different classes of colormaps, and many individual maps are contained in each class. To view some examples for each class, use the dropdown arrow next to the class name below.

1. Sequential: These colormaps incrementally increase or decrease in lightness and/or saturation of color. In general, they work best for ordered data.

Perceptually Sequential

Sequential

Sequential2

Perceptually Sequential

Sequential

Sequential2

2. Diverging: These colormaps contain two colors that change in lightness and/or saturation in proportion to distance from the middle, and an unsaturated color in the middle. They are almost always used with data containing a natural zero point, such as sea level.

Diverging

Diverging

3. Cyclic: These colormaps have two different colors that change in lightness and meet in the middle, and unsaturated colors at the beginning and end. They are usually best for data values that wrap around, such as longitude.

Cyclic

Cyclic

4. Qualitative: These colormaps have no pattern, and are mostly bands of miscellaneous colors. You should only use these colormaps for unordered data without 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: By using colormaps that do not contain both red and green, you can help people with the most common form of color blindness read your data plots more easily. The GeoCAT examples gallery has a section about picking better colormaps that covers this issue in greater detail.

  2. Grayscale conversion: It is not too uncommon for a plot originally rendered in color to be converted to black-and-white (monochrome grayscale). This reduces the usefulness of specific colormaps, as shown below.

hsv colormap in grayscale

Basic Colorbars

Before we look at a colorbar, let’s generate some fake X and Y data using numpy.random, and set a number of bins for a histogram:

npts = 1000
nbins = 15

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

Now we can use our fake data to plot a 2-D histogram with the number of bins set above. We then add a colorbar to the plot, using the default colormap viridis.

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

plt.hist2d(x, y, bins=nbins, density=True)
plt.colorbar();
../../_images/2065861a6e5ff272f46998bd9dd20416733ab907706c2db4750fd1112eca8021.png

We can change which colormap to use by setting the keyword argument cmap = 'colormap_name' in the plotting function call. This sets the colormap not only for the plot, but for the colorbar as well. In this case, we use the magma colormap:

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

plt.hist2d(x, y, bins=nbins, density=True, cmap='magma')
plt.colorbar();
../../_images/892f3e661f34611708861df520799e1f9dfa016176f173787df867f30a959090.png

Shared Colorbars

Oftentimes, you are plotting multiple subplots, or multiple Axes objects, simultaneously. In these scenarios, you can create colorbars that span multiple plots, as shown in the following example:

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 0x7f34cc461070>
../../_images/7bad0482f337fa5604a9e6dd39e004f4239ab45db418fe488581666f4e0444b9.png

You may be wondering why the call to fig.colorbar uses the argument hist1[3]. The explanation is as follows: hist1 is a tuple returned by hist2d, and hist1[3] contains 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 values are used for both plots, it doesn’t matter whether we pass in hist1[3] or hist2[3] to fig.colorbar. You can learn more about this topic by reviewing the matplotlib.axes.Axes.hist2d documentation.

In addition, there are many other types of plots that can also share colorbars. An actual use case that is quite common is to use shared colorbars to compare data between filled contour plots. The vmin and vmax keyword arguments behave the same way for contourf as they do for hist2d. However, there is a potential downside to using the vmin and vmax kwargs. When plotting two different datasets, the dataset with the smaller range of values won’t show the full range of colors, even though the colormaps are the same. Thus, it can potentially matter which output from contourf is used to make a colorbar. The following examples demonstrate general plotting technique for filled contour plots with shared colorbars, as well as best practices for dealing with some of these logistical issues:

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/a39dc4df0a70f1b25137b97d2a2f6f190d7bd31cbf9522d0bd3012b8bef57228.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/e686fffe535db4b54576f28d0dd4eef4cd5dedb9c0c5b9d3390ae7f1d909deba.png

Custom Colorbars

Despite the availability of a large number of premade colorbar styles, it can still occasionally be helpful to create your own colorbars.

Below are 2 similar examples of using custom colorbars.

The first example uses a very discrete list of colors, simply named colors, and creates a colormap from this list by using the call ListedColormap.

The second example uses the function LinearSegmentedColormap to create a new colormap, using interpolation and the colors list defined in the first example.

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/57374fa94fcd8910a522ed48be206e3992985057f56415b86c8f254a46eae418.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/b5738ebe6e082ff526e5de2dc05214e2d4efbaf3396fa0d05c43502af4edf070.png

The Normalize Class

Notice that both of these examples contain plotting functions that make use of the norm kwarg. This keyword argument takes an object of the Normalize class. A Normalize object is constructed with two numeric values, representing the start and end of the data. It then linearly normalizes the data in that range into an interval of [0,1]. If this sounds familiar, it is because this functionality was used in a previous histogram example. Feel free to review any previous examples if you need a refresher on particular topics. In this example, the values of the vmin and vmax kwargs used in hist2d are reused as arguments to the Normalize class constructor. This sets the values of vmin and vmax as the starting and ending data values for our Normalize object, which is passed to the norm kwarg of hist2d to normalize the data. There are many different options for normalizing data, and it is important to explicitly specify how you want your data normalized, especially when making a custom colormap.

For information on nonlinear and other complex forms of normalization, review this Colormap Normalization tutorial.

Mosaic Subplots

One of the helpful features recently added to Matplotlib is the subplot_mosaic method. This method allows you to specify the structure of your figure using specially formatted strings, and will generate subplots automatically based on that structure.

For example, if we wanted two plots on top, and one on the bottom, we can construct them by passing the following string to subplot_mosaic:

""
AB
CC
""

This creates three Axes objects corresponding to three subplots. The subplots A and B are on top of the subplot C, and the C subplot spans the combined width of A and B.

Once we create the subplots, we can access them using the dictionary returned by subplot_mosaic. You can specify an Axes object (in this example, your_axis) in the dictionary (in this example, axes_dict) by using the syntax axes_dict['your_axis']. A full example of subplot_mosaic is as follows:

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/c714da0d9f13bd54b8a36ce958675662af94d0dbb5ac30d22a6505fd67a6ff57.png

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

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

  • Which subplots (Axes objects) 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/bf48c85773701fa55437a1af149a748466ad14f1d60a442660c7d427f35104d5.png

Summary

  • You can use features in Matplotlib to add text annotations to your plots, including equations in mathematical notation

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

  • You can create your own colormaps with Matplotlib

  • Various subplots and corresponding Axes objects in a figure can share colorbars

Resources and references