Matplotlib: beautiful plots with style

Example charts using the Matplotlib BMH style

Matplotlib is both powerful and complex: being able to adjust every aspect of a plot is powerful, but it's often time-consuming and complex to create a beautiful plot. The Matplotlib 1.5 release makes it easier to achieve aesthetically pleasing results by incorporating a set of styles [1] .

In this post I'm going to cover setting up a style, demonstrate some of the different styles in action and show how it's easy to alter matplotlibs settings to suit your own tastes.

Each style creates a common look that can be easily applied to all the different plot types. They alter all the main visual aspects of the plot such as xticks, legends and labels. There are 21 styles in the Matplotlib 1.5.1 release, they can be listed by doing:

In [1]: import matplotlib as plt
In [2]: plt.style.available
Out[2]: # Big list of styles

The ones with distinctive looks are:

Example charts using the Matplotlib seaborn-white style

The figures in this post show each of these main styles, plotting some data about an imaginary Product A's sales performance in a year. The first plot is a simple bar chart showing sales by financial quarter, the second plot is a histogram showing how long it takes to sell our imaginary product, and the final plot is a line chart showing how the different marketing channels are creating leads for sales.

Simple bar chart using the BMH style

There are two ways to set styles, at a library level or for a specific plot. If a style is set at a global level it will effect all figures and plots that are created. For example, if you create multiple plots in a jupyter notebook then they'll all be displayed using the style that's been defined. The call is:

plt.style.use('fivethirtyeight')

An alternative, is to set the style for the specific plot:

with plt.style.context('ggplot'):
    # plot command goes here
    plt.bar([1, 2, 3, 4], [5, 9, 18, 7])

This latter approach is advantageous if you don't want to change every plot that you're creating. I've also found that the fivethirtyeight style changes the legend box in a way I can't revert, but using it with plt.style.context() doesn't change my legend settings.

The styles create much better looking charts, but they don't get us all the way to beautiful plots. Some further alterations, particularly around font selection and font size are necessary. I'm making fairly basic changes, but you can go much further and create your own theme [5].

Simple bar chart using the ggplot style

The default is for Matplotlib to use a sans-serif font for describing the text and marking up the plot, with a different font for Maths mark-up [6]. It's possible to change these settings by specifying the font and text properties: the common aspects to define are the font type, weight, style, size and colour. The most specific way, is to change the properties of a particular command:

plt.title("Some title", fontname='Ubuntu', fontsize=14,
            fontstyle='italic', fontweight='bold',
            fontcolor='green')

The parameters are pretty self-explanatory, with them we're telling Matplotlib to gives us a title using the Ubuntu font at 14 points with italic, bold and green text. This provides a great deal of control over the look of each command, particularly when used with the other properties that are available. However, the downside is that every command becomes quite long-winded.

The alternative to specifying the characteristics at a method level, is to define global settings.

Matplotlib can be configured through global settings which are defined in a configuration file, or at runtime [7]. The pyplot interface to the runtime interface is through pylot.rcParams [8] dictionary. The common aspects I override are:

plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.serif'] = 'Ubuntu'
plt.rcParams['font.monospace'] = 'Ubuntu Mono'
plt.rcParams['font.size'] = 10
plt.rcParams['axes.labelsize'] = 10
plt.rcParams['axes.labelweight'] = 'bold'
plt.rcParams['xtick.labelsize'] = 8
plt.rcParams['ytick.labelsize'] = 8
plt.rcParams['legend.fontsize'] = 10
plt.rcParams['figure.titlesize'] = 12
Simple bar chart using the fivethirtyeight style

The font.family specifies that we're using a serif font, if no other setting was used then Matplotlib would use the default serif font that was available. The font.serif and font.monospace set which ever font we want to use, in my case 'Ubuntu' and 'Ubuntu Mono'. We've set the default font size (font.size) to be 10 points, so any size that's not set will use this. The axes commands tell matplotlib to use 10 points and bold for the axes labels (e.g. Sales and Time (FY) in our example plot). The xtick.labelsize and ytick.labelsize sets the numbers along the axis (e.g. Q1 in our example plot), it uses the monospace font that was set earlier. The figure.titlesize specifies the overall figure title.

The pyplot.rcParams interface provides extensive access to configuring matplotlib, these are just the basics, so it's worth looking through. It covers the vast majority of what I want to configure, the only item I haven't been able to alter is the style (e.g. italics) of some titles.

Styles and Fonts example

The following example is the code that was used to create the different figures scattered through this post. Nothing too complex, the only alteration is for fivethirtyeight style to work properly we have to alter the axes a bit: it's personal taste, I just don't like the axis line cutting through the marker for points that are at zero.

import matplotlib.pyplot as plt

# Set the style globally
# Alternatives include bmh, fivethirtyeight, ggplot,
# dark_background, seaborn-deep, etc
plt.style.use('seaborn-white')

plt.rcParams['font.family'] = 'serif'
plt.rcParams['font.serif'] = 'Ubuntu'
plt.rcParams['font.monospace'] = 'Ubuntu Mono'
plt.rcParams['font.size'] = 10
plt.rcParams['axes.labelsize'] = 10
plt.rcParams['axes.labelweight'] = 'bold'
plt.rcParams['axes.titlesize'] = 10
plt.rcParams['xtick.labelsize'] = 8
plt.rcParams['ytick.labelsize'] = 8
plt.rcParams['legend.fontsize'] = 10
plt.rcParams['figure.titlesize'] = 12

# Set an aspect ratio
width, height = plt.figaspect(1.68)
fig = plt.figure(figsize=(width,height), dpi=400)

# Product sales plot
ax1 = plt.subplot(221)
ax1.bar([1, 2, 3, 4], [125, 100, 90, 110], label="Product A", width=0.8,
            align='center')
plt.xticks([1, 2, 3, 4], ['Q1', 'Q2', 'Q3', 'Q4'])
plt.xlabel('Time (FY)')
plt.ylabel('Sales')
# Font style isn't accessible through rcParams
ax1.set_title("Product A sales", fontstyle='italic')

# Opportunities by age plot
ax2 = plt.subplot(222)
population_ages = [1,2,
                   10,11,13,14,14,
                   20,20,21,21,22,22,22,23,25,25,25,25,25,27,27,
                   30,30,30,31,32,32,32,33,33,33,33,34,34,34,34,34,34,36,37,38,39,
                   41,41,42,42,42,43,45,45,49,
                   55,57,59,
                   72,]
bins = [0,10,20,30,40,50,60]
ax2.hist(population_ages, bins, histtype='bar', rwidth=0.8)
plt.xlabel('Age (days)')
plt.ylabel('Closed sales')
ax2.set_title('Opportunities age', fontstyle='italic')

# Marketing channels line plot
ax3 = plt.subplot(2,2,(3,4))
y_series = [1,2,3,4,5]
x_1 = [5,9,9,12,9]
x_2 = [4,3,12,8,14]
x_3 = [1,3,4,2,5]
ax3.plot(y_series, x_1, linewidth=2, linestyle=':', marker='o', label='web')
ax3.plot(y_series, x_2, linewidth=2, linestyle='--', marker='v', label='telemarketing')
ax3.plot(y_series, x_3, linewidth=2, linestyle='-.', marker='s', label='sales team')
plt.xlabel('Months')
plt.ylabel('Marketing leads')
plt.xticks([1,2,3,4,5], ['Oct', 'Nov', 'Dec', 'Jan', 'Feb'])
#ax2.tick_params(axis='x', pad=8)
leg=plt.legend(loc='best', numpoints=1, fancybox=True)

# Axes alteration to put zero values inside the figure Axes
# Avoids axis white lines cutting through zero values - fivethirtyeight style
xmin, xmax, ymin, ymax = ax3.axis()
ax3.axis([xmin-0.1, xmax+0.1, ymin-0.1, ymax+0.4])
ax3.set_title('Leads by channel', fontstyle='italic')

# Space plots a bit
plt.subplots_adjust(hspace=0.25, wspace=0.40)

fig.suptitle("Seaborn-white style example")
plt.savefig('seaborn-style.svg', bbox_inches='tight')

My favourites are ggplot and fivethirtyeight as they're at the more sparse end of the scale. However, my needs aren't for scientific levels of precision and everyone has their own aesthetic sense.


[1]What's new in Matplotlib 1.5
[2]Controlling figure aesthetics
[3]Bayesian Methods for Hackers
[4]Ggplot2 homepage
[5]For more see Customizing Matplotlib's Plotting Styles and Customizing with style sheets
[6]Writing mathematical expressions in Matplotlib
[7]See Customising Matplotlib for the section on the matplotlibrc file.
[8]matplotlib.rcParams documentation

Posted in Tech Saturday 27 February 2016
Tagged with Python Matplotlib