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.
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.
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.
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')
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})
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.
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 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:
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.
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')
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()
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')
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 |