Transformations in Matplotlib


Transformations in Matplotlib

Files for this tutorial can be found in this repository Folder

In Matplotlib there are four different coordinate spaces that are available. The most obvious, and most used, is the data coordinate system. This is where your data are being plotted. So if you are plotting a sin curve, your data will be in a coordinate system that likely has the limits 0.0 to 1.0, or a little beyond this.Matplotlib calls this the userland coordinatesystem. It is controlled by the xlim and ylim properties. These may be determined automatically or set by you (the user) through the set_xlim() and set_ylim() functions

The Axes coordinate system refers to the axes space of your plot, or subplot. If you have only one plot, the origin of the coordinate system is the bottom left-hand side (0,0) of your axes. The top right corner is (1.0,1.0). If you have multiple plots or subplots (say, four panels) then each subplot axes has its own origin in the bottom left-hand side at 0.0. You can move outside to the left by supplying negative values, or further to the right by using greater than 1.0 values. Same for the top and the bottom.

CoordinateTransformation Object
dataax.transData
axesax.transAxes
figurefig.transFigure
displayNone

Some Examples

Some examples may help clarify these two transformations. Let’s look at the data coordinate system first. As always you will need to import the matplotlib.pyplot library. We will also use numpy to generate some data for us

import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline

Create some data for the x and y axes.

x = np.arange(0,1,.005) #values from 0 to 5 at .005 intervals
y = np.cos(2.0*x*np.pi) #convert the x values to cosine for a nice curve

Now we can plot the values as a figure for a single subplot. First we will get the figure object and store it as a variable fig. Then get the axes object, called ax. Finally, use the ax.plot method to plot a line.

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x, y)
[<matplotlib.lines.Line2D at 0x8785b38>]

png

Matplotlib automatically selects the bounds of the data coordinates to fit the data. These can be changed by accessing the set_xlim() and set_ylim() functions. Say we want to make sure there is some space in the top and bottom of the graph.

fig = plt.figure()
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y)
[<matplotlib.lines.Line2D at 0x812e780>]

png

If we want to convert the data coordinates into display coordinates, we can use the ax.transData.transform() function to do this. We can also go backwards and get the data coordinates from display coordinates. First, we will set new figure to exact pixel dimensions (1200x1200 at 300 dpi) so we can better now where the display and data coordinates are intersecting. Let’s also plot a point to be sure where it goes

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y)
ax.plot(.5,0.0,'ro') #plot a red point at x=.5 and y=0
[<matplotlib.lines.Line2D at 0x922b438>]

png

For the most part, this point is at the center of our data coordinate system, so we would expect it to be about the center of the display coordinates. Let’s apply the transformation to find out.

print(ax.transData.transform((.5,0)))
[ 615.  615.]

The value is close to half of 1200, but because we have the axes to consider, it is 15 pixels more in both the width and height. Now if we go in the opposite direction we can see that we should get the same values as the red point

invax = ax.transData.inverted()
print(invax.transform((615,615)))
[  5.00000000e-01   2.22044605e-16]

If you recall your scientific notation, then the first value is .5. The second value is close to zero

Now we will experiment with the axes coordinate system. We will add a label according using the axes coordinates. As you recall, the origin is in the lower left corner. We will add a label in the lower left and upper right to show where these are placed in the figure.

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y)
ax.text(0.0,0.0,"(0,0)",transform=ax.transAxes,fontsize=12,fontweight='bold',
       ha='center',va='center')
ax.text(1.0,1.0,"(1,1)",transform=ax.transAxes,fontsize=12,fontweight='bold',
       ha='center',va='center')
<matplotlib.text.Text at 0x970ddd8>

png

The code is nearly the same, except at the bottom to pieces of text are being added. The first three arguments are: the x coordinate, y coordinate, and text string. The first piece of text is added at the origin, and the second at the top right-hand corner. The transform argument is added to specify which coordinate system the x and y coordinates are in. In both cases it is set to the axes coordinate system. Two properties for the text (fontsizeandfontweight) are added to help distinguish the added text from the axes labels. The horizontal and vertical alignments are set to zero, so the center of the text is placed exactly on the x and y coordinates. So what happens now when we add more panels to the figure? We need to make sure we specify which axes the text is associated with. Let’s convert this to a loop to create four subplots. We can use the same data in each. We’ll also double the height and width of the figure to make it a little more roomy.

fdpi = 300
fig = plt.figure(figsize=(2400/fdpi, 2400/fdpi), dpi=fdpi)
for i in range(1,5):
    ax = fig.add_subplot(2,2,i)
    ax.set_xlim(0, 1.0)
    ax.set_ylim(-1.2, 1.2)
    ax.plot(x, y)
    ax.text(0.0,0.0,"(0,0)",transform=ax.transAxes,fontsize=12,fontweight='bold',
           ha='center',va='center')
    ax.text(1.0,1.0,"(1,1)",transform=ax.transAxes,fontsize=12,fontweight='bold',
           ha='center',va='center')

png

Another approach may be that we want to highlight a specific region of our plot, but do not know the data coordinates. We could use the transform as above to go between display and data coordinates. We could also plot directly onto our axes coordinate system. In this case, let’s use a circle to call out a portion of our cosine line. First we need to import another class from matplotlib called patches. Patches are like polygons. We’ll use a circle patch.

import matplotlib.patches as patches

Then back to the single subplot figure we have been working with.

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y)
circ = patches.Circle((0.7, 0.4), 0.1, transform=ax.transAxes,
                      facecolor='none', edgecolor='k',linewidth=2.0)
ax.add_patch(circ)
<matplotlib.patches.Circle at 0x8703f60>

png

The circle is added at axes coordinates x=.7 and y=.4. That is .7 units from the left, and .4 units from the bottom. The transform is set to ax.transAxes to make sure we are drawing using the axes coordinates What about the case where we know the range of values we want to use in the data coordinates for one axis, and want to use axes coordinates for the other axis. In this case, we can use a blended transform to set this up. We need import another class for this.

import matplotlib.transforms as transforms

Then we need to create a blended transform factory that will be initialized using the two transformations we want:transData and transAxes. Again, we will use a patch, but a rectangle patch in this case. The x and y coordinates are for the lower left-hand corner of the rectangle. The width and x coordinate use the transData function, and the height and y coordinate use the transAxes function.

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y)
trans = transforms.blended_transform_factory(
    ax.transData, ax.transAxes)
rect = patches.Rectangle((0.4,0), width=.2, height=1,
                         transform=trans, color='yellow',
                         alpha=0.5)
ax.add_patch(rect)
<matplotlib.patches.Rectangle at 0xa0bcda0>

png

The axes coordinates run from 0 to 1. We can specify the y coordinate as zero for the bottom, and the height as 1 to go all the way to the top. If we used data coordinates to figure this we would have to know the ylim to achieve the same effect. Then anytime the ylim changed this would need to be changed to reflect it. It is much easier to use the axes coordinate to fill this space.

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y)
rect = patches.Rectangle((0.4,-1.2), width=.2, height=2.4,
                         transform=ax.transData, color='yellow',
                         alpha=0.5)
ax.add_patch(rect)
<matplotlib.patches.Rectangle at 0xa60a9e8>

png

Figure Transform

Legends Placement

The other transform is the figure transform. Again, the origin is the lower left-hand corner of the figure (0,0). The top right of the figure is (1,1). Let’s use transFigure to demonstrate

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y)
ax.text(0.0,0.0,"(0,0)",transform=fig.transFigure,fontsize=12,fontweight='bold',
       ha='center',va='center')
ax.text(1.0,1.0,"(1,1)",transform=fig.transFigure,fontsize=12,fontweight='bold',
       ha='center',va='center')
<matplotlib.text.Text at 0x877a9e8>

png

The figure includes the data area, and the axes. In a sense these coordinate systems are nested within each other. You can use transformations to move between them. Let’s use tight_layout() to see how this changes the placement

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y)
ax.text(0.0,0.0,"(0,0)",transform=fig.transFigure,fontsize=12,fontweight='bold',
       ha='center',va='center')
ax.text(1.0,1.0,"(1,1)",transform=fig.transFigure,fontsize=12,fontweight='bold',
       ha='center',va='center')
plt.tight_layout()

png

One feature of matplotlib that often uses figure coordinates is the legend. Often, it is enough to use the default legend position provided through the loc parameter (see the table below). The default location is the upper right.

Default Legend Positions Available in Matplotlib for the loc parameter
As StringAs Integer
best0
upper right1
upper left2
lower left3
lower right4
right5
center left6
center right7
lower center8
upper center9
center10

source: https://matplotlib.org/api/pyplot_api.html#matplotlib.pyplot.legend

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y,label="Cosine line")
plt.legend()
<matplotlib.legend.Legend at 0x9e3ea20>

png

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y,label="Cosine line")
plt.legend(loc=3)
<matplotlib.legend.Legend at 0x81a74a8>

png

When adding a legend, you can specify the bbox_to_anchor argument, the bbox_transform argument, and loc to really place the legend wherever you want. loc is like the alignment point of the text above. The default is the upper right corner of the legend bounding box. bbox_to_anchor is dependent on the transformation. It is the coordinates where the anchor (set by loc ) is going to be placed. bbox_transform tells matplotlib which transformation to apply, or which coordinate system to place the legend using.Confused? Let’s visualize it. The above figures use the location to specify the placement of the legend. Now let’s leave the location as the upper right corner of the legend bounding box (default) but change the bbox_to_anchor parameter.

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y,label="Cosine line")
ax.text(1.0,1.0,"Figure Upper Right",transform=fig.transFigure,fontsize=9,fontweight='bold',
       ha='center',va='center')
plt.legend(bbox_to_anchor=(1.0, 1.0), bbox_transform=fig.transFigure)
<matplotlib.legend.Legend at 0xb106208>

png

In the above figure the anchor is upper right, and the anchor point is set to 1.0,1.0. The transformation is the figure, and a piece of text is added there as well to show the upper right of the figure coordinate system. Below, the location is set to the center of the legend, but the anchor and coordinate system are left as figure upper right

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y,label="Cosine line")

plt.legend(bbox_to_anchor=(1.0, 1.0), loc=10, bbox_transform=fig.transFigure)

<matplotlib.text.Text at 0x9c6e9e8>

png

This allows for the placement at a wide range of places. Below we set the legend to outside the axes upper right corner, using the loc arguement set to two, or upper left. That is, the upper left of the legend bounding box. In the first figure the transform is left as the figure coordinate. The second figure this property is left as the default axes coordinates. Also note the bbox_to_anchor is set slightly higher than 1. That is slightly outside the 0 to 1 bounds. This is odd for the figure coordinate system, but with the axes coordinate system you are saying you want to be just outside the axis line. borderaxespad is set to zero to make the legend fit closer to the axis edge.

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y,label="Cosine line")
ax.text(1.0,1.0,"Figure Upper Right",transform=fig.transFigure,fontsize=9,fontweight='bold',
       ha='center',va='center')
plt.legend(bbox_to_anchor=(1.05, 1.0), loc=2, bbox_transform=fig.transFigure)

<matplotlib.legend.Legend at 0xc4fd780>

png

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y,label="Cosine line")

plt.legend(bbox_to_anchor=(1.05, 1.0), loc=2,borderaxespad=0.)
<matplotlib.legend.Legend at 0xbe46208>

png

Say we wanted to place this below the bottom x-axis. We know we want to use the axes coordinate system, so we can leave this as default. We also probably want to fill the space at the bottom rather than leave the legend as we have. Now we need to specify four coordinates. We’ll stick with the upper left anchor point for the legend box. Change the mode to “expand” to fill the space. Now we add our anchor locations. We’ll use negative values to set slightly outside the axes. -.02 places the upper left at -.02 axes x coordinate. -.05 is axes y coordinate. 1.02 is the right side of the axes x coordinate. And the 0 in the 4th spot of the tuple is to stop the legend from going up.

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y,label="Cosine line")

plt.legend(bbox_to_anchor=(-.02, -.05,1.02,0), loc=2, mode="expand")
<matplotlib.legend.Legend at 0xdc887f0>

png

Here are some extreme examples to show the effect of each setting on the bbox_to_anchor

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y,label="Cosine line")

plt.legend(bbox_to_anchor=(-.02, -.05,2,0), loc=2, mode="expand")
<matplotlib.legend.Legend at 0xdae7ba8>

png

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y,label="Cosine line")

plt.legend(bbox_to_anchor=(-.3, -.3,1,0), loc=2, mode="expand")
<matplotlib.legend.Legend at 0x8e582e8>

png

fdpi = 300
fig = plt.figure(figsize=(1200/fdpi, 1200/fdpi), dpi=fdpi)
ax = fig.add_subplot(111)
ax.set_xlim(0, 1.0)
ax.set_ylim(-1.2, 1.2)
ax.plot(x, y,label="Cosine line")

plt.legend(bbox_to_anchor=(-.025,0,1.05,.5), loc=2, mode="expand")
<matplotlib.legend.Legend at 0xe4d41d0>

png

Transformations is a complicated subject, because there are so many different coordinate systems involved. Hopefully having reviewed these here, you have a better understanding of them

Credit where credit is due. Much of this information is derived from the matplotlib help. Particularly these pages: https://matplotlib.org/users/transforms_tutorial.html and https://matplotlib.org/users/legend_guide.html