123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146 |
- """
- 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`
- """
- from enum import IntEnum
- import logging
- import re
- import time
- from types import SimpleNamespace
- from weakref import WeakKeyDictionary
- import numpy as np
- from matplotlib import rcParams
- from matplotlib._pylab_helpers import Gcf
- import matplotlib.cbook as cbook
- _log = logging.getLogger(__name__)
- class Cursors(IntEnum): # Must subclass int for the macOS backend.
- """Backend-independent cursor types."""
- HAND, POINTER, SELECT_REGION, MOVE, WAIT = range(5)
- cursors = Cursors # Backcompat.
- # Views positions tool
- _views_positions = 'viewpos'
- class ToolBase:
- """
- 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 : str
- Used as **Id** of the tool, has to be unique among tools of the same
- ToolManager
- """
- default_keymap = None
- """
- 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 = None
- """
- Description of the Tool
- **String**: If the Tool is included in the Toolbar this text is used
- as a Tooltip
- """
- image = None
- """
- 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
- """
- def __init__(self, toolmanager, name):
- cbook._warn_external(
- 'The new Tool classes introduced in v1.5 are experimental; their '
- 'API (including names) will likely change in future versions.')
- self._name = name
- self._toolmanager = toolmanager
- self._figure = None
- @property
- def figure(self):
- return self._figure
- @figure.setter
- def figure(self, figure):
- self.set_figure(figure)
- @property
- def canvas(self):
- if not self._figure:
- return None
- return self._figure.canvas
- @property
- def toolmanager(self):
- return self._toolmanager
- def _make_classic_style_pseudo_toolbar(self):
- """
- Return a placeholder object with a single `canvas` attribute.
- This is useful to reuse the implementations of tools already provided
- by the classic Toolbars.
- """
- return SimpleNamespace(canvas=self.canvas)
- def set_figure(self, figure):
- """
- Assign a figure to the tool
- Parameters
- ----------
- figure : `Figure`
- """
- self._figure = figure
- def trigger(self, sender, event, data=None):
- """
- 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
- @property
- def name(self):
- """Tool Id"""
- return self._name
- def destroy(self):
- """
- Destroy the tool
- This method is called when the tool is removed by
- `matplotlib.backend_managers.ToolManager.remove_tool`
- """
- pass
- class ToolToggleBase(ToolBase):
- """
- 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
- """
- radio_group = None
- """Attribute to group 'radio' like tools (mutually exclusive)
- **String** that identifies the group or **None** if not belonging to a
- group
- """
- cursor = None
- """Cursor to use when the tool is active"""
- default_toggled = False
- """Default of toggled state"""
- def __init__(self, *args, **kwargs):
- self._toggled = kwargs.pop('toggled', self.default_toggled)
- ToolBase.__init__(self, *args, **kwargs)
- def trigger(self, sender, event, data=None):
- """Calls `enable` or `disable` based on `toggled` value"""
- if self._toggled:
- self.disable(event)
- else:
- self.enable(event)
- self._toggled = not self._toggled
- def enable(self, event=None):
- """
- Enable the toggle tool
- `trigger` calls this method when `toggled` is False
- """
- pass
- def disable(self, event=None):
- """
- 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
- @property
- def toggled(self):
- """State of the toggled tool"""
- return self._toggled
- def set_figure(self, figure):
- 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
- class SetCursorBase(ToolBase):
- """
- 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
- """
- def __init__(self, *args, **kwargs):
- 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)
- def set_figure(self, figure):
- 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)
- def _tool_trigger_cbk(self, event):
- if event.tool.toggled:
- self._cursor = event.tool.cursor
- else:
- self._cursor = None
- self._set_cursor_cbk(event.canvasevent)
- def _add_tool(self, tool):
- """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)
- def _add_tool_cbk(self, event):
- """Process every newly added tool."""
- if event.tool is self:
- return
- self._add_tool(event.tool)
- def _set_cursor_cbk(self, event):
- 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
- def set_cursor(self, cursor):
- """
- Set the cursor
- This method has to be implemented per backend
- """
- raise NotImplementedError
- class ToolCursorPosition(ToolBase):
- """
- Send message with the current pointer position
- This tool runs in the background reporting the position of the cursor
- """
- def __init__(self, *args, **kwargs):
- self._idDrag = None
- ToolBase.__init__(self, *args, **kwargs)
- def set_figure(self, figure):
- 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)
- def send_message(self, event):
- """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)
- class RubberbandBase(ToolBase):
- """Draw and remove rubberband"""
- def trigger(self, sender, event, data):
- """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()
- def draw_rubberband(self, *data):
- """
- Draw rubberband
- This method must get implemented per backend
- """
- raise NotImplementedError
- def remove_rubberband(self):
- """
- Remove rubberband
- This method should get implemented per backend
- """
- pass
- class ToolQuit(ToolBase):
- """Tool to call the figure manager destroy method"""
- description = 'Quit the figure'
- default_keymap = rcParams['keymap.quit']
- def trigger(self, sender, event, data=None):
- Gcf.destroy_fig(self.figure)
- class ToolQuitAll(ToolBase):
- """Tool to call the figure manager destroy method"""
- description = 'Quit all figures'
- default_keymap = rcParams['keymap.quit_all']
- def trigger(self, sender, event, data=None):
- Gcf.destroy_all()
- class ToolEnableAllNavigation(ToolBase):
- """Tool to enable all axes for toolmanager interaction"""
- description = 'Enable all axes toolmanager'
- default_keymap = rcParams['keymap.all_axes']
- def trigger(self, sender, event, data=None):
- 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)
- class ToolEnableNavigation(ToolBase):
- """Tool to enable a specific axes for toolmanager interaction"""
- description = 'Enable one axes toolmanager'
- default_keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9)
- def trigger(self, sender, event, data=None):
- if event.inaxes is None:
- return
- n = int(event.key) - 1
- if n < len(self.figure.get_axes()):
- 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)
- class _ToolGridBase(ToolBase):
- """Common functionality between ToolGrid and ToolMinorGrid."""
- _cycle = [(False, False), (True, False), (True, True), (False, True)]
- def trigger(self, sender, event, data=None):
- 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()
- @staticmethod
- 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.gridline.get_visible() for tick in ticks):
- return True
- elif not any(tick.gridline.get_visible() for tick in ticks):
- return False
- else:
- return None
- class ToolGrid(_ToolGridBase):
- """Tool to toggle the major grids of the figure"""
- description = 'Toggle major grids'
- default_keymap = rcParams['keymap.grid']
- def _get_next_grid_states(self, ax):
- 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")
- class ToolMinorGrid(_ToolGridBase):
- """Tool to toggle the major and minor grids of the figure"""
- description = 'Toggle major and minor grids'
- default_keymap = rcParams['keymap.grid_minor']
- def _get_next_grid_states(self, ax):
- 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"
- class ToolFullScreen(ToolToggleBase):
- """Tool to toggle full screen"""
- description = 'Toggle fullscreen mode'
- default_keymap = rcParams['keymap.fullscreen']
- def enable(self, event):
- self.figure.canvas.manager.full_screen_toggle()
- def disable(self, event):
- self.figure.canvas.manager.full_screen_toggle()
- class AxisScaleBase(ToolToggleBase):
- """Base Tool to toggle between linear and logarithmic"""
- def trigger(self, sender, event, data=None):
- if event.inaxes is None:
- return
- ToolToggleBase.trigger(self, sender, event, data)
- def enable(self, event):
- self.set_scale(event.inaxes, 'log')
- self.figure.canvas.draw_idle()
- def disable(self, event):
- self.set_scale(event.inaxes, 'linear')
- self.figure.canvas.draw_idle()
- class ToolYScale(AxisScaleBase):
- """Tool to toggle between linear and logarithmic scales on the Y axis"""
- description = 'Toggle scale Y axis'
- default_keymap = rcParams['keymap.yscale']
- def set_scale(self, ax, scale):
- ax.set_yscale(scale)
- class ToolXScale(AxisScaleBase):
- """Tool to toggle between linear and logarithmic scales on the X axis"""
- description = 'Toggle scale X axis'
- default_keymap = rcParams['keymap.xscale']
- def set_scale(self, ax, scale):
- ax.set_xscale(scale)
- class ToolViewsPositions(ToolBase):
- """
- 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`
- """
- def __init__(self, *args, **kwargs):
- self.views = WeakKeyDictionary()
- self.positions = WeakKeyDictionary()
- self.home_views = WeakKeyDictionary()
- ToolBase.__init__(self, *args, **kwargs)
- def add_figure(self, figure):
- """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))
- def clear(self, figure):
- """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()
- def update_view(self):
- """
- 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()
- def push_current(self, figure=None):
- """
- 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)
- def _axes_pos(self, ax):
- """
- 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())
- def update_home_views(self, figure=None):
- """
- 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()
- def refresh_locators(self):
- """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()
- def home(self):
- """Recall the first view and position from the stack"""
- self.views[self.figure].home()
- self.positions[self.figure].home()
- def back(self):
- """Back one step in the stack of views and positions"""
- self.views[self.figure].back()
- self.positions[self.figure].back()
- def forward(self):
- """Forward one step in the stack of views and positions"""
- self.views[self.figure].forward()
- self.positions[self.figure].forward()
- class ViewsPositionsBase(ToolBase):
- """Base class for `ToolHome`, `ToolBack` and `ToolForward`"""
- _on_trigger = None
- def trigger(self, sender, event, data=None):
- 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()
- class ToolHome(ViewsPositionsBase):
- """Restore the original view lim"""
- description = 'Reset original view'
- image = 'home'
- default_keymap = rcParams['keymap.home']
- _on_trigger = 'home'
- class ToolBack(ViewsPositionsBase):
- """Move back up the view lim stack"""
- description = 'Back to previous view'
- image = 'back'
- default_keymap = rcParams['keymap.back']
- _on_trigger = 'back'
- class ToolForward(ViewsPositionsBase):
- """Move forward in the view lim stack"""
- description = 'Forward to next view'
- image = 'forward'
- default_keymap = rcParams['keymap.forward']
- _on_trigger = 'forward'
- class ConfigureSubplotsBase(ToolBase):
- """Base tool for the configuration of subplots"""
- description = 'Configure subplots'
- image = 'subplots'
- class SaveFigureBase(ToolBase):
- """Base tool for figure saving"""
- description = 'Save the figure'
- image = 'filesave'
- default_keymap = rcParams['keymap.save']
- class ZoomPanBase(ToolToggleBase):
- """Base class for `ToolZoom` and `ToolPan`"""
- def __init__(self, *args):
- 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
- def enable(self, event):
- """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)
- def disable(self, event):
- """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)
- def trigger(self, sender, event, data=None):
- self.toolmanager.get_tool(_views_positions).add_figure(self.figure)
- ToolToggleBase.trigger(self, sender, event, data)
- def scroll_zoom(self, event):
- # 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()
- class ToolZoom(ZoomPanBase):
- """Zoom to rectangle"""
- description = 'Zoom to rectangle'
- image = 'zoom_to_rect'
- default_keymap = rcParams['keymap.zoom']
- cursor = cursors.SELECT_REGION
- radio_group = 'default'
- def __init__(self, *args):
- ZoomPanBase.__init__(self, *args)
- self._ids_zoom = []
- def _cancel_action(self):
- 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
- def _press(self, event):
- """Callback for mouse button presses in zoom-to-rectangle mode."""
- # 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
- def _switch_on_zoom_mode(self, event):
- self._zoom_mode = event.key
- self._mouse_move(event)
- def _switch_off_zoom_mode(self, event):
- self._zoom_mode = None
- self._mouse_move(event)
- def _mouse_move(self, event):
- """Callback for mouse moves in zoom-to-rectangle 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))
- def _release(self, event):
- """Callback for mouse button releases in zoom-to-rectangle 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, twiny 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()
- class ToolPan(ZoomPanBase):
- """Pan axes with left mouse, zoom with right"""
- default_keymap = rcParams['keymap.pan']
- description = 'Pan axes with left mouse, zoom with right'
- image = 'move'
- cursor = cursors.MOVE
- radio_group = 'default'
- def __init__(self, *args):
- ZoomPanBase.__init__(self, *args)
- self._idDrag = None
- def _cancel_action(self):
- 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()
- def _press(self, event):
- 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)
- def _release(self, event):
- 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()
- def _mouse_move(self, event):
- 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()
- class ToolHelpBase(ToolBase):
- description = 'Print tool list, shortcuts and description'
- default_keymap = rcParams['keymap.help']
- image = 'help.png'
- @staticmethod
- 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())
- def _format_tool_keymap(self, name):
- keymaps = self.toolmanager.get_tool_keymap(name)
- return ", ".join(self.format_shortcut(keymap) for keymap in keymaps)
- def _get_help_entries(self):
- return [(name, self._format_tool_keymap(name), tool.description)
- for name, tool in sorted(self.toolmanager.tools.items())
- if tool.description]
- def _get_help_text(self):
- entries = self._get_help_entries()
- entries = ["{}: {}\n\t{}".format(*entry) for entry in entries]
- return "\n".join(entries)
- def _get_help_html(self):
- 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>")
- class ToolCopyToClipboardBase(ToolBase):
- """Tool to copy the figure to the clipboard"""
- description = 'Copy the canvas figure to clipboard'
- default_keymap = rcParams['keymap.copy']
- def trigger(self, *args, **kwargs):
- message = "Copy tool is not available"
- self.toolmanager.message_event(message, self)
- default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward,
- '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"""
- default_toolbar_tools = [['navigation', ['home', 'back', 'forward']],
- ['zoompan', ['pan', 'zoom', 'subplots']],
- ['io', ['save', 'help']]]
- """Default tools in the toolbar"""
- def add_tools_to_manager(toolmanager, tools=default_tools):
- """
- 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)
- def add_tools_to_container(container, tools=default_toolbar_tools):
- """
- 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)
|