""" Abstract base classes define the primitives for Tools. These tools are used by `matplotlib.backend_managers.ToolManager`
:class:`ToolBase` Simple stateless tool
:class:`ToolToggleBase` Tool that has two states, only one Toggle tool can be active at any given time for the same `matplotlib.backend_managers.ToolManager` """
"""Simple namespace for cursor reference"""
# Views positions tool
""" Base tool class
A base tool, only implements `trigger` method or not method at all. The tool is instantiated by `matplotlib.backend_managers.ToolManager`
Attributes ---------- toolmanager: `matplotlib.backend_managers.ToolManager` ToolManager that controls this Tool figure: `FigureCanvas` Figure instance that is affected by this Tool name: String Used as **Id** of the tool, has to be unique among tools of the same ToolManager """
""" Keymap to associate with this tool
**String**: List of comma separated keys that will be used to call this tool when the keypress event of *self.figure.canvas* is emitted """
""" Description of the Tool
**String**: If the Tool is included in the Toolbar this text is used as a Tooltip """
""" Filename of the image
**String**: Filename of the image to use in the toolbar. If None, the `name` is used as a label in the toolbar button """
warnings.warn('Treat the new Tool classes introduced in v1.5 as ' + 'experimental for now, the API will likely change in ' + 'version 2.1, and some tools might change name') self._name = name self._toolmanager = toolmanager self._figure = None
def figure(self): return self._figure
def figure(self, figure): self.set_figure(figure)
def canvas(self): if not self._figure: return None return self._figure.canvas
def toolmanager(self): return self._toolmanager
""" Assign a figure to the tool
Parameters ---------- figure: `Figure` """ self._figure = figure
""" Called when this tool gets used
This method is called by `matplotlib.backend_managers.ToolManager.trigger_tool`
Parameters ---------- event: `Event` The Canvas event that caused this tool to be called sender: object Object that requested the tool to be triggered data: object Extra data """
pass
def name(self): """Tool Id""" return self._name
""" Destroy the tool
This method is called when the tool is removed by `matplotlib.backend_managers.ToolManager.remove_tool` """ pass
""" Toggleable tool
Every time it is triggered, it switches between enable and disable
Parameters ---------- ``*args`` Variable length argument to be used by the Tool ``**kwargs`` `toggled` if present and True, sets the initial state of the Tool Arbitrary keyword arguments to be consumed by the Tool """
"""Attribute to group 'radio' like tools (mutually exclusive)
**String** that identifies the group or **None** if not belonging to a group """
"""Cursor to use when the tool is active"""
"""Default of toggled state"""
self._toggled = kwargs.pop('toggled', self.default_toggled) ToolBase.__init__(self, *args, **kwargs)
"""Calls `enable` or `disable` based on `toggled` value""" if self._toggled: self.disable(event) else: self.enable(event) self._toggled = not self._toggled
""" Enable the toggle tool
`trigger` calls this method when `toggled` is False """
pass
""" Disable the toggle tool
`trigger` call this method when `toggled` is True.
This can happen in different circumstances
* Click on the toolbar tool button * Call to `matplotlib.backend_managers.ToolManager.trigger_tool` * Another `ToolToggleBase` derived tool is triggered (from the same `ToolManager`) """
pass
def toggled(self): """State of the toggled tool"""
return self._toggled
toggled = self.toggled if toggled: if self.figure: self.trigger(self, None) else: # if no figure the internal state is not changed # we change it here so next call to trigger will change it back self._toggled = False ToolBase.set_figure(self, figure) if toggled: if figure: self.trigger(self, None) else: # if there is no figure, trigger won't change the internal # state we change it back self._toggled = True
""" Change to the current cursor while inaxes
This tool, keeps track of all `ToolToggleBase` derived tools, and calls set_cursor when a tool gets triggered """ ToolBase.__init__(self, *args, **kwargs) self._idDrag = None self._cursor = None self._default_cursor = cursors.POINTER self._last_cursor = self._default_cursor self.toolmanager.toolmanager_connect('tool_added_event', self._add_tool_cbk)
# process current tools for tool in self.toolmanager.tools.values(): self._add_tool(tool)
if self._idDrag: self.canvas.mpl_disconnect(self._idDrag) ToolBase.set_figure(self, figure) if figure: self._idDrag = self.canvas.mpl_connect( 'motion_notify_event', self._set_cursor_cbk)
if event.tool.toggled: self._cursor = event.tool.cursor else: self._cursor = None
self._set_cursor_cbk(event.canvasevent)
"""set the cursor when the tool is triggered""" if getattr(tool, 'cursor', None) is not None: self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name, self._tool_trigger_cbk)
"""Process every newly added tool""" if event.tool is self: return
self._add_tool(event.tool)
if not event: return
if not getattr(event, 'inaxes', False) or not self._cursor: if self._last_cursor != self._default_cursor: self.set_cursor(self._default_cursor) self._last_cursor = self._default_cursor elif self._cursor: cursor = self._cursor if cursor and self._last_cursor != cursor: self.set_cursor(cursor) self._last_cursor = cursor
""" Set the cursor
This method has to be implemented per backend """ raise NotImplementedError
""" Send message with the current pointer position
This tool runs in the background reporting the position of the cursor """ self._idDrag = None ToolBase.__init__(self, *args, **kwargs)
if self._idDrag: self.canvas.mpl_disconnect(self._idDrag) ToolBase.set_figure(self, figure) if figure: self._idDrag = self.canvas.mpl_connect( 'motion_notify_event', self.send_message)
"""Call `matplotlib.backend_managers.ToolManager.message_event`""" if self.toolmanager.messagelock.locked(): return
message = ' '
if event.inaxes and event.inaxes.get_navigate(): try: s = event.inaxes.format_coord(event.xdata, event.ydata) except (ValueError, OverflowError): pass else: artists = [a for a in event.inaxes._mouseover_set if a.contains(event) and a.get_visible()]
if artists: a = cbook._topmost_artist(artists) if a is not event.inaxes.patch: data = a.get_cursor_data(event) if data is not None: data_str = a.format_cursor_data(data) if data_str is not None: s = s + ' ' + data_str
message = s self.toolmanager.message_event(message, self)
"""Draw and remove rubberband""" """Call `draw_rubberband` or `remove_rubberband` based on data""" if not self.figure.canvas.widgetlock.available(sender): return if data is not None: self.draw_rubberband(*data) else: self.remove_rubberband()
""" Draw rubberband
This method must get implemented per backend """ raise NotImplementedError
""" Remove rubberband
This method should get implemented per backend """ pass
"""Tool to call the figure manager destroy method"""
Gcf.destroy_fig(self.figure)
"""Tool to call the figure manager destroy method"""
Gcf.destroy_all()
"""Tool to enable all axes for toolmanager interaction"""
if event.inaxes is None: return
for a in self.figure.get_axes(): if (event.x is not None and event.y is not None and a.in_axes(event)): a.set_navigate(True)
"""Tool to enable a specific axes for toolmanager interaction"""
if event.inaxes is None: return
n = int(event.key) - 1 for i, a in enumerate(self.figure.get_axes()): if (event.x is not None and event.y is not None and a.in_axes(event)): a.set_navigate(i == n)
"""Common functionality between ToolGrid and ToolMinorGrid."""
ax = event.inaxes if ax is None: return try: x_state, x_which, y_state, y_which = self._get_next_grid_states(ax) except ValueError: pass else: ax.grid(x_state, which=x_which, axis="x") ax.grid(y_state, which=y_which, axis="y") ax.figure.canvas.draw_idle()
def _get_uniform_grid_state(ticks): """ Check whether all grid lines are in the same visibility state.
Returns True/False if all grid lines are on or off, None if they are not all in the same state. """ if all(tick.gridOn for tick in ticks): return True elif not any(tick.gridOn for tick in ticks): return False else: return None
"""Tool to toggle the major grids of the figure"""
if None in map(self._get_uniform_grid_state, [ax.xaxis.minorTicks, ax.yaxis.minorTicks]): # Bail out if minor grids are not in a uniform state. raise ValueError x_state, y_state = map(self._get_uniform_grid_state, [ax.xaxis.majorTicks, ax.yaxis.majorTicks]) cycle = self._cycle # Bail out (via ValueError) if major grids are not in a uniform state. x_state, y_state = ( cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)]) return (x_state, "major" if x_state else "both", y_state, "major" if y_state else "both")
"""Tool to toggle the major and minor grids of the figure"""
if None in map(self._get_uniform_grid_state, [ax.xaxis.majorTicks, ax.yaxis.majorTicks]): # Bail out if major grids are not in a uniform state. raise ValueError x_state, y_state = map(self._get_uniform_grid_state, [ax.xaxis.minorTicks, ax.yaxis.minorTicks]) cycle = self._cycle # Bail out (via ValueError) if minor grids are not in a uniform state. x_state, y_state = ( cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)]) return x_state, "both", y_state, "both"
"""Tool to toggle full screen"""
self.figure.canvas.manager.full_screen_toggle()
self.figure.canvas.manager.full_screen_toggle()
"""Base Tool to toggle between linear and logarithmic"""
if event.inaxes is None: return ToolToggleBase.trigger(self, sender, event, data)
self.set_scale(event.inaxes, 'log') self.figure.canvas.draw_idle()
self.set_scale(event.inaxes, 'linear') self.figure.canvas.draw_idle()
"""Tool to toggle between linear and logarithmic scales on the Y axis"""
ax.set_yscale(scale)
"""Tool to toggle between linear and logarithmic scales on the X axis"""
ax.set_xscale(scale)
""" Auxiliary Tool to handle changes in views and positions
Runs in the background and should get used by all the tools that need to access the figure's history of views and positions, e.g.
* `ToolZoom` * `ToolPan` * `ToolHome` * `ToolBack` * `ToolForward` """
self.views = WeakKeyDictionary() self.positions = WeakKeyDictionary() self.home_views = WeakKeyDictionary() ToolBase.__init__(self, *args, **kwargs)
"""Add the current figure to the stack of views and positions"""
if figure not in self.views: self.views[figure] = cbook.Stack() self.positions[figure] = cbook.Stack() self.home_views[figure] = WeakKeyDictionary() # Define Home self.push_current(figure) # Make sure we add a home view for new axes as they're added figure.add_axobserver(lambda fig: self.update_home_views(fig))
"""Reset the axes stack""" if figure in self.views: self.views[figure].clear() self.positions[figure].clear() self.home_views[figure].clear() self.update_home_views()
""" Update the view limits and position for each axes from the current stack position. If any axes are present in the figure that aren't in the current stack position, use the home view limits for those axes and don't update *any* positions. """
views = self.views[self.figure]() if views is None: return pos = self.positions[self.figure]() if pos is None: return home_views = self.home_views[self.figure] all_axes = self.figure.get_axes() for a in all_axes: if a in views: cur_view = views[a] else: cur_view = home_views[a] a._set_view(cur_view)
if set(all_axes).issubset(pos): for a in all_axes: # Restore both the original and modified positions a._set_position(pos[a][0], 'original') a._set_position(pos[a][1], 'active')
self.figure.canvas.draw_idle()
""" Push the current view limits and position onto their respective stacks """ if not figure: figure = self.figure views = WeakKeyDictionary() pos = WeakKeyDictionary() for a in figure.get_axes(): views[a] = a._get_view() pos[a] = self._axes_pos(a) self.views[figure].push(views) self.positions[figure].push(pos)
""" Return the original and modified positions for the specified axes
Parameters ---------- ax : (matplotlib.axes.AxesSubplot) The axes to get the positions for
Returns ------- limits : (tuple) A tuple of the original and modified positions """
return (ax.get_position(True).frozen(), ax.get_position().frozen())
""" Make sure that self.home_views has an entry for all axes present in the figure """
if not figure: figure = self.figure for a in figure.get_axes(): if a not in self.home_views[figure]: self.home_views[figure][a] = a._get_view()
"""Redraw the canvases, update the locators""" for a in self.figure.get_axes(): xaxis = getattr(a, 'xaxis', None) yaxis = getattr(a, 'yaxis', None) zaxis = getattr(a, 'zaxis', None) locators = [] if xaxis is not None: locators.append(xaxis.get_major_locator()) locators.append(xaxis.get_minor_locator()) if yaxis is not None: locators.append(yaxis.get_major_locator()) locators.append(yaxis.get_minor_locator()) if zaxis is not None: locators.append(zaxis.get_major_locator()) locators.append(zaxis.get_minor_locator())
for loc in locators: loc.refresh() self.figure.canvas.draw_idle()
"""Recall the first view and position from the stack""" self.views[self.figure].home() self.positions[self.figure].home()
"""Back one step in the stack of views and positions""" self.views[self.figure].back() self.positions[self.figure].back()
"""Forward one step in the stack of views and positions""" self.views[self.figure].forward() self.positions[self.figure].forward()
"""Base class for `ToolHome`, `ToolBack` and `ToolForward`"""
self.toolmanager.get_tool(_views_positions).add_figure(self.figure) getattr(self.toolmanager.get_tool(_views_positions), self._on_trigger)() self.toolmanager.get_tool(_views_positions).update_view()
"""Restore the original view lim"""
"""Move back up the view lim stack"""
"""Move forward in the view lim stack"""
"""Base tool for the configuration of subplots"""
"""Base tool for figure saving"""
"""Base class for `ToolZoom` and `ToolPan`""" ToolToggleBase.__init__(self, *args) self._button_pressed = None self._xypress = None self._idPress = None self._idRelease = None self._idScroll = None self.base_scale = 2. self.scrollthresh = .5 # .5 second scroll threshold self.lastscroll = time.time()-self.scrollthresh
"""Connect press/release events and lock the canvas""" self.figure.canvas.widgetlock(self) self._idPress = self.figure.canvas.mpl_connect( 'button_press_event', self._press) self._idRelease = self.figure.canvas.mpl_connect( 'button_release_event', self._release) self._idScroll = self.figure.canvas.mpl_connect( 'scroll_event', self.scroll_zoom)
"""Release the canvas and disconnect press/release events""" self._cancel_action() self.figure.canvas.widgetlock.release(self) self.figure.canvas.mpl_disconnect(self._idPress) self.figure.canvas.mpl_disconnect(self._idRelease) self.figure.canvas.mpl_disconnect(self._idScroll)
self.toolmanager.get_tool(_views_positions).add_figure(self.figure) ToolToggleBase.trigger(self, sender, event, data)
# https://gist.github.com/tacaswell/3144287 if event.inaxes is None: return
if event.button == 'up': # deal with zoom in scl = self.base_scale elif event.button == 'down': # deal with zoom out scl = 1/self.base_scale else: # deal with something that should never happen scl = 1
ax = event.inaxes ax._set_view_from_bbox([event.x, event.y, scl])
# If last scroll was done within the timing threshold, delete the # previous view if (time.time()-self.lastscroll) < self.scrollthresh: self.toolmanager.get_tool(_views_positions).back()
self.figure.canvas.draw_idle() # force re-draw
self.lastscroll = time.time() self.toolmanager.get_tool(_views_positions).push_current()
"""Zoom to rectangle"""
ZoomPanBase.__init__(self, *args) self._ids_zoom = []
for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) self.toolmanager.trigger_tool('rubberband', self) self.toolmanager.get_tool(_views_positions).refresh_locators() self._xypress = None self._button_pressed = None self._ids_zoom = [] return
"""the _press mouse button in zoom to rect mode callback"""
# If we're already in the middle of a zoom, pressing another # button works to "cancel" if self._ids_zoom != []: self._cancel_action()
if event.button == 1: self._button_pressed = 1 elif event.button == 3: self._button_pressed = 3 else: self._cancel_action() return
x, y = event.x, event.y
self._xypress = [] for i, a in enumerate(self.figure.get_axes()): if (x is not None and y is not None and a.in_axes(event) and a.get_navigate() and a.can_zoom()): self._xypress.append((x, y, a, i, a._get_view()))
id1 = self.figure.canvas.mpl_connect( 'motion_notify_event', self._mouse_move) id2 = self.figure.canvas.mpl_connect( 'key_press_event', self._switch_on_zoom_mode) id3 = self.figure.canvas.mpl_connect( 'key_release_event', self._switch_off_zoom_mode)
self._ids_zoom = id1, id2, id3 self._zoom_mode = event.key
self._zoom_mode = event.key self._mouse_move(event)
self._zoom_mode = None self._mouse_move(event)
"""the drag callback in zoom mode"""
if self._xypress: x, y = event.x, event.y lastx, lasty, a, ind, view = self._xypress[0] (x1, y1), (x2, y2) = np.clip( [[lastx, lasty], [x, y]], a.bbox.min, a.bbox.max) if self._zoom_mode == "x": y1, y2 = a.bbox.intervaly elif self._zoom_mode == "y": x1, x2 = a.bbox.intervalx self.toolmanager.trigger_tool( 'rubberband', self, data=(x1, y1, x2, y2))
"""the release mouse button callback in zoom to rect mode"""
for zoom_id in self._ids_zoom: self.figure.canvas.mpl_disconnect(zoom_id) self._ids_zoom = []
if not self._xypress: self._cancel_action() return
last_a = []
for cur_xypress in self._xypress: x, y = event.x, event.y lastx, lasty, a, _ind, view = cur_xypress # ignore singular clicks - 5 pixels is a threshold if abs(x - lastx) < 5 or abs(y - lasty) < 5: self._cancel_action() return
# detect twinx,y axes and avoid double zooming twinx, twiny = False, False if last_a: for la in last_a: if a.get_shared_x_axes().joined(a, la): twinx = True if a.get_shared_y_axes().joined(a, la): twiny = True last_a.append(a)
if self._button_pressed == 1: direction = 'in' elif self._button_pressed == 3: direction = 'out' else: continue
a._set_view_from_bbox((lastx, lasty, x, y), direction, self._zoom_mode, twinx, twiny)
self._zoom_mode = None self.toolmanager.get_tool(_views_positions).push_current() self._cancel_action()
"""Pan axes with left mouse, zoom with right"""
ZoomPanBase.__init__(self, *args) self._idDrag = None
self._button_pressed = None self._xypress = [] self.figure.canvas.mpl_disconnect(self._idDrag) self.toolmanager.messagelock.release(self) self.toolmanager.get_tool(_views_positions).refresh_locators()
if event.button == 1: self._button_pressed = 1 elif event.button == 3: self._button_pressed = 3 else: self._cancel_action() return
x, y = event.x, event.y
self._xypress = [] for i, a in enumerate(self.figure.get_axes()): if (x is not None and y is not None and a.in_axes(event) and a.get_navigate() and a.can_pan()): a.start_pan(x, y, event.button) self._xypress.append((a, i)) self.toolmanager.messagelock(self) self._idDrag = self.figure.canvas.mpl_connect( 'motion_notify_event', self._mouse_move)
if self._button_pressed is None: self._cancel_action() return
self.figure.canvas.mpl_disconnect(self._idDrag) self.toolmanager.messagelock.release(self)
for a, _ind in self._xypress: a.end_pan() if not self._xypress: self._cancel_action() return
self.toolmanager.get_tool(_views_positions).push_current() self._cancel_action()
for a, _ind in self._xypress: # safer to use the recorded button at the _press than current # button: # multiple button can get pressed during motion... a.drag_pan(self._button_pressed, event.key, event.x, event.y) self.toolmanager.canvas.draw_idle()
def format_shortcut(key_sequence): """ Converts a shortcut string from the notation used in rc config to the standard notation for displaying shortcuts, e.g. 'ctrl+a' -> 'Ctrl+A'. """ return (key_sequence if len(key_sequence) == 1 else re.sub(r"\+[A-Z]", r"+Shift\g<0>", key_sequence).title())
keymaps = self.toolmanager.get_tool_keymap(name) return ", ".join(self.format_shortcut(keymap) for keymap in keymaps)
entries = [] for name, tool in sorted(self.toolmanager.tools.items()): if not tool.description: continue entries.append((name, self._format_tool_keymap(name), tool.description)) return entries
entries = self._get_help_entries() entries = ["{}: {}\n\t{}".format(*entry) for entry in entries] return "\n".join(entries)
fmt = "<tr><td>{}</td><td>{}</td><td>{}</td></tr>" rows = [fmt.format( "<b>Action</b>", "<b>Shortcuts</b>", "<b>Description</b>")] rows += [fmt.format(*row) for row in self._get_help_entries()] return ("<style>td {padding: 0px 4px}</style>" "<table><thead>" + rows[0] + "</thead>" "<tbody>".join(rows[1:]) + "</tbody></table>")
"""Tool to copy the figure to the clipboard"""
message = "Copy tool is not available" self.toolmanager.message_event(message, self)
'zoom': ToolZoom, 'pan': ToolPan, 'subplots': 'ToolConfigureSubplots', 'save': 'ToolSaveFigure', 'grid': ToolGrid, 'grid_minor': ToolMinorGrid, 'fullscreen': ToolFullScreen, 'quit': ToolQuit, 'quit_all': ToolQuitAll, 'allnav': ToolEnableAllNavigation, 'nav': ToolEnableNavigation, 'xscale': ToolXScale, 'yscale': ToolYScale, 'position': ToolCursorPosition, _views_positions: ToolViewsPositions, 'cursor': 'ToolSetCursor', 'rubberband': 'ToolRubberband', 'help': 'ToolHelp', 'copy': 'ToolCopyToClipboard', } """Default tools"""
['zoompan', ['pan', 'zoom', 'subplots']], ['io', ['save', 'help']]] """Default tools in the toolbar"""
""" Add multiple tools to `ToolManager`
Parameters ---------- toolmanager: ToolManager `backend_managers.ToolManager` object that will get the tools added tools : {str: class_like}, optional The tools to add in a {name: tool} dict, see `add_tool` for more info. """
for name, tool in tools.items(): toolmanager.add_tool(name, tool)
""" Add multiple tools to the container.
Parameters ---------- container: Container `backend_bases.ToolContainerBase` object that will get the tools added tools : list, optional List in the form [[group1, [tool1, tool2 ...]], [group2, [...]]] Where the tools given by tool1, and tool2 will display in group1. See `add_tool` for details. """
for group, grouptools in tools: for position, tool in enumerate(grouptools): container.add_tool(tool, group, position) |