Why does get_legend_handles_labels() return empty lists when legend labels are added via ax.legend() [duplicate] - matplotlib

This one is blowing my mind. I'm creating a little customize legend function where you put in some text and properties and it'll make it for you. So, before this is reachable in the GUI, a figure with a legend has already been created by this point which has "run5" and "run6" in it.
I wrote something that deletes the existing legend and calls legend on that same axis again with new handles/labels. However, when I do ax.get_legend_handles_labels() right afterwards it returns the deleted legend's handle and labels, completely ignoring the legend call I just did.
I've tried removing the legend and then just recreating it. But clearly ax is holding onto the previous legend's data.
from matplotlib.lines import Line2D
ax = self.axes[ind] #just the axis handle
custom_lines, custom_strings = [], []
try:
ax.get_legend().remove()
except:
# Means no legend exists
pass
for idx, i in enumerate(self.lgndRowWidget):
if not i.isHidden():
#The below five lines are grabbing data from GUI
lineText = self.lgndStr[idx].text() # Is "test" here
lineType = self.lgndLine[idx].currentText()
lineWidth = self.lgndWidth[idx].value()
lineMrkr = self.lgndMrkr[idx].currentText()
lineClr = self.lgndClr[idx].text()
custom_lines.append(Line2D([0],[0],
lw=lineWidth,
ls=lineType,
marker=lineMrkr,
color=lineClr))
custom_strings.append(lineText)
if len(custom_lines) != 0:
print(custom_strings)
self.legList = ax.legend(custom_lines, custom_strings)
a,b = ax.get_legend_handles_labels()
print(b)
self.fig.canvas.draw()
print(custom_strings) returns whatever I input. In this case "test".
print(b) returns what was previously in the legend that I can't seem to get rid of: the initial "run5" and "run6". It SHOULD be "test".

You might have misunderstood the functionality of ax.get_legend_handles_labels().
What it does is to look for artists (like lines, collections etc.) that have a label.
It then returns those artists and their respective labels. Hence
ax.legend() is roughly equivalent to
handles, labels = ax.get_legend_handles_labels()
ax.legend(handles=handles, labels=labels)
.get_legend_handles_labels() does not know about whether there is a legend present, because it returns what is supposed to be in the legend, not what currently is in it.

Related

Using a subfigure as textbody fails on tight layout with a lot of text

Using a subfigure as textbody fails on tight layout with a lot of text. As seen in the provided example, the bound of a figure are overreached by one subfigure, as if the text was not wrapped.
import pandas as pd
from matplotlib import pyplot as plt
# Paramters for A4 Paper
fullheight = 11.69
fullwidth = 8.27
# how the subfigures dive the space
fig0_factor = 0.7
fig1_factor = 0.3
# generate the figure
fig = plt.figure(constrained_layout = True, figsize=(fullwidth, fullheight)) #
# generate 2 subfigures
subfigs = fig.subfigures(2,height_ratios=[fig0_factor,fig1_factor])
# fill the fist subfigure
axes = subfigs[0].subplots(nrows=1)
# some plot
ax = plt.plot([0,0],[-1,1], lw=1, c='b', figure=subfigs[0])
# some text
subfigs[0].text(0.55, 0.00001, 'Figure 1', ha='center', va='center', rotation='horizontal',weight='bold')
# fill the second subfigure
text_ax = subfigs[1].subplots(1)
# make its background transparent
subfigs[1].patch.set_alpha(0.0)
# remove the axis, not removing it makes no difference regarding the problem
text_ax.set_axis_off()
# generate some text
message = ['word']*50 # 50 is enough to make the problem visable, my usecase has a way longer text
message = ' '.join(message)
# fill in the Text and wrap it
text_ax.text(0.00001, 0.8, message, horizontalalignment='left', verticalalignment='top',size=7, wrap=True)
# call tight layout
# this is neccecary for the whole plot, that is not shown for simplicity reasons
# explaination: subfigure one would be an array of subplots with different scales and descriptions,
# ever changing depending on the data that is to be plotted, so tight_layout mittigates
# a lot of tideos formatting
fig.tight_layout()
Please notice the figures right edge reaches for the whole panel, as seen in the second image
# omitting the wrap, it is clear why the figure is out of bound: somehow the layout gets its information from the unwrapped text
text_ax.text(0.00001, 0.8, message, horizontalalignment='left', verticalalignment='top',size=7, wrap=False)
Is there another way to have the text rendered in the subplot with it correctly noticing the wrapped bounds instead of the unwrapped bounds?
If you modify your code above to remove fig.tight_layout() and to take the text out of the layout then everything works as expected.
# fill in the Text and wrap it
thetext = text_ax.text(0.00001, 0.8, message, horizontalalignment='left',
verticalalignment='top',size=7, wrap=True)
thetext.set_in_layout(False)
There should be nothing that tight_layout does that layout='constrained' cannot do, and constrained layout is much more flexible.
BTW, you might consider subfigs[0].supxlabel() instead of the text command for "Figure 1"

Heatmap colorbars accumulating in Matplotlib/Seaborn figures

I have a list of data frames, and I want to make heatmaps of every data frame in the list. The first heatmap comes out perfectly, but the second one has two colorbars, one much larger than the other, which distorts the figure. The third has THREE colorbars, the last one being even larger, and this continues for as many heatmaps as I make.
This seems like a bug to me, as I have no idea why it's happening. Each heatmap should be stored as a separate element in the list of heatmaps, and even if I plot them individually, instead of using a loop or list comprehension, I get the same problem.
Here is my code:
# Set the seaborn font size.
sns.set(font_scale=0.5)
# Ensure that labels are not cut off.
plt.gcf().subplots_adjust(bottom=0.18)
plt.gcf().subplots_adjust(right=.3)
black_yellow = sns.dark_palette("yellow",10)
heatmap_list = [sns.heatmap(df, cmap=black_yellow, xticklabels=True, yticklabels=True) for df in df_list]
[heatmap_list[x].figure.savefig(file_names_list[x]+'.pdf', format='pdf') for x in range(0,len(heatmap_list))]
sns.heatmap() creates a problem while we are working in loop. To resolve this issue, the first iteration will be done individually and rest of the loop remains the same but we will add a parameter cbar=False to stop this recursion of colorbar in the loop portion.
# Set the seaborn font size.
sns.set(font_scale=0.5)
# Ensure that labels are not cut off.
plt.gcf().subplots_adjust(bottom=0.18)
plt.gcf().subplots_adjust(right=.3)
black_yellow = sns.dark_palette("yellow", 10)
hm = sns.heatmap(df_list[0], cmap=black_yellow, xticklabels=True, yticklabels=True)
hm.figure.savefig(file_names_list[0]+'.pdf', format='pdf')
heatmap_list = [sns.heatmap(df_list[i], cmap=black_yellow, xticklabels=True, yticklabels=True, cbar=False) for i in range(1, len(df_list))]
[heatmap_list[x].figure.savefig(file_names_list[x+1]+'.pdf', format='pdf') for x in range(0, len(heatmap_list))]

change matplotlib data in gui

I've developed an gui with python pyqt. There I have a matplotlib figure with x,y-Data and vlines that needs to change dynamically with a QSlider.
Right now I change the data just with deleting everything and plot again but this is not effective
This is how I do it:
def update_verticalLines(self, Data, xData, valueSlider1, valueSlider2, PlotNr, width_wg):
if PlotNr == 2:
self.axes.cla()
self.axes.plot(xData, Data, color='b', linewidth=2)
self.axes.vlines(valueSlider1,min(Data),max(Data),color='r',linewidth=1.5, zorder = 4)
self.axes.vlines(valueSlider2,min(Data),max(Data),color='r',linewidth=1.5, zorder = 4)
self.axes.text(1,0.8*max(Data),str(np.round(width_wg,2))+u"µm", fontsize=16, bbox=dict(facecolor='m', alpha=0.5))
self.axes.text(1,0.6*max(Data),"Pos1: "+str(round(valueSlider1,2))+u"µm", fontsize=16, bbox=dict(facecolor='m', alpha=0.5))
self.axes.text(1,0.4*max(Data),"Pos2: "+str(round(valueSlider2,2))+u"µm", fontsize=16, bbox=dict(facecolor='m', alpha=0.5))
self.axes.grid(True)
self.draw()
"vlines" are LineCollections in matplotlib. I searched in the documentation but could not find any hint to a function like 'set_xdata' How can I change the x value of vertical lines when they are already drawn and embedded into FigureCanvas?
I have the same problem with changing the x and y data. When trying the known functions of matplotlib like 'set_data', I get an error that AxisSubPlot does not have this attribute.
In the following is my code for the FigureCanvas Class. The def update_verticalLines should only contain commands for changing the x coord of the vlines and not complete redraw.
Edit: solution
Thanks #Craigular Joe
This was not exactly how it worked for me. I needed to change something:
def update_verticalLines(self, Data, xData, valueSlider1, valueSlider2, PlotNr, width_wg):
self.vLine1.remove()
self.vLine1 = self.axes.vlines(valueSlider1,min(Data), max(Data), color='g', linewidth=1.5, zorder = 4)
self.vLine2.remove()
self.vLine2 = self.axes.vlines(valueSlider2,min(Data), max(Data), color='g', linewidth=1.5, zorder = 4)
self.axes.draw_artist(self.vLine1)
self.axes.draw_artist(self.vLine2)
#self.update()
#self.flush_events()
self.draw()
update() did not work without draw(). (The old vlines stayed)
flush_events() did some crazy stuff. I have two instances of FigureCanvas. flush_events() caused that within the second instance call the vlines moved with the slider but moved then back to the start position.
When you create the vlines, save a reference to them, e.g.
self.my_vlines = self.axes.vlines(...)
so that when you want to change them, you can just remove and replace them, e.g.
self.my_vlines.remove()
self.my_vlines = self.axes.vlines(...)
# Redraw vline
self.axes.draw_artist(self.my_vlines)
# Add newly-rendered lines to drawing backend
self.update()
# Flush GUI events for figure
self.flush_events()
By the way, in the future you should try your best to pare down your code sample to just the essential parts. Having a lot of unnecessary sample code makes it hard to understand your question. :)

How might I plot multiple lines (same data, different styles) with a single plotting command in matplotlib?

In my example, I want to plot a "highlighting" line (a heavy black line with a narrower yellow line over it to create a black outline) behind the actual data (the blue 'Pitch Angle' line in my example).
My function highlight does this for elements in pandas Series x where h is True and also finally plots x. highlight returns unique identifiers for the highlighting lines created and the actual line styles used to allow the legend entries to be edited to produce what you see in the image, but as far as I can tell that has to be done after all plot commands, i.e. outside of highlight, assuming other things will be added to the Axes e.g. the 'Fine pitch' and '10 degrees' lines in my example.
def highlight(x, h, hlw=8, hc='#FFFBCC', hll='Highlighted', **kwargs):
"""
Plot data in Series x, highlighting the elements correponding to True in h.
"""
# For passing to extra the highlighter lines, drop any passed kwargs which
# would clobber their appearance. Keep a copy of the full set for the data.
# The extra lines are plotted first so they appear underneath the actual
# data.
fullkwargs = kwargs.copy()
for k in ['lw', 'linewidth', 'linestyle', 'ls', 'color', 'c', 'label']:
kwargs.pop(k, None)
# Generate a probably unique ID so that the legend entries of these lines
# can be identified by the caller.
import random
gid = random.random()
# Plot the highlighting lines. First, plot a heavier black line to provide
# a clear outline to the bright highlighted region.
x.where(h).plot(lw=hlw+2, c='k', label='_dont_put_me_in_legend', **kwargs)
# Plot the bright highlighter line on top of that. Give this line an id.
x.where(h).plot(lw=hlw, c=hc, label=hll, gid=gid, **kwargs)
# Plot the actual data.
a = x.plot(**fullkwargs)
# Generate the custom legend entry artists.
import matplotlib.lines as mlines
l1 = mlines.Line2D([], [], lw=hlw+2, c='k')
l2 = mlines.Line2D([], [], lw=hlw, c=hc)
# Return the unique identifier of this call, and the tuple of line styles to
# go to HandlerTuple (see
# http://matplotlib.org/users/legend_guide.html#legend-handlers). This
# makes it possible to update all "highlighter" legend entries with the
# correct color-on-black visual style produced here.
return {gid: (l1, l2)}
I manage to generate an accurate legend entry for the highlighting by making use of HandlerTuple when calling plt.legend() by looping over all legend entries and replacing legend keys where gid is one of the values returned by highlight.
handles,labels = a.get_legend_handles_labels()
newhandles = []
for h in handles:
if h.get_gid() in hi:
newhandles.append(hi[h.get_gid()])
else:
newhandles.append(h)
a.legend(newhandles, labels)
Is there a way in matplotlib to define a "compound" line style such that one plot command produces the desired appearance and has only one legend entry?

Add a new axis to the right/left/top-right of an axis

How do you add an axis to the outside of another axis, keeping it within the figure as a whole? legend and colorbar both have this capability, but implemented in rather complicated (and for me, hard to reproduce) ways.
You can use the subplots command to achieve this, this can be as simple as py.subplot(2,2,1) where the first two numbers describe the geometry of the plots (2x2) and the third is the current plot number. In general it is better to be explicit as in the following example
import pylab as py
# Make some data
x = py.linspace(0,10,1000)
cos_x = py.cos(x)
sin_x = py.sin(x)
# Initiate a figure, there are other options in addition to figsize
fig = py.figure(figsize=(6,6))
# Plot the first set of data on ax1
ax1 = fig.add_subplot(2,1,1)
ax1.plot(x,sin_x)
# Plot the second set of data on ax2
ax2 = fig.add_subplot(2,1,2)
ax2.plot(x,cos_x)
# This final line can be used to adjust the subplots, if uncommentted it will remove all white space
#fig.subplots_adjust(left=0.13, right=0.9, top=0.9, bottom=0.12,hspace=0.0,wspace=0.0)
Notice that this means things like py.xlabel may not work as expected since you have two axis. Instead you need to specify ax1.set_xlabel("..") this makes the code easier to read.
More examples can be found here.