在matplotlib中添加新的导航模式
我正在写一个wx/matplotlib的应用程序,但在给matplotlib的导航工具栏添加新工具时遇到了很大的困难。
简单来说,我想添加一些选择工具(比如框选、套索等),这些工具可以切换控制子图的鼠标模式。不过到目前为止,我还没有找到什么简单的方法来实现这个功能。
不过,我刚发现了一个看起来可能有用的函数:http://matplotlib.sourceforge.net/api/axes_api.html?highlight=set_navigate_mode#matplotlib.axes.Axes.set_navigate_mode
可惜的是,正如警告所说的,这个函数并没有真正帮到我。
有没有人知道该怎么做?下面是一个简化的例子,展示了我目前的进展。书签图标用来代替我的套索图标,并且为了简洁起见,我去掉了套索的功能。
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()
谢谢,
亚当
2 个回答
7
这是一个改进版的 MyNavToolbar
。主要需要注意的是新增了一个 add_user_tool
方法。我在 __init__
里面调用了它,但你可能更想在 MyNavToolbar
类外部调用它。这样一来,你就可以为不同的图表类型添加不同的工具。
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()
2
好了,这就是了,虽然看起来不太好,但能用。我就让文档字符串来解释吧,这个东西已经浪费了我太多时间。
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()