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.py182
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)