Hiding lines after showing a pyplot figure
I'm using pyplot to display a line graph of up to 30 lines. I would like to add a way to quickly show and hide individual lines on the graph. Pyplot does have a menu where you can edit line properties to change the color or style, but its rather clunky when you want to hide lines to isolate the one you're interested in. Ideally, I'd like to use checkboxes on the legend to show and hide lines. (Similar to showing and hiding layers in image editors like Paint.Net) I'm not sure if this is possible with pyplot, so I am open to other modules as long as they're somewhat easy to distribute.
If you'd like, you can hook up a callback to the legend that will show/hide lines when they're clicked. There's a simple example here: http://matplotlib.org/examples/event_handling/legend_picking.html
Here's a "fancier" example that should work without needing to manually specify the relationship of the lines and legend markers (Also has a few more features).
(Updated version in August 2019, as a response to repeated reports about this not working correctly; now it should! For the old version see version history)
import numpy as np
import matplotlib.pyplot as plt
def main():
x = np.arange(10)
fig, ax = plt.subplots()
for i in range(1, 31):
ax.plot(x, i * x, label=r'$y={}x$'.format(i))
ax.legend(loc='upper left', bbox_to_anchor=(1.05, 1),
ncol=2, borderaxespad=0)
fig.subplots_adjust(right=0.55)
fig.suptitle('Right-click to hide all\nMiddle-click to show all',
va='top', size='large')
leg = interactive_legend()
return fig, ax, leg
def interactive_legend(ax=None):
if ax is None:
ax = plt.gca()
if ax.legend_ is None:
ax.legend()
return InteractiveLegend(ax.get_legend())
class InteractiveLegend(object):
def __init__(self, legend):
self.legend = legend
self.fig = legend.axes.figure
self.lookup_artist, self.lookup_handle = self._build_lookups(legend)
self._setup_connections()
self.update()
def _setup_connections(self):
for artist in self.legend.texts + self.legend.legendHandles:
artist.set_picker(10) # 10 points tolerance
self.fig.canvas.mpl_connect('pick_event', self.on_pick)
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
def _build_lookups(self, legend):
labels = [t.get_text() for t in legend.texts]
handles = legend.legendHandles
label2handle = dict(zip(labels, handles))
handle2text = dict(zip(handles, legend.texts))
lookup_artist = {}
lookup_handle = {}
for artist in legend.axes.get_children():
if artist.get_label() in labels:
handle = label2handle[artist.get_label()]
lookup_handle[artist] = handle
lookup_artist[handle] = artist
lookup_artist[handle2text[handle]] = artist
lookup_handle.update(zip(handles, handles))
lookup_handle.update(zip(legend.texts, handles))
return lookup_artist, lookup_handle
def on_pick(self, event):
handle = event.artist
if handle in self.lookup_artist:
artist = self.lookup_artist[handle]
artist.set_visible(not artist.get_visible())
self.update()
def on_click(self, event):
if event.button == 3:
visible = False
elif event.button == 2:
visible = True
else:
return
for artist in self.lookup_artist.values():
artist.set_visible(visible)
self.update()
def update(self):
for artist in self.lookup_artist.values():
handle = self.lookup_handle[artist]
if artist.get_visible():
handle.set_visible(True)
else:
handle.set_visible(False)
self.fig.canvas.draw()
def show(self):
plt.show()
if __name__ == '__main__':
fig, ax, leg = main()
plt.show()
This allows you to click on legend items to toggle their corresponding artists on/off. For example, you can go from this:
To this:
Thanks for the post! I extended the class above such that it can handle multiple legends - such as for example if you are using subplots. (I am sharing it here since i could not find any other example somewhere else... and it might be handy for someone else...)
class InteractiveLegend(object):
def __init__(self):
self.legends = []
self.figures = []
self.lookup_artists = []
self.lookup_handles = []
self.host = socket.gethostname()
def add_legends(self, legend):
self.legends.append(legend)
def init_legends(self):
for legend in self.legends:
self.figures.append(legend.axes.figure)
lookup_artist, lookup_handle = self._build_lookups(legend)
#print("init", type(lookup))
self.lookup_artists.append(lookup_artist)
self.lookup_handles.append(lookup_handle)
self._setup_connections()
self.update()
def _setup_connections(self):
for legend in self.legends:
for artist in legend.texts + legend.legendHandles:
artist.set_picker(10) # 10 points tolerance
for figs in self.figures:
figs.canvas.mpl_connect('pick_event', self.on_pick)
figs.canvas.mpl_connect('button_press_event', self.on_click)
def _build_lookups(self, legend):
labels = [t.get_text() for t in legend.texts]
handles = legend.legendHandles
label2handle = dict(zip(labels, handles))
handle2text = dict(zip(handles, legend.texts))
lookup_artist = {}
lookup_handle = {}
for artist in legend.axes.get_children():
if artist.get_label() in labels:
handle = label2handle[artist.get_label()]
lookup_handle[artist] = handle
lookup_artist[handle] = artist
lookup_artist[handle2text[handle]] = artist
lookup_handle.update(zip(handles, handles))
lookup_handle.update(zip(legend.texts, handles))
#print("build", type(lookup_handle))
return lookup_artist, lookup_handle
def on_pick(self, event):
#print event.artist
handle = event.artist
for lookup_artist in self.lookup_artists:
if handle in lookup_artist:
artist = lookup_artist[handle]
artist.set_visible(not artist.get_visible())
self.update()
def on_click(self, event):
if event.button == 3:
visible = False
elif event.button == 2:
visible = True
else:
return
for lookup_artist in self.lookup_artists:
for artist in lookup_artist.values():
artist.set_visible(visible)
self.update()
def update(self):
for idx, lookup_artist in enumerate(self.lookup_artists):
for artist in lookup_artist.values():
handle = self.lookup_handles[idx][artist]
if artist.get_visible():
handle.set_visible(True)
else:
handle.set_visible(False)
self.figures[idx].canvas.draw()
def show(self):
plt.show()
use it as follow:
leg1 = ax1.legend(loc='upper left', bbox_to_anchor=(1.05, 1), ncol=2, borderaxespad=0)
leg2 = ax2.legend(loc='upper left', bbox_to_anchor=(1.05, 1), ncol=2, borderaxespad=0)
fig.subplots_adjust(right=0.7)
interactive_legend = InteractiveLegend()
interactive_legend.add_legends(leg1)
interactive_legend.add_legends(leg2)
interactive_legend.init_legends()
interactive_legend.show()
Inspired by @JoeKington's answer, here is what I use (a slightly modified version, that doesn't require ax, fig
but can work directly with plt.plot(...)
; also plt.legend()
is kept outside out the scope of the main object):
Ready-to-use example pltinteractivelegend.py
:
import numpy as np
import matplotlib.pyplot as plt
class InteractiveLegend(object):
def __init__(self, legend=None):
if legend == None:
legend = plt.gca().get_legend()
self.legend = legend
self.fig = legend.axes.figure
self.lookup_artist, self.lookup_handle = self._build_lookups(legend)
self._setup_connections()
self.update()
def _setup_connections(self):
for artist in self.legend.texts + self.legend.legendHandles:
artist.set_picker(10) # 10 points tolerance
self.fig.canvas.mpl_connect('pick_event', self.on_pick)
self.fig.canvas.mpl_connect('button_press_event', self.on_click)
def _build_lookups(self, legend):
labels = [t.get_text() for t in legend.texts]
handles = legend.legendHandles
label2handle = dict(zip(labels, handles))
handle2text = dict(zip(handles, legend.texts))
lookup_artist = {}
lookup_handle = {}
for artist in legend.axes.get_children():
if artist.get_label() in labels:
handle = label2handle[artist.get_label()]
lookup_handle[artist] = handle
lookup_artist[handle] = artist
lookup_artist[handle2text[handle]] = artist
lookup_handle.update(zip(handles, handles))
lookup_handle.update(zip(legend.texts, handles))
return lookup_artist, lookup_handle
def on_pick(self, event):
handle = event.artist
if handle in self.lookup_artist:
artist = self.lookup_artist[handle]
artist.set_visible(not artist.get_visible())
self.update()
def on_click(self, event):
if event.button == 3:
visible = False
elif event.button == 2:
visible = True
else:
return
for artist in self.lookup_artist.values():
artist.set_visible(visible)
self.update()
def update(self):
for artist in self.lookup_artist.values():
handle = self.lookup_handle[artist]
if artist.get_visible():
handle.set_visible(True)
else:
handle.set_visible(False)
self.fig.canvas.draw()
if __name__ == '__main__':
for i in range(20):
plt.plot(np.random.randn(1000), label=i)
plt.legend()
leg = InteractiveLegend()
plt.show()
Usage as a library:
import numpy as np
import matplotlib.pyplot as plt
import pltinteractivelegend
for i in range(20):
plt.plot(np.random.randn(1000), label=i)
plt.legend()
leg = pltinteractivelegend.InteractiveLegend() # mandatory: keep the object with leg = ...; else it won't work
plt.show()