diff options
Diffstat (limited to 'silx/gui/plot/items/core.py')
-rw-r--r-- | silx/gui/plot/items/core.py | 223 |
1 files changed, 200 insertions, 23 deletions
diff --git a/silx/gui/plot/items/core.py b/silx/gui/plot/items/core.py index 6d6575b..9426a13 100644 --- a/silx/gui/plot/items/core.py +++ b/silx/gui/plot/items/core.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2019 European Synchrotron Radiation Facility +# Copyright (c) 2017-2020 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 @@ -43,6 +43,7 @@ import weakref import numpy import six +from ....utils.deprecation import deprecated from ....utils.enum import Enum as _Enum from ... import qt from ... import colors @@ -143,6 +144,9 @@ class ItemChangedType(enum.Enum): EDITABLE = 'editableChanged' """Item's editable state changed flags.""" + SELECTABLE = 'selectableChanged' + """Item's selectable state changed flags.""" + class Item(qt.QObject): """Description of an item of the plot""" @@ -150,9 +154,6 @@ class Item(qt.QObject): _DEFAULT_Z_LAYER = 0 """Default layer for overlay rendering""" - _DEFAULT_LEGEND = '' - """Default legend of items""" - _DEFAULT_SELECTABLE = False """Default selectable state of items""" @@ -168,19 +169,19 @@ class Item(qt.QObject): self._dirty = True self._plotRef = None self._visible = True - self._legend = self._DEFAULT_LEGEND self._selectable = self._DEFAULT_SELECTABLE self._z = self._DEFAULT_Z_LAYER self._info = None self._xlabel = None self._ylabel = None + self.__name = '' self._backendRenderer = None def getPlot(self): - """Returns Plot this item belongs to. + """Returns the ~silx.gui.plot.PlotWidget this item belongs to. - :rtype: Plot or None + :rtype: Union[~silx.gui.plot.PlotWidget,None] """ return None if self._plotRef is None else self._plotRef() @@ -189,7 +190,7 @@ class Item(qt.QObject): WARNING: This should only be called from the Plot. - :param Plot plot: The Plot instance. + :param Union[~silx.gui.plot.PlotWidget,None] plot: The Plot instance. """ if plot is not None and self._plotRef is not None: raise RuntimeError('Trying to add a node at two places.') @@ -234,19 +235,35 @@ class Item(qt.QObject): """ return False - def getLegend(self): - """Returns the legend of this item (str)""" - return self._legend + def getName(self): + """Returns the name of the item which is used as legend. - def _setLegend(self, legend): - """Set the legend. + :rtype: str + """ + return self.__name - This is private as it is used by the plot as an identifier + def setName(self, name): + """Set the name of the item which is used as legend. - :param str legend: Item legend + :param str name: New name of the item + :raises RuntimeError: If item belongs to a PlotWidget. """ - legend = str(legend) if legend is not None else self._DEFAULT_LEGEND - self._legend = legend + name = str(name) + if self.__name != name: + if self.getPlot() is not None: + raise RuntimeError( + "Cannot change name while item is in a PlotWidget") + + self.__name = name + self._updated(ItemChangedType.NAME) + + def getLegend(self): # Replaced by getName for API consistency + return self.getName() + + @deprecated(replacement='setName', since_version='0.13') + def _setLegend(self, legend): + legend = str(legend) if legend is not None else '' + self.setName(legend) def isSelectable(self): """Returns true if item is selectable (bool)""" @@ -355,12 +372,12 @@ class Item(qt.QObject): if indices is None: return None else: - return PickingResult(self, indices if len(indices) != 0 else None) + return PickingResult(self, indices) # Mix-in classes ############################################################## -class ItemMixInBase(qt.QObject): +class ItemMixInBase(object): """Base class for Item mix-in""" def _updated(self, event=None, checkVisibility=True): @@ -454,6 +471,8 @@ class ColormapMixIn(ItemMixInBase): def __init__(self): self._colormap = Colormap() self._colormap.sigChanged.connect(self._colormapChanged) + self.__data = None + self.__cacheColormapRange = {} # Store {normalization: range} def getColormap(self): """Return the used colormap""" @@ -480,6 +499,70 @@ class ColormapMixIn(ItemMixInBase): """Handle updates of the colormap""" self._updated(ItemChangedType.COLORMAP) + def _setColormappedData(self, data, copy=True, + min_=None, minPositive=None, max_=None): + """Set the data used to compute the colormapped display. + + It also resets the cache of data ranges. + + This method MUST be called by inheriting classes when data is updated. + + :param Union[None,numpy.ndarray] data: + :param Union[None,float] min_: Minimum value of the data + :param Union[None,float] minPositive: + Minimum of strictly positive values of the data + :param Union[None,float] max_: Maximum value of the data + """ + self.__data = None if data is None else numpy.array(data, copy=copy) + self.__cacheColormapRange = {} # Reset cache + + # Fill-up colormap range cache if values are provided + if max_ is not None and numpy.isfinite(max_): + if min_ is not None and numpy.isfinite(min_): + self.__cacheColormapRange[Colormap.LINEAR, Colormap.MINMAX] = min_, max_ + if minPositive is not None and numpy.isfinite(minPositive): + self.__cacheColormapRange[Colormap.LOGARITHM, Colormap.MINMAX] = minPositive, max_ + + colormap = self.getColormap() + if None in (colormap.getVMin(), colormap.getVMax()): + self._colormapChanged() + + def getColormappedData(self, copy=True): + """Returns the data used to compute the displayed colors + + :param bool copy: True to get a copy, + False to get internal data (do not modify!). + :rtype: Union[None,numpy.ndarray] + """ + if self.__data is None: + return None + else: + return numpy.array(self.__data, copy=copy) + + def _getColormapAutoscaleRange(self, colormap=None): + """Returns the autoscale range for current data and colormap. + + :param Union[None,~silx.gui.colors.Colormap] colormap: + The colormap for which to compute the autoscale range. + If None, the default, the colormap of the item is used + :return: (vmin, vmax) range (vmin and /or vmax might be `None`) + """ + if colormap is None: + colormap = self.getColormap() + + data = self.getColormappedData(copy=False) + if colormap is None or data is None: + return None, None + + normalization = colormap.getNormalization() + autoscaleMode = colormap.getAutoscaleMode() + key = normalization, autoscaleMode + vRange = self.__cacheColormapRange.get(key, None) + if vRange is None: + vRange = colormap._computeAutoscaleRange(data) + self.__cacheColormapRange[key] = vRange + return vRange + class SymbolMixIn(ItemMixInBase): """Mix-in class for items with symbol type""" @@ -712,6 +795,8 @@ class ColorMixIn(ItemMixInBase): """ if isinstance(color, six.string_types): color = colors.rgba(color) + elif isinstance(color, qt.QColor): + color = colors.rgba(color) else: color = numpy.array(color, copy=copy) # TODO more checks + improve color array support @@ -941,6 +1026,10 @@ class ScatterVisualizationMixIn(ItemMixInBase): (either all lines from left to right or all from right to left). """ + BINNED_STATISTIC = 'binned_statistic' + """Display scatter plot as 2D binned statistic (i.e., generalized histogram). + """ + @enum.unique class VisualizationParameter(_Enum): """Different parameter names for scatter plot visualizations""" @@ -967,10 +1056,30 @@ class ScatterVisualizationMixIn(ItemMixInBase): in which case the grid is not fully filled. """ + BINNED_STATISTIC_SHAPE = 'binned_statistic_shape' + """The number of bins in each dimension (height, width). + """ + + BINNED_STATISTIC_FUNCTION = 'binned_statistic_function' + """The reduction function to apply to each bin (str). + + Available reduction functions are: 'mean' (default), 'count', 'sum'. + """ + + _SUPPORTED_VISUALIZATION_PARAMETER_VALUES = { + VisualizationParameter.GRID_MAJOR_ORDER: ('row', 'column'), + VisualizationParameter.BINNED_STATISTIC_FUNCTION: ('mean', 'count', 'sum'), + } + """Supported visualization parameter values. + + Defined for parameters with a set of acceptable values. + """ + def __init__(self): self.__visualization = self.Visualization.POINTS self.__parameters = dict( # Init parameters to None (parameter, None) for parameter in self.VisualizationParameter) + self.__parameters[self.VisualizationParameter.BINNED_STATISTIC_FUNCTION] = 'mean' @classmethod def supportedVisualizations(cls): @@ -985,6 +1094,20 @@ class ScatterVisualizationMixIn(ItemMixInBase): else: return cls._SUPPORTED_SCATTER_VISUALIZATION + @classmethod + def supportedVisualizationParameterValues(cls, parameter): + """Returns the list of supported scatter visualization modes. + + See :meth:`VisualizationParameters` + + :param VisualizationParameter parameter: + This parameter for which to retrieve the supported values. + :returns: tuple of supported of values or None if not defined. + """ + parameter = cls.VisualizationParameter(parameter) + return cls._SUPPORTED_VISUALIZATION_PARAMETER_VALUES.get( + parameter, None) + def setVisualization(self, mode): """Set the scatter plot visualization mode to use. @@ -1024,10 +1147,15 @@ class ScatterVisualizationMixIn(ItemMixInBase): :raises ValueError: If parameter is not supported :return: True if parameter was set, False if is was already set :rtype: bool + :raise ValueError: If value is not supported """ parameter = self.VisualizationParameter.from_value(parameter) if self.__parameters[parameter] != value: + validValues = self.supportedVisualizationParameterValues(parameter) + if validValues is not None and value not in validValues: + raise ValueError("Unsupported parameter value: %s" % str(value)) + self.__parameters[parameter] = value self._updated(ItemChangedType.VISUALIZATION_MODE) return True @@ -1151,14 +1279,12 @@ class PointsBase(Item, SymbolMixIn, AlphaMixIn): if xPositive: x = self.getXData(copy=False) - with warnings.catch_warnings(): # Ignore NaN warnings - warnings.simplefilter('ignore', category=RuntimeWarning) + with numpy.errstate(invalid='ignore'): # Ignore NaN warnings xclipped = x <= 0 if yPositive: y = self.getYData(copy=False) - with warnings.catch_warnings(): # Ignore NaN warnings - warnings.simplefilter('ignore', category=RuntimeWarning) + with numpy.errstate(invalid='ignore'): # Ignore NaN warnings yclipped = y <= 0 self._clippedCache[(xPositive, yPositive)] = \ @@ -1386,3 +1512,54 @@ class BaselineMixIn(object): return numpy.array(self._baseline, copy=True) else: return self._baseline + + +class _Style: + """Object which store styles""" + + +class HighlightedMixIn(ItemMixInBase): + + def __init__(self): + self._highlightStyle = self._DEFAULT_HIGHLIGHT_STYLE + self._highlighted = False + + def isHighlighted(self): + """Returns True if curve is highlighted. + + :rtype: bool + """ + return self._highlighted + + def setHighlighted(self, highlighted): + """Set the highlight state of the curve + + :param bool highlighted: + """ + highlighted = bool(highlighted) + if highlighted != self._highlighted: + self._highlighted = highlighted + # TODO inefficient: better to use backend's setCurveColor + self._updated(ItemChangedType.HIGHLIGHTED) + + def getHighlightedStyle(self): + """Returns the highlighted style in use + + :rtype: CurveStyle + """ + return self._highlightStyle + + def setHighlightedStyle(self, style): + """Set the style to use for highlighting + + :param CurveStyle style: New style to use + """ + previous = self.getHighlightedStyle() + if style != previous: + assert isinstance(style, _Style) + self._highlightStyle = style + self._updated(ItemChangedType.HIGHLIGHTED_STYLE) + + # Backward compatibility event + if previous.getColor() != style.getColor(): + self._updated(ItemChangedType.HIGHLIGHTED_COLOR) |