summaryrefslogtreecommitdiff
path: root/silx/gui/plot/items
diff options
context:
space:
mode:
Diffstat (limited to 'silx/gui/plot/items')
-rw-r--r--silx/gui/plot/items/__init__.py13
-rw-r--r--silx/gui/plot/items/axis.py477
-rw-r--r--silx/gui/plot/items/core.py182
-rw-r--r--silx/gui/plot/items/curve.py18
-rw-r--r--silx/gui/plot/items/histogram.py36
-rw-r--r--silx/gui/plot/items/image.py75
-rw-r--r--silx/gui/plot/items/marker.py11
-rw-r--r--silx/gui/plot/items/scatter.py10
-rw-r--r--silx/gui/plot/items/shape.py14
9 files changed, 725 insertions, 111 deletions
diff --git a/silx/gui/plot/items/__init__.py b/silx/gui/plot/items/__init__.py
index b16fe40..bf39c87 100644
--- a/silx/gui/plot/items/__init__.py
+++ b/silx/gui/plot/items/__init__.py
@@ -22,22 +22,23 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""This package provides classes that describes :class:`.Plot` content.
+"""This package provides classes that describes :class:`.PlotWidget` content.
-Instances of those classes are returned by :class:`.Plot` methods that give
-access to its content such as :meth:`.Plot.getCurve`, :meth:`.Plot.getImage`.
+Instances of those classes are returned by :class:`.PlotWidget` methods that give
+access to its content such as :meth:`.PlotWidget.getCurve`, :meth:`.PlotWidget.getImage`.
"""
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "06/03/2017"
+__date__ = "22/06/2017"
from .core import (Item, LabelsMixIn, DraggableMixIn, ColormapMixIn, # noqa
SymbolMixIn, ColorMixIn, YAxisMixIn, FillMixIn, # noqa
- AlphaMixIn, LineMixIn) # noqa
+ AlphaMixIn, LineMixIn, ItemChangedType) # noqa
from .curve import Curve # noqa
from .histogram import Histogram # noqa
-from .image import ImageBase, ImageData, ImageRgba # noqa
+from .image import ImageBase, ImageData, ImageRgba, MaskImageData # noqa
from .shape import Shape # noqa
from .scatter import Scatter # noqa
from .marker import Marker, XMarker, YMarker # noqa
+from .axis import Axis, XAxis, YAxis, YRightAxis
diff --git a/silx/gui/plot/items/axis.py b/silx/gui/plot/items/axis.py
new file mode 100644
index 0000000..56fd762
--- /dev/null
+++ b/silx/gui/plot/items/axis.py
@@ -0,0 +1,477 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2017 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
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""This module provides the class for axes of the :class:`PlotWidget`.
+"""
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "30/08/2017"
+
+import logging
+from ... import qt
+
+_logger = logging.getLogger(__name__)
+
+
+class Axis(qt.QObject):
+ """This class describes and controls a plot axis.
+
+ Note: This is an abstract class.
+ """
+ # States are half-stored on the backend of the plot, and half-stored on this
+ # object.
+ # TODO It would be good to store all the states of an axis in this object.
+ # i.e. vmin and vmax
+
+ LINEAR = "linear"
+ """Constant defining a linear scale"""
+
+ LOGARITHMIC = "log"
+ """Constant defining a logarithmic scale"""
+
+ _SCALES = set([LINEAR, LOGARITHMIC])
+
+ sigInvertedChanged = qt.Signal(bool)
+ """Signal emitted when axis orientation has changed"""
+
+ sigScaleChanged = qt.Signal(str)
+ """Signal emitted when axis scale has changed"""
+
+ _sigLogarithmicChanged = qt.Signal(bool)
+ """Signal emitted when axis scale has changed to or from logarithmic"""
+
+ sigAutoScaleChanged = qt.Signal(bool)
+ """Signal emitted when axis autoscale has changed"""
+
+ sigLimitsChanged = qt.Signal(float, float)
+ """Signal emitted when axis autoscale has changed"""
+
+ def __init__(self, plot):
+ """Constructor
+
+ :param silx.gui.plot.PlotWidget.PlotWidget plot: Parent plot of this
+ axis
+ """
+ qt.QObject.__init__(self, parent=plot)
+ self._scale = self.LINEAR
+ self._isAutoScale = True
+ # Store default labels provided to setGraph[X|Y]Label
+ self._defaultLabel = ''
+ # Store currently displayed labels
+ # Current label can differ from input one with active curve handling
+ self._currentLabel = ''
+ self._plot = plot
+
+ def getLimits(self):
+ """Get the limits of this axis.
+
+ :return: Minimum and maximum values of this axis as tuple
+ """
+ return self._internalGetLimits()
+
+ def setLimits(self, vmin, vmax):
+ """Set this axis limits.
+
+ :param float vmin: minimum axis value
+ :param float vmax: maximum axis value
+ """
+ vmin, vmax = self._checkLimits(vmin, vmax)
+ if self.getLimits() == (vmin, vmax):
+ return
+
+ self._internalSetLimits(vmin, vmax)
+ self._plot._setDirtyPlot()
+
+ self._emitLimitsChanged()
+
+ def _emitLimitsChanged(self):
+ """Emit axis sigLimitsChanged and PlotWidget limitsChanged event"""
+ vmin, vmax = self.getLimits()
+ self.sigLimitsChanged.emit(vmin, vmax)
+ self._plot._notifyLimitsChanged(emitSignal=False)
+
+ def _checkLimits(self, vmin, vmax):
+ """Makes sure axis range is not empty
+
+ :param float vmin: Min axis value
+ :param float vmax: Max axis value
+ :return: (min, max) making sure min < max
+ :rtype: 2-tuple of float
+ """
+ if vmax < vmin:
+ _logger.debug('%s axis: max < min, inverting limits.', self._defaultLabel)
+ vmin, vmax = vmax, vmin
+ elif vmax == vmin:
+ _logger.debug('%s axis: max == min, expanding limits.', self._defaultLabel)
+ if vmin == 0.:
+ vmin, vmax = -0.1, 0.1
+ elif vmin < 0:
+ vmin, vmax = vmin * 1.1, vmin * 0.9
+ else: # xmin > 0
+ vmin, vmax = vmin * 0.9, vmin * 1.1
+
+ return vmin, vmax
+
+ def isInverted(self):
+ """Return True if the axis is inverted (top to bottom for the y-axis),
+ False otherwise. It is always False for the X axis.
+
+ :rtype: bool
+ """
+ return False
+
+ def setInverted(self, isInverted):
+ """Set the axis orientation.
+
+ This is only available for the Y axis.
+
+ :param bool flag: True for Y axis going from top to bottom,
+ False for Y axis going from bottom to top
+ """
+ if isInverted == self.isInverted():
+ return
+ raise NotImplementedError()
+
+ def getLabel(self):
+ """Return the current displayed label of this axis.
+
+ :param str axis: The Y axis for which to get the label (left or right)
+ :rtype: str
+ """
+ return self._currentLabel
+
+ def setLabel(self, label):
+ """Set the label displayed on the plot for this axis.
+
+ The provided label can be temporarily replaced by the label of the
+ active curve if any.
+
+ :param str label: The axis label
+ """
+ self._defaultLabel = label
+ self._setCurrentLabel(label)
+ self._plot._setDirtyPlot()
+
+ def _setCurrentLabel(self, label):
+ """Define the label currently displayed.
+
+ If the label is None or empty the default label is used.
+
+ :param str label: Currently displayed label
+ """
+ if label is None or label == '':
+ label = self._defaultLabel
+ if label is None:
+ label = ''
+ self._currentLabel = label
+ self._internalSetCurrentLabel(label)
+
+ def getScale(self):
+ """Return the name of the scale used by this axis.
+
+ :rtype: str
+ """
+ return self._scale
+
+ def setScale(self, scale):
+ """Set the scale to be used by this axis.
+
+ :param str scale: Name of the scale ("log", or "linear")
+ """
+ assert(scale in self._SCALES)
+ if self._scale == scale:
+ return
+
+ # For the backward compatibility signal
+ emitLog = self._scale == self.LOGARITHMIC or scale == self.LOGARITHMIC
+
+ if scale == self.LOGARITHMIC:
+ self._internalSetLogarithmic(True)
+ elif scale == self.LINEAR:
+ self._internalSetLogarithmic(False)
+ else:
+ raise ValueError("Scale %s unsupported" % scale)
+
+ self._scale = scale
+
+ # TODO hackish way of forcing update of curves and images
+ for item in self._plot._getItems(withhidden=True):
+ item._updated()
+ self._plot._invalidateDataRange()
+ self._plot.resetZoom()
+
+ self.sigScaleChanged.emit(self._scale)
+ if emitLog:
+ self._sigLogarithmicChanged.emit(self._scale == self.LOGARITHMIC)
+
+ def _isLogarithmic(self):
+ """Return True if this axis scale is logarithmic, False if linear.
+
+ :rtype: bool
+ """
+ return self._scale == self.LOGARITHMIC
+
+ def _setLogarithmic(self, flag):
+ """Set the scale of this axes (either linear or logarithmic).
+
+ :param bool flag: True to use a logarithmic scale, False for linear.
+ """
+ flag = bool(flag)
+ self.setScale(self.LOGARITHMIC if flag else self.LINEAR)
+
+ def isAutoScale(self):
+ """Return True if axis is automatically adjusting its limits.
+
+ :rtype: bool
+ """
+ return self._isAutoScale
+
+ def setAutoScale(self, flag=True):
+ """Set the axis limits adjusting behavior of :meth:`resetZoom`.
+
+ :param bool flag: True to resize limits automatically,
+ False to disable it.
+ """
+ self._isAutoScale = bool(flag)
+ self.sigAutoScaleChanged.emit(self._isAutoScale)
+
+ def _setLimitsConstraints(self, minPos=None, maxPos=None):
+ raise NotImplementedError()
+
+ def setLimitsConstraints(self, minPos=None, maxPos=None):
+ """
+ Set a constaints on the position of the axes.
+
+ :param float minPos: Minimum allowed axis value.
+ :param float maxPos: Maximum allowed axis value.
+ :return: True if the constaints was updated
+ :rtype: bool
+ """
+ updated = self._setLimitsConstraints(minPos, maxPos)
+ if updated:
+ plot = self._plot
+ xMin, xMax = plot.getXAxis().getLimits()
+ yMin, yMax = plot.getYAxis().getLimits()
+ y2Min, y2Max = plot.getYAxis('right').getLimits()
+ plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
+ return updated
+
+ def _setRangeConstraints(self, minRange=None, maxRange=None):
+ raise NotImplementedError()
+
+ def setRangeConstraints(self, minRange=None, maxRange=None):
+ """
+ Set a constaints on the position of the axes.
+
+ :param float minRange: Minimum allowed left-to-right span across the
+ view
+ :param float maxRange: Maximum allowed left-to-right span across the
+ view
+ :return: True if the constaints was updated
+ :rtype: bool
+ """
+ updated = self._setRangeConstraints(minRange, maxRange)
+ if updated:
+ plot = self._plot
+ xMin, xMax = plot.getXAxis().getLimits()
+ yMin, yMax = plot.getYAxis().getLimits()
+ y2Min, y2Max = plot.getYAxis('right').getLimits()
+ plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
+ return updated
+
+
+class XAxis(Axis):
+ """Axis class defining primitives for the X axis"""
+
+ # TODO With some changes on the backend, it will be able to remove all this
+ # specialised implementations (prefixel by '_internal')
+
+ def _internalSetCurrentLabel(self, label):
+ self._plot._backend.setGraphXLabel(label)
+
+ def _internalGetLimits(self):
+ return self._plot._backend.getGraphXLimits()
+
+ def _internalSetLimits(self, xmin, xmax):
+ self._plot._backend.setGraphXLimits(xmin, xmax)
+
+ def _internalSetLogarithmic(self, flag):
+ self._plot._backend.setXAxisLogarithmic(flag)
+
+ def _setLimitsConstraints(self, minPos=None, maxPos=None):
+ constrains = self._plot._getViewConstraints()
+ updated = constrains.update(xMin=minPos, xMax=maxPos)
+ return updated
+
+ def _setRangeConstraints(self, minRange=None, maxRange=None):
+ constrains = self._plot._getViewConstraints()
+ updated = constrains.update(minXRange=minRange, maxXRange=maxRange)
+ return updated
+
+
+class YAxis(Axis):
+ """Axis class defining primitives for the Y axis"""
+
+ # TODO With some changes on the backend, it will be able to remove all this
+ # specialised implementations (prefixel by '_internal')
+
+ def _internalSetCurrentLabel(self, label):
+ self._plot._backend.setGraphYLabel(label, axis='left')
+
+ def _internalGetLimits(self):
+ return self._plot._backend.getGraphYLimits(axis='left')
+
+ def _internalSetLimits(self, ymin, ymax):
+ self._plot._backend.setGraphYLimits(ymin, ymax, axis='left')
+
+ def _internalSetLogarithmic(self, flag):
+ self._plot._backend.setYAxisLogarithmic(flag)
+
+ def setInverted(self, flag=True):
+ """Set the axis orientation.
+
+ This is only available for the Y axis.
+
+ :param bool flag: True for Y axis going from top to bottom,
+ False for Y axis going from bottom to top
+ """
+ flag = bool(flag)
+ self._plot._backend.setYAxisInverted(flag)
+ self._plot._setDirtyPlot()
+ self.sigInvertedChanged.emit(flag)
+
+ def isInverted(self):
+ """Return True if the axis is inverted (top to bottom for the y-axis),
+ False otherwise. It is always False for the X axis.
+
+ :rtype: bool
+ """
+ return self._plot._backend.isYAxisInverted()
+
+ def _setLimitsConstraints(self, minPos=None, maxPos=None):
+ constrains = self._plot._getViewConstraints()
+ updated = constrains.update(yMin=minPos, yMax=maxPos)
+ return updated
+
+ def _setRangeConstraints(self, minRange=None, maxRange=None):
+ constrains = self._plot._getViewConstraints()
+ updated = constrains.update(minYRange=minRange, maxYRange=maxRange)
+ return updated
+
+
+class YRightAxis(Axis):
+ """Proxy axis for the secondary Y axes. It manages it own label and limit
+ but share the some state like scale and direction with the main axis."""
+
+ # TODO With some changes on the backend, it will be able to remove all this
+ # specialised implementations (prefixel by '_internal')
+
+ def __init__(self, plot, mainAxis):
+ """Constructor
+
+ :param silx.gui.plot.PlotWidget.PlotWidget plot: Parent plot of this
+ axis
+ :param Axis mainAxis: Axis which sharing state with this axis
+ """
+ Axis.__init__(self, plot)
+ self.__mainAxis = mainAxis
+
+ @property
+ def sigInvertedChanged(self):
+ """Signal emitted when axis orientation has changed"""
+ return self.__mainAxis.sigInvertedChanged
+
+ @property
+ def sigScaleChanged(self):
+ """Signal emitted when axis scale has changed"""
+ return self.__mainAxis.sigScaleChanged
+
+ @property
+ def _sigLogarithmicChanged(self):
+ """Signal emitted when axis scale has changed to or from logarithmic"""
+ return self.__mainAxis._sigLogarithmicChanged
+
+ @property
+ def sigAutoScaleChanged(self):
+ """Signal emitted when axis autoscale has changed"""
+ return self.__mainAxis.sigAutoScaleChanged
+
+ def _internalSetCurrentLabel(self, label):
+ self._plot._backend.setGraphYLabel(label, axis='right')
+
+ def _internalGetLimits(self):
+ return self._plot._backend.getGraphYLimits(axis='right')
+
+ def _internalSetLimits(self, ymin, ymax):
+ self._plot._backend.setGraphYLimits(ymin, ymax, axis='right')
+
+ def setInverted(self, flag=True):
+ """Set the Y axis orientation.
+
+ :param bool flag: True for Y axis going from top to bottom,
+ False for Y axis going from bottom to top
+ """
+ return self.__mainAxis.setInverted(flag)
+
+ def isInverted(self):
+ """Return True if Y axis goes from top to bottom, False otherwise."""
+ return self.__mainAxis.isInverted()
+
+ def getScale(self):
+ """Return the name of the scale used by this axis.
+
+ :rtype: str
+ """
+ return self.__mainAxis.getScale()
+
+ def setScale(self, scale):
+ """Set the scale to be used by this axis.
+
+ :param str scale: Name of the scale ("log", or "linear")
+ """
+ self.__mainAxis.setScale(scale)
+
+ def _isLogarithmic(self):
+ """Return True if Y axis scale is logarithmic, False if linear."""
+ return self.__mainAxis._isLogarithmic()
+
+ def _setLogarithmic(self, flag):
+ """Set the Y axes scale (either linear or logarithmic).
+
+ :param bool flag: True to use a logarithmic scale, False for linear.
+ """
+ return self.__mainAxis._setLogarithmic(flag)
+
+ def isAutoScale(self):
+ """Return True if Y axes are automatically adjusting its limits."""
+ return self.__mainAxis.isAutoScale()
+
+ def setAutoScale(self, flag=True):
+ """Set the Y axis limits adjusting behavior of :meth:`PlotWidget.resetZoom`.
+
+ :param bool flag: True to resize limits automatically,
+ False to disable it.
+ """
+ return self.__mainAxis.setAutoScale(flag)
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)
diff --git a/silx/gui/plot/items/curve.py b/silx/gui/plot/items/curve.py
index d25ae00..ce7f03e 100644
--- a/silx/gui/plot/items/curve.py
+++ b/silx/gui/plot/items/curve.py
@@ -32,11 +32,9 @@ __date__ = "06/03/2017"
import logging
-import numpy
-
from .. import Colors
-from .core import (Points, LabelsMixIn, SymbolMixIn,
- ColorMixIn, YAxisMixIn, FillMixIn, LineMixIn)
+from .core import (Points, LabelsMixIn, ColorMixIn, YAxisMixIn,
+ FillMixIn, LineMixIn, ItemChangedType)
_logger = logging.getLogger(__name__)
@@ -132,15 +130,15 @@ class Curve(Points, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn, LineMixIn):
:param bool visible: True to display it, False otherwise
"""
- visibleChanged = self.isVisible() != bool(visible)
- super(Curve, self).setVisible(visible)
-
+ visible = bool(visible)
# TODO hackish data range implementation
- if visibleChanged:
+ if self.isVisible() != visible:
plot = self.getPlot()
if plot is not None:
plot._invalidateDataRange()
+ super(Curve, self).setVisible(visible)
+
def isHighlighted(self):
"""Returns True if curve is highlighted.
@@ -157,7 +155,7 @@ class Curve(Points, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn, LineMixIn):
if highlighted != self._highlighted:
self._highlighted = highlighted
# TODO inefficient: better to use backend's setCurveColor
- self._updated()
+ self._updated(ItemChangedType.HIGHLIGHTED)
def getHighlightedColor(self):
"""Returns the RGBA highlight color of the item
@@ -176,7 +174,7 @@ class Curve(Points, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn, LineMixIn):
color = Colors.rgba(color)
if color != self._highlightColor:
self._highlightColor = color
- self._updated()
+ self._updated(ItemChangedType.HIGHLIGHTED_COLOR)
def getCurrentColor(self):
"""Returns the current color of the curve.
diff --git a/silx/gui/plot/items/histogram.py b/silx/gui/plot/items/histogram.py
index c3821bc..ad89677 100644
--- a/silx/gui/plot/items/histogram.py
+++ b/silx/gui/plot/items/histogram.py
@@ -27,7 +27,7 @@
__authors__ = ["H. Payno", "T. Vincent"]
__license__ = "MIT"
-__date__ = "02/05/2017"
+__date__ = "27/06/2017"
import logging
@@ -35,7 +35,7 @@ import logging
import numpy
from .core import (Item, AlphaMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn)
+ LineMixIn, YAxisMixIn, ItemChangedType)
_logger = logging.getLogger(__name__)
@@ -139,8 +139,8 @@ class Histogram(Item, AlphaMixIn, ColorMixIn, FillMixIn,
# Filter-out values <= 0
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
@@ -174,8 +174,8 @@ class Histogram(Item, AlphaMixIn, ColorMixIn, FillMixIn,
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
@@ -185,14 +185,19 @@ class Histogram(Item, AlphaMixIn, ColorMixIn, FillMixIn,
if xPositive:
# Replace edges <= 0 by NaN and corresponding values by NaN
- clipped = (edges <= 0)
+ clipped_edges = (edges <= 0)
edges = numpy.array(edges, copy=True, dtype=numpy.float)
- edges[clipped] = numpy.nan
- values[numpy.logical_or(clipped[:-1], clipped[1:])] = numpy.nan
+ edges[clipped_edges] = numpy.nan
+ clipped_values = numpy.logical_or(clipped_edges[:-1],
+ clipped_edges[1:])
+ else:
+ clipped_values = numpy.zeros_like(values, dtype=numpy.bool)
if yPositive:
# Replace values <= 0 by NaN, do not modify edges
- values[values <= 0] = numpy.nan
+ clipped_values = numpy.logical_or(clipped_values, values <= 0)
+
+ values[clipped_values] = numpy.nan
if xPositive or yPositive:
return (numpy.nanmin(edges),
@@ -211,14 +216,13 @@ class Histogram(Item, AlphaMixIn, ColorMixIn, FillMixIn,
:param bool visible: True to display it, False otherwise
"""
- visibleChanged = self.isVisible() != bool(visible)
- super(Histogram, self).setVisible(visible)
-
+ visible = bool(visible)
# TODO hackish data range implementation
- if visibleChanged:
+ if self.isVisible() != visible:
plot = self.getPlot()
if plot is not None:
plot._invalidateDataRange()
+ super(Histogram, self).setVisible(visible)
def getValueData(self, copy=True):
"""The values of the histogram
@@ -248,7 +252,7 @@ class Histogram(Item, AlphaMixIn, ColorMixIn, FillMixIn,
:returns: (N histogram value, N+1 bin edges)
:rtype: 2-tuple of numpy.nadarray
"""
- return (self.getValueData(copy), self.getBinEdgesData(copy))
+ return self.getValueData(copy), self.getBinEdgesData(copy)
def setData(self, histogram, edges, align='center', copy=True):
"""Set the histogram values and bin edges.
@@ -286,3 +290,5 @@ class Histogram(Item, AlphaMixIn, ColorMixIn, FillMixIn,
self._histogram = histogram
self._edges = edges
+
+ self._updated(ItemChangedType.DATA)
diff --git a/silx/gui/plot/items/image.py b/silx/gui/plot/items/image.py
index 7e1dd8b..acf7bf6 100644
--- a/silx/gui/plot/items/image.py
+++ b/silx/gui/plot/items/image.py
@@ -28,7 +28,7 @@ of the :class:`Plot`.
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "06/03/2017"
+__date__ = "27/06/2017"
from collections import Sequence
@@ -36,7 +36,8 @@ import logging
import numpy
-from .core import Item, LabelsMixIn, DraggableMixIn, ColormapMixIn, AlphaMixIn
+from .core import (Item, LabelsMixIn, DraggableMixIn, ColormapMixIn,
+ AlphaMixIn, ItemChangedType)
from ..Colors import applyColormapToData
@@ -130,14 +131,23 @@ class ImageBase(Item, LabelsMixIn, DraggableMixIn, AlphaMixIn):
:param bool visible: True to display it, False otherwise
"""
- visibleChanged = self.isVisible() != bool(visible)
- super(ImageBase, self).setVisible(visible)
-
+ visible = bool(visible)
# TODO hackish data range implementation
- if visibleChanged:
+ if self.isVisible() != visible:
plot = self.getPlot()
if plot is not None:
plot._invalidateDataRange()
+ super(ImageBase, self).setVisible(visible)
+
+ def _isPlotLinear(self, plot):
+ """Return True if plot only uses linear scale for both of x and y
+ axes."""
+ linear = plot.getXAxis().LINEAR
+ if plot.getXAxis().getScale() != linear:
+ return False
+ if plot.getYAxis().getScale() != linear:
+ return False
+ return True
def _getBounds(self):
if self.getData(copy=False).size == 0: # Empty data
@@ -156,8 +166,7 @@ class ImageBase(Item, LabelsMixIn, DraggableMixIn, AlphaMixIn):
ymin, ymax = ymax, ymin
plot = self.getPlot()
- if (plot is not None and
- plot.isXAxisLogarithmic() or plot.isYAxisLogarithmic()):
+ if plot is not None and not self._isPlotLinear(plot):
return None
else:
return xmin, xmax, ymin, ymax
@@ -197,7 +206,6 @@ class ImageBase(Item, LabelsMixIn, DraggableMixIn, AlphaMixIn):
origin = float(origin), float(origin)
if origin != self._origin:
self._origin = origin
- self._updated()
# TODO hackish data range implementation
if self.isVisible():
@@ -205,6 +213,8 @@ class ImageBase(Item, LabelsMixIn, DraggableMixIn, AlphaMixIn):
if plot is not None:
plot._invalidateDataRange()
+ self._updated(ItemChangedType.POSITION)
+
def getScale(self):
"""Returns the scale of the image in data coordinates.
@@ -222,9 +232,17 @@ class ImageBase(Item, LabelsMixIn, DraggableMixIn, AlphaMixIn):
scale = float(scale[0]), float(scale[1])
else: # single value scale
scale = float(scale), float(scale)
+
if scale != self._scale:
self._scale = scale
- self._updated()
+
+ # TODO hackish data range implementation
+ if self.isVisible():
+ plot = self.getPlot()
+ if plot is not None:
+ plot._invalidateDataRange()
+
+ self._updated(ItemChangedType.SCALE)
class ImageData(ImageBase, ColormapMixIn):
@@ -240,8 +258,9 @@ class ImageData(ImageBase, ColormapMixIn):
"""Update backend renderer"""
plot = self.getPlot()
assert plot is not None
- if plot.isXAxisLogarithmic() or plot.isYAxisLogarithmic():
- return None # Do not render with log scales
+ if not self._isPlotLinear(plot):
+ # Do not render with non linear scales
+ return None
if self.getAlternativeImageData(copy=False) is not None:
dataToUse = self.getAlternativeImageData(copy=False)
@@ -283,8 +302,7 @@ class ImageData(ImageBase, ColormapMixIn):
else:
# Apply colormap, in this case an new array is always returned
colormap = self.getColormap()
- image = applyColormapToData(self.getData(copy=False),
- **colormap)
+ image = colormap.applyToData(self.getData(copy=False))
return image
def getAlternativeImageData(self, copy=True):
@@ -312,6 +330,14 @@ class ImageData(ImageBase, ColormapMixIn):
"""
data = numpy.array(data, copy=copy)
assert data.ndim == 2
+ if data.dtype.kind == 'b':
+ _logger.warning(
+ 'Converting boolean image to int8 to plot it.')
+ data = numpy.array(data, copy=False, dtype=numpy.int8)
+ elif numpy.issubdtype(data.dtype, numpy.complex):
+ _logger.warning(
+ 'Converting complex image to absolute value to plot it.')
+ data = numpy.absolute(data)
self._data = data
if alternative is not None:
@@ -320,7 +346,6 @@ class ImageData(ImageBase, ColormapMixIn):
assert alternative.shape[2] in (3, 4)
assert alternative.shape[:2] == data.shape[:2]
self._alternativeImage = alternative
- self._updated()
# TODO hackish data range implementation
if self.isVisible():
@@ -328,6 +353,8 @@ class ImageData(ImageBase, ColormapMixIn):
if plot is not None:
plot._invalidateDataRange()
+ self._updated(ItemChangedType.DATA)
+
class ImageRgba(ImageBase):
"""Description of an RGB(A) image"""
@@ -339,8 +366,9 @@ class ImageRgba(ImageBase):
"""Update backend renderer"""
plot = self.getPlot()
assert plot is not None
- if plot.isXAxisLogarithmic() or plot.isYAxisLogarithmic():
- return None # Do not render with log scales
+ if not self._isPlotLinear(plot):
+ # Do not render with non linear scales
+ return None
data = self.getData(copy=False)
@@ -376,10 +404,19 @@ class ImageRgba(ImageBase):
assert data.shape[-1] in (3, 4)
self._data = data
- self._updated()
-
# TODO hackish data range implementation
if self.isVisible():
plot = self.getPlot()
if plot is not None:
plot._invalidateDataRange()
+
+ self._updated(ItemChangedType.DATA)
+
+
+class MaskImageData(ImageData):
+ """Description of an image used as a mask.
+
+ This class is used to flag mask items. This information is used to improve
+ internal silx widgets.
+ """
+ pass
diff --git a/silx/gui/plot/items/marker.py b/silx/gui/plot/items/marker.py
index c05558b..5f930b7 100644
--- a/silx/gui/plot/items/marker.py
+++ b/silx/gui/plot/items/marker.py
@@ -32,7 +32,8 @@ __date__ = "06/03/2017"
import logging
-from .core import Item, DraggableMixIn, ColorMixIn, SymbolMixIn
+from .core import (Item, DraggableMixIn, ColorMixIn, SymbolMixIn,
+ ItemChangedType)
_logger = logging.getLogger(__name__)
@@ -95,7 +96,7 @@ class _BaseMarker(Item, DraggableMixIn, ColorMixIn):
text = str(text)
if text != self._text:
self._text = text
- self._updated()
+ self._updated(ItemChangedType.TEXT)
def getXPosition(self):
"""Returns the X position of the marker line in data coordinates
@@ -130,7 +131,7 @@ class _BaseMarker(Item, DraggableMixIn, ColorMixIn):
x, y = float(x), float(y)
if x != self._x or y != self._y:
self._x, self._y = x, y
- self._updated()
+ self._updated(ItemChangedType.POSITION)
def getConstraint(self):
"""Returns the dragging constraint of this item"""
@@ -216,7 +217,7 @@ class XMarker(_BaseMarker):
x = float(x)
if x != self._x:
self._x = x
- self._updated()
+ self._updated(ItemChangedType.POSITION)
class YMarker(_BaseMarker):
@@ -238,4 +239,4 @@ class YMarker(_BaseMarker):
y = float(y)
if y != self._y:
self._y = y
- self._updated()
+ self._updated(ItemChangedType.POSITION)
diff --git a/silx/gui/plot/items/scatter.py b/silx/gui/plot/items/scatter.py
index 3897dc1..98ed473 100644
--- a/silx/gui/plot/items/scatter.py
+++ b/silx/gui/plot/items/scatter.py
@@ -35,7 +35,7 @@ import logging
import numpy
from .core import Points, ColormapMixIn
-from silx.gui.plot.Colors import applyColormapToData # TODO: cherry-pick commit or wait for PR merge
+
_logger = logging.getLogger(__name__)
@@ -60,13 +60,7 @@ class Scatter(Points, ColormapMixIn):
return None # No data to display, do not add renderer to backend
cmap = self.getColormap()
- rgbacolors = applyColormapToData(self._value,
- cmap["name"],
- cmap["normalization"],
- cmap["autoscale"],
- cmap["vmin"],
- cmap["vmax"],
- cmap.get("colors"))
+ rgbacolors = cmap.applyToData(self._value)
return backend.addCurve(xFiltered, yFiltered, self.getLegend(),
color=rgbacolors,
diff --git a/silx/gui/plot/items/shape.py b/silx/gui/plot/items/shape.py
index b663989..65b26a1 100644
--- a/silx/gui/plot/items/shape.py
+++ b/silx/gui/plot/items/shape.py
@@ -27,14 +27,14 @@
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "06/03/2017"
+__date__ = "17/05/2017"
import logging
import numpy
-from .core import Item, ColorMixIn, FillMixIn
+from .core import (Item, ColorMixIn, FillMixIn, ItemChangedType)
_logger = logging.getLogger(__name__)
@@ -46,7 +46,7 @@ class Shape(Item, ColorMixIn, FillMixIn):
"""Description of a shape item
:param str type_: The type of shape in:
- 'hline', 'polygon', 'rectangle', 'vline', 'polyline'
+ 'hline', 'polygon', 'rectangle', 'vline', 'polylines'
"""
def __init__(self, type_):
@@ -54,7 +54,7 @@ class Shape(Item, ColorMixIn, FillMixIn):
ColorMixIn.__init__(self)
FillMixIn.__init__(self)
self._overlay = False
- assert type_ in ('hline', 'polygon', 'rectangle', 'vline', 'polyline')
+ assert type_ in ('hline', 'polygon', 'rectangle', 'vline', 'polylines')
self._type = type_
self._points = ()
@@ -88,12 +88,12 @@ class Shape(Item, ColorMixIn, FillMixIn):
overlay = bool(overlay)
if overlay != self._overlay:
self._overlay = overlay
- self._updated()
+ self._updated(ItemChangedType.OVERLAY)
def getType(self):
"""Returns the type of shape to draw.
- One of: 'hline', 'polygon', 'rectangle', 'vline', 'polyline'
+ One of: 'hline', 'polygon', 'rectangle', 'vline', 'polylines'
:rtype: str
"""
@@ -118,4 +118,4 @@ class Shape(Item, ColorMixIn, FillMixIn):
:return:
"""
self._points = numpy.array(points, copy=copy)
- self._updated()
+ self._updated(ItemChangedType.DATA)