Handling text in Matplotlib

Matplotlib provides a range of methods for describing and annotating a plot. There are methods to set elements (e.g. a title), as well as annotating anywhere on the figure. All the text handling methods use the same underlying object ensuring that the keyword arguments and formatting capabilities are consistent.

Text handling capabilities in Matplotlib

The limits of the matplotlib text object are that it formats the entire string, it can't format within a text string (e.g italicise some words). I'll cover some options for complex formatting (LaTeX and other solutions) in a later post - this post covers the standard capabilities [1]. The figure on the right shows the text capabilities in action, the code for generating it is at the bottom of this post.

The underlying object that handles text is matplotlib.text.Text() ensuring that all text handling methods are consistent. Text is dealt with at this object level, so a line of text can be given a specific font, size, style and colour. The major items we use text for on a plot are:

We'll cover each of these in turn.

Plot title

To set an overall title we use the pyplot.title() [2]. In the simplest form:

plt.title('A title for this plot')

The text object properties are all available, so we can format the string:

plt.title('A title for this plot', color='green', fontsize=14)

The Text properties section covers the formatting keywords in more detail.

Axis labels

Commonly, we want to label the axis so that it's clear what's being displayed. The methods are xlabel() [3] and ylabel() [4]:

plt.xlabel('Label for X')
plt.ylabel('Label for Y')

Using the formatting keywords:

plt.xlabel('Label for X', backgroundcolor='yellow', fontstyle='italic')

Axes text

Aside from describing the plot, the other use for text is to provide general notes. There are two categories of methods for this, text() and annotation(). The difference is that with annotation we can add an arrow to a point on the plot. We use matplotlib.axes.Axes.text() [5] method to provide general text any where within the Axes:

pyplot.text(x, y, string, fontdict, withdash, **kwargs)

Where x and y are the co-ordinates as measured by the scales of the Axes: if the figure's X axis is from 0-10, and the Y axis is 0-4, then to put some text in the top left we would do:

plt.text(2, 4,"A test string")

We can provide the standard text keyword arguments, for example:

plt.text(2, 4,"Test string", fontsize=14, fontname='Ubuntu', fontweight='bold')

We can also put the text within a bounding box:

plt.text(6, 1.5, 'Text box with a comment\nthat continues', style='italic',
    fontsize=10, fontname='Ubuntu',
    bbox={'facecolor':'grey', 'alpha':0.5, 'pad':10})

Figure text

The alternative to Axes.text() is to use the figtext() [6] function that lets you place text in any location on the figure. The main advantage is you can put text outside the Axes.

This method uses the whole of the figure for co-ordinates, where 0,0 is bottom left, and 1,1 is top right, so 0.5,0.5 is the middle of the figure. The function definition is:

pyplot.figtext(x, y, string, fontdict=None, **kwargs)

For example, if we wanted to call it to place some text underneath the axis with the text starting half way across the X axis, the call would be:

plt.figtext(0.5, 0,"Comment: Test string that is a note about something", wrap=True,
            horizontalalignment='center', fontsize=12)

The wrap argument tells Matplotlib that text should not go outside the figure width, and horizontalalignment means it places and measures the text in the centre.

Annotate

To annotate an element in the plot, we generally want to put the text on the figure and provide an arrow or a connector showing the area we're commenting on. There's an annotate() [7] method for this:

matplotlib.pyplot.annotate( string, xy, xytext, arrowprops, **kwargs)

The string is what you're annotating the figure with. The two XY elements are:

  • The location being annotated, the part of the plot we're pointing at: defined in the xy argument
  • The location of the text: defined in the xytext argument

The co-ordinates are data co-ordinates from the Axes: the documentation also covers using other co-ordinates systems. To start simply, lets say we have a figure that is from 0-10 on the X, and 0-4 on the Y, to comment on point 6, 3 in our plot we'd do:

plt.annotate('A note', xy=(6,3), xytext=(5,3.5))

The xy parameter is the point we're interested in on the plot (point 6, 3 in this example). The comment itself can be placed anywhere on the plot using the xytext parameter (point 5, 3.5) in this example.

To create an arrow from the text to the point we're annotating we use the arrowprops dictionary. It's optional (if you don't want an arrow you can leave it unset) and defines the line properties for the arrow. In the simplest form it would be:

plt.annotate('A note', xy=(6,3), xytest=(5,3.5), arrowprop=dict(arrowstyle='->'))

The slight complexity with the arrow properties is that they can be altered in two ways:

  • If an arrowstyle parameter is provided then a FancyArrowPatch object is created.
  • Otherwise a YAArrow object is created.

You can't mix the two together, so you can't use arrowstyle with width, headwidth or shrink parameters. If you're happy with the default arrow shape and want a simple way to alter just the basics then this is where YAArrow is useful:

Property Description
width Width of the arrow in points
headwidth The width of the base of the arrow head in points
shrink Move the tip and base some percent away from the annotated point and text
kwargs Any matplotlib.patches.Polygon argument
  Text keyword arguments can also be used

As an example of altering the arrow properties, we could make the arrow thinner, change the width of the arrow head and makes it green:

axes = plt.axes()
axes.annotate('A note', xy=(6,3), xytext=(5,3.5), arrowprops=dict(width=1, headwidth=8, facecolor='green',shrink=0.1))

For more complex alterations FancyArrowPatch is the way to go. The major properties are:

FancyArrowPatch Property Description
arrowstyle The arrowstyle
->
Open arrow head
-|>
Closed arrow head
-[
Square head
fancy Complex drawn arrow
simple Simple drawn arrow
<->
Any of the connector types for a double ended arrow.
connectionstyle Use this to define an arc or a right angle on the arrow. e.g connectionstyle="arc3,rad=0.2"
kwargs capstyle 'butt', 'round', 'projected'
color color for the arrow - overides edgecolor and facecolor parameters
edgecolor edges of the arrow color
facecolor arrow color
fill 'True' or 'False'
hatch Hatch to put in the arrow '/', '\', '|', '-', '+', 'X', 'o'
linestyle 'solid', 'dashed', 'dashdot', 'dotted', 'offset'
linewidth Width of the line in points

The main advantage of the arrowstyle option is that you can define both the look of the arrow, and do complex connections like putting in an arc or a right arrow. Slightly oddly you can define the shape of the arrow by providing a comma separated list in the arrowstyle [8] parameter:

arrowstyle='-|>, head_width=0.5',

Finally, it's possible to send Text() keyword properties to alter the format of the text. In this example we put a bounding box around the text and change the fontsize of the text:

axes = plt.axes
axes.annotate('A note', xy=(6,3), xytext=(5,3.5),
            arrowprops=dict(width=1, headwidth=8, facecolor='black', shrink=0.1),
            fontsize=14, bbox=dict(boxstyle="round", color='white',ec="0.5", alpha=1) )

See the Annotations Guide for much more complex capabilities such as curves on the arrow.

Text properties

All of the text handling methods and objects use the matplotlib.text.Text() [9] object underneath. This means all the text handling methods use the same keywords arguments to alter text properties. A constraint of this object is that you cannot mark-up individual words within the text string, any property applies to the whole string. The most important properties and their keywords are as follows:

Property Description
axes An axes instance
backgroundcolor A background colour for the text
bbox A box around, you provide a dictionary of values
color Colour of the text
horizontalalignment 'center', 'right', 'left'
multialignment Controls alignment of text on multiple lines 'left', 'right', 'center'
position(x,y) The position of the text
rotation Angle in degrees, 'vertical' or 'horizontal'
text The text to use
verticalalignment 'Center', 'top', 'bottom' or 'baseline'
fontname Font to use, fc-list to see availability
fontsize Size of the font in points
fontstyle 'normal', 'italic' or 'oblique'
fontweight 'normal', 'bold', 'heavy', 'light', 'ultrabold' 'ultralight'
wrap Set to 'True' it stops text going outside the Figure

The named colors are defined in the matplot documentation, the alternative is to HTML hexadecimal colours e.g '#ffff00'. Some examples:

plt.text(5, 5, 'Some text at an angle', rotation=45)
plt.title('A title in bold with a font', fontweight='bold', fontname='Linux Biolinum')
plt.xlabel('Label with yellow and grey', fontcolor='#FFFF00', backgroundcolor='grey')

Bounding box

Most of the text methods will accept a bounding box argument, this limits the size of the text within the plot and can be made visible to display a box around the text. It's called with the bbox argument, and a dictionary of values are provided. The method is defined as [10]:

matplotlib.patches.FancyBboxPatch(xy, height, boxstyle='round', bbox_transmuter=None,
                                    mutation_scale=1.0, mutation_aspect=None)

This looks complicated but as it's called indirectly through a text method we don't have to supply most of these arguments directly. The most important attribute is boxstyle which styles the look and padding of the box, a set of choices are provided e.g 'square' and 'round'. For example to provide a basic square box and make it grey around some text we do:

plt.text( 5, 5, 'A bounded piece of text', bbox=dict{boxstyle='square', color='grey'})

The most significant keyword properties are:

Property Description
alpha Transparency value, 0-1
boxstyle 'circle', 'darrow' (down arrow), 'larrow' (left arrow) 'rarrow', round (rounded edges), 'round4', 'roundtooth', 'sawtooth', 'square'
color colour, both facecolor and edgecolor
edgecolor or ec Edge color, lines around the outside
facecolor or fc Color of the box
fill True or False
hatch One of the hatch styles
linestyle One of the linestyles
linewidth or lw Linewidth in points

The properties are very extensive and can be defined even more precisely by providing a comma delimited list to the Boxstyle attribute which allows further configuration of aspects like padding. Further information in the BoxStyle [11] documentation.

To do more complex formatting we can access the FancyBboxPatch type directly by retrieving it from the text method, then using setter methods on it directly:

txtbox1 = plt.text( 5, 5, 'A bounded piece of text', bbox=dict{boxstyle='square', color='grey'})
boundb = t.get_bbox_patch()

Text example

The figure at the top show the text methods and many of the properties, the code used to create it is below:

import matplotlib.pyplot as plt
import textwrap as tw

plt.style.use('seaborn-notebook')
plt.rcParams['font.family'] = 'Ubuntu'

plt.figure(figsize=(8, 6), dpi=400)
plt.bar([1,2,3,4], [125,100,90,110], label="Product A", width=0.5,
            align='center')

# Text places anywhere within the Axis
plt.text(0.6, 130, 'Q1 accelerated with inventory refill',
         horizontalalignment='left', backgroundcolor='palegreen')

# The first line is escaped so that textwrap.dedent works correctly
comment1_txt = '''\
    Marketing campaign started in Q3 shows some impact in Q4. Further
    positive impact is expected in later quarters.
    '''
# We remove the indents and strip new lines from the text
# fill() creates the text with the specified width of 40 chars
annot_txt = tw.fill(tw.dedent(comment1_txt.rstrip()), width=40)

# Annotate using an altered arrowstyle for the head_width, the rest
# of the arguments are standard
plt.annotate(annot_txt, xy=(4,80), xytext=(1.50,105),
             arrowprops=dict(arrowstyle='-|>, head_width=0.5',
                             linewidth=2, facecolor='black'),
             bbox=dict(boxstyle="round", color='yellow', ec="0.5",
                       alpha=1), fontstyle='italic')

comment2_txt = '''\
    Notes: Sales for Product A have been flat through the year. We
    expect improvement after the new release in Q2.
    '''
fig_txt = tw.fill(tw.dedent(comment2_txt.rstrip() ), width=80)

# The YAxis value is -0.07 to push the text down slightly
plt.figtext(0.5, -0.07, fig_txt, horizontalalignment='center',
            fontsize=12, multialignment='left',
            bbox=dict(boxstyle="round", facecolor='#D8D8D8',
                      ec="0.5", pad=0.5, alpha=1), fontweight='bold')

# Standard description of the plot
plt.xticks([1,2,3,4],['Q1','Q2','Q3','Q4'])
plt.xlabel('Time')
plt.ylabel('Sales')
plt.title('Total sales by quarter', color='blue', fontstyle='italic')
plt.legend(loc='best')

plt.savefig('matplotlib-text-handling-example.svg', bbox_inches='tight')

Final words

Phew! That pretty much covers it. With these methods handling text is straightforward in Matplotlib, whether describing a plot or adding some notes within it. The standard text properties provide a simple way to specialise each command, and for more general settings plt.rcParams (covered in a previous post) provides a nice interface. The limitation is complex formatting within a text string, which I'll cover another time.

If you think I've missed something or misunderstood some element, please comment away!


[1]The beginners tutorial provides a full Text introduction
[2]matplotlib.pyplot.title() documentation
[3]Matplotlib.pyplot.xlabel() documentation
[4]Matplotlib.pyplot.ylabel() documentation
[5]matplotlib.axes.Axes.text() documentation
[6]matplotlib.pyplot.figtext() documentation
[7]matplotlib.axes.Axes.annotate() documentation
[8]matplotlib.patches.FancyArrowPatch documentation requires some careful reading to figure out that you can provide a comma-delimited list within the parameter.
[9]matplotlib.text.Text() documentation
[10]matplotlib.patches.FancyBboxPatch documentation
[11]matplotlib.patches.BoxStyle documentation

Posted in Tech Tuesday 01 March 2016
Tagged with Python Matplotlib