Images can be used to highlight the strengths of your visualization in addition to pure data values. Many examples have proven that by using symbolic images, we map deeper into the viewer's mental model, thereby helping the viewer to remember the visualizations better and for a longer time. One way to do this is to place images where your data is, to map the values to what they represent. The matplotlib
library is capable of delivering this functionality, and here we demonstrate how to do it.
We will use the fictional example from the story The Gospel of the Flying Spaghetti Monster, by Bobby Henderson, where the author correlates the number of pirates with the sea-surface temperature. To highlight this correlation, we will display the size of the pirate ship proportional to the value representing the number of pirates in the year the sea-surface temperature is measured.
We will use Python matplotlib library's ability to annotate using images and text with advanced location settings, as well as arrow capabilities.
All the files required in the following recipe are available in the source code repository in the Chapter06
folder.
The following example shows how to add an annotation to a chart using images and text:
import matplotlib.pyplot as plt from matplotlib._png import read_png from matplotlib.offsetbox import TextArea, OffsetImage, AnnotationBbox def load_data(): import csv with open('pirates_temperature.csv', 'r') as f: reader = csv.reader(f) header = reader.next() datarows = [] for row in reader: datarows.append(row) return header, datarows def format_data(datarows): years, temps, pirates = [], [], [] for each in datarows: years.append(each[0]) temps.append(each[1]) pirates.append(each[2]) return years, temps, pirates
After we have defined helper functions, we can approach the construction of the figure object and add subplots. We will annotate these for every year in the collection of years using the image of the ship, scaling the image to the appropriate size:
if __name__ == "__main__": fig = plt.figure(figsize=(16,8)) ax = plt.subplot(111) # add sub-plot header, datarows = load_data() xlabel, ylabel = header[0], header[1] years, temperature, pirates = format_data(datarows) title = "Global Average Temperature vs. Number of Pirates" plt.plot(years, temperature, lw=2) plt.xlabel(xlabel) plt.ylabel(ylabel) # for every data point annotate with image and number for x in xrange(len(years)): # current data coordinate xy = years[x], temperature[x] # add image ax.plot(xy[0], xy[1], "ok") # load pirate image pirate = read_png('tall-ship.png') # zoom coefficient (move image with size) zoomc = int(pirates[x]) * (1 / 90000.) # create OffsetImage imagebox = OffsetImage(pirate, zoom=zoomc) # create anotation bbox with image and setup properties ab = AnnotationBbox(imagebox, xy, xybox=(-200.*zoomc, 200.*zoomc), xycoords='data', boxcoords="offset points", pad=0.1, arrowprops=dict(arrowstyle="->", connectionstyle="angle,angleA=0,angleB=-30,rad=3") ) ax.add_artist(ab) # add text no_pirates = TextArea(pirates[x], minimumdescent=False) ab = AnnotationBbox(no_pirates, xy, xybox=(50., -25.), xycoords='data', boxcoords="offset points", pad=0.3, arrowprops=dict(arrowstyle="->", connectionstyle="angle,angleA=0,angleB=-30,rad=3") ) ax.add_artist(ab) plt.grid(1) plt.xlim(1800, 2020) plt.ylim(14, 16) plt.title(title) plt.show()
We start by creating a figure of a decent size, that is, 16 x 8. We need this size to fit the images we want to display. Now, we load our data from the file, using the csv
module. Instantiating the csv reader object, we can iterate over the data from the file row by row. Note that the first row is special, it is the header describing our columns. As we have plotted years on the x axis and temperature on the y axis, we read that:
xlabel, ylabel, _ = header
And use the following lines:
plt.xlabel(xlabel) plt.ylabel(ylabel)
We return the header
and datarows
lists from the load_data
function to the main caller.
Using the format_data()
function, we read every item in the list and add each separate entity (year, temperature, and number of pirates) into the relevant ID list for that entity.
Year is displayed along the x axis, while temperature is on the y axis. The number of pirates is displayed as an image of a pirate ship, and also to add precision the value is displayed.
We plot year/temperature values using the standard plot()
function, not adding anything more, apart from making the line a bit wider (2 pt).
We proceed then to add one image for every measurement and to illustrate the number of pirates for a given year. For this, we loop over the range of values of length (range(len(years)))
, plotting one black point on each year/temperature coordinate:
ax.plot(xy[0], xy[1], "ok")
The image of the ship is loaded from the file into a suitable array format using the read_png
helper function:
pirate = read_png('tall-ship.png')
We then compute the zoom coefficient (zoomc
) to enable us to scale the size of the image in proportion to the number of pirates for the current (pirates[x]
) measurement. We also use the same coefficient to position the image along the plot.
The actual image is then instantiated inside OffsetImage
—the image container with relative position to its parent (AnnotationBbox
).
AnnotationBbox
is an annotation-like class, but instead of displaying just text as with the Axes.annotate
function, it can display other OffsetBox
instances. This allows us to load an image or text object in an annotation and locate it at a particular distance from the data point, as well as allowing us to use the arrowing capabilities (arrowprops
) to precisely point to an annotated data point.
We supply the AnnotateBbox
constructor with certain arguments:
Imagebox
: This must be an instance of OffsetBox
(for example, OffsetImage
); it is the content of the annotation boxxy
: This is the data point coordinate that the annotation relates toxybox
: This defines the location of the annotation boxxycoords
: This defines what coordinating system is used by xy
(for example, data coordinates)boxcoords
: This defines what coordinating system is used by xybox
(for example, offset from the xy
location)pad
: This specifies the amount of paddingarrowprops
: This is the dictionary of properties for drawing an arrow connection from an annotation-bounding box to a data pointWe add text annotation to this plot, using the same data items from the pirates list with a slightly different relative position. Most of the arguments of the second AnnotationBbox
are the same—we adjust xybox
and pad to locate the text to the opposite side of the line. The text is inside the TextArea
class instance. This is similar to what we do with the image, but with text time.TextArea
and OffsetImage
inherit from the same OffsetBox
parent class.
We set the text in this TextArea
instance to no_pirates
and put it in our AnnotationBbox
.