Using Matplotlib version 3.8.4, suspecting this might be a bug. Newbie to Python.
When plotting errorbar in a figure, the sample code below experiences an unusually large hitbox for the pick_event toggle resulting in unwanted multi-selection of items in the legend. Couldn’t figure out from the documentation why changing pickradius doesn’t seem to help, appears to be a bug:
I also commented this problem on github under a similar issue previously raised by someone else: (https://github.com/matplotlib/matplotlib/issues/1962)
I tried several work-around attempts, none which worked. Can’t find a similar attempt either.
Also, one legend item toggle doesn’t work and I’m not sure why? Thanks in advance <3
from matplotlib import pyplot as plt, patches
from matplotlib.lines import Line2D # Can be used to check event.artist type. (doesn't resolve problem)
from matplotlib.patches import Rectangle # Can be used to check event.artist type. (doesn't resolve problem)
from matplotlib.offsetbox import VPacker
import numpy as np
%matplotlib qt5
'''
def print_version(): # So matplotlib is only imported when checking version, not able to affect code.
import matplotlib
print('matplotlib: {}'.format(matplotlib.__version__))
print_version() # I'm using version 3.8.4
'''
x = np.arange(0.1,4,0.5)
y0 = np.exp(-x)*1.2-0.25
y1 = np.exp(-x)
y2 = np.exp(-x)*1.2+0.25
fig, ax = plt.subplots(figsize=(10,6))
ax.grid(True)
(m, ) = ax.plot(x , y1 , color = "red" , label = "line_m")
(n, ) = ax.plot(x*2, y1*2, color = "red" , label = "line_n")
shadow_1 = ax.fill_between(x, y0, y2, label = 'shadow_1')
(a, b, c) = ax.errorbar(x , y2 , yerr=0.1, color="blue" , marker='*', capsize=4, label="errorbar a")
(d, e, f) = ax.errorbar(x+2, y2+2, xerr=0.1, color="green", marker='.', capsize=4, label="errorbar d")
(g, h, i) = ax.errorbar(x-2, y2-2, yerr=0.1, xerr=0.1, color='black', marker='o', capsize=4, label='errorbar g')
leg = ax.legend()
leg.set_draggable(True)
lines = [m, n]
fill_plots = [shadow_1]
bars_line = [a, d, g] # line of errorbar
bars_lower = [b[0], e[0], h[0]] # lower error bar
bars_upper = [b[1], e[1], h[1]] # upper error bar
bars_vertical = [c, f, i] # vertical line through datapoint
pickradius = 5
map_legend_to_ax = {}
for legend_line, ax_line in zip(leg.get_lines(), lines):
legend_line.set_picker(pickradius)
map_legend_to_ax[legend_line] = ax_line
leg_to_fill = {}
for leg_obj, fill_plot in zip(leg.findobj(patches.Rectangle), fill_plots):
leg_obj.set_picker(pickradius)
leg_to_fill[leg_obj] = fill_plot
line_to_error_bars = {}
for line_obj, errorbar_line in zip(leg.get_children(), bars_line): # leg.findobj(VPacker) also works sometimes.
line_obj.set_picker(pickradius)
line_obj.pickradius = 1
line_to_error_bars[line_obj] = errorbar_line
upper_error = {}
for obj, upper in zip(leg.get_children(), bars_upper):
obj.set_picker(pickradius)
obj.pickradius = 1
upper_error[obj] = upper
lower_error = {}
for obj, lower in zip(leg.get_children(), bars_lower):
obj.set_picker(1)
obj.pickradius = 1
lower_error[obj] = lower
vertical_error_line = {}
for obj, vertical in zip(leg.get_children(), bars_vertical):
obj.set_picker(True)
obj.pickradius = 1
vertical_error_line[obj] = vertical
def toggle_(event, fig, map_legend_to_ax, leg_to_fill, line_to_error_bars, lower_error, upper_error, vertical_error_line):
'''Using pick_event (select with mouse click), find the original line/shadow corresponding to the legend
proxy line/shadow, and toggle its visibility.'''
legend_plot_object = event.artist
shadow_alpha_off = 0.2
# Can also use isinstance(artist, Line2D) etc. here. Tried to see if we can avoid toggle error this way... nope :(
condition_errorbar = (legend_plot_object in line_to_error_bars) and (legend_plot_object in lower_error) and (legend_plot_object in upper_error) and (legend_plot_object in vertical_error_line)
condition_shadow = (legend_plot_object in leg_to_fill)
condition_line = (legend_plot_object in map_legend_to_ax)
if condition_line and not condition_errorbar:
legend_line = legend_plot_object
ax_line = map_legend_to_ax[legend_line]
visible = not ax_line.get_visible()
ax_line.set_visible(visible)
legend_line.set_alpha(1.0 if visible else shadow_alpha_off)
fig.canvas.draw()
return True
if condition_shadow and not condition_errorbar:
leg_obj = legend_plot_object
fill_plot = leg_to_fill[leg_obj]
visible = not fill_plot.get_visible()
fill_plot.set_visible(visible)
leg_obj.set_alpha(1.0 if visible else shadow_alpha_off)
fig.canvas.draw()
return True
elif condition_errorbar:
leg_obj = legend_plot_object
line_and_error = line_to_error_bars[leg_obj]
lower_bar = lower_error[leg_obj]
upper_bar = upper_error[leg_obj]
vertical_bar = vertical_error_line[leg_obj]
visible_line = not line_and_error.get_visible()
visible_lower = not lower_bar.get_visible()
visible_upper = not upper_bar.get_visible()
for i in vertical_bar:
visible_i = not i.get_visible()
i.set_visible(visible_i)
line_and_error.set_visible(visible_line)
lower_bar.set_visible(visible_lower)
upper_bar.set_visible(visible_upper)
leg_obj.set_alpha(1.0 if (visible_line and visible_lower) else shadow_alpha_off)
fig.canvas.draw()
return True
fig.canvas.mpl_connect('pick_event', lambda event: toggle_(event,
fig,
map_legend_to_ax,
leg_to_fill,
line_to_error_bars,
lower_error,
upper_error,
vertical_error_line)
)
plt.show()
- Tried changing pickradius of errorbar, VPacker doesn’t have a set_pickerradius so assigned pickradius directly. Doesn’t affect the toggling.
- Tried adding a custom legend icon and label, didn’t resolve the problem.
- Tried to use mousehover so check which item the mouse is over, always returns false hence didn’t work.
- Tried tracking the number of event triggers and using the last entry, didn’t work as I can’t predict when the final trigger is.
- Tried placing each errorbar in its own legend, works but not pragmatic.
- Tried increasing spacing between labels and legend icons, didn’t work.
- Can’t remember what else I tried over the last few weeks!
David Elcock is a new contributor to this site. Take care in asking for clarification, commenting, and answering.
Check out our Code of Conduct.