Using annotate to place a text box below legend in Matplotlib - python

I wish to display some text in a Matplotlib plot using annotate(), aligned beneath a legend box. I have examined the solution proposed in How to place a text box directly below legend in matplotlib? and can make an adaption that works for my situation, however adding additional axes etc. seems overkill for my situation. I simply wish to place an annotation whose top right corner is 20 pixels below the lower right corner of the legend box.
I also wish to save a PDF files of the plot.
Here is my code.
import matplotlib.pyplot as plt
import matplotlib.text
fig, ax = plt.subplots()
ax.plot([5,1], label="Label 1")
ax.plot([3,0], label="Label 2")
legend = ax.legend(loc="upper right")
# Create offset from lower right corner of legend box,
# (1.0,0) is the coordinates of the offset point in the legend coordinate system
offset = matplotlib.text.OffsetFrom(legend, (1.0, 0.0))
# Create annotation. Top right corner located -20 pixels below the offset point
# (lower right corner of legend).
ax.annotate("info_string", xy=(0,0),size=14,
xycoords='figure fraction', xytext=(0,-20), textcoords=offset,
horizontalalignment='right', verticalalignment='top')
# Draw the canvas for offset to take effect
fig.canvas.draw()
fig.savefig('plot1.png')
fig.savefig('plot1.pdf')
plt.show()
When the plot is first shown it looks correct, with the information text correctly beneath the legend. Also, the saved png file shows correctly. However, the text is missing in the pdf output.
I am not sure if there are gaps in my understanding, or if something is wrong. Can anyone shed some light why the pdf output is incorrect?
png output
pdf output
(matplotlib=2.2, python=2.7.13, Windows 7)

Related

How can I fix the space between a plot and legend so that new text doesn't change the spacing?

To be more specific, How can I fix the space between a plot and legend such that if more text is included the legend won't overlap the plot?
For example, if you look at the plots below, when I add more text from "back now"(Plot 1) to "let's break hu"(Plot 2) in the first legend entry -- the legend extends to the left where it begins to cover the plot.
Is there a way to keep the space between the plot and the legend fixed? So that when there is more text the figure extends to the right rather than onto the plot itself.
Plot 1, Plot 2
Code used for the legend:
lgd = ax.legend( patches, lgnd, loc="center right", bbox_to_anchor=(1.25, 0.5), prop=font )
Part of the answer you're looking for depends on how you made the legend and placed them in the first place. That's why people emphasize "Minimal, Complete, and Verifiable example".
To give you a simple introduction, you can control legend location using bbox_to_anchor, which accepts tuple of floats. Since you want to put the legend on the right, I would suggest using bbox_to_anchor with loc=2. This setting comes from bbox_transform. A simplified way to understand it is: bbox_to_anchor defines the relative location of the corner of the legend box and which corner of the 4 is defined by loc.
In the following example, it puts the "upper left" corner of the legend box at (1, 1) which is the upper right corner of the plot ((0,0) is the "lower left" corner of the plot). And to make it clear, I set borderaxespad to 0.
import matplotlib.pyplot as plt
plt.plot([1,2,3], label="test1")
plt.legend(bbox_to_anchor=(1, 1), loc=2, borderaxespad=0.)
plt.show()

Python - Legend overlaps with the pie chart

Using matplotlib in python. The legend overlaps with my pie chart. Tried various options for "loc" such as "best" ,1,2,3... but to no avail. Any Suggestions as to how to either exactly mention the legend position (such as giving padding from the pie chart boundaries) or at least make sure that it does not overlap?
The short answer is: You may use plt.legend's arguments loc, bbox_to_anchor and additionally bbox_transform and mode, to position the legend in an axes or figure.
The long version:
Step 1: Making sure a legend is needed.
In many cases no legend is needed at all and the information can be inferred by the context or the color directly:
If indeed the plot cannot live without a legend, proceed to step 2.
Step 2: Making sure, a pie chart is needed.
In many cases pie charts are not the best way to convey information.
If the need for a pie chart is unambiguously determined, let's proceed to place the legend.
Placing the legend
plt.legend() has two main arguments to determine the position of the legend. The most important and in itself sufficient is the loc argument.
E.g. plt.legend(loc="upper left") placed the legend such that it sits in the upper left corner of its bounding box. If no further argument is specified, this bounding box will be the entire axes.
However, we may specify our own bounding box using the bbox_to_anchor argument. If bbox_to_anchor is given a 2-tuple e.g. bbox_to_anchor=(1,1) it means that the bounding box is located at the upper right corner of the axes and has no extent. It then acts as a point relative to which the legend will be placed according to the loc argument. It will then expand out of the zero-size bounding box. E.g. if loc is "upper left", the upper left corner of the legend is at position (1,1) and the legend will expand to the right and downwards.
This concept is used for the above plot, which tells us the shocking truth about the bias in Miss Universe elections.
import matplotlib.pyplot as plt
import matplotlib.patches
total = [100]
labels = ["Earth", "Mercury", "Venus", "Mars", "Jupiter", "Saturn",
"Uranus", "Neptune", "Pluto *"]
plt.title('Origin of Miss Universe since 1952')
plt.gca().axis("equal")
pie = plt.pie(total, startangle=90, colors=[plt.cm.Set3(0)],
wedgeprops = { 'linewidth': 2, "edgecolor" :"k" })
handles = []
for i, l in enumerate(labels):
handles.append(matplotlib.patches.Patch(color=plt.cm.Set3((i)/8.), label=l))
plt.legend(handles,labels, bbox_to_anchor=(0.85,1.025), loc="upper left")
plt.gcf().text(0.93,0.04,"* out of competition since 2006", ha="right")
plt.subplots_adjust(left=0.1, bottom=0.1, right=0.75)
In order for the legend not to exceed the figure, we use plt.subplots_adjust to obtain more space between the figure edge and the axis, which can then be taken up by the legend.
There is also the option to use a 4-tuple to bbox_to_anchor. How to use or interprete this is detailed in this question: What does a 4-element tuple argument for 'bbox_to_anchor' mean in matplotlib?
and one may then use the mode="expand" argument to make the legend fit into the specified bounding box.
There are some useful alternatives to this approach:
Using figure coordinates
Instead of specifying the legend position in axes coordinates, one may use figure coordinates. The advantage is that this will allow to simply place the legend in one corner of the figure without adjusting much of the rest. To this end, one would use the bbox_transform argument and supply the figure transformation to it. The coordinates given to bbox_to_anchor are then interpreted as figure coordinates.
plt.legend(pie[0],labels, bbox_to_anchor=(1,0), loc="lower right",
bbox_transform=plt.gcf().transFigure)
Here (1,0) is the lower right corner of the figure. Because of the default spacings between axes and figure edge, this suffices to place the legend such that it does not overlap with the pie.
In other cases, one might still need to adapt those spacings such that no overlap is seen, e.g.
title = plt.title('What slows down my computer')
title.set_ha("left")
plt.gca().axis("equal")
pie = plt.pie(total, startangle=0)
labels=["Trojans", "Viruses", "Too many open tabs", "The anti-virus software"]
plt.legend(pie[0],labels, bbox_to_anchor=(1,0.5), loc="center right", fontsize=10,
bbox_transform=plt.gcf().transFigure)
plt.subplots_adjust(left=0.0, bottom=0.1, right=0.45)
Saving the file with bbox_inches="tight"
Now there may be cases where we are more interested in the saved figure than at what is shown on the screen. We may then simply position the legend at the edge of the figure, like so
but then save it using the bbox_inches="tight" to savefig,
plt.savefig("output.png", bbox_inches="tight")
This will create a larger figure, which sits tight around the contents of the canvas:
A sophisticated approach, which allows to place the legend tightly inside the figure, without changing the figure size is presented here:
Creating figure with exact size and no padding (and legend outside the axes)
Using Subplots
An alternative is to use subplots to reserve space for the legend. In this case one subplot could take the pie chart, another subplot would contain the legend. This is shown below.
fig = plt.figure(4, figsize=(3,3))
ax = fig.add_subplot(211)
total = [4,3,2,81]
labels = ["tough working conditions", "high risk of accident",
"harsh weather", "it's not allowed to watch DVDs"]
ax.set_title('What people know about oil rigs')
ax.axis("equal")
pie = ax.pie(total, startangle=0)
ax2 = fig.add_subplot(212)
ax2.axis("off")
ax2.legend(pie[0],labels, loc="center")

Matplotlib legend vertical rotation

Does someone perhaps know if it is possible to rotate a legend on a plot in matplotlib? I made a simple plot with the below code, and edited the graph in paint to show what I want.
plt.plot([4,5,6], label = 'test')
ax = plt.gca()
ax.legend()
plt.show()
I went to a similar problem and solved it by writing the function legendAsLatex that generates a latex code to be used as the label of the y-axis. The function gathers the color, the marker, the line style, and the label provided to the plot function. It requires enabling the latex and loading the required packages. Here is the code to generate your plot with extra curves that use both vertical axis.
from matplotlib import pyplot as plt
import matplotlib.colors as cor
plt.rc('text', usetex=True)
plt.rc('text.latex', preamble=r'\usepackage{amsmath} \usepackage{wasysym}'+
r'\usepackage[dvipsnames]{xcolor} \usepackage{MnSymbol} \usepackage{txfonts}')
def legendAsLatex(axes, rotation=90) :
'''Generate a latex code to be used instead of the legend.
Uses the label, color, marker and linestyle provided to the pyplot.plot.
The marker and the linestyle must be defined using the one or two character
abreviations shown in the help of pyplot.plot.
Rotation of the markers must be multiple of 90.
'''
latexLine = {'-':'\\textbf{\Large ---}',
'-.':'\\textbf{\Large --\:\!$\\boldsymbol{\cdot}$\:\!--}',
'--':'\\textbf{\Large --\,--}',':':'\\textbf{\Large -\:\!-}'}
latexSymbol = {'o':'medbullet', 'd':'diamond', 's':'filledmedsquare',
'D':'Diamondblack', '*':'bigstar', '+':'boldsymbol{\plus}',
'x':'boldsymbol{\\times}', 'p':'pentagon', 'h':'hexagon',
',':'boldsymbol{\cdot}', '_':'boldsymbol{\minus}','<':'LHD',
'>':'RHD','v':'blacktriangledown', '^':'blacktriangle'}
rot90=['^','<','v','>']
di = [0,-1,2,1][rotation%360//90]
latexSymbol.update({rot90[i]:latexSymbol[rot90[(i+di)%4]] for i in range(4)})
return ', '.join(['\\textcolor[rgb]{'\
+ ','.join([str(x) for x in cor.to_rgb(handle.get_color())]) +'}{'
+ '$\\'+latexSymbol.get(handle.get_marker(),';')+'$'
+ latexLine.get(handle.get_linestyle(),'') + '} ' + label
for handle,label in zip(*axes.get_legend_handles_labels())])
ax = plt.axes()
ax.plot(range(0,10), 'b-', label = 'Blue line')
ax.plot(range(10,0,-1), 'sm', label = 'Magenta squares')
ax.set_ylabel(legendAsLatex(ax))
ax2 = plt.twinx()
ax2.plot([x**0.5 for x in range(0,10)], 'ro', label = 'Red circles')
ax2.plot([x**0.5 for x in range(10,0,-1)],'g--', label = 'Green dashed line')
ax2.set_ylabel(legendAsLatex(ax2))
plt.savefig('legend.eps')
plt.close()
Figure generated by the code:
I spent a few hours chipping away at this yesterday, and made a bit of progress so I'll share that below along with some suggestions moving forward.
First, it seems that we can certainly rotate and translate the bounding box (bbox) or frame around the legend. In the first example below you can see that a transform can be applied, albeit requiring some oddly large translation numbers after applying the 90 degree rotation. But, there are actually problems saving the translated legend frame to an image file so I had to take a screenshot from the IPython notebook. I've added some comments as well.
import matplotlib.pyplot as plt
%matplotlib inline
import numpy as np
import matplotlib.transforms
fig = plt.figure()
ax = fig.add_subplot('121') #make room for second subplot, where we are actually placing the legend
ax2 = fig.add_subplot('122') #blank subplot to make space for legend
ax2.axis('off')
ax.plot([4,5,6], label = 'test')
transform = matplotlib.transforms.Affine2D(matrix=np.eye(3)) #start with the identity transform, which does nothing
transform.rotate_deg(90) #add the desired 90 degree rotation
transform.translate(410,11) #for some reason we need to play with some pretty extreme translation values to position the rotated legend
legend = ax.legend(bbox_to_anchor=[1.5,1.0])
legend.set_title('test title')
legend.get_frame().set_transform(transform) #This actually works! But, only for the frame of the legend (see below)
frame = legend.get_frame()
fig.subplots_adjust(wspace = 0.4, right = 0.9)
fig.savefig('rotate_legend_1.png',bbox_extra_artists=(legend,frame),bbox_inches='tight', dpi = 300) #even with the extra bbox parameters the legend frame is still getting clipped
Next, I thought it would be smart to explore the get_methods() of other legend components. You can sort of dig through these things with dir(legend) and legend.__dict__ and so on. In particular, I noticed that you can do this: legend.get_title().set_transform(transform), which would seem to imply that we could translate the legend text (and not just the frame as above). Let's see what happens when I tried that:
fig2 = plt.figure()
ax = fig2.add_subplot('121')
ax2 = fig2.add_subplot('122')
ax2.axis('off')
ax.plot([4,5,6], label = 'test')
transform = matplotlib.transforms.Affine2D(matrix=np.eye(3))
transform.rotate_deg(90)
transform.translate(410,11)
legend = ax.legend(bbox_to_anchor=[1.5,1.0])
legend.set_title('test title')
legend.get_frame().set_transform(transform)
legend.get_title().set_transform(transform) #one would expect this to apply the same transformation to the title text in the legend, rotating it 90 degrees and translating it
frame = legend.get_frame()
fig2.subplots_adjust(wspace = 0.4, right = 0.9)
fig2.savefig('rotate_legend_1.png',bbox_extra_artists=(legend,frame),bbox_inches='tight', dpi = 300)
The legend title seems to have disappeared in the screenshot from the IPython notebook. But, if we look at the saved file the legend title is now in the bottom left corner and seems to have ignored the rotation component of the transformation (why?):
I had similar technical difficulties with this type of approach:
bbox = matplotlib.transforms.Bbox([[0.,1],[1,1]])
trans_bbox = matplotlib.transforms.TransformedBbox(bbox, transform)
legend.set_bbox_to_anchor(trans_bbox)
Other notes and suggestions:
It might be a sensible idea to dig into the differences in behaviour between the legend title and frame objects--why do they both accept transforms, but only the frame accepts a rotation? Perhaps it would be possible to subclass the legend object in the source code and make some adjustments.
We also need to find a solution for the rotated / translated legend frame not being saved to output, even after following various related suggestion on SO (i.e., Matplotlib savefig with a legend outside the plot).

Annotation / Text at cross between y-label and title in matplotlib

I am struggling for almost a complete day now with positioning a simple text field in a matplotlib subplot. My goal is to position a one-letter text field (e.g., "A") at the top left of a subplot. However, it should be outside of the plot.
I know that I can position it relative to the axes by e.g., manually specifiying values. Yet, my goal is to automatize this process as I have to process a large amount of figures.
My idea was to use the x-coordinate of the ylabel and the y-coordinate of the title -- yet, with not much success.
To give an example:
I would like to add "A" exactly where "ylabel" and "Title" would imaginary cross.
Wanted to do the same thing just now and couldn't find anything helpful in the docs or after googling, so ended up looking at the code for tight_layout in tight_layout.py under the matplotlib directory. The function you're looking for is axes.get_tightbbox. Here's how to do what you want:
from matplotlib import pyplot as plt
from matplotlib.transforms import TransformedBbox
fig = plt.gcf()
ax = fig.gca()
renderer = fig.canvas.get_renderer()
# Get the bounding box in pixel coordinates
bb = ax.get_tightbbox(renderer)
# Transform the bbox to figure coordinates
bb = TransformedBbox(bb, fig.transFigure.inverted())
# Write text at the top left corner using figure coordinates.
plt.text(bb.x0, bb.y1, "HELLO", transform=fig.transFigure, verticalalignment="top", horizontalalignment="left")

Autoscale a matplotlib Axes to make room for legend

I am plotting a 2D view of a spacecraft orbit using matplotlib. On this orbit, I identify and mark certain events, and then list these events and the corresponding dates in a legend. Before saving the figure to a file, I autozoom on my orbit plot, which causes the legend to be printed directly on top of my plot. What I would like to do is, after autoscaling, somehow find out the width of my legend, and then expand my xaxis to "make room" for the legend on the right side of the plot. Conceptually, something like this;
# ... code that generates my plot up here, then:
ax.autoscale_view()
leg = ax.get_legend()
leg_width = # Somehow get the width of legend in units that I can use to modify my axes
xlims = ax.get_xlim()
ax.set_xlim( [xlims[0], xlims[1] + leg_width] )
fig.savefig('myplot.ps',format='ps')
The main problem I'm having is that ax.set_xlim() takes "data" specific values, whereas leg.get_window_extent reports in window pixels (I think), and even that only after the canvas has been drawn, so I'm not sure how I can get the legend "width" in a way that I can use similar to above.
You can save the figure once to get the real legend location, and then use transData.inverted() to transform screen coordinate to data coordinate.
import pylab as pl
ax = pl.subplot(111)
pl.plot(pl.randn(1000), pl.randn(1000), label="ok")
leg = pl.legend()
pl.savefig("test.png") # save once to get the legend location
x,y,w,h = leg.get_window_extent().bounds
# transform from screen coordinate to screen coordinate
tmp1, tmp2 = ax.transData.inverted().transform([0, w])
print abs(tmp1-tmp2) # this is the with of legend in data coordinate
pl.savefig("test.png")

Categories