I: Our Imports

We need to import matplotlib itself this time.

In [1]:
import numpy as np
from matplotlib import pyplot as plt
from matplotlib import gridspec
import matplotlib as mpl
In [2]:
%matplotlib tk

II: Creating a Figure

When preparing a figure for a paper or whatever, you will want more control. We can define the size and make another improvement during figure creation using some options.

The most important thing is to specify the width. If the graph does not have too much detail, it should be sized so that it fits within one column of text (the width should be slightly less than 3 3/8 in).

More detailed figures may take up to 1.5 or 2 columns, as necessary.

In [3]:
fig = plt.figure(figsize=(3.3,3), tight_layout=True) 

The figsize parameter requires a tuple of (width, height), both specified in inches.

The tight_layout option removes some whitespace from the outer edges.

In [4]:
ax = fig.add_subplot(1,1,1)

We'll discuss what the 1,1,1 is about later.

Read in some data and plot it.

In [5]:
t, V1, V2, V3 = np.loadtxt('data.csv', delimiter=',', unpack=True, skiprows=1)
In [6]:
ax.plot(t, V1, label='Underdamped')
Out[6]:
[<matplotlib.lines.Line2D at 0x7f4a2474efd0>]

What the label parameter is for we'll discuss later. For our current purposes, it's optional.

The horizontal axis looks bad. Close the window and start again, but this time convert to microseconds.

In [8]:
# CISV
fig = plt.figure(figsize=(3.3,3), tight_layout=True)
ax = fig.add_subplot(1,1,1)
ax.plot(t/(1e-6), V1, label='Underdamped')
Out[8]:
[<matplotlib.lines.Line2D at 0x7f4a2476ad30>]

Label the axes.

In [9]:
# CISV
ax.set_xlabel(r'$t$ ($\mu$s)')
ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
Out[9]:
Text(9.444444444444445, 0.5, '$V_\\mathrm{C}$ (V)')

Add the other plots.

In [10]:
# CISV
ax.plot(t/1e-6, V2, label='Overdamped')
ax.plot(t/1e-6, V3, label='Critically damped')
Out[10]:
[<matplotlib.lines.Line2D at 0x7f4a246367f0>]

Now we consider the label argument we've been passing.

In [10]:
# CISV
ax.legend()
Out[10]:
<matplotlib.legend.Legend at 0x7f317fdf9668>

So...it tells matplotlib how to label legends.


III: Plot Customizations

Copy all of the essential commands for reproducing that plot, but do not execute immediately. We'll work with a larger figure for now, so we can see more of what's going on.

Mark up the cell so that we can find it again.

In [11]:
# LOOK AT ME!!! WE'LL COPY AND PASTE THESE LINES MANY TIMES

fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
ax = fig.add_subplot(1,1,1)
ax.plot(t/1e-6, V1, label='Underdamped')
ax.plot(t/1e-6, V2, label='Overdamped')
ax.plot(t/1e-6, V3, label='Critically damped')
ax.set_xlabel(r'$t$ ($\mu$s)')
ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
ax.legend()
Out[11]:
<matplotlib.legend.Legend at 0x7f317fd9dac8>

A: Colors

There are several ways of specifying colors.

  1. A (case-insensitive) string specifying an X11 color name. A full list of possible values can be found on Wikipedia, though you'll have to remove spaces. There are a lot of options, but here are some examples.

    • 'blue'
    • 'cyan'
    • 'lightslategray'
    • 'slateblue'
  2. One of the single-character (MATLAB-like) codes below.

    • 'b' : blue
    • 'g' : green
    • 'r' : red
    • 'c' : cyan
    • 'm' : magenta
    • 'y' : yellow
    • 'k' : black
    • 'w' : white
  3. A tuple of floats in the range [0,1], representing either the (R, G, B) or (R, G, B, A) values.

  4. A hex-code RBG or RBGA string. (For example, use '#008000' for what X11 calls "web green".)

B: Lines

The thickness of the plot lines can be modified using the linewidth parameter. The value should be specified in points.

In [12]:
# CISV
fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
ax = fig.add_subplot(1,1,1)
ax.plot(t/1e-6, V1, linewidth=1, label='Underdamped')
ax.plot(t/1e-6, V2, linewidth=1.5, label='Overdamped')
ax.plot(t/1e-6, V3, linewidth=2, label='Critically damped')
ax.set_xlabel(r'$t$ ($\mu$s)')
ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
ax.legend()
Out[12]:
<matplotlib.legend.Legend at 0x7f317fcb6320>

The style of the line can be configured using the linestyle parameter. The options are

  • '-' : solid line
  • '--' : dashed line
  • '-.' : dash-dot line
  • ':' : dotted line
  • '' : no line
In [13]:
# CISV
fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
ax = fig.add_subplot(1,1,1)
ax.plot(t/1e-6, V1, linestyle='-', label='Underdamped')
ax.plot(t/1e-6, V2, linestyle='--', label='Overdamped')
ax.plot(t/1e-6, V3, linestyle=':', label='Critically damped')
ax.set_xlabel(r'$t$ ($\mu$s)')
ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
ax.legend()
Out[13]:
<matplotlib.legend.Legend at 0x7f317fc40780>

The color parameter sets the color of the line and the markers. Values are specified using the syntax from Section A.

In [14]:
# CISV
fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
ax = fig.add_subplot(1,1,1)
ax.plot(t/1e-6, V1, color='slateblue', label='Underdamped')
ax.plot(t/1e-6, V2, color='m', label='Overdamped')
ax.plot(t/1e-6, V3, color=(0,1,0.5), label='Critically damped')
ax.set_xlabel(r'$t$ ($\mu$s)')
ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
ax.legend()
Out[14]:
<matplotlib.legend.Legend at 0x7f317fbbaf98>

C: Markers

The data we've been working with are too dense to see markers clearly, so we'll load some sparser data.

In [15]:
t, V1, V2, V3 = np.loadtxt('data_sparse.csv', delimiter=',', 
                           unpack=True, skiprows=1)

We can specify the marker style using the marker parameter. There are many styles available. Here are some common ones:

  • '.' : point
  • ',' : pixel
  • 'o' : circle
  • 'v' : triangle down
  • '^' : triangle up
  • '<' : triangle left
  • '>' : triangle right
  • 's' : square
  • '*' : star
  • '+' : plus
  • 'x' : x
  • 'D' : diamond
  • 'd' : thin diamond
In [16]:
# CISV
fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
ax = fig.add_subplot(1,1,1)
ax.plot(t/1e-6, V1, marker='.', label='Underdamped')
ax.plot(t/1e-6, V2, marker='<', label='Overdamped')
ax.plot(t/1e-6, V3, marker='*', label='Critically damped')
ax.set_xlabel(r'$t$ ($\mu$s)')
ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
ax.legend()
Out[16]:
<matplotlib.legend.Legend at 0x7f317fb48be0>

Markers have two components---a face and an edge. They are customized separately.

The sizes are configured using markersize (default 6) and markeredgewidth (default 1.0), both of which expect a float in points.

In [17]:
# CISV
fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
ax = fig.add_subplot(1,1,1)
ax.plot(t/1e-6, V1, marker='.', markersize=9, label='Underdamped')
ax.plot(t/1e-6, V2, marker='<', label='Overdamped')
ax.plot(t/1e-6, V3, marker='*', markeredgewidth=3, label='Critically damped')
ax.set_xlabel(r'$t$ ($\mu$s)')
ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
ax.legend()
Out[17]:
<matplotlib.legend.Legend at 0x7f317facee48>

The face and edge colors can also be independently specified, using markeredgecolor and markerfacecolor.

If either of these is not given, the color of the respective part comes from the argument passed to color (or the default color, if color is not given). Normally, this is best.

In [18]:
# CISV
fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
ax = fig.add_subplot(1,1,1)
ax.plot(t/1e-6, V1, marker='.', markersize=20, markeredgecolor='k',
        markerfacecolor='m', label='Underdamped')
ax.plot(t/1e-6, V2, marker='<', markersize=20, markeredgecolor='k',
        label='Overdamped')
ax.plot(t/1e-6, V3, marker='*', label='Critically damped')
ax.set_xlabel(r'$t$ ($\mu$s)')
ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
ax.legend()
Out[18]:
<matplotlib.legend.Legend at 0x7f317fa81860>

D: Format Strings

It is usually faster to specify formatting using a format string, which is the third posititional argument to plot (with keyword fmt if going out of order).

The structure is

fmt = '[marker][line][color]'

Each is optional. If any value is not given, the default from the cycle is used. If no marker is specified, the default is a line without markers.

If only color is specified, you can use any of the methods given above in the discussion of line colors. Otherwise, only the single-letter, MATLAB-style abbreviations are allowed.

The order only matters in cases where ambiguity would arise. For example,

  • '.-' gives a solid line with point markers, while '-.' gives a dash-dot line with no markers.

  • '-o' and 'o-' both give solid lines with circle markers.

In [19]:
# CISV
fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
ax = fig.add_subplot(1,1,1)
ax.plot(t/1e-6, V1, 'slateblue', label='Underdamped')
ax.plot(t/1e-6, V2, 'r-.', label='Overdamped')
ax.plot(t/1e-6, V3, 'k.-', label='Critically damped')
ax.set_xlabel(r'$t$ ($\mu$s)')
ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
ax.legend()
Out[19]:
<matplotlib.legend.Legend at 0x7f317f9e6588>

IV: Axes Configuration

Let's plot sine!

In [20]:
# CISV
fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
ax = fig.add_subplot(1,1,1)
theta = np.linspace(0, 2.5*np.pi, 500)
sin_theta = np.sin(theta)
ax.plot(theta, sin_theta)
ax.set_xlabel(r'$\theta$ (rad)')
ax.set_ylabel(r'$\sin\,\theta$')
Out[20]:
Text(0, 0.5, '$\\sin\\,\\theta$')

A: Limits

Note the extra space at the edges. Suppose we wanted to get rid of it. The limits of the x and y axes are given by

ax.get_xlim()
ax.get_ylim()
In [21]:
# CISV
xlim = ax.get_xlim()
print('x-limits:', xlim)
ylim = ax.get_ylim()
print('y-limits:', ylim)
x-limits: (-0.39269908169872414, 8.246680715673207)
y-limits: (-1.0999791907454364, 1.099999009083116)

We can change them using

ax.set_xlim(left=<new_xmin>, right=<new_max>)
ax.set_ylim(bottom=<new_ymin>, top=<new_ymax>)

Any argument set to None will remain unchanged.

Thus, to get rid of all surrounding space, we could use

In [22]:
# CISV
ax.set_xlim(left=0, right=2.5*np.pi)
ax.set_ylim(bottom=-1, top=1)
Out[22]:
(-1, 1)

We can reverse this by auto-scaling.

In [23]:
# CISV
ax.autoscale(tight=False)

If we had set tight=True, it would have removed the space, producing the same effect as our manual change.

B: Ticks and Labels

We can configure the positions of ticks and labels using

ax.set_xticks(<list_of_x_tick_positions>)
ax.set_yticks(<list_of_y_tick_positions>)

Suppose we only want ticks on the vertical axis at multiples of one-half. We can do it like this:

In [24]:
# CISV
ax.set_yticks([-1.0, -0.5, 0.0, 0.5, 1.0])
Out[24]:
[<matplotlib.axis.YTick at 0x7f317f945c50>,
 <matplotlib.axis.YTick at 0x7f317f9454e0>,
 <matplotlib.axis.YTick at 0x7f317fbe1908>,
 <matplotlib.axis.YTick at 0x7f317f932ef0>,
 <matplotlib.axis.YTick at 0x7f317f90f160>]

We could also put the horizontal ticks at half-integer multiples of $\pi$. We can simplify it using list comprehensions.

In [25]:
# CISV
ax.set_xticks([i*np.pi/2 for i in range(6)])
Out[25]:
[<matplotlib.axis.XTick at 0x7f317f93a4e0>,
 <matplotlib.axis.XTick at 0x7f317f93a358>,
 <matplotlib.axis.XTick at 0x7f317f932fd0>,
 <matplotlib.axis.XTick at 0x7f317f932898>,
 <matplotlib.axis.XTick at 0x7f317f969128>,
 <matplotlib.axis.XTick at 0x7f317f9695c0>]

The tick labels can be similarly changed, using

ax.set_xticklabels(<list_of_x_tick_labels>)
ax.set_yticklabels(<list_of_y_tick_labels>)

Both lists should contain strings.

For the vertical axis ticks, let's use in-line fractions.

In [26]:
# CISV
ax.set_yticklabels(['$-1$', '$-1/2$', '$0$', '$1/2$', '$1$'])
Out[26]:
[Text(0, -1.25, '$-1$'),
 Text(0, -1.0, '$-1/2$'),
 Text(0, -0.75, '$0$'),
 Text(0, -0.5, '$1/2$'),
 Text(0, -0.25, '$1$')]

For the horizontal-axis ticks, we can get some nice, pretty $\pi$s.

In [27]:
# CISV
new_xlabels = ['0', r'$\frac{\pi}{2}$', r'$\pi$']
for i in range(3, 6):
    if i % 2 == 0:
        new_xlabels.append(f'${int(i/2)}\\pi$')
    else:
        new_xlabels.append(f'$\\frac{{{i}\\pi}}{{2}}$')

ax.set_xticklabels(new_xlabels)
Out[27]:
[Text(-1.0, 0, '0'),
 Text(0.0, 0, '$\\frac{\\pi}{2}$'),
 Text(1.0, 0, '$\\pi$'),
 Text(2.0, 0, '$\\frac{3\\pi}{2}$'),
 Text(3.0, 0, '$2\\pi$'),
 Text(4.0, 0, '$\\frac{5\\pi}{2}$')]

C: Advanced Tick Formatting

More advanced control over tick formatting is done through

ax.tick_params(axis='both', **kwargs)

where **kwargs is a sequence of keyword-arguments that allow you to mess with a lot of different properties of the ticks and their labels.

Here are some examples.

  • axis can be one of 'x', 'y', and 'both' to indicate which axis to modify.
  • length and width take float arguments to specify the length width of tick marks, in points.
  • color, labelcolor, and colors take mpl color spec arguments to specify the colors of the ticks, the labels, or both.
  • pad takes a float to indicate the distance between the tick and the label in points.
  • labelsize takes a float to indicate the font size of the label.
In [28]:
# CISV
ax.tick_params(axis='x', labelcolor='m')
ax.tick_params(axis='x', color='y')
ax.tick_params(axis='x', labelsize=14)
ax.tick_params(axis='y', colors='limegreen', width=2, length=6)

If you ever turn in a plot that looks that stupid, you will get an F for the course.

Reset all the crap.

In [29]:
# CISV
ax.tick_params(axis='y', reset=True)

On to some more common things. Some like to have ticks along all four sides, pointing in. This can be accomplished using the parameters

  • direction, which can be 'in', 'out', or 'inout'
  • bottom, top, left, right, each of which takes a bool indicating whether to draw the respective marks.
In [30]:
# CISV
ax.tick_params(axis='both', direction='in', 
               bottom=True, left=True, right=True, top=True)

ax.tick_params also allows you to modify properties of gridlines, if applicable.


V: Subplots

We won't be using markers again for a while, so we can go back to the smoother data file.

In [31]:
t, V1, V2, V3 = np.loadtxt('data.csv', delimiter=',', 
                           unpack=True, skiprows=1)

A: The Basics

Create a figure as before.

In [32]:
fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)

Before, to add an Axes, we used

ax = fig.add_subplot(1,1,1)

The syntax is

ax = fig.add_subplot(num_rows, num_cols, position)

position is an integer, starting from 1 and incrementing in (English) reading order.

Thus, if we want to plot our three data sets on three different sets of axes, arranged as the first three parts of a two-by-two grid, we can do that:

In [33]:
# CISV
ax1 = fig.add_subplot(2,2,1)
ax1.plot(t/1e-6, V1)
ax2 = fig.add_subplot(2,2,2)
ax2.plot(t/1e-6, V2)
ax3 = fig.add_subplot(2,2,3)
ax3.plot(t/1e-6, V3)
Out[33]:
[<matplotlib.lines.Line2D at 0x7f317f8b2860>]

In this particular case, all three graphs should have the same labels, so the most efficient way to label them would be using a loop.

In [34]:
# CISV
for ax in [ax1, ax2, ax3]:
    ax.set_xlabel(r'$t$ ($\mu$s)')
    ax.set_ylabel(r'$V_\mathrm{C}$ (V)')

B: Linking Axes

Axes can be linked, or shared. Let's make a narrower figure where we vertically stack all three axes.

In [35]:
# CISV
fig = plt.figure(figsize=(4,6), tight_layout=True)
ax1 = fig.add_subplot(3,1,1)
ax1.plot(t/1e-6, V1)
ax2 = fig.add_subplot(3,1,2, sharex=ax1)
ax2.plot(t/1e-6, V2)
ax3 = fig.add_subplot(3,1,3, sharex=ax1)
ax3.plot(t/1e-6, V3)
Out[35]:
[<matplotlib.lines.Line2D at 0x7f317f885278>]

Apply our axis labels.

In [36]:
# CISV
ax3.set_xlabel(r'$t$ ($\mu$s)')
for ax in [ax1, ax2, ax3]:
    ax.set_ylabel(r'$V_\mathrm{C}$ (V)')

Often, since the x-axes are now linked, we won't want to label all three sets of ticks. Typically, only the bottom-most ticks are labeled. We can turn the tick labels on the other two graphs invisible as follows.

The following two methods are equivalent.

In [37]:
# CISV
for i1, i2 in zip(ax1.get_xticklabels(), ax2.get_xticklabels()):
    i1.set_visible(False)
    i2.set_visible(False)
In [38]:
# CISV
ax1.tick_params('x', labelbottom=False)
ax2.tick_params('x', labelbottom=False)

C: More Advanced Method: GridSpec

It's often convenient, when sharing axes, to use a GridSpec object. Instead of supplying the number of rows, the number of columns, and the position to fig.add_subplot, we first create GridSpecs:

gs = gridspec.GridSpec(<num_rows>, <num_cols>)

Now the positions in gs are accessed using typical index notation:

gs[<row>, <col>]

We can pass the GridSpec positions as arguments to fig.add_subplot:

ax<num> = fig.add_subplot(gs[<row>, <col>])
In [39]:
fig = plt.figure(figsize=(4,6), tight_layout=True)
gs = gridspec.GridSpec(3,1)

ax1 = fig.add_subplot(gs[0])
ax1.plot(t/1e-6, V1)
ax2 = fig.add_subplot(gs[1])
ax2.plot(t/1e-6, V2)
ax3 = fig.add_subplot(gs[2])
ax3.plot(t/1e-6, V3)
Out[39]:
[<matplotlib.lines.Line2D at 0x7f317f700f98>]

NOTE: Since our GridSpec had only one dimension (three rows, one column), we only need to specify one index, rather than two. If we had, say, a two-by-two grid, we would need two indices.

Now, we can turn the tick labels off using methods from before, but passing empty lists.

In [40]:
ax1.set_xticklabels([])
ax2.set_xticklabels([])
Out[40]:
[]

If we want to get rid of the vertical space space between the rows, we use

In [41]:
gs.update(hspace=0)

Note that the tick marks are now invisible (covered by lower graphs). We could fix this by making ticks point inward instead of outward:

In [42]:
ax1.tick_params(axis='x', direction='in')
ax2.tick_params(axis='x', direction='in')
ax3.tick_params(axis='x', direction='inout')

An alternative fix for the disappearing ticks (if you'd rather they still appear pointing outward) is to create the axes in reverse order:

In [43]:
fig = plt.figure(figsize=(4,6), tight_layout=True)
gs = gridspec.GridSpec(3,1)

# Exactly the same as before except that the order is reversed.
ax3 = fig.add_subplot(gs[2])
ax3.plot(t/1e-6, V3)
ax2 = fig.add_subplot(gs[1])
ax2.plot(t/1e-6, V2)
ax1 = fig.add_subplot(gs[0])
ax1.plot(t/1e-6, V1)

ax1.set_xticklabels([])
ax2.set_xticklabels([])

gs.update(hspace=0)

A third, possibly better, option, is to make the axes backgrounds transparent.

In [44]:
fig = plt.figure(figsize=(4,6), tight_layout=True)
gs = gridspec.GridSpec(3,1)

# Exactly the same as before except that the order is reversed.
ax1 = fig.add_subplot(gs[0])
ax1.plot(t/1e-6, V1)
ax2 = fig.add_subplot(gs[1])
ax2.plot(t/1e-6, V2)
ax3 = fig.add_subplot(gs[2])
ax3.plot(t/1e-6, V3)

ax1.set_xticklabels([])
ax2.set_xticklabels([])

ax1.set_facecolor((0,0,0,0))
ax2.set_facecolor((0,0,0,0))
ax3.set_facecolor((0,0,0,0))

gs.update(hspace=0)

VI: Configuring Default Fonts

We'll be playing around with our graph several times with all the same arguments but different defaults, so let's create a function which generates it.

In [45]:
def make_plot():
    fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
    ax = fig.add_subplot(1,1,1)
    ax.plot(t/1e-6, V1, label='Underdamped')
    ax.plot(t/1e-6, V2, label='Overdamped')
    ax.plot(t/1e-6, V3, label='Critically damped')
    ax.set_xlabel(r'$t$ ($\mu$s)')
    ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
    fig.legend()
    return fig, ax

Run it!

In [46]:
make_plot()
Out[46]:
(<Figure size 640x480 with 1 Axes>,
 <matplotlib.axes._subplots.AxesSubplot at 0x7f317f430b70>)

There are a couple of ways of specifying default values of various parameters. One is to use

mpl.rcParams[<key>] = <value>

Another is to create an 'matplotlibrc' file in the local directory or somewhere in a broader system path (finding the correct locations can be tricky), where the lines are

<key>  :  <val>

In the first method, <key> will be a string, with quotation marks around it. In the second method, quotation marks are not necessary around <key>.

Consistent font usage throughout a document is good (unless you want to look like a crazy person), so it's good to define defaults for fonts. Also, most publications print in serif font, rather than matplotlib's sans-serif default. So let's customize!

In [47]:
mpl.rcParams['font.family'] = 'serif'
mpl.rcParams['font.size'] = 10.0  # This is already default.
make_plot()
Out[47]:
(<Figure size 640x480 with 1 Axes>,
 <matplotlib.axes._subplots.AxesSubplot at 0x7f317f849dd8>)

The original default choice for the sans-serif font is whichever is first found from this list:

DejaVu Serif, Bitstream Vera Serif, New Century Schoolbook, Century Schoolbook L, 
Utopia, ITC Bookman, Bookman, Nimbus Roman No9 L, Times New Roman, Times, Palatino, 
Charter, serif

This is the initial value of mpl.rcParams['font.serif']. Revise it to look for a variant of Times first.

In [48]:
mpl.rcParams['font.serif'] = ['TeX Gyre Termes', 'Nimbus Roman No9 L',
                              'Times New Roman', 'Times']
make_plot()
Out[48]:
(<Figure size 640x480 with 1 Axes>,
 <matplotlib.axes._subplots.AxesSubplot at 0x7f317f3498d0>)

If the previous didn't work, run this.

mpl.font_manager._rebuild()

Then run the previous again.

At the same time, we should choose an equivalent math font. The following options are available:

'dejavusans' (default), 'dejavuserif', 'cm' (Computer Modern), 'stix',
'stixsans' or 'custom'

These are possible values of mpl.rcParams['mathtext.fontset']. 'custom' is more trouble than it's worth, at least for our purposes. The closest to Times is stix.

In [49]:
mpl.rcParams['mathtext.fontset'] = 'stix'
make_plot()
Out[49]:
(<Figure size 640x480 with 1 Axes>,
 <matplotlib.axes._subplots.AxesSubplot at 0x7f317f2b27f0>)

For better formatting, we can have all text processed through LaTeX. It's much slower to do it this way, but it looks better. I'd recommend enabling this option right before creating figures for the final product.

In [50]:
mpl.rcParams['text.usetex'] = True

# The following is comma-separated list of LaTeX statements, in
# raw-string form, to include in the preamble. WARNING: the matplotlib
# folks recommend against using it---it can cause unexpected failures
# and is not supported.

#mpl.rcParams['text.latex.preamble'] = [r'\usepackage{libertine}', 
#                                       r'\usepackage{libertinust1math}', 
#                                       r'\usepackage[T1]{fontenc}']

#mpl.rcParams['text.latex.preamble'] = [r'\usepackage{newtxmath}', 
#                                       r'\usepackage{amsmath}', 
#                                       r'\usepackage[T1]{fontenc}']

#mpl.rcParams['text.latex.preamble'] = []
                                       
make_plot()
Out[50]:
(<Figure size 640x480 with 1 Axes>,
 <matplotlib.axes._subplots.AxesSubplot at 0x7f317f2ad6d8>)

VII: Customizing Legends

A: Positioning

Before continuing, we'll be using markers again, so let's go back to the less dense data.

In [51]:
t, V1, V2, V3 = np.loadtxt('data_sparse.csv', delimiter=',', 
                           unpack=True, skiprows=1)

NOTE: Much of what follows applies to positioning manually-placed text inside figures.

The ax.legend method takes several optional arguments. Here are the three most useful for positioning the legend.

  1. loc : most commonly a string indicating which point on the legend's box will be the anchor (i.e. the point that will be placed at the position specified by the next arguments). It can also be an int code corresponding to one of the strings (but this makes code harder to read, so don't do that). Anyway, the options are as follows.
Code Location String
0 'best'
1 'upper right'
2 'upper left'
3 'lower left'
4 'lower right'
5 'right'
6 'center left'
7 'center right'
8 'lower center'
9 'upper center'
10 'center'
  1. bbox_to_anchor : A tuple of float specifying the point at which loc should be located, in "axis coordinates". The lower left corner of the axes is at (0,0), and the upper right corner is at (1,1).

    Then to put the center of the legend in the middle of the plot area, use

    ax.legend(loc='center', bbox_to_anchor=(0.5,0.5))
    

    To have the center-top of the legend at the top of the plot area, centered on the right-hand plot-area line, use

    ax.legend(loc='upper center', bbox_to_anchor=(1,1))
    

    In the second case, you will have to take some care when saving.

In [52]:
fig = plt.figure(figsize=(6.4,4.8), tight_layout=True)
ax = fig.add_subplot(1,1,1)
ax.plot(t/1e-6, V1, 'slateblue', label='Underdamped')
ax.plot(t/1e-6, V2, 'r-.', label='Overdamped')
ax.plot(t/1e-6, V3, 'k.-', label='Critically damped')
ax.set_xlabel(r'$t$ ($\mu$s)')
ax.set_ylabel(r'$V_\mathrm{C}$ (V)')
ax.legend(loc='center', bbox_to_anchor=(0.5,0.5))
Out[52]:
<matplotlib.legend.Legend at 0x7f317f18e630>
In [53]:
ax.legend(loc='upper center', bbox_to_anchor=(1.0,1.0))
Out[53]:
<matplotlib.legend.Legend at 0x7f317f13e4e0>
In [54]:
ax.legend(loc='lower right', bbox_to_anchor=(1.0,0.0))
Out[54]:
<matplotlib.legend.Legend at 0x7f317f0e37b8>
  1. bbox_transform : an object which changes which coordinates are used for anchoring. If None (the default), "axes coordinates" are used. There are two other (relatively) common options.

    1. Data coordinates : specify points according to the actual values of the data in the plot (this is especially useful for positioning text). Use

      bbox_transform = ax.transData
      
    2. Figure coordinates : specify points relative to the figure, so now instead of (1,1) giving the top right corner of the box that bounds the plot area, it gives the top-right corner of the whole figure. Use

      bbox_transform = fig.transFigure
      
In [55]:
ax.legend(loc='lower right', bbox_to_anchor=(315,-0.1),
          bbox_transform=ax.transData)
Out[55]:
<matplotlib.legend.Legend at 0x7f317f0f2d30>
In [56]:
ax.legend(loc='upper right', bbox_to_anchor=(1,1),
          bbox_transform=fig.transFigure)
Out[56]:
<matplotlib.legend.Legend at 0x7f317f15c208>

Another important, related parameter is ncol, which should be an integer representing the number of columns in the legend.

In [57]:
ax.legend(loc='lower center', bbox_to_anchor=(0.5,1), ncol=3)
Out[57]:
<matplotlib.legend.Legend at 0x7f317f114828>

B: Titling

The whole legend can be given a title using the title parameter.

In [58]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), title='Graphs')
Out[58]:
<matplotlib.legend.Legend at 0x7f317f0aa4a8>

C: Formatting

NOTE: Most of the following should not be changed from graph to graph within a given project (for the sake of consistency). They should be set at the level of matplotlib defaults, rather than in each legend. However, especially when you are dealing with small figures, breaking consistency may be necessary to make the figure more legible.

After each command is the appropriate rcParams command to set the default, as a comment, with the default value given.

In any case, if None is specified, the default is used.

  1. frameon : None or bool

    Whether to draw a frame around the legend.

In [59]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), frameon=False)
#mpl.rcParams['legend.frameon'] = True
Out[59]:
<matplotlib.legend.Legend at 0x7f317f0e3550>
  1. fancybox : None or bool

    Whether to curve the corners or not.

In [60]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), fancybox=False)
#mpl.rcParams['legend.fancybox'] = True
Out[60]:
<matplotlib.legend.Legend at 0x7f317f0cc160>
  1. shadow : None or bool

    Whether to draw a shadow under the legend.

In [61]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), shadow=True)
#mpl.rcParams['legend.shadow'] = False
Out[61]:
<matplotlib.legend.Legend at 0x7f317f0800f0>
  1. framealpha : None or float

    The transparency of the background.

In [62]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), framealpha=1)
#mpl.rcParams['legend.framealpha'] = 0.8
Out[62]:
<matplotlib.legend.Legend at 0x7f317f0800b8>
  1. facecolor : None or a color spec

    The color of the background. "color spec" means a color specified using any of the methods described above.

In [63]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), facecolor='slateblue')
#mpl.rcParams['legend.facecolor'] = 'inherit'  # the facecolor is the same
#                                              # as that of the owning Axis
Out[63]:
<matplotlib.legend.Legend at 0x7f317f06e358>
  1. edgecolor : None or a color spec

    The color of the legend border. "color spec" means a color specified using any of the methods described above.

In [64]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), edgecolor='m')
#mpl.rcParams['legend.edgecolor'] = 0.8
Out[64]:
<matplotlib.legend.Legend at 0x7f317f090160>
  1. fontsize : int or float or str

    The size of the font. If an int or a float, it will be taken as an absolute font size it points.

    If a str, it should be one of 'xx-small', 'x-small', 'small', 'medium', 'large', 'x-large', 'xx-large'.

In [65]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), fontsize=12)
#mpl.rcParams['legend.fontsize'] = 'medium'  # same as default font size
Out[65]:
<matplotlib.legend.Legend at 0x7f317f081a58>
In [66]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), fontsize='x-small')
Out[66]:
<matplotlib.legend.Legend at 0x7f317f5c5240>
  1. handlelength : None or float

    The length of the line/point sample in font-size units.

In [67]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), handlelength=1)
#mpl.rcParams['legend.handlelength'] = 2.0
Out[67]:
<matplotlib.legend.Legend at 0x7f317f590e80>
  1. numpoints : None or int

    The number of markers to show on a scatter-plot line/point sample.

In [68]:
ax.legend(loc='center', bbox_to_anchor=(1,0.5), numpoints=2)
#mpl.rcParams['legend.numpoints'] = 1
Out[68]:
<matplotlib.legend.Legend at 0x7f317f590a20>

There are several others (mostly involving spacing) that we won't go into. Check them out on the Internet if you want.

There's also an option to rotate tick labels. This is sometimes useful if one of your axes contains something fairly long (e.g. dates).


VIII: Saving Figures

Figures are saved using

fig.savefig('filename.ext')

where the ext determines the file type. For most use cases, the best option is .pdf. Other options include .eps, .png, .jpg, among others.

In [ ]:
fig.savefig('output.pdf')

In some cases, if the legend hangs outside of the axes, it will get clipped. In that case, you need to get the legend as a separate object, and tell the figure about it.

In [ ]:
leg = ax.legend()
fig.savefig('output.pdf', bbox_extra_artists=(leg,))