diff options
Diffstat (limited to 'silx/gui/plot/items/core.py')
-rw-r--r-- | silx/gui/plot/items/core.py | 182 |
1 files changed, 141 insertions, 41 deletions
diff --git a/silx/gui/plot/items/core.py b/silx/gui/plot/items/core.py index 72bfd9a..0f4ffb9 100644 --- a/silx/gui/plot/items/core.py +++ b/silx/gui/plot/items/core.py @@ -27,22 +27,96 @@ __authors__ = ["T. Vincent"] __license__ = "MIT" -__date__ = "26/04/2017" +__date__ = "27/06/2017" +import collections from copy import deepcopy import logging import weakref import numpy -from silx.third_party import six +from silx.third_party import six, enum +from ... import qt from .. import Colors - +from ..Colormap import Colormap _logger = logging.getLogger(__name__) -class Item(object): +@enum.unique +class ItemChangedType(enum.Enum): + """Type of modification provided by :attr:`Item.sigItemChanged` signal.""" + # Private setters and setInfo are not emitting sigItemChanged signal. + # Signals to consider: + # COLORMAP_SET emitted when setColormap is called but not forward colormap object signal + # CURRENT_COLOR_CHANGED emitted current color changed because highlight changed, + # highlighted color changed or color changed depending on hightlight state. + + VISIBLE = 'visibleChanged' + """Item's visibility changed flag.""" + + ZVALUE = 'zValueChanged' + """Item's Z value changed flag.""" + + COLORMAP = 'colormapChanged' # Emitted when set + forward events from the colormap object + """Item's colormap changed flag. + + This is emitted both when setting a new colormap and + when the current colormap object is updated. + """ + + SYMBOL = 'symbolChanged' + """Item's symbol changed flag.""" + + SYMBOL_SIZE = 'symbolSizeChanged' + """Item's symbol size changed flag.""" + + LINE_WIDTH = 'lineWidthChanged' + """Item's line width changed flag.""" + + LINE_STYLE = 'lineStyleChanged' + """Item's line style changed flag.""" + + COLOR = 'colorChanged' + """Item's color changed flag.""" + + YAXIS = 'yAxisChanged' + """Item's Y axis binding changed flag.""" + + FILL = 'fillChanged' + """Item's fill changed flag.""" + + ALPHA = 'alphaChanged' + """Item's transparency alpha changed flag.""" + + DATA = 'dataChanged' + """Item's data changed flag""" + + HIGHLIGHTED = 'highlightedChanged' + """Item's highlight state changed flag.""" + + HIGHLIGHTED_COLOR = 'highlightedColorChanged' + """Item's highlighted color changed flag.""" + + SCALE = 'scaleChanged' + """Item's scale changed flag.""" + + TEXT = 'textChanged' + """Item's text changed flag.""" + + POSITION = 'positionChanged' + """Item's position changed flag. + + This is emitted when a marker position changed and + when an image origin changed. + """ + + OVERLAY = 'overlayChanged' + """Item's overlay state changed flag.""" + + +class Item(qt.QObject): """Description of an item of the plot""" _DEFAULT_Z_LAYER = 0 @@ -54,7 +128,15 @@ class Item(object): _DEFAULT_SELECTABLE = False """Default selectable state of items""" + sigItemChanged = qt.Signal(object) + """Signal emitted when the item has changed. + + It provides a flag describing which property of the item has changed. + See :class:`ItemChangedType` for flags description. + """ + def __init__(self): + super(Item, self).__init__() self._dirty = True self._plotRef = None self._visible = True @@ -114,7 +196,8 @@ class Item(object): if visible != self._visible: self._visible = visible # When visibility has changed, always mark as dirty - self._updated(checkVisibility=False) + self._updated(ItemChangedType.VISIBLE, + checkVisibility=False) def isOverlay(self): """Return true if item is drawn as an overlay. @@ -158,7 +241,7 @@ class Item(object): z = int(z) if z is not None else self._DEFAULT_Z_LAYER if z != self._z: self._z = z - self._updated() + self._updated(ItemChangedType.ZVALUE) def getInfo(self, copy=True): """Returns the info associated to this item @@ -172,11 +255,12 @@ class Item(object): info = deepcopy(info) self._info = info - def _updated(self, checkVisibility=True): + def _updated(self, event=None, checkVisibility=True): """Mark the item as dirty (i.e., needing update). This also triggers Plot.replot. + :param event: The event to send to :attr:`sigItemChanged` signal. :param bool checkVisibility: True to only mark as dirty if visible, False to always mark as dirty. """ @@ -187,6 +271,8 @@ class Item(object): plot = self.getPlot() if plot is not None: plot._itemRequiresUpdate(self) + if event is not None: + self.sigItemChanged.emit(event) def _update(self, backend): """Called by Plot to update the backend for this item. @@ -292,25 +378,32 @@ class DraggableMixIn(object): class ColormapMixIn(object): """Mix-in class for items with colormap""" - _DEFAULT_COLORMAP = {'name': 'gray', 'normalization': 'linear', - 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0} - """Default colormap of the item""" - def __init__(self): - self._colormap = self._DEFAULT_COLORMAP + self._colormap = Colormap() + self._colormap.sigChanged.connect(self._colormapChanged) def getColormap(self): """Return the used colormap""" - return self._colormap.copy() + return self._colormap def setColormap(self, colormap): """Set the colormap of this image - :param dict colormap: colormap description + :param Colormap colormap: colormap description """ - self._colormap = colormap.copy() - # TODO colormap comparison + colormap object and events on modification - self._updated() + if isinstance(colormap, dict): + colormap = Colormap._fromDict(colormap) + + if self._colormap is not None: + self._colormap.sigChanged.disconnect(self._colormapChanged) + self._colormap = colormap + if self._colormap is not None: + self._colormap.sigChanged.connect(self._colormapChanged) + self._colormapChanged() + + def _colormapChanged(self): + """Handle updates of the colormap""" + self._updated(ItemChangedType.COLORMAP) class SymbolMixIn(object): @@ -355,7 +448,7 @@ class SymbolMixIn(object): symbol = self._DEFAULT_SYMBOL if symbol != self._symbol: self._symbol = symbol - self._updated() + self._updated(ItemChangedType.SYMBOL) def getSymbolSize(self): """Return the point marker size in points. @@ -375,7 +468,7 @@ class SymbolMixIn(object): size = self._DEFAULT_SYMBOL_SIZE if size != self._symbol_size: self._symbol_size = size - self._updated() + self._updated(ItemChangedType.SYMBOL_SIZE) class LineMixIn(object): @@ -405,7 +498,7 @@ class LineMixIn(object): width = float(width) if width != self._linewidth: self._linewidth = width - self._updated() + self._updated(ItemChangedType.LINE_WIDTH) def getLineStyle(self): """Return the type of the line @@ -435,7 +528,7 @@ class LineMixIn(object): style = self._DEFAULT_LINESTYLE if style != self._linestyle: self._linestyle = style - self._updated() + self._updated(ItemChangedType.LINE_STYLE) class ColorMixIn(object): @@ -473,8 +566,9 @@ class ColorMixIn(object): else: # Array of colors assert color.ndim == 2 - self._color = color - self._updated() + if self._color != color: + self._color = color + self._updated(ItemChangedType.COLOR) class YAxisMixIn(object): @@ -504,7 +598,7 @@ class YAxisMixIn(object): assert yaxis in ('left', 'right') if yaxis != self._yaxis: self._yaxis = yaxis - self._updated() + self._updated(ItemChangedType.YAXIS) class FillMixIn(object): @@ -528,7 +622,7 @@ class FillMixIn(object): fill = bool(fill) if fill != self._fill: self._fill = fill - self._updated() + self._updated(ItemChangedType.FILL) class AlphaMixIn(object): @@ -561,7 +655,7 @@ class AlphaMixIn(object): alpha = max(0., min(alpha, 1.)) # Clip alpha to [0., 1.] range if alpha != self._alpha: self._alpha = alpha - self._updated() + self._updated(ItemChangedType.ALPHA) class Points(Item, SymbolMixIn, AlphaMixIn): @@ -690,8 +784,8 @@ class Points(Item, SymbolMixIn, AlphaMixIn): plot = self.getPlot() if plot is not None: - xPositive = plot.isXAxisLogarithmic() - yPositive = plot.isYAxisLogarithmic() + xPositive = plot.getXAxis()._isLogarithmic() + yPositive = plot.getYAxis()._isLogarithmic() else: xPositive = False yPositive = False @@ -724,8 +818,8 @@ class Points(Item, SymbolMixIn, AlphaMixIn): Return None if caching is not applicable.""" plot = self.getPlot() if plot is not None: - xPositive = plot.isXAxisLogarithmic() - yPositive = plot.isYAxisLogarithmic() + xPositive = plot.getXAxis()._isLogarithmic() + yPositive = plot.getYAxis()._isLogarithmic() if xPositive or yPositive: # At least one axis has log scale, filter data if (xPositive, yPositive) not in self._filteredCache: @@ -779,24 +873,24 @@ class Points(Item, SymbolMixIn, AlphaMixIn): :param copy: True (Default) to get a copy, False to use internal representation (do not modify!) - :rtype: numpy.ndarray or None + :rtype: numpy.ndarray, float or None """ - if self._xerror is None: - return None - else: + if isinstance(self._xerror, numpy.ndarray): return numpy.array(self._xerror, copy=copy) + else: + return self._xerror # float or None def getYErrorData(self, copy=True): """Returns the y error of the points :param copy: True (Default) to get a copy, False to use internal representation (do not modify!) - :rtype: numpy.ndarray or None + :rtype: numpy.ndarray, float or None """ - if self._yerror is None: - return None - else: + if isinstance(self._yerror, numpy.ndarray): return numpy.array(self._yerror, copy=copy) + else: + return self._yerror # float or None def setData(self, x, y, xerror=None, yerror=None, copy=True): """Set the data of the curve. @@ -820,9 +914,15 @@ class Points(Item, SymbolMixIn, AlphaMixIn): assert x.ndim == y.ndim == 1 if xerror is not None: - xerror = numpy.array(xerror, copy=copy) + if isinstance(xerror, collections.Iterable): + xerror = numpy.array(xerror, copy=copy) + else: + xerror = float(xerror) if yerror is not None: - yerror = numpy.array(yerror, copy=copy) + if isinstance(yerror, collections.Iterable): + yerror = numpy.array(yerror, copy=copy) + else: + yerror = float(yerror) # TODO checks on xerror, yerror self._x, self._y = x, y self._xerror, self._yerror = xerror, yerror @@ -831,9 +931,9 @@ class Points(Item, SymbolMixIn, AlphaMixIn): self._filteredCache = {} # Reset cached filtered data self._clippedCache = {} # Reset cached clipped bool array - self._updated() # TODO hackish data range implementation if self.isVisible(): plot = self.getPlot() if plot is not None: plot._invalidateDataRange() + self._updated(ItemChangedType.DATA) |