2011-01-19 7 views
8

Estoy escribiendo una aplicación wx/matplotlib, y estoy teniendo considerables dificultades para agregar una herramienta nueva a matplotlib NavigationToolbar.Agregue nuevos modos de navegación en matplotlib

Básicamente quiero agregar herramientas para la selección (marquesina, lazo, etc.) que activarán el modo de mouse de las subtramas controladas. Hasta el momento no he podido encontrar ninguna característica que me permita hacer esto fácilmente.

yo, sin embargo, acaba de descubrir esta función que parecía que sería de gran ayuda: http://matplotlib.sourceforge.net/api/axes_api.html?highlight=set_navigate_mode#matplotlib.axes.Axes.set_navigate_mode

Por desgracia, como el aviso indica, que en realidad no me ayuda.

¿Alguien tiene una idea de cómo hacer esto? A continuación se muestra un ejemplo reducido que muestra lo lejos que he llegado. El ícono de marcador se usa en lugar de mi ícono de lazo, y he eliminado la funcionalidad de lazo para abreviar.

import wx 
from matplotlib.patches import Rectangle 
from matplotlib.widgets import Lasso 
from matplotlib.figure import Figure 
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg 
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg as NavigationToolbar 

class ScatterPanel(FigureCanvasWxAgg): 
    ''' 
    Contains the guts for drawing scatter plots. 
    ''' 
    def __init__(self, parent, **kwargs): 
     self.figure = Figure() 
     FigureCanvasWxAgg.__init__(self, parent, -1, self.figure, **kwargs) 
     self.canvas = self.figure.canvas 
     self.SetMinSize((100,100)) 
     self.figure.set_facecolor((1,1,1)) 
     self.figure.set_edgecolor((1,1,1)) 
     self.canvas.SetBackgroundColour('white') 

     self.subplot = self.figure.add_subplot(111) 
     self.navtoolbar = None 
     self.lasso = None 
     self.redraw() 

     self.canvas.mpl_connect('button_press_event', self.on_press) 
     self.canvas.mpl_connect('button_release_event', self.on_release) 

    def lasso_callback(self, verts): 
     pass 

    def on_press(self, evt): 
     if evt.button == 1: 
      if self.canvas.widgetlock.locked(): 
       return 
      if evt.inaxes is None: 
       return 
      if self.navtoolbar.mode == 'lasso': 
       self.lasso = Lasso(evt.inaxes, (evt.xdata, evt.ydata), self.lasso_callback) 
       self.canvas.widgetlock(self.lasso) 

    def on_release(self, evt): 
     # Note: lasso_callback is not called on click without drag so we release 
     # the lock here to handle this case as well. 
     if evt.button == 1: 
      if self.lasso: 
       self.canvas.draw_idle() 
       self.canvas.widgetlock.release(self.lasso) 
       self.lasso = None 
     else: 
      self.show_popup_menu((evt.x, self.canvas.GetSize()[1]-evt.y), None) 

    def redraw(self): 
     self.subplot.clear() 
     self.subplot.scatter([1,2,3],[3,1,2]) 

    def toggle_lasso_tool(self, evt): 
     if evt.Checked(): 
      self.navtoolbar.mode = 'lasso' 
      #self.subplot.set_navigate_mode('lasso') 
      # Cheat: untoggle the zoom and pan tools 
      self.navtoolbar.ToggleTool(self.navtoolbar._NTB2_ZOOM, False) 
      self.navtoolbar.ToggleTool(self.navtoolbar._NTB2_PAN, False) 
     else: 
      self.navtoolbar.mode = '' 
      self.lasso = None 
      #self.subplot.set_navigate_mode('') 

    def get_toolbar(self): 
     if not self.navtoolbar: 
      self.navtoolbar = NavigationToolbar(self.canvas) 
      self.navtoolbar.DeleteToolByPos(6) 
      ID_LASSO_TOOL = wx.NewId() 
      lasso = self.navtoolbar.InsertSimpleTool(5, ID_LASSO_TOOL, 
          wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK), 
          isToggle=True) 
      self.navtoolbar.Realize() 
      self.navtoolbar.Bind(wx.EVT_TOOL, self.toggle_lasso_tool, id=ID_LASSO_TOOL) 
     return self.navtoolbar 

if __name__ == "__main__": 
    app = wx.PySimpleApp() 
    f = wx.Frame(None, size=(600,600)) 
    p = ScatterPanel(f) 
    f.SetToolBar(p.get_toolbar())    
    f.Show() 
    app.MainLoop() 

Gracias, Adam

Respuesta

2

Pues aquí está, feo pero funcionales. Dejaré que los docsurgen hablen, esto ha desperdiciado suficiente tiempo.

import wx 
from matplotlib.patches import Rectangle 
from matplotlib.widgets import Lasso 
from matplotlib.figure import Figure 
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg 
from matplotlib.backends.backend_wxagg import NavigationToolbar2WxAgg 

class MyNavToolbar(NavigationToolbar2WxAgg): 
    """wx/mpl NavToolbar hack with an additional tools user interaction. 
    This class is necessary because simply adding a new togglable tool to the 
    toolbar won't (1) radio-toggle between the new tool and the pan/zoom tools. 
    (2) disable the pan/zoom tool modes in the associated subplot(s). 
    """ 
    ID_LASSO_TOOL = wx.NewId() 
    def __init__(self, canvas): 
     super(NavigationToolbar2WxAgg, self).__init__(canvas) 

     self.pan_tool = self.FindById(self._NTB2_PAN) 
     self.zoom_tool = self.FindById(self._NTB2_ZOOM) 

     self.lasso_tool = self.InsertSimpleTool(5, self.ID_LASSO_TOOL, 
          wx.ArtProvider.GetBitmap(wx.ART_ADD_BOOKMARK), 
          isToggle=True) 
     self.Bind(wx.EVT_TOOL, self.on_toggle_lasso_tool, self.lasso_tool) 
     self.Bind(wx.EVT_TOOL, self.on_toggle_pan_zoom, self.zoom_tool) 
     self.Bind(wx.EVT_TOOL, self.on_toggle_pan_zoom, self.pan_tool) 

    def get_mode(self): 
     """Use this rather than navtoolbar.mode 
     """ 
     if self.lasso_tool.IsToggled(): 
      return 'lasso' 
     else: 
      return self.mode 

    def untoggle_mpl_tools(self): 
     """Hack city: Since I can't figure out how to change the way the 
     associated subplot(s) handles mouse events: I generate events to turn 
     off whichever tool mode is enabled (if any). 
     This function needs to be called whenever any user-defined tool 
     (eg: lasso) is clicked. 
     """ 
     if self.pan_tool.IsToggled(): 
      wx.PostEvent(
       self.GetEventHandler(), 
       wx.CommandEvent(wx.EVT_TOOL.typeId, self._NTB2_PAN) 
      ) 
      self.ToggleTool(self._NTB2_PAN, False) 
     elif self.zoom_tool.IsToggled(): 
      wx.PostEvent(
       self.GetEventHandler(), 
       wx.CommandEvent(wx.EVT_TOOL.typeId, self._NTB2_ZOOM) 
      ) 
      self.ToggleTool(self._NTB2_ZOOM, False) 

    def on_toggle_lasso_tool(self, evt): 
     """Lasso tool handler. 
     """ 
     if evt.Checked(): 
      self.untoggle_mpl_tools() 

    def on_toggle_pan_zoom(self, evt): 
     """Called when pan or zoom is toggled. 
     We need to manually untoggle user-defined tools. 
     """ 
     if evt.Checked(): 
      self.ToggleTool(self.ID_LASSO_TOOL, False) 
     # Make sure the regular pan/zoom handlers get the event 
     evt.Skip() 

class ScatterPanel(FigureCanvasWxAgg): 
    """Contains the guts for drawing scatter plots. 
    """ 
    def __init__(self, parent, **kwargs): 
     self.figure = Figure() 
     FigureCanvasWxAgg.__init__(self, parent, -1, self.figure, **kwargs) 
     self.canvas = self.figure.canvas 
     self.SetMinSize((100,100)) 
     self.figure.set_facecolor((1,1,1)) 
     self.figure.set_edgecolor((1,1,1)) 
     self.canvas.SetBackgroundColour('white') 

     self.subplot = self.figure.add_subplot(111) 
     self.navtoolbar = None 
     self.lasso = None 
     self.redraw() 

     self.canvas.mpl_connect('button_press_event', self.on_press) 
     self.canvas.mpl_connect('button_release_event', self.on_release) 

    def lasso_callback(self, verts): 
     pass 

    def on_press(self, evt): 
     """canvas mousedown handler 
     """ 
     if evt.button == 1: 
      if self.canvas.widgetlock.locked(): 
       return 
      if evt.inaxes is None: 
       return 
      if self.navtoolbar and self.navtoolbar.get_mode() == 'lasso': 
       self.lasso = Lasso(evt.inaxes, (evt.xdata, evt.ydata), self.lasso_callback) 
       self.canvas.widgetlock(self.lasso) 

    def on_release(self, evt): 
     """canvas mouseup handler 
     """ 
     # Note: lasso_callback is not called on click without drag so we release 
     # the lock here to handle this case as well. 
     if evt.button == 1: 
      if self.lasso: 
       self.canvas.draw_idle() 
       self.canvas.widgetlock.release(self.lasso) 
       self.lasso = None 
     else: 
      self.show_popup_menu((evt.x, self.canvas.GetSize()[1]-evt.y), None) 

    def redraw(self): 
     self.subplot.clear() 
     self.subplot.scatter([1,2,3],[3,1,2]) 

    def get_toolbar(self): 
     if not self.navtoolbar: 
      self.navtoolbar = MyNavToolbar(self.canvas) 
      self.navtoolbar.Realize() 
     return self.navtoolbar 

if __name__ == "__main__": 
    app = wx.PySimpleApp() 
    f = wx.Frame(None, size=(600,600)) 
    p = ScatterPanel(f) 
    f.SetToolBar(p.get_toolbar())    
    f.Show() 
    app.MainLoop() 
+1

Solo un FYI para cualquiera que lea esto. Esto solía funcionar para mí también, pero ahora con Matplotlib 1.2.0 tengo que cambiar dos de las líneas. '' self._NTB2_PAN' convierte self.wx_ids [ 'Pan'] '' y self._NTB2_ZOOM' se convierte en 'self.wx_ids [ 'zoom']' –

7

Aquí hay una versión mejorada de MyNavToolbar. Lo principal a tener en cuenta es la adición del método add_user_tool. Lo llamo desde __init__, pero es probable que desee llamarlo desde fuera de la clase MyNavToolbar. De esta forma, podría tener diferentes herramientas para tipos de gráficos.

class MyNavToolbar(NavigationToolbar2WxAgg): 
    """wx/mpl NavToolbar hack with an additional tools user interaction. 
    This class is necessary because simply adding a new togglable tool to the 
    toolbar won't (1) radio-toggle between the new tool and the pan/zoom tools. 
    (2) disable the pan/zoom tool modes in the associated subplot(s). 
    """ 
    def __init__(self, canvas): 
     super(NavigationToolbar2WxAgg, self).__init__(canvas) 
     self.pan_tool = self.FindById(self._NTB2_PAN) 
     self.zoom_tool = self.FindById(self._NTB2_ZOOM) 
     self.Bind(wx.EVT_TOOL, self.on_toggle_pan_zoom, self.zoom_tool) 
     self.Bind(wx.EVT_TOOL, self.on_toggle_pan_zoom, self.pan_tool) 

     self.user_tools = {} # user_tools['tool_mode'] : wx.ToolBarToolBase 

     self.InsertSeparator(5) 
     self.add_user_tool('lasso', 6, icons.lasso_tool.ConvertToBitmap(), True, 'Lasso') 
     self.add_user_tool('gate', 7, icons.gate_tool.ConvertToBitmap(), True, 'Gate') 

    def add_user_tool(self, mode, pos, bmp, istoggle=True, shortHelp=''): 
     """Adds a new user-defined tool to the toolbar. 
     mode -- the value that MyNavToolbar.get_mode() will return if this tool 
       is toggled on 
     pos -- the position in the toolbar to add the icon 
     bmp -- a wx.Bitmap of the icon to use in the toolbar 
     isToggle -- whether or not the new tool toggles on/off with the other 
        togglable tools 
     shortHelp -- the tooltip shown to the user for the new tool 
     """ 
     tool_id = wx.NewId() 
     self.user_tools[mode] = self.InsertSimpleTool(pos, tool_id, bmp, 
          isToggle=istoggle, shortHelpString=shortHelp) 
     self.Bind(wx.EVT_TOOL, self.on_toggle_user_tool, self.user_tools[mode]) 

    def get_mode(self): 
     """Use this rather than navtoolbar.mode 
     """ 
     for mode, tool in self.user_tools.items(): 
      if tool.IsToggled(): 
       return mode 
     return self.mode 

    def untoggle_mpl_tools(self): 
     """Hack city: Since I can't figure out how to change the way the 
     associated subplot(s) handles mouse events: I generate events to turn 
     off whichever tool mode is enabled (if any). 
     This function needs to be called whenever any user-defined tool 
     (eg: lasso) is clicked. 
     """ 
     if self.pan_tool.IsToggled(): 
      wx.PostEvent(
       self.GetEventHandler(), 
       wx.CommandEvent(wx.EVT_TOOL.typeId, self._NTB2_PAN) 
      ) 
      self.ToggleTool(self._NTB2_PAN, False) 
     elif self.zoom_tool.IsToggled(): 
      wx.PostEvent(
       self.GetEventHandler(), 
       wx.CommandEvent(wx.EVT_TOOL.typeId, self._NTB2_ZOOM) 
      ) 
      self.ToggleTool(self._NTB2_ZOOM, False) 

    def on_toggle_user_tool(self, evt): 
     """user tool click handler. 
     """ 
     if evt.Checked(): 
      self.untoggle_mpl_tools() 
      #untoggle other user tools 
      for tool in self.user_tools.values(): 
       if tool.Id != evt.Id: 
        self.ToggleTool(tool.Id, False) 

    def on_toggle_pan_zoom(self, evt): 
     """Called when pan or zoom is toggled. 
     We need to manually untoggle user-defined tools. 
     """ 
     if evt.Checked(): 
      for tool in self.user_tools.values(): 
       self.ToggleTool(tool.Id, False) 
     # Make sure the regular pan/zoom handlers get the event 
     evt.Skip() 

    def reset_history(self): 
     """More hacky junk to clear/reset the toolbar history. 
     """ 
     self._views.clear() 
     self._positions.clear() 
     self.push_current() 
+0

Wow, esto es increíblemente útil para mí, muchas gracias por responder Estoy triste porque nadie respondió ni comentó en absoluto. –

+0

Gracias Adam, tu función 'reset_history()' fue inmensamente útil. –

+0

Con Matplotlib 1.2.0 tengo que cambiar algunas cosas. 'self._NTB2_PAN' se convierte en' self.wx_ids ['Pan'] ' y ' self._NTB2_ZOOM' se convierte en 'self.wx_ids ['Zoom']' –

Cuestiones relacionadas