Plotting with images

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.

Getting ready

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.

How to do it...

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()

The preceding code should give the following plot:

How to do it...

How it works...

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)

Tip

We used a neat Python convention here to unpack the header into three variables, where using _ for variable name, we indicate that we are not interested in the value of that variable.

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 box
  • xy: This is the data point coordinate that the annotation relates to
  • xybox: This defines the location of the annotation box
  • xycoords: 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 padding
  • arrowprops: This is the dictionary of properties for drawing an arrow connection from an annotation-bounding box to a data point

We 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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.223.170.223