Plotting tips

While it is very tempting to use the default plotting settings in Matplotlib, often a little bit of customisation can really improve the quality of a figure.

Choosing an appropriate size of figure

The size you choose in plt.subplots(figsize=(width, height)) is important - simply stretching or scaling your figure after the fact leads to low resolution images, or incorrectly sized text.

Up until now, we’ve used figsize=(4, 3.5) to make a figure with a width of 4 inches and a height of 3.5 inches. This is a good general purpose size, but won’t always be a good choice. Think about an NMR spectrum - this is usually shown in a wide and short figure.

Think wisely about the shape of your figure - what shape is typically used for the data you’re working with?

You should also consider how large your figure will be when placed into a word processor or presentation - see the section at the bottom of this page for more on this.

Choosing appropriate axis labels

Axis labels need to actually describe the quantity that is being plotted, otherwise they’re meaningless. They should always include units (when appropriate), and those units should be correctly formatted to use super- and sub-scripts, Greek symbols, etc.

Matplotlib allows us to use Mathtext to format mathematical symbols and operators.

Important

Be careful when formatting axis labels, variables, and units. The following rules always apply:

  • Variables should always be italic – \(x\) not x.

  • Units should always be Roman (non-italic), never italic, e.g. \(\mathrm{mol\, dm^{-3}}\) and not \(mol\, dm^{-3}\). Remember that you can obtain Roman characters in Mathtext using $\mathrm{}$.

  • Units should be separated by a small space - this can be specified in Mathtext using '$\,$' as, by default, spaces are ignored in Mathtext. e.g $\mathrm{mol\, dm^{-3}}$ and not $\mathrm{mol dm^{-3}}$

  • Subscripts should be italic when referring to a variable, and Roman when referring to anything else. For example, Avogadro’s constant is \(N_\mathrm{A}\) where the \(N\) is italic because it refers to the concept of a “number”, and the subscript \(\mathrm{A}\) is Roman because it literally means the name Avogadro.

Titles - do you really need one?

In School, you may have been told that “graphs” (or plots/figures) always need a title, usually beginning with “A graph to show.” Unfortunately, this is completely incorrect.

A brief look at pretty much any scientific paper demonstrates that titles are quite rarely included. The proper ways to include information on “what” you’re plotting are:

  • Correct axis labels.
  • Correct legend entries.
  • A good caption below your figure.

There’s no need to have a title which needlessly restates these things.

Titles might be useful when many subplots are present in a figure, and can be included with ax.set_title.

Choosing appropriate colours

The default Matplotlib colours are quite good for a quick plot, and in fact are colourblind friendly!

When plotting a single set of data, we recommend you use black markers or a black line - the colour is not required.

Save colours for emphasis, such as when plotting sets of data which vary due to an external factor - e.g. data for different molecules. These colours should be clearly distinguishable - i.e. not blue and purple.

Also consider whether your chosen colour is visible on a white background - bright green and bright yellow are rarely good choices!

Tick marks and axis limits

It’s nearly always a good idea to enable minor tick marks.

It’s also nice to set your axis limits so that the data always appears within the tick marks, and never outside of it.

import matplotlib.pyplot as plt
import numpy as np
%config InlineBackend.figure_format='retina'
# Load the infrared data from file
wavenumber, transmittance, absorbance = np.loadtxt(
  'water_ir_spectrum.dat',
  unpack=True,
  skiprows=1
)

# Create the figure and the axes
fig, ax = plt.subplots(figsize=(5, 4))

ax.plot(wavenumber, transmittance, color='k', label='Transmittance')

ax.set_xlabel(r'Wavenumber / cm$^\mathregular{-1}$')
ax.set_ylabel('Transmittance')

# Set axis limits and enable minor ticks
ax.set_ylim(-0.05, 1)

# Create twinned axis for absorbance
# Twinx means they will share the x axis but have different y scales
aax = ax.twinx()

# Plot absorbance data onto new twinned axis
aax.plot(wavenumber, absorbance, color='C0', label='Absorbance')
# Set y label for twinned axis with same color as the line
aax.set_ylabel('Absorbance', fontdict={'color': 'C0'})

fig.tight_layout()
plt.show()

The above figure contains data outside of the x-axis tick marks and whitespace around the plotted data - this is poor form and can easily be avoided by enabling minor ticks and manually setting the axis limits.

# Load the infrared data from file
wavenumber, transmittance, absorbance = np.loadtxt('water_ir_spectrum.dat', unpack=True, skiprows=1)

# Create the figure and the axes
fig, ax = plt.subplots(figsize=(5, 4))

ax.plot(wavenumber, transmittance, color='k', label='Transmittance')

ax.set_xlabel(r'Wavenumber / cm$^\mathregular{-1}$')
ax.set_ylabel('Transmittance')

# Set axis limits and enable minor ticks
ax.set_ylim(-0.05, 1)
ax.set_xlim(250, 3800)
ax.minorticks_on()

# Create twinned axis for absorbance
# Twinx means they will share the x axis but have different y scales
aax = ax.twinx()

# Plot absorbance data onto new twinned axis
aax.plot(wavenumber, absorbance, color='C0', label='Absorbance')
# Set y label for twinned axis with same color as the line
aax.set_ylabel('Absorbance', fontdict={'color': 'C0'})
# Add minor ticks
aax.minorticks_on()

fig.tight_layout()
plt.show()

Legends - contents and position

A figure legend should simply state which “thing” a given line corresponds to.

You should be able to use the axis labels and legend to read out the following sentence

This figure shows y_label as a function of/versus x_label for legend_entry_1, legend_entry_2, .. and legend_entry_n.

For example

from matplotlib.ticker import MultipleLocator

# 1000 linearly spaced values between -10 and 10
x = np.linspace(-10, 10, 1000)

# Create the figure and the axes
fig, ax = plt.subplots(figsize=(5, 4))

# Plot the data, and provide a label for each plot with custom colours
ax.plot(x, x ** 2, label=r'$f(x) = x^2$')
ax.plot(x, x ** 3, label=r'$f(x) = x^3$')

# Remove right and upper spines
# these are the "outlines" of the axis
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# Set the axis labels
ax.set_xlabel(r'$x$')
ax.set_ylabel(r'$f(x)$')

# Turn on the legend
ax.legend()

# Tighten the layout
fig.tight_layout()

# Show the plot
plt.show()

“This figure shows \(f(x)\) versus \(x\) for \(f(x)=x^2\), and \(f(x)=x^3\).”

Or, another example is

from matplotlib.ticker import MultipleLocator

# Time values 0 to 10 seconds
t = np.linspace(0, 10, 1000)

# 1st order rate constants
k1 = 1  # s^{-1}
k2 = 5 # s^{-1}

# Initial concentration
c0 = 1  # M

# Concentrations as fn of time for two rate constants
conc_1 = c0 * np.exp(-k1 * t)  # M
conc_2 = c0 * np.exp(-k2 * t)  # M

# Create the figure and the axes
fig, ax = plt.subplots(figsize=(5, 4))

# Plot the data, and provide a label for each plot with custom colours
ax.plot(t, conc_1, label=r'$k$ = 1 s$^\mathregular{-1}$')
ax.plot(t, conc_2, label=r'$k$ = 5 s$^\mathregular{-1}$')

# Remove right and upper spines
# these are the "outlines" of the axis
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# Set the axis labels
ax.set_xlabel(r'Time / s')
ax.set_ylabel(r'Concentration / M')

# Turn on the legend
ax.legend()

# Tighten the layout
fig.tight_layout()

# Show the plot
plt.show()

“This figure shows concentration versus time for a first order reaction with \(k=1\, \mathrm{s^{-1}}\) and \(k=5\, \mathrm{s^{-1}}\).”

Your legend labels should not be anything like “concentration versus time” - this is what the axis labels are for.

Figure resolution

Use %config InlineBackend.figure_format='retina' once per notebook - this displays figures in high resolution.

When saving figures, ensure that you save them at a sufficiently high resolution, also known as DPI (dots-per-inch).

For word processed documents, dpi=300 will be sufficient, but this is more complex for presentations - see below.

Exporting your figure to Word or Powerpoint

You’ll certainly want to include your figure(s) in a Word processed document (e.g. in Microsoft Word), or a presentation (e.g. Microsoft Powerpoint).

As we’ve mentioned, in the past you’ll have manually scaled the figure inside of Word or Powerpoint, but this usually distorts your figure and makes it look, frankly, bad.

Word Processors (Microsoft Word)

When making a document in a Word processor, you always know the size of the document you’re producing. More often than not, you’ll be working with an A4 sized document. For the rest of this tutorial we’ll assume that’s the case, but if you’re working with something different then the approach below can be adapted.

The maximum width of a figure in Microsoft Word’s default (portrait) page width is 6.5 inches, so your figure should never be wider than this. A good figure width is 4 inches, so lets repeat our figure of \(x^2\) and \(x^3\) from earlier at this size.

width = 4  # inches
height = 7/8 * width

# Create the figure and the axes
fig, ax = plt.subplots(figsize=(width, height))

# Plot the data, and provide a label for each plot with custom colours
ax.plot(t, conc_1, label=r'$k$ = 1 s$^\mathregular{-1}$')
ax.plot(t, conc_2, label=r'$k$ = 5 s$^\mathregular{-1}$')

# Remove right and upper spines
# these are the "outlines" of the axis
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# Set the axis labels
ax.set_xlabel(r'Time / s')
ax.set_ylabel(r'Concentration / M')

# Enable minor ticks
ax.minorticks_on()

# Turn on the legend
ax.legend()

# Tighten the layout
fig.tight_layout()

# Show the plot
plt.show()

We’ve made the height ever so slightly smaller than the width - this accounts for the room taken up by the \(y\) axis label, and keeps the actual plot roughly square.

The next thing we need to think about is the font size for our figure. We want this to be a little bit smaller than our document text (size 12), so we can choose size 10 for everything other than the legend which we’ll set to size 9. We’re also going to change the font family used in our plot to Arial, since this is what we’re using in our report. To do this you’ll need to upload this file to Noteable.

We can set all of these options by redefining Matplotlib's defaults. This only needs to be done once per notebook - every figure after this will use the new settings.

Remember that the DPI of an image states how many pixels (“dots”) are contained in a given inch.

For an image with a fixed size, higher DPI leads to a sharper image.

#### Font settings - these things only need to be configured once per notebook
# Register Arial font 
import matplotlib.font_manager as fm
fm.fontManager.addfont("Arial.ttf")

# Set font size to 10, and legend font size to 9
plt.rcParams['font.family'] = 'Arial'
plt.rcParams['font.size'] = 10
plt.rcParams['legend.fontsize'] = 9
####

# Create the figure and the axes
fig, ax = plt.subplots(figsize=(4, 3.5))

# Plot the data, and provide a label for each plot with custom colours
ax.plot(t, conc_1, label=r'$k$ = 1 s$^\mathregular{-1}$')
ax.plot(t, conc_2, label=r'$k$ = 5 s$^\mathregular{-1}$')

# Remove right and upper spines
# these are the "outlines" of the axis
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# Set the axis labels
ax.set_xlabel(r'Time / s')
ax.set_ylabel(r'Concentration / M')

# Enable minor ticks
ax.minorticks_on()

# Turn on the legend
ax.legend()

# Tighten the layout
fig.tight_layout()

# Save to file
plt.savefig('conc_report.png', dpi=300)

# Show the plot
plt.show()

Where we’ve also saved our plot to conc_report.png. We can now file into Word, and set its width to 4 inches (or 10.16 cm) using the Picture Format tab. The result should look something like this.

Powerpoint

The situation for a Powerpoint presentation is a little bit different - you don’t (generally) know how big your slide will be when it appears on a projector, and so instead of working with distances like inches or centimetres, we have to work in units of pixels (the tiny squares that make up the image on your screen) since these change size between displays. Compare a 4K 52 inch TV to a 4K projector: both have the same number of pixels (since they’re both 4K), but the pixels have different physical dimensions.

The full width of a slide will be between 1400 and 1600 pixels (excluding margins), so we can use this information to size our plots accordingly.

To Matplotlib to produce an image with a specified size in pixels we have to state both the size of the image in inches, and the DPI of the image. This is slightly annoying, but is in fact very easy. For a powerpoint slide, you can always set the DPI to 150 since, as we’re working in pixels, this will no longer affect the sharpness of the image in the same way as it did before (e.g. for a Word document).

To calculate the width in inches, we use the following simple formula

\[\begin{equation*} \mathrm{width [inch] = width [px] / DPI} \end{equation*}\]

As an example, let’s make our concentration versus time graph 850 pixels wide. If we set our DPI as 150, then the width of the figure can be calculated.

# Calculate the figure size for a fixed number of pixels and dpi
pixels = 850
dpi = 150
width_in = pixels / dpi
height_in = width_in * 7/8

We’ve done the same trick with width and height as we did for a Word processed document: a slightly smaller height than width gives a square plot.

# Set font size to 18, and legend font size to 17
plt.rcParams['font.size'] = 18
plt.rcParams['legend.fontsize'] = 17

fig, ax = plt.subplots(figsize=(width_in, height_in), dpi=dpi)

# Plot the data, and provide a label for each plot with custom colours
# Here we've used a thicker linewidth for our presentation
ax.plot(t, conc_1, label=r'$k$ = 1 s$^\mathregular{-1}$', lw=2.5)
ax.plot(t, conc_2, label=r'$k$ = 5 s$^\mathregular{-1}$', lw=2.5)

# Remove right and upper spines
# these are the "outlines" of the axis
ax.spines['top'].set_visible(False)
ax.spines['right'].set_visible(False)

# Set the axis labels
ax.set_xlabel(r'Time / s')
ax.set_ylabel(r'Concentration / M')

# Enable minor ticks
ax.minorticks_on()

# Turn on the legend
ax.legend()

# Tighten the layout
fig.tight_layout()

# Save to file but no need to specify DPI - the value
# used in plt.subplots will be used.
plt.savefig('conc_pres.png')

# Show the plot
plt.show()

This looks like a huge plot when viewed on this website, and on Noteable! But, if imported into powerpoint and set with a width of 850 px in the Picture Format tab, you’ll get a slide that looks like this.

One last thing - font sizes. The projector image is definitely going to be bigger than A4, so we’re going to need a larger font size than 12.

The following are recommended font sizes for slides:

Slide Element Recommended Font Size (for Arial)
Title 32–40 pt
Section header 26–32 pt
Main body text 20–24 pt
Figure text 18–22 pt
Figure captions 16–18 pt
Footnotes / References 14–16 pt (minimum)

A good choice is a figure text size of 18, and a legend text size of 17 - this is what we’ve used in the example above. Notice that when the above procedure is followed, the font size on the image is correct versus that of a text box with equivalently sized text.