42

I'm drawing a legend on an axes object in matplotlib but the default positioning which claims to place it in a smart place doesn't seem to work. Ideally, I'd like to have the legend be draggable by the user. How can this be done?

Juha
  • 2,053
  • 23
  • 44
Adam Fraser
  • 6,255
  • 10
  • 42
  • 54
  • Adam: given that this was substantial, thorough, and relevant enough to include in the Matplotlib distro, and given that (i think) you removed your original Question, would you mind including a couple of sentences at the top of this Q so users can get an idea of what this code is for (to save them from having to read through the code itself). And nice work, by the way +1 from me. – doug Mar 30 '10 at 06:12
  • Thanks doug. I phrased the question at the top as you suggested. Hopefully this will be helpful. :] – Adam Fraser Mar 30 '10 at 14:48
  • can this be extended for secondary axis? – denfromufa Sep 29 '14 at 06:34

3 Answers3

30

Note: This is now built into matplotlib

leg = plt.legend()
if leg:
    leg.draggable()

will work as expected


Well, I found bits and pieces of the solution scattered among mailing lists. I've come up with a nice modular chunk of code that you can drop in and use... here it is:

class DraggableLegend:
    def __init__(self, legend):
        self.legend = legend
        self.gotLegend = False
        legend.figure.canvas.mpl_connect('motion_notify_event', self.on_motion)
        legend.figure.canvas.mpl_connect('pick_event', self.on_pick)
        legend.figure.canvas.mpl_connect('button_release_event', self.on_release)
        legend.set_picker(self.my_legend_picker)

    def on_motion(self, evt):
        if self.gotLegend:
            dx = evt.x - self.mouse_x
            dy = evt.y - self.mouse_y
            loc_in_canvas = self.legend_x + dx, self.legend_y + dy
            loc_in_norm_axes = self.legend.parent.transAxes.inverted().transform_point(loc_in_canvas)
            self.legend._loc = tuple(loc_in_norm_axes)
            self.legend.figure.canvas.draw()

    def my_legend_picker(self, legend, evt): 
        return self.legend.legendPatch.contains(evt)   

    def on_pick(self, evt): 
        if evt.artist == self.legend:
            bbox = self.legend.get_window_extent()
            self.mouse_x = evt.mouseevent.x
            self.mouse_y = evt.mouseevent.y
            self.legend_x = bbox.xmin
            self.legend_y = bbox.ymin 
            self.gotLegend = 1

    def on_release(self, event):
        if self.gotLegend:
            self.gotLegend = False

...and in your code...

def draw(self): 
    ax = self.figure.add_subplot(111)
    scatter = ax.scatter(np.random.randn(100), np.random.randn(100))


legend = DraggableLegend(ax.legend())

I emailed the Matplotlib-users group and John Hunter was kind enough to add my solution it to SVN HEAD.

On Thu, Jan 28, 2010 at 3:02 PM, Adam Fraser wrote:

I thought I'd share a solution to the draggable legend problem since it took me forever to assimilate all the scattered knowledge on the mailing lists...

Cool -- nice example. I added the code to legend.py. Now you can do

leg = ax.legend()
leg.draggable()

to enable draggable mode. You can repeatedly call this func to toggle the draggable state.

I hope this is helpful to people working with matplotlib.

Juha
  • 2,053
  • 23
  • 44
Adam Fraser
  • 6,255
  • 10
  • 42
  • 54
  • 4
    Note that this is a great but really old version, and you should better use set_draggable (see [my answer](https://stackoverflow.com/a/53717995/2964927) below) – borgr Dec 11 '18 at 05:38
16

In even newer versions (3.0.2) it is deprecated and will potentially be representing a property in future versions (hence, it will not be callable).

plot(range(10), range(10), label="test label")
plot(range(10), [5 for x in range(10)], label="another test")
plt.legend().set_draggable(True)
borgr
  • 20,175
  • 6
  • 25
  • 35
15

In newer versions of Matplotlib (v1.0.1), this is built-in.

def draw(self): 
    ax = self.figure.add_subplot(111)
    scatter = ax.scatter(np.random.randn(100), np.random.randn(100))
    legend = ax.legend()
    legend.draggable(state=True)

If you are using matplotlib interactively (for example, in IPython's pylab mode).

plot(range(10), range(10), label="test label")
plot(range(10), [5 for x in range(10)], label="another test")
l = legend()
l.draggable(True)
Tim Swast
  • 14,091
  • 4
  • 38
  • 61