Highlight a label in a legend, matplotlib - python

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

Related

Precise control over subplot locations in matplotlib

I am currently producing a figure for a paper, which looks like this:
The above is pretty close to how I want it to look, but I have a strong feeling that I'm not doing this the "right way", since it was really fiddly to produce, and my code is full of all sorts of magic numbers where I fine-tuned the positioning by hand. Thus my question is, what is the right way to produce a plot like this?
Here are the important features of this plot that made it hard to produce:
The aspect ratios of the three subplots are fixed by the data, but the images are not all at the same resolution.
I wanted all three plots to take up the full height of the figure
I wanted (a) and (b) to be close together since they share their y axis, while (c) is further away
Ideally, I would like the top of the top colour bar to exactly match the top of the three images, and similarly with the bottom of the lower colour bar. (In fact they aren't quite aligned, because I did this by guessing numbers and re-compiling the image.)
In producing this figure, I first tried using GridSpec, but I wasn't able to control the relative spacing between the three main subplots. I then tried ImageGrid, which is part of the AxisGrid toolkit, but the differing resolutions between the three images caused that to behave strangely. Delving deeper into AxesGrid, I was able to position the three main subplots using the append_axes function, but I still had to position the three colourbars by hand. (I created the colourbars manually.)
I'd rather not post my existing code, because it's a horrible collection of hacks and magic numbers. Rather my question is, is there any way in MatPlotLib to just specify the logical layout of the figure (i.e. the content of the bullet points above) and have the layout calculated for me automatically?
Here is a possible solution. You'd start with the figure width (which makes sense when preparing a paper) and calculate your way through, using the aspects of the figures, some arbitrary spacings between the subplots and the margins. The formulas are similar to the ones I used in this answer. And the unequal aspects are taken care of by GridSpec's width_ratios argument.
You then end up with a figure height such that the subplots' are equal in height.
So you cannot avoid typing in some numbers, but they are not "magic". All are related to acessible quatities like fraction of figure size or fraction of mean subplots size. Since the system is closed, changing any number will simply produce a different figure height, but will not destroy the layout.
import matplotlib.pyplot as plt
import matplotlib.gridspec as gridspec
import numpy as np; np.random.seed(42)
imgs = []
shapes = [(550,200), ( 550,205), (1100,274) ]
for shape in shapes:
imgs.append(np.random.random(shape))
# calculate inverse aspect(width/height) for all images
inva = np.array([ img.shape[1]/float(img.shape[0]) for img in imgs])
# set width of empty column used to stretch layout
emptycol = 0.02
r = np.array([inva[0],inva[1], emptycol, inva[2], 3*emptycol, emptycol])
# set a figure width in inch
figw = 8
# border, can be set independently of all other quantities
left = 0.1; right=1-left
bottom=0.1; top=1-bottom
# wspace (=average relative space between subplots)
wspace = 0.1
#calculate scale
s = figw*(right-left)/(len(r)+(len(r)-1)*wspace)
# mean aspect
masp = len(r)/np.sum(r)
#calculate figheight
figh = s*masp/float(top-bottom)
gs = gridspec.GridSpec(3,len(r), width_ratios=r)
fig = plt.figure(figsize=(figw,figh))
plt.subplots_adjust(left, bottom, right, top, wspace)
ax1 = plt.subplot(gs[:,0])
ax2 = plt.subplot(gs[:,1])
ax2.set_yticks([])
ax3 = plt.subplot(gs[:,3])
ax3.yaxis.tick_right()
ax3.yaxis.set_label_position("right")
cax1 = plt.subplot(gs[0,5])
cax2 = plt.subplot(gs[1,5])
cax3 = plt.subplot(gs[2,5])
im1 = ax1.imshow(imgs[0], cmap="viridis")
im2 = ax2.imshow(imgs[1], cmap="plasma")
im3 = ax3.imshow(imgs[2], cmap="RdBu")
fig.colorbar(im1, ax=ax1, cax=cax1)
fig.colorbar(im2, ax=ax2, cax=cax2)
fig.colorbar(im3, ax=ax3, cax=cax3)
ax1.set_title("image title")
ax1.set_xlabel("xlabel")
ax1.set_ylabel("ylabel")
plt.show()

Matplotlib: Change color of individual grid lines

I've only been using Python for about a month now, so I'm sorry if there's some simple solution to this that I overlooked.
Basically I have a figure with 4 subplots, the 2 on the left show longitudinal plots and the ones on the right show scatter plots at certain points of the longitudinal plots. You can click through the scatter plots at different points of the longitudinal plot with buttons, and the tick label of the longitudinal plot you're currently at will be highlighted in blue.
Coloring a certain tick label already works with this:
xlabels = []
labelcolors = []
for i, item in enumerate(mr.segmentlst):
if re.search('SFX|MC|MQ|MS|MKC', item):
xlabels.append(mr.segmentlst[i])
else:
xlabels.append('')
for i, item in enumerate(mr.segmentlst):
if re.search('SFX', item):
labelcolors.append('black')
else:
labelcolors.append('gray')
labelcolors[self.ind]='blue'
[t.set_color(i) for (i,t) in zip(labelcolors, ax1.xaxis.get_ticklabels())]
[t.set_color(i) for (i,t) in zip(labelcolors, ax2.xaxis.get_ticklabels())]
It only shows certain tick labels and changes their colors accordingly (I don't know if there is another solution for this, it's the only one I could find). Don't mind the mr.segmentlist, I've currently hardcoded the plot to use an attribute from another method so I can easily keep testing it in Spyder.
I'd like to also change the grid line color of the currently highlighted tick label (only xgridlines are visible) in the longitudinal plots, is there some kind of similar way of doing this? I've searched the internet for a solution for about 2 hours now and didn't really find anything helpful.
I thought something like ax1.get_xgridlines() might be used, but I have no idea how I could transform it into a useful list.
Thanks,
Tamara
get_xgridlines() returns a list of Line2D objects, so if you can locate which line you want to modify, you can modify any of their properties
x = np.random.random_sample((10,))
fig = plt.figure()
ax = fig.add_subplot(111)
ax.scatter(x,x)
ax.grid()
a = ax.get_xgridlines()
b = a[2]
b.set_color('red')
b.set_linewidth(3)
since the above solution only works with major gridlines
(since get_gridlines() is currently hardcoded to use only the major ones),
here's how you can also access the minor gridlines by adapting
the get_gridlines() function (from here):
from matplotlib import cbook
def get_gridlines(ax, which):
'''
Parameters:
ax : ax.xaxis or ax.yaxis instance
which : 'major' or 'minor'
Returns:
The grid lines as a list of Line2D instance
'''
if which == 'major':
ticks = ax.get_major_ticks()
if which == 'minor':
ticks = ax.get_minor_ticks()
return cbook.silent_list('Line2D gridline',
[tick.gridline for tick in ticks])

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

Clear overlay scatter on matplotlib image

So I am back again with another silly question.
Consider this piece of code
x = linspace(-10,10,100);
[X,Y]=meshgrid(x,x)
g = np.exp(-(square(X)+square(Y))/2)
plt.imshow(g)
scat = plt.scatter(50,50,c='r',marker='+')
Is there a way to clear only the scatter point on the graph without clearing all the image?
In fact, I am writing a code where the appearance of the scatter point is bound with a Tkinter Checkbutton and I want it to appear/disappear when I click/unclick the button.
Thanks for your help!
The return handle of plt.scatter has several methods, including remove(). So all you need to do is call that. With your example:
x = np.linspace(-10,10,100);
[X,Y] = np.meshgrid(x,x)
g = np.exp(-(np.square(X) + np.square(Y))/2)
im_handle = plt.imshow(g)
scat = plt.scatter(50,50,c='r', marker='+')
# image, with scatter point overlayed
scat.remove()
plt.draw()
# underlying image, no more scatter point(s) now shown
# For completeness, can also remove the other way around:
plt.clf()
im_handle = plt.imshow(g)
scat = plt.scatter(50,50,c='r', marker='+')
# image with both components
im_handle.remove()
plt.draw()
# now just the scatter points remain.
(almost?) all matplotlib rendering functions return a handle, which have some method to remove the rendered item.
Note that you need the call to redraw to see the effects of remove() -- from the remove help (my emphasis):
Remove the artist from the figure if possible. The effect will not be
visible until the figure is redrawn, e.g., with
:meth:matplotlib.axes.Axes.draw_idle.

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