4

I have a matplotlib figure embedded in a wxpython frame with a few sizers. Everything works fine until I include a legend but then the sizers don't seem to be working with the legend.

Even when I resize the window by dragging at the corner, the main figure changes size, but only the edge of the legend is ever shown.

enter image description here

That is, note that the legend is not visible in the wxFrame.

import wx
import matplotlib as mpl
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as Canvas
from random import shuffle  

class PlotFrame(wx.Frame):

    def __init__(self):     

        wx.Frame.__init__(self, None, -1, title="Plot", size=(-1, -1))
        self.main_panel = wx.Panel(self, -1)
        self.plot_panel = PlotPanel(self.main_panel)

        s0 = wx.BoxSizer(wx.VERTICAL)
        s0.Add(self.main_panel, 1, wx.EXPAND)
        self.SetSizer(s0)
        self.s0 = s0

        self.main_sizer = wx.BoxSizer(wx.VERTICAL)        
        self.main_sizer.Add(self.plot_panel, 1, wx.EXPAND)        
        self.main_panel.SetSizer(self.main_sizer)       

class PlotPanel(wx.Panel):

    def __init__(self, parent, id = -1, dpi = None, **kwargs):
        wx.Panel.__init__(self, parent, id=id, **kwargs)
        self.figure = mpl.figure.Figure(dpi=dpi, figsize=(2,2))
        self.canvas = Canvas(self, -1, self.figure)

        sizer = wx.BoxSizer(wx.VERTICAL)
        sizer.Add(self.canvas,1,wx.EXPAND)
        self.SetSizer(sizer)
        sizer.SetMinSize((600, 500))
        self.sizer = sizer

def test(plot_panel):
    axes = plot_panel.figure.gca()
    for c in ['r', 'b', 'k']:
        vals = [20, 30, 40, 50, 80, 20, 50, 60, 70, 70, 80]
        shuffle(vals)
        axes.plot(range(len(vals)), vals, "-o", color=c, label=c*10)
    legend = axes.legend(loc='center left', bbox_to_anchor=(1.05, 0.5))
    return legend

if __name__=="__main__":
    app = wx.PySimpleApp()
    frame = PlotFrame()
    legend = test(frame.plot_panel)
    frame.Fit()
    print "legend frame pre show: ", legend.get_frame()
    frame.Show(True)
    print "legend frame post show:", legend.get_frame()
    frame.Fit()
    app.MainLoop()

Edit:
For a solution to be useful to me, I would like it to look good when the figure is automatically drawn by the program, so adjustment parameters can be hard coded in the program, or, for example, on a window resize event, but not adjusted by hand for each plot. The main things that I expect to change here are: 1) the lengths of the labels (from, say, 1 to 25 characters), 2) the windows size (usually by the user dragging around the corner, and 3) the number of points and lines. (Also, if it matters, eventually, I'll want to have dates on the bottom axis.)

I've put the legend outside of the axes so that it won't cover any data points, and I'd prefer that it stay to the right of the axes.

I'm using Python 2.6.6, wxPython 2.8.12.1, and matplotlib 1.1.0 and am stuck with these for now.

tom10
  • 67,082
  • 10
  • 127
  • 137
  • `Fit` is a wx level function that will resize the _figure_ to fit in your window, the legend is _part of_ the figure widget. The bounding box changes size after the draw because the size of the text isn't sorted out until the figure is drawn. – tacaswell Sep 12 '13 at 19:52
  • I know what you said. It still doesn't solve the problem. – tom10 Sep 12 '13 at 20:00
  • You want a) the legend to not be on the `axes` and b) to be able to see the legend. The only way to do that with the legend to the right is to make your axes take up less space in _figure fraction_ units. You can then make your figure wider be re-sizing your window. – tacaswell Sep 12 '13 at 20:15
  • It seems like what is happening is either a bug or that I'm doing something wrong, but intuitively,the size of the figure/canvas that wx uses should also include the legend, even if the legend is outside of the axes. Or am I wrong about this? I'm hoping someone can tell me how to use the total size correctly, but otherwise I'll just write it into the sizer myself. It's a bit ugly code-wise, but I think it will make for a nicer looking figure than manually changing the sizes, eg, using figure fraction units. – tom10 Sep 12 '13 at 20:31
  • It might be a lack-of-feature, but it is not a bug. – tacaswell Sep 12 '13 at 20:38
  • http://stackoverflow.com/questions/10101700/moving-matplotlib-legend-outside-of-the-axis-makes-it-cutoff-by-the-figure-box – tacaswell Sep 12 '13 at 23:48
  • please see my newest round of edits – tacaswell Sep 17 '13 at 01:39
  • Did you see the last round of edits to my answer? – tacaswell Sep 22 '13 at 05:43

1 Answers1

5

It is re-sizing correctly, you just didn't tell it to do what you want it to do.

The problem is this line:

axes.legend(loc='center left', bbox_to_anchor=(1.05, 0.5))

Pretty sure the bbox_to_anchor kwarg is over-ridding the loc kwarg and you are pegging the bottom left of the legend to (1.05, 0.5) in axes units. If the axes expands to fill your window, the left edge of the legend will always be 5% of the width axes to the right of the right edge of you axes, hence always out of view.

You either need to put your legend someplace else or shrink your axes (in figure fraction).

option 1 move the legend:

axes.legend(bbox_to_anchor=(0.5, 0.5)) #find a better place this is in the center

option 2 move the axes + resize the figure:

axes.set_position([.1, .1, .5, .8]) # units are in figure fraction

set_position

fig = figure()
axes = fig.add_subplot(111)

for c in ['r', 'b', 'k']:
    vals = [20, 30, 40, 50, 80, 20, 50, 60, 70, 70, 80]
    shuffle(vals)
    axes.plot(range(len(vals)), vals, "-o", color=c, label=c*10)

legend = axes.legend(loc='center left', bbox_to_anchor=(1.05, 0.5))

enter image description here

# adjust the figure size (in inches)
fig.set_size_inches(fig.get_size_inches() * np.array([1.5, 1]), forward=True)

# and the axes size (in figure fraction)
# to (more-or-less) preserve the aspect ratio of the original axes
# and show the legend
pos = np.array(axes.get_position().bounds)
pos[2] = .66
axes.set_position(pos)

enter image description here

option 3: automate option 2

fig = figure() # use plt to set this up for demo purposes
axes = fig.add_subplot(111) # add a subplot

# control paramters 
left_pad = .05 
right_pad = .05

# plot data
for c in ['r', 'b', 'k']:
    vals = [20, 30, 40, 50, 80, 20, 50, 60, 70, 70, 80]
    shuffle(vals)
    axes.plot(range(len(vals)), vals, "-o", color=c, label=c*10)
# set axes labels
axes.set_xlabel('test x')
axes.set_ylabel('test y')

# make the legend
legend = axes.legend(loc='center left', bbox_to_anchor=(1 + left_pad, 0.5))

# function to 'squeeze' the legend into place
def squeeze_legend(event):
    fig.tight_layout()
    right_frac = 1 - legend.get_window_extent().width / fig.get_window_extent().width - left_pad - right_pad
    fig.subplots_adjust(right=right_frac)
    fig.canvas.draw()

# call it so the first draw is right
squeeze_legend()

# use the resize event call-back to make sure it works even if the window is re-sized
fig.canvas.mpl_connect('resize_event', squeeze_legend)
tacaswell
  • 84,579
  • 22
  • 210
  • 199
  • OK, I see your edits, but this isn't what I'm looking for. Instead, I want a good way to get the plot to fit nicely in the frame while the legend is outside of the main figure. Thanks for the ideas though. – tom10 Sep 12 '13 at 19:32
  • `matplotlib` can only draw to it's canvas, and it's canvas is the size of the `figure` widget. Be a bit careful about the nomenclature, the `figure` is the wiget and the `axes` is the white box you can draw lines on to. You can put the legend outside of the `axes`, but you can't put _anything_ outside of the `figure` – tacaswell Sep 12 '13 at 19:37
  • Thanks for your input, but this just won't work for me. I need a solution that will work without tweeking each plot by hand, and that can be resized by dragging, have different length labels, etc, and still look good. – tom10 Sep 12 '13 at 23:21
  • well, you can't will functionality that does not exist (to my knowledge, I would say I'm 80% sure on this). Look at how `tight_layout` works underneath. You can ask the aritists (the axes and the legend) how big they are on the rendered figure and then programtically re-size them. – tacaswell Sep 12 '13 at 23:44
  • A side-ways method would be to look into the `DraggableLegend` http://matplotlib.org/api/legend_api.html#matplotlib.legend.DraggableLegend http://stackoverflow.com/questions/2539477/how-to-draggable-legend-in-matplotlib – tacaswell Sep 12 '13 at 23:46