diff options
Diffstat (limited to 'silx/gui/plot/items/core.py')
-rw-r--r-- | silx/gui/plot/items/core.py | 197 |
1 files changed, 186 insertions, 11 deletions
diff --git a/silx/gui/plot/items/core.py b/silx/gui/plot/items/core.py index bf3b719..e7342b0 100644 --- a/silx/gui/plot/items/core.py +++ b/silx/gui/plot/items/core.py @@ -30,6 +30,10 @@ __license__ = "MIT" __date__ = "29/01/2019" import collections +try: + from collections import abc +except ImportError: # Python2 support + import collections as abc from copy import deepcopy import logging import enum @@ -39,6 +43,7 @@ import weakref import numpy import six +from ....utils.enum import Enum as _Enum from ... import qt from ... import colors from ...colors import Colormap @@ -128,6 +133,9 @@ class ItemChangedType(enum.Enum): VISUALIZATION_MODE = 'visualizationModeChanged' """Item's visualization mode changed flag.""" + COMPLEX_MODE = 'complexModeChanged' + """Item's complex data visualization mode changed flag.""" + class Item(qt.QObject): """Description of an item of the plot""" @@ -404,6 +412,14 @@ class DraggableMixIn(ItemMixInBase): """ self._draggable = bool(draggable) + def drag(self, from_, to): + """Perform a drag of the item. + + :param List[float] from_: (x, y) previous position in data coordinates + :param List[float] to: (x, y) current position in data coordinates + """ + raise NotImplementedError("Must be implemented in subclass") + class ColormapMixIn(ItemMixInBase): """Mix-in class for items with colormap""" @@ -757,7 +773,164 @@ class AlphaMixIn(ItemMixInBase): self._updated(ItemChangedType.ALPHA) -class Points(Item, SymbolMixIn, AlphaMixIn): +class ComplexMixIn(ItemMixInBase): + """Mix-in class for complex data mode""" + + _SUPPORTED_COMPLEX_MODES = None + """Override to only support a subset of all ComplexMode""" + + class ComplexMode(_Enum): + """Identify available display mode for complex""" + ABSOLUTE = 'amplitude' + PHASE = 'phase' + REAL = 'real' + IMAGINARY = 'imaginary' + AMPLITUDE_PHASE = 'amplitude_phase' + LOG10_AMPLITUDE_PHASE = 'log10_amplitude_phase' + SQUARE_AMPLITUDE = 'square_amplitude' + + def __init__(self): + self.__complex_mode = self.ComplexMode.ABSOLUTE + + def getComplexMode(self): + """Returns the current complex visualization mode. + + :rtype: ComplexMode + """ + return self.__complex_mode + + def setComplexMode(self, mode): + """Set the complex visualization mode. + + :param ComplexMode mode: The visualization mode in: + 'real', 'imaginary', 'phase', 'amplitude' + :return: True if value was set, False if is was already set + :rtype: bool + """ + mode = self.ComplexMode.from_value(mode) + assert mode in self.supportedComplexModes() + + if mode != self.__complex_mode: + self.__complex_mode = mode + self._updated(ItemChangedType.COMPLEX_MODE) + return True + else: + return False + + def _convertComplexData(self, data, mode=None): + """Convert complex data to the specific mode. + + :param Union[ComplexMode,None] mode: + The kind of value to compute. + If None (the default), the current complex mode is used. + :return: The converted dataset + :rtype: Union[numpy.ndarray[float],None] + """ + if data is None: + return None + + if mode is None: + mode = self.getComplexMode() + + if mode is self.ComplexMode.REAL: + return numpy.real(data) + elif mode is self.ComplexMode.IMAGINARY: + return numpy.imag(data) + elif mode is self.ComplexMode.ABSOLUTE: + return numpy.absolute(data) + elif mode is self.ComplexMode.PHASE: + return numpy.angle(data) + elif mode is self.ComplexMode.SQUARE_AMPLITUDE: + return numpy.absolute(data) ** 2 + else: + raise ValueError('Unsupported conversion mode: %s', str(mode)) + + @classmethod + def supportedComplexModes(cls): + """Returns the list of supported complex visualization modes. + + See :class:`ComplexMode` and :meth:`setComplexMode`. + + :rtype: List[ComplexMode] + """ + if cls._SUPPORTED_COMPLEX_MODES is None: + return cls.ComplexMode.members() + else: + return cls._SUPPORTED_COMPLEX_MODES + + +class ScatterVisualizationMixIn(ItemMixInBase): + """Mix-in class for scatter plot visualization modes""" + + _SUPPORTED_SCATTER_VISUALIZATION = None + """Allows to override supported Visualizations""" + + @enum.unique + class Visualization(_Enum): + """Different modes of scatter plot visualizations""" + + POINTS = 'points' + """Display scatter plot as a point cloud""" + + LINES = 'lines' + """Display scatter plot as a wireframe. + + This is based on Delaunay triangulation + """ + + SOLID = 'solid' + """Display scatter plot as a set of filled triangles. + + This is based on Delaunay triangulation + """ + + def __init__(self): + self.__visualization = self.Visualization.POINTS + + @classmethod + def supportedVisualizations(cls): + """Returns the list of supported scatter visualization modes. + + See :meth:`setVisualization` + + :rtype: List[Visualization] + """ + if cls._SUPPORTED_SCATTER_VISUALIZATION is None: + return cls.Visualization.members() + else: + return cls._SUPPORTED_SCATTER_VISUALIZATION + + def setVisualization(self, mode): + """Set the scatter plot visualization mode to use. + + See :class:`Visualization` for all possible values, + and :meth:`supportedVisualizations` for supported ones. + + :param Union[str,Visualization] mode: + The visualization mode to use. + :return: True if value was set, False if is was already set + :rtype: bool + """ + mode = self.Visualization.from_value(mode) + assert mode in self.supportedVisualizations() + + if mode != self.__visualization: + self.__visualization = mode + + self._updated(ItemChangedType.VISUALIZATION_MODE) + return True + else: + return False + + def getVisualization(self): + """Returns the scatter plot visualization mode in use. + + :rtype: Visualization + """ + return self.__visualization + + +class PointsBase(Item, SymbolMixIn, AlphaMixIn): """Base class for :class:`Curve` and :class:`Scatter`""" # note: _logFilterData must be overloaded if you overload # getData to change its signature @@ -906,8 +1079,7 @@ class Points(Item, SymbolMixIn, AlphaMixIn): if (xPositive, yPositive) not in self._boundsCache: # use the getData class method because instance method can be # overloaded to return additional arrays - data = Points.getData(self, copy=False, - displayed=True) + data = PointsBase.getData(self, copy=False, displayed=True) if len(data) == 5: # hack to avoid duplicating caching mechanism in Scatter # (happens when cached data is used, caching done using @@ -916,12 +1088,15 @@ class Points(Item, SymbolMixIn, AlphaMixIn): else: x, y, _xerror, _yerror = data - self._boundsCache[(xPositive, yPositive)] = ( - numpy.nanmin(x), - numpy.nanmax(x), - numpy.nanmin(y), - numpy.nanmax(y) - ) + with warnings.catch_warnings(): + warnings.simplefilter('ignore', category=RuntimeWarning) + # Ignore All-NaN slice encountered + self._boundsCache[(xPositive, yPositive)] = ( + numpy.nanmin(x), + numpy.nanmax(x), + numpy.nanmin(y), + numpy.nanmax(y) + ) return self._boundsCache[(xPositive, yPositive)] def _getCachedData(self): @@ -1026,12 +1201,12 @@ class Points(Item, SymbolMixIn, AlphaMixIn): assert x.ndim == y.ndim == 1 if xerror is not None: - if isinstance(xerror, collections.Iterable): + if isinstance(xerror, abc.Iterable): xerror = numpy.array(xerror, copy=copy) else: xerror = float(xerror) if yerror is not None: - if isinstance(yerror, collections.Iterable): + if isinstance(yerror, abc.Iterable): yerror = numpy.array(yerror, copy=copy) else: yerror = float(yerror) |