Matplotlib Text and Annotate โ€” A Simple Guide

You’d like to add text to your plot, perhaps to explain an outlier or label points. Matplotlib‘s text method allows you to add text as specified coordinates. But if you want the text to refer to a particular point, but you don’t want the text centered on that point? Often you’ll want the text slightly below or above the point it’s labeling. In that situation, you’ll want the annotate method. With annotate, we can specify both the point we want to label and the position for the label.

Basic text method example

Let’s start with an example of the first situation – we simply want to add text at a particular point on our plot. The text method will place text anywhere you’d like on the plot, or even place text outside the plot. After the import statement, we pass the required parameters – the x and y coordinates and the text.

import matplotlib.pyplot as plt

x, y, text = .5, .5, "text on plot"

fig, ax = plt.subplots()
ax.text(x, y, text)
x, y, text = 1.3, .5, "text outside plot"
ax.text(x, y, text)
Text(1.3, 0.5, 'text outside plot')

Changing the font size and font color

We can customize the text position and format using optional parameters. The font itself can be customized using either a fontdict object or with individual parameters.

x, y, text = .3, .5, "formatted with fontdict"
fontdict = {'family': 'serif', 'weight': 'bold', 'size': 16, 'color' : 'green'}
fig, ax = plt.subplots()
ax.text(x, y, text, fontdict=fontdict)
x, y, text = .2, .2, "formatted with individual parameters"
ax.text(x, y, text, fontsize = 12, color = 'red', fontstyle = 'italic')
Text(0.2, 0.2, 'formatted with individual parameters')

How to change the text alignment?

We specify the xy coordinates for the text, but of course, the text can’t fit on a single point. So is the text centered on the point, or is the first letter in the text positioned on that point? Let’s see.

fig, ax = plt.subplots()
ax.set_title("Different horizonal alignment options when x = .5")
ax.text(.5, .8, 'ha left', fontsize = 12, color = 'red', ha = 'left')
ax.text(.5, .6, 'ha right', fontsize = 12, color = 'green', ha = 'right')
ax.text(.5, .4, 'ha center', fontsize = 12, color = 'blue', ha = 'center')
ax.text(.5, .2, 'ha default', fontsize = 12)
Text(0.5, 0.2, 'ha default')

The text is left horizontal aligned by default. Left alignment positions the beginning of the text is on the specified coordinates. Center alignment positions the middle of the text on the xy coordinates. Right alignment positions the end of the text on the coordinates.

Creating a text box

The fontdict dictionary object allows you to customize the font. Similarly, passing the bbox dictionary object allows you to set the properties for a box around the text. Color values between 0 and 1 determine the shade of gray, with 0 being totally black and 1 being totally white. We can also use boxstyle to determine the shape of the box. If the facecolor is too dark, it can be lightened by trying a value of alpha closer to 0.

fig, ax = plt.subplots()
x, y, text = .5, .7, "Text in grey box with\nrectangular box corners."
ax.text(x, y, text,bbox={'facecolor': '.9', 'edgecolor':'blue', 'boxstyle':'square'})
x, y, text = .5, .5, "Text in blue box with\nrounded corners and alpha of .1."
ax.text(x, y, text,bbox={'facecolor': 'blue', 'edgecolor':'none', 'boxstyle':'round', 'alpha' : 0.05})
x, y, text = .1, .3, "Text in a circle.\nalpha of .5 darker\nthan alpha of .1"
ax.text(x, y, text,bbox={'facecolor': 'blue', 'edgecolor':'black', 'boxstyle':'circle', 'alpha' : 0.5})
Text(0.1, 0.3, 'Text in a circle.\nalpha of .5 darker\nthan alpha of .1')

Basic annotate method example

Like we said earlier, often you’ll want the text to be below or above the point it’s labeling. We could do this with the text method, but annotate makes it easier to place text relative to a point. The annotate method allows us to specify two pairs of coordinates. One xy coordinate specifies the point we wish to label. Another xy coordinate specifies the position of the label itself. For example, here we plot a point at (.5,.5) but put the annotation a little higher, at (.5,.503).

fig, ax = plt.subplots()
x, y, annotation = .5, .5, "annotation"
ax.title.set_text = "Annotating point (.5,.5) with label located at (.5,.503)"
ax.scatter(x,y)
ax.annotate(annotation,xy=(x,y),xytext=(x,y+.003))
Text(0.5, 0.503, 'annotation')

Annotate with an arrow

Okay, so we have a point at xy and an annotation at xytext. How can we connect the two? Can we draw an arrow from the annotation to the point? Absolutely! What we’ve done with annotate so far looks the same as if we’d just used the text method to put the point at (.5, .503). But annotate can also draw an arrow connecting the label to the point. The arrow is styled by passing a dictionary to arrowprops.

fig, ax = plt.subplots()
x, y, annotation = .5, .5, "annotation"
ax.scatter(x,y)
ax.annotate(annotation,xy=(x,y),xytext=(x,y+.003),arrowprops={'arrowstyle' : 'simple'})
Text(0.5, 0.503, 'annotation')

Adjusting the arrow length

It looks a little weird to have the arrow touch the point. How can we have the arrow go close to the point, but not quite touch it? Again, styling options are passed in a dictionary object. Larger values from shrinkA will move the tail further from the label and larger values of shrinkB will move the head farther from the point. The default for shrinkA and shrinkB is 2, so by setting shrinkB to 5 we move the head of the arrow further from the point.

fig, ax = plt.subplots()
x, y, annotation = .5, .5, "annotation"
ax.scatter(x,y)
ax.annotate(annotation,xy=(x,y),xytext=(x,y+.003),arrowprops={'arrowstyle' : 'simple', 'shrinkB' : 5})
Text(0.5, 0.503, 'annotation')

Do the annotate and text methods have the same styling options?

Yes, all the parameters that work with text will also work with annotate. So, for example, we can put the annotation in a text box and set the fontstyle as italic, the same way as we did above.

fig, ax = plt.subplots()
x, y, text = .5, .7, "Italic text in grey box with\nrectangular box corner\ndemonstrating that the\nformatting options\nthat work with text\nalso work with annotate."
ax.scatter(x,y)
ax.annotate(text, xy=(x,y),xytext=(x,y+.01)
            ,fontstyle = 'italic'
            ,bbox={'facecolor': '.9', 'edgecolor':'blue', 'boxstyle':'square', 'alpha' : 0.5}
            ,arrowprops={'arrowstyle' : 'simple', 'shrinkB' : 5})
Text(0.5, 0.71, 'Italic text in grey box with\nrectangular box corner\ndemonstrating that the\nformatting options\nthat work with text\nalso work with annotate.')

Are there any shorthands for styling the arrow?

Yes, arrowstyle can be used instead of the other styling keys. More options here including 'fancy', 'simple', '-' and '->'.

fig, ax = plt.subplots()
x, y, annotation = .5, .5, "wedge style"
ax.scatter(x,y)
ax.annotate(annotation,xy=(x,y),xytext=(x,y+.01),arrowprops={'arrowstyle':'wedge'})
another_annotation = '- style'
ax.annotate(another_annotation,xy=(x,y),xytext=(x,y-.01),arrowprops={'arrowstyle':'-'})
Text(0.5, 0.49, '- style')

How can we annotate all the points on a scatter plot?

We can first create 15 test points with associated labels. Then loop through the points and use the annotate method at each point to add a label.

import random
random.seed(2)

x = range(15)
y = [element * (2 + random.random()) for element in x]
n = ['label for ' + str(i) for i in x]

fig, ax = plt.subplots()
ax.scatter(x, y)

texts = []
for i, txt in enumerate(n):
    ax.annotate(txt, xy=(x[i], y[i]), xytext=(x[i],y[i]+.3))

Handling overlapping annotations

The annotations are overlapping each other. How do we prevent that? You could manually adjust the location of each label, but that would be very time-consuming. Luckily the python library adjustText will do the work for us. You’ll have to pip install it first, and we’ll need to store the annotations in a list so that we can pass them as an argument to adjust_text. Doing this, we see for example that “label for 6” is shifted to the left so that it no longer overlaps with “label for 7.”

from adjustText import adjust_text

fig, ax = plt.subplots()
ax.scatter(x, y)

texts = []
for i, txt in enumerate(n):
    texts.append(ax.annotate(txt, xy=(x[i], y[i]), xytext=(x[i],y[i]+.3)))
    
adjust_text(texts)
226

Conclusion

You should now be able to position and format text and annotations on your plots. Thanks for reading! Please check out my other work at LearningTableau, PowerBISkills, and DataScienceDrills.