Source code for silx.gui.plot.Plot

# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
# ###########################################################################*/
"""Plot API for 1D and 2D data.

The :class:`Plot` implements the plot API initially provided in PyMca.


Colormap
--------

The :class:`Plot` uses a dictionary to describe a colormap.
This dictionary has the following keys:

- 'name': str, name of the colormap. Available colormap are returned by
          :meth:`Plot.getSupportedColormaps`.
          At least 'gray', 'reversed gray', 'temperature',
          'red', 'green', 'blue' are supported.
- 'normalization': Either 'linear' or 'log'
- 'autoscale': bool, True to get bounds from the min and max of the
               data, False to use [vmin, vmax]
- 'vmin': float, min value, ignored if autoscale is True
- 'vmax': float, max value, ignored if autoscale is True
- 'colors': optional, custom colormap.
            Nx3 or Nx4 numpy array of RGB(A) colors,
            either uint8 or float in [0, 1].
            If 'name' is None, then this array is used as the colormap.


Plot Events
-----------

The Plot sends some event to the registered callback
(See :meth:`Plot.setCallback`).
Those events are sent as a dictionary with a key 'event' describing the kind
of event.

Drawing events
..............

'drawingProgress' and 'drawingFinished' events are sent during drawing
interaction (See :meth:`Plot.setInteractiveMode`).

- 'event': 'drawingProgress' or 'drawingFinished'
- 'parameters': dict of parameters used by the drawing mode.
                It has the following keys: 'shape', 'label', 'color'.
                See :meth:`Plot.setInteractiveMode`.
- 'points': Points (x, y) in data coordinates of the drawn shape.
            For 'hline' and 'vline', it is the 2 points defining the line.
            For 'line' and 'rectangle', it is the coordinates of the start
            drawing point and the latest drawing point.
            For 'polygon', it is the coordinates of all points of the shape.
- 'type': The type of drawing in 'line', 'hline', 'polygon', 'rectangle',
          'vline'.
- 'xdata' and 'ydata': X coords and Y coords of shape points in data
                       coordinates (as in 'points').

When the type is 'rectangle', the following additional keys are provided:

- 'x' and 'y': The origin of the rectangle in data coordinates
- 'widht' and 'height': The size of the rectangle in data coordinates


Mouse events
............

'mouseMoved', 'mouseClicked' and 'mouseDoubleClicked' events are sent for
mouse events.

They provide the following keys:

- 'event': 'mouseMoved', 'mouseClicked' or 'mouseDoubleClicked'
- 'button': the mouse button that was pressed in 'left', 'middle', 'right'
- 'x' and 'y': The mouse position in data coordinates
- 'xpixel' and 'ypixel': The mouse position in pixels


Marker events
.............

'hover', 'markerClicked', 'markerMoving' and 'markerMoved' events are
sent during interaction with markers.

'hover' is sent when the mouse cursor is over a marker.
'markerClicker' is sent when the user click on a selectable marker.
'markerMoving' and 'markerMoved' are sent when a draggable marker is moved.

They provide the following keys:

- 'event': 'hover', 'markerClicked', 'markerMoving' or 'markerMoved'
- 'button': the mouse button that is pressed in 'left', 'middle', 'right'
- 'draggable': True if the marker is draggable, False otherwise
- 'label': The legend associated with the clicked image or curve
- 'selectable': True if the marker is selectable, False otherwise
- 'type': 'marker'
- 'x' and 'y': The mouse position in data coordinates
- 'xdata' and 'ydata': The marker position in data coordinates

'markerClicked' and 'markerMoving' events have a 'xpixel' and a 'ypixel'
additional keys, that provide the mouse position in pixels.


Image and curve events
......................

'curveClicked' and 'imageClicked' events are sent when a selectable curve
or image is clicked.

Both share the following keys:

- 'event': 'curveClicked' or 'imageClicked'
- 'button': the mouse button that was pressed in 'left', 'middle', 'right'
- 'label': The legend associated with the clicked image or curve
- 'type': The type of item in 'curve', 'image'
- 'x' and 'y': The clicked position in data coordinates
- 'xpixel' and 'ypixel': The clicked position in pixels

'curveClicked' events have a 'xdata' and a 'ydata' additional keys, that
provide the coordinates of the picked points of the curve.
There can be more than one point of the curve being picked, and if a line of
the curve is picked, only the first point of the line is included in the list.

'imageClicked' have a 'col' and a 'row' additional keys, that provide
the column and row index in the image array that was clicked.


Limits changed events
.....................

'limitsChanged' events are sent when the limits of the plot are changed.
This can results from user interaction or API calls.

It provides the following keys:

- 'event': 'limitsChanged'
- 'source': id of the widget that emitted this event.
- 'xdata': Range of X in graph coordinates: (xMin, xMax).
- 'ydata': Range of Y in graph coordinates: (yMin, yMax).
- 'y2data': Range of right axis in graph coordinates (y2Min, y2Max) or None.

Plot state change events
........................

The following events are emitted when the plot is modified.
They provide the new state:

- 'setGraphCursor' event with a 'state' key (bool)
- 'setGraphGrid' event with a 'which' key (str), see :meth:`setGraphGrid`
- 'setKeepDataAspectRatio' event with a 'state' key (bool)
- 'setXAxisAutoScale' event with a 'state' key (bool)
- 'setXAxisLogarithmic' event with a 'state' key (bool)
- 'setYAxisAutoScale' event with a 'state' key (bool)
- 'setYAxisInverted' event with a 'state' key (bool)
- 'setYAxisLogarithmic' event with a 'state' key (bool)

A 'contentChanged' event is triggered when the content of the plot is updated.
It provides the following keys:

- 'action': The change of the plot: 'add' or 'remove'
- 'kind': The kind of primitive changed: 'curve', 'image', 'item' or 'marker'
- 'legend': The legend of the primitive changed.

'activeCurveChanged' and 'activeImageChanged' events with the following keys:

- 'legend': Name (str) of the current active item or None if no active item.
- 'previous': Name (str) of the previous active item or None if no item was
              active. It is the same as 'legend' if 'updated' == True
- 'updated': (bool) True if active item name did not changed,
             but active item data or style was updated.

'interactiveModeChanged' event with a 'source' key identifying the object
setting the interactive mode.
"""

__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
__date__ = "23/02/2016"


from collections import Iterable, OrderedDict, namedtuple
import logging

import numpy

# Import matplotlib backend here to init matplotlib our way
from .BackendMatplotlib import BackendMatplotlibQt
from . import Colors
from . import PlotInteraction
from . import PlotEvents
from . import _utils


_logger = logging.getLogger(__name__)


_COLORDICT = Colors.COLORDICT
_COLORLIST = [_COLORDICT['black'],
              _COLORDICT['blue'],
              _COLORDICT['red'],
              _COLORDICT['green'],
              _COLORDICT['pink'],
              _COLORDICT['yellow'],
              _COLORDICT['brown'],
              _COLORDICT['cyan'],
              _COLORDICT['magenta'],
              _COLORDICT['orange'],
              _COLORDICT['violet'],
              # _COLORDICT['bluegreen'],
              _COLORDICT['grey'],
              _COLORDICT['darkBlue'],
              _COLORDICT['darkRed'],
              _COLORDICT['darkGreen'],
              _COLORDICT['darkCyan'],
              _COLORDICT['darkMagenta'],
              _COLORDICT['darkYellow'],
              _COLORDICT['darkBrown']]


"""
Object returned when requesting the data range.
"""
_PlotDataRange = namedtuple('PlotDataRange',
                            ['x', 'y', 'yright'])


[docs]class Plot(object): """This class implements the plot API initially provided in PyMca. Supported backends: - 'matplotlib' and 'mpl': Matplotlib with Qt. - 'none': No backend, to run headless for testing purpose. :param parent: The parent widget of the plot (Default: None) :param backend: The backend to use. A str in: 'matplotlib', 'mpl', 'none' or a :class:`BackendBase.BackendBase` class """ defaultBackend = 'matplotlib' """Class attribute setting the default backend for all instances.""" colorList = _COLORLIST colorDict = _COLORDICT def __init__(self, parent=None, backend=None): self._autoreplot = False self._dirty = False if backend is None: backend = self.defaultBackend if hasattr(backend, "__call__"): self._backend = backend(self, parent) elif hasattr(backend, "lower"): lowerCaseString = backend.lower() if lowerCaseString in ("matplotlib", "mpl"): backendClass = BackendMatplotlibQt elif lowerCaseString == 'none': from .BackendBase import BackendBase as backendClass else: raise ValueError("Backend not supported %s" % backend) self._backend = backendClass(self, parent) else: raise ValueError("Backend not supported %s" % str(backend)) super(Plot, self).__init__() self.setCallback() # set _callback # Items handling self._curves = OrderedDict() self._hiddenCurves = set() self._images = OrderedDict() self._markers = OrderedDict() self._items = OrderedDict() self._dataRange = False # line types self._styleList = ['-', '--', '-.', ':'] self._colorIndex = 0 self._styleIndex = 0 self._activeCurveHandling = True self._activeCurve = None self._activeCurveColor = "#000000" self._activeImage = None # default properties self._cursorConfiguration = None self._logY = False self._logX = False self._xAutoScale = True self._yAutoScale = True self._grid = None # Store default labels provided to setGraph[X|Y]Label self._defaultLabels = {'x': '', 'y': '', 'yright': ''} # Store currently displayed labels # Current label can differ from input one with active curve handling self._currentLabels = {'x': '', 'y': '', 'yright': ''} self.setGraphTitle() self.setGraphXLabel() self.setGraphYLabel() self.setGraphYLabel('', axis='right') self.setDefaultColormap() # Init default colormap self.setDefaultPlotPoints(False) self.setDefaultPlotLines(True) self._eventHandler = PlotInteraction.PlotInteraction(self) self._eventHandler.setInteractiveMode('zoom', color=(0., 0., 0., 1.)) self._pressedButtons = [] # Currently pressed mouse buttons self._defaultDataMargins = (0., 0., 0., 0.) # Only activate autoreplot at the end # This avoids errors when loaded in Qt designer self._dirty = False self._autoreplot = True def _getDirtyPlot(self): """Return the plot dirty flag. If False, the plot has not changed since last replot. If True, the full plot need to be redrawn. If 'overlay', only the overlay has changed since last replot. It can be accessed by backend to check the dirty state. :return: False, True, 'overlay' """ return self._dirty def _setDirtyPlot(self, overlayOnly=False): """Mark the plot as needing redraw :param bool overlayOnly: True to redraw only the overlay, False to redraw everything """ wasDirty = self._dirty if not self._dirty and overlayOnly: self._dirty = 'overlay' else: self._dirty = True if self._autoreplot and not wasDirty: self._backend.postRedisplay() def _invalidateDataRange(self): """ Notifies this Plot instance that the range has changed and will have to be recomputed. """ self._dataRange = False def _updateDataRange(self): """ Recomputes the range of the data displayed on this Plot. """ # already available if self._dataRange is not False: return self._dataRange xMin = yMinLeft = yMinRight = float('nan') xMax = yMaxLeft = yMaxRight = float('nan') for curve, info in self._curves.items(): # using numpy's separate min and max is faster than # a pure python minmax. if info['xmin'] is not None: xMin = numpy.nanmin([xMin, info['xmin']]) if info['xmax'] is not None: xMax = numpy.nanmax([xMax, info['xmax']]) if info['params']['yaxis'] == 'left': if info['ymin'] is not None: yMinLeft = numpy.nanmin([yMinLeft, info['ymin']]) if info['ymax'] is not None: yMaxLeft = numpy.nanmax([yMaxLeft, info['ymax']]) else: if info['ymin'] is not None: yMinRight = numpy.nanmin([yMinRight, info['ymin']]) if info['ymax'] is not None: yMaxRight = numpy.nanmax([yMaxRight, info['ymax']]) if not self.isXAxisLogarithmic() and not self.isYAxisLogarithmic(): for image, info in self._images.items(): if info['data'] is not None: height, width = info['data'].shape[:2] params = info['params'] origin = params['origin'] scale = params['scale'] # Taking care of scale might be < 0 xCoords = [origin[0], origin[0] + width * scale[0]] xMin = numpy.nanmin([xMin] + xCoords) xMax = numpy.nanmax([xMax] + xCoords) # Taking care of scale might be < 0 yCoords = [origin[1], origin[1] + height * scale[1]] yMinLeft = numpy.nanmin([yMinLeft] + yCoords) yMaxLeft = numpy.nanmax([yMaxLeft] + yCoords) lGetRange = (lambda x, y: None if numpy.isnan(x) and numpy.isnan(y) else (x, y)) xRange = lGetRange(xMin, xMax) yLeftRange = lGetRange(yMinLeft, yMaxLeft) yRightRange = lGetRange(yMinRight, yMaxRight) self._dataRange = _PlotDataRange(x=xRange, y=yLeftRange, yright=yRightRange) return self._dataRange
[docs] def getDataRange(self): """ Returns this Plot's data range. :return: a namedtuple with the following members : x, y (left y axis), yright. Each member is a tuple (min, max) or None if no data is associated with the axis. :rtype: namedtuple """ return self._updateDataRange() # Add # add * input arguments management: # If an arg is set, then use it. # Else: # If a curve with the same legend exists, then use its arg value # Else, use a default value. # Store used value. # This value is used when curve is updated either internally or by user.
[docs] def addCurve(self, x, y, legend=None, info=None, replace=False, replot=None, color=None, symbol=None, linewidth=None, linestyle=None, xlabel=None, ylabel=None, yaxis=None, xerror=None, yerror=None, z=None, selectable=None, fill=None, resetzoom=True, **kw): """Add a 1D curve given by x an y to the graph. Curves are uniquely identified by their legend. To add multiple curves, call :meth:`addCurve` multiple times with different legend argument. To replace/update an existing curve, call :meth:`addCurve` with the existing curve legend. When curve parameters are not provided, if a curve with the same legend is displayed in the plot, its parameters are used. :param numpy.ndarray x: The data corresponding to the x coordinates :param numpy.ndarray y: The data corresponding to the y coordinates :param str legend: The legend to be associated to the curve (or None) :param info: User-defined information associated to the curve :param bool replace: True (the default) to delete already existing curves :param color: color(s) to be used :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or one of the predefined color names defined in Colors.py :param str symbol: Symbol to be drawn at each (x, y) position:: - 'o' circle - '.' point - ',' pixel - '+' cross - 'x' x-cross - 'd' diamond - 's' square - None (the default) to use default symbol :param float linewidth: The width of the curve in pixels (Default: 1). :param str linestyle: Type of line:: - ' ' no line - '-' solid line - '--' dashed line - '-.' dash-dot line - ':' dotted line - None (the default) to use default line style :param str xlabel: Label to show on the X axis when the curve is active or None to keep default axis label. :param str ylabel: Label to show on the Y axis when the curve is active or None to keep default axis label. :param str yaxis: The Y axis this curve is attached to. Either 'left' (the default) or 'right' :param xerror: Values with the uncertainties on the x values :type xerror: A float, or a numpy.ndarray of float32. If it is an array, it can either be a 1D array of same length as the data or a 2D array with 2 rows of same length as the data: row 0 for positive errors, row 1 for negative errors. :param yerror: Values with the uncertainties on the y values :type yerror: A float, or a numpy.ndarray of float32. See xerror. :param int z: Layer on which to draw the curve (default: 1) This allows to control the overlay. :param bool selectable: Indicate if the curve can be selected. (Default: True) :param bool fill: True to fill the curve, False otherwise (default). :param bool resetzoom: True (the default) to reset the zoom. :returns: The key string identify this curve """ # Take care of input parameters: check/conversion, default value if replot is not None: _logger.warning( 'addCurve deprecated replot argument, use resetzoom instead') resetzoom = replot and resetzoom if kw: _logger.warning('addCurve: deprecated extra arguments') legend = "Unnamed curve 1.1" if legend is None else str(legend) # Check/Convert input arguments # Convert to arrays (not forcing type) in order to avoid # problems at unexpected places: missing min or max attributes, problem # when using numpy.nonzero on lists, ... x = numpy.asarray(x) y = numpy.asarray(y) # TODO check color assert symbol in ('o', '.', ',', '+', 'x', 'd', 's', '', None) assert linestyle in ('', ' ', '-', '--', '-.', ':', None) if xlabel is not None: xlabel = str(xlabel) if ylabel is not None: ylabel = str(ylabel) assert yaxis in (None, 'left', 'right') # TODO check xerror, yerror if xerror is not None: xerror = numpy.asarray(xerror) if yerror is not None: yerror = numpy.asarray(yerror) if z is not None: z = int(z) if selectable is not None: selectable = bool(selectable) if fill is not None: fill = bool(fill) # Store all params with defaults in a dict to treat them at once params = { 'info': info, 'color': color, 'symbol': symbol, 'linewidth': linewidth, 'linestyle': linestyle, 'xlabel': xlabel, 'ylabel': ylabel, 'yaxis': yaxis, 'xerror': xerror, 'yerror': yerror, 'z': z, 'selectable': selectable, 'fill': fill } # Check if curve is previously active wasActive = self.getActiveCurve(just_legend=True) == legend # First, try to get defaults from existing curve with same name previousCurve = self._curves.get(legend, None) if previousCurve is not None: defaults = previousCurve['params'] else: # If no existing curve use default values default_color, default_linestyle = self._getColorAndStyle() defaults = { 'info': None, 'color': default_color, 'symbol': self._defaultPlotPoints, 'linewidth': 1, 'linestyle': default_linestyle, 'xlabel': None, 'ylabel': None, 'yaxis': 'left', 'xerror': None, 'yerror': None, 'z': 1, 'selectable': True, 'fill': False } # If a parameter is not given as argument, use its default value for key in defaults: if params[key] is None: params[key] = defaults[key] # Add: replace, filter data, add # This must be done after getting params from existing curve if replace: self.remove(kind='curve') else: # Remove previous curve from backend # but not from _curves and hiddenCurves to keep its place # This is a subset of self.remove(legend, kind='curve') if legend in self._curves: handle = self._curves[legend]['handle'] if handle is not None: self._backend.remove(handle) self._curves[legend]['handle'] = None self._setDirtyPlot() # Filter-out values <= 0 xFiltered, yFiltered, color, xerror, yerror = self._logFilterData( x, y, params['color'], params['xerror'], params['yerror'], self.isXAxisLogarithmic(), self.isYAxisLogarithmic()) if len(xFiltered) and not self.isCurveHidden(legend): handle = self._backend.addCurve(xFiltered, yFiltered, legend, color=color, symbol=params['symbol'], linestyle=params['linestyle'], linewidth=params['linewidth'], yaxis=params['yaxis'], xerror=xerror, yerror=yerror, z=params['z'], selectable=params['selectable'], fill=params['fill']) self._setDirtyPlot() # caching the min and max values for the getDataRange method. xMin = numpy.nanmin(xFiltered) xMax = numpy.nanmax(xFiltered) yMin = numpy.nanmin(yFiltered) yMax = numpy.nanmax(yFiltered) else: # The curve has no points or is hidden handle = None xMin, xMax, yMin, yMax = None, None, None, None self._curves[legend] = { 'handle': handle, 'x': x, 'y': y, 'params': params, 'xmin': xMin, 'xmax': xMax, 'ymin': yMin, 'ymax': yMax } self._invalidateDataRange() self.notify( 'contentChanged', action='add', kind='curve', legend=legend) if len(self._curves) == 1 or wasActive: self.setActiveCurve(legend) if resetzoom: # We ask for a zoom reset in order to handle the plot scaling # if the user does not want that, autoscale of the different # axes has to be set to off. self.resetZoom() return legend
[docs] def addImage(self, data, legend=None, info=None, replace=True, replot=None, xScale=None, yScale=None, z=None, selectable=False, draggable=False, colormap=None, pixmap=None, xlabel=None, ylabel=None, origin=None, scale=None, resetzoom=True, **kw): """Add a 2D dataset or an image to the plot. It displays either an array of data using a colormap or a RGB(A) image. Images are uniquely identified by their legend. To add multiple images, call :meth:`addImage` multiple times with different legend argument. To replace/update an existing image, call :meth:`addImage` with the existing image legend. When image parameters are not provided, if an image with the same legend is displayed in the plot, its parameters are used. :param numpy.ndarray data: (nrows, ncolumns) data or (nrows, ncolumns, RGBA) ubyte array :param str legend: The legend to be associated to the image (or None) :param info: User-defined information associated to the image :param bool replace: True (default) to delete already existing images :param int z: Layer on which to draw the image (default: 0) This allows to control the overlay. :param bool selectable: Indicate if the image can be selected. (default: False) :param bool draggable: Indicate if the image can be moved. (default: False) :param dict colormap: Description of the colormap to use (or None) This is ignored if data is a RGB(A) image. See :mod:`Plot` for the documentation of the colormap dict. :param pixmap: Pixmap representation of the data (if any) :type pixmap: (nrows, ncolumns, RGBA) ubyte array or None (default) :param str xlabel: X axis label to show when this curve is active, or None to keep default axis label. :param str ylabel: Y axis label to show when this curve is active, or None to keep default axis label. :param origin: (origin X, origin Y) of the data. It is possible to pass a single float if both coordinates are equal. Default: (0., 0.) :type origin: float or 2-tuple of float :param scale: (scale X, scale Y) of the data. It is possible to pass a single float if both coordinates are equal. Default: (1., 1.) :type scale: float or 2-tuple of float :param bool resetzoom: True (the default) to reset the zoom. :returns: The key string identify this image """ # Take care of input parameters: check/conversion, default value if xScale is not None or yScale is not None: _logger.warning( 'addImage deprecated xScale and yScale arguments,' 'use origin, scale arguments instead.') if origin is None and scale is None: origin = xScale[0], yScale[0] scale = xScale[1], yScale[1] else: _logger.warning( 'addCurve: xScale, yScale and origin, scale arguments' ' are conflicting. xScale and yScale are ignored.' ' Use only origin, scale arguments.') if replot is not None: _logger.warning( 'addImage deprecated replot argument, use resetzoom instead') resetzoom = replot and resetzoom if kw: _logger.warning('addImage: deprecated extra arguments') legend = "Unnamed Image 1.1" if legend is None else str(legend) # Check/Convert input arguments data = numpy.asarray(data) if origin is not None: if isinstance(origin, Iterable): origin = float(origin[0]), float(origin[1]) else: # single value origin origin = float(origin), float(origin) if scale is not None: if isinstance(scale, Iterable): scale = float(scale[0]), float(scale[1]) else: # single value scale scale = float(scale), float(scale) if z is not None: z = int(z) if selectable is not None: selectable = bool(selectable) if draggable is not None: draggable = bool(draggable) if pixmap is not None: pixmap = numpy.asarray(pixmap) if xlabel is not None: xlabel = str(xlabel) if ylabel is not None: ylabel = str(ylabel) # Store all params with defaults in a dict to treat them at once params = { 'info': info, 'origin': origin, 'scale': scale, 'z': z, 'selectable': selectable, 'draggable': draggable, 'colormap': colormap, 'xlabel': xlabel, 'ylabel': ylabel } # First, try to get defaults from existing curve with same name previousImage = self._images.get(legend, None) if previousImage is not None: defaults = previousImage['params'] else: # If no existing image use default values defaults = { 'info': None, 'origin': (0., 0.), 'scale': (1., 1.), 'z': 0, 'selectable': False, 'draggable': False, 'colormap': self.getDefaultColormap(), 'xlabel': None, 'ylabel': None } # If a parameter is not given as argument, use its default value for key in defaults: if params[key] is None: params[key] = defaults[key] # Check if curve is previously active wasActive = self.getActiveImage(just_legend=True) == legend # Add: replace, filter data, add if replace: self.remove(kind='image') else: # Remove previous image from backend # but not from _images to keep its place # This is a subset of self.remove(legend, kind='image') if legend in self._images: handle = self._images[legend]['handle'] if handle is not None: self._backend.remove(handle) self._images[legend]['handle'] = None self._setDirtyPlot() if self.isXAxisLogarithmic() or self.isYAxisLogarithmic(): _logger.info('Hide image while axes has log scale.') if (data is not None and not self.isXAxisLogarithmic() and not self.isYAxisLogarithmic()): if pixmap is not None: dataToSend = pixmap else: dataToSend = data handle = self._backend.addImage(dataToSend, legend=legend, origin=params['origin'], scale=params['scale'], z=params['z'], selectable=params['selectable'], draggable=params['draggable'], colormap=params['colormap']) self._setDirtyPlot() else: handle = None # data is None or log scale self._images[legend] = { 'handle': handle, 'data': data, 'pixmap': pixmap, 'params': params } if len(self._images) == 1 or wasActive: self.setActiveImage(legend) self._invalidateDataRange() self.notify( 'contentChanged', action='add', kind='image', legend=legend) if resetzoom: # We ask for a zoom reset in order to handle the plot scaling # if the user does not want that, autoscale of the different # axes has to be set to off. self.resetZoom() return legend
[docs] def addItem(self, xdata, ydata, legend=None, info=None, replace=False, shape="polygon", color='black', fill=True, overlay=False, z=None, **kw): """Add an item (i.e. a shape) to the plot. Items are uniquely identified by their legend. To add multiple items, call :meth:`addItem` multiple times with different legend argument. To replace/update an existing item, call :meth:`addItem` with the existing item legend. :param numpy.ndarray xdata: The X coords of the points of the shape :param numpy.ndarray ydata: The Y coords of the points of the shape :param str legend: The legend to be associated to the item :param info: User-defined information associated to the item :param bool replace: True (default) to delete already existing images :param str shape: Type of item to be drawn in hline, polygon (the default), rectangle, vline, polylines :param str color: Color of the item, e.g., 'blue', 'b', '#FF0000' (Default: 'black') :param bool fill: True (the default) to fill the shape :param bool overlay: True if item is an overlay (Default: False). This allows for rendering optimization if this item is changed often. :param int z: Layer on which to draw the item (default: 2) :returns: The key string identify this item """ # expected to receive the same parameters as the signal if kw: _logger.warning('addItem deprecated parameters: %s', str(kw)) legend = "Unnamed Item 1.1" if legend is None else str(legend) z = int(z) if z is not None else 2 if replace: self.remove(kind='item') else: self.remove(legend, kind='item') handle = self._backend.addItem(xdata, ydata, legend=legend, shape=shape, color=color, fill=fill, overlay=overlay, z=z) self._setDirtyPlot(overlayOnly=overlay) self._items[legend] = {'handle': handle, 'overlay': overlay} self.notify('contentChanged', action='add', kind='item', legend=legend) return legend
[docs] def addXMarker(self, x, legend=None, text=None, color=None, selectable=False, draggable=False, constraint=None, **kw): """Add a vertical line marker to the plot. Markers are uniquely identified by their legend. As opposed to curves, images and items, two calls to :meth:`addXMarker` without legend argument adds two markers with different identifying legends. :param float x: Position of the marker on the X axis in data coordinates :param str legend: Legend associated to the marker to identify it :param str text: Text to display on the marker. :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000' (Default: 'black') :param bool selectable: Indicate if the marker can be selected. (default: False) :param bool draggable: Indicate if the marker can be moved. (default: False) :param constraint: A function filtering marker displacement by dragging operations or None for no filter. This function is called each time a marker is moved. This parameter is only used if draggable is True. :type constraint: None or a callable that takes the coordinates of the current cursor position in the plot as input and that returns the filtered coordinates. :return: The key string identify this marker """ if kw: _logger.warning( 'addXMarker deprecated extra parameters: %s', str(kw)) return self._addMarker(x=x, y=None, legend=legend, text=text, color=color, selectable=selectable, draggable=draggable, symbol=None, constraint=constraint)
[docs] def addYMarker(self, y, legend=None, text=None, color=None, selectable=False, draggable=False, constraint=None, **kw): """Add a horizontal line marker to the plot. Markers are uniquely identified by their legend. As opposed to curves, images and items, two calls to :meth:`addYMarker` without legend argument adds two markers with different identifying legends. :param float y: Position of the marker on the Y axis in data coordinates :param str legend: Legend associated to the marker to identify it :param str text: Text to display next to the marker. :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000' (Default: 'black') :param bool selectable: Indicate if the marker can be selected. (default: False) :param bool draggable: Indicate if the marker can be moved. (default: False) :param constraint: A function filtering marker displacement by dragging operations or None for no filter. This function is called each time a marker is moved. This parameter is only used if draggable is True. :type constraint: None or a callable that takes the coordinates of the current cursor position in the plot as input and that returns the filtered coordinates. :return: The key string identify this marker """ if kw: _logger.warning( 'addYMarker deprecated extra parameters: %s', str(kw)) return self._addMarker(x=None, y=y, legend=legend, text=text, color=color, selectable=selectable, draggable=draggable, symbol=None, constraint=constraint)
[docs] def addMarker(self, x, y, legend=None, text=None, color=None, selectable=False, draggable=False, symbol='+', constraint=None, **kw): """Add a point marker to the plot. Markers are uniquely identified by their legend. As opposed to curves, images and items, two calls to :meth:`addMarker` without legend argument adds two markers with different identifying legends. :param float x: Position of the marker on the X axis in data coordinates :param float y: Position of the marker on the Y axis in data coordinates :param str legend: Legend associated to the marker to identify it :param str text: Text to display next to the marker :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000' (Default: 'black') :param bool selectable: Indicate if the marker can be selected. (default: False) :param bool draggable: Indicate if the marker can be moved. (default: False) :param str symbol: Symbol representing the marker in:: - 'o' circle - '.' point - ',' pixel - '+' cross (the default) - 'x' x-cross - 'd' diamond - 's' square :param constraint: A function filtering marker displacement by dragging operations or None for no filter. This function is called each time a marker is moved. This parameter is only used if draggable is True. :type constraint: None or a callable that takes the coordinates of the current cursor position in the plot as input and that returns the filtered coordinates. :return: The key string identify this marker """ if kw: _logger.warning( 'addMarker deprecated extra parameters: %s', str(kw)) if x is None: xmin, xmax = self.getGraphXLimits() x = 0.5 * (xmax + xmin) if y is None: ymin, ymax = self.getGraphYLimits() y = 0.5 * (ymax + ymin) return self._addMarker(x=x, y=y, legend=legend, text=text, color=color, selectable=selectable, draggable=draggable, symbol=symbol, constraint=constraint)
def _addMarker(self, x, y, legend, text, color, selectable, draggable, symbol, constraint): """Common method for adding point, vline and hline marker. See :meth:`addMarker` for argument documentation. """ if legend is None: legend = "Unnamed Marker 0" i = 1 while legend in self._markers: legend = "Unnamed Marker %d" % i i += 1 if color is None: color = self.colorDict['black'] elif color in self.colorDict: color = self.colorDict[color] if constraint is not None and not callable(constraint): # Then it must be a string if hasattr(constraint, 'lower'): if constraint.lower().startswith('h'): constraint = lambda xData, yData: (xData, y) elif constraint.lower().startswith('v'): constraint = lambda xData, yData: (x, yData) else: raise ValueError( "Unsupported constraint name: %s" % constraint) else: raise ValueError("Unsupported constraint") # Apply constraint to provided position if draggable and constraint is not None: x, y = constraint(x, y) if legend in self._markers: self.remove(legend, kind='marker') handle = self._backend.addMarker( x=x, y=y, legend=legend, text=text, color=color, selectable=selectable, draggable=draggable, symbol=symbol, constraint=constraint, overlay=draggable) self._markers[legend] = {'handle': handle, 'params': { 'x': x, 'y': y, 'text': text, 'color': color, 'selectable': selectable, 'draggable': draggable, 'symbol': symbol, 'constraint': constraint} } self._setDirtyPlot(overlayOnly=draggable) self.notify( 'contentChanged', action='add', kind='marker', legend=legend) return legend # Hide
[docs] def isCurveHidden(self, legend): """Returns True if the curve associated to legend is hidden, else False :param str legend: The legend key identifying the curve :return: True if the associated curve is hidden, False otherwise """ return legend in self._hiddenCurves
[docs] def hideCurve(self, legend, flag=True, replot=None): """Show/Hide the curve associated to legend. Even when hidden, the curve is kept in the list of curves. :param str legend: The legend associated to the curve to be hidden :param bool flag: True (default) to hide the curve, False to show it """ if replot is not None: _logger.warning('hideCurve deprecated replot parameter') if legend not in self._curves: _logger.warning('Curve not in plot: %s', legend) return if flag: handle = self._curves[legend]['handle'] if handle is not None: self._backend.remove(handle) self._curves[legend]['handle'] = None self._hiddenCurves.add(legend) else: self._hiddenCurves.discard(legend) curve = self._curves[legend] self.addCurve(curve['x'], curve['y'], legend, resetzoom=False, **curve['params']) self._setDirtyPlot() # Remove
ITEM_KINDS = 'curve', 'image', 'item', 'marker'
[docs] def remove(self, legend=None, kind=ITEM_KINDS): """Remove one or all element(s) of the given legend and kind. Examples: - ``remove()`` clears the plot - ``remove(kind='curve')`` removes all curves from the plot - ``remove('myCurve', kind='curve')`` removes the curve with legend 'myCurve' from the plot. - ``remove('myImage, kind='image')`` removes the image with legend 'myImage' from the plot. - ``remove('myImage')`` removes elements (for instance curve, image, item and marker) with legend 'myImage'. :param str legend: The legend associated to the element to remove, or None to remove :param kind: The kind of elements to remove from the plot. In: 'all', 'curve', 'image', 'item', 'marker'. By default, it removes all kind of elements. :type kind: str or tuple of str to specify multiple kinds. """ if kind is 'all': # Replace all by tuple of all kinds kind = self.ITEM_KINDS if kind in self.ITEM_KINDS: # Kind is a str, make it a tuple kind = (kind,) if legend is None: # This is a clear # Clear each given kind for aKind in kind: if aKind == 'curve': # Copy as _curves gets changed for legend in list(self._curves): self.remove(legend, kind='curve') self._curves = OrderedDict() self._hiddenCurves = set() self._colorIndex = 0 self._styleIndex = 0 elif aKind == 'image': # Copy as _images gets changed for legend in list(self._images): self.remove(legend, kind='image') self._images = OrderedDict() elif aKind == 'item': # Copy as _items gets changed for legend in list(self._items): self.remove(legend, kind='item') self._items = OrderedDict() elif aKind == 'marker': # Copy as _markers gets changed for legend in list(self._markers): self.remove(legend, kind='marker') self._markers = OrderedDict() else: _logger.warning('remove: Unhandled item kind %s', aKind) else: # This is removing a single element # Remove each given kind for aKind in kind: if aKind == 'curve': self._hiddenCurves.discard(legend) if legend in self._curves: if self.getActiveCurve(just_legend=True) == legend: # Reset active curve self.setActiveCurve(None) handle = self._curves[legend]['handle'] if handle is not None: self._backend.remove(handle) self._setDirtyPlot() del self._curves[legend] if not self._curves: self._colorIndex = 0 self._styleIndex = 0 self._invalidateDataRange() self.notify('contentChanged', action='remove', kind='curve', legend=legend) elif aKind == 'image': if legend in self._images: if self.getActiveImage(just_legend=True) == legend: # Reset active image self.setActiveImage(None) handle = self._images[legend]['handle'] if handle is not None: self._backend.remove(handle) self._setDirtyPlot() del self._images[legend] self._invalidateDataRange() self.notify('contentChanged', action='remove', kind='image', legend=legend) elif aKind == 'item': item = self._items.pop(legend, None) if item is not None: if item['handle'] is not None: self._backend.remove(item['handle']) self._setDirtyPlot(overlayOnly=item['overlay']) self.notify('contentChanged', action='remove', kind='item', legend=legend) elif aKind == 'marker': marker = self._markers.pop(legend, None) if marker is not None: if marker['handle'] is not None: self._backend.remove(marker['handle']) self._setDirtyPlot( overlayOnly=marker['params']['draggable']) self.notify('contentChanged', action='remove', kind='marker', legend=legend) else: _logger.warning('remove: Unhandled item kind %s', aKind)
[docs] def removeCurve(self, legend): """Remove the curve associated to legend from the graph. :param str legend: The legend associated to the curve to be deleted """ if legend is None: return self.remove(legend, kind='curve')
[docs] def removeImage(self, legend): """Remove the image associated to legend from the graph. :param str legend: The legend associated to the image to be deleted """ if legend is None: return self.remove(legend, kind='image')
[docs] def removeItem(self, legend): """Remove the item associated to legend from the graph. :param str legend: The legend associated to the item to be deleted """ if legend is None: return self.remove(legend, kind='item')
[docs] def removeMarker(self, legend): """Remove the marker associated to legend from the graph. :param str legend: The legend associated to the marker to be deleted """ if legend is None: return self.remove(legend, kind='marker') # Clear
[docs] def clear(self): """Remove everything from the plot.""" self.remove()
[docs] def clearCurves(self): """Remove all the curves from the plot.""" self.remove(kind='curve')
[docs] def clearImages(self): """Remove all the images from the plot.""" self.remove(kind='image')
[docs] def clearItems(self): """Remove all the items from the plot. """ self.remove(kind='item')
[docs] def clearMarkers(self): """Remove all the markers from the plot.""" self.remove(kind='marker') # Interaction
[docs] def getGraphCursor(self): """Returns the state of the crosshair cursor. See :meth:`setGraphCursor`. :return: None if the crosshair cursor is not active, else a tuple (color, linewidth, linestyle). """ return self._cursorConfiguration
[docs] def setGraphCursor(self, flag=False, color='black', linewidth=1, linestyle='-'): """Toggle the display of a crosshair cursor and set its attributes. :param bool flag: Toggle the display of a crosshair cursor. The crosshair cursor is hidden by default. :param color: The color to use for the crosshair. :type color: A string (either a predefined color name in Colors.py or "#RRGGBB")) or a 4 columns unsigned byte array (Default: black). :param int linewidth: The width of the lines of the crosshair (Default: 1). :param str linestyle: Type of line:: - ' ' no line - '-' solid line (the default) - '--' dashed line - '-.' dash-dot line - ':' dotted line """ if flag: self._cursorConfiguration = color, linewidth, linestyle else: self._cursorConfiguration = None self._backend.setGraphCursor(flag=flag, color=color, linewidth=linewidth, linestyle=linestyle) self._setDirtyPlot() self.notify('setGraphCursor', state=self._cursorConfiguration is not None)
[docs] def pan(self, direction, factor=0.1): """Pan the graph in the given direction by the given factor. Warning: Pan of right Y axis not implemented! :param str direction: One of 'up', 'down', 'left', 'right'. :param float factor: Proportion of the range used to pan the graph. Must be strictly positive. """ assert direction in ('up', 'down', 'left', 'right') assert factor > 0. if direction in ('left', 'right'): xFactor = factor if direction == 'right' else - factor xMin, xMax = self.getGraphXLimits() xMin, xMax = _utils.applyPan(xMin, xMax, xFactor, self.isXAxisLogarithmic()) self.setGraphXLimits(xMin, xMax) else: # direction in ('up', 'down') sign = -1. if self.isYAxisInverted() else 1. yFactor = sign * (factor if direction == 'up' else -factor) yMin, yMax = self.getGraphYLimits() yIsLog = self.isYAxisLogarithmic() yMin, yMax = _utils.applyPan(yMin, yMax, yFactor, yIsLog) self.setGraphYLimits(yMin, yMax, axis='left') y2Min, y2Max = self.getGraphYLimits(axis='right') y2Min, y2Max = _utils.applyPan(y2Min, y2Max, yFactor, yIsLog) self.setGraphYLimits(y2Min, y2Max, axis='right') # Active Curve/Image
[docs] def isActiveCurveHandling(self): """Returns True if active curve selection is enabled.""" return self._activeCurveHandling
[docs] def setActiveCurveHandling(self, flag=True): """Enable/Disable active curve selection. :param bool flag: True (the default) to enable active curve selection. """ if not flag: self.setActiveCurve(None) # Reset active curve self._activeCurveHandling = bool(flag)
[docs] def getActiveCurveColor(self): """Get the color used to display the currently active curve. See :meth:`setActiveCurveColor`. """ return self._activeCurveColor
[docs] def setActiveCurveColor(self, color="#000000"): """Set the color to use to display the currently active curve. :param str color: Color of the active curve, e.g., 'blue', 'b', '#FF0000' (Default: 'black') """ if color is None: color = "black" if color in self.colorDict: color = self.colorDict[color] self._activeCurveColor = color
[docs] def getActiveCurve(self, just_legend=False): """Return the currently active curve. It returns None in case of not having an active curve. Default output has the form: [x, y, legend, info, params] where params is a dictionary containing curve parameters. Warning: Returned values MUST not be modified. Make a copy if you need to modify them. :param bool just_legend: True to get the legend of the curve, False (the default) to get the curve data and info. :return: legend of the active curve or [x, y, legend, info, params] :rtype: str or list """ if not self.isActiveCurveHandling(): return None if self._activeCurve not in self._curves: self._activeCurve = None if self._activeCurve is None: return None if just_legend: return self._activeCurve else: curve = self._curves[self._activeCurve] return (curve['x'], curve['y'], self._activeCurve, curve['params']['info'] or {}, curve['params'])
[docs] def setActiveCurve(self, legend, replot=None): """Make the curve associated to legend the active curve. :param str legend: The legend associated to the curve or None to have no active curve. """ if replot is not None: _logger.warning('setActiveCurve deprecated replot parameter') if not self.isActiveCurveHandling(): return xLabel = self._defaultLabels['x'] yLabel = self._defaultLabels['y'] yRightLabel = self._defaultLabels['yright'] oldActiveCurveLegend = self.getActiveCurve(just_legend=True) if oldActiveCurveLegend: # Reset previous active curve handle = self._curves[oldActiveCurveLegend]['handle'] if handle is not None: self._backend.setActiveCurve(handle, False) if legend is None: self._activeCurve = None else: legend = str(legend) if legend not in self._curves: _logger.warning("This curve does not exist: %s", legend) self._activeCurve = None else: self._activeCurve = legend handle = self._curves[self._activeCurve]['handle'] if handle is not None: self._backend.setActiveCurve(handle, True, self.getActiveCurveColor()) curveParams = self._curves[self._activeCurve]['params'] if curveParams['xlabel'] is not None: xLabel = curveParams['xlabel'] if curveParams['ylabel'] is not None: if curveParams['yaxis'] == 'left': yLabel = curveParams['ylabel'] else: yRightLabel = curveParams['ylabel'] # Store current labels and update plot self._currentLabels['x'] = xLabel self._currentLabels['y'] = yLabel self._currentLabels['yright'] = yRightLabel self._backend.setGraphXLabel(xLabel) self._backend.setGraphYLabel(yLabel, axis='left') self._backend.setGraphYLabel(yRightLabel, axis='right') self._setDirtyPlot() if oldActiveCurveLegend is not None or self._activeCurve is not None: self.notify('activeCurveChanged', updated=oldActiveCurveLegend != self._activeCurve, previous=oldActiveCurveLegend, legend=self._activeCurve) return self._activeCurve
[docs] def getActiveImage(self, just_legend=False): """Returns the currently active image. It returns None in case of not having an active image. Default output has the form: [data, legend, info, pixmap, params] where params is a dictionnary containing image parameters. Warning: Returned values MUST not be modified. Make a copy if you need to modify them. :param bool just_legend: True to get the legend of the image, False (the default) to get the image data and info. :return: legend of active image or [data, legend, info, pixmap, params] :rtype: str or list """ if self._activeImage not in self._images: self._activeImage = None if just_legend: return self._activeImage if self._activeImage is None: return None else: image = self._images[self._activeImage] return (image['data'], self._activeImage, image['params']['info'] or {}, image['pixmap'], image['params'])
[docs] def setActiveImage(self, legend, replot=None): """Make the image associated to legend the active image. :param str legend: The legend associated to the image or None to have no active image. """ if replot is not None: _logger.warning('setActiveImage deprecated replot parameter') xLabel = self._defaultLabels['x'] yLabel = self._defaultLabels['y'] oldActiveImageLegend = self.getActiveImage(just_legend=True) if legend is None: self._activeImage = None else: legend = str(legend) if legend not in self._images: _logger.warning( "setActiveImage: This image does not exist: %s", legend) self._activeImage = None else: self._activeImage = legend imageParams = self._images[self._activeImage]['params'] if imageParams['xlabel'] is not None: xLabel = imageParams['xlabel'] if imageParams['ylabel'] is not None: yLabel = imageParams['ylabel'] # Store current labels and update plot self._currentLabels['x'] = xLabel self._currentLabels['y'] = yLabel self._backend.setGraphXLabel(xLabel) self._backend.setGraphYLabel(yLabel, axis='left') if oldActiveImageLegend is not None or self._activeImage is not None: self.notify('activeImageChanged', updated=oldActiveImageLegend != self._activeImage, previous=oldActiveImageLegend, legend=self._activeImage) return self._activeImage # Getters
[docs] def getAllCurves(self, just_legend=False, withhidden=False): """Returns all curves legend or info and data. It returns an empty list in case of not having any curve. If just_legend is False, it returns a list of the form: [[xvalues0, yvalues0, legend0, info0, params0], [xvalues1, yvalues1, legend1, info1, params1], [...], [xvaluesn, yvaluesn, legendn, infon, paramsn]] If just_legend is True, it returns a list of the form: [legend0, legend1, ..., legendn] Warning: Returned values MUST not be modified. Make a copy if you need to modify them. :param bool just_legend: True to get the legend of the curves, False (the default) to get the curves' data and info. :param bool withhidden: False (default) to skip hidden curves. :return: list of legends or list of [x, y, legend, info, params] :rtype: list of str or list of list """ output = [] for key in self._curves: if not withhidden and self.isCurveHidden(key): continue if just_legend: output.append(key) else: curve = self._curves[key] output.append((curve['x'], curve['y'], key, curve['params']['info'] or {}, curve['params'])) return output
[docs] def getCurve(self, legend=None): """Get the data and info of a specific curve. It returns None in case no matching curve is found. Warning: Returned values MUST not be modified. Make a copy if you need to modify them. :param str legend: The legend identifying the curve. If not provided or None (the default), the active curve is returned or if there is no active curve, the lastest updated curve that is not hidden. is returned if there are curves in the plot. :return: None or list [x, y, legend, parameters] """ if legend is None: legend = self.getActiveCurve(just_legend=True) if legend is None and self._curves: # There is no active curve, but there is some curves: # get one that is not hidden for curveLegend in reversed(list(self._curves.keys())): if curveLegend not in self._hiddenCurves: legend = curveLegend break if legend is not None and legend in self._curves: curve = self._curves[legend] return (curve['x'], curve['y'], legend, curve['params']['info'] or {}, curve['params']) else: return None
[docs] def getImage(self, legend=None): """Get the data and info of a specific image. It returns None in case no matching image is found. Warning: Returned values MUST not be modified. Make a copy if you need to modify them. :param str legend: The legend identifying the image. If not provided or None (the default), the active image is returned or if there is no active image, the lastest updated image is returned if there are images in the plot. :return: None or list [image, legend, info, pixmap, params] """ if legend is None: legend = self.getActiveImage(just_legend=True) if legend is None and self._images: # There is no active image, but there is some images: get one legend = list(self._images.keys())[-1] if legend is not None and legend in self._images: image = self._images[legend] return (image['data'], legend, image['params']['info'] or {}, image['pixmap'], image['params']) else: return None # Limits
def _notifyLimitsChanged(self): """Send an event when plot area limits are changed.""" xRange = self.getGraphXLimits() yRange = self.getGraphYLimits(axis='left') y2Range = self.getGraphYLimits(axis='right') event = PlotEvents.prepareLimitsChangedSignal( id(self.getWidgetHandle()), xRange, yRange, y2Range) self.notify(**event)
[docs] def getGraphXLimits(self): """Get the graph X (bottom) limits. :return: Minimum and maximum values of the X axis """ return self._backend.getGraphXLimits()
[docs] def setGraphXLimits(self, xmin, xmax, replot=None): """Set the graph X (bottom) limits. :param float xmin: minimum bottom axis value :param float xmax: maximum bottom axis value """ if replot is not None: _logger.warning('setGraphXLimits deprecated replot parameter') # Deal with incorrect values if xmax < xmin: _logger.warning('setGraphXLimits xmax < xmin, inverting limits.') xmin, xmax = xmax, xmin elif xmax == xmin: _logger.warning('setGraphXLimits xmax == xmin, expanding limits.') if xmin == 0.: xmin, xmax = -0.1, 0.1 else: xmin, xmax = xmin * 1.1, xmax * 0.9 self._backend.setGraphXLimits(xmin, xmax) self._setDirtyPlot() self._notifyLimitsChanged()
[docs] def getGraphYLimits(self, axis='left'): """Get the graph Y limits. :param str axis: The axis for which to get the limits: Either 'left' or 'right' :return: Minimum and maximum values of the X axis """ assert axis in ('left', 'right') return self._backend.getGraphYLimits(axis)
[docs] def setGraphYLimits(self, ymin, ymax, axis='left', replot=None): """Set the graph Y limits. :param float xmin: minimum bottom axis value :param float xmax: maximum bottom axis value :param str axis: The axis for which to get the limits: Either 'left' or 'right' """ if replot is not None: _logger.warning('setGraphYLimits deprecated replot parameter') # Deal with incorrect values if ymax < ymin: _logger.warning('setGraphYLimits ymax < ymin, inverting limits.') ymin, ymax = ymax, ymin elif ymax == ymin: _logger.warning('setGraphXLimits ymax == ymin, expanding limits.') if ymin == 0.: ymin, ymax = -0.1, 0.1 else: ymin, ymax = ymin * 1.1, ymax * 0.9 assert axis in ('left', 'right') self._backend.setGraphYLimits(ymin, ymax, axis) self._setDirtyPlot() self._notifyLimitsChanged()
[docs] def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None): """Set the limits of the X and Y axes at once. If y2min or y2max is None, the right Y axis limits are not updated. :param float xmin: minimum bottom axis value :param float xmax: maximum bottom axis value :param float ymin: minimum left axis value :param float ymax: maximum left axis value :param float y2min: minimum right axis value or None (the default) :param float y2max: maximum right axis value or None (the default) """ # Deal with incorrect values if xmax < xmin: _logger.warning('setLimits xmax < xmin, inverting limits.') xmin, xmax = xmax, xmin elif xmax == xmin: _logger.warning('setLimits xmax == xmin, expanding limits.') if xmin == 0.: xmin, xmax = -0.1, 0.1 else: xmin, xmax = xmin * 1.1, xmax * 0.9 if ymax < ymin: _logger.warning('setLimits ymax < ymin, inverting limits.') ymin, ymax = ymax, ymin elif ymax == ymin: _logger.warning('setLimits ymax == ymin, expanding limits.') if ymin == 0.: ymin, ymax = -0.1, 0.1 else: ymin, ymax = ymin * 1.1, ymax * 0.9 if y2min is None or y2max is None: # if one limit is None, both are ignored y2min, y2max = None, None else: if y2max < y2min: _logger.warning('setLimits y2max < y2min, inverting limits.') y2min, y2max = y2max, y2min elif y2max == y2min: _logger.warning('setLimits y2max == y2min, expanding limits.') if y2min == 0.: y2min, y2max = -0.1, 0.1 else: y2min, y2max = y2min * 1.1, y2max * 0.9 self._backend.setLimits(xmin, xmax, ymin, ymax, y2min, y2max) self._setDirtyPlot() self._notifyLimitsChanged() # Title and labels
[docs] def getGraphTitle(self): """Return the plot main title as a str.""" return self._graphTitle
[docs] def setGraphTitle(self, title=""): """Set the plot main title. :param str title: Main title of the plot (default: '') """ self._graphTitle = str(title) self._backend.setGraphTitle(title) self._setDirtyPlot()
[docs] def getGraphXLabel(self): """Return the current X axis label as a str.""" return self._defaultLabels['x']
[docs] def setGraphXLabel(self, label="X"): """Set the plot X axis label. The provided label can be temporarily replaced by the X label of the active curve if any. :param str label: The X axis label (default: 'X') """ self._defaultLabels['x'] = label self._currentLabels['x'] = label self._backend.setGraphXLabel(label) self._setDirtyPlot()
[docs] def getGraphYLabel(self, axis='left'): """Return the current Y axis label as a str. :param str axis: The Y axis for which to get the label (left or right) """ assert axis in ('left', 'right') return self._currentLabels['y' if axis == 'left' else 'yright']
[docs] def setGraphYLabel(self, label="Y", axis='left'): """Set the plot Y axis label. The provided label can be temporarily replaced by the Y label of the active curve if any. :param str label: The Y axis label (default: 'Y') :param str axis: The Y axis for which to set the label (left or right) """ assert axis in ('left', 'right') if axis == 'left': self._defaultLabels['y'] = label self._currentLabels['y'] = label else: self._defaultLabels['yright'] = label self._currentLabels['yright'] = label self._backend.setGraphYLabel(label, axis=axis) self._setDirtyPlot() # Axes
[docs] def setYAxisInverted(self, flag=True): """Set the Y axis orientation. :param bool flag: True for Y axis going from top to bottom, False for Y axis going from bottom to top """ flag = bool(flag) self._backend.setYAxisInverted(flag) self._setDirtyPlot() self.notify('setYAxisInverted', state=flag)
[docs] def isYAxisInverted(self): """Return True if Y axis goes from top to bottom, False otherwise.""" return self._backend.isYAxisInverted()
[docs] def isXAxisLogarithmic(self): """Return True if X axis scale is logarithmic, False if linear.""" return self._logX
[docs] def setXAxisLogarithmic(self, flag): """Set the bottom X axis scale (either linear or logarithmic). :param bool flag: True to use a logarithmic scale, False for linear. """ if bool(flag) == self._logX: return self._logX = bool(flag) if self._logX: # Switch to log scale for image in self._images.values(): if image['handle'] is not None: self._backend.remove(image['handle']) image['handle'] = None for curve in self._curves.values(): handle = curve['handle'] if handle is not None: self._backend.remove(handle) curve['handle'] = None # matplotlib 1.5 crashes if the log set is made before # the call to self._update() # TODO: Decide what is better for other backends if (hasattr(self._backend, "matplotlibVersion") and self._backend.matplotlibVersion >= "1.5"): self._update() self._backend.setXAxisLogarithmic(self._logX) else: self._backend.setXAxisLogarithmic(self._logX) self._update() else: self._backend.setXAxisLogarithmic(self._logX) self._update() self._invalidateDataRange() self._setDirtyPlot() self.resetZoom() self.notify('setXAxisLogarithmic', state=self._logX)
[docs] def isYAxisLogarithmic(self): """Return True if Y axis scale is logarithmic, False if linear.""" return self._logY
[docs] def setYAxisLogarithmic(self, flag): """Set the Y axes scale (either linear or logarithmic). :param bool flag: True to use a logarithmic scale, False for linear. """ if bool(flag) == self._logY: return self._logY = bool(flag) if self._logY: # Switch to log scale for image in self._images.values(): if image['handle'] is not None: self._backend.remove(image['handle']) image['handle'] = None for curve in self._curves.values(): handle = curve['handle'] if handle is not None: self._backend.remove(handle) curve['handle'] = None # matplotlib 1.5 crashes if the log set is made before # the call to self._update() # TODO: Decide what is better for other backends if (hasattr(self._backend, "matplotlibVersion") and self._backend.matplotlibVersion >= "1.5"): self._update() self._backend.setYAxisLogarithmic(self._logY) else: self._backend.setYAxisLogarithmic(self._logY) self._update() else: self._backend.setYAxisLogarithmic(self._logY) self._update() self._invalidateDataRange() self._setDirtyPlot() self.resetZoom() self.notify('setYAxisLogarithmic', state=self._logY)
[docs] def isXAxisAutoScale(self): """Return True if X axis is automatically adjusting its limits.""" return self._xAutoScale
[docs] def setXAxisAutoScale(self, flag=True): """Set the X axis limits adjusting behavior of :meth:`resetZoom`. :param bool flag: True to resize limits automatically, False to disable it. """ self._xAutoScale = bool(flag) self.notify('setXAxisAutoScale', state=self._xAutoScale)
[docs] def isYAxisAutoScale(self): """Return True if Y axes are automatically adjusting its limits.""" return self._yAutoScale
[docs] def setYAxisAutoScale(self, flag=True): """Set the Y axis limits adjusting behavior of :meth:`resetZoom`. :param bool flag: True to resize limits automatically, False to disable it. """ self._yAutoScale = bool(flag) self.notify('setYAxisAutoScale', state=self._yAutoScale)
[docs] def isKeepDataAspectRatio(self): """Returns whether the plot is keeping data aspect ratio or not.""" return self._backend.isKeepDataAspectRatio()
[docs] def setKeepDataAspectRatio(self, flag=True): """Set whether the plot keeps data aspect ratio or not. :param bool flag: True to respect data aspect ratio """ flag = bool(flag) self._backend.setKeepDataAspectRatio(flag=flag) self._setDirtyPlot() self.resetZoom() self.notify('setKeepDataAspectRatio', state=flag)
[docs] def getGraphGrid(self): """Return the current grid mode, either None, 'major' or 'both'. See :meth:`setGraphGrid`. """ return self._grid
[docs] def setGraphGrid(self, which=True): """Set the type of grid to display. :param which: None or False to disable the grid, 'major' or True for grid on major ticks (the default), 'both' for grid on both major and minor ticks. :type which: str of bool """ assert which in (None, True, False, 'both', 'major') if not which: which = None elif which is True: which = 'major' self._grid = which self._backend.setGraphGrid(which) self._setDirtyPlot() self.notify('setGraphGrid', which=str(which)) # Defaults
[docs] def isDefaultPlotPoints(self): """Return True if default Curve symbol is 'o', False for no symbol.""" return self._defaultPlotPoints == 'o'
[docs] def setDefaultPlotPoints(self, flag): """Set the default symbol of all curves. When called, this reset the symbol of all existing curves. :param bool flag: True to use 'o' as the default curve symbol, False to use no symbol. """ self._defaultPlotPoints = 'o' if flag else '' # Reset symbol of all curves for curve in self._curves.values(): curve['params']['symbol'] = self._defaultPlotPoints if self._curves: self._update() self._setDirtyPlot()
[docs] def isDefaultPlotLines(self): """Return True for line as default line style, False for no line.""" return self._plotLines
[docs] def setDefaultPlotLines(self, flag): """Toggle the use of lines as the default curve line style. :param bool flag: True to use a line as the default line style, False to use no line as the default line style. """ self._plotLines = bool(flag) # Reset linestyle of all curves for curve in self._curves.values(): curve['params']['linestyle'] = '-' if self._plotLines else ' ' if self._curves: self._update() self._setDirtyPlot()
[docs] def getDefaultColormap(self): """Return the default colormap used by :meth:`addImage` as a dict. See :mod:`Plot` for the documentation of the colormap dict. """ return self._defaultColormap.copy()
[docs] def setDefaultColormap(self, colormap=None): """Set the default colormap used by :meth:`addImage`. Setting the default colormap do not change any currently displayed image. It only affects future calls to :meth:`addImage` without the colormap parameter. :param dict colormap: The description of the default colormap, or None to set the colormap to a linear autoscale gray colormap. See :mod:`Plot` for the documentation of the colormap dict. """ if colormap is None: colormap = {'name': 'gray', 'normalization': 'linear', 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0} self._defaultColormap = colormap.copy()
[docs] def getSupportedColormaps(self): """Get the supported colormap names as a tuple of str. The list should at least contain and start by: ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue') """ return self._backend.getSupportedColormaps()
def _getColorAndStyle(self): color = self.colorList[self._colorIndex] style = self._styleList[self._styleIndex] # Loop over color and then styles self._colorIndex += 1 if self._colorIndex >= len(self.colorList): self._colorIndex = 0 self._styleIndex = (self._styleIndex + 1) % len(self._styleList) # If color is the one of active curve, take the next one if color == self.getActiveCurveColor(): color, style = self._getColorAndStyle() if not self._plotLines: style = ' ' return color, style # Misc.
[docs] def getWidgetHandle(self): """Return the widget the plot is displayed in. This widget is owned by the backend. """ return self._backend.getWidgetHandle()
[docs] def notify(self, event, **kwargs): """Send an event to the listeners. Event are passed to the registered callback as a dict with an 'event' key for backward compatibility with PyMca. :param str event: The type of event :param kwargs: The information of the event. """ eventDict = kwargs.copy() eventDict['event'] = event self._callback(eventDict)
[docs] def setCallback(self, callbackFunction=None): """Attach a listener to the backend. Limitation: Only one listener at a time. :param callbackFunction: function accepting a dictionnary as input to handle the graph events If None (default), use a default listener. """ # TODO allow multiple listeners, keep a weakref on it # allow register listener by event type if callbackFunction is None: callbackFunction = self.graphCallback self._callback = callbackFunction
[docs] def graphCallback(self, ddict=None): """This callback is going to receive all the events from the plot. Those events will consist on a dictionnary and among the dictionnary keys the key 'event' is mandatory to describe the type of event. This default implementation only handles setting the active curve. """ if ddict is None: ddict = {} _logger.debug("Received dict keys = %s", str(ddict.keys())) _logger.debug(str(ddict)) if ddict['event'] in ["legendClicked", "curveClicked"]: if ddict['button'] == "left": self.setActiveCurve(ddict['label'])
[docs] def saveGraph(self, filename, fileFormat=None, dpi=None, **kw): """Save a snapshot of the plot. Supported file formats: "png", "svg", "pdf", "ps", "eps", "tif", "tiff", "jpeg", "jpg". :param filename: Destination :type filename: str, StringIO or BytesIO :param str fileFormat: String specifying the format :return: False if cannot save the plot, True otherwise """ if kw: _logger.warning('Extra parameters ignored: %s', str(kw)) if fileFormat is None: if not hasattr(filename, 'lower'): _logger.warning( 'saveGraph cancelled, cannot define file format.') return False else: fileFormat = (filename.split(".")[-1]).lower() supportedFormats = ("png", "svg", "pdf", "ps", "eps", "tif", "tiff", "jpeg", "jpg") if fileFormat not in supportedFormats: _logger.warning('Unsupported format %s', fileFormat) return False else: self._backend.saveGraph(filename, fileFormat=fileFormat, dpi=dpi) return True
[docs] def getDataMargins(self): """Get the default data margin ratios, see :meth:`setDataMargins`. :return: The margin ratios for each side (xMin, xMax, yMin, yMax). :rtype: A 4-tuple of floats. """ return self._defaultDataMargins
[docs] def setDataMargins(self, xMinMargin=0., xMaxMargin=0., yMinMargin=0., yMaxMargin=0.): """Set the default data margins to use in :meth:`resetZoom`. Set the default ratios of margins (as floats) to add around the data inside the plot area for each side. """ self._defaultDataMargins = (xMinMargin, xMaxMargin, yMinMargin, yMaxMargin)
[docs] def getAutoReplot(self): """Return True if replot is automatically handled, False otherwise. See :meth`setAutoReplot`. """ return self._autoreplot
[docs] def setAutoReplot(self, autoreplot=True): """Set automatic replot mode. When enabled, the plot is redrawn automatically when changed. When disabled, the plot is not redrawn when its content change. Instead, it :meth:`replot` must be called. :param bool autoreplot: True to enable it (default), False to disable it. """ self._autoreplot = bool(autoreplot) # If the plot is dirty before enabling autoreplot, # then _backend.postRedisplay will never be called from _setDirtyPlot if self._autoreplot and self._getDirtyPlot(): self._backend.postRedisplay()
[docs] def replot(self): """Redraw the plot immediately.""" self._backend.replot() self._dirty = False # reset dirty flag
[docs] def resetZoom(self, dataMargins=None): """Reset the plot limits to the bounds of the data and redraw the plot. It automatically scale limits of axes that are in autoscale mode (See :meth:`setXAxisAutoScale`, :meth:`setYAxisAutoScale`). It keeps current limits on axes that are not in autoscale mode. Extra margins can be added around the data inside the plot area. Margins are given as one ratio of the data range per limit of the data (xMin, xMax, yMin and yMax limits). For log scale, extra margins are applied in log10 of the data. :param dataMargins: Ratios of margins to add around the data inside the plot area for each side (Default: no margins). :type dataMargins: A 4-tuple of float as (xMin, xMax, yMin, yMax). """ if dataMargins is None: dataMargins = self._defaultDataMargins xlim = self.getGraphXLimits() ylim = self.getGraphYLimits(axis='left') y2lim = self.getGraphYLimits(axis='right') self._backend.resetZoom(dataMargins) self._setDirtyPlot() if (xlim != self.getGraphXLimits() or ylim != self.getGraphYLimits(axis='left') or y2lim != self.getGraphYLimits(axis='right')): self._notifyLimitsChanged() # Internal
@staticmethod def _logFilterData(x, y, color, xerror, yerror, xLog, yLog): """Filter out values with x or y <= 0 on log axes All arrays are expected to have the same length. :param x: The x coords. :param y: The y coords. :param color: The addCurve color arg (might not be an array). :param xerror: The addCuve xerror arg (might not be an array). :param yerror: The addCuve yerror arg (might not be an array). :param bool xLog: True to filter arrays according to X coords. :param bool yLog: True to filter arrays according to Y coords. :return: The filter arrays or unchanged object if :rtype: (x, y, color, xerror, yerror) """ if xLog and yLog: idx = numpy.nonzero((x > 0) & (y > 0))[0] elif yLog: idx = numpy.nonzero(y > 0)[0] elif xLog: idx = numpy.nonzero(x > 0)[0] else: return x, y, color, xerror, yerror x = numpy.take(x, idx) y = numpy.take(y, idx) if isinstance(color, numpy.ndarray) and len(color) == len(x): # Nx(3 or 4) array (do not change RGBA color defined as an array) color = numpy.take(color, idx, axis=0) if isinstance(xerror, numpy.ndarray): if len(xerror) == len(x): # N or Nx1 array xerror = numpy.take(xerror, idx, axis=0) elif len(xerror) == 2 and len(xerror.shape) == 2: # 2xN array (+/- error) xerror = xerror[:, idx] if isinstance(yerror, numpy.ndarray): if len(yerror) == len(y): # N or Nx1 array yerror = numpy.take(yerror, idx, axis=0) elif len(yerror) == 2 and len(yerror.shape) == 2: # 2xN array (+/- error) yerror = yerror[:, idx] return x, y, color, xerror, yerror def _update(self): _logger.debug("_update called") # curves activeCurve = self.getActiveCurve(just_legend=True) curves = list(self._curves) for legend in curves: curve = self._curves[legend] self.addCurve(curve['x'], curve['y'], legend, resetzoom=False, **curve['params']) if len(curves): if activeCurve not in curves: activeCurve = curves[0] else: activeCurve = None self.setActiveCurve(activeCurve) # images if not self.isXAxisLogarithmic() and not self.isYAxisLogarithmic(): for legend in list(self._images): # Copy has images is changed image = self._images[legend] self.addImage(image['data'], legend, replace=False, resetzoom=False, pixmap=image['pixmap'], **image['params']) # Coord conversion
[docs] def dataToPixel(self, x=None, y=None, axis="left", check=True): """Convert a position in data coordinates to a position in pixels. :param float x: The X coordinate in data space. If None (default) the middle position of the displayed data is used. :param float y: The Y coordinate in data space. If None (default) the middle position of the displayed data is used. :param str axis: The Y axis to use for the conversion ('left' or 'right'). :param bool check: True to return None if outside displayed area, False to convert to pixels anyway :returns: The corresponding position in pixels or None if the data position is not in the displayed area and check is True. :rtype: A tuple of 2 floats: (xPixel, yPixel) or None. """ assert axis in ("left", "right") xmin, xmax = self.getGraphXLimits() ymin, ymax = self.getGraphYLimits(axis=axis) if x is None: x = 0.5 * (xmax + xmin) if y is None: y = 0.5 * (ymax + ymin) if check: if x > xmax or x < xmin: return None if y > ymax or y < ymin: return None return self._backend.dataToPixel(x, y, axis=axis)
[docs] def pixelToData(self, x, y, axis="left", check=False): """Convert a position in pixels to a position in data coordinates. :param float x: The X coordinate in pixels. If None (default) the center of the widget is used. :param float y: The Y coordinate in pixels. If None (default) the center of the widget is used. :param str axis: The Y axis to use for the conversion ('left' or 'right'). :param bool check: Toggle checking if pixel is in plot area. If False, this method never returns None. :returns: The corresponding position in data space or None if the pixel position is not in the plot area. :rtype: A tuple of 2 floats: (xData, yData) or None. """ assert axis in ("left", "right") return self._backend.pixelToData(x, y, axis=axis, check=check)
[docs] def getPlotBoundsInPixels(self): """Plot area bounds in widget coordinates in pixels. :return: bounds as a 4-tuple of int: (left, top, width, height) """ return self._backend.getPlotBoundsInPixels() # Interaction support
[docs] def setGraphCursorShape(self, cursor=None): """Set the cursor shape. :param str cursor: Name of the cursor shape """ self._backend.setGraphCursorShape(cursor)
def _pickMarker(self, x, y, test=None): """Pick a marker at the given position. To use for interaction implementation. :param float x: X position in pixels. :param float y: Y position in pixels. :param test: A callable to call for each picked marker to filter picked markers. If None (default), do not filter markers. """ if test is None: test = lambda marker: True markers = self._backend.pickItems(x, y) markers = [item for item in markers if item['kind'] == 'marker'] for item in reversed(markers): legend = item['legend'] params = self._getMarker(legend) if params is not None and test(params): params['legend'] = legend return params return None def _moveMarker(self, legend, x, y): """Move a marker to a position. To use for interaction implementation. :param str legend: The legend associated to the marker. :param float x: The new X position of the marker in data coordinates. :param float y: The new Y position of the marker in data coordinates. """ params = self._getMarker(legend) if params is not None: if params['x'] is not None: params['x'] = x if params['y'] is not None: params['y'] = y self._addMarker(**params) def _getMarker(self, legend): """Get the parameters of a marker :param str legend: The legend of the marker to retrieve :return: A copy of the parameters the marker has been created with :rtype: dict or None if marker does not exist """ marker = self._markers.get(legend, None) if marker is None: return None else: # Return a shallow copy params = marker['params'].copy() params['legend'] = legend return params def _pickImageOrCurve(self, x, y, test=None): """Pick an image or a curve at the given position. To use for interaction implementation. :param float x: X position in pixels. :param float y: Y position in pixels. :param test: A callable to call for each picked item to filter picked items. If None (default), do not filter items. """ if test is None: test = lambda item: True items = self._backend.pickItems(x, y) items = [item for item in items if item['kind'] in ['curve', 'image']] for item in reversed(items): kind, legend = item['kind'], item['legend'] if kind == 'curve': curve = self._curves.get(legend, None) if curve is not None: params = curve['params'].copy() # shallow copy if test(params): params['legend'] = legend return kind, params, item['xdata'], item['ydata'] elif kind == 'image': image = self._images.get(legend, None) if image is not None: params = image['params'].copy() # shallow copy if test(params): params['legend'] = legend return kind, params, None else: _logger.warning('Unsupported kind: %s', kind) return None def _moveImage(self, legend, dx, dy): """Move an image to a position. To use for interaction implementation. :param str legend: The legend associated to the image. :param float dx: The X offset to apply to the image in data coords. :param float dy: The Y offset to apply to the image in data coords. """ # TODO: poor implementation, better to do move image in backend... image = self._images[legend] params = image['params'].copy() params['origin'] = params['origin'][0] + dx, params['origin'][1] + dy self.addImage(image['data'], legend, replace=False, resetzoom=False, pixmap=image['pixmap'], **params) # User event handling # def _isPositionInPlotArea(self, x, y): """Project position in pixel to the closest point in the plot area :param float x: X coordinate in widget coordinate (in pixel) :param float y: Y coordinate in widget coordinate (in pixel) :return: (x, y) in widget coord (in pixel) in the plot area """ left, top, width, height = self.getPlotBoundsInPixels() xPlot = _utils.clamp(x, left, left + width) yPlot = _utils.clamp(y, top, top + height) return xPlot, yPlot
[docs] def onMousePress(self, xPixel, yPixel, btn): """Handle mouse press event. :param float xPixel: X mouse position in pixels :param float yPixel: Y mouse position in pixels :param str btn: Mouse button in 'left', 'middle', 'right' """ if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel): self._pressedButtons.append(btn) self._eventHandler.handleEvent('press', xPixel, yPixel, btn)
[docs] def onMouseMove(self, xPixel, yPixel): """Handle mouse move event. :param float xPixel: X mouse position in pixels :param float yPixel: Y mouse position in pixels """ inXPixel, inYPixel = self._isPositionInPlotArea(xPixel, yPixel) isCursorInPlot = inXPixel == xPixel and inYPixel == yPixel if isCursorInPlot: # Signal mouse move event dataPos = self.pixelToData(inXPixel, inYPixel) assert dataPos is not None btn = self._pressedButtons[-1] if self._pressedButtons else None event = PlotEvents.prepareMouseSignal( 'mouseMoved', btn, dataPos[0], dataPos[1], xPixel, yPixel) self.notify(**event) # Either button was pressed in the plot or cursor is in the plot if isCursorInPlot or self._pressedButtons: self._eventHandler.handleEvent('move', inXPixel, inYPixel)
[docs] def onMouseRelease(self, xPixel, yPixel, btn): """Handle mouse release event. :param float xPixel: X mouse position in pixels :param float yPixel: Y mouse position in pixels :param str btn: Mouse button in 'left', 'middle', 'right' """ try: self._pressedButtons.remove(btn) except ValueError: pass else: xPixel, yPixel = self._isPositionInPlotArea(xPixel, yPixel) self._eventHandler.handleEvent('release', xPixel, yPixel, btn)
[docs] def onMouseWheel(self, xPixel, yPixel, angleInDegrees): """Handle mouse wheel event. :param float xPixel: X mouse position in pixels :param float yPixel: Y mouse position in pixels :param float angleInDegrees: Angle corresponding to wheel motion. Positive for movement away from the user, negative for movement toward the user. """ if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel): self._eventHandler.handleEvent( 'wheel', xPixel, yPixel, angleInDegrees) # Interaction modes #
[docs] def getInteractiveMode(self): """Returns the current interactive mode as a dict. The returned dict contains at least the key 'mode'. Mode can be: 'draw', 'pan', 'select', 'zoom'. It can also contains extra keys (e.g., 'color') specific to a mode as provided to :meth:`setInteractiveMode`. """ return self._eventHandler.getInteractiveMode()
[docs] def setInteractiveMode(self, mode, color='black', shape='polygon', label=None, zoomOnWheel=True, source=None): """Switch the interactive mode. :param str mode: The name of the interactive mode. In 'draw', 'pan', 'select', 'zoom'. :param color: Only for 'draw' and 'zoom' modes. Color to use for drawing selection area. Default black. :type color: Color description: The name as a str or a tuple of 4 floats. :param str shape: Only for 'draw' mode. The kind of shape to draw. In 'polygon', 'rectangle', 'line', 'vline', 'hline', 'freeline'. Default is 'polygon'. :param str label: Only for 'draw' mode, sent in drawing events. :param bool zoomOnWheel: Toggle zoom on wheel support :param source: A user-defined object (typically the caller object) that will be send in the interactiveModeChanged event, to identify which object required a mode change. Default: None """ self._eventHandler.setInteractiveMode(mode, color, shape, label) self._eventHandler.zoomOnWheel = zoomOnWheel self.notify( 'interactiveModeChanged', source=source) # Deprecated #
[docs] def isDrawModeEnabled(self): """Deprecated, use :meth:`getInteractiveMode` instead. Return True if the current interactive state is drawing.""" _logger.warning( 'isDrawModeEnabled deprecated, use getInteractiveMode instead') return self.getInteractiveMode()['mode'] == 'draw'
[docs] def setDrawModeEnabled(self, flag=True, shape='polygon', label=None, color=None, **kwargs): """Deprecated, use :meth:`setInteractiveMode` instead. Set the drawing mode if flag is True and its parameters. If flag is False, only item selection is enabled. Warning: Zoom and drawing are not compatible and cannot be enabled simultanelously. :param bool flag: True to enable drawing and disable zoom and select. :param str shape: Type of item to be drawn in: hline, vline, rectangle, polygon (default) :param str label: Associated text for identifying draw signals :param color: The color to use to draw the selection area :type color: string ("#RRGGBB") or 4 column unsigned byte array or one of the predefined color names defined in Colors.py """ _logger.warning( 'setDrawModeEnabled deprecated, use setInteractiveMode instead') if kwargs: _logger.warning('setDrawModeEnabled ignores additional parameters') if color is None: color = 'black' if flag: self.setInteractiveMode('draw', shape=shape, label=label, color=color) elif self.getInteractiveMode()['mode'] == 'draw': self.setInteractiveMode('select')
[docs] def getDrawMode(self): """Deprecated, use :meth:`getInteractiveMode` instead. Return the draw mode parameters as a dict of None. It returns None if the interactive moed is not a drawing mode, otherwise, it returns a dict containing the drawing mode parameters as provided to :meth:`setDrawModeEnabled`. """ _logger.warning( 'getDrawMode deprecated, use getInteractiveMode instead') mode = self.getInteractiveMode() return mode if mode['mode'] == 'draw' else None
[docs] def isZoomModeEnabled(self): """Deprecated, use :meth:`getInteractiveMode` instead. Return True if the current interactive state is zooming.""" _logger.warning( 'isZoomModeEnabled deprecated, use getInteractiveMode instead') return self.getInteractiveMode()['mode'] == 'zoom'
[docs] def setZoomModeEnabled(self, flag=True, color=None): """Deprecated, use :meth:`setInteractiveMode` instead. Set the zoom mode if flag is True, else item selection is enabled. Warning: Zoom and drawing are not compatible and cannot be enabled simultanelously :param bool flag: If True, enable zoom and select mode. :param color: The color to use to draw the selection area. (Default: 'black') :param color: The color to use to draw the selection area :type color: string ("#RRGGBB") or 4 column unsigned byte array or one of the predefined color names defined in Colors.py """ _logger.warning( 'setZoomModeEnabled deprecated, use setInteractiveMode instead') if color is None: color = 'black' if flag: self.setInteractiveMode('zoom', color=color) elif self.getInteractiveMode()['mode'] == 'zoom': self.setInteractiveMode('select')
[docs] def insertMarker(self, *args, **kwargs): """Deprecated, use :meth:`addMarker` instead.""" _logger.warning( 'insertMarker deprecated, use addMarker instead.') return self.addMarker(*args, **kwargs)
[docs] def insertXMarker(self, *args, **kwargs): """Deprecated, use :meth:`addXMarker` instead.""" _logger.warning( 'insertXMarker deprecated, use addXMarker instead.') return self.addXMarker(*args, **kwargs)
[docs] def insertYMarker(self, *args, **kwargs): """Deprecated, use :meth:`addYMarker` instead.""" _logger.warning( 'insertYMarker deprecated, use addYMarker instead.') return self.addYMarker(*args, **kwargs)
[docs] def isActiveCurveHandlingEnabled(self): """Deprecated, use :meth:`isActiveCurveHandling` instead.""" _logger.warning( 'isActiveCurveHandlingEnabled deprecated, ' 'use isActiveCurveHandling instead.') return self.isActiveCurveHandling()
[docs] def enableActiveCurveHandling(self, *args, **kwargs): """Deprecated, use :meth:`setActiveCurveHandling` instead.""" _logger.warning( 'enableActiveCurveHandling deprecated, ' 'use setActiveCurveHandling instead.') return self.setActiveCurveHandling(*args, **kwargs)
[docs] def invertYAxis(self, *args, **kwargs): """Deprecated, use :meth:`setYAxisInverted` instead.""" _logger.warning('invertYAxis deprecated, ' 'use setYAxisInverted instead.') return self.setYAxisInverted(*args, **kwargs)
[docs] def showGrid(self, flag=True): """Deprecated, use :meth:`setGraphGrid` instead.""" _logger.warning("showGrid deprecated, use setGraphGrid instead") if flag in (0, False): flag = None elif flag in (1, True): flag = 'major' else: flag = 'both' return self.setGraphGrid(flag)
[docs] def keepDataAspectRatio(self, *args, **kwargs): """Deprecated, use :meth:`setKeepDataAspectRatio`.""" _logger.warning('keepDataAspectRatio deprecated,' 'use setKeepDataAspectRatio instead') return self.setKeepDataAspectRatio(*args, **kwargs)