summaryrefslogtreecommitdiff
path: root/silx/gui/plot
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2017-10-07 07:59:01 +0200
committerPicca Frédéric-Emmanuel <picca@debian.org>2017-10-07 07:59:01 +0200
commitbfa4dba15485b4192f8bbe13345e9658c97ecf76 (patch)
treefb9c6e5860881fbde902f7cbdbd41dc4a3a9fb5d /silx/gui/plot
parentf7bdc2acff3c13a6d632c28c4569690ab106eed7 (diff)
New upstream version 0.6.0+dfsg
Diffstat (limited to 'silx/gui/plot')
-rw-r--r--silx/gui/plot/ColorBar.py456
-rw-r--r--silx/gui/plot/Colormap.py410
-rw-r--r--silx/gui/plot/ColormapDialog.py78
-rw-r--r--silx/gui/plot/Colors.py217
-rw-r--r--silx/gui/plot/ComplexImageView.py670
-rw-r--r--silx/gui/plot/CurvesROIWidget.py20
-rw-r--r--silx/gui/plot/ImageView.py157
-rw-r--r--silx/gui/plot/ItemsSelectionDialog.py282
-rw-r--r--silx/gui/plot/LegendSelector.py21
-rw-r--r--silx/gui/plot/LimitsHistory.py83
-rw-r--r--silx/gui/plot/MPLColormap.py1062
-rw-r--r--silx/gui/plot/MaskToolsWidget.py87
-rw-r--r--silx/gui/plot/Plot.py2925
-rw-r--r--silx/gui/plot/PlotActions.py1399
-rw-r--r--silx/gui/plot/PlotInteraction.py79
-rw-r--r--silx/gui/plot/PlotToolButtons.py13
-rw-r--r--silx/gui/plot/PlotTools.py152
-rw-r--r--silx/gui/plot/PlotWidget.py3020
-rw-r--r--silx/gui/plot/PlotWindow.py193
-rw-r--r--silx/gui/plot/PrintPreviewToolButton.py350
-rw-r--r--silx/gui/plot/Profile.py77
-rw-r--r--silx/gui/plot/ScatterMaskToolsWidget.py13
-rw-r--r--silx/gui/plot/StackView.py479
-rw-r--r--silx/gui/plot/_BaseMaskToolsWidget.py124
-rw-r--r--silx/gui/plot/__init__.py7
-rw-r--r--silx/gui/plot/_utils/__init__.py11
-rw-r--r--silx/gui/plot/_utils/panzoom.py154
-rw-r--r--silx/gui/plot/actions/PlotAction.py79
-rw-r--r--silx/gui/plot/actions/__init__.py38
-rw-r--r--silx/gui/plot/actions/control.py549
-rw-r--r--silx/gui/plot/actions/fit.py189
-rw-r--r--silx/gui/plot/actions/histogram.py170
-rw-r--r--silx/gui/plot/actions/io.py538
-rw-r--r--silx/gui/plot/actions/medfilt.py145
-rw-r--r--silx/gui/plot/actions/mode.py100
-rw-r--r--silx/gui/plot/backends/BackendBase.py39
-rw-r--r--silx/gui/plot/backends/BackendMatplotlib.py104
-rw-r--r--silx/gui/plot/backends/BackendOpenGL.py144
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py2
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py24
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py56
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py43
-rw-r--r--silx/gui/plot/backends/glutils/PlotImageFile.py14
-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
-rw-r--r--silx/gui/plot/matplotlib/Colormap.py282
-rw-r--r--silx/gui/plot/matplotlib/ModestImage.py (renamed from silx/gui/plot/backends/ModestImage.py)2
-rw-r--r--silx/gui/plot/matplotlib/__init__.py (renamed from silx/gui/plot/backends/_matplotlib.py)14
-rw-r--r--silx/gui/plot/setup.py5
-rw-r--r--silx/gui/plot/test/__init__.py80
-rw-r--r--silx/gui/plot/test/testColorBar.py227
-rw-r--r--silx/gui/plot/test/testColormap.py291
-rw-r--r--silx/gui/plot/test/testColors.py8
-rw-r--r--silx/gui/plot/test/testComplexImageView.py95
-rw-r--r--silx/gui/plot/test/testCurvesROIWidget.py3
-rw-r--r--silx/gui/plot/test/testItem.py231
-rw-r--r--silx/gui/plot/test/testLegendSelector.py3
-rw-r--r--silx/gui/plot/test/testLimitConstraints.py125
-rw-r--r--silx/gui/plot/test/testMaskToolsWidget.py38
-rw-r--r--silx/gui/plot/test/testPlotInteraction.py27
-rw-r--r--silx/gui/plot/test/testPlotTools.py23
-rw-r--r--silx/gui/plot/test/testPlotWidget.py775
-rw-r--r--silx/gui/plot/test/testPlotWidgetNoBackend.py (renamed from silx/gui/plot/test/testPlot.py)98
-rw-r--r--silx/gui/plot/test/testPlotWindow.py14
-rw-r--r--silx/gui/plot/test/testScatterMaskToolsWidget.py37
-rw-r--r--silx/gui/plot/test/testStackView.py33
-rw-r--r--silx/gui/plot/test/testUtilsAxis.py148
-rw-r--r--silx/gui/plot/test/utils.py194
-rw-r--r--silx/gui/plot/utils/__init__.py30
-rw-r--r--silx/gui/plot/utils/axis.py164
77 files changed, 11405 insertions, 7151 deletions
diff --git a/silx/gui/plot/ColorBar.py b/silx/gui/plot/ColorBar.py
index 93e3c36..8f4bde2 100644
--- a/silx/gui/plot/ColorBar.py
+++ b/silx/gui/plot/ColorBar.py
@@ -33,11 +33,8 @@ __date__ = "11/04/2017"
import logging
import numpy
from ._utils import ticklayout
-from ._utils import clipColormapLogRange
-
-
-from .. import qt
-from silx.gui.plot import Colors
+from .. import qt, icons
+from silx.gui.plot import Colormap
_logger = logging.getLogger(__name__)
@@ -66,12 +63,17 @@ class ColorBarWidget(qt.QWidget):
:param parent: See :class:`QWidget`
:param plot: PlotWidget the colorbar is attached to (optional)
- :param str legend: the label to set to the colormap
+ :param str legend: the label to set to the colorbar
"""
def __init__(self, parent=None, plot=None, legend=None):
- super(ColorBarWidget, self).__init__(parent)
+ self._isConnected = False
self._plot = None
+ self._viewAction = None
+ self._colormap = None
+ self._data = None
+
+ super(ColorBarWidget, self).__init__(parent)
self.__buildGUI()
self.setLegend(legend)
@@ -90,8 +92,6 @@ class ColorBarWidget(qt.QWidget):
self.layout().addWidget(self.legend)
self.layout().setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
- self.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Expanding)
- self.layout().setContentsMargins(0, 0, 0, 0)
def getPlot(self):
"""Returns the :class:`Plot` associated to this widget or None"""
@@ -100,46 +100,75 @@ class ColorBarWidget(qt.QWidget):
def setPlot(self, plot):
"""Associate a plot to the ColorBar
- :param plot: the plot to associate with the colorbar. If None will remove
- any connection with a previous plot.
+ :param plot: the plot to associate with the colorbar.
+ If None will remove any connection with a previous plot.
"""
- # removing previous plot if any
- if self._plot is not None:
- self._plot.sigActiveImageChanged.disconnect(self._activeImageChanged)
-
- # setting the new plot
+ self._disconnectPlot()
self._plot = plot
- if self._plot is not None:
+ self._connectPlot()
+
+ def _disconnectPlot(self):
+ """Disconnect from Plot signals"""
+ if self._plot is not None and self._isConnected:
+ self._isConnected = False
+ self._plot.sigActiveImageChanged.disconnect(
+ self._activeImageChanged)
+ self._plot.sigPlotSignal.disconnect(self._defaultColormapChanged)
+
+ def _connectPlot(self):
+ """Connect to Plot signals"""
+ if self._plot is not None and not self._isConnected:
+ activeImageLegend = self._plot.getActiveImage(just_legend=True)
+ if activeImageLegend is None: # Show plot default colormap
+ self._syncWithDefaultColormap()
+ else: # Show active image colormap
+ self._activeImageChanged(None, activeImageLegend)
self._plot.sigActiveImageChanged.connect(self._activeImageChanged)
- self._activeImageChanged(self._plot.getActiveImage(just_legend=True))
+ self._plot.sigPlotSignal.connect(self._defaultColormapChanged)
+ self._isConnected = True
+
+ def showEvent(self, event):
+ self._connectPlot()
+ if self._viewAction is not None:
+ self._viewAction.setChecked(True)
+
+ def hideEvent(self, event):
+ self._disconnectPlot()
+ if self._viewAction is not None:
+ self._viewAction.setChecked(False)
def getColormap(self):
- """Return the colormap displayed in the colorbar as a dict.
+ """
+
+ :return: the :class:`.Colormap` colormap displayed in the colorbar.
- It returns None if no colormap is set.
- See :class:`silx.gui.plot.Plot` documentation for the description of the colormap
- dict description.
"""
- return self._colormap.copy()
+ return self.getColorScaleBar().getColormap()
- def setColormap(self, colormap):
+ def setColormap(self, colormap, data=None):
"""Set the colormap to be displayed.
- :param dict colormap: The colormap to apply on the ColorBarWidget
+ :param colormap: The colormap to apply on the
+ ColorBarWidget
+ :type colormap: :class:`.Colormap`
+ :param numpy.ndarray data: the data to display, needed if the colormap
+ require an autoscale
"""
+ self._data = data
+ self.getColorScaleBar().setColormap(colormap=colormap,
+ data=data)
+ if self._colormap is not None:
+ self._colormap.sigChanged.disconnect(self._colormapHasChanged)
self._colormap = colormap
- if self._colormap is None:
- return
-
- if self._colormap['normalization'] not in ('log', 'linear'):
- raise ValueError('Wrong normalization %s' % self._colormap['normalization'])
+ if self._colormap is not None:
+ self._colormap.sigChanged.connect(self._colormapHasChanged)
- if self._colormap['normalization'] is 'log':
- if self._colormap['vmin'] < 1. or self._colormap['vmax'] < 1.:
- _logger.warning('Log colormap with bound <= 1: changing bounds.')
- clipColormapLogRange(colormap)
-
- self.getColorScaleBar().setColormap(self._colormap)
+ def _colormapHasChanged(self):
+ """handler of the Colormap.sigChanged signal
+ """
+ assert self._colormap is not None
+ self.setColormap(colormap=self._colormap,
+ data=self._data)
def setLegend(self, legend):
"""Set the legend displayed along the colorbar
@@ -150,7 +179,7 @@ class ColorBarWidget(qt.QWidget):
self.legend.hide()
self.legend.setText("")
else:
- assert(type(legend) is str)
+ assert type(legend) is str
self.legend.show()
self.legend.setText(legend)
@@ -163,10 +192,10 @@ class ColorBarWidget(qt.QWidget):
"""
return self.legend.getText()
- def _activeImageChanged(self, legend):
+ def _activeImageChanged(self, previous, legend):
"""Handle plot active curve changed"""
- if legend is None: # No active image, display default colormap
- self._syncWithDefaultColormap()
+ if legend is None: # No active image, display no colormap
+ self.setColormap(colormap=None)
return
# Sync with active image
@@ -174,32 +203,25 @@ class ColorBarWidget(qt.QWidget):
# RGB(A) image, display default colormap
if image.ndim != 2:
- self._syncWithDefaultColormap()
+ self.setColormap(colormap=None)
return
# data image, sync with image colormap
# do we need the copy here : used in the case we are changing
# vmin and vmax but should have already be done by the plot
- cmap = self._plot.getActiveImage().getColormap().copy()
- if cmap['autoscale']:
- if cmap['normalization'] == 'log':
- data = image[
- numpy.logical_and(image > 0, numpy.isfinite(image))]
- else:
- data = image[numpy.isfinite(image)]
- cmap['vmin'], cmap['vmax'] = data.min(), data.max()
-
- self.setColormap(cmap)
+ self.setColormap(colormap=self._plot.getActiveImage().getColormap(),
+ data=image)
- def _defaultColormapChanged(self):
+ def _defaultColormapChanged(self, event):
"""Handle plot default colormap changed"""
- if self._plot.getActiveImage() is None:
+ if (event['event'] == 'defaultColormapChanged' and
+ self._plot.getActiveImage() is None):
# No active image, take default colormap update into account
self._syncWithDefaultColormap()
- def _syncWithDefaultColormap(self):
+ def _syncWithDefaultColormap(self, data=None):
"""Update colorbar according to plot default colormap"""
- self.setColormap(self._plot.getDefaultColormap())
+ self.setColormap(self._plot.getDefaultColormap(), data)
def getColorScaleBar(self):
"""
@@ -208,6 +230,21 @@ class ColorBarWidget(qt.QWidget):
and ticks"""
return self._colorScale
+ def getToggleViewAction(self):
+ """Returns a checkable action controlling this widget's visibility.
+
+ :rtype: QAction
+ """
+ if self._viewAction is None:
+ self._viewAction = qt.QAction(self)
+ self._viewAction.setText('Colorbar')
+ self._viewAction.setIcon(icons.getQIcon('colorbar'))
+ self._viewAction.setToolTip('Show/Hide the colorbar')
+ self._viewAction.setCheckable(True)
+ self._viewAction.setChecked(self.isVisible())
+ self._viewAction.toggled[bool].connect(self.setVisible)
+ return self._viewAction
+
class _VerticalLegend(qt.QLabel):
"""Display vertically the given text
@@ -251,12 +288,11 @@ class ColorScaleBar(qt.QWidget):
To run the following sample code, a QApplication must be initialized.
- >>> colormap={'name':'gray',
- ... 'normalization':'log',
- ... 'vmin':1,
- ... 'vmax':100000,
- ... 'autoscale':False
- ... }
+ >>> colormap = Colormap(name='gray',
+ ... norm='log',
+ ... vmin=1,
+ ... vmax=100000,
+ ... )
>>> colorscale = ColorScaleBar(parent=None,
... colormap=colormap )
>>> colorscale.show()
@@ -272,15 +308,8 @@ class ColorScaleBar(qt.QWidget):
"""The tick bar need a margin to display all labels at the correct place.
So the ColorScale should have the same margin in order for both to fit"""
- _MIN_LIM_SCI_FORM = -1000
- """Used for the min and max label to know when we should display it under
- the scientific form"""
-
- _MAX_LIM_SCI_FORM = 1000
- """Used for the min and max label to know when we should display it under
- the scientific form"""
-
- def __init__(self, parent=None, colormap=None, displayTicksValues=True):
+ def __init__(self, parent=None, colormap=None, data=None,
+ displayTicksValues=True):
super(ColorScaleBar, self).__init__(parent)
self.minVal = None
@@ -292,33 +321,41 @@ class ColorScaleBar(qt.QWidget):
# create the left side group (ColorScale)
self.colorScale = _ColorScale(colormap=colormap,
- parent=self,
- margin=ColorScaleBar._TEXT_MARGIN)
+ data=data,
+ parent=self,
+ margin=ColorScaleBar._TEXT_MARGIN)
+ if colormap:
+ vmin, vmax = colormap.getColormapRange(data)
+ else:
+ vmin, vmax = Colormap.DEFAULT_MIN_LIN, Colormap.DEFAULT_MAX_LIN
- self.tickbar = _TickBar(vmin=colormap['vmin'] if colormap else 0.0,
- vmax=colormap['vmax'] if colormap else 1.0,
- norm=colormap['normalization'] if colormap else 'linear',
- parent=self,
- displayValues=displayTicksValues,
- margin=ColorScaleBar._TEXT_MARGIN)
+ norm = colormap.getNormalization() if colormap else Colormap.Colormap.LINEAR
+ self.tickbar = _TickBar(vmin=vmin,
+ vmax=vmax,
+ norm=norm,
+ parent=self,
+ displayValues=displayTicksValues,
+ margin=ColorScaleBar._TEXT_MARGIN)
- self.layout().addWidget(self.tickbar, 1, 0)
- self.layout().addWidget(self.colorScale, 1, 1)
+ self.layout().addWidget(self.tickbar, 1, 0, 1, 1, qt.Qt.AlignRight)
+ self.layout().addWidget(self.colorScale, 1, 1, qt.Qt.AlignLeft)
self.layout().setContentsMargins(0, 0, 0, 0)
self.layout().setSpacing(0)
# max label
self._maxLabel = qt.QLabel(str(1.0), parent=self)
- self._maxLabel.setAlignment(qt.Qt.AlignHCenter)
- self._maxLabel.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Minimum)
- self.layout().addWidget(self._maxLabel, 0, 1)
+ self._maxLabel.setToolTip(str(0.0))
+ self.layout().addWidget(self._maxLabel, 0, 0, 1, 2, qt.Qt.AlignRight)
# min label
self._minLabel = qt.QLabel(str(0.0), parent=self)
- self._minLabel.setAlignment(qt.Qt.AlignHCenter)
- self._minLabel.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Minimum)
- self.layout().addWidget(self._minLabel, 2, 1)
+ self._minLabel.setToolTip(str(0.0))
+ self.layout().addWidget(self._minLabel, 2, 0, 1, 2, qt.Qt.AlignRight)
+
+ self.layout().setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
+ self.layout().setColumnStretch(0, 1)
+ self.layout().setRowStretch(1, 1)
def getTickBar(self):
"""
@@ -334,19 +371,34 @@ class ColorScaleBar(qt.QWidget):
"""
return self.colorScale
- def setColormap(self, colormap):
+ def getColormap(self):
+ """
+
+ :returns: the colormap.
+ :rtype: :class:`.Colormap`
+ """
+ return self.colorScale.getColormap()
+
+ def setColormap(self, colormap, data=None):
"""Set the new colormap to be displayed
- :param dict colormap: the colormap to set
+ :param Colormap colormap: the colormap to set
+ :param numpy.ndarray data: the data to display, needed if the colormap
+ require an autoscale
"""
- if colormap is not None:
- self.colorScale.setColormap(colormap)
+ self.colorScale.setColormap(colormap, data)
- self.tickbar.update(vmin=colormap['vmin'],
- vmax=colormap['vmax'],
- norm=colormap['normalization'])
+ if colormap is not None:
+ vmin, vmax = colormap.getColormapRange(data)
+ norm = colormap.getNormalization()
+ else:
+ vmin, vmax = None, None
+ norm = None
- self._setMinMaxLabels(colormap['vmin'], colormap['vmax'])
+ self.tickbar.update(vmin=vmin,
+ vmax=vmax,
+ norm=norm)
+ self._setMinMaxLabels(vmin, vmax)
def setMinMaxVisible(self, val=True):
"""Change visibility of the min label and the max label
@@ -359,17 +411,29 @@ class ColorScaleBar(qt.QWidget):
def _updateMinMax(self):
"""Update the min and max label if we are in the case of the
configuration 'minMaxValueOnly'"""
- if self._minLabel is not None and self._maxLabel is not None:
- if self.minVal is not None:
- if ColorScaleBar._MIN_LIM_SCI_FORM <= self.minVal <= ColorScaleBar._MAX_LIM_SCI_FORM:
- self._minLabel.setText(str(self.minVal))
- else:
- self._minLabel.setText("{0:.0e}".format(self.minVal))
- if self.maxVal is not None:
- if ColorScaleBar._MIN_LIM_SCI_FORM <= self.maxVal <= ColorScaleBar._MAX_LIM_SCI_FORM:
- self._maxLabel.setText(str(self.maxVal))
- else:
- self._maxLabel.setText("{0:.0e}".format(self.maxVal))
+ if self.minVal is None:
+ text, tooltip = '', ''
+ else:
+ if self.minVal == 0 or 0 <= numpy.log10(abs(self.minVal)) < 7:
+ text = '%.7g' % self.minVal
+ else:
+ text = '%.2e' % self.minVal
+ tooltip = repr(self.minVal)
+
+ self._minLabel.setText(text)
+ self._minLabel.setToolTip(tooltip)
+
+ if self.maxVal is None:
+ text, tooltip = '', ''
+ else:
+ if self.maxVal == 0 or 0 <= numpy.log10(abs(self.maxVal)) < 7:
+ text = '%.7g' % self.maxVal
+ else:
+ text = '%.2e' % self.maxVal
+ tooltip = repr(self.maxVal)
+
+ self._maxLabel.setText(text)
+ self._maxLabel.setToolTip(tooltip)
def _setMinMaxLabels(self, minVal, maxVal):
"""Change the value of the min and max labels to be displayed.
@@ -400,12 +464,11 @@ class _ColorScale(qt.QWidget):
To run the following sample code, a QApplication must be initialized.
- >>> colormap={'name':'viridis',
- ... 'normalization':'log',
- ... 'vmin':1,
- ... 'vmax':100000,
- ... 'autoscale':False
- ... }
+ >>> colormap = Colormap(name='viridis',
+ ... norm='log',
+ ... vmin=1,
+ ... vmax=100000,
+ ... )
>>> colorscale = ColorScale(parent=None,
... colormap=colormap)
>>> colorscale.show()
@@ -423,83 +486,94 @@ class _ColorScale(qt.QWidget):
_NB_CONTROL_POINTS = 256
- def __init__(self, colormap, parent=None, margin=5):
+ def __init__(self, colormap, parent=None, margin=5, data=None):
qt.QWidget.__init__(self, parent)
- self.colormap = None
- self.setColormap(colormap)
+ self._colormap = None
+ self.margin = margin
+ self.setColormap(colormap, data)
self.setLayout(qt.QVBoxLayout())
- self.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
+ self.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Expanding)
# needed to get the mouse event without waiting for button click
self.setMouseTracking(True)
self.setMargin(margin)
self.setContentsMargins(0, 0, 0, 0)
- def setColormap(self, colormap):
+ self.setMinimumHeight(self._NB_CONTROL_POINTS // 2 + 2 * self.margin)
+ self.setFixedWidth(25)
+
+ def setColormap(self, colormap, data=None):
"""Set the new colormap to be displayed
:param dict colormap: the colormap to set
+ :param data: Optional data for which to compute colormap range.
"""
- if colormap is None:
- return
+ self._colormap = colormap
+ self.setEnabled(colormap is not None)
- if colormap['normalization'] not in ('log', 'linear'):
- raise ValueError("Unrecognized normalization, should be 'linear' or 'log'")
+ if colormap is None:
+ self.vmin, self.vmax = None, None
+ else:
+ assert colormap.getNormalization() in Colormap.Colormap.NORMALIZATIONS
+ self.vmin, self.vmax = self._colormap.getColormapRange(data=data)
+ self._updateColorGradient()
+ self.update()
- if colormap['normalization'] is 'log':
- if not (colormap['vmin'] > 0 and colormap['vmax'] > 0):
- raise ValueError('vmin and vmax should be positives')
- self.colormap = colormap
- self._computeColorPoints()
+ def getColormap(self):
+ """Returns the colormap
- def _computeColorPoints(self):
- """Compute the color points for the gradient
+ :rtype: :class:`.Colormap`
"""
- if self.colormap is None:
+ return None if self._colormap is None else self._colormap
+
+ def _updateColorGradient(self):
+ """Compute the color gradient"""
+ colormap = self.getColormap()
+ if colormap is None:
return
- vmin = self.colormap['vmin']
- vmax = self.colormap['vmax']
- steps = (vmax - vmin)/float(_ColorScale._NB_CONTROL_POINTS)
- self.ctrPoints = numpy.arange(vmin, vmax, steps)
- self.colorsCtrPts = Colors.applyColormapToData(self.ctrPoints,
- name=self.colormap['name'],
- normalization='linear',
- autoscale=self.colormap['autoscale'],
- vmin=vmin,
- vmax=vmax)
+ indices = numpy.linspace(0., 1., self._NB_CONTROL_POINTS)
+ colormapDisp = Colormap.Colormap(name=colormap.getName(),
+ normalization=Colormap.Colormap.LINEAR,
+ vmin=None,
+ vmax=None,
+ colors=colormap.getColormapLUT())
+ colors = colormapDisp.applyToData(indices)
+ self._gradient = qt.QLinearGradient(0, 1, 0, 0)
+ self._gradient.setCoordinateMode(qt.QGradient.StretchToDeviceMode)
+ self._gradient.setStops(
+ [(i, qt.QColor(*color)) for i, color in zip(indices, colors)]
+ )
def paintEvent(self, event):
""""""
- qt.QWidget.paintEvent(self, event)
- if self.colormap is None:
- return
-
- vmin = self.colormap['vmin']
- vmax = self.colormap['vmax']
-
painter = qt.QPainter(self)
- gradient = qt.QLinearGradient(0, 0, 0, self.rect().height() - 2*self.margin)
- for iPt, pt in enumerate(self.ctrPoints):
- colormapPosition = 1 - (pt-vmin) / (vmax-vmin)
- assert(colormapPosition >= 0.0)
- assert(colormapPosition <= 1.0)
- gradient.setColorAt(colormapPosition, qt.QColor(*(self.colorsCtrPts[iPt])))
+ if self.getColormap() is not None:
+ painter.setBrush(self._gradient)
+ penColor = self.palette().color(qt.QPalette.Active,
+ qt.QPalette.Foreground)
+ else:
+ penColor = self.palette().color(qt.QPalette.Disabled,
+ qt.QPalette.Foreground)
+ painter.setPen(penColor)
- painter.setBrush(gradient)
- painter.drawRect(
- qt.QRect(0, self.margin, self.width(), self.height() - 2.*self.margin))
+ painter.drawRect(qt.QRect(
+ 0,
+ self.margin,
+ self.width() - 1.,
+ self.height() - 2. * self.margin - 1.))
def mouseMoveEvent(self, event):
- """"""
- self.setToolTip(str(self.getValueFromRelativePosition(self._getRelativePosition(event.y()))))
+ tooltip = str(self.getValueFromRelativePosition(
+ self._getRelativePosition(event.y())))
+ qt.QToolTip.showText(event.globalPos(), tooltip, self)
super(_ColorScale, self).mouseMoveEvent(event)
def _getRelativePosition(self, yPixel):
"""yPixel : pixel position into _ColorScale widget reference
"""
# widgets are bottom-top referencial but we display in top-bottom referential
- return 1 - float(yPixel)/float(self.height() - 2*self.margin)
+ return 1. - (yPixel - self.margin) / float(self.height() - 2 * self.margin)
def getValueFromRelativePosition(self, value):
"""Return the value in the colorMap from a relative position in the
@@ -508,17 +582,22 @@ class _ColorScale(qt.QWidget):
:param value: float value in [0, 1]
:return: the value in [colormap['vmin'], colormap['vmax']]
"""
+ colormap = self.getColormap()
+ if colormap is None:
+ return
+
value = max(0.0, value)
value = min(value, 1.0)
- vmin = self.colormap['vmin']
- vmax = self.colormap['vmax']
- if self.colormap['normalization'] is 'linear':
+
+ vmin = self.vmin
+ vmax = self.vmax
+ if colormap.getNormalization() == Colormap.Colormap.LINEAR:
return vmin + (vmax - vmin) * value
- elif self.colormap['normalization'] is 'log':
+ elif colormap.getNormalization() == Colormap.Colormap.LOGARITHM:
rpos = (numpy.log10(vmax) - numpy.log10(vmin)) * value + numpy.log10(vmin)
return numpy.power(10., rpos)
else:
- err = "normalization type (%s) is not managed by the _ColorScale Widget" % self.colormap['normalization']
+ err = "normalization type (%s) is not managed by the _ColorScale Widget" % colormap['normalization']
raise ValueError(err)
def setMargin(self, margin):
@@ -529,6 +608,7 @@ class _ColorScale(qt.QWidget):
:param int margin: the margin to apply on the top and bottom.
"""
self.margin = margin
+ self.update()
class _TickBar(qt.QWidget):
@@ -536,7 +616,7 @@ class _TickBar(qt.QWidget):
To run the following sample code, a QApplication must be initialized.
- >>> bar = TickBar(1, 1000, norm='log', parent=None, displayValues=True)
+ >>> bar = _TickBar(1, 1000, norm='log', parent=None, displayValues=True)
>>> bar.show()
.. image:: img/tickbar.png
@@ -569,24 +649,19 @@ class _TickBar(qt.QWidget):
def __init__(self, vmin, vmax, norm, parent=None, displayValues=True,
nticks=None, margin=5):
super(_TickBar, self).__init__(parent)
+ self.margin = margin
+ self._nticks = None
+ self.ticks = ()
+ self.subTicks = ()
self._forcedDisplayType = None
self.ticksDensity = _TickBar.DEFAULT_TICK_DENSITY
self._vmin = vmin
self._vmax = vmax
- # TODO : should be grouped into a global function, called by all
- # logScale displayer to make sure we have the same behavior everywhere
- if self._vmin < 1. or self._vmax < 1.:
- _logger.warning(
- 'Log colormap with bound <= 1: changing bounds.')
- self._vmin, self._vmax = 1., 10.
-
self._norm = norm
self.displayValues = displayValues
self.setTicksNumber(nticks)
- self.setMargin(margin)
- self.setLayout(qt.QVBoxLayout())
self.setMargin(margin)
self.setContentsMargins(0, 0, 0, 0)
@@ -597,8 +672,8 @@ class _TickBar(qt.QWidget):
self._resetWidth()
def _resetWidth(self):
- self.width = _TickBar._WIDTH_DISP_VAL if self.displayValues else _TickBar._WIDTH_NO_DISP_VAL
- self.setFixedWidth(self.width)
+ width = self._WIDTH_DISP_VAL if self.displayValues else self._WIDTH_NO_DISP_VAL
+ self.setFixedWidth(width)
def update(self, vmin, vmax, norm):
self._vmin = vmin
@@ -623,7 +698,6 @@ class _TickBar(qt.QWidget):
optimal number of ticks from the tick density.
"""
self._nticks = nticks
- self.ticks = None
self.computeTicks()
qt.QWidget.update(self)
@@ -644,9 +718,13 @@ class _TickBar(qt.QWidget):
if nticks is None:
nticks = self._getOptimalNbTicks()
- if self._norm == 'log':
+ if self._vmin == self._vmax:
+ # No range: no ticks
+ self.ticks = ()
+ self.subTicks = ()
+ elif self._norm == Colormap.Colormap.LOGARITHM:
self._computeTicksLog(nticks)
- elif self._norm == 'linear':
+ elif self._norm == Colormap.Colormap.LINEAR:
self._computeTicksLin(nticks)
else:
err = 'TickBar - Wrong normalization %s' % self._norm
@@ -693,22 +771,19 @@ class _TickBar(qt.QWidget):
painter.setFont(font)
# paint ticks
- if self.ticks is not None:
- for val in self.ticks:
- self._paintTick(val, painter, majorTick=True)
-
- # paint subticks
- for val in self.subTicks:
- self._paintTick(val, painter, majorTick=False)
+ for val in self.ticks:
+ self._paintTick(val, painter, majorTick=True)
- qt.QWidget.paintEvent(self, event)
+ # paint subticks
+ for val in self.subTicks:
+ self._paintTick(val, painter, majorTick=False)
def _getRelativePosition(self, val):
"""Return the relative position of val according to min and max value
"""
- if self._norm == 'linear':
+ if self._norm == Colormap.Colormap.LINEAR:
return 1 - (val - self._vmin) / (self._vmax - self._vmin)
- elif self._norm == 'log':
+ elif self._norm == Colormap.Colormap.LOGARITHM:
return 1 - (numpy.log10(val) - numpy.log10(self._vmin))/(numpy.log10(self._vmax) - numpy.log(self._vmin))
else:
raise ValueError('Norm is not recognized')
@@ -720,7 +795,7 @@ class _TickBar(qt.QWidget):
with a smaller width
"""
fm = qt.QFontMetrics(painter.font())
- viewportHeight = self.rect().height() - self.margin * 2
+ viewportHeight = self.rect().height() - self.margin * 2 - 1
relativePos = self._getRelativePosition(val)
height = viewportHeight * relativePos
height += self.margin
@@ -728,9 +803,9 @@ class _TickBar(qt.QWidget):
if majorTick is False:
lineWidth /= 2
- painter.drawLine(qt.QLine(self.width - lineWidth,
+ painter.drawLine(qt.QLine(self.width() - lineWidth,
height,
- self.width,
+ self.width(),
height))
if self.displayValues and majorTick is True:
@@ -774,7 +849,6 @@ class _TickBar(qt.QWidget):
:param QFont font: the font we want want to use durint the painting
"""
- assert(type(self._vmin) == type(self._vmax))
form = self._getStandardFormat()
fm = qt.QFontMetrics(font)
diff --git a/silx/gui/plot/Colormap.py b/silx/gui/plot/Colormap.py
new file mode 100644
index 0000000..abe8546
--- /dev/null
+++ b/silx/gui/plot/Colormap.py
@@ -0,0 +1,410 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2015-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 Colormap object
+"""
+
+from __future__ import absolute_import
+
+__authors__ = ["T. Vincent", "H.Payno"]
+__license__ = "MIT"
+__date__ = "05/12/2016"
+
+from silx.gui import qt
+import copy as copy_mdl
+import numpy
+from .matplotlib import Colormap as MPLColormap
+import logging
+from silx.math.combo import min_max
+
+_logger = logging.getLogger(__file__)
+
+DEFAULT_COLORMAPS = (
+ 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
+"""Tuple of supported colormap names."""
+
+DEFAULT_MIN_LIN = 0
+"""Default min value if in linear normalization"""
+DEFAULT_MAX_LIN = 1
+"""Default max value if in linear normalization"""
+DEFAULT_MIN_LOG = 1
+"""Default min value if in log normalization"""
+DEFAULT_MAX_LOG = 10
+"""Default max value if in log normalization"""
+
+
+class Colormap(qt.QObject):
+ """Description of a colormap
+
+ :param str name: Name of the colormap
+ :param tuple colors: optional, custom colormap.
+ Nx3 or Nx4 numpy array of RGB(A) colors,
+ either uint8 or float in [0, 1].
+ If 'name' is None, then this array is used as the colormap.
+ :param str norm: Normalization: 'linear' (default) or 'log'
+ :param float vmin:
+ Lower bound of the colormap or None for autoscale (default)
+ :param float vmax:
+ Upper bounds of the colormap or None for autoscale (default)
+ """
+
+ LINEAR = 'linear'
+ """constant for linear normalization"""
+
+ LOGARITHM = 'log'
+ """constant for logarithmic normalization"""
+
+ NORMALIZATIONS = (LINEAR, LOGARITHM)
+ """Tuple of managed normalizations"""
+
+ sigChanged = qt.Signal()
+
+ def __init__(self, name='gray', colors=None, normalization=LINEAR, vmin=None, vmax=None):
+ qt.QObject.__init__(self)
+ assert normalization in Colormap.NORMALIZATIONS
+ assert not (name is None and colors is None)
+ if normalization is Colormap.LOGARITHM:
+ if (vmin is not None and vmin < 0) or (vmax is not None and vmax < 0):
+ m = "Unsuported vmin (%s) and/or vmax (%s) given for a log scale."
+ m += ' Autoscale will be performed.'
+ m = m % (vmin, vmax)
+ _logger.warning(m)
+ vmin = None
+ vmax = None
+
+ self._name = str(name) if name is not None else None
+ self._setColors(colors)
+ self._normalization = str(normalization)
+ self._vmin = float(vmin) if vmin is not None else None
+ self._vmax = float(vmax) if vmax is not None else None
+
+ def isAutoscale(self):
+ """Return True if both min and max are in autoscale mode"""
+ return self._vmin is None or self._vmax is None
+
+ def getName(self):
+ """Return the name of the colormap
+ :rtype: str
+ """
+ return self._name
+
+ def _setColors(self, colors):
+ if colors is None:
+ self._colors = None
+ else:
+ self._colors = numpy.array(colors, copy=True)
+
+ def setName(self, name):
+ """Set the name of the colormap and load the colors corresponding to
+ the name
+
+ :param str name: the name of the colormap (should be in ['gray',
+ 'reversed gray', 'temperature', 'red', 'green', 'blue', 'jet',
+ 'viridis', 'magma', 'inferno', 'plasma']
+ """
+ assert name in self.getSupportedColormaps()
+ self._name = str(name)
+ self._colors = None
+ self.sigChanged.emit()
+
+ def getColormapLUT(self):
+ """Return the list of colors for the colormap. None if not setted
+
+ :return: the list of colors for the colormap. None if not setted
+ :rtype: numpy.ndarray
+ """
+ return self._colors
+
+ def setColormapLUT(self, colors):
+ """
+ Set the colors of the colormap.
+
+ :param numpy.ndarray colors: the colors of the LUT
+
+ .. warning: this will set the value of name to an empty string
+ """
+ self._setColors(colors)
+ if len(colors) is 0:
+ self._colors = None
+
+ self._name = None
+ self.sigChanged.emit()
+
+ def getNormalization(self):
+ """Return the normalization of the colormap ('log' or 'linear')
+
+ :return: the normalization of the colormap
+ :rtype: str
+ """
+ return self._normalization
+
+ def setNormalization(self, norm):
+ """Set the norm ('log', 'linear')
+
+ :param str norm: the norm to set
+ """
+ self._normalization = str(norm)
+ self.sigChanged.emit()
+
+ def getVMin(self):
+ """Return the lower bound of the colormap
+
+ :return: the lower bound of the colormap
+ :rtype: float or None
+ """
+ return self._vmin
+
+ def setVMin(self, vmin):
+ """Set the minimal value of the colormap
+
+ :param float vmin: Lower bound of the colormap or None for autoscale
+ (default)
+ value)
+ """
+ if vmin is not None:
+ if self._vmax is not None and vmin >= self._vmax:
+ err = "Can't set vmin because vmin >= vmax."
+ err += "vmin = %s, vmax = %s" %(vmin, self._vmax)
+ raise ValueError(err)
+
+ self._vmin = vmin
+ self.sigChanged.emit()
+
+ def getVMax(self):
+ """Return the upper bounds of the colormap or None
+
+ :return: the upper bounds of the colormap or None
+ :rtype: float or None
+ """
+ return self._vmax
+
+ def setVMax(self, vmax):
+ """Set the maximal value of the colormap
+
+ :param float vmax: Upper bounds of the colormap or None for autoscale
+ (default)
+ """
+ if vmax is not None:
+ if self._vmin is not None and vmax <= self._vmin:
+ err = "Can't set vmax because vmax <= vmin."
+ err += "vmin = %s, vmax = %s" %(self._vmin, vmax)
+ raise ValueError(err)
+
+ self._vmax = vmax
+ self.sigChanged.emit()
+
+ def getColormapRange(self, data=None):
+ """Return (vmin, vmax)
+
+ :return: the tuple vmin, vmax fitting vmin, vmax, normalization and
+ data if any given
+ :rtype: tuple
+ """
+ vmin = self._vmin
+ vmax = self._vmax
+ assert vmin is None or vmax is None or vmin <= vmax # TODO handle this in setters
+
+ if self.getNormalization() == self.LOGARITHM:
+ # Handle negative bounds as autoscale
+ if vmin is not None and (vmin is not None and vmin <= 0.):
+ mess = 'negative vmin, moving to autoscale for lower bound'
+ _logger.warning(mess)
+ vmin = None
+ if vmax is not None and (vmax is not None and vmax <= 0.):
+ mess = 'negative vmax, moving to autoscale for upper bound'
+ _logger.warning(mess)
+ vmax = None
+
+ if vmin is None or vmax is None: # Handle autoscale
+ # Get min/max from data
+ if data is not None:
+ data = numpy.array(data, copy=False)
+ if data.size == 0: # Fallback an array but no data
+ min_, max_ = self._getDefaultMin(), self._getDefaultMax()
+ else:
+ if self.getNormalization() == self.LOGARITHM:
+ result = min_max(data, min_positive=True, finite=True)
+ min_ = result.min_positive # >0 or None
+ max_ = result.maximum # can be <= 0
+ else:
+ min_, max_ = min_max(data, min_positive=False, finite=True)
+
+ # Handle fallback
+ if min_ is None or not numpy.isfinite(min_):
+ min_ = self._getDefaultMin()
+ if max_ is None or not numpy.isfinite(max_):
+ max_ = self._getDefaultMax()
+ else: # Fallback if no data is provided
+ min_, max_ = self._getDefaultMin(), self._getDefaultMax()
+
+ if vmin is None: # Set vmin respecting provided vmax
+ vmin = min_ if vmax is None else min(min_, vmax)
+
+ if vmax is None:
+ vmax = max(max_, vmin) # Handle max_ <= 0 for log scale
+
+ return vmin, vmax
+
+ def setVRange(self, vmin, vmax):
+ """
+ Set bounds to the colormap
+
+ :param vmin: Lower bound of the colormap or None for autoscale
+ (default)
+ :param vmax: Upper bounds of the colormap or None for autoscale
+ (default)
+ """
+ if vmin is not None and vmax is not None:
+ if vmin >= vmax:
+ err = "Can't set vmin and vmax because vmin >= vmax"
+ err += "vmin = %s, vmax = %s" %(vmin, self._vmax)
+ raise ValueError(err)
+
+ self._vmin = vmin
+ self._vmax = vmax
+ self.sigChanged.emit()
+
+ def __getitem__(self, item):
+ if item == 'autoscale':
+ return self.isAutoscale()
+ elif item == 'name':
+ return self.getName()
+ elif item == 'normalization':
+ return self.getNormalization()
+ elif item == 'vmin':
+ return self.getVMin()
+ elif item == 'vmax':
+ return self.getVMax()
+ elif item == 'colors':
+ return self.getColormapLUT()
+ else:
+ raise KeyError(item)
+
+ def _toDict(self):
+ """Return the equivalent colormap as a dictionary
+ (old colormap representation)
+
+ :return: the representation of the Colormap as a dictionary
+ :rtype: dict
+ """
+ return {
+ 'name': self._name,
+ 'colors': copy_mdl.copy(self._colors),
+ 'vmin': self._vmin,
+ 'vmax': self._vmax,
+ 'autoscale': self.isAutoscale(),
+ 'normalization': self._normalization
+ }
+
+ def _setFromDict(self, dic):
+ """Set values to the colormap from a dictionary
+
+ :param dict dic: the colormap as a dictionary
+ """
+ name = dic['name'] if 'name' in dic else None
+ colors = dic['colors'] if 'colors' in dic else None
+ vmin = dic['vmin'] if 'vmin' in dic else None
+ vmax = dic['vmax'] if 'vmax' in dic else None
+ if 'normalization' in dic:
+ normalization = dic['normalization']
+ else:
+ warn = 'Normalization not given in the dictionary, '
+ warn += 'set by default to ' + Colormap.LINEAR
+ _logger.warning(warn)
+ normalization = Colormap.LINEAR
+
+ if name is None and colors is None:
+ err = 'The colormap should have a name defined or a tuple of colors'
+ raise ValueError(err)
+ if normalization not in Colormap.NORMALIZATIONS:
+ err = 'Given normalization is not recoginized (%s)' % normalization
+ raise ValueError(err)
+
+ # If autoscale, then set boundaries to None
+ if dic.get('autoscale', False):
+ vmin, vmax = None, None
+
+ self._name = name
+ self._colors = colors
+ self._vmin = vmin
+ self._vmax = vmax
+ self._autoscale = True if (vmin is None and vmax is None) else False
+ self._normalization = normalization
+
+ self.sigChanged.emit()
+
+ @staticmethod
+ def _fromDict(dic):
+ colormap = Colormap(name="")
+ colormap._setFromDict(dic)
+ return colormap
+
+ def copy(self):
+ """
+
+ :return: a copy of the Colormap object
+ """
+ return Colormap(name=self._name,
+ colors=copy_mdl.copy(self._colors),
+ vmin=self._vmin,
+ vmax=self._vmax,
+ normalization=self._normalization)
+
+ def applyToData(self, data):
+ """Apply the colormap to the data
+
+ :param numpy.ndarray data: The data to convert.
+ """
+ rgbaImage = MPLColormap.applyColormapToData(colormap=self, data=data)
+ return rgbaImage
+
+ @staticmethod
+ def getSupportedColormaps():
+ """Get the supported colormap names as a tuple of str.
+
+ The list should at least contain and start by:
+ ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
+ :rtype: tuple
+ """
+ maps = MPLColormap.getSupportedColormaps()
+ return DEFAULT_COLORMAPS + maps
+
+ def __str__(self):
+ return str(self._toDict())
+
+ def _getDefaultMin(self):
+ return DEFAULT_MIN_LIN if self._normalization == Colormap.LINEAR else DEFAULT_MIN_LOG
+
+ def _getDefaultMax(self):
+ return DEFAULT_MAX_LIN if self._normalization == Colormap.LINEAR else DEFAULT_MAX_LOG
+
+ def __eq__(self, other):
+ """Compare colormap values and not pointers"""
+ return (self.getName() == other.getName() and
+ self.getNormalization() == other.getNormalization() and
+ self.getVMin() == other.getVMin() and
+ self.getVMax() == other.getVMax() and
+ numpy.array_equal(self.getColormapLUT(), other.getColormapLUT())
+ )
+
diff --git a/silx/gui/plot/ColormapDialog.py b/silx/gui/plot/ColormapDialog.py
index ad1425c..748dd72 100644
--- a/silx/gui/plot/ColormapDialog.py
+++ b/silx/gui/plot/ColormapDialog.py
@@ -1,7 +1,7 @@
# coding: utf-8
# /*##########################################################################
#
-# Copyright (c) 2004-2016 European Synchrotron Radiation Facility
+# Copyright (c) 2004-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
@@ -42,7 +42,7 @@ Create the colormap dialog and set the colormap description and data range:
Get the colormap description (compatible with :class:`Plot`) from the dialog:
>>> cmap = dialog.getColormap()
->>> cmap['name']
+>>> cmap.getName()
'red'
It is also possible to display an histogram of the image in the dialog.
@@ -61,7 +61,7 @@ from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "29/03/2016"
+__date__ = "02/10/2017"
import logging
@@ -69,37 +69,13 @@ import logging
import numpy
from .. import qt
+from .Colormap import Colormap
from . import PlotWidget
-
+from silx.gui.widgets.FloatEdit import FloatEdit
_logger = logging.getLogger(__name__)
-class _FloatEdit(qt.QLineEdit):
- """Field to edit a float value.
-
- :param parent: See :class:`QLineEdit`
- :param float value: The value to set the QLineEdit to.
- """
- def __init__(self, parent=None, value=None):
- qt.QLineEdit.__init__(self, parent)
- self.setValidator(qt.QDoubleValidator())
- self.setAlignment(qt.Qt.AlignRight)
- if value is not None:
- self.setValue(value)
-
- def value(self):
- """Return the QLineEdit current value as a float."""
- return float(self.text())
-
- def setValue(self, value):
- """Set the current value of the LineEdit
-
- :param float value: The value to set the QLineEdit to.
- """
- self.setText('%g' % value)
-
-
class ColormapDialog(qt.QDialog):
"""A QDialog widget to set the colormap.
@@ -107,7 +83,7 @@ class ColormapDialog(qt.QDialog):
:param str title: The QDialog title
"""
- sigColormapChanged = qt.Signal(dict)
+ sigColormapChanged = qt.Signal(Colormap)
"""Signal triggered when the colormap is changed.
It provides a dict describing the colormap to the slot.
@@ -122,10 +98,13 @@ class ColormapDialog(qt.QDialog):
self._dataRange = None
self._minMaxWasEdited = False
- self._colormapList = (
+ colormaps = [
'gray', 'reversed gray',
'temperature', 'red', 'green', 'blue', 'jet',
- 'viridis', 'magma', 'inferno', 'plasma')
+ 'viridis', 'magma', 'inferno', 'plasma']
+ if 'hsv' in Colormap.getSupportedColormaps():
+ colormaps.append('hsv')
+ self._colormapList = tuple(colormaps)
# Make the GUI
vLayout = qt.QVBoxLayout(self)
@@ -172,14 +151,14 @@ class ColormapDialog(qt.QDialog):
formLayout.addRow('Range:', self._rangeAutoscaleButton)
# Min row
- self._minValue = _FloatEdit(value=1.)
+ self._minValue = FloatEdit(parent=self, value=1.)
self._minValue.setEnabled(False)
self._minValue.textEdited.connect(self._minMaxTextEdited)
self._minValue.editingFinished.connect(self._minEditingFinished)
formLayout.addRow('\tMin:', self._minValue)
# Max row
- self._maxValue = _FloatEdit(value=10.)
+ self._maxValue = FloatEdit(parent=self, value=10.)
self._maxValue.setEnabled(False)
self._maxValue.textEdited.connect(self._minMaxTextEdited)
self._maxValue.editingFinished.connect(self._maxEditingFinished)
@@ -214,8 +193,8 @@ class ColormapDialog(qt.QDialog):
"""Init the plot to display the range and the values"""
self._plot = PlotWidget()
self._plot.setDataMargins(yMinMargin=0.125, yMaxMargin=0.125)
- self._plot.setGraphXLabel("Data Values")
- self._plot.setGraphYLabel("")
+ self._plot.getXAxis().setLabel("Data Values")
+ self._plot.getYAxis().setLabel("")
self._plot.setInteractiveMode('select', zoomOnWheel=False)
self._plot.setActiveCurveHandling(False)
self._plot.setMinimumSize(qt.QSize(250, 200))
@@ -392,17 +371,22 @@ class ColormapDialog(qt.QDialog):
self._plotUpdate()
def getColormap(self):
- """Return the colormap description as a dict.
+ """Return the colormap description as a :class:`.Colormap`.
- See :class:`Plot` for documentation on the colormap dict.
"""
isNormLinear = self._normButtonLinear.isChecked()
- colormap = {
- 'name': str(self._comboBoxColormap.currentText()).lower(),
- 'normalization': 'linear' if isNormLinear else 'log',
- 'autoscale': self._rangeAutoscaleButton.isChecked(),
- 'vmin': self._minValue.value(),
- 'vmax': self._maxValue.value()}
+ if self._rangeAutoscaleButton.isChecked():
+ vmin = None
+ vmax = None
+ else:
+ vmin = self._minValue.value()
+ vmax = self._maxValue.value()
+ norm = Colormap.LINEAR if isNormLinear else Colormap.LOGARITHM
+ colormap = Colormap(
+ name=str(self._comboBoxColormap.currentText()).lower(),
+ normalization=norm,
+ vmin=vmin,
+ vmax=vmax)
return colormap
def setColormap(self, name=None, normalization=None,
@@ -423,9 +407,9 @@ class ColormapDialog(qt.QDialog):
self._comboBoxColormap.setCurrentIndex(index)
if normalization is not None:
- assert normalization in ('linear', 'log')
- self._normButtonLinear.setChecked(normalization == 'linear')
- self._normButtonLog.setChecked(normalization == 'log')
+ assert normalization in Colormap.NORMALIZATIONS
+ self._normButtonLinear.setChecked(normalization == Colormap.LINEAR)
+ self._normButtonLog.setChecked(normalization == Colormap.LOGARITHM)
if vmin is not None:
self._minValue.setValue(vmin)
diff --git a/silx/gui/plot/Colors.py b/silx/gui/plot/Colors.py
index 7a3cd97..2d44d4d 100644
--- a/silx/gui/plot/Colors.py
+++ b/silx/gui/plot/Colors.py
@@ -24,20 +24,18 @@
# ###########################################################################*/
"""Color conversion function, color dictionary and colormap tools."""
-__authors__ = ["V.A. Sole", "T. VINCENT"]
+from __future__ import absolute_import
+
+__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "16/01/2017"
+__date__ = "15/05/2017"
+from silx.utils.deprecation import deprecated
import logging
-
import numpy
-import matplotlib
-import matplotlib.colors
-import matplotlib.cm
-
-from . import MPLColormap
+from .Colormap import Colormap
_logger = logging.getLogger(__name__)
@@ -143,159 +141,7 @@ def cursorColorForColormap(colormapName):
return _COLORMAP_CURSOR_COLORS.get(colormapName, 'black')
-_CMAPS = {} # Store additional colormaps
-
-
-def getMPLColormap(name):
- """Returns matplotlib colormap corresponding to given name
-
- :param str name: The name of the colormap
- :return: The corresponding colormap
- :rtype: matplolib.colors.Colormap
- """
- if not _CMAPS: # Lazy initialization of own colormaps
- cdict = {'red': ((0.0, 0.0, 0.0),
- (1.0, 1.0, 1.0)),
- 'green': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0))}
- _CMAPS['red'] = matplotlib.colors.LinearSegmentedColormap(
- 'red', cdict, 256)
-
- cdict = {'red': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'green': ((0.0, 0.0, 0.0),
- (1.0, 1.0, 1.0)),
- 'blue': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0))}
- _CMAPS['green'] = matplotlib.colors.LinearSegmentedColormap(
- 'green', cdict, 256)
-
- cdict = {'red': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'green': ((0.0, 0.0, 0.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 0.0, 0.0),
- (1.0, 1.0, 1.0))}
- _CMAPS['blue'] = matplotlib.colors.LinearSegmentedColormap(
- 'blue', cdict, 256)
-
- # Temperature as defined in spslut
- cdict = {'red': ((0.0, 0.0, 0.0),
- (0.5, 0.0, 0.0),
- (0.75, 1.0, 1.0),
- (1.0, 1.0, 1.0)),
- 'green': ((0.0, 0.0, 0.0),
- (0.25, 1.0, 1.0),
- (0.75, 1.0, 1.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 1.0, 1.0),
- (0.25, 1.0, 1.0),
- (0.5, 0.0, 0.0),
- (1.0, 0.0, 0.0))}
- # but limited to 256 colors for a faster display (of the colorbar)
- _CMAPS['temperature'] = \
- matplotlib.colors.LinearSegmentedColormap(
- 'temperature', cdict, 256)
-
- # reversed gray
- cdict = {'red': ((0.0, 1.0, 1.0),
- (1.0, 0.0, 0.0)),
- 'green': ((0.0, 1.0, 1.0),
- (1.0, 0.0, 0.0)),
- 'blue': ((0.0, 1.0, 1.0),
- (1.0, 0.0, 0.0))}
-
- _CMAPS['reversed gray'] = \
- matplotlib.colors.LinearSegmentedColormap(
- 'yerg', cdict, 256)
-
- if name in _CMAPS:
- return _CMAPS[name]
- elif hasattr(MPLColormap, name): # viridis and sister colormaps
- return getattr(MPLColormap, name)
- else:
- # matplotlib built-in
- return matplotlib.cm.get_cmap(name)
-
-
-def getMPLScalarMappable(colormap, data=None):
- """Returns matplotlib ScalarMappable corresponding to colormap
-
- :param dict colormap: The colormap to convert
- :param numpy.ndarray data:
- The data on which the colormap is applied.
- If provided, it is used to compute autoscale.
- :return: matplotlib object corresponding to colormap
- :rtype: matplotlib.cm.ScalarMappable
- """
- assert colormap is not None
-
- if colormap['name'] is not None:
- cmap = getMPLColormap(colormap['name'])
-
- else: # No name, use custom colors
- if 'colors' not in colormap:
- raise ValueError(
- 'addImage: colormap no name nor list of colors.')
- colors = numpy.array(colormap['colors'], copy=True)
- assert len(colors.shape) == 2
- assert colors.shape[-1] in (3, 4)
- if colors.dtype == numpy.uint8:
- # Convert to float in [0., 1.]
- colors = colors.astype(numpy.float32) / 255.
- cmap = matplotlib.colors.ListedColormap(colors)
-
- if colormap['normalization'].startswith('log'):
- vmin, vmax = None, None
- if not colormap['autoscale']:
- if colormap['vmin'] > 0.:
- vmin = colormap['vmin']
- if colormap['vmax'] > 0.:
- vmax = colormap['vmax']
-
- if vmin is None or vmax is None:
- _logger.warning('Log colormap with negative bounds, ' +
- 'changing bounds to positive ones.')
- elif vmin > vmax:
- _logger.warning('Colormap bounds are inverted.')
- vmin, vmax = vmax, vmin
-
- # Set unset/negative bounds to positive bounds
- if (vmin is None or vmax is None) and data is not None:
- finiteData = data[numpy.isfinite(data)]
- posData = finiteData[finiteData > 0]
- if vmax is None:
- # 1. as an ultimate fallback
- vmax = posData.max() if posData.size > 0 else 1.
- if vmin is None:
- vmin = posData.min() if posData.size > 0 else vmax
- if vmin > vmax:
- vmin = vmax
-
- norm = matplotlib.colors.LogNorm(vmin, vmax)
-
- else: # Linear normalization
- if colormap['autoscale']:
- if data is None:
- vmin, vmax = None, None
- else:
- finiteData = data[numpy.isfinite(data)]
- vmin = finiteData.min()
- vmax = finiteData.max()
- else:
- vmin = colormap['vmin']
- vmax = colormap['vmax']
- if vmin > vmax:
- _logger.warning('Colormap bounds are inverted.')
- vmin, vmax = vmax, vmin
-
- norm = matplotlib.colors.Normalize(vmin, vmax)
-
- return matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap)
-
-
+@deprecated(replacement='silx.gui.plot.Colormap.applyColormap')
def applyColormapToData(data,
name='gray',
normalization='linear',
@@ -324,36 +170,19 @@ def applyColormapToData(data,
:return: The computed RGBA image
:rtype: numpy.ndarray of uint8
"""
- # Debian 7 specific support
- # No transparent colormap with matplotlib < 1.2.0
- # Add support for transparent colormap for uint8 data with
- # colormap with 256 colors, linear norm, [0, 255] range
- if matplotlib.__version__ < '1.2.0':
- if name is None and colors is not None:
- colors = numpy.array(colors, copy=False)
- if (colors.shape[-1] == 4 and
- not numpy.all(numpy.equal(colors[3], 255))):
- # This is a transparent colormap
- if (colors.shape == (256, 4) and
- normalization == 'linear' and
- not autoscale and
- vmin == 0 and vmax == 255 and
- data.dtype == numpy.uint8):
- # Supported case, convert data to RGBA
- return colors[data.reshape(-1)].reshape(
- data.shape + (4,))
- else:
- _logger.warning(
- 'matplotlib %s does not support transparent '
- 'colormap.', matplotlib.__version__)
-
- colormap = dict(name=name,
- normalization=normalization,
- autoscale=autoscale,
- vmin=vmin,
- vmax=vmax,
- colors=colors)
- scalarMappable = getMPLScalarMappable(colormap, data)
- rgbaImage = scalarMappable.to_rgba(data, bytes=True)
-
- return rgbaImage
+ colormap = Colormap(name=name,
+ normalization=normalization,
+ vmin=vmin,
+ vmax=vmax,
+ colors=colors)
+ return colormap.applyToData(data)
+
+
+@deprecated(replacement='silx.gui.plot.Colormap.getSupportedColormaps')
+def getSupportedColormaps():
+ """Get the supported colormap names as a tuple of str.
+
+ The list should at least contain and start by:
+ ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
+ """
+ return Colormap.getSupportedColormaps()
diff --git a/silx/gui/plot/ComplexImageView.py b/silx/gui/plot/ComplexImageView.py
new file mode 100644
index 0000000..1463293
--- /dev/null
+++ b/silx/gui/plot/ComplexImageView.py
@@ -0,0 +1,670 @@
+# 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 a widget to view 2D complex data.
+
+The :class:`ComplexImageView` widget is dedicated to visualize a single 2D dataset
+of complex data.
+"""
+
+from __future__ import absolute_import
+
+__authors__ = ["Vincent Favre-Nicolin", "T. Vincent"]
+__license__ = "MIT"
+__date__ = "02/10/2017"
+
+
+import logging
+import numpy
+
+from .. import qt, icons
+from .PlotWindow import Plot2D
+from .Colormap import Colormap
+from . import items
+from silx.gui.widgets.FloatEdit import FloatEdit
+
+_logger = logging.getLogger(__name__)
+
+
+_PHASE_COLORMAP = Colormap(
+ name='hsv',
+ vmin=-numpy.pi,
+ vmax=numpy.pi)
+"""Colormap to use for phase"""
+
+# Complex colormap functions
+
+def _phase2rgb(data):
+ """Creates RGBA image with colour-coded phase.
+
+ :param numpy.ndarray data: The data to convert
+ :return: Array of RGBA colors
+ :rtype: numpy.ndarray
+ """
+ if data.size == 0:
+ return numpy.zeros((0, 0, 4), dtype=numpy.uint8)
+
+ phase = numpy.angle(data)
+ return _PHASE_COLORMAP.applyToData(phase)
+
+
+def _complex2rgbalog(data, amin=0., dlogs=2, smax=None):
+ """Returns RGBA colors: colour-coded phases and log10(amplitude) in alpha.
+
+ :param numpy.ndarray data: the complex data array to convert to RGBA
+ :param float amin: the minimum value for the alpha channel
+ :param float dlogs: amplitude range displayed, in log10 units
+ :param float smax:
+ if specified, all values above max will be displayed with an alpha=1
+ """
+ if data.size == 0:
+ return numpy.zeros((0, 0, 4), dtype=numpy.uint8)
+
+ rgba = _phase2rgb(data)
+ sabs = numpy.absolute(data)
+ if smax is not None:
+ sabs[sabs > smax] = smax
+ a = numpy.log10(sabs + 1e-20)
+ a -= a.max() - dlogs # display dlogs orders of magnitude
+ rgba[..., 3] = 255 * (amin + a / dlogs * (1 - amin) * (a > 0))
+ return rgba
+
+
+def _complex2rgbalin(data, gamma=1.0, smax=None):
+ """Returns RGBA colors: colour-coded phase and linear amplitude in alpha.
+
+ :param numpy.ndarray data:
+ :param float gamma: Optional exponent gamma applied to the amplitude
+ :param float smax:
+ """
+ if data.size == 0:
+ return numpy.zeros((0, 0, 4), dtype=numpy.uint8)
+
+ rgba = _phase2rgb(data)
+ a = numpy.absolute(data)
+ if smax is not None:
+ a[a > smax] = smax
+ a /= a.max()
+ rgba[..., 3] = 255 * a**gamma
+ return rgba
+
+
+# Dedicated plot item
+
+class _ImageComplexData(items.ImageData):
+ """Specific plot item to force colormap when using complex colormap.
+
+ This is returning the specific colormap when displaying
+ colored phase + amplitude.
+ """
+
+ def __init__(self):
+ super(_ImageComplexData, self).__init__()
+ self._readOnlyColormap = False
+ self._mode = 'absolute'
+ self._colormaps = { # Default colormaps for all modes
+ 'absolute': Colormap(),
+ 'phase': _PHASE_COLORMAP.copy(),
+ 'real': Colormap(),
+ 'imaginary': Colormap(),
+ 'amplitude_phase': _PHASE_COLORMAP.copy(),
+ 'log10_amplitude_phase': _PHASE_COLORMAP.copy(),
+ }
+
+ _READ_ONLY_MODES = 'amplitude_phase', 'log10_amplitude_phase'
+ """Modes that requires a read-only colormap."""
+
+ def setVisualizationMode(self, mode):
+ """Set the visualization mode to use.
+
+ :param str mode:
+ """
+ mode = str(mode)
+ assert mode in self._colormaps
+
+ if mode != self._mode:
+ # Save current colormap
+ self._colormaps[self._mode] = self.getColormap()
+ self._mode = mode
+
+ # Set colormap for new mode
+ self.setColormap(self._colormaps[mode])
+
+ def getVisualizationMode(self):
+ """Returns the visualization mode in use."""
+ return self._mode
+
+ def _isReadOnlyColormap(self):
+ """Returns True if colormap should not be modified."""
+ return self.getVisualizationMode() in self._READ_ONLY_MODES
+
+ def setColormap(self, colormap):
+ if not self._isReadOnlyColormap():
+ super(_ImageComplexData, self).setColormap(colormap)
+
+ def getColormap(self):
+ if self._isReadOnlyColormap():
+ return _PHASE_COLORMAP.copy()
+ else:
+ return super(_ImageComplexData, self).getColormap()
+
+
+# Widgets
+
+class _AmplitudeRangeDialog(qt.QDialog):
+ """QDialog asking for the amplitude range to display."""
+
+ sigRangeChanged = qt.Signal(tuple)
+ """Signal emitted when the range has changed.
+
+ It provides the new range as a 2-tuple: (max, delta)
+ """
+
+ def __init__(self,
+ parent=None,
+ amplitudeRange=None,
+ displayedRange=(None, 2)):
+ super(_AmplitudeRangeDialog, self).__init__(parent)
+ self.setWindowTitle('Set Displayed Amplitude Range')
+
+ if amplitudeRange is not None:
+ amplitudeRange = min(amplitudeRange), max(amplitudeRange)
+ self._amplitudeRange = amplitudeRange
+ self._defaultDisplayedRange = displayedRange
+
+ layout = qt.QFormLayout()
+ self.setLayout(layout)
+
+ if self._amplitudeRange is not None:
+ min_, max_ = self._amplitudeRange
+ layout.addRow(
+ qt.QLabel('Data Amplitude Range: [%g, %g]' % (min_, max_)))
+
+ self._maxLineEdit = FloatEdit(parent=self)
+ self._maxLineEdit.validator().setBottom(0.)
+ self._maxLineEdit.setAlignment(qt.Qt.AlignRight)
+
+ self._maxLineEdit.editingFinished.connect(self._rangeUpdated)
+ layout.addRow('Displayed Max.:', self._maxLineEdit)
+
+ self._autoscale = qt.QCheckBox('autoscale')
+ self._autoscale.toggled.connect(self._autoscaleCheckBoxToggled)
+ layout.addRow('', self._autoscale)
+
+ self._deltaLineEdit = FloatEdit(parent=self)
+ self._deltaLineEdit.validator().setBottom(1.)
+ self._deltaLineEdit.setAlignment(qt.Qt.AlignRight)
+ self._deltaLineEdit.editingFinished.connect(self._rangeUpdated)
+ layout.addRow('Displayed delta (log10 unit):', self._deltaLineEdit)
+
+ buttons = qt.QDialogButtonBox(self)
+ buttons.addButton(qt.QDialogButtonBox.Ok)
+ buttons.addButton(qt.QDialogButtonBox.Cancel)
+ buttons.accepted.connect(self.accept)
+ buttons.rejected.connect(self.reject)
+ layout.addRow(buttons)
+
+ # Set dialog from default values
+ self._resetDialogToDefault()
+
+ self.rejected.connect(self._handleRejected)
+
+ def _resetDialogToDefault(self):
+ """Set Widgets of the dialog from range information
+ """
+ max_, delta = self._defaultDisplayedRange
+
+ if max_ is not None: # Not in autoscale
+ displayedMax = max_
+ elif self._amplitudeRange is not None: # Autoscale with data
+ displayedMax = self._amplitudeRange[1]
+ else: # Autoscale without data
+ displayedMax = ''
+ if displayedMax == "":
+ self._maxLineEdit.setText("")
+ else:
+ self._maxLineEdit.setValue(displayedMax)
+ self._maxLineEdit.setEnabled(max_ is not None)
+
+ self._deltaLineEdit.setValue(delta)
+
+ self._autoscale.setChecked(self._defaultDisplayedRange[0] is None)
+
+ def getRangeInfo(self):
+ """Returns the current range as a 2-tuple (max, delta (in log10))"""
+ if self._autoscale.isChecked():
+ max_ = None
+ else:
+ maxStr = self._maxLineEdit.text()
+ max_ = self._maxLineEdit.value() if maxStr else None
+ return max_, self._deltaLineEdit.value() if self._deltaLineEdit.text() else 2
+
+ def _handleRejected(self):
+ """Reset range info to default when rejected"""
+ self._resetDialogToDefault()
+ self._rangeUpdated()
+
+ def _rangeUpdated(self):
+ """Handle QLineEdit editing finised"""
+ self.sigRangeChanged.emit(self.getRangeInfo())
+
+ def _autoscaleCheckBoxToggled(self, checked):
+ """Handle autoscale checkbox state changes"""
+ if checked: # Use default values
+ if self._amplitudeRange is None:
+ max_ = ''
+ else:
+ max_ = self._amplitudeRange[1]
+ if max_ == "":
+ self._maxLineEdit.setText("")
+ else:
+ self._maxLineEdit.setValue(max_)
+ self._maxLineEdit.setEnabled(not checked)
+ self._rangeUpdated()
+
+
+class _ComplexDataToolButton(qt.QToolButton):
+ """QToolButton providing choices of complex data visualization modes
+
+ :param parent: See :class:`QToolButton`
+ :param plot: The :class:`ComplexImageView` to control
+ """
+
+ _MODES = [
+ ('absolute', 'math-amplitude', 'Amplitude'),
+ ('phase', 'math-phase', 'Phase'),
+ ('real', 'math-real', 'Real part'),
+ ('imaginary', 'math-imaginary', 'Imaginary part'),
+ ('amplitude_phase', 'math-phase-color', 'Amplitude and Phase'),
+ ('log10_amplitude_phase', 'math-phase-color-log', 'Log10(Amp.) and Phase')]
+
+ _RANGE_DIALOG_TEXT = 'Set Amplitude Range...'
+
+ def __init__(self, parent=None, plot=None):
+ super(_ComplexDataToolButton, self).__init__(parent=parent)
+
+ assert plot is not None
+ self._plot2DComplex = plot
+
+ menu = qt.QMenu(self)
+ menu.triggered.connect(self._triggered)
+ self.setMenu(menu)
+
+ for _, icon, text in self._MODES:
+ action = qt.QAction(icons.getQIcon(icon), text, self)
+ action.setIconVisibleInMenu(True)
+ menu.addAction(action)
+
+ self._rangeDialogAction = qt.QAction(self)
+ self._rangeDialogAction.setText(self._RANGE_DIALOG_TEXT)
+ menu.addAction(self._rangeDialogAction)
+
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+
+ self._modeChanged(self._plot2DComplex.getVisualizationMode())
+ self._plot2DComplex.sigVisualizationModeChanged.connect(
+ self._modeChanged)
+
+ def _modeChanged(self, mode):
+ """Handle change of visualization modes"""
+ for actionMode, icon, text in self._MODES:
+ if actionMode == mode:
+ self.setIcon(icons.getQIcon(icon))
+ self.setToolTip('Display the ' + text.lower())
+ break
+
+ self._rangeDialogAction.setEnabled(mode == 'log10_amplitude_phase')
+
+ def _triggered(self, action):
+ """Handle triggering of menu actions"""
+ actionText = action.text()
+
+ if actionText == self._RANGE_DIALOG_TEXT: # Show dialog
+ # Get amplitude range
+ data = self._plot2DComplex.getData(copy=False)
+
+ if data.size > 0:
+ absolute = numpy.absolute(data)
+ dataRange = (numpy.nanmin(absolute), numpy.nanmax(absolute))
+ else:
+ dataRange = None
+
+ # Show dialog
+ dialog = _AmplitudeRangeDialog(
+ parent=self,
+ amplitudeRange=dataRange,
+ displayedRange=self._plot2DComplex._getAmplitudeRangeInfo())
+ dialog.sigRangeChanged.connect(self._rangeChanged)
+ dialog.exec_()
+ dialog.sigRangeChanged.disconnect(self._rangeChanged)
+
+ else: # update mode
+ for mode, _, text in self._MODES:
+ if actionText == text:
+ self._plot2DComplex.setVisualizationMode(mode)
+
+ def _rangeChanged(self, range_):
+ """Handle updates of range in the dialog"""
+ self._plot2DComplex._setAmplitudeRangeInfo(*range_)
+
+
+class ComplexImageView(qt.QWidget):
+ """Display an image of complex data and allow to choose the visualization.
+
+ :param parent: See :class:`QMainWindow`
+ """
+
+ sigDataChanged = qt.Signal()
+ """Signal emitted when data has changed."""
+
+ sigVisualizationModeChanged = qt.Signal(str)
+ """Signal emitted when the visualization mode has changed.
+
+ It provides the new visualization mode.
+ """
+
+ def __init__(self, parent=None):
+ super(ComplexImageView, self).__init__(parent)
+ if parent is None:
+ self.setWindowTitle('ComplexImageView')
+
+ self._mode = 'absolute'
+ self._amplitudeRangeInfo = None, 2
+ self._data = numpy.zeros((0, 0), dtype=numpy.complex)
+ self._displayedData = numpy.zeros((0, 0), dtype=numpy.float)
+
+ self._plot2D = Plot2D(self)
+
+ layout = qt.QHBoxLayout(self)
+ layout.setSpacing(0)
+ layout.setContentsMargins(0, 0, 0, 0)
+ layout.addWidget(self._plot2D)
+ self.setLayout(layout)
+
+ # Create and add image to the plot
+ self._plotImage = _ImageComplexData()
+ self._plotImage._setLegend('__ComplexImageView__complex_image__')
+ self._plotImage.setData(self._displayedData)
+ self._plotImage.setVisualizationMode(self._mode)
+ self._plot2D._add(self._plotImage)
+ self._plot2D.setActiveImage(self._plotImage.getLegend())
+
+ toolBar = qt.QToolBar('Complex', self)
+ toolBar.addWidget(
+ _ComplexDataToolButton(parent=self, plot=self))
+
+ self._plot2D.insertToolBar(self._plot2D.getProfileToolbar(), toolBar)
+
+ def getPlot(self):
+ """Return the PlotWidget displaying the data"""
+ return self._plot2D
+
+ def _convertData(self, data, mode):
+ """Convert complex data according to provided mode.
+
+ :param numpy.ndarray data: The complex data to convert
+ :param str mode: The visualization mode
+ :return: The data corresponding to the mode
+ :rtype: 2D numpy.ndarray of float or RGBA image
+ """
+ if mode == 'absolute':
+ return numpy.absolute(data)
+ elif mode == 'phase':
+ return numpy.angle(data)
+ elif mode == 'real':
+ return numpy.real(data)
+ elif mode == 'imaginary':
+ return numpy.imag(data)
+ elif mode == 'amplitude_phase':
+ return _complex2rgbalin(data)
+ elif mode == 'log10_amplitude_phase':
+ max_, delta = self._getAmplitudeRangeInfo()
+ return _complex2rgbalog(data, dlogs=delta, smax=max_)
+ else:
+ _logger.error(
+ 'Unsupported conversion mode: %s, fallback to absolute',
+ str(mode))
+ return numpy.absolute(data)
+
+ def _updatePlot(self):
+ """Update the image in the plot"""
+
+ mode = self.getVisualizationMode()
+
+ self.getPlot().getColormapAction().setDisabled(
+ mode in ('amplitude_phase', 'log10_amplitude_phase'))
+
+ self._plotImage.setVisualizationMode(mode)
+
+ image = self.getDisplayedData(copy=False)
+ if mode in ('amplitude_phase', 'log10_amplitude_phase'):
+ # Combined view
+ absolute = numpy.absolute(self.getData(copy=False))
+ self._plotImage.setData(
+ absolute, alternative=image, copy=False)
+ else:
+ self._plotImage.setData(
+ image, alternative=None, copy=False)
+
+ def setData(self, data=None, copy=True):
+ """Set the complex data to display.
+
+ :param numpy.ndarray data: 2D complex data
+ :param bool copy: True (default) to copy the data,
+ False to use provided data (do not modify!).
+ """
+ if data is None:
+ data = numpy.zeros((0, 0), dtype=numpy.complex)
+ else:
+ data = numpy.array(data, copy=copy)
+
+ assert data.ndim == 2
+ if data.dtype.kind != 'c': # Convert to complex
+ data = numpy.array(data, dtype=numpy.complex)
+ shape_changed = (self._data.shape != data.shape)
+ self._data = data
+ self._displayedData = self._convertData(
+ data, self.getVisualizationMode())
+
+ self._updatePlot()
+ if shape_changed:
+ self.getPlot().resetZoom()
+
+ self.sigDataChanged.emit()
+
+ def getData(self, copy=True):
+ """Get the currently displayed complex data.
+
+ :param bool copy: True (default) to return a copy of the data,
+ False to return internal data (do not modify!).
+ :return: The complex data array.
+ :rtype: numpy.ndarray of complex with 2 dimensions
+ """
+ return numpy.array(self._data, copy=copy)
+
+ def getDisplayedData(self, copy=True):
+ """Returns the displayed data depending on the visualization mode
+
+ WARNING: The returned data can be a uint8 RGBA image
+
+ :param bool copy: True (default) to return a copy of the data,
+ False to return internal data (do not modify!)
+ :rtype: numpy.ndarray of float with 2 dims or RGBA image (uint8).
+ """
+ return numpy.array(self._displayedData, copy=copy)
+
+ @staticmethod
+ def getSupportedVisualizationModes():
+ """Returns the supported visualization modes.
+
+ Supported visualization modes are:
+
+ - amplitude: The absolute value provided by numpy.absolute
+ - phase: The phase (or argument) provided by numpy.angle
+ - real: Real part
+ - imaginary: Imaginary part
+ - amplitude_phase: Color-coded phase with amplitude as alpha.
+ - log10_amplitude_phase:
+ Color-coded phase with log10(amplitude) as alpha.
+
+ :rtype: tuple of str
+ """
+ return ('absolute',
+ 'phase',
+ 'real',
+ 'imaginary',
+ 'amplitude_phase',
+ 'log10_amplitude_phase')
+
+ def setVisualizationMode(self, mode):
+ """Set the mode of visualization of the complex data.
+
+ See :meth:`getSupportedVisualizationModes` for the list of
+ supported modes.
+
+ :param str mode: The mode to use.
+ """
+ assert mode in self.getSupportedVisualizationModes()
+ if mode != self._mode:
+ self._mode = mode
+ self._displayedData = self._convertData(
+ self.getData(copy=False), mode)
+ self._updatePlot()
+ self.sigVisualizationModeChanged.emit(mode)
+
+ def getVisualizationMode(self):
+ """Get the current visualization mode of the complex data.
+
+ :rtype: str
+ """
+ return self._mode
+
+ def _setAmplitudeRangeInfo(self, max_=None, delta=2):
+ """Set the amplitude range to display for 'log10_amplitude_phase' mode.
+
+ :param max_: Max of the amplitude range.
+ If None it autoscales to data max.
+ :param float delta: Delta range in log10 to display
+ """
+ self._amplitudeRangeInfo = max_, float(delta)
+ mode = self.getVisualizationMode()
+ if mode == 'log10_amplitude_phase':
+ self._displayedData = self._convertData(
+ self.getData(copy=False), mode)
+ self._updatePlot()
+
+ def _getAmplitudeRangeInfo(self):
+ """Returns the amplitude range to use for 'log10_amplitude_phase' mode.
+
+ :return: (max, delta), if max is None, then it autoscales to data max
+ :rtype: 2-tuple"""
+ return self._amplitudeRangeInfo
+
+ # Image item proxy
+
+ def setColormap(self, colormap):
+ """Set the colormap to use for amplitude, phase, real or imaginary.
+
+ WARNING: This colormap is not used when displaying both
+ amplitude and phase.
+
+ :param Colormap colormap: The colormap
+ """
+ self._plotImage.setColormap(colormap)
+
+ def getColormap(self):
+ """Returns the colormap used to display the data.
+
+ :rtype: Colormap
+ """
+ # Returns internal colormap and bypass forcing colormap
+ return items.ImageData.getColormap(self._plotImage)
+
+ def getOrigin(self):
+ """Returns the offset from origin at which to display the image.
+
+ :rtype: 2-tuple of float
+ """
+ return self._plotImage.getOrigin()
+
+ def setOrigin(self, origin):
+ """Set the offset from origin at which to display the image.
+
+ :param origin: (ox, oy) Offset from origin
+ :type origin: float or 2-tuple of float
+ """
+ self._plotImage.setOrigin(origin)
+
+ def getScale(self):
+ """Returns the scale of the image in data coordinates.
+
+ :rtype: 2-tuple of float
+ """
+ return self._plotImage.getScale()
+
+ def setScale(self, scale):
+ """Set the scale of the image
+
+ :param scale: (sx, sy) Scale of the image
+ :type scale: float or 2-tuple of float
+ """
+ self._plotImage.setScale(scale)
+
+ # PlotWidget API proxy
+
+ def getXAxis(self):
+ """Returns the X axis
+
+ :rtype: :class:`.items.Axis`
+ """
+ return self.getPlot().getXAxis()
+
+ def getYAxis(self):
+ """Returns an Y axis
+
+ :rtype: :class:`.items.Axis`
+ """
+ return self.getPlot().getYAxis(axis='left')
+
+ def getGraphTitle(self):
+ """Return the plot main title as a str."""
+ return self.getPlot().getGraphTitle()
+
+ def setGraphTitle(self, title=""):
+ """Set the plot main title.
+
+ :param str title: Main title of the plot (default: '')
+ """
+ self.getPlot().setGraphTitle(title)
+
+ def setKeepDataAspectRatio(self, flag):
+ """Set whether the plot keeps data aspect ratio or not.
+
+ :param bool flag: True to respect data aspect ratio
+ """
+ self.getPlot().setKeepDataAspectRatio(flag)
+
+ def isKeepDataAspectRatio(self):
+ """Returns whether the plot is keeping data aspect ratio or not."""
+ return self.getPlot().isKeepDataAspectRatio()
diff --git a/silx/gui/plot/CurvesROIWidget.py b/silx/gui/plot/CurvesROIWidget.py
index 13c3de0..4b10cd6 100644
--- a/silx/gui/plot/CurvesROIWidget.py
+++ b/silx/gui/plot/CurvesROIWidget.py
@@ -46,7 +46,7 @@ ROI are defined by :
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "26/04/2017"
+__date__ = "27/06/2017"
from collections import OrderedDict
@@ -168,9 +168,9 @@ class CurvesROIWidget(qt.QWidget):
The dictionary keys are the ROI names.
Each value is a sub-dictionary of ROI info with the following fields:
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
+ - ``"from"``: x coordinate of the left limit, as a float
+ - ``"to"``: x coordinate of the right limit, as a float
+ - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
:param roidict: Dictionary of ROIs
@@ -194,9 +194,11 @@ class CurvesROIWidget(qt.QWidget):
The dictionary keys are the ROI names.
Each value is a sub-dictionary of ROI info with the following fields:
- - ``"from"``: x coordinate of the left limit, as a float
- - ``"to"``: x coordinate of the right limit, as a float
- - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
+ - ``"from"``: x coordinate of the left limit, as a float
+ - ``"to"``: x coordinate of the right limit, as a float
+ - ``"type"``: type of ROI, as a string (e.g "channels", "energy")
+
+
:param order: Field used for ordering the ROIs.
One of "from", "to", "type", "netcounts", "rawcounts".
None (default) to get the same order as displayed in the widget.
@@ -742,7 +744,7 @@ class CurvesROIDockWidget(qt.QDockWidget):
"""Handle ROI widget signal"""
_logger.debug("PlotWindow._roiSignal %s", str(ddict))
if ddict['event'] == "AddROI":
- xmin, xmax = self.plot.getGraphXLimits()
+ xmin, xmax = self.plot.getXAxis().getLimits()
fromdata = xmin + 0.25 * (xmax - xmin)
todata = xmin + 0.75 * (xmax - xmin)
self.plot.remove('ROI min', kind='marker')
@@ -786,7 +788,7 @@ class CurvesROIDockWidget(qt.QDockWidget):
if newroi == "ICR":
roiDict[newroi]['type'] = "Default"
else:
- roiDict[newroi]['type'] = self.plot.getGraphXLabel()
+ roiDict[newroi]['type'] = self.plot.getXAxis().getLabel()
roiDict[newroi]['from'] = fromdata
roiDict[newroi]['to'] = todata
self.roiWidget.fillFromROIDict(roilist=roiList,
diff --git a/silx/gui/plot/ImageView.py b/silx/gui/plot/ImageView.py
index 780215e..803a2fc 100644
--- a/silx/gui/plot/ImageView.py
+++ b/silx/gui/plot/ImageView.py
@@ -34,12 +34,7 @@ Basic usage of :class:`ImageView` is through the following methods:
default colormap to use and update the currently displayed image.
- :meth:`ImageView.setImage` to update the displayed image.
-The :class:`ImageView` uses :class:`PlotWindow` and also
-exposes :class:`silx.gui.plot.Plot` API for further control
-(plot title, axes labels, adding other images, ...).
-
-For an example of use, see the implementation of :class:`ImageViewMainWindow`,
-and `example/imageview.py`.
+For an example of use, see `imageview.py` in :ref:`sample-code`.
"""
from __future__ import division
@@ -47,7 +42,7 @@ from __future__ import division
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "13/10/2016"
+__date__ = "17/08/2017"
import logging
@@ -55,7 +50,8 @@ import numpy
from .. import qt
-from . import items, PlotWindow, PlotWidget, PlotActions
+from . import items, PlotWindow, PlotWidget, actions
+from .Colormap import Colormap
from .Colors import cursorColorForColormap
from .PlotTools import LimitsToolBar
from .Profile import ProfileToolBar
@@ -253,9 +249,13 @@ class ImageView(PlotWindow):
Use :meth:`setImage` to control the displayed image.
This class also provides the :class:`silx.gui.plot.Plot` API.
+ The :class:`ImageView` inherits from :class:`.PlotWindow` (which provides
+ the toolbars) and also exposes :class:`.PlotWidget` API for further
+ plot control (plot title, axes labels, aspect ratio, ...).
+
:param parent: The parent of this widget or None.
:param backend: The backend to use for the plot (default: matplotlib).
- See :class:`.Plot` for the list of supported backend.
+ See :class:`.PlotWidget` for the list of supported backend.
:type backend: str or :class:`BackendBase.BackendBase`
"""
@@ -318,7 +318,7 @@ class ImageView(PlotWindow):
self._histoHPlot = PlotWidget(backend=backend)
self._histoHPlot.setInteractiveMode('zoom')
- self._histoHPlot.setCallback(self._histoHPlotCB)
+ self._histoHPlot.sigPlotSignal.connect(self._histoHPlotCB)
self._histoHPlot.getWidgetHandle().sizeHint = sizeHint
self._histoHPlot.getWidgetHandle().minimumSizeHint = sizeHint
@@ -326,39 +326,39 @@ class ImageView(PlotWindow):
self.setInteractiveMode('zoom') # Color set in setColormap
self.sigPlotSignal.connect(self._imagePlotCB)
- self.sigSetYAxisInverted.connect(self._updateYAxisInverted)
+ self.getYAxis().sigInvertedChanged.connect(self._updateYAxisInverted)
self.sigActiveImageChanged.connect(self._activeImageChangedSlot)
self._histoVPlot = PlotWidget(backend=backend)
self._histoVPlot.setInteractiveMode('zoom')
- self._histoVPlot.setCallback(self._histoVPlotCB)
+ self._histoVPlot.sigPlotSignal.connect(self._histoVPlotCB)
self._histoVPlot.getWidgetHandle().sizeHint = sizeHint
self._histoVPlot.getWidgetHandle().minimumSizeHint = sizeHint
self._radarView = RadarView()
self._radarView.visibleRectDragged.connect(self._radarViewCB)
- self._layout = qt.QGridLayout()
- self._layout.addWidget(self.getWidgetHandle(), 0, 0)
- self._layout.addWidget(self._histoVPlot.getWidgetHandle(), 0, 1)
- self._layout.addWidget(self._histoHPlot.getWidgetHandle(), 1, 0)
- self._layout.addWidget(self._radarView, 1, 1)
+ layout = qt.QGridLayout()
+ layout.addWidget(self.getWidgetHandle(), 0, 0)
+ layout.addWidget(self._histoVPlot.getWidgetHandle(), 0, 1)
+ layout.addWidget(self._histoHPlot.getWidgetHandle(), 1, 0)
+ layout.addWidget(self._radarView, 1, 1)
- self._layout.setColumnMinimumWidth(0, self.IMAGE_MIN_SIZE)
- self._layout.setColumnStretch(0, 1)
- self._layout.setColumnMinimumWidth(1, self.HISTOGRAMS_HEIGHT)
- self._layout.setColumnStretch(1, 0)
+ layout.setColumnMinimumWidth(0, self.IMAGE_MIN_SIZE)
+ layout.setColumnStretch(0, 1)
+ layout.setColumnMinimumWidth(1, self.HISTOGRAMS_HEIGHT)
+ layout.setColumnStretch(1, 0)
- self._layout.setRowMinimumHeight(0, self.IMAGE_MIN_SIZE)
- self._layout.setRowStretch(0, 1)
- self._layout.setRowMinimumHeight(1, self.HISTOGRAMS_HEIGHT)
- self._layout.setRowStretch(1, 0)
+ layout.setRowMinimumHeight(0, self.IMAGE_MIN_SIZE)
+ layout.setRowStretch(0, 1)
+ layout.setRowMinimumHeight(1, self.HISTOGRAMS_HEIGHT)
+ layout.setRowStretch(1, 0)
- self._layout.setSpacing(0)
- self._layout.setContentsMargins(0, 0, 0, 0)
+ layout.setSpacing(0)
+ layout.setContentsMargins(0, 0, 0, 0)
centralWidget = qt.QWidget()
- centralWidget.setLayout(self._layout)
+ centralWidget.setLayout(layout)
self.setCentralWidget(centralWidget)
def _dirtyCache(self):
@@ -376,8 +376,8 @@ class ImageView(PlotWindow):
scale = activeImage.getScale()
height, width = data.shape
- xMin, xMax = self.getGraphXLimits()
- yMin, yMax = self.getGraphYLimits()
+ xMin, xMax = self.getXAxis().getLimits()
+ yMin, yMax = self.getYAxis().getLimits()
# Convert plot area limits to image coordinates
# and work in image coordinates (i.e., in pixels)
@@ -440,8 +440,8 @@ class ImageView(PlotWindow):
vOffset = 0.1 * (vMax - vMin)
if vOffset == 0.:
vOffset = 1.
- self._histoHPlot.setGraphYLimits(vMin - vOffset,
- vMax + vOffset)
+ self._histoHPlot.getYAxis().setLimits(vMin - vOffset,
+ vMax + vOffset)
coords = numpy.arange(2 * histoVVisibleData.size)
yCoords = (coords + 1) // 2 + subsetYMin
@@ -458,8 +458,8 @@ class ImageView(PlotWindow):
vOffset = 0.1 * (vMax - vMin)
if vOffset == 0.:
vOffset = 1.
- self._histoVPlot.setGraphXLimits(vMin - vOffset,
- vMax + vOffset)
+ self._histoVPlot.getXAxis().setLimits(vMin - vOffset,
+ vMax + vOffset)
else:
self._dirtyCache()
self._histoHPlot.remove(kind='curve')
@@ -472,8 +472,8 @@ class ImageView(PlotWindow):
Takes care of y coordinate conversion.
"""
- xMin, xMax = self.getGraphXLimits()
- yMin, yMax = self.getGraphYLimits()
+ xMin, xMax = self.getXAxis().getLimits()
+ yMin, yMax = self.getYAxis().getLimits()
self._radarView.setVisibleRect(xMin, yMin, xMax - xMin, yMax - yMin)
# Plots event listeners
@@ -499,6 +499,9 @@ class ImageView(PlotWindow):
data[y][x])
elif eventDict['event'] == 'limitsChanged':
+ self._updateHistogramsLimits()
+
+ def _updateHistogramsLimits(self):
# Do not handle histograms limitsChanged while
# updating their limits from here.
self._updatingLimits = True
@@ -506,15 +509,14 @@ class ImageView(PlotWindow):
# Refresh histograms
self._updateHistograms()
- # could use eventDict['xdata'], eventDict['ydata'] instead
- xMin, xMax = self.getGraphXLimits()
- yMin, yMax = self.getGraphYLimits()
+ xMin, xMax = self.getXAxis().getLimits()
+ yMin, yMax = self.getYAxis().getLimits()
# Set horizontal histo limits
- self._histoHPlot.setGraphXLimits(xMin, xMax)
+ self._histoHPlot.getXAxis().setLimits(xMin, xMax)
# Set vertical histo limits
- self._histoVPlot.setGraphYLimits(yMin, yMax)
+ self._histoVPlot.getYAxis().setLimits(yMin, yMax)
self._updateRadarView()
@@ -542,9 +544,9 @@ class ImageView(PlotWindow):
elif eventDict['event'] == 'limitsChanged':
if (not self._updatingLimits and
- eventDict['xdata'] != self.getGraphXLimits()):
+ eventDict['xdata'] != self.getXAxis().getLimits()):
xMin, xMax = eventDict['xdata']
- self.setGraphXLimits(xMin, xMax)
+ self.getXAxis().setLimits(xMin, xMax)
def _histoVPlotCB(self, eventDict):
"""Callback for vertical histogram plot events."""
@@ -568,9 +570,9 @@ class ImageView(PlotWindow):
elif eventDict['event'] == 'limitsChanged':
if (not self._updatingLimits and
- eventDict['ydata'] != self.getGraphYLimits()):
+ eventDict['ydata'] != self.getYAxis().getLimits()):
yMin, yMax = eventDict['ydata']
- self.setGraphYLimits(yMin, yMax)
+ self.getYAxis().setLimits(yMin, yMax)
def _radarViewCB(self, left, top, width, height):
"""Slot for radar view visible rectangle changes."""
@@ -582,9 +584,9 @@ class ImageView(PlotWindow):
"""Sync image, vertical histogram and radar view axis orientation."""
if inverted is None:
# Do not perform this when called from plot signal
- inverted = self.isYAxisInverted()
+ inverted = self.getYAxis().isInverted()
- self._histoVPlot.setYAxisInverted(inverted)
+ self._histoVPlot.getYAxis().setInverted(inverted)
# Use scale to invert radarView
# RadarView default Y direction is from top to bottom
@@ -643,7 +645,7 @@ class ImageView(PlotWindow):
self._radarView.visibleRectDragged.disconnect(self._radarViewCB)
self._radarView = radarView
self._radarView.visibleRectDragged.connect(self._radarViewCB)
- self._layout.addWidget(self._radarView, 1, 1)
+ self.centralWidget().layout().addWidget(self._radarView, 1, 1)
self._updateYAxisInverted()
@@ -693,42 +695,46 @@ class ImageView(PlotWindow):
:param numpy.ndarray colors: Only used if name is None.
Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
"""
- cmapDict = self.getDefaultColormap()
+ cmap = self.getDefaultColormap()
+
+ if isinstance(colormap, Colormap):
+ # Replace colormap
+ cmap = colormap
+
+ self.setDefaultColormap(cmap)
+
+ # Update active image colormap
+ activeImage = self.getActiveImage()
+ if isinstance(activeImage, items.ColormapMixIn):
+ activeImage.setColormap(cmap)
- if isinstance(colormap, dict):
+ elif isinstance(colormap, dict):
# Support colormap parameter as a dict
assert normalization is None
assert autoscale is None
assert vmin is None
assert vmax is None
assert colors is None
- for key, value in colormap.items():
- cmapDict[key] = value
+ cmap._setFromDict(colormap)
else:
if colormap is not None:
- cmapDict['name'] = colormap
+ cmap.setName(colormap)
if normalization is not None:
- cmapDict['normalization'] = normalization
- if autoscale is not None:
- cmapDict['autoscale'] = autoscale
- if vmin is not None:
- cmapDict['vmin'] = vmin
- if vmax is not None:
- cmapDict['vmax'] = vmax
+ cmap.setNormalization(normalization)
+ if autoscale:
+ cmap.setVRange(None, None)
+ else:
+ if vmin is not None:
+ cmap.setVMin(vmin)
+ if vmax is not None:
+ cmap.setVMax(vmax)
if colors is not None:
- cmapDict['colors'] = colors
+ cmap.setColormapLUT(colors)
- cursorColor = cursorColorForColormap(cmapDict['name'])
+ cursorColor = cursorColorForColormap(cmap.getName())
self.setInteractiveMode('zoom', color=cursorColor)
- self.setDefaultColormap(cmapDict)
-
- # Update active image colormap
- activeImage = self.getActiveImage()
- if isinstance(activeImage, items.ColormapMixIn):
- activeImage.setColormap(self.getColormap())
-
def setImage(self, image, origin=(0, 0), scale=(1., 1.),
copy=True, reset=True):
"""Set the image to display.
@@ -768,7 +774,7 @@ class ImageView(PlotWindow):
legend=self._imageLegend,
origin=origin, scale=scale,
colormap=self.getColormap(),
- replace=False)
+ replace=False, resetzoom=False)
self.setActiveImage(self._imageLegend)
self._updateHistograms()
@@ -779,6 +785,8 @@ class ImageView(PlotWindow):
if reset:
self.resetZoom()
+ else:
+ self._updateHistogramsLimits()
# ImageViewMainWindow #########################################################
@@ -793,8 +801,8 @@ class ImageViewMainWindow(ImageView):
super(ImageViewMainWindow, self).__init__(parent, backend)
self.setWindowFlags(qt.Qt.Window)
- self.setGraphXLabel('X')
- self.setGraphYLabel('Y')
+ self.getXAxis().setLabel('X')
+ self.getYAxis().setLabel('Y')
self.setGraphTitle('Image')
# Add toolbars and status bar
@@ -814,11 +822,10 @@ class ImageViewMainWindow(ImageView):
menu.addSeparator()
menu.addAction(self.resetZoomAction)
menu.addAction(self.colormapAction)
- menu.addAction(PlotActions.KeepAspectRatioAction(self, self))
- menu.addAction(PlotActions.YAxisInvertedAction(self, self))
+ menu.addAction(actions.control.KeepAspectRatioAction(self, self))
+ menu.addAction(actions.control.YAxisInvertedAction(self, self))
menu = self.menuBar().addMenu('Profile')
- menu.addAction(self.profile.browseAction)
menu.addAction(self.profile.hLineAction)
menu.addAction(self.profile.vLineAction)
menu.addAction(self.profile.lineAction)
diff --git a/silx/gui/plot/ItemsSelectionDialog.py b/silx/gui/plot/ItemsSelectionDialog.py
new file mode 100644
index 0000000..acb287a
--- /dev/null
+++ b/silx/gui/plot/ItemsSelectionDialog.py
@@ -0,0 +1,282 @@
+# 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 a dialog widget to select plot items.
+
+.. autoclass:: ItemsSelectionDialog
+
+"""
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "28/06/2017"
+
+import logging
+
+from silx.gui import qt
+from silx.gui.plot.PlotWidget import PlotWidget
+
+_logger = logging.getLogger(__name__)
+
+
+class KindsSelector(qt.QListWidget):
+ """List widget allowing to select plot item kinds
+ ("curve", "scatter", "image"...)
+ """
+ sigSelectedKindsChanged = qt.Signal(list)
+
+ def __init__(self, parent=None, kinds=None):
+ """
+
+ :param parent: Parent QWidget or None
+ :param tuple(str) kinds: Sequence of kinds. If None, the default
+ behavior is to provide a checkbox for all possible item kinds.
+ """
+ qt.QListWidget.__init__(self, parent)
+
+ self.plot_item_kinds = []
+
+ self.setAvailableKinds(kinds if kinds is not None else PlotWidget.ITEM_KINDS)
+
+ self.setSelectionMode(qt.QAbstractItemView.ExtendedSelection)
+ self.selectAll()
+
+ self.itemSelectionChanged.connect(self.emitSigKindsSelectionChanged)
+
+ def emitSigKindsSelectionChanged(self):
+ self.sigSelectedKindsChanged.emit(self.selectedKinds)
+
+ @property
+ def selectedKinds(self):
+ """Tuple of all selected kinds (as strings)."""
+ # check for updates when self.itemSelectionChanged
+ return [item.text() for item in self.selectedItems()]
+
+ def setAvailableKinds(self, kinds):
+ """Set a list of kinds to be displayed.
+
+ :param list[str] kinds: Sequence of kinds
+ """
+ self.plot_item_kinds = kinds
+
+ self.clear()
+ for kind in self.plot_item_kinds:
+ item = qt.QListWidgetItem(self)
+ item.setText(kind)
+ self.addItem(item)
+
+ def selectAll(self):
+ """Select all available kinds."""
+ if self.selectionMode() in [qt.QAbstractItemView.SingleSelection,
+ qt.QAbstractItemView.NoSelection]:
+ raise RuntimeError("selectAll requires a multiple selection mode")
+ for i in range(self.count()):
+ self.item(i).setSelected(True)
+
+
+class PlotItemsSelector(qt.QTableWidget):
+ """Table widget displaying the legend and kind of all
+ plot items corresponding to a list of specified kinds.
+
+ Selected plot items are provided as property :attr:`selectedPlotItems`.
+ You can be warned of selection changes by listening to signal
+ :attr:`itemSelectionChanged`.
+ """
+ def __init__(self, parent=None, plot=None):
+ if plot is None or not isinstance(plot, PlotWidget):
+ raise AttributeError("parameter plot is required")
+ self.plot = plot
+ """:class:`PlotWidget` instance"""
+
+ self.plot_item_kinds = None
+ """List of plot item kinds (strings)"""
+
+ qt.QTableWidget.__init__(self, parent)
+
+ self.setColumnCount(2)
+
+ self.setSelectionBehavior(qt.QTableWidget.SelectRows)
+
+ def _clear(self):
+ self.clear()
+ self.setHorizontalHeaderLabels(["legend", "type"])
+
+ def setAllKindsFilter(self):
+ """Display all kinds of plot items."""
+ self.setKindsFilter(PlotWidget.ITEM_KINDS)
+
+ def setKindsFilter(self, kinds):
+ """Set list of all kinds of plot items to be displayed.
+
+ :param list[str] kinds: Sequence of kinds
+ """
+ if not set(kinds) <= set(PlotWidget.ITEM_KINDS):
+ raise KeyError("Illegal plot item kinds: %s" %
+ set(kinds) - set(PlotWidget.ITEM_KINDS))
+ self.plot_item_kinds = kinds
+
+ self.updatePlotItems()
+
+ def updatePlotItems(self):
+ self._clear()
+
+ nrows = len(self.plot._getItems(kind=self.plot_item_kinds,
+ just_legend=True))
+ self.setRowCount(nrows)
+
+ # respect order of kinds as set in method setKindsFilter
+ i = 0
+ for kind in self.plot_item_kinds:
+ for plot_item in self.plot._getItems(kind=kind):
+ legend_twitem = qt.QTableWidgetItem(plot_item.getLegend())
+ self.setItem(i, 0, legend_twitem)
+
+ kind_twitem = qt.QTableWidgetItem(kind)
+ self.setItem(i, 1, kind_twitem)
+ i += 1
+
+ @property
+ def selectedPlotItems(self):
+ """List of all selected items"""
+ selection_model = self.selectionModel()
+ selected_rows_idx = selection_model.selectedRows()
+ selected_rows = [idx.row() for idx in selected_rows_idx]
+
+ items = []
+ for row in selected_rows:
+ legend = self.item(row, 0).text()
+ kind = self.item(row, 1).text()
+ items.append(self.plot._getItem(kind, legend))
+
+ return items
+
+
+class ItemsSelectionDialog(qt.QDialog):
+ """This widget is a modal dialog allowing to select one or more plot
+ items, in a table displaying their legend and kind.
+
+ Public methods:
+
+ - :meth:`getSelectedItems`
+ - :meth:`setAvailableKinds`
+ - :meth:`setItemsSelectionMode`
+
+ This widget inherits QDialog and therefore implements the usual
+ dialog methods, e.g. :meth:`exec_`.
+
+ A trivial usage example would be::
+
+ isd = ItemsSelectionDialog(plot=my_plot_widget)
+ isd.setItemsSelectionMode(qt.QTableWidget.SingleSelection)
+ result = isd.exec_()
+ if result:
+ for item in isd.getSelectedItems():
+ print(item.getLegend(), type(item))
+ else:
+ print("Selection cancelled")
+ """
+ def __init__(self, parent=None, plot=None):
+ if plot is None or not isinstance(plot, PlotWidget):
+ raise AttributeError("parameter plot is required")
+ qt.QDialog.__init__(self, parent)
+
+ self.setWindowTitle("Plot items selector")
+
+ kind_selector_label = qt.QLabel("Filter item kinds:", self)
+ item_selector_label = qt.QLabel("Select items:", self)
+
+ self.kind_selector = KindsSelector(self)
+ self.kind_selector.setToolTip(
+ "select one or more item kinds to show them in the item list")
+
+ self.item_selector = PlotItemsSelector(self, plot)
+ self.item_selector.setToolTip("select items")
+
+ self.item_selector.setKindsFilter(self.kind_selector.selectedKinds)
+ self.kind_selector.sigSelectedKindsChanged.connect(
+ self.item_selector.setKindsFilter
+ )
+
+ okb = qt.QPushButton("OK", self)
+ okb.clicked.connect(self.accept)
+
+ cancelb = qt.QPushButton("Cancel", self)
+ cancelb.clicked.connect(self.reject)
+
+ layout = qt.QGridLayout(self)
+ layout.addWidget(kind_selector_label, 0, 0)
+ layout.addWidget(item_selector_label, 0, 1)
+ layout.addWidget(self.kind_selector, 1, 0)
+ layout.addWidget(self.item_selector, 1, 1)
+ layout.addWidget(okb, 2, 0)
+ layout.addWidget(cancelb, 2, 1)
+
+ self.setLayout(layout)
+
+ def getSelectedItems(self):
+ """Return a list of selected plot items
+
+ :return: List of selected plot items
+ :rtype: list[silx.gui.plot.items.Item]"""
+ return self.item_selector.selectedPlotItems
+
+ def setAvailableKinds(self, kinds):
+ """Set a list of kinds to be displayed.
+
+ :param list[str] kinds: Sequence of kinds
+ """
+ self.kind_selector.setAvailableKinds(kinds)
+
+ def selectAllKinds(self):
+ self.kind_selector.selectAll()
+
+ def setItemsSelectionMode(self, mode):
+ """Set selection mode for plot item (single item selection,
+ multiple...).
+
+ :param mode: One of :class:`QTableWidget` selection modes
+ """
+ if mode == self.item_selector.SingleSelection:
+ self.item_selector.setToolTip(
+ "Select one item by clicking on it.")
+ elif mode == self.item_selector.MultiSelection:
+ self.item_selector.setToolTip(
+ "Select one or more items by clicking with the left mouse"
+ " button.\nYou can unselect items by clicking them again.\n"
+ "Multiple items can be toggled by dragging the mouse over them.")
+ elif mode == self.item_selector.ExtendedSelection:
+ self.item_selector.setToolTip(
+ "Select one or more items. You can select multiple items "
+ "by keeping the Ctrl key pushed when clicking.\nYou can "
+ "select a range of items by clicking on the first and "
+ "last while keeping the Shift key pushed.")
+ elif mode == self.item_selector.ContiguousSelection:
+ self.item_selector.setToolTip(
+ "Select one item by clicking on it. If you press the Shift"
+ " key while clicking on a second item,\nall items between "
+ "the two will be selected.")
+ elif mode == self.item_selector.NoSelection:
+ raise ValueError("The NoSelection mode is not allowed "
+ "in this context.")
+ self.item_selector.setSelectionMode(mode)
diff --git a/silx/gui/plot/LegendSelector.py b/silx/gui/plot/LegendSelector.py
index 3af9050..31bc3db 100644
--- a/silx/gui/plot/LegendSelector.py
+++ b/silx/gui/plot/LegendSelector.py
@@ -29,7 +29,7 @@ This widget is meant to work with :class:`PlotWindow`.
__authors__ = ["V.A. Sole", "T. Rueter", "T. Vincent"]
__license__ = "MIT"
-__data__ = "28/04/2016"
+__data__ = "08/08/2016"
import logging
@@ -259,6 +259,7 @@ class LegendModel(qt.QAbstractListModel):
legendList = []
self.legendList = []
self.insertLegendList(0, legendList)
+ self._palette = qt.QPalette()
def __getitem__(self, idx):
if idx >= len(self.legendList):
@@ -282,6 +283,7 @@ class LegendModel(qt.QAbstractListModel):
raise IndexError('list index out of range')
item = self.legendList[idx]
+ isActive = item[1].get("active", False)
if role == qt.Qt.DisplayRole:
# Data to be rendered in the form of text
legend = str(item[0])
@@ -295,14 +297,19 @@ class LegendModel(qt.QAbstractListModel):
return alignment
elif role == qt.Qt.BackgroundRole:
# Background color, must be QBrush
- if idx % 2:
+ if isActive:
+ brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.Highlight)
+ elif idx % 2:
brush = qt.QBrush(qt.QColor(240, 240, 240))
else:
brush = qt.QBrush(qt.Qt.white)
return brush
elif role == qt.Qt.ForegroundRole:
# ForegroundRole color, must be QBrush
- brush = qt.QBrush(qt.Qt.blue)
+ if isActive:
+ brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.HighlightedText)
+ else:
+ brush = self._palette.brush(qt.QPalette.Normal, qt.QPalette.WindowText)
return brush
elif role == qt.Qt.CheckStateRole:
return bool(item[2]) # item[2] == True
@@ -513,6 +520,7 @@ class LegendListItemWidget(qt.QItemDelegate):
textAlign = modelIndex.data(qt.Qt.TextAlignmentRole)
painter.setBrush(textBrush)
painter.setFont(self.legend.font())
+ painter.setPen(textBrush.color())
painter.drawText(labelRect, textAlign, legendText)
# Draw icon
@@ -614,7 +622,7 @@ class LegendListView(qt.QListView):
self.setSelectionMode(qt.QAbstractItemView.NoSelection)
if model is None:
- model = LegendModel()
+ model = LegendModel(parent=self)
self.setModel(model)
self.setContextMenu(contextMenu)
@@ -1053,15 +1061,18 @@ class LegendsDockWidget(qt.QDockWidget):
# Use active color if curve is active
if legend == self.plot.getActiveCurve(just_legend=True):
color = qt.QColor(self.plot.getActiveCurveColor())
+ isActive = True
else:
color = qt.QColor.fromRgbF(*curve.getColor())
+ isActive = False
curveInfo = {
'color': color,
'linewidth': curve.getLineWidth(),
'linestyle': curve.getLineStyle(),
'symbol': curve.getSymbol(),
- 'selected': not self.plot.isCurveHidden(legend)}
+ 'selected': not self.plot.isCurveHidden(legend),
+ 'active': isActive}
legendList.append((legend, curveInfo))
self._legendWidget.setLegendList(legendList)
diff --git a/silx/gui/plot/LimitsHistory.py b/silx/gui/plot/LimitsHistory.py
new file mode 100644
index 0000000..a323548
--- /dev/null
+++ b/silx/gui/plot/LimitsHistory.py
@@ -0,0 +1,83 @@
+# 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 handling of :class:`PlotWidget` limits history.
+"""
+
+__authors__ = ["T. Vincent"]
+__license__ = "MIT"
+__date__ = "19/07/2017"
+
+
+from .. import qt
+
+
+class LimitsHistory(qt.QObject):
+ """Class handling history of limits of a :class:`PlotWidget`.
+
+ :param PlotWidget parent: The plot widget this object is bound to.
+ """
+
+ def __init__(self, parent):
+ self._history = []
+ super(LimitsHistory, self).__init__(parent)
+ self.setParent(parent)
+
+ def setParent(self, parent):
+ """See :meth:`QObject.setParent`.
+
+ :param PlotWidget parent: The PlotWidget this object is bound to.
+ """
+ self.clear() # Clear history when changing parent
+ super(LimitsHistory, self).setParent(parent)
+
+ def push(self):
+ """Append current limits to the history."""
+ plot = self.parent()
+ xmin, xmax = plot.getXAxis().getLimits()
+ ymin, ymax = plot.getYAxis(axis='left').getLimits()
+ y2min, y2max = plot.getYAxis(axis='right').getLimits()
+ self._history.append((xmin, xmax, ymin, ymax, y2min, y2max))
+
+ def pop(self):
+ """Restore previously limits stored in the history.
+
+ :return: True if limits were restored, False if history was empty.
+ :rtype: bool
+ """
+ plot = self.parent()
+ if self._history:
+ limits = self._history.pop(-1)
+ plot.setLimits(*limits)
+ return True
+ else:
+ plot.resetZoom()
+ return False
+
+ def clear(self):
+ """Clear stored limits states."""
+ self._history = []
+
+ def __len__(self):
+ return len(self._history)
diff --git a/silx/gui/plot/MPLColormap.py b/silx/gui/plot/MPLColormap.py
deleted file mode 100644
index 49b11d7..0000000
--- a/silx/gui/plot/MPLColormap.py
+++ /dev/null
@@ -1,1062 +0,0 @@
-# New matplotlib colormaps by Nathaniel J. Smith, Stefan van der Walt,
-# and (in the case of viridis) Eric Firing.
-#
-# This file and the colormaps in it are released under the CC0 license /
-# public domain dedication. We would appreciate credit if you use or
-# redistribute these colormaps, but do not impose any legal restrictions.
-#
-# To the extent possible under law, the persons who associated CC0 with
-# mpl-colormaps have waived all copyright and related or neighboring rights
-# to mpl-colormaps.
-#
-# You should have received a copy of the CC0 legalcode along with this
-# work. If not, see <http://creativecommons.org/publicdomain/zero/1.0/>.
-"""Matplotlib's new colormaps"""
-
-
-from matplotlib.colors import ListedColormap
-
-
-__all__ = ['magma', 'inferno', 'plasma', 'viridis']
-
-_magma_data = [[0.001462, 0.000466, 0.013866],
- [0.002258, 0.001295, 0.018331],
- [0.003279, 0.002305, 0.023708],
- [0.004512, 0.003490, 0.029965],
- [0.005950, 0.004843, 0.037130],
- [0.007588, 0.006356, 0.044973],
- [0.009426, 0.008022, 0.052844],
- [0.011465, 0.009828, 0.060750],
- [0.013708, 0.011771, 0.068667],
- [0.016156, 0.013840, 0.076603],
- [0.018815, 0.016026, 0.084584],
- [0.021692, 0.018320, 0.092610],
- [0.024792, 0.020715, 0.100676],
- [0.028123, 0.023201, 0.108787],
- [0.031696, 0.025765, 0.116965],
- [0.035520, 0.028397, 0.125209],
- [0.039608, 0.031090, 0.133515],
- [0.043830, 0.033830, 0.141886],
- [0.048062, 0.036607, 0.150327],
- [0.052320, 0.039407, 0.158841],
- [0.056615, 0.042160, 0.167446],
- [0.060949, 0.044794, 0.176129],
- [0.065330, 0.047318, 0.184892],
- [0.069764, 0.049726, 0.193735],
- [0.074257, 0.052017, 0.202660],
- [0.078815, 0.054184, 0.211667],
- [0.083446, 0.056225, 0.220755],
- [0.088155, 0.058133, 0.229922],
- [0.092949, 0.059904, 0.239164],
- [0.097833, 0.061531, 0.248477],
- [0.102815, 0.063010, 0.257854],
- [0.107899, 0.064335, 0.267289],
- [0.113094, 0.065492, 0.276784],
- [0.118405, 0.066479, 0.286321],
- [0.123833, 0.067295, 0.295879],
- [0.129380, 0.067935, 0.305443],
- [0.135053, 0.068391, 0.315000],
- [0.140858, 0.068654, 0.324538],
- [0.146785, 0.068738, 0.334011],
- [0.152839, 0.068637, 0.343404],
- [0.159018, 0.068354, 0.352688],
- [0.165308, 0.067911, 0.361816],
- [0.171713, 0.067305, 0.370771],
- [0.178212, 0.066576, 0.379497],
- [0.184801, 0.065732, 0.387973],
- [0.191460, 0.064818, 0.396152],
- [0.198177, 0.063862, 0.404009],
- [0.204935, 0.062907, 0.411514],
- [0.211718, 0.061992, 0.418647],
- [0.218512, 0.061158, 0.425392],
- [0.225302, 0.060445, 0.431742],
- [0.232077, 0.059889, 0.437695],
- [0.238826, 0.059517, 0.443256],
- [0.245543, 0.059352, 0.448436],
- [0.252220, 0.059415, 0.453248],
- [0.258857, 0.059706, 0.457710],
- [0.265447, 0.060237, 0.461840],
- [0.271994, 0.060994, 0.465660],
- [0.278493, 0.061978, 0.469190],
- [0.284951, 0.063168, 0.472451],
- [0.291366, 0.064553, 0.475462],
- [0.297740, 0.066117, 0.478243],
- [0.304081, 0.067835, 0.480812],
- [0.310382, 0.069702, 0.483186],
- [0.316654, 0.071690, 0.485380],
- [0.322899, 0.073782, 0.487408],
- [0.329114, 0.075972, 0.489287],
- [0.335308, 0.078236, 0.491024],
- [0.341482, 0.080564, 0.492631],
- [0.347636, 0.082946, 0.494121],
- [0.353773, 0.085373, 0.495501],
- [0.359898, 0.087831, 0.496778],
- [0.366012, 0.090314, 0.497960],
- [0.372116, 0.092816, 0.499053],
- [0.378211, 0.095332, 0.500067],
- [0.384299, 0.097855, 0.501002],
- [0.390384, 0.100379, 0.501864],
- [0.396467, 0.102902, 0.502658],
- [0.402548, 0.105420, 0.503386],
- [0.408629, 0.107930, 0.504052],
- [0.414709, 0.110431, 0.504662],
- [0.420791, 0.112920, 0.505215],
- [0.426877, 0.115395, 0.505714],
- [0.432967, 0.117855, 0.506160],
- [0.439062, 0.120298, 0.506555],
- [0.445163, 0.122724, 0.506901],
- [0.451271, 0.125132, 0.507198],
- [0.457386, 0.127522, 0.507448],
- [0.463508, 0.129893, 0.507652],
- [0.469640, 0.132245, 0.507809],
- [0.475780, 0.134577, 0.507921],
- [0.481929, 0.136891, 0.507989],
- [0.488088, 0.139186, 0.508011],
- [0.494258, 0.141462, 0.507988],
- [0.500438, 0.143719, 0.507920],
- [0.506629, 0.145958, 0.507806],
- [0.512831, 0.148179, 0.507648],
- [0.519045, 0.150383, 0.507443],
- [0.525270, 0.152569, 0.507192],
- [0.531507, 0.154739, 0.506895],
- [0.537755, 0.156894, 0.506551],
- [0.544015, 0.159033, 0.506159],
- [0.550287, 0.161158, 0.505719],
- [0.556571, 0.163269, 0.505230],
- [0.562866, 0.165368, 0.504692],
- [0.569172, 0.167454, 0.504105],
- [0.575490, 0.169530, 0.503466],
- [0.581819, 0.171596, 0.502777],
- [0.588158, 0.173652, 0.502035],
- [0.594508, 0.175701, 0.501241],
- [0.600868, 0.177743, 0.500394],
- [0.607238, 0.179779, 0.499492],
- [0.613617, 0.181811, 0.498536],
- [0.620005, 0.183840, 0.497524],
- [0.626401, 0.185867, 0.496456],
- [0.632805, 0.187893, 0.495332],
- [0.639216, 0.189921, 0.494150],
- [0.645633, 0.191952, 0.492910],
- [0.652056, 0.193986, 0.491611],
- [0.658483, 0.196027, 0.490253],
- [0.664915, 0.198075, 0.488836],
- [0.671349, 0.200133, 0.487358],
- [0.677786, 0.202203, 0.485819],
- [0.684224, 0.204286, 0.484219],
- [0.690661, 0.206384, 0.482558],
- [0.697098, 0.208501, 0.480835],
- [0.703532, 0.210638, 0.479049],
- [0.709962, 0.212797, 0.477201],
- [0.716387, 0.214982, 0.475290],
- [0.722805, 0.217194, 0.473316],
- [0.729216, 0.219437, 0.471279],
- [0.735616, 0.221713, 0.469180],
- [0.742004, 0.224025, 0.467018],
- [0.748378, 0.226377, 0.464794],
- [0.754737, 0.228772, 0.462509],
- [0.761077, 0.231214, 0.460162],
- [0.767398, 0.233705, 0.457755],
- [0.773695, 0.236249, 0.455289],
- [0.779968, 0.238851, 0.452765],
- [0.786212, 0.241514, 0.450184],
- [0.792427, 0.244242, 0.447543],
- [0.798608, 0.247040, 0.444848],
- [0.804752, 0.249911, 0.442102],
- [0.810855, 0.252861, 0.439305],
- [0.816914, 0.255895, 0.436461],
- [0.822926, 0.259016, 0.433573],
- [0.828886, 0.262229, 0.430644],
- [0.834791, 0.265540, 0.427671],
- [0.840636, 0.268953, 0.424666],
- [0.846416, 0.272473, 0.421631],
- [0.852126, 0.276106, 0.418573],
- [0.857763, 0.279857, 0.415496],
- [0.863320, 0.283729, 0.412403],
- [0.868793, 0.287728, 0.409303],
- [0.874176, 0.291859, 0.406205],
- [0.879464, 0.296125, 0.403118],
- [0.884651, 0.300530, 0.400047],
- [0.889731, 0.305079, 0.397002],
- [0.894700, 0.309773, 0.393995],
- [0.899552, 0.314616, 0.391037],
- [0.904281, 0.319610, 0.388137],
- [0.908884, 0.324755, 0.385308],
- [0.913354, 0.330052, 0.382563],
- [0.917689, 0.335500, 0.379915],
- [0.921884, 0.341098, 0.377376],
- [0.925937, 0.346844, 0.374959],
- [0.929845, 0.352734, 0.372677],
- [0.933606, 0.358764, 0.370541],
- [0.937221, 0.364929, 0.368567],
- [0.940687, 0.371224, 0.366762],
- [0.944006, 0.377643, 0.365136],
- [0.947180, 0.384178, 0.363701],
- [0.950210, 0.390820, 0.362468],
- [0.953099, 0.397563, 0.361438],
- [0.955849, 0.404400, 0.360619],
- [0.958464, 0.411324, 0.360014],
- [0.960949, 0.418323, 0.359630],
- [0.963310, 0.425390, 0.359469],
- [0.965549, 0.432519, 0.359529],
- [0.967671, 0.439703, 0.359810],
- [0.969680, 0.446936, 0.360311],
- [0.971582, 0.454210, 0.361030],
- [0.973381, 0.461520, 0.361965],
- [0.975082, 0.468861, 0.363111],
- [0.976690, 0.476226, 0.364466],
- [0.978210, 0.483612, 0.366025],
- [0.979645, 0.491014, 0.367783],
- [0.981000, 0.498428, 0.369734],
- [0.982279, 0.505851, 0.371874],
- [0.983485, 0.513280, 0.374198],
- [0.984622, 0.520713, 0.376698],
- [0.985693, 0.528148, 0.379371],
- [0.986700, 0.535582, 0.382210],
- [0.987646, 0.543015, 0.385210],
- [0.988533, 0.550446, 0.388365],
- [0.989363, 0.557873, 0.391671],
- [0.990138, 0.565296, 0.395122],
- [0.990871, 0.572706, 0.398714],
- [0.991558, 0.580107, 0.402441],
- [0.992196, 0.587502, 0.406299],
- [0.992785, 0.594891, 0.410283],
- [0.993326, 0.602275, 0.414390],
- [0.993834, 0.609644, 0.418613],
- [0.994309, 0.616999, 0.422950],
- [0.994738, 0.624350, 0.427397],
- [0.995122, 0.631696, 0.431951],
- [0.995480, 0.639027, 0.436607],
- [0.995810, 0.646344, 0.441361],
- [0.996096, 0.653659, 0.446213],
- [0.996341, 0.660969, 0.451160],
- [0.996580, 0.668256, 0.456192],
- [0.996775, 0.675541, 0.461314],
- [0.996925, 0.682828, 0.466526],
- [0.997077, 0.690088, 0.471811],
- [0.997186, 0.697349, 0.477182],
- [0.997254, 0.704611, 0.482635],
- [0.997325, 0.711848, 0.488154],
- [0.997351, 0.719089, 0.493755],
- [0.997351, 0.726324, 0.499428],
- [0.997341, 0.733545, 0.505167],
- [0.997285, 0.740772, 0.510983],
- [0.997228, 0.747981, 0.516859],
- [0.997138, 0.755190, 0.522806],
- [0.997019, 0.762398, 0.528821],
- [0.996898, 0.769591, 0.534892],
- [0.996727, 0.776795, 0.541039],
- [0.996571, 0.783977, 0.547233],
- [0.996369, 0.791167, 0.553499],
- [0.996162, 0.798348, 0.559820],
- [0.995932, 0.805527, 0.566202],
- [0.995680, 0.812706, 0.572645],
- [0.995424, 0.819875, 0.579140],
- [0.995131, 0.827052, 0.585701],
- [0.994851, 0.834213, 0.592307],
- [0.994524, 0.841387, 0.598983],
- [0.994222, 0.848540, 0.605696],
- [0.993866, 0.855711, 0.612482],
- [0.993545, 0.862859, 0.619299],
- [0.993170, 0.870024, 0.626189],
- [0.992831, 0.877168, 0.633109],
- [0.992440, 0.884330, 0.640099],
- [0.992089, 0.891470, 0.647116],
- [0.991688, 0.898627, 0.654202],
- [0.991332, 0.905763, 0.661309],
- [0.990930, 0.912915, 0.668481],
- [0.990570, 0.920049, 0.675675],
- [0.990175, 0.927196, 0.682926],
- [0.989815, 0.934329, 0.690198],
- [0.989434, 0.941470, 0.697519],
- [0.989077, 0.948604, 0.704863],
- [0.988717, 0.955742, 0.712242],
- [0.988367, 0.962878, 0.719649],
- [0.988033, 0.970012, 0.727077],
- [0.987691, 0.977154, 0.734536],
- [0.987387, 0.984288, 0.742002],
- [0.987053, 0.991438, 0.749504]]
-
-_inferno_data = [[0.001462, 0.000466, 0.013866],
- [0.002267, 0.001270, 0.018570],
- [0.003299, 0.002249, 0.024239],
- [0.004547, 0.003392, 0.030909],
- [0.006006, 0.004692, 0.038558],
- [0.007676, 0.006136, 0.046836],
- [0.009561, 0.007713, 0.055143],
- [0.011663, 0.009417, 0.063460],
- [0.013995, 0.011225, 0.071862],
- [0.016561, 0.013136, 0.080282],
- [0.019373, 0.015133, 0.088767],
- [0.022447, 0.017199, 0.097327],
- [0.025793, 0.019331, 0.105930],
- [0.029432, 0.021503, 0.114621],
- [0.033385, 0.023702, 0.123397],
- [0.037668, 0.025921, 0.132232],
- [0.042253, 0.028139, 0.141141],
- [0.046915, 0.030324, 0.150164],
- [0.051644, 0.032474, 0.159254],
- [0.056449, 0.034569, 0.168414],
- [0.061340, 0.036590, 0.177642],
- [0.066331, 0.038504, 0.186962],
- [0.071429, 0.040294, 0.196354],
- [0.076637, 0.041905, 0.205799],
- [0.081962, 0.043328, 0.215289],
- [0.087411, 0.044556, 0.224813],
- [0.092990, 0.045583, 0.234358],
- [0.098702, 0.046402, 0.243904],
- [0.104551, 0.047008, 0.253430],
- [0.110536, 0.047399, 0.262912],
- [0.116656, 0.047574, 0.272321],
- [0.122908, 0.047536, 0.281624],
- [0.129285, 0.047293, 0.290788],
- [0.135778, 0.046856, 0.299776],
- [0.142378, 0.046242, 0.308553],
- [0.149073, 0.045468, 0.317085],
- [0.155850, 0.044559, 0.325338],
- [0.162689, 0.043554, 0.333277],
- [0.169575, 0.042489, 0.340874],
- [0.176493, 0.041402, 0.348111],
- [0.183429, 0.040329, 0.354971],
- [0.190367, 0.039309, 0.361447],
- [0.197297, 0.038400, 0.367535],
- [0.204209, 0.037632, 0.373238],
- [0.211095, 0.037030, 0.378563],
- [0.217949, 0.036615, 0.383522],
- [0.224763, 0.036405, 0.388129],
- [0.231538, 0.036405, 0.392400],
- [0.238273, 0.036621, 0.396353],
- [0.244967, 0.037055, 0.400007],
- [0.251620, 0.037705, 0.403378],
- [0.258234, 0.038571, 0.406485],
- [0.264810, 0.039647, 0.409345],
- [0.271347, 0.040922, 0.411976],
- [0.277850, 0.042353, 0.414392],
- [0.284321, 0.043933, 0.416608],
- [0.290763, 0.045644, 0.418637],
- [0.297178, 0.047470, 0.420491],
- [0.303568, 0.049396, 0.422182],
- [0.309935, 0.051407, 0.423721],
- [0.316282, 0.053490, 0.425116],
- [0.322610, 0.055634, 0.426377],
- [0.328921, 0.057827, 0.427511],
- [0.335217, 0.060060, 0.428524],
- [0.341500, 0.062325, 0.429425],
- [0.347771, 0.064616, 0.430217],
- [0.354032, 0.066925, 0.430906],
- [0.360284, 0.069247, 0.431497],
- [0.366529, 0.071579, 0.431994],
- [0.372768, 0.073915, 0.432400],
- [0.379001, 0.076253, 0.432719],
- [0.385228, 0.078591, 0.432955],
- [0.391453, 0.080927, 0.433109],
- [0.397674, 0.083257, 0.433183],
- [0.403894, 0.085580, 0.433179],
- [0.410113, 0.087896, 0.433098],
- [0.416331, 0.090203, 0.432943],
- [0.422549, 0.092501, 0.432714],
- [0.428768, 0.094790, 0.432412],
- [0.434987, 0.097069, 0.432039],
- [0.441207, 0.099338, 0.431594],
- [0.447428, 0.101597, 0.431080],
- [0.453651, 0.103848, 0.430498],
- [0.459875, 0.106089, 0.429846],
- [0.466100, 0.108322, 0.429125],
- [0.472328, 0.110547, 0.428334],
- [0.478558, 0.112764, 0.427475],
- [0.484789, 0.114974, 0.426548],
- [0.491022, 0.117179, 0.425552],
- [0.497257, 0.119379, 0.424488],
- [0.503493, 0.121575, 0.423356],
- [0.509730, 0.123769, 0.422156],
- [0.515967, 0.125960, 0.420887],
- [0.522206, 0.128150, 0.419549],
- [0.528444, 0.130341, 0.418142],
- [0.534683, 0.132534, 0.416667],
- [0.540920, 0.134729, 0.415123],
- [0.547157, 0.136929, 0.413511],
- [0.553392, 0.139134, 0.411829],
- [0.559624, 0.141346, 0.410078],
- [0.565854, 0.143567, 0.408258],
- [0.572081, 0.145797, 0.406369],
- [0.578304, 0.148039, 0.404411],
- [0.584521, 0.150294, 0.402385],
- [0.590734, 0.152563, 0.400290],
- [0.596940, 0.154848, 0.398125],
- [0.603139, 0.157151, 0.395891],
- [0.609330, 0.159474, 0.393589],
- [0.615513, 0.161817, 0.391219],
- [0.621685, 0.164184, 0.388781],
- [0.627847, 0.166575, 0.386276],
- [0.633998, 0.168992, 0.383704],
- [0.640135, 0.171438, 0.381065],
- [0.646260, 0.173914, 0.378359],
- [0.652369, 0.176421, 0.375586],
- [0.658463, 0.178962, 0.372748],
- [0.664540, 0.181539, 0.369846],
- [0.670599, 0.184153, 0.366879],
- [0.676638, 0.186807, 0.363849],
- [0.682656, 0.189501, 0.360757],
- [0.688653, 0.192239, 0.357603],
- [0.694627, 0.195021, 0.354388],
- [0.700576, 0.197851, 0.351113],
- [0.706500, 0.200728, 0.347777],
- [0.712396, 0.203656, 0.344383],
- [0.718264, 0.206636, 0.340931],
- [0.724103, 0.209670, 0.337424],
- [0.729909, 0.212759, 0.333861],
- [0.735683, 0.215906, 0.330245],
- [0.741423, 0.219112, 0.326576],
- [0.747127, 0.222378, 0.322856],
- [0.752794, 0.225706, 0.319085],
- [0.758422, 0.229097, 0.315266],
- [0.764010, 0.232554, 0.311399],
- [0.769556, 0.236077, 0.307485],
- [0.775059, 0.239667, 0.303526],
- [0.780517, 0.243327, 0.299523],
- [0.785929, 0.247056, 0.295477],
- [0.791293, 0.250856, 0.291390],
- [0.796607, 0.254728, 0.287264],
- [0.801871, 0.258674, 0.283099],
- [0.807082, 0.262692, 0.278898],
- [0.812239, 0.266786, 0.274661],
- [0.817341, 0.270954, 0.270390],
- [0.822386, 0.275197, 0.266085],
- [0.827372, 0.279517, 0.261750],
- [0.832299, 0.283913, 0.257383],
- [0.837165, 0.288385, 0.252988],
- [0.841969, 0.292933, 0.248564],
- [0.846709, 0.297559, 0.244113],
- [0.851384, 0.302260, 0.239636],
- [0.855992, 0.307038, 0.235133],
- [0.860533, 0.311892, 0.230606],
- [0.865006, 0.316822, 0.226055],
- [0.869409, 0.321827, 0.221482],
- [0.873741, 0.326906, 0.216886],
- [0.878001, 0.332060, 0.212268],
- [0.882188, 0.337287, 0.207628],
- [0.886302, 0.342586, 0.202968],
- [0.890341, 0.347957, 0.198286],
- [0.894305, 0.353399, 0.193584],
- [0.898192, 0.358911, 0.188860],
- [0.902003, 0.364492, 0.184116],
- [0.905735, 0.370140, 0.179350],
- [0.909390, 0.375856, 0.174563],
- [0.912966, 0.381636, 0.169755],
- [0.916462, 0.387481, 0.164924],
- [0.919879, 0.393389, 0.160070],
- [0.923215, 0.399359, 0.155193],
- [0.926470, 0.405389, 0.150292],
- [0.929644, 0.411479, 0.145367],
- [0.932737, 0.417627, 0.140417],
- [0.935747, 0.423831, 0.135440],
- [0.938675, 0.430091, 0.130438],
- [0.941521, 0.436405, 0.125409],
- [0.944285, 0.442772, 0.120354],
- [0.946965, 0.449191, 0.115272],
- [0.949562, 0.455660, 0.110164],
- [0.952075, 0.462178, 0.105031],
- [0.954506, 0.468744, 0.099874],
- [0.956852, 0.475356, 0.094695],
- [0.959114, 0.482014, 0.089499],
- [0.961293, 0.488716, 0.084289],
- [0.963387, 0.495462, 0.079073],
- [0.965397, 0.502249, 0.073859],
- [0.967322, 0.509078, 0.068659],
- [0.969163, 0.515946, 0.063488],
- [0.970919, 0.522853, 0.058367],
- [0.972590, 0.529798, 0.053324],
- [0.974176, 0.536780, 0.048392],
- [0.975677, 0.543798, 0.043618],
- [0.977092, 0.550850, 0.039050],
- [0.978422, 0.557937, 0.034931],
- [0.979666, 0.565057, 0.031409],
- [0.980824, 0.572209, 0.028508],
- [0.981895, 0.579392, 0.026250],
- [0.982881, 0.586606, 0.024661],
- [0.983779, 0.593849, 0.023770],
- [0.984591, 0.601122, 0.023606],
- [0.985315, 0.608422, 0.024202],
- [0.985952, 0.615750, 0.025592],
- [0.986502, 0.623105, 0.027814],
- [0.986964, 0.630485, 0.030908],
- [0.987337, 0.637890, 0.034916],
- [0.987622, 0.645320, 0.039886],
- [0.987819, 0.652773, 0.045581],
- [0.987926, 0.660250, 0.051750],
- [0.987945, 0.667748, 0.058329],
- [0.987874, 0.675267, 0.065257],
- [0.987714, 0.682807, 0.072489],
- [0.987464, 0.690366, 0.079990],
- [0.987124, 0.697944, 0.087731],
- [0.986694, 0.705540, 0.095694],
- [0.986175, 0.713153, 0.103863],
- [0.985566, 0.720782, 0.112229],
- [0.984865, 0.728427, 0.120785],
- [0.984075, 0.736087, 0.129527],
- [0.983196, 0.743758, 0.138453],
- [0.982228, 0.751442, 0.147565],
- [0.981173, 0.759135, 0.156863],
- [0.980032, 0.766837, 0.166353],
- [0.978806, 0.774545, 0.176037],
- [0.977497, 0.782258, 0.185923],
- [0.976108, 0.789974, 0.196018],
- [0.974638, 0.797692, 0.206332],
- [0.973088, 0.805409, 0.216877],
- [0.971468, 0.813122, 0.227658],
- [0.969783, 0.820825, 0.238686],
- [0.968041, 0.828515, 0.249972],
- [0.966243, 0.836191, 0.261534],
- [0.964394, 0.843848, 0.273391],
- [0.962517, 0.851476, 0.285546],
- [0.960626, 0.859069, 0.298010],
- [0.958720, 0.866624, 0.310820],
- [0.956834, 0.874129, 0.323974],
- [0.954997, 0.881569, 0.337475],
- [0.953215, 0.888942, 0.351369],
- [0.951546, 0.896226, 0.365627],
- [0.950018, 0.903409, 0.380271],
- [0.948683, 0.910473, 0.395289],
- [0.947594, 0.917399, 0.410665],
- [0.946809, 0.924168, 0.426373],
- [0.946392, 0.930761, 0.442367],
- [0.946403, 0.937159, 0.458592],
- [0.946903, 0.943348, 0.474970],
- [0.947937, 0.949318, 0.491426],
- [0.949545, 0.955063, 0.507860],
- [0.951740, 0.960587, 0.524203],
- [0.954529, 0.965896, 0.540361],
- [0.957896, 0.971003, 0.556275],
- [0.961812, 0.975924, 0.571925],
- [0.966249, 0.980678, 0.587206],
- [0.971162, 0.985282, 0.602154],
- [0.976511, 0.989753, 0.616760],
- [0.982257, 0.994109, 0.631017],
- [0.988362, 0.998364, 0.644924]]
-
-_plasma_data = [[0.050383, 0.029803, 0.527975],
- [0.063536, 0.028426, 0.533124],
- [0.075353, 0.027206, 0.538007],
- [0.086222, 0.026125, 0.542658],
- [0.096379, 0.025165, 0.547103],
- [0.105980, 0.024309, 0.551368],
- [0.115124, 0.023556, 0.555468],
- [0.123903, 0.022878, 0.559423],
- [0.132381, 0.022258, 0.563250],
- [0.140603, 0.021687, 0.566959],
- [0.148607, 0.021154, 0.570562],
- [0.156421, 0.020651, 0.574065],
- [0.164070, 0.020171, 0.577478],
- [0.171574, 0.019706, 0.580806],
- [0.178950, 0.019252, 0.584054],
- [0.186213, 0.018803, 0.587228],
- [0.193374, 0.018354, 0.590330],
- [0.200445, 0.017902, 0.593364],
- [0.207435, 0.017442, 0.596333],
- [0.214350, 0.016973, 0.599239],
- [0.221197, 0.016497, 0.602083],
- [0.227983, 0.016007, 0.604867],
- [0.234715, 0.015502, 0.607592],
- [0.241396, 0.014979, 0.610259],
- [0.248032, 0.014439, 0.612868],
- [0.254627, 0.013882, 0.615419],
- [0.261183, 0.013308, 0.617911],
- [0.267703, 0.012716, 0.620346],
- [0.274191, 0.012109, 0.622722],
- [0.280648, 0.011488, 0.625038],
- [0.287076, 0.010855, 0.627295],
- [0.293478, 0.010213, 0.629490],
- [0.299855, 0.009561, 0.631624],
- [0.306210, 0.008902, 0.633694],
- [0.312543, 0.008239, 0.635700],
- [0.318856, 0.007576, 0.637640],
- [0.325150, 0.006915, 0.639512],
- [0.331426, 0.006261, 0.641316],
- [0.337683, 0.005618, 0.643049],
- [0.343925, 0.004991, 0.644710],
- [0.350150, 0.004382, 0.646298],
- [0.356359, 0.003798, 0.647810],
- [0.362553, 0.003243, 0.649245],
- [0.368733, 0.002724, 0.650601],
- [0.374897, 0.002245, 0.651876],
- [0.381047, 0.001814, 0.653068],
- [0.387183, 0.001434, 0.654177],
- [0.393304, 0.001114, 0.655199],
- [0.399411, 0.000859, 0.656133],
- [0.405503, 0.000678, 0.656977],
- [0.411580, 0.000577, 0.657730],
- [0.417642, 0.000564, 0.658390],
- [0.423689, 0.000646, 0.658956],
- [0.429719, 0.000831, 0.659425],
- [0.435734, 0.001127, 0.659797],
- [0.441732, 0.001540, 0.660069],
- [0.447714, 0.002080, 0.660240],
- [0.453677, 0.002755, 0.660310],
- [0.459623, 0.003574, 0.660277],
- [0.465550, 0.004545, 0.660139],
- [0.471457, 0.005678, 0.659897],
- [0.477344, 0.006980, 0.659549],
- [0.483210, 0.008460, 0.659095],
- [0.489055, 0.010127, 0.658534],
- [0.494877, 0.011990, 0.657865],
- [0.500678, 0.014055, 0.657088],
- [0.506454, 0.016333, 0.656202],
- [0.512206, 0.018833, 0.655209],
- [0.517933, 0.021563, 0.654109],
- [0.523633, 0.024532, 0.652901],
- [0.529306, 0.027747, 0.651586],
- [0.534952, 0.031217, 0.650165],
- [0.540570, 0.034950, 0.648640],
- [0.546157, 0.038954, 0.647010],
- [0.551715, 0.043136, 0.645277],
- [0.557243, 0.047331, 0.643443],
- [0.562738, 0.051545, 0.641509],
- [0.568201, 0.055778, 0.639477],
- [0.573632, 0.060028, 0.637349],
- [0.579029, 0.064296, 0.635126],
- [0.584391, 0.068579, 0.632812],
- [0.589719, 0.072878, 0.630408],
- [0.595011, 0.077190, 0.627917],
- [0.600266, 0.081516, 0.625342],
- [0.605485, 0.085854, 0.622686],
- [0.610667, 0.090204, 0.619951],
- [0.615812, 0.094564, 0.617140],
- [0.620919, 0.098934, 0.614257],
- [0.625987, 0.103312, 0.611305],
- [0.631017, 0.107699, 0.608287],
- [0.636008, 0.112092, 0.605205],
- [0.640959, 0.116492, 0.602065],
- [0.645872, 0.120898, 0.598867],
- [0.650746, 0.125309, 0.595617],
- [0.655580, 0.129725, 0.592317],
- [0.660374, 0.134144, 0.588971],
- [0.665129, 0.138566, 0.585582],
- [0.669845, 0.142992, 0.582154],
- [0.674522, 0.147419, 0.578688],
- [0.679160, 0.151848, 0.575189],
- [0.683758, 0.156278, 0.571660],
- [0.688318, 0.160709, 0.568103],
- [0.692840, 0.165141, 0.564522],
- [0.697324, 0.169573, 0.560919],
- [0.701769, 0.174005, 0.557296],
- [0.706178, 0.178437, 0.553657],
- [0.710549, 0.182868, 0.550004],
- [0.714883, 0.187299, 0.546338],
- [0.719181, 0.191729, 0.542663],
- [0.723444, 0.196158, 0.538981],
- [0.727670, 0.200586, 0.535293],
- [0.731862, 0.205013, 0.531601],
- [0.736019, 0.209439, 0.527908],
- [0.740143, 0.213864, 0.524216],
- [0.744232, 0.218288, 0.520524],
- [0.748289, 0.222711, 0.516834],
- [0.752312, 0.227133, 0.513149],
- [0.756304, 0.231555, 0.509468],
- [0.760264, 0.235976, 0.505794],
- [0.764193, 0.240396, 0.502126],
- [0.768090, 0.244817, 0.498465],
- [0.771958, 0.249237, 0.494813],
- [0.775796, 0.253658, 0.491171],
- [0.779604, 0.258078, 0.487539],
- [0.783383, 0.262500, 0.483918],
- [0.787133, 0.266922, 0.480307],
- [0.790855, 0.271345, 0.476706],
- [0.794549, 0.275770, 0.473117],
- [0.798216, 0.280197, 0.469538],
- [0.801855, 0.284626, 0.465971],
- [0.805467, 0.289057, 0.462415],
- [0.809052, 0.293491, 0.458870],
- [0.812612, 0.297928, 0.455338],
- [0.816144, 0.302368, 0.451816],
- [0.819651, 0.306812, 0.448306],
- [0.823132, 0.311261, 0.444806],
- [0.826588, 0.315714, 0.441316],
- [0.830018, 0.320172, 0.437836],
- [0.833422, 0.324635, 0.434366],
- [0.836801, 0.329105, 0.430905],
- [0.840155, 0.333580, 0.427455],
- [0.843484, 0.338062, 0.424013],
- [0.846788, 0.342551, 0.420579],
- [0.850066, 0.347048, 0.417153],
- [0.853319, 0.351553, 0.413734],
- [0.856547, 0.356066, 0.410322],
- [0.859750, 0.360588, 0.406917],
- [0.862927, 0.365119, 0.403519],
- [0.866078, 0.369660, 0.400126],
- [0.869203, 0.374212, 0.396738],
- [0.872303, 0.378774, 0.393355],
- [0.875376, 0.383347, 0.389976],
- [0.878423, 0.387932, 0.386600],
- [0.881443, 0.392529, 0.383229],
- [0.884436, 0.397139, 0.379860],
- [0.887402, 0.401762, 0.376494],
- [0.890340, 0.406398, 0.373130],
- [0.893250, 0.411048, 0.369768],
- [0.896131, 0.415712, 0.366407],
- [0.898984, 0.420392, 0.363047],
- [0.901807, 0.425087, 0.359688],
- [0.904601, 0.429797, 0.356329],
- [0.907365, 0.434524, 0.352970],
- [0.910098, 0.439268, 0.349610],
- [0.912800, 0.444029, 0.346251],
- [0.915471, 0.448807, 0.342890],
- [0.918109, 0.453603, 0.339529],
- [0.920714, 0.458417, 0.336166],
- [0.923287, 0.463251, 0.332801],
- [0.925825, 0.468103, 0.329435],
- [0.928329, 0.472975, 0.326067],
- [0.930798, 0.477867, 0.322697],
- [0.933232, 0.482780, 0.319325],
- [0.935630, 0.487712, 0.315952],
- [0.937990, 0.492667, 0.312575],
- [0.940313, 0.497642, 0.309197],
- [0.942598, 0.502639, 0.305816],
- [0.944844, 0.507658, 0.302433],
- [0.947051, 0.512699, 0.299049],
- [0.949217, 0.517763, 0.295662],
- [0.951344, 0.522850, 0.292275],
- [0.953428, 0.527960, 0.288883],
- [0.955470, 0.533093, 0.285490],
- [0.957469, 0.538250, 0.282096],
- [0.959424, 0.543431, 0.278701],
- [0.961336, 0.548636, 0.275305],
- [0.963203, 0.553865, 0.271909],
- [0.965024, 0.559118, 0.268513],
- [0.966798, 0.564396, 0.265118],
- [0.968526, 0.569700, 0.261721],
- [0.970205, 0.575028, 0.258325],
- [0.971835, 0.580382, 0.254931],
- [0.973416, 0.585761, 0.251540],
- [0.974947, 0.591165, 0.248151],
- [0.976428, 0.596595, 0.244767],
- [0.977856, 0.602051, 0.241387],
- [0.979233, 0.607532, 0.238013],
- [0.980556, 0.613039, 0.234646],
- [0.981826, 0.618572, 0.231287],
- [0.983041, 0.624131, 0.227937],
- [0.984199, 0.629718, 0.224595],
- [0.985301, 0.635330, 0.221265],
- [0.986345, 0.640969, 0.217948],
- [0.987332, 0.646633, 0.214648],
- [0.988260, 0.652325, 0.211364],
- [0.989128, 0.658043, 0.208100],
- [0.989935, 0.663787, 0.204859],
- [0.990681, 0.669558, 0.201642],
- [0.991365, 0.675355, 0.198453],
- [0.991985, 0.681179, 0.195295],
- [0.992541, 0.687030, 0.192170],
- [0.993032, 0.692907, 0.189084],
- [0.993456, 0.698810, 0.186041],
- [0.993814, 0.704741, 0.183043],
- [0.994103, 0.710698, 0.180097],
- [0.994324, 0.716681, 0.177208],
- [0.994474, 0.722691, 0.174381],
- [0.994553, 0.728728, 0.171622],
- [0.994561, 0.734791, 0.168938],
- [0.994495, 0.740880, 0.166335],
- [0.994355, 0.746995, 0.163821],
- [0.994141, 0.753137, 0.161404],
- [0.993851, 0.759304, 0.159092],
- [0.993482, 0.765499, 0.156891],
- [0.993033, 0.771720, 0.154808],
- [0.992505, 0.777967, 0.152855],
- [0.991897, 0.784239, 0.151042],
- [0.991209, 0.790537, 0.149377],
- [0.990439, 0.796859, 0.147870],
- [0.989587, 0.803205, 0.146529],
- [0.988648, 0.809579, 0.145357],
- [0.987621, 0.815978, 0.144363],
- [0.986509, 0.822401, 0.143557],
- [0.985314, 0.828846, 0.142945],
- [0.984031, 0.835315, 0.142528],
- [0.982653, 0.841812, 0.142303],
- [0.981190, 0.848329, 0.142279],
- [0.979644, 0.854866, 0.142453],
- [0.977995, 0.861432, 0.142808],
- [0.976265, 0.868016, 0.143351],
- [0.974443, 0.874622, 0.144061],
- [0.972530, 0.881250, 0.144923],
- [0.970533, 0.887896, 0.145919],
- [0.968443, 0.894564, 0.147014],
- [0.966271, 0.901249, 0.148180],
- [0.964021, 0.907950, 0.149370],
- [0.961681, 0.914672, 0.150520],
- [0.959276, 0.921407, 0.151566],
- [0.956808, 0.928152, 0.152409],
- [0.954287, 0.934908, 0.152921],
- [0.951726, 0.941671, 0.152925],
- [0.949151, 0.948435, 0.152178],
- [0.946602, 0.955190, 0.150328],
- [0.944152, 0.961916, 0.146861],
- [0.941896, 0.968590, 0.140956],
- [0.940015, 0.975158, 0.131326]]
-
-_viridis_data = [[0.267004, 0.004874, 0.329415],
- [0.268510, 0.009605, 0.335427],
- [0.269944, 0.014625, 0.341379],
- [0.271305, 0.019942, 0.347269],
- [0.272594, 0.025563, 0.353093],
- [0.273809, 0.031497, 0.358853],
- [0.274952, 0.037752, 0.364543],
- [0.276022, 0.044167, 0.370164],
- [0.277018, 0.050344, 0.375715],
- [0.277941, 0.056324, 0.381191],
- [0.278791, 0.062145, 0.386592],
- [0.279566, 0.067836, 0.391917],
- [0.280267, 0.073417, 0.397163],
- [0.280894, 0.078907, 0.402329],
- [0.281446, 0.084320, 0.407414],
- [0.281924, 0.089666, 0.412415],
- [0.282327, 0.094955, 0.417331],
- [0.282656, 0.100196, 0.422160],
- [0.282910, 0.105393, 0.426902],
- [0.283091, 0.110553, 0.431554],
- [0.283197, 0.115680, 0.436115],
- [0.283229, 0.120777, 0.440584],
- [0.283187, 0.125848, 0.444960],
- [0.283072, 0.130895, 0.449241],
- [0.282884, 0.135920, 0.453427],
- [0.282623, 0.140926, 0.457517],
- [0.282290, 0.145912, 0.461510],
- [0.281887, 0.150881, 0.465405],
- [0.281412, 0.155834, 0.469201],
- [0.280868, 0.160771, 0.472899],
- [0.280255, 0.165693, 0.476498],
- [0.279574, 0.170599, 0.479997],
- [0.278826, 0.175490, 0.483397],
- [0.278012, 0.180367, 0.486697],
- [0.277134, 0.185228, 0.489898],
- [0.276194, 0.190074, 0.493001],
- [0.275191, 0.194905, 0.496005],
- [0.274128, 0.199721, 0.498911],
- [0.273006, 0.204520, 0.501721],
- [0.271828, 0.209303, 0.504434],
- [0.270595, 0.214069, 0.507052],
- [0.269308, 0.218818, 0.509577],
- [0.267968, 0.223549, 0.512008],
- [0.266580, 0.228262, 0.514349],
- [0.265145, 0.232956, 0.516599],
- [0.263663, 0.237631, 0.518762],
- [0.262138, 0.242286, 0.520837],
- [0.260571, 0.246922, 0.522828],
- [0.258965, 0.251537, 0.524736],
- [0.257322, 0.256130, 0.526563],
- [0.255645, 0.260703, 0.528312],
- [0.253935, 0.265254, 0.529983],
- [0.252194, 0.269783, 0.531579],
- [0.250425, 0.274290, 0.533103],
- [0.248629, 0.278775, 0.534556],
- [0.246811, 0.283237, 0.535941],
- [0.244972, 0.287675, 0.537260],
- [0.243113, 0.292092, 0.538516],
- [0.241237, 0.296485, 0.539709],
- [0.239346, 0.300855, 0.540844],
- [0.237441, 0.305202, 0.541921],
- [0.235526, 0.309527, 0.542944],
- [0.233603, 0.313828, 0.543914],
- [0.231674, 0.318106, 0.544834],
- [0.229739, 0.322361, 0.545706],
- [0.227802, 0.326594, 0.546532],
- [0.225863, 0.330805, 0.547314],
- [0.223925, 0.334994, 0.548053],
- [0.221989, 0.339161, 0.548752],
- [0.220057, 0.343307, 0.549413],
- [0.218130, 0.347432, 0.550038],
- [0.216210, 0.351535, 0.550627],
- [0.214298, 0.355619, 0.551184],
- [0.212395, 0.359683, 0.551710],
- [0.210503, 0.363727, 0.552206],
- [0.208623, 0.367752, 0.552675],
- [0.206756, 0.371758, 0.553117],
- [0.204903, 0.375746, 0.553533],
- [0.203063, 0.379716, 0.553925],
- [0.201239, 0.383670, 0.554294],
- [0.199430, 0.387607, 0.554642],
- [0.197636, 0.391528, 0.554969],
- [0.195860, 0.395433, 0.555276],
- [0.194100, 0.399323, 0.555565],
- [0.192357, 0.403199, 0.555836],
- [0.190631, 0.407061, 0.556089],
- [0.188923, 0.410910, 0.556326],
- [0.187231, 0.414746, 0.556547],
- [0.185556, 0.418570, 0.556753],
- [0.183898, 0.422383, 0.556944],
- [0.182256, 0.426184, 0.557120],
- [0.180629, 0.429975, 0.557282],
- [0.179019, 0.433756, 0.557430],
- [0.177423, 0.437527, 0.557565],
- [0.175841, 0.441290, 0.557685],
- [0.174274, 0.445044, 0.557792],
- [0.172719, 0.448791, 0.557885],
- [0.171176, 0.452530, 0.557965],
- [0.169646, 0.456262, 0.558030],
- [0.168126, 0.459988, 0.558082],
- [0.166617, 0.463708, 0.558119],
- [0.165117, 0.467423, 0.558141],
- [0.163625, 0.471133, 0.558148],
- [0.162142, 0.474838, 0.558140],
- [0.160665, 0.478540, 0.558115],
- [0.159194, 0.482237, 0.558073],
- [0.157729, 0.485932, 0.558013],
- [0.156270, 0.489624, 0.557936],
- [0.154815, 0.493313, 0.557840],
- [0.153364, 0.497000, 0.557724],
- [0.151918, 0.500685, 0.557587],
- [0.150476, 0.504369, 0.557430],
- [0.149039, 0.508051, 0.557250],
- [0.147607, 0.511733, 0.557049],
- [0.146180, 0.515413, 0.556823],
- [0.144759, 0.519093, 0.556572],
- [0.143343, 0.522773, 0.556295],
- [0.141935, 0.526453, 0.555991],
- [0.140536, 0.530132, 0.555659],
- [0.139147, 0.533812, 0.555298],
- [0.137770, 0.537492, 0.554906],
- [0.136408, 0.541173, 0.554483],
- [0.135066, 0.544853, 0.554029],
- [0.133743, 0.548535, 0.553541],
- [0.132444, 0.552216, 0.553018],
- [0.131172, 0.555899, 0.552459],
- [0.129933, 0.559582, 0.551864],
- [0.128729, 0.563265, 0.551229],
- [0.127568, 0.566949, 0.550556],
- [0.126453, 0.570633, 0.549841],
- [0.125394, 0.574318, 0.549086],
- [0.124395, 0.578002, 0.548287],
- [0.123463, 0.581687, 0.547445],
- [0.122606, 0.585371, 0.546557],
- [0.121831, 0.589055, 0.545623],
- [0.121148, 0.592739, 0.544641],
- [0.120565, 0.596422, 0.543611],
- [0.120092, 0.600104, 0.542530],
- [0.119738, 0.603785, 0.541400],
- [0.119512, 0.607464, 0.540218],
- [0.119423, 0.611141, 0.538982],
- [0.119483, 0.614817, 0.537692],
- [0.119699, 0.618490, 0.536347],
- [0.120081, 0.622161, 0.534946],
- [0.120638, 0.625828, 0.533488],
- [0.121380, 0.629492, 0.531973],
- [0.122312, 0.633153, 0.530398],
- [0.123444, 0.636809, 0.528763],
- [0.124780, 0.640461, 0.527068],
- [0.126326, 0.644107, 0.525311],
- [0.128087, 0.647749, 0.523491],
- [0.130067, 0.651384, 0.521608],
- [0.132268, 0.655014, 0.519661],
- [0.134692, 0.658636, 0.517649],
- [0.137339, 0.662252, 0.515571],
- [0.140210, 0.665859, 0.513427],
- [0.143303, 0.669459, 0.511215],
- [0.146616, 0.673050, 0.508936],
- [0.150148, 0.676631, 0.506589],
- [0.153894, 0.680203, 0.504172],
- [0.157851, 0.683765, 0.501686],
- [0.162016, 0.687316, 0.499129],
- [0.166383, 0.690856, 0.496502],
- [0.170948, 0.694384, 0.493803],
- [0.175707, 0.697900, 0.491033],
- [0.180653, 0.701402, 0.488189],
- [0.185783, 0.704891, 0.485273],
- [0.191090, 0.708366, 0.482284],
- [0.196571, 0.711827, 0.479221],
- [0.202219, 0.715272, 0.476084],
- [0.208030, 0.718701, 0.472873],
- [0.214000, 0.722114, 0.469588],
- [0.220124, 0.725509, 0.466226],
- [0.226397, 0.728888, 0.462789],
- [0.232815, 0.732247, 0.459277],
- [0.239374, 0.735588, 0.455688],
- [0.246070, 0.738910, 0.452024],
- [0.252899, 0.742211, 0.448284],
- [0.259857, 0.745492, 0.444467],
- [0.266941, 0.748751, 0.440573],
- [0.274149, 0.751988, 0.436601],
- [0.281477, 0.755203, 0.432552],
- [0.288921, 0.758394, 0.428426],
- [0.296479, 0.761561, 0.424223],
- [0.304148, 0.764704, 0.419943],
- [0.311925, 0.767822, 0.415586],
- [0.319809, 0.770914, 0.411152],
- [0.327796, 0.773980, 0.406640],
- [0.335885, 0.777018, 0.402049],
- [0.344074, 0.780029, 0.397381],
- [0.352360, 0.783011, 0.392636],
- [0.360741, 0.785964, 0.387814],
- [0.369214, 0.788888, 0.382914],
- [0.377779, 0.791781, 0.377939],
- [0.386433, 0.794644, 0.372886],
- [0.395174, 0.797475, 0.367757],
- [0.404001, 0.800275, 0.362552],
- [0.412913, 0.803041, 0.357269],
- [0.421908, 0.805774, 0.351910],
- [0.430983, 0.808473, 0.346476],
- [0.440137, 0.811138, 0.340967],
- [0.449368, 0.813768, 0.335384],
- [0.458674, 0.816363, 0.329727],
- [0.468053, 0.818921, 0.323998],
- [0.477504, 0.821444, 0.318195],
- [0.487026, 0.823929, 0.312321],
- [0.496615, 0.826376, 0.306377],
- [0.506271, 0.828786, 0.300362],
- [0.515992, 0.831158, 0.294279],
- [0.525776, 0.833491, 0.288127],
- [0.535621, 0.835785, 0.281908],
- [0.545524, 0.838039, 0.275626],
- [0.555484, 0.840254, 0.269281],
- [0.565498, 0.842430, 0.262877],
- [0.575563, 0.844566, 0.256415],
- [0.585678, 0.846661, 0.249897],
- [0.595839, 0.848717, 0.243329],
- [0.606045, 0.850733, 0.236712],
- [0.616293, 0.852709, 0.230052],
- [0.626579, 0.854645, 0.223353],
- [0.636902, 0.856542, 0.216620],
- [0.647257, 0.858400, 0.209861],
- [0.657642, 0.860219, 0.203082],
- [0.668054, 0.861999, 0.196293],
- [0.678489, 0.863742, 0.189503],
- [0.688944, 0.865448, 0.182725],
- [0.699415, 0.867117, 0.175971],
- [0.709898, 0.868751, 0.169257],
- [0.720391, 0.870350, 0.162603],
- [0.730889, 0.871916, 0.156029],
- [0.741388, 0.873449, 0.149561],
- [0.751884, 0.874951, 0.143228],
- [0.762373, 0.876424, 0.137064],
- [0.772852, 0.877868, 0.131109],
- [0.783315, 0.879285, 0.125405],
- [0.793760, 0.880678, 0.120005],
- [0.804182, 0.882046, 0.114965],
- [0.814576, 0.883393, 0.110347],
- [0.824940, 0.884720, 0.106217],
- [0.835270, 0.886029, 0.102646],
- [0.845561, 0.887322, 0.099702],
- [0.855810, 0.888601, 0.097452],
- [0.866013, 0.889868, 0.095953],
- [0.876168, 0.891125, 0.095250],
- [0.886271, 0.892374, 0.095374],
- [0.896320, 0.893616, 0.096335],
- [0.906311, 0.894855, 0.098125],
- [0.916242, 0.896091, 0.100717],
- [0.926106, 0.897330, 0.104071],
- [0.935904, 0.898570, 0.108131],
- [0.945636, 0.899815, 0.112838],
- [0.955300, 0.901065, 0.118128],
- [0.964894, 0.902323, 0.123941],
- [0.974417, 0.903590, 0.130215],
- [0.983868, 0.904867, 0.136897],
- [0.993248, 0.906157, 0.143936]]
-
-
-cmaps = {}
-for (name, data) in (('magma', _magma_data),
- ('inferno', _inferno_data),
- ('plasma', _plasma_data),
- ('viridis', _viridis_data)):
-
- cmaps[name] = ListedColormap(data, name=name)
-
-magma = cmaps['magma']
-inferno = cmaps['inferno']
-plasma = cmaps['plasma']
-viridis = cmaps['viridis']
diff --git a/silx/gui/plot/MaskToolsWidget.py b/silx/gui/plot/MaskToolsWidget.py
index 6407d44..09c5ca5 100644
--- a/silx/gui/plot/MaskToolsWidget.py
+++ b/silx/gui/plot/MaskToolsWidget.py
@@ -35,13 +35,14 @@ from __future__ import division
__authors__ = ["T. Vincent", "P. Knobel"]
__license__ = "MIT"
-__date__ = "20/04/2017"
+__date__ = "20/06/2017"
import os
import sys
import numpy
import logging
+import collections
from silx.image import shapes
@@ -211,17 +212,13 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_maxLevelNumber = 255
def __init__(self, parent=None, plot=None):
+ super(MaskToolsWidget, self).__init__(parent, plot,
+ mask=ImageMask())
self._origin = (0., 0.) # Mask origin in plot
self._scale = (1., 1.) # Mask scale in plot
self._z = 1 # Mask layer in plot
self._data = numpy.zeros((0, 0), dtype=numpy.uint8) # Store image
- self._mask = ImageMask()
-
- super(MaskToolsWidget, self).__init__(parent, plot)
-
- self._initWidgets()
-
def setSelectionMask(self, mask, copy=True):
"""Set the mask to a new array.
@@ -239,6 +236,13 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_logger.error('Not an image, shape: %d', len(mask.shape))
return None
+ # ensure all mask attributes are synchronized with the active image
+ # and connect listener
+ activeImage = self.plot.getActiveImage()
+ if activeImage is not None and activeImage.getLegend() != self._maskName:
+ self._activeImageChanged()
+ self.plot.sigActiveImageChanged.connect(self._activeImageChanged)
+
if self._data.shape[0:2] == (0, 0) or mask.shape == self._data.shape[0:2]:
self._mask.setMask(mask, copy=copy)
self._mask.commit()
@@ -262,12 +266,22 @@ class MaskToolsWidget(BaseMaskToolsWidget):
"""Update mask image in plot"""
mask = self.getSelectionMask(copy=False)
if len(mask):
- self.plot.addImage(mask, legend=self._maskName,
- colormap=self._colormap,
- origin=self._origin,
- scale=self._scale,
- z=self._z,
- replace=False, resetzoom=False)
+ # get the mask from the plot
+ maskItem = self.plot.getImage(self._maskName)
+ mustBeAdded = maskItem is None
+ if mustBeAdded:
+ maskItem = items.MaskImageData()
+ maskItem._setLegend(self._maskName)
+ # update the items
+ maskItem.setData(mask, copy=False)
+ maskItem.setColormap(self._colormap)
+ maskItem.setOrigin(self._origin)
+ maskItem.setScale(self._scale)
+ maskItem.setZValue(self._z)
+
+ if mustBeAdded:
+ self.plot._add(maskItem)
+
elif self.plot.getImage(self._maskName):
self.plot.remove(self._maskName, kind='image')
@@ -281,7 +295,11 @@ class MaskToolsWidget(BaseMaskToolsWidget):
self.plot.sigActiveImageChanged.connect(self._activeImageChanged)
def hideEvent(self, event):
- self.plot.sigActiveImageChanged.disconnect(self._activeImageChanged)
+ try:
+ self.plot.sigActiveImageChanged.disconnect(
+ self._activeImageChanged)
+ except (RuntimeError, TypeError):
+ pass
if not self.browseAction.isChecked():
self.browseAction.trigger() # Disable drawing tool
@@ -337,8 +355,9 @@ class MaskToolsWidget(BaseMaskToolsWidget):
def _activeImageChanged(self, *args):
"""Update widget and mask according to active image changes"""
activeImage = self.plot.getActiveImage()
- if activeImage is None or activeImage.getLegend() == self._maskName:
- # No active image or active image is the mask...
+ if (activeImage is None or activeImage.getLegend() == self._maskName or
+ activeImage.getData(copy=False).size == 0):
+ # No active image or active image is the mask or image has no data...
self.setEnabled(False)
self._data = numpy.zeros((0, 0), dtype=numpy.uint8)
@@ -390,6 +409,14 @@ class MaskToolsWidget(BaseMaskToolsWidget):
_logger.error("Can't load filename '%s'", filename)
_logger.debug("Backtrace", exc_info=True)
raise RuntimeError('File "%s" is not a numpy file.', filename)
+ elif extension in ["tif", "tiff"]:
+ try:
+ image = TiffIO(filename, mode="r")
+ mask = image.getImage(0)
+ except Exception as e:
+ _logger.error("Can't load filename %s", filename)
+ _logger.debug("Backtrace", exc_info=True)
+ raise e
elif extension == "edf":
try:
mask = EdfFile(filename, access='r').GetData(0)
@@ -423,14 +450,21 @@ class MaskToolsWidget(BaseMaskToolsWidget):
dialog = qt.QFileDialog(self)
dialog.setWindowTitle("Load Mask")
dialog.setModal(1)
- filters = [
- 'EDF (*.edf)',
- 'TIFF (*.tif)',
- 'NumPy binary file (*.npy)',
- # Fit2D mask is displayed anyway fabio is here or not
- # to show to the user that the option exists
- 'Fit2D mask (*.msk)',
- ]
+
+ extensions = collections.OrderedDict()
+ extensions["EDF files"] = "*.edf"
+ extensions["TIFF files"] = "*.tif *.tiff"
+ extensions["NumPy binary files"] = "*.npy"
+ # Fit2D mask is displayed anyway fabio is here or not
+ # to show to the user that the option exists
+ extensions["Fit2D mask files"] = "*.msk"
+
+ filters = []
+ filters.append("All supported files (%s)" % " ".join(extensions.values()))
+ for name, extension in extensions.items():
+ filters.append("%s (%s)" % (name, extension))
+ filters.append("All files (*)")
+
dialog.setNameFilters(filters)
dialog.setFileMode(qt.QFileDialog.ExistingFile)
dialog.setDirectory(self.maskFileDir)
@@ -610,6 +644,5 @@ class MaskToolsDockWidget(BaseMaskToolsDockWidget):
:paran str name: The title of this widget
"""
def __init__(self, parent=None, plot=None, name='Mask'):
- super(MaskToolsDockWidget, self).__init__(parent, name)
- self.setWidget(MaskToolsWidget(plot=plot))
- self.widget().sigMaskChanged.connect(self._emitSigMaskChanged)
+ widget = MaskToolsWidget(plot=plot)
+ super(MaskToolsDockWidget, self).__init__(parent, name, widget)
diff --git a/silx/gui/plot/Plot.py b/silx/gui/plot/Plot.py
deleted file mode 100644
index fe0a7b8..0000000
--- a/silx/gui/plot/Plot.py
+++ /dev/null
@@ -1,2925 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-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.
-# ###########################################################################*/
-"""Plot API for 1D and 2D data.
-
-The :class:`Plot` implements the plot API initially provided in PyMca.
-
-
-Colormap
---------
-
-The :class:`Plot` uses a dictionary to describe a colormap.
-This dictionary has the following keys:
-
-- 'name': str, name of the colormap. Available colormap are returned by
- :meth:`Plot.getSupportedColormaps`.
- At least 'gray', 'reversed gray', 'temperature',
- 'red', 'green', 'blue' are supported.
-- 'normalization': Either 'linear' or 'log'
-- 'autoscale': bool, True to get bounds from the min and max of the
- data, False to use [vmin, vmax]
-- 'vmin': float, min value, ignored if autoscale is True
-- 'vmax': float, max value, ignored if autoscale is True
-- 'colors': optional, custom colormap.
- Nx3 or Nx4 numpy array of RGB(A) colors,
- either uint8 or float in [0, 1].
- If 'name' is None, then this array is used as the colormap.
-
-
-Plot Events
------------
-
-The Plot sends some event to the registered callback
-(See :meth:`Plot.setCallback`).
-Those events are sent as a dictionary with a key 'event' describing the kind
-of event.
-
-Drawing events
-..............
-
-'drawingProgress' and 'drawingFinished' events are sent during drawing
-interaction (See :meth:`Plot.setInteractiveMode`).
-
-- 'event': 'drawingProgress' or 'drawingFinished'
-- 'parameters': dict of parameters used by the drawing mode.
- It has the following keys: 'shape', 'label', 'color'.
- See :meth:`Plot.setInteractiveMode`.
-- 'points': Points (x, y) in data coordinates of the drawn shape.
- For 'hline' and 'vline', it is the 2 points defining the line.
- For 'line' and 'rectangle', it is the coordinates of the start
- drawing point and the latest drawing point.
- For 'polygon', it is the coordinates of all points of the shape.
-- 'type': The type of drawing in 'line', 'hline', 'polygon', 'rectangle',
- 'vline'.
-- 'xdata' and 'ydata': X coords and Y coords of shape points in data
- coordinates (as in 'points').
-
-When the type is 'rectangle', the following additional keys are provided:
-
-- 'x' and 'y': The origin of the rectangle in data coordinates
-- 'widht' and 'height': The size of the rectangle in data coordinates
-
-
-Mouse events
-............
-
-'mouseMoved', 'mouseClicked' and 'mouseDoubleClicked' events are sent for
-mouse events.
-
-They provide the following keys:
-
-- 'event': 'mouseMoved', 'mouseClicked' or 'mouseDoubleClicked'
-- 'button': the mouse button that was pressed in 'left', 'middle', 'right'
-- 'x' and 'y': The mouse position in data coordinates
-- 'xpixel' and 'ypixel': The mouse position in pixels
-
-
-Marker events
-.............
-
-'hover', 'markerClicked', 'markerMoving' and 'markerMoved' events are
-sent during interaction with markers.
-
-'hover' is sent when the mouse cursor is over a marker.
-'markerClicker' is sent when the user click on a selectable marker.
-'markerMoving' and 'markerMoved' are sent when a draggable marker is moved.
-
-They provide the following keys:
-
-- 'event': 'hover', 'markerClicked', 'markerMoving' or 'markerMoved'
-- 'button': the mouse button that is pressed in 'left', 'middle', 'right'
-- 'draggable': True if the marker is draggable, False otherwise
-- 'label': The legend associated with the clicked image or curve
-- 'selectable': True if the marker is selectable, False otherwise
-- 'type': 'marker'
-- 'x' and 'y': The mouse position in data coordinates
-- 'xdata' and 'ydata': The marker position in data coordinates
-
-'markerClicked' and 'markerMoving' events have a 'xpixel' and a 'ypixel'
-additional keys, that provide the mouse position in pixels.
-
-
-Image and curve events
-......................
-
-'curveClicked' and 'imageClicked' events are sent when a selectable curve
-or image is clicked.
-
-Both share the following keys:
-
-- 'event': 'curveClicked' or 'imageClicked'
-- 'button': the mouse button that was pressed in 'left', 'middle', 'right'
-- 'label': The legend associated with the clicked image or curve
-- 'type': The type of item in 'curve', 'image'
-- 'x' and 'y': The clicked position in data coordinates
-- 'xpixel' and 'ypixel': The clicked position in pixels
-
-'curveClicked' events have a 'xdata' and a 'ydata' additional keys, that
-provide the coordinates of the picked points of the curve.
-There can be more than one point of the curve being picked, and if a line of
-the curve is picked, only the first point of the line is included in the list.
-
-'imageClicked' have a 'col' and a 'row' additional keys, that provide
-the column and row index in the image array that was clicked.
-
-
-Limits changed events
-.....................
-
-'limitsChanged' events are sent when the limits of the plot are changed.
-This can results from user interaction or API calls.
-
-It provides the following keys:
-
-- 'event': 'limitsChanged'
-- 'source': id of the widget that emitted this event.
-- 'xdata': Range of X in graph coordinates: (xMin, xMax).
-- 'ydata': Range of Y in graph coordinates: (yMin, yMax).
-- 'y2data': Range of right axis in graph coordinates (y2Min, y2Max) or None.
-
-Plot state change events
-........................
-
-The following events are emitted when the plot is modified.
-They provide the new state:
-
-- 'setGraphCursor' event with a 'state' key (bool)
-- 'setGraphGrid' event with a 'which' key (str), see :meth:`setGraphGrid`
-- 'setKeepDataAspectRatio' event with a 'state' key (bool)
-- 'setXAxisAutoScale' event with a 'state' key (bool)
-- 'setXAxisLogarithmic' event with a 'state' key (bool)
-- 'setYAxisAutoScale' event with a 'state' key (bool)
-- 'setYAxisInverted' event with a 'state' key (bool)
-- 'setYAxisLogarithmic' event with a 'state' key (bool)
-
-A 'contentChanged' event is triggered when the content of the plot is updated.
-It provides the following keys:
-
-- 'action': The change of the plot: 'add' or 'remove'
-- 'kind': The kind of primitive changed: 'curve', 'image', 'item' or 'marker'
-- 'legend': The legend of the primitive changed.
-
-'activeCurveChanged' and 'activeImageChanged' events with the following keys:
-
-- 'legend': Name (str) of the current active item or None if no active item.
-- 'previous': Name (str) of the previous active item or None if no item was
- active. It is the same as 'legend' if 'updated' == True
-- 'updated': (bool) True if active item name did not changed,
- but active item data or style was updated.
-
-'interactiveModeChanged' event with a 'source' key identifying the object
-setting the interactive mode.
-"""
-
-from __future__ import division
-
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "16/02/2017"
-
-
-from collections import OrderedDict, namedtuple
-import itertools
-import logging
-
-import numpy
-
-# Import matplotlib backend here to init matplotlib our way
-from .backends.BackendMatplotlib import BackendMatplotlibQt
-
-try:
- from matplotlib import cm as matplotlib_cm
-except ImportError:
- matplotlib_cm = None
-
-from . import Colors
-from . import PlotInteraction
-from . import PlotEvents
-from . import _utils
-
-from . import items
-
-
-_logger = logging.getLogger(__name__)
-
-
-_COLORDICT = Colors.COLORDICT
-_COLORLIST = [_COLORDICT['black'],
- _COLORDICT['blue'],
- _COLORDICT['red'],
- _COLORDICT['green'],
- _COLORDICT['pink'],
- _COLORDICT['yellow'],
- _COLORDICT['brown'],
- _COLORDICT['cyan'],
- _COLORDICT['magenta'],
- _COLORDICT['orange'],
- _COLORDICT['violet'],
- # _COLORDICT['bluegreen'],
- _COLORDICT['grey'],
- _COLORDICT['darkBlue'],
- _COLORDICT['darkRed'],
- _COLORDICT['darkGreen'],
- _COLORDICT['darkCyan'],
- _COLORDICT['darkMagenta'],
- _COLORDICT['darkYellow'],
- _COLORDICT['darkBrown']]
-
-
-"""
-Object returned when requesting the data range.
-"""
-_PlotDataRange = namedtuple('PlotDataRange',
- ['x', 'y', 'yright'])
-
-
-class Plot(object):
- """This class implements the plot API initially provided in PyMca.
-
- Supported backends:
-
- - 'matplotlib' and 'mpl': Matplotlib with Qt.
- - 'opengl' and 'gl': OpenGL backend (requires PyOpenGL and OpenGL >= 2.1)
- - 'none': No backend, to run headless for testing purpose.
-
- :param parent: The parent widget of the plot (Default: None)
- :param backend: The backend to use. A str in:
- 'matplotlib', 'mpl', 'opengl', 'gl', 'none'
- or a :class:`BackendBase.BackendBase` class
- """
-
- DEFAULT_BACKEND = 'matplotlib'
- """Class attribute setting the default backend for all instances."""
-
- colorList = _COLORLIST
- colorDict = _COLORDICT
-
- def __init__(self, parent=None, backend=None):
- self._autoreplot = False
- self._dirty = False
- self._cursorInPlot = False
-
- if backend is None:
- backend = self.DEFAULT_BACKEND
-
- if hasattr(backend, "__call__"):
- self._backend = backend(self, parent)
-
- elif hasattr(backend, "lower"):
- lowerCaseString = backend.lower()
- if lowerCaseString in ("matplotlib", "mpl"):
- backendClass = BackendMatplotlibQt
- elif lowerCaseString in ('gl', 'opengl'):
- from .backends.BackendOpenGL import BackendOpenGL
- backendClass = BackendOpenGL
- elif lowerCaseString == 'none':
- from .backends.BackendBase import BackendBase as backendClass
- else:
- raise ValueError("Backend not supported %s" % backend)
- self._backend = backendClass(self, parent)
-
- else:
- raise ValueError("Backend not supported %s" % str(backend))
-
- super(Plot, self).__init__()
-
- self.setCallback() # set _callback
-
- # Items handling
- self._content = OrderedDict()
- self._contentToUpdate = set()
-
- self._dataRange = None
-
- # line types
- self._styleList = ['-', '--', '-.', ':']
- self._colorIndex = 0
- self._styleIndex = 0
-
- self._activeCurveHandling = True
- self._activeCurveColor = "#000000"
- self._activeLegend = {'curve': None, 'image': None,
- 'scatter': None}
-
- # default properties
- self._cursorConfiguration = None
-
- self._logY = False
- self._logX = False
- self._xAutoScale = True
- self._yAutoScale = True
- self._grid = None
-
- # Store default labels provided to setGraph[X|Y]Label
- self._defaultLabels = {'x': '', 'y': '', 'yright': ''}
- # Store currently displayed labels
- # Current label can differ from input one with active curve handling
- self._currentLabels = {'x': '', 'y': '', 'yright': ''}
-
- self._graphTitle = ''
-
- self.setGraphTitle()
- self.setGraphXLabel()
- self.setGraphYLabel()
- self.setGraphYLabel('', axis='right')
-
- self.setDefaultColormap() # Init default colormap
-
- self.setDefaultPlotPoints(False)
- self.setDefaultPlotLines(True)
-
- self._eventHandler = PlotInteraction.PlotInteraction(self)
- self._eventHandler.setInteractiveMode('zoom', color=(0., 0., 0., 1.))
-
- self._pressedButtons = [] # Currently pressed mouse buttons
-
- self._defaultDataMargins = (0., 0., 0., 0.)
-
- # Only activate autoreplot at the end
- # This avoids errors when loaded in Qt designer
- self._dirty = False
- self._autoreplot = True
-
- def _getDirtyPlot(self):
- """Return the plot dirty flag.
-
- If False, the plot has not changed since last replot.
- If True, the full plot need to be redrawn.
- If 'overlay', only the overlay has changed since last replot.
-
- It can be accessed by backend to check the dirty state.
-
- :return: False, True, 'overlay'
- """
- return self._dirty
-
- def _setDirtyPlot(self, overlayOnly=False):
- """Mark the plot as needing redraw
-
- :param bool overlayOnly: True to redraw only the overlay,
- False to redraw everything
- """
- wasDirty = self._dirty
-
- if not self._dirty and overlayOnly:
- self._dirty = 'overlay'
- else:
- self._dirty = True
-
- if self._autoreplot and not wasDirty:
- self._backend.postRedisplay()
-
- def _invalidateDataRange(self):
- """
- Notifies this Plot instance that the range has changed and will have
- to be recomputed.
- """
- self._dataRange = None
-
- def _updateDataRange(self):
- """
- Recomputes the range of the data displayed on this Plot.
- """
- xMin = yMinLeft = yMinRight = float('nan')
- xMax = yMaxLeft = yMaxRight = float('nan')
-
- for item in self._content.values():
- if item.isVisible():
- bounds = item.getBounds()
- if bounds is not None:
- xMin = numpy.nanmin([xMin, bounds[0]])
- xMax = numpy.nanmax([xMax, bounds[1]])
- # Take care of right axis
- if (isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right'):
- yMinRight = numpy.nanmin([yMinRight, bounds[2]])
- yMaxRight = numpy.nanmax([yMaxRight, bounds[3]])
- else:
- yMinLeft = numpy.nanmin([yMinLeft, bounds[2]])
- yMaxLeft = numpy.nanmax([yMaxLeft, bounds[3]])
-
- def lGetRange(x, y):
- return None if numpy.isnan(x) and numpy.isnan(y) else (x, y)
- xRange = lGetRange(xMin, xMax)
- yLeftRange = lGetRange(yMinLeft, yMaxLeft)
- yRightRange = lGetRange(yMinRight, yMaxRight)
-
- self._dataRange = _PlotDataRange(x=xRange,
- y=yLeftRange,
- yright=yRightRange)
-
- def getDataRange(self):
- """
- Returns this Plot's data range.
-
- :return: a namedtuple with the following members :
- x, y (left y axis), yright. Each member is a tuple (min, max)
- or None if no data is associated with the axis.
- :rtype: namedtuple
- """
- if self._dataRange is None:
- self._updateDataRange()
- return self._dataRange
-
- # Content management
-
- @staticmethod
- def _itemKey(item):
- """Build the key of given :class:`Item` in the plot
-
- :param Item item: The item to make the key from
- :return: (legend, kind)
- :rtype: (str, str)
- """
- if isinstance(item, items.Curve):
- kind = 'curve'
- elif isinstance(item, items.ImageBase):
- kind = 'image'
- elif isinstance(item, items.Scatter):
- kind = 'scatter'
- elif isinstance(item, (items.Marker,
- items.XMarker, items.YMarker)):
- kind = 'marker'
- elif isinstance(item, items.Shape):
- kind = 'item'
- elif isinstance(item, items.Histogram):
- kind = 'histogram'
- else:
- raise ValueError('Unsupported item type %s' % type(item))
-
- return item.getLegend(), kind
-
- def _add(self, item):
- """Add the given :class:`Item` to the plot.
-
- :param Item item: The item to append to the plot content
- """
- key = self._itemKey(item)
- if key in self._content:
- raise RuntimeError('Item already in the plot')
-
- # Add item to plot
- self._content[key] = item
- item._setPlot(self)
- if item.isVisible():
- self._itemRequiresUpdate(item)
- if isinstance(item, (items.Curve, items.ImageBase)):
- self._invalidateDataRange() # TODO handle this automatically
-
- def _remove(self, item):
- """Remove the given :class:`Item` from the plot.
-
- :param Item item: The item to remove from the plot content
- """
- key = self._itemKey(item)
- if key not in self._content:
- raise RuntimeError('Item not in the plot')
-
- # Remove item from plot
- self._content.pop(key)
- self._contentToUpdate.discard(item)
- if item.isVisible():
- self._setDirtyPlot(overlayOnly=item.isOverlay())
- if item.getBounds() is not None:
- self._invalidateDataRange()
- item._removeBackendRenderer(self._backend)
- item._setPlot(None)
-
- def _itemRequiresUpdate(self, item):
- """Called by items in the plot for asynchronous update
-
- :param Item item: The item that required update
- """
- assert item.getPlot() == self
- self._contentToUpdate.add(item)
- self._setDirtyPlot(overlayOnly=item.isOverlay())
-
- # Add
-
- # add * input arguments management:
- # If an arg is set, then use it.
- # Else:
- # If a curve with the same legend exists, then use its arg value
- # Else, use a default value.
- # Store used value.
- # This value is used when curve is updated either internally or by user.
-
- def addCurve(self, x, y, legend=None, info=None,
- replace=False, replot=None,
- color=None, symbol=None,
- linewidth=None, linestyle=None,
- xlabel=None, ylabel=None, yaxis=None,
- xerror=None, yerror=None, z=None, selectable=None,
- fill=None, resetzoom=True,
- histogram=None, copy=True, **kw):
- """Add a 1D curve given by x an y to the graph.
-
- Curves are uniquely identified by their legend.
- To add multiple curves, call :meth:`addCurve` multiple times with
- different legend argument.
- To replace an existing curve, call :meth:`addCurve` with the
- existing curve legend.
- If you want to display the curve values as an histogram see the
- histogram parameter or :meth:`addHistogram`.
-
- When curve parameters are not provided, if a curve with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- If you attempt to plot an histogram you can set edges values in x.
- In this case len(x) = len(y) + 1
- :param numpy.ndarray y: The data corresponding to the y coordinates
- :param str legend: The legend to be associated to the curve (or None)
- :param info: User-defined information associated to the curve
- :param bool replace: True (the default) to delete already existing
- curves
- :param color: color(s) to be used
- :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in Colors.py
- :param str symbol: Symbol to be drawn at each (x, y) position::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
- - None (the default) to use default symbol
-
- :param float linewidth: The width of the curve in pixels (Default: 1).
- :param str linestyle: Type of line::
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
- - None (the default) to use default line style
-
- :param str xlabel: Label to show on the X axis when the curve is active
- or None to keep default axis label.
- :param str ylabel: Label to show on the Y axis when the curve is active
- or None to keep default axis label.
- :param str yaxis: The Y axis this curve is attached to.
- Either 'left' (the default) or 'right'
- :param xerror: Values with the uncertainties on the x values
- :type xerror: A float, or a numpy.ndarray of float32.
- If it is an array, it can either be a 1D array of
- same length as the data or a 2D array with 2 rows
- of same length as the data: row 0 for positive errors,
- row 1 for negative errors.
- :param yerror: Values with the uncertainties on the y values
- :type yerror: A float, or a numpy.ndarray of float32. See xerror.
- :param int z: Layer on which to draw the curve (default: 1)
- This allows to control the overlay.
- :param bool selectable: Indicate if the curve can be selected.
- (Default: True)
- :param bool fill: True to fill the curve, False otherwise (default).
- :param bool resetzoom: True (the default) to reset the zoom.
- :param str histogram: if not None then the curve will be draw as an
- histogram. The step for each values of the curve can be set to the
- left, center or right of the original x curve values.
- If histogram is not None and len(x) == len(y)+1 then x is directly
- take as edges of the histogram.
- Type of histogram::
-
- - None (default)
- - 'left'
- - 'right'
- - 'center'
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- :returns: The key string identify this curve
- """
- # Deprecation warnings
- if replot is not None:
- _logger.warning(
- 'addCurve deprecated replot argument, use resetzoom instead')
- resetzoom = replot and resetzoom
-
- if kw:
- _logger.warning('addCurve: deprecated extra arguments')
-
- # This is an histogram, use addHistogram
- if histogram is not None:
- histoLegend = self.addHistogram(histogram=y,
- edges=x,
- legend=legend,
- color=color,
- fill=fill,
- align=histogram,
- copy=copy)
- histo = self.getHistogram(histoLegend)
-
- histo.setInfo(info)
- if linewidth is not None:
- histo.setLineWidth(linewidth)
- if linestyle is not None:
- histo.setLineStyle(linestyle)
- if xlabel is not None:
- _logger.warning(
- 'addCurve: Histogram does not support xlabel argument')
- if ylabel is not None:
- _logger.warning(
- 'addCurve: Histogram does not support ylabel argument')
- if yaxis is not None:
- histo.setYAxis(yaxis)
- if z is not None:
- histo.setZValue(z)
- if selectable is not None:
- _logger.warning(
- 'addCurve: Histogram does not support selectable argument')
-
- return
-
- legend = 'Unnamed curve 1.1' if legend is None else str(legend)
-
- # Check if curve was previously active
- wasActive = self.getActiveCurve(just_legend=True) == legend
-
- # Create/Update curve object
- curve = self.getCurve(legend)
- if curve is None:
- # No previous curve, create a default one and add it to the plot
- curve = items.Curve() if histogram is None else items.Histogram()
- curve._setLegend(legend)
- # Set default color, linestyle and symbol
- default_color, default_linestyle = self._getColorAndStyle()
- curve.setColor(default_color)
- curve.setLineStyle(default_linestyle)
- curve.setSymbol(self._defaultPlotPoints)
- self._add(curve)
-
- # Override previous/default values with provided ones
- curve.setInfo(info)
- if color is not None:
- curve.setColor(color)
- if symbol is not None:
- curve.setSymbol(symbol)
- if linewidth is not None:
- curve.setLineWidth(linewidth)
- if linestyle is not None:
- curve.setLineStyle(linestyle)
- if xlabel is not None:
- curve._setXLabel(xlabel)
- if ylabel is not None:
- curve._setYLabel(ylabel)
- if yaxis is not None:
- curve.setYAxis(yaxis)
- if z is not None:
- curve.setZValue(z)
- if selectable is not None:
- curve._setSelectable(selectable)
- if fill is not None:
- curve.setFill(fill)
-
- # Set curve data
- # If errors not provided, reuse previous ones
- # TODO: Issue if size of data change but not that of errors
- if xerror is None:
- xerror = curve.getXErrorData(copy=False)
- if yerror is None:
- yerror = curve.getYErrorData(copy=False)
-
- curve.setData(x, y, xerror, yerror, copy=copy)
-
- if replace: # Then remove all other curves
- for c in self.getAllCurves(withhidden=True):
- if c is not curve:
- self._remove(c)
-
- self.notify(
- 'contentChanged', action='add', kind='curve', legend=legend)
-
- if wasActive:
- self.setActiveCurve(curve.getLegend())
-
- if resetzoom:
- # We ask for a zoom reset in order to handle the plot scaling
- # if the user does not want that, autoscale of the different
- # axes has to be set to off.
- self.resetZoom()
-
- return legend
-
- def addHistogram(self,
- histogram,
- edges,
- legend=None,
- color=None,
- fill=None,
- align='center',
- resetzoom=True,
- copy=True):
- """Add an histogram to the graph.
-
- This is NOT computing the histogram, this method takes as parameter
- already computed histogram values.
-
- Histogram are uniquely identified by their legend.
- To add multiple histograms, call :meth:`addHistogram` multiple times
- with different legend argument.
-
- When histogram parameters are not provided, if an histogram with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray histogram: The values of the histogram.
- :param numpy.ndarray edges:
- The bin edges of the histogram.
- If histogram and edges have the same length, the bin edges
- are computed according to the align parameter.
- :param str legend:
- The legend to be associated to the histogram (or None)
- :param color: color to be used
- :type color: str ("#RRGGBB") or RGB unsigned byte array or
- one of the predefined color names defined in Colors.py
- :param bool fill: True to fill the curve, False otherwise (default).
- :param str align:
- In case histogram values and edges have the same length N,
- the N+1 bin edges are computed according to the alignment in:
- 'center' (default), 'left', 'right'.
- :param bool resetzoom: True (the default) to reset the zoom.
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- :returns: The key string identify this histogram
- """
- legend = 'Unnamed histogram' if legend is None else str(legend)
-
- # Create/Update histogram object
- histo = self.getHistogram(legend)
- if histo is None:
- # No previous histogram, create a default one and
- # add it to the plot
- histo = items.Histogram()
- histo._setLegend(legend)
- histo.setColor(self._getColorAndStyle()[0])
- self._add(histo)
-
- # Override previous/default values with provided ones
- if color is not None:
- histo.setColor(color)
- if fill is not None:
- histo.setFill(fill)
-
- # Set histogram data
- histo.setData(histogram, edges, align=align, copy=copy)
-
- self.notify(
- 'contentChanged', action='add', kind='histogram', legend=legend)
-
- if resetzoom:
- # We ask for a zoom reset in order to handle the plot scaling
- # if the user does not want that, autoscale of the different
- # axes has to be set to off.
- self.resetZoom()
-
- return legend
-
- def addImage(self, data, legend=None, info=None,
- replace=True, replot=None,
- xScale=None, yScale=None, z=None,
- selectable=None, draggable=None,
- colormap=None, pixmap=None,
- xlabel=None, ylabel=None,
- origin=None, scale=None,
- resetzoom=True, copy=True, **kw):
- """Add a 2D dataset or an image to the plot.
-
- It displays either an array of data using a colormap or a RGB(A) image.
-
- Images are uniquely identified by their legend.
- To add multiple images, call :meth:`addImage` multiple times with
- different legend argument.
- To replace/update an existing image, call :meth:`addImage` with the
- existing image legend.
-
- When image parameters are not provided, if an image with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray data: (nrows, ncolumns) data or
- (nrows, ncolumns, RGBA) ubyte array
- :param str legend: The legend to be associated to the image (or None)
- :param info: User-defined information associated to the image
- :param bool replace: True (default) to delete already existing images
- :param int z: Layer on which to draw the image (default: 0)
- This allows to control the overlay.
- :param bool selectable: Indicate if the image can be selected.
- (default: False)
- :param bool draggable: Indicate if the image can be moved.
- (default: False)
- :param dict colormap: Description of the colormap to use (or None)
- This is ignored if data is a RGB(A) image.
- See :mod:`Plot` for the documentation
- of the colormap dict.
- :param pixmap: Pixmap representation of the data (if any)
- :type pixmap: (nrows, ncolumns, RGBA) ubyte array or None (default)
- :param str xlabel: X axis label to show when this curve is active,
- or None to keep default axis label.
- :param str ylabel: Y axis label to show when this curve is active,
- or None to keep default axis label.
- :param origin: (origin X, origin Y) of the data.
- It is possible to pass a single float if both
- coordinates are equal.
- Default: (0., 0.)
- :type origin: float or 2-tuple of float
- :param scale: (scale X, scale Y) of the data.
- It is possible to pass a single float if both
- coordinates are equal.
- Default: (1., 1.)
- :type scale: float or 2-tuple of float
- :param bool resetzoom: True (the default) to reset the zoom.
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- :returns: The key string identify this image
- """
- # Deprecation warnings
- if xScale is not None or yScale is not None:
- _logger.warning(
- 'addImage deprecated xScale and yScale arguments,'
- 'use origin, scale arguments instead.')
- if origin is None and scale is None:
- origin = xScale[0], yScale[0]
- scale = xScale[1], yScale[1]
- else:
- _logger.warning(
- 'addCurve: xScale, yScale and origin, scale arguments'
- ' are conflicting. xScale and yScale are ignored.'
- ' Use only origin, scale arguments.')
-
- if replot is not None:
- _logger.warning(
- 'addImage deprecated replot argument, use resetzoom instead')
- resetzoom = replot and resetzoom
-
- if kw:
- _logger.warning('addImage: deprecated extra arguments')
-
- legend = "Unnamed Image 1.1" if legend is None else str(legend)
-
- # Check if image was previously active
- wasActive = self.getActiveImage(just_legend=True) == legend
-
- data = numpy.array(data, copy=False)
- assert data.ndim in (2, 3)
-
- image = self.getImage(legend)
- if image is not None and image.getData(copy=False).ndim != data.ndim:
- # Update a data image with RGBA image or the other way around:
- # Remove previous image
- # In this case, we don't retrieve defaults from the previous image
- self._remove(image)
- image = None
-
- if image is None:
- # No previous image, create a default one and add it to the plot
- if data.ndim == 2:
- image = items.ImageData()
- image.setColormap(self.getDefaultColormap())
- else:
- image = items.ImageRgba()
- image._setLegend(legend)
- self._add(image)
-
- # Override previous/default values with provided ones
- image.setInfo(info)
- if origin is not None:
- image.setOrigin(origin)
- if scale is not None:
- image.setScale(scale)
- if z is not None:
- image.setZValue(z)
- if selectable is not None:
- image._setSelectable(selectable)
- if draggable is not None:
- image._setDraggable(draggable)
- if colormap is not None and isinstance(image, items.ColormapMixIn):
- image.setColormap(colormap)
- if xlabel is not None:
- image._setXLabel(xlabel)
- if ylabel is not None:
- image._setYLabel(ylabel)
-
- if data.ndim == 2:
- image.setData(data, alternative=pixmap, copy=copy)
- else: # RGB(A) image
- if pixmap is not None:
- _logger.warning(
- 'addImage: pixmap argument ignored when data is RGB(A)')
- image.setData(data, copy=copy)
-
- if replace:
- for img in self.getAllImages():
- if img is not image:
- self._remove(img)
-
- if len(self.getAllImages()) == 1 or wasActive:
- self.setActiveImage(legend)
-
- self.notify(
- 'contentChanged', action='add', kind='image', legend=legend)
-
- if resetzoom:
- # We ask for a zoom reset in order to handle the plot scaling
- # if the user does not want that, autoscale of the different
- # axes has to be set to off.
- self.resetZoom()
-
- return legend
-
- def addScatter(self, x, y, value, legend=None, colormap=None,
- info=None, symbol=None, xerror=None, yerror=None,
- z=None, copy=True):
- """Add a (x, y, value) scatter to the graph.
-
- Scatters are uniquely identified by their legend.
- To add multiple scatters, call :meth:`addScatter` multiple times with
- different legend argument.
- To replace/update an existing scatter, call :meth:`addScatter` with the
- existing scatter legend.
-
- When scatter parameters are not provided, if a scatter with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- :param numpy.ndarray y: The data corresponding to the y coordinates
- :param numpy.ndarray value: The data value associated with each point
- :param str legend: The legend to be associated to the scatter (or None)
- :param dict colormap: The colormap to be used for the scatter (or None)
- See :mod:`Plot` for the documentation
- of the colormap dict.
- :param info: User-defined information associated to the curve
- :param str symbol: Symbol to be drawn at each (x, y) position::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
- - None (the default) to use default symbol
-
- :param xerror: Values with the uncertainties on the x values
- :type xerror: A float, or a numpy.ndarray of float32.
- If it is an array, it can either be a 1D array of
- same length as the data or a 2D array with 2 rows
- of same length as the data: row 0 for positive errors,
- row 1 for negative errors.
- :param yerror: Values with the uncertainties on the y values
- :type yerror: A float, or a numpy.ndarray of float32. See xerror.
- :param int z: Layer on which to draw the scatter (default: 1)
- This allows to control the overlay.
-
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- :returns: The key string identify this scatter
- """
- legend = 'Unnamed scatter 1.1' if legend is None else str(legend)
-
- # Check if scatter was previously active
- wasActive = self._getActiveItem(kind='scatter',
- just_legend=True) == legend
-
- # Create/Update curve object
- scatter = self._getItem(kind='scatter', legend=legend)
- if scatter is None:
- # No previous scatter, create a default one and add it to the plot
- scatter = items.Scatter()
- scatter._setLegend(legend)
- scatter.setColormap(self.getDefaultColormap())
- self._add(scatter)
-
- # Override previous/default values with provided ones
- scatter.setInfo(info)
- if symbol is not None:
- scatter.setSymbol(symbol)
- if z is not None:
- scatter.setZValue(z)
- if colormap is not None:
- scatter.setColormap(colormap)
-
- # Set scatter data
- # If errors not provided, reuse previous ones
- if xerror is None:
- xerror = scatter.getXErrorData(copy=False)
- if xerror is not None and len(xerror) != len(x):
- xerror = None
- if yerror is None:
- yerror = scatter.getYErrorData(copy=False)
- if yerror is not None and len(yerror) != len(y):
- yerror = None
-
- scatter.setData(x, y, value, xerror, yerror, copy=copy)
-
- self.notify(
- 'contentChanged', action='add', kind='scatter', legend=legend)
-
- if len(self._getItems(kind="scatter")) == 1 or wasActive:
- self._setActiveItem('scatter', scatter.getLegend())
-
- return legend
-
- def addItem(self, xdata, ydata, legend=None, info=None,
- replace=False,
- shape="polygon", color='black', fill=True,
- overlay=False, z=None, **kw):
- """Add an item (i.e. a shape) to the plot.
-
- Items are uniquely identified by their legend.
- To add multiple items, call :meth:`addItem` multiple times with
- different legend argument.
- To replace/update an existing item, call :meth:`addItem` with the
- existing item legend.
-
- :param numpy.ndarray xdata: The X coords of the points of the shape
- :param numpy.ndarray ydata: The Y coords of the points of the shape
- :param str legend: The legend to be associated to the item
- :param info: User-defined information associated to the item
- :param bool replace: True (default) to delete already existing images
- :param str shape: Type of item to be drawn in
- hline, polygon (the default), rectangle, vline,
- polylines
- :param str color: Color of the item, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool fill: True (the default) to fill the shape
- :param bool overlay: True if item is an overlay (Default: False).
- This allows for rendering optimization if this
- item is changed often.
- :param int z: Layer on which to draw the item (default: 2)
- :returns: The key string identify this item
- """
- # expected to receive the same parameters as the signal
-
- if kw:
- _logger.warning('addItem deprecated parameters: %s', str(kw))
-
- legend = "Unnamed Item 1.1" if legend is None else str(legend)
-
- z = int(z) if z is not None else 2
-
- if replace:
- self.remove(kind='item')
- else:
- self.remove(legend, kind='item')
-
- item = items.Shape(shape)
- item._setLegend(legend)
- item.setInfo(info)
- item.setColor(color)
- item.setFill(fill)
- item.setOverlay(overlay)
- item.setZValue(z)
- item.setPoints(numpy.array((xdata, ydata)).T)
-
- self._add(item)
-
- self.notify('contentChanged', action='add', kind='item', legend=legend)
-
- return legend
-
- def addXMarker(self, x, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- **kw):
- """Add a vertical line marker to the plot.
-
- Markers are uniquely identified by their legend.
- As opposed to curves, images and items, two calls to
- :meth:`addXMarker` without legend argument adds two markers with
- different identifying legends.
-
- :param float x: Position of the marker on the X axis in data
- coordinates
- :param str legend: Legend associated to the marker to identify it
- :param str text: Text to display on the marker.
- :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool selectable: Indicate if the marker can be selected.
- (default: False)
- :param bool draggable: Indicate if the marker can be moved.
- (default: False)
- :param constraint: A function filtering marker displacement by
- dragging operations or None for no filter.
- This function is called each time a marker is
- moved.
- This parameter is only used if draggable is True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- and that returns the filtered coordinates.
- :return: The key string identify this marker
- """
- if kw:
- _logger.warning(
- 'addXMarker deprecated extra parameters: %s', str(kw))
-
- return self._addMarker(x=x, y=None, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=None, constraint=constraint)
-
- def addYMarker(self, y,
- legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- **kw):
- """Add a horizontal line marker to the plot.
-
- Markers are uniquely identified by their legend.
- As opposed to curves, images and items, two calls to
- :meth:`addYMarker` without legend argument adds two markers with
- different identifying legends.
-
- :param float y: Position of the marker on the Y axis in data
- coordinates
- :param str legend: Legend associated to the marker to identify it
- :param str text: Text to display next to the marker.
- :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool selectable: Indicate if the marker can be selected.
- (default: False)
- :param bool draggable: Indicate if the marker can be moved.
- (default: False)
- :param constraint: A function filtering marker displacement by
- dragging operations or None for no filter.
- This function is called each time a marker is
- moved.
- This parameter is only used if draggable is True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- and that returns the filtered coordinates.
- :return: The key string identify this marker
- """
- if kw:
- _logger.warning(
- 'addYMarker deprecated extra parameters: %s', str(kw))
-
- return self._addMarker(x=None, y=y, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=None, constraint=constraint)
-
- def addMarker(self, x, y, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- symbol='+',
- constraint=None,
- **kw):
- """Add a point marker to the plot.
-
- Markers are uniquely identified by their legend.
- As opposed to curves, images and items, two calls to
- :meth:`addMarker` without legend argument adds two markers with
- different identifying legends.
-
- :param float x: Position of the marker on the X axis in data
- coordinates
- :param float y: Position of the marker on the Y axis in data
- coordinates
- :param str legend: Legend associated to the marker to identify it
- :param str text: Text to display next to the marker
- :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool selectable: Indicate if the marker can be selected.
- (default: False)
- :param bool draggable: Indicate if the marker can be moved.
- (default: False)
- :param str symbol: Symbol representing the marker in::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross (the default)
- - 'x' x-cross
- - 'd' diamond
- - 's' square
-
- :param constraint: A function filtering marker displacement by
- dragging operations or None for no filter.
- This function is called each time a marker is
- moved.
- This parameter is only used if draggable is True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- and that returns the filtered coordinates.
- :return: The key string identify this marker
- """
- if kw:
- _logger.warning(
- 'addMarker deprecated extra parameters: %s', str(kw))
-
- if x is None:
- xmin, xmax = self.getGraphXLimits()
- x = 0.5 * (xmax + xmin)
-
- if y is None:
- ymin, ymax = self.getGraphYLimits()
- y = 0.5 * (ymax + ymin)
-
- return self._addMarker(x=x, y=y, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=symbol, constraint=constraint)
-
- def _addMarker(self, x, y, legend,
- text, color,
- selectable, draggable,
- symbol, constraint):
- """Common method for adding point, vline and hline marker.
-
- See :meth:`addMarker` for argument documentation.
- """
- assert (x, y) != (None, None)
-
- if legend is None: # Find an unused legend
- markerLegends = self._getAllMarkers(just_legend=True)
- for index in itertools.count():
- legend = "Unnamed Marker %d" % index
- if legend not in markerLegends:
- break # Keep this legend
- legend = str(legend)
-
- if x is None:
- markerClass = items.YMarker
- elif y is None:
- markerClass = items.XMarker
- else:
- markerClass = items.Marker
-
- # Create/Update marker object
- marker = self._getMarker(legend)
- if marker is not None and not isinstance(marker, markerClass):
- _logger.warning('Adding marker with same legend'
- ' but different type replaces it')
- self._remove(marker)
- marker = None
-
- if marker is None:
- # No previous marker, create one
- marker = markerClass()
- marker._setLegend(legend)
- self._add(marker)
-
- if text is not None:
- marker.setText(text)
- if color is not None:
- marker.setColor(color)
- if selectable is not None:
- marker._setSelectable(selectable)
- if draggable is not None:
- marker._setDraggable(draggable)
- if symbol is not None:
- marker.setSymbol(symbol)
-
- # TODO to improve, but this ensure constraint is applied
- marker.setPosition(x, y)
- if constraint is not None:
- marker._setConstraint(constraint)
- marker.setPosition(x, y)
-
- self.notify(
- 'contentChanged', action='add', kind='marker', legend=legend)
-
- return legend
-
- # Hide
-
- def isCurveHidden(self, legend):
- """Returns True if the curve associated to legend is hidden, else False
-
- :param str legend: The legend key identifying the curve
- :return: True if the associated curve is hidden, False otherwise
- """
- curve = self._getItem('curve', legend)
- return curve is not None and not curve.isVisible()
-
- def hideCurve(self, legend, flag=True, replot=None):
- """Show/Hide the curve associated to legend.
-
- Even when hidden, the curve is kept in the list of curves.
-
- :param str legend: The legend associated to the curve to be hidden
- :param bool flag: True (default) to hide the curve, False to show it
- """
- if replot is not None:
- _logger.warning('hideCurve deprecated replot parameter')
-
- curve = self._getItem('curve', legend)
- if curve is None:
- _logger.warning('Curve not in plot: %s', legend)
- return
-
- isVisible = not flag
- if isVisible != curve.isVisible():
- curve.setVisible(isVisible)
-
- # Remove
-
- ITEM_KINDS = 'curve', 'image', 'scatter', 'item', 'marker', 'histogram'
-
- def remove(self, legend=None, kind=ITEM_KINDS):
- """Remove one or all element(s) of the given legend and kind.
-
- Examples:
-
- - ``remove()`` clears the plot
- - ``remove(kind='curve')`` removes all curves from the plot
- - ``remove('myCurve', kind='curve')`` removes the curve with
- legend 'myCurve' from the plot.
- - ``remove('myImage, kind='image')`` removes the image with
- legend 'myImage' from the plot.
- - ``remove('myImage')`` removes elements (for instance curve, image,
- item and marker) with legend 'myImage'.
-
- :param str legend: The legend associated to the element to remove,
- or None to remove
- :param kind: The kind of elements to remove from the plot.
- In: 'all', 'curve', 'image', 'item', 'marker'.
- By default, it removes all kind of elements.
- :type kind: str or tuple of str to specify multiple kinds.
- """
- if kind is 'all': # Replace all by tuple of all kinds
- kind = self.ITEM_KINDS
-
- if kind in self.ITEM_KINDS: # Kind is a str, make it a tuple
- kind = (kind,)
-
- for aKind in kind:
- assert aKind in self.ITEM_KINDS
-
- if legend is None: # This is a clear
- # Clear each given kind
- for aKind in kind:
- for legend in self._getItems(
- kind=aKind, just_legend=True, withhidden=True):
- self.remove(legend=legend, kind=aKind)
-
- else: # This is removing a single element
- # Remove each given kind
- for aKind in kind:
- item = self._getItem(aKind, legend)
- if item is not None:
- if aKind in ('curve', 'image'):
- if self._getActiveItem(aKind) == item:
- # Reset active item
- self._setActiveItem(aKind, None)
-
- self._remove(item)
-
- if (aKind == 'curve' and
- not self.getAllCurves(just_legend=True,
- withhidden=True)):
- self._colorIndex = 0
- self._styleIndex = 0
-
- self.notify('contentChanged', action='remove',
- kind=aKind, legend=legend)
-
- def removeCurve(self, legend):
- """Remove the curve associated to legend from the graph.
-
- :param str legend: The legend associated to the curve to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='curve')
-
- def removeImage(self, legend):
- """Remove the image associated to legend from the graph.
-
- :param str legend: The legend associated to the image to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='image')
-
- def removeItem(self, legend):
- """Remove the item associated to legend from the graph.
-
- :param str legend: The legend associated to the item to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='item')
-
- def removeMarker(self, legend):
- """Remove the marker associated to legend from the graph.
-
- :param str legend: The legend associated to the marker to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='marker')
-
- # Clear
-
- def clear(self):
- """Remove everything from the plot."""
- self.remove()
-
- def clearCurves(self):
- """Remove all the curves from the plot."""
- self.remove(kind='curve')
-
- def clearImages(self):
- """Remove all the images from the plot."""
- self.remove(kind='image')
-
- def clearItems(self):
- """Remove all the items from the plot. """
- self.remove(kind='item')
-
- def clearMarkers(self):
- """Remove all the markers from the plot."""
- self.remove(kind='marker')
-
- # Interaction
-
- def getGraphCursor(self):
- """Returns the state of the crosshair cursor.
-
- See :meth:`setGraphCursor`.
-
- :return: None if the crosshair cursor is not active,
- else a tuple (color, linewidth, linestyle).
- """
- return self._cursorConfiguration
-
- def setGraphCursor(self, flag=False, color='black',
- linewidth=1, linestyle='-'):
- """Toggle the display of a crosshair cursor and set its attributes.
-
- :param bool flag: Toggle the display of a crosshair cursor.
- The crosshair cursor is hidden by default.
- :param color: The color to use for the crosshair.
- :type color: A string (either a predefined color name in Colors.py
- or "#RRGGBB")) or a 4 columns unsigned byte array
- (Default: black).
- :param int linewidth: The width of the lines of the crosshair
- (Default: 1).
- :param str linestyle: Type of line::
-
- - ' ' no line
- - '-' solid line (the default)
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
- """
- if flag:
- self._cursorConfiguration = color, linewidth, linestyle
- else:
- self._cursorConfiguration = None
-
- self._backend.setGraphCursor(flag=flag, color=color,
- linewidth=linewidth, linestyle=linestyle)
- self._setDirtyPlot()
- self.notify('setGraphCursor',
- state=self._cursorConfiguration is not None)
-
- def pan(self, direction, factor=0.1):
- """Pan the graph in the given direction by the given factor.
-
- Warning: Pan of right Y axis not implemented!
-
- :param str direction: One of 'up', 'down', 'left', 'right'.
- :param float factor: Proportion of the range used to pan the graph.
- Must be strictly positive.
- """
- assert direction in ('up', 'down', 'left', 'right')
- assert factor > 0.
-
- if direction in ('left', 'right'):
- xFactor = factor if direction == 'right' else - factor
- xMin, xMax = self.getGraphXLimits()
-
- xMin, xMax = _utils.applyPan(xMin, xMax, xFactor,
- self.isXAxisLogarithmic())
- self.setGraphXLimits(xMin, xMax)
-
- else: # direction in ('up', 'down')
- sign = -1. if self.isYAxisInverted() else 1.
- yFactor = sign * (factor if direction == 'up' else -factor)
- yMin, yMax = self.getGraphYLimits()
- yIsLog = self.isYAxisLogarithmic()
-
- yMin, yMax = _utils.applyPan(yMin, yMax, yFactor, yIsLog)
- self.setGraphYLimits(yMin, yMax, axis='left')
-
- y2Min, y2Max = self.getGraphYLimits(axis='right')
-
- y2Min, y2Max = _utils.applyPan(y2Min, y2Max, yFactor, yIsLog)
- self.setGraphYLimits(y2Min, y2Max, axis='right')
-
- # Active Curve/Image
-
- def isActiveCurveHandling(self):
- """Returns True if active curve selection is enabled."""
- return self._activeCurveHandling
-
- def setActiveCurveHandling(self, flag=True):
- """Enable/Disable active curve selection.
-
- :param bool flag: True (the default) to enable active curve selection.
- """
- if not flag:
- self.setActiveCurve(None) # Reset active curve
-
- self._activeCurveHandling = bool(flag)
-
- def getActiveCurveColor(self):
- """Get the color used to display the currently active curve.
-
- See :meth:`setActiveCurveColor`.
- """
- return self._activeCurveColor
-
- def setActiveCurveColor(self, color="#000000"):
- """Set the color to use to display the currently active curve.
-
- :param str color: Color of the active curve,
- e.g., 'blue', 'b', '#FF0000' (Default: 'black')
- """
- if color is None:
- color = "black"
- if color in self.colorDict:
- color = self.colorDict[color]
- self._activeCurveColor = color
-
- def getActiveCurve(self, just_legend=False):
- """Return the currently active curve.
-
- It returns None in case of not having an active curve.
-
- :param bool just_legend: True to get the legend of the curve,
- False (the default) to get the curve data
- and info.
- :return: Active curve's legend or corresponding
- :class:`.items.Curve`
- :rtype: str or :class:`.items.Curve` or None
- """
- if not self.isActiveCurveHandling():
- return None
-
- return self._getActiveItem(kind='curve', just_legend=just_legend)
-
- def setActiveCurve(self, legend, replot=None):
- """Make the curve associated to legend the active curve.
-
- :param legend: The legend associated to the curve
- or None to have no active curve.
- :type legend: str or None
- """
- if replot is not None:
- _logger.warning('setActiveCurve deprecated replot parameter')
-
- if not self.isActiveCurveHandling():
- return
-
- return self._setActiveItem(kind='curve', legend=legend)
-
- def getActiveImage(self, just_legend=False):
- """Returns the currently active image.
-
- It returns None in case of not having an active image.
-
- :param bool just_legend: True to get the legend of the image,
- False (the default) to get the image data
- and info.
- :return: Active image's legend or corresponding image object
- :rtype: str, :class:`.items.ImageData`, :class:`.items.ImageRgba`
- or None
- """
- return self._getActiveItem(kind='image', just_legend=just_legend)
-
- def setActiveImage(self, legend, replot=None):
- """Make the image associated to legend the active image.
-
- :param str legend: The legend associated to the image
- or None to have no active image.
- """
- if replot is not None:
- _logger.warning('setActiveImage deprecated replot parameter')
-
- return self._setActiveItem(kind='image', legend=legend)
-
- def _getActiveItem(self, kind, just_legend=False):
- """Return the currently active item of that kind if any
-
- :param str kind: Type of item: 'curve', 'scatter' or 'image'
- :param bool just_legend: True to get the legend,
- False (default) to get the item
- :return: legend or item or None if no active item
- """
- assert kind in ('curve', 'scatter', 'image')
-
- if self._activeLegend[kind] is None:
- return None
-
- if (self._activeLegend[kind], kind) not in self._content:
- self._activeLegend[kind] = None
- return None
-
- if just_legend:
- return self._activeLegend[kind]
- else:
- return self._getItem(kind, self._activeLegend[kind])
-
- def _setActiveItem(self, kind, legend):
- """Make the curve associated to legend the active curve.
-
- :param str kind: Type of item: 'curve' or 'image'
- :param legend: The legend associated to the curve
- or None to have no active curve.
- :type legend: str or None
- """
- assert kind in ('curve', 'image', 'scatter')
-
- xLabel = self._defaultLabels['x']
- yLabel = self._defaultLabels['y']
- yRightLabel = self._defaultLabels['yright']
-
- oldActiveItem = self._getActiveItem(kind=kind)
-
- # Curve specific: Reset highlight of previous active curve
- if kind == 'curve' and oldActiveItem is not None:
- oldActiveItem.setHighlighted(False)
-
- if legend is None:
- self._activeLegend[kind] = None
- else:
- legend = str(legend)
- item = self._getItem(kind, legend)
- if item is None:
- _logger.warning("This %s does not exist: %s", kind, legend)
- self._activeLegend[kind] = None
- else:
- self._activeLegend[kind] = legend
-
- # Curve specific: handle highlight
- if kind == 'curve':
- item.setHighlightedColor(self.getActiveCurveColor())
- item.setHighlighted(True)
-
- if isinstance(item, items.LabelsMixIn):
- if item.getXLabel() is not None:
- xLabel = item.getXLabel()
- if item.getYLabel() is not None:
- if (isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right'):
- yRightLabel = item.getYLabel()
- else:
- yLabel = item.getYLabel()
-
- # Store current labels and update plot
- self._currentLabels['x'] = xLabel
- self._currentLabels['y'] = yLabel
- self._currentLabels['yright'] = yRightLabel
-
- self._backend.setGraphXLabel(xLabel)
- self._backend.setGraphYLabel(yLabel, axis='left')
- self._backend.setGraphYLabel(yRightLabel, axis='right')
-
- self._setDirtyPlot()
-
- activeLegend = self._activeLegend[kind]
- if oldActiveItem is not None or activeLegend is not None:
- if oldActiveItem is None:
- oldActiveLegend = None
- else:
- oldActiveLegend = oldActiveItem.getLegend()
- self.notify(
- 'active' + kind[0].upper() + kind[1:] + 'Changed',
- updated=oldActiveLegend != activeLegend,
- previous=oldActiveLegend,
- legend=activeLegend)
-
- return activeLegend
-
- # Getters
-
- def getAllCurves(self, just_legend=False, withhidden=False):
- """Returns all curves legend or info and data.
-
- It returns an empty list in case of not having any curve.
-
- If just_legend is False, it returns a list of :class:`items.Curve`
- objects describing the curves.
- If just_legend is True, it returns a list of curves' legend.
-
- :param bool just_legend: True to get the legend of the curves,
- False (the default) to get the curves' data
- and info.
- :param bool withhidden: False (default) to skip hidden curves.
- :return: list of curves' legend or :class:`.items.Curve`
- :rtype: list of str or list of :class:`.items.Curve`
- """
- return self._getItems(kind='curve',
- just_legend=just_legend,
- withhidden=withhidden)
-
- def getCurve(self, legend=None):
- """Get the object describing a specific curve.
-
- It returns None in case no matching curve is found.
-
- :param str legend:
- The legend identifying the curve.
- If not provided or None (the default), the active curve is returned
- or if there is no active curve, the latest updated curve that is
- not hidden is returned if there are curves in the plot.
- :return: None or :class:`.items.Curve` object
- """
- return self._getItem(kind='curve', legend=legend)
-
- def getAllImages(self, just_legend=False):
- """Returns all images legend or objects.
-
- It returns an empty list in case of not having any image.
-
- If just_legend is False, it returns a list of :class:`items.ImageBase`
- objects describing the images.
- If just_legend is True, it returns a list of legends.
-
- :param bool just_legend: True to get the legend of the images,
- False (the default) to get the images'
- object.
- :return: list of images' legend or :class:`.items.ImageBase`
- :rtype: list of str or list of :class:`.items.ImageBase`
- """
- return self._getItems(kind='image',
- just_legend=just_legend,
- withhidden=True)
-
- def getImage(self, legend=None):
- """Get the object describing a specific image.
-
- It returns None in case no matching image is found.
-
- :param str legend:
- The legend identifying the image.
- If not provided or None (the default), the active image is returned
- or if there is no active image, the latest updated image
- is returned if there are images in the plot.
- :return: None or :class:`.items.ImageBase` object
- """
- return self._getItem(kind='image', legend=legend)
-
- def getScatter(self, legend=None):
- """Get the object describing a specific scatter.
-
- It returns None in case no matching scatter is found.
-
- :param str legend:
- The legend identifying the scatter.
- If not provided or None (the default), the active scatter is
- returned or if there is no active scatter, the latest updated
- scatter is returned if there are scatters in the plot.
- :return: None or :class:`.items.Scatter` object
- """
- return self._getItem(kind='scatter', legend=legend)
-
- def getHistogram(self, legend=None):
- """Get the object describing a specific histogram.
-
- It returns None in case no matching histogram is found.
-
- :param str legend:
- The legend identifying the histogram.
- If not provided or None (the default), the latest updated scatter
- is returned if there are histograms in the plot.
- :return: None or :class:`.items.Histogram` object
- """
- return self._getItem(kind='histogram', legend=legend)
-
- def _getItems(self, kind, just_legend=False, withhidden=False):
- """Retrieve all items of a kind in the plot
-
- :param str kind: Type of item: 'curve' or 'image'
- :param bool just_legend: True to get the legend of the curves,
- False (the default) to get the curves' data
- and info.
- :param bool withhidden: False (default) to skip hidden curves.
- :return: list of legends or item objects
- """
- assert kind in self.ITEM_KINDS
- output = []
- for (legend, type_), item in self._content.items():
- if type_ == kind and (withhidden or item.isVisible()):
- output.append(legend if just_legend else item)
- return output
-
- def _getItem(self, kind, legend=None):
- """Get an item from the plot: either an image or a curve.
-
- Returns None if no match found
-
- :param str kind: Type of item: 'curve' or 'image'
- :param str legend: Legend of the item or
- None to get active or last item
- :return: Object describing the item or None
- """
- assert kind in self.ITEM_KINDS
-
- if legend is not None:
- return self._content.get((legend, kind), None)
- else:
- if kind in ('curve', 'image', 'scatter'):
- item = self._getActiveItem(kind=kind)
- if item is not None: # Return active item if available
- return item
- # Return last visible item if any
- allItems = self._getItems(
- kind=kind, just_legend=False, withhidden=False)
- return allItems[-1] if allItems else None
-
- # Limits
-
- def _notifyLimitsChanged(self):
- """Send an event when plot area limits are changed."""
- xRange = self.getGraphXLimits()
- yRange = self.getGraphYLimits(axis='left')
- y2Range = self.getGraphYLimits(axis='right')
- event = PlotEvents.prepareLimitsChangedSignal(
- id(self.getWidgetHandle()), xRange, yRange, y2Range)
- self.notify(**event)
-
- def _checkLimits(self, min_, max_, axis):
- """Makes sure axis range is not empty
-
- :param float min_: Min axis value
- :param float max_: Max axis value
- :param str axis: 'x', 'y' or 'y2' the axis to deal with
- :return: (min, max) making sure min < max
- :rtype: 2-tuple of float
- """
- if max_ < min_:
- _logger.info('%s axis: max < min, inverting limits.', axis)
- min_, max_ = max_, min_
- elif max_ == min_:
- _logger.info('%s axis: max == min, expanding limits.', axis)
- if min_ == 0.:
- min_, max_ = -0.1, 0.1
- elif min_ < 0:
- min_, max_ = min_ * 1.1, min_ * 0.9
- else: # xmin > 0
- min_, max_ = min_ * 0.9, min_ * 1.1
-
- return min_, max_
-
- def getGraphXLimits(self):
- """Get the graph X (bottom) limits.
-
- :return: Minimum and maximum values of the X axis
- """
- return self._backend.getGraphXLimits()
-
- def setGraphXLimits(self, xmin, xmax, replot=None):
- """Set the graph X (bottom) limits.
-
- :param float xmin: minimum bottom axis value
- :param float xmax: maximum bottom axis value
- """
- if replot is not None:
- _logger.warning('setGraphXLimits deprecated replot parameter')
-
- xmin, xmax = self._checkLimits(xmin, xmax, axis='x')
-
- self._backend.setGraphXLimits(xmin, xmax)
- self._setDirtyPlot()
-
- self._notifyLimitsChanged()
-
- def getGraphYLimits(self, axis='left'):
- """Get the graph Y limits.
-
- :param str axis: The axis for which to get the limits:
- Either 'left' or 'right'
- :return: Minimum and maximum values of the X axis
- """
- assert axis in ('left', 'right')
- return self._backend.getGraphYLimits(axis)
-
- def setGraphYLimits(self, ymin, ymax, axis='left', replot=None):
- """Set the graph Y limits.
-
- :param float ymin: minimum bottom axis value
- :param float ymax: maximum bottom axis value
- :param str axis: The axis for which to get the limits:
- Either 'left' or 'right'
- """
- if replot is not None:
- _logger.warning('setGraphYLimits deprecated replot parameter')
-
- assert axis in ('left', 'right')
-
- ymin, ymax = self._checkLimits(ymin, ymax,
- axis='y' if axis == 'left' else 'y2')
-
- self._backend.setGraphYLimits(ymin, ymax, axis)
- self._setDirtyPlot()
-
- self._notifyLimitsChanged()
-
- def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
- """Set the limits of the X and Y axes at once.
-
- If y2min or y2max is None, the right Y axis limits are not updated.
-
- :param float xmin: minimum bottom axis value
- :param float xmax: maximum bottom axis value
- :param float ymin: minimum left axis value
- :param float ymax: maximum left axis value
- :param float y2min: minimum right axis value or None (the default)
- :param float y2max: maximum right axis value or None (the default)
- """
- # Deal with incorrect values
- xmin, xmax = self._checkLimits(xmin, xmax, axis='x')
- ymin, ymax = self._checkLimits(ymin, ymax, axis='y')
-
- if y2min is None or y2max is None:
- # if one limit is None, both are ignored
- y2min, y2max = None, None
- else:
- y2min, y2max = self._checkLimits(y2min, y2max, axis='y2')
-
- self._backend.setLimits(xmin, xmax, ymin, ymax, y2min, y2max)
- self._setDirtyPlot()
- self._notifyLimitsChanged()
-
- # Title and labels
-
- def getGraphTitle(self):
- """Return the plot main title as a str."""
- return self._graphTitle
-
- def setGraphTitle(self, title=""):
- """Set the plot main title.
-
- :param str title: Main title of the plot (default: '')
- """
- self._graphTitle = str(title)
- self._backend.setGraphTitle(title)
- self._setDirtyPlot()
-
- def getGraphXLabel(self):
- """Return the current X axis label as a str."""
- return self._currentLabels['x']
-
- def setGraphXLabel(self, label="X"):
- """Set the plot X axis label.
-
- The provided label can be temporarily replaced by the X label of the
- active curve if any.
-
- :param str label: The X axis label (default: 'X')
- """
- self._defaultLabels['x'] = label
- self._currentLabels['x'] = label
- self._backend.setGraphXLabel(label)
- self._setDirtyPlot()
-
- def getGraphYLabel(self, axis='left'):
- """Return the current Y axis label as a str.
-
- :param str axis: The Y axis for which to get the label (left or right)
- """
- assert axis in ('left', 'right')
-
- return self._currentLabels['y' if axis == 'left' else 'yright']
-
- def setGraphYLabel(self, label="Y", axis='left'):
- """Set the plot Y axis label.
-
- The provided label can be temporarily replaced by the Y label of the
- active curve if any.
-
- :param str label: The Y axis label (default: 'Y')
- :param str axis: The Y axis for which to set the label (left or right)
- """
- assert axis in ('left', 'right')
-
- if axis == 'left':
- self._defaultLabels['y'] = label
- self._currentLabels['y'] = label
- else:
- self._defaultLabels['yright'] = label
- self._currentLabels['yright'] = label
-
- self._backend.setGraphYLabel(label, axis=axis)
- self._setDirtyPlot()
-
- # Axes
-
- def setYAxisInverted(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
- """
- flag = bool(flag)
- self._backend.setYAxisInverted(flag)
- self._setDirtyPlot()
- self.notify('setYAxisInverted', state=flag)
-
- def isYAxisInverted(self):
- """Return True if Y axis goes from top to bottom, False otherwise."""
- return self._backend.isYAxisInverted()
-
- def isXAxisLogarithmic(self):
- """Return True if X axis scale is logarithmic, False if linear."""
- return self._logX
-
- def setXAxisLogarithmic(self, flag):
- """Set the bottom X axis scale (either linear or logarithmic).
-
- :param bool flag: True to use a logarithmic scale, False for linear.
- """
- if bool(flag) == self._logX:
- return
- self._logX = bool(flag)
-
- self._backend.setXAxisLogarithmic(self._logX)
-
- # TODO hackish way of forcing update of curves and images
- for curve in self.getAllCurves():
- curve._updated()
- for image in self.getAllImages():
- image._updated()
- self._invalidateDataRange()
-
- self.resetZoom()
- self.notify('setXAxisLogarithmic', state=self._logX)
-
- def isYAxisLogarithmic(self):
- """Return True if Y axis scale is logarithmic, False if linear."""
- return self._logY
-
- def setYAxisLogarithmic(self, flag):
- """Set the Y axes scale (either linear or logarithmic).
-
- :param bool flag: True to use a logarithmic scale, False for linear.
- """
- if bool(flag) == self._logY:
- return
- self._logY = bool(flag)
-
- self._backend.setYAxisLogarithmic(self._logY)
-
- # TODO hackish way of forcing update of curves and images
- for curve in self.getAllCurves():
- curve._updated()
- for image in self.getAllImages():
- image._updated()
- self._invalidateDataRange()
-
- self.resetZoom()
- self.notify('setYAxisLogarithmic', state=self._logY)
-
- def isXAxisAutoScale(self):
- """Return True if X axis is automatically adjusting its limits."""
- return self._xAutoScale
-
- def setXAxisAutoScale(self, flag=True):
- """Set the X axis limits adjusting behavior of :meth:`resetZoom`.
-
- :param bool flag: True to resize limits automatically,
- False to disable it.
- """
- self._xAutoScale = bool(flag)
- self.notify('setXAxisAutoScale', state=self._xAutoScale)
-
- def isYAxisAutoScale(self):
- """Return True if Y axes are automatically adjusting its limits."""
- return self._yAutoScale
-
- def setYAxisAutoScale(self, flag=True):
- """Set the Y axis limits adjusting behavior of :meth:`resetZoom`.
-
- :param bool flag: True to resize limits automatically,
- False to disable it.
- """
- self._yAutoScale = bool(flag)
- self.notify('setYAxisAutoScale', state=self._yAutoScale)
-
- def isKeepDataAspectRatio(self):
- """Returns whether the plot is keeping data aspect ratio or not."""
- return self._backend.isKeepDataAspectRatio()
-
- def setKeepDataAspectRatio(self, flag=True):
- """Set whether the plot keeps data aspect ratio or not.
-
- :param bool flag: True to respect data aspect ratio
- """
- flag = bool(flag)
- self._backend.setKeepDataAspectRatio(flag=flag)
- self._setDirtyPlot()
- self.resetZoom()
- self.notify('setKeepDataAspectRatio', state=flag)
-
- def getGraphGrid(self):
- """Return the current grid mode, either None, 'major' or 'both'.
-
- See :meth:`setGraphGrid`.
- """
- return self._grid
-
- def setGraphGrid(self, which=True):
- """Set the type of grid to display.
-
- :param which: None or False to disable the grid,
- 'major' or True for grid on major ticks (the default),
- 'both' for grid on both major and minor ticks.
- :type which: str of bool
- """
- assert which in (None, True, False, 'both', 'major')
- if not which:
- which = None
- elif which is True:
- which = 'major'
- self._grid = which
- self._backend.setGraphGrid(which)
- self._setDirtyPlot()
- self.notify('setGraphGrid', which=str(which))
-
- # Defaults
-
- def isDefaultPlotPoints(self):
- """Return True if default Curve symbol is 'o', False for no symbol."""
- return self._defaultPlotPoints == 'o'
-
- def setDefaultPlotPoints(self, flag):
- """Set the default symbol of all curves.
-
- When called, this reset the symbol of all existing curves.
-
- :param bool flag: True to use 'o' as the default curve symbol,
- False to use no symbol.
- """
- self._defaultPlotPoints = 'o' if flag else ''
-
- # Reset symbol of all curves
- curves = self.getAllCurves(just_legend=False, withhidden=True)
-
- if curves:
- for curve in curves:
- curve.setSymbol(self._defaultPlotPoints)
-
- def isDefaultPlotLines(self):
- """Return True for line as default line style, False for no line."""
- return self._plotLines
-
- def setDefaultPlotLines(self, flag):
- """Toggle the use of lines as the default curve line style.
-
- :param bool flag: True to use a line as the default line style,
- False to use no line as the default line style.
- """
- self._plotLines = bool(flag)
-
- linestyle = '-' if self._plotLines else ' '
-
- # Reset linestyle of all curves
- curves = self.getAllCurves(withhidden=True)
-
- if curves:
- for curve in curves:
- curve.setLineStyle(linestyle)
-
- def getDefaultColormap(self):
- """Return the default colormap used by :meth:`addImage` as a dict.
-
- See :mod:`Plot` for the documentation of the colormap dict.
- """
- return self._defaultColormap.copy()
-
- def setDefaultColormap(self, colormap=None):
- """Set the default colormap used by :meth:`addImage`.
-
- Setting the default colormap do not change any currently displayed
- image.
- It only affects future calls to :meth:`addImage` without the colormap
- parameter.
-
- :param dict colormap: The description of the default colormap, or
- None to set the colormap to a linear autoscale
- gray colormap.
- See :mod:`Plot` for the documentation
- of the colormap dict.
- """
- if colormap is None:
- colormap = {'name': 'gray', 'normalization': 'linear',
- 'autoscale': True, 'vmin': 0.0, 'vmax': 1.0}
- self._defaultColormap = colormap.copy()
-
- def getSupportedColormaps(self):
- """Get the supported colormap names as a tuple of str.
-
- The list should at least contain and start by:
- ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
- """
- default = ('gray', 'reversed gray',
- 'temperature',
- 'red', 'green', 'blue')
- if matplotlib_cm is None:
- return default
- else:
- maps = [m for m in matplotlib_cm.datad]
- maps.sort()
- return default + tuple(maps)
-
- def _getColorAndStyle(self):
- color = self.colorList[self._colorIndex]
- style = self._styleList[self._styleIndex]
-
- # Loop over color and then styles
- self._colorIndex += 1
- if self._colorIndex >= len(self.colorList):
- self._colorIndex = 0
- self._styleIndex = (self._styleIndex + 1) % len(self._styleList)
-
- # If color is the one of active curve, take the next one
- if color == self.getActiveCurveColor():
- color, style = self._getColorAndStyle()
-
- if not self._plotLines:
- style = ' '
-
- return color, style
-
- # Misc.
-
- def getWidgetHandle(self):
- """Return the widget the plot is displayed in.
-
- This widget is owned by the backend.
- """
- return self._backend.getWidgetHandle()
-
- def notify(self, event, **kwargs):
- """Send an event to the listeners.
-
- Event are passed to the registered callback as a dict with an 'event'
- key for backward compatibility with PyMca.
-
- :param str event: The type of event
- :param kwargs: The information of the event.
- """
- eventDict = kwargs.copy()
- eventDict['event'] = event
- self._callback(eventDict)
-
- def setCallback(self, callbackFunction=None):
- """Attach a listener to the backend.
-
- Limitation: Only one listener at a time.
-
- :param callbackFunction: function accepting a dictionary as input
- to handle the graph events
- If None (default), use a default listener.
- """
- # TODO allow multiple listeners, keep a weakref on it
- # allow register listener by event type
- if callbackFunction is None:
- callbackFunction = self.graphCallback
- self._callback = callbackFunction
-
- def graphCallback(self, ddict=None):
- """This callback is going to receive all the events from the plot.
-
- Those events will consist on a dictionary and among the dictionary
- keys the key 'event' is mandatory to describe the type of event.
- This default implementation only handles setting the active curve.
- """
-
- if ddict is None:
- ddict = {}
- _logger.debug("Received dict keys = %s", str(ddict.keys()))
- _logger.debug(str(ddict))
- if ddict['event'] in ["legendClicked", "curveClicked"]:
- if ddict['button'] == "left":
- self.setActiveCurve(ddict['label'])
-
- def saveGraph(self, filename, fileFormat=None, dpi=None, **kw):
- """Save a snapshot of the plot.
-
- Supported file formats: "png", "svg", "pdf", "ps", "eps",
- "tif", "tiff", "jpeg", "jpg".
-
- :param filename: Destination
- :type filename: str, StringIO or BytesIO
- :param str fileFormat: String specifying the format
- :return: False if cannot save the plot, True otherwise
- """
- if kw:
- _logger.warning('Extra parameters ignored: %s', str(kw))
-
- if fileFormat is None:
- if not hasattr(filename, 'lower'):
- _logger.warning(
- 'saveGraph cancelled, cannot define file format.')
- return False
- else:
- fileFormat = (filename.split(".")[-1]).lower()
-
- supportedFormats = ("png", "svg", "pdf", "ps", "eps",
- "tif", "tiff", "jpeg", "jpg")
-
- if fileFormat not in supportedFormats:
- _logger.warning('Unsupported format %s', fileFormat)
- return False
- else:
- self._backend.saveGraph(filename,
- fileFormat=fileFormat,
- dpi=dpi)
- return True
-
- def getDataMargins(self):
- """Get the default data margin ratios, see :meth:`setDataMargins`.
-
- :return: The margin ratios for each side (xMin, xMax, yMin, yMax).
- :rtype: A 4-tuple of floats.
- """
- return self._defaultDataMargins
-
- def setDataMargins(self, xMinMargin=0., xMaxMargin=0.,
- yMinMargin=0., yMaxMargin=0.):
- """Set the default data margins to use in :meth:`resetZoom`.
-
- Set the default ratios of margins (as floats) to add around the data
- inside the plot area for each side.
- """
- self._defaultDataMargins = (xMinMargin, xMaxMargin,
- yMinMargin, yMaxMargin)
-
- def getAutoReplot(self):
- """Return True if replot is automatically handled, False otherwise.
-
- See :meth`setAutoReplot`.
- """
- return self._autoreplot
-
- def setAutoReplot(self, autoreplot=True):
- """Set automatic replot mode.
-
- When enabled, the plot is redrawn automatically when changed.
- When disabled, the plot is not redrawn when its content change.
- Instead, it :meth:`replot` must be called.
-
- :param bool autoreplot: True to enable it (default),
- False to disable it.
- """
- self._autoreplot = bool(autoreplot)
-
- # If the plot is dirty before enabling autoreplot,
- # then _backend.postRedisplay will never be called from _setDirtyPlot
- if self._autoreplot and self._getDirtyPlot():
- self._backend.postRedisplay()
-
- def replot(self):
- """Redraw the plot immediately."""
- for item in self._contentToUpdate:
- item._update(self._backend)
- self._contentToUpdate.clear()
- self._backend.replot()
- self._dirty = False # reset dirty flag
-
- def resetZoom(self, dataMargins=None):
- """Reset the plot limits to the bounds of the data and redraw the plot.
-
- It automatically scale limits of axes that are in autoscale mode
- (See :meth:`setXAxisAutoScale`, :meth:`setYAxisAutoScale`).
- It keeps current limits on axes that are not in autoscale mode.
-
- Extra margins can be added around the data inside the plot area.
- Margins are given as one ratio of the data range per limit of the
- data (xMin, xMax, yMin and yMax limits).
- For log scale, extra margins are applied in log10 of the data.
-
- :param dataMargins: Ratios of margins to add around the data inside
- the plot area for each side (Default: no margins).
- :type dataMargins: A 4-tuple of float as (xMin, xMax, yMin, yMax).
- """
- if dataMargins is None:
- dataMargins = self._defaultDataMargins
-
- xLimits = self.getGraphXLimits()
- yLimits = self.getGraphYLimits(axis='left')
- y2Limits = self.getGraphYLimits(axis='right')
-
- xAuto = self.isXAxisAutoScale()
- yAuto = self.isYAxisAutoScale()
-
- if not xAuto and not yAuto:
- _logger.debug("Nothing to autoscale")
- else: # Some axes to autoscale
-
- # Get data range
- ranges = self.getDataRange()
- xmin, xmax = (1., 100.) if ranges.x is None else ranges.x
- ymin, ymax = (1., 100.) if ranges.y is None else ranges.y
- if ranges.yright is None:
- ymin2, ymax2 = None, None
- else:
- ymin2, ymax2 = ranges.yright
-
- # Add margins around data inside the plot area
- newLimits = list(_utils.addMarginsToLimits(
- dataMargins,
- self.isXAxisLogarithmic(),
- self.isYAxisLogarithmic(),
- xmin, xmax, ymin, ymax, ymin2, ymax2))
-
- if self.isKeepDataAspectRatio():
- # Use limits with margins to keep ratio
- xmin, xmax, ymin, ymax = newLimits[:4]
-
- # Compute bbox wth figure aspect ratio
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
- plotRatio = plotHeight / plotWidth
-
- if plotRatio > 0.:
- dataRatio = (ymax - ymin) / (xmax - xmin)
- if dataRatio < plotRatio:
- # Increase y range
- ycenter = 0.5 * (ymax + ymin)
- yrange = (xmax - xmin) * plotRatio
- newLimits[2] = ycenter - 0.5 * yrange
- newLimits[3] = ycenter + 0.5 * yrange
-
- elif dataRatio > plotRatio:
- # Increase x range
- xcenter = 0.5 * (xmax + xmin)
- xrange_ = (ymax - ymin) / plotRatio
- newLimits[0] = xcenter - 0.5 * xrange_
- newLimits[1] = xcenter + 0.5 * xrange_
-
- self.setLimits(*newLimits)
-
- if not xAuto and yAuto:
- self.setGraphXLimits(*xLimits)
- elif xAuto and not yAuto:
- if y2Limits is not None:
- self.setGraphYLimits(
- y2Limits[0], y2Limits[1], axis='right')
- if yLimits is not None:
- self.setGraphYLimits(yLimits[0], yLimits[1], axis='left')
-
- self._setDirtyPlot()
-
- if (xLimits != self.getGraphXLimits() or
- yLimits != self.getGraphYLimits(axis='left') or
- y2Limits != self.getGraphYLimits(axis='right')):
- self._notifyLimitsChanged()
-
- # Coord conversion
-
- def dataToPixel(self, x=None, y=None, axis="left", check=True):
- """Convert a position in data coordinates to a position in pixels.
-
- :param float x: The X coordinate in data space. If None (default)
- the middle position of the displayed data is used.
- :param float y: The Y coordinate in data space. If None (default)
- the middle position of the displayed data is used.
- :param str axis: The Y axis to use for the conversion
- ('left' or 'right').
- :param bool check: True to return None if outside displayed area,
- False to convert to pixels anyway
- :returns: The corresponding position in pixels or
- None if the data position is not in the displayed area and
- check is True.
- :rtype: A tuple of 2 floats: (xPixel, yPixel) or None.
- """
- assert axis in ("left", "right")
-
- xmin, xmax = self.getGraphXLimits()
- ymin, ymax = self.getGraphYLimits(axis=axis)
-
- if x is None:
- x = 0.5 * (xmax + xmin)
- if y is None:
- y = 0.5 * (ymax + ymin)
-
- if check:
- if x > xmax or x < xmin:
- return None
-
- if y > ymax or y < ymin:
- return None
-
- return self._backend.dataToPixel(x, y, axis=axis)
-
- def pixelToData(self, x, y, axis="left", check=False):
- """Convert a position in pixels to a position in data coordinates.
-
- :param float x: The X coordinate in pixels. If None (default)
- the center of the widget is used.
- :param float y: The Y coordinate in pixels. If None (default)
- the center of the widget is used.
- :param str axis: The Y axis to use for the conversion
- ('left' or 'right').
- :param bool check: Toggle checking if pixel is in plot area.
- If False, this method never returns None.
- :returns: The corresponding position in data space or
- None if the pixel position is not in the plot area.
- :rtype: A tuple of 2 floats: (xData, yData) or None.
- """
- assert axis in ("left", "right")
- return self._backend.pixelToData(x, y, axis=axis, check=check)
-
- def getPlotBoundsInPixels(self):
- """Plot area bounds in widget coordinates in pixels.
-
- :return: bounds as a 4-tuple of int: (left, top, width, height)
- """
- return self._backend.getPlotBoundsInPixels()
-
- # Interaction support
-
- def setGraphCursorShape(self, cursor=None):
- """Set the cursor shape.
-
- :param str cursor: Name of the cursor shape
- """
- self._backend.setGraphCursorShape(cursor)
-
- def _pickMarker(self, x, y, test=None):
- """Pick a marker at the given position.
-
- To use for interaction implementation.
-
- :param float x: X position in pixels.
- :param float y: Y position in pixels.
- :param test: A callable to call for each picked marker to filter
- picked markers. If None (default), do not filter markers.
- """
- if test is None:
- def test(mark):
- return True
-
- markers = self._backend.pickItems(x, y)
- legends = [m['legend'] for m in markers if m['kind'] == 'marker']
-
- for legend in reversed(legends):
- marker = self._getMarker(legend)
- if marker is not None and test(marker):
- return marker
- return None
-
- def _getAllMarkers(self, just_legend=False):
- """Returns all markers' legend or objects
-
- :param bool just_legend: True to get the legend of the markers,
- False (the default) to get marker objects.
- :return: list of legend of list of marker objects
- :rtype: list of str or list of marker objects
- """
- return self._getItems(
- kind='marker', just_legend=just_legend, withhidden=True)
-
- def _getMarker(self, legend=None):
- """Get the object describing a specific marker.
-
- It returns None in case no matching marker is found
-
- :param str legend: The legend of the marker to retrieve
- :rtype: None of marker object
- """
- return self._getItem(kind='marker', legend=legend)
-
- def _pickImageOrCurve(self, x, y, test=None):
- """Pick an image or a curve at the given position.
-
- To use for interaction implementation.
-
- :param float x: X position in pixelsparam float y: Y position in pixels
- :param test: A callable to call for each picked item to filter
- picked items. If None (default), do not filter items.
- """
- if test is None:
- def test(i):
- return True
-
- allItems = self._backend.pickItems(x, y)
- allItems = [item for item in allItems
- if item['kind'] in ['curve', 'image']]
-
- for item in reversed(allItems):
- kind, legend = item['kind'], item['legend']
- if kind == 'curve':
- curve = self.getCurve(legend)
- if curve is not None and test(curve):
- return kind, curve, item['xdata'], item['ydata']
-
- elif kind == 'image':
- image = self.getImage(legend)
- if image is not None and test(image):
- return kind, image, None
-
- else:
- _logger.warning('Unsupported kind: %s', kind)
-
- return None
-
- # User event handling #
-
- def _isPositionInPlotArea(self, x, y):
- """Project position in pixel to the closest point in the plot area
-
- :param float x: X coordinate in widget coordinate (in pixel)
- :param float y: Y coordinate in widget coordinate (in pixel)
- :return: (x, y) in widget coord (in pixel) in the plot area
- """
- left, top, width, height = self.getPlotBoundsInPixels()
- xPlot = numpy.clip(x, left, left + width)
- yPlot = numpy.clip(y, top, top + height)
- return xPlot, yPlot
-
- def onMousePress(self, xPixel, yPixel, btn):
- """Handle mouse press event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- :param str btn: Mouse button in 'left', 'middle', 'right'
- """
- if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
- self._pressedButtons.append(btn)
- self._eventHandler.handleEvent('press', xPixel, yPixel, btn)
-
- def onMouseMove(self, xPixel, yPixel):
- """Handle mouse move event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- """
- inXPixel, inYPixel = self._isPositionInPlotArea(xPixel, yPixel)
- isCursorInPlot = inXPixel == xPixel and inYPixel == yPixel
-
- if self._cursorInPlot != isCursorInPlot:
- self._cursorInPlot = isCursorInPlot
- self._eventHandler.handleEvent(
- 'enter' if self._cursorInPlot else 'leave')
-
- if isCursorInPlot:
- # Signal mouse move event
- dataPos = self.pixelToData(inXPixel, inYPixel)
- assert dataPos is not None
-
- btn = self._pressedButtons[-1] if self._pressedButtons else None
- event = PlotEvents.prepareMouseSignal(
- 'mouseMoved', btn, dataPos[0], dataPos[1], xPixel, yPixel)
- self.notify(**event)
-
- # Either button was pressed in the plot or cursor is in the plot
- if isCursorInPlot or self._pressedButtons:
- self._eventHandler.handleEvent('move', inXPixel, inYPixel)
-
- def onMouseRelease(self, xPixel, yPixel, btn):
- """Handle mouse release event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- :param str btn: Mouse button in 'left', 'middle', 'right'
- """
- try:
- self._pressedButtons.remove(btn)
- except ValueError:
- pass
- else:
- xPixel, yPixel = self._isPositionInPlotArea(xPixel, yPixel)
- self._eventHandler.handleEvent('release', xPixel, yPixel, btn)
-
- def onMouseWheel(self, xPixel, yPixel, angleInDegrees):
- """Handle mouse wheel event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- :param float angleInDegrees: Angle corresponding to wheel motion.
- Positive for movement away from the user,
- negative for movement toward the user.
- """
- if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
- self._eventHandler.handleEvent(
- 'wheel', xPixel, yPixel, angleInDegrees)
-
- def onMouseLeaveWidget(self):
- """Handle mouse leave widget event."""
- if self._cursorInPlot:
- self._cursorInPlot = False
- self._eventHandler.handleEvent('leave')
-
- # Interaction modes #
-
- def getInteractiveMode(self):
- """Returns the current interactive mode as a dict.
-
- The returned dict contains at least the key 'mode'.
- Mode can be: 'draw', 'pan', 'select', 'zoom'.
- It can also contains extra keys (e.g., 'color') specific to a mode
- as provided to :meth:`setInteractiveMode`.
- """
- return self._eventHandler.getInteractiveMode()
-
- def setInteractiveMode(self, mode, color='black',
- shape='polygon', label=None,
- zoomOnWheel=True, source=None, width=None):
- """Switch the interactive mode.
-
- :param str mode: The name of the interactive mode.
- In 'draw', 'pan', 'select', 'zoom'.
- :param color: Only for 'draw' and 'zoom' modes.
- Color to use for drawing selection area. Default black.
- :type color: Color description: The name as a str or
- a tuple of 4 floats.
- :param str shape: Only for 'draw' mode. The kind of shape to draw.
- In 'polygon', 'rectangle', 'line', 'vline', 'hline',
- 'freeline'.
- Default is 'polygon'.
- :param str label: Only for 'draw' mode, sent in drawing events.
- :param bool zoomOnWheel: Toggle zoom on wheel support
- :param source: A user-defined object (typically the caller object)
- that will be send in the interactiveModeChanged event,
- to identify which object required a mode change.
- Default: None
- :param float width: Width of the pencil. Only for draw pencil mode.
- """
- self._eventHandler.setInteractiveMode(mode, color, shape, label, width)
- self._eventHandler.zoomOnWheel = zoomOnWheel
-
- self.notify(
- 'interactiveModeChanged', source=source)
-
- # Deprecated #
-
- def isDrawModeEnabled(self):
- """Deprecated, use :meth:`getInteractiveMode` instead.
-
- Return True if the current interactive state is drawing."""
- _logger.warning(
- 'isDrawModeEnabled deprecated, use getInteractiveMode instead')
- return self.getInteractiveMode()['mode'] == 'draw'
-
- def setDrawModeEnabled(self, flag=True, shape='polygon', label=None,
- color=None, **kwargs):
- """Deprecated, use :meth:`setInteractiveMode` instead.
-
- Set the drawing mode if flag is True and its parameters.
-
- If flag is False, only item selection is enabled.
-
- Warning: Zoom and drawing are not compatible and cannot be enabled
- simultaneously.
-
- :param bool flag: True to enable drawing and disable zoom and select.
- :param str shape: Type of item to be drawn in:
- hline, vline, rectangle, polygon (default)
- :param str label: Associated text for identifying draw signals
- :param color: The color to use to draw the selection area
- :type color: string ("#RRGGBB") or 4 column unsigned byte array or
- one of the predefined color names defined in Colors.py
- """
- _logger.warning(
- 'setDrawModeEnabled deprecated, use setInteractiveMode instead')
-
- if kwargs:
- _logger.warning('setDrawModeEnabled ignores additional parameters')
-
- if color is None:
- color = 'black'
-
- if flag:
- self.setInteractiveMode('draw', shape=shape,
- label=label, color=color)
- elif self.getInteractiveMode()['mode'] == 'draw':
- self.setInteractiveMode('select')
-
- def getDrawMode(self):
- """Deprecated, use :meth:`getInteractiveMode` instead.
-
- Return the draw mode parameters as a dict of None.
-
- It returns None if the interactive mode is not a drawing mode,
- otherwise, it returns a dict containing the drawing mode parameters
- as provided to :meth:`setDrawModeEnabled`.
- """
- _logger.warning(
- 'getDrawMode deprecated, use getInteractiveMode instead')
- mode = self.getInteractiveMode()
- return mode if mode['mode'] == 'draw' else None
-
- def isZoomModeEnabled(self):
- """Deprecated, use :meth:`getInteractiveMode` instead.
-
- Return True if the current interactive state is zooming."""
- _logger.warning(
- 'isZoomModeEnabled deprecated, use getInteractiveMode instead')
- return self.getInteractiveMode()['mode'] == 'zoom'
-
- def setZoomModeEnabled(self, flag=True, color=None):
- """Deprecated, use :meth:`setInteractiveMode` instead.
-
- Set the zoom mode if flag is True, else item selection is enabled.
-
- Warning: Zoom and drawing are not compatible and cannot be enabled
- simultaneously
-
- :param bool flag: If True, enable zoom and select mode.
- :param color: The color to use to draw the selection area.
- (Default: 'black')
- :param color: The color to use to draw the selection area
- :type color: string ("#RRGGBB") or 4 column unsigned byte array or
- one of the predefined color names defined in Colors.py
- """
- _logger.warning(
- 'setZoomModeEnabled deprecated, use setInteractiveMode instead')
- if color is None:
- color = 'black'
-
- if flag:
- self.setInteractiveMode('zoom', color=color)
- elif self.getInteractiveMode()['mode'] == 'zoom':
- self.setInteractiveMode('select')
-
- def insertMarker(self, *args, **kwargs):
- """Deprecated, use :meth:`addMarker` instead."""
- _logger.warning(
- 'insertMarker deprecated, use addMarker instead.')
- return self.addMarker(*args, **kwargs)
-
- def insertXMarker(self, *args, **kwargs):
- """Deprecated, use :meth:`addXMarker` instead."""
- _logger.warning(
- 'insertXMarker deprecated, use addXMarker instead.')
- return self.addXMarker(*args, **kwargs)
-
- def insertYMarker(self, *args, **kwargs):
- """Deprecated, use :meth:`addYMarker` instead."""
- _logger.warning(
- 'insertYMarker deprecated, use addYMarker instead.')
- return self.addYMarker(*args, **kwargs)
-
- def isActiveCurveHandlingEnabled(self):
- """Deprecated, use :meth:`isActiveCurveHandling` instead."""
- _logger.warning(
- 'isActiveCurveHandlingEnabled deprecated, '
- 'use isActiveCurveHandling instead.')
- return self.isActiveCurveHandling()
-
- def enableActiveCurveHandling(self, *args, **kwargs):
- """Deprecated, use :meth:`setActiveCurveHandling` instead."""
- _logger.warning(
- 'enableActiveCurveHandling deprecated, '
- 'use setActiveCurveHandling instead.')
- return self.setActiveCurveHandling(*args, **kwargs)
-
- def invertYAxis(self, *args, **kwargs):
- """Deprecated, use :meth:`setYAxisInverted` instead."""
- _logger.warning('invertYAxis deprecated, '
- 'use setYAxisInverted instead.')
- return self.setYAxisInverted(*args, **kwargs)
-
- def showGrid(self, flag=True):
- """Deprecated, use :meth:`setGraphGrid` instead."""
- _logger.warning("showGrid deprecated, use setGraphGrid instead")
- if flag in (0, False):
- flag = None
- elif flag in (1, True):
- flag = 'major'
- else:
- flag = 'both'
- return self.setGraphGrid(flag)
-
- def keepDataAspectRatio(self, *args, **kwargs):
- """Deprecated, use :meth:`setKeepDataAspectRatio`."""
- _logger.warning('keepDataAspectRatio deprecated,'
- 'use setKeepDataAspectRatio instead')
- return self.setKeepDataAspectRatio(*args, **kwargs)
diff --git a/silx/gui/plot/PlotActions.py b/silx/gui/plot/PlotActions.py
index aad27d2..dd16221 100644
--- a/silx/gui/plot/PlotActions.py
+++ b/silx/gui/plot/PlotActions.py
@@ -22,1365 +22,46 @@
# THE SOFTWARE.
#
# ###########################################################################*/
-"""This module provides a set of QAction to use with :class:`.PlotWidget`.
+"""Depracted module linking old PlotAction with the actions.xxx"""
-The following QAction are available:
-- :class:`ColormapAction`
-- :class:`CopyAction`
-- :class:`CrosshairAction`
-- :class:`CurveStyleAction`
-- :class:`FitAction`
-- :class:`GridAction`
-- :class:`KeepAspectRatioAction`
-- :class:`PanWithArrowKeysAction`
-- :class:`PrintAction`
-- :class:`ResetZoomAction`
-- :class:`SaveAction`
-- :class:`XAxisLogarithmicAction`
-- :class:`XAxisAutoScaleAction`
-- :class:`YAxisInvertedAction`
-- :class:`YAxisLogarithmicAction`
-- :class:`YAxisAutoScaleAction`
-- :class:`ZoomInAction`
-- :class:`ZoomOutAction`
-"""
-
-from __future__ import division
-
-
-__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
+__author__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "20/04/2017"
-
-
-from collections import OrderedDict
-import logging
-import sys
-import traceback
-import weakref
-
-if sys.version_info[0] == 3:
- from io import BytesIO
-else:
- import cStringIO as _StringIO
- BytesIO = _StringIO.StringIO
-
-import numpy
-
-from .. import icons
-from .. import qt
-from .._utils import convertArrayToQImage
-from . import Colors, items
-from .ColormapDialog import ColormapDialog
-from ._utils import applyZoomToPlot as _applyZoomToPlot
-from silx.third_party.EdfFile import EdfFile
-from silx.third_party.TiffIO import TiffIO
-from silx.math.histogram import Histogramnd
-from silx.math.medianfilter import medfilt2d
-from silx.gui.widgets.MedianFilterDialog import MedianFilterDialog
-
-from silx.io.utils import save1D, savespec
-
-
-_logger = logging.getLogger(__name__)
-
-
-class PlotAction(qt.QAction):
- """Base class for QAction that operates on a PlotWidget.
-
- :param plot: :class:`.PlotWidget` instance on which to operate.
- :param icon: QIcon or str name of icon to use
- :param str text: The name of this action to be used for menu label
- :param str tooltip: The text of the tooltip
- :param triggered: The callback to connect to the action's triggered
- signal or None for no callback.
- :param bool checkable: True for checkable action, False otherwise (default)
- :param parent: See :class:`QAction`.
- """
-
- def __init__(self, plot, icon, text, tooltip=None,
- triggered=None, checkable=False, parent=None):
- assert plot is not None
- self._plotRef = weakref.ref(plot)
-
- if not isinstance(icon, qt.QIcon):
- # Try with icon as a string and load corresponding icon
- icon = icons.getQIcon(icon)
-
- super(PlotAction, self).__init__(icon, text, parent)
-
- if tooltip is not None:
- self.setToolTip(tooltip)
-
- self.setCheckable(checkable)
-
- if triggered is not None:
- self.triggered[bool].connect(triggered)
-
- @property
- def plot(self):
- """The :class:`.PlotWidget` this action group is controlling."""
- return self._plotRef()
-
-
-class ResetZoomAction(PlotAction):
- """QAction controlling reset zoom on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(ResetZoomAction, self).__init__(
- plot, icon='zoom-original', text='Reset Zoom',
- tooltip='Auto-scale the graph',
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
- self._autoscaleChanged(True)
- plot.sigSetXAxisAutoScale.connect(self._autoscaleChanged)
- plot.sigSetYAxisAutoScale.connect(self._autoscaleChanged)
-
- def _autoscaleChanged(self, enabled):
- self.setEnabled(
- self.plot.isXAxisAutoScale() or self.plot.isYAxisAutoScale())
-
- if self.plot.isXAxisAutoScale() and self.plot.isYAxisAutoScale():
- tooltip = 'Auto-scale the graph'
- elif self.plot.isXAxisAutoScale(): # And not Y axis
- tooltip = 'Auto-scale the x-axis of the graph only'
- elif self.plot.isYAxisAutoScale(): # And not X axis
- tooltip = 'Auto-scale the y-axis of the graph only'
- else: # no axis in autoscale
- tooltip = 'Auto-scale the graph'
- self.setToolTip(tooltip)
-
- def _actionTriggered(self, checked=False):
- self.plot.resetZoom()
-
-
-class ZoomInAction(PlotAction):
- """QAction performing a zoom-in on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(ZoomInAction, self).__init__(
- plot, icon='zoom-in', text='Zoom In',
- tooltip='Zoom in the plot',
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
- self.setShortcut(qt.QKeySequence.ZoomIn)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
-
- def _actionTriggered(self, checked=False):
- _applyZoomToPlot(self.plot, 1.1)
-
-
-class ZoomOutAction(PlotAction):
- """QAction performing a zoom-out on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(ZoomOutAction, self).__init__(
- plot, icon='zoom-out', text='Zoom Out',
- tooltip='Zoom out the plot',
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
- self.setShortcut(qt.QKeySequence.ZoomOut)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
-
- def _actionTriggered(self, checked=False):
- _applyZoomToPlot(self.plot, 1. / 1.1)
-
-
-class XAxisAutoScaleAction(PlotAction):
- """QAction controlling X axis autoscale on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(XAxisAutoScaleAction, self).__init__(
- plot, icon='plot-xauto', text='X Autoscale',
- tooltip='Enable x-axis auto-scale when checked.\n'
- 'If unchecked, x-axis does not change when reseting zoom.',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.isXAxisAutoScale())
- plot.sigSetXAxisAutoScale.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setXAxisAutoScale(checked)
- if checked:
- self.plot.resetZoom()
-
-
-class YAxisAutoScaleAction(PlotAction):
- """QAction controlling Y axis autoscale on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(YAxisAutoScaleAction, self).__init__(
- plot, icon='plot-yauto', text='Y Autoscale',
- tooltip='Enable y-axis auto-scale when checked.\n'
- 'If unchecked, y-axis does not change when reseting zoom.',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.isXAxisAutoScale())
- plot.sigSetYAxisAutoScale.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setYAxisAutoScale(checked)
- if checked:
- self.plot.resetZoom()
-
-
-class XAxisLogarithmicAction(PlotAction):
- """QAction controlling X axis log scale on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(XAxisLogarithmicAction, self).__init__(
- plot, icon='plot-xlog', text='X Log. scale',
- tooltip='Logarithmic x-axis when checked',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.isXAxisLogarithmic())
- plot.sigSetXAxisLogarithmic.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setXAxisLogarithmic(checked)
-
-
-class YAxisLogarithmicAction(PlotAction):
- """QAction controlling Y axis log scale on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(YAxisLogarithmicAction, self).__init__(
- plot, icon='plot-ylog', text='Y Log. scale',
- tooltip='Logarithmic y-axis when checked',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.isYAxisLogarithmic())
- plot.sigSetYAxisLogarithmic.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setYAxisLogarithmic(checked)
-
-
-class GridAction(PlotAction):
- """QAction controlling grid mode on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param str gridMode: The grid mode to use in 'both', 'major'.
- See :meth:`.PlotWidget.setGraphGrid`
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, gridMode='both', parent=None):
- assert gridMode in ('both', 'major')
- self._gridMode = gridMode
-
- super(GridAction, self).__init__(
- plot, icon='plot-grid', text='Grid',
- tooltip='Toggle grid (on/off)',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.getGraphGrid() is not None)
- plot.sigSetGraphGrid.connect(self._gridChanged)
-
- def _gridChanged(self, which):
- """Slot listening for PlotWidget grid mode change."""
- self.setChecked(which != 'None')
-
- def _actionTriggered(self, checked=False):
- self.plot.setGraphGrid(self._gridMode if checked else None)
-
-
-class CurveStyleAction(PlotAction):
- """QAction controlling curve style on a :class:`.PlotWidget`.
-
- It changes the default line and markers style which updates all
- curves on the plot.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(CurveStyleAction, self).__init__(
- plot, icon='plot-toggle-points', text='Curve style',
- tooltip='Change curve line and markers style',
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
-
- def _actionTriggered(self, checked=False):
- currentState = (self.plot.isDefaultPlotLines(),
- self.plot.isDefaultPlotPoints())
-
- # line only, line and symbol, symbol only
- states = (True, False), (True, True), (False, True)
- newState = states[(states.index(currentState) + 1) % 3]
-
- self.plot.setDefaultPlotLines(newState[0])
- self.plot.setDefaultPlotPoints(newState[1])
-
-
-class ColormapAction(PlotAction):
- """QAction opening a ColormapDialog to update the colormap.
-
- Both the active image colormap and the default colormap are updated.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
- def __init__(self, plot, parent=None):
- self._dialog = None # To store an instance of ColormapDialog
- super(ColormapAction, self).__init__(
- plot, icon='colormap', text='Colormap',
- tooltip="Change colormap",
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
-
- def _actionTriggered(self, checked=False):
- """Create a cmap dialog and update active image and default cmap."""
- # Create the dialog if not already existing
- if self._dialog is None:
- self._dialog = ColormapDialog()
-
- image = self.plot.getActiveImage()
- if not isinstance(image, items.ColormapMixIn):
- # No active image or active image is RGBA,
- # set dialog from default info
- colormap = self.plot.getDefaultColormap()
-
- self._dialog.setHistogram() # Reset histogram and range if any
-
- else:
- # Set dialog from active image
- colormap = image.getColormap()
-
- data = image.getData(copy=False)
-
- goodData = data[numpy.isfinite(data)]
- if goodData.size > 0:
- dataMin = goodData.min()
- dataMax = goodData.max()
- else:
- qt.QMessageBox.warning(
- self, "No Data",
- "Image data does not contain any real value")
- dataMin, dataMax = 1., 10.
-
- self._dialog.setHistogram() # Reset histogram if any
- self._dialog.setDataRange(dataMin, dataMax)
- # The histogram should be done in a worker thread
- # hist, bin_edges = numpy.histogram(goodData, bins=256)
- # self._dialog.setHistogram(hist, bin_edges)
-
- self._dialog.setColormap(**colormap)
-
- # Run the dialog listening to colormap change
- self._dialog.sigColormapChanged.connect(self._colormapChanged)
- result = self._dialog.exec_()
- self._dialog.sigColormapChanged.disconnect(self._colormapChanged)
-
- if not result: # Restore the previous colormap
- self._colormapChanged(colormap)
-
- def _colormapChanged(self, colormap):
- # Update default colormap
- self.plot.setDefaultColormap(colormap)
-
- # Update active image colormap
- activeImage = self.plot.getActiveImage()
- if isinstance(activeImage, items.ColormapMixIn):
- activeImage.setColormap(colormap)
-
-
-class KeepAspectRatioAction(PlotAction):
- """QAction controlling aspect ratio on a :class:`.PlotWidget`.
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- # Uses two images for checked/unchecked states
- self._states = {
- False: (icons.getQIcon('shape-circle-solid'),
- "Keep data aspect ratio"),
- True: (icons.getQIcon('shape-ellipse-solid'),
- "Do no keep data aspect ratio")
- }
-
- icon, tooltip = self._states[plot.isKeepDataAspectRatio()]
- super(KeepAspectRatioAction, self).__init__(
- plot,
- icon=icon,
- text='Toggle keep aspect ratio',
- tooltip=tooltip,
- triggered=self._actionTriggered,
- checkable=False,
- parent=parent)
- plot.sigSetKeepDataAspectRatio.connect(
- self._keepDataAspectRatioChanged)
-
- def _keepDataAspectRatioChanged(self, aspectRatio):
- """Handle Plot set keep aspect ratio signal"""
- icon, tooltip = self._states[aspectRatio]
- self.setIcon(icon)
- self.setToolTip(tooltip)
-
- def _actionTriggered(self, checked=False):
- # This will trigger _keepDataAspectRatioChanged
- self.plot.setKeepDataAspectRatio(not self.plot.isKeepDataAspectRatio())
-
-
-class YAxisInvertedAction(PlotAction):
- """QAction controlling Y orientation on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- # Uses two images for checked/unchecked states
- self._states = {
- False: (icons.getQIcon('plot-ydown'),
- "Orient Y axis downward"),
- True: (icons.getQIcon('plot-yup'),
- "Orient Y axis upward"),
- }
-
- icon, tooltip = self._states[plot.isYAxisInverted()]
- super(YAxisInvertedAction, self).__init__(
- plot,
- icon=icon,
- text='Invert Y Axis',
- tooltip=tooltip,
- triggered=self._actionTriggered,
- checkable=False,
- parent=parent)
- plot.sigSetYAxisInverted.connect(self._yAxisInvertedChanged)
-
- def _yAxisInvertedChanged(self, inverted):
- """Handle Plot set y axis inverted signal"""
- icon, tooltip = self._states[inverted]
- self.setIcon(icon)
- self.setToolTip(tooltip)
-
- def _actionTriggered(self, checked=False):
- # This will trigger _yAxisInvertedChanged
- self.plot.setYAxisInverted(not self.plot.isYAxisInverted())
-
-
-class SaveAction(PlotAction):
- """QAction for saving Plot content.
-
- It opens a Save as... dialog.
-
- :param plot: :class:`.PlotWidget` instance on which to operate.
- :param parent: See :class:`QAction`.
- """
- # TODO find a way to make the filter list selectable and extensible
-
- SNAPSHOT_FILTER_SVG = 'Plot Snapshot as SVG (*.svg)'
-
- SNAPSHOT_FILTERS = ('Plot Snapshot as PNG (*.png)',
- 'Plot Snapshot as JPEG (*.jpg)',
- SNAPSHOT_FILTER_SVG)
-
- # Dict of curve filters with CSV-like format
- # Using ordered dict to guarantee filters order
- # Note: '%.18e' is numpy.savetxt default format
- CURVE_FILTERS_TXT = OrderedDict((
- ('Curve as Raw ASCII (*.txt)',
- {'fmt': '%.18e', 'delimiter': ' ', 'header': False}),
- ('Curve as ";"-separated CSV (*.csv)',
- {'fmt': '%.18e', 'delimiter': ';', 'header': True}),
- ('Curve as ","-separated CSV (*.csv)',
- {'fmt': '%.18e', 'delimiter': ',', 'header': True}),
- ('Curve as tab-separated CSV (*.csv)',
- {'fmt': '%.18e', 'delimiter': '\t', 'header': True}),
- ('Curve as OMNIC CSV (*.csv)',
- {'fmt': '%.7E', 'delimiter': ',', 'header': False}),
- ('Curve as SpecFile (*.dat)',
- {'fmt': '%.7g', 'delimiter': '', 'header': False})
- ))
-
- CURVE_FILTER_NPY = 'Curve as NumPy binary file (*.npy)'
-
- CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [CURVE_FILTER_NPY]
-
- ALL_CURVES_FILTERS = ("All curves as SpecFile (*.dat)", )
-
- IMAGE_FILTER_EDF = 'Image data as EDF (*.edf)'
- IMAGE_FILTER_TIFF = 'Image data as TIFF (*.tif)'
- IMAGE_FILTER_NUMPY = 'Image data as NumPy binary file (*.npy)'
- IMAGE_FILTER_ASCII = 'Image data as ASCII (*.dat)'
- IMAGE_FILTER_CSV_COMMA = 'Image data as ,-separated CSV (*.csv)'
- IMAGE_FILTER_CSV_SEMICOLON = 'Image data as ;-separated CSV (*.csv)'
- IMAGE_FILTER_CSV_TAB = 'Image data as tab-separated CSV (*.csv)'
- IMAGE_FILTER_RGB_PNG = 'Image as PNG (*.png)'
- IMAGE_FILTER_RGB_TIFF = 'Image as TIFF (*.tif)'
- IMAGE_FILTERS = (IMAGE_FILTER_EDF,
- IMAGE_FILTER_TIFF,
- IMAGE_FILTER_NUMPY,
- IMAGE_FILTER_ASCII,
- IMAGE_FILTER_CSV_COMMA,
- IMAGE_FILTER_CSV_SEMICOLON,
- IMAGE_FILTER_CSV_TAB,
- IMAGE_FILTER_RGB_PNG,
- IMAGE_FILTER_RGB_TIFF)
-
- def __init__(self, plot, parent=None):
- super(SaveAction, self).__init__(
- plot, icon='document-save', text='Save as...',
- tooltip='Save curve/image/plot snapshot dialog',
- triggered=self._actionTriggered,
- checkable=False, parent=parent)
- self.setShortcut(qt.QKeySequence.Save)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
-
- def _errorMessage(self, informativeText=''):
- """Display an error message."""
- # TODO issue with QMessageBox size fixed and too small
- msg = qt.QMessageBox(self.plot)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setInformativeText(informativeText + ' ' + str(sys.exc_info()[1]))
- msg.setDetailedText(traceback.format_exc())
- msg.exec_()
-
- def _saveSnapshot(self, filename, nameFilter):
- """Save a snapshot of the :class:`PlotWindow` widget.
-
- :param str filename: The name of the file to write
- :param str nameFilter: The selected name filter
- :return: False if format is not supported or save failed,
- True otherwise.
- """
- if nameFilter == self.SNAPSHOT_FILTER_SVG:
- self.plot.saveGraph(filename, fileFormat='svg')
-
- else:
- if hasattr(qt.QPixmap, "grabWidget"):
- # Qt 4
- pixmap = qt.QPixmap.grabWidget(self.plot.getWidgetHandle())
- else:
- # Qt 5
- pixmap = self.plot.getWidgetHandle().grab()
- if not pixmap.save(filename):
- self._errorMessage()
- return False
- return True
-
- def _saveCurve(self, filename, nameFilter):
- """Save a curve from the plot.
-
- :param str filename: The name of the file to write
- :param str nameFilter: The selected name filter
- :return: False if format is not supported or save failed,
- True otherwise.
- """
- if nameFilter not in self.CURVE_FILTERS:
- return False
-
- # Check if a curve is to be saved
- curve = self.plot.getActiveCurve()
- # before calling _saveCurve, if there is no selected curve, we
- # make sure there is only one curve on the graph
- if curve is None:
- curves = self.plot.getAllCurves()
- if not curves:
- self._errorMessage("No curve to be saved")
- return False
- curve = curves[0]
-
- if nameFilter in self.CURVE_FILTERS_TXT:
- filter_ = self.CURVE_FILTERS_TXT[nameFilter]
- fmt = filter_['fmt']
- csvdelim = filter_['delimiter']
- autoheader = filter_['header']
- else:
- # .npy
- fmt, csvdelim, autoheader = ("", "", False)
-
- # If curve has no associated label, get the default from the plot
- xlabel = curve.getXLabel()
- if xlabel is None:
- xlabel = self.plot.getGraphXLabel()
- ylabel = curve.getYLabel()
- if ylabel is None:
- ylabel = self.plot.getGraphYLabel()
-
- try:
- save1D(filename,
- curve.getXData(copy=False),
- curve.getYData(copy=False),
- xlabel, [ylabel],
- fmt=fmt, csvdelim=csvdelim,
- autoheader=autoheader)
- except IOError:
- self._errorMessage('Save failed\n')
- return False
-
- return True
-
- def _saveCurves(self, filename, nameFilter):
- """Save all curves from the plot.
-
- :param str filename: The name of the file to write
- :param str nameFilter: The selected name filter
- :return: False if format is not supported or save failed,
- True otherwise.
- """
- if nameFilter not in self.ALL_CURVES_FILTERS:
- return False
-
- curves = self.plot.getAllCurves()
- if not curves:
- self._errorMessage("No curves to be saved")
- return False
-
- curve = curves[0]
- scanno = 1
- try:
- specfile = savespec(filename,
- curve.getXData(copy=False),
- curve.getYData(copy=False),
- curve.getXLabel(),
- curve.getYLabel(),
- fmt="%.7g", scan_number=1, mode="w",
- write_file_header=True,
- close_file=False)
- except IOError:
- self._errorMessage('Save failed\n')
- return False
-
- for curve in curves[1:]:
- try:
- scanno += 1
- specfile = savespec(specfile,
- curve.getXData(copy=False),
- curve.getYData(copy=False),
- curve.getXLabel(),
- curve.getYLabel(),
- fmt="%.7g", scan_number=scanno, mode="w",
- write_file_header=False,
- close_file=False)
- except IOError:
- self._errorMessage('Save failed\n')
- return False
- specfile.close()
-
- return True
-
- def _saveImage(self, filename, nameFilter):
- """Save an image from the plot.
-
- :param str filename: The name of the file to write
- :param str nameFilter: The selected name filter
- :return: False if format is not supported or save failed,
- True otherwise.
- """
- if nameFilter not in self.IMAGE_FILTERS:
- return False
-
- image = self.plot.getActiveImage()
- if image is None:
- qt.QMessageBox.warning(
- self.plot, "No Data", "No image to be saved")
- return False
-
- data = image.getData(copy=False)
-
- # TODO Use silx.io for writing files
- if nameFilter == self.IMAGE_FILTER_EDF:
- edfFile = EdfFile(filename, access="w+")
- edfFile.WriteImage({}, data, Append=0)
- return True
-
- elif nameFilter == self.IMAGE_FILTER_TIFF:
- tiffFile = TiffIO(filename, mode='w')
- tiffFile.writeImage(data, software='silx')
- return True
-
- elif nameFilter == self.IMAGE_FILTER_NUMPY:
- try:
- numpy.save(filename, data)
- except IOError:
- self._errorMessage('Save failed\n')
- return False
- return True
-
- elif nameFilter in (self.IMAGE_FILTER_ASCII,
- self.IMAGE_FILTER_CSV_COMMA,
- self.IMAGE_FILTER_CSV_SEMICOLON,
- self.IMAGE_FILTER_CSV_TAB):
- csvdelim, filetype = {
- self.IMAGE_FILTER_ASCII: (' ', 'txt'),
- self.IMAGE_FILTER_CSV_COMMA: (',', 'csv'),
- self.IMAGE_FILTER_CSV_SEMICOLON: (';', 'csv'),
- self.IMAGE_FILTER_CSV_TAB: ('\t', 'csv'),
- }[nameFilter]
-
- height, width = data.shape
- rows, cols = numpy.mgrid[0:height, 0:width]
- try:
- save1D(filename, rows.ravel(), (cols.ravel(), data.ravel()),
- filetype=filetype,
- xlabel='row',
- ylabels=['column', 'value'],
- csvdelim=csvdelim,
- autoheader=True)
-
- except IOError:
- self._errorMessage('Save failed\n')
- return False
- return True
-
- elif nameFilter in (self.IMAGE_FILTER_RGB_PNG,
- self.IMAGE_FILTER_RGB_TIFF):
- # Get displayed image
- rgbaImage = image.getRbgaImageData(copy=False)
- # Convert RGB QImage
- qimage = convertArrayToQImage(rgbaImage[:, :, :3])
-
- if nameFilter == self.IMAGE_FILTER_RGB_PNG:
- fileFormat = 'PNG'
- else:
- fileFormat = 'TIFF'
-
- if qimage.save(filename, fileFormat):
- return True
- else:
- _logger.error('Failed to save image as %s', filename)
- qt.QMessageBox.critical(
- self.parent(),
- 'Save image as',
- 'Failed to save image')
-
- return False
-
- def _actionTriggered(self, checked=False):
- """Handle save action."""
- # Set-up filters
- filters = []
-
- # Add image filters if there is an active image
- if self.plot.getActiveImage() is not None:
- filters.extend(self.IMAGE_FILTERS)
-
- # Add curve filters if there is a curve to save
- if (self.plot.getActiveCurve() is not None or
- len(self.plot.getAllCurves()) == 1):
- filters.extend(self.CURVE_FILTERS)
- if len(self.plot.getAllCurves()) > 1:
- filters.extend(self.ALL_CURVES_FILTERS)
-
- filters.extend(self.SNAPSHOT_FILTERS)
-
- # Create and run File dialog
- dialog = qt.QFileDialog(self.plot)
- dialog.setWindowTitle("Output File Selection")
- dialog.setModal(1)
- dialog.setNameFilters(filters)
-
- dialog.setFileMode(dialog.AnyFile)
- dialog.setAcceptMode(dialog.AcceptSave)
-
- if not dialog.exec_():
- return False
-
- nameFilter = dialog.selectedNameFilter()
- filename = dialog.selectedFiles()[0]
- dialog.close()
-
- # Forces the filename extension to match the chosen filter
- extension = nameFilter.split()[-1][2:-1]
- if (len(filename) <= len(extension) or
- filename[-len(extension):].lower() != extension.lower()):
- filename += extension
-
- # Handle save
- if nameFilter in self.SNAPSHOT_FILTERS:
- return self._saveSnapshot(filename, nameFilter)
- elif nameFilter in self.CURVE_FILTERS:
- return self._saveCurve(filename, nameFilter)
- elif nameFilter in self.ALL_CURVES_FILTERS:
- return self._saveCurves(filename, nameFilter)
- elif nameFilter in self.IMAGE_FILTERS:
- return self._saveImage(filename, nameFilter)
- else:
- _logger.warning('Unsupported file filter: %s', nameFilter)
- return False
-
-
-def _plotAsPNG(plot):
- """Save a :class:`Plot` as PNG and return the payload.
-
- :param plot: The :class:`Plot` to save
- """
- pngFile = BytesIO()
- plot.saveGraph(pngFile, fileFormat='png')
- pngFile.flush()
- pngFile.seek(0)
- data = pngFile.read()
- pngFile.close()
- return data
-
-
-class PrintAction(PlotAction):
- """QAction for printing the plot.
-
- It opens a Print dialog.
-
- Current implementation print a bitmap of the plot area and not vector
- graphics, so printing quality is not great.
-
- :param plot: :class:`.PlotWidget` instance on which to operate.
- :param parent: See :class:`QAction`.
- """
-
- # Share QPrinter instance to propose latest used as default
- _printer = None
-
- def __init__(self, plot, parent=None):
- super(PrintAction, self).__init__(
- plot, icon='document-print', text='Print...',
- tooltip='Open print dialog',
- triggered=self.printPlot,
- checkable=False, parent=parent)
- self.setShortcut(qt.QKeySequence.Print)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
-
- @property
- def printer(self):
- """The QPrinter instance used by the actions.
-
- This is shared accross all instances of PrintAct
- """
- if self._printer is None:
- PrintAction._printer = qt.QPrinter()
- return self._printer
-
- def printPlotAsWidget(self):
- """Open the print dialog and print the plot.
-
- Use :meth:`QWidget.render` to print the plot
-
- :return: True if successful
- """
- dialog = qt.QPrintDialog(self.printer, self.plot)
- dialog.setWindowTitle('Print Plot')
- if not dialog.exec_():
- return False
-
- # Print a snapshot of the plot widget at the top of the page
- widget = self.plot.centralWidget()
-
- painter = qt.QPainter()
- if not painter.begin(self.printer):
- return False
-
- pageRect = self.printer.pageRect()
- xScale = pageRect.width() / widget.width()
- yScale = pageRect.height() / widget.height()
- scale = min(xScale, yScale)
-
- painter.translate(pageRect.width() / 2., 0.)
- painter.scale(scale, scale)
- painter.translate(-widget.width() / 2., 0.)
- widget.render(painter)
- painter.end()
-
- return True
-
- def printPlot(self):
- """Open the print dialog and print the plot.
-
- Use :meth:`Plot.saveGraph` to print the plot.
-
- :return: True if successful
- """
- # Init printer and start printer dialog
- dialog = qt.QPrintDialog(self.printer, self.plot)
- dialog.setWindowTitle('Print Plot')
- if not dialog.exec_():
- return False
-
- # Save Plot as PNG and make a pixmap from it with default dpi
- pngData = _plotAsPNG(self.plot)
-
- pixmap = qt.QPixmap()
- pixmap.loadFromData(pngData, 'png')
-
- xScale = self.printer.pageRect().width() / pixmap.width()
- yScale = self.printer.pageRect().height() / pixmap.height()
- scale = min(xScale, yScale)
-
- # Draw pixmap with painter
- painter = qt.QPainter()
- if not painter.begin(self.printer):
- return False
-
- painter.drawPixmap(0, 0,
- pixmap.width() * scale,
- pixmap.height() * scale,
- pixmap)
- painter.end()
-
- return True
-
-
-class CopyAction(PlotAction):
- """QAction to copy :class:`.PlotWidget` content to clipboard.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(CopyAction, self).__init__(
- plot, icon='edit-copy', text='Copy plot',
- tooltip='Copy a snapshot of the plot into the clipboard',
- triggered=self.copyPlot,
- checkable=False, parent=parent)
- self.setShortcut(qt.QKeySequence.Copy)
- self.setShortcutContext(qt.Qt.WidgetShortcut)
-
- def copyPlot(self):
- """Copy plot content to the clipboard as a bitmap."""
- # Save Plot as PNG and make a QImage from it with default dpi
- pngData = _plotAsPNG(self.plot)
- image = qt.QImage.fromData(pngData, 'png')
- qt.QApplication.clipboard().setImage(image)
-
-
-class CrosshairAction(PlotAction):
- """QAction toggling crosshair cursor on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param str color: Color to use to draw the crosshair
- :param int linewidth: Width of the crosshair cursor
- :param str linestyle: Style of line. See :meth:`.Plot.setGraphCursor`
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, color='black', linewidth=1, linestyle='-',
- parent=None):
- self.color = color
- """Color used to draw the crosshair (str)."""
-
- self.linewidth = linewidth
- """Width of the crosshair cursor (int)."""
-
- self.linestyle = linestyle
- """Style of line of the cursor (str)."""
-
- super(CrosshairAction, self).__init__(
- plot, icon='crosshair', text='Crosshair Cursor',
- tooltip='Enable crosshair cursor when checked',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.getGraphCursor() is not None)
- plot.sigSetGraphCursor.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setGraphCursor(checked,
- color=self.color,
- linestyle=self.linestyle,
- linewidth=self.linewidth)
-
-
-class PanWithArrowKeysAction(PlotAction):
- """QAction toggling pan with arrow keys on a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
-
- super(PanWithArrowKeysAction, self).__init__(
- plot, icon='arrow-keys', text='Pan with arrow keys',
- tooltip='Enable pan with arrow keys when checked',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- self.setChecked(plot.isPanWithArrowKeys())
- plot.sigSetPanWithArrowKeys.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setPanWithArrowKeys(checked)
-
-
-def _warningMessage(informativeText='', detailedText='', parent=None):
- """Display a popup warning message."""
- msg = qt.QMessageBox(parent)
- msg.setIcon(qt.QMessageBox.Warning)
- msg.setInformativeText(informativeText)
- msg.setDetailedText(detailedText)
- msg.exec_()
-
-
-def _getOneCurve(plt, mode="unique"):
- """Get a single curve from the plot.
- By default, get the active curve if any, else if a single curve is plotted
- get it, else return None and display a warning popup.
-
- This behavior can be adjusted by modifying the *mode* parameter: always
- return the active curve if any, but adjust the behavior in case no curve
- is active.
-
- :param plt: :class:`.PlotWidget` instance on which to operate
- :param mode: Parameter defining the behavior when no curve is active.
- Possible modes:
- - "none": return None (enforce curve activation)
- - "unique": return the unique curve or None if multiple curves
- - "first": return first curve
- - "last": return last curve (most recently added one)
- :return: return value of plt.getActiveCurve(), or plt.getAllCurves()[0],
- or plt.getAllCurves()[-1], or None
- """
- curve = plt.getActiveCurve()
- if curve is not None:
- return curve
-
- if mode is None or mode.lower() == "none":
- _warningMessage("You must activate a curve!",
- parent=plt)
- return None
-
- curves = plt.getAllCurves()
- if len(curves) == 0:
- _warningMessage("No curve on this plot.",
- parent=plt)
- return None
-
- if len(curves) == 1:
- return curves[0]
-
- if len(curves) > 1:
- if mode == "unique":
- _warningMessage("Multiple curves are plotted. " +
- "Please activate the one you want to use.",
- parent=plt)
- return None
- if mode.lower() == "first":
- return curves[0]
- if mode.lower() == "last":
- return curves[-1]
-
- raise ValueError("Illegal value for parameter 'mode'." +
- " Allowed values: 'none', 'unique', 'first', 'last'.")
-
-
-class FitAction(PlotAction):
- """QAction to open a :class:`FitWidget` and set its data to the
- active curve if any, or to the first curve.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
- def __init__(self, plot, parent=None):
- super(FitAction, self).__init__(
- plot, icon='math-fit', text='Fit curve',
- tooltip='Open a fit dialog',
- triggered=self._getFitWindow,
- checkable=False, parent=parent)
- self.fit_window = None
-
- def _getFitWindow(self):
- curve = _getOneCurve(self.plot)
- if curve is None:
- return
- self.xlabel = self.plot.getGraphXLabel()
- self.ylabel = self.plot.getGraphYLabel()
- self.x = curve.getXData(copy=False)
- self.y = curve.getYData(copy=False)
- self.legend = curve.getLegend()
- self.xmin, self.xmax = self.plot.getGraphXLimits()
-
- # open a window with a FitWidget
- if self.fit_window is None:
- self.fit_window = qt.QMainWindow()
- # import done here rather than at module level to avoid circular import
- # FitWidget -> BackgroundWidget -> PlotWindow -> PlotActions -> FitWidget
- from ..fit.FitWidget import FitWidget
- self.fit_widget = FitWidget(parent=self.fit_window)
- self.fit_window.setCentralWidget(
- self.fit_widget)
- self.fit_widget.guibuttons.DismissButton.clicked.connect(
- self.fit_window.close)
- self.fit_widget.sigFitWidgetSignal.connect(
- self.handle_signal)
- self.fit_window.show()
- else:
- if self.fit_window.isHidden():
- self.fit_window.show()
- self.fit_widget.show()
- self.fit_window.raise_()
-
- self.fit_widget.setData(self.x, self.y,
- xmin=self.xmin, xmax=self.xmax)
- self.fit_window.setWindowTitle(
- "Fitting " + self.legend +
- " on x range %f-%f" % (self.xmin, self.xmax))
-
- def handle_signal(self, ddict):
- x_fit = self.x[self.xmin <= self.x]
- x_fit = x_fit[x_fit <= self.xmax]
- fit_legend = "Fit <%s>" % self.legend
- fit_curve = self.plot.getCurve(fit_legend)
-
- if ddict["event"] == "FitFinished":
- y_fit = self.fit_widget.fitmanager.gendata()
- if fit_curve is None:
- self.plot.addCurve(x_fit, y_fit,
- fit_legend,
- xlabel=self.xlabel, ylabel=self.ylabel,
- resetzoom=False)
- else:
- fit_curve.setData(x_fit, y_fit)
- fit_curve.setVisible(True)
-
- if ddict["event"] in ["FitStarted", "FitFailed"]:
- if fit_curve is not None:
- fit_curve.setVisible(False)
-
-
-class PixelIntensitiesHistoAction(PlotAction):
- """QAction to plot the pixels intensities diagram
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- PlotAction.__init__(self,
- plot,
- icon='pixel-intensities',
- text='pixels intensity',
- tooltip='Compute image intensity distribution',
- triggered=self._triggered,
- parent=parent,
- checkable=True)
- self._plotHistogram = None
- self._connectedToActiveImage = False
- self._histo = None
-
- def _triggered(self, checked):
- """Update the plot of the histogram visibility status
-
- :param bool checked: status of the action button
- """
- if checked:
- if not self._connectedToActiveImage:
- self.plot.sigActiveImageChanged.connect(
- self._activeImageChanged)
- self._connectedToActiveImage = True
- self.computeIntensityDistribution()
-
- self.getHistogramPlotWidget().show()
-
- else:
- if self._connectedToActiveImage:
- self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChanged)
- self._connectedToActiveImage = False
-
- self.getHistogramPlotWidget().hide()
-
- def _activeImageChanged(self, previous, legend):
- """Handle active image change: toggle enabled toolbar, update curve"""
- if self.isChecked():
- self.computeIntensityDistribution()
-
- def computeIntensityDistribution(self):
- """Get the active image and compute the image intensity distribution
- """
- activeImage = self.plot.getActiveImage()
-
- if activeImage is not None:
- image = activeImage.getData(copy=False)
- if image.ndim == 3: # RGB(A) images
- _logger.info('Converting current image from RGB(A) to grayscale\
- in order to compute the intensity distribution')
- image = (image[:, :, 0] * 0.299 +
- image[:, :, 1] * 0.587 +
- image[:, :, 2] * 0.114)
-
- xmin = numpy.nanmin(image)
- xmax = numpy.nanmax(image)
- nbins = min(1024, int(numpy.sqrt(image.size)))
- data_range = xmin, xmax
-
- # bad hack: get 256 bins in the case we have a B&W
- if numpy.issubdtype(image.dtype, numpy.integer):
- if nbins > xmax - xmin:
- nbins = xmax - xmin
-
- nbins = max(2, nbins)
-
- data = image.ravel().astype(numpy.float32)
- histogram = Histogramnd(data, n_bins=nbins, histo_range=data_range)
- assert len(histogram.edges) == 1
- self._histo = histogram.histo
- edges = histogram.edges[0]
- plot = self.getHistogramPlotWidget()
- plot.addHistogram(histogram=self._histo,
- edges=edges,
- legend='pixel intensity',
- fill=True,
- color='red')
- plot.resetZoom()
-
- def eventFilter(self, qobject, event):
- """Observe when the close event is emitted then
- simply uncheck the action button
-
- :param qobject: the object observe
- :param event: the event received by qobject
- """
- if event.type() == qt.QEvent.Close:
- if self._plotHistogram is not None:
- self._plotHistogram.hide()
- self.setChecked(False)
-
- return PlotAction.eventFilter(self, qobject, event)
-
- def getHistogramPlotWidget(self):
- """Create the plot histogram if needed, otherwise create it
-
- :return: the PlotWidget showing the histogram of the pixel intensities
- """
- from silx.gui.plot.PlotWindow import Plot1D
- if self._plotHistogram is None:
- self._plotHistogram = Plot1D(parent=self.plot)
- self._plotHistogram.setWindowFlags(qt.Qt.Window)
- self._plotHistogram.setWindowTitle('Image Intensity Histogram')
- self._plotHistogram.installEventFilter(self)
- self._plotHistogram.setGraphXLabel("Value")
- self._plotHistogram.setGraphYLabel("Count")
-
- return self._plotHistogram
-
- def getHistogram(self):
- """Return the last computed histogram
-
- :return: the histogram displayed in the HistogramPlotWiget
- """
- return self._histo
-
-
-class MedianFilterAction(PlotAction):
- """QAction to plot the pixels intensities diagram
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- PlotAction.__init__(self,
- plot,
- icon='median-filter',
- text='median filter',
- tooltip='Apply a median filter on the image',
- triggered=self._triggered,
- parent=parent)
- self._originalImage = None
- self._legend = None
- self._filteredImage = None
- self._popup = MedianFilterDialog(parent=None)
- self._popup.sigFilterOptChanged.connect(self._updateFilter)
- self.plot.sigActiveImageChanged.connect( self._updateActiveImage)
- self._updateActiveImage()
-
- def _triggered(self, checked):
- """Update the plot of the histogram visibility status
-
- :param bool checked: status of the action button
- """
- self._popup.show()
-
- def _updateActiveImage(self):
- """Set _activeImageLegend and _originalImage from the active image"""
- self._activeImageLegend = self.plot.getActiveImage(just_legend=True)
- if self._activeImageLegend is None:
- self._originalImage = None
- self._legend = None
- else:
- self._originalImage = self.plot.getImage(self._activeImageLegend).getData(copy=False)
- self._legend = self.plot.getImage(self._activeImageLegend).getLegend()
-
- def _updateFilter(self, kernelWidth, conditional=False):
- if self._originalImage is None:
- return
-
- self.plot.sigActiveImageChanged.disconnect(self._updateActiveImage)
- filteredImage = self._computeFilteredImage(kernelWidth, conditional)
- self.plot.addImage(data=filteredImage,
- legend=self._legend,
- replace=True)
- self.plot.sigActiveImageChanged.connect(self._updateActiveImage)
-
- def _computeFilteredImage(self, kernelWidth, conditional):
- raise NotImplemented('MedianFilterAction is a an abstract class')
-
- def getFilteredImage(self):
- """
- :return: the image with the median filter apply on"""
- return self._filteredImage
-
-
-class MedianFilter1DAction(MedianFilterAction):
- """Define the MedianFilterAction for 1D
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
- def __init__(self, plot, parent=None):
- MedianFilterAction.__init__(self,
- plot,
- parent=parent)
-
- def _computeFilteredImage(self, kernelWidth, conditional):
- assert(self.plot is not None)
- return medfilt2d(self._originalImage,
- (kernelWidth, 1),
- conditional)
-
-
-class MedianFilter2DAction(MedianFilterAction):
- """Define the MedianFilterAction for 2D
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
- def __init__(self, plot, parent=None):
- MedianFilterAction.__init__(self,
- plot,
- parent=parent)
-
- def _computeFilteredImage(self, kernelWidth, conditional):
- assert(self.plot is not None)
- return medfilt2d(self._originalImage,
- (kernelWidth, kernelWidth),
- conditional)
+__date__ = "01/06/2017"
+
+from silx.utils.deprecation import deprecated_warning
+
+deprecated_warning(type_='module',
+ name=__file__,
+ reason='PlotActions refactoring',
+ replacement='plot.actions',
+ since_version='0.6')
+
+from .actions import PlotAction
+
+from .actions.io import CopyAction
+from .actions.io import PrintAction
+from .actions.io import SaveAction
+
+from .actions.control import ColormapAction
+from .actions.control import CrosshairAction
+from .actions.control import CurveStyleAction
+from .actions.control import GridAction
+from .actions.control import KeepAspectRatioAction
+from .actions.control import PanWithArrowKeysAction
+from .actions.control import ResetZoomAction
+from .actions.control import XAxisAutoScaleAction
+from .actions.control import XAxisLogarithmicAction
+from .actions.control import YAxisAutoScaleAction
+from .actions.control import YAxisLogarithmicAction
+from .actions.control import YAxisInvertedAction
+from .actions.control import ZoomInAction
+from .actions.control import ZoomOutAction
+
+from .actions.medfilt import MedianFilter1DAction
+from .actions.medfilt import MedianFilter2DAction
+from .actions.medfilt import MedianFilterAction
+
+from .actions.histogram import PixelIntensitiesHistoAction
+
+from .actions.fit import FitAction
diff --git a/silx/gui/plot/PlotInteraction.py b/silx/gui/plot/PlotInteraction.py
index fbc9c1f..865073b 100644
--- a/silx/gui/plot/PlotInteraction.py
+++ b/silx/gui/plot/PlotInteraction.py
@@ -26,7 +26,7 @@
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "24/01/2017"
+__date__ = "27/06/2017"
import math
@@ -153,11 +153,11 @@ class Pan(_ZoomOnWheel):
xData, yData, y2Data = self._pixelToData(x, y)
lastX, lastY, lastY2 = self._previousDataPos
- xMin, xMax = self.plot.getGraphXLimits()
- yMin, yMax = self.plot.getGraphYLimits(axis='left')
- y2Min, y2Max = self.plot.getGraphYLimits(axis='right')
+ xMin, xMax = self.plot.getXAxis().getLimits()
+ yMin, yMax = self.plot.getYAxis().getLimits()
+ y2Min, y2Max = self.plot.getYAxis(axis='right').getLimits()
- if self.plot.isXAxisLogarithmic():
+ if self.plot.getXAxis()._isLogarithmic():
try:
dx = math.log10(xData) - math.log10(lastX)
newXMin = pow(10., (math.log10(xMin) - dx))
@@ -176,7 +176,7 @@ class Pan(_ZoomOnWheel):
if newXMin < FLOAT32_SAFE_MIN or newXMax > FLOAT32_SAFE_MAX:
newXMin, newXMax = xMin, xMax
- if self.plot.isYAxisLogarithmic():
+ if self.plot.getYAxis()._isLogarithmic():
try:
dy = math.log10(yData) - math.log10(lastY)
newYMin = pow(10., math.log10(yMin) - dy)
@@ -233,10 +233,10 @@ class Zoom(_ZoomOnWheel):
def __init__(self, plot, color):
self.color = color
- self.zoomStack = []
self._lastClick = 0., None
super(Zoom, self).__init__(plot)
+ self.plot.getLimitsHistory().clear()
def _areaWithAspectRatio(self, x0, y0, x1, y1):
_plotLeft, _plotTop, plotW, plotH = self.plot.getPlotBoundsInPixels()
@@ -286,25 +286,14 @@ class Zoom(_ZoomOnWheel):
self._lastClick = time.time(), (dataPos[0], dataPos[1], x, y)
- # Zoom-in centered on mouse cursor
- # xMin, xMax = self.plot.getGraphXLimits()
- # yMin, yMax = self.plot.getGraphYLimits()
- # y2Min, y2Max = self.plot.getGraphYLimits(axis="right")
- # self.zoomStack.append((xMin, xMax, yMin, yMax, y2Min, y2Max))
- # self._zoom(x, y, 2)
elif btn == RIGHT_BTN:
- try:
- xMin, xMax, yMin, yMax, y2Min, y2Max = self.zoomStack.pop()
- except IndexError:
- # Signal mouse clicked event
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
- eventDict = prepareMouseSignal('mouseClicked', 'right',
- dataPos[0], dataPos[1],
- x, y)
- self.plot.notify(**eventDict)
- else:
- self.plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
+ # Signal mouse clicked event
+ dataPos = self.plot.pixelToData(x, y)
+ assert dataPos is not None
+ eventDict = prepareMouseSignal('mouseClicked', 'right',
+ dataPos[0], dataPos[1],
+ x, y)
+ self.plot.notify(**eventDict)
def beginDrag(self, x, y):
dataPos = self.plot.pixelToData(x, y)
@@ -354,10 +343,7 @@ class Zoom(_ZoomOnWheel):
if x0 != x1 or y0 != y1: # Avoid empty zoom area
# Store current zoom state in stack
- xMin, xMax = self.plot.getGraphXLimits()
- yMin, yMax = self.plot.getGraphYLimits()
- y2Min, y2Max = self.plot.getGraphYLimits(axis="right")
- self.zoomStack.append((xMin, xMax, yMin, yMax, y2Min, y2Max))
+ self.plot.getLimitsHistory().push()
if self.plot.isKeepDataAspectRatio():
x0, y0, x1, y1 = self._areaWithAspectRatio(x0, y0, x1, y1)
@@ -510,33 +496,6 @@ class SelectPolygon(Select):
self.points[-1] = dataPos
return True
-
- elif btn == RIGHT_BTN:
- self.machine.resetSelectionArea()
-
- firstPos = self.machine.plot.dataToPixel(*self._firstPos,
- check=False)
- dx, dy = abs(firstPos[0] - x), abs(firstPos[1] - y)
-
- if (dx < self.machine.DRAG_THRESHOLD_DIST and
- dy < self.machine.DRAG_THRESHOLD_DIST):
- self.points[-1] = self.points[0]
- else:
- dataPos = self.machine.plot.pixelToData(x, y)
- assert dataPos is not None
- self.points[-1] = dataPos
- if self.points[-2] == self.points[-1]:
- self.points.pop()
- self.points.append(self.points[0])
-
- eventDict = prepareDrawingSignal('drawingFinished',
- 'polygon',
- self.points,
- self.machine.parameters)
- self.machine.plot.notify(**eventDict)
- self.goto('idle')
- return False
-
return False
def onMove(self, x, y):
@@ -1100,14 +1059,19 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
elif picked[0] == 'curve':
curve = picked[1]
+ indices = picked[2]
dataPos = self.plot.pixelToData(x, y)
assert dataPos is not None
+ xData = curve.getXData(copy=False)
+ yData = curve.getYData(copy=False)
+
eventDict = prepareCurveSignal('left',
curve.getLegend(),
'curve',
- picked[2], picked[3],
+ xData[indices],
+ yData[indices],
dataPos[0], dataPos[1],
x, y)
return eventDict
@@ -1123,7 +1087,6 @@ class ItemsInteraction(ClickOrDrag, _PlotInteraction):
scale = image.getScale()
column = int((dataPos[0] - origin[0]) / float(scale[0]))
row = int((dataPos[1] - origin[1]) / float(scale[1]))
-
eventDict = prepareImageSignal('left',
image.getLegend(),
'image',
diff --git a/silx/gui/plot/PlotToolButtons.py b/silx/gui/plot/PlotToolButtons.py
index 8042391..430489d 100644
--- a/silx/gui/plot/PlotToolButtons.py
+++ b/silx/gui/plot/PlotToolButtons.py
@@ -34,7 +34,7 @@ The following QToolButton are available:
__authors__ = ["V. Valls", "H. Payno"]
__license__ = "MIT"
-__date__ = "26/01/2017"
+__date__ = "27/06/2017"
import logging
@@ -197,25 +197,26 @@ class YAxisOriginToolButton(PlotToolButton):
return qt.QAction(icon, text, self)
def _connectPlot(self, plot):
- plot.sigSetYAxisInverted.connect(self._yAxisInvertedChanged)
- self._yAxisInvertedChanged(plot.isYAxisInverted())
+ yAxis = plot.getYAxis()
+ yAxis.sigInvertedChanged.connect(self._yAxisInvertedChanged)
+ self._yAxisInvertedChanged(yAxis.isInverted())
def _disconnectPlot(self, plot):
- plot.sigSetYAxisInverted.disconnect(self._yAxisInvertedChanged)
+ plot.getYAxis().sigInvertedChanged.disconnect(self._yAxisInvertedChanged)
def setYAxisUpward(self):
"""Configure the plot to use y-axis upward"""
plot = self.plot()
if plot is not None:
# This will trigger _yAxisInvertedChanged
- plot.setYAxisInverted(False)
+ plot.getYAxis().setInverted(False)
def setYAxisDownward(self):
"""Configure the plot to use y-axis downward"""
plot = self.plot()
if plot is not None:
# This will trigger _yAxisInvertedChanged
- plot.setYAxisInverted(True)
+ plot.getYAxis().setInverted(True)
def _yAxisInvertedChanged(self, inverted):
"""Handle Plot set y axis inverted signal"""
diff --git a/silx/gui/plot/PlotTools.py b/silx/gui/plot/PlotTools.py
index 7158d0e..85dcc31 100644
--- a/silx/gui/plot/PlotTools.py
+++ b/silx/gui/plot/PlotTools.py
@@ -29,7 +29,7 @@ from __future__ import division
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "03/03/2017"
+__date__ = "02/10/2017"
import logging
@@ -40,9 +40,9 @@ import weakref
import numpy
from .. import qt
+from silx.gui.widgets.FloatEdit import FloatEdit
_logger = logging.getLogger(__name__)
-_logger.setLevel(logging.DEBUG)
# PositionInfo ################################################################
@@ -150,56 +150,70 @@ class PositionInfo(qt.QWidget):
:param dict event: Plot event
"""
if event['event'] == 'mouseMoved':
- x, y = event['x'], event['y'] # Position in data
- styleSheet = "color: rgb(0, 0, 0);" # Default style
-
- if self.autoSnapToActiveCurve and self.plot.getGraphCursor():
- # Check if near active curve with symbols.
-
- styleSheet = "color: rgb(255, 0, 0);" # Style far from curve
-
- activeCurve = self.plot.getActiveCurve()
- if activeCurve:
- xData = activeCurve.getXData(copy=False)
- yData = activeCurve.getYData(copy=False)
- if activeCurve.getSymbol(): # Only handled if symbols on curve
- closestIndex = numpy.argmin(
- pow(xData - x, 2) + pow(yData - y, 2))
-
- xClosest = xData[closestIndex]
- yClosest = yData[closestIndex]
-
- closestInPixels = self.plot.dataToPixel(
- xClosest, yClosest, axis=activeCurve.getYAxis())
- if closestInPixels is not None:
- xPixel, yPixel = event['xpixel'], event['ypixel']
-
- if (abs(closestInPixels[0] - xPixel) < 5 and
- abs(closestInPixels[1] - yPixel) < 5):
- # Update label style sheet
- styleSheet = "color: rgb(0, 0, 0);"
-
- # if close enough, wrap to data point coords
- x, y = xClosest, yClosest
-
- for label, name, func in self._fields:
- label.setStyleSheet(styleSheet)
-
- try:
- value = func(x, y)
- except:
- label.setText('Error')
- _logger.error(
- "Error while converting coordinates (%f, %f)"
- "with converter '%s'" % (x, y, name))
- _logger.error(traceback.format_exc())
- else:
- if isinstance(value, numbers.Real):
- value = '%.7g' % value # Use this for floats and int
- else:
- value = str(value) # Fallback for other types
- label.setText(value)
+ x, y = event['x'], event['y']
+ self._updateStatusBar(x, y)
+ def _updateStatusBar(self, x, y):
+ """Update information from the status bar using the definitions.
+
+ :param float x: Position-x in data
+ :param float y: Position-y in data
+ """
+ styleSheet = "color: rgb(0, 0, 0);" # Default style
+
+ if self.autoSnapToActiveCurve and self.plot.getGraphCursor():
+ # Check if near active curve with symbols.
+
+ styleSheet = "color: rgb(255, 0, 0);" # Style far from curve
+
+ activeCurve = self.plot.getActiveCurve()
+ if activeCurve:
+ xData = activeCurve.getXData(copy=False)
+ yData = activeCurve.getYData(copy=False)
+ if activeCurve.getSymbol(): # Only handled if symbols on curve
+ closestIndex = numpy.argmin(
+ pow(xData - x, 2) + pow(yData - y, 2))
+
+ xClosest = xData[closestIndex]
+ yClosest = yData[closestIndex]
+
+ closestInPixels = self.plot.dataToPixel(
+ xClosest, yClosest, axis=activeCurve.getYAxis())
+ if closestInPixels is not None:
+ xPixel, yPixel = event['xpixel'], event['ypixel']
+
+ if (abs(closestInPixels[0] - xPixel) < 5 and
+ abs(closestInPixels[1] - yPixel) < 5):
+ # Update label style sheet
+ styleSheet = "color: rgb(0, 0, 0);"
+
+ # if close enough, wrap to data point coords
+ x, y = xClosest, yClosest
+
+ for label, name, func in self._fields:
+ label.setStyleSheet(styleSheet)
+
+ try:
+ value = func(x, y)
+ text = self.valueToString(value)
+ label.setText(text)
+ except:
+ label.setText('Error')
+ _logger.error(
+ "Error while converting coordinates (%f, %f)"
+ "with converter '%s'" % (x, y, name))
+ _logger.error(traceback.format_exc())
+
+ def valueToString(self, value):
+ if isinstance(value, (tuple, list)):
+ value = [self.valueToString(v) for v in value]
+ return ", ".join(value)
+ elif isinstance(value, numbers.Real):
+ # Use this for floats and int
+ return '%.7g' % value
+ else:
+ # Fallback for other types
+ return str(value)
# LimitsToolBar ##############################################################
@@ -226,22 +240,6 @@ class LimitsToolBar(qt.QToolBar):
:param str title: See :class:`QToolBar`.
"""
- class _FloatEdit(qt.QLineEdit):
- """Field to edit a float value."""
- def __init__(self, value=None, *args, **kwargs):
- qt.QLineEdit.__init__(self, *args, **kwargs)
- self.setValidator(qt.QDoubleValidator())
- self.setFixedWidth(100)
- self.setAlignment(qt.Qt.AlignLeft)
- if value is not None:
- self.setValue(value)
-
- def value(self):
- return float(self.text())
-
- def setValue(self, value):
- self.setText('%g' % value)
-
def __init__(self, parent=None, plot=None, title='Limits'):
super(LimitsToolBar, self).__init__(title, parent)
assert plot is not None
@@ -257,28 +255,28 @@ class LimitsToolBar(qt.QToolBar):
def _initWidgets(self):
"""Create and init Toolbar widgets."""
- xMin, xMax = self.plot.getGraphXLimits()
- yMin, yMax = self.plot.getGraphYLimits()
+ xMin, xMax = self.plot.getXAxis().getLimits()
+ yMin, yMax = self.plot.getYAxis().getLimits()
self.addWidget(qt.QLabel('Limits: '))
self.addWidget(qt.QLabel(' X: '))
- self._xMinFloatEdit = self._FloatEdit(xMin)
+ self._xMinFloatEdit = FloatEdit(self, xMin)
self._xMinFloatEdit.editingFinished[()].connect(
self._xFloatEditChanged)
self.addWidget(self._xMinFloatEdit)
- self._xMaxFloatEdit = self._FloatEdit(xMax)
+ self._xMaxFloatEdit = FloatEdit(self, xMax)
self._xMaxFloatEdit.editingFinished[()].connect(
self._xFloatEditChanged)
self.addWidget(self._xMaxFloatEdit)
self.addWidget(qt.QLabel(' Y: '))
- self._yMinFloatEdit = self._FloatEdit(yMin)
+ self._yMinFloatEdit = FloatEdit(self, yMin)
self._yMinFloatEdit.editingFinished[()].connect(
self._yFloatEditChanged)
self.addWidget(self._yMinFloatEdit)
- self._yMaxFloatEdit = self._FloatEdit(yMax)
+ self._yMaxFloatEdit = FloatEdit(self, yMax)
self._yMaxFloatEdit.editingFinished[()].connect(
self._yFloatEditChanged)
self.addWidget(self._yMaxFloatEdit)
@@ -288,8 +286,8 @@ class LimitsToolBar(qt.QToolBar):
if event['event'] not in ('limitsChanged',):
return
- xMin, xMax = self.plot.getGraphXLimits()
- yMin, yMax = self.plot.getGraphYLimits()
+ xMin, xMax = self.plot.getXAxis().getLimits()
+ yMin, yMax = self.plot.getYAxis().getLimits()
self._xMinFloatEdit.setValue(xMin)
self._xMaxFloatEdit.setValue(xMax)
@@ -302,7 +300,7 @@ class LimitsToolBar(qt.QToolBar):
if xMax < xMin:
xMin, xMax = xMax, xMin
- self.plot.setGraphXLimits(xMin, xMax)
+ self.plot.getXAxis().setLimits(xMin, xMax)
def _yFloatEditChanged(self):
"""Handle Y limits changed from the GUI."""
@@ -310,4 +308,4 @@ class LimitsToolBar(qt.QToolBar):
if yMax < yMin:
yMin, yMax = yMax, yMin
- self.plot.setGraphYLimits(yMin, yMax)
+ self.plot.getYAxis().setLimits(yMin, yMax)
diff --git a/silx/gui/plot/PlotWidget.py b/silx/gui/plot/PlotWidget.py
index 5666d56..8fd5a5e 100644
--- a/silx/gui/plot/PlotWidget.py
+++ b/silx/gui/plot/PlotWidget.py
@@ -20,64 +20,253 @@
# 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.
-#
# ###########################################################################*/
-"""Qt widget providing Plot API for 1D and 2D data.
+"""Qt widget providing plot API for 1D and 2D data.
+
+Widget with plot API for 1D and 2D data.
+
+The :class:`PlotWidget` implements the plot API initially provided in PyMca.
+
+Plot Events
+-----------
+
+The :class:`PlotWidget` sends some event to the registered callback
+(See :meth:`PlotWidget.setCallback`).
+Those events are sent as a dictionary with a key 'event' describing the kind
+of event.
+
+Drawing events
+..............
+
+'drawingProgress' and 'drawingFinished' events are sent during drawing
+interaction (See :meth:`PlotWidget.setInteractiveMode`).
+
+- 'event': 'drawingProgress' or 'drawingFinished'
+- 'parameters': dict of parameters used by the drawing mode.
+ It has the following keys: 'shape', 'label', 'color'.
+ See :meth:`PlotWidget.setInteractiveMode`.
+- 'points': Points (x, y) in data coordinates of the drawn shape.
+ For 'hline' and 'vline', it is the 2 points defining the line.
+ For 'line' and 'rectangle', it is the coordinates of the start
+ drawing point and the latest drawing point.
+ For 'polygon', it is the coordinates of all points of the shape.
+- 'type': The type of drawing in 'line', 'hline', 'polygon', 'rectangle',
+ 'vline'.
+- 'xdata' and 'ydata': X coords and Y coords of shape points in data
+ coordinates (as in 'points').
+
+When the type is 'rectangle', the following additional keys are provided:
+
+- 'x' and 'y': The origin of the rectangle in data coordinates
+- 'widht' and 'height': The size of the rectangle in data coordinates
+
+
+Mouse events
+............
+
+'mouseMoved', 'mouseClicked' and 'mouseDoubleClicked' events are sent for
+mouse events.
+
+They provide the following keys:
+
+- 'event': 'mouseMoved', 'mouseClicked' or 'mouseDoubleClicked'
+- 'button': the mouse button that was pressed in 'left', 'middle', 'right'
+- 'x' and 'y': The mouse position in data coordinates
+- 'xpixel' and 'ypixel': The mouse position in pixels
+
+
+Marker events
+.............
+
+'hover', 'markerClicked', 'markerMoving' and 'markerMoved' events are
+sent during interaction with markers.
+
+'hover' is sent when the mouse cursor is over a marker.
+'markerClicker' is sent when the user click on a selectable marker.
+'markerMoving' and 'markerMoved' are sent when a draggable marker is moved.
+
+They provide the following keys:
+
+- 'event': 'hover', 'markerClicked', 'markerMoving' or 'markerMoved'
+- 'button': the mouse button that is pressed in 'left', 'middle', 'right'
+- 'draggable': True if the marker is draggable, False otherwise
+- 'label': The legend associated with the clicked image or curve
+- 'selectable': True if the marker is selectable, False otherwise
+- 'type': 'marker'
+- 'x' and 'y': The mouse position in data coordinates
+- 'xdata' and 'ydata': The marker position in data coordinates
+
+'markerClicked' and 'markerMoving' events have a 'xpixel' and a 'ypixel'
+additional keys, that provide the mouse position in pixels.
+
-This provides the plot API of :class:`silx.gui.plot.Plot.Plot` as a
-Qt widget.
+Image and curve events
+......................
+
+'curveClicked' and 'imageClicked' events are sent when a selectable curve
+or image is clicked.
+
+Both share the following keys:
+
+- 'event': 'curveClicked' or 'imageClicked'
+- 'button': the mouse button that was pressed in 'left', 'middle', 'right'
+- 'label': The legend associated with the clicked image or curve
+- 'type': The type of item in 'curve', 'image'
+- 'x' and 'y': The clicked position in data coordinates
+- 'xpixel' and 'ypixel': The clicked position in pixels
+
+'curveClicked' events have a 'xdata' and a 'ydata' additional keys, that
+provide the coordinates of the picked points of the curve.
+There can be more than one point of the curve being picked, and if a line of
+the curve is picked, only the first point of the line is included in the list.
+
+'imageClicked' have a 'col' and a 'row' additional keys, that provide
+the column and row index in the image array that was clicked.
+
+
+Limits changed events
+.....................
+
+'limitsChanged' events are sent when the limits of the plot are changed.
+This can results from user interaction or API calls.
+
+It provides the following keys:
+
+- 'event': 'limitsChanged'
+- 'source': id of the widget that emitted this event.
+- 'xdata': Range of X in graph coordinates: (xMin, xMax).
+- 'ydata': Range of Y in graph coordinates: (yMin, yMax).
+- 'y2data': Range of right axis in graph coordinates (y2Min, y2Max) or None.
+
+Plot state change events
+........................
+
+The following events are emitted when the plot is modified.
+They provide the new state:
+
+- 'setGraphCursor' event with a 'state' key (bool)
+- 'setGraphGrid' event with a 'which' key (str), see :meth:`setGraphGrid`
+- 'setKeepDataAspectRatio' event with a 'state' key (bool)
+
+A 'contentChanged' event is triggered when the content of the plot is updated.
+It provides the following keys:
+
+- 'action': The change of the plot: 'add' or 'remove'
+- 'kind': The kind of primitive changed: 'curve', 'image', 'item' or 'marker'
+- 'legend': The legend of the primitive changed.
+
+'activeCurveChanged' and 'activeImageChanged' events with the following keys:
+
+- 'legend': Name (str) of the current active item or None if no active item.
+- 'previous': Name (str) of the previous active item or None if no item was
+ active. It is the same as 'legend' if 'updated' == True
+- 'updated': (bool) True if active item name did not changed,
+ but active item data or style was updated.
+
+'interactiveModeChanged' event with a 'source' key identifying the object
+setting the interactive mode.
+
+'defaultColormapChanged' event is triggered when the default colormap of
+the plot is updated.
"""
+from __future__ import division
+
+
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "22/02/2016"
+__date__ = "30/08/2017"
+from collections import OrderedDict, namedtuple
+from contextlib import contextmanager
+import itertools
import logging
-from . import Plot
+import numpy
+
+from silx.utils.deprecation import deprecated
+# Import matplotlib backend here to init matplotlib our way
+from .backends.BackendMatplotlib import BackendMatplotlibQt
+
+from .Colormap import Colormap
+from . import Colors
+from . import PlotInteraction
+from . import PlotEvents
+from .LimitsHistory import LimitsHistory
+from . import _utils
+
+from . import items
from .. import qt
+from ._utils.panzoom import ViewConstraints
_logger = logging.getLogger(__name__)
-class PlotWidget(qt.QMainWindow, Plot.Plot):
+_COLORDICT = Colors.COLORDICT
+_COLORLIST = [_COLORDICT['black'],
+ _COLORDICT['blue'],
+ _COLORDICT['red'],
+ _COLORDICT['green'],
+ _COLORDICT['pink'],
+ _COLORDICT['yellow'],
+ _COLORDICT['brown'],
+ _COLORDICT['cyan'],
+ _COLORDICT['magenta'],
+ _COLORDICT['orange'],
+ _COLORDICT['violet'],
+ # _COLORDICT['bluegreen'],
+ _COLORDICT['grey'],
+ _COLORDICT['darkBlue'],
+ _COLORDICT['darkRed'],
+ _COLORDICT['darkGreen'],
+ _COLORDICT['darkCyan'],
+ _COLORDICT['darkMagenta'],
+ _COLORDICT['darkYellow'],
+ _COLORDICT['darkBrown']]
+
+
+"""
+Object returned when requesting the data range.
+"""
+_PlotDataRange = namedtuple('PlotDataRange',
+ ['x', 'y', 'yright'])
+
+
+class PlotWidget(qt.QMainWindow):
"""Qt Widget providing a 1D/2D plot.
This widget is a QMainWindow.
- It provides Qt signals for the Plot and add supports for panning
- with arrow keys.
+ This class implements the plot API initially provided in PyMca.
+
+ Supported backends:
- :param parent: The parent of this widget or None.
- :param backend: The backend to use for the plot (default: matplotlib).
- See :class:`.Plot` for the list of supported backend.
+ - 'matplotlib' and 'mpl': Matplotlib with Qt.
+ - 'opengl' and 'gl': OpenGL backend (requires PyOpenGL and OpenGL >= 2.1)
+ - 'none': No backend, to run headless for testing purpose.
+
+ :param parent: The parent of this widget or None (default).
+ :param backend: The backend to use, in:
+ 'matplotlib' (default), 'mpl', 'opengl', 'gl', 'none'
+ or a :class:`BackendBase.BackendBase` class
:type backend: str or :class:`BackendBase.BackendBase`
"""
+ DEFAULT_BACKEND = 'matplotlib'
+ """Class attribute setting the default backend for all instances."""
+
+ colorList = _COLORLIST
+ colorDict = _COLORDICT
+
sigPlotSignal = qt.Signal(object)
"""Signal for all events of the plot.
The signal information is provided as a dict.
- See :class:`.Plot` for documentation of the content of the dict.
+ See :class:`PlotWidget` for documentation of the content of the dict.
"""
- sigSetYAxisInverted = qt.Signal(bool)
- """Signal emitted when Y axis orientation has changed"""
-
- sigSetXAxisLogarithmic = qt.Signal(bool)
- """Signal emitted when X axis scale has changed"""
-
- sigSetYAxisLogarithmic = qt.Signal(bool)
- """Signal emitted when Y axis scale has changed"""
-
- sigSetXAxisAutoScale = qt.Signal(bool)
- """Signal emitted when X axis autoscale has changed"""
-
- sigSetYAxisAutoScale = qt.Signal(bool)
- """Signal emitted when Y axis autoscale has changed"""
-
sigSetKeepDataAspectRatio = qt.Signal(bool)
"""Signal emitted when plot keep aspect ratio has changed"""
@@ -90,10 +279,13 @@ class PlotWidget(qt.QMainWindow, Plot.Plot):
sigSetPanWithArrowKeys = qt.Signal(bool)
"""Signal emitted when pan with arrow keys has changed"""
+ _sigAxesVisibilityChanged = qt.Signal(bool)
+ """Signal emitted when the axes visibility changed"""
+
sigContentChanged = qt.Signal(str, str, str)
"""Signal emitted when the content of the plot is changed.
- It provides 3 informations:
+ It provides the following information:
- action: The change of the plot: 'add' or 'remove'
- kind: The kind of primitive changed:
@@ -104,7 +296,7 @@ class PlotWidget(qt.QMainWindow, Plot.Plot):
sigActiveCurveChanged = qt.Signal(object, object)
"""Signal emitted when the active curve has changed.
- It provides 2 informations:
+ It provides the following information:
- previous: The legend of the previous active curve or None
- legend: The legend of the new active curve or None if no curve is active
@@ -113,7 +305,7 @@ class PlotWidget(qt.QMainWindow, Plot.Plot):
sigActiveImageChanged = qt.Signal(object, object)
"""Signal emitted when the active image has changed.
- It provides 2 informations:
+ It provides the following information:
- previous: The legend of the previous active image or None
- legend: The legend of the new active image or None if no image is active
@@ -122,7 +314,7 @@ class PlotWidget(qt.QMainWindow, Plot.Plot):
sigActiveScatterChanged = qt.Signal(object, object)
"""Signal emitted when the active Scatter has changed.
- It provides following information:
+ It provides the following information:
- previous: The legend of the previous active scatter or None
- legend: The legend of the new active image or None if no image is active
@@ -136,6 +328,10 @@ class PlotWidget(qt.QMainWindow, Plot.Plot):
def __init__(self, parent=None, backend=None,
legends=False, callback=None, **kw):
+ self._autoreplot = False
+ self._dirty = False
+ self._cursorInPlot = False
+ self.__muteActiveItemChanged = False
if kw:
_logger.warning(
@@ -146,42 +342,2137 @@ class PlotWidget(qt.QMainWindow, Plot.Plot):
_logger.warning('deprecated: __init__ callback argument')
self._panWithArrowKeys = True
+ self._viewConstrains = None
- qt.QMainWindow.__init__(self, parent)
+ super(PlotWidget, self).__init__(parent)
if parent is not None:
# behave as a widget
self.setWindowFlags(qt.Qt.Widget)
else:
self.setWindowTitle('PlotWidget')
- Plot.Plot.__init__(self, parent, backend=backend)
+ if backend is None:
+ backend = self.DEFAULT_BACKEND
+
+ if hasattr(backend, "__call__"):
+ self._backend = backend(self, parent)
+
+ elif hasattr(backend, "lower"):
+ lowerCaseString = backend.lower()
+ if lowerCaseString in ("matplotlib", "mpl"):
+ backendClass = BackendMatplotlibQt
+ elif lowerCaseString in ('gl', 'opengl'):
+ from .backends.BackendOpenGL import BackendOpenGL
+ backendClass = BackendOpenGL
+ elif lowerCaseString == 'none':
+ from .backends.BackendBase import BackendBase as backendClass
+ else:
+ raise ValueError("Backend not supported %s" % backend)
+ self._backend = backendClass(self, parent)
+
+ else:
+ raise ValueError("Backend not supported %s" % str(backend))
+
+ self.setCallback() # set _callback
+
+ # Items handling
+ self._content = OrderedDict()
+ self._contentToUpdate = [] # Used as an OrderedSet
+
+ self._dataRange = None
+
+ # line types
+ self._styleList = ['-', '--', '-.', ':']
+ self._colorIndex = 0
+ self._styleIndex = 0
+
+ self._activeCurveHandling = True
+ self._activeCurveColor = "#000000"
+ self._activeLegend = {'curve': None, 'image': None,
+ 'scatter': None}
+
+ # default properties
+ self._cursorConfiguration = None
+
+ self._xAxis = items.XAxis(self)
+ self._yAxis = items.YAxis(self)
+ self._yRightAxis = items.YRightAxis(self, self._yAxis)
+
+ self._grid = None
+ self._graphTitle = ''
+
+ self.setGraphTitle()
+ self.setGraphXLabel()
+ self.setGraphYLabel()
+ self.setGraphYLabel('', axis='right')
+
+ self.setDefaultColormap() # Init default colormap
+
+ self.setDefaultPlotPoints(False)
+ self.setDefaultPlotLines(True)
+
+ self._limitsHistory = LimitsHistory(self)
+
+ self._eventHandler = PlotInteraction.PlotInteraction(self)
+ self._eventHandler.setInteractiveMode('zoom', color=(0., 0., 0., 1.))
+
+ self._pressedButtons = [] # Currently pressed mouse buttons
+
+ self._defaultDataMargins = (0., 0., 0., 0.)
+
+ # Only activate autoreplot at the end
+ # This avoids errors when loaded in Qt designer
+ self._dirty = False
+ self._autoreplot = True
widget = self.getWidgetHandle()
if widget is not None:
self.setCentralWidget(widget)
else:
- _logger.warning("Plot backend does not support widget")
+ _logger.info("PlotWidget backend does not support widget")
self.setFocusPolicy(qt.Qt.StrongFocus)
self.setFocus(qt.Qt.OtherFocusReason)
+ # Set default limits
+ self.setGraphXLimits(0., 100.)
+ self.setGraphYLimits(0., 100., axis='right')
+ self.setGraphYLimits(0., 100., axis='left')
+
+ @staticmethod
+ def setDefaultBackend(backend):
+ """Set system wide default plot backend.
+
+ .. versionadded:: 0.6
+
+ :param backend: The backend to use, in:
+ 'matplotlib' (default), 'mpl', 'opengl', 'gl', 'none'
+ or a :class:`BackendBase.BackendBase` class
+ """
+ PlotWidget.DEFAULT_BACKEND = backend
+
+ def _getDirtyPlot(self):
+ """Return the plot dirty flag.
+
+ If False, the plot has not changed since last replot.
+ If True, the full plot need to be redrawn.
+ If 'overlay', only the overlay has changed since last replot.
+
+ It can be accessed by backend to check the dirty state.
+
+ :return: False, True, 'overlay'
+ """
+ return self._dirty
+
+ def _setDirtyPlot(self, overlayOnly=False):
+ """Mark the plot as needing redraw
+
+ :param bool overlayOnly: True to redraw only the overlay,
+ False to redraw everything
+ """
+ wasDirty = self._dirty
+
+ if not self._dirty and overlayOnly:
+ self._dirty = 'overlay'
+ else:
+ self._dirty = True
+
+ if self._autoreplot and not wasDirty:
+ self._backend.postRedisplay()
+
+ def _invalidateDataRange(self):
+ """
+ Notifies this PlotWidget instance that the range has changed
+ and will have to be recomputed.
+ """
+ self._dataRange = None
+
+ def _updateDataRange(self):
+ """
+ Recomputes the range of the data displayed on this PlotWidget.
+ """
+ xMin = yMinLeft = yMinRight = float('nan')
+ xMax = yMaxLeft = yMaxRight = float('nan')
+
+ for item in self._content.values():
+ if item.isVisible():
+ bounds = item.getBounds()
+ if bounds is not None:
+ xMin = numpy.nanmin([xMin, bounds[0]])
+ xMax = numpy.nanmax([xMax, bounds[1]])
+ # Take care of right axis
+ if (isinstance(item, items.YAxisMixIn) and
+ item.getYAxis() == 'right'):
+ yMinRight = numpy.nanmin([yMinRight, bounds[2]])
+ yMaxRight = numpy.nanmax([yMaxRight, bounds[3]])
+ else:
+ yMinLeft = numpy.nanmin([yMinLeft, bounds[2]])
+ yMaxLeft = numpy.nanmax([yMaxLeft, bounds[3]])
+
+ def lGetRange(x, y):
+ return None if numpy.isnan(x) and numpy.isnan(y) else (x, y)
+ xRange = lGetRange(xMin, xMax)
+ yLeftRange = lGetRange(yMinLeft, yMaxLeft)
+ yRightRange = lGetRange(yMinRight, yMaxRight)
+
+ self._dataRange = _PlotDataRange(x=xRange,
+ y=yLeftRange,
+ yright=yRightRange)
+
+ def getDataRange(self):
+ """
+ Returns this PlotWidget's data range.
+
+ :return: a namedtuple with the following members :
+ x, y (left y axis), yright. Each member is a tuple (min, max)
+ or None if no data is associated with the axis.
+ :rtype: namedtuple
+ """
+ if self._dataRange is None:
+ self._updateDataRange()
+ return self._dataRange
+
+ # Content management
+
+ @staticmethod
+ def _itemKey(item):
+ """Build the key of given :class:`Item` in the plot
+
+ :param Item item: The item to make the key from
+ :return: (legend, kind)
+ :rtype: (str, str)
+ """
+ if isinstance(item, items.Curve):
+ kind = 'curve'
+ elif isinstance(item, items.ImageBase):
+ kind = 'image'
+ elif isinstance(item, items.Scatter):
+ kind = 'scatter'
+ elif isinstance(item, (items.Marker,
+ items.XMarker, items.YMarker)):
+ kind = 'marker'
+ elif isinstance(item, items.Shape):
+ kind = 'item'
+ elif isinstance(item, items.Histogram):
+ kind = 'histogram'
+ else:
+ raise ValueError('Unsupported item type %s' % type(item))
+
+ return item.getLegend(), kind
+
+ def _add(self, item):
+ """Add the given :class:`Item` to the plot.
+
+ :param Item item: The item to append to the plot content
+ """
+ key = self._itemKey(item)
+ if key in self._content:
+ raise RuntimeError('Item already in the plot')
+
+ # Add item to plot
+ self._content[key] = item
+ item._setPlot(self)
+ if item.isVisible():
+ self._itemRequiresUpdate(item)
+ if isinstance(item, (items.Curve, items.ImageBase)):
+ self._invalidateDataRange() # TODO handle this automatically
+
+ self._notifyContentChanged(item)
+
+ def _notifyContentChanged(self, item):
+ legend, kind = self._itemKey(item)
+ self.notify('contentChanged', action='add', kind=kind, legend=legend)
+
+ def _remove(self, item):
+ """Remove the given :class:`Item` from the plot.
+
+ :param Item item: The item to remove from the plot content
+ """
+ key = self._itemKey(item)
+ if key not in self._content:
+ raise RuntimeError('Item not in the plot')
+
+ legend, kind = key
+
+ if kind in self._ACTIVE_ITEM_KINDS:
+ if self._getActiveItem(kind) == item:
+ # Reset active item
+ self._setActiveItem(kind, None)
+
+ # Remove item from plot
+ self._content.pop(key)
+ if item in self._contentToUpdate:
+ self._contentToUpdate.remove(item)
+ if item.isVisible():
+ self._setDirtyPlot(overlayOnly=item.isOverlay())
+ if item.getBounds() is not None:
+ self._invalidateDataRange()
+ item._removeBackendRenderer(self._backend)
+ item._setPlot(None)
+
+ if (kind == 'curve' and not self.getAllCurves(just_legend=True,
+ withhidden=True)):
+ self._colorIndex = 0
+ self._styleIndex = 0
+
+ self.notify('contentChanged', action='remove',
+ kind=kind, legend=legend)
+
+ def _itemRequiresUpdate(self, item):
+ """Called by items in the plot for asynchronous update
+
+ :param Item item: The item that required update
+ """
+ assert item.getPlot() == self
+ # Pu item at the end of the list
+ if item in self._contentToUpdate:
+ self._contentToUpdate.remove(item)
+ self._contentToUpdate.append(item)
+ self._setDirtyPlot(overlayOnly=item.isOverlay())
+
+ @contextmanager
+ def _muteActiveItemChangedSignal(self):
+ self.__muteActiveItemChanged = True
+ yield
+ self.__muteActiveItemChanged = False
+
+ # Add
+
+ # add * input arguments management:
+ # If an arg is set, then use it.
+ # Else:
+ # If a curve with the same legend exists, then use its arg value
+ # Else, use a default value.
+ # Store used value.
+ # This value is used when curve is updated either internally or by user.
+
+ def addCurve(self, x, y, legend=None, info=None,
+ replace=False, replot=None,
+ color=None, symbol=None,
+ linewidth=None, linestyle=None,
+ xlabel=None, ylabel=None, yaxis=None,
+ xerror=None, yerror=None, z=None, selectable=None,
+ fill=None, resetzoom=True,
+ histogram=None, copy=True, **kw):
+ """Add a 1D curve given by x an y to the graph.
+
+ Curves are uniquely identified by their legend.
+ To add multiple curves, call :meth:`addCurve` multiple times with
+ different legend argument.
+ To replace an existing curve, call :meth:`addCurve` with the
+ existing curve legend.
+ If you want to display the curve values as an histogram see the
+ histogram parameter or :meth:`addHistogram`.
+
+ When curve parameters are not provided, if a curve with the
+ same legend is displayed in the plot, its parameters are used.
+
+ :param numpy.ndarray x: The data corresponding to the x coordinates.
+ If you attempt to plot an histogram you can set edges values in x.
+ In this case len(x) = len(y) + 1
+ :param numpy.ndarray y: The data corresponding to the y coordinates
+ :param str legend: The legend to be associated to the curve (or None)
+ :param info: User-defined information associated to the curve
+ :param bool replace: True (the default) to delete already existing
+ curves
+ :param color: color(s) to be used
+ :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
+ one of the predefined color names defined in Colors.py
+ :param str symbol: Symbol to be drawn at each (x, y) position::
+
+ - 'o' circle
+ - '.' point
+ - ',' pixel
+ - '+' cross
+ - 'x' x-cross
+ - 'd' diamond
+ - 's' square
+ - None (the default) to use default symbol
+
+ :param float linewidth: The width of the curve in pixels (Default: 1).
+ :param str linestyle: Type of line::
+
+ - ' ' no line
+ - '-' solid line
+ - '--' dashed line
+ - '-.' dash-dot line
+ - ':' dotted line
+ - None (the default) to use default line style
+
+ :param str xlabel: Label to show on the X axis when the curve is active
+ or None to keep default axis label.
+ :param str ylabel: Label to show on the Y axis when the curve is active
+ or None to keep default axis label.
+ :param str yaxis: The Y axis this curve is attached to.
+ Either 'left' (the default) or 'right'
+ :param xerror: Values with the uncertainties on the x values
+ :type xerror: A float, or a numpy.ndarray of float32.
+ If it is an array, it can either be a 1D array of
+ same length as the data or a 2D array with 2 rows
+ of same length as the data: row 0 for positive errors,
+ row 1 for negative errors.
+ :param yerror: Values with the uncertainties on the y values
+ :type yerror: A float, or a numpy.ndarray of float32. See xerror.
+ :param int z: Layer on which to draw the curve (default: 1)
+ This allows to control the overlay.
+ :param bool selectable: Indicate if the curve can be selected.
+ (Default: True)
+ :param bool fill: True to fill the curve, False otherwise (default).
+ :param bool resetzoom: True (the default) to reset the zoom.
+ :param str histogram: if not None then the curve will be draw as an
+ histogram. The step for each values of the curve can be set to the
+ left, center or right of the original x curve values.
+ If histogram is not None and len(x) == len(y)+1 then x is directly
+ take as edges of the histogram.
+ Type of histogram::
+
+ - None (default)
+ - 'left'
+ - 'right'
+ - 'center'
+ :param bool copy: True make a copy of the data (default),
+ False to use provided arrays.
+ :returns: The key string identify this curve
+ """
+ # Deprecation warnings
+ if replot is not None:
+ _logger.warning(
+ 'addCurve deprecated replot argument, use resetzoom instead')
+ resetzoom = replot and resetzoom
+
+ if kw:
+ _logger.warning('addCurve: deprecated extra arguments')
+
+ # This is an histogram, use addHistogram
+ if histogram is not None:
+ histoLegend = self.addHistogram(histogram=y,
+ edges=x,
+ legend=legend,
+ color=color,
+ fill=fill,
+ align=histogram,
+ copy=copy)
+ histo = self.getHistogram(histoLegend)
+
+ histo.setInfo(info)
+ if linewidth is not None:
+ histo.setLineWidth(linewidth)
+ if linestyle is not None:
+ histo.setLineStyle(linestyle)
+ if xlabel is not None:
+ _logger.warning(
+ 'addCurve: Histogram does not support xlabel argument')
+ if ylabel is not None:
+ _logger.warning(
+ 'addCurve: Histogram does not support ylabel argument')
+ if yaxis is not None:
+ histo.setYAxis(yaxis)
+ if z is not None:
+ histo.setZValue(z)
+ if selectable is not None:
+ _logger.warning(
+ 'addCurve: Histogram does not support selectable argument')
+
+ return
+
+ legend = 'Unnamed curve 1.1' if legend is None else str(legend)
+
+ # Check if curve was previously active
+ wasActive = self.getActiveCurve(just_legend=True) == legend
+
+ # Create/Update curve object
+ curve = self.getCurve(legend)
+ mustBeAdded = curve is None
+ if curve is None:
+ # No previous curve, create a default one and add it to the plot
+ curve = items.Curve() if histogram is None else items.Histogram()
+ curve._setLegend(legend)
+ # Set default color, linestyle and symbol
+ default_color, default_linestyle = self._getColorAndStyle()
+ curve.setColor(default_color)
+ curve.setLineStyle(default_linestyle)
+ curve.setSymbol(self._defaultPlotPoints)
+
+ # Do not emit sigActiveCurveChanged,
+ # it will be sent once with _setActiveItem
+ with self._muteActiveItemChangedSignal():
+ # Override previous/default values with provided ones
+ curve.setInfo(info)
+ if color is not None:
+ curve.setColor(color)
+ if symbol is not None:
+ curve.setSymbol(symbol)
+ if linewidth is not None:
+ curve.setLineWidth(linewidth)
+ if linestyle is not None:
+ curve.setLineStyle(linestyle)
+ if xlabel is not None:
+ curve._setXLabel(xlabel)
+ if ylabel is not None:
+ curve._setYLabel(ylabel)
+ if yaxis is not None:
+ curve.setYAxis(yaxis)
+ if z is not None:
+ curve.setZValue(z)
+ if selectable is not None:
+ curve._setSelectable(selectable)
+ if fill is not None:
+ curve.setFill(fill)
+
+ # Set curve data
+ # If errors not provided, reuse previous ones
+ # TODO: Issue if size of data change but not that of errors
+ if xerror is None:
+ xerror = curve.getXErrorData(copy=False)
+ if yerror is None:
+ yerror = curve.getYErrorData(copy=False)
+
+ curve.setData(x, y, xerror, yerror, copy=copy)
+
+ if replace: # Then remove all other curves
+ for c in self.getAllCurves(withhidden=True):
+ if c is not curve:
+ self._remove(c)
+
+ if mustBeAdded:
+ self._add(curve)
+ else:
+ self._notifyContentChanged(curve)
+
+ if wasActive:
+ self.setActiveCurve(curve.getLegend())
+
+ if resetzoom:
+ # We ask for a zoom reset in order to handle the plot scaling
+ # if the user does not want that, autoscale of the different
+ # axes has to be set to off.
+ self.resetZoom()
+
+ return legend
+
+ def addHistogram(self,
+ histogram,
+ edges,
+ legend=None,
+ color=None,
+ fill=None,
+ align='center',
+ resetzoom=True,
+ copy=True):
+ """Add an histogram to the graph.
+
+ This is NOT computing the histogram, this method takes as parameter
+ already computed histogram values.
+
+ Histogram are uniquely identified by their legend.
+ To add multiple histograms, call :meth:`addHistogram` multiple times
+ with different legend argument.
+
+ When histogram parameters are not provided, if an histogram with the
+ same legend is displayed in the plot, its parameters are used.
+
+ :param numpy.ndarray histogram: The values of the histogram.
+ :param numpy.ndarray edges:
+ The bin edges of the histogram.
+ If histogram and edges have the same length, the bin edges
+ are computed according to the align parameter.
+ :param str legend:
+ The legend to be associated to the histogram (or None)
+ :param color: color to be used
+ :type color: str ("#RRGGBB") or RGB unsigned byte array or
+ one of the predefined color names defined in Colors.py
+ :param bool fill: True to fill the curve, False otherwise (default).
+ :param str align:
+ In case histogram values and edges have the same length N,
+ the N+1 bin edges are computed according to the alignment in:
+ 'center' (default), 'left', 'right'.
+ :param bool resetzoom: True (the default) to reset the zoom.
+ :param bool copy: True make a copy of the data (default),
+ False to use provided arrays.
+ :returns: The key string identify this histogram
+ """
+ legend = 'Unnamed histogram' if legend is None else str(legend)
+
+ # Create/Update histogram object
+ histo = self.getHistogram(legend)
+ mustBeAdded = histo is None
+ if histo is None:
+ # No previous histogram, create a default one and
+ # add it to the plot
+ histo = items.Histogram()
+ histo._setLegend(legend)
+ histo.setColor(self._getColorAndStyle()[0])
+
+ # Override previous/default values with provided ones
+ if color is not None:
+ histo.setColor(color)
+ if fill is not None:
+ histo.setFill(fill)
+
+ # Set histogram data
+ histo.setData(histogram, edges, align=align, copy=copy)
+
+ if mustBeAdded:
+ self._add(histo)
+ else:
+ self._notifyContentChanged(histo)
+
+ if resetzoom:
+ # We ask for a zoom reset in order to handle the plot scaling
+ # if the user does not want that, autoscale of the different
+ # axes has to be set to off.
+ self.resetZoom()
+
+ return legend
+
+ def addImage(self, data, legend=None, info=None,
+ replace=True, replot=None,
+ xScale=None, yScale=None, z=None,
+ selectable=None, draggable=None,
+ colormap=None, pixmap=None,
+ xlabel=None, ylabel=None,
+ origin=None, scale=None,
+ resetzoom=True, copy=True, **kw):
+ """Add a 2D dataset or an image to the plot.
+
+ It displays either an array of data using a colormap or a RGB(A) image.
+
+ Images are uniquely identified by their legend.
+ To add multiple images, call :meth:`addImage` multiple times with
+ different legend argument.
+ To replace/update an existing image, call :meth:`addImage` with the
+ existing image legend.
+
+ When image parameters are not provided, if an image with the
+ same legend is displayed in the plot, its parameters are used.
+
+ :param numpy.ndarray data:
+ (nrows, ncolumns) data or
+ (nrows, ncolumns, RGBA) ubyte array
+ Note: boolean values are converted to int8.
+ :param str legend: The legend to be associated to the image (or None)
+ :param info: User-defined information associated to the image
+ :param bool replace: True (default) to delete already existing images
+ :param int z: Layer on which to draw the image (default: 0)
+ This allows to control the overlay.
+ :param bool selectable: Indicate if the image can be selected.
+ (default: False)
+ :param bool draggable: Indicate if the image can be moved.
+ (default: False)
+ :param colormap: Description of the :class:`.Colormap` to use
+ (or None).
+ This is ignored if data is a RGB(A) image.
+ :type colormap: Colormap or dict (old API )
+ :param pixmap: Pixmap representation of the data (if any)
+ :type pixmap: (nrows, ncolumns, RGBA) ubyte array or None (default)
+ :param str xlabel: X axis label to show when this curve is active,
+ or None to keep default axis label.
+ :param str ylabel: Y axis label to show when this curve is active,
+ or None to keep default axis label.
+ :param origin: (origin X, origin Y) of the data.
+ It is possible to pass a single float if both
+ coordinates are equal.
+ Default: (0., 0.)
+ :type origin: float or 2-tuple of float
+ :param scale: (scale X, scale Y) of the data.
+ It is possible to pass a single float if both
+ coordinates are equal.
+ Default: (1., 1.)
+ :type scale: float or 2-tuple of float
+ :param bool resetzoom: True (the default) to reset the zoom.
+ :param bool copy: True make a copy of the data (default),
+ False to use provided arrays.
+ :returns: The key string identify this image
+ """
+ # Deprecation warnings
+ if xScale is not None or yScale is not None:
+ _logger.warning(
+ 'addImage deprecated xScale and yScale arguments,'
+ 'use origin, scale arguments instead.')
+ if origin is None and scale is None:
+ origin = xScale[0], yScale[0]
+ scale = xScale[1], yScale[1]
+ else:
+ _logger.warning(
+ 'addCurve: xScale, yScale and origin, scale arguments'
+ ' are conflicting. xScale and yScale are ignored.'
+ ' Use only origin, scale arguments.')
+
+ if replot is not None:
+ _logger.warning(
+ 'addImage deprecated replot argument, use resetzoom instead')
+ resetzoom = replot and resetzoom
+
+ if kw:
+ _logger.warning('addImage: deprecated extra arguments')
+
+ legend = "Unnamed Image 1.1" if legend is None else str(legend)
+
+ # Check if image was previously active
+ wasActive = self.getActiveImage(just_legend=True) == legend
+
+ data = numpy.array(data, copy=False)
+ assert data.ndim in (2, 3)
+
+ image = self.getImage(legend)
+ if image is not None and image.getData(copy=False).ndim != data.ndim:
+ # Update a data image with RGBA image or the other way around:
+ # Remove previous image
+ # In this case, we don't retrieve defaults from the previous image
+ self._remove(image)
+ image = None
+
+ mustBeAdded = image is None
+ if image is None:
+ # No previous image, create a default one and add it to the plot
+ if data.ndim == 2:
+ image = items.ImageData()
+ image.setColormap(self.getDefaultColormap())
+ else:
+ image = items.ImageRgba()
+ image._setLegend(legend)
+
+ # Do not emit sigActiveImageChanged,
+ # it will be sent once with _setActiveItem
+ with self._muteActiveItemChangedSignal():
+ # Override previous/default values with provided ones
+ image.setInfo(info)
+ if origin is not None:
+ image.setOrigin(origin)
+ if scale is not None:
+ image.setScale(scale)
+ if z is not None:
+ image.setZValue(z)
+ if selectable is not None:
+ image._setSelectable(selectable)
+ if draggable is not None:
+ image._setDraggable(draggable)
+ if colormap is not None and isinstance(image, items.ColormapMixIn):
+ if isinstance(colormap, dict):
+ image.setColormap(Colormap._fromDict(colormap))
+ else:
+ assert isinstance(colormap, Colormap)
+ image.setColormap(colormap)
+ if xlabel is not None:
+ image._setXLabel(xlabel)
+ if ylabel is not None:
+ image._setYLabel(ylabel)
+
+ if data.ndim == 2:
+ image.setData(data, alternative=pixmap, copy=copy)
+ else: # RGB(A) image
+ if pixmap is not None:
+ _logger.warning(
+ 'addImage: pixmap argument ignored when data is RGB(A)')
+ image.setData(data, copy=copy)
+
+ if replace:
+ for img in self.getAllImages():
+ if img is not image:
+ self._remove(img)
+
+ if mustBeAdded:
+ self._add(image)
+ else:
+ self._notifyContentChanged(image)
+
+ if len(self.getAllImages()) == 1 or wasActive:
+ self.setActiveImage(legend)
+
+ if resetzoom:
+ # We ask for a zoom reset in order to handle the plot scaling
+ # if the user does not want that, autoscale of the different
+ # axes has to be set to off.
+ self.resetZoom()
+
+ return legend
+
+ def addScatter(self, x, y, value, legend=None, colormap=None,
+ info=None, symbol=None, xerror=None, yerror=None,
+ z=None, copy=True):
+ """Add a (x, y, value) scatter to the graph.
+
+ Scatters are uniquely identified by their legend.
+ To add multiple scatters, call :meth:`addScatter` multiple times with
+ different legend argument.
+ To replace/update an existing scatter, call :meth:`addScatter` with the
+ existing scatter legend.
+
+ When scatter parameters are not provided, if a scatter with the
+ same legend is displayed in the plot, its parameters are used.
+
+ :param numpy.ndarray x: The data corresponding to the x coordinates.
+ :param numpy.ndarray y: The data corresponding to the y coordinates
+ :param numpy.ndarray value: The data value associated with each point
+ :param str legend: The legend to be associated to the scatter (or None)
+ :param Colormap colormap: The :class:`.Colormap`. to be used for the
+ scatter (or None)
+ :param info: User-defined information associated to the curve
+ :param str symbol: Symbol to be drawn at each (x, y) position::
+
+ - 'o' circle
+ - '.' point
+ - ',' pixel
+ - '+' cross
+ - 'x' x-cross
+ - 'd' diamond
+ - 's' square
+ - None (the default) to use default symbol
+
+ :param xerror: Values with the uncertainties on the x values
+ :type xerror: A float, or a numpy.ndarray of float32.
+ If it is an array, it can either be a 1D array of
+ same length as the data or a 2D array with 2 rows
+ of same length as the data: row 0 for positive errors,
+ row 1 for negative errors.
+ :param yerror: Values with the uncertainties on the y values
+ :type yerror: A float, or a numpy.ndarray of float32. See xerror.
+ :param int z: Layer on which to draw the scatter (default: 1)
+ This allows to control the overlay.
+
+ :param bool copy: True make a copy of the data (default),
+ False to use provided arrays.
+ :returns: The key string identify this scatter
+ """
+ legend = 'Unnamed scatter 1.1' if legend is None else str(legend)
+
+ # Check if scatter was previously active
+ wasActive = self._getActiveItem(kind='scatter',
+ just_legend=True) == legend
+
+ # Create/Update curve object
+ scatter = self._getItem(kind='scatter', legend=legend)
+ mustBeAdded = scatter is None
+ if scatter is None:
+ # No previous scatter, create a default one and add it to the plot
+ scatter = items.Scatter()
+ scatter._setLegend(legend)
+ scatter.setColormap(self.getDefaultColormap())
+
+ # Do not emit sigActiveScatterChanged,
+ # it will be sent once with _setActiveItem
+ with self._muteActiveItemChangedSignal():
+ # Override previous/default values with provided ones
+ scatter.setInfo(info)
+ if symbol is not None:
+ scatter.setSymbol(symbol)
+ if z is not None:
+ scatter.setZValue(z)
+ if colormap is not None:
+ if isinstance(colormap, dict):
+ scatter.setColormap(Colormap._fromDict(colormap))
+ else:
+ assert isinstance(colormap, Colormap)
+ scatter.setColormap(colormap)
+
+ # Set scatter data
+ # If errors not provided, reuse previous ones
+ if xerror is None:
+ xerror = scatter.getXErrorData(copy=False)
+ if xerror is not None and len(xerror) != len(x):
+ xerror = None
+ if yerror is None:
+ yerror = scatter.getYErrorData(copy=False)
+ if yerror is not None and len(yerror) != len(y):
+ yerror = None
+
+ scatter.setData(x, y, value, xerror, yerror, copy=copy)
+
+ if mustBeAdded:
+ self._add(scatter)
+ else:
+ self._notifyContentChanged(scatter)
+
+ if len(self._getItems(kind="scatter")) == 1 or wasActive:
+ self._setActiveItem('scatter', scatter.getLegend())
+
+ return legend
+
+ def addItem(self, xdata, ydata, legend=None, info=None,
+ replace=False,
+ shape="polygon", color='black', fill=True,
+ overlay=False, z=None, **kw):
+ """Add an item (i.e. a shape) to the plot.
+
+ Items are uniquely identified by their legend.
+ To add multiple items, call :meth:`addItem` multiple times with
+ different legend argument.
+ To replace/update an existing item, call :meth:`addItem` with the
+ existing item legend.
+
+ :param numpy.ndarray xdata: The X coords of the points of the shape
+ :param numpy.ndarray ydata: The Y coords of the points of the shape
+ :param str legend: The legend to be associated to the item
+ :param info: User-defined information associated to the item
+ :param bool replace: True (default) to delete already existing images
+ :param str shape: Type of item to be drawn in
+ hline, polygon (the default), rectangle, vline,
+ polylines
+ :param str color: Color of the item, e.g., 'blue', 'b', '#FF0000'
+ (Default: 'black')
+ :param bool fill: True (the default) to fill the shape
+ :param bool overlay: True if item is an overlay (Default: False).
+ This allows for rendering optimization if this
+ item is changed often.
+ :param int z: Layer on which to draw the item (default: 2)
+ :returns: The key string identify this item
+ """
+ # expected to receive the same parameters as the signal
+
+ if kw:
+ _logger.warning('addItem deprecated parameters: %s', str(kw))
+
+ legend = "Unnamed Item 1.1" if legend is None else str(legend)
+
+ z = int(z) if z is not None else 2
+
+ if replace:
+ self.remove(kind='item')
+ else:
+ self.remove(legend, kind='item')
+
+ item = items.Shape(shape)
+ item._setLegend(legend)
+ item.setInfo(info)
+ item.setColor(color)
+ item.setFill(fill)
+ item.setOverlay(overlay)
+ item.setZValue(z)
+ item.setPoints(numpy.array((xdata, ydata)).T)
+
+ self._add(item)
+
+ return legend
+
+ def addXMarker(self, x, legend=None,
+ text=None,
+ color=None,
+ selectable=False,
+ draggable=False,
+ constraint=None,
+ **kw):
+ """Add a vertical line marker to the plot.
+
+ Markers are uniquely identified by their legend.
+ As opposed to curves, images and items, two calls to
+ :meth:`addXMarker` without legend argument adds two markers with
+ different identifying legends.
+
+ :param float x: Position of the marker on the X axis in data
+ coordinates
+ :param str legend: Legend associated to the marker to identify it
+ :param str text: Text to display on the marker.
+ :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
+ (Default: 'black')
+ :param bool selectable: Indicate if the marker can be selected.
+ (default: False)
+ :param bool draggable: Indicate if the marker can be moved.
+ (default: False)
+ :param constraint: A function filtering marker displacement by
+ dragging operations or None for no filter.
+ This function is called each time a marker is
+ moved.
+ This parameter is only used if draggable is True.
+ :type constraint: None or a callable that takes the coordinates of
+ the current cursor position in the plot as input
+ and that returns the filtered coordinates.
+ :return: The key string identify this marker
+ """
+ if kw:
+ _logger.warning(
+ 'addXMarker deprecated extra parameters: %s', str(kw))
+
+ return self._addMarker(x=x, y=None, legend=legend,
+ text=text, color=color,
+ selectable=selectable, draggable=draggable,
+ symbol=None, constraint=constraint)
+
+ def addYMarker(self, y,
+ legend=None,
+ text=None,
+ color=None,
+ selectable=False,
+ draggable=False,
+ constraint=None,
+ **kw):
+ """Add a horizontal line marker to the plot.
+
+ Markers are uniquely identified by their legend.
+ As opposed to curves, images and items, two calls to
+ :meth:`addYMarker` without legend argument adds two markers with
+ different identifying legends.
+
+ :param float y: Position of the marker on the Y axis in data
+ coordinates
+ :param str legend: Legend associated to the marker to identify it
+ :param str text: Text to display next to the marker.
+ :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
+ (Default: 'black')
+ :param bool selectable: Indicate if the marker can be selected.
+ (default: False)
+ :param bool draggable: Indicate if the marker can be moved.
+ (default: False)
+ :param constraint: A function filtering marker displacement by
+ dragging operations or None for no filter.
+ This function is called each time a marker is
+ moved.
+ This parameter is only used if draggable is True.
+ :type constraint: None or a callable that takes the coordinates of
+ the current cursor position in the plot as input
+ and that returns the filtered coordinates.
+ :return: The key string identify this marker
+ """
+ if kw:
+ _logger.warning(
+ 'addYMarker deprecated extra parameters: %s', str(kw))
+
+ return self._addMarker(x=None, y=y, legend=legend,
+ text=text, color=color,
+ selectable=selectable, draggable=draggable,
+ symbol=None, constraint=constraint)
+
+ def addMarker(self, x, y, legend=None,
+ text=None,
+ color=None,
+ selectable=False,
+ draggable=False,
+ symbol='+',
+ constraint=None,
+ **kw):
+ """Add a point marker to the plot.
+
+ Markers are uniquely identified by their legend.
+ As opposed to curves, images and items, two calls to
+ :meth:`addMarker` without legend argument adds two markers with
+ different identifying legends.
+
+ :param float x: Position of the marker on the X axis in data
+ coordinates
+ :param float y: Position of the marker on the Y axis in data
+ coordinates
+ :param str legend: Legend associated to the marker to identify it
+ :param str text: Text to display next to the marker
+ :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
+ (Default: 'black')
+ :param bool selectable: Indicate if the marker can be selected.
+ (default: False)
+ :param bool draggable: Indicate if the marker can be moved.
+ (default: False)
+ :param str symbol: Symbol representing the marker in::
+
+ - 'o' circle
+ - '.' point
+ - ',' pixel
+ - '+' cross (the default)
+ - 'x' x-cross
+ - 'd' diamond
+ - 's' square
+
+ :param constraint: A function filtering marker displacement by
+ dragging operations or None for no filter.
+ This function is called each time a marker is
+ moved.
+ This parameter is only used if draggable is True.
+ :type constraint: None or a callable that takes the coordinates of
+ the current cursor position in the plot as input
+ and that returns the filtered coordinates.
+ :return: The key string identify this marker
+ """
+ if kw:
+ _logger.warning(
+ 'addMarker deprecated extra parameters: %s', str(kw))
+
+ if x is None:
+ xmin, xmax = self._xAxis.getLimits()
+ x = 0.5 * (xmax + xmin)
+
+ if y is None:
+ ymin, ymax = self._yAxis.getLimits()
+ y = 0.5 * (ymax + ymin)
+
+ return self._addMarker(x=x, y=y, legend=legend,
+ text=text, color=color,
+ selectable=selectable, draggable=draggable,
+ symbol=symbol, constraint=constraint)
+
+ def _addMarker(self, x, y, legend,
+ text, color,
+ selectable, draggable,
+ symbol, constraint):
+ """Common method for adding point, vline and hline marker.
+
+ See :meth:`addMarker` for argument documentation.
+ """
+ assert (x, y) != (None, None)
+
+ if legend is None: # Find an unused legend
+ markerLegends = self._getAllMarkers(just_legend=True)
+ for index in itertools.count():
+ legend = "Unnamed Marker %d" % index
+ if legend not in markerLegends:
+ break # Keep this legend
+ legend = str(legend)
+
+ if x is None:
+ markerClass = items.YMarker
+ elif y is None:
+ markerClass = items.XMarker
+ else:
+ markerClass = items.Marker
+
+ # Create/Update marker object
+ marker = self._getMarker(legend)
+ if marker is not None and not isinstance(marker, markerClass):
+ _logger.warning('Adding marker with same legend'
+ ' but different type replaces it')
+ self._remove(marker)
+ marker = None
+
+ mustBeAdded = marker is None
+ if marker is None:
+ # No previous marker, create one
+ marker = markerClass()
+ marker._setLegend(legend)
+
+ if text is not None:
+ marker.setText(text)
+ if color is not None:
+ marker.setColor(color)
+ if selectable is not None:
+ marker._setSelectable(selectable)
+ if draggable is not None:
+ marker._setDraggable(draggable)
+ if symbol is not None:
+ marker.setSymbol(symbol)
+
+ # TODO to improve, but this ensure constraint is applied
+ marker.setPosition(x, y)
+ if constraint is not None:
+ marker._setConstraint(constraint)
+ marker.setPosition(x, y)
+
+ if mustBeAdded:
+ self._add(marker)
+ else:
+ self._notifyContentChanged(marker)
+
+ return legend
+
+ # Hide
+
+ def isCurveHidden(self, legend):
+ """Returns True if the curve associated to legend is hidden, else False
+
+ :param str legend: The legend key identifying the curve
+ :return: True if the associated curve is hidden, False otherwise
+ """
+ curve = self._getItem('curve', legend)
+ return curve is not None and not curve.isVisible()
+
+ def hideCurve(self, legend, flag=True, replot=None):
+ """Show/Hide the curve associated to legend.
+
+ Even when hidden, the curve is kept in the list of curves.
+
+ :param str legend: The legend associated to the curve to be hidden
+ :param bool flag: True (default) to hide the curve, False to show it
+ """
+ if replot is not None:
+ _logger.warning('hideCurve deprecated replot parameter')
+
+ curve = self._getItem('curve', legend)
+ if curve is None:
+ _logger.warning('Curve not in plot: %s', legend)
+ return
+
+ isVisible = not flag
+ if isVisible != curve.isVisible():
+ curve.setVisible(isVisible)
+
+ # Remove
+
+ ITEM_KINDS = 'curve', 'image', 'scatter', 'item', 'marker', 'histogram'
+ """List of supported kind of items in the plot."""
+
+ _ACTIVE_ITEM_KINDS = 'curve', 'scatter', 'image'
+ """List of item's kind which have a active item."""
+
+ def remove(self, legend=None, kind=ITEM_KINDS):
+ """Remove one or all element(s) of the given legend and kind.
+
+ Examples:
+
+ - ``remove()`` clears the plot
+ - ``remove(kind='curve')`` removes all curves from the plot
+ - ``remove('myCurve', kind='curve')`` removes the curve with
+ legend 'myCurve' from the plot.
+ - ``remove('myImage, kind='image')`` removes the image with
+ legend 'myImage' from the plot.
+ - ``remove('myImage')`` removes elements (for instance curve, image,
+ item and marker) with legend 'myImage'.
+
+ :param str legend: The legend associated to the element to remove,
+ or None to remove
+ :param kind: The kind of elements to remove from the plot.
+ See :attr:`ITEM_KINDS`.
+ By default, it removes all kind of elements.
+ :type kind: str or tuple of str to specify multiple kinds.
+ """
+ if kind is 'all': # Replace all by tuple of all kinds
+ kind = self.ITEM_KINDS
+
+ if kind in self.ITEM_KINDS: # Kind is a str, make it a tuple
+ kind = (kind,)
+
+ for aKind in kind:
+ assert aKind in self.ITEM_KINDS
+
+ if legend is None: # This is a clear
+ # Clear each given kind
+ for aKind in kind:
+ for legend in self._getItems(
+ kind=aKind, just_legend=True, withhidden=True):
+ self.remove(legend=legend, kind=aKind)
+
+ else: # This is removing a single element
+ # Remove each given kind
+ for aKind in kind:
+ item = self._getItem(aKind, legend)
+ if item is not None:
+ self._remove(item)
+
+ def removeCurve(self, legend):
+ """Remove the curve associated to legend from the graph.
+
+ :param str legend: The legend associated to the curve to be deleted
+ """
+ if legend is None:
+ return
+ self.remove(legend, kind='curve')
+
+ def removeImage(self, legend):
+ """Remove the image associated to legend from the graph.
+
+ :param str legend: The legend associated to the image to be deleted
+ """
+ if legend is None:
+ return
+ self.remove(legend, kind='image')
+
+ def removeItem(self, legend):
+ """Remove the item associated to legend from the graph.
+
+ :param str legend: The legend associated to the item to be deleted
+ """
+ if legend is None:
+ return
+ self.remove(legend, kind='item')
+
+ def removeMarker(self, legend):
+ """Remove the marker associated to legend from the graph.
+
+ :param str legend: The legend associated to the marker to be deleted
+ """
+ if legend is None:
+ return
+ self.remove(legend, kind='marker')
+
+ # Clear
+
+ def clear(self):
+ """Remove everything from the plot."""
+ self.remove()
+
+ def clearCurves(self):
+ """Remove all the curves from the plot."""
+ self.remove(kind='curve')
+
+ def clearImages(self):
+ """Remove all the images from the plot."""
+ self.remove(kind='image')
+
+ def clearItems(self):
+ """Remove all the items from the plot. """
+ self.remove(kind='item')
+
+ def clearMarkers(self):
+ """Remove all the markers from the plot."""
+ self.remove(kind='marker')
+
+ # Interaction
+
+ def getGraphCursor(self):
+ """Returns the state of the crosshair cursor.
+
+ See :meth:`setGraphCursor`.
+
+ :return: None if the crosshair cursor is not active,
+ else a tuple (color, linewidth, linestyle).
+ """
+ return self._cursorConfiguration
+
+ def setGraphCursor(self, flag=False, color='black',
+ linewidth=1, linestyle='-'):
+ """Toggle the display of a crosshair cursor and set its attributes.
+
+ :param bool flag: Toggle the display of a crosshair cursor.
+ The crosshair cursor is hidden by default.
+ :param color: The color to use for the crosshair.
+ :type color: A string (either a predefined color name in Colors.py
+ or "#RRGGBB")) or a 4 columns unsigned byte array
+ (Default: black).
+ :param int linewidth: The width of the lines of the crosshair
+ (Default: 1).
+ :param str linestyle: Type of line::
+
+ - ' ' no line
+ - '-' solid line (the default)
+ - '--' dashed line
+ - '-.' dash-dot line
+ - ':' dotted line
+ """
+ if flag:
+ self._cursorConfiguration = color, linewidth, linestyle
+ else:
+ self._cursorConfiguration = None
+
+ self._backend.setGraphCursor(flag=flag, color=color,
+ linewidth=linewidth, linestyle=linestyle)
+ self._setDirtyPlot()
+ self.notify('setGraphCursor',
+ state=self._cursorConfiguration is not None)
+
+ def pan(self, direction, factor=0.1):
+ """Pan the graph in the given direction by the given factor.
+
+ Warning: Pan of right Y axis not implemented!
+
+ :param str direction: One of 'up', 'down', 'left', 'right'.
+ :param float factor: Proportion of the range used to pan the graph.
+ Must be strictly positive.
+ """
+ assert direction in ('up', 'down', 'left', 'right')
+ assert factor > 0.
+
+ if direction in ('left', 'right'):
+ xFactor = factor if direction == 'right' else - factor
+ xMin, xMax = self._xAxis.getLimits()
+
+ xMin, xMax = _utils.applyPan(xMin, xMax, xFactor,
+ self._xAxis.getScale() == self._xAxis.LOGARITHMIC)
+ self._xAxis.setLimits(xMin, xMax)
+
+ else: # direction in ('up', 'down')
+ sign = -1. if self._yAxis.isInverted() else 1.
+ yFactor = sign * (factor if direction == 'up' else -factor)
+ yMin, yMax = self._yAxis.getLimits()
+ yIsLog = self._yAxis.getScale() == self._yAxis.LOGARITHMIC
+
+ yMin, yMax = _utils.applyPan(yMin, yMax, yFactor, yIsLog)
+ self._yAxis.setLimits(yMin, yMax)
+
+ y2Min, y2Max = self._yRightAxis.getLimits()
+
+ y2Min, y2Max = _utils.applyPan(y2Min, y2Max, yFactor, yIsLog)
+ self._yRightAxis.setLimits(y2Min, y2Max)
+
+ # Active Curve/Image
+
+ def isActiveCurveHandling(self):
+ """Returns True if active curve selection is enabled."""
+ return self._activeCurveHandling
+
+ def setActiveCurveHandling(self, flag=True):
+ """Enable/Disable active curve selection.
+
+ :param bool flag: True (the default) to enable active curve selection.
+ """
+ if not flag:
+ self.setActiveCurve(None) # Reset active curve
+
+ self._activeCurveHandling = bool(flag)
+
+ def getActiveCurveColor(self):
+ """Get the color used to display the currently active curve.
+
+ See :meth:`setActiveCurveColor`.
+ """
+ return self._activeCurveColor
+
+ def setActiveCurveColor(self, color="#000000"):
+ """Set the color to use to display the currently active curve.
+
+ :param str color: Color of the active curve,
+ e.g., 'blue', 'b', '#FF0000' (Default: 'black')
+ """
+ if color is None:
+ color = "black"
+ if color in self.colorDict:
+ color = self.colorDict[color]
+ self._activeCurveColor = color
+
+ def getActiveCurve(self, just_legend=False):
+ """Return the currently active curve.
+
+ It returns None in case of not having an active curve.
+
+ :param bool just_legend: True to get the legend of the curve,
+ False (the default) to get the curve data
+ and info.
+ :return: Active curve's legend or corresponding
+ :class:`.items.Curve`
+ :rtype: str or :class:`.items.Curve` or None
+ """
+ if not self.isActiveCurveHandling():
+ return None
+
+ return self._getActiveItem(kind='curve', just_legend=just_legend)
+
+ def setActiveCurve(self, legend, replot=None):
+ """Make the curve associated to legend the active curve.
+
+ :param legend: The legend associated to the curve
+ or None to have no active curve.
+ :type legend: str or None
+ """
+ if replot is not None:
+ _logger.warning('setActiveCurve deprecated replot parameter')
+
+ if not self.isActiveCurveHandling():
+ return
+
+ return self._setActiveItem(kind='curve', legend=legend)
+
+ def getActiveImage(self, just_legend=False):
+ """Returns the currently active image.
+
+ It returns None in case of not having an active image.
+
+ :param bool just_legend: True to get the legend of the image,
+ False (the default) to get the image data
+ and info.
+ :return: Active image's legend or corresponding image object
+ :rtype: str, :class:`.items.ImageData`, :class:`.items.ImageRgba`
+ or None
+ """
+ return self._getActiveItem(kind='image', just_legend=just_legend)
+
+ def setActiveImage(self, legend, replot=None):
+ """Make the image associated to legend the active image.
+
+ :param str legend: The legend associated to the image
+ or None to have no active image.
+ """
+ if replot is not None:
+ _logger.warning('setActiveImage deprecated replot parameter')
+
+ return self._setActiveItem(kind='image', legend=legend)
+
+ def _getActiveItem(self, kind, just_legend=False):
+ """Return the currently active item of that kind if any
+
+ :param str kind: Type of item: 'curve', 'scatter' or 'image'
+ :param bool just_legend: True to get the legend,
+ False (default) to get the item
+ :return: legend or item or None if no active item
+ """
+ assert kind in self._ACTIVE_ITEM_KINDS
+
+ if self._activeLegend[kind] is None:
+ return None
+
+ if (self._activeLegend[kind], kind) not in self._content:
+ self._activeLegend[kind] = None
+ return None
+
+ if just_legend:
+ return self._activeLegend[kind]
+ else:
+ return self._getItem(kind, self._activeLegend[kind])
+
+ def _setActiveItem(self, kind, legend):
+ """Make the curve associated to legend the active curve.
+
+ :param str kind: Type of item: 'curve' or 'image'
+ :param legend: The legend associated to the curve
+ or None to have no active curve.
+ :type legend: str or None
+ """
+ assert kind in self._ACTIVE_ITEM_KINDS
+
+ xLabel = None
+ yLabel = None
+ yRightLabel = None
+
+ oldActiveItem = self._getActiveItem(kind=kind)
+
+ if oldActiveItem is not None: # Stop listening previous active image
+ oldActiveItem.sigItemChanged.disconnect(self._activeItemChanged)
+
+ # Curve specific: Reset highlight of previous active curve
+ if kind == 'curve' and oldActiveItem is not None:
+ oldActiveItem.setHighlighted(False)
+
+ if legend is None:
+ self._activeLegend[kind] = None
+ else:
+ legend = str(legend)
+ item = self._getItem(kind, legend)
+ if item is None:
+ _logger.warning("This %s does not exist: %s", kind, legend)
+ self._activeLegend[kind] = None
+ else:
+ self._activeLegend[kind] = legend
+
+ # Curve specific: handle highlight
+ if kind == 'curve':
+ item.setHighlightedColor(self.getActiveCurveColor())
+ item.setHighlighted(True)
+
+ if isinstance(item, items.LabelsMixIn):
+ if item.getXLabel() is not None:
+ xLabel = item.getXLabel()
+ if item.getYLabel() is not None:
+ if (isinstance(item, items.YAxisMixIn) and
+ item.getYAxis() == 'right'):
+ yRightLabel = item.getYLabel()
+ else:
+ yLabel = item.getYLabel()
+
+ # Start listening new active item
+ item.sigItemChanged.connect(self._activeItemChanged)
+
+ # Store current labels and update plot
+ self._xAxis._setCurrentLabel(xLabel)
+ self._yAxis._setCurrentLabel(yLabel)
+ self._yRightAxis._setCurrentLabel(yRightLabel)
+
+ self._setDirtyPlot()
+
+ activeLegend = self._activeLegend[kind]
+ if oldActiveItem is not None or activeLegend is not None:
+ if oldActiveItem is None:
+ oldActiveLegend = None
+ else:
+ oldActiveLegend = oldActiveItem.getLegend()
+ self.notify(
+ 'active' + kind[0].upper() + kind[1:] + 'Changed',
+ updated=oldActiveLegend != activeLegend,
+ previous=oldActiveLegend,
+ legend=activeLegend)
+
+ return activeLegend
+
+ def _activeItemChanged(self, type_):
+ """Listen for active item changed signal and broadcast signal
+
+ :param item.ItemChangedType type_: The type of item change
+ """
+ if not self.__muteActiveItemChanged:
+ item = self.sender()
+ if item is not None:
+ legend, kind = self._itemKey(item)
+ self.notify(
+ 'active' + kind[0].upper() + kind[1:] + 'Changed',
+ updated=False,
+ previous=legend,
+ legend=legend)
+
+ # Getters
+
+ def getAllCurves(self, just_legend=False, withhidden=False):
+ """Returns all curves legend or info and data.
+
+ It returns an empty list in case of not having any curve.
+
+ If just_legend is False, it returns a list of :class:`items.Curve`
+ objects describing the curves.
+ If just_legend is True, it returns a list of curves' legend.
+
+ :param bool just_legend: True to get the legend of the curves,
+ False (the default) to get the curves' data
+ and info.
+ :param bool withhidden: False (default) to skip hidden curves.
+ :return: list of curves' legend or :class:`.items.Curve`
+ :rtype: list of str or list of :class:`.items.Curve`
+ """
+ return self._getItems(kind='curve',
+ just_legend=just_legend,
+ withhidden=withhidden)
+
+ def getCurve(self, legend=None):
+ """Get the object describing a specific curve.
+
+ It returns None in case no matching curve is found.
+
+ :param str legend:
+ The legend identifying the curve.
+ If not provided or None (the default), the active curve is returned
+ or if there is no active curve, the latest updated curve that is
+ not hidden is returned if there are curves in the plot.
+ :return: None or :class:`.items.Curve` object
+ """
+ return self._getItem(kind='curve', legend=legend)
+
+ def getAllImages(self, just_legend=False):
+ """Returns all images legend or objects.
+
+ It returns an empty list in case of not having any image.
+
+ If just_legend is False, it returns a list of :class:`items.ImageBase`
+ objects describing the images.
+ If just_legend is True, it returns a list of legends.
+
+ :param bool just_legend: True to get the legend of the images,
+ False (the default) to get the images'
+ object.
+ :return: list of images' legend or :class:`.items.ImageBase`
+ :rtype: list of str or list of :class:`.items.ImageBase`
+ """
+ return self._getItems(kind='image',
+ just_legend=just_legend,
+ withhidden=True)
+
+ def getImage(self, legend=None):
+ """Get the object describing a specific image.
+
+ It returns None in case no matching image is found.
+
+ :param str legend:
+ The legend identifying the image.
+ If not provided or None (the default), the active image is returned
+ or if there is no active image, the latest updated image
+ is returned if there are images in the plot.
+ :return: None or :class:`.items.ImageBase` object
+ """
+ return self._getItem(kind='image', legend=legend)
+
+ def getScatter(self, legend=None):
+ """Get the object describing a specific scatter.
+
+ It returns None in case no matching scatter is found.
+
+ :param str legend:
+ The legend identifying the scatter.
+ If not provided or None (the default), the active scatter is
+ returned or if there is no active scatter, the latest updated
+ scatter is returned if there are scatters in the plot.
+ :return: None or :class:`.items.Scatter` object
+ """
+ return self._getItem(kind='scatter', legend=legend)
+
+ def getHistogram(self, legend=None):
+ """Get the object describing a specific histogram.
+
+ It returns None in case no matching histogram is found.
+
+ :param str legend:
+ The legend identifying the histogram.
+ If not provided or None (the default), the latest updated scatter
+ is returned if there are histograms in the plot.
+ :return: None or :class:`.items.Histogram` object
+ """
+ return self._getItem(kind='histogram', legend=legend)
+
+ def _getItems(self, kind=ITEM_KINDS, just_legend=False, withhidden=False):
+ """Retrieve all items of a kind in the plot
+
+ :param kind: The kind of elements to retrieve from the plot.
+ See :attr:`ITEM_KINDS`.
+ By default, it removes all kind of elements.
+ :type kind: str or tuple of str to specify multiple kinds.
+ :param str kind: Type of item: 'curve' or 'image'
+ :param bool just_legend: True to get the legend of the curves,
+ False (the default) to get the curves' data
+ and info.
+ :param bool withhidden: False (default) to skip hidden curves.
+ :return: list of legends or item objects
+ """
+ if kind is 'all': # Replace all by tuple of all kinds
+ kind = self.ITEM_KINDS
+
+ if kind in self.ITEM_KINDS: # Kind is a str, make it a tuple
+ kind = (kind,)
+
+ for aKind in kind:
+ assert aKind in self.ITEM_KINDS
+
+ output = []
+ for (legend, type_), item in self._content.items():
+ if type_ in kind and (withhidden or item.isVisible()):
+ output.append(legend if just_legend else item)
+ return output
+
+ def _getItem(self, kind, legend=None):
+ """Get an item from the plot: either an image or a curve.
+
+ Returns None if no match found.
+
+ :param str kind: Type of item to retrieve,
+ see :attr:`ITEM_KINDS`.
+ :param str legend: Legend of the item or
+ None to get active or last item
+ :return: Object describing the item or None
+ """
+ assert kind in self.ITEM_KINDS
+
+ if legend is not None:
+ return self._content.get((legend, kind), None)
+ else:
+ if kind in self._ACTIVE_ITEM_KINDS:
+ item = self._getActiveItem(kind=kind)
+ if item is not None: # Return active item if available
+ return item
+ # Return last visible item if any
+ allItems = self._getItems(
+ kind=kind, just_legend=False, withhidden=False)
+ return allItems[-1] if allItems else None
+
+ # Limits
+
+ def _notifyLimitsChanged(self, emitSignal=True):
+ """Send an event when plot area limits are changed."""
+ xRange = self._xAxis.getLimits()
+ yRange = self._yAxis.getLimits()
+ y2Range = self._yRightAxis.getLimits()
+ if emitSignal:
+ axes = self.getXAxis(), self.getYAxis(), self.getYAxis(axis="right")
+ ranges = xRange, yRange, y2Range
+ for axis, limits in zip(axes, ranges):
+ axis.sigLimitsChanged.emit(*limits)
+ event = PlotEvents.prepareLimitsChangedSignal(
+ id(self.getWidgetHandle()), xRange, yRange, y2Range)
+ self.notify(**event)
+
+ def getLimitsHistory(self):
+ """Returns the object handling the history of limits of the plot"""
+ return self._limitsHistory
+
+ def getGraphXLimits(self):
+ """Get the graph X (bottom) limits.
+
+ :return: Minimum and maximum values of the X axis
+ """
+ return self._backend.getGraphXLimits()
+
+ def setGraphXLimits(self, xmin, xmax, replot=None):
+ """Set the graph X (bottom) limits.
+
+ :param float xmin: minimum bottom axis value
+ :param float xmax: maximum bottom axis value
+ """
+ if replot is not None:
+ _logger.warning('setGraphXLimits deprecated replot parameter')
+ self._xAxis.setLimits(xmin, xmax)
+
+ def getGraphYLimits(self, axis='left'):
+ """Get the graph Y limits.
+
+ :param str axis: The axis for which to get the limits:
+ Either 'left' or 'right'
+ :return: Minimum and maximum values of the X axis
+ """
+ assert axis in ('left', 'right')
+ yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ return yAxis.getLimits()
+
+ def setGraphYLimits(self, ymin, ymax, axis='left', replot=None):
+ """Set the graph Y limits.
+
+ :param float ymin: minimum bottom axis value
+ :param float ymax: maximum bottom axis value
+ :param str axis: The axis for which to get the limits:
+ Either 'left' or 'right'
+ """
+ if replot is not None:
+ _logger.warning('setGraphYLimits deprecated replot parameter')
+ assert axis in ('left', 'right')
+ yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ return yAxis.setLimits(ymin, ymax)
+
+ def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
+ """Set the limits of the X and Y axes at once.
+
+ If y2min or y2max is None, the right Y axis limits are not updated.
+
+ :param float xmin: minimum bottom axis value
+ :param float xmax: maximum bottom axis value
+ :param float ymin: minimum left axis value
+ :param float ymax: maximum left axis value
+ :param float y2min: minimum right axis value or None (the default)
+ :param float y2max: maximum right axis value or None (the default)
+ """
+ # Deal with incorrect values
+ axis = self.getXAxis()
+ xmin, xmax = axis._checkLimits(xmin, xmax)
+ axis = self.getYAxis()
+ ymin, ymax = axis._checkLimits(ymin, ymax)
+
+ if y2min is None or y2max is None:
+ # if one limit is None, both are ignored
+ y2min, y2max = None, None
+ else:
+ axis = self.getYAxis(axis="right")
+ y2min, y2max = axis._checkLimits(y2min, y2max)
+
+ if self._viewConstrains:
+ view = self._viewConstrains.normalize(xmin, xmax, ymin, ymax)
+ xmin, xmax, ymin, ymax = view
+
+ self._backend.setLimits(xmin, xmax, ymin, ymax, y2min, y2max)
+ self._setDirtyPlot()
+ self._notifyLimitsChanged()
+
+ def _getViewConstraints(self):
+ """Return the plot object managing constaints on the plot view.
+
+ :rtype: ViewConstraints
+ """
+ if self._viewConstrains is None:
+ self._viewConstrains = ViewConstraints()
+ return self._viewConstrains
+
+ # Title and labels
+
+ def getGraphTitle(self):
+ """Return the plot main title as a str."""
+ return self._graphTitle
+
+ def setGraphTitle(self, title=""):
+ """Set the plot main title.
+
+ :param str title: Main title of the plot (default: '')
+ """
+ self._graphTitle = str(title)
+ self._backend.setGraphTitle(title)
+ self._setDirtyPlot()
+
+ def getGraphXLabel(self):
+ """Return the current X axis label as a str."""
+ return self._xAxis.getLabel()
+
+ def setGraphXLabel(self, label="X"):
+ """Set the plot X axis label.
+
+ The provided label can be temporarily replaced by the X label of the
+ active curve if any.
+
+ :param str label: The X axis label (default: 'X')
+ """
+ self._xAxis.setLabel(label)
+
+ def getGraphYLabel(self, axis='left'):
+ """Return the current Y axis label as a str.
+
+ :param str axis: The Y axis for which to get the label (left or right)
+ """
+ assert axis in ('left', 'right')
+ yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ return yAxis.getLabel()
+
+ def setGraphYLabel(self, label="Y", axis='left'):
+ """Set the plot Y axis label.
+
+ The provided label can be temporarily replaced by the Y label of the
+ active curve if any.
+
+ :param str label: The Y axis label (default: 'Y')
+ :param str axis: The Y axis for which to set the label (left or right)
+ """
+ assert axis in ('left', 'right')
+ yAxis = self._yAxis if axis == 'left' else self._yRightAxis
+ return yAxis.setLabel(label)
+
+ # Axes
+
+ def getXAxis(self):
+ """Returns the X axis
+
+ .. versionadded:: 0.6
+
+ :rtype: :class:`.items.Axis`
+ """
+ return self._xAxis
+
+ def getYAxis(self, axis="left"):
+ """Returns an Y axis
+
+ .. versionadded:: 0.6
+
+ :param str axis: The Y axis to return
+ ('left' or 'right').
+ :rtype: :class:`.items.Axis`
+ """
+ assert(axis in ["left", "right"])
+ return self._yAxis if axis == "left" else self._yRightAxis
+
+ def setAxesDisplayed(self, displayed):
+ """Display or not the axes.
+
+ :param bool displayed: If `True` axes are displayed. If `False` axes
+ are not anymore visible and the margin used for them is removed.
+ """
+ self._backend.setAxesDisplayed(displayed)
+ self._setDirtyPlot()
+ self._sigAxesVisibilityChanged.emit(displayed)
+
+ def _isAxesDisplayed(self):
+ return self._backend.isAxesDisplayed()
+
+ @property
+ @deprecated(since_version='0.6')
+ def sigSetYAxisInverted(self):
+ """Signal emitted when Y axis orientation has changed"""
+ return self._yAxis.sigInvertedChanged
+
+ @property
+ @deprecated(since_version='0.6')
+ def sigSetXAxisLogarithmic(self):
+ """Signal emitted when X axis scale has changed"""
+ return self._xAxis._sigLogarithmicChanged
+
+ @property
+ @deprecated(since_version='0.6')
+ def sigSetYAxisLogarithmic(self):
+ """Signal emitted when Y axis scale has changed"""
+ return self._yAxis._sigLogarithmicChanged
+
+ @property
+ @deprecated(since_version='0.6')
+ def sigSetXAxisAutoScale(self):
+ """Signal emitted when X axis autoscale has changed"""
+ return self._xAxis.sigAutoScaleChanged
+
+ @property
+ @deprecated(since_version='0.6')
+ def sigSetYAxisAutoScale(self):
+ """Signal emitted when Y axis autoscale has changed"""
+ return self._yAxis.sigAutoScaleChanged
+
+ def setYAxisInverted(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
+ """
+ self._yAxis.setInverted(flag)
+
+ def isYAxisInverted(self):
+ """Return True if Y axis goes from top to bottom, False otherwise."""
+ return self._yAxis.isInverted()
+
+ def isXAxisLogarithmic(self):
+ """Return True if X axis scale is logarithmic, False if linear."""
+ return self._xAxis._isLogarithmic()
+
+ def setXAxisLogarithmic(self, flag):
+ """Set the bottom X axis scale (either linear or logarithmic).
+
+ :param bool flag: True to use a logarithmic scale, False for linear.
+ """
+ self._xAxis._setLogarithmic(flag)
+
+ def isYAxisLogarithmic(self):
+ """Return True if Y axis scale is logarithmic, False if linear."""
+ return self._yAxis._isLogarithmic()
+
+ def setYAxisLogarithmic(self, flag):
+ """Set the Y axes scale (either linear or logarithmic).
+
+ :param bool flag: True to use a logarithmic scale, False for linear.
+ """
+ self._yAxis._setLogarithmic(flag)
+
+ def isXAxisAutoScale(self):
+ """Return True if X axis is automatically adjusting its limits."""
+ return self._xAxis.isAutoScale()
+
+ def setXAxisAutoScale(self, flag=True):
+ """Set the X axis limits adjusting behavior of :meth:`resetZoom`.
+
+ :param bool flag: True to resize limits automatically,
+ False to disable it.
+ """
+ self._xAxis.setAutoScale(flag)
+
+ def isYAxisAutoScale(self):
+ """Return True if Y axes are automatically adjusting its limits."""
+ return self._yAxis.isAutoScale()
+
+ def setYAxisAutoScale(self, flag=True):
+ """Set the Y axis limits adjusting behavior of :meth:`resetZoom`.
+
+ :param bool flag: True to resize limits automatically,
+ False to disable it.
+ """
+ self._yAxis.setAutoScale(flag)
+
+ def isKeepDataAspectRatio(self):
+ """Returns whether the plot is keeping data aspect ratio or not."""
+ return self._backend.isKeepDataAspectRatio()
+
+ def setKeepDataAspectRatio(self, flag=True):
+ """Set whether the plot keeps data aspect ratio or not.
+
+ :param bool flag: True to respect data aspect ratio
+ """
+ flag = bool(flag)
+ self._backend.setKeepDataAspectRatio(flag=flag)
+ self._setDirtyPlot()
+ self.resetZoom()
+ self.notify('setKeepDataAspectRatio', state=flag)
+
+ def getGraphGrid(self):
+ """Return the current grid mode, either None, 'major' or 'both'.
+
+ See :meth:`setGraphGrid`.
+ """
+ return self._grid
+
+ def setGraphGrid(self, which=True):
+ """Set the type of grid to display.
+
+ :param which: None or False to disable the grid,
+ 'major' or True for grid on major ticks (the default),
+ 'both' for grid on both major and minor ticks.
+ :type which: str of bool
+ """
+ assert which in (None, True, False, 'both', 'major')
+ if not which:
+ which = None
+ elif which is True:
+ which = 'major'
+ self._grid = which
+ self._backend.setGraphGrid(which)
+ self._setDirtyPlot()
+ self.notify('setGraphGrid', which=str(which))
+
+ # Defaults
+
+ def isDefaultPlotPoints(self):
+ """Return True if default Curve symbol is 'o', False for no symbol."""
+ return self._defaultPlotPoints == 'o'
+
+ def setDefaultPlotPoints(self, flag):
+ """Set the default symbol of all curves.
+
+ When called, this reset the symbol of all existing curves.
+
+ :param bool flag: True to use 'o' as the default curve symbol,
+ False to use no symbol.
+ """
+ self._defaultPlotPoints = 'o' if flag else ''
+
+ # Reset symbol of all curves
+ curves = self.getAllCurves(just_legend=False, withhidden=True)
+
+ if curves:
+ for curve in curves:
+ curve.setSymbol(self._defaultPlotPoints)
+
+ def isDefaultPlotLines(self):
+ """Return True for line as default line style, False for no line."""
+ return self._plotLines
+
+ def setDefaultPlotLines(self, flag):
+ """Toggle the use of lines as the default curve line style.
+
+ :param bool flag: True to use a line as the default line style,
+ False to use no line as the default line style.
+ """
+ self._plotLines = bool(flag)
+
+ linestyle = '-' if self._plotLines else ' '
+
+ # Reset linestyle of all curves
+ curves = self.getAllCurves(withhidden=True)
+
+ if curves:
+ for curve in curves:
+ curve.setLineStyle(linestyle)
+
+ def getDefaultColormap(self):
+ """Return the default :class:`.Colormap` used by :meth:`addImage`.
+
+ """
+ return self._defaultColormap
+
+ def setDefaultColormap(self, colormap=None):
+ """Set the default colormap used by :meth:`addImage`.
+
+ Setting the default colormap do not change any currently displayed
+ image.
+ It only affects future calls to :meth:`addImage` without the colormap
+ parameter.
+
+ :param Colormap colormap: The description of the default colormap, or
+ None to set the :class:`.Colormap` to a linear
+ autoscale gray colormap.
+ """
+ if colormap is None:
+ colormap = Colormap(name='gray',
+ normalization='linear',
+ vmin=None,
+ vmax=None)
+ if isinstance(colormap, dict):
+ self._defaultColormap = Colormap._fromDict(colormap)
+ else:
+ assert isinstance(colormap, Colormap)
+ self._defaultColormap = colormap
+ self.notify('defaultColormapChanged')
+
+ @staticmethod
+ def getSupportedColormaps():
+ """Get the supported colormap names as a tuple of str.
+
+ The list contains at least:
+ ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue',
+ 'magma', 'inferno', 'plasma', 'viridis')
+ """
+ return Colormap.getSupportedColormaps()
+
+ def _getColorAndStyle(self):
+ color = self.colorList[self._colorIndex]
+ style = self._styleList[self._styleIndex]
+
+ # Loop over color and then styles
+ self._colorIndex += 1
+ if self._colorIndex >= len(self.colorList):
+ self._colorIndex = 0
+ self._styleIndex = (self._styleIndex + 1) % len(self._styleList)
+
+ # If color is the one of active curve, take the next one
+ if color == self.getActiveCurveColor():
+ color, style = self._getColorAndStyle()
+
+ if not self._plotLines:
+ style = ' '
+
+ return color, style
+
+ # Misc.
+
+ def getWidgetHandle(self):
+ """Return the widget the plot is displayed in.
+
+ This widget is owned by the backend.
+ """
+ return self._backend.getWidgetHandle()
+
def notify(self, event, **kwargs):
- """Override :meth:`Plot.notify` to send Qt signals."""
+ """Send an event to the listeners and send signals.
+
+ Event are passed to the registered callback as a dict with an 'event'
+ key for backward compatibility with PyMca.
+
+ :param str event: The type of event
+ :param kwargs: The information of the event.
+ """
eventDict = kwargs.copy()
eventDict['event'] = event
self.sigPlotSignal.emit(eventDict)
- if event == 'setYAxisInverted':
- self.sigSetYAxisInverted.emit(kwargs['state'])
- elif event == 'setXAxisLogarithmic':
- self.sigSetXAxisLogarithmic.emit(kwargs['state'])
- elif event == 'setYAxisLogarithmic':
- self.sigSetYAxisLogarithmic.emit(kwargs['state'])
- elif event == 'setXAxisAutoScale':
- self.sigSetXAxisAutoScale.emit(kwargs['state'])
- elif event == 'setYAxisAutoScale':
- self.sigSetYAxisAutoScale.emit(kwargs['state'])
- elif event == 'setKeepDataAspectRatio':
+ if event == 'setKeepDataAspectRatio':
self.sigSetKeepDataAspectRatio.emit(kwargs['state'])
elif event == 'setGraphGrid':
self.sigSetGraphGrid.emit(kwargs['which'])
@@ -201,7 +2492,494 @@ class PlotWidget(qt.QMainWindow, Plot.Plot):
kwargs['previous'], kwargs['legend'])
elif event == 'interactiveModeChanged':
self.sigInteractiveModeChanged.emit(kwargs['source'])
- Plot.Plot.notify(self, event, **kwargs)
+
+ eventDict = kwargs.copy()
+ eventDict['event'] = event
+ self._callback(eventDict)
+
+ def setCallback(self, callbackFunction=None):
+ """Attach a listener to the backend.
+
+ Limitation: Only one listener at a time.
+
+ :param callbackFunction: function accepting a dictionary as input
+ to handle the graph events
+ If None (default), use a default listener.
+ """
+ # TODO allow multiple listeners, keep a weakref on it
+ # allow register listener by event type
+ if callbackFunction is None:
+ callbackFunction = self.graphCallback
+ self._callback = callbackFunction
+
+ def graphCallback(self, ddict=None):
+ """This callback is going to receive all the events from the plot.
+
+ Those events will consist on a dictionary and among the dictionary
+ keys the key 'event' is mandatory to describe the type of event.
+ This default implementation only handles setting the active curve.
+ """
+
+ if ddict is None:
+ ddict = {}
+ _logger.debug("Received dict keys = %s", str(ddict.keys()))
+ _logger.debug(str(ddict))
+ if ddict['event'] in ["legendClicked", "curveClicked"]:
+ if ddict['button'] == "left":
+ self.setActiveCurve(ddict['label'])
+
+ def saveGraph(self, filename, fileFormat=None, dpi=None, **kw):
+ """Save a snapshot of the plot.
+
+ Supported file formats depends on the backend in use.
+ The following file formats are always supported: "png", "svg".
+ The matplotlib backend supports more formats:
+ "pdf", "ps", "eps", "tiff", "jpeg", "jpg".
+
+ :param filename: Destination
+ :type filename: str, StringIO or BytesIO
+ :param str fileFormat: String specifying the format
+ :return: False if cannot save the plot, True otherwise
+ """
+ if kw:
+ _logger.warning('Extra parameters ignored: %s', str(kw))
+
+ if fileFormat is None:
+ if not hasattr(filename, 'lower'):
+ _logger.warning(
+ 'saveGraph cancelled, cannot define file format.')
+ return False
+ else:
+ fileFormat = (filename.split(".")[-1]).lower()
+
+ supportedFormats = ("png", "svg", "pdf", "ps", "eps",
+ "tif", "tiff", "jpeg", "jpg")
+
+ if fileFormat not in supportedFormats:
+ _logger.warning('Unsupported format %s', fileFormat)
+ return False
+ else:
+ self._backend.saveGraph(filename,
+ fileFormat=fileFormat,
+ dpi=dpi)
+ return True
+
+ def getDataMargins(self):
+ """Get the default data margin ratios, see :meth:`setDataMargins`.
+
+ :return: The margin ratios for each side (xMin, xMax, yMin, yMax).
+ :rtype: A 4-tuple of floats.
+ """
+ return self._defaultDataMargins
+
+ def setDataMargins(self, xMinMargin=0., xMaxMargin=0.,
+ yMinMargin=0., yMaxMargin=0.):
+ """Set the default data margins to use in :meth:`resetZoom`.
+
+ Set the default ratios of margins (as floats) to add around the data
+ inside the plot area for each side.
+ """
+ self._defaultDataMargins = (xMinMargin, xMaxMargin,
+ yMinMargin, yMaxMargin)
+
+ def getAutoReplot(self):
+ """Return True if replot is automatically handled, False otherwise.
+
+ See :meth`setAutoReplot`.
+ """
+ return self._autoreplot
+
+ def setAutoReplot(self, autoreplot=True):
+ """Set automatic replot mode.
+
+ When enabled, the plot is redrawn automatically when changed.
+ When disabled, the plot is not redrawn when its content change.
+ Instead, it :meth:`replot` must be called.
+
+ :param bool autoreplot: True to enable it (default),
+ False to disable it.
+ """
+ self._autoreplot = bool(autoreplot)
+
+ # If the plot is dirty before enabling autoreplot,
+ # then _backend.postRedisplay will never be called from _setDirtyPlot
+ if self._autoreplot and self._getDirtyPlot():
+ self._backend.postRedisplay()
+
+ def replot(self):
+ """Redraw the plot immediately."""
+ for item in self._contentToUpdate:
+ item._update(self._backend)
+ self._contentToUpdate = []
+ self._backend.replot()
+ self._dirty = False # reset dirty flag
+
+ def resetZoom(self, dataMargins=None):
+ """Reset the plot limits to the bounds of the data and redraw the plot.
+
+ It automatically scale limits of axes that are in autoscale mode
+ (see :meth:`getXAxis`, :meth:`getYAxis` and :meth:`Axis.setAutoScale`).
+ It keeps current limits on axes that are not in autoscale mode.
+
+ Extra margins can be added around the data inside the plot area
+ (see :meth:`setDataMargins`).
+ Margins are given as one ratio of the data range per limit of the
+ data (xMin, xMax, yMin and yMax limits).
+ For log scale, extra margins are applied in log10 of the data.
+
+ :param dataMargins: Ratios of margins to add around the data inside
+ the plot area for each side (default: no margins).
+ :type dataMargins: A 4-tuple of float as (xMin, xMax, yMin, yMax).
+ """
+ if dataMargins is None:
+ dataMargins = self._defaultDataMargins
+
+ xLimits = self._xAxis.getLimits()
+ yLimits = self._yAxis.getLimits()
+ y2Limits = self._yRightAxis.getLimits()
+
+ xAuto = self._xAxis.isAutoScale()
+ yAuto = self._yAxis.isAutoScale()
+
+ if not xAuto and not yAuto:
+ _logger.debug("Nothing to autoscale")
+ else: # Some axes to autoscale
+
+ # Get data range
+ ranges = self.getDataRange()
+ xmin, xmax = (1., 100.) if ranges.x is None else ranges.x
+ ymin, ymax = (1., 100.) if ranges.y is None else ranges.y
+ if ranges.yright is None:
+ ymin2, ymax2 = None, None
+ else:
+ ymin2, ymax2 = ranges.yright
+
+ # Add margins around data inside the plot area
+ newLimits = list(_utils.addMarginsToLimits(
+ dataMargins,
+ self._xAxis._isLogarithmic(),
+ self._yAxis._isLogarithmic(),
+ xmin, xmax, ymin, ymax, ymin2, ymax2))
+
+ if self.isKeepDataAspectRatio():
+ # Use limits with margins to keep ratio
+ xmin, xmax, ymin, ymax = newLimits[:4]
+
+ # Compute bbox wth figure aspect ratio
+ plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
+ plotRatio = plotHeight / plotWidth
+
+ if plotRatio > 0.:
+ dataRatio = (ymax - ymin) / (xmax - xmin)
+ if dataRatio < plotRatio:
+ # Increase y range
+ ycenter = 0.5 * (ymax + ymin)
+ yrange = (xmax - xmin) * plotRatio
+ newLimits[2] = ycenter - 0.5 * yrange
+ newLimits[3] = ycenter + 0.5 * yrange
+
+ elif dataRatio > plotRatio:
+ # Increase x range
+ xcenter = 0.5 * (xmax + xmin)
+ xrange_ = (ymax - ymin) / plotRatio
+ newLimits[0] = xcenter - 0.5 * xrange_
+ newLimits[1] = xcenter + 0.5 * xrange_
+
+ self.setLimits(*newLimits)
+
+ if not xAuto and yAuto:
+ self.setGraphXLimits(*xLimits)
+ elif xAuto and not yAuto:
+ if y2Limits is not None:
+ self.setGraphYLimits(
+ y2Limits[0], y2Limits[1], axis='right')
+ if yLimits is not None:
+ self.setGraphYLimits(yLimits[0], yLimits[1], axis='left')
+
+ self._setDirtyPlot()
+
+ if (xLimits != self._xAxis.getLimits() or
+ yLimits != self._yAxis.getLimits() or
+ y2Limits != self._yRightAxis.getLimits()):
+ self._notifyLimitsChanged()
+
+ # Coord conversion
+
+ def dataToPixel(self, x=None, y=None, axis="left", check=True):
+ """Convert a position in data coordinates to a position in pixels.
+
+ :param float x: The X coordinate in data space. If None (default)
+ the middle position of the displayed data is used.
+ :param float y: The Y coordinate in data space. If None (default)
+ the middle position of the displayed data is used.
+ :param str axis: The Y axis to use for the conversion
+ ('left' or 'right').
+ :param bool check: True to return None if outside displayed area,
+ False to convert to pixels anyway
+ :returns: The corresponding position in pixels or
+ None if the data position is not in the displayed area and
+ check is True.
+ :rtype: A tuple of 2 floats: (xPixel, yPixel) or None.
+ """
+ assert axis in ("left", "right")
+
+ xmin, xmax = self._xAxis.getLimits()
+ yAxis = self.getYAxis(axis=axis)
+ ymin, ymax = yAxis.getLimits()
+
+ if x is None:
+ x = 0.5 * (xmax + xmin)
+ if y is None:
+ y = 0.5 * (ymax + ymin)
+
+ if check:
+ if x > xmax or x < xmin:
+ return None
+
+ if y > ymax or y < ymin:
+ return None
+
+ return self._backend.dataToPixel(x, y, axis=axis)
+
+ def pixelToData(self, x, y, axis="left", check=False):
+ """Convert a position in pixels to a position in data coordinates.
+
+ :param float x: The X coordinate in pixels. If None (default)
+ the center of the widget is used.
+ :param float y: The Y coordinate in pixels. If None (default)
+ the center of the widget is used.
+ :param str axis: The Y axis to use for the conversion
+ ('left' or 'right').
+ :param bool check: Toggle checking if pixel is in plot area.
+ If False, this method never returns None.
+ :returns: The corresponding position in data space or
+ None if the pixel position is not in the plot area.
+ :rtype: A tuple of 2 floats: (xData, yData) or None.
+ """
+ assert axis in ("left", "right")
+ return self._backend.pixelToData(x, y, axis=axis, check=check)
+
+ def getPlotBoundsInPixels(self):
+ """Plot area bounds in widget coordinates in pixels.
+
+ :return: bounds as a 4-tuple of int: (left, top, width, height)
+ """
+ return self._backend.getPlotBoundsInPixels()
+
+ # Interaction support
+
+ def setGraphCursorShape(self, cursor=None):
+ """Set the cursor shape.
+
+ :param str cursor: Name of the cursor shape
+ """
+ self._backend.setGraphCursorShape(cursor)
+
+ def _pickMarker(self, x, y, test=None):
+ """Pick a marker at the given position.
+
+ To use for interaction implementation.
+
+ :param float x: X position in pixels.
+ :param float y: Y position in pixels.
+ :param test: A callable to call for each picked marker to filter
+ picked markers. If None (default), do not filter markers.
+ """
+ if test is None:
+ def test(mark):
+ return True
+
+ markers = self._backend.pickItems(x, y)
+ legends = [m['legend'] for m in markers if m['kind'] == 'marker']
+
+ for legend in reversed(legends):
+ marker = self._getMarker(legend)
+ if marker is not None and test(marker):
+ return marker
+ return None
+
+ def _getAllMarkers(self, just_legend=False):
+ """Returns all markers' legend or objects
+
+ :param bool just_legend: True to get the legend of the markers,
+ False (the default) to get marker objects.
+ :return: list of legend of list of marker objects
+ :rtype: list of str or list of marker objects
+ """
+ return self._getItems(
+ kind='marker', just_legend=just_legend, withhidden=True)
+
+ def _getMarker(self, legend=None):
+ """Get the object describing a specific marker.
+
+ It returns None in case no matching marker is found
+
+ :param str legend: The legend of the marker to retrieve
+ :rtype: None of marker object
+ """
+ return self._getItem(kind='marker', legend=legend)
+
+ def _pickImageOrCurve(self, x, y, test=None):
+ """Pick an image or a curve at the given position.
+
+ To use for interaction implementation.
+
+ :param float x: X position in pixelsparam float y: Y position in pixels
+ :param test: A callable to call for each picked item to filter
+ picked items. If None (default), do not filter items.
+ """
+ if test is None:
+ def test(i):
+ return True
+
+ allItems = self._backend.pickItems(x, y)
+ allItems = [item for item in allItems
+ if item['kind'] in ['curve', 'image']]
+
+ for item in reversed(allItems):
+ kind, legend = item['kind'], item['legend']
+ if kind == 'curve':
+ curve = self.getCurve(legend)
+ if curve is not None and test(curve):
+ return kind, curve, item['indices']
+
+ elif kind == 'image':
+ image = self.getImage(legend)
+ if image is not None and test(image):
+ return kind, image, None
+
+ else:
+ _logger.warning('Unsupported kind: %s', kind)
+
+ return None
+
+ # User event handling #
+
+ def _isPositionInPlotArea(self, x, y):
+ """Project position in pixel to the closest point in the plot area
+
+ :param float x: X coordinate in widget coordinate (in pixel)
+ :param float y: Y coordinate in widget coordinate (in pixel)
+ :return: (x, y) in widget coord (in pixel) in the plot area
+ """
+ left, top, width, height = self.getPlotBoundsInPixels()
+ xPlot = numpy.clip(x, left, left + width)
+ yPlot = numpy.clip(y, top, top + height)
+ return xPlot, yPlot
+
+ def onMousePress(self, xPixel, yPixel, btn):
+ """Handle mouse press event.
+
+ :param float xPixel: X mouse position in pixels
+ :param float yPixel: Y mouse position in pixels
+ :param str btn: Mouse button in 'left', 'middle', 'right'
+ """
+ if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
+ self._pressedButtons.append(btn)
+ self._eventHandler.handleEvent('press', xPixel, yPixel, btn)
+
+ def onMouseMove(self, xPixel, yPixel):
+ """Handle mouse move event.
+
+ :param float xPixel: X mouse position in pixels
+ :param float yPixel: Y mouse position in pixels
+ """
+ inXPixel, inYPixel = self._isPositionInPlotArea(xPixel, yPixel)
+ isCursorInPlot = inXPixel == xPixel and inYPixel == yPixel
+
+ if self._cursorInPlot != isCursorInPlot:
+ self._cursorInPlot = isCursorInPlot
+ self._eventHandler.handleEvent(
+ 'enter' if self._cursorInPlot else 'leave')
+
+ if isCursorInPlot:
+ # Signal mouse move event
+ dataPos = self.pixelToData(inXPixel, inYPixel)
+ assert dataPos is not None
+
+ btn = self._pressedButtons[-1] if self._pressedButtons else None
+ event = PlotEvents.prepareMouseSignal(
+ 'mouseMoved', btn, dataPos[0], dataPos[1], xPixel, yPixel)
+ self.notify(**event)
+
+ # Either button was pressed in the plot or cursor is in the plot
+ if isCursorInPlot or self._pressedButtons:
+ self._eventHandler.handleEvent('move', inXPixel, inYPixel)
+
+ def onMouseRelease(self, xPixel, yPixel, btn):
+ """Handle mouse release event.
+
+ :param float xPixel: X mouse position in pixels
+ :param float yPixel: Y mouse position in pixels
+ :param str btn: Mouse button in 'left', 'middle', 'right'
+ """
+ try:
+ self._pressedButtons.remove(btn)
+ except ValueError:
+ pass
+ else:
+ xPixel, yPixel = self._isPositionInPlotArea(xPixel, yPixel)
+ self._eventHandler.handleEvent('release', xPixel, yPixel, btn)
+
+ def onMouseWheel(self, xPixel, yPixel, angleInDegrees):
+ """Handle mouse wheel event.
+
+ :param float xPixel: X mouse position in pixels
+ :param float yPixel: Y mouse position in pixels
+ :param float angleInDegrees: Angle corresponding to wheel motion.
+ Positive for movement away from the user,
+ negative for movement toward the user.
+ """
+ if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
+ self._eventHandler.handleEvent(
+ 'wheel', xPixel, yPixel, angleInDegrees)
+
+ def onMouseLeaveWidget(self):
+ """Handle mouse leave widget event."""
+ if self._cursorInPlot:
+ self._cursorInPlot = False
+ self._eventHandler.handleEvent('leave')
+
+ # Interaction modes #
+
+ def getInteractiveMode(self):
+ """Returns the current interactive mode as a dict.
+
+ The returned dict contains at least the key 'mode'.
+ Mode can be: 'draw', 'pan', 'select', 'zoom'.
+ It can also contains extra keys (e.g., 'color') specific to a mode
+ as provided to :meth:`setInteractiveMode`.
+ """
+ return self._eventHandler.getInteractiveMode()
+
+ def setInteractiveMode(self, mode, color='black',
+ shape='polygon', label=None,
+ zoomOnWheel=True, source=None, width=None):
+ """Switch the interactive mode.
+
+ :param str mode: The name of the interactive mode.
+ In 'draw', 'pan', 'select', 'zoom'.
+ :param color: Only for 'draw' and 'zoom' modes.
+ Color to use for drawing selection area. Default black.
+ :type color: Color description: The name as a str or
+ a tuple of 4 floats.
+ :param str shape: Only for 'draw' mode. The kind of shape to draw.
+ In 'polygon', 'rectangle', 'line', 'vline', 'hline',
+ 'freeline'.
+ Default is 'polygon'.
+ :param str label: Only for 'draw' mode, sent in drawing events.
+ :param bool zoomOnWheel: Toggle zoom on wheel support
+ :param source: A user-defined object (typically the caller object)
+ that will be send in the interactiveModeChanged event,
+ to identify which object required a mode change.
+ Default: None
+ :param float width: Width of the pencil. Only for draw pencil mode.
+ """
+ self._eventHandler.setInteractiveMode(mode, color, shape, label, width)
+ self._eventHandler.zoomOnWheel = zoomOnWheel
+
+ self.notify(
+ 'interactiveModeChanged', source=source)
# Panning with arrow keys
@@ -265,3 +3043,149 @@ class PlotWidget(qt.QMainWindow, Plot.Plot):
# Only call base class implementation when key is not handled.
# See QWidget.keyPressEvent for details.
super(PlotWidget, self).keyPressEvent(event)
+
+ # Deprecated #
+
+ def isDrawModeEnabled(self):
+ """Deprecated, use :meth:`getInteractiveMode` instead.
+
+ Return True if the current interactive state is drawing."""
+ _logger.warning(
+ 'isDrawModeEnabled deprecated, use getInteractiveMode instead')
+ return self.getInteractiveMode()['mode'] == 'draw'
+
+ def setDrawModeEnabled(self, flag=True, shape='polygon', label=None,
+ color=None, **kwargs):
+ """Deprecated, use :meth:`setInteractiveMode` instead.
+
+ Set the drawing mode if flag is True and its parameters.
+
+ If flag is False, only item selection is enabled.
+
+ Warning: Zoom and drawing are not compatible and cannot be enabled
+ simultaneously.
+
+ :param bool flag: True to enable drawing and disable zoom and select.
+ :param str shape: Type of item to be drawn in:
+ hline, vline, rectangle, polygon (default)
+ :param str label: Associated text for identifying draw signals
+ :param color: The color to use to draw the selection area
+ :type color: string ("#RRGGBB") or 4 column unsigned byte array or
+ one of the predefined color names defined in Colors.py
+ """
+ _logger.warning(
+ 'setDrawModeEnabled deprecated, use setInteractiveMode instead')
+
+ if kwargs:
+ _logger.warning('setDrawModeEnabled ignores additional parameters')
+
+ if color is None:
+ color = 'black'
+
+ if flag:
+ self.setInteractiveMode('draw', shape=shape,
+ label=label, color=color)
+ elif self.getInteractiveMode()['mode'] == 'draw':
+ self.setInteractiveMode('select')
+
+ def getDrawMode(self):
+ """Deprecated, use :meth:`getInteractiveMode` instead.
+
+ Return the draw mode parameters as a dict of None.
+
+ It returns None if the interactive mode is not a drawing mode,
+ otherwise, it returns a dict containing the drawing mode parameters
+ as provided to :meth:`setDrawModeEnabled`.
+ """
+ _logger.warning(
+ 'getDrawMode deprecated, use getInteractiveMode instead')
+ mode = self.getInteractiveMode()
+ return mode if mode['mode'] == 'draw' else None
+
+ def isZoomModeEnabled(self):
+ """Deprecated, use :meth:`getInteractiveMode` instead.
+
+ Return True if the current interactive state is zooming."""
+ _logger.warning(
+ 'isZoomModeEnabled deprecated, use getInteractiveMode instead')
+ return self.getInteractiveMode()['mode'] == 'zoom'
+
+ def setZoomModeEnabled(self, flag=True, color=None):
+ """Deprecated, use :meth:`setInteractiveMode` instead.
+
+ Set the zoom mode if flag is True, else item selection is enabled.
+
+ Warning: Zoom and drawing are not compatible and cannot be enabled
+ simultaneously
+
+ :param bool flag: If True, enable zoom and select mode.
+ :param color: The color to use to draw the selection area.
+ (Default: 'black')
+ :param color: The color to use to draw the selection area
+ :type color: string ("#RRGGBB") or 4 column unsigned byte array or
+ one of the predefined color names defined in Colors.py
+ """
+ _logger.warning(
+ 'setZoomModeEnabled deprecated, use setInteractiveMode instead')
+ if color is None:
+ color = 'black'
+
+ if flag:
+ self.setInteractiveMode('zoom', color=color)
+ elif self.getInteractiveMode()['mode'] == 'zoom':
+ self.setInteractiveMode('select')
+
+ def insertMarker(self, *args, **kwargs):
+ """Deprecated, use :meth:`addMarker` instead."""
+ _logger.warning(
+ 'insertMarker deprecated, use addMarker instead.')
+ return self.addMarker(*args, **kwargs)
+
+ def insertXMarker(self, *args, **kwargs):
+ """Deprecated, use :meth:`addXMarker` instead."""
+ _logger.warning(
+ 'insertXMarker deprecated, use addXMarker instead.')
+ return self.addXMarker(*args, **kwargs)
+
+ def insertYMarker(self, *args, **kwargs):
+ """Deprecated, use :meth:`addYMarker` instead."""
+ _logger.warning(
+ 'insertYMarker deprecated, use addYMarker instead.')
+ return self.addYMarker(*args, **kwargs)
+
+ def isActiveCurveHandlingEnabled(self):
+ """Deprecated, use :meth:`isActiveCurveHandling` instead."""
+ _logger.warning(
+ 'isActiveCurveHandlingEnabled deprecated, '
+ 'use isActiveCurveHandling instead.')
+ return self.isActiveCurveHandling()
+
+ def enableActiveCurveHandling(self, *args, **kwargs):
+ """Deprecated, use :meth:`setActiveCurveHandling` instead."""
+ _logger.warning(
+ 'enableActiveCurveHandling deprecated, '
+ 'use setActiveCurveHandling instead.')
+ return self.setActiveCurveHandling(*args, **kwargs)
+
+ def invertYAxis(self, *args, **kwargs):
+ """Deprecated, use :meth:`Axis.setInverted` instead."""
+ _logger.warning('invertYAxis deprecated, '
+ 'use getYAxis().setInverted instead.')
+ return self.getYAxis().setInverted(*args, **kwargs)
+
+ def showGrid(self, flag=True):
+ """Deprecated, use :meth:`setGraphGrid` instead."""
+ _logger.warning("showGrid deprecated, use setGraphGrid instead")
+ if flag in (0, False):
+ flag = None
+ elif flag in (1, True):
+ flag = 'major'
+ else:
+ flag = 'both'
+ return self.setGraphGrid(flag)
+
+ def keepDataAspectRatio(self, *args, **kwargs):
+ """Deprecated, use :meth:`setKeepDataAspectRatio`."""
+ _logger.warning('keepDataAspectRatio deprecated,'
+ 'use setKeepDataAspectRatio instead')
+ return self.setKeepDataAspectRatio(*args, **kwargs)
diff --git a/silx/gui/plot/PlotWindow.py b/silx/gui/plot/PlotWindow.py
index ae25cfd..a23db04 100644
--- a/silx/gui/plot/PlotWindow.py
+++ b/silx/gui/plot/PlotWindow.py
@@ -25,26 +25,30 @@
"""A :class:`.PlotWidget` with additional toolbars.
The :class:`PlotWindow` is a subclass of :class:`.PlotWidget`.
-It provides the plot API fully defined in :class:`.Plot`.
"""
__authors__ = ["V.A. Sole", "T. Vincent"]
__license__ = "MIT"
-__date__ = "27/04/2017"
+__date__ = "17/08/2017"
import collections
import logging
-from silx.utils.decorators import deprecated
+from silx.utils.deprecation import deprecated
from . import PlotWidget
-from . import PlotActions
+from . import actions
+from . import items
+from .actions import medfilt as actions_medfilt
+from .actions import fit as actions_fit
+from .actions import histogram as actions_histogram
from . import PlotToolButtons
from .PlotTools import PositionInfo
from .Profile import ProfileToolBar
from .LegendSelector import LegendsDockWidget
from .CurvesROIWidget import CurvesROIDockWidget
from .MaskToolsWidget import MaskToolsDockWidget
+from .ColorBar import ColorBarWidget
try:
from ..console import IPythonDockWidget
except ImportError:
@@ -65,7 +69,7 @@ class PlotWindow(PlotWidget):
:param parent: The parent of this widget or None.
:param backend: The backend to use for the plot (default: matplotlib).
- See :class:`.Plot` for the list of supported backend.
+ See :class:`.PlotWidget` for the list of supported backend.
:type backend: str or :class:`BackendBase.BackendBase`
:param bool resetzoom: Toggle visibility of reset zoom action.
:param bool autoScale: Toggle visibility of axes autoscale actions.
@@ -113,46 +117,54 @@ class PlotWindow(PlotWidget):
self.group = qt.QActionGroup(self)
self.group.setExclusive(False)
- self.resetZoomAction = self.group.addAction(PlotActions.ResetZoomAction(self))
+ self.zoomModeAction = self.group.addAction(
+ actions.mode.ZoomModeAction(self))
+ self.panModeAction = self.group.addAction(
+ actions.mode.PanModeAction(self))
+
+ self.resetZoomAction = self.group.addAction(
+ actions.control.ResetZoomAction(self))
self.resetZoomAction.setVisible(resetzoom)
self.addAction(self.resetZoomAction)
- self.zoomInAction = PlotActions.ZoomInAction(self)
+ self.zoomInAction = actions.control.ZoomInAction(self)
self.addAction(self.zoomInAction)
- self.zoomOutAction = PlotActions.ZoomOutAction(self)
+ self.zoomOutAction = actions.control.ZoomOutAction(self)
self.addAction(self.zoomOutAction)
self.xAxisAutoScaleAction = self.group.addAction(
- PlotActions.XAxisAutoScaleAction(self))
+ actions.control.XAxisAutoScaleAction(self))
self.xAxisAutoScaleAction.setVisible(autoScale)
self.addAction(self.xAxisAutoScaleAction)
self.yAxisAutoScaleAction = self.group.addAction(
- PlotActions.YAxisAutoScaleAction(self))
+ actions.control.YAxisAutoScaleAction(self))
self.yAxisAutoScaleAction.setVisible(autoScale)
self.addAction(self.yAxisAutoScaleAction)
self.xAxisLogarithmicAction = self.group.addAction(
- PlotActions.XAxisLogarithmicAction(self))
+ actions.control.XAxisLogarithmicAction(self))
self.xAxisLogarithmicAction.setVisible(logScale)
self.addAction(self.xAxisLogarithmicAction)
self.yAxisLogarithmicAction = self.group.addAction(
- PlotActions.YAxisLogarithmicAction(self))
+ actions.control.YAxisLogarithmicAction(self))
self.yAxisLogarithmicAction.setVisible(logScale)
self.addAction(self.yAxisLogarithmicAction)
self.gridAction = self.group.addAction(
- PlotActions.GridAction(self, gridMode='both'))
+ actions.control.GridAction(self, gridMode='both'))
self.gridAction.setVisible(grid)
self.addAction(self.gridAction)
- self.curveStyleAction = self.group.addAction(PlotActions.CurveStyleAction(self))
+ self.curveStyleAction = self.group.addAction(
+ actions.control.CurveStyleAction(self))
self.curveStyleAction.setVisible(curveStyle)
self.addAction(self.curveStyleAction)
- self.colormapAction = self.group.addAction(PlotActions.ColormapAction(self))
+ self.colormapAction = self.group.addAction(
+ actions.control.ColormapAction(self))
self.colormapAction.setVisible(colormap)
self.addAction(self.colormapAction)
@@ -171,34 +183,34 @@ class PlotWindow(PlotWidget):
self.getMaskAction().setVisible(mask)
self._intensityHistoAction = self.group.addAction(
- PlotActions.PixelIntensitiesHistoAction(self))
+ actions_histogram.PixelIntensitiesHistoAction(self))
self._intensityHistoAction.setVisible(False)
self._medianFilter2DAction = self.group.addAction(
- PlotActions.MedianFilter2DAction(self))
+ actions_medfilt.MedianFilter2DAction(self))
self._medianFilter2DAction.setVisible(False)
self._medianFilter1DAction = self.group.addAction(
- PlotActions.MedianFilter1DAction(self))
+ actions_medfilt.MedianFilter1DAction(self))
self._medianFilter1DAction.setVisible(False)
self._separator = qt.QAction('separator', self)
self._separator.setSeparator(True)
self.group.addAction(self._separator)
- self.copyAction = self.group.addAction(PlotActions.CopyAction(self))
+ self.copyAction = self.group.addAction(actions.io.CopyAction(self))
self.copyAction.setVisible(copy)
self.addAction(self.copyAction)
- self.saveAction = self.group.addAction(PlotActions.SaveAction(self))
+ self.saveAction = self.group.addAction(actions.io.SaveAction(self))
self.saveAction.setVisible(save)
self.addAction(self.saveAction)
- self.printAction = self.group.addAction(PlotActions.PrintAction(self))
+ self.printAction = self.group.addAction(actions.io.PrintAction(self))
self.printAction.setVisible(print_)
self.addAction(self.printAction)
- self.fitAction = self.group.addAction(PlotActions.FitAction(self))
+ self.fitAction = self.group.addAction(actions_fit.FitAction(self))
self.fitAction.setVisible(fit)
self.addAction(self.fitAction)
@@ -207,6 +219,28 @@ class PlotWindow(PlotWidget):
self._panWithArrowKeysAction = None
self._crosshairAction = None
+ # Create color bar, hidden by default for backward compatibility
+ self._colorbar = ColorBarWidget(parent=self, plot=self)
+ self._colorbar.setVisible(False)
+
+ # Make colorbar background white
+ self._colorbar.setAutoFillBackground(True)
+ palette = self._colorbar.palette()
+ palette.setColor(qt.QPalette.Background, qt.Qt.white)
+ palette.setColor(qt.QPalette.Window, qt.Qt.white)
+ self._colorbar.setPalette(palette)
+
+ gridLayout = qt.QGridLayout()
+ gridLayout.setSpacing(0)
+ gridLayout.setContentsMargins(0, 0, 0, 0)
+ gridLayout.addWidget(self.getWidgetHandle(), 0, 0)
+ gridLayout.addWidget(self._colorbar, 0, 1)
+ gridLayout.setRowStretch(0, 1)
+ gridLayout.setColumnStretch(0, 1)
+ centralWidget = qt.QWidget()
+ centralWidget.setLayout(gridLayout)
+ self.setCentralWidget(centralWidget)
+
if control or position:
hbox = qt.QHBoxLayout()
hbox.setContentsMargins(0, 0, 0, 0)
@@ -239,16 +273,7 @@ class PlotWindow(PlotWidget):
bottomBar = qt.QWidget()
bottomBar.setLayout(hbox)
- layout = qt.QVBoxLayout()
- layout.setSpacing(0)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self.getWidgetHandle())
- layout.addWidget(bottomBar)
- layout.setStretch(0, 1)
-
- centralWidget = qt.QWidget()
- centralWidget.setLayout(layout)
- self.setCentralWidget(centralWidget)
+ gridLayout.addWidget(bottomBar, 1, 0, 1, -1)
# Creating the toolbar also create actions for toolbuttons
self._toolbar = self._createToolBar(title='Plot', parent=None)
@@ -322,6 +347,8 @@ class PlotWindow(PlotWidget):
self.yAxisInvertedAction = toolbar.addWidget(obj)
else:
raise RuntimeError()
+ if obj is self.panModeAction:
+ toolbar.addSeparator()
return toolbar
def toolBar(self):
@@ -378,6 +405,13 @@ class PlotWindow(PlotWidget):
self.tabifyDockWidget(self._dockWidgets[0],
dock_widget)
+ def getColorBarWidget(self):
+ """Returns the embedded :class:`ColorBarWidget` widget.
+
+ :rtype: ColorBarWidget
+ """
+ return self._colorbar
+
# getters for dock widgets
@property
@deprecated(replacement="getLegendsDockWidget()", since_version="0.4.0")
@@ -464,10 +498,10 @@ class PlotWindow(PlotWidget):
def getCrosshairAction(self):
"""Action toggling crosshair cursor mode.
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
if self._crosshairAction is None:
- self._crosshairAction = PlotActions.CrosshairAction(self, color='red')
+ self._crosshairAction = actions.control.CrosshairAction(self, color='red')
return self._crosshairAction
@property
@@ -491,10 +525,10 @@ class PlotWindow(PlotWidget):
def getPanWithArrowKeysAction(self):
"""Action toggling pan with arrow keys.
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
if self._panWithArrowKeysAction is None:
- self._panWithArrowKeysAction = PlotActions.PanWithArrowKeysAction(self)
+ self._panWithArrowKeysAction = actions.control.PanWithArrowKeysAction(self)
return self._panWithArrowKeysAction
@property
@@ -512,63 +546,63 @@ class PlotWindow(PlotWidget):
def getResetZoomAction(self):
"""Action resetting the zoom
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.resetZoomAction
def getZoomInAction(self):
"""Action to zoom in
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.zoomInAction
def getZoomOutAction(self):
"""Action to zoom out
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.zoomOutAction
def getXAxisAutoScaleAction(self):
"""Action to toggle the X axis autoscale on zoom reset
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.xAxisAutoScaleAction
def getYAxisAutoScaleAction(self):
"""Action to toggle the Y axis autoscale on zoom reset
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.yAxisAutoScaleAction
def getXAxisLogarithmicAction(self):
"""Action to toggle logarithmic X axis
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.xAxisLogarithmicAction
def getYAxisLogarithmicAction(self):
"""Action to toggle logarithmic Y axis
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.yAxisLogarithmicAction
def getGridAction(self):
"""Action to toggle the grid visibility in the plot
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.gridAction
def getCurveStyleAction(self):
"""Action to change curve line and markers styles
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.curveStyleAction
@@ -576,7 +610,7 @@ class PlotWindow(PlotWidget):
"""Action open a colormap dialog to change active image
and default colormap.
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.colormapAction
@@ -592,7 +626,7 @@ class PlotWindow(PlotWidget):
Use this to change the visibility of keepDataAspectRatioButton in the
toolbar (See :meth:`QToolBar.addWidget` documentation).
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.keepDataAspectRatioButton
@@ -608,56 +642,56 @@ class PlotWindow(PlotWidget):
Use this to change the visibility yAxisInvertedButton in the toolbar.
(See :meth:`QToolBar.addWidget` documentation).
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.yAxisInvertedAction
def getIntensityHistogramAction(self):
"""Action toggling the histogram intensity Plot widget
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self._intensityHistoAction
def getCopyAction(self):
"""Action to copy plot snapshot to clipboard
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.copyAction
def getSaveAction(self):
"""Action to save plot
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.saveAction
def getPrintAction(self):
"""Action to print plot
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.printAction
def getFitAction(self):
"""Action to fit selected curve
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self.fitAction
def getMedianFilter1DAction(self):
"""Action toggling the 1D median filter
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self._medianFilter1DAction
def getMedianFilter2DAction(self):
"""Action toggling the 2D median filter
- :rtype: PlotActions.PlotAction
+ :rtype: actions.PlotAction
"""
return self._medianFilter2DAction
@@ -669,7 +703,7 @@ class Plot1D(PlotWindow):
:param parent: The parent of this widget
:param backend: The backend to use for the plot (default: matplotlib).
- See :class:`.Plot` for the list of supported backend.
+ See :class:`.PlotWidget` for the list of supported backend.
:type backend: str or :class:`BackendBase.BackendBase`
"""
@@ -684,8 +718,8 @@ class Plot1D(PlotWindow):
roi=True, mask=False, fit=True)
if parent is None:
self.setWindowTitle('Plot1D')
- self.setGraphXLabel('X')
- self.setGraphYLabel('Y')
+ self.getXAxis().setLabel('X')
+ self.getYAxis().setLabel('Y')
class Plot2D(PlotWindow):
@@ -695,7 +729,7 @@ class Plot2D(PlotWindow):
:param parent: The parent of this widget
:param backend: The backend to use for the plot (default: matplotlib).
- See :class:`.Plot` for the list of supported backend.
+ See :class:`.PlotWidget` for the list of supported backend.
:type backend: str or :class:`BackendBase.BackendBase`
"""
@@ -716,26 +750,44 @@ class Plot2D(PlotWindow):
roi=False, mask=True)
if parent is None:
self.setWindowTitle('Plot2D')
- self.setGraphXLabel('Columns')
- self.setGraphYLabel('Rows')
+ self.getXAxis().setLabel('Columns')
+ self.getYAxis().setLabel('Rows')
self.profile = ProfileToolBar(plot=self)
-
self.addToolBar(self.profile)
+ self.getColorBarWidget().setVisible(True)
+
+ # Put colorbar action after colormap action
+ actions = self.toolBar().actions()
+ for index, action in enumerate(actions):
+ if action is self.getColormapAction():
+ break
+ self.toolBar().insertAction(
+ actions[index + 1],
+ self.getColorBarWidget().getToggleViewAction())
+
def _getImageValue(self, x, y):
- """Get value of top most image at position (x, y)
+ """Get status bar value of top most image at position (x, y)
:param float x: X position in plot coordinates
:param float y: Y position in plot coordinates
:return: The value at that point or '-'
"""
value = '-'
- valueZ = - float('inf')
+ valueZ = -float('inf')
+ mask = 0
+ maskZ = -float('inf')
for image in self.getAllImages():
data = image.getData(copy=False)
- if image.getZValue() >= valueZ: # This image is over the previous one
+ isMask = isinstance(image, items.MaskImageData)
+ if isMask:
+ zIndex = maskZ
+ else:
+ zIndex = valueZ
+ if image.getZValue() >= zIndex:
+ # This image is over the previous one
ox, oy = image.getOrigin()
sx, sy = image.getScale()
row, col = (y - oy) / sy, (x - ox) / sx
@@ -743,8 +795,15 @@ class Plot2D(PlotWindow):
# Test positive before cast otherwise issue with int(-0.5) = 0
row, col = int(row), int(col)
if (row < data.shape[0] and col < data.shape[1]):
- value = data[row, col]
- valueZ = image.getZValue()
+ v, z = data[row, col], image.getZValue()
+ if not isMask:
+ value = v
+ valueZ = z
+ else:
+ mask = v
+ maskZ = z
+ if maskZ > valueZ and mask > 0:
+ return value, "Masked"
return value
def getProfileToolbar(self):
diff --git a/silx/gui/plot/PrintPreviewToolButton.py b/silx/gui/plot/PrintPreviewToolButton.py
new file mode 100644
index 0000000..c5479b8
--- /dev/null
+++ b/silx/gui/plot/PrintPreviewToolButton.py
@@ -0,0 +1,350 @@
+# 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 modules provides tool buttons to send the content of a plot to a
+print preview page.
+The plot content can then be moved on the page and resized prior to printing.
+
+Classes
+-------
+
+- :class:`PrintPreviewToolButton`
+- :class:`SingletonPrintPreviewToolButton`
+
+Examples
+--------
+
+Simple example
+++++++++++++++
+
+.. code-block:: python
+
+ from silx.gui import qt
+ from silx.gui.plot import PlotWidget
+ from silx.gui.plot.PrintPreviewToolButton import PrintPreviewToolButton
+ import numpy
+
+ app = qt.QApplication([])
+
+ pw = PlotWidget()
+ toolbar = qt.QToolBar(pw)
+ toolbutton = PrintPreviewToolButton(parent=toolbar, plot=pw)
+ pw.addToolBar(toolbar)
+ toolbar.addWidget(toolbutton)
+ pw.show()
+
+ x = numpy.arange(1000)
+ y = x / numpy.sin(x)
+ pw.addCurve(x, y)
+
+ app.exec_()
+
+Singleton example
++++++++++++++++++
+
+This example illustrates how to print the content of several different
+plots on the same page. The plots all instantiate a
+:class:`SingletonPrintPreviewToolButton`, which relies on a singleton widget
+(:class:`silx.gui.widgets.PrintPreview.SingletonPrintPreviewDialog`).
+
+.. image:: img/printPreviewMultiPlot.png
+
+.. code-block:: python
+
+ from silx.gui import qt
+ from silx.gui.plot import PlotWidget
+ from silx.gui.plot.PrintPreviewToolButton import SingletonPrintPreviewToolButton
+ import numpy
+
+ app = qt.QApplication([])
+
+ plot_widgets = []
+
+ for i in range(3):
+ pw = PlotWidget()
+ toolbar = qt.QToolBar(pw)
+ toolbutton = SingletonPrintPreviewToolButton(parent=toolbar,
+ plot=pw)
+ pw.addToolBar(toolbar)
+ toolbar.addWidget(toolbutton)
+ pw.show()
+ plot_widgets.append(pw)
+
+ x = numpy.arange(1000)
+
+ plot_widgets[0].addCurve(x, numpy.sin(x * 2 * numpy.pi / 1000))
+ plot_widgets[1].addCurve(x, numpy.cos(x * 2 * numpy.pi / 1000))
+ plot_widgets[2].addCurve(x, numpy.tan(x * 2 * numpy.pi / 1000))
+
+ app.exec_()
+
+"""
+from __future__ import absolute_import
+
+import logging
+from io import StringIO
+
+from .. import qt
+from .. import icons
+from . import PlotWidget
+from ..widgets.PrintPreview import PrintPreviewDialog, SingletonPrintPreviewDialog
+from ..widgets.PrintGeometryDialog import PrintGeometryDialog
+
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "18/07/2017"
+
+_logger = logging.getLogger(__name__)
+# _logger.setLevel(logging.DEBUG)
+
+
+class PrintPreviewToolButton(qt.QToolButton):
+ """QToolButton to open a :class:`PrintPreviewDialog` (if not already open)
+ and add the current plot to its page to be printed.
+
+ :param parent: See :class:`QAction`
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ """
+ def __init__(self, parent=None, plot=None):
+ super(PrintPreviewToolButton, self).__init__(parent)
+
+ if not isinstance(plot, PlotWidget):
+ raise TypeError("plot parameter must be a PlotWidget")
+ self.plot = plot
+
+ self.setIcon(icons.getQIcon('document-print'))
+
+ printGeomAction = qt.QAction("Print geometry", self)
+ printGeomAction.setToolTip("Define a print geometry prior to sending "
+ "the plot to the print preview dialog")
+ printGeomAction.setIcon(icons.getQIcon('shape-rectangle')) # fixme: icon not displayed in menu
+ printGeomAction.triggered.connect(self._setPrintConfiguration)
+
+ printPreviewAction = qt.QAction("Print preview", self)
+ printPreviewAction.setToolTip("Send plot to the print preview dialog")
+ printPreviewAction.setIcon(icons.getQIcon('document-print')) # fixme: icon not displayed
+ printPreviewAction.triggered.connect(self._plotToPrintPreview)
+
+ menu = qt.QMenu(self)
+ menu.addAction(printGeomAction)
+ menu.addAction(printPreviewAction)
+ self.setMenu(menu)
+ self.setPopupMode(qt.QToolButton.InstantPopup)
+
+ self._printPreviewDialog = None
+ self._printConfigurationDialog = None
+
+ self._printGeometry = {"xOffset": 0.1,
+ "yOffset": 0.1,
+ "width": 0.9,
+ "height": 0.9,
+ "units": "page",
+ "keepAspectRatio": True}
+
+ @property
+ def printPreviewDialog(self):
+ """Lazy loaded :class:`PrintPreviewDialog`"""
+ # if changes are made here, don't forget making them in
+ # SingletonPrintPreviewToolButton.printPreviewDialog as well
+ if self._printPreviewDialog is None:
+ self._printPreviewDialog = PrintPreviewDialog(self.parent())
+ return self._printPreviewDialog
+
+ def _plotToPrintPreview(self):
+ """Grab the plot widget and send it to the print preview dialog.
+ Make sure the print preview dialog is shown and raised."""
+ self.printPreviewDialog.ensurePrinterIsSet()
+
+ if qt.HAS_SVG:
+ svgRenderer, viewBox = self._getSvgRendererAndViewbox()
+ self.printPreviewDialog.addSvgItem(svgRenderer,
+ viewBox=viewBox)
+ else:
+ _logger.warning("Missing QtSvg library, using a raster image")
+ if qt.BINDING in ["PyQt4", "PySide"]:
+ pixmap = qt.QPixmap.grabWidget(self.plot.centralWidget())
+ else:
+ # PyQt5 and hopefully PyQt6+
+ pixmap = self.plot.centralWidget().grab()
+ self.printPreviewDialog.addPixmap(pixmap)
+ self.printPreviewDialog.show()
+ self.printPreviewDialog.raise_()
+
+ def _getSvgRendererAndViewbox(self):
+ """Return a SVG renderer displaying the plot and its viewbox
+ (interactively specified by the user the first time this is called).
+
+ The size of the renderer is adjusted to the printer configuration
+ and to the geometry configuration (width, height, ratio) specified
+ by the user."""
+ imgData = StringIO()
+ assert self.plot.saveGraph(imgData, fileFormat="svg"), \
+ "Unable to save graph"
+ imgData.flush()
+ imgData.seek(0)
+ svgData = imgData.read()
+
+ svgRenderer = qt.QSvgRenderer()
+
+ viewbox = self._getViewBox()
+
+ svgRenderer.setViewBox(viewbox)
+
+ xml_stream = qt.QXmlStreamReader(svgData.encode(errors="replace"))
+
+ # This is for PyMca compatibility, to share a print preview with PyMca plots
+ svgRenderer._viewBox = viewbox
+ svgRenderer._svgRawData = svgData.encode(errors="replace")
+ svgRenderer._svgRendererData = xml_stream
+
+ if not svgRenderer.load(xml_stream):
+ raise RuntimeError("Cannot interpret svg data")
+
+ return svgRenderer, viewbox
+
+ def _getViewBox(self):
+ """
+ """
+ printer = self.printPreviewDialog.printer
+ dpix = printer.logicalDpiX()
+ dpiy = printer.logicalDpiY()
+ availableWidth = printer.width()
+ availableHeight = printer.height()
+
+ config = self._printGeometry
+ width = config['width']
+ height = config['height']
+ xOffset = config['xOffset']
+ yOffset = config['yOffset']
+ units = config['units']
+ keepAspectRatio = config['keepAspectRatio']
+ aspectRatio = self._getPlotAspectRatio()
+
+ # convert the offsets to dots
+ if units.lower() in ['inch', 'inches']:
+ xOffset = xOffset * dpix
+ yOffset = yOffset * dpiy
+ if width is not None:
+ width = width * dpix
+ if height is not None:
+ height = height * dpiy
+ elif units.lower() in ['cm', 'centimeters']:
+ xOffset = (xOffset / 2.54) * dpix
+ yOffset = (yOffset / 2.54) * dpiy
+ if width is not None:
+ width = (width / 2.54) * dpix
+ if height is not None:
+ height = (height / 2.54) * dpiy
+ else:
+ # page units
+ xOffset = availableWidth * xOffset
+ yOffset = availableHeight * yOffset
+ if width is not None:
+ width = availableWidth * width
+ if height is not None:
+ height = availableHeight * height
+
+ availableWidth -= xOffset
+ availableHeight -= yOffset
+
+ if width is not None:
+ if (availableWidth + 0.1) < width:
+ txt = "Available width %f is less than requested width %f" % \
+ (availableWidth, width)
+ raise ValueError(txt)
+ if height is not None:
+ if (availableHeight + 0.1) < height:
+ txt = "Available height %f is less than requested height %f" % \
+ (availableHeight, height)
+ raise ValueError(txt)
+
+ if keepAspectRatio:
+ bodyWidth = width or availableWidth
+ bodyHeight = bodyWidth * aspectRatio
+
+ if bodyHeight > availableHeight:
+ bodyHeight = availableHeight
+ bodyWidth = bodyHeight / aspectRatio
+
+ else:
+ bodyWidth = width or availableWidth
+ bodyHeight = height or availableHeight
+
+ return qt.QRectF(xOffset,
+ yOffset,
+ bodyWidth,
+ bodyHeight)
+
+ def _setPrintConfiguration(self):
+ """Open a dialog to prompt the user to adjust print
+ geometry parameters."""
+ self.printPreviewDialog.ensurePrinterIsSet()
+ if self._printConfigurationDialog is None:
+ self._printConfigurationDialog = PrintGeometryDialog(self.parent())
+
+ self._printConfigurationDialog.setPrintGeometry(self._printGeometry)
+ if self._printConfigurationDialog.exec_():
+ self._printGeometry = self._printConfigurationDialog.getPrintGeometry()
+
+ def _getPlotAspectRatio(self):
+ widget = self.plot.centralWidget()
+ graphWidth = float(widget.width())
+ graphHeight = float(widget.height())
+ return graphHeight / graphWidth
+
+
+class SingletonPrintPreviewToolButton(PrintPreviewToolButton):
+ """This class is similar to its parent class :class:`PrintPreviewToolButton`
+ but it uses a singleton print preview widget.
+
+ This allows for several plots to send their content to the
+ same print page, and for users to arrange them."""
+ def __init__(self, parent=None, plot=None):
+ PrintPreviewToolButton.__init__(self, parent, plot)
+
+ @property
+ def printPreviewDialog(self):
+ if self._printPreviewDialog is None:
+ self._printPreviewDialog = SingletonPrintPreviewDialog(self.parent())
+ return self._printPreviewDialog
+
+
+if __name__ == '__main__':
+ import numpy
+ app = qt.QApplication([])
+
+ pw = PlotWidget()
+ toolbar = qt.QToolBar(pw)
+ toolbutton = PrintPreviewToolButton(parent=toolbar,
+ plot=pw)
+ pw.addToolBar(toolbar)
+ toolbar.addWidget(toolbutton)
+ pw.show()
+
+ x = numpy.arange(1000)
+ y = x / numpy.sin(x)
+ pw.addCurve(x, y)
+
+ app.exec_()
diff --git a/silx/gui/plot/Profile.py b/silx/gui/plot/Profile.py
index a11b3f0..ff85695 100644
--- a/silx/gui/plot/Profile.py
+++ b/silx/gui/plot/Profile.py
@@ -28,7 +28,7 @@ and stacks of images"""
__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel", "H. Payno"]
__license__ = "MIT"
-__date__ = "24/04/2017"
+__date__ = "17/08/2017"
import numpy
@@ -39,11 +39,11 @@ from .. import icons
from .. import qt
from . import items
from .Colors import cursorColorForColormap
-from .PlotActions import PlotAction
+from . import actions
from .PlotToolButtons import ProfileToolButton
from .ProfileMainWindow import ProfileMainWindow
-from silx.utils.decorators import deprecated
+from silx.utils.deprecation import deprecated
def _alignedFullProfile(data, origin, scale, position, roiWidth, axis):
@@ -372,13 +372,8 @@ class ProfileToolBar(qt.QToolBar):
self._profileMainWindow = ProfileMainWindow(self)
# Actions
- self.browseAction = qt.QAction(
- icons.getQIcon('normal'),
- 'Browsing Mode', None)
- self.browseAction.setToolTip(
- 'Enables zooming interaction mode')
- self.browseAction.setCheckable(True)
- self.browseAction.triggered[bool].connect(self._browseActionTriggered)
+ self._browseAction = actions.mode.ZoomModeAction(self.plot, parent=self)
+ self._browseAction.setVisible(False)
self.hLineAction = qt.QAction(
icons.getQIcon('shape-horizontal'),
@@ -414,15 +409,13 @@ class ProfileToolBar(qt.QToolBar):
# ActionGroup
self.actionGroup = qt.QActionGroup(self)
- self.actionGroup.addAction(self.browseAction)
+ self.actionGroup.addAction(self._browseAction)
self.actionGroup.addAction(self.hLineAction)
self.actionGroup.addAction(self.vLineAction)
self.actionGroup.addAction(self.lineAction)
- self.browseAction.setChecked(True)
-
# Add actions to ToolBar
- self.addAction(self.browseAction)
+ self.addAction(self._browseAction)
self.addAction(self.hLineAction)
self.addAction(self.vLineAction)
self.addAction(self.lineAction)
@@ -450,6 +443,11 @@ class ProfileToolBar(qt.QToolBar):
self.getProfileMainWindow().sigClose.connect(self.clearProfile)
@property
+ @deprecated(since_version="0.6.0")
+ def browseAction(self):
+ return self._browseAction
+
+ @property
@deprecated(replacement="getProfilePlot", since_version="0.5.0")
def profileWindow(self):
return self.getProfilePlot()
@@ -473,10 +471,15 @@ class ProfileToolBar(qt.QToolBar):
def _activeImageChanged(self, previous, legend):
"""Handle active image change: toggle enabled toolbar, update curve"""
- self.setEnabled(legend is not None)
- if legend is not None:
- # Update default profile color
+ if legend is None:
+ self.setEnabled(False)
+ else:
activeImage = self.plot.getActiveImage()
+
+ # Disable for empty image
+ self.setEnabled(activeImage.getData(copy=False).size > 0)
+
+ # Update default profile color
if isinstance(activeImage, items.ColormapMixIn):
self._defaultOverlayColor = cursorColorForColormap(
activeImage.getColormap()['name'])
@@ -495,7 +498,15 @@ class ProfileToolBar(qt.QToolBar):
If changed from elsewhere, disable drawing tool
"""
if source is not self:
- self.browseAction.setChecked(True)
+ self.clearProfile()
+
+ # Uncheck all drawing profile modes
+ self.hLineAction.setChecked(False)
+ self.vLineAction.setChecked(False)
+ self.lineAction.setChecked(False)
+
+ if self.getProfileMainWindow() is not None:
+ self.getProfileMainWindow().hide()
def _hLineActionToggled(self, checked):
"""Handle horizontal line profile action toggle"""
@@ -524,14 +535,6 @@ class ProfileToolBar(qt.QToolBar):
else:
self.plot.sigPlotSignal.disconnect(self._plotWindowSlot)
- def _browseActionTriggered(self, checked):
- """Handle browse action mode triggered by user."""
- if checked:
- self.clearProfile()
- self.plot.setInteractiveMode('zoom', source=self)
- if self.getProfileMainWindow() is not None:
- self.getProfileMainWindow().hide()
-
def _plotWindowSlot(self, event):
"""Listen to Plot to handle drawing events to refresh ROI and profile.
"""
@@ -585,8 +588,8 @@ class ProfileToolBar(qt.QToolBar):
self.plot.remove(self._POLYGON_LEGEND, kind='item')
self.getProfilePlot().clear()
self.getProfilePlot().setGraphTitle('')
- self.getProfilePlot().setGraphXLabel('X')
- self.getProfilePlot().setGraphYLabel('Y')
+ self.getProfilePlot().getXAxis().setLabel('X')
+ self.getProfilePlot().getYAxis().setLabel('Y')
self._createProfile(currentData=image.getData(copy=False),
origin=image.getOrigin(),
@@ -678,17 +681,21 @@ class ProfileToolBar(qt.QToolBar):
class Profile3DToolBar(ProfileToolBar):
- def __init__(self, parent=None, plot=None, title='Profile Selection'):
+ def __init__(self, parent=None, stackview=None,
+ title='Profile Selection'):
"""QToolBar providing profile tools for an image or a stack of images.
:param parent: the parent QWidget
- :param plot: :class:`PlotWindow` instance on which to operate.
+ :param stackview: :class:`StackView` instance on which to operate.
:param str title: See :class:`QToolBar`.
:param parent: See :class:`QToolBar`.
"""
# TODO: add param profileWindow (specify the plot used for profiles)
- super(Profile3DToolBar, self).__init__(parent=parent, plot=plot,
+ super(Profile3DToolBar, self).__init__(parent=parent,
+ plot=stackview.getPlot(),
title=title)
+ self.stackView = stackview
+ """:class:`StackView` instance"""
self.profile3dAction = ProfileToolButton(
parent=self, plot=self.plot)
@@ -721,15 +728,15 @@ class Profile3DToolBar(ProfileToolBar):
if self._profileType == "1D":
super(Profile3DToolBar, self).updateProfile()
elif self._profileType == "2D":
- stackData = self.plot.getCurrentView(copy=False,
- returnNumpyArray=True)
+ stackData = self.stackView.getCurrentView(copy=False,
+ returnNumpyArray=True)
if stackData is None:
return
self.plot.remove(self._POLYGON_LEGEND, kind='item')
self.getProfilePlot().clear()
self.getProfilePlot().setGraphTitle('')
- self.getProfilePlot().setGraphXLabel('X')
- self.getProfilePlot().setGraphYLabel('Y')
+ self.getProfilePlot().getXAxis().setLabel('X')
+ self.getProfilePlot().getYAxis().setLabel('Y')
self._createProfile(currentData=stackData[0],
origin=stackData[1]['origin'],
diff --git a/silx/gui/plot/ScatterMaskToolsWidget.py b/silx/gui/plot/ScatterMaskToolsWidget.py
index 793719d..a9c1073 100644
--- a/silx/gui/plot/ScatterMaskToolsWidget.py
+++ b/silx/gui/plot/ScatterMaskToolsWidget.py
@@ -181,18 +181,14 @@ class ScatterMaskToolsWidget(BaseMaskToolsWidget):
:class:`PlotWidget`."""
def __init__(self, parent=None, plot=None):
+ super(ScatterMaskToolsWidget, self).__init__(parent, plot,
+ mask=ScatterMask())
self._z = 2 # Mask layer in plot
self._data_scatter = None
"""plot Scatter item for data"""
self._mask_scatter = None
"""plot Scatter item for representing the mask"""
- self._mask = ScatterMask()
-
- super(ScatterMaskToolsWidget, self).__init__(parent, plot)
-
- self._initWidgets()
-
def setSelectionMask(self, mask, copy=True):
"""Set the mask to a new array.
@@ -524,6 +520,5 @@ class ScatterMaskToolsDockWidget(BaseMaskToolsDockWidget):
:paran str name: The title of this widget
"""
def __init__(self, parent=None, plot=None, name='Mask'):
- super(ScatterMaskToolsDockWidget, self).__init__(parent, name)
- self.setWidget(ScatterMaskToolsWidget(plot=plot))
- self.widget().sigMaskChanged.connect(self._emitSigMaskChanged)
+ widget = ScatterMaskToolsWidget(plot=plot)
+ super(ScatterMaskToolsDockWidget, self).__init__(parent, name, widget)
diff --git a/silx/gui/plot/StackView.py b/silx/gui/plot/StackView.py
index 9bb0cf0..938447b 100644
--- a/silx/gui/plot/StackView.py
+++ b/silx/gui/plot/StackView.py
@@ -69,18 +69,14 @@ Example::
__authors__ = ["P. Knobel", "H. Payno"]
__license__ = "MIT"
-__date__ = "20/01/2017"
+__date__ = "11/09/2017"
import numpy
-try:
- import h5py
-except ImportError:
- h5py = None
-
from silx.gui import qt
from .. import icons
-from . import items, PlotWindow, PlotActions
+from . import items, PlotWindow, actions
+from .Colormap import Colormap
from .Colors import cursorColorForColormap
from .PlotTools import LimitsToolBar
from .Profile import Profile3DToolBar
@@ -88,6 +84,16 @@ from ..widgets.FrameBrowser import HorizontalSliderWithBrowser
from silx.utils.array_like import DatasetView, ListOfImages
from silx.math import calibration
+from silx.utils.deprecation import deprecated_warning
+
+try:
+ import h5py
+except ImportError:
+ def is_dataset(obj):
+ return False
+ h5py = None
+else:
+ from silx.io.utils import is_dataset
class StackView(qt.QMainWindow):
@@ -100,7 +106,7 @@ class StackView(qt.QMainWindow):
:param QWidget parent: the Qt parent, or None
:param backend: The backend to use for the plot (default: matplotlib).
- See :class:`.Plot` for the list of supported backend.
+ See :class:`.PlotWidget` for the list of supported backend.
:type backend: str or :class:`BackendBase.BackendBase`
:param bool resetzoom: Toggle visibility of reset zoom action.
:param bool autoScale: Toggle visibility of axes autoscale actions.
@@ -181,6 +187,10 @@ class StackView(qt.QMainWindow):
self._first_stack_dimension = 0
"""Used for dimension labels and combobox"""
+ self._titleCallback = self._defaultTitleCallback
+ """Function returning the plot title based on the frame index.
+ It can be set to a custom function using :meth:`setTitleCallback`"""
+
central_widget = qt.QWidget(self)
self._plot = PlotWindow(parent=central_widget, backend=backend,
@@ -195,11 +205,13 @@ class StackView(qt.QMainWindow):
self.sigActiveImageChanged = self._plot.sigActiveImageChanged
self.sigPlotSignal = self._plot.sigPlotSignal
+ self._addColorBarAction()
+
self._plot.profile = Profile3DToolBar(parent=self._plot,
- plot=self)
+ stackview=self)
self._plot.addToolBar(self._plot.profile)
- self._plot.setGraphXLabel('Columns')
- self._plot.setGraphYLabel('Rows')
+ self._plot.getXAxis().setLabel('Columns')
+ self._plot.getYAxis().setLabel('Rows')
self._plot.sigPlotSignal.connect(self._plotCallback)
self.__planeSelection = PlanesWidget(self._plot)
@@ -227,14 +239,15 @@ class StackView(qt.QMainWindow):
self.__planeSelection.sigPlaneSelectionChanged.connect(
self._plot.profile.clearProfile)
- def setOptionVisible(self, isVisible):
- """
- Set the visibility of the browsing options.
-
- :param bool isVisible: True to have the options visible, else False
- """
- self._browser.setVisible(isVisible)
- self.__planeSelection.setVisible(isVisible)
+ def _addColorBarAction(self):
+ self._plot.getColorBarWidget().setVisible(True)
+ actions = self._plot.toolBar().actions()
+ for index, action in enumerate(actions):
+ if action is self._plot.getColormapAction():
+ break
+ self._plot.toolBar().insertAction(
+ actions[index + 1],
+ self._plot.getColorBarWidget().getToggleViewAction())
def _plotCallback(self, eventDict):
"""Callback for plot events.
@@ -242,7 +255,7 @@ class StackView(qt.QMainWindow):
Emit :attr:`valueChanged` signal, with (x, y, value) tuple of the
cursor location in the plot."""
if eventDict['event'] == 'mouseMoved':
- activeImage = self.getActiveImage()
+ activeImage = self._plot.getActiveImage()
if activeImage is not None:
data = activeImage.getData()
height, width = data.shape
@@ -305,8 +318,7 @@ class StackView(qt.QMainWindow):
if isinstance(self._stack, numpy.ndarray):
self.__transposed_view = self._stack
- elif h5py is not None and isinstance(self._stack, h5py.Dataset) or \
- isinstance(self._stack, DatasetView):
+ elif is_dataset(self._stack) or isinstance(self._stack, DatasetView):
self.__transposed_view = DatasetView(self._stack)
elif isinstance(self._stack, ListOfImages):
@@ -321,13 +333,6 @@ class StackView(qt.QMainWindow):
self._browser.setRange(0, self.__transposed_view.shape[0] - 1)
self._browser.setValue(0)
- def setFrameNumber(self, number):
- """Set the frame selection to a specific value\
-
- :param int number: Number of the frame
- """
- self._browser.setValue(number)
-
def __updateFrameNumber(self, index):
"""Update the current image.
@@ -339,7 +344,7 @@ class StackView(qt.QMainWindow):
scale=self._getImageScale(),
legend=self.__imageLegend,
resetzoom=False, replace=False)
- self._plot.setGraphTitle("Image z=%g" % self._getImageZ(index))
+ self._updateTitle()
def _set3DScaleAndOrigin(self, calibrations):
"""Set scale and origin for all 3 axes, to be used when plotting
@@ -396,7 +401,14 @@ class StackView(qt.QMainWindow):
_xcalib, _ycalib, zcalib = self._getXYZCalibs()
return zcalib(index)
- # public API
+ def _updateTitle(self):
+ frame_idx = self._browser.value()
+ self._plot.setGraphTitle(self._titleCallback(frame_idx))
+
+ def _defaultTitleCallback(self, index):
+ return "Image z=%g" % self._getImageZ(index)
+
+ # public API, stack specific methods
def setStack(self, stack, perspective=0, reset=True, calibrations=None):
"""Set the 3D stack.
@@ -426,7 +438,7 @@ class StackView(qt.QMainWindow):
# stack as list of 2D arrays: must be converted into an array_like
if not isinstance(stack, numpy.ndarray):
- if h5py is None or not isinstance(stack, h5py.Dataset):
+ if not is_dataset(stack):
try:
assert hasattr(stack, "__len__")
for img in stack:
@@ -492,7 +504,7 @@ class StackView(qt.QMainWindow):
:return: 3D stack and parameters.
:rtype: (numpy.ndarray, dict)
"""
- image = self.getActiveImage()
+ image = self._plot.getActiveImage()
if image is None:
return None
@@ -543,7 +555,7 @@ class StackView(qt.QMainWindow):
:return: 3D stack and parameters.
:rtype: (numpy.ndarray, dict)
"""
- image = self.getActiveImage()
+ image = self._plot.getActiveImage()
if image is None:
return None
@@ -567,18 +579,57 @@ class StackView(qt.QMainWindow):
return numpy.array(self.__transposed_view, copy=copy), params
return self.__transposed_view, params
- def getActiveImage(self, just_legend=False):
- """Returns the currently active image object.
+ def setFrameNumber(self, number):
+ """Set the frame selection to a specific value
- It returns None in case of not having an active image.
+ :param int number: Number of the frame
+ """
+ self._browser.setValue(number)
- :param bool just_legend: True to get the legend of the image,
- False (the default) to get the image data and info.
- Note: :class:`StackView` uses the same legend for all frames.
- :return: legend or image object
- :rtype: str or list or None
+ def setFirstStackDimension(self, first_stack_dimension):
+ """When viewing the last 3 dimensions of an n-D array (n>3), you can
+ use this method to change the text in the combobox.
+
+ For instance, for a 7-D array, first stack dim is 4, so the default
+ "Dim1-Dim2" text should be replaced with "Dim5-Dim6" (dimensions
+ numbers are 0-based).
+
+ :param int first_stack_dim: First stack dimension (n-3) when viewing the
+ last 3 dimensions of an n-D array.
"""
- return self._plot.getActiveImage(just_legend=just_legend)
+ old_state = self.__planeSelection.blockSignals(True)
+ self.__planeSelection.setFirstStackDimension(first_stack_dimension)
+ self.__planeSelection.blockSignals(old_state)
+ self._first_stack_dimension = first_stack_dimension
+ self._browser_label.setText("Image index (Dim%d):" % first_stack_dimension)
+
+ def setTitleCallback(self, callback):
+ """Set a user defined function to generate the plot title based on the
+ image/frame index.
+
+ The callback function must accept an integer as a its first positional
+ parameter and must not require any other mandatory parameter.
+ It must return a string.
+
+ To switch back the default behavior, you can pass ``None``::
+
+ mystackview.setTitleCallback(None)
+
+ To have no title, pass a function that returns an empty string::
+
+ mystackview.setTitleCallback(lambda idx: "")
+
+ :param callback: Callback function generating the stack title based
+ on the frame number.
+ """
+
+ if callback is None:
+ self._titleCallback = self._defaultTitleCallback
+ elif callable(callback):
+ self._titleCallback = callback
+ else:
+ raise TypeError("Provided callback is not callable")
+ self._updateTitle()
def clear(self):
"""Clear the widget:
@@ -592,22 +643,6 @@ class StackView(qt.QMainWindow):
self._browser.setEnabled(False)
self._plot.clear()
- def resetZoom(self):
- """Reset the plot limits to the bounds of the data and redraw the plot.
- """
- self._plot.resetZoom()
-
- def getGraphTitle(self):
- """Return the plot main title as a str."""
- return self._plot.getGraphTitle()
-
- def setGraphTitle(self, title=""):
- """Set the plot main title.
-
- :param str title: Main title of the plot (default: '')
- """
- return self._plot.setGraphTitle(title)
-
def setLabels(self, labels=None):
"""Set the labels to be displayed on the plot axes.
@@ -635,55 +670,13 @@ class StackView(qt.QMainWindow):
self.__dimensionsLabels = new_labels
self.__updatePlotLabels()
- def getGraphXLabel(self):
- """Return the current horizontal axis label as a str."""
- return self._plot.getGraphXLabel()
-
- def setGraphXLabel(self, label=None):
- """Set the plot horizontal axis label.
-
- :param str label: The horizontal axis label
- """
- if label is None:
- label = self.__dimensionsLabels[1 if self._perspective == 2 else 2]
- self._plot.setGraphXLabel(label)
-
- def getGraphYLabel(self, axis='left'):
- """Return the current vertical axis label as a str.
-
- :param str axis: The Y axis for which to get the label (left or right)
- """
- return self._plot.getGraphYLabel(axis)
-
- def setGraphYLabel(self, label=None, axis='left'):
- """Set the vertical axis label on the plot.
-
- :param str label: The Y axis label
- :param str axis: The Y axis for which to set the label (left or right)
- """
- if label is None:
- label = self.__dimensionsLabels[1 if self._perspective == 0 else 0]
- self._plot.setGraphYLabel(label, axis)
-
- def setYAxisInverted(self, flag=True):
- """Set the Y axis orientation.
+ def getLabels(self):
+ """Return dimension labels displayed on the plot axes
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- self._plot.setYAxisInverted(flag)
-
- def isYAxisInverted(self):
- """Return True if Y axis goes from top to bottom, False otherwise."""
- return self._backend.isYAxisInverted()
-
- def getSupportedColormaps(self):
- """Get the supported colormap names as a tuple of str.
-
- The list should at least contain and start by:
- ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
+ :return: List of three strings corresponding to the 3 dimensions
+ of the stack: (name_dim0, name_dim1, name_dim2)
"""
- return self._plot.getSupportedColormaps()
+ return self.__dimensionsLabels
def getColormap(self):
"""Get the current colormap description.
@@ -720,12 +713,12 @@ class StackView(qt.QMainWindow):
:param colormap: Name of the colormap in
'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'.
- Or the description of the colormap as a dict.
+ Or a :class`.Colormap` object.
:type colormap: dict or str.
:param str normalization: Colormap mapping: 'linear' or 'log'.
:param bool autoscale: Whether to use autoscale or [vmin, vmax] range.
- Default value of autoscale is True if data is a numpy array,
- False if data is a h5py dataset.
+ Default value of autoscale is False. This option is not compatible
+ with h5py datasets.
:param float vmin: The minimum value of the range to use if
'autoscale' is False.
:param float vmax: The maximum value of the range to use if
@@ -733,84 +726,85 @@ class StackView(qt.QMainWindow):
:param numpy.ndarray colors: Only used if name is None.
Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
"""
- cmapDict = self.getColormap()
-
- if isinstance(colormap, dict):
+ # if is a colormap object or a dictionary
+ if isinstance(colormap, Colormap) or isinstance(colormap, dict):
# Support colormap parameter as a dict
- errmsg = "If colormap is provided as a dict, all other parameters"
+ errmsg = "If colormap is provided as a Colormap object, all other parameters"
errmsg += " must not be specified when calling setColormap"
assert normalization is None, errmsg
assert autoscale is None, errmsg
assert vmin is None, errmsg
assert vmax is None, errmsg
assert colors is None, errmsg
- cmapDict.update(colormap)
+ if isinstance(colormap, dict):
+ reason = 'colormap parameter should now be an object'
+ replacement = 'Colormap()'
+ since_version = '0.6'
+ deprecated_warning(type_='function',
+ name='setColormap',
+ reason=reason,
+ replacement=replacement,
+ since_version=since_version)
+ _colormap = Colormap._fromDict(colormap)
+ else:
+ _colormap = colormap
else:
- if colormap is not None:
- cmapDict['name'] = colormap
- if normalization is not None:
- cmapDict['normalization'] = normalization
- if colors is not None:
- cmapDict['colors'] = colors
-
- # Default meaning of autoscale is to reset min and max
- # each time a new image is added to the plot.
- # We want to use min and max of global volume,
- # and not change them when browsing slides
- cmapDict['autoscale'] = False
-
+ norm = normalization if normalization is not None else 'linear'
+ name = colormap if colormap is not None else 'gray'
+ _colormap = Colormap(name=name,
+ normalization=norm,
+ vmin=vmin,
+ vmax=vmax,
+ colors=colors)
+
+ # Patch: since we don't apply this colormap to a single 2D data but
+ # a 2D stack we have to deal manually with vmin, vmax
if autoscale is None:
# set default
autoscale = False
- # TODO: assess cost of computing min/max for large 3D array
- # if isinstance(self._stack, numpy.ndarray):
- # autoscale = True
- # else: # h5py.Dataset
- # autoscale = False
- elif autoscale and isinstance(self._stack, h5py.Dataset):
+ elif autoscale and is_dataset(self._stack):
# h5py dataset has no min()/max() methods
raise RuntimeError(
"Cannot auto-scale colormap for a h5py dataset")
else:
autoscale = autoscale
self.__autoscaleCmap = autoscale
+
if autoscale and (self._stack is not None):
- cmapDict['vmin'] = self._stack.min()
- cmapDict['vmax'] = self._stack.max()
+ _vmin, _vmax = _colormap.getColormapRange(data=self._stack)
+ _colormap.setVRange(vmin=_vmin, vmax=_vmax)
else:
- if vmin is not None:
- cmapDict['vmin'] = vmin
- if vmax is not None:
- cmapDict['vmax'] = vmax
+ if vmin is None and self._stack is not None:
+ _colormap.setVMin(self._stack.min())
+ else:
+ _colormap.setVMin(vmin)
+ if vmax is None and self._stack is not None:
+ _colormap.setVMax(self._stack.max())
+ else:
+ _colormap.setVMax(vmax)
- cursorColor = cursorColorForColormap(cmapDict['name'])
+ cursorColor = cursorColorForColormap(_colormap.getName())
self._plot.setInteractiveMode('zoom', color=cursorColor)
- self._plot.setDefaultColormap(cmapDict)
+ self._plot.setDefaultColormap(_colormap)
# Update active image colormap
activeImage = self._plot.getActiveImage()
if isinstance(activeImage, items.ColormapMixIn):
activeImage.setColormap(self.getColormap())
- def isKeepDataAspectRatio(self):
- """Returns whether the plot is keeping data aspect ratio or not."""
- return self._plot.isKeepDataAspectRatio()
+ def getPlot(self):
+ """Return the :class:`PlotWidget`.
- def setKeepDataAspectRatio(self, flag=True):
- """Set whether the plot keeps data aspect ratio or not.
+ This gives access to advanced plot configuration options.
+ Be warned that modifying the plot can cause issues, and some changes
+ you make to the plot could be overwritten by the :class:`StackView`
+ widget's internal methods and callbacks.
- :param bool flag: True to respect data aspect ratio
+ :return: instance of :class:`PlotWidget` used in widget
"""
- self._plot.setKeepDataAspectRatio(flag)
-
- def getProfileToolbar(self):
- """Profile tools attached to this plot
-
- See :class:`silx.gui.plot.Profile.Profile3DToolBar`
- """
- return self._plot.profile
+ return self._plot
def getProfileWindow1D(self):
"""Plot window used to display 1D profile curve.
@@ -826,7 +820,158 @@ class StackView(qt.QMainWindow):
"""
return self._plot.profile.getProfileWindow2D()
+ def setOptionVisible(self, isVisible):
+ """
+ Set the visibility of the browsing options.
+
+ :param bool isVisible: True to have the options visible, else False
+ """
+ self._browser.setVisible(isVisible)
+ self.__planeSelection.setVisible(isVisible)
+
+ # proxies to PlotWidget or PlotWindow methods
+ def getProfileToolbar(self):
+ """Profile tools attached to this plot
+
+ See :class:`silx.gui.plot.Profile.Profile3DToolBar`
+ """
+ return self._plot.profile
+
+ def getGraphTitle(self):
+ """Return the plot main title as a str.
+ """
+ return self._plot.getGraphTitle()
+
+ def setGraphTitle(self, title=""):
+ """Set the plot main title.
+
+ :param str title: Main title of the plot (default: '')
+ """
+ return self._plot.setGraphTitle(title)
+
+ def getGraphXLabel(self):
+ """Return the current horizontal axis label as a str.
+ """
+ return self._plot.getXAxis().getLabel()
+
+ def setGraphXLabel(self, label=None):
+ """Set the plot horizontal axis label.
+
+ :param str label: The horizontal axis label
+ """
+ if label is None:
+ label = self.__dimensionsLabels[1 if self._perspective == 2 else 2]
+ self._plot.getXAxis().setLabel(label)
+
+ def getGraphYLabel(self, axis='left'):
+ """Return the current vertical axis label as a str.
+
+ :param str axis: The Y axis for which to get the label (left or right)
+ """
+ return self._plot.getYAxis().getLabel(axis)
+
+ def setGraphYLabel(self, label=None, axis='left'):
+ """Set the vertical axis label on the plot.
+
+ :param str label: The Y axis label
+ :param str axis: The Y axis for which to set the label (left or right)
+ """
+ if label is None:
+ label = self.__dimensionsLabels[1 if self._perspective == 0 else 0]
+ self._plot.getYAxis(axis=axis).setLabel(label)
+
+ def resetZoom(self):
+ """Reset the plot limits to the bounds of the data and redraw the plot.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().resetZoom()
+ """
+ self._plot.resetZoom()
+
+ def setYAxisInverted(self, flag=True):
+ """Set the Y axis orientation.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().setYAxisInverted(flag)
+
+ :param bool flag: True for Y axis going from top to bottom,
+ False for Y axis going from bottom to top
+ """
+ self._plot.setYAxisInverted(flag)
+
+ def isYAxisInverted(self):
+ """Return True if Y axis goes from top to bottom, False otherwise.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().isYAxisInverted()"""
+ return self._plot.isYAxisInverted()
+
+ def getSupportedColormaps(self):
+ """Get the supported colormap names as a tuple of str.
+
+ The list should at least contain and start by:
+ ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().getSupportedColormaps()
+ """
+ return self._plot.getSupportedColormaps()
+
+ def isKeepDataAspectRatio(self):
+ """Returns whether the plot is keeping data aspect ratio or not.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().isKeepDataAspectRatio()"""
+ return self._plot.isKeepDataAspectRatio()
+
+ def setKeepDataAspectRatio(self, flag=True):
+ """Set whether the plot keeps data aspect ratio or not.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().setKeepDataAspectRatio(flag)
+
+ :param bool flag: True to respect data aspect ratio
+ """
+ self._plot.setKeepDataAspectRatio(flag)
+
# kind of private methods, but needed by Profile
+ def getActiveImage(self, just_legend=False):
+ """Returns the currently active image object.
+
+ It returns None in case of not having an active image.
+
+ This method is a simple proxy to the legacy :class:`PlotWidget` method
+ of the same name. Using the object oriented approach is now
+ preferred::
+
+ stackview.getPlot().getActiveImage()
+
+ :param bool just_legend: True to get the legend of the image,
+ False (the default) to get the image data and info.
+ Note: :class:`StackView` uses the same legend for all frames.
+ :return: legend or image object
+ :rtype: str or list or None
+ """
+ return self._plot.getActiveImage(just_legend=just_legend)
+
def remove(self, legend=None,
kind=('curve', 'image', 'item', 'marker')):
"""See :meth:`Plot.Plot.remove`"""
@@ -844,23 +989,6 @@ class StackView(qt.QMainWindow):
"""
self._plot.addItem(*args, **kwargs)
- def setFirstStackDimension(self, first_stack_dimension):
- """When viewing the last 3 dimensions of an n-D array (n>3), you can
- use this method to change the text in the combobox.
-
- For instance, for a 7-D array, first stack dim is 4, so the default
- "Dim1-Dim2" text should be replaced with "Dim5-Dim6" (dimensions
- numbers are 0-based).
-
- :param int first_stack_dim: First stack dimension (n-3) when viewing the
- last 3 dimensions of an n-D array.
- """
- old_state = self.__planeSelection.blockSignals(True)
- self.__planeSelection.setFirstStackDimension(first_stack_dimension)
- self.__planeSelection.blockSignals(old_state)
- self._first_stack_dimension = first_stack_dimension
- self._browser_label.setText("Image index (Dim%d):" % first_stack_dimension)
-
class PlanesWidget(qt.QWidget):
"""Widget for the plane/perspective selection
@@ -974,11 +1102,12 @@ class StackViewMainWindow(StackView):
menu.addSeparator()
menu.addAction(self._plot.resetZoomAction)
menu.addAction(self._plot.colormapAction)
- menu.addAction(PlotActions.KeepAspectRatioAction(self._plot, self))
- menu.addAction(PlotActions.YAxisInvertedAction(self._plot, self))
+ menu.addAction(self._plot.getColorBarWidget().getToggleViewAction())
+
+ menu.addAction(actions.control.KeepAspectRatioAction(self._plot, self))
+ menu.addAction(actions.control.YAxisInvertedAction(self._plot, self))
menu = self.menuBar().addMenu('Profile')
- menu.addAction(self._plot.profile.browseAction)
menu.addAction(self._plot.profile.hLineAction)
menu.addAction(self._plot.profile.vLineAction)
menu.addAction(self._plot.profile.lineAction)
diff --git a/silx/gui/plot/_BaseMaskToolsWidget.py b/silx/gui/plot/_BaseMaskToolsWidget.py
index 91bbe1c..35a48ae 100644
--- a/silx/gui/plot/_BaseMaskToolsWidget.py
+++ b/silx/gui/plot/_BaseMaskToolsWidget.py
@@ -27,17 +27,19 @@
"""
from __future__ import division
-
__authors__ = ["T. Vincent", "P. Knobel"]
__license__ = "MIT"
-__date__ = "20/04/2017"
+__date__ = "02/10/2017"
import os
import numpy
from silx.gui import qt, icons
+from silx.gui.widgets.FloatEdit import FloatEdit
+from silx.gui.plot.Colormap import Colormap
from silx.gui.plot.Colors import rgba
+from .actions.mode import PanModeAction
class BaseMask(qt.QObject):
@@ -353,24 +355,38 @@ class BaseMaskToolsWidget(qt.QWidget):
sigMaskChanged = qt.Signal()
_maxLevelNumber = 255
- def __init__(self, parent=None, plot=None):
+ def __init__(self, parent=None, plot=None, mask=None):
+ """
+
+ :param parent: Parent QWidget
+ :param plot: Plot widget on which to operate
+ :param mask: Instance of subclass of :class:`BaseMask`
+ (e.g. :class:`ImageMask`)
+ """
+ super(BaseMaskToolsWidget, self).__init__(parent)
# register if the user as force a color for the corresponding mask level
self._defaultColors = numpy.ones((self._maxLevelNumber + 1), dtype=numpy.bool)
# overlays colors set by the user
self._overlayColors = numpy.zeros((self._maxLevelNumber + 1, 3), dtype=numpy.float32)
+ # as parent have to be the first argument of the widget to fit
+ # QtDesigner need but here plot can't be None by default.
+ assert plot is not None
self._plot = plot
self._maskName = '__MASK_TOOLS_%d' % id(self) # Legend of the mask
- self._colormap = {
- 'name': None,
- 'normalization': 'linear',
- 'autoscale': False,
- 'vmin': 0, 'vmax': self._maxLevelNumber,
- 'colors': None}
+ self._colormap = Colormap(name="",
+ normalization='linear',
+ vmin=0,
+ vmax=self._maxLevelNumber,
+ colors=None)
self._defaultOverlayColor = rgba('gray') # Color of the mask
self._setMaskColors(1, 0.5)
+ if not isinstance(mask, BaseMask):
+ raise TypeError("mask is not an instance of BaseMask")
+ self._mask = mask
+
self._mask.sigChanged.connect(self._updatePlotMask)
self._mask.sigChanged.connect(self._emitSigMaskChanged)
@@ -378,12 +394,12 @@ class BaseMaskToolsWidget(qt.QWidget):
self._lastPencilPos = None
self._multipleMasks = 'exclusive'
- super(BaseMaskToolsWidget, self).__init__(parent)
-
self._maskFileDir = qt.QDir.home().absolutePath()
self.plot.sigInteractiveModeChanged.connect(
self._interactiveModeChanged)
+ self._initWidgets()
+
def _emitSigMaskChanged(self):
"""Notify mask changes"""
self.sigMaskChanged.emit()
@@ -570,17 +586,10 @@ class BaseMaskToolsWidget(qt.QWidget):
"""Init drawing tools widgets"""
layout = qt.QVBoxLayout()
- # Draw tools
- self.browseAction = qt.QAction(
- icons.getQIcon('normal'), 'Browse', None)
- self.browseAction.setShortcut(qt.QKeySequence(qt.Qt.Key_B))
- self.browseAction.setToolTip(
- 'Disables drawing tools, enables zooming interaction mode'
- ' <b>B</b>')
- self.browseAction.setCheckable(True)
- self.browseAction.triggered.connect(self._activeBrowseMode)
+ self.browseAction = PanModeAction(self.plot, self.plot)
self.addAction(self.browseAction)
+ # Draw tools
self.rectAction = qt.QAction(
icons.getQIcon('shape-rectangle'), 'Rectangle selection', None)
self.rectAction.setToolTip(
@@ -608,23 +617,22 @@ class BaseMaskToolsWidget(qt.QWidget):
'Pencil tool: (Un)Mask using a pencil <b>P</b>')
self.pencilAction.setCheckable(True)
self.pencilAction.triggered.connect(self._activePencilMode)
- self.addAction(self.polygonAction)
+ self.addAction(self.pencilAction)
self.drawActionGroup = qt.QActionGroup(self)
self.drawActionGroup.setExclusive(True)
- self.drawActionGroup.addAction(self.browseAction)
self.drawActionGroup.addAction(self.rectAction)
self.drawActionGroup.addAction(self.polygonAction)
self.drawActionGroup.addAction(self.pencilAction)
- self.browseAction.setChecked(True)
-
- self.drawButtons = {}
- for action in self.drawActionGroup.actions():
+ actions = (self.browseAction, self.rectAction,
+ self.polygonAction, self.pencilAction)
+ drawButtons = []
+ for action in actions:
btn = qt.QToolButton()
btn.setDefaultAction(action)
- self.drawButtons[action.text()] = btn
- container = self._hboxWidget(*self.drawButtons.values())
+ drawButtons.append(btn)
+ container = self._hboxWidget(*drawButtons)
layout.addWidget(container)
# Mask/Unmask radio buttons
@@ -644,10 +652,7 @@ class BaseMaskToolsWidget(qt.QWidget):
self.maskStateWidget = self._hboxWidget(maskRadioBtn, unmaskRadioBtn)
layout.addWidget(self.maskStateWidget)
- # Connect mask state widget visibility with browse action
- self.maskStateWidget.setHidden(self.browseAction.isChecked())
- self.browseAction.toggled[bool].connect(
- self.maskStateWidget.setHidden)
+ self.maskStateWidget.setHidden(True)
# Pencil settings
self.pencilSetting = self._createPencilSettings(None)
@@ -752,15 +757,11 @@ class BaseMaskToolsWidget(qt.QWidget):
form = qt.QFormLayout()
- self.minLineEdit = qt.QLineEdit()
- self.minLineEdit.setText('0')
- self.minLineEdit.setValidator(qt.QDoubleValidator())
+ self.minLineEdit = FloatEdit(self, value=0)
self.minLineEdit.setEnabled(False)
form.addRow('Min:', self.minLineEdit)
- self.maxLineEdit = qt.QLineEdit()
- self.maxLineEdit.setText('0')
- self.maxLineEdit.setValidator(qt.QDoubleValidator())
+ self.maxLineEdit = FloatEdit(self, value=0)
self.maxLineEdit.setEnabled(False)
form.addRow('Max:', self.maxLineEdit)
@@ -790,8 +791,9 @@ class BaseMaskToolsWidget(qt.QWidget):
"""Reset drawing action when disabling widget"""
if (event.type() == qt.QEvent.EnabledChange and
not self.isEnabled() and
- not self.browseAction.isChecked()):
- self.browseAction.trigger() # Disable drawing tool
+ self.drawActionGroup.checkedAction()):
+ # Disable drawing tool by setting interaction to zoom
+ self.browseAction.trigger()
def save(self, filename, kind):
"""Save current mask in a file
@@ -839,7 +841,7 @@ class BaseMaskToolsWidget(qt.QWidget):
# Set no mask level
colors[0] = (0., 0., 0., 0.)
- self._colormap['colors'] = colors
+ self._colormap.setColormapLUT(colors)
def resetMaskColors(self, level=None):
"""Reset the mask color at the given level to be defaultColors
@@ -928,10 +930,11 @@ class BaseMaskToolsWidget(qt.QWidget):
If changed from elsewhere, disable drawing tool
"""
if source is not self:
- # Do not trigger browseAction to avoid to call
- # self.plot.setInteractiveMode
- self.browseAction.setChecked(True)
+ self.pencilAction.setChecked(False)
+ self.rectAction.setChecked(False)
+ self.polygonAction.setChecked(False)
self._releaseDrawingMode()
+ self._updateDrawingModeWidgets()
def _releaseDrawingMode(self):
"""Release the drawing mode if is was used"""
@@ -940,16 +943,6 @@ class BaseMaskToolsWidget(qt.QWidget):
self.plot.sigPlotSignal.disconnect(self._plotDrawEvent)
self._drawingMode = None
- def _activeBrowseMode(self):
- """Handle browse action mode triggered by user.
-
- Set plot interactive mode only when
- the user is triggering the browse action.
- """
- self._releaseDrawingMode()
- self.plot.setInteractiveMode('zoom', source=self)
- self._updateDrawingModeWidgets()
-
def _activeRectMode(self):
"""Handle rect action mode triggering"""
self._releaseDrawingMode()
@@ -981,6 +974,7 @@ class BaseMaskToolsWidget(qt.QWidget):
self._updateDrawingModeWidgets()
def _updateDrawingModeWidgets(self):
+ self.maskStateWidget.setVisible(self._drawingMode is not None)
self.pencilSetting.setVisible(self._drawingMode == 'pencil')
# Handle plot drawing events
@@ -1032,20 +1026,20 @@ class BaseMaskToolsWidget(qt.QWidget):
if self.belowThresholdAction.isChecked():
if self.minLineEdit.text():
self._mask.updateBelowThreshold(self.levelSpinBox.value(),
- float(self.minLineEdit.text()))
+ self.minLineEdit.value())
self._mask.commit()
elif self.betweenThresholdAction.isChecked():
if self.minLineEdit.text() and self.maxLineEdit.text():
- min_ = float(self.minLineEdit.text())
- max_ = float(self.maxLineEdit.text())
+ min_ = self.minLineEdit.value()
+ max_ = self.maxLineEdit.value()
self._mask.updateBetweenThresholds(self.levelSpinBox.value(),
min_, max_)
self._mask.commit()
elif self.aboveThresholdAction.isChecked():
if self.maxLineEdit.text():
- max_ = float(self.maxLineEdit.text())
+ max_ = float(self.maxLineEdit.value())
self._mask.updateAboveThreshold(self.levelSpinBox.value(),
max_)
self._mask.commit()
@@ -1059,7 +1053,7 @@ class BaseMaskToolsWidget(qt.QWidget):
class BaseMaskToolsDockWidget(qt.QDockWidget):
"""Base class for :class:`MaskToolsWidget` and
- :class:`ScatterMaskToolsWidget`
+ :class:`ScatterMaskToolsWidget`.
For integration in a :class:`PlotWindow`.
@@ -1069,10 +1063,15 @@ class BaseMaskToolsDockWidget(qt.QDockWidget):
sigMaskChanged = qt.Signal()
- def __init__(self, parent=None, name='Mask'):
+ def __init__(self, parent=None, name='Mask', widget=None):
super(BaseMaskToolsDockWidget, self).__init__(parent)
self.setWindowTitle(name)
+ if not isinstance(widget, BaseMaskToolsWidget):
+ raise TypeError("BaseMaskToolsDockWidget requires a MaskToolsWidget")
+ self.setWidget(widget)
+ self.widget().sigMaskChanged.connect(self._emitSigMaskChanged)
+
self.layout().setContentsMargins(0, 0, 0, 0)
self.dockLocationChanged.connect(self._dockLocationChanged)
self.topLevelChanged.connect(self._topLevelChanged)
@@ -1107,6 +1106,11 @@ class BaseMaskToolsDockWidget(qt.QDockWidget):
"""
return self.widget().setSelectionMask(mask, copy=copy)
+ def resetSelectionMask(self):
+ """Reset the mask to an array of zeros with the shape of the
+ current data."""
+ self.widget().resetSelectionMask()
+
def toggleViewAction(self):
"""Returns a checkable action that shows or closes this widget.
diff --git a/silx/gui/plot/__init__.py b/silx/gui/plot/__init__.py
index 06a24a7..b03392d 100644
--- a/silx/gui/plot/__init__.py
+++ b/silx/gui/plot/__init__.py
@@ -43,7 +43,7 @@ List of Qt widgets:
By default, those widget are using matplotlib_.
They can optionally use a faster OpenGL-based rendering (beta feature),
which is enabled by setting the ``backend`` argument to ``'gl'``
-when creating the widgets (See :class:`.Plot`).
+when creating the widgets (See :class:`.PlotWidget`).
.. note::
@@ -56,12 +56,9 @@ when creating the widgets (See :class:`.Plot`).
__authors__ = ["T. Vincent"]
__license__ = "MIT"
-__date__ = "22/02/2016"
+__date__ = "03/05/2017"
-# First of all init matplotlib and set its backend
-from .backends import _matplotlib # noqa
-
from .PlotWidget import PlotWidget # noqa
from .PlotWindow import PlotWindow, Plot1D, Plot2D # noqa
from .ImageView import ImageView # noqa
diff --git a/silx/gui/plot/_utils/__init__.py b/silx/gui/plot/_utils/__init__.py
index 355bc02..3c2dfa4 100644
--- a/silx/gui/plot/_utils/__init__.py
+++ b/silx/gui/plot/_utils/__init__.py
@@ -35,17 +35,6 @@ from .panzoom import FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX
from .panzoom import applyZoomToPlot, applyPan
-def clipColormapLogRange(colormap):
- """Clip colormap vmin and vmax to 1, 10 if normalization is 'log' and vmin
- or vmax <1
-
- :param dict colormap: the colormap for which we want to clip vmin and vmax
- """
- if colormap['normalization'] is 'log':
- if colormap['vmin'] < 1. or colormap['vmax'] < 1.:
- colormap['vmin'], colormap['vmax'] = 1., 10.
-
-
def addMarginsToLimits(margins, isXLog, isYLog,
xMin, xMax, yMin, yMax, y2Min=None, y2Max=None):
"""Returns updated limits by extending them with margins.
diff --git a/silx/gui/plot/_utils/panzoom.py b/silx/gui/plot/_utils/panzoom.py
index bec31df..3946a04 100644
--- a/silx/gui/plot/_utils/panzoom.py
+++ b/silx/gui/plot/_utils/panzoom.py
@@ -24,13 +24,12 @@
# ###########################################################################*/
"""Functions to apply pan and zoom on a Plot"""
-__authors__ = ["T. Vincent"]
+__authors__ = ["T. Vincent", "V. Valls"]
__license__ = "MIT"
-__date__ = "21/03/2017"
+__date__ = "08/08/2017"
import math
-
import numpy
@@ -93,8 +92,8 @@ def applyZoomToPlot(plot, scaleF, center=None):
:param center: (x, y) coords in pixel coordinates of the zoom center.
:type center: 2-tuple of float
"""
- xMin, xMax = plot.getGraphXLimits()
- yMin, yMax = plot.getGraphYLimits()
+ xMin, xMax = plot.getXAxis().getLimits()
+ yMin, yMax = plot.getYAxis().getLimits()
if center is None:
left, top, width, height = plot.getPlotBoundsInPixels()
@@ -106,17 +105,17 @@ def applyZoomToPlot(plot, scaleF, center=None):
assert dataCenterPos is not None
xMin, xMax = scale1DRange(xMin, xMax, dataCenterPos[0], scaleF,
- plot.isXAxisLogarithmic())
+ plot.getXAxis()._isLogarithmic())
yMin, yMax = scale1DRange(yMin, yMax, dataCenterPos[1], scaleF,
- plot.isYAxisLogarithmic())
+ plot.getYAxis()._isLogarithmic())
dataPos = plot.pixelToData(cx, cy, axis="right")
assert dataPos is not None
y2Center = dataPos[1]
- y2Min, y2Max = plot.getGraphYLimits(axis="right")
+ y2Min, y2Max = plot.getYAxis(axis="right").getLimits()
y2Min, y2Max = scale1DRange(y2Min, y2Max, y2Center, scaleF,
- plot.isYAxisLogarithmic())
+ plot.getYAxis()._isLogarithmic())
plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
@@ -154,3 +153,140 @@ def applyPan(min_, max_, panFactor, isLog10):
if newMin > - float('inf') and newMax < float('inf'):
min_, max_ = newMin, newMax
return min_, max_
+
+
+class _Unset(object):
+ """To be able to have distinction between None and unset"""
+ pass
+
+
+class ViewConstraints(object):
+ """
+ Store constraints applied on the view box and compute the resulting view box.
+ """
+
+ def __init__(self):
+ self._min = [None, None]
+ self._max = [None, None]
+ self._minRange = [None, None]
+ self._maxRange = [None, None]
+
+ def update(self, xMin=_Unset, xMax=_Unset,
+ yMin=_Unset, yMax=_Unset,
+ minXRange=_Unset, maxXRange=_Unset,
+ minYRange=_Unset, maxYRange=_Unset):
+ """
+ Update the constraints managed by the object
+
+ The constraints are the same as the ones provided by PyQtGraph.
+
+ :param float xMin: Minimum allowed x-axis value.
+ (default do not change the stat, None remove the constraint)
+ :param float xMax: Maximum allowed x-axis value.
+ (default do not change the stat, None remove the constraint)
+ :param float yMin: Minimum allowed y-axis value.
+ (default do not change the stat, None remove the constraint)
+ :param float yMax: Maximum allowed y-axis value.
+ (default do not change the stat, None remove the constraint)
+ :param float minXRange: Minimum allowed left-to-right span across the
+ view (default do not change the stat, None remove the constraint)
+ :param float maxXRange: Maximum allowed left-to-right span across the
+ view (default do not change the stat, None remove the constraint)
+ :param float minYRange: Minimum allowed top-to-bottom span across the
+ view (default do not change the stat, None remove the constraint)
+ :param float maxYRange: Maximum allowed top-to-bottom span across the
+ view (default do not change the stat, None remove the constraint)
+ :return: True if the constraints was changed
+ """
+ updated = False
+
+ minRange = [minXRange, minYRange]
+ maxRange = [maxXRange, maxYRange]
+ minPos = [xMin, yMin]
+ maxPos = [xMax, yMax]
+
+ for axis in range(2):
+
+ value = minPos[axis]
+ if value is not _Unset and value != self._min[axis]:
+ self._min[axis] = value
+ updated = True
+
+ value = maxPos[axis]
+ if value is not _Unset and value != self._max[axis]:
+ self._max[axis] = value
+ updated = True
+
+ value = minRange[axis]
+ if value is not _Unset and value != self._minRange[axis]:
+ self._minRange[axis] = value
+ updated = True
+
+ value = maxRange[axis]
+ if value is not _Unset and value != self._maxRange[axis]:
+ self._maxRange[axis] = value
+ updated = True
+
+ # Sanity checks
+
+ for axis in range(2):
+ if self._maxRange[axis] is not None and self._min[axis] is not None and self._max[axis] is not None:
+ # max range cannot be larger than bounds
+ diff = self._max[axis] - self._min[axis]
+ self._maxRange[axis] = min(self._maxRange[axis], diff)
+ updated = True
+
+ return updated
+
+ def normalize(self, xMin, xMax, yMin, yMax, allow_scaling=True):
+ """Normalize a view range defined by x and y corners using predefined
+ containts.
+
+ :param float xMin: Min position of the x-axis
+ :param float xMax: Max position of the x-axis
+ :param float yMin: Min position of the y-axis
+ :param float yMax: Max position of the y-axis
+ :param bool allow_scaling: Allow or not to apply scaling for the
+ normalization. Used according to the interaction mode.
+ :return: A normalized tuple of (xMin, xMax, yMin, yMax)
+ """
+ viewRange = [[xMin, xMax], [yMin, yMax]]
+
+ for axis in range(2):
+ # clamp xRange and yRange
+ if allow_scaling:
+ diff = viewRange[axis][1] - viewRange[axis][0]
+ delta = None
+ if self._maxRange[axis] is not None and diff > self._maxRange[axis]:
+ delta = self._maxRange[axis] - diff
+ elif self._minRange[axis] is not None and diff < self._minRange[axis]:
+ delta = self._minRange[axis] - diff
+ if delta is not None:
+ viewRange[axis][0] -= delta * 0.5
+ viewRange[axis][1] += delta * 0.5
+
+ # clamp min and max positions
+ outMin = self._min[axis] is not None and viewRange[axis][0] < self._min[axis]
+ outMax = self._max[axis] is not None and viewRange[axis][1] > self._max[axis]
+
+ if outMin and outMax:
+ if allow_scaling:
+ # we can clamp both sides
+ viewRange[axis][0] = self._min[axis]
+ viewRange[axis][1] = self._max[axis]
+ else:
+ # center the result
+ delta = viewRange[axis][1] - viewRange[axis][0]
+ mid = self._min[axis] + self._max[axis] - self._min[axis]
+ viewRange[axis][0] = mid - delta
+ viewRange[axis][1] = mid + delta
+ elif outMin:
+ delta = self._min[axis] - viewRange[axis][0]
+ viewRange[axis][0] += delta
+ viewRange[axis][1] += delta
+ elif outMax:
+ delta = self._max[axis] - viewRange[axis][1]
+ viewRange[axis][0] += delta
+ viewRange[axis][1] += delta
+
+ return viewRange[0][0], viewRange[0][1], viewRange[1][0], viewRange[1][1]
diff --git a/silx/gui/plot/actions/PlotAction.py b/silx/gui/plot/actions/PlotAction.py
new file mode 100644
index 0000000..6eb9ba3
--- /dev/null
+++ b/silx/gui/plot/actions/PlotAction.py
@@ -0,0 +1,79 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-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.
+#
+# ###########################################################################*/
+"""
+The class :class:`.PlotAction` help the creation of a qt.QAction associated
+with a :class:`.PlotWidget`.
+"""
+
+from __future__ import division
+
+
+__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
+__license__ = "MIT"
+__date__ = "20/04/2017"
+
+
+from collections import OrderedDict
+import weakref
+from silx.gui import icons
+from silx.gui import qt
+
+
+class PlotAction(qt.QAction):
+ """Base class for QAction that operates on a PlotWidget.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate.
+ :param icon: QIcon or str name of icon to use
+ :param str text: The name of this action to be used for menu label
+ :param str tooltip: The text of the tooltip
+ :param triggered: The callback to connect to the action's triggered
+ signal or None for no callback.
+ :param bool checkable: True for checkable action, False otherwise (default)
+ :param parent: See :class:`QAction`.
+ """
+
+ def __init__(self, plot, icon, text, tooltip=None,
+ triggered=None, checkable=False, parent=None):
+ assert plot is not None
+ self._plotRef = weakref.ref(plot)
+
+ if not isinstance(icon, qt.QIcon):
+ # Try with icon as a string and load corresponding icon
+ icon = icons.getQIcon(icon)
+
+ super(PlotAction, self).__init__(icon, text, parent)
+
+ if tooltip is not None:
+ self.setToolTip(tooltip)
+
+ self.setCheckable(checkable)
+
+ if triggered is not None:
+ self.triggered[bool].connect(triggered)
+
+ @property
+ def plot(self):
+ """The :class:`.PlotWidget` this action group is controlling."""
+ return self._plotRef()
diff --git a/silx/gui/plot/actions/__init__.py b/silx/gui/plot/actions/__init__.py
new file mode 100644
index 0000000..73829cd
--- /dev/null
+++ b/silx/gui/plot/actions/__init__.py
@@ -0,0 +1,38 @@
+# 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 package provides a set of QActions to use with :class:`PlotWidget`
+
+It also contains the :class:'.PlotAction' (Base class for QAction that operates
+on a PlotWidget)
+"""
+
+__authors__ = ["H. Payno"]
+__license__ = "MIT"
+__date__ = "16/08/2017"
+
+from .PlotAction import PlotAction
+from . import control
+from . import mode
+from . import io
diff --git a/silx/gui/plot/actions/control.py b/silx/gui/plot/actions/control.py
new file mode 100644
index 0000000..23e710e
--- /dev/null
+++ b/silx/gui/plot/actions/control.py
@@ -0,0 +1,549 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-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.
+#
+# ###########################################################################*/
+"""
+:mod:`silx.gui.plot.actions.control` provides a set of QAction relative to control
+of a :class:`.PlotWidget`.
+
+The following QAction are available:
+
+- :class:`ColormapAction`
+- :class:`CrosshairAction`
+- :class:`CurveStyleAction`
+- :class:`GridAction`
+- :class:`KeepAspectRatioAction`
+- :class:`PanWithArrowKeysAction`
+- :class:`ResetZoomAction`
+- :class:`XAxisLogarithmicAction`
+- :class:`XAxisAutoScaleAction`
+- :class:`YAxisInvertedAction`
+- :class:`YAxisLogarithmicAction`
+- :class:`YAxisAutoScaleAction`
+- :class:`ZoomBackAction`
+- :class:`ZoomInAction`
+- :class:`ZoomOutAction`
+- :class:'ShowAxisAction'
+"""
+
+from __future__ import division
+
+__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
+__license__ = "MIT"
+__date__ = "27/06/2017"
+
+from . import PlotAction
+import logging
+import numpy
+from silx.gui.plot import items
+from silx.gui.plot.ColormapDialog import ColormapDialog
+from silx.gui.plot._utils import applyZoomToPlot as _applyZoomToPlot
+from silx.gui import qt
+from silx.gui import icons
+
+_logger = logging.getLogger(__name__)
+
+
+class ResetZoomAction(PlotAction):
+ """QAction controlling reset zoom on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ super(ResetZoomAction, self).__init__(
+ plot, icon='zoom-original', text='Reset Zoom',
+ tooltip='Auto-scale the graph',
+ triggered=self._actionTriggered,
+ checkable=False, parent=parent)
+ self._autoscaleChanged(True)
+ plot.getXAxis().sigAutoScaleChanged.connect(self._autoscaleChanged)
+ plot.getYAxis().sigAutoScaleChanged.connect(self._autoscaleChanged)
+
+ def _autoscaleChanged(self, enabled):
+ xAxis = self.plot.getXAxis()
+ yAxis = self.plot.getYAxis()
+ self.setEnabled(xAxis.isAutoScale() or yAxis.isAutoScale())
+
+ if xAxis.isAutoScale() and yAxis.isAutoScale():
+ tooltip = 'Auto-scale the graph'
+ elif xAxis.isAutoScale(): # And not Y axis
+ tooltip = 'Auto-scale the x-axis of the graph only'
+ elif yAxis.isAutoScale(): # And not X axis
+ tooltip = 'Auto-scale the y-axis of the graph only'
+ else: # no axis in autoscale
+ tooltip = 'Auto-scale the graph'
+ self.setToolTip(tooltip)
+
+ def _actionTriggered(self, checked=False):
+ self.plot.resetZoom()
+
+
+class ZoomBackAction(PlotAction):
+ """QAction performing a zoom-back in :class:`.PlotWidget` limits history.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ super(ZoomBackAction, self).__init__(
+ plot, icon='zoom-back', text='Zoom Back',
+ tooltip='Zoom back the plot',
+ triggered=self._actionTriggered,
+ checkable=False, parent=parent)
+ self.setShortcutContext(qt.Qt.WidgetShortcut)
+
+ def _actionTriggered(self, checked=False):
+ self.plot.getLimitsHistory().pop()
+
+
+class ZoomInAction(PlotAction):
+ """QAction performing a zoom-in on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ super(ZoomInAction, self).__init__(
+ plot, icon='zoom-in', text='Zoom In',
+ tooltip='Zoom in the plot',
+ triggered=self._actionTriggered,
+ checkable=False, parent=parent)
+ self.setShortcut(qt.QKeySequence.ZoomIn)
+ self.setShortcutContext(qt.Qt.WidgetShortcut)
+
+ def _actionTriggered(self, checked=False):
+ _applyZoomToPlot(self.plot, 1.1)
+
+
+class ZoomOutAction(PlotAction):
+ """QAction performing a zoom-out on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ super(ZoomOutAction, self).__init__(
+ plot, icon='zoom-out', text='Zoom Out',
+ tooltip='Zoom out the plot',
+ triggered=self._actionTriggered,
+ checkable=False, parent=parent)
+ self.setShortcut(qt.QKeySequence.ZoomOut)
+ self.setShortcutContext(qt.Qt.WidgetShortcut)
+
+ def _actionTriggered(self, checked=False):
+ _applyZoomToPlot(self.plot, 1. / 1.1)
+
+
+class XAxisAutoScaleAction(PlotAction):
+ """QAction controlling X axis autoscale on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ super(XAxisAutoScaleAction, self).__init__(
+ plot, icon='plot-xauto', text='X Autoscale',
+ tooltip='Enable x-axis auto-scale when checked.\n'
+ 'If unchecked, x-axis does not change when reseting zoom.',
+ triggered=self._actionTriggered,
+ checkable=True, parent=parent)
+ self.setChecked(plot.getXAxis().isAutoScale())
+ plot.getXAxis().sigAutoScaleChanged.connect(self.setChecked)
+
+ def _actionTriggered(self, checked=False):
+ self.plot.getXAxis().setAutoScale(checked)
+ if checked:
+ self.plot.resetZoom()
+
+
+class YAxisAutoScaleAction(PlotAction):
+ """QAction controlling Y axis autoscale on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ super(YAxisAutoScaleAction, self).__init__(
+ plot, icon='plot-yauto', text='Y Autoscale',
+ tooltip='Enable y-axis auto-scale when checked.\n'
+ 'If unchecked, y-axis does not change when reseting zoom.',
+ triggered=self._actionTriggered,
+ checkable=True, parent=parent)
+ self.setChecked(plot.getYAxis().isAutoScale())
+ plot.getYAxis().sigAutoScaleChanged.connect(self.setChecked)
+
+ def _actionTriggered(self, checked=False):
+ self.plot.getYAxis().setAutoScale(checked)
+ if checked:
+ self.plot.resetZoom()
+
+
+class XAxisLogarithmicAction(PlotAction):
+ """QAction controlling X axis log scale on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ super(XAxisLogarithmicAction, self).__init__(
+ plot, icon='plot-xlog', text='X Log. scale',
+ tooltip='Logarithmic x-axis when checked',
+ triggered=self._actionTriggered,
+ checkable=True, parent=parent)
+ self.axis = plot.getXAxis()
+ self.setChecked(self.axis.getScale() == self.axis.LOGARITHMIC)
+ self.axis.sigScaleChanged.connect(self._setCheckedIfLogScale)
+
+ def _setCheckedIfLogScale(self, scale):
+ self.setChecked(scale == self.axis.LOGARITHMIC)
+
+ def _actionTriggered(self, checked=False):
+ scale = self.axis.LOGARITHMIC if checked else self.axis.LINEAR
+ self.axis.setScale(scale)
+
+
+class YAxisLogarithmicAction(PlotAction):
+ """QAction controlling Y axis log scale on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ super(YAxisLogarithmicAction, self).__init__(
+ plot, icon='plot-ylog', text='Y Log. scale',
+ tooltip='Logarithmic y-axis when checked',
+ triggered=self._actionTriggered,
+ checkable=True, parent=parent)
+ self.axis = plot.getYAxis()
+ self.setChecked(self.axis.getScale() == self.axis.LOGARITHMIC)
+ self.axis.sigScaleChanged.connect(self._setCheckedIfLogScale)
+
+ def _setCheckedIfLogScale(self, scale):
+ self.setChecked(scale == self.axis.LOGARITHMIC)
+
+ def _actionTriggered(self, checked=False):
+ scale = self.axis.LOGARITHMIC if checked else self.axis.LINEAR
+ self.axis.setScale(scale)
+
+
+class GridAction(PlotAction):
+ """QAction controlling grid mode on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param str gridMode: The grid mode to use in 'both', 'major'.
+ See :meth:`.PlotWidget.setGraphGrid`
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, gridMode='both', parent=None):
+ assert gridMode in ('both', 'major')
+ self._gridMode = gridMode
+
+ super(GridAction, self).__init__(
+ plot, icon='plot-grid', text='Grid',
+ tooltip='Toggle grid (on/off)',
+ triggered=self._actionTriggered,
+ checkable=True, parent=parent)
+ self.setChecked(plot.getGraphGrid() is not None)
+ plot.sigSetGraphGrid.connect(self._gridChanged)
+
+ def _gridChanged(self, which):
+ """Slot listening for PlotWidget grid mode change."""
+ self.setChecked(which != 'None')
+
+ def _actionTriggered(self, checked=False):
+ self.plot.setGraphGrid(self._gridMode if checked else None)
+
+
+class CurveStyleAction(PlotAction):
+ """QAction controlling curve style on a :class:`.PlotWidget`.
+
+ It changes the default line and markers style which updates all
+ curves on the plot.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ super(CurveStyleAction, self).__init__(
+ plot, icon='plot-toggle-points', text='Curve style',
+ tooltip='Change curve line and markers style',
+ triggered=self._actionTriggered,
+ checkable=False, parent=parent)
+
+ def _actionTriggered(self, checked=False):
+ currentState = (self.plot.isDefaultPlotLines(),
+ self.plot.isDefaultPlotPoints())
+
+ # line only, line and symbol, symbol only
+ states = (True, False), (True, True), (False, True)
+ newState = states[(states.index(currentState) + 1) % 3]
+
+ self.plot.setDefaultPlotLines(newState[0])
+ self.plot.setDefaultPlotPoints(newState[1])
+
+
+class ColormapAction(PlotAction):
+ """QAction opening a ColormapDialog to update the colormap.
+
+ Both the active image colormap and the default colormap are updated.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+ def __init__(self, plot, parent=None):
+ self._dialog = None # To store an instance of ColormapDialog
+ super(ColormapAction, self).__init__(
+ plot, icon='colormap', text='Colormap',
+ tooltip="Change colormap",
+ triggered=self._actionTriggered,
+ checkable=False, parent=parent)
+
+ def _actionTriggered(self, checked=False):
+ """Create a cmap dialog and update active image and default cmap."""
+ # Create the dialog if not already existing
+ if self._dialog is None:
+ self._dialog = ColormapDialog()
+
+ image = self.plot.getActiveImage()
+ if not isinstance(image, items.ColormapMixIn):
+ # No active image or active image is RGBA,
+ # set dialog from default info
+ colormap = self.plot.getDefaultColormap()
+
+ self._dialog.setHistogram() # Reset histogram and range if any
+
+ else:
+ # Set dialog from active image
+ colormap = image.getColormap()
+
+ data = image.getData(copy=False)
+
+ goodData = data[numpy.isfinite(data)]
+ if goodData.size > 0:
+ dataMin = goodData.min()
+ dataMax = goodData.max()
+ else:
+ qt.QMessageBox.warning(
+ None, "No Data",
+ "Image data does not contain any real value")
+ dataMin, dataMax = 1., 10.
+
+ self._dialog.setHistogram() # Reset histogram if any
+ self._dialog.setDataRange(dataMin, dataMax)
+ # The histogram should be done in a worker thread
+ # hist, bin_edges = numpy.histogram(goodData, bins=256)
+ # self._dialog.setHistogram(hist, bin_edges)
+
+ self._dialog.setColormap(name=colormap.getName(),
+ normalization=colormap.getNormalization(),
+ autoscale=colormap.isAutoscale(),
+ vmin=colormap.getVMin(),
+ vmax=colormap.getVMax(),
+ colors=colormap.getColormapLUT())
+
+ # Run the dialog listening to colormap change
+ self._dialog.sigColormapChanged.connect(self._colormapChanged)
+ result = self._dialog.exec_()
+ self._dialog.sigColormapChanged.disconnect(self._colormapChanged)
+
+ if not result: # Restore the previous colormap
+ self._colormapChanged(colormap)
+
+ def _colormapChanged(self, colormap):
+ # Update default colormap
+ self.plot.setDefaultColormap(colormap)
+
+ # Update active image colormap
+ activeImage = self.plot.getActiveImage()
+ if isinstance(activeImage, items.ColormapMixIn):
+ activeImage.setColormap(colormap)
+
+
+class KeepAspectRatioAction(PlotAction):
+ """QAction controlling aspect ratio on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ # Uses two images for checked/unchecked states
+ self._states = {
+ False: (icons.getQIcon('shape-circle-solid'),
+ "Keep data aspect ratio"),
+ True: (icons.getQIcon('shape-ellipse-solid'),
+ "Do no keep data aspect ratio")
+ }
+
+ icon, tooltip = self._states[plot.isKeepDataAspectRatio()]
+ super(KeepAspectRatioAction, self).__init__(
+ plot,
+ icon=icon,
+ text='Toggle keep aspect ratio',
+ tooltip=tooltip,
+ triggered=self._actionTriggered,
+ checkable=False,
+ parent=parent)
+ plot.sigSetKeepDataAspectRatio.connect(
+ self._keepDataAspectRatioChanged)
+
+ def _keepDataAspectRatioChanged(self, aspectRatio):
+ """Handle Plot set keep aspect ratio signal"""
+ icon, tooltip = self._states[aspectRatio]
+ self.setIcon(icon)
+ self.setToolTip(tooltip)
+
+ def _actionTriggered(self, checked=False):
+ # This will trigger _keepDataAspectRatioChanged
+ self.plot.setKeepDataAspectRatio(not self.plot.isKeepDataAspectRatio())
+
+
+class YAxisInvertedAction(PlotAction):
+ """QAction controlling Y orientation on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ # Uses two images for checked/unchecked states
+ self._states = {
+ False: (icons.getQIcon('plot-ydown'),
+ "Orient Y axis downward"),
+ True: (icons.getQIcon('plot-yup'),
+ "Orient Y axis upward"),
+ }
+
+ icon, tooltip = self._states[plot.getYAxis().isInverted()]
+ super(YAxisInvertedAction, self).__init__(
+ plot,
+ icon=icon,
+ text='Invert Y Axis',
+ tooltip=tooltip,
+ triggered=self._actionTriggered,
+ checkable=False,
+ parent=parent)
+ plot.getYAxis().sigInvertedChanged.connect(self._yAxisInvertedChanged)
+
+ def _yAxisInvertedChanged(self, inverted):
+ """Handle Plot set y axis inverted signal"""
+ icon, tooltip = self._states[inverted]
+ self.setIcon(icon)
+ self.setToolTip(tooltip)
+
+ def _actionTriggered(self, checked=False):
+ # This will trigger _yAxisInvertedChanged
+ yAxis = self.plot.getYAxis()
+ yAxis.setInverted(not yAxis.isInverted())
+
+
+class CrosshairAction(PlotAction):
+ """QAction toggling crosshair cursor on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param str color: Color to use to draw the crosshair
+ :param int linewidth: Width of the crosshair cursor
+ :param str linestyle: Style of line. See :meth:`.Plot.setGraphCursor`
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, color='black', linewidth=1, linestyle='-',
+ parent=None):
+ self.color = color
+ """Color used to draw the crosshair (str)."""
+
+ self.linewidth = linewidth
+ """Width of the crosshair cursor (int)."""
+
+ self.linestyle = linestyle
+ """Style of line of the cursor (str)."""
+
+ super(CrosshairAction, self).__init__(
+ plot, icon='crosshair', text='Crosshair Cursor',
+ tooltip='Enable crosshair cursor when checked',
+ triggered=self._actionTriggered,
+ checkable=True, parent=parent)
+ self.setChecked(plot.getGraphCursor() is not None)
+ plot.sigSetGraphCursor.connect(self.setChecked)
+
+ def _actionTriggered(self, checked=False):
+ self.plot.setGraphCursor(checked,
+ color=self.color,
+ linestyle=self.linestyle,
+ linewidth=self.linewidth)
+
+
+class PanWithArrowKeysAction(PlotAction):
+ """QAction toggling pan with arrow keys on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+
+ super(PanWithArrowKeysAction, self).__init__(
+ plot, icon='arrow-keys', text='Pan with arrow keys',
+ tooltip='Enable pan with arrow keys when checked',
+ triggered=self._actionTriggered,
+ checkable=True, parent=parent)
+ self.setChecked(plot.isPanWithArrowKeys())
+ plot.sigSetPanWithArrowKeys.connect(self.setChecked)
+
+ def _actionTriggered(self, checked=False):
+ self.plot.setPanWithArrowKeys(checked)
+
+
+class ShowAxisAction(PlotAction):
+ """QAction controlling axis visibility on a :class:`.PlotWidget`.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ tooltip = 'Show plot axis when checked, otherwise hide them'
+ PlotAction.__init__(self,
+ plot,
+ icon='axis',
+ text='show axis',
+ tooltip=tooltip,
+ triggered=self._actionTriggered,
+ checkable=True,
+ parent=parent)
+ self.setChecked(self.plot._backend.isAxesDisplayed())
+ plot._sigAxesVisibilityChanged.connect(self.setChecked)
+
+ def _actionTriggered(self, checked=False):
+ self.plot.setAxesDisplayed(checked)
diff --git a/silx/gui/plot/actions/fit.py b/silx/gui/plot/actions/fit.py
new file mode 100644
index 0000000..d7256ab
--- /dev/null
+++ b/silx/gui/plot/actions/fit.py
@@ -0,0 +1,189 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-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.
+#
+# ###########################################################################*/
+"""
+:mod:`silx.gui.plot.actions.fit` module provides actions relative to fit.
+
+The following QAction are available:
+
+- :class:`.FitAction`
+
+.. autoclass:`.FitAction`
+"""
+
+from __future__ import division
+
+__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
+__license__ = "MIT"
+__date__ = "28/06/2017"
+
+from . import PlotAction
+import logging
+from silx.gui import qt
+from silx.gui.plot.ItemsSelectionDialog import ItemsSelectionDialog
+from silx.gui.plot.items import Curve, Histogram
+
+_logger = logging.getLogger(__name__)
+
+
+def _getUniqueCurve(plt):
+ """Get a single curve from the plot.
+ Get the active curve if any, else if a single curve is plotted
+ get it, else return None.
+
+ :param plt: :class:`.PlotWidget` instance on which to operate
+
+ :return: return value of plt.getActiveCurve(), or plt.getAllCurves()[0],
+ or None
+ """
+ curve = plt.getActiveCurve()
+ if curve is not None:
+ return curve
+
+ curves = plt.getAllCurves()
+ if len(curves) == 0:
+ return None
+
+ if len(curves) == 1 and len(plt._getItems(kind='histogram')) == 0:
+ return curves[0]
+
+ return None
+
+
+def _getUniqueHistogram(plt):
+ """Return the histogram if there is a single histogram and no curve in
+ the plot. In all other cases, return None.
+
+ :param plt: :class:`.PlotWidget` instance on which to operate
+ :return: histogram or None
+ """
+ histograms = plt._getItems(kind='histogram')
+ if len(histograms) != 1:
+ return None
+ if plt.getAllCurves(just_legend=True):
+ return None
+ return histograms[0]
+
+
+class FitAction(PlotAction):
+ """QAction to open a :class:`FitWidget` and set its data to the
+ active curve if any, or to the first curve.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+ def __init__(self, plot, parent=None):
+ super(FitAction, self).__init__(
+ plot, icon='math-fit', text='Fit curve',
+ tooltip='Open a fit dialog',
+ triggered=self._getFitWindow,
+ checkable=False, parent=parent)
+ self.fit_window = None
+
+ def _getFitWindow(self):
+ self.xlabel = self.plot.getXAxis().getLabel()
+ self.ylabel = self.plot.getYAxis().getLabel()
+ self.xmin, self.xmax = self.plot.getXAxis().getLimits()
+
+ histo = _getUniqueHistogram(self.plot)
+ curve = _getUniqueCurve(self.plot)
+
+ if histo is None and curve is None:
+ # ambiguous case, we need to ask which plot item to fit
+ isd = ItemsSelectionDialog(plot=self.plot)
+ isd.setWindowTitle("Select item to be fitted")
+ isd.setItemsSelectionMode(qt.QTableWidget.SingleSelection)
+ isd.setAvailableKinds(["curve", "histogram"])
+ isd.selectAllKinds()
+
+ result = isd.exec_()
+ if result and len(isd.getSelectedItems()) == 1:
+ item = isd.getSelectedItems()[0]
+ else:
+ return
+ elif histo is not None:
+ # presence of a unique histo and no curve
+ item = histo
+ elif curve is not None:
+ # presence of a unique or active curve
+ item = curve
+
+ self.legend = item.getLegend()
+
+ if isinstance(item, Histogram):
+ bin_edges = item.getBinEdgesData(copy=False)
+ # take the middle coordinate between adjacent bin edges
+ self.x = (bin_edges[1:] + bin_edges[:-1]) / 2
+ self.y = item.getValueData(copy=False)
+ # else take the active curve, or else the unique curve
+ elif isinstance(item, Curve):
+ self.x = item.getXData(copy=False)
+ self.y = item.getYData(copy=False)
+
+ # open a window with a FitWidget
+ if self.fit_window is None:
+ self.fit_window = qt.QMainWindow()
+ # import done here rather than at module level to avoid circular import
+ # FitWidget -> BackgroundWidget -> PlotWindow -> actions -> fit -> FitWidget
+ from ...fit.FitWidget import FitWidget
+ self.fit_widget = FitWidget(parent=self.fit_window)
+ self.fit_window.setCentralWidget(
+ self.fit_widget)
+ self.fit_widget.guibuttons.DismissButton.clicked.connect(
+ self.fit_window.close)
+ self.fit_widget.sigFitWidgetSignal.connect(
+ self.handle_signal)
+ self.fit_window.show()
+ else:
+ if self.fit_window.isHidden():
+ self.fit_window.show()
+ self.fit_widget.show()
+ self.fit_window.raise_()
+
+ self.fit_widget.setData(self.x, self.y,
+ xmin=self.xmin, xmax=self.xmax)
+ self.fit_window.setWindowTitle(
+ "Fitting " + self.legend +
+ " on x range %f-%f" % (self.xmin, self.xmax))
+
+ def handle_signal(self, ddict):
+ x_fit = self.x[self.xmin <= self.x]
+ x_fit = x_fit[x_fit <= self.xmax]
+ fit_legend = "Fit <%s>" % self.legend
+ fit_curve = self.plot.getCurve(fit_legend)
+
+ if ddict["event"] == "FitFinished":
+ y_fit = self.fit_widget.fitmanager.gendata()
+ if fit_curve is None:
+ self.plot.addCurve(x_fit, y_fit,
+ fit_legend,
+ xlabel=self.xlabel, ylabel=self.ylabel,
+ resetzoom=False)
+ else:
+ fit_curve.setData(x_fit, y_fit)
+ fit_curve.setVisible(True)
+
+ if ddict["event"] in ["FitStarted", "FitFailed"]:
+ if fit_curve is not None:
+ fit_curve.setVisible(False)
diff --git a/silx/gui/plot/actions/histogram.py b/silx/gui/plot/actions/histogram.py
new file mode 100644
index 0000000..a4a91e9
--- /dev/null
+++ b/silx/gui/plot/actions/histogram.py
@@ -0,0 +1,170 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-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.
+#
+# ###########################################################################*/
+"""
+:mod:`silx.gui.plot.actions.histogram` provides actions relative to histograms
+for :class:`.PlotWidget`.
+
+The following QAction are available:
+
+- :class:`PixelIntensitiesHistoAction`
+"""
+
+from __future__ import division
+
+__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
+__date__ = "27/06/2017"
+__license__ = "MIT"
+
+from . import PlotAction
+from silx.math.histogram import Histogramnd
+import numpy
+import logging
+from silx.gui import qt
+
+_logger = logging.getLogger(__name__)
+
+
+class PixelIntensitiesHistoAction(PlotAction):
+ """QAction to plot the pixels intensities diagram
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ PlotAction.__init__(self,
+ plot,
+ icon='pixel-intensities',
+ text='pixels intensity',
+ tooltip='Compute image intensity distribution',
+ triggered=self._triggered,
+ parent=parent,
+ checkable=True)
+ self._plotHistogram = None
+ self._connectedToActiveImage = False
+ self._histo = None
+
+ def _triggered(self, checked):
+ """Update the plot of the histogram visibility status
+
+ :param bool checked: status of the action button
+ """
+ if checked:
+ if not self._connectedToActiveImage:
+ self.plot.sigActiveImageChanged.connect(
+ self._activeImageChanged)
+ self._connectedToActiveImage = True
+ self.computeIntensityDistribution()
+
+ self.getHistogramPlotWidget().show()
+
+ else:
+ if self._connectedToActiveImage:
+ self.plot.sigActiveImageChanged.disconnect(
+ self._activeImageChanged)
+ self._connectedToActiveImage = False
+
+ self.getHistogramPlotWidget().hide()
+
+ def _activeImageChanged(self, previous, legend):
+ """Handle active image change: toggle enabled toolbar, update curve"""
+ if self.isChecked():
+ self.computeIntensityDistribution()
+
+ def computeIntensityDistribution(self):
+ """Get the active image and compute the image intensity distribution
+ """
+ activeImage = self.plot.getActiveImage()
+
+ if activeImage is not None:
+ image = activeImage.getData(copy=False)
+ if image.ndim == 3: # RGB(A) images
+ _logger.info('Converting current image from RGB(A) to grayscale\
+ in order to compute the intensity distribution')
+ image = (image[:, :, 0] * 0.299 +
+ image[:, :, 1] * 0.587 +
+ image[:, :, 2] * 0.114)
+
+ xmin = numpy.nanmin(image)
+ xmax = numpy.nanmax(image)
+ nbins = min(1024, int(numpy.sqrt(image.size)))
+ data_range = xmin, xmax
+
+ # bad hack: get 256 bins in the case we have a B&W
+ if numpy.issubdtype(image.dtype, numpy.integer):
+ if nbins > xmax - xmin:
+ nbins = xmax - xmin
+
+ nbins = max(2, nbins)
+
+ data = image.ravel().astype(numpy.float32)
+ histogram = Histogramnd(data, n_bins=nbins, histo_range=data_range)
+ assert len(histogram.edges) == 1
+ self._histo = histogram.histo
+ edges = histogram.edges[0]
+ plot = self.getHistogramPlotWidget()
+ plot.addHistogram(histogram=self._histo,
+ edges=edges,
+ legend='pixel intensity',
+ fill=True,
+ color='red')
+ plot.resetZoom()
+
+ def eventFilter(self, qobject, event):
+ """Observe when the close event is emitted then
+ simply uncheck the action button
+
+ :param qobject: the object observe
+ :param event: the event received by qobject
+ """
+ if event.type() == qt.QEvent.Close:
+ if self._plotHistogram is not None:
+ self._plotHistogram.hide()
+ self.setChecked(False)
+
+ return PlotAction.eventFilter(self, qobject, event)
+
+ def getHistogramPlotWidget(self):
+ """Create the plot histogram if needed, otherwise create it
+
+ :return: the PlotWidget showing the histogram of the pixel intensities
+ """
+ from silx.gui.plot.PlotWindow import Plot1D
+ if self._plotHistogram is None:
+ self._plotHistogram = Plot1D(parent=self.plot)
+ self._plotHistogram.setWindowFlags(qt.Qt.Window)
+ self._plotHistogram.setWindowTitle('Image Intensity Histogram')
+ self._plotHistogram.installEventFilter(self)
+ self._plotHistogram.getXAxis().setLabel("Value")
+ self._plotHistogram.getYAxis().setLabel("Count")
+
+ return self._plotHistogram
+
+ def getHistogram(self):
+ """Return the last computed histogram
+
+ :return: the histogram displayed in the HistogramPlotWiget
+ """
+ return self._histo
diff --git a/silx/gui/plot/actions/io.py b/silx/gui/plot/actions/io.py
new file mode 100644
index 0000000..50410e3
--- /dev/null
+++ b/silx/gui/plot/actions/io.py
@@ -0,0 +1,538 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-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.
+#
+# ###########################################################################*/
+"""
+:mod:`silx.gui.plot.actions.io` provides a set of QAction relative of inputs
+and outputs for a :class:`.PlotWidget`.
+
+The following QAction are available:
+
+- :class:`CopyAction`
+- :class:`PrintAction`
+- :class:`SaveAction`
+"""
+
+from __future__ import division
+
+__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
+__license__ = "MIT"
+__date__ = "27/06/2017"
+
+from . import PlotAction
+from silx.io.utils import save1D, savespec
+import logging
+import sys
+from collections import OrderedDict
+import traceback
+import numpy
+from silx.gui import qt
+from silx.third_party.EdfFile import EdfFile
+from silx.third_party.TiffIO import TiffIO
+from silx.gui._utils import convertArrayToQImage
+if sys.version_info[0] == 3:
+ from io import BytesIO
+else:
+ import cStringIO as _StringIO
+ BytesIO = _StringIO.StringIO
+
+_logger = logging.getLogger(__name__)
+
+
+class SaveAction(PlotAction):
+ """QAction for saving Plot content.
+
+ It opens a Save as... dialog.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate.
+ :param parent: See :class:`QAction`.
+ """
+ # TODO find a way to make the filter list selectable and extensible
+
+ SNAPSHOT_FILTER_SVG = 'Plot Snapshot as SVG (*.svg)'
+ SNAPSHOT_FILTER_PNG = 'Plot Snapshot as PNG (*.png)'
+
+ SNAPSHOT_FILTERS = (SNAPSHOT_FILTER_PNG, SNAPSHOT_FILTER_SVG)
+
+ # Dict of curve filters with CSV-like format
+ # Using ordered dict to guarantee filters order
+ # Note: '%.18e' is numpy.savetxt default format
+ CURVE_FILTERS_TXT = OrderedDict((
+ ('Curve as Raw ASCII (*.txt)',
+ {'fmt': '%.18e', 'delimiter': ' ', 'header': False}),
+ ('Curve as ";"-separated CSV (*.csv)',
+ {'fmt': '%.18e', 'delimiter': ';', 'header': True}),
+ ('Curve as ","-separated CSV (*.csv)',
+ {'fmt': '%.18e', 'delimiter': ',', 'header': True}),
+ ('Curve as tab-separated CSV (*.csv)',
+ {'fmt': '%.18e', 'delimiter': '\t', 'header': True}),
+ ('Curve as OMNIC CSV (*.csv)',
+ {'fmt': '%.7E', 'delimiter': ',', 'header': False}),
+ ('Curve as SpecFile (*.dat)',
+ {'fmt': '%.7g', 'delimiter': '', 'header': False})
+ ))
+
+ CURVE_FILTER_NPY = 'Curve as NumPy binary file (*.npy)'
+
+ CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [CURVE_FILTER_NPY]
+
+ ALL_CURVES_FILTERS = ("All curves as SpecFile (*.dat)", )
+
+ IMAGE_FILTER_EDF = 'Image data as EDF (*.edf)'
+ IMAGE_FILTER_TIFF = 'Image data as TIFF (*.tif)'
+ IMAGE_FILTER_NUMPY = 'Image data as NumPy binary file (*.npy)'
+ IMAGE_FILTER_ASCII = 'Image data as ASCII (*.dat)'
+ IMAGE_FILTER_CSV_COMMA = 'Image data as ,-separated CSV (*.csv)'
+ IMAGE_FILTER_CSV_SEMICOLON = 'Image data as ;-separated CSV (*.csv)'
+ IMAGE_FILTER_CSV_TAB = 'Image data as tab-separated CSV (*.csv)'
+ IMAGE_FILTER_RGB_PNG = 'Image as PNG (*.png)'
+ IMAGE_FILTER_RGB_TIFF = 'Image as TIFF (*.tif)'
+ IMAGE_FILTERS = (IMAGE_FILTER_EDF,
+ IMAGE_FILTER_TIFF,
+ IMAGE_FILTER_NUMPY,
+ IMAGE_FILTER_ASCII,
+ IMAGE_FILTER_CSV_COMMA,
+ IMAGE_FILTER_CSV_SEMICOLON,
+ IMAGE_FILTER_CSV_TAB,
+ IMAGE_FILTER_RGB_PNG,
+ IMAGE_FILTER_RGB_TIFF)
+
+ def __init__(self, plot, parent=None):
+ super(SaveAction, self).__init__(
+ plot, icon='document-save', text='Save as...',
+ tooltip='Save curve/image/plot snapshot dialog',
+ triggered=self._actionTriggered,
+ checkable=False, parent=parent)
+ self.setShortcut(qt.QKeySequence.Save)
+ self.setShortcutContext(qt.Qt.WidgetShortcut)
+
+ def _errorMessage(self, informativeText=''):
+ """Display an error message."""
+ # TODO issue with QMessageBox size fixed and too small
+ msg = qt.QMessageBox(self.plot)
+ msg.setIcon(qt.QMessageBox.Critical)
+ msg.setInformativeText(informativeText + ' ' + str(sys.exc_info()[1]))
+ msg.setDetailedText(traceback.format_exc())
+ msg.exec_()
+
+ def _saveSnapshot(self, filename, nameFilter):
+ """Save a snapshot of the :class:`PlotWindow` widget.
+
+ :param str filename: The name of the file to write
+ :param str nameFilter: The selected name filter
+ :return: False if format is not supported or save failed,
+ True otherwise.
+ """
+ if nameFilter == self.SNAPSHOT_FILTER_PNG:
+ fileFormat = 'png'
+ elif nameFilter == self.SNAPSHOT_FILTER_SVG:
+ fileFormat = 'svg'
+ else: # Format not supported
+ _logger.error(
+ 'Saving plot snapshot failed: format not supported')
+ return False
+
+ self.plot.saveGraph(filename, fileFormat=fileFormat)
+ return True
+
+ def _saveCurve(self, filename, nameFilter):
+ """Save a curve from the plot.
+
+ :param str filename: The name of the file to write
+ :param str nameFilter: The selected name filter
+ :return: False if format is not supported or save failed,
+ True otherwise.
+ """
+ if nameFilter not in self.CURVE_FILTERS:
+ return False
+
+ # Check if a curve is to be saved
+ curve = self.plot.getActiveCurve()
+ # before calling _saveCurve, if there is no selected curve, we
+ # make sure there is only one curve on the graph
+ if curve is None:
+ curves = self.plot.getAllCurves()
+ if not curves:
+ self._errorMessage("No curve to be saved")
+ return False
+ curve = curves[0]
+
+ if nameFilter in self.CURVE_FILTERS_TXT:
+ filter_ = self.CURVE_FILTERS_TXT[nameFilter]
+ fmt = filter_['fmt']
+ csvdelim = filter_['delimiter']
+ autoheader = filter_['header']
+ else:
+ # .npy
+ fmt, csvdelim, autoheader = ("", "", False)
+
+ # If curve has no associated label, get the default from the plot
+ xlabel = curve.getXLabel()
+ if xlabel is None:
+ xlabel = self.plot.getXAxis().getLabel()
+ ylabel = curve.getYLabel()
+ if ylabel is None:
+ ylabel = self.plot.getYAxis().getLabel()
+
+ try:
+ save1D(filename,
+ curve.getXData(copy=False),
+ curve.getYData(copy=False),
+ xlabel, [ylabel],
+ fmt=fmt, csvdelim=csvdelim,
+ autoheader=autoheader)
+ except IOError:
+ self._errorMessage('Save failed\n')
+ return False
+
+ return True
+
+ def _saveCurves(self, filename, nameFilter):
+ """Save all curves from the plot.
+
+ :param str filename: The name of the file to write
+ :param str nameFilter: The selected name filter
+ :return: False if format is not supported or save failed,
+ True otherwise.
+ """
+ if nameFilter not in self.ALL_CURVES_FILTERS:
+ return False
+
+ curves = self.plot.getAllCurves()
+ if not curves:
+ self._errorMessage("No curves to be saved")
+ return False
+
+ curve = curves[0]
+ scanno = 1
+ try:
+ specfile = savespec(filename,
+ curve.getXData(copy=False),
+ curve.getYData(copy=False),
+ curve.getXLabel(),
+ curve.getYLabel(),
+ fmt="%.7g", scan_number=1, mode="w",
+ write_file_header=True,
+ close_file=False)
+ except IOError:
+ self._errorMessage('Save failed\n')
+ return False
+
+ for curve in curves[1:]:
+ try:
+ scanno += 1
+ specfile = savespec(specfile,
+ curve.getXData(copy=False),
+ curve.getYData(copy=False),
+ curve.getXLabel(),
+ curve.getYLabel(),
+ fmt="%.7g", scan_number=scanno, mode="w",
+ write_file_header=False,
+ close_file=False)
+ except IOError:
+ self._errorMessage('Save failed\n')
+ return False
+ specfile.close()
+
+ return True
+
+ def _saveImage(self, filename, nameFilter):
+ """Save an image from the plot.
+
+ :param str filename: The name of the file to write
+ :param str nameFilter: The selected name filter
+ :return: False if format is not supported or save failed,
+ True otherwise.
+ """
+ if nameFilter not in self.IMAGE_FILTERS:
+ return False
+
+ image = self.plot.getActiveImage()
+ if image is None:
+ qt.QMessageBox.warning(
+ self.plot, "No Data", "No image to be saved")
+ return False
+
+ data = image.getData(copy=False)
+
+ # TODO Use silx.io for writing files
+ if nameFilter == self.IMAGE_FILTER_EDF:
+ edfFile = EdfFile(filename, access="w+")
+ edfFile.WriteImage({}, data, Append=0)
+ return True
+
+ elif nameFilter == self.IMAGE_FILTER_TIFF:
+ tiffFile = TiffIO(filename, mode='w')
+ tiffFile.writeImage(data, software='silx')
+ return True
+
+ elif nameFilter == self.IMAGE_FILTER_NUMPY:
+ try:
+ numpy.save(filename, data)
+ except IOError:
+ self._errorMessage('Save failed\n')
+ return False
+ return True
+
+ elif nameFilter in (self.IMAGE_FILTER_ASCII,
+ self.IMAGE_FILTER_CSV_COMMA,
+ self.IMAGE_FILTER_CSV_SEMICOLON,
+ self.IMAGE_FILTER_CSV_TAB):
+ csvdelim, filetype = {
+ self.IMAGE_FILTER_ASCII: (' ', 'txt'),
+ self.IMAGE_FILTER_CSV_COMMA: (',', 'csv'),
+ self.IMAGE_FILTER_CSV_SEMICOLON: (';', 'csv'),
+ self.IMAGE_FILTER_CSV_TAB: ('\t', 'csv'),
+ }[nameFilter]
+
+ height, width = data.shape
+ rows, cols = numpy.mgrid[0:height, 0:width]
+ try:
+ save1D(filename, rows.ravel(), (cols.ravel(), data.ravel()),
+ filetype=filetype,
+ xlabel='row',
+ ylabels=['column', 'value'],
+ csvdelim=csvdelim,
+ autoheader=True)
+
+ except IOError:
+ self._errorMessage('Save failed\n')
+ return False
+ return True
+
+ elif nameFilter in (self.IMAGE_FILTER_RGB_PNG,
+ self.IMAGE_FILTER_RGB_TIFF):
+ # Get displayed image
+ rgbaImage = image.getRgbaImageData(copy=False)
+ # Convert RGB QImage
+ qimage = convertArrayToQImage(rgbaImage[:, :, :3])
+
+ if nameFilter == self.IMAGE_FILTER_RGB_PNG:
+ fileFormat = 'PNG'
+ else:
+ fileFormat = 'TIFF'
+
+ if qimage.save(filename, fileFormat):
+ return True
+ else:
+ _logger.error('Failed to save image as %s', filename)
+ qt.QMessageBox.critical(
+ self.parent(),
+ 'Save image as',
+ 'Failed to save image')
+
+ return False
+
+ def _actionTriggered(self, checked=False):
+ """Handle save action."""
+ # Set-up filters
+ filters = []
+
+ # Add image filters if there is an active image
+ if self.plot.getActiveImage() is not None:
+ filters.extend(self.IMAGE_FILTERS)
+
+ # Add curve filters if there is a curve to save
+ if (self.plot.getActiveCurve() is not None or
+ len(self.plot.getAllCurves()) == 1):
+ filters.extend(self.CURVE_FILTERS)
+ if len(self.plot.getAllCurves()) > 1:
+ filters.extend(self.ALL_CURVES_FILTERS)
+
+ filters.extend(self.SNAPSHOT_FILTERS)
+
+ # Create and run File dialog
+ dialog = qt.QFileDialog(self.plot)
+ dialog.setWindowTitle("Output File Selection")
+ dialog.setModal(1)
+ dialog.setNameFilters(filters)
+
+ dialog.setFileMode(dialog.AnyFile)
+ dialog.setAcceptMode(dialog.AcceptSave)
+
+ if not dialog.exec_():
+ return False
+
+ nameFilter = dialog.selectedNameFilter()
+ filename = dialog.selectedFiles()[0]
+ dialog.close()
+
+ # Forces the filename extension to match the chosen filter
+ extension = nameFilter.split()[-1][2:-1]
+ if (len(filename) <= len(extension) or
+ filename[-len(extension):].lower() != extension.lower()):
+ filename += extension
+
+ # Handle save
+ if nameFilter in self.SNAPSHOT_FILTERS:
+ return self._saveSnapshot(filename, nameFilter)
+ elif nameFilter in self.CURVE_FILTERS:
+ return self._saveCurve(filename, nameFilter)
+ elif nameFilter in self.ALL_CURVES_FILTERS:
+ return self._saveCurves(filename, nameFilter)
+ elif nameFilter in self.IMAGE_FILTERS:
+ return self._saveImage(filename, nameFilter)
+ else:
+ _logger.warning('Unsupported file filter: %s', nameFilter)
+ return False
+
+
+def _plotAsPNG(plot):
+ """Save a :class:`Plot` as PNG and return the payload.
+
+ :param plot: The :class:`Plot` to save
+ """
+ pngFile = BytesIO()
+ plot.saveGraph(pngFile, fileFormat='png')
+ pngFile.flush()
+ pngFile.seek(0)
+ data = pngFile.read()
+ pngFile.close()
+ return data
+
+
+class PrintAction(PlotAction):
+ """QAction for printing the plot.
+
+ It opens a Print dialog.
+
+ Current implementation print a bitmap of the plot area and not vector
+ graphics, so printing quality is not great.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate.
+ :param parent: See :class:`QAction`.
+ """
+
+ # Share QPrinter instance to propose latest used as default
+ _printer = None
+
+ def __init__(self, plot, parent=None):
+ super(PrintAction, self).__init__(
+ plot, icon='document-print', text='Print...',
+ tooltip='Open print dialog',
+ triggered=self.printPlot,
+ checkable=False, parent=parent)
+ self.setShortcut(qt.QKeySequence.Print)
+ self.setShortcutContext(qt.Qt.WidgetShortcut)
+
+ @property
+ def printer(self):
+ """The QPrinter instance used by the actions.
+
+ This is shared accross all instances of PrintAct
+ """
+ if self._printer is None:
+ PrintAction._printer = qt.QPrinter()
+ return self._printer
+
+ def printPlotAsWidget(self):
+ """Open the print dialog and print the plot.
+
+ Use :meth:`QWidget.render` to print the plot
+
+ :return: True if successful
+ """
+ dialog = qt.QPrintDialog(self.printer, self.plot)
+ dialog.setWindowTitle('Print Plot')
+ if not dialog.exec_():
+ return False
+
+ # Print a snapshot of the plot widget at the top of the page
+ widget = self.plot.centralWidget()
+
+ painter = qt.QPainter()
+ if not painter.begin(self.printer):
+ return False
+
+ pageRect = self.printer.pageRect()
+ xScale = pageRect.width() / widget.width()
+ yScale = pageRect.height() / widget.height()
+ scale = min(xScale, yScale)
+
+ painter.translate(pageRect.width() / 2., 0.)
+ painter.scale(scale, scale)
+ painter.translate(-widget.width() / 2., 0.)
+ widget.render(painter)
+ painter.end()
+
+ return True
+
+ def printPlot(self):
+ """Open the print dialog and print the plot.
+
+ Use :meth:`Plot.saveGraph` to print the plot.
+
+ :return: True if successful
+ """
+ # Init printer and start printer dialog
+ dialog = qt.QPrintDialog(self.printer, self.plot)
+ dialog.setWindowTitle('Print Plot')
+ if not dialog.exec_():
+ return False
+
+ # Save Plot as PNG and make a pixmap from it with default dpi
+ pngData = _plotAsPNG(self.plot)
+
+ pixmap = qt.QPixmap()
+ pixmap.loadFromData(pngData, 'png')
+
+ xScale = self.printer.pageRect().width() / pixmap.width()
+ yScale = self.printer.pageRect().height() / pixmap.height()
+ scale = min(xScale, yScale)
+
+ # Draw pixmap with painter
+ painter = qt.QPainter()
+ if not painter.begin(self.printer):
+ return False
+
+ painter.drawPixmap(0, 0,
+ pixmap.width() * scale,
+ pixmap.height() * scale,
+ pixmap)
+ painter.end()
+
+ return True
+
+
+class CopyAction(PlotAction):
+ """QAction to copy :class:`.PlotWidget` content to clipboard.
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ super(CopyAction, self).__init__(
+ plot, icon='edit-copy', text='Copy plot',
+ tooltip='Copy a snapshot of the plot into the clipboard',
+ triggered=self.copyPlot,
+ checkable=False, parent=parent)
+ self.setShortcut(qt.QKeySequence.Copy)
+ self.setShortcutContext(qt.Qt.WidgetShortcut)
+
+ def copyPlot(self):
+ """Copy plot content to the clipboard as a bitmap."""
+ # Save Plot as PNG and make a QImage from it with default dpi
+ pngData = _plotAsPNG(self.plot)
+ image = qt.QImage.fromData(pngData, 'png')
+ qt.QApplication.clipboard().setImage(image)
diff --git a/silx/gui/plot/actions/medfilt.py b/silx/gui/plot/actions/medfilt.py
new file mode 100644
index 0000000..3305d1b
--- /dev/null
+++ b/silx/gui/plot/actions/medfilt.py
@@ -0,0 +1,145 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-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.
+#
+# ###########################################################################*/
+"""
+:mod:`silx.gui.plot.actions.medfilt` provides a set of QAction to apply filter
+on data contained in a :class:`.PlotWidget`.
+
+The following QAction are available:
+
+- :class:`MedianFilterAction`
+- :class:`MedianFilter1DAction`
+- :class:`MedianFilter2DAction`
+
+"""
+
+from __future__ import division
+
+__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
+__license__ = "MIT"
+
+__date__ = "24/05/2017"
+
+from . import PlotAction
+from silx.gui.widgets.MedianFilterDialog import MedianFilterDialog
+from silx.math.medianfilter import medfilt2d
+import logging
+
+_logger = logging.getLogger(__name__)
+
+
+class MedianFilterAction(PlotAction):
+ """QAction to plot the pixels intensities diagram
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+
+ def __init__(self, plot, parent=None):
+ PlotAction.__init__(self,
+ plot,
+ icon='median-filter',
+ text='median filter',
+ tooltip='Apply a median filter on the image',
+ triggered=self._triggered,
+ parent=parent)
+ self._originalImage = None
+ self._legend = None
+ self._filteredImage = None
+ self._popup = MedianFilterDialog(parent=None)
+ self._popup.sigFilterOptChanged.connect(self._updateFilter)
+ self.plot.sigActiveImageChanged.connect(self._updateActiveImage)
+ self._updateActiveImage()
+
+ def _triggered(self, checked):
+ """Update the plot of the histogram visibility status
+
+ :param bool checked: status of the action button
+ """
+ self._popup.show()
+
+ def _updateActiveImage(self):
+ """Set _activeImageLegend and _originalImage from the active image"""
+ self._activeImageLegend = self.plot.getActiveImage(just_legend=True)
+ if self._activeImageLegend is None:
+ self._originalImage = None
+ self._legend = None
+ else:
+ self._originalImage = self.plot.getImage(self._activeImageLegend).getData(copy=False)
+ self._legend = self.plot.getImage(self._activeImageLegend).getLegend()
+
+ def _updateFilter(self, kernelWidth, conditional=False):
+ if self._originalImage is None:
+ return
+
+ self.plot.sigActiveImageChanged.disconnect(self._updateActiveImage)
+ filteredImage = self._computeFilteredImage(kernelWidth, conditional)
+ self.plot.addImage(data=filteredImage,
+ legend=self._legend,
+ replace=True)
+ self.plot.sigActiveImageChanged.connect(self._updateActiveImage)
+
+ def _computeFilteredImage(self, kernelWidth, conditional):
+ raise NotImplemented('MedianFilterAction is a an abstract class')
+
+ def getFilteredImage(self):
+ """
+ :return: the image with the median filter apply on"""
+ return self._filteredImage
+
+
+class MedianFilter1DAction(MedianFilterAction):
+ """Define the MedianFilterAction for 1D
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+ def __init__(self, plot, parent=None):
+ MedianFilterAction.__init__(self,
+ plot,
+ parent=parent)
+
+ def _computeFilteredImage(self, kernelWidth, conditional):
+ assert(self.plot is not None)
+ return medfilt2d(self._originalImage,
+ (kernelWidth, 1),
+ conditional)
+
+
+class MedianFilter2DAction(MedianFilterAction):
+ """Define the MedianFilterAction for 2D
+
+ :param plot: :class:`.PlotWidget` instance on which to operate
+ :param parent: See :class:`QAction`
+ """
+ def __init__(self, plot, parent=None):
+ MedianFilterAction.__init__(self,
+ plot,
+ parent=parent)
+
+ def _computeFilteredImage(self, kernelWidth, conditional):
+ assert(self.plot is not None)
+ return medfilt2d(self._originalImage,
+ (kernelWidth, kernelWidth),
+ conditional)
diff --git a/silx/gui/plot/actions/mode.py b/silx/gui/plot/actions/mode.py
new file mode 100644
index 0000000..026a94d
--- /dev/null
+++ b/silx/gui/plot/actions/mode.py
@@ -0,0 +1,100 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (c) 2004-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.
+#
+# ###########################################################################*/
+"""
+:mod:`silx.gui.plot.actions.mode` provides a set of QAction relative to mouse
+mode