Hi, I am trying to create interactive plots via matplotlib and have run into some issues I cannot find a solution for. Maybe somebody can point me into the right direction?
The aim is to toggle visibility for a group of lines using entries in a custom legend (based on matplotlib.org/tutorials/intermediate/legend_guide.html )
The problem I run into is two-fold:
-
that the patches in the legend do not selectively catch any mouse-events. So somehow the interactivity that is built-in with any Line2D created from the data set (like in https://matplotlib.org/examples/event_handling/legend_picking.html ) is not achieved with the legend patches. (The legend itself can be turned into a button that toggles line visibility, but the same logic does not seem to apply for the individual Patches added to a custom legend). Am I missing something, in the sense that I need to build a separate class for these so that these become responsive?
-
Then, also related to mouse-events, but now the opposite: how not to catch them: When a subsection of the data is drawn (say by setting xlim and ylim to 80%) the lines continue beyond the panel boundaries and they are still interactive. This creates a confusing using-interface and ideally you would like to prevent this.
In this respect, too: the axes themselves still catch mouse-events that are picked up by the lines; thus the same mouse-event is registered by various Artists. How can that be controlled (i.e. that the mouse-click intended for the line is not picked up by the underlying axes)?
I tried using ‘clip’ and z-order settings to see whether that would help but it did not. Below an example showing the above issues.
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
# beyond_axes.py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.patches import Rectangle, Patch
from matplotlib.lines import Line2D
from matplotlib.axes import Axes
from matplotlib.transforms import Bbox
import matplotlib as mpl
from matplotlib.legend import Legend
#
def plot_set():
"""plot a figure"""
## prepare data for plotting
lim=100 #20*5
df=pd.DataFrame({
'a0':np.arange(lim)*3,
'a1':np.arange(lim)*0.8,
'a2':np.arange(lim)*2,
'c0':np.arange(lim)*0.5,
'b3':np.arange(lim)*6.6,
'd1':np.arange(lim)*1.8 ,
'b1':np.arange(lim)*0.2,
'c1':np.arange(lim)*1.2,
'b2':np.arange(lim)*2.5,
'd2':np.arange(lim)*4
})
## prepare for plotting
lowalph = 0.3 # built up the plot so intensity is measure for overlap
highalph = 0.9 # on click show whole plot
lowlw = 1.5
highlw = 4
print(df.columns)
# set up figure
fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(10, 5))
# try to keep interactivity within the panel (ax)
print(ax.bbox)
ax.set_clip_box(Bbox.from_bounds(0, 0, 10.0, 5.0)) # ax.axis())# gives 0,1,0,1
ax.set_clip_on(True) #does not make a difference
print(ax.get_clip_on())
print(ax.get_clip_box())
ax.set_picker(True) # comment this out and lines toggle inside the axes as well, not only when clicked outside
# plot various groups
def plotdf(df=None, xlim_=[0,80], ylim_=[0,80]):
df[['a0','a1','a2']].plot(ax=ax, legend=False, label='A', gid='A', color='b', alpha=lowalph, xlim=xlim_, ylim=ylim_, lw=lowlw, picker=5)
df[['b1','b2','b3']].plot(ax=ax, legend=False, label='B', gid='B', color='orange', alpha=lowalph, xlim=xlim_, ylim=ylim_, lw=lowlw, picker=5)
df[['c0','c1','d1','d2']].plot(ax=ax, legend=False, label='CD', gid='CD',color='g', alpha=lowalph, xlim=xlim_, ylim=ylim_, lw=lowlw, picker=5)
#make custom legend
a_patch = Patch(color='blue', zorder=6, alpha=lowalph, picker=True, label='A' ) # get it on top of; legend has z-order 5;
b_patch = Patch(color='orange', zorder=6, alpha=lowalph, picker=True, label='B' ) # but z-order change does not help to catch
cd_patch = Patch(color='green', zorder=6, alpha=lowalph, picker=True, label='CD') # clicks (being part of legend takes over?)
leg=fig.legend(handles = [a_patch,b_patch,cd_patch], fontsize=('small'), ncol=1 ,loc='upper right', bbox_to_anchor=(0.8, 0.4))
leg.set_draggable(True)
plotdf(df)
a_patch.set_zorder(7) # also no effect
print("Legend z-order:",leg.get_zorder())
print("A-patch z-order:",a_patch.get_zorder())
print("B-patch z-order:",b_patch.get_zorder())
# set up interactivity
# matplotlib.org/3.2.2/users/event_handling.html
def onpick(pevent): #PickEvent
if isinstance(pevent.artist, Patch): # not picked up; legend takes all clicks
print("Patch")
elif isinstance(pevent.artist, Legend):
print("Legend!")
for line in ax.get_lines():
if line.get_gid() =='B':
print("Toggle:", line.get_gid())
line.set_visible(False) if line.get_visible()==True else line.set_visible(True)
elif isinstance(pevent.artist, Line2D): # also picked outwith axes area
thisline = pevent.artist
lab = mpl.artist.getp(thisline,"label")
print("Line width toggled for",lab)
lineon = highlw if thisline.get_lw() == lowlw else lowlw
linealph = highalph if thisline.get_alpha() == lowalph else lowalph
thisline.set_lw(lineon)
thisline.set_alpha(linealph)
elif isinstance(pevent.artist,Axes): # to turn off line if clicked besides it
print('Axes')
for line in ax.get_lines():
if line.get_alpha() == highalph:
line.set_alpha(lowalph)
if line.get_lw() == highlw:
line.set_lw(lowlw)
fig.canvas.draw_idle()
#show the figure
fig.canvas.mpl_connect('pick_event', onpick)
plt.show()
def main(args):
plot_set()
return 0
if __name__ == '__main__':
import sys
sys.exit(main(sys.argv))