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

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

Related

Using annotate to place a text box below legend in Matplotlib

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)

Highlight a label in a legend, matplotlib

As of now I am using Matplotlib to generate plots.
The legend on the plot can be tweaked using some parameters (as mentioned in this guide). But I would like to have something specific in the legend, as attached in this image below.
I would like to highlight one of the labels in the legend like shown (as of now done using MS paint).
If there are other ways of highlighting a specific label, that would also suffice.
The answer by FLab is actually quite reasonable given how painful it can be to backtrace the coordinates of the plotted items. However, the demands of publication-grade figures are quite often unreasonable, and seeing matplotlib challenged by MS Paint is a enough good motivation for answering this.
Lets consider this example from the matplotlib gallery as a starting point:
N = 100
x = np.arange(N)
fig = plt.figure()
ax = fig.add_subplot(111)
xx = x - (N/2.0)
plt.plot(xx, (xx*xx)-1225, label='$y=x^2$')
plt.plot(xx, 25*xx, label='$y=25x$')
plt.plot(xx, -25*xx, label='$y=-25x$')
legend = plt.legend()
plt.show()
Once an image has been drawn, we can backtrace the elements in the legend instance to find out their coordinates. There are two difficulties associated with this:
The coordinates we'll get through the get_window_extent method are in pixels, not "data" coordinates, so we'll need to use a transform function. A great overview of the transforms is given here.
Finding a proper boundary is tricky. The legend instance above has two useful attributes, legend.legendHandles and legend.texts - two lists with a list of line artists and text labels respectively. One would need to get a bounding box for both elements, while keeping in mind that the implementation might not be perfect and is backend-specific (c.f. this SO question). This is a proper way to do this, but it's not the one in this answer, because...
.. because luckily in your case the legend items seem to be uniformly separated, so we could just get the legend box, split it into a number of rectangles equal to the number of rows in your legend, and draw one of the rectangles on-screen. Below we'll define two functions, one to get the data coordinates of the legend box, and another one to split them into chunks and draw a rectangle according to an index:
from matplotlib.patches import Rectangle
def get_legend_box_coord(ax, legend):
""" Returns coordinates of the legend box """
disp2data = ax.transData.inverted().transform
box = legend.legendPatch
# taken from here:
# https://stackoverflow.com/a/28728709/4118756
box_pixcoords = box.get_window_extent(ax)
box_xycoords = [disp2data(box_pixcoords.p0), disp2data(box_pixcoords.p1)]
box_xx, box_yy = np.array(box_xycoords).T
return box_xx, box_yy
def draw_sublegend_box(ax, legend, idx):
nitems = len(legend.legendHandles)
xx, yy = get_legend_box_coord(ax, legend)
# assuming equal spacing between legend items:
y_divisors = np.linspace(*yy, num=nitems+1)
height = y_divisors[idx]-y_divisors[idx+1]
width = np.diff(xx)
lower_left_xy = [xx[0], y_divisors[idx+1]]
legend_box = Rectangle(
xy = lower_left_xy,
width = width,
height = height,
fill = False,
zorder = 10)
ax.add_patch(legend_box)
Now, calling draw_sublegend_box(ax, legend, 1) produces the following plot:
Note that annotating the legend in such is way is only possible once the figure has been drawn.
In order to highlight a specific label, you could have it in bold.
Here's the link to another SO answer that suggest how to use Latex to format entries of a legend:
Styling part of label in legend in matplotlib

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

Change axis range into latitude and longitude using matplotlib in python

How can I use yaxis and xaxis, which I want and that are not correlated with data in the plot?
For example, I want to plot the world map as an image using the code below:
import matplotlib.pyplot as plt
fig = plt.figure()
plt.imshow(world_map)
As a result, I got xaxis: 0...image_size_x from the left to the rigth and yaxis: 0...image_size_y from top to bottom.
What do I need to to do to change its axis range into latitude and longitude formats? Thus the figure axis should contain degrees (from 90 to -90) on the both fields (x and y) regardless of what its real data plotted in the figure.
Setting
pylab.ylim([90,-90])
will shift the image to the bottom by 90 pixels and reduced the y-dimension of the image into the scale of image_size_y/90. So it'll not work because xlim/ylim works with data, plotted in the figure.
In short: Use the extent keyword with imshow.
In code:
import matplotlib.pyplot as plt
fig = plt.figure()
ax = fig.add_subaxis(111)
ax.imshow(world_map, extent=[-180,180,-90,90], aspect='auto')
If your map is then upside down, add the keyword argument origin='lower' to the imshow. That aspect='auto' is needed to make the map scalable in both dimensions independently. (The rest of the extra rows with add_subaxis are just to make the code more object-oriented, the real beef is in the keyword arguments.)
If imshow is not given the extents of the image, it thinks that you'll want to have each pixel centered at positions (0,0), (0,1), ..., (Nx-1, Ny-1), and then the image extents will start from (-.5, -.5).
Assuming (based on your post) the image is fine but the axis labels are off, try playing around with this, which will manually implement the axis labels:
plt.figure(1)
ax = plt.subplot(111)
#... do your stuff
#need to figure out your image size divided by the number of labels you want
#FOR EXample, if image size was 180, and you wanted every second coordinate labeled:
ax.set_xticks([i for i in range(0,180,2)]) #python3 code to create 90 tick marks
ax.set_xticklabels([-i for i in range(-90,90,2)]) #python3 code to create 90 labels
#DO SAME FOR Y
The trick im using is to figure out how many labels you want (here, its 90: 180/2), add the tickmarks evenly in the range (0,imagesize), then manually do the labels. Here is a general formula:
ax.set_xticks([i for i in range(0,IMAGE_SIZE,_EVERY_XTH_COORD_LABELED)]) #python3 code to create 90 tick marks
ax.set_xticklabels([-i for i in range(-90,90,EVERY_XTH_COORD_LABELED)]) #python3 code to create 90 labels

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