summaryrefslogtreecommitdiff
path: root/silx/gui/plot/items/core.py
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/items/core.py')
-rw-r--r--silx/gui/plot/items/core.py223
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)