How to customise the NavigationToolbar2 toolbar in matplotlib

My CurrentCost desktop software is written in Python, and uses the matplotlib library for plotting graphs.

I am a big fan of matplotlib – you can create some very cool graphs with it. It’s not without it’s issues… for example, I’ve still yet to work out how to do realtime graphing with it that isn’t massively inefficient and resource intensive. But that’s for another post :-)

One of the nice things that you get with it is a toolbar that lets the user switch between different modes that the mouse supports – zooming, panning, and so on.

Adding the toolbar is straightforward – I create an object of the class NavigationToolbar2Wx and add it to a sizer which in turn I add to the graph Plot.

self.toolbar = Toolbar(self.canvas)
self.toolbar.Realize()
sizer = wx.BoxSizer(wx.VERTICAL)
sizer.Add(self.canvas, 1, wx.EXPAND)
sizer.Add(self.toolbar, 0 , wx.LEFT | wx.EXPAND)
self.SetSizer(sizer)

The NavigationToolbar2 toolbar provides a number of features but it doesn’t exactly meet my needs so last night I had a go at customising it.

I wanted to:

  • remove the configure subplots button – as I don’t use subplots in my graphs
  • add a couple of new buttons – for different navigation abilities

I couldn’t find this documented clearly anywhere, so thought it was worth sharing the code that I came up with.

Creating a custom toolbar

Step one for customising the toolbar is to create a subclass of NavigationToolbar2Wx, and add this to your Plot instead.

class MyCustomToolbar(NavigationToolbar2Wx): 
    def __init__(self, plotCanvas):
        NavigationToolbar2Wx.__init__(self, plotCanvas)

You’ve now got something you can start customising.

Removing buttons from the toolbar

The most straightforward approach I’ve settled on is to run the init method from the NavigationToolbar2Wx superclass, and then go back and remove the bits I don’t want.

class MyCustomToolbar(NavigationToolbar2Wx): 
    def __init__(self, plotCanvas):
        # create the default toolbar
        NavigationToolbar2Wx.__init__(self, plotCanvas)
        # remove the unwanted button
        POSITION_OF_CONFIGURE_SUBPLOTS_BTN = 6
        self.DeleteToolByPos(POSITION_OF_CONFIGURE_SUBPLOTS_BTN) 

Adding new buttons to the toolbar

You can pan matplotlib graphs by dragging the mouse across but a couple of users of my CurrentCost app said that they found this difficult and clunky, and would rather have another way to scroll the graph from the toolbar.

So I added a couple of extra buttons: one which scrolls the graph a screen to the left, and one which scrolls the graph a screen to the right.

I’m using stock images from matplotlib for the button icons, but you could use your own custom images – either way you need to use _load_bitmap to load the image to use for the icon.

In the action methods that are called when the custom buttons are clicked, I use axes.get_xlim() to get the min and max values for the x axis, then get the difference between them to work out the horizontal length of the graph. I can then move the x axis min and max by that amount to scroll the graph by one screen.

class MyCustomToolbar(NavigationToolbar2Wx): 
    ON_CUSTOM_LEFT  = wx.NewId()
    ON_CUSTOM_RIGHT = wx.NewId()

    def __init__(self, plotCanvas):
        # create the default toolbar
        NavigationToolbar2Wx.__init__(self, plotCanvas)
        # add new toolbar buttons 
        self.AddSimpleTool(self.ON_CUSTOM_LEFT, _load_bitmap('stock_left.xpm'),
                           'Pan to the left', 'Pan graph to the left')
        wx.EVT_TOOL(self, self.ON_CUSTOM_LEFT, self._on_custom_pan_left)
        self.AddSimpleTool(self.ON_CUSTOM_RIGHT, _load_bitmap('stock_right.xpm'),
                           'Pan to the right', 'Pan graph to the right')
        wx.EVT_TOOL(self, self.ON_CUSTOM_RIGHT, self._on_custom_pan_right)

    # pan the graph to the left
    def _on_custom_pan_left(self, evt):
        ONE_SCREEN = 1
        axes = self.canvas.figure.axes[0]
        x1,x2 = axes.get_xlim()
        ONE_SCREEN = x2 - x1
        axes.set_xlim(x1 - ONE_SCREEN, x2 - ONE_SCREEN)
        self.canvas.draw()

    # pan the graph to the right
    def _on_custom_pan_right(self, evt):
        ONE_SCREEN = 1
        axes = self.canvas.figure.axes[0]
        x1,x2 = axes.get_xlim()
        ONE_SCREEN = x2 - x1
        axes.set_xlim(x1 + ONE_SCREEN, x2 + ONE_SCREEN)
        self.canvas.draw()

Now back to thinking about how to do realtime graphing with matplotlib… :-)

Tags: , ,

4 Responses to “How to customise the NavigationToolbar2 toolbar in matplotlib”

  1. The-Maxx says:

    Thank you very much for your great post. That was exactly was i was looking for!
    ATM i am trying to change the style of the toolbar in wxpython, but i am not sure how to do it for the matplotlib Toolbar. Is there an easy way to build a Matplotlib Toolbar from scratch? Thanks in advance, Maxx

  2. dale says:

    I don’t know – I’ve not tried. The source code for the toolbar is all open, so you could take a look and decide how much of it you think you will need.

    That said, I prefer to subclass the existing one because it makes it more likely that I will gain from future improvements to it. Forking it or re-implementing my own copy limits future improvements.

  3. Jonathan says:

    I tried to remove buttons the same way you describe, but it seems it doesn’t work when using the Qt4 backends since DeleteToolByPos is not recognized (‘MyCustomToolbar’ object has no attribute ‘DeleteToolByPos’) :-((

    You maybe now a workaround for PyQt4?

    Thanks in advance, Jonathan

  4. Juampy says:

    Thanks for the post. It worked perfectly.

    I have a problem now, and I couldn’t find the right answer. I’ve got a FigCanvas with more than 10 plots sharing the X axis. Since they don’t fit in the screen, I want to put the canvas in a scroll panel so the user can scroll up and down the plots. The thing is that when I add the toolbar, it is added to the same scrolling panel where the FigCanvas is. So the toolbar “goes away” from the screen as soon as the user scrolls. My question is, did you find any way to put the canvas and the toolbar in two different wx.Panels?
    Thanks in advance,
    Juan Pablo