Source code for straditize.cross_mark

# -*- coding: utf-8 -*-
"""Module for a cross mark to select one point in a matplotlib axes

**Disclaimer**

Copyright (C) 2018-2019  Philipp S. Sommer

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU General Public License for more details.

You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>."""
import numpy as np
import six
from psyplot.data import Signal
from psyplot.utils import _temp_bool_prop
from itertools import chain, repeat, product
from straditize.common import docstrings

if six.PY2:
    from itertools import izip_longest as zip_longest
else:
    from itertools import zip_longest


[docs]class CrossMarks(object): """ A set of draggable marks in a matplotlib axes """ @property def fig(self): """The :class:`matplotlib.figure.Figure` that this mark plots on""" return self.ax.figure @property def y(self): """The y-position of the mark""" return self.ya[self._i_hline] @y.setter def y(self, value): """The y-position of the mark""" self.ya[self._i_hline] = value @property def x(self): """The x-position of the mark""" return self.xa[self._i_vline] @x.setter def x(self, value): """The x-position of the mark""" self.xa[self._i_vline] = value @property def hline(self): """The current horizontal line""" return self.hlines[self._i_hline] @property def vline(self): """The current vertical line""" return self.vlines[self._i_vline] @property def pos(self): """The position of the current line""" return np.array( [self.xa[self._i_vline], self.ya[self._i_hline]]) @pos.setter def pos(self, value): """The position of the current line""" self.xa[self._i_vline] = value[0] if np.ndim(value) else value self.ya[self._i_hline] = value[1] if np.ndim(value) else value @property def points(self): """The x-y-coordinates of the points as a (N, 2)-shaped array""" return np.array(list(product(self.xa, self.ya))) @property def line_connections(self): """The line connections to the current position""" return self._all_line_connections[self._i_hline][self._i_vline] @line_connections.setter def line_connections(self, value): """The line connections to the current position""" self._all_line_connections[self._i_hline][self._i_vline] = value @property def other_connections(self): """All other connections to the current position""" return self._all_other_connections[self._i_hline][self._i_vline] @other_connections.setter def other_connections(self, value): """All other connections to the current position""" self._all_other_connections[self._i_hline][self._i_vline] = value @property def idx_h(self): """The index for vertical lines""" return None if not self._idx_h else self._idx_h[self._i_vline] @idx_h.setter def idx_h(self, value): """The index for vertical lines""" if self._idx_h is None: self._idx_h = [None] * len(self.xa) self._idx_h[self._i_vline] = value @property def idx_v(self): """The index for horizontal lines""" return None if not self._idx_v else self._idx_v[self._i_hline] @idx_v.setter def idx_v(self, value): """The index for horizontal lines""" if self._idx_v is None: self._idx_v = [None] * len(self.ya) self._idx_v[self._i_hline] = value #: Boolean to control whether the vertical lines should be hidden hide_vertical = False #: Boolean to control whether the horizontal lines should be hidden hide_horizontal = False #: A signal that is emitted when the mark is moved. Connected function are #: expected to accept two arguments. One tuple with the old position and #: the CrossMarks instance itself moved = Signal('_moved') block_signals = _temp_bool_prop( 'block_signals', "Block the emitting of signals of this instance") #: The index of the selected hline _i_hline = 0 #: The index of the selected vline _i_vline = 0 #: Boolean that is True, if the animated property of the lines should be #: used _animated = True #: The matplotlib axes to plot on ax = None #: The x-limits of the :attr:`hlines` xlim = None #: The x-limits of the :attr:`vlines` ylim = None #: Class attribute that is set to a :class:`CrossMark` instance to lock the #: selection of marks lock = None #: A boolean to control whether the connected artists should be shown #: at all show_connected_artists = True #: a list of :class:`matplotlib.artist.Artist` whose colors are changed #: when this mark is selected connected_artists = [] #: The default properties of the unselected mark, complementing the #: :attr:`_select_props` _unselect_props = {} #: the list of horizontal lines hlines = [] #: the list of vertical lines vlines = [] @docstrings.get_sectionsf('CrossMarks') @docstrings.dedent def __init__(self, pos=(0, 0), ax=None, selectable=['h', 'v'], draggable=['h', 'v'], idx_h=None, idx_v=None, xlim=None, ylim=None, select_props={'c': 'r'}, auto_hide=False, connected_artists=[], lock=True, draw_lines=True, hide_vertical=None, hide_horizontal=None, **kwargs): """ Parameters ---------- pos: tuple of 2 arrays The initial positions of the crosses. The first item marks the x-coordinates of the points, the second the y-coordinates ax: matplotlib.axes.Axes The axes object to draw to. If not specified and draw_lines is True, the current axes object is used selectable: list of {'x', 'y'} Determine whether only the x-, y-, or both lines should be selectable draggable: list of {'x', 'y'} Determine whether only the x-, y-, or both lines should be draggable idx_h: pandas.Index The index for the horizontal coordinates. If not provided, we use a continuous movement along x. idx_v: pandas.Index The index for the vertical coordinates. If not provided, we use a continuous movement along y. xlim: tuple of floats (xmin, xmax) The minimum and maximum x value for the lines ylim: tuple for floats (ymin, ymax) The minimum and maximum y value for the lines select_props: color The line properties for selected marks auto_hide: bool If True, the lines are hidden if they are not selected. connected_artists: list of artists List of artists whose properties should be changed to `select_props` when this marks is selected lock: bool If True, at most one mark can be selected at a time draw_lines: bool If True, the cross mark lines are drawn. Otherwise, you must call the `draw_lines` method explicitly hide_vertical: bool Boolean to control whether the vertical lines should be hidden. If None, the default class attribute is used hide_horizontal: bool Boolean to control whether the horizontal lines should be hidden. If None, the default class attribute is used ``**kwargs`` Any other keyword argument that is passed to the :func:`matplotlib.pyplot.plot` function""" self.xa = np.asarray([pos[0]] if not np.ndim(pos[0]) else pos[0], dtype=float) self.ya = np.asarray([pos[1]] if not np.ndim(pos[1]) else pos[1], dtype=float) self._xa0 = self.xa.copy() self._ya0 = self.ya.copy() self._constant_dist_x = [] self._constant_dist_x_marks = [] self._constant_dist_y = [] self._constant_dist_y_marks = [] self.selectable = list(selectable) self.draggable = list(draggable) if hide_horizontal is not None: self.hide_horizontal = hide_horizontal if hide_vertical is not None: self.hide_vertical = hide_vertical self._select_props = select_props.copy() self.press = None if idx_h is not None: try: idx_h[0][0] except IndexError: idx_h = [idx_h] * len(self.xa) if idx_v is not None and np.ndim(idx_v) != 2: try: idx_v[0][0] except IndexError: idx_v = [idx_v] * len(self.ya) self._idx_h = idx_h self._idx_v = idx_v self.xlim = xlim self.ylim = ylim self.other_marks = [] self._connection_visible = [] self._all_line_connections = [[[] for _ in range(len(self.xa))] for _ in range(len(self.ya))] self._all_other_connections = [[[] for _ in range(len(self.xa))] for _ in range(len(self.ya))] self._lock_mark = lock kwargs.setdefault('marker', '+') self.auto_hide = auto_hide self._line_kwargs = kwargs self.set_connected_artists(list(connected_artists)) if draw_lines: self.ax = ax self.draw_lines() self.connect() elif ax is not None: self.ax = ax
[docs] def set_connected_artists(self, artists): """Set the connected artists Parameters ---------- artists: matplotlib.artist.Artist The artists (e.g. other lines) that should be connected and highlighted if this mark is selected""" self.connected_artists = artists self._connected_artists_props = [ {key: getattr(a, 'get_' + key)() for key in self._select_props} for a in artists]
[docs] def draw_lines(self, **kwargs): """Draw the vertical and horizontal lines Parameters ---------- ``**kwargs`` An keyword that is passed to the :func:`matplotlib.pyplot.plot` function""" if kwargs: self._line_kwargs = kwargs else: kwargs = self._line_kwargs if self.ax is None: import matplotlib.pyplot as plt self.ax = plt.gca() if self.ylim is None: self.ylim = ylim = self.ax.get_ylim() else: ylim = self.ylim if self.xlim is None: self.xlim = xlim = self.ax.get_xlim() else: xlim = self.xlim xmin = min(xlim) xmax = max(xlim) ymin = min(ylim) ymax = max(ylim) xy = zip(repeat(self.xa), self.ya) x, y = next(xy) # we plot the first separate line to get the correct color line = self.ax.plot(np.r_[[xmin], x, [xmax]], [y] * (len(x) + 2), markevery=slice(1, len(x) + 1), label='cross_mark_hline', visible=not self.hide_horizontal, **kwargs)[0] if 'color' not in kwargs and 'c' not in kwargs: kwargs['c'] = line.get_c() # now the rest of the horizontal lines self.hlines = [line] + [ self.ax.plot(np.r_[[xmin], x, [xmax]], [y] * (len(x) + 2), markevery=slice(1, len(x) + 1), label='cross_mark_hline', visible=not self.hide_horizontal, **kwargs)[0] for x, y in xy] # and the vertical lines self.vlines = [ self.ax.plot([x] * (len(y) + 2), np.r_[[ymin], y, [ymax]], markevery=slice(1, len(y) + 1), label='cross_mark_vline', visible=not self.hide_vertical, **kwargs)[0] for x, y in zip(self.xa, repeat(self.ya))] for h, v in zip(self.hlines, self.vlines): visible = v.get_visible() v.update_from(h) v.set_visible(visible) line = self.hlines[0] props = self._select_props if 'lw' not in props and 'linewidth' not in props: props.setdefault('lw', line.get_lw()) # copy the current attributes from the lines self._unselect_props = {key: getattr(line, 'get_' + key)() for key in props} if self.auto_hide: for l in chain(self.hlines, self.vlines, self.line_connections): l.set_lw(0)
[docs] def set_visible(self, b): """Set the visibility of the mark Parameters ---------- b: bool If False, hide all horizontal and vertical lines, and the :attr:`connected_artists`""" for l in self.hlines: l.set_visible(b and not self.hide_horizontal) for l in self.vlines: l.set_visible(b and not self.hide_vertical) show_connected = self.show_connected_artists and b for l in self.connected_artists: l.set_visible(show_connected)
def __reduce__(self): return ( self.__class__, ((self.xa, self.ya), # pos None, # ax -- do not make a plot self.selectable, # selectable self.draggable, # draggable self._idx_h, # idx_h self._idx_v, # idx_v self.xlim, # xlim self.ylim, # ylim self._select_props, # select_props self.auto_hide, # auto_hide [], # connected_artists self._lock_mark, # lock False, # draw_lines -- do not draw the lines ), {'_line_kwargs': self._line_kwargs, 'hide_horizontal': self.hide_horizontal, 'hide_vertical': self.hide_vertical, '_unselect_props': self._unselect_props, 'xa': self.xa, 'ya': self.ya} )
[docs] @staticmethod def maintain_y(marks): """Connect marks and maintain a constant vertical distance between them Parameters ---------- marks: list of CrossMarks A list of marks. If one of the marks is moved vertically, the others are, too""" for mark in marks: mark._maintain_y([m for m in marks if m is not mark])
def _maintain_y(self, marks): """Connect to marks and maintain a constant vertical distance Parameters ---------- marks: list of CrossMarks A list of other marks. If this mark is moved vertically, the others are, too""" y = self.y self._constant_dist_y.extend(m.y - y for m in marks) self._constant_dist_y_marks.extend(marks)
[docs] @staticmethod def maintain_x(marks): """Connect marks and maintain a constant horizontal distance Parameters ---------- marks: list of CrossMarks A list of marks. If one of the marks is moved horizontally, the others are, too""" for mark in marks: mark._maintain_x([m for m in marks if m is not mark])
def _maintain_x(self, marks): """Connect to marks and maintain a constant horizontal distance Parameters ---------- marks: list of CrossMarks A list of other marks. If this mark is moved horizontally, the others are, too""" x = self.x self._constant_dist_x.extend(m.x - x for m in marks) self._constant_dist_x_marks.extend(marks)
[docs] def connect_to_marks(self, marks, visible=False, append=True): """Append other marks that should be considered for aligning the lines Parameters ---------- marks: list of CrossMarks A list of other marks visible: bool If True, the marks are connected through visible lines append: bool If True, the marks are appended. This is important if the mark will be moved by the `set_pos` method Notes ----- This method can only be used to connect other marks with this mark. If you want to connect multiple marks within each other, use the :meth:`connect_marks` static method """ if append: self.other_marks.extend(marks) self._connection_visible.extend([visible] * len(marks)) if visible: ya = self.ya xa = self.xa ax = self.ax for m in marks: for i1, j1 in product(range(len(xa)), range(len(ya))): self.set_current_point(i1, j1) pos = self.pos for i2, j2 in product(range(len(m.xa)), range(len(m.ya))): m.set_current_point(i2, j2) line = ax.plot([pos[0], m.pos[0]], [pos[1], m.pos[1]], label='cross_mark_connection', **self._unselect_props)[0] if self.auto_hide: line.set_lw(0) self.line_connections.append(line) m.other_connections.append(line)
[docs] @staticmethod def connect_marks(marks, visible=False): """Connect multiple marks to each other Parameters ---------- marks: list of CrossMarks A list of marks visible: bool If True, the marks are connected through visible lines Notes ----- Different from the :meth:`connect_to_marks` method, this static function connects each of the marks to the others. """ for mark in marks: mark.connect_to_marks([m for m in marks if m is not mark], visible)
[docs] def connect(self): """Connect the marks matplotlib events""" fig = self.fig self.cidpress = fig.canvas.mpl_connect( 'button_press_event', self.on_press) self.cidrelease = fig.canvas.mpl_connect( 'button_release_event', self.on_release) self.cidmotion = fig.canvas.mpl_connect( 'motion_notify_event', self.on_motion)
[docs] def is_selected_by(self, event, buttons=[1]): """Test if the given `event` selects the mark Parameters ---------- event: matplotlib.backend_bases.MouseEvent The matplotlib event button: list of int Possible buttons to select this mark Returns ------- bool True, if it is selected""" return not ( self.lock is not None or event.inaxes != self.ax or event.button not in buttons or self.fig.canvas.manager.toolbar.mode != '' or not self.contains(event))
[docs] def set_current_point(self, x, y, nearest=False): """Set the current point that is selected Parameters ---------- x: int The index of the x-value in the :attr:`xa` attribute y: int The index of the y-value in the :attr:`ya` attribute nearest: bool If not None, `x` and `y` are interpreted as x- and y-values and we select the closest one """ if nearest: x = np.abs(self.xa - x).argmin() y = np.abs(self.ya - y).argmin() self._i_vline = x self._i_hline = y
[docs] def on_press(self, event, force=False, connected=True): """Select the mark Parameters ---------- event: matplotlib.backend_bases.MouseEvent The mouseevent that selects the mark force: bool If True, the mark is selected although it does not contain the `event` connected: bool If True, connected marks that should maintain a constant x- and y-distance are selected, too""" if not force and not self.is_selected_by(event): return self.set_current_point(event.xdata, event.ydata, True) # use only the upper most CrossMarks if self._lock_mark and connected: CrossMarks.lock = self if self._animated: self.hline.set_animated(True) self.vline.set_animated(True) self.background = self.fig.canvas.copy_from_bbox(self.ax.bbox) self.hline.update(self._select_props) self.vline.update(self._select_props) # toggle line connections artist_props = self._select_props.copy() for a in chain(self.line_connections, self.other_connections): a.update(artist_props) # toggle connected artists artist_props['visible'] = ( self.show_connected_artists and artist_props.get('visible', True)) for a in self.connected_artists: a.update(artist_props) self.press = self.pos[0], self.pos[1], event.xdata, event.ydata # select the connected marks that should maintain the distance if connected: for m in set(chain(self._constant_dist_y_marks, self._constant_dist_x_marks)): m._i_vline = self._i_vline m._i_hline = self._i_hline event.xdata, event.ydata = m.pos m.on_press(event, True, False) event.xdata, event.ydata = self.press[2:] for l in chain(self.other_connections, self.line_connections, self.connected_artists): self.ax.draw_artist(l) self.ax.draw_artist(self.hline) self.ax.draw_artist(self.vline) if self._animated: self.fig.canvas.blit(self.ax.bbox)
[docs] def contains(self, event): """Test if the mark is selected by the given `event` Parameters ---------- event: ButtonPressEvent The ButtonPressEvent that has been triggered""" contains = None if 'h' in self.selectable: contains = any(l.contains(event)[0] for l in self.hlines) if not contains and 'v' in self.selectable: contains = any(l.contains(event)[0] for l in self.vlines) return contains
[docs] def on_motion(self, event, force=False, move_connected=True, restore=True): """Move the lines of this mark Parameters ---------- event: matplotlib.backend_bases.MouseEvent The mouseevent that moves the mark force: bool If True, the mark is moved although it does not contain the `event` move_connected: bool If True, connected marks that should maintain a constant x- and y-distance are moved, too restore: bool If True, the axes background is restored""" if self.press is None or (not force and self._lock_mark and self.lock is not self): return if not force and event.inaxes != self.ax: return x0, y0, xpress, ypress = self.press dx = event.xdata - xpress dy = event.ydata - ypress canvas = self.fig.canvas if dy and 'h' in self.draggable: y1 = y0 + dy one_percent = np.abs(0.01 * np.diff(self.ax.get_ylim())[0]) for mark in filter(lambda m: m.ax is self.ax, self.other_marks): if np.abs(mark.pos[1] - y1) < one_percent: y1 = mark.pos[1] break if self.idx_v is not None: y1 = self.idx_v[self.idx_v.get_loc(y1, method='nearest')] self.hline.set_ydata([y1] * len(self.hline.get_ydata())) self.y = y1 # first we move the horizontal line that is associated with this # mark ydata = self.vline.get_ydata()[:] ydata[self._i_hline + 1] = y1 for l in self.vlines: l.set_ydata(ydata) # now we move all connections that are connected to this horizontal # layer for l in chain.from_iterable( self._all_line_connections[self._i_hline]): l.set_ydata([y1, l.get_ydata()[1]]) for l in chain.from_iterable( self._all_other_connections[self._i_hline]): l.set_ydata([l.get_ydata()[0], y1]) if dx and 'v' in self.draggable: x1 = x0 + dx one_percent = np.abs(0.01 * np.diff(self.ax.get_xlim())[0]) for mark in filter(lambda m: m.ax is self.ax, self.other_marks): if np.abs(mark.pos[0] - x1) < one_percent: x1 = mark.pos[0] break if self.idx_h is not None: x1 = self.idx_h[self.idx_h.get_loc(x1, method='nearest')] self.vline.set_xdata([x1] * len(self.vline.get_xdata())) self.x = x1 # first we move the vertical line that is associated with this mark xdata = self.hline.get_xdata()[:] xdata[self._i_vline + 1] = x1 for l in self.hlines: l.set_xdata(xdata) # now we move all connections that are connected to this vertical # layer for l in chain.from_iterable( l[self._i_vline] for l in self._all_line_connections): l.set_xdata([x1, l.get_xdata()[1]]) for l in chain.from_iterable( l[self._i_vline] for l in self._all_other_connections): l.set_xdata([l.get_xdata()[0], x1]) if restore and self._animated: canvas.restore_region(self.background) for l in chain(self.other_connections, self.line_connections, self.connected_artists): self.ax.draw_artist(l) if restore and self._animated: self.ax.draw_artist(self.hline) self.ax.draw_artist(self.vline) canvas.blit(self.ax.bbox) else: self.ax.figure.canvas.draw_idle() # move the marks that should maintain a constant distance orig_xy = (event.xdata, event.ydata) if move_connected and dy and 'h' in self.draggable: for dist, m in zip(self._constant_dist_y, self._constant_dist_y_marks): event.xdata = m.press[-2] event.ydata = y1 + dist m.on_motion(event, True, False, m.ax is not self.ax) if move_connected and dx and 'v' in self.draggable: for dist, m in zip(self._constant_dist_x, self._constant_dist_x_marks): event.xdata = x1 + dist event.ydata = m.press[-1] m.on_motion(event, True, False, m.ax is not self.ax) event.xdata, event.ydata = orig_xy
[docs] def set_connected_artists_visible(self, visible): """Set the visibility of the connected artists Parameters ---------- visible: bool True, show the connected artists, else don't""" self.show_connected_artists = visible for a in self.connected_artists: a.set_visible(visible) for d in self._connected_artists_props: d['visible'] = visible
[docs] def on_release(self, event, force=False, connected=True, draw=True, *args, **kwargs): """Release the mark and unselect it Parameters ---------- event: matplotlib.backend_bases.MouseEvent The mouseevent that releases the mark force: bool If True, the mark is released although it does not contain the `event` connected: bool If True, connected marks that should maintain a constant x- and y-distance are released, too draw: bool If True, the figure is drawn ``*args, **kwargs`` Any other parameter that is passed to the connected lines""" if (not force and self._lock_mark and self.lock is not self or self.press is None): return self.hline.update(self._unselect_props) self.vline.update(self._unselect_props) for d, a in zip_longest(self._connected_artists_props, self.connected_artists, fillvalue=self._unselect_props): a.update(d) for l in chain(self.line_connections, self.other_connections): l.update(self._unselect_props) if self.auto_hide: self.hline.set_lw(0) self.vline.set_lw(0) for l in chain(self.line_connections, self.other_connections): l.set_lw(0) self.xa[self._i_vline] = self.pos[0] self.ya[self._i_hline] = self.pos[1] pos0 = self.press[:2] self.press = None if self._animated: self.hline.set_animated(False) self.vline.set_animated(False) self.background = None if connected: for m in set(chain(self._constant_dist_y_marks, self._constant_dist_x_marks)): m.on_release(event, True, False, m.fig is not self.fig, *args, **kwargs) if self._lock_mark and self.lock is self: CrossMarks.lock = None if draw: self.fig.canvas.draw_idle() self.moved.emit(pos0, self)
[docs] def disconnect(self): """Disconnect all the stored connection ids""" fig = self.fig fig.canvas.mpl_disconnect(self.cidpress) fig.canvas.mpl_disconnect(self.cidrelease) fig.canvas.mpl_disconnect(self.cidmotion)
[docs] def remove(self, artists=True): """Remove all lines and disconnect the mark Parameters ---------- artists: bool If True, the :attr:`connected_artists` list is cleared and the corresponding artists are removed as well""" for l in chain(self.hlines, self.vlines, self.connected_artists if artists else [], chain.from_iterable(chain.from_iterable( self._all_other_connections)), chain.from_iterable(chain.from_iterable( self._all_line_connections))): try: l.remove() except ValueError: pass self.hlines.clear() self.vlines.clear() if artists: self.connected_artists.clear() # Remove the line connections visible_connections = [ m for m, v in zip(self.other_marks, self._connection_visible) if v] for m in visible_connections: for l in chain.from_iterable(chain.from_iterable( self._all_line_connections)): for i, j in product(range(len(m.ya)), range(len(m.xa))): if l in m._all_other_connections[i][j]: m._all_other_connections[i][j].remove(l) break for l in chain.from_iterable(chain.from_iterable( self._all_other_connections)): for i, j in product(range(len(m.ya)), range(len(m.xa))): if l in m._all_line_connections[i][j]: m._all_line_connections[i][j].remove(l) break self._all_line_connections = [[[] for _ in range(len(self.xa))] for _ in range(len(self.ya))] self._all_other_connections = [[[] for _ in range(len(self.xa))] for _ in range(len(self.ya))] self.disconnect()
[docs] def set_pos(self, pos): """Move the point(s) to another position Parameters ---------- pos: tuple of 2 arrays The positions of the crosses. The first item marks the x-coordinates of the points, the second the y-coordinates""" self.remove(artists=False) self.xa[:] = pos[0] self.ya[:] = pos[1] self.draw_lines(**self._line_kwargs) self.connect() visible_connections = [ m for m, v in zip(self.other_marks, self._connection_visible) if v] if visible_connections: self.connect_to_marks(visible_connections, True, append=False)
[docs]class DraggableHLine(CrossMarks): """A draggable horizontal line""" @property def x(self): raise NotImplementedError( 'There is no single x-value for a horizontal line!') hide_vertical = True docstrings.delete_params('CrossMarks.parameters', 'pos', 'ax', 'selectable', 'draggable') @docstrings.get_sectionsf('DraggableHLine') @docstrings.dedent def __init__(self, y, ax=None, *args, **kwargs): """ Parameters ---------- y: float The y-position for the horizontal line ax: matplotlib.axes.Axes The matplotlib axes %(CrossMarks.parameters.no_pos|ax|selectable|draggable)s """ if ax is None: import matplotlib.pyplot as plt ax = plt.gca() xlim = kwargs.get('xlim', ax.get_xlim()) x = np.mean(xlim) super(DraggableHLine, self).__init__( (x, y), ax, ['h'], ['h'], *args, **kwargs) def __reduce__(self): ret = list(super(DraggableHLine, self).__reduce__()) ret[1] = (self.ya, None) + ret[1][4:] return tuple(ret) def _maintain_x(self, marks): """Not implemented for DraggableHLine""" pass
[docs] def set_visible(self, b): for l in self.hlines: l.set_visible(b) show_connected = self.show_connected_artists and b for a in self.connected_artists: a.set_visible(show_connected)
[docs]class DraggableVLine(CrossMarks): """A draggable vertical line""" @property def y(self): raise NotImplementedError( 'There is no single y-value for a vertical line!') hide_horizontal = True @docstrings.get_sectionsf('DraggableVLine') @docstrings.dedent def __init__(self, x, ax=None, *args, **kwargs): """ Parameters ---------- x: float The x-position for the vertical line ax: matplotlib.axes.Axes The matplotlib axes %(CrossMarks.parameters.no_pos|ax|selectable|draggable)s """ if ax is None: import matplotlib.pyplot as plt ax = plt.gca() ylim = kwargs.get('ylim', ax.get_ylim()) y = np.mean(ylim) super(DraggableVLine, self).__init__( (x, y), ax, ['v'], ['v'], *args, **kwargs) def __reduce__(self): ret = list(super(DraggableVLine, self).__reduce__()) ret[1] = (self.xa, None) + ret[1][4:] return tuple(ret) def _maintain_y(self, marks): """Not implemented for DraggableVLine""" pass
[docs] def set_visible(self, b): for l in self.vlines: l.set_visible(b) show_connected = self.show_connected_artists and b for a in self.connected_artists: a.set_visible(show_connected)
docstrings.params['CrossMarksText.parameters.new'] = """ dtype: object The data type for the data conversion message: str The message to display in the dialog label: str The label to how this value should be named value: float The initial value to use""".strip()
[docs]class CrossMarkText(CrossMarks): """A CrossMarks that opens a QInputDialog after changing the position """ #: The value of this cross mark value = None @docstrings.get_sectionsf('CrossMarkText') @docstrings.dedent def __init__(self, *args, **kwargs): """ Parameters ---------- %(CrossMarks.parameters)s %(CrossMarksText.parameters.new)s""" self.dtype = kwargs.pop('dtype', str) self.message = kwargs.pop('message', 'Enter the value for this position') self.label = kwargs.pop('label', self.message) self.value = kwargs.pop('value', None) super(CrossMarkText, self).__init__(*args, **kwargs)
[docs] def ask_for_value(self, val=None, label=None): """Ask for a value for the cross mark This method opens a QInputDialog to ask for a new :attr:`value` Parameters ---------- val: float The initial value label: str the name of what to ask for""" from psyplot_gui.compat.qtcompat import QInputDialog, QLineEdit from psyplot_gui.main import mainwindow initial = str(val) if val is not None else '' value, ok = QInputDialog().getText( mainwindow, self.message, label or self.label, QLineEdit.Normal, initial) if ok: self.value = self.dtype(value)
[docs] def on_release(self, event, *args, **kwargs): """reimplemented to ask for the value if the shift key is not pressed """ if (kwargs.get('ask', True) and self.press is not None and event.key != 'shift'): self.ask_for_value() kwargs['ask'] = False super(CrossMarkText, self).on_release(event, *args, **kwargs)
on_release.__doc__ = CrossMarks.on_release.__doc__ def __reduce__(self): ret = super(CrossMarkText, self).__reduce__() ret[2]['dtype'] = self.dtype ret[2]['message'] = self.message return ret
[docs]class DraggableHLineText(DraggableHLine): """A CrossMarks that opens a QInputDialog after changing the position """ @docstrings.dedent def __init__(self, *args, **kwargs): """ Parameters ---------- %(DraggableHLine.parameters)s %(CrossMarksText.parameters.new)s""" self.dtype = kwargs.pop('dtype', str) self.message = kwargs.pop('message', 'Enter the value for this position') self.label = kwargs.pop('label', self.message) self.value = kwargs.pop('value', None) super(DraggableHLineText, self).__init__(*args, **kwargs)
[docs] def on_release(self, event, *args, **kwargs): # ask for the value if the shift key is not pressed if (kwargs.get('ask', True) and self.press is not None and event.key != 'shift'): self.ask_for_value() kwargs['ask'] = False super(DraggableHLineText, self).on_release(event, *args, **kwargs)
on_release.__doc__ = CrossMarks.on_release.__doc__
[docs] def ask_for_value(self, val=None, label=None): from psyplot_gui.compat.qtcompat import QInputDialog, QLineEdit from psyplot_gui.main import mainwindow initial = str(val) if val is not None else '' value, ok = QInputDialog().getText( mainwindow, self.message, label or self.label, QLineEdit.Normal, initial) if ok: self.value = self.dtype(value)
ask_for_value.__doc__ = CrossMarkText.ask_for_value.__doc__ def __reduce__(self): ret = super(DraggableHLineText, self).__reduce__() ret[2]['dtype'] = self.dtype ret[2]['message'] = self.message ret[2]['label'] = self.label ret[2]['value'] = self.value return ret
[docs]class DraggableVLineText(DraggableVLine): """A CrossMarks that opens a QInputDialog after changing the position """ @docstrings.dedent def __init__(self, *args, **kwargs): """ Parameters ---------- %(DraggableVLine.parameters)s %(CrossMarksText.parameters.new)s""" self.dtype = kwargs.pop('dtype', str) self.message = kwargs.pop('message', 'Enter the value for this position') self.label = kwargs.pop('label', self.message) self.value = kwargs.pop('value', None) super(DraggableVLineText, self).__init__(*args, **kwargs)
[docs] def on_release(self, event, *args, **kwargs): # ask for the value if the shift key is not pressed if (kwargs.get('ask', True) and self.press is not None and event.key != 'shift'): self.ask_for_value() kwargs['ask'] = False super(DraggableVLineText, self).on_release(event, *args, **kwargs)
on_release.__doc__ = CrossMarks.on_release.__doc__
[docs] def ask_for_value(self, val=None, label=None): from psyplot_gui.compat.qtcompat import QInputDialog, QLineEdit from psyplot_gui.main import mainwindow initial = str(val) if val is not None else '' value, ok = QInputDialog().getText( mainwindow, self.message, label or self.label, QLineEdit.Normal, initial) if ok: self.value = self.dtype(value)
ask_for_value.__doc__ = CrossMarkText.ask_for_value.__doc__ def __reduce__(self): ret = super(DraggableVLineText, self).__reduce__() ret[2]['dtype'] = self.dtype ret[2]['message'] = self.message ret[2]['label'] = self.label ret[2]['value'] = self.value return ret