Search code examples
pythonmatplotlibwxpython

(wxpython) make the matplotlib NavigationToolbar static for a figure inside scrolledpanel


I have a Matplotlib figure with a lot of subplots which needs a ScrolledPanel. Now I want a navigation toolbar for the figure which remains static when the figure panel is scrolled. I tried adding it to another panel than the canvas's parent but that does not work(navigation toolbar remains inside canvas's parent only).
Is there any way to have the matplotlib navigation toolbar remain static but making rest of the canvas scrollable?
Here's my complete code:

import wx
import wx.lib.scrolledpanel

import matplotlib
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg as NavigationToolbar
from matplotlib.figure import Figure

import wx.lib.inspection

matplotlib.use('WXAgg')


class PlotDemoApp(object):
    def __init__(self, data):
        self.app = wx.App()
        self.frame = PlotCanvas(None, -1, "PlotCanvas", data)
        self.frame.Show(True)
        wx.lib.inspection.InspectionTool().Show()
        self.app.MainLoop()


class PlotCanvas(wx.Frame):
    def __init__(self, parent, wxid, title, data):
        wx.Frame.__init__(self, parent, wxid, title)

        self.box_main = wx.BoxSizer(wx.VERTICAL)

        panel_lower = wx.lib.scrolledpanel.ScrolledPanel(self, size=(2500,-1))
        panel_lower.SetupScrolling()
        panel_lower.SetBackgroundColour("White")
        self.box_lower = wx.BoxSizer(wx.HORIZONTAL)

        box_local = wx.BoxSizer(wx.VERTICAL)
        plt = Figure(figsize=(95,10))
        num_columns = len(data.columns)
        axes_1 = plt.add_subplot(1, num_columns, 1)
        data_numpy = data[data.columns[0]].to_numpy()
        depth = data.index.to_numpy()
        plt.gca().invert_yaxis()
        plt.subplots_adjust(left=0.01, right=1.00, top=0.95, bottom=0.05)
        axes_1.plot(data_numpy, depth)
        for i in range(1, num_columns):
            axes_tmp = plt.add_subplot(1, num_columns, i+1, sharey=axes_1)
            axes_tmp.set(xlabel=data.columns[i], ylabel='Depth', title='Depth vs ' + data.columns[i])

            data_numpy = data[data.columns[i]].to_numpy()

            plt.gca().invert_yaxis()
            axes_tmp.plot(data_numpy, depth)
        canvas = FigureCanvas(panel_lower, -1, plt)
        canvas.draw()
        box_local.Add(canvas, 1, wx.EXPAND)
        panel_nav = wx.Panel(self)
        box_nav = wx.BoxSizer(wx.HORIZONTAL)
        toolbar = NavigationToolbar(canvas)
        box_nav.Add(toolbar)
        panel_nav.SetSizer(box_nav)
        toolbar.Realize()
        self.box_main.Add(panel_nav, 0, wx.CENTER)
        self.box_lower.Add(box_local, 1, wx.EXPAND)

        self.box_main.Add(panel_lower, 1, wx.EXPAND)
        panel_lower.SetSizer(self.box_lower)
        self.SetSizer(self.box_main)
        self.box_main.Layout()

data here is a pandas dataframe
Sample data:

| index | BS   | CAL  |
|-------|------|------|
| 162   | 17.5 | 17.4 |
| 163   | 17.8 | 17.7 |
| 164   | 17.8 | 17.9 |

Solution

  • Hopefully this is what you are after or at the least points you in the right direction.

    The matplotlib navigation toolbar seems to be welded to the plot itself but it can be hidden.

    Whilst it remains hidden, its functions are still available, so we can create our own toolbar and assign to it the functions in the hidden toolbar.

    Like this:

    import wx
    import wx.lib.scrolledpanel as scrolled
    import pandas as pd
    from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
    from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg as NavigationToolbar
    from matplotlib.figure import Figure
    
    # images in /usr/share/matplotlib/mpl-data/images
    # Your system may differ!
    
    class PlotPanel(scrolled.ScrolledPanel):
        def __init__(self,parent):
            scrolled.ScrolledPanel.__init__(self, parent)
            self.SetupScrolling(scroll_x=True, scroll_y=True, scrollToTop=False, scrollIntoView=False)
            self.ShowScrollbars(True,True)
            self.plt = Figure(figsize=(95,10))
            self.canvas = FigureCanvas(self,-1, self.plt)
            self.toolbar = NavigationToolbar(self.canvas)
    
            #Hide the Matplotlib toolbar because we are going to create own own
            #but use the functions of this toolbar.
            self.toolbar.Hide()
    
        def plot(self):
            d = {'BS':[17.54,17.55,17.54,17.53,17.55,17.54],'CAL':[17.46,17.47,17.49,17.44,17.47,17.49]}
            data = pd.DataFrame(d, index=['1','2','3','4','5','6'])
    
            num_columns = len(data.columns)
            axes_1 = self.plt.add_subplot(1, num_columns, 1)
            #data_numpy = data[data.columns[0]].to_numpy()  # My version of pandas doesn't have .to_numpy()
            data_numpy = data[data.columns[0]]
            #depth = data.index.to_numpy()
            depth = data.index
            self.plt.gca().invert_yaxis()
            self.plt.subplots_adjust(left=0.01, right=0.50, top=0.95, bottom=0.05)
            axes_1.plot(data_numpy, depth)
            for i in range(0, num_columns):
                axes_tmp = self.plt.add_subplot(1, num_columns, i+1, sharey=axes_1)
                axes_tmp.set(xlabel=data.columns[i], ylabel='Depth', title='Depth vs ' + data.columns[i])
    
                #data_numpy = data[data.columns[i]].to_numpy()
                data_numpy = data[data.columns[i]]
    
                self.plt.gca().invert_yaxis()
                axes_tmp.plot(data_numpy, depth)
            self.canvas.draw()
            self.SetSize(self.canvas.GetSize())
    
    class TestFrame(wx.Frame):
        def __init__(self,parent,title):
            wx.Frame.__init__(self,parent,title=title,size=(600,600))
    
            self.p1 = PlotPanel(self)
    
            #Create our own toolbar
            toolbar = self.CreateToolBar(style=wx.TB_HORIZONTAL|wx.TB_DOCKABLE|wx.TB_TEXT)
            hometool = toolbar.AddTool(wx.ID_ANY, 'Home', wx.Bitmap('/usr/share/matplotlib/mpl-data/images/home.png'))
            backtool = toolbar.AddTool(wx.ID_ANY, 'Back', wx.Bitmap('/usr/share/matplotlib/mpl-data/images/back.png'))
            fwdtool = toolbar.AddTool(wx.ID_ANY, 'Forward', wx.Bitmap('/usr/share/matplotlib/mpl-data/images/forward.png'))
            pantool = toolbar.AddTool(wx.ID_ANY, 'Pan', wx.Bitmap('/usr/share/matplotlib/mpl-data/images/move.png'))
            zoomtool = toolbar.AddTool(wx.ID_ANY, 'Zoom', wx.Bitmap('/usr/share/matplotlib/mpl-data/images/zoom_to_rect.png'))
            subtool = toolbar.AddTool(wx.ID_ANY, 'Subplots', wx.Bitmap('/usr/share/matplotlib/mpl-data/images/subplots.png'))
            savetool = toolbar.AddTool(wx.ID_ANY, 'Save', wx.Bitmap('/usr/share/matplotlib/mpl-data/images/filesave.png'))
    
            self.Bind(wx.EVT_TOOL, self.home, hometool)
            self.Bind(wx.EVT_TOOL, self.back, backtool)
            self.Bind(wx.EVT_TOOL, self.fwd, fwdtool)
            self.Bind(wx.EVT_TOOL, self.pan, pantool)
            self.Bind(wx.EVT_TOOL, self.zoom, zoomtool)
            self.Bind(wx.EVT_TOOL, self.sub, subtool)
            self.Bind(wx.EVT_TOOL, self.save, savetool)
            toolbar.Realize()
    
            sizer = wx.BoxSizer(wx.VERTICAL)
            sizer.Add(self.p1,1,wx.EXPAND,10)
            self.statusbar=self.CreateStatusBar()
            self.SetSizer(sizer)
            self.Show()
            self.plot()
    
        #Self defined Navigation Toolbar controls used to call hidden matplotlib Toolbar functions
        def home(self,event):
            self.statusbar.SetStatusText("Home")
            self.p1.toolbar.home()
            # Also scroll panel to start position
            self.p1.Scroll(0,0)
    
        def back(self,event):
            self.statusbar.SetStatusText("Back")
            self.p1.toolbar.back()
    
        def fwd(self,event):
            self.statusbar.SetStatusText("Fwd")
            self.p1.toolbar.forward()
    
        def pan(self,event):
            self.statusbar.SetStatusText("Pan")
            self.p1.toolbar.pan()
    
        def zoom(self,event):
            self.statusbar.SetStatusText("Zoom")
            self.p1.toolbar.zoom()
    
        def sub(self,event):
            self.statusbar.SetStatusText("Subplots")
            self.p1.toolbar.configure_subplots(event)
    
        def save(self,event):
            self.statusbar.SetStatusText("Save")
            self.p1.toolbar.save_figure()
    
        def plot(self):
            self.p1.plot()
    
    app = wx.App(redirect=False)
    frame = TestFrame(None,"Plot in Scrolled panel with replacement Navigation")
    app.MainLoop()
    

    enter image description here

    You will notice that I made the replacement toolbar Dockable, so I can move the toolbar anywhere I want on the screen. It is re-attachable.

    enter image description here

    Because we are defining our own toolbar it can be anywhere we want, just look into the options for wx.Toolbar.
    Here, I lost the text and made it vertical.

    enter image description here