summaryrefslogtreecommitdiff
path: root/silx/gui/plot
diff options
context:
space:
mode:
authorPicca Frédéric-Emmanuel <picca@debian.org>2022-02-02 14:19:58 +0100
committerPicca Frédéric-Emmanuel <picca@debian.org>2022-02-02 14:19:58 +0100
commit4e774db12d5ebe7a20eded6dd434a289e27999e5 (patch)
treea9822974ba45196f1e3740995ab157d6eb214a04 /silx/gui/plot
parentd3194b1a9c4404ba93afac43d97172ab24c57098 (diff)
New upstream version 1.0.0+dfsg
Diffstat (limited to 'silx/gui/plot')
-rw-r--r--silx/gui/plot/AlphaSlider.py300
-rw-r--r--silx/gui/plot/ColorBar.py881
-rw-r--r--silx/gui/plot/Colormap.py42
-rw-r--r--silx/gui/plot/ColormapDialog.py43
-rw-r--r--silx/gui/plot/Colors.py90
-rw-r--r--silx/gui/plot/CompareImages.py1249
-rw-r--r--silx/gui/plot/ComplexImageView.py518
-rw-r--r--silx/gui/plot/CurvesROIWidget.py1584
-rw-r--r--silx/gui/plot/ImageStack.py636
-rw-r--r--silx/gui/plot/ImageView.py854
-rw-r--r--silx/gui/plot/Interaction.py350
-rw-r--r--silx/gui/plot/ItemsSelectionDialog.py286
-rwxr-xr-xsilx/gui/plot/LegendSelector.py1036
-rw-r--r--silx/gui/plot/LimitsHistory.py83
-rw-r--r--silx/gui/plot/MaskToolsWidget.py919
-rw-r--r--silx/gui/plot/PlotActions.py67
-rw-r--r--silx/gui/plot/PlotEvents.py166
-rw-r--r--silx/gui/plot/PlotInteraction.py1748
-rw-r--r--silx/gui/plot/PlotToolButtons.py592
-rw-r--r--silx/gui/plot/PlotTools.py43
-rwxr-xr-xsilx/gui/plot/PlotWidget.py3621
-rw-r--r--silx/gui/plot/PlotWindow.py994
-rw-r--r--silx/gui/plot/PrintPreviewToolButton.py392
-rw-r--r--silx/gui/plot/Profile.py352
-rw-r--r--silx/gui/plot/ProfileMainWindow.py110
-rw-r--r--silx/gui/plot/ROIStatsWidget.py780
-rw-r--r--silx/gui/plot/ScatterMaskToolsWidget.py621
-rw-r--r--silx/gui/plot/ScatterView.py405
-rw-r--r--silx/gui/plot/StackView.py1254
-rw-r--r--silx/gui/plot/StatsWidget.py1661
-rw-r--r--silx/gui/plot/_BaseMaskToolsWidget.py1282
-rw-r--r--silx/gui/plot/__init__.py71
-rw-r--r--silx/gui/plot/_utils/__init__.py93
-rw-r--r--silx/gui/plot/_utils/delaunay.py62
-rw-r--r--silx/gui/plot/_utils/dtime_ticklayout.py442
-rw-r--r--silx/gui/plot/_utils/panzoom.py292
-rw-r--r--silx/gui/plot/_utils/setup.py42
-rw-r--r--silx/gui/plot/_utils/test/__init__.py43
-rw-r--r--silx/gui/plot/_utils/test/test_dtime_ticklayout.py93
-rw-r--r--silx/gui/plot/_utils/test/test_ticklayout.py92
-rw-r--r--silx/gui/plot/_utils/ticklayout.py267
-rw-r--r--silx/gui/plot/actions/PlotAction.py78
-rw-r--r--silx/gui/plot/actions/PlotToolAction.py150
-rw-r--r--silx/gui/plot/actions/__init__.py42
-rwxr-xr-xsilx/gui/plot/actions/control.py694
-rw-r--r--silx/gui/plot/actions/fit.py403
-rw-r--r--silx/gui/plot/actions/histogram.py392
-rw-r--r--silx/gui/plot/actions/io.py818
-rw-r--r--silx/gui/plot/actions/medfilt.py147
-rw-r--r--silx/gui/plot/actions/mode.py104
-rwxr-xr-xsilx/gui/plot/backends/BackendBase.py578
-rwxr-xr-xsilx/gui/plot/backends/BackendMatplotlib.py1544
-rwxr-xr-xsilx/gui/plot/backends/BackendOpenGL.py1420
-rw-r--r--silx/gui/plot/backends/__init__.py29
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py1375
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py1219
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py756
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotItem.py99
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotTriangles.py197
-rw-r--r--silx/gui/plot/backends/glutils/GLSupport.py158
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py287
-rw-r--r--silx/gui/plot/backends/glutils/GLTexture.py241
-rw-r--r--silx/gui/plot/backends/glutils/PlotImageFile.py153
-rw-r--r--silx/gui/plot/backends/glutils/__init__.py46
-rw-r--r--silx/gui/plot/items/__init__.py52
-rw-r--r--silx/gui/plot/items/_arc_roi.py878
-rw-r--r--silx/gui/plot/items/_pick.py72
-rw-r--r--silx/gui/plot/items/_roi_base.py835
-rw-r--r--silx/gui/plot/items/axis.py569
-rw-r--r--silx/gui/plot/items/complex.py386
-rw-r--r--silx/gui/plot/items/core.py1734
-rw-r--r--silx/gui/plot/items/curve.py326
-rw-r--r--silx/gui/plot/items/histogram.py389
-rw-r--r--silx/gui/plot/items/image.py617
-rwxr-xr-xsilx/gui/plot/items/marker.py281
-rw-r--r--silx/gui/plot/items/roi.py1519
-rw-r--r--silx/gui/plot/items/scatter.py973
-rw-r--r--silx/gui/plot/items/shape.py288
-rw-r--r--silx/gui/plot/matplotlib/Colormap.py249
-rw-r--r--silx/gui/plot/matplotlib/__init__.py37
-rw-r--r--silx/gui/plot/setup.py54
-rw-r--r--silx/gui/plot/stats/__init__.py33
-rw-r--r--silx/gui/plot/stats/stats.py890
-rw-r--r--silx/gui/plot/stats/statshandler.py202
-rw-r--r--silx/gui/plot/test/__init__.py92
-rw-r--r--silx/gui/plot/test/testAlphaSlider.py218
-rw-r--r--silx/gui/plot/test/testColorBar.py354
-rw-r--r--silx/gui/plot/test/testCompareImages.py117
-rw-r--r--silx/gui/plot/test/testComplexImageView.py95
-rw-r--r--silx/gui/plot/test/testCurvesROIWidget.py469
-rw-r--r--silx/gui/plot/test/testImageStack.py197
-rw-r--r--silx/gui/plot/test/testImageView.py136
-rw-r--r--silx/gui/plot/test/testInteraction.py89
-rw-r--r--silx/gui/plot/test/testItem.py340
-rw-r--r--silx/gui/plot/test/testLegendSelector.py142
-rw-r--r--silx/gui/plot/test/testLimitConstraints.py125
-rw-r--r--silx/gui/plot/test/testMaskToolsWidget.py316
-rw-r--r--silx/gui/plot/test/testPixelIntensityHistoAction.py157
-rw-r--r--silx/gui/plot/test/testPlotInteraction.py172
-rwxr-xr-xsilx/gui/plot/test/testPlotWidget.py2072
-rw-r--r--silx/gui/plot/test/testPlotWidgetNoBackend.py631
-rw-r--r--silx/gui/plot/test/testPlotWindow.py185
-rw-r--r--silx/gui/plot/test/testRoiStatsWidget.py290
-rw-r--r--silx/gui/plot/test/testSaveAction.py143
-rw-r--r--silx/gui/plot/test/testScatterMaskToolsWidget.py318
-rw-r--r--silx/gui/plot/test/testScatterView.py134
-rw-r--r--silx/gui/plot/test/testStackView.py261
-rw-r--r--silx/gui/plot/test/testStats.py1058
-rw-r--r--silx/gui/plot/test/testUtilsAxis.py214
-rw-r--r--silx/gui/plot/test/utils.py94
-rw-r--r--silx/gui/plot/tools/CurveLegendsWidget.py247
-rw-r--r--silx/gui/plot/tools/LimitsToolBar.py131
-rw-r--r--silx/gui/plot/tools/PositionInfo.py376
-rw-r--r--silx/gui/plot/tools/RadarView.py361
-rw-r--r--silx/gui/plot/tools/__init__.py50
-rw-r--r--silx/gui/plot/tools/profile/ScatterProfileToolBar.py54
-rw-r--r--silx/gui/plot/tools/profile/__init__.py38
-rw-r--r--silx/gui/plot/tools/profile/core.py525
-rw-r--r--silx/gui/plot/tools/profile/editors.py307
-rw-r--r--silx/gui/plot/tools/profile/manager.py1076
-rw-r--r--silx/gui/plot/tools/profile/rois.py1156
-rw-r--r--silx/gui/plot/tools/profile/toolbar.py172
-rw-r--r--silx/gui/plot/tools/roi.py1417
-rw-r--r--silx/gui/plot/tools/test/__init__.py52
-rw-r--r--silx/gui/plot/tools/test/testCurveLegendsWidget.py125
-rw-r--r--silx/gui/plot/tools/test/testProfile.py673
-rw-r--r--silx/gui/plot/tools/test/testROI.py694
-rw-r--r--silx/gui/plot/tools/test/testScatterProfileToolBar.py196
-rw-r--r--silx/gui/plot/tools/test/testTools.py147
-rw-r--r--silx/gui/plot/tools/toolbars.py362
-rw-r--r--silx/gui/plot/utils/__init__.py30
-rw-r--r--silx/gui/plot/utils/axis.py403
-rw-r--r--silx/gui/plot/utils/intersections.py101
133 files changed, 0 insertions, 62882 deletions
diff --git a/silx/gui/plot/AlphaSlider.py b/silx/gui/plot/AlphaSlider.py
deleted file mode 100644
index ab2e5aa..0000000
--- a/silx/gui/plot/AlphaSlider.py
+++ /dev/null
@@ -1,300 +0,0 @@
-# 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 defines slider widgets interacting with the transparency
-of an image on a :class:`PlotWidget`
-
-Classes:
---------
-
-- :class:`BaseAlphaSlider` (abstract class)
-- :class:`NamedImageAlphaSlider`
-- :class:`ActiveImageAlphaSlider`
-
-Example:
---------
-
-This widget can, for instance, be added to a plot toolbar.
-
-.. code-block:: python
-
- import numpy
- from silx.gui import qt
- from silx.gui.plot import PlotWidget
- from silx.gui.plot.ImageAlphaSlider import NamedImageAlphaSlider
-
- app = qt.QApplication([])
- pw = PlotWidget()
-
- img0 = numpy.arange(200*150).reshape((200, 150))
- pw.addImage(img0, legend="my background", z=0, origin=(50, 50))
-
- x, y = numpy.meshgrid(numpy.linspace(-10, 10, 200),
- numpy.linspace(-10, 5, 150),
- indexing="ij")
- img1 = numpy.asarray(numpy.sin(x * y) / (x * y),
- dtype='float32')
-
- pw.addImage(img1, legend="my data", z=1,
- replace=False)
-
- alpha_slider = NamedImageAlphaSlider(parent=pw,
- plot=pw,
- legend="my data")
- alpha_slider.setOrientation(qt.Qt.Horizontal)
-
- toolbar = qt.QToolBar("plot", pw)
- toolbar.addWidget(alpha_slider)
- pw.addToolBar(toolbar)
-
- pw.show()
- app.exec_()
-
-"""
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "24/03/2017"
-
-import logging
-
-from silx.gui import qt
-
-_logger = logging.getLogger(__name__)
-
-
-class BaseAlphaSlider(qt.QSlider):
- """Slider widget to be used in a plot toolbar to control the
- transparency of a plot primitive (image, scatter or curve).
-
- Internally, the slider stores its state as an integer between
- 0 and 255. This is the value emitted by the :attr:`valueChanged`
- signal.
-
- The method :meth:`getAlpha` returns the corresponding opacity/alpha
- as a float between 0. and 1. (with a step of :math:`\frac{1}{255}`).
-
- You must subclass this class and implement :meth:`getItem`.
- """
- sigAlphaChanged = qt.Signal(float)
- """Emits the alpha value when the slider's value changes,
- as a float between 0. and 1."""
-
- def __init__(self, parent=None, plot=None):
- """
-
- :param parent: Parent QWidget
- :param plot: Parent plot widget
- """
- assert plot is not None
- super(BaseAlphaSlider, self).__init__(parent)
-
- self.plot = plot
-
- self.setRange(0, 255)
-
- # if already connected to an item, use its alpha as initial value
- if self.getItem() is None:
- self.setValue(255)
- self.setEnabled(False)
- else:
- alpha = self.getItem().getAlpha()
- self.setValue(round(255*alpha))
-
- self.valueChanged.connect(self._valueChanged)
-
- def getItem(self):
- """You must implement this class to define which item
- to work on. It must return an item that inherits
- :class:`silx.gui.plot.items.core.AlphaMixIn`.
-
- :return: Item on which to operate, or None
- :rtype: :class:`silx.plot.items.Item`
- """
- raise NotImplementedError(
- "BaseAlphaSlider must be subclassed to " +
- "implement getItem()")
-
- def getAlpha(self):
- """Get the opacity, as a float between 0. and 1.
-
- :return: Alpha value in [0., 1.]
- :rtype: float
- """
- return self.value() / 255.
-
- def _valueChanged(self, value):
- self._updateItem()
- self.sigAlphaChanged.emit(value / 255.)
-
- def _updateItem(self):
- """Update the item's alpha channel.
- """
- item = self.getItem()
- if item is not None:
- item.setAlpha(self.getAlpha())
-
-
-class ActiveImageAlphaSlider(BaseAlphaSlider):
- """Slider widget to be used in a plot toolbar to control the
- transparency of the **active image**.
-
- :param parent: Parent QWidget
- :param plot: Plot on which to operate
-
- See documentation of :class:`BaseAlphaSlider`
- """
- def __init__(self, parent=None, plot=None):
- """
-
- :param parent: Parent QWidget
- :param plot: Plot widget on which to operate
- """
- super(ActiveImageAlphaSlider, self).__init__(parent, plot)
- plot.sigActiveImageChanged.connect(self._activeImageChanged)
-
- def getItem(self):
- return self.plot.getActiveImage()
-
- def _activeImageChanged(self, previous, new):
- """Activate or deactivate slider depending on presence of a new
- active image.
- Apply transparency value to new active image.
-
- :param previous: Legend of previous active image, or None
- :param new: Legend of new active image, or None
- """
- if new is not None and not self.isEnabled():
- self.setEnabled(True)
- elif new is None and self.isEnabled():
- self.setEnabled(False)
-
- self._updateItem()
-
-
-class NamedItemAlphaSlider(BaseAlphaSlider):
- """Slider widget to be used in a plot toolbar to control the
- transparency of an item (defined by its kind and legend).
-
- :param parent: Parent QWidget
- :param plot: Plot on which to operate
- :param str kind: Kind of item whose transparency is to be
- controlled: "scatter", "image" or "curve".
- :param str legend: Legend of item whose transparency is to be
- controlled.
- """
- def __init__(self, parent=None, plot=None,
- kind=None, legend=None):
- self._item_legend = legend
- self._item_kind = kind
-
- super(NamedItemAlphaSlider, self).__init__(parent, plot)
-
- self._updateState()
- plot.sigContentChanged.connect(self._onContentChanged)
-
- def _onContentChanged(self, action, kind, legend):
- if legend == self._item_legend and kind == self._item_kind:
- if action == "add":
- self.setEnabled(True)
- elif action == "remove":
- self.setEnabled(False)
-
- def _updateState(self):
- """Enable or disable widget based on item's availability."""
- if self.getItem() is not None:
- self.setEnabled(True)
- else:
- self.setEnabled(False)
-
- def getItem(self):
- """Return plot item currently associated to this widget (can be
- a curve, an image, a scatter...)
-
- :rtype: subclass of :class:`silx.gui.plot.items.Item`"""
- if self._item_legend is None or self._item_kind is None:
- return None
- return self.plot._getItem(kind=self._item_kind,
- legend=self._item_legend)
-
- def setLegend(self, legend):
- """Associate a different item (of the same kind) to the slider.
-
- :param legend: New legend of item whose transparency is to be
- controlled.
- """
- self._item_legend = legend
- self._updateState()
-
- def getLegend(self):
- """Return legend of the item currently controlled by this slider.
-
- :return: Image legend associated to the slider
- """
- return self._item_kind
-
- def setItemKind(self, legend):
- """Associate a different item (of the same kind) to the slider.
-
- :param legend: New legend of item whose transparency is to be
- controlled.
- """
- self._item_legend = legend
- self._updateState()
-
- def getItemKind(self):
- """Return kind of the item currently controlled by this slider.
-
- :return: Item kind ("image", "scatter"...)
- :rtype: str on None
- """
- return self._item_kind
-
-
-class NamedImageAlphaSlider(NamedItemAlphaSlider):
- """Slider widget to be used in a plot toolbar to control the
- transparency of an image (defined by its legend).
-
- :param parent: Parent QWidget
- :param plot: Plot on which to operate
- :param str legend: Legend of image whose transparency is to be
- controlled.
- """
- def __init__(self, parent=None, plot=None, legend=None):
- NamedItemAlphaSlider.__init__(self, parent, plot,
- kind="image", legend=legend)
-
-
-class NamedScatterAlphaSlider(NamedItemAlphaSlider):
- """Slider widget to be used in a plot toolbar to control the
- transparency of a scatter (defined by its legend).
-
- :param parent: Parent QWidget
- :param plot: Plot on which to operate
- :param str legend: Legend of scatter whose transparency is to be
- controlled.
- """
- def __init__(self, parent=None, plot=None, legend=None):
- NamedItemAlphaSlider.__init__(self, parent, plot,
- kind="scatter", legend=legend)
diff --git a/silx/gui/plot/ColorBar.py b/silx/gui/plot/ColorBar.py
deleted file mode 100644
index d869825..0000000
--- a/silx/gui/plot/ColorBar.py
+++ /dev/null
@@ -1,881 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2021 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.
-#
-# ###########################################################################*/
-"""Module containing several widgets associated to a colormap.
-"""
-
-__authors__ = ["H. Payno", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-import logging
-import weakref
-import numpy
-
-from ._utils import ticklayout
-from .. import qt
-from ..qt import inspect as qt_inspect
-from silx.gui import colors
-
-_logger = logging.getLogger(__name__)
-
-
-class ColorBarWidget(qt.QWidget):
- """Colorbar widget displaying a colormap
-
- It uses a description of colormap as dict compatible with :class:`Plot`.
-
- .. image:: img/linearColorbar.png
- :width: 80px
- :align: center
-
- To run the following sample code, a QApplication must be initialized.
-
- >>> from silx.gui.plot import Plot2D
- >>> from silx.gui.plot.ColorBar import ColorBarWidget
-
- >>> plot = Plot2D() # Create a plot widget
- >>> plot.show()
-
- >>> colorbar = ColorBarWidget(plot=plot, legend='Colormap') # Associate the colorbar with it
- >>> colorbar.show()
-
- Initializer parameters:
-
- :param parent: See :class:`QWidget`
- :param plot: PlotWidget the colorbar is attached to (optional)
- :param str legend: the label to set to the colorbar
- """
- sigVisibleChanged = qt.Signal(bool)
- """Emitted when the property `visible` have changed."""
-
- def __init__(self, parent=None, plot=None, legend=None):
- self._isConnected = False
- self._plotRef = None
- self._colormap = None
- self._data = None
-
- super(ColorBarWidget, self).__init__(parent)
-
- self.__buildGUI()
- self.setLegend(legend)
- self.setPlot(plot)
-
- def __buildGUI(self):
- self.setLayout(qt.QHBoxLayout())
-
- # create color scale widget
- self._colorScale = ColorScaleBar(parent=self,
- colormap=None)
- self.layout().addWidget(self._colorScale)
-
- # legend (is the right group)
- self.legend = _VerticalLegend('', self)
- self.layout().addWidget(self.legend)
-
- self.layout().setSizeConstraint(qt.QLayout.SetMinAndMaxSize)
-
- def getPlot(self):
- """Returns the :class:`Plot` associated to this widget or None"""
- return None if self._plotRef is None else self._plotRef()
-
- 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.
- """
- self._disconnectPlot()
- self._plotRef = None if plot is None else weakref.ref(plot)
- self._connectPlot()
-
- def _disconnectPlot(self):
- """Disconnect from Plot signals"""
- if self._isConnected:
- self._isConnected = False
- plot = self.getPlot()
- if plot is not None and qt_inspect.isValid(plot):
- plot.sigActiveImageChanged.disconnect(
- self._activeImageChanged)
- plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChanged)
- plot.sigPlotSignal.disconnect(self._defaultColormapChanged)
-
- def _connectPlot(self):
- """Connect to Plot signals"""
- plot = self.getPlot()
- if plot is not None and not self._isConnected:
- activeImageLegend = plot.getActiveImage(just_legend=True)
- activeScatterLegend = plot._getActiveItem(
- kind='scatter', just_legend=True)
- if activeImageLegend is None and activeScatterLegend is None:
- # Show plot default colormap
- self._syncWithDefaultColormap()
- elif activeImageLegend is not None: # Show active image colormap
- self._activeImageChanged(None, activeImageLegend)
- elif activeScatterLegend is not None: # Show active scatter colormap
- self._activeScatterChanged(None, activeScatterLegend)
-
- plot.sigActiveImageChanged.connect(self._activeImageChanged)
- plot.sigActiveScatterChanged.connect(self._activeScatterChanged)
- plot.sigPlotSignal.connect(self._defaultColormapChanged)
- self._isConnected = True
-
- def setVisible(self, isVisible):
- qt.QWidget.setVisible(self, isVisible)
- self.sigVisibleChanged.emit(isVisible)
-
- def showEvent(self, event):
- self._connectPlot()
-
- def hideEvent(self, event):
- self._disconnectPlot()
-
- def getColormap(self):
- """Returns the colormap displayed in the colorbar.
-
- :rtype: ~silx.gui.colors.Colormap
- """
- return self.getColorScaleBar().getColormap()
-
- def setColormap(self, colormap, data=None):
- """Set the colormap to be displayed.
-
- :param ~silx.gui.colors.Colormap colormap:
- The colormap to apply on the ColorBarWidget
- :param Union[numpy.ndarray,~silx.gui.plot.items.ColormapMixin] data:
- The data to display or item, 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 not None:
- self._colormap.sigChanged.connect(self._colormapHasChanged)
-
- 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
-
- :param str legend: The label
- """
- if legend is None or legend == "":
- self.legend.hide()
- self.legend.setText("")
- else:
- assert type(legend) is str
- self.legend.show()
- self.legend.setText(legend)
-
- def getLegend(self):
- """
- Returns the legend displayed along the colorbar
-
- :return: return the legend displayed along the colorbar
- :rtype: str
- """
- return self.legend.text()
-
- def _activeScatterChanged(self, previous, legend):
- """Handle plot active scatter changed"""
- plot = self.getPlot()
-
- # Do not handle active scatter while there is an image
- if plot.getActiveImage() is not None:
- return
-
- if legend is None: # No active scatter, display no colormap
- self.setColormap(colormap=None)
- return
-
- # Sync with active scatter
- scatter = plot._getActiveItem(kind='scatter')
-
- self.setColormap(colormap=scatter.getColormap(),
- data=scatter)
-
- def _activeImageChanged(self, previous, legend):
- """Handle plot active image changed"""
- plot = self.getPlot()
-
- if legend is None: # No active image, try with active scatter
- activeScatterLegend = plot._getActiveItem(
- kind='scatter', just_legend=True)
- # No more active image, use active scatter if any
- self._activeScatterChanged(None, activeScatterLegend)
- else:
- # Sync with active image
- image = plot.getActiveImage()
-
- # RGB(A) image, display default colormap
- array = image.getData(copy=False)
- if array.ndim != 2:
- 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
- self.setColormap(colormap=image.getColormap(), data=image)
-
- def _defaultColormapChanged(self, event):
- """Handle plot default colormap changed"""
- if event['event'] == 'defaultColormapChanged':
- plot = self.getPlot()
- if (plot is not None and
- plot.getActiveImage() is None and
- plot._getActiveItem(kind='scatter') is None):
- # No active item, take default colormap update into account
- self._syncWithDefaultColormap()
-
- def _syncWithDefaultColormap(self):
- """Update colorbar according to plot default colormap"""
- self.setColormap(self.getPlot().getDefaultColormap())
-
- def getColorScaleBar(self):
- """
-
- :return: return the :class:`ColorScaleBar` used to display ColorScale
- and ticks"""
- return self._colorScale
-
-
-class _VerticalLegend(qt.QLabel):
- """Display vertically the given text
- """
- def __init__(self, text, parent=None):
- """
-
- :param text: the legend
- :param parent: the Qt parent if any
- """
- qt.QLabel.__init__(self, text, parent)
- self.setLayout(qt.QVBoxLayout())
- self.layout().setContentsMargins(0, 0, 0, 0)
-
- def paintEvent(self, event):
- painter = qt.QPainter(self)
- painter.setFont(self.font())
-
- painter.translate(0, self.rect().height())
- painter.rotate(270)
- newRect = qt.QRect(0, 0, self.rect().height(), self.rect().width())
-
- painter.drawText(newRect, qt.Qt.AlignHCenter, self.text())
-
- fm = qt.QFontMetrics(self.font())
- preferedHeight = fm.width(self.text())
- preferedWidth = fm.height()
- self.setFixedWidth(preferedWidth)
- self.setMinimumHeight(preferedHeight)
-
-
-class ColorScaleBar(qt.QWidget):
- """This class is making the composition of a :class:`_ColorScale` and a
- :class:`_TickBar`.
-
- It is the simplest widget displaying ticks and colormap gradient.
-
- .. image:: img/colorScaleBar.png
- :width: 150px
- :align: center
-
- To run the following sample code, a QApplication must be initialized.
-
- >>> colormap = Colormap(name='gray',
- ... norm='log',
- ... vmin=1,
- ... vmax=100000,
- ... )
- >>> colorscale = ColorScaleBar(parent=None,
- ... colormap=colormap )
- >>> colorscale.show()
-
- Initializer parameters :
-
- :param colormap: the colormap to be displayed
- :param parent: the Qt parent if any
- :param displayTicksValues: display the ticks value or only the '-'
- """
-
- _TEXT_MARGIN = 5
- """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"""
-
- def __init__(self, parent=None, colormap=None, data=None,
- displayTicksValues=True):
- super(ColorScaleBar, self).__init__(parent)
-
- self.minVal = None
- """Value set to the _minLabel"""
- self.maxVal = None
- """Value set to the _maxLabel"""
-
- self.setLayout(qt.QGridLayout())
-
- # create the left side group (ColorScale)
- self.colorScale = _ColorScale(colormap=colormap,
- data=data,
- parent=self,
- margin=ColorScaleBar._TEXT_MARGIN)
- if colormap:
- vmin, vmax = colormap.getColormapRange(data)
- normalizer = colormap._getNormalizer()
- else:
- vmin, vmax = colors.DEFAULT_MIN_LIN, colors.DEFAULT_MAX_LIN
- normalizer = None
-
- self.tickbar = _TickBar(vmin=vmin,
- vmax=vmax,
- normalizer=normalizer,
- parent=self,
- displayValues=displayTicksValues,
- margin=ColorScaleBar._TEXT_MARGIN)
-
- 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.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.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):
- """
-
- :return: the instanciation of the :class:`_TickBar`
- """
- return self.tickbar
-
- def getColorScale(self):
- """
-
- :return: the instanciation of the :class:`_ColorScale`
- """
- return self.colorScale
-
- 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 Colormap colormap: the colormap to set
- :param Union[numpy.ndarray,~silx.gui.plot.items.Item] data:
- The data or item to display, needed if the colormap requires an autoscale
- """
- self.colorScale.setColormap(colormap, data)
-
- if colormap is not None:
- vmin, vmax = colormap.getColormapRange(data)
- normalizer = colormap._getNormalizer()
- else:
- vmin, vmax = None, None
- normalizer = None
-
- self.tickbar.update(vmin=vmin,
- vmax=vmax,
- normalizer=normalizer)
- self._setMinMaxLabels(vmin, vmax)
-
- def setMinMaxVisible(self, val=True):
- """Change visibility of the min label and the max label
-
- :param val: if True, set the labels visible, otherwise set it not visible
- """
- self._minLabel.setVisible(val)
- self._maxLabel.setVisible(val)
-
- def _updateMinMax(self):
- """Update the min and max label if we are in the case of the
- configuration 'minMaxValueOnly'"""
- 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.
-
- :param minVal: the minimal value of the TickBar (not str)
- :param maxVal: the maximal value of the TickBar (not str)
- """
- # bad hack to try to display has much information as possible
- self.minVal = minVal
- self.maxVal = maxVal
- self._updateMinMax()
-
- def resizeEvent(self, event):
- qt.QWidget.resizeEvent(self, event)
- self._updateMinMax()
-
-
-class _ColorScale(qt.QWidget):
- """Widget displaying the colormap colorScale.
-
- Show matching value between the gradient color (from the colormap) at mouse
- position and value.
-
- .. image:: img/colorScale.png
- :width: 20px
- :align: center
-
-
- To run the following sample code, a QApplication must be initialized.
-
- >>> colormap = Colormap(name='viridis',
- ... norm='log',
- ... vmin=1,
- ... vmax=100000,
- ... )
- >>> colorscale = ColorScale(parent=None,
- ... colormap=colormap)
- >>> colorscale.show()
-
- Initializer parameters :
-
- :param colormap: the colormap to be displayed
- :param parent: the Qt parent if any
- :param int margin: the top and left margin to apply.
- :param Union[None,numpy.ndarray,~silx.gui.plot.items.ColormapMixin] data:
- The data or item to use for getting the range for autoscale colormap.
-
- .. warning:: Value drawing will be
- done at the center of ticks. So if no margin is done your values
- drawing might not be fully done for extrems values.
- """
-
- _NB_CONTROL_POINTS = 256
-
- def __init__(self, colormap, parent=None, margin=5, data=None):
- qt.QWidget.__init__(self, parent)
- self._colormap = None
- self.margin = margin
- self.setColormap(colormap, data)
-
- self.setLayout(qt.QVBoxLayout())
- 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)
-
- 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 Union[None,numpy.ndarray,~silx.gui.plot.items.ColormapMixin] data:
- Optional data for which to compute colormap range.
- """
- self._colormap = colormap
- self.setEnabled(colormap is not None)
-
- if colormap is None:
- self.vmin, self.vmax = None, None
- else:
- assert colormap.getNormalization() in colors.Colormap.NORMALIZATIONS
- self.vmin, self.vmax = self._colormap.getColormapRange(data=data)
- self._updateColorGradient()
- self.update()
-
- def getColormap(self):
- """Returns the colormap
-
- :rtype: :class:`.Colormap`
- """
- 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
-
- indices = numpy.linspace(0., 1., self._NB_CONTROL_POINTS)
- colors = colormap.getNColors(nbColors=self._NB_CONTROL_POINTS)
- 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):
- """"""
- painter = qt.QPainter(self)
- 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.drawRect(qt.QRect(
- 0,
- self.margin,
- self.width() - 1,
- self.height() - 2 * self.margin - 1))
-
- def mouseMoveEvent(self, event):
- 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. - (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
- ColorScaleBar (y)
-
- :param value: float value in [0, 1]
- :return: the value in [colormap['vmin'], colormap['vmax']]
- """
- colormap = self.getColormap()
- if colormap is None:
- return
-
- value = numpy.clip(value, 0., 1.)
- normalizer = colormap._getNormalizer()
- normMin, normMax = normalizer.apply([self.vmin, self.vmax], self.vmin, self.vmax)
-
- return normalizer.revert(
- normMin + (normMax - normMin) * value, self.vmin, self.vmax)
-
- def setMargin(self, margin):
- """Define the margin to fit with a TickBar object.
- This is needed since we can only paint on the viewport of the widget.
- Didn't work with a simple setContentsMargins
-
- :param int margin: the margin to apply on the top and bottom.
- """
- self.margin = int(margin)
- self.update()
-
-
-class _TickBar(qt.QWidget):
- """Bar grouping the ticks displayed
-
- To run the following sample code, a QApplication must be initialized.
-
- >>> bar = _TickBar(1, 1000, norm='log', parent=None, displayValues=True)
- >>> bar.show()
-
- .. image:: img/tickbar.png
- :width: 40px
- :align: center
-
- :param int vmin: smaller value of the range of values
- :param int vmax: higher value of the range of values
- :param normalizer: Normalization object.
- :param parent: the Qt parent if any
- :param bool displayValues: if True display the values close to the tick,
- Otherwise only signal it by '-'
- :param int nticks: the number of tick we want to display. Should be an
- unsigned int ot None. If None, let the Tick bar find the optimal
- number of ticks from the tick density.
- :param int margin: margin to set on the top and bottom
- """
- _WIDTH_DISP_VAL = 45
- """widget width when displayed with ticks labels"""
- _WIDTH_NO_DISP_VAL = 10
- """widget width when displayed without ticks labels"""
- _FONT_SIZE = 10
- """font size for ticks labels"""
- _LINE_WIDTH = 10
- """width of the line to mark a tick"""
-
- DEFAULT_TICK_DENSITY = 0.015
-
- def __init__(self, vmin, vmax, normalizer, 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
- self._normalizer = normalizer
- self.displayValues = displayValues
- self.setTicksNumber(nticks)
-
- self.setMargin(margin)
- self.setContentsMargins(0, 0, 0, 0)
-
- self._resetWidth()
-
- def setTicksValuesVisible(self, val):
- self.displayValues = val
- self._resetWidth()
-
- def _resetWidth(self):
- width = self._WIDTH_DISP_VAL if self.displayValues else self._WIDTH_NO_DISP_VAL
- self.setFixedWidth(width)
-
- def update(self, vmin, vmax, normalizer):
- self._vmin = vmin
- self._vmax = vmax
- self._normalizer = normalizer
- self.computeTicks()
- qt.QWidget.update(self)
-
- def setMargin(self, margin):
- """Define the margin to fit with a _ColorScale object.
- This is needed since we can only paint on the viewport of the widget
-
- :param int margin: the margin to apply on the top and bottom.
- """
- self.margin = margin
-
- def setTicksNumber(self, nticks):
- """Set the number of ticks to display.
-
- :param nticks: the number of tick to be display. Should be an
- unsigned int ot None. If None, let the :class:`_TickBar` find the
- optimal number of ticks from the tick density.
- """
- self._nticks = nticks
- self.computeTicks()
- qt.QWidget.update(self)
-
- def setTicksDensity(self, density):
- """If you let :class:`_TickBar` deal with the number of ticks
- (nticks=None) then you can specify a ticks density to be displayed.
- """
- if density < 0.0:
- raise ValueError('Density should be a positive value')
- self.ticksDensity = density
-
- def computeTicks(self):
- """This function compute ticks values labels. It is called at each
- update and each resize event.
- Deal only with linear and log scale.
- """
- nticks = self._nticks
- if nticks is None:
- nticks = self._getOptimalNbTicks()
-
- if self._vmin == self._vmax:
- # No range: no ticks
- self.ticks = ()
- self.subTicks = ()
- elif isinstance(self._normalizer, colors._LogarithmicNormalization):
- self._computeTicksLog(nticks)
- else: # Fallback: use linear
- self._computeTicksLin(nticks)
-
- # update the form
- font = qt.QFont()
- font.setPixelSize(_TickBar._FONT_SIZE)
-
- self.form = self._getFormat(font)
-
- def _computeTicksLog(self, nticks):
- logMin = numpy.log10(self._vmin)
- logMax = numpy.log10(self._vmax)
- lowBound, highBound, spacing, self._nfrac = ticklayout.niceNumbersForLog10(logMin,
- logMax,
- nticks)
- self.ticks = numpy.power(10., numpy.arange(lowBound, highBound, spacing))
- if spacing == 1:
- self.subTicks = ticklayout.computeLogSubTicks(ticks=self.ticks,
- lowBound=numpy.power(10., lowBound),
- highBound=numpy.power(10., highBound))
- else:
- self.subTicks = []
-
- def resizeEvent(self, event):
- qt.QWidget.resizeEvent(self, event)
- self.computeTicks()
-
- def _computeTicksLin(self, nticks):
- _min, _max, _spacing, self._nfrac = ticklayout.niceNumbers(self._vmin,
- self._vmax,
- nticks)
-
- self.ticks = numpy.arange(_min, _max, _spacing)
- self.subTicks = []
-
- def _getOptimalNbTicks(self):
- return max(2, int(round(self.ticksDensity * self.rect().height())))
-
- def paintEvent(self, event):
- painter = qt.QPainter(self)
- font = painter.font()
- font.setPixelSize(_TickBar._FONT_SIZE)
- painter.setFont(font)
-
- # paint ticks
- for val in self.ticks:
- self._paintTick(val, painter, majorTick=True)
-
- # 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._normalizer is None:
- return 0.
- normMin, normMax, normVal = self._normalizer.apply(
- [self._vmin, self._vmax, val],
- self._vmin,
- self._vmax)
-
- if normMin == normMax:
- return 0.
- else:
- return 1. - (normVal - normMin) / (normMax - normMin)
-
- def _paintTick(self, val, painter, majorTick=True):
- """
-
- :param bool majorTick: if False will never draw text and will set a line
- with a smaller width
- """
- fm = qt.QFontMetrics(painter.font())
- viewportHeight = self.rect().height() - self.margin * 2 - 1
- relativePos = self._getRelativePosition(val)
- height = int(viewportHeight * relativePos + self.margin)
- lineWidth = _TickBar._LINE_WIDTH
- if majorTick is False:
- lineWidth /= 2
-
- painter.drawLine(qt.QLine(int(self.width() - lineWidth),
- height,
- self.width(),
- height))
-
- if self.displayValues and majorTick is True:
- painter.drawText(qt.QPoint(0, int(height + fm.height() / 2)),
- self.form.format(val))
-
- def setDisplayType(self, disType):
- """Set the type of display we want to set for ticks labels
-
- :param str disType: The type of display we want to set. disType values
- can be :
-
- - 'std' for standard, meaning only a formatting on the number of
- digits is done
- - 'e' for scientific display
- - None to let the _TickBar guess the best display for this kind of data.
- """
- if disType not in (None, 'std', 'e'):
- raise ValueError("display type not recognized, value should be in (None, 'std', 'e'")
- self._forcedDisplayType = disType
-
- def _getStandardFormat(self):
- return "{0:.%sf}" % self._nfrac
-
- def _getFormat(self, font):
- if self._forcedDisplayType is None:
- return self._guessType(font)
- elif self._forcedDisplayType == 'std':
- return self._getStandardFormat()
- elif self._forcedDisplayType == 'e':
- return self._getScientificForm()
- else:
- err = 'Forced type for display %s is not recognized' % self._forcedDisplayType
- raise ValueError(err)
-
- def _getScientificForm(self):
- return "{0:.0e}"
-
- def _guessType(self, font):
- """Try fo find the better format to display the tick's labels
-
- :param QFont font: the font we want to use during the painting
- """
- form = self._getStandardFormat()
-
- fm = qt.QFontMetrics(font)
- width = 0
- for tick in self.ticks:
- width = max(fm.boundingRect(form.format(tick)).width(), width)
-
- # if the length of the string are too long we are moving to scientific
- # display
- if width > _TickBar._WIDTH_DISP_VAL - _TickBar._LINE_WIDTH:
- return self._getScientificForm()
- else:
- return form
diff --git a/silx/gui/plot/Colormap.py b/silx/gui/plot/Colormap.py
deleted file mode 100644
index 22fea7f..0000000
--- a/silx/gui/plot/Colormap.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2018 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.
-#
-# ###########################################################################*/
-"""Deprecated module providing the Colormap object
-"""
-
-__authors__ = ["T. Vincent", "H.Payno"]
-__license__ = "MIT"
-__date__ = "27/11/2020"
-
-import silx.utils.deprecation
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.Colormap",
- reason="moved",
- replacement="silx.gui.colors.Colormap",
- since_version="0.8.0",
- only_once=True,
- skip_backtrace_count=1)
-
-from ..colors import * # noqa
diff --git a/silx/gui/plot/ColormapDialog.py b/silx/gui/plot/ColormapDialog.py
deleted file mode 100644
index 7c66cb8..0000000
--- a/silx/gui/plot/ColormapDialog.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2018 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.
-#
-# ###########################################################################*/
-"""Deprecated module providing ColormapDialog."""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent", "H.Payno"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-import silx.utils.deprecation
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.ColormapDialog",
- reason="moved",
- replacement="silx.gui.dialog.ColormapDialog",
- since_version="0.8.0",
- only_once=True,
- skip_backtrace_count=1)
-
-from ..dialog.ColormapDialog import * # noqa
diff --git a/silx/gui/plot/Colors.py b/silx/gui/plot/Colors.py
deleted file mode 100644
index 277e104..0000000
--- a/silx/gui/plot/Colors.py
+++ /dev/null
@@ -1,90 +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.
-#
-# ###########################################################################*/
-"""Color conversion function, color dictionary and colormap tools."""
-
-from __future__ import absolute_import
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "14/06/2018"
-
-import silx.utils.deprecation
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.Colors",
- reason="moved",
- replacement="silx.gui.colors",
- since_version="0.8.0",
- only_once=True,
- skip_backtrace_count=1)
-
-from ..colors import * # noqa
-
-
-@silx.utils.deprecation.deprecated(replacement='silx.gui.colors.Colormap.applyColormap')
-def applyColormapToData(data,
- name='gray',
- normalization='linear',
- autoscale=True,
- vmin=0.,
- vmax=1.,
- colors=None):
- """Apply a colormap to the data and returns the RGBA image
-
- This supports data of any dimensions (not only of dimension 2).
- The returned array will have one more dimension (with 4 entries)
- than the input data to store the RGBA channels
- corresponding to each bin in the array.
-
- :param numpy.ndarray data: The data to convert.
- :param str name: Name of the colormap (default: 'gray').
- :param str normalization: Colormap mapping: 'linear' or 'log'.
- :param bool autoscale: Whether to use data min/max (True, default)
- or [vmin, vmax] range (False).
- :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
- 'autoscale' is False.
- :param numpy.ndarray colors: Only used if name is None.
- Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
- :return: The computed RGBA image
- :rtype: numpy.ndarray of uint8
- """
- colormap = Colormap(name=name,
- normalization=normalization,
- vmin=vmin,
- vmax=vmax,
- colors=colors)
- return colormap.applyToData(data)
-
-
-@silx.utils.deprecation.deprecated(replacement='silx.gui.colors.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/CompareImages.py b/silx/gui/plot/CompareImages.py
deleted file mode 100644
index 3875be4..0000000
--- a/silx/gui/plot/CompareImages.py
+++ /dev/null
@@ -1,1249 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2019 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.
-#
-# ###########################################################################*/
-"""A widget dedicated to compare 2 images.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "23/07/2018"
-
-
-import enum
-import logging
-import numpy
-import weakref
-import collections
-import math
-
-import silx.image.bilinear
-from silx.gui import qt
-from silx.gui import plot
-from silx.gui import icons
-from silx.gui.colors import Colormap
-from silx.gui.plot import tools
-
-_logger = logging.getLogger(__name__)
-
-from silx.opencl import ocl
-if ocl is not None:
- try:
- from silx.opencl import sift
- except ImportError:
- # sift module is not available (e.g., in official Debian packages)
- sift = None
-else: # No OpenCL device or no pyopencl
- sift = None
-
-
-@enum.unique
-class VisualizationMode(enum.Enum):
- """Enum for each visualization mode available."""
- ONLY_A = 'a'
- ONLY_B = 'b'
- VERTICAL_LINE = 'vline'
- HORIZONTAL_LINE = 'hline'
- COMPOSITE_RED_BLUE_GRAY = "rbgchannel"
- COMPOSITE_RED_BLUE_GRAY_NEG = "rbgnegchannel"
- COMPOSITE_A_MINUS_B = "aminusb"
-
-
-@enum.unique
-class AlignmentMode(enum.Enum):
- """Enum for each alignment mode available."""
- ORIGIN = 'origin'
- CENTER = 'center'
- STRETCH = 'stretch'
- AUTO = 'auto'
-
-
-AffineTransformation = collections.namedtuple("AffineTransformation",
- ["tx", "ty", "sx", "sy", "rot"])
-"""Contains a 2D affine transformation: translation, scale and rotation"""
-
-
-class CompareImagesToolBar(qt.QToolBar):
- """ToolBar containing specific tools to custom the configuration of a
- :class:`CompareImages` widget
-
- Use :meth:`setCompareWidget` to connect this toolbar to a specific
- :class:`CompareImages` widget.
-
- :param Union[qt.QWidget,None] parent: Parent of this widget.
- """
- def __init__(self, parent=None):
- qt.QToolBar.__init__(self, parent)
-
- self.__compareWidget = None
-
- menu = qt.QMenu(self)
- self.__visualizationAction = qt.QAction(self)
- self.__visualizationAction.setMenu(menu)
- self.__visualizationAction.setCheckable(False)
- self.addAction(self.__visualizationAction)
- self.__visualizationGroup = qt.QActionGroup(self)
- self.__visualizationGroup.setExclusive(True)
- self.__visualizationGroup.triggered.connect(self.__visualizationModeChanged)
-
- icon = icons.getQIcon("compare-mode-a")
- action = qt.QAction(icon, "Display the first image only", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_A))
- action.setProperty("mode", VisualizationMode.ONLY_A)
- menu.addAction(action)
- self.__aModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-b")
- action = qt.QAction(icon, "Display the second image only", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_B))
- action.setProperty("mode", VisualizationMode.ONLY_B)
- menu.addAction(action)
- self.__bModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-vline")
- action = qt.QAction(icon, "Vertical compare mode", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_V))
- action.setProperty("mode", VisualizationMode.VERTICAL_LINE)
- menu.addAction(action)
- self.__vlineModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-hline")
- action = qt.QAction(icon, "Horizontal compare mode", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_H))
- action.setProperty("mode", VisualizationMode.HORIZONTAL_LINE)
- menu.addAction(action)
- self.__hlineModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-rb-channel")
- action = qt.QAction(icon, "Blue/red compare mode (additive mode)", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_C))
- action.setProperty("mode", VisualizationMode.COMPOSITE_RED_BLUE_GRAY)
- menu.addAction(action)
- self.__brChannelModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-rbneg-channel")
- action = qt.QAction(icon, "Yellow/cyan compare mode (subtractive mode)", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_W))
- action.setProperty("mode", VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG)
- menu.addAction(action)
- self.__ycChannelModeAction = action
- self.__visualizationGroup.addAction(action)
-
- icon = icons.getQIcon("compare-mode-a-minus-b")
- action = qt.QAction(icon, "Raw A minus B compare mode", self)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_W))
- action.setProperty("mode", VisualizationMode.COMPOSITE_A_MINUS_B)
- menu.addAction(action)
- self.__ycChannelModeAction = action
- self.__visualizationGroup.addAction(action)
-
- menu = qt.QMenu(self)
- self.__alignmentAction = qt.QAction(self)
- self.__alignmentAction.setMenu(menu)
- self.__alignmentAction.setIconVisibleInMenu(True)
- self.addAction(self.__alignmentAction)
- self.__alignmentGroup = qt.QActionGroup(self)
- self.__alignmentGroup.setExclusive(True)
- self.__alignmentGroup.triggered.connect(self.__alignmentModeChanged)
-
- icon = icons.getQIcon("compare-align-origin")
- action = qt.QAction(icon, "Align images on their upper-left pixel", self)
- action.setProperty("mode", AlignmentMode.ORIGIN)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__originAlignAction = action
- menu.addAction(action)
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-align-center")
- action = qt.QAction(icon, "Center images", self)
- action.setProperty("mode", AlignmentMode.CENTER)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__centerAlignAction = action
- menu.addAction(action)
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-align-stretch")
- action = qt.QAction(icon, "Stretch the second image on the first one", self)
- action.setProperty("mode", AlignmentMode.STRETCH)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__stretchAlignAction = action
- menu.addAction(action)
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-align-auto")
- action = qt.QAction(icon, "Auto-alignment of the second image", self)
- action.setProperty("mode", AlignmentMode.AUTO)
- action.setIconVisibleInMenu(True)
- action.setCheckable(True)
- self.__autoAlignAction = action
- menu.addAction(action)
- if sift is None:
- action.setEnabled(False)
- action.setToolTip("Sift module is not available")
- self.__alignmentGroup.addAction(action)
-
- icon = icons.getQIcon("compare-keypoints")
- action = qt.QAction(icon, "Display/hide alignment keypoints", self)
- action.setCheckable(True)
- action.triggered.connect(self.__keypointVisibilityChanged)
- self.addAction(action)
- self.__displayKeypoints = action
-
- def setCompareWidget(self, widget):
- """
- Connect this tool bar to a specific :class:`CompareImages` widget.
-
- :param Union[None,CompareImages] widget: The widget to connect with.
- """
- compareWidget = self.getCompareWidget()
- if compareWidget is not None:
- compareWidget.sigConfigurationChanged.disconnect(self.__updateSelectedActions)
- compareWidget = widget
- if compareWidget is None:
- self.__compareWidget = None
- else:
- self.__compareWidget = weakref.ref(compareWidget)
- if compareWidget is not None:
- widget.sigConfigurationChanged.connect(self.__updateSelectedActions)
- self.__updateSelectedActions()
-
- def getCompareWidget(self):
- """Returns the connected widget.
-
- :rtype: CompareImages
- """
- if self.__compareWidget is None:
- return None
- else:
- return self.__compareWidget()
-
- def __updateSelectedActions(self):
- """
- Update the state of this tool bar according to the state of the
- connected :class:`CompareImages` widget.
- """
- widget = self.getCompareWidget()
- if widget is None:
- return
-
- mode = widget.getVisualizationMode()
- action = None
- for a in self.__visualizationGroup.actions():
- actionMode = a.property("mode")
- if mode == actionMode:
- action = a
- break
- old = self.__visualizationGroup.blockSignals(True)
- if action is not None:
- # Check this action
- action.setChecked(True)
- else:
- action = self.__visualizationGroup.checkedAction()
- if action is not None:
- # Uncheck this action
- action.setChecked(False)
- self.__updateVisualizationMenu()
- self.__visualizationGroup.blockSignals(old)
-
- mode = widget.getAlignmentMode()
- action = None
- for a in self.__alignmentGroup.actions():
- actionMode = a.property("mode")
- if mode == actionMode:
- action = a
- break
- old = self.__alignmentGroup.blockSignals(True)
- if action is not None:
- # Check this action
- action.setChecked(True)
- else:
- action = self.__alignmentGroup.checkedAction()
- if action is not None:
- # Uncheck this action
- action.setChecked(False)
- self.__updateAlignmentMenu()
- self.__alignmentGroup.blockSignals(old)
-
- def __visualizationModeChanged(self, selectedAction):
- """Called when user requesting changes of the visualization mode.
- """
- self.__updateVisualizationMenu()
- widget = self.getCompareWidget()
- if widget is not None:
- mode = selectedAction.property("mode")
- widget.setVisualizationMode(mode)
-
- def __updateVisualizationMenu(self):
- """Update the state of the action containing visualization menu.
- """
- selectedAction = self.__visualizationGroup.checkedAction()
- if selectedAction is not None:
- self.__visualizationAction.setText(selectedAction.text())
- self.__visualizationAction.setIcon(selectedAction.icon())
- self.__visualizationAction.setToolTip(selectedAction.toolTip())
- else:
- self.__visualizationAction.setText("")
- self.__visualizationAction.setIcon(qt.QIcon())
- self.__visualizationAction.setToolTip("")
-
- def __alignmentModeChanged(self, selectedAction):
- """Called when user requesting changes of the alignment mode.
- """
- self.__updateAlignmentMenu()
- widget = self.getCompareWidget()
- if widget is not None:
- mode = selectedAction.property("mode")
- widget.setAlignmentMode(mode)
-
- def __updateAlignmentMenu(self):
- """Update the state of the action containing alignment menu.
- """
- selectedAction = self.__alignmentGroup.checkedAction()
- if selectedAction is not None:
- self.__alignmentAction.setText(selectedAction.text())
- self.__alignmentAction.setIcon(selectedAction.icon())
- self.__alignmentAction.setToolTip(selectedAction.toolTip())
- else:
- self.__alignmentAction.setText("")
- self.__alignmentAction.setIcon(qt.QIcon())
- self.__alignmentAction.setToolTip("")
-
- def __keypointVisibilityChanged(self):
- """Called when action managing keypoints visibility changes"""
- widget = self.getCompareWidget()
- if widget is not None:
- keypointsVisible = self.__displayKeypoints.isChecked()
- widget.setKeypointsVisible(keypointsVisible)
-
-
-class CompareImagesStatusBar(qt.QStatusBar):
- """StatusBar containing specific information contained in a
- :class:`CompareImages` widget
-
- Use :meth:`setCompareWidget` to connect this toolbar to a specific
- :class:`CompareImages` widget.
-
- :param Union[qt.QWidget,None] parent: Parent of this widget.
- """
- def __init__(self, parent=None):
- qt.QStatusBar.__init__(self, parent)
- self.setSizeGripEnabled(False)
- self.layout().setSpacing(0)
- self.__compareWidget = None
- self._label1 = qt.QLabel(self)
- self._label1.setFrameShape(qt.QFrame.WinPanel)
- self._label1.setFrameShadow(qt.QFrame.Sunken)
- self._label2 = qt.QLabel(self)
- self._label2.setFrameShape(qt.QFrame.WinPanel)
- self._label2.setFrameShadow(qt.QFrame.Sunken)
- self._transform = qt.QLabel(self)
- self._transform.setFrameShape(qt.QFrame.WinPanel)
- self._transform.setFrameShadow(qt.QFrame.Sunken)
- self.addWidget(self._label1)
- self.addWidget(self._label2)
- self.addWidget(self._transform)
- self._pos = None
- self._updateStatusBar()
-
- def setCompareWidget(self, widget):
- """
- Connect this tool bar to a specific :class:`CompareImages` widget.
-
- :param Union[None,CompareImages] widget: The widget to connect with.
- """
- compareWidget = self.getCompareWidget()
- if compareWidget is not None:
- compareWidget.getPlot().sigPlotSignal.disconnect(self.__plotSignalReceived)
- compareWidget.sigConfigurationChanged.disconnect(self.__dataChanged)
- compareWidget = widget
- if compareWidget is None:
- self.__compareWidget = None
- else:
- self.__compareWidget = weakref.ref(compareWidget)
- if compareWidget is not None:
- compareWidget.getPlot().sigPlotSignal.connect(self.__plotSignalReceived)
- compareWidget.sigConfigurationChanged.connect(self.__dataChanged)
-
- def getCompareWidget(self):
- """Returns the connected widget.
-
- :rtype: CompareImages
- """
- if self.__compareWidget is None:
- return None
- else:
- return self.__compareWidget()
-
- def __plotSignalReceived(self, event):
- """Called when old style signals at emmited from the plot."""
- if event["event"] == "mouseMoved":
- x, y = event["x"], event["y"]
- self.__mouseMoved(x, y)
-
- def __mouseMoved(self, x, y):
- """Called when mouse move over the plot."""
- self._pos = x, y
- self._updateStatusBar()
-
- def __dataChanged(self):
- """Called when internal data from the connected widget changes."""
- self._updateStatusBar()
-
- def _formatData(self, data):
- """Format pixel of an image.
-
- It supports intensity, RGB, and RGBA.
-
- :param Union[int,float,numpy.ndarray,str]: Value of a pixel
- :rtype: str
- """
- if data is None:
- return "No data"
- if isinstance(data, (int, numpy.integer)):
- return "%d" % data
- if isinstance(data, (float, numpy.floating)):
- return "%f" % data
- if isinstance(data, numpy.ndarray):
- # RGBA value
- if data.shape == (3,):
- return "R:%d G:%d B:%d" % (data[0], data[1], data[2])
- elif data.shape == (4,):
- return "R:%d G:%d B:%d A:%d" % (data[0], data[1], data[2], data[3])
- _logger.debug("Unsupported data format %s. Cast it to string.", type(data))
- return str(data)
-
- def _updateStatusBar(self):
- """Update the content of the status bar"""
- widget = self.getCompareWidget()
- if widget is None:
- self._label1.setText("Image1: NA")
- self._label2.setText("Image2: NA")
- self._transform.setVisible(False)
- else:
- transform = widget.getTransformation()
- self._transform.setVisible(transform is not None)
- if transform is not None:
- has_notable_translation = not numpy.isclose(transform.tx, 0.0, atol=0.01) \
- or not numpy.isclose(transform.ty, 0.0, atol=0.01)
- has_notable_scale = not numpy.isclose(transform.sx, 1.0, atol=0.01) \
- or not numpy.isclose(transform.sy, 1.0, atol=0.01)
- has_notable_rotation = not numpy.isclose(transform.rot, 0.0, atol=0.01)
-
- strings = []
- if has_notable_translation:
- strings.append("Translation")
- if has_notable_scale:
- strings.append("Scale")
- if has_notable_rotation:
- strings.append("Rotation")
- if strings == []:
- has_translation = not numpy.isclose(transform.tx, 0.0) \
- or not numpy.isclose(transform.ty, 0.0)
- has_scale = not numpy.isclose(transform.sx, 1.0) \
- or not numpy.isclose(transform.sy, 1.0)
- has_rotation = not numpy.isclose(transform.rot, 0.0)
- if has_translation or has_scale or has_rotation:
- text = "No big changes"
- else:
- text = "No changes"
- else:
- text = "+".join(strings)
- self._transform.setText("Align: " + text)
-
- strings = []
- if not numpy.isclose(transform.ty, 0.0):
- strings.append("Translation x: %0.3fpx" % transform.tx)
- if not numpy.isclose(transform.ty, 0.0):
- strings.append("Translation y: %0.3fpx" % transform.ty)
- if not numpy.isclose(transform.sx, 1.0):
- strings.append("Scale x: %0.3f" % transform.sx)
- if not numpy.isclose(transform.sy, 1.0):
- strings.append("Scale y: %0.3f" % transform.sy)
- if not numpy.isclose(transform.rot, 0.0):
- strings.append("Rotation: %0.3fdeg" % (transform.rot * 180 / numpy.pi))
- if strings == []:
- text = "No transformation"
- else:
- text = "\n".join(strings)
- self._transform.setToolTip(text)
-
- if self._pos is None:
- self._label1.setText("Image1: NA")
- self._label2.setText("Image2: NA")
- else:
- data1, data2 = widget.getRawPixelData(self._pos[0], self._pos[1])
- if isinstance(data1, str):
- self._label1.setToolTip(data1)
- text1 = "NA"
- else:
- self._label1.setToolTip("")
- text1 = self._formatData(data1)
- if isinstance(data2, str):
- self._label2.setToolTip(data2)
- text2 = "NA"
- else:
- self._label2.setToolTip("")
- text2 = self._formatData(data2)
- self._label1.setText("Image1: %s" % text1)
- self._label2.setText("Image2: %s" % text2)
-
-
-class CompareImages(qt.QMainWindow):
- """Widget providing tools to compare 2 images.
-
- .. image:: img/CompareImages.png
-
- :param Union[qt.QWidget,None] parent: Parent of this widget.
- :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`
- """
-
- VisualizationMode = VisualizationMode
- """Available visualization modes"""
-
- AlignmentMode = AlignmentMode
- """Available alignment modes"""
-
- sigConfigurationChanged = qt.Signal()
- """Emitted when the configuration of the widget (visualization mode,
- alignement mode...) have changed."""
-
- def __init__(self, parent=None, backend=None):
- qt.QMainWindow.__init__(self, parent)
- self._resetZoomActive = True
- self._colormap = Colormap()
- """Colormap shared by all modes, except the compose images (rgb image)"""
- self._colormapKeyPoints = Colormap('spring')
- """Colormap used for sift keypoints"""
-
- if parent is None:
- self.setWindowTitle('Compare images')
- else:
- self.setWindowFlags(qt.Qt.Widget)
-
- self.__transformation = None
- self.__raw1 = None
- self.__raw2 = None
- self.__data1 = None
- self.__data2 = None
- self.__previousSeparatorPosition = None
-
- self.__plot = plot.PlotWidget(parent=self, backend=backend)
- self.__plot.setDefaultColormap(self._colormap)
- self.__plot.getXAxis().setLabel('Columns')
- self.__plot.getYAxis().setLabel('Rows')
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
- self.__plot.getYAxis().setInverted(True)
-
- self.__plot.setKeepDataAspectRatio(True)
- self.__plot.sigPlotSignal.connect(self.__plotSlot)
- self.__plot.setAxesDisplayed(False)
-
- self.setCentralWidget(self.__plot)
-
- legend = VisualizationMode.VERTICAL_LINE.name
- self.__plot.addXMarker(
- 0,
- legend=legend,
- text='',
- draggable=True,
- color='blue',
- constraint=self.__separatorConstraint)
- self.__vline = self.__plot._getMarker(legend)
-
- legend = VisualizationMode.HORIZONTAL_LINE.name
- self.__plot.addYMarker(
- 0,
- legend=legend,
- text='',
- draggable=True,
- color='blue',
- constraint=self.__separatorConstraint)
- self.__hline = self.__plot._getMarker(legend)
-
- # default values
- self.__visualizationMode = ""
- self.__alignmentMode = ""
- self.__keypointsVisible = True
-
- self.setAlignmentMode(AlignmentMode.ORIGIN)
- self.setVisualizationMode(VisualizationMode.VERTICAL_LINE)
- self.setKeypointsVisible(False)
-
- # Toolbars
-
- self._createToolBars(self.__plot)
- if self._interactiveModeToolBar is not None:
- self.addToolBar(self._interactiveModeToolBar)
- if self._imageToolBar is not None:
- self.addToolBar(self._imageToolBar)
- if self._compareToolBar is not None:
- self.addToolBar(self._compareToolBar)
-
- # Statusbar
-
- self._createStatusBar(self.__plot)
- if self._statusBar is not None:
- self.setStatusBar(self._statusBar)
-
- def _createStatusBar(self, plot):
- self._statusBar = CompareImagesStatusBar(self)
- self._statusBar.setCompareWidget(self)
-
- def _createToolBars(self, plot):
- """Create tool bars displayed by the widget"""
- toolBar = tools.InteractiveModeToolBar(parent=self, plot=plot)
- self._interactiveModeToolBar = toolBar
- toolBar = tools.ImageToolBar(parent=self, plot=plot)
- self._imageToolBar = toolBar
- toolBar = CompareImagesToolBar(self)
- toolBar.setCompareWidget(self)
- self._compareToolBar = toolBar
-
- def getPlot(self):
- """Returns the plot which is used to display the images.
-
- :rtype: silx.gui.plot.PlotWidget
- """
- return self.__plot
-
- def getColormap(self):
- """
-
- :return: colormap used for compare image
- :rtype: silx.gui.colors.Colormap
- """
- return self._colormap
-
- def getRawPixelData(self, x, y):
- """Return the raw pixel of each image data from axes positions.
-
- If the coordinate is outside of the image it returns None element in
- the tuple.
-
- The pixel is reach from the raw data image without filter or
- transformation. But the coordinate x and y are in the reference of the
- current displayed mode.
-
- :param float x: X-coordinate of the pixel in the current displayed plot
- :param float y: Y-coordinate of the pixel in the current displayed plot
- :return: A tuple of for each images containing pixel information. It
- could be a scalar value or an array in case of RGB/RGBA informations.
- It also could be a string containing information is some cases.
- :rtype: Tuple(Union[int,float,numpy.ndarray,str],Union[int,float,numpy.ndarray,str])
- """
- data2 = None
- alignmentMode = self.__alignmentMode
- raw1, raw2 = self.__raw1, self.__raw2
- if alignmentMode == AlignmentMode.ORIGIN:
- x1 = x
- y1 = y
- x2 = x
- y2 = y
- elif alignmentMode == AlignmentMode.CENTER:
- yy = max(raw1.shape[0], raw2.shape[0])
- xx = max(raw1.shape[1], raw2.shape[1])
- x1 = x - (xx - raw1.shape[1]) * 0.5
- x2 = x - (xx - raw2.shape[1]) * 0.5
- y1 = y - (yy - raw1.shape[0]) * 0.5
- y2 = y - (yy - raw2.shape[0]) * 0.5
- elif alignmentMode == AlignmentMode.STRETCH:
- x1 = x
- y1 = y
- x2 = x * raw2.shape[1] / raw1.shape[1]
- y2 = x * raw2.shape[1] / raw1.shape[1]
- elif alignmentMode == AlignmentMode.AUTO:
- x1 = x
- y1 = y
- # Not implemented
- data2 = "Not implemented with sift"
- else:
- assert(False)
-
- x1, y1 = int(x1), int(y1)
- if raw1 is None or y1 < 0 or y1 >= raw1.shape[0] or x1 < 0 or x1 >= raw1.shape[1]:
- data1 = None
- else:
- data1 = raw1[y1, x1]
-
- if data2 is None:
- x2, y2 = int(x2), int(y2)
- if raw2 is None or y2 < 0 or y2 >= raw2.shape[0] or x2 < 0 or x2 >= raw2.shape[1]:
- data2 = None
- else:
- data2 = raw2[y2, x2]
-
- return data1, data2
-
- def setVisualizationMode(self, mode):
- """Set the visualization mode.
-
- :param str mode: New visualization to display the image comparison
- """
- if self.__visualizationMode == mode:
- return
- self.__visualizationMode = mode
- mode = self.getVisualizationMode()
- self.__vline.setVisible(mode == VisualizationMode.VERTICAL_LINE)
- self.__hline.setVisible(mode == VisualizationMode.HORIZONTAL_LINE)
- self.__updateData()
- self.sigConfigurationChanged.emit()
-
- def getVisualizationMode(self):
- """Returns the current interaction mode."""
- return self.__visualizationMode
-
- def setAlignmentMode(self, mode):
- """Set the alignment mode.
-
- :param str mode: New alignement to apply to images
- """
- if self.__alignmentMode == mode:
- return
- self.__alignmentMode = mode
- self.__updateData()
- self.sigConfigurationChanged.emit()
-
- def getAlignmentMode(self):
- """Returns the current selected alignemnt mode."""
- return self.__alignmentMode
-
- def setKeypointsVisible(self, isVisible):
- """Set keypoints visibility.
-
- :param bool isVisible: If True, keypoints are displayed (if some)
- """
- if self.__keypointsVisible == isVisible:
- return
- self.__keypointsVisible = isVisible
- self.__updateKeyPoints()
- self.sigConfigurationChanged.emit()
-
- def __setDefaultAlignmentMode(self):
- """Reset the alignemnt mode to the default value"""
- self.setAlignmentMode(AlignmentMode.ORIGIN)
-
- def __plotSlot(self, event):
- """Handle events from the plot"""
- if event['event'] in ('markerMoving', 'markerMoved'):
- mode = self.getVisualizationMode()
- legend = mode.name
- if event['label'] == legend:
- if mode == VisualizationMode.VERTICAL_LINE:
- value = int(float(str(event['xdata'])))
- elif mode == VisualizationMode.HORIZONTAL_LINE:
- value = int(float(str(event['ydata'])))
- else:
- assert(False)
- if self.__previousSeparatorPosition != value:
- self.__separatorMoved(value)
- self.__previousSeparatorPosition = value
-
- def __separatorConstraint(self, x, y):
- """Manage contains on the separators to clamp them inside the images."""
- if self.__data1 is None:
- return 0, 0
- x = int(x)
- if x < 0:
- x = 0
- elif x > self.__data1.shape[1]:
- x = self.__data1.shape[1]
- y = int(y)
- if y < 0:
- y = 0
- elif y > self.__data1.shape[0]:
- y = self.__data1.shape[0]
- return x, y
-
- def __updateSeparators(self):
- """Redraw images according to the current state of the separators.
- """
- mode = self.getVisualizationMode()
- if mode == VisualizationMode.VERTICAL_LINE:
- pos = self.__vline.getXPosition()
- self.__separatorMoved(pos)
- self.__previousSeparatorPosition = pos
- elif mode == VisualizationMode.HORIZONTAL_LINE:
- pos = self.__hline.getYPosition()
- self.__separatorMoved(pos)
- self.__previousSeparatorPosition = pos
- else:
- self.__image1.setOrigin((0, 0))
- self.__image2.setOrigin((0, 0))
-
- def __separatorMoved(self, pos):
- """Called when vertical or horizontal separators have moved.
-
- Update the displayed images.
- """
- if self.__data1 is None:
- return
-
- mode = self.getVisualizationMode()
- if mode == VisualizationMode.VERTICAL_LINE:
- pos = int(pos)
- if pos <= 0:
- pos = 0
- elif pos >= self.__data1.shape[1]:
- pos = self.__data1.shape[1]
- data1 = self.__data1[:, 0:pos]
- data2 = self.__data2[:, pos:]
- self.__image1.setData(data1, copy=False)
- self.__image2.setData(data2, copy=False)
- self.__image2.setOrigin((pos, 0))
- elif mode == VisualizationMode.HORIZONTAL_LINE:
- pos = int(pos)
- if pos <= 0:
- pos = 0
- elif pos >= self.__data1.shape[0]:
- pos = self.__data1.shape[0]
- data1 = self.__data1[0:pos, :]
- data2 = self.__data2[pos:, :]
- self.__image1.setData(data1, copy=False)
- self.__image2.setData(data2, copy=False)
- self.__image2.setOrigin((0, pos))
- else:
- assert(False)
-
- def setData(self, image1, image2):
- """Set images to compare.
-
- Images can contains floating-point or integer values, or RGB and RGBA
- values, but should have comparable intensities.
-
- RGB and RGBA images are provided as an array as `[width,height,channels]`
- of usigned integer 8-bits or floating-points between 0.0 to 1.0.
-
- :param numpy.ndarray image1: The first image
- :param numpy.ndarray image2: The second image
- """
- self.__raw1 = image1
- self.__raw2 = image2
- self.__updateData()
- if self.isAutoResetZoom():
- self.__plot.resetZoom()
-
- def setImage1(self, image1):
- """Set image1 to be compared.
-
- Images can contains floating-point or integer values, or RGB and RGBA
- values, but should have comparable intensities.
-
- RGB and RGBA images are provided as an array as `[width,height,channels]`
- of usigned integer 8-bits or floating-points between 0.0 to 1.0.
-
- :param numpy.ndarray image1: The first image
- """
- self.__raw1 = image1
- self.__updateData()
- if self.isAutoResetZoom():
- self.__plot.resetZoom()
-
- def setImage2(self, image2):
- """Set image2 to be compared.
-
- Images can contains floating-point or integer values, or RGB and RGBA
- values, but should have comparable intensities.
-
- RGB and RGBA images are provided as an array as `[width,height,channels]`
- of usigned integer 8-bits or floating-points between 0.0 to 1.0.
-
- :param numpy.ndarray image2: The second image
- """
- self.__raw2 = image2
- self.__updateData()
- if self.isAutoResetZoom():
- self.__plot.resetZoom()
-
- def __updateKeyPoints(self):
- """Update the displayed keypoints using cached keypoints.
- """
- if self.__keypointsVisible:
- data = self.__matching_keypoints
- else:
- data = [], [], []
- self.__plot.addScatter(x=data[0],
- y=data[1],
- z=1,
- value=data[2],
- colormap=self._colormapKeyPoints,
- legend="keypoints")
-
- def __updateData(self):
- """Compute aligned image when the alignment mode changes.
-
- This function cache input images which are used when
- vertical/horizontal separators moves.
- """
- raw1, raw2 = self.__raw1, self.__raw2
- if raw1 is None or raw2 is None:
- return
-
- alignmentMode = self.getAlignmentMode()
- self.__transformation = None
-
- if alignmentMode == AlignmentMode.ORIGIN:
- yy = max(raw1.shape[0], raw2.shape[0])
- xx = max(raw1.shape[1], raw2.shape[1])
- size = yy, xx
- data1 = self.__createMarginImage(raw1, size, transparent=True)
- data2 = self.__createMarginImage(raw2, size, transparent=True)
- self.__matching_keypoints = [0.0], [0.0], [1.0]
- elif alignmentMode == AlignmentMode.CENTER:
- yy = max(raw1.shape[0], raw2.shape[0])
- xx = max(raw1.shape[1], raw2.shape[1])
- size = yy, xx
- data1 = self.__createMarginImage(raw1, size, transparent=True, center=True)
- data2 = self.__createMarginImage(raw2, size, transparent=True, center=True)
- self.__matching_keypoints = ([data1.shape[1] // 2],
- [data1.shape[0] // 2],
- [1.0])
- elif alignmentMode == AlignmentMode.STRETCH:
- data1 = raw1
- data2 = self.__rescaleImage(raw2, data1.shape)
- self.__matching_keypoints = ([0, data1.shape[1], data1.shape[1], 0],
- [0, 0, data1.shape[0], data1.shape[0]],
- [1.0, 1.0, 1.0, 1.0])
- elif alignmentMode == AlignmentMode.AUTO:
- # TODO: sift implementation do not support RGBA images
- yy = max(raw1.shape[0], raw2.shape[0])
- xx = max(raw1.shape[1], raw2.shape[1])
- size = yy, xx
- data1 = self.__createMarginImage(raw1, size)
- data2 = self.__createMarginImage(raw2, size)
- self.__matching_keypoints = [0.0], [0.0], [1.0]
- try:
- data1, data2 = self.__createSiftData(data1, data2)
- if data2 is None:
- raise ValueError("Unexpected None value")
- except Exception as e:
- # TODO: Display it on the GUI
- _logger.error(e)
- self.__setDefaultAlignmentMode()
- return
- else:
- assert(False)
-
- mode = self.getVisualizationMode()
- if mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG:
- data1 = self.__composeImage(data1, data2, mode)
- data2 = numpy.empty((0, 0))
- elif mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY:
- data1 = self.__composeImage(data1, data2, mode)
- data2 = numpy.empty((0, 0))
- elif mode == VisualizationMode.COMPOSITE_A_MINUS_B:
- data1 = self.__composeImage(data1, data2, mode)
- data2 = numpy.empty((0, 0))
- elif mode == VisualizationMode.ONLY_A:
- data2 = numpy.empty((0, 0))
- elif mode == VisualizationMode.ONLY_B:
- data1 = numpy.empty((0, 0))
-
- self.__data1, self.__data2 = data1, data2
- self.__plot.addImage(data1, z=0, legend="image1", resetzoom=False)
- self.__plot.addImage(data2, z=0, legend="image2", resetzoom=False)
- self.__image1 = self.__plot.getImage("image1")
- self.__image2 = self.__plot.getImage("image2")
- self.__updateKeyPoints()
-
- # Set the separator into the middle
- if self.__previousSeparatorPosition is None:
- value = self.__data1.shape[1] // 2
- self.__vline.setPosition(value, 0)
- value = self.__data1.shape[0] // 2
- self.__hline.setPosition(0, value)
- self.__updateSeparators()
-
- # Avoid to change the colormap range when the separator is moving
- # TODO: The colormap histogram will still be wrong
- mode1 = self.__getImageMode(data1)
- mode2 = self.__getImageMode(data2)
- if mode1 == "intensity" and mode1 == mode2:
- if self.__data1.size == 0:
- vmin = self.__data2.min()
- vmax = self.__data2.max()
- elif self.__data2.size == 0:
- vmin = self.__data1.min()
- vmax = self.__data1.max()
- else:
- vmin = min(self.__data1.min(), self.__data2.min())
- vmax = max(self.__data1.max(), self.__data2.max())
- colormap = self.getColormap()
- colormap.setVRange(vmin=vmin, vmax=vmax)
- self.__image1.setColormap(colormap)
- self.__image2.setColormap(colormap)
-
- def __getImageMode(self, image):
- """Returns a value identifying the way the image is stored in the
- array.
-
- :param numpy.ndarray image: Image to check
- :rtype: str
- """
- if len(image.shape) == 2:
- return "intensity"
- elif len(image.shape) == 3:
- if image.shape[2] == 3:
- return "rgb"
- elif image.shape[2] == 4:
- return "rgba"
- raise TypeError("'image' argument is not an image.")
-
- def __rescaleImage(self, image, shape):
- """Rescale an image to the requested shape.
-
- :rtype: numpy.ndarray
- """
- mode = self.__getImageMode(image)
- if mode == "intensity":
- data = self.__rescaleArray(image, shape)
- elif mode == "rgb":
- data = numpy.empty((shape[0], shape[1], 3), dtype=image.dtype)
- for c in range(3):
- data[:, :, c] = self.__rescaleArray(image[:, :, c], shape)
- elif mode == "rgba":
- data = numpy.empty((shape[0], shape[1], 4), dtype=image.dtype)
- for c in range(4):
- data[:, :, c] = self.__rescaleArray(image[:, :, c], shape)
- return data
-
- def __composeImage(self, data1, data2, mode):
- """Returns an RBG image containing composition of data1 and data2 in 2
- different channels
-
- :param numpy.ndarray data1: First image
- :param numpy.ndarray data1: Second image
- :param VisualizationMode mode: Composition mode.
- :rtype: numpy.ndarray
- """
- assert(data1.shape[0:2] == data2.shape[0:2])
- if mode == VisualizationMode.COMPOSITE_A_MINUS_B:
- # TODO: this calculation has no interest of generating a 'composed'
- # rgb image, this could be moved in an other function or doc
- # should be modified
- _type = data1.dtype
- result = data1.astype(numpy.float64) - data2.astype(numpy.float64)
- return result
- mode1 = self.__getImageMode(data1)
- if mode1 in ["rgb", "rgba"]:
- intensity1 = self.__luminosityImage(data1)
- vmin1, vmax1 = 0.0, 1.0
- else:
- intensity1 = data1
- vmin1, vmax1 = data1.min(), data1.max()
-
- mode2 = self.__getImageMode(data2)
- if mode2 in ["rgb", "rgba"]:
- intensity2 = self.__luminosityImage(data2)
- vmin2, vmax2 = 0.0, 1.0
- else:
- intensity2 = data2
- vmin2, vmax2 = data2.min(), data2.max()
-
- vmin, vmax = min(vmin1, vmin2) * 1.0, max(vmax1, vmax2) * 1.0
- shape = data1.shape
- result = numpy.empty((shape[0], shape[1], 3), dtype=numpy.uint8)
- a = (intensity1 - vmin) * (1.0 / (vmax - vmin)) * 255.0
- b = (intensity2 - vmin) * (1.0 / (vmax - vmin)) * 255.0
- if mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY:
- result[:, :, 0] = a
- result[:, :, 1] = (a + b) / 2
- result[:, :, 2] = b
- elif mode == VisualizationMode.COMPOSITE_RED_BLUE_GRAY_NEG:
- result[:, :, 0] = 255 - b
- result[:, :, 1] = 255 - (a + b) / 2
- result[:, :, 2] = 255 - a
- return result
-
- def __luminosityImage(self, image):
- """Returns the luminosity channel from an RBG(A) image.
- The alpha channel is ignored.
-
- :rtype: numpy.ndarray
- """
- mode = self.__getImageMode(image)
- assert(mode in ["rgb", "rgba"])
- is_uint8 = image.dtype.type == numpy.uint8
- # luminosity
- image = 0.21 * image[..., 0] + 0.72 * image[..., 1] + 0.07 * image[..., 2]
- if is_uint8:
- image = image / 255.0
- return image
-
- def __rescaleArray(self, image, shape):
- """Rescale a 2D array to the requested shape.
-
- :rtype: numpy.ndarray
- """
- y, x = numpy.ogrid[:shape[0], :shape[1]]
- y, x = y * 1.0 * (image.shape[0] - 1) / (shape[0] - 1), x * 1.0 * (image.shape[1] - 1) / (shape[1] - 1)
- b = silx.image.bilinear.BilinearImage(image)
- # TODO: could be optimized using strides
- x2d = numpy.zeros_like(y) + x
- y2d = numpy.zeros_like(x) + y
- result = b.map_coordinates((y2d, x2d))
- return result
-
- def __createMarginImage(self, image, size, transparent=False, center=False):
- """Returns a new image with margin to respect the requested size.
-
- :rtype: numpy.ndarray
- """
- assert(image.shape[0] <= size[0])
- assert(image.shape[1] <= size[1])
- if image.shape == size:
- return image
- mode = self.__getImageMode(image)
-
- if center:
- pos0 = size[0] // 2 - image.shape[0] // 2
- pos1 = size[1] // 2 - image.shape[1] // 2
- else:
- pos0, pos1 = 0, 0
-
- if mode == "intensity":
- data = numpy.zeros(size, dtype=image.dtype)
- data[pos0:pos0 + image.shape[0], pos1:pos1 + image.shape[1]] = image
- # TODO: It is maybe possible to put NaN on the margin
- else:
- if transparent:
- data = numpy.zeros((size[0], size[1], 4), dtype=numpy.uint8)
- else:
- data = numpy.zeros((size[0], size[1], 3), dtype=numpy.uint8)
- depth = min(data.shape[2], image.shape[2])
- data[pos0:pos0 + image.shape[0], pos1:pos1 + image.shape[1], 0:depth] = image[:, :, 0:depth]
- if transparent and depth == 3:
- data[pos0:pos0 + image.shape[0], pos1:pos1 + image.shape[1], 3] = 255
- return data
-
- def __toAffineTransformation(self, sift_result):
- """Returns an affine transformation from the sift result.
-
- :param dict sift_result: Result of sift when using `all_result=True`
- :rtype: AffineTransformation
- """
- offset = sift_result["offset"]
- matrix = sift_result["matrix"]
-
- tx = offset[0]
- ty = offset[1]
- a = matrix[0, 0]
- b = matrix[0, 1]
- c = matrix[1, 0]
- d = matrix[1, 1]
- rot = math.atan2(-b, a)
- sx = (-1.0 if a < 0 else 1.0) * math.sqrt(a**2 + b**2)
- sy = (-1.0 if d < 0 else 1.0) * math.sqrt(c**2 + d**2)
- return AffineTransformation(tx, ty, sx, sy, rot)
-
- def getTransformation(self):
- """Retuns the affine transformation applied to the second image to align
- it to the first image.
-
- This result is only valid for sift alignment.
-
- :rtype: Union[None,AffineTransformation]
- """
- return self.__transformation
-
- def __createSiftData(self, image, second_image):
- """Generate key points and aligned images from 2 images.
-
- If no keypoints matches, unaligned data are anyway returns.
-
- :rtype: Tuple(numpy.ndarray,numpy.ndarray)
- """
- devicetype = "GPU"
-
- # Compute base image
- sift_ocl = sift.SiftPlan(template=image, devicetype=devicetype)
- keypoints = sift_ocl(image)
-
- # Check image compatibility
- second_keypoints = sift_ocl(second_image)
- mp = sift.MatchPlan()
- match = mp(keypoints, second_keypoints)
- _logger.info("Number of Keypoints within image 1: %i" % keypoints.size)
- _logger.info(" within image 2: %i" % second_keypoints.size)
-
- self.__matching_keypoints = (match[:].x[:, 0],
- match[:].y[:, 0],
- match[:].scale[:, 0])
- matching_keypoints = match.shape[0]
- _logger.info("Matching keypoints: %i" % matching_keypoints)
- if matching_keypoints == 0:
- return image, second_image
-
- # TODO: Problem here is we have to compute 2 time sift
- # The first time to extract matching keypoints, second time
- # to extract the aligned image.
-
- # Normalize the second image
- sa = sift.LinearAlign(image, devicetype=devicetype)
- data1 = image
- # TODO: Create a sift issue: if data1 is RGB and data2 intensity
- # it returns None, while extracting manually keypoints (above) works
- result = sa.align(second_image, return_all=True)
- data2 = result["result"]
- self.__transformation = self.__toAffineTransformation(result)
- return data1, data2
-
- def setAutoResetZoom(self, activate=True):
- """
-
- :param bool activate: True if we want to activate the automatic
- plot reset zoom when setting images.
- """
- self._resetZoomActive = activate
-
- def isAutoResetZoom(self):
- """
-
- :return: True if the automatic call to resetzoom is activated
- :rtype: bool
- """
- return self._resetZoomActive
diff --git a/silx/gui/plot/ComplexImageView.py b/silx/gui/plot/ComplexImageView.py
deleted file mode 100644
index dc6bf63..0000000
--- a/silx/gui/plot/ComplexImageView.py
+++ /dev/null
@@ -1,518 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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__ = "24/04/2018"
-
-
-import logging
-import collections
-import numpy
-
-from ...utils.deprecation import deprecated
-from .. import qt, icons
-from .PlotWindow import Plot2D
-from . import items
-from .items import ImageComplexData
-from silx.gui.widgets.FloatEdit import FloatEdit
-
-_logger = logging.getLogger(__name__)
-
-
-# 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 = collections.OrderedDict([
- (ImageComplexData.ComplexMode.ABSOLUTE, ('math-amplitude', 'Amplitude')),
- (ImageComplexData.ComplexMode.SQUARE_AMPLITUDE,
- ('math-square-amplitude', 'Square amplitude')),
- (ImageComplexData.ComplexMode.PHASE, ('math-phase', 'Phase')),
- (ImageComplexData.ComplexMode.REAL, ('math-real', 'Real part')),
- (ImageComplexData.ComplexMode.IMAGINARY,
- ('math-imaginary', 'Imaginary part')),
- (ImageComplexData.ComplexMode.AMPLITUDE_PHASE,
- ('math-phase-color', 'Amplitude and Phase')),
- (ImageComplexData.ComplexMode.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 mode, info in self._MODES.items():
- icon, text = info
- action = qt.QAction(icons.getQIcon(icon), text, self)
- action.setData(mode)
- 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.getComplexMode())
- self._plot2DComplex.sigVisualizationModeChanged.connect(
- self._modeChanged)
-
- def _modeChanged(self, mode):
- """Handle change of visualization modes"""
- icon, text = self._MODES[mode]
- self.setIcon(icons.getQIcon(icon))
- self.setToolTip('Display the ' + text.lower())
- self._rangeDialogAction.setEnabled(
- mode == ImageComplexData.ComplexMode.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
- mode = action.data()
- if isinstance(mode, ImageComplexData.ComplexMode):
- self._plot2DComplex.setComplexMode(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`
- """
-
- ComplexMode = ImageComplexData.ComplexMode
- """Complex Modes enumeration"""
-
- sigDataChanged = qt.Signal()
- """Signal emitted when data has changed."""
-
- sigVisualizationModeChanged = qt.Signal(object)
- """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._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.setName('__ComplexImageView__complex_image__')
- self._plotImage.sigItemChanged.connect(self._itemChanged)
- self._plot2D.addItem(self._plotImage)
- self._plot2D.setActiveImage(self._plotImage.getName())
-
- toolBar = qt.QToolBar('Complex', self)
- toolBar.addWidget(
- _ComplexDataToolButton(parent=self, plot=self))
-
- self._plot2D.insertToolBar(self._plot2D.getProfileToolbar(), toolBar)
-
- def _itemChanged(self, event):
- """Handle item changed signal"""
- if event is items.ItemChangedType.DATA:
- self.sigDataChanged.emit()
- elif event is items.ItemChangedType.VISUALIZATION_MODE:
- mode = self.getComplexMode()
- self.sigVisualizationModeChanged.emit(mode)
-
- def getPlot(self):
- """Return the PlotWidget displaying the data"""
- return self._plot2D
-
- 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.complex64)
-
- previousData = self._plotImage.getComplexData(copy=False)
-
- self._plotImage.setData(data, copy=copy)
-
- if previousData.shape != data.shape:
- self.getPlot().resetZoom()
-
- 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 self._plotImage.getComplexData(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).
- """
- mode = self.getComplexMode()
- if mode in (self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
- return self._plotImage.getRgbaImageData(copy=copy)
- else:
- return self._plotImage.getData(copy=copy)
-
- # Backward compatibility
-
- Mode = ComplexMode
-
- @classmethod
- @deprecated(replacement='supportedComplexModes', since_version='0.11.0')
- def getSupportedVisualizationModes(cls):
- return cls.supportedComplexModes()
-
- @deprecated(replacement='setComplexMode', since_version='0.11.0')
- def setVisualizationMode(self, mode):
- return self.setComplexMode(mode)
-
- @deprecated(replacement='getComplexMode', since_version='0.11.0')
- def getVisualizationMode(self):
- return self.getComplexMode()
-
- # Image item proxy
-
- @staticmethod
- def supportedComplexModes():
- """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: List[ComplexMode]
- """
- return ImageComplexData.supportedComplexModes()
-
- def setComplexMode(self, mode):
- """Set the mode of visualization of the complex data.
-
- See :meth:`supportedComplexModes` for the list of
- supported modes.
-
- How-to change visualization mode::
-
- widget = ComplexImageView()
- widget.setComplexMode(ComplexImageView.ComplexMode.PHASE)
- # or
- widget.setComplexMode('phase')
-
- :param Unions[ComplexMode,str] mode: The mode to use.
- """
- self._plotImage.setComplexMode(mode)
-
- def getComplexMode(self):
- """Get the current visualization mode of the complex data.
-
- :rtype: ComplexMode
- """
- return self._plotImage.getComplexMode()
-
- 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._plotImage._setAmplitudeRangeInfo(max_, delta)
-
- 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._plotImage._getAmplitudeRangeInfo()
-
- def setColormap(self, colormap, mode=None):
- """Set the colormap to use for amplitude, phase, real or imaginary.
-
- WARNING: This colormap is not used when displaying both
- amplitude and phase.
-
- :param ~silx.gui.colors.Colormap colormap: The colormap
- :param ComplexMode mode: If specified, set the colormap of this specific mode
- """
- self._plotImage.setColormap(colormap, mode)
-
- def getColormap(self, mode=None):
- """Returns the colormap used to display the data.
-
- :param ComplexMode mode: If specified, set the colormap of this specific mode
- :rtype: ~silx.gui.colors.Colormap
- """
- return self._plotImage.getColormap(mode=mode)
-
- 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
deleted file mode 100644
index 5c9033e..0000000
--- a/silx/gui/plot/CurvesROIWidget.py
+++ /dev/null
@@ -1,1584 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""
-Widget to handle regions of interest (:class:`ROI`) on curves displayed in a
-:class:`PlotWindow`.
-
-This widget is meant to work with :class:`PlotWindow`.
-"""
-
-__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"]
-__license__ = "MIT"
-__date__ = "13/03/2018"
-
-from collections import OrderedDict
-import logging
-import os
-import sys
-import functools
-import numpy
-from silx.io import dictdump
-from silx.utils import deprecation
-from silx.utils.weakref import WeakMethodProxy
-from silx.utils.proxy import docstring
-from .. import icons, qt
-from silx.math.combo import min_max
-import weakref
-from silx.gui.widgets.TableWidget import TableWidget
-from . import items
-from .items.roi import _RegionOfInterestBase
-
-
-_logger = logging.getLogger(__name__)
-
-
-class CurvesROIWidget(qt.QWidget):
- """
- Widget displaying a table of ROI information.
-
- Implements also the following behavior:
-
- * if the roiTable has no ROI when showing create the default ICR one
-
- :param parent: See :class:`QWidget`
- :param str name: The title of this widget
- """
-
- sigROIWidgetSignal = qt.Signal(object)
- """Signal of ROIs modifications.
-
- Modification information if given as a dict with an 'event' key
- providing the type of events.
-
- Type of events:
-
- - AddROI, DelROI, LoadROI and ResetROI with keys: 'roilist', 'roidict'
- - selectionChanged with keys: 'row', 'col' 'roi', 'key', 'colheader',
- 'rowheader'
- """
-
- sigROISignal = qt.Signal(object)
-
- def __init__(self, parent=None, name=None, plot=None):
- super(CurvesROIWidget, self).__init__(parent)
- if name is not None:
- self.setWindowTitle(name)
- self.__lastSigROISignal = None
- """Store the last value emitted for the sigRoiSignal. In the case the
- active curve change we need to add this extra step in order to make
- sure we won't send twice the sigROISignal.
- This come from the fact sigROISignal is connected to the
- activeROIChanged signal which is emitted when raw and net counts
- values are changing but are not embed in the sigROISignal.
- """
- assert plot is not None
- self._plotRef = weakref.ref(plot)
- self._showAllMarkers = False
- self.currentROI = None
-
- layout = qt.QVBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
-
- self.headerLabel = qt.QLabel(self)
- self.headerLabel.setAlignment(qt.Qt.AlignHCenter)
- self.setHeader()
- layout.addWidget(self.headerLabel)
-
- widgetAllCheckbox = qt.QWidget(parent=self)
- self._showAllCheckBox = qt.QCheckBox("show all ROI",
- parent=widgetAllCheckbox)
- widgetAllCheckbox.setLayout(qt.QHBoxLayout())
- spacer = qt.QWidget(parent=widgetAllCheckbox)
- spacer.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed)
- widgetAllCheckbox.layout().addWidget(spacer)
- widgetAllCheckbox.layout().addWidget(self._showAllCheckBox)
- layout.addWidget(widgetAllCheckbox)
-
- self.roiTable = ROITable(self, plot=plot)
- rheight = self.roiTable.horizontalHeader().sizeHint().height()
- self.roiTable.setMinimumHeight(4 * rheight)
- layout.addWidget(self.roiTable)
- self._roiFileDir = qt.QDir.home().absolutePath()
- self._showAllCheckBox.toggled.connect(self.roiTable.showAllMarkers)
-
- hbox = qt.QWidget(self)
- hboxlayout = qt.QHBoxLayout(hbox)
- hboxlayout.setContentsMargins(0, 0, 0, 0)
- hboxlayout.setSpacing(0)
-
- hboxlayout.addStretch(0)
-
- self.addButton = qt.QPushButton(hbox)
- self.addButton.setText("Add ROI")
- self.addButton.setToolTip('Create a new ROI')
- self.delButton = qt.QPushButton(hbox)
- self.delButton.setText("Delete ROI")
- self.addButton.setToolTip('Remove the selected ROI')
- self.resetButton = qt.QPushButton(hbox)
- self.resetButton.setText("Reset")
- self.addButton.setToolTip('Clear all created ROIs. We only let the '
- 'default ROI')
-
- hboxlayout.addWidget(self.addButton)
- hboxlayout.addWidget(self.delButton)
- hboxlayout.addWidget(self.resetButton)
-
- hboxlayout.addStretch(0)
-
- self.loadButton = qt.QPushButton(hbox)
- self.loadButton.setText("Load")
- self.loadButton.setToolTip('Load ROIs from a .ini file')
- self.saveButton = qt.QPushButton(hbox)
- self.saveButton.setText("Save")
- self.loadButton.setToolTip('Save ROIs to a .ini file')
- hboxlayout.addWidget(self.loadButton)
- hboxlayout.addWidget(self.saveButton)
- layout.setStretchFactor(self.headerLabel, 0)
- layout.setStretchFactor(self.roiTable, 1)
- layout.setStretchFactor(hbox, 0)
-
- layout.addWidget(hbox)
-
- # Signal / Slot connections
- self.addButton.clicked.connect(self._add)
- self.delButton.clicked.connect(self._del)
- self.resetButton.clicked.connect(self._reset)
-
- self.loadButton.clicked.connect(self._load)
- self.saveButton.clicked.connect(self._save)
-
- self.roiTable.activeROIChanged.connect(self._emitCurrentROISignal)
-
- self._isConnected = False # True if connected to plot signals
- self._isInit = False
-
- # expose API
- self.getROIListAndDict = self.roiTable.getROIListAndDict
-
- def getPlotWidget(self):
- """Returns the associated PlotWidget or None
-
- :rtype: Union[~silx.gui.plot.PlotWidget,None]
- """
- return None if self._plotRef is None else self._plotRef()
-
- def showEvent(self, event):
- self._visibilityChangedHandler(visible=True)
- qt.QWidget.showEvent(self, event)
-
- @property
- def roiFileDir(self):
- """The directory from which to load/save ROI from/to files."""
- if not os.path.isdir(self._roiFileDir):
- self._roiFileDir = qt.QDir.home().absolutePath()
- return self._roiFileDir
-
- @roiFileDir.setter
- def roiFileDir(self, roiFileDir):
- self._roiFileDir = str(roiFileDir)
-
- def setRois(self, rois, order=None):
- return self.roiTable.setRois(rois, order)
-
- def getRois(self, order=None):
- return self.roiTable.getRois(order)
-
- def setMiddleROIMarkerFlag(self, flag=True):
- return self.roiTable.setMiddleROIMarkerFlag(flag)
-
- def _add(self):
- """Add button clicked handler"""
- def getNextRoiName():
- rois = self.roiTable.getRois(order=None)
- roisNames = []
- [roisNames.append(roiName) for roiName in rois]
- nrois = len(rois)
- if nrois == 0:
- return "ICR"
- else:
- i = 1
- newroi = "newroi %d" % i
- while newroi in roisNames:
- i += 1
- newroi = "newroi %d" % i
- return newroi
- roi = ROI(name=getNextRoiName())
-
- if roi.getName() == "ICR":
- roi.setType("Default")
- else:
- roi.setType(self.getPlotWidget().getXAxis().getLabel())
-
- xmin, xmax = self.getPlotWidget().getXAxis().getLimits()
- fromdata = xmin + 0.25 * (xmax - xmin)
- todata = xmin + 0.75 * (xmax - xmin)
- if roi.isICR():
- fromdata, dummy0, todata, dummy1 = self._getAllLimits()
- roi.setFrom(fromdata)
- roi.setTo(todata)
- self.roiTable.addRoi(roi)
-
- # back compatibility pymca roi signals
- ddict = {}
- ddict['event'] = "AddROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
- self.sigROIWidgetSignal.emit(ddict)
- # end back compatibility pymca roi signals
-
- def _del(self):
- """Delete button clicked handler"""
- self.roiTable.deleteActiveRoi()
-
- # back compatibility pymca roi signals
- ddict = {}
- ddict['event'] = "DelROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
- self.sigROIWidgetSignal.emit(ddict)
- # end back compatibility pymca roi signals
-
- def _reset(self):
- """Reset button clicked handler"""
- self.roiTable.clear()
- old = self.blockSignals(True) # avoid several sigROISignal emission
- self._add()
- self.blockSignals(old)
-
- # back compatibility pymca roi signals
- ddict = {}
- ddict['event'] = "ResetROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
- self.sigROIWidgetSignal.emit(ddict)
- # end back compatibility pymca roi signals
-
- def _load(self):
- """Load button clicked handler"""
- dialog = qt.QFileDialog(self)
- dialog.setNameFilters(
- ['INI File *.ini', 'JSON File *.json', 'All *.*'])
- dialog.setFileMode(qt.QFileDialog.ExistingFile)
- dialog.setDirectory(self.roiFileDir)
- if not dialog.exec_():
- dialog.close()
- return
-
- # pyflakes bug http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=666494
- outputFile = dialog.selectedFiles()[0]
- dialog.close()
-
- self.roiFileDir = os.path.dirname(outputFile)
- self.roiTable.load(outputFile)
-
- # back compatibility pymca roi signals
- ddict = {}
- ddict['event'] = "LoadROI"
- ddict['roilist'] = self.roiTable.roidict.values()
- ddict['roidict'] = self.roiTable.roidict
- self.sigROIWidgetSignal.emit(ddict)
- # end back compatibility pymca roi signals
-
- def load(self, filename):
- """Load ROI widget information from a file storing a dict of ROI.
-
- :param str filename: The file from which to load ROI
- """
- self.roiTable.load(filename)
-
- def _save(self):
- """Save button clicked handler"""
- dialog = qt.QFileDialog(self)
- dialog.setNameFilters(['INI File *.ini', 'JSON File *.json'])
- dialog.setFileMode(qt.QFileDialog.AnyFile)
- dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
- dialog.setDirectory(self.roiFileDir)
- if not dialog.exec_():
- dialog.close()
- return
-
- outputFile = dialog.selectedFiles()[0]
- extension = '.' + dialog.selectedNameFilter().split('.')[-1]
- dialog.close()
-
- if not outputFile.endswith(extension):
- outputFile += extension
-
- if os.path.exists(outputFile):
- try:
- os.remove(outputFile)
- except IOError:
- msg = qt.QMessageBox(self)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setText("Input Output Error: %s" % (sys.exc_info()[1]))
- msg.exec_()
- return
- self.roiFileDir = os.path.dirname(outputFile)
- self.save(outputFile)
-
- def save(self, filename):
- """Save current ROIs of the widget as a dict of ROI to a file.
-
- :param str filename: The file to which to save the ROIs
- """
- self.roiTable.save(filename)
-
- def setHeader(self, text='ROIs'):
- """Set the header text of this widget"""
- self.headerLabel.setText("<b>%s<\b>" % text)
-
- @deprecation.deprecated(replacement="calculateRois",
- reason="CamelCase convention",
- since_version="0.7")
- def calculateROIs(self, *args, **kw):
- self.calculateRois(*args, **kw)
-
- def calculateRois(self, roiList=None, roiDict=None):
- """Compute ROI information"""
- return self.roiTable.calculateRois()
-
- def showAllMarkers(self, _show=True):
- self.roiTable.showAllMarkers(_show)
-
- def _getAllLimits(self):
- """Retrieve the limits based on the curves."""
- plot = self.getPlotWidget()
- curves = () if plot is None else plot.getAllCurves()
- if not curves:
- return 1.0, 1.0, 100., 100.
-
- xmin, ymin = None, None
- xmax, ymax = None, None
-
- for curve in curves:
- x = curve.getXData(copy=False)
- y = curve.getYData(copy=False)
- if xmin is None:
- xmin = x.min()
- else:
- xmin = min(xmin, x.min())
- if xmax is None:
- xmax = x.max()
- else:
- xmax = max(xmax, x.max())
- if ymin is None:
- ymin = y.min()
- else:
- ymin = min(ymin, y.min())
- if ymax is None:
- ymax = y.max()
- else:
- ymax = max(ymax, y.max())
-
- return xmin, ymin, xmax, ymax
-
- def showEvent(self, event):
- self._visibilityChangedHandler(visible=True)
- qt.QWidget.showEvent(self, event)
-
- def hideEvent(self, event):
- self._visibilityChangedHandler(visible=False)
- qt.QWidget.hideEvent(self, event)
-
- def _visibilityChangedHandler(self, visible):
- """Handle widget's visibility updates.
-
- It is connected to plot signals only when visible.
- """
- if visible:
- # if no ROI existing yet, add the default one
- if self.roiTable.rowCount() == 0:
- old = self.blockSignals(True) # avoid several sigROISignal emission
- self._add()
- self.blockSignals(old)
- self.calculateRois()
-
- def fillFromROIDict(self, *args, **kwargs):
- self.roiTable.fillFromROIDict(*args, **kwargs)
-
- def _emitCurrentROISignal(self):
- ddict = {}
- ddict['event'] = "currentROISignal"
- if self.roiTable.activeRoi is not None:
- ddict['ROI'] = self.roiTable.activeRoi.toDict()
- ddict['current'] = self.roiTable.activeRoi.getName()
- else:
- ddict['current'] = None
-
- if self.__lastSigROISignal != ddict:
- self.__lastSigROISignal = ddict
- self.sigROISignal.emit(ddict)
-
- @property
- def currentRoi(self):
- return self.roiTable.activeRoi
-
-
-class _FloatItem(qt.QTableWidgetItem):
- """
- Simple QTableWidgetItem overloading the < operator to deal with ordering
- """
- def __init__(self):
- qt.QTableWidgetItem.__init__(self, type=qt.QTableWidgetItem.Type)
-
- def __lt__(self, other):
- if self.text() in ('', ROITable.INFO_NOT_FOUND):
- return False
- if other.text() in ('', ROITable.INFO_NOT_FOUND):
- return True
- return float(self.text()) < float(other.text())
-
-
-class ROITable(TableWidget):
- """Table widget displaying ROI information.
-
- See :class:`QTableWidget` for constructor arguments.
-
- Behavior: listen at the active curve changed only when the widget is
- visible. Otherwise won't compute the row and net counts...
- """
-
- activeROIChanged = qt.Signal()
- """Signal emitted when the active roi changed or when the value of the
- active roi are changing"""
-
- COLUMNS_INDEX = OrderedDict([
- ('ID', 0),
- ('ROI', 1),
- ('Type', 2),
- ('From', 3),
- ('To', 4),
- ('Raw Counts', 5),
- ('Net Counts', 6),
- ('Raw Area', 7),
- ('Net Area', 8),
- ])
-
- COLUMNS = list(COLUMNS_INDEX.keys())
-
- INFO_NOT_FOUND = '????????'
-
- def __init__(self, parent=None, plot=None, rois=None):
- super(ROITable, self).__init__(parent)
- self._showAllMarkers = False
- self._userIsEditingRoi = False
- """bool used to avoid conflict when editing the ROI object"""
- self._isConnected = False
- self._roiToItems = {}
- self._roiDict = {}
- """dict of ROI object. Key is ROi id, value is the ROI object"""
- self._markersHandler = _RoiMarkerManager()
-
- """
- Associate for each marker legend used when the `_showAllMarkers` option
- is active a roi.
- """
- self.setColumnCount(len(self.COLUMNS))
- self.setPlot(plot)
- self.__setTooltip()
- self.setSortingEnabled(True)
- self.itemChanged.connect(self._itemChanged)
-
- @property
- def roidict(self):
- return self._getRoiDict()
-
- @property
- def activeRoi(self):
- return self._markersHandler._activeRoi
-
- def _getRoiDict(self):
- ddict = {}
- for id in self._roiDict:
- ddict[self._roiDict[id].getName()] = self._roiDict[id]
- return ddict
-
- def clear(self):
- """
- .. note:: clear the interface only. keep the roidict...
- """
- self._markersHandler.clear()
- self._roiToItems = {}
- self._roiDict = {}
-
- qt.QTableWidget.clear(self)
- self.setRowCount(0)
- self.setHorizontalHeaderLabels(self.COLUMNS)
- header = self.horizontalHeader()
- if hasattr(header, 'setSectionResizeMode'): # Qt5
- header.setSectionResizeMode(qt.QHeaderView.ResizeToContents)
- else: # Qt4
- header.setResizeMode(qt.QHeaderView.ResizeToContents)
- self.sortByColumn(0, qt.Qt.AscendingOrder)
- self.hideColumn(self.COLUMNS_INDEX['ID'])
-
- def setPlot(self, plot):
- self.clear()
- self.plot = plot
-
- def __setTooltip(self):
- self.horizontalHeaderItem(self.COLUMNS_INDEX['ROI']).setToolTip(
- 'Region of interest identifier')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['Type']).setToolTip(
- 'Type of the ROI')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['From']).setToolTip(
- 'X-value of the min point')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['To']).setToolTip(
- 'X-value of the max point')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['Raw Counts']).setToolTip(
- 'Estimation of the integral between y=0 and the selected curve')
- self.horizontalHeaderItem(self.COLUMNS_INDEX['Net Counts']).setToolTip(
- 'Estimation of the integral between the segment [maxPt, minPt] '
- 'and the selected curve')
-
- def setRois(self, rois, order=None):
- """Set the ROIs by providing a dictionary of ROI information.
-
- 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")
-
-
- :param roidict: Dictionary of ROIs
- :param str order: Field used for ordering the ROIs.
- One of "from", "to", "type".
- None (default) for no ordering, or same order as specified
- in parameter ``roidict`` if provided as an OrderedDict.
- """
- assert order in [None, "from", "to", "type"]
- self.clear()
-
- # backward compatibility since 0.10.0
- if isinstance(rois, dict):
- for roiName, roi in rois.items():
- if isinstance(roi, ROI):
- _roi = roi
- else:
- roi['name'] = roiName
- _roi = ROI._fromDict(roi)
- self.addRoi(_roi)
- else:
- for roi in rois:
- assert isinstance(roi, ROI)
- self.addRoi(roi)
- self._updateMarkers()
-
- def addRoi(self, roi):
- """
-
- :param :class:`ROI` roi: roi to add to the table
- """
- assert isinstance(roi, ROI)
- self._getItem(name='ID', row=None, roi=roi)
- self._roiDict[roi.getID()] = roi
- self._markersHandler.add(roi, _RoiMarkerHandler(roi, self.plot))
- self._updateRoiInfo(roi.getID())
- callback = functools.partial(WeakMethodProxy(self._updateRoiInfo),
- roi.getID())
- roi.sigChanged.connect(callback)
- # set it as the active one
- self.setActiveRoi(roi)
-
- def _getItem(self, name, row, roi):
- if row:
- item = self.item(row, self.COLUMNS_INDEX[name])
- else:
- item = None
- if item:
- return item
- else:
- if name == 'ID':
- assert roi
- if roi.getID() in self._roiToItems:
- return self._roiToItems[roi.getID()]
- else:
- # create a new row
- row = self.rowCount()
- self.setRowCount(self.rowCount() + 1)
- item = qt.QTableWidgetItem(str(roi.getID()),
- type=qt.QTableWidgetItem.Type)
- self._roiToItems[roi.getID()] = item
- elif name == 'ROI':
- item = qt.QTableWidgetItem(roi.getName() if roi else '',
- type=qt.QTableWidgetItem.Type)
- if roi.getName().upper() in ('ICR', 'DEFAULT'):
- item.setFlags(qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled)
- else:
- item.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsEditable)
- elif name == 'Type':
- item = qt.QTableWidgetItem(type=qt.QTableWidgetItem.Type)
- item.setFlags((qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled))
- elif name in ('To', 'From'):
- item = _FloatItem()
- if roi.getName().upper() in ('ICR', 'DEFAULT'):
- item.setFlags(qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled)
- else:
- item.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsEditable)
- elif name in ('Raw Counts', 'Net Counts', 'Raw Area', 'Net Area'):
- item = _FloatItem()
- item.setFlags((qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled))
- else:
- raise ValueError('item type not recognized')
-
- self.setItem(row, self.COLUMNS_INDEX[name], item)
- return item
-
- def _itemChanged(self, item):
- def getRoi():
- IDItem = self.item(item.row(), self.COLUMNS_INDEX['ID'])
- assert IDItem
- id = int(IDItem.text())
- assert id in self._roiDict
- roi = self._roiDict[id]
- return roi
-
- def signalChanged(roi):
- if self.activeRoi and roi.getID() == self.activeRoi.getID():
- self.activeROIChanged.emit()
-
- self._userIsEditingRoi = True
- if item.column() in (self.COLUMNS_INDEX['To'], self.COLUMNS_INDEX['From']):
- roi = getRoi()
-
- if item.text() not in ('', self.INFO_NOT_FOUND):
- try:
- value = float(item.text())
- except ValueError:
- value = 0
- changed = False
- if item.column() == self.COLUMNS_INDEX['To']:
- if value != roi.getTo():
- roi.setTo(value)
- changed = True
- else:
- assert(item.column() == self.COLUMNS_INDEX['From'])
- if value != roi.getFrom():
- roi.setFrom(value)
- changed = True
- if changed:
- self._updateMarker(roi.getName())
- signalChanged(roi)
-
- if item.column() is self.COLUMNS_INDEX['ROI']:
- roi = getRoi()
- if roi.getName() != item.text():
- roi.setName(item.text())
- self._markersHandler.getMarkerHandler(roi.getID()).updateTexts()
- signalChanged(roi)
-
- self._userIsEditingRoi = False
-
- def deleteActiveRoi(self):
- """
- remove the current active roi
- """
- activeItems = self.selectedItems()
- if len(activeItems) == 0:
- return
- old = self.blockSignals(True) # avoid several emission of sigROISignal
- roiToRm = set()
- for item in activeItems:
- row = item.row()
- itemID = self.item(row, self.COLUMNS_INDEX['ID'])
- roiToRm.add(self._roiDict[int(itemID.text())])
- [self.removeROI(roi) for roi in roiToRm]
- self.blockSignals(old)
- self.setActiveRoi(None)
-
- def removeROI(self, roi):
- """
- remove the requested roi
-
- :param str name: the name of the roi to remove from the table
- """
- if roi and roi.getID() in self._roiToItems:
- item = self._roiToItems[roi.getID()]
- self.removeRow(item.row())
- del self._roiToItems[roi.getID()]
-
- assert roi.getID() in self._roiDict
- del self._roiDict[roi.getID()]
- self._markersHandler.remove(roi)
-
- callback = functools.partial(WeakMethodProxy(self._updateRoiInfo),
- roi.getID())
- roi.sigChanged.connect(callback)
-
- def setActiveRoi(self, roi):
- """
- Define the given roi as the active one.
-
- .. warning:: this roi should already be registred / added to the table
-
- :param :class:`ROI` roi: the roi to defined as active
- """
- if roi is None:
- self.clearSelection()
- self._markersHandler.setActiveRoi(None)
- self.activeROIChanged.emit()
- else:
- assert isinstance(roi, ROI)
- if roi and roi.getID() in self._roiToItems.keys():
- # avoid several call back to setActiveROI
- old = self.blockSignals(True)
- self.selectRow(self._roiToItems[roi.getID()].row())
- self.blockSignals(old)
- self._markersHandler.setActiveRoi(roi)
- self.activeROIChanged.emit()
-
- def _updateRoiInfo(self, roiID):
- if self._userIsEditingRoi is True:
- return
- if roiID not in self._roiDict:
- return
- roi = self._roiDict[roiID]
- if roi.isICR():
- activeCurve = self.plot.getActiveCurve()
- if activeCurve:
- xData = activeCurve.getXData()
- if len(xData) > 0:
- min, max = min_max(xData)
- roi.blockSignals(True)
- roi.setFrom(min)
- roi.setTo(max)
- roi.blockSignals(False)
-
- itemID = self._getItem(name='ID', roi=roi, row=None)
- itemName = self._getItem(name='ROI', row=itemID.row(), roi=roi)
- itemName.setText(roi.getName())
-
- itemType = self._getItem(name='Type', row=itemID.row(), roi=roi)
- itemType.setText(roi.getType() or self.INFO_NOT_FOUND)
-
- itemFrom = self._getItem(name='From', row=itemID.row(), roi=roi)
- fromdata = str(roi.getFrom()) if roi.getFrom() is not None else self.INFO_NOT_FOUND
- itemFrom.setText(fromdata)
-
- itemTo = self._getItem(name='To', row=itemID.row(), roi=roi)
- todata = str(roi.getTo()) if roi.getTo() is not None else self.INFO_NOT_FOUND
- itemTo.setText(todata)
-
- rawCounts, netCounts = roi.computeRawAndNetCounts(
- curve=self.plot.getActiveCurve(just_legend=False))
- itemRawCounts = self._getItem(name='Raw Counts', row=itemID.row(),
- roi=roi)
- rawCounts = str(rawCounts) if rawCounts is not None else self.INFO_NOT_FOUND
- itemRawCounts.setText(rawCounts)
-
- itemNetCounts = self._getItem(name='Net Counts', row=itemID.row(),
- roi=roi)
- netCounts = str(netCounts) if netCounts is not None else self.INFO_NOT_FOUND
- itemNetCounts.setText(netCounts)
-
- rawArea, netArea = roi.computeRawAndNetArea(
- curve=self.plot.getActiveCurve(just_legend=False))
- itemRawArea = self._getItem(name='Raw Area', row=itemID.row(),
- roi=roi)
- rawArea = str(rawArea) if rawArea is not None else self.INFO_NOT_FOUND
- itemRawArea.setText(rawArea)
-
- itemNetArea = self._getItem(name='Net Area', row=itemID.row(),
- roi=roi)
- netArea = str(netArea) if netArea is not None else self.INFO_NOT_FOUND
- itemNetArea.setText(netArea)
-
- if self.activeRoi and roi.getID() == self.activeRoi.getID():
- self.activeROIChanged.emit()
-
- def currentChanged(self, current, previous):
- if previous and current.row() != previous.row() and current.row() >= 0:
- roiItem = self.item(current.row(),
- self.COLUMNS_INDEX['ID'])
-
- assert roiItem
- self.setActiveRoi(self._roiDict[int(roiItem.text())])
- self._markersHandler.updateAllMarkers()
- qt.QTableWidget.currentChanged(self, current, previous)
-
- @deprecation.deprecated(reason="Removed",
- replacement="roidict and roidict.values()",
- since_version="0.10.0")
- def getROIListAndDict(self):
- """
-
- :return: the list of roi objects and the dictionary of roi name to roi
- object.
- """
- roidict = self._roiDict
- return list(roidict.values()), roidict
-
- def calculateRois(self, roiList=None, roiDict=None):
- """
- Update values of all registred rois (raw and net counts in particular)
-
- :param roiList: deprecated parameter
- :param roiDict: deprecated parameter
- """
- if roiDict:
- deprecation.deprecated_warning(name='roiDict', type_='Parameter',
- reason='Unused parameter',
- since_version="0.10.0")
- if roiList:
- deprecation.deprecated_warning(name='roiList', type_='Parameter',
- reason='Unused parameter',
- since_version="0.10.0")
-
- for roiID in self._roiDict:
- self._updateRoiInfo(roiID)
-
- def _updateMarker(self, roiID):
- """Make sure the marker of the given roi name is updated"""
- if self._showAllMarkers or (self.activeRoi
- and self.activeRoi.getName() == roiID):
- self._updateMarkers()
-
- def _updateMarkers(self):
- if self._showAllMarkers is True:
- self._markersHandler.updateMarkers()
- else:
- if not self.activeRoi or not self.plot:
- return
- assert isinstance(self.activeRoi, ROI)
- markerHandler = self._markersHandler.getMarkerHandler(self.activeRoi.getID())
- if markerHandler is not None:
- markerHandler.updateMarkers()
-
- def getRois(self, order):
- """
- Return the currently defined ROIs, as an ordered dict.
-
- The dictionary keys are the ROI names.
- Each value is a :class:`ROI` object..
-
- :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.
- :return: Ordered dictionary of ROI information
- """
-
- if order is None or order.lower() == "none":
- ordered_roilist = list(self._roiDict.values())
- res = OrderedDict([(roi.getName(), self._roiDict[roi.getID()]) for roi in ordered_roilist])
- else:
- assert order in ["from", "to", "type", "netcounts", "rawcounts"]
- ordered_roilist = sorted(self._roiDict.keys(),
- key=lambda roi_id: self._roiDict[roi_id].get(order))
- res = OrderedDict([(roi.getName(), self._roiDict[id]) for id in ordered_roilist])
-
- return res
-
- def save(self, filename):
- """
- Save current ROIs of the widget as a dict of ROI to a file.
-
- :param str filename: The file to which to save the ROIs
- """
- roilist = []
- roidict = {}
- for roiID, roi in self._roiDict.items():
- roilist.append(roi.toDict())
- roidict[roi.getName()] = roi.toDict()
- datadict = {'ROI': {'roilist': roilist, 'roidict': roidict}}
- dictdump.dump(datadict, filename)
-
- def load(self, filename):
- """
- Load ROI widget information from a file storing a dict of ROI.
-
- :param str filename: The file from which to load ROI
- """
- roisDict = dictdump.load(filename)
- rois = []
-
- # Remove rawcounts and netcounts from ROIs
- for roiDict in roisDict['ROI']['roidict'].values():
- roiDict.pop('rawcounts', None)
- roiDict.pop('netcounts', None)
- rois.append(ROI._fromDict(roiDict))
-
- self.setRois(rois)
-
- def showAllMarkers(self, _show=True):
- """
-
- :param bool _show: if true show all the markers of all the ROIs
- boundaries otherwise will only show the one of
- the active ROI.
- """
- self._markersHandler.setShowAllMarkers(_show)
-
- def setMiddleROIMarkerFlag(self, flag=True):
- """
- Activate or deactivate middle marker.
-
- This allows shifting both min and max limits at once, by dragging
- a marker located in the middle.
-
- :param bool flag: True to activate middle ROI marker
- """
- self._markersHandler._middleROIMarkerFlag = flag
-
- def _handleROIMarkerEvent(self, ddict):
- """Handle plot signals related to marker events."""
- if ddict['event'] == 'markerMoved':
- label = ddict['label']
- roiID = self._markersHandler.getRoiID(markerID=label)
- if roiID is not None:
- # avoid several emission of sigROISignal
- old = self.blockSignals(True)
- self._markersHandler.changePosition(markerID=label,
- x=ddict['x'])
- self.blockSignals(old)
- self._updateRoiInfo(roiID)
-
- def showEvent(self, event):
- self._visibilityChangedHandler(visible=True)
- qt.QWidget.showEvent(self, event)
-
- def hideEvent(self, event):
- self._visibilityChangedHandler(visible=False)
- qt.QWidget.hideEvent(self, event)
-
- def _visibilityChangedHandler(self, visible):
- """Handle widget's visibility updates.
-
- It is connected to plot signals only when visible.
- """
- if visible:
- assert self.plot
- if self._isConnected is False:
- self.plot.sigPlotSignal.connect(self._handleROIMarkerEvent)
- self.plot.sigActiveCurveChanged.connect(self._activeCurveChanged)
- self._isConnected = True
- self.calculateRois()
- else:
- if self._isConnected:
- self.plot.sigPlotSignal.disconnect(self._handleROIMarkerEvent)
- self.plot.sigActiveCurveChanged.disconnect(self._activeCurveChanged)
- self._isConnected = False
-
- def _activeCurveChanged(self, curve):
- self.calculateRois()
-
- def setCountsVisible(self, visible):
- """
- Display the columns relative to areas or not
-
- :param bool visible: True if the columns 'Raw Area' and 'Net Area'
- should be visible.
- """
- if visible is True:
- self.showColumn(self.COLUMNS_INDEX['Raw Counts'])
- self.showColumn(self.COLUMNS_INDEX['Net Counts'])
- else:
- self.hideColumn(self.COLUMNS_INDEX['Raw Counts'])
- self.hideColumn(self.COLUMNS_INDEX['Net Counts'])
-
- def setAreaVisible(self, visible):
- """
- Display the columns relative to areas or not
-
- :param bool visible: True if the columns 'Raw Area' and 'Net Area'
- should be visible.
- """
- if visible is True:
- self.showColumn(self.COLUMNS_INDEX['Raw Area'])
- self.showColumn(self.COLUMNS_INDEX['Net Area'])
- else:
- self.hideColumn(self.COLUMNS_INDEX['Raw Area'])
- self.hideColumn(self.COLUMNS_INDEX['Net Area'])
-
- def fillFromROIDict(self, roilist=(), roidict=None, currentroi=None):
- """
- This function API is kept for compatibility.
- But `setRois` should be preferred.
-
- Set the ROIs by providing a list of ROI names and a dictionary
- of ROI information for each ROI.
- The ROI names must match an existing dictionary key.
- The name list is used to provide an order for the ROIs.
- The dictionary's values are sub-dictionaries containing 3
- mandatory 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")
-
- :param roilist: List of ROI names (keys of roidict)
- :type roilist: List
- :param dict roidict: Dict of ROI information
- :param currentroi: Name of the selected ROI or None (no selection)
- """
- if roidict is not None:
- self.setRois(roidict)
- else:
- self.setRois(roilist)
- if currentroi:
- self.setActiveRoi(currentroi)
-
-
-_indexNextROI = 0
-
-
-class ROI(_RegionOfInterestBase):
- """The Region Of Interest is defined by:
-
- - A name
- - A type. The type is the label of the x axis. This can be used to apply or
- not some ROI to a curve and do some post processing.
- - The x coordinate of the left limit (fromdata)
- - The x coordinate of the right limit (todata)
-
- :param str: name of the ROI
- :param fromdata: left limit of the roi
- :param todata: right limit of the roi
- :param type: type of the ROI
- """
-
- sigChanged = qt.Signal()
- """Signal emitted when the ROI is edited"""
-
- def __init__(self, name, fromdata=None, todata=None, type_=None):
- _RegionOfInterestBase.__init__(self)
- self.setName(name)
- global _indexNextROI
- self._id = _indexNextROI
- _indexNextROI += 1
-
- self._fromdata = fromdata
- self._todata = todata
- self._type = type_ or 'Default'
-
- self.sigItemChanged.connect(self.__itemChanged)
-
- def __itemChanged(self, event):
- """Handle name change"""
- if event == items.ItemChangedType.NAME:
- self.sigChanged.emit()
-
- def getID(self):
- """
-
- :return int: the unique ID of the ROI
- """
- return self._id
-
- def setType(self, type_):
- """
-
- :param str type_:
- """
- if self._type != type_:
- self._type = type_
- self.sigChanged.emit()
-
- def getType(self):
- """
-
- :return str: the type of the ROI.
- """
- return self._type
-
- def setFrom(self, frm):
- """
-
- :param frm: set x coordinate of the left limit
- """
- if self._fromdata != frm:
- self._fromdata = frm
- self.sigChanged.emit()
-
- def getFrom(self):
- """
-
- :return: x coordinate of the left limit
- """
- return self._fromdata
-
- def setTo(self, to):
- """
-
- :param to: x coordinate of the right limit
- """
- if self._todata != to:
- self._todata = to
- self.sigChanged.emit()
-
- def getTo(self):
- """
-
- :return: x coordinate of the right limit
- """
- return self._todata
-
- def getMiddle(self):
- """
-
- :return: middle position between 'from' and 'to' values
- """
- return 0.5 * (self.getFrom() + self.getTo())
-
- def toDict(self):
- """
-
- :return: dict containing the roi parameters
- """
- ddict = {
- 'type': self._type,
- 'name': self.getName(),
- 'from': self._fromdata,
- 'to': self._todata,
- }
- if hasattr(self, '_extraInfo'):
- ddict.update(self._extraInfo)
- return ddict
-
- @staticmethod
- def _fromDict(dic):
- assert 'name' in dic
- roi = ROI(name=dic['name'])
- roi._extraInfo = {}
- for key in dic:
- if key == 'from':
- roi.setFrom(dic['from'])
- elif key == 'to':
- roi.setTo(dic['to'])
- elif key == 'type':
- roi.setType(dic['type'])
- else:
- roi._extraInfo[key] = dic[key]
-
- return roi
-
- def isICR(self):
- """
-
- :return: True if the ROI is the `ICR`
- """
- return self.getName() == 'ICR'
-
- def computeRawAndNetCounts(self, curve):
- """Compute the Raw and net counts in the ROI for the given curve.
-
- - Raw count: Points values sum of the curve in the defined Region Of
- Interest.
-
- .. image:: img/rawCounts.png
-
- - Net count: Raw counts minus background
-
- .. image:: img/netCounts.png
-
- :param CurveItem curve:
- :return tuple: rawCount, netCount
- """
- assert isinstance(curve, items.Curve) or curve is None
-
- if curve is None:
- return None, None
-
- x = curve.getXData(copy=False)
- y = curve.getYData(copy=False)
-
- idx = numpy.nonzero((self._fromdata <= x) &
- (x <= self._todata))[0]
- if len(idx):
- xw = x[idx]
- yw = y[idx]
- rawCounts = yw.sum(dtype=numpy.float64)
- deltaX = xw[-1] - xw[0]
- deltaY = yw[-1] - yw[0]
- if deltaX > 0.0:
- slope = (deltaY / deltaX)
- background = yw[0] + slope * (xw - xw[0])
- netCounts = (rawCounts -
- background.sum(dtype=numpy.float64))
- else:
- netCounts = 0.0
- else:
- rawCounts = 0.0
- netCounts = 0.0
- return rawCounts, netCounts
-
- def computeRawAndNetArea(self, curve):
- """Compute the Raw and net counts in the ROI for the given curve.
-
- - Raw area: integral of the curve between the min ROI point and the
- max ROI point to the y = 0 line.
-
- .. image:: img/rawArea.png
-
- - Net area: Raw counts minus background
-
- .. image:: img/netArea.png
-
- :param CurveItem curve:
- :return tuple: rawArea, netArea
- """
- assert isinstance(curve, items.Curve) or curve is None
-
- if curve is None:
- return None, None
-
- x = curve.getXData(copy=False)
- y = curve.getYData(copy=False)
-
- y = y[(x >= self._fromdata) & (x <= self._todata)]
- x = x[(x >= self._fromdata) & (x <= self._todata)]
-
- if x.size == 0:
- return 0.0, 0.0
-
- rawArea = numpy.trapz(y, x=x)
- # to speed up and avoid an intersection calculation we are taking the
- # closest index to the ROI
- closestXLeftIndex = (numpy.abs(x - self.getFrom())).argmin()
- closestXRightIndex = (numpy.abs(x - self.getTo())).argmin()
- yBackground = y[closestXLeftIndex], y[closestXRightIndex]
- background = numpy.trapz(yBackground, x=x)
- netArea = rawArea - background
- return rawArea, netArea
-
- @docstring(_RegionOfInterestBase)
- def contains(self, position):
- return self._fromdata <= position[0] <= self._todata
-
-
-class _RoiMarkerManager(object):
- """
- Deal with all the ROI markers
- """
- def __init__(self):
- self._roiMarkerHandlers = {}
- self._middleROIMarkerFlag = False
- self._showAllMarkers = False
- self._activeRoi = None
-
- def setActiveRoi(self, roi):
- self._activeRoi = roi
- self.updateAllMarkers()
-
- def setShowAllMarkers(self, show):
- if show != self._showAllMarkers:
- self._showAllMarkers = show
- self.updateAllMarkers()
-
- def add(self, roi, markersHandler):
- assert isinstance(roi, ROI)
- assert isinstance(markersHandler, _RoiMarkerHandler)
- if roi.getID() in self._roiMarkerHandlers:
- raise ValueError('roi with the same ID already existing')
- else:
- self._roiMarkerHandlers[roi.getID()] = markersHandler
-
- def getMarkerHandler(self, roiID):
- if roiID in self._roiMarkerHandlers:
- return self._roiMarkerHandlers[roiID]
- else:
- return None
-
- def clear(self):
- roisHandler = list(self._roiMarkerHandlers.values())
- for roiHandler in roisHandler:
- self.remove(roiHandler.roi)
-
- def remove(self, roi):
- if roi is None:
- return
- assert isinstance(roi, ROI)
- if roi.getID() in self._roiMarkerHandlers:
- self._roiMarkerHandlers[roi.getID()].clear()
- del self._roiMarkerHandlers[roi.getID()]
-
- def hasMarker(self, markerID):
- assert type(markerID) is str
- return self.getMarker(markerID) is not None
-
- def changePosition(self, markerID, x):
- markerHandler = self.getMarker(markerID)
- if markerHandler is None:
- raise ValueError('Marker %s not register' % markerID)
- markerHandler.changePosition(markerID=markerID, x=x)
-
- def updateMarker(self, markerID):
- markerHandler = self.getMarker(markerID)
- if markerHandler is None:
- raise ValueError('Marker %s not register' % markerID)
- roiID = self.getRoiID(markerID)
- visible = (self._activeRoi and self._activeRoi.getID() == roiID) or self._showAllMarkers is True
- markerHandler.setVisible(visible)
- markerHandler.updateAllMarkers()
-
- def updateRoiMarkers(self, roiID):
- if roiID in self._roiMarkerHandlers:
- visible = ((self._activeRoi and self._activeRoi.getID() == roiID)
- or self._showAllMarkers is True)
- _roi = self._roiMarkerHandlers[roiID]._roi()
- if _roi and not _roi.isICR():
- self._roiMarkerHandlers[roiID].showMiddleMarker(self._middleROIMarkerFlag)
- self._roiMarkerHandlers[roiID].setVisible(visible)
- self._roiMarkerHandlers[roiID].updateMarkers()
-
- def getMarker(self, markerID):
- assert type(markerID) is str
- for marker in list(self._roiMarkerHandlers.values()):
- if marker.hasMarker(markerID):
- return marker
-
- def updateMarkers(self):
- for markerHandler in list(self._roiMarkerHandlers.values()):
- markerHandler.updateMarkers()
-
- def getRoiID(self, markerID):
- for roiID, markerHandler in self._roiMarkerHandlers.items():
- if markerHandler.hasMarker(markerID):
- return roiID
- return None
-
- def setShowMiddleMarkers(self, show):
- self._middleROIMarkerFlag = show
- self._roiMarkerHandlers.updateAllMarkers()
-
- def updateAllMarkers(self):
- for roiID in self._roiMarkerHandlers:
- self.updateRoiMarkers(roiID)
-
- def getVisibleRois(self):
- res = {}
- for roiID, roiHandler in self._roiMarkerHandlers.items():
- markers = (roiHandler.getMarker('min'), roiHandler.getMarker('max'),
- roiHandler.getMarker('middle'))
- for marker in markers:
- if marker.isVisible():
- if roiID not in res:
- res[roiID] = []
- res[roiID].append(marker)
- return res
-
-
-class _RoiMarkerHandler(object):
- """Used to deal with ROI markers used in ROITable"""
- def __init__(self, roi, plot):
- assert roi and isinstance(roi, ROI)
- assert plot
-
- self._roi = weakref.ref(roi)
- self._plot = weakref.ref(plot)
- self._draggable = False if roi.isICR() else True
- self._color = 'black' if roi.isICR() else 'blue'
- self._displayMidMarker = False
- self._visible = True
-
- @property
- def draggable(self):
- return self._draggable
-
- @property
- def plot(self):
- return self._plot()
-
- def clear(self):
- if self.plot and self.roi:
- self.plot.removeMarker(self._markerID('min'))
- self.plot.removeMarker(self._markerID('max'))
- self.plot.removeMarker(self._markerID('middle'))
-
- @property
- def roi(self):
- return self._roi()
-
- def setVisible(self, visible):
- if visible != self._visible:
- self._visible = visible
- self.updateMarkers()
-
- def showMiddleMarker(self, visible):
- if self.draggable is False and visible is True:
- _logger.warning("ROI is not draggable. Won't display middle marker")
- return
- self._displayMidMarker = visible
- self.getMarker('middle').setVisible(self._displayMidMarker)
-
- def updateMarkers(self):
- if self.roi is None:
- return
- self._updateMinMarkerPos()
- self._updateMaxMarkerPos()
- self._updateMiddleMarkerPos()
-
- def _updateMinMarkerPos(self):
- self.getMarker('min').setPosition(x=self.roi.getFrom(), y=None)
- self.getMarker('min').setVisible(self._visible)
-
- def _updateMaxMarkerPos(self):
- self.getMarker('max').setPosition(x=self.roi.getTo(), y=None)
- self.getMarker('max').setVisible(self._visible)
-
- def _updateMiddleMarkerPos(self):
- self.getMarker('middle').setPosition(x=self.roi.getMiddle(), y=None)
- self.getMarker('middle').setVisible(self._displayMidMarker and self._visible)
-
- def getMarker(self, markerType):
- if self.plot is None:
- return None
- assert markerType in ('min', 'max', 'middle')
- if self.plot._getMarker(self._markerID(markerType)) is None:
- assert self.roi
- if markerType == 'min':
- val = self.roi.getFrom()
- elif markerType == 'max':
- val = self.roi.getTo()
- else:
- val = self.roi.getMiddle()
-
- _color = self._color
- if markerType == 'middle':
- _color = 'yellow'
- self.plot.addXMarker(val,
- legend=self._markerID(markerType),
- text=self.getMarkerName(markerType),
- color=_color,
- draggable=self.draggable)
- return self.plot._getMarker(self._markerID(markerType))
-
- def _markerID(self, markerType):
- assert markerType in ('min', 'max', 'middle')
- assert self.roi
- return '_'.join((str(self.roi.getID()), markerType))
-
- def getMarkerName(self, markerType):
- assert markerType in ('min', 'max', 'middle')
- assert self.roi
- return ' '.join((self.roi.getName(), markerType))
-
- def updateTexts(self):
- self.getMarker('min').setText(self.getMarkerName('min'))
- self.getMarker('max').setText(self.getMarkerName('max'))
- self.getMarker('middle').setText(self.getMarkerName('middle'))
-
- def changePosition(self, markerID, x):
- assert self.hasMarker(markerID)
- markerType = self._getMarkerType(markerID)
- assert markerType is not None
- if self.roi is None:
- return
- if markerType == 'min':
- self.roi.setFrom(x)
- self._updateMiddleMarkerPos()
- elif markerType == 'max':
- self.roi.setTo(x)
- self._updateMiddleMarkerPos()
- else:
- delta = x - 0.5 * (self.roi.getFrom() + self.roi.getTo())
- self.roi.setFrom(self.roi.getFrom() + delta)
- self.roi.setTo(self.roi.getTo() + delta)
- self._updateMinMarkerPos()
- self._updateMaxMarkerPos()
-
- def hasMarker(self, marker):
- return marker in (self._markerID('min'),
- self._markerID('max'),
- self._markerID('middle'))
-
- def _getMarkerType(self, markerID):
- if markerID.endswith('_min'):
- return 'min'
- elif markerID.endswith('_max'):
- return 'max'
- elif markerID.endswith('_middle'):
- return 'middle'
- else:
- return None
-
-
-class CurvesROIDockWidget(qt.QDockWidget):
- """QDockWidget with a :class:`CurvesROIWidget` connected to a PlotWindow.
-
- It makes the link between the :class:`CurvesROIWidget` and the PlotWindow.
-
- :param parent: See :class:`QDockWidget`
- :param plot: :class:`.PlotWindow` instance on which to operate
- :param name: See :class:`QDockWidget`
- """
- sigROISignal = qt.Signal(object)
- """Deprecated signal for backward compatibility with silx < 0.7.
- Prefer connecting directly to :attr:`CurvesRoiWidget.sigRoiSignal`
- """
-
- def __init__(self, parent=None, plot=None, name=None):
- super(CurvesROIDockWidget, self).__init__(name, parent)
-
- assert plot is not None
- self.plot = plot
- self.roiWidget = CurvesROIWidget(self, name, plot=plot)
- """Main widget of type :class:`CurvesROIWidget`"""
-
- # convenience methods to offer a simpler API allowing to ignore
- # the details of the underlying implementation
- # (ALL DEPRECATED)
- self.calculateROIs = self.calculateRois = self.roiWidget.calculateRois
- self.setRois = self.roiWidget.setRois
- self.getRois = self.roiWidget.getRois
-
- self.roiWidget.sigROISignal.connect(self._forwardSigROISignal)
-
- self.layout().setContentsMargins(0, 0, 0, 0)
- self.setWidget(self.roiWidget)
-
- self.setAreaVisible = self.roiWidget.roiTable.setAreaVisible
- self.setCountsVisible = self.roiWidget.roiTable.setCountsVisible
-
- def _forwardSigROISignal(self, ddict):
- # emit deprecated signal for backward compatibility (silx < 0.7)
- self.sigROISignal.emit(ddict)
-
- def toggleViewAction(self):
- """Returns a checkable action that shows or closes this widget.
-
- See :class:`QMainWindow`.
- """
- action = super(CurvesROIDockWidget, self).toggleViewAction()
- action.setIcon(icons.getQIcon('plot-roi'))
- return action
-
- def showEvent(self, event):
- """Make sure this widget is raised when it is shown
- (when it is first created as a tab in PlotWindow or when it is shown
- again after hiding).
- """
- self.raise_()
- qt.QDockWidget.showEvent(self, event)
-
- @property
- def currentROI(self):
- return self.roiWidget.currentRoi
diff --git a/silx/gui/plot/ImageStack.py b/silx/gui/plot/ImageStack.py
deleted file mode 100644
index fe4b451..0000000
--- a/silx/gui/plot/ImageStack.py
+++ /dev/null
@@ -1,636 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2020-2021 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.
-#
-# ###########################################################################*/
-"""Image stack view with data prefetch capabilty."""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "04/03/2019"
-
-
-from silx.gui import icons, qt
-from silx.gui.plot import Plot2D
-from silx.gui.utils import concurrent
-from silx.io.url import DataUrl
-from silx.io.utils import get_data
-from collections import OrderedDict
-from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser
-import time
-import threading
-import typing
-import logging
-
-_logger = logging.getLogger(__name__)
-
-
-class _PlotWithWaitingLabel(qt.QWidget):
- """Image plot widget with an overlay 'waiting' status.
- """
-
- class AnimationThread(threading.Thread):
- def __init__(self, label):
- self.running = True
- self._label = label
- self.animated_icon = icons.getWaitIcon()
- self.animated_icon.register(self._label)
- super(_PlotWithWaitingLabel.AnimationThread, self).__init__()
-
- def run(self):
- while self.running:
- time.sleep(0.05)
- icon = self.animated_icon.currentIcon()
- self.future_result = concurrent.submitToQtMainThread(
- self._label.setPixmap, icon.pixmap(30, state=qt.QIcon.On))
-
- def stop(self):
- """Stop the update thread"""
- self.animated_icon.unregister(self._label)
- self.running = False
- self.join(2)
-
- def __init__(self, parent):
- super(_PlotWithWaitingLabel, self).__init__(parent=parent)
- self._autoResetZoom = True
- layout = qt.QStackedLayout(self)
- layout.setStackingMode(qt.QStackedLayout.StackAll)
-
- self._waiting_label = qt.QLabel(parent=self)
- self._waiting_label.setAlignment(qt.Qt.AlignHCenter | qt.Qt.AlignVCenter)
- layout.addWidget(self._waiting_label)
-
- self._plot = Plot2D(parent=self)
- layout.addWidget(self._plot)
-
- self.updateThread = _PlotWithWaitingLabel.AnimationThread(self._waiting_label)
- self.updateThread.start()
-
- def close(self) -> bool:
- super(_PlotWithWaitingLabel, self).close()
- self.updateThread.stop()
-
- def setAutoResetZoom(self, reset):
- """
- Should we reset the zoom when adding an image (eq. when browsing)
-
- :param bool reset:
- """
- self._autoResetZoom = reset
- if self._autoResetZoom:
- self._plot.resetZoom()
-
- def isAutoResetZoom(self):
- """
-
- :return: True if a reset is done when the image change
- :rtype: bool
- """
- return self._autoResetZoom
-
- def setWaiting(self, activate=True):
- if activate is True:
- self._plot.clear()
- self._waiting_label.show()
- else:
- self._waiting_label.hide()
-
- def setData(self, data):
- self.setWaiting(activate=False)
- self._plot.addImage(data=data, resetzoom=self._autoResetZoom)
-
- def clear(self):
- self._plot.clear()
- self.setWaiting(False)
-
- def getPlotWidget(self):
- return self._plot
-
-
-class _HorizontalSlider(HorizontalSliderWithBrowser):
-
- sigCurrentUrlIndexChanged = qt.Signal(int)
-
- def __init__(self, parent):
- super(_HorizontalSlider, self).__init__(parent=parent)
- # connect signal / slot
- self.valueChanged.connect(self._urlChanged)
-
- def setUrlIndex(self, index):
- self.setValue(index)
- self.sigCurrentUrlIndexChanged.emit(index)
-
- def _urlChanged(self, value):
- self.sigCurrentUrlIndexChanged.emit(value)
-
-
-class UrlList(qt.QWidget):
- """List of URLs the user to select an URL"""
-
- sigCurrentUrlChanged = qt.Signal(str)
- """Signal emitted when the active/current url change"""
-
- def __init__(self, parent=None):
- super(UrlList, self).__init__(parent)
- self.setLayout(qt.QVBoxLayout())
- self.layout().setSpacing(0)
- self.layout().setContentsMargins(0, 0, 0, 0)
- self._listWidget = qt.QListWidget(parent=self)
- self.layout().addWidget(self._listWidget)
-
- # connect signal / Slot
- self._listWidget.currentItemChanged.connect(self._notifyCurrentUrlChanged)
-
- # expose API
- self.currentItem = self._listWidget.currentItem
-
- def setUrls(self, urls: list) -> None:
- url_names = []
- [url_names.append(url.path()) for url in urls]
- self._listWidget.addItems(url_names)
-
- def _notifyCurrentUrlChanged(self, current, previous):
- if current is None:
- pass
- else:
- self.sigCurrentUrlChanged.emit(current.text())
-
- def setUrl(self, url: DataUrl) -> None:
- assert isinstance(url, DataUrl)
- sel_items = self._listWidget.findItems(url.path(), qt.Qt.MatchExactly)
- if sel_items is None:
- _logger.warning(url.path(), ' is not registered in the list.')
- elif len(sel_items) > 0:
- item = sel_items[0]
- self._listWidget.setCurrentItem(item)
- self.sigCurrentUrlChanged.emit(item.text())
-
- def clear(self):
- self._listWidget.clear()
-
-
-class _ToggleableUrlSelectionTable(qt.QWidget):
-
- _BUTTON_ICON = qt.QStyle.SP_ToolBarHorizontalExtensionButton # noqa
-
- sigCurrentUrlChanged = qt.Signal(str)
- """Signal emitted when the active/current url change"""
-
- def __init__(self, parent=None) -> None:
- qt.QWidget.__init__(self, parent)
- self.setLayout(qt.QGridLayout())
- self._toggleButton = qt.QPushButton(parent=self)
- self.layout().addWidget(self._toggleButton, 0, 2, 1, 1)
- self._toggleButton.setSizePolicy(qt.QSizePolicy.Fixed,
- qt.QSizePolicy.Fixed)
-
- self._urlsTable = UrlList(parent=self)
- self.layout().addWidget(self._urlsTable, 1, 1, 1, 2)
-
- # set up
- self._setButtonIcon(show=True)
-
- # Signal / slot connection
- self._toggleButton.clicked.connect(self.toggleUrlSelectionTable)
- self._urlsTable.sigCurrentUrlChanged.connect(self._propagateSignal)
-
- # expose API
- self.setUrls = self._urlsTable.setUrls
- self.setUrl = self._urlsTable.setUrl
- self.currentItem = self._urlsTable.currentItem
-
- def toggleUrlSelectionTable(self):
- visible = not self.urlSelectionTableIsVisible()
- self._setButtonIcon(show=visible)
- self._urlsTable.setVisible(visible)
-
- def _setButtonIcon(self, show):
- style = qt.QApplication.instance().style()
- # return a QIcon
- icon = style.standardIcon(self._BUTTON_ICON)
- if show is False:
- pixmap = icon.pixmap(32, 32).transformed(qt.QTransform().scale(-1, 1))
- icon = qt.QIcon(pixmap)
- self._toggleButton.setIcon(icon)
-
- def urlSelectionTableIsVisible(self):
- return self._urlsTable.isVisible()
-
- def _propagateSignal(self, url):
- self.sigCurrentUrlChanged.emit(url)
-
- def clear(self):
- self._urlsTable.clear()
-
-
-class UrlLoader(qt.QThread):
- """
- Thread use to load DataUrl
- """
- def __init__(self, parent, url):
- super(UrlLoader, self).__init__(parent=parent)
- assert isinstance(url, DataUrl)
- self.url = url
- self.data = None
-
- def run(self):
- try:
- self.data = get_data(self.url)
- except IOError:
- self.data = None
-
-
-class ImageStack(qt.QMainWindow):
- """Widget loading on the fly images contained the given urls.
-
- It prefetches images close to the displayed one.
- """
-
- N_PRELOAD = 10
-
- sigLoaded = qt.Signal(str)
- """Signal emitted when new data is available"""
-
- sigCurrentUrlChanged = qt.Signal(str)
- """Signal emitted when the current url change"""
-
- def __init__(self, parent=None) -> None:
- super(ImageStack, self).__init__(parent)
- self.__n_prefetch = ImageStack.N_PRELOAD
- self._loadingThreads = []
- self.setWindowFlags(qt.Qt.Widget)
- self._current_url = None
- self._url_loader = UrlLoader
- "class to instantiate for loading urls"
-
- # main widget
- self._plot = _PlotWithWaitingLabel(parent=self)
- self._plot.setAttribute(qt.Qt.WA_DeleteOnClose, True)
- self.setWindowTitle("Image stack")
- self.setCentralWidget(self._plot)
-
- # dock widget: url table
- self._tableDockWidget = qt.QDockWidget(parent=self)
- self._urlsTable = _ToggleableUrlSelectionTable(parent=self)
- self._tableDockWidget.setWidget(self._urlsTable)
- self._tableDockWidget.setFeatures(qt.QDockWidget.DockWidgetMovable)
- self.addDockWidget(qt.Qt.RightDockWidgetArea, self._tableDockWidget)
- # dock widget: qslider
- self._sliderDockWidget = qt.QDockWidget(parent=self)
- self._slider = _HorizontalSlider(parent=self)
- self._sliderDockWidget.setWidget(self._slider)
- self.addDockWidget(qt.Qt.BottomDockWidgetArea, self._sliderDockWidget)
- self._sliderDockWidget.setFeatures(qt.QDockWidget.DockWidgetMovable)
-
- self.reset()
-
- # connect signal / slot
- self._urlsTable.sigCurrentUrlChanged.connect(self.setCurrentUrl)
- self._slider.sigCurrentUrlIndexChanged.connect(self.setCurrentUrlIndex)
-
- def close(self) -> bool:
- self._freeLoadingThreads()
- self._plot.close()
- super(ImageStack, self).close()
-
- def setUrlLoaderClass(self, urlLoader: typing.Type[UrlLoader]) -> None:
- """
-
- :param urlLoader: define the class to call for loading urls.
- warning: this should be a class object and not a
- class instance.
- """
- assert isinstance(urlLoader, type(UrlLoader))
- self._url_loader = urlLoader
-
- def getUrlLoaderClass(self):
- """
-
- :return: class to instantiate for loading urls
- :rtype: typing.Type[UrlLoader]
- """
- return self._url_loader
-
- def _freeLoadingThreads(self):
- for thread in self._loadingThreads:
- thread.blockSignals(True)
- thread.wait(5)
- self._loadingThreads.clear()
-
- def getPlotWidget(self) -> Plot2D:
- """
- Returns the PlotWidget contained in this window
-
- :return: PlotWidget contained in this window
- :rtype: Plot2D
- """
- return self._plot.getPlotWidget()
-
- def reset(self) -> None:
- """Clear the plot and remove any link to url"""
- self._freeLoadingThreads()
- self._urls = None
- self._urlIndexes = None
- self._urlData = OrderedDict({})
- self._current_url = None
- self._plot.clear()
- self._urlsTable.clear()
- self._slider.setMaximum(-1)
-
- def _preFetch(self, urls: list) -> None:
- """Pre-fetch the given urls if necessary
-
- :param urls: list of DataUrl to prefetch
- :type: list
- """
- for url in urls:
- if url.path() not in self._urlData:
- self._load(url)
-
- def _load(self, url):
- """
- Launch background load of a DataUrl
-
- :param url:
- :type: DataUrl
- """
- assert isinstance(url, DataUrl)
- url_path = url.path()
- assert url_path in self._urlIndexes
- loader = self._url_loader(parent=self, url=url)
- loader.finished.connect(self._urlLoaded, qt.Qt.QueuedConnection)
- self._loadingThreads.append(loader)
- loader.start()
-
- def _urlLoaded(self) -> None:
- """
-
- :param url: restul of DataUrl.path() function
- :return:
- """
- sender = self.sender()
- assert isinstance(sender, UrlLoader)
- url = sender.url.path()
- if url in self._urlIndexes:
- self._urlData[url] = sender.data
- if self.getCurrentUrl().path() == url:
- self._plot.setData(self._urlData[url])
- if sender in self._loadingThreads:
- self._loadingThreads.remove(sender)
- self.sigLoaded.emit(url)
-
- def setNPrefetch(self, n: int) -> None:
- """
- Define the number of url to prefetch around
-
- :param int n: number of url to prefetch on left and right sides.
- In total n*2 DataUrl will be prefetch
- """
- self.__n_prefetch = n
- current_url = self.getCurrentUrl()
- if current_url is not None:
- self.setCurrentUrl(current_url)
-
- def getNPrefetch(self) -> int:
- """
-
- :return: number of url to prefetch on left and right sides. In total
- will load 2* NPrefetch DataUrls
- """
- return self.__n_prefetch
-
- def setUrls(self, urls: list) -> None:
- """list of urls within an index. Warning: urls should contain an image
- compatible with the silx.gui.plot.Plot class
-
- :param urls: urls we want to set in the stack. Key is the index
- (position in the stack), value is the DataUrl
- :type: list
- """
- def createUrlIndexes():
- indexes = OrderedDict()
- for index, url in enumerate(urls):
- indexes[index] = url
- return indexes
-
- urls_with_indexes = createUrlIndexes()
- urlsToIndex = self._urlsToIndex(urls_with_indexes)
- self.reset()
- self._urls = urls_with_indexes
- self._urlIndexes = urlsToIndex
-
- old_url_table = self._urlsTable.blockSignals(True)
- self._urlsTable.setUrls(urls=list(self._urls.values()))
- self._urlsTable.blockSignals(old_url_table)
-
- old_slider = self._slider.blockSignals(True)
- self._slider.setMinimum(0)
- self._slider.setMaximum(len(self._urls) - 1)
- self._slider.blockSignals(old_slider)
-
- if self.getCurrentUrl() in self._urls:
- self.setCurrentUrl(self.getCurrentUrl())
- else:
- if len(self._urls.keys()) > 0:
- first_url = self._urls[list(self._urls.keys())[0]]
- self.setCurrentUrl(first_url)
-
- def getUrls(self) -> tuple:
- """
-
- :return: tuple of urls
- :rtype: tuple
- """
- return tuple(self._urlIndexes.keys())
-
- def _getNextUrl(self, url: DataUrl) -> typing.Union[None, DataUrl]:
- """
- return the next url in the stack
-
- :param url: url for which we want the next url
- :type: DataUrl
- :return: next url in the stack or None if `url` is the last one
- :rtype: Union[None, DataUrl]
- """
- assert isinstance(url, DataUrl)
- if self._urls is None:
- return None
- else:
- index = self._urlIndexes[url.path()]
- indexes = list(self._urls.keys())
- res = list(filter(lambda x: x > index, indexes))
- if len(res) == 0:
- return None
- else:
- return self._urls[res[0]]
-
- def _getPreviousUrl(self, url: DataUrl) -> typing.Union[None, DataUrl]:
- """
- return the previous url in the stack
-
- :param url: url for which we want the previous url
- :type: DataUrl
- :return: next url in the stack or None if `url` is the last one
- :rtype: Union[None, DataUrl]
- """
- if self._urls is None:
- return None
- else:
- index = self._urlIndexes[url.path()]
- indexes = list(self._urls.keys())
- res = list(filter(lambda x: x < index, indexes))
- if len(res) == 0:
- return None
- else:
- return self._urls[res[-1]]
-
- def _getNNextUrls(self, n: int, url: DataUrl) -> list:
- """
- Deduce the next urls in the stack after `url`
-
- :param n: the number of url store after `url`
- :type: int
- :param url: url for which we want n next url
- :type: DataUrl
- :return: list of next urls.
- :rtype: list
- """
- res = []
- next_free = self._getNextUrl(url=url)
- while len(res) < n and next_free is not None:
- assert isinstance(next_free, DataUrl)
- res.append(next_free)
- next_free = self._getNextUrl(res[-1])
- return res
-
- def _getNPreviousUrls(self, n: int, url: DataUrl):
- """
- Deduce the previous urls in the stack after `url`
-
- :param n: the number of url store after `url`
- :type: int
- :param url: url for which we want n previous url
- :type: DataUrl
- :return: list of previous urls.
- :rtype: list
- """
- res = []
- next_free = self._getPreviousUrl(url=url)
- while len(res) < n and next_free is not None:
- res.insert(0, next_free)
- next_free = self._getPreviousUrl(res[0])
- return res
-
- def setCurrentUrlIndex(self, index: int):
- """
- Define the url to be displayed
-
- :param index: url to be displayed
- :type: int
- """
- if index < 0:
- return
- if self._urls is None:
- return
- elif index >= len(self._urls):
- raise ValueError('requested index out of bounds')
- else:
- return self.setCurrentUrl(self._urls[index])
-
- def setCurrentUrl(self, url: typing.Union[DataUrl, str]) -> None:
- """
- Define the url to be displayed
-
- :param url: url to be displayed
- :type: DataUrl
- """
- assert isinstance(url, (DataUrl, str))
- if isinstance(url, str):
- url = DataUrl(path=url)
- if url != self._current_url:
- self._current_url = url
- self.sigCurrentUrlChanged.emit(url.path())
-
- old_url_table = self._urlsTable.blockSignals(True)
- old_slider = self._slider.blockSignals(True)
-
- self._urlsTable.setUrl(url)
- self._slider.setUrlIndex(self._urlIndexes[url.path()])
- if self._current_url is None:
- self._plot.clear()
- else:
- if self._current_url.path() in self._urlData:
- self._plot.setData(self._urlData[url.path()])
- else:
- self._load(url)
- self._notifyLoading()
- self._preFetch(self._getNNextUrls(self.__n_prefetch, url))
- self._preFetch(self._getNPreviousUrls(self.__n_prefetch, url))
- self._urlsTable.blockSignals(old_url_table)
- self._slider.blockSignals(old_slider)
-
- def getCurrentUrl(self) -> typing.Union[None, DataUrl]:
- """
-
- :return: url currently displayed
- :rtype: Union[None, DataUrl]
- """
- return self._current_url
-
- def getCurrentUrlIndex(self) -> typing.Union[None, int]:
- """
-
- :return: index of the url currently displayed
- :rtype: Union[None, int]
- """
- if self._current_url is None:
- return None
- else:
- return self._urlIndexes[self._current_url.path()]
-
- @staticmethod
- def _urlsToIndex(urls):
- """util, return a dictionary with url as key and index as value"""
- res = {}
- for index, url in urls.items():
- res[url.path()] = index
- return res
-
- def _notifyLoading(self):
- """display a simple image of loading..."""
- self._plot.setWaiting(activate=True)
-
- def setAutoResetZoom(self, reset):
- """
- Should we reset the zoom when adding an image (eq. when browsing)
-
- :param bool reset:
- """
- self._plot.setAutoResetZoom(reset)
-
- def isAutoResetZoom(self) -> bool:
- """
-
- :return: True if a reset is done when the image change
- :rtype: bool
- """
- return self._plot.isAutoResetZoom()
diff --git a/silx/gui/plot/ImageView.py b/silx/gui/plot/ImageView.py
deleted file mode 100644
index 1befe58..0000000
--- a/silx/gui/plot/ImageView.py
+++ /dev/null
@@ -1,854 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2021 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.
-#
-# ###########################################################################*/
-"""QWidget displaying a 2D image with histograms on its sides.
-
-The :class:`ImageView` implements this widget, and
-:class:`ImageViewMainWindow` provides a main window with additional toolbar
-and status bar.
-
-Basic usage of :class:`ImageView` is through the following methods:
-
-- :meth:`ImageView.getColormap`, :meth:`ImageView.setColormap` to update the
- default colormap to use and update the currently displayed image.
-- :meth:`ImageView.setImage` to update the displayed image.
-
-For an example of use, see `imageview.py` in :ref:`sample-code`.
-"""
-
-from __future__ import division
-
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "26/04/2018"
-
-
-import logging
-import numpy
-import collections
-from typing import Union
-import weakref
-
-import silx
-from .. import qt
-from .. import colors
-
-from . import items, PlotWindow, PlotWidget, actions
-from ..colors import Colormap
-from ..colors import cursorColorForColormap
-from .tools import LimitsToolBar
-from .Profile import ProfileToolBar
-from ...utils.proxy import docstring
-from ...utils.enum import Enum
-from .tools.RadarView import RadarView
-from .utils.axis import SyncAxes
-from ..utils import blockSignals
-from . import _utils
-from .tools.profile import manager
-from .tools.profile import rois
-
-_logger = logging.getLogger(__name__)
-
-
-ProfileSumResult = collections.namedtuple("ProfileResult",
- ["dataXRange", "dataYRange",
- 'histoH', 'histoHRange',
- 'histoV', 'histoVRange',
- "xCoords", "xData",
- "yCoords", "yData"])
-
-
-def computeProfileSumOnRange(imageItem, xRange, yRange, cache=None):
- """
- Compute a full vertical and horizontal profile on an image item using a
- a range in the plot referential.
-
- Optionally takes a previous computed result to be able to skip the
- computation.
-
- :rtype: ProfileSumResult
- """
- data = imageItem.getValueData(copy=False)
- origin = imageItem.getOrigin()
- scale = imageItem.getScale()
- height, width = data.shape
-
- xMin, xMax = xRange
- yMin, yMax = yRange
-
- # Convert plot area limits to image coordinates
- # and work in image coordinates (i.e., in pixels)
- xMin = int((xMin - origin[0]) / scale[0])
- xMax = int((xMax - origin[0]) / scale[0])
- yMin = int((yMin - origin[1]) / scale[1])
- yMax = int((yMax - origin[1]) / scale[1])
-
- if (xMin >= width or xMax < 0 or
- yMin >= height or yMax < 0):
- return None
-
- # The image is at least partly in the plot area
- # Get the visible bounds in image coords (i.e., in pixels)
- subsetXMin = 0 if xMin < 0 else xMin
- subsetXMax = (width if xMax >= width else xMax) + 1
- subsetYMin = 0 if yMin < 0 else yMin
- subsetYMax = (height if yMax >= height else yMax) + 1
-
- if cache is not None:
- if ((subsetXMin, subsetXMax) == cache.dataXRange and
- (subsetYMin, subsetYMax) == cache.dataYRange):
- # The visible area of data is the same
- return cache
-
- # Rebuild histograms for visible area
- visibleData = data[subsetYMin:subsetYMax,
- subsetXMin:subsetXMax]
- histoHVisibleData = numpy.nansum(visibleData, axis=0)
- histoVVisibleData = numpy.nansum(visibleData, axis=1)
- histoHMin = numpy.nanmin(histoHVisibleData)
- histoHMax = numpy.nanmax(histoHVisibleData)
- histoVMin = numpy.nanmin(histoVVisibleData)
- histoVMax = numpy.nanmax(histoVVisibleData)
-
- # Convert to histogram curve and update plots
- # Taking into account origin and scale
- coords = numpy.arange(2 * histoHVisibleData.size)
- xCoords = (coords + 1) // 2 + subsetXMin
- xCoords = origin[0] + scale[0] * xCoords
- xData = numpy.take(histoHVisibleData, coords // 2)
- coords = numpy.arange(2 * histoVVisibleData.size)
- yCoords = (coords + 1) // 2 + subsetYMin
- yCoords = origin[1] + scale[1] * yCoords
- yData = numpy.take(histoVVisibleData, coords // 2)
-
- result = ProfileSumResult(
- dataXRange=(subsetXMin, subsetXMax),
- dataYRange=(subsetYMin, subsetYMax),
- histoH=histoHVisibleData,
- histoHRange=(histoHMin, histoHMax),
- histoV=histoVVisibleData,
- histoVRange=(histoVMin, histoVMax),
- xCoords=xCoords,
- xData=xData,
- yCoords=yCoords,
- yData=yData)
-
- return result
-
-
-class _SideHistogram(PlotWidget):
- """
- Widget displaying one of the side profile of the ImageView.
-
- Implement ProfileWindow
- """
-
- sigClose = qt.Signal()
-
- sigMouseMoved = qt.Signal(float, float)
-
- def __init__(self, parent=None, backend=None, direction=qt.Qt.Horizontal):
- super(_SideHistogram, self).__init__(parent=parent, backend=backend)
- self._direction = direction
- self.sigPlotSignal.connect(self._plotEvents)
- self._color = "blue"
- self.__profile = None
- self.__profileSum = None
-
- def _plotEvents(self, eventDict):
- """Callback for horizontal histogram plot events."""
- if eventDict['event'] == 'mouseMoved':
- self.sigMouseMoved.emit(eventDict['x'], eventDict['y'])
-
- def setProfileColor(self, color):
- self._color = color
-
- def setProfileSum(self, result):
- self.__profileSum = result
- if self.__profile is None:
- self.__drawProfileSum()
-
- def prepareWidget(self, roi):
- """Implements `ProfileWindow`"""
- pass
-
- def setRoiProfile(self, roi):
- """Implements `ProfileWindow`"""
- if roi is None:
- return
- self._roiColor = colors.rgba(roi.getColor())
-
- def getProfile(self):
- """Implements `ProfileWindow`"""
- return self.__profile
-
- def setProfile(self, data):
- """Implements `ProfileWindow`"""
- self.__profile = data
- if data is None:
- self.__drawProfileSum()
- else:
- self.__drawProfile()
-
- def __drawProfileSum(self):
- """Only draw the profile sum on the plot.
-
- Other elements are removed
- """
- profileSum = self.__profileSum
-
- try:
- self.removeCurve('profile')
- except Exception:
- pass
-
- if profileSum is None:
- try:
- self.removeCurve('profilesum')
- except Exception:
- pass
- return
-
- if self._direction == qt.Qt.Horizontal:
- xx, yy = profileSum.xCoords, profileSum.xData
- elif self._direction == qt.Qt.Vertical:
- xx, yy = profileSum.yData, profileSum.yCoords
- else:
- assert False
-
- self.addCurve(xx, yy,
- xlabel='', ylabel='',
- legend="profilesum",
- color=self._color,
- linestyle='-',
- selectable=False,
- resetzoom=False)
-
- self.__updateLimits()
-
- def __drawProfile(self):
- """Only draw the profile on the plot.
-
- Other elements are removed
- """
- profile = self.__profile
-
- try:
- self.removeCurve('profilesum')
- except Exception:
- pass
-
- if profile is None:
- try:
- self.removeCurve('profile')
- except Exception:
- pass
- self.setProfileSum(self.__profileSum)
- return
-
- if self._direction == qt.Qt.Horizontal:
- xx, yy = profile.coords, profile.profile
- elif self._direction == qt.Qt.Vertical:
- xx, yy = profile.profile, profile.coords
- else:
- assert False
-
- self.addCurve(xx,
- yy,
- legend="profile",
- color=self._roiColor,
- resetzoom=False)
-
- self.__updateLimits()
-
- def __updateLimits(self):
- if self.__profile:
- data = self.__profile.profile
- vMin = numpy.nanmin(data)
- vMax = numpy.nanmax(data)
- elif self.__profileSum is not None:
- if self._direction == qt.Qt.Horizontal:
- vMin, vMax = self.__profileSum.histoHRange
- elif self._direction == qt.Qt.Vertical:
- vMin, vMax = self.__profileSum.histoVRange
- else:
- assert False
- else:
- vMin, vMax = 0, 0
-
- # Tune the result using the data margins
- margins = self.getDataMargins()
- if self._direction == qt.Qt.Horizontal:
- _, _, vMin, vMax = _utils.addMarginsToLimits(margins, False, False, 0, 0, vMin, vMax)
- elif self._direction == qt.Qt.Vertical:
- vMin, vMax, _, _ = _utils.addMarginsToLimits(margins, False, False, vMin, vMax, 0, 0)
- else:
- assert False
-
- if self._direction == qt.Qt.Horizontal:
- dataAxis = self.getYAxis()
- elif self._direction == qt.Qt.Vertical:
- dataAxis = self.getXAxis()
- else:
- assert False
-
- with blockSignals(dataAxis):
- dataAxis.setLimits(vMin, vMax)
-
-
-class ImageView(PlotWindow):
- """Display a single image with horizontal and vertical histograms.
-
- 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:`.PlotWidget` for the list of supported backend.
- :type backend: str or :class:`BackendBase.BackendBase`
- """
-
- HISTOGRAMS_COLOR = 'blue'
- """Color to use for the side histograms."""
-
- HISTOGRAMS_HEIGHT = 200
- """Height in pixels of the side histograms."""
-
- IMAGE_MIN_SIZE = 200
- """Minimum size in pixels of the image area."""
-
- # Qt signals
- valueChanged = qt.Signal(float, float, float)
- """Signals that the data value under the cursor has changed.
-
- It provides: row, column, data value.
-
- When the cursor is over an histogram, either row or column is Nan
- and the provided data value is the histogram value
- (i.e., the sum along the corresponding row/column).
- Row and columns are either Nan or integer values.
- """
-
- class ProfileWindowBehavior(Enum):
- """ImageView's profile window behavior options"""
-
- POPUP = 'popup'
- """All profiles are displayed in pop-up windows"""
-
- EMBEDDED = 'embedded'
- """Horizontal, vertical and cross profiles are displayed in
- sides widgets, others are displayed in pop-up windows.
- """
-
- def __init__(self, parent=None, backend=None):
- self._imageLegend = '__ImageView__image' + str(id(self))
- self._cache = None # Store currently visible data information
-
- super(ImageView, self).__init__(parent=parent, backend=backend,
- resetzoom=True, autoScale=False,
- logScale=False, grid=False,
- curveStyle=False, colormap=True,
- aspectRatio=True, yInverted=True,
- copy=True, save=True, print_=True,
- control=False, position=False,
- roi=False, mask=True)
-
- # Enable mask synchronisation to use it in profiles
- maskToolsWidget = self.getMaskToolsDockWidget().widget()
- maskToolsWidget.setItemMaskUpdated(True)
-
- if parent is None:
- self.setWindowTitle('ImageView')
-
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
- self.getYAxis().setInverted(True)
-
- self._initWidgets(backend)
-
- self.__profileWindowBehavior = self.ProfileWindowBehavior.POPUP
- self.__profile = ProfileToolBar(plot=self)
- self.addToolBar(self.__profile)
-
- def _initWidgets(self, backend):
- """Set-up layout and plots."""
- self._histoHPlot = _SideHistogram(backend=backend, parent=self, direction=qt.Qt.Horizontal)
- widgetHandle = self._histoHPlot.getWidgetHandle()
- widgetHandle.setMinimumHeight(self.HISTOGRAMS_HEIGHT)
- widgetHandle.setMaximumHeight(self.HISTOGRAMS_HEIGHT)
- self._histoHPlot.setInteractiveMode('zoom')
- self._histoHPlot.setDataMargins(0., 0., 0.1, 0.1)
- self._histoHPlot.sigMouseMoved.connect(self._mouseMovedOnHistoH)
- self._histoHPlot.setProfileColor(self.HISTOGRAMS_COLOR)
-
- self._histoVPlot = _SideHistogram(backend=backend, parent=self, direction=qt.Qt.Vertical)
- widgetHandle = self._histoVPlot.getWidgetHandle()
- widgetHandle.setMinimumWidth(self.HISTOGRAMS_HEIGHT)
- widgetHandle.setMaximumWidth(self.HISTOGRAMS_HEIGHT)
- self._histoVPlot.setInteractiveMode('zoom')
- self._histoVPlot.setDataMargins(0.1, 0.1, 0., 0.)
- self._histoVPlot.sigMouseMoved.connect(self._mouseMovedOnHistoV)
- self._histoVPlot.setProfileColor(self.HISTOGRAMS_COLOR)
-
- self.setPanWithArrowKeys(True)
- self.setInteractiveMode('zoom') # Color set in setColormap
- self.sigPlotSignal.connect(self._imagePlotCB)
- self.sigActiveImageChanged.connect(self._activeImageChangedSlot)
-
- self._radarView = RadarView(parent=self)
- self._radarView.setPlotWidget(self)
-
- self.__syncXAxis = SyncAxes([self.getXAxis(), self._histoHPlot.getXAxis()])
- self.__syncYAxis = SyncAxes([self.getYAxis(), self._histoVPlot.getYAxis()])
-
- self.__setCentralWidget()
-
- def __setCentralWidget(self):
- """Set central widget with all its content"""
- 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, 1, 2)
- layout.addWidget(self.getColorBarWidget(), 0, 2)
-
- layout.setColumnMinimumWidth(0, self.IMAGE_MIN_SIZE)
- layout.setColumnStretch(0, 1)
- layout.setColumnMinimumWidth(1, self.HISTOGRAMS_HEIGHT)
- layout.setColumnStretch(1, 0)
-
- layout.setRowMinimumHeight(0, self.IMAGE_MIN_SIZE)
- layout.setRowStretch(0, 1)
- layout.setRowMinimumHeight(1, self.HISTOGRAMS_HEIGHT)
- layout.setRowStretch(1, 0)
-
- layout.setSpacing(0)
- layout.setContentsMargins(0, 0, 0, 0)
-
- centralWidget = qt.QWidget(self)
- centralWidget.setLayout(layout)
- self.setCentralWidget(centralWidget)
-
- @docstring(PlotWidget)
- def setBackend(self, backend):
- # Use PlotWidget here since we override PlotWindow behavior
- PlotWidget.setBackend(self, backend)
- self.__setCentralWidget()
-
- def _dirtyCache(self):
- self._cache = None
-
- def _updateHistograms(self):
- """Update histograms content using current active image."""
- activeImage = self.getActiveImage()
- if activeImage is not None:
- xRange = self.getXAxis().getLimits()
- yRange = self.getYAxis().getLimits()
- result = computeProfileSumOnRange(activeImage, xRange, yRange, self._cache)
- self._cache = result
- self._histoHPlot.setProfileSum(result)
- self._histoVPlot.setProfileSum(result)
-
- # Plots event listeners
-
- def _imagePlotCB(self, eventDict):
- """Callback for imageView plot events."""
- if eventDict['event'] == 'mouseMoved':
- activeImage = self.getActiveImage()
- if activeImage is not None:
- data = activeImage.getData(copy=False)
- height, width = data.shape
-
- # Get corresponding coordinate in image
- origin = activeImage.getOrigin()
- scale = activeImage.getScale()
- if (eventDict['x'] >= origin[0] and
- eventDict['y'] >= origin[1]):
- x = int((eventDict['x'] - origin[0]) / scale[0])
- y = int((eventDict['y'] - origin[1]) / scale[1])
-
- if x >= 0 and x < width and y >= 0 and y < height:
- self.valueChanged.emit(float(x), float(y),
- data[y][x])
-
- elif eventDict['event'] == 'limitsChanged':
- self._updateHistograms()
-
- def _mouseMovedOnHistoH(self, x, y):
- if self._cache is None:
- return
- activeImage = self.getActiveImage()
- if activeImage is None:
- return
-
- xOrigin = activeImage.getOrigin()[0]
- xScale = activeImage.getScale()[0]
-
- minValue = xOrigin + xScale * self._cache.dataXRange[0]
-
- if x >= minValue:
- data = self._cache.histoH
- column = int((x - minValue) / xScale)
- if column >= 0 and column < data.shape[0]:
- self.valueChanged.emit(
- float('nan'),
- float(column + self._cache.dataXRange[0]),
- data[column])
-
- def _mouseMovedOnHistoV(self, x, y):
- if self._cache is None:
- return
- activeImage = self.getActiveImage()
- if activeImage is None:
- return
-
- yOrigin = activeImage.getOrigin()[1]
- yScale = activeImage.getScale()[1]
-
- minValue = yOrigin + yScale * self._cache.dataYRange[0]
-
- if y >= minValue:
- data = self._cache.histoV
- row = int((y - minValue) / yScale)
- if row >= 0 and row < data.shape[0]:
- self.valueChanged.emit(
- float(row + self._cache.dataYRange[0]),
- float('nan'),
- data[row])
-
- def _activeImageChangedSlot(self, previous, legend):
- """Handle Plot active image change.
-
- Resets side histograms cache
- """
- self._dirtyCache()
- self._updateHistograms()
-
- def setProfileWindowBehavior(self, behavior: Union[str, ProfileWindowBehavior]):
- """Set where profile widgets are displayed.
-
- :param ProfileWindowBehavior behavior:
- - 'popup': All profiles are displayed in pop-up windows
- - 'embedded': Horizontal, vertical and cross profiles are displayed in
- sides widgets, others are displayed in pop-up windows.
- """
- behavior = self.ProfileWindowBehavior.from_value(behavior)
- if behavior is not self.getProfileWindowBehavior():
- manager = self.__profile.getProfileManager()
- manager.clearProfile()
- manager.requestUpdateAllProfile()
-
- if behavior is self.ProfileWindowBehavior.EMBEDDED:
- horizontalProfileWindow = self._histoHPlot
- verticalProfileWindow = self._histoVPlot
- else:
- horizontalProfileWindow = None
- verticalProfileWindow = None
-
- manager.setSpecializedProfileWindow(
- rois.ProfileImageHorizontalLineROI, horizontalProfileWindow
- )
- manager.setSpecializedProfileWindow(
- rois.ProfileImageVerticalLineROI, verticalProfileWindow
- )
- self.__profileWindowBehavior = behavior
-
- def getProfileWindowBehavior(self) -> ProfileWindowBehavior:
- """Returns current profile display behavior.
-
- See :meth:`setProfileWindowBehavior` and :class:`ProfileWindowBehavior`
- """
- return self.__profileWindowBehavior
-
- def getProfileToolBar(self):
- """"Returns profile tools attached to this plot.
-
- :rtype: silx.gui.plot.PlotTools.ProfileToolBar
- """
- return self.__profile
-
- @property
- def profile(self):
- return self.getProfileToolBar()
-
- def getHistogram(self, axis):
- """Return the histogram and corresponding row or column extent.
-
- The returned value when an histogram is available is a dict with keys:
-
- - 'data': numpy array of the histogram values.
- - 'extent': (start, end) row or column index.
- end index is not included in the histogram.
-
- :param str axis: 'x' for horizontal, 'y' for vertical
- :return: The histogram and its extent as a dict or None.
- :rtype: dict
- """
- assert axis in ('x', 'y')
- if self._cache is None:
- return None
- else:
- if axis == 'x':
- return dict(
- data=numpy.array(self._cache.histoH, copy=True),
- extent=self._cache.dataXRange)
- else:
- return dict(
- data=numpy.array(self._cache.histoV, copy=True),
- extent=(self._cache.dataYRange))
-
- def radarView(self):
- """Get the lower right radarView widget."""
- return self._radarView
-
- def setRadarView(self, radarView):
- """Change the lower right radarView widget.
-
- :param RadarView radarView: Widget subclassing RadarView to replace
- the lower right corner widget.
- """
- self._radarView = radarView
- self._radarView.setPlotWidget(self)
- self.centralWidget().layout().addWidget(self._radarView, 1, 1)
-
- # High-level API
-
- def getColormap(self):
- """Get the default colormap description.
-
- :return: A description of the current colormap.
- See :meth:`setColormap` for details.
- :rtype: dict
- """
- return self.getDefaultColormap()
-
- def setColormap(self, colormap=None, normalization=None,
- autoscale=None, vmin=None, vmax=None, colors=None):
- """Set the default colormap and update active image.
-
- Parameters that are not provided are taken from the current colormap.
-
- The colormap parameter can also be a dict with the following keys:
-
- - *name*: string. The colormap to use:
- 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'.
- - *normalization*: string. The mapping to use for the colormap:
- either 'linear' or 'log'.
- - *autoscale*: bool. Whether to use autoscale (True)
- or range provided by keys 'vmin' and 'vmax' (False).
- - *vmin*: float. The minimum value of the range to use if 'autoscale'
- is False.
- - *vmax*: float. The maximum value of the range to use if 'autoscale'
- is False.
- - *colors*: optional. Nx3 or Nx4 array of float in [0, 1] or uint8.
- List of RGB or RGBA colors to use (only if name is None)
-
- :param colormap: Name of the colormap in
- 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'.
- Or the description of the colormap as a dict.
- :type colormap: dict or str.
- :param str normalization: Colormap mapping: 'linear' or 'log'.
- :param bool autoscale: Whether to use autoscale (True)
- or [vmin, vmax] range (False).
- :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
- 'autoscale' is False.
- :param numpy.ndarray colors: Only used if name is None.
- Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
- """
- 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)
-
- 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
- cmap._setFromDict(colormap)
-
- else:
- if colormap is not None:
- cmap.setName(colormap)
- if normalization is not None:
- 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:
- cmap.setColormapLUT(colors)
-
- cursorColor = cursorColorForColormap(cmap.getName())
- self.setInteractiveMode('zoom', color=cursorColor)
-
- def setImage(self, image, origin=(0, 0), scale=(1., 1.),
- copy=True, reset=True):
- """Set the image to display.
-
- :param image: A 2D array representing the image or None to empty plot.
- :type image: numpy.ndarray-like with 2 dimensions or None.
- :param origin: The (x, y) position of the origin of the image.
- Default: (0, 0).
- The origin is the lower left corner of the image when
- the Y axis is not inverted.
- :type origin: Tuple of 2 floats: (origin x, origin y).
- :param scale: The scale factor to apply to the image on X and Y axes.
- Default: (1, 1).
- It is the size of a pixel in the coordinates of the axes.
- Scales must be positive numbers.
- :type scale: Tuple of 2 floats: (scale x, scale y).
- :param bool copy: Whether to copy image data (default) or not.
- :param bool reset: Whether to reset zoom and ROI (default) or not.
- """
- self._dirtyCache()
-
- assert len(origin) == 2
- assert len(scale) == 2
- assert scale[0] > 0
- assert scale[1] > 0
-
- if image is None:
- self.remove(self._imageLegend, kind='image')
- return
-
- data = numpy.array(image, order='C', copy=copy)
- assert data.size != 0
- assert len(data.shape) == 2
-
- self.addImage(data,
- legend=self._imageLegend,
- origin=origin, scale=scale,
- colormap=self.getColormap(),
- resetzoom=False)
- self.setActiveImage(self._imageLegend)
- self._updateHistograms()
- if reset:
- self.resetZoom()
-
-
-# ImageViewMainWindow #########################################################
-
-class ImageViewMainWindow(ImageView):
- """:class:`ImageView` with additional toolbars
-
- Adds extra toolbar and a status bar to :class:`ImageView`.
- """
- def __init__(self, parent=None, backend=None):
- self._dataInfo = None
- super(ImageViewMainWindow, self).__init__(parent, backend)
- self.setWindowFlags(qt.Qt.Window)
-
- self.getXAxis().setLabel('X')
- self.getYAxis().setLabel('Y')
- self.setGraphTitle('Image')
-
- # Add toolbars and status bar
- self.addToolBar(qt.Qt.BottomToolBarArea, LimitsToolBar(plot=self))
-
- self.statusBar()
-
- menu = self.menuBar().addMenu('File')
- menu.addAction(self.getOutputToolBar().getSaveAction())
- menu.addAction(self.getOutputToolBar().getPrintAction())
- menu.addSeparator()
- action = menu.addAction('Quit')
- action.triggered[bool].connect(qt.QApplication.instance().quit)
-
- menu = self.menuBar().addMenu('Edit')
- menu.addAction(self.getOutputToolBar().getCopyAction())
- menu.addSeparator()
- menu.addAction(self.getResetZoomAction())
- menu.addAction(self.getColormapAction())
- menu.addAction(actions.control.KeepAspectRatioAction(self, self))
- menu.addAction(actions.control.YAxisInvertedAction(self, self))
-
- self.__profileMenu = self.menuBar().addMenu('Profile')
- self.__updateProfileMenu()
-
- # Connect to ImageView's signal
- self.valueChanged.connect(self._statusBarSlot)
-
- def __updateProfileMenu(self):
- """Update actions available in 'Profile' menu"""
- profile = self.getProfileToolBar()
- self.__profileMenu.clear()
- self.__profileMenu.addAction(profile.hLineAction)
- self.__profileMenu.addAction(profile.vLineAction)
- self.__profileMenu.addAction(profile.crossAction)
- self.__profileMenu.addAction(profile.lineAction)
- self.__profileMenu.addAction(profile.clearAction)
-
- def _statusBarSlot(self, row, column, value):
- """Update status bar with coordinates/value from plots."""
- if numpy.isnan(row):
- msg = 'Column: %d, Sum: %g' % (int(column), value)
- elif numpy.isnan(column):
- msg = 'Row: %d, Sum: %g' % (int(row), value)
- else:
- msg = 'Position: (%d, %d), Value: %g' % (int(row), int(column),
- value)
- if self._dataInfo is not None:
- msg = self._dataInfo + ', ' + msg
-
- self.statusBar().showMessage(msg)
-
- @docstring(ImageView)
- def setProfileWindowBehavior(self, behavior: str):
- super().setProfileWindowBehavior(behavior)
- self.__updateProfileMenu()
-
- @docstring(ImageView)
- def setImage(self, image, *args, **kwargs):
- if hasattr(image, 'dtype') and hasattr(image, 'shape'):
- assert len(image.shape) == 2
- height, width = image.shape
- self._dataInfo = 'Data: %dx%d (%s)' % (width, height,
- str(image.dtype))
- self.statusBar().showMessage(self._dataInfo)
- else:
- self._dataInfo = None
-
- # Set the new image in ImageView widget
- super(ImageViewMainWindow, self).setImage(image, *args, **kwargs)
- self.setStatusBar(None)
diff --git a/silx/gui/plot/Interaction.py b/silx/gui/plot/Interaction.py
deleted file mode 100644
index 6213889..0000000
--- a/silx/gui/plot/Interaction.py
+++ /dev/null
@@ -1,350 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 an implementation of state machines for interaction.
-
-Sample code of a state machine with two states ('idle' and 'active')
-with transitions on left button press/release:
-
-.. code-block:: python
-
- from silx.gui.plot.Interaction import *
-
- class SampleStateMachine(StateMachine):
-
- class Idle(State):
- def onPress(self, x, y, btn):
- if btn == LEFT_BTN:
- self.goto('active')
-
- class Active(State):
- def enterState(self):
- print('Enabled') # Handle enter active state here
-
- def leaveState(self):
- print('Disabled') # Handle leave active state here
-
- def onRelease(self, x, y, btn):
- if btn == LEFT_BTN:
- self.goto('idle')
-
- def __init__(self):
- # State machine has 2 states
- states = {
- 'idle': SampleStateMachine.Idle,
- 'active': SampleStateMachine.Active
- }
- super(TwoStates, self).__init__(states, 'idle')
- # idle is the initial state
-
- stateMachine = SampleStateMachine()
-
- # Triggers a transition to the Active state:
- stateMachine.handleEvent('press', 0, 0, LEFT_BTN)
-
- # Triggers a transition to the Idle state:
- stateMachine.handleEvent('release', 0, 0, LEFT_BTN)
-
-See :class:`ClickOrDrag` for another example of a state machine.
-
-See `Renaud Blanch, Michel Beaudouin-Lafon.
-Programming Rich Interactions using the Hierarchical State Machine Toolkit.
-In Proceedings of AVI 2006. p 51-58.
-<http://iihm.imag.fr/en/publication/BB06a/>`_
-for a discussion of using (hierarchical) state machines for interaction.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "18/02/2016"
-
-
-import weakref
-
-
-# state machine ###############################################################
-
-class State(object):
- """Base class for the states of a state machine.
-
- This class is meant to be subclassed.
- """
-
- def __init__(self, machine):
- """State instances should be created by the :class:`StateMachine`.
-
- They are not intended to be used outside this context.
-
- :param machine: The state machine instance this state belongs to.
- :type machine: StateMachine
- """
- self._machineRef = weakref.ref(machine) # Prevent cyclic reference
-
- @property
- def machine(self):
- """The state machine this state belongs to.
-
- Useful to access data or methods that are shared across states.
- """
- machine = self._machineRef()
- if machine is not None:
- return machine
- else:
- raise RuntimeError("Associated StateMachine is not valid")
-
- def goto(self, state, *args, **kwargs):
- """Performs a transition to a new state.
-
- Extra arguments are passed to the :meth:`enterState` method of the
- new state.
-
- :param str state: The name of the state to go to.
- """
- self.machine._goto(state, *args, **kwargs)
-
- def enterState(self, *args, **kwargs):
- """Called when the state machine enters this state.
-
- Arguments are those provided to the :meth:`goto` method that
- triggered the transition to this state.
- """
- pass
-
- def leaveState(self):
- """Called when the state machine leaves this state
- (i.e., when :meth:`goto` is called).
- """
- pass
-
- def validate(self):
- """Called externally to validate the current interaction in case of a
- creation.
- """
- pass
-
-class StateMachine(object):
- """State machine controller.
-
- This is the entry point of a state machine.
- It is in charge of dispatching received event and handling the
- current active state.
- """
-
- def __init__(self, states, initState, *args, **kwargs):
- """Create a state machine controller with an initial state.
-
- Extra arguments are passed to the :meth:`enterState` method
- of the initState.
-
- :param states: All states of the state machine
- :type states: dict of: {str name: State subclass}
- :param str initState: Key of the initial state in states
- """
- self.states = states
-
- self.state = self.states[initState](self)
- self.state.enterState(*args, **kwargs)
-
- def _goto(self, state, *args, **kwargs):
- self.state.leaveState()
- self.state = self.states[state](self)
- self.state.enterState(*args, **kwargs)
-
- def handleEvent(self, eventName, *args, **kwargs):
- """Process an event with the state machine.
-
- This method looks up for an event handler in the current state
- and then in the :class:`StateMachine` instance.
- Handler are looked up as 'onEventName' method.
- If a handler is found, it is called with the provided extra
- arguments, and this method returns the return value of the
- handler.
- If no handler is found, this method returns None.
-
- :param str eventName: Name of the event to handle
- :returns: The return value of the handler or None
- """
- handlerName = 'on' + eventName[0].upper() + eventName[1:]
- try:
- handler = getattr(self.state, handlerName)
- except AttributeError:
- try:
- handler = getattr(self, handlerName)
- except AttributeError:
- handler = None
- if handler is not None:
- return handler(*args, **kwargs)
-
- def validate(self):
- """Called externally to validate the current interaction in case of a
- creation.
- """
- self.state.validate()
-
-
-# clickOrDrag #################################################################
-
-LEFT_BTN = 'left'
-"""Left mouse button."""
-
-RIGHT_BTN = 'right'
-"""Right mouse button."""
-
-MIDDLE_BTN = 'middle'
-"""Middle mouse button."""
-
-
-class ClickOrDrag(StateMachine):
- """State machine for left and right click and left drag interaction.
-
- It is intended to be used through subclassing by overriding
- :meth:`click`, :meth:`beginDrag`, :meth:`drag` and :meth:`endDrag`.
-
- :param Set[str] clickButtons: Set of buttons that provides click interaction
- :param Set[str] dragButtons: Set of buttons that provides drag interaction
- """
-
- DRAG_THRESHOLD_SQUARE_DIST = 5 ** 2
-
- class Idle(State):
- def onPress(self, x, y, btn):
- if btn in self.machine.dragButtons:
- self.goto('clickOrDrag', x, y, btn)
- return True
- elif btn in self.machine.clickButtons:
- self.goto('click', x, y, btn)
- return True
-
- class Click(State):
- def enterState(self, x, y, btn):
- self.initPos = x, y
- self.button = btn
-
- def onMove(self, x, y):
- dx2 = (x - self.initPos[0]) ** 2
- dy2 = (y - self.initPos[1]) ** 2
- if (dx2 + dy2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
- self.goto('idle')
-
- def onRelease(self, x, y, btn):
- if btn == self.button:
- self.machine.click(x, y, btn)
- self.goto('idle')
-
- class ClickOrDrag(State):
- def enterState(self, x, y, btn):
- self.initPos = x, y
- self.button = btn
-
- def onMove(self, x, y):
- dx2 = (x - self.initPos[0]) ** 2
- dy2 = (y - self.initPos[1]) ** 2
- if (dx2 + dy2) >= self.machine.DRAG_THRESHOLD_SQUARE_DIST:
- self.goto('drag', self.initPos, (x, y), self.button)
-
- def onRelease(self, x, y, btn):
- if btn == self.button:
- if btn in self.machine.clickButtons:
- self.machine.click(x, y, btn)
- self.goto('idle')
-
- class Drag(State):
- def enterState(self, initPos, curPos, btn):
- self.initPos = initPos
- self.button = btn
- self.machine.beginDrag(*initPos, btn)
- self.machine.drag(*curPos, btn)
-
- def onMove(self, x, y):
- self.machine.drag(x, y, self.button)
-
- def onRelease(self, x, y, btn):
- if btn == self.button:
- self.machine.endDrag(self.initPos, (x, y), btn)
- self.goto('idle')
-
- def __init__(self,
- clickButtons=(LEFT_BTN, RIGHT_BTN),
- dragButtons=(LEFT_BTN,)):
- states = {
- 'idle': self.Idle,
- 'click': self.Click,
- 'clickOrDrag': self.ClickOrDrag,
- 'drag': self.Drag
- }
- self.__clickButtons = set(clickButtons)
- self.__dragButtons = set(dragButtons)
- super(ClickOrDrag, self).__init__(states, 'idle')
-
- clickButtons = property(lambda self: self.__clickButtons,
- doc="Buttons with click interaction (Set[int])")
-
- dragButtons = property(lambda self: self.__dragButtons,
- doc="Buttons with drag interaction (Set[int])")
-
- def click(self, x, y, btn):
- """Called upon a button supporting click.
-
- Override in subclass.
-
- :param int x: X mouse position in pixels.
- :param int y: Y mouse position in pixels.
- :param str btn: The mouse button which was clicked.
- """
- pass
-
- def beginDrag(self, x, y, btn):
- """Called at the beginning of a drag gesture with mouse button pressed.
-
- Override in subclass.
-
- :param int x: X mouse position in pixels.
- :param int y: Y mouse position in pixels.
- :param str btn: The mouse button for which a drag is starting.
- """
- pass
-
- def drag(self, x, y, btn):
- """Called on mouse moved during a drag gesture.
-
- Override in subclass.
-
- :param int x: X mouse position in pixels.
- :param int y: Y mouse position in pixels.
- :param str btn: The mouse button for which a drag is in progress.
- """
- pass
-
- def endDrag(self, startPoint, endPoint, btn):
- """Called at the end of a drag gesture when the mouse button is released.
-
- Override in subclass.
-
- :param List[int] startPoint:
- (x, y) mouse position in pixels at the beginning of the drag.
- :param List[int] endPoint:
- (x, y) mouse position in pixels at the end of the drag.
- :param str btn: The mouse button for which a drag is done.
- """
- pass
diff --git a/silx/gui/plot/ItemsSelectionDialog.py b/silx/gui/plot/ItemsSelectionDialog.py
deleted file mode 100644
index ebd1c64..0000000
--- a/silx/gui/plot/ItemsSelectionDialog.py
+++ /dev/null
@@ -1,286 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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()
-
- # respect order of kinds as set in method setKindsFilter
- itemsAndKind = []
- for kind in self.plot_item_kinds:
- itemClasses = self.plot._KIND_TO_CLASSES[kind]
- for item in self.plot.getItems():
- if isinstance(item, itemClasses) and item.isVisible():
- itemsAndKind.append((item, kind))
-
- self.setRowCount(len(itemsAndKind))
-
- for index, (item, kind) in enumerate(itemsAndKind):
- legend_twitem = qt.QTableWidgetItem(item.getName())
- self.setItem(index, 0, legend_twitem)
-
- kind_twitem = qt.QTableWidgetItem(kind)
- self.setItem(index, 1, kind_twitem)
-
- @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()
- item = self.plot._getItem(kind, legend)
- if item is not None:
- items.append(item)
-
- 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.getName(), 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
deleted file mode 100755
index 94112aa..0000000
--- a/silx/gui/plot/LegendSelector.py
+++ /dev/null
@@ -1,1036 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Widget displaying curves legends and allowing to operate on curves.
-
-This widget is meant to work with :class:`PlotWindow`.
-"""
-
-__authors__ = ["V.A. Sole", "T. Rueter", "T. Vincent"]
-__license__ = "MIT"
-__data__ = "16/10/2017"
-
-
-import logging
-import weakref
-
-import numpy
-
-from .. import qt, colors
-from ..widgets.LegendIconWidget import LegendIconWidget
-from . import items
-
-
-_logger = logging.getLogger(__name__)
-
-
-class LegendIcon(LegendIconWidget):
- """Object displaying a curve linestyle and symbol.
-
- :param QWidget parent: See :class:`QWidget`
- :param Union[~silx.gui.plot.items.Curve,None] curve:
- Curve with which to synchronize
- """
-
- def __init__(self, parent=None, curve=None):
- super(LegendIcon, self).__init__(parent)
- self._curveRef = None
- self.setCurve(curve)
-
- def getCurve(self):
- """Returns curve associated to this widget
-
- :rtype: Union[~silx.gui.plot.items.Curve,None]
- """
- return None if self._curveRef is None else self._curveRef()
-
- def setCurve(self, curve):
- """Set the curve with which to synchronize this widget.
-
- :param curve: Union[~silx.gui.plot.items.Curve,None]
- """
- assert curve is None or isinstance(curve, items.Curve)
-
- previousCurve = self.getCurve()
- if curve == previousCurve:
- return
-
- if previousCurve is not None:
- previousCurve.sigItemChanged.disconnect(self._curveChanged)
-
- self._curveRef = None if curve is None else weakref.ref(curve)
-
- if curve is not None:
- curve.sigItemChanged.connect(self._curveChanged)
-
- self._update()
-
- def _update(self):
- """Update widget according to current curve state.
- """
- curve = self.getCurve()
- if curve is None:
- _logger.error('Curve no more exists')
- self.setEnabled(False)
- return
-
- style = curve.getCurrentStyle()
-
- self.setEnabled(curve.isVisible())
- self.setSymbol(style.getSymbol())
- self.setLineWidth(style.getLineWidth())
- self.setLineStyle(style.getLineStyle())
-
- color = style.getColor()
- if numpy.array(color, copy=False).ndim != 1:
- # array of colors, use transparent black
- color = 0., 0., 0., 0.
- color = colors.rgba(color) # Make sure it is float in [0, 1]
- alpha = curve.getAlpha()
- color = qt.QColor.fromRgbF(
- color[0], color[1], color[2], color[3] * alpha)
- self.setLineColor(color)
- self.setSymbolColor(color)
- self.update() # TODO this should not be needed
-
- def _curveChanged(self, event):
- """Handle update of curve item
-
- :param event: Kind of change
- """
- if event in (items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SYMBOL,
- items.ItemChangedType.SYMBOL_SIZE,
- items.ItemChangedType.LINE_WIDTH,
- items.ItemChangedType.LINE_STYLE,
- items.ItemChangedType.COLOR,
- items.ItemChangedType.ALPHA,
- items.ItemChangedType.HIGHLIGHTED,
- items.ItemChangedType.HIGHLIGHTED_STYLE):
- self._update()
-
-
-class LegendModel(qt.QAbstractListModel):
- """Data model of curve legends.
-
- It holds the information of the curve:
-
- - color
- - line width
- - line style
- - visibility of the lines
- - symbol
- - visibility of the symbols
- """
- iconColorRole = qt.Qt.UserRole + 0
- iconLineWidthRole = qt.Qt.UserRole + 1
- iconLineStyleRole = qt.Qt.UserRole + 2
- showLineRole = qt.Qt.UserRole + 3
- iconSymbolRole = qt.Qt.UserRole + 4
- showSymbolRole = qt.Qt.UserRole + 5
-
- def __init__(self, legendList=None, parent=None):
- super(LegendModel, self).__init__(parent)
- if legendList is None:
- legendList = []
- self.legendList = []
- self.insertLegendList(0, legendList)
- self._palette = qt.QPalette()
-
- def __getitem__(self, idx):
- if idx >= len(self.legendList):
- raise IndexError('list index out of range')
- return self.legendList[idx]
-
- def rowCount(self, modelIndex=None):
- return len(self.legendList)
-
- def flags(self, index):
- return (qt.Qt.ItemIsEditable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsSelectable)
-
- def data(self, modelIndex, role):
- if modelIndex.isValid:
- idx = modelIndex.row()
- else:
- return None
- if idx >= len(self.legendList):
- 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])
- return legend
- elif role == qt.Qt.SizeHintRole:
- # size = qt.QSize(200,50)
- _logger.warning('LegendModel -- size hint role not implemented')
- return qt.QSize()
- elif role == qt.Qt.TextAlignmentRole:
- alignment = qt.Qt.AlignVCenter | qt.Qt.AlignLeft
- return alignment
- elif role == qt.Qt.BackgroundRole:
- # Background color, must be QBrush
- 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
- 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
- elif role == qt.Qt.ToolTipRole or role == qt.Qt.StatusTipRole:
- return ''
- elif role == self.iconColorRole:
- return item[1]['color']
- elif role == self.iconLineWidthRole:
- return item[1]['linewidth']
- elif role == self.iconLineStyleRole:
- return item[1]['linestyle']
- elif role == self.iconSymbolRole:
- return item[1]['symbol']
- elif role == self.showLineRole:
- return item[3]
- elif role == self.showSymbolRole:
- return item[4]
- else:
- _logger.info('Unkown role requested: %s', str(role))
- return None
-
- def setData(self, modelIndex, value, role):
- if modelIndex.isValid:
- idx = modelIndex.row()
- else:
- return None
- if idx >= len(self.legendList):
- # raise IndexError('list index out of range')
- _logger.warning(
- 'setData -- List index out of range, idx: %d', idx)
- return None
-
- item = self.legendList[idx]
- try:
- if role == qt.Qt.DisplayRole:
- # Set legend
- item[0] = str(value)
- elif role == self.iconColorRole:
- item[1]['color'] = qt.QColor(value)
- elif role == self.iconLineWidthRole:
- item[1]['linewidth'] = int(value)
- elif role == self.iconLineStyleRole:
- item[1]['linestyle'] = str(value)
- elif role == self.iconSymbolRole:
- item[1]['symbol'] = str(value)
- elif role == qt.Qt.CheckStateRole:
- item[2] = value
- elif role == self.showLineRole:
- item[3] = value
- elif role == self.showSymbolRole:
- item[4] = value
- except ValueError:
- _logger.warning('Conversion failed:\n\tvalue: %s\n\trole: %s',
- str(value), str(role))
- # Can that be right? Read docs again..
- self.dataChanged.emit(modelIndex, modelIndex)
- return True
-
- def insertLegendList(self, row, llist):
- """
- :param int row: Determines after which row the items are inserted
- :param llist: Carries the new legend information
- :type llist: List
- """
- modelIndex = self.createIndex(row, 0)
- count = len(llist)
- super(LegendModel, self).beginInsertRows(modelIndex,
- row,
- row + count)
- head = self.legendList[0:row]
- tail = self.legendList[row:]
- new = []
- for (legend, icon) in llist:
- linestyle = icon.get('linestyle', None)
- if LegendIconWidget.isEmptyLineStyle(linestyle):
- # Curve had no line, give it one and hide it
- # So when toggle line, it will display a solid line
- showLine = False
- icon['linestyle'] = '-'
- else:
- showLine = True
-
- symbol = icon.get('symbol', None)
- if LegendIconWidget.isEmptySymbol(symbol):
- # Curve had no symbol, give it one and hide it
- # So when toggle symbol, it will display 'o'
- showSymbol = False
- icon['symbol'] = 'o'
- else:
- showSymbol = True
-
- selected = icon.get('selected', True)
- item = [legend,
- icon,
- selected,
- showLine,
- showSymbol]
- new.append(item)
- self.legendList = head + new + tail
- super(LegendModel, self).endInsertRows()
- return True
-
- def insertRows(self, row, count, modelIndex=qt.QModelIndex()):
- raise NotImplementedError('Use LegendModel.insertLegendList instead')
-
- def removeRow(self, row):
- return self.removeRows(row, 1)
-
- def removeRows(self, row, count, modelIndex=qt.QModelIndex()):
- length = len(self.legendList)
- if length == 0:
- # Nothing to do..
- return True
- if row < 0 or row >= length:
- raise IndexError('Index out of range -- ' +
- 'idx: %d, len: %d' % (row, length))
- if count == 0:
- return False
- super(LegendModel, self).beginRemoveRows(modelIndex,
- row,
- row + count)
- del(self.legendList[row:row + count])
- super(LegendModel, self).endRemoveRows()
- return True
-
- def setEditor(self, event, editor):
- """
- :param str event: String that identifies the editor
- :param editor: Widget used to change data in the underlying model
- :type editor: QWidget
- """
- if event not in self.eventList:
- raise ValueError('setEditor -- Event must be in %s' %
- str(self.eventList))
- self.editorDict[event] = editor
-
-
-class LegendListItemWidget(qt.QItemDelegate):
- """Object displaying a single item (i.e., a row) in the list."""
-
- # Notice: LegendListItem does NOT inherit
- # from QObject, it cannot emit signals!
-
- def __init__(self, parent=None, itemType=0):
- super(LegendListItemWidget, self).__init__(parent)
-
- # Dictionary to render checkboxes
- self.cbDict = {}
- self.labelDict = {}
- self.iconDict = {}
-
- # Keep checkbox and legend to get sizeHint
- self.checkbox = qt.QCheckBox()
- self.legend = qt.QLabel()
- self.icon = LegendIcon()
-
- # Context Menu and Editors
- self.contextMenu = None
-
- def paint(self, painter, option, modelIndex):
- """
- Here be docs..
-
- :param QPainter painter:
- :param QStyleOptionViewItem option:
- :param QModelIndex modelIndex:
- """
- painter.save()
- rect = option.rect
-
- # Calculate the icon rectangle
- iconSize = self.icon.sizeHint()
- # Calculate icon position
- x = rect.left() + 2
- y = rect.top() + int(.5 * (rect.height() - iconSize.height()))
- iconRect = qt.QRect(qt.QPoint(x, y), iconSize)
-
- # Calculate label rectangle
- legendSize = qt.QSize(rect.width() - iconSize.width() - 30,
- rect.height())
- # Calculate label position
- x = rect.left() + iconRect.width()
- y = rect.top()
- labelRect = qt.QRect(qt.QPoint(x, y), legendSize)
- labelRect.translate(qt.QPoint(10, 0))
-
- # Calculate the checkbox rectangle
- x = rect.right() - 30
- y = rect.top()
- chBoxRect = qt.QRect(qt.QPoint(x, y), rect.bottomRight())
-
- # Remember the rectangles
- idx = modelIndex.row()
- self.cbDict[idx] = chBoxRect
- self.iconDict[idx] = iconRect
- self.labelDict[idx] = labelRect
-
- # Draw background first!
- if option.state & qt.QStyle.State_MouseOver:
- backgroundBrush = option.palette.highlight()
- else:
- backgroundBrush = modelIndex.data(qt.Qt.BackgroundRole)
- painter.fillRect(rect, backgroundBrush)
-
- # Draw label
- legendText = modelIndex.data(qt.Qt.DisplayRole)
- textBrush = modelIndex.data(qt.Qt.ForegroundRole)
- 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
- iconColor = modelIndex.data(LegendModel.iconColorRole)
- iconLineWidth = modelIndex.data(LegendModel.iconLineWidthRole)
- iconLineStyle = modelIndex.data(LegendModel.iconLineStyleRole)
- iconSymbol = modelIndex.data(LegendModel.iconSymbolRole)
- icon = LegendIcon()
- icon.resize(iconRect.size())
- icon.move(iconRect.topRight())
- icon.showSymbol = modelIndex.data(LegendModel.showSymbolRole)
- icon.showLine = modelIndex.data(LegendModel.showLineRole)
- icon.setSymbolColor(iconColor)
- icon.setLineColor(iconColor)
- icon.setLineWidth(iconLineWidth)
- icon.setLineStyle(iconLineStyle)
- icon.setSymbol(iconSymbol)
- icon.symbolOutlineBrush = backgroundBrush
- icon.paint(painter, iconRect, option.palette)
-
- # Draw the checkbox
- if modelIndex.data(qt.Qt.CheckStateRole):
- checkState = qt.Qt.Checked
- else:
- checkState = qt.Qt.Unchecked
-
- self.drawCheck(
- painter, qt.QStyleOptionViewItem(), chBoxRect, checkState)
-
- painter.restore()
-
- def editorEvent(self, event, model, option, modelIndex):
- # From the docs:
- # Mouse events are sent to editorEvent()
- # even if they don't start editing of the item.
- if event.button() == qt.Qt.RightButton and self.contextMenu:
- self.contextMenu.exec_(event.globalPos(), modelIndex)
- return True
- elif event.button() == qt.Qt.LeftButton:
- # Check if checkbox was clicked
- idx = modelIndex.row()
- cbRect = self.cbDict[idx]
- if cbRect.contains(event.pos()):
- # Toggle checkbox
- model.setData(modelIndex,
- not modelIndex.data(qt.Qt.CheckStateRole),
- qt.Qt.CheckStateRole)
- event.ignore()
- return True
- else:
- return super(LegendListItemWidget, self).editorEvent(
- event, model, option, modelIndex)
-
- def createEditor(self, parent, option, idx):
- _logger.info('### Editor request ###')
-
- def sizeHint(self, option, idx):
- # return qt.QSize(68,24)
- iconSize = self.icon.sizeHint()
- legendSize = self.legend.sizeHint()
- checkboxSize = self.checkbox.sizeHint()
- height = max([iconSize.height(),
- legendSize.height(),
- checkboxSize.height()]) + 4
- width = iconSize.width() + legendSize.width() + checkboxSize.width()
- return qt.QSize(width, height)
-
-
-class LegendListView(qt.QListView):
- """Widget displaying a list of curve legends, line style and symbol."""
-
- sigLegendSignal = qt.Signal(object)
- """Signal emitting a dict when an action is triggered by the user."""
-
- __mouseClickedEvent = 'mouseClicked'
- __checkBoxClickedEvent = 'checkBoxClicked'
- __legendClickedEvent = 'legendClicked'
-
- def __init__(self, parent=None, model=None, contextMenu=None):
- super(LegendListView, self).__init__(parent)
- self.__lastButton = None
- self.__lastClickPos = None
- self.__lastModelIdx = None
- # Set default delegate
- self.setItemDelegate(LegendListItemWidget())
- # Set default editors
- # self.setSizePolicy(qt.QSizePolicy.MinimumExpanding,
- # qt.QSizePolicy.MinimumExpanding)
- # Set edit triggers by hand using self.edit(QModelIndex)
- # in mousePressEvent (better to control than signals)
- self.setEditTriggers(qt.QAbstractItemView.NoEditTriggers)
-
- # Control layout
- # self.setBatchSize(2)
- # self.setLayoutMode(qt.QListView.Batched)
- # self.setFlow(qt.QListView.LeftToRight)
-
- # Control selection
- self.setSelectionMode(qt.QAbstractItemView.NoSelection)
-
- if model is None:
- model = LegendModel(parent=self)
- self.setModel(model)
- self.setContextMenu(contextMenu)
-
- def setLegendList(self, legendList, row=None):
- if row is not None:
- model = self.model()
- model.insertLegendList(row, legendList)
- elif len(legendList) != self.model().rowCount():
- self.clear()
- model = self.model()
- model.insertLegendList(0, legendList)
- else:
- model = self.model()
- for i, (new_legend, icon) in enumerate(legendList):
- modelIndex = model.index(i)
- legend = str(modelIndex.data(qt.Qt.DisplayRole))
- if new_legend != legend:
- model.setData(modelIndex, new_legend, qt.Qt.DisplayRole)
-
- color = modelIndex.data(LegendModel.iconColorRole)
- new_color = icon.get('color', None)
- if new_color != color:
- model.setData(modelIndex, new_color, LegendModel.iconColorRole)
-
- linewidth = modelIndex.data(LegendModel.iconLineWidthRole)
- new_linewidth = icon.get('linewidth', 1.0)
- if new_linewidth != linewidth:
- model.setData(modelIndex, new_linewidth, LegendModel.iconLineWidthRole)
-
- linestyle = modelIndex.data(LegendModel.iconLineStyleRole)
- new_linestyle = icon.get('linestyle', None)
- visible = not LegendIconWidget.isEmptyLineStyle(new_linestyle)
- model.setData(modelIndex, visible, LegendModel.showLineRole)
- if new_linestyle != linestyle:
- model.setData(modelIndex, new_linestyle, LegendModel.iconLineStyleRole)
-
- symbol = modelIndex.data(LegendModel.iconSymbolRole)
- new_symbol = icon.get('symbol', None)
- visible = not LegendIconWidget.isEmptySymbol(new_symbol)
- model.setData(modelIndex, visible, LegendModel.showSymbolRole)
- if new_symbol != symbol:
- model.setData(modelIndex, new_symbol, LegendModel.iconSymbolRole)
-
- selected = modelIndex.data(qt.Qt.CheckStateRole)
- new_selected = icon.get('selected', True)
- if new_selected != selected:
- model.setData(modelIndex, new_selected, qt.Qt.CheckStateRole)
- _logger.debug('LegendListView.setLegendList(legendList) finished')
-
- def clear(self):
- model = self.model()
- model.removeRows(0, model.rowCount())
- _logger.debug('LegendListView.clear() finished')
-
- def setContextMenu(self, contextMenu=None):
- delegate = self.itemDelegate()
- if isinstance(delegate, LegendListItemWidget) and self.model():
- if contextMenu is None:
- delegate.contextMenu = LegendListContextMenu(self.model())
- delegate.contextMenu.sigContextMenu.connect(
- self._contextMenuSlot)
- else:
- delegate.contextMenu = contextMenu
-
- def __getitem__(self, idx):
- model = self.model()
- try:
- item = model[idx]
- except ValueError:
- item = None
- return item
-
- def _contextMenuSlot(self, ddict):
- self.sigLegendSignal.emit(ddict)
-
- def mousePressEvent(self, event):
- self.__lastButton = event.button()
- self.__lastPosition = event.pos()
- super(LegendListView, self).mousePressEvent(event)
- # call _handleMouseClick after editing was handled
- # If right click (context menu) is aborted, no
- # signal is emitted..
- self._handleMouseClick(self.indexAt(self.__lastPosition))
-
- def mouseDoubleClickEvent(self, event):
- self.__lastButton = event.button()
- self.__lastPosition = event.pos()
- super(LegendListView, self).mouseDoubleClickEvent(event)
- # call _handleMouseClick after editing was handled
- # If right click (context menu) is aborted, no
- # signal is emitted..
- self._handleMouseClick(self.indexAt(self.__lastPosition))
-
- def mouseMoveEvent(self, event):
- # LegendListView.mouseMoveEvent is overwritten
- # to suppress unwanted behavior in the delegate.
- pass
-
- def mouseReleaseEvent(self, event):
- # LegendListView.mouseReleaseEvent is overwritten
- # to subpress unwanted behavior in the delegate.
- pass
-
- def _handleMouseClick(self, modelIndex):
- """
- Distinguish between mouse click on Legend
- and mouse click on CheckBox by setting the
- currentCheckState attribute in LegendListItem.
-
- Emits signal sigLegendSignal(ddict)
-
- :param QModelIndex modelIndex: index of the clicked item
- """
- _logger.debug('self._handleMouseClick called')
- if self.__lastButton not in [qt.Qt.LeftButton,
- qt.Qt.RightButton]:
- return
- if not modelIndex.isValid():
- _logger.debug('_handleMouseClick -- Invalid QModelIndex')
- return
- # model = self.model()
- idx = modelIndex.row()
-
- delegate = self.itemDelegate()
- cbClicked = False
- if isinstance(delegate, LegendListItemWidget):
- for cbRect in delegate.cbDict.values():
- if cbRect.contains(self.__lastPosition):
- cbClicked = True
- break
-
- # TODO: Check for doubleclicks on legend/icon and spawn editors
-
- ddict = {
- 'legend': str(modelIndex.data(qt.Qt.DisplayRole)),
- 'icon': {
- 'linewidth': str(modelIndex.data(
- LegendModel.iconLineWidthRole)),
- 'linestyle': str(modelIndex.data(
- LegendModel.iconLineStyleRole)),
- 'symbol': str(modelIndex.data(LegendModel.iconSymbolRole))
- },
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data())
- }
- if self.__lastButton == qt.Qt.RightButton:
- _logger.debug('Right clicked')
- ddict['button'] = "right"
- ddict['event'] = self.__mouseClickedEvent
- elif cbClicked:
- _logger.debug('CheckBox clicked')
- ddict['button'] = "left"
- ddict['event'] = self.__checkBoxClickedEvent
- else:
- _logger.debug('Legend clicked')
- ddict['button'] = "left"
- ddict['event'] = self.__legendClickedEvent
- _logger.debug(' idx: %d\n ddict: %s', idx, str(ddict))
- self.sigLegendSignal.emit(ddict)
-
-
-class LegendListContextMenu(qt.QMenu):
- """Contextual menu associated to items in a :class:`LegendListView`."""
-
- sigContextMenu = qt.Signal(object)
- """Signal emitting a dict upon contextual menu actions."""
-
- def __init__(self, model):
- super(LegendListContextMenu, self).__init__(parent=None)
- self.model = model
-
- self.addAction('Set Active', self.setActiveAction)
- self.addAction('Map to left', self.mapToLeftAction)
- self.addAction('Map to right', self.mapToRightAction)
-
- self._pointsAction = self.addAction(
- 'Points', self.togglePointsAction)
- self._pointsAction.setCheckable(True)
-
- self._linesAction = self.addAction('Lines', self.toggleLinesAction)
- self._linesAction.setCheckable(True)
-
- self.addAction('Remove curve', self.removeItemAction)
- self.addAction('Rename curve', self.renameItemAction)
-
- def exec_(self, pos, idx):
- self.__currentIdx = idx
-
- # Set checkable action state
- modelIndex = self.currentIdx()
- self._pointsAction.setChecked(
- modelIndex.data(LegendModel.showSymbolRole))
- self._linesAction.setChecked(
- modelIndex.data(LegendModel.showLineRole))
-
- super(LegendListContextMenu, self).popup(pos)
-
- def currentIdx(self):
- return self.__currentIdx
-
- def mapToLeftAction(self):
- _logger.debug('LegendListContextMenu.mapToLeftAction called')
- modelIndex = self.currentIdx()
- legend = str(modelIndex.data(qt.Qt.DisplayRole))
- ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "mapToLeft"
- }
- self.sigContextMenu.emit(ddict)
-
- def mapToRightAction(self):
- _logger.debug('LegendListContextMenu.mapToRightAction called')
- modelIndex = self.currentIdx()
- legend = str(modelIndex.data(qt.Qt.DisplayRole))
- ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "mapToRight"
- }
- self.sigContextMenu.emit(ddict)
-
- def removeItemAction(self):
- _logger.debug('LegendListContextMenu.removeCurveAction called')
- modelIndex = self.currentIdx()
- legend = str(modelIndex.data(qt.Qt.DisplayRole))
- ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "removeCurve"
- }
- self.model.removeRow(modelIndex.row())
- self.sigContextMenu.emit(ddict)
-
- def renameItemAction(self):
- _logger.debug('LegendListContextMenu.renameCurveAction called')
- modelIndex = self.currentIdx()
- legend = str(modelIndex.data(qt.Qt.DisplayRole))
- ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "renameCurve"
- }
- self.sigContextMenu.emit(ddict)
-
- def toggleLinesAction(self):
- modelIndex = self.currentIdx()
- legend = str(modelIndex.data(qt.Qt.DisplayRole))
- ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- }
- linestyle = modelIndex.data(LegendModel.iconLineStyleRole)
- visible = not modelIndex.data(LegendModel.showLineRole)
- _logger.debug('toggleLinesAction -- lines visible: %s', str(visible))
- ddict['event'] = "toggleLine"
- ddict['line'] = visible
- ddict['linestyle'] = linestyle if visible else ''
- self.model.setData(modelIndex, visible, LegendModel.showLineRole)
- self.sigContextMenu.emit(ddict)
-
- def togglePointsAction(self):
- modelIndex = self.currentIdx()
- legend = str(modelIndex.data(qt.Qt.DisplayRole))
- ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- }
- flag = modelIndex.data(LegendModel.showSymbolRole)
- symbol = modelIndex.data(LegendModel.iconSymbolRole)
- visible = not flag or LegendIconWidget.isEmptySymbol(symbol)
- _logger.debug(
- 'togglePointsAction -- Symbols visible: %s', str(visible))
-
- ddict['event'] = "togglePoints"
- ddict['points'] = visible
- ddict['symbol'] = symbol if visible else ''
- self.model.setData(modelIndex, visible, LegendModel.showSymbolRole)
- self.sigContextMenu.emit(ddict)
-
- def setActiveAction(self):
- modelIndex = self.currentIdx()
- legend = str(modelIndex.data(qt.Qt.DisplayRole))
- _logger.debug('setActiveAction -- active curve: %s', legend)
- ddict = {
- 'legend': legend,
- 'label': legend,
- 'selected': modelIndex.data(qt.Qt.CheckStateRole),
- 'type': str(modelIndex.data()),
- 'event': "setActiveCurve",
- }
- self.sigContextMenu.emit(ddict)
-
-
-class RenameCurveDialog(qt.QDialog):
- """Dialog box to input the name of a curve."""
-
- def __init__(self, parent=None, current="", curves=()):
- super(RenameCurveDialog, self).__init__(parent)
- self.setWindowTitle("Rename Curve %s" % current)
- self.curves = curves
- layout = qt.QVBoxLayout(self)
- self.lineEdit = qt.QLineEdit(self)
- self.lineEdit.setText(current)
- self.hbox = qt.QWidget(self)
- self.hboxLayout = qt.QHBoxLayout(self.hbox)
- self.hboxLayout.addStretch(1)
- self.okButton = qt.QPushButton(self.hbox)
- self.okButton.setText('OK')
- self.hboxLayout.addWidget(self.okButton)
- self.cancelButton = qt.QPushButton(self.hbox)
- self.cancelButton.setText('Cancel')
- self.hboxLayout.addWidget(self.cancelButton)
- self.hboxLayout.addStretch(1)
- layout.addWidget(self.lineEdit)
- layout.addWidget(self.hbox)
- self.okButton.clicked.connect(self.preAccept)
- self.cancelButton.clicked.connect(self.reject)
-
- def preAccept(self):
- text = str(self.lineEdit.text())
- addedText = ""
- if len(text):
- if text not in self.curves:
- self.accept()
- return
- else:
- addedText = "Curve already exists."
- text = "Invalid Curve Name"
- msg = qt.QMessageBox(self)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setWindowTitle(text)
- text += "\n%s" % addedText
- msg.setText(text)
- msg.exec_()
-
- def getText(self):
- return str(self.lineEdit.text())
-
-
-class LegendsDockWidget(qt.QDockWidget):
- """QDockWidget with a :class:`LegendSelector` connected to a PlotWindow.
-
- It makes the link between the LegendListView widget and the PlotWindow.
-
- :param parent: See :class:`QDockWidget`
- :param plot: :class:`.PlotWindow` instance on which to operate
- """
-
- def __init__(self, parent=None, plot=None):
- assert plot is not None
- self._plotRef = weakref.ref(plot)
- self._isConnected = False # True if widget connected to plot signals
-
- super(LegendsDockWidget, self).__init__("Legends", parent)
-
- self._legendWidget = LegendListView()
-
- self.layout().setContentsMargins(0, 0, 0, 0)
- self.setWidget(self._legendWidget)
-
- self.visibilityChanged.connect(
- self._visibilityChangedHandler)
-
- self._legendWidget.sigLegendSignal.connect(self._legendSignalHandler)
-
- @property
- def plot(self):
- """The :class:`.PlotWindow` this widget is attached to."""
- return self._plotRef()
-
- def renameCurve(self, oldLegend, newLegend):
- """Change the name of a curve using remove and addCurve
-
- :param str oldLegend: The legend of the curve to be changed
- :param str newLegend: The new legend of the curve
- """
- is_active = self.plot.getActiveCurve(just_legend=True) == oldLegend
- curve = self.plot.getCurve(oldLegend)
- self.plot.remove(oldLegend, kind='curve')
- self.plot.addCurve(curve.getXData(copy=False),
- curve.getYData(copy=False),
- legend=newLegend,
- info=curve.getInfo(),
- color=curve.getColor(),
- symbol=curve.getSymbol(),
- linewidth=curve.getLineWidth(),
- linestyle=curve.getLineStyle(),
- xlabel=curve.getXLabel(),
- ylabel=curve.getYLabel(),
- xerror=curve.getXErrorData(copy=False),
- yerror=curve.getYErrorData(copy=False),
- z=curve.getZValue(),
- selectable=curve.isSelectable(),
- fill=curve.isFill(),
- resetzoom=False)
- if is_active:
- self.plot.setActiveCurve(newLegend)
-
- def _legendSignalHandler(self, ddict):
- """Handles events from the LegendListView signal"""
- _logger.debug("Legend signal ddict = %s", str(ddict))
-
- if ddict['event'] == "legendClicked":
- if ddict['button'] == "left":
- self.plot.setActiveCurve(ddict['legend'])
-
- elif ddict['event'] == "removeCurve":
- self.plot.removeCurve(ddict['legend'])
-
- elif ddict['event'] == "renameCurve":
- curveList = self.plot.getAllCurves(just_legend=True)
- oldLegend = ddict['legend']
- dialog = RenameCurveDialog(self.plot, oldLegend, curveList)
- ret = dialog.exec_()
- if ret:
- newLegend = dialog.getText()
- self.renameCurve(oldLegend, newLegend)
-
- elif ddict['event'] == "setActiveCurve":
- self.plot.setActiveCurve(ddict['legend'])
-
- elif ddict['event'] == "checkBoxClicked":
- self.plot.hideCurve(ddict['legend'], not ddict['selected'])
-
- elif ddict['event'] in ["mapToRight", "mapToLeft"]:
- legend = ddict['legend']
- curve = self.plot.getCurve(legend)
- yaxis = 'right' if ddict['event'] == 'mapToRight' else 'left'
- self.plot.addCurve(x=curve.getXData(copy=False),
- y=curve.getYData(copy=False),
- legend=curve.getName(),
- info=curve.getInfo(),
- yaxis=yaxis)
-
- elif ddict['event'] == "togglePoints":
- legend = ddict['legend']
- curve = self.plot.getCurve(legend)
- symbol = ddict['symbol'] if ddict['points'] else ''
- self.plot.addCurve(x=curve.getXData(copy=False),
- y=curve.getYData(copy=False),
- legend=curve.getName(),
- info=curve.getInfo(),
- symbol=symbol)
-
- elif ddict['event'] == "toggleLine":
- legend = ddict['legend']
- curve = self.plot.getCurve(legend)
- linestyle = ddict['linestyle'] if ddict['line'] else ''
- self.plot.addCurve(x=curve.getXData(copy=False),
- y=curve.getYData(copy=False),
- legend=curve.getName(),
- info=curve.getInfo(),
- linestyle=linestyle)
-
- else:
- _logger.debug("unhandled event %s", str(ddict['event']))
-
- def updateLegends(self, *args):
- """Sync the LegendSelector widget displayed info with the plot.
- """
- legendList = []
- for curve in self.plot.getAllCurves(withhidden=True):
- legend = curve.getName()
- # Use active color if curve is active
- isActive = legend == self.plot.getActiveCurve(just_legend=True)
- style = curve.getCurrentStyle()
- color = style.getColor()
- if numpy.array(color, copy=False).ndim != 1:
- # array of colors, use transparent black
- color = 0., 0., 0., 0.
-
- curveInfo = {
- 'color': qt.QColor.fromRgbF(*color),
- 'linewidth': style.getLineWidth(),
- 'linestyle': style.getLineStyle(),
- 'symbol': style.getSymbol(),
- 'selected': not self.plot.isCurveHidden(legend),
- 'active': isActive}
- legendList.append((legend, curveInfo))
-
- self._legendWidget.setLegendList(legendList)
-
- def _visibilityChangedHandler(self, visible):
- if visible:
- self.updateLegends()
- if not self._isConnected:
- self.plot.sigContentChanged.connect(self.updateLegends)
- self.plot.sigActiveCurveChanged.connect(self.updateLegends)
- self._isConnected = True
- else:
- if self._isConnected:
- self.plot.sigContentChanged.disconnect(self.updateLegends)
- self.plot.sigActiveCurveChanged.disconnect(self.updateLegends)
- self._isConnected = False
-
- def showEvent(self, event):
- """Make sure this widget is raised when it is shown
- (when it is first created as a tab in PlotWindow or when it is shown
- again after hiding).
- """
- self.raise_()
diff --git a/silx/gui/plot/LimitsHistory.py b/silx/gui/plot/LimitsHistory.py
deleted file mode 100644
index a323548..0000000
--- a/silx/gui/plot/LimitsHistory.py
+++ /dev/null
@@ -1,83 +0,0 @@
-# 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/MaskToolsWidget.py b/silx/gui/plot/MaskToolsWidget.py
deleted file mode 100644
index 1ec1e7f..0000000
--- a/silx/gui/plot/MaskToolsWidget.py
+++ /dev/null
@@ -1,919 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Widget providing a set of tools to draw masks on a PlotWidget.
-
-This widget is meant to work with :class:`silx.gui.plot.PlotWidget`.
-
-- :class:`ImageMask`: Handle mask bitmap update and history
-- :class:`MaskToolsWidget`: GUI for :class:`Mask`
-- :class:`MaskToolsDockWidget`: DockWidget to integrate in :class:`PlotWindow`
-"""
-from __future__ import division
-
-__authors__ = ["T. Vincent", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "08/12/2020"
-
-import os
-import sys
-import numpy
-import logging
-import collections
-import h5py
-
-from silx.image import shapes
-from silx.io.utils import NEXUS_HDF5_EXT, is_dataset
-from silx.gui.dialog.DatasetDialog import DatasetDialog
-
-from ._BaseMaskToolsWidget import BaseMask, BaseMaskToolsWidget, BaseMaskToolsDockWidget
-from . import items
-from ..colors import cursorColorForColormap, rgba
-from .. import qt
-from ..utils import LockReentrant
-
-from silx.third_party.EdfFile import EdfFile
-from silx.third_party.TiffIO import TiffIO
-
-import fabio
-
-_logger = logging.getLogger(__name__)
-
-_HDF5_EXT_STR = ' '.join(['*' + ext for ext in NEXUS_HDF5_EXT])
-
-
-def _selectDataset(filename, mode=DatasetDialog.SaveMode):
- """Open a dialog to prompt the user to select a dataset in
- a hdf5 file.
-
- :param str filename: name of an existing HDF5 file
- :param mode: DatasetDialog.SaveMode or DatasetDialog.LoadMode
- :rtype: str
- :return: Name of selected dataset
- """
- dialog = DatasetDialog()
- dialog.addFile(filename)
- dialog.setWindowTitle("Select a 2D dataset")
- dialog.setMode(mode)
- if not dialog.exec_():
- return None
- return dialog.getSelectedDataUrl().data_path()
-
-
-class ImageMask(BaseMask):
- """A 2D mask field with update operations.
-
- Coords follows (row, column) convention and are in mask array coords.
-
- This is meant for internal use by :class:`MaskToolsWidget`.
- """
-
- def __init__(self, image=None):
- """
-
- :param image: :class:`silx.gui.plot.items.ImageBase` instance
- """
- BaseMask.__init__(self, image)
- self.reset(shape=(0, 0)) # Init the mask with a 2D shape
-
- def getDataValues(self):
- """Return image data as a 2D or 3D array (if it is a RGBA image).
-
- :rtype: 2D or 3D numpy.ndarray
- """
- return self._dataItem.getData(copy=False)
-
- def save(self, filename, kind):
- """Save current mask in a file
-
- :param str filename: The file where to save to mask
- :param str kind: The kind of file to save in 'edf', 'tif', 'npy', 'h5'
- or 'msk' (if FabIO is installed)
- :raise Exception: Raised if the file writing fail
- """
- if kind == 'edf':
- edfFile = EdfFile(filename, access="w+")
- header = {"program_name": "silx-mask", "masked_value": "nonzero"}
- edfFile.WriteImage(header, self.getMask(copy=False), Append=0)
-
- elif kind == 'tif':
- tiffFile = TiffIO(filename, mode='w')
- tiffFile.writeImage(self.getMask(copy=False), software='silx')
-
- elif kind == 'npy':
- try:
- numpy.save(filename, self.getMask(copy=False))
- except IOError:
- raise RuntimeError("Mask file can't be written")
-
- elif ("." + kind) in NEXUS_HDF5_EXT:
- self._saveToHdf5(filename, self.getMask(copy=False))
-
- elif kind == 'msk':
- try:
- data = self.getMask(copy=False)
- image = fabio.fabioimage.FabioImage(data=data)
- image = image.convert(fabio.fit2dmaskimage.Fit2dMaskImage)
- image.save(filename)
- except Exception:
- _logger.debug("Backtrace", exc_info=True)
- raise RuntimeError("Mask file can't be written")
- else:
- raise ValueError("Format '%s' is not supported" % kind)
-
- @staticmethod
- def _saveToHdf5(filename, mask):
- """Save a mask array to a HDF5 file.
-
- :param str filename: name of an existing HDF5 file
- :param numpy.ndarray mask: Mask array.
- :returns: True if operation succeeded, False otherwise.
- """
- if not os.path.exists(filename):
- # create new file
- with h5py.File(filename, "w") as _h5f:
- pass
- dataPath = _selectDataset(filename)
- if dataPath is None:
- return False
- with h5py.File(filename, "a") as h5f:
- existing_ds = h5f.get(dataPath)
- if existing_ds is not None:
- reply = qt.QMessageBox.question(
- None,
- "Confirm overwrite",
- "Do you want to overwrite an existing dataset?",
- qt.QMessageBox.Yes | qt.QMessageBox.No)
- if reply != qt.QMessageBox.Yes:
- return False
- del h5f[dataPath]
- try:
- h5f.create_dataset(dataPath, data=mask)
- except Exception:
- return False
- return True
-
- # Drawing operations
- def updateRectangle(self, level, row, col, height, width, mask=True):
- """Mask/Unmask a rectangle of the given mask level.
-
- :param int level: Mask level to update.
- :param int row: Starting row of the rectangle
- :param int col: Starting column of the rectangle
- :param int height:
- :param int width:
- :param bool mask: True to mask (default), False to unmask.
- """
- assert 0 < level < 256
- if row + height <= 0 or col + width <= 0:
- return # Rectangle outside image, avoid negative indices
- selection = self._mask[max(0, row):row + height + 1,
- max(0, col):col + width + 1]
- if mask:
- selection[:,:] = level
- else:
- selection[selection == level] = 0
- self._notify()
-
- def updatePolygon(self, level, vertices, mask=True):
- """Mask/Unmask a polygon of the given mask level.
-
- :param int level: Mask level to update.
- :param vertices: Nx2 array of polygon corners as (row, col)
- :param bool mask: True to mask (default), False to unmask.
- """
- fill = shapes.polygon_fill_mask(vertices, self._mask.shape)
- if mask:
- self._mask[fill != 0] = level
- else:
- self._mask[numpy.logical_and(fill != 0,
- self._mask == level)] = 0
- self._notify()
-
- def updatePoints(self, level, rows, cols, mask=True):
- """Mask/Unmask points with given coordinates.
-
- :param int level: Mask level to update.
- :param rows: Rows of selected points
- :type rows: 1D numpy.ndarray
- :param cols: Columns of selected points
- :type cols: 1D numpy.ndarray
- :param bool mask: True to mask (default), False to unmask.
- """
- valid = numpy.logical_and(
- numpy.logical_and(rows >= 0, cols >= 0),
- numpy.logical_and(rows < self._mask.shape[0],
- cols < self._mask.shape[1]))
- rows, cols = rows[valid], cols[valid]
-
- if mask:
- self._mask[rows, cols] = level
- else:
- inMask = self._mask[rows, cols] == level
- self._mask[rows[inMask], cols[inMask]] = 0
- self._notify()
-
- def updateDisk(self, level, crow, ccol, radius, mask=True):
- """Mask/Unmask a disk of the given mask level.
-
- :param int level: Mask level to update.
- :param int crow: Disk center row.
- :param int ccol: Disk center column.
- :param float radius: Radius of the disk in mask array unit
- :param bool mask: True to mask (default), False to unmask.
- """
- rows, cols = shapes.circle_fill(crow, ccol, radius)
- self.updatePoints(level, rows, cols, mask)
-
- def updateEllipse(self, level, crow, ccol, radius_r, radius_c, mask=True):
- """Mask/Unmask an ellipse of the given mask level.
-
- :param int level: Mask level to update.
- :param int crow: Row of the center of the ellipse
- :param int ccol: Column of the center of the ellipse
- :param float radius_r: Radius of the ellipse in the row
- :param float radius_c: Radius of the ellipse in the column
- :param bool mask: True to mask (default), False to unmask.
- """
- rows, cols = shapes.ellipse_fill(crow, ccol, radius_r, radius_c)
- self.updatePoints(level, rows, cols, mask)
-
- def updateLine(self, level, row0, col0, row1, col1, width, mask=True):
- """Mask/Unmask a line of the given mask level.
-
- :param int level: Mask level to update.
- :param int row0: Row of the starting point.
- :param int col0: Column of the starting point.
- :param int row1: Row of the end point.
- :param int col1: Column of the end point.
- :param int width: Width of the line in mask array unit.
- :param bool mask: True to mask (default), False to unmask.
- """
- rows, cols = shapes.draw_line(row0, col0, row1, col1, width)
- self.updatePoints(level, rows, cols, mask)
-
-
-class MaskToolsWidget(BaseMaskToolsWidget):
- """Widget with tools for drawing mask on an image in a PlotWidget."""
-
- _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.__itemMaskUpdatedLock = LockReentrant()
- self.__itemMaskUpdated = False
-
- def __maskStateChanged(self) -> None:
- """Handle mask commit to update item mask"""
- item = self._mask.getDataItem()
- if item is not None:
- with self.__itemMaskUpdatedLock:
- item.setMaskData(self._mask.getMask(copy=True), copy=False)
-
- def setItemMaskUpdated(self, enabled: bool) -> None:
- """Toggle item mask and mask tool synchronisation.
-
- :param bool enabled: True to synchronise. Default: False
- """
- enabled = bool(enabled)
- if enabled != self.__itemMaskUpdated:
- if self.__itemMaskUpdated:
- self._mask.sigStateChanged.disconnect(self.__maskStateChanged)
- self.__itemMaskUpdated = enabled
- if self.__itemMaskUpdated:
- # Synchronize item and tool mask
- self._setMaskedImage(self._mask.getDataItem())
- self._mask.sigStateChanged.connect(self.__maskStateChanged)
-
- def isItemMaskUpdated(self) -> bool:
- """Returns whether or not item and mask tool masks are synchronised.
-
- :rtype: bool
- """
- return self.__itemMaskUpdated
-
- def setSelectionMask(self, mask, copy=True):
- """Set the mask to a new array.
-
- :param numpy.ndarray mask:
- The array to use for the mask or None to reset the mask.
- :type mask: numpy.ndarray of uint8 of dimension 2, C-contiguous.
- Array of other types are converted.
- :param bool copy: True (the default) to copy the array,
- False to use it as is if possible.
- :return: None if failed, shape of mask as 2-tuple if successful.
- The mask can be cropped or padded to fit active image,
- the returned shape is that of the active image.
- """
- if mask is None:
- self.resetSelectionMask()
- return self._data.shape[:2]
-
- mask = numpy.array(mask, copy=False, dtype=numpy.uint8)
- if len(mask.shape) != 2:
- _logger.error('Not an image, shape: %d', len(mask.shape))
- return None
-
- # Handle mask with single level
- if self.multipleMasks() == 'single':
- mask = numpy.array(mask != 0, dtype=numpy.uint8)
-
- # if mask has not changed, do nothing
- if numpy.array_equal(mask, self.getSelectionMask()):
- return mask.shape
-
- 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()
- return mask.shape
- else:
- _logger.warning('Mask has not the same size as current image.'
- ' Mask will be cropped or padded to fit image'
- ' dimensions. %s != %s',
- str(mask.shape), str(self._data.shape))
- resizedMask = numpy.zeros(self._data.shape[0:2],
- dtype=numpy.uint8)
- height = min(self._data.shape[0], mask.shape[0])
- width = min(self._data.shape[1], mask.shape[1])
- resizedMask[:height,:width] = mask[:height,:width]
- self._mask.setMask(resizedMask, copy=False)
- self._mask.commit()
- return resizedMask.shape
-
- # Handle mask refresh on the plot
- def _updatePlotMask(self):
- """Update mask image in plot"""
- mask = self.getSelectionMask(copy=False)
- if mask is not None:
- # get the mask from the plot
- maskItem = self.plot.getImage(self._maskName)
- mustBeAdded = maskItem is None
- if mustBeAdded:
- maskItem = items.MaskImageData()
- maskItem.setName(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.addItem(maskItem)
-
- elif self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
-
- def showEvent(self, event):
- try:
- self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChangedAfterCare)
- except (RuntimeError, TypeError):
- pass
-
- # Sync with current active image
- self._setMaskedImage(self.plot.getActiveImage())
- self.plot.sigActiveImageChanged.connect(self._activeImageChanged)
-
- def hideEvent(self, event):
- try:
- self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChanged)
- except (RuntimeError, TypeError):
- pass
-
- image = self.getMaskedItem()
- if image is not None:
- try:
- image.sigItemChanged.disconnect(self.__imageChanged)
- except (RuntimeError, TypeError):
- pass # TODO should not happen
-
- if self.isMaskInteractionActivated():
- # Disable drawing tool
- self.browseAction.trigger()
-
- if self.isItemMaskUpdated(): # No "after-care"
- self._data = numpy.zeros((0, 0), dtype=numpy.uint8)
- self._mask.setDataItem(None)
- self._mask.reset()
-
- if self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
-
- elif self.getSelectionMask(copy=False) is not None:
- self.plot.sigActiveImageChanged.connect(
- self._activeImageChangedAfterCare)
-
- def _activeImageChanged(self, previous, current):
- """Reacts upon active image change.
-
- Only handle change of active image items here.
- """
- if previous != current:
- image = self.plot.getActiveImage()
- if image is not None and image.getName() == self._maskName:
- image = None # Active image is the mask
- self._setMaskedImage(image)
-
- def _setOverlayColorForImage(self, image):
- """Set the color of overlay adapted to image
-
- :param image: :class:`.items.ImageBase` object to set color for.
- """
- if isinstance(image, items.ColormapMixIn):
- colormap = image.getColormap()
- self._defaultOverlayColor = rgba(
- cursorColorForColormap(colormap['name']))
- else:
- self._defaultOverlayColor = rgba('black')
-
- def _activeImageChangedAfterCare(self, *args):
- """Check synchro of active image and mask when mask widget is hidden.
-
- If active image has no more the same size as the mask, the mask is
- removed, otherwise it is adjusted to origin, scale and z.
- """
- activeImage = self.plot.getActiveImage()
- if activeImage is None or activeImage.getName() == self._maskName:
- # No active image or active image is the mask...
- self._data = numpy.zeros((0, 0), dtype=numpy.uint8)
- self._mask.setDataItem(None)
- self._mask.reset()
-
- if self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
-
- self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChangedAfterCare)
- else:
- self._setOverlayColorForImage(activeImage)
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
-
- self._origin = activeImage.getOrigin()
- self._scale = activeImage.getScale()
- self._z = activeImage.getZValue() + 1
- self._data = activeImage.getData(copy=False)
- if self._data.shape[:2] != self._mask.getMask(copy=False).shape:
- # Image has not the same size, remove mask and stop listening
- if self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
-
- self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChangedAfterCare)
- else:
- # Refresh in case origin, scale, z changed
- self._mask.setDataItem(activeImage)
- self._updatePlotMask()
-
- def _setMaskedImage(self, image):
- """Change the image that is used a reference to author the mask"""
- previous = self.getMaskedItem()
- if previous is not None and self.isVisible():
- # Disconnect from previous image
- try:
- previous.sigItemChanged.disconnect(self.__imageChanged)
- except TypeError:
- pass # TODO fixme should not happen
-
- # Set the image
- self._mask.setDataItem(image)
-
- if image is None: # No image, disable mask
- self.setEnabled(False)
-
- self._data = numpy.zeros((0, 0), dtype=numpy.uint8)
- self._mask.reset()
- self._mask.commit()
-
- self._updateInteractiveMode()
-
- else: # Update and connect to image's sigItemChanged
- if self.isItemMaskUpdated():
- if image.getMaskData(copy=False) is None:
- # Image item has no mask: use current mask from the tool
- image.setMaskData(
- self.getSelectionMask(copy=False), copy=True)
- else: # Image item has a mask: set it in tool
- self.setSelectionMask(
- image.getMaskData(copy=False), copy=True)
- self._mask.resetHistory()
- self.__imageUpdated()
- if self.isVisible():
- image.sigItemChanged.connect(self.__imageChanged)
-
- def __imageChanged(self, event):
- """Reacts upon image item changes"""
- image = self._mask.getDataItem()
- if image is None:
- _logger.error("Mask is not attached to an image")
- return
-
- if event in (items.ItemChangedType.COLORMAP,
- items.ItemChangedType.DATA,
- items.ItemChangedType.POSITION,
- items.ItemChangedType.SCALE,
- items.ItemChangedType.VISIBLE,
- items.ItemChangedType.ZVALUE):
- self.__imageUpdated()
-
- elif (event == items.ItemChangedType.MASK and
- self.isItemMaskUpdated() and
- not self.__itemMaskUpdatedLock.locked()):
- # Update mask from the image item unless mask tool is updating it
- self.setSelectionMask(image.getMaskData(copy=False), copy=True)
-
- def __imageUpdated(self):
- """Synchronize mask with current state of the image"""
- image = self._mask.getDataItem()
- if image is None:
- _logger.error("No active image while expecting one")
- return
-
- self._setOverlayColorForImage(image)
-
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
-
- self._origin = image.getOrigin()
- self._scale = image.getScale()
- self._z = image.getZValue() + 1
- self._data = image.getData(copy=False)
- self._mask.setDataItem(image)
- if self._data.shape[:2] != self._mask.getMask(copy=False).shape:
- self._mask.reset(self._data.shape[:2])
- self._mask.commit()
- else:
- # Refresh in case origin, scale, z changed
- self._updatePlotMask()
-
- # Visible and with data
- self.setEnabled(image.isVisible() and self._data.size != 0)
-
- # Threshold tools only available for data with colormap
- self.thresholdGroup.setEnabled(self._data.ndim == 2)
-
- self._updateInteractiveMode()
-
- # Handle whole mask operations
- def load(self, filename):
- """Load a mask from an image file.
-
- :param str filename: File name from which to load the mask
- :raise Exception: An exception in case of failure
- :raise RuntimeWarning: In case the mask was applied but with some
- import changes to notice
- """
- _, extension = os.path.splitext(filename)
- extension = extension.lower()[1:]
-
- if extension == "npy":
- try:
- mask = numpy.load(filename)
- except IOError:
- _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)
- except Exception as e:
- _logger.error("Can't load filename %s", filename)
- _logger.debug("Backtrace", exc_info=True)
- raise e
- elif extension == "msk":
- try:
- mask = fabio.open(filename).data
- except Exception as e:
- _logger.error("Can't load fit2d mask file")
- _logger.debug("Backtrace", exc_info=True)
- raise e
- elif ("." + extension) in NEXUS_HDF5_EXT:
- mask = self._loadFromHdf5(filename)
- if mask is None:
- raise IOError("Could not load mask from HDF5 dataset")
- else:
- msg = "Extension '%s' is not supported."
- raise RuntimeError(msg % extension)
-
- effectiveMaskShape = self.setSelectionMask(mask, copy=False)
- if effectiveMaskShape is None:
- return
- if mask.shape != effectiveMaskShape:
- msg = 'Mask was resized from %s to %s'
- msg = msg % (str(mask.shape), str(effectiveMaskShape))
- raise RuntimeWarning(msg)
-
- def _loadMask(self):
- """Open load mask dialog"""
- dialog = qt.QFileDialog(self)
- dialog.setWindowTitle("Load Mask")
- dialog.setModal(1)
-
- extensions = collections.OrderedDict()
- extensions["EDF files"] = "*.edf"
- extensions["TIFF files"] = "*.tif *.tiff"
- extensions["NumPy binary files"] = "*.npy"
- extensions["HDF5 files"] = _HDF5_EXT_STR
- # 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)
- if not dialog.exec_():
- dialog.close()
- return
-
- filename = dialog.selectedFiles()[0]
- dialog.close()
-
- # Update the directory according to the user selection
- self.maskFileDir = os.path.dirname(filename)
-
- try:
- self.load(filename)
- except RuntimeWarning as e:
- message = e.args[0]
- msg = qt.QMessageBox(self)
- msg.setIcon(qt.QMessageBox.Warning)
- msg.setText("Mask loaded but an operation was applied.\n" + message)
- msg.exec_()
- except Exception as e:
- message = e.args[0]
- msg = qt.QMessageBox(self)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setText("Cannot load mask from file. " + message)
- msg.exec_()
-
- @staticmethod
- def _loadFromHdf5(filename):
- """Load a mask array from a HDF5 file.
-
- :param str filename: name of an existing HDF5 file
- :returns: A mask as a numpy array, or None if the interactive dialog
- was cancelled
- """
- dataPath = _selectDataset(filename, mode=DatasetDialog.LoadMode)
- if dataPath is None:
- return None
-
- with h5py.File(filename, "r") as h5f:
- dataset = h5f.get(dataPath)
- if not is_dataset(dataset):
- raise IOError("%s is not a dataset" % dataPath)
- mask = dataset[()]
- return mask
-
- def _saveMask(self):
- """Open Save mask dialog"""
- dialog = qt.QFileDialog(self)
- dialog.setWindowTitle("Save Mask")
- dialog.setOption(dialog.DontUseNativeDialog)
- dialog.setModal(1)
- hdf5Filter = 'HDF5 (%s)' % _HDF5_EXT_STR
- filters = [
- 'EDF (*.edf)',
- 'TIFF (*.tif)',
- 'NumPy binary file (*.npy)',
- hdf5Filter,
- # Fit2D mask is displayed anyway fabio is here or not
- # to show to the user that the option exists
- 'Fit2D mask (*.msk)',
- ]
- dialog.setNameFilters(filters)
- dialog.setFileMode(qt.QFileDialog.AnyFile)
- dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
- dialog.setDirectory(self.maskFileDir)
-
- def onFilterSelection(filt_):
- # disable overwrite confirmation for HDF5,
- # because we append the data to existing files
- if filt_ == hdf5Filter:
- dialog.setOption(dialog.DontConfirmOverwrite)
- else:
- dialog.setOption(dialog.DontConfirmOverwrite, False)
-
- dialog.filterSelected.connect(onFilterSelection)
- if not dialog.exec_():
- dialog.close()
- return
-
- nameFilter = dialog.selectedNameFilter()
- filename = dialog.selectedFiles()[0]
- dialog.close()
-
- if "HDF5" in nameFilter:
- has_allowed_ext = False
- for ext in NEXUS_HDF5_EXT:
- if (len(filename) > len(ext) and
- filename[-len(ext):].lower() == ext.lower()):
- has_allowed_ext = True
- extension = ext
- if not has_allowed_ext:
- extension = ".h5"
- filename += ".h5"
- else:
- # convert filter name to extension name with the .
- extension = nameFilter.split()[-1][2:-1]
- if not filename.lower().endswith(extension):
- filename += extension
-
- if os.path.exists(filename) and "HDF5" not in nameFilter:
- try:
- os.remove(filename)
- except IOError as e:
- msg = qt.QMessageBox(self)
- msg.setWindowTitle("Removing existing file")
- msg.setIcon(qt.QMessageBox.Critical)
-
- if hasattr(e, "strerror"):
- strerror = e.strerror
- else:
- strerror = sys.exc_info()[1]
- msg.setText("Cannot save.\n"
- "Input Output Error: %s" % strerror)
- msg.exec_()
- return
-
- # Update the directory according to the user selection
- self.maskFileDir = os.path.dirname(filename)
-
- try:
- self.save(filename, extension[1:])
- except Exception as e:
- msg = qt.QMessageBox(self)
- msg.setWindowTitle("Saving mask file")
- msg.setIcon(qt.QMessageBox.Critical)
-
- if hasattr(e, "strerror"):
- strerror = e.strerror
- else:
- strerror = sys.exc_info()[1]
- msg.setText("Cannot save file %s\n%s" % (filename, strerror))
- msg.exec_()
-
- def resetSelectionMask(self):
- """Reset the mask"""
- self._mask.reset(shape=self._data.shape[:2])
- self._mask.commit()
-
- def _plotDrawEvent(self, event):
- """Handle draw events from the plot"""
- if (self._drawingMode is None or
- event['event'] not in ('drawingProgress', 'drawingFinished')):
- return
-
- if not len(self._data):
- return
-
- level = self.levelSpinBox.value()
-
- if self._drawingMode == 'rectangle':
- if event['event'] == 'drawingFinished':
- # Convert from plot to array coords
- doMask = self._isMasking()
- ox, oy = self._origin
- sx, sy = self._scale
-
- height = int(abs(event['height'] / sy))
- width = int(abs(event['width'] / sx))
-
- row = int((event['y'] - oy) / sy)
- if sy < 0:
- row -= height
-
- col = int((event['x'] - ox) / sx)
- if sx < 0:
- col -= width
-
- self._mask.updateRectangle(
- level,
- row=row,
- col=col,
- height=height,
- width=width,
- mask=doMask)
- self._mask.commit()
-
- elif self._drawingMode == 'ellipse':
- if event['event'] == 'drawingFinished':
- doMask = self._isMasking()
- # Convert from plot to array coords
- center = (event['points'][0] - self._origin) / self._scale
- size = event['points'][1] / self._scale
- center = center.astype(numpy.int64) # (row, col)
- self._mask.updateEllipse(level, center[1], center[0], size[1], size[0], doMask)
- self._mask.commit()
-
- elif self._drawingMode == 'polygon':
- if event['event'] == 'drawingFinished':
- doMask = self._isMasking()
- # Convert from plot to array coords
- vertices = (event['points'] - self._origin) / self._scale
- vertices = vertices.astype(numpy.int64)[:, (1, 0)] # (row, col)
- self._mask.updatePolygon(level, vertices, doMask)
- self._mask.commit()
-
- elif self._drawingMode == 'pencil':
- doMask = self._isMasking()
- # convert from plot to array coords
- col, row = (event['points'][-1] - self._origin) / self._scale
- col, row = int(col), int(row)
- brushSize = self._getPencilWidth()
-
- if self._lastPencilPos != (row, col):
- if self._lastPencilPos is not None:
- # Draw the line
- self._mask.updateLine(
- level,
- self._lastPencilPos[0], self._lastPencilPos[1],
- row, col,
- brushSize,
- doMask)
-
- # Draw the very first, or last point
- self._mask.updateDisk(level, row, col, brushSize / 2., doMask)
-
- if event['event'] == 'drawingFinished':
- self._mask.commit()
- self._lastPencilPos = None
- else:
- self._lastPencilPos = row, col
- else:
- _logger.error("Drawing mode %s unsupported", self._drawingMode)
-
- def _loadRangeFromColormapTriggered(self):
- """Set range from active image colormap range"""
- activeImage = self.plot.getActiveImage()
- if (isinstance(activeImage, items.ColormapMixIn) and
- activeImage.getName() != self._maskName):
- # Update thresholds according to colormap
- colormap = activeImage.getColormap()
- if colormap['autoscale']:
- min_ = numpy.nanmin(activeImage.getData(copy=False))
- max_ = numpy.nanmax(activeImage.getData(copy=False))
- else:
- min_, max_ = colormap['vmin'], colormap['vmax']
- self.minLineEdit.setText(str(min_))
- self.maxLineEdit.setText(str(max_))
-
-
-class MaskToolsDockWidget(BaseMaskToolsDockWidget):
- """:class:`MaskToolsWidget` embedded in a QDockWidget.
-
- For integration in a :class:`PlotWindow`.
-
- :param parent: See :class:`QDockWidget`
- :param plot: The PlotWidget this widget is operating on
- :paran str name: The title of this widget
- """
-
- def __init__(self, parent=None, plot=None, name='Mask'):
- widget = MaskToolsWidget(plot=plot)
- super(MaskToolsDockWidget, self).__init__(parent, name, widget)
diff --git a/silx/gui/plot/PlotActions.py b/silx/gui/plot/PlotActions.py
deleted file mode 100644
index dd16221..0000000
--- a/silx/gui/plot/PlotActions.py
+++ /dev/null
@@ -1,67 +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.
-#
-# ###########################################################################*/
-"""Depracted module linking old PlotAction with the actions.xxx"""
-
-
-__author__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__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/PlotEvents.py b/silx/gui/plot/PlotEvents.py
deleted file mode 100644
index 83f253c..0000000
--- a/silx/gui/plot/PlotEvents.py
+++ /dev/null
@@ -1,166 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2016 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.
-#
-# ###########################################################################*/
-"""Functions to prepare events to be sent to Plot callback."""
-
-__author__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "18/02/2016"
-
-
-import numpy as np
-
-
-def prepareDrawingSignal(event, type_, points, parameters=None):
- """See Plot documentation for content of events"""
- assert event in ('drawingProgress', 'drawingFinished')
-
- if parameters is None:
- parameters = {}
-
- eventDict = {}
- eventDict['event'] = event
- eventDict['type'] = type_
- points = np.array(points, dtype=np.float32)
- points.shape = -1, 2
- eventDict['points'] = points
- eventDict['xdata'] = points[:, 0]
- eventDict['ydata'] = points[:, 1]
- if type_ in ('rectangle',):
- eventDict['x'] = eventDict['xdata'].min()
- eventDict['y'] = eventDict['ydata'].min()
- eventDict['width'] = eventDict['xdata'].max() - eventDict['x']
- eventDict['height'] = eventDict['ydata'].max() - eventDict['y']
- eventDict['parameters'] = parameters.copy()
- return eventDict
-
-
-def prepareMouseSignal(eventType, button, xData, yData, xPixel, yPixel):
- """See Plot documentation for content of events"""
- assert eventType in ('mouseMoved', 'mouseClicked', 'mouseDoubleClicked')
- assert button in (None, 'left', 'middle', 'right')
-
- return {'event': eventType,
- 'x': xData,
- 'y': yData,
- 'xpixel': xPixel,
- 'ypixel': yPixel,
- 'button': button}
-
-
-def prepareHoverSignal(label, type_, posData, posPixel, draggable, selectable):
- """See Plot documentation for content of events"""
- return {'event': 'hover',
- 'label': label,
- 'type': type_,
- 'x': posData[0],
- 'y': posData[1],
- 'xpixel': posPixel[0],
- 'ypixel': posPixel[1],
- 'draggable': draggable,
- 'selectable': selectable}
-
-
-def prepareMarkerSignal(eventType, button, label, type_,
- draggable, selectable,
- posDataMarker,
- posPixelCursor=None, posDataCursor=None):
- """See Plot documentation for content of events"""
- if eventType == 'markerClicked':
- assert posPixelCursor is not None
- assert posDataCursor is None
-
- posDataCursor = list(posDataMarker)
- if hasattr(posDataCursor[0], "__len__"):
- posDataCursor[0] = posDataCursor[0][-1]
- if hasattr(posDataCursor[1], "__len__"):
- posDataCursor[1] = posDataCursor[1][-1]
-
- elif eventType == 'markerMoving':
- assert posPixelCursor is not None
- assert posDataCursor is not None
-
- elif eventType == 'markerMoved':
- assert posPixelCursor is None
- assert posDataCursor is None
-
- posDataCursor = posDataMarker
- else:
- raise NotImplementedError("Unknown event type {0}".format(eventType))
-
- eventDict = {'event': eventType,
- 'button': button,
- 'label': label,
- 'type': type_,
- 'x': posDataCursor[0],
- 'y': posDataCursor[1],
- 'xdata': posDataMarker[0],
- 'ydata': posDataMarker[1],
- 'draggable': draggable,
- 'selectable': selectable}
-
- if eventType in ('markerMoving', 'markerClicked'):
- eventDict['xpixel'] = posPixelCursor[0]
- eventDict['ypixel'] = posPixelCursor[1]
-
- return eventDict
-
-
-def prepareImageSignal(button, label, type_, col, row,
- x, y, xPixel, yPixel):
- """See Plot documentation for content of events"""
- return {'event': 'imageClicked',
- 'button': button,
- 'label': label,
- 'type': type_,
- 'col': col,
- 'row': row,
- 'x': x,
- 'y': y,
- 'xpixel': xPixel,
- 'ypixel': yPixel}
-
-
-def prepareCurveSignal(button, label, type_, xData, yData,
- x, y, xPixel, yPixel):
- """See Plot documentation for content of events"""
- return {'event': 'curveClicked',
- 'button': button,
- 'label': label,
- 'type': type_,
- 'xdata': xData,
- 'ydata': yData,
- 'x': x,
- 'y': y,
- 'xpixel': xPixel,
- 'ypixel': yPixel}
-
-
-def prepareLimitsChangedSignal(sourceObj, xRange, yRange, y2Range):
- """See Plot documentation for content of events"""
- return {'event': 'limitsChanged',
- 'source': id(sourceObj),
- 'xdata': xRange,
- 'ydata': yRange,
- 'y2data': y2Range}
diff --git a/silx/gui/plot/PlotInteraction.py b/silx/gui/plot/PlotInteraction.py
deleted file mode 100644
index cfe140b..0000000
--- a/silx/gui/plot/PlotInteraction.py
+++ /dev/null
@@ -1,1748 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Implementation of the interaction for the :class:`Plot`."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "15/02/2019"
-
-
-import math
-import numpy
-import time
-import weakref
-
-from .. import colors
-from .. import qt
-from . import items
-from .Interaction import (ClickOrDrag, LEFT_BTN, RIGHT_BTN, MIDDLE_BTN,
- State, StateMachine)
-from .PlotEvents import (prepareCurveSignal, prepareDrawingSignal,
- prepareHoverSignal, prepareImageSignal,
- prepareMarkerSignal, prepareMouseSignal)
-
-from .backends.BackendBase import (CURSOR_POINTING, CURSOR_SIZE_HOR,
- CURSOR_SIZE_VER, CURSOR_SIZE_ALL)
-
-from ._utils import (FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX,
- applyZoomToPlot)
-
-
-# Base class ##################################################################
-
-class _PlotInteraction(object):
- """Base class for interaction handler.
-
- It provides a weakref to the plot and methods to set/reset overlay.
- """
- def __init__(self, plot):
- """Init.
-
- :param plot: The plot to apply modifications to.
- """
- self._needReplot = False
- self._selectionAreas = set()
- self._plot = weakref.ref(plot) # Avoid cyclic-ref
-
- @property
- def plot(self):
- plot = self._plot()
- assert plot is not None
- return plot
-
- def setSelectionArea(self, points, fill, color, name='', shape='polygon'):
- """Set a polygon selection area overlaid on the plot.
- Multiple simultaneous areas are supported through the name parameter.
-
- :param points: The 2D coordinates of the points of the polygon
- :type points: An iterable of (x, y) coordinates
- :param str fill: The fill mode: 'hatch', 'solid' or 'none'
- :param color: RGBA color to use or None to disable display
- :type color: list or tuple of 4 float in the range [0, 1]
- :param name: The key associated with this selection area
- :param str shape: Shape of the area in 'polygon', 'polylines'
- """
- assert shape in ('polygon', 'polylines')
-
- if color is None:
- return
-
- points = numpy.asarray(points)
-
- # TODO Not very nice, but as is for now
- legend = '__SELECTION_AREA__' + name
-
- fill = fill != 'none' # TODO not very nice either
-
- greyed = colors.greyed(color)[0]
- if greyed < 0.5:
- color2 = "white"
- else:
- color2 = "black"
-
- self.plot.addShape(points[:, 0], points[:, 1], legend=legend,
- replace=False,
- shape=shape, fill=fill,
- color=color, linebgcolor=color2, linestyle="--",
- overlay=True)
-
- self._selectionAreas.add(legend)
-
- def resetSelectionArea(self):
- """Remove all selection areas set by setSelectionArea."""
- for legend in self._selectionAreas:
- self.plot.remove(legend, kind='item')
- self._selectionAreas = set()
-
-
-# Zoom/Pan ####################################################################
-
-class _ZoomOnWheel(ClickOrDrag, _PlotInteraction):
- """:class:`ClickOrDrag` state machine with zooming on mouse wheel.
-
- Base class for :class:`Pan` and :class:`Zoom`
- """
-
- _DOUBLE_CLICK_TIMEOUT = 0.4
-
- class Idle(ClickOrDrag.Idle):
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.machine.plot, scaleF, (x, y))
-
- def click(self, x, y, btn):
- """Handle clicks by sending events
-
- :param int x: Mouse X position in pixels
- :param int y: Mouse Y position in pixels
- :param btn: Clicked mouse button
- """
- if btn == LEFT_BTN:
- lastClickTime, lastClickPos = self._lastClick
-
- # Signal mouse double clicked event first
- if (time.time() - lastClickTime) <= self._DOUBLE_CLICK_TIMEOUT:
- # Use position of first click
- eventDict = prepareMouseSignal('mouseDoubleClicked', 'left',
- *lastClickPos)
- self.plot.notify(**eventDict)
-
- self._lastClick = 0., None
- else:
- # Signal mouse clicked event
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
- eventDict = prepareMouseSignal('mouseClicked', 'left',
- dataPos[0], dataPos[1],
- x, y)
- self.plot.notify(**eventDict)
-
- self._lastClick = time.time(), (dataPos[0], dataPos[1], x, y)
-
- elif btn == RIGHT_BTN:
- # 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 __init__(self, plot, **kwargs):
- """Init.
-
- :param plot: The plot to apply modifications to.
- """
- self._lastClick = 0., None
-
- _PlotInteraction.__init__(self, plot)
- ClickOrDrag.__init__(self, **kwargs)
-
-
-# Pan #########################################################################
-
-class Pan(_ZoomOnWheel):
- """Pan plot content and zoom on wheel state machine."""
-
- def _pixelToData(self, x, y):
- xData, yData = self.plot.pixelToData(x, y)
- _, y2Data = self.plot.pixelToData(x, y, axis='right')
- return xData, yData, y2Data
-
- def beginDrag(self, x, y, btn):
- self._previousDataPos = self._pixelToData(x, y)
-
- def drag(self, x, y, btn):
- xData, yData, y2Data = self._pixelToData(x, y)
- lastX, lastY, lastY2 = self._previousDataPos
-
- xMin, xMax = self.plot.getXAxis().getLimits()
- yMin, yMax = self.plot.getYAxis().getLimits()
- y2Min, y2Max = self.plot.getYAxis(axis='right').getLimits()
-
- if self.plot.getXAxis()._isLogarithmic():
- try:
- dx = math.log10(xData) - math.log10(lastX)
- newXMin = pow(10., (math.log10(xMin) - dx))
- newXMax = pow(10., (math.log10(xMax) - dx))
- except (ValueError, OverflowError):
- newXMin, newXMax = xMin, xMax
-
- # Makes sure both values stays in positive float32 range
- if newXMin < FLOAT32_MINPOS or newXMax > FLOAT32_SAFE_MAX:
- newXMin, newXMax = xMin, xMax
- else:
- dx = xData - lastX
- newXMin, newXMax = xMin - dx, xMax - dx
-
- # Makes sure both values stays in float32 range
- if newXMin < FLOAT32_SAFE_MIN or newXMax > FLOAT32_SAFE_MAX:
- newXMin, newXMax = xMin, xMax
-
- if self.plot.getYAxis()._isLogarithmic():
- try:
- dy = math.log10(yData) - math.log10(lastY)
- newYMin = pow(10., math.log10(yMin) - dy)
- newYMax = pow(10., math.log10(yMax) - dy)
-
- dy2 = math.log10(y2Data) - math.log10(lastY2)
- newY2Min = pow(10., math.log10(y2Min) - dy2)
- newY2Max = pow(10., math.log10(y2Max) - dy2)
- except (ValueError, OverflowError):
- newYMin, newYMax = yMin, yMax
- newY2Min, newY2Max = y2Min, y2Max
-
- # Makes sure y and y2 stays in positive float32 range
- if (newYMin < FLOAT32_MINPOS or newYMax > FLOAT32_SAFE_MAX or
- newY2Min < FLOAT32_MINPOS or newY2Max > FLOAT32_SAFE_MAX):
- newYMin, newYMax = yMin, yMax
- newY2Min, newY2Max = y2Min, y2Max
- else:
- dy = yData - lastY
- dy2 = y2Data - lastY2
- newYMin, newYMax = yMin - dy, yMax - dy
- newY2Min, newY2Max = y2Min - dy2, y2Max - dy2
-
- # Makes sure y and y2 stays in float32 range
- if (newYMin < FLOAT32_SAFE_MIN or
- newYMax > FLOAT32_SAFE_MAX or
- newY2Min < FLOAT32_SAFE_MIN or
- newY2Max > FLOAT32_SAFE_MAX):
- newYMin, newYMax = yMin, yMax
- newY2Min, newY2Max = y2Min, y2Max
-
- self.plot.setLimits(newXMin, newXMax,
- newYMin, newYMax,
- newY2Min, newY2Max)
-
- self._previousDataPos = self._pixelToData(x, y)
-
- def endDrag(self, startPos, endPos, btn):
- del self._previousDataPos
-
- def cancel(self):
- pass
-
-
-# Zoom ########################################################################
-
-class Zoom(_ZoomOnWheel):
- """Zoom-in/out state machine.
-
- Zoom-in on selected area, zoom-out on right click,
- and zoom on mouse wheel.
- """
-
- SURFACE_THRESHOLD = 5
-
- def __init__(self, plot, color):
- self.color = color
-
- super(Zoom, self).__init__(plot)
- self.plot.getLimitsHistory().clear()
-
- def _areaWithAspectRatio(self, x0, y0, x1, y1):
- _plotLeft, _plotTop, plotW, plotH = self.plot.getPlotBoundsInPixels()
-
- areaX0, areaY0, areaX1, areaY1 = x0, y0, x1, y1
-
- if plotH != 0.:
- plotRatio = plotW / float(plotH)
- width, height = math.fabs(x1 - x0), math.fabs(y1 - y0)
-
- if height != 0. and width != 0.:
- if width / height > plotRatio:
- areaHeight = width / plotRatio
- areaX0, areaX1 = x0, x1
- center = 0.5 * (y0 + y1)
- areaY0 = center - numpy.sign(y1 - y0) * 0.5 * areaHeight
- areaY1 = center + numpy.sign(y1 - y0) * 0.5 * areaHeight
- else:
- areaWidth = height * plotRatio
- areaY0, areaY1 = y0, y1
- center = 0.5 * (x0 + x1)
- areaX0 = center - numpy.sign(x1 - x0) * 0.5 * areaWidth
- areaX1 = center + numpy.sign(x1 - x0) * 0.5 * areaWidth
-
- return areaX0, areaY0, areaX1, areaY1
-
- def beginDrag(self, x, y, btn):
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
- self.x0, self.y0 = x, y
-
- def drag(self, x1, y1, btn):
- if self.color is None:
- return # Do not draw zoom area
-
- dataPos = self.plot.pixelToData(x1, y1)
- assert dataPos is not None
-
- if self.plot.isKeepDataAspectRatio():
- area = self._areaWithAspectRatio(self.x0, self.y0, x1, y1)
- areaX0, areaY0, areaX1, areaY1 = area
- areaPoints = ((areaX0, areaY0),
- (areaX1, areaY0),
- (areaX1, areaY1),
- (areaX0, areaY1))
- areaPoints = numpy.array([self.plot.pixelToData(
- x, y, check=False) for (x, y) in areaPoints])
-
- if self.color != 'video inverted':
- areaColor = list(self.color)
- areaColor[3] *= 0.25
- else:
- areaColor = [1., 1., 1., 1.]
-
- self.setSelectionArea(areaPoints,
- fill='none',
- color=areaColor,
- name="zoomedArea")
-
- corners = ((self.x0, self.y0),
- (self.x0, y1),
- (x1, y1),
- (x1, self.y0))
- corners = numpy.array([self.plot.pixelToData(x, y, check=False)
- for (x, y) in corners])
-
- self.setSelectionArea(corners, fill='none', color=self.color)
-
- def _zoom(self, x0, y0, x1, y1):
- """Zoom to the rectangle view x0,y0 x1,y1.
- """
- startPos = x0, y0
- endPos = x1, y1
-
- # Store current zoom state in stack
- self.plot.getLimitsHistory().push()
-
- if self.plot.isKeepDataAspectRatio():
- x0, y0, x1, y1 = self._areaWithAspectRatio(x0, y0, x1, y1)
-
- # Convert to data space and set limits
- x0, y0 = self.plot.pixelToData(x0, y0, check=False)
-
- dataPos = self.plot.pixelToData(
- startPos[0], startPos[1], axis="right", check=False)
- y2_0 = dataPos[1]
-
- x1, y1 = self.plot.pixelToData(x1, y1, check=False)
-
- dataPos = self.plot.pixelToData(
- endPos[0], endPos[1], axis="right", check=False)
- y2_1 = dataPos[1]
-
- xMin, xMax = min(x0, x1), max(x0, x1)
- yMin, yMax = min(y0, y1), max(y0, y1)
- y2Min, y2Max = min(y2_0, y2_1), max(y2_0, y2_1)
-
- self.plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
-
- def endDrag(self, startPos, endPos, btn):
- x0, y0 = startPos
- x1, y1 = endPos
-
- if abs(x0 - x1) * abs(y0 - y1) >= self.SURFACE_THRESHOLD:
- # Avoid empty zoom area
- self._zoom(x0, y0, x1, y1)
-
- self.resetSelectionArea()
-
- def cancel(self):
- if isinstance(self.state, self.states['drag']):
- self.resetSelectionArea()
-
-
-# Select ######################################################################
-
-class Select(StateMachine, _PlotInteraction):
- """Base class for drawing selection areas."""
-
- def __init__(self, plot, parameters, states, state):
- """Init a state machine.
-
- :param plot: The plot to apply changes to.
- :param dict parameters: A dict of parameters such as color.
- :param dict states: The states of the state machine.
- :param str state: The name of the initial state.
- """
- _PlotInteraction.__init__(self, plot)
- self.parameters = parameters
- StateMachine.__init__(self, states, state)
-
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.plot, scaleF, (x, y))
-
- @property
- def color(self):
- return self.parameters.get('color', None)
-
-
-class SelectPolygon(Select):
- """Drawing selection polygon area state machine."""
-
- DRAG_THRESHOLD_DIST = 4
-
- class Idle(State):
- def onPress(self, x, y, btn):
- if btn == LEFT_BTN:
- self.goto('select', x, y)
- return True
-
- class Select(State):
- def enterState(self, x, y):
- dataPos = self.machine.plot.pixelToData(x, y)
- assert dataPos is not None
- self._firstPos = dataPos
- self.points = [dataPos, dataPos]
-
- self.updateFirstPoint()
-
- def updateFirstPoint(self):
- """Update drawing first point, using self._firstPos"""
- x, y = self.machine.plot.dataToPixel(*self._firstPos, check=False)
-
- offset = self.machine.getDragThreshold()
- points = [(x - offset, y - offset),
- (x - offset, y + offset),
- (x + offset, y + offset),
- (x + offset, y - offset)]
- points = [self.machine.plot.pixelToData(xpix, ypix, check=False)
- for xpix, ypix in points]
- self.machine.setSelectionArea(points, fill=None,
- color=self.machine.color,
- name='first_point')
-
- def updateSelectionArea(self):
- """Update drawing selection area using self.points"""
- self.machine.setSelectionArea(self.points,
- fill='hatch',
- color=self.machine.color)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'polygon',
- self.points,
- self.machine.parameters)
- self.machine.plot.notify(**eventDict)
-
- def validate(self):
- if len(self.points) > 2:
- self.closePolygon()
- else:
- # It would be nice to have a cancel event.
- # The plot is not aware that the interaction was cancelled
- self.machine.cancel()
-
- def closePolygon(self):
- self.machine.resetSelectionArea()
- self.points[-1] = self.points[0]
- eventDict = prepareDrawingSignal('drawingFinished',
- 'polygon',
- self.points,
- self.machine.parameters)
- self.machine.plot.notify(**eventDict)
- self.goto('idle')
-
- def onWheel(self, x, y, angle):
- self.machine.onWheel(x, y, angle)
- self.updateFirstPoint()
-
- def onRelease(self, x, y, btn):
- if btn == LEFT_BTN:
- # checking if the position is close to the first point
- # if yes : closing the "loop"
- firstPos = self.machine.plot.dataToPixel(*self._firstPos,
- check=False)
- dx, dy = abs(firstPos[0] - x), abs(firstPos[1] - y)
-
- threshold = self.machine.getDragThreshold()
-
- # Only allow to close polygon after first point
- if len(self.points) > 2 and dx <= threshold and dy <= threshold:
- self.closePolygon()
- return False
-
- # Update polygon last point not too close to previous one
- dataPos = self.machine.plot.pixelToData(x, y)
- assert dataPos is not None
- self.updateSelectionArea()
-
- # checking that the new points isnt the same (within range)
- # of the previous one
- # This has to be done because sometimes the mouse release event
- # is caught right after entering the Select state (i.e : press
- # in Idle state, but with a slightly different position that
- # the mouse press. So we had the two first vertices that were
- # almost identical.
- previousPos = self.machine.plot.dataToPixel(*self.points[-2],
- check=False)
- dx, dy = abs(previousPos[0] - x), abs(previousPos[1] - y)
- if dx >= threshold or dy >= threshold:
- self.points.append(dataPos)
- else:
- self.points[-1] = dataPos
-
- return True
- return False
-
- def onMove(self, x, y):
- firstPos = self.machine.plot.dataToPixel(*self._firstPos,
- check=False)
- dx, dy = abs(firstPos[0] - x), abs(firstPos[1] - y)
- threshold = self.machine.getDragThreshold()
-
- if dx <= threshold and dy <= threshold:
- x, y = firstPos # Snap to first point
-
- dataPos = self.machine.plot.pixelToData(x, y)
- assert dataPos is not None
- self.points[-1] = dataPos
- self.updateSelectionArea()
-
- def __init__(self, plot, parameters):
- states = {
- 'idle': SelectPolygon.Idle,
- 'select': SelectPolygon.Select
- }
- super(SelectPolygon, self).__init__(plot, parameters,
- states, 'idle')
-
- def cancel(self):
- if isinstance(self.state, self.states['select']):
- self.resetSelectionArea()
-
- def getDragThreshold(self):
- """Return dragging ratio with device to pixel ratio applied.
-
- :rtype: float
- """
- ratio = 1.
- if qt.BINDING in ('PyQt5', 'PySide2'):
- ratio = self.plot.window().windowHandle().devicePixelRatio()
- return self.DRAG_THRESHOLD_DIST * ratio
-
-
-class Select2Points(Select):
- """Base class for drawing selection based on 2 input points."""
- class Idle(State):
- def onPress(self, x, y, btn):
- if btn == LEFT_BTN:
- self.goto('start', x, y)
- return True
-
- class Start(State):
- def enterState(self, x, y):
- self.machine.beginSelect(x, y)
-
- def onMove(self, x, y):
- self.goto('select', x, y)
-
- def onRelease(self, x, y, btn):
- if btn == LEFT_BTN:
- self.goto('select', x, y)
- return True
-
- class Select(State):
- def enterState(self, x, y):
- self.onMove(x, y)
-
- def onMove(self, x, y):
- self.machine.select(x, y)
-
- def onRelease(self, x, y, btn):
- if btn == LEFT_BTN:
- self.machine.endSelect(x, y)
- self.goto('idle')
-
- def __init__(self, plot, parameters):
- states = {
- 'idle': Select2Points.Idle,
- 'start': Select2Points.Start,
- 'select': Select2Points.Select
- }
- super(Select2Points, self).__init__(plot, parameters,
- states, 'idle')
-
- def beginSelect(self, x, y):
- pass
-
- def select(self, x, y):
- pass
-
- def endSelect(self, x, y):
- pass
-
- def cancelSelect(self):
- pass
-
- def cancel(self):
- if isinstance(self.state, self.states['select']):
- self.cancelSelect()
-
-
-class SelectEllipse(Select2Points):
- """Drawing ellipse selection area state machine."""
- def beginSelect(self, x, y):
- self.center = self.plot.pixelToData(x, y)
- assert self.center is not None
-
- def _getEllipseSize(self, pointInEllipse):
- """
- Returns the size from the center to the bounding box of the ellipse.
-
- :param Tuple[float,float] pointInEllipse: A point of the ellipse
- :rtype: Tuple[float,float]
- """
- x = abs(self.center[0] - pointInEllipse[0])
- y = abs(self.center[1] - pointInEllipse[1])
- if x == 0 or y == 0:
- return x, y
- # Ellipse definitions
- # e: eccentricity
- # a: length fron center to bounding box width
- # b: length fron center to bounding box height
- # Equations
- # (1) b < a
- # (2) For x,y a point in the ellipse: x^2/a^2 + y^2/b^2 = 1
- # (3) b = a * sqrt(1-e^2)
- # (4) e = sqrt(a^2 - b^2) / a
-
- # The eccentricity of the ellipse defined by a,b=x,y is the same
- # as the one we are searching for.
- swap = x < y
- if swap:
- x, y = y, x
- e = math.sqrt(x**2 - y**2) / x
- # From (2) using (3) to replace b
- # a^2 = x^2 + y^2 / (1-e^2)
- a = math.sqrt(x**2 + y**2 / (1.0 - e**2))
- b = a * math.sqrt(1 - e**2)
- if swap:
- a, b = b, a
- return a, b
-
- def select(self, x, y):
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
- width, height = self._getEllipseSize(dataPos)
-
- # Circle used for circle preview
- nbpoints = 27.
- angles = numpy.arange(nbpoints) * numpy.pi * 2.0 / nbpoints
- circleShape = numpy.array((numpy.cos(angles) * width,
- numpy.sin(angles) * height)).T
- circleShape += numpy.array(self.center)
-
- self.setSelectionArea(circleShape,
- shape="polygon",
- fill='hatch',
- color=self.color)
-
- eventDict = prepareDrawingSignal('drawingProgress',
- 'ellipse',
- (self.center, (width, height)),
- self.parameters)
- self.plot.notify(**eventDict)
-
- def endSelect(self, x, y):
- self.resetSelectionArea()
-
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
- width, height = self._getEllipseSize(dataPos)
-
- eventDict = prepareDrawingSignal('drawingFinished',
- 'ellipse',
- (self.center, (width, height)),
- self.parameters)
- self.plot.notify(**eventDict)
-
- def cancelSelect(self):
- self.resetSelectionArea()
-
-
-class SelectRectangle(Select2Points):
- """Drawing rectangle selection area state machine."""
- def beginSelect(self, x, y):
- self.startPt = self.plot.pixelToData(x, y)
- assert self.startPt is not None
-
- def select(self, x, y):
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
-
- self.setSelectionArea((self.startPt,
- (self.startPt[0], dataPos[1]),
- dataPos,
- (dataPos[0], self.startPt[1])),
- fill='hatch',
- color=self.color)
-
- eventDict = prepareDrawingSignal('drawingProgress',
- 'rectangle',
- (self.startPt, dataPos),
- self.parameters)
- self.plot.notify(**eventDict)
-
- def endSelect(self, x, y):
- self.resetSelectionArea()
-
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
-
- eventDict = prepareDrawingSignal('drawingFinished',
- 'rectangle',
- (self.startPt, dataPos),
- self.parameters)
- self.plot.notify(**eventDict)
-
- def cancelSelect(self):
- self.resetSelectionArea()
-
-
-class SelectLine(Select2Points):
- """Drawing line selection area state machine."""
- def beginSelect(self, x, y):
- self.startPt = self.plot.pixelToData(x, y)
- assert self.startPt is not None
-
- def select(self, x, y):
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
-
- self.setSelectionArea((self.startPt, dataPos),
- fill='hatch',
- color=self.color)
-
- eventDict = prepareDrawingSignal('drawingProgress',
- 'line',
- (self.startPt, dataPos),
- self.parameters)
- self.plot.notify(**eventDict)
-
- def endSelect(self, x, y):
- self.resetSelectionArea()
-
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
-
- eventDict = prepareDrawingSignal('drawingFinished',
- 'line',
- (self.startPt, dataPos),
- self.parameters)
- self.plot.notify(**eventDict)
-
- def cancelSelect(self):
- self.resetSelectionArea()
-
-
-class Select1Point(Select):
- """Base class for drawing selection area based on one input point."""
- class Idle(State):
- def onPress(self, x, y, btn):
- if btn == LEFT_BTN:
- self.goto('select', x, y)
- return True
-
- class Select(State):
- def enterState(self, x, y):
- self.onMove(x, y)
-
- def onMove(self, x, y):
- self.machine.select(x, y)
-
- def onRelease(self, x, y, btn):
- if btn == LEFT_BTN:
- self.machine.endSelect(x, y)
- self.goto('idle')
-
- def onWheel(self, x, y, angle):
- self.machine.onWheel(x, y, angle) # Call select default wheel
- self.machine.select(x, y)
-
- def __init__(self, plot, parameters):
- states = {
- 'idle': Select1Point.Idle,
- 'select': Select1Point.Select
- }
- super(Select1Point, self).__init__(plot, parameters, states, 'idle')
-
- def select(self, x, y):
- pass
-
- def endSelect(self, x, y):
- pass
-
- def cancelSelect(self):
- pass
-
- def cancel(self):
- if isinstance(self.state, self.states['select']):
- self.cancelSelect()
-
-
-class SelectHLine(Select1Point):
- """Drawing a horizontal line selection area state machine."""
- def _hLine(self, y):
- """Return points in data coords of the segment visible in the plot.
-
- Supports non-orthogonal axes.
- """
- left, _top, width, _height = self.plot.getPlotBoundsInPixels()
-
- dataPos1 = self.plot.pixelToData(left, y, check=False)
- dataPos2 = self.plot.pixelToData(left + width, y, check=False)
- return dataPos1, dataPos2
-
- def select(self, x, y):
- points = self._hLine(y)
- self.setSelectionArea(points, fill='hatch', color=self.color)
-
- eventDict = prepareDrawingSignal('drawingProgress',
- 'hline',
- points,
- self.parameters)
- self.plot.notify(**eventDict)
-
- def endSelect(self, x, y):
- self.resetSelectionArea()
-
- eventDict = prepareDrawingSignal('drawingFinished',
- 'hline',
- self._hLine(y),
- self.parameters)
- self.plot.notify(**eventDict)
-
- def cancelSelect(self):
- self.resetSelectionArea()
-
-
-class SelectVLine(Select1Point):
- """Drawing a vertical line selection area state machine."""
- def _vLine(self, x):
- """Return points in data coords of the segment visible in the plot.
-
- Supports non-orthogonal axes.
- """
- _left, top, _width, height = self.plot.getPlotBoundsInPixels()
-
- dataPos1 = self.plot.pixelToData(x, top, check=False)
- dataPos2 = self.plot.pixelToData(x, top + height, check=False)
- return dataPos1, dataPos2
-
- def select(self, x, y):
- points = self._vLine(x)
- self.setSelectionArea(points, fill='hatch', color=self.color)
-
- eventDict = prepareDrawingSignal('drawingProgress',
- 'vline',
- points,
- self.parameters)
- self.plot.notify(**eventDict)
-
- def endSelect(self, x, y):
- self.resetSelectionArea()
-
- eventDict = prepareDrawingSignal('drawingFinished',
- 'vline',
- self._vLine(x),
- self.parameters)
- self.plot.notify(**eventDict)
-
- def cancelSelect(self):
- self.resetSelectionArea()
-
-
-class DrawFreeHand(Select):
- """Interaction for drawing pencil. It display the preview of the pencil
- before pressing the mouse.
- """
-
- class Idle(State):
- def onPress(self, x, y, btn):
- if btn == LEFT_BTN:
- self.goto('select', x, y)
- return True
-
- def onMove(self, x, y):
- self.machine.updatePencilShape(x, y)
-
- def onLeave(self):
- self.machine.cancel()
-
- class Select(State):
- def enterState(self, x, y):
- self.__isOut = False
- self.machine.setFirstPoint(x, y)
-
- def onMove(self, x, y):
- self.machine.updatePencilShape(x, y)
- self.machine.select(x, y)
-
- def onRelease(self, x, y, btn):
- if btn == LEFT_BTN:
- if self.__isOut:
- self.machine.resetSelectionArea()
- self.machine.endSelect(x, y)
- self.goto('idle')
-
- def onEnter(self):
- self.__isOut = False
-
- def onLeave(self):
- self.__isOut = True
-
- def __init__(self, plot, parameters):
- # Circle used for pencil preview
- angle = numpy.arange(13.) * numpy.pi * 2.0 / 13.
- size = parameters.get('width', 1.) * 0.5
- self._circle = size * numpy.array((numpy.cos(angle),
- numpy.sin(angle))).T
-
- states = {
- 'idle': DrawFreeHand.Idle,
- 'select': DrawFreeHand.Select
- }
- super(DrawFreeHand, self).__init__(plot, parameters, states, 'idle')
-
- @property
- def width(self):
- return self.parameters.get('width', None)
-
- def setFirstPoint(self, x, y):
- self._points = []
- self.select(x, y)
-
- def updatePencilShape(self, x, y):
- center = self.plot.pixelToData(x, y, check=False)
- assert center is not None
-
- polygon = center + self._circle
-
- self.setSelectionArea(polygon, fill='none', color=self.color)
-
- def select(self, x, y):
- pos = self.plot.pixelToData(x, y, check=False)
- if len(self._points) > 0:
- if self._points[-1] == pos:
- # Skip same points
- return
- self._points.append(pos)
- eventDict = prepareDrawingSignal('drawingProgress',
- 'polylines',
- self._points,
- self.parameters)
- self.plot.notify(**eventDict)
-
- def endSelect(self, x, y):
- pos = self.plot.pixelToData(x, y, check=False)
- if len(self._points) > 0:
- if self._points[-1] != pos:
- # Append if different
- self._points.append(pos)
-
- eventDict = prepareDrawingSignal('drawingFinished',
- 'polylines',
- self._points,
- self.parameters)
- self.plot.notify(**eventDict)
- self._points = None
-
- def cancelSelect(self):
- self.resetSelectionArea()
-
- def cancel(self):
- self.resetSelectionArea()
-
-
-class SelectFreeLine(ClickOrDrag, _PlotInteraction):
- """Base class for drawing free lines with tools such as pencil."""
-
- def __init__(self, plot, parameters):
- """Init a state machine.
-
- :param plot: The plot to apply changes to.
- :param dict parameters: A dict of parameters such as color.
- """
- # self.DRAG_THRESHOLD_SQUARE_DIST = 1 # Disable first move threshold
- self._points = []
- ClickOrDrag.__init__(self)
- _PlotInteraction.__init__(self, plot)
- self.parameters = parameters
-
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.plot, scaleF, (x, y))
-
- @property
- def color(self):
- return self.parameters.get('color', None)
-
- def click(self, x, y, btn):
- if btn == LEFT_BTN:
- self._processEvent(x, y, isLast=True)
-
- def beginDrag(self, x, y, btn):
- self._processEvent(x, y, isLast=False)
-
- def drag(self, x, y, btn):
- self._processEvent(x, y, isLast=False)
-
- def endDrag(self, startPos, endPos, btn):
- x, y = endPos
- self._processEvent(x, y, isLast=True)
-
- def cancel(self):
- self.resetSelectionArea()
- self._points = []
-
- def _processEvent(self, x, y, isLast):
- dataPos = self.plot.pixelToData(x, y, check=False)
- isNewPoint = not self._points or dataPos != self._points[-1]
-
- if isNewPoint:
- self._points.append(dataPos)
-
- if isNewPoint or isLast:
- eventDict = prepareDrawingSignal(
- 'drawingFinished' if isLast else 'drawingProgress',
- 'polylines',
- self._points,
- self.parameters)
- self.plot.notify(**eventDict)
-
- if not isLast:
- self.setSelectionArea(self._points, fill='none', color=self.color,
- shape='polylines')
- else:
- self.cancel()
-
-
-# ItemInteraction #############################################################
-
-class ItemsInteraction(ClickOrDrag, _PlotInteraction):
- """Interaction with items (markers, curves and images).
-
- This class provides selection and dragging of plot primitives
- that support those interaction.
- It is also meant to be combined with the zoom interaction.
- """
-
- class Idle(ClickOrDrag.Idle):
- def __init__(self, *args, **kw):
- super(ItemsInteraction.Idle, self).__init__(*args, **kw)
- self._hoverMarker = None
-
- def onWheel(self, x, y, angle):
- scaleF = 1.1 if angle > 0 else 1. / 1.1
- applyZoomToPlot(self.machine.plot, scaleF, (x, y))
-
- def onMove(self, x, y):
- marker = self.machine.plot._getMarkerAt(x, y)
-
- if marker is not None:
- dataPos = self.machine.plot.pixelToData(x, y)
- assert dataPos is not None
- eventDict = prepareHoverSignal(
- marker.getName(), 'marker',
- dataPos, (x, y),
- marker.isDraggable(),
- marker.isSelectable())
- self.machine.plot.notify(**eventDict)
-
- if marker != self._hoverMarker:
- self._hoverMarker = marker
-
- if marker is None:
- self.machine.plot.setGraphCursorShape()
-
- elif marker.isDraggable():
- if isinstance(marker, items.YMarker):
- self.machine.plot.setGraphCursorShape(CURSOR_SIZE_VER)
- elif isinstance(marker, items.XMarker):
- self.machine.plot.setGraphCursorShape(CURSOR_SIZE_HOR)
- else:
- self.machine.plot.setGraphCursorShape(CURSOR_SIZE_ALL)
-
- elif marker.isSelectable():
- self.machine.plot.setGraphCursorShape(CURSOR_POINTING)
- else:
- self.machine.plot.setGraphCursorShape()
-
- return True
-
- def __init__(self, plot):
- self._pan = Pan(plot)
-
- _PlotInteraction.__init__(self, plot)
- ClickOrDrag.__init__(self,
- clickButtons=(LEFT_BTN, RIGHT_BTN),
- dragButtons=(LEFT_BTN, MIDDLE_BTN))
-
- def click(self, x, y, btn):
- """Handle mouse click
-
- :param x: X position of the mouse in pixels
- :param y: Y position of the mouse in pixels
- :param btn: Pressed button id
- :return: True if click is catched by an item, False otherwise
- """
- # Signal mouse clicked event
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
- eventDict = prepareMouseSignal('mouseClicked', btn,
- dataPos[0], dataPos[1],
- x, y)
- self.plot.notify(**eventDict)
-
- eventDict = self._handleClick(x, y, btn)
- if eventDict is not None:
- self.plot.notify(**eventDict)
-
- def _handleClick(self, x, y, btn):
- """Perform picking and prepare event if click is handled here
-
- :param x: X position of the mouse in pixels
- :param y: Y position of the mouse in pixels
- :param btn: Pressed button id
- :return: event description to send of None if not handling event.
- :rtype: dict or None
- """
-
- if btn == LEFT_BTN:
- result = self.plot._pickTopMost(x, y, lambda i: i.isSelectable())
- if result is None:
- return None
-
- item = result.getItem()
-
- if isinstance(item, items.MarkerBase):
- xData, yData = item.getPosition()
- if xData is None:
- xData = [0, 1]
- if yData is None:
- yData = [0, 1]
-
- eventDict = prepareMarkerSignal('markerClicked',
- 'left',
- item.getName(),
- 'marker',
- item.isDraggable(),
- item.isSelectable(),
- (xData, yData),
- (x, y), None)
- return eventDict
-
- elif isinstance(item, items.Curve):
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
-
- xData = item.getXData(copy=False)
- yData = item.getYData(copy=False)
-
- indices = result.getIndices(copy=False)
- eventDict = prepareCurveSignal('left',
- item.getName(),
- 'curve',
- xData[indices],
- yData[indices],
- dataPos[0], dataPos[1],
- x, y)
- return eventDict
-
- elif isinstance(item, items.ImageBase):
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
-
- indices = result.getIndices(copy=False)
- row, column = indices[0][0], indices[1][0]
- eventDict = prepareImageSignal('left',
- item.getName(),
- 'image',
- column, row,
- dataPos[0], dataPos[1],
- x, y)
- return eventDict
-
- return None
-
- def _signalMarkerMovingEvent(self, eventType, marker, x, y):
- assert marker is not None
-
- xData, yData = marker.getPosition()
- if xData is None:
- xData = [0, 1]
- if yData is None:
- yData = [0, 1]
-
- posDataCursor = self.plot.pixelToData(x, y)
- assert posDataCursor is not None
-
- eventDict = prepareMarkerSignal(eventType,
- 'left',
- marker.getName(),
- 'marker',
- marker.isDraggable(),
- marker.isSelectable(),
- (xData, yData),
- (x, y),
- posDataCursor)
- self.plot.notify(**eventDict)
-
- @staticmethod
- def __isDraggableItem(item):
- return isinstance(item, items.DraggableMixIn) and item.isDraggable()
-
- def __terminateDrag(self):
- """Finalize a drag operation by reseting to initial state"""
- self.plot.setGraphCursorShape()
- self.draggedItemRef = None
-
- def beginDrag(self, x, y, btn):
- """Handle begining of drag interaction
-
- :param x: X position of the mouse in pixels
- :param y: Y position of the mouse in pixels
- :param str btn: The mouse button for which a drag is starting.
- :return: True if drag is catched by an item, False otherwise
- """
- if btn == LEFT_BTN:
- self._lastPos = self.plot.pixelToData(x, y)
- assert self._lastPos is not None
-
- result = self.plot._pickTopMost(x, y, self.__isDraggableItem)
- item = result.getItem() if result is not None else None
-
- self.draggedItemRef = None if item is None else weakref.ref(item)
-
- if item is None:
- self.__terminateDrag()
- return False
-
- if isinstance(item, items.MarkerBase):
- self._signalMarkerMovingEvent('markerMoving', item, x, y)
- item._startDrag()
-
- return True
- elif btn == MIDDLE_BTN:
- self._pan.beginDrag(x, y, btn)
- return True
-
- def drag(self, x, y, btn):
- if btn == LEFT_BTN:
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
-
- item = None if self.draggedItemRef is None else self.draggedItemRef()
- if item is not None:
- item.drag(self._lastPos, dataPos)
-
- if isinstance(item, items.MarkerBase):
- self._signalMarkerMovingEvent('markerMoving', item, x, y)
-
- self._lastPos = dataPos
- elif btn == MIDDLE_BTN:
- self._pan.drag(x, y, btn)
-
- def endDrag(self, startPos, endPos, btn):
- if btn == LEFT_BTN:
- item = None if self.draggedItemRef is None else self.draggedItemRef()
- if isinstance(item, items.MarkerBase):
- posData = list(item.getPosition())
- if posData[0] is None:
- posData[0] = 1.
- if posData[1] is None:
- posData[1] = 1.
-
- eventDict = prepareMarkerSignal(
- 'markerMoved',
- 'left',
- item.getLegend(),
- 'marker',
- item.isDraggable(),
- item.isSelectable(),
- posData)
- self.plot.notify(**eventDict)
- item._endDrag()
-
- self.__terminateDrag()
- elif btn == MIDDLE_BTN:
- self._pan.endDrag(startPos, endPos, btn)
-
- def cancel(self):
- self._pan.cancel()
- self.__terminateDrag()
-
-
-class ItemsInteractionForCombo(ItemsInteraction):
- """Interaction with items to combine through :class:`FocusManager`.
- """
-
- class Idle(ItemsInteraction.Idle):
- @staticmethod
- def __isItemSelectableOrDraggable(item):
- return (item.isSelectable() or (
- isinstance(item, items.DraggableMixIn) and item.isDraggable()))
-
- def onPress(self, x, y, btn):
- if btn == LEFT_BTN:
- result = self.machine.plot._pickTopMost(
- x, y, self.__isItemSelectableOrDraggable)
- if result is not None: # Request focus and handle interaction
- self.goto('clickOrDrag', x, y, btn)
- return True
- else: # Do not request focus
- return False
- else:
- return super().onPress(x, y, btn)
-
-
-# FocusManager ################################################################
-
-class FocusManager(StateMachine):
- """Manages focus across multiple event handlers
-
- On press an event handler can acquire focus.
- By default it looses focus when all buttons are released.
- """
- class Idle(State):
- def onPress(self, x, y, btn):
- if btn == LEFT_BTN:
- for eventHandler in self.machine.eventHandlers:
- requestFocus = eventHandler.handleEvent('press', x, y, btn)
- if requestFocus:
- self.goto('focus', eventHandler, btn)
- break
-
- def _processEvent(self, *args):
- for eventHandler in self.machine.eventHandlers:
- consumeEvent = eventHandler.handleEvent(*args)
- if consumeEvent:
- break
-
- def onMove(self, x, y):
- self._processEvent('move', x, y)
-
- def onRelease(self, x, y, btn):
- if btn == LEFT_BTN:
- self._processEvent('release', x, y, btn)
-
- def onWheel(self, x, y, angle):
- self._processEvent('wheel', x, y, angle)
-
- class Focus(State):
- def enterState(self, eventHandler, btn):
- self.eventHandler = eventHandler
- self.focusBtns = {btn}
-
- def validate(self):
- self.eventHandler.validate()
- self.goto('idle')
-
- def onPress(self, x, y, btn):
- if btn == LEFT_BTN:
- self.focusBtns.add(btn)
- self.eventHandler.handleEvent('press', x, y, btn)
-
- def onMove(self, x, y):
- self.eventHandler.handleEvent('move', x, y)
-
- def onRelease(self, x, y, btn):
- if btn == LEFT_BTN:
- self.focusBtns.discard(btn)
- requestFocus = self.eventHandler.handleEvent('release', x, y, btn)
- if len(self.focusBtns) == 0 and not requestFocus:
- self.goto('idle')
-
- def onWheel(self, x, y, angleInDegrees):
- self.eventHandler.handleEvent('wheel', x, y, angleInDegrees)
-
- def __init__(self, eventHandlers=()):
- self.eventHandlers = list(eventHandlers)
-
- states = {
- 'idle': FocusManager.Idle,
- 'focus': FocusManager.Focus
- }
- super(FocusManager, self).__init__(states, 'idle')
-
- def cancel(self):
- for handler in self.eventHandlers:
- handler.cancel()
-
-
-class ZoomAndSelect(ItemsInteraction):
- """Combine Zoom and ItemInteraction state machine.
-
- :param plot: The Plot to which this interaction is attached
- :param color: The color to use for the zoom area bounding box
- """
-
- def __init__(self, plot, color):
- super(ZoomAndSelect, self).__init__(plot)
- self._zoom = Zoom(plot, color)
- self._doZoom = False
-
- @property
- def color(self):
- """Color of the zoom area"""
- return self._zoom.color
-
- def click(self, x, y, btn):
- """Handle mouse click
-
- :param x: X position of the mouse in pixels
- :param y: Y position of the mouse in pixels
- :param btn: Pressed button id
- :return: True if click is catched by an item, False otherwise
- """
- eventDict = self._handleClick(x, y, btn)
-
- if eventDict is not None:
- # Signal mouse clicked event
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
- clickedEventDict = prepareMouseSignal('mouseClicked', btn,
- dataPos[0], dataPos[1],
- x, y)
- self.plot.notify(**clickedEventDict)
-
- self.plot.notify(**eventDict)
-
- else:
- self._zoom.click(x, y, btn)
-
- def beginDrag(self, x, y, btn):
- """Handle start drag and switching between zoom and item drag.
-
- :param x: X position in pixels
- :param y: Y position in pixels
- :param str btn: The mouse button for which a drag is starting.
- """
- self._doZoom = not super(ZoomAndSelect, self).beginDrag(x, y, btn)
- if self._doZoom:
- self._zoom.beginDrag(x, y, btn)
-
- def drag(self, x, y, btn):
- """Handle drag, eventually forwarding to zoom.
-
- :param x: X position in pixels
- :param y: Y position in pixels
- :param str btn: The mouse button for which a drag is in progress.
- """
- if self._doZoom:
- return self._zoom.drag(x, y, btn)
- else:
- return super(ZoomAndSelect, self).drag(x, y, btn)
-
- def endDrag(self, startPos, endPos, btn):
- """Handle end of drag, eventually forwarding to zoom.
-
- :param startPos: (x, y) position at the beginning of the drag
- :param endPos: (x, y) position at the end of the drag
- :param str btn: The mouse button for which a drag is done.
- """
- if self._doZoom:
- return self._zoom.endDrag(startPos, endPos, btn)
- else:
- return super(ZoomAndSelect, self).endDrag(startPos, endPos, btn)
-
-
-class PanAndSelect(ItemsInteraction):
- """Combine Pan and ItemInteraction state machine.
-
- :param plot: The Plot to which this interaction is attached
- """
-
- def __init__(self, plot):
- super(PanAndSelect, self).__init__(plot)
- self._pan = Pan(plot)
- self._doPan = False
-
- def click(self, x, y, btn):
- """Handle mouse click
-
- :param x: X position of the mouse in pixels
- :param y: Y position of the mouse in pixels
- :param btn: Pressed button id
- :return: True if click is catched by an item, False otherwise
- """
- eventDict = self._handleClick(x, y, btn)
-
- if eventDict is not None:
- # Signal mouse clicked event
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
- clickedEventDict = prepareMouseSignal('mouseClicked', btn,
- dataPos[0], dataPos[1],
- x, y)
- self.plot.notify(**clickedEventDict)
-
- self.plot.notify(**eventDict)
-
- else:
- self._pan.click(x, y, btn)
-
- def beginDrag(self, x, y, btn):
- """Handle start drag and switching between zoom and item drag.
-
- :param x: X position in pixels
- :param y: Y position in pixels
- :param str btn: The mouse button for which a drag is starting.
- """
- self._doPan = not super(PanAndSelect, self).beginDrag(x, y, btn)
- if self._doPan:
- self._pan.beginDrag(x, y, btn)
-
- def drag(self, x, y, btn):
- """Handle drag, eventually forwarding to zoom.
-
- :param x: X position in pixels
- :param y: Y position in pixels
- :param str btn: The mouse button for which a drag is in progress.
- """
- if self._doPan:
- return self._pan.drag(x, y, btn)
- else:
- return super(PanAndSelect, self).drag(x, y, btn)
-
- def endDrag(self, startPos, endPos, btn):
- """Handle end of drag, eventually forwarding to zoom.
-
- :param startPos: (x, y) position at the beginning of the drag
- :param endPos: (x, y) position at the end of the drag
- :param str btn: The mouse button for which a drag is done.
- """
- if self._doPan:
- return self._pan.endDrag(startPos, endPos, btn)
- else:
- return super(PanAndSelect, self).endDrag(startPos, endPos, btn)
-
-
-# Interaction mode control ####################################################
-
-# Mapping of draw modes: event handler
-_DRAW_MODES = {
- 'polygon': SelectPolygon,
- 'rectangle': SelectRectangle,
- 'ellipse': SelectEllipse,
- 'line': SelectLine,
- 'vline': SelectVLine,
- 'hline': SelectHLine,
- 'polylines': SelectFreeLine,
- 'pencil': DrawFreeHand,
- }
-
-
-class DrawMode(FocusManager):
- """Interactive mode for draw and select"""
-
- def __init__(self, plot, shape, label, color, width):
- eventHandlerClass = _DRAW_MODES[shape]
- parameters = {
- 'shape': shape,
- 'label': label,
- 'color': color,
- 'width': width,
- }
- super().__init__((
- Pan(plot, clickButtons=(), dragButtons=(MIDDLE_BTN,)),
- eventHandlerClass(plot, parameters)))
-
- def getDescription(self):
- """Returns the dict describing this interactive mode"""
- params = self.eventHandlers[1].parameters.copy()
- params['mode'] = 'draw'
- return params
-
-
-class DrawSelectMode(FocusManager):
- """Interactive mode for draw and select"""
-
- def __init__(self, plot, shape, label, color, width):
- eventHandlerClass = _DRAW_MODES[shape]
- self._pan = Pan(plot)
- self._panStart = None
- parameters = {
- 'shape': shape,
- 'label': label,
- 'color': color,
- 'width': width,
- }
- super().__init__((
- ItemsInteractionForCombo(plot),
- eventHandlerClass(plot, parameters)))
-
- def handleEvent(self, eventName, *args, **kwargs):
- # Hack to add pan interaction to select-draw
- # See issue Refactor PlotWidget interaction #3292
- if eventName == 'press' and args[2] == MIDDLE_BTN:
- self._panStart = args[:2]
- self._pan.beginDrag(*args)
- return # Consume middle click events
- elif eventName == 'release' and args[2] == MIDDLE_BTN:
- self._panStart = None
- self._pan.endDrag(self._panStart, args[:2], MIDDLE_BTN)
- return # Consume middle click events
- elif self._panStart is not None and eventName == 'move':
- x, y = args[:2]
- self._pan.drag(x, y, MIDDLE_BTN)
-
- super().handleEvent(eventName, *args, **kwargs)
-
- def getDescription(self):
- """Returns the dict describing this interactive mode"""
- params = self.eventHandlers[1].parameters.copy()
- params['mode'] = 'select-draw'
- return params
-
-
-class PlotInteraction(object):
- """Proxy to currently use state machine for interaction.
-
- This allows to switch interactive mode.
-
- :param plot: The :class:`Plot` to apply interaction to
- """
-
- _DRAW_MODES = {
- 'polygon': SelectPolygon,
- 'rectangle': SelectRectangle,
- 'ellipse': SelectEllipse,
- 'line': SelectLine,
- 'vline': SelectVLine,
- 'hline': SelectHLine,
- 'polylines': SelectFreeLine,
- 'pencil': DrawFreeHand,
- }
-
- def __init__(self, plot):
- self._plot = weakref.ref(plot) # Avoid cyclic-ref
-
- self.zoomOnWheel = True
- """True to enable zoom on wheel, False otherwise."""
-
- # Default event handler
- self._eventHandler = ItemsInteraction(plot)
-
- 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', 'select-draw', 'zoom'.
- It can also contains extra keys (e.g., 'color') specific to a mode
- as provided to :meth:`setInteractiveMode`.
- """
- if isinstance(self._eventHandler, ZoomAndSelect):
- return {'mode': 'zoom', 'color': self._eventHandler.color}
-
- elif isinstance(self._eventHandler, (DrawMode, DrawSelectMode)):
- return self._eventHandler.getDescription()
-
- elif isinstance(self._eventHandler, PanAndSelect):
- return {'mode': 'pan'}
-
- else:
- return {'mode': 'select'}
-
- def validate(self):
- """Validate the current interaction if possible
-
- If was designed to close the polygon interaction.
- """
- self._eventHandler.validate()
-
- def setInteractiveMode(self, mode, color='black',
- shape='polygon', label=None, width=None):
- """Switch the interactive mode.
-
- :param str mode: The name of the interactive mode.
- In 'draw', 'pan', 'select', 'select-draw', 'zoom'.
- :param color: Only for 'draw' and 'zoom' modes.
- Color to use for drawing selection area. Default black.
- If None, selection area is not drawn.
- :type color: Color description: The name as a str or
- a tuple of 4 floats or None.
- :param str shape: Only for 'draw' mode. The kind of shape to draw.
- In 'polygon', 'rectangle', 'line', 'vline', 'hline',
- 'polylines'.
- Default is 'polygon'.
- :param str label: Only for 'draw' mode.
- :param float width: Width of the pencil. Only for draw pencil mode.
- """
- assert mode in ('draw', 'pan', 'select', 'select-draw', 'zoom')
-
- plot = self._plot()
- assert plot is not None
-
- if isinstance(color, numpy.ndarray) or color not in (None, 'video inverted'):
- color = colors.rgba(color)
-
- if mode in ('draw', 'select-draw'):
- self._eventHandler.cancel()
- handlerClass = DrawMode if mode == 'draw' else DrawSelectMode
- self._eventHandler = handlerClass(plot, shape, label, color, width)
-
- elif mode == 'pan':
- # Ignores color, shape and label
- self._eventHandler.cancel()
- self._eventHandler = PanAndSelect(plot)
-
- elif mode == 'zoom':
- # Ignores shape and label
- self._eventHandler.cancel()
- self._eventHandler = ZoomAndSelect(plot, color)
-
- else: # Default mode: interaction with plot objects
- # Ignores color, shape and label
- self._eventHandler.cancel()
- self._eventHandler = ItemsInteraction(plot)
-
- def handleEvent(self, event, *args, **kwargs):
- """Forward event to current interactive mode state machine."""
- if not self.zoomOnWheel and event == 'wheel':
- return # Discard wheel events
- self._eventHandler.handleEvent(event, *args, **kwargs)
diff --git a/silx/gui/plot/PlotToolButtons.py b/silx/gui/plot/PlotToolButtons.py
deleted file mode 100644
index 3970896..0000000
--- a/silx/gui/plot/PlotToolButtons.py
+++ /dev/null
@@ -1,592 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 set of QToolButton to use with
-:class:`~silx.gui.plot.PlotWidget`.
-
-The following QToolButton are available:
-
-- :class:`.AspectToolButton`
-- :class:`.YAxisOriginToolButton`
-- :class:`.ProfileToolButton`
-- :class:`.SymbolToolButton`
-
-"""
-
-__authors__ = ["V. Valls", "H. Payno"]
-__license__ = "MIT"
-__date__ = "27/06/2017"
-
-
-import functools
-import logging
-import weakref
-
-from .. import icons
-from .. import qt
-from ... import config
-
-from .items import SymbolMixIn, Scatter
-
-
-_logger = logging.getLogger(__name__)
-
-
-class PlotToolButton(qt.QToolButton):
- """A QToolButton connected to a :class:`~silx.gui.plot.PlotWidget`.
- """
-
- def __init__(self, parent=None, plot=None):
- super(PlotToolButton, self).__init__(parent)
- self._plotRef = None
- if plot is not None:
- self.setPlot(plot)
-
- def plot(self):
- """
- Returns the plot connected to the widget.
- """
- return None if self._plotRef is None else self._plotRef()
-
- def setPlot(self, plot):
- """
- Set the plot connected to the widget
-
- :param plot: :class:`.PlotWidget` instance on which to operate.
- """
- previousPlot = self.plot()
-
- if previousPlot is plot:
- return
- if previousPlot is not None:
- self._disconnectPlot(previousPlot)
-
- if plot is None:
- self._plotRef = None
- else:
- self._plotRef = weakref.ref(plot)
- self._connectPlot(plot)
-
- def _connectPlot(self, plot):
- """
- Called when the plot is connected to the widget
-
- :param plot: :class:`.PlotWidget` instance
- """
- pass
-
- def _disconnectPlot(self, plot):
- """
- Called when the plot is disconnected from the widget
-
- :param plot: :class:`.PlotWidget` instance
- """
- pass
-
-
-class AspectToolButton(PlotToolButton):
- """Tool button to switch keep aspect ratio of a plot"""
-
- STATE = None
- """Lazy loaded states used to feed AspectToolButton"""
-
- def __init__(self, parent=None, plot=None):
- if self.STATE is None:
- self.STATE = {}
- # dont keep ratio
- self.STATE[False, "icon"] = icons.getQIcon('shape-ellipse-solid')
- self.STATE[False, "state"] = "Aspect ratio is not kept"
- self.STATE[False, "action"] = "Do no keep data aspect ratio"
- # keep ratio
- self.STATE[True, "icon"] = icons.getQIcon('shape-circle-solid')
- self.STATE[True, "state"] = "Aspect ratio is kept"
- self.STATE[True, "action"] = "Keep data aspect ratio"
-
- super(AspectToolButton, self).__init__(parent=parent, plot=plot)
-
- keepAction = self._createAction(True)
- keepAction.triggered.connect(self.keepDataAspectRatio)
- keepAction.setIconVisibleInMenu(True)
-
- dontKeepAction = self._createAction(False)
- dontKeepAction.triggered.connect(self.dontKeepDataAspectRatio)
- dontKeepAction.setIconVisibleInMenu(True)
-
- menu = qt.QMenu(self)
- menu.addAction(keepAction)
- menu.addAction(dontKeepAction)
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
-
- def _createAction(self, keepAspectRatio):
- icon = self.STATE[keepAspectRatio, "icon"]
- text = self.STATE[keepAspectRatio, "action"]
- return qt.QAction(icon, text, self)
-
- def _connectPlot(self, plot):
- plot.sigSetKeepDataAspectRatio.connect(self._keepDataAspectRatioChanged)
- self._keepDataAspectRatioChanged(plot.isKeepDataAspectRatio())
-
- def _disconnectPlot(self, plot):
- plot.sigSetKeepDataAspectRatio.disconnect(self._keepDataAspectRatioChanged)
-
- def keepDataAspectRatio(self):
- """Configure the plot to keep the aspect ratio"""
- plot = self.plot()
- if plot is not None:
- # This will trigger _keepDataAspectRatioChanged
- plot.setKeepDataAspectRatio(True)
-
- def dontKeepDataAspectRatio(self):
- """Configure the plot to not keep the aspect ratio"""
- plot = self.plot()
- if plot is not None:
- # This will trigger _keepDataAspectRatioChanged
- plot.setKeepDataAspectRatio(False)
-
- def _keepDataAspectRatioChanged(self, aspectRatio):
- """Handle Plot set keep aspect ratio signal"""
- icon, toolTip = self.STATE[aspectRatio, "icon"], self.STATE[aspectRatio, "state"]
- self.setIcon(icon)
- self.setToolTip(toolTip)
-
-
-class YAxisOriginToolButton(PlotToolButton):
- """Tool button to switch the Y axis orientation of a plot."""
-
- STATE = None
- """Lazy loaded states used to feed YAxisOriginToolButton"""
-
- def __init__(self, parent=None, plot=None):
- if self.STATE is None:
- self.STATE = {}
- # is down
- self.STATE[False, "icon"] = icons.getQIcon('plot-ydown')
- self.STATE[False, "state"] = "Y-axis is oriented downward"
- self.STATE[False, "action"] = "Orient Y-axis downward"
- # keep ration
- self.STATE[True, "icon"] = icons.getQIcon('plot-yup')
- self.STATE[True, "state"] = "Y-axis is oriented upward"
- self.STATE[True, "action"] = "Orient Y-axis upward"
-
- super(YAxisOriginToolButton, self).__init__(parent=parent, plot=plot)
-
- upwardAction = self._createAction(True)
- upwardAction.triggered.connect(self.setYAxisUpward)
- upwardAction.setIconVisibleInMenu(True)
-
- downwardAction = self._createAction(False)
- downwardAction.triggered.connect(self.setYAxisDownward)
- downwardAction.setIconVisibleInMenu(True)
-
- menu = qt.QMenu(self)
- menu.addAction(upwardAction)
- menu.addAction(downwardAction)
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
-
- def _createAction(self, isUpward):
- icon = self.STATE[isUpward, "icon"]
- text = self.STATE[isUpward, "action"]
- return qt.QAction(icon, text, self)
-
- def _connectPlot(self, plot):
- yAxis = plot.getYAxis()
- yAxis.sigInvertedChanged.connect(self._yAxisInvertedChanged)
- self._yAxisInvertedChanged(yAxis.isInverted())
-
- def _disconnectPlot(self, plot):
- 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.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.getYAxis().setInverted(True)
-
- def _yAxisInvertedChanged(self, inverted):
- """Handle Plot set y axis inverted signal"""
- isUpward = not inverted
- icon, toolTip = self.STATE[isUpward, "icon"], self.STATE[isUpward, "state"]
- self.setIcon(icon)
- self.setToolTip(toolTip)
-
-
-class ProfileOptionToolButton(PlotToolButton):
- """Button to define option on the profile"""
- sigMethodChanged = qt.Signal(str)
-
- def __init__(self, parent=None, plot=None):
- PlotToolButton.__init__(self, parent=parent, plot=plot)
-
- self.STATE = {}
- # is down
- self.STATE['sum', "icon"] = icons.getQIcon('math-sigma')
- self.STATE['sum', "state"] = "Compute profile sum"
- self.STATE['sum', "action"] = "Compute profile sum"
- # keep ration
- self.STATE['mean', "icon"] = icons.getQIcon('math-mean')
- self.STATE['mean', "state"] = "Compute profile mean"
- self.STATE['mean', "action"] = "Compute profile mean"
-
- self.sumAction = self._createAction('sum')
- self.sumAction.triggered.connect(self.setSum)
- self.sumAction.setIconVisibleInMenu(True)
- self.sumAction.setCheckable(True)
- self.sumAction.setChecked(True)
-
- self.meanAction = self._createAction('mean')
- self.meanAction.triggered.connect(self.setMean)
- self.meanAction.setIconVisibleInMenu(True)
- self.meanAction.setCheckable(True)
-
- menu = qt.QMenu(self)
- menu.addAction(self.sumAction)
- menu.addAction(self.meanAction)
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
- self._method = 'mean'
- self._update()
-
- def _createAction(self, method):
- icon = self.STATE[method, "icon"]
- text = self.STATE[method, "action"]
- return qt.QAction(icon, text, self)
-
- def setSum(self):
- self.setMethod('sum')
-
- def _update(self):
- icon = self.STATE[self._method, "icon"]
- toolTip = self.STATE[self._method, "state"]
- self.setIcon(icon)
- self.setToolTip(toolTip)
- self.sumAction.setChecked(self._method == "sum")
- self.meanAction.setChecked(self._method == "mean")
-
- def setMean(self):
- self.setMethod('mean')
-
- def setMethod(self, method):
- """Set the method to use.
-
- :param str method: Either 'sum' or 'mean'
- """
- if method != self._method:
- if method in ('sum', 'mean'):
- self._method = method
- self.sigMethodChanged.emit(self._method)
- self._update()
- else:
- _logger.warning(
- "Unsupported method '%s'. Setting ignored.", method)
-
- def getMethod(self):
- """Returns the current method in use (See :meth:`setMethod`).
-
- :rtype: str
- """
- return self._method
-
-
-class ProfileToolButton(PlotToolButton):
- """Button used in Profile3DToolbar to switch between 2D profile
- and 1D profile."""
- STATE = None
- """Lazy loaded states used to feed ProfileToolButton"""
-
- sigDimensionChanged = qt.Signal(int)
-
- def __init__(self, parent=None, plot=None):
- if self.STATE is None:
- self.STATE = {
- (1, "icon"): icons.getQIcon('profile1D'),
- (1, "state"): "1D profile is computed on visible image",
- (1, "action"): "1D profile on visible image",
- (2, "icon"): icons.getQIcon('profile2D'),
- (2, "state"): "2D profile is computed, one 1D profile for each image in the stack",
- (2, "action"): "2D profile on image stack"}
- # Compute 1D profile
- # Compute 2D profile
-
- super(ProfileToolButton, self).__init__(parent=parent, plot=plot)
-
- self._dimension = 1
-
- profile1DAction = self._createAction(1)
- profile1DAction.triggered.connect(self.computeProfileIn1D)
- profile1DAction.setIconVisibleInMenu(True)
- profile1DAction.setCheckable(True)
- profile1DAction.setChecked(True)
- self._profile1DAction = profile1DAction
-
- profile2DAction = self._createAction(2)
- profile2DAction.triggered.connect(self.computeProfileIn2D)
- profile2DAction.setIconVisibleInMenu(True)
- profile2DAction.setCheckable(True)
- self._profile2DAction = profile2DAction
-
- menu = qt.QMenu(self)
- menu.addAction(profile1DAction)
- menu.addAction(profile2DAction)
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
- menu.setTitle('Select profile dimension')
- self.computeProfileIn1D()
-
- def _createAction(self, profileDimension):
- icon = self.STATE[profileDimension, "icon"]
- text = self.STATE[profileDimension, "action"]
- return qt.QAction(icon, text, self)
-
- def _profileDimensionChanged(self, profileDimension):
- """Update icon in toolbar, emit number of dimensions for profile"""
- self.setIcon(self.STATE[profileDimension, "icon"])
- self.setToolTip(self.STATE[profileDimension, "state"])
- self._dimension = profileDimension
- self.sigDimensionChanged.emit(profileDimension)
- self._profile1DAction.setChecked(profileDimension == 1)
- self._profile2DAction.setChecked(profileDimension == 2)
-
- def computeProfileIn1D(self):
- self._profileDimensionChanged(1)
-
- def computeProfileIn2D(self):
- self._profileDimensionChanged(2)
-
- def setDimension(self, dimension):
- """Set the selected dimension"""
- assert dimension in [1, 2]
- if self._dimension == dimension:
- return
- if dimension == 1:
- self.computeProfileIn1D()
- elif dimension == 2:
- self.computeProfileIn2D()
- else:
- _logger.warning("Unsupported dimension '%s'. Setting ignored.", dimension)
-
- def getDimension(self):
- """Get the selected dimension.
-
- :rtype: int (1 or 2)
- """
- return self._dimension
-
-
-class _SymbolToolButtonBase(PlotToolButton):
- """Base class for PlotToolButton setting marker and size.
-
- :param parent: See QWidget
- :param plot: The `~silx.gui.plot.PlotWidget` to control
- """
-
- def __init__(self, parent=None, plot=None):
- super(_SymbolToolButtonBase, self).__init__(parent=parent, plot=plot)
-
- def _addSizeSliderToMenu(self, menu):
- """Add a slider to set size to the given menu
-
- :param QMenu menu:
- """
- slider = qt.QSlider(qt.Qt.Horizontal)
- slider.setRange(1, 20)
- slider.setValue(int(config.DEFAULT_PLOT_SYMBOL_SIZE))
- slider.setTracking(False)
- slider.valueChanged.connect(self._sizeChanged)
- widgetAction = qt.QWidgetAction(menu)
- widgetAction.setDefaultWidget(slider)
- menu.addAction(widgetAction)
-
- def _addSymbolsToMenu(self, menu):
- """Add symbols to the given menu
-
- :param QMenu menu:
- """
- for marker, name in zip(SymbolMixIn.getSupportedSymbols(),
- SymbolMixIn.getSupportedSymbolNames()):
- action = qt.QAction(name, menu)
- action.setCheckable(False)
- action.triggered.connect(
- functools.partial(self._markerChanged, marker))
- menu.addAction(action)
-
- def _sizeChanged(self, value):
- """Manage slider value changed
-
- :param int value: Marker size
- """
- plot = self.plot()
- if plot is None:
- return
-
- for item in plot.getItems():
- if isinstance(item, SymbolMixIn):
- item.setSymbolSize(value)
-
- def _markerChanged(self, marker):
- """Manage change of marker.
-
- :param str marker: Letter describing the marker
- """
- plot = self.plot()
- if plot is None:
- return
-
- for item in plot.getItems():
- if isinstance(item, SymbolMixIn):
- item.setSymbol(marker)
-
-
-class SymbolToolButton(_SymbolToolButtonBase):
- """A tool button with a drop-down menu to control symbol size and marker.
-
- :param parent: See QWidget
- :param plot: The `~silx.gui.plot.PlotWidget` to control
- """
-
- def __init__(self, parent=None, plot=None):
- super(SymbolToolButton, self).__init__(parent=parent, plot=plot)
-
- self.setToolTip('Set symbol size and marker')
- self.setIcon(icons.getQIcon('plot-symbols'))
-
- menu = qt.QMenu(self)
- self._addSizeSliderToMenu(menu)
- menu.addSeparator()
- self._addSymbolsToMenu(menu)
-
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
-
-
-class ScatterVisualizationToolButton(_SymbolToolButtonBase):
- """QToolButton to select the visualization mode of scatter plot
-
- :param parent: See QWidget
- :param plot: The `~silx.gui.plot.PlotWidget` to control
- """
-
- def __init__(self, parent=None, plot=None):
- super(ScatterVisualizationToolButton, self).__init__(
- parent=parent, plot=plot)
-
- self.setToolTip(
- 'Set scatter visualization mode, symbol marker and size')
- self.setIcon(icons.getQIcon('eye'))
-
- menu = qt.QMenu(self)
-
- # Add visualization modes
-
- for mode in Scatter.supportedVisualizations():
- if mode is not Scatter.Visualization.BINNED_STATISTIC:
- name = mode.value.capitalize()
- action = qt.QAction(name, menu)
- action.setCheckable(False)
- action.triggered.connect(
- functools.partial(self._visualizationChanged, mode, None))
- menu.addAction(action)
-
- if Scatter.Visualization.BINNED_STATISTIC in Scatter.supportedVisualizations():
- reductions = Scatter.supportedVisualizationParameterValues(
- Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION)
- if reductions:
- submenu = menu.addMenu('Binned Statistic')
- for reduction in reductions:
- name = reduction.capitalize()
- action = qt.QAction(name, menu)
- action.setCheckable(False)
- action.triggered.connect(functools.partial(
- self._visualizationChanged,
- Scatter.Visualization.BINNED_STATISTIC,
- {Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION: reduction}))
- submenu.addAction(action)
-
- submenu.addSeparator()
- binsmenu = submenu.addMenu('N Bins')
-
- slider = qt.QSlider(qt.Qt.Horizontal)
- slider.setRange(10, 1000)
- slider.setValue(100)
- slider.setTracking(False)
- slider.valueChanged.connect(self._binningChanged)
- widgetAction = qt.QWidgetAction(binsmenu)
- widgetAction.setDefaultWidget(slider)
- binsmenu.addAction(widgetAction)
-
- menu.addSeparator()
-
- submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol")
- self._addSymbolsToMenu(submenu)
-
- submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol Size")
- self._addSizeSliderToMenu(submenu)
-
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
-
- def _visualizationChanged(self, mode, parameters=None):
- """Handle change of visualization mode.
-
- :param ScatterVisualizationMixIn.Visualization mode:
- The visualization mode to use for scatter
- :param Union[dict,None] parameters:
- Dict of VisualizationParameter: parameter_value to set
- with the visualization.
- """
- plot = self.plot()
- if plot is None:
- return
-
- for item in plot.getItems():
- if isinstance(item, Scatter):
- if parameters:
- for parameter, value in parameters.items():
- item.setVisualizationParameter(parameter, value)
- item.setVisualization(mode)
-
- def _binningChanged(self, value):
- """Handle change of binning.
-
- :param int value: The number of bin on each dimension.
- """
- plot = self.plot()
- if plot is None:
- return
-
- for item in plot.getItems():
- if isinstance(item, Scatter):
- item.setVisualizationParameter(
- Scatter.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- (value, value))
- item.setVisualization(Scatter.Visualization.BINNED_STATISTIC)
diff --git a/silx/gui/plot/PlotTools.py b/silx/gui/plot/PlotTools.py
deleted file mode 100644
index 5929473..0000000
--- a/silx/gui/plot/PlotTools.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 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.
-#
-# ###########################################################################*/
-"""Set of widgets to associate with a :class:'PlotWidget'.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "01/03/2018"
-
-
-from ...utils.deprecation import deprecated_warning
-
-deprecated_warning(type_='module',
- name=__file__,
- reason='Plot tools refactoring',
- replacement='silx.gui.plot.tools',
- since_version='0.8')
-
-from .tools import PositionInfo, LimitsToolBar # noqa
diff --git a/silx/gui/plot/PlotWidget.py b/silx/gui/plot/PlotWidget.py
deleted file mode 100755
index 2a211de..0000000
--- a/silx/gui/plot/PlotWidget.py
+++ /dev/null
@@ -1,3621 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2021 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.
-# ###########################################################################*/
-"""Qt widget providing plot API for 1D and 2D data.
-
-The :class:`PlotWidget` implements the plot API initially provided in PyMca.
-"""
-
-from __future__ import division
-
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "21/12/2018"
-
-import logging
-
-_logger = logging.getLogger(__name__)
-
-
-from collections import OrderedDict, namedtuple
-from contextlib import contextmanager
-import datetime as dt
-import itertools
-import typing
-import warnings
-
-import numpy
-
-import silx
-from silx.utils.weakref import WeakMethodProxy
-from silx.utils.property import classproperty
-from silx.utils.deprecation import deprecated, deprecated_warning
-try:
- # Import matplotlib now to init matplotlib our way
- import silx.gui.utils.matplotlib # noqa
-except ImportError:
- _logger.debug("matplotlib not available")
-
-import six
-from ..colors import Colormap
-from .. import colors
-from . import PlotInteraction
-from . import PlotEvents
-from .LimitsHistory import LimitsHistory
-from . import _utils
-
-from . import items
-from .items.curve import CurveStyle
-from .items.axis import TickMode # noqa
-
-from .. import qt
-from ._utils.panzoom import ViewConstraints
-from ...gui.plot._utils.dtime_ticklayout import timestamp
-
-
-
-_COLORDICT = colors.COLORDICT
-_COLORLIST = silx.config.DEFAULT_PLOT_CURVE_COLORS
-
-"""
-Object returned when requesting the data range.
-"""
-_PlotDataRange = namedtuple('PlotDataRange',
- ['x', 'y', 'yright'])
-
-
-class _PlotWidgetSelection(qt.QObject):
- """Object managing a :class:`PlotWidget` selection.
-
- It is a wrapper over :class:`PlotWidget`'s active items API.
-
- :param PlotWidget parent:
- """
-
- sigCurrentItemChanged = qt.Signal(object, object)
- """This signal is emitted whenever the current item changes.
-
- It provides the current and previous items.
- """
-
- sigSelectedItemsChanged = qt.Signal()
- """Signal emitted whenever the list of selected items changes."""
-
- def __init__(self, parent):
- assert isinstance(parent, PlotWidget)
- super(_PlotWidgetSelection, self).__init__(parent=parent)
-
- # Init history
- self.__history = [ # Store active items from most recent to oldest
- item for item in (parent.getActiveCurve(),
- parent.getActiveImage(),
- parent.getActiveScatter())
- if item is not None]
-
- self.__current = self.__mostRecentActiveItem()
-
- parent.sigActiveImageChanged.connect(self._activeImageChanged)
- parent.sigActiveCurveChanged.connect(self._activeCurveChanged)
- parent.sigActiveScatterChanged.connect(self._activeScatterChanged)
-
- def __mostRecentActiveItem(self) -> typing.Optional[items.Item]:
- """Returns most recent active item."""
- return self.__history[0] if len(self.__history) >= 1 else None
-
- def getSelectedItems(self) -> typing.Tuple[items.Item]:
- """Returns the list of currently selected items in the :class:`PlotWidget`.
-
- The list is given from most recently current item to oldest one."""
- plot = self.parent()
- if plot is None:
- return ()
-
- active = tuple(self.__history)
-
- current = self.getCurrentItem()
- if current is not None and current not in active:
- # Current might not be an active item, if so add it
- active = (current,) + active
-
- return active
-
- def getCurrentItem(self) -> typing.Optional[items.Item]:
- """Returns the current item in the :class:`PlotWidget` or None. """
- return self.__current
-
- def setCurrentItem(self, item: typing.Optional[items.Item]):
- """Set the current item in the :class:`PlotWidget`.
-
- :param item:
- The new item to select or None to clear the selection.
- :raise ValueError: If the item is not the :class:`PlotWidget`
- """
- previous = self.getCurrentItem()
- if previous is item:
- return
-
- previousSelected = self.getSelectedItems()
-
- if item is None:
- self.__current = None
-
- # Reset all PlotWidget active items
- plot = self.parent()
- if plot is not None:
- for kind in PlotWidget._ACTIVE_ITEM_KINDS:
- if plot._getActiveItem(kind) is not None:
- plot._setActiveItem(kind, None)
-
- elif isinstance(item, items.Item):
- plot = self.parent()
- if plot is None or item.getPlot() is not plot:
- raise ValueError(
- "Item is not in the PlotWidget: %s" % str(item))
- self.__current = item
-
- kind = plot._itemKind(item)
-
- # Clean-up history to be safe
- self.__history = [item for item in self.__history
- if PlotWidget._itemKind(item) != kind]
-
- # Sync active item if needed
- if (kind in plot._ACTIVE_ITEM_KINDS and
- item is not plot._getActiveItem(kind)):
- plot._setActiveItem(kind, item.getName())
- else:
- raise ValueError("Not an Item: %s" % str(item))
-
- self.sigCurrentItemChanged.emit(previous, item)
-
- if previousSelected != self.getSelectedItems():
- self.sigSelectedItemsChanged.emit()
-
- def __activeItemChanged(self,
- kind: str,
- previous: typing.Optional[str],
- legend: typing.Optional[str]):
- """Set current item from kind and legend"""
- if previous == legend:
- return # No-op for update of item
-
- plot = self.parent()
- if plot is None:
- return
-
- previousSelected = self.getSelectedItems()
-
- # Remove items of this kind from the history
- self.__history = [item for item in self.__history
- if PlotWidget._itemKind(item) != kind]
-
- # Retrieve current item
- if legend is None: # Use most recent active item
- currentItem = self.__mostRecentActiveItem()
- else:
- currentItem = plot._getItem(kind=kind, legend=legend)
- if currentItem is None: # Fallback in case something went wrong
- currentItem = self.__mostRecentActiveItem()
-
- # Update history
- if currentItem is not None:
- while currentItem in self.__history:
- self.__history.remove(currentItem)
- self.__history.insert(0, currentItem)
-
- if currentItem != self.__current:
- previousItem = self.__current
- self.__current = currentItem
- self.sigCurrentItemChanged.emit(previousItem, currentItem)
-
- if previousSelected != self.getSelectedItems():
- self.sigSelectedItemsChanged.emit()
-
- def _activeImageChanged(self, previous, current):
- """Handle active image change"""
- self.__activeItemChanged('image', previous, current)
-
- def _activeCurveChanged(self, previous, current):
- """Handle active curve change"""
- self.__activeItemChanged('curve', previous, current)
-
- def _activeScatterChanged(self, previous, current):
- """Handle active scatter change"""
- self.__activeItemChanged('scatter', previous, current)
-
-
-class PlotWidget(qt.QMainWindow):
- """Qt Widget providing a 1D/2D plot.
-
- This widget is a QMainWindow.
- 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 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`
- """
-
- # TODO: Can be removed for silx 0.10
- @classproperty
- @deprecated(replacement="silx.config.DEFAULT_PLOT_BACKEND", since_version="0.8", skip_backtrace_count=2)
- def DEFAULT_BACKEND(self):
- """Class attribute setting the default backend for all instances."""
- return silx.config.DEFAULT_PLOT_BACKEND
-
- colorList = _COLORLIST
- colorDict = _COLORDICT
-
- sigPlotSignal = qt.Signal(object)
- """Signal for all events of the plot.
-
- The signal information is provided as a dict.
- See the :ref:`plot signal documentation page <plot_signal>` for
- information about the content of the dict
- """
-
- sigSetKeepDataAspectRatio = qt.Signal(bool)
- """Signal emitted when plot keep aspect ratio has changed"""
-
- sigSetGraphGrid = qt.Signal(str)
- """Signal emitted when plot grid has changed"""
-
- sigSetGraphCursor = qt.Signal(bool)
- """Signal emitted when plot crosshair cursor has changed"""
-
- 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 the following information:
-
- - action: The change of the plot: 'add' or 'remove'
- - kind: The kind of primitive changed:
- 'curve', 'image', 'scatter', 'histogram', 'item' or 'marker'
- - legend: The legend of the primitive changed.
- """
-
- sigActiveCurveChanged = qt.Signal(object, object)
- """Signal emitted when the active curve has changed.
-
- 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
- """
-
- sigActiveImageChanged = qt.Signal(object, object)
- """Signal emitted when the active image has changed.
-
- 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
- """
-
- sigActiveScatterChanged = qt.Signal(object, object)
- """Signal emitted when the active Scatter has changed.
-
- 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
- """
-
- sigInteractiveModeChanged = qt.Signal(object)
- """Signal emitted when the interactive mode has changed
-
- It provides the source as passed to :meth:`setInteractiveMode`.
- """
-
- sigItemAdded = qt.Signal(items.Item)
- """Signal emitted when an item was just added to the plot
-
- It provides the added item.
- """
-
- sigItemAboutToBeRemoved = qt.Signal(items.Item)
- """Signal emitted right before an item is removed from the plot.
-
- It provides the item that will be removed.
- """
-
- sigItemRemoved = qt.Signal(items.Item)
- """Signal emitted right after an item was removed from the plot.
-
- It provides the item that was removed.
- """
-
- sigVisibilityChanged = qt.Signal(bool)
- """Signal emitted when the widget becomes visible (or invisible).
- This happens when the widget is hidden or shown.
-
- It provides the visible state.
- """
-
- _sigDefaultContextMenu = qt.Signal(qt.QMenu)
- """Signal emitted when the default context menu of the plot is feed.
-
- It provides the menu which will be displayed.
- """
-
- def __init__(self, parent=None, backend=None):
- self._autoreplot = False
- self._dirty = False
- self._cursorInPlot = False
- self.__muteActiveItemChanged = False
-
- self._panWithArrowKeys = True
- self._viewConstrains = None
-
- super(PlotWidget, self).__init__(parent)
- if parent is not None:
- # behave as a widget
- self.setWindowFlags(qt.Qt.Widget)
- else:
- self.setWindowTitle('PlotWidget')
-
- # Init the backend
- self._backend = self.__getBackendClass(backend)(self, self)
-
- 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._activeCurveSelectionMode = "atmostone"
- self._activeCurveStyle = CurveStyle(color='#000000')
- self._activeLegend = {'curve': None, 'image': None,
- 'scatter': None}
-
- # plot colors (updated later to sync backend)
- self._foregroundColor = 0., 0., 0., 1.
- self._gridColor = .7, .7, .7, 1.
- self._backgroundColor = 1., 1., 1., 1.
- self._dataBackgroundColor = 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.__graphCursorShape = 'default'
-
- # Set axes margins
- self.__axesDisplayed = True
- self.__axesMargins = 0., 0., 0., 0.
- self.setAxesMargins(.15, .1, .1, .15)
-
- self.setGraphTitle()
- self.setGraphXLabel()
- self.setGraphYLabel()
- self.setGraphYLabel('', axis='right')
-
- self.setDefaultColormap() # Init default colormap
-
- self.setDefaultPlotPoints(silx.config.DEFAULT_PLOT_CURVE_SYMBOL_MODE)
- self.setDefaultPlotLines(True)
-
- self._limitsHistory = LimitsHistory(self)
-
- self._eventHandler = PlotInteraction.PlotInteraction(self)
- self._eventHandler.setInteractiveMode('zoom', color=(0., 0., 0., 1.))
- self._previousDefaultMode = "zoom", True
-
- 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.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')
-
- # Sync backend colors with default ones
- self._foregroundColorsUpdated()
- self._backgroundColorsUpdated()
-
- # selection handling
- self.__selection = None
-
- def __getBackendClass(self, backend):
- """Returns backend class corresponding to backend.
-
- If multiple backends are provided, the first available one is used.
-
- :param Union[str,BackendBase,List[Union[str,BackendBase]]] backend:
- The name of the backend or its class or an iterable of those.
- :rtype: BackendBase
- :raise ValueError: In case the backend is not supported
- :raise RuntimeError: If a backend is not available
- """
- if backend is None:
- backend = silx.config.DEFAULT_PLOT_BACKEND
-
- if callable(backend):
- return backend
-
- elif isinstance(backend, six.string_types):
- backend = backend.lower()
- if backend in ('matplotlib', 'mpl'):
- try:
- from .backends.BackendMatplotlib import \
- BackendMatplotlibQt as backendClass
- except ImportError:
- _logger.debug("Backtrace", exc_info=True)
- raise RuntimeError("matplotlib backend is not available")
-
- elif backend in ('gl', 'opengl'):
- from ..utils.glutils import isOpenGLAvailable
- checkOpenGL = isOpenGLAvailable(version=(2, 1), runtimeCheck=False)
- if not checkOpenGL:
- _logger.debug("OpenGL check failed")
- raise RuntimeError(
- "OpenGL backend is not available: %s" % checkOpenGL.error)
-
- try:
- from .backends.BackendOpenGL import \
- BackendOpenGL as backendClass
- except ImportError:
- _logger.debug("Backtrace", exc_info=True)
- raise RuntimeError("OpenGL backend is not available")
-
- elif backend == 'none':
- from .backends.BackendBase import BackendBase as backendClass
-
- else:
- raise ValueError("Backend not supported %s" % backend)
-
- return backendClass
-
- elif isinstance(backend, (tuple, list)):
- for b in backend:
- try:
- return self.__getBackendClass(b)
- except RuntimeError:
- pass
- else: # No backend was found
- raise RuntimeError("None of the request backends are available")
-
- raise ValueError("Backend not supported %s" % str(backend))
-
- def selection(self):
- """Returns the selection hander"""
- if self.__selection is None: # Lazy initialization
- self.__selection = _PlotWidgetSelection(parent=self)
- return self.__selection
-
- # TODO: Can be removed for silx 0.10
- @staticmethod
- @deprecated(replacement="silx.config.DEFAULT_PLOT_BACKEND", since_version="0.8", skip_backtrace_count=2)
- 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
- """
- silx.config.DEFAULT_PLOT_BACKEND = backend
-
- def setBackend(self, backend):
- """Set the backend to use for rendering.
-
- 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 Union[str,BackendBase,List[Union[str,BackendBase]]] backend:
- The backend to use, in:
- 'matplotlib' (default), 'mpl', 'opengl', 'gl', 'none',
- a :class:`BackendBase.BackendBase` class.
- If multiple backends are provided, the first available one is used.
- :raises ValueError: Unsupported backend descriptor
- :raises RuntimeError: Error while loading a backend
- """
- backend = self.__getBackendClass(backend)(self, self)
-
- # First save state that is stored in the backend
- xaxis = self.getXAxis()
- xmin, xmax = xaxis.getLimits()
- ymin, ymax = self.getYAxis(axis='left').getLimits()
- y2min, y2max = self.getYAxis(axis='right').getLimits()
- isKeepDataAspectRatio = self.isKeepDataAspectRatio()
- xTimeZone = xaxis.getTimeZone()
- isXAxisTimeSeries = xaxis.getTickMode() == TickMode.TIME_SERIES
-
- isYAxisInverted = self.getYAxis().isInverted()
-
- # Remove all items from previous backend
- for item in self.getItems():
- item._removeBackendRenderer(self._backend)
-
- # Switch backend
- self._backend = backend
- widget = self._backend.getWidgetHandle()
- self.setCentralWidget(widget)
- if widget is None:
- _logger.info("PlotWidget backend does not support widget")
-
- # Mark as newly dirty
- self._dirty = False
- self._setDirtyPlot()
-
- # Synchronize/restore state
- self._foregroundColorsUpdated()
- self._backgroundColorsUpdated()
-
- self._backend.setGraphCursorShape(self.getGraphCursorShape())
- crosshairConfig = self.getGraphCursor()
- if crosshairConfig is None:
- self._backend.setGraphCursor(False, 'black', 1, '-')
- else:
- self._backend.setGraphCursor(True, *crosshairConfig)
-
- self._backend.setGraphTitle(self.getGraphTitle())
- self._backend.setGraphGrid(self.getGraphGrid())
- if self.isAxesDisplayed():
- self._backend.setAxesMargins(*self.getAxesMargins())
- else:
- self._backend.setAxesMargins(0., 0., 0., 0.)
-
- # Set axes
- xaxis = self.getXAxis()
- self._backend.setGraphXLabel(xaxis.getLabel())
- self._backend.setXAxisTimeZone(xTimeZone)
- self._backend.setXAxisTimeSeries(isXAxisTimeSeries)
- self._backend.setXAxisLogarithmic(
- xaxis.getScale() == items.Axis.LOGARITHMIC)
-
- for axis in ('left', 'right'):
- self._backend.setGraphYLabel(self.getYAxis(axis).getLabel(), axis)
- self._backend.setYAxisInverted(isYAxisInverted)
- self._backend.setYAxisLogarithmic(
- self.getYAxis().getScale() == items.Axis.LOGARITHMIC)
-
- # Finally restore aspect ratio and limits
- self._backend.setKeepDataAspectRatio(isKeepDataAspectRatio)
- self.setLimits(xmin, xmax, ymin, ymax, y2min, y2max)
-
- # Mark all items for update with new backend
- for item in self.getItems():
- item._updated()
-
- def getBackend(self):
- """Returns the backend currently used by :class:`PlotWidget`.
-
- :rtype: ~silx.gui.plot.backend.BackendBase.BackendBase
- """
- return self._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
-
- # Default Qt context menu
-
- def contextMenuEvent(self, event):
- """Override QWidget.contextMenuEvent to implement the context menu"""
- menu = qt.QMenu(self)
- from .actions.control import ZoomBackAction # Avoid cyclic import
- zoomBackAction = ZoomBackAction(plot=self, parent=menu)
- menu.addAction(zoomBackAction)
-
- mode = self.getInteractiveMode()
- if "shape" in mode and mode["shape"] == "polygon":
- from .actions.control import ClosePolygonInteractionAction # Avoid cyclic import
- action = ClosePolygonInteractionAction(plot=self, parent=menu)
- menu.addAction(action)
-
- self._sigDefaultContextMenu.emit(menu)
-
- # Make sure the plot is updated, especially when the plot is in
- # draw interaction mode
- menu.aboutToHide.connect(self.__simulateMouseMove)
-
- menu.exec_(event.globalPos())
-
- 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 and self.isVisible():
- self._backend.postRedisplay()
-
- def _foregroundColorsUpdated(self):
- """Handle change of foreground/grid color"""
- if self._gridColor is None:
- gridColor = self._foregroundColor
- else:
- gridColor = self._gridColor
- self._backend.setForegroundColors(
- self._foregroundColor, gridColor)
- self._setDirtyPlot()
-
- def getForegroundColor(self):
- """Returns the RGBA colors used to display the foreground of this widget
-
- :rtype: qt.QColor
- """
- return qt.QColor.fromRgbF(*self._foregroundColor)
-
- def setForegroundColor(self, color):
- """Set the foreground color of this widget.
-
- :param Union[List[int],List[float],QColor] color:
- The new RGB(A) color.
- """
- color = colors.rgba(color)
- if self._foregroundColor != color:
- self._foregroundColor = color
- self._foregroundColorsUpdated()
-
- def getGridColor(self):
- """Returns the RGBA colors used to display the grid lines
-
- An invalid QColor is returned if there is no grid color,
- in which case the foreground color is used.
-
- :rtype: qt.QColor
- """
- if self._gridColor is None:
- return qt.QColor() # An invalid color
- else:
- return qt.QColor.fromRgbF(*self._gridColor)
-
- def setGridColor(self, color):
- """Set the grid lines color
-
- :param Union[List[int],List[float],QColor,None] color:
- The new RGB(A) color.
- """
- if isinstance(color, qt.QColor) and not color.isValid():
- color = None
- if color is not None:
- color = colors.rgba(color)
- if self._gridColor != color:
- self._gridColor = color
- self._foregroundColorsUpdated()
-
- def _backgroundColorsUpdated(self):
- """Handle change of background/data background color"""
- if self._dataBackgroundColor is None:
- dataBGColor = self._backgroundColor
- else:
- dataBGColor = self._dataBackgroundColor
- self._backend.setBackgroundColors(
- self._backgroundColor, dataBGColor)
- self._setDirtyPlot()
-
- def getBackgroundColor(self):
- """Returns the RGBA colors used to display the background of this widget.
-
- :rtype: qt.QColor
- """
- return qt.QColor.fromRgbF(*self._backgroundColor)
-
- def setBackgroundColor(self, color):
- """Set the background color of this widget.
-
- :param Union[List[int],List[float],QColor] color:
- The new RGB(A) color.
- """
- color = colors.rgba(color)
- if self._backgroundColor != color:
- self._backgroundColor = color
- self._backgroundColorsUpdated()
-
- def getDataBackgroundColor(self):
- """Returns the RGBA colors used to display the background of the plot
- view displaying the data.
-
- An invalid QColor is returned if there is no data background color.
-
- :rtype: qt.QColor
- """
- if self._dataBackgroundColor is None:
- # An invalid color
- return qt.QColor()
- else:
- return qt.QColor.fromRgbF(*self._dataBackgroundColor)
-
- def setDataBackgroundColor(self, color):
- """Set the background color of the plot area.
-
- Set to None or an invalid QColor to use the background color.
-
- :param Union[List[int],List[float],QColor,None] color:
- The new RGB(A) color.
- """
- if isinstance(color, qt.QColor) and not color.isValid():
- color = None
- if color is not None:
- color = colors.rgba(color)
- if self._dataBackgroundColor != color:
- self._dataBackgroundColor = color
- self._backgroundColorsUpdated()
-
- dataBackgroundColor = qt.Property(
- qt.QColor, getDataBackgroundColor, setDataBackgroundColor
- )
-
- backgroundColor = qt.Property(qt.QColor, getBackgroundColor, setBackgroundColor)
-
- foregroundColor = qt.Property(qt.QColor, getForegroundColor, setForegroundColor)
-
- gridColor = qt.Property(qt.QColor, getGridColor, setGridColor)
-
- def showEvent(self, event):
- if self._autoreplot and self._dirty:
- self._backend.postRedisplay()
- super(PlotWidget, self).showEvent(event)
- self.sigVisibilityChanged.emit(True)
-
- def hideEvent(self, event):
- super(PlotWidget, self).hideEvent(event)
- self.sigVisibilityChanged.emit(False)
-
- 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.getItems():
- if item.isVisible():
- bounds = item.getBounds()
- if bounds is not None:
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
- # Ignore All-NaN slice encountered
- 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'):
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
- # Ignore All-NaN slice encountered
- yMinRight = numpy.nanmin([yMinRight, bounds[2]])
- yMaxRight = numpy.nanmax([yMaxRight, bounds[3]])
- else:
- with warnings.catch_warnings():
- warnings.simplefilter('ignore', category=RuntimeWarning)
- # Ignore All-NaN slice encountered
- 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
-
- _KIND_TO_CLASSES = {
- 'curve': (items.Curve,),
- 'image': (items.ImageBase,),
- 'scatter': (items.Scatter,),
- 'marker': (items.MarkerBase,),
- 'item': (items.Shape,
- items.BoundingRect,
- items.XAxisExtent,
- items.YAxisExtent),
- 'histogram': (items.Histogram,),
- }
- """Mapping kind to item classes of this kind"""
-
- @classmethod
- def _itemKind(cls, item):
- """Returns the "kind" of a given item
-
- :param Item item: The item get the kind
- :rtype: str
- """
- for kind, itemClasses in cls._KIND_TO_CLASSES.items():
- if isinstance(item, itemClasses):
- return kind
- raise ValueError('Unsupported item type %s' % type(item))
-
- def _notifyContentChanged(self, item):
- self.notify('contentChanged', action='add',
- kind=self._itemKind(item), legend=item.getName())
-
- 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
- # Put 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())
-
- def addItem(self, item=None, *args, **kwargs):
- """Add an item to the plot content.
-
- :param ~silx.gui.plot.items.Item item: The item to add.
- :raises ValueError: If item is already in the plot.
- """
- if not isinstance(item, items.Item):
- deprecated_warning(
- 'Function',
- 'addItem',
- replacement='addShape',
- since_version='0.13')
- if item is None and not args: # Only kwargs
- return self.addShape(**kwargs)
- else:
- return self.addShape(item, *args, **kwargs)
-
- assert not args and not kwargs
- if item in self.getItems():
- raise ValueError('Item already in the plot')
-
- # Add item to plot
- self._content[(item.getName(), self._itemKind(item))] = item
- item._setPlot(self)
- self._itemRequiresUpdate(item)
- if isinstance(item, items.DATA_ITEMS):
- self._invalidateDataRange() # TODO handle this automatically
-
- self._notifyContentChanged(item)
- self.sigItemAdded.emit(item)
-
- def removeItem(self, item):
- """Remove the item from the plot.
-
- :param ~silx.gui.plot.items.Item item: Item to remove from the plot.
- :raises ValueError: If item is not in the plot.
- """
- if not isinstance(item, items.Item): # Previous method usage
- deprecated_warning(
- 'Function',
- 'removeItem',
- replacement='remove(legend, kind="item")',
- since_version='0.13')
- if item is None:
- return
- self.remove(item, kind='item')
- return
-
- if item not in self.getItems():
- raise ValueError('Item not in the plot')
-
- self.sigItemAboutToBeRemoved.emit(item)
-
- kind = self._itemKind(item)
-
- 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((item.getName(), kind))
- 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._resetColorAndStyle()
-
- self.sigItemRemoved.emit(item)
-
- self.notify('contentChanged', action='remove',
- kind=kind, legend=item.getName())
-
- def discardItem(self, item) -> bool:
- """Remove the item from the plot.
-
- Same as :meth:`removeItem` but do not raise an exception.
-
- :param ~silx.gui.plot.items.Item item: Item to remove from the plot.
- :returns: True if the item was present, False otherwise.
- """
- try:
- self.removeItem(item)
- except ValueError:
- return False
- else:
- return True
-
- @deprecated(replacement='addItem', since_version='0.13')
- def _add(self, item):
- return self.addItem(item)
-
- @deprecated(replacement='removeItem', since_version='0.13')
- def _remove(self, item):
- return self.removeItem(item)
-
- def getItems(self):
- """Returns the list of items in the plot
-
- :rtype: List[silx.gui.plot.items.Item]
- """
- return tuple(self._content.values())
-
- @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,
- 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,
- baseline=None):
- """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.
- If x contains datetime objects the XAxis tickMode is set to
- TickMode.TIME_SERIES.
- :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 to delete already existing curves
- (the default is False)
- :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.
- :param baseline: curve baseline
- :type: Union[None,float,numpy.ndarray]
- :returns: The key string identify this curve
- """
- # 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
-
- if replace:
- self._resetColorAndStyle()
-
- # 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.setName(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)
- curve._setBaseline(baseline=baseline)
-
- # 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)
-
- # Convert x to timestamps so that the internal representation
- # remains floating points. The user is expected to set the axis'
- # tickMode to TickMode.TIME_SERIES and, if necessary, set the axis
- # to the correct time zone.
- if len(x) > 0 and isinstance(x[0], dt.datetime):
- x = [timestamp(d) for d in x]
-
- curve.setData(x, y, xerror, yerror, baseline=baseline, copy=copy)
-
- if replace: # Then remove all other curves
- for c in self.getAllCurves(withhidden=True):
- if c is not curve:
- self.removeItem(c)
-
- if mustBeAdded:
- self.addItem(curve)
- else:
- self._notifyContentChanged(curve)
-
- if wasActive:
- self.setActiveCurve(curve.getName())
- elif self.getActiveCurveSelectionMode() == "legacy":
- if self.getActiveCurve(just_legend=True) is None:
- if len(self.getAllCurves(just_legend=True,
- withhidden=False)) == 1:
- if curve.isVisible():
- self.setActiveCurve(curve.getName())
-
- 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,
- z=None,
- baseline=None):
- """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.
- :param int z: Layer on which to draw the histogram
- :param baseline: histogram baseline
- :type: Union[None,float,numpy.ndarray]
- :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.setName(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)
- if z is not None:
- histo.setZValue(z=z)
-
- # Set histogram data
- histo.setData(histogram=histogram, edges=edges, baseline=baseline,
- align=align, copy=copy)
-
- if mustBeAdded:
- self.addItem(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=False,
- z=None,
- selectable=None, draggable=None,
- colormap=None, pixmap=None,
- xlabel=None, ylabel=None,
- origin=None, scale=None,
- resetzoom=True, copy=True):
- """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 to delete already existing images (Default: False).
- :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: Colormap object to use (or None).
- This is ignored if data is a RGB(A) image.
- :type colormap: Union[~silx.gui.colors.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
- """
- 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.removeItem(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.setName(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.removeItem(img)
-
- if mustBeAdded:
- self.addItem(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 ~silx.gui.colors.Colormap colormap:
- Colormap object 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.setName(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.addItem(scatter)
- else:
- self._notifyContentChanged(scatter)
-
- scatters = [item for item in self.getItems()
- if isinstance(item, items.Scatter) and item.isVisible()]
- if len(scatters) == 1 or wasActive:
- self._setActiveItem('scatter', scatter.getName())
-
- return legend
-
- def addShape(self, xdata, ydata, legend=None, info=None,
- replace=False,
- shape="polygon", color='black', fill=True,
- overlay=False, z=None, linestyle="-", linewidth=1.0,
- linebgcolor=None):
- """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)
- :param str linestyle: Style of the line.
- Only relevant for line markers where X or Y is None.
- Value in:
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
- :param float linewidth: Width of the line.
- Only relevant for line markers where X or Y is None.
- :param str linebgcolor: Background color of the line, e.g., 'blue', 'b',
- '#FF0000'. It is used to draw dotted line using a second color.
- :returns: The key string identify this item
- """
- # expected to receive the same parameters as the signal
-
- 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.setName(legend)
- item.setInfo(info)
- item.setColor(color)
- item.setFill(fill)
- item.setOverlay(overlay)
- item.setZValue(z)
- item.setPoints(numpy.array((xdata, ydata)).T)
- item.setLineStyle(linestyle)
- item.setLineWidth(linewidth)
- item.setLineBgColor(linebgcolor)
-
- self.addItem(item)
-
- return legend
-
- def addXMarker(self, x, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- yaxis='left'):
- """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 x: Position of the marker on the X axis in data coordinates
- :type x: Union[None, float]
- :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.
- :param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
- :return: The key string identify this marker
- """
- return self._addMarker(x=x, y=None, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=None, constraint=constraint,
- yaxis=yaxis)
-
- def addYMarker(self, y,
- legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- yaxis='left'):
- """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.
- :param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
- :return: The key string identify this marker
- """
- return self._addMarker(x=None, y=y, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=None, constraint=constraint,
- yaxis=yaxis)
-
- def addMarker(self, x, y, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- symbol='+',
- constraint=None,
- yaxis='left'):
- """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.
- :param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
- :return: The key string identify this marker
- """
- 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,
- yaxis=yaxis)
-
- def _addMarker(self, x, y, legend,
- text, color,
- selectable, draggable,
- symbol, constraint,
- yaxis=None):
- """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 = [item.getName() for item in self.getItems()
- if isinstance(item, items.MarkerBase)]
- 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.removeItem(marker)
- marker = None
-
- mustBeAdded = marker is None
- if marker is None:
- # No previous marker, create one
- marker = markerClass()
- marker.setName(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)
- marker.setYAxis(yaxis)
-
- # 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.addItem(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):
- """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
- """
- 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 == '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 item in self.getItems():
- if (isinstance(item, self._KIND_TO_CLASSES[aKind]) and
- item.getPlot() is self): # Make sure item is still in the plot
- self.removeItem(item)
-
- 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.removeItem(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 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."""
- for item in self.getItems():
- if item.getPlot() is self: # Make sure item is still in the plot
- self.removeItem(item)
-
- 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.
-
- :rtype: bool
- """
- return self.getActiveCurveSelectionMode() != 'none'
-
- def setActiveCurveHandling(self, flag=True):
- """Enable/Disable active curve selection.
-
- :param bool flag: True to enable 'atmostone' active curve selection,
- False to disable active curve selection.
- """
- self.setActiveCurveSelectionMode('atmostone' if flag else 'none')
-
- def getActiveCurveStyle(self):
- """Returns the current style applied to active curve
-
- :rtype: CurveStyle
- """
- return self._activeCurveStyle
-
- def setActiveCurveStyle(self,
- color=None,
- linewidth=None,
- linestyle=None,
- symbol=None,
- symbolsize=None):
- """Set the style of active curve
-
- :param color: Color
- :param Union[str,None] linestyle: Style of the line
- :param Union[float,None] linewidth: Width of the line
- :param Union[str,None] symbol: Symbol of the markers
- :param Union[float,None] symbolsize: Size of the symbols
- """
- self._activeCurveStyle = CurveStyle(color=color,
- linewidth=linewidth,
- linestyle=linestyle,
- symbol=symbol,
- symbolsize=symbolsize)
- curve = self.getActiveCurve()
- if curve is not None:
- curve.setHighlightedStyle(self.getActiveCurveStyle())
-
- @deprecated(replacement="getActiveCurveStyle", since_version="0.9")
- def getActiveCurveColor(self):
- """Get the color used to display the currently active curve.
-
- See :meth:`setActiveCurveColor`.
- """
- return self._activeCurveStyle.getColor()
-
- @deprecated(replacement="setActiveCurveStyle", since_version="0.9")
- 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.setActiveCurveStyle(color=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):
- """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 not self.isActiveCurveHandling():
- return
- if legend is None and self.getActiveCurveSelectionMode() == "legacy":
- _logger.info(
- 'setActiveCurve(None) ignored due to active curve selection mode')
- return
-
- return self._setActiveItem(kind='curve', legend=legend)
-
- def setActiveCurveSelectionMode(self, mode):
- """Sets the current selection mode.
-
- :param str mode: The active curve selection mode to use.
- It can be: 'legacy', 'atmostone' or 'none'.
- """
- assert mode in ('legacy', 'atmostone', 'none')
-
- if mode != self._activeCurveSelectionMode:
- self._activeCurveSelectionMode = mode
- if mode == 'none': # reset active curve
- self._setActiveItem(kind='curve', legend=None)
-
- elif mode == 'legacy' and self.getActiveCurve() is None:
- # Select an active curve
- curves = self.getAllCurves(just_legend=False,
- withhidden=False)
- if len(curves) == 1:
- if curves[0].isVisible():
- self.setActiveCurve(curves[0].getName())
-
- def getActiveCurveSelectionMode(self):
- """Returns the current selection mode.
-
- It can be "atmostone", "legacy" or "none".
-
- :rtype: str
- """
- return self._activeCurveSelectionMode
-
- 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):
- """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.
- """
- return self._setActiveItem(kind='image', legend=legend)
-
- def getActiveScatter(self, just_legend=False):
- """Returns the currently active scatter.
-
- It returns None in case of not having an active scatter.
-
- :param bool just_legend: True to get the legend of the scatter,
- False (the default) to get the scatter data
- and info.
- :return: Active scatter's legend or corresponding scatter object
- :rtype: str, :class:`.items.Scatter` or None
- """
- return self._getActiveItem(kind='scatter', just_legend=just_legend)
-
- def setActiveScatter(self, legend):
- """Make the scatter associated to legend the active scatter.
-
- :param str legend: The legend associated to the scatter
- or None to have no active scatter.
- """
- return self._setActiveItem(kind='scatter', 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
-
- item = self._getItem(kind, self._activeLegend[kind])
- if item is None:
- return None
-
- return item.getName() if just_legend else item
-
- 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.setHighlightedStyle(self.getActiveCurveStyle())
- 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.getName()
- 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:
- kind = self._itemKind(item)
- self.notify(
- 'active' + kind[0].upper() + kind[1:] + 'Changed',
- updated=False,
- previous=item.getName(),
- legend=item.getName())
-
- # 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`
- """
- curves = [item for item in self.getItems() if
- isinstance(item, items.Curve) and
- (withhidden or item.isVisible())]
- return [curve.getName() for curve in curves] if just_legend else curves
-
- 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`
- """
- images = [item for item in self.getItems()
- if isinstance(item, items.ImageBase)]
- return [image.getName() for image in images] if just_legend else images
-
- 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)
-
- @deprecated(replacement='getItems', since_version='0.13')
- 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 == '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 item in self.getItems():
- type_ = self._itemKind(item)
- if type_ in kind and (withhidden or item.isVisible()):
- output.append(item.getName() 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
- itemClasses = self._KIND_TO_CLASSES[kind]
- allItems = [item for item in self.getItems()
- if isinstance(item, itemClasses) and item.isVisible()]
- 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):
- """Set the graph X (bottom) limits.
-
- :param float xmin: minimum bottom axis value
- :param float xmax: maximum bottom axis value
- """
- 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'):
- """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'
- """
- 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: bool):
- """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.
- """
- if displayed != self.__axesDisplayed:
- self.__axesDisplayed = displayed
- if displayed:
- self._backend.setAxesMargins(*self.__axesMargins)
- else:
- self._backend.setAxesMargins(0., 0., 0., 0.)
- self._setDirtyPlot()
- self._sigAxesVisibilityChanged.emit(displayed)
-
- def isAxesDisplayed(self) -> bool:
- """Returns whether or not axes are currently displayed
-
- :rtype: bool
- """
- return self.__axesDisplayed
-
- def setAxesMargins(
- self, left: float, top: float, right: float, bottom: float):
- """Set ratios of margins surrounding data plot area.
-
- All ratios must be within [0., 1.].
- Sums of ratios of opposed side must be < 1.
-
- :param float left: Left-side margin ratio.
- :param float top: Top margin ratio
- :param float right: Right-side margin ratio
- :param float bottom: Bottom margin ratio
- :raises ValueError:
- """
- for value in (left, top, right, bottom):
- if value < 0. or value > 1.:
- raise ValueError("Margin ratios must be within [0., 1.]")
- if left + right >= 1. or top + bottom >= 1.:
- raise ValueError("Sum of ratios of opposed sides >= 1")
- margins = left, top, right, bottom
-
- if margins != self.__axesMargins:
- self.__axesMargins = margins
- if self.isAxesDisplayed(): # Only apply if axes are displayed
- self._backend.setAxesMargins(*margins)
- self._setDirtyPlot()
-
- def getAxesMargins(self):
- """Returns ratio of margins surrounding data plot area.
-
- :return: (left, top, right, bottom)
- :rtype: List[float]
- """
- return self.__axesMargins
-
- 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)
- if flag == self.isKeepDataAspectRatio():
- return
- self._backend.setKeepDataAspectRatio(flag=flag)
- self._setDirtyPlot()
- self._forceResetZoom()
- 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 the default Curve symbol is set and False if not."""
- return self._defaultPlotPoints == silx.config.DEFAULT_PLOT_SYMBOL
-
- 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 = silx.config.DEFAULT_PLOT_SYMBOL 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`.
-
- :rtype: ~silx.gui.colors.Colormap
- """
- 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 ~silx.gui.colors.Colormap colormap:
- The description of the default colormap, or
- None to set the colormap to a linear
- autoscale gray colormap.
- """
- if colormap is None:
- colormap = Colormap(name=silx.config.DEFAULT_COLORMAP_NAME,
- 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 _resetColorAndStyle(self):
- self._colorIndex = 0
- self._styleIndex = 0
-
- 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 colors.rgba(color) == self.getActiveCurveStyle().getColor():
- 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 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 == 'setKeepDataAspectRatio':
- self.sigSetKeepDataAspectRatio.emit(kwargs['state'])
- elif event == 'setGraphGrid':
- self.sigSetGraphGrid.emit(kwargs['which'])
- elif event == 'setGraphCursor':
- self.sigSetGraphCursor.emit(kwargs['state'])
- elif event == 'contentChanged':
- self.sigContentChanged.emit(
- kwargs['action'], kwargs['kind'], kwargs['legend'])
- elif event == 'activeCurveChanged':
- self.sigActiveCurveChanged.emit(
- kwargs['previous'], kwargs['legend'])
- elif event == 'activeImageChanged':
- self.sigActiveImageChanged.emit(
- kwargs['previous'], kwargs['legend'])
- elif event == 'activeScatterChanged':
- self.sigActiveScatterChanged.emit(
- kwargs['previous'], kwargs['legend'])
- elif event == 'interactiveModeChanged':
- self.sigInteractiveModeChanged.emit(kwargs['source'])
-
- 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
- # allow register listener by event type
- if callbackFunction is None:
- callbackFunction = WeakMethodProxy(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'])
- qt.QToolTip.showText(self.cursor().pos(), ddict['label'])
- elif ddict['event'] == 'mouseClicked' and ddict['button'] == 'left':
- self.setActiveCurve(None)
-
- def saveGraph(self, filename, fileFormat=None, dpi=None):
- """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 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 _forceResetZoom(self, dataMargins=None):
- """Reset the plot limits to the bounds of the data and redraw the plot.
-
- This method forces a reset zoom and does not check axis autoscale.
-
- 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
-
- # 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 = ymin, ymax
- else:
- ymin2, ymax2 = ranges.yright
- if ranges.y is None:
- ymin, ymax = 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:]
- if plotWidth > 0 and plotHeight > 0:
- plotRatio = plotHeight / plotWidth
- 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)
-
- 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).
- """
- xLimits = self._xAxis.getLimits()
- yLimits = self._yAxis.getLimits()
- y2Limits = self._yRightAxis.getLimits()
-
- xAuto = self._xAxis.isAutoScale()
- yAuto = self._yAxis.isAutoScale()
-
- # With log axes, autoscale if limits are <= 0
- # This avoids issues with toggling log scale with matplotlib 2.1.0
- if self._xAxis.getScale() == self._xAxis.LOGARITHMIC and xLimits[0] <= 0:
- xAuto = True
- if self._yAxis.getScale() == self._yAxis.LOGARITHMIC and (yLimits[0] <= 0 or y2Limits[0] <= 0):
- yAuto = True
-
- if not xAuto and not yAuto:
- _logger.debug("Nothing to autoscale")
- else: # Some axes to autoscale
- self._forceResetZoom(dataMargins=dataMargins)
-
- # Restore limits for axis not in autoscale
- 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')
-
- 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")
-
- if x is None:
- x = self.width() // 2
- if y is None:
- y = self.height() // 2
-
- if check:
- left, top, width, height = self.getPlotBoundsInPixels()
- if not (left <= x <= left + width and top <= y <= top + height):
- return None
-
- return self._backend.pixelToData(x, y, axis)
-
- 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 getGraphCursorShape(self):
- """Returns the current cursor shape.
-
- :rtype: str
- """
- return self.__graphCursorShape
-
- def setGraphCursorShape(self, cursor=None):
- """Set the cursor shape.
-
- :param str cursor: Name of the cursor shape
- """
- self.__graphCursorShape = cursor
- self._backend.setGraphCursorShape(cursor)
-
- @deprecated(replacement='getItems', since_version='0.13')
- def _getAllMarkers(self, just_legend=False):
- markers = [item for item in self.getItems() if isinstance(item, items.MarkerBase)]
- if just_legend:
- return [marker.getName() for marker in markers]
- else:
- return markers
-
- def _getMarkerAt(self, x, y):
- """Return the most interactive marker at a location, else None
-
- :param float x: X position in pixels
- :param float y: Y position in pixels
- :rtype: None of marker object
- """
- def checkDraggable(item):
- return isinstance(item, items.MarkerBase) and item.isDraggable()
- def checkSelectable(item):
- return isinstance(item, items.MarkerBase) and item.isSelectable()
- def check(item):
- return isinstance(item, items.MarkerBase)
-
- result = self._pickTopMost(x, y, checkDraggable)
- if not result:
- result = self._pickTopMost(x, y, checkSelectable)
- if not result:
- result = self._pickTopMost(x, y, check)
- marker = result.getItem() if result is not None else None
- return marker
-
- 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 pickItems(self, x, y, condition=None):
- """Generator of picked items in the plot at given position.
-
- Items are returned from front to back.
-
- :param float x: X position in pixels
- :param float y: Y position in pixels
- :param callable condition:
- Callable taking an item as input and returning False for items
- to skip during picking. If None (default) no item is skipped.
- :return: Iterable of :class:`PickingResult` objects at picked position.
- Items are ordered from front to back.
- """
- for item in reversed(self._backend.getItemsFromBackToFront(condition=condition)):
- result = item.pick(x, y)
- if result is not None:
- yield result
-
- def _pickTopMost(self, x, y, condition=None):
- """Returns top-most picked item in the plot at given position.
-
- Items are checked from front to back.
-
- :param float x: X position in pixels
- :param float y: Y position in pixels
- :param callable condition:
- Callable taking an item as input and returning False for items
- to skip during picking. If None (default) no item is skipped.
- :return: :class:`PickingResult` object at picked position.
- If no item is picked, it returns None
- :rtype: Union[None,PickingResult]
- """
- for result in self.pickItems(x, y, condition):
- return result
- 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', 'select-draw', 'zoom'.
- It can also contains extra keys (e.g., 'color') specific to a mode
- as provided to :meth:`setInteractiveMode`.
- """
- return self._eventHandler.getInteractiveMode()
-
- def resetInteractiveMode(self):
- """Reset the interactive mode to use the previous basic interactive
- mode used.
-
- It can be one of "zoom" or "pan".
- """
- mode, zoomOnWheel = self._previousDefaultMode
- self.setInteractiveMode(mode=mode, zoomOnWheel=zoomOnWheel)
-
- 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', 'select-draw', '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
- if mode in ["pan", "zoom"]:
- self._previousDefaultMode = mode, zoomOnWheel
-
- self.notify(
- 'interactiveModeChanged', source=source)
-
- # Panning with arrow keys
-
- def isPanWithArrowKeys(self):
- """Returns whether or not panning the graph with arrow keys is enabled.
-
- See :meth:`setPanWithArrowKeys`.
- """
- return self._panWithArrowKeys
-
- def setPanWithArrowKeys(self, pan=False):
- """Enable/Disable panning the graph with arrow keys.
-
- This grabs the keyboard.
-
- :param bool pan: True to enable panning, False to disable.
- """
- pan = bool(pan)
- panHasChanged = self._panWithArrowKeys != pan
-
- self._panWithArrowKeys = pan
- if not self._panWithArrowKeys:
- self.setFocusPolicy(qt.Qt.NoFocus)
- else:
- self.setFocusPolicy(qt.Qt.StrongFocus)
- self.setFocus(qt.Qt.OtherFocusReason)
-
- if panHasChanged:
- self.sigSetPanWithArrowKeys.emit(pan)
-
- # Dict to convert Qt arrow key code to direction str.
- _ARROWS_TO_PAN_DIRECTION = {
- qt.Qt.Key_Left: 'left',
- qt.Qt.Key_Right: 'right',
- qt.Qt.Key_Up: 'up',
- qt.Qt.Key_Down: 'down'
- }
-
- def __simulateMouseMove(self):
- qapp = qt.QApplication.instance()
- event = qt.QMouseEvent(
- qt.QEvent.MouseMove,
- self.getWidgetHandle().mapFromGlobal(qt.QCursor.pos()),
- qt.Qt.NoButton,
- qapp.mouseButtons(),
- qapp.keyboardModifiers())
- qapp.sendEvent(self.getWidgetHandle(), event)
-
- def keyPressEvent(self, event):
- """Key event handler handling panning on arrow keys.
-
- Overrides base class implementation.
- """
- key = event.key()
- if self._panWithArrowKeys and key in self._ARROWS_TO_PAN_DIRECTION:
- self.pan(self._ARROWS_TO_PAN_DIRECTION[key], factor=0.1)
-
- # Send a mouse move event to the plot widget to take into account
- # that even if mouse didn't move on the screen, it moved relative
- # to the plotted data.
- self.__simulateMouseMove()
- else:
- # Only call base class implementation when key is not handled.
- # See QWidget.keyPressEvent for details.
- super(PlotWidget, self).keyPressEvent(event)
diff --git a/silx/gui/plot/PlotWindow.py b/silx/gui/plot/PlotWindow.py
deleted file mode 100644
index 3cd605f..0000000
--- a/silx/gui/plot/PlotWindow.py
+++ /dev/null
@@ -1,994 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""A :class:`.PlotWidget` with additional toolbars.
-
-The :class:`PlotWindow` is a subclass of :class:`.PlotWidget`.
-"""
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "12/04/2019"
-
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-import logging
-import weakref
-
-import silx
-from silx.utils.weakref import WeakMethodProxy
-from silx.utils.deprecation import deprecated
-from silx.utils.proxy import docstring
-
-from . import PlotWidget
-from . import actions
-from . import items
-from .actions import medfilt as actions_medfilt
-from .actions import fit as actions_fit
-from .actions import control as actions_control
-from .actions import histogram as actions_histogram
-from . import PlotToolButtons
-from . import tools
-from .Profile import ProfileToolBar
-from .LegendSelector import LegendsDockWidget
-from .CurvesROIWidget import CurvesROIDockWidget
-from .MaskToolsWidget import MaskToolsDockWidget
-from .StatsWidget import BasicStatsWidget
-from .ColorBar import ColorBarWidget
-try:
- from ..console import IPythonDockWidget
-except ImportError:
- IPythonDockWidget = None
-
-from .. import qt
-
-
-_logger = logging.getLogger(__name__)
-
-
-class PlotWindow(PlotWidget):
- """Qt Widget providing a 1D/2D plot area and additional tools.
-
- This widgets inherits from :class:`.PlotWidget` and provides its plot API.
-
- Initialiser parameters:
-
- :param parent: The parent of this widget or None.
- :param backend: The backend to use for the plot (default: matplotlib).
- 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.
- :param bool logScale: Toggle visibility of axes log scale actions.
- :param bool grid: Toggle visibility of grid mode action.
- :param bool curveStyle: Toggle visibility of curve style action.
- :param bool colormap: Toggle visibility of colormap action.
- :param bool aspectRatio: Toggle visibility of aspect ratio button.
- :param bool yInverted: Toggle visibility of Y axis direction button.
- :param bool copy: Toggle visibility of copy action.
- :param bool save: Toggle visibility of save action.
- :param bool print_: Toggle visibility of print action.
- :param bool control: True to display an Options button with a sub-menu
- to show legends, toggle crosshair and pan with arrows.
- (Default: False)
- :param position: True to display widget with (x, y) mouse position
- (Default: False).
- It also supports a list of (name, funct(x, y)->value)
- to customize the displayed values.
- See :class:`~silx.gui.plot.tools.PositionInfo`.
- :param bool roi: Toggle visibilty of ROI action.
- :param bool mask: Toggle visibilty of mask action.
- :param bool fit: Toggle visibilty of fit action.
- """
-
- def __init__(self, parent=None, backend=None,
- resetzoom=True, autoScale=True, logScale=True, grid=True,
- curveStyle=True, colormap=True,
- aspectRatio=True, yInverted=True,
- copy=True, save=True, print_=True,
- control=False, position=False,
- roi=True, mask=True, fit=False):
- super(PlotWindow, self).__init__(parent=parent, backend=backend)
- if parent is None:
- self.setWindowTitle('PlotWindow')
-
- self._dockWidgets = []
-
- # lazy loaded dock widgets
- self._legendsDockWidget = None
- self._curvesROIDockWidget = None
- self._maskToolsDockWidget = None
- self._consoleDockWidget = None
- self._statsDockWidget = None
-
- # Create color bar, hidden by default for backward compatibility
- self._colorbar = ColorBarWidget(parent=self, plot=self)
-
- # Init actions
- self.group = qt.QActionGroup(self)
- self.group.setExclusive(False)
-
- self.resetZoomAction = self.group.addAction(
- actions.control.ResetZoomAction(self, parent=self))
- self.resetZoomAction.setVisible(resetzoom)
- self.addAction(self.resetZoomAction)
-
- self.zoomInAction = actions.control.ZoomInAction(self, parent=self)
- self.addAction(self.zoomInAction)
-
- self.zoomOutAction = actions.control.ZoomOutAction(self, parent=self)
- self.addAction(self.zoomOutAction)
-
- self.xAxisAutoScaleAction = self.group.addAction(
- actions.control.XAxisAutoScaleAction(self, parent=self))
- self.xAxisAutoScaleAction.setVisible(autoScale)
- self.addAction(self.xAxisAutoScaleAction)
-
- self.yAxisAutoScaleAction = self.group.addAction(
- actions.control.YAxisAutoScaleAction(self, parent=self))
- self.yAxisAutoScaleAction.setVisible(autoScale)
- self.addAction(self.yAxisAutoScaleAction)
-
- self.xAxisLogarithmicAction = self.group.addAction(
- actions.control.XAxisLogarithmicAction(self, parent=self))
- self.xAxisLogarithmicAction.setVisible(logScale)
- self.addAction(self.xAxisLogarithmicAction)
-
- self.yAxisLogarithmicAction = self.group.addAction(
- actions.control.YAxisLogarithmicAction(self, parent=self))
- self.yAxisLogarithmicAction.setVisible(logScale)
- self.addAction(self.yAxisLogarithmicAction)
-
- self.gridAction = self.group.addAction(
- actions.control.GridAction(self, gridMode='both', parent=self))
- self.gridAction.setVisible(grid)
- self.addAction(self.gridAction)
-
- self.curveStyleAction = self.group.addAction(
- actions.control.CurveStyleAction(self, parent=self))
- self.curveStyleAction.setVisible(curveStyle)
- self.addAction(self.curveStyleAction)
-
- self.colormapAction = self.group.addAction(
- actions.control.ColormapAction(self, parent=self))
- self.colormapAction.setVisible(colormap)
- self.addAction(self.colormapAction)
-
- self.colorbarAction = self.group.addAction(
- actions_control.ColorBarAction(self, parent=self))
- self.colorbarAction.setVisible(False)
- self.addAction(self.colorbarAction)
- self._colorbar.setVisible(False)
-
- self.keepDataAspectRatioButton = PlotToolButtons.AspectToolButton(
- parent=self, plot=self)
- self.keepDataAspectRatioButton.setVisible(aspectRatio)
-
- self.yAxisInvertedButton = PlotToolButtons.YAxisOriginToolButton(
- parent=self, plot=self)
- self.yAxisInvertedButton.setVisible(yInverted)
-
- self.group.addAction(self.getRoiAction())
- self.getRoiAction().setVisible(roi)
-
- self.group.addAction(self.getMaskAction())
- self.getMaskAction().setVisible(mask)
-
- self._intensityHistoAction = self.group.addAction(
- actions_histogram.PixelIntensitiesHistoAction(self, parent=self))
- self._intensityHistoAction.setVisible(False)
-
- self._medianFilter2DAction = self.group.addAction(
- actions_medfilt.MedianFilter2DAction(self, parent=self))
- self._medianFilter2DAction.setVisible(False)
-
- self._medianFilter1DAction = self.group.addAction(
- actions_medfilt.MedianFilter1DAction(self, parent=self))
- self._medianFilter1DAction.setVisible(False)
-
- self.fitAction = self.group.addAction(actions_fit.FitAction(self, parent=self))
- self.fitAction.setVisible(fit)
- self.addAction(self.fitAction)
-
- # lazy loaded actions needed by the controlButton menu
- self._consoleAction = None
- self._statsAction = None
- self._panWithArrowKeysAction = None
- self._crosshairAction = None
-
- # Make colorbar background white
- self._colorbar.setAutoFillBackground(True)
- self._sigAxesVisibilityChanged.connect(self._updateColorBarBackground)
- self._updateColorBarBackground()
-
- if control: # Create control button only if requested
- self.controlButton = qt.QToolButton()
- self.controlButton.setText("Options")
- self.controlButton.setToolButtonStyle(qt.Qt.ToolButtonTextBesideIcon)
- self.controlButton.setAutoRaise(True)
- self.controlButton.setPopupMode(qt.QToolButton.InstantPopup)
- menu = qt.QMenu(self)
- menu.aboutToShow.connect(self._customControlButtonMenu)
- self.controlButton.setMenu(menu)
-
- self._positionWidget = None
- if position: # Add PositionInfo widget to the bottom of the plot
- if isinstance(position, abc.Iterable):
- # Use position as a set of converters
- converters = position
- else:
- converters = None
- self._positionWidget = tools.PositionInfo(
- plot=self, converters=converters)
- # Set a snapping mode that is consistent with legacy one
- self._positionWidget.setSnappingMode(
- tools.PositionInfo.SNAPPING_CROSSHAIR |
- tools.PositionInfo.SNAPPING_ACTIVE_ONLY |
- tools.PositionInfo.SNAPPING_SYMBOLS_ONLY |
- tools.PositionInfo.SNAPPING_CURVE |
- tools.PositionInfo.SNAPPING_SCATTER)
-
- self.__setCentralWidget()
-
- # Creating the toolbar also create actions for toolbuttons
- self._interactiveModeToolBar = tools.InteractiveModeToolBar(
- parent=self, plot=self)
- self.addToolBar(self._interactiveModeToolBar)
-
- self._toolbar = self._createToolBar(title='Plot', parent=self)
- self.addToolBar(self._toolbar)
-
- self._outputToolBar = tools.OutputToolBar(parent=self, plot=self)
- self._outputToolBar.getCopyAction().setVisible(copy)
- self._outputToolBar.getSaveAction().setVisible(save)
- self._outputToolBar.getPrintAction().setVisible(print_)
- self.addToolBar(self._outputToolBar)
-
- # Activate shortcuts in PlotWindow widget:
- for toolbar in (self._interactiveModeToolBar, self._outputToolBar):
- for action in toolbar.actions():
- self.addAction(action)
-
- def __setCentralWidget(self):
- """Set central widget to host plot backend, colorbar, and bottom bar"""
- 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(self)
- centralWidget.setLayout(gridLayout)
-
- if hasattr(self, "controlButton") or self._positionWidget is not None:
- hbox = qt.QHBoxLayout()
- hbox.setContentsMargins(0, 0, 0, 0)
-
- if hasattr(self, "controlButton"):
- hbox.addWidget(self.controlButton)
-
- if self._positionWidget is not None:
- hbox.addWidget(self._positionWidget)
-
- hbox.addStretch(1)
- bottomBar = qt.QWidget(centralWidget)
- bottomBar.setLayout(hbox)
-
- gridLayout.addWidget(bottomBar, 1, 0, 1, -1)
-
- self.setCentralWidget(centralWidget)
-
- @docstring(PlotWidget)
- def setBackend(self, backend):
- super(PlotWindow, self).setBackend(backend)
- self.__setCentralWidget() # Recreate PlotWindow's central widget
-
- @docstring(PlotWidget)
- def setBackgroundColor(self, color):
- super(PlotWindow, self).setBackgroundColor(color)
- self._updateColorBarBackground()
-
- @docstring(PlotWidget)
- def setDataBackgroundColor(self, color):
- super(PlotWindow, self).setDataBackgroundColor(color)
- self._updateColorBarBackground()
-
- @docstring(PlotWidget)
- def setForegroundColor(self, color):
- super(PlotWindow, self).setForegroundColor(color)
- self._updateColorBarBackground()
-
- def _updateColorBarBackground(self):
- """Update the colorbar background according to the state of the plot"""
- if self.isAxesDisplayed():
- color = self.getBackgroundColor()
- else:
- color = self.getDataBackgroundColor()
- if not color.isValid():
- # If no color defined, use the background one
- color = self.getBackgroundColor()
-
- foreground = self.getForegroundColor()
-
- palette = self._colorbar.palette()
- palette.setColor(qt.QPalette.Background, color)
- palette.setColor(qt.QPalette.Window, color)
- palette.setColor(qt.QPalette.WindowText, foreground)
- palette.setColor(qt.QPalette.Text, foreground)
- self._colorbar.setPalette(palette)
-
- def getInteractiveModeToolBar(self):
- """Returns QToolBar controlling interactive mode.
-
- :rtype: QToolBar
- """
- return self._interactiveModeToolBar
-
- def getOutputToolBar(self):
- """Returns QToolBar containing save, copy and print actions
-
- :rtype: QToolBar
- """
- return self._outputToolBar
-
- @property
- @deprecated(replacement="getPositionInfoWidget()", since_version="0.8.0")
- def positionWidget(self):
- return self.getPositionInfoWidget()
-
- def getPositionInfoWidget(self):
- """Returns the widget displaying current cursor position information
-
- :rtype: ~silx.gui.plot.tools.PositionInfo
- """
- return self._positionWidget
-
- def getSelectionMask(self):
- """Return the current mask handled by :attr:`maskToolsDockWidget`.
-
- :return: The array of the mask with dimension of the 'active' image.
- If there is no active image, an empty array is returned.
- :rtype: 2D numpy.ndarray of uint8
- """
- return self.getMaskToolsDockWidget().getSelectionMask()
-
- def setSelectionMask(self, mask):
- """Set the mask handled by :attr:`maskToolsDockWidget`.
-
- If the provided mask has not the same dimension as the 'active'
- image, it will by cropped or padded.
-
- :param mask: The array to use for the mask.
- :type mask: numpy.ndarray of uint8 of dimension 2, C-contiguous.
- Array of other types are converted.
- :return: True if success, False if failed
- """
- return bool(self.getMaskToolsDockWidget().setSelectionMask(mask))
-
- def _toggleConsoleVisibility(self, isChecked=False):
- """Create IPythonDockWidget if needed,
- show it or hide it."""
- # create widget if needed (first call)
- if self._consoleDockWidget is None:
- available_vars = {"plt": weakref.proxy(self)}
- banner = "The variable 'plt' is available. Use the 'whos' "
- banner += "and 'help(plt)' commands for more information.\n\n"
- self._consoleDockWidget = IPythonDockWidget(
- available_vars=available_vars,
- custom_banner=banner,
- parent=self)
- self.addTabbedDockWidget(self._consoleDockWidget)
- # self._consoleDockWidget.setVisible(True)
- self._consoleDockWidget.toggleViewAction().toggled.connect(
- self.getConsoleAction().setChecked)
-
- self._consoleDockWidget.setVisible(isChecked)
-
- def _toggleStatsVisibility(self, isChecked=False):
- self.getStatsWidget().parent().setVisible(isChecked)
-
- def _createToolBar(self, title, parent):
- """Create a QToolBar from the QAction of the PlotWindow.
-
- :param str title: The title of the QMenu
- :param qt.QWidget parent: See :class:`QToolBar`
- """
- toolbar = qt.QToolBar(title, parent)
-
- # Order widgets with actions
- objects = self.group.actions()
-
- # Add push buttons to list
- index = objects.index(self.colormapAction)
- objects.insert(index + 1, self.keepDataAspectRatioButton)
- objects.insert(index + 2, self.yAxisInvertedButton)
-
- for obj in objects:
- if isinstance(obj, qt.QAction):
- toolbar.addAction(obj)
- else:
- # Add action for toolbutton in order to allow changing
- # visibility (see doc QToolBar.addWidget doc)
- if obj is self.keepDataAspectRatioButton:
- self.keepDataAspectRatioAction = toolbar.addWidget(obj)
- elif obj is self.yAxisInvertedButton:
- self.yAxisInvertedAction = toolbar.addWidget(obj)
- else:
- raise RuntimeError()
- return toolbar
-
- def toolBar(self):
- """Return a QToolBar from the QAction of the PlotWindow.
- """
- return self._toolbar
-
- def menu(self, title='Plot', parent=None):
- """Return a QMenu from the QAction of the PlotWindow.
-
- :param str title: The title of the QMenu
- :param parent: See :class:`QMenu`
- """
- menu = qt.QMenu(title, parent)
- for action in self.group.actions():
- menu.addAction(action)
- return menu
-
- def _customControlButtonMenu(self):
- """Display Options button sub-menu."""
- controlMenu = self.controlButton.menu()
- controlMenu.clear()
- controlMenu.addAction(self.getLegendsDockWidget().toggleViewAction())
- controlMenu.addAction(self.getRoiAction())
- controlMenu.addAction(self.getStatsAction())
- controlMenu.addAction(self.getMaskAction())
- controlMenu.addAction(self.getConsoleAction())
-
- controlMenu.addSeparator()
- controlMenu.addAction(self.getCrosshairAction())
- controlMenu.addAction(self.getPanWithArrowKeysAction())
-
- def addTabbedDockWidget(self, dock_widget):
- """Add a dock widget as a new tab if there are already dock widgets
- in the plot. When the first tab is added, the area is chosen
- depending on the plot geometry:
- if the window is much wider than it is high, the right dock area
- is used, else the bottom dock area is used.
-
- :param dock_widget: Instance of :class:`QDockWidget` to be added.
- """
- if dock_widget not in self._dockWidgets:
- self._dockWidgets.append(dock_widget)
- if len(self._dockWidgets) == 1:
- # The first created dock widget must be added to a Widget area
- width = self.centralWidget().width()
- height = self.centralWidget().height()
- if width > (1.25 * height):
- area = qt.Qt.RightDockWidgetArea
- else:
- area = qt.Qt.BottomDockWidgetArea
- self.addDockWidget(area, dock_widget)
- else:
- # Other dock widgets are added as tabs to the same widget area
- self.tabifyDockWidget(self._dockWidgets[0],
- dock_widget)
-
- def removeDockWidget(self, dockwidget):
- """Removes the *dockwidget* from the main window layout and hides it.
-
- Note that the *dockwidget* is *not* deleted.
-
- :param QDockWidget dockwidget:
- """
- if dockwidget in self._dockWidgets:
- self._dockWidgets.remove(dockwidget)
- super(PlotWindow, self).removeDockWidget(dockwidget)
-
- def __handleFirstDockWidgetShow(self, visible):
- """Handle QDockWidget.visibilityChanged
-
- It calls :meth:`addTabbedDockWidget` for the `sender` widget.
- This allows to call `addTabbedDockWidget` lazily.
-
- It disconnect itself from the signal once done.
-
- :param bool visible:
- """
- if visible:
- dockWidget = self.sender()
- dockWidget.visibilityChanged.disconnect(
- self.__handleFirstDockWidgetShow)
- self.addTabbedDockWidget(dockWidget)
-
- def getColorBarWidget(self):
- """Returns the embedded :class:`ColorBarWidget` widget.
-
- :rtype: ColorBarWidget
- """
- return self._colorbar
-
- # getters for dock widgets
-
- def getLegendsDockWidget(self):
- """DockWidget with Legend panel"""
- if self._legendsDockWidget is None:
- self._legendsDockWidget = LegendsDockWidget(plot=self)
- self._legendsDockWidget.hide()
- self._legendsDockWidget.visibilityChanged.connect(
- self.__handleFirstDockWidgetShow)
- return self._legendsDockWidget
-
- def getCurvesRoiDockWidget(self):
- # Undocumented for a "soft deprecation" in version 0.7.0
- # (still used internally for lazy loading)
- if self._curvesROIDockWidget is None:
- self._curvesROIDockWidget = CurvesROIDockWidget(
- plot=self, name='Regions Of Interest')
- self._curvesROIDockWidget.hide()
- self._curvesROIDockWidget.visibilityChanged.connect(
- self.__handleFirstDockWidgetShow)
- return self._curvesROIDockWidget
-
- def getCurvesRoiWidget(self):
- """Return the :class:`CurvesROIWidget`.
-
- :class:`silx.gui.plot.CurvesROIWidget.CurvesROIWidget` offers a getter
- and a setter for the ROI data:
-
- - :meth:`CurvesROIWidget.getRois`
- - :meth:`CurvesROIWidget.setRois`
- """
- return self.getCurvesRoiDockWidget().roiWidget
-
- def getMaskToolsDockWidget(self):
- """DockWidget with image mask panel (lazy-loaded)."""
- if self._maskToolsDockWidget is None:
- self._maskToolsDockWidget = MaskToolsDockWidget(
- plot=self, name='Mask')
- self._maskToolsDockWidget.hide()
- self._maskToolsDockWidget.visibilityChanged.connect(
- self.__handleFirstDockWidgetShow)
- return self._maskToolsDockWidget
-
- def getStatsWidget(self):
- """Returns a BasicStatsWidget connected to this plot
-
- :rtype: BasicStatsWidget
- """
- if self._statsDockWidget is None:
- self._statsDockWidget = qt.QDockWidget()
- self._statsDockWidget.setWindowTitle("Curves stats")
- self._statsDockWidget.layout().setContentsMargins(0, 0, 0, 0)
- statsWidget = BasicStatsWidget(parent=self, plot=self)
- self._statsDockWidget.setWidget(statsWidget)
- statsWidget.sigVisibilityChanged.connect(
- self.getStatsAction().setChecked)
- self._statsDockWidget.hide()
- self._statsDockWidget.visibilityChanged.connect(
- self.__handleFirstDockWidgetShow)
- return self._statsDockWidget.widget()
-
- # getters for actions
- @property
- @deprecated(replacement="getInteractiveModeToolBar().getZoomModeAction()",
- since_version="0.8.0")
- def zoomModeAction(self):
- return self.getInteractiveModeToolBar().getZoomModeAction()
-
- @property
- @deprecated(replacement="getInteractiveModeToolBar().getPanModeAction()",
- since_version="0.8.0")
- def panModeAction(self):
- return self.getInteractiveModeToolBar().getPanModeAction()
-
- def getConsoleAction(self):
- """QAction handling the IPython console activation.
-
- By default, it is connected to a method that initializes the
- console widget the first time the user clicks the "Console" menu
- button. The following clicks, after initialization is done,
- will toggle the visibility of the console widget.
-
- :rtype: QAction
- """
- if self._consoleAction is None:
- self._consoleAction = qt.QAction('Console', self)
- self._consoleAction.setCheckable(True)
- if IPythonDockWidget is not None:
- self._consoleAction.toggled.connect(self._toggleConsoleVisibility)
- else:
- self._consoleAction.setEnabled(False)
- return self._consoleAction
-
- def getCrosshairAction(self):
- """Action toggling crosshair cursor mode.
-
- :rtype: actions.PlotAction
- """
- if self._crosshairAction is None:
- self._crosshairAction = actions.control.CrosshairAction(self, color='red')
- return self._crosshairAction
-
- def getMaskAction(self):
- """QAction toggling image mask dock widget
-
- :rtype: QAction
- """
- return self.getMaskToolsDockWidget().toggleViewAction()
-
- def getPanWithArrowKeysAction(self):
- """Action toggling pan with arrow keys.
-
- :rtype: actions.PlotAction
- """
- if self._panWithArrowKeysAction is None:
- self._panWithArrowKeysAction = actions.control.PanWithArrowKeysAction(self)
- return self._panWithArrowKeysAction
-
- def getStatsAction(self):
- if self._statsAction is None:
- self._statsAction = qt.QAction('Curves stats', self)
- self._statsAction.setCheckable(True)
- self._statsAction.setChecked(self.getStatsWidget().parent().isVisible())
- self._statsAction.toggled.connect(self._toggleStatsVisibility)
- return self._statsAction
-
- def getRoiAction(self):
- """QAction toggling curve ROI dock widget
-
- :rtype: QAction
- """
- return self.getCurvesRoiDockWidget().toggleViewAction()
-
- def getResetZoomAction(self):
- """Action resetting the zoom
-
- :rtype: actions.PlotAction
- """
- return self.resetZoomAction
-
- def getZoomInAction(self):
- """Action to zoom in
-
- :rtype: actions.PlotAction
- """
- return self.zoomInAction
-
- def getZoomOutAction(self):
- """Action to zoom out
-
- :rtype: actions.PlotAction
- """
- return self.zoomOutAction
-
- def getXAxisAutoScaleAction(self):
- """Action to toggle the X axis autoscale on zoom reset
-
- :rtype: actions.PlotAction
- """
- return self.xAxisAutoScaleAction
-
- def getYAxisAutoScaleAction(self):
- """Action to toggle the Y axis autoscale on zoom reset
-
- :rtype: actions.PlotAction
- """
- return self.yAxisAutoScaleAction
-
- def getXAxisLogarithmicAction(self):
- """Action to toggle logarithmic X axis
-
- :rtype: actions.PlotAction
- """
- return self.xAxisLogarithmicAction
-
- def getYAxisLogarithmicAction(self):
- """Action to toggle logarithmic Y axis
-
- :rtype: actions.PlotAction
- """
- return self.yAxisLogarithmicAction
-
- def getGridAction(self):
- """Action to toggle the grid visibility in the plot
-
- :rtype: actions.PlotAction
- """
- return self.gridAction
-
- def getCurveStyleAction(self):
- """Action to change curve line and markers styles
-
- :rtype: actions.PlotAction
- """
- return self.curveStyleAction
-
- def getColormapAction(self):
- """Action open a colormap dialog to change active image
- and default colormap.
-
- :rtype: actions.PlotAction
- """
- return self.colormapAction
-
- def getKeepDataAspectRatioButton(self):
- """Button to toggle aspect ratio preservation
-
- :rtype: PlotToolButtons.AspectToolButton
- """
- return self.keepDataAspectRatioButton
-
- def getKeepDataAspectRatioAction(self):
- """Action associated to keepDataAspectRatioButton.
- Use this to change the visibility of keepDataAspectRatioButton in the
- toolbar (See :meth:`QToolBar.addWidget` documentation).
-
- :rtype: actions.PlotAction
- """
- return self.keepDataAspectRatioButton
-
- def getYAxisInvertedButton(self):
- """Button to switch the Y axis orientation
-
- :rtype: PlotToolButtons.YAxisOriginToolButton
- """
- return self.yAxisInvertedButton
-
- def getYAxisInvertedAction(self):
- """Action associated to yAxisInvertedButton.
- Use this to change the visibility yAxisInvertedButton in the toolbar.
- (See :meth:`QToolBar.addWidget` documentation).
-
- :rtype: actions.PlotAction
- """
- return self.yAxisInvertedAction
-
- def getIntensityHistogramAction(self):
- """Action toggling the histogram intensity Plot widget
-
- :rtype: actions.PlotAction
- """
- return self._intensityHistoAction
-
- def getCopyAction(self):
- """Action to copy plot snapshot to clipboard
-
- :rtype: actions.PlotAction
- """
- return self.getOutputToolBar().getCopyAction()
-
- def getSaveAction(self):
- """Action to save plot
-
- :rtype: actions.PlotAction
- """
- return self.getOutputToolBar().getSaveAction()
-
- def getPrintAction(self):
- """Action to print plot
-
- :rtype: actions.PlotAction
- """
- return self.getOutputToolBar().getPrintAction()
-
- def getFitAction(self):
- """Action to fit selected curve
-
- :rtype: actions.PlotAction
- """
- return self.fitAction
-
- def getMedianFilter1DAction(self):
- """Action toggling the 1D median filter
-
- :rtype: actions.PlotAction
- """
- return self._medianFilter1DAction
-
- def getMedianFilter2DAction(self):
- """Action toggling the 2D median filter
-
- :rtype: actions.PlotAction
- """
- return self._medianFilter2DAction
-
- def getColorBarAction(self):
- """Action toggling the colorbar show/hide action
-
- .. warning:: to show/hide the plot colorbar call directly the ColorBar
- widget using getColorBarWidget()
-
- :rtype: actions.PlotAction
- """
- return self.colorbarAction
-
-
-class Plot1D(PlotWindow):
- """PlotWindow with tools specific for curves.
-
- This widgets provides the plot API of :class:`.PlotWidget`.
-
- :param parent: The parent of this widget
- :param backend: The backend to use for the plot (default: matplotlib).
- See :class:`.PlotWidget` for the list of supported backend.
- :type backend: str or :class:`BackendBase.BackendBase`
- """
-
- def __init__(self, parent=None, backend=None):
- super(Plot1D, self).__init__(parent=parent, backend=backend,
- resetzoom=True, autoScale=True,
- logScale=True, grid=True,
- curveStyle=True, colormap=False,
- aspectRatio=False, yInverted=False,
- copy=True, save=True, print_=True,
- control=True, position=True,
- roi=True, mask=False, fit=True)
- if parent is None:
- self.setWindowTitle('Plot1D')
- self.getXAxis().setLabel('X')
- self.getYAxis().setLabel('Y')
- action = self.getFitAction()
- action.setXRangeUpdatedOnZoom(True)
- action.setFittedItemUpdatedFromActiveCurve(True)
-
-
-class Plot2D(PlotWindow):
- """PlotWindow with a toolbar specific for images.
-
- This widgets provides the plot API of :~:`.PlotWidget`.
-
- :param parent: The parent of this widget
- :param backend: The backend to use for the plot (default: matplotlib).
- See :class:`.PlotWidget` for the list of supported backend.
- :type backend: str or :class:`BackendBase.BackendBase`
- """
-
- def __init__(self, parent=None, backend=None):
- # List of information to display at the bottom of the plot
- posInfo = [
- ('X', lambda x, y: x),
- ('Y', lambda x, y: y),
- ('Data', WeakMethodProxy(self._getImageValue)),
- ('Dims', WeakMethodProxy(self._getImageDims)),
- ]
-
- super(Plot2D, self).__init__(parent=parent, backend=backend,
- resetzoom=True, autoScale=False,
- logScale=False, grid=False,
- curveStyle=False, colormap=True,
- aspectRatio=True, yInverted=True,
- copy=True, save=True, print_=True,
- control=False, position=posInfo,
- roi=False, mask=True)
- if parent is None:
- self.setWindowTitle('Plot2D')
- self.getXAxis().setLabel('Columns')
- self.getYAxis().setLabel('Rows')
-
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
- self.getYAxis().setInverted(True)
-
- self.profile = ProfileToolBar(plot=self)
- self.addToolBar(self.profile)
-
- self.colorbarAction.setVisible(True)
- self.getColorBarWidget().setVisible(True)
-
- # Put colorbar action after colormap action
- actions = self.toolBar().actions()
- for action in actions:
- if action is self.getColormapAction():
- break
-
- self.sigActiveImageChanged.connect(self.__activeImageChanged)
-
- def __activeImageChanged(self, previous, legend):
- """Handle change of active image
-
- :param Union[str,None] previous: Legend of previous active image
- :param Union[str,None] legend: Legend of current active image
- """
- if previous is not None:
- item = self.getImage(previous)
- if item is not None:
- item.sigItemChanged.disconnect(self.__imageChanged)
-
- if legend is not None:
- item = self.getImage(legend)
- item.sigItemChanged.connect(self.__imageChanged)
-
- positionInfo = self.getPositionInfoWidget()
- if positionInfo is not None:
- positionInfo.updateInfo()
-
- def __imageChanged(self, event):
- """Handle update of active image item
-
- :param event: Type of changed event
- """
- if event == items.ItemChangedType.DATA:
- positionInfo = self.getPositionInfoWidget()
- if positionInfo is not None:
- positionInfo.updateInfo()
-
- def _getImageValue(self, 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 '-'
- """
- pickedMask = None
- for picked in self.pickItems(
- *self.dataToPixel(x, y, check=False),
- lambda item: isinstance(item, items.ImageBase)):
- if isinstance(picked.getItem(), items.MaskImageData):
- if pickedMask is None: # Use top-most if many masks
- pickedMask = picked
- else:
- image = picked.getItem()
-
- indices = picked.getIndices(copy=False)
- if indices is not None:
- row, col = indices[0][0], indices[1][0]
- value = image.getData(copy=False)[row, col]
-
- if pickedMask is not None: # Check if masked
- maskItem = pickedMask.getItem()
- indices = pickedMask.getIndices()
- row, col = indices[0][0], indices[1][0]
- if maskItem.getData(copy=False)[row, col] != 0:
- return value, "Masked"
- return value
-
- return '-' # No image picked
-
- def _getImageDims(self, *args):
- activeImage = self.getActiveImage()
- if (activeImage is not None and
- activeImage.getData(copy=False) is not None):
- dims = activeImage.getData(copy=False).shape[1::-1]
- return 'x'.join(str(dim) for dim in dims)
- else:
- return '-'
-
- def getProfileToolbar(self):
- """Profile tools attached to this plot
-
- See :class:`silx.gui.plot.Profile.ProfileToolBar`
- """
- return self.profile
-
- @deprecated(replacement="getProfilePlot", since_version="0.5.0")
- def getProfileWindow(self):
- return self.getProfilePlot()
-
- def getProfilePlot(self):
- """Return plot window used to display profile curve.
-
- :return: :class:`Plot1D`
- """
- return self.profile.getProfilePlot()
diff --git a/silx/gui/plot/PrintPreviewToolButton.py b/silx/gui/plot/PrintPreviewToolButton.py
deleted file mode 100644
index d857c18..0000000
--- a/silx/gui/plot/PrintPreviewToolButton.py
+++ /dev/null
@@ -1,392 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2018 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
-from silx.utils.deprecation import deprecated
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "20/12/2018"
-
-_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'))
- 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'))
- 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 getTitle(self):
- """Implement this method to fetch the title in the plot.
-
- :return: Title to be printed above the plot, or None (no title added)
- :rtype: str or None
- """
- return None
-
- def getCommentAndPosition(self):
- """Implement this method to fetch the legend to be printed below the
- figure and its position.
-
- :return: Legend to be printed below the figure and its position:
- "CENTER", "LEFT" or "RIGHT"
- :rtype: (str, str) or (None, None)
- """
- return None, None
-
- @property
- @deprecated(since_version="0.10",
- replacement="getPlot()")
- def plot(self):
- return self._plot
-
- def getPlot(self):
- """Return the :class:`.PlotWidget` associated with this tool button.
-
- :rtype: :class:`.PlotWidget`
- """
- return self._plot
-
- 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."""
- if not self.printPreviewDialog.ensurePrinterIsSet():
- return
-
- comment, commentPosition = self.getCommentAndPosition()
-
- if qt.HAS_SVG:
- svgRenderer, viewBox = self._getSvgRendererAndViewbox()
- self.printPreviewDialog.addSvgItem(svgRenderer,
- title=self.getTitle(),
- comment=comment,
- commentPosition=commentPosition,
- viewBox=viewBox,
- keepRatio=self._printGeometry["keepAspectRatio"])
- 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,
- title=self.getTitle(),
- comment=comment,
- commentPosition=commentPosition)
- 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
deleted file mode 100644
index 7565155..0000000
--- a/silx/gui/plot/Profile.py
+++ /dev/null
@@ -1,352 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2021 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.
-#
-# ###########################################################################*/
-"""Utility functions, toolbars and actions to create profile on images
-and stacks of images"""
-
-
-__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel", "H. Payno"]
-__license__ = "MIT"
-__date__ = "12/04/2019"
-
-
-import weakref
-
-from .. import qt
-from . import actions
-from .tools.profile import core
-from .tools.profile import manager
-from .tools.profile import rois
-from silx.gui.widgets.MultiModeAction import MultiModeAction
-
-from silx.utils.deprecation import deprecated
-from silx.utils.deprecation import deprecated_warning
-from .tools import roi as roi_mdl
-from silx.gui.plot import items
-
-
-@deprecated(replacement="silx.gui.plot.tools.profile.createProfile", since_version="0.13.0")
-def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
- return core.createProfile(roiInfo, currentData, origin,
- scale, lineWidth, method)
-
-
-class _CustomProfileManager(manager.ProfileManager):
- """This custom profile manager uses a single predefined profile window
- if it is specified. Else the behavior is the same as the default
- ProfileManager """
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.__profileWindow = None
- self.__specializedProfileWindows = {}
-
- def setSpecializedProfileWindow(self, roiClass, profileWindow):
- """Set a profile window for a given class or ROI.
-
- Setting profileWindow to None removes the roiClass from the list.
-
- :param roiClass:
- :param profileWindow:
- """
- if profileWindow is None:
- self.__specializedProfileWindows.pop(roiClass, None)
- else:
- self.__specializedProfileWindows[roiClass] = profileWindow
-
- def setProfileWindow(self, profileWindow):
- self.__profileWindow = profileWindow
-
- def createProfileWindow(self, plot, roi):
- for roiClass, specializedProfileWindow in self.__specializedProfileWindows.items():
- if isinstance(roi, roiClass):
- return specializedProfileWindow
-
- if self.__profileWindow is not None:
- return self.__profileWindow
- else:
- return super(_CustomProfileManager, self).createProfileWindow(plot, roi)
-
- def clearProfileWindow(self, profileWindow):
- for specializedProfileWindow in self.__specializedProfileWindows.values():
- if profileWindow is specializedProfileWindow:
- profileWindow.setProfile(None)
- return
-
- if self.__profileWindow is not None:
- self.__profileWindow.setProfile(None)
- else:
- return super(_CustomProfileManager, self).clearProfileWindow(profileWindow)
-
-
-class ProfileToolBar(qt.QToolBar):
- """QToolBar providing profile tools operating on a :class:`PlotWindow`.
-
- Attributes:
-
- - plot: Associated :class:`PlotWindow` on which the profile line is drawn.
- - actionGroup: :class:`QActionGroup` of available actions.
-
- To run the following sample code, a QApplication must be initialized.
- First, create a PlotWindow and add a :class:`ProfileToolBar`.
-
- >>> from silx.gui.plot import PlotWindow
- >>> from silx.gui.plot.Profile import ProfileToolBar
-
- >>> plot = PlotWindow() # Create a PlotWindow
- >>> toolBar = ProfileToolBar(plot=plot) # Create a profile toolbar
- >>> plot.addToolBar(toolBar) # Add it to plot
- >>> plot.show() # To display the PlotWindow with the profile toolbar
-
- :param plot: :class:`PlotWindow` instance on which to operate.
- :param profileWindow: Plot widget instance where to
- display the profile curve or None to create one.
- :param str title: See :class:`QToolBar`.
- :param parent: See :class:`QToolBar`.
- """
-
- def __init__(self, parent=None, plot=None, profileWindow=None,
- title=None):
- super(ProfileToolBar, self).__init__(title, parent)
- assert plot is not None
-
- if title is not None:
- deprecated_warning("Attribute",
- name="title",
- reason="removed",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
-
- self._plotRef = weakref.ref(plot)
-
- # If a profileWindow is defined,
- # It will be used to display all the profiles
- self._manager = self.createProfileManager(self, plot)
- self._manager.setProfileWindow(profileWindow)
- self._manager.setDefaultColorFromCursorColor(True)
- self._manager.setItemType(image=True)
- self._manager.setActiveItemTracking(True)
-
- # Actions
- self._browseAction = actions.mode.ZoomModeAction(plot, parent=self)
- self._browseAction.setVisible(False)
- self.freeLineAction = None
- self._createProfileActions()
- self._editor = self._manager.createEditorAction(self)
-
- # ActionGroup
- self.actionGroup = qt.QActionGroup(self)
- self.actionGroup.addAction(self._browseAction)
- self.actionGroup.addAction(self.hLineAction)
- self.actionGroup.addAction(self.vLineAction)
- self.actionGroup.addAction(self.lineAction)
- self.actionGroup.addAction(self._editor)
-
- modes = MultiModeAction(self)
- modes.addAction(self.hLineAction)
- modes.addAction(self.vLineAction)
- modes.addAction(self.lineAction)
- if self.freeLineAction is not None:
- modes.addAction(self.freeLineAction)
- modes.addAction(self.crossAction)
- self.__multiAction = modes
-
- # Add actions to ToolBar
- self.addAction(self._browseAction)
- self.addAction(modes)
- self.addAction(self._editor)
- self.addAction(self.clearAction)
-
- plot.sigActiveImageChanged.connect(self._activeImageChanged)
- self._activeImageChanged()
-
- def createProfileManager(self, parent, plot):
- return _CustomProfileManager(parent, plot)
-
- def _createProfileActions(self):
- self.hLineAction = self._manager.createProfileAction(rois.ProfileImageHorizontalLineROI, self)
- self.vLineAction = self._manager.createProfileAction(rois.ProfileImageVerticalLineROI, self)
- self.lineAction = self._manager.createProfileAction(rois.ProfileImageLineROI, self)
- self.freeLineAction = self._manager.createProfileAction(rois.ProfileImageDirectedLineROI, self)
- self.crossAction = self._manager.createProfileAction(rois.ProfileImageCrossROI, self)
- self.clearAction = self._manager.createClearAction(self)
-
- def getPlotWidget(self):
- """The :class:`.PlotWidget` associated to the toolbar."""
- return self._plotRef()
-
- @property
- @deprecated(since_version="0.13.0", replacement="getPlotWidget()")
- def plot(self):
- return self.getPlotWidget()
-
- def _setRoiActionEnabled(self, itemKind, enabled):
- for action in self.__multiAction.getMenu().actions():
- if not isinstance(action, roi_mdl.CreateRoiModeAction):
- continue
- roiClass = action.getRoiClass()
- if issubclass(itemKind, roiClass.ITEM_KIND):
- action.setEnabled(enabled)
-
- def _activeImageChanged(self, previous=None, legend=None):
- """Handle active image change to toggle actions"""
- if legend is None:
- self._setRoiActionEnabled(items.ImageStack, False)
- self._setRoiActionEnabled(items.ImageBase, False)
- else:
- plot = self.getPlotWidget()
- image = plot.getActiveImage()
- # Disable for empty image
- enabled = image.getData(copy=False).size > 0
- self._setRoiActionEnabled(type(image), enabled)
-
- @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()
-
- def getProfileManager(self):
- """Return the manager of the profiles.
-
- :rtype: ProfileManager
- """
- return self._manager
-
- @deprecated(since_version="0.13.0")
- def getProfilePlot(self):
- """Return plot widget in which the profile curve or the
- profile image is plotted.
- """
- window = self.getProfileMainWindow()
- if window is None:
- return None
- return window.getCurrentPlotWidget()
-
- @deprecated(replacement="getProfileManager().getCurrentRoi().getProfileWindow()", since_version="0.13.0")
- def getProfileMainWindow(self):
- """Return window containing the profile curve widget.
-
- This can return None if no profile was computed.
- """
- roi = self._manager.getCurrentRoi()
- if roi is None:
- return None
- return roi.getProfileWindow()
-
- @property
- @deprecated(since_version="0.13.0")
- def overlayColor(self):
- """This method does nothing anymore. But could be implemented if needed.
-
- It was used to set color to use for the ROI.
-
- If set to None (the default), the overlay color is adapted to the
- active image colormap and changes if the active image colormap changes.
- """
- pass
-
- @overlayColor.setter
- @deprecated(since_version="0.13.0")
- def overlayColor(self, color):
- """This method does nothing anymore. But could be implemented if needed.
- """
- pass
-
- def clearProfile(self):
- """Remove profile curve and profile area."""
- self._manager.clearProfile()
-
- @deprecated(since_version="0.13.0")
- def updateProfile(self):
- """This method does nothing anymore. But could be implemented if needed.
-
- It was used to update the displayed profile and profile ROI.
-
- This uses the current active image of the plot and the current ROI.
- """
- pass
-
- @deprecated(replacement="clearProfile()", since_version="0.13.0")
- def hideProfileWindow(self):
- """Hide profile window.
- """
- self.clearProfile()
-
- @deprecated(since_version="0.13.0")
- def setProfileMethod(self, method):
- assert method in ('sum', 'mean')
- roi = self._manager.getCurrentRoi()
- if roi is None:
- raise RuntimeError("No profile ROI selected")
- roi.setProfileMethod(method)
-
- @deprecated(since_version="0.13.0")
- def getProfileMethod(self):
- roi = self._manager.getCurrentRoi()
- if roi is None:
- raise RuntimeError("No profile ROI selected")
- return roi.getProfileMethod()
-
- @deprecated(since_version="0.13.0")
- def getProfileOptionToolAction(self):
- return self._editor
-
-
-class Profile3DToolBar(ProfileToolBar):
- def __init__(self, parent=None, stackview=None,
- title=None):
- """QToolBar providing profile tools for an image or a stack of images.
-
- :param parent: the parent QWidget
- :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=stackview.getPlotWidget())
-
- if title is not None:
- deprecated_warning("Attribute",
- name="title",
- reason="removed",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
-
- self.stackView = stackview
- """:class:`StackView` instance"""
-
- def _createProfileActions(self):
- self.hLineAction = self._manager.createProfileAction(rois.ProfileImageStackHorizontalLineROI, self)
- self.vLineAction = self._manager.createProfileAction(rois.ProfileImageStackVerticalLineROI, self)
- self.lineAction = self._manager.createProfileAction(rois.ProfileImageStackLineROI, self)
- self.crossAction = self._manager.createProfileAction(rois.ProfileImageStackCrossROI, self)
- self.clearAction = self._manager.createClearAction(self)
diff --git a/silx/gui/plot/ProfileMainWindow.py b/silx/gui/plot/ProfileMainWindow.py
deleted file mode 100644
index ce56cfd..0000000
--- a/silx/gui/plot/ProfileMainWindow.py
+++ /dev/null
@@ -1,110 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 contains a QMainWindow class used to display profile plots.
-"""
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "21/02/2017"
-
-import silx.utils.deprecation
-from silx.gui import qt
-from .tools.profile.manager import ProfileWindow
-
-silx.utils.deprecation.deprecated_warning("Module",
- name="silx.gui.plot.ProfileMainWindow",
- reason="moved",
- replacement="silx.gui.plot.tools.profile.manager.ProfileWindow",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
-
-class ProfileMainWindow(ProfileWindow):
- """QMainWindow providing 2 plot widgets specialized in
- 1D and 2D plotting, with different toolbars.
-
- Only one of the plots is visible at any given time.
-
- :param qt.QWidget parent: The parent of this widget or None (default).
- :param Union[str,Class] backend: The backend to use, in:
- 'matplotlib' (default), 'mpl', 'opengl', 'gl', 'none'
- or a :class:`BackendBase.BackendBase` class
- """
-
- sigProfileDimensionsChanged = qt.Signal(int)
- """This signal is emitted when :meth:`setProfileDimensions` is called.
- It carries the number of dimensions for the profile data (1 or 2).
- It can be used to be notified that the profile plot widget has changed.
-
- Note: This signal should be removed.
- """
-
- sigProfileMethodChanged = qt.Signal(str)
- """Emitted when the method to compute the profile changed (for now can be
- sum or mean)
-
- Note: This signal should be removed.
- """
-
- def __init__(self, parent=None, backend=None):
- ProfileWindow.__init__(self, parent=parent, backend=backend)
- # by default, profile is assumed to be a 1D curve
- self._profileType = None
-
- def setProfileType(self, profileType):
- """Set which profile plot widget (1D or 2D) is to be used
-
- Note: This method should be removed.
-
- :param str profileType: Type of profile data,
- "1D" for a curve or "2D" for an image
- """
- self._profileType = profileType
- if self._profileType == "1D":
- self._showPlot1D()
- elif self._profileType == "2D":
- self._showPlot2D()
- else:
- raise ValueError("Profile type must be '1D' or '2D'")
- self.sigProfileDimensionsChanged.emit(profileType)
-
- def getPlot(self):
- """Return the profile plot widget which is currently in use.
- This can be the 2D profile plot or the 1D profile plot.
-
- Note: This method should be removed.
- """
- return self.getCurrentPlotWidget()
-
- def setProfileMethod(self, method):
- """
- Note: This method should be removed.
-
- :param str method: method to manage the 'width' in the profile
- (computing mean or sum).
- """
- assert method in ('sum', 'mean')
- self._method = method
- self.sigProfileMethodChanged.emit(self._method)
diff --git a/silx/gui/plot/ROIStatsWidget.py b/silx/gui/plot/ROIStatsWidget.py
deleted file mode 100644
index 094d66a..0000000
--- a/silx/gui/plot/ROIStatsWidget.py
+++ /dev/null
@@ -1,780 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 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 widget for displaying statistics relative to a
-Region of interest and an item
-"""
-
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "22/07/2019"
-
-
-from contextlib import contextmanager
-from silx.gui import qt
-from silx.gui import icons
-from silx.gui.plot.StatsWidget import _StatsWidgetBase, StatsTable, _Container
-from silx.gui.plot.StatsWidget import UpdateModeWidget, UpdateMode
-from silx.gui.widgets.TableWidget import TableWidget
-from silx.gui.plot.items.roi import RegionOfInterest
-from silx.gui.plot import items as plotitems
-from silx.gui.plot.items.core import ItemChangedType
-from silx.gui.plot3d import items as plot3ditems
-from silx.gui.plot.CurvesROIWidget import ROI
-from silx.gui.plot import stats as statsmdl
-from collections import OrderedDict
-from silx.utils.proxy import docstring
-import silx.gui.plot.items.marker
-import silx.gui.plot.items.shape
-import functools
-import logging
-
-_logger = logging.getLogger(__name__)
-
-
-class _GetROIItemCoupleDialog(qt.QDialog):
- """
- Dialog used to know which plot item and which roi he wants
- """
- _COMPATIBLE_KINDS = ('curve', 'image', 'scatter', 'histogram')
-
- def __init__(self, parent=None, plot=None, rois=None):
- qt.QDialog.__init__(self, parent=parent)
- assert plot is not None
- assert rois is not None
- self._plot = plot
- self._rois = rois
-
- self.setLayout(qt.QVBoxLayout())
-
- # define the selection widget
- self._selection_widget = qt.QWidget()
- self._selection_widget.setLayout(qt.QHBoxLayout())
- self._kindCB = qt.QComboBox(parent=self)
- self._selection_widget.layout().addWidget(self._kindCB)
- self._itemCB = qt.QComboBox(parent=self)
- self._selection_widget.layout().addWidget(self._itemCB)
- self._roiCB = qt.QComboBox(parent=self)
- self._selection_widget.layout().addWidget(self._roiCB)
- self.layout().addWidget(self._selection_widget)
-
- # define modal buttons
- types = qt.QDialogButtonBox.Ok | qt.QDialogButtonBox.Cancel
- self._buttonsModal = qt.QDialogButtonBox(parent=self)
- self._buttonsModal.setStandardButtons(types)
- self.layout().addWidget(self._buttonsModal)
- self._buttonsModal.accepted.connect(self.accept)
- self._buttonsModal.rejected.connect(self.reject)
-
- # connect signal / slot
- self._kindCB.currentIndexChanged.connect(self._updateValidItemAndRoi)
-
- def _getCompatibleRois(self, kind):
- """Return compatible rois for the given item kind"""
- def is_compatible(roi, kind):
- if isinstance(roi, RegionOfInterest):
- return kind in ('image', 'scatter')
- elif isinstance(roi, ROI):
- return kind in ('curve', 'histogram')
- else:
- raise ValueError('kind not managed')
- return list(filter(lambda x: is_compatible(x, kind), self._rois))
-
- def exec_(self):
- self._kindCB.clear()
- self._itemCB.clear()
- # filter kind without any items
- self._valid_kinds = {}
- # key is item type, value kinds
- self._valid_rois = {}
- # key is item type, value rois
- self._kind_name_to_roi = {}
- # key is (kind, roi name) value is roi
- self._kind_name_to_item = {}
- # key is (kind, legend name) value is item
- for kind in _GetROIItemCoupleDialog._COMPATIBLE_KINDS:
- def getItems(kind):
- output = []
- for item in self._plot.getItems():
- type_ = self._plot._itemKind(item)
- if type_ in kind and item.isVisible():
- output.append(item)
- return output
-
- items = getItems(kind=kind)
- rois = self._getCompatibleRois(kind=kind)
- if len(items) > 0 and len(rois) > 0:
- self._valid_kinds[kind] = items
- self._valid_rois[kind] = rois
- for roi in rois:
- name = roi.getName()
- self._kind_name_to_roi[(kind, name)] = roi
- for item in items:
- self._kind_name_to_item[(kind, item.getLegend())] = item
-
- # filter roi according to kinds
- if len(self._valid_kinds) == 0:
- _logger.warning('no couple item/roi detected for displaying stats')
- return self.reject()
-
- for kind in self._valid_kinds:
- self._kindCB.addItem(kind)
- self._updateValidItemAndRoi()
-
- return qt.QDialog.exec_(self)
-
- def _updateValidItemAndRoi(self, *args, **kwargs):
- self._itemCB.clear()
- self._roiCB.clear()
- kind = self._kindCB.currentText()
- for roi in self._valid_rois[kind]:
- self._roiCB.addItem(roi.getName())
- for item in self._valid_kinds[kind]:
- self._itemCB.addItem(item.getLegend())
-
- def getROI(self):
- kind = self._kindCB.currentText()
- roi_name = self._roiCB.currentText()
- return self._kind_name_to_roi[(kind, roi_name)]
-
- def getItem(self):
- kind = self._kindCB.currentText()
- item_name = self._itemCB.currentText()
- return self._kind_name_to_item[(kind, item_name)]
-
-
-class ROIStatsItemHelper(object):
- """Item utils to associate a plot item and a roi
-
- Display on one row statistics regarding the couple
- (Item (plot item) / roi).
-
- :param Item plot_item: item for which we want statistics
- :param Union[ROI,RegionOfInterest]: region of interest to use for
- statistics.
- """
- def __init__(self, plot_item, roi):
- self._plot_item = plot_item
- self._roi = roi
-
- @property
- def roi(self):
- """roi"""
- return self._roi
-
- def roi_name(self):
- if isinstance(self._roi, ROI):
- return self._roi.getName()
- elif isinstance(self._roi, RegionOfInterest):
- return self._roi.getName()
- else:
- raise TypeError('Unmanaged roi type')
-
- @property
- def roi_kind(self):
- """roi class"""
- return self._roi.__class__
-
- # TODO: should call a util function from the wrapper ?
- def item_kind(self):
- """item kind"""
- if isinstance(self._plot_item, plotitems.Curve):
- return 'curve'
- elif isinstance(self._plot_item, plotitems.ImageData):
- return 'image'
- elif isinstance(self._plot_item, plotitems.Scatter):
- return 'scatter'
- elif isinstance(self._plot_item, plotitems.Histogram):
- return 'histogram'
- elif isinstance(self._plot_item, (plot3ditems.ImageData,
- plot3ditems.ScalarField3D)):
- return 'image'
- elif isinstance(self._plot_item, (plot3ditems.Scatter2D,
- plot3ditems.Scatter3D)):
- return 'scatter'
-
- @property
- def item_legend(self):
- """legend of the plot Item"""
- return self._plot_item.getLegend()
-
- def id_key(self):
- """unique key to represent the couple (item, roi)"""
- return (self.item_kind(), self.item_legend, self.roi_kind,
- self.roi_name())
-
-
-class _StatsROITable(_StatsWidgetBase, TableWidget):
- """
- Table sued to display some statistics regarding a couple (item/roi)
- """
- _LEGEND_HEADER_DATA = 'legend'
-
- _KIND_HEADER_DATA = 'kind'
-
- _ROI_HEADER_DATA = 'roi'
-
- sigUpdateModeChanged = qt.Signal(object)
- """Signal emitted when the update mode changed"""
-
- def __init__(self, parent, plot):
- TableWidget.__init__(self, parent)
- _StatsWidgetBase.__init__(self, statsOnVisibleData=False,
- displayOnlyActItem=False)
- self.__region_edition_callback = {}
- """We need to keep trace of the roi signals connection because
- the roi emits the sigChanged during roi edition"""
- self._items = {}
- self.setRowCount(0)
- self.setColumnCount(3)
-
- # Init headers
- headerItem = qt.QTableWidgetItem(self._LEGEND_HEADER_DATA.title())
- headerItem.setData(qt.Qt.UserRole, self._LEGEND_HEADER_DATA)
- self.setHorizontalHeaderItem(0, headerItem)
- headerItem = qt.QTableWidgetItem(self._KIND_HEADER_DATA.title())
- headerItem.setData(qt.Qt.UserRole, self._KIND_HEADER_DATA)
- self.setHorizontalHeaderItem(1, headerItem)
- headerItem = qt.QTableWidgetItem(self._ROI_HEADER_DATA.title())
- headerItem.setData(qt.Qt.UserRole, self._ROI_HEADER_DATA)
- self.setHorizontalHeaderItem(2, headerItem)
-
- self.setSortingEnabled(True)
- self.setPlot(plot)
-
- self.__plotItemToItems = {}
- """Key is plotItem, values is list of __RoiStatsItemWidget"""
- self.__roiToItems = {}
- """Key is roi, values is list of __RoiStatsItemWidget"""
- self.__roisKeyToRoi = {}
-
- def add(self, item):
- assert isinstance(item, ROIStatsItemHelper)
- if item.id_key() in self._items:
- _logger.warning(item.id_key(), 'is already present')
- return None
- self._items[item.id_key()] = item
- self._addItem(item)
- return item
-
- def _addItem(self, item):
- """
- Add a _RoiStatsItemWidget item to the table.
-
- :param item:
- :return: True if successfully added.
- """
- if not isinstance(item, ROIStatsItemHelper):
- # skipped because also receive all new plot item (Marker...) that
- # we don't want to manage in this case.
- return
- # plotItem = item.getItem()
- # roi = item.getROI()
- kind = item.item_kind()
- if kind not in statsmdl.BASIC_COMPATIBLE_KINDS:
- _logger.info("Item has not a supported type: %s", item)
- return False
-
- # register the roi and the kind
- self._registerPlotItem(item)
- self._registerROI(item)
-
- # Prepare table items
- tableItems = [
- qt.QTableWidgetItem(), # Legend
- qt.QTableWidgetItem(), # Kind
- qt.QTableWidgetItem()] # roi
-
- for column in range(3, self.columnCount()):
- header = self.horizontalHeaderItem(column)
- name = header.data(qt.Qt.UserRole)
-
- formatter = self._statsHandler.formatters[name]
- if formatter:
- tableItem = formatter.tabWidgetItemClass()
- else:
- tableItem = qt.QTableWidgetItem()
-
- tooltip = self._statsHandler.stats[name].getToolTip(kind=kind)
- if tooltip is not None:
- tableItem.setToolTip(tooltip)
-
- tableItems.append(tableItem)
-
- # Disable sorting while adding table items
- with self._disableSorting():
- # Add a row to the table
- self.setRowCount(self.rowCount() + 1)
-
- # Add table items to the last row
- row = self.rowCount() - 1
- for column, tableItem in enumerate(tableItems):
- tableItem.setData(qt.Qt.UserRole, _Container(item))
- tableItem.setFlags(
- qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
- self.setItem(row, column, tableItem)
-
- # Update table items content
- self._updateStats(item, data_changed=True)
-
- # Listen for item changes
- # Using queued connection to avoid issue with sender
- # being that of the signal calling the signal
- item._plot_item.sigItemChanged.connect(self._plotItemChanged,
- qt.Qt.QueuedConnection)
- return True
-
- def _removeAllItems(self):
- for row in range(self.rowCount()):
- tableItem = self.item(row, 0)
- # item = self._tableItemToItem(tableItem)
- # item.sigItemChanged.disconnect(self._plotItemChanged)
- self.clearContents()
- self.setRowCount(0)
-
- def clear(self):
- self._removeAllItems()
-
- def setStats(self, statsHandler):
- """Set which stats to display and the associated formatting.
-
- :param StatsHandler statsHandler:
- Set the statistics to be displayed and how to format them using
- """
- self._removeAllItems()
- _StatsWidgetBase.setStats(self, statsHandler)
-
- self.setRowCount(0)
- self.setColumnCount(len(self._statsHandler.stats) + 3) # + legend, kind and roi # noqa
-
- for index, stat in enumerate(self._statsHandler.stats.values()):
- headerItem = qt.QTableWidgetItem(stat.name.capitalize())
- headerItem.setData(qt.Qt.UserRole, stat.name)
- if stat.description is not None:
- headerItem.setToolTip(stat.description)
- self.setHorizontalHeaderItem(3 + index, headerItem)
-
- horizontalHeader = self.horizontalHeader()
- if hasattr(horizontalHeader, 'setSectionResizeMode'): # Qt5
- horizontalHeader.setSectionResizeMode(qt.QHeaderView.ResizeToContents)
- else: # Qt4
- horizontalHeader.setResizeMode(qt.QHeaderView.ResizeToContents)
-
- self._updateItemObserve()
-
- def _updateItemObserve(self, *args):
- pass
-
- def _dataChanged(self, item):
- pass
-
- def _updateStats(self, item, data_changed=False, roi_changed=False):
- assert isinstance(item, ROIStatsItemHelper)
- plotItem = item._plot_item
- roi = item._roi
- if item is None:
- return
- plot = self.getPlot()
- if plot is None:
- _logger.info("Plot not available")
- return
-
- row = self._itemToRow(item)
- if row is None:
- _logger.error("This item is not in the table: %s", str(item))
- return
-
- statsHandler = self.getStatsHandler()
- if statsHandler is not None:
- stats = statsHandler.calculate(plotItem, plot,
- onlimits=self._statsOnVisibleData,
- roi=roi, data_changed=data_changed,
- roi_changed=roi_changed)
- else:
- stats = {}
-
- with self._disableSorting():
- for name, tableItem in self._itemToTableItems(item).items():
- if name == self._LEGEND_HEADER_DATA:
- text = self._plotWrapper.getLabel(plotItem)
- tableItem.setText(text)
- elif name == self._KIND_HEADER_DATA:
- tableItem.setText(self._plotWrapper.getKind(plotItem))
- elif name == self._ROI_HEADER_DATA:
- name = roi.getName()
- tableItem.setText(name)
- else:
- value = stats.get(name)
- if value is None:
- _logger.error("Value not found for: %s", name)
- tableItem.setText('-')
- else:
- tableItem.setText(str(value))
-
- @contextmanager
- def _disableSorting(self):
- """Context manager that disables table sorting
-
- Previous state is restored when leaving
- """
- sorting = self.isSortingEnabled()
- if sorting:
- self.setSortingEnabled(False)
- yield
- if sorting:
- self.setSortingEnabled(sorting)
-
- def _itemToRow(self, item):
- """Find the row corresponding to a plot item
-
- :param item: The plot item
- :return: The corresponding row index
- :rtype: Union[int,None]
- """
- for row in range(self.rowCount()):
- tableItem = self.item(row, 0)
- if self._tableItemToItem(tableItem) == item:
- return row
- return None
-
- def _tableItemToItem(self, tableItem):
- """Find the plot item corresponding to a table item
-
- :param QTableWidgetItem tableItem:
- :rtype: QObject
- """
- container = tableItem.data(qt.Qt.UserRole)
- return container()
-
- def _itemToTableItems(self, item):
- """Find all table items corresponding to a plot item
-
- :param item: The plot item
- :return: An ordered dict of column name to QTableWidgetItem mapping
- for the given plot item.
- :rtype: OrderedDict
- """
- result = OrderedDict()
- row = self._itemToRow(item)
- if row is not None:
- for column in range(self.columnCount()):
- tableItem = self.item(row, column)
- if self._tableItemToItem(tableItem) != item:
- _logger.error("Table item/plot item mismatch")
- else:
- header = self.horizontalHeaderItem(column)
- name = header.data(qt.Qt.UserRole)
- result[name] = tableItem
- return result
-
- def _plotItemToItems(self, plotItem):
- """Return all _RoiStatsItemWidget associated to the plotItem
- Needed for updating on itemChanged signal
- """
- if plotItem in self.__plotItemToItems:
- return []
- else:
- return self.__plotItemToItems[plotItem]
-
- def _registerPlotItem(self, item):
- if item._plot_item not in self.__plotItemToItems:
- self.__plotItemToItems[item._plot_item] = set()
- self.__plotItemToItems[item._plot_item].add(item)
-
- def _roiToItems(self, roi):
- """Return all _RoiStatsItemWidget associated to the roi
- Needed for updating on roiChanged signal
- """
- if roi in self.__roiToItems:
- return []
- else:
- return self.__roiToItems[roi]
-
- def _registerROI(self, item):
- if item._roi not in self.__roiToItems:
- self.__roiToItems[item._roi] = set()
- # TODO: normalize also sig name
- if isinstance(item._roi, RegionOfInterest):
- # item connection within sigRegionChanged should only be
- # stopped during the region edition
- self.__region_edition_callback[item._roi] = functools.partial(
- self._updateAllStats, False, True)
- item._roi.sigRegionChanged.connect(self.__region_edition_callback[item._roi])
- item._roi.sigEditingStarted.connect(functools.partial(
- self._startFiltering, item._roi))
- item._roi.sigEditingFinished.connect(functools.partial(
- self._endFiltering, item._roi))
- else:
- item._roi.sigChanged.connect(functools.partial(
- self._updateAllStats, False, True))
- self.__roiToItems[item._roi].add(item)
-
- def _startFiltering(self, roi):
- roi.sigRegionChanged.disconnect(self.__region_edition_callback[roi])
-
- def _endFiltering(self, roi):
- roi.sigRegionChanged.connect(self.__region_edition_callback[roi])
- self._updateAllStats(roi_changed=True)
-
- def unregisterROI(self, roi):
- if roi in self.__roiToItems:
- del self.__roiToItems[roi]
- if isinstance(roi, RegionOfInterest):
- roi.sigRegionEditionStarted.disconnect(functools.partial(
- self._startFiltering, roi))
- roi.sigRegionEditionFinished.disconnect(functools.partial(
- self._startFiltering, roi))
- try:
- roi.sigRegionChanged.disconnect(self._updateAllStats)
- except:
- pass
- else:
- roi.sigChanged.disconnect(self._updateAllStats)
-
- def _plotItemChanged(self, event):
- """Handle modifications of the items.
-
- :param event:
- """
- if event is ItemChangedType.DATA:
- if self.getUpdateMode() is UpdateMode.MANUAL:
- return
- if self._skipPlotItemChangedEvent(event) is True:
- return
- else:
- sender = self.sender()
- for item in self.__plotItemToItems[sender]:
- # TODO: get all concerned items
- self._updateStats(item, data_changed=True)
- # deal with stat items visibility
- if event is ItemChangedType.VISIBLE:
- if len(self._itemToTableItems(item).items()) > 0:
- item_0 = list(self._itemToTableItems(item).values())[0]
- row_index = item_0.row()
- self.setRowHidden(row_index, not item.isVisible())
-
- def _removeItem(self, itemKey):
- if isinstance(itemKey, (silx.gui.plot.items.marker.Marker,
- silx.gui.plot.items.shape.Shape)):
- return
- if itemKey not in self._items:
- _logger.warning('key not recognized. Won\'t remove any item')
- return
- item = self._items[itemKey]
- row = self._itemToRow(item)
- if row is None:
- kind = self._plotWrapper.getKind(item)
- if kind in statsmdl.BASIC_COMPATIBLE_KINDS:
- _logger.error("Removing item that is not in table: %s", str(item))
- return
- item._plot_item.sigItemChanged.disconnect(self._plotItemChanged)
- self.removeRow(row)
- del self._items[itemKey]
-
- def _updateAllStats(self, is_request=False, roi_changed=False):
- """Update stats for all rows in the table
-
- :param bool is_request: True if come from a manual request
- """
- if (self.getUpdateMode() is UpdateMode.MANUAL and
- not is_request and not roi_changed):
- return
-
- with self._disableSorting():
- for row in range(self.rowCount()):
- tableItem = self.item(row, 0)
- item = self._tableItemToItem(tableItem)
- self._updateStats(item, roi_changed=roi_changed,
- data_changed=is_request)
-
- def _plotCurrentChanged(self, *args):
- pass
-
- def _getRoi(self, kind, name):
- """return the roi fitting the requirement kind, name. This information
- is enough to be sure it is unique (in the widget)"""
- for roi in self.__roiToItems:
- roiName = roi.getName()
- if isinstance(roi, kind) and name == roiName:
- return roi
- return None
-
- def _getPlotItem(self, kind, legend):
- """return the plotItem fitting the requirement kind, legend.
- This information is enough to be sure it is unique (in the widget)"""
- for plotItem in self.__plotItemToItems:
- if legend == plotItem.getLegend() and self._plotWrapper.getKind(plotItem) == kind:
- return plotItem
- return None
-
-
-class ROIStatsWidget(qt.QMainWindow):
- """
- Widget used to define stats item for a couple(roi, plotItem).
- Stats will be computing on a given item (curve, image...) in the given
- region of interest.
-
- It also provide an interface for adding and removing items.
-
- .. snapshotqt:: img/ROIStatsWidget.png
- :width: 300px
- :align: center
-
- from silx.gui import qt
- from silx.gui.plot import Plot2D
- from silx.gui.plot.ROIStatsWidget import ROIStatsWidget
- from silx.gui.plot.items.roi import RectangleROI
- import numpy
- plot = Plot2D()
- plot.addImage(numpy.arange(10000).reshape(100, 100), legend='img')
- plot.show()
- rectangleROI = RectangleROI()
- rectangleROI.setGeometry(origin=(0, 100), size=(20, 20))
- rectangleROI.setName('Initial ROI')
- widget = ROIStatsWidget(plot=plot)
- widget.setStats([('sum', numpy.sum), ('mean', numpy.mean)])
- widget.registerROI(rectangleROI)
- widget.addItem(roi=rectangleROI, plotItem=plot.getImage('img'))
- widget.show()
-
- :param Union[qt.QWidget,None] parent: parent qWidget
- :param PlotWindow plot: plot widget containing the items
- :param stats: stats to display
- :param tuple rois: tuple of rois to manage
- """
-
- def __init__(self, parent=None, plot=None, stats=None, rois=None):
- qt.QMainWindow.__init__(self, parent)
-
- toolbar = qt.QToolBar(self)
- icon = icons.getQIcon('add')
- self._rois = list(rois) if rois is not None else []
- self._addAction = qt.QAction(icon, 'add item/roi', toolbar)
- self._addAction.triggered.connect(self._addRoiStatsItem)
- icon = icons.getQIcon('rm')
- self._removeAction = qt.QAction(icon, 'remove item/roi', toolbar)
- self._removeAction.triggered.connect(self._removeCurrentRow)
-
- toolbar.addAction(self._addAction)
- toolbar.addAction(self._removeAction)
- self.addToolBar(toolbar)
-
- self._plot = plot
- self._statsROITable = _StatsROITable(parent=self, plot=self._plot)
- self.setStats(stats=stats)
- self.setCentralWidget(self._statsROITable)
- self.setWindowFlags(qt.Qt.Widget)
-
- # expose API
- self._setUpdateMode = self._statsROITable.setUpdateMode
- self._updateAllStats = self._statsROITable._updateAllStats
-
- # setup
- self._statsROITable.setSelectionBehavior(qt.QTableWidget.SelectRows)
-
- def registerROI(self, roi):
- """For now there is no direct link between roi and plot. That is why
- we need to add/register them to be able to associate them"""
- self._rois.append(roi)
-
- def setPlot(self, plot):
- """Define the plot to interact with
-
- :param Union[PlotWidget,SceneWidget,None] plot:
- The plot containing the items on which statistics are applied
- """
- self._plot = plot
-
- def getPlot(self):
- return self._plot
-
- @docstring(_StatsROITable)
- def setStats(self, stats):
- if stats is not None:
- self._statsROITable.setStats(statsHandler=stats)
-
- @docstring(_StatsROITable)
- def getStatsHandler(self):
- """
-
- :return:
- """
- return self._statsROITable.getStatsHandler()
-
- def _addRoiStatsItem(self):
- """Ask the user what couple ROI / item he want to display"""
- dialog = _GetROIItemCoupleDialog(parent=self, plot=self._plot,
- rois=self._rois)
- if dialog.exec_():
- self.addItem(roi=dialog.getROI(), plotItem=dialog.getItem())
-
- def addItem(self, plotItem, roi):
- """
- Add a row of statitstic regarding the couple (plotItem, roi)
-
- :param Item plotItem: item to use for statistics
- :param roi: region of interest to limit the statistic.
- :type: Union[ROI, RegionOfInterest]
- :return: None of failed to add the item
- :rtype: Union[None,ROIStatsItemHelper]
- """
- statsItem = ROIStatsItemHelper(roi=roi, plot_item=plotItem)
- return self._statsROITable.add(item=statsItem)
-
- def removeItem(self, plotItem, roi):
- """
- Remove the row associated to the couple (plotItem, roi)
-
- :param Item plotItem: item to use for statistics
- :param roi: region of interest to limit the statistic.
- :type: Union[ROI,RegionOfInterest]
- """
- statsItem = ROIStatsItemHelper(roi=roi, plot_item=plotItem)
- self._statsROITable._removeItem(itemKey=statsItem.id_key())
-
- def _removeCurrentRow(self):
- def is1DKind(kind):
- if kind in ('curve', 'histogram', 'scatter'):
- return True
- else:
- return False
-
- currentRow = self._statsROITable.currentRow()
- item_kind = self._statsROITable.item(currentRow, 1).text()
- item_legend = self._statsROITable.item(currentRow, 0).text()
-
- roi_name = self._statsROITable.item(currentRow, 2).text()
- roi_kind = ROI if is1DKind(item_kind) else RegionOfInterest
- roi = self._statsROITable._getRoi(kind=roi_kind, name=roi_name)
- if roi is None:
- _logger.warning('failed to retrieve the roi you want to remove')
- return False
- plot_item = self._statsROITable._getPlotItem(kind=item_kind,
- legend=item_legend)
- if plot_item is None:
- _logger.warning('failed to retrieve the plot item you want to'
- 'remove')
- return False
- return self.removeItem(plotItem=plot_item, roi=roi)
diff --git a/silx/gui/plot/ScatterMaskToolsWidget.py b/silx/gui/plot/ScatterMaskToolsWidget.py
deleted file mode 100644
index 5ae8653..0000000
--- a/silx/gui/plot/ScatterMaskToolsWidget.py
+++ /dev/null
@@ -1,621 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Widget providing a set of tools to draw masks on a PlotWidget.
-
-This widget is meant to work with a modified :class:`silx.gui.plot.PlotWidget`
-
-- :class:`ScatterMask`: Handle scatter mask update and history
-- :class:`ScatterMaskToolsWidget`: GUI for :class:`ScatterMask`
-- :class:`ScatterMaskToolsDockWidget`: DockWidget to integrate in :class:`PlotWindow`
-"""
-
-from __future__ import division
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "15/02/2019"
-
-
-import math
-import logging
-import os
-import numpy
-import sys
-
-from .. import qt
-from ...math.combo import min_max
-from ...image import shapes
-
-from .items import ItemChangedType, Scatter
-from ._BaseMaskToolsWidget import BaseMask, BaseMaskToolsWidget, BaseMaskToolsDockWidget
-from ..colors import cursorColorForColormap, rgba
-
-
-_logger = logging.getLogger(__name__)
-
-
-class ScatterMask(BaseMask):
- """A 1D mask for scatter data.
- """
- def __init__(self, scatter=None):
- """
-
- :param scatter: :class:`silx.gui.plot.items.Scatter` instance
- """
- BaseMask.__init__(self, scatter)
-
- def _getXY(self):
- x = self._dataItem.getXData(copy=False)
- y = self._dataItem.getYData(copy=False)
- return x, y
-
- def getDataValues(self):
- """Return scatter data values as a 1D array.
-
- :rtype: 1D numpy.ndarray
- """
- return self._dataItem.getValueData(copy=False)
-
- def save(self, filename, kind):
- if kind == 'npy':
- try:
- numpy.save(filename, self.getMask(copy=False))
- except IOError:
- raise RuntimeError("Mask file can't be written")
- elif kind in ["csv", "txt"]:
- try:
- numpy.savetxt(filename, self.getMask(copy=False))
- except IOError:
- raise RuntimeError("Mask file can't be written")
-
- def updatePoints(self, level, indices, mask=True):
- """Mask/Unmask points with given indices.
-
- :param int level: Mask level to update.
- :param indices: Sequence or 1D array of indices of points to be
- updated
- :param bool mask: True to mask (default), False to unmask.
- """
- if mask:
- self._mask[indices] = level
- else:
- # unmask only where mask level is the specified value
- indices_stencil = numpy.zeros_like(self._mask, dtype=bool)
- indices_stencil[indices] = True
- self._mask[numpy.logical_and(self._mask == level, indices_stencil)] = 0
- self._notify()
-
- # update shapes
- def updatePolygon(self, level, vertices, mask=True):
- """Mask/Unmask a polygon of the given mask level.
-
- :param int level: Mask level to update.
- :param vertices: Nx2 array of polygon corners as (y, x) or (row, col)
- :param bool mask: True to mask (default), False to unmask.
- """
- polygon = shapes.Polygon(vertices)
- x, y = self._getXY()
-
- # TODO: this could be optimized if necessary
- indices_in_polygon = [idx for idx in range(len(x)) if
- polygon.is_inside(y[idx], x[idx])]
-
- self.updatePoints(level, indices_in_polygon, mask)
-
- def updateRectangle(self, level, y, x, height, width, mask=True):
- """Mask/Unmask data inside a rectangle
-
- :param int level: Mask level to update.
- :param float y: Y coordinate of bottom left corner of the rectangle
- :param float x: X coordinate of bottom left corner of the rectangle
- :param float height:
- :param float width:
- :param bool mask: True to mask (default), False to unmask.
- """
- vertices = [(y, x),
- (y + height, x),
- (y + height, x + width),
- (y, x + width)]
- self.updatePolygon(level, vertices, mask)
-
- def updateDisk(self, level, cy, cx, radius, mask=True):
- """Mask/Unmask a disk of the given mask level.
-
- :param int level: Mask level to update.
- :param float cy: Disk center (y).
- :param float cx: Disk center (x).
- :param float radius: Radius of the disk in mask array unit
- :param bool mask: True to mask (default), False to unmask.
- """
- x, y = self._getXY()
- stencil = (y - cy)**2 + (x - cx)**2 < radius**2
- self.updateStencil(level, stencil, mask)
-
- def updateEllipse(self, level, crow, ccol, radius_r, radius_c, mask=True):
- """Mask/Unmask an ellipse of the given mask level.
-
- :param int level: Mask level to update.
- :param int crow: Row of the center of the ellipse
- :param int ccol: Column of the center of the ellipse
- :param float radius_r: Radius of the ellipse in the row
- :param float radius_c: Radius of the ellipse in the column
- :param bool mask: True to mask (default), False to unmask.
- """
- def is_inside(px, py):
- return (px - ccol)**2 / radius_c**2 + (py - crow)**2 / radius_r**2 <= 1.0
- x, y = self._getXY()
- indices_inside = [idx for idx in range(len(x)) if is_inside(x[idx], y[idx])]
- self.updatePoints(level, indices_inside, mask)
-
- def updateLine(self, level, y0, x0, y1, x1, width, mask=True):
- """Mask/Unmask points inside a rectangle defined by a line (two
- end points) and a width.
-
- :param int level: Mask level to update.
- :param float y0: Row of the starting point.
- :param float x0: Column of the starting point.
- :param float row1: Row of the end point.
- :param float col1: Column of the end point.
- :param float width: Width of the line.
- :param bool mask: True to mask (default), False to unmask.
- """
- # theta is the angle between the horizontal and the line
- theta = math.atan((y1 - y0) / (x1 - x0)) if x1 - x0 else 0
- w_over_2_sin_theta = width / 2. * math.sin(theta)
- w_over_2_cos_theta = width / 2. * math.cos(theta)
-
- vertices = [(y0 - w_over_2_cos_theta, x0 + w_over_2_sin_theta),
- (y0 + w_over_2_cos_theta, x0 - w_over_2_sin_theta),
- (y1 + w_over_2_cos_theta, x1 - w_over_2_sin_theta),
- (y1 - w_over_2_cos_theta, x1 + w_over_2_sin_theta)]
-
- self.updatePolygon(level, vertices, mask)
-
-
-class ScatterMaskToolsWidget(BaseMaskToolsWidget):
- """Widget with tools for masking data points on a scatter in a
- :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._data_extent = None
- """Maximum extent of the data i.e., max(xMax-xMin, yMax-yMin)"""
-
- self._mask_scatter = None
- """plot Scatter item for representing the mask"""
-
- def setSelectionMask(self, mask, copy=True):
- """Set the mask to a new array.
-
- :param numpy.ndarray mask:
- The array to use for the mask or None to reset the mask.
- :type mask: numpy.ndarray of uint8, C-contiguous.
- Array of other types are converted.
- :param bool copy: True (the default) to copy the array,
- False to use it as is if possible.
- :return: None if failed, shape of mask as 1-tuple if successful.
- The mask can be cropped or padded to fit active scatter,
- the returned shape is that of the scatter data.
- """
- if self._data_scatter is None:
- # this can happen if the mask tools widget has never been shown
- self._data_scatter = self.plot._getActiveItem(kind="scatter")
- if self._data_scatter is None:
- return None
- self._adjustColorAndBrushSize(self._data_scatter)
-
- if mask is None:
- self.resetSelectionMask()
- return self._data_scatter.getXData(copy=False).shape
-
- mask = numpy.array(mask, copy=False, dtype=numpy.uint8)
-
- if self._data_scatter.getXData(copy=False).shape == (0,) \
- or mask.shape == self._data_scatter.getXData(copy=False).shape:
- self._mask.setMask(mask, copy=copy)
- self._mask.commit()
- return mask.shape
- else:
- raise ValueError("Mask does not have the same shape as the data")
-
- # Handle mask refresh on the plot
-
- def _updatePlotMask(self):
- """Update mask image in plot"""
- mask = self.getSelectionMask(copy=False)
- if mask is not None:
- self.plot.addScatter(self._data_scatter.getXData(),
- self._data_scatter.getYData(),
- mask,
- legend=self._maskName,
- colormap=self._colormap,
- z=self._z)
- self._mask_scatter = self.plot._getItem(kind="scatter",
- legend=self._maskName)
- self._mask_scatter.setSymbolSize(
- self._data_scatter.getSymbolSize() + 2.0)
- self._mask_scatter.sigItemChanged.connect(self.__maskScatterChanged)
- elif self.plot._getItem(kind="scatter",
- legend=self._maskName) is not None:
- self.plot.remove(self._maskName, kind='scatter')
-
- def __maskScatterChanged(self, event):
- """Handles update of mask scatter"""
- if (event is ItemChangedType.VISUALIZATION_MODE and
- self._mask_scatter is not None):
- self._mask_scatter.setVisualization(Scatter.Visualization.POINTS)
-
- # track widget visibility and plot active image changes
-
- def showEvent(self, event):
- try:
- self.plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChangedAfterCare)
- except (RuntimeError, TypeError):
- pass
- self._activeScatterChanged(None, None) # Init mask + enable/disable widget
- self.plot.sigActiveScatterChanged.connect(self._activeScatterChanged)
-
- def hideEvent(self, event):
- try:
- # if the method is not connected this raises a TypeError and there is no way
- # to know the connected slots
- self.plot.sigActiveScatterChanged.disconnect(self._activeScatterChanged)
- except (RuntimeError, TypeError):
- _logger.info(sys.exc_info()[1])
- if not self.browseAction.isChecked():
- self.browseAction.trigger() # Disable drawing tool
-
- if self.getSelectionMask(copy=False) is not None:
- self.plot.sigActiveScatterChanged.connect(
- self._activeScatterChangedAfterCare)
-
- def _adjustColorAndBrushSize(self, activeScatter):
- colormap = activeScatter.getColormap()
- self._defaultOverlayColor = rgba(cursorColorForColormap(colormap['name']))
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
- self._z = activeScatter.getZValue() + 1
- self._data_scatter = activeScatter
-
- # Adjust brush size to data range
- xData = self._data_scatter.getXData(copy=False)
- yData = self._data_scatter.getYData(copy=False)
- # Adjust brush size to data range
- if xData.size > 0 and yData.size > 0:
- xMin, xMax = min_max(xData)
- yMin, yMax = min_max(yData)
- self._data_extent = max(xMax - xMin, yMax - yMin)
- else:
- self._data_extent = None
-
- def _activeScatterChangedAfterCare(self, previous, next):
- """Check synchro of active scatter and mask when mask widget is hidden.
-
- If active image has no more the same size as the mask, the mask is
- removed, otherwise it is adjusted to z.
- """
- # check that content changed was the active scatter
- activeScatter = self.plot._getActiveItem(kind="scatter")
-
- if activeScatter is None or activeScatter.getName() == self._maskName:
- # No active scatter or active scatter is the mask...
- self.plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChangedAfterCare)
- self._data_extent = None
- self._data_scatter = None
-
- else:
- self._adjustColorAndBrushSize(activeScatter)
-
- if self._data_scatter.getXData(copy=False).shape != self._mask.getMask(copy=False).shape:
- # scatter has not the same size, remove mask and stop listening
- if self.plot._getItem(kind="scatter", legend=self._maskName):
- self.plot.remove(self._maskName, kind='scatter')
-
- self.plot.sigActiveScatterChanged.disconnect(
- self._activeScatterChangedAfterCare)
- self._data_extent = None
- self._data_scatter = None
-
- else:
- # Refresh in case z changed
- self._mask.setDataItem(self._data_scatter)
- self._updatePlotMask()
-
- def _activeScatterChanged(self, previous, next):
- """Update widget and mask according to active scatter changes"""
- activeScatter = self.plot._getActiveItem(kind="scatter")
-
- if activeScatter is None or activeScatter.getName() == self._maskName:
- # No active scatter or active scatter is the mask...
- self.setEnabled(False)
-
- self._data_scatter = None
- self._data_extent = None
- self._mask.reset()
- self._mask.commit()
-
- else: # There is an active scatter
- self.setEnabled(True)
- self._adjustColorAndBrushSize(activeScatter)
-
- self._mask.setDataItem(self._data_scatter)
- if self._data_scatter.getXData(copy=False).shape != self._mask.getMask(copy=False).shape:
- self._mask.reset(self._data_scatter.getXData(copy=False).shape)
- self._mask.commit()
- else:
- # Refresh in case z changed
- self._updatePlotMask()
-
- self._updateInteractiveMode()
-
- # Handle whole mask operations
-
- def load(self, filename):
- """Load a mask from an image file.
-
- :param str filename: File name from which to load the mask
- :raise Exception: An exception in case of failure
- :raise RuntimeWarning: In case the mask was applied but with some
- import changes to notice
- """
- _, extension = os.path.splitext(filename)
- extension = extension.lower()[1:]
- if extension == "npy":
- try:
- mask = numpy.load(filename)
- except IOError:
- _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 ["txt", "csv"]:
- try:
- mask = numpy.loadtxt(filename)
- except IOError:
- _logger.error("Can't load filename '%s'", filename)
- _logger.debug("Backtrace", exc_info=True)
- raise RuntimeError('File "%s" is not a numpy txt file.',
- filename)
- else:
- msg = "Extension '%s' is not supported."
- raise RuntimeError(msg % extension)
-
- self.setSelectionMask(mask, copy=False)
-
- def _loadMask(self):
- """Open load mask dialog"""
- dialog = qt.QFileDialog(self)
- dialog.setWindowTitle("Load Mask")
- dialog.setModal(1)
- filters = [
- 'NumPy binary file (*.npy)',
- 'CSV text file (*.csv)',
- ]
- dialog.setNameFilters(filters)
- dialog.setFileMode(qt.QFileDialog.ExistingFile)
- dialog.setDirectory(self.maskFileDir)
- if not dialog.exec_():
- dialog.close()
- return
-
- filename = dialog.selectedFiles()[0]
- dialog.close()
-
- # Update the directory according to the user selection
- self.maskFileDir = os.path.dirname(filename)
-
- try:
- self.load(filename)
- # except RuntimeWarning as e:
- # message = e.args[0]
- # msg = qt.QMessageBox(self)
- # msg.setIcon(qt.QMessageBox.Warning)
- # msg.setText("Mask loaded but an operation was applied.\n" + message)
- # msg.exec_()
- except Exception as e:
- message = e.args[0]
- msg = qt.QMessageBox(self)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setText("Cannot load mask from file. " + message)
- msg.exec_()
-
- def _saveMask(self):
- """Open Save mask dialog"""
- dialog = qt.QFileDialog(self)
- dialog.setWindowTitle("Save Mask")
- dialog.setModal(1)
- filters = [
- 'NumPy binary file (*.npy)',
- 'CSV text file (*.csv)',
- ]
- dialog.setNameFilters(filters)
- dialog.setFileMode(qt.QFileDialog.AnyFile)
- dialog.setAcceptMode(qt.QFileDialog.AcceptSave)
- dialog.setDirectory(self.maskFileDir)
- if not dialog.exec_():
- dialog.close()
- return
-
- # convert filter name to extension name with the .
- extension = dialog.selectedNameFilter().split()[-1][2:-1]
- filename = dialog.selectedFiles()[0]
- dialog.close()
-
- if not filename.lower().endswith(extension):
- filename += extension
-
- if os.path.exists(filename):
- try:
- os.remove(filename)
- except IOError as e:
- msg = qt.QMessageBox(self)
- msg.setWindowTitle("Removing existing file")
- msg.setIcon(qt.QMessageBox.Critical)
-
- if hasattr(e, "strerror"):
- strerror = e.strerror
- else:
- strerror = sys.exc_info()[1]
- msg.setText("Cannot save.\n"
- "Input Output Error: %s" % strerror)
- msg.exec_()
- return
-
- # Update the directory according to the user selection
- self.maskFileDir = os.path.dirname(filename)
-
- try:
- self.save(filename, extension[1:])
- except Exception as e:
- msg = qt.QMessageBox(self)
- msg.setWindowTitle("Saving mask file")
- msg.setIcon(qt.QMessageBox.Critical)
-
- if hasattr(e, "strerror"):
- strerror = e.strerror
- else:
- strerror = sys.exc_info()[1]
- msg.setText("Cannot save file %s\n%s" % (filename, strerror))
- msg.exec_()
-
- def resetSelectionMask(self):
- """Reset the mask"""
- self._mask.reset(
- shape=self._data_scatter.getXData(copy=False).shape)
- self._mask.commit()
-
- def _getPencilWidth(self):
- """Returns the width of the pencil to use in data coordinates`
-
- :rtype: float
- """
- width = super(ScatterMaskToolsWidget, self)._getPencilWidth()
- if self._data_extent is not None:
- width *= 0.01 * self._data_extent
- return width
-
- def _plotDrawEvent(self, event):
- """Handle draw events from the plot"""
- if (self._drawingMode is None or
- event['event'] not in ('drawingProgress', 'drawingFinished')):
- return
-
- if not len(self._data_scatter.getXData(copy=False)):
- return
-
- level = self.levelSpinBox.value()
-
- if self._drawingMode == 'rectangle':
- if event['event'] == 'drawingFinished':
- doMask = self._isMasking()
-
- self._mask.updateRectangle(
- level,
- y=event['y'],
- x=event['x'],
- height=abs(event['height']),
- width=abs(event['width']),
- mask=doMask)
- self._mask.commit()
-
- elif self._drawingMode == 'ellipse':
- if event['event'] == 'drawingFinished':
- doMask = self._isMasking()
- center = event['points'][0]
- size = event['points'][1]
- self._mask.updateEllipse(level, center[1], center[0],
- size[1], size[0], doMask)
- self._mask.commit()
-
- elif self._drawingMode == 'polygon':
- if event['event'] == 'drawingFinished':
- doMask = self._isMasking()
- vertices = event['points']
- vertices = vertices[:, (1, 0)] # (y, x)
- self._mask.updatePolygon(level, vertices, doMask)
- self._mask.commit()
-
- elif self._drawingMode == 'pencil':
- doMask = self._isMasking()
- # convert from plot to array coords
- x, y = event['points'][-1]
-
- brushSize = self._getPencilWidth()
-
- if self._lastPencilPos != (y, x):
- if self._lastPencilPos is not None:
- # Draw the line
- self._mask.updateLine(
- level,
- self._lastPencilPos[0], self._lastPencilPos[1],
- y, x,
- brushSize,
- doMask)
-
- # Draw the very first, or last point
- self._mask.updateDisk(level, y, x, brushSize / 2., doMask)
-
- if event['event'] == 'drawingFinished':
- self._mask.commit()
- self._lastPencilPos = None
- else:
- self._lastPencilPos = y, x
- else:
- _logger.error("Drawing mode %s unsupported", self._drawingMode)
-
- def _loadRangeFromColormapTriggered(self):
- """Set range from active scatter colormap range"""
- if self._data_scatter is not None:
- # Update thresholds according to colormap
- colormap = self._data_scatter.getColormap()
- if colormap['autoscale']:
- min_ = numpy.nanmin(self._data_scatter.getValueData(copy=False))
- max_ = numpy.nanmax(self._data_scatter.getValueData(copy=False))
- else:
- min_, max_ = colormap['vmin'], colormap['vmax']
- self.minLineEdit.setText(str(min_))
- self.maxLineEdit.setText(str(max_))
-
-
-class ScatterMaskToolsDockWidget(BaseMaskToolsDockWidget):
- """:class:`ScatterMaskToolsWidget` embedded in a QDockWidget.
-
- For integration in a :class:`PlotWindow`.
-
- :param parent: See :class:`QDockWidget`
- :param plot: The PlotWidget this widget is operating on
- :paran str name: The title of this widget
- """
- def __init__(self, parent=None, plot=None, name='Mask'):
- widget = ScatterMaskToolsWidget(plot=plot)
- super(ScatterMaskToolsDockWidget, self).__init__(parent, name, widget)
diff --git a/silx/gui/plot/ScatterView.py b/silx/gui/plot/ScatterView.py
deleted file mode 100644
index 0423648..0000000
--- a/silx/gui/plot/ScatterView.py
+++ /dev/null
@@ -1,405 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""A widget dedicated to display scatter plots
-
-It is based on a :class:`~silx.gui.plot.PlotWidget` with additional tools
-for scatter plots.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "14/06/2018"
-
-
-import logging
-import weakref
-
-import numpy
-
-from . import items
-from . import PlotWidget
-from . import tools
-from .actions import histogram as actions_histogram
-from .tools.profile import ScatterProfileToolBar
-from .ColorBar import ColorBarWidget
-from .ScatterMaskToolsWidget import ScatterMaskToolsWidget
-
-from ..widgets.BoxLayoutDockWidget import BoxLayoutDockWidget
-from .. import qt, icons
-from ...utils.proxy import docstring
-from ...utils.weakref import WeakMethodProxy
-
-
-_logger = logging.getLogger(__name__)
-
-
-class ScatterView(qt.QMainWindow):
- """Main window with a PlotWidget and tools specific for scatter plots.
-
- :param parent: The parent of this widget
- :param backend: The backend to use for the plot (default: matplotlib).
- See :class:`~silx.gui.plot.PlotWidget` for the list of supported backend.
- :type backend: Union[str,~silx.gui.plot.backends.BackendBase.BackendBase]
- """
-
- _SCATTER_LEGEND = ' '
- """Legend used for the scatter item"""
-
- def __init__(self, parent=None, backend=None):
- super(ScatterView, self).__init__(parent=parent)
- if parent is not None:
- # behave as a widget
- self.setWindowFlags(qt.Qt.Widget)
- else:
- self.setWindowTitle('ScatterView')
-
- # Create plot widget
- plot = PlotWidget(parent=self, backend=backend)
- self._plot = weakref.ref(plot)
-
- # Add an empty scatter
- self.__createEmptyScatter()
-
- # Create colorbar widget with white background
- self._colorbar = ColorBarWidget(parent=self, plot=plot)
- 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)
-
- # Create PositionInfo widget
- self.__lastPickingPos = None
- self.__pickingCache = None
- self._positionInfo = tools.PositionInfo(
- plot=plot,
- converters=(('X', WeakMethodProxy(self._getPickedX)),
- ('Y', WeakMethodProxy(self._getPickedY)),
- ('Data', WeakMethodProxy(self._getPickedValue)),
- ('Index', WeakMethodProxy(self._getPickedIndex))))
-
- # Combine plot, position info and colorbar into central widget
- gridLayout = qt.QGridLayout()
- gridLayout.setSpacing(0)
- gridLayout.setContentsMargins(0, 0, 0, 0)
- gridLayout.addWidget(plot, 0, 0)
- gridLayout.addWidget(self._colorbar, 0, 1)
- gridLayout.addWidget(self._positionInfo, 1, 0, 1, -1)
- gridLayout.setRowStretch(0, 1)
- gridLayout.setColumnStretch(0, 1)
- centralWidget = qt.QWidget(self)
- centralWidget.setLayout(gridLayout)
- self.setCentralWidget(centralWidget)
-
- # Create mask tool dock widget
- self._maskToolsWidget = ScatterMaskToolsWidget(parent=self, plot=plot)
- self._maskDock = BoxLayoutDockWidget()
- self._maskDock.setWindowTitle('Scatter Mask')
- self._maskDock.setWidget(self._maskToolsWidget)
- self._maskDock.setVisible(False)
- self.addDockWidget(qt.Qt.BottomDockWidgetArea, self._maskDock)
-
- self._maskAction = self._maskDock.toggleViewAction()
- self._maskAction.setIcon(icons.getQIcon('image-mask'))
- self._maskAction.setToolTip("Display/hide mask tools")
-
- self._intensityHistoAction = actions_histogram.PixelIntensitiesHistoAction(plot=plot, parent=self)
-
- # Create toolbars
- self._interactiveModeToolBar = tools.InteractiveModeToolBar(
- parent=self, plot=plot)
-
- self._scatterToolBar = tools.ScatterToolBar(
- parent=self, plot=plot)
- self._scatterToolBar.addAction(self._maskAction)
- self._scatterToolBar.addAction(self._intensityHistoAction)
-
- self._profileToolBar = ScatterProfileToolBar(parent=self, plot=plot)
-
- self._outputToolBar = tools.OutputToolBar(parent=self, plot=plot)
-
- # Activate shortcuts in PlotWindow widget:
- for toolbar in (self._interactiveModeToolBar,
- self._scatterToolBar,
- self._profileToolBar,
- self._outputToolBar):
- self.addToolBar(toolbar)
- for action in toolbar.actions():
- self.addAction(action)
-
-
- def __createEmptyScatter(self):
- """Create an empty scatter item that is used to display the data
-
- :rtype: ~silx.gui.plot.items.Scatter
- """
- plot = self.getPlotWidget()
- plot.addScatter(x=(), y=(), value=(), legend=self._SCATTER_LEGEND)
- scatter = plot._getItem(
- kind='scatter', legend=self._SCATTER_LEGEND)
- # Profile is not selectable,
- # so it does not interfere with profile interaction
- scatter._setSelectable(False)
- return scatter
-
- def _pickScatterData(self, x, y):
- """Get data and index and value of top most scatter plot at position (x, y)
-
- :param float x: X position in plot coordinates
- :param float y: Y position in plot coordinates
- :return: The data index and value at that point or None
- """
- pickingPos = x, y
- if self.__lastPickingPos != pickingPos:
- self.__pickingCache = None
- self.__lastPickingPos = pickingPos
-
- plot = self.getPlotWidget()
- if plot is not None:
- pixelPos = plot.dataToPixel(x, y)
- if pixelPos is not None:
- # Start from top-most item
- result = plot._pickTopMost(
- pixelPos[0], pixelPos[1],
- lambda item: isinstance(item, items.Scatter))
- if result is not None:
- item = result.getItem()
- if item.getVisualization() is items.Scatter.Visualization.BINNED_STATISTIC:
- # Get highest index of closest points
- selected = result.getIndices(copy=False)[::-1]
- dataIndex = selected[numpy.argmin(
- (item.getXData(copy=False)[selected] - x)**2 +
- (item.getYData(copy=False)[selected] - y)**2)]
- else:
- # Get last index
- # with matplotlib it should be the top-most point
- dataIndex = result.getIndices(copy=False)[-1]
- self.__pickingCache = (
- dataIndex,
- item.getXData(copy=False)[dataIndex],
- item.getYData(copy=False)[dataIndex],
- item.getValueData(copy=False)[dataIndex])
-
- return self.__pickingCache
-
- def _getPickedIndex(self, x, y):
- """Get data index of top most scatter plot at position (x, y)
-
- :param float x: X position in plot coordinates
- :param float y: Y position in plot coordinates
- :return: The data index at that point or '-'
- """
- picking = self._pickScatterData(x, y)
- return '-' if picking is None else picking[0]
-
- def _getPickedX(self, x, y):
- """Returns X position snapped to scatter plot when close enough
-
- :param float x:
- :param float y:
- :rtype: float
- """
- picking = self._pickScatterData(x, y)
- return x if picking is None else picking[1]
-
- def _getPickedY(self, x, y):
- """Returns Y position snapped to scatter plot when close enough
-
- :param float x:
- :param float y:
- :rtype: float
- """
- picking = self._pickScatterData(x, y)
- return y if picking is None else picking[2]
-
- def _getPickedValue(self, x, y):
- """Get data value of top most scatter plot at position (x, y)
-
- :param float x: X position in plot coordinates
- :param float y: Y position in plot coordinates
- :return: The data value at that point or '-'
- """
- picking = self._pickScatterData(x, y)
- return '-' if picking is None else picking[3]
-
- def _mouseInPlotArea(self, x, y):
- """Clip mouse coordinates to plot area coordinates
-
- :param float x: X position in pixels
- :param float y: Y position in pixels
- :return: (x, y) in data coordinates
- """
- plot = self.getPlotWidget()
- left, top, width, height = plot.getPlotBoundsInPixels()
- xPlot = numpy.clip(x, left, left + width - 1)
- yPlot = numpy.clip(y, top, top + height - 1)
- return xPlot, yPlot
-
- def getPlotWidget(self):
- """Returns the :class:`~silx.gui.plot.PlotWidget` this window is based on.
-
- :rtype: ~silx.gui.plot.PlotWidget
- """
- return self._plot()
-
- def getPositionInfoWidget(self):
- """Returns the widget display mouse coordinates information.
-
- :rtype: ~silx.gui.plot.tools.PositionInfo
- """
- return self._positionInfo
-
- def getMaskToolsWidget(self):
- """Returns the widget controlling mask drawing
-
- :rtype: ~silx.gui.plot.ScatterMaskToolsWidget
- """
- return self._maskToolsWidget
-
- def getInteractiveModeToolBar(self):
- """Returns QToolBar controlling interactive mode.
-
- :rtype: ~silx.gui.plot.tools.InteractiveModeToolBar
- """
- return self._interactiveModeToolBar
-
- def getScatterToolBar(self):
- """Returns QToolBar providing scatter plot tools.
-
- :rtype: ~silx.gui.plot.tools.ScatterToolBar
- """
- return self._scatterToolBar
-
- def getScatterProfileToolBar(self):
- """Returns QToolBar providing scatter profile tools.
-
- :rtype: ~silx.gui.plot.tools.profile.ScatterProfileToolBar
- """
- return self._profileToolBar
-
- def getOutputToolBar(self):
- """Returns QToolBar containing save, copy and print actions
-
- :rtype: ~silx.gui.plot.tools.OutputToolBar
- """
- return self._outputToolBar
-
- def setColormap(self, colormap=None):
- """Set the colormap for the displayed scatter and the
- default plot colormap.
-
- :param ~silx.gui.colors.Colormap colormap:
- The description of the colormap.
- """
- self.getScatterItem().setColormap(colormap)
- # Resilient to call to PlotWidget API (e.g., clear)
- self.getPlotWidget().setDefaultColormap(colormap)
-
- def getColormap(self):
- """Return the colormap object in use.
-
- :return: Colormap currently in use
- :rtype: ~silx.gui.colors.Colormap
- """
- return self.getScatterItem().getColormap()
-
- # Control displayed scatter plot
-
- def setData(self, x, y, value, xerror=None, yerror=None, alpha=None, copy=True):
- """Set the data of the scatter plot.
-
- To reset the scatter plot, set x, y and value to None.
-
- :param Union[numpy.ndarray,None] x: X coordinates.
- :param Union[numpy.ndarray,None] y: Y coordinates.
- :param Union[numpy.ndarray,None] value:
- The data corresponding to the value of the data points.
- :param xerror: Values with the uncertainties on the x values.
- 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.
- :type xerror: A float, or a numpy.ndarray of float32.
-
- :param yerror: Values with the uncertainties on the y values
- :type yerror: A float, or a numpy.ndarray of float32. See xerror.
- :param alpha: Values with the transparency (between 0 and 1)
- :type alpha: A float, or a numpy.ndarray of float32
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- """
- x = () if x is None else x
- y = () if y is None else y
- value = () if value is None else value
-
- self.getScatterItem().setData(
- x=x, y=y, value=value, xerror=xerror, yerror=yerror, alpha=alpha, copy=copy)
-
- @docstring(items.Scatter)
- def getData(self, *args, **kwargs):
- return self.getScatterItem().getData(*args, **kwargs)
-
- def getScatterItem(self):
- """Returns the plot item displaying the scatter data.
-
- This allows to set the style of the displayed scatter.
-
- :rtype: ~silx.gui.plot.items.Scatter
- """
- plot = self.getPlotWidget()
- scatter = plot._getItem(kind='scatter', legend=self._SCATTER_LEGEND)
- if scatter is None: # Resilient to call to PlotWidget API (e.g., clear)
- scatter = self.__createEmptyScatter()
- return scatter
-
- # Convenient proxies
-
- @docstring(PlotWidget)
- def getXAxis(self, *args, **kwargs):
- return self.getPlotWidget().getXAxis(*args, **kwargs)
-
- @docstring(PlotWidget)
- def getYAxis(self, *args, **kwargs):
- return self.getPlotWidget().getYAxis(*args, **kwargs)
-
- @docstring(PlotWidget)
- def setGraphTitle(self, *args, **kwargs):
- return self.getPlotWidget().setGraphTitle(*args, **kwargs)
-
- @docstring(PlotWidget)
- def getGraphTitle(self, *args, **kwargs):
- return self.getPlotWidget().getGraphTitle(*args, **kwargs)
-
- @docstring(PlotWidget)
- def resetZoom(self, *args, **kwargs):
- return self.getPlotWidget().resetZoom(*args, **kwargs)
-
- @docstring(ScatterMaskToolsWidget)
- def getSelectionMask(self, *args, **kwargs):
- return self.getMaskToolsWidget().getSelectionMask(*args, **kwargs)
-
- @docstring(ScatterMaskToolsWidget)
- def setSelectionMask(self, *args, **kwargs):
- return self.getMaskToolsWidget().setSelectionMask(*args, **kwargs)
diff --git a/silx/gui/plot/StackView.py b/silx/gui/plot/StackView.py
deleted file mode 100644
index 40e0661..0000000
--- a/silx/gui/plot/StackView.py
+++ /dev/null
@@ -1,1254 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""QWidget displaying a 3D volume as a stack of 2D images.
-
-The :class:`StackView` class implements this widget.
-
-Basic usage of :class:`StackView` is through the following methods:
-
-- :meth:`StackView.getColormap`, :meth:`StackView.setColormap` to update the
- default colormap to use and update the currently displayed image.
-- :meth:`StackView.setStack` to update the displayed image.
-
-The :class:`StackView` uses :class:`PlotWindow` and also
-exposes a subset of the :class:`silx.gui.plot.Plot` API for further control
-(plot title, axes labels, ...).
-
-The :class:`StackViewMainWindow` class implements a widget that adds a status
-bar displaying the 3D index and the value under the mouse cursor.
-
-Example::
-
- import numpy
- import sys
- from silx.gui import qt
- from silx.gui.plot.StackView import StackViewMainWindow
-
-
- app = qt.QApplication(sys.argv[1:])
-
- # synthetic data, stack of 100 images of size 200x300
- mystack = numpy.fromfunction(
- lambda i, j, k: numpy.sin(i/15.) + numpy.cos(j/4.) + 2 * numpy.sin(k/6.),
- (100, 200, 300)
- )
-
-
- sv = StackViewMainWindow()
- sv.setColormap("jet", autoscale=True)
- sv.setStack(mystack)
- sv.setLabels(["1st dim (0-99)", "2nd dim (0-199)",
- "3rd dim (0-299)"])
- sv.show()
-
- app.exec_()
-
-"""
-
-__authors__ = ["P. Knobel", "H. Payno"]
-__license__ = "MIT"
-__date__ = "10/10/2018"
-
-import numpy
-import logging
-
-import silx
-from silx.gui import qt
-from .. import icons
-from . import items, PlotWindow, actions
-from .items.image import ImageStack
-from ..colors import Colormap
-from ..colors import cursorColorForColormap
-from .tools import LimitsToolBar
-from .Profile import Profile3DToolBar
-from ..widgets.FrameBrowser import HorizontalSliderWithBrowser
-
-from silx.gui.plot.actions import control as actions_control
-from silx.gui.plot.actions import io as silx_io
-from silx.io.nxdata import save_NXdata
-from silx.utils.array_like import DatasetView, ListOfImages
-from silx.math import calibration
-from silx.utils.deprecation import deprecated_warning
-from silx.utils.deprecation import deprecated
-
-import h5py
-from silx.io.utils import is_dataset
-
-_logger = logging.getLogger(__name__)
-
-
-class StackView(qt.QMainWindow):
- """Stack view widget, to display and browse through stack of
- images.
-
- The profile tool can be switched to "3D" mode, to compute the profile
- on each image of the stack (not only the active image currently displayed)
- and display the result as a slice.
-
- :param QWidget parent: the Qt parent, or None
- :param backend: The backend to use for the plot (default: matplotlib).
- 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.
- :param bool logScale: Toggle visibility of axes log scale actions.
- :param bool grid: Toggle visibility of grid mode action.
- :param bool colormap: Toggle visibility of colormap action.
- :param bool aspectRatio: Toggle visibility of aspect ratio button.
- :param bool yInverted: Toggle visibility of Y axis direction button.
- :param bool copy: Toggle visibility of copy action.
- :param bool save: Toggle visibility of save action.
- :param bool print_: Toggle visibility of print action.
- :param bool control: True to display an Options button with a sub-menu
- to show legends, toggle crosshair and pan with arrows.
- (Default: False)
- :param position: True to display widget with (x, y) mouse position
- (Default: False).
- It also supports a list of (name, funct(x, y)->value)
- to customize the displayed values.
- See :class:`silx.gui.plot.PlotTools.PositionInfo`.
- :param bool mask: Toggle visibilty of mask action.
- """
- # Qt signals
- valueChanged = qt.Signal(object, object, object)
- """Signals that the data value under the cursor has changed.
-
- It provides: row, column, data value.
- """
-
- sigPlaneSelectionChanged = qt.Signal(int)
- """Signal emitted when there is a change is perspective/displayed axes.
-
- It provides the perspective as an integer, with the following meaning:
-
- - 0: axis Y is the 2nd dimension, axis X is the 3rd dimension
- - 1: axis Y is the 1st dimension, axis X is the 3rd dimension
- - 2: axis Y is the 1st dimension, axis X is the 2nd dimension
- """
-
- sigStackChanged = qt.Signal(int)
- """Signal emitted when the stack is changed.
- This happens when a new volume is loaded, or when the current volume
- is transposed (change in perspective).
-
- The signal provides the size (number of pixels) of the stack.
- This will be 0 if the stack is cleared, else it will be a positive
- integer.
- """
-
- sigFrameChanged = qt.Signal(int)
- """Signal emitter when the frame number has changed.
-
- This signal provides the current frame number.
- """
-
- IMAGE_STACK_FILTER_NXDATA = 'Stack of images as NXdata (%s)' % silx_io._NEXUS_HDF5_EXT_STR
-
-
- def __init__(self, parent=None, resetzoom=True, backend=None,
- autoScale=False, logScale=False, grid=False,
- colormap=True, aspectRatio=True, yinverted=True,
- copy=True, save=True, print_=True, control=False,
- position=None, mask=True):
- qt.QMainWindow.__init__(self, parent)
- if parent is not None:
- # behave as a widget
- self.setWindowFlags(qt.Qt.Widget)
- else:
- self.setWindowTitle('StackView')
-
- self._stack = None
- """Loaded stack, as a 3D array, a 3D dataset or a list of 2D arrays."""
- self.__transposed_view = None
- """View on :attr:`_stack` with the axes sorted, to have
- the orthogonal dimension first"""
- self._perspective = 0
- """Orthogonal dimension (depth) in :attr:`_stack`"""
-
- self._stackItem = ImageStack()
- """Hold the item displaying the stack"""
- imageLegend = '__StackView__image' + str(id(self))
- self._stackItem.setName(imageLegend)
-
- self.__autoscaleCmap = False
- """Flag to disable/enable colormap auto-scaling
- based on the min/max values of the entire 3D volume"""
- self.__dimensionsLabels = ["Dimension 0", "Dimension 1",
- "Dimension 2"]
- """These labels are displayed on the X and Y axes.
- :meth:`setLabels` updates this attribute."""
-
- 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`"""
-
- self.calibrations3D = (calibration.NoCalibration(),
- calibration.NoCalibration(),
- calibration.NoCalibration())
-
- central_widget = qt.QWidget(self)
-
- self._plot = PlotWindow(parent=central_widget, backend=backend,
- resetzoom=resetzoom, autoScale=autoScale,
- logScale=logScale, grid=grid,
- curveStyle=False, colormap=colormap,
- aspectRatio=aspectRatio, yInverted=yinverted,
- copy=copy, save=save, print_=print_,
- control=control, position=position,
- roi=False, mask=mask)
- self._plot.addItem(self._stackItem)
- self._plot.getIntensityHistogramAction().setVisible(True)
- self.sigInteractiveModeChanged = self._plot.sigInteractiveModeChanged
- self.sigActiveImageChanged = self._plot.sigActiveImageChanged
- self.sigPlotSignal = self._plot.sigPlotSignal
-
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward':
- self._plot.getYAxis().setInverted(True)
-
- self._addColorBarAction()
-
- self._profileToolBar = Profile3DToolBar(parent=self._plot,
- stackview=self)
- self._plot.addToolBar(self._profileToolBar)
- self._plot.getXAxis().setLabel('Columns')
- self._plot.getYAxis().setLabel('Rows')
- self._plot.sigPlotSignal.connect(self._plotCallback)
- self._plot.getSaveAction().setFileFilter('image', self.IMAGE_STACK_FILTER_NXDATA, func=self._saveImageStack, appendToFile=True)
-
- self.__planeSelection = PlanesWidget(self._plot)
- self.__planeSelection.sigPlaneSelectionChanged.connect(self.setPerspective)
-
- self._browser_label = qt.QLabel("Image index (Dim0):")
-
- self._browser = HorizontalSliderWithBrowser(central_widget)
- self._browser.setRange(0, 0)
- self._browser.valueChanged[int].connect(self.__updateFrameNumber)
- self._browser.setEnabled(False)
-
- layout = qt.QGridLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self._plot, 0, 0, 1, 3)
- layout.addWidget(self.__planeSelection, 1, 0)
- layout.addWidget(self._browser_label, 1, 1)
- layout.addWidget(self._browser, 1, 2)
-
- central_widget.setLayout(layout)
- self.setCentralWidget(central_widget)
-
- # clear profile lines when the perspective changes (plane browsed changed)
- self.__planeSelection.sigPlaneSelectionChanged.connect(
- self._profileToolBar.clearProfile)
-
- def _saveImageStack(self, plot, filename, nameFilter):
- """Save all images from the stack into a volume.
-
- :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.
- :raises: ValueError if nameFilter is invalid
- """
- if not nameFilter == self.IMAGE_STACK_FILTER_NXDATA:
- raise ValueError('Wrong callback')
- entryPath = silx_io.SaveAction._selectWriteableOutputGroup(filename, parent=self)
- if entryPath is None:
- return False
- return save_NXdata(filename,
- nxentry_name=entryPath,
- signal=self.getStack(copy=False, returnNumpyArray=True)[0],
- signal_name="image_stack")
-
- 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._colorbarAction = actions_control.ColorBarAction(self._plot, self._plot)
- self._plot.toolBar().insertAction(actions[index + 1], self._colorbarAction)
-
- def _plotCallback(self, eventDict):
- """Callback for plot events.
-
- Emit :attr:`valueChanged` signal, with (x, y, value) tuple of the
- cursor location in the plot."""
- if eventDict['event'] == 'mouseMoved':
- activeImage = self.getActiveImage()
- if activeImage is not None:
- data = activeImage.getData()
- height, width = data.shape
-
- # Get corresponding coordinate in image
- origin = activeImage.getOrigin()
- scale = activeImage.getScale()
- x = int((eventDict['x'] - origin[0]) / scale[0])
- y = int((eventDict['y'] - origin[1]) / scale[1])
-
- if 0 <= x < width and 0 <= y < height:
- self.valueChanged.emit(float(x), float(y),
- data[y][x])
- else:
- self.valueChanged.emit(float(x), float(y),
- None)
-
- def getPerspective(self):
- """Returns the index of the dimension the stack is browsed with
-
- Possible values are: 0, 1, or 2.
-
- :rtype: int
- """
- return self._perspective
-
- def setPerspective(self, perspective):
- """Set the index of the dimension the stack is browsed with:
-
- - slice plane Dim1-Dim2: perspective 0
- - slice plane Dim0-Dim2: perspective 1
- - slice plane Dim0-Dim1: perspective 2
-
- :param int perspective: Orthogonal dimension number (0, 1, or 2)
- """
- if perspective == self._perspective:
- return
- else:
- if perspective > 2 or perspective < 0:
- raise ValueError(
- "Perspective must be 0, 1 or 2, not %s" % perspective)
-
- self._perspective = int(perspective)
- self.__createTransposedView()
- self.__updateFrameNumber(self._browser.value())
- self._plot.resetZoom()
- self.__updatePlotLabels()
- self._updateTitle()
- self._browser_label.setText("Image index (Dim%d):" %
- (self._first_stack_dimension + perspective))
-
- self.sigPlaneSelectionChanged.emit(perspective)
- self.sigStackChanged.emit(self._stack.size if
- self._stack is not None else 0)
- self.__planeSelection.sigPlaneSelectionChanged.disconnect(self.setPerspective)
- self.__planeSelection.setPerspective(self._perspective)
- self.__planeSelection.sigPlaneSelectionChanged.connect(self.setPerspective)
-
- def __updatePlotLabels(self):
- """Update plot axes labels depending on perspective"""
- y, x = (1, 2) if self._perspective == 0 else \
- (0, 2) if self._perspective == 1 else (0, 1)
- self.setGraphXLabel(self.__dimensionsLabels[x])
- self.setGraphYLabel(self.__dimensionsLabels[y])
-
- def __createTransposedView(self):
- """Create the new view on the stack depending on the perspective
- (set orthogonal axis browsed on the viewer as first dimension)
- """
- assert self._stack is not None
- assert 0 <= self._perspective < 3
-
- # ensure we have the stack encapsulated in an array-like object
- # having a transpose() method
- if isinstance(self._stack, numpy.ndarray):
- self.__transposed_view = self._stack
-
- elif is_dataset(self._stack) or isinstance(self._stack, DatasetView):
- self.__transposed_view = DatasetView(self._stack)
-
- elif isinstance(self._stack, ListOfImages):
- self.__transposed_view = ListOfImages(self._stack)
-
- # transpose the array-like object if necessary
- if self._perspective == 1:
- self.__transposed_view = self.__transposed_view.transpose((1, 0, 2))
- elif self._perspective == 2:
- self.__transposed_view = self.__transposed_view.transpose((2, 0, 1))
-
- self._browser.setRange(0, self.__transposed_view.shape[0] - 1)
- self._browser.setValue(0)
-
- # Update the item structure
- self._stackItem.setStackData(self.__transposed_view, 0, copy=False)
- self._stackItem.setColormap(self.getColormap())
- self._stackItem.setOrigin(self._getImageOrigin())
- self._stackItem.setScale(self._getImageScale())
-
- def __updateFrameNumber(self, index):
- """Update the current image.
-
- :param index: index of the frame to be displayed
- """
- if self.__transposed_view is None:
- # no data set
- return
-
- self._stackItem.setStackPosition(index)
-
- self._updateTitle()
- self.sigFrameChanged.emit(index)
-
- def _set3DScaleAndOrigin(self, calibrations):
- """Set scale and origin for all 3 axes, to be used when plotting
- an image.
-
- See setStack for parameter documentation
- """
- if calibrations is None:
- self.calibrations3D = (calibration.NoCalibration(),
- calibration.NoCalibration(),
- calibration.NoCalibration())
- else:
- self.calibrations3D = []
- for i, calib in enumerate(calibrations):
- if hasattr(calib, "__len__") and len(calib) == 2:
- calib = calibration.LinearCalibration(calib[0], calib[1])
- elif calib is None:
- calib = calibration.NoCalibration()
- elif not isinstance(calib, calibration.AbstractCalibration):
- raise TypeError("calibration must be a 2-tuple, None or" +
- " an instance of an AbstractCalibration " +
- "subclass")
- elif not calib.is_affine():
- _logger.warning(
- "Calibration for dimension %d is not linear, "
- "it will be ignored for scaling the graph axes.",
- i)
- self.calibrations3D.append(calib)
-
- def getCalibrations(self, order='array'):
- """Returns currently used calibrations for each axis
-
- Returned calibrations might differ from the ones that were set as
- non-linear calibrations used for image axes are temporarily ignored.
-
- :param str order:
- 'array' to sort calibrations as data array (dim0, dim1, dim2),
- 'axes' to sort calibrations as currently selected x, y and z axes.
- :return: Calibrations ordered depending on order
- :rtype: List[~silx.math.calibration.AbstractCalibration]
- """
- assert order in ('array', 'axes')
- calibs = []
-
- # filter out non-linear calibration for graph axes
- for index, calib in enumerate(self.calibrations3D):
- if index != self._perspective and not calib.is_affine():
- calib = calibration.NoCalibration()
- calibs.append(calib)
-
- if order == 'axes': # Move 'z' axis to the end
- xy_dims = [d for d in (0, 1, 2) if d != self._perspective]
- calibs = [calibs[max(xy_dims)],
- calibs[min(xy_dims)],
- calibs[self._perspective]]
-
- return tuple(calibs)
-
- def _getImageScale(self):
- """
- :return: 2-tuple (XScale, YScale) for current image view
- """
- xcalib, ycalib, _zcalib = self.getCalibrations(order='axes')
- return xcalib.get_slope(), ycalib.get_slope()
-
- def _getImageOrigin(self):
- """
- :return: 2-tuple (XOrigin, YOrigin) for current image view
- """
- xcalib, ycalib, _zcalib = self.getCalibrations(order='axes')
- return xcalib(0), ycalib(0)
-
- def _getImageZ(self, index):
- """
- :param idx: 0-based image index in the stack
- :return: calibrated Z value corresponding to the image idx
- """
- _xcalib, _ycalib, zcalib = self.getCalibrations(order='axes')
- return zcalib(index)
-
- 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=None, reset=True, calibrations=None):
- """Set the 3D stack.
-
- The perspective parameter is used to define which dimension of the 3D
- array is to be used as frame index. The lowest remaining dimension
- number is the row index of the displayed image (Y axis), and the highest
- remaining dimension is the column index (X axis).
-
- :param stack: 3D stack, or `None` to clear plot.
- :type stack: 3D numpy.ndarray, or 3D h5py.Dataset, or list/tuple of 2D
- numpy arrays, or None.
- :param int perspective: Dimension for the frame index: 0, 1 or 2.
- Use ``None`` to keep the current perspective (default).
- :param bool reset: Whether to reset zoom or not.
- :param calibrations: Sequence of 3 calibration objects for each axis.
- These objects can be a subclass of :class:`AbstractCalibration`,
- or 2-tuples *(a, b)* where *a* is the y-intercept and *b* is the
- slope of a linear calibration (:math:`x \\mapsto a + b x`)
- """
- if stack is None:
- self.clear()
- self.sigStackChanged.emit(0)
- return
-
- self._set3DScaleAndOrigin(calibrations)
-
- # stack as list of 2D arrays: must be converted into an array_like
- if not isinstance(stack, numpy.ndarray):
- if not is_dataset(stack):
- try:
- assert hasattr(stack, "__len__")
- for img in stack:
- assert hasattr(img, "shape")
- assert len(img.shape) == 2
- except AssertionError:
- raise ValueError(
- "Stack must be a 3D array/dataset or a list of " +
- "2D arrays.")
- stack = ListOfImages(stack)
-
- assert len(stack.shape) == 3, "data must be 3D"
-
- self._stack = stack
- self.__createTransposedView()
-
- perspective_changed = False
- if perspective not in [None, self._perspective]:
- perspective_changed = True
- self.setPerspective(perspective)
-
- if self.__autoscaleCmap:
- self.scaleColormapRangeToStack()
-
- # init plot
- self._stackItem.setStackData(self.__transposed_view, 0, copy=False)
- self._stackItem.setColormap(self.getColormap())
- self._stackItem.setOrigin(self._getImageOrigin())
- self._stackItem.setScale(self._getImageScale())
- self._stackItem.setVisible(True)
-
- # Put back the item in the plot in case it was cleared
- exists = self._plot.getImage(self._stackItem.getName())
- if exists is None:
- self._plot.addItem(self._stackItem)
-
- self._plot.setActiveImage(self._stackItem.getName())
- self.__updatePlotLabels()
- self._updateTitle()
-
- if reset:
- self._plot.resetZoom()
-
- # enable and init browser
- self._browser.setEnabled(True)
-
- if not perspective_changed: # avoid double signal (see self.setPerspective)
- self.sigStackChanged.emit(stack.size)
-
- def getStack(self, copy=True, returnNumpyArray=False):
- """Get the original stack, as a 3D array or dataset.
-
- The output has the form: [data, params]
- where params is a dictionary containing display parameters.
-
- :param bool copy: If True (default), then the object is copied
- and returned as a numpy array.
- Else, a reference to original data is returned, if possible.
- If the original data is not a numpy array and parameter
- returnNumpyArray is True, a copy will be made anyway.
- :param bool returnNumpyArray: If True, the returned object is
- guaranteed to be a numpy array.
- :return: 3D stack and parameters.
- :rtype: (numpy.ndarray, dict)
- """
- if self._stack is None:
- return None
-
- image = self._stackItem
- colormap = image.getColormap()
-
- params = {
- 'info': image.getInfo(),
- 'origin': image.getOrigin(),
- 'scale': image.getScale(),
- 'z': image.getZValue(),
- 'selectable': image.isSelectable(),
- 'draggable': image.isDraggable(),
- 'colormap': colormap,
- 'xlabel': image.getXLabel(),
- 'ylabel': image.getYLabel(),
- }
- if returnNumpyArray or copy:
- return numpy.array(self._stack, copy=copy), params
-
- # if a list of 2D arrays was cast into a ListOfImages,
- # return the original list
- if isinstance(self._stack, ListOfImages):
- return self._stack.images, params
-
- return self._stack, params
-
- def getCurrentView(self, copy=True, returnNumpyArray=False):
- """Get the stack, as it is currently displayed.
-
- The first index of the returned stack is always the frame
- index. If the perspective has been changed in the widget since the
- data was first loaded, this will be reflected in the order of the
- dimensions of the returned object.
-
- The output has the form: [data, params]
- where params is a dictionary containing display parameters.
-
- :param bool copy: If True (default), then the object is copied
- and returned as a numpy array.
- Else, a reference to original data is returned, if possible.
- If the original data is not a numpy array and parameter
- `returnNumpyArray` is `True`, a copy will be made anyway.
- :param bool returnNumpyArray: If `True`, the returned object is
- guaranteed to be a numpy array.
- :return: 3D stack and parameters.
- :rtype: (numpy.ndarray, dict)
- """
- image = self.getActiveImage()
- if image is None:
- return None
-
- if isinstance(image, items.ColormapMixIn):
- colormap = image.getColormap()
- else:
- colormap = None
-
- params = {
- 'info': image.getInfo(),
- 'origin': image.getOrigin(),
- 'scale': image.getScale(),
- 'z': image.getZValue(),
- 'selectable': image.isSelectable(),
- 'draggable': image.isDraggable(),
- 'colormap': colormap,
- 'xlabel': image.getXLabel(),
- 'ylabel': image.getYLabel(),
- }
- if returnNumpyArray or copy:
- return numpy.array(self.__transposed_view, copy=copy), params
- return self.__transposed_view, params
-
- def setFrameNumber(self, number):
- """Set the frame selection to a specific value
-
- :param int number: Number of the frame
- """
- self._browser.setValue(number)
-
- def getFrameNumber(self):
- """Set the frame selection to a specific value
-
- :return: Index of currently displayed frame
- :rtype: int
- """
- return self._browser.value()
-
- 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)
-
- 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:
-
- - clear the plot
- - clear the loaded data volume
- """
- self._stack = None
- self.__transposed_view = None
- self._perspective = 0
- self._browser.setEnabled(False)
- # reset browser range
- self._browser.setRange(0, 0)
- self._plot.clear()
-
- def setLabels(self, labels=None):
- """Set the labels to be displayed on the plot axes.
-
- You must provide a sequence of 3 strings, corresponding to the 3
- dimensions of the original data volume.
- The proper label will automatically be selected for each plot axis
- when the volume is rotated (when different axes are selected as the
- X and Y axes).
-
- :param List[str] labels: 3 labels corresponding to the 3 dimensions
- of the data volumes.
- """
-
- default_labels = ["Dimension %d" % self._first_stack_dimension,
- "Dimension %d" % (self._first_stack_dimension + 1),
- "Dimension %d" % (self._first_stack_dimension + 2)]
- if labels is None:
- new_labels = default_labels
- else:
- # filter-out None
- new_labels = []
- for i, label in enumerate(labels):
- new_labels.append(label or default_labels[i])
-
- self.__dimensionsLabels = new_labels
- self.__updatePlotLabels()
-
- def getLabels(self):
- """Return dimension labels displayed on the plot axes
-
- :return: List of three strings corresponding to the 3 dimensions
- of the stack: (name_dim0, name_dim1, name_dim2)
- """
- return self.__dimensionsLabels
-
- def getColormap(self):
- """Get the current colormap description.
-
- :return: A description of the current colormap.
- See :meth:`setColormap` for details.
- :rtype: dict
- """
- # "default" colormap used by addImage when image is added without
- # specifying a special colormap
- return self._plot.getDefaultColormap()
-
- def scaleColormapRangeToStack(self):
- """Scale colormap range according to current stack data.
-
- If no stack has been set through :meth:`setStack`, this has no effect.
-
- The range scaling mode is given by current :class:`Colormap`'s
- :meth:`Colormap.getAutoscaleMode`.
- """
- stack = self.getStack(copy=False, returnNumpyArray=True)
- if stack is None:
- return # No-op
-
- colormap = self.getColormap()
- vmin, vmax = colormap.getColormapRange(data=stack[0])
- colormap.setVRange(vmin=vmin, vmax=vmax)
-
- def setColormap(self, colormap=None, normalization=None,
- autoscale=None, vmin=None, vmax=None, colors=None):
- """Set the colormap and update active image.
-
- Parameters that are not provided are taken from the current colormap.
-
- The colormap parameter can also be a dict with the following keys:
-
- - *name*: string. The colormap to use:
- 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'.
- - *normalization*: string. The mapping to use for the colormap:
- either 'linear' or 'log'.
- - *autoscale*: bool. Whether to use autoscale (True) or range
- provided by keys
- 'vmin' and 'vmax' (False).
- - *vmin*: float. The minimum value of the range to use if 'autoscale'
- is False.
- - *vmax*: float. The maximum value of the range to use if 'autoscale'
- is False.
- - *colors*: optional. Nx3 or Nx4 array of float in [0, 1] or uint8.
- List of RGB or RGBA colors to use (only if name is None)
-
- :param colormap: Name of the colormap in
- 'gray', 'reversed gray', 'temperature', 'red', 'green', 'blue'.
- 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 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
- 'autoscale' is False.
- :param numpy.ndarray colors: Only used if name is None.
- Custom colormap colors as Nx3 or Nx4 RGB or RGBA arrays
- """
- # 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 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
-
- 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:
- 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)
-
- if autoscale is not None:
- deprecated_warning(
- type_='function',
- name='setColormap',
- reason='autoscale argument is replaced by a method',
- replacement='scaleColormapRangeToStack',
- since_version='0.14')
- self.__autoscaleCmap = bool(autoscale)
-
- cursorColor = cursorColorForColormap(_colormap.getName())
- self._plot.setInteractiveMode('zoom', color=cursorColor)
-
- self._plot.setDefaultColormap(_colormap)
-
- # Update active image colormap
- activeImage = self.getActiveImage()
- if isinstance(activeImage, items.ColormapMixIn):
- activeImage.setColormap(self.getColormap())
-
- if self.__autoscaleCmap:
- # scaleColormapRangeToStack needs to be called **after**
- # setDefaultColormap so getColormap returns the right colormap
- self.scaleColormapRangeToStack()
-
-
- @deprecated(replacement="getPlotWidget", since_version="0.13")
- def getPlot(self):
- return self.getPlotWidget()
-
- def getPlotWidget(self):
- """Return the :class:`PlotWidget`.
-
- 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.
-
- :return: instance of :class:`PlotWidget` used in widget
- """
- return self._plot
-
- 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
- """
- return self._profileToolBar
-
- 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 stack image object.
- """
- if just_legend:
- return self._stackItem.getName()
- return self._stackItem
-
- def getColorBarAction(self):
- """Returns the action managing the visibility of the colorbar.
-
- .. warning:: to show/hide the plot colorbar call directly the ColorBar
- widget using getColorBarWidget()
-
- :rtype: QAction
- """
- return self._colorbarAction
-
- def remove(self, legend=None,
- kind=('curve', 'image', 'item', 'marker')):
- """See :meth:`Plot.Plot.remove`"""
- self._plot.remove(legend, kind)
-
- def setInteractiveMode(self, *args, **kwargs):
- """
- See :meth:`Plot.Plot.setInteractiveMode`
- """
- self._plot.setInteractiveMode(*args, **kwargs)
-
- @deprecated(replacement="addShape", since_version="0.13")
- def addItem(self, *args, **kwargs):
- self.addShape(*args, **kwargs)
-
- def addShape(self, *args, **kwargs):
- """
- See :meth:`Plot.Plot.addShape`
- """
- self._plot.addShape(*args, **kwargs)
-
-
-class PlanesWidget(qt.QWidget):
- """Widget for the plane/perspective selection
-
- :param parent: the parent QWidget
- """
- sigPlaneSelectionChanged = qt.Signal(int)
-
- def __init__(self, parent):
- super(PlanesWidget, self).__init__(parent)
-
- self.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Minimum)
- layout0 = qt.QHBoxLayout()
- self.setLayout(layout0)
- layout0.setContentsMargins(0, 0, 0, 0)
-
- layout0.addWidget(qt.QLabel("Axes selection:"))
-
- # By default, the first dimension (dim0) is the frame index/depth/z,
- # the second dimension is the image row number/y axis
- # and the third dimension is the image column index/x axis
-
- # 1
- # | 0
- # |/__2
- self.qcbAxisSelection = qt.QComboBox(self)
- self._setCBChoices(first_stack_dimension=0)
- self.qcbAxisSelection.currentIndexChanged[int].connect(
- self.__planeSelectionChanged)
-
- layout0.addWidget(self.qcbAxisSelection)
-
- def __planeSelectionChanged(self, idx):
- """Callback function when the combobox selection changes
-
- idx is the dimension number orthogonal to the slice plane,
- following the convention:
-
- - slice plane Dim1-Dim2: perspective 0
- - slice plane Dim0-Dim2: perspective 1
- - slice plane Dim0-Dim1: perspective 2
- """
- self.sigPlaneSelectionChanged.emit(idx)
-
- def _setCBChoices(self, first_stack_dimension):
- self.qcbAxisSelection.clear()
-
- dim1dim2 = 'Dim%d-Dim%d' % (first_stack_dimension + 1,
- first_stack_dimension + 2)
- dim0dim2 = 'Dim%d-Dim%d' % (first_stack_dimension,
- first_stack_dimension + 2)
- dim0dim1 = 'Dim%d-Dim%d' % (first_stack_dimension,
- first_stack_dimension + 1)
-
- self.qcbAxisSelection.addItem(icons.getQIcon("cube-front"), dim1dim2)
- self.qcbAxisSelection.addItem(icons.getQIcon("cube-bottom"), dim0dim2)
- self.qcbAxisSelection.addItem(icons.getQIcon("cube-left"), dim0dim1)
-
- def setFirstStackDimension(self, first_stack_dim):
- """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.
- """
- self._setCBChoices(first_stack_dim)
-
- def setPerspective(self, perspective):
- """Update the combobox selection.
-
- - slice plane Dim1-Dim2: perspective 0
- - slice plane Dim0-Dim2: perspective 1
- - slice plane Dim0-Dim1: perspective 2
-
- :param perspective: Orthogonal dimension number (0, 1, or 2)
- """
- self.qcbAxisSelection.setCurrentIndex(perspective)
-
-
-class StackViewMainWindow(StackView):
- """This class is a :class:`StackView` with a menu, an additional toolbar
- to set the plot limits, and a status bar to display the value and 3D
- index of the data samples hovered by the mouse cursor.
-
- :param QWidget parent: Parent widget, or None
- """
- def __init__(self, parent=None):
- self._dataInfo = None
- super(StackViewMainWindow, self).__init__(parent)
- self.setWindowFlags(qt.Qt.Window)
-
- # Add toolbars and status bar
- self.addToolBar(qt.Qt.BottomToolBarArea,
- LimitsToolBar(plot=self._plot))
-
- self.statusBar()
-
- menu = self.menuBar().addMenu('File')
- menu.addAction(self._plot.getOutputToolBar().getSaveAction())
- menu.addAction(self._plot.getOutputToolBar().getPrintAction())
- menu.addSeparator()
- action = menu.addAction('Quit')
- action.triggered[bool].connect(qt.QApplication.instance().quit)
-
- menu = self.menuBar().addMenu('Edit')
- menu.addAction(self._plot.getOutputToolBar().getCopyAction())
- menu.addSeparator()
- menu.addAction(self._plot.getResetZoomAction())
- menu.addAction(self._plot.getColormapAction())
- menu.addAction(self.getColorBarAction())
-
- menu.addAction(actions.control.KeepAspectRatioAction(self._plot, self))
- menu.addAction(actions.control.YAxisInvertedAction(self._plot, self))
-
- menu = self.menuBar().addMenu('Profile')
- profileToolBar = self._profileToolBar
- menu.addAction(profileToolBar.hLineAction)
- menu.addAction(profileToolBar.vLineAction)
- menu.addAction(profileToolBar.lineAction)
- menu.addAction(profileToolBar.crossAction)
- menu.addSeparator()
- menu.addAction(profileToolBar._editor)
- menu.addSeparator()
- menu.addAction(profileToolBar.clearAction)
-
- # Connect to StackView's signal
- self.valueChanged.connect(self._statusBarSlot)
-
- def _statusBarSlot(self, x, y, value):
- """Update status bar with coordinates/value from plots."""
- # todo (after implementing calibration):
- # - use floats for (x, y, z)
- # - display both indices (dim0, dim1, dim2) and (x, y, z)
- msg = "Cursor out of range"
- if x is not None and y is not None:
- img_idx = self._browser.value()
-
- if self._perspective == 0:
- dim0, dim1, dim2 = img_idx, int(y), int(x)
- elif self._perspective == 1:
- dim0, dim1, dim2 = int(y), img_idx, int(x)
- elif self._perspective == 2:
- dim0, dim1, dim2 = int(y), int(x), img_idx
-
- msg = 'Position: (%d, %d, %d)' % (dim0, dim1, dim2)
- if value is not None:
- msg += ', Value: %g' % value
- if self._dataInfo is not None:
- msg = self._dataInfo + ', ' + msg
-
- self.statusBar().showMessage(msg)
-
- def setStack(self, stack, *args, **kwargs):
- """Set the displayed stack.
-
- See :meth:`StackView.setStack` for details.
- """
- if hasattr(stack, 'dtype') and hasattr(stack, 'shape'):
- assert len(stack.shape) == 3
- nframes, height, width = stack.shape
- self._dataInfo = 'Data: %dx%dx%d (%s)' % (nframes, height, width,
- str(stack.dtype))
- self.statusBar().showMessage(self._dataInfo)
- else:
- self._dataInfo = None
-
- # Set the new stack in StackView widget
- super(StackViewMainWindow, self).setStack(stack, *args, **kwargs)
- self.setStatusBar(None)
diff --git a/silx/gui/plot/StatsWidget.py b/silx/gui/plot/StatsWidget.py
deleted file mode 100644
index 6d8739e..0000000
--- a/silx/gui/plot/StatsWidget.py
+++ /dev/null
@@ -1,1661 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""
-Module containing widgets displaying stats from items of a plot.
-"""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "24/07/2018"
-
-
-from collections import OrderedDict
-from contextlib import contextmanager
-import logging
-import weakref
-import functools
-import numpy
-import enum
-from silx.utils.proxy import docstring
-from silx.utils.enum import Enum as _Enum
-from silx.gui import qt
-from silx.gui import icons
-from silx.gui.plot import stats as statsmdl
-from silx.gui.widgets.TableWidget import TableWidget
-from silx.gui.plot.stats.statshandler import StatsHandler, StatFormatter
-from silx.gui.plot.items.core import ItemChangedType
-from silx.gui.widgets.FlowLayout import FlowLayout
-from . import PlotWidget
-from . import items as plotitems
-
-
-_logger = logging.getLogger(__name__)
-
-
-@enum.unique
-class UpdateMode(_Enum):
- AUTO = 'auto'
- MANUAL = 'manual'
-
-
-# Helper class to handle specific calls to PlotWidget and SceneWidget
-
-
-class _Wrapper(qt.QObject):
- """Base class for connection with PlotWidget and SceneWidget.
-
- This class is used when no PlotWidget or SceneWidget is connected.
-
- :param plot: The plot to be used
- """
-
- sigItemAdded = qt.Signal(object)
- """Signal emitted when a new item is added.
-
- It provides the added item.
- """
-
- sigItemRemoved = qt.Signal(object)
- """Signal emitted when an item is (about to be) removed.
-
- It provides the removed item.
- """
-
- sigCurrentChanged = qt.Signal(object)
- """Signal emitted when the current item has changed.
-
- It provides the current item.
- """
-
- sigVisibleDataChanged = qt.Signal()
- """Signal emitted when the visible data area has changed"""
-
- def __init__(self, plot=None):
- super(_Wrapper, self).__init__(parent=None)
- self._plotRef = None if plot is None else weakref.ref(plot)
-
- def getPlot(self):
- """Returns the plot attached to this widget"""
- return None if self._plotRef is None else self._plotRef()
-
- def getItems(self):
- """Returns the list of items in the plot
-
- :rtype: List[object]
- """
- return ()
-
- def getSelectedItems(self):
- """Returns the list of selected items in the plot
-
- :rtype: List[object]
- """
- return ()
-
- def setCurrentItem(self, item):
- """Set the current/active item in the plot
-
- :param item: The plot item to set as active/current
- """
- pass
-
- def getLabel(self, item):
- """Returns the label of the given item.
-
- :param item:
- :rtype: str
- """
- return ''
-
- def getKind(self, item):
- """Returns the kind of an item or None if not supported
-
- :param item:
- :rtype: Union[str,None]
- """
- return None
-
-
-class _PlotWidgetWrapper(_Wrapper):
- """Class handling PlotWidget specific calls and signal connections
-
- See :class:`._Wrapper` for documentation
-
- :param PlotWidget plot:
- """
-
- def __init__(self, plot):
- assert isinstance(plot, PlotWidget)
- super(_PlotWidgetWrapper, self).__init__(plot)
- plot.sigItemAdded.connect(self.sigItemAdded.emit)
- plot.sigItemAboutToBeRemoved.connect(self.sigItemRemoved.emit)
- plot.sigActiveCurveChanged.connect(self._activeCurveChanged)
- plot.sigActiveImageChanged.connect(self._activeImageChanged)
- plot.sigActiveScatterChanged.connect(self._activeScatterChanged)
- plot.sigPlotSignal.connect(self._limitsChanged)
-
- def _activeChanged(self, kind):
- """Handle change of active curve/image/scatter"""
- plot = self.getPlot()
- if plot is not None:
- item = plot._getActiveItem(kind=kind)
- if item is None or self.getKind(item) is not None:
- self.sigCurrentChanged.emit(item)
-
- def _activeCurveChanged(self, previous, current):
- self._activeChanged(kind='curve')
-
- def _activeImageChanged(self, previous, current):
- self._activeChanged(kind='image')
-
- def _activeScatterChanged(self, previous, current):
- self._activeChanged(kind='scatter')
-
- def _limitsChanged(self, event):
- """Handle change of plot area limits."""
- if event['event'] == 'limitsChanged':
- self.sigVisibleDataChanged.emit()
-
- def getItems(self):
- plot = self.getPlot()
- if plot is None:
- return ()
- else:
- return [item for item in plot.getItems() if item.isVisible()]
-
- def getSelectedItems(self):
- plot = self.getPlot()
- items = []
- if plot is not None:
- for kind in plot._ACTIVE_ITEM_KINDS:
- item = plot._getActiveItem(kind=kind)
- if item is not None:
- items.append(item)
- return tuple(items)
-
- def setCurrentItem(self, item):
- plot = self.getPlot()
- if plot is not None:
- kind = self.getKind(item)
- if kind in plot._ACTIVE_ITEM_KINDS:
- if plot._getActiveItem(kind) != item:
- plot._setActiveItem(kind, item.getName())
-
- def getLabel(self, item):
- return item.getName()
-
- def getKind(self, item):
- if isinstance(item, plotitems.Curve):
- return 'curve'
- elif isinstance(item, plotitems.ImageData):
- return 'image'
- elif isinstance(item, plotitems.Scatter):
- return 'scatter'
- elif isinstance(item, plotitems.Histogram):
- return 'histogram'
- else:
- return None
-
-
-class _SceneWidgetWrapper(_Wrapper):
- """Class handling SceneWidget specific calls and signal connections
-
- See :class:`._Wrapper` for documentation
-
- :param SceneWidget plot:
- """
-
- def __init__(self, plot):
- # Lazy-import to avoid circular imports
- from ..plot3d.SceneWidget import SceneWidget
-
- assert isinstance(plot, SceneWidget)
- super(_SceneWidgetWrapper, self).__init__(plot)
- plot.getSceneGroup().sigItemAdded.connect(self.sigItemAdded)
- plot.getSceneGroup().sigItemRemoved.connect(self.sigItemRemoved)
- plot.selection().sigCurrentChanged.connect(self._currentChanged)
- # sigVisibleDataChanged is never emitted
-
- def _currentChanged(self, current, previous):
- self.sigCurrentChanged.emit(current)
-
- def getItems(self):
- plot = self.getPlot()
- return () if plot is None else tuple(plot.getSceneGroup().visit())
-
- def getSelectedItems(self):
- plot = self.getPlot()
- return () if plot is None else (plot.selection().getCurrentItem(),)
-
- def setCurrentItem(self, item):
- plot = self.getPlot()
- if plot is not None:
- plot.selection().setCurrentItem(item)
-
- def getLabel(self, item):
- return item.getLabel()
-
- def getKind(self, item):
- from ..plot3d import items as plot3ditems
-
- if isinstance(item, (plot3ditems.ImageData,
- plot3ditems.ScalarField3D)):
- return 'image'
- elif isinstance(item, (plot3ditems.Scatter2D,
- plot3ditems.Scatter3D)):
- return 'scatter'
- else:
- return None
-
-
-class _ScalarFieldViewWrapper(_Wrapper):
- """Class handling ScalarFieldView specific calls and signal connections
-
- See :class:`._Wrapper` for documentation
-
- :param SceneWidget plot:
- """
-
- def __init__(self, plot):
- # Lazy-import to avoid circular imports
- from ..plot3d.ScalarFieldView import ScalarFieldView
- from ..plot3d.items import ScalarField3D
-
- assert isinstance(plot, ScalarFieldView)
- super(_ScalarFieldViewWrapper, self).__init__(plot)
- self._item = ScalarField3D()
- self._dataChanged()
- plot.sigDataChanged.connect(self._dataChanged)
- # sigItemAdded, sigItemRemoved, sigVisibleDataChanged are never emitted
-
- def _dataChanged(self):
- plot = self.getPlot()
- if plot is not None:
- self._item.setData(plot.getData(copy=False), copy=False)
- self.sigCurrentChanged.emit(self._item)
-
- def getItems(self):
- plot = self.getPlot()
- return () if plot is None else (self._item,)
-
- def getSelectedItems(self):
- return self.getItems()
-
- def setCurrentItem(self, item):
- pass
-
- def getLabel(self, item):
- return 'Data'
-
- def getKind(self, item):
- return 'image'
-
-
-class _Container(object):
- """Class to contain a plot item.
-
- This is apparently needed for compatibility with PySide2,
-
- :param QObject obj:
- """
- def __init__(self, obj):
- self._obj = obj
-
- def __call__(self):
- return self._obj
-
-
-class _StatsWidgetBase(object):
- """
- Base class for all widgets which want to display statistics
- """
-
- def __init__(self, statsOnVisibleData, displayOnlyActItem):
- self._displayOnlyActItem = displayOnlyActItem
- self._statsOnVisibleData = statsOnVisibleData
- self._statsHandler = None
- self._updateMode = UpdateMode.AUTO
-
- self.__default_skipped_events = (
- ItemChangedType.ALPHA,
- ItemChangedType.COLOR,
- ItemChangedType.COLORMAP,
- ItemChangedType.SYMBOL,
- ItemChangedType.SYMBOL_SIZE,
- ItemChangedType.LINE_WIDTH,
- ItemChangedType.LINE_STYLE,
- ItemChangedType.LINE_BG_COLOR,
- ItemChangedType.FILL,
- ItemChangedType.HIGHLIGHTED_COLOR,
- ItemChangedType.HIGHLIGHTED_STYLE,
- ItemChangedType.TEXT,
- ItemChangedType.OVERLAY,
- ItemChangedType.VISUALIZATION_MODE,
- )
-
- self._plotWrapper = _Wrapper()
- self._dealWithPlotConnection(create=True)
-
- def setPlot(self, plot):
- """Define the plot to interact with
-
- :param Union[PlotWidget,SceneWidget,None] plot:
- The plot containing the items on which statistics are applied
- """
- try:
- import OpenGL
- except ImportError:
- has_opengl = False
- else:
- has_opengl = True
- from ..plot3d.SceneWidget import SceneWidget # Lazy import
- self._dealWithPlotConnection(create=False)
- self.clear()
- if plot is None:
- self._plotWrapper = _Wrapper()
- elif isinstance(plot, PlotWidget):
- self._plotWrapper = _PlotWidgetWrapper(plot)
- else:
- if has_opengl is True:
- if isinstance(plot, SceneWidget):
- self._plotWrapper = _SceneWidgetWrapper(plot)
- else: # Expect a ScalarFieldView
- self._plotWrapper = _ScalarFieldViewWrapper(plot)
- else:
- _logger.warning('OpenGL not installed, %s not managed' % ('SceneWidget qnd ScalarFieldView'))
- self._dealWithPlotConnection(create=True)
-
- def setStats(self, statsHandler):
- """Set which stats to display and the associated formatting.
-
- :param StatsHandler statsHandler:
- Set the statistics to be displayed and how to format them using
- """
- if statsHandler is None:
- statsHandler = StatsHandler(statFormatters=())
- elif isinstance(statsHandler, (list, tuple)):
- statsHandler = StatsHandler(statsHandler)
- assert isinstance(statsHandler, StatsHandler)
-
- self._statsHandler = statsHandler
-
- def getStatsHandler(self):
- """Returns the :class:`StatsHandler` in use.
-
- :rtype: StatsHandler
- """
- return self._statsHandler
-
- def getPlot(self):
- """Returns the plot attached to this widget
-
- :rtype: Union[PlotWidget,SceneWidget,None]
- """
- return self._plotWrapper.getPlot()
-
- def _dealWithPlotConnection(self, create=True):
- """Manage connection to plot signals
-
- Note: connection on Item are managed by _addItem and _removeItem methods
- """
- connections = [] # List of (signal, slot) to connect/disconnect
- if self._statsOnVisibleData:
- connections.append(
- (self._plotWrapper.sigVisibleDataChanged, self._updateAllStats))
-
- if self._displayOnlyActItem:
- connections.append(
- (self._plotWrapper.sigCurrentChanged, self._updateCurrentItem))
- else:
- connections += [
- (self._plotWrapper.sigItemAdded, self._addItem),
- (self._plotWrapper.sigItemRemoved, self._removeItem),
- (self._plotWrapper.sigCurrentChanged, self._plotCurrentChanged)]
-
- for signal, slot in connections:
- if create:
- signal.connect(slot)
- else:
- signal.disconnect(slot)
-
- def _updateItemObserve(self, *args):
- """Reload table depending on mode"""
- raise NotImplementedError('Base class')
-
- def _updateCurrentItem(self, *args):
- """specific callback for the sigCurrentChanged and with the
- _displayOnlyActItem option."""
- raise NotImplementedError('Base class')
-
- def _updateStats(self, item, data_changed=False, roi_changed=False):
- """Update displayed information for given plot item
-
- :param item: The plot item
- :param bool data_changed: is the item data changed.
- :param bool roi_changed: is the associated roi changed.
- """
- raise NotImplementedError('Base class')
-
- def _updateAllStats(self):
- """Update stats for all rows in the table"""
- raise NotImplementedError('Base class')
-
- def setDisplayOnlyActiveItem(self, displayOnlyActItem):
- """Toggle display off all items or only the active/selected one
-
- :param bool displayOnlyActItem:
- True if we want to only show active item
- """
- self._displayOnlyActItem = displayOnlyActItem
-
- def setStatsOnVisibleData(self, b):
- """Toggle computation of statistics on whole data or only visible ones.
-
- .. warning:: When visible data is activated we will process to a simple
- filtering of visible data by the user. The filtering is a
- simple data sub-sampling. No interpolation is made to fit
- data to boundaries.
-
- :param bool b: True if we want to apply statistics only on visible data
- """
- if self._statsOnVisibleData != b:
- self._dealWithPlotConnection(create=False)
- self._statsOnVisibleData = b
- self._dealWithPlotConnection(create=True)
- self._updateAllStats()
-
- def _addItem(self, item):
- """Add a plot item to the table
-
- If item is not supported, it is ignored.
-
- :param item: The plot item
- :returns: True if the item is added to the widget.
- :rtype: bool
- """
- raise NotImplementedError('Base class')
-
- def _removeItem(self, item):
- """Remove table items corresponding to given plot item from the table.
-
- :param item: The plot item
- """
- raise NotImplementedError('Base class')
-
- def _plotCurrentChanged(self, current):
- """Handle change of current item and update selection in table
-
- :param current:
- """
- raise NotImplementedError('Base class')
-
- def clear(self):
- """clear GUI"""
- pass
-
- def _skipPlotItemChangedEvent(self, event):
- """
-
- :param ItemChangedtype event: event to filter or not
- :return: True if we want to ignore this ItemChangedtype
- :rtype: bool
- """
- return event in self.__default_skipped_events
-
- def setUpdateMode(self, mode):
- """Set the way to update the displayed statistics.
-
- :param mode: mode requested for update
- :type mode: Union[str,UpdateMode]
- """
- mode = UpdateMode.from_value(mode)
- if mode != self._updateMode:
- self._updateMode = mode
- self._updateModeHasChanged()
-
- def getUpdateMode(self):
- """Returns update mode (See :meth:`setUpdateMode`).
-
- :return: update mode
- :rtype: UpdateMode
- """
- return self._updateMode
-
- def _updateModeHasChanged(self):
- """callback when the update mode has changed"""
- pass
-
-
-class StatsTable(_StatsWidgetBase, TableWidget):
- """
- TableWidget displaying for each items contained by the Plot some
- information:
-
- * legend
- * minimal value
- * maximal value
- * standard deviation (std)
-
- :param QWidget parent: The widget's parent.
- :param Union[PlotWidget,SceneWidget] plot:
- :class:`PlotWidget` or :class:`SceneWidget` instance on which to operate
- """
-
- _LEGEND_HEADER_DATA = 'legend'
- _KIND_HEADER_DATA = 'kind'
-
- sigUpdateModeChanged = qt.Signal(object)
- """Signal emitted when the update mode changed"""
-
- def __init__(self, parent=None, plot=None):
- TableWidget.__init__(self, parent)
- _StatsWidgetBase.__init__(self, statsOnVisibleData=False,
- displayOnlyActItem=False)
-
- # Init for _displayOnlyActItem == False
- assert self._displayOnlyActItem is False
- self.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
- self.setSelectionMode(qt.QAbstractItemView.SingleSelection)
- self.currentItemChanged.connect(self._currentItemChanged)
-
- self.setRowCount(0)
- self.setColumnCount(2)
-
- # Init headers
- headerItem = qt.QTableWidgetItem(self._LEGEND_HEADER_DATA.title())
- headerItem.setData(qt.Qt.UserRole, self._LEGEND_HEADER_DATA)
- self.setHorizontalHeaderItem(0, headerItem)
- headerItem = qt.QTableWidgetItem(self._KIND_HEADER_DATA.title())
- headerItem.setData(qt.Qt.UserRole, self._KIND_HEADER_DATA)
- self.setHorizontalHeaderItem(1, headerItem)
-
- self.setSortingEnabled(True)
- self.setPlot(plot)
-
- @contextmanager
- def _disableSorting(self):
- """Context manager that disables table sorting
-
- Previous state is restored when leaving
- """
- sorting = self.isSortingEnabled()
- if sorting:
- self.setSortingEnabled(False)
- yield
- if sorting:
- self.setSortingEnabled(sorting)
-
- def setStats(self, statsHandler):
- """Set which stats to display and the associated formatting.
-
- :param StatsHandler statsHandler:
- Set the statistics to be displayed and how to format them using
- """
- self._removeAllItems()
- _StatsWidgetBase.setStats(self, statsHandler)
-
- self.setRowCount(0)
- self.setColumnCount(len(self._statsHandler.stats) + 2) # + legend and kind
-
- for index, stat in enumerate(self._statsHandler.stats.values()):
- headerItem = qt.QTableWidgetItem(stat.name.capitalize())
- headerItem.setData(qt.Qt.UserRole, stat.name)
- if stat.description is not None:
- headerItem.setToolTip(stat.description)
- self.setHorizontalHeaderItem(2 + index, headerItem)
-
- horizontalHeader = self.horizontalHeader()
- if hasattr(horizontalHeader, 'setSectionResizeMode'): # Qt5
- horizontalHeader.setSectionResizeMode(qt.QHeaderView.ResizeToContents)
- else: # Qt4
- horizontalHeader.setResizeMode(qt.QHeaderView.ResizeToContents)
-
- self._updateItemObserve()
-
- def setPlot(self, plot):
- """Define the plot to interact with
-
- :param Union[PlotWidget,SceneWidget,None] plot:
- The plot containing the items on which statistics are applied
- """
- _StatsWidgetBase.setPlot(self, plot)
- self._updateItemObserve()
-
- def clear(self):
- """Define the plot to interact with
-
- :param Union[PlotWidget,SceneWidget,None] plot:
- The plot containing the items on which statistics are applied
- """
- self._removeAllItems()
-
- def _updateItemObserve(self, *args):
- """Reload table depending on mode"""
- self._removeAllItems()
-
- # Get selected or all items from the plot
- if self._displayOnlyActItem: # Only selected
- items = self._plotWrapper.getSelectedItems()
- else: # All items
- items = self._plotWrapper.getItems()
-
- # Add items to the plot
- for item in items:
- self._addItem(item)
-
- def _updateCurrentItem(self, *args):
- """specific callback for the sigCurrentChanged and with the
- _displayOnlyActItem option.
-
- Behavior: create the tableItems if does not exists.
- If exists, update it only when we are in 'auto' mode"""
- if self.getUpdateMode() is UpdateMode.MANUAL:
- # when sigCurrentChanged is giving the current item
- if len(args) > 0 and isinstance(args[0], (plotitems.Curve, plotitems.Histogram, plotitems.ImageData, plotitems.Scatter)):
- item = args[0]
- tableItems = self._itemToTableItems(item)
- # if the table does not exists yet
- if len(tableItems) == 0:
- self._updateItemObserve()
- else:
- # in this case no current item
- self._updateItemObserve(args)
- else:
- # auto mode
- self._updateItemObserve(args)
-
- def _plotCurrentChanged(self, current):
- """Handle change of current item and update selection in table
-
- :param current:
- """
- row = self._itemToRow(current)
- if row is None:
- if self.currentRow() >= 0:
- self.setCurrentCell(-1, -1)
- elif row != self.currentRow():
- self.setCurrentCell(row, 0)
-
- def _tableItemToItem(self, tableItem):
- """Find the plot item corresponding to a table item
-
- :param QTableWidgetItem tableItem:
- :rtype: QObject
- """
- container = tableItem.data(qt.Qt.UserRole)
- return container()
-
- def _itemToRow(self, item):
- """Find the row corresponding to a plot item
-
- :param item: The plot item
- :return: The corresponding row index
- :rtype: Union[int,None]
- """
- for row in range(self.rowCount()):
- tableItem = self.item(row, 0)
- if self._tableItemToItem(tableItem) == item:
- return row
- return None
-
- def _itemToTableItems(self, item):
- """Find all table items corresponding to a plot item
-
- :param item: The plot item
- :return: An ordered dict of column name to QTableWidgetItem mapping
- for the given plot item.
- :rtype: OrderedDict
- """
- result = OrderedDict()
- row = self._itemToRow(item)
- if row is not None:
- for column in range(self.columnCount()):
- tableItem = self.item(row, column)
- if self._tableItemToItem(tableItem) != item:
- _logger.error("Table item/plot item mismatch")
- else:
- header = self.horizontalHeaderItem(column)
- name = header.data(qt.Qt.UserRole)
- result[name] = tableItem
- return result
-
- def _plotItemChanged(self, event):
- """Handle modifications of the items.
-
- :param event:
- """
- if self.getUpdateMode() is UpdateMode.MANUAL:
- return
- if self._skipPlotItemChangedEvent(event) is True:
- return
- else:
- item = self.sender()
- self._updateStats(item, data_changed=True)
- # deal with stat items visibility
- if event is ItemChangedType.VISIBLE:
- if len(self._itemToTableItems(item).items()) > 0:
- item_0 = list(self._itemToTableItems(item).values())[0]
- row_index = item_0.row()
- self.setRowHidden(row_index, not item.isVisible())
-
- def _addItem(self, item):
- """Add a plot item to the table
-
- If item is not supported, it is ignored.
-
- :param item: The plot item
- :returns: True if the item is added to the widget.
- :rtype: bool
- """
- if self._itemToRow(item) is not None:
- _logger.info("Item already present in the table")
- self._updateStats(item)
- return True
-
- kind = self._plotWrapper.getKind(item)
- if kind not in statsmdl.BASIC_COMPATIBLE_KINDS:
- _logger.info("Item has not a supported type: %s", item)
- return False
-
- # Prepare table items
- tableItems = [
- qt.QTableWidgetItem(), # Legend
- qt.QTableWidgetItem()] # Kind
-
- for column in range(2, self.columnCount()):
- header = self.horizontalHeaderItem(column)
- name = header.data(qt.Qt.UserRole)
-
- formatter = self._statsHandler.formatters[name]
- if formatter:
- tableItem = formatter.tabWidgetItemClass()
- else:
- tableItem = qt.QTableWidgetItem()
-
- tooltip = self._statsHandler.stats[name].getToolTip(kind=kind)
- if tooltip is not None:
- tableItem.setToolTip(tooltip)
-
- tableItems.append(tableItem)
-
- # Disable sorting while adding table items
- with self._disableSorting():
- # Add a row to the table
- self.setRowCount(self.rowCount() + 1)
-
- # Add table items to the last row
- row = self.rowCount() - 1
- for column, tableItem in enumerate(tableItems):
- tableItem.setData(qt.Qt.UserRole, _Container(item))
- tableItem.setFlags(
- qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
- self.setItem(row, column, tableItem)
-
- # Update table items content
- self._updateStats(item, data_changed=True)
-
- # Listen for item changes
- # Using queued connection to avoid issue with sender
- # being that of the signal calling the signal
- item.sigItemChanged.connect(self._plotItemChanged,
- qt.Qt.QueuedConnection)
-
- return True
-
- def _removeItem(self, item):
- """Remove table items corresponding to given plot item from the table.
-
- :param item: The plot item
- """
- row = self._itemToRow(item)
- if row is None:
- kind = self._plotWrapper.getKind(item)
- if kind in statsmdl.BASIC_COMPATIBLE_KINDS:
- _logger.error("Removing item that is not in table: %s", str(item))
- return
- item.sigItemChanged.disconnect(self._plotItemChanged)
- self.removeRow(row)
-
- def _removeAllItems(self):
- """Remove content of the table"""
- for row in range(self.rowCount()):
- tableItem = self.item(row, 0)
- item = self._tableItemToItem(tableItem)
- item.sigItemChanged.disconnect(self._plotItemChanged)
- self.clearContents()
- self.setRowCount(0)
-
- def _updateStats(self, item, data_changed=False, roi_changed=False):
- """Update displayed information for given plot item
-
- :param item: The plot item
- :param bool data_changed: is the item data changed.
- :param bool roi_changed: is the associated roi changed.
- """
- if item is None:
- return
- plot = self.getPlot()
- if plot is None:
- _logger.info("Plot not available")
- return
-
- row = self._itemToRow(item)
- if row is None:
- _logger.error("This item is not in the table: %s", str(item))
- return
-
- statsHandler = self.getStatsHandler()
- if statsHandler is not None:
- # _updateStats is call when the plot visible area change.
- # to force stats update we consider roi changed
- if self._statsOnVisibleData:
- roi_changed = True
- else:
- roi_changed = False
- stats = statsHandler.calculate(
- item, plot, self._statsOnVisibleData,
- data_changed=data_changed, roi_changed=roi_changed)
- else:
- stats = {}
-
- with self._disableSorting():
- for name, tableItem in self._itemToTableItems(item).items():
- if name == self._LEGEND_HEADER_DATA:
- text = self._plotWrapper.getLabel(item)
- tableItem.setText(text)
- elif name == self._KIND_HEADER_DATA:
- tableItem.setText(self._plotWrapper.getKind(item))
- else:
- value = stats.get(name)
- if value is None:
- _logger.error("Value not found for: %s", name)
- tableItem.setText('-')
- else:
- tableItem.setText(str(value))
-
- def _updateAllStats(self, is_request=False):
- """Update stats for all rows in the table
-
- :param bool is_request: True if come from a manual request
- """
- if self.getUpdateMode() is UpdateMode.MANUAL and not is_request:
- return
- with self._disableSorting():
- for row in range(self.rowCount()):
- tableItem = self.item(row, 0)
- item = self._tableItemToItem(tableItem)
- self._updateStats(item, data_changed=is_request)
-
- def _currentItemChanged(self, current, previous):
- """Handle change of selection in table and sync plot selection
-
- :param QTableWidgetItem current:
- :param QTableWidgetItem previous:
- """
- if current and current.row() >= 0:
- item = self._tableItemToItem(current)
- self._plotWrapper.setCurrentItem(item)
-
- def setDisplayOnlyActiveItem(self, displayOnlyActItem):
- """Toggle display off all items or only the active/selected one
-
- :param bool displayOnlyActItem:
- True if we want to only show active item
- """
- if self._displayOnlyActItem == displayOnlyActItem:
- return
- self._dealWithPlotConnection(create=False)
- if not self._displayOnlyActItem:
- self.currentItemChanged.disconnect(self._currentItemChanged)
-
- _StatsWidgetBase.setDisplayOnlyActiveItem(self, displayOnlyActItem)
-
- self._updateItemObserve()
- self._dealWithPlotConnection(create=True)
-
- if not self._displayOnlyActItem:
- self.currentItemChanged.connect(self._currentItemChanged)
- self.setSelectionMode(qt.QAbstractItemView.SingleSelection)
- else:
- self.setSelectionMode(qt.QAbstractItemView.NoSelection)
-
- def _updateModeHasChanged(self):
- self.sigUpdateModeChanged.emit(self._updateMode)
-
-
-class UpdateModeWidget(qt.QWidget):
- """Widget used to select the mode of update"""
- sigUpdateModeChanged = qt.Signal(object)
- """signal emitted when the mode for update changed"""
- sigUpdateRequested = qt.Signal()
- """signal emitted when an manual request for example is activate"""
-
- def __init__(self, parent=None):
- qt.QWidget.__init__(self, parent)
- self.setLayout(qt.QHBoxLayout())
- self._buttonGrp = qt.QButtonGroup(parent=self)
- self._buttonGrp.setExclusive(True)
-
- spacer = qt.QSpacerItem(20, 20,
- qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Minimum)
- self.layout().addItem(spacer)
-
- self._autoRB = qt.QRadioButton('auto', parent=self)
- self.layout().addWidget(self._autoRB)
- self._buttonGrp.addButton(self._autoRB)
-
- self._manualRB = qt.QRadioButton('manual', parent=self)
- self.layout().addWidget(self._manualRB)
- self._buttonGrp.addButton(self._manualRB)
- self._manualRB.setChecked(True)
-
- refresh_icon = icons.getQIcon('view-refresh')
- self._updatePB = qt.QPushButton(refresh_icon, '', parent=self)
- self.layout().addWidget(self._updatePB)
-
- # connect signal / SLOT
- self._updatePB.clicked.connect(self._updateRequested)
- self._manualRB.toggled.connect(self._manualButtonToggled)
- self._autoRB.toggled.connect(self._autoButtonToggled)
-
- def _manualButtonToggled(self, checked):
- if checked:
- self.setUpdateMode(UpdateMode.MANUAL)
- self.sigUpdateModeChanged.emit(self.getUpdateMode())
-
- def _autoButtonToggled(self, checked):
- if checked:
- self.setUpdateMode(UpdateMode.AUTO)
- self.sigUpdateModeChanged.emit(self.getUpdateMode())
-
- def _updateRequested(self):
- if self.getUpdateMode() is UpdateMode.MANUAL:
- self.sigUpdateRequested.emit()
-
- def setUpdateMode(self, mode):
- """Set the way to update the displayed statistics.
-
- :param mode: mode requested for update
- :type mode: Union[str,UpdateMode]
- """
- mode = UpdateMode.from_value(mode)
-
- if mode is UpdateMode.AUTO:
- if not self._autoRB.isChecked():
- self._autoRB.setChecked(True)
- elif mode is UpdateMode.MANUAL:
- if not self._manualRB.isChecked():
- self._manualRB.setChecked(True)
- else:
- raise ValueError('mode', mode, 'is not recognized')
-
- def getUpdateMode(self):
- """Returns update mode (See :meth:`setUpdateMode`).
-
- :return: the active update mode
- :rtype: UpdateMode
- """
- if self._manualRB.isChecked():
- return UpdateMode.MANUAL
- elif self._autoRB.isChecked():
- return UpdateMode.AUTO
- else:
- raise RuntimeError("No mode selected")
-
- def showRadioButtons(self, show):
- """show / hide the QRadioButtons
-
- :param bool show: if True make RadioButton visible
- """
- self._autoRB.setVisible(show)
- self._manualRB.setVisible(show)
-
-
-class _OptionsWidget(qt.QToolBar):
-
- def __init__(self, parent=None, updateMode=None, displayOnlyActItem=False):
- assert updateMode is not None
- qt.QToolBar.__init__(self, parent)
- self.setIconSize(qt.QSize(16, 16))
-
- action = qt.QAction(self)
- action.setIcon(icons.getQIcon("stats-active-items"))
- action.setText("Active items only")
- action.setToolTip("Display stats for active items only.")
- action.setCheckable(True)
- action.setChecked(displayOnlyActItem)
- self.__displayActiveItems = action
-
- action = qt.QAction(self)
- action.setIcon(icons.getQIcon("stats-whole-items"))
- action.setText("All items")
- action.setToolTip("Display stats for all available items.")
- action.setCheckable(True)
- self.__displayWholeItems = action
-
- action = qt.QAction(self)
- action.setIcon(icons.getQIcon("stats-visible-data"))
- action.setText("Use the visible data range")
- action.setToolTip("Use the visible data range.<br/>"
- "If activated the data is filtered to only use"
- "visible data of the plot."
- "The filtering is a data sub-sampling."
- "No interpolation is made to fit data to"
- "boundaries.")
- action.setCheckable(True)
- self.__useVisibleData = action
-
- action = qt.QAction(self)
- action.setIcon(icons.getQIcon("stats-whole-data"))
- action.setText("Use the full data range")
- action.setToolTip("Use the full data range.")
- action.setCheckable(True)
- action.setChecked(True)
- self.__useWholeData = action
-
- self.addAction(self.__displayWholeItems)
- self.addAction(self.__displayActiveItems)
- self.addSeparator()
- self.addAction(self.__useVisibleData)
- self.addAction(self.__useWholeData)
-
- self.itemSelection = qt.QActionGroup(self)
- self.itemSelection.setExclusive(True)
- self.itemSelection.addAction(self.__displayActiveItems)
- self.itemSelection.addAction(self.__displayWholeItems)
-
- self.dataRangeSelection = qt.QActionGroup(self)
- self.dataRangeSelection.setExclusive(True)
- self.dataRangeSelection.addAction(self.__useWholeData)
- self.dataRangeSelection.addAction(self.__useVisibleData)
-
- self.__updateStatsAction = qt.QAction(self)
- self.__updateStatsAction.setIcon(icons.getQIcon("view-refresh"))
- self.__updateStatsAction.setText("update statistics")
- self.__updateStatsAction.setToolTip("update statistics")
- self.__updateStatsAction.setCheckable(False)
- self._updateStatsSep = self.addSeparator()
- self.addAction(self.__updateStatsAction)
-
- self._setUpdateMode(mode=updateMode)
-
- # expose API
- self.sigUpdateStats = self.__updateStatsAction.triggered
-
- def isActiveItemMode(self):
- return self.itemSelection.checkedAction() is self.__displayActiveItems
-
- def setDisplayActiveItems(self, only_active):
- self.__displayActiveItems.setChecked(only_active)
- self.__displayWholeItems.setChecked(not only_active)
-
- def isVisibleDataRangeMode(self):
- return self.dataRangeSelection.checkedAction() is self.__useVisibleData
-
- def setVisibleDataRangeModeEnabled(self, enabled):
- """Enable/Disable the visible data range mode
-
- :param bool enabled: True to allow user to choose
- stats on visible data
- """
- self.__useVisibleData.setEnabled(enabled)
- if not enabled:
- self.__useWholeData.setChecked(True)
-
- def _setUpdateMode(self, mode):
- self.__updateStatsAction.setVisible(mode == UpdateMode.MANUAL)
- self._updateStatsSep.setVisible(mode == UpdateMode.MANUAL)
-
- def getUpdateStatsAction(self):
- """
-
- :return: the action for the automatic mode
- :rtype: QAction
- """
- return self.__updateStatsAction
-
-
-class StatsWidget(qt.QWidget):
- """
- Widget displaying a set of :class:`Stat` to be displayed on a
- :class:`StatsTable` and to be apply on items contained in the :class:`Plot`
- Also contains options to:
-
- * compute statistics on all the data or on visible data only
- * show statistics of all items or only the active one
-
- :param QWidget parent: Qt parent
- :param Union[PlotWidget,SceneWidget] plot:
- The plot containing items on which we want statistics.
- :param StatsHandler stats:
- Set the statistics to be displayed and how to format them using
- """
-
- sigVisibilityChanged = qt.Signal(bool)
- """Signal emitted when the visibility of this widget changes.
-
- It Provides the visibility of the widget.
- """
-
- NUMBER_FORMAT = '{0:.3f}'
-
- def __init__(self, parent=None, plot=None, stats=None):
- qt.QWidget.__init__(self, parent)
- self.setLayout(qt.QVBoxLayout())
- self.layout().setContentsMargins(0, 0, 0, 0)
- self._options = _OptionsWidget(parent=self, updateMode=UpdateMode.MANUAL)
- self.layout().addWidget(self._options)
- self._statsTable = StatsTable(parent=self, plot=plot)
- self._statsTable.setDisplayOnlyActiveItem(self._options.isActiveItemMode())
- self._options._setUpdateMode(mode=self._statsTable.getUpdateMode())
- self.setStats(stats)
-
- self.layout().addWidget(self._statsTable)
-
- old = self._statsTable.blockSignals(True)
- self._options.itemSelection.triggered.connect(
- self._optSelectionChanged)
- self._options.dataRangeSelection.triggered.connect(
- self._optDataRangeChanged)
- self._optDataRangeChanged()
- self._statsTable.blockSignals(old)
-
- self._statsTable.sigUpdateModeChanged.connect(self._options._setUpdateMode)
- callback = functools.partial(self._getStatsTable()._updateAllStats, is_request=True)
- self._options.sigUpdateStats.connect(callback)
-
- def _getStatsTable(self):
- """Returns the :class:`StatsTable` used by this widget.
-
- :rtype: StatsTable
- """
- return self._statsTable
-
- def showEvent(self, event):
- self.sigVisibilityChanged.emit(True)
- qt.QWidget.showEvent(self, event)
-
- def hideEvent(self, event):
- self.sigVisibilityChanged.emit(False)
- qt.QWidget.hideEvent(self, event)
-
- def _optSelectionChanged(self, action=None):
- self._getStatsTable().setDisplayOnlyActiveItem(
- self._options.isActiveItemMode())
-
- def _optDataRangeChanged(self, action=None):
- self._getStatsTable().setStatsOnVisibleData(
- self._options.isVisibleDataRangeMode())
-
- # Proxy methods
-
- @docstring(StatsTable)
- def setStats(self, statsHandler):
- return self._getStatsTable().setStats(statsHandler=statsHandler)
-
- @docstring(StatsTable)
- def setPlot(self, plot):
- self._options.setVisibleDataRangeModeEnabled(
- plot is None or isinstance(plot, PlotWidget))
- return self._getStatsTable().setPlot(plot=plot)
-
- @docstring(StatsTable)
- def getPlot(self):
- return self._getStatsTable().getPlot()
-
- @docstring(StatsTable)
- def setDisplayOnlyActiveItem(self, displayOnlyActItem):
- old = self._options.blockSignals(True)
- # update the options
- self._options.setDisplayActiveItems(displayOnlyActItem)
- self._options.blockSignals(old)
- return self._getStatsTable().setDisplayOnlyActiveItem(
- displayOnlyActItem=displayOnlyActItem)
-
- @docstring(StatsTable)
- def setStatsOnVisibleData(self, b):
- return self._getStatsTable().setStatsOnVisibleData(b=b)
-
- @docstring(StatsTable)
- def getUpdateMode(self):
- return self._statsTable.getUpdateMode()
-
- @docstring(StatsTable)
- def setUpdateMode(self, mode):
- self._statsTable.setUpdateMode(mode)
-
-
-DEFAULT_STATS = StatsHandler((
- (statsmdl.StatMin(), StatFormatter()),
- statsmdl.StatCoordMin(),
- (statsmdl.StatMax(), StatFormatter()),
- statsmdl.StatCoordMax(),
- statsmdl.StatCOM(),
- (('mean', numpy.mean), StatFormatter()),
- (('std', numpy.std), StatFormatter()),
-))
-
-
-class BasicStatsWidget(StatsWidget):
- """
- Widget defining a simple set of :class:`Stat` to be displayed on a
- :class:`StatsWidget`.
-
- :param QWidget parent: Qt parent
- :param PlotWidget plot:
- The plot containing items on which we want statistics.
- :param StatsHandler stats:
- Set the statistics to be displayed and how to format them using
-
- .. snapshotqt:: img/BasicStatsWidget.png
- :width: 300px
- :align: center
-
- from silx.gui.plot import Plot1D
- from silx.gui.plot.StatsWidget import BasicStatsWidget
-
- plot = Plot1D()
- x = range(100)
- y = x
- plot.addCurve(x, y, legend='curve_0')
- plot.setActiveCurve('curve_0')
-
- widget = BasicStatsWidget(plot=plot)
- widget.show()
- """
- def __init__(self, parent=None, plot=None):
- StatsWidget.__init__(self, parent=parent, plot=plot,
- stats=DEFAULT_STATS)
-
-
-class _BaseLineStatsWidget(_StatsWidgetBase, qt.QWidget):
- """
- Widget made to display stats into a QLayout with couple (QLabel, QLineEdit)
- created for each stats.
- The layout can be defined prior of adding any statistic.
-
- :param QWidget parent: Qt parent
- :param Union[PlotWidget,SceneWidget] plot:
- The plot containing items on which we want statistics.
- :param str kind: the kind of plotitems we want to display
- :param StatsHandler stats:
- Set the statistics to be displayed and how to format them using
- :param bool statsOnVisibleData: compute statistics for the whole data or
- only visible ones.
- """
-
- sigUpdateModeChanged = qt.Signal(object)
- """Signal emitted when the update mode changed"""
-
- def __init__(self, parent=None, plot=None, kind='curve', stats=None,
- statsOnVisibleData=False):
- self._item_kind = kind
- """The item displayed"""
- self._statQlineEdit = {}
- """list of legends actually displayed"""
- self._n_statistics_per_line = 4
- """number of statistics displayed per line in the grid layout"""
- qt.QWidget.__init__(self, parent)
- _StatsWidgetBase.__init__(self,
- statsOnVisibleData=statsOnVisibleData,
- displayOnlyActItem=True)
- self.setLayout(self._createLayout())
- self.setPlot(plot)
- if stats is not None:
- self.setStats(stats)
-
- def _addItemForStatistic(self, statistic):
- assert isinstance(statistic, statsmdl.StatBase)
- assert statistic.name in self._statsHandler.stats
-
- self.layout().setSpacing(2)
- self.layout().setContentsMargins(2, 2, 2, 2)
-
- if isinstance(self.layout(), qt.QGridLayout):
- parent = self
- else:
- widget = qt.QWidget(parent=self)
- parent = widget
-
- qLabel = qt.QLabel(statistic.name + ':', parent=parent)
- qLineEdit = qt.QLineEdit('', parent=parent)
- qLineEdit.setReadOnly(True)
-
- self._addStatsWidgetsToLayout(qLabel=qLabel, qLineEdit=qLineEdit)
- self._statQlineEdit[statistic.name] = qLineEdit
-
- def setPlot(self, plot):
- """Define the plot to interact with
-
- :param Union[PlotWidget,SceneWidget,None] plot:
- The plot containing the items on which statistics are applied
- """
- _StatsWidgetBase.setPlot(self, plot)
- self._updateAllStats()
-
- def _addStatsWidgetsToLayout(self, qLabel, qLineEdit):
- raise NotImplementedError('Base class')
-
- def setStats(self, statsHandler):
- """Set which stats to display and the associated formatting.
-
- :param StatsHandler statsHandler:
- Set the statistics to be displayed and how to format them using
- """
- _StatsWidgetBase.setStats(self, statsHandler)
- for statName, stat in list(self._statsHandler.stats.items()):
- self._addItemForStatistic(stat)
- self._updateAllStats()
-
- def _activeItemChanged(self, kind, previous, current):
- if self.getUpdateMode() is UpdateMode.MANUAL:
- return
- if kind == self._item_kind:
- self._updateAllStats()
-
- def _updateAllStats(self):
- plot = self.getPlot()
- if plot is not None:
- _items = self._plotWrapper.getSelectedItems()
-
- def kind_filter(_item):
- return self._plotWrapper.getKind(_item) == self.getKind()
- items = list(filter(kind_filter, _items))
- assert len(items) in (0, 1)
- if len(items) == 1:
- self._setItem(items[0])
-
- def setKind(self, kind):
- """Change the kind of active item to display
- :param str kind: kind of item to display information for ('curve' ...)
- """
- if self._item_kind != kind:
- self._item_kind = kind
- self._updateItemObserve()
-
- def getKind(self):
- """
- :return: kind of item we want to compute statistic for
- :rtype: str
- """
- return self._item_kind
-
- def _setItem(self, item, data_changed=True):
- if item is None:
- for stat_name, stat_widget in self._statQlineEdit.items():
- stat_widget.setText('')
- elif (self._statsHandler is not None and len(
- self._statsHandler.stats) > 0):
- plot = self.getPlot()
- if plot is not None:
- statsValDict = self._statsHandler.calculate(item,
- plot,
- self._statsOnVisibleData,
- data_changed=data_changed)
- for statName, statVal in list(statsValDict.items()):
- self._statQlineEdit[statName].setText(statVal)
-
- def _updateItemObserve(self, *argv):
- if self.getUpdateMode() is UpdateMode.MANUAL:
- return
- assert self._displayOnlyActItem
- _items = self._plotWrapper.getSelectedItems()
-
- def kind_filter(_item):
- return self._plotWrapper.getKind(_item) == self.getKind()
- items = list(filter(kind_filter, _items))
- assert len(items) in (0, 1)
- _item = items[0] if len(items) == 1 else None
- self._setItem(_item, data_changed=True)
-
- def _updateCurrentItem(self):
- self._updateItemObserve()
-
- def _createLayout(self):
- """create an instance of the main QLayout"""
- raise NotImplementedError('Base class')
-
- def _addItem(self, item):
- raise NotImplementedError('Display only the active item')
-
- def _removeItem(self, item):
- raise NotImplementedError('Display only the active item')
-
- def _plotCurrentChanged(self, current):
- raise NotImplementedError('Display only the active item')
-
- def _updateModeHasChanged(self):
- self.sigUpdateModeChanged.emit(self._updateMode)
-
-
-class _BasicLineStatsWidget(_BaseLineStatsWidget):
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False):
- _BaseLineStatsWidget.__init__(self, parent=parent, kind=kind,
- plot=plot, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
-
- def _createLayout(self):
- return FlowLayout()
-
- def _addStatsWidgetsToLayout(self, qLabel, qLineEdit):
- # create a mother widget to make sure both qLabel & qLineEdit will
- # always be displayed side by side
- widget = qt.QWidget(parent=self)
- widget.setLayout(qt.QHBoxLayout())
- widget.layout().setSpacing(0)
- widget.layout().setContentsMargins(0, 0, 0, 0)
-
- widget.layout().addWidget(qLabel)
- widget.layout().addWidget(qLineEdit)
-
- self.layout().addWidget(widget)
-
- def _addOptionsWidget(self, widget):
- self.layout().addWidget(widget)
-
-
-class BasicLineStatsWidget(qt.QWidget):
- """
- Widget defining a simple set of :class:`Stat` to be displayed on a
- :class:`LineStatsWidget`.
-
- :param QWidget parent: Qt parent
- :param Union[PlotWidget,SceneWidget] plot:
- The plot containing items on which we want statistics.
- :param str kind: the kind of plotitems we want to display
- :param StatsHandler stats:
- Set the statistics to be displayed and how to format them using
- :param bool statsOnVisibleData: compute statistics for the whole data or
- only visible ones.
- """
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False):
- qt.QWidget.__init__(self, parent)
- self.setLayout(qt.QHBoxLayout())
- self.layout().setSpacing(0)
- self.layout().setContentsMargins(0, 0, 0, 0)
- self._lineStatsWidget = _BasicLineStatsWidget(parent=self, plot=plot,
- kind=kind, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
- self.layout().addWidget(self._lineStatsWidget)
-
- self._options = UpdateModeWidget()
- self._options.setUpdateMode(self._lineStatsWidget.getUpdateMode())
- self._options.showRadioButtons(False)
- self.layout().addWidget(self._options)
-
- # connect Signal ? SLOT
- self._lineStatsWidget.sigUpdateModeChanged.connect(self._options.setUpdateMode)
- self._options.sigUpdateModeChanged.connect(self._lineStatsWidget.setUpdateMode)
- self._options.sigUpdateRequested.connect(self._lineStatsWidget._updateAllStats)
-
- def showControl(self, visible):
- self._options.setVisible(visible)
-
- # Proxy methods
-
- @docstring(_BasicLineStatsWidget)
- def setUpdateMode(self, mode):
- self._lineStatsWidget.setUpdateMode(mode=mode)
-
- @docstring(_BasicLineStatsWidget)
- def getUpdateMode(self):
- return self._lineStatsWidget.getUpdateMode()
-
- @docstring(_BasicLineStatsWidget)
- def setPlot(self, plot):
- self._lineStatsWidget.setPlot(plot=plot)
-
- @docstring(_BasicLineStatsWidget)
- def setStats(self, statsHandler):
- self._lineStatsWidget.setStats(statsHandler=statsHandler)
-
- @docstring(_BasicLineStatsWidget)
- def setKind(self, kind):
- self._lineStatsWidget.setKind(kind=kind)
-
- @docstring(_BasicLineStatsWidget)
- def getKind(self):
- return self._lineStatsWidget.getKind()
-
- @docstring(_BasicLineStatsWidget)
- def setStatsOnVisibleData(self, b):
- self._lineStatsWidget.setStatsOnVisibleData(b)
-
- @docstring(UpdateModeWidget)
- def showRadioButtons(self, show):
- self._options.showRadioButtons(show=show)
-
-
-class _BasicGridStatsWidget(_BaseLineStatsWidget):
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False,
- statsPerLine=4):
- _BaseLineStatsWidget.__init__(self, parent=parent, kind=kind,
- plot=plot, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
- self._n_statistics_per_line = statsPerLine
-
- def _addStatsWidgetsToLayout(self, qLabel, qLineEdit):
- column = len(self._statQlineEdit) % self._n_statistics_per_line
- row = len(self._statQlineEdit) // self._n_statistics_per_line
- self.layout().addWidget(qLabel, row, column * 2)
- self.layout().addWidget(qLineEdit, row, column * 2 + 1)
-
- def _createLayout(self):
- return qt.QGridLayout()
-
-
-class BasicGridStatsWidget(qt.QWidget):
- """
- pymca design like widget
-
- :param QWidget parent: Qt parent
- :param Union[PlotWidget,SceneWidget] plot:
- The plot containing items on which we want statistics.
- :param StatsHandler stats:
- Set the statistics to be displayed and how to format them using
- :param str kind: the kind of plotitems we want to display
- :param bool statsOnVisibleData: compute statistics for the whole data or
- only visible ones.
- :param int statsPerLine: number of statistic to be displayed per line
-
- .. snapshotqt:: img/BasicGridStatsWidget.png
- :width: 600px
- :align: center
-
- from silx.gui.plot import Plot1D
- from silx.gui.plot.StatsWidget import BasicGridStatsWidget
-
- plot = Plot1D()
- x = range(100)
- y = x
- plot.addCurve(x, y, legend='curve_0')
- plot.setActiveCurve('curve_0')
-
- widget = BasicGridStatsWidget(plot=plot, kind='curve')
- widget.show()
- """
-
- def __init__(self, parent=None, plot=None, kind='curve',
- stats=DEFAULT_STATS, statsOnVisibleData=False):
- qt.QWidget.__init__(self, parent)
- self.setLayout(qt.QVBoxLayout())
- self.layout().setSpacing(0)
- self.layout().setContentsMargins(0, 0, 0, 0)
-
- self._options = UpdateModeWidget()
- self._options.showRadioButtons(False)
- self.layout().addWidget(self._options)
-
- self._lineStatsWidget = _BasicGridStatsWidget(parent=self, plot=plot,
- kind=kind, stats=stats,
- statsOnVisibleData=statsOnVisibleData)
- self.layout().addWidget(self._lineStatsWidget)
-
- # tune options
- self._options.setUpdateMode(self._lineStatsWidget.getUpdateMode())
-
- # connect Signal ? SLOT
- self._lineStatsWidget.sigUpdateModeChanged.connect(self._options.setUpdateMode)
- self._options.sigUpdateModeChanged.connect(self._lineStatsWidget.setUpdateMode)
- self._options.sigUpdateRequested.connect(self._lineStatsWidget._updateAllStats)
-
- def showControl(self, visible):
- self._options.setVisible(visible)
-
- @docstring(_BasicGridStatsWidget)
- def setUpdateMode(self, mode):
- self._lineStatsWidget.setUpdateMode(mode=mode)
-
- @docstring(_BasicGridStatsWidget)
- def getUpdateMode(self):
- return self._lineStatsWidget.getUpdateMode()
-
- @docstring(_BasicGridStatsWidget)
- def setPlot(self, plot):
- self._lineStatsWidget.setPlot(plot=plot)
-
- @docstring(_BasicGridStatsWidget)
- def setStats(self, statsHandler):
- self._lineStatsWidget.setStats(statsHandler=statsHandler)
-
- @docstring(_BasicGridStatsWidget)
- def setKind(self, kind):
- self._lineStatsWidget.setKind(kind=kind)
-
- @docstring(_BasicGridStatsWidget)
- def getKind(self):
- return self._lineStatsWidget.getKind()
-
- @docstring(_BasicGridStatsWidget)
- def setStatsOnVisibleData(self, b):
- self._lineStatsWidget.setStatsOnVisibleData(b)
-
- @docstring(UpdateModeWidget)
- def showRadioButtons(self, show):
- self._options.showRadioButtons(show=show)
diff --git a/silx/gui/plot/_BaseMaskToolsWidget.py b/silx/gui/plot/_BaseMaskToolsWidget.py
deleted file mode 100644
index 407ab11..0000000
--- a/silx/gui/plot/_BaseMaskToolsWidget.py
+++ /dev/null
@@ -1,1282 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 is a collection of base classes used in modules
-:mod:`.MaskToolsWidget` (images) and :mod:`.ScatterMaskToolsWidget`
-"""
-from __future__ import division
-
-__authors__ = ["T. Vincent", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "08/12/2020"
-
-import os
-import weakref
-
-import numpy
-
-from silx.gui import qt, icons
-from silx.gui.widgets.FloatEdit import FloatEdit
-from silx.gui.colors import Colormap
-from silx.gui.colors import rgba
-from .actions.mode import PanModeAction
-
-
-class BaseMask(qt.QObject):
- """Base class for :class:`ImageMask` and :class:`ScatterMask`
-
- A mask field with update operations.
-
- A mask is an array of the same shape as some underlying data. The mask
- array stores integer values in the range 0-255, to allow for 254 levels
- of mask (value 0 is reserved for unmasked data).
-
- The mask is updated using spatial selection methods: data located inside
- a selected area is masked with a specified mask level.
-
- """
-
- sigChanged = qt.Signal()
- """Signal emitted when the mask has changed"""
-
- sigStateChanged = qt.Signal()
- """Signal emitted for each mask commit/undo/redo operation"""
-
- sigUndoable = qt.Signal(bool)
- """Signal emitted when undo becomes possible/impossible"""
-
- sigRedoable = qt.Signal(bool)
- """Signal emitted when redo becomes possible/impossible"""
-
- def __init__(self, dataItem=None):
- self.historyDepth = 10
- """Maximum number of operation stored in history list for undo"""
- # Init lists for undo/redo
- self._history = []
- self._redo = []
-
- # Store the mask
- self._mask = numpy.array((), dtype=numpy.uint8)
-
- # Store the plot item to be masked
- self._dataItem = None
- if dataItem is not None:
- self.setDataItem(dataItem)
- self.reset(self.getDataValues().shape)
- super(BaseMask, self).__init__()
-
- def setDataItem(self, item):
- """Set a data item
-
- :param item: A plot item, subclass of :class:`silx.gui.plot.items.Item`
- :return:
- """
- self._dataItem = item
-
- def getDataItem(self):
- """Returns current plot item the mask is on.
-
- :rtype: Union[~silx.gui.plot.items.Item,None]
- """
- return self._dataItem
-
- def getDataValues(self):
- """Return data values, as a numpy array with the same shape
- as the mask.
-
- This method must be implemented in a subclass, as the way of
- accessing data depends on the data item passed to :meth:`setDataItem`
-
- :return: Data values associated with the data item.
- :rtype: numpy.ndarray
- """
- raise NotImplementedError("To be implemented in subclass")
-
- def _notify(self):
- """Notify of mask change."""
- self.sigChanged.emit()
-
- def getMask(self, copy=True):
- """Get the current mask as a numpy array.
-
- :param bool copy: True (default) to get a copy of the mask.
- If False, the returned array MUST not be modified.
- :return: The array of the mask with dimension of the data to be masked.
- :rtype: numpy.ndarray of uint8
- """
- return numpy.array(self._mask, copy=copy)
-
- def setMask(self, mask, copy=True):
- """Set the mask to a new array.
-
- :param numpy.ndarray mask: The array to use for the mask.
- :type mask: numpy.ndarray of uint8, C-contiguous.
- Array of other types are converted.
- :param bool copy: True (the default) to copy the array,
- False to use it as is if possible.
- """
- self._mask = numpy.array(mask, copy=copy, order='C', dtype=numpy.uint8)
- self._notify()
-
- # History control
- def resetHistory(self):
- """Reset history"""
- self._history = [numpy.array(self._mask, copy=True)]
- self._redo = []
- self.sigUndoable.emit(False)
- self.sigRedoable.emit(False)
-
- def commit(self):
- """Append the current mask to history if changed"""
- if (not self._history or self._redo or
- not numpy.array_equal(self._mask, self._history[-1])):
- if self._redo:
- self._redo = [] # Reset redo as a new action as been performed
- self.sigRedoable[bool].emit(False)
-
- while len(self._history) >= self.historyDepth:
- self._history.pop(0)
- self._history.append(numpy.array(self._mask, copy=True))
-
- if len(self._history) == 2:
- self.sigUndoable.emit(True)
- self.sigStateChanged.emit()
-
- def undo(self):
- """Restore previous mask if any"""
- if len(self._history) > 1:
- self._redo.append(self._history.pop())
- self._mask = numpy.array(self._history[-1], copy=True)
- self._notify() # Do not store this change in history
-
- if len(self._redo) == 1: # First redo
- self.sigRedoable.emit(True)
- if len(self._history) == 1: # Last value in history
- self.sigUndoable.emit(False)
- self.sigStateChanged.emit()
-
- def redo(self):
- """Restore previously undone modification if any"""
- if self._redo:
- self._mask = self._redo.pop()
- self._history.append(numpy.array(self._mask, copy=True))
- self._notify()
-
- if not self._redo: # No more redo
- self.sigRedoable.emit(False)
- if len(self._history) == 2: # Something to undo
- self.sigUndoable.emit(True)
- self.sigStateChanged.emit()
-
- # Whole mask operations
-
- def clear(self, level):
- """Set all values of the given mask level to 0.
-
- :param int level: Value of the mask to set to 0.
- """
- assert 0 < level < 256
- self._mask[self._mask == level] = 0
- self._notify()
-
- def invert(self, level):
- """Invert mask of the given mask level.
-
- 0 values become level and level values become 0.
-
- :param int level: The level to invert.
- """
- assert 0 < level < 256
- masked = self._mask == level
- self._mask[self._mask == 0] = level
- self._mask[masked] = 0
- self._notify()
-
- def reset(self, shape=None):
- """Reset the mask to zero and change its shape.
-
- :param shape: Shape of the new mask with the correct dimensionality
- with regards to the data dimensionality,
- or None to have an empty mask
- :type shape: tuple of int
- """
- if shape is None:
- # assume dimensionality never changes
- shape = (0,) * len(self._mask.shape) # empty array
- shapeChanged = (shape != self._mask.shape)
- self._mask = numpy.zeros(shape, dtype=numpy.uint8)
- if shapeChanged:
- self.resetHistory()
-
- self._notify()
-
- # To be implemented
- def save(self, filename, kind):
- """Save current mask in a file
-
- :param str filename: The file where to save to mask
- :param str kind: The kind of file to save (e.g 'npy')
- :raise Exception: Raised if the file writing fail
- """
- raise NotImplementedError("To be implemented in subclass")
-
- # update thresholds
- def updateStencil(self, level, stencil, mask=True):
- """Mask/Unmask points from boolean mask: all elements that are True
- in the boolean mask are set to ``level`` (if ``mask=True``) or 0
- (if ``mask=False``)
-
- :param int level: Mask level to update.
- :param stencil: Boolean mask.
- :type stencil: numpy.array of same dimension as the mask
- :param bool mask: True to mask (default), False to unmask.
- """
- if mask:
- self._mask[stencil] = level
- else:
- self._mask[numpy.logical_and(self._mask == level, stencil)] = 0
- self._notify()
-
- def updateBelowThreshold(self, level, threshold, mask=True):
- """Mask/unmask all points whose values are below a threshold.
-
- :param int level:
- :param float threshold: Threshold
- :param bool mask: True to mask (default), False to unmask.
- """
- self.updateStencil(level,
- self.getDataValues() < threshold,
- mask)
-
- def updateBetweenThresholds(self, level, min_, max_, mask=True):
- """Mask/unmask all points whose values are in a range.
-
- :param int level:
- :param float min_: Lower threshold
- :param float max_: Upper threshold
- :param bool mask: True to mask (default), False to unmask.
- """
- stencil = numpy.logical_and(min_ <= self.getDataValues(),
- self.getDataValues() <= max_)
- self.updateStencil(level, stencil, mask)
-
- def updateAboveThreshold(self, level, threshold, mask=True):
- """Mask/unmask all points whose values are above a threshold.
-
- :param int level: Mask level to update.
- :param float threshold: Threshold.
- :param bool mask: True to mask (default), False to unmask.
- """
- self.updateStencil(level,
- self.getDataValues() > threshold,
- mask)
-
- def updateNotFinite(self, level, mask=True):
- """Mask/unmask all points whose values are not finite.
-
- :param int level: Mask level to update.
- :param bool mask: True to mask (default), False to unmask.
- """
- self.updateStencil(level,
- numpy.logical_not(numpy.isfinite(self.getDataValues())),
- mask)
-
- # Drawing operations:
- def updateRectangle(self, level, row, col, height, width, mask=True):
- """Mask/Unmask data inside a rectangle, with the given mask level.
-
- :param int level: Mask level to update, in range 1-255.
- :param row: Starting row/y of the rectangle
- :param col: Starting column/x of the rectangle
- :param height:
- :param width:
- :param bool mask: True to mask (default), False to unmask.
- """
- raise NotImplementedError("To be implemented in subclass")
-
- def updatePolygon(self, level, vertices, mask=True):
- """Mask/Unmask data inside a polygon, with the given mask level.
-
- :param int level: Mask level to update.
- :param vertices: Nx2 array of polygon corners as (row, col) / (y, x)
- :param bool mask: True to mask (default), False to unmask.
- """
- raise NotImplementedError("To be implemented in subclass")
-
- def updatePoints(self, level, rows, cols, mask=True):
- """Mask/Unmask points with given coordinates.
-
- :param int level: Mask level to update.
- :param rows: Rows/ordinates (y) of selected points
- :type rows: 1D numpy.ndarray
- :param cols: Columns/abscissa (x) of selected points
- :type cols: 1D numpy.ndarray
- :param bool mask: True to mask (default), False to unmask.
- """
- raise NotImplementedError("To be implemented in subclass")
-
- def updateDisk(self, level, crow, ccol, radius, mask=True):
- """Mask/Unmask data located inside a dick of the given mask level.
-
- :param int level: Mask level to update.
- :param crow: Disk center row/ordinate (y).
- :param ccol: Disk center column/abscissa.
- :param float radius: Radius of the disk in mask array unit
- :param bool mask: True to mask (default), False to unmask.
- """
- raise NotImplementedError("To be implemented in subclass")
-
- def updateEllipse(self, level, crow, ccol, radius_r, radius_c, mask=True):
- """Mask/Unmask a disk of the given mask level.
-
- :param int level: Mask level to update.
- :param int crow: Row of the center of the ellipse
- :param int ccol: Column of the center of the ellipse
- :param float radius_r: Radius of the ellipse in the row
- :param float radius_c: Radius of the ellipse in the column
- :param bool mask: True to mask (default), False to unmask.
- """
- raise NotImplementedError("To be implemented in subclass")
-
- def updateLine(self, level, row0, col0, row1, col1, width, mask=True):
- """Mask/Unmask a line of the given mask level.
-
- :param int level: Mask level to update.
- :param row0: Row/y of the starting point.
- :param col0: Column/x of the starting point.
- :param row1: Row/y of the end point.
- :param col1: Column/x of the end point.
- :param width: Width of the line in mask array unit.
- :param bool mask: True to mask (default), False to unmask.
- """
- raise NotImplementedError("To be implemented in subclass")
-
-
-class BaseMaskToolsWidget(qt.QWidget):
- """Base class for :class:`MaskToolsWidget` (image mask) and
- :class:`scatterMaskToolsWidget`"""
-
- sigMaskChanged = qt.Signal()
- _maxLevelNumber = 255
-
- 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=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._plotRef = weakref.ref(plot)
- self._maskName = '__MASK_TOOLS_%d' % id(self) # Legend of the mask
-
- self._colormap = Colormap(normalization='linear',
- vmin=0,
- vmax=self._maxLevelNumber)
- self._defaultOverlayColor = rgba('gray') # Color of the mask
- self._setMaskColors(1, 0.5) # Set the colormap LUT
-
- 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)
-
- self._drawingMode = None # Store current drawing mode
- self._lastPencilPos = None
- self._multipleMasks = 'exclusive'
-
- self._maskFileDir = qt.QDir.home().absolutePath()
- self.plot.sigInteractiveModeChanged.connect(
- self._interactiveModeChanged)
-
- self._initWidgets()
-
- def _emitSigMaskChanged(self):
- """Notify mask changes"""
- self.sigMaskChanged.emit()
-
- def getMaskedItem(self):
- """Returns the item that is currently being masked
-
- :rtype: Union[~silx.gui.plot.items.Item,None]
- """
- return self._mask.getDataItem()
-
- def getSelectionMask(self, copy=True):
- """Get the current mask as a numpy array.
-
- :param bool copy: True (default) to get a copy of the mask.
- If False, the returned array MUST not be modified.
- :return: The mask (as an array of uint8) with dimension of
- the 'active' plot item.
- If there is no active image or scatter, it returns None.
- :rtype: Union[numpy.ndarray,None]
- """
- mask = self._mask.getMask(copy=copy)
- return None if mask.size == 0 else mask
-
- def setSelectionMask(self, mask):
- """Set the mask: Must be implemented in subclass"""
- raise NotImplementedError()
-
- def resetSelectionMask(self):
- """Reset the mask: Must be implemented in subclass"""
- raise NotImplementedError()
-
- def multipleMasks(self):
- """Return the current mode of multiple masks support.
-
- See :meth:`setMultipleMasks`
- """
- return self._multipleMasks
-
- def setMultipleMasks(self, mode):
- """Set the mode of multiple masks support.
-
- Available modes:
-
- - 'single': Edit a single level of mask
- - 'exclusive': Supports to 256 levels of non overlapping masks
-
- :param str mode: The mode to use
- """
- assert mode in ('exclusive', 'single')
- if mode != self._multipleMasks:
- self._multipleMasks = mode
- self._levelWidget.setVisible(self._multipleMasks != 'single')
- self._clearAllBtn.setVisible(self._multipleMasks != 'single')
-
- def setMaskFileDirectory(self, path):
- """Set the default directory to use by load/save GUI tools
-
- The directory is also updated by the user, if he change the location
- of the dialog.
- """
- self.maskFileDir = path
-
- def getMaskFileDirectory(self):
- """Get the default directory used by load/save GUI tools"""
- return self.maskFileDir
-
- @property
- def maskFileDir(self):
- """The directory from which to load/save mask from/to files."""
- if not os.path.isdir(self._maskFileDir):
- self._maskFileDir = qt.QDir.home().absolutePath()
- return self._maskFileDir
-
- @maskFileDir.setter
- def maskFileDir(self, maskFileDir):
- self._maskFileDir = str(maskFileDir)
-
- @property
- def plot(self):
- """The :class:`.PlotWindow` this widget is attached to."""
- plot = self._plotRef()
- if plot is None:
- raise RuntimeError(
- 'Mask widget attached to a PlotWidget that no longer exists')
- return plot
-
- def setDirection(self, direction=qt.QBoxLayout.LeftToRight):
- """Set the direction of the layout of the widget
-
- :param direction: QBoxLayout direction
- """
- self.layout().setDirection(direction)
-
- def _initWidgets(self):
- """Create widgets"""
- layout = qt.QBoxLayout(qt.QBoxLayout.LeftToRight)
- layout.addWidget(self._initMaskGroupBox())
- layout.addWidget(self._initDrawGroupBox())
- layout.addWidget(self._initThresholdGroupBox())
- layout.addWidget(self._initOtherToolsGroupBox())
- layout.addStretch(1)
- self.setLayout(layout)
-
- @staticmethod
- def _hboxWidget(*widgets, **kwargs):
- """Place widgets in widget with horizontal layout
-
- :param widgets: Widgets to position horizontally
- :param bool stretch: True for trailing stretch (default),
- False for no trailing stretch
- :return: A QWidget with a QHBoxLayout
- """
- stretch = kwargs.get('stretch', True)
-
- layout = qt.QHBoxLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- for widget in widgets:
- layout.addWidget(widget)
- if stretch:
- layout.addStretch(1)
- widget = qt.QWidget()
- widget.setLayout(layout)
- return widget
-
- def _initTransparencyWidget(self):
- """ Init the mask transparency widget """
- transparencyWidget = qt.QWidget(parent=self)
- grid = qt.QGridLayout()
- grid.setContentsMargins(0, 0, 0, 0)
- self.transparencySlider = qt.QSlider(qt.Qt.Horizontal, parent=transparencyWidget)
- self.transparencySlider.setRange(3, 10)
- self.transparencySlider.setValue(8)
- self.transparencySlider.setToolTip(
- 'Set the transparency of the mask display')
- self.transparencySlider.valueChanged.connect(self._updateColors)
- grid.addWidget(qt.QLabel('Display:', parent=transparencyWidget), 0, 0)
- grid.addWidget(self.transparencySlider, 0, 1, 1, 3)
- grid.addWidget(qt.QLabel('<small><b>Transparent</b></small>', parent=transparencyWidget), 1, 1)
- grid.addWidget(qt.QLabel('<small><b>Opaque</b></small>', parent=transparencyWidget), 1, 3)
- transparencyWidget.setLayout(grid)
- return transparencyWidget
-
- def _initMaskGroupBox(self):
- """Init general mask operation widgets"""
-
- # Mask level
- self.levelSpinBox = qt.QSpinBox()
- self.levelSpinBox.setRange(1, self._maxLevelNumber)
- self.levelSpinBox.setToolTip(
- 'Choose which mask level is edited.\n'
- 'A mask can have up to 255 non-overlapping levels.')
- self.levelSpinBox.valueChanged[int].connect(self._updateColors)
- self._levelWidget = self._hboxWidget(qt.QLabel('Mask level:'),
- self.levelSpinBox)
- # Transparency
- self._transparencyWidget = self._initTransparencyWidget()
-
- style = qt.QApplication.style()
-
- def getIcon(*identifiyers):
- for i in identifiyers:
- if isinstance(i, str):
- if qt.QIcon.hasThemeIcon(i):
- return qt.QIcon.fromTheme(i)
- elif isinstance(i, qt.QIcon):
- return i
- else:
- return style.standardIcon(i)
- return qt.QIcon()
-
- undoAction = qt.QAction(self)
- undoAction.setText('Undo')
- icon = getIcon("edit-undo", qt.QStyle.SP_ArrowBack)
- undoAction.setIcon(icon)
- undoAction.setShortcut(qt.QKeySequence.Undo)
- undoAction.setToolTip('Undo last mask change <b>%s</b>' %
- undoAction.shortcut().toString())
- self._mask.sigUndoable.connect(undoAction.setEnabled)
- undoAction.triggered.connect(self._mask.undo)
-
- redoAction = qt.QAction(self)
- redoAction.setText('Redo')
- icon = getIcon("edit-redo", qt.QStyle.SP_ArrowForward)
- redoAction.setIcon(icon)
- redoAction.setShortcut(qt.QKeySequence.Redo)
- redoAction.setToolTip('Redo last undone mask change <b>%s</b>' %
- redoAction.shortcut().toString())
- self._mask.sigRedoable.connect(redoAction.setEnabled)
- redoAction.triggered.connect(self._mask.redo)
-
- loadAction = qt.QAction(self)
- loadAction.setText('Load...')
- icon = icons.getQIcon("document-open")
- loadAction.setIcon(icon)
- loadAction.setToolTip('Load mask from file')
- loadAction.triggered.connect(self._loadMask)
-
- saveAction = qt.QAction(self)
- saveAction.setText('Save...')
- icon = icons.getQIcon("document-save")
- saveAction.setIcon(icon)
- saveAction.setToolTip('Save mask to file')
- saveAction.triggered.connect(self._saveMask)
-
- invertAction = qt.QAction(self)
- invertAction.setText('Invert')
- icon = icons.getQIcon("mask-invert")
- invertAction.setIcon(icon)
- invertAction.setShortcut(qt.Qt.CTRL + qt.Qt.Key_I)
- invertAction.setToolTip('Invert current mask <b>%s</b>' %
- invertAction.shortcut().toString())
- invertAction.triggered.connect(self._handleInvertMask)
-
- clearAction = qt.QAction(self)
- clearAction.setText('Clear')
- icon = icons.getQIcon("mask-clear")
- clearAction.setIcon(icon)
- clearAction.setShortcut(qt.QKeySequence.Delete)
- clearAction.setToolTip('Clear current mask level <b>%s</b>' %
- clearAction.shortcut().toString())
- clearAction.triggered.connect(self._handleClearMask)
-
- clearAllAction = qt.QAction(self)
- clearAllAction.setText('Clear all')
- icon = icons.getQIcon("mask-clear-all")
- clearAllAction.setIcon(icon)
- clearAllAction.setToolTip('Clear all mask levels')
- clearAllAction.triggered.connect(self.resetSelectionMask)
-
- # Buttons group
- margin1 = qt.QWidget(self)
- margin1.setMinimumWidth(6)
- margin2 = qt.QWidget(self)
- margin2.setMinimumWidth(6)
-
- actions = (loadAction, saveAction, margin1,
- undoAction, redoAction, margin2,
- invertAction, clearAction, clearAllAction)
- widgets = []
- for action in actions:
- if isinstance(action, qt.QWidget):
- widgets.append(action)
- continue
- btn = qt.QToolButton()
- btn.setDefaultAction(action)
- widgets.append(btn)
- if action is clearAllAction:
- self._clearAllBtn = btn
- container = self._hboxWidget(*widgets)
- container.layout().setSpacing(1)
-
- layout = qt.QVBoxLayout()
- layout.addWidget(container)
- layout.addWidget(self._levelWidget)
- layout.addWidget(self._transparencyWidget)
- layout.addStretch(1)
-
- maskGroup = qt.QGroupBox('Mask')
- maskGroup.setLayout(layout)
- return maskGroup
-
- def isMaskInteractionActivated(self):
- """Returns true if any mask interaction is activated"""
- return self.drawActionGroup.checkedAction() is not None
-
- def _initDrawGroupBox(self):
- """Init drawing tools widgets"""
- layout = qt.QVBoxLayout()
-
- self.browseAction = PanModeAction(self.plot, self.plot)
- self.addAction(self.browseAction)
-
- # Draw tools
- self.rectAction = qt.QAction(icons.getQIcon('shape-rectangle'),
- 'Rectangle selection',
- self)
- self.rectAction.setToolTip(
- 'Rectangle selection tool: (Un)Mask a rectangular region <b>R</b>')
- self.rectAction.setShortcut(qt.QKeySequence(qt.Qt.Key_R))
- self.rectAction.setCheckable(True)
- self.rectAction.triggered.connect(self._activeRectMode)
- self.addAction(self.rectAction)
-
- self.ellipseAction = qt.QAction(icons.getQIcon('shape-ellipse'),
- 'Circle selection',
- self)
- self.ellipseAction.setToolTip(
- 'Rectangle selection tool: (Un)Mask a circle region <b>R</b>')
- self.ellipseAction.setShortcut(qt.QKeySequence(qt.Qt.Key_R))
- self.ellipseAction.setCheckable(True)
- self.ellipseAction.triggered.connect(self._activeEllipseMode)
- self.addAction(self.ellipseAction)
-
- self.polygonAction = qt.QAction(icons.getQIcon('shape-polygon'),
- 'Polygon selection',
- self)
- self.polygonAction.setShortcut(qt.QKeySequence(qt.Qt.Key_S))
- self.polygonAction.setToolTip(
- 'Polygon selection tool: (Un)Mask a polygonal region <b>S</b><br>'
- 'Left-click to place new polygon corners<br>'
- 'Left-click on first corner to close the polygon')
- self.polygonAction.setCheckable(True)
- self.polygonAction.triggered.connect(self._activePolygonMode)
- self.addAction(self.polygonAction)
-
- self.pencilAction = qt.QAction(icons.getQIcon('draw-pencil'),
- 'Pencil tool',
- self)
- self.pencilAction.setShortcut(qt.QKeySequence(qt.Qt.Key_P))
- self.pencilAction.setToolTip(
- 'Pencil tool: (Un)Mask using a pencil <b>P</b>')
- self.pencilAction.setCheckable(True)
- self.pencilAction.triggered.connect(self._activePencilMode)
- self.addAction(self.pencilAction)
-
- self.drawActionGroup = qt.QActionGroup(self)
- self.drawActionGroup.setExclusive(True)
- self.drawActionGroup.addAction(self.rectAction)
- self.drawActionGroup.addAction(self.ellipseAction)
- self.drawActionGroup.addAction(self.polygonAction)
- self.drawActionGroup.addAction(self.pencilAction)
-
- actions = (self.browseAction, self.rectAction, self.ellipseAction,
- self.polygonAction, self.pencilAction)
- drawButtons = []
- for action in actions:
- btn = qt.QToolButton()
- btn.setDefaultAction(action)
- drawButtons.append(btn)
- container = self._hboxWidget(*drawButtons)
- layout.addWidget(container)
-
- # Mask/Unmask radio buttons
- maskRadioBtn = qt.QRadioButton('Mask')
- maskRadioBtn.setToolTip(
- 'Drawing masks with current level. Press <b>Ctrl</b> to unmask')
- maskRadioBtn.setChecked(True)
-
- unmaskRadioBtn = qt.QRadioButton('Unmask')
- unmaskRadioBtn.setToolTip(
- 'Drawing unmasks with current level. Press <b>Ctrl</b> to mask')
-
- self.maskStateGroup = qt.QButtonGroup()
- self.maskStateGroup.addButton(maskRadioBtn, 1)
- self.maskStateGroup.addButton(unmaskRadioBtn, 0)
-
- self.maskStateWidget = self._hboxWidget(maskRadioBtn, unmaskRadioBtn)
- layout.addWidget(self.maskStateWidget)
-
- self.maskStateWidget.setHidden(True)
-
- # Pencil settings
- self.pencilSetting = self._createPencilSettings(None)
- self.pencilSetting.setVisible(False)
- layout.addWidget(self.pencilSetting)
-
- layout.addStretch(1)
-
- drawGroup = qt.QGroupBox('Draw tools')
- drawGroup.setLayout(layout)
- return drawGroup
-
- def _createPencilSettings(self, parent=None):
- pencilSetting = qt.QWidget(parent)
-
- self.pencilSpinBox = qt.QSpinBox(parent=pencilSetting)
- self.pencilSpinBox.setRange(1, 1024)
- pencilToolTip = """Set pencil drawing tool size in pixels of the image
- on which to make the mask."""
- self.pencilSpinBox.setToolTip(pencilToolTip)
-
- self.pencilSlider = qt.QSlider(qt.Qt.Horizontal, parent=pencilSetting)
- self.pencilSlider.setRange(1, 50)
- self.pencilSlider.setToolTip(pencilToolTip)
-
- pencilLabel = qt.QLabel('Pencil size:', parent=pencilSetting)
-
- layout = qt.QGridLayout()
- layout.addWidget(pencilLabel, 0, 0)
- layout.addWidget(self.pencilSpinBox, 0, 1)
- layout.addWidget(self.pencilSlider, 1, 1)
- pencilSetting.setLayout(layout)
-
- self.pencilSpinBox.valueChanged.connect(self._pencilWidthChanged)
- self.pencilSlider.valueChanged.connect(self._pencilWidthChanged)
-
- return pencilSetting
-
- def _initThresholdGroupBox(self):
- """Init thresholding widgets"""
-
- self.belowThresholdAction = qt.QAction(icons.getQIcon('plot-roi-below'),
- 'Mask below threshold',
- self)
- self.belowThresholdAction.setToolTip(
- 'Mask image where values are below given threshold')
- self.belowThresholdAction.setCheckable(True)
- self.belowThresholdAction.setChecked(True)
-
- self.betweenThresholdAction = qt.QAction(icons.getQIcon('plot-roi-between'),
- 'Mask within range',
- self)
- self.betweenThresholdAction.setToolTip(
- 'Mask image where values are within given range')
- self.betweenThresholdAction.setCheckable(True)
-
- self.aboveThresholdAction = qt.QAction(icons.getQIcon('plot-roi-above'),
- 'Mask above threshold',
- self)
- self.aboveThresholdAction.setToolTip(
- 'Mask image where values are above given threshold')
- self.aboveThresholdAction.setCheckable(True)
-
- self.thresholdActionGroup = qt.QActionGroup(self)
- self.thresholdActionGroup.setExclusive(True)
- self.thresholdActionGroup.addAction(self.belowThresholdAction)
- self.thresholdActionGroup.addAction(self.betweenThresholdAction)
- self.thresholdActionGroup.addAction(self.aboveThresholdAction)
- self.thresholdActionGroup.triggered.connect(
- self._thresholdActionGroupTriggered)
-
- self.loadColormapRangeAction = qt.QAction(icons.getQIcon('view-refresh'),
- 'Set min-max from colormap',
- self)
- self.loadColormapRangeAction.setToolTip(
- 'Set min and max values from current colormap range')
- self.loadColormapRangeAction.setCheckable(False)
- self.loadColormapRangeAction.triggered.connect(
- self._loadRangeFromColormapTriggered)
-
- widgets = []
- for action in self.thresholdActionGroup.actions():
- btn = qt.QToolButton()
- btn.setDefaultAction(action)
- widgets.append(btn)
-
- spacer = qt.QWidget(parent=self)
- spacer.setSizePolicy(qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Preferred)
- widgets.append(spacer)
-
- loadColormapRangeBtn = qt.QToolButton()
- loadColormapRangeBtn.setDefaultAction(self.loadColormapRangeAction)
- widgets.append(loadColormapRangeBtn)
-
- toolBar = self._hboxWidget(*widgets, stretch=False)
-
- config = qt.QGridLayout()
- config.setContentsMargins(0, 0, 0, 0)
-
- self.minLineLabel = qt.QLabel("Min:", self)
- self.minLineEdit = FloatEdit(self, value=0)
- config.addWidget(self.minLineLabel, 0, 0)
- config.addWidget(self.minLineEdit, 0, 1)
-
- self.maxLineLabel = qt.QLabel("Max:", self)
- self.maxLineEdit = FloatEdit(self, value=0)
- config.addWidget(self.maxLineLabel, 1, 0)
- config.addWidget(self.maxLineEdit, 1, 1)
-
- self.applyMaskBtn = qt.QPushButton('Apply mask')
- self.applyMaskBtn.clicked.connect(self._maskBtnClicked)
-
- layout = qt.QVBoxLayout()
- layout.addWidget(toolBar)
- layout.addLayout(config)
- layout.addWidget(self.applyMaskBtn)
- layout.addStretch(1)
-
- self.thresholdGroup = qt.QGroupBox('Threshold')
- self.thresholdGroup.setLayout(layout)
-
- # Init widget state
- self._thresholdActionGroupTriggered(self.belowThresholdAction)
- return self.thresholdGroup
-
- # track widget visibility and plot active image changes
-
- def _initOtherToolsGroupBox(self):
- layout = qt.QVBoxLayout()
-
- self.maskNanBtn = qt.QPushButton('Mask not finite values')
- self.maskNanBtn.setToolTip('Mask Not a Number and infinite values')
- self.maskNanBtn.clicked.connect(self._maskNotFiniteBtnClicked)
- layout.addWidget(self.maskNanBtn)
- layout.addStretch(1)
-
- self.otherToolGroup = qt.QGroupBox('Other tools')
- self.otherToolGroup.setLayout(layout)
- return self.otherToolGroup
-
- def changeEvent(self, event):
- """Reset drawing action when disabling widget"""
- if (event.type() == qt.QEvent.EnabledChange and
- not self.isEnabled() and
- 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
-
- :param str filename: The file where to save to mask
- :param str kind: The kind of file to save in 'edf', 'tif', 'npy'
- :raise Exception: Raised if the process fails
- """
- self._mask.save(filename, kind)
-
- def getCurrentMaskColor(self):
- """Returns the color of the current selected level.
-
- :rtype: A tuple or a python array
- """
- currentLevel = self.levelSpinBox.value()
- if self._defaultColors[currentLevel]:
- return self._defaultOverlayColor
- else:
- return self._overlayColors[currentLevel].tolist()
-
- def _setMaskColors(self, level, alpha):
- """Set-up the mask colormap to highlight current mask level.
-
- :param int level: The mask level to highlight
- :param float alpha: Alpha level of mask in [0., 1.]
- """
- assert 0 < level <= self._maxLevelNumber
-
- colors = numpy.empty((self._maxLevelNumber + 1, 4), dtype=numpy.float32)
-
- # Set color
- colors[:,:3] = self._defaultOverlayColor[:3]
-
- # check if some colors has been directly set by the user
- mask = numpy.equal(self._defaultColors, False)
- colors[mask,:3] = self._overlayColors[mask,:3]
-
- # Set alpha
- colors[:, -1] = alpha / 2.
-
- # Set highlighted level color
- colors[level, 3] = alpha
-
- # Set no mask level
- colors[0] = (0., 0., 0., 0.)
-
- self._colormap.setColormapLUT(colors)
-
- def resetMaskColors(self, level=None):
- """Reset the mask color at the given level to be defaultColors
-
- :param level:
- The index of the mask for which we want to reset the color.
- If none we will reset color for all masks.
- """
- if level is None:
- self._defaultColors[level] = True
- else:
- self._defaultColors[:] = True
-
- self._updateColors()
-
- def setMaskColors(self, rgb, level=None):
- """Set the masks color
-
- :param rgb: The rgb color
- :param level:
- The index of the mask for which we want to change the color.
- If none set this color for all the masks
- """
- rgb = rgba(rgb)[0:3]
- if level is None:
- self._overlayColors[:] = rgb
- self._defaultColors[:] = False
- else:
- self._overlayColors[level] = rgb
- self._defaultColors[level] = False
-
- self._updateColors()
-
- def getMaskColors(self):
- """masks colors getter"""
- return self._overlayColors
-
- def _updateColors(self, *args):
- """Rebuild mask colormap when selected level or transparency change"""
- self._setMaskColors(self.levelSpinBox.value(),
- self.transparencySlider.value() /
- self.transparencySlider.maximum())
- self._updatePlotMask()
- self._updateInteractiveMode()
-
- def _pencilWidthChanged(self, width):
-
- old = self.pencilSpinBox.blockSignals(True)
- try:
- self.pencilSpinBox.setValue(width)
- finally:
- self.pencilSpinBox.blockSignals(old)
-
- old = self.pencilSlider.blockSignals(True)
- try:
- self.pencilSlider.setValue(width)
- finally:
- self.pencilSlider.blockSignals(old)
- self._updateInteractiveMode()
-
- def _updateInteractiveMode(self):
- """Update the current mode to the same if some cached data have to be
- updated. It is the case for the color for example.
- """
- if self._drawingMode == 'rectangle':
- self._activeRectMode()
- elif self._drawingMode == 'ellipse':
- self._activeEllipseMode()
- elif self._drawingMode == 'polygon':
- self._activePolygonMode()
- elif self._drawingMode == 'pencil':
- self._activePencilMode()
-
- def _handleClearMask(self):
- """Handle clear button clicked: reset current level mask"""
- self._mask.clear(self.levelSpinBox.value())
- self._mask.commit()
-
- def _handleInvertMask(self):
- """Invert the current mask level selection."""
- self._mask.invert(self.levelSpinBox.value())
- self._mask.commit()
-
- # Handle drawing tools UI events
-
- def _interactiveModeChanged(self, source):
- """Handle plot interactive mode changed:
-
- If changed from elsewhere, disable drawing tool
- """
- if source is not self:
- 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"""
- if self._drawingMode is None:
- return
- self.plot.sigPlotSignal.disconnect(self._plotDrawEvent)
- self._drawingMode = None
-
- def _activeRectMode(self):
- """Handle rect action mode triggering"""
- self._releaseDrawingMode()
- self._drawingMode = 'rectangle'
- self.plot.sigPlotSignal.connect(self._plotDrawEvent)
- color = self.getCurrentMaskColor()
- self.plot.setInteractiveMode(
- 'draw', shape='rectangle', source=self, color=color)
- self._updateDrawingModeWidgets()
-
- def _activeEllipseMode(self):
- """Handle circle action mode triggering"""
- self._releaseDrawingMode()
- self._drawingMode = 'ellipse'
- self.plot.sigPlotSignal.connect(self._plotDrawEvent)
- color = self.getCurrentMaskColor()
- self.plot.setInteractiveMode(
- 'draw', shape='ellipse', source=self, color=color)
- self._updateDrawingModeWidgets()
-
- def _activePolygonMode(self):
- """Handle polygon action mode triggering"""
- self._releaseDrawingMode()
- self._drawingMode = 'polygon'
- self.plot.sigPlotSignal.connect(self._plotDrawEvent)
- color = self.getCurrentMaskColor()
- self.plot.setInteractiveMode('draw', shape='polygon', source=self, color=color)
- self._updateDrawingModeWidgets()
-
- def _getPencilWidth(self):
- """Returns the width of the pencil to use in data coordinates`
-
- :rtype: float
- """
- return self.pencilSpinBox.value()
-
- def _activePencilMode(self):
- """Handle pencil action mode triggering"""
- self._releaseDrawingMode()
- self._drawingMode = 'pencil'
- self.plot.sigPlotSignal.connect(self._plotDrawEvent)
- color = self.getCurrentMaskColor()
- width = self._getPencilWidth()
- self.plot.setInteractiveMode(
- 'draw', shape='pencil', source=self, color=color, width=width)
- self._updateDrawingModeWidgets()
-
- def _updateDrawingModeWidgets(self):
- self.maskStateWidget.setVisible(self._drawingMode is not None)
- self.pencilSetting.setVisible(self._drawingMode == 'pencil')
-
- # Handle plot drawing events
-
- def _isMasking(self):
- """Returns true if the tool is used for masking, else it is used for
- unmasking.
-
- :rtype: bool"""
- # First draw event, use current modifiers for all draw sequence
- doMask = (self.maskStateGroup.checkedId() == 1)
- if qt.QApplication.keyboardModifiers() & qt.Qt.ControlModifier:
- doMask = not doMask
- return doMask
-
- # Handle threshold UI events
-
- def _thresholdActionGroupTriggered(self, triggeredAction):
- """Threshold action group listener."""
- if triggeredAction is self.belowThresholdAction:
- self.minLineLabel.setVisible(True)
- self.maxLineLabel.setVisible(False)
- self.minLineEdit.setVisible(True)
- self.maxLineEdit.setVisible(False)
- self.applyMaskBtn.setText("Mask below")
- elif triggeredAction is self.betweenThresholdAction:
- self.minLineLabel.setVisible(True)
- self.maxLineLabel.setVisible(True)
- self.minLineEdit.setVisible(True)
- self.maxLineEdit.setVisible(True)
- self.applyMaskBtn.setText("Mask between")
- elif triggeredAction is self.aboveThresholdAction:
- self.minLineLabel.setVisible(False)
- self.maxLineLabel.setVisible(True)
- self.minLineEdit.setVisible(False)
- self.maxLineEdit.setVisible(True)
- self.applyMaskBtn.setText("Mask above")
- self.applyMaskBtn.setToolTip(triggeredAction.toolTip())
-
- def _maskBtnClicked(self):
- if self.belowThresholdAction.isChecked():
- if self.minLineEdit.text():
- self._mask.updateBelowThreshold(self.levelSpinBox.value(),
- self.minLineEdit.value())
- self._mask.commit()
-
- elif self.betweenThresholdAction.isChecked():
- if self.minLineEdit.text() and 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.value())
- self._mask.updateAboveThreshold(self.levelSpinBox.value(),
- max_)
- self._mask.commit()
-
- def _maskNotFiniteBtnClicked(self):
- """Handle not finite mask button clicked: mask NaNs and inf"""
- self._mask.updateNotFinite(
- self.levelSpinBox.value())
- self._mask.commit()
-
-
-class BaseMaskToolsDockWidget(qt.QDockWidget):
- """Base class for :class:`MaskToolsWidget` and
- :class:`ScatterMaskToolsWidget`.
-
- For integration in a :class:`PlotWindow`.
-
- :param parent: See :class:`QDockWidget`
- :paran str name: The title of this widget
- """
-
- sigMaskChanged = qt.Signal()
-
- 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)
-
- def _emitSigMaskChanged(self):
- """Notify mask changes"""
- # must be connected to self.widget().sigMaskChanged in child class
- self.sigMaskChanged.emit()
-
- def getSelectionMask(self, copy=True):
- """Get the current mask as a 2D array.
-
- :param bool copy: True (default) to get a copy of the mask.
- If False, the returned array MUST not be modified.
- :return: The array of the mask with dimension of the 'active' image.
- If there is no active image, an empty array is returned.
- :rtype: 2D numpy.ndarray of uint8
- """
- return self.widget().getSelectionMask(copy=copy)
-
- def setSelectionMask(self, mask, copy=True):
- """Set the mask to a new array.
-
- :param numpy.ndarray mask: The array to use for the mask.
- :type mask: numpy.ndarray of uint8 of dimension 2, C-contiguous.
- Array of other types are converted.
- :param bool copy: True (the default) to copy the array,
- False to use it as is if possible.
- :return: None if failed, shape of mask as 2-tuple if successful.
- The mask can be cropped or padded to fit active image,
- the returned shape is that of the active image.
- """
- 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.
-
- See :class:`QMainWindow`.
- """
- action = super(BaseMaskToolsDockWidget, self).toggleViewAction()
- action.setIcon(icons.getQIcon('image-mask'))
- action.setToolTip("Display/hide mask tools")
- return action
-
- def _dockLocationChanged(self, area):
- if area in (qt.Qt.LeftDockWidgetArea, qt.Qt.RightDockWidgetArea):
- direction = qt.QBoxLayout.TopToBottom
- else:
- direction = qt.QBoxLayout.LeftToRight
- self.widget().setDirection(direction)
-
- def _topLevelChanged(self, topLevel):
- if topLevel:
- self.widget().setDirection(qt.QBoxLayout.LeftToRight)
- self.resize(self.widget().minimumSize())
- self.adjustSize()
-
- def showEvent(self, event):
- """Make sure this widget is raised when it is shown
- (when it is first created as a tab in PlotWindow or when it is shown
- again after hiding).
- """
- self.raise_()
diff --git a/silx/gui/plot/__init__.py b/silx/gui/plot/__init__.py
deleted file mode 100644
index 3a141b3..0000000
--- a/silx/gui/plot/__init__.py
+++ /dev/null
@@ -1,71 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 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 Qt widgets for plotting curves and images.
-
-The plotting API is inherited from the `PyMca <http://pymca.sourceforge.net/>`_
-plot API and is mostly compatible with it.
-
-Those widgets supports interaction (e.g., zoom, pan, selections).
-
-List of Qt widgets:
-
-.. currentmodule:: silx.gui.plot
-
-- :mod:`.PlotWidget`: A widget displaying a single plot.
-- :mod:`.PlotWindow`: A :mod:`.PlotWidget` with a configurable set of tools.
-- :class:`.Plot1D`: A widget with tools for curves.
-- :class:`.Plot2D`: A widget with tools for images.
-- :class:`.ScatterView`: A widget with tools for scatter plot.
-- :class:`.ImageView`: A widget with tools for images and a side histogram.
-- :class:`.StackView`: A widget with tools for a stack of images.
-
-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:`.PlotWidget`).
-
-.. note::
-
- This package depends on matplotlib_.
- The OpenGL backend further depends on
- `PyOpenGL <http://pyopengl.sourceforge.net/>`_ and OpenGL >= 2.1.
-
-.. _matplotlib: http://matplotlib.org/
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/05/2017"
-
-
-from .PlotWidget import PlotWidget # noqa
-from .PlotWindow import PlotWindow, Plot1D, Plot2D # noqa
-from .items.axis import TickMode
-from .ImageView import ImageView # noqa
-from .StackView import StackView # noqa
-from .ScatterView import ScatterView # noqa
-
-__all__ = ['ImageView', 'PlotWidget', 'PlotWindow', 'Plot1D', 'Plot2D',
- 'StackView', 'ScatterView', 'TickMode']
diff --git a/silx/gui/plot/_utils/__init__.py b/silx/gui/plot/_utils/__init__.py
deleted file mode 100644
index 3c2dfa4..0000000
--- a/silx/gui/plot/_utils/__init__.py
+++ /dev/null
@@ -1,93 +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.
-#
-# ###########################################################################*/
-"""Miscellaneous utility functions for the Plot"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "21/03/2017"
-
-
-import numpy
-
-from .panzoom import FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX
-from .panzoom import applyZoomToPlot, applyPan
-
-
-def addMarginsToLimits(margins, isXLog, isYLog,
- xMin, xMax, yMin, yMax, y2Min=None, y2Max=None):
- """Returns updated limits by extending them with margins.
-
- :param margins: The ratio of the margins to add or None for no margins.
- :type margins: A 4-tuple of floats as
- (xMinMargin, xMaxMargin, yMinMargin, yMaxMargin)
-
- :return: The updated limits
- :rtype: tuple of 4 or 6 floats: Either (xMin, xMax, yMin, yMax) or
- (xMin, xMax, yMin, yMax, y2Min, y2Max) if y2Min and y2Max
- are provided.
- """
- if margins is not None:
- xMinMargin, xMaxMargin, yMinMargin, yMaxMargin = margins
-
- if not isXLog:
- xRange = xMax - xMin
- xMin -= xMinMargin * xRange
- xMax += xMaxMargin * xRange
-
- elif xMin > 0. and xMax > 0.: # Log scale
- # Do not apply margins if limits < 0
- xMinLog, xMaxLog = numpy.log10(xMin), numpy.log10(xMax)
- xRangeLog = xMaxLog - xMinLog
- xMin = pow(10., xMinLog - xMinMargin * xRangeLog)
- xMax = pow(10., xMaxLog + xMaxMargin * xRangeLog)
-
- if not isYLog:
- yRange = yMax - yMin
- yMin -= yMinMargin * yRange
- yMax += yMaxMargin * yRange
- elif yMin > 0. and yMax > 0.: # Log scale
- # Do not apply margins if limits < 0
- yMinLog, yMaxLog = numpy.log10(yMin), numpy.log10(yMax)
- yRangeLog = yMaxLog - yMinLog
- yMin = pow(10., yMinLog - yMinMargin * yRangeLog)
- yMax = pow(10., yMaxLog + yMaxMargin * yRangeLog)
-
- if y2Min is not None and y2Max is not None:
- if not isYLog:
- yRange = y2Max - y2Min
- y2Min -= yMinMargin * yRange
- y2Max += yMaxMargin * yRange
- elif y2Min > 0. and y2Max > 0.: # Log scale
- # Do not apply margins if limits < 0
- yMinLog, yMaxLog = numpy.log10(y2Min), numpy.log10(y2Max)
- yRangeLog = yMaxLog - yMinLog
- y2Min = pow(10., yMinLog - yMinMargin * yRangeLog)
- y2Max = pow(10., yMaxLog + yMaxMargin * yRangeLog)
-
- if y2Min is None or y2Max is None:
- return xMin, xMax, yMin, yMax
- else:
- return xMin, xMax, yMin, yMax, y2Min, y2Max
-
diff --git a/silx/gui/plot/_utils/delaunay.py b/silx/gui/plot/_utils/delaunay.py
deleted file mode 100644
index 49ad05f..0000000
--- a/silx/gui/plot/_utils/delaunay.py
+++ /dev/null
@@ -1,62 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2019 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.
-#
-# ###########################################################################*/
-"""Wrapper over Delaunay implementation"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "02/05/2019"
-
-
-import logging
-import sys
-
-import numpy
-
-
-_logger = logging.getLogger(__name__)
-
-
-def delaunay(x, y):
- """Returns Delaunay instance for x, y points
-
- :param numpy.ndarray x:
- :param numpy.ndarray y:
- :rtype: Union[None,scipy.spatial.Delaunay]
- """
- # Lazy-loading of Delaunay
- try:
- from scipy.spatial import Delaunay as _Delaunay
- except ImportError: # Fallback using local Delaunay
- from silx.third_party.scipy_spatial import Delaunay as _Delaunay
-
- points = numpy.array((x, y)).T
- try:
- delaunay = _Delaunay(points)
- except (RuntimeError, ValueError):
- _logger.error("Delaunay tesselation failed: %s",
- sys.exc_info()[1])
- delaunay = None
-
- return delaunay
diff --git a/silx/gui/plot/_utils/dtime_ticklayout.py b/silx/gui/plot/_utils/dtime_ticklayout.py
deleted file mode 100644
index ebf775b..0000000
--- a/silx/gui/plot/_utils/dtime_ticklayout.py
+++ /dev/null
@@ -1,442 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2018 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 implements date-time labels layout on graph axes."""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["P. Kenter"]
-__license__ = "MIT"
-__date__ = "04/04/2018"
-
-
-import datetime as dt
-import enum
-import logging
-import math
-import time
-
-import dateutil.tz
-
-from dateutil.relativedelta import relativedelta
-
-from .ticklayout import niceNumGeneric
-
-_logger = logging.getLogger(__name__)
-
-
-MICROSECONDS_PER_SECOND = 1000000
-SECONDS_PER_MINUTE = 60
-SECONDS_PER_HOUR = 60 * SECONDS_PER_MINUTE
-SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
-SECONDS_PER_YEAR = 365.25 * SECONDS_PER_DAY
-SECONDS_PER_MONTH_AVERAGE = SECONDS_PER_YEAR / 12 # Seconds per average month
-
-
-# No dt.timezone in Python 2.7 so we use dateutil.tz.tzutc
-_EPOCH = dt.datetime(1970, 1, 1, tzinfo=dateutil.tz.tzutc())
-
-def timestamp(dtObj):
- """ Returns POSIX timestamp of a datetime objects.
-
- If the dtObj object has a timestamp() method (python 3.3), this is
- used. Otherwise (e.g. python 2.7) it is calculated here.
-
- The POSIX timestamp is a floating point value of the number of seconds
- since the start of an epoch (typically 1970-01-01). For details see:
- https://docs.python.org/3/library/datetime.html#datetime.datetime.timestamp
-
- :param datetime.datetime dtObj: date-time representation.
- :return: POSIX timestamp
- :rtype: float
- """
- if hasattr(dtObj, "timestamp"):
- return dtObj.timestamp()
- else:
- # Back ported from Python 3.5
- if dtObj.tzinfo is None:
- return time.mktime((dtObj.year, dtObj.month, dtObj.day,
- dtObj.hour, dtObj.minute, dtObj.second,
- -1, -1, -1)) + dtObj.microsecond / 1e6
- else:
- return (dtObj - _EPOCH).total_seconds()
-
-
-@enum.unique
-class DtUnit(enum.Enum):
- YEARS = 0
- MONTHS = 1
- DAYS = 2
- HOURS = 3
- MINUTES = 4
- SECONDS = 5
- MICRO_SECONDS = 6 # a fraction of a second
-
-
-def getDateElement(dateTime, unit):
- """ Picks the date element with the unit from the dateTime
-
- E.g. getDateElement(datetime(1970, 5, 6), DtUnit.Day) will return 6
-
- :param datetime dateTime: date/time to pick from
- :param DtUnit unit: The unit describing the date element.
- """
- if unit == DtUnit.YEARS:
- return dateTime.year
- elif unit == DtUnit.MONTHS:
- return dateTime.month
- elif unit == DtUnit.DAYS:
- return dateTime.day
- elif unit == DtUnit.HOURS:
- return dateTime.hour
- elif unit == DtUnit.MINUTES:
- return dateTime.minute
- elif unit == DtUnit.SECONDS:
- return dateTime.second
- elif unit == DtUnit.MICRO_SECONDS:
- return dateTime.microsecond
- else:
- raise ValueError("Unexpected DtUnit: {}".format(unit))
-
-
-def setDateElement(dateTime, value, unit):
- """ Returns a copy of dateTime with the tickStep unit set to value
-
- :param datetime.datetime: date time object
- :param int value: value to set
- :param DtUnit unit: unit
- :return: datetime.datetime
- """
- intValue = int(value)
- _logger.debug("setDateElement({}, {} (int={}), {})"
- .format(dateTime, value, intValue, unit))
-
- year = dateTime.year
- month = dateTime.month
- day = dateTime.day
- hour = dateTime.hour
- minute = dateTime.minute
- second = dateTime.second
- microsecond = dateTime.microsecond
-
- if unit == DtUnit.YEARS:
- year = intValue
- elif unit == DtUnit.MONTHS:
- month = intValue
- elif unit == DtUnit.DAYS:
- day = intValue
- elif unit == DtUnit.HOURS:
- hour = intValue
- elif unit == DtUnit.MINUTES:
- minute = intValue
- elif unit == DtUnit.SECONDS:
- second = intValue
- elif unit == DtUnit.MICRO_SECONDS:
- microsecond = intValue
- else:
- raise ValueError("Unexpected DtUnit: {}".format(unit))
-
- _logger.debug("creating date time {}"
- .format((year, month, day, hour, minute, second, microsecond)))
-
- return dt.datetime(year, month, day, hour, minute, second, microsecond,
- tzinfo=dateTime.tzinfo)
-
-
-
-def roundToElement(dateTime, unit):
- """ Returns a copy of dateTime rounded to given unit
-
- :param datetime.datetime: date time object
- :param DtUnit unit: unit
- :return: datetime.datetime
- """
- year = dateTime.year
- month = dateTime.month
- day = dateTime.day
- hour = dateTime.hour
- minute = dateTime.minute
- second = dateTime.second
- microsecond = dateTime.microsecond
-
- if unit.value < DtUnit.YEARS.value:
- pass # Never round years
- if unit.value < DtUnit.MONTHS.value:
- month = 1
- if unit.value < DtUnit.DAYS.value:
- day = 1
- if unit.value < DtUnit.HOURS.value:
- hour = 0
- if unit.value < DtUnit.MINUTES.value:
- minute = 0
- if unit.value < DtUnit.SECONDS.value:
- second = 0
- if unit.value < DtUnit.MICRO_SECONDS.value:
- microsecond = 0
-
- result = dt.datetime(year, month, day, hour, minute, second, microsecond,
- tzinfo=dateTime.tzinfo)
-
- return result
-
-
-def addValueToDate(dateTime, value, unit):
- """ Adds a value with unit to a dateTime.
-
- Uses dateutil.relativedelta.relativedelta from the standard library to do
- the actual math. This function doesn't allow for fractional month or years,
- so month and year are truncated to integers before adding.
-
- :param datetime dateTime: date time
- :param float value: value to be added
- :param DtUnit unit: of the value
- :return:
- """
- #logger.debug("addValueToDate({}, {}, {})".format(dateTime, value, unit))
-
- if unit == DtUnit.YEARS:
- intValue = int(value) # floats not implemented in relativeDelta(years)
- return dateTime + relativedelta(years=intValue)
- elif unit == DtUnit.MONTHS:
- intValue = int(value) # floats not implemented in relativeDelta(mohths)
- return dateTime + relativedelta(months=intValue)
- elif unit == DtUnit.DAYS:
- return dateTime + relativedelta(days=value)
- elif unit == DtUnit.HOURS:
- return dateTime + relativedelta(hours=value)
- elif unit == DtUnit.MINUTES:
- return dateTime + relativedelta(minutes=value)
- elif unit == DtUnit.SECONDS:
- return dateTime + relativedelta(seconds=value)
- elif unit == DtUnit.MICRO_SECONDS:
- return dateTime + relativedelta(microseconds=value)
- else:
- raise ValueError("Unexpected DtUnit: {}".format(unit))
-
-
-def bestUnit(durationInSeconds):
- """ Gets the best tick spacing given a duration in seconds.
-
- :param durationInSeconds: time span duration in seconds
- :return: DtUnit enumeration.
- """
-
- # Based on; https://stackoverflow.com/a/2144398/
- # If the duration is longer than two years the tick spacing will be in
- # years. Else, if the duration is longer than two months, the spacing will
- # be in months, Etcetera.
- #
- # This factor differs per unit. As a baseline it is 2, but for instance,
- # for Months this needs to be higher (3>), This because it is impossible to
- # have partial months so the tick spacing is always at least 1 month. A
- # duration of two months would result in two ticks, which is too few.
- # months would then results
-
- if durationInSeconds > SECONDS_PER_YEAR * 3:
- return (durationInSeconds / SECONDS_PER_YEAR, DtUnit.YEARS)
- elif durationInSeconds > SECONDS_PER_MONTH_AVERAGE * 3:
- return (durationInSeconds / SECONDS_PER_MONTH_AVERAGE, DtUnit.MONTHS)
- elif durationInSeconds > SECONDS_PER_DAY * 2:
- return (durationInSeconds / SECONDS_PER_DAY, DtUnit.DAYS)
- elif durationInSeconds > SECONDS_PER_HOUR * 2:
- return (durationInSeconds / SECONDS_PER_HOUR, DtUnit.HOURS)
- elif durationInSeconds > SECONDS_PER_MINUTE * 2:
- return (durationInSeconds / SECONDS_PER_MINUTE, DtUnit.MINUTES)
- elif durationInSeconds > 1 * 2:
- return (durationInSeconds, DtUnit.SECONDS)
- else:
- return (durationInSeconds * MICROSECONDS_PER_SECOND,
- DtUnit.MICRO_SECONDS)
-
-
-NICE_DATE_VALUES = {
- DtUnit.YEARS: [1, 2, 5, 10],
- DtUnit.MONTHS: [1, 2, 3, 4, 6, 12],
- DtUnit.DAYS: [1, 2, 3, 7, 14, 28],
- DtUnit.HOURS: [1, 2, 3, 4, 6, 12],
- DtUnit.MINUTES: [1, 2, 3, 5, 10, 15, 30],
- DtUnit.SECONDS: [1, 2, 3, 5, 10, 15, 30],
- DtUnit.MICRO_SECONDS : [1.0, 2.0, 5.0, 10.0], # floats for microsec
-}
-
-
-def bestFormatString(spacing, unit):
- """ Finds the best format string given the spacing and DtUnit.
-
- If the spacing is a fractional number < 1 the format string will take this
- into account
-
- :param spacing: spacing between ticks
- :param DtUnit unit:
- :return: Format string for use in strftime
- :rtype: str
- """
- isSmall = spacing < 1
-
- if unit == DtUnit.YEARS:
- return "%Y-m" if isSmall else "%Y"
- elif unit == DtUnit.MONTHS:
- return "%Y-%m-%d" if isSmall else "%Y-%m"
- elif unit == DtUnit.DAYS:
- return "%H:%M" if isSmall else "%Y-%m-%d"
- elif unit == DtUnit.HOURS:
- return "%H:%M" if isSmall else "%H:%M"
- elif unit == DtUnit.MINUTES:
- return "%H:%M:%S" if isSmall else "%H:%M"
- elif unit == DtUnit.SECONDS:
- return "%S.%f" if isSmall else "%H:%M:%S"
- elif unit == DtUnit.MICRO_SECONDS:
- return "%S.%f"
- else:
- raise ValueError("Unexpected DtUnit: {}".format(unit))
-
-
-def niceDateTimeElement(value, unit, isRound=False):
- """ Uses the Nice Numbers algorithm to determine a nice value.
-
- The fractions are optimized for the unit of the date element.
- """
-
- niceValues = NICE_DATE_VALUES[unit]
- elemValue = niceNumGeneric(value, niceValues, isRound=isRound)
-
- if unit == DtUnit.YEARS or unit == DtUnit.MONTHS:
- elemValue = max(1, int(elemValue))
-
- return elemValue
-
-
-def findStartDate(dMin, dMax, nTicks):
- """ Rounds a date down to the nearest nice number of ticks
- """
- assert dMax >= dMin, \
- "dMin ({}) should come before dMax ({})".format(dMin, dMax)
-
- if dMin == dMax:
- # Fallback when range is smaller than microsecond resolution
- return dMin, 1, DtUnit.MICRO_SECONDS
-
- delta = dMax - dMin
- lengthSec = delta.total_seconds()
- _logger.debug("findStartDate: {}, {} (duration = {} sec, {} days)"
- .format(dMin, dMax, lengthSec, lengthSec / SECONDS_PER_DAY))
-
- length, unit = bestUnit(lengthSec)
- niceLength = niceDateTimeElement(length, unit)
-
- _logger.debug("Length: {:8.3f} {} (nice = {})"
- .format(length, unit.name, niceLength))
-
- niceSpacing = niceDateTimeElement(niceLength / nTicks, unit, isRound=True)
-
- _logger.debug("Spacing: {:8.3f} {} (nice = {})"
- .format(niceLength / nTicks, unit.name, niceSpacing))
-
- dVal = getDateElement(dMin, unit)
-
- if unit == DtUnit.MONTHS: # TODO: better rounding?
- niceVal = math.floor((dVal-1) / niceSpacing) * niceSpacing + 1
- elif unit == DtUnit.DAYS:
- niceVal = math.floor((dVal-1) / niceSpacing) * niceSpacing + 1
- else:
- niceVal = math.floor(dVal / niceSpacing) * niceSpacing
-
- _logger.debug("StartValue: dVal = {}, niceVal: {} ({})"
- .format(dVal, niceVal, unit.name))
-
- startDate = roundToElement(dMin, unit)
- startDate = setDateElement(startDate, niceVal, unit)
-
- return startDate, niceSpacing, unit
-
-
-def dateRange(dMin, dMax, step, unit, includeFirstBeyond = False):
- """ Generates a range of dates
-
- :param datetime dMin: start date
- :param datetime dMax: end date
- :param int step: the step size
- :param DtUnit unit: the unit of the step size
- :param bool includeFirstBeyond: if True the first date later than dMax will
- be included in the range. If False (the default), the last generated
- datetime will always be smaller than dMax.
- :return:
- """
- if (unit == DtUnit.YEARS or unit == DtUnit.MONTHS or
- unit == DtUnit.MICRO_SECONDS):
- # No support for fractional month or year and resolution is microsecond
- # In those cases, make sure the step is at least 1
- step = max(1, step)
- else:
- assert step > 0, "tickstep is 0"
-
- dateTime = dMin
- while dateTime < dMax:
- yield dateTime
- dateTime = addValueToDate(dateTime, step, unit)
-
- if includeFirstBeyond:
- yield dateTime
-
-
-
-def calcTicks(dMin, dMax, nTicks):
- """Returns tick positions.
-
- :param datetime.datetime dMin: The min value on the axis
- :param datetime.datetime dMax: The max value on the axis
- :param int nTicks: The target number of ticks. The actual number of found
- ticks may differ.
- :returns: (list of datetimes, DtUnit) tuple
- """
- _logger.debug("Calc calcTicks({}, {}, nTicks={})"
- .format(dMin, dMax, nTicks))
-
- startDate, niceSpacing, unit = findStartDate(dMin, dMax, nTicks)
-
- result = []
- for d in dateRange(startDate, dMax, niceSpacing, unit,
- includeFirstBeyond=True):
- result.append(d)
-
- assert result[0] <= dMin, \
- "First nice date ({}) should be <= dMin {}".format(result[0], dMin)
-
- assert result[-1] >= dMax, \
- "Last nice date ({}) should be >= dMax {}".format(result[-1], dMax)
-
- return result, niceSpacing, unit
-
-
-def calcTicksAdaptive(dMin, dMax, axisLength, tickDensity):
- """ Calls calcTicks with a variable number of ticks, depending on axisLength
- """
- # At least 2 ticks
- nticks = max(2, int(round(tickDensity * axisLength)))
- return calcTicks(dMin, dMax, nticks)
-
-
-
-
-
diff --git a/silx/gui/plot/_utils/panzoom.py b/silx/gui/plot/_utils/panzoom.py
deleted file mode 100644
index 3946a04..0000000
--- a/silx/gui/plot/_utils/panzoom.py
+++ /dev/null
@@ -1,292 +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.
-#
-# ###########################################################################*/
-"""Functions to apply pan and zoom on a Plot"""
-
-__authors__ = ["T. Vincent", "V. Valls"]
-__license__ = "MIT"
-__date__ = "08/08/2017"
-
-
-import math
-import numpy
-
-
-# Float 32 info ###############################################################
-# Using min/max value below limits of float32
-# so operation with such value (e.g., max - min) do not overflow
-
-FLOAT32_SAFE_MIN = -1e37
-FLOAT32_MINPOS = numpy.finfo(numpy.float32).tiny
-FLOAT32_SAFE_MAX = 1e37
-# TODO double support
-
-
-def scale1DRange(min_, max_, center, scale, isLog):
- """Scale a 1D range given a scale factor and an center point.
-
- Keeps the values in a smaller range than float32.
-
- :param float min_: The current min value of the range.
- :param float max_: The current max value of the range.
- :param float center: The center of the zoom (i.e., invariant point).
- :param float scale: The scale to use for zoom
- :param bool isLog: Whether using log scale or not.
- :return: The zoomed range.
- :rtype: tuple of 2 floats: (min, max)
- """
- if isLog:
- # Min and center can be < 0 when
- # autoscale is off and switch to log scale
- # max_ < 0 should not happen
- min_ = numpy.log10(min_) if min_ > 0. else FLOAT32_MINPOS
- center = numpy.log10(center) if center > 0. else FLOAT32_MINPOS
- max_ = numpy.log10(max_) if max_ > 0. else FLOAT32_MINPOS
-
- if min_ == max_:
- return min_, max_
-
- offset = (center - min_) / (max_ - min_)
- range_ = (max_ - min_) / scale
- newMin = center - offset * range_
- newMax = center + (1. - offset) * range_
-
- if isLog:
- # No overflow as exponent is log10 of a float32
- newMin = pow(10., newMin)
- newMax = pow(10., newMax)
- newMin = numpy.clip(newMin, FLOAT32_MINPOS, FLOAT32_SAFE_MAX)
- newMax = numpy.clip(newMax, FLOAT32_MINPOS, FLOAT32_SAFE_MAX)
- else:
- newMin = numpy.clip(newMin, FLOAT32_SAFE_MIN, FLOAT32_SAFE_MAX)
- newMax = numpy.clip(newMax, FLOAT32_SAFE_MIN, FLOAT32_SAFE_MAX)
- return newMin, newMax
-
-
-def applyZoomToPlot(plot, scaleF, center=None):
- """Zoom in/out plot given a scale and a center point.
-
- :param plot: The plot on which to apply zoom.
- :param float scaleF: Scale factor of zoom.
- :param center: (x, y) coords in pixel coordinates of the zoom center.
- :type center: 2-tuple of float
- """
- xMin, xMax = plot.getXAxis().getLimits()
- yMin, yMax = plot.getYAxis().getLimits()
-
- if center is None:
- left, top, width, height = plot.getPlotBoundsInPixels()
- cx, cy = left + width // 2, top + height // 2
- else:
- cx, cy = center
-
- dataCenterPos = plot.pixelToData(cx, cy)
- assert dataCenterPos is not None
-
- xMin, xMax = scale1DRange(xMin, xMax, dataCenterPos[0], scaleF,
- plot.getXAxis()._isLogarithmic())
-
- yMin, yMax = scale1DRange(yMin, yMax, dataCenterPos[1], scaleF,
- plot.getYAxis()._isLogarithmic())
-
- dataPos = plot.pixelToData(cx, cy, axis="right")
- assert dataPos is not None
- y2Center = dataPos[1]
- y2Min, y2Max = plot.getYAxis(axis="right").getLimits()
- y2Min, y2Max = scale1DRange(y2Min, y2Max, y2Center, scaleF,
- plot.getYAxis()._isLogarithmic())
-
- plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
-
-
-def applyPan(min_, max_, panFactor, isLog10):
- """Returns a new range with applied panning.
-
- Moves the range according to panFactor.
- If isLog10 is True, converts to log10 before moving.
-
- :param float min_: Min value of the data range to pan.
- :param float max_: Max value of the data range to pan.
- Must be >= min.
- :param float panFactor: Signed proportion of the range to use for pan.
- :param bool isLog10: True if log10 scale, False if linear scale.
- :return: New min and max value with pan applied.
- :rtype: 2-tuple of float.
- """
- if isLog10 and min_ > 0.:
- # Negative range and log scale can happen with matplotlib
- logMin, logMax = math.log10(min_), math.log10(max_)
- logOffset = panFactor * (logMax - logMin)
- newMin = pow(10., logMin + logOffset)
- newMax = pow(10., logMax + logOffset)
-
- # Takes care of out-of-range values
- if newMin > 0. and newMax < float('inf'):
- min_, max_ = newMin, newMax
-
- else:
- offset = panFactor * (max_ - min_)
- newMin, newMax = min_ + offset, max_ + offset
-
- # Takes care of out-of-range values
- 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/_utils/setup.py b/silx/gui/plot/_utils/setup.py
deleted file mode 100644
index 0271745..0000000
--- a/silx/gui/plot/_utils/setup.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "21/03/2017"
-
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('_utils', parent_package, top_path)
- config.add_subpackage('test')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
-
- setup(configuration=configuration)
diff --git a/silx/gui/plot/_utils/test/__init__.py b/silx/gui/plot/_utils/test/__init__.py
deleted file mode 100644
index 624dbcb..0000000
--- a/silx/gui/plot/_utils/test/__init__.py
+++ /dev/null
@@ -1,43 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 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.
-#
-# ###########################################################################*/
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "18/10/2016"
-
-
-import unittest
-
-from .test_dtime_ticklayout import suite as test_dtime_ticklayout_suite
-from .test_ticklayout import suite as test_ticklayout_suite
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(test_dtime_ticklayout_suite())
- testsuite.addTest(test_ticklayout_suite())
- return testsuite
diff --git a/silx/gui/plot/_utils/test/test_dtime_ticklayout.py b/silx/gui/plot/_utils/test/test_dtime_ticklayout.py
deleted file mode 100644
index 2b87148..0000000
--- a/silx/gui/plot/_utils/test/test_dtime_ticklayout.py
+++ /dev/null
@@ -1,93 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2018 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.
-#
-# ###########################################################################*/
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["P. Kenter"]
-__license__ = "MIT"
-__date__ = "06/04/2018"
-
-
-import datetime as dt
-import unittest
-
-
-from silx.gui.plot._utils.dtime_ticklayout import (
- calcTicks, DtUnit, SECONDS_PER_YEAR)
-
-
-class DtTestTickLayout(unittest.TestCase):
- """Test ticks layout algorithms"""
-
- def testSmallMonthlySpacing(self):
- """ Tests a range that did result in a spacing of less than 1 month.
- It is impossible to add fractional month so the unit must be in days
- """
- from dateutil import parser
- d1 = parser.parse("2017-01-03 13:15:06.000044")
- d2 = parser.parse("2017-03-08 09:16:16.307584")
- _ticks, _units, spacing = calcTicks(d1, d2, nTicks=4)
-
- self.assertEqual(spacing, DtUnit.DAYS)
-
-
- def testNoCrash(self):
- """ Creates many combinations of and number-of-ticks and end-dates;
- tests that it doesn't give an exception and returns a reasonable number
- of ticks.
- """
- d1 = dt.datetime(2017, 1, 3, 13, 15, 6, 44)
-
- value = 100e-6 # Start at 100 micro sec range.
-
- while value <= 200 * SECONDS_PER_YEAR:
-
- d2 = d1 + dt.timedelta(microseconds=value*1e6) # end date range
-
- for numTicks in range(2, 12):
- ticks, _, _ = calcTicks(d1, d2, numTicks)
-
- margin = 2.5
- self.assertTrue(
- numTicks/margin <= len(ticks) <= numTicks*margin,
- "Condition {} <= {} <= {} failed for # ticks={} and d2={}:"
- .format(numTicks/margin, len(ticks), numTicks * margin,
- numTicks, d2))
-
- value = value * 1.5 # let date period grow exponentially
-
-
-
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(DtTestTickLayout))
- return testsuite
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/silx/gui/plot/_utils/test/test_ticklayout.py b/silx/gui/plot/_utils/test/test_ticklayout.py
deleted file mode 100644
index 927ffb6..0000000
--- a/silx/gui/plot/_utils/test/test_ticklayout.py
+++ /dev/null
@@ -1,92 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "17/01/2018"
-
-
-import unittest
-import numpy
-
-from silx.utils.testutils import ParametricTestCase
-
-from silx.gui.plot._utils import ticklayout
-
-
-class TestTickLayout(ParametricTestCase):
- """Test ticks layout algorithms"""
-
- def testTicks(self):
- """Test of :func:`ticks`"""
- tests = { # (vmin, vmax): ref_ticks
- (1., 1.): (1.,),
- (0.5, 10.5): (2.0, 4.0, 6.0, 8.0, 10.0),
- (0.001, 0.005): (0.001, 0.002, 0.003, 0.004, 0.005)
- }
-
- for (vmin, vmax), ref_ticks in tests.items():
- with self.subTest(vmin=vmin, vmax=vmax):
- ticks, labels = ticklayout.ticks(vmin, vmax)
- self.assertTrue(numpy.allclose(ticks, ref_ticks))
-
- def testNiceNumbers(self):
- """Minimalistic tests of :func:`niceNumbers`"""
- tests = { # (vmin, vmax): ref_ticks
- (0.5, 10.5): (0.0, 12.0, 2.0, 0),
- (10000., 10000.5): (10000.0, 10000.5, 0.1, 1),
- (0.001, 0.005): (0.001, 0.005, 0.001, 3)
- }
-
- for (vmin, vmax), ref_ticks in tests.items():
- with self.subTest(vmin=vmin, vmax=vmax):
- ticks = ticklayout.niceNumbers(vmin, vmax)
- self.assertEqual(ticks, ref_ticks)
-
- def testNiceNumbersLog(self):
- """Minimalistic tests of :func:`niceNumbersForLog10`"""
- tests = { # (log10(min), log10(max): ref_ticks
- (0., 3.): (0, 3, 1, 0),
- (-3., 3): (-3, 3, 1, 0),
- (-32., 0.): (-36, 0, 6, 0)
- }
-
- for (vmin, vmax), ref_ticks in tests.items():
- with self.subTest(vmin=vmin, vmax=vmax):
- ticks = ticklayout.niceNumbersForLog10(vmin, vmax)
- self.assertEqual(ticks, ref_ticks)
-
-
-def suite():
- testsuite = unittest.TestSuite()
- testsuite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestTickLayout))
- return testsuite
-
-
-if __name__ == '__main__':
- unittest.main()
diff --git a/silx/gui/plot/_utils/ticklayout.py b/silx/gui/plot/_utils/ticklayout.py
deleted file mode 100644
index c9fd3e6..0000000
--- a/silx/gui/plot/_utils/ticklayout.py
+++ /dev/null
@@ -1,267 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2018 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 implements labels layout on graph axes."""
-
-from __future__ import absolute_import, division, unicode_literals
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "18/10/2016"
-
-
-import math
-
-
-# utils #######################################################################
-
-def numberOfDigits(tickSpacing):
- """Returns the number of digits to display for text label.
-
- :param float tickSpacing: Step between ticks in data space.
- :return: Number of digits to show for labels.
- :rtype: int
- """
- nfrac = int(-math.floor(math.log10(tickSpacing)))
- if nfrac < 0:
- nfrac = 0
- return nfrac
-
-
-# Nice Numbers ################################################################
-
-# This is the original niceNum implementation. For the date time ticks a more
-# generic implementation was needed.
-#
-# def _niceNum(value, isRound=False):
-# expvalue = math.floor(math.log10(value))
-# frac = value/pow(10., expvalue)
-# if isRound:
-# if frac < 1.5:
-# nicefrac = 1.
-# elif frac < 3.: # In niceNumGeneric this is (2+5)/2 = 3.5
-# nicefrac = 2.
-# elif frac < 7.:
-# nicefrac = 5. # In niceNumGeneric this is (5+10)/2 = 7.5
-# else:
-# nicefrac = 10.
-# else:
-# if frac <= 1.:
-# nicefrac = 1.
-# elif frac <= 2.:
-# nicefrac = 2.
-# elif frac <= 5.:
-# nicefrac = 5.
-# else:
-# nicefrac = 10.
-# return nicefrac * pow(10., expvalue)
-
-
-def niceNumGeneric(value, niceFractions=None, isRound=False):
- """ A more generic implementation of the _niceNum function
-
- Allows the user to specify the fractions instead of using a hardcoded
- list of [1, 2, 5, 10.0].
- """
- if value == 0:
- return value
-
- if niceFractions is None: # Use default values
- niceFractions = 1., 2., 5., 10.
- roundFractions = (1.5, 3., 7., 10.) if isRound else niceFractions
-
- else:
- roundFractions = list(niceFractions)
- if isRound:
- # Take the average with the next element. The last remains the same.
- for i in range(len(roundFractions) - 1):
- roundFractions[i] = (niceFractions[i] + niceFractions[i+1]) / 2
-
- highest = niceFractions[-1]
- value = float(value)
-
- expvalue = math.floor(math.log(value, highest))
- frac = value / pow(highest, expvalue)
-
- for niceFrac, roundFrac in zip(niceFractions, roundFractions):
- if frac <= roundFrac:
- return niceFrac * pow(highest, expvalue)
-
- # should not come here
- assert False, "should not come here"
-
-
-def niceNumbers(vMin, vMax, nTicks=5):
- """Returns tick positions.
-
- This function implements graph labels layout using nice numbers
- by Paul Heckbert from "Graphics Gems", Academic Press, 1990.
- See `C code <http://tog.acm.org/resources/GraphicsGems/gems/Label.c>`_.
-
- :param float vMin: The min value on the axis
- :param float vMax: The max value on the axis
- :param int nTicks: The number of ticks to position
- :returns: min, max, increment value of tick positions and
- number of fractional digit to show
- :rtype: tuple
- """
- vrange = niceNumGeneric(vMax - vMin, isRound=False)
- spacing = niceNumGeneric(vrange / nTicks, isRound=True)
- graphmin = math.floor(vMin / spacing) * spacing
- graphmax = math.ceil(vMax / spacing) * spacing
- nfrac = numberOfDigits(spacing)
- return graphmin, graphmax, spacing, nfrac
-
-
-def _frange(start, stop, step):
- """range for float (including stop)."""
- assert step >= 0.
- while start <= stop:
- yield start
- start += step
-
-
-def ticks(vMin, vMax, nbTicks=5):
- """Returns tick positions and labels using nice numbers algorithm.
-
- This enforces ticks to be within [vMin, vMax] range.
- It returns at least 1 tick (when vMin == vMax).
-
- :param float vMin: The min value on the axis
- :param float vMax: The max value on the axis
- :param int nbTicks: The number of ticks to position
- :returns: tick positions and corresponding text labels
- :rtype: 2-tuple: list of float, list of string
- """
- assert vMin <= vMax
- if vMin == vMax:
- positions = [vMin]
- nfrac = 0
-
- else:
- start, end, step, nfrac = niceNumbers(vMin, vMax, nbTicks)
- positions = [t for t in _frange(start, end, step) if vMin <= t <= vMax]
-
- # Makes sure there is at least 2 ticks
- if len(positions) < 2:
- positions = [vMin, vMax]
- nfrac = numberOfDigits(vMax - vMin)
-
- # Generate labels
- format_ = '%g' if nfrac == 0 else '%.{}f'.format(nfrac)
- labels = [format_ % tick for tick in positions]
- return positions, labels
-
-
-def niceNumbersAdaptative(vMin, vMax, axisLength, tickDensity):
- """Returns tick positions using :func:`niceNumbers` and a
- density of ticks.
-
- axisLength and tickDensity are based on the same unit (e.g., pixel).
-
- :param float vMin: The min value on the axis
- :param float vMax: The max value on the axis
- :param float axisLength: The length of the axis.
- :param float tickDensity: The density of ticks along the axis.
- :returns: min, max, increment value of tick positions and
- number of fractional digit to show
- :rtype: tuple
- """
- # At least 2 ticks
- nticks = max(2, int(round(tickDensity * axisLength)))
- tickmin, tickmax, step, nfrac = niceNumbers(vMin, vMax, nticks)
-
- return tickmin, tickmax, step, nfrac
-
-
-# Nice Numbers for log scale ##################################################
-
-def niceNumbersForLog10(minLog, maxLog, nTicks=5):
- """Return tick positions for logarithmic scale
-
- :param float minLog: log10 of the min value on the axis
- :param float maxLog: log10 of the max value on the axis
- :param int nTicks: The number of ticks to position
- :returns: log10 of min, max, increment value of tick positions and
- number of fractional digit to show
- :rtype: tuple of int
- """
- graphminlog = math.floor(minLog)
- graphmaxlog = math.ceil(maxLog)
- rangelog = graphmaxlog - graphminlog
-
- if rangelog <= nTicks:
- spacing = 1.
- else:
- spacing = math.floor(rangelog / nTicks)
-
- graphminlog = math.floor(graphminlog / spacing) * spacing
- graphmaxlog = math.ceil(graphmaxlog / spacing) * spacing
-
- nfrac = numberOfDigits(spacing)
-
- return int(graphminlog), int(graphmaxlog), int(spacing), nfrac
-
-
-def niceNumbersAdaptativeForLog10(vMin, vMax, axisLength, tickDensity):
- """Returns tick positions using :func:`niceNumbers` and a
- density of ticks.
-
- axisLength and tickDensity are based on the same unit (e.g., pixel).
-
- :param float vMin: The min value on the axis
- :param float vMax: The max value on the axis
- :param float axisLength: The length of the axis.
- :param float tickDensity: The density of ticks along the axis.
- :returns: log10 of min, max, increment value of tick positions and
- number of fractional digit to show
- :rtype: tuple
- """
- # At least 2 ticks
- nticks = max(2, int(round(tickDensity * axisLength)))
- tickmin, tickmax, step, nfrac = niceNumbersForLog10(vMin, vMax, nticks)
-
- return tickmin, tickmax, step, nfrac
-
-
-def computeLogSubTicks(ticks, lowBound, highBound):
- """Return the sub ticks for the log scale for all given ticks if subtick
- is in [lowBound, highBound]
-
- :param ticks: log10 of the ticks
- :param lowBound: the lower boundary of ticks
- :param highBound: the higher boundary of ticks
- :return: all the sub ticks contained in ticks (log10)
- """
- if len(ticks) < 1:
- return []
-
- res = []
- for logPos in ticks:
- dataOrigPos = logPos
- for index in range(2, 10):
- dataPos = dataOrigPos * index
- if lowBound <= dataPos <= highBound:
- res.append(dataPos)
- return res
diff --git a/silx/gui/plot/actions/PlotAction.py b/silx/gui/plot/actions/PlotAction.py
deleted file mode 100644
index 2983775..0000000
--- a/silx/gui/plot/actions/PlotAction.py
+++ /dev/null
@@ -1,78 +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.
-#
-# ###########################################################################*/
-"""
-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__ = "03/01/2018"
-
-
-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/PlotToolAction.py b/silx/gui/plot/actions/PlotToolAction.py
deleted file mode 100644
index fbb0b0f..0000000
--- a/silx/gui/plot/actions/PlotToolAction.py
+++ /dev/null
@@ -1,150 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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:`.PlotToolAction` help the creation of a qt.QAction associating
-a tool window with a :class:`.PlotWidget`.
-"""
-
-from __future__ import division
-
-
-__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "10/10/2018"
-
-
-import weakref
-
-from .PlotAction import PlotAction
-from silx.gui import qt
-
-
-class PlotToolAction(PlotAction):
- """Base class for QAction that maintain a tool window operating on a
- PlotWidget."""
-
- def __init__(self, plot, icon, text, tooltip=None,
- triggered=None, checkable=False, parent=None):
- PlotAction.__init__(self,
- plot=plot,
- icon=icon,
- text=text,
- tooltip=tooltip,
- triggered=self._triggered,
- parent=parent,
- checkable=True)
- self._previousGeometry = None
- self._toolWindow = None
-
- def _triggered(self, checked):
- """Update the plot of the histogram visibility status
-
- :param bool checked: status of the action button
- """
- self._setToolWindowVisible(checked)
-
- def _setToolWindowVisible(self, visible):
- """Set the tool window visible or hidden."""
- tool = self._getToolWindow()
- if tool.isVisible() == visible:
- # Nothing to do
- return
-
- if visible:
- self._connectPlot(tool)
- tool.show()
- if self._previousGeometry is not None:
- # Restore the geometry
- tool.setGeometry(self._previousGeometry)
- else:
- self._disconnectPlot(tool)
- # Save the geometry
- self._previousGeometry = tool.geometry()
- tool.hide()
-
- def _connectPlot(self, window):
- """Called if the tool is visible and have to be updated according to
- event of the plot.
-
- :param qt.QWidget window: The tool window
- """
- pass
-
- def _disconnectPlot(self, window):
- """Called if the tool is not visible and dont have anymore to be updated
- according to event of the plot.
-
- :param qt.QWidget window: The tool window
- """
- pass
-
- def _isWindowInUse(self):
- """Returns true if the tool window is currently in use."""
- if not self.isChecked():
- return False
- return self._toolWindow is not None
-
- def _ownerVisibilityChanged(self, isVisible):
- """Called when the visibility of the parent of the tool window changes
-
- :param bool isVisible: True if the parent became visible
- """
- if self._isWindowInUse():
- self._setToolWindowVisible(isVisible)
-
- 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._toolWindow is not None:
- window = self._toolWindow()
- self._previousGeometry = window.geometry()
- window.hide()
- self.setChecked(False)
-
- return PlotAction.eventFilter(self, qobject, event)
-
- def _getToolWindow(self):
- """Returns the window containing the tool.
-
- It uses lazy loading to create this tool..
- """
- if self._toolWindow is None:
- window = self._createToolWindow()
- if self._previousGeometry is not None:
- window.setGeometry(self._previousGeometry)
- window.installEventFilter(self)
- plot = self.plot
- plot.sigVisibilityChanged.connect(self._ownerVisibilityChanged)
- self._toolWindow = weakref.ref(window)
- return self._toolWindow()
-
- def _createToolWindow(self):
- """Create the tool window managing the plot."""
- raise NotImplementedError()
diff --git a/silx/gui/plot/actions/__init__.py b/silx/gui/plot/actions/__init__.py
deleted file mode 100644
index 930c728..0000000
--- a/silx/gui/plot/actions/__init__.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2018 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 QAction to use with
-:class:`~silx.gui.plot.PlotWidget`
-
-Those actions are useful to add menu items or toolbar items
-that interact with a :class:`~silx.gui.plot.PlotWidget`.
-
-It provides a base class used to define new plot actions:
-:class:`~silx.gui.plot.actions.PlotAction`.
-"""
-
-__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
deleted file mode 100755
index 439985e..0000000
--- a/silx/gui/plot/actions/control.py
+++ /dev/null
@@ -1,694 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2019 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:`ShowAxisAction`
-- :class:`XAxisLogarithmicAction`
-- :class:`XAxisAutoScaleAction`
-- :class:`YAxisInvertedAction`
-- :class:`YAxisLogarithmicAction`
-- :class:`YAxisAutoScaleAction`
-- :class:`ZoomBackAction`
-- :class:`ZoomInAction`
-- :class:`ZoomOutAction`
-"""
-
-from __future__ import division
-
-__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "27/11/2020"
-
-from . import PlotAction
-import logging
-from silx.gui.plot import items
-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())
-
- if currentState == (False, False):
- newState = True, False
- else:
- # 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=True, parent=parent)
- self.plot.sigActiveImageChanged.connect(self._updateColormap)
- self.plot.sigActiveScatterChanged.connect(self._updateColormap)
-
- def setColorDialog(self, colorDialog):
- """Set a specific color dialog instead of using the default dialog."""
- assert(colorDialog is not None)
- assert(self._dialog is None)
- self._dialog = colorDialog
- self._dialog.visibleChanged.connect(self._dialogVisibleChanged)
- self.setChecked(self._dialog.isVisible())
-
- @staticmethod
- def _createDialog(parent):
- """Create the dialog if not already existing
-
- :parent QWidget parent: Parent of the new colormap
- :rtype: ColormapDialog
- """
- from silx.gui.dialog.ColormapDialog import ColormapDialog
- dialog = ColormapDialog(parent=parent)
- dialog.setModal(False)
- return dialog
-
- def _actionTriggered(self, checked=False):
- """Create a cmap dialog and update active image and default cmap."""
- if self._dialog is None:
- self._dialog = self._createDialog(self.plot)
- self._dialog.visibleChanged.connect(self._dialogVisibleChanged)
-
- # Run the dialog listening to colormap change
- if checked is True:
- self._updateColormap()
- self._dialog.show()
- else:
- self._dialog.hide()
-
- def _dialogVisibleChanged(self, isVisible):
- self.setChecked(isVisible)
-
- def _updateColormap(self):
- if self._dialog is None:
- return
- image = self.plot.getActiveImage()
-
- if isinstance(image, items.ColormapMixIn):
- # Set dialog from active image
- colormap = image.getColormap()
- # Set histogram and range if any
- self._dialog.setItem(image)
-
- else:
- # No active image or active image is RGBA,
- # Check for active scatter plot
- scatter = self.plot._getActiveItem(kind='scatter')
- if scatter is not None:
- colormap = scatter.getColormap()
- self._dialog.setItem(scatter)
-
- else:
- # No active data image nor scatter,
- # set dialog from default info
- colormap = self.plot.getDefaultColormap()
- # Reset histogram and range if any
- self._dialog.setData(None)
-
- self._dialog.setColormap(colormap)
-
-
-class ColorBarAction(PlotAction):
- """QAction opening the ColorBarWidget of the specified plot.
-
- :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 ColorBar
- super(ColorBarAction, self).__init__(
- plot, icon='colorbar', text='Colorbar',
- tooltip="Show/Hide the colorbar",
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- colorBarWidget = self.plot.getColorBarWidget()
- old = self.blockSignals(True)
- self.setChecked(colorBarWidget.isVisibleTo(self.plot))
- self.blockSignals(old)
- colorBarWidget.sigVisibleChanged.connect(self._widgetVisibleChanged)
-
- def _widgetVisibleChanged(self, isVisible):
- """Callback when the colorbar `visible` property change."""
- if self.isChecked() == isVisible:
- return
- self.setChecked(isVisible)
-
- def _actionTriggered(self, checked=False):
- """Create a cmap dialog and update active image and default cmap."""
- colorBarWidget = self.plot.getColorBarWidget()
- if not colorBarWidget.isHidden() == checked:
- return
- self.plot.getColorBarWidget().setVisible(checked)
-
-
-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.isAxesDisplayed())
- plot._sigAxesVisibilityChanged.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setAxesDisplayed(checked)
-
-
-class ClosePolygonInteractionAction(PlotAction):
- """QAction controlling closure of a polygon in draw interaction mode
- if the :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- tooltip = 'Close the current polygon drawn'
- PlotAction.__init__(self,
- plot,
- icon='add-shape-polygon',
- text='Close the polygon',
- tooltip=tooltip,
- triggered=self._actionTriggered,
- checkable=True,
- parent=parent)
- self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
- self._modeChanged(None)
-
- def _modeChanged(self, source):
- mode = self.plot.getInteractiveMode()
- enabled = "shape" in mode and mode["shape"] == "polygon"
- self.setEnabled(enabled)
-
- def _actionTriggered(self, checked=False):
- self.plot._eventHandler.validate()
-
-
-class OpenGLAction(PlotAction):
- """QAction controlling rendering of a :class:`.PlotWidget`.
-
- For now it can enable or not the OpenGL backend.
-
- :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 = {
- "opengl": (icons.getQIcon('backend-opengl'),
- "OpenGL rendering (fast)\nClick to disable OpenGL"),
- "matplotlib": (icons.getQIcon('backend-opengl'),
- "Matplotlib rendering (safe)\nClick to enable OpenGL"),
- "unknown": (icons.getQIcon('backend-opengl'),
- "Custom rendering")
- }
-
- name = self._getBackendName(plot)
- self.__state = name
- icon, tooltip = self._states[name]
- super(OpenGLAction, self).__init__(
- plot,
- icon=icon,
- text='Enable/disable OpenGL rendering',
- tooltip=tooltip,
- triggered=self._actionTriggered,
- checkable=True,
- parent=parent)
-
- def _backendUpdated(self):
- name = self._getBackendName(self.plot)
- self.__state = name
- icon, tooltip = self._states[name]
- self.setIcon(icon)
- self.setToolTip(tooltip)
- self.setChecked(name == "opengl")
-
- def _getBackendName(self, plot):
- backend = plot.getBackend()
- name = type(backend).__name__.lower()
- if "opengl" in name:
- return "opengl"
- elif "matplotlib" in name:
- return "matplotlib"
- else:
- return "unknown"
-
- def _actionTriggered(self, checked=False):
- plot = self.plot
- name = self._getBackendName(self.plot)
- if self.__state != name:
- # THere is no event to know the backend was updated
- # So here we check if there is a mismatch between the displayed state
- # and the real state of the widget
- self._backendUpdated()
- return
- if name != "opengl":
- from silx.gui.utils import glutils
- result = glutils.isOpenGLAvailable()
- if not result:
- qt.QMessageBox.critical(plot, "OpenGL rendering not available", result.error)
- # Uncheck if needed
- self._backendUpdated()
- return
- plot.setBackend("opengl")
- else:
- plot.setBackend("matplotlib")
- self._backendUpdated()
diff --git a/silx/gui/plot/actions/fit.py b/silx/gui/plot/actions/fit.py
deleted file mode 100644
index f3c9e1c..0000000
--- a/silx/gui/plot/actions/fit.py
+++ /dev/null
@@ -1,403 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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__ = "10/10/2018"
-
-import logging
-
-import numpy
-
-from .PlotToolAction import PlotToolAction
-from .. import items
-from ....utils.deprecation import deprecated
-from silx.gui import qt
-from silx.gui.plot.ItemsSelectionDialog import ItemsSelectionDialog
-
-_logger = logging.getLogger(__name__)
-
-
-def _getUniqueCurveOrHistogram(plot):
- """Returns unique :class:`Curve` or :class:`Histogram` in a `PlotWidget`.
-
- If there is an active curve, returns it, else return curve or histogram
- only if alone in the plot.
-
- :param PlotWidget plot:
- :rtype: Union[None,~silx.gui.plot.items.Curve,~silx.gui.plot.items.Histogram]
- """
- curve = plot.getActiveCurve()
- if curve is not None:
- return curve
-
- histograms = [item for item in plot.getItems()
- if isinstance(item, items.Histogram) and item.isVisible()]
- curves = [item for item in plot.getItems()
- if isinstance(item, items.Curve) and item.isVisible()]
-
- if len(histograms) == 1 and len(curves) == 0:
- return histograms[0]
- elif len(curves) == 1 and len(histograms) == 0:
- return curves[0]
- else:
- return None
-
-
-class FitAction(PlotToolAction):
- """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):
- self.__item = None
- self.__activeCurveSynchroEnabled = False
- self.__range = 0, 1
- self.__rangeAutoUpdate = False
- self.__x, self.__y = None, None # Data to fit
- self.__curveParams = {} # Store curve parameters to use for fit result
- self.__legend = None
-
- super(FitAction, self).__init__(
- plot, icon='math-fit', text='Fit curve',
- tooltip='Open a fit dialog',
- parent=parent)
-
- @property
- @deprecated(replacement='getXRange()[0]', since_version='0.13.0')
- def xmin(self):
- return self.getXRange()[0]
-
- @property
- @deprecated(replacement='getXRange()[1]', since_version='0.13.0')
- def xmax(self):
- return self.getXRange()[1]
-
- @property
- @deprecated(replacement='getXData()', since_version='0.13.0')
- def x(self):
- return self.getXData()
-
- @property
- @deprecated(replacement='getYData()', since_version='0.13.0')
- def y(self):
- return self.getYData()
-
- @property
- @deprecated(since_version='0.13.0')
- def xlabel(self):
- return self.__curveParams.get('xlabel', None)
-
- @property
- @deprecated(since_version='0.13.0')
- def ylabel(self):
- return self.__curveParams.get('ylabel', None)
-
- @property
- @deprecated(since_version='0.13.0')
- def legend(self):
- return self.__legend
-
- def _createToolWindow(self):
- # import done here rather than at module level to avoid circular import
- # FitWidget -> BackgroundWidget -> PlotWindow -> actions -> fit -> FitWidget
- from ...fit.FitWidget import FitWidget
-
- window = FitWidget(parent=self.plot)
- window.setWindowFlags(qt.Qt.Dialog)
- window.sigFitWidgetSignal.connect(self.handle_signal)
- return window
-
- def _connectPlot(self, window):
- if self.isXRangeUpdatedOnZoom():
- self.__setAutoXRangeEnabled(True)
- else:
- plot = self.plot
- if plot is None:
- _logger.error("No associated PlotWidget")
- return
- self._setXRange(*plot.getXAxis().getLimits())
-
- if self.isFittedItemUpdatedFromActiveCurve():
- self.__setFittedItemAutoUpdateEnabled(True)
- else:
- # Wait for the next iteration, else the plot is not yet initialized
- # No curve available
- qt.QTimer.singleShot(10, self._initFit)
-
- def _disconnectPlot(self, window):
- if self.isXRangeUpdatedOnZoom():
- self.__setAutoXRangeEnabled(False)
-
- if self.isFittedItemUpdatedFromActiveCurve():
- self.__setFittedItemAutoUpdateEnabled(False)
-
- def _initFit(self):
- plot = self.plot
- if plot is None:
- _logger.error("No associated PlotWidget")
- return
-
- item = _getUniqueCurveOrHistogram(plot)
- if item is None:
- # ambiguous case, we need to ask which plot item to fit
- isd = ItemsSelectionDialog(parent=plot, plot=plot)
- isd.setWindowTitle("Select item to be fitted")
- isd.setItemsSelectionMode(qt.QTableWidget.SingleSelection)
- isd.setAvailableKinds(["curve", "histogram"])
- isd.selectAllKinds()
-
- if not isd.exec_(): # Cancel
- self._getToolWindow().setVisible(False)
- else:
- selectedItems = isd.getSelectedItems()
- item = selectedItems[0] if len(selectedItems) == 1 else None
-
- self._setXRange(*plot.getXAxis().getLimits())
- self._setFittedItem(item)
-
- def __updateFitWidget(self):
- """Update the data/range used by the FitWidget"""
- fitWidget = self._getToolWindow()
-
- item = self._getFittedItem()
- xdata = self.getXData(copy=False)
- ydata = self.getYData(copy=False)
- if item is None or xdata is None or ydata is None:
- fitWidget.setData(y=None)
- fitWidget.setWindowTitle("No curve selected")
-
- else:
- xmin, xmax = self.getXRange()
- fitWidget.setData(
- xdata, ydata, xmin=xmin, xmax=xmax)
- fitWidget.setWindowTitle(
- "Fitting " + item.getName() +
- " on x range %f-%f" % (xmin, xmax))
-
- # X Range management
-
- def getXRange(self):
- """Returns the range on the X axis on which to perform the fit."""
- return self.__range
-
- def _setXRange(self, xmin, xmax):
- """Set the range on which the fit is done.
-
- :param float xmin:
- :param float xmax:
- """
- range_ = float(xmin), float(xmax)
- if self.__range != range_:
- self.__range = range_
- self.__updateFitWidget()
-
- def __setAutoXRangeEnabled(self, enabled):
- """Implement the change of update mode of the X range.
-
- :param bool enabled:
- """
- plot = self.plot
- if plot is None:
- _logger.error("No associated PlotWidget")
- return
-
- if enabled:
- self._setXRange(*plot.getXAxis().getLimits())
- plot.getXAxis().sigLimitsChanged.connect(self._setXRange)
- else:
- plot.getXAxis().sigLimitsChanged.disconnect(self._setXRange)
-
- def setXRangeUpdatedOnZoom(self, enabled):
- """Set whether or not to update the X range on zoom change.
-
- :param bool enabled:
- """
- if enabled != self.__rangeAutoUpdate:
- self.__rangeAutoUpdate = enabled
- if self._getToolWindow().isVisible():
- self.__setAutoXRangeEnabled(enabled)
-
- def isXRangeUpdatedOnZoom(self):
- """Returns the current mode of fitted data X range update.
-
- :rtype: bool
- """
- return self.__rangeAutoUpdate
-
- # Fitted item update
-
- def getXData(self, copy=True):
- """Returns the X data used for the fit or None if undefined.
-
- :param bool copy:
- True to get a copy of the data, False to get the internal data.
- :rtype: Union[numpy.ndarray,None]
- """
- return None if self.__x is None else numpy.array(self.__x, copy=copy)
-
- def getYData(self, copy=True):
- """Returns the Y data used for the fit or None if undefined.
-
- :param bool copy:
- True to get a copy of the data, False to get the internal data.
- :rtype: Union[numpy.ndarray,None]
- """
- return None if self.__y is None else numpy.array(self.__y, copy=copy)
-
- def _getFittedItem(self):
- """Returns the current item used for the fit
-
- :rtype: Union[~silx.gui.plot.items.Curve,~silx.gui.plot.items.Histogram,None]
- """
- return self.__item
-
- def _setFittedItem(self, item):
- """Set the curve to use for fitting.
-
- :param Union[~silx.gui.plot.items.Curve,~silx.gui.plot.items.Histogram,None] item:
- """
- plot = self.plot
- if plot is None:
- _logger.error("No associated PlotWidget")
-
- if plot is None or item is None:
- self.__item = None
- self.__curveParams = {}
- self.__updateFitWidget()
- return
-
- axis = item.getYAxis() if isinstance(item, items.YAxisMixIn) else 'left'
- self.__curveParams = {
- 'yaxis': axis,
- 'xlabel': plot.getXAxis().getLabel(),
- 'ylabel': plot.getYAxis(axis).getLabel(),
- }
- self.__legend = item.getName()
-
- if isinstance(item, items.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, items.Curve):
- self.__x = item.getXData(copy=False)
- self.__y = item.getYData(copy=False)
-
- self.__item = item
- self.__updateFitWidget()
-
- def __activeCurveChanged(self, previous, current):
- """Handle change of active curve in the PlotWidget
- """
- if current is None:
- self._setFittedItem(None)
- else:
- item = self.plot.getCurve(current)
- self._setFittedItem(item)
-
- def __setFittedItemAutoUpdateEnabled(self, enabled):
- """Implement the change of fitted item update mode
-
- :param bool enabled:
- """
- plot = self.plot
- if plot is None:
- _logger.error("No associated PlotWidget")
- return
-
- if enabled:
- self._setFittedItem(plot.getActiveCurve())
- plot.sigActiveCurveChanged.connect(self.__activeCurveChanged)
-
- else:
- plot.sigActiveCurveChanged.disconnect(
- self.__activeCurveChanged)
-
- def setFittedItemUpdatedFromActiveCurve(self, enabled):
- """Toggle fitted data synchronization with plot active curve.
-
- :param bool enabled:
- """
- enabled = bool(enabled)
- if enabled != self.__activeCurveSynchroEnabled:
- self.__activeCurveSynchroEnabled = enabled
- if self._getToolWindow().isVisible():
- self.__setFittedItemAutoUpdateEnabled(enabled)
-
- def isFittedItemUpdatedFromActiveCurve(self):
- """Returns True if fitted data is synchronized with plot.
-
- :rtype: bool
- """
- return self.__activeCurveSynchroEnabled
-
- # Handle fit completed
-
- def handle_signal(self, ddict):
- xdata = self.getXData(copy=False)
- if xdata is None:
- _logger.error("No reference data to display fit result for")
- return
-
- xmin, xmax = self.getXRange()
- x_fit = xdata[xmin <= xdata]
- x_fit = x_fit[x_fit <= xmax]
- fit_legend = "Fit <%s>" % self.__legend
- fit_curve = self.plot.getCurve(fit_legend)
-
- if ddict["event"] == "FitFinished":
- fit_widget = self._getToolWindow()
- if fit_widget is None:
- return
- y_fit = fit_widget.fitmanager.gendata()
- if fit_curve is None:
- self.plot.addCurve(x_fit, y_fit,
- fit_legend,
- resetzoom=False,
- **self.__curveParams)
- else:
- fit_curve.setData(x_fit, y_fit)
- fit_curve.setVisible(True)
- fit_curve.setYAxis(self.__curveParams.get('yaxis', 'left'))
-
- 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
deleted file mode 100644
index 0bba558..0000000
--- a/silx/gui/plot/actions/histogram.py
+++ /dev/null
@@ -1,392 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2021 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__ = "01/12/2020"
-__license__ = "MIT"
-
-import numpy
-import logging
-import typing
-import weakref
-
-from .PlotToolAction import PlotToolAction
-
-from silx.math.histogram import Histogramnd
-from silx.math.combo import min_max
-from silx.gui import qt
-from silx.gui.plot import items
-from silx.gui.widgets.ElidedLabel import ElidedLabel
-from silx.utils.deprecation import deprecated
-
-_logger = logging.getLogger(__name__)
-
-
-class _ElidedLabel(ElidedLabel):
- """QLabel with a default size larger than what is displayed."""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
-
- def sizeHint(self):
- hint = super().sizeHint()
- nbchar = max(len(self.getText()), 12)
- width = self.fontMetrics().boundingRect('#' * nbchar).width()
- return qt.QSize(max(hint.width(), width), hint.height())
-
-
-class _StatWidget(qt.QWidget):
- """Widget displaying a name and a value
-
- :param parent:
- :param name:
- """
-
- def __init__(self, parent=None, name: str=''):
- super().__init__(parent)
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
-
- keyWidget = qt.QLabel(parent=self)
- keyWidget.setText("<b>" + name.capitalize() + ":<b>")
- layout.addWidget(keyWidget)
- self.__valueWidget = _ElidedLabel(parent=self)
- self.__valueWidget.setText("-")
- self.__valueWidget.setTextInteractionFlags(
- qt.Qt.TextSelectableByMouse | qt.Qt.TextSelectableByKeyboard)
- layout.addWidget(self.__valueWidget)
-
- def setValue(self, value: typing.Optional[float]):
- """Set the displayed value
-
- :param value:
- """
- self.__valueWidget.setText(
- "-" if value is None else "{:.5g}".format(value))
-
-
-class HistogramWidget(qt.QWidget):
- """Widget displaying a histogram and some statistic indicators"""
-
- _SUPPORTED_ITEM_CLASS = items.ImageBase, items.Scatter
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.setWindowTitle('Histogram')
-
- self.__itemRef = None # weakref on the item to track
-
- layout = qt.QVBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
-
- # Plot
- # Lazy import to avoid circular dependencies
- from silx.gui.plot.PlotWindow import Plot1D
- self.__plot = Plot1D(self)
- layout.addWidget(self.__plot)
-
- self.__plot.setDataMargins(0.1, 0.1, 0.1, 0.1)
- self.__plot.getXAxis().setLabel("Value")
- self.__plot.getYAxis().setLabel("Count")
- posInfo = self.__plot.getPositionInfoWidget()
- posInfo.setSnappingMode(posInfo.SNAPPING_CURVE)
-
- # Stats display
- statsWidget = qt.QWidget(self)
- layout.addWidget(statsWidget)
- statsLayout = qt.QHBoxLayout(statsWidget)
- statsLayout.setContentsMargins(4, 4, 4, 4)
-
- self.__statsWidgets = dict(
- (name, _StatWidget(parent=statsWidget, name=name))
- for name in ("min", "max", "mean", "std", "sum"))
-
- for widget in self.__statsWidgets.values():
- statsLayout.addWidget(widget)
- statsLayout.addStretch(1)
-
- def getPlotWidget(self):
- """Returns :class:`PlotWidget` use to display the histogram"""
- return self.__plot
-
- def resetZoom(self):
- """Reset PlotWidget zoom"""
- self.getPlotWidget().resetZoom()
-
- def reset(self):
- """Clear displayed information"""
- self.getPlotWidget().clear()
- self.setStatistics()
-
- def getItem(self) -> typing.Optional[items.Item]:
- """Returns item used to display histogram and statistics."""
- return None if self.__itemRef is None else self.__itemRef()
-
- def setItem(self, item: typing.Optional[items.Item]):
- """Set item from which to display histogram and statistics.
-
- :param item:
- """
- previous = self.getItem()
- if previous is not None:
- previous.sigItemChanged.disconnect(self.__itemChanged)
-
- self.__itemRef = None if item is None else weakref.ref(item)
- if item is not None:
- if isinstance(item, self._SUPPORTED_ITEM_CLASS):
- # Only listen signal for supported items
- item.sigItemChanged.connect(self.__itemChanged)
- self._updateFromItem()
-
- def __itemChanged(self, event):
- """Handle update of the item"""
- if event in (items.ItemChangedType.DATA, items.ItemChangedType.MASK):
- self._updateFromItem()
-
- def _updateFromItem(self):
- """Update histogram and stats from the item"""
- item = self.getItem()
-
- if item is None:
- self.reset()
- return
-
- if not isinstance(item, self._SUPPORTED_ITEM_CLASS):
- _logger.error("Unsupported item", item)
- self.reset()
- return
-
- # Compute histogram and stats
- array = item.getValueData(copy=False)
-
- if array.size == 0:
- self.reset()
- return
-
- xmin, xmax = min_max(array, min_positive=False, finite=True)
- nbins = min(1024, int(numpy.sqrt(array.size)))
- data_range = xmin, xmax
-
- # bad hack: get 256 bins in the case we have a B&W
- if numpy.issubdtype(array.dtype, numpy.integer):
- if nbins > xmax - xmin:
- nbins = xmax - xmin
-
- nbins = max(2, nbins)
-
- data = array.ravel().astype(numpy.float32)
- histogram = Histogramnd(data, n_bins=nbins, histo_range=data_range)
- if len(histogram.edges) != 1:
- _logger.error("Error while computing the histogram")
- self.reset()
- return
-
- self.setHistogram(histogram.histo, histogram.edges[0])
- self.resetZoom()
- self.setStatistics(
- min_=xmin,
- max_=xmax,
- mean=numpy.nanmean(array),
- std=numpy.nanstd(array),
- sum_=numpy.nansum(array))
-
- def setHistogram(self, histogram, edges):
- """Set displayed histogram
-
- :param histogram: Bin values (N)
- :param edges: Bin edges (N+1)
- """
- self.getPlotWidget().addHistogram(
- histogram=histogram,
- edges=edges,
- legend='histogram',
- fill=True,
- color='#66aad7',
- resetzoom=False)
-
- def getHistogram(self, copy: bool=True):
- """Returns currently displayed histogram.
-
- :param copy: True to get a copy,
- False to get internal representation (Do not modify!)
- :return: (histogram, edges) or None
- """
- for item in self.getPlotWidget().getItems():
- if item.getName() == 'histogram':
- return (item.getValueData(copy=copy),
- item.getBinEdgesData(copy=copy))
- else:
- return None
-
- def setStatistics(self,
- min_: typing.Optional[float] = None,
- max_: typing.Optional[float] = None,
- mean: typing.Optional[float] = None,
- std: typing.Optional[float] = None,
- sum_: typing.Optional[float] = None):
- """Set displayed statistic indicators."""
- self.__statsWidgets['min'].setValue(min_)
- self.__statsWidgets['max'].setValue(max_)
- self.__statsWidgets['mean'].setValue(mean)
- self.__statsWidgets['std'].setValue(std)
- self.__statsWidgets['sum'].setValue(sum_)
-
-
-class _LastActiveItem(qt.QObject):
-
- sigActiveItemChanged = qt.Signal(object, object)
- """Emitted when the active plot item have changed"""
-
- def __init__(self, parent, plot):
- assert plot is not None
- super(_LastActiveItem, self).__init__(parent=parent)
- self.__plot = weakref.ref(plot)
- self.__item = None
- item = self.__findActiveItem()
- self.setActiveItem(item)
- plot.sigActiveImageChanged.connect(self._activeImageChanged)
- plot.sigActiveScatterChanged.connect(self._activeScatterChanged)
-
- def getPlotWidget(self):
- return self.__plot()
-
- def __findActiveItem(self):
- plot = self.getPlotWidget()
- image = plot.getActiveImage()
- if image is not None:
- return image
- scatter = plot.getActiveScatter()
- if scatter is not None:
- return scatter
-
- def getActiveItem(self):
- if self.__item is None:
- return None
- item = self.__item()
- if item is None:
- self.__item = None
- return item
-
- def setActiveItem(self, item):
- previous = self.getActiveItem()
- if previous is item:
- return
- if item is None:
- self.__item = None
- else:
- self.__item = weakref.ref(item)
- self.sigActiveItemChanged.emit(previous, item)
-
- def _activeImageChanged(self, previous, current):
- """Handle active image change"""
- plot = self.getPlotWidget()
- if current is None: # Fall-back to active scatter if any
- self.setActiveItem(plot.getActiveScatter())
- else:
- item = plot.getImage(current)
- if item is None:
- self.setActiveItem(None)
- elif isinstance(item, items.ImageBase):
- self.setActiveItem(item)
- else:
- # Do not touch anything, which is consistent with silx v0.12 behavior
- pass
-
- def _activeScatterChanged(self, previous, current):
- """Handle active scatter change"""
- plot = self.getPlotWidget()
- if current is None: # Fall-back to active image if any
- self.setActiveItem(plot.getActiveImage())
- else:
- item = plot.getScatter(current)
- self.setActiveItem(item)
-
-
-class PixelIntensitiesHistoAction(PlotToolAction):
- """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):
- PlotToolAction.__init__(self,
- plot,
- icon='pixel-intensities',
- text='pixels intensity',
- tooltip='Compute image intensity distribution',
- parent=parent)
- self._lastItemFilter = _LastActiveItem(self, plot)
-
- def _connectPlot(self, window):
- self._lastItemFilter.sigActiveItemChanged.connect(self._activeItemChanged)
- item = self._lastItemFilter.getActiveItem()
- self.getHistogramWidget().setItem(item)
- PlotToolAction._connectPlot(self, window)
-
- def _disconnectPlot(self, window):
- self._lastItemFilter.sigActiveItemChanged.disconnect(self._activeItemChanged)
- PlotToolAction._disconnectPlot(self, window)
- self.getHistogramWidget().setItem(None)
-
- def _activeItemChanged(self, previous, current):
- if self._isWindowInUse():
- self.getHistogramWidget().setItem(current)
-
- @deprecated(since_version='0.15.0')
- def computeIntensityDistribution(self):
- self.getHistogramWidget()._updateFromItem()
-
- def getHistogramWidget(self):
- """Returns the widget displaying the histogram"""
- return self._getToolWindow()
-
- @deprecated(since_version='0.15.0',
- replacement='getHistogramWidget().getPlotWidget()')
- def getHistogramPlotWidget(self):
- return self._getToolWindow().getPlotWidget()
-
- def _createToolWindow(self):
- return HistogramWidget(self.plot, qt.Qt.Window)
-
- def getHistogram(self) -> typing.Optional[numpy.ndarray]:
- """Return the last computed histogram
-
- :return: the histogram displayed in the HistogramWidget
- """
- histogram = self.getHistogramWidget().getHistogram()
- return None if histogram is None else histogram[0]
diff --git a/silx/gui/plot/actions/io.py b/silx/gui/plot/actions/io.py
deleted file mode 100644
index f728b7a..0000000
--- a/silx/gui/plot/actions/io.py
+++ /dev/null
@@ -1,818 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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__ = "25/09/2020"
-
-from . import PlotAction
-from silx.io.utils import save1D, savespec, NEXUS_HDF5_EXT
-from silx.io.nxdata import save_NXdata
-import logging
-import sys
-import os.path
-from collections import OrderedDict
-import traceback
-import numpy
-from silx.utils.deprecation import deprecated
-from silx.gui import qt, printer
-from silx.gui.dialog.GroupDialog import GroupDialog
-from silx.third_party.EdfFile import EdfFile
-from silx.third_party.TiffIO import TiffIO
-from ...utils.image import convertArrayToQImage
-if sys.version_info[0] == 3:
- from io import BytesIO
-else:
- import cStringIO as _StringIO
- BytesIO = _StringIO.StringIO
-
-_logger = logging.getLogger(__name__)
-
-_NEXUS_HDF5_EXT_STR = ' '.join(['*' + ext for ext in NEXUS_HDF5_EXT])
-
-
-def selectOutputGroup(h5filename):
- """Open a dialog to prompt the user to select a group in
- which to output data.
-
- :param str h5filename: name of an existing HDF5 file
- :rtype: str
- :return: Name of output group, or None if the dialog was cancelled
- """
- dialog = GroupDialog()
- dialog.addFile(h5filename)
- dialog.setWindowTitle("Select an output group")
- if not dialog.exec_():
- return None
- return dialog.getSelectedDataUrl().data_path()
-
-
-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`.
- """
-
- SNAPSHOT_FILTER_SVG = 'Plot Snapshot as SVG (*.svg)'
- SNAPSHOT_FILTER_PNG = 'Plot Snapshot as PNG (*.png)'
-
- DEFAULT_ALL_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': '%.10g', 'delimiter': '', 'header': False})
- ))
-
- CURVE_FILTER_NPY = 'Curve as NumPy binary file (*.npy)'
-
- CURVE_FILTER_NXDATA = 'Curve as NXdata (%s)' % _NEXUS_HDF5_EXT_STR
-
- DEFAULT_CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [
- CURVE_FILTER_NPY, CURVE_FILTER_NXDATA]
-
- DEFAULT_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_NXDATA = 'Image as NXdata (%s)' % _NEXUS_HDF5_EXT_STR
-
- DEFAULT_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_NXDATA)
-
- SCATTER_FILTER_NXDATA = 'Scatter as NXdata (%s)' % _NEXUS_HDF5_EXT_STR
- DEFAULT_SCATTER_FILTERS = (SCATTER_FILTER_NXDATA,)
-
- # filters for which we don't want an "overwrite existing file" warning
- DEFAULT_APPEND_FILTERS = (CURVE_FILTER_NXDATA, IMAGE_FILTER_NXDATA,
- SCATTER_FILTER_NXDATA)
-
- def __init__(self, plot, parent=None):
- self._filters = {
- 'all': OrderedDict(),
- 'curve': OrderedDict(),
- 'curves': OrderedDict(),
- 'image': OrderedDict(),
- 'scatter': OrderedDict()}
-
- self._appendFilters = list(self.DEFAULT_APPEND_FILTERS)
-
- # Initialize filters
- for nameFilter in self.DEFAULT_ALL_FILTERS:
- self.setFileFilter(
- dataKind='all', nameFilter=nameFilter, func=self._saveSnapshot)
-
- for nameFilter in self.DEFAULT_CURVE_FILTERS:
- self.setFileFilter(
- dataKind='curve', nameFilter=nameFilter, func=self._saveCurve)
-
- for nameFilter in self.DEFAULT_ALL_CURVES_FILTERS:
- self.setFileFilter(
- dataKind='curves', nameFilter=nameFilter, func=self._saveCurves)
-
- for nameFilter in self.DEFAULT_IMAGE_FILTERS:
- self.setFileFilter(
- dataKind='image', nameFilter=nameFilter, func=self._saveImage)
-
- for nameFilter in self.DEFAULT_SCATTER_FILTERS:
- self.setFileFilter(
- dataKind='scatter', nameFilter=nameFilter, func=self._saveScatter)
-
- 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)
-
- @staticmethod
- def _errorMessage(informativeText='', parent=None):
- """Display an error message."""
- # TODO issue with QMessageBox size fixed and too small
- msg = qt.QMessageBox(parent)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setInformativeText(informativeText + ' ' + str(sys.exc_info()[1]))
- msg.setDetailedText(traceback.format_exc())
- msg.exec_()
-
- def _saveSnapshot(self, plot, 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
-
- plot.saveGraph(filename, fileFormat=fileFormat)
- return True
-
- def _getAxesLabels(self, item):
- # If curve has no associated label, get the default from the plot
- xlabel = item.getXLabel() or self.plot.getXAxis().getLabel()
- ylabel = item.getYLabel() or self.plot.getYAxis().getLabel()
- return xlabel, ylabel
-
- def _get1dData(self, item):
- "provide xdata, [ydata], xlabel, [ylabel] and manages error bars"
- xlabel, ylabel = self._getAxesLabels(item)
- x_data = item.getXData(copy=False)
- y_data = item.getYData(copy=False)
- x_err = item.getXErrorData(copy=False)
- y_err = item.getYErrorData(copy=False)
- labels = [ylabel]
- data = [y_data]
-
- if x_err is not None:
- if numpy.isscalar(x_err):
- data.append(numpy.zeros_like(y_data) + x_err)
- labels.append(xlabel + "_errors")
- elif x_err.ndim == 1:
- data.append(x_err)
- labels.append(xlabel + "_errors")
- elif x_err.ndim == 2:
- data.append(x_err[0])
- labels.append(xlabel + "_errors_below")
- data.append(x_err[1])
- labels.append(xlabel + "_errors_above")
-
- if y_err is not None:
- if numpy.isscalar(y_err):
- data.append(numpy.zeros_like(y_data) + y_err)
- labels.append(ylabel + "_errors")
- elif y_err.ndim == 1:
- data.append(y_err)
- labels.append(ylabel + "_errors")
- elif y_err.ndim == 2:
- data.append(y_err[0])
- labels.append(ylabel + "_errors_below")
- data.append(y_err[1])
- labels.append(ylabel + "_errors_above")
- return x_data, data, xlabel, labels
-
- @staticmethod
- def _selectWriteableOutputGroup(filename, parent):
- if os.path.exists(filename) and os.path.isfile(filename) \
- and os.access(filename, os.W_OK):
- entryPath = selectOutputGroup(filename)
- if entryPath is None:
- _logger.info("Save operation cancelled")
- return None
- return entryPath
- elif not os.path.exists(filename):
- # create new entry in new file
- return "/entry"
- else:
- SaveAction._errorMessage('Save failed (file access issue)\n', parent=parent)
- return None
-
- def _saveCurveAsNXdata(self, curve, filename):
- entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot)
- if entryPath is None:
- return False
-
- xlabel, ylabel = self._getAxesLabels(curve)
-
- return save_NXdata(
- filename,
- nxentry_name=entryPath,
- signal=curve.getYData(copy=False),
- axes=[curve.getXData(copy=False)],
- signal_name="y",
- axes_names=["x"],
- signal_long_name=ylabel,
- axes_long_names=[xlabel],
- signal_errors=curve.getYErrorData(copy=False),
- axes_errors=[curve.getXErrorData(copy=True)],
- title=self.plot.getGraphTitle())
-
- def _saveCurve(self, plot, 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.DEFAULT_CURVE_FILTERS:
- return False
-
- # Check if a curve is to be saved
- curve = 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 = plot.getAllCurves()
- if not curves:
- self._errorMessage("No curve to be saved", parent=self.plot)
- 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 or nxdata
- fmt, csvdelim, autoheader = ("", "", False)
-
- if nameFilter == self.CURVE_FILTER_NXDATA:
- return self._saveCurveAsNXdata(curve, filename)
-
- xdata, data, xlabel, labels = self._get1dData(curve)
-
- try:
- save1D(filename,
- xdata, data,
- xlabel, labels,
- fmt=fmt, csvdelim=csvdelim,
- autoheader=autoheader)
- except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
- return False
-
- return True
-
- def _saveCurves(self, plot, 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.DEFAULT_ALL_CURVES_FILTERS:
- return False
-
- curves = plot.getAllCurves()
- if not curves:
- self._errorMessage("No curves to be saved", parent=self.plot)
- return False
-
- curve = curves[0]
- scanno = 1
- try:
- xdata, data, xlabel, labels = self._get1dData(curve)
-
- specfile = savespec(filename,
- xdata, data,
- xlabel, labels,
- fmt="%.7g", scan_number=1, mode="w",
- write_file_header=True,
- close_file=False)
- except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
- return False
-
- for curve in curves[1:]:
- try:
- scanno += 1
- xdata, data, xlabel, labels = self._get1dData(curve)
- specfile = savespec(specfile,
- xdata, data,
- xlabel, labels,
- fmt="%.7g", scan_number=scanno,
- write_file_header=False,
- close_file=False)
- except IOError:
- self._errorMessage('Save failed\n', parent=self.plot)
- return False
- specfile.close()
-
- return True
-
- def _saveImage(self, plot, 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.DEFAULT_IMAGE_FILTERS:
- return False
-
- image = plot.getActiveImage()
- if image is None:
- qt.QMessageBox.warning(
- 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', parent=self.plot)
- return False
- return True
-
- elif nameFilter == self.IMAGE_FILTER_NXDATA:
- entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot)
- if entryPath is None:
- return False
- xorigin, yorigin = image.getOrigin()
- xscale, yscale = image.getScale()
- xaxis = xorigin + xscale * numpy.arange(data.shape[1])
- yaxis = yorigin + yscale * numpy.arange(data.shape[0])
- xlabel, ylabel = self._getAxesLabels(image)
- interpretation = "image" if len(data.shape) == 2 else "rgba-image"
-
- return save_NXdata(filename,
- nxentry_name=entryPath,
- signal=data,
- axes=[yaxis, xaxis],
- signal_name="image",
- axes_names=["y", "x"],
- axes_long_names=[ylabel, xlabel],
- title=plot.getGraphTitle(),
- interpretation=interpretation)
-
- 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', parent=self.plot)
- return False
- return True
-
- elif nameFilter == self.IMAGE_FILTER_RGB_PNG:
- # Get displayed image
- rgbaImage = image.getRgbaImageData(copy=False)
- # Convert RGB QImage
- qimage = convertArrayToQImage(rgbaImage[:, :, :3])
-
- if qimage.save(filename, 'PNG'):
- 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 _saveScatter(self, plot, 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.DEFAULT_SCATTER_FILTERS:
- return False
-
- if nameFilter == self.SCATTER_FILTER_NXDATA:
- entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot)
- if entryPath is None:
- return False
- scatter = plot.getScatter()
-
- x = scatter.getXData(copy=False)
- y = scatter.getYData(copy=False)
- z = scatter.getValueData(copy=False)
-
- xerror = scatter.getXErrorData(copy=False)
- if isinstance(xerror, float):
- xerror = xerror * numpy.ones(x.shape, dtype=numpy.float32)
-
- yerror = scatter.getYErrorData(copy=False)
- if isinstance(yerror, float):
- yerror = yerror * numpy.ones(x.shape, dtype=numpy.float32)
-
- xlabel = plot.getGraphXLabel()
- ylabel = plot.getGraphYLabel()
-
- return save_NXdata(
- filename,
- nxentry_name=entryPath,
- signal=z,
- axes=[x, y],
- signal_name="values",
- axes_names=["x", "y"],
- axes_long_names=[xlabel, ylabel],
- axes_errors=[xerror, yerror],
- title=plot.getGraphTitle())
-
- def setFileFilter(self, dataKind, nameFilter, func, index=None, appendToFile=False):
- """Set a name filter to add/replace a file format support
-
- :param str dataKind:
- The kind of data for which the provided filter is valid.
- One of: 'all', 'curve', 'curves', 'image', 'scatter'
- :param str nameFilter: The name filter in the QFileDialog.
- See :meth:`QFileDialog.setNameFilters`.
- :param callable func: The function to call to perform saving.
- Expected signature is:
- bool func(PlotWidget plot, str filename, str nameFilter)
- :param bool appendToFile: True to append the data into the selected
- file.
- :param integer index: Index of the filter in the final list (or None)
- """
- assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter')
-
- if appendToFile:
- self._appendFilters.append(nameFilter)
-
- # first append or replace the new filter to prevent colissions
- self._filters[dataKind][nameFilter] = func
- if index is None:
- # we are already done
- return
-
- # get the current ordered list of keys
- keyList = list(self._filters[dataKind].keys())
-
- # deal with negative indices
- if index < 0:
- index = len(keyList) + index
- if index < 0:
- index = 0
-
- if index >= len(keyList):
- # nothing to be done, already at the end
- txt = 'Requested index %d impossible, already at the end' % index
- _logger.info(txt)
- return
-
- # get the new ordered list
- oldIndex = keyList.index(nameFilter)
- del keyList[oldIndex]
- keyList.insert(index, nameFilter)
-
- # build the new filters
- newFilters = OrderedDict()
- for key in keyList:
- newFilters[key] = self._filters[dataKind][key]
-
- # and update the filters
- self._filters[dataKind] = newFilters
- return
-
- def getFileFilters(self, dataKind):
- """Returns the nameFilter and associated function for a kind of data.
-
- :param str dataKind:
- The kind of data for which the provided filter is valid.
- On of: 'all', 'curve', 'curves', 'image', 'scatter'
- :return: {nameFilter: function} associations.
- :rtype: collections.OrderedDict
- """
- assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter')
-
- return self._filters[dataKind].copy()
-
- def _actionTriggered(self, checked=False):
- """Handle save action."""
- # Set-up filters
- filters = OrderedDict()
-
- # Add image filters if there is an active image
- if self.plot.getActiveImage() is not None:
- filters.update(self._filters['image'].items())
-
- # Add curve filters if there is a curve to save
- if (self.plot.getActiveCurve() is not None or
- len(self.plot.getAllCurves()) == 1):
- filters.update(self._filters['curve'].items())
- if len(self.plot.getAllCurves()) >= 1:
- filters.update(self._filters['curves'].items())
-
- # Add scatter filters if there is a scatter
- # todo: CSV
- if self.plot.getScatter() is not None:
- filters.update(self._filters['scatter'].items())
-
- filters.update(self._filters['all'].items())
-
- # Create and run File dialog
- dialog = qt.QFileDialog(self.plot)
- dialog.setOption(dialog.DontUseNativeDialog)
- dialog.setWindowTitle("Output File Selection")
- dialog.setModal(1)
- dialog.setNameFilters(list(filters.keys()))
-
- dialog.setFileMode(dialog.AnyFile)
- dialog.setAcceptMode(dialog.AcceptSave)
-
- def onFilterSelection(filt_):
- # disable overwrite confirmation for NXdata types,
- # because we append the data to existing files
- if filt_ in self._appendFilters:
- dialog.setOption(dialog.DontConfirmOverwrite)
- else:
- dialog.setOption(dialog.DontConfirmOverwrite, False)
-
- dialog.filterSelected.connect(onFilterSelection)
-
- if not dialog.exec_():
- return False
-
- nameFilter = dialog.selectedNameFilter()
- filename = dialog.selectedFiles()[0]
- dialog.close()
-
- if '(' in nameFilter and ')' == nameFilter.strip()[-1]:
- # Check for correct file extension
- # Extract file extensions as .something
- extensions = [ext[ext.find('.'):] for ext in
- nameFilter[nameFilter.find('(') + 1:-1].split()]
- for ext in extensions:
- if (len(filename) > len(ext) and
- filename[-len(ext):].lower() == ext.lower()):
- break
- else: # filename has no extension supported in nameFilter, add one
- if len(extensions) >= 1:
- filename += extensions[0]
-
- # Handle save
- func = filters.get(nameFilter, None)
- if func is not None:
- return func(self.plot, filename, nameFilter)
- else:
- _logger.error('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`.
- """
-
- 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)
-
- def getPrinter(self):
- """The QPrinter instance used by the PrintAction.
-
- :rtype: QPrinter
- """
- return printer.getDefaultPrinter()
-
- @property
- @deprecated(replacement="getPrinter()", since_version="0.8.0")
- def printer(self):
- return self.getPrinter()
-
- 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.getPrinter(), 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.getPrinter()):
- return False
-
- pageRect = self.getPrinter().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.getPrinter(), 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.getPrinter().pageRect().width() / pixmap.width()
- yScale = self.getPrinter().pageRect().height() / pixmap.height()
- scale = min(xScale, yScale)
-
- # Draw pixmap with painter
- painter = qt.QPainter()
- if not painter.begin(self.getPrinter()):
- 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
deleted file mode 100644
index f86a377..0000000
--- a/silx/gui/plot/actions/medfilt.py
+++ /dev/null
@@ -1,147 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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__ = "10/10/2018"
-
-from .PlotToolAction import PlotToolAction
-from silx.gui.widgets.MedianFilterDialog import MedianFilterDialog
-from silx.math.medianfilter import medfilt2d
-import logging
-
-_logger = logging.getLogger(__name__)
-
-
-class MedianFilterAction(PlotToolAction):
- """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):
- PlotToolAction.__init__(self,
- plot,
- icon='median-filter',
- text='median filter',
- tooltip='Apply a median filter on the image',
- parent=parent)
- self._originalImage = None
- self._legend = None
- self._filteredImage = None
-
- def _createToolWindow(self):
- popup = MedianFilterDialog(parent=self.plot)
- popup.sigFilterOptChanged.connect(self._updateFilter)
- return popup
-
- def _connectPlot(self, window):
- PlotToolAction._connectPlot(self, window)
- self.plot.sigActiveImageChanged.connect(self._updateActiveImage)
- self._updateActiveImage()
-
- def _disconnectPlot(self, window):
- PlotToolAction._disconnectPlot(self, window)
- self.plot.sigActiveImageChanged.disconnect(self._updateActiveImage)
-
- 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).getName()
-
- 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 NotImplementedError('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
deleted file mode 100644
index ee05256..0000000
--- a/silx/gui/plot/actions/mode.py
+++ /dev/null
@@ -1,104 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2018 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 of a :class:`.PlotWidget`.
-
-The following QAction are available:
-
-- :class:`ZoomModeAction`
-- :class:`PanModeAction`
-"""
-
-from __future__ import division
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "16/08/2017"
-
-from . import PlotAction
-import logging
-
-_logger = logging.getLogger(__name__)
-
-
-class ZoomModeAction(PlotAction):
- """QAction controlling the zoom mode of a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(ZoomModeAction, self).__init__(
- plot, icon='zoom', text='Zoom mode',
- tooltip='Zoom in or out',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- # Listen to mode change
- self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
- # Init the state
- self._modeChanged(None)
-
- def _modeChanged(self, source):
- modeDict = self.plot.getInteractiveMode()
- old = self.blockSignals(True)
- self.setChecked(modeDict["mode"] == "zoom")
- self.blockSignals(old)
-
- def _actionTriggered(self, checked=False):
- plot = self.plot
- if plot is not None:
- plot.setInteractiveMode('zoom', source=self)
-
-
-class PanModeAction(PlotAction):
- """QAction controlling the pan mode of a :class:`.PlotWidget`.
-
- :param plot: :class:`.PlotWidget` instance on which to operate
- :param parent: See :class:`QAction`
- """
-
- def __init__(self, plot, parent=None):
- super(PanModeAction, self).__init__(
- plot, icon='pan', text='Pan mode',
- tooltip='Pan the view',
- triggered=self._actionTriggered,
- checkable=True, parent=parent)
- # Listen to mode change
- self.plot.sigInteractiveModeChanged.connect(self._modeChanged)
- # Init the state
- self._modeChanged(None)
-
- def _modeChanged(self, source):
- modeDict = self.plot.getInteractiveMode()
- old = self.blockSignals(True)
- self.setChecked(modeDict["mode"] == "pan")
- self.blockSignals(old)
-
- def _actionTriggered(self, checked=False):
- plot = self.plot
- if plot is not None:
- plot.setInteractiveMode('pan', source=self)
diff --git a/silx/gui/plot/backends/BackendBase.py b/silx/gui/plot/backends/BackendBase.py
deleted file mode 100755
index 6fc1aa7..0000000
--- a/silx/gui/plot/backends/BackendBase.py
+++ /dev/null
@@ -1,578 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ############################################################################*/
-"""Base class for Plot backends.
-
-It documents the Plot backend API.
-
-This API is a simplified version of PyMca PlotBackend API.
-"""
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "21/12/2018"
-
-import weakref
-from ... import qt
-
-
-# Names for setCursor
-CURSOR_DEFAULT = 'default'
-CURSOR_POINTING = 'pointing'
-CURSOR_SIZE_HOR = 'size horizontal'
-CURSOR_SIZE_VER = 'size vertical'
-CURSOR_SIZE_ALL = 'size all'
-
-
-class BackendBase(object):
- """Class defining the API a backend of the Plot should provide."""
-
- def __init__(self, plot, parent=None):
- """Init.
-
- :param Plot plot: The Plot this backend is attached to
- :param parent: The parent widget of the plot widget.
- """
- self.__xLimits = 1., 100.
- self.__yLimits = {'left': (1., 100.), 'right': (1., 100.)}
- self.__yAxisInverted = False
- self.__keepDataAspectRatio = False
- self.__xAxisTimeSeries = False
- self._xAxisTimeZone = None
- # Store a weakref to get access to the plot state.
- self._setPlot(plot)
-
- @property
- def _plot(self):
- """The plot this backend is attached to."""
- if self._plotRef is None:
- raise RuntimeError('This backend is not attached to a Plot')
-
- plot = self._plotRef()
- if plot is None:
- raise RuntimeError('This backend is no more attached to a Plot')
- return plot
-
- def _setPlot(self, plot):
- """Allow to set plot after init.
-
- Use with caution, basically **immediately** after init.
- """
- self._plotRef = weakref.ref(plot)
-
- # Add methods
-
- def addCurve(self, x, y,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror,
- fill, alpha, symbolsize, baseline):
- """Add a 1D curve given by x an y to the graph.
-
- :param numpy.ndarray x: The data corresponding to the x axis
- :param numpy.ndarray y: The data corresponding to the y axis
- :param color: color(s) to be used
- :type color: string ("#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::
-
- - ' ' or '' no symbol
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
-
- :param float linewidth: The width of the curve in pixels
- :param str linestyle: Type of line::
-
- - ' ' or '' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
-
- :param str yaxis: The Y axis this curve belongs to in: 'left', 'right'
- :param xerror: Values with the uncertainties on the x values
- :type xerror: numpy.ndarray or None
- :param yerror: Values with the uncertainties on the y values
- :type yerror: numpy.ndarray or None
- :param bool fill: True to fill the curve, False otherwise
- :param float alpha: Curve opacity, as a float in [0., 1.]
- :param float symbolsize: Size of the symbol (if any) drawn
- at each (x, y) position.
- :returns: The handle used by the backend to univocally access the curve
- """
- return object()
-
- def addImage(self, data,
- origin, scale,
- colormap, alpha):
- """Add an image to the plot.
-
- :param numpy.ndarray data: (nrows, ncolumns) data or
- (nrows, ncolumns, RGBA) ubyte array
- :param origin: (origin X, origin Y) of the data.
- Default: (0., 0.)
- :type origin: 2-tuple of float
- :param scale: (scale X, scale Y) of the data.
- Default: (1., 1.)
- :type scale: 2-tuple of float
- :param ~silx.gui.colors.Colormap colormap: Colormap object to use.
- Ignored if data is RGB(A).
- :param float alpha: Opacity of the image, as a float in range [0, 1].
- :returns: The handle used by the backend to univocally access the image
- """
- return object()
-
- def addTriangles(self, x, y, triangles,
- color, alpha):
- """Add a set of triangles.
-
- :param numpy.ndarray x: The data corresponding to the x axis
- :param numpy.ndarray y: The data corresponding to the y axis
- :param numpy.ndarray triangles: The indices to make triangles
- as a (Ntriangle, 3) array
- :param numpy.ndarray color: color(s) as (npoints, 4) array
- :param float alpha: Opacity as a float in [0., 1.]
- :returns: The triangles' unique identifier used by the backend
- """
- return object()
-
- def addShape(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
- """Add an item (i.e. a shape) to the plot.
-
- :param numpy.ndarray x: The X coords of the points of the shape
- :param numpy.ndarray y: The Y coords of the points of the shape
- :param str shape: Type of item to be drawn in
- hline, polygon, rectangle, vline, polylines
- :param str color: Color of the item
- :param bool fill: True to fill the shape
- :param bool overlay: True if item is an overlay, False otherwise
- :param str linestyle: Style of the line.
- Only relevant for line markers where X or Y is None.
- Value in:
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
- :param float linewidth: Width of the line.
- Only relevant for line markers where X or Y is None.
- :param str linebgcolor: Background color of the line, e.g., 'blue', 'b',
- '#FF0000'. It is used to draw dotted line using a second color.
- :returns: The handle used by the backend to univocally access the item
- """
- return object()
-
- def addMarker(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
- """Add a point, vertical line or horizontal line marker to the plot.
-
- :param float x: Horizontal position of the marker in graph coordinates.
- If None, the marker is a horizontal line.
- :param float y: Vertical position of the marker in graph coordinates.
- If None, the marker is a vertical line.
- :param str text: Text associated to the marker (or None for no text)
- :param str color: Color to be used for instance 'blue', 'b', '#FF0000'
- :param str symbol: Symbol representing the marker.
- Only relevant for point markers where X and Y are not None.
- Value in:
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
- :param str linestyle: Style of the line.
- Only relevant for line markers where X or Y is None.
- Value in:
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
- :param float linewidth: Width of the line.
- Only relevant for line markers where X or Y is None.
- :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.
- :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.
- :param str yaxis: The Y axis this marker belongs to in: 'left', 'right'
- :return: Handle used by the backend to univocally access the marker
- """
- return object()
-
- # Remove methods
-
- def remove(self, item):
- """Remove an existing item from the plot.
-
- :param item: A backend specific item handle returned by a add* method
- """
- pass
-
- # Interaction methods
-
- def setGraphCursorShape(self, cursor):
- """Set the cursor shape.
-
- To override in interactive backends.
-
- :param str cursor: Name of the cursor shape or None
- """
- pass
-
- def setGraphCursor(self, flag, color, linewidth, linestyle):
- """Toggle the display of a crosshair cursor and set its attributes.
-
- To override in interactive backends.
-
- :param bool flag: Toggle the display of a crosshair cursor.
- :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.
- :param int linewidth: The width of the lines of the crosshair.
- :param linestyle: Type of line::
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
-
- :type linestyle: None or one of the predefined styles.
- """
- pass
-
- def getItemsFromBackToFront(self, condition=None):
- """Returns the list of plot items order as rendered by the backend.
-
- This is the order used for rendering.
- By default, it takes into account overlays, z value and order of addition of items,
- but backends can override it.
-
- :param callable condition:
- Callable taking an item as input and returning False for items to skip.
- If None (default), no item is skipped.
- :rtype: List[~silx.gui.plot.items.Item]
- """
- # Sort items: Overlays first, then others
- # and in each category ordered by z and then by order of addition
- # as content keeps this order.
- content = self._plot.getItems()
- if condition is not None:
- content = [item for item in content if condition(item)]
-
- return sorted(
- content,
- key=lambda i: ((1 if i.isOverlay() else 0), i.getZValue()))
-
- def pickItem(self, x, y, item):
- """Return picked indices if any, or None.
-
- :param float x: The x pixel coord where to pick.
- :param float y: The y pixel coord where to pick.
- :param item: A backend item created with add* methods.
- :return: None if item was not picked, else returns
- picked indices information.
- :rtype: Union[None,List]
- """
- return None
-
- # Update curve
-
- def setCurveColor(self, curve, color):
- """Set the color of a curve.
-
- :param curve: The curve handle
- :param str color: The color to use.
- """
- pass
-
- # Misc.
-
- def getWidgetHandle(self):
- """Return the widget this backend is drawing to."""
- return None
-
- def postRedisplay(self):
- """Trigger a :meth:`Plot.replot`.
-
- Default implementation triggers a synchronous replot if plot is dirty.
- This method should be overridden by the embedding widget in order to
- provide an asynchronous call to replot in order to optimize the number
- replot operations.
- """
- # This method can be deferred and it might happen that plot has been
- # destroyed in between, especially with unittests
-
- plot = self._plotRef()
- if plot is not None and plot._getDirtyPlot():
- plot.replot()
-
- def replot(self):
- """Redraw the plot."""
- pass
-
- def saveGraph(self, fileName, fileFormat, dpi):
- """Save the graph to a file (or a StringIO)
-
- At least "png", "svg" are supported.
-
- :param fileName: Destination
- :type fileName: String or StringIO or BytesIO
- :param str fileFormat: String specifying the format
- :param int dpi: The resolution to use or None.
- """
- pass
-
- # Graph labels
-
- def setGraphTitle(self, title):
- """Set the main title of the plot.
-
- :param str title: Title associated to the plot
- """
- pass
-
- def setGraphXLabel(self, label):
- """Set the X axis label.
-
- :param str label: label associated to the plot bottom X axis
- """
- pass
-
- def setGraphYLabel(self, label, axis):
- """Set the left Y axis label.
-
- :param str label: label associated to the plot left Y axis
- :param str axis: The axis for which to get the limits: left or right
- """
- pass
-
- # Graph limits
-
- def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
- """Set the limits of the X and Y axes at once.
-
- :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
- :param float y2max: maximum right axis value
- """
- self.__xLimits = xmin, xmax
- self.__yLimits['left'] = ymin, ymax
- if y2min is not None and y2max is not None:
- self.__yLimits['right'] = y2min, y2max
-
- def getGraphXLimits(self):
- """Get the graph X (bottom) limits.
-
- :return: Minimum and maximum values of the X axis
- """
- return self.__xLimits
-
- def setGraphXLimits(self, xmin, xmax):
- """Set the limits of X axis.
-
- :param float xmin: minimum bottom axis value
- :param float xmax: maximum bottom axis value
- """
- self.__xLimits = xmin, xmax
-
- def getGraphYLimits(self, axis):
- """Get the graph Y (left) limits.
-
- :param str axis: The axis for which to get the limits: left or right
- :return: Minimum and maximum values of the Y axis
- """
- return self.__yLimits[axis]
-
- def setGraphYLimits(self, ymin, ymax, axis):
- """Set the limits of the Y axis.
-
- :param float ymin: minimum left axis value
- :param float ymax: maximum left axis value
- :param str axis: The axis for which to get the limits: left or right
- """
- self.__yLimits[axis] = ymin, ymax
-
- # Graph axes
-
-
- def getXAxisTimeZone(self):
- """Returns tzinfo that is used if the X-Axis plots date-times.
-
- None means the datetimes are interpreted as local time.
-
- :rtype: datetime.tzinfo of None.
- """
- return self._xAxisTimeZone
-
- def setXAxisTimeZone(self, tz):
- """Sets tzinfo that is used if the X-Axis plots date-times.
-
- Use None to let the datetimes be interpreted as local time.
-
- :rtype: datetime.tzinfo of None.
- """
- self._xAxisTimeZone = tz
-
- def isXAxisTimeSeries(self):
- """Return True if the X-axis scale shows datetime objects.
-
- :rtype: bool
- """
- return self.__xAxisTimeSeries
-
- def setXAxisTimeSeries(self, isTimeSeries):
- """Set whether the X-axis is a time series
-
- :param bool flag: True to switch to time series, False for regular axis.
- """
- self.__xAxisTimeSeries = bool(isTimeSeries)
-
- def setXAxisLogarithmic(self, flag):
- """Set the X axis scale between linear and log.
-
- :param bool flag: If True, the bottom axis will use a log scale
- """
- pass
-
- def setYAxisLogarithmic(self, flag):
- """Set the Y axis scale between linear and log.
-
- :param bool flag: If True, the left axis will use a log scale
- """
- pass
-
- def setYAxisInverted(self, flag):
- """Invert the Y axis.
-
- :param bool flag: If True, put the vertical axis origin on the top
- """
- self.__yAxisInverted = bool(flag)
-
- def isYAxisInverted(self):
- """Return True if left Y axis is inverted, False otherwise."""
- return self.__yAxisInverted
-
- def isKeepDataAspectRatio(self):
- """Returns whether the plot is keeping data aspect ratio or not."""
- return self.__keepDataAspectRatio
-
- def setKeepDataAspectRatio(self, flag):
- """Set whether to keep data aspect ratio or not.
-
- :param flag: True to respect data aspect ratio
- :type flag: Boolean, default True
- """
- self.__keepDataAspectRatio = bool(flag)
-
- def setGraphGrid(self, which):
- """Set grid.
-
- :param which: None to disable grid, 'major' for major grid,
- 'both' for major and minor grid
- """
- pass
-
- # Data <-> Pixel coordinates conversion
-
- def dataToPixel(self, x, y, axis):
- """Convert a position in data space to a position in pixels
- in the widget.
-
- :param float x: The X coordinate in data space.
- :param float y: The Y coordinate in data space.
- :param str axis: The Y axis to use for the conversion
- ('left' or 'right').
- :returns: The corresponding position in pixels or
- None if the data position is not in the displayed area.
- :rtype: A tuple of 2 floats: (xPixel, yPixel) or None.
- """
- raise NotImplementedError()
-
- def pixelToData(self, x, y, axis):
- """Convert a position in pixels in the widget to a position in
- the data space.
-
- :param float x: The X coordinate in pixels.
- :param float y: The Y coordinate in pixels.
- :param str axis: The Y axis to use for the conversion
- ('left' or 'right').
- :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.
- """
- raise NotImplementedError()
-
- def getPlotBoundsInPixels(self):
- """Plot area bounds in widget coordinates in pixels.
-
- :return: bounds as a 4-tuple of int: (left, top, width, height)
- """
- raise NotImplementedError()
-
- def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
- """Set the size of plot margins as ratios.
-
- Values are expected in [0., 1.]
-
- :param float left:
- :param float top:
- :param float right:
- :param float bottom:
- """
- pass
-
- def setForegroundColors(self, foregroundColor, gridColor):
- """Set foreground and grid colors used to display this widget.
-
- :param List[float] foregroundColor: RGBA foreground color of the widget
- :param List[float] gridColor: RGBA grid color of the data view
- """
- pass
-
- def setBackgroundColors(self, backgroundColor, dataBackgroundColor):
- """Set background colors used to display this widget.
-
- :param List[float] backgroundColor: RGBA background color of the widget
- :param Union[Tuple[float],None] dataBackgroundColor:
- RGBA background color of the data view
- """
- pass
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
deleted file mode 100755
index 432b0b0..0000000
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ /dev/null
@@ -1,1544 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2021 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.
-#
-# ###########################################################################*/
-"""Matplotlib Plot backend."""
-
-from __future__ import division
-
-__authors__ = ["V.A. Sole", "T. Vincent, H. Payno"]
-__license__ = "MIT"
-__date__ = "21/12/2018"
-
-
-import logging
-import datetime as dt
-from typing import Tuple
-import numpy
-
-from pkg_resources import parse_version as _parse_version
-
-
-_logger = logging.getLogger(__name__)
-
-
-from ... import qt
-
-# First of all init matplotlib and set its backend
-from ...utils.matplotlib import FigureCanvasQTAgg
-import matplotlib
-from matplotlib.container import Container
-from matplotlib.figure import Figure
-from matplotlib.patches import Rectangle, Polygon
-from matplotlib.image import AxesImage
-from matplotlib.backend_bases import MouseEvent
-from matplotlib.lines import Line2D
-from matplotlib.text import Text
-from matplotlib.collections import PathCollection, LineCollection
-from matplotlib.ticker import Formatter, ScalarFormatter, Locator
-from matplotlib.tri import Triangulation
-from matplotlib.collections import TriMesh
-from matplotlib import path as mpath
-
-from . import BackendBase
-from .. import items
-from .._utils import FLOAT32_MINPOS
-from .._utils.dtime_ticklayout import calcTicks, bestFormatString, timestamp
-
-_PATCH_LINESTYLE = {
- "-": 'solid',
- "--": 'dashed',
- '-.': 'dashdot',
- ':': 'dotted',
- '': "solid",
- None: "solid",
-}
-"""Patches do not uses the same matplotlib syntax"""
-
-_MARKER_PATHS = {}
-"""Store cached extra marker paths"""
-
-_SPECIAL_MARKERS = {
- 'tickleft': 0,
- 'tickright': 1,
- 'tickup': 2,
- 'tickdown': 3,
- 'caretleft': 4,
- 'caretright': 5,
- 'caretup': 6,
- 'caretdown': 7,
-}
-
-
-def normalize_linestyle(linestyle):
- """Normalize known old-style linestyle, else return the provided value."""
- return _PATCH_LINESTYLE.get(linestyle, linestyle)
-
-def get_path_from_symbol(symbol):
- """Get the path representation of a symbol, else None if
- it is not provided.
-
- :param str symbol: Symbol description used by silx
- :rtype: Union[None,matplotlib.path.Path]
- """
- if symbol == u'\u2665':
- path = _MARKER_PATHS.get(symbol, None)
- if path is not None:
- return path
- vertices = numpy.array([
- [0,-99],
- [31,-73], [47,-55], [55,-46],
- [63,-37], [94,-2], [94,33],
- [94,69], [71,89], [47,89],
- [24,89], [8,74], [0,58],
- [-8,74], [-24,89], [-47,89],
- [-71,89], [-94,69], [-94,33],
- [-94,-2], [-63,-37], [-55,-46],
- [-47,-55], [-31,-73], [0,-99],
- [0,-99]])
- codes = [mpath.Path.CURVE4] * len(vertices)
- codes[0] = mpath.Path.MOVETO
- codes[-1] = mpath.Path.CLOSEPOLY
- path = mpath.Path(vertices, codes)
- _MARKER_PATHS[symbol] = path
- return path
- return None
-
-class NiceDateLocator(Locator):
- """
- Matplotlib Locator that uses Nice Numbers algorithm (adapted to dates)
- to find the tick locations. This results in the same number behaviour
- as when using the silx Open GL backend.
-
- Expects the data to be posix timestampes (i.e. seconds since 1970)
- """
- def __init__(self, numTicks=5, tz=None):
- """
- :param numTicks: target number of ticks
- :param datetime.tzinfo tz: optional time zone. None is local time.
- """
- super(NiceDateLocator, self).__init__()
- self.numTicks = numTicks
-
- self._spacing = None
- self._unit = None
- self.tz = tz
-
- @property
- def spacing(self):
- """ The current spacing. Will be updated when new tick value are made"""
- return self._spacing
-
- @property
- def unit(self):
- """ The current DtUnit. Will be updated when new tick value are made"""
- return self._unit
-
- def __call__(self):
- """Return the locations of the ticks"""
- vmin, vmax = self.axis.get_view_interval()
- return self.tick_values(vmin, vmax)
-
- def tick_values(self, vmin, vmax):
- """ Calculates tick values
- """
- if vmax < vmin:
- vmin, vmax = vmax, vmin
-
- # vmin and vmax should be timestamps (i.e. seconds since 1 Jan 1970)
- dtMin = dt.datetime.fromtimestamp(vmin, tz=self.tz)
- dtMax = dt.datetime.fromtimestamp(vmax, tz=self.tz)
- dtTicks, self._spacing, self._unit = \
- calcTicks(dtMin, dtMax, self.numTicks)
-
- # Convert datetime back to time stamps.
- ticks = [timestamp(dtTick) for dtTick in dtTicks]
- return ticks
-
-
-class NiceAutoDateFormatter(Formatter):
- """
- Matplotlib FuncFormatter that is linked to a NiceDateLocator and gives the
- best possible formats given the locators current spacing an date unit.
- """
-
- def __init__(self, locator, tz=None):
- """
- :param niceDateLocator: a NiceDateLocator object
- :param datetime.tzinfo tz: optional time zone. None is local time.
- """
- super(NiceAutoDateFormatter, self).__init__()
- self.locator = locator
- self.tz = tz
-
- @property
- def formatString(self):
- if self.locator.spacing is None or self.locator.unit is None:
- # Locator has no spacing or units yet. Return elaborate fmtString
- return "Y-%m-%d %H:%M:%S"
- else:
- return bestFormatString(self.locator.spacing, self.locator.unit)
-
- def __call__(self, x, pos=None):
- """Return the format for tick val *x* at position *pos*
- Expects x to be a POSIX timestamp (seconds since 1 Jan 1970)
- """
- dateTime = dt.datetime.fromtimestamp(x, tz=self.tz)
- tickStr = dateTime.strftime(self.formatString)
- return tickStr
-
-
-class _PickableContainer(Container):
- """Artists container with a :meth:`contains` method"""
-
- def __init__(self, *args, **kwargs):
- Container.__init__(self, *args, **kwargs)
- self.__zorder = None
-
- @property
- def axes(self):
- """Mimin Artist.axes"""
- for child in self.get_children():
- if hasattr(child, 'axes'):
- return child.axes
- return None
-
- def draw(self, *args, **kwargs):
- """artist-like draw to broadcast draw to children"""
- for child in self.get_children():
- child.draw(*args, **kwargs)
-
- def get_zorder(self):
- """Mimic Artist.get_zorder"""
- return self.__zorder
-
- def set_zorder(self, z):
- """Mimic Artist.set_zorder to broadcast to children"""
- if z != self.__zorder:
- self.__zorder = z
- for child in self.get_children():
- child.set_zorder(z)
-
- def contains(self, mouseevent):
- """Mimic Artist.contains, and call it on all children.
-
- :param mouseevent:
- :return: Picking status and associated information as a dict
- :rtype: (bool,dict)
- """
- # Goes through children from front to back and return first picked one.
- for child in reversed(self.get_children()):
- picked, info = child.contains(mouseevent)
- if picked:
- return picked, info
- return False, {}
-
-
-class _TextWithOffset(Text):
- """Text object which can be displayed at a specific position
- of the plot, but with a pixel offset"""
-
- def __init__(self, *args, **kwargs):
- Text.__init__(self, *args, **kwargs)
- self.pixel_offset = (0, 0)
- self.__cache = None
-
- def draw(self, renderer):
- self.__cache = None
- return Text.draw(self, renderer)
-
- def __get_xy(self):
- if self.__cache is not None:
- return self.__cache
-
- align = self.get_horizontalalignment()
- if align == "left":
- xoffset = self.pixel_offset[0]
- elif align == "right":
- xoffset = -self.pixel_offset[0]
- else:
- xoffset = 0
-
- align = self.get_verticalalignment()
- if align == "top":
- yoffset = -self.pixel_offset[1]
- elif align == "bottom":
- yoffset = self.pixel_offset[1]
- else:
- yoffset = 0
-
- trans = self.get_transform()
- x = super(_TextWithOffset, self).convert_xunits(self._x)
- y = super(_TextWithOffset, self).convert_xunits(self._y)
- pos = x, y
-
- try:
- invtrans = trans.inverted()
- except numpy.linalg.LinAlgError:
- # Cannot inverse transform, fallback: pos without offset
- self.__cache = None
- return pos
-
- proj = trans.transform_point(pos)
- proj = proj + numpy.array((xoffset, yoffset))
- pos = invtrans.transform_point(proj)
- self.__cache = pos
- return pos
-
- def convert_xunits(self, x):
- """Return the pixel position of the annotated point."""
- return self.__get_xy()[0]
-
- def convert_yunits(self, y):
- """Return the pixel position of the annotated point."""
- return self.__get_xy()[1]
-
-
-class _MarkerContainer(_PickableContainer):
- """Marker artists container supporting draw/remove and text position update
-
- :param artists:
- Iterable with either one Line2D or a Line2D and a Text.
- The use of an iterable if enforced by Container being
- a subclass of tuple that defines a specific __new__.
- :param x: X coordinate of the marker (None for horizontal lines)
- :param y: Y coordinate of the marker (None for vertical lines)
- """
-
- def __init__(self, artists, symbol, x, y, yAxis):
- self.line = artists[0]
- self.text = artists[1] if len(artists) > 1 else None
- self.symbol = symbol
- self.x = x
- self.y = y
- self.yAxis = yAxis
-
- _PickableContainer.__init__(self, artists)
-
- def draw(self, *args, **kwargs):
- """artist-like draw to broadcast draw to line and text"""
- self.line.draw(*args, **kwargs)
- if self.text is not None:
- self.text.draw(*args, **kwargs)
-
- def updateMarkerText(self, xmin, xmax, ymin, ymax, yinverted):
- """Update marker text position and visibility according to plot limits
-
- :param xmin: X axis lower limit
- :param xmax: X axis upper limit
- :param ymin: Y axis lower limit
- :param ymax: Y axis upper limit
- :param yinverted: True if the y axis is inverted
- """
- if self.text is not None:
- visible = ((self.x is None or xmin <= self.x <= xmax) and
- (self.y is None or ymin <= self.y <= ymax))
- self.text.set_visible(visible)
-
- if self.x is not None and self.y is not None:
- if self.symbol is None:
- valign = 'baseline'
- else:
- if yinverted:
- valign = 'bottom'
- else:
- valign = 'top'
- self.text.set_verticalalignment(valign)
-
- elif self.y is None: # vertical line
- # Always display it on top
- center = (ymax + ymin) * 0.5
- pos = (ymax - ymin) * 0.5 * 0.99
- if yinverted:
- pos = -pos
- self.text.set_y(center + pos)
-
- elif self.x is None: # Horizontal line
- delta = abs(xmax - xmin)
- if xmin > xmax:
- xmax = xmin
- xmax -= 0.005 * delta
- self.text.set_x(xmax)
-
- def contains(self, mouseevent):
- """Mimic Artist.contains, and call it on the line Artist.
-
- :param mouseevent:
- :return: Picking status and associated information as a dict
- :rtype: (bool,dict)
- """
- return self.line.contains(mouseevent)
-
-
-class _DoubleColoredLinePatch(matplotlib.patches.Patch):
- """Matplotlib patch to display any patch using double color."""
-
- def __init__(self, patch):
- super(_DoubleColoredLinePatch, self).__init__()
- self.__patch = patch
- self.linebgcolor = None
-
- def __getattr__(self, name):
- return getattr(self.__patch, name)
-
- def draw(self, renderer):
- oldLineStype = self.__patch.get_linestyle()
- if self.linebgcolor is not None and oldLineStype != "solid":
- oldLineColor = self.__patch.get_edgecolor()
- oldHatch = self.__patch.get_hatch()
- self.__patch.set_linestyle("solid")
- self.__patch.set_edgecolor(self.linebgcolor)
- self.__patch.set_hatch(None)
- self.__patch.draw(renderer)
- self.__patch.set_linestyle(oldLineStype)
- self.__patch.set_edgecolor(oldLineColor)
- self.__patch.set_hatch(oldHatch)
- self.__patch.draw(renderer)
-
- def set_transform(self, transform):
- self.__patch.set_transform(transform)
-
- def get_path(self):
- return self.__patch.get_path()
-
- def contains(self, mouseevent, radius=None):
- return self.__patch.contains(mouseevent, radius)
-
- def contains_point(self, point, radius=None):
- return self.__patch.contains_point(point, radius)
-
-
-class Image(AxesImage):
- """An AxesImage with a fast path for uint8 RGBA images.
-
- :param List[float] silx_origin: (ox, oy) Offset of the image.
- :param List[float] silx_scale: (sx, sy) Scale of the image.
- """
-
- def __init__(self, *args,
- silx_origin=(0., 0.),
- silx_scale=(1., 1.),
- **kwargs):
- super().__init__(*args, **kwargs)
- self.__silx_origin = silx_origin
- self.__silx_scale = silx_scale
-
- def contains(self, mouseevent):
- """Overridden to fill 'ind' with row and column"""
- inside, info = super().contains(mouseevent)
- if inside:
- x, y = mouseevent.xdata, mouseevent.ydata
- ox, oy = self.__silx_origin
- sx, sy = self.__silx_scale
- height, width = self.get_size()
- column = numpy.clip(int((x - ox) / sx), 0, width - 1)
- row = numpy.clip(int((y - oy) / sy), 0, height - 1)
- info['ind'] = (row,), (column,)
- return inside, info
-
- def set_data(self, A):
- """Overridden to add a fast path for RGBA unit8 images"""
- A = numpy.array(A, copy=False)
- if A.ndim != 3 or A.shape[2] != 4 or A.dtype != numpy.uint8:
- super(Image, self).set_data(A)
- else:
- # Call AxesImage.set_data with small data to set attributes
- super(Image, self).set_data(numpy.zeros((2, 2, 4), dtype=A.dtype))
- self._A = A # Override stored data
-
-
-class BackendMatplotlib(BackendBase.BackendBase):
- """Base class for Matplotlib backend without a FigureCanvas.
-
- For interactive on screen plot, see :class:`BackendMatplotlibQt`.
-
- See :class:`BackendBase.BackendBase` for public API documentation.
- """
-
- def __init__(self, plot, parent=None):
- super(BackendMatplotlib, self).__init__(plot, parent)
-
- # matplotlib is handling keep aspect ratio at draw time
- # When keep aspect ratio is on, and one changes the limits and
- # ask them *before* next draw has been performed he will get the
- # limits without applying keep aspect ratio.
- # This attribute is used to ensure consistent values returned
- # when getting the limits at the expense of a replot
- self._dirtyLimits = True
- self._axesDisplayed = True
- self._matplotlibVersion = _parse_version(matplotlib.__version__)
-
- self.fig = Figure()
- self.fig.set_facecolor("w")
-
- self.ax = self.fig.add_axes([.15, .15, .75, .75], label="left")
- self.ax2 = self.ax.twinx()
- self.ax2.set_label("right")
- # Make sure background of Axes is displayed
- self.ax2.patch.set_visible(False)
- self.ax.patch.set_visible(True)
-
- # Set axis zorder=0.5 so grid is displayed at 0.5
- self.ax.set_axisbelow(True)
-
- # disable the use of offsets
- try:
- axes = [
- self.ax.get_yaxis().get_major_formatter(),
- self.ax.get_xaxis().get_major_formatter(),
- self.ax2.get_yaxis().get_major_formatter(),
- self.ax2.get_xaxis().get_major_formatter(),
- ]
- for axis in axes:
- axis.set_useOffset(False)
- axis.set_scientific(False)
- except:
- _logger.warning('Cannot disabled axes offsets in %s '
- % matplotlib.__version__)
-
- self.ax2.set_autoscaley_on(True)
-
- # this works but the figure color is left
- if self._matplotlibVersion < _parse_version('2'):
- self.ax.set_axis_bgcolor('none')
- else:
- self.ax.set_facecolor('none')
- self.fig.sca(self.ax)
-
- self._background = None
-
- self._colormaps = {}
-
- self._graphCursor = tuple()
-
- self._enableAxis('right', False)
- self._isXAxisTimeSeries = False
-
- def getItemsFromBackToFront(self, condition=None):
- """Order as BackendBase + take into account matplotlib Axes structure"""
- def axesOrder(item):
- if item.isOverlay():
- return 2
- elif isinstance(item, items.YAxisMixIn) and item.getYAxis() == 'right':
- return 1
- else:
- return 0
-
- return sorted(
- BackendBase.BackendBase.getItemsFromBackToFront(
- self, condition=condition),
- key=axesOrder)
-
- def _overlayItems(self):
- """Generator of backend renderer for overlay items"""
- for item in self._plot.getItems():
- if (item.isOverlay() and
- item.isVisible() and
- item._backendRenderer is not None):
- yield item._backendRenderer
-
- def _hasOverlays(self):
- """Returns whether there is an overlay layer or not.
-
- The overlay layers contains overlay items and the crosshair.
-
- :rtype: bool
- """
- if self._graphCursor:
- return True # There is the crosshair
-
- for item in self._overlayItems():
- return True # There is at least one overlay item
- return False
-
- # Add methods
-
- def _getMarkerFromSymbol(self, symbol):
- """Returns a marker that can be displayed by matplotlib.
-
- :param str symbol: A symbol description used by silx
- :rtype: Union[str,int,matplotlib.path.Path]
- """
- path = get_path_from_symbol(symbol)
- if path is not None:
- return path
- num = _SPECIAL_MARKERS.get(symbol, None)
- if num is not None:
- return num
- # This symbol must be supported by matplotlib
- return symbol
-
- def addCurve(self, x, y,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror,
- fill, alpha, symbolsize, baseline):
- for parameter in (x, y, color, symbol, linewidth, linestyle,
- yaxis, fill, alpha, symbolsize):
- assert parameter is not None
- assert yaxis in ('left', 'right')
-
- if (len(color) == 4 and
- type(color[3]) in [type(1), numpy.uint8, numpy.int8]):
- color = numpy.array(color, dtype=numpy.float64) / 255.
-
- if yaxis == "right":
- axes = self.ax2
- self._enableAxis("right", True)
- else:
- axes = self.ax
-
- pickradius = 3
-
- artists = [] # All the artists composing the curve
-
- # First add errorbars if any so they are behind the curve
- if xerror is not None or yerror is not None:
- if hasattr(color, 'dtype') and len(color) == len(x):
- errorbarColor = 'k'
- else:
- errorbarColor = color
-
- # Nx1 error array deprecated in matplotlib >=3.1 (removed in 3.3)
- if (isinstance(xerror, numpy.ndarray) and xerror.ndim == 2 and
- xerror.shape[1] == 1):
- xerror = numpy.ravel(xerror)
- if (isinstance(yerror, numpy.ndarray) and yerror.ndim == 2 and
- yerror.shape[1] == 1):
- yerror = numpy.ravel(yerror)
-
- errorbars = axes.errorbar(x, y,
- xerr=xerror, yerr=yerror,
- linestyle=' ', color=errorbarColor)
- artists += list(errorbars.get_children())
-
- if hasattr(color, 'dtype') and len(color) == len(x):
- # scatter plot
- if color.dtype not in [numpy.float32, numpy.float64]:
- actualColor = color / 255.
- else:
- actualColor = color
-
- if linestyle not in ["", " ", None]:
- # scatter plot with an actual line ...
- # we need to assign a color ...
- curveList = axes.plot(x, y,
- linestyle=linestyle,
- color=actualColor[0],
- linewidth=linewidth,
- picker=True,
- pickradius=pickradius,
- marker=None)
- artists += list(curveList)
-
- marker = self._getMarkerFromSymbol(symbol)
- scatter = axes.scatter(x, y,
- color=actualColor,
- marker=marker,
- picker=True,
- pickradius=pickradius,
- s=symbolsize**2)
- artists.append(scatter)
-
- if fill:
- if baseline is None:
- _baseline = FLOAT32_MINPOS
- else:
- _baseline = baseline
- artists.append(axes.fill_between(
- x, _baseline, y, facecolor=actualColor[0], linestyle=''))
-
- else: # Curve
- curveList = axes.plot(x, y,
- linestyle=linestyle,
- color=color,
- linewidth=linewidth,
- marker=symbol,
- picker=True,
- pickradius=pickradius,
- markersize=symbolsize)
- artists += list(curveList)
-
- if fill:
- if baseline is None:
- _baseline = FLOAT32_MINPOS
- else:
- _baseline = baseline
- artists.append(
- axes.fill_between(x, _baseline, y, facecolor=color))
-
- for artist in artists:
- if alpha < 1:
- artist.set_alpha(alpha)
-
- return _PickableContainer(artists)
-
- def addImage(self, data, origin, scale, colormap, alpha):
- # Non-uniform image
- # http://wiki.scipy.org/Cookbook/Histograms
- # Non-linear axes
- # http://stackoverflow.com/questions/11488800/non-linear-axes-for-imshow-in-matplotlib
- for parameter in (data, origin, scale):
- assert parameter is not None
-
- origin = float(origin[0]), float(origin[1])
- scale = float(scale[0]), float(scale[1])
- height, width = data.shape[0:2]
-
- # All image are shown as RGBA image
- image = Image(self.ax,
- interpolation='nearest',
- picker=True,
- origin='lower',
- silx_origin=origin,
- silx_scale=scale)
-
- if alpha < 1:
- image.set_alpha(alpha)
-
- # Set image extent
- xmin = origin[0]
- xmax = xmin + scale[0] * width
- if scale[0] < 0.:
- xmin, xmax = xmax, xmin
-
- ymin = origin[1]
- ymax = ymin + scale[1] * height
- if scale[1] < 0.:
- ymin, ymax = ymax, ymin
-
- image.set_extent((xmin, xmax, ymin, ymax))
-
- # Set image data
- if scale[0] < 0. or scale[1] < 0.:
- # For negative scale, step by -1
- xstep = 1 if scale[0] >= 0. else -1
- ystep = 1 if scale[1] >= 0. else -1
- data = data[::ystep, ::xstep]
-
- if data.ndim == 2: # Data image, convert to RGBA image
- data = colormap.applyToData(data)
- elif data.dtype == numpy.uint16:
- # Normalize uint16 data to have a similar behavior as opengl backend
- data = data.astype(numpy.float32)
- data /= 65535
-
- image.set_data(data)
- self.ax.add_artist(image)
- return image
-
- def addTriangles(self, x, y, triangles, color, alpha):
- for parameter in (x, y, triangles, color, alpha):
- assert parameter is not None
-
- color = numpy.array(color, copy=False)
- assert color.ndim == 2 and len(color) == len(x)
-
- if color.dtype not in [numpy.float32, numpy.float64]:
- color = color.astype(numpy.float32) / 255.
-
- collection = TriMesh(
- Triangulation(x, y, triangles),
- alpha=alpha,
- pickradius=0) # 0 enables picking on filled triangle
- collection.set_color(color)
- self.ax.add_collection(collection)
-
- return collection
-
- def addShape(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
- if (linebgcolor is not None and
- shape not in ('rectangle', 'polygon', 'polylines')):
- _logger.warning(
- 'linebgcolor not implemented for %s with matplotlib backend',
- shape)
- xView = numpy.array(x, copy=False)
- yView = numpy.array(y, copy=False)
-
- linestyle = normalize_linestyle(linestyle)
-
- if shape == "line":
- item = self.ax.plot(x, y, color=color,
- linestyle=linestyle, linewidth=linewidth,
- marker=None)[0]
-
- elif shape == "hline":
- if hasattr(y, "__len__"):
- y = y[-1]
- item = self.ax.axhline(y, color=color,
- linestyle=linestyle, linewidth=linewidth)
-
- elif shape == "vline":
- if hasattr(x, "__len__"):
- x = x[-1]
- item = self.ax.axvline(x, color=color,
- linestyle=linestyle, linewidth=linewidth)
-
- elif shape == 'rectangle':
- xMin = numpy.nanmin(xView)
- xMax = numpy.nanmax(xView)
- yMin = numpy.nanmin(yView)
- yMax = numpy.nanmax(yView)
- w = xMax - xMin
- h = yMax - yMin
- item = Rectangle(xy=(xMin, yMin),
- width=w,
- height=h,
- fill=False,
- color=color,
- linestyle=linestyle,
- linewidth=linewidth)
- if fill:
- item.set_hatch('.')
-
- if linestyle != "solid" and linebgcolor is not None:
- item = _DoubleColoredLinePatch(item)
- item.linebgcolor = linebgcolor
-
- self.ax.add_patch(item)
-
- elif shape in ('polygon', 'polylines'):
- points = numpy.array((xView, yView)).T
- if shape == 'polygon':
- closed = True
- else: # shape == 'polylines'
- closed = numpy.all(numpy.equal(points[0], points[-1]))
- item = Polygon(points,
- closed=closed,
- fill=False,
- color=color,
- linestyle=linestyle,
- linewidth=linewidth)
- if fill and shape == 'polygon':
- item.set_hatch('/')
-
- if linestyle != "solid" and linebgcolor is not None:
- item = _DoubleColoredLinePatch(item)
- item.linebgcolor = linebgcolor
-
- self.ax.add_patch(item)
-
- else:
- raise NotImplementedError("Unsupported item shape %s" % shape)
-
- if overlay:
- item.set_animated(True)
-
- return item
-
- def addMarker(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
- textArtist = None
-
- xmin, xmax = self.getGraphXLimits()
- ymin, ymax = self.getGraphYLimits(axis=yaxis)
-
- if yaxis == 'left':
- ax = self.ax
- elif yaxis == 'right':
- ax = self.ax2
- else:
- assert(False)
-
- marker = self._getMarkerFromSymbol(symbol)
- if x is not None and y is not None:
- line = ax.plot(x, y,
- linestyle=" ",
- color=color,
- marker=marker,
- markersize=10.)[-1]
-
- if text is not None:
- textArtist = _TextWithOffset(x, y, text,
- color=color,
- horizontalalignment='left')
- if symbol is not None:
- textArtist.pixel_offset = 10, 3
- elif x is not None:
- line = ax.axvline(x,
- color=color,
- linewidth=linewidth,
- linestyle=linestyle)
- if text is not None:
- # Y position will be updated in updateMarkerText call
- textArtist = _TextWithOffset(x, 1., text,
- color=color,
- horizontalalignment='left',
- verticalalignment='top')
- textArtist.pixel_offset = 5, 3
- elif y is not None:
- line = ax.axhline(y,
- color=color,
- linewidth=linewidth,
- linestyle=linestyle)
-
- if text is not None:
- # X position will be updated in updateMarkerText call
- textArtist = _TextWithOffset(1., y, text,
- color=color,
- horizontalalignment='right',
- verticalalignment='top')
- textArtist.pixel_offset = 5, 3
- else:
- raise RuntimeError('A marker must at least have one coordinate')
-
- line.set_picker(True)
- line.set_pickradius(5)
-
- # All markers are overlays
- line.set_animated(True)
- if textArtist is not None:
- ax.add_artist(textArtist)
- textArtist.set_animated(True)
-
- artists = [line] if textArtist is None else [line, textArtist]
- container = _MarkerContainer(artists, symbol, x, y, yaxis)
- container.updateMarkerText(xmin, xmax, ymin, ymax, self.isYAxisInverted())
-
- return container
-
- def _updateMarkers(self):
- xmin, xmax = self.ax.get_xbound()
- ymin1, ymax1 = self.ax.get_ybound()
- ymin2, ymax2 = self.ax2.get_ybound()
- yinverted = self.isYAxisInverted()
- for item in self._overlayItems():
- if isinstance(item, _MarkerContainer):
- if item.yAxis == 'left':
- item.updateMarkerText(xmin, xmax, ymin1, ymax1, yinverted)
- else:
- item.updateMarkerText(xmin, xmax, ymin2, ymax2, yinverted)
-
- # Remove methods
-
- def remove(self, item):
- try:
- item.remove()
- except ValueError:
- pass # Already removed e.g., in set[X|Y]AxisLogarithmic
-
- # Interaction methods
-
- def setGraphCursor(self, flag, color, linewidth, linestyle):
- if flag:
- lineh = self.ax.axhline(
- self.ax.get_ybound()[0], visible=False, color=color,
- linewidth=linewidth, linestyle=linestyle)
- lineh.set_animated(True)
-
- linev = self.ax.axvline(
- self.ax.get_xbound()[0], visible=False, color=color,
- linewidth=linewidth, linestyle=linestyle)
- linev.set_animated(True)
-
- self._graphCursor = lineh, linev
- else:
- if self._graphCursor:
- lineh, linev = self._graphCursor
- lineh.remove()
- linev.remove()
- self._graphCursor = tuple()
-
- # Active curve
-
- def setCurveColor(self, curve, color):
- # Store Line2D and PathCollection
- for artist in curve.get_children():
- if isinstance(artist, (Line2D, LineCollection)):
- artist.set_color(color)
- elif isinstance(artist, PathCollection):
- artist.set_facecolors(color)
- artist.set_edgecolors(color)
- else:
- _logger.warning(
- 'setActiveCurve ignoring artist %s', str(artist))
-
- # Misc.
-
- def getWidgetHandle(self):
- return self.fig.canvas
-
- def _enableAxis(self, axis, flag=True):
- """Show/hide Y axis
-
- :param str axis: Axis name: 'left' or 'right'
- :param bool flag: Default, True
- """
- assert axis in ('right', 'left')
- axes = self.ax2 if axis == 'right' else self.ax
- axes.get_yaxis().set_visible(flag)
-
- def replot(self):
- """Do not perform rendering.
-
- Override in subclass to actually draw something.
- """
- # TODO images, markers? scatter plot? move in remove?
- # Right Y axis only support curve for now
- # Hide right Y axis if no line is present
- self._dirtyLimits = False
- if not self.ax2.lines:
- self._enableAxis('right', False)
-
- def _drawOverlays(self):
- """Draw overlays if any."""
- def condition(item):
- return (item.isVisible() and
- item._backendRenderer is not None and
- item.isOverlay())
-
- for item in self.getItemsFromBackToFront(condition=condition):
- if (isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right'):
- axes = self.ax2
- else:
- axes = self.ax
- axes.draw_artist(item._backendRenderer)
-
- for item in self._graphCursor:
- self.ax.draw_artist(item)
-
- def updateZOrder(self):
- """Reorder all items with z order from 0 to 1"""
- items = self.getItemsFromBackToFront(
- lambda item: item.isVisible() and item._backendRenderer is not None)
- count = len(items)
- for index, item in enumerate(items):
- if item.getZValue() < 0.5:
- # Make sure matplotlib z order is below the grid (with z=0.5)
- zorder = 0.5 * index / count
- else: # Make sure matplotlib z order is above the grid (> 0.5)
- zorder = 1. + index / count
- if zorder != item._backendRenderer.get_zorder():
- item._backendRenderer.set_zorder(zorder)
-
- def saveGraph(self, fileName, fileFormat, dpi):
- self.updateZOrder()
-
- # fileName can be also a StringIO or file instance
- if dpi is not None:
- self.fig.savefig(fileName, format=fileFormat, dpi=dpi)
- else:
- self.fig.savefig(fileName, format=fileFormat)
- self._plot._setDirtyPlot()
-
- # Graph labels
-
- def setGraphTitle(self, title):
- self.ax.set_title(title)
-
- def setGraphXLabel(self, label):
- self.ax.set_xlabel(label)
-
- def setGraphYLabel(self, label, axis):
- axes = self.ax if axis == 'left' else self.ax2
- axes.set_ylabel(label)
-
- # Graph limits
-
- def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
- # Let matplotlib taking care of keep aspect ratio if any
- self._dirtyLimits = True
- self.ax.set_xlim(min(xmin, xmax), max(xmin, xmax))
-
- if y2min is not None and y2max is not None:
- if not self.isYAxisInverted():
- self.ax2.set_ylim(min(y2min, y2max), max(y2min, y2max))
- else:
- self.ax2.set_ylim(max(y2min, y2max), min(y2min, y2max))
-
- if not self.isYAxisInverted():
- self.ax.set_ylim(min(ymin, ymax), max(ymin, ymax))
- else:
- self.ax.set_ylim(max(ymin, ymax), min(ymin, ymax))
-
- self._updateMarkers()
-
- def getGraphXLimits(self):
- if self._dirtyLimits and self.isKeepDataAspectRatio():
- self.ax.apply_aspect()
- self.ax2.apply_aspect()
- self._dirtyLimits = False
- return self.ax.get_xbound()
-
- def setGraphXLimits(self, xmin, xmax):
- self._dirtyLimits = True
- self.ax.set_xlim(min(xmin, xmax), max(xmin, xmax))
- self._updateMarkers()
-
- def getGraphYLimits(self, axis):
- assert axis in ('left', 'right')
- ax = self.ax2 if axis == 'right' else self.ax
-
- if not ax.get_visible():
- return None
-
- if self._dirtyLimits and self.isKeepDataAspectRatio():
- self.ax.apply_aspect()
- self.ax2.apply_aspect()
- self._dirtyLimits = False
-
- return ax.get_ybound()
-
- def setGraphYLimits(self, ymin, ymax, axis):
- ax = self.ax2 if axis == 'right' else self.ax
- if ymax < ymin:
- ymin, ymax = ymax, ymin
- self._dirtyLimits = True
-
- if self.isKeepDataAspectRatio():
- # matplotlib keeps limits of shared axis when keeping aspect ratio
- # So x limits are kept when changing y limits....
- # Change x limits first by taking into account aspect ratio
- # and then change y limits.. so matplotlib does not need
- # to make change (to y) to keep aspect ratio
- xmin, xmax = ax.get_xbound()
- curYMin, curYMax = ax.get_ybound()
-
- newXRange = (xmax - xmin) * (ymax - ymin) / (curYMax - curYMin)
- xcenter = 0.5 * (xmin + xmax)
- ax.set_xlim(xcenter - 0.5 * newXRange, xcenter + 0.5 * newXRange)
-
- if not self.isYAxisInverted():
- ax.set_ylim(ymin, ymax)
- else:
- ax.set_ylim(ymax, ymin)
-
- self._updateMarkers()
-
- # Graph axes
-
- def setXAxisTimeZone(self, tz):
- super(BackendMatplotlib, self).setXAxisTimeZone(tz)
-
- # Make new formatter and locator with the time zone.
- self.setXAxisTimeSeries(self.isXAxisTimeSeries())
-
- def isXAxisTimeSeries(self):
- return self._isXAxisTimeSeries
-
- def setXAxisTimeSeries(self, isTimeSeries):
- self._isXAxisTimeSeries = isTimeSeries
- if self._isXAxisTimeSeries:
- # We can't use a matplotlib.dates.DateFormatter because it expects
- # the data to be in datetimes. Silx works internally with
- # timestamps (floats).
- locator = NiceDateLocator(tz=self.getXAxisTimeZone())
- self.ax.xaxis.set_major_locator(locator)
- self.ax.xaxis.set_major_formatter(
- NiceAutoDateFormatter(locator, tz=self.getXAxisTimeZone()))
- else:
- try:
- scalarFormatter = ScalarFormatter(useOffset=False)
- except:
- _logger.warning('Cannot disabled axes offsets in %s ' %
- matplotlib.__version__)
- scalarFormatter = ScalarFormatter()
- self.ax.xaxis.set_major_formatter(scalarFormatter)
-
- def setXAxisLogarithmic(self, flag):
- # Workaround for matplotlib 2.1.0 when one tries to set an axis
- # to log scale with both limits <= 0
- # In this case a draw with positive limits is needed first
- if flag and self._matplotlibVersion >= _parse_version('2.1.0'):
- xlim = self.ax.get_xlim()
- if xlim[0] <= 0 and xlim[1] <= 0:
- self.ax.set_xlim(1, 10)
- self.draw()
-
- self.ax2.set_xscale('log' if flag else 'linear')
- self.ax.set_xscale('log' if flag else 'linear')
-
- def setYAxisLogarithmic(self, flag):
- # Workaround for matplotlib 2.0 issue with negative bounds
- # before switching to log scale
- if flag and self._matplotlibVersion >= _parse_version('2.0.0'):
- redraw = False
- for axis, dataRangeIndex in ((self.ax, 1), (self.ax2, 2)):
- ylim = axis.get_ylim()
- if ylim[0] <= 0 or ylim[1] <= 0:
- dataRange = self._plot.getDataRange()[dataRangeIndex]
- if dataRange is None:
- dataRange = 1, 100 # Fallback
- axis.set_ylim(*dataRange)
- redraw = True
- if redraw:
- self.draw()
-
- self.ax2.set_yscale('log' if flag else 'linear')
- self.ax.set_yscale('log' if flag else 'linear')
-
- def setYAxisInverted(self, flag):
- if self.ax.yaxis_inverted() != bool(flag):
- self.ax.invert_yaxis()
- self._updateMarkers()
-
- def isYAxisInverted(self):
- return self.ax.yaxis_inverted()
-
- def isKeepDataAspectRatio(self):
- return self.ax.get_aspect() in (1.0, 'equal')
-
- def setKeepDataAspectRatio(self, flag):
- self.ax.set_aspect(1.0 if flag else 'auto')
- self.ax2.set_aspect(1.0 if flag else 'auto')
-
- def setGraphGrid(self, which):
- self.ax.grid(False, which='both') # Disable all grid first
- if which is not None:
- self.ax.grid(True, which=which)
-
- # Data <-> Pixel coordinates conversion
-
- def _getDevicePixelRatio(self) -> float:
- """Compatibility wrapper for devicePixelRatioF"""
- return 1.
-
- def _mplToQtPosition(self, x: float, y: float) -> Tuple[float, float]:
- """Convert matplotlib "display" space coord to Qt widget logical pixel
- """
- ratio = self._getDevicePixelRatio()
- # Convert from matplotlib origin (bottom) to Qt origin (top)
- # and apply device pixel ratio
- return x / ratio, (self.fig.get_window_extent().height - y) / ratio
-
- def _qtToMplPosition(self, x: float, y: float) -> Tuple[float, float]:
- """Convert Qt widget logical pixel to matplotlib "display" space coord
- """
- ratio = self._getDevicePixelRatio()
- # Apply device pixel ration and
- # convert from Qt origin (top) to matplotlib origin (bottom)
- return x * ratio, self.fig.get_window_extent().height - (y * ratio)
-
- def dataToPixel(self, x, y, axis):
- ax = self.ax2 if axis == "right" else self.ax
- displayPos = ax.transData.transform_point((x, y)).transpose()
- return self._mplToQtPosition(*displayPos)
-
- def pixelToData(self, x, y, axis):
- ax = self.ax2 if axis == "right" else self.ax
- displayPos = self._qtToMplPosition(x, y)
- return tuple(ax.transData.inverted().transform_point(displayPos))
-
- def getPlotBoundsInPixels(self):
- bbox = self.ax.get_window_extent()
- # Warning this is not returning int...
- ratio = self._getDevicePixelRatio()
- return tuple(int(value / ratio) for value in (
- bbox.xmin,
- self.fig.get_window_extent().height - bbox.ymax,
- bbox.width,
- bbox.height))
-
- def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
- width, height = 1. - left - right, 1. - top - bottom
- position = left, bottom, width, height
-
- # Toggle display of axes and viewbox rect
- isFrameOn = position != (0., 0., 1., 1.)
- self.ax.set_frame_on(isFrameOn)
- self.ax2.set_frame_on(isFrameOn)
-
- self.ax.set_position(position)
- self.ax2.set_position(position)
-
- self._synchronizeBackgroundColors()
- self._synchronizeForegroundColors()
- self._plot._setDirtyPlot()
-
- def _synchronizeBackgroundColors(self):
- backgroundColor = self._plot.getBackgroundColor().getRgbF()
-
- dataBackgroundColor = self._plot.getDataBackgroundColor()
- if dataBackgroundColor.isValid():
- dataBackgroundColor = dataBackgroundColor.getRgbF()
- else:
- dataBackgroundColor = backgroundColor
-
- if self.ax.get_frame_on():
- self.fig.patch.set_facecolor(backgroundColor)
- if self._matplotlibVersion < _parse_version('2'):
- self.ax.set_axis_bgcolor(dataBackgroundColor)
- else:
- self.ax.set_facecolor(dataBackgroundColor)
- else:
- self.fig.patch.set_facecolor(dataBackgroundColor)
-
- def _synchronizeForegroundColors(self):
- foregroundColor = self._plot.getForegroundColor().getRgbF()
-
- gridColor = self._plot.getGridColor()
- if gridColor.isValid():
- gridColor = gridColor.getRgbF()
- else:
- gridColor = foregroundColor
-
- for axes in (self.ax, self.ax2):
- if axes.get_frame_on():
- axes.spines['bottom'].set_color(foregroundColor)
- axes.spines['top'].set_color(foregroundColor)
- axes.spines['right'].set_color(foregroundColor)
- axes.spines['left'].set_color(foregroundColor)
- axes.tick_params(axis='x', colors=foregroundColor)
- axes.tick_params(axis='y', colors=foregroundColor)
- axes.yaxis.label.set_color(foregroundColor)
- axes.xaxis.label.set_color(foregroundColor)
- axes.title.set_color(foregroundColor)
-
- for line in axes.get_xgridlines():
- line.set_color(gridColor)
-
- for line in axes.get_ygridlines():
- line.set_color(gridColor)
- # axes.grid().set_markeredgecolor(gridColor)
-
- def setBackgroundColors(self, backgroundColor, dataBackgroundColor):
- self._synchronizeBackgroundColors()
-
- def setForegroundColors(self, foregroundColor, gridColor):
- self._synchronizeForegroundColors()
-
-
-class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib):
- """QWidget matplotlib backend using a QtAgg canvas.
-
- It adds fast overlay drawing and mouse event management.
- """
-
- _sigPostRedisplay = qt.Signal()
- """Signal handling automatic asynchronous replot"""
-
- def __init__(self, plot, parent=None):
- BackendMatplotlib.__init__(self, plot, parent)
- FigureCanvasQTAgg.__init__(self, self.fig)
- self.setParent(parent)
-
- self._limitsBeforeResize = None
-
- FigureCanvasQTAgg.setSizePolicy(
- self, qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding)
- FigureCanvasQTAgg.updateGeometry(self)
-
- # Make postRedisplay asynchronous using Qt signal
- self._sigPostRedisplay.connect(
- super(BackendMatplotlibQt, self).postRedisplay,
- qt.Qt.QueuedConnection)
-
- self._picked = None
-
- self.mpl_connect('button_press_event', self._onMousePress)
- self.mpl_connect('button_release_event', self._onMouseRelease)
- self.mpl_connect('motion_notify_event', self._onMouseMove)
- self.mpl_connect('scroll_event', self._onMouseWheel)
-
- def postRedisplay(self):
- self._sigPostRedisplay.emit()
-
- def _getDevicePixelRatio(self) -> float:
- """Compatibility wrapper for devicePixelRatioF"""
- if hasattr(self, 'devicePixelRatioF'):
- ratio = self.devicePixelRatioF()
- else: # Qt < 5.6 compatibility
- ratio = float(self.devicePixelRatio())
- # Safety net: avoid returning 0
- return ratio if ratio != 0. else 1.
-
- # Mouse event forwarding
-
- _MPL_TO_PLOT_BUTTONS = {1: 'left', 2: 'middle', 3: 'right'}
-
- def _onMousePress(self, event):
- button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None)
- if button is not None:
- x, y = self._mplToQtPosition(event.x, event.y)
- self._plot.onMousePress(int(x), int(y), button)
-
- def _onMouseMove(self, event):
- x, y = self._mplToQtPosition(event.x, event.y)
- if self._graphCursor:
- position = self._plot.pixelToData(
- x, y, axis='left', check=True)
- lineh, linev = self._graphCursor
- if position is not None:
- linev.set_visible(True)
- linev.set_xdata((position[0], position[0]))
- lineh.set_visible(True)
- lineh.set_ydata((position[1], position[1]))
- self._plot._setDirtyPlot(overlayOnly=True)
- elif lineh.get_visible():
- lineh.set_visible(False)
- linev.set_visible(False)
- self._plot._setDirtyPlot(overlayOnly=True)
- # onMouseMove must trigger replot if dirty flag is raised
-
- self._plot.onMouseMove(int(x), int(y))
-
- def _onMouseRelease(self, event):
- button = self._MPL_TO_PLOT_BUTTONS.get(event.button, None)
- if button is not None:
- x, y = self._mplToQtPosition(event.x, event.y)
- self._plot.onMouseRelease(int(x), int(y), button)
-
- def _onMouseWheel(self, event):
- x, y = self._mplToQtPosition(event.x, event.y)
- self._plot.onMouseWheel(int(x), int(y), event.step)
-
- def leaveEvent(self, event):
- """QWidget event handler"""
- try:
- plot = self._plot
- except RuntimeError:
- pass
- else:
- plot.onMouseLeaveWidget()
-
- # picking
-
- def pickItem(self, x, y, item):
- xDisplay, yDisplay = self._qtToMplPosition(x, y)
- mouseEvent = MouseEvent(
- 'button_press_event', self, int(xDisplay), int(yDisplay))
- # Override axes and data position with the axes
- mouseEvent.inaxes = item.axes
- mouseEvent.xdata, mouseEvent.ydata = self.pixelToData(
- x, y, axis='left' if item.axes is self.ax else 'right')
- picked, info = item.contains(mouseEvent)
-
- if not picked:
- return None
-
- elif isinstance(item, TriMesh):
- # Convert selected triangle to data point indices
- triangulation = item._triangulation
- indices = triangulation.get_masked_triangles()[info['ind'][0]]
-
- # Sort picked triangle points by distance to mouse
- # from furthest to closest to put closest point last
- # This is to be somewhat consistent with last scatter point
- # being the top one.
- xdata, ydata = self.pixelToData(x, y, axis='left')
- dists = ((triangulation.x[indices] - xdata) ** 2 +
- (triangulation.y[indices] - ydata) ** 2)
- return indices[numpy.flip(numpy.argsort(dists), axis=0)]
-
- else: # Returns indices if any
- return info.get('ind', ())
-
- # replot control
-
- def resizeEvent(self, event):
- # Store current limits
- self._limitsBeforeResize = (
- self.ax.get_xbound(), self.ax.get_ybound(), self.ax2.get_ybound())
-
- FigureCanvasQTAgg.resizeEvent(self, event)
- if self.isKeepDataAspectRatio() or self._hasOverlays():
- # This is needed with matplotlib 1.5.x and 2.0.x
- self._plot._setDirtyPlot()
-
- def draw(self):
- """Overload draw
-
- It performs a full redraw (including overlays) of the plot.
- It also resets background and emit limits changed signal.
-
- This is directly called by matplotlib for widget resize.
- """
- self.updateZOrder()
-
- # Starting with mpl 2.1.0, toggling autoscale raises a ValueError
- # in some situations. See #1081, #1136, #1163,
- if self._matplotlibVersion >= _parse_version("2.0.0"):
- try:
- FigureCanvasQTAgg.draw(self)
- except ValueError as err:
- _logger.debug(
- "ValueError caught while calling FigureCanvasQTAgg.draw: "
- "'%s'", err)
- else:
- FigureCanvasQTAgg.draw(self)
-
- if self._hasOverlays():
- # Save background
- self._background = self.copy_from_bbox(self.fig.bbox)
- else:
- self._background = None # Reset background
-
- # Check if limits changed due to a resize of the widget
- if self._limitsBeforeResize is not None:
- xLimits, yLimits, yRightLimits = self._limitsBeforeResize
- self._limitsBeforeResize = None
-
- if (xLimits != self.ax.get_xbound() or
- yLimits != self.ax.get_ybound()):
- self._updateMarkers()
-
- if xLimits != self.ax.get_xbound():
- self._plot.getXAxis()._emitLimitsChanged()
- if yLimits != self.ax.get_ybound():
- self._plot.getYAxis(axis='left')._emitLimitsChanged()
- if yRightLimits != self.ax2.get_ybound():
- self._plot.getYAxis(axis='right')._emitLimitsChanged()
-
- self._drawOverlays()
-
- def replot(self):
- BackendMatplotlib.replot(self)
-
- dirtyFlag = self._plot._getDirtyPlot()
-
- if dirtyFlag == 'overlay':
- # Only redraw overlays using fast rendering path
- if self._background is None:
- self._background = self.copy_from_bbox(self.fig.bbox)
- self.restore_region(self._background)
- self._drawOverlays()
- self.blit(self.fig.bbox)
-
- elif dirtyFlag: # Need full redraw
- self.draw()
-
- # Workaround issue of rendering overlays with some matplotlib versions
- if (_parse_version('1.5') <= self._matplotlibVersion < _parse_version('2.1') and
- not hasattr(self, '_firstReplot')):
- self._firstReplot = False
- if self._hasOverlays():
- qt.QTimer.singleShot(0, self.draw) # Request async draw
-
- # cursor
-
- _QT_CURSORS = {
- BackendBase.CURSOR_DEFAULT: qt.Qt.ArrowCursor,
- BackendBase.CURSOR_POINTING: qt.Qt.PointingHandCursor,
- BackendBase.CURSOR_SIZE_HOR: qt.Qt.SizeHorCursor,
- BackendBase.CURSOR_SIZE_VER: qt.Qt.SizeVerCursor,
- BackendBase.CURSOR_SIZE_ALL: qt.Qt.SizeAllCursor,
- }
-
- def setGraphCursorShape(self, cursor):
- if cursor is None:
- FigureCanvasQTAgg.unsetCursor(self)
- else:
- cursor = self._QT_CURSORS[cursor]
- FigureCanvasQTAgg.setCursor(self, qt.QCursor(cursor))
diff --git a/silx/gui/plot/backends/BackendOpenGL.py b/silx/gui/plot/backends/BackendOpenGL.py
deleted file mode 100755
index 6fde9df..0000000
--- a/silx/gui/plot/backends/BackendOpenGL.py
+++ /dev/null
@@ -1,1420 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2021 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.
-#
-# ############################################################################*/
-"""OpenGL Plot backend."""
-
-from __future__ import division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "21/12/2018"
-
-import logging
-import weakref
-
-import numpy
-
-from .. import items
-from .._utils import FLOAT32_MINPOS
-from . import BackendBase
-from ... import colors
-from ... import qt
-
-from ..._glutils import gl
-from ... import _glutils as glu
-from . import glutils
-from .glutils.PlotImageFile import saveImageToFile
-
-_logger = logging.getLogger(__name__)
-
-
-# TODO idea: BackendQtMixIn class to share code between mpl and gl
-# TODO check if OpenGL is available
-# TODO make an off-screen mesa backend
-
-# Content #####################################################################
-
-class _ShapeItem(dict):
- def __init__(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
- super(_ShapeItem, self).__init__()
-
- if shape not in ('polygon', 'rectangle', 'line',
- 'vline', 'hline', 'polylines'):
- raise NotImplementedError("Unsupported shape {0}".format(shape))
-
- x = numpy.array(x, copy=False)
- y = numpy.array(y, copy=False)
-
- if shape == 'rectangle':
- xMin, xMax = x
- x = numpy.array((xMin, xMin, xMax, xMax))
- yMin, yMax = y
- y = numpy.array((yMin, yMax, yMax, yMin))
-
- # Ignore fill for polylines to mimic matplotlib
- fill = fill if shape != 'polylines' else False
-
- self.update({
- 'shape': shape,
- 'color': colors.rgba(color),
- 'fill': 'hatch' if fill else None,
- 'x': x,
- 'y': y,
- 'linestyle': linestyle,
- 'linewidth': linewidth,
- 'linebgcolor': linebgcolor,
- })
-
-
-class _MarkerItem(dict):
- def __init__(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
- super(_MarkerItem, self).__init__()
-
- if symbol is None:
- symbol = '+'
-
- # Apply constraint to provided position
- isConstraint = (constraint is not None and
- x is not None and y is not None)
- if isConstraint:
- x, y = constraint(x, y)
-
- self.update({
- 'x': x,
- 'y': y,
- 'text': text,
- 'color': colors.rgba(color),
- 'constraint': constraint if isConstraint else None,
- 'symbol': symbol,
- 'linestyle': linestyle,
- 'linewidth': linewidth,
- 'yaxis': yaxis,
- })
-
-
-# shaders #####################################################################
-
-_baseVertShd = """
- attribute vec2 position;
- uniform mat4 matrix;
- uniform bvec2 isLog;
-
- const float oneOverLog10 = 0.43429448190325176;
-
- void main(void) {
- vec2 posTransformed = position;
- if (isLog.x) {
- posTransformed.x = oneOverLog10 * log(position.x);
- }
- if (isLog.y) {
- posTransformed.y = oneOverLog10 * log(position.y);
- }
- gl_Position = matrix * vec4(posTransformed, 0.0, 1.0);
- }
- """
-
-_baseFragShd = """
- uniform vec4 color;
- uniform int hatchStep;
- uniform float tickLen;
-
- void main(void) {
- if (tickLen != 0.) {
- if (mod((gl_FragCoord.x + gl_FragCoord.y) / tickLen, 2.) < 1.) {
- gl_FragColor = color;
- } else {
- discard;
- }
- } else if (hatchStep == 0 ||
- mod(gl_FragCoord.x - gl_FragCoord.y, float(hatchStep)) == 0.) {
- gl_FragColor = color;
- } else {
- discard;
- }
- }
- """
-
-_texVertShd = """
- attribute vec2 position;
- attribute vec2 texCoords;
- uniform mat4 matrix;
-
- varying vec2 coords;
-
- void main(void) {
- gl_Position = matrix * vec4(position, 0.0, 1.0);
- coords = texCoords;
- }
- """
-
-_texFragShd = """
- uniform sampler2D tex;
-
- varying vec2 coords;
-
- void main(void) {
- gl_FragColor = texture2D(tex, coords);
- gl_FragColor.a = 1.0;
- }
- """
-
-# BackendOpenGL ###############################################################
-
-
-class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget):
- """OpenGL-based Plot backend.
-
- WARNINGS:
- Unless stated otherwise, this API is NOT thread-safe and MUST be
- called from the main thread.
- When numpy arrays are passed as arguments to the API (through
- :func:`addCurve` and :func:`addImage`), they are copied only if
- required.
- So, the caller should not modify these arrays afterwards.
- """
-
- _sigPostRedisplay = qt.Signal()
- """Signal handling automatic asynchronous replot"""
-
- def __init__(self, plot, parent=None, f=qt.Qt.WindowFlags()):
- glu.OpenGLWidget.__init__(self, parent,
- alphaBufferSize=8,
- depthBufferSize=0,
- stencilBufferSize=0,
- version=(2, 1),
- f=f)
- BackendBase.BackendBase.__init__(self, plot, parent)
-
- self._backgroundColor = 1., 1., 1., 1.
- self._dataBackgroundColor = 1., 1., 1., 1.
-
- self.matScreenProj = glutils.mat4Identity()
-
- self._progBase = glu.Program(
- _baseVertShd, _baseFragShd, attrib0='position')
- self._progTex = glu.Program(
- _texVertShd, _texFragShd, attrib0='position')
- self._plotFBOs = weakref.WeakKeyDictionary()
-
- self._keepDataAspectRatio = False
-
- self._crosshairCursor = None
- self._mousePosInPixels = None
-
- self._glGarbageCollector = []
-
- self._plotFrame = glutils.GLPlotFrame2D(
- foregroundColor=(0., 0., 0., 1.),
- gridColor=(.7, .7, .7, 1.),
- marginRatios=(.15, .1, .1, .15))
- self._plotFrame.size = ( # Init size with size int
- int(self.getDevicePixelRatio() * 640),
- int(self.getDevicePixelRatio() * 480))
-
- # Make postRedisplay asynchronous using Qt signal
- self._sigPostRedisplay.connect(
- super(BackendOpenGL, self).postRedisplay,
- qt.Qt.QueuedConnection)
-
- self.setAutoFillBackground(False)
- self.setMouseTracking(True)
-
- # QWidget
-
- _MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'}
-
- def sizeHint(self):
- return qt.QSize(8 * 80, 6 * 80) # Mimic MatplotlibBackend
-
- def mousePressEvent(self, event):
- if event.button() not in self._MOUSE_BTNS:
- return super(BackendOpenGL, self).mousePressEvent(event)
- self._plot.onMousePress(
- event.x(), event.y(), self._MOUSE_BTNS[event.button()])
- event.accept()
-
- def mouseMoveEvent(self, event):
- qtPos = event.x(), event.y()
-
- previousMousePosInPixels = self._mousePosInPixels
- if qtPos == self._mouseInPlotArea(*qtPos):
- devicePixelRatio = self.getDevicePixelRatio()
- devicePos = qtPos[0] * devicePixelRatio, qtPos[1] * devicePixelRatio
- self._mousePosInPixels = devicePos # Mouse in plot area
- else:
- self._mousePosInPixels = None # Mouse outside plot area
-
- if (self._crosshairCursor is not None and
- previousMousePosInPixels != self._mousePosInPixels):
- # Avoid replot when cursor remains outside plot area
- self._plot._setDirtyPlot(overlayOnly=True)
-
- self._plot.onMouseMove(*qtPos)
- event.accept()
-
- def mouseReleaseEvent(self, event):
- if event.button() not in self._MOUSE_BTNS:
- return super(BackendOpenGL, self).mouseReleaseEvent(event)
- self._plot.onMouseRelease(
- event.x(), event.y(), self._MOUSE_BTNS[event.button()])
- event.accept()
-
- def wheelEvent(self, event):
- if hasattr(event, 'angleDelta'): # Qt 5
- delta = event.angleDelta().y()
- else: # Qt 4 support
- delta = event.delta()
- angleInDegrees = delta / 8.
- self._plot.onMouseWheel(event.x(), event.y(), angleInDegrees)
- event.accept()
-
- def leaveEvent(self, _):
- self._plot.onMouseLeaveWidget()
-
- # OpenGLWidget API
-
- def initializeGL(self):
- gl.testGL()
-
- gl.glClearStencil(0)
-
- gl.glEnable(gl.GL_BLEND)
- # gl.glBlendFunc(gl.GL_SRC_ALPHA, gl.GL_ONE_MINUS_SRC_ALPHA)
- gl.glBlendFuncSeparate(gl.GL_SRC_ALPHA,
- gl.GL_ONE_MINUS_SRC_ALPHA,
- gl.GL_ONE,
- gl.GL_ONE)
-
- # For lines
- gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
-
- # For points
- gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
- gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
- # gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
-
- def _paintDirectGL(self):
- self._renderPlotAreaGL()
- self._plotFrame.render()
- self._renderOverlayGL()
-
- def _paintFBOGL(self):
- context = glu.Context.getCurrent()
- plotFBOTex = self._plotFBOs.get(context)
- if (self._plot._getDirtyPlot() or self._plotFrame.isDirty or
- plotFBOTex is None):
- self._plotVertices = (
- # Vertex coordinates
- numpy.array(((-1., -1.), (1., -1.), (-1., 1.), (1., 1.)),
- dtype=numpy.float32),
- # Texture coordinates
- numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
- dtype=numpy.float32))
- if plotFBOTex is None or \
- plotFBOTex.shape[1] != self._plotFrame.size[0] or \
- plotFBOTex.shape[0] != self._plotFrame.size[1]:
- if plotFBOTex is not None:
- plotFBOTex.discard()
- plotFBOTex = glu.FramebufferTexture(
- gl.GL_RGBA,
- shape=(self._plotFrame.size[1],
- self._plotFrame.size[0]),
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=(gl.GL_CLAMP_TO_EDGE,
- gl.GL_CLAMP_TO_EDGE))
- self._plotFBOs[context] = plotFBOTex
-
- with plotFBOTex:
- gl.glClearColor(*self._backgroundColor)
- gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT)
- self._renderPlotAreaGL()
- self._plotFrame.render()
-
- # Render plot in screen coords
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
-
- self._progTex.use()
- texUnit = 0
-
- gl.glUniform1i(self._progTex.uniforms['tex'], texUnit)
- gl.glUniformMatrix4fv(self._progTex.uniforms['matrix'], 1, gl.GL_TRUE,
- glutils.mat4Identity().astype(numpy.float32))
-
- gl.glEnableVertexAttribArray(self._progTex.attributes['position'])
- gl.glVertexAttribPointer(self._progTex.attributes['position'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._plotVertices[0])
-
- gl.glEnableVertexAttribArray(self._progTex.attributes['texCoords'])
- gl.glVertexAttribPointer(self._progTex.attributes['texCoords'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._plotVertices[1])
-
- with plotFBOTex.texture:
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices[0]))
-
- self._renderOverlayGL()
-
- def paintGL(self):
- with glu.Context.current(self.context()):
- # Release OpenGL resources
- for item in self._glGarbageCollector:
- item.discard()
- self._glGarbageCollector = []
-
- gl.glClearColor(*self._backgroundColor)
- gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT)
-
- # Check if window is large enough
- if self._plotFrame.plotSize <= (2, 2):
- return
-
- # Sync plot frame with window
- self._plotFrame.devicePixelRatio = self.getDevicePixelRatio()
- # self._paintDirectGL()
- self._paintFBOGL()
-
- def _renderItems(self, overlay=False):
- """Render items according to :class:`PlotWidget` order
-
- Note: Scissor test should already be set.
-
- :param bool overlay:
- False (the default) to render item that are not overlays.
- True to render items that are overlays.
- """
- # Values that are often used
- plotWidth, plotHeight = self._plotFrame.plotSize
- isXLog = self._plotFrame.xAxis.isLog
- isYLog = self._plotFrame.yAxis.isLog
- isYInverted = self._plotFrame.isYAxisInverted
-
- # Used by marker rendering
- labels = []
- pixelOffset = 3
-
- context = glutils.RenderContext(
- isXLog=isXLog, isYLog=isYLog, dpi=self.getDotsPerInch())
-
- for plotItem in self.getItemsFromBackToFront(
- condition=lambda i: i.isVisible() and i.isOverlay() == overlay):
- if plotItem._backendRenderer is None:
- continue
-
- item = plotItem._backendRenderer
-
- if isinstance(item, glutils.GLPlotItem): # Render data items
- gl.glViewport(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
- # Set matrix
- if item.yaxis == 'right':
- context.matrix = self._plotFrame.transformedDataY2ProjMat
- else:
- context.matrix = self._plotFrame.transformedDataProjMat
- item.render(context)
-
- elif isinstance(item, _ShapeItem): # Render shape items
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
-
- if ((isXLog and numpy.min(item['x']) < FLOAT32_MINPOS) or
- (isYLog and numpy.min(item['y']) < FLOAT32_MINPOS)):
- # Ignore items <= 0. on log axes
- continue
-
- if item['shape'] == 'hline':
- width = self._plotFrame.size[0]
- _, yPixel = self._plotFrame.dataToPixel(
- 0.5 * sum(self._plotFrame.dataRanges[0]),
- item['y'],
- axis='left')
- subShapes = [numpy.array(((0., yPixel), (width, yPixel)),
- dtype=numpy.float32)]
-
- elif item['shape'] == 'vline':
- xPixel, _ = self._plotFrame.dataToPixel(
- item['x'],
- 0.5 * sum(self._plotFrame.dataRanges[1]),
- axis='left')
- height = self._plotFrame.size[1]
- subShapes = [numpy.array(((xPixel, 0), (xPixel, height)),
- dtype=numpy.float32)]
-
- else:
- # Split sub-shapes at not finite values
- splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
- numpy.isfinite(item['x']), numpy.isfinite(item['y']))))[0]
- splits = numpy.concatenate(([-1], splits, [len(item['x'])]))
- subShapes = []
- for begin, end in zip(splits[:-1] + 1, splits[1:]):
- if end > begin:
- subShapes.append(numpy.array([
- self._plotFrame.dataToPixel(x, y, axis='left')
- for (x, y) in zip(item['x'][begin:end], item['y'][begin:end])]))
-
- for points in subShapes: # Draw each sub-shape
- # Draw the fill
- if (item['fill'] is not None and
- item['shape'] not in ('hline', 'vline')):
- self._progBase.use()
- gl.glUniformMatrix4fv(
- self._progBase.uniforms['matrix'], 1, gl.GL_TRUE,
- self.matScreenProj.astype(numpy.float32))
- gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
- gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
-
- shape2D = glutils.FilledShape2D(
- points, style=item['fill'], color=item['color'])
- shape2D.render(
- posAttrib=self._progBase.attributes['position'],
- colorUnif=self._progBase.uniforms['color'],
- hatchStepUnif=self._progBase.uniforms['hatchStep'])
-
- # Draw the stroke
- if item['linestyle'] not in ('', ' ', None):
- if item['shape'] != 'polylines':
- # close the polyline
- points = numpy.append(points,
- numpy.atleast_2d(points[0]), axis=0)
-
- lines = glutils.GLLines2D(
- points[:, 0], points[:, 1],
- style=item['linestyle'],
- color=item['color'],
- dash2ndColor=item['linebgcolor'],
- width=item['linewidth'])
- context.matrix = self.matScreenProj
- lines.render(context)
-
- elif isinstance(item, _MarkerItem):
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
-
- xCoord, yCoord, yAxis = item['x'], item['y'], item['yaxis']
-
- if ((isXLog and xCoord is not None and xCoord <= 0) or
- (isYLog and yCoord is not None and yCoord <= 0)):
- # Do not render markers with negative coords on log axis
- continue
-
- if xCoord is None or yCoord is None:
- if xCoord is None: # Horizontal line in data space
- pixelPos = self._plotFrame.dataToPixel(
- 0.5 * sum(self._plotFrame.dataRanges[0]),
- yCoord,
- axis=yAxis)
-
- if item['text'] is not None:
- x = self._plotFrame.size[0] - \
- self._plotFrame.margins.right - pixelOffset
- y = pixelPos[1] - pixelOffset
- label = glutils.Text2D(
- item['text'], x, y,
- color=item['color'],
- bgColor=(1., 1., 1., 0.5),
- align=glutils.RIGHT,
- valign=glutils.BOTTOM,
- devicePixelRatio=self.getDevicePixelRatio())
- labels.append(label)
-
- width = self._plotFrame.size[0]
- lines = glutils.GLLines2D(
- (0, width), (pixelPos[1], pixelPos[1]),
- style=item['linestyle'],
- color=item['color'],
- width=item['linewidth'])
- context.matrix = self.matScreenProj
- lines.render(context)
-
- else: # yCoord is None: vertical line in data space
- yRange = self._plotFrame.dataRanges[1 if yAxis == 'left' else 2]
- pixelPos = self._plotFrame.dataToPixel(
- xCoord, 0.5 * sum(yRange), axis=yAxis)
-
- if item['text'] is not None:
- x = pixelPos[0] + pixelOffset
- y = self._plotFrame.margins.top + pixelOffset
- label = glutils.Text2D(
- item['text'], x, y,
- color=item['color'],
- bgColor=(1., 1., 1., 0.5),
- align=glutils.LEFT,
- valign=glutils.TOP,
- devicePixelRatio=self.getDevicePixelRatio())
- labels.append(label)
-
- height = self._plotFrame.size[1]
- lines = glutils.GLLines2D(
- (pixelPos[0], pixelPos[0]), (0, height),
- style=item['linestyle'],
- color=item['color'],
- width=item['linewidth'])
- context.matrix = self.matScreenProj
- lines.render(context)
-
- else:
- xmin, xmax = self._plot.getXAxis().getLimits()
- ymin, ymax = self._plot.getYAxis(axis=yAxis).getLimits()
- if not xmin < xCoord < xmax or not ymin < yCoord < ymax:
- # Do not render markers outside visible plot area
- continue
- pixelPos = self._plotFrame.dataToPixel(
- xCoord, yCoord, axis=yAxis)
-
- if isYInverted:
- valign = glutils.BOTTOM
- vPixelOffset = -pixelOffset
- else:
- valign = glutils.TOP
- vPixelOffset = pixelOffset
-
- if item['text'] is not None:
- x = pixelPos[0] + pixelOffset
- y = pixelPos[1] + vPixelOffset
- label = glutils.Text2D(
- item['text'], x, y,
- color=item['color'],
- bgColor=(1., 1., 1., 0.5),
- align=glutils.LEFT,
- valign=valign,
- devicePixelRatio=self.getDevicePixelRatio())
- labels.append(label)
-
- # For now simple implementation: using a curve for each marker
- # Should pack all markers to a single set of points
- markerCurve = glutils.GLPlotCurve2D(
- numpy.array((pixelPos[0],), dtype=numpy.float64),
- numpy.array((pixelPos[1],), dtype=numpy.float64),
- marker=item['symbol'],
- markerColor=item['color'],
- markerSize=11)
-
- context = glutils.RenderContext(
- matrix=self.matScreenProj,
- isXLog=False,
- isYLog=False,
- dpi=self.getDotsPerInch())
- markerCurve.render(context)
- markerCurve.discard()
-
- else:
- _logger.error('Unsupported item: %s', str(item))
- continue
-
- # Render marker labels
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
- for label in labels:
- label.render(self.matScreenProj)
-
- def _renderOverlayGL(self):
- """Render overlay layer: overlay items and crosshair."""
- plotWidth, plotHeight = self._plotFrame.plotSize
-
- # Scissor to plot area
- gl.glScissor(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
- gl.glEnable(gl.GL_SCISSOR_TEST)
-
- self._renderItems(overlay=True)
-
- # Render crosshair cursor
- if self._crosshairCursor is not None and self._mousePosInPixels is not None:
- self._progBase.use()
- gl.glUniform2i(self._progBase.uniforms['isLog'], False, False)
- gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
- posAttrib = self._progBase.attributes['position']
- matrixUnif = self._progBase.uniforms['matrix']
- colorUnif = self._progBase.uniforms['color']
- hatchStepUnif = self._progBase.uniforms['hatchStep']
-
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
-
- gl.glUniformMatrix4fv(matrixUnif, 1, gl.GL_TRUE,
- self.matScreenProj.astype(numpy.float32))
-
- color, lineWidth = self._crosshairCursor
- gl.glUniform4f(colorUnif, *color)
- gl.glUniform1i(hatchStepUnif, 0)
-
- xPixel, yPixel = self._mousePosInPixels
- xPixel, yPixel = xPixel + 0.5, yPixel + 0.5
- vertices = numpy.array(((0., yPixel),
- (self._plotFrame.size[0], yPixel),
- (xPixel, 0.),
- (xPixel, self._plotFrame.size[1])),
- dtype=numpy.float32)
-
- gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, vertices)
- gl.glLineWidth(lineWidth)
- gl.glDrawArrays(gl.GL_LINES, 0, len(vertices))
-
- gl.glDisable(gl.GL_SCISSOR_TEST)
-
- def _renderPlotAreaGL(self):
- """Render base layer of plot area.
-
- It renders the background, grid and items except overlays
- """
- plotWidth, plotHeight = self._plotFrame.plotSize
-
- gl.glScissor(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
- gl.glEnable(gl.GL_SCISSOR_TEST)
-
- if self._dataBackgroundColor != self._backgroundColor:
- gl.glClearColor(*self._dataBackgroundColor)
- gl.glClear(gl.GL_COLOR_BUFFER_BIT)
-
- self._plotFrame.renderGrid()
-
- # Matrix
- trBounds = self._plotFrame.transformedDataRanges
- if trBounds.x[0] != trBounds.x[1] and trBounds.y[0] != trBounds.y[1]:
- # Do rendering of items
- self._renderItems(overlay=False)
-
- gl.glDisable(gl.GL_SCISSOR_TEST)
-
- def resizeGL(self, width, height):
- if width == 0 or height == 0: # Do not resize
- return
-
- self._plotFrame.size = (
- int(self.getDevicePixelRatio() * width),
- int(self.getDevicePixelRatio() * height))
-
- self.matScreenProj = glutils.mat4Ortho(
- 0, self._plotFrame.size[0],
- self._plotFrame.size[1], 0,
- 1, -1)
-
- # Store current ranges
- previousXRange = self.getGraphXLimits()
- previousYRange = self.getGraphYLimits(axis='left')
- previousYRightRange = self.getGraphYLimits(axis='right')
-
- (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \
- self._plotFrame.dataRanges
- self.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
-
- # If plot range has changed, then emit signal
- if previousXRange != self.getGraphXLimits():
- self._plot.getXAxis()._emitLimitsChanged()
- if previousYRange != self.getGraphYLimits(axis='left'):
- self._plot.getYAxis(axis='left')._emitLimitsChanged()
- if previousYRightRange != self.getGraphYLimits(axis='right'):
- self._plot.getYAxis(axis='right')._emitLimitsChanged()
-
- # Add methods
-
- @staticmethod
- def _castArrayTo(v):
- """Returns best floating type to cast the array to.
-
- :param numpy.ndarray v: Array to cast
- :rtype: numpy.dtype
- :raise ValueError: If dtype is not supported
- """
- if numpy.issubdtype(v.dtype, numpy.floating):
- return numpy.float32 if v.itemsize <= 4 else numpy.float64
- elif numpy.issubdtype(v.dtype, numpy.integer):
- return numpy.float32 if v.itemsize <= 2 else numpy.float64
- else:
- raise ValueError('Unsupported data type')
-
- def addCurve(self, x, y,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror,
- fill, alpha, symbolsize, baseline):
- for parameter in (x, y, color, symbol, linewidth, linestyle,
- yaxis, fill, symbolsize):
- assert parameter is not None
- assert yaxis in ('left', 'right')
-
- # Convert input data
- x = numpy.array(x, copy=False)
- y = numpy.array(y, copy=False)
-
- # Check if float32 is enough
- if (self._castArrayTo(x) is numpy.float32 and
- self._castArrayTo(y) is numpy.float32):
- dtype = numpy.float32
- else:
- dtype = numpy.float64
-
- x = numpy.array(x, dtype=dtype, copy=False, order='C')
- y = numpy.array(y, dtype=dtype, copy=False, order='C')
-
- # Convert errors to float32
- if xerror is not None:
- xerror = numpy.array(
- xerror, dtype=numpy.float32, copy=False, order='C')
- if yerror is not None:
- yerror = numpy.array(
- yerror, dtype=numpy.float32, copy=False, order='C')
-
- # Handle axes log scale: convert data
-
- if self._plotFrame.xAxis.isLog:
- logX = numpy.log10(x)
-
- if xerror is not None:
- # Transform xerror so that
- # log10(x) +/- xerror' = log10(x +/- xerror)
- if hasattr(xerror, 'shape') and len(xerror.shape) == 2:
- xErrorMinus, xErrorPlus = xerror[0], xerror[1]
- else:
- xErrorMinus, xErrorPlus = xerror, xerror
- with numpy.errstate(divide='ignore', invalid='ignore'):
- # Ignore divide by zero, invalid value encountered in log10
- xErrorMinus = logX - numpy.log10(x - xErrorMinus)
- xErrorPlus = numpy.log10(x + xErrorPlus) - logX
- xerror = numpy.array((xErrorMinus, xErrorPlus),
- dtype=numpy.float32)
-
- x = logX
-
- isYLog = (yaxis == 'left' and self._plotFrame.yAxis.isLog) or (
- yaxis == 'right' and self._plotFrame.y2Axis.isLog)
-
- if isYLog:
- logY = numpy.log10(y)
-
- if yerror is not None:
- # Transform yerror so that
- # log10(y) +/- yerror' = log10(y +/- yerror)
- if hasattr(yerror, 'shape') and len(yerror.shape) == 2:
- yErrorMinus, yErrorPlus = yerror[0], yerror[1]
- else:
- yErrorMinus, yErrorPlus = yerror, yerror
- with numpy.errstate(divide='ignore', invalid='ignore'):
- # Ignore divide by zero, invalid value encountered in log10
- yErrorMinus = logY - numpy.log10(y - yErrorMinus)
- yErrorPlus = numpy.log10(y + yErrorPlus) - logY
- yerror = numpy.array((yErrorMinus, yErrorPlus),
- dtype=numpy.float32)
-
- y = logY
-
- # TODO check if need more filtering of error (e.g., clip to positive)
-
- # TODO check and improve this
- if (len(color) == 4 and
- type(color[3]) in [type(1), numpy.uint8, numpy.int8]):
- color = numpy.array(color, dtype=numpy.float32) / 255.
-
- if isinstance(color, numpy.ndarray) and color.ndim == 2:
- colorArray = color
- color = None
- else:
- colorArray = None
- color = colors.rgba(color)
-
- if alpha < 1.: # Apply image transparency
- if colorArray is not None and colorArray.shape[1] == 4:
- # multiply alpha channel
- colorArray[:, 3] = colorArray[:, 3] * alpha
- if color is not None:
- color = color[0], color[1], color[2], color[3] * alpha
-
- fillColor = None
- if fill is True:
- fillColor = color
- curve = glutils.GLPlotCurve2D(
- x, y, colorArray,
- xError=xerror,
- yError=yerror,
- lineStyle=linestyle,
- lineColor=color,
- lineWidth=linewidth,
- marker=symbol,
- markerColor=color,
- markerSize=symbolsize,
- fillColor=fillColor,
- baseline=baseline,
- isYLog=isYLog)
- curve.yaxis = 'left' if yaxis is None else yaxis
-
- if yaxis == "right":
- self._plotFrame.isY2Axis = True
-
- return curve
-
- def addImage(self, data,
- origin, scale,
- colormap, alpha):
- for parameter in (data, origin, scale):
- assert parameter is not None
-
- if data.ndim == 2:
- # Ensure array is contiguous and eventually convert its type
- dtypes = [dtype for dtype in (
- numpy.float32, numpy.float16, numpy.uint8, numpy.uint16)
- if glu.isSupportedGLType(dtype)]
- if data.dtype in dtypes:
- data = numpy.array(data, copy=False, order='C')
- else:
- _logger.info(
- 'addImage: Convert %s data to float32', str(data.dtype))
- data = numpy.array(data, dtype=numpy.float32, order='C')
-
- normalization = colormap.getNormalization()
- if normalization in glutils.GLPlotColormap.SUPPORTED_NORMALIZATIONS:
- # Fast path applying colormap on the GPU
- cmapRange = colormap.getColormapRange(data=data)
- colormapLut = colormap.getNColors(nbColors=256)
- gamma = colormap.getGammaNormalizationParameter()
- nanColor = colors.rgba(colormap.getNaNColor())
-
- image = glutils.GLPlotColormap(
- data,
- origin,
- scale,
- colormapLut,
- normalization,
- gamma,
- cmapRange,
- alpha,
- nanColor)
-
- else: # Fallback applying colormap on CPU
- rgba = colormap.applyToData(data)
- image = glutils.GLPlotRGBAImage(rgba, origin, scale, alpha)
-
- elif len(data.shape) == 3:
- # For RGB, RGBA data
- assert data.shape[2] in (3, 4)
-
- if numpy.issubdtype(data.dtype, numpy.floating):
- data = numpy.array(data, dtype=numpy.float32, copy=False)
- elif data.dtype in [numpy.uint8, numpy.uint16]:
- pass
- elif numpy.issubdtype(data.dtype, numpy.integer):
- data = numpy.array(data, dtype=numpy.uint8, copy=False)
- else:
- raise ValueError('Unsupported data type')
-
- image = glutils.GLPlotRGBAImage(data, origin, scale, alpha)
-
- else:
- raise RuntimeError("Unsupported data shape {0}".format(data.shape))
-
- # TODO is this needed?
- if self._plotFrame.xAxis.isLog and image.xMin <= 0.:
- raise RuntimeError(
- 'Cannot add image with X <= 0 with X axis log scale')
- if self._plotFrame.yAxis.isLog and image.yMin <= 0.:
- raise RuntimeError(
- 'Cannot add image with Y <= 0 with Y axis log scale')
-
- return image
-
- def addTriangles(self, x, y, triangles,
- color, alpha):
- # Handle axes log scale: convert data
- if self._plotFrame.xAxis.isLog:
- x = numpy.log10(x)
- if self._plotFrame.yAxis.isLog:
- y = numpy.log10(y)
-
- triangles = glutils.GLPlotTriangles(x, y, color, triangles, alpha)
-
- return triangles
-
- def addShape(self, x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor):
- x = numpy.array(x, copy=False)
- y = numpy.array(y, copy=False)
-
- # TODO is this needed?
- if self._plotFrame.xAxis.isLog and x.min() <= 0.:
- raise RuntimeError(
- 'Cannot add item with X <= 0 with X axis log scale')
- if self._plotFrame.yAxis.isLog and y.min() <= 0.:
- raise RuntimeError(
- 'Cannot add item with Y <= 0 with Y axis log scale')
-
- return _ShapeItem(x, y, shape, color, fill, overlay,
- linestyle, linewidth, linebgcolor)
-
- def addMarker(self, x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis):
- return _MarkerItem(x, y, text, color,
- symbol, linestyle, linewidth, constraint, yaxis)
-
- # Remove methods
-
- def remove(self, item):
- if isinstance(item, glutils.GLPlotItem):
- if item.yaxis == 'right':
- # Check if some curves remains on the right Y axis
- y2AxisItems = (item for item in self._plot.getItems()
- if isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right')
- self._plotFrame.isY2Axis = next(y2AxisItems, None) is not None
-
- if item.isInitialized():
- self._glGarbageCollector.append(item)
-
- elif isinstance(item, (_MarkerItem, _ShapeItem)):
- pass # No-op
-
- else:
- _logger.error('Unsupported item: %s', str(item))
-
- # Interaction methods
-
- _QT_CURSORS = {
- BackendBase.CURSOR_DEFAULT: qt.Qt.ArrowCursor,
- BackendBase.CURSOR_POINTING: qt.Qt.PointingHandCursor,
- BackendBase.CURSOR_SIZE_HOR: qt.Qt.SizeHorCursor,
- BackendBase.CURSOR_SIZE_VER: qt.Qt.SizeVerCursor,
- BackendBase.CURSOR_SIZE_ALL: qt.Qt.SizeAllCursor,
- }
-
- def setGraphCursorShape(self, cursor):
- if cursor is None:
- super(BackendOpenGL, self).unsetCursor()
- else:
- cursor = self._QT_CURSORS[cursor]
- super(BackendOpenGL, self).setCursor(qt.QCursor(cursor))
-
- def setGraphCursor(self, flag, color, linewidth, linestyle):
- if linestyle != '-':
- _logger.warning(
- "BackendOpenGL.setGraphCursor linestyle parameter ignored")
-
- if flag:
- color = colors.rgba(color)
- crosshairCursor = color, linewidth
- else:
- crosshairCursor = None
-
- if crosshairCursor != self._crosshairCursor:
- self._crosshairCursor = crosshairCursor
-
- _PICK_OFFSET = 3 # Offset in pixel used for picking
-
- def _mouseInPlotArea(self, x, y):
- """Returns closest visible position in the plot.
-
- This is performed in Qt widget pixel, not device pixel.
-
- :param float x: X coordinate in Qt widget pixel
- :param float y: Y coordinate in Qt widget pixel
- :return: (x, y) closest point in the plot.
- :rtype: List[float]
- """
- left, top, width, height = self.getPlotBoundsInPixels()
- return (numpy.clip(x, left, left + width - 1), # TODO -1?
- numpy.clip(y, top, top + height - 1))
-
- def __pickCurves(self, item, x, y):
- """Perform picking on a curve item.
-
- :param GLPlotCurve2D item:
- :param float x: X position of the mouse in widget coordinates
- :param float y: Y position of the mouse in widget coordinates
- :return: List of indices of picked points or None if not picked
- :rtype: Union[List[int],None]
- """
- offset = self._PICK_OFFSET
- if item.marker is not None:
- # Convert markerSize from points to qt pixels
- qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
- size = item.markerSize / 72. * qtDpi
- offset = max(size / 2., offset)
- if item.lineStyle is not None:
- # Convert line width from points to qt pixels
- qtDpi = self.getDotsPerInch() / self.getDevicePixelRatio()
- lineWidth = item.lineWidth / 72. * qtDpi
- offset = max(lineWidth / 2., offset)
-
- inAreaPos = self._mouseInPlotArea(x - offset, y - offset)
- dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=item.yaxis, check=True)
- if dataPos is None:
- return None
- xPick0, yPick0 = dataPos
-
- inAreaPos = self._mouseInPlotArea(x + offset, y + offset)
- dataPos = self._plot.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=item.yaxis, check=True)
- if dataPos is None:
- return None
- xPick1, yPick1 = dataPos
-
- if xPick0 < xPick1:
- xPickMin, xPickMax = xPick0, xPick1
- else:
- xPickMin, xPickMax = xPick1, xPick0
-
- if yPick0 < yPick1:
- yPickMin, yPickMax = yPick0, yPick1
- else:
- yPickMin, yPickMax = yPick1, yPick0
-
- # Apply log scale if axis is log
- if self._plotFrame.xAxis.isLog:
- xPickMin = numpy.log10(xPickMin)
- xPickMax = numpy.log10(xPickMax)
-
- if (item.yaxis == 'left' and self._plotFrame.yAxis.isLog) or (
- item.yaxis == 'right' and self._plotFrame.y2Axis.isLog):
- yPickMin = numpy.log10(yPickMin)
- yPickMax = numpy.log10(yPickMax)
-
- return item.pick(xPickMin, yPickMin,
- xPickMax, yPickMax)
-
- def pickItem(self, x, y, item):
- # Picking is performed in Qt widget pixels not device pixels
- dataPos = self._plot.pixelToData(x, y, axis='left', check=True)
- if dataPos is None:
- return None # Outside plot area
-
- if item is None:
- _logger.error("No item provided for picking")
- return None
-
- # Pick markers
- if isinstance(item, _MarkerItem):
- yaxis = item['yaxis']
- pixelPos = self._plot.dataToPixel(
- item['x'], item['y'], axis=yaxis, check=False)
- if pixelPos is None:
- return None # negative coord on a log axis
-
- if item['x'] is None: # Horizontal line
- pt1 = self._plot.pixelToData(
- x, y - self._PICK_OFFSET, axis=yaxis, check=False)
- pt2 = self._plot.pixelToData(
- x, y + self._PICK_OFFSET, axis=yaxis, check=False)
- isPicked = (min(pt1[1], pt2[1]) <= item['y'] <=
- max(pt1[1], pt2[1]))
-
- elif item['y'] is None: # Vertical line
- pt1 = self._plot.pixelToData(
- x - self._PICK_OFFSET, y, axis=yaxis, check=False)
- pt2 = self._plot.pixelToData(
- x + self._PICK_OFFSET, y, axis=yaxis, check=False)
- isPicked = (min(pt1[0], pt2[0]) <= item['x'] <=
- max(pt1[0], pt2[0]))
-
- else:
- isPicked = (
- numpy.fabs(x - pixelPos[0]) <= self._PICK_OFFSET and
- numpy.fabs(y - pixelPos[1]) <= self._PICK_OFFSET)
-
- return (0,) if isPicked else None
-
- # Pick image, curve, triangles
- elif isinstance(item, glutils.GLPlotItem):
- if isinstance(item, glutils.GLPlotCurve2D):
- return self.__pickCurves(item, x, y)
- else:
- return item.pick(*dataPos) # Might be None
-
- # Update curve
-
- def setCurveColor(self, curve, color):
- pass # TODO
-
- # Misc.
-
- def getWidgetHandle(self):
- return self
-
- def postRedisplay(self):
- self._sigPostRedisplay.emit()
-
- def replot(self):
- self.update() # async redraw
- # self.repaint() # immediate redraw
-
- def saveGraph(self, fileName, fileFormat, dpi):
- if dpi is not None:
- _logger.warning("saveGraph ignores dpi parameter")
-
- if fileFormat not in ['png', 'ppm', 'svg', 'tiff']:
- raise NotImplementedError('Unsupported format: %s' % fileFormat)
-
- if not self.isValid():
- _logger.error('OpenGL 2.1 not available, cannot save OpenGL image')
- width, height = self._plotFrame.size
- data = numpy.zeros((height, width, 3), dtype=numpy.uint8)
- else:
- self.makeCurrent()
-
- data = numpy.empty(
- (self._plotFrame.size[1], self._plotFrame.size[0], 3),
- dtype=numpy.uint8, order='C')
-
- context = self.context()
- framebufferTexture = self._plotFBOs.get(context)
- if framebufferTexture is None:
- # Fallback, supports direct rendering mode: _paintDirectGL
- # might have issues as it can read on-screen framebuffer
- fboName = self.defaultFramebufferObject()
- width, height = self._plotFrame.size
- else:
- fboName = framebufferTexture.name
- height, width = framebufferTexture.shape
-
- previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING)
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, fboName)
- gl.glPixelStorei(gl.GL_PACK_ALIGNMENT, 1)
- gl.glReadPixels(0, 0, width, height,
- gl.GL_RGB, gl.GL_UNSIGNED_BYTE, data)
- gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, previousFramebuffer)
-
- # glReadPixels gives bottom to top,
- # while images are stored as top to bottom
- data = numpy.flipud(data)
-
- # fileName is either a file-like object or a str
- saveImageToFile(data, fileName, fileFormat)
-
- # Graph labels
-
- def setGraphTitle(self, title):
- self._plotFrame.title = title
-
- def setGraphXLabel(self, label):
- self._plotFrame.xAxis.title = label
-
- def setGraphYLabel(self, label, axis):
- if axis == 'left':
- self._plotFrame.yAxis.title = label
- else: # right axis
- self._plotFrame.y2Axis.title = label
-
- # Graph limits
-
- def _setDataRanges(self, xlim=None, ylim=None, y2lim=None):
- """Set the visible range of data in the plot frame.
-
- This clips the ranges to possible values (takes care of float32
- range + positive range for log).
- This also takes care of non-orthogonal axes.
-
- This should be moved to PlotFrame.
- """
- # Update axes range with a clipped range if too wide
- self._plotFrame.setDataRanges(xlim, ylim, y2lim)
-
- def _ensureAspectRatio(self, keepDim=None):
- """Update plot bounds in order to keep aspect ratio.
-
- Warning: keepDim on right Y axis is not implemented !
-
- :param str keepDim: The dimension to maintain: 'x', 'y' or None.
- If None (the default), the dimension with the largest range.
- """
- plotWidth, plotHeight = self._plotFrame.plotSize
- if plotWidth <= 2 or plotHeight <= 2:
- return
-
- if keepDim is None:
- ranges = self._plot.getDataRange()
- if (ranges.y is not None and
- ranges.x is not None and
- (ranges.y[1] - ranges.y[0]) != 0.):
- dataRatio = (ranges.x[1] - ranges.x[0]) / float(ranges.y[1] - ranges.y[0])
- plotRatio = plotWidth / float(plotHeight) # Test != 0 before
-
- keepDim = 'x' if dataRatio > plotRatio else 'y'
- else: # Limit case
- keepDim = 'x'
-
- (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = \
- self._plotFrame.dataRanges
- if keepDim == 'y':
- dataW = (yMax - yMin) * plotWidth / float(plotHeight)
- xCenter = 0.5 * (xMin + xMax)
- xMin = xCenter - 0.5 * dataW
- xMax = xCenter + 0.5 * dataW
- elif keepDim == 'x':
- dataH = (xMax - xMin) * plotHeight / float(plotWidth)
- yCenter = 0.5 * (yMin + yMax)
- yMin = yCenter - 0.5 * dataH
- yMax = yCenter + 0.5 * dataH
- y2Center = 0.5 * (y2Min + y2Max)
- y2Min = y2Center - 0.5 * dataH
- y2Max = y2Center + 0.5 * dataH
- else:
- raise RuntimeError('Unsupported dimension to keep: %s' % keepDim)
-
- # Update plot frame bounds
- self._setDataRanges(xlim=(xMin, xMax),
- ylim=(yMin, yMax),
- y2lim=(y2Min, y2Max))
-
- def _setPlotBounds(self, xRange=None, yRange=None, y2Range=None,
- keepDim=None):
- # Update axes range with a clipped range if too wide
- self._setDataRanges(xlim=xRange,
- ylim=yRange,
- y2lim=y2Range)
-
- # Keep data aspect ratio
- if self.isKeepDataAspectRatio():
- self._ensureAspectRatio(keepDim)
-
- def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
- assert xmin < xmax
- assert ymin < ymax
-
- if y2min is None or y2max is None:
- y2Range = None
- else:
- assert y2min < y2max
- y2Range = y2min, y2max
- self._setPlotBounds((xmin, xmax), (ymin, ymax), y2Range)
-
- def getGraphXLimits(self):
- return self._plotFrame.dataRanges.x
-
- def setGraphXLimits(self, xmin, xmax):
- assert xmin < xmax
- self._setPlotBounds(xRange=(xmin, xmax), keepDim='x')
-
- def getGraphYLimits(self, axis):
- assert axis in ("left", "right")
- if axis == "left":
- return self._plotFrame.dataRanges.y
- else:
- return self._plotFrame.dataRanges.y2
-
- def setGraphYLimits(self, ymin, ymax, axis):
- assert ymin < ymax
- assert axis in ("left", "right")
-
- if axis == "left":
- self._setPlotBounds(yRange=(ymin, ymax), keepDim='y')
- else:
- self._setPlotBounds(y2Range=(ymin, ymax), keepDim='y')
-
- # Graph axes
-
- def getXAxisTimeZone(self):
- return self._plotFrame.xAxis.timeZone
-
- def setXAxisTimeZone(self, tz):
- self._plotFrame.xAxis.timeZone = tz
-
- def isXAxisTimeSeries(self):
- return self._plotFrame.xAxis.isTimeSeries
-
- def setXAxisTimeSeries(self, isTimeSeries):
- self._plotFrame.xAxis.isTimeSeries = isTimeSeries
-
- def setXAxisLogarithmic(self, flag):
- if flag != self._plotFrame.xAxis.isLog:
- if flag and self._keepDataAspectRatio:
- _logger.warning(
- "KeepDataAspectRatio is ignored with log axes")
-
- self._plotFrame.xAxis.isLog = flag
-
- def setYAxisLogarithmic(self, flag):
- if (flag != self._plotFrame.yAxis.isLog or
- flag != self._plotFrame.y2Axis.isLog):
- if flag and self._keepDataAspectRatio:
- _logger.warning(
- "KeepDataAspectRatio is ignored with log axes")
-
- self._plotFrame.yAxis.isLog = flag
- self._plotFrame.y2Axis.isLog = flag
-
- def setYAxisInverted(self, flag):
- if flag != self._plotFrame.isYAxisInverted:
- self._plotFrame.isYAxisInverted = flag
-
- def isYAxisInverted(self):
- return self._plotFrame.isYAxisInverted
-
- def isKeepDataAspectRatio(self):
- if self._plotFrame.xAxis.isLog or self._plotFrame.yAxis.isLog:
- return False
- else:
- return self._keepDataAspectRatio
-
- def setKeepDataAspectRatio(self, flag):
- if flag and (self._plotFrame.xAxis.isLog or
- self._plotFrame.yAxis.isLog):
- _logger.warning("KeepDataAspectRatio is ignored with log axes")
-
- self._keepDataAspectRatio = flag
-
- def setGraphGrid(self, which):
- assert which in (None, 'major', 'both')
- self._plotFrame.grid = which is not None # TODO True grid support
-
- # Data <-> Pixel coordinates conversion
-
- def dataToPixel(self, x, y, axis):
- result = self._plotFrame.dataToPixel(x, y, axis)
- if result is None:
- return None
- else:
- devicePixelRatio = self.getDevicePixelRatio()
- return tuple(value/devicePixelRatio for value in result)
-
- def pixelToData(self, x, y, axis):
- devicePixelRatio = self.getDevicePixelRatio()
- return self._plotFrame.pixelToData(
- x * devicePixelRatio, y * devicePixelRatio, axis)
-
- def getPlotBoundsInPixels(self):
- devicePixelRatio = self.getDevicePixelRatio()
- return tuple(int(value / devicePixelRatio)
- for value in self._plotFrame.plotOrigin + self._plotFrame.plotSize)
-
- def setAxesMargins(self, left: float, top: float, right: float, bottom: float):
- self._plotFrame.marginRatios = left, top, right, bottom
-
- def setForegroundColors(self, foregroundColor, gridColor):
- self._plotFrame.foregroundColor = foregroundColor
- self._plotFrame.gridColor = gridColor
-
- def setBackgroundColors(self, backgroundColor, dataBackgroundColor):
- self._backgroundColor = backgroundColor
- self._dataBackgroundColor = dataBackgroundColor
diff --git a/silx/gui/plot/backends/__init__.py b/silx/gui/plot/backends/__init__.py
deleted file mode 100644
index 966d9df..0000000
--- a/silx/gui/plot/backends/__init__.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# 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 implements the backend of the Plot."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "21/03/2017"
diff --git a/silx/gui/plot/backends/glutils/GLPlotCurve.py b/silx/gui/plot/backends/glutils/GLPlotCurve.py
deleted file mode 100644
index 34844c6..0000000
--- a/silx/gui/plot/backends/glutils/GLPlotCurve.py
+++ /dev/null
@@ -1,1375 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2021 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 classes to render 2D lines and scatter plots
-"""
-
-from __future__ import division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/04/2017"
-
-
-import math
-import logging
-
-import numpy
-
-from silx.math.combo import min_max
-
-from ...._glutils import gl
-from ...._glutils import Program, vertexBuffer, VertexBufferAttrib
-from .GLSupport import buildFillMaskIndices, mat4Identity, mat4Translate
-from .GLPlotImage import GLPlotItem
-
-
-_logger = logging.getLogger(__name__)
-
-
-_MPL_NONES = None, 'None', '', ' '
-"""Possible values for None"""
-
-
-def _notNaNSlices(array, length=1):
- """Returns slices of none NaN values in the array.
-
- :param numpy.ndarray array: 1D array from which to get slices
- :param int length: Slices shorter than length gets discarded
- :return: Array of (start, end) slice indices
- :rtype: numpy.ndarray
- """
- isnan = numpy.isnan(numpy.array(array, copy=False).reshape(-1))
- notnan = numpy.logical_not(isnan)
- start = numpy.where(numpy.logical_and(isnan[:-1], notnan[1:]))[0] + 1
- if notnan[0]:
- start = numpy.append(0, start)
- end = numpy.where(numpy.logical_and(notnan[:-1], isnan[1:]))[0] + 1
- if notnan[-1]:
- end = numpy.append(end, len(array))
- slices = numpy.transpose((start, end))
- if length > 1:
- # discard slices with less than length values
- slices = slices[numpy.diff(slices, axis=1).ravel() >= length]
- return slices
-
-
-# fill ########################################################################
-
-class _Fill2D(object):
- """Object rendering curve filling as polygons
-
- :param numpy.ndarray xData: X coordinates of points
- :param numpy.ndarray yData: Y coordinates of points
- :param float baseline: Y value of the 'bottom' of the fill.
- 0 for linear Y scale, -38 for log Y scale
- :param List[float] color: RGBA color as 4 float in [0, 1]
- :param List[float] offset: Translation of coordinates (ox, oy)
- """
-
- _PROGRAM = Program(
- vertexShader="""
- #version 120
-
- uniform mat4 matrix;
- attribute float xPos;
- attribute float yPos;
-
- void main(void) {
- gl_Position = matrix * vec4(xPos, yPos, 0.0, 1.0);
- }
- """,
- fragmentShader="""
- #version 120
-
- uniform vec4 color;
-
- void main(void) {
- gl_FragColor = color;
- }
- """,
- attrib0='xPos')
-
- def __init__(self, xData=None, yData=None,
- baseline=0,
- color=(0., 0., 0., 1.),
- offset=(0., 0.)):
- self.xData = xData
- self.yData = yData
- self._xFillVboData = None
- self._yFillVboData = None
- self.color = color
- self.offset = offset
-
- # Offset baseline
- self.baseline = baseline - self.offset[1]
-
- def prepare(self):
- """Rendering preparation: build indices and bounding box vertices"""
- if (self._xFillVboData is None and
- self.xData is not None and self.yData is not None):
-
- # Get slices of not NaN values longer than 1 element
- isnan = numpy.logical_or(numpy.isnan(self.xData), numpy.isnan(self.yData))
- notnan = numpy.logical_not(isnan)
- start = numpy.where(numpy.logical_and(isnan[:-1], notnan[1:]))[0] + 1
- if notnan[0]:
- start = numpy.append(0, start)
- end = numpy.where(numpy.logical_and(notnan[:-1], isnan[1:]))[0] + 1
- if notnan[-1]:
- end = numpy.append(end, len(isnan))
- slices = numpy.transpose((start, end))
- # discard slices with less than length values
- slices = slices[numpy.diff(slices, axis=1).reshape(-1) >= 2]
-
- # Number of points: slice + 2 * leading and trailing points
- # Twice leading and trailing points to produce degenerated triangles
- nbPoints = numpy.sum(numpy.diff(slices, axis=1)) * 2 + 4 * len(slices)
- points = numpy.empty((nbPoints, 2), dtype=numpy.float32)
-
- offset = 0
- # invert baseline for filling
- new_y_data = numpy.append(self.yData, self.baseline)
- for start, end in slices:
- # Duplicate first point for connecting degenerated triangle
- points[offset:offset+2] = self.xData[start], new_y_data[start]
-
- # 2nd point of the polygon is last point
- points[offset+2] = self.xData[start], self.baseline[start]
-
- indices = numpy.append(numpy.arange(start, end),
- numpy.arange(len(self.xData) + end-1, len(self.xData) + start-1, -1))
- indices = indices[buildFillMaskIndices(len(indices))]
-
- points[offset+3:offset+3+len(indices), 0] = self.xData[indices % len(self.xData)]
- points[offset+3:offset+3+len(indices), 1] = new_y_data[indices]
-
- # Duplicate last point for connecting degenerated triangle
- points[offset+3+len(indices)] = points[offset+3+len(indices)-1]
-
- offset += len(indices) + 4
-
- self._xFillVboData, self._yFillVboData = vertexBuffer(points.T)
-
- def render(self, context):
- """Perform rendering
-
- :param RenderContext context:
- """
- self.prepare()
-
- if self._xFillVboData is None:
- return # Nothing to display
-
- self._PROGRAM.use()
-
- gl.glUniformMatrix4fv(
- self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE,
- numpy.dot(context.matrix,
- mat4Translate(*self.offset)).astype(numpy.float32))
-
- gl.glUniform4f(self._PROGRAM.uniforms['color'], *self.color)
-
- xPosAttrib = self._PROGRAM.attributes['xPos']
- yPosAttrib = self._PROGRAM.attributes['yPos']
-
- gl.glEnableVertexAttribArray(xPosAttrib)
- self._xFillVboData.setVertexAttrib(xPosAttrib)
-
- gl.glEnableVertexAttribArray(yPosAttrib)
- self._yFillVboData.setVertexAttrib(yPosAttrib)
-
- # Prepare fill mask
- gl.glEnable(gl.GL_STENCIL_TEST)
- gl.glStencilMask(1)
- gl.glStencilFunc(gl.GL_ALWAYS, 1, 1)
- gl.glStencilOp(gl.GL_INVERT, gl.GL_INVERT, gl.GL_INVERT)
- gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
- gl.glDepthMask(gl.GL_FALSE)
-
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, self._xFillVboData.size)
-
- gl.glStencilFunc(gl.GL_EQUAL, 1, 1)
- # Reset stencil while drawing
- gl.glStencilOp(gl.GL_ZERO, gl.GL_ZERO, gl.GL_ZERO)
- gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
- gl.glDepthMask(gl.GL_TRUE)
-
- # Draw directly in NDC
- gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'], 1, gl.GL_TRUE,
- mat4Identity().astype(numpy.float32))
-
- # NDC vertices
- gl.glVertexAttribPointer(
- xPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0,
- numpy.array((-1., -1., 1., 1.), dtype=numpy.float32))
- gl.glVertexAttribPointer(
- yPosAttrib, 1, gl.GL_FLOAT, gl.GL_FALSE, 0,
- numpy.array((-1., 1., -1., 1.), dtype=numpy.float32))
-
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
-
- gl.glDisable(gl.GL_STENCIL_TEST)
-
- def discard(self):
- """Release VBOs"""
- if self.isInitialized():
- self._xFillVboData.vbo.discard()
-
- self._xFillVboData = None
- self._yFillVboData = None
-
- def isInitialized(self):
- return self._xFillVboData is not None
-
-
-# line ########################################################################
-
-SOLID, DASHED, DASHDOT, DOTTED = '-', '--', '-.', ':'
-
-
-class GLLines2D(object):
- """Object rendering curve as a polyline
-
- :param xVboData: X coordinates VBO
- :param yVboData: Y coordinates VBO
- :param colorVboData: VBO of colors
- :param distVboData: VBO of distance along the polyline
- :param str style: Line style in: '-', '--', '-.', ':'
- :param List[float] color: RGBA color as 4 float in [0, 1]
- :param float width: Line width
- :param float dashPeriod: Period of dashes
- :param drawMode: OpenGL drawing mode
- :param List[float] offset: Translation of coordinates (ox, oy)
- """
-
- STYLES = SOLID, DASHED, DASHDOT, DOTTED
- """Supported line styles"""
-
- _SOLID_PROGRAM = Program(
- vertexShader="""
- #version 120
-
- uniform mat4 matrix;
- attribute float xPos;
- attribute float yPos;
- attribute vec4 color;
-
- varying vec4 vColor;
-
- void main(void) {
- gl_Position = matrix * vec4(xPos, yPos, 0., 1.) ;
- vColor = color;
- }
- """,
- fragmentShader="""
- #version 120
-
- varying vec4 vColor;
-
- void main(void) {
- gl_FragColor = vColor;
- }
- """,
- attrib0='xPos')
-
- # Limitation: Dash using an estimate of distance in screen coord
- # to avoid computing distance when viewport is resized
- # results in inequal dashes when viewport aspect ratio is far from 1
- _DASH_PROGRAM = Program(
- vertexShader="""
- #version 120
-
- uniform mat4 matrix;
- uniform vec2 halfViewportSize;
- attribute float xPos;
- attribute float yPos;
- attribute vec4 color;
- attribute float distance;
-
- varying float vDist;
- varying vec4 vColor;
-
- void main(void) {
- gl_Position = matrix * vec4(xPos, yPos, 0., 1.);
- //Estimate distance in pixels
- vec2 probe = vec2(matrix * vec4(1., 1., 0., 0.)) *
- halfViewportSize;
- float pixelPerDataEstimate = length(probe)/sqrt(2.);
- vDist = distance * pixelPerDataEstimate;
- vColor = color;
- }
- """,
- fragmentShader="""
- #version 120
-
- /* Dashes: [0, x], [y, z]
- Dash period: w */
- uniform vec4 dash;
- uniform vec4 dash2ndColor;
-
- varying float vDist;
- varying vec4 vColor;
-
- void main(void) {
- float dist = mod(vDist, dash.w);
- if ((dist > dash.x && dist < dash.y) || dist > dash.z) {
- if (dash2ndColor.a == 0.) {
- discard; // Discard full transparent bg color
- } else {
- gl_FragColor = dash2ndColor;
- }
- } else {
- gl_FragColor = vColor;
- }
- }
- """,
- attrib0='xPos')
-
- def __init__(self, xVboData=None, yVboData=None,
- colorVboData=None, distVboData=None,
- style=SOLID, color=(0., 0., 0., 1.), dash2ndColor=None,
- width=1, dashPeriod=10., drawMode=None,
- offset=(0., 0.)):
- if (xVboData is not None and
- not isinstance(xVboData, VertexBufferAttrib)):
- xVboData = numpy.array(xVboData, copy=False, dtype=numpy.float32)
- self.xVboData = xVboData
-
- if (yVboData is not None and
- not isinstance(yVboData, VertexBufferAttrib)):
- yVboData = numpy.array(yVboData, copy=False, dtype=numpy.float32)
- self.yVboData = yVboData
-
- # Compute distances if not given while providing numpy array coordinates
- if (isinstance(self.xVboData, numpy.ndarray) and
- isinstance(self.yVboData, numpy.ndarray) and
- distVboData is None):
- distVboData = distancesFromArrays(self.xVboData, self.yVboData)
-
- if (distVboData is not None and
- not isinstance(distVboData, VertexBufferAttrib)):
- distVboData = numpy.array(
- distVboData, copy=False, dtype=numpy.float32)
- self.distVboData = distVboData
-
- if colorVboData is not None:
- assert isinstance(colorVboData, VertexBufferAttrib)
- self.colorVboData = colorVboData
- self.useColorVboData = colorVboData is not None
-
- self.color = color
- self.dash2ndColor = dash2ndColor
- self.width = width
- self._style = None
- self.style = style
- self.dashPeriod = dashPeriod
- self.offset = offset
-
- self._drawMode = drawMode if drawMode is not None else gl.GL_LINE_STRIP
-
- @property
- def style(self):
- """Line style (Union[str,None])"""
- return self._style
-
- @style.setter
- def style(self, style):
- if style in _MPL_NONES:
- self._style = None
- else:
- assert style in self.STYLES
- self._style = style
-
- @classmethod
- def init(cls):
- """OpenGL context initialization"""
- gl.glHint(gl.GL_LINE_SMOOTH_HINT, gl.GL_NICEST)
-
- def render(self, context):
- """Perform rendering
-
- :param RenderContext context:
- """
- width = self.width / 72. * context.dpi
-
- style = self.style
- if style is None:
- return
-
- elif style == SOLID:
- program = self._SOLID_PROGRAM
- program.use()
-
- else: # DASHED, DASHDOT, DOTTED
- program = self._DASH_PROGRAM
- program.use()
-
- x, y, viewWidth, viewHeight = gl.glGetFloatv(gl.GL_VIEWPORT)
- gl.glUniform2f(program.uniforms['halfViewportSize'],
- 0.5 * viewWidth, 0.5 * viewHeight)
-
- dashPeriod = self.dashPeriod * width
- if self.style == DOTTED:
- dash = (0.2 * dashPeriod,
- 0.5 * dashPeriod,
- 0.7 * dashPeriod,
- dashPeriod)
- elif self.style == DASHDOT:
- dash = (0.3 * dashPeriod,
- 0.5 * dashPeriod,
- 0.6 * dashPeriod,
- dashPeriod)
- else:
- dash = (0.5 * dashPeriod,
- dashPeriod,
- dashPeriod,
- dashPeriod)
-
- gl.glUniform4f(program.uniforms['dash'], *dash)
-
- if self.dash2ndColor is None:
- # Use fully transparent color which gets discarded in shader
- dash2ndColor = (0., 0., 0., 0.)
- else:
- dash2ndColor = self.dash2ndColor
- gl.glUniform4f(program.uniforms['dash2ndColor'], *dash2ndColor)
-
- distAttrib = program.attributes['distance']
- gl.glEnableVertexAttribArray(distAttrib)
- if isinstance(self.distVboData, VertexBufferAttrib):
- self.distVboData.setVertexAttrib(distAttrib)
- else:
- gl.glVertexAttribPointer(distAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.distVboData)
-
- if width != 1:
- gl.glEnable(gl.GL_LINE_SMOOTH)
-
- matrix = numpy.dot(context.matrix,
- mat4Translate(*self.offset)).astype(numpy.float32)
- gl.glUniformMatrix4fv(program.uniforms['matrix'],
- 1, gl.GL_TRUE, matrix)
-
- colorAttrib = program.attributes['color']
- if self.useColorVboData and self.colorVboData is not None:
- gl.glEnableVertexAttribArray(colorAttrib)
- self.colorVboData.setVertexAttrib(colorAttrib)
- else:
- gl.glDisableVertexAttribArray(colorAttrib)
- gl.glVertexAttrib4f(colorAttrib, *self.color)
-
- xPosAttrib = program.attributes['xPos']
- gl.glEnableVertexAttribArray(xPosAttrib)
- if isinstance(self.xVboData, VertexBufferAttrib):
- self.xVboData.setVertexAttrib(xPosAttrib)
- else:
- gl.glVertexAttribPointer(xPosAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.xVboData)
-
- yPosAttrib = program.attributes['yPos']
- gl.glEnableVertexAttribArray(yPosAttrib)
- if isinstance(self.yVboData, VertexBufferAttrib):
- self.yVboData.setVertexAttrib(yPosAttrib)
- else:
- gl.glVertexAttribPointer(yPosAttrib,
- 1,
- gl.GL_FLOAT,
- False,
- 0,
- self.yVboData)
-
- gl.glLineWidth(width)
- gl.glDrawArrays(self._drawMode, 0, self.xVboData.size)
-
- gl.glDisable(gl.GL_LINE_SMOOTH)
-
-
-def distancesFromArrays(xData, yData):
- """Returns distances between each points
-
- :param numpy.ndarray xData: X coordinate of points
- :param numpy.ndarray yData: Y coordinate of points
- :rtype: numpy.ndarray
- """
- # Split array into sub-shapes at not finite points
- splits = numpy.nonzero(numpy.logical_not(numpy.logical_and(
- numpy.isfinite(xData), numpy.isfinite(yData))))[0]
- splits = numpy.concatenate(([-1], splits, [len(xData) - 1]))
-
- # Compute distance independently for each sub-shapes,
- # putting not finite points as last points of sub-shapes
- distances = []
- for begin, end in zip(splits[:-1] + 1, splits[1:] + 1):
- if begin == end: # Empty shape
- continue
- elif end - begin == 1: # Single element
- distances.append([0])
- else:
- deltas = numpy.dstack((
- numpy.ediff1d(xData[begin:end], to_begin=numpy.float32(0.)),
- numpy.ediff1d(yData[begin:end], to_begin=numpy.float32(0.))))[0]
- distances.append(
- numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1))))
- return numpy.concatenate(distances)
-
-
-# points ######################################################################
-
-DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK = \
- 'd', 'o', 's', '+', 'x', '.', ',', '*'
-
-H_LINE, V_LINE, HEART = '_', '|', u'\u2665'
-
-TICK_LEFT = "tickleft"
-TICK_RIGHT = "tickright"
-TICK_UP = "tickup"
-TICK_DOWN = "tickdown"
-CARET_LEFT = "caretleft"
-CARET_RIGHT = "caretright"
-CARET_UP = "caretup"
-CARET_DOWN = "caretdown"
-
-
-class _Points2D(object):
- """Object rendering curve markers
-
- :param xVboData: X coordinates VBO
- :param yVboData: Y coordinates VBO
- :param colorVboData: VBO of colors
- :param str marker: Kind of symbol to use, see :attr:`MARKERS`.
- :param List[float] color: RGBA color as 4 float in [0, 1]
- :param float size: Marker size
- :param List[float] offset: Translation of coordinates (ox, oy)
- """
-
- MARKERS = (DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK,
- H_LINE, V_LINE, HEART, TICK_LEFT, TICK_RIGHT, TICK_UP, TICK_DOWN,
- CARET_LEFT, CARET_RIGHT, CARET_UP, CARET_DOWN)
- """List of supported markers"""
-
- _VERTEX_SHADER = """
- #version 120
-
- uniform mat4 matrix;
- uniform int transform;
- uniform float size;
- attribute float xPos;
- attribute float yPos;
- attribute vec4 color;
-
- varying vec4 vColor;
-
- void main(void) {
- gl_Position = matrix * vec4(xPos, yPos, 0., 1.);
- vColor = color;
- gl_PointSize = size;
- }
- """
-
- _FRAGMENT_SHADER_SYMBOLS = {
- DIAMOND: """
- float alphaSymbol(vec2 coord, float size) {
- vec2 centerCoord = abs(coord - vec2(0.5, 0.5));
- float f = centerCoord.x + centerCoord.y;
- return clamp(size * (0.5 - f), 0.0, 1.0);
- }
- """,
- CIRCLE: """
- float alphaSymbol(vec2 coord, float size) {
- float radius = 0.5;
- float r = distance(coord, vec2(0.5, 0.5));
- return clamp(size * (radius - r), 0.0, 1.0);
- }
- """,
- SQUARE: """
- float alphaSymbol(vec2 coord, float size) {
- return 1.0;
- }
- """,
- PLUS: """
- float alphaSymbol(vec2 coord, float size) {
- vec2 d = abs(size * (coord - vec2(0.5, 0.5)));
- if (min(d.x, d.y) < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- X_MARKER: """
- float alphaSymbol(vec2 coord, float size) {
- vec2 pos = floor(size * coord) + 0.5;
- vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size));
- if (min(d_x.x, d_x.y) <= 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- ASTERISK: """
- float alphaSymbol(vec2 coord, float size) {
- /* Combining +, x and circle */
- vec2 d_plus = abs(size * (coord - vec2(0.5, 0.5)));
- vec2 pos = floor(size * coord) + 0.5;
- vec2 d_x = abs(pos.x + vec2(- pos.y, pos.y - size));
- if (min(d_plus.x, d_plus.y) < 0.5) {
- return 1.0;
- } else if (min(d_x.x, d_x.y) <= 0.5) {
- float r = distance(coord, vec2(0.5, 0.5));
- return clamp(size * (0.5 - r), 0.0, 1.0);
- } else {
- return 0.0;
- }
- }
- """,
- H_LINE: """
- float alphaSymbol(vec2 coord, float size) {
- float dy = abs(size * (coord.y - 0.5));
- if (dy < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- V_LINE: """
- float alphaSymbol(vec2 coord, float size) {
- float dx = abs(size * (coord.x - 0.5));
- if (dx < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- HEART: """
- float alphaSymbol(vec2 coord, float size) {
- coord = (coord - 0.5) * 2.;
- coord *= 0.75;
- coord.y += 0.25;
- float a = atan(coord.x,-coord.y)/3.141593;
- float r = length(coord);
- float h = abs(a);
- float d = (13.0*h - 22.0*h*h + 10.0*h*h*h)/(6.0-5.0*h);
- float res = clamp(r-d, 0., 1.);
- // antialiasing
- res = smoothstep(0.1, 0.001, res);
- return res;
- }
- """,
- TICK_LEFT: """
- float alphaSymbol(vec2 coord, float size) {
- coord = size * (coord - 0.5);
- float dy = abs(coord.y);
- if (dy < 0.5 && coord.x < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- TICK_RIGHT: """
- float alphaSymbol(vec2 coord, float size) {
- coord = size * (coord - 0.5);
- float dy = abs(coord.y);
- if (dy < 0.5 && coord.x > -0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- TICK_UP: """
- float alphaSymbol(vec2 coord, float size) {
- coord = size * (coord - 0.5);
- float dx = abs(coord.x);
- if (dx < 0.5 && coord.y < 0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- TICK_DOWN: """
- float alphaSymbol(vec2 coord, float size) {
- coord = size * (coord - 0.5);
- float dx = abs(coord.x);
- if (dx < 0.5 && coord.y > -0.5) {
- return 1.0;
- } else {
- return 0.0;
- }
- }
- """,
- CARET_LEFT: """
- float alphaSymbol(vec2 coord, float size) {
- coord = size * (coord - 0.5);
- float d = abs(coord.x) - abs(coord.y);
- if (d >= -0.1 && coord.x > 0.5) {
- return smoothstep(-0.1, 0.1, d);
- } else {
- return 0.0;
- }
- }
- """,
- CARET_RIGHT: """
- float alphaSymbol(vec2 coord, float size) {
- coord = size * (coord - 0.5);
- float d = abs(coord.x) - abs(coord.y);
- if (d >= -0.1 && coord.x < 0.5) {
- return smoothstep(-0.1, 0.1, d);
- } else {
- return 0.0;
- }
- }
- """,
- CARET_UP: """
- float alphaSymbol(vec2 coord, float size) {
- coord = size * (coord - 0.5);
- float d = abs(coord.y) - abs(coord.x);
- if (d >= -0.1 && coord.y > 0.5) {
- return smoothstep(-0.1, 0.1, d);
- } else {
- return 0.0;
- }
- }
- """,
- CARET_DOWN: """
- float alphaSymbol(vec2 coord, float size) {
- coord = size * (coord - 0.5);
- float d = abs(coord.y) - abs(coord.x);
- if (d >= -0.1 && coord.y < 0.5) {
- return smoothstep(-0.1, 0.1, d);
- } else {
- return 0.0;
- }
- }
- """,
- }
-
- _FRAGMENT_SHADER_TEMPLATE = """
- #version 120
-
- uniform float size;
-
- varying vec4 vColor;
-
- %s
-
- void main(void) {
- float alpha = alphaSymbol(gl_PointCoord, size);
- if (alpha <= 0.0) {
- discard;
- } else {
- gl_FragColor = vec4(vColor.rgb, alpha * clamp(vColor.a, 0.0, 1.0));
- }
- }
- """
-
- _PROGRAMS = {}
-
- def __init__(self, xVboData=None, yVboData=None, colorVboData=None,
- marker=SQUARE, color=(0., 0., 0., 1.), size=7,
- offset=(0., 0.)):
- self.color = color
- self._marker = None
- self.marker = marker
- self.size = size
- self.offset = offset
-
- self.xVboData = xVboData
- self.yVboData = yVboData
- self.colorVboData = colorVboData
- self.useColorVboData = colorVboData is not None
-
- @property
- def marker(self):
- """Symbol used to display markers (str)"""
- return self._marker
-
- @marker.setter
- def marker(self, marker):
- if marker in _MPL_NONES:
- self._marker = None
- else:
- assert marker in self.MARKERS
- self._marker = marker
-
- @classmethod
- def _getProgram(cls, marker):
- """On-demand shader program creation."""
- if marker == PIXEL:
- marker = SQUARE
- elif marker == POINT:
- marker = CIRCLE
-
- if marker not in cls._PROGRAMS:
- cls._PROGRAMS[marker] = Program(
- vertexShader=cls._VERTEX_SHADER,
- fragmentShader=(cls._FRAGMENT_SHADER_TEMPLATE %
- cls._FRAGMENT_SHADER_SYMBOLS[marker]),
- attrib0='xPos')
-
- return cls._PROGRAMS[marker]
-
- @classmethod
- def init(cls):
- """OpenGL context initialization"""
- version = gl.glGetString(gl.GL_VERSION)
- majorVersion = int(version[0])
- assert majorVersion >= 2
- gl.glEnable(gl.GL_VERTEX_PROGRAM_POINT_SIZE) # OpenGL 2
- gl.glEnable(gl.GL_POINT_SPRITE) # OpenGL 2
- if majorVersion >= 3: # OpenGL 3
- gl.glEnable(gl.GL_PROGRAM_POINT_SIZE)
-
- def render(self, context):
- """Perform rendering
-
- :param RenderContext context:
- """
- if self.marker is None:
- return
-
- program = self._getProgram(self.marker)
- program.use()
-
- matrix = numpy.dot(context.matrix,
- mat4Translate(*self.offset)).astype(numpy.float32)
- gl.glUniformMatrix4fv(program.uniforms['matrix'], 1, gl.GL_TRUE, matrix)
-
- if self.marker == PIXEL:
- size = 1
- elif self.marker == POINT:
- size = math.ceil(0.5 * self.size) + 1 # Mimic Matplotlib point
- else:
- size = self.size
- size = size / 72. * context.dpi
-
- if self.marker in (PLUS, H_LINE, V_LINE,
- TICK_LEFT, TICK_RIGHT, TICK_UP, TICK_DOWN):
- # Convert to nearest odd number
- size = size // 2 * 2 + 1.
-
- gl.glUniform1f(program.uniforms['size'], size)
- # gl.glPointSize(self.size)
-
- cAttrib = program.attributes['color']
- if self.useColorVboData and self.colorVboData is not None:
- gl.glEnableVertexAttribArray(cAttrib)
- self.colorVboData.setVertexAttrib(cAttrib)
- else:
- gl.glDisableVertexAttribArray(cAttrib)
- gl.glVertexAttrib4f(cAttrib, *self.color)
-
- xAttrib = program.attributes['xPos']
- gl.glEnableVertexAttribArray(xAttrib)
- self.xVboData.setVertexAttrib(xAttrib)
-
- yAttrib = program.attributes['yPos']
- gl.glEnableVertexAttribArray(yAttrib)
- self.yVboData.setVertexAttrib(yAttrib)
-
- gl.glDrawArrays(gl.GL_POINTS, 0, self.xVboData.size)
-
- gl.glUseProgram(0)
-
-
-# error bars ##################################################################
-
-class _ErrorBars(object):
- """Display errors bars.
-
- This is using its own VBO as opposed to fill/points/lines.
- There is no picking on error bars.
-
- It uses 2 vertices per error bars and uses :class:`GLLines2D` to
- render error bars and :class:`_Points2D` to render the ends.
-
- :param numpy.ndarray xData: X coordinates of the data.
- :param numpy.ndarray yData: Y coordinates of the data.
- :param xError: The absolute error on the X axis.
- :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 negative errors,
- row 1 for positive errors.
- :param yError: The absolute error on the Y axis.
- :type yError: A float, or a numpy.ndarray of float32. See xError.
- :param float xMin: The min X value already computed by GLPlotCurve2D.
- :param float yMin: The min Y value already computed by GLPlotCurve2D.
- :param List[float] color: RGBA color as 4 float in [0, 1]
- :param List[float] offset: Translation of coordinates (ox, oy)
- """
-
- def __init__(self, xData, yData, xError, yError,
- xMin, yMin,
- color=(0., 0., 0., 1.),
- offset=(0., 0.)):
- self._attribs = None
- self._xMin, self._yMin = xMin, yMin
- self.offset = offset
-
- if xError is not None or yError is not None:
- self._xData = numpy.array(
- xData, order='C', dtype=numpy.float32, copy=False)
- self._yData = numpy.array(
- yData, order='C', dtype=numpy.float32, copy=False)
-
- # This also works if xError, yError is a float/int
- self._xError = numpy.array(
- xError, order='C', dtype=numpy.float32, copy=False)
- self._yError = numpy.array(
- yError, order='C', dtype=numpy.float32, copy=False)
- else:
- self._xData, self._yData = None, None
- self._xError, self._yError = None, None
-
- self._lines = GLLines2D(
- None, None, color=color, drawMode=gl.GL_LINES, offset=offset)
- self._xErrPoints = _Points2D(
- None, None, color=color, marker=V_LINE, offset=offset)
- self._yErrPoints = _Points2D(
- None, None, color=color, marker=H_LINE, offset=offset)
-
- def _buildVertices(self):
- """Generates error bars vertices"""
- nbLinesPerDataPts = (0 if self._xError is None else 2) + \
- (0 if self._yError is None else 2)
-
- nbDataPts = len(self._xData)
-
- # interleave coord+error, coord-error.
- # xError vertices first if any, then yError vertices if any.
- xCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2,
- dtype=numpy.float32)
- yCoords = numpy.empty(nbDataPts * nbLinesPerDataPts * 2,
- dtype=numpy.float32)
-
- if self._xError is not None: # errors on the X axis
- if len(self._xError.shape) == 2:
- xErrorMinus, xErrorPlus = self._xError[0], self._xError[1]
- else:
- # numpy arrays of len 1 or len(xData)
- xErrorMinus, xErrorPlus = self._xError, self._xError
-
- # Interleave vertices for xError
- endXError = 4 * nbDataPts
- xCoords[0:endXError-3:4] = self._xData + xErrorPlus
- xCoords[1:endXError-2:4] = self._xData
- xCoords[2:endXError-1:4] = self._xData
- xCoords[3:endXError:4] = self._xData - xErrorMinus
-
- yCoords[0:endXError-3:4] = self._yData
- yCoords[1:endXError-2:4] = self._yData
- yCoords[2:endXError-1:4] = self._yData
- yCoords[3:endXError:4] = self._yData
-
- else:
- endXError = 0
-
- if self._yError is not None: # errors on the Y axis
- if len(self._yError.shape) == 2:
- yErrorMinus, yErrorPlus = self._yError[0], self._yError[1]
- else:
- # numpy arrays of len 1 or len(yData)
- yErrorMinus, yErrorPlus = self._yError, self._yError
-
- # Interleave vertices for yError
- xCoords[endXError::4] = self._xData
- xCoords[endXError+1::4] = self._xData
- xCoords[endXError+2::4] = self._xData
- xCoords[endXError+3::4] = self._xData
-
- yCoords[endXError::4] = self._yData + yErrorPlus
- yCoords[endXError+1::4] = self._yData
- yCoords[endXError+2::4] = self._yData
- yCoords[endXError+3::4] = self._yData - yErrorMinus
-
- return xCoords, yCoords
-
- def prepare(self):
- """Rendering preparation: build indices and bounding box vertices"""
- if self._xData is None:
- return
-
- if self._attribs is None:
- xCoords, yCoords = self._buildVertices()
-
- xAttrib, yAttrib = vertexBuffer((xCoords, yCoords))
- self._attribs = xAttrib, yAttrib
-
- self._lines.xVboData = xAttrib
- self._lines.yVboData = yAttrib
-
- # Set xError points using the same VBO as lines
- self._xErrPoints.xVboData = xAttrib.copy()
- self._xErrPoints.xVboData.size //= 2
- self._xErrPoints.yVboData = yAttrib.copy()
- self._xErrPoints.yVboData.size //= 2
-
- # Set yError points using the same VBO as lines
- self._yErrPoints.xVboData = xAttrib.copy()
- self._yErrPoints.xVboData.size //= 2
- self._yErrPoints.xVboData.offset += (xAttrib.itemsize *
- xAttrib.size // 2)
- self._yErrPoints.yVboData = yAttrib.copy()
- self._yErrPoints.yVboData.size //= 2
- self._yErrPoints.yVboData.offset += (yAttrib.itemsize *
- yAttrib.size // 2)
-
- def render(self, context):
- """Perform rendering
-
- :param RenderContext context:
- """
- self.prepare()
-
- if self._attribs is not None:
- self._lines.render(context)
- self._xErrPoints.render(context)
- self._yErrPoints.render(context)
-
- def discard(self):
- """Release VBOs"""
- if self.isInitialized():
- self._lines.xVboData, self._lines.yVboData = None, None
- self._xErrPoints.xVboData, self._xErrPoints.yVboData = None, None
- self._yErrPoints.xVboData, self._yErrPoints.yVboData = None, None
- self._attribs[0].vbo.discard()
- self._attribs = None
-
- def isInitialized(self):
- return self._attribs is not None
-
-
-# curves ######################################################################
-
-def _proxyProperty(*componentsAttributes):
- """Create a property to access an attribute of attribute(s).
- Useful for composition.
- Supports multiple components this way:
- getter returns the first found, setter sets all
- """
- def getter(self):
- for compName, attrName in componentsAttributes:
- try:
- component = getattr(self, compName)
- except AttributeError:
- pass
- else:
- return getattr(component, attrName)
-
- def setter(self, value):
- for compName, attrName in componentsAttributes:
- component = getattr(self, compName)
- setattr(component, attrName, value)
- return property(getter, setter)
-
-
-class GLPlotCurve2D(GLPlotItem):
- def __init__(self, xData, yData, colorData=None,
- xError=None, yError=None,
- lineStyle=SOLID,
- lineColor=(0., 0., 0., 1.),
- lineWidth=1,
- lineDashPeriod=20,
- marker=SQUARE,
- markerColor=(0., 0., 0., 1.),
- markerSize=7,
- fillColor=None,
- baseline=None,
- isYLog=False):
- super().__init__()
- self.colorData = colorData
-
- # Compute x bounds
- if xError is None:
- self.xMin, self.xMax = min_max(xData, min_positive=False)
- else:
- # Takes the error into account
- if hasattr(xError, 'shape') and len(xError.shape) == 2:
- xErrorMinus, xErrorPlus = xError[0], xError[1]
- else:
- xErrorMinus, xErrorPlus = xError, xError
- self.xMin = numpy.nanmin(xData - xErrorMinus)
- self.xMax = numpy.nanmax(xData + xErrorPlus)
-
- # Compute y bounds
- if yError is None:
- self.yMin, self.yMax = min_max(yData, min_positive=False)
- else:
- # Takes the error into account
- if hasattr(yError, 'shape') and len(yError.shape) == 2:
- yErrorMinus, yErrorPlus = yError[0], yError[1]
- else:
- yErrorMinus, yErrorPlus = yError, yError
- self.yMin = numpy.nanmin(yData - yErrorMinus)
- self.yMax = numpy.nanmax(yData + yErrorPlus)
-
- # Handle data offset
- if xData.itemsize > 4 or yData.itemsize > 4: # Use normalization
- # offset data, do not offset error as it is relative
- self.offset = self.xMin, self.yMin
- self.xData = (xData - self.offset[0]).astype(numpy.float32)
- self.yData = (yData - self.offset[1]).astype(numpy.float32)
-
- else: # float32
- self.offset = 0., 0.
- self.xData = xData
- self.yData = yData
- if fillColor is not None:
- def deduce_baseline(baseline):
- if baseline is None:
- _baseline = 0
- else:
- _baseline = baseline
- if not isinstance(_baseline, numpy.ndarray):
- _baseline = numpy.repeat(_baseline,
- len(self.xData))
- if isYLog is True:
- with numpy.errstate(divide='ignore', invalid='ignore'):
- log_val = numpy.log10(_baseline)
- _baseline = numpy.where(_baseline>0.0, log_val, -38)
- return _baseline
-
- _baseline = deduce_baseline(baseline)
-
- # Use different baseline depending of Y log scale
- self.fill = _Fill2D(self.xData, self.yData,
- baseline=_baseline,
- color=fillColor,
- offset=self.offset)
- else:
- self.fill = None
-
- self._errorBars = _ErrorBars(self.xData, self.yData,
- xError, yError,
- self.xMin, self.yMin,
- offset=self.offset)
-
- self.lines = GLLines2D()
- self.lines.style = lineStyle
- self.lines.color = lineColor
- self.lines.width = lineWidth
- self.lines.dashPeriod = lineDashPeriod
- self.lines.offset = self.offset
-
- self.points = _Points2D()
- self.points.marker = marker
- self.points.color = markerColor
- self.points.size = markerSize
- self.points.offset = self.offset
-
- xVboData = _proxyProperty(('lines', 'xVboData'), ('points', 'xVboData'))
-
- yVboData = _proxyProperty(('lines', 'yVboData'), ('points', 'yVboData'))
-
- colorVboData = _proxyProperty(('lines', 'colorVboData'),
- ('points', 'colorVboData'))
-
- useColorVboData = _proxyProperty(('lines', 'useColorVboData'),
- ('points', 'useColorVboData'))
-
- distVboData = _proxyProperty(('lines', 'distVboData'))
-
- lineStyle = _proxyProperty(('lines', 'style'))
-
- lineColor = _proxyProperty(('lines', 'color'))
-
- lineWidth = _proxyProperty(('lines', 'width'))
-
- lineDashPeriod = _proxyProperty(('lines', 'dashPeriod'))
-
- marker = _proxyProperty(('points', 'marker'))
-
- markerColor = _proxyProperty(('points', 'color'))
-
- markerSize = _proxyProperty(('points', 'size'))
-
- @classmethod
- def init(cls):
- """OpenGL context initialization"""
- GLLines2D.init()
- _Points2D.init()
-
- def prepare(self):
- """Rendering preparation: build indices and bounding box vertices"""
- if self.xVboData is None:
- xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None
- if self.lineStyle in (DASHED, DASHDOT, DOTTED):
- dists = distancesFromArrays(self.xData, self.yData)
- if self.colorData is None:
- xAttrib, yAttrib, dAttrib = vertexBuffer(
- (self.xData, self.yData, dists))
- else:
- xAttrib, yAttrib, cAttrib, dAttrib = vertexBuffer(
- (self.xData, self.yData, self.colorData, dists))
- elif self.colorData is None:
- xAttrib, yAttrib = vertexBuffer((self.xData, self.yData))
- else:
- xAttrib, yAttrib, cAttrib = vertexBuffer(
- (self.xData, self.yData, self.colorData))
-
- self.xVboData = xAttrib
- self.yVboData = yAttrib
- self.distVboData = dAttrib
-
- if cAttrib is not None and self.colorData.dtype.kind == 'u':
- cAttrib.normalization = True # Normalize uint to [0, 1]
- self.colorVboData = cAttrib
- self.useColorVboData = cAttrib is not None
-
- def render(self, context):
- """Perform rendering
-
- :param RenderContext context: Rendering information
- """
- self.prepare()
- if self.fill is not None:
- self.fill.render(context)
- self._errorBars.render(context)
- self.lines.render(context)
- self.points.render(context)
-
- def discard(self):
- """Release VBOs"""
- if self.xVboData is not None:
- self.xVboData.vbo.discard()
-
- self.xVboData = None
- self.yVboData = None
- self.colorVboData = None
- self.distVboData = None
-
- self._errorBars.discard()
- if self.fill is not None:
- self.fill.discard()
-
- def isInitialized(self):
- return (self.xVboData is not None or
- self._errorBars.isInitialized() or
- (self.fill is not None and self.fill.isInitialized()))
-
- def pick(self, xPickMin, yPickMin, xPickMax, yPickMax):
- """Perform picking on the curve according to its rendering.
-
- The picking area is [xPickMin, xPickMax], [yPickMin, yPickMax].
-
- In case a segment between 2 points with indices i, i+1 is picked,
- only its lower index end point (i.e., i) is added to the result.
- In case an end point with index i is picked it is added to the result,
- and the segment [i-1, i] is not tested for picking.
-
- :return: The indices of the picked data
- :rtype: Union[List[int],None]
- """
- if (self.marker is None and self.lineStyle is None) or \
- self.xMin > xPickMax or xPickMin > self.xMax or \
- self.yMin > yPickMax or yPickMin > self.yMax:
- return None
-
- # offset picking bounds
- xPickMin = xPickMin - self.offset[0]
- xPickMax = xPickMax - self.offset[0]
- yPickMin = yPickMin - self.offset[1]
- yPickMax = yPickMax - self.offset[1]
-
- if self.lineStyle is not None:
- # Using Cohen-Sutherland algorithm for line clipping
- with numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings
- codes = ((self.yData > yPickMax) << 3) | \
- ((self.yData < yPickMin) << 2) | \
- ((self.xData > xPickMax) << 1) | \
- (self.xData < xPickMin)
-
- notNaN = numpy.logical_not(numpy.logical_or(
- numpy.isnan(self.xData), numpy.isnan(self.yData)))
-
- # Add all points that are inside the picking area
- indices = numpy.nonzero(
- numpy.logical_and(codes == 0, notNaN))[0].tolist()
-
- # Segment that might cross the area with no end point inside it
- segToTestIdx = numpy.nonzero((codes[:-1] != 0) &
- (codes[1:] != 0) &
- ((codes[:-1] & codes[1:]) == 0))[0]
-
- TOP, BOTTOM, RIGHT, LEFT = (1 << 3), (1 << 2), (1 << 1), (1 << 0)
-
- for index in segToTestIdx:
- if index not in indices:
- x0, y0 = self.xData[index], self.yData[index]
- x1, y1 = self.xData[index + 1], self.yData[index + 1]
- code1 = codes[index + 1]
-
- # check for crossing with horizontal bounds
- # y0 == y1 is a never event:
- # => pt0 and pt1 in same vertical area are not in segToTest
- if code1 & TOP:
- x = x0 + (x1 - x0) * (yPickMax - y0) / (y1 - y0)
- elif code1 & BOTTOM:
- x = x0 + (x1 - x0) * (yPickMin - y0) / (y1 - y0)
- else:
- x = None # No horizontal bounds intersection test
-
- if x is not None and xPickMin <= x <= xPickMax:
- # Intersection
- indices.append(index)
-
- else:
- # check for crossing with vertical bounds
- # x0 == x1 is a never event (see remark for y)
- if code1 & RIGHT:
- y = y0 + (y1 - y0) * (xPickMax - x0) / (x1 - x0)
- elif code1 & LEFT:
- y = y0 + (y1 - y0) * (xPickMin - x0) / (x1 - x0)
- else:
- y = None # No vertical bounds intersection test
-
- if y is not None and yPickMin <= y <= yPickMax:
- # Intersection
- indices.append(index)
-
- indices.sort()
-
- else:
- with numpy.errstate(invalid='ignore'): # Ignore NaN comparison warnings
- indices = numpy.nonzero((self.xData >= xPickMin) &
- (self.xData <= xPickMax) &
- (self.yData >= yPickMin) &
- (self.yData <= yPickMax))[0].tolist()
-
- return tuple(indices) if len(indices) > 0 else None
diff --git a/silx/gui/plot/backends/glutils/GLPlotFrame.py b/silx/gui/plot/backends/glutils/GLPlotFrame.py
deleted file mode 100644
index c5ee75b..0000000
--- a/silx/gui/plot/backends/glutils/GLPlotFrame.py
+++ /dev/null
@@ -1,1219 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2018 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 the rendering of plot titles, axes and grid.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/04/2017"
-
-
-# TODO
-# keep aspect ratio managed here?
-# smarter dirty flag handling?
-
-import datetime as dt
-import math
-import weakref
-import logging
-from collections import namedtuple
-
-import numpy
-
-from ...._glutils import gl, Program
-from ..._utils import FLOAT32_SAFE_MIN, FLOAT32_MINPOS, FLOAT32_SAFE_MAX
-from .GLSupport import mat4Ortho
-from .GLText import Text2D, CENTER, BOTTOM, TOP, LEFT, RIGHT, ROTATE_270
-from ..._utils.ticklayout import niceNumbersAdaptative, niceNumbersForLog10
-from ..._utils.dtime_ticklayout import calcTicksAdaptive, bestFormatString
-from ..._utils.dtime_ticklayout import timestamp
-
-_logger = logging.getLogger(__name__)
-
-
-# PlotAxis ####################################################################
-
-class PlotAxis(object):
- """Represents a 1D axis of the plot.
- This class is intended to be used with :class:`GLPlotFrame`.
- """
-
- def __init__(self, plotFrame,
- tickLength=(0., 0.),
- foregroundColor=(0., 0., 0., 1.0),
- labelAlign=CENTER, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=CENTER,
- titleRotate=0, titleOffset=(0., 0.)):
- self._ticks = None
-
- self._plotFrameRef = weakref.ref(plotFrame)
-
- self._isDateTime = False
- self._timeZone = None
- self._isLog = False
- self._dataRange = 1., 100.
- self._displayCoords = (0., 0.), (1., 0.)
- self._title = ''
-
- self._tickLength = tickLength
- self._foregroundColor = foregroundColor
- self._labelAlign = labelAlign
- self._labelVAlign = labelVAlign
- self._titleAlign = titleAlign
- self._titleVAlign = titleVAlign
- self._titleRotate = titleRotate
- self._titleOffset = titleOffset
-
- @property
- def dataRange(self):
- """The range of the data represented on the axis as a tuple
- of 2 floats: (min, max)."""
- return self._dataRange
-
- @dataRange.setter
- def dataRange(self, dataRange):
- assert len(dataRange) == 2
- assert dataRange[0] <= dataRange[1]
- dataRange = float(dataRange[0]), float(dataRange[1])
-
- if dataRange != self._dataRange:
- self._dataRange = dataRange
- self._dirtyTicks()
-
- @property
- def isLog(self):
- """Whether the axis is using a log10 scale or not as a bool."""
- return self._isLog
-
- @isLog.setter
- def isLog(self, isLog):
- isLog = bool(isLog)
- if isLog != self._isLog:
- self._isLog = isLog
- self._dirtyTicks()
-
- @property
- def timeZone(self):
- """Returnss datetime.tzinfo that is used if this axis plots date times."""
- return self._timeZone
-
- @timeZone.setter
- def timeZone(self, tz):
- """Sets dateetime.tzinfo that is used if this axis plots date times."""
- self._timeZone = tz
- self._dirtyTicks()
-
- @property
- def isTimeSeries(self):
- """Whether the axis is showing floats as datetime objects"""
- return self._isDateTime
-
- @isTimeSeries.setter
- def isTimeSeries(self, isTimeSeries):
- isTimeSeries = bool(isTimeSeries)
- if isTimeSeries != self._isDateTime:
- self._isDateTime = isTimeSeries
- self._dirtyTicks()
-
- @property
- def displayCoords(self):
- """The coordinates of the start and end points of the axis
- in display space (i.e., in pixels) as a tuple of 2 tuples of
- 2 floats: ((x0, y0), (x1, y1)).
- """
- return self._displayCoords
-
- @displayCoords.setter
- def displayCoords(self, displayCoords):
- assert len(displayCoords) == 2
- assert len(displayCoords[0]) == 2
- assert len(displayCoords[1]) == 2
- displayCoords = tuple(displayCoords[0]), tuple(displayCoords[1])
- if displayCoords != self._displayCoords:
- self._displayCoords = displayCoords
- self._dirtyTicks()
-
- @property
- def devicePixelRatio(self):
- """Returns the ratio between qt pixels and device pixels."""
- plotFrame = self._plotFrameRef()
- return plotFrame.devicePixelRatio if plotFrame is not None else 1.
-
- @property
- def title(self):
- """The text label associated with this axis as a str in latin-1."""
- return self._title
-
- @title.setter
- def title(self, title):
- if title != self._title:
- self._title = title
- self._dirtyPlotFrame()
-
- @property
- def titleOffset(self):
- """Title offset in pixels (x: int, y: int)"""
- return self._titleOffset
-
- @titleOffset.setter
- def titleOffset(self, offset):
- if offset != self._titleOffset:
- self._titleOffset = offset
- self._dirtyTicks()
-
- @property
- def foregroundColor(self):
- """Color used for frame and labels"""
- return self._foregroundColor
-
- @foregroundColor.setter
- def foregroundColor(self, color):
- """Color used for frame and labels"""
- assert len(color) == 4, \
- "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
- if self._foregroundColor != color:
- self._foregroundColor = color
- self._dirtyTicks()
-
- @property
- def ticks(self):
- """Ticks as tuples: ((x, y) in display, dataPos, textLabel)."""
- if self._ticks is None:
- self._ticks = tuple(self._ticksGenerator())
- return self._ticks
-
- def getVerticesAndLabels(self):
- """Create the list of vertices for axis and associated text labels.
-
- :returns: A tuple: List of 2D line vertices, List of Text2D labels.
- """
- vertices = list(self.displayCoords) # Add start and end points
- labels = []
- tickLabelsSize = [0., 0.]
-
- xTickLength, yTickLength = self._tickLength
- xTickLength *= self.devicePixelRatio
- yTickLength *= self.devicePixelRatio
- for (xPixel, yPixel), dataPos, text in self.ticks:
- if text is None:
- tickScale = 0.5
- else:
- tickScale = 1.
-
- label = Text2D(text=text,
- color=self._foregroundColor,
- x=xPixel - xTickLength,
- y=yPixel - yTickLength,
- align=self._labelAlign,
- valign=self._labelVAlign,
- devicePixelRatio=self.devicePixelRatio)
-
- width, height = label.size
- if width > tickLabelsSize[0]:
- tickLabelsSize[0] = width
- if height > tickLabelsSize[1]:
- tickLabelsSize[1] = height
-
- labels.append(label)
-
- vertices.append((xPixel, yPixel))
- vertices.append((xPixel + tickScale * xTickLength,
- yPixel + tickScale * yTickLength))
-
- (x0, y0), (x1, y1) = self.displayCoords
- xAxisCenter = 0.5 * (x0 + x1)
- yAxisCenter = 0.5 * (y0 + y1)
-
- xOffset, yOffset = self.titleOffset
-
- # Adaptative title positioning:
- # tickNorm = math.sqrt(xTickLength ** 2 + yTickLength ** 2)
- # xOffset = -tickLabelsSize[0] * xTickLength / tickNorm
- # xOffset -= 3 * xTickLength
- # yOffset = -tickLabelsSize[1] * yTickLength / tickNorm
- # yOffset -= 3 * yTickLength
-
- axisTitle = Text2D(text=self.title,
- color=self._foregroundColor,
- x=xAxisCenter + xOffset,
- y=yAxisCenter + yOffset,
- align=self._titleAlign,
- valign=self._titleVAlign,
- rotate=self._titleRotate,
- devicePixelRatio=self.devicePixelRatio)
- labels.append(axisTitle)
-
- return vertices, labels
-
- def _dirtyPlotFrame(self):
- """Dirty parent GLPlotFrame"""
- plotFrame = self._plotFrameRef()
- if plotFrame is not None:
- plotFrame._dirty()
-
- def _dirtyTicks(self):
- """Mark ticks as dirty and notify listener (i.e., background)."""
- self._ticks = None
- self._dirtyPlotFrame()
-
- @staticmethod
- def _frange(start, stop, step):
- """range for float (including stop)."""
- while start <= stop:
- yield start
- start += step
-
- def _ticksGenerator(self):
- """Generator of ticks as tuples:
- ((x, y) in display, dataPos, textLabel).
- """
- dataMin, dataMax = self.dataRange
- if self.isLog and dataMin <= 0.:
- _logger.warning(
- 'Getting ticks while isLog=True and dataRange[0]<=0.')
- dataMin = 1.
- if dataMax < dataMin:
- dataMax = 1.
-
- if dataMin != dataMax: # data range is not null
- (x0, y0), (x1, y1) = self.displayCoords
-
- if self.isLog:
-
- if self.isTimeSeries:
- _logger.warning("Time series not implemented for log-scale")
-
- logMin, logMax = math.log10(dataMin), math.log10(dataMax)
- tickMin, tickMax, step, _ = niceNumbersForLog10(logMin, logMax)
-
- xScale = (x1 - x0) / (logMax - logMin)
- yScale = (y1 - y0) / (logMax - logMin)
-
- for logPos in self._frange(tickMin, tickMax, step):
- if logMin <= logPos <= logMax:
- dataPos = 10 ** logPos
- xPixel = x0 + (logPos - logMin) * xScale
- yPixel = y0 + (logPos - logMin) * yScale
- text = '1e%+03d' % logPos
- yield ((xPixel, yPixel), dataPos, text)
-
- if step == 1:
- ticks = list(self._frange(tickMin, tickMax, step))[:-1]
- for logPos in ticks:
- dataOrigPos = 10 ** logPos
- for index in range(2, 10):
- dataPos = dataOrigPos * index
- if dataMin <= dataPos <= dataMax:
- logSubPos = math.log10(dataPos)
- xPixel = x0 + (logSubPos - logMin) * xScale
- yPixel = y0 + (logSubPos - logMin) * yScale
- yield ((xPixel, yPixel), dataPos, None)
-
- else:
- xScale = (x1 - x0) / (dataMax - dataMin)
- yScale = (y1 - y0) / (dataMax - dataMin)
-
- nbPixels = math.sqrt(pow(x1 - x0, 2) + pow(y1 - y0, 2)) / self.devicePixelRatio
-
- # Density of 1.3 label per 92 pixels
- # i.e., 1.3 label per inch on a 92 dpi screen
- tickDensity = 1.3 / 92
-
- if not self.isTimeSeries:
- tickMin, tickMax, step, nbFrac = niceNumbersAdaptative(
- dataMin, dataMax, nbPixels, tickDensity)
-
- for dataPos in self._frange(tickMin, tickMax, step):
- if dataMin <= dataPos <= dataMax:
- xPixel = x0 + (dataPos - dataMin) * xScale
- yPixel = y0 + (dataPos - dataMin) * yScale
-
- if nbFrac == 0:
- text = '%g' % dataPos
- else:
- text = ('%.' + str(nbFrac) + 'f') % dataPos
- yield ((xPixel, yPixel), dataPos, text)
- else:
- # Time series
- dtMin = dt.datetime.fromtimestamp(dataMin, tz=self.timeZone)
- dtMax = dt.datetime.fromtimestamp(dataMax, tz=self.timeZone)
-
- tickDateTimes, spacing, unit = calcTicksAdaptive(
- dtMin, dtMax, nbPixels, tickDensity)
-
- for tickDateTime in tickDateTimes:
- if dtMin <= tickDateTime <= dtMax:
-
- dataPos = timestamp(tickDateTime)
- xPixel = x0 + (dataPos - dataMin) * xScale
- yPixel = y0 + (dataPos - dataMin) * yScale
-
- fmtStr = bestFormatString(spacing, unit)
- text = tickDateTime.strftime(fmtStr)
-
- yield ((xPixel, yPixel), dataPos, text)
-
-
-# GLPlotFrame #################################################################
-
-class GLPlotFrame(object):
- """Base class for rendering a 2D frame surrounded by axes."""
-
- _TICK_LENGTH_IN_PIXELS = 5
- _LINE_WIDTH = 1
-
- _SHADERS = {
- 'vertex': """
- attribute vec2 position;
- uniform mat4 matrix;
-
- void main(void) {
- gl_Position = matrix * vec4(position, 0.0, 1.0);
- }
- """,
- 'fragment': """
- uniform vec4 color;
- uniform float tickFactor; /* = 1./tickLength or 0. for solid line */
-
- void main(void) {
- if (mod(tickFactor * (gl_FragCoord.x + gl_FragCoord.y), 2.) < 1.) {
- gl_FragColor = color;
- } else {
- discard;
- }
- }
- """
- }
-
- _Margins = namedtuple('Margins', ('left', 'right', 'top', 'bottom'))
-
- # Margins used when plot frame is not displayed
- _NoDisplayMargins = _Margins(0, 0, 0, 0)
-
- def __init__(self, marginRatios, foregroundColor, gridColor):
- """
- :param List[float] marginRatios:
- The ratios of margins around plot area for axis and labels.
- (left, top, right, bottom) as float in [0., 1.]
- :param foregroundColor: color used for the frame and labels.
- :type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0
- :param gridColor: color used for grid lines.
- :type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0
- """
- self._renderResources = None
-
- self.__marginRatios = marginRatios
- self.__marginsCache = None
-
- self._foregroundColor = foregroundColor
- self._gridColor = gridColor
-
- self.axes = [] # List of PlotAxis to be updated by subclasses
-
- self._grid = False
- self._size = 0., 0.
- self._title = ''
-
- self._devicePixelRatio = 1.
-
- @property
- def isDirty(self):
- """True if it need to refresh graphic rendering, False otherwise."""
- return self._renderResources is None
-
- GRID_NONE = 0
- GRID_MAIN_TICKS = 1
- GRID_SUB_TICKS = 2
- GRID_ALL_TICKS = (GRID_MAIN_TICKS + GRID_SUB_TICKS)
-
- @property
- def foregroundColor(self):
- """Color used for frame and labels"""
- return self._foregroundColor
-
- @foregroundColor.setter
- def foregroundColor(self, color):
- """Color used for frame and labels"""
- assert len(color) == 4, \
- "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
- if self._foregroundColor != color:
- self._foregroundColor = color
- for axis in self.axes:
- axis.foregroundColor = color
- self._dirty()
-
- @property
- def gridColor(self):
- """Color used for frame and labels"""
- return self._gridColor
-
- @gridColor.setter
- def gridColor(self, color):
- """Color used for frame and labels"""
- assert len(color) == 4, \
- "gridColor must have length 4, got {}".format(len(self._gridColor))
- if self._gridColor != color:
- self._gridColor = color
- self._dirty()
-
- @property
- def marginRatios(self):
- """Plot margin ratios: (left, top, right, bottom) as 4 float in [0, 1].
- """
- return self.__marginRatios
-
- @marginRatios.setter
- def marginRatios(self, ratios):
- ratios = tuple(float(v) for v in ratios)
- assert len(ratios) == 4
- for value in ratios:
- assert 0. <= value <= 1.
- assert ratios[0] + ratios[2] < 1.
- assert ratios[1] + ratios[3] < 1.
-
- if self.__marginRatios != ratios:
- self.__marginRatios = ratios
- self.__marginsCache = None # Clear cached margins
- self._dirty()
-
- @property
- def margins(self):
- """Margins in pixels around the plot."""
- if self.__marginsCache is None:
- width, height = self.size
- left, top, right, bottom = self.marginRatios
- self.__marginsCache = self._Margins(
- left=int(left*width),
- right=int(right*width),
- top=int(top*height),
- bottom=int(bottom*height))
- return self.__marginsCache
-
- @property
- def devicePixelRatio(self):
- return self._devicePixelRatio
-
- @devicePixelRatio.setter
- def devicePixelRatio(self, ratio):
- if ratio != self._devicePixelRatio:
- self._devicePixelRatio = ratio
- self._dirty()
-
- @property
- def grid(self):
- """Grid display mode:
- - 0: No grid.
- - 1: Grid on main ticks.
- - 2: Grid on sub-ticks for log scale axes.
- - 3: Grid on main and sub ticks."""
- return self._grid
-
- @grid.setter
- def grid(self, grid):
- assert grid in (self.GRID_NONE, self.GRID_MAIN_TICKS,
- self.GRID_SUB_TICKS, self.GRID_ALL_TICKS)
- if grid != self._grid:
- self._grid = grid
- self._dirty()
-
- @property
- def size(self):
- """Size in device pixels of the plot area including margins."""
- return self._size
-
- @size.setter
- def size(self, size):
- assert len(size) == 2
- size = tuple(size)
- if size != self._size:
- self._size = size
- self.__marginsCache = None # Clear cached margins
- self._dirty()
-
- @property
- def plotOrigin(self):
- """Plot area origin (left, top) in widget coordinates in pixels."""
- return self.margins.left, self.margins.top
-
- @property
- def plotSize(self):
- """Plot area size (width, height) in pixels."""
- w, h = self.size
- w -= self.margins.left + self.margins.right
- h -= self.margins.top + self.margins.bottom
- return w, h
-
- @property
- def title(self):
- """Main title as a str in latin-1."""
- return self._title
-
- @title.setter
- def title(self, title):
- if title != self._title:
- self._title = title
- self._dirty()
-
- # In-place update
- # if self._renderResources is not None:
- # self._renderResources[-1][-1].text = title
-
- def _dirty(self):
- # When Text2D require discard we need to handle it
- self._renderResources = None
-
- def _buildGridVertices(self):
- if self._grid == self.GRID_NONE:
- return []
-
- elif self._grid == self.GRID_MAIN_TICKS:
- def test(text):
- return text is not None
- elif self._grid == self.GRID_SUB_TICKS:
- def test(text):
- return text is None
- elif self._grid == self.GRID_ALL_TICKS:
- def test(_):
- return True
- else:
- logging.warning('Wrong grid mode: %d' % self._grid)
- return []
-
- return self._buildGridVerticesWithTest(test)
-
- def _buildGridVerticesWithTest(self, test):
- """Override in subclass to generate grid vertices"""
- return []
-
- def _buildVerticesAndLabels(self):
- # To fill with copy of axes lists
- vertices = []
- labels = []
-
- for axis in self.axes:
- axisVertices, axisLabels = axis.getVerticesAndLabels()
- vertices += axisVertices
- labels += axisLabels
-
- vertices = numpy.array(vertices, dtype=numpy.float32)
-
- # Add main title
- xTitle = (self.size[0] + self.margins.left -
- self.margins.right) // 2
- yTitle = self.margins.top - self._TICK_LENGTH_IN_PIXELS
- labels.append(Text2D(text=self.title,
- color=self._foregroundColor,
- x=xTitle,
- y=yTitle,
- align=CENTER,
- valign=BOTTOM,
- devicePixelRatio=self.devicePixelRatio))
-
- # grid
- gridVertices = numpy.array(self._buildGridVertices(),
- dtype=numpy.float32)
-
- self._renderResources = (vertices, gridVertices, labels)
-
- _program = Program(
- _SHADERS['vertex'], _SHADERS['fragment'], attrib0='position')
-
- def render(self):
- if self.margins == self._NoDisplayMargins:
- return
-
- if self._renderResources is None:
- self._buildVerticesAndLabels()
- vertices, gridVertices, labels = self._renderResources
-
- width, height = self.size
- matProj = mat4Ortho(0, width, height, 0, 1, -1)
-
- gl.glViewport(0, 0, width, height)
-
- prog = self._program
- prog.use()
-
- gl.glLineWidth(self._LINE_WIDTH)
-
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- matProj.astype(numpy.float32))
- gl.glUniform4f(prog.uniforms['color'], *self._foregroundColor)
- gl.glUniform1f(prog.uniforms['tickFactor'], 0.)
-
- gl.glEnableVertexAttribArray(prog.attributes['position'])
- gl.glVertexAttribPointer(prog.attributes['position'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, vertices)
-
- gl.glDrawArrays(gl.GL_LINES, 0, len(vertices))
-
- for label in labels:
- label.render(matProj)
-
- def renderGrid(self):
- if self._grid == self.GRID_NONE:
- return
-
- if self._renderResources is None:
- self._buildVerticesAndLabels()
- vertices, gridVertices, labels = self._renderResources
-
- width, height = self.size
- matProj = mat4Ortho(0, width, height, 0, 1, -1)
-
- gl.glViewport(0, 0, width, height)
-
- prog = self._program
- prog.use()
-
- gl.glLineWidth(self._LINE_WIDTH)
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- matProj.astype(numpy.float32))
- gl.glUniform4f(prog.uniforms['color'], *self._gridColor)
- gl.glUniform1f(prog.uniforms['tickFactor'], 0.) # 1/2.) # 1/tickLen
-
- gl.glEnableVertexAttribArray(prog.attributes['position'])
- gl.glVertexAttribPointer(prog.attributes['position'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, gridVertices)
-
- gl.glDrawArrays(gl.GL_LINES, 0, len(gridVertices))
-
-
-# GLPlotFrame2D ###############################################################
-
-class GLPlotFrame2D(GLPlotFrame):
- def __init__(self, marginRatios, foregroundColor, gridColor):
- """
- :param List[float] marginRatios:
- The ratios of margins around plot area for axis and labels.
- (left, top, right, bottom) as float in [0., 1.]
- :param foregroundColor: color used for the frame and labels.
- :type foregroundColor: tuple with RGBA values ranging from 0.0 to 1.0
- :param gridColor: color used for grid lines.
- :type gridColor: tuple RGBA with RGBA values ranging from 0.0 to 1.0
-
- """
- super(GLPlotFrame2D, self).__init__(marginRatios, foregroundColor, gridColor)
- self.axes.append(PlotAxis(self,
- tickLength=(0., -5.),
- foregroundColor=self._foregroundColor,
- labelAlign=CENTER, labelVAlign=TOP,
- titleAlign=CENTER, titleVAlign=TOP,
- titleRotate=0))
-
- self._x2AxisCoords = ()
-
- self.axes.append(PlotAxis(self,
- tickLength=(5., 0.),
- foregroundColor=self._foregroundColor,
- labelAlign=RIGHT, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=BOTTOM,
- titleRotate=ROTATE_270))
-
- self._y2Axis = PlotAxis(self,
- tickLength=(-5., 0.),
- foregroundColor=self._foregroundColor,
- labelAlign=LEFT, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=TOP,
- titleRotate=ROTATE_270)
-
- self._isYAxisInverted = False
-
- self._dataRanges = {
- 'x': (1., 100.), 'y': (1., 100.), 'y2': (1., 100.)}
-
- self._baseVectors = (1., 0.), (0., 1.)
-
- self._transformedDataRanges = None
- self._transformedDataProjMat = None
- self._transformedDataY2ProjMat = None
-
- def _dirty(self):
- super(GLPlotFrame2D, self)._dirty()
- self._transformedDataRanges = None
- self._transformedDataProjMat = None
- self._transformedDataY2ProjMat = None
-
- @property
- def isDirty(self):
- """True if it need to refresh graphic rendering, False otherwise."""
- return (super(GLPlotFrame2D, self).isDirty or
- self._transformedDataRanges is None or
- self._transformedDataProjMat is None or
- self._transformedDataY2ProjMat is None)
-
- @property
- def xAxis(self):
- return self.axes[0]
-
- @property
- def yAxis(self):
- return self.axes[1]
-
- @property
- def y2Axis(self):
- return self._y2Axis
-
- @property
- def isY2Axis(self):
- """Whether to display the left Y axis or not."""
- return len(self.axes) == 3
-
- @isY2Axis.setter
- def isY2Axis(self, isY2Axis):
- if isY2Axis != self.isY2Axis:
- if isY2Axis:
- self.axes.append(self._y2Axis)
- else:
- self.axes = self.axes[:2]
-
- self._dirty()
-
- @property
- def isYAxisInverted(self):
- """Whether Y axes are inverted or not as a bool."""
- return self._isYAxisInverted
-
- @isYAxisInverted.setter
- def isYAxisInverted(self, value):
- value = bool(value)
- if value != self._isYAxisInverted:
- self._isYAxisInverted = value
- self._dirty()
-
- DEFAULT_BASE_VECTORS = (1., 0.), (0., 1.)
- """Values of baseVectors for orthogonal axes."""
-
- @property
- def baseVectors(self):
- """Coordinates of the X and Y axes in the orthogonal plot coords.
-
- Raises ValueError if corresponding matrix is singular.
-
- 2 tuples of 2 floats: (xx, xy), (yx, yy)
- """
- return self._baseVectors
-
- @baseVectors.setter
- def baseVectors(self, baseVectors):
- self._dirty()
-
- (xx, xy), (yx, yy) = baseVectors
- vectors = (float(xx), float(xy)), (float(yx), float(yy))
-
- det = (vectors[0][0] * vectors[1][1] - vectors[1][0] * vectors[0][1])
- if det == 0.:
- raise ValueError("Singular matrix for base vectors: " +
- str(vectors))
-
- if vectors != self._baseVectors:
- self._baseVectors = vectors
- self._dirty()
-
- def _updateTitleOffset(self):
- """Update axes title offset according to margins"""
- margins = self.margins
- self.xAxis.titleOffset = 0, margins.bottom // 2
- self.yAxis.titleOffset = -3 * margins.left // 4, 0
- self.y2Axis.titleOffset = 3 * margins.right // 4, 0
-
- # Override size and marginRatios setters to update titleOffsets
- @GLPlotFrame.size.setter
- def size(self, size):
- GLPlotFrame.size.fset(self, size)
- self._updateTitleOffset()
-
- @GLPlotFrame.marginRatios.setter
- def marginRatios(self, ratios):
- GLPlotFrame.marginRatios.fset(self, ratios)
- self._updateTitleOffset()
-
- @property
- def dataRanges(self):
- """Ranges of data visible in the plot on x, y and y2 axes.
-
- This is different to the axes range when axes are not orthogonal.
-
- Type: ((xMin, xMax), (yMin, yMax), (y2Min, y2Max))
- """
- return self._DataRanges(self._dataRanges['x'],
- self._dataRanges['y'],
- self._dataRanges['y2'])
-
- @staticmethod
- def _clipToSafeRange(min_, max_, isLog):
- # Clip range if needed
- minLimit = FLOAT32_MINPOS if isLog else FLOAT32_SAFE_MIN
- min_ = numpy.clip(min_, minLimit, FLOAT32_SAFE_MAX)
- max_ = numpy.clip(max_, minLimit, FLOAT32_SAFE_MAX)
- assert min_ < max_
- return min_, max_
-
- def setDataRanges(self, x=None, y=None, y2=None):
- """Set data range over each axes.
-
- The provided ranges are clipped to possible values
- (i.e., 32 float range + positive range for log scale).
-
- :param x: (min, max) data range over X axis
- :param y: (min, max) data range over Y axis
- :param y2: (min, max) data range over Y2 axis
- """
- if x is not None:
- self._dataRanges['x'] = \
- self._clipToSafeRange(x[0], x[1], self.xAxis.isLog)
-
- if y is not None:
- self._dataRanges['y'] = \
- self._clipToSafeRange(y[0], y[1], self.yAxis.isLog)
-
- if y2 is not None:
- self._dataRanges['y2'] = \
- self._clipToSafeRange(y2[0], y2[1], self.y2Axis.isLog)
-
- self.xAxis.dataRange = self._dataRanges['x']
- self.yAxis.dataRange = self._dataRanges['y']
- self.y2Axis.dataRange = self._dataRanges['y2']
-
- _DataRanges = namedtuple('dataRanges', ('x', 'y', 'y2'))
-
- @property
- def transformedDataRanges(self):
- """Bounds of the displayed area in transformed data coordinates
- (i.e., log scale applied if any as well as skew)
-
- 3-tuple of 2-tuple (min, max) for each axis: x, y, y2.
- """
- if self._transformedDataRanges is None:
- (xMin, xMax), (yMin, yMax), (y2Min, y2Max) = self.dataRanges
-
- if self.xAxis.isLog:
- try:
- xMin = math.log10(xMin)
- except ValueError:
- _logger.info('xMin: warning log10(%f)', xMin)
- xMin = 0.
- try:
- xMax = math.log10(xMax)
- except ValueError:
- _logger.info('xMax: warning log10(%f)', xMax)
- xMax = 0.
-
- if self.yAxis.isLog:
- try:
- yMin = math.log10(yMin)
- except ValueError:
- _logger.info('yMin: warning log10(%f)', yMin)
- yMin = 0.
- try:
- yMax = math.log10(yMax)
- except ValueError:
- _logger.info('yMax: warning log10(%f)', yMax)
- yMax = 0.
-
- try:
- y2Min = math.log10(y2Min)
- except ValueError:
- _logger.info('yMin: warning log10(%f)', y2Min)
- y2Min = 0.
- try:
- y2Max = math.log10(y2Max)
- except ValueError:
- _logger.info('yMax: warning log10(%f)', y2Max)
- y2Max = 0.
-
- self._transformedDataRanges = self._DataRanges(
- (xMin, xMax), (yMin, yMax), (y2Min, y2Max))
-
- return self._transformedDataRanges
-
- @property
- def transformedDataProjMat(self):
- """Orthographic projection matrix for rendering transformed data
-
- :type: numpy.matrix
- """
- if self._transformedDataProjMat is None:
- xMin, xMax = self.transformedDataRanges.x
- yMin, yMax = self.transformedDataRanges.y
-
- if self.isYAxisInverted:
- mat = mat4Ortho(xMin, xMax, yMax, yMin, 1, -1)
- else:
- mat = mat4Ortho(xMin, xMax, yMin, yMax, 1, -1)
- self._transformedDataProjMat = mat
-
- return self._transformedDataProjMat
-
- @property
- def transformedDataY2ProjMat(self):
- """Orthographic projection matrix for rendering transformed data
- for the 2nd Y axis
-
- :type: numpy.matrix
- """
- if self._transformedDataY2ProjMat is None:
- xMin, xMax = self.transformedDataRanges.x
- y2Min, y2Max = self.transformedDataRanges.y2
-
- if self.isYAxisInverted:
- mat = mat4Ortho(xMin, xMax, y2Max, y2Min, 1, -1)
- else:
- mat = mat4Ortho(xMin, xMax, y2Min, y2Max, 1, -1)
- self._transformedDataY2ProjMat = mat
-
- return self._transformedDataY2ProjMat
-
- def dataToPixel(self, x, y, axis='left'):
- """Convert data coordinate to widget pixel coordinate.
- """
- assert axis in ('left', 'right')
-
- trBounds = self.transformedDataRanges
-
- if self.xAxis.isLog:
- if x < FLOAT32_MINPOS:
- return None
- xDataTr = math.log10(x)
- else:
- xDataTr = x
-
- if self.yAxis.isLog:
- if y < FLOAT32_MINPOS:
- return None
- yDataTr = math.log10(y)
- else:
- yDataTr = y
-
- # Non-orthogonal axes
- if self.baseVectors != self.DEFAULT_BASE_VECTORS:
- (xx, xy), (yx, yy) = self.baseVectors
- skew_mat = numpy.array(((xx, yx), (xy, yy)))
-
- coords = numpy.dot(skew_mat, numpy.array((xDataTr, yDataTr)))
- xDataTr, yDataTr = coords
-
- plotWidth, plotHeight = self.plotSize
-
- xPixel = int(self.margins.left +
- plotWidth * (xDataTr - trBounds.x[0]) /
- (trBounds.x[1] - trBounds.x[0]))
-
- usedAxis = trBounds.y if axis == "left" else trBounds.y2
- yOffset = (plotHeight * (yDataTr - usedAxis[0]) /
- (usedAxis[1] - usedAxis[0]))
-
- if self.isYAxisInverted:
- yPixel = int(self.margins.top + yOffset)
- else:
- yPixel = int(self.size[1] - self.margins.bottom - yOffset)
-
- return xPixel, yPixel
-
- def pixelToData(self, x, y, axis="left"):
- """Convert pixel position to data coordinates.
-
- :param float x: X coord
- :param float y: Y coord
- :param str axis: Y axis to use in ('left', 'right')
- :return: (x, y) position in data coords
- """
- assert axis in ("left", "right")
-
- plotWidth, plotHeight = self.plotSize
-
- trBounds = self.transformedDataRanges
-
- xData = (x - self.margins.left + 0.5) / float(plotWidth)
- xData = trBounds.x[0] + xData * (trBounds.x[1] - trBounds.x[0])
-
- usedAxis = trBounds.y if axis == "left" else trBounds.y2
- if self.isYAxisInverted:
- yData = (y - self.margins.top + 0.5) / float(plotHeight)
- yData = usedAxis[0] + yData * (usedAxis[1] - usedAxis[0])
- else:
- yData = self.size[1] - self.margins.bottom - y - 0.5
- yData /= float(plotHeight)
- yData = usedAxis[0] + yData * (usedAxis[1] - usedAxis[0])
-
- # non-orthogonal axis
- if self.baseVectors != self.DEFAULT_BASE_VECTORS:
- (xx, xy), (yx, yy) = self.baseVectors
- skew_mat = numpy.array(((xx, yx), (xy, yy)))
- skew_mat = numpy.linalg.inv(skew_mat)
-
- coords = numpy.dot(skew_mat, numpy.array((xData, yData)))
- xData, yData = coords
-
- if self.xAxis.isLog:
- xData = pow(10, xData)
- if self.yAxis.isLog:
- yData = pow(10, yData)
-
- return xData, yData
-
- def _buildGridVerticesWithTest(self, test):
- vertices = []
-
- if self.baseVectors == self.DEFAULT_BASE_VECTORS:
- for axis in self.axes:
- for (xPixel, yPixel), data, text in axis.ticks:
- if test(text):
- vertices.append((xPixel, yPixel))
- if axis == self.xAxis:
- vertices.append((xPixel, self.margins.top))
- elif axis == self.yAxis:
- vertices.append((self.size[0] - self.margins.right,
- yPixel))
- else: # axis == self.y2Axis
- vertices.append((self.margins.left, yPixel))
-
- else:
- # Get plot corners in data coords
- plotLeft, plotTop = self.plotOrigin
- plotWidth, plotHeight = self.plotSize
-
- corners = [(plotLeft, plotTop),
- (plotLeft, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop)]
-
- for axis in self.axes:
- if axis == self.xAxis:
- cornersInData = numpy.array([
- self.pixelToData(x, y) for (x, y) in corners])
- borders = ((cornersInData[0], cornersInData[3]), # top
- (cornersInData[1], cornersInData[0]), # left
- (cornersInData[3], cornersInData[2])) # right
-
- for (xPixel, yPixel), data, text in axis.ticks:
- if test(text):
- for (x0, y0), (x1, y1) in borders:
- if min(x0, x1) <= data < max(x0, x1):
- yIntersect = (data - x0) * \
- (y1 - y0) / (x1 - x0) + y0
-
- pixelPos = self.dataToPixel(
- data, yIntersect)
- if pixelPos is not None:
- vertices.append((xPixel, yPixel))
- vertices.append(pixelPos)
- break # Stop at first intersection
-
- else: # y or y2 axes
- if axis == self.yAxis:
- axis_name = 'left'
- cornersInData = numpy.array([
- self.pixelToData(x, y) for (x, y) in corners])
- borders = (
- (cornersInData[3], cornersInData[2]), # right
- (cornersInData[0], cornersInData[3]), # top
- (cornersInData[2], cornersInData[1])) # bottom
-
- else: # axis == self.y2Axis
- axis_name = 'right'
- corners = numpy.array([self.pixelToData(
- x, y, axis='right') for (x, y) in corners])
- borders = (
- (cornersInData[1], cornersInData[0]), # left
- (cornersInData[0], cornersInData[3]), # top
- (cornersInData[2], cornersInData[1])) # bottom
-
- for (xPixel, yPixel), data, text in axis.ticks:
- if test(text):
- for (x0, y0), (x1, y1) in borders:
- if min(y0, y1) <= data < max(y0, y1):
- xIntersect = (data - y0) * \
- (x1 - x0) / (y1 - y0) + x0
-
- pixelPos = self.dataToPixel(
- xIntersect, data, axis=axis_name)
- if pixelPos is not None:
- vertices.append((xPixel, yPixel))
- vertices.append(pixelPos)
- break # Stop at first intersection
-
- return vertices
-
- def _buildVerticesAndLabels(self):
- width, height = self.size
-
- xCoords = (self.margins.left - 0.5,
- width - self.margins.right + 0.5)
- yCoords = (height - self.margins.bottom + 0.5,
- self.margins.top - 0.5)
-
- self.axes[0].displayCoords = ((xCoords[0], yCoords[0]),
- (xCoords[1], yCoords[0]))
-
- self._x2AxisCoords = ((xCoords[0], yCoords[1]),
- (xCoords[1], yCoords[1]))
-
- if self.isYAxisInverted:
- # Y axes are inverted, axes coordinates are inverted
- yCoords = yCoords[1], yCoords[0]
-
- self.axes[1].displayCoords = ((xCoords[0], yCoords[0]),
- (xCoords[0], yCoords[1]))
-
- self._y2Axis.displayCoords = ((xCoords[1], yCoords[0]),
- (xCoords[1], yCoords[1]))
-
- super(GLPlotFrame2D, self)._buildVerticesAndLabels()
-
- vertices, gridVertices, labels = self._renderResources
-
- # Adds vertices for borders without axis
- extraVertices = []
- extraVertices += self._x2AxisCoords
- if not self.isY2Axis:
- extraVertices += self._y2Axis.displayCoords
-
- extraVertices = numpy.array(
- extraVertices, copy=False, dtype=numpy.float32)
- vertices = numpy.append(vertices, extraVertices, axis=0)
-
- self._renderResources = (vertices, gridVertices, labels)
-
- @property
- def foregroundColor(self):
- """Color used for frame and labels"""
- return self._foregroundColor
-
- @foregroundColor.setter
- def foregroundColor(self, color):
- """Color used for frame and labels"""
- assert len(color) == 4, \
- "foregroundColor must have length 4, got {}".format(len(self._foregroundColor))
- if self._foregroundColor != color:
- self._y2Axis.foregroundColor = color
- GLPlotFrame.foregroundColor.fset(self, color) # call parent property
diff --git a/silx/gui/plot/backends/glutils/GLPlotImage.py b/silx/gui/plot/backends/glutils/GLPlotImage.py
deleted file mode 100644
index 3ad94b9..0000000
--- a/silx/gui/plot/backends/glutils/GLPlotImage.py
+++ /dev/null
@@ -1,756 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2021 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 class to render 2D array as a colormap or RGB(A) image
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/04/2017"
-
-
-import math
-import numpy
-
-from silx.math.combo import min_max
-
-from ...._glutils import gl, Program, Texture
-from ..._utils import FLOAT32_MINPOS
-from .GLSupport import mat4Translate, mat4Scale
-from .GLTexture import Image
-from .GLPlotItem import GLPlotItem
-
-
-class _GLPlotData2D(GLPlotItem):
- def __init__(self, data, origin, scale):
- super().__init__()
- self.data = data
- assert len(origin) == 2
- self.origin = tuple(origin)
- assert len(scale) == 2
- self.scale = tuple(scale)
-
- def pick(self, x, y):
- if self.xMin <= x <= self.xMax and self.yMin <= y <= self.yMax:
- ox, oy = self.origin
- sx, sy = self.scale
- col = int((x - ox) / sx)
- row = int((y - oy) / sy)
- return (row,), (col,)
- else:
- return None
-
- @property
- def xMin(self):
- ox, sx = self.origin[0], self.scale[0]
- return ox if sx >= 0. else ox + sx * self.data.shape[1]
-
- @property
- def yMin(self):
- oy, sy = self.origin[1], self.scale[1]
- return oy if sy >= 0. else oy + sy * self.data.shape[0]
-
- @property
- def xMax(self):
- ox, sx = self.origin[0], self.scale[0]
- return ox + sx * self.data.shape[1] if sx >= 0. else ox
-
- @property
- def yMax(self):
- oy, sy = self.origin[1], self.scale[1]
- return oy + sy * self.data.shape[0] if sy >= 0. else oy
-
-
-class GLPlotColormap(_GLPlotData2D):
-
- _SHADERS = {
- 'linear': {
- 'vertex': """
- #version 120
-
- uniform mat4 matrix;
- attribute vec2 texCoords;
- attribute vec2 position;
-
- varying vec2 coords;
-
- void main(void) {
- coords = texCoords;
- gl_Position = matrix * vec4(position, 0.0, 1.0);
- }
- """,
- 'fragTransform': """
- vec2 textureCoords(void) {
- return coords;
- }
- """},
-
- 'log': {
- 'vertex': """
- #version 120
-
- attribute vec2 position;
- uniform mat4 matrix;
- uniform mat4 matOffset;
- uniform bvec2 isLog;
-
- varying vec2 coords;
-
- const float oneOverLog10 = 0.43429448190325176;
-
- void main(void) {
- vec4 dataPos = matOffset * vec4(position, 0.0, 1.0);
- if (isLog.x) {
- dataPos.x = oneOverLog10 * log(dataPos.x);
- }
- if (isLog.y) {
- dataPos.y = oneOverLog10 * log(dataPos.y);
- }
- coords = dataPos.xy;
- gl_Position = matrix * dataPos;
- }
- """,
- 'fragTransform': """
- uniform bvec2 isLog;
- uniform vec2 bounds_oneOverRange;
- uniform vec2 bounds_originOverRange;
-
- vec2 textureCoords(void) {
- vec2 pos = coords;
- if (isLog.x) {
- pos.x = pow(10., coords.x);
- }
- if (isLog.y) {
- pos.y = pow(10., coords.y);
- }
- return pos * bounds_oneOverRange - bounds_originOverRange;
- // TODO texture coords in range different from [0, 1]
- }
- """},
-
- 'fragment': """
- #version 120
-
- /* isnan declaration for compatibility with GLSL 1.20 */
- bool isnan(float value) {
- return (value != value);
- }
-
- uniform sampler2D data;
- uniform sampler2D cmap_texture;
- uniform int cmap_normalization;
- uniform float cmap_parameter;
- uniform float cmap_min;
- uniform float cmap_oneOverRange;
- uniform float alpha;
- uniform vec4 nancolor;
-
- varying vec2 coords;
-
- %s
-
- const float oneOverLog10 = 0.43429448190325176;
-
- void main(void) {
- float data = texture2D(data, textureCoords()).r;
- float value = data;
- if (cmap_normalization == 1) { /*Logarithm mapping*/
- if (value > 0.) {
- value = clamp(cmap_oneOverRange *
- (oneOverLog10 * log(value) - cmap_min),
- 0., 1.);
- } else {
- value = 0.;
- }
- } else if (cmap_normalization == 2) { /*Square root mapping*/
- if (value >= 0.) {
- value = clamp(cmap_oneOverRange * (sqrt(value) - cmap_min),
- 0., 1.);
- } else {
- value = 0.;
- }
- } else if (cmap_normalization == 3) { /*Gamma correction mapping*/
- value = pow(
- clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.),
- cmap_parameter);
- } else if (cmap_normalization == 4) { /* arcsinh mapping */
- /* asinh = log(x + sqrt(x*x + 1) for compatibility with GLSL 1.20 */
- value = clamp(cmap_oneOverRange * (log(value + sqrt(value*value + 1.0)) - cmap_min), 0., 1.);
- } else { /*Linear mapping and fallback*/
- value = clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.);
- }
-
- if (isnan(data)) {
- gl_FragColor = nancolor;
- } else {
- gl_FragColor = texture2D(cmap_texture, vec2(value, 0.5));
- }
- gl_FragColor.a *= alpha;
- }
- """
- }
-
- _DATA_TEX_UNIT = 0
- _CMAP_TEX_UNIT = 1
-
- _INTERNAL_FORMATS = {
- numpy.dtype(numpy.float32): gl.GL_R32F,
- numpy.dtype(numpy.float16): gl.GL_R16F,
- # Use normalized integer for unsigned int formats
- numpy.dtype(numpy.uint16): gl.GL_R16,
- numpy.dtype(numpy.uint8): gl.GL_R8,
- }
-
- _linearProgram = Program(_SHADERS['linear']['vertex'],
- _SHADERS['fragment'] %
- _SHADERS['linear']['fragTransform'],
- attrib0='position')
-
- _logProgram = Program(_SHADERS['log']['vertex'],
- _SHADERS['fragment'] %
- _SHADERS['log']['fragTransform'],
- attrib0='position')
-
- SUPPORTED_NORMALIZATIONS = 'linear', 'log', 'sqrt', 'gamma', 'arcsinh'
-
- def __init__(self, data, origin, scale,
- colormap, normalization='linear', gamma=0., cmapRange=None,
- alpha=1.0, nancolor=(1., 1., 1., 0.)):
- """Create a 2D colormap
-
- :param data: The 2D scalar data array to display
- :type data: numpy.ndarray with 2 dimensions (dtype=numpy.float32)
- :param origin: (x, y) coordinates of the origin of the data array
- :type origin: 2-tuple of floats.
- :param scale: (sx, sy) scale factors of the data array.
- This is the size of a data pixel in plot data space.
- :type scale: 2-tuple of floats.
- :param str colormap: Name of the colormap to use
- TODO: Accept a 1D scalar array as the colormap
- :param str normalization: The colormap normalization.
- One of: 'linear', 'log', 'sqrt', 'gamma'
- ;param float gamma: The gamma parameter (for 'gamma' normalization)
- :param cmapRange: The range of colormap or None for autoscale colormap
- For logarithmic colormap, the range is in the untransformed data
- TODO: check consistency with matplotlib
- :type cmapRange: (float, float) or None
- :param float alpha: Opacity from 0 (transparent) to 1 (opaque)
- :param nancolor: RGBA color for Not-A-Number values
- :type nancolor: 4-tuple of float in [0., 1.]
- """
- assert data.dtype in self._INTERNAL_FORMATS
- assert normalization in self.SUPPORTED_NORMALIZATIONS
-
- super(GLPlotColormap, self).__init__(data, origin, scale)
- self.colormap = numpy.array(colormap, copy=False)
- self.normalization = normalization
- self.gamma = gamma
- self._cmapRange = (1., 10.) # Colormap range
- self.cmapRange = cmapRange # Update _cmapRange
- self._alpha = numpy.clip(alpha, 0., 1.)
- self._nancolor = numpy.clip(nancolor, 0., 1.)
-
- self._cmap_texture = None
- self._texture = None
- self._textureIsDirty = False
-
- def discard(self):
- if self._cmap_texture is not None:
- self._cmap_texture.discard()
- self._cmap_texture = None
-
- if self._texture is not None:
- self._texture.discard()
- self._texture = None
- self._textureIsDirty = False
-
- def isInitialized(self):
- return (self._cmap_texture is not None or
- self._texture is not None)
-
- @property
- def cmapRange(self):
- if self.normalization == 'log':
- assert self._cmapRange[0] > 0. and self._cmapRange[1] > 0.
- elif self.normalization == 'sqrt':
- assert self._cmapRange[0] >= 0. and self._cmapRange[1] >= 0.
- return self._cmapRange
-
- @cmapRange.setter
- def cmapRange(self, cmapRange):
- assert len(cmapRange) == 2
- assert cmapRange[0] <= cmapRange[1]
- self._cmapRange = float(cmapRange[0]), float(cmapRange[1])
-
- @property
- def alpha(self):
- return self._alpha
-
- def updateData(self, data):
- assert data.dtype in self._INTERNAL_FORMATS
- oldData = self.data
- self.data = data
-
- if self._texture is not None:
- if (self.data.shape != oldData.shape or
- self.data.dtype != oldData.dtype):
- self.discard()
- else:
- self._textureIsDirty = True
-
- def prepare(self):
- if self._cmap_texture is None:
- # TODO share cmap texture accross Images
- # put all cmaps in one texture
- colormap = numpy.empty((16, 256, self.colormap.shape[1]),
- dtype=self.colormap.dtype)
- colormap[:] = self.colormap
- format_ = gl.GL_RGBA if colormap.shape[-1] == 4 else gl.GL_RGB
- self._cmap_texture = Texture(internalFormat=format_,
- data=colormap,
- format_=format_,
- texUnit=self._CMAP_TEX_UNIT,
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=(gl.GL_CLAMP_TO_EDGE,
- gl.GL_CLAMP_TO_EDGE))
- self._cmap_texture.prepare()
-
- if self._texture is None:
- internalFormat = self._INTERNAL_FORMATS[self.data.dtype]
-
- self._texture = Image(internalFormat,
- self.data,
- format_=gl.GL_RED,
- texUnit=self._DATA_TEX_UNIT)
- elif self._textureIsDirty:
- self._textureIsDirty = True
- self._texture.updateAll(format_=gl.GL_RED, data=self.data)
-
- def _setCMap(self, prog):
- dataMin, dataMax = self.cmapRange # If log, it is stricly positive
- param = 0.
-
- if self.data.dtype in (numpy.uint16, numpy.uint8):
- # Using unsigned int as normalized integer in OpenGL
- # So normalize range
- maxInt = float(numpy.iinfo(self.data.dtype).max)
- dataMin, dataMax = dataMin / maxInt, dataMax / maxInt
-
- if self.normalization == 'log':
- dataMin = math.log10(dataMin)
- dataMax = math.log10(dataMax)
- normID = 1
- elif self.normalization == 'sqrt':
- dataMin = math.sqrt(dataMin)
- dataMax = math.sqrt(dataMax)
- normID = 2
- elif self.normalization == 'gamma':
- # Keep dataMin, dataMax as is
- param = self.gamma
- normID = 3
- elif self.normalization == 'arcsinh':
- dataMin = numpy.arcsinh(dataMin)
- dataMax = numpy.arcsinh(dataMax)
- normID = 4
- else: # Linear and fallback
- normID = 0
-
- gl.glUniform1i(prog.uniforms['cmap_texture'],
- self._cmap_texture.texUnit)
- gl.glUniform1i(prog.uniforms['cmap_normalization'], normID)
- gl.glUniform1f(prog.uniforms['cmap_parameter'], param)
- gl.glUniform1f(prog.uniforms['cmap_min'], dataMin)
- if dataMax > dataMin:
- oneOverRange = 1. / (dataMax - dataMin)
- else:
- oneOverRange = 0. # Fall-back
- gl.glUniform1f(prog.uniforms['cmap_oneOverRange'], oneOverRange)
-
- gl.glUniform4f(prog.uniforms['nancolor'], *self._nancolor)
-
- self._cmap_texture.bind()
-
- def _renderLinear(self, context):
- """Perform rendering when both axes have linear scales
-
- :param RenderContext context: Rendering information
- """
- self.prepare()
-
- prog = self._linearProgram
- prog.use()
-
- gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
-
- mat = numpy.dot(numpy.dot(context.matrix,
- mat4Translate(*self.origin)),
- mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
-
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
-
- self._setCMap(prog)
-
- self._texture.render(prog.attributes['position'],
- prog.attributes['texCoords'],
- self._DATA_TEX_UNIT)
-
- def _renderLog10(self, context):
- """Perform rendering when one axis has log scale
-
- :param RenderContext context: Rendering information
- """
- xMin, yMin = self.xMin, self.yMin
- if ((context.isXLog and xMin < FLOAT32_MINPOS) or
- (context.isYLog and yMin < FLOAT32_MINPOS)):
- # Do not render images that are partly or totally <= 0
- return
-
- self.prepare()
-
- prog = self._logProgram
- prog.use()
-
- ox, oy = self.origin
-
- gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
-
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- context.matrix.astype(numpy.float32))
- mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
-
- gl.glUniform2i(prog.uniforms['isLog'], context.isXLog, context.isYLog)
-
- ex = ox + self.scale[0] * self.data.shape[1]
- ey = oy + self.scale[1] * self.data.shape[0]
-
- xOneOverRange = 1. / (ex - ox)
- yOneOverRange = 1. / (ey - oy)
- gl.glUniform2f(prog.uniforms['bounds_originOverRange'],
- ox * xOneOverRange, oy * yOneOverRange)
- gl.glUniform2f(prog.uniforms['bounds_oneOverRange'],
- xOneOverRange, yOneOverRange)
-
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
-
- self._setCMap(prog)
-
- try:
- tiles = self._texture.tiles
- except AttributeError:
- raise RuntimeError("No texture, discard has already been called")
- if len(tiles) > 1:
- raise NotImplementedError(
- "Image over multiple textures not supported with log scale")
-
- texture, vertices, info = tiles[0]
-
- texture.bind(self._DATA_TEX_UNIT)
-
- posAttrib = prog.attributes['position']
- stride = vertices.shape[-1] * vertices.itemsize
- gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, vertices)
-
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
-
- def render(self, context):
- """Perform rendering
-
- :param RenderContext context: Rendering information
- """
- if any((context.isXLog, context.isYLog)):
- self._renderLog10(context)
- else:
- self._renderLinear(context)
-
- # Unbind colormap texture
- gl.glActiveTexture(gl.GL_TEXTURE0 + self._cmap_texture.texUnit)
- gl.glBindTexture(self._cmap_texture.target, 0)
-
-
-# image #######################################################################
-
-class GLPlotRGBAImage(_GLPlotData2D):
-
- _SHADERS = {
- 'linear': {
- 'vertex': """
- #version 120
-
- attribute vec2 position;
- attribute vec2 texCoords;
- uniform mat4 matrix;
-
- varying vec2 coords;
-
- void main(void) {
- gl_Position = matrix * vec4(position, 0.0, 1.0);
- coords = texCoords;
- }
- """,
- 'fragment': """
- #version 120
-
- uniform sampler2D tex;
- uniform float alpha;
-
- varying vec2 coords;
-
- void main(void) {
- gl_FragColor = texture2D(tex, coords);
- gl_FragColor.a *= alpha;
- }
- """},
-
- 'log': {
- 'vertex': """
- #version 120
-
- attribute vec2 position;
- uniform mat4 matrix;
- uniform mat4 matOffset;
- uniform bvec2 isLog;
-
- varying vec2 coords;
-
- const float oneOverLog10 = 0.43429448190325176;
-
- void main(void) {
- vec4 dataPos = matOffset * vec4(position, 0.0, 1.0);
- if (isLog.x) {
- dataPos.x = oneOverLog10 * log(dataPos.x);
- }
- if (isLog.y) {
- dataPos.y = oneOverLog10 * log(dataPos.y);
- }
- coords = dataPos.xy;
- gl_Position = matrix * dataPos;
- }
- """,
- 'fragment': """
- #version 120
-
- uniform sampler2D tex;
- uniform bvec2 isLog;
- uniform vec2 bounds_oneOverRange;
- uniform vec2 bounds_originOverRange;
- uniform float alpha;
-
- varying vec2 coords;
-
- vec2 textureCoords(void) {
- vec2 pos = coords;
- if (isLog.x) {
- pos.x = pow(10., coords.x);
- }
- if (isLog.y) {
- pos.y = pow(10., coords.y);
- }
- return pos * bounds_oneOverRange - bounds_originOverRange;
- // TODO texture coords in range different from [0, 1]
- }
-
- void main(void) {
- gl_FragColor = texture2D(tex, textureCoords());
- gl_FragColor.a *= alpha;
- }
- """}
- }
-
- _DATA_TEX_UNIT = 0
-
- _SUPPORTED_DTYPES = (numpy.dtype(numpy.float32),
- numpy.dtype(numpy.uint8),
- numpy.dtype(numpy.uint16))
-
- _linearProgram = Program(_SHADERS['linear']['vertex'],
- _SHADERS['linear']['fragment'],
- attrib0='position')
-
- _logProgram = Program(_SHADERS['log']['vertex'],
- _SHADERS['log']['fragment'],
- attrib0='position')
-
- def __init__(self, data, origin, scale, alpha):
- """Create a 2D RGB(A) image from data
-
- :param data: The 2D image data array to display
- :type data: numpy.ndarray with 3 dimensions
- (dtype=numpy.uint8 or numpy.float32)
- :param origin: (x, y) coordinates of the origin of the data array
- :type origin: 2-tuple of floats.
- :param scale: (sx, sy) scale factors of the data array.
- This is the size of a data pixel in plot data space.
- :type scale: 2-tuple of floats.
- :param float alpha: Opacity from 0 (transparent) to 1 (opaque)
- """
- assert data.dtype in self._SUPPORTED_DTYPES
- super(GLPlotRGBAImage, self).__init__(data, origin, scale)
- self._texture = None
- self._textureIsDirty = False
- self._alpha = numpy.clip(alpha, 0., 1.)
-
- @property
- def alpha(self):
- return self._alpha
-
- def discard(self):
- if self.isInitialized():
- self._texture.discard()
- self._texture = None
- self._textureIsDirty = False
-
- def isInitialized(self):
- return self._texture is not None
-
- def updateData(self, data):
- assert data.dtype in self._SUPPORTED_DTYPES
- oldData = self.data
- self.data = data
-
- if self._texture is not None:
- if self.data.shape != oldData.shape:
- self.discard()
- else:
- self._textureIsDirty = True
-
- def prepare(self):
- if self._texture is None:
- formatName = 'GL_RGBA' if self.data.shape[2] == 4 else 'GL_RGB'
- format_ = getattr(gl, formatName)
-
- if self.data.dtype == numpy.uint16:
- formatName += '16' # Use sized internal format for uint16
- internalFormat = getattr(gl, formatName)
-
- self._texture = Image(internalFormat,
- self.data,
- format_=format_,
- texUnit=self._DATA_TEX_UNIT)
- elif self._textureIsDirty:
- self._textureIsDirty = False
-
- # We should check that internal format is the same
- format_ = gl.GL_RGBA if self.data.shape[2] == 4 else gl.GL_RGB
- self._texture.updateAll(format_=format_, data=self.data)
-
- def _renderLinear(self, context):
- """Perform rendering with both axes having linear scales
-
- :param RenderContext context: Rendering information
- """
- self.prepare()
-
- prog = self._linearProgram
- prog.use()
-
- gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
-
- mat = numpy.dot(numpy.dot(context.matrix, mat4Translate(*self.origin)),
- mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
-
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
-
- self._texture.render(prog.attributes['position'],
- prog.attributes['texCoords'],
- self._DATA_TEX_UNIT)
-
- def _renderLog(self, context):
- """Perform rendering with axes having log scale
-
- :param RenderContext context: Rendering information
- """
- self.prepare()
-
- prog = self._logProgram
- prog.use()
-
- ox, oy = self.origin
-
- gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
-
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- context.matrix.astype(numpy.float32))
- mat = numpy.dot(mat4Translate(ox, oy), mat4Scale(*self.scale))
- gl.glUniformMatrix4fv(prog.uniforms['matOffset'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
-
- gl.glUniform2i(prog.uniforms['isLog'], context.isXLog, context.isYLog)
-
- gl.glUniform1f(prog.uniforms['alpha'], self.alpha)
-
- ex = ox + self.scale[0] * self.data.shape[1]
- ey = oy + self.scale[1] * self.data.shape[0]
-
- xOneOverRange = 1. / (ex - ox)
- yOneOverRange = 1. / (ey - oy)
- gl.glUniform2f(prog.uniforms['bounds_originOverRange'],
- ox * xOneOverRange, oy * yOneOverRange)
- gl.glUniform2f(prog.uniforms['bounds_oneOverRange'],
- xOneOverRange, yOneOverRange)
-
- try:
- tiles = self._texture.tiles
- except AttributeError:
- raise RuntimeError("No texture, discard has already been called")
- if len(tiles) > 1:
- raise NotImplementedError(
- "Image over multiple textures not supported with log scale")
-
- texture, vertices, info = tiles[0]
-
- texture.bind(self._DATA_TEX_UNIT)
-
- posAttrib = prog.attributes['position']
- stride = vertices.shape[-1] * vertices.itemsize
- gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, vertices)
-
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
-
- def render(self, context):
- """Perform rendering
-
- :param RenderContext context: Rendering information
- """
- if any((context.isXLog, context.isYLog)):
- self._renderLog(context)
- else:
- self._renderLinear(context)
diff --git a/silx/gui/plot/backends/glutils/GLPlotItem.py b/silx/gui/plot/backends/glutils/GLPlotItem.py
deleted file mode 100644
index ae13091..0000000
--- a/silx/gui/plot/backends/glutils/GLPlotItem.py
+++ /dev/null
@@ -1,99 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2020-2021 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 base class for PlotWidget OpenGL backend primitives
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "02/07/2020"
-
-
-class RenderContext:
- """Context with which to perform OpenGL rendering.
-
- :param numpy.ndarray matrix: 4x4 transform matrix to use for rendering
- :param bool isXLog: Whether X axis is log scale or not
- :param bool isYLog: Whether Y axis is log scale or not
- :param float dpi: Number of device pixels per inch
- """
-
- def __init__(self, matrix=None, isXLog=False, isYLog=False, dpi=96.):
- self.matrix = matrix
- """Current transformation matrix"""
-
- self.__isXLog = isXLog
- self.__isYLog = isYLog
- self.__dpi = dpi
-
- @property
- def isXLog(self):
- """True if X axis is using log scale"""
- return self.__isXLog
-
- @property
- def isYLog(self):
- """True if Y axis is using log scale"""
- return self.__isYLog
-
- @property
- def dpi(self):
- """Number of device pixels per inch"""
- return self.__dpi
-
-
-class GLPlotItem:
- """Base class for primitives used in the PlotWidget OpenGL backend"""
-
- def __init__(self):
- self.yaxis = 'left'
- "YAxis this item is attached to (either 'left' or 'right')"
-
- def pick(self, x, y):
- """Perform picking at given position.
-
- :param float x: X coordinate in plot data frame of reference
- :param float y: Y coordinate in plot data frame of reference
- :returns:
- Result of picking as a list of indices or None if nothing picked
- :rtype: Union[List[int],None]
- """
- return None
-
- def render(self, context):
- """Performs OpenGL rendering of the item.
-
- :param RenderContext context: Rendering context information
- """
- pass
-
- def discard(self):
- """Discards OpenGL resources this item has created."""
- pass
-
- def isInitialized(self) -> bool:
- """Returns True if resources where initialized and requires `discard`.
- """
- return True
diff --git a/silx/gui/plot/backends/glutils/GLPlotTriangles.py b/silx/gui/plot/backends/glutils/GLPlotTriangles.py
deleted file mode 100644
index fbe9e02..0000000
--- a/silx/gui/plot/backends/glutils/GLPlotTriangles.py
+++ /dev/null
@@ -1,197 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2019-2021 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 class to render a set of 2D triangles
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/04/2017"
-
-
-import ctypes
-
-import numpy
-
-from .....math.combo import min_max
-from .... import _glutils as glutils
-from ...._glutils import gl
-from .GLPlotItem import GLPlotItem
-
-
-class GLPlotTriangles(GLPlotItem):
- """Handle rendering of a set of colored triangles"""
-
- _PROGRAM = glutils.Program(
- vertexShader="""
- #version 120
-
- uniform mat4 matrix;
- attribute float xPos;
- attribute float yPos;
- attribute vec4 color;
-
- varying vec4 vColor;
-
- void main(void) {
- gl_Position = matrix * vec4(xPos, yPos, 0.0, 1.0);
- vColor = color;
- }
- """,
- fragmentShader="""
- #version 120
-
- uniform float alpha;
- varying vec4 vColor;
-
- void main(void) {
- gl_FragColor = vColor;
- gl_FragColor.a *= alpha;
- }
- """,
- attrib0='xPos')
-
- def __init__(self, x, y, color, triangles, alpha=1.):
- """
-
- :param numpy.ndarray x: X coordinates of triangle corners
- :param numpy.ndarray y: Y coordinates of triangle corners
- :param numpy.ndarray color: color for each point
- :param numpy.ndarray triangles: (N, 3) array of indices of triangles
- :param float alpha: Opacity in [0, 1]
- """
- super().__init__()
- # Check and convert input data
- x = numpy.ravel(numpy.array(x, dtype=numpy.float32))
- y = numpy.ravel(numpy.array(y, dtype=numpy.float32))
- color = numpy.array(color, copy=False)
- # Cast to uint32
- triangles = numpy.array(triangles, copy=False, dtype=numpy.uint32)
-
- assert x.size == y.size
- assert x.size == len(color)
- assert color.ndim == 2 and color.shape[1] in (3, 4)
- if numpy.issubdtype(color.dtype, numpy.floating):
- color = numpy.array(color, dtype=numpy.float32, copy=False)
- elif numpy.issubdtype(color.dtype, numpy.integer):
- color = numpy.array(color, dtype=numpy.uint8, copy=False)
- else:
- raise ValueError('Unsupported color type')
- assert triangles.ndim == 2 and triangles.shape[1] == 3
-
- self.__x_y_color = x, y, color
- self.xMin, self.xMax = min_max(x, finite=True)
- self.yMin, self.yMax = min_max(y, finite=True)
- self.__triangles = triangles
- self.__alpha = numpy.clip(float(alpha), 0., 1.)
- self.__vbos = None
- self.__indicesVbo = None
- self.__picking_triangles = None
-
- def pick(self, x, y):
- """Perform picking
-
- :param float x: X coordinates in plot data frame
- :param float y: Y coordinates in plot data frame
- :return: List of picked data point indices
- :rtype: Union[List[int],None]
- """
- if (x < self.xMin or x > self.xMax or
- y < self.yMin or y > self.yMax):
- return None
-
- xPts, yPts = self.__x_y_color[:2]
- if self.__picking_triangles is None:
- self.__picking_triangles = numpy.zeros(
- self.__triangles.shape + (3,), dtype=numpy.float32)
- self.__picking_triangles[:, :, 0] = xPts[self.__triangles]
- self.__picking_triangles[:, :, 1] = yPts[self.__triangles]
-
- segment = numpy.array(((x, y, -1), (x, y, 1)), dtype=numpy.float32)
- # Picked triangle indices
- indices = glutils.segmentTrianglesIntersection(
- segment, self.__picking_triangles)[0]
- # Point indices
- indices = numpy.unique(numpy.ravel(self.__triangles[indices]))
-
- # Sorted from furthest to closest point
- dists = (xPts[indices] - x) ** 2 + (yPts[indices] - y) ** 2
- indices = indices[numpy.flip(numpy.argsort(dists), axis=0)]
-
- return tuple(indices) if len(indices) > 0 else None
-
- def discard(self):
- """Release resources on the GPU"""
- if self.isInitialized():
- self.__vbos[0].vbo.discard()
- self.__vbos = None
- self.__indicesVbo.discard()
- self.__indicesVbo = None
-
- def isInitialized(self):
- return self.__vbos is not None
-
- def prepare(self):
- """Allocate resources on the GPU"""
- if self.__vbos is None:
- self.__vbos = glutils.vertexBuffer(self.__x_y_color)
- # Normalization is need for color
- self.__vbos[-1].normalization = True
-
- if self.__indicesVbo is None:
- self.__indicesVbo = glutils.VertexBuffer(
- numpy.ravel(self.__triangles),
- usage=gl.GL_STATIC_DRAW,
- target=gl.GL_ELEMENT_ARRAY_BUFFER)
-
- def render(self, context):
- """Perform rendering
-
- :param RenderContext context: Rendering information
- """
- self.prepare()
-
- if self.__vbos is None or self.__indicesVbo is None:
- return # Nothing to display
-
- self._PROGRAM.use()
-
- gl.glUniformMatrix4fv(self._PROGRAM.uniforms['matrix'],
- 1,
- gl.GL_TRUE,
- context.matrix.astype(numpy.float32))
-
- gl.glUniform1f(self._PROGRAM.uniforms['alpha'], self.__alpha)
-
- for index, name in enumerate(('xPos', 'yPos', 'color')):
- attr = self._PROGRAM.attributes[name]
- gl.glEnableVertexAttribArray(attr)
- self.__vbos[index].setVertexAttrib(attr)
-
- with self.__indicesVbo:
- gl.glDrawElements(gl.GL_TRIANGLES,
- self.__triangles.size,
- glutils.numpyToGLType(self.__triangles.dtype),
- ctypes.c_void_p(0))
diff --git a/silx/gui/plot/backends/glutils/GLSupport.py b/silx/gui/plot/backends/glutils/GLSupport.py
deleted file mode 100644
index da6dffa..0000000
--- a/silx/gui/plot/backends/glutils/GLSupport.py
+++ /dev/null
@@ -1,158 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2018 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 convenient classes and functions for OpenGL rendering.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/04/2017"
-
-
-import numpy
-
-from ...._glutils import gl
-
-
-def buildFillMaskIndices(nIndices, dtype=None):
- """Returns triangle strip indices for rendering a filled polygon mask
-
- :param int nIndices: Number of points
- :param Union[numpy.dtype,None] dtype:
- If specified the dtype of the returned indices array
- :return: 1D array of indices constructing a triangle strip
- :rtype: numpy.ndarray
- """
- if dtype is None:
- if nIndices <= numpy.iinfo(numpy.uint16).max + 1:
- dtype = numpy.uint16
- else:
- dtype = numpy.uint32
-
- lastIndex = nIndices - 1
- splitIndex = lastIndex // 2 + 1
- indices = numpy.empty(nIndices, dtype=dtype)
- indices[::2] = numpy.arange(0, splitIndex, step=1, dtype=dtype)
- indices[1::2] = numpy.arange(lastIndex, splitIndex - 1, step=-1,
- dtype=dtype)
- return indices
-
-
-class FilledShape2D(object):
- _NO_HATCH = 0
- _HATCH_STEP = 20
-
- def __init__(self, points, style='solid', color=(0., 0., 0., 1.)):
- self.vertices = numpy.array(points, dtype=numpy.float32, copy=False)
- self._indices = buildFillMaskIndices(len(self.vertices))
-
- tVertex = numpy.transpose(self.vertices)
- xMin, xMax = min(tVertex[0]), max(tVertex[0])
- yMin, yMax = min(tVertex[1]), max(tVertex[1])
- self.bboxVertices = numpy.array(((xMin, yMin), (xMin, yMax),
- (xMax, yMin), (xMax, yMax)),
- dtype=numpy.float32)
- self._xMin, self._xMax = xMin, xMax
- self._yMin, self._yMax = yMin, yMax
-
- self.style = style
- self.color = color
-
- def render(self, posAttrib, colorUnif, hatchStepUnif):
- assert self.style in ('hatch', 'solid')
- gl.glUniform4f(colorUnif, *self.color)
- step = self._HATCH_STEP if self.style == 'hatch' else self._NO_HATCH
- gl.glUniform1i(hatchStepUnif, step)
-
- # Prepare fill mask
- gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, self.vertices)
-
- gl.glEnable(gl.GL_STENCIL_TEST)
- gl.glStencilMask(1)
- gl.glStencilFunc(gl.GL_ALWAYS, 1, 1)
- gl.glStencilOp(gl.GL_INVERT, gl.GL_INVERT, gl.GL_INVERT)
- gl.glColorMask(gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE, gl.GL_FALSE)
- gl.glDepthMask(gl.GL_FALSE)
-
- gl.glDrawElements(gl.GL_TRIANGLE_STRIP, len(self._indices),
- gl.GL_UNSIGNED_SHORT, self._indices)
-
- gl.glStencilFunc(gl.GL_EQUAL, 1, 1)
- # Reset stencil while drawing
- gl.glStencilOp(gl.GL_ZERO, gl.GL_ZERO, gl.GL_ZERO)
- gl.glColorMask(gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE, gl.GL_TRUE)
- gl.glDepthMask(gl.GL_TRUE)
-
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, self.bboxVertices)
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self.bboxVertices))
-
- gl.glDisable(gl.GL_STENCIL_TEST)
-
-
-# matrix ######################################################################
-
-def mat4Ortho(left, right, bottom, top, near, far):
- """Orthographic projection matrix (row-major)"""
- return numpy.array((
- (2./(right - left), 0., 0., -(right+left)/float(right-left)),
- (0., 2./(top - bottom), 0., -(top+bottom)/float(top-bottom)),
- (0., 0., -2./(far-near), -(far+near)/float(far-near)),
- (0., 0., 0., 1.)), dtype=numpy.float64)
-
-
-def mat4Translate(x=0., y=0., z=0.):
- """Translation matrix (row-major)"""
- return numpy.array((
- (1., 0., 0., x),
- (0., 1., 0., y),
- (0., 0., 1., z),
- (0., 0., 0., 1.)), dtype=numpy.float64)
-
-
-def mat4Scale(sx=1., sy=1., sz=1.):
- """Scale matrix (row-major)"""
- return numpy.array((
- (sx, 0., 0., 0.),
- (0., sy, 0., 0.),
- (0., 0., sz, 0.),
- (0., 0., 0., 1.)), dtype=numpy.float64)
-
-
-def mat4Identity():
- """Identity matrix"""
- return numpy.array((
- (1., 0., 0., 0.),
- (0., 1., 0., 0.),
- (0., 0., 1., 0.),
- (0., 0., 0., 1.)), dtype=numpy.float64)
diff --git a/silx/gui/plot/backends/glutils/GLText.py b/silx/gui/plot/backends/glutils/GLText.py
deleted file mode 100644
index d6ae6fa..0000000
--- a/silx/gui/plot/backends/glutils/GLText.py
+++ /dev/null
@@ -1,287 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 minimalistic text support for OpenGL.
-It provides Latin-1 (ISO8859-1) characters for one monospace font at one size.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/04/2017"
-
-
-from collections import OrderedDict
-import weakref
-
-import numpy
-
-from ...._glutils import font, gl, Context, Program, Texture
-from .GLSupport import mat4Translate
-
-
-# TODO: Font should be configurable by the main program: using mpl.rcParams?
-
-
-class _Cache(object):
- """LRU (Least Recent Used) cache.
-
- :param int maxsize: Maximum number of (key, value) pairs in the cache
- :param callable callback:
- Called when a (key, value) pair is removed from the cache.
- It must take 2 arguments: key and value.
- """
-
- def __init__(self, maxsize=128, callback=None):
- self._maxsize = int(maxsize)
- self._callback = callback
- self._cache = OrderedDict()
-
- def __contains__(self, item):
- return item in self._cache
-
- def __getitem__(self, key):
- if key in self._cache:
- # Remove/add key from ordered dict to store last access info
- value = self._cache.pop(key)
- self._cache[key] = value
- return value
- else:
- raise KeyError
-
- def __setitem__(self, key, value):
- """Add a key, value pair to the cache.
-
- :param key: The key to set
- :param value: The corresponding value
- """
- if key not in self._cache and len(self._cache) >= self._maxsize:
- removedKey, removedValue = self._cache.popitem(last=False)
- if self._callback is not None:
- self._callback(removedKey, removedValue)
- self._cache[key] = value
-
-
-# Text2D ######################################################################
-
-LEFT, CENTER, RIGHT = 'left', 'center', 'right'
-TOP, BASELINE, BOTTOM = 'top', 'baseline', 'bottom'
-ROTATE_90, ROTATE_180, ROTATE_270 = 90, 180, 270
-
-
-class Text2D(object):
-
- _SHADERS = {
- 'vertex': """
- #version 120
-
- attribute vec2 position;
- attribute vec2 texCoords;
- uniform mat4 matrix;
-
- varying vec2 vCoords;
-
- void main(void) {
- gl_Position = matrix * vec4(position, 0.0, 1.0);
- vCoords = texCoords;
- }
- """,
- 'fragment': """
- #version 120
-
- uniform sampler2D texText;
- uniform vec4 color;
- uniform vec4 bgColor;
-
- varying vec2 vCoords;
-
- void main(void) {
- gl_FragColor = mix(bgColor, color, texture2D(texText, vCoords).r);
- }
- """
- }
-
- _TEX_COORDS = numpy.array(((0., 0.), (1., 0.), (0., 1.), (1., 1.)),
- dtype=numpy.float32).ravel()
-
- _program = Program(_SHADERS['vertex'],
- _SHADERS['fragment'],
- attrib0='position')
-
- # Discard texture objects when removed from the cache
- _textures = weakref.WeakKeyDictionary()
- """Cache already created textures"""
-
- _sizes = _Cache()
- """Cache already computed sizes"""
-
- def __init__(self, text, x=0, y=0,
- color=(0., 0., 0., 1.),
- bgColor=None,
- align=LEFT, valign=BASELINE,
- rotate=0,
- devicePixelRatio= 1.):
- self.devicePixelRatio = devicePixelRatio
- self._vertices = None
- self._text = text
- self.x = x
- self.y = y
- self.color = color
- self.bgColor = bgColor
-
- if align not in (LEFT, CENTER, RIGHT):
- raise ValueError(
- "Horizontal alignment not supported: {0}".format(align))
- self._align = align
-
- if valign not in (TOP, CENTER, BASELINE, BOTTOM):
- raise ValueError(
- "Vertical alignment not supported: {0}".format(valign))
- self._valign = valign
-
- self._rotate = numpy.radians(rotate)
-
- def _getTexture(self, text, devicePixelRatio):
- # Retrieve/initialize texture cache for current context
- textureKey = text, devicePixelRatio
-
- context = Context.getCurrent()
- if context not in self._textures:
- self._textures[context] = _Cache(
- callback=lambda key, value: value[0].discard())
- textures = self._textures[context]
-
- if textureKey not in textures:
- image, offset = font.rasterText(
- text,
- font.getDefaultFontFamily(),
- devicePixelRatio=self.devicePixelRatio)
- if textureKey not in self._sizes:
- self._sizes[textureKey] = image.shape[1], image.shape[0]
-
- texture = Texture(
- gl.GL_RED,
- data=image,
- minFilter=gl.GL_NEAREST,
- magFilter=gl.GL_NEAREST,
- wrap=(gl.GL_CLAMP_TO_EDGE,
- gl.GL_CLAMP_TO_EDGE))
- texture.prepare()
- textures[textureKey] = texture, offset
-
- return textures[textureKey]
-
- @property
- def text(self):
- return self._text
-
- @property
- def size(self):
- textureKey = self.text, self.devicePixelRatio
- if textureKey not in self._sizes:
- image, offset = font.rasterText(
- self.text,
- font.getDefaultFontFamily(),
- devicePixelRatio=self.devicePixelRatio)
- self._sizes[textureKey] = image.shape[1], image.shape[0]
- return self._sizes[textureKey]
-
- def getVertices(self, offset, shape):
- height, width = shape
-
- if self._align == LEFT:
- xOrig = 0
- elif self._align == RIGHT:
- xOrig = - width
- else: # CENTER
- xOrig = - width // 2
-
- if self._valign == BASELINE:
- yOrig = - offset
- elif self._valign == TOP:
- yOrig = 0
- elif self._valign == BOTTOM:
- yOrig = - height
- else: # CENTER
- yOrig = - height // 2
-
- vertices = numpy.array((
- (xOrig, yOrig),
- (xOrig + width, yOrig),
- (xOrig, yOrig + height),
- (xOrig + width, yOrig + height)), dtype=numpy.float32)
-
- cos, sin = numpy.cos(self._rotate), numpy.sin(self._rotate)
- vertices = numpy.ascontiguousarray(numpy.transpose(numpy.array((
- cos * vertices[:, 0] - sin * vertices[:, 1],
- sin * vertices[:, 0] + cos * vertices[:, 1]),
- dtype=numpy.float32)))
-
- return vertices
-
- def render(self, matrix):
- if not self.text:
- return
-
- prog = self._program
- prog.use()
-
- texUnit = 0
- texture, offset = self._getTexture(self.text, self.devicePixelRatio)
-
- gl.glUniform1i(prog.uniforms['texText'], texUnit)
-
- mat = numpy.dot(matrix, mat4Translate(int(self.x), int(self.y)))
- gl.glUniformMatrix4fv(prog.uniforms['matrix'], 1, gl.GL_TRUE,
- mat.astype(numpy.float32))
-
- gl.glUniform4f(prog.uniforms['color'], *self.color)
- if self.bgColor is not None:
- bgColor = self.bgColor
- else:
- bgColor = self.color[0], self.color[1], self.color[2], 0.
- gl.glUniform4f(prog.uniforms['bgColor'], *bgColor)
-
- vertices = self.getVertices(offset, texture.shape)
-
- posAttrib = prog.attributes['position']
- gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- vertices)
-
- texAttrib = prog.attributes['texCoords']
- gl.glEnableVertexAttribArray(texAttrib)
- gl.glVertexAttribPointer(texAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0,
- self._TEX_COORDS)
-
- with texture:
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, 4)
diff --git a/silx/gui/plot/backends/glutils/GLTexture.py b/silx/gui/plot/backends/glutils/GLTexture.py
deleted file mode 100644
index 37fbdd0..0000000
--- a/silx/gui/plot/backends/glutils/GLTexture.py
+++ /dev/null
@@ -1,241 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 classes wrapping OpenGL texture."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/04/2017"
-
-
-from ctypes import c_void_p
-import logging
-
-import numpy
-
-from ...._glutils import gl, Texture, numpyToGLType
-
-
-_logger = logging.getLogger(__name__)
-
-
-def _checkTexture2D(internalFormat, shape,
- format_=None, type_=gl.GL_FLOAT, border=0):
- """Check if texture size with provided parameters is supported
-
- :rtype: bool
- """
- height, width = shape
- gl.glTexImage2D(gl.GL_PROXY_TEXTURE_2D, 0, internalFormat,
- width, height, border,
- format_ or internalFormat,
- type_, c_void_p(0))
- width = gl.glGetTexLevelParameteriv(
- gl.GL_PROXY_TEXTURE_2D, 0, gl.GL_TEXTURE_WIDTH)
- return bool(width)
-
-
-MIN_TEXTURE_SIZE = 64
-
-
-def _getMaxSquareTexture2DSize(internalFormat=gl.GL_RGBA,
- format_=None,
- type_=gl.GL_FLOAT,
- border=0):
- """Returns a supported size for a corresponding square texture
-
- :returns: GL_MAX_TEXTURE_SIZE or a smaller supported size (not optimal)
- :rtype: int
- """
- # Is this useful?
- maxTexSize = gl.glGetIntegerv(gl.GL_MAX_TEXTURE_SIZE)
- while maxTexSize > MIN_TEXTURE_SIZE and \
- not _checkTexture2D(internalFormat, (maxTexSize, maxTexSize),
- format_, type_, border):
- maxTexSize //= 2
- return max(MIN_TEXTURE_SIZE, maxTexSize)
-
-
-class Image(object):
- """Image of any size eventually using multiple textures or larger texture
- """
-
- _WRAP = (gl.GL_CLAMP_TO_EDGE, gl.GL_CLAMP_TO_EDGE)
- _MIN_FILTER = gl.GL_NEAREST
- _MAG_FILTER = gl.GL_NEAREST
-
- def __init__(self, internalFormat, data, format_=None, texUnit=0):
- self.internalFormat = internalFormat
- self.height, self.width = data.shape[0:2]
- type_ = numpyToGLType(data.dtype)
-
- if _checkTexture2D(internalFormat, data.shape[0:2], format_, type_):
- texture = Texture(internalFormat,
- data,
- format_,
- texUnit=texUnit,
- minFilter=self._MIN_FILTER,
- magFilter=self._MAG_FILTER,
- wrap=self._WRAP)
- texture.prepare()
- vertices = numpy.array((
- (0., 0., 0., 0.),
- (self.width, 0., 1., 0.),
- (0., self.height, 0., 1.),
- (self.width, self.height, 1., 1.)), dtype=numpy.float32)
- self.tiles = ((texture, vertices,
- {'xOrigData': 0, 'yOrigData': 0,
- 'wData': self.width, 'hData': self.height}),)
-
- else:
- # Handle dimension too large: make tiles
- maxTexSize = _getMaxSquareTexture2DSize(internalFormat,
- format_, type_)
-
- nCols = (self.width+maxTexSize-1) // maxTexSize
- colWidths = [self.width // nCols] * nCols
- colWidths[-1] += self.width % nCols
-
- nRows = (self.height+maxTexSize-1) // maxTexSize
- rowHeights = [self.height//nRows] * nRows
- rowHeights[-1] += self.height % nRows
-
- tiles = []
- yOrig = 0
- for hData in rowHeights:
- xOrig = 0
- for wData in colWidths:
- if (hData < MIN_TEXTURE_SIZE or wData < MIN_TEXTURE_SIZE) \
- and not _checkTexture2D(internalFormat,
- (hData, wData),
- format_,
- type_):
- # Ensure texture size is at least MIN_TEXTURE_SIZE
- tH = max(hData, MIN_TEXTURE_SIZE)
- tW = max(wData, MIN_TEXTURE_SIZE)
-
- uMax, vMax = float(wData)/tW, float(hData)/tH
-
- # TODO issue with type_ and alignment
- texture = Texture(internalFormat,
- data=None,
- format_=format_,
- shape=(tH, tW),
- texUnit=texUnit,
- minFilter=self._MIN_FILTER,
- magFilter=self._MAG_FILTER,
- wrap=self._WRAP)
- # TODO handle unpack
- texture.update(format_,
- data[yOrig:yOrig+hData,
- xOrig:xOrig+wData])
- # texture.update(format_, type_, data,
- # width=wData, height=hData,
- # unpackRowLength=width,
- # unpackSkipPixels=xOrig,
- # unpackSkipRows=yOrig)
- else:
- uMax, vMax = 1, 1
- # TODO issue with type_ and unpacking tiles
- # TODO idea to handle unpack: use array strides
- # As it is now, it will make a copy
- texture = Texture(internalFormat,
- data[yOrig:yOrig+hData,
- xOrig:xOrig+wData],
- format_,
- texUnit=texUnit,
- minFilter=self._MIN_FILTER,
- magFilter=self._MAG_FILTER,
- wrap=self._WRAP)
- # TODO
- # unpackRowLength=width,
- # unpackSkipPixels=xOrig,
- # unpackSkipRows=yOrig)
- vertices = numpy.array((
- (xOrig, yOrig, 0., 0.),
- (xOrig + wData, yOrig, uMax, 0.),
- (xOrig, yOrig + hData, 0., vMax),
- (xOrig + wData, yOrig + hData, uMax, vMax)),
- dtype=numpy.float32)
- texture.prepare()
- tiles.append((texture, vertices,
- {'xOrigData': xOrig, 'yOrigData': yOrig,
- 'wData': wData, 'hData': hData}))
- xOrig += wData
- yOrig += hData
- self.tiles = tuple(tiles)
-
- def discard(self):
- for texture, vertices, _ in self.tiles:
- texture.discard()
- del self.tiles
-
- def updateAll(self, format_, data, texUnit=0):
- if not hasattr(self, 'tiles'):
- raise RuntimeError("No texture, discard has already been called")
-
- assert data.shape[:2] == (self.height, self.width)
- if len(self.tiles) == 1:
- self.tiles[0][0].update(format_, data, texUnit=texUnit)
- else:
- for texture, _, info in self.tiles:
- yOrig, xOrig = info['yOrigData'], info['xOrigData']
- height, width = info['hData'], info['wData']
- texture.update(format_,
- data[yOrig:yOrig+height, xOrig:xOrig+width],
- texUnit=texUnit)
- texture.prepare()
- # TODO check
- # width=info['wData'], height=info['hData'],
- # texUnit=texUnit, unpackAlign=unpackAlign,
- # unpackRowLength=self.width,
- # unpackSkipPixels=info['xOrigData'],
- # unpackSkipRows=info['yOrigData'])
-
- def render(self, posAttrib, texAttrib, texUnit=0):
- try:
- tiles = self.tiles
- except AttributeError:
- raise RuntimeError("No texture, discard has already been called")
-
- for texture, vertices, _ in tiles:
- texture.bind(texUnit)
-
- stride = vertices.shape[-1] * vertices.itemsize
- gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, vertices)
-
- texCoordsPtr = c_void_p(vertices.ctypes.data +
- 2 * vertices.itemsize)
- gl.glEnableVertexAttribArray(texAttrib)
- gl.glVertexAttribPointer(texAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, texCoordsPtr)
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(vertices))
diff --git a/silx/gui/plot/backends/glutils/PlotImageFile.py b/silx/gui/plot/backends/glutils/PlotImageFile.py
deleted file mode 100644
index 5fb6853..0000000
--- a/silx/gui/plot/backends/glutils/PlotImageFile.py
+++ /dev/null
@@ -1,153 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ############################################################################*/
-"""Function to save an image to a file."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/04/2017"
-
-
-import base64
-import struct
-import sys
-import zlib
-
-
-# Image writer ################################################################
-
-def convertRGBDataToPNG(data):
- """Convert a RGB bitmap to PNG.
-
- It only supports RGB bitmap with one byte per channel stored as a 3D array.
- See `Definitive Guide <http://www.libpng.org/pub/png/book/>`_ and
- `Specification <http://www.libpng.org/pub/png/spec/1.2/>`_ for details.
-
- :param data: A 3D array (h, w, rgb) storing an RGB image
- :type data: numpy.ndarray of unsigned bytes
- :returns: The PNG encoded data
- :rtype: bytes
- """
- height, width = data.shape[0], data.shape[1]
- depth = 8 # 8 bit per channel
- colorType = 2 # 'truecolor' = RGB
- interlace = 0 # No
-
- IHDRdata = struct.pack(">ccccIIBBBBB", b'I', b'H', b'D', b'R',
- width, height, depth, colorType,
- 0, 0, interlace)
-
- # Add filter 'None' before each scanline
- preparedData = b'\x00' + b'\x00'.join(line.tobytes() for line in data)
- compressedData = zlib.compress(preparedData, 8)
-
- IDATdata = struct.pack("cccc", b'I', b'D', b'A', b'T')
- IDATdata += compressedData
-
- return b''.join([
- b'\x89PNG\r\n\x1a\n', # PNG signature
- # IHDR chunk: Image Header
- struct.pack(">I", 13), # length
- IHDRdata,
- struct.pack(">I", zlib.crc32(IHDRdata) & 0xffffffff), # CRC
- # IDAT chunk: Payload
- struct.pack(">I", len(compressedData)),
- IDATdata,
- struct.pack(">I", zlib.crc32(IDATdata) & 0xffffffff), # CRC
- b'\x00\x00\x00\x00IEND\xaeB`\x82' # IEND chunk: footer
- ])
-
-
-def saveImageToFile(data, fileNameOrObj, fileFormat):
- """Save a RGB image to a file.
-
- :param data: A 3D array (h, w, 3) storing an RGB image.
- :type data: numpy.ndarray with of unsigned bytes.
- :param fileNameOrObj: Filename or object to use to write the image.
- :type fileNameOrObj: A str or a 'file-like' object with a 'write' method.
- :param str fileFormat: The type of the file in: 'png', 'ppm', 'svg', 'tiff'.
- """
- assert len(data.shape) == 3
- assert data.shape[2] == 3
- assert fileFormat in ('png', 'ppm', 'svg', 'tiff')
-
- if not hasattr(fileNameOrObj, 'write'):
- if sys.version_info < (3, ):
- fileObj = open(fileNameOrObj, "wb")
- else:
- if fileFormat in ('png', 'ppm', 'tiff'):
- # Open in binary mode
- fileObj = open(fileNameOrObj, 'wb')
- else:
- fileObj = open(fileNameOrObj, 'w', newline='')
- else: # Use as a file-like object
- fileObj = fileNameOrObj
-
- if fileFormat == 'svg':
- height, width = data.shape[:2]
- base64Data = base64.b64encode(convertRGBDataToPNG(data))
-
- fileObj.write(
- '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n')
- fileObj.write('<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN"\n')
- fileObj.write(
- ' "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n')
- fileObj.write('<svg xmlns:xlink="http://www.w3.org/1999/xlink"\n')
- fileObj.write(' xmlns="http://www.w3.org/2000/svg"\n')
- fileObj.write(' version="1.1"\n')
- fileObj.write(' width="%d"\n' % width)
- fileObj.write(' height="%d">\n' % height)
- fileObj.write(' <image xlink:href="data:image/png;base64,')
- fileObj.write(base64Data.decode('ascii'))
- fileObj.write('"\n')
- fileObj.write(' x="0"\n')
- fileObj.write(' y="0"\n')
- fileObj.write(' width="%d"\n' % width)
- fileObj.write(' height="%d"\n' % height)
- fileObj.write(' id="image" />\n')
- fileObj.write('</svg>')
-
- elif fileFormat == 'ppm':
- height, width = data.shape[:2]
-
- fileObj.write(b'P6\n')
- fileObj.write(b'%d %d\n' % (width, height))
- fileObj.write(b'255\n')
- fileObj.write(data.tobytes())
-
- elif fileFormat == 'png':
- fileObj.write(convertRGBDataToPNG(data))
-
- elif fileFormat == 'tiff':
- if fileObj == fileNameOrObj:
- raise NotImplementedError(
- 'Save TIFF to a file-like object not implemented')
-
- from silx.third_party.TiffIO import TiffIO
-
- tif = TiffIO(fileNameOrObj, mode='wb+')
- tif.writeImage(data, info={'Title': 'OpenGL Plot Snapshot'})
-
- if fileObj != fileNameOrObj:
- fileObj.close()
diff --git a/silx/gui/plot/backends/glutils/__init__.py b/silx/gui/plot/backends/glutils/__init__.py
deleted file mode 100644
index f87d7c1..0000000
--- a/silx/gui/plot/backends/glutils/__init__.py
+++ /dev/null
@@ -1,46 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 convenient classes for the OpenGL rendering backend.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/04/2017"
-
-
-import logging
-
-
-_logger = logging.getLogger(__name__)
-
-
-from .GLPlotCurve import * # noqa
-from .GLPlotFrame import * # noqa
-from .GLPlotImage import * # noqa
-from .GLPlotItem import GLPlotItem, RenderContext # noqa
-from .GLPlotTriangles import GLPlotTriangles # noqa
-from .GLSupport import * # noqa
-from .GLText import * # noqa
-from .GLTexture import * # noqa
diff --git a/silx/gui/plot/items/__init__.py b/silx/gui/plot/items/__init__.py
deleted file mode 100644
index 0484025..0000000
--- a/silx/gui/plot/items/__init__.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 classes that describes :class:`.PlotWidget` content.
-
-Instances of those classes are returned by :class:`.PlotWidget` methods that give
-access to its content such as :meth:`.PlotWidget.getCurve`, :meth:`.PlotWidget.getImage`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "22/06/2017"
-
-from .core import (Item, DataItem, # noqa
- LabelsMixIn, DraggableMixIn, ColormapMixIn, # noqa
- SymbolMixIn, ColorMixIn, YAxisMixIn, FillMixIn, # noqa
- AlphaMixIn, LineMixIn, ScatterVisualizationMixIn, # noqa
- ComplexMixIn, ItemChangedType, PointsBase) # noqa
-from .complex import ImageComplexData # noqa
-from .curve import Curve, CurveStyle # noqa
-from .histogram import Histogram # noqa
-from .image import ImageBase, ImageData, ImageRgba, ImageStack, MaskImageData # noqa
-from .shape import Shape, BoundingRect, XAxisExtent, YAxisExtent # noqa
-from .scatter import Scatter # noqa
-from .marker import MarkerBase, Marker, XMarker, YMarker # noqa
-from .axis import Axis, XAxis, YAxis, YRightAxis
-
-DATA_ITEMS = (ImageComplexData, Curve, Histogram, ImageBase, Scatter,
- BoundingRect, XAxisExtent, YAxisExtent)
-"""Classes of items representing data and to consider to compute data bounds.
-"""
diff --git a/silx/gui/plot/items/_arc_roi.py b/silx/gui/plot/items/_arc_roi.py
deleted file mode 100644
index 23416ec..0000000
--- a/silx/gui/plot/items/_arc_roi.py
+++ /dev/null
@@ -1,878 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2021 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 Arc ROI item for the :class:`~silx.gui.plot.PlotWidget`.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-import logging
-import numpy
-
-from ... import utils
-from .. import items
-from ...colors import rgba
-from ....utils.proxy import docstring
-from ._roi_base import HandleBasedROI
-from ._roi_base import InteractionModeMixIn
-from ._roi_base import RoiInteractionMode
-
-
-logger = logging.getLogger(__name__)
-
-
-class _ArcGeometry:
- """
- Non-mutable object to store the geometry of the arc ROI.
-
- The aim is is to switch between consistent state without dealing with
- intermediate values.
- """
- def __init__(self, center, startPoint, endPoint, radius,
- weight, startAngle, endAngle, closed=False):
- """Constructor for a consistent arc geometry.
-
- There is also specific class method to create different kind of arc
- geometry.
- """
- self.center = center
- self.startPoint = startPoint
- self.endPoint = endPoint
- self.radius = radius
- self.weight = weight
- self.startAngle = startAngle
- self.endAngle = endAngle
- self._closed = closed
-
- @classmethod
- def createEmpty(cls):
- """Create an arc geometry from an empty shape
- """
- zero = numpy.array([0, 0])
- return cls(zero, zero.copy(), zero.copy(), 0, 0, 0, 0)
-
- @classmethod
- def createRect(cls, startPoint, endPoint, weight):
- """Create an arc geometry from a definition of a rectangle
- """
- return cls(None, startPoint, endPoint, None, weight, None, None, False)
-
- @classmethod
- def createCircle(cls, center, startPoint, endPoint, radius,
- weight, startAngle, endAngle):
- """Create an arc geometry from a definition of a circle
- """
- return cls(center, startPoint, endPoint, radius,
- weight, startAngle, endAngle, True)
-
- def withWeight(self, weight):
- """Return a new geometry based on this object, with a specific weight
- """
- return _ArcGeometry(self.center, self.startPoint, self.endPoint,
- self.radius, weight,
- self.startAngle, self.endAngle, self._closed)
-
- def withRadius(self, radius):
- """Return a new geometry based on this object, with a specific radius.
-
- The weight and the center is conserved.
- """
- startPoint = self.center + (self.startPoint - self.center) / self.radius * radius
- endPoint = self.center + (self.endPoint - self.center) / self.radius * radius
- return _ArcGeometry(self.center, startPoint, endPoint,
- radius, self.weight,
- self.startAngle, self.endAngle, self._closed)
-
- def withStartAngle(self, startAngle):
- """Return a new geometry based on this object, with a specific start angle
- """
- vector = numpy.array([numpy.cos(startAngle), numpy.sin(startAngle)])
- startPoint = self.center + vector * self.radius
-
- # Never add more than 180 to maintain coherency
- deltaAngle = startAngle - self.startAngle
- if deltaAngle > numpy.pi:
- deltaAngle -= numpy.pi * 2
- elif deltaAngle < -numpy.pi:
- deltaAngle += numpy.pi * 2
-
- startAngle = self.startAngle + deltaAngle
- return _ArcGeometry(
- self.center,
- startPoint,
- self.endPoint,
- self.radius,
- self.weight,
- startAngle,
- self.endAngle,
- self._closed,
- )
-
- def withEndAngle(self, endAngle):
- """Return a new geometry based on this object, with a specific end angle
- """
- vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
- endPoint = self.center + vector * self.radius
-
- # Never add more than 180 to maintain coherency
- deltaAngle = endAngle - self.endAngle
- if deltaAngle > numpy.pi:
- deltaAngle -= numpy.pi * 2
- elif deltaAngle < -numpy.pi:
- deltaAngle += numpy.pi * 2
-
- endAngle = self.endAngle + deltaAngle
- return _ArcGeometry(
- self.center,
- self.startPoint,
- endPoint,
- self.radius,
- self.weight,
- self.startAngle,
- endAngle,
- self._closed,
- )
-
- def translated(self, dx, dy):
- """Return the translated geometry by dx, dy"""
- delta = numpy.array([dx, dy])
- center = None if self.center is None else self.center + delta
- startPoint = None if self.startPoint is None else self.startPoint + delta
- endPoint = None if self.endPoint is None else self.endPoint + delta
- return _ArcGeometry(center, startPoint, endPoint,
- self.radius, self.weight,
- self.startAngle, self.endAngle, self._closed)
-
- def getKind(self):
- """Returns the kind of shape defined"""
- if self.center is None:
- return "rect"
- elif numpy.isnan(self.startAngle):
- return "point"
- elif self.isClosed():
- if self.weight <= 0 or self.weight * 0.5 >= self.radius:
- return "circle"
- else:
- return "donut"
- else:
- if self.weight * 0.5 < self.radius:
- return "arc"
- else:
- return "camembert"
-
- def isClosed(self):
- """Returns True if the geometry is a circle like"""
- if self._closed is not None:
- return self._closed
- delta = numpy.abs(self.endAngle - self.startAngle)
- self._closed = numpy.isclose(delta, numpy.pi * 2)
- return self._closed
-
- def __str__(self):
- return str((self.center,
- self.startPoint,
- self.endPoint,
- self.radius,
- self.weight,
- self.startAngle,
- self.endAngle,
- self._closed))
-
-
-class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn):
- """A ROI identifying an arc of a circle with a width.
-
- This ROI provides
- - 3 handle to control the curvature
- - 1 handle to control the weight
- - 1 anchor to translate the shape.
- """
-
- ICON = 'add-shape-arc'
- NAME = 'arc ROI'
- SHORT_NAME = "arc"
- """Metadata for this kind of ROI"""
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- ThreePointMode = RoiInteractionMode("3 points", "Provides 3 points to define the main radius circle")
- PolarMode = RoiInteractionMode("Polar", "Provides anchors to edit the ROI in polar coords")
- # FIXME: MoveMode was designed cause there is too much anchors
- # FIXME: It would be good replace it by a dnd on the shape
- MoveMode = RoiInteractionMode("Translation", "Provides anchors to only move the ROI")
-
- def __init__(self, parent=None):
- HandleBasedROI.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- InteractionModeMixIn.__init__(self)
-
- self._geometry = _ArcGeometry.createEmpty()
- self._handleLabel = self.addLabelHandle()
-
- self._handleStart = self.addHandle()
- self._handleMid = self.addHandle()
- self._handleEnd = self.addHandle()
- self._handleWeight = self.addHandle()
- self._handleWeight._setConstraint(self._arcCurvatureMarkerConstraint)
- self._handleMove = self.addTranslateHandle()
-
- shape = items.Shape("polygon")
- shape.setPoints([[0, 0], [0, 0]])
- shape.setColor(rgba(self.getColor()))
- shape.setFill(False)
- shape.setOverlay(True)
- shape.setLineStyle(self.getLineStyle())
- shape.setLineWidth(self.getLineWidth())
- self.__shape = shape
- self.addItem(shape)
-
- self._initInteractionMode(self.ThreePointMode)
- self._interactiveModeUpdated(self.ThreePointMode)
-
- def availableInteractionModes(self):
- """Returns the list of available interaction modes
-
- :rtype: List[RoiInteractionMode]
- """
- return [self.ThreePointMode, self.PolarMode, self.MoveMode]
-
- def _interactiveModeUpdated(self, modeId):
- """Set the interaction mode.
-
- :param RoiInteractionMode modeId:
- """
- if modeId is self.ThreePointMode:
- self._handleStart.setSymbol("s")
- self._handleMid.setSymbol("s")
- self._handleEnd.setSymbol("s")
- self._handleWeight.setSymbol("d")
- self._handleMove.setSymbol("+")
- elif modeId is self.PolarMode:
- self._handleStart.setSymbol("o")
- self._handleMid.setSymbol("o")
- self._handleEnd.setSymbol("o")
- self._handleWeight.setSymbol("d")
- self._handleMove.setSymbol("+")
- elif modeId is self.MoveMode:
- self._handleStart.setSymbol("")
- self._handleMid.setSymbol("+")
- self._handleEnd.setSymbol("")
- self._handleWeight.setSymbol("")
- self._handleMove.setSymbol("+")
- else:
- assert False
- if self._geometry.isClosed():
- if modeId != self.MoveMode:
- self._handleStart.setSymbol("x")
- self._handleEnd.setSymbol("x")
- self._updateHandles()
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.VISIBLE:
- self._updateItemProperty(event, self, self.__shape)
- super(ArcROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(ArcROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- """"Initialize the ROI using the points from the first interaction.
-
- This interaction is constrained by the plot API and only supports few
- shapes.
- """
- # The first shape is a line
- point0 = points[0]
- point1 = points[1]
-
- # Compute a non collinear point for the curvature
- center = (point1 + point0) * 0.5
- normal = point1 - center
- normal = numpy.array((normal[1], -normal[0]))
- defaultCurvature = numpy.pi / 5.0
- weightCoef = 0.20
- mid = center - normal * defaultCurvature
- distance = numpy.linalg.norm(point0 - point1)
- weight = distance * weightCoef
-
- geometry = self._createGeometryFromControlPoints(point0, mid, point1, weight)
- self._geometry = geometry
- self._updateHandles()
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def _updateMidHandle(self):
- """Keep the same geometry, but update the location of the control
- points.
-
- So calling this function do not trigger sigRegionChanged.
- """
- geometry = self._geometry
-
- if geometry.isClosed():
- start = numpy.array(self._handleStart.getPosition())
- midPos = geometry.center + geometry.center - start
- else:
- if geometry.center is None:
- midPos = geometry.startPoint * 0.5 + geometry.endPoint * 0.5
- else:
- midAngle = geometry.startAngle * 0.5 + geometry.endAngle * 0.5
- vector = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
- midPos = geometry.center + geometry.radius * vector
-
- with utils.blockSignals(self._handleMid):
- self._handleMid.setPosition(*midPos)
-
- def _updateWeightHandle(self):
- geometry = self._geometry
- if geometry.center is None:
- # rectangle
- center = (geometry.startPoint + geometry.endPoint) * 0.5
- normal = geometry.endPoint - geometry.startPoint
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- if distance != 0:
- normal = normal / distance
- weightPos = center + normal * geometry.weight * 0.5
- else:
- if geometry.isClosed():
- midAngle = geometry.startAngle + numpy.pi * 0.5
- elif geometry.center is not None:
- midAngle = (geometry.startAngle + geometry.endAngle) * 0.5
- vector = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
- weightPos = geometry.center + (geometry.radius + geometry.weight * 0.5) * vector
-
- with utils.blockSignals(self._handleWeight):
- self._handleWeight.setPosition(*weightPos)
-
- def _getWeightFromHandle(self, weightPos):
- geometry = self._geometry
- if geometry.center is None:
- # rectangle
- center = (geometry.startPoint + geometry.endPoint) * 0.5
- return numpy.linalg.norm(center - weightPos) * 2
- else:
- distance = numpy.linalg.norm(geometry.center - weightPos)
- return abs(distance - geometry.radius) * 2
-
- def _updateHandles(self):
- geometry = self._geometry
- with utils.blockSignals(self._handleStart):
- self._handleStart.setPosition(*geometry.startPoint)
- with utils.blockSignals(self._handleEnd):
- self._handleEnd.setPosition(*geometry.endPoint)
-
- self._updateMidHandle()
- self._updateWeightHandle()
- self._updateShape()
-
- def _updateCurvature(self, start, mid, end, updateCurveHandles, checkClosed=False, updateStart=False):
- """Update the curvature using 3 control points in the curve
-
- :param bool updateCurveHandles: If False curve handles are already at
- the right location
- """
- if checkClosed:
- closed = self._isCloseInPixel(start, end)
- else:
- closed = self._geometry.isClosed()
- if closed:
- if updateStart:
- start = end
- else:
- end = start
-
- if updateCurveHandles:
- with utils.blockSignals(self._handleStart):
- self._handleStart.setPosition(*start)
- with utils.blockSignals(self._handleMid):
- self._handleMid.setPosition(*mid)
- with utils.blockSignals(self._handleEnd):
- self._handleEnd.setPosition(*end)
-
- weight = self._geometry.weight
- geometry = self._createGeometryFromControlPoints(start, mid, end, weight, closed=closed)
- self._geometry = geometry
-
- self._updateWeightHandle()
- self._updateShape()
-
- def _updateCloseInAngle(self, geometry, updateStart):
- azim = numpy.abs(geometry.endAngle - geometry.startAngle)
- if numpy.pi < azim < 3 * numpy.pi:
- closed = self._isCloseInPixel(geometry.startPoint, geometry.endPoint)
- geometry._closed = closed
- if closed:
- sign = 1 if geometry.startAngle < geometry.endAngle else -1
- if updateStart:
- geometry.startPoint = geometry.endPoint
- geometry.startAngle = geometry.endAngle - sign * 2*numpy.pi
- else:
- geometry.endPoint = geometry.startPoint
- geometry.endAngle = geometry.startAngle + sign * 2*numpy.pi
-
- def handleDragUpdated(self, handle, origin, previous, current):
- modeId = self.getInteractionMode()
- if handle is self._handleStart:
- if modeId is self.ThreePointMode:
- mid = numpy.array(self._handleMid.getPosition())
- end = numpy.array(self._handleEnd.getPosition())
- self._updateCurvature(
- current, mid, end, checkClosed=True, updateStart=True,
- updateCurveHandles=False
- )
- elif modeId is self.PolarMode:
- v = current - self._geometry.center
- startAngle = numpy.angle(complex(v[0], v[1]))
- geometry = self._geometry.withStartAngle(startAngle)
- self._updateCloseInAngle(geometry, updateStart=True)
- self._geometry = geometry
- self._updateHandles()
- elif handle is self._handleMid:
- if modeId is self.ThreePointMode:
- if self._geometry.isClosed():
- radius = numpy.linalg.norm(self._geometry.center - current)
- self._geometry = self._geometry.withRadius(radius)
- self._updateHandles()
- else:
- start = numpy.array(self._handleStart.getPosition())
- end = numpy.array(self._handleEnd.getPosition())
- self._updateCurvature(start, current, end, updateCurveHandles=False)
- elif modeId is self.PolarMode:
- radius = numpy.linalg.norm(self._geometry.center - current)
- self._geometry = self._geometry.withRadius(radius)
- self._updateHandles()
- elif modeId is self.MoveMode:
- delta = current - previous
- self.translate(*delta)
- elif handle is self._handleEnd:
- if modeId is self.ThreePointMode:
- start = numpy.array(self._handleStart.getPosition())
- mid = numpy.array(self._handleMid.getPosition())
- self._updateCurvature(
- start, mid, current, checkClosed=True, updateStart=False,
- updateCurveHandles=False
- )
- elif modeId is self.PolarMode:
- v = current - self._geometry.center
- endAngle = numpy.angle(complex(v[0], v[1]))
- geometry = self._geometry.withEndAngle(endAngle)
- self._updateCloseInAngle(geometry, updateStart=False)
- self._geometry = geometry
- self._updateHandles()
- elif handle is self._handleWeight:
- weight = self._getWeightFromHandle(current)
- self._geometry = self._geometry.withWeight(weight)
- self._updateShape()
- elif handle is self._handleMove:
- delta = current - previous
- self.translate(*delta)
-
- def _isCloseInPixel(self, point1, point2):
- manager = self.parent()
- if manager is None:
- return False
- plot = manager.parent()
- if plot is None:
- return False
- point1 = plot.dataToPixel(*point1)
- if point1 is None:
- return False
- point2 = plot.dataToPixel(*point2)
- if point2 is None:
- return False
- return abs(point1[0] - point2[0]) + abs(point1[1] - point2[1]) < 15
-
- def _normalizeGeometry(self):
- """Keep the same phisical geometry, but with normalized parameters.
- """
- geometry = self._geometry
- if geometry.weight * 0.5 >= geometry.radius:
- radius = (geometry.weight * 0.5 + geometry.radius) * 0.5
- geometry = geometry.withRadius(radius)
- geometry = geometry.withWeight(radius * 2)
- self._geometry = geometry
- return True
- return False
-
- def handleDragFinished(self, handle, origin, current):
- modeId = self.getInteractionMode()
- if handle in [self._handleStart, self._handleMid, self._handleEnd]:
- if modeId is self.ThreePointMode:
- self._normalizeGeometry()
- self._updateHandles()
-
- if self._geometry.isClosed():
- if modeId is self.MoveMode:
- self._handleStart.setSymbol("")
- self._handleEnd.setSymbol("")
- else:
- self._handleStart.setSymbol("x")
- self._handleEnd.setSymbol("x")
- else:
- if modeId is self.ThreePointMode:
- self._handleStart.setSymbol("s")
- self._handleEnd.setSymbol("s")
- elif modeId is self.PolarMode:
- self._handleStart.setSymbol("o")
- self._handleEnd.setSymbol("o")
- if modeId is self.MoveMode:
- self._handleStart.setSymbol("")
- self._handleEnd.setSymbol("")
-
- def _createGeometryFromControlPoints(self, start, mid, end, weight, closed=None):
- """Returns the geometry of the object"""
- if closed or (closed is None and numpy.allclose(start, end)):
- # Special arc: It's a closed circle
- center = (start + mid) * 0.5
- radius = numpy.linalg.norm(start - center)
- v = start - center
- startAngle = numpy.angle(complex(v[0], v[1]))
- endAngle = startAngle + numpy.pi * 2.0
- return _ArcGeometry.createCircle(
- center, start, end, radius, weight, startAngle, endAngle
- )
-
- elif numpy.linalg.norm(numpy.cross(mid - start, end - start)) < 1e-5:
- # Degenerated arc, it's a rectangle
- return _ArcGeometry.createRect(start, end, weight)
- else:
- center, radius = self._circleEquation(start, mid, end)
- v = start - center
- startAngle = numpy.angle(complex(v[0], v[1]))
- v = mid - center
- midAngle = numpy.angle(complex(v[0], v[1]))
- v = end - center
- endAngle = numpy.angle(complex(v[0], v[1]))
-
- # Is it clockwise or anticlockwise
- relativeMid = (endAngle - midAngle + 2 * numpy.pi) % (2 * numpy.pi)
- relativeEnd = (endAngle - startAngle + 2 * numpy.pi) % (2 * numpy.pi)
- if relativeMid < relativeEnd:
- if endAngle < startAngle:
- endAngle += 2 * numpy.pi
- else:
- if endAngle > startAngle:
- endAngle -= 2 * numpy.pi
-
- return _ArcGeometry(center, start, end,
- radius, weight, startAngle, endAngle)
-
- def _createShapeFromGeometry(self, geometry):
- kind = geometry.getKind()
- if kind == "rect":
- # It is not an arc
- # but we can display it as an intermediate shape
- normal = geometry.endPoint - geometry.startPoint
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- if distance != 0:
- normal /= distance
- points = numpy.array([
- geometry.startPoint + normal * geometry.weight * 0.5,
- geometry.endPoint + normal * geometry.weight * 0.5,
- geometry.endPoint - normal * geometry.weight * 0.5,
- geometry.startPoint - normal * geometry.weight * 0.5])
- elif kind == "point":
- # It is not an arc
- # but we can display it as an intermediate shape
- # NOTE: At least 2 points are expected
- points = numpy.array([geometry.startPoint, geometry.startPoint])
- elif kind == "circle":
- outerRadius = geometry.radius + geometry.weight * 0.5
- angles = numpy.linspace(0, 2 * numpy.pi, num=50)
- # It's a circle
- points = []
- numpy.append(angles, angles[-1])
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.append(geometry.center + direction * outerRadius)
- points = numpy.array(points)
- elif kind == "donut":
- innerRadius = geometry.radius - geometry.weight * 0.5
- outerRadius = geometry.radius + geometry.weight * 0.5
- angles = numpy.linspace(0, 2 * numpy.pi, num=50)
- # It's a donut
- points = []
- # NOTE: NaN value allow to create 2 separated circle shapes
- # using a single plot item. It's a kind of cheat
- points.append(numpy.array([float("nan"), float("nan")]))
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.insert(0, geometry.center + direction * innerRadius)
- points.append(geometry.center + direction * outerRadius)
- points.append(numpy.array([float("nan"), float("nan")]))
- points = numpy.array(points)
- else:
- innerRadius = geometry.radius - geometry.weight * 0.5
- outerRadius = geometry.radius + geometry.weight * 0.5
-
- delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1
- if geometry.startAngle == geometry.endAngle:
- # Degenerated, it's a line (single radius)
- angle = geometry.startAngle
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points = []
- points.append(geometry.center + direction * innerRadius)
- points.append(geometry.center + direction * outerRadius)
- return numpy.array(points)
-
- angles = numpy.arange(geometry.startAngle, geometry.endAngle, delta)
- if angles[-1] != geometry.endAngle:
- angles = numpy.append(angles, geometry.endAngle)
-
- if kind == "camembert":
- # It's a part of camembert
- points = []
- points.append(geometry.center)
- points.append(geometry.startPoint)
- delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.append(geometry.center + direction * outerRadius)
- points.append(geometry.endPoint)
- points.append(geometry.center)
- elif kind == "arc":
- # It's a part of donut
- points = []
- points.append(geometry.startPoint)
- for angle in angles:
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- points.insert(0, geometry.center + direction * innerRadius)
- points.append(geometry.center + direction * outerRadius)
- points.insert(0, geometry.endPoint)
- points.append(geometry.endPoint)
- else:
- assert False
-
- points = numpy.array(points)
-
- return points
-
- def _updateShape(self):
- geometry = self._geometry
- points = self._createShapeFromGeometry(geometry)
- self.__shape.setPoints(points)
-
- index = numpy.nanargmin(points[:, 1])
- pos = points[index]
- with utils.blockSignals(self._handleLabel):
- self._handleLabel.setPosition(pos[0], pos[1])
-
- if geometry.center is None:
- movePos = geometry.startPoint * 0.34 + geometry.endPoint * 0.66
- else:
- movePos = geometry.center
-
- with utils.blockSignals(self._handleMove):
- self._handleMove.setPosition(*movePos)
-
- self.sigRegionChanged.emit()
-
- def getGeometry(self):
- """Returns a tuple containing the geometry of this ROI
-
- It is a symmetric function of :meth:`setGeometry`.
-
- If `startAngle` is smaller than `endAngle` the rotation is clockwise,
- else the rotation is anticlockwise.
-
- :rtype: Tuple[numpy.ndarray,float,float,float,float]
- :raise ValueError: In case the ROI can't be represented as section of
- a circle
- """
- geometry = self._geometry
- if geometry.center is None:
- raise ValueError("This ROI can't be represented as a section of circle")
- return geometry.center, self.getInnerRadius(), self.getOuterRadius(), geometry.startAngle, geometry.endAngle
-
- def isClosed(self):
- """Returns true if the arc is a closed shape, like a circle or a donut.
-
- :rtype: bool
- """
- return self._geometry.isClosed()
-
- def getCenter(self):
- """Returns the center of the circle used to draw arcs of this ROI.
-
- This center is usually outside the the shape itself.
-
- :rtype: numpy.ndarray
- """
- return self._geometry.center
-
- def getStartAngle(self):
- """Returns the angle of the start of the section of this ROI (in radian).
-
- If `startAngle` is smaller than `endAngle` the rotation is clockwise,
- else the rotation is anticlockwise.
-
- :rtype: float
- """
- return self._geometry.startAngle
-
- def getEndAngle(self):
- """Returns the angle of the end of the section of this ROI (in radian).
-
- If `startAngle` is smaller than `endAngle` the rotation is clockwise,
- else the rotation is anticlockwise.
-
- :rtype: float
- """
- return self._geometry.endAngle
-
- def getInnerRadius(self):
- """Returns the radius of the smaller arc used to draw this ROI.
-
- :rtype: float
- """
- geometry = self._geometry
- radius = geometry.radius - geometry.weight * 0.5
- if radius < 0:
- radius = 0
- return radius
-
- def getOuterRadius(self):
- """Returns the radius of the bigger arc used to draw this ROI.
-
- :rtype: float
- """
- geometry = self._geometry
- radius = geometry.radius + geometry.weight * 0.5
- return radius
-
- def setGeometry(self, center, innerRadius, outerRadius, startAngle, endAngle):
- """
- Set the geometry of this arc.
-
- :param numpy.ndarray center: Center of the circle.
- :param float innerRadius: Radius of the smaller arc of the section.
- :param float outerRadius: Weight of the bigger arc of the section.
- It have to be bigger than `innerRadius`
- :param float startAngle: Location of the start of the section (in radian)
- :param float endAngle: Location of the end of the section (in radian).
- If `startAngle` is smaller than `endAngle` the rotation is clockwise,
- else the rotation is anticlockwise.
- """
- if innerRadius > outerRadius:
- logger.error("inner radius larger than outer radius")
- innerRadius, outerRadius = outerRadius, innerRadius
- center = numpy.array(center)
- radius = (innerRadius + outerRadius) * 0.5
- weight = outerRadius - innerRadius
-
- vector = numpy.array([numpy.cos(startAngle), numpy.sin(startAngle)])
- startPoint = center + vector * radius
- vector = numpy.array([numpy.cos(endAngle), numpy.sin(endAngle)])
- endPoint = center + vector * radius
-
- geometry = _ArcGeometry(center, startPoint, endPoint,
- radius, weight,
- startAngle, endAngle, closed=None)
- self._geometry = geometry
- self._updateHandles()
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- # first check distance, fastest
- center = self.getCenter()
- distance = numpy.sqrt((position[1] - center[1]) ** 2 + ((position[0] - center[0])) ** 2)
- is_in_distance = self.getInnerRadius() <= distance <= self.getOuterRadius()
- if not is_in_distance:
- return False
- rel_pos = position[1] - center[1], position[0] - center[0]
- angle = numpy.arctan2(*rel_pos)
- # angle is inside [-pi, pi]
-
- # Normalize the start angle between [-pi, pi]
- # with a positive angle range
- start_angle = self.getStartAngle()
- end_angle = self.getEndAngle()
- azim_range = end_angle - start_angle
- if azim_range < 0:
- start_angle = end_angle
- azim_range = -azim_range
- start_angle = numpy.mod(start_angle + numpy.pi, 2 * numpy.pi) - numpy.pi
-
- if angle < start_angle:
- angle += 2 * numpy.pi
- return start_angle <= angle <= start_angle + azim_range
-
- def translate(self, x, y):
- self._geometry = self._geometry.translated(x, y)
- self._updateHandles()
-
- def _arcCurvatureMarkerConstraint(self, x, y):
- """Curvature marker remains on perpendicular bisector"""
- geometry = self._geometry
- if geometry.center is None:
- center = (geometry.startPoint + geometry.endPoint) * 0.5
- vector = geometry.startPoint - geometry.endPoint
- vector = numpy.array((vector[1], -vector[0]))
- vdist = numpy.linalg.norm(vector)
- if vdist != 0:
- normal = numpy.array((vector[1], -vector[0])) / vdist
- else:
- normal = numpy.array((0, 0))
- else:
- if geometry.isClosed():
- midAngle = geometry.startAngle + numpy.pi * 0.5
- else:
- midAngle = (geometry.startAngle + geometry.endAngle) * 0.5
- normal = numpy.array([numpy.cos(midAngle), numpy.sin(midAngle)])
- center = geometry.center
- dist = numpy.dot(normal, (numpy.array((x, y)) - center))
- dist = numpy.clip(dist, geometry.radius, geometry.radius * 2)
- x, y = center + dist * normal
- return x, y
-
- @staticmethod
- def _circleEquation(pt1, pt2, pt3):
- """Circle equation from 3 (x, y) points
-
- :return: Position of the center of the circle and the radius
- :rtype: Tuple[Tuple[float,float],float]
- """
- x, y, z = complex(*pt1), complex(*pt2), complex(*pt3)
- w = z - x
- w /= y - x
- c = (x - y) * (w - abs(w) ** 2) / 2j / w.imag - x
- return numpy.array((-c.real, -c.imag)), abs(c + x)
-
- def __str__(self):
- try:
- center, innerRadius, outerRadius, startAngle, endAngle = self.getGeometry()
- params = center[0], center[1], innerRadius, outerRadius, startAngle, endAngle
- params = 'center: %f %f; radius: %f %f; angles: %f %f' % params
- except ValueError:
- params = "invalid"
- return "%s(%s)" % (self.__class__.__name__, params)
diff --git a/silx/gui/plot/items/_pick.py b/silx/gui/plot/items/_pick.py
deleted file mode 100644
index 8c8e781..0000000
--- a/silx/gui/plot/items/_pick.py
+++ /dev/null
@@ -1,72 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2019-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 classes supporting item picking."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "04/06/2019"
-
-import numpy
-
-
-class PickingResult(object):
- """Class to access picking information in a :class:`PlotWidget`"""
-
- def __init__(self, item, indices=None):
- """Init
-
- :param item: The picked item
- :param numpy.ndarray indices: Array-like of indices of picked data.
- Either 1D or 2D with dim0: data dimension and dim1: indices.
- No copy is made.
- """
- self._item = item
-
- if indices is None or len(indices) == 0:
- self._indices = None
- else:
- # Indices is set to None if indices array is empty
- indices = numpy.array(indices, copy=False, dtype=numpy.int64)
- self._indices = None if indices.size == 0 else indices
-
- def getItem(self):
- """Returns the item this results corresponds to."""
- return self._item
-
- def getIndices(self, copy=True):
- """Returns indices of picked data.
-
- If data is 1D, it returns a numpy.ndarray, otherwise
- it returns a tuple with as many numpy.ndarray as there are
- dimensions in the data.
-
- :param bool copy: True (default) to get a copy,
- False to return internal arrays
- :rtype: Union[None,numpy.ndarray,List[numpy.ndarray]]
- """
- if self._indices is None:
- return None
- indices = numpy.array(self._indices, copy=copy)
- return indices if indices.ndim == 1 else tuple(indices)
diff --git a/silx/gui/plot/items/_roi_base.py b/silx/gui/plot/items/_roi_base.py
deleted file mode 100644
index 3eb6cf4..0000000
--- a/silx/gui/plot/items/_roi_base.py
+++ /dev/null
@@ -1,835 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 base components to create ROI item for
-the :class:`~silx.gui.plot.PlotWidget`.
-
-.. inheritance-diagram::
- silx.gui.plot.items.roi
- :parts: 1
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import logging
-import numpy
-import weakref
-
-from ....utils.weakref import WeakList
-from ... import qt
-from .. import items
-from ..items import core
-from ...colors import rgba
-import silx.utils.deprecation
-from ....utils.proxy import docstring
-
-
-logger = logging.getLogger(__name__)
-
-
-class _RegionOfInterestBase(qt.QObject):
- """Base class of 1D and 2D region of interest
-
- :param QObject parent: See QObject
- :param str name: The name of the ROI
- """
-
- sigAboutToBeRemoved = qt.Signal()
- """Signal emitted just before this ROI is removed from its manager."""
-
- sigItemChanged = qt.Signal(object)
- """Signal emitted when item has changed.
-
- It provides a flag describing which property of the item has changed.
- See :class:`ItemChangedType` for flags description.
- """
-
- def __init__(self, parent=None):
- qt.QObject.__init__(self, parent=parent)
- self.__name = ''
-
- def getName(self):
- """Returns the name of the ROI
-
- :return: name of the region of interest
- :rtype: str
- """
- return self.__name
-
- def setName(self, name):
- """Set the name of the ROI
-
- :param str name: name of the region of interest
- """
- name = str(name)
- if self.__name != name:
- self.__name = name
- self._updated(items.ItemChangedType.NAME)
-
- def _updated(self, event=None, checkVisibility=True):
- """Implement Item mix-in update method by updating the plot items
-
- See :class:`~silx.gui.plot.items.Item._updated`
- """
- self.sigItemChanged.emit(event)
-
- def contains(self, position):
- """Returns True if the `position` is in this ROI.
-
- :param tuple[float,float] position: position to check
- :return: True if the value / point is consider to be in the region of
- interest.
- :rtype: bool
- """
- return False # Override in subclass to perform actual test
-
-
-class RoiInteractionMode(object):
- """Description of an interaction mode.
-
- An interaction mode provide a specific kind of interaction for a ROI.
- A ROI can implement many interaction.
- """
-
- def __init__(self, label, description=None):
- self._label = label
- self._description = description
-
- @property
- def label(self):
- return self._label
-
- @property
- def description(self):
- return self._description
-
-
-class InteractionModeMixIn(object):
- """Mix in feature which can be implemented by a ROI object.
-
- This provides user interaction to switch between different
- interaction mode to edit the ROI.
-
- This ROI modes have to be described using `RoiInteractionMode`,
- and taken into account during interation with handles.
- """
-
- sigInteractionModeChanged = qt.Signal(object)
-
- def __init__(self):
- self.__modeId = None
-
- def _initInteractionMode(self, modeId):
- """Set the mode without updating anything.
-
- Must be one of the returned :meth:`availableInteractionModes`.
-
- :param RoiInteractionMode modeId: Mode to use
- """
- self.__modeId = modeId
-
- def availableInteractionModes(self):
- """Returns the list of available interaction modes
-
- Must be implemented when inherited to provide all available modes.
-
- :rtype: List[RoiInteractionMode]
- """
- raise NotImplementedError()
-
- def setInteractionMode(self, modeId):
- """Set the interaction mode.
-
- :param RoiInteractionMode modeId: Mode to use
- """
- self.__modeId = modeId
- self._interactiveModeUpdated(modeId)
- self.sigInteractionModeChanged.emit(modeId)
-
- def _interactiveModeUpdated(self, modeId):
- """Called directly after an update of the mode.
-
- The signal `sigInteractionModeChanged` is triggered after this
- call.
-
- Must be implemented when inherited to take care of the change.
- """
- raise NotImplementedError()
-
- def getInteractionMode(self):
- """Returns the interaction mode.
-
- Must be one of the returned :meth:`availableInteractionModes`.
-
- :rtype: RoiInteractionMode
- """
- return self.__modeId
-
-
-class RegionOfInterest(_RegionOfInterestBase, core.HighlightedMixIn):
- """Object describing a region of interest in a plot.
-
- :param QObject parent:
- The RegionOfInterestManager that created this object
- """
-
- _DEFAULT_LINEWIDTH = 1.
- """Default line width of the curve"""
-
- _DEFAULT_LINESTYLE = '-'
- """Default line style of the curve"""
-
- _DEFAULT_HIGHLIGHT_STYLE = items.CurveStyle(linewidth=2)
- """Default highlight style of the item"""
-
- ICON, NAME, SHORT_NAME = None, None, None
- """Metadata to describe the ROI in labels, tooltips and widgets
-
- Should be set by inherited classes to custom the ROI manager widget.
- """
-
- sigRegionChanged = qt.Signal()
- """Signal emitted everytime the shape or position of the ROI changes"""
-
- sigEditingStarted = qt.Signal()
- """Signal emitted when the user start editing the roi"""
-
- sigEditingFinished = qt.Signal()
- """Signal emitted when the region edition is finished. During edition
- sigEditionChanged will be emitted several times and
- sigRegionEditionFinished only at end"""
-
- def __init__(self, parent=None):
- # Avoid circular dependency
- from ..tools import roi as roi_tools
- assert parent is None or isinstance(parent, roi_tools.RegionOfInterestManager)
- _RegionOfInterestBase.__init__(self, parent)
- core.HighlightedMixIn.__init__(self)
- self._color = rgba('red')
- self._editable = False
- self._selectable = False
- self._focusProxy = None
- self._visible = True
- self._child = WeakList()
-
- def _connectToPlot(self, plot):
- """Called after connection to a plot"""
- for item in self.getItems():
- # This hack is needed to avoid reentrant call from _disconnectFromPlot
- # to the ROI manager. It also speed up the item tests in _itemRemoved
- item._roiGroup = True
- plot.addItem(item)
-
- def _disconnectFromPlot(self, plot):
- """Called before disconnection from a plot"""
- for item in self.getItems():
- # The item could be already be removed by the plot
- if item.getPlot() is not None:
- del item._roiGroup
- plot.removeItem(item)
-
- def _setItemName(self, item):
- """Helper to generate a unique id to a plot item"""
- legend = "__ROI-%d__%d" % (id(self), id(item))
- item.setName(legend)
-
- def setParent(self, parent):
- """Set the parent of the RegionOfInterest
-
- :param Union[None,RegionOfInterestManager] parent: The new parent
- """
- # Avoid circular dependency
- from ..tools import roi as roi_tools
- if (parent is not None and not isinstance(parent, roi_tools.RegionOfInterestManager)):
- raise ValueError('Unsupported parent')
-
- previousParent = self.parent()
- if previousParent is not None:
- previousPlot = previousParent.parent()
- if previousPlot is not None:
- self._disconnectFromPlot(previousPlot)
- super(RegionOfInterest, self).setParent(parent)
- if parent is not None:
- plot = parent.parent()
- if plot is not None:
- self._connectToPlot(plot)
-
- def addItem(self, item):
- """Add an item to the set of this ROI children.
-
- This item will be added and removed to the plot used by the ROI.
-
- If the ROI is already part of a plot, the item will also be added to
- the plot.
-
- It the item do not have a name already, a unique one is generated to
- avoid item collision in the plot.
-
- :param silx.gui.plot.items.Item item: A plot item
- """
- assert item is not None
- self._child.append(item)
- if item.getName() == '':
- self._setItemName(item)
- manager = self.parent()
- if manager is not None:
- plot = manager.parent()
- if plot is not None:
- item._roiGroup = True
- plot.addItem(item)
-
- def removeItem(self, item):
- """Remove an item from this ROI children.
-
- If the item is part of a plot it will be removed too.
-
- :param silx.gui.plot.items.Item item: A plot item
- """
- assert item is not None
- self._child.remove(item)
- plot = item.getPlot()
- if plot is not None:
- del item._roiGroup
- plot.removeItem(item)
-
- def getItems(self):
- """Returns the list of PlotWidget items of this RegionOfInterest.
-
- :rtype: List[~silx.gui.plot.items.Item]
- """
- return tuple(self._child)
-
- @classmethod
- def _getShortName(cls):
- """Return an human readable kind of ROI
-
- :rtype: str
- """
- if hasattr(cls, "SHORT_NAME"):
- name = cls.SHORT_NAME
- if name is None:
- name = cls.__name__
- return name
-
- def getColor(self):
- """Returns the color of this ROI
-
- :rtype: QColor
- """
- return qt.QColor.fromRgbF(*self._color)
-
- def setColor(self, color):
- """Set the color used for this ROI.
-
- :param color: The color to use for ROI shape as
- either a color name, a QColor, a list of uint8 or float in [0, 1].
- """
- color = rgba(color)
- if color != self._color:
- self._color = color
- self._updated(items.ItemChangedType.COLOR)
-
- @silx.utils.deprecation.deprecated(reason='API modification',
- replacement='getName()',
- since_version=0.12)
- def getLabel(self):
- """Returns the label displayed for this ROI.
-
- :rtype: str
- """
- return self.getName()
-
- @silx.utils.deprecation.deprecated(reason='API modification',
- replacement='setName(name)',
- since_version=0.12)
- def setLabel(self, label):
- """Set the label displayed with this ROI.
-
- :param str label: The text label to display
- """
- self.setName(name=label)
-
- def isEditable(self):
- """Returns whether the ROI is editable by the user or not.
-
- :rtype: bool
- """
- return self._editable
-
- def setEditable(self, editable):
- """Set whether the ROI can be changed interactively.
-
- :param bool editable: True to allow edition by the user,
- False to disable.
- """
- editable = bool(editable)
- if self._editable != editable:
- self._editable = editable
- self._updated(items.ItemChangedType.EDITABLE)
-
- def isSelectable(self):
- """Returns whether the ROI is selectable by the user or not.
-
- :rtype: bool
- """
- return self._selectable
-
- def setSelectable(self, selectable):
- """Set whether the ROI can be selected interactively.
-
- :param bool selectable: True to allow selection by the user,
- False to disable.
- """
- selectable = bool(selectable)
- if self._selectable != selectable:
- self._selectable = selectable
- self._updated(items.ItemChangedType.SELECTABLE)
-
- def getFocusProxy(self):
- """Returns the ROI which have to be selected when this ROI is selected,
- else None if no proxy specified.
-
- :rtype: RegionOfInterest
- """
- proxy = self._focusProxy
- if proxy is None:
- return None
- proxy = proxy()
- if proxy is None:
- self._focusProxy = None
- return proxy
-
- def setFocusProxy(self, roi):
- """Set the real ROI which will be selected when this ROI is selected,
- else None to remove the proxy already specified.
-
- :param RegionOfInterest roi: A ROI
- """
- if roi is not None:
- self._focusProxy = weakref.ref(roi)
- else:
- self._focusProxy = None
-
- def isVisible(self):
- """Returns whether the ROI is visible in the plot.
-
- .. note::
- This does not take into account whether or not the plot
- widget itself is visible (unlike :meth:`QWidget.isVisible` which
- checks the visibility of all its parent widgets up to the window)
-
- :rtype: bool
- """
- return self._visible
-
- def setVisible(self, visible):
- """Set whether the plot items associated with this ROI are
- visible in the plot.
-
- :param bool visible: True to show the ROI in the plot, False to
- hide it.
- """
- visible = bool(visible)
- if self._visible != visible:
- self._visible = visible
- self._updated(items.ItemChangedType.VISIBLE)
-
- @classmethod
- def showFirstInteractionShape(cls):
- """Returns True if the shape created by the first interaction and
- managed by the plot have to be visible.
-
- :rtype: bool
- """
- return False
-
- @classmethod
- def getFirstInteractionShape(cls):
- """Returns the shape kind which will be used by the very first
- interaction with the plot.
-
- This interactions are hardcoded inside the plot
-
- :rtype: str
- """
- return cls._plotShape
-
- def setFirstShapePoints(self, points):
- """"Initialize the ROI using the points from the first interaction.
-
- This interaction is constrained by the plot API and only supports few
- shapes.
- """
- raise NotImplementedError()
-
- def creationStarted(self):
- """"Called when the ROI creation interaction was started.
- """
- pass
-
- def creationFinalized(self):
- """"Called when the ROI creation interaction was finalized.
- """
- pass
-
- def _updateItemProperty(self, event, source, destination):
- """Update the item property of a destination from an item source.
-
- :param items.ItemChangedType event: Property type to update
- :param silx.gui.plot.items.Item source: The reference for the data
- :param event Union[Item,List[Item]] destination: The item(s) to update
- """
- if not isinstance(destination, (list, tuple)):
- destination = [destination]
- if event == items.ItemChangedType.NAME:
- value = source.getName()
- for d in destination:
- d.setName(value)
- elif event == items.ItemChangedType.EDITABLE:
- value = source.isEditable()
- for d in destination:
- d.setEditable(value)
- elif event == items.ItemChangedType.SELECTABLE:
- value = source.isSelectable()
- for d in destination:
- d._setSelectable(value)
- elif event == items.ItemChangedType.COLOR:
- value = rgba(source.getColor())
- for d in destination:
- d.setColor(value)
- elif event == items.ItemChangedType.LINE_STYLE:
- value = self.getLineStyle()
- for d in destination:
- d.setLineStyle(value)
- elif event == items.ItemChangedType.LINE_WIDTH:
- value = self.getLineWidth()
- for d in destination:
- d.setLineWidth(value)
- elif event == items.ItemChangedType.SYMBOL:
- value = self.getSymbol()
- for d in destination:
- d.setSymbol(value)
- elif event == items.ItemChangedType.SYMBOL_SIZE:
- value = self.getSymbolSize()
- for d in destination:
- d.setSymbolSize(value)
- elif event == items.ItemChangedType.VISIBLE:
- value = self.isVisible()
- for d in destination:
- d.setVisible(value)
- else:
- assert False
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.HIGHLIGHTED:
- style = self.getCurrentStyle()
- self._updatedStyle(event, style)
- else:
- styleEvents = [items.ItemChangedType.COLOR,
- items.ItemChangedType.LINE_STYLE,
- items.ItemChangedType.LINE_WIDTH,
- items.ItemChangedType.SYMBOL,
- items.ItemChangedType.SYMBOL_SIZE]
- if self.isHighlighted():
- styleEvents.append(items.ItemChangedType.HIGHLIGHTED_STYLE)
-
- if event in styleEvents:
- style = self.getCurrentStyle()
- self._updatedStyle(event, style)
-
- super(RegionOfInterest, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- """Called when the current displayed style of the ROI was changed.
-
- :param event: The event responsible of the change of the style
- :param items.CurveStyle style: The current style
- """
- pass
-
- def getCurrentStyle(self):
- """Returns the current curve style.
-
- Curve style depends on curve highlighting
-
- :rtype: CurveStyle
- """
- baseColor = rgba(self.getColor())
- if isinstance(self, core.LineMixIn):
- baseLinestyle = self.getLineStyle()
- baseLinewidth = self.getLineWidth()
- else:
- baseLinestyle = self._DEFAULT_LINESTYLE
- baseLinewidth = self._DEFAULT_LINEWIDTH
- if isinstance(self, core.SymbolMixIn):
- baseSymbol = self.getSymbol()
- baseSymbolsize = self.getSymbolSize()
- else:
- baseSymbol = 'o'
- baseSymbolsize = 1
-
- if self.isHighlighted():
- style = self.getHighlightedStyle()
- color = style.getColor()
- linestyle = style.getLineStyle()
- linewidth = style.getLineWidth()
- symbol = style.getSymbol()
- symbolsize = style.getSymbolSize()
-
- return items.CurveStyle(
- color=baseColor if color is None else color,
- linestyle=baseLinestyle if linestyle is None else linestyle,
- linewidth=baseLinewidth if linewidth is None else linewidth,
- symbol=baseSymbol if symbol is None else symbol,
- symbolsize=baseSymbolsize if symbolsize is None else symbolsize)
- else:
- return items.CurveStyle(color=baseColor,
- linestyle=baseLinestyle,
- linewidth=baseLinewidth,
- symbol=baseSymbol,
- symbolsize=baseSymbolsize)
-
- def _editingStarted(self):
- assert self._editable is True
- self.sigEditingStarted.emit()
-
- def _editingFinished(self):
- self.sigEditingFinished.emit()
-
-
-class HandleBasedROI(RegionOfInterest):
- """Manage a ROI based on a set of handles"""
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- self._handles = []
- self._posOrigin = None
- self._posPrevious = None
-
- def addUserHandle(self, item=None):
- """
- Add a new free handle to the ROI.
-
- This handle do nothing. It have to be managed by the ROI
- implementing this class.
-
- :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
- add, else None to create a default marker.
- :rtype: silx.gui.plot.items.Marker
- """
- return self.addHandle(item, role="user")
-
- def addLabelHandle(self, item=None):
- """
- Add a new label handle to the ROI.
-
- This handle is not draggable nor selectable.
-
- It is displayed without symbol, but it is always visible anyway
- the ROI is editable, in order to display text.
-
- :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
- add, else None to create a default marker.
- :rtype: silx.gui.plot.items.Marker
- """
- return self.addHandle(item, role="label")
-
- def addTranslateHandle(self, item=None):
- """
- Add a new translate handle to the ROI.
-
- Dragging translate handles affect the position position of the ROI
- but not the shape itself.
-
- :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
- add, else None to create a default marker.
- :rtype: silx.gui.plot.items.Marker
- """
- return self.addHandle(item, role="translate")
-
- def addHandle(self, item=None, role="default"):
- """
- Add a new handle to the ROI.
-
- Dragging handles while affect the position or the shape of the
- ROI.
-
- :param Union[None,silx.gui.plot.items.Marker] item: The new marker to
- add, else None to create a default marker.
- :rtype: silx.gui.plot.items.Marker
- """
- if item is None:
- item = items.Marker()
- color = rgba(self.getColor())
- color = self._computeHandleColor(color)
- item.setColor(color)
- if role == "default":
- item.setSymbol("s")
- elif role == "user":
- pass
- elif role == "translate":
- item.setSymbol("+")
- elif role == "label":
- item.setSymbol("")
-
- if role == "user":
- pass
- elif role == "label":
- item._setSelectable(False)
- item._setDraggable(False)
- item.setVisible(True)
- else:
- self.__updateEditable(item, self.isEditable(), remove=False)
- item._setSelectable(False)
-
- self._handles.append((item, role))
- self.addItem(item)
- return item
-
- def removeHandle(self, handle):
- data = [d for d in self._handles if d[0] is handle][0]
- self._handles.remove(data)
- role = data[1]
- if role not in ["user", "label"]:
- if self.isEditable():
- self.__updateEditable(handle, False)
- self.removeItem(handle)
-
- def getHandles(self):
- """Returns the list of handles of this HandleBasedROI.
-
- :rtype: List[~silx.gui.plot.items.Marker]
- """
- return tuple(data[0] for data in self._handles)
-
- def _updated(self, event=None, checkVisibility=True):
- """Implement Item mix-in update method by updating the plot items
-
- See :class:`~silx.gui.plot.items.Item._updated`
- """
- if event == items.ItemChangedType.NAME:
- self._updateText(self.getName())
- elif event == items.ItemChangedType.VISIBLE:
- for item, role in self._handles:
- visible = self.isVisible()
- editionVisible = visible and self.isEditable()
- if role not in ["user", "label"]:
- item.setVisible(editionVisible)
- else:
- item.setVisible(visible)
- elif event == items.ItemChangedType.EDITABLE:
- for item, role in self._handles:
- editable = self.isEditable()
- if role not in ["user", "label"]:
- self.__updateEditable(item, editable)
- super(HandleBasedROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(HandleBasedROI, self)._updatedStyle(event, style)
-
- # Update color of shape items in the plot
- color = rgba(self.getColor())
- handleColor = self._computeHandleColor(color)
- for item, role in self._handles:
- if role == 'user':
- pass
- elif role == 'label':
- item.setColor(color)
- else:
- item.setColor(handleColor)
-
- def __updateEditable(self, handle, editable, remove=True):
- # NOTE: visibility change emit a position update event
- handle.setVisible(editable and self.isVisible())
- handle._setDraggable(editable)
- if editable:
- handle.sigDragStarted.connect(self._handleEditingStarted)
- handle.sigItemChanged.connect(self._handleEditingUpdated)
- handle.sigDragFinished.connect(self._handleEditingFinished)
- else:
- if remove:
- handle.sigDragStarted.disconnect(self._handleEditingStarted)
- handle.sigItemChanged.disconnect(self._handleEditingUpdated)
- handle.sigDragFinished.disconnect(self._handleEditingFinished)
-
- def _handleEditingStarted(self):
- super(HandleBasedROI, self)._editingStarted()
- handle = self.sender()
- self._posOrigin = numpy.array(handle.getPosition())
- self._posPrevious = numpy.array(self._posOrigin)
- self.handleDragStarted(handle, self._posOrigin)
-
- def _handleEditingUpdated(self):
- if self._posOrigin is None:
- # Avoid to handle events when visibility change
- return
- handle = self.sender()
- current = numpy.array(handle.getPosition())
- self.handleDragUpdated(handle, self._posOrigin, self._posPrevious, current)
- self._posPrevious = current
-
- def _handleEditingFinished(self):
- handle = self.sender()
- current = numpy.array(handle.getPosition())
- self.handleDragFinished(handle, self._posOrigin, current)
- self._posPrevious = None
- self._posOrigin = None
- super(HandleBasedROI, self)._editingFinished()
-
- def isHandleBeingDragged(self):
- """Returns True if one of the handles is currently being dragged.
-
- :rtype: bool
- """
- return self._posOrigin is not None
-
- def handleDragStarted(self, handle, origin):
- """Called when an handler drag started"""
- pass
-
- def handleDragUpdated(self, handle, origin, previous, current):
- """Called when an handle drag position changed"""
- pass
-
- def handleDragFinished(self, handle, origin, current):
- """Called when an handle drag finished"""
- pass
-
- def _computeHandleColor(self, color):
- """Returns the anchor color from the base ROI color
-
- :param Union[numpy.array,Tuple,List]: color
- :rtype: Union[numpy.array,Tuple,List]
- """
- return color[:3] + (0.5,)
-
- def _updateText(self, text):
- """Update the text displayed by this ROI
-
- :param str text: A text
- """
- pass
diff --git a/silx/gui/plot/items/axis.py b/silx/gui/plot/items/axis.py
deleted file mode 100644
index be85e6a..0000000
--- a/silx/gui/plot/items/axis.py
+++ /dev/null
@@ -1,569 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the class for axes of the :class:`PlotWidget`.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "22/11/2018"
-
-import datetime as dt
-import enum
-import logging
-
-import dateutil.tz
-
-from ... import qt
-
-
-_logger = logging.getLogger(__name__)
-
-
-class TickMode(enum.Enum):
- """Determines if ticks are regular number or datetimes."""
- DEFAULT = 0 # Ticks are regular numbers
- TIME_SERIES = 1 # Ticks are datetime objects
-
-
-class Axis(qt.QObject):
- """This class describes and controls a plot axis.
-
- Note: This is an abstract class.
- """
- # States are half-stored on the backend of the plot, and half-stored on this
- # object.
- # TODO It would be good to store all the states of an axis in this object.
- # i.e. vmin and vmax
-
- LINEAR = "linear"
- """Constant defining a linear scale"""
-
- LOGARITHMIC = "log"
- """Constant defining a logarithmic scale"""
-
- _SCALES = set([LINEAR, LOGARITHMIC])
-
- sigInvertedChanged = qt.Signal(bool)
- """Signal emitted when axis orientation has changed"""
-
- sigScaleChanged = qt.Signal(str)
- """Signal emitted when axis scale has changed"""
-
- _sigLogarithmicChanged = qt.Signal(bool)
- """Signal emitted when axis scale has changed to or from logarithmic"""
-
- sigAutoScaleChanged = qt.Signal(bool)
- """Signal emitted when axis autoscale has changed"""
-
- sigLimitsChanged = qt.Signal(float, float)
- """Signal emitted when axis limits have changed"""
-
- def __init__(self, plot):
- """Constructor
-
- :param silx.gui.plot.PlotWidget.PlotWidget plot: Parent plot of this
- axis
- """
- qt.QObject.__init__(self, parent=plot)
- self._scale = self.LINEAR
- self._isAutoScale = True
- # Store default labels provided to setGraph[X|Y]Label
- self._defaultLabel = ''
- # Store currently displayed labels
- # Current label can differ from input one with active curve handling
- self._currentLabel = ''
-
- def _getPlot(self):
- """Returns the PlotWidget this Axis belongs to.
-
- :rtype: PlotWidget
- """
- plot = self.parent()
- if plot is None:
- raise RuntimeError("Axis no longer attached to a PlotWidget")
- return plot
-
- def _getBackend(self):
- """Returns the backend
-
- :rtype: BackendBase
- """
- return self._getPlot()._backend
-
- def getLimits(self):
- """Get the limits of this axis.
-
- :return: Minimum and maximum values of this axis as tuple
- """
- return self._internalGetLimits()
-
- def setLimits(self, vmin, vmax):
- """Set this axis limits.
-
- :param float vmin: minimum axis value
- :param float vmax: maximum axis value
- """
- vmin, vmax = self._checkLimits(vmin, vmax)
- if self.getLimits() == (vmin, vmax):
- return
-
- self._internalSetLimits(vmin, vmax)
- self._getPlot()._setDirtyPlot()
-
- self._emitLimitsChanged()
-
- def _emitLimitsChanged(self):
- """Emit axis sigLimitsChanged and PlotWidget limitsChanged event"""
- vmin, vmax = self.getLimits()
- self.sigLimitsChanged.emit(vmin, vmax)
- self._getPlot()._notifyLimitsChanged(emitSignal=False)
-
- def _checkLimits(self, vmin, vmax):
- """Makes sure axis range is not empty
-
- :param float vmin: Min axis value
- :param float vmax: Max axis value
- :return: (min, max) making sure min < max
- :rtype: 2-tuple of float
- """
- if vmax < vmin:
- _logger.debug('%s axis: max < min, inverting limits.', self._defaultLabel)
- vmin, vmax = vmax, vmin
- elif vmax == vmin:
- _logger.debug('%s axis: max == min, expanding limits.', self._defaultLabel)
- if vmin == 0.:
- vmin, vmax = -0.1, 0.1
- elif vmin < 0:
- vmin, vmax = vmin * 1.1, vmin * 0.9
- else: # xmin > 0
- vmin, vmax = vmin * 0.9, vmin * 1.1
-
- return vmin, vmax
-
- def isInverted(self):
- """Return True if the axis is inverted (top to bottom for the y-axis),
- False otherwise. It is always False for the X axis.
-
- :rtype: bool
- """
- return False
-
- def setInverted(self, isInverted):
- """Set the axis orientation.
-
- This is only available for the Y axis.
-
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- if isInverted == self.isInverted():
- return
- raise NotImplementedError()
-
- def getLabel(self):
- """Return the current displayed label of this axis.
-
- :param str axis: The Y axis for which to get the label (left or right)
- :rtype: str
- """
- return self._currentLabel
-
- def setLabel(self, label):
- """Set the label displayed on the plot for this axis.
-
- The provided label can be temporarily replaced by the label of the
- active curve if any.
-
- :param str label: The axis label
- """
- self._defaultLabel = label
- self._setCurrentLabel(label)
- self._getPlot()._setDirtyPlot()
-
- def _setCurrentLabel(self, label):
- """Define the label currently displayed.
-
- If the label is None or empty the default label is used.
-
- :param str label: Currently displayed label
- """
- if label is None or label == '':
- label = self._defaultLabel
- if label is None:
- label = ''
- self._currentLabel = label
- self._internalSetCurrentLabel(label)
-
- def getScale(self):
- """Return the name of the scale used by this axis.
-
- :rtype: str
- """
- return self._scale
-
- def setScale(self, scale):
- """Set the scale to be used by this axis.
-
- :param str scale: Name of the scale ("log", or "linear")
- """
- assert(scale in self._SCALES)
- if self._scale == scale:
- return
-
- # For the backward compatibility signal
- emitLog = self._scale == self.LOGARITHMIC or scale == self.LOGARITHMIC
-
- self._scale = scale
-
- # TODO hackish way of forcing update of curves and images
- plot = self._getPlot()
- for item in plot.getItems():
- item._updated()
- plot._invalidateDataRange()
-
- if scale == self.LOGARITHMIC:
- self._internalSetLogarithmic(True)
- elif scale == self.LINEAR:
- self._internalSetLogarithmic(False)
- else:
- raise ValueError("Scale %s unsupported" % scale)
-
- plot._forceResetZoom()
-
- self.sigScaleChanged.emit(self._scale)
- if emitLog:
- self._sigLogarithmicChanged.emit(self._scale == self.LOGARITHMIC)
-
- def _isLogarithmic(self):
- """Return True if this axis scale is logarithmic, False if linear.
-
- :rtype: bool
- """
- return self._scale == self.LOGARITHMIC
-
- def _setLogarithmic(self, flag):
- """Set the scale of this axes (either linear or logarithmic).
-
- :param bool flag: True to use a logarithmic scale, False for linear.
- """
- flag = bool(flag)
- self.setScale(self.LOGARITHMIC if flag else self.LINEAR)
-
- def getTimeZone(self):
- """Sets tzinfo that is used if this axis plots date times.
-
- None means the datetimes are interpreted as local time.
-
- :rtype: datetime.tzinfo of None.
- """
- raise NotImplementedError()
-
- def setTimeZone(self, tz):
- """Sets tzinfo that is used if this axis' tickMode is TIME_SERIES
-
- The tz must be a descendant of the datetime.tzinfo class, "UTC" or None.
- Use None to let the datetimes be interpreted as local time.
- Use the string "UTC" to let the date datetimes be in UTC time.
-
- :param tz: datetime.tzinfo, "UTC" or None.
- """
- raise NotImplementedError()
-
- def getTickMode(self):
- """Determines if axis ticks are number or datetimes.
-
- :rtype: TickMode enum.
- """
- raise NotImplementedError()
-
- def setTickMode(self, tickMode):
- """Determines if axis ticks are number or datetimes.
-
- :param TickMode tickMode: tick mode enum.
- """
- raise NotImplementedError()
-
- def isAutoScale(self):
- """Return True if axis is automatically adjusting its limits.
-
- :rtype: bool
- """
- return self._isAutoScale
-
- def setAutoScale(self, flag=True):
- """Set the axis limits adjusting behavior of :meth:`resetZoom`.
-
- :param bool flag: True to resize limits automatically,
- False to disable it.
- """
- self._isAutoScale = bool(flag)
- self.sigAutoScaleChanged.emit(self._isAutoScale)
-
- def _setLimitsConstraints(self, minPos=None, maxPos=None):
- raise NotImplementedError()
-
- def setLimitsConstraints(self, minPos=None, maxPos=None):
- """
- Set a constraint on the position of the axes.
-
- :param float minPos: Minimum allowed axis value.
- :param float maxPos: Maximum allowed axis value.
- :return: True if the constaints was updated
- :rtype: bool
- """
- updated = self._setLimitsConstraints(minPos, maxPos)
- if updated:
- plot = self._getPlot()
- xMin, xMax = plot.getXAxis().getLimits()
- yMin, yMax = plot.getYAxis().getLimits()
- y2Min, y2Max = plot.getYAxis('right').getLimits()
- plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
- return updated
-
- def _setRangeConstraints(self, minRange=None, maxRange=None):
- raise NotImplementedError()
-
- def setRangeConstraints(self, minRange=None, maxRange=None):
- """
- Set a constraint on the position of the axes.
-
- :param float minRange: Minimum allowed left-to-right span across the
- view
- :param float maxRange: Maximum allowed left-to-right span across the
- view
- :return: True if the constaints was updated
- :rtype: bool
- """
- updated = self._setRangeConstraints(minRange, maxRange)
- if updated:
- plot = self._getPlot()
- xMin, xMax = plot.getXAxis().getLimits()
- yMin, yMax = plot.getYAxis().getLimits()
- y2Min, y2Max = plot.getYAxis('right').getLimits()
- plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max)
- return updated
-
-
-class XAxis(Axis):
- """Axis class defining primitives for the X axis"""
-
- # TODO With some changes on the backend, it will be able to remove all this
- # specialised implementations (prefixel by '_internal')
-
- def getTimeZone(self):
- return self._getBackend().getXAxisTimeZone()
-
- def setTimeZone(self, tz):
- if isinstance(tz, str) and tz.upper() == "UTC":
- tz = dateutil.tz.tzutc()
- elif not(tz is None or isinstance(tz, dt.tzinfo)):
- raise TypeError("tz must be a dt.tzinfo object, None or 'UTC'.")
-
- self._getBackend().setXAxisTimeZone(tz)
- self._getPlot()._setDirtyPlot()
-
- def getTickMode(self):
- if self._getBackend().isXAxisTimeSeries():
- return TickMode.TIME_SERIES
- else:
- return TickMode.DEFAULT
-
- def setTickMode(self, tickMode):
- if tickMode == TickMode.DEFAULT:
- self._getBackend().setXAxisTimeSeries(False)
- elif tickMode == TickMode.TIME_SERIES:
- self._getBackend().setXAxisTimeSeries(True)
- else:
- raise ValueError("Unexpected TickMode: {}".format(tickMode))
-
- def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphXLabel(label)
-
- def _internalGetLimits(self):
- return self._getBackend().getGraphXLimits()
-
- def _internalSetLimits(self, xmin, xmax):
- self._getBackend().setGraphXLimits(xmin, xmax)
-
- def _internalSetLogarithmic(self, flag):
- self._getBackend().setXAxisLogarithmic(flag)
-
- def _setLimitsConstraints(self, minPos=None, maxPos=None):
- constrains = self._getPlot()._getViewConstraints()
- updated = constrains.update(xMin=minPos, xMax=maxPos)
- return updated
-
- def _setRangeConstraints(self, minRange=None, maxRange=None):
- constrains = self._getPlot()._getViewConstraints()
- updated = constrains.update(minXRange=minRange, maxXRange=maxRange)
- return updated
-
-
-class YAxis(Axis):
- """Axis class defining primitives for the Y axis"""
-
- # TODO With some changes on the backend, it will be able to remove all this
- # specialised implementations (prefixel by '_internal')
-
- def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphYLabel(label, axis='left')
-
- def _internalGetLimits(self):
- return self._getBackend().getGraphYLimits(axis='left')
-
- def _internalSetLimits(self, ymin, ymax):
- self._getBackend().setGraphYLimits(ymin, ymax, axis='left')
-
- def _internalSetLogarithmic(self, flag):
- self._getBackend().setYAxisLogarithmic(flag)
-
- def setInverted(self, flag=True):
- """Set the axis orientation.
-
- This is only available for the Y axis.
-
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- flag = bool(flag)
- if self.isInverted() == flag:
- return
- self._getBackend().setYAxisInverted(flag)
- self._getPlot()._setDirtyPlot()
- self.sigInvertedChanged.emit(flag)
-
- def isInverted(self):
- """Return True if the axis is inverted (top to bottom for the y-axis),
- False otherwise. It is always False for the X axis.
-
- :rtype: bool
- """
- return self._getBackend().isYAxisInverted()
-
- def _setLimitsConstraints(self, minPos=None, maxPos=None):
- constrains = self._getPlot()._getViewConstraints()
- updated = constrains.update(yMin=minPos, yMax=maxPos)
- return updated
-
- def _setRangeConstraints(self, minRange=None, maxRange=None):
- constrains = self._getPlot()._getViewConstraints()
- updated = constrains.update(minYRange=minRange, maxYRange=maxRange)
- return updated
-
-
-class YRightAxis(Axis):
- """Proxy axis for the secondary Y axes. It manages it own label and limit
- but share the some state like scale and direction with the main axis."""
-
- # TODO With some changes on the backend, it will be able to remove all this
- # specialised implementations (prefixel by '_internal')
-
- def __init__(self, plot, mainAxis):
- """Constructor
-
- :param silx.gui.plot.PlotWidget.PlotWidget plot: Parent plot of this
- axis
- :param Axis mainAxis: Axis which sharing state with this axis
- """
- Axis.__init__(self, plot)
- self.__mainAxis = mainAxis
-
- @property
- def sigInvertedChanged(self):
- """Signal emitted when axis orientation has changed"""
- return self.__mainAxis.sigInvertedChanged
-
- @property
- def sigScaleChanged(self):
- """Signal emitted when axis scale has changed"""
- return self.__mainAxis.sigScaleChanged
-
- @property
- def _sigLogarithmicChanged(self):
- """Signal emitted when axis scale has changed to or from logarithmic"""
- return self.__mainAxis._sigLogarithmicChanged
-
- @property
- def sigAutoScaleChanged(self):
- """Signal emitted when axis autoscale has changed"""
- return self.__mainAxis.sigAutoScaleChanged
-
- def _internalSetCurrentLabel(self, label):
- self._getBackend().setGraphYLabel(label, axis='right')
-
- def _internalGetLimits(self):
- return self._getBackend().getGraphYLimits(axis='right')
-
- def _internalSetLimits(self, ymin, ymax):
- self._getBackend().setGraphYLimits(ymin, ymax, axis='right')
-
- def setInverted(self, flag=True):
- """Set the Y axis orientation.
-
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- return self.__mainAxis.setInverted(flag)
-
- def isInverted(self):
- """Return True if Y axis goes from top to bottom, False otherwise."""
- return self.__mainAxis.isInverted()
-
- def getScale(self):
- """Return the name of the scale used by this axis.
-
- :rtype: str
- """
- return self.__mainAxis.getScale()
-
- def setScale(self, scale):
- """Set the scale to be used by this axis.
-
- :param str scale: Name of the scale ("log", or "linear")
- """
- self.__mainAxis.setScale(scale)
-
- def _isLogarithmic(self):
- """Return True if Y axis scale is logarithmic, False if linear."""
- return self.__mainAxis._isLogarithmic()
-
- def _setLogarithmic(self, flag):
- """Set the Y axes scale (either linear or logarithmic).
-
- :param bool flag: True to use a logarithmic scale, False for linear.
- """
- return self.__mainAxis._setLogarithmic(flag)
-
- def isAutoScale(self):
- """Return True if Y axes are automatically adjusting its limits."""
- return self.__mainAxis.isAutoScale()
-
- def setAutoScale(self, flag=True):
- """Set the Y axis limits adjusting behavior of :meth:`PlotWidget.resetZoom`.
-
- :param bool flag: True to resize limits automatically,
- False to disable it.
- """
- return self.__mainAxis.setAutoScale(flag)
diff --git a/silx/gui/plot/items/complex.py b/silx/gui/plot/items/complex.py
deleted file mode 100644
index abb64ad..0000000
--- a/silx/gui/plot/items/complex.py
+++ /dev/null
@@ -1,386 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`ImageComplexData` of the :class:`Plot`.
-"""
-
-from __future__ import absolute_import
-
-__authors__ = ["Vincent Favre-Nicolin", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "14/06/2018"
-
-
-import logging
-
-import numpy
-
-from ....utils.proxy import docstring
-from ....utils.deprecation import deprecated
-from ...colors import Colormap
-from .core import ColormapMixIn, ComplexMixIn, ItemChangedType
-from .image import ImageBase
-
-
-_logger = logging.getLogger(__name__)
-
-
-# Complex colormap functions
-
-def _phase2rgb(colormap, data):
- """Creates RGBA image with colour-coded phase.
-
- :param Colormap colormap: The colormap to use
- :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 colormap.applyToData(phase)
-
-
-def _complex2rgbalog(phaseColormap, data, amin=0., dlogs=2, smax=None):
- """Returns RGBA colors: colour-coded phases and log10(amplitude) in alpha.
-
- :param Colormap phaseColormap: Colormap to use for the phase
- :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(phaseColormap, 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(phaseColormap, data, gamma=1.0, smax=None):
- """Returns RGBA colors: colour-coded phase and linear amplitude in alpha.
-
- :param Colormap phaseColormap: Colormap to use for the phase
- :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(phaseColormap, data)
- a = numpy.absolute(data)
- if smax is not None:
- a[a > smax] = smax
- a /= a.max()
- rgba[..., 3] = 255 * a**gamma
- return rgba
-
-
-class ImageComplexData(ImageBase, ColormapMixIn, ComplexMixIn):
- """Specific plot item to force colormap when using complex colormap.
-
- This is returning the specific colormap when displaying
- colored phase + amplitude.
- """
-
- _SUPPORTED_COMPLEX_MODES = (
- ComplexMixIn.ComplexMode.ABSOLUTE,
- ComplexMixIn.ComplexMode.PHASE,
- ComplexMixIn.ComplexMode.REAL,
- ComplexMixIn.ComplexMode.IMAGINARY,
- ComplexMixIn.ComplexMode.AMPLITUDE_PHASE,
- ComplexMixIn.ComplexMode.LOG10_AMPLITUDE_PHASE,
- ComplexMixIn.ComplexMode.SQUARE_AMPLITUDE)
- """Overrides supported ComplexMode"""
-
- def __init__(self):
- ImageBase.__init__(self, numpy.zeros((0, 0), dtype=numpy.complex64))
- ColormapMixIn.__init__(self)
- ComplexMixIn.__init__(self)
- self._dataByModesCache = {}
- self._amplitudeRangeInfo = None, 2
-
- # Use default from ColormapMixIn
- colormap = super(ImageComplexData, self).getColormap()
-
- phaseColormap = Colormap(
- name='hsv',
- vmin=-numpy.pi,
- vmax=numpy.pi)
-
- self._colormaps = { # Default colormaps for all modes
- self.ComplexMode.ABSOLUTE: colormap,
- self.ComplexMode.PHASE: phaseColormap,
- self.ComplexMode.REAL: colormap,
- self.ComplexMode.IMAGINARY: colormap,
- self.ComplexMode.AMPLITUDE_PHASE: phaseColormap,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE: phaseColormap,
- self.ComplexMode.SQUARE_AMPLITUDE: colormap,
- }
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- plot = self.getPlot()
- assert plot is not None
- if not self._isPlotLinear(plot):
- # Do not render with non linear scales
- return None
-
- mode = self.getComplexMode()
- if mode in (self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
- # For those modes, compute RGBA image here
- colormap = None
- data = self.getRgbaImageData(copy=False)
- else:
- colormap = self.getColormap()
- if colormap.isAutoscale():
- # Avoid backend to compute autoscale: use item cache
- colormap = colormap.copy()
- colormap.setVRange(*colormap.getColormapRange(self))
-
- data = self.getData(copy=False)
-
- if data.size == 0:
- return None # No data to display
-
- return backend.addImage(data,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=colormap,
- alpha=self.getAlpha())
-
- @docstring(ComplexMixIn)
- def setComplexMode(self, mode):
- changed = super(ImageComplexData, self).setComplexMode(mode)
- if changed:
- self._valueDataChanged()
-
- # Backward compatibility
- self._updated(ItemChangedType.VISUALIZATION_MODE)
-
- # Update ColormapMixIn colormap
- colormap = self._colormaps[self.getComplexMode()]
- if colormap is not super(ImageComplexData, self).getColormap():
- super(ImageComplexData, self).setColormap(colormap)
-
- # Send data updated as value returned by getData has changed
- self._updated(ItemChangedType.DATA)
- return changed
-
- 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)
- self._updated(ItemChangedType.VISUALIZATION_MODE)
-
- 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
-
- def setColormap(self, colormap, mode=None):
- """Set the colormap for this specific mode.
-
- :param ~silx.gui.colors.Colormap colormap: The colormap
- :param Union[ComplexMode,str] mode:
- If specified, set the colormap of this specific mode.
- Default: current mode.
- """
- if mode is None:
- mode = self.getComplexMode()
- else:
- mode = self.ComplexMode.from_value(mode)
-
- self._colormaps[mode] = colormap
- if mode is self.getComplexMode():
- super(ImageComplexData, self).setColormap(colormap)
- else:
- self._updated(ItemChangedType.COLORMAP)
-
- def getColormap(self, mode=None):
- """Get the colormap for the (current) mode.
-
- :param Union[ComplexMode,str] mode:
- If specified, get the colormap of this specific mode.
- Default: current mode.
- :rtype: ~silx.gui.colors.Colormap
- """
- if mode is None:
- mode = self.getComplexMode()
- else:
- mode = self.ComplexMode.from_value(mode)
-
- return self._colormaps[mode]
-
- def setData(self, data, copy=True):
- """"Set the image complex data
-
- :param numpy.ndarray data: 2D array of complex with 2 dimensions (h, w)
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 2
- if not numpy.issubdtype(data.dtype, numpy.complexfloating):
- _logger.warning(
- 'Image is not complex, converting it to complex to plot it.')
- data = numpy.array(data, dtype=numpy.complex64)
-
- # Compute current mode data and set colormap data
- mode = self.getComplexMode()
- dataForMode = self.__convertComplexData(data, self.getComplexMode())
- self._dataByModesCache = {mode: dataForMode}
-
- super().setData(data)
-
- def _updated(self, event=None, checkVisibility=True):
- # Synchronizes colormapped data if changed
- # ItemChangedType.COMPLEX_MODE triggers ItemChangedType.DATA
- # No need to handle it twice.
- if event in (ItemChangedType.DATA, ItemChangedType.MASK):
- # Color-mapped data is NOT the `getValueData` for some modes
- if self.getComplexMode() in (
- self.ComplexMode.AMPLITUDE_PHASE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE):
- data = self.getData(copy=False, mode=self.ComplexMode.PHASE)
- mask = self.getMaskData(copy=False)
- if mask is not None:
- data = numpy.copy(data)
- data[mask != 0] = numpy.nan
- else:
- data = self.getValueData(copy=False)
- self._setColormappedData(data, copy=False)
- super()._updated(event=event, checkVisibility=checkVisibility)
-
- def getComplexData(self, copy=True):
- """Returns the image complex data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray of complex
- """
- return super().getData(copy=copy)
-
- def __convertComplexData(self, data, mode):
- """Convert complex data to given mode.
-
- :param numpy.ndarray data:
- :param Union[ComplexMode,str] mode:
- :rtype: numpy.ndarray of float
- """
- if mode is self.ComplexMode.PHASE:
- return numpy.angle(data)
- elif mode is self.ComplexMode.REAL:
- return numpy.real(data)
- elif mode is self.ComplexMode.IMAGINARY:
- return numpy.imag(data)
- elif mode in (self.ComplexMode.ABSOLUTE,
- self.ComplexMode.LOG10_AMPLITUDE_PHASE,
- self.ComplexMode.AMPLITUDE_PHASE):
- return numpy.absolute(data)
- elif mode is self.ComplexMode.SQUARE_AMPLITUDE:
- return numpy.absolute(data) ** 2
- else:
- _logger.error(
- 'Unsupported conversion mode: %s, fallback to absolute',
- str(mode))
- return numpy.absolute(data)
-
- def getData(self, copy=True, mode=None):
- """Returns the image data corresponding to (current) mode.
-
- The returned data is always floats, to get the complex data, use
- :meth:`getComplexData`.
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :param Union[ComplexMode,str] mode:
- If specified, get data corresponding to the mode.
- Default: Current mode.
- :rtype: numpy.ndarray of float
- """
- if mode is None:
- mode = self.getComplexMode()
- else:
- mode = self.ComplexMode.from_value(mode)
-
- if mode not in self._dataByModesCache:
- self._dataByModesCache[mode] = self.__convertComplexData(
- self.getComplexData(copy=False), mode)
-
- return numpy.array(self._dataByModesCache[mode], copy=copy)
-
- def getRgbaImageData(self, copy=True, mode=None):
- """Get the displayed RGB(A) image for (current) mode
-
- :param bool copy: Ignored for this class
- :param Union[ComplexMode,str] mode:
- If specified, get data corresponding to the mode.
- Default: Current mode.
- :rtype: numpy.ndarray of uint8 of shape (height, width, 4)
- """
- if mode is None:
- mode = self.getComplexMode()
- else:
- mode = self.ComplexMode.from_value(mode)
-
- colormap = self.getColormap(mode=mode)
- if mode is self.ComplexMode.AMPLITUDE_PHASE:
- data = self.getComplexData(copy=False)
- return _complex2rgbalin(colormap, data)
- elif mode is self.ComplexMode.LOG10_AMPLITUDE_PHASE:
- data = self.getComplexData(copy=False)
- max_, delta = self._getAmplitudeRangeInfo()
- return _complex2rgbalog(colormap, data, dlogs=delta, smax=max_)
- else:
- data = self.getData(copy=False, mode=mode)
- return colormap.applyToData(data)
-
- # Backward compatibility
-
- Mode = ComplexMixIn.ComplexMode
-
- @deprecated(replacement='setComplexMode', since_version='0.11.0')
- def setVisualizationMode(self, mode):
- return self.setComplexMode(mode)
-
- @deprecated(replacement='getComplexMode', since_version='0.11.0')
- def getVisualizationMode(self):
- return self.getComplexMode()
diff --git a/silx/gui/plot/items/core.py b/silx/gui/plot/items/core.py
deleted file mode 100644
index 95a65ad..0000000
--- a/silx/gui/plot/items/core.py
+++ /dev/null
@@ -1,1734 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 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 base class for items of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "08/12/2020"
-
-import collections
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-from copy import deepcopy
-import logging
-import enum
-from typing import Optional, Tuple
-import warnings
-import weakref
-
-import numpy
-import six
-
-from ....utils.deprecation import deprecated
-from ....utils.proxy import docstring
-from ....utils.enum import Enum as _Enum
-from ....math.combo import min_max
-from ... import qt
-from ... import colors
-from ...colors import Colormap
-from ._pick import PickingResult
-
-from silx import config
-
-_logger = logging.getLogger(__name__)
-
-
-@enum.unique
-class ItemChangedType(enum.Enum):
- """Type of modification provided by :attr:`Item.sigItemChanged` signal."""
- # Private setters and setInfo are not emitting sigItemChanged signal.
- # Signals to consider:
- # COLORMAP_SET emitted when setColormap is called but not forward colormap object signal
- # CURRENT_COLOR_CHANGED emitted current color changed because highlight changed,
- # highlighted color changed or color changed depending on hightlight state.
-
- VISIBLE = 'visibleChanged'
- """Item's visibility changed flag."""
-
- ZVALUE = 'zValueChanged'
- """Item's Z value changed flag."""
-
- COLORMAP = 'colormapChanged' # Emitted when set + forward events from the colormap object
- """Item's colormap changed flag.
-
- This is emitted both when setting a new colormap and
- when the current colormap object is updated.
- """
-
- SYMBOL = 'symbolChanged'
- """Item's symbol changed flag."""
-
- SYMBOL_SIZE = 'symbolSizeChanged'
- """Item's symbol size changed flag."""
-
- LINE_WIDTH = 'lineWidthChanged'
- """Item's line width changed flag."""
-
- LINE_STYLE = 'lineStyleChanged'
- """Item's line style changed flag."""
-
- COLOR = 'colorChanged'
- """Item's color changed flag."""
-
- LINE_BG_COLOR = 'lineBgColorChanged'
- """Item's line background color changed flag."""
-
- YAXIS = 'yAxisChanged'
- """Item's Y axis binding changed flag."""
-
- FILL = 'fillChanged'
- """Item's fill changed flag."""
-
- ALPHA = 'alphaChanged'
- """Item's transparency alpha changed flag."""
-
- DATA = 'dataChanged'
- """Item's data changed flag"""
-
- MASK = 'maskChanged'
- """Item's mask changed flag"""
-
- HIGHLIGHTED = 'highlightedChanged'
- """Item's highlight state changed flag."""
-
- HIGHLIGHTED_COLOR = 'highlightedColorChanged'
- """Deprecated, use HIGHLIGHTED_STYLE instead."""
-
- HIGHLIGHTED_STYLE = 'highlightedStyleChanged'
- """Item's highlighted style changed flag."""
-
- SCALE = 'scaleChanged'
- """Item's scale changed flag."""
-
- TEXT = 'textChanged'
- """Item's text changed flag."""
-
- POSITION = 'positionChanged'
- """Item's position changed flag.
-
- This is emitted when a marker position changed and
- when an image origin changed.
- """
-
- OVERLAY = 'overlayChanged'
- """Item's overlay state changed flag."""
-
- VISUALIZATION_MODE = 'visualizationModeChanged'
- """Item's visualization mode changed flag."""
-
- COMPLEX_MODE = 'complexModeChanged'
- """Item's complex data visualization mode changed flag."""
-
- NAME = 'nameChanged'
- """Item's name changed flag."""
-
- EDITABLE = 'editableChanged'
- """Item's editable state changed flags."""
-
- SELECTABLE = 'selectableChanged'
- """Item's selectable state changed flags."""
-
-
-class Item(qt.QObject):
- """Description of an item of the plot"""
-
- _DEFAULT_Z_LAYER = 0
- """Default layer for overlay rendering"""
-
- _DEFAULT_SELECTABLE = False
- """Default selectable state of items"""
-
- sigItemChanged = qt.Signal(object)
- """Signal emitted when the item has changed.
-
- It provides a flag describing which property of the item has changed.
- See :class:`ItemChangedType` for flags description.
- """
-
- _sigVisibleBoundsChanged = qt.Signal()
- """Signal emitted when the visible extent of the item in the plot has changed.
-
- This signal is emitted only if visible extent tracking is enabled
- (see :meth:`_setVisibleBoundsTracking`).
- """
-
- def __init__(self):
- qt.QObject.__init__(self)
- self._dirty = True
- self._plotRef = None
- self._visible = True
- self._selectable = self._DEFAULT_SELECTABLE
- self._z = self._DEFAULT_Z_LAYER
- self._info = None
- self._xlabel = None
- self._ylabel = None
- self.__name = ''
-
- self.__visibleBoundsTracking = False
- self.__previousVisibleBounds = None
-
- self._backendRenderer = None
-
- def getPlot(self):
- """Returns the ~silx.gui.plot.PlotWidget this item belongs to.
-
- :rtype: Union[~silx.gui.plot.PlotWidget,None]
- """
- return None if self._plotRef is None else self._plotRef()
-
- def _setPlot(self, plot):
- """Set the plot this item belongs to.
-
- WARNING: This should only be called from the Plot.
-
- :param Union[~silx.gui.plot.PlotWidget,None] plot: The Plot instance.
- """
- if plot is not None and self._plotRef is not None:
- raise RuntimeError('Trying to add a node at two places.')
- self.__disconnectFromPlotWidget()
- self._plotRef = None if plot is None else weakref.ref(plot)
- self.__connectToPlotWidget()
- self._updated()
-
- def getBounds(self): # TODO return a Bounds object rather than a tuple
- """Returns the bounding box of this item in data coordinates
-
- :returns: (xmin, xmax, ymin, ymax) or None
- :rtype: 4-tuple of float or None
- """
- return self._getBounds()
-
- def _getBounds(self):
- """:meth:`getBounds` implementation to override by sub-class"""
- return None
-
- def isVisible(self):
- """True if item is visible, False otherwise
-
- :rtype: bool
- """
- return self._visible
-
- def setVisible(self, visible):
- """Set visibility of item.
-
- :param bool visible: True to display it, False otherwise
- """
- visible = bool(visible)
- if visible != self._visible:
- self._visible = visible
- # When visibility has changed, always mark as dirty
- self._updated(ItemChangedType.VISIBLE,
- checkVisibility=False)
-
- def isOverlay(self):
- """Return true if item is drawn as an overlay.
-
- :rtype: bool
- """
- return False
-
- def getName(self):
- """Returns the name of the item which is used as legend.
-
- :rtype: str
- """
- return self.__name
-
- def setName(self, name):
- """Set the name of the item which is used as legend.
-
- :param str name: New name of the item
- :raises RuntimeError: If item belongs to a PlotWidget.
- """
- name = str(name)
- if self.__name != name:
- if self.getPlot() is not None:
- raise RuntimeError(
- "Cannot change name while item is in a PlotWidget")
-
- self.__name = name
- self._updated(ItemChangedType.NAME)
-
- def getLegend(self): # Replaced by getName for API consistency
- return self.getName()
-
- @deprecated(replacement='setName', since_version='0.13')
- def _setLegend(self, legend):
- legend = str(legend) if legend is not None else ''
- self.setName(legend)
-
- def isSelectable(self):
- """Returns true if item is selectable (bool)"""
- return self._selectable
-
- def _setSelectable(self, selectable): # TODO support update
- """Set whether item is selectable or not.
-
- This is private for now as change is not handled.
-
- :param bool selectable: True to make item selectable
- """
- self._selectable = bool(selectable)
-
- def getZValue(self):
- """Returns the layer on which to draw this item (int)"""
- return self._z
-
- def setZValue(self, z):
- z = int(z) if z is not None else self._DEFAULT_Z_LAYER
- if z != self._z:
- self._z = z
- self._updated(ItemChangedType.ZVALUE)
-
- def getInfo(self, copy=True):
- """Returns the info associated to this item
-
- :param bool copy: True to get a deepcopy, False otherwise.
- """
- return deepcopy(self._info) if copy else self._info
-
- def setInfo(self, info, copy=True):
- if copy:
- info = deepcopy(info)
- self._info = info
-
- def getVisibleBounds(self) -> Optional[Tuple[float, float, float, float]]:
- """Returns visible bounds of the item bounding box in the plot area.
-
- :returns:
- (xmin, xmax, ymin, ymax) in data coordinates of the visible area or
- None if item is not visible in the plot area.
- :rtype: Union[List[float],None]
- """
- plot = self.getPlot()
- bounds = self.getBounds()
- if plot is None or bounds is None or not self.isVisible():
- return None
-
- xmin, xmax = numpy.clip(bounds[:2], *plot.getXAxis().getLimits())
- ymin, ymax = numpy.clip(
- bounds[2:], *plot.getYAxis(self.__getYAxis()).getLimits())
-
- if xmin == xmax or ymin == ymax: # Outside the plot area
- return None
- else:
- return xmin, xmax, ymin, ymax
-
- def _isVisibleBoundsTracking(self) -> bool:
- """Returns True if visible bounds changes are tracked.
-
- When enabled, :attr:`_sigVisibleBoundsChanged` is emitted upon changes.
- :rtype: bool
- """
- return self.__visibleBoundsTracking
-
- def _setVisibleBoundsTracking(self, enable: bool) -> None:
- """Set whether or not to track visible bounds changes.
-
- :param bool enable:
- """
- if enable != self.__visibleBoundsTracking:
- self.__disconnectFromPlotWidget()
- self.__previousVisibleBounds = None
- self.__visibleBoundsTracking = enable
- self.__connectToPlotWidget()
-
- def __getYAxis(self) -> str:
- """Returns current Y axis ('left' or 'right')"""
- return self.getYAxis() if isinstance(self, YAxisMixIn) else 'left'
-
- def __connectToPlotWidget(self) -> None:
- """Connect to PlotWidget signals and install event filter"""
- if not self._isVisibleBoundsTracking():
- return
-
- plot = self.getPlot()
- if plot is not None:
- for axis in (plot.getXAxis(), plot.getYAxis(self.__getYAxis())):
- axis.sigLimitsChanged.connect(self._visibleBoundsChanged)
-
- plot.installEventFilter(self)
-
- self._visibleBoundsChanged()
-
- def __disconnectFromPlotWidget(self) -> None:
- """Disconnect from PlotWidget signals and remove event filter"""
- if not self._isVisibleBoundsTracking():
- return
-
- plot = self.getPlot()
- if plot is not None:
- for axis in (plot.getXAxis(), plot.getYAxis(self.__getYAxis())):
- axis.sigLimitsChanged.disconnect(self._visibleBoundsChanged)
-
- plot.removeEventFilter(self)
-
- def _visibleBoundsChanged(self, *args) -> None:
- """Check if visible extent actually changed and emit signal"""
- if not self._isVisibleBoundsTracking():
- return # No visible extent tracking
-
- plot = self.getPlot()
- if plot is None or not plot.isVisible():
- return # No plot or plot not visible
-
- extent = self.getVisibleBounds()
- if extent != self.__previousVisibleBounds:
- self.__previousVisibleBounds = extent
- self._sigVisibleBoundsChanged.emit()
-
- def eventFilter(self, watched, event):
- """Event filter to handle PlotWidget show events"""
- if watched is self.getPlot() and event.type() == qt.QEvent.Show:
- self._visibleBoundsChanged()
- return super().eventFilter(watched, event)
-
- def _updated(self, event=None, checkVisibility=True):
- """Mark the item as dirty (i.e., needing update).
-
- This also triggers Plot.replot.
-
- :param event: The event to send to :attr:`sigItemChanged` signal.
- :param bool checkVisibility: True to only mark as dirty if visible,
- False to always mark as dirty.
- """
- if not checkVisibility or self.isVisible():
- if not self._dirty:
- self._dirty = True
- # TODO: send event instead of explicit call
- plot = self.getPlot()
- if plot is not None:
- plot._itemRequiresUpdate(self)
- if event is not None:
- self.sigItemChanged.emit(event)
-
- def _update(self, backend):
- """Called by Plot to update the backend for this item.
-
- This is meant to be called asynchronously from _updated.
- This optimizes the number of call to _update.
-
- :param backend: The backend to update
- """
- if self._dirty:
- # Remove previous renderer from backend if any
- self._removeBackendRenderer(backend)
-
- # If not visible, do not add renderer to backend
- if self.isVisible():
- self._backendRenderer = self._addBackendRenderer(backend)
-
- self._dirty = False
-
- def _addBackendRenderer(self, backend):
- """Override in subclass to add specific backend renderer.
-
- :param BackendBase backend: The backend to update
- :return: The renderer handle to store or None if no renderer in backend
- """
- return None
-
- def _removeBackendRenderer(self, backend):
- """Override in subclass to remove specific backend renderer.
-
- :param BackendBase backend: The backend to update
- """
- if self._backendRenderer is not None:
- backend.remove(self._backendRenderer)
- self._backendRenderer = None
-
- def pick(self, x, y):
- """Run picking test on this item
-
- :param float x: The x pixel coord where to pick.
- :param float y: The y pixel coord where to pick.
- :return: None if not picked, else the picked position information
- :rtype: Union[None,PickingResult]
- """
- if not self.isVisible() or self._backendRenderer is None:
- return None
- plot = self.getPlot()
- if plot is None:
- return None
-
- indices = plot._backend.pickItem(x, y, self._backendRenderer)
- if indices is None:
- return None
- else:
- return PickingResult(self, indices)
-
-
-class DataItem(Item):
- """Item with a data extent in the plot"""
-
- def _boundsChanged(self, checkVisibility: bool=True) -> None:
- """Call this method in subclass when data bounds has changed.
-
- :param bool checkVisibility:
- """
- if not checkVisibility or self.isVisible():
- self._visibleBoundsChanged()
-
- # TODO hackish data range implementation
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
-
- @docstring(Item)
- def setVisible(self, visible: bool):
- if visible != self.isVisible():
- self._boundsChanged(checkVisibility=False)
- super().setVisible(visible)
-
-# Mix-in classes ##############################################################
-
-
-class ItemMixInBase(object):
- """Base class for Item mix-in"""
-
- def _updated(self, event=None, checkVisibility=True):
- """This is implemented in :class:`Item`.
-
- Mark the item as dirty (i.e., needing update).
- This also triggers Plot.replot.
-
- :param event: The event to send to :attr:`sigItemChanged` signal.
- :param bool checkVisibility: True to only mark as dirty if visible,
- False to always mark as dirty.
- """
- raise RuntimeError(
- "Issue with Mix-In class inheritance order")
-
-
-class LabelsMixIn(ItemMixInBase):
- """Mix-in class for items with x and y labels
-
- Setters are private, otherwise it needs to check the plot
- current active curve and access the internal current labels.
- """
-
- def __init__(self):
- self._xlabel = None
- self._ylabel = None
-
- def getXLabel(self):
- """Return the X axis label associated to this curve
-
- :rtype: str or None
- """
- return self._xlabel
-
- def _setXLabel(self, label):
- """Set the X axis label associated with this curve
-
- :param str label: The X axis label
- """
- self._xlabel = str(label)
-
- def getYLabel(self):
- """Return the Y axis label associated to this curve
-
- :rtype: str or None
- """
- return self._ylabel
-
- def _setYLabel(self, label):
- """Set the Y axis label associated with this curve
-
- :param str label: The Y axis label
- """
- self._ylabel = str(label)
-
-
-class DraggableMixIn(ItemMixInBase):
- """Mix-in class for draggable items"""
-
- def __init__(self):
- self._draggable = False
-
- def isDraggable(self):
- """Returns true if image is draggable
-
- :rtype: bool
- """
- return self._draggable
-
- def _setDraggable(self, draggable): # TODO support update
- """Set if image is draggable or not.
-
- This is private for not as it does not support update.
-
- :param bool draggable:
- """
- self._draggable = bool(draggable)
-
- def drag(self, from_, to):
- """Perform a drag of the item.
-
- :param List[float] from_: (x, y) previous position in data coordinates
- :param List[float] to: (x, y) current position in data coordinates
- """
- raise NotImplementedError("Must be implemented in subclass")
-
-
-class ColormapMixIn(ItemMixInBase):
- """Mix-in class for items with colormap"""
-
- def __init__(self):
- self._colormap = Colormap()
- self._colormap.sigChanged.connect(self._colormapChanged)
- self.__data = None
- self.__cacheColormapRange = {} # Store {normalization: range}
-
- def getColormap(self):
- """Return the used colormap"""
- return self._colormap
-
- def setColormap(self, colormap):
- """Set the colormap of this item
-
- :param silx.gui.colors.Colormap colormap: colormap description
- """
- if self._colormap is colormap:
- return
- if isinstance(colormap, dict):
- colormap = Colormap._fromDict(colormap)
-
- if self._colormap is not None:
- self._colormap.sigChanged.disconnect(self._colormapChanged)
- self._colormap = colormap
- if self._colormap is not None:
- self._colormap.sigChanged.connect(self._colormapChanged)
- self._colormapChanged()
-
- def _colormapChanged(self):
- """Handle updates of the colormap"""
- self._updated(ItemChangedType.COLORMAP)
-
- def _setColormappedData(self, data, copy=True,
- min_=None, minPositive=None, max_=None):
- """Set the data used to compute the colormapped display.
-
- It also resets the cache of data ranges.
-
- This method MUST be called by inheriting classes when data is updated.
-
- :param Union[None,numpy.ndarray] data:
- :param Union[None,float] min_: Minimum value of the data
- :param Union[None,float] minPositive:
- Minimum of strictly positive values of the data
- :param Union[None,float] max_: Maximum value of the data
- """
- self.__data = None if data is None else numpy.array(data, copy=copy)
- self.__cacheColormapRange = {} # Reset cache
-
- # Fill-up colormap range cache if values are provided
- if max_ is not None and numpy.isfinite(max_):
- if min_ is not None and numpy.isfinite(min_):
- self.__cacheColormapRange[Colormap.LINEAR, Colormap.MINMAX] = min_, max_
- if minPositive is not None and numpy.isfinite(minPositive):
- self.__cacheColormapRange[Colormap.LOGARITHM, Colormap.MINMAX] = minPositive, max_
-
- colormap = self.getColormap()
- if None in (colormap.getVMin(), colormap.getVMax()):
- self._colormapChanged()
-
- def getColormappedData(self, copy=True):
- """Returns the data used to compute the displayed colors
-
- :param bool copy: True to get a copy,
- False to get internal data (do not modify!).
- :rtype: Union[None,numpy.ndarray]
- """
- if self.__data is None:
- return None
- else:
- return numpy.array(self.__data, copy=copy)
-
- def _getColormapAutoscaleRange(self, colormap=None):
- """Returns the autoscale range for current data and colormap.
-
- :param Union[None,~silx.gui.colors.Colormap] colormap:
- The colormap for which to compute the autoscale range.
- If None, the default, the colormap of the item is used
- :return: (vmin, vmax) range (vmin and /or vmax might be `None`)
- """
- if colormap is None:
- colormap = self.getColormap()
-
- data = self.getColormappedData(copy=False)
- if colormap is None or data is None:
- return None, None
-
- normalization = colormap.getNormalization()
- autoscaleMode = colormap.getAutoscaleMode()
- key = normalization, autoscaleMode
- vRange = self.__cacheColormapRange.get(key, None)
- if vRange is None:
- vRange = colormap._computeAutoscaleRange(data)
- self.__cacheColormapRange[key] = vRange
- return vRange
-
-
-class SymbolMixIn(ItemMixInBase):
- """Mix-in class for items with symbol type"""
-
- _DEFAULT_SYMBOL = None
- """Default marker of the item"""
-
- _DEFAULT_SYMBOL_SIZE = config.DEFAULT_PLOT_SYMBOL_SIZE
- """Default marker size of the item"""
-
- _SUPPORTED_SYMBOLS = collections.OrderedDict((
- ('o', 'Circle'),
- ('d', 'Diamond'),
- ('s', 'Square'),
- ('+', 'Plus'),
- ('x', 'Cross'),
- ('.', 'Point'),
- (',', 'Pixel'),
- ('|', 'Vertical line'),
- ('_', 'Horizontal line'),
- ('tickleft', 'Tick left'),
- ('tickright', 'Tick right'),
- ('tickup', 'Tick up'),
- ('tickdown', 'Tick down'),
- ('caretleft', 'Caret left'),
- ('caretright', 'Caret right'),
- ('caretup', 'Caret up'),
- ('caretdown', 'Caret down'),
- (u'\u2665', 'Heart'),
- ('', 'None')))
- """Dict of supported symbols"""
-
- def __init__(self):
- if self._DEFAULT_SYMBOL is None: # Use default from config
- self._symbol = config.DEFAULT_PLOT_SYMBOL
- else:
- self._symbol = self._DEFAULT_SYMBOL
-
- if self._DEFAULT_SYMBOL_SIZE is None: # Use default from config
- self._symbol_size = config.DEFAULT_PLOT_SYMBOL_SIZE
- else:
- self._symbol_size = self._DEFAULT_SYMBOL_SIZE
-
- @classmethod
- def getSupportedSymbols(cls):
- """Returns the list of supported symbol names.
-
- :rtype: tuple of str
- """
- return tuple(cls._SUPPORTED_SYMBOLS.keys())
-
- @classmethod
- def getSupportedSymbolNames(cls):
- """Returns the list of supported symbol human-readable names.
-
- :rtype: tuple of str
- """
- return tuple(cls._SUPPORTED_SYMBOLS.values())
-
- def getSymbolName(self, symbol=None):
- """Returns human-readable name for a symbol.
-
- :param str symbol: The symbol from which to get the name.
- Default: current symbol.
- :rtype: str
- :raise KeyError: if symbol is not in :meth:`getSupportedSymbols`.
- """
- if symbol is None:
- symbol = self.getSymbol()
- return self._SUPPORTED_SYMBOLS[symbol]
-
- def getSymbol(self):
- """Return the point marker type.
-
- Marker type::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
-
- :rtype: str
- """
- return self._symbol
-
- def setSymbol(self, symbol):
- """Set the marker type
-
- See :meth:`getSymbol`.
-
- :param str symbol: Marker type or marker name
- """
- if symbol is None:
- symbol = self._DEFAULT_SYMBOL
-
- elif symbol not in self.getSupportedSymbols():
- for symbolCode, name in self._SUPPORTED_SYMBOLS.items():
- if name.lower() == symbol.lower():
- symbol = symbolCode
- break
- else:
- raise ValueError('Unsupported symbol %s' % str(symbol))
-
- if symbol != self._symbol:
- self._symbol = symbol
- self._updated(ItemChangedType.SYMBOL)
-
- def getSymbolSize(self):
- """Return the point marker size in points.
-
- :rtype: float
- """
- return self._symbol_size
-
- def setSymbolSize(self, size):
- """Set the point marker size in points.
-
- See :meth:`getSymbolSize`.
-
- :param str symbol: Marker type
- """
- if size is None:
- size = self._DEFAULT_SYMBOL_SIZE
- if size != self._symbol_size:
- self._symbol_size = size
- self._updated(ItemChangedType.SYMBOL_SIZE)
-
-
-class LineMixIn(ItemMixInBase):
- """Mix-in class for item with line"""
-
- _DEFAULT_LINEWIDTH = 1.
- """Default line width"""
-
- _DEFAULT_LINESTYLE = '-'
- """Default line style"""
-
- _SUPPORTED_LINESTYLE = '', ' ', '-', '--', '-.', ':', None
- """Supported line styles"""
-
- def __init__(self):
- self._linewidth = self._DEFAULT_LINEWIDTH
- self._linestyle = self._DEFAULT_LINESTYLE
-
- @classmethod
- def getSupportedLineStyles(cls):
- """Returns list of supported line styles.
-
- :rtype: List[str,None]
- """
- return cls._SUPPORTED_LINESTYLE
-
- def getLineWidth(self):
- """Return the curve line width in pixels
-
- :rtype: float
- """
- return self._linewidth
-
- def setLineWidth(self, width):
- """Set the width in pixel of the curve line
-
- See :meth:`getLineWidth`.
-
- :param float width: Width in pixels
- """
- width = float(width)
- if width != self._linewidth:
- self._linewidth = width
- self._updated(ItemChangedType.LINE_WIDTH)
-
- def getLineStyle(self):
- """Return the type of the line
-
- Type of line::
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
-
- :rtype: str
- """
- return self._linestyle
-
- def setLineStyle(self, style):
- """Set the style of the curve line.
-
- See :meth:`getLineStyle`.
-
- :param str style: Line style
- """
- style = str(style)
- assert style in self.getSupportedLineStyles()
- if style is None:
- style = self._DEFAULT_LINESTYLE
- if style != self._linestyle:
- self._linestyle = style
- self._updated(ItemChangedType.LINE_STYLE)
-
-
-class ColorMixIn(ItemMixInBase):
- """Mix-in class for item with color"""
-
- _DEFAULT_COLOR = (0., 0., 0., 1.)
- """Default color of the item"""
-
- def __init__(self):
- self._color = self._DEFAULT_COLOR
-
- def getColor(self):
- """Returns the RGBA color of the item
-
- :rtype: 4-tuple of float in [0, 1] or array of colors
- """
- return self._color
-
- def setColor(self, color, copy=True):
- """Set item color
-
- :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 bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- if isinstance(color, six.string_types):
- color = colors.rgba(color)
- elif isinstance(color, qt.QColor):
- color = colors.rgba(color)
- else:
- color = numpy.array(color, copy=copy)
- # TODO more checks + improve color array support
- if color.ndim == 1: # Single RGBA color
- color = colors.rgba(color)
- else: # Array of colors
- assert color.ndim == 2
-
- self._color = color
- self._updated(ItemChangedType.COLOR)
-
-
-class YAxisMixIn(ItemMixInBase):
- """Mix-in class for item with yaxis"""
-
- _DEFAULT_YAXIS = 'left'
- """Default Y axis the item belongs to"""
-
- def __init__(self):
- self._yaxis = self._DEFAULT_YAXIS
-
- def getYAxis(self):
- """Returns the Y axis this curve belongs to.
-
- Either 'left' or 'right'.
-
- :rtype: str
- """
- return self._yaxis
-
- def setYAxis(self, yaxis):
- """Set the Y axis this curve belongs to.
-
- :param str yaxis: 'left' or 'right'
- """
- yaxis = str(yaxis)
- assert yaxis in ('left', 'right')
- if yaxis != self._yaxis:
- self._yaxis = yaxis
- # Handle data extent changed for DataItem
- if isinstance(self, DataItem):
- self._boundsChanged()
-
- # Handle visible extent changed
- if self._isVisibleBoundsTracking():
- # Switch Y axis signal connection
- plot = self.getPlot()
- if plot is not None:
- previousYAxis = 'left' if self.getXAxis() == 'right' else 'right'
- plot.getYAxis(previousYAxis).sigLimitsChanged.disconnect(
- self._visibleBoundsChanged)
- plot.getYAxis(self.getYAxis()).sigLimitsChanged.connect(
- self._visibleBoundsChanged)
- self._visibleBoundsChanged()
-
- self._updated(ItemChangedType.YAXIS)
-
-
-class FillMixIn(ItemMixInBase):
- """Mix-in class for item with fill"""
-
- def __init__(self):
- self._fill = False
-
- def isFill(self):
- """Returns whether the item is filled or not.
-
- :rtype: bool
- """
- return self._fill
-
- def setFill(self, fill):
- """Set whether to fill the item or not.
-
- :param bool fill:
- """
- fill = bool(fill)
- if fill != self._fill:
- self._fill = fill
- self._updated(ItemChangedType.FILL)
-
-
-class AlphaMixIn(ItemMixInBase):
- """Mix-in class for item with opacity"""
-
- def __init__(self):
- self._alpha = 1.
-
- def getAlpha(self):
- """Returns the opacity of the item
-
- :rtype: float in [0, 1.]
- """
- return self._alpha
-
- def setAlpha(self, alpha):
- """Set the opacity of the item
-
- .. note::
-
- If the colormap already has some transparency, this alpha
- adds additional transparency. The alpha channel of the colormap
- is multiplied by this value.
-
- :param alpha: Opacity of the item, between 0 (full transparency)
- and 1. (full opacity)
- :type alpha: float
- """
- alpha = float(alpha)
- alpha = max(0., min(alpha, 1.)) # Clip alpha to [0., 1.] range
- if alpha != self._alpha:
- self._alpha = alpha
- self._updated(ItemChangedType.ALPHA)
-
-
-class ComplexMixIn(ItemMixInBase):
- """Mix-in class for complex data mode"""
-
- _SUPPORTED_COMPLEX_MODES = None
- """Override to only support a subset of all ComplexMode"""
-
- class ComplexMode(_Enum):
- """Identify available display mode for complex"""
- NONE = 'none'
- ABSOLUTE = 'amplitude'
- PHASE = 'phase'
- REAL = 'real'
- IMAGINARY = 'imaginary'
- AMPLITUDE_PHASE = 'amplitude_phase'
- LOG10_AMPLITUDE_PHASE = 'log10_amplitude_phase'
- SQUARE_AMPLITUDE = 'square_amplitude'
-
- def __init__(self):
- self.__complex_mode = self.ComplexMode.ABSOLUTE
-
- def getComplexMode(self):
- """Returns the current complex visualization mode.
-
- :rtype: ComplexMode
- """
- return self.__complex_mode
-
- def setComplexMode(self, mode):
- """Set the complex visualization mode.
-
- :param ComplexMode mode: The visualization mode in:
- 'real', 'imaginary', 'phase', 'amplitude'
- :return: True if value was set, False if is was already set
- :rtype: bool
- """
- mode = self.ComplexMode.from_value(mode)
- assert mode in self.supportedComplexModes()
-
- if mode != self.__complex_mode:
- self.__complex_mode = mode
- self._updated(ItemChangedType.COMPLEX_MODE)
- return True
- else:
- return False
-
- def _convertComplexData(self, data, mode=None):
- """Convert complex data to the specific mode.
-
- :param Union[ComplexMode,None] mode:
- The kind of value to compute.
- If None (the default), the current complex mode is used.
- :return: The converted dataset
- :rtype: Union[numpy.ndarray[float],None]
- """
- if data is None:
- return None
-
- if mode is None:
- mode = self.getComplexMode()
-
- if mode is self.ComplexMode.REAL:
- return numpy.real(data)
- elif mode is self.ComplexMode.IMAGINARY:
- return numpy.imag(data)
- elif mode is self.ComplexMode.ABSOLUTE:
- return numpy.absolute(data)
- elif mode is self.ComplexMode.PHASE:
- return numpy.angle(data)
- elif mode is self.ComplexMode.SQUARE_AMPLITUDE:
- return numpy.absolute(data) ** 2
- else:
- raise ValueError('Unsupported conversion mode: %s', str(mode))
-
- @classmethod
- def supportedComplexModes(cls):
- """Returns the list of supported complex visualization modes.
-
- See :class:`ComplexMode` and :meth:`setComplexMode`.
-
- :rtype: List[ComplexMode]
- """
- if cls._SUPPORTED_COMPLEX_MODES is None:
- return cls.ComplexMode.members()
- else:
- return cls._SUPPORTED_COMPLEX_MODES
-
-
-class ScatterVisualizationMixIn(ItemMixInBase):
- """Mix-in class for scatter plot visualization modes"""
-
- _SUPPORTED_SCATTER_VISUALIZATION = None
- """Allows to override supported Visualizations"""
-
- @enum.unique
- class Visualization(_Enum):
- """Different modes of scatter plot visualizations"""
-
- POINTS = 'points'
- """Display scatter plot as a point cloud"""
-
- LINES = 'lines'
- """Display scatter plot as a wireframe.
-
- This is based on Delaunay triangulation
- """
-
- SOLID = 'solid'
- """Display scatter plot as a set of filled triangles.
-
- This is based on Delaunay triangulation
- """
-
- REGULAR_GRID = 'regular_grid'
- """Display scatter plot as an image.
-
- It expects the points to be the intersection of a regular grid,
- and the order of points following that of an image.
- First line, then second one, and always in the same direction
- (either all lines from left to right or all from right to left).
- """
-
- IRREGULAR_GRID = 'irregular_grid'
- """Display scatter plot as contiguous quadrilaterals.
-
- It expects the points to be the intersection of an irregular grid,
- and the order of points following that of an image.
- First line, then second one, and always in the same direction
- (either all lines from left to right or all from right to left).
- """
-
- BINNED_STATISTIC = 'binned_statistic'
- """Display scatter plot as 2D binned statistic (i.e., generalized histogram).
- """
-
- @enum.unique
- class VisualizationParameter(_Enum):
- """Different parameter names for scatter plot visualizations"""
-
- GRID_MAJOR_ORDER = 'grid_major_order'
- """The major order of points in the regular grid.
-
- Either 'row' (row-major, fast X) or 'column' (column-major, fast Y).
- """
-
- GRID_BOUNDS = 'grid_bounds'
- """The expected range in data coordinates of the regular grid.
-
- A 2-tuple of 2-tuple: (begin (x, y), end (x, y)).
- This provides the data coordinates of the first point and the expected
- last on.
- As for `GRID_SHAPE`, this can be wider than the current data.
- """
-
- GRID_SHAPE = 'grid_shape'
- """The expected size of the regular grid (height, width).
-
- The given shape can be wider than the number of points,
- in which case the grid is not fully filled.
- """
-
- BINNED_STATISTIC_SHAPE = 'binned_statistic_shape'
- """The number of bins in each dimension (height, width).
- """
-
- BINNED_STATISTIC_FUNCTION = 'binned_statistic_function'
- """The reduction function to apply to each bin (str).
-
- Available reduction functions are: 'mean' (default), 'count', 'sum'.
- """
-
- DATA_BOUNDS_HINT = 'data_bounds_hint'
- """The expected bounds of the data in data coordinates.
-
- A 2-tuple of 2-tuple: ((ymin, ymax), (xmin, xmax)).
- This provides a hint for the data ranges in both dimensions.
- It is eventually enlarged with actually data ranges.
-
- WARNING: dimension 0 i.e., Y first.
- """
-
- _SUPPORTED_VISUALIZATION_PARAMETER_VALUES = {
- VisualizationParameter.GRID_MAJOR_ORDER: ('row', 'column'),
- VisualizationParameter.BINNED_STATISTIC_FUNCTION: ('mean', 'count', 'sum'),
- }
- """Supported visualization parameter values.
-
- Defined for parameters with a set of acceptable values.
- """
-
- def __init__(self):
- self.__visualization = self.Visualization.POINTS
- self.__parameters = dict(# Init parameters to None
- (parameter, None) for parameter in self.VisualizationParameter)
- self.__parameters[self.VisualizationParameter.BINNED_STATISTIC_FUNCTION] = 'mean'
-
- @classmethod
- def supportedVisualizations(cls):
- """Returns the list of supported scatter visualization modes.
-
- See :meth:`setVisualization`
-
- :rtype: List[Visualization]
- """
- if cls._SUPPORTED_SCATTER_VISUALIZATION is None:
- return cls.Visualization.members()
- else:
- return cls._SUPPORTED_SCATTER_VISUALIZATION
-
- @classmethod
- def supportedVisualizationParameterValues(cls, parameter):
- """Returns the list of supported scatter visualization modes.
-
- See :meth:`VisualizationParameters`
-
- :param VisualizationParameter parameter:
- This parameter for which to retrieve the supported values.
- :returns: tuple of supported of values or None if not defined.
- """
- parameter = cls.VisualizationParameter(parameter)
- return cls._SUPPORTED_VISUALIZATION_PARAMETER_VALUES.get(
- parameter, None)
-
- def setVisualization(self, mode):
- """Set the scatter plot visualization mode to use.
-
- See :class:`Visualization` for all possible values,
- and :meth:`supportedVisualizations` for supported ones.
-
- :param Union[str,Visualization] mode:
- The visualization mode to use.
- :return: True if value was set, False if is was already set
- :rtype: bool
- """
- mode = self.Visualization.from_value(mode)
- assert mode in self.supportedVisualizations()
-
- if mode != self.__visualization:
- self.__visualization = mode
-
- self._updated(ItemChangedType.VISUALIZATION_MODE)
- return True
- else:
- return False
-
- def getVisualization(self):
- """Returns the scatter plot visualization mode in use.
-
- :rtype: Visualization
- """
- return self.__visualization
-
- def setVisualizationParameter(self, parameter, value=None):
- """Set the given visualization parameter.
-
- :param Union[str,VisualizationParameter] parameter:
- The name of the parameter to set
- :param value: The value to use for this parameter
- Set to None to automatically set the parameter
- :raises ValueError: If parameter is not supported
- :return: True if parameter was set, False if is was already set
- :rtype: bool
- :raise ValueError: If value is not supported
- """
- parameter = self.VisualizationParameter.from_value(parameter)
-
- if self.__parameters[parameter] != value:
- validValues = self.supportedVisualizationParameterValues(parameter)
- if validValues is not None and value not in validValues:
- raise ValueError("Unsupported parameter value: %s" % str(value))
-
- self.__parameters[parameter] = value
- self._updated(ItemChangedType.VISUALIZATION_MODE)
- return True
- return False
-
- def getVisualizationParameter(self, parameter):
- """Returns the value of the given visualization parameter.
-
- This method returns the parameter as set by
- :meth:`setVisualizationParameter`.
-
- :param parameter: The name of the parameter to retrieve
- :returns: The value previously set or None if automatically set
- :raises ValueError: If parameter is not supported
- """
- if parameter not in self.VisualizationParameter:
- raise ValueError("parameter not supported: %s", parameter)
-
- return self.__parameters[parameter]
-
- def getCurrentVisualizationParameter(self, parameter):
- """Returns the current value of the given visualization parameter.
-
- If the parameter was set by :meth:`setVisualizationParameter` to
- a value that is not None, this value is returned;
- else the current value that is automatically computed is returned.
-
- :param parameter: The name of the parameter to retrieve
- :returns: The current value (either set or automatically computed)
- :raises ValueError: If parameter is not supported
- """
- # Override in subclass to provide automatically computed parameters
- return self.getVisualizationParameter(parameter)
-
-
-class PointsBase(DataItem, SymbolMixIn, AlphaMixIn):
- """Base class for :class:`Curve` and :class:`Scatter`"""
- # note: _logFilterData must be overloaded if you overload
- # getData to change its signature
-
- _DEFAULT_Z_LAYER = 1
- """Default overlay layer for points,
- on top of images."""
-
- def __init__(self):
- DataItem.__init__(self)
- SymbolMixIn.__init__(self)
- AlphaMixIn.__init__(self)
- self._x = ()
- self._y = ()
- self._xerror = None
- self._yerror = None
-
- # Store filtered data for x > 0 and/or y > 0
- self._filteredCache = {}
- self._clippedCache = {}
-
- # Store bounds depending on axes filtering >0:
- # key is (isXPositiveFilter, isYPositiveFilter)
- self._boundsCache = {}
-
- @staticmethod
- def _logFilterError(value, error):
- """Filter/convert error values if they go <= 0.
-
- Replace error leading to negative values by nan
-
- :param numpy.ndarray value: 1D array of values
- :param numpy.ndarray error:
- Array of errors: scalar, N, Nx1 or 2xN or None.
- :return: Filtered error so error bars are never negative
- """
- if error is not None:
- # Convert Nx1 to N
- if error.ndim == 2 and error.shape[1] == 1 and len(value) != 1:
- error = numpy.ravel(error)
-
- # Supports error being scalar, N or 2xN array
- valueMinusError = value - numpy.atleast_2d(error)[0]
- errorClipped = numpy.isnan(valueMinusError)
- mask = numpy.logical_not(errorClipped)
- errorClipped[mask] = valueMinusError[mask] <= 0
-
- if numpy.any(errorClipped): # Need filtering
-
- # expand errorbars to 2xN
- if error.size == 1: # Scalar
- error = numpy.full(
- (2, len(value)), error, dtype=numpy.float64)
-
- elif error.ndim == 1: # N array
- newError = numpy.empty((2, len(value)),
- dtype=numpy.float64)
- newError[0,:] = error
- newError[1,:] = error
- error = newError
-
- elif error.size == 2 * len(value): # 2xN array
- error = numpy.array(
- error, copy=True, dtype=numpy.float64)
-
- else:
- _logger.error("Unhandled error array")
- return error
-
- error[0, errorClipped] = numpy.nan
-
- return error
-
- def _getClippingBoolArray(self, xPositive, yPositive):
- """Compute a boolean array to filter out points with negative
- coordinates on log axes.
-
- :param bool xPositive: True to filter arrays according to X coords.
- :param bool yPositive: True to filter arrays according to Y coords.
- :rtype: boolean numpy.ndarray
- """
- assert xPositive or yPositive
- if (xPositive, yPositive) not in self._clippedCache:
- xclipped, yclipped = False, False
-
- if xPositive:
- x = self.getXData(copy=False)
- with numpy.errstate(invalid='ignore'): # Ignore NaN warnings
- xclipped = x <= 0
-
- if yPositive:
- y = self.getYData(copy=False)
- with numpy.errstate(invalid='ignore'): # Ignore NaN warnings
- yclipped = y <= 0
-
- self._clippedCache[(xPositive, yPositive)] = \
- numpy.logical_or(xclipped, yclipped)
- return self._clippedCache[(xPositive, yPositive)]
-
- def _logFilterData(self, xPositive, yPositive):
- """Filter out values with x or y <= 0 on log axes
-
- :param bool xPositive: True to filter arrays according to X coords.
- :param bool yPositive: True to filter arrays according to Y coords.
- :return: The filter arrays or unchanged object if filtering not needed
- :rtype: (x, y, xerror, yerror)
- """
- x = self.getXData(copy=False)
- y = self.getYData(copy=False)
- xerror = self.getXErrorData(copy=False)
- yerror = self.getYErrorData(copy=False)
-
- if xPositive or yPositive:
- clipped = self._getClippingBoolArray(xPositive, yPositive)
-
- if numpy.any(clipped):
- # copy to keep original array and convert to float
- x = numpy.array(x, copy=True, dtype=numpy.float64)
- x[clipped] = numpy.nan
- y = numpy.array(y, copy=True, dtype=numpy.float64)
- y[clipped] = numpy.nan
-
- if xPositive and xerror is not None:
- xerror = self._logFilterError(x, xerror)
-
- if yPositive and yerror is not None:
- yerror = self._logFilterError(y, yerror)
-
- return x, y, xerror, yerror
-
- def _getBounds(self):
- if self.getXData(copy=False).size == 0: # Empty data
- return None
-
- plot = self.getPlot()
- if plot is not None:
- xPositive = plot.getXAxis()._isLogarithmic()
- yPositive = plot.getYAxis()._isLogarithmic()
- else:
- xPositive = False
- yPositive = False
-
- # TODO bounds do not take error bars into account
- if (xPositive, yPositive) not in self._boundsCache:
- # use the getData class method because instance method can be
- # overloaded to return additional arrays
- data = PointsBase.getData(self, copy=False, displayed=True)
- if len(data) == 5:
- # hack to avoid duplicating caching mechanism in Scatter
- # (happens when cached data is used, caching done using
- # Scatter._logFilterData)
- x, y, _xerror, _yerror = data[0], data[1], data[3], data[4]
- else:
- x, y, _xerror, _yerror = data
-
- xmin, xmax = min_max(x, finite=True)
- ymin, ymax = min_max(y, finite=True)
- self._boundsCache[(xPositive, yPositive)] = tuple([
- (bound if bound is not None else numpy.nan)
- for bound in (xmin, xmax, ymin, ymax)])
- return self._boundsCache[(xPositive, yPositive)]
-
- def _getCachedData(self):
- """Return cached filtered data if applicable,
- i.e. if any axis is in log scale.
- Return None if caching is not applicable."""
- plot = self.getPlot()
- if plot is not None:
- xPositive = plot.getXAxis()._isLogarithmic()
- yPositive = plot.getYAxis()._isLogarithmic()
- if xPositive or yPositive:
- # At least one axis has log scale, filter data
- if (xPositive, yPositive) not in self._filteredCache:
- self._filteredCache[(xPositive, yPositive)] = \
- self._logFilterData(xPositive, yPositive)
- return self._filteredCache[(xPositive, yPositive)]
- return None
-
- def getData(self, copy=True, displayed=False):
- """Returns the x, y values of the curve points and xerror, yerror
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :param bool displayed: True to only get curve points that are displayed
- in the plot. Default: False
- Note: If plot has log scale, negative points
- are not displayed.
- :returns: (x, y, xerror, yerror)
- :rtype: 4-tuple of numpy.ndarray
- """
- if displayed: # filter data according to plot state
- cached_data = self._getCachedData()
- if cached_data is not None:
- return cached_data
-
- return (self.getXData(copy),
- self.getYData(copy),
- self.getXErrorData(copy),
- self.getYErrorData(copy))
-
- def getXData(self, copy=True):
- """Returns the x coordinates of the data points
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self._x, copy=copy)
-
- def getYData(self, copy=True):
- """Returns the y coordinates of the data points
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self._y, copy=copy)
-
- def getXErrorData(self, copy=True):
- """Returns the x error of the points
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray, float or None
- """
- if isinstance(self._xerror, numpy.ndarray):
- return numpy.array(self._xerror, copy=copy)
- else:
- return self._xerror # float or None
-
- def getYErrorData(self, copy=True):
- """Returns the y error of the points
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray, float or None
- """
- if isinstance(self._yerror, numpy.ndarray):
- return numpy.array(self._yerror, copy=copy)
- else:
- return self._yerror # float or None
-
- def setData(self, x, y, xerror=None, yerror=None, copy=True):
- """Set the data of the curve.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- :param numpy.ndarray y: The data corresponding to the y coordinates.
- :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 bool copy: True make a copy of the data (default),
- False to use provided arrays.
- """
- x = numpy.array(x, copy=copy)
- y = numpy.array(y, copy=copy)
- assert len(x) == len(y)
- assert x.ndim == y.ndim == 1
-
- # Convert complex data
- if numpy.iscomplexobj(x):
- _logger.warning(
- 'Converting x data to absolute value to plot it.')
- x = numpy.absolute(x)
- if numpy.iscomplexobj(y):
- _logger.warning(
- 'Converting y data to absolute value to plot it.')
- y = numpy.absolute(y)
-
- if xerror is not None:
- if isinstance(xerror, abc.Iterable):
- xerror = numpy.array(xerror, copy=copy)
- if numpy.iscomplexobj(xerror):
- _logger.warning(
- 'Converting xerror data to absolute value to plot it.')
- xerror = numpy.absolute(xerror)
- else:
- xerror = float(xerror)
- if yerror is not None:
- if isinstance(yerror, abc.Iterable):
- yerror = numpy.array(yerror, copy=copy)
- if numpy.iscomplexobj(yerror):
- _logger.warning(
- 'Converting yerror data to absolute value to plot it.')
- yerror = numpy.absolute(yerror)
- else:
- yerror = float(yerror)
- # TODO checks on xerror, yerror
- self._x, self._y = x, y
- self._xerror, self._yerror = xerror, yerror
-
- self._boundsCache = {} # Reset cached bounds
- self._filteredCache = {} # Reset cached filtered data
- self._clippedCache = {} # Reset cached clipped bool array
-
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
-
-class BaselineMixIn(object):
- """Base class for Baseline mix-in"""
-
- def __init__(self, baseline=None):
- self._baseline = baseline
-
- def _setBaseline(self, baseline):
- """
- Set baseline value
-
- :param baseline: baseline value(s)
- :type: Union[None,float,numpy.ndarray]
- """
- if (isinstance(baseline, abc.Iterable)):
- baseline = numpy.array(baseline)
- self._baseline = baseline
-
- def getBaseline(self, copy=True):
- """
-
- :param bool copy:
- :return: histogram baseline
- :rtype: Union[None,float,numpy.ndarray]
- """
- if isinstance(self._baseline, numpy.ndarray):
- return numpy.array(self._baseline, copy=True)
- else:
- return self._baseline
-
-
-class _Style:
- """Object which store styles"""
-
-
-class HighlightedMixIn(ItemMixInBase):
-
- def __init__(self):
- self._highlightStyle = self._DEFAULT_HIGHLIGHT_STYLE
- self._highlighted = False
-
- def isHighlighted(self):
- """Returns True if curve is highlighted.
-
- :rtype: bool
- """
- return self._highlighted
-
- def setHighlighted(self, highlighted):
- """Set the highlight state of the curve
-
- :param bool highlighted:
- """
- highlighted = bool(highlighted)
- if highlighted != self._highlighted:
- self._highlighted = highlighted
- # TODO inefficient: better to use backend's setCurveColor
- self._updated(ItemChangedType.HIGHLIGHTED)
-
- def getHighlightedStyle(self):
- """Returns the highlighted style in use
-
- :rtype: CurveStyle
- """
- return self._highlightStyle
-
- def setHighlightedStyle(self, style):
- """Set the style to use for highlighting
-
- :param CurveStyle style: New style to use
- """
- previous = self.getHighlightedStyle()
- if style != previous:
- assert isinstance(style, _Style)
- self._highlightStyle = style
- self._updated(ItemChangedType.HIGHLIGHTED_STYLE)
-
- # Backward compatibility event
- if previous.getColor() != style.getColor():
- self._updated(ItemChangedType.HIGHLIGHTED_COLOR)
diff --git a/silx/gui/plot/items/curve.py b/silx/gui/plot/items/curve.py
deleted file mode 100644
index 75e7f01..0000000
--- a/silx/gui/plot/items/curve.py
+++ /dev/null
@@ -1,326 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`Curve` item of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-import logging
-
-import numpy
-import six
-
-from ....utils.deprecation import deprecated
-from ... import colors
-from .core import (PointsBase, LabelsMixIn, ColorMixIn, YAxisMixIn,
- FillMixIn, LineMixIn, SymbolMixIn, ItemChangedType,
- BaselineMixIn, HighlightedMixIn, _Style)
-
-
-_logger = logging.getLogger(__name__)
-
-
-class CurveStyle(_Style):
- """Object storing the style of a curve.
-
- Set a value to None to use the default
-
- :param color: Color
- :param Union[str,None] linestyle: Style of the line
- :param Union[float,None] linewidth: Width of the line
- :param Union[str,None] symbol: Symbol for markers
- :param Union[float,None] symbolsize: Size of the markers
- """
-
- def __init__(self, color=None, linestyle=None, linewidth=None,
- symbol=None, symbolsize=None):
- if color is None:
- self._color = None
- else:
- if isinstance(color, six.string_types):
- color = colors.rgba(color)
- else: # array-like expected
- color = numpy.array(color, copy=False)
- if color.ndim == 1: # Array is 1D, this is a single color
- color = colors.rgba(color)
- self._color = color
-
- if linestyle is not None:
- assert linestyle in LineMixIn.getSupportedLineStyles()
- self._linestyle = linestyle
-
- self._linewidth = None if linewidth is None else float(linewidth)
-
- if symbol is not None:
- assert symbol in SymbolMixIn.getSupportedSymbols()
- self._symbol = symbol
-
- self._symbolsize = None if symbolsize is None else float(symbolsize)
-
- def getColor(self, copy=True):
- """Returns the color or None if not set.
-
- :param bool copy: True to get a copy (default),
- False to get internal representation (do not modify!)
-
- :rtype: Union[List[float],None]
- """
- if isinstance(self._color, numpy.ndarray):
- return numpy.array(self._color, copy=copy)
- else:
- return self._color
-
- def getLineStyle(self):
- """Return the type of the line or None if not set.
-
- Type of line::
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
-
- :rtype: Union[str,None]
- """
- return self._linestyle
-
- def getLineWidth(self):
- """Return the curve line width in pixels or None if not set.
-
- :rtype: Union[float,None]
- """
- return self._linewidth
-
- def getSymbol(self):
- """Return the point marker type.
-
- Marker type::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
-
- :rtype: Union[str,None]
- """
- return self._symbol
-
- def getSymbolSize(self):
- """Return the point marker size in points.
-
- :rtype: Union[float,None]
- """
- return self._symbolsize
-
- def __eq__(self, other):
- if isinstance(other, CurveStyle):
- return (numpy.array_equal(self.getColor(), other.getColor()) and
- self.getLineStyle() == other.getLineStyle() and
- self.getLineWidth() == other.getLineWidth() and
- self.getSymbol() == other.getSymbol() and
- self.getSymbolSize() == other.getSymbolSize())
- else:
- return False
-
-
-class Curve(PointsBase, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn,
- LineMixIn, BaselineMixIn, HighlightedMixIn):
- """Description of a curve"""
-
- _DEFAULT_Z_LAYER = 1
- """Default overlay layer for curves"""
-
- _DEFAULT_SELECTABLE = True
- """Default selectable state for curves"""
-
- _DEFAULT_LINEWIDTH = 1.
- """Default line width of the curve"""
-
- _DEFAULT_LINESTYLE = '-'
- """Default line style of the curve"""
-
- _DEFAULT_HIGHLIGHT_STYLE = CurveStyle(color='black')
- """Default highlight style of the item"""
-
- _DEFAULT_BASELINE = None
-
- def __init__(self):
- PointsBase.__init__(self)
- ColorMixIn.__init__(self)
- YAxisMixIn.__init__(self)
- FillMixIn.__init__(self)
- LabelsMixIn.__init__(self)
- LineMixIn.__init__(self)
- BaselineMixIn.__init__(self)
- HighlightedMixIn.__init__(self)
-
- self._setBaseline(Curve._DEFAULT_BASELINE)
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- # Filter-out values <= 0
- xFiltered, yFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
-
- if len(xFiltered) == 0 or not numpy.any(numpy.isfinite(xFiltered)):
- return None # No data to display, do not add renderer to backend
-
- style = self.getCurrentStyle()
-
- return backend.addCurve(xFiltered, yFiltered,
- color=style.getColor(),
- symbol=style.getSymbol(),
- linestyle=style.getLineStyle(),
- linewidth=style.getLineWidth(),
- yaxis=self.getYAxis(),
- xerror=xerror,
- yerror=yerror,
- fill=self.isFill(),
- alpha=self.getAlpha(),
- symbolsize=style.getSymbolSize(),
- baseline=self.getBaseline(copy=False))
-
- def __getitem__(self, item):
- """Compatibility with PyMca and silx <= 0.4.0"""
- if isinstance(item, slice):
- return [self[index] for index in range(*item.indices(5))]
- elif item == 0:
- return self.getXData(copy=False)
- elif item == 1:
- return self.getYData(copy=False)
- elif item == 2:
- return self.getName()
- elif item == 3:
- info = self.getInfo(copy=False)
- return {} if info is None else info
- elif item == 4:
- params = {
- 'info': self.getInfo(),
- 'color': self.getColor(),
- 'symbol': self.getSymbol(),
- 'linewidth': self.getLineWidth(),
- 'linestyle': self.getLineStyle(),
- 'xlabel': self.getXLabel(),
- 'ylabel': self.getYLabel(),
- 'yaxis': self.getYAxis(),
- 'xerror': self.getXErrorData(copy=False),
- 'yerror': self.getYErrorData(copy=False),
- 'z': self.getZValue(),
- 'selectable': self.isSelectable(),
- 'fill': self.isFill(),
- }
- return params
- else:
- raise IndexError("Index out of range: %s", str(item))
-
- @deprecated(replacement='Curve.getHighlightedStyle().getColor()',
- since_version='0.9.0')
- def getHighlightedColor(self):
- """Returns the RGBA highlight color of the item
-
- :rtype: 4-tuple of float in [0, 1]
- """
- return self.getHighlightedStyle().getColor()
-
- @deprecated(replacement='Curve.setHighlightedStyle()',
- since_version='0.9.0')
- def setHighlightedColor(self, color):
- """Set the color to use when highlighted
-
- :param color: color(s) to be used for highlight
- :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in colors.py
- """
- self.setHighlightedStyle(CurveStyle(color))
-
- def getCurrentStyle(self):
- """Returns the current curve style.
-
- Curve style depends on curve highlighting
-
- :rtype: CurveStyle
- """
- if self.isHighlighted():
- style = self.getHighlightedStyle()
- color = style.getColor()
- linestyle = style.getLineStyle()
- linewidth = style.getLineWidth()
- symbol = style.getSymbol()
- symbolsize = style.getSymbolSize()
-
- return CurveStyle(
- color=self.getColor() if color is None else color,
- linestyle=self.getLineStyle() if linestyle is None else linestyle,
- linewidth=self.getLineWidth() if linewidth is None else linewidth,
- symbol=self.getSymbol() if symbol is None else symbol,
- symbolsize=self.getSymbolSize() if symbolsize is None else symbolsize)
-
- else:
- return CurveStyle(color=self.getColor(),
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- symbol=self.getSymbol(),
- symbolsize=self.getSymbolSize())
-
- @deprecated(replacement='Curve.getCurrentStyle()',
- since_version='0.9.0')
- def getCurrentColor(self):
- """Returns the current color of the curve.
-
- This color is either the color of the curve or the highlighted color,
- depending on the highlight state.
-
- :rtype: 4-tuple of float in [0, 1]
- """
- return self.getCurrentStyle().getColor()
-
- def setData(self, x, y, xerror=None, yerror=None, baseline=None, copy=True):
- """Set the data of the curve.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- :param numpy.ndarray y: The data corresponding to the y coordinates.
- :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 baseline: curve baseline
- :type baseline: Union[None,float,numpy.ndarray]
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- """
- PointsBase.setData(self, x=x, y=y, xerror=xerror, yerror=yerror,
- copy=copy)
- self._setBaseline(baseline=baseline)
diff --git a/silx/gui/plot/items/histogram.py b/silx/gui/plot/items/histogram.py
deleted file mode 100644
index 16bbefa..0000000
--- a/silx/gui/plot/items/histogram.py
+++ /dev/null
@@ -1,389 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 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::t
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`Histogram` item of the :class:`Plot`.
-"""
-
-__authors__ = ["H. Payno", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/08/2018"
-
-import logging
-import typing
-
-import numpy
-from collections import OrderedDict, namedtuple
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-
-from ....utils.proxy import docstring
-from .core import (DataItem, AlphaMixIn, BaselineMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, ItemChangedType, Item)
-from ._pick import PickingResult
-
-_logger = logging.getLogger(__name__)
-
-
-def _computeEdges(x, histogramType):
- """Compute the edges from a set of xs and a rule to generate the edges
-
- :param x: the x value of the curve to transform into an histogram
- :param histogramType: the type of histogram we wan't to generate.
- This define the way to center the histogram values compared to the
- curve value. Possible values can be::
-
- - 'left'
- - 'right'
- - 'center'
-
- :return: the edges for the given x and the histogramType
- """
- # for now we consider that the spaces between xs are constant
- edges = x.copy()
- if histogramType == 'left':
- width = 1
- if len(x) > 1:
- width = x[1] - x[0]
- edges = numpy.append(x[0] - width, edges)
- if histogramType == 'center':
- edges = _computeEdges(edges, 'right')
- widths = (edges[1:] - edges[0:-1]) / 2.0
- widths = numpy.append(widths, widths[-1])
- edges = edges - widths
- if histogramType == 'right':
- width = 1
- if len(x) > 1:
- width = x[-1] - x[-2]
- edges = numpy.append(edges, x[-1] + width)
-
- return edges
-
-
-def _getHistogramCurve(histogram, edges):
- """Returns the x and y value of a curve corresponding to the histogram
-
- :param numpy.ndarray histogram: The values of the histogram
- :param numpy.ndarray edges: The bin edges of the histogram
- :return: a tuple(x, y) which contains the value of the curve to use
- to display the histogram
- """
- assert len(histogram) + 1 == len(edges)
- x = numpy.empty(len(histogram) * 2, dtype=edges.dtype)
- y = numpy.empty(len(histogram) * 2, dtype=histogram.dtype)
- # Make a curve with stairs
- x[:-1:2] = edges[:-1]
- x[1::2] = edges[1:]
- y[:-1:2] = histogram
- y[1::2] = histogram
-
- return x, y
-
-
-# TODO: Yerror, test log scale
-class Histogram(DataItem, AlphaMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, BaselineMixIn):
- """Description of an histogram"""
-
- _DEFAULT_Z_LAYER = 1
- """Default overlay layer for histograms"""
-
- _DEFAULT_SELECTABLE = False
- """Default selectable state for histograms"""
-
- _DEFAULT_LINEWIDTH = 1.
- """Default line width of the histogram"""
-
- _DEFAULT_LINESTYLE = '-'
- """Default line style of the histogram"""
-
- _DEFAULT_BASELINE = None
-
- def __init__(self):
- DataItem.__init__(self)
- AlphaMixIn.__init__(self)
- BaselineMixIn.__init__(self)
- ColorMixIn.__init__(self)
- FillMixIn.__init__(self)
- LineMixIn.__init__(self)
- YAxisMixIn.__init__(self)
-
- self._histogram = ()
- self._edges = ()
- self._setBaseline(Histogram._DEFAULT_BASELINE)
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- values, edges, baseline = self.getData(copy=False)
-
- if values.size == 0:
- return None # No data to display, do not add renderer
-
- if values.size == 0:
- return None # No data to display, do not add renderer to backend
-
- x, y = _getHistogramCurve(values, edges)
-
- # Filter-out values <= 0
- plot = self.getPlot()
- if plot is not None:
- xPositive = plot.getXAxis()._isLogarithmic()
- yPositive = plot.getYAxis()._isLogarithmic()
- else:
- xPositive = False
- yPositive = False
-
- if xPositive or yPositive:
- clipped = numpy.logical_or(
- (x <= 0) if xPositive else False,
- (y <= 0) if yPositive else False)
- # Make a copy and replace negative points by NaN
- x = numpy.array(x, dtype=numpy.float64)
- y = numpy.array(y, dtype=numpy.float64)
- x[clipped] = numpy.nan
- y[clipped] = numpy.nan
-
- return backend.addCurve(x, y,
- color=self.getColor(),
- symbol='',
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- yaxis=self.getYAxis(),
- xerror=None,
- yerror=None,
- fill=self.isFill(),
- alpha=self.getAlpha(),
- baseline=baseline,
- symbolsize=1)
-
- def _getBounds(self):
- values, edges, baseline = self.getData(copy=False)
-
- plot = self.getPlot()
- if plot is not None:
- xPositive = plot.getXAxis()._isLogarithmic()
- yPositive = plot.getYAxis()._isLogarithmic()
- else:
- xPositive = False
- yPositive = False
-
- if xPositive or yPositive:
- values = numpy.array(values, copy=True, dtype=numpy.float64)
-
- if xPositive:
- # Replace edges <= 0 by NaN and corresponding values by NaN
- clipped_edges = (edges <= 0)
- edges = numpy.array(edges, copy=True, dtype=numpy.float64)
- edges[clipped_edges] = numpy.nan
- clipped_values = numpy.logical_or(clipped_edges[:-1],
- clipped_edges[1:])
- else:
- clipped_values = numpy.zeros_like(values, dtype=bool)
-
- if yPositive:
- # Replace values <= 0 by NaN, do not modify edges
- clipped_values = numpy.logical_or(clipped_values, values <= 0)
-
- values[clipped_values] = numpy.nan
-
- if yPositive:
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- numpy.nanmin(values),
- numpy.nanmax(values))
-
- else: # No log scale on y axis, include 0 in bounds
- if numpy.all(numpy.isnan(values)):
- return None
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- min(0, numpy.nanmin(values)),
- max(0, numpy.nanmax(values)))
-
- def __pickFilledHistogram(self, x: float, y: float) -> typing.Optional[PickingResult]:
- """Picking implementation for filled histogram
-
- :param x: X position in pixels
- :param y: Y position in pixels
- """
- if not self.isFill():
- return None
-
- plot = self.getPlot()
- if plot is None:
- return None
-
- xData, yData = plot.pixelToData(x, y, axis=self.getYAxis())
- xmin, xmax, ymin, ymax = self.getBounds()
- if not xmin < xData < xmax or not ymin < yData < ymax:
- return None # Outside bounding box
-
- # Check x
- edges = self.getBinEdgesData(copy=False)
- index = numpy.searchsorted(edges, (xData,), side='left')[0] - 1
- # Safe indexing in histogram values
- index = numpy.clip(index, 0, len(edges) - 2)
-
- # Check y
- baseline = self.getBaseline(copy=False)
- if baseline is None:
- baseline = 0 # Default value
-
- value = self.getValueData(copy=False)[index]
- if ((baseline <= value and baseline <= yData <= value) or
- (value < baseline and value <= yData <= baseline)):
- return PickingResult(self, numpy.array([index]))
- else:
- return None
-
- @docstring(DataItem)
- def pick(self, x, y):
- if self.isFill():
- return self.__pickFilledHistogram(x, y)
- else:
- result = super().pick(x, y)
- if result is None:
- return None
- else: # Convert from curve indices to histogram indices
- return PickingResult(self, numpy.unique(result.getIndices() // 2))
-
- def getValueData(self, copy=True):
- """The values of the histogram
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :returns: The values of the histogram
- :rtype: numpy.ndarray
- """
- return numpy.array(self._histogram, copy=copy)
-
- def getBinEdgesData(self, copy=True):
- """The bin edges of the histogram (number of histogram values + 1)
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :returns: The bin edges of the histogram
- :rtype: numpy.ndarray
- """
- return numpy.array(self._edges, copy=copy)
-
- def getData(self, copy=True):
- """Return the histogram values, bin edges and baseline
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :returns: (N histogram value, N+1 bin edges)
- :rtype: 2-tuple of numpy.nadarray
- """
- return (self.getValueData(copy),
- self.getBinEdgesData(copy),
- self.getBaseline(copy))
-
- def setData(self, histogram, edges, align='center', baseline=None,
- copy=True):
- """Set the histogram values and bin edges.
-
- :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 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 baseline: histogram baseline
- :type baseline: Union[None,float,numpy.ndarray]
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- """
- histogram = numpy.array(histogram, copy=copy)
- edges = numpy.array(edges, copy=copy)
-
- assert histogram.ndim == 1
- assert edges.ndim == 1
- assert edges.size in (histogram.size, histogram.size + 1)
- assert align in ('center', 'left', 'right')
-
- if histogram.size == 0: # No data
- self._histogram = ()
- self._edges = ()
- else:
- if edges.size == histogram.size: # Compute true bin edges
- edges = _computeEdges(edges, align)
-
- # Check that bin edges are monotonic
- edgesDiff = numpy.diff(edges)
- edgesDiff = edgesDiff[numpy.logical_not(numpy.isnan(edgesDiff))]
- assert numpy.all(edgesDiff >= 0) or numpy.all(edgesDiff <= 0)
- # manage baseline
- if (isinstance(baseline, abc.Iterable)):
- baseline = numpy.array(baseline)
- if baseline.size == histogram.size:
- new_baseline = numpy.empty(baseline.shape[0] * 2)
- for i_value, value in enumerate(baseline):
- new_baseline[i_value*2:i_value*2+2] = value
- baseline = new_baseline
- self._histogram = histogram
- self._edges = edges
- self._alignement = align
- self._setBaseline(baseline)
-
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
- def getAlignment(self):
- """
-
- :return: histogram alignement. Value in ('center', 'left', 'right').
- """
- return self._alignement
-
- def _revertComputeEdges(self, x, histogramType):
- """Compute the edges from a set of xs and a rule to generate the edges
-
- :param x: the x value of the curve to transform into an histogram
- :param histogramType: the type of histogram we wan't to generate.
- This define the way to center the histogram values compared to the
- curve value. Possible values can be::
-
- - 'left'
- - 'right'
- - 'center'
-
- :return: the edges for the given x and the histogramType
- """
- # for now we consider that the spaces between xs are constant
- edges = x.copy()
- if histogramType == 'left':
- return edges[1:]
- if histogramType == 'center':
- edges = (edges[1:] + edges[:-1]) / 2.0
- if histogramType == 'right':
- width = 1
- if len(x) > 1:
- width = x[-1] + x[-2]
- edges = edges[:-1]
- return edges
diff --git a/silx/gui/plot/items/image.py b/silx/gui/plot/items/image.py
deleted file mode 100644
index 0d9c9a4..0000000
--- a/silx/gui/plot/items/image.py
+++ /dev/null
@@ -1,617 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`ImageData` and :class:`ImageRgba` items
-of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "08/12/2020"
-
-try:
- from collections import abc
-except ImportError: # Python2 support
- import collections as abc
-import logging
-
-import numpy
-
-from ....utils.proxy import docstring
-from .core import (DataItem, LabelsMixIn, DraggableMixIn, ColormapMixIn,
- AlphaMixIn, ItemChangedType)
-
-_logger = logging.getLogger(__name__)
-
-
-def _convertImageToRgba32(image, copy=True):
- """Convert an RGB or RGBA image to RGBA32.
-
- It converts from floats in [0, 1], bool, integer and uint in [0, 255]
-
- If the input image is already an RGBA32 image,
- the returned image shares the same data.
-
- :param image: Image to convert to
- :type image: numpy.ndarray with 3 dimensions: height, width, color channels
- :param bool copy: True (Default) to get a copy, False, avoid copy if possible
- :return: The image converted to RGBA32 with dimension: (height, width, 4)
- :rtype: numpy.ndarray of uint8
- """
- assert image.ndim == 3
- assert image.shape[-1] in (3, 4)
-
- # Convert type to uint8
- if image.dtype.name != 'uint8':
- if image.dtype.kind == 'f': # Float in [0, 1]
- image = (numpy.clip(image, 0., 1.) * 255).astype(numpy.uint8)
- elif image.dtype.kind == 'b': # boolean
- image = image.astype(numpy.uint8) * 255
- elif image.dtype.kind in ('i', 'u'): # int, uint
- image = numpy.clip(image, 0, 255).astype(numpy.uint8)
- else:
- raise ValueError('Unsupported image dtype: %s', image.dtype.name)
- copy = False # A copy as already been done, avoid next one
-
- # Convert RGB to RGBA
- if image.shape[-1] == 3:
- new_image = numpy.empty((image.shape[0], image.shape[1], 4),
- dtype=numpy.uint8)
- new_image[:,:,:3] = image
- new_image[:,:, 3] = 255
- return new_image # This is a copy anyway
- else:
- return numpy.array(image, copy=copy)
-
-
-class ImageBase(DataItem, LabelsMixIn, DraggableMixIn, AlphaMixIn):
- """Description of an image
-
- :param numpy.ndarray data: Initial image data
- """
-
- def __init__(self, data=None, mask=None):
- DataItem.__init__(self)
- LabelsMixIn.__init__(self)
- DraggableMixIn.__init__(self)
- AlphaMixIn.__init__(self)
- if data is None:
- data = numpy.zeros((0, 0, 4), dtype=numpy.uint8)
- self._data = data
- self._mask = mask
- self.__valueDataCache = None # Store default data
- self._origin = (0., 0.)
- self._scale = (1., 1.)
-
- def __getitem__(self, item):
- """Compatibility with PyMca and silx <= 0.4.0"""
- if isinstance(item, slice):
- return [self[index] for index in range(*item.indices(5))]
- elif item == 0:
- return self.getData(copy=False)
- elif item == 1:
- return self.getName()
- elif item == 2:
- info = self.getInfo(copy=False)
- return {} if info is None else info
- elif item == 3:
- return None
- elif item == 4:
- params = {
- 'info': self.getInfo(),
- 'origin': self.getOrigin(),
- 'scale': self.getScale(),
- 'z': self.getZValue(),
- 'selectable': self.isSelectable(),
- 'draggable': self.isDraggable(),
- 'colormap': None,
- 'xlabel': self.getXLabel(),
- 'ylabel': self.getYLabel(),
- }
- return params
- else:
- raise IndexError("Index out of range: %s" % str(item))
-
- def _isPlotLinear(self, plot):
- """Return True if plot only uses linear scale for both of x and y
- axes."""
- linear = plot.getXAxis().LINEAR
- if plot.getXAxis().getScale() != linear:
- return False
- if plot.getYAxis().getScale() != linear:
- return False
- return True
-
- def _getBounds(self):
- if self.getData(copy=False).size == 0: # Empty data
- return None
-
- height, width = self.getData(copy=False).shape[:2]
- origin = self.getOrigin()
- scale = self.getScale()
- # Taking care of scale might be < 0
- xmin, xmax = origin[0], origin[0] + width * scale[0]
- if xmin > xmax:
- xmin, xmax = xmax, xmin
- # Taking care of scale might be < 0
- ymin, ymax = origin[1], origin[1] + height * scale[1]
- if ymin > ymax:
- ymin, ymax = ymax, ymin
-
- plot = self.getPlot()
- if plot is not None and not self._isPlotLinear(plot):
- return None
- else:
- return xmin, xmax, ymin, ymax
-
- @docstring(DraggableMixIn)
- def drag(self, from_, to):
- origin = self.getOrigin()
- self.setOrigin((origin[0] + to[0] - from_[0],
- origin[1] + to[1] - from_[1]))
-
- def getData(self, copy=True):
- """Returns the image data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self._data, copy=copy)
-
- def setData(self, data):
- """Set the image data
-
- :param numpy.ndarray data:
- """
- previousShape = self._data.shape
- self._data = data
- self._valueDataChanged()
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
- if (self.getMaskData(copy=False) is not None and
- previousShape != self._data.shape):
- # Data shape changed, so mask shape changes.
- # Send event, mask is lazily updated in getMaskData
- self._updated(ItemChangedType.MASK)
-
- def getMaskData(self, copy=True):
- """Returns the mask data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: Union[None,numpy.ndarray]
- """
- if self._mask is None:
- return None
-
- # Update mask if it does not match data shape
- shape = self.getData(copy=False).shape[:2]
- if self._mask.shape != shape:
- # Clip/extend mask to match data
- newMask = numpy.zeros(shape, dtype=self._mask.dtype)
- newMask[:self._mask.shape[0], :self._mask.shape[1]] = self._mask[:shape[0], :shape[1]]
- self._mask = newMask
-
- return numpy.array(self._mask, copy=copy)
-
- def setMaskData(self, mask, copy=True):
- """Set the image data
-
- :param numpy.ndarray data:
- :param bool copy: True (Default) to make a copy,
- False to use as is (do not modify!)
- """
- if mask is not None:
- mask = numpy.array(mask, copy=copy)
-
- shape = self.getData(copy=False).shape[:2]
- if mask.shape != shape:
- _logger.warning("Inconsistent shape between mask and data %s, %s", mask.shape, shape)
- # Clip/extent is done lazily in getMaskData
- elif self._mask is None:
- return # No update
-
- self._mask = mask
- self._valueDataChanged()
- self._updated(ItemChangedType.MASK)
-
- def _valueDataChanged(self):
- """Clear cache of default data array"""
- self.__valueDataCache = None
-
- def _getValueData(self, copy=True):
- """Return data used by :meth:`getValueData`
-
- :param bool copy:
- :rtype: numpy.ndarray
- """
- return self.getData(copy=copy)
-
- def getValueData(self, copy=True):
- """Return data (converted to int or float) with mask applied.
-
- Masked values are set to Not-A-Number.
- It returns a 2D array of values (int or float).
-
- :param bool copy:
- :rtype: numpy.ndarray
- """
- if self.__valueDataCache is None:
- data = self._getValueData(copy=False)
- mask = self.getMaskData(copy=False)
- if mask is not None:
- if numpy.issubdtype(data.dtype, numpy.floating):
- dtype = data.dtype
- else:
- dtype = numpy.float64
- data = numpy.array(data, dtype=dtype, copy=True)
- data[mask != 0] = numpy.NaN
- self.__valueDataCache = data
- return numpy.array(self.__valueDataCache, copy=copy)
-
- def getRgbaImageData(self, copy=True):
- """Get the displayed RGB(A) image
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :returns: numpy.ndarray of uint8 of shape (height, width, 4)
- """
- raise NotImplementedError('This MUST be implemented in sub-class')
-
- def getOrigin(self):
- """Returns the offset from origin at which to display the image.
-
- :rtype: 2-tuple of float
- """
- return self._origin
-
- 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
- """
- if isinstance(origin, abc.Sequence):
- origin = float(origin[0]), float(origin[1])
- else: # single value origin
- origin = float(origin), float(origin)
- if origin != self._origin:
- self._origin = origin
- self._boundsChanged()
- self._updated(ItemChangedType.POSITION)
-
- def getScale(self):
- """Returns the scale of the image in data coordinates.
-
- :rtype: 2-tuple of float
- """
- return self._scale
-
- 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
- """
- if isinstance(scale, abc.Sequence):
- scale = float(scale[0]), float(scale[1])
- else: # single value scale
- scale = float(scale), float(scale)
-
- if scale != self._scale:
- self._scale = scale
- self._boundsChanged()
- self._updated(ItemChangedType.SCALE)
-
-
-class ImageData(ImageBase, ColormapMixIn):
- """Description of a data image with a colormap"""
-
- def __init__(self):
- ImageBase.__init__(self, numpy.zeros((0, 0), dtype=numpy.float32))
- ColormapMixIn.__init__(self)
- self._alternativeImage = None
- self.__alpha = None
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- plot = self.getPlot()
- assert plot is not None
- if not self._isPlotLinear(plot):
- # Do not render with non linear scales
- return None
-
- if (self.getAlternativeImageData(copy=False) is not None or
- self.getAlphaData(copy=False) is not None):
- dataToUse = self.getRgbaImageData(copy=False)
- else:
- dataToUse = self.getData(copy=False)
-
- if dataToUse.size == 0:
- return None # No data to display
-
- colormap = self.getColormap()
- if colormap.isAutoscale():
- # Avoid backend to compute autoscale: use item cache
- colormap = colormap.copy()
- colormap.setVRange(*colormap.getColormapRange(self))
-
- return backend.addImage(dataToUse,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=colormap,
- alpha=self.getAlpha())
-
- def __getitem__(self, item):
- """Compatibility with PyMca and silx <= 0.4.0"""
- if item == 3:
- return self.getAlternativeImageData(copy=False)
-
- params = ImageBase.__getitem__(self, item)
- if item == 4:
- params['colormap'] = self.getColormap()
-
- return params
-
- def getRgbaImageData(self, copy=True):
- """Get the displayed RGB(A) image
-
- :returns: Array of uint8 of shape (height, width, 4)
- :rtype: numpy.ndarray
- """
- alternative = self.getAlternativeImageData(copy=False)
- if alternative is not None:
- return _convertImageToRgba32(alternative, copy=copy)
- else:
- # Apply colormap, in this case an new array is always returned
- colormap = self.getColormap()
- image = colormap.applyToData(self)
- alphaImage = self.getAlphaData(copy=False)
- if alphaImage is not None:
- # Apply transparency
- image[:,:, 3] = image[:,:, 3] * alphaImage
- return image
-
- def getAlternativeImageData(self, copy=True):
- """Get the optional RGBA image that is displayed instead of the data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: Union[None,numpy.ndarray]
- """
- if self._alternativeImage is None:
- return None
- else:
- return numpy.array(self._alternativeImage, copy=copy)
-
- def getAlphaData(self, copy=True):
- """Get the optional transparency image applied on the data
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: Union[None,numpy.ndarray]
- """
- if self.__alpha is None:
- return None
- else:
- return numpy.array(self.__alpha, copy=copy)
-
- def setData(self, data, alternative=None, alpha=None, copy=True):
- """"Set the image data and optionally an alternative RGB(A) representation
-
- :param numpy.ndarray data: Data array with 2 dimensions (h, w)
- :param alternative: RGB(A) image to display instead of data,
- shape: (h, w, 3 or 4)
- :type alternative: Union[None,numpy.ndarray]
- :param alpha: An array of transparency value in [0, 1] to use for
- display with shape: (h, w)
- :type alpha: Union[None,numpy.ndarray]
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 2
- if data.dtype.kind == 'b':
- _logger.warning(
- 'Converting boolean image to int8 to plot it.')
- data = numpy.array(data, copy=False, dtype=numpy.int8)
- elif numpy.iscomplexobj(data):
- _logger.warning(
- 'Converting complex image to absolute value to plot it.')
- data = numpy.absolute(data)
-
- if alternative is not None:
- alternative = numpy.array(alternative, copy=copy)
- assert alternative.ndim == 3
- assert alternative.shape[2] in (3, 4)
- assert alternative.shape[:2] == data.shape[:2]
- self._alternativeImage = alternative
-
- if alpha is not None:
- alpha = numpy.array(alpha, copy=copy)
- assert alpha.shape == data.shape
- if alpha.dtype.kind != 'f':
- alpha = alpha.astype(numpy.float32)
- if numpy.any(numpy.logical_or(alpha < 0., alpha > 1.)):
- alpha = numpy.clip(alpha, 0., 1.)
- self.__alpha = alpha
-
- super().setData(data)
-
- def _updated(self, event=None, checkVisibility=True):
- # Synchronizes colormapped data if changed
- if event in (ItemChangedType.DATA, ItemChangedType.MASK):
- self._setColormappedData(
- self.getValueData(copy=False),
- copy=False)
- super()._updated(event=event, checkVisibility=checkVisibility)
-
-
-class ImageRgba(ImageBase):
- """Description of an RGB(A) image"""
-
- def __init__(self):
- ImageBase.__init__(self, numpy.zeros((0, 0, 4), dtype=numpy.uint8))
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- plot = self.getPlot()
- assert plot is not None
- if not self._isPlotLinear(plot):
- # Do not render with non linear scales
- return None
-
- data = self.getData(copy=False)
-
- if data.size == 0:
- return None # No data to display
-
- return backend.addImage(data,
- origin=self.getOrigin(),
- scale=self.getScale(),
- colormap=None,
- alpha=self.getAlpha())
-
- def getRgbaImageData(self, copy=True):
- """Get the displayed RGB(A) image
-
- :returns: numpy.ndarray of uint8 of shape (height, width, 4)
- """
- return _convertImageToRgba32(self.getData(copy=False), copy=copy)
-
- def setData(self, data, copy=True):
- """Set the image data
-
- :param data: RGB(A) image data to set
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- data = numpy.array(data, copy=copy)
- assert data.ndim == 3
- assert data.shape[-1] in (3, 4)
- super().setData(data)
-
- def _getValueData(self, copy=True):
- """Compute the intensity of the RGBA image as default data.
-
- Conversion: https://en.wikipedia.org/wiki/YCbCr#ITU-R_BT.601_conversion
-
- :param bool copy:
- """
- rgba = self.getRgbaImageData(copy=False).astype(numpy.float32)
- intensity = (rgba[:, :, 0] * 0.299 +
- rgba[:, :, 1] * 0.587 +
- rgba[:, :, 2] * 0.114)
- intensity *= rgba[:, :, 3] / 255.
- return intensity
-
-
-class MaskImageData(ImageData):
- """Description of an image used as a mask.
-
- This class is used to flag mask items. This information is used to improve
- internal silx widgets.
- """
- pass
-
-
-class ImageStack(ImageData):
- """Item to store a stack of images and to show it in the plot as one
- of the images of the stack.
-
- The stack is a 3D array ordered this way: `frame id, y, x`.
- So the first image of the stack can be reached this way: `stack[0, :, :]`
- """
-
- def __init__(self):
- ImageData.__init__(self)
- self.__stack = None
- """A 3D numpy array (or a mimic one, see ListOfImages)"""
- self.__stackPosition = None
- """Displayed position in the cube"""
-
- def setStackData(self, stack, position=None, copy=True):
- """Set the stack data
-
- :param stack: A 3D numpy array like
- :param int position: The position of the displayed image in the stack
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- if self.__stack is stack:
- return
- if copy:
- stack = numpy.array(stack)
- assert stack.ndim == 3
- self.__stack = stack
- if position is not None:
- self.__stackPosition = position
- if self.__stackPosition is None:
- self.__stackPosition = 0
- self.__updateDisplayedData()
-
- def getStackData(self, copy=True):
- """Get the stored stack array.
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: A 3D numpy array, or numpy array like
- """
- if copy:
- return numpy.array(self.__stack)
- else:
- return self.__stack
-
- def setStackPosition(self, pos):
- """Set the displayed position on the stack.
-
- This function will clamp the stack position according to
- the real size of the first axis of the stack.
-
- :param int pos: A position on the first axis of the stack.
- """
- if self.__stackPosition == pos:
- return
- self.__stackPosition = pos
- self.__updateDisplayedData()
-
- def getStackPosition(self):
- """Get the displayed position of the stack.
-
- :rtype: int
- """
- return self.__stackPosition
-
- def __updateDisplayedData(self):
- """Update the displayed frame whenever the stack or the stack
- position are updated."""
- if self.__stack is None or self.__stackPosition is None:
- empty = numpy.array([]).reshape(0, 0)
- self.setData(empty, copy=False)
- return
- size = len(self.__stack)
- self.__stackPosition = numpy.clip(self.__stackPosition, 0, size)
- self.setData(self.__stack[self.__stackPosition], copy=False)
diff --git a/silx/gui/plot/items/marker.py b/silx/gui/plot/items/marker.py
deleted file mode 100755
index 50d070c..0000000
--- a/silx/gui/plot/items/marker.py
+++ /dev/null
@@ -1,281 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 markers item of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "06/03/2017"
-
-
-import logging
-
-from ....utils.proxy import docstring
-from .core import (Item, DraggableMixIn, ColorMixIn, LineMixIn, SymbolMixIn,
- ItemChangedType, YAxisMixIn)
-from silx.gui import qt
-
-_logger = logging.getLogger(__name__)
-
-
-class MarkerBase(Item, DraggableMixIn, ColorMixIn, YAxisMixIn):
- """Base class for markers"""
-
- sigDragStarted = qt.Signal()
- """Signal emitted when the marker is pressed"""
- sigDragFinished = qt.Signal()
- """Signal emitted when the marker is released"""
-
- _DEFAULT_COLOR = (0., 0., 0., 1.)
- """Default color of the markers"""
-
- def __init__(self):
- Item.__init__(self)
- DraggableMixIn.__init__(self)
- ColorMixIn.__init__(self)
- YAxisMixIn.__init__(self)
-
- self._text = ''
- self._x = None
- self._y = None
- self._constraint = self._defaultConstraint
- self.__isBeingDragged = False
-
- def _addRendererCall(self, backend,
- symbol=None, linestyle='-', linewidth=1):
- """Perform the update of the backend renderer"""
- return backend.addMarker(
- x=self.getXPosition(),
- y=self.getYPosition(),
- text=self.getText(),
- color=self.getColor(),
- symbol=symbol,
- linestyle=linestyle,
- linewidth=linewidth,
- constraint=self.getConstraint(),
- yaxis=self.getYAxis())
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- raise NotImplementedError()
-
- @docstring(DraggableMixIn)
- def drag(self, from_, to):
- self.setPosition(to[0], to[1])
-
- def isOverlay(self):
- """Returns True: A marker is always rendered as an overlay.
-
- :rtype: bool
- """
- return True
-
- def getText(self):
- """Returns marker text.
-
- :rtype: str
- """
- return self._text
-
- def setText(self, text):
- """Set the text of the marker.
-
- :param str text: The text to use
- """
- text = str(text)
- if text != self._text:
- self._text = text
- self._updated(ItemChangedType.TEXT)
-
- def getXPosition(self):
- """Returns the X position of the marker line in data coordinates
-
- :rtype: float or None
- """
- return self._x
-
- def getYPosition(self):
- """Returns the Y position of the marker line in data coordinates
-
- :rtype: float or None
- """
- return self._y
-
- def getPosition(self):
- """Returns the (x, y) position of the marker in data coordinates
-
- :rtype: 2-tuple of float or None
- """
- return self._x, self._y
-
- def setPosition(self, x, y):
- """Set marker position in data coordinates
-
- Constraint are applied if any.
-
- :param float x: X coordinates in data frame
- :param float y: Y coordinates in data frame
- """
- x, y = self.getConstraint()(x, y)
- x, y = float(x), float(y)
- if x != self._x or y != self._y:
- self._x, self._y = x, y
- self._updated(ItemChangedType.POSITION)
-
- def getConstraint(self):
- """Returns the dragging constraint of this item"""
- return self._constraint
-
- def _setConstraint(self, constraint): # TODO support update
- """Set the constraint.
-
- This is private for now as update is not handled.
-
- :param callable constraint:
- :param constraint: A function filtering item displacement by
- dragging operations or None for no filter.
- This function is called each time the item is
- moved.
- This is only used if isDraggable returns 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.
- """
- if constraint is None:
- constraint = self._defaultConstraint
- assert callable(constraint)
- self._constraint = constraint
-
- @staticmethod
- def _defaultConstraint(*args):
- """Default constraint not doing anything"""
- return args
-
- def _startDrag(self):
- self.__isBeingDragged = True
- self.sigDragStarted.emit()
-
- def _endDrag(self):
- self.__isBeingDragged = False
- self.sigDragFinished.emit()
-
- def isBeingDragged(self) -> bool:
- """Returns whether the marker is currently dragged by the user."""
- return self.__isBeingDragged
-
-
-class Marker(MarkerBase, SymbolMixIn):
- """Description of a marker"""
-
- _DEFAULT_SYMBOL = '+'
- """Default symbol of the marker"""
-
- def __init__(self):
- MarkerBase.__init__(self)
- SymbolMixIn.__init__(self)
-
- self._x = 0.
- self._y = 0.
-
- def _addBackendRenderer(self, backend):
- return self._addRendererCall(backend, symbol=self.getSymbol())
-
- def _setConstraint(self, constraint):
- """Set the constraint function of the marker drag.
-
- It also supports 'horizontal' and 'vertical' str as constraint.
-
- :param constraint: The constraint of the dragging of this marker
- :type: constraint: callable or str
- """
- if constraint == 'horizontal':
- constraint = self._horizontalConstraint
- elif constraint == 'vertical':
- constraint = self._verticalConstraint
-
- super(Marker, self)._setConstraint(constraint)
-
- def _horizontalConstraint(self, _, y):
- return self.getXPosition(), y
-
- def _verticalConstraint(self, x, _):
- return x, self.getYPosition()
-
-
-class _LineMarker(MarkerBase, LineMixIn):
- """Base class for line markers"""
-
- def __init__(self):
- MarkerBase.__init__(self)
- LineMixIn.__init__(self)
-
- def _addBackendRenderer(self, backend):
- return self._addRendererCall(backend,
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth())
-
-
-class XMarker(_LineMarker):
- """Description of a marker"""
-
- def __init__(self):
- _LineMarker.__init__(self)
- self._x = 0.
-
- def setPosition(self, x, y):
- """Set marker line position in data coordinates
-
- Constraint are applied if any.
-
- :param float x: X coordinates in data frame
- :param float y: Y coordinates in data frame
- """
- x, _ = self.getConstraint()(x, y)
- x = float(x)
- if x != self._x:
- self._x = x
- self._updated(ItemChangedType.POSITION)
-
-
-class YMarker(_LineMarker):
- """Description of a marker"""
-
- def __init__(self):
- _LineMarker.__init__(self)
- self._y = 0.
-
- def setPosition(self, x, y):
- """Set marker line position in data coordinates
-
- Constraint are applied if any.
-
- :param float x: X coordinates in data frame
- :param float y: Y coordinates in data frame
- """
- _, y = self.getConstraint()(x, y)
- y = float(y)
- if y != self._y:
- self._y = y
- self._updated(ItemChangedType.POSITION)
diff --git a/silx/gui/plot/items/roi.py b/silx/gui/plot/items/roi.py
deleted file mode 100644
index 38a1424..0000000
--- a/silx/gui/plot/items/roi.py
+++ /dev/null
@@ -1,1519 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 ROI item for the :class:`~silx.gui.plot.PlotWidget`.
-
-.. inheritance-diagram::
- silx.gui.plot.items.roi
- :parts: 1
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import logging
-import numpy
-
-from ... import utils
-from .. import items
-from ...colors import rgba
-from silx.image.shapes import Polygon
-from silx.image._boundingbox import _BoundingBox
-from ....utils.proxy import docstring
-from ..utils.intersections import segments_intersection
-from ._roi_base import _RegionOfInterestBase
-
-# He following imports have to be exposed by this module
-from ._roi_base import RegionOfInterest
-from ._roi_base import HandleBasedROI
-from ._arc_roi import ArcROI # noqa
-from ._roi_base import InteractionModeMixIn # noqa
-from ._roi_base import RoiInteractionMode # noqa
-
-
-logger = logging.getLogger(__name__)
-
-
-class PointROI(RegionOfInterest, items.SymbolMixIn):
- """A ROI identifying a point in a 2D plot."""
-
- ICON = 'add-shape-point'
- NAME = 'point markers'
- SHORT_NAME = "point"
- """Metadata for this kind of ROI"""
-
- _plotShape = "point"
- """Plot shape which is used for the first interaction"""
-
- _DEFAULT_SYMBOL = '+'
- """Default symbol of the PointROI
-
- It overwrite the `SymbolMixIn` class attribte.
- """
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- items.SymbolMixIn.__init__(self)
- self._marker = items.Marker()
- self._marker.sigItemChanged.connect(self._pointPositionChanged)
- self._marker.setSymbol(self._DEFAULT_SYMBOL)
- self._marker.sigDragStarted.connect(self._editingStarted)
- self._marker.sigDragFinished.connect(self._editingFinished)
- self.addItem(self._marker)
-
- def setFirstShapePoints(self, points):
- self.setPosition(points[0])
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
- self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
- self._updateItemProperty(event, self, self._marker)
- super(PointROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- self._marker.setColor(style.getColor())
-
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
- return self._marker.getPosition()
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param numpy.ndarray pos: 2d-coordinate of this point
- """
- self._marker.setPosition(*pos)
-
- @docstring(_RegionOfInterestBase)
- def contains(self, position):
- roiPos = self.getPosition()
- return position[0] == roiPos[0] and position[1] == roiPos[1]
-
- def _pointPositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- self.sigRegionChanged.emit()
-
- def __str__(self):
- params = '%f %f' % self.getPosition()
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class CrossROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a point in a 2D plot and displayed as a cross
- """
-
- ICON = 'add-shape-cross'
- NAME = 'cross marker'
- SHORT_NAME = "cross"
- """Metadata for this kind of ROI"""
-
- _plotShape = "point"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- HandleBasedROI.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._handle = self.addHandle()
- self._handle.sigItemChanged.connect(self._handlePositionChanged)
- self._handleLabel = self.addLabelHandle()
- self._vmarker = self.addUserHandle(items.YMarker())
- self._vmarker._setSelectable(False)
- self._vmarker._setDraggable(False)
- self._vmarker.setPosition(*self.getPosition())
- self._hmarker = self.addUserHandle(items.XMarker())
- self._hmarker._setSelectable(False)
- self._hmarker._setDraggable(False)
- self._hmarker.setPosition(*self.getPosition())
-
- def _updated(self, event=None, checkVisibility=True):
- if event in [items.ItemChangedType.VISIBLE]:
- markers = (self._vmarker, self._hmarker)
- self._updateItemProperty(event, self, markers)
- super(CrossROI, self)._updated(event, checkVisibility)
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def _updatedStyle(self, event, style):
- super(CrossROI, self)._updatedStyle(event, style)
- for marker in [self._vmarker, self._hmarker]:
- marker.setColor(style.getColor())
- marker.setLineStyle(style.getLineStyle())
- marker.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- pos = points[0]
- self.setPosition(pos)
-
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
- return self._handle.getPosition()
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param numpy.ndarray pos: 2d-coordinate of this point
- """
- self._handle.setPosition(*pos)
-
- def _handlePositionChanged(self, event):
- """Handle center marker position updates"""
- if event is items.ItemChangedType.POSITION:
- position = self.getPosition()
- self._handleLabel.setPosition(*position)
- self._vmarker.setPosition(*position)
- self._hmarker.setPosition(*position)
- self.sigRegionChanged.emit()
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- roiPos = self.getPosition()
- return position[0] == roiPos[0] or position[1] == roiPos[1]
-
-
-class LineROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a line in a 2D plot.
-
- This ROI provides 1 anchor for each boundary of the line, plus an center
- in the center to translate the full ROI.
- """
-
- ICON = 'add-shape-diagonal'
- NAME = 'line ROI'
- SHORT_NAME = "line"
- """Metadata for this kind of ROI"""
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- HandleBasedROI.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._handleStart = self.addHandle()
- self._handleEnd = self.addHandle()
- self._handleCenter = self.addTranslateHandle()
- self._handleLabel = self.addLabelHandle()
-
- shape = items.Shape("polylines")
- shape.setPoints([[0, 0], [0, 0]])
- shape.setColor(rgba(self.getColor()))
- shape.setFill(False)
- shape.setOverlay(True)
- shape.setLineStyle(self.getLineStyle())
- shape.setLineWidth(self.getLineWidth())
- self.__shape = shape
- self.addItem(shape)
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.VISIBLE:
- self._updateItemProperty(event, self, self.__shape)
- super(LineROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(LineROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- assert len(points) == 2
- self.setEndPoints(points[0], points[1])
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def setEndPoints(self, startPoint, endPoint):
- """Set this line location using the ending points
-
- :param numpy.ndarray startPoint: Staring bounding point of the line
- :param numpy.ndarray endPoint: Ending bounding point of the line
- """
- if not numpy.array_equal((startPoint, endPoint), self.getEndPoints()):
- self.__updateEndPoints(startPoint, endPoint)
-
- def __updateEndPoints(self, startPoint, endPoint):
- """Update marker and shape to match given end points
-
- :param numpy.ndarray startPoint: Staring bounding point of the line
- :param numpy.ndarray endPoint: Ending bounding point of the line
- """
- startPoint = numpy.array(startPoint)
- endPoint = numpy.array(endPoint)
- center = (startPoint + endPoint) * 0.5
-
- with utils.blockSignals(self._handleStart):
- self._handleStart.setPosition(startPoint[0], startPoint[1])
- with utils.blockSignals(self._handleEnd):
- self._handleEnd.setPosition(endPoint[0], endPoint[1])
- with utils.blockSignals(self._handleCenter):
- self._handleCenter.setPosition(center[0], center[1])
- with utils.blockSignals(self._handleLabel):
- self._handleLabel.setPosition(center[0], center[1])
-
- line = numpy.array((startPoint, endPoint))
- self.__shape.setPoints(line)
- self.sigRegionChanged.emit()
-
- def getEndPoints(self):
- """Returns bounding points of this ROI.
-
- :rtype: Tuple(numpy.ndarray,numpy.ndarray)
- """
- startPoint = numpy.array(self._handleStart.getPosition())
- endPoint = numpy.array(self._handleEnd.getPosition())
- return (startPoint, endPoint)
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle is self._handleStart:
- _start, end = self.getEndPoints()
- self.__updateEndPoints(current, end)
- elif handle is self._handleEnd:
- start, _end = self.getEndPoints()
- self.__updateEndPoints(start, current)
- elif handle is self._handleCenter:
- start, end = self.getEndPoints()
- delta = current - previous
- start += delta
- end += delta
- self.setEndPoints(start, end)
-
- @docstring(_RegionOfInterestBase)
- def contains(self, position):
- bottom_left = position[0], position[1]
- bottom_right = position[0] + 1, position[1]
- top_left = position[0], position[1] + 1
- top_right = position[0] + 1, position[1] + 1
-
- points = self.__shape.getPoints()
- line_pt1 = points[0]
- line_pt2 = points[1]
-
- bb1 = _BoundingBox.from_points(points)
- if not bb1.contains(position):
- return False
-
- return (
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=bottom_left, seg2_end_pt=bottom_right) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=bottom_right, seg2_end_pt=top_right) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=top_right, seg2_end_pt=top_left) or
- segments_intersection(seg1_start_pt=line_pt1, seg1_end_pt=line_pt2,
- seg2_start_pt=top_left, seg2_end_pt=bottom_left)
- ) is not None
-
- def __str__(self):
- start, end = self.getEndPoints()
- params = start[0], start[1], end[0], end[1]
- params = 'start: %f %f; end: %f %f' % params
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class HorizontalLineROI(RegionOfInterest, items.LineMixIn):
- """A ROI identifying an horizontal line in a 2D plot."""
-
- ICON = 'add-shape-horizontal'
- NAME = 'horizontal line ROI'
- SHORT_NAME = "hline"
- """Metadata for this kind of ROI"""
-
- _plotShape = "hline"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._marker = items.YMarker()
- self._marker.sigItemChanged.connect(self._linePositionChanged)
- self._marker.sigDragStarted.connect(self._editingStarted)
- self._marker.sigDragFinished.connect(self._editingFinished)
- self.addItem(self._marker)
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
- self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
- self._updateItemProperty(event, self, self._marker)
- super(HorizontalLineROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- self._marker.setColor(style.getColor())
- self._marker.setLineStyle(style.getLineStyle())
- self._marker.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- pos = points[0, 1]
- if pos == self.getPosition():
- return
- self.setPosition(pos)
-
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
- pos = self._marker.getPosition()
- return pos[1]
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param float pos: Horizontal position of this line
- """
- self._marker.setPosition(0, pos)
-
- @docstring(_RegionOfInterestBase)
- def contains(self, position):
- return position[1] == self.getPosition()
-
- def _linePositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- self.sigRegionChanged.emit()
-
- def __str__(self):
- params = 'y: %f' % self.getPosition()
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class VerticalLineROI(RegionOfInterest, items.LineMixIn):
- """A ROI identifying a vertical line in a 2D plot."""
-
- ICON = 'add-shape-vertical'
- NAME = 'vertical line ROI'
- SHORT_NAME = "vline"
- """Metadata for this kind of ROI"""
-
- _plotShape = "vline"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._marker = items.XMarker()
- self._marker.sigItemChanged.connect(self._linePositionChanged)
- self._marker.sigDragStarted.connect(self._editingStarted)
- self._marker.sigDragFinished.connect(self._editingFinished)
- self.addItem(self._marker)
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- label = self.getName()
- self._marker.setText(label)
- elif event == items.ItemChangedType.EDITABLE:
- self._marker._setDraggable(self.isEditable())
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
- self._updateItemProperty(event, self, self._marker)
- super(VerticalLineROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- self._marker.setColor(style.getColor())
- self._marker.setLineStyle(style.getLineStyle())
- self._marker.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- pos = points[0, 0]
- self.setPosition(pos)
-
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
- pos = self._marker.getPosition()
- return pos[0]
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param float pos: Horizontal position of this line
- """
- self._marker.setPosition(pos, 0)
-
- @docstring(RegionOfInterest)
- def contains(self, position):
- return position[0] == self.getPosition()
-
- def _linePositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- self.sigRegionChanged.emit()
-
- def __str__(self):
- params = 'x: %f' % self.getPosition()
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class RectangleROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a rectangle in a 2D plot.
-
- This ROI provides 1 anchor for each corner, plus an anchor in the
- center to translate the full ROI.
- """
-
- ICON = 'add-shape-rectangle'
- NAME = 'rectangle ROI'
- SHORT_NAME = "rectangle"
- """Metadata for this kind of ROI"""
-
- _plotShape = "rectangle"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- HandleBasedROI.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._handleTopLeft = self.addHandle()
- self._handleTopRight = self.addHandle()
- self._handleBottomLeft = self.addHandle()
- self._handleBottomRight = self.addHandle()
- self._handleCenter = self.addTranslateHandle()
- self._handleLabel = self.addLabelHandle()
-
- shape = items.Shape("rectangle")
- shape.setPoints([[0, 0], [0, 0]])
- shape.setFill(False)
- shape.setOverlay(True)
- shape.setLineStyle(self.getLineStyle())
- shape.setLineWidth(self.getLineWidth())
- shape.setColor(rgba(self.getColor()))
- self.__shape = shape
- self.addItem(shape)
-
- def _updated(self, event=None, checkVisibility=True):
- if event in [items.ItemChangedType.VISIBLE]:
- self._updateItemProperty(event, self, self.__shape)
- super(RectangleROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(RectangleROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- assert len(points) == 2
- self._setBound(points)
-
- def _setBound(self, points):
- """Initialize the rectangle from a bunch of points"""
- top = max(points[:, 1])
- bottom = min(points[:, 1])
- left = min(points[:, 0])
- right = max(points[:, 0])
- size = right - left, top - bottom
- self._updateGeometry(origin=(left, bottom), size=size)
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def getCenter(self):
- """Returns the central point of this rectangle
-
- :rtype: numpy.ndarray([float,float])
- """
- pos = self._handleCenter.getPosition()
- return numpy.array(pos)
-
- def getOrigin(self):
- """Returns the corner point with the smaller coordinates
-
- :rtype: numpy.ndarray([float,float])
- """
- pos = self._handleBottomLeft.getPosition()
- return numpy.array(pos)
-
- def getSize(self):
- """Returns the size of this rectangle
-
- :rtype: numpy.ndarray([float,float])
- """
- vmin = self._handleBottomLeft.getPosition()
- vmax = self._handleTopRight.getPosition()
- vmin, vmax = numpy.array(vmin), numpy.array(vmax)
- return vmax - vmin
-
- def setOrigin(self, position):
- """Set the origin position of this ROI
-
- :param numpy.ndarray position: Location of the smaller corner of the ROI
- """
- size = self.getSize()
- self.setGeometry(origin=position, size=size)
-
- def setSize(self, size):
- """Set the size of this ROI
-
- :param numpy.ndarray size: Size of the center of the ROI
- """
- origin = self.getOrigin()
- self.setGeometry(origin=origin, size=size)
-
- def setCenter(self, position):
- """Set the size of this ROI
-
- :param numpy.ndarray position: Location of the center of the ROI
- """
- size = self.getSize()
- self.setGeometry(center=position, size=size)
-
- def setGeometry(self, origin=None, size=None, center=None):
- """Set the geometry of the ROI
- """
- if ((origin is None or numpy.array_equal(origin, self.getOrigin())) and
- (center is None or numpy.array_equal(center, self.getCenter())) and
- numpy.array_equal(size, self.getSize())):
- return # Nothing has changed
-
- self._updateGeometry(origin, size, center)
-
- def _updateGeometry(self, origin=None, size=None, center=None):
- """Forced update of the geometry of the ROI"""
- if origin is not None:
- origin = numpy.array(origin)
- size = numpy.array(size)
- points = numpy.array([origin, origin + size])
- center = origin + size * 0.5
- elif center is not None:
- center = numpy.array(center)
- size = numpy.array(size)
- points = numpy.array([center - size * 0.5, center + size * 0.5])
- else:
- raise ValueError("Origin or center expected")
-
- with utils.blockSignals(self._handleBottomLeft):
- self._handleBottomLeft.setPosition(points[0, 0], points[0, 1])
- with utils.blockSignals(self._handleBottomRight):
- self._handleBottomRight.setPosition(points[1, 0], points[0, 1])
- with utils.blockSignals(self._handleTopLeft):
- self._handleTopLeft.setPosition(points[0, 0], points[1, 1])
- with utils.blockSignals(self._handleTopRight):
- self._handleTopRight.setPosition(points[1, 0], points[1, 1])
- with utils.blockSignals(self._handleCenter):
- self._handleCenter.setPosition(center[0], center[1])
- with utils.blockSignals(self._handleLabel):
- self._handleLabel.setPosition(points[0, 0], points[0, 1])
-
- self.__shape.setPoints(points)
- self.sigRegionChanged.emit()
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- assert isinstance(position, (tuple, list, numpy.array))
- points = self.__shape.getPoints()
- bb1 = _BoundingBox.from_points(points)
- return bb1.contains(position)
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle is self._handleCenter:
- # It is the center anchor
- size = self.getSize()
- self._updateGeometry(center=current, size=size)
- else:
- opposed = {
- self._handleBottomLeft: self._handleTopRight,
- self._handleTopRight: self._handleBottomLeft,
- self._handleBottomRight: self._handleTopLeft,
- self._handleTopLeft: self._handleBottomRight,
- }
- handle2 = opposed[handle]
- current2 = handle2.getPosition()
- points = numpy.array([current, current2])
-
- # Switch handles if they were crossed by interaction
- if self._handleBottomLeft.getXPosition() > self._handleBottomRight.getXPosition():
- self._handleBottomLeft, self._handleBottomRight = self._handleBottomRight, self._handleBottomLeft
-
- if self._handleTopLeft.getXPosition() > self._handleTopRight.getXPosition():
- self._handleTopLeft, self._handleTopRight = self._handleTopRight, self._handleTopLeft
-
- if self._handleBottomLeft.getYPosition() > self._handleTopLeft.getYPosition():
- self._handleBottomLeft, self._handleTopLeft = self._handleTopLeft, self._handleBottomLeft
-
- if self._handleBottomRight.getYPosition() > self._handleTopRight.getYPosition():
- self._handleBottomRight, self._handleTopRight = self._handleTopRight, self._handleBottomRight
-
- self._setBound(points)
-
- def __str__(self):
- origin = self.getOrigin()
- w, h = self.getSize()
- params = origin[0], origin[1], w, h
- params = 'origin: %f %f; width: %f; height: %f' % params
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class CircleROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a circle in a 2D plot.
-
- This ROI provides 1 anchor at the center to translate the circle,
- and one anchor on the perimeter to change the radius.
- """
-
- ICON = 'add-shape-circle'
- NAME = 'circle ROI'
- SHORT_NAME = "circle"
- """Metadata for this kind of ROI"""
-
- _kind = "Circle"
- """Label for this kind of ROI"""
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- items.LineMixIn.__init__(self)
- HandleBasedROI.__init__(self, parent=parent)
- self._handlePerimeter = self.addHandle()
- self._handleCenter = self.addTranslateHandle()
- self._handleCenter.sigItemChanged.connect(self._centerPositionChanged)
- self._handleLabel = self.addLabelHandle()
-
- shape = items.Shape("polygon")
- shape.setPoints([[0, 0], [0, 0]])
- shape.setColor(rgba(self.getColor()))
- shape.setFill(False)
- shape.setOverlay(True)
- shape.setLineStyle(self.getLineStyle())
- shape.setLineWidth(self.getLineWidth())
- self.__shape = shape
- self.addItem(shape)
-
- self.__radius = 0
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.VISIBLE:
- self._updateItemProperty(event, self, self.__shape)
- super(CircleROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(CircleROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- assert len(points) == 2
- self._setRay(points)
-
- def _setRay(self, points):
- """Initialize the circle from the center point and a
- perimeter point."""
- center = points[0]
- radius = numpy.linalg.norm(points[0] - points[1])
- self.setGeometry(center=center, radius=radius)
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def getCenter(self):
- """Returns the central point of this rectangle
-
- :rtype: numpy.ndarray([float,float])
- """
- pos = self._handleCenter.getPosition()
- return numpy.array(pos)
-
- def getRadius(self):
- """Returns the radius of this circle
-
- :rtype: float
- """
- return self.__radius
-
- def setCenter(self, position):
- """Set the center point of this ROI
-
- :param numpy.ndarray position: Location of the center of the circle
- """
- self._handleCenter.setPosition(*position)
-
- def setRadius(self, radius):
- """Set the size of this ROI
-
- :param float size: Radius of the circle
- """
- radius = float(radius)
- if radius != self.__radius:
- self.__radius = radius
- self._updateGeometry()
-
- def setGeometry(self, center, radius):
- """Set the geometry of the ROI
- """
- if numpy.array_equal(center, self.getCenter()):
- self.setRadius(radius)
- else:
- self.__radius = float(radius) # Update radius directly
- self.setCenter(center) # Calls _updateGeometry
-
- def _updateGeometry(self):
- """Update the handles and shape according to given parameters"""
- center = self.getCenter()
- perimeter_point = numpy.array([center[0] + self.__radius, center[1]])
-
- self._handlePerimeter.setPosition(perimeter_point[0], perimeter_point[1])
- self._handleLabel.setPosition(center[0], center[1])
-
- nbpoints = 27
- angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints
- circleShape = numpy.array((numpy.cos(angles) * self.__radius,
- numpy.sin(angles) * self.__radius)).T
- circleShape += center
- self.__shape.setPoints(circleShape)
- self.sigRegionChanged.emit()
-
- def _centerPositionChanged(self, event):
- """Handle position changed events of the center marker"""
- if event is items.ItemChangedType.POSITION:
- self._updateGeometry()
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle is self._handlePerimeter:
- center = self.getCenter()
- self.setRadius(numpy.linalg.norm(center - current))
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- return numpy.linalg.norm(self.getCenter() - position) <= self.getRadius()
-
- def __str__(self):
- center = self.getCenter()
- radius = self.getRadius()
- params = center[0], center[1], radius
- params = 'center: %f %f; radius: %f;' % params
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class EllipseROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying an oriented ellipse in a 2D plot.
-
- This ROI provides 1 anchor at the center to translate the circle,
- and two anchors on the perimeter to modify the major-radius and
- minor-radius. These two anchors also allow to change the orientation.
- """
-
- ICON = 'add-shape-ellipse'
- NAME = 'ellipse ROI'
- SHORT_NAME = "ellipse"
- """Metadata for this kind of ROI"""
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- items.LineMixIn.__init__(self)
- HandleBasedROI.__init__(self, parent=parent)
- self._handleAxis0 = self.addHandle()
- self._handleAxis1 = self.addHandle()
- self._handleCenter = self.addTranslateHandle()
- self._handleCenter.sigItemChanged.connect(self._centerPositionChanged)
- self._handleLabel = self.addLabelHandle()
-
- shape = items.Shape("polygon")
- shape.setPoints([[0, 0], [0, 0]])
- shape.setColor(rgba(self.getColor()))
- shape.setFill(False)
- shape.setOverlay(True)
- shape.setLineStyle(self.getLineStyle())
- shape.setLineWidth(self.getLineWidth())
- self.__shape = shape
- self.addItem(shape)
-
- self._radius = 0., 0.
- self._orientation = 0. # angle in radians between the X-axis and the _handleAxis0
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.VISIBLE:
- self._updateItemProperty(event, self, self.__shape)
- super(EllipseROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(EllipseROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
-
- def setFirstShapePoints(self, points):
- assert len(points) == 2
- self._setRay(points)
-
- @staticmethod
- def _calculateOrientation(p0, p1):
- """return angle in radians between the vector p0-p1
- and the X axis
-
- :param p0: first point coordinates (x, y)
- :param p1: second point coordinates
- :return:
- """
- vector = (p1[0] - p0[0], p1[1] - p0[1])
- x_unit_vector = (1, 0)
- norm = numpy.linalg.norm(vector)
- if norm != 0:
- theta = numpy.arccos(numpy.dot(vector, x_unit_vector) / norm)
- else:
- theta = 0
- if vector[1] < 0:
- # arccos always returns values in range [0, pi]
- theta = 2 * numpy.pi - theta
- return theta
-
- def _setRay(self, points):
- """Initialize the circle from the center point and a
- perimeter point."""
- center = points[0]
- radius = numpy.linalg.norm(points[0] - points[1])
- orientation = self._calculateOrientation(points[0], points[1])
- self.setGeometry(center=center,
- radius=(radius, radius),
- orientation=orientation)
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def getCenter(self):
- """Returns the central point of this rectangle
-
- :rtype: numpy.ndarray([float,float])
- """
- pos = self._handleCenter.getPosition()
- return numpy.array(pos)
-
- def getMajorRadius(self):
- """Returns the half-diameter of the major axis.
-
- :rtype: float
- """
- return max(self._radius)
-
- def getMinorRadius(self):
- """Returns the half-diameter of the minor axis.
-
- :rtype: float
- """
- return min(self._radius)
-
- def getOrientation(self):
- """Return angle in radians between the horizontal (X) axis
- and the major axis of the ellipse in [0, 2*pi[
-
- :rtype: float:
- """
- return self._orientation
-
- def setCenter(self, center):
- """Set the center point of this ROI
-
- :param numpy.ndarray position: Coordinates (X, Y) of the center
- of the ellipse
- """
- self._handleCenter.setPosition(*center)
-
- def setMajorRadius(self, radius):
- """Set the half-diameter of the major axis of the ellipse.
-
- :param float radius:
- Major radius of the ellipsis. Must be a positive value.
- """
- if self._radius[0] > self._radius[1]:
- newRadius = radius, self._radius[1]
- else:
- newRadius = self._radius[0], radius
- self.setGeometry(radius=newRadius)
-
- def setMinorRadius(self, radius):
- """Set the half-diameter of the minor axis of the ellipse.
-
- :param float radius:
- Minor radius of the ellipsis. Must be a positive value.
- """
- if self._radius[0] > self._radius[1]:
- newRadius = self._radius[0], radius
- else:
- newRadius = radius, self._radius[1]
- self.setGeometry(radius=newRadius)
-
- def setOrientation(self, orientation):
- """Rotate the ellipse
-
- :param float orientation: Angle in radians between the horizontal and
- the major axis.
- :return:
- """
- self.setGeometry(orientation=orientation)
-
- def setGeometry(self, center=None, radius=None, orientation=None):
- """
-
- :param center: (X, Y) coordinates
- :param float majorRadius:
- :param float minorRadius:
- :param float orientation: angle in radians between the major axis and the
- horizontal
- :return:
- """
- if center is None:
- center = self.getCenter()
-
- if radius is None:
- radius = self._radius
- else:
- radius = float(radius[0]), float(radius[1])
-
- if orientation is None:
- orientation = self._orientation
- else:
- # ensure that we store the orientation in range [0, 2*pi
- orientation = numpy.mod(orientation, 2 * numpy.pi)
-
- if (numpy.array_equal(center, self.getCenter()) or
- radius != self._radius or
- orientation != self._orientation):
-
- # Update parameters directly
- self._radius = radius
- self._orientation = orientation
-
- if numpy.array_equal(center, self.getCenter()):
- self._updateGeometry()
- else:
- # This will call _updateGeometry
- self.setCenter(center)
-
- def _updateGeometry(self):
- """Update shape and markers"""
- center = self.getCenter()
-
- orientation = self.getOrientation()
- if self._radius[1] > self._radius[0]:
- # _handleAxis1 is the major axis
- orientation -= numpy.pi / 2
-
- point0 = numpy.array([center[0] + self._radius[0] * numpy.cos(orientation),
- center[1] + self._radius[0] * numpy.sin(orientation)])
- point1 = numpy.array([center[0] - self._radius[1] * numpy.sin(orientation),
- center[1] + self._radius[1] * numpy.cos(orientation)])
- with utils.blockSignals(self._handleAxis0):
- self._handleAxis0.setPosition(*point0)
- with utils.blockSignals(self._handleAxis1):
- self._handleAxis1.setPosition(*point1)
- with utils.blockSignals(self._handleLabel):
- self._handleLabel.setPosition(*center)
-
- nbpoints = 27
- angles = numpy.arange(nbpoints) * 2.0 * numpy.pi / nbpoints
- X = (self._radius[0] * numpy.cos(angles) * numpy.cos(orientation)
- - self._radius[1] * numpy.sin(angles) * numpy.sin(orientation))
- Y = (self._radius[0] * numpy.cos(angles) * numpy.sin(orientation)
- + self._radius[1] * numpy.sin(angles) * numpy.cos(orientation))
-
- ellipseShape = numpy.array((X, Y)).T
- ellipseShape += center
- self.__shape.setPoints(ellipseShape)
- self.sigRegionChanged.emit()
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle in (self._handleAxis0, self._handleAxis1):
- center = self.getCenter()
- orientation = self._calculateOrientation(center, current)
- distance = numpy.linalg.norm(center - current)
-
- if handle is self._handleAxis1:
- if self._radius[0] > distance:
- # _handleAxis1 is not the major axis, rotate -90 degrees
- orientation -= numpy.pi / 2
- radius = self._radius[0], distance
-
- else: # _handleAxis0
- if self._radius[1] > distance:
- # _handleAxis0 is not the major axis, rotate +90 degrees
- orientation += numpy.pi / 2
- radius = distance, self._radius[1]
-
- self.setGeometry(radius=radius, orientation=orientation)
-
- def _centerPositionChanged(self, event):
- """Handle position changed events of the center marker"""
- if event is items.ItemChangedType.POSITION:
- self._updateGeometry()
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- major, minor = self.getMajorRadius(), self.getMinorRadius()
- delta = self.getOrientation()
- x, y = position - self.getCenter()
- return ((x*numpy.cos(delta) + y*numpy.sin(delta))**2/major**2 +
- (x*numpy.sin(delta) - y*numpy.cos(delta))**2/minor**2) <= 1
-
- def __str__(self):
- center = self.getCenter()
- major = self.getMajorRadius()
- minor = self.getMinorRadius()
- orientation = self.getOrientation()
- params = center[0], center[1], major, minor, orientation
- params = 'center: %f %f; major radius: %f: minor radius: %f; orientation: %f' % params
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class PolygonROI(HandleBasedROI, items.LineMixIn):
- """A ROI identifying a closed polygon in a 2D plot.
-
- This ROI provides 1 anchor for each point of the polygon.
- """
-
- ICON = 'add-shape-polygon'
- NAME = 'polygon ROI'
- SHORT_NAME = "polygon"
- """Metadata for this kind of ROI"""
-
- _plotShape = "polygon"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- HandleBasedROI.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._handleLabel = self.addLabelHandle()
- self._handleCenter = self.addTranslateHandle()
- self._handlePoints = []
- self._points = numpy.empty((0, 2))
- self._handleClose = None
-
- self._polygon_shape = None
- shape = self.__createShape()
- self.__shape = shape
- self.addItem(shape)
-
- def _updated(self, event=None, checkVisibility=True):
- if event in [items.ItemChangedType.VISIBLE]:
- self._updateItemProperty(event, self, self.__shape)
- super(PolygonROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- super(PolygonROI, self)._updatedStyle(event, style)
- self.__shape.setColor(style.getColor())
- self.__shape.setLineStyle(style.getLineStyle())
- self.__shape.setLineWidth(style.getLineWidth())
- if self._handleClose is not None:
- color = self._computeHandleColor(style.getColor())
- self._handleClose.setColor(color)
-
- def __createShape(self, interaction=False):
- kind = "polygon" if not interaction else "polylines"
- shape = items.Shape(kind)
- shape.setPoints([[0, 0], [0, 0]])
- shape.setFill(False)
- shape.setOverlay(True)
- style = self.getCurrentStyle()
- shape.setLineStyle(style.getLineStyle())
- shape.setLineWidth(style.getLineWidth())
- shape.setColor(rgba(style.getColor()))
- return shape
-
- def setFirstShapePoints(self, points):
- if self._handleClose is not None:
- self._handleClose.setPosition(*points[0])
- self.setPoints(points)
-
- def creationStarted(self):
- """"Called when the ROI creation interaction was started.
- """
- # Handle to see where to close the polygon
- self._handleClose = self.addUserHandle()
- self._handleClose.setSymbol("o")
- color = self._computeHandleColor(rgba(self.getColor()))
- self._handleClose.setColor(color)
-
- # Hide the center while creating the first shape
- self._handleCenter.setSymbol("")
-
- # In interaction replace the polygon by a line, to display something unclosed
- self.removeItem(self.__shape)
- self.__shape = self.__createShape(interaction=True)
- self.__shape.setPoints(self._points)
- self.addItem(self.__shape)
-
- def isBeingCreated(self):
- """Returns true if the ROI is in creation step"""
- return self._handleClose is not None
-
- def creationFinalized(self):
- """"Called when the ROI creation interaction was finalized.
- """
- self.removeHandle(self._handleClose)
- self._handleClose = None
- self.removeItem(self.__shape)
- self.__shape = self.__createShape()
- self.__shape.setPoints(self._points)
- self.addItem(self.__shape)
- # Hide the center while creating the first shape
- self._handleCenter.setSymbol("+")
- for handle in self._handlePoints:
- handle.setSymbol("s")
-
- def _updateText(self, text):
- self._handleLabel.setText(text)
-
- def getPoints(self):
- """Returns the list of the points of this polygon.
-
- :rtype: numpy.ndarray
- """
- return self._points.copy()
-
- def setPoints(self, points):
- """Set the position of this ROI
-
- :param numpy.ndarray pos: 2d-coordinate of this point
- """
- assert(len(points.shape) == 2 and points.shape[1] == 2)
-
- if numpy.array_equal(points, self._points):
- return # Nothing has changed
-
- self._polygon_shape = None
-
- # Update the needed handles
- while len(self._handlePoints) != len(points):
- if len(self._handlePoints) < len(points):
- handle = self.addHandle()
- self._handlePoints.append(handle)
- if self.isBeingCreated():
- handle.setSymbol("")
- else:
- handle = self._handlePoints.pop(-1)
- self.removeHandle(handle)
-
- for handle, position in zip(self._handlePoints, points):
- with utils.blockSignals(handle):
- handle.setPosition(position[0], position[1])
-
- if len(points) > 0:
- if not self.isHandleBeingDragged():
- vmin = numpy.min(points, axis=0)
- vmax = numpy.max(points, axis=0)
- center = (vmax + vmin) * 0.5
- with utils.blockSignals(self._handleCenter):
- self._handleCenter.setPosition(center[0], center[1])
-
- num = numpy.argmin(points[:, 1])
- pos = points[num]
- with utils.blockSignals(self._handleLabel):
- self._handleLabel.setPosition(pos[0], pos[1])
-
- if len(points) == 0:
- self._points = numpy.empty((0, 2))
- else:
- self._points = points
- self.__shape.setPoints(self._points)
- self.sigRegionChanged.emit()
-
- def translate(self, x, y):
- points = self.getPoints()
- delta = numpy.array([x, y])
- self.setPoints(points)
- self.setPoints(points + delta)
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle is self._handleCenter:
- delta = current - previous
- self.translate(delta[0], delta[1])
- else:
- points = self.getPoints()
- num = self._handlePoints.index(handle)
- points[num] = current
- self.setPoints(points)
-
- def handleDragFinished(self, handle, origin, current):
- points = self._points
- if len(points) > 0:
- # Only update the center at the end
- # To avoid to disturb the interaction
- vmin = numpy.min(points, axis=0)
- vmax = numpy.max(points, axis=0)
- center = (vmax + vmin) * 0.5
- with utils.blockSignals(self._handleCenter):
- self._handleCenter.setPosition(center[0], center[1])
-
- def __str__(self):
- points = self._points
- params = '; '.join('%f %f' % (pt[0], pt[1]) for pt in points)
- return "%s(%s)" % (self.__class__.__name__, params)
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- bb1 = _BoundingBox.from_points(self.getPoints())
- if bb1.contains(position) is False:
- return False
-
- if self._polygon_shape is None:
- self._polygon_shape = Polygon(vertices=self.getPoints())
-
- # warning: both the polygon and the value are inverted
- return self._polygon_shape.is_inside(row=position[0], col=position[1])
-
- def _setControlPoints(self, points):
- RegionOfInterest._setControlPoints(self, points=points)
- self._polygon_shape = None
-
-
-class HorizontalRangeROI(RegionOfInterest, items.LineMixIn):
- """A ROI identifying an horizontal range in a 1D plot."""
-
- ICON = 'add-range-horizontal'
- NAME = 'horizontal range ROI'
- SHORT_NAME = "hrange"
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- items.LineMixIn.__init__(self)
- self._markerMin = items.XMarker()
- self._markerMax = items.XMarker()
- self._markerCen = items.XMarker()
- self._markerCen.setLineStyle(" ")
- self._markerMin._setConstraint(self.__positionMinConstraint)
- self._markerMax._setConstraint(self.__positionMaxConstraint)
- self._markerMin.sigDragStarted.connect(self._editingStarted)
- self._markerMin.sigDragFinished.connect(self._editingFinished)
- self._markerMax.sigDragStarted.connect(self._editingStarted)
- self._markerMax.sigDragFinished.connect(self._editingFinished)
- self._markerCen.sigDragStarted.connect(self._editingStarted)
- self._markerCen.sigDragFinished.connect(self._editingFinished)
- self.addItem(self._markerCen)
- self.addItem(self._markerMin)
- self.addItem(self._markerMax)
- self.__filterReentrant = utils.LockReentrant()
-
- def setFirstShapePoints(self, points):
- vmin = min(points[:, 0])
- vmax = max(points[:, 0])
- self._updatePos(vmin, vmax)
-
- def _updated(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- self._updateText()
- elif event == items.ItemChangedType.EDITABLE:
- self._updateEditable()
- self._updateText()
- elif event == items.ItemChangedType.LINE_STYLE:
- markers = [self._markerMin, self._markerMax]
- self._updateItemProperty(event, self, markers)
- elif event in [items.ItemChangedType.VISIBLE,
- items.ItemChangedType.SELECTABLE]:
- markers = [self._markerMin, self._markerMax, self._markerCen]
- self._updateItemProperty(event, self, markers)
- super(HorizontalRangeROI, self)._updated(event, checkVisibility)
-
- def _updatedStyle(self, event, style):
- markers = [self._markerMin, self._markerMax, self._markerCen]
- for m in markers:
- m.setColor(style.getColor())
- m.setLineWidth(style.getLineWidth())
-
- def _updateText(self):
- text = self.getName()
- if self.isEditable():
- self._markerMin.setText("")
- self._markerCen.setText(text)
- else:
- self._markerMin.setText(text)
- self._markerCen.setText("")
-
- def _updateEditable(self):
- editable = self.isEditable()
- self._markerMin._setDraggable(editable)
- self._markerMax._setDraggable(editable)
- self._markerCen._setDraggable(editable)
- if self.isEditable():
- self._markerMin.sigItemChanged.connect(self._minPositionChanged)
- self._markerMax.sigItemChanged.connect(self._maxPositionChanged)
- self._markerCen.sigItemChanged.connect(self._cenPositionChanged)
- self._markerCen.setLineStyle(":")
- else:
- self._markerMin.sigItemChanged.disconnect(self._minPositionChanged)
- self._markerMax.sigItemChanged.disconnect(self._maxPositionChanged)
- self._markerCen.sigItemChanged.disconnect(self._cenPositionChanged)
- self._markerCen.setLineStyle(" ")
-
- def _updatePos(self, vmin, vmax, force=False):
- """Update marker position and emit signal.
-
- :param float vmin:
- :param float vmax:
- :param bool force:
- True to update even if already at the right position.
- """
- if not force and numpy.array_equal((vmin, vmax), self.getRange()):
- return # Nothing has changed
-
- center = (vmin + vmax) * 0.5
- with self.__filterReentrant:
- with utils.blockSignals(self._markerMin):
- self._markerMin.setPosition(vmin, 0)
- with utils.blockSignals(self._markerCen):
- self._markerCen.setPosition(center, 0)
- with utils.blockSignals(self._markerMax):
- self._markerMax.setPosition(vmax, 0)
- self.sigRegionChanged.emit()
-
- def setRange(self, vmin, vmax):
- """Set the range of this ROI.
-
- :param float vmin: Staring location of the range
- :param float vmax: Ending location of the range
- """
- if vmin is None or vmax is None:
- err = "Can't set vmin or vmax to None"
- raise ValueError(err)
- if vmin > vmax:
- err = "Can't set vmin and vmax because vmin >= vmax " \
- "vmin = %s, vmax = %s" % (vmin, vmax)
- raise ValueError(err)
- self._updatePos(vmin, vmax)
-
- def getRange(self):
- """Returns the range of this ROI.
-
- :rtype: Tuple[float,float]
- """
- vmin = self.getMin()
- vmax = self.getMax()
- return vmin, vmax
-
- def setMin(self, vmin):
- """Set the min of this ROI.
-
- :param float vmin: New min
- """
- vmax = self.getMax()
- self._updatePos(vmin, vmax)
-
- def getMin(self):
- """Returns the min value of this ROI.
-
- :rtype: float
- """
- return self._markerMin.getPosition()[0]
-
- def setMax(self, vmax):
- """Set the max of this ROI.
-
- :param float vmax: New max
- """
- vmin = self.getMin()
- self._updatePos(vmin, vmax)
-
- def getMax(self):
- """Returns the max value of this ROI.
-
- :rtype: float
- """
- return self._markerMax.getPosition()[0]
-
- def setCenter(self, center):
- """Set the center of this ROI.
-
- :param float center: New center
- """
- vmin, vmax = self.getRange()
- previousCenter = (vmin + vmax) * 0.5
- delta = center - previousCenter
- self._updatePos(vmin + delta, vmax + delta)
-
- def getCenter(self):
- """Returns the center location of this ROI.
-
- :rtype: float
- """
- vmin, vmax = self.getRange()
- return (vmin + vmax) * 0.5
-
- def __positionMinConstraint(self, x, y):
- """Constraint of the min marker"""
- if self.__filterReentrant.locked():
- # Ignore the constraint when we set an explicit value
- return x, y
- vmax = self.getMax()
- if vmax is None:
- return x, y
- return min(x, vmax), y
-
- def __positionMaxConstraint(self, x, y):
- """Constraint of the max marker"""
- if self.__filterReentrant.locked():
- # Ignore the constraint when we set an explicit value
- return x, y
- vmin = self.getMin()
- if vmin is None:
- return x, y
- return max(x, vmin), y
-
- def _minPositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- marker = self.sender()
- self._updatePos(marker.getXPosition(), self.getMax(), force=True)
-
- def _maxPositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- marker = self.sender()
- self._updatePos(self.getMin(), marker.getXPosition(), force=True)
-
- def _cenPositionChanged(self, event):
- """Handle position changed events of the marker"""
- if event is items.ItemChangedType.POSITION:
- marker = self.sender()
- self.setCenter(marker.getXPosition())
-
- @docstring(HandleBasedROI)
- def contains(self, position):
- return self.getMin() <= position[0] <= self.getMax()
-
- def __str__(self):
- vrange = self.getRange()
- params = 'min: %f; max: %f' % vrange
- return "%s(%s)" % (self.__class__.__name__, params)
diff --git a/silx/gui/plot/items/scatter.py b/silx/gui/plot/items/scatter.py
deleted file mode 100644
index 2d54223..0000000
--- a/silx/gui/plot/items/scatter.py
+++ /dev/null
@@ -1,973 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`Scatter` item of the :class:`Plot`.
-"""
-
-from __future__ import division
-
-
-__authors__ = ["T. Vincent", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "29/03/2017"
-
-
-from collections import namedtuple
-import logging
-import threading
-import numpy
-
-from collections import defaultdict
-from concurrent.futures import ThreadPoolExecutor, CancelledError
-
-from ....utils.proxy import docstring
-from ....math.combo import min_max
-from ....math.histogram import Histogramnd
-from ....utils.weakref import WeakList
-from .._utils.delaunay import delaunay
-from .core import PointsBase, ColormapMixIn, ScatterVisualizationMixIn
-from .axis import Axis
-from ._pick import PickingResult
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _GreedyThreadPoolExecutor(ThreadPoolExecutor):
- """:class:`ThreadPoolExecutor` with an extra :meth:`submit_greedy` method.
- """
-
- def __init__(self, *args, **kwargs):
- super(_GreedyThreadPoolExecutor, self).__init__(*args, **kwargs)
- self.__futures = defaultdict(WeakList)
- self.__lock = threading.RLock()
-
- def submit_greedy(self, queue, fn, *args, **kwargs):
- """Same as :meth:`submit` but cancel previous tasks in given queue.
-
- This means that when a new task is submitted for a given queue,
- all other pending tasks of that queue are cancelled.
-
- :param queue: Identifier of the queue. This must be hashable.
- :param callable fn: The callable to call with provided extra arguments
- :return: Future corresponding to this task
- :rtype: concurrent.futures.Future
- """
- with self.__lock:
- # Cancel previous tasks in given queue
- for future in self.__futures.pop(queue, []):
- if not future.done():
- future.cancel()
-
- future = super(_GreedyThreadPoolExecutor, self).submit(
- fn, *args, **kwargs)
- self.__futures[queue].append(future)
-
- return future
-
-
-# Functions to guess grid shape from coordinates
-
-def _get_z_line_length(array):
- """Return length of line if array is a Z-like 2D regular grid.
-
- :param numpy.ndarray array: The 1D array of coordinates to check
- :return: 0 if no line length could be found,
- else the number of element per line.
- :rtype: int
- """
- sign = numpy.sign(numpy.diff(array))
- if len(sign) == 0 or sign[0] == 0: # We don't handle that
- return 0
- # Check this way to account for 0 sign (i.e., diff == 0)
- beginnings = numpy.where(sign == - sign[0])[0] + 1
- if len(beginnings) == 0:
- return 0
- length = beginnings[0]
- if numpy.all(numpy.equal(numpy.diff(beginnings), length)):
- return length
- return 0
-
-
-def _guess_z_grid_shape(x, y):
- """Guess the shape of a grid from (x, y) coordinates.
-
- The grid might contain more elements than x and y,
- as the last line might be partly filled.
-
- :param numpy.ndarray x:
- :paran numpy.ndarray y:
- :returns: (order, (height, width)) of the regular grid,
- or None if could not guess one.
- 'order' is 'row' if X (i.e., column) is the fast dimension, else 'column'.
- :rtype: Union[List(str,int),None]
- """
- width = _get_z_line_length(x)
- if width != 0:
- return 'row', (int(numpy.ceil(len(x) / width)), width)
- else:
- height = _get_z_line_length(y)
- if height != 0:
- return 'column', (height, int(numpy.ceil(len(y) / height)))
- return None
-
-
-def is_monotonic(array):
- """Returns whether array is monotonic (increasing or decreasing).
-
- :param numpy.ndarray array: 1D array-like container.
- :returns: 1 if array is monotonically increasing,
- -1 if array is monotonically decreasing,
- 0 if array is not monotonic
- :rtype: int
- """
- diff = numpy.diff(numpy.ravel(array))
- with numpy.errstate(invalid='ignore'):
- if numpy.all(diff >= 0):
- return 1
- elif numpy.all(diff <= 0):
- return -1
- else:
- return 0
-
-
-def _guess_grid(x, y):
- """Guess a regular grid from the points.
-
- Result convention is (x, y)
-
- :param numpy.ndarray x: X coordinates of the points
- :param numpy.ndarray y: Y coordinates of the points
- :returns: (order, (height, width)
- order is 'row' or 'column'
- :rtype: Union[List[str,List[int]],None]
- """
- x, y = numpy.ravel(x), numpy.ravel(y)
-
- guess = _guess_z_grid_shape(x, y)
- if guess is not None:
- return guess
-
- else:
- # Cannot guess a regular grid
- # Let's assume it's a single line
- order = 'row' # or 'column' doesn't matter for a single line
- y_monotonic = is_monotonic(y)
- if is_monotonic(x) or y_monotonic: # we can guess a line
- x_min, x_max = min_max(x)
- y_min, y_max = min_max(y)
-
- if not y_monotonic or x_max - x_min >= y_max - y_min:
- # x only is monotonic or both are and X varies more
- # line along X
- shape = 1, len(x)
- else:
- # y only is monotonic or both are and Y varies more
- # line along Y
- shape = len(y), 1
-
- else: # Cannot guess a line from the points
- return None
-
- return order, shape
-
-
-def _quadrilateral_grid_coords(points):
- """Compute an irregular grid of quadrilaterals from a set of points
-
- The input points are expected to lie on a grid.
-
- :param numpy.ndarray points:
- 3D data set of 2D input coordinates (height, width, 2)
- height and width must be at least 2.
- :return: 3D dataset of 2D coordinates of the grid (height+1, width+1, 2)
- """
- assert points.ndim == 3
- assert points.shape[0] >= 2
- assert points.shape[1] >= 2
- assert points.shape[2] == 2
-
- dim0, dim1 = points.shape[:2]
- grid_points = numpy.zeros((dim0 + 1, dim1 + 1, 2), dtype=numpy.float64)
-
- # Compute inner points as mean of 4 neighbours
- neighbour_view = numpy.lib.stride_tricks.as_strided(
- points,
- shape=(dim0 - 1, dim1 - 1, 2, 2, points.shape[2]),
- strides=points.strides[:2] + points.strides[:2] + points.strides[-1:], writeable=False)
- inner_points = numpy.mean(neighbour_view, axis=(2, 3))
- grid_points[1:-1, 1:-1] = inner_points
-
- # Compute 'vertical' sides
- # Alternative: grid_points[1:-1, [0, -1]] = points[:-1, [0, -1]] + points[1:, [0, -1]] - inner_points[:, [0, -1]]
- grid_points[1:-1, [0, -1], 0] = points[:-1, [0, -1], 0] + points[1:, [0, -1], 0] - inner_points[:, [0, -1], 0]
- grid_points[1:-1, [0, -1], 1] = inner_points[:, [0, -1], 1]
-
- # Compute 'horizontal' sides
- grid_points[[0, -1], 1:-1, 0] = inner_points[[0, -1], :, 0]
- grid_points[[0, -1], 1:-1, 1] = points[[0, -1], :-1, 1] + points[[0, -1], 1:, 1] - inner_points[[0, -1], :, 1]
-
- # Compute corners
- d0, d1 = [0, 0, -1, -1], [0, -1, -1, 0]
- grid_points[d0, d1] = 2 * points[d0, d1] - inner_points[d0, d1]
- return grid_points
-
-
-def _quadrilateral_grid_as_triangles(points):
- """Returns the points and indices to make a grid of quadirlaterals
-
- :param numpy.ndarray points:
- 3D array of points (height, width, 2)
- :return: triangle corners (4 * N, 2), triangle indices (2 * N, 3)
- With N = height * width, the number of input points
- """
- nbpoints = numpy.prod(points.shape[:2])
-
- grid = _quadrilateral_grid_coords(points)
- coords = numpy.empty((4 * nbpoints, 2), dtype=grid.dtype)
- coords[::4] = grid[:-1, :-1].reshape(-1, 2)
- coords[1::4] = grid[1:, :-1].reshape(-1, 2)
- coords[2::4] = grid[:-1, 1:].reshape(-1, 2)
- coords[3::4] = grid[1:, 1:].reshape(-1, 2)
-
- indices = numpy.empty((2 * nbpoints, 3), dtype=numpy.uint32)
- indices[::2, 0] = numpy.arange(0, 4 * nbpoints, 4)
- indices[::2, 1] = numpy.arange(1, 4 * nbpoints, 4)
- indices[::2, 2] = numpy.arange(2, 4 * nbpoints, 4)
- indices[1::2, 0] = indices[::2, 1]
- indices[1::2, 1] = indices[::2, 2]
- indices[1::2, 2] = numpy.arange(3, 4 * nbpoints, 4)
-
- return coords, indices
-
-
-_RegularGridInfo = namedtuple(
- '_RegularGridInfo', ['bounds', 'origin', 'scale', 'shape', 'order'])
-
-
-_HistogramInfo = namedtuple(
- '_HistogramInfo', ['mean', 'count', 'sum', 'origin', 'scale', 'shape'])
-
-
-class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn):
- """Description of a scatter"""
-
- _DEFAULT_SELECTABLE = True
- """Default selectable state for scatter plots"""
-
- _SUPPORTED_SCATTER_VISUALIZATION = (
- ScatterVisualizationMixIn.Visualization.POINTS,
- ScatterVisualizationMixIn.Visualization.SOLID,
- ScatterVisualizationMixIn.Visualization.REGULAR_GRID,
- ScatterVisualizationMixIn.Visualization.IRREGULAR_GRID,
- ScatterVisualizationMixIn.Visualization.BINNED_STATISTIC,
- )
- """Overrides supported Visualizations"""
-
- def __init__(self):
- PointsBase.__init__(self)
- ColormapMixIn.__init__(self)
- ScatterVisualizationMixIn.__init__(self)
- self._value = ()
- self.__alpha = None
- # Cache Delaunay triangulation future object
- self.__delaunayFuture = None
- # Cache interpolator future object
- self.__interpolatorFuture = None
- self.__executor = None
-
- # Cache triangles: x, y, indices
- self.__cacheTriangles = None, None, None
-
- # Cache regular grid and histogram info
- self.__cacheRegularGridInfo = None
- self.__cacheHistogramInfo = None
-
- def _updateColormappedData(self):
- """Update the colormapped data, to be called when changed"""
- if self.getVisualization() is self.Visualization.BINNED_STATISTIC:
- histoInfo = self.__getHistogramInfo()
- if histoInfo is None:
- data = None
- else:
- data = getattr(
- histoInfo,
- self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION))
- else:
- data = self.getValueData(copy=False)
- self._setColormappedData(data, copy=False)
-
- @docstring(ScatterVisualizationMixIn)
- def setVisualization(self, mode):
- previous = self.getVisualization()
- if super().setVisualization(mode):
- if (bool(mode is self.Visualization.BINNED_STATISTIC) ^
- bool(previous is self.Visualization.BINNED_STATISTIC)):
- self._updateColormappedData()
- return True
- else:
- return False
-
- @docstring(ScatterVisualizationMixIn)
- def setVisualizationParameter(self, parameter, value):
- parameter = self.VisualizationParameter.from_value(parameter)
-
- if super(Scatter, self).setVisualizationParameter(parameter, value):
- if parameter in (self.VisualizationParameter.GRID_BOUNDS,
- self.VisualizationParameter.GRID_MAJOR_ORDER,
- self.VisualizationParameter.GRID_SHAPE):
- self.__cacheRegularGridInfo = None
-
- if parameter in (self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION,
- self.VisualizationParameter.DATA_BOUNDS_HINT):
- if parameter in (self.VisualizationParameter.BINNED_STATISTIC_SHAPE,
- self.VisualizationParameter.DATA_BOUNDS_HINT):
- self.__cacheHistogramInfo = None # Clean-up cache
- if self.getVisualization() is self.Visualization.BINNED_STATISTIC:
- self._updateColormappedData()
- return True
- else:
- return False
-
- @docstring(ScatterVisualizationMixIn)
- def getCurrentVisualizationParameter(self, parameter):
- value = self.getVisualizationParameter(parameter)
- if (parameter is self.VisualizationParameter.DATA_BOUNDS_HINT or
- value is not None):
- return value # Value has been set, return it
-
- elif parameter is self.VisualizationParameter.GRID_BOUNDS:
- grid = self.__getRegularGridInfo()
- return None if grid is None else grid.bounds
-
- elif parameter is self.VisualizationParameter.GRID_MAJOR_ORDER:
- grid = self.__getRegularGridInfo()
- return None if grid is None else grid.order
-
- elif parameter is self.VisualizationParameter.GRID_SHAPE:
- grid = self.__getRegularGridInfo()
- return None if grid is None else grid.shape
-
- elif parameter is self.VisualizationParameter.BINNED_STATISTIC_SHAPE:
- info = self.__getHistogramInfo()
- return None if info is None else info.shape
-
- else:
- raise NotImplementedError()
-
- def __getRegularGridInfo(self):
- """Get grid info"""
- if self.__cacheRegularGridInfo is None:
- shape = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_SHAPE)
- order = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_MAJOR_ORDER)
- if shape is None or order is None:
- guess = _guess_grid(self.getXData(copy=False),
- self.getYData(copy=False))
- if guess is None:
- _logger.warning(
- 'Cannot guess a grid: Cannot display as regular grid image')
- return None
- if shape is None:
- shape = guess[1]
- if order is None:
- order = guess[0]
-
- nbpoints = len(self.getXData(copy=False))
- if nbpoints > shape[0] * shape[1]:
- # More data points that provided grid shape: enlarge grid
- _logger.warning(
- "More data points than provided grid shape size: extends grid")
- dim0, dim1 = shape
- if order == 'row': # keep dim1, enlarge dim0
- dim0 = nbpoints // dim1 + (1 if nbpoints % dim1 else 0)
- else: # keep dim0, enlarge dim1
- dim1 = nbpoints // dim0 + (1 if nbpoints % dim0 else 0)
- shape = dim0, dim1
-
- bounds = self.getVisualizationParameter(
- self.VisualizationParameter.GRID_BOUNDS)
- if bounds is None:
- x, y = self.getXData(copy=False), self.getYData(copy=False)
- min_, max_ = min_max(x)
- xRange = (min_, max_) if (x[0] - min_) < (max_ - x[0]) else (max_, min_)
- min_, max_ = min_max(y)
- yRange = (min_, max_) if (y[0] - min_) < (max_ - y[0]) else (max_, min_)
- bounds = (xRange[0], yRange[0]), (xRange[1], yRange[1])
-
- begin, end = bounds
- scale = ((end[0] - begin[0]) / max(1, shape[1] - 1),
- (end[1] - begin[1]) / max(1, shape[0] - 1))
- if scale[0] == 0 and scale[1] == 0:
- scale = 1., 1.
- elif scale[0] == 0:
- scale = scale[1], scale[1]
- elif scale[1] == 0:
- scale = scale[0], scale[0]
-
- origin = begin[0] - 0.5 * scale[0], begin[1] - 0.5 * scale[1]
-
- self.__cacheRegularGridInfo = _RegularGridInfo(
- bounds=bounds, origin=origin, scale=scale, shape=shape, order=order)
-
- return self.__cacheRegularGridInfo
-
- def __getHistogramInfo(self):
- """Get histogram info"""
- if self.__cacheHistogramInfo is None:
- shape = self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_SHAPE)
- if shape is None:
- shape = 100, 100 # TODO compute auto shape
-
- x, y, values = self.getData(copy=False)[:3]
- if len(x) == 0: # No histogram
- return None
-
- if not numpy.issubdtype(x.dtype, numpy.floating):
- x = x.astype(numpy.float64)
- if not numpy.issubdtype(y.dtype, numpy.floating):
- y = y.astype(numpy.float64)
- if not numpy.issubdtype(values.dtype, numpy.floating):
- values = values.astype(numpy.float64)
-
- ranges = (tuple(min_max(y, finite=True)),
- tuple(min_max(x, finite=True)))
- rangesHint = self.getVisualizationParameter(
- self.VisualizationParameter.DATA_BOUNDS_HINT)
- if rangesHint is not None:
- ranges = tuple((min(dataMin, hintMin), max(dataMax, hintMax))
- for (dataMin, dataMax), (hintMin, hintMax) in zip(ranges, rangesHint))
-
- points = numpy.transpose(numpy.array((y, x)))
- counts, sums, bin_edges = Histogramnd(
- points,
- histo_range=ranges,
- n_bins=shape,
- weights=values)
- yEdges, xEdges = bin_edges
- origin = xEdges[0], yEdges[0]
- scale = ((xEdges[-1] - xEdges[0]) / (len(xEdges) - 1),
- (yEdges[-1] - yEdges[0]) / (len(yEdges) - 1))
-
- with numpy.errstate(divide='ignore', invalid='ignore'):
- histo = sums / counts
-
- self.__cacheHistogramInfo = _HistogramInfo(
- mean=histo, count=counts, sum=sums,
- origin=origin, scale=scale, shape=shape)
-
- return self.__cacheHistogramInfo
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- # Filter-out values <= 0
- xFiltered, yFiltered, valueFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
-
- # Remove not finite numbers (this includes filtered out x, y <= 0)
- mask = numpy.logical_and(numpy.isfinite(xFiltered), numpy.isfinite(yFiltered))
- xFiltered = xFiltered[mask]
- yFiltered = yFiltered[mask]
-
- if len(xFiltered) == 0:
- return None # No data to display, do not add renderer to backend
-
- visualization = self.getVisualization()
-
- if visualization is self.Visualization.BINNED_STATISTIC:
- plot = self.getPlot()
- if (plot is None or
- plot.getXAxis().getScale() != Axis.LINEAR or
- plot.getYAxis().getScale() != Axis.LINEAR):
- # Those visualizations are not available with log scaled axes
- return None
-
- histoInfo = self.__getHistogramInfo()
- if histoInfo is None:
- return None
- data = getattr(histoInfo, self.getVisualizationParameter(
- self.VisualizationParameter.BINNED_STATISTIC_FUNCTION))
-
- return backend.addImage(
- data=data,
- origin=histoInfo.origin,
- scale=histoInfo.scale,
- colormap=self.getColormap(),
- alpha=self.getAlpha())
-
- # Compute colors
- cmap = self.getColormap()
- rgbacolors = cmap.applyToData(self)
-
- if self.__alpha is not None:
- rgbacolors[:, -1] = (rgbacolors[:, -1] * self.__alpha).astype(numpy.uint8)
-
- visualization = self.getVisualization()
-
- if visualization is self.Visualization.POINTS:
- return backend.addCurve(xFiltered, yFiltered,
- color=rgbacolors[mask],
- symbol=self.getSymbol(),
- linewidth=0,
- linestyle="",
- yaxis='left',
- xerror=xerror,
- yerror=yerror,
- fill=False,
- alpha=self.getAlpha(),
- symbolsize=self.getSymbolSize(),
- baseline=None)
-
- else:
- plot = self.getPlot()
- if (plot is None or
- plot.getXAxis().getScale() != Axis.LINEAR or
- plot.getYAxis().getScale() != Axis.LINEAR):
- # Those visualizations are not available with log scaled axes
- return None
-
- if visualization is self.Visualization.SOLID:
- triangulation = self._getDelaunay().result()
- if triangulation is None:
- _logger.warning(
- 'Cannot get a triangulation: Cannot display as solid surface')
- return None
- else:
- triangles = triangulation.simplices.astype(numpy.int32)
- return backend.addTriangles(xFiltered,
- yFiltered,
- triangles,
- color=rgbacolors[mask],
- alpha=self.getAlpha())
-
- elif visualization is self.Visualization.REGULAR_GRID:
- gridInfo = self.__getRegularGridInfo()
- if gridInfo is None:
- return None
-
- dim0, dim1 = gridInfo.shape
- if gridInfo.order == 'column': # transposition needed
- dim0, dim1 = dim1, dim0
-
- if len(rgbacolors) == dim0 * dim1:
- image = rgbacolors.reshape(dim0, dim1, -1)
- else:
- # The points do not fill the whole image
- image = numpy.empty((dim0 * dim1, 4), dtype=rgbacolors.dtype)
- image[:len(rgbacolors)] = rgbacolors
- image[len(rgbacolors):] = 0, 0, 0, 0 # Transparent pixels
- image.shape = dim0, dim1, -1
-
- if gridInfo.order == 'column':
- image = numpy.transpose(image, axes=(1, 0, 2))
-
- return backend.addImage(
- data=image,
- origin=gridInfo.origin,
- scale=gridInfo.scale,
- colormap=None,
- alpha=self.getAlpha())
-
- elif visualization is self.Visualization.IRREGULAR_GRID:
- gridInfo = self.__getRegularGridInfo()
- if gridInfo is None:
- return None
-
- shape = gridInfo.shape
- if shape is None: # No shape, no display
- return None
-
- nbpoints = len(xFiltered)
- if nbpoints == 1:
- # single point, render as a square points
- return backend.addCurve(xFiltered, yFiltered,
- color=rgbacolors[mask],
- symbol='s',
- linewidth=0,
- linestyle="",
- yaxis='left',
- xerror=None,
- yerror=None,
- fill=False,
- alpha=self.getAlpha(),
- symbolsize=7,
- baseline=None)
-
- # Make shape include all points
- gridOrder = gridInfo.order
- if nbpoints != numpy.prod(shape):
- if gridOrder == 'row':
- shape = int(numpy.ceil(nbpoints / shape[1])), shape[1]
- else: # column-major order
- shape = shape[0], int(numpy.ceil(nbpoints / shape[0]))
-
- if shape[0] < 2 or shape[1] < 2: # Single line, at least 2 points
- points = numpy.ones((2, nbpoints, 2), dtype=numpy.float64)
- # Use row/column major depending on shape, not on info value
- gridOrder = 'row' if shape[0] == 1 else 'column'
-
- if gridOrder == 'row':
- points[0, :, 0] = xFiltered
- points[0, :, 1] = yFiltered
- else: # column-major order
- points[0, :, 0] = yFiltered
- points[0, :, 1] = xFiltered
-
- # Add a second line that will be clipped in the end
- points[1, :-1] = points[0, :-1] + numpy.cross(
- points[0, 1:] - points[0, :-1], (0., 0., 1.))[:, :2]
- points[1, -1] = points[0, -1] + numpy.cross(
- points[0, -1] - points[0, -2], (0., 0., 1.))[:2]
-
- points.shape = 2, nbpoints, 2 # Use same shape for both orders
- coords, indices = _quadrilateral_grid_as_triangles(points)
-
- elif gridOrder == 'row': # row-major order
- if nbpoints != numpy.prod(shape):
- points = numpy.empty((numpy.prod(shape), 2), dtype=numpy.float64)
- points[:nbpoints, 0] = xFiltered
- points[:nbpoints, 1] = yFiltered
- # Index of last element of last fully filled row
- index = (nbpoints // shape[1]) * shape[1]
- points[nbpoints:, 0] = xFiltered[index - (numpy.prod(shape) - nbpoints):index]
- points[nbpoints:, 1] = yFiltered[-1]
- else:
- points = numpy.transpose((xFiltered, yFiltered))
- points.shape = shape[0], shape[1], 2
-
- else: # column-major order
- if nbpoints != numpy.prod(shape):
- points = numpy.empty((numpy.prod(shape), 2), dtype=numpy.float64)
- points[:nbpoints, 0] = yFiltered
- points[:nbpoints, 1] = xFiltered
- # Index of last element of last fully filled column
- index = (nbpoints // shape[0]) * shape[0]
- points[nbpoints:, 0] = yFiltered[index - (numpy.prod(shape) - nbpoints):index]
- points[nbpoints:, 1] = xFiltered[-1]
- else:
- points = numpy.transpose((yFiltered, xFiltered))
- points.shape = shape[1], shape[0], 2
-
- coords, indices = _quadrilateral_grid_as_triangles(points)
-
- # Remove unused extra triangles
- coords = coords[:4*nbpoints]
- indices = indices[:2*nbpoints]
-
- if gridOrder == 'row':
- x, y = coords[:, 0], coords[:, 1]
- else: # column-major order
- y, x = coords[:, 0], coords[:, 1]
-
- rgbacolors = rgbacolors[mask] # Filter-out not finite points
- gridcolors = numpy.empty(
- (4 * nbpoints, rgbacolors.shape[-1]), dtype=rgbacolors.dtype)
- for first in range(4):
- gridcolors[first::4] = rgbacolors[:nbpoints]
-
- return backend.addTriangles(x,
- y,
- indices,
- color=gridcolors,
- alpha=self.getAlpha())
-
- else:
- _logger.error("Unhandled visualization %s", visualization)
- return None
-
- @docstring(PointsBase)
- def pick(self, x, y):
- result = super(Scatter, self).pick(x, y)
-
- if result is not None:
- visualization = self.getVisualization()
-
- if visualization is self.Visualization.IRREGULAR_GRID:
- # Specific handling of picking for the irregular grid mode
- index = result.getIndices(copy=False)[0] // 4
- result = PickingResult(self, (index,))
-
- elif visualization is self.Visualization.REGULAR_GRID:
- # Specific handling of picking for the regular grid mode
- picked = result.getIndices(copy=False)
- if picked is None:
- return None
- row, column = picked[0][0], picked[1][0]
-
- gridInfo = self.__getRegularGridInfo()
- if gridInfo is None:
- return None
-
- if gridInfo.order == 'row':
- index = row * gridInfo.shape[1] + column
- else:
- index = row + column * gridInfo.shape[0]
- if index >= len(self.getXData(copy=False)): # OK as long as not log scale
- return None # Image can be larger than scatter
-
- result = PickingResult(self, (index,))
-
- elif visualization is self.Visualization.BINNED_STATISTIC:
- picked = result.getIndices(copy=False)
- if picked is None or len(picked) == 0 or len(picked[0]) == 0:
- return None
- row, col = picked[0][0], picked[1][0]
- histoInfo = self.__getHistogramInfo()
- if histoInfo is None:
- return None
- sx, sy = histoInfo.scale
- ox, oy = histoInfo.origin
- xdata = self.getXData(copy=False)
- ydata = self.getYData(copy=False)
- indices = numpy.nonzero(numpy.logical_and(
- numpy.logical_and(xdata >= ox + sx * col, xdata < ox + sx * (col + 1)),
- numpy.logical_and(ydata >= oy + sy * row, ydata < oy + sy * (row + 1))))[0]
- result = None if len(indices) == 0 else PickingResult(self, indices)
-
- return result
-
- def __getExecutor(self):
- """Returns async greedy executor
-
- :rtype: _GreedyThreadPoolExecutor
- """
- if self.__executor is None:
- self.__executor = _GreedyThreadPoolExecutor(max_workers=2)
- return self.__executor
-
- def _getDelaunay(self):
- """Returns a :class:`Future` which result is the Delaunay object.
-
- :rtype: concurrent.futures.Future
- """
- if self.__delaunayFuture is None or self.__delaunayFuture.cancelled():
- # Need to init a new delaunay
- x, y = self.getData(copy=False)[:2]
- # Remove not finite points
- mask = numpy.logical_and(numpy.isfinite(x), numpy.isfinite(y))
-
- self.__delaunayFuture = self.__getExecutor().submit_greedy(
- 'delaunay', delaunay, x[mask], y[mask])
-
- return self.__delaunayFuture
-
- @staticmethod
- def __initInterpolator(delaunayFuture, values):
- """Returns an interpolator for the given data points
-
- :param concurrent.futures.Future delaunayFuture:
- Future object which result is a Delaunay object
- :param numpy.ndarray values: The data value of valid points.
- :rtype: Union[callable,None]
- """
- # Wait for Delaunay to complete
- try:
- triangulation = delaunayFuture.result()
- except CancelledError:
- triangulation = None
-
- if triangulation is None:
- interpolator = None # Error case
- else:
- # Lazy-loading of interpolator
- try:
- from scipy.interpolate import LinearNDInterpolator
- except ImportError:
- LinearNDInterpolator = None
-
- if LinearNDInterpolator is not None:
- interpolator = LinearNDInterpolator(triangulation, values)
-
- # First call takes a while, do it here
- interpolator([(0., 0.)])
-
- else:
- # Fallback using matplotlib interpolator
- import matplotlib.tri
-
- x, y = triangulation.points.T
- tri = matplotlib.tri.Triangulation(
- x, y, triangles=triangulation.simplices)
- mplInterpolator = matplotlib.tri.LinearTriInterpolator(
- tri, values)
-
- # Wrap interpolator to have same API as scipy's one
- def interpolator(points):
- return mplInterpolator(*points.T)
-
- return interpolator
-
- def _getInterpolator(self):
- """Returns a :class:`Future` which result is the interpolator.
-
- The interpolator is a callable taking an array Nx2 of points
- as a single argument.
- The :class:`Future` result is None in case the interpolator cannot
- be initialized.
-
- :rtype: concurrent.futures.Future
- """
- if (self.__interpolatorFuture is None or
- self.__interpolatorFuture.cancelled()):
- # Need to init a new interpolator
- x, y, values = self.getData(copy=False)[:3]
- # Remove not finite points
- mask = numpy.logical_and(numpy.isfinite(x), numpy.isfinite(y))
- x, y, values = x[mask], y[mask], values[mask]
-
- self.__interpolatorFuture = self.__getExecutor().submit_greedy(
- 'interpolator',
- self.__initInterpolator, self._getDelaunay(), values)
- return self.__interpolatorFuture
-
- def _logFilterData(self, xPositive, yPositive):
- """Filter out values with x or y <= 0 on log axes
-
- :param bool xPositive: True to filter arrays according to X coords.
- :param bool yPositive: True to filter arrays according to Y coords.
- :return: The filtered arrays or unchanged object if not filtering needed
- :rtype: (x, y, value, xerror, yerror)
- """
- # overloaded from PointsBase to filter also value.
- value = self.getValueData(copy=False)
-
- if xPositive or yPositive:
- clipped = self._getClippingBoolArray(xPositive, yPositive)
-
- if numpy.any(clipped):
- # copy to keep original array and convert to float
- value = numpy.array(value, copy=True, dtype=numpy.float64)
- value[clipped] = numpy.nan
-
- x, y, xerror, yerror = PointsBase._logFilterData(self, xPositive, yPositive)
-
- return x, y, value, xerror, yerror
-
- def getValueData(self, copy=True):
- """Returns the value assigned to the scatter data points.
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self._value, copy=copy)
-
- def getAlphaData(self, copy=True):
- """Returns the alpha (transparency) assigned to the scatter data points.
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :rtype: numpy.ndarray
- """
- return numpy.array(self.__alpha, copy=copy)
-
- def getData(self, copy=True, displayed=False):
- """Returns the x, y coordinates and the value of the data points
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :param bool displayed: True to only get curve points that are displayed
- in the plot. Default: False.
- Note: If plot has log scale, negative points
- are not displayed.
- :returns: (x, y, value, xerror, yerror)
- :rtype: 5-tuple of numpy.ndarray
- """
- if displayed:
- data = self._getCachedData()
- if data is not None:
- assert len(data) == 5
- return data
-
- return (self.getXData(copy),
- self.getYData(copy),
- self.getValueData(copy),
- self.getXErrorData(copy),
- self.getYErrorData(copy))
-
- # reimplemented from PointsBase to handle `value`
- def setData(self, x, y, value, xerror=None, yerror=None, alpha=None, copy=True):
- """Set the data of the scatter.
-
- :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 corresponding to the value of
- the data points.
- :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 alpha: Values with the transparency (between 0 and 1)
- :type alpha: A float, or a numpy.ndarray of float32
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- """
- value = numpy.array(value, copy=copy)
- assert value.ndim == 1
- assert len(x) == len(value)
-
- # Convert complex data
- if numpy.iscomplexobj(value):
- _logger.warning(
- 'Converting value data to absolute value to plot it.')
- value = numpy.absolute(value)
-
- # Reset triangulation and interpolator
- if self.__delaunayFuture is not None:
- self.__delaunayFuture.cancel()
- self.__delaunayFuture = None
- if self.__interpolatorFuture is not None:
- self.__interpolatorFuture.cancel()
- self.__interpolatorFuture = None
-
- # Data changed, this needs update
- self.__cacheRegularGridInfo = None
- self.__cacheHistogramInfo = None
-
- self._value = value
- self._updateColormappedData()
-
- if alpha is not None:
- # Make sure alpha is an array of float in [0, 1]
- alpha = numpy.array(alpha, copy=copy)
- assert alpha.ndim == 1
- assert len(x) == len(alpha)
- if alpha.dtype.kind != 'f':
- alpha = alpha.astype(numpy.float32)
- if numpy.any(numpy.logical_or(alpha < 0., alpha > 1.)):
- alpha = numpy.clip(alpha, 0., 1.)
- self.__alpha = alpha
-
- # set x, y, xerror, yerror
-
- # call self._updated + plot._invalidateDataRange()
- PointsBase.setData(self, x, y, xerror, yerror, copy)
diff --git a/silx/gui/plot/items/shape.py b/silx/gui/plot/items/shape.py
deleted file mode 100644
index 955dfe3..0000000
--- a/silx/gui/plot/items/shape.py
+++ /dev/null
@@ -1,288 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# in the Software without restriction, including without limitation the rights
-# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-# copies of the Software, and to permit persons to whom the Software is
-# furnished to do so, subject to the following conditions:
-#
-# The above copyright notice and this permission notice shall be included in
-# all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
-# THE SOFTWARE.
-#
-# ###########################################################################*/
-"""This module provides the :class:`Shape` item of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "21/12/2018"
-
-
-import logging
-
-import numpy
-import six
-
-from ... import colors
-from .core import (
- Item, DataItem,
- ColorMixIn, FillMixIn, ItemChangedType, LineMixIn, YAxisMixIn)
-
-
-_logger = logging.getLogger(__name__)
-
-
-# TODO probably make one class for each kind of shape
-# TODO check fill:polygon/polyline + fill = duplicated
-class Shape(Item, ColorMixIn, FillMixIn, LineMixIn):
- """Description of a shape item
-
- :param str type_: The type of shape in:
- 'hline', 'polygon', 'rectangle', 'vline', 'polylines'
- """
-
- def __init__(self, type_):
- Item.__init__(self)
- ColorMixIn.__init__(self)
- FillMixIn.__init__(self)
- LineMixIn.__init__(self)
- self._overlay = False
- assert type_ in ('hline', 'polygon', 'rectangle', 'vline', 'polylines')
- self._type = type_
- self._points = ()
- self._lineBgColor = None
-
- self._handle = None
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- points = self.getPoints(copy=False)
- x, y = points.T[0], points.T[1]
- return backend.addShape(x,
- y,
- shape=self.getType(),
- color=self.getColor(),
- fill=self.isFill(),
- overlay=self.isOverlay(),
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- linebgcolor=self.getLineBgColor())
-
- def isOverlay(self):
- """Return true if shape is drawn as an overlay
-
- :rtype: bool
- """
- return self._overlay
-
- def setOverlay(self, overlay):
- """Set the overlay state of the shape
-
- :param bool overlay: True to make it an overlay
- """
- overlay = bool(overlay)
- if overlay != self._overlay:
- self._overlay = overlay
- self._updated(ItemChangedType.OVERLAY)
-
- def getType(self):
- """Returns the type of shape to draw.
-
- One of: 'hline', 'polygon', 'rectangle', 'vline', 'polylines'
-
- :rtype: str
- """
- return self._type
-
- def getPoints(self, copy=True):
- """Get the control points of the shape.
-
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :return: Array of point coordinates
- :rtype: numpy.ndarray with 2 dimensions
- """
- return numpy.array(self._points, copy=copy)
-
- def setPoints(self, points, copy=True):
- """Set the point coordinates
-
- :param numpy.ndarray points: Array of point coordinates
- :param bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :return:
- """
- self._points = numpy.array(points, copy=copy)
- self._updated(ItemChangedType.DATA)
-
- def getLineBgColor(self):
- """Returns the RGBA color of the item
- :rtype: 4-tuple of float in [0, 1] or array of colors
- """
- return self._lineBgColor
-
- def setLineBgColor(self, color, copy=True):
- """Set item color
- :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 bool copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- """
- if color is not None:
- if isinstance(color, six.string_types):
- color = colors.rgba(color)
- else:
- color = numpy.array(color, copy=copy)
- # TODO more checks + improve color array support
- if color.ndim == 1: # Single RGBA color
- color = colors.rgba(color)
- else: # Array of colors
- assert color.ndim == 2
-
- self._lineBgColor = color
- self._updated(ItemChangedType.LINE_BG_COLOR)
-
-
-class BoundingRect(DataItem, YAxisMixIn):
- """An invisible shape which enforce the plot view to display the defined
- space on autoscale.
-
- This item do not display anything. But if the visible property is true,
- this bounding box is used by the plot, if not, the bounding box is
- ignored. That's the default behaviour for plot items.
-
- It can be applied on the "left" or "right" axes. Not both at the same time.
- """
-
- def __init__(self):
- DataItem.__init__(self)
- YAxisMixIn.__init__(self)
- self.__bounds = None
-
- def setBounds(self, rect):
- """Set the bounding box of this item in data coordinates
-
- :param Union[None,List[float]] rect: (xmin, xmax, ymin, ymax) or None
- """
- if rect is not None:
- rect = float(rect[0]), float(rect[1]), float(rect[2]), float(rect[3])
- assert rect[0] <= rect[1]
- assert rect[2] <= rect[3]
-
- if rect != self.__bounds:
- self.__bounds = rect
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
- def _getBounds(self):
- if self.__bounds is None:
- return None
- plot = self.getPlot()
- if plot is not None:
- xPositive = plot.getXAxis()._isLogarithmic()
- yPositive = plot.getYAxis()._isLogarithmic()
- if xPositive or yPositive:
- bounds = list(self.__bounds)
- if xPositive and bounds[1] <= 0:
- return None
- if xPositive and bounds[0] <= 0:
- bounds[0] = bounds[1]
- if yPositive and bounds[3] <= 0:
- return None
- if yPositive and bounds[2] <= 0:
- bounds[2] = bounds[3]
- return tuple(bounds)
-
- return self.__bounds
-
-
-class _BaseExtent(DataItem):
- """Base class for :class:`XAxisExtent` and :class:`YAxisExtent`.
-
- :param str axis: Either 'x' or 'y'.
- """
-
- def __init__(self, axis='x'):
- assert axis in ('x', 'y')
- DataItem.__init__(self)
- self.__axis = axis
- self.__range = 1., 100.
-
- def setRange(self, min_, max_):
- """Set the range of the extent of this item in data coordinates.
-
- :param float min_: Lower bound of the extent
- :param float max_: Upper bound of the extent
- :raises ValueError: If min > max or not finite bounds
- """
- range_ = float(min_), float(max_)
- if not numpy.all(numpy.isfinite(range_)):
- raise ValueError("min_ and max_ must be finite numbers.")
- if range_[0] > range_[1]:
- raise ValueError("min_ must be lesser or equal to max_")
-
- if range_ != self.__range:
- self.__range = range_
- self._boundsChanged()
- self._updated(ItemChangedType.DATA)
-
- def getRange(self):
- """Returns the range (min, max) of the extent in data coordinates.
-
- :rtype: List[float]
- """
- return self.__range
-
- def _getBounds(self):
- min_, max_ = self.getRange()
-
- plot = self.getPlot()
- if plot is not None:
- axis = plot.getXAxis() if self.__axis == 'x' else plot.getYAxis()
- if axis._isLogarithmic():
- if max_ <= 0:
- return None
- if min_ <= 0:
- min_ = max_
-
- if self.__axis == 'x':
- return min_, max_, float('nan'), float('nan')
- else:
- return float('nan'), float('nan'), min_, max_
-
-
-class XAxisExtent(_BaseExtent):
- """Invisible item with a settable horizontal data extent.
-
- This item do not display anything, but it behaves as a data
- item with a horizontal extent regarding plot data bounds, i.e.,
- :meth:`PlotWidget.resetZoom` will take this horizontal extent into account.
- """
- def __init__(self):
- _BaseExtent.__init__(self, axis='x')
-
-
-class YAxisExtent(_BaseExtent, YAxisMixIn):
- """Invisible item with a settable vertical data extent.
-
- This item do not display anything, but it behaves as a data
- item with a vertical extent regarding plot data bounds, i.e.,
- :meth:`PlotWidget.resetZoom` will take this vertical extent into account.
- """
-
- def __init__(self):
- _BaseExtent.__init__(self, axis='y')
- YAxisMixIn.__init__(self)
diff --git a/silx/gui/plot/matplotlib/Colormap.py b/silx/gui/plot/matplotlib/Colormap.py
deleted file mode 100644
index dc432b2..0000000
--- a/silx/gui/plot/matplotlib/Colormap.py
+++ /dev/null
@@ -1,249 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-# Copyright (C) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ############################################################################*/
-"""Matplotlib's new colormaps"""
-
-import numpy
-import logging
-from matplotlib.colors import ListedColormap
-import matplotlib.colors
-import matplotlib.cm
-import silx.resources
-from silx.utils.deprecation import deprecated, deprecated_warning
-
-
-deprecated_warning(type_='module',
- name=__file__,
- replacement='silx.gui.colors.Colormap',
- since_version='0.10.0')
-
-
-_logger = logging.getLogger(__name__)
-
-_AVAILABLE_AS_RESOURCE = ('magma', 'inferno', 'plasma', 'viridis')
-"""List available colormap name as resources"""
-
-_AVAILABLE_AS_BUILTINS = ('gray', 'reversed gray',
- 'temperature', 'red', 'green', 'blue')
-"""List of colormaps available through built-in declarations"""
-
-_CMAPS = {}
-"""Cache colormaps"""
-
-
-@property
-@deprecated(since_version='0.10.0')
-def magma():
- return getColormap('magma')
-
-
-@property
-@deprecated(since_version='0.10.0')
-def inferno():
- return getColormap('inferno')
-
-
-@property
-@deprecated(since_version='0.10.0')
-def plasma():
- return getColormap('plasma')
-
-
-@property
-@deprecated(since_version='0.10.0')
-def viridis():
- return getColormap('viridis')
-
-
-@deprecated(since_version='0.10.0')
-def getColormap(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 name in _AVAILABLE_AS_RESOURCE:
- filename = silx.resources.resource_filename("gui/colormaps/%s.npy" % name)
- data = numpy.load(filename)
- lut = ListedColormap(data, name=name)
- _CMAPS[name] = lut
- return lut
- else:
- # matplotlib built-in
- return matplotlib.cm.get_cmap(name)
-
-
-@deprecated(since_version='0.10.0')
-def getScalarMappable(colormap, data=None):
- """Returns matplotlib ScalarMappable corresponding to colormap
-
- :param :class:`.Colormap` 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.getName() is not None:
- cmap = getColormap(colormap.getName())
-
- else: # No name, use custom colors
- if colormap.getColormapLUT() is None:
- raise ValueError(
- 'addImage: colormap no name nor list of colors.')
- colors = colormap.getColormapLUT()
- 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)
-
- vmin, vmax = colormap.getColormapRange(data)
- normalization = colormap.getNormalization()
- if normalization == colormap.LOGARITHM:
- norm = matplotlib.colors.LogNorm(vmin, vmax)
- elif normalization == colormap.LINEAR:
- norm = matplotlib.colors.Normalize(vmin, vmax)
- else:
- raise RuntimeError("Unsupported normalization: %s" % normalization)
-
- return matplotlib.cm.ScalarMappable(norm=norm, cmap=cmap)
-
-
-@deprecated(replacement='silx.colors.Colormap.applyToData',
- since_version='0.8.0')
-def applyColormapToData(data, colormap):
- """Apply a colormap to the data and returns the RGBA image
-
- This supports data of any dimensions (not only of dimension 2).
- The returned array will have one more dimension (with 4 entries)
- than the input data to store the RGBA channels
- corresponding to each bin in the array.
-
- :param numpy.ndarray data: The data to convert.
- :param :class:`.Colormap`: The colormap to apply
- """
- # 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 (colormap.getName() is None and
- colormap.getColormapLUT() is not None):
- colors = colormap.getColormapLUT()
- 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
- colormap.getNormalization() == 'linear' and
- not colormap.isAutoscale() and
- colormap.getVMin() == 0 and
- colormap.getVMax() == 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__)
-
- scalarMappable = getScalarMappable(colormap, data)
- rgbaImage = scalarMappable.to_rgba(data, bytes=True)
-
- return rgbaImage
-
-
-@deprecated(replacement='silx.colors.Colormap.getSupportedColormaps',
- since_version='0.10.0')
-def getSupportedColormaps():
- """Get the supported colormap names as a tuple of str.
- """
- colormaps = set(matplotlib.cm.datad.keys())
- colormaps.update(_AVAILABLE_AS_BUILTINS)
- colormaps.update(_AVAILABLE_AS_RESOURCE)
- return tuple(sorted(colormaps))
diff --git a/silx/gui/plot/matplotlib/__init__.py b/silx/gui/plot/matplotlib/__init__.py
deleted file mode 100644
index e787240..0000000
--- a/silx/gui/plot/matplotlib/__init__.py
+++ /dev/null
@@ -1,37 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "15/07/2020"
-
-from silx.utils.deprecation import deprecated_warning
-
-deprecated_warning(type_='module',
- name=__file__,
- replacement='silx.gui.utils.matplotlib',
- since_version='0.14.0')
-
-from silx.gui.utils.matplotlib import FigureCanvasQTAgg # noqa
diff --git a/silx/gui/plot/setup.py b/silx/gui/plot/setup.py
deleted file mode 100644
index e0b2c91..0000000
--- a/silx/gui/plot/setup.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "29/06/2017"
-
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('plot', parent_package, top_path)
- config.add_subpackage('_utils')
- config.add_subpackage('utils')
- config.add_subpackage('matplotlib')
- config.add_subpackage('stats')
- config.add_subpackage('backends')
- config.add_subpackage('backends.glutils')
- config.add_subpackage('items')
- config.add_subpackage('test')
- config.add_subpackage('tools')
- config.add_subpackage('tools.profile')
- config.add_subpackage('tools.test')
- config.add_subpackage('actions')
-
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
-
- setup(configuration=configuration)
diff --git a/silx/gui/plot/stats/__init__.py b/silx/gui/plot/stats/__init__.py
deleted file mode 100644
index 04a5327..0000000
--- a/silx/gui/plot/stats/__init__.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""
-"""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "07/03/2018"
-
-
-from .stats import *
diff --git a/silx/gui/plot/stats/stats.py b/silx/gui/plot/stats/stats.py
deleted file mode 100644
index a81f7bb..0000000
--- a/silx/gui/plot/stats/stats.py
+++ /dev/null
@@ -1,890 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2021 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 mechanism relative to stats calculation within a
-:class:`PlotWidget`.
-It also include the implementation of the statistics themselves.
-"""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "06/06/2018"
-
-
-from collections import OrderedDict
-from functools import lru_cache
-import logging
-
-import numpy
-import numpy.ma
-
-from .. import items
-from ..CurvesROIWidget import ROI
-from ..items.roi import RegionOfInterest
-
-from ....math.combo import min_max
-from silx.utils.proxy import docstring
-from ....utils.deprecation import deprecated
-
-logger = logging.getLogger(__name__)
-
-
-class Stats(OrderedDict):
- """Class to define a set of statistic relative to a dataset
- (image, curve...).
-
- The goal of this class is to avoid multiple recalculation of some
- basic operations such as filtering data area where the statistics has to
- be apply.
- Min and max are also stored because they can be used several time.
-
- :param List statslist: List of the :class:`Stat` object to be computed.
- """
- def __init__(self, statslist=None):
- OrderedDict.__init__(self)
- _statslist = statslist if not None else []
- if statslist is not None:
- for stat in _statslist:
- self.add(stat)
-
- def calculate(self, item, plot, onlimits, roi, data_changed=False,
- roi_changed=False):
- """
- Call all :class:`Stat` object registered and return the result of the
- computation.
-
- :param item: the item for which we want statistics
- :param plot: plot containing the item
- :param bool onlimits: True if we want to apply statistic only on
- visible data.
- :param roi: region of interest for statistic calculation. Incompatible
- with the `onlimits` option.
- :type roi: Union[None, :class:`~_RegionOfInterestBase`]
- :param bool data_changed: did the data changed since last calculation.
- :param bool roi_changed: did the associated roi (if any) has changed
- since last calculation.
- :return dict: dictionary with :class:`Stat` name as ket and result
- of the calculation as value
- """
- res = {}
- context = self._getContext(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
- for statName, stat in list(self.items()):
- if context.kind not in stat.compatibleKinds:
- logger.debug('kind %s not managed by statistic %s'
- % (context.kind, stat.name))
- res[statName] = None
- else:
- if roi_changed is True:
- context.clear_mask()
- if data_changed is True or roi_changed is True:
- # if data changed or mask changed
- context.clipData(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
- # init roi and data
- res[statName] = stat.calculate(context)
- return res
-
- def __setitem__(self, key, value):
- assert isinstance(value, StatBase)
- OrderedDict.__setitem__(self, key, value)
-
- def add(self, stat):
- """Add a :class:`Stat` to the set
-
- :param Stat stat: stat to add to the set
- """
- self.__setitem__(key=stat.name, value=stat)
-
- @staticmethod
- @lru_cache(maxsize=50)
- def _getContext(item, plot, onlimits, roi):
- context = None
- # Check for PlotWidget items
- if isinstance(item, items.Curve):
- context = _CurveContext(item, plot, onlimits, roi=roi)
- elif isinstance(item, items.ImageData):
- context = _ImageContext(item, plot, onlimits, roi=roi)
- elif isinstance(item, items.Scatter):
- context = _ScatterContext(item, plot, onlimits, roi=roi)
- elif isinstance(item, items.Histogram):
- context = _HistogramContext(item, plot, onlimits, roi=roi)
- else:
- # Check for SceneWidget items
- from ...plot3d import items as items3d # Lazy import
-
- if isinstance(item, (items3d.Scatter2D, items3d.Scatter3D)):
- context = _plot3DScatterContext(item, plot, onlimits,
- roi=roi)
- elif isinstance(item,
- (items3d.ImageData, items3d.ScalarField3D)):
- context = _plot3DArrayContext(item, plot, onlimits,
- roi=roi)
- if context is None:
- raise ValueError('Item type not managed')
- return context
-
-
-class _StatsContext(object):
- """
- The context is designed to be a simple buffer and avoid repetition of
- calculations that can appear during stats evaluation.
-
- .. warning:: this class gives access to the data to be used for computation
- . It deal with filtering data visible by the user on plot.
- The filtering is a simple data sub-sampling. No interpolation
- is made to fit data to boundaries.
-
- :param item: the item for which we want to compute the context
- :param str kind: the kind of the item
- :param plot: the plot containing the item
- :param bool onlimits: True if we want to apply statistic only on
- visible data.
- :param roi: Region of interest for computing the statistics.
- For now, incompatible with `onlimits` calculation
- :type roi: Union[None,:class:`_RegionOfInterestBase`]
- """
- def __init__(self, item, kind, plot, onlimits, roi):
- assert item
- assert plot
- assert type(onlimits) is bool
- self.kind = kind
- self.min = None
- self.max = None
- self.data = None
- self.roi = None
- self.onlimits = onlimits
-
- self.values = None
- """The array of data with limit filtering if any. Is a numpy.ma.array,
- meaning that it embed the mask applied by the roi if any"""
-
- self.axes = None
- """A list of array of position on each axis.
-
- If the signal is an array,
- then each axis has the length of that dimension,
- and the order is (z, y, x) (i.e., as the array shape).
- If the signal is not an array,
- then each axis has the same length as the signal,
- and the order is (x, y, z).
- """
-
- self.clipData(item, plot, onlimits, roi=roi)
-
- def clear_mask(self):
- """
- Remove the mask to force recomputation of it on next iteration
- :return:
- """
- raise NotImplementedError()
-
- @property
- def mask(self):
- if self.values is not None:
- assert isinstance(self.values, numpy.ma.MaskedArray)
- return self.values.mask
- else:
- return None
-
- @property
- def is_mask_valid(self, **kwargs):
- """Return if the mask is valid for the data or need to be recomputed"""
- raise NotImplementedError("Base class")
-
- def _set_mask_validity(self, **kwargs):
- """User to set some values that allows to define the mask properties
- and boundaries"""
- raise NotImplementedError("Base class")
-
- def clipData(self, item, plot, onlimits, roi):
- """Clip the data to the current mask to have accurate statistics
-
- Function called before computing each statistics associated to this
- context. It will insure the context for the (item, plot, onlimits, roi)
- is created.
-
- :param item: item for which we want statistics
- :param plot: plot containing the statistics
- :param bool onlimits: True if we want to apply statistic only on
- visible data.
- :param roi: Region of interest for computing the statistics.
- For now, incompatible with `onlimits` calculation
- :type roi: Union[None,:class:`_RegionOfInterestBase`]
- """
- raise NotImplementedError("Base class")
-
- @deprecated(reason="context are now stored and keep during stats life."
- "So this function will be called only once",
- replacement="clipData", since_version="0.13.0")
- def createContext(self, item, plot, onlimits, roi):
- return self.clipData(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
-
- def isStructuredData(self):
- """Returns True if data as an array-like structure.
-
- :rtype: bool
- """
- if self.values is None or self.axes is None:
- return False
-
- if numpy.prod([len(axis) for axis in self.axes]) == self.values.size:
- return True
- else:
- # Make sure there is the right number of value in axes
- for axis in self.axes:
- assert len(axis) == self.values.size
- return False
-
- def isScalarData(self):
- """Returns True if data is a scalar.
-
- :rtype: bool
- """
- if self.values is None or self.axes is None:
- return False
- if self.isStructuredData():
- return len(self.axes) == self.values.ndim
- else:
- return self.values.ndim == 1
-
- def _checkContextInputs(self, item, plot, onlimits, roi):
- if roi is not None and onlimits is True:
- raise ValueError('Stats context is unable to manage both a ROI'
- 'and the `onlimits` option')
-
-
-class _ScatterCurveHistoMixInContext(_StatsContext):
- def __init__(self, kind, item, plot, onlimits, roi):
- self.clear_mask()
- _StatsContext.__init__(self, item=item, kind=kind,
- plot=plot, onlimits=onlimits, roi=roi)
-
- def _set_mask_validity(self, onlimits, from_, to_):
- self._onlimits = onlimits
- self._from_ = from_
- self._to_ = to_
-
- def clear_mask(self):
- self._onlimits = None
- self._from_ = None
- self._to_ = None
-
- def is_mask_valid(self, onlimits, from_, to_):
- return (onlimits == self.onlimits and from_ == self._from_ and
- to_ == self._to_)
-
-
-class _CurveContext(_ScatterCurveHistoMixInContext):
- """
- StatsContext for :class:`Curve`
-
- :param item: the item for which we want to compute the context
- :param plot: the plot containing the item
- :param bool onlimits: True if we want to apply statistic only on
- visible data.
- :param roi: Region of interest for computing the statistics.
- For now, incompatible with `onlinits` calculation
- :type roi: Union[None, :class:`ROI`]
- """
- def __init__(self, item, plot, onlimits, roi):
- _ScatterCurveHistoMixInContext.__init__(self, kind='curve', item=item,
- plot=plot, onlimits=onlimits,
- roi=roi)
-
- @docstring(_StatsContext)
- def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
- self.roi = roi
- self.onlimits = onlimits
- xData, yData = item.getData(copy=True)[0:2]
-
- if onlimits:
- minX, maxX = plot.getXAxis().getLimits()
- if self.is_mask_valid(onlimits=onlimits, from_=minX, to_=maxX):
- mask = self.mask
- else:
- mask = (minX <= xData) & (xData <= maxX)
- mask = mask == 0
- self._set_mask_validity(onlimits=onlimits, from_=minX, to_=maxX)
- elif roi:
- minX, maxX = roi.getFrom(), roi.getTo()
- if self.is_mask_valid(onlimits=onlimits, from_=minX, to_=maxX):
- mask = self.mask
- else:
- mask = (minX <= xData) & (xData <= maxX)
- mask = mask == 0
- self._set_mask_validity(onlimits=onlimits, from_=minX, to_=maxX)
- else:
- mask = numpy.zeros_like(yData)
-
- mask = mask.astype(numpy.uint32)
- self.xData = xData
- self.yData = yData
- self.values = numpy.ma.array(yData, mask=mask)
- unmasked_data = self.values.compressed()
- if len(unmasked_data) > 0:
- self.min, self.max = min_max(unmasked_data)
- else:
- self.min, self.max = None, None
- self.data = (xData, yData)
- self.axes = (xData,)
-
- def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
- if roi is not None and not isinstance(roi, ROI):
- raise TypeError('curve `context` can ony manage 1D roi')
-
-
-class _HistogramContext(_ScatterCurveHistoMixInContext):
- """
- StatsContext for :class:`Histogram`
-
- :param item: the item for which we want to compute the context
- :param plot: the plot containing the item
- :param bool onlimits: True if we want to apply statistic only on
- visible data.
- :param roi: Region of interest for computing the statistics.
- For now, incompatible with `onlinits` calculation
- :type roi: Union[None, :class:`ROI`]
- """
- def __init__(self, item, plot, onlimits, roi):
- _ScatterCurveHistoMixInContext.__init__(self, kind='histogram',
- item=item, plot=plot,
- onlimits=onlimits, roi=roi)
-
- @docstring(_StatsContext)
- def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
- yData, edges = item.getData(copy=True)[0:2]
- xData = item._revertComputeEdges(x=edges, histogramType=item.getAlignment())
-
- if onlimits:
- minX, maxX = plot.getXAxis().getLimits()
- if self.is_mask_valid(onlimits=onlimits, from_=minX, to_=maxX):
- mask = self.mask
- else:
- mask = (minX <= xData) & (xData <= maxX)
- mask = mask == 0
- self._set_mask_validity(onlimits=onlimits, from_=minX, to_=maxX)
- elif roi:
- if self.is_mask_valid(onlimits=onlimits, from_=roi._fromdata, to_=roi._todata):
- mask = self.mask
- else:
- mask = (roi._fromdata <= xData) & (xData <= roi._todata)
- mask = mask == 0
- self._set_mask_validity(onlimits=onlimits, from_=roi._fromdata,
- to_=roi._todata)
- else:
- mask = numpy.zeros_like(yData)
- mask = mask.astype(numpy.uint32)
- self.xData = xData
- self.yData = yData
- self.values = numpy.ma.array(yData, mask=(mask))
- unmasked_data = self.values.compressed()
- if len(unmasked_data) > 0:
- self.min, self.max = min_max(unmasked_data)
- else:
- self.min, self.max = None, None
- self.data = (self.xData, self.yData)
- self.axes = (self.xData,)
-
- def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
-
- if roi is not None and not isinstance(roi, ROI):
- raise TypeError('curve `context` can ony manage 1D roi')
-
-
-class _ScatterContext(_ScatterCurveHistoMixInContext):
- """StatsContext scatter plots.
-
- It supports :class:`~silx.gui.plot.items.Scatter`.
-
- :param item: the item for which we want to compute the context
- :param plot: the plot containing the item
- :param bool onlimits: True if we want to apply statistic only on
- visible data.
- :param roi: Region of interest for computing the statistics.
- For now, incompatible with `onlinits` calculation
- :type roi: Union[None, :class:`ROI`]
- """
- def __init__(self, item, plot, onlimits, roi):
- _ScatterCurveHistoMixInContext.__init__(self, kind='scatter',
- item=item, plot=plot,
- onlimits=onlimits, roi=roi)
-
- @docstring(_ScatterCurveHistoMixInContext)
- def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
- valueData = item.getValueData(copy=True)
- xData = item.getXData(copy=True)
- yData = item.getYData(copy=True)
-
- if onlimits:
- minX, maxX = plot.getXAxis().getLimits()
- minY, maxY = plot.getYAxis().getLimits()
-
- # filter on X axis
- valueData = valueData[(minX <= xData) & (xData <= maxX)]
- yData = yData[(minX <= xData) & (xData <= maxX)]
- xData = xData[(minX <= xData) & (xData <= maxX)]
- # filter on Y axis
- valueData = valueData[(minY <= yData) & (yData <= maxY)]
- xData = xData[(minY <= yData) & (yData <= maxY)]
- yData = yData[(minY <= yData) & (yData <= maxY)]
-
- if roi:
- if self.is_mask_valid(onlimits=onlimits, from_=roi.getFrom(),
- to_=roi.getTo()):
- mask = self.mask
- else:
- mask = (xData < roi.getFrom()) | (xData > roi.getTo())
- else:
- mask = numpy.zeros_like(xData)
-
- self.data = (xData, yData, valueData)
- self.values = numpy.ma.array(valueData, mask=mask)
- self.axes = (xData, yData)
-
- unmasked_values = self.values.compressed()
- if len(unmasked_values) > 0:
- self.min, self.max = min_max(unmasked_values)
- else:
- self.min, self.max = None, None
-
- def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
-
- if roi is not None and not isinstance(roi, ROI):
- raise TypeError('curve `context` can ony manage 1D roi')
-
-
-class _ImageContext(_StatsContext):
- """StatsContext for images.
-
- It supports :class:`~silx.gui.plot.items.ImageData`.
-
- :warning: behaviour of scale images: now the statistics are computed on
- the entire data array (there is no sampling in the array or
- interpolation regarding the scale).
- This also mean that the result can differ from what is displayed.
- But I guess there is no perfect behaviour.
-
- :warning: `isIn` functions for image context: for now have basically a
- binary approach, the pixel is in a roi or not. To have a fully
- 'correct behaviour' we should add a weight on stats calculation
- to moderate the pixel value.
-
- :param item: the item for which we want to compute the context
- :param plot: the plot containing the item
- :param bool onlimits: True if we want to apply statistic only on
- visible data.
- :param roi: Region of interest for computing the statistics.
- For now, incompatible with `onlinits` calculation
- :type roi: Union[None, :class:`ROI`]
- """
- def __init__(self, item, plot, onlimits, roi):
- self.clear_mask()
- _StatsContext.__init__(self, kind='image', item=item,
- plot=plot, onlimits=onlimits, roi=roi)
-
- def _set_mask_validity(self, xmin: float, xmax: float, ymin: float, ymax
- : float):
- self._mask_x_min = xmin
- self._mask_x_max = xmax
- self._mask_y_min = ymin
- self._mask_y_max = ymax
-
- def clear_mask(self):
- self._mask_x_min = None
- self._mask_x_max = None
- self._mask_y_min = None
- self._mask_y_max = None
-
- def is_mask_valid(self, xmin, xmax, ymin, ymax):
- return (xmin == self._mask_x_min and xmax == self._mask_x_max and
- ymin == self._mask_y_min and ymax == self._mask_y_max)
-
- @docstring(_StatsContext)
- def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
- self.origin = item.getOrigin()
- self.scale = item.getScale()
-
- self.data = item.getData(copy=True)
- mask = numpy.zeros_like(self.data)
- """mask use to know of the stat should be count in or not"""
-
- if onlimits:
- minX, maxX = plot.getXAxis().getLimits()
- minY, maxY = plot.getYAxis().getLimits()
-
- XMinBound = int((minX - self.origin[0]) / self.scale[0])
- YMinBound = int((minY - self.origin[1]) / self.scale[1])
- XMaxBound = int((maxX - self.origin[0]) / self.scale[0])
- YMaxBound = int((maxY - self.origin[1]) / self.scale[1])
-
- XMinBound = max(XMinBound, 0)
- YMinBound = max(YMinBound, 0)
-
- if onlimits:
- if XMaxBound <= XMinBound or YMaxBound <= YMinBound:
- self.data = None
- else:
- self.data = self.data[YMinBound:YMaxBound + 1,
- XMinBound:XMaxBound + 1]
- mask = numpy.zeros_like(self.data)
- elif roi:
- minX, maxX = 0, self.data.shape[1]
- minY, maxY = 0, self.data.shape[0]
-
- XMinBound = max(minX, 0)
- YMinBound = max(minY, 0)
- XMaxBound = min(maxX, self.data.shape[1])
- YMaxBound = min(maxY, self.data.shape[0])
-
- if self.is_mask_valid(xmin=XMinBound, xmax=XMaxBound,
- ymin=YMinBound, ymax=YMaxBound):
- mask = self.mask
- else:
- for x in range(XMinBound, XMaxBound):
- for y in range(YMinBound, YMaxBound):
- _x = (x * self.scale[0]) + self.origin[0]
- _y = (y * self.scale[1]) + self.origin[1]
- mask[y, x] = not roi.contains((_x, _y))
- self._set_mask_validity(xmin=XMinBound, xmax=XMaxBound,
- ymin=YMinBound, ymax=YMaxBound)
- self.values = numpy.ma.array(self.data, mask=mask)
- if self.values.compressed().size > 0:
- self.min, self.max = min_max(self.values.compressed())
- else:
- self.min, self.max = None, None
-
- if self.values is not None:
- self.axes = (self.origin[1] + self.scale[1] * numpy.arange(self.data.shape[0]),
- self.origin[0] + self.scale[0] * numpy.arange(self.data.shape[1]))
-
- def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
-
- if roi is not None and not isinstance(roi, RegionOfInterest):
- raise TypeError('curve `context` can ony manage 2D roi')
-
-
-class _plot3DScatterContext(_StatsContext):
- """StatsContext for 3D scatter plots.
-
- It supports :class:`~silx.gui.plot3d.items.Scatter2D` and
- :class:`~silx.gui.plot3d.items.Scatter3D`.
-
- :param item: the item for which we want to compute the context
- :param plot: the plot containing the item
- :param bool onlimits: True if we want to apply statistic only on
- visible data.
- :param roi: Region of interest for computing the statistics.
- For now, incompatible with `onlinits` calculation
- :type roi: Union[None, :class:`ROI`]
- """
- def __init__(self, item, plot, onlimits, roi):
- _StatsContext.__init__(self, kind='scatter', item=item, plot=plot,
- onlimits=onlimits, roi=roi)
-
- @docstring(_StatsContext)
- def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
- if onlimits:
- raise RuntimeError("Unsupported plot %s" % str(plot))
- values = item.getValueData(copy=False)
- if roi:
- logger.warning("Roi are unsupported on volume for now")
- mask = numpy.zeros_like(values)
- else:
- mask = numpy.zeros_like(values)
-
- if values is not None and len(values) > 0:
- self.values = values
- axes = [item.getXData(copy=False), item.getYData(copy=False)]
- if self.values.ndim == 3:
- axes.append(item.getZData(copy=False))
- self.axes = tuple(axes)
- self.min, self.max = min_max(self.values)
- self.values = numpy.ma.array(self.values, mask=mask)
- else:
- self.values = None
- self.axes = None
- self.min, self.max = None, None
-
- def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
-
- if roi is not None and not isinstance(roi, RegionOfInterest):
- raise TypeError('curve `context` can ony manage 2D roi')
-
-
-class _plot3DArrayContext(_StatsContext):
- """StatsContext for 3D scalar field and data image.
-
- It supports :class:`~silx.gui.plot3d.items.ScalarField3D` and
- :class:`~silx.gui.plot3d.items.ImageData`.
-
- :param item: the item for which we want to compute the context
- :param plot: the plot containing the item
- :param bool onlimits: True if we want to apply statistic only on
- visible data.
- :param roi: Region of interest for computing the statistics.
- For now, incompatible with `onlinits` calculation
- :type roi: Union[None, :class:`ROI`]
- """
- def __init__(self, item, plot, onlimits, roi):
- _StatsContext.__init__(self, kind='image', item=item, plot=plot,
- onlimits=onlimits, roi=roi)
-
- @docstring(_StatsContext)
- def clipData(self, item, plot, onlimits, roi):
- self._checkContextInputs(item=item, plot=plot, onlimits=onlimits,
- roi=roi)
- if onlimits:
- raise RuntimeError("Unsupported plot %s" % str(plot))
-
- values = item.getData(copy=False)
- if roi:
- logger.warning("Roi are unsuported on volume for now")
- mask = numpy.zeros_like(values)
- else:
- mask = numpy.zeros_like(values)
-
- if values is not None and len(values) > 0:
- self.values = values
- self.axes = tuple([numpy.arange(size) for size in self.values.shape])
- self.min, self.max = min_max(self.values)
- self.values = numpy.ma.array(self.values, mask=mask)
- else:
- self.values = None
- self.axes = None
- self.min, self.max = None, None
-
- def _checkContextInputs(self, item, plot, onlimits, roi):
- _StatsContext._checkContextInputs(self, item=item, plot=plot,
- onlimits=onlimits, roi=roi)
-
- if roi is not None and not isinstance(roi, RegionOfInterest):
- raise TypeError('curve `context` can ony manage 2D roi')
-
-
-BASIC_COMPATIBLE_KINDS = 'curve', 'image', 'scatter', 'histogram'
-
-
-class StatBase(object):
- """
- Base class for defining a statistic.
-
- :param str name: the name of the statistic. Must be unique.
- :param List[str] compatibleKinds:
- The kind of items (curve, scatter...) for which the statistic apply.
- """
- def __init__(self, name, compatibleKinds=BASIC_COMPATIBLE_KINDS, description=None):
- self.name = name
- self.compatibleKinds = compatibleKinds
- self.description = description
-
- def calculate(self, context):
- """
- compute the statistic for the given :class:`StatsContext`
-
- :param _StatsContext context:
- :return dict: key is stat name, statistic computed is the dict value
- """
- raise NotImplementedError('Base class')
-
- def getToolTip(self, kind):
- """
- If necessary add a tooltip for a stat kind
-
- :param str kind: the kind of item the statistic is compute for.
- :return: tooltip or None if no tooltip
- """
- return None
-
-
-class Stat(StatBase):
- """
- Create a StatBase class based on a function pointer.
-
- :param str name: name of the statistic. Used as id
- :param fct: function which should have as unique mandatory parameter the
- data. Should be able to adapt to all `kinds` defined as
- compatible
- :param tuple kinds: the compatible item kinds of the function (curve,
- image...)
- """
- def __init__(self, name, fct, kinds=BASIC_COMPATIBLE_KINDS):
- StatBase.__init__(self, name, kinds)
- self._fct = fct
-
- @docstring(StatBase)
- def calculate(self, context):
- if context.values is not None:
- if context.kind in self.compatibleKinds:
- return self._fct(context.values)
- else:
- raise ValueError('Kind %s not managed by %s'
- '' % (context.kind, self.name))
- else:
- return None
-
-
-class StatMin(StatBase):
- """Compute the minimal value on data"""
- def __init__(self):
- StatBase.__init__(self, name='min')
-
- @docstring(StatBase)
- def calculate(self, context):
- return context.min
-
-
-class StatMax(StatBase):
- """Compute the maximal value on data"""
- def __init__(self):
- StatBase.__init__(self, name='max')
-
- @docstring(StatBase)
- def calculate(self, context):
- return context.max
-
-
-class StatDelta(StatBase):
- """Compute the delta between minimal and maximal on data"""
- def __init__(self):
- StatBase.__init__(self, name='delta')
-
- @docstring(StatBase)
- def calculate(self, context):
- return context.max - context.min
-
-
-class _StatCoord(StatBase):
- """Base class for argmin and argmax stats"""
-
- def _indexToCoordinates(self, context, index):
- """Returns the coordinates of data point at given index
-
- If data is an array, coordinates are in reverse order from data shape.
-
- :param _StatsContext context:
- :param int index: Index in the flattened data array
- :rtype: List[int]
- """
-
- axes = context.axes
-
- if context.isStructuredData() or context.roi:
- coordinates = []
- for axis in reversed(axes):
- coordinates.append(axis[index % len(axis)])
- index = index // len(axis)
- return tuple(coordinates)
- else:
- return tuple(axis[index] for axis in axes)
-
-
-class StatCoordMin(_StatCoord):
- """Compute the coordinates of the first minimum value of the data"""
- def __init__(self):
- _StatCoord.__init__(self, name='coords min')
-
- @docstring(StatBase)
- def calculate(self, context):
- if context.values is None or not context.isScalarData():
- return None
-
- index = context.values.argmin()
- return self._indexToCoordinates(context, index)
-
- @docstring(StatBase)
- def getToolTip(self, kind):
- return "Coordinates of the first minimum value of the data"
-
-
-class StatCoordMax(_StatCoord):
- """Compute the coordinates of the first maximum value of the data"""
- def __init__(self):
- _StatCoord.__init__(self, name='coords max')
-
- @docstring(StatBase)
- def calculate(self, context):
- if context.values is None or not context.isScalarData():
- return None
-
- # TODO: the values should be a mask array by default, will be simpler
- # if possible
- index = context.values.argmax()
- return self._indexToCoordinates(context, index)
-
- @docstring(StatBase)
- def getToolTip(self, kind):
- return "Coordinates of the first maximum value of the data"
-
-
-class StatCOM(StatBase):
- """Compute data center of mass"""
- def __init__(self):
- StatBase.__init__(self, name='COM', description='Center of mass')
-
- @docstring(StatBase)
- def calculate(self, context):
- if context.values is None or not context.isScalarData():
- return None
-
- values = numpy.ma.array(context.values, mask=context.mask, dtype=numpy.float64)
- sum_ = numpy.sum(values)
- if sum_ == 0.:
- return (numpy.nan,) * len(context.axes)
-
- if context.isStructuredData():
- centerofmass = []
- for index, axis in enumerate(context.axes):
- axes = tuple([i for i in range(len(context.axes)) if i != index])
- centerofmass.append(
- numpy.sum(axis * numpy.sum(values, axis=axes)) / sum_)
- return tuple(reversed(centerofmass))
- else:
- return tuple(
- numpy.sum(axis * values) / sum_ for axis in context.axes)
-
- @docstring(StatBase)
- def getToolTip(self, kind):
- return "Compute the center of mass of the dataset"
diff --git a/silx/gui/plot/stats/statshandler.py b/silx/gui/plot/stats/statshandler.py
deleted file mode 100644
index 17578d8..0000000
--- a/silx/gui/plot/stats/statshandler.py
+++ /dev/null
@@ -1,202 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2019 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 containts the classes relative to the management of statistics
-display.
-"""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "05/06/2018"
-
-
-import logging
-
-from silx.gui import qt
-from silx.gui.plot import stats as statsmdl
-
-logger = logging.getLogger(__name__)
-
-
-class _FloatItem(qt.QTableWidgetItem):
- """Simple QTableWidgetItem allowing ordering on floats"""
-
- def __init__(self, type=qt.QTableWidgetItem.Type):
- qt.QTableWidgetItem.__init__(self, type=type)
-
- def __lt__(self, other):
- self_values = self.text().lstrip('(').rstrip(')').split(',')
- other_values = other.text().lstrip('(').rstrip(')').split(',')
- for self_value, other_value in zip(self_values, other_values):
- f_self_value = float(self_value)
- f_other_value = float(other_value)
- if f_self_value != f_other_value:
- return f_self_value < f_other_value
- return False
-
-
-class StatFormatter(object):
- """
- Class used to apply format on :class:`Stat`
-
- :param formatter: the formatter. Defined as str.format()
- :param qItemClass: the class inheriting from :class:`QTableWidgetItem`
- which will be used to display the result of the
- statistic computation.
- """
- DEFAULT_FORMATTER = '{0:.3f}'
-
- def __init__(self, formatter=DEFAULT_FORMATTER, qItemClass=_FloatItem):
- self.formatter = formatter
- self.tabWidgetItemClass = qItemClass
-
- def format(self, val):
- if self.formatter is None or val is None:
- return str(val)
- else:
- return self.formatter.format(val)
-
-
-class StatsHandler(object):
- """
- Give
- create:
-
- * Stats object which will manage the statistic computation
- * Associate formatter and :class:`Stat`
-
- :param statFormatters: Stat and optional formatter.
- If elements are given as a tuple, elements
- should be (:class:`Stat`, formatter).
- Otherwise should be :class:`Stat` elements.
- :rtype: List or tuple
- """
-
- def __init__(self, statFormatters):
- self.stats = statsmdl.Stats()
- self.formatters = {}
- for elmt in statFormatters:
- stat, formatter = self._processStatArgument(elmt)
- self.add(stat=stat, formatter=formatter)
-
- @staticmethod
- def _processStatArgument(arg):
- """Process an element of the init arguments
-
- :param arg: The argument to process
- :return: Corresponding (StatBase, StatFormatter)
- """
- stat, formatter = None, None
-
- if isinstance(arg, statsmdl.StatBase):
- stat = arg
- else:
- assert len(arg) > 0
- if isinstance(arg[0], statsmdl.StatBase):
- stat = arg[0]
- if len(arg) > 2:
- raise ValueError('To many argument with %s. At most one '
- 'argument can be associated with the '
- 'BaseStat (the `StatFormatter`')
- if len(arg) == 2:
- assert arg[1] is None or isinstance(arg[1], (StatFormatter, str))
- formatter = arg[1]
- else:
- if isinstance(arg[0], tuple):
- if len(arg) > 1:
- formatter = arg[1]
- arg = arg[0]
-
- if type(arg[0]) is not str:
- raise ValueError('first element of the tuple should be a string'
- ' or a StatBase instance')
- if len(arg) == 1:
- raise ValueError('A function should be associated with the'
- 'stat name')
- if len(arg) > 3:
- raise ValueError('Two much argument given for defining statistic.'
- 'Take at most three arguments (name, function, '
- 'kinds)')
- if len(arg) == 2:
- stat = statsmdl.Stat(name=arg[0], fct=arg[1])
- else:
- stat = statsmdl.Stat(name=arg[0], fct=arg[1], kinds=arg[2])
-
- return stat, formatter
-
- def add(self, stat, formatter=None):
- """Add a stat to the list.
-
- :param StatBase stat:
- :param Union[None,StatFormatter] formatter:
- """
- assert isinstance(stat, statsmdl.StatBase)
- self.stats.add(stat)
- _formatter = formatter
- if type(_formatter) is str:
- _formatter = StatFormatter(formatter=_formatter)
- self.formatters[stat.name] = _formatter
-
- def format(self, name, val):
- """Apply the format for the `name` statistic and the given value
-
- :param str name: the name of the associated statistic
- :param val: value before formatting
- :return: formatted value
- """
- if name not in self.formatters:
- logger.warning("statistic %s haven't been registred" % name)
- return val
- else:
- if self.formatters[name] is None:
- return str(val)
- else:
- if isinstance(val, (tuple, list)):
- res = []
- [res.append(self.formatters[name].format(_val)) for _val in val]
- return ', '.join(res)
- else:
- return self.formatters[name].format(val)
-
- def calculate(self, item, plot, onlimits, roi=None, data_changed=False,
- roi_changed=False):
- """
- compute all statistic registered and return the list of formatted
- statistics result.
-
- :param item: item for which we want to compute statistics
- :param plot: plot containing the item
- :param onlimits: True if we want to compute statistics on visible data
- only
- :type: bool
- :param roi: region of interest for statistic calculation
- :type: Union[None,:class:`_RegionOfInterestBase`]
- :return: list of formatted statistics (as str)
- :rtype: dict
- """
- res = self.stats.calculate(item, plot, onlimits, roi,
- data_changed=data_changed, roi_changed=roi_changed)
- for resName, resValue in list(res.items()):
- res[resName] = self.format(resName, res[resName])
- return res
diff --git a/silx/gui/plot/test/__init__.py b/silx/gui/plot/test/__init__.py
deleted file mode 100644
index dfb7c2e..0000000
--- a/silx/gui/plot/test/__init__.py
+++ /dev/null
@@ -1,92 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "23/07/2018"
-
-
-import unittest
-
-from .._utils import test
-from . import testColorBar
-from . import testCurvesROIWidget
-from . import testStats
-from . import testAlphaSlider
-from . import testInteraction
-from . import testLegendSelector
-from . import testMaskToolsWidget
-from . import testScatterMaskToolsWidget
-from . import testPlotInteraction
-from . import testPlotWidgetNoBackend
-from . import testPlotWidget
-from . import testPlotWindow
-from . import testStackView
-from . import testImageStack
-from . import testItem
-from . import testUtilsAxis
-from . import testLimitConstraints
-from . import testComplexImageView
-from . import testImageView
-from . import testSaveAction
-from . import testScatterView
-from . import testPixelIntensityHistoAction
-from . import testCompareImages
-from . import testRoiStatsWidget
-
-
-def suite():
- # Lazy-loading to avoid cyclic reference
- from ..tools import test as testTools
-
- test_suite = unittest.TestSuite()
- test_suite.addTests(
- [test.suite(),
- testTools.suite(),
- testColorBar.suite(),
- testCurvesROIWidget.suite(),
- testStats.suite(),
- testAlphaSlider.suite(),
- testInteraction.suite(),
- testLegendSelector.suite(),
- testMaskToolsWidget.suite(),
- testScatterMaskToolsWidget.suite(),
- testPlotInteraction.suite(),
- testPlotWidgetNoBackend.suite(),
- testPlotWidget.suite(),
- testPlotWindow.suite(),
- testStackView.suite(),
- testImageStack.suite(),
- testItem.suite(),
- testUtilsAxis.suite(),
- testLimitConstraints.suite(),
- testComplexImageView.suite(),
- testImageView.suite(),
- testSaveAction.suite(),
- testScatterView.suite(),
- testPixelIntensityHistoAction.suite(),
- testCompareImages.suite(),
- testRoiStatsWidget.suite(),
- ])
- return test_suite
diff --git a/silx/gui/plot/test/testAlphaSlider.py b/silx/gui/plot/test/testAlphaSlider.py
deleted file mode 100644
index 01e6969..0000000
--- a/silx/gui/plot/test/testAlphaSlider.py
+++ /dev/null
@@ -1,218 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2019 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.
-#
-# ###########################################################################*/
-"""Tests for ImageAlphaSlider"""
-
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "28/03/2017"
-
-import numpy
-import unittest
-
-from silx.gui import qt
-from silx.gui.utils.testutils import TestCaseQt
-from silx.gui.plot import PlotWidget
-from silx.gui.plot import AlphaSlider
-
-
-class TestActiveImageAlphaSlider(TestCaseQt):
- def setUp(self):
- super(TestActiveImageAlphaSlider, self).setUp()
- self.plot = PlotWidget()
- self.aslider = AlphaSlider.ActiveImageAlphaSlider(plot=self.plot)
- self.aslider.setOrientation(qt.Qt.Horizontal)
-
- toolbar = qt.QToolBar("plot", self.plot)
- toolbar.addWidget(self.aslider)
- self.plot.addToolBar(toolbar)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- self.mouseMove(self.plot) # Move to center
- self.qapp.processEvents()
-
- def tearDown(self):
- self.qapp.processEvents()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- del self.aslider
-
- super(TestActiveImageAlphaSlider, self).tearDown()
-
- def testWidgetEnabled(self):
- # no active image initially, slider must be deactivate
- self.assertFalse(self.aslider.isEnabled())
-
- self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]))
- # now we have an active image
- self.assertTrue(self.aslider.isEnabled())
-
- self.plot.setActiveImage(None)
- self.assertFalse(self.aslider.isEnabled())
-
- def testGetImage(self):
- self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]))
- self.assertEqual(self.plot.getActiveImage(),
- self.aslider.getItem())
-
- self.plot.addImage(numpy.array([[0, 1, 3], [2, 4, 6]]), legend="2")
- self.plot.setActiveImage("2")
- self.assertEqual(self.plot.getImage("2"),
- self.aslider.getItem())
-
- def testGetAlpha(self):
- self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1")
- self.aslider.setValue(137)
- self.assertAlmostEqual(self.aslider.getAlpha(),
- 137. / 255)
-
-
-class TestNamedImageAlphaSlider(TestCaseQt):
- def setUp(self):
- super(TestNamedImageAlphaSlider, self).setUp()
- self.plot = PlotWidget()
- self.aslider = AlphaSlider.NamedImageAlphaSlider(plot=self.plot)
- self.aslider.setOrientation(qt.Qt.Horizontal)
-
- toolbar = qt.QToolBar("plot", self.plot)
- toolbar.addWidget(self.aslider)
- self.plot.addToolBar(toolbar)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- self.mouseMove(self.plot) # Move to center
- self.qapp.processEvents()
-
- def tearDown(self):
- self.qapp.processEvents()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- del self.aslider
-
- super(TestNamedImageAlphaSlider, self).tearDown()
-
- def testWidgetEnabled(self):
- # no image set initially, slider must be deactivate
- self.assertFalse(self.aslider.isEnabled())
-
- self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1")
- self.aslider.setLegend("1")
- # now we have an image set
- self.assertTrue(self.aslider.isEnabled())
-
- def testGetImage(self):
- self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1")
- self.plot.addImage(numpy.array([[0, 1, 3], [2, 4, 6]]), legend="2")
- self.aslider.setLegend("1")
- self.assertEqual(self.plot.getImage("1"),
- self.aslider.getItem())
-
- self.aslider.setLegend("2")
- self.assertEqual(self.plot.getImage("2"),
- self.aslider.getItem())
-
- def testGetAlpha(self):
- self.plot.addImage(numpy.array([[0, 1, 2], [3, 4, 5]]), legend="1")
- self.aslider.setLegend("1")
- self.aslider.setValue(128)
- self.assertAlmostEqual(self.aslider.getAlpha(),
- 128. / 255)
-
-
-class TestNamedScatterAlphaSlider(TestCaseQt):
- def setUp(self):
- super(TestNamedScatterAlphaSlider, self).setUp()
- self.plot = PlotWidget()
- self.aslider = AlphaSlider.NamedScatterAlphaSlider(plot=self.plot)
- self.aslider.setOrientation(qt.Qt.Horizontal)
-
- toolbar = qt.QToolBar("plot", self.plot)
- toolbar.addWidget(self.aslider)
- self.plot.addToolBar(toolbar)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- self.mouseMove(self.plot) # Move to center
- self.qapp.processEvents()
-
- def tearDown(self):
- self.qapp.processEvents()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- del self.aslider
-
- super(TestNamedScatterAlphaSlider, self).tearDown()
-
- def testWidgetEnabled(self):
- # no Scatter set initially, slider must be deactivate
- self.assertFalse(self.aslider.isEnabled())
-
- self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7],
- legend="1")
- self.aslider.setLegend("1")
- # now we have an image set
- self.assertTrue(self.aslider.isEnabled())
-
- def testGetScatter(self):
- self.plot.addScatter([0, 1, 2], [2, 3, 4], [5, 6, 7],
- legend="1")
- self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70],
- legend="2")
- self.aslider.setLegend("1")
- self.assertEqual(self.plot.getScatter("1"),
- self.aslider.getItem())
-
- self.aslider.setLegend("2")
- self.assertEqual(self.plot.getScatter("2"),
- self.aslider.getItem())
-
- def testGetAlpha(self):
- self.plot.addScatter([0, 10, 20], [20, 30, 40], [50, 60, 70],
- legend="1")
- self.aslider.setLegend("1")
- self.aslider.setValue(128)
- self.assertAlmostEqual(self.aslider.getAlpha(),
- 128. / 255)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- # test_suite.addTest(positionInfoTestSuite)
- for testClass in (TestActiveImageAlphaSlider, TestNamedImageAlphaSlider,
- TestNamedScatterAlphaSlider):
- test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(
- testClass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testColorBar.py b/silx/gui/plot/test/testColorBar.py
deleted file mode 100644
index a6f141c..0000000
--- a/silx/gui/plot/test/testColorBar.py
+++ /dev/null
@@ -1,354 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Basic tests for ColorBar featues and sub widgets of Colorbar module"""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-import unittest
-from silx.gui.utils.testutils import TestCaseQt
-from silx.gui.plot.ColorBar import _ColorScale
-from silx.gui.plot.ColorBar import ColorBarWidget
-from silx.gui.colors import Colormap
-from silx.gui import colors
-from silx.gui.plot import Plot2D
-from silx.gui import qt
-import numpy
-
-
-class TestColorScale(TestCaseQt):
- """Test that interaction with the colorScale is correct"""
- def setUp(self):
- super(TestColorScale, self).setUp()
- self.colorScaleWidget = _ColorScale(colormap=None, parent=None)
- self.colorScaleWidget.show()
- self.qWaitForWindowExposed(self.colorScaleWidget)
-
- def tearDown(self):
- self.qapp.processEvents()
- self.colorScaleWidget.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.colorScaleWidget.close()
- del self.colorScaleWidget
- super(TestColorScale, self).tearDown()
-
- def testNoColormap(self):
- """Test _ColorScale without a colormap"""
- colormap = self.colorScaleWidget.getColormap()
- self.assertIsNone(colormap)
-
- def testRelativePositionLinear(self):
- self.colorMapLin1 = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=0.0,
- vmax=1.0)
- self.colorScaleWidget.setColormap(self.colorMapLin1)
-
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.25) == 0.25)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.5) == 0.5)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(1.0) == 1.0)
-
- self.colorMapLin2 = Colormap(name='viridis',
- normalization=Colormap.LINEAR,
- vmin=-10,
- vmax=0)
- self.colorScaleWidget.setColormap(self.colorMapLin2)
-
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.25) == -7.5)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(0.5) == -5.0)
- self.assertTrue(
- self.colorScaleWidget.getValueFromRelativePosition(1.0) == 0.0)
-
- def testRelativePositionLog(self):
- self.colorMapLog1 = Colormap(name='temperature',
- normalization=Colormap.LOGARITHM,
- vmin=1.0,
- vmax=100.0)
-
- self.colorScaleWidget.setColormap(self.colorMapLog1)
-
- val = self.colorScaleWidget.getValueFromRelativePosition(1.0)
- self.assertAlmostEqual(val, 100.0)
-
- val = self.colorScaleWidget.getValueFromRelativePosition(0.5)
- self.assertAlmostEqual(val, 10.0)
-
- val = self.colorScaleWidget.getValueFromRelativePosition(0.0)
- self.assertTrue(val == 1.0)
-
-
-class TestNoAutoscale(TestCaseQt):
- """Test that ticks and color displayed are correct in the case of a colormap
- with no autoscale
- """
-
- def setUp(self):
- super(TestNoAutoscale, self).setUp()
- self.plot = Plot2D()
- self.colorBar = self.plot.getColorBarWidget()
- self.colorBar.setVisible(True) # Makes sure the colormap is visible
- self.tickBar = self.colorBar.getColorScaleBar().getTickBar()
- self.colorScale = self.colorBar.getColorScaleBar().getColorScale()
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- def tearDown(self):
- self.qapp.processEvents()
- self.tickBar = None
- self.colorScale = None
- del self.colorBar
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- super(TestNoAutoscale, self).tearDown()
-
- def testLogNormNoAutoscale(self):
- colormapLog = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=1.0,
- vmax=100.0)
-
- data = numpy.linspace(10, 1e10, 9).reshape(3, 3)
- self.plot.addImage(data=data, colormap=colormapLog, legend='toto')
- self.plot.setActiveImage('toto')
-
- # test Ticks
- self.tickBar.setTicksNumber(10)
- self.tickBar.computeTicks()
-
- ticksTh = numpy.linspace(1.0, 100.0, 10)
- ticksTh = 10**ticksTh
- numpy.array_equal(self.tickBar.ticks, ticksTh)
-
- # test ColorScale
- val = self.colorScale.getValueFromRelativePosition(1.0)
- self.assertAlmostEqual(val, 100.0)
-
- val = self.colorScale.getValueFromRelativePosition(0.0)
- self.assertTrue(val == 1.0)
-
- def testLinearNormNoAutoscale(self):
- colormapLog = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=-4,
- vmax=5)
-
- data = numpy.linspace(1, 9, 9).reshape(3, 3)
- self.plot.addImage(data=data, colormap=colormapLog, legend='toto')
- self.plot.setActiveImage('toto')
-
- # test Ticks
- self.tickBar.setTicksNumber(10)
- self.tickBar.computeTicks()
-
- numpy.array_equal(self.tickBar.ticks, numpy.linspace(-4, 5, 10))
-
- # test ColorScale
- val = self.colorScale.getValueFromRelativePosition(1.0)
- self.assertTrue(val == 5.0)
-
- val = self.colorScale.getValueFromRelativePosition(0.0)
- self.assertTrue(val == -4.0)
-
-
-class TestColorBarWidget(TestCaseQt):
- """Test interaction with the ColorBarWidget"""
-
- def setUp(self):
- super(TestColorBarWidget, self).setUp()
- self.plot = Plot2D()
- self.colorBar = self.plot.getColorBarWidget()
- self.colorBar.setVisible(True) # Makes sure the colormap is visible
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- def tearDown(self):
- self.qapp.processEvents()
- del self.colorBar
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- super(TestColorBarWidget, self).tearDown()
-
- def testEmptyColorBar(self):
- colorBar = ColorBarWidget(parent=None)
- colorBar.show()
- self.qWaitForWindowExposed(colorBar)
-
- def testNegativeColormaps(self):
- """test the behavior of the ColorBarWidget in the case of negative
- values
-
- Note : colorbar is modified by the Plot directly not ColorBarWidget
- """
- colormapLog = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=None)
-
- data = numpy.array([-5, -4, 0, 2, 3, 5, 10, 20, 30])
- data = data.reshape(3, 3)
- self.plot.addImage(data=data, colormap=colormapLog, legend='toto')
- self.plot.setActiveImage('toto')
-
- # default behavior when with log and negative values: should set vmin
- # to 1 and vmax to 10
- self.assertTrue(self.colorBar.getColorScaleBar().minVal == 2)
- self.assertTrue(self.colorBar.getColorScaleBar().maxVal == 30)
-
- # if data is positive
- data[data < 1] = data.max()
- self.plot.addImage(data=data,
- colormap=colormapLog,
- legend='toto',
- replace=True)
- self.plot.setActiveImage('toto')
-
- self.assertTrue(self.colorBar.getColorScaleBar().minVal == data.min())
- self.assertTrue(self.colorBar.getColorScaleBar().maxVal == data.max())
-
- def testPlotAssocation(self):
- """Make sure the ColorBarWidget is properly connected with the plot"""
- colormap = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None)
-
- # make sure that default settings are the same (but a copy of the
- self.colorBar.setPlot(self.plot)
- self.assertTrue(
- self.colorBar.getColormap() is self.plot.getDefaultColormap())
-
- data = numpy.linspace(0, 10, 100).reshape(10, 10)
- self.plot.addImage(data=data, colormap=colormap, legend='toto')
- self.plot.setActiveImage('toto')
-
- # make sure the modification of the colormap has been done
- self.assertFalse(
- self.colorBar.getColormap() is self.plot.getDefaultColormap())
- self.assertTrue(
- self.colorBar.getColormap() is colormap)
-
- # test that colorbar is updated when default plot colormap changes
- self.plot.clear()
- plotColormap = Colormap(name='gray',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=None)
- self.plot.setDefaultColormap(plotColormap)
- self.assertTrue(self.colorBar.getColormap() is plotColormap)
-
- def testColormapWithoutRange(self):
- """Test with a colormap with vmin==vmax"""
- colormap = Colormap(name='gray',
- normalization=Colormap.LINEAR,
- vmin=1.0,
- vmax=1.0)
- self.colorBar.setColormap(colormap)
-
-
-class TestColorBarUpdate(TestCaseQt):
- """Test that the ColorBar is correctly updated when the signal 'sigChanged'
- of the colormap is emitted
- """
-
- def setUp(self):
- super(TestColorBarUpdate, self).setUp()
- self.plot = Plot2D()
- self.colorBar = self.plot.getColorBarWidget()
- self.colorBar.setVisible(True) # Makes sure the colormap is visible
- self.colorBar.setPlot(self.plot)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
- self.data = numpy.random.rand(9).reshape(3, 3)
-
- def tearDown(self):
- self.qapp.processEvents()
- del self.colorBar
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- super(TestColorBarUpdate, self).tearDown()
-
- def testUpdateColorMap(self):
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=0,
- vmax=1)
-
- # check inital state
- self.plot.addImage(data=self.data, colormap=colormap, legend='toto')
- self.plot.setActiveImage('toto')
-
- self.assertTrue(self.colorBar.getColorScaleBar().minVal == 0)
- self.assertTrue(self.colorBar.getColorScaleBar().maxVal == 1)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmin == 0)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmax == 1)
- self.assertIsInstance(
- self.colorBar.getColorScaleBar().getTickBar()._normalizer,
- colors._LinearNormalization)
-
- # update colormap
- colormap.setVMin(0.5)
- self.assertTrue(self.colorBar.getColorScaleBar().minVal == 0.5)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmin == 0.5)
-
- colormap.setVMax(0.8)
- self.assertTrue(self.colorBar.getColorScaleBar().maxVal == 0.8)
- self.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._vmax == 0.8)
-
- colormap.setNormalization('log')
- self.assertIsInstance(
- self.colorBar.getColorScaleBar().getTickBar()._normalizer,
- colors._LogarithmicNormalization)
-
- # TODO : should also check that if the colormap is changing then values (especially in log scale)
- # should be coherent if in autoscale
-
-
-def suite():
- test_suite = unittest.TestSuite()
- for ui in (TestColorScale, TestNoAutoscale, TestColorBarWidget,
- TestColorBarUpdate):
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(ui))
-
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testCompareImages.py b/silx/gui/plot/test/testCompareImages.py
deleted file mode 100644
index ed6942a..0000000
--- a/silx/gui/plot/test/testCompareImages.py
+++ /dev/null
@@ -1,117 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""Tests for CompareImages widget"""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "23/07/2018"
-
-import unittest
-import numpy
-import weakref
-
-from silx.gui.utils.testutils import TestCaseQt
-from silx.gui.plot.CompareImages import CompareImages
-
-
-class TestCompareImages(TestCaseQt):
- """Test that CompareImages widget is working in some cases"""
-
- def setUp(self):
- super(TestCompareImages, self).setUp()
- self.widget = CompareImages()
-
- def tearDown(self):
- ref = weakref.ref(self.widget)
- self.widget = None
- self.qWaitForDestroy(ref)
- super(TestCompareImages, self).tearDown()
-
- def testIntensityImage(self):
- image1 = numpy.random.rand(10, 10)
- image2 = numpy.random.rand(10, 10)
- self.widget.setData(image1, image2)
-
- def testRgbImage(self):
- image1 = numpy.random.randint(0, 255, size=(10, 10, 3))
- image2 = numpy.random.randint(0, 255, size=(10, 10, 3))
- self.widget.setData(image1, image2)
-
- def testRgbaImage(self):
- image1 = numpy.random.randint(0, 255, size=(10, 10, 4))
- image2 = numpy.random.randint(0, 255, size=(10, 10, 4))
- self.widget.setData(image1, image2)
-
- def testVizualisations(self):
- image1 = numpy.random.rand(10, 10)
- image2 = numpy.random.rand(10, 10)
- self.widget.setData(image1, image2)
- for mode in CompareImages.VisualizationMode:
- self.widget.setVisualizationMode(mode)
-
- def testAlignemnt(self):
- image1 = numpy.random.rand(10, 10)
- image2 = numpy.random.rand(5, 5)
- self.widget.setData(image1, image2)
- for mode in CompareImages.AlignmentMode:
- self.widget.setAlignmentMode(mode)
-
- def testGetPixel(self):
- image1 = numpy.random.rand(11, 11)
- image2 = numpy.random.rand(5, 5)
- image1[5, 5] = 111.111
- image2[2, 2] = 222.222
- self.widget.setData(image1, image2)
- expectedValue = {}
- expectedValue[CompareImages.AlignmentMode.CENTER] = 222.222
- expectedValue[CompareImages.AlignmentMode.STRETCH] = 222.222
- expectedValue[CompareImages.AlignmentMode.ORIGIN] = None
- for mode in expectedValue.keys():
- self.widget.setAlignmentMode(mode)
- data = self.widget.getRawPixelData(11 / 2.0, 11 / 2.0)
- data1, data2 = data
- self.assertEqual(data1, 111.111)
- self.assertEqual(data2, expectedValue[mode])
-
- def testImageEmpty(self):
- self.widget.setData(image1=None, image2=None)
- self.assertTrue(self.widget.getRawPixelData(11 / 2.0, 11 / 2.0) == (None, None))
-
- def testSetImageSeparately(self):
- self.widget.setImage1(numpy.random.rand(10, 10))
- self.widget.setImage2(numpy.random.rand(10, 10))
- for mode in CompareImages.VisualizationMode:
- self.widget.setVisualizationMode(mode)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loadTests(TestCompareImages))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testComplexImageView.py b/silx/gui/plot/test/testComplexImageView.py
deleted file mode 100644
index 4ac3488..0000000
--- a/silx/gui/plot/test/testComplexImageView.py
+++ /dev/null
@@ -1,95 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Test suite for :class:`ComplexImageView`"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "17/01/2018"
-
-
-import unittest
-import logging
-import numpy
-
-from silx.utils.testutils import ParametricTestCase
-from silx.gui.plot import ComplexImageView
-
-from .utils import PlotWidgetTestCase
-
-
-logger = logging.getLogger(__name__)
-
-
-class TestComplexImageView(PlotWidgetTestCase, ParametricTestCase):
- """Test suite of ComplexImageView widget"""
-
- def _createPlot(self):
- return ComplexImageView.ComplexImageView()
-
- def testPlot2DComplex(self):
- """Test API of ComplexImageView widget"""
- data = numpy.array(((0, 1j), (1, 1 + 1j)), dtype=numpy.complex64)
- self.plot.setData(data)
- self.plot.setKeepDataAspectRatio(True)
- self.plot.getPlot().resetZoom()
- self.qWait(100)
-
- # Test colormap API
- colormap = self.plot.getColormap().copy()
- colormap.setName('magma')
- self.plot.setColormap(colormap)
- self.qWait(100)
-
- # Test all modes
- modes = self.plot.supportedComplexModes()
- for mode in modes:
- with self.subTest(mode=mode):
- self.plot.setComplexMode(mode)
- self.qWait(100)
-
- # Test origin and scale API
- self.plot.setScale((2, 1))
- self.qWait(100)
- self.plot.setOrigin((1, 1))
- self.qWait(100)
-
- # Test no data
- self.plot.setData(numpy.zeros((0, 0), dtype=numpy.complex64))
- self.qWait(100)
-
- # Test float data
- self.plot.setData(numpy.arange(100, dtype=numpy.float64).reshape(10, 10))
- self.qWait(100)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(
- TestComplexImageView))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testCurvesROIWidget.py b/silx/gui/plot/test/testCurvesROIWidget.py
deleted file mode 100644
index 6a0ab8c..0000000
--- a/silx/gui/plot/test/testCurvesROIWidget.py
+++ /dev/null
@@ -1,469 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Basic tests for CurvesROIWidget"""
-
-__authors__ = ["T. Vincent", "P. Knobel", "H. Payno"]
-__license__ = "MIT"
-__date__ = "16/11/2017"
-
-
-import logging
-import os.path
-import unittest
-from collections import OrderedDict
-import numpy
-
-from silx.gui import qt
-from silx.gui.plot import items
-from silx.gui.plot import Plot1D
-from silx.test.utils import temp_dir
-from silx.gui.utils.testutils import TestCaseQt, SignalListener
-from silx.gui.plot import PlotWindow, CurvesROIWidget
-from silx.gui.plot.CurvesROIWidget import ROITable
-from silx.gui.utils.testutils import getQToolButtonFromAction
-from silx.gui.plot.PlotInteraction import ItemsInteraction
-
-_logger = logging.getLogger(__name__)
-
-
-class TestCurvesROIWidget(TestCaseQt):
- """Basic test for CurvesROIWidget"""
-
- def setUp(self):
- super(TestCurvesROIWidget, self).setUp()
- self.plot = PlotWindow()
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- self.widget = self.plot.getCurvesRoiDockWidget()
-
- self.widget.show()
- self.qWaitForWindowExposed(self.widget)
-
- def tearDown(self):
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
-
- self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.widget.close()
- del self.widget
-
- super(TestCurvesROIWidget, self).tearDown()
-
- def testDummyAPI(self):
- """Simple test of the getRois and setRois API"""
- roi_neg = CurvesROIWidget.ROI(name='negative', fromdata=-20,
- todata=-10, type_='X')
- roi_pos = CurvesROIWidget.ROI(name='positive', fromdata=10,
- todata=20, type_='X')
-
- self.widget.roiWidget.setRois((roi_pos, roi_neg))
-
- rois_defs = self.widget.roiWidget.getRois()
- self.widget.roiWidget.setRois(rois=rois_defs)
-
- def testWithCurves(self):
- """Plot with curves: test all ROI widget buttons"""
- for offset in range(2):
- self.plot.addCurve(numpy.arange(1000),
- offset + numpy.random.random(1000),
- legend=str(offset))
-
- # Add two ROI
- self.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton)
- self.qWait(200)
- self.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton)
- self.qWait(200)
-
- # Change active curve
- self.plot.setActiveCurve(str(1))
-
- # Delete a ROI
- self.mouseClick(self.widget.roiWidget.delButton, qt.Qt.LeftButton)
- self.qWait(200)
-
- with temp_dir() as tmpDir:
- self.tmpFile = os.path.join(tmpDir, 'test.ini')
-
- # Save ROIs
- self.widget.roiWidget.save(self.tmpFile)
- self.assertTrue(os.path.isfile(self.tmpFile))
- self.assertEqual(len(self.widget.getRois()), 2)
-
- # Reset ROIs
- self.mouseClick(self.widget.roiWidget.resetButton,
- qt.Qt.LeftButton)
- self.qWait(200)
- rois = self.widget.getRois()
- self.assertEqual(len(rois), 1)
- roiID = list(rois.keys())[0]
- self.assertEqual(rois[roiID].getName(), 'ICR')
-
- # Load ROIs
- self.widget.roiWidget.load(self.tmpFile)
- self.assertEqual(len(self.widget.getRois()), 2)
-
- del self.tmpFile
-
- def testMiddleMarker(self):
- """Test with middle marker enabled"""
- self.widget.roiWidget.roiTable.setMiddleROIMarkerFlag(True)
-
- # Add a ROI
- self.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton)
-
- for roiID in self.widget.roiWidget.roiTable._markersHandler._roiMarkerHandlers:
- handler = self.widget.roiWidget.roiTable._markersHandler._roiMarkerHandlers[roiID]
- assert handler.getMarker('min')
- xleftMarker = handler.getMarker('min').getXPosition()
- xMiddleMarker = handler.getMarker('middle').getXPosition()
- xRightMarker = handler.getMarker('max').getXPosition()
- thValue = xleftMarker + (xRightMarker - xleftMarker) / 2.
- self.assertAlmostEqual(xMiddleMarker, thValue)
-
- def testAreaCalculation(self):
- """Test result of area calculation"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
-
- # Add two curves
- self.plot.addCurve(x, y, legend="positive")
- self.plot.addCurve(-x, y, legend="negative")
-
- # Make sure there is an active curve and it is the positive one
- self.plot.setActiveCurve("positive")
-
- # Add two ROIs
- roi_neg = CurvesROIWidget.ROI(name='negative', fromdata=-20,
- todata=-10, type_='X')
- roi_pos = CurvesROIWidget.ROI(name='positive', fromdata=10,
- todata=20, type_='X')
-
- self.widget.roiWidget.setRois((roi_pos, roi_neg))
-
- posCurve = self.plot.getCurve('positive')
- negCurve = self.plot.getCurve('negative')
-
- self.assertEqual(roi_pos.computeRawAndNetArea(posCurve),
- (numpy.trapz(y=[10, 20], x=[10, 20]),
- 0.0))
- self.assertEqual(roi_pos.computeRawAndNetArea(negCurve),
- (0.0, 0.0))
- self.assertEqual(roi_neg.computeRawAndNetArea(posCurve),
- ((0.0), 0.0))
- self.assertEqual(roi_neg.computeRawAndNetArea(negCurve),
- ((-150.0), 0.0))
-
- def testCountsCalculation(self):
- """Test result of count calculation"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
-
- # Add two curves
- self.plot.addCurve(x, y, legend="positive")
- self.plot.addCurve(-x, y, legend="negative")
-
- # Make sure there is an active curve and it is the positive one
- self.plot.setActiveCurve("positive")
-
- # Add two ROIs
- roi_neg = CurvesROIWidget.ROI(name='negative', fromdata=-20,
- todata=-10, type_='X')
- roi_pos = CurvesROIWidget.ROI(name='positive', fromdata=10,
- todata=20, type_='X')
-
- self.widget.roiWidget.setRois((roi_pos, roi_neg))
-
- posCurve = self.plot.getCurve('positive')
- negCurve = self.plot.getCurve('negative')
-
- self.assertEqual(roi_pos.computeRawAndNetCounts(posCurve),
- (y[10:21].sum(), 0.0))
- self.assertEqual(roi_pos.computeRawAndNetCounts(negCurve),
- (0.0, 0.0))
- self.assertEqual(roi_neg.computeRawAndNetCounts(posCurve),
- ((0.0), 0.0))
- self.assertEqual(roi_neg.computeRawAndNetCounts(negCurve),
- (y[10:21].sum(), 0.0))
-
- def testDeferedInit(self):
- """Test behavior of the deferedInit"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
- self.plot.addCurve(x=x, y=y, legend="name", replace="True")
- roisDefs = OrderedDict([
- ["range1",
- OrderedDict([["from", 20], ["to", 200], ["type", "energy"]])],
- ["range2",
- OrderedDict([["from", 300], ["to", 500], ["type", "energy"]])]
- ])
-
- roiWidget = self.plot.getCurvesRoiDockWidget().roiWidget
- self.plot.getCurvesRoiDockWidget().setRois(roisDefs)
- self.assertEqual(len(roiWidget.getRois()), len(roisDefs))
- self.plot.getCurvesRoiDockWidget().setVisible(True)
- self.assertEqual(len(roiWidget.getRois()), len(roisDefs))
-
- def testDictCompatibility(self):
- """Test that ROI api is valid with dict and not information is lost"""
- roiDict = {'from': 20, 'to': 200, 'type': 'energy', 'comment': 'no',
- 'name': 'myROI', 'calibration': [1, 2, 3]}
- roi = CurvesROIWidget.ROI._fromDict(roiDict)
- self.assertEqual(roi.toDict(), roiDict)
-
- def testShowAllROI(self):
- """Test the show allROI action"""
- x = numpy.arange(100.)
- y = numpy.arange(100.)
- self.plot.addCurve(x=x, y=y, legend="name", replace="True")
-
- roisDefsDict = {
- "range1": {"from": 20, "to": 200,"type": "energy"},
- "range2": {"from": 300, "to": 500, "type": "energy"}
- }
-
- roisDefsObj = (
- CurvesROIWidget.ROI(name='range3', fromdata=20, todata=200,
- type_='energy'),
- CurvesROIWidget.ROI(name='range4', fromdata=300, todata=500,
- type_='energy')
- )
- self.widget.roiWidget.showAllMarkers(True)
- roiWidget = self.plot.getCurvesRoiDockWidget().roiWidget
- roiWidget.setRois(roisDefsDict)
- markers = [item for item in self.plot.getItems()
- if isinstance(item, items.MarkerBase)]
- self.assertEqual(len(markers), 2*3)
-
- markersHandler = self.widget.roiWidget.roiTable._markersHandler
- roiWidget.showAllMarkers(True)
- ICRROI = markersHandler.getVisibleRois()
- self.assertEqual(len(ICRROI), 2)
-
- roiWidget.showAllMarkers(False)
- ICRROI = markersHandler.getVisibleRois()
- self.assertEqual(len(ICRROI), 1)
-
- roiWidget.setRois(roisDefsObj)
- self.qapp.processEvents()
- markers = [item for item in self.plot.getItems()
- if isinstance(item, items.MarkerBase)]
- self.assertEqual(len(markers), 2*3)
-
- markersHandler = self.widget.roiWidget.roiTable._markersHandler
- roiWidget.showAllMarkers(True)
- ICRROI = markersHandler.getVisibleRois()
- self.assertEqual(len(ICRROI), 2)
-
- roiWidget.showAllMarkers(False)
- ICRROI = markersHandler.getVisibleRois()
- self.assertEqual(len(ICRROI), 1)
-
- def testRoiEdition(self):
- """Make sure if the ROI object is edited the ROITable will be updated
- """
- roi = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
- self.widget.roiWidget.setRois((roi, ))
-
- x = (0, 1, 1, 2, 2, 3)
- y = (1, 1, 2, 2, 1, 1)
- self.plot.addCurve(x=x, y=y, legend='linearCurve')
- self.plot.setActiveCurve(legend='linearCurve')
- self.widget.calculateROIs()
-
- roiTable = self.widget.roiWidget.roiTable
- indexesColumns = CurvesROIWidget.ROITable.COLUMNS_INDEX
- itemRawCounts = roiTable.item(0, indexesColumns['Raw Counts'])
- itemNetCounts = roiTable.item(0, indexesColumns['Net Counts'])
-
- self.assertTrue(itemRawCounts.text() == '8.0')
- self.assertTrue(itemNetCounts.text() == '2.0')
-
- itemRawArea = roiTable.item(0, indexesColumns['Raw Area'])
- itemNetArea = roiTable.item(0, indexesColumns['Net Area'])
-
- self.assertTrue(itemRawArea.text() == '4.0')
- self.assertTrue(itemNetArea.text() == '1.0')
-
- roi.setTo(2)
- itemRawArea = roiTable.item(0, indexesColumns['Raw Area'])
- self.assertTrue(itemRawArea.text() == '3.0')
- roi.setFrom(1)
- itemRawArea = roiTable.item(0, indexesColumns['Raw Area'])
- self.assertTrue(itemRawArea.text() == '2.0')
-
- def testRemoveActiveROI(self):
- """Test widget behavior when removing the active ROI"""
- roi = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
- self.widget.roiWidget.setRois((roi,))
-
- self.widget.roiWidget.roiTable.setActiveRoi(None)
- self.assertEqual(len(self.widget.roiWidget.roiTable.selectedItems()), 0)
- self.widget.roiWidget.setRois((roi,))
- self.plot.setActiveCurve(legend='linearCurve')
- self.widget.calculateROIs()
-
- def testEmitCurrentROI(self):
- """Test behavior of the CurvesROIWidget.sigROISignal"""
- roi = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
- self.widget.roiWidget.setRois((roi,))
- signalListener = SignalListener()
- self.widget.roiWidget.sigROISignal.connect(signalListener.partial())
- self.widget.show()
- self.qapp.processEvents()
- self.assertEqual(signalListener.callCount(), 0)
- self.assertIs(self.widget.roiWidget.roiTable.activeRoi, roi)
- roi.setFrom(0.0)
- self.qapp.processEvents()
- self.assertEqual(signalListener.callCount(), 0)
- roi.setFrom(0.3)
- self.qapp.processEvents()
- self.assertEqual(signalListener.callCount(), 1)
-
-
-class TestRoiWidgetSignals(TestCaseQt):
- """Test Signals emitted by the RoiWidgetSignals"""
-
- def setUp(self):
- self.plot = Plot1D()
- x = range(20)
- y = range(20)
- self.plot.addCurve(x, y, legend='curve0')
- self.listener = SignalListener()
- self.curves_roi_widget = self.plot.getCurvesRoiWidget()
- self.curves_roi_widget.sigROISignal.connect(self.listener)
- assert self.curves_roi_widget.isVisible() is False
- assert self.listener.callCount() == 0
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- toolButton = getQToolButtonFromAction(self.plot.getRoiAction())
- self.mouseClick(widget=toolButton, button=qt.Qt.LeftButton)
-
- self.curves_roi_widget.show()
- self.qWaitForWindowExposed(self.curves_roi_widget)
-
- def tearDown(self):
- self.plot = None
-
- def testSigROISignalAddRmRois(self):
- """Test SigROISignal when adding and removing ROIS"""
- self.assertEqual(self.listener.callCount(), 1)
- self.listener.clear()
-
- roi1 = CurvesROIWidget.ROI(name='linear', fromdata=0, todata=5)
- self.curves_roi_widget.roiTable.registerROI(roi1)
- self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear')
- self.listener.clear()
-
- roi2 = CurvesROIWidget.ROI(name='linear2', fromdata=0, todata=5)
- self.curves_roi_widget.roiTable.registerROI(roi2)
- self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear2')
- self.listener.clear()
-
- self.curves_roi_widget.roiTable.removeROI(roi2)
- self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.curves_roi_widget.roiTable.activeRoi == roi1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear')
- self.listener.clear()
-
- self.curves_roi_widget.roiTable.deleteActiveRoi()
- self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.curves_roi_widget.roiTable.activeRoi is None)
- self.assertTrue(self.listener.arguments()[0][0]['current'] is None)
- self.listener.clear()
-
- self.curves_roi_widget.roiTable.registerROI(roi1)
- self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'linear')
- self.assertTrue(self.curves_roi_widget.roiTable.activeRoi == roi1)
- self.listener.clear()
- self.qapp.processEvents()
-
- self.curves_roi_widget.roiTable.removeROI(roi1)
- self.qapp.processEvents()
- self.assertEqual(self.listener.callCount(), 1)
- self.assertTrue(self.listener.arguments()[0][0]['current'] == 'ICR')
- self.listener.clear()
-
- def testSigROISignalModifyROI(self):
- """Test SigROISignal when modifying it"""
- self.curves_roi_widget.roiTable.setMiddleROIMarkerFlag(True)
- roi1 = CurvesROIWidget.ROI(name='linear', fromdata=2, todata=5)
- self.curves_roi_widget.roiTable.registerROI(roi1)
- self.curves_roi_widget.roiTable.setActiveRoi(roi1)
-
- # test modify the roi2 object
- self.listener.clear()
- roi1.setFrom(0.56)
- self.assertEqual(self.listener.callCount(), 1)
- self.listener.clear()
- roi1.setTo(2.56)
- self.assertEqual(self.listener.callCount(), 1)
- self.listener.clear()
- roi1.setName('linear2')
- self.assertEqual(self.listener.callCount(), 1)
- self.listener.clear()
- roi1.setType('new type')
- self.assertEqual(self.listener.callCount(), 1)
-
- # modify roi limits (from the gui)
- roi_marker_handler = self.curves_roi_widget.roiTable._markersHandler.getMarkerHandler(roi1.getID())
- for marker_type in ('min', 'max', 'middle'):
- with self.subTest(marker_type=marker_type):
- self.listener.clear()
- marker = roi_marker_handler.getMarker(marker_type)
- self.qapp.processEvents()
- items_interaction = ItemsInteraction(plot=self.plot)
- x_pix, y_pix = self.plot.dataToPixel(marker.getXPosition(), 1)
- items_interaction.beginDrag(x_pix, y_pix)
- self.qapp.processEvents()
- items_interaction.endDrag(x_pix+10, y_pix)
- self.qapp.processEvents()
- self.assertEqual(self.listener.callCount(), 1)
-
- def testSetActiveCurve(self):
- """Test sigRoiSignal when set an active curve"""
- roi1 = CurvesROIWidget.ROI(name='linear', fromdata=2, todata=5)
- self.curves_roi_widget.roiTable.registerROI(roi1)
- self.curves_roi_widget.roiTable.setActiveRoi(roi1)
- self.listener.clear()
- self.plot.setActiveCurve('curve0')
- self.assertEqual(self.listener.callCount(), 0)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- for TestClass in (TestCurvesROIWidget,):
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestClass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testImageStack.py b/silx/gui/plot/test/testImageStack.py
deleted file mode 100644
index 9c21469..0000000
--- a/silx/gui/plot/test/testImageStack.py
+++ /dev/null
@@ -1,197 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Basic tests for ImageStack"""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "15/01/2020"
-
-
-import unittest
-import tempfile
-import numpy
-import h5py
-
-from silx.gui import qt
-from silx.gui.utils.testutils import TestCaseQt
-from silx.io.url import DataUrl
-from silx.gui.plot.ImageStack import ImageStack
-from silx.gui.utils.testutils import SignalListener
-from collections import OrderedDict
-import os
-import time
-import shutil
-
-
-class TestImageStack(TestCaseQt):
- """Simple test of the Image stack"""
-
- def setUp(self):
- TestCaseQt.setUp(self)
- self.urls = OrderedDict()
- self._raw_data = {}
- self._folder = tempfile.mkdtemp()
- self._n_urls = 10
- file_name = os.path.join(self._folder, 'test_inage_stack_file.h5')
- with h5py.File(file_name, 'w') as h5f:
- for i in range(self._n_urls):
- width = numpy.random.randint(10, 40)
- height = numpy.random.randint(10, 40)
- raw_data = numpy.random.random((width, height))
- self._raw_data[i] = raw_data
- h5f[str(i)] = raw_data
- self.urls[i] = DataUrl(file_path=file_name,
- data_path=str(i),
- scheme='silx')
- self.widget = ImageStack()
-
- self.urlLoadedListener = SignalListener()
- self.widget.sigLoaded.connect(self.urlLoadedListener)
-
- self.currentUrlChangedListener = SignalListener()
- self.widget.sigCurrentUrlChanged.connect(self.currentUrlChangedListener)
-
- def tearDown(self):
- shutil.rmtree(self._folder)
- self.widget.setAttribute(qt.Qt.WA_DeleteOnClose, True)
- self.widget.close()
- TestCaseQt.setUp(self)
-
- def testControls(self):
- """Test that selection using the url table and the slider are working
- """
- self.widget.show()
- self.assertEqual(self.widget.getCurrentUrl(), None)
- self.assertEqual(self.widget.getCurrentUrlIndex(), None)
- self.widget.setUrls(list(self.urls.values()))
-
- # wait for image to be loaded
- self._waitUntilUrlLoaded()
-
- self.assertEqual(self.widget.getCurrentUrl(), self.urls[0])
-
- # make sure all image are loaded
- self.assertEqual(self.urlLoadedListener.callCount(), self._n_urls)
- numpy.testing.assert_array_equal(
- self.widget.getPlotWidget().getActiveImage(just_legend=False).getData(),
- self._raw_data[0])
- self.assertEqual(self.widget._slider.value(), 0)
-
- self.widget._urlsTable.setUrl(self.urls[4])
- numpy.testing.assert_array_equal(
- self.widget.getPlotWidget().getActiveImage(just_legend=False).getData(),
- self._raw_data[4])
- self.assertEqual(self.widget._slider.value(), 4)
- self.assertEqual(self.widget.getCurrentUrl(), self.urls[4])
- self.assertEqual(self.widget.getCurrentUrlIndex(), 4)
-
- self.widget._slider.setUrlIndex(6)
- numpy.testing.assert_array_equal(
- self.widget.getPlotWidget().getActiveImage(just_legend=False).getData(),
- self._raw_data[6])
- self.assertEqual(self.widget._urlsTable.currentItem().text(),
- self.urls[6].path())
-
- def testCurrentUrlSignals(self):
- """Test emission of 'currentUrlChangedListener'"""
- # check initialization
- self.assertEqual(self.currentUrlChangedListener.callCount(), 0)
- self.widget.setUrls(list(self.urls.values()))
- self.qapp.processEvents()
- time.sleep(0.5)
- self.qapp.processEvents()
- # once loaded the two signals should have been sended
- self.assertEqual(self.currentUrlChangedListener.callCount(), 1)
- # if the slider is stuck to the same position no signal should be
- # emitted
- self.qapp.processEvents()
- time.sleep(0.5)
- self.qapp.processEvents()
- self.assertEqual(self.widget._slider.value(), 0)
- self.assertEqual(self.currentUrlChangedListener.callCount(), 1)
- # if slider position is changed, one of each signal should have been
- # emitted
- self.widget._urlsTable.setUrl(self.urls[4])
- self.qapp.processEvents()
- time.sleep(1.5)
- self.qapp.processEvents()
- self.assertEqual(self.currentUrlChangedListener.callCount(), 2)
-
- def testUtils(self):
- """Test that some utils functions are working"""
- self.widget.show()
- self.widget.setUrls(list(self.urls.values()))
- self.assertEqual(len(self.widget.getUrls()), len(self.urls))
-
- # wait for image to be loaded
- self._waitUntilUrlLoaded()
-
- urls_values = list(self.urls.values())
- self.assertEqual(urls_values[0], self.urls[0])
- self.assertEqual(urls_values[7], self.urls[7])
-
- self.assertEqual(self.widget._getNextUrl(urls_values[2]).path(),
- urls_values[3].path())
- self.assertEqual(self.widget._getPreviousUrl(urls_values[0]), None)
- self.assertEqual(self.widget._getPreviousUrl(urls_values[6]).path(),
- urls_values[5].path())
-
- self.assertEqual(self.widget._getNNextUrls(2, urls_values[0]),
- urls_values[1:3])
- self.assertEqual(self.widget._getNNextUrls(5, urls_values[7]),
- urls_values[8:])
- self.assertEqual(self.widget._getNPreviousUrls(3, urls_values[2]),
- urls_values[:2])
- self.assertEqual(self.widget._getNPreviousUrls(5, urls_values[8]),
- urls_values[3:8])
-
- def _waitUntilUrlLoaded(self, timeout=2.0):
- """Wait until all image urls are loaded"""
- loop_duration = 0.2
- remaining_duration = timeout
- while(len(self.widget._loadingThreads) > 0 and remaining_duration > 0):
- remaining_duration -= loop_duration
- time.sleep(loop_duration)
- self.qapp.processEvents()
-
- if remaining_duration <= 0.0:
- remaining_urls = []
- for thread_ in self.widget._loadingThreads:
- remaining_urls.append(thread_.url.path())
- mess = 'All images are not loaded after the time out. ' \
- 'Remaining urls are: ' + str(remaining_urls)
- raise TimeoutError(mess)
- return True
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestImageStack))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testImageView.py b/silx/gui/plot/test/testImageView.py
deleted file mode 100644
index 3c8d84c..0000000
--- a/silx/gui/plot/test/testImageView.py
+++ /dev/null
@@ -1,136 +0,0 @@
-# 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.
-#
-# ###########################################################################*/
-"""Basic tests for PlotWindow"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-import unittest
-import numpy
-
-from silx.gui import qt
-from silx.gui.utils.testutils import TestCaseQt
-
-from silx.gui.plot import ImageView
-from silx.gui.colors import Colormap
-
-
-class TestImageView(TestCaseQt):
- """Tests of ImageView widget."""
-
- def setUp(self):
- super(TestImageView, self).setUp()
- self.plot = ImageView()
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- def tearDown(self):
- self.qapp.processEvents()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- self.qapp.processEvents()
- super(TestImageView, self).tearDown()
-
- def testSetImage(self):
- """Test setImage"""
- image = numpy.arange(100).reshape(10, 10)
-
- self.plot.setImage(image, reset=True)
- self.qWait(100)
- self.assertEqual(self.plot.getXAxis().getLimits(), (0, 10))
- self.assertEqual(self.plot.getYAxis().getLimits(), (0, 10))
-
- # With reset=False
- self.plot.setImage(image[::2, ::2], reset=False)
- self.qWait(100)
- self.assertEqual(self.plot.getXAxis().getLimits(), (0, 10))
- self.assertEqual(self.plot.getYAxis().getLimits(), (0, 10))
-
- self.plot.setImage(image, origin=(10, 20), scale=(2, 4), reset=False)
- self.qWait(100)
- self.assertEqual(self.plot.getXAxis().getLimits(), (0, 10))
- self.assertEqual(self.plot.getYAxis().getLimits(), (0, 10))
-
- # With reset=True
- self.plot.setImage(image, origin=(1, 2), scale=(1, 0.5), reset=True)
- self.qWait(100)
- self.assertEqual(self.plot.getXAxis().getLimits(), (1, 11))
- self.assertEqual(self.plot.getYAxis().getLimits(), (2, 7))
-
- self.plot.setImage(image[::2, ::2], reset=True)
- self.qWait(100)
- self.assertEqual(self.plot.getXAxis().getLimits(), (0, 5))
- self.assertEqual(self.plot.getYAxis().getLimits(), (0, 5))
-
- def testColormap(self):
- """Test get|setColormap"""
- image = numpy.arange(100).reshape(10, 10)
- self.plot.setImage(image)
-
- # Colormap as dict
- self.plot.setColormap({'name': 'viridis',
- 'normalization': 'log',
- 'autoscale': False,
- 'vmin': 0,
- 'vmax': 1})
- colormap = self.plot.getColormap()
- self.assertEqual(colormap.getName(), 'viridis')
- self.assertEqual(colormap.getNormalization(), 'log')
- self.assertEqual(colormap.getVMin(), 0)
- self.assertEqual(colormap.getVMax(), 1)
-
- # Colormap as keyword arguments
- self.plot.setColormap(colormap='magma',
- normalization='linear',
- autoscale=True,
- vmin=1,
- vmax=2)
- self.assertEqual(colormap.getName(), 'magma')
- self.assertEqual(colormap.getNormalization(), 'linear')
- self.assertEqual(colormap.getVMin(), None)
- self.assertEqual(colormap.getVMax(), None)
-
- # Update colormap with keyword argument
- self.plot.setColormap(normalization='log')
- self.assertEqual(colormap.getNormalization(), 'log')
-
- # Colormap as Colormap object
- cmap = Colormap()
- self.plot.setColormap(cmap)
- self.assertIs(self.plot.getColormap(), cmap)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestImageView))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testInteraction.py b/silx/gui/plot/test/testInteraction.py
deleted file mode 100644
index a47337e..0000000
--- a/silx/gui/plot/test/testInteraction.py
+++ /dev/null
@@ -1,89 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Tests from interaction state machines"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "18/02/2016"
-
-
-import unittest
-
-from silx.gui.plot import Interaction
-
-
-class TestInteraction(unittest.TestCase):
- def testClickOrDrag(self):
- """Minimalistic test for click or drag state machine."""
- events = []
-
- class TestClickOrDrag(Interaction.ClickOrDrag):
- def click(self, x, y, btn):
- events.append(('click', x, y, btn))
-
- def beginDrag(self, x, y, btn):
- events.append(('beginDrag', x, y, btn))
-
- def drag(self, x, y, btn):
- events.append(('drag', x, y, btn))
-
- def endDrag(self, start, end, btn):
- events.append(('endDrag', start, end, btn))
-
- clickOrDrag = TestClickOrDrag()
-
- # click
- clickOrDrag.handleEvent('press', 10, 10, Interaction.LEFT_BTN)
- self.assertEqual(len(events), 0)
-
- clickOrDrag.handleEvent('release', 10, 10, Interaction.LEFT_BTN)
- self.assertEqual(len(events), 1)
- self.assertEqual(events[0], ('click', 10, 10, Interaction.LEFT_BTN))
-
- # drag
- events = []
- clickOrDrag.handleEvent('press', 10, 10, Interaction.LEFT_BTN)
- self.assertEqual(len(events), 0)
- clickOrDrag.handleEvent('move', 15, 10)
- self.assertEqual(len(events), 2) # Received beginDrag and drag
- self.assertEqual(events[0], ('beginDrag', 10, 10, Interaction.LEFT_BTN))
- self.assertEqual(events[1], ('drag', 15, 10, Interaction.LEFT_BTN))
- clickOrDrag.handleEvent('move', 20, 10)
- self.assertEqual(len(events), 3)
- self.assertEqual(events[-1], ('drag', 20, 10, Interaction.LEFT_BTN))
- clickOrDrag.handleEvent('release', 20, 10, Interaction.LEFT_BTN)
- self.assertEqual(len(events), 4)
- self.assertEqual(events[-1], ('endDrag', (10, 10), (20, 10), Interaction.LEFT_BTN))
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestInteraction))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testItem.py b/silx/gui/plot/test/testItem.py
deleted file mode 100644
index 8dacdea..0000000
--- a/silx/gui/plot/test/testItem.py
+++ /dev/null
@@ -1,340 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2019 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.
-#
-# ###########################################################################*/
-"""Tests for PlotWidget items."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "01/09/2017"
-
-
-import unittest
-
-import numpy
-
-from silx.gui.utils.testutils import SignalListener
-from silx.gui.plot.items import ItemChangedType
-from silx.gui.plot import items
-from .utils import PlotWidgetTestCase
-
-
-class TestSigItemChangedSignal(PlotWidgetTestCase):
- """Test item's sigItemChanged signal"""
-
- def testCurveChanged(self):
- """Test sigItemChanged for curve"""
- self.plot.addCurve(numpy.arange(10), numpy.arange(10), legend='test')
- curve = self.plot.getCurve('test')
-
- listener = SignalListener()
- curve.sigItemChanged.connect(listener)
-
- # Test for signal in Item class
- curve.setVisible(False)
- curve.setVisible(True)
- curve.setZValue(100)
-
- # Test for signals in PointsBase class
- curve.setData(numpy.arange(100), numpy.arange(100))
-
- # SymbolMixIn
- curve.setSymbol('Circle')
- curve.setSymbol('d')
- curve.setSymbolSize(20)
-
- # AlphaMixIn
- curve.setAlpha(0.5)
-
- # Test for signals in Curve class
- # ColorMixIn
- curve.setColor('yellow')
- # YAxisMixIn
- curve.setYAxis('right')
- # FillMixIn
- curve.setFill(True)
- # LineMixIn
- curve.setLineStyle(':')
- curve.setLineStyle(':') # Not sending event
- curve.setLineWidth(2)
-
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.VISIBLE,
- ItemChangedType.VISIBLE,
- ItemChangedType.ZVALUE,
- ItemChangedType.DATA,
- ItemChangedType.SYMBOL,
- ItemChangedType.SYMBOL,
- ItemChangedType.SYMBOL_SIZE,
- ItemChangedType.ALPHA,
- ItemChangedType.COLOR,
- ItemChangedType.YAXIS,
- ItemChangedType.FILL,
- ItemChangedType.LINE_STYLE,
- ItemChangedType.LINE_WIDTH])
-
- def testHistogramChanged(self):
- """Test sigItemChanged for Histogram"""
- self.plot.addHistogram(
- numpy.arange(10), edges=numpy.arange(11), legend='test')
- histogram = self.plot.getHistogram('test')
- listener = SignalListener()
- histogram.sigItemChanged.connect(listener)
-
- # Test signals in Histogram class
- histogram.setData(numpy.zeros(10), numpy.arange(11))
-
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.DATA])
-
- def testImageDataChanged(self):
- """Test sigItemChanged for ImageData"""
- self.plot.addImage(numpy.arange(100).reshape(10, 10), legend='test')
- image = self.plot.getImage('test')
-
- listener = SignalListener()
- image.sigItemChanged.connect(listener)
-
- # ColormapMixIn
- colormap = self.plot.getDefaultColormap().copy()
- image.setColormap(colormap)
- image.getColormap().setName('viridis')
-
- # Test of signals in ImageBase class
- image.setOrigin(10)
- image.setScale(2)
-
- # Test of signals in ImageData class
- image.setData(numpy.ones((10, 10)))
-
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.COLORMAP,
- ItemChangedType.COLORMAP,
- ItemChangedType.POSITION,
- ItemChangedType.SCALE,
- ItemChangedType.COLORMAP,
- ItemChangedType.DATA])
-
- def testImageRgbaChanged(self):
- """Test sigItemChanged for ImageRgba"""
- self.plot.addImage(numpy.ones((10, 10, 3)), legend='rgb')
- image = self.plot.getImage('rgb')
-
- listener = SignalListener()
- image.sigItemChanged.connect(listener)
-
- # Test of signals in ImageRgba class
- image.setData(numpy.zeros((10, 10, 3)))
-
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.DATA])
-
- def testMarkerChanged(self):
- """Test sigItemChanged for markers"""
- self.plot.addMarker(10, 20, legend='test')
- marker = self.plot._getMarker('test')
-
- listener = SignalListener()
- marker.sigItemChanged.connect(listener)
-
- # Test signals in _BaseMarker
- marker.setPosition(10, 10)
- marker.setPosition(10, 10) # Not sending event
- marker.setText('toto')
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.POSITION,
- ItemChangedType.TEXT])
-
- # XMarker
- self.plot.addXMarker(10, legend='x')
- marker = self.plot._getMarker('x')
-
- listener = SignalListener()
- marker.sigItemChanged.connect(listener)
- marker.setPosition(20, 20)
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.POSITION])
-
- # YMarker
- self.plot.addYMarker(10, legend='x')
- marker = self.plot._getMarker('x')
-
- listener = SignalListener()
- marker.sigItemChanged.connect(listener)
- marker.setPosition(20, 20)
- self.assertEqual(listener.arguments(argumentIndex=0),
- [ItemChangedType.POSITION])
-
- def testScatterChanged(self):
- """Test sigItemChanged for scatter"""
- data = numpy.arange(10)
- self.plot.addScatter(data, data, data, legend='test')
- scatter = self.plot.getScatter('test')
-
- listener = SignalListener()
- scatter.sigItemChanged.connect(listener)
-
- # ColormapMixIn
- scatter.getColormap().setName('viridis')
-
- # Test of signals in Scatter class
- scatter.setData((0, 1, 2), (1, 0, 2), (0, 1, 2))
-
- # Visualization mode changed
- scatter.setVisualization(scatter.Visualization.SOLID)
-
- self.assertEqual(listener.arguments(),
- [(ItemChangedType.COLORMAP,),
- (ItemChangedType.COLORMAP,),
- (ItemChangedType.DATA,),
- (ItemChangedType.VISUALIZATION_MODE,)])
-
- def testShapeChanged(self):
- """Test sigItemChanged for shape"""
- data = numpy.array((1., 10.))
- self.plot.addShape(data, data, legend='test', shape='rectangle')
- shape = self.plot._getItem(kind='item', legend='test')
-
- listener = SignalListener()
- shape.sigItemChanged.connect(listener)
-
- shape.setOverlay(True)
- shape.setPoints(((2., 2.), (3., 3.)))
-
- self.assertEqual(listener.arguments(),
- [(ItemChangedType.OVERLAY,),
- (ItemChangedType.DATA,)])
-
-
-class TestSymbol(PlotWidgetTestCase):
- """Test item's symbol """
-
- def test(self):
- """Test sigItemChanged for curve"""
- self.plot.addCurve(numpy.arange(10), numpy.arange(10), legend='test')
- curve = self.plot.getCurve('test')
-
- # SymbolMixIn
- curve.setSymbol('o')
- name = curve.getSymbolName()
- self.assertEqual('Circle', name)
-
- name = curve.getSymbolName('d')
- self.assertEqual('Diamond', name)
-
-
-class TestVisibleExtent(PlotWidgetTestCase):
- """Test item's visible extent feature"""
-
- def testGetVisibleBounds(self):
- """Test Item.getVisibleBounds"""
-
- # Create test items (with a bounding box of x: [1,3], y: [0,2])
- curve = items.Curve()
- curve.setData((1, 2, 3), (0, 1, 2))
-
- histogram = items.Histogram()
- histogram.setData((0, 1, 2), (1, 5/3, 7/3, 3))
-
- image = items.ImageData()
- image.setOrigin((1, 0))
- image.setData(numpy.arange(4).reshape(2, 2))
-
- scatter = items.Scatter()
- scatter.setData((1, 2, 3), (0, 1, 2), (1, 2, 3))
-
- bbox = items.BoundingRect()
- bbox.setBounds((1, 3, 0, 2))
-
- xaxis, yaxis = self.plot.getXAxis(), self.plot.getYAxis()
- for item in (curve, histogram, image, scatter, bbox):
- with self.subTest(item=item):
- xaxis.setLimits(0, 100)
- yaxis.setLimits(0, 100)
- self.plot.addItem(item)
- self.assertEqual(item.getVisibleBounds(), (1., 3., 0., 2.))
-
- xaxis.setLimits(0.5, 2.5)
- self.assertEqual(item.getVisibleBounds(), (1, 2.5, 0., 2.))
-
- yaxis.setLimits(0.5, 1.5)
- self.assertEqual(item.getVisibleBounds(), (1, 2.5, 0.5, 1.5))
-
- item.setVisible(False)
- self.assertIsNone(item.getVisibleBounds())
-
- self.plot.clear()
-
- def testVisibleExtentTracking(self):
- """Test Item's visible extent tracking"""
- image = items.ImageData()
- image.setData(numpy.arange(6).reshape(2, 3))
-
- listener = SignalListener()
- image._sigVisibleBoundsChanged.connect(listener)
- image._setVisibleBoundsTracking(True)
- self.assertTrue(image._isVisibleBoundsTracking())
-
- self.plot.addItem(image)
- self.assertEqual(listener.callCount(), 1)
-
- self.plot.getXAxis().setLimits(0, 1)
- self.assertEqual(listener.callCount(), 2)
-
- self.plot.hide()
- self.qapp.processEvents()
- # No event here
- self.assertEqual(listener.callCount(), 2)
-
- self.plot.getXAxis().setLimits(1, 2)
- # No event since PlotWidget is hidden, delayed to PlotWidget show
- self.assertEqual(listener.callCount(), 2)
-
- self.plot.show()
- self.qapp.processEvents()
- # Receives delayed event now
- self.assertEqual(listener.callCount(), 3)
-
- image.setOrigin((-1, -1))
- self.assertEqual(listener.callCount(), 4)
-
- image.setVisible(False)
- image.setOrigin((0, 0))
- # No event since item is not visible
- self.assertEqual(listener.callCount(), 4)
-
- image.setVisible(True)
- # Receives delayed event now
- self.assertEqual(listener.callCount(), 5)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- for klass in (TestSigItemChangedSignal, TestSymbol, TestVisibleExtent):
- test_suite.addTest(loadTests(klass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testLegendSelector.py b/silx/gui/plot/test/testLegendSelector.py
deleted file mode 100644
index de5ffde..0000000
--- a/silx/gui/plot/test/testLegendSelector.py
+++ /dev/null
@@ -1,142 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2004-2016 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.
-#
-# ###########################################################################*/
-"""Basic tests for PlotWidget"""
-
-__authors__ = ["T. Rueter", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "15/05/2017"
-
-
-import logging
-import unittest
-
-from silx.gui import qt
-from silx.gui.utils.testutils import TestCaseQt
-from silx.gui.plot import LegendSelector
-
-
-_logger = logging.getLogger(__name__)
-
-
-class TestLegendSelector(TestCaseQt):
- """Basic test for LegendSelector"""
-
- def testLegendSelector(self):
- """Test copied from __main__ of LegendSelector in PyMca"""
- class Notifier(qt.QObject):
- def __init__(self):
- qt.QObject.__init__(self)
- self.chk = True
-
- def signalReceived(self, **kw):
- obj = self.sender()
- _logger.info('NOTIFIER -- signal received\n\tsender: %s',
- str(obj))
-
- notifier = Notifier()
-
- legends = ['Legend0',
- 'Legend1',
- 'Long Legend 2',
- 'Foo Legend 3',
- 'Even Longer Legend 4',
- 'Short Leg 5',
- 'Dot symbol 6',
- 'Comma symbol 7']
- colors = [qt.Qt.darkRed, qt.Qt.green, qt.Qt.yellow, qt.Qt.darkCyan,
- qt.Qt.blue, qt.Qt.darkBlue, qt.Qt.red, qt.Qt.darkYellow]
- symbols = ['o', 't', '+', 'x', 's', 'd', '.', ',']
-
- win = LegendSelector.LegendListView()
- # win = LegendListContextMenu()
- # win = qt.QWidget()
- # layout = qt.QVBoxLayout()
- # layout.setContentsMargins(0,0,0,0)
- llist = []
-
- for _idx, (l, c, s) in enumerate(zip(legends, colors, symbols)):
- ddict = {
- 'color': qt.QColor(c),
- 'linewidth': 4,
- 'symbol': s,
- }
- legend = l
- llist.append((legend, ddict))
- # item = qt.QListWidgetItem(win)
- # legendWidget = LegendListItemWidget(l)
- # legendWidget.icon.setSymbol(s)
- # legendWidget.icon.setColor(qt.QColor(c))
- # layout.addWidget(legendWidget)
- # win.setItemWidget(item, legendWidget)
-
- # win = LegendListItemWidget('Some Legend 1')
- # print(llist)
- model = LegendSelector.LegendModel(legendList=llist)
- win.setModel(model)
- win.setSelectionModel(qt.QItemSelectionModel(model))
- win.setContextMenu()
- # print('Edit triggers: %d'%win.editTriggers())
-
- # win = LegendListWidget(None, legends)
- # win[0].updateItem(ddict)
- # win.setLayout(layout)
- win.sigLegendSignal.connect(notifier.signalReceived)
- win.show()
-
- win.clear()
- win.setLegendList(llist)
-
- self.qWaitForWindowExposed(win)
-
-
-class TestRenameCurveDialog(TestCaseQt):
- """Basic test for RenameCurveDialog"""
-
- def testDialog(self):
- """Create dialog, change name and press OK"""
- self.dialog = LegendSelector.RenameCurveDialog(
- None, 'curve1', ['curve1', 'curve2', 'curve3'])
- self.dialog.open()
- self.qWaitForWindowExposed(self.dialog)
- self.keyClicks(self.dialog.lineEdit, 'changed')
- self.mouseClick(self.dialog.okButton, qt.Qt.LeftButton)
- self.qapp.processEvents()
- ret = self.dialog.result()
- self.assertEqual(ret, qt.QDialog.Accepted)
- newName = self.dialog.getText()
- self.assertEqual(newName, 'curve1changed')
- del self.dialog
-
-
-def suite():
- test_suite = unittest.TestSuite()
- for TestClass in (TestLegendSelector, TestRenameCurveDialog):
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestClass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testLimitConstraints.py b/silx/gui/plot/test/testLimitConstraints.py
deleted file mode 100644
index 5e7e0b1..0000000
--- a/silx/gui/plot/test/testLimitConstraints.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 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.
-#
-# ###########################################################################*/
-"""Test setLimitConstaints on the PlotWidget"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "30/08/2017"
-
-
-import unittest
-from silx.gui.plot import PlotWidget
-
-
-class TestLimitConstaints(unittest.TestCase):
- """Tests setLimitConstaints class"""
-
- def setUp(self):
- self.plot = PlotWidget()
-
- def tearDown(self):
- self.plot = None
-
- def testApi(self):
- """Test availability of the API"""
- self.plot.getXAxis().setLimitsConstraints(minPos=1, maxPos=10)
- self.plot.getXAxis().setRangeConstraints(minRange=1, maxRange=1)
- self.plot.getYAxis().setLimitsConstraints(minPos=1, maxPos=10)
- self.plot.getYAxis().setRangeConstraints(minRange=1, maxRange=1)
-
- def testXMinMax(self):
- """Test limit constains on x-axis"""
- self.plot.getXAxis().setLimitsConstraints(minPos=0, maxPos=100)
- self.plot.setLimits(xmin=-1, xmax=101, ymin=-1, ymax=101)
- self.assertEqual(self.plot.getXAxis().getLimits(), (0, 100))
- self.assertEqual(self.plot.getYAxis().getLimits(), (-1, 101))
-
- def testYMinMax(self):
- """Test limit constains on y-axis"""
- self.plot.getYAxis().setLimitsConstraints(minPos=0, maxPos=100)
- self.plot.setLimits(xmin=-1, xmax=101, ymin=-1, ymax=101)
- self.assertEqual(self.plot.getXAxis().getLimits(), (-1, 101))
- self.assertEqual(self.plot.getYAxis().getLimits(), (0, 100))
-
- def testMinXRange(self):
- """Test min range constains on x-axis"""
- self.plot.getXAxis().setRangeConstraints(minRange=100)
- self.plot.setLimits(xmin=1, xmax=99, ymin=1, ymax=99)
- limits = self.plot.getXAxis().getLimits()
- self.assertEqual(limits[1] - limits[0], 100)
- limits = self.plot.getYAxis().getLimits()
- self.assertNotEqual(limits[1] - limits[0], 100)
-
- def testMaxXRange(self):
- """Test max range constains on x-axis"""
- self.plot.getXAxis().setRangeConstraints(maxRange=100)
- self.plot.setLimits(xmin=-1, xmax=101, ymin=-1, ymax=101)
- limits = self.plot.getXAxis().getLimits()
- self.assertEqual(limits[1] - limits[0], 100)
- limits = self.plot.getYAxis().getLimits()
- self.assertNotEqual(limits[1] - limits[0], 100)
-
- def testMinYRange(self):
- """Test min range constains on y-axis"""
- self.plot.getYAxis().setRangeConstraints(minRange=100)
- self.plot.setLimits(xmin=1, xmax=99, ymin=1, ymax=99)
- limits = self.plot.getXAxis().getLimits()
- self.assertNotEqual(limits[1] - limits[0], 100)
- limits = self.plot.getYAxis().getLimits()
- self.assertEqual(limits[1] - limits[0], 100)
-
- def testMaxYRange(self):
- """Test max range constains on y-axis"""
- self.plot.getYAxis().setRangeConstraints(maxRange=100)
- self.plot.setLimits(xmin=-1, xmax=101, ymin=-1, ymax=101)
- limits = self.plot.getXAxis().getLimits()
- self.assertNotEqual(limits[1] - limits[0], 100)
- limits = self.plot.getYAxis().getLimits()
- self.assertEqual(limits[1] - limits[0], 100)
-
- def testChangeOfConstraints(self):
- """Test changing of the constraints"""
- self.plot.getXAxis().setRangeConstraints(minRange=10, maxRange=10)
- # There is no more constraints on the range
- self.plot.getXAxis().setRangeConstraints(minRange=None, maxRange=None)
- self.plot.setLimits(xmin=-1, xmax=101, ymin=-1, ymax=101)
- self.assertEqual(self.plot.getXAxis().getLimits(), (-1, 101))
-
- def testSettingConstraints(self):
- """Test setting a constaint (setLimits first then the constaint)"""
- self.plot.setLimits(xmin=-1, xmax=101, ymin=-1, ymax=101)
- self.plot.getXAxis().setLimitsConstraints(minPos=0, maxPos=100)
- self.assertEqual(self.plot.getXAxis().getLimits(), (0, 100))
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loadTests(TestLimitConstaints))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testMaskToolsWidget.py b/silx/gui/plot/test/testMaskToolsWidget.py
deleted file mode 100644
index c22975f..0000000
--- a/silx/gui/plot/test/testMaskToolsWidget.py
+++ /dev/null
@@ -1,316 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""Basic tests for MaskToolsWidget"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "17/01/2018"
-
-
-import logging
-import os.path
-import unittest
-
-import numpy
-
-from silx.gui import qt
-from silx.test.utils import temp_dir
-from silx.utils.testutils import ParametricTestCase
-from silx.gui.utils.testutils import getQToolButtonFromAction
-from silx.gui.plot import PlotWindow, MaskToolsWidget
-from .utils import PlotWidgetTestCase
-
-import fabio
-
-
-_logger = logging.getLogger(__name__)
-
-
-class TestMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
- """Basic test for MaskToolsWidget"""
-
- def _createPlot(self):
- return PlotWindow()
-
- def setUp(self):
- super(TestMaskToolsWidget, self).setUp()
- self.widget = MaskToolsWidget.MaskToolsDockWidget(plot=self.plot, name='TEST')
- self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, self.widget)
- self.maskWidget = self.widget.widget()
-
- def tearDown(self):
- del self.maskWidget
- del self.widget
- super(TestMaskToolsWidget, self).tearDown()
-
- def testEmptyPlot(self):
- """Empty plot, display MaskToolsDockWidget, toggle multiple masks"""
- self.maskWidget.setMultipleMasks('single')
- self.qapp.processEvents()
-
- self.maskWidget.setMultipleMasks('exclusive')
- self.qapp.processEvents()
-
- def _drag(self):
- """Drag from plot center to offset position"""
- plot = self.plot.getWidgetHandle()
- xCenter, yCenter = plot.width() // 2, plot.height() // 2
- offset = min(plot.width(), plot.height()) // 10
-
- pos0 = xCenter, yCenter
- pos1 = xCenter + offset, yCenter + offset
-
- self.mouseMove(plot, pos=(0, 0))
- self.mouseMove(plot, pos=pos0)
- self.qapp.processEvents()
- self.mousePress(plot, qt.Qt.LeftButton, pos=pos0)
- self.qapp.processEvents()
- self.mouseMove(plot, pos=(pos0[0] + offset // 2, pos0[1] + offset // 2))
- self.mouseMove(plot, pos=pos1)
- self.qapp.processEvents()
- self.mouseRelease(plot, qt.Qt.LeftButton, pos=pos1)
- self.qapp.processEvents()
- self.mouseMove(plot, pos=(0, 0))
-
- def _drawPolygon(self):
- """Draw a star polygon in the plot"""
- plot = self.plot.getWidgetHandle()
- x, y = plot.width() // 2, plot.height() // 2
- offset = min(plot.width(), plot.height()) // 10
-
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset),
- (x, y + offset)] # Close polygon
-
- self.mouseMove(plot, pos=(0, 0))
- for pos in star:
- self.mouseMove(plot, pos=pos)
- self.qapp.processEvents()
- self.mousePress(plot, qt.Qt.LeftButton, pos=pos)
- self.qapp.processEvents()
- self.mouseRelease(plot, qt.Qt.LeftButton, pos=pos)
- self.qapp.processEvents()
-
- def _drawPencil(self):
- """Draw a star polygon in the plot"""
- plot = self.plot.getWidgetHandle()
- x, y = plot.width() // 2, plot.height() // 2
- offset = min(plot.width(), plot.height()) // 10
-
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset)]
-
- self.mouseMove(plot, pos=(0, 0))
- self.mouseMove(plot, pos=star[0])
- self.mousePress(plot, qt.Qt.LeftButton, pos=star[0])
- for pos in star[1:]:
- self.mouseMove(plot, pos=pos)
- self.mouseRelease(
- plot, qt.Qt.LeftButton, pos=star[-1])
-
- def _isMaskItemSync(self):
- """Check if masks from item and tools are sync or not"""
- if self.maskWidget.isItemMaskUpdated():
- return numpy.all(numpy.equal(
- self.maskWidget.getSelectionMask(),
- self.plot.getActiveImage().getMaskData(copy=False)))
- else:
- return True
-
- def testWithAnImage(self):
- """Plot with an image: test MaskToolsWidget interactions"""
-
- # Add and remove a image (this should enable/disable GUI + change mask)
- self.plot.addImage(numpy.random.random(1024**2).reshape(1024, 1024),
- legend='test')
- self.qapp.processEvents()
-
- self.plot.remove('test', kind='image')
- self.qapp.processEvents()
-
- tests = [((0, 0), (1, 1)),
- ((1000, 1000), (1, 1)),
- ((0, 0), (-1, -1)),
- ((1000, 1000), (-1, -1))]
-
- for itemMaskUpdated in (False, True):
- for origin, scale in tests:
- with self.subTest(origin=origin, scale=scale):
- self.maskWidget.setItemMaskUpdated(itemMaskUpdated)
- self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024),
- legend='test',
- origin=origin,
- scale=scale)
- self.qapp.processEvents()
-
- self.assertEqual(
- self.maskWidget.isItemMaskUpdated(), itemMaskUpdated)
-
- # Test draw rectangle #
- toolButton = getQToolButtonFromAction(self.maskWidget.rectAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
-
- # mask
- self.maskWidget.maskStateGroup.button(1).click()
- self.qapp.processEvents()
- self._drag()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
- self.assertTrue(self._isMaskItemSync())
-
- # unmask same region
- self.maskWidget.maskStateGroup.button(0).click()
- self.qapp.processEvents()
- self._drag()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
- self.assertTrue(self._isMaskItemSync())
-
- # Test draw polygon #
- toolButton = getQToolButtonFromAction(self.maskWidget.polygonAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
-
- # mask
- self.maskWidget.maskStateGroup.button(1).click()
- self.qapp.processEvents()
- self._drawPolygon()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
- self.assertTrue(self._isMaskItemSync())
-
- # unmask same region
- self.maskWidget.maskStateGroup.button(0).click()
- self.qapp.processEvents()
- self._drawPolygon()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
- self.assertTrue(self._isMaskItemSync())
-
- # Test draw pencil #
- toolButton = getQToolButtonFromAction(self.maskWidget.pencilAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
-
- self.maskWidget.pencilSpinBox.setValue(30)
- self.qapp.processEvents()
-
- # mask
- self.maskWidget.maskStateGroup.button(1).click()
- self.qapp.processEvents()
- self._drawPencil()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
- self.assertTrue(self._isMaskItemSync())
-
- # unmask same region
- self.maskWidget.maskStateGroup.button(0).click()
- self.qapp.processEvents()
- self._drawPencil()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
- self.assertTrue(self._isMaskItemSync())
-
- # Test no draw tool #
- toolButton = getQToolButtonFromAction(self.maskWidget.browseAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
-
- self.plot.clear()
-
- def __loadSave(self, file_format):
- """Plot with an image: test MaskToolsWidget operations"""
- self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024),
- legend='test')
- self.qapp.processEvents()
-
- # Draw a polygon mask
- toolButton = getQToolButtonFromAction(self.maskWidget.polygonAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
- self._drawPolygon()
-
- ref_mask = self.maskWidget.getSelectionMask()
- self.assertFalse(numpy.all(numpy.equal(ref_mask, 0)))
-
- with temp_dir() as tmp:
- mask_filename = os.path.join(tmp, 'mask.' + file_format)
- self.maskWidget.save(mask_filename, file_format)
-
- self.maskWidget.resetSelectionMask()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
-
- self.maskWidget.load(mask_filename)
- self.assertTrue(numpy.all(numpy.equal(
- self.maskWidget.getSelectionMask(), ref_mask)))
-
- def testLoadSaveNpy(self):
- self.__loadSave("npy")
-
- def testLoadSaveFit2D(self):
- self.__loadSave("msk")
-
- def testSigMaskChangedEmitted(self):
- self.plot.addImage(numpy.arange(512**2).reshape(512, 512),
- legend='test')
- self.plot.resetZoom()
- self.qapp.processEvents()
-
- l = []
-
- def slot():
- l.append(1)
-
- self.maskWidget.sigMaskChanged.connect(slot)
-
- # rectangle mask
- toolButton = getQToolButtonFromAction(self.maskWidget.rectAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
- self.maskWidget.maskStateGroup.button(1).click()
- self.qapp.processEvents()
- self._drag()
-
- self.assertGreater(len(l), 0)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- for TestClass in (TestMaskToolsWidget,):
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestClass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testPixelIntensityHistoAction.py b/silx/gui/plot/test/testPixelIntensityHistoAction.py
deleted file mode 100644
index ac29952..0000000
--- a/silx/gui/plot/test/testPixelIntensityHistoAction.py
+++ /dev/null
@@ -1,157 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 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.
-#
-# ###########################################################################*/
-"""Basic tests for PixelIntensitiesHistoAction"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "02/03/2018"
-
-
-import numpy
-import unittest
-
-from silx.utils.testutils import ParametricTestCase
-from silx.gui.utils.testutils import TestCaseQt, getQToolButtonFromAction
-from silx.gui import qt
-from silx.gui.plot import Plot2D
-
-
-class TestPixelIntensitiesHisto(TestCaseQt, ParametricTestCase):
- """Tests for PixelIntensitiesHistoAction widget."""
-
- def setUp(self):
- super(TestPixelIntensitiesHisto, self).setUp()
- self.image = numpy.random.rand(10, 10)
- self.plotImage = Plot2D()
- self.plotImage.getIntensityHistogramAction().setVisible(True)
-
- def tearDown(self):
- del self.plotImage
- super(TestPixelIntensitiesHisto, self).tearDown()
-
- def testShowAndHide(self):
- """Simple test that the plot is showing and hiding when activating the
- action"""
- self.plotImage.addImage(self.image, origin=(0, 0), legend='sino')
- self.plotImage.show()
-
- histoAction = self.plotImage.getIntensityHistogramAction()
-
- # test the pixel intensity diagram is showing
- button = getQToolButtonFromAction(histoAction)
- self.assertIsNot(button, None)
- self.mouseMove(button)
- self.mouseClick(button, qt.Qt.LeftButton)
- self.qapp.processEvents()
- self.assertTrue(histoAction.getHistogramWidget().isVisible())
-
- # test the pixel intensity diagram is hiding
- self.qapp.setActiveWindow(self.plotImage)
- self.qapp.processEvents()
- self.mouseMove(button)
- self.mouseClick(button, qt.Qt.LeftButton)
- self.qapp.processEvents()
- self.assertFalse(histoAction.getHistogramWidget().isVisible())
-
- def testImageFormatInput(self):
- """Test multiple type as image input"""
- typesToTest = [numpy.uint8, numpy.int8, numpy.int16, numpy.int32,
- numpy.float32, numpy.float64]
- self.plotImage.addImage(self.image, origin=(0, 0), legend='sino')
- self.plotImage.show()
- button = getQToolButtonFromAction(
- self.plotImage.getIntensityHistogramAction())
- self.mouseMove(button)
- self.mouseClick(button, qt.Qt.LeftButton)
- self.qapp.processEvents()
- for typeToTest in typesToTest:
- with self.subTest(typeToTest=typeToTest):
- self.plotImage.addImage(self.image.astype(typeToTest),
- origin=(0, 0), legend='sino')
-
- def testScatter(self):
- """Test that an histogram from a scatter is displayed"""
- xx = numpy.arange(10)
- yy = numpy.arange(10)
- value = numpy.sin(xx)
- self.plotImage.addScatter(xx, yy, value)
- self.plotImage.show()
-
- histoAction = self.plotImage.getIntensityHistogramAction()
-
- # test the pixel intensity diagram is showing
- button = getQToolButtonFromAction(histoAction)
- self.assertIsNot(button, None)
- self.mouseMove(button)
- self.mouseClick(button, qt.Qt.LeftButton)
- self.qapp.processEvents()
-
- widget = histoAction.getHistogramWidget()
- self.assertTrue(widget.isVisible())
- items = widget.getPlotWidget().getItems()
- self.assertEqual(len(items), 1)
-
- def testChangeItem(self):
- """Test that histogram changes it the item changes"""
- xx = numpy.arange(10)
- yy = numpy.arange(10)
- value = numpy.sin(xx)
- self.plotImage.addScatter(xx, yy, value)
- self.plotImage.show()
-
- histoAction = self.plotImage.getIntensityHistogramAction()
-
- # test the pixel intensity diagram is showing
- button = getQToolButtonFromAction(histoAction)
- self.assertIsNot(button, None)
- self.mouseMove(button)
- self.mouseClick(button, qt.Qt.LeftButton)
- self.qapp.processEvents()
-
- # Reach histogram from the first item
- widget = histoAction.getHistogramWidget()
- self.assertTrue(widget.isVisible())
- items = widget.getPlotWidget().getItems()
- data1 = items[0].getValueData(copy=False)
-
- # Set another item to the plot
- self.plotImage.addImage(self.image, origin=(0, 0), legend='sino')
- self.qapp.processEvents()
- data2 = items[0].getValueData(copy=False)
-
- # Histogram is not the same
- self.assertFalse(numpy.array_equal(data1, data2))
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(
- TestPixelIntensitiesHisto))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testPlotInteraction.py b/silx/gui/plot/test/testPlotInteraction.py
deleted file mode 100644
index 7a30434..0000000
--- a/silx/gui/plot/test/testPlotInteraction.py
+++ /dev/null
@@ -1,172 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016=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.
-#
-# ###########################################################################*/
-"""Tests of plot interaction, through a PlotWidget"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "01/09/2017"
-
-
-import unittest
-from silx.gui import qt
-from .utils import PlotWidgetTestCase
-
-
-class _SignalDump(object):
- """Callable object that store passed arguments in a list"""
-
- def __init__(self):
- self._received = []
-
- def __call__(self, *args):
- self._received.append(args)
-
- @property
- def received(self):
- """Return a shallow copy of the list of received arguments"""
- return list(self._received)
-
-
-class TestSelectPolygon(PlotWidgetTestCase):
- """Test polygon selection interaction"""
-
- def _interactionModeChanged(self, source):
- """Check that source received in event is the correct one"""
- self.assertEqual(source, self)
-
- def _draw(self, polygon):
- """Draw a polygon in the plot
-
- :param polygon: List of points (x, y) of the polygon (closed)
- """
- plot = self.plot.getWidgetHandle()
-
- dump = _SignalDump()
- self.plot.sigPlotSignal.connect(dump)
-
- for pos in polygon:
- self.mouseMove(plot, pos=pos)
- self.qapp.processEvents()
- self.mousePress(plot, qt.Qt.LeftButton, pos=pos)
- self.qapp.processEvents()
- self.mouseRelease(plot, qt.Qt.LeftButton, pos=pos)
- self.qapp.processEvents()
-
- self.plot.sigPlotSignal.disconnect(dump)
- return [args[0] for args in dump.received]
-
- def test(self):
- """Test draw polygons + events"""
- self.plot.sigInteractiveModeChanged.connect(
- self._interactionModeChanged)
-
- self.plot.setInteractiveMode(
- 'draw', shape='polygon', label='test', source=self)
- interaction = self.plot.getInteractiveMode()
-
- self.assertEqual(interaction['mode'], 'draw')
- self.assertEqual(interaction['shape'], 'polygon')
-
- self.plot.sigInteractiveModeChanged.disconnect(
- self._interactionModeChanged)
-
- plot = self.plot.getWidgetHandle()
- xCenter, yCenter = plot.width() // 2, plot.height() // 2
- offset = min(plot.width(), plot.height()) // 10
-
- # Star polygon
- star = [(xCenter, yCenter + offset),
- (xCenter - offset, yCenter - offset),
- (xCenter + offset, yCenter),
- (xCenter - offset, yCenter),
- (xCenter + offset, yCenter - offset),
- (xCenter, yCenter + offset)] # Close polygon
-
- # Draw while dumping signals
- events = self._draw(star)
-
- # Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 6)
-
- # Large square
- largeSquare = [(xCenter - offset, yCenter - offset),
- (xCenter + offset, yCenter - offset),
- (xCenter + offset, yCenter + offset),
- (xCenter - offset, yCenter + offset),
- (xCenter - offset, yCenter - offset)] # Close polygon
-
- # Draw while dumping signals
- events = self._draw(largeSquare)
-
- # Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 5)
-
- # Rectangle too thin along X: Some points are ignored
- thinRectX = [(xCenter, yCenter - offset),
- (xCenter, yCenter + offset),
- (xCenter + 1, yCenter + offset),
- (xCenter + 1, yCenter - offset)] # Close polygon
-
- # Draw while dumping signals
- events = self._draw(thinRectX)
-
- # Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 3)
-
- # Rectangle too thin along Y: Some points are ignored
- thinRectY = [(xCenter - offset, yCenter),
- (xCenter + offset, yCenter),
- (xCenter + offset, yCenter + 1),
- (xCenter - offset, yCenter + 1)] # Close polygon
-
- # Draw while dumping signals
- events = self._draw(thinRectY)
-
- # Test last event
- drawEvents = [event for event in events
- if event['event'].startswith('drawing')]
- self.assertEqual(drawEvents[-1]['event'], 'drawingFinished')
- self.assertEqual(len(drawEvents[-1]['points']), 3)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- for TestClass in (TestSelectPolygon,):
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestClass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testPlotWidget.py b/silx/gui/plot/test/testPlotWidget.py
deleted file mode 100755
index b55260e..0000000
--- a/silx/gui/plot/test/testPlotWidget.py
+++ /dev/null
@@ -1,2072 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2021 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.
-#
-# ###########################################################################*/
-"""Basic tests for PlotWidget"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "03/01/2019"
-
-
-import unittest
-import logging
-import numpy
-import sys
-
-from silx.utils.testutils import ParametricTestCase, parameterize
-from silx.gui.utils.testutils import SignalListener
-from silx.gui.utils.testutils import TestCaseQt
-
-from silx.test.utils import test_options
-
-from silx.gui import qt
-from silx.gui.plot import PlotWidget
-from silx.gui.plot.items.curve import CurveStyle
-from silx.gui.plot.items import BoundingRect, XAxisExtent, YAxisExtent, Axis
-from silx.gui.colors import Colormap
-
-from .utils import PlotWidgetTestCase
-
-
-SIZE = 1024
-"""Size of the test image"""
-
-DATA_2D = numpy.arange(SIZE ** 2).reshape(SIZE, SIZE)
-"""Image data set"""
-
-
-logger = logging.getLogger(__name__)
-
-
-class TestSpecialBackend(PlotWidgetTestCase, ParametricTestCase):
-
- def __init__(self, methodName='runTest', backend=None):
- TestCaseQt.__init__(self, methodName=methodName)
- self.__backend = backend
-
- def _createPlot(self):
- return PlotWidget(backend=self.__backend)
-
- def testPlot(self):
- self.assertIsNotNone(self.plot)
-
-
-class TestPlotWidget(PlotWidgetTestCase, ParametricTestCase):
- """Basic tests for PlotWidget"""
-
- def testShow(self):
- """Most basic test"""
- pass
-
- def testSetTitleLabels(self):
- """Set title and axes labels"""
-
- title, xlabel, ylabel = 'the title', 'x label', 'y label'
- self.plot.setGraphTitle(title)
- self.plot.getXAxis().setLabel(xlabel)
- self.plot.getYAxis().setLabel(ylabel)
- self.qapp.processEvents()
-
- self.assertEqual(self.plot.getGraphTitle(), title)
- self.assertEqual(self.plot.getXAxis().getLabel(), xlabel)
- self.assertEqual(self.plot.getYAxis().getLabel(), ylabel)
-
- def _checkLimits(self,
- expectedXLim=None,
- expectedYLim=None,
- expectedRatio=None):
- """Assert that limits are as expected"""
- xlim = self.plot.getXAxis().getLimits()
- ylim = self.plot.getYAxis().getLimits()
- ratio = abs(xlim[1] - xlim[0]) / abs(ylim[1] - ylim[0])
-
- if expectedXLim is not None:
- self.assertEqual(expectedXLim, xlim)
-
- if expectedYLim is not None:
- self.assertEqual(expectedYLim, ylim)
-
- if expectedRatio is not None:
- self.assertTrue(
- numpy.allclose(expectedRatio, ratio, atol=0.01))
-
- def testChangeLimitsWithAspectRatio(self):
- self.plot.setKeepDataAspectRatio()
- self.qapp.processEvents()
- xlim = self.plot.getXAxis().getLimits()
- ylim = self.plot.getYAxis().getLimits()
- defaultRatio = abs(xlim[1] - xlim[0]) / abs(ylim[1] - ylim[0])
-
- self.plot.getXAxis().setLimits(1., 10.)
- self._checkLimits(expectedXLim=(1., 10.), expectedRatio=defaultRatio)
- self.qapp.processEvents()
- self._checkLimits(expectedXLim=(1., 10.), expectedRatio=defaultRatio)
-
- self.plot.getYAxis().setLimits(1., 10.)
- self._checkLimits(expectedYLim=(1., 10.), expectedRatio=defaultRatio)
- self.qapp.processEvents()
- self._checkLimits(expectedYLim=(1., 10.), expectedRatio=defaultRatio)
-
- def testResizeWidget(self):
- """Test resizing the widget and receiving limitsChanged events"""
- self.plot.resize(200, 200)
- self.qapp.processEvents()
- self.qWait(100)
-
- xlim = self.plot.getXAxis().getLimits()
- ylim = self.plot.getYAxis().getLimits()
-
- listener = SignalListener()
- self.plot.getXAxis().sigLimitsChanged.connect(listener.partial('x'))
- self.plot.getYAxis().sigLimitsChanged.connect(listener.partial('y'))
-
- # Resize without aspect ratio
- self.plot.resize(200, 300)
- self.qapp.processEvents()
- self.qWait(100)
- self._checkLimits(expectedXLim=xlim, expectedYLim=ylim)
- self.assertEqual(listener.callCount(), 0)
-
- # Resize with aspect ratio
- self.plot.setKeepDataAspectRatio(True)
- self.qapp.processEvents()
- self.qWait(1000)
- listener.clear() # Clean-up received signal
-
- self.plot.resize(200, 200)
- self.qapp.processEvents()
- self.qWait(100)
- self.assertNotEqual(listener.callCount(), 0)
-
- def testAddRemoveItemSignals(self):
- """Test sigItemAdded and sigItemAboutToBeRemoved"""
- listener = SignalListener()
- self.plot.sigItemAdded.connect(listener.partial('add'))
- self.plot.sigItemAboutToBeRemoved.connect(listener.partial('remove'))
-
- self.plot.addCurve((1, 2, 3), (3, 2, 1), legend='curve')
- self.assertEqual(listener.callCount(), 1)
-
- curve = self.plot.getCurve('curve')
- self.plot.remove('curve')
- self.assertEqual(listener.callCount(), 2)
- self.assertEqual(listener.arguments(callIndex=0), ('add', curve))
- self.assertEqual(listener.arguments(callIndex=1), ('remove', curve))
-
- def testGetItems(self):
- """Test getItems method"""
- curve_x = 1, 2
- self.plot.addCurve(curve_x, (3, 4))
- image = (0, 1), (2, 3)
- self.plot.addImage(image)
- scatter_x = 10, 11
- self.plot.addScatter(scatter_x, (12, 13), (0, 1))
- marker_pos = 5, 5
- self.plot.addMarker(*marker_pos)
- marker_x = 6
- self.plot.addXMarker(marker_x)
- self.plot.addShape((0, 5), (2, 10), shape='rectangle')
-
- items = self.plot.getItems()
- self.assertEqual(len(items), 6)
- self.assertTrue(numpy.all(numpy.equal(items[0].getXData(), curve_x)))
- self.assertTrue(numpy.all(numpy.equal(items[1].getData(), image)))
- self.assertTrue(numpy.all(numpy.equal(items[2].getXData(), scatter_x)))
- self.assertTrue(numpy.all(numpy.equal(items[3].getPosition(), marker_pos)))
- self.assertTrue(numpy.all(numpy.equal(items[4].getPosition()[0], marker_x)))
- self.assertEqual(items[5].getType(), 'rectangle')
-
- def testRemoveDiscardItem(self):
- """Test removeItem and discardItem"""
- self.plot.addCurve((1, 2, 3), (1, 2, 3))
- curve = self.plot.getItems()[0]
- self.plot.removeItem(curve)
- with self.assertRaises(ValueError):
- self.plot.removeItem(curve)
-
- self.plot.addCurve((1, 2, 3), (1, 2, 3))
- curve = self.plot.getItems()[0]
- result = self.plot.discardItem(curve)
- self.assertTrue(result)
- result = self.plot.discardItem(curve)
- self.assertFalse(result)
-
- def testBackGroundColors(self):
- self.plot.setVisible(True)
- self.qWaitForWindowExposed(self.plot)
- self.qapp.processEvents()
-
- # Custom the full background
- color = self.plot.getBackgroundColor()
- self.assertTrue(color.isValid())
- self.assertEqual(color, qt.QColor(255, 255, 255))
- self.plot.setBackgroundColor("red")
- color = self.plot.getBackgroundColor()
- self.assertTrue(color.isValid())
- self.qapp.processEvents()
-
- # Custom the data background
- color = self.plot.getDataBackgroundColor()
- self.assertFalse(color.isValid())
- self.plot.setDataBackgroundColor("red")
- color = self.plot.getDataBackgroundColor()
- self.assertTrue(color.isValid())
- self.qapp.processEvents()
-
- # Back to default
- self.plot.setBackgroundColor('white')
- self.plot.setDataBackgroundColor(None)
- color = self.plot.getBackgroundColor()
- self.assertTrue(color.isValid())
- self.assertEqual(color, qt.QColor(255, 255, 255))
- color = self.plot.getDataBackgroundColor()
- self.assertFalse(color.isValid())
- self.qapp.processEvents()
-
-
-class TestPlotImage(PlotWidgetTestCase, ParametricTestCase):
- """Basic tests for addImage"""
-
- def setUp(self):
- super(TestPlotImage, self).setUp()
-
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
-
- def testPlotColormapTemperature(self):
- self.plot.setGraphTitle('Temp. Linear')
-
- colormap = Colormap(name='temperature',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
-
- def testPlotColormapGray(self):
- self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('Gray Linear')
-
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
-
- def testPlotColormapTemperatureLog(self):
- self.plot.setGraphTitle('Temp. Log')
-
- colormap = Colormap(name='temperature',
- normalization=Colormap.LOGARITHM,
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
-
- def testPlotRgbRgba(self):
- self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('RGB + RGBA')
-
- rgb = numpy.array(
- (((0, 0, 0), (128, 0, 0), (255, 0, 0)),
- ((0, 128, 0), (0, 128, 128), (0, 128, 255))),
- dtype=numpy.uint8)
-
- self.plot.addImage(rgb, legend="rgb_uint8",
- origin=(0, 0), scale=(1, 1),
- resetzoom=False)
-
- rgb = numpy.array(
- (((0, 0, 0), (32768, 0, 0), (65535, 0, 0)),
- ((0, 32768, 0), (0, 32768, 32768), (0, 32768, 65535))),
- dtype=numpy.uint16)
-
- self.plot.addImage(rgb, legend="rgb_uint16",
- origin=(3, 2), scale=(2, 2),
- resetzoom=False)
-
- rgba = numpy.array(
- (((0, 0, 0, .5), (.5, 0, 0, 1), (1, 0, 0, .5)),
- ((0, .5, 0, 1), (0, .5, .5, 1), (0, 1, 1, .5))),
- dtype=numpy.float32)
-
- self.plot.addImage(rgba, legend="rgba_float32",
- origin=(9, 6), scale=(1, 1),
- resetzoom=False)
-
- self.plot.resetZoom()
-
- def testPlotColormapCustom(self):
- self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('Custom colormap')
-
- colormap = Colormap(name=None,
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None,
- colors=((0., 0., 0.), (1., 0., 0.),
- (0., 1., 0.), (0., 0., 1.)))
- self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap,
- resetzoom=False)
-
- colormap = Colormap(name=None,
- normalization=Colormap.LINEAR,
- vmin=None,
- vmax=None,
- colors=numpy.array(
- ((0, 0, 0, 0), (0, 0, 0, 128),
- (128, 128, 128, 128), (255, 255, 255, 255)),
- dtype=numpy.uint8))
- self.plot.addImage(DATA_2D, legend="image 2", colormap=colormap,
- origin=(DATA_2D.shape[0], 0),
- resetzoom=False)
- self.plot.resetZoom()
-
- def testPlotColormapNaNColor(self):
- self.plot.setKeepDataAspectRatio(False)
- self.plot.setGraphTitle('Colormap with NaN color')
-
- colormap = Colormap()
- colormap.setNaNColor('red')
- self.assertEqual(colormap.getNaNColor(), qt.QColor(255, 0, 0))
- data = DATA_2D.astype(numpy.float32)
- data[len(data)//2:] = numpy.nan
- self.plot.addImage(data, legend="image 1", colormap=colormap,
- resetzoom=False)
- self.plot.resetZoom()
-
- colormap.setNaNColor((0., 1., 0., 1.))
- self.assertEqual(colormap.getNaNColor(), qt.QColor(0, 255, 0))
- self.qapp.processEvents()
-
- def testImageOriginScale(self):
- """Test of image with different origin and scale"""
- self.plot.setGraphTitle('origin and scale')
-
- tests = [ # (origin, scale)
- ((10, 20), (1, 1)),
- ((10, 20), (-1, -1)),
- ((-10, 20), (2, 1)),
- ((10, -20), (-1, -2)),
- (100, 2),
- (-100, (1, 1)),
- ((10, 20), 2),
- ]
-
- for origin, scale in tests:
- with self.subTest(origin=origin, scale=scale):
- self.plot.addImage(DATA_2D, origin=origin, scale=scale)
-
- try:
- ox, oy = origin
- except TypeError:
- ox, oy = origin, origin
- try:
- sx, sy = scale
- except TypeError:
- sx, sy = scale, scale
- xbounds = ox, ox + DATA_2D.shape[1] * sx
- ybounds = oy, oy + DATA_2D.shape[0] * sy
-
- # Check limits without aspect ratio
- xmin, xmax = self.plot.getXAxis().getLimits()
- ymin, ymax = self.plot.getYAxis().getLimits()
- self.assertEqual(xmin, min(xbounds))
- self.assertEqual(xmax, max(xbounds))
- self.assertEqual(ymin, min(ybounds))
- self.assertEqual(ymax, max(ybounds))
-
- # Check limits with aspect ratio
- self.plot.setKeepDataAspectRatio(True)
- xmin, xmax = self.plot.getXAxis().getLimits()
- ymin, ymax = self.plot.getYAxis().getLimits()
- self.assertTrue(round(xmin, 7) <= min(xbounds))
- self.assertTrue(round(xmax, 7) >= max(xbounds))
- self.assertTrue(round(ymin, 7) <= min(ybounds))
- self.assertTrue(round(ymax, 7) >= max(ybounds))
-
- self.plot.setKeepDataAspectRatio(False) # Reset aspect ratio
- self.plot.clear()
- self.plot.resetZoom()
-
- def testPlotColormapDictAPI(self):
- """Test that the addImage API using a colormap dictionary is still
- working"""
- self.plot.setGraphTitle('Temp. Log')
-
- colormap = {
- 'name': 'temperature',
- 'normalization': 'log',
- 'vmin': None,
- 'vmax': None
- }
- self.plot.addImage(DATA_2D, legend="image 1", colormap=colormap)
-
- def testPlotComplexImage(self):
- """Test that a complex image is displayed as its absolute value."""
- data = numpy.linspace(1, 1j, 100).reshape(10, 10)
- self.plot.addImage(data, legend='complex')
-
- image = self.plot.getActiveImage()
- retrievedData = image.getData(copy=False)
- self.assertTrue(
- numpy.all(numpy.equal(retrievedData, numpy.absolute(data))))
-
- def testPlotBooleanImage(self):
- """Test that a boolean image is displayed and converted to int8."""
- data = numpy.zeros((10, 10), dtype=bool)
- data[::2, ::2] = True
- self.plot.addImage(data, legend='boolean')
-
- image = self.plot.getActiveImage()
- retrievedData = image.getData(copy=False)
- self.assertTrue(numpy.all(numpy.equal(retrievedData, data)))
- self.assertIs(retrievedData.dtype.type, numpy.int8)
-
- def testPlotAlphaImage(self):
- """Test with an alpha image layer"""
- data = numpy.random.random((10, 10))
- alpha = numpy.linspace(0, 1, 100).reshape(10, 10)
- self.plot.addImage(data, legend='image')
- image = self.plot.getActiveImage()
- image.setData(data, alpha=alpha)
- self.qapp.processEvents()
- self.assertTrue(numpy.array_equal(alpha, image.getAlphaData()))
-
-
-class TestPlotCurve(PlotWidgetTestCase):
- """Basic tests for addCurve."""
-
- # Test data sets
- xData = numpy.arange(1000)
- yData = -500 + 100 * numpy.sin(xData)
- xData2 = xData + 1000
- yData2 = xData - 1000 + 200 * numpy.random.random(1000)
-
- def setUp(self):
- super(TestPlotCurve, self).setUp()
- self.plot.setGraphTitle('Curve')
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
-
- self.plot.setActiveCurveHandling(False)
-
- def testPlotCurveInfinite(self):
- """Test plot curves with not finite data"""
- tests = {
- 'y all not finite': ([0, 1, 2], [numpy.inf, numpy.nan, -numpy.inf]),
- 'x all not finite': ([numpy.inf, numpy.nan, -numpy.inf], [0, 1, 2]),
- 'x some inf': ([0, numpy.inf, 2], [0, 1, 2]),
- 'y some inf': ([0, 1, 2], [0, numpy.inf, 2])
- }
- for name, args in tests.items():
- with self.subTest(name):
- self.plot.addCurve(*args)
- self.plot.resetZoom()
- self.qapp.processEvents()
- self.plot.clear()
-
- def testPlotCurveColorFloat(self):
- color = numpy.array(numpy.random.random(3 * 1000),
- dtype=numpy.float32).reshape(1000, 3)
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 1",
- replace=False, resetzoom=False,
- color=color,
- linestyle="", symbol="s")
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
- self.plot.resetZoom()
-
- def testPlotCurveColorByte(self):
- color = numpy.array(255 * numpy.random.random(3 * 1000),
- dtype=numpy.uint8).reshape(1000, 3)
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 1",
- replace=False, resetzoom=False,
- color=color,
- linestyle="", symbol="s")
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
- self.plot.resetZoom()
-
- def testPlotCurveColors(self):
- color = numpy.array(numpy.random.random(3 * 1000),
- dtype=numpy.float32).reshape(1000, 3)
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color=color, linestyle="-", symbol='o')
- self.plot.resetZoom()
-
- # Test updating color array
-
- # From array to array
- newColors = numpy.ones((len(self.xData), 3), dtype=numpy.float32)
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color=newColors, symbol='o')
-
- # Array to single color
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color='green', symbol='o')
-
- # single color to array
- self.plot.addCurve(self.xData, self.yData,
- legend="curve 2",
- replace=False, resetzoom=False,
- color=color, symbol='o')
-
- def testPlotBaselineNumpyArray(self):
- """simple test of the API with baseline as a numpy array"""
- x = numpy.arange(0, 10, step=0.1)
- my_sin = numpy.sin(x)
- y = numpy.arange(-4, 6, step=0.1) + my_sin
- baseline = y - 1.0
-
- self.plot.addCurve(x=x, y=y, color='grey', legend='curve1', fill=True,
- baseline=baseline)
-
- def testPlotBaselineScalar(self):
- """simple test of the API with baseline as an int"""
- x = numpy.arange(0, 10, step=0.1)
- my_sin = numpy.sin(x)
- y = numpy.arange(-4, 6, step=0.1) + my_sin
-
- self.plot.addCurve(x=x, y=y, color='grey', legend='curve1', fill=True,
- baseline=0)
-
- def testPlotBaselineList(self):
- """simple test of the API with baseline as an int"""
- x = numpy.arange(0, 10, step=0.1)
- my_sin = numpy.sin(x)
- y = numpy.arange(-4, 6, step=0.1) + my_sin
-
- self.plot.addCurve(x=x, y=y, color='grey', legend='curve1', fill=True,
- baseline=list(range(0, 100, 1)))
-
- def testPlotCurveComplexData(self):
- """Test curve with complex data"""
- data = numpy.arange(100.) + 1j
- self.plot.addCurve(x=data, y=data, xerror=data, yerror=data)
-
-
-class TestPlotHistogram(PlotWidgetTestCase):
- """Basic tests for add Histogram"""
- def setUp(self):
- super(TestPlotHistogram, self).setUp()
- self.edges = numpy.arange(0, 10, step=1)
- self.histogram = numpy.random.random(len(self.edges))
-
- def testPlot(self):
- self.plot.addHistogram(histogram=self.histogram,
- edges=self.edges,
- legend='histogram1')
-
- def testPlotBaseline(self):
- self.plot.addHistogram(histogram=self.histogram,
- edges=self.edges,
- legend='histogram1',
- color='blue',
- baseline=-2,
- z=2,
- fill=True)
-
-
-class TestPlotScatter(PlotWidgetTestCase, ParametricTestCase):
- """Basic tests for addScatter"""
-
- def testScatter(self):
- x = numpy.arange(100)
- y = numpy.arange(100)
- value = numpy.arange(100)
- self.plot.addScatter(x, y, value)
- self.plot.resetZoom()
-
- def testScatterComplexData(self):
- """Test scatter item with complex data"""
- data = numpy.arange(100.) + 1j
- self.plot.addScatter(
- x=data, y=data, value=data, xerror=data, yerror=data)
- self.plot.resetZoom()
-
- def testScatterVisualization(self):
- self.plot.addScatter((0, 1, 0, 1), (0, 0, 2, 2), (0, 1, 2, 3))
- self.plot.resetZoom()
- self.qapp.processEvents()
-
- scatter = self.plot.getItems()[0]
-
- for visualization in ('solid',
- 'points',
- 'regular_grid',
- 'irregular_grid',
- 'binned_statistic',
- scatter.Visualization.SOLID,
- scatter.Visualization.POINTS,
- scatter.Visualization.REGULAR_GRID,
- scatter.Visualization.IRREGULAR_GRID,
- scatter.Visualization.BINNED_STATISTIC):
- with self.subTest(visualization=visualization):
- scatter.setVisualization(visualization)
- self.qapp.processEvents()
-
- def testGridVisualization(self):
- """Test regular and irregular grid mode with different points"""
- points = { # name: (x, y, order)
- 'single point': ((1.,), (1.,), 'row'),
- 'horizontal line': ((0, 1, 2), (0, 0, 0), 'row'),
- 'horizontal line backward': ((2, 1, 0), (0, 0, 0), 'row'),
- 'vertical line': ((0, 0, 0), (0, 1, 2), 'row'),
- 'vertical line backward': ((0, 0, 0), (2, 1, 0), 'row'),
- 'grid fast x, +x +y': ((0, 1, 2, 0, 1, 2), (0, 0, 0, 1, 1, 1), 'row'),
- 'grid fast x, +x -y': ((0, 1, 2, 0, 1, 2), (1, 1, 1, 0, 0, 0), 'row'),
- 'grid fast x, -x -y': ((2, 1, 0, 2, 1, 0), (1, 1, 1, 0, 0, 0), 'row'),
- 'grid fast x, -x +y': ((2, 1, 0, 2, 1, 0), (0, 0, 0, 1, 1, 1), 'row'),
- 'grid fast y, +x +y': ((0, 0, 0, 1, 1, 1), (0, 1, 2, 0, 1, 2), 'column'),
- 'grid fast y, +x -y': ((0, 0, 0, 1, 1, 1), (2, 1, 0, 2, 1, 0), 'column'),
- 'grid fast y, -x -y': ((1, 1, 1, 0, 0, 0), (2, 1, 0, 2, 1, 0), 'column'),
- 'grid fast y, -x +y': ((1, 1, 1, 0, 0, 0), (0, 1, 2, 0, 1, 2), 'column'),
- }
-
- self.plot.addScatter((), (), ())
- scatter = self.plot.getItems()[0]
-
- self.qapp.processEvents()
-
- for visualization in (scatter.Visualization.REGULAR_GRID,
- scatter.Visualization.IRREGULAR_GRID):
- scatter.setVisualization(visualization)
- self.assertIs(scatter.getVisualization(), visualization)
-
- for name, (x, y, ref_order) in points.items():
- with self.subTest(name=name, visualization=visualization.name):
- scatter.setData(x, y, numpy.arange(len(x)))
- self.plot.setGraphTitle(name)
- self.plot.resetZoom()
- self.qapp.processEvents()
-
- order = scatter.getCurrentVisualizationParameter(
- scatter.VisualizationParameter.GRID_MAJOR_ORDER)
- self.assertEqual(ref_order, order)
-
- ref_bounds = (x[0], y[0]), (x[-1], y[-1])
- bounds = scatter.getCurrentVisualizationParameter(
- scatter.VisualizationParameter.GRID_BOUNDS)
- self.assertEqual(ref_bounds, bounds)
-
- shape = scatter.getCurrentVisualizationParameter(
- scatter.VisualizationParameter.GRID_SHAPE)
-
- self.plot.getXAxis().setLimits(numpy.min(x) - 1, numpy.max(x) + 1)
- self.plot.getYAxis().setLimits(numpy.min(y) - 1, numpy.max(y) + 1)
- self.qapp.processEvents()
-
- for index, position in enumerate(zip(x, y)):
- xpixel, ypixel = self.plot.dataToPixel(*position)
- result = scatter.pick(xpixel, ypixel)
- self.assertIsNotNone(result)
- self.assertIs(result.getItem(), scatter)
- self.assertEqual(result.getIndices(), (index,))
-
- def testBinnedStatisticVisualization(self):
- """Test binned display"""
- self.plot.addScatter((), (), ())
- scatter = self.plot.getItems()[0]
- scatter.setVisualization(scatter.Visualization.BINNED_STATISTIC)
- self.assertIs(scatter.getVisualization(),
- scatter.Visualization.BINNED_STATISTIC)
- self.assertEqual(
- scatter.getVisualizationParameter(
- scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION),
- 'mean')
-
- self.qapp.processEvents()
-
- scatter.setData(*numpy.random.random(3000).reshape(3, -1))
-
- for reduction in ('count', 'sum', 'mean'):
- with self.subTest(reduction=reduction):
- scatter.setVisualizationParameter(
- scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION,
- reduction)
- self.assertEqual(
- scatter.getVisualizationParameter(
- scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION),
- reduction)
-
- self.qapp.processEvents()
-
-
-class TestPlotMarker(PlotWidgetTestCase):
- """Basic tests for add*Marker"""
-
- def setUp(self):
- super(TestPlotMarker, self).setUp()
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
-
- self.plot.getXAxis().setAutoScale(False)
- self.plot.getYAxis().setAutoScale(False)
- self.plot.setKeepDataAspectRatio(False)
- self.plot.setLimits(0., 100., -100., 100.)
-
- def testPlotMarkerX(self):
- self.plot.setGraphTitle('Markers X')
-
- markers = [
- (10., 'blue', False, False),
- (20., 'red', False, False),
- (40., 'green', True, False),
- (60., 'gray', True, True),
- (80., 'black', False, True),
- ]
-
- for x, color, select, drag in markers:
- name = str(x)
- if select:
- name += " sel."
- if drag:
- name += " drag"
- self.plot.addXMarker(x, name, name, color, select, drag)
- self.plot.resetZoom()
-
- def testPlotMarkerY(self):
- self.plot.setGraphTitle('Markers Y')
-
- markers = [
- (-50., 'blue', False, False),
- (-30., 'red', False, False),
- (0., 'green', True, False),
- (10., 'gray', True, True),
- (80., 'black', False, True),
- ]
-
- for y, color, select, drag in markers:
- name = str(y)
- if select:
- name += " sel."
- if drag:
- name += " drag"
- self.plot.addYMarker(y, name, name, color, select, drag)
- self.plot.resetZoom()
-
- def testPlotMarkerPt(self):
- self.plot.setGraphTitle('Markers Pt')
-
- markers = [
- (10., -50., 'blue', False, False),
- (40., -30., 'red', False, False),
- (50., 0., 'green', True, False),
- (50., 20., 'gray', True, True),
- (70., 50., 'black', False, True),
- ]
- for x, y, color, select, drag in markers:
- name = "{0},{1}".format(x, y)
- if select:
- name += " sel."
- if drag:
- name += " drag"
- self.plot.addMarker(x, y, name, name, color, select, drag)
-
- self.plot.resetZoom()
-
- def testPlotMarkerWithoutLegend(self):
- self.plot.setGraphTitle('Markers without legend')
- self.plot.getYAxis().setInverted(True)
-
- # Markers without legend
- self.plot.addMarker(10, 10)
- self.plot.addMarker(10, 20)
- self.plot.addMarker(40, 50, text='test', symbol=None)
- self.plot.addMarker(40, 50, text='test', symbol='+')
- self.plot.addXMarker(25)
- self.plot.addXMarker(35)
- self.plot.addXMarker(45, text='test')
- self.plot.addYMarker(55)
- self.plot.addYMarker(65)
- self.plot.addYMarker(75, text='test')
-
- self.plot.resetZoom()
-
- def testPlotMarkerYAxis(self):
- # Check only the API
-
- legend = self.plot.addMarker(10, 10)
- item = self.plot._getMarker(legend)
- self.assertEqual(item.getYAxis(), "left")
-
- legend = self.plot.addMarker(10, 10, yaxis="right")
- item = self.plot._getMarker(legend)
- self.assertEqual(item.getYAxis(), "right")
-
- legend = self.plot.addMarker(10, 10, yaxis="left")
- item = self.plot._getMarker(legend)
- self.assertEqual(item.getYAxis(), "left")
-
- legend = self.plot.addXMarker(10, yaxis="right")
- item = self.plot._getMarker(legend)
- self.assertEqual(item.getYAxis(), "right")
-
- legend = self.plot.addXMarker(10, yaxis="left")
- item = self.plot._getMarker(legend)
- self.assertEqual(item.getYAxis(), "left")
-
- legend = self.plot.addYMarker(10, yaxis="right")
- item = self.plot._getMarker(legend)
- self.assertEqual(item.getYAxis(), "right")
-
- legend = self.plot.addYMarker(10, yaxis="left")
- item = self.plot._getMarker(legend)
- self.assertEqual(item.getYAxis(), "left")
-
- self.plot.resetZoom()
-
-
-# TestPlotItem ################################################################
-
-class TestPlotItem(PlotWidgetTestCase):
- """Basic tests for addItem."""
-
- # Polygon coordinates and color
- POLYGONS = [ # legend, x coords, y coords, color
- ('triangle', numpy.array((10, 30, 50)),
- numpy.array((55, 70, 55)), 'red'),
- ('square', numpy.array((10, 10, 50, 50)),
- numpy.array((10, 50, 50, 10)), 'green'),
- ('star', numpy.array((60, 70, 80, 60, 80)),
- numpy.array((25, 50, 25, 40, 40)), 'blue'),
- ('2 triangles-simple',
- numpy.array((90., 95., 100., numpy.nan, 90., 95., 100.)),
- numpy.array((25., 5., 25., numpy.nan, 30., 50., 30.)),
- 'pink'),
- ('2 triangles-extra NaN',
- numpy.array((numpy.nan, 90., 95., 100., numpy.nan, 0., 90., 95., 100., numpy.nan)),
- numpy.array((0., 55., 70., 55., numpy.nan, numpy.nan, 75., 90., 75., numpy.nan)),
- 'black'),
- ]
-
- # Rectangle coordinantes and color
- RECTANGLES = [ # legend, x coords, y coords, color
- ('square 1', numpy.array((1., 10.)),
- numpy.array((1., 10.)), 'red'),
- ('square 2', numpy.array((10., 20.)),
- numpy.array((10., 20.)), 'green'),
- ('square 3', numpy.array((20., 30.)),
- numpy.array((20., 30.)), 'blue'),
- ('rect 1', numpy.array((1., 30.)),
- numpy.array((35., 40.)), 'black'),
- ('line h', numpy.array((1., 30.)),
- numpy.array((45., 45.)), 'darkRed'),
- ]
-
- SCALES = Axis.LINEAR, Axis.LOGARITHMIC
-
- def setUp(self):
- super(TestPlotItem, self).setUp()
-
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
- self.plot.getXAxis().setAutoScale(False)
- self.plot.getYAxis().setAutoScale(False)
- self.plot.setKeepDataAspectRatio(False)
- self.plot.setLimits(0., 100., -100., 100.)
-
- def testPlotItemPolygonFill(self):
- for scale in self.SCALES:
- with self.subTest(scale=scale):
- self.plot.clear()
- self.plot.getXAxis().setScale(scale)
- self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Item Fill %s' % scale)
-
- for legend, xList, yList, color in self.POLYGONS:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False, linestyle='--',
- shape="polygon", fill=True, color=color)
- self.plot.resetZoom()
-
- def testPlotItemPolygonNoFill(self):
- for scale in self.SCALES:
- with self.subTest(scale=scale):
- self.plot.clear()
- self.plot.getXAxis().setScale(scale)
- self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Item No Fill %s' % scale)
-
- for legend, xList, yList, color in self.POLYGONS:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False, linestyle='--',
- shape="polygon", fill=False, color=color)
- self.plot.resetZoom()
-
- def testPlotItemRectangleFill(self):
- for scale in self.SCALES:
- with self.subTest(scale=scale):
- self.plot.clear()
- self.plot.getXAxis().setScale(scale)
- self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Rectangle Fill %s' % scale)
-
- for legend, xList, yList, color in self.RECTANGLES:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False,
- shape="rectangle", fill=True, color=color)
- self.plot.resetZoom()
-
- def testPlotItemRectangleNoFill(self):
- for scale in self.SCALES:
- with self.subTest(scale=scale):
- self.plot.clear()
- self.plot.getXAxis().setScale(scale)
- self.plot.getYAxis().setScale(scale)
- self.plot.setGraphTitle('Rectangle No Fill %s' % scale)
-
- for legend, xList, yList, color in self.RECTANGLES:
- self.plot.addShape(xList, yList, legend=legend,
- replace=False,
- shape="rectangle", fill=False, color=color)
- self.plot.resetZoom()
-
-
-class TestPlotActiveCurveImage(PlotWidgetTestCase):
- """Basic tests for active curve and image handling"""
- xData = numpy.arange(1000)
- yData = -500 + 100 * numpy.sin(xData)
- xData2 = xData + 1000
- yData2 = xData - 1000 + 200 * numpy.random.random(1000)
-
- def tearDown(self):
- self.plot.setActiveCurveHandling(False)
- super(TestPlotActiveCurveImage, self).tearDown()
-
- def testActiveCurveAndLabels(self):
- # Active curve handling off, no label change
- self.plot.setActiveCurveHandling(False)
- self.plot.getXAxis().setLabel('XLabel')
- self.plot.getYAxis().setLabel('YLabel')
- self.plot.addCurve((1, 2), (1, 2))
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.addCurve((1, 2), (2, 3), xlabel='x1', ylabel='y1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.clear()
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- # Active curve handling on, label changes
- self.plot.setActiveCurveHandling(True)
- self.plot.getXAxis().setLabel('XLabel')
- self.plot.getYAxis().setLabel('YLabel')
-
- # labels changed as active curve
- self.plot.addCurve((1, 2), (1, 2), legend='1',
- xlabel='x1', ylabel='y1')
- self.plot.setActiveCurve('1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels not changed as not active curve
- self.plot.addCurve((1, 2), (2, 3), legend='2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels changed
- self.plot.setActiveCurve('2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.setActiveCurve('1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- self.plot.clear()
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- def testPlotActiveCurveSelectionMode(self):
- self.plot.clear()
- self.plot.setActiveCurveHandling(True)
- legend = "curve 1"
- self.plot.addCurve(self.xData, self.yData,
- legend=legend,
- color="green")
-
- # active curve should be None
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
-
- # active curve should be None when None is set as active curve
- self.plot.setActiveCurve(legend)
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, legend)
- self.plot.setActiveCurve(None)
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, None)
-
- # testing it automatically toggles if there is only one
- self.plot.setActiveCurveSelectionMode("legacy")
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, legend)
-
- # active curve should not change when None set as active curve
- self.assertEqual(self.plot.getActiveCurveSelectionMode(), "legacy")
- self.plot.setActiveCurve(None)
- current = self.plot.getActiveCurve(just_legend=True)
- self.assertEqual(current, legend)
-
- # situation where no curve is active
- self.plot.clear()
- self.plot.setActiveCurveHandling(True)
- self.assertEqual(self.plot.getActiveCurveSelectionMode(), "atmostone")
- self.plot.addCurve(self.xData, self.yData,
- legend=legend,
- color="green")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- color="red")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
- self.plot.setActiveCurveSelectionMode("legacy")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), None)
-
- # the first curve added should be active
- self.plot.clear()
- self.plot.addCurve(self.xData, self.yData,
- legend=legend,
- color="green")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), legend)
- self.plot.addCurve(self.xData2, self.yData2,
- legend="curve 2",
- color="red")
- self.assertEqual(self.plot.getActiveCurve(just_legend=True), legend)
-
- def testActiveCurveStyle(self):
- """Test change of active curve style"""
- self.plot.setActiveCurveHandling(True)
- self.plot.setActiveCurveStyle(color='black')
- style = self.plot.getActiveCurveStyle()
- self.assertEqual(style.getColor(), (0., 0., 0., 1.))
- self.assertIsNone(style.getLineStyle())
- self.assertIsNone(style.getLineWidth())
- self.assertIsNone(style.getSymbol())
- self.assertIsNone(style.getSymbolSize())
-
- self.plot.addCurve(x=self.xData, y=self.yData, legend="curve1")
- curve = self.plot.getCurve("curve1")
- curve.setColor('blue')
- curve.setLineStyle('-')
- curve.setLineWidth(1)
- curve.setSymbol('o')
- curve.setSymbolSize(5)
-
- # Check default current style
- defaultStyle = curve.getCurrentStyle()
- self.assertEqual(defaultStyle, CurveStyle(color='blue',
- linestyle='-',
- linewidth=1,
- symbol='o',
- symbolsize=5))
-
- # Activate curve with highlight color=black
- self.plot.setActiveCurve("curve1")
- style = curve.getCurrentStyle()
- self.assertEqual(style.getColor(), (0., 0., 0., 1.))
- self.assertEqual(style.getLineStyle(), '-')
- self.assertEqual(style.getLineWidth(), 1)
- self.assertEqual(style.getSymbol(), 'o')
- self.assertEqual(style.getSymbolSize(), 5)
-
- # Change highlight to linewidth=2
- self.plot.setActiveCurveStyle(linewidth=2)
- style = curve.getCurrentStyle()
- self.assertEqual(style.getColor(), (0., 0., 1., 1.))
- self.assertEqual(style.getLineStyle(), '-')
- self.assertEqual(style.getLineWidth(), 2)
- self.assertEqual(style.getSymbol(), 'o')
- self.assertEqual(style.getSymbolSize(), 5)
-
- self.plot.setActiveCurve(None)
- self.assertEqual(curve.getCurrentStyle(), defaultStyle)
-
- def testActiveImageAndLabels(self):
- # Active image handling always on, no API for toggling it
- self.plot.getXAxis().setLabel('XLabel')
- self.plot.getYAxis().setLabel('YLabel')
-
- # labels changed as active curve
- self.plot.addImage(numpy.arange(100).reshape(10, 10),
- legend='1', xlabel='x1', ylabel='y1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels not changed as not active curve
- self.plot.addImage(numpy.arange(100).reshape(10, 10),
- legend='2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- # labels changed
- self.plot.setActiveImage('2')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
- self.plot.setActiveImage('1')
- self.assertEqual(self.plot.getXAxis().getLabel(), 'x1')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'y1')
-
- self.plot.clear()
- self.assertEqual(self.plot.getXAxis().getLabel(), 'XLabel')
- self.assertEqual(self.plot.getYAxis().getLabel(), 'YLabel')
-
-
-##############################################################################
-# Log
-##############################################################################
-
-class TestPlotEmptyLog(PlotWidgetTestCase):
- """Basic tests for log plot"""
- def testEmptyPlotTitleLabelsLog(self):
- self.plot.setGraphTitle('Empty Log Log')
- self.plot.getXAxis().setLabel('X')
- self.plot.getYAxis().setLabel('Y')
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.getYAxis()._setLogarithmic(True)
- self.plot.resetZoom()
-
-
-class TestPlotAxes(TestCaseQt, ParametricTestCase):
-
- # Test data
- xData = numpy.arange(1, 10)
- yData = xData ** 2
-
- def __init__(self, methodName='runTest', backend=None):
- unittest.TestCase.__init__(self, methodName)
- self.__backend = backend
-
- def setUp(self):
- super(TestPlotAxes, self).setUp()
- self.plot = PlotWidget(backend=self.__backend)
- # It is not needed to display the plot
- # It saves a lot of time
- # self.plot.show()
- # self.qWaitForWindowExposed(self.plot)
-
- def tearDown(self):
- self.qapp.processEvents()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- super(TestPlotAxes, self).tearDown()
-
- def testDefaultAxes(self):
- axis = self.plot.getXAxis()
- self.assertEqual(axis.getScale(), axis.LINEAR)
- axis = self.plot.getYAxis()
- self.assertEqual(axis.getScale(), axis.LINEAR)
- axis = self.plot.getYAxis(axis="right")
- self.assertEqual(axis.getScale(), axis.LINEAR)
-
- def testOldPlotAxis_getterSetter(self):
- """Test silx API prior to silx 0.6"""
- x = self.plot.getXAxis()
- y = self.plot.getYAxis()
- p = self.plot
-
- tests = [
- # setters
- (p.setGraphXLimits, (10, 20), x.getLimits, (10, 20)),
- (p.setGraphYLimits, (10, 20), y.getLimits, (10, 20)),
- (p.setGraphXLabel, "foox", x.getLabel, "foox"),
- (p.setGraphYLabel, "fooy", y.getLabel, "fooy"),
- (p.setYAxisInverted, True, y.isInverted, True),
- (p.setXAxisLogarithmic, True, x.getScale, x.LOGARITHMIC),
- (p.setYAxisLogarithmic, True, y.getScale, y.LOGARITHMIC),
- (p.setXAxisAutoScale, False, x.isAutoScale, False),
- (p.setYAxisAutoScale, False, y.isAutoScale, False),
- # getters
- (x.setLimits, (11, 20), p.getGraphXLimits, (11, 20)),
- (y.setLimits, (11, 20), p.getGraphYLimits, (11, 20)),
- (x.setLabel, "fooxx", p.getGraphXLabel, "fooxx"),
- (y.setLabel, "fooyy", p.getGraphYLabel, "fooyy"),
- (y.setInverted, False, p.isYAxisInverted, False),
- (x.setScale, x.LINEAR, p.isXAxisLogarithmic, False),
- (y.setScale, y.LINEAR, p.isYAxisLogarithmic, False),
- (x.setAutoScale, True, p.isXAxisAutoScale, True),
- (y.setAutoScale, True, p.isYAxisAutoScale, True),
- ]
- for testCase in tests:
- setter, value, getter, expected = testCase
- with self.subTest():
- if setter is not None:
- if not isinstance(value, tuple):
- value = (value, )
- setter(*value)
- if getter is not None:
- self.assertEqual(getter(), expected)
-
- def testOldPlotAxis_Logarithmic(self):
- """Test silx API prior to silx 0.6"""
- x = self.plot.getXAxis()
- y = self.plot.getYAxis()
- yright = self.plot.getYAxis(axis="right")
-
- self.assertEqual(x.getScale(), x.LINEAR)
- self.assertEqual(y.getScale(), x.LINEAR)
- self.assertEqual(yright.getScale(), x.LINEAR)
-
- self.plot.setXAxisLogarithmic(True)
- self.assertEqual(x.getScale(), x.LOGARITHMIC)
- self.assertEqual(y.getScale(), x.LINEAR)
- self.assertEqual(yright.getScale(), x.LINEAR)
- self.assertEqual(self.plot.isXAxisLogarithmic(), True)
- self.assertEqual(self.plot.isYAxisLogarithmic(), False)
-
- self.plot.setYAxisLogarithmic(True)
- self.assertEqual(x.getScale(), x.LOGARITHMIC)
- self.assertEqual(y.getScale(), x.LOGARITHMIC)
- self.assertEqual(yright.getScale(), x.LOGARITHMIC)
- self.assertEqual(self.plot.isXAxisLogarithmic(), True)
- self.assertEqual(self.plot.isYAxisLogarithmic(), True)
-
- yright.setScale(yright.LINEAR)
- self.assertEqual(x.getScale(), x.LOGARITHMIC)
- self.assertEqual(y.getScale(), x.LINEAR)
- self.assertEqual(yright.getScale(), x.LINEAR)
- self.assertEqual(self.plot.isXAxisLogarithmic(), True)
- self.assertEqual(self.plot.isYAxisLogarithmic(), False)
-
- def testOldPlotAxis_AutoScale(self):
- """Test silx API prior to silx 0.6"""
- x = self.plot.getXAxis()
- y = self.plot.getYAxis()
- yright = self.plot.getYAxis(axis="right")
-
- self.assertEqual(x.isAutoScale(), True)
- self.assertEqual(y.isAutoScale(), True)
- self.assertEqual(yright.isAutoScale(), True)
-
- self.plot.setXAxisAutoScale(False)
- self.assertEqual(x.isAutoScale(), False)
- self.assertEqual(y.isAutoScale(), True)
- self.assertEqual(yright.isAutoScale(), True)
- self.assertEqual(self.plot.isXAxisAutoScale(), False)
- self.assertEqual(self.plot.isYAxisAutoScale(), True)
-
- self.plot.setYAxisAutoScale(False)
- self.assertEqual(x.isAutoScale(), False)
- self.assertEqual(y.isAutoScale(), False)
- self.assertEqual(yright.isAutoScale(), False)
- self.assertEqual(self.plot.isXAxisAutoScale(), False)
- self.assertEqual(self.plot.isYAxisAutoScale(), False)
-
- yright.setAutoScale(True)
- self.assertEqual(x.isAutoScale(), False)
- self.assertEqual(y.isAutoScale(), True)
- self.assertEqual(yright.isAutoScale(), True)
- self.assertEqual(self.plot.isXAxisAutoScale(), False)
- self.assertEqual(self.plot.isYAxisAutoScale(), True)
-
- def testOldPlotAxis_Inverted(self):
- """Test silx API prior to silx 0.6"""
- x = self.plot.getXAxis()
- y = self.plot.getYAxis()
- yright = self.plot.getYAxis(axis="right")
-
- self.assertEqual(x.isInverted(), False)
- self.assertEqual(y.isInverted(), False)
- self.assertEqual(yright.isInverted(), False)
-
- self.plot.setYAxisInverted(True)
- self.assertEqual(x.isInverted(), False)
- self.assertEqual(y.isInverted(), True)
- self.assertEqual(yright.isInverted(), True)
- self.assertEqual(self.plot.isYAxisInverted(), True)
-
- yright.setInverted(False)
- self.assertEqual(x.isInverted(), False)
- self.assertEqual(y.isInverted(), False)
- self.assertEqual(yright.isInverted(), False)
- self.assertEqual(self.plot.isYAxisInverted(), False)
-
- def testLogXWithData(self):
- self.plot.setGraphTitle('Curve X: Log Y: Linear')
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
- axis = self.plot.getXAxis()
- axis.setScale(axis.LOGARITHMIC)
-
- self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
-
- def testLogYWithData(self):
- self.plot.setGraphTitle('Curve X: Linear Y: Log')
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
- axis = self.plot.getYAxis()
- axis.setScale(axis.LOGARITHMIC)
-
- self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
- axis = self.plot.getYAxis(axis="right")
- self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
-
- def testLogYRightWithData(self):
- self.plot.setGraphTitle('Curve X: Linear Y: Log')
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
- axis = self.plot.getYAxis(axis="right")
- axis.setScale(axis.LOGARITHMIC)
-
- self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
- axis = self.plot.getYAxis()
- self.assertEqual(axis.getScale(), axis.LOGARITHMIC)
-
- def testLimitsChanged_setLimits(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
- listener = SignalListener()
- self.plot.getXAxis().sigLimitsChanged.connect(listener.partial(axis="x"))
- self.plot.getYAxis().sigLimitsChanged.connect(listener.partial(axis="y"))
- self.plot.getYAxis(axis="right").sigLimitsChanged.connect(listener.partial(axis="y2"))
- self.plot.setLimits(0, 1, 0, 1, 0, 1)
- # at least one event per axis
- self.assertEqual(len(set(listener.karguments(argumentName="axis"))), 3)
-
- def testLimitsChanged_resetZoom(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
- listener = SignalListener()
- self.plot.getXAxis().sigLimitsChanged.connect(listener.partial(axis="x"))
- self.plot.getYAxis().sigLimitsChanged.connect(listener.partial(axis="y"))
- self.plot.getYAxis(axis="right").sigLimitsChanged.connect(listener.partial(axis="y2"))
- self.plot.resetZoom()
- # at least one event per axis
- self.assertEqual(len(set(listener.karguments(argumentName="axis"))), 3)
-
- def testLimitsChanged_setXLimit(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
- listener = SignalListener()
- axis = self.plot.getXAxis()
- axis.sigLimitsChanged.connect(listener)
- axis.setLimits(20, 30)
- # at least one event per axis
- self.assertEqual(listener.arguments(callIndex=-1), (20.0, 30.0))
- self.assertEqual(axis.getLimits(), (20.0, 30.0))
-
- def testLimitsChanged_setYLimit(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
- listener = SignalListener()
- axis = self.plot.getYAxis()
- axis.sigLimitsChanged.connect(listener)
- axis.setLimits(20, 30)
- # at least one event per axis
- self.assertEqual(listener.arguments(callIndex=-1), (20.0, 30.0))
- self.assertEqual(axis.getLimits(), (20.0, 30.0))
-
- def testLimitsChanged_setYRightLimit(self):
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=False,
- color='green', linestyle="-", symbol='o')
- listener = SignalListener()
- axis = self.plot.getYAxis(axis="right")
- axis.sigLimitsChanged.connect(listener)
- axis.setLimits(20, 30)
- # at least one event per axis
- self.assertEqual(listener.arguments(callIndex=-1), (20.0, 30.0))
- self.assertEqual(axis.getLimits(), (20.0, 30.0))
-
- def testScaleProxy(self):
- listener = SignalListener()
- y = self.plot.getYAxis()
- yright = self.plot.getYAxis(axis="right")
- y.sigScaleChanged.connect(listener.partial("left"))
- yright.sigScaleChanged.connect(listener.partial("right"))
- yright.setScale(yright.LOGARITHMIC)
-
- self.assertEqual(y.getScale(), y.LOGARITHMIC)
- events = listener.arguments()
- self.assertEqual(len(events), 2)
- self.assertIn(("left", y.LOGARITHMIC), events)
- self.assertIn(("right", y.LOGARITHMIC), events)
-
- def testAutoScaleProxy(self):
- listener = SignalListener()
- y = self.plot.getYAxis()
- yright = self.plot.getYAxis(axis="right")
- y.sigAutoScaleChanged.connect(listener.partial("left"))
- yright.sigAutoScaleChanged.connect(listener.partial("right"))
- yright.setAutoScale(False)
-
- self.assertEqual(y.isAutoScale(), False)
- events = listener.arguments()
- self.assertEqual(len(events), 2)
- self.assertIn(("left", False), events)
- self.assertIn(("right", False), events)
-
- def testInvertedProxy(self):
- listener = SignalListener()
- y = self.plot.getYAxis()
- yright = self.plot.getYAxis(axis="right")
- y.sigInvertedChanged.connect(listener.partial("left"))
- yright.sigInvertedChanged.connect(listener.partial("right"))
- yright.setInverted(True)
-
- self.assertEqual(y.isInverted(), True)
- events = listener.arguments()
- self.assertEqual(len(events), 2)
- self.assertIn(("left", True), events)
- self.assertIn(("right", True), events)
-
- def testAxesDisplayedFalse(self):
- """Test coverage on setAxesDisplayed(False)"""
- self.plot.setAxesDisplayed(False)
-
- def testAxesDisplayedTrue(self):
- """Test coverage on setAxesDisplayed(True)"""
- self.plot.setAxesDisplayed(True)
-
- def testAxesMargins(self):
- """Test PlotWidget's getAxesMargins and setAxesMargins"""
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- margins = self.plot.getAxesMargins()
- self.assertEqual(margins, (.15, .1, .1, .15))
-
- for margins in ((0., 0., 0., 0.), (.15, .1, .1, .15)):
- with self.subTest(margins=margins):
- self.plot.setAxesMargins(*margins)
- self.qapp.processEvents()
- self.assertEqual(self.plot.getAxesMargins(), margins)
-
- def testBoundingRectItem(self):
- item = BoundingRect()
- item.setBounds((-1000, 1000, -2000, 2000))
- self.plot.addItem(item)
- self.plot.resetZoom()
- limits = numpy.array(self.plot.getXAxis().getLimits())
- numpy.testing.assert_almost_equal(limits, numpy.array([-1000, 1000]))
- limits = numpy.array(self.plot.getYAxis().getLimits())
- numpy.testing.assert_almost_equal(limits, numpy.array([-2000, 2000]))
-
- def testBoundingRectRightItem(self):
- item = BoundingRect()
- item.setYAxis("right")
- item.setBounds((-1000, 1000, -2000, 2000))
- self.plot.addItem(item)
- self.plot.resetZoom()
- limits = numpy.array(self.plot.getXAxis().getLimits())
- numpy.testing.assert_almost_equal(limits, numpy.array([-1000, 1000]))
- limits = numpy.array(self.plot.getYAxis("right").getLimits())
- numpy.testing.assert_almost_equal(limits, numpy.array([-2000, 2000]))
-
- def testBoundingRectArguments(self):
- item = BoundingRect()
- with self.assertRaises(Exception):
- item.setBounds((1000, -1000, -2000, 2000))
- with self.assertRaises(Exception):
- item.setBounds((-1000, 1000, 2000, -2000))
-
- def testBoundingRectWithLog(self):
- item = BoundingRect()
- self.plot.addItem(item)
-
- item.setBounds((-1000, 1000, -2000, 2000))
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.getYAxis()._setLogarithmic(False)
- self.assertEqual(item.getBounds(), (1000, 1000, -2000, 2000))
-
- item.setBounds((-1000, 1000, -2000, 2000))
- self.plot.getXAxis()._setLogarithmic(False)
- self.plot.getYAxis()._setLogarithmic(True)
- self.assertEqual(item.getBounds(), (-1000, 1000, 2000, 2000))
-
- item.setBounds((-1000, 0, -2000, 2000))
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.getYAxis()._setLogarithmic(False)
- self.assertIsNone(item.getBounds())
-
- def testAxisExtent(self):
- """Test XAxisExtent and yAxisExtent"""
- for cls, axis in ((XAxisExtent, self.plot.getXAxis()),
- (YAxisExtent, self.plot.getYAxis())):
- for range_, logRange in (((2, 3), (2, 3)),
- ((-2, -1), (1, 100)),
- ((-1, 3), (3. * 0.9, 3. * 1.1))):
- extent = cls()
- extent.setRange(*range_)
- self.plot.addItem(extent)
-
- for isLog, plotRange in ((False, range_), (True, logRange)):
- with self.subTest(
- cls=cls.__name__, range=range_, isLog=isLog):
- axis._setLogarithmic(isLog)
- self.plot.resetZoom()
- self.qapp.processEvents()
- self.assertEqual(axis.getLimits(), plotRange)
-
- axis._setLogarithmic(False)
- self.plot.clear()
-
-
-class TestPlotCurveLog(PlotWidgetTestCase, ParametricTestCase):
- """Basic tests for addCurve with log scale axes"""
-
- # Test data
- xData = numpy.arange(1000) + 1
- yData = xData ** 2
-
- def _setLabels(self):
- self.plot.getXAxis().setLabel('X')
- self.plot.getYAxis().setLabel('X * X')
-
- def testPlotCurveLogX(self):
- self._setLabels()
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('Curve X: Log Y: Linear')
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
-
- def testPlotCurveLogY(self):
- self._setLabels()
- self.plot.getYAxis()._setLogarithmic(True)
-
- self.plot.setGraphTitle('Curve X: Linear Y: Log')
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
-
- def testPlotCurveLogXY(self):
- self._setLabels()
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.getYAxis()._setLogarithmic(True)
-
- self.plot.setGraphTitle('Curve X: Log Y: Log')
-
- self.plot.addCurve(self.xData, self.yData,
- legend="curve",
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
-
- def testPlotCurveErrorLogXY(self):
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.getYAxis()._setLogarithmic(True)
-
- # Every second error leads to negative number
- errors = numpy.ones_like(self.xData)
- errors[::2] = self.xData[::2] + 1
-
- tests = [ # name, xerror, yerror
- ('xerror=3', 3, None),
- ('xerror=N array', errors, None),
- ('xerror=Nx1 array', errors.reshape(len(errors), 1), None),
- ('xerror=2xN array', numpy.array((errors, errors)), None),
- ('yerror=6', None, 6),
- ('yerror=N array', None, errors ** 2),
- ('yerror=Nx1 array', None, (errors ** 2).reshape(len(errors), 1)),
- ('yerror=2xN array', None, numpy.array((errors, errors)) ** 2),
- ]
-
- for name, xError, yError in tests:
- with self.subTest(name):
- self.plot.setGraphTitle(name)
- self.plot.addCurve(self.xData, self.yData,
- legend=name,
- xerror=xError, yerror=yError,
- replace=False, resetzoom=True,
- color='green', linestyle="-", symbol='o')
-
- self.qapp.processEvents()
-
- self.plot.clear()
- self.plot.resetZoom()
- self.qapp.processEvents()
-
- def testPlotCurveToggleLog(self):
- """Add a curve with negative data and toggle log axis"""
- arange = numpy.arange(1000) + 1
- tests = [ # name, xData, yData
- ('x>0, some negative y', arange, arange - 500),
- ('x>0, y<0', arange, -arange),
- ('some negative x, y>0', arange - 500, arange),
- ('x<0, y>0', -arange, arange),
- ('some negative x and y', arange - 500, arange - 500),
- ('x<0, y<0', -arange, -arange),
- ]
-
- for name, xData, yData in tests:
- with self.subTest(name):
- self.plot.addCurve(xData, yData, resetzoom=True)
- self.qapp.processEvents()
-
- # no log axis
- xLim = self.plot.getXAxis().getLimits()
- self.assertEqual(xLim, (min(xData), max(xData)))
- yLim = self.plot.getYAxis().getLimits()
- self.assertEqual(yLim, (min(yData), max(yData)))
-
- # x axis log
- self.plot.getXAxis()._setLogarithmic(True)
- self.qapp.processEvents()
-
- xLim = self.plot.getXAxis().getLimits()
- yLim = self.plot.getYAxis().getLimits()
- positives = xData > 0
- if numpy.any(positives):
- self.assertTrue(numpy.allclose(
- xLim, (min(xData[positives]), max(xData[positives]))))
- self.assertEqual(
- yLim, (min(yData[positives]), max(yData[positives])))
- else: # No positive x in the curve
- self.assertEqual(xLim, (1., 100.))
- self.assertEqual(yLim, (1., 100.))
-
- # x axis and y axis log
- self.plot.getYAxis()._setLogarithmic(True)
- self.qapp.processEvents()
-
- xLim = self.plot.getXAxis().getLimits()
- yLim = self.plot.getYAxis().getLimits()
- positives = numpy.logical_and(xData > 0, yData > 0)
- if numpy.any(positives):
- self.assertTrue(numpy.allclose(
- xLim, (min(xData[positives]), max(xData[positives]))))
- self.assertTrue(numpy.allclose(
- yLim, (min(yData[positives]), max(yData[positives]))))
- else: # No positive x and y in the curve
- self.assertEqual(xLim, (1., 100.))
- self.assertEqual(yLim, (1., 100.))
-
- # y axis log
- self.plot.getXAxis()._setLogarithmic(False)
- self.qapp.processEvents()
-
- xLim = self.plot.getXAxis().getLimits()
- yLim = self.plot.getYAxis().getLimits()
- positives = yData > 0
- if numpy.any(positives):
- self.assertEqual(
- xLim, (min(xData[positives]), max(xData[positives])))
- self.assertTrue(numpy.allclose(
- yLim, (min(yData[positives]), max(yData[positives]))))
- else: # No positive y in the curve
- self.assertEqual(xLim, (1., 100.))
- self.assertEqual(yLim, (1., 100.))
-
- # no log axis
- self.plot.getYAxis()._setLogarithmic(False)
- self.qapp.processEvents()
-
- xLim = self.plot.getXAxis().getLimits()
- self.assertEqual(xLim, (min(xData), max(xData)))
- yLim = self.plot.getYAxis().getLimits()
- self.assertEqual(yLim, (min(yData), max(yData)))
-
- self.plot.clear()
- self.plot.resetZoom()
- self.qapp.processEvents()
-
-
-class TestPlotImageLog(PlotWidgetTestCase):
- """Basic tests for addImage with log scale axes."""
-
- def setUp(self):
- super(TestPlotImageLog, self).setUp()
-
- self.plot.getXAxis().setLabel('Columns')
- self.plot.getYAxis().setLabel('Rows')
-
- def testPlotColormapGrayLogX(self):
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('CMap X: Log Y: Linear')
-
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1",
- origin=(1., 1.), scale=(1., 1.),
- resetzoom=False, colormap=colormap)
- self.plot.resetZoom()
-
- def testPlotColormapGrayLogY(self):
- self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('CMap X: Linear Y: Log')
-
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1",
- origin=(1., 1.), scale=(1., 1.),
- resetzoom=False, colormap=colormap)
- self.plot.resetZoom()
-
- def testPlotColormapGrayLogXY(self):
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('CMap X: Log Y: Log')
-
- colormap = Colormap(name='gray',
- normalization='linear',
- vmin=None,
- vmax=None)
- self.plot.addImage(DATA_2D, legend="image 1",
- origin=(1., 1.), scale=(1., 1.),
- resetzoom=False, colormap=colormap)
- self.plot.resetZoom()
-
- def testPlotRgbRgbaLogXY(self):
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.getYAxis()._setLogarithmic(True)
- self.plot.setGraphTitle('RGB + RGBA X: Log Y: Log')
-
- rgb = numpy.array(
- (((0, 0, 0), (128, 0, 0), (255, 0, 0)),
- ((0, 128, 0), (0, 128, 128), (0, 128, 256))),
- dtype=numpy.uint8)
-
- self.plot.addImage(rgb, legend="rgb",
- origin=(1, 1), scale=(10, 10),
- resetzoom=False)
-
- rgba = numpy.array(
- (((0, 0, 0, .5), (.5, 0, 0, 1), (1, 0, 0, .5)),
- ((0, .5, 0, 1), (0, .5, .5, 1), (0, 1, 1, .5))),
- dtype=numpy.float32)
-
- self.plot.addImage(rgba, legend="rgba",
- origin=(5., 5.), scale=(10., 10.),
- resetzoom=False)
- self.plot.resetZoom()
-
-
-class TestPlotMarkerLog(PlotWidgetTestCase):
- """Basic tests for markers on log scales"""
-
- # Test marker parameters
- markers = [ # x, y, color, selectable, draggable
- (10., 10., 'blue', False, False),
- (20., 20., 'red', False, False),
- (40., 100., 'green', True, False),
- (40., 500., 'gray', True, True),
- (60., 800., 'black', False, True),
- ]
-
- def setUp(self):
- super(TestPlotMarkerLog, self).setUp()
-
- self.plot.getYAxis().setLabel('Rows')
- self.plot.getXAxis().setLabel('Columns')
- self.plot.getXAxis().setAutoScale(False)
- self.plot.getYAxis().setAutoScale(False)
- self.plot.setKeepDataAspectRatio(False)
- self.plot.setLimits(1., 100., 1., 1000.)
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.getYAxis()._setLogarithmic(True)
-
- def testPlotMarkerXLog(self):
- self.plot.setGraphTitle('Markers X, Log axes')
-
- for x, _, color, select, drag in self.markers:
- name = str(x)
- if select:
- name += " sel."
- if drag:
- name += " drag"
- self.plot.addXMarker(x, name, name, color, select, drag)
- self.plot.resetZoom()
-
- def testPlotMarkerYLog(self):
- self.plot.setGraphTitle('Markers Y, Log axes')
-
- for _, y, color, select, drag in self.markers:
- name = str(y)
- if select:
- name += " sel."
- if drag:
- name += " drag"
- self.plot.addYMarker(y, name, name, color, select, drag)
- self.plot.resetZoom()
-
- def testPlotMarkerPtLog(self):
- self.plot.setGraphTitle('Markers Pt, Log axes')
-
- for x, y, color, select, drag in self.markers:
- name = "{0},{1}".format(x, y)
- if select:
- name += " sel."
- if drag:
- name += " drag"
- self.plot.addMarker(x, y, name, name, color, select, drag)
- self.plot.resetZoom()
-
-
-class TestPlotWidgetSwitchBackend(PlotWidgetTestCase):
- """Test [get|set]Backend to switch backend"""
-
- def testSwitchBackend(self):
- """Test switching a plot with a few items"""
- backends = {'none': 'BackendBase', 'mpl': 'BackendMatplotlibQt'}
- if test_options.WITH_GL_TEST:
- backends['gl'] = 'BackendOpenGL'
-
- self.plot.addImage(numpy.arange(100).reshape(10, 10))
- self.plot.addCurve((-3, -2, -1), (1, 2, 3))
- self.plot.resetZoom()
- xlimits = self.plot.getXAxis().getLimits()
- ylimits = self.plot.getYAxis().getLimits()
- items = self.plot.getItems()
- self.assertEqual(len(items), 2)
-
- for backend, className in backends.items():
- with self.subTest(backend=backend):
- self.plot.setBackend(backend)
- self.plot.replot()
-
- retrievedBackend = self.plot.getBackend()
- self.assertEqual(type(retrievedBackend).__name__, className)
- self.assertEqual(self.plot.getXAxis().getLimits(), xlimits)
- self.assertEqual(self.plot.getYAxis().getLimits(), ylimits)
- self.assertEqual(self.plot.getItems(), items)
-
-
-class TestPlotWidgetSelection(PlotWidgetTestCase):
- """Test PlotWidget.selection and active items handling"""
-
- def _checkSelection(self, selection, current=None, selected=()):
- """Check current item and selected items."""
- self.assertIs(selection.getCurrentItem(), current)
- self.assertEqual(selection.getSelectedItems(), selected)
-
- def testSyncWithActiveItems(self):
- """Test update of PlotWidgetSelection according to active items"""
- listener = SignalListener()
-
- selection = self.plot.selection()
- selection.sigCurrentItemChanged.connect(listener)
- self._checkSelection(selection)
-
- # Active item is current
- self.plot.addImage(((0, 1), (2, 3)), legend='image')
- image = self.plot.getActiveImage()
- self.assertEqual(listener.callCount(), 1)
- self._checkSelection(selection, image, (image,))
-
- # No active = no current
- self.plot.setActiveImage(None)
- self.assertEqual(listener.callCount(), 2)
- self._checkSelection(selection)
-
- # Active item is current
- self.plot.setActiveImage('image')
- self.assertEqual(listener.callCount(), 3)
- self._checkSelection(selection, image, (image,))
-
- # Mosted recently "actived" item is current
- self.plot.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend='scatter')
- scatter = self.plot.getActiveScatter()
- self.assertEqual(listener.callCount(), 4)
- self._checkSelection(selection, scatter, (scatter, image))
-
- # Previously mosted recently "actived" item is current
- self.plot.setActiveScatter(None)
- self.assertEqual(listener.callCount(), 5)
- self._checkSelection(selection, image, (image,))
-
- # Mosted recently "actived" item is current
- self.plot.setActiveScatter('scatter')
- self.assertEqual(listener.callCount(), 6)
- self._checkSelection(selection, scatter, (scatter, image))
-
- # No active = no current
- self.plot.setActiveImage(None)
- self.plot.setActiveScatter(None)
- self.assertEqual(listener.callCount(), 7)
- self._checkSelection(selection)
-
- # Mosted recently "actived" item is current
- self.plot.setActiveScatter('scatter')
- self.assertEqual(listener.callCount(), 8)
- self.plot.setActiveImage('image')
- self.assertEqual(listener.callCount(), 9)
- self._checkSelection(selection, image, (image, scatter))
-
- # Add a curve which is not active by default
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve')
- curve = self.plot.getCurve('curve')
- self.assertEqual(listener.callCount(), 9)
- self._checkSelection(selection, image, (image, scatter))
-
- # Mosted recently "actived" item is current
- self.plot.setActiveCurve('curve')
- self.assertEqual(listener.callCount(), 10)
- self._checkSelection(selection, curve, (curve, image, scatter))
-
- # Add a curve which is not active by default
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve2')
- curve2 = self.plot.getCurve('curve2')
- self.assertEqual(listener.callCount(), 10)
- self._checkSelection(selection, curve, (curve, image, scatter))
-
- # Mosted recently "actived" item is current, previous curve is removed
- self.plot.setActiveCurve('curve2')
- self.assertEqual(listener.callCount(), 11)
- self._checkSelection(selection, curve2, (curve2, image, scatter))
-
- # No items = no current
- self.plot.clear()
- self.assertEqual(listener.callCount(), 12)
- self._checkSelection(selection)
-
- def testPlotWidgetWithItems(self):
- """Test init of selection on a plot with items"""
- self.plot.addImage(((0, 1), (2, 3)), legend='image')
- self.plot.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend='scatter')
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve')
- self.plot.setActiveCurve('curve')
-
- selection = self.plot.selection()
- self.assertIsNotNone(selection.getCurrentItem())
- selected = selection.getSelectedItems()
- self.assertEqual(len(selected), 3)
- self.assertIn(self.plot.getActiveCurve(), selected)
- self.assertIn(self.plot.getActiveImage(), selected)
- self.assertIn(self.plot.getActiveScatter(), selected)
-
- def testSetCurrentItem(self):
- """Test setCurrentItem"""
- # Add items to the plot
- self.plot.addImage(((0, 1), (2, 3)), legend='image')
- image = self.plot.getActiveImage()
- self.plot.addScatter((3, 2, 1), (0, 1, 2), (0, 1, 2), legend='scatter')
- scatter = self.plot.getActiveScatter()
- self.plot.addCurve((0, 1, 2), (0, 1, 2), legend='curve')
- self.plot.setActiveCurve('curve')
- curve = self.plot.getActiveCurve()
-
- selection = self.plot.selection()
- self.assertIsNotNone(selection.getCurrentItem())
- self.assertEqual(len(selection.getSelectedItems()), 3)
-
- # Set current to None reset all active items
- selection.setCurrentItem(None)
- self._checkSelection(selection)
- self.assertIsNone(self.plot.getActiveCurve())
- self.assertIsNone(self.plot.getActiveImage())
- self.assertIsNone(self.plot.getActiveScatter())
-
- # Set current to an item makes it active
- selection.setCurrentItem(image)
- self._checkSelection(selection, image, (image,))
- self.assertIsNone(self.plot.getActiveCurve())
- self.assertIs(self.plot.getActiveImage(), image)
- self.assertIsNone(self.plot.getActiveScatter())
-
- # Set current to an item makes it active and keeps other active
- selection.setCurrentItem(curve)
- self._checkSelection(selection, curve, (curve, image))
- self.assertIs(self.plot.getActiveCurve(), curve)
- self.assertIs(self.plot.getActiveImage(), image)
- self.assertIsNone(self.plot.getActiveScatter())
-
- # Set current to an item makes it active and keeps other active
- selection.setCurrentItem(scatter)
- self._checkSelection(selection, scatter, (scatter, curve, image))
- self.assertIs(self.plot.getActiveCurve(), curve)
- self.assertIs(self.plot.getActiveImage(), image)
- self.assertIs(self.plot.getActiveScatter(), scatter)
-
-
-def suite():
- testClasses = (TestPlotWidget,
- TestPlotImage,
- TestPlotCurve,
- TestPlotHistogram,
- TestPlotScatter,
- TestPlotMarker,
- TestPlotItem,
- TestPlotAxes,
- TestPlotActiveCurveImage,
- TestPlotEmptyLog,
- TestPlotCurveLog,
- TestPlotImageLog,
- TestPlotMarkerLog,
- TestPlotWidgetSelection)
-
- test_suite = unittest.TestSuite()
-
- # Tests with matplotlib
- for testClass in testClasses:
- test_suite.addTest(parameterize(testClass, backend=None))
-
- test_suite.addTest(parameterize(TestSpecialBackend, backend=u"mpl"))
- if sys.version_info[0] == 2:
- test_suite.addTest(parameterize(TestSpecialBackend, backend=b"mpl"))
-
- if test_options.WITH_GL_TEST:
- # Tests with OpenGL backend
- for testClass in testClasses:
- test_suite.addTest(parameterize(testClass, backend='gl'))
-
- test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(
- TestPlotWidgetSwitchBackend))
-
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testPlotWidgetNoBackend.py b/silx/gui/plot/test/testPlotWidgetNoBackend.py
deleted file mode 100644
index edd3cd7..0000000
--- a/silx/gui/plot/test/testPlotWidgetNoBackend.py
+++ /dev/null
@@ -1,631 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Basic tests for PlotWidget with 'none' backend"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "17/01/2018"
-
-
-import unittest
-from functools import reduce
-from silx.utils.testutils import ParametricTestCase
-
-import numpy
-
-from silx.gui.plot.PlotWidget import PlotWidget
-from silx.gui.plot.items.histogram import _getHistogramCurve, _computeEdges
-
-
-class TestPlot(unittest.TestCase):
- """Basic tests of Plot without backend"""
-
- def testPlotTitleLabels(self):
- """Create a Plot and set the labels"""
-
- plot = PlotWidget(backend='none')
-
- title, xlabel, ylabel = 'the title', 'x label', 'y label'
- plot.setGraphTitle(title)
- plot.getXAxis().setLabel(xlabel)
- plot.getYAxis().setLabel(ylabel)
-
- self.assertEqual(plot.getGraphTitle(), title)
- self.assertEqual(plot.getXAxis().getLabel(), xlabel)
- self.assertEqual(plot.getYAxis().getLabel(), ylabel)
-
- def testAddNoRemove(self):
- """add objects to the Plot"""
-
- plot = PlotWidget(backend='none')
- plot.addCurve(x=(1, 2, 3), y=(3, 2, 1))
- plot.addImage(numpy.arange(100.).reshape(10, -1))
- plot.addShape(numpy.array((1., 10.)),
- numpy.array((10., 10.)),
- shape="rectangle")
- plot.addXMarker(10.)
-
-
-class TestPlotRanges(ParametricTestCase):
- """Basic tests of Plot data ranges without backend"""
-
- _getValidValues = {True: lambda ar: ar > 0,
- False: lambda ar: numpy.ones(shape=ar.shape,
- dtype=bool)}
-
- @staticmethod
- def _getRanges(arrays, are_logs):
- gen = (TestPlotRanges._getValidValues[is_log](ar)
- for (ar, is_log) in zip(arrays, are_logs))
- indices = numpy.where(reduce(numpy.logical_and, gen))[0]
- if len(indices) > 0:
- ranges = [(ar[indices[0]], ar[indices[-1]]) for ar in arrays]
- else:
- ranges = [None] * len(arrays)
-
- return ranges
-
- @staticmethod
- def _getRangesMinmax(ranges):
- # TODO : error if None in ranges.
- rangeMin = numpy.min([rng[0] for rng in ranges])
- rangeMax = numpy.max([rng[1] for rng in ranges])
- return rangeMin, rangeMax
-
- def testDataRangeNoPlot(self):
- """empty plot data range"""
-
- plot = PlotWidget(backend='none')
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
- with self.subTest(logX=logX, logY=logY):
- plot.getXAxis()._setLogarithmic(logX)
- plot.getYAxis()._setLogarithmic(logY)
- dataRange = plot.getDataRange()
- self.assertIsNone(dataRange.x)
- self.assertIsNone(dataRange.y)
- self.assertIsNone(dataRange.yright)
-
- def testDataRangeLeft(self):
- """left axis range"""
-
- plot = PlotWidget(backend='none')
-
- xData = numpy.arange(10) - 4.9 # range : -4.9 , 4.1
- yData = numpy.arange(10) - 6.9 # range : -6.9 , 2.1
-
- plot.addCurve(x=xData,
- y=yData,
- legend='plot_0',
- yaxis='left')
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
- with self.subTest(logX=logX, logY=logY):
- plot.getXAxis()._setLogarithmic(logX)
- plot.getYAxis()._setLogarithmic(logY)
- dataRange = plot.getDataRange()
- xRange, yRange = self._getRanges([xData, yData],
- [logX, logY])
- self.assertSequenceEqual(dataRange.x, xRange)
- self.assertSequenceEqual(dataRange.y, yRange)
- self.assertIsNone(dataRange.yright)
-
- def testDataRangeRight(self):
- """right axis range"""
-
- plot = PlotWidget(backend='none')
- xData = numpy.arange(10) - 4.9 # range : -4.9 , 4.1
- yData = numpy.arange(10) - 6.9 # range : -6.9 , 2.1
- plot.addCurve(x=xData,
- y=yData,
- legend='plot_0',
- yaxis='right')
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
- with self.subTest(logX=logX, logY=logY):
- plot.getXAxis()._setLogarithmic(logX)
- plot.getYAxis()._setLogarithmic(logY)
- dataRange = plot.getDataRange()
- xRange, yRange = self._getRanges([xData, yData],
- [logX, logY])
- self.assertSequenceEqual(dataRange.x, xRange)
- self.assertIsNone(dataRange.y)
- self.assertSequenceEqual(dataRange.yright, yRange)
-
- def testDataRangeImage(self):
- """image data range"""
-
- origin = (-10, 25)
- scale = (3., 8.)
- image = numpy.arange(100.).reshape(20, 5)
-
- plot = PlotWidget(backend='none')
- plot.addImage(image,
- origin=origin, scale=scale)
-
- xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
- yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
-
- ranges = {(False, False): (xRange, yRange),
- (True, False): (None, None),
- (True, True): (None, None),
- (False, True): (None, None)}
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
- with self.subTest(logX=logX, logY=logY):
- plot.getXAxis()._setLogarithmic(logX)
- plot.getYAxis()._setLogarithmic(logY)
- dataRange = plot.getDataRange()
- xRange, yRange = ranges[logX, logY]
- self.assertTrue(numpy.array_equal(dataRange.x, xRange),
- msg='{0} != {1}'.format(dataRange.x, xRange))
- self.assertTrue(numpy.array_equal(dataRange.y, yRange),
- msg='{0} != {1}'.format(dataRange.y, yRange))
- self.assertIsNone(dataRange.yright)
-
- def testDataRangeLeftRight(self):
- """right+left axis range"""
-
- plot = PlotWidget(backend='none')
-
- xData_l = numpy.arange(10) - 0.9 # range : -0.9 , 8.1
- yData_l = numpy.arange(10) - 1.9 # range : -1.9 , 7.1
- plot.addCurve(x=xData_l,
- y=yData_l,
- legend='plot_l',
- yaxis='left')
-
- xData_r = numpy.arange(10) - 4.9 # range : -4.9 , 4.1
- yData_r = numpy.arange(10) - 6.9 # range : -6.9 , 2.1
- plot.addCurve(x=xData_r,
- y=yData_r,
- legend='plot_r',
- yaxis='right')
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
- with self.subTest(logX=logX, logY=logY):
- plot.getXAxis()._setLogarithmic(logX)
- plot.getYAxis()._setLogarithmic(logY)
- dataRange = plot.getDataRange()
- xRangeL, yRangeL = self._getRanges([xData_l, yData_l],
- [logX, logY])
- xRangeR, yRangeR = self._getRanges([xData_r, yData_r],
- [logX, logY])
- xRangeLR = self._getRangesMinmax([xRangeL, xRangeR])
- self.assertSequenceEqual(dataRange.x, xRangeLR)
- self.assertSequenceEqual(dataRange.y, yRangeL)
- self.assertSequenceEqual(dataRange.yright, yRangeR)
-
- def testDataRangeCurveImage(self):
- """right+left+image axis range"""
-
- # overlapping ranges :
- # image sets x min and y max
- # plot_left sets y min
- # plot_right sets x max (and yright)
- plot = PlotWidget(backend='none')
-
- origin = (-10, 5)
- scale = (3., 8.)
- image = numpy.arange(100.).reshape(20, 5)
-
- plot.addImage(image,
- origin=origin, scale=scale, legend='image')
-
- xData_l = numpy.arange(10) - 0.9 # range : -0.9 , 8.1
- yData_l = numpy.arange(10) - 1.9 # range : -1.9 , 7.1
- plot.addCurve(x=xData_l,
- y=yData_l,
- legend='plot_l',
- yaxis='left')
-
- xData_r = numpy.arange(10) + 4.1 # range : 4.1 , 13.1
- yData_r = numpy.arange(10) - 0.9 # range : -0.9 , 8.1
- plot.addCurve(x=xData_r,
- y=yData_r,
- legend='plot_r',
- yaxis='right')
-
- imgXRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
- imgYRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
- with self.subTest(logX=logX, logY=logY):
- plot.getXAxis()._setLogarithmic(logX)
- plot.getYAxis()._setLogarithmic(logY)
- dataRange = plot.getDataRange()
- xRangeL, yRangeL = self._getRanges([xData_l, yData_l],
- [logX, logY])
- xRangeR, yRangeR = self._getRanges([xData_r, yData_r],
- [logX, logY])
- if logX or logY:
- xRangeLR = self._getRangesMinmax([xRangeL, xRangeR])
- else:
- xRangeLR = self._getRangesMinmax([xRangeL,
- xRangeR,
- imgXRange])
- yRangeL = self._getRangesMinmax([yRangeL, imgYRange])
- self.assertSequenceEqual(dataRange.x, xRangeLR)
- self.assertSequenceEqual(dataRange.y, yRangeL)
- self.assertSequenceEqual(dataRange.yright, yRangeR)
-
- def testDataRangeImageNegativeScaleX(self):
- """image data range, negative scale"""
-
- origin = (-10, 25)
- scale = (-3., 8.)
- image = numpy.arange(100.).reshape(20, 5)
-
- plot = PlotWidget(backend='none')
- plot.addImage(image,
- origin=origin, scale=scale)
-
- xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
- xRange.sort() # negative scale!
- yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
-
- ranges = {(False, False): (xRange, yRange),
- (True, False): (None, None),
- (True, True): (None, None),
- (False, True): (None, None)}
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
- with self.subTest(logX=logX, logY=logY):
- plot.getXAxis()._setLogarithmic(logX)
- plot.getYAxis()._setLogarithmic(logY)
- dataRange = plot.getDataRange()
- xRange, yRange = ranges[logX, logY]
- self.assertTrue(numpy.array_equal(dataRange.x, xRange),
- msg='{0} != {1}'.format(dataRange.x, xRange))
- self.assertTrue(numpy.array_equal(dataRange.y, yRange),
- msg='{0} != {1}'.format(dataRange.y, yRange))
- self.assertIsNone(dataRange.yright)
-
- def testDataRangeImageNegativeScaleY(self):
- """image data range, negative scale"""
-
- origin = (-10, 25)
- scale = (3., -8.)
- image = numpy.arange(100.).reshape(20, 5)
-
- plot = PlotWidget(backend='none')
- plot.addImage(image,
- origin=origin, scale=scale)
-
- xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
- yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
- yRange.sort() # negative scale!
-
- ranges = {(False, False): (xRange, yRange),
- (True, False): (None, None),
- (True, True): (None, None),
- (False, True): (None, None)}
-
- for logX, logY in ((False, False),
- (True, False),
- (True, True),
- (False, True),
- (False, False)):
- with self.subTest(logX=logX, logY=logY):
- plot.getXAxis()._setLogarithmic(logX)
- plot.getYAxis()._setLogarithmic(logY)
- dataRange = plot.getDataRange()
- xRange, yRange = ranges[logX, logY]
- self.assertTrue(numpy.array_equal(dataRange.x, xRange),
- msg='{0} != {1}'.format(dataRange.x, xRange))
- self.assertTrue(numpy.array_equal(dataRange.y, yRange),
- msg='{0} != {1}'.format(dataRange.y, yRange))
- self.assertIsNone(dataRange.yright)
-
- def testDataRangeHiddenCurve(self):
- """curves with a hidden curve"""
- plot = PlotWidget(backend='none')
- plot.addCurve((0, 1), (0, 1), legend='shown')
- plot.addCurve((0, 1, 2), (5, 5, 5), legend='hidden')
- range1 = plot.getDataRange()
- self.assertEqual(range1.x, (0, 2))
- self.assertEqual(range1.y, (0, 5))
- plot.hideCurve('hidden')
- range2 = plot.getDataRange()
- self.assertEqual(range2.x, (0, 1))
- self.assertEqual(range2.y, (0, 1))
-
-
-class TestPlotGetCurveImage(unittest.TestCase):
- """Test of plot getCurve and getImage methods"""
-
- def testGetCurve(self):
- """PlotWidget.getCurve and Plot.getActiveCurve tests"""
-
- plot = PlotWidget(backend='none')
-
- # No curve
- curve = plot.getCurve()
- self.assertIsNone(curve) # No curve
-
- plot.setActiveCurveHandling(True)
- plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 0')
- plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 1')
- plot.addCurve(x=(0, 1), y=(0, 1), legend='curve 2')
- plot.setActiveCurve('curve 0')
-
- # Active curve
- active = plot.getActiveCurve()
- self.assertEqual(active.getName(), 'curve 0')
- curve = plot.getCurve()
- self.assertEqual(curve.getName(), 'curve 0')
-
- # No active curve and curves
- plot.setActiveCurveHandling(False)
- active = plot.getActiveCurve()
- self.assertIsNone(active) # No active curve
- curve = plot.getCurve()
- self.assertEqual(curve.getName(), 'curve 2') # Last added curve
-
- # Last curve hidden
- plot.hideCurve('curve 2', True)
- curve = plot.getCurve()
- self.assertEqual(curve.getName(), 'curve 1') # Last added curve
-
- # All curves hidden
- plot.hideCurve('curve 1', True)
- plot.hideCurve('curve 0', True)
- curve = plot.getCurve()
- self.assertIsNone(curve)
-
- def testGetCurveOldApi(self):
- """old API PlotWidget.getCurve and Plot.getActiveCurve tests"""
-
- plot = PlotWidget(backend='none')
-
- # No curve
- curve = plot.getCurve()
- self.assertIsNone(curve) # No curve
-
- plot.setActiveCurveHandling(True)
- x = numpy.arange(10.).astype(numpy.float32)
- y = x * x
- plot.addCurve(x=x, y=y, legend='curve 0', info=["whatever"])
- plot.addCurve(x=x, y=2*x, legend='curve 1', info="anything")
- plot.setActiveCurve('curve 0')
-
- # Active curve (4 elements)
- xOut, yOut, legend, info = plot.getActiveCurve()[:4]
- self.assertEqual(legend, 'curve 0')
- self.assertTrue(numpy.allclose(xOut, x), 'curve 0 wrong x data')
- self.assertTrue(numpy.allclose(yOut, y), 'curve 0 wrong y data')
-
- # Active curve (5 elements)
- xOut, yOut, legend, info, params = plot.getCurve("curve 1")
- self.assertEqual(legend, 'curve 1')
- self.assertEqual(info, 'anything')
- self.assertTrue(numpy.allclose(xOut, x), 'curve 1 wrong x data')
- self.assertTrue(numpy.allclose(yOut, 2 * x), 'curve 1 wrong y data')
-
- def testGetImage(self):
- """PlotWidget.getImage and PlotWidget.getActiveImage tests"""
-
- plot = PlotWidget(backend='none')
-
- # No image
- image = plot.getImage()
- self.assertIsNone(image)
-
- plot.addImage(((0, 1), (2, 3)), legend='image 0')
- plot.addImage(((0, 1), (2, 3)), legend='image 1')
-
- # Active image
- active = plot.getActiveImage()
- self.assertEqual(active.getName(), 'image 0')
- image = plot.getImage()
- self.assertEqual(image.getName(), 'image 0')
-
- # No active image
- plot.addImage(((0, 1), (2, 3)), legend='image 2')
- plot.setActiveImage(None)
- active = plot.getActiveImage()
- self.assertIsNone(active)
- image = plot.getImage()
- self.assertEqual(image.getName(), 'image 2')
-
- # Active image
- plot.setActiveImage('image 1')
- active = plot.getActiveImage()
- self.assertEqual(active.getName(), 'image 1')
- image = plot.getImage()
- self.assertEqual(image.getName(), 'image 1')
-
- def testGetImageOldApi(self):
- """PlotWidget.getImage and PlotWidget.getActiveImage old API tests"""
-
- plot = PlotWidget(backend='none')
-
- # No image
- image = plot.getImage()
- self.assertIsNone(image)
-
- image = numpy.arange(10).astype(numpy.float32)
- image.shape = 5, 2
-
- plot.addImage(image, legend='image 0', info=["Hi!"])
-
- # Active image
- data, legend, info, something, params = plot.getActiveImage()
- self.assertEqual(legend, 'image 0')
- self.assertEqual(info, ["Hi!"])
- self.assertTrue(numpy.allclose(data, image), "image 0 data not correct")
-
- def testGetAllImages(self):
- """PlotWidget.getAllImages test"""
-
- plot = PlotWidget(backend='none')
-
- # No image
- images = plot.getAllImages()
- self.assertEqual(len(images), 0)
-
- # 2 images
- data = numpy.arange(100).reshape(10, 10)
- plot.addImage(data, legend='1')
- plot.addImage(data, origin=(10, 10), legend='2')
- images = plot.getAllImages(just_legend=True)
- self.assertEqual(list(images), ['1', '2'])
- images = plot.getAllImages(just_legend=False)
- self.assertEqual(len(images), 2)
- self.assertEqual(images[0].getName(), '1')
- self.assertEqual(images[1].getName(), '2')
-
-
-class TestPlotAddScatter(unittest.TestCase):
- """Test of plot addScatter"""
-
- def testAddGetScatter(self):
-
- plot = PlotWidget(backend='none')
-
- # No curve
- scatter = plot._getItem(kind="scatter")
- self.assertIsNone(scatter) # No curve
-
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 0')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 1')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 2')
- plot._setActiveItem('scatter', 'scatter 0')
-
- # Active scatter
- active = plot._getActiveItem(kind='scatter')
- self.assertEqual(active.getName(), 'scatter 0')
-
- # check default values
- self.assertAlmostEqual(active.getSymbolSize(), active._DEFAULT_SYMBOL_SIZE)
- self.assertEqual(active.getSymbol(), "o")
- self.assertAlmostEqual(active.getAlpha(), 1.0)
-
- # modify parameters
- active.setSymbolSize(20.5)
- active.setSymbol("d")
- active.setAlpha(0.777)
-
- s0 = plot.getScatter("scatter 0")
-
- self.assertAlmostEqual(s0.getSymbolSize(), 20.5)
- self.assertEqual(s0.getSymbol(), "d")
- self.assertAlmostEqual(s0.getAlpha(), 0.777)
-
- scatter1 = plot._getItem(kind='scatter', legend='scatter 1')
- self.assertEqual(scatter1.getName(), 'scatter 1')
-
- def testGetAllScatters(self):
- """PlotWidget.getAllImages test"""
-
- plot = PlotWidget(backend='none')
-
- items = plot.getItems()
- self.assertEqual(len(items), 0)
-
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 0')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 1')
- plot.addScatter(x=(0, 1), y=(0, 1), value=(0, 1), legend='scatter 2')
-
- items = plot.getItems()
- self.assertEqual(len(items), 3)
- self.assertEqual(items[0].getName(), 'scatter 0')
- self.assertEqual(items[1].getName(), 'scatter 1')
- self.assertEqual(items[2].getName(), 'scatter 2')
-
-
-class TestPlotHistogram(unittest.TestCase):
- """Basic tests for histogram."""
-
- def testEdges(self):
- x = numpy.array([0, 1, 2])
- edgesRight = numpy.array([0, 1, 2, 3])
- edgesLeft = numpy.array([-1, 0, 1, 2])
- edgesCenter = numpy.array([-0.5, 0.5, 1.5, 2.5])
-
- # testing x values for right
- edges = _computeEdges(x, 'right')
- numpy.testing.assert_array_equal(edges, edgesRight)
-
- edges = _computeEdges(x, 'center')
- numpy.testing.assert_array_equal(edges, edgesCenter)
-
- edges = _computeEdges(x, 'left')
- numpy.testing.assert_array_equal(edges, edgesLeft)
-
- def testHistogramCurve(self):
- y = numpy.array([3, 2, 5])
- edges = numpy.array([0, 1, 2, 3])
-
- xHisto, yHisto = _getHistogramCurve(y, edges)
- numpy.testing.assert_array_equal(
- yHisto, numpy.array([3, 3, 2, 2, 5, 5]))
-
- y = numpy.array([-3, 2, 5, 0])
- edges = numpy.array([-2, -1, 0, 1, 2])
- xHisto, yHisto = _getHistogramCurve(y, edges)
- numpy.testing.assert_array_equal(
- yHisto, numpy.array([-3, -3, 2, 2, 5, 5, 0, 0]))
-
-
-def suite():
- test_suite = unittest.TestSuite()
- for TestClass in (TestPlot, TestPlotRanges, TestPlotGetCurveImage,
- TestPlotHistogram, TestPlotAddScatter):
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestClass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testPlotWindow.py b/silx/gui/plot/test/testPlotWindow.py
deleted file mode 100644
index e12b756..0000000
--- a/silx/gui/plot/test/testPlotWindow.py
+++ /dev/null
@@ -1,185 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Basic tests for PlotWindow"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "27/06/2017"
-
-
-import unittest
-import numpy
-
-from silx.gui.utils.testutils import TestCaseQt, getQToolButtonFromAction
-from silx.test.utils import test_options
-
-from silx.gui import qt
-from silx.gui.plot import PlotWindow
-from silx.gui.colors import Colormap
-
-class TestPlotWindow(TestCaseQt):
- """Base class for tests of PlotWindow."""
-
- def setUp(self):
- super(TestPlotWindow, self).setUp()
- self.plot = PlotWindow()
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- def tearDown(self):
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- super(TestPlotWindow, self).tearDown()
-
- def testActions(self):
- """Test the actions QToolButtons"""
- self.plot.setLimits(1, 100, 1, 100)
-
- checkList = [ # QAction, Plot state getter
- (self.plot.xAxisAutoScaleAction, self.plot.getXAxis().isAutoScale),
- (self.plot.yAxisAutoScaleAction, self.plot.getYAxis().isAutoScale),
- (self.plot.xAxisLogarithmicAction, self.plot.getXAxis()._isLogarithmic),
- (self.plot.yAxisLogarithmicAction, self.plot.getYAxis()._isLogarithmic),
- (self.plot.gridAction, self.plot.getGraphGrid),
- ]
-
- for action, getter in checkList:
- self.mouseMove(self.plot)
- initialState = getter()
- toolButton = getQToolButtonFromAction(action)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
- self.assertNotEqual(getter(), initialState,
- msg='"%s" state not changed' % action.text())
-
- self.mouseClick(toolButton, qt.Qt.LeftButton)
- self.assertEqual(getter(), initialState,
- msg='"%s" state not changed' % action.text())
-
- # Trigger a zoom reset
- self.mouseMove(self.plot)
- resetZoomAction = self.plot.resetZoomAction
- toolButton = getQToolButtonFromAction(resetZoomAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
-
- def testDockWidgets(self):
- """Test add/remove dock widgets"""
- dock1 = qt.QDockWidget('Test 1')
- dock1.setWidget(qt.QLabel('Test 1'))
-
- self.plot.addTabbedDockWidget(dock1)
- self.qapp.processEvents()
-
- self.plot.removeDockWidget(dock1)
- self.qapp.processEvents()
-
- dock2 = qt.QDockWidget('Test 2')
- dock2.setWidget(qt.QLabel('Test 2'))
-
- self.plot.addTabbedDockWidget(dock2)
- self.qapp.processEvents()
-
- if qt.BINDING != 'PySide2':
- # Weird bug with PySide2 later upon gc.collect() when getting the layout
- self.assertNotEqual(self.plot.layout().indexOf(dock2),
- -1,
- "dock2 not properly displayed")
-
- def testToolAspectRatio(self):
- self.plot.toolBar()
- self.plot.keepDataAspectRatioButton.keepDataAspectRatio()
- self.assertTrue(self.plot.isKeepDataAspectRatio())
- self.plot.keepDataAspectRatioButton.dontKeepDataAspectRatio()
- self.assertFalse(self.plot.isKeepDataAspectRatio())
-
- def testToolYAxisOrigin(self):
- self.plot.toolBar()
- self.plot.yAxisInvertedButton.setYAxisUpward()
- self.assertFalse(self.plot.getYAxis().isInverted())
- self.plot.yAxisInvertedButton.setYAxisDownward()
- self.assertTrue(self.plot.getYAxis().isInverted())
-
- def testColormapAutoscaleCache(self):
- # Test that the min/max cache is not computed twice
-
- old = Colormap._computeAutoscaleRange
- self._count = 0
- def _computeAutoscaleRange(colormap, data):
- self._count = self._count + 1
- return 10, 20
- Colormap._computeAutoscaleRange = _computeAutoscaleRange
- try:
- colormap = Colormap(name='red')
- self.plot.setVisible(True)
-
- # Add an image
- data = numpy.arange(8**2).reshape(8, 8)
- self.plot.addImage(data, legend="foo", colormap=colormap)
- self.plot.setActiveImage("foo")
-
- # Use the colorbar
- self.plot.getColorBarWidget().setVisible(True)
- self.qWait(50)
-
- # Remove and add again the same item
- image = self.plot.getImage("foo")
- self.plot.removeImage("foo")
- self.plot.addItem(image)
- self.qWait(50)
- finally:
- Colormap._computeAutoscaleRange = old
- self.assertEqual(self._count, 1)
- del self._count
-
- @unittest.skipUnless(test_options.WITH_GL_TEST,
- test_options.WITH_QT_TEST_REASON)
- def testSwitchBackend(self):
- """Test switching an empty plot"""
- self.plot.resetZoom()
- xlimits = self.plot.getXAxis().getLimits()
- ylimits = self.plot.getYAxis().getLimits()
- isKeepAspectRatio = self.plot.isKeepDataAspectRatio()
-
- for backend in ('gl', 'mpl'):
- with self.subTest():
- self.plot.setBackend(backend)
- self.plot.replot()
- self.assertEqual(self.plot.getXAxis().getLimits(), xlimits)
- self.assertEqual(self.plot.getYAxis().getLimits(), ylimits)
- self.assertEqual(
- self.plot.isKeepDataAspectRatio(), isKeepAspectRatio)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotWindow))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testRoiStatsWidget.py b/silx/gui/plot/test/testRoiStatsWidget.py
deleted file mode 100644
index 378d499..0000000
--- a/silx/gui/plot/test/testRoiStatsWidget.py
+++ /dev/null
@@ -1,290 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2019 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.
-#
-# ###########################################################################*/
-"""Tests for ROIStatsWidget"""
-
-
-from silx.gui.utils.testutils import TestCaseQt
-from silx.gui import qt
-from silx.gui.plot import PlotWindow
-from silx.gui.plot.stats.stats import Stats
-from silx.gui.plot.ROIStatsWidget import ROIStatsWidget
-from silx.gui.plot.CurvesROIWidget import ROI
-from silx.gui.plot.items.roi import RectangleROI, PolygonROI
-from silx.gui.plot.StatsWidget import UpdateMode
-import unittest
-import numpy
-
-
-
-class _TestRoiStatsBase(TestCaseQt):
- """Base class for several unittest relative to ROIStatsWidget"""
- def setUp(self):
- TestCaseQt.setUp(self)
- # define plot
- self.plot = PlotWindow()
- self.plot.addImage(numpy.arange(10000).reshape(100, 100),
- legend='img1')
- self.img_item = self.plot.getImage('img1')
- self.plot.addCurve(x=numpy.linspace(0, 10, 56), y=numpy.arange(56),
- legend='curve1')
- self.curve_item = self.plot.getCurve('curve1')
- self.plot.addHistogram(edges=numpy.linspace(0, 10, 56),
- histogram=numpy.arange(56), legend='histo1')
- self.histogram_item = self.plot.getHistogram(legend='histo1')
- self.plot.addScatter(x=numpy.linspace(0, 10, 56),
- y=numpy.linspace(0, 10, 56),
- value=numpy.arange(56),
- legend='scatter1')
- self.scatter_item = self.plot.getScatter(legend='scatter1')
-
- # stats widget
- self.statsWidget = ROIStatsWidget(plot=self.plot)
-
- # define stats
- stats = [
- ('sum', numpy.sum),
- ('mean', numpy.mean),
- ]
- self.statsWidget.setStats(stats=stats)
-
- # define rois
- self.roi1D = ROI(name='range1', fromdata=0, todata=4, type_='energy')
- self.rectangle_roi = RectangleROI()
- self.rectangle_roi.setGeometry(origin=(0, 0), size=(20, 20))
- self.rectangle_roi.setName('Initial ROI')
- self.polygon_roi = PolygonROI()
- points = numpy.array([[0, 5], [5, 0], [10, 5], [5, 10]])
- self.polygon_roi.setPoints(points)
-
- def statsTable(self):
- return self.statsWidget._statsROITable
-
- def tearDown(self):
- Stats._getContext.cache_clear()
- self.statsWidget.setAttribute(qt.Qt.WA_DeleteOnClose, True)
- self.statsWidget.close()
- self.statsWidget = None
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose, True)
- self.plot.close()
- self.plot = None
- TestCaseQt.tearDown(self)
-
-
-class TestRoiStatsCouple(_TestRoiStatsBase):
- """
- Test different possible couple (roi, plotItem).
- Check that:
-
- * computation is correct if couple is valid
- * raise an error if couple is invalid
- """
- def testROICurve(self):
- """
- Test that the couple (ROI, curveItem) can be used for stats
- """
- item = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.curve_item)
- assert item is not None
- tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '253')
- self.assertEqual(tableItems['mean'].text(), '11.0')
-
- def testRectangleImage(self):
- """
- Test that the couple (RectangleROI, imageItem) can be used for stats
- """
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
- assert item is not None
- self.plot.addImage(numpy.ones(10000).reshape(100, 100),
- legend='img1')
- self.qapp.processEvents()
- tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), str(float(21*21)))
- self.assertEqual(tableItems['mean'].text(), '1.0')
-
- def testPolygonImage(self):
- """
- Test that the couple (PolygonROI, imageItem) can be used for stats
- """
- item = self.statsWidget.addItem(roi=self.polygon_roi,
- plotItem=self.img_item)
- assert item is not None
- tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '22750')
- self.assertEqual(tableItems['mean'].text(), '455.0')
-
- def testROIImage(self):
- """
- Test that the couple (ROI, imageItem) is raising an error
- """
- with self.assertRaises(TypeError):
- self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.img_item)
-
- def testRectangleCurve(self):
- """
- Test that the couple (rectangleROI, curveItem) is raising an error
- """
- with self.assertRaises(TypeError):
- self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.curve_item)
-
- def testROIHistogram(self):
- """
- Test that the couple (PolygonROI, imageItem) can be used for stats
- """
- item = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.histogram_item)
- assert item is not None
- tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '253')
- self.assertEqual(tableItems['mean'].text(), '11.0')
-
- def testROIScatter(self):
- """
- Test that the couple (PolygonROI, imageItem) can be used for stats
- """
- item = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.scatter_item)
- assert item is not None
- tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '253')
- self.assertEqual(tableItems['mean'].text(), '11.0')
-
-
-class TestRoiStatsAddRemoveItem(_TestRoiStatsBase):
- """Test adding and removing (roi, plotItem) items"""
- def testAddRemoveItems(self):
- item1 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.scatter_item)
- self.assertTrue(item1 is not None)
- self.assertEqual(self.statsTable().rowCount(), 1)
- item2 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.histogram_item)
- self.assertTrue(item2 is not None)
- self.assertEqual(self.statsTable().rowCount(), 2)
- # try to add twice the same item
- item3 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.histogram_item)
- self.assertTrue(item3 is None)
- self.assertEqual(self.statsTable().rowCount(), 2)
- item4 = self.statsWidget.addItem(roi=self.roi1D,
- plotItem=self.curve_item)
- self.assertTrue(item4 is not None)
- self.assertEqual(self.statsTable().rowCount(), 3)
-
- self.statsWidget.removeItem(plotItem=item4._plot_item,
- roi=item4._roi)
- self.assertEqual(self.statsTable().rowCount(), 2)
- # try to remove twice the same item
- self.statsWidget.removeItem(plotItem=item4._plot_item,
- roi=item4._roi)
- self.assertEqual(self.statsTable().rowCount(), 2)
- self.statsWidget.removeItem(plotItem=item2._plot_item,
- roi=item2._roi)
- self.statsWidget.removeItem(plotItem=item1._plot_item,
- roi=item1._roi)
- self.assertEqual(self.statsTable().rowCount(), 0)
-
-
-class TestRoiStatsRoiUpdate(_TestRoiStatsBase):
- """Test that the stats will be updated if the roi is updated"""
- def testChangeRoi(self):
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
- assert item is not None
- tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '445410')
- self.assertEqual(tableItems['mean'].text(), '1010.0')
-
- # update roi
- self.rectangle_roi.setOrigin(position=(10, 10))
- self.assertNotEqual(tableItems['sum'].text(), '445410')
- self.assertNotEqual(tableItems['mean'].text(), '1010.0')
-
- def testUpdateModeScenario(self):
- """Test update according to a simple scenario"""
- self.statsWidget._setUpdateMode(UpdateMode.AUTO)
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
-
- assert item is not None
- tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['sum'].text(), '445410')
- self.assertEqual(tableItems['mean'].text(), '1010.0')
- self.statsWidget._setUpdateMode(UpdateMode.MANUAL)
- self.rectangle_roi.setOrigin(position=(10, 10))
- self.qapp.processEvents()
- self.assertNotEqual(tableItems['sum'].text(), '445410')
- self.assertNotEqual(tableItems['mean'].text(), '1010.0')
- self.statsWidget._updateAllStats(is_request=True)
- self.assertNotEqual(tableItems['sum'].text(), '445410')
- self.assertNotEqual(tableItems['mean'].text(), '1010.0')
-
-
-class TestRoiStatsPlotItemUpdate(_TestRoiStatsBase):
- """Test that the stats will be updated if the plot item is updated"""
- def testChangeImage(self):
- self.statsWidget._setUpdateMode(UpdateMode.AUTO)
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
-
- assert item is not None
- tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['mean'].text(), '1010.0')
-
- # update plot
- self.plot.addImage(numpy.arange(100, 10100).reshape(100, 100),
- legend='img1')
- self.assertNotEqual(tableItems['mean'].text(), '1059.5')
-
- def testUpdateModeScenario(self):
- """Test update according to a simple scenario"""
- self.statsWidget._setUpdateMode(UpdateMode.MANUAL)
- item = self.statsWidget.addItem(roi=self.rectangle_roi,
- plotItem=self.img_item)
-
- assert item is not None
- tableItems = self.statsTable()._itemToTableItems(item)
- self.assertEqual(tableItems['mean'].text(), '1010.0')
- self.plot.addImage(numpy.arange(100, 10100).reshape(100, 100),
- legend='img1')
- self.assertEqual(tableItems['mean'].text(), '1010.0')
- self.statsWidget._updateAllStats(is_request=True)
- self.assertEqual(tableItems['mean'].text(), '1110.0')
-
-
-def suite():
- test_suite = unittest.TestSuite()
- for TestClass in (TestRoiStatsCouple, TestRoiStatsRoiUpdate,
- TestRoiStatsPlotItemUpdate):
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestClass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testSaveAction.py b/silx/gui/plot/test/testSaveAction.py
deleted file mode 100644
index 0eb129d..0000000
--- a/silx/gui/plot/test/testSaveAction.py
+++ /dev/null
@@ -1,143 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2019 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.
-#
-# ###########################################################################*/
-"""Test the plot's save action (consistency of output)"""
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "28/11/2017"
-
-
-import unittest
-import tempfile
-import os
-
-from silx.gui.plot.test.utils import PlotWidgetTestCase
-
-from silx.gui.plot import PlotWidget
-from silx.gui.plot.actions.io import SaveAction
-
-
-class TestSaveActionSaveCurvesAsSpec(unittest.TestCase):
-
- def setUp(self):
- self.plot = PlotWidget(backend='none')
- self.saveAction = SaveAction(plot=self.plot)
-
- self.tempdir = tempfile.mkdtemp()
- self.out_fname = os.path.join(self.tempdir, "out.dat")
-
- def tearDown(self):
- os.unlink(self.out_fname)
- os.rmdir(self.tempdir)
-
- def testSaveMultipleCurvesAsSpec(self):
- """Test that labels are properly used."""
- self.plot.setGraphXLabel("graph x label")
- self.plot.setGraphYLabel("graph y label")
-
- self.plot.addCurve([0, 1], [1, 2], "curve with labels",
- xlabel="curve0 X", ylabel="curve0 Y")
- self.plot.addCurve([-1, 3], [-6, 2], "curve with X label",
- xlabel="curve1 X")
- self.plot.addCurve([-2, 0], [8, 12], "curve with Y label",
- ylabel="curve2 Y")
- self.plot.addCurve([3, 1], [7, 6], "curve with no labels")
-
- self.saveAction._saveCurves(self.plot,
- self.out_fname,
- SaveAction.DEFAULT_ALL_CURVES_FILTERS[0]) # "All curves as SpecFile (*.dat)"
-
- with open(self.out_fname, "rb") as f:
- file_content = f.read()
- if hasattr(file_content, "decode"):
- file_content = file_content.decode()
-
- # case with all curve labels specified
- self.assertIn("#S 1 curve0 Y", file_content)
- self.assertIn("#L curve0 X curve0 Y", file_content)
-
- # graph X&Y labels are used when no curve label is specified
- self.assertIn("#S 2 graph y label", file_content)
- self.assertIn("#L curve1 X graph y label", file_content)
-
- self.assertIn("#S 3 curve2 Y", file_content)
- self.assertIn("#L graph x label curve2 Y", file_content)
-
- self.assertIn("#S 4 graph y label", file_content)
- self.assertIn("#L graph x label graph y label", file_content)
-
-
-class TestSaveActionExtension(PlotWidgetTestCase):
- """Test SaveAction file filter API"""
-
- def _dummySaveFunction(self, plot, filename, nameFilter):
- pass
-
- def testFileFilterAPI(self):
- """Test addition/update of a file filter"""
- saveAction = SaveAction(plot=self.plot, parent=self.plot)
-
- # Add a new file filter
- nameFilter = 'Dummy file (*.dummy)'
- saveAction.setFileFilter('all', nameFilter, self._dummySaveFunction)
- self.assertTrue(nameFilter in saveAction.getFileFilters('all'))
- self.assertEqual(saveAction.getFileFilters('all')[nameFilter],
- self._dummySaveFunction)
-
- # Add a new file filter at a particular position
- nameFilter = 'Dummy file2 (*.dummy)'
- saveAction.setFileFilter('all', nameFilter,
- self._dummySaveFunction, index=3)
- self.assertTrue(nameFilter in saveAction.getFileFilters('all'))
- filters = saveAction.getFileFilters('all')
- self.assertEqual(filters[nameFilter], self._dummySaveFunction)
- self.assertEqual(list(filters.keys()).index(nameFilter),3)
-
- # Update an existing file filter
- nameFilter = SaveAction.IMAGE_FILTER_EDF
- saveAction.setFileFilter('image', nameFilter, self._dummySaveFunction)
- self.assertEqual(saveAction.getFileFilters('image')[nameFilter],
- self._dummySaveFunction)
-
- # Change the position of an existing file filter
- nameFilter = 'Dummy file2 (*.dummy)'
- oldIndex = list(saveAction.getFileFilters('all')).index(nameFilter)
- newIndex = oldIndex - 1
- saveAction.setFileFilter('all', nameFilter,
- self._dummySaveFunction, index=newIndex)
- filters = saveAction.getFileFilters('all')
- self.assertEqual(filters[nameFilter], self._dummySaveFunction)
- self.assertEqual(list(filters.keys()).index(nameFilter), newIndex)
-
-def suite():
- test_suite = unittest.TestSuite()
- for cls in (TestSaveActionSaveCurvesAsSpec, TestSaveActionExtension):
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(cls))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testScatterMaskToolsWidget.py b/silx/gui/plot/test/testScatterMaskToolsWidget.py
deleted file mode 100644
index 800f30e..0000000
--- a/silx/gui/plot/test/testScatterMaskToolsWidget.py
+++ /dev/null
@@ -1,318 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""Basic tests for MaskToolsWidget"""
-
-__authors__ = ["T. Vincent", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "17/01/2018"
-
-
-import logging
-import os.path
-import unittest
-
-import numpy
-
-from silx.gui import qt
-from silx.test.utils import temp_dir
-from silx.utils.testutils import ParametricTestCase
-from silx.gui.utils.testutils import getQToolButtonFromAction
-from silx.gui.plot import PlotWindow, ScatterMaskToolsWidget
-from .utils import PlotWidgetTestCase
-
-import fabio
-
-
-_logger = logging.getLogger(__name__)
-
-
-class TestScatterMaskToolsWidget(PlotWidgetTestCase, ParametricTestCase):
- """Basic test for MaskToolsWidget"""
-
- def _createPlot(self):
- return PlotWindow()
-
- def setUp(self):
- super(TestScatterMaskToolsWidget, self).setUp()
- self.widget = ScatterMaskToolsWidget.ScatterMaskToolsDockWidget(
- plot=self.plot, name='TEST')
- self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, self.widget)
-
- self.maskWidget = self.widget.widget()
-
- def tearDown(self):
- del self.maskWidget
- del self.widget
- super(TestScatterMaskToolsWidget, self).tearDown()
-
- def testEmptyPlot(self):
- """Empty plot, display MaskToolsDockWidget, toggle multiple masks"""
- self.maskWidget.setMultipleMasks('single')
- self.qapp.processEvents()
-
- self.maskWidget.setMultipleMasks('exclusive')
- self.qapp.processEvents()
-
- def _drag(self):
- """Drag from plot center to offset position"""
- plot = self.plot.getWidgetHandle()
- xCenter, yCenter = plot.width() // 2, plot.height() // 2
- offset = min(plot.width(), plot.height()) // 10
-
- pos0 = xCenter, yCenter
- pos1 = xCenter + offset, yCenter + offset
-
- self.mouseMove(plot, pos=(0, 0))
- self.mouseMove(plot, pos=pos0)
- self.qapp.processEvents()
- self.mousePress(plot, qt.Qt.LeftButton, pos=pos0)
- self.qapp.processEvents()
-
- self.mouseMove(plot, pos=(pos0[0] + offset // 2, pos0[1] + offset // 2))
- self.mouseMove(plot, pos=pos1)
- self.qapp.processEvents()
- self.mouseRelease(plot, qt.Qt.LeftButton, pos=pos1)
- self.qapp.processEvents()
- self.mouseMove(plot, pos=(0, 0))
-
- def _drawPolygon(self):
- """Draw a star polygon in the plot"""
- plot = self.plot.getWidgetHandle()
- x, y = plot.width() // 2, plot.height() // 2
- offset = min(plot.width(), plot.height()) // 10
-
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset),
- (x, y + offset)] # Close polygon
-
- self.mouseMove(plot, pos=[0, 0])
- for pos in star:
- self.mouseMove(plot, pos=pos)
- self.qapp.processEvents()
- self.mousePress(plot, qt.Qt.LeftButton, pos=pos)
- self.qapp.processEvents()
- self.mouseRelease(plot, qt.Qt.LeftButton, pos=pos)
- self.qapp.processEvents()
-
- def _drawPencil(self):
- """Draw a star polygon in the plot"""
- plot = self.plot.getWidgetHandle()
- x, y = plot.width() // 2, plot.height() // 2
- offset = min(plot.width(), plot.height()) // 10
-
- star = [(x, y + offset),
- (x - offset, y - offset),
- (x + offset, y),
- (x - offset, y),
- (x + offset, y - offset)]
-
- self.mouseMove(plot, pos=[0, 0])
- self.mouseMove(plot, pos=star[0])
- self.mousePress(plot, qt.Qt.LeftButton, pos=star[0])
- for pos in star[1:]:
- self.mouseMove(plot, pos=pos)
- self.mouseRelease(
- plot, qt.Qt.LeftButton, pos=star[-1])
-
- def testWithAScatter(self):
- """Plot with a Scatter: test MaskToolsWidget interactions"""
-
- # Add and remove a scatter (this should enable/disable GUI + change mask)
- self.plot.addScatter(
- x=numpy.arange(256),
- y=numpy.arange(256),
- value=numpy.random.random(256),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
- self.qapp.processEvents()
-
- self.plot.remove('test', kind='scatter')
- self.qapp.processEvents()
-
- self.plot.addScatter(
- x=numpy.arange(1000),
- y=1000 * (numpy.arange(1000) % 20),
- value=numpy.random.random(1000),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
- self.plot.resetZoom()
- self.qapp.processEvents()
-
- # Test draw rectangle #
- toolButton = getQToolButtonFromAction(self.maskWidget.rectAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
-
- # mask
- self.maskWidget.maskStateGroup.button(1).click()
- self.qapp.processEvents()
- self._drag()
-
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
-
- # unmask same region
- self.maskWidget.maskStateGroup.button(0).click()
- self.qapp.processEvents()
- self._drag()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
-
- # Test draw polygon #
- toolButton = getQToolButtonFromAction(self.maskWidget.polygonAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
-
- # mask
- self.maskWidget.maskStateGroup.button(1).click()
- self.qapp.processEvents()
- self._drawPolygon()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
-
- # unmask same region
- self.maskWidget.maskStateGroup.button(0).click()
- self.qapp.processEvents()
- self._drawPolygon()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
-
- # Test draw pencil #
- toolButton = getQToolButtonFromAction(self.maskWidget.pencilAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
-
- self.maskWidget.pencilSpinBox.setValue(30)
- self.qapp.processEvents()
-
- # mask
- self.maskWidget.maskStateGroup.button(1).click()
- self.qapp.processEvents()
- self._drawPencil()
- self.assertFalse(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
-
- # unmask same region
- self.maskWidget.maskStateGroup.button(0).click()
- self.qapp.processEvents()
- self._drawPencil()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
-
- # Test no draw tool #
- toolButton = getQToolButtonFromAction(self.maskWidget.browseAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
-
- self.plot.clear()
-
- def __loadSave(self, file_format):
- self.plot.addScatter(
- x=numpy.arange(256),
- y=25 * (numpy.arange(256) % 10),
- value=numpy.random.random(256),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
- self.plot.resetZoom()
- self.qapp.processEvents()
-
- # Draw a polygon mask
- toolButton = getQToolButtonFromAction(self.maskWidget.polygonAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
- self._drawPolygon()
-
- ref_mask = self.maskWidget.getSelectionMask()
- self.assertFalse(numpy.all(numpy.equal(ref_mask, 0)))
-
- with temp_dir() as tmp:
- mask_filename = os.path.join(tmp, 'mask.' + file_format)
- self.maskWidget.save(mask_filename, file_format)
-
- self.maskWidget.resetSelectionMask()
- self.assertTrue(
- numpy.all(numpy.equal(self.maskWidget.getSelectionMask(), 0)))
-
- self.maskWidget.load(mask_filename)
- self.assertTrue(numpy.all(numpy.equal(
- self.maskWidget.getSelectionMask(), ref_mask)))
-
- def testLoadSaveNpy(self):
- self.__loadSave("npy")
-
- def testLoadSaveCsv(self):
- self.__loadSave("csv")
-
- def testSigMaskChangedEmitted(self):
- self.qapp.processEvents()
- self.plot.addScatter(
- x=numpy.arange(1000),
- y=1000 * (numpy.arange(1000) % 20),
- value=numpy.ones((1000,)),
- legend='test')
- self.plot._setActiveItem(kind="scatter", legend="test")
- self.plot.resetZoom()
- self.qapp.processEvents()
-
- self.plot.remove('test', kind='scatter')
- self.qapp.processEvents()
-
- self.plot.addScatter(
- x=numpy.arange(1000),
- y=1000 * (numpy.arange(1000) % 20),
- value=numpy.random.random(1000),
- legend='test')
-
- l = []
-
- def slot():
- l.append(1)
-
- self.maskWidget.sigMaskChanged.connect(slot)
-
- # rectangle mask
- toolButton = getQToolButtonFromAction(self.maskWidget.rectAction)
- self.assertIsNot(toolButton, None)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
- self.maskWidget.maskStateGroup.button(1).click()
- self.qapp.processEvents()
- self._drag()
-
- self.assertGreater(len(l), 0)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- for TestClass in (TestScatterMaskToolsWidget,):
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestClass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testScatterView.py b/silx/gui/plot/test/testScatterView.py
deleted file mode 100644
index 583e3ed..0000000
--- a/silx/gui/plot/test/testScatterView.py
+++ /dev/null
@@ -1,134 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018 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.
-#
-# ###########################################################################*/
-"""Basic tests for ScatterView"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "06/03/2018"
-
-
-import unittest
-
-import numpy
-
-from silx.gui.plot.items import Axis, Scatter
-from silx.gui.plot import ScatterView
-from silx.gui.plot.test.utils import PlotWidgetTestCase
-
-
-class TestScatterView(PlotWidgetTestCase):
- """Test of ScatterView widget"""
-
- def _createPlot(self):
- return ScatterView()
-
- def test(self):
- """Simple tests"""
- x = numpy.arange(100)
- y = numpy.arange(100)
- value = numpy.arange(100)
- self.plot.setData(x, y, value)
- self.qapp.processEvents()
-
- data = self.plot.getData()
- self.assertEqual(len(data), 5)
- self.assertTrue(numpy.all(numpy.equal(x, data[0])))
- self.assertTrue(numpy.all(numpy.equal(y, data[1])))
- self.assertTrue(numpy.all(numpy.equal(value, data[2])))
- self.assertIsNone(data[3]) # xerror
- self.assertIsNone(data[4]) # yerror
-
- # Test access to scatter item
- self.assertIsInstance(self.plot.getScatterItem(), Scatter)
-
- # Test toolbar actions
-
- action = self.plot.getScatterToolBar().getXAxisLogarithmicAction()
- action.trigger()
- self.qapp.processEvents()
-
- maskAction = self.plot.getScatterToolBar().actions()[-1]
- maskAction.trigger()
- self.qapp.processEvents()
-
- # Test proxy API
-
- self.plot.resetZoom()
- self.qapp.processEvents()
-
- scale = self.plot.getXAxis().getScale()
- self.assertEqual(scale, Axis.LOGARITHMIC)
-
- scale = self.plot.getYAxis().getScale()
- self.assertEqual(scale, Axis.LINEAR)
-
- title = 'Test ScatterView'
- self.plot.setGraphTitle(title)
- self.assertEqual(self.plot.getGraphTitle(), title)
-
- self.qapp.processEvents()
-
- # Reset scatter data
-
- self.plot.setData(None, None, None)
- self.qapp.processEvents()
-
- data = self.plot.getData()
- self.assertEqual(len(data), 5)
- self.assertEqual(len(data[0]), 0) # x
- self.assertEqual(len(data[1]), 0) # y
- self.assertEqual(len(data[2]), 0) # value
- self.assertIsNone(data[3]) # xerror
- self.assertIsNone(data[4]) # yerror
-
- def testAlpha(self):
- """Test alpha transparency in setData"""
- _pts = 100
- _levels = 100
- _fwhm = 50
- x = numpy.random.rand(_pts)*_levels
- y = numpy.random.rand(_pts)*_levels
- value = numpy.random.rand(_pts)*_levels
- x0 = x[int(_pts/2)]
- y0 = x[int(_pts/2)]
- #2D Gaussian kernel
- alpha = numpy.exp(-4*numpy.log(2) * ((x-x0)**2 + (y-y0)**2) / _fwhm**2)
-
- self.plot.setData(x, y, value, alpha=alpha)
- self.qapp.processEvents()
-
- alphaData = self.plot.getScatterItem().getAlphaData()
- self.assertTrue(numpy.all(numpy.equal(alpha, alphaData)))
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loadTests(TestScatterView))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testStackView.py b/silx/gui/plot/test/testStackView.py
deleted file mode 100644
index 7605bbc..0000000
--- a/silx/gui/plot/test/testStackView.py
+++ /dev/null
@@ -1,261 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Basic tests for StackView"""
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "20/03/2017"
-
-
-import unittest
-import numpy
-
-from silx.gui.utils.testutils import TestCaseQt, SignalListener
-
-from silx.gui import qt
-from silx.gui.plot import StackView
-from silx.gui.plot.StackView import StackViewMainWindow
-
-from silx.utils.array_like import ListOfImages
-
-
-class TestStackView(TestCaseQt):
- """Base class for tests of StackView."""
-
- def setUp(self):
- super(TestStackView, self).setUp()
- self.stackview = StackView()
- self.stackview.show()
- self.qWaitForWindowExposed(self.stackview)
- self.mystack = numpy.fromfunction(
- lambda i, j, k: numpy.sin(i/15.) + numpy.cos(j/4.) + 2 * numpy.sin(k/6.),
- (10, 20, 30)
- )
-
- def tearDown(self):
- self.stackview.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.stackview.close()
- del self.stackview
- super(TestStackView, self).tearDown()
-
- def testScaleColormapRangeToStack(self):
- """Test scaleColormapRangeToStack"""
- self.stackview.setStack(self.mystack)
- self.stackview.setColormap("viridis")
- colormap = self.stackview.getColormap()
-
- # Colormap autoscale to image
- self.assertEqual(colormap.getVRange(), (None, None))
- self.stackview.scaleColormapRangeToStack()
-
- # Colormap range set according to stack range
- self.assertEqual(colormap.getVRange(), (self.mystack.min(), self.mystack.max()))
-
- def testSetStack(self):
- self.stackview.setStack(self.mystack)
- self.stackview.setColormap("viridis", autoscale=True)
- my_trans_stack, params = self.stackview.getStack()
- self.assertEqual(my_trans_stack.shape, self.mystack.shape)
- self.assertTrue(numpy.array_equal(self.mystack,
- my_trans_stack))
- self.assertEqual(params["colormap"]["name"],
- "viridis")
-
- def testSetStackPerspective(self):
- self.stackview.setStack(self.mystack, perspective=1)
- # my_orig_stack, params = self.stackview.getStack()
- my_trans_stack, params = self.stackview.getCurrentView()
-
- # get stack returns the transposed data, depending on the perspective
- self.assertEqual(my_trans_stack.shape,
- (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2]))
- self.assertTrue(numpy.array_equal(numpy.transpose(self.mystack, axes=(1, 0, 2)),
- my_trans_stack))
-
- def testSetStackListOfImages(self):
- loi = [self.mystack[i] for i in range(self.mystack.shape[0])]
-
- self.stackview.setStack(loi)
- my_orig_stack, params = self.stackview.getStack(returnNumpyArray=True)
- my_trans_stack, params = self.stackview.getStack(returnNumpyArray=True)
- self.assertEqual(my_trans_stack.shape, self.mystack.shape)
- self.assertTrue(numpy.array_equal(self.mystack,
- my_trans_stack))
- self.assertTrue(numpy.array_equal(self.mystack,
- my_orig_stack))
- self.assertIsInstance(my_trans_stack, numpy.ndarray)
-
- self.stackview.setStack(loi, perspective=2)
- my_orig_stack, params = self.stackview.getStack(copy=False)
- my_trans_stack, params = self.stackview.getCurrentView(copy=False)
- # getStack(copy=False) must return the object set in setStack
- self.assertIs(my_orig_stack, loi)
- # getCurrentView(copy=False) returns a ListOfImages whose .images
- # attr is the original data
- self.assertEqual(my_trans_stack.shape,
- (self.mystack.shape[2], self.mystack.shape[0], self.mystack.shape[1]))
- self.assertTrue(numpy.array_equal(numpy.array(my_trans_stack),
- numpy.transpose(self.mystack, axes=(2, 0, 1))))
- self.assertIsInstance(my_trans_stack,
- ListOfImages) # returnNumpyArray=False by default in getStack
- self.assertIs(my_trans_stack.images, loi)
-
- def testPerspective(self):
- self.stackview.setStack(numpy.arange(24).reshape((2, 3, 4)))
- self.assertEqual(self.stackview._perspective, 0,
- "Default perspective is not 0 (dim1-dim2).")
-
- self.stackview._StackView__planeSelection.setPerspective(1)
- self.assertEqual(self.stackview._perspective, 1,
- "Plane selection combobox not updating perspective")
-
- self.stackview.setStack(numpy.arange(6).reshape((1, 2, 3)))
- self.assertEqual(self.stackview._perspective, 1,
- "Perspective not preserved when calling setStack "
- "without specifying the perspective parameter.")
-
- self.stackview.setStack(numpy.arange(24).reshape((2, 3, 4)), perspective=2)
- self.assertEqual(self.stackview._perspective, 2,
- "Perspective not set in setStack(..., perspective=2).")
-
- def testDefaultTitle(self):
- """Test that the plot title contains the proper Z information"""
- self.stackview.setStack(numpy.arange(24).reshape((4, 3, 2)),
- calibrations=[(0, 1), (-10, 10), (3.14, 3.14)])
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=0")
- self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=2")
-
- self.stackview._StackView__planeSelection.setPerspective(1)
- self.stackview.setFrameNumber(0)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=-10")
- self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=10")
-
- self.stackview._StackView__planeSelection.setPerspective(2)
- self.stackview.setFrameNumber(0)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=3.14")
- self.stackview.setFrameNumber(1)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Image z=6.28")
-
- def testCustomTitle(self):
- """Test setting the plot title with a user defined callback"""
- self.stackview.setStack(numpy.arange(24).reshape((4, 3, 2)),
- calibrations=[(0, 1), (-10, 10), (3.14, 3.14)])
-
- def title_callback(frame_idx):
- return "Cubed index title %d" % (frame_idx**3)
-
- self.stackview.setTitleCallback(title_callback)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 0")
- self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 8")
-
- # perspective should not matter, only frame index
- self.stackview._StackView__planeSelection.setPerspective(1)
- self.stackview.setFrameNumber(0)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 0")
- self.stackview.setFrameNumber(2)
- self.assertEqual(self.stackview._plot.getGraphTitle(),
- "Cubed index title 8")
-
- with self.assertRaises(TypeError):
- # setTitleCallback should not accept non-callable objects like strings
- self.stackview.setTitleCallback(
- "Là, vous faites sirop de vingt-et-un et vous dites : "
- "beau sirop, mi-sirop, siroté, gagne-sirop, sirop-grelot,"
- " passe-montagne, sirop au bon goût.")
-
- def testStackFrameNumber(self):
- self.stackview.setStack(self.mystack)
- self.assertEqual(self.stackview.getFrameNumber(), 0)
-
- listener = SignalListener()
- self.stackview.sigFrameChanged.connect(listener)
-
- self.stackview.setFrameNumber(1)
- self.assertEqual(self.stackview.getFrameNumber(), 1)
- self.assertEqual(listener.arguments(), [(1,)])
-
-
-class TestStackViewMainWindow(TestCaseQt):
- """Base class for tests of StackView."""
-
- def setUp(self):
- super(TestStackViewMainWindow, self).setUp()
- self.stackview = StackViewMainWindow()
- self.stackview.show()
- self.qWaitForWindowExposed(self.stackview)
- self.mystack = numpy.fromfunction(
- lambda i, j, k: numpy.sin(i/15.) + numpy.cos(j/4.) + 2 * numpy.sin(k/6.),
- (10, 20, 30)
- )
-
- def tearDown(self):
- self.stackview.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.stackview.close()
- del self.stackview
- super(TestStackViewMainWindow, self).tearDown()
-
- def testSetStack(self):
- self.stackview.setStack(self.mystack)
- self.stackview.setColormap("viridis", autoscale=True)
- my_trans_stack, params = self.stackview.getStack()
- self.assertEqual(my_trans_stack.shape, self.mystack.shape)
- self.assertTrue(numpy.array_equal(self.mystack,
- my_trans_stack))
- self.assertEqual(params["colormap"]["name"],
- "viridis")
-
- def testSetStackPerspective(self):
- self.stackview.setStack(self.mystack, perspective=1)
- my_trans_stack, params = self.stackview.getCurrentView()
- # get stack returns the transposed data, depending on the perspective
- self.assertEqual(my_trans_stack.shape,
- (self.mystack.shape[1], self.mystack.shape[0], self.mystack.shape[2]))
- self.assertTrue(numpy.array_equal(numpy.transpose(self.mystack, axes=(1, 0, 2)),
- my_trans_stack))
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestStackView))
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestStackViewMainWindow))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testStats.py b/silx/gui/plot/test/testStats.py
deleted file mode 100644
index d5046ba..0000000
--- a/silx/gui/plot/test/testStats.py
+++ /dev/null
@@ -1,1058 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-"""Basic tests for CurvesROIWidget"""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "07/03/2018"
-
-
-from silx.gui import qt
-from silx.gui.plot.stats import stats
-from silx.gui.plot import StatsWidget
-from silx.gui.plot.stats import statshandler
-from silx.gui.utils.testutils import TestCaseQt, SignalListener
-from silx.gui.plot import Plot1D, Plot2D
-from silx.gui.plot3d.SceneWidget import SceneWidget
-from silx.gui.plot.items.roi import RectangleROI, PolygonROI
-from silx.gui.plot.tools.roi import RegionOfInterestManager
-from silx.gui.plot.stats.stats import Stats
-from silx.gui.plot.CurvesROIWidget import ROI
-from silx.utils.testutils import ParametricTestCase
-import unittest
-import logging
-import numpy
-
-_logger = logging.getLogger(__name__)
-
-
-class TestStatsBase(object):
- """Base class for stats TestCase"""
- def setUp(self):
- self.createCurveContext()
- self.createImageContext()
- self.createScatterContext()
-
- def tearDown(self):
- self.plot1d.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot1d.close()
- del self.plot1d
- self.plot2d.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot2d.close()
- del self.plot2d
- self.scatterPlot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.scatterPlot.close()
- del self.scatterPlot
-
- def createCurveContext(self):
- self.plot1d = Plot1D()
- x = range(20)
- y = range(20)
- self.plot1d.addCurve(x, y, legend='curve0')
-
- self.curveContext = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
- plot=self.plot1d,
- onlimits=False,
- roi=None)
-
- def createScatterContext(self):
- self.scatterPlot = Plot2D()
- lgd = 'scatter plot'
- self.xScatterData = numpy.array([0, 2, 3, 20, 50, 60, 36])
- self.yScatterData = numpy.array([2, 3, 4, 26, 69, 6, 18])
- self.valuesScatterData = numpy.array([5, 6, 7, 10, 90, 20, 5])
- self.scatterPlot.addScatter(self.xScatterData, self.yScatterData,
- self.valuesScatterData, legend=lgd)
- self.scatterContext = stats._ScatterContext(
- item=self.scatterPlot.getScatter(lgd),
- plot=self.scatterPlot,
- onlimits=False,
- roi=None
- )
-
- def createImageContext(self):
- self.plot2d = Plot2D()
- self._imgLgd = 'test image'
- self.imageData = numpy.arange(32*128).reshape(32, 128)
- self.plot2d.addImage(data=self.imageData,
- legend=self._imgLgd, replace=False)
- self.imageContext = stats._ImageContext(
- item=self.plot2d.getImage(self._imgLgd),
- plot=self.plot2d,
- onlimits=False,
- roi=None
- )
-
- def getBasicStats(self):
- return {
- 'min': stats.StatMin(),
- 'minCoords': stats.StatCoordMin(),
- 'max': stats.StatMax(),
- 'maxCoords': stats.StatCoordMax(),
- 'std': stats.Stat(name='std', fct=numpy.std),
- 'mean': stats.Stat(name='mean', fct=numpy.mean),
- 'com': stats.StatCOM()
- }
-
-
-class TestStats(TestStatsBase, TestCaseQt):
- """
- Test :class:`BaseClass` class and inheriting classes
- """
- def setUp(self):
- TestCaseQt.setUp(self)
- TestStatsBase.setUp(self)
-
- def tearDown(self):
- TestStatsBase.tearDown(self)
- TestCaseQt.tearDown(self)
-
- def testBasicStatsCurve(self):
- """Test result for simple stats on a curve"""
- _stats = self.getBasicStats()
- xData = yData = numpy.array(range(20))
- self.assertEqual(_stats['min'].calculate(self.curveContext), 0)
- self.assertEqual(_stats['max'].calculate(self.curveContext), 19)
- self.assertEqual(_stats['minCoords'].calculate(self.curveContext), (0,))
- self.assertEqual(_stats['maxCoords'].calculate(self.curveContext), (19,))
- self.assertEqual(_stats['std'].calculate(self.curveContext), numpy.std(yData))
- self.assertEqual(_stats['mean'].calculate(self.curveContext), numpy.mean(yData))
- com = numpy.sum(xData * yData) / numpy.sum(yData)
- self.assertEqual(_stats['com'].calculate(self.curveContext), com)
-
- def testBasicStatsImage(self):
- """Test result for simple stats on an image"""
- _stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.imageContext), 0)
- self.assertEqual(_stats['max'].calculate(self.imageContext), 128 * 32 - 1)
- self.assertEqual(_stats['minCoords'].calculate(self.imageContext), (0, 0))
- self.assertEqual(_stats['maxCoords'].calculate(self.imageContext), (127, 31))
- self.assertEqual(_stats['std'].calculate(self.imageContext), numpy.std(self.imageData))
- self.assertEqual(_stats['mean'].calculate(self.imageContext), numpy.mean(self.imageData))
-
- yData = numpy.sum(self.imageData.astype(numpy.float64), axis=1)
- xData = numpy.sum(self.imageData.astype(numpy.float64), axis=0)
- dataXRange = range(self.imageData.shape[1])
- dataYRange = range(self.imageData.shape[0])
-
- ycom = numpy.sum(yData*dataYRange) / numpy.sum(yData)
- xcom = numpy.sum(xData*dataXRange) / numpy.sum(xData)
-
- self.assertEqual(_stats['com'].calculate(self.imageContext), (xcom, ycom))
-
- def testStatsImageAdv(self):
- """Test that scale and origin are taking into account for images"""
-
- image2Data = numpy.arange(32 * 128).reshape(32, 128)
- self.plot2d.addImage(data=image2Data, legend=self._imgLgd,
- replace=True, origin=(100, 10), scale=(2, 0.5))
- image2Context = stats._ImageContext(
- item=self.plot2d.getImage(self._imgLgd),
- plot=self.plot2d,
- onlimits=False,
- roi=None,
- )
- _stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(image2Context), 0)
- self.assertEqual(
- _stats['max'].calculate(image2Context), 128 * 32 - 1)
- self.assertEqual(
- _stats['minCoords'].calculate(image2Context), (100, 10))
- self.assertEqual(
- _stats['maxCoords'].calculate(image2Context), (127*2. + 100,
- 31 * 0.5 + 10))
- self.assertEqual(_stats['std'].calculate(image2Context),
- numpy.std(self.imageData))
- self.assertEqual(_stats['mean'].calculate(image2Context),
- numpy.mean(self.imageData))
-
- yData = numpy.sum(self.imageData, axis=1)
- xData = numpy.sum(self.imageData, axis=0)
- dataXRange = numpy.arange(self.imageData.shape[1], dtype=numpy.float64)
- dataYRange = numpy.arange(self.imageData.shape[0], dtype=numpy.float64)
-
- ycom = numpy.sum(yData * dataYRange) / numpy.sum(yData)
- ycom = (ycom * 0.5) + 10
- xcom = numpy.sum(xData * dataXRange) / numpy.sum(xData)
- xcom = (xcom * 2.) + 100
- self.assertTrue(numpy.allclose(
- _stats['com'].calculate(image2Context), (xcom, ycom)))
-
- def testBasicStatsScatter(self):
- """Test result for simple stats on a scatter"""
- _stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.scatterContext), 5)
- self.assertEqual(_stats['max'].calculate(self.scatterContext), 90)
- self.assertEqual(_stats['minCoords'].calculate(self.scatterContext), (0, 2))
- self.assertEqual(_stats['maxCoords'].calculate(self.scatterContext), (50, 69))
- self.assertEqual(_stats['std'].calculate(self.scatterContext), numpy.std(self.valuesScatterData))
- self.assertEqual(_stats['mean'].calculate(self.scatterContext), numpy.mean(self.valuesScatterData))
-
- data = self.valuesScatterData.astype(numpy.float64)
- comx = numpy.sum(self.xScatterData * data) / numpy.sum(data)
- comy = numpy.sum(self.yScatterData * data) / numpy.sum(data)
- self.assertEqual(_stats['com'].calculate(self.scatterContext),
- (comx, comy))
-
- def testKindNotManagedByStat(self):
- """Make sure an exception is raised if we try to execute calculate
- of the base class"""
- b = stats.StatBase(name='toto', compatibleKinds='curve')
- with self.assertRaises(NotImplementedError):
- b.calculate(self.imageContext)
-
- def testKindNotManagedByContext(self):
- """
- Make sure an error is raised if we try to calculate a statistic with
- a context not managed
- """
- myStat = stats.Stat(name='toto', fct=numpy.std, kinds=('curve'))
- myStat.calculate(self.curveContext)
- with self.assertRaises(ValueError):
- myStat.calculate(self.scatterContext)
- with self.assertRaises(ValueError):
- myStat.calculate(self.imageContext)
-
- def testOnLimits(self):
- stat = stats.StatMin()
-
- self.plot1d.getXAxis().setLimitsConstraints(minPos=2, maxPos=5)
- curveContextOnLimits = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
- plot=self.plot1d,
- onlimits=True,
- roi=None)
- self.assertEqual(stat.calculate(curveContextOnLimits), 2)
-
- self.plot2d.getXAxis().setLimitsConstraints(minPos=32)
- imageContextOnLimits = stats._ImageContext(
- item=self.plot2d.getImage('test image'),
- plot=self.plot2d,
- onlimits=True,
- roi=None)
- self.assertEqual(stat.calculate(imageContextOnLimits), 32)
-
- self.scatterPlot.getXAxis().setLimitsConstraints(minPos=40)
- scatterContextOnLimits = stats._ScatterContext(
- item=self.scatterPlot.getScatter('scatter plot'),
- plot=self.scatterPlot,
- onlimits=True,
- roi=None)
- self.assertEqual(stat.calculate(scatterContextOnLimits), 20)
-
-
-class TestStatsFormatter(TestCaseQt):
- """Simple test to check usage of the :class:`StatsFormatter`"""
- def setUp(self):
- TestCaseQt.setUp(self)
- self.plot1d = Plot1D()
- x = range(20)
- y = range(20)
- self.plot1d.addCurve(x, y, legend='curve0')
-
- self.curveContext = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
- plot=self.plot1d,
- onlimits=False,
- roi=None)
-
- self.stat = stats.StatMin()
-
- def tearDown(self):
- self.plot1d.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot1d.close()
- del self.plot1d
- TestCaseQt.tearDown(self)
-
- def testEmptyFormatter(self):
- """Make sure a formatter with no formatter definition will return a
- simple cast to str"""
- emptyFormatter = statshandler.StatFormatter()
- self.assertEqual(
- emptyFormatter.format(self.stat.calculate(self.curveContext)), '0.000')
-
- def testSettedFormatter(self):
- """Make sure a formatter with no formatter definition will return a
- simple cast to str"""
- formatter= statshandler.StatFormatter(formatter='{0:.3f}')
- self.assertEqual(
- formatter.format(self.stat.calculate(self.curveContext)), '0.000')
-
-
-class TestStatsHandler(TestCaseQt):
- """Make sure the StatHandler is correctly making the link between
- :class:`StatBase` and :class:`StatFormatter` and checking the API is valid
- """
- def setUp(self):
- TestCaseQt.setUp(self)
- self.plot1d = Plot1D()
- x = range(20)
- y = range(20)
- self.plot1d.addCurve(x, y, legend='curve0')
- self.curveItem = self.plot1d.getCurve('curve0')
-
- self.stat = stats.StatMin()
-
- def tearDown(self):
- Stats._getContext.cache_clear()
- self.plot1d.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot1d.close()
- self.plot1d = None
- TestCaseQt.tearDown(self)
-
- def testConstructor(self):
- """Make sure the constructor can deal will all possible arguments:
-
- * tuple of :class:`StatBase` derivated classes
- * tuple of tuples (:class:`StatBase`, :class:`StatFormatter`)
- * tuple of tuples (str, pointer to function, kind)
- """
- handler0 = statshandler.StatsHandler(
- (stats.StatMin(), stats.StatMax())
- )
-
- res = handler0.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('min' in res)
- self.assertEqual(res['min'], '0')
- self.assertTrue('max' in res)
- self.assertEqual(res['max'], '19')
-
- handler1 = statshandler.StatsHandler(
- (
- (stats.StatMin(), statshandler.StatFormatter(formatter=None)),
- (stats.StatMax(), statshandler.StatFormatter())
- )
- )
-
- res = handler1.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('min' in res)
- self.assertEqual(res['min'], '0')
- self.assertTrue('max' in res)
- self.assertEqual(res['max'], '19.000')
-
- handler2 = statshandler.StatsHandler(
- (
- (stats.StatMin(), None),
- (stats.StatMax(), statshandler.StatFormatter())
- ))
-
- res = handler2.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('min' in res)
- self.assertEqual(res['min'], '0')
- self.assertTrue('max' in res)
- self.assertEqual(res['max'], '19.000')
-
- handler3 = statshandler.StatsHandler((
- (('amin', numpy.argmin), statshandler.StatFormatter()),
- ('amax', numpy.argmax)
- ))
-
- res = handler3.calculate(item=self.curveItem, plot=self.plot1d,
- onlimits=False)
- self.assertTrue('amin' in res)
- self.assertEqual(res['amin'], '0.000')
- self.assertTrue('amax' in res)
- self.assertEqual(res['amax'], '19')
-
- with self.assertRaises(ValueError):
- statshandler.StatsHandler(('name'))
-
-
-class TestStatsWidgetWithCurves(TestCaseQt, ParametricTestCase):
- """Basic test for StatsWidget with curves"""
- def setUp(self):
- TestCaseQt.setUp(self)
- self.plot = Plot1D()
- self.plot.show()
- x = range(20)
- y = range(20)
- self.plot.addCurve(x, y, legend='curve0')
- y = range(12, 32)
- self.plot.addCurve(x, y, legend='curve1')
- y = range(-2, 18)
- self.plot.addCurve(x, y, legend='curve2')
- self.widget = StatsWidget.StatsWidget(plot=self.plot)
- self.statsTable = self.widget._statsTable
-
- mystats = statshandler.StatsHandler((
- stats.StatMin(),
- (stats.StatCoordMin(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatMax(),
- (stats.StatCoordMax(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatDelta(),
- ('std', numpy.std),
- ('mean', numpy.mean),
- stats.StatCOM()
- ))
-
- self.statsTable.setStats(mystats)
-
- def tearDown(self):
- Stats._getContext.cache_clear()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- self.statsTable = None
- self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.widget.close()
- self.widget = None
- self.plot = None
- TestCaseQt.tearDown(self)
-
- def testDisplayActiveItemsSyncOptions(self):
- """
- Test that the several option of the sync options are well
- synchronized between the different object"""
- widget = StatsWidget.StatsWidget(plot=self.plot)
- table = StatsWidget.StatsTable(plot=self.plot)
-
- def check_display_only_active_item(only_active):
- # check internal value
- self.assertIs(widget._statsTable._displayOnlyActItem, only_active)
- # self.assertTrue(table._displayOnlyActItem is only_active)
- # check gui display
- self.assertEqual(widget._options.isActiveItemMode(), only_active)
-
- for displayOnlyActiveItems in (True, False):
- with self.subTest(displayOnlyActiveItems=displayOnlyActiveItems):
- widget.setDisplayOnlyActiveItem(displayOnlyActiveItems)
- # table.setDisplayOnlyActiveItem(displayOnlyActiveItems)
- check_display_only_active_item(displayOnlyActiveItems)
-
- check_display_only_active_item(only_active=False)
- widget.setAttribute(qt.Qt.WA_DeleteOnClose)
- table.setAttribute(qt.Qt.WA_DeleteOnClose)
- widget.close()
- table.close()
-
- def testInit(self):
- """Make sure all the curves are registred on initialization"""
- self.assertEqual(self.statsTable.rowCount(), 3)
-
- def testRemoveCurve(self):
- """Make sure the Curves stats take into account the curve removal from
- plot"""
- self.plot.removeCurve('curve2')
- self.assertEqual(self.statsTable.rowCount(), 2)
- for iRow in range(2):
- self.assertTrue(self.statsTable.item(iRow, 0).text() in ('curve0', 'curve1'))
-
- self.plot.removeCurve('curve0')
- self.assertEqual(self.statsTable.rowCount(), 1)
- self.plot.removeCurve('curve1')
- self.assertEqual(self.statsTable.rowCount(), 0)
-
- def testAddCurve(self):
- """Make sure the Curves stats take into account the add curve action"""
- self.plot.addCurve(legend='curve3', x=range(10), y=range(10))
- self.assertEqual(self.statsTable.rowCount(), 4)
-
- def testUpdateCurveFromAddCurve(self):
- """Make sure the stats of the cuve will be removed after updating a
- curve"""
- self.plot.addCurve(legend='curve0', x=range(10), y=range(10))
- self.qapp.processEvents()
- self.assertEqual(self.statsTable.rowCount(), 3)
- curve = self.plot._getItem(kind='curve', legend='curve0')
- tableItems = self.statsTable._itemToTableItems(curve)
- self.assertEqual(tableItems['max'].text(), '9')
-
- def testUpdateCurveFromCurveObj(self):
- self.plot.getCurve('curve0').setData(x=range(4), y=range(4))
- self.qapp.processEvents()
- self.assertEqual(self.statsTable.rowCount(), 3)
- curve = self.plot._getItem(kind='curve', legend='curve0')
- tableItems = self.statsTable._itemToTableItems(curve)
- self.assertEqual(tableItems['max'].text(), '3')
-
- def testSetAnotherPlot(self):
- plot2 = Plot1D()
- plot2.addCurve(x=range(26), y=range(26), legend='new curve')
- self.statsTable.setPlot(plot2)
- self.assertEqual(self.statsTable.rowCount(), 1)
- self.qapp.processEvents()
- plot2.setAttribute(qt.Qt.WA_DeleteOnClose)
- plot2.close()
- plot2 = None
-
- def testUpdateMode(self):
- """Make sure the update modes are well take into account"""
- self.plot.setActiveCurve('curve0')
- for display_only_active in (True, False):
- with self.subTest(display_only_active=display_only_active):
- self.widget.setDisplayOnlyActiveItem(display_only_active)
- self.plot.getCurve('curve0').setData(x=range(4), y=range(4))
- self.widget.setUpdateMode(StatsWidget.UpdateMode.AUTO)
- update_stats_action = self.widget._options.getUpdateStatsAction()
- # test from api
- self.assertEqual(self.widget.getUpdateMode(), StatsWidget.UpdateMode.AUTO)
- self.widget.show()
- # check stats change in auto mode
- self.plot.getCurve('curve0').setData(x=range(4), y=range(-1, 3))
- self.qapp.processEvents()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == -1.)
-
- self.plot.getCurve('curve0').setData(x=range(4), y=range(1, 5))
- self.qapp.processEvents()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == 1.)
-
- # check stats change in manual mode only if requested
- self.widget.setUpdateMode(StatsWidget.UpdateMode.MANUAL)
- self.assertEqual(self.widget.getUpdateMode(), StatsWidget.UpdateMode.MANUAL)
-
- self.plot.getCurve('curve0').setData(x=range(4), y=range(2, 6))
- self.qapp.processEvents()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == 1.)
-
- update_stats_action.trigger()
- tableItems = self.statsTable._itemToTableItems(self.plot.getCurve('curve0'))
- curve0_min = tableItems['min'].text()
- self.assertTrue(float(curve0_min) == 2.)
-
- def testItemHidden(self):
- """Test if an item is hide, then the associated stats item is also
- hide"""
- curve0 = self.plot.getCurve('curve0')
- curve1 = self.plot.getCurve('curve1')
- curve2 = self.plot.getCurve('curve2')
-
- self.plot.show()
- self.widget.show()
- self.qWaitForWindowExposed(self.widget)
- self.assertFalse(self.statsTable.isRowHidden(0))
- self.assertFalse(self.statsTable.isRowHidden(1))
- self.assertFalse(self.statsTable.isRowHidden(2))
-
- curve0.setVisible(False)
- self.qapp.processEvents()
- self.assertTrue(self.statsTable.isRowHidden(0))
- curve0.setVisible(True)
- self.qapp.processEvents()
- self.assertFalse(self.statsTable.isRowHidden(0))
- curve1.setVisible(False)
- self.qapp.processEvents()
- self.assertTrue(self.statsTable.isRowHidden(1))
- tableItems = self.statsTable._itemToTableItems(curve2)
- curve2_min = tableItems['min'].text()
- self.assertTrue(float(curve2_min) == -2.)
-
- curve0.setVisible(False)
- curve1.setVisible(False)
- curve2.setVisible(False)
- self.qapp.processEvents()
- self.assertTrue(self.statsTable.isRowHidden(0))
- self.assertTrue(self.statsTable.isRowHidden(1))
- self.assertTrue(self.statsTable.isRowHidden(2))
-
-
-class TestStatsWidgetWithImages(TestCaseQt):
- """Basic test for StatsWidget with images"""
-
- IMAGE_LEGEND = 'test image'
-
- def setUp(self):
- TestCaseQt.setUp(self)
- self.plot = Plot2D()
-
- self.plot.addImage(data=numpy.arange(128*128).reshape(128, 128),
- legend=self.IMAGE_LEGEND, replace=False)
-
- self.widget = StatsWidget.StatsTable(plot=self.plot)
-
- mystats = statshandler.StatsHandler((
- (stats.StatMin(), statshandler.StatFormatter()),
- (stats.StatCoordMin(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- (stats.StatMax(), statshandler.StatFormatter()),
- (stats.StatCoordMax(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- (stats.StatDelta(), statshandler.StatFormatter()),
- ('std', numpy.std),
- ('mean', numpy.mean),
- (stats.StatCOM(), statshandler.StatFormatter(None))
- ))
-
- self.widget.setStats(mystats)
-
- def tearDown(self):
- Stats._getContext.cache_clear()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.widget.close()
- self.widget = None
- self.plot = None
- TestCaseQt.tearDown(self)
-
- def test(self):
- image = self.plot._getItem(
- kind='image', legend=self.IMAGE_LEGEND)
- tableItems = self.widget._itemToTableItems(image)
-
- maxText = '{0:.3f}'.format((128 * 128) - 1)
- self.assertEqual(tableItems['legend'].text(), self.IMAGE_LEGEND)
- self.assertEqual(tableItems['min'].text(), '0.000')
- self.assertEqual(tableItems['max'].text(), maxText)
- self.assertEqual(tableItems['delta'].text(), maxText)
- self.assertEqual(tableItems['coords min'].text(), '0.0, 0.0')
- self.assertEqual(tableItems['coords max'].text(), '127.0, 127.0')
-
- def testItemHidden(self):
- """Test if an item is hide, then the associated stats item is also
- hide"""
- self.widget.show()
- self.plot.show()
- self.qWaitForWindowExposed(self.widget)
- self.assertFalse(self.widget.isRowHidden(0))
- self.plot.getImage(self.IMAGE_LEGEND).setVisible(False)
- self.qapp.processEvents()
- self.assertTrue(self.widget.isRowHidden(0))
-
-
-class TestStatsWidgetWithScatters(TestCaseQt):
-
- SCATTER_LEGEND = 'scatter plot'
-
- def setUp(self):
- TestCaseQt.setUp(self)
- self.scatterPlot = Plot2D()
- self.scatterPlot.addScatter([0, 1, 2, 20, 50, 60],
- [2, 3, 4, 26, 69, 6],
- [5, 6, 7, 10, 90, 20],
- legend=self.SCATTER_LEGEND)
- self.widget = StatsWidget.StatsTable(plot=self.scatterPlot)
-
- mystats = statshandler.StatsHandler((
- stats.StatMin(),
- (stats.StatCoordMin(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatMax(),
- (stats.StatCoordMax(), statshandler.StatFormatter(None, qt.QTableWidgetItem)),
- stats.StatDelta(),
- ('std', numpy.std),
- ('mean', numpy.mean),
- stats.StatCOM()
- ))
-
- self.widget.setStats(mystats)
-
- def tearDown(self):
- Stats._getContext.cache_clear()
- self.scatterPlot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.scatterPlot.close()
- self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.widget.close()
- self.widget = None
- self.scatterPlot = None
- TestCaseQt.tearDown(self)
-
- def testStats(self):
- scatter = self.scatterPlot._getItem(
- kind='scatter', legend=self.SCATTER_LEGEND)
- tableItems = self.widget._itemToTableItems(scatter)
- self.assertEqual(tableItems['legend'].text(), self.SCATTER_LEGEND)
- self.assertEqual(tableItems['min'].text(), '5')
- self.assertEqual(tableItems['coords min'].text(), '0, 2')
- self.assertEqual(tableItems['max'].text(), '90')
- self.assertEqual(tableItems['coords max'].text(), '50, 69')
- self.assertEqual(tableItems['delta'].text(), '85')
-
-
-class TestEmptyStatsWidget(TestCaseQt):
- def test(self):
- widget = StatsWidget.StatsWidget()
- widget.show()
- self.qWaitForWindowExposed(widget)
-
-
-# skip unit test for pyqt4 because there is some unrealised widget without
-# apparent reason
-@unittest.skipIf(qt.qVersion().split('.')[0] == '4', reason='PyQt4 not tested')
-class TestLineWidget(TestCaseQt):
- """Some test for the StatsLineWidget."""
- def setUp(self):
- TestCaseQt.setUp(self)
-
- mystats = statshandler.StatsHandler((
- (stats.StatMin(), statshandler.StatFormatter()),
- ))
-
- self.plot = Plot1D()
- self.plot.show()
- self.x = range(20)
- self.y0 = range(20)
- self.curve0 = self.plot.addCurve(self.x, self.y0, legend='curve0')
- self.y1 = range(12, 32)
- self.plot.addCurve(self.x, self.y1, legend='curve1')
- self.y2 = range(-2, 18)
- self.plot.addCurve(self.x, self.y2, legend='curve2')
- self.widget = StatsWidget.BasicGridStatsWidget(plot=self.plot,
- kind='curve',
- stats=mystats)
-
- def tearDown(self):
- Stats._getContext.cache_clear()
- self.qapp.processEvents()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- self.widget.setPlot(None)
- self.widget._lineStatsWidget._statQlineEdit.clear()
- self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.widget.close()
- self.widget = None
- self.plot = None
- TestCaseQt.tearDown(self)
-
- def testProcessing(self):
- self.widget._lineStatsWidget.setStatsOnVisibleData(False)
- self.qapp.processEvents()
- self.plot.setActiveCurve(legend='curve0')
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '0.000')
- self.plot.setActiveCurve(legend='curve1')
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '12.000')
- self.plot.getXAxis().setLimitsConstraints(minPos=2, maxPos=5)
- self.widget.setStatsOnVisibleData(True)
- self.qapp.processEvents()
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '14.000')
- self.plot.setActiveCurve(None)
- self.assertIsNone(self.plot.getActiveCurve())
- self.widget.setStatsOnVisibleData(False)
- self.qapp.processEvents()
- self.assertFalse(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '14.000')
- self.widget.setKind('image')
- self.plot.addImage(numpy.arange(100*100).reshape(100, 100) + 0.312)
- self.qapp.processEvents()
- self.assertTrue(self.widget._lineStatsWidget._statQlineEdit['min'].text() == '0.312')
-
- def testUpdateMode(self):
- """Make sure the update modes are well take into account"""
- self.plot.setActiveCurve(self.curve0)
- _autoRB = self.widget._options._autoRB
- _manualRB = self.widget._options._manualRB
- # test from api
- self.widget.setUpdateMode(StatsWidget.UpdateMode.AUTO)
- self.assertTrue(_autoRB.isChecked())
- self.assertFalse(_manualRB.isChecked())
-
- # check stats change in auto mode
- curve0_min = self.widget._lineStatsWidget._statQlineEdit['min'].text()
- new_y = numpy.array(self.y0) - 2.56
- self.plot.addCurve(x=self.x, y=new_y, legend=self.curve0)
- curve0_min2 = self.widget._lineStatsWidget._statQlineEdit['min'].text()
- self.assertTrue(curve0_min != curve0_min2)
-
- # check stats change in manual mode only if requested
- self.widget.setUpdateMode(StatsWidget.UpdateMode.MANUAL)
- self.assertFalse(_autoRB.isChecked())
- self.assertTrue(_manualRB.isChecked())
-
- new_y = numpy.array(self.y0) - 1.2
- self.plot.addCurve(x=self.x, y=new_y, legend=self.curve0)
- curve0_min3 = self.widget._lineStatsWidget._statQlineEdit['min'].text()
- self.assertTrue(curve0_min3 == curve0_min2)
- self.widget._options._updateRequested()
- curve0_min3 = self.widget._lineStatsWidget._statQlineEdit['min'].text()
- self.assertTrue(curve0_min3 != curve0_min2)
-
- # test from gui
- self.widget.showRadioButtons(True)
- self.widget._options._autoRB.toggle()
- self.assertTrue(_autoRB.isChecked())
- self.assertFalse(_manualRB.isChecked())
-
- self.widget._options._manualRB.toggle()
- self.assertFalse(_autoRB.isChecked())
- self.assertTrue(_manualRB.isChecked())
-
-
-class TestUpdateModeWidget(TestCaseQt):
- """Test UpdateModeWidget"""
- def setUp(self):
- TestCaseQt.setUp(self)
- self.widget = StatsWidget.UpdateModeWidget(parent=None)
-
- def tearDown(self):
- self.widget.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.widget.close()
- self.widget = None
- TestCaseQt.tearDown(self)
-
- def testSignals(self):
- """Test the signal emission of the widget"""
- self.widget.setUpdateMode(StatsWidget.UpdateMode.AUTO)
- modeChangedListener = SignalListener()
- manualUpdateListener = SignalListener()
- self.widget.sigUpdateModeChanged.connect(modeChangedListener)
- self.widget.sigUpdateRequested.connect(manualUpdateListener)
- self.widget.setUpdateMode(StatsWidget.UpdateMode.AUTO)
- self.assertEqual(self.widget.getUpdateMode(), StatsWidget.UpdateMode.AUTO)
- self.assertEqual(modeChangedListener.callCount(), 0)
- self.qapp.processEvents()
-
- self.widget.setUpdateMode(StatsWidget.UpdateMode.MANUAL)
- self.assertEqual(self.widget.getUpdateMode(), StatsWidget.UpdateMode.MANUAL)
- self.qapp.processEvents()
- self.assertEqual(modeChangedListener.callCount(), 1)
- self.assertEqual(manualUpdateListener.callCount(), 0)
- self.widget._updatePB.click()
- self.widget._updatePB.click()
- self.assertEqual(manualUpdateListener.callCount(), 2)
-
- self.widget._autoRB.setChecked(True)
- self.assertEqual(modeChangedListener.callCount(), 2)
- self.widget._updatePB.click()
- self.assertEqual(manualUpdateListener.callCount(), 2)
-
-
-class TestStatsROI(TestStatsBase, TestCaseQt):
- """
- Test stats based on ROI
- """
- def setUp(self):
- TestCaseQt.setUp(self)
- self.createRois()
- TestStatsBase.setUp(self)
- self.createHistogramContext()
-
- self.roiManager = RegionOfInterestManager(self.plot2d)
- self.roiManager.addRoi(self._2Droi_rect)
- self.roiManager.addRoi(self._2Droi_poly)
-
- def tearDown(self):
- self.roiManager.clear()
- self.roiManager = None
- self._1Droi = None
- self._2Droi_rect = None
- self._2Droi_poly = None
- self.plotHisto.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plotHisto.close()
- self.plotHisto = None
- TestStatsBase.tearDown(self)
- TestCaseQt.tearDown(self)
-
- def createRois(self):
- self._1Droi = ROI(name='my1DRoi', fromdata=2.0, todata=5.0)
- self._2Droi_rect = RectangleROI()
- self._2Droi_rect.setGeometry(size=(10, 10), origin=(10, 0))
- self._2Droi_poly = PolygonROI()
- points = numpy.array(((0, 20), (0, 0), (10, 0)))
- self._2Droi_poly.setPoints(points=points)
-
- def createCurveContext(self):
- TestStatsBase.createCurveContext(self)
- self.curveContext = stats._CurveContext(
- item=self.plot1d.getCurve('curve0'),
- plot=self.plot1d,
- onlimits=False,
- roi=self._1Droi)
-
- def createHistogramContext(self):
- self.plotHisto = Plot1D()
- x = range(20)
- y = range(20)
- self.plotHisto.addHistogram(x, y, legend='histo0')
-
- self.histoContext = stats._HistogramContext(
- item=self.plotHisto.getHistogram('histo0'),
- plot=self.plotHisto,
- onlimits=False,
- roi=self._1Droi)
-
- def createScatterContext(self):
- TestStatsBase.createScatterContext(self)
- self.scatterContext = stats._ScatterContext(
- item=self.scatterPlot.getScatter('scatter plot'),
- plot=self.scatterPlot,
- onlimits=False,
- roi=self._1Droi
- )
-
- def createImageContext(self):
- TestStatsBase.createImageContext(self)
-
- self.imageContext = stats._ImageContext(
- item=self.plot2d.getImage(self._imgLgd),
- plot=self.plot2d,
- onlimits=False,
- roi=self._2Droi_rect
- )
-
- self.imageContext_2 = stats._ImageContext(
- item=self.plot2d.getImage(self._imgLgd),
- plot=self.plot2d,
- onlimits=False,
- roi=self._2Droi_poly
- )
-
- def testErrors(self):
- # test if onlimits is True and give also a roi
- with self.assertRaises(ValueError):
- stats._CurveContext(item=self.plot1d.getCurve('curve0'),
- plot=self.plot1d,
- onlimits=True,
- roi=self._1Droi)
-
- # test if is a curve context and give an invalid 2D roi
- with self.assertRaises(TypeError):
- stats._CurveContext(item=self.plot1d.getCurve('curve0'),
- plot=self.plot1d,
- onlimits=False,
- roi=self._2Droi_rect)
-
- def testBasicStatsCurve(self):
- """Test result for simple stats on a curve"""
- _stats = self.getBasicStats()
- xData = yData = numpy.array(range(0, 10))
- self.assertEqual(_stats['min'].calculate(self.curveContext), 2)
- self.assertEqual(_stats['max'].calculate(self.curveContext), 5)
- self.assertEqual(_stats['minCoords'].calculate(self.curveContext), (2,))
- self.assertEqual(_stats['maxCoords'].calculate(self.curveContext), (5,))
- self.assertEqual(_stats['std'].calculate(self.curveContext), numpy.std(yData[2:6]))
- self.assertEqual(_stats['mean'].calculate(self.curveContext), numpy.mean(yData[2:6]))
- com = numpy.sum(xData[2:6] * yData[2:6]) / numpy.sum(yData[2:6])
- self.assertEqual(_stats['com'].calculate(self.curveContext), com)
-
- def testBasicStatsImageRectRoi(self):
- """Test result for simple stats on an image"""
- self.assertEqual(self.imageContext.values.compressed().size, 121)
- _stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.imageContext), 10)
- self.assertEqual(_stats['max'].calculate(self.imageContext), 1300)
- self.assertEqual(_stats['minCoords'].calculate(self.imageContext), (10, 0))
- self.assertEqual(_stats['maxCoords'].calculate(self.imageContext), (20.0, 10.0))
- self.assertAlmostEqual(_stats['std'].calculate(self.imageContext),
- numpy.std(self.imageData[0:11, 10:21]))
- self.assertAlmostEqual(_stats['mean'].calculate(self.imageContext),
- numpy.mean(self.imageData[0:11, 10:21]))
-
- compressed_values = self.imageContext.values.compressed()
- compressed_values = compressed_values.reshape(11, 11)
- yData = numpy.sum(compressed_values.astype(numpy.float64), axis=1)
- xData = numpy.sum(compressed_values.astype(numpy.float64), axis=0)
-
- dataYRange = range(11)
- dataXRange = range(10, 21)
-
- ycom = numpy.sum(yData*dataYRange) / numpy.sum(yData)
- xcom = numpy.sum(xData*dataXRange) / numpy.sum(xData)
- self.assertEqual(_stats['com'].calculate(self.imageContext), (xcom, ycom))
-
- def testBasicStatsImagePolyRoi(self):
- """Test a simple rectangle ROI"""
- _stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.imageContext_2), 0)
- self.assertEqual(_stats['max'].calculate(self.imageContext_2), 2432)
- self.assertEqual(_stats['minCoords'].calculate(self.imageContext_2), (0.0, 0.0))
- # not 0.0, 19.0 because not fully in. Should all pixel have a weight,
- # on to manage them in stats. For now 0 if the center is not in, else 1
- self.assertEqual(_stats['maxCoords'].calculate(self.imageContext_2), (0.0, 19.0))
-
- def testBasicStatsScatter(self):
- self.assertEqual(self.scatterContext.values.compressed().size, 2)
- _stats = self.getBasicStats()
- self.assertEqual(_stats['min'].calculate(self.scatterContext), 6)
- self.assertEqual(_stats['max'].calculate(self.scatterContext), 7)
- self.assertEqual(_stats['minCoords'].calculate(self.scatterContext), (2, 3))
- self.assertEqual(_stats['maxCoords'].calculate(self.scatterContext), (3, 4))
- self.assertEqual(_stats['std'].calculate(self.scatterContext), numpy.std([6, 7]))
- self.assertEqual(_stats['mean'].calculate(self.scatterContext), numpy.mean([6, 7]))
-
- def testBasicHistogram(self):
- _stats = self.getBasicStats()
- xData = yData = numpy.array(range(2, 6))
- self.assertEqual(_stats['min'].calculate(self.histoContext), 2)
- self.assertEqual(_stats['max'].calculate(self.histoContext), 5)
- self.assertEqual(_stats['minCoords'].calculate(self.histoContext), (2,))
- self.assertEqual(_stats['maxCoords'].calculate(self.histoContext), (5,))
- self.assertEqual(_stats['std'].calculate(self.histoContext), numpy.std(yData))
- self.assertEqual(_stats['mean'].calculate(self.histoContext), numpy.mean(yData))
- com = numpy.sum(xData * yData) / numpy.sum(yData)
- self.assertEqual(_stats['com'].calculate(self.histoContext), com)
-
-
-class TestAdvancedROIImageContext(TestCaseQt):
- """Test stats result on an image context with different scale and
- origins"""
-
- def setUp(self):
- TestCaseQt.setUp(self)
- self.data_dims = (100, 100)
- self.data = numpy.random.rand(*self.data_dims)
- self.plot = Plot2D()
-
- def test(self):
- """Test stats result on an image context with different scale and
- origins"""
- roi_origins = [(0, 0), (2, 10), (14, 20)]
- img_origins = [(0, 0), (14, 20), (2, 10)]
- img_scales = [1.0, 0.5, 2.0]
- _stats = {'sum': stats.Stat(name='sum', fct=numpy.sum), }
- for roi_origin in roi_origins:
- for img_origin in img_origins:
- for img_scale in img_scales:
- with self.subTest(roi_origin=roi_origin,
- img_origin=img_origin,
- img_scale=img_scale):
- self.plot.addImage(self.data, legend='img',
- origin=img_origin,
- scale=img_scale)
- roi = RectangleROI()
- roi.setGeometry(origin=roi_origin, size=(20, 20))
- context = stats._ImageContext(
- item=self.plot.getImage('img'),
- plot=self.plot,
- onlimits=False,
- roi=roi)
- x_start = int((roi_origin[0] - img_origin[0]) / img_scale)
- x_end = int(x_start + (20 / img_scale)) + 1
- y_start = int((roi_origin[1] - img_origin[1])/ img_scale)
- y_end = int(y_start + (20 / img_scale)) + 1
- x_start = max(x_start, 0)
- x_end = min(max(x_end, 0), self.data_dims[1])
- y_start = max(y_start, 0)
- y_end = min(max(y_end, 0), self.data_dims[0])
- th_sum = numpy.sum(self.data[y_start:y_end, x_start:x_end])
- self.assertAlmostEqual(_stats['sum'].calculate(context),
- th_sum)
-
-def suite():
- test_suite = unittest.TestSuite()
- for TestClass in (TestStats, TestStatsHandler, TestStatsWidgetWithScatters,
- TestStatsWidgetWithImages, TestStatsWidgetWithCurves,
- TestStatsFormatter, TestEmptyStatsWidget, TestStatsROI,
- TestLineWidget, TestUpdateModeWidget, ):
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestClass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testUtilsAxis.py b/silx/gui/plot/test/testUtilsAxis.py
deleted file mode 100644
index 64373b8..0000000
--- a/silx/gui/plot/test/testUtilsAxis.py
+++ /dev/null
@@ -1,214 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016 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.
-#
-# ###########################################################################*/
-"""Basic tests for PlotWidget"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "20/11/2018"
-
-
-import unittest
-from silx.gui.plot import PlotWidget
-from silx.gui.utils.testutils import TestCaseQt
-from silx.gui.plot.utils.axis import SyncAxes
-
-
-class TestAxisSync(TestCaseQt):
- """Tests AxisSync class"""
-
- def setUp(self):
- TestCaseQt.setUp(self)
- self.plot1 = PlotWidget()
- self.plot2 = PlotWidget()
- self.plot3 = PlotWidget()
-
- def tearDown(self):
- self.plot1 = None
- self.plot2 = None
- self.plot3 = None
- TestCaseQt.tearDown(self)
-
- def testMoveFirstAxis(self):
- """Test synchronization after construction"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
-
- self.plot1.getXAxis().setLimits(10, 500)
- self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot2.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot3.getXAxis().getLimits(), (10, 500))
-
- def testMoveSecondAxis(self):
- """Test synchronization after construction"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
-
- self.plot2.getXAxis().setLimits(10, 500)
- self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot2.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot3.getXAxis().getLimits(), (10, 500))
-
- def testMoveTwoAxes(self):
- """Test synchronization after construction"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
-
- self.plot1.getXAxis().setLimits(1, 50)
- self.plot2.getXAxis().setLimits(10, 500)
- self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot2.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot3.getXAxis().getLimits(), (10, 500))
-
- def testDestruction(self):
- """Test synchronization when sync object is destroyed"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
- del sync
-
- self.plot1.getXAxis().setLimits(10, 500)
- self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
- self.assertNotEqual(self.plot2.getXAxis().getLimits(), (10, 500))
- self.assertNotEqual(self.plot3.getXAxis().getLimits(), (10, 500))
-
- def testAxisDestruction(self):
- """Test synchronization when an axis disappear"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
-
- # Destroy the plot is possible
- import weakref
- plot = weakref.ref(self.plot2)
- self.plot2 = None
- result = self.qWaitForDestroy(plot)
- if not result:
- # We can't test
- self.skipTest("Object not destroyed")
-
- self.plot1.getXAxis().setLimits(10, 500)
- self.assertEqual(self.plot3.getXAxis().getLimits(), (10, 500))
-
- def testStop(self):
- """Test synchronization after calling stop"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
- sync.stop()
-
- self.plot1.getXAxis().setLimits(10, 500)
- self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
- self.assertNotEqual(self.plot2.getXAxis().getLimits(), (10, 500))
- self.assertNotEqual(self.plot3.getXAxis().getLimits(), (10, 500))
-
- def testStopMovingStart(self):
- """Test synchronization after calling stop, moving an axis, then start again"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
- sync.stop()
- self.plot1.getXAxis().setLimits(10, 500)
- self.plot2.getXAxis().setLimits(1, 50)
- self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
- sync.start()
-
- # The first axis is the reference
- self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot2.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot3.getXAxis().getLimits(), (10, 500))
-
- def testDoubleStop(self):
- """Test double stop"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
- sync.stop()
- self.assertRaises(RuntimeError, sync.stop)
-
- def testDoubleStart(self):
- """Test double stop"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
- self.assertRaises(RuntimeError, sync.start)
-
- def testScale(self):
- """Test scale change"""
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
- self.plot1.getXAxis().setScale(self.plot1.getXAxis().LOGARITHMIC)
- self.assertEqual(self.plot1.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC)
- self.assertEqual(self.plot2.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC)
- self.assertEqual(self.plot3.getXAxis().getScale(), self.plot1.getXAxis().LOGARITHMIC)
-
- def testDirection(self):
- """Test direction change"""
- _sync = SyncAxes([self.plot1.getYAxis(), self.plot2.getYAxis(), self.plot3.getYAxis()])
- self.plot1.getYAxis().setInverted(True)
- self.assertEqual(self.plot1.getYAxis().isInverted(), True)
- self.assertEqual(self.plot2.getYAxis().isInverted(), True)
- self.assertEqual(self.plot3.getYAxis().isInverted(), True)
-
- def testSyncCenter(self):
- """Test direction change"""
- # Not the same scale
- self.plot1.getXAxis().setLimits(0, 200)
- self.plot2.getXAxis().setLimits(0, 20)
- self.plot3.getXAxis().setLimits(0, 2)
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()],
- syncLimits=False, syncCenter=True)
-
- self.assertEqual(self.plot1.getXAxis().getLimits(), (0, 200))
- self.assertEqual(self.plot2.getXAxis().getLimits(), (100 - 10, 100 + 10))
- self.assertEqual(self.plot3.getXAxis().getLimits(), (100 - 1, 100 + 1))
-
- def testSyncCenterAndZoom(self):
- """Test direction change"""
- # Not the same scale
- self.plot1.getXAxis().setLimits(0, 200)
- self.plot2.getXAxis().setLimits(0, 20)
- self.plot3.getXAxis().setLimits(0, 2)
- _sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()],
- syncLimits=False, syncCenter=True, syncZoom=True)
-
- # Supposing all the plots use the same size
- self.assertEqual(self.plot1.getXAxis().getLimits(), (0, 200))
- self.assertEqual(self.plot2.getXAxis().getLimits(), (0, 200))
- self.assertEqual(self.plot3.getXAxis().getLimits(), (0, 200))
-
- def testAddAxis(self):
- """Test synchronization after construction"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis()])
- sync.addAxis(self.plot3.getXAxis())
-
- self.plot1.getXAxis().setLimits(10, 500)
- self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot2.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot3.getXAxis().getLimits(), (10, 500))
-
- def testRemoveAxis(self):
- """Test synchronization after construction"""
- sync = SyncAxes([self.plot1.getXAxis(), self.plot2.getXAxis(), self.plot3.getXAxis()])
- sync.removeAxis(self.plot3.getXAxis())
-
- self.plot1.getXAxis().setLimits(10, 500)
- self.assertEqual(self.plot1.getXAxis().getLimits(), (10, 500))
- self.assertEqual(self.plot2.getXAxis().getLimits(), (10, 500))
- self.assertNotEqual(self.plot3.getXAxis().getLimits(), (10, 500))
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loadTests(TestAxisSync))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/utils.py b/silx/gui/plot/test/utils.py
deleted file mode 100644
index ed1917a..0000000
--- a/silx/gui/plot/test/utils.py
+++ /dev/null
@@ -1,94 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 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.
-#
-# ###########################################################################*/
-"""Basic tests for PlotWidget"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "26/01/2018"
-
-
-import logging
-
-from silx.gui.utils.testutils import TestCaseQt
-
-from silx.gui import qt
-from silx.gui.plot import PlotWidget
-
-
-logger = logging.getLogger(__name__)
-
-
-class PlotWidgetTestCase(TestCaseQt):
- """Base class for tests of PlotWidget, not a TestCase in itself.
-
- plot attribute is the PlotWidget created for the test.
- """
-
- __screenshot_already_taken = False
-
- def __init__(self, methodName='runTest', backend=None):
- TestCaseQt.__init__(self, methodName=methodName)
- self.__backend = backend
-
- def _createPlot(self):
- return PlotWidget(backend=self.__backend)
-
- def setUp(self):
- super(PlotWidgetTestCase, self).setUp()
- self.plot = self._createPlot()
- self.plot.show()
- self.plotAlive = True
- self.qWaitForWindowExposed(self.plot)
- TestCaseQt.mouseClick(self, self.plot, button=qt.Qt.LeftButton, pos=(0, 0))
-
- def __onPlotDestroyed(self):
- self.plotAlive = False
-
- def _waitForPlotClosed(self):
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.destroyed.connect(self.__onPlotDestroyed)
- self.plot.close()
- del self.plot
- for _ in range(100):
- if not self.plotAlive:
- break
- self.qWait(10)
- else:
- logger.error("Plot is still alive")
-
- def tearDown(self):
- if not self._currentTestSucceeded():
- # MPL is the only widget which uses the real system mouse.
- # In case of a the windows is outside of the screen, minimzed,
- # overlapped by a system popup, the MPL widget will not receive the
- # mouse event.
- # Taking a screenshot help debuging this cases in the continuous
- # integration environement.
- if not PlotWidgetTestCase.__screenshot_already_taken:
- PlotWidgetTestCase.__screenshot_already_taken = True
- self.logScreenShot()
- self.qapp.processEvents()
- self._waitForPlotClosed()
- super(PlotWidgetTestCase, self).tearDown()
diff --git a/silx/gui/plot/tools/CurveLegendsWidget.py b/silx/gui/plot/tools/CurveLegendsWidget.py
deleted file mode 100644
index 4a517dd..0000000
--- a/silx/gui/plot/tools/CurveLegendsWidget.py
+++ /dev/null
@@ -1,247 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 display :class:`PlotWidget` curve legends.
-"""
-
-from __future__ import division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "20/07/2018"
-
-
-import logging
-import weakref
-
-
-from ... import qt
-from ...widgets.FlowLayout import FlowLayout as _FlowLayout
-from ..LegendSelector import LegendIcon as _LegendIcon
-from .. import items
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _LegendWidget(qt.QWidget):
- """Widget displaying curve style and its legend
-
- :param QWidget parent: See :class:`QWidget`
- :param ~silx.gui.plot.items.Curve curve: Associated curve
- """
-
- def __init__(self, parent, curve):
- super(_LegendWidget, self).__init__(parent)
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(10, 0, 10, 0)
-
- curve.sigItemChanged.connect(self._curveChanged)
-
- icon = _LegendIcon(curve=curve)
- layout.addWidget(icon)
-
- label = qt.QLabel(curve.getName())
- label.setAlignment(qt.Qt.AlignLeft | qt.Qt.AlignVCenter)
- layout.addWidget(label)
-
- self._update()
-
- def getCurve(self):
- """Returns curve associated to this widget
-
- :rtype: Union[~silx.gui.plot.items.Curve,None]
- """
- icon = self.findChild(_LegendIcon)
- return icon.getCurve()
-
- def _update(self):
- """Update widget according to current curve state.
- """
- curve = self.getCurve()
- if curve is None:
- _logger.error('Curve no more exists')
- self.setVisible(False)
- return
-
- self.setEnabled(curve.isVisible())
-
- label = self.findChild(qt.QLabel)
- if curve.isHighlighted():
- label.setStyleSheet("border: 1px solid black")
- else:
- label.setStyleSheet("")
-
- def _curveChanged(self, event):
- """Handle update of curve item
-
- :param event: Kind of change
- """
- if event in (items.ItemChangedType.VISIBLE,
- items.ItemChangedType.HIGHLIGHTED,
- items.ItemChangedType.HIGHLIGHTED_STYLE):
- self._update()
-
-
-class CurveLegendsWidget(qt.QWidget):
- """Widget displaying curves legends in a plot
-
- :param QWidget parent: See :class:`QWidget`
- """
-
- sigCurveClicked = qt.Signal(object)
- """Signal emitted when the legend of a curve is clicked
-
- It provides the corresponding curve.
- """
-
- def __init__(self, parent=None):
- super(CurveLegendsWidget, self).__init__(parent)
- self._clicked = None
- self._legends = {}
- self._plotRef = None
-
- def layout(self):
- layout = super(CurveLegendsWidget, self).layout()
- if layout is None:
- # Lazy layout initialization to allow overloading
- layout = _FlowLayout()
- layout.setHorizontalSpacing(0)
- self.setLayout(layout)
- return layout
-
- def getPlotWidget(self):
- """Returns the associated :class:`PlotWidget`
-
- :rtype: Union[~silx.gui.plot.PlotWidget,None]
- """
- return None if self._plotRef is None else self._plotRef()
-
- def setPlotWidget(self, plot):
- """Set the associated :class:`PlotWidget`
-
- :param ~silx.gui.plot.PlotWidget plot: Plot widget to attach
- """
- previousPlot = self.getPlotWidget()
- if previousPlot is not None:
- previousPlot.sigItemAdded.disconnect( self._itemAdded)
- previousPlot.sigItemAboutToBeRemoved.disconnect(self._itemRemoved)
- for legend in list(self._legends.keys()):
- self._removeLegend(legend)
-
- self._plotRef = None if plot is None else weakref.ref(plot)
-
- if plot is not None:
- plot.sigItemAdded.connect(self._itemAdded)
- plot.sigItemAboutToBeRemoved.connect(self._itemRemoved)
-
- for legend in plot.getAllCurves(just_legend=True):
- self._addLegend(legend)
-
- def curveAt(self, *args):
- """Returns the curve object represented at the given position
-
- Either takes a QPoint or x and y as input in widget coordinates.
-
- :rtype: Union[~silx.gui.plot.items.Curve,None]
- """
- if len(args) == 1:
- point = args[0]
- elif len(args) == 2:
- point = qt.QPoint(*args)
- else:
- raise ValueError('Unsupported arguments')
- assert isinstance(point, qt.QPoint)
-
- widget = self.childAt(point)
- while widget not in (self, None):
- if isinstance(widget, _LegendWidget):
- return widget.getCurve()
- widget = widget.parent()
- return None # No widget or not in _LegendWidget
-
- def _itemAdded(self, item):
- """Handle item added to the plot content"""
- if isinstance(item, items.Curve):
- self._addLegend(item.getName())
-
- def _itemRemoved(self, item):
- """Handle item removed from the plot content"""
- if isinstance(item, items.Curve):
- self._removeLegend(item.getName())
-
- def _addLegend(self, legend):
- """Add a curve to the legends
-
- :param str legend: Curve's legend
- """
- if legend in self._legends:
- return # Can happen when changing curve's y axis
-
- plot = self.getPlotWidget()
- if plot is None:
- return None
-
- curve = plot.getCurve(legend)
- if curve is None:
- _logger.error('Curve not found: %s' % legend)
- return
-
- widget = _LegendWidget(parent=self, curve=curve)
- self.layout().addWidget(widget)
- self._legends[legend] = widget
-
- def _removeLegend(self, legend):
- """Remove a curve from the legends if it exists
-
- :param str legend: The curve's legend
- """
- widget = self._legends.pop(legend, None)
- if widget is None:
- _logger.warning('Unknown legend: %s' % legend)
- else:
- self.layout().removeWidget(widget)
- widget.setParent(None)
-
- def mousePressEvent(self, event):
- if event.button() == qt.Qt.LeftButton:
- self._clicked = event.pos()
-
- _CLICK_THRESHOLD = 5
- """Threshold for clicks"""
-
- def mouseMoveEvent(self, event):
- if self._clicked is not None:
- dx = abs(self._clicked.x() - event.pos().x())
- dy = abs(self._clicked.y() - event.pos().y())
- if dx > self._CLICK_THRESHOLD or dy > self._CLICK_THRESHOLD:
- self._clicked = None # Click is cancelled
-
- def mouseReleaseEvent(self, event):
- if event.button() == qt.Qt.LeftButton and self._clicked is not None:
- curve = self.curveAt(event.pos())
- if curve is not None:
- self.sigCurveClicked.emit(curve)
-
- self._clicked = None
diff --git a/silx/gui/plot/tools/LimitsToolBar.py b/silx/gui/plot/tools/LimitsToolBar.py
deleted file mode 100644
index fc192a6..0000000
--- a/silx/gui/plot/tools/LimitsToolBar.py
+++ /dev/null
@@ -1,131 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2018 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.
-#
-# ###########################################################################*/
-"""A toolbar to display and edit limits of a PlotWidget
-"""
-
-
-from __future__ import division
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "16/10/2017"
-
-
-from ... import qt
-from ...widgets.FloatEdit import FloatEdit
-
-
-class LimitsToolBar(qt.QToolBar):
- """QToolBar displaying and controlling the limits of a :class:`PlotWidget`.
-
- To run the following sample code, a QApplication must be initialized.
- First, create a PlotWindow:
-
- >>> from silx.gui.plot import PlotWindow
- >>> plot = PlotWindow() # Create a PlotWindow to add the toolbar to
-
- Then, create the LimitsToolBar and add it to the PlotWindow.
-
- >>> from silx.gui import qt
- >>> from silx.gui.plot.tools import LimitsToolBar
-
- >>> toolbar = LimitsToolBar(plot=plot) # Create the toolbar
- >>> plot.addToolBar(qt.Qt.BottomToolBarArea, toolbar) # Add it to the plot
- >>> plot.show() # To display the PlotWindow with the limits toolbar
-
- :param parent: See :class:`QToolBar`.
- :param plot: :class:`PlotWidget` instance on which to operate.
- :param str title: See :class:`QToolBar`.
- """
-
- def __init__(self, parent=None, plot=None, title='Limits'):
- super(LimitsToolBar, self).__init__(title, parent)
- assert plot is not None
- self._plot = plot
- self._plot.sigPlotSignal.connect(self._plotWidgetSlot)
-
- self._initWidgets()
-
- @property
- def plot(self):
- """The :class:`PlotWidget` the toolbar is attached to."""
- return self._plot
-
- def _initWidgets(self):
- """Create and init Toolbar widgets."""
- xMin, xMax = self.plot.getXAxis().getLimits()
- yMin, yMax = self.plot.getYAxis().getLimits()
-
- self.addWidget(qt.QLabel('Limits: '))
- self.addWidget(qt.QLabel(' X: '))
- self._xMinFloatEdit = FloatEdit(self, xMin)
- self._xMinFloatEdit.editingFinished[()].connect(
- self._xFloatEditChanged)
- self.addWidget(self._xMinFloatEdit)
-
- self._xMaxFloatEdit = FloatEdit(self, xMax)
- self._xMaxFloatEdit.editingFinished[()].connect(
- self._xFloatEditChanged)
- self.addWidget(self._xMaxFloatEdit)
-
- self.addWidget(qt.QLabel(' Y: '))
- self._yMinFloatEdit = FloatEdit(self, yMin)
- self._yMinFloatEdit.editingFinished[()].connect(
- self._yFloatEditChanged)
- self.addWidget(self._yMinFloatEdit)
-
- self._yMaxFloatEdit = FloatEdit(self, yMax)
- self._yMaxFloatEdit.editingFinished[()].connect(
- self._yFloatEditChanged)
- self.addWidget(self._yMaxFloatEdit)
-
- def _plotWidgetSlot(self, event):
- """Listen to :class:`PlotWidget` events."""
- if event['event'] not in ('limitsChanged',):
- return
-
- xMin, xMax = self.plot.getXAxis().getLimits()
- yMin, yMax = self.plot.getYAxis().getLimits()
-
- self._xMinFloatEdit.setValue(xMin)
- self._xMaxFloatEdit.setValue(xMax)
- self._yMinFloatEdit.setValue(yMin)
- self._yMaxFloatEdit.setValue(yMax)
-
- def _xFloatEditChanged(self):
- """Handle X limits changed from the GUI."""
- xMin, xMax = self._xMinFloatEdit.value(), self._xMaxFloatEdit.value()
- if xMax < xMin:
- xMin, xMax = xMax, xMin
-
- self.plot.getXAxis().setLimits(xMin, xMax)
-
- def _yFloatEditChanged(self):
- """Handle Y limits changed from the GUI."""
- yMin, yMax = self._yMinFloatEdit.value(), self._yMaxFloatEdit.value()
- if yMax < yMin:
- yMin, yMax = yMax, yMin
-
- self.plot.getYAxis().setLimits(yMin, yMax)
diff --git a/silx/gui/plot/tools/PositionInfo.py b/silx/gui/plot/tools/PositionInfo.py
deleted file mode 100644
index 81d312a..0000000
--- a/silx/gui/plot/tools/PositionInfo.py
+++ /dev/null
@@ -1,376 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2021 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 displaying mouse coordinates in a PlotWidget.
-
-It can be configured to provide more information.
-"""
-
-from __future__ import division
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "16/10/2017"
-
-
-import logging
-import numbers
-import traceback
-import weakref
-
-import numpy
-
-from ....utils.deprecation import deprecated
-from ... import qt
-from .. import items
-from ...widgets.ElidedLabel import ElidedLabel
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _PositionInfoLabel(ElidedLabel):
- """QLabel with a default size larger than what is displayed."""
-
- def __init__(self, *args, **kwargs):
- super().__init__(*args, **kwargs)
- self.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
-
- def sizeHint(self):
- hint = super().sizeHint()
- width = self.fontMetrics().boundingRect('##############').width()
- return qt.QSize(max(hint.width(), width), hint.height())
-
-
-# PositionInfo ################################################################
-
-class PositionInfo(qt.QWidget):
- """QWidget displaying coords converted from data coords of the mouse.
-
- Provide this widget with a list of couple:
-
- - A name to display before the data
- - A function that takes (x, y) as arguments and returns something that
- gets converted to a string.
- If the result is a float it is converted with '%.7g' format.
-
- To run the following sample code, a QApplication must be initialized.
- First, create a PlotWindow and add a QToolBar where to place the
- PositionInfo widget.
-
- >>> from silx.gui.plot import PlotWindow
- >>> from silx.gui import qt
-
- >>> plot = PlotWindow() # Create a PlotWindow to add the widget to
- >>> toolBar = qt.QToolBar() # Create a toolbar to place the widget in
- >>> plot.addToolBar(qt.Qt.BottomToolBarArea, toolBar) # Add it to plot
-
- Then, create the PositionInfo widget and add it to the toolbar.
- The PositionInfo widget is created with a list of converters, here
- to display polar coordinates of the mouse position.
-
- >>> import numpy
- >>> from silx.gui.plot.tools import PositionInfo
-
- >>> position = PositionInfo(plot=plot, converters=[
- ... ('Radius', lambda x, y: numpy.sqrt(x*x + y*y)),
- ... ('Angle', lambda x, y: numpy.degrees(numpy.arctan2(y, x)))])
- >>> toolBar.addWidget(position) # Add the widget to the toolbar
- <...>
- >>> plot.show() # To display the PlotWindow with the position widget
-
- :param plot: The PlotWidget this widget is displaying data coords from.
- :param converters:
- List of 2-tuple: name to display and conversion function from (x, y)
- in data coords to displayed value.
- If None, the default, it displays X and Y.
- :param parent: Parent widget
- """
-
- SNAP_THRESHOLD_DIST = 5
-
- def __init__(self, parent=None, plot=None, converters=None):
- assert plot is not None
- self._plotRef = weakref.ref(plot)
- self._snappingMode = self.SNAPPING_DISABLED
-
- super(PositionInfo, self).__init__(parent)
-
- if converters is None:
- converters = (('X', lambda x, y: x), ('Y', lambda x, y: y))
-
- self._fields = [] # To store (QLineEdit, name, function (x, y)->v)
-
- # Create a new layout with new widgets
- layout = qt.QHBoxLayout()
- layout.setContentsMargins(0, 0, 0, 0)
- # layout.setSpacing(0)
-
- # Create all QLabel and store them with the corresponding converter
- for name, func in converters:
- layout.addWidget(qt.QLabel('<b>' + name + ':</b>'))
-
- contentWidget = _PositionInfoLabel(self)
- contentWidget.setText('------')
- layout.addWidget(contentWidget)
- self._fields.append((contentWidget, name, func))
-
- layout.addStretch(1)
- self.setLayout(layout)
-
- # Connect to Plot events
- plot.sigPlotSignal.connect(self._plotEvent)
-
- def getPlotWidget(self):
- """Returns the PlotWidget this widget is attached to or None.
-
- :rtype: Union[~silx.gui.plot.PlotWidget,None]
- """
- return self._plotRef()
-
- @property
- @deprecated(replacement='getPlotWidget', since_version='0.8.0')
- def plot(self):
- return self.getPlotWidget()
-
- def getConverters(self):
- """Return the list of converters as 2-tuple (name, function)."""
- return [(name, func) for _label, name, func in self._fields]
-
- def _plotEvent(self, event):
- """Handle events from the Plot.
-
- :param dict event: Plot event
- """
- if event['event'] == 'mouseMoved':
- x, y = event['x'], event['y']
- xPixel, yPixel = event['xpixel'], event['ypixel']
- self._updateStatusBar(x, y, xPixel, yPixel)
-
- def updateInfo(self):
- """Update displayed information"""
- plot = self.getPlotWidget()
- if plot is None:
- _logger.error("Trying to update PositionInfo "
- "while PlotWidget no longer exists")
- return
-
- widget = plot.getWidgetHandle()
- position = widget.mapFromGlobal(qt.QCursor.pos())
- xPixel, yPixel = position.x(), position.y()
- dataPos = plot.pixelToData(xPixel, yPixel, check=True)
- if dataPos is not None: # Inside plot area
- x, y = dataPos
- self._updateStatusBar(x, y, xPixel, yPixel)
-
- def _updateStatusBar(self, x, y, xPixel, yPixel):
- """Update information from the status bar using the definitions.
-
- :param float x: Position-x in data
- :param float y: Position-y in data
- :param float xPixel: Position-x in pixels
- :param float yPixel: Position-y in pixels
- """
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- styleSheet = "color: rgb(0, 0, 0);" # Default style
- xData, yData = x, y
-
- snappingMode = self.getSnappingMode()
-
- # Snapping when crosshair either not requested or active
- if (snappingMode & (self.SNAPPING_CURVE | self.SNAPPING_SCATTER) and
- (not (snappingMode & self.SNAPPING_CROSSHAIR) or
- plot.getGraphCursor())):
- styleSheet = "color: rgb(255, 0, 0);" # Style far from item
-
- if snappingMode & self.SNAPPING_ACTIVE_ONLY:
- selectedItems = []
-
- if snappingMode & self.SNAPPING_CURVE:
- activeCurve = plot.getActiveCurve()
- if activeCurve:
- selectedItems.append(activeCurve)
-
- if snappingMode & self.SNAPPING_SCATTER:
- activeScatter = plot._getActiveItem(kind='scatter')
- if activeScatter:
- selectedItems.append(activeScatter)
-
- else:
- kinds = []
- if snappingMode & self.SNAPPING_CURVE:
- kinds.append(items.Curve)
- kinds.append(items.Histogram)
- if snappingMode & self.SNAPPING_SCATTER:
- kinds.append(items.Scatter)
- selectedItems = [item for item in plot.getItems()
- if isinstance(item, tuple(kinds)) and item.isVisible()]
-
- # Compute distance threshold
- if qt.BINDING in ('PyQt5', 'PySide2'):
- window = plot.window()
- windowHandle = window.windowHandle()
- if windowHandle is not None:
- ratio = windowHandle.devicePixelRatio()
- else:
- ratio = qt.QGuiApplication.primaryScreen().devicePixelRatio()
- else:
- ratio = 1.
-
- # Baseline squared distance threshold
- distInPixels = (self.SNAP_THRESHOLD_DIST * ratio)**2
-
- for item in selectedItems:
- if (snappingMode & self.SNAPPING_SYMBOLS_ONLY and (
- not isinstance(item, items.SymbolMixIn) or
- not item.getSymbol())):
- # Only handled if item symbols are visible
- continue
-
- if isinstance(item, items.Histogram):
- result = item.pick(xPixel, yPixel)
- if result is not None: # Histogram picked
- index = result.getIndices()[0]
- edges = item.getBinEdgesData(copy=False)
-
- # Snap to bin center and value
- xData = 0.5 * (edges[index] + edges[index + 1])
- yData = item.getValueData(copy=False)[index]
-
- # Update label style sheet
- styleSheet = "color: rgb(0, 0, 0);"
- break
-
- else: # Curve, Scatter
- xArray = item.getXData(copy=False)
- yArray = item.getYData(copy=False)
- closestIndex = numpy.argmin(
- pow(xArray - x, 2) + pow(yArray - y, 2))
-
- xClosest = xArray[closestIndex]
- yClosest = yArray[closestIndex]
-
- if isinstance(item, items.YAxisMixIn):
- axis = item.getYAxis()
- else:
- axis = 'left'
-
- closestInPixels = plot.dataToPixel(
- xClosest, yClosest, axis=axis)
- if closestInPixels is not None:
- curveDistInPixels = (
- (closestInPixels[0] - xPixel)**2 +
- (closestInPixels[1] - yPixel)**2)
-
- if curveDistInPixels <= distInPixels:
- # Update label style sheet
- styleSheet = "color: rgb(0, 0, 0);"
-
- # if close enough, snap to data point coord
- xData, yData = xClosest, yClosest
- distInPixels = curveDistInPixels
-
- for label, name, func in self._fields:
- label.setStyleSheet(styleSheet)
-
- try:
- value = func(xData, yData)
- text = self.valueToString(value)
- label.setText(text)
- except:
- label.setText('Error')
- _logger.error(
- "Error while converting coordinates (%f, %f)"
- "with converter '%s'" % (xPixel, yPixel, 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)
-
- # Snapping mode
-
- SNAPPING_DISABLED = 0
- """No snapping occurs"""
-
- SNAPPING_CROSSHAIR = 1 << 0
- """Snapping only enabled when crosshair cursor is enabled"""
-
- SNAPPING_ACTIVE_ONLY = 1 << 1
- """Snapping only enabled for active item"""
-
- SNAPPING_SYMBOLS_ONLY = 1 << 2
- """Snapping only when symbols are visible"""
-
- SNAPPING_CURVE = 1 << 3
- """Snapping on curves"""
-
- SNAPPING_SCATTER = 1 << 4
- """Snapping on scatter"""
-
- def setSnappingMode(self, mode):
- """Set the snapping mode.
-
- The mode is a mask.
-
- :param int mode: The mode to use
- """
- if mode != self._snappingMode:
- self._snappingMode = mode
- self.updateInfo()
-
- def getSnappingMode(self):
- """Returns the snapping mode as a mask
-
- :rtype: int
- """
- return self._snappingMode
-
- _SNAPPING_LEGACY = (SNAPPING_CROSSHAIR |
- SNAPPING_ACTIVE_ONLY |
- SNAPPING_SYMBOLS_ONLY |
- SNAPPING_CURVE |
- SNAPPING_SCATTER)
- """Legacy snapping mode"""
-
- @property
- @deprecated(replacement="getSnappingMode", since_version="0.8")
- def autoSnapToActiveCurve(self):
- return self.getSnappingMode() == self._SNAPPING_LEGACY
-
- @autoSnapToActiveCurve.setter
- @deprecated(replacement="setSnappingMode", since_version="0.8")
- def autoSnapToActiveCurve(self, flag):
- self.setSnappingMode(
- self._SNAPPING_LEGACY if flag else self.SNAPPING_DISABLED)
diff --git a/silx/gui/plot/tools/RadarView.py b/silx/gui/plot/tools/RadarView.py
deleted file mode 100644
index 7076835..0000000
--- a/silx/gui/plot/tools/RadarView.py
+++ /dev/null
@@ -1,361 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2015-2018 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.
-#
-# ###########################################################################*/
-"""QWidget displaying an overview of a 2D plot.
-
-This shows the available range of the data, and the current location of the
-plot view.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "22/02/2021"
-
-import logging
-import weakref
-from ... import qt
-from ...utils import LockReentrant
-
-_logger = logging.getLogger(__name__)
-
-
-class _DraggableRectItem(qt.QGraphicsRectItem):
- """RectItem which signals its change through visibleRectDragged."""
- def __init__(self, *args, **kwargs):
- super(_DraggableRectItem, self).__init__(
- *args, **kwargs)
-
- self._previousCursor = None
- self.setFlag(qt.QGraphicsItem.ItemIsMovable)
- self.setFlag(qt.QGraphicsItem.ItemSendsGeometryChanges)
- self.setAcceptHoverEvents(True)
- self._ignoreChange = False
- self._constraint = 0, 0, 0, 0
-
- def setConstraintRect(self, left, top, width, height):
- """Set the constraint rectangle for dragging.
-
- The coordinates are in the _DraggableRectItem coordinate system.
-
- This constraint only applies to modification through interaction
- (i.e., this constraint is not applied to change through API).
-
- If the _DraggableRectItem is smaller than the constraint rectangle,
- the _DraggableRectItem remains within the constraint rectangle.
- If the _DraggableRectItem is wider than the constraint rectangle,
- the constraint rectangle remains within the _DraggableRectItem.
- """
- self._constraint = left, left + width, top, top + height
-
- def setPos(self, *args, **kwargs):
- """Overridden to ignore changes from API in itemChange."""
- self._ignoreChange = True
- super(_DraggableRectItem, self).setPos(*args, **kwargs)
- self._ignoreChange = False
-
- def moveBy(self, *args, **kwargs):
- """Overridden to ignore changes from API in itemChange."""
- self._ignoreChange = True
- super(_DraggableRectItem, self).moveBy(*args, **kwargs)
- self._ignoreChange = False
-
- def itemChange(self, change, value):
- """Callback called before applying changes to the item."""
- if (change == qt.QGraphicsItem.ItemPositionChange and
- not self._ignoreChange):
- # Makes sure that the visible area is in the data
- # or that data is in the visible area if area is too wide
- x, y = value.x(), value.y()
- xMin, xMax, yMin, yMax = self._constraint
-
- if self.rect().width() <= (xMax - xMin):
- if x < xMin:
- value.setX(xMin)
- elif x > xMax - self.rect().width():
- value.setX(xMax - self.rect().width())
- else:
- if x > xMin:
- value.setX(xMin)
- elif x < xMax - self.rect().width():
- value.setX(xMax - self.rect().width())
-
- if self.rect().height() <= (yMax - yMin):
- if y < yMin:
- value.setY(yMin)
- elif y > yMax - self.rect().height():
- value.setY(yMax - self.rect().height())
- else:
- if y > yMin:
- value.setY(yMin)
- elif y < yMax - self.rect().height():
- value.setY(yMax - self.rect().height())
-
- if self.pos() != value:
- # Notify change through signal
- views = self.scene().views()
- assert len(views) == 1
- views[0].visibleRectDragged.emit(
- value.x() + self.rect().left(),
- value.y() + self.rect().top(),
- self.rect().width(),
- self.rect().height())
-
- return value
-
- return super(_DraggableRectItem, self).itemChange(
- change, value)
-
- def hoverEnterEvent(self, event):
- """Called when the mouse enters the rectangle area"""
- self._previousCursor = self.cursor()
- self.setCursor(qt.Qt.OpenHandCursor)
-
- def hoverLeaveEvent(self, event):
- """Called when the mouse leaves the rectangle area"""
- if self._previousCursor is not None:
- self.setCursor(self._previousCursor)
- self._previousCursor = None
-
-
-class RadarView(qt.QGraphicsView):
- """Widget presenting a synthetic view of a 2D area and
- the current visible area.
-
- Coordinates are as in QGraphicsView:
- x goes from left to right and y goes from top to bottom.
- This widget preserves the aspect ratio of the areas.
-
- The 2D area and the visible area can be set with :meth:`setDataRect`
- and :meth:`setVisibleRect`.
- When the visible area has been dragged by the user, its new position
- is signaled by the *visibleRectDragged* signal.
-
- It is possible to invert the direction of the axes by using the
- :meth:`scale` method of QGraphicsView.
- """
-
- visibleRectDragged = qt.Signal(float, float, float, float)
- """Signals that the visible rectangle has been dragged.
-
- It provides: left, top, width, height in data coordinates.
- """
-
- _DATA_PEN = qt.QPen(qt.QColor('white'))
- _DATA_BRUSH = qt.QBrush(qt.QColor('light gray'))
- _ACTIVEDATA_PEN = qt.QPen(qt.QColor('black'))
- _ACTIVEDATA_BRUSH = qt.QBrush(qt.QColor('transparent'))
- _ACTIVEDATA_PEN.setWidth(2)
- _ACTIVEDATA_PEN.setCosmetic(True)
- _VISIBLE_PEN = qt.QPen(qt.QColor('blue'))
- _VISIBLE_PEN.setWidth(2)
- _VISIBLE_PEN.setCosmetic(True)
- _VISIBLE_BRUSH = qt.QBrush(qt.QColor(0, 0, 0, 0))
- _TOOLTIP = 'Radar View:\nRed contour: Visible area\nGray area: The image'
-
- _PIXMAP_SIZE = 256
-
- def __init__(self, parent=None):
- self.__plotRef = None
- self._scene = qt.QGraphicsScene()
- self._dataRect = self._scene.addRect(0, 0, 1, 1,
- self._DATA_PEN,
- self._DATA_BRUSH)
- self._imageRect = self._scene.addRect(0, 0, 1, 1,
- self._ACTIVEDATA_PEN,
- self._ACTIVEDATA_BRUSH)
- self._imageRect.setVisible(False)
- self._scatterRect = self._scene.addRect(0, 0, 1, 1,
- self._ACTIVEDATA_PEN,
- self._ACTIVEDATA_BRUSH)
- self._scatterRect.setVisible(False)
- self._curveRect = self._scene.addRect(0, 0, 1, 1,
- self._ACTIVEDATA_PEN,
- self._ACTIVEDATA_BRUSH)
- self._curveRect.setVisible(False)
-
- self._visibleRect = _DraggableRectItem(0, 0, 1, 1)
- self._visibleRect.setPen(self._VISIBLE_PEN)
- self._visibleRect.setBrush(self._VISIBLE_BRUSH)
- self._scene.addItem(self._visibleRect)
-
- super(RadarView, self).__init__(self._scene, parent)
- self.setHorizontalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff)
- self.setVerticalScrollBarPolicy(qt.Qt.ScrollBarAlwaysOff)
- self.setFocusPolicy(qt.Qt.NoFocus)
- self.setStyleSheet('border: 0px')
- self.setToolTip(self._TOOLTIP)
-
- self.__reentrant = LockReentrant()
- self.visibleRectDragged.connect(self._viewRectDragged)
-
- self.__timer = qt.QTimer(self)
- self.__timer.timeout.connect(self._updateDataContent)
-
- def sizeHint(self):
- # """Overridden to avoid sizeHint to depend on content size."""
- return self.minimumSizeHint()
-
- def wheelEvent(self, event):
- # """Overridden to disable vertical scrolling with wheel."""
- event.ignore()
-
- def resizeEvent(self, event):
- # """Overridden to fit current content to new size."""
- self.fitInView(self._scene.itemsBoundingRect(), qt.Qt.KeepAspectRatio)
- super(RadarView, self).resizeEvent(event)
-
- def setDataRect(self, left, top, width, height):
- """Set the bounds of the data rectangular area.
-
- This sets the coordinate system.
- """
- self._dataRect.setRect(left, top, width, height)
- self._visibleRect.setConstraintRect(left, top, width, height)
- self.fitInView(self._scene.itemsBoundingRect(), qt.Qt.KeepAspectRatio)
-
- def setVisibleRect(self, left, top, width, height):
- """Set the visible rectangular area.
-
- The coordinates are relative to the data rect.
- """
- self.__visibleRect = left, top, width, height
- self._visibleRect.setRect(0, 0, width, height)
- self._visibleRect.setPos(left, top)
- self.fitInView(self._scene.itemsBoundingRect(), qt.Qt.KeepAspectRatio)
-
- def __setVisibleRectFromPlot(self, plot):
- """Update radar view visible area.
-
- Takes care of y coordinate conversion.
- """
- xMin, xMax = plot.getXAxis().getLimits()
- yMin, yMax = plot.getYAxis().getLimits()
- self.setVisibleRect(xMin, yMin, xMax - xMin, yMax - yMin)
-
- def getPlotWidget(self):
- """Returns the connected plot
-
- :rtype: Union[None,PlotWidget]
- """
- if self.__plotRef is None:
- return None
- plot = self.__plotRef()
- if plot is None:
- self.__plotRef = None
- return plot
-
- def setPlotWidget(self, plot):
- """Set the PlotWidget this radar view connects to.
-
- As result `setDataRect` and `setVisibleRect` will be called
- automatically.
-
- :param Union[None,PlotWidget] plot:
- """
- previousPlot = self.getPlotWidget()
- if previousPlot is not None: # Disconnect previous plot
- plot.getXAxis().sigLimitsChanged.disconnect(self._xLimitChanged)
- plot.getYAxis().sigLimitsChanged.disconnect(self._yLimitChanged)
- plot.getYAxis().sigInvertedChanged.disconnect(self._updateYAxisInverted)
-
- # Reset plot and timer
- # FIXME: It would be good to clean up the display here
- self.__plotRef = None
- self.__timer.stop()
-
- if plot is not None: # Connect new plot
- self.__plotRef = weakref.ref(plot)
- plot.getXAxis().sigLimitsChanged.connect(self._xLimitChanged)
- plot.getYAxis().sigLimitsChanged.connect(self._yLimitChanged)
- plot.getYAxis().sigInvertedChanged.connect(self._updateYAxisInverted)
- self.__setVisibleRectFromPlot(plot)
- self._updateYAxisInverted()
- self.__timer.start(500)
-
- def _xLimitChanged(self, vmin, vmax):
- plot = self.getPlotWidget()
- self.__setVisibleRectFromPlot(plot)
-
- def _yLimitChanged(self, vmin, vmax):
- plot = self.getPlotWidget()
- self.__setVisibleRectFromPlot(plot)
-
- def _updateYAxisInverted(self, inverted=None):
- """Sync radar view axis orientation."""
- plot = self.getPlotWidget()
- if inverted is None:
- # Do not perform this when called from plot signal
- inverted = plot.getYAxis().isInverted()
- # Use scale to invert radarView
- # RadarView default Y direction is from top to bottom
- # As opposed to Plot. So invert RadarView when Plot is NOT inverted.
- self.resetTransform()
- if not inverted:
- self.scale(1., -1.)
- self.update()
-
- def _viewRectDragged(self, left, top, width, height):
- """Slot for radar view visible rectangle changes."""
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- if self.__reentrant.locked():
- return
-
- with self.__reentrant:
- plot.setLimits(left, left + width, top, top + height)
-
- def _updateDataContent(self):
- """Update the content to the current data content"""
- plot = self.getPlotWidget()
- if plot is None:
- return
- ranges = plot.getDataRange()
- xmin, xmax = ranges.x if ranges.x is not None else (0, 0)
- ymin, ymax = ranges.y if ranges.y is not None else (0, 0)
- self.setDataRect(xmin, ymin, xmax - xmin, ymax - ymin)
-
- self.__updateItem(self._imageRect, plot.getActiveImage())
- self.__updateItem(self._scatterRect, plot.getActiveScatter())
- self.__updateItem(self._curveRect, plot.getActiveCurve())
-
- def __updateItem(self, rect, item):
- """Sync rect with item bounds
-
- :param QGraphicsRectItem rect:
- :param Item item:
- """
- if item is None:
- rect.setVisible(False)
- return
- ranges = item._getBounds()
- if ranges is None:
- rect.setVisible(False)
- return
- xmin, xmax, ymin, ymax = ranges
- width = xmax - xmin
- height = ymax - ymin
- rect.setRect(xmin, ymin, width, height)
- rect.setVisible(True)
diff --git a/silx/gui/plot/tools/__init__.py b/silx/gui/plot/tools/__init__.py
deleted file mode 100644
index 09f468c..0000000
--- a/silx/gui/plot/tools/__init__.py
+++ /dev/null
@@ -1,50 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018 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 widgets working with :class:`PlotWidget`.
-
-It provides some QToolBar and QWidget:
-
-- :class:`InteractiveModeToolBar`
-- :class:`OutputToolBar`
-- :class:`ImageToolBar`
-- :class:`CurveToolBar`
-- :class:`LimitsToolBar`
-- :class:`PositionInfo`
-
-It also provides a :mod:`~silx.gui.plot.tools.roi` module to handle
-interactive region of interest on a :class:`~silx.gui.plot.PlotWidget`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "01/03/2018"
-
-
-from .toolbars import InteractiveModeToolBar # noqa
-from .toolbars import OutputToolBar # noqa
-from .toolbars import ImageToolBar, CurveToolBar, ScatterToolBar # noqa
-
-from .LimitsToolBar import LimitsToolBar # noqa
-from .PositionInfo import PositionInfo # noqa
diff --git a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py b/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
deleted file mode 100644
index 44187ef..0000000
--- a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
+++ /dev/null
@@ -1,54 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2019 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 profile tools for scatter plots.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-from silx.utils import deprecation
-from . import toolbar
-
-
-class ScatterProfileToolBar(toolbar.ProfileToolBar):
- """QToolBar providing scatter plot profiling tools
-
- :param parent: See :class:`QToolBar`.
- :param plot: :class:`~silx.gui.plot.PlotWidget` on which to operate.
- :param str title: See :class:`QToolBar`.
- """
-
- def __init__(self, parent=None, plot=None, title=None):
- super(ScatterProfileToolBar, self).__init__(parent, plot)
- if title is not None:
- deprecation.deprecated_warning("Attribute",
- name="title",
- reason="removed",
- since_version="0.13.0",
- only_once=True,
- skip_backtrace_count=1)
- self.setScheme("scatter")
diff --git a/silx/gui/plot/tools/profile/__init__.py b/silx/gui/plot/tools/profile/__init__.py
deleted file mode 100644
index d91191e..0000000
--- a/silx/gui/plot/tools/profile/__init__.py
+++ /dev/null
@@ -1,38 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018 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 tools to get profiles on plot data.
-
-It provides:
-
-- :class:`ScatterProfileToolBar`: a QToolBar to handle profile on scatter data
-
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "07/06/2018"
-
-
-from .ScatterProfileToolBar import ScatterProfileToolBar # noqa
diff --git a/silx/gui/plot/tools/profile/core.py b/silx/gui/plot/tools/profile/core.py
deleted file mode 100644
index 200f5cf..0000000
--- a/silx/gui/plot/tools/profile/core.py
+++ /dev/null
@@ -1,525 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 define core objects for profile tools.
-"""
-
-__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel", "H. Payno", "V. Valls"]
-__license__ = "MIT"
-__date__ = "17/04/2020"
-
-import collections
-import numpy
-import weakref
-
-from silx.image.bilinear import BilinearImage
-from silx.gui import qt
-
-
-CurveProfileData = collections.namedtuple(
- 'CurveProfileData', [
- "coords",
- "profile",
- "title",
- "xLabel",
- "yLabel",
- ])
-
-RgbaProfileData = collections.namedtuple(
- 'RgbaProfileData', [
- "coords",
- "profile",
- "profile_r",
- "profile_g",
- "profile_b",
- "profile_a",
- "title",
- "xLabel",
- "yLabel",
- ])
-
-ImageProfileData = collections.namedtuple(
- 'ImageProfileData', [
- 'coords',
- 'profile',
- 'title',
- 'xLabel',
- 'yLabel',
- 'colormap',
- ])
-
-
-class ProfileRoiMixIn:
- """Base mix-in for ROI which can be used to select a profile.
-
- This mix-in have to be applied to a :class:`~silx.gui.plot.items.roi.RegionOfInterest`
- in order to be usable by a :class:`~silx.gui.plot.tools.profile.manager.ProfileManager`.
- """
-
- ITEM_KIND = None
- """Define the plot item which can be used with this profile ROI"""
-
- sigProfilePropertyChanged = qt.Signal()
- """Emitted when a property of this profile have changed"""
-
- sigPlotItemChanged = qt.Signal()
- """Emitted when the plot item linked to this profile have changed"""
-
- def __init__(self, parent=None):
- self.__profileWindow = None
- self.__profileManager = None
- self.__plotItem = None
- self.setName("Profile")
- self.setEditable(True)
- self.setSelectable(True)
-
- def invalidateProfile(self):
- """Must be called by the implementation when the profile have to be
- recomputed."""
- profileManager = self.getProfileManager()
- if profileManager is not None:
- profileManager.requestUpdateProfile(self)
-
- def invalidateProperties(self):
- """Must be called when a property of the profile have changed."""
- self.sigProfilePropertyChanged.emit()
-
- def _setPlotItem(self, plotItem):
- """Specify the plot item to use with this profile
-
- :param `~silx.gui.plot.items.item.Item` plotItem: A plot item
- """
- previousPlotItem = self.getPlotItem()
- if previousPlotItem is plotItem:
- return
- self.__plotItem = weakref.ref(plotItem)
- self.sigPlotItemChanged.emit()
-
- def getPlotItem(self):
- """Returns the plot item used by this profile
-
- :rtype: `~silx.gui.plot.items.item.Item`
- """
- if self.__plotItem is None:
- return None
- plotItem = self.__plotItem()
- if plotItem is None:
- self.__plotItem = None
- return plotItem
-
- def _setProfileManager(self, profileManager):
- self.__profileManager = profileManager
-
- def getProfileManager(self):
- """
- Returns the profile manager connected to this ROI.
-
- :rtype: ~silx.gui.plot.tools.profile.manager.ProfileManager
- """
- return self.__profileManager
-
- def getProfileWindow(self):
- """
- Returns the windows associated to this ROI, else None.
-
- :rtype: ProfileWindow
- """
- return self.__profileWindow
-
- def setProfileWindow(self, profileWindow):
- """
- Associate a window to this ROI. Can be None.
-
- :param ProfileWindow profileWindow: A main window
- to display the profile.
- """
- if profileWindow is self.__profileWindow:
- return
- if self.__profileWindow is not None:
- self.__profileWindow.sigClose.disconnect(self.__profileWindowAboutToClose)
- self.__profileWindow.setRoiProfile(None)
- self.__profileWindow = profileWindow
- if self.__profileWindow is not None:
- self.__profileWindow.sigClose.connect(self.__profileWindowAboutToClose)
- self.__profileWindow.setRoiProfile(self)
-
- def __profileWindowAboutToClose(self):
- profileManager = self.getProfileManager()
- roiManager = profileManager.getRoiManager()
- try:
- roiManager.removeRoi(self)
- except ValueError:
- pass
-
- def computeProfile(self, item):
- """
- Compute the profile which will be displayed.
-
- This method is not called from the main Qt thread, but from a thread
- pool.
-
- :param ~silx.gui.plot.items.Item item: A plot item
- :rtype: Union[CurveProfileData,ImageProfileData]
- """
- raise NotImplementedError()
-
-
-def _alignedFullProfile(data, origin, scale, position, roiWidth, axis, method):
- """Get a profile along one axis on a stack of images
-
- :param numpy.ndarray data: 3D volume (stack of 2D images)
- The first dimension is the image index.
- :param origin: Origin of image in plot (ox, oy)
- :param scale: Scale of image in plot (sx, sy)
- :param float position: Position of profile line in plot coords
- on the axis orthogonal to the profile direction.
- :param int roiWidth: Width of the profile in image pixels.
- :param int axis: 0 for horizontal profile, 1 for vertical.
- :param str method: method to compute the profile. Can be 'mean' or 'sum' or
- 'none'
- :return: profile image + effective ROI area corners in plot coords
- """
- assert axis in (0, 1)
- assert len(data.shape) == 3
- assert method in ('mean', 'sum', 'none')
-
- # Convert from plot to image coords
- imgPos = int((position - origin[1 - axis]) / scale[1 - axis])
-
- if axis == 1: # Vertical profile
- # Transpose image to always do a horizontal profile
- data = numpy.transpose(data, (0, 2, 1))
-
- nimages, height, width = data.shape
-
- roiWidth = min(height, roiWidth) # Clip roi width to image size
-
- # Get [start, end[ coords of the roi in the data
- start = int(int(imgPos) + 0.5 - roiWidth / 2.)
- start = min(max(0, start), height - roiWidth)
- end = start + roiWidth
-
- if method == 'none':
- profile = None
- else:
- if start < height and end > 0:
- if method == 'mean':
- fct = numpy.mean
- elif method == 'sum':
- fct = numpy.sum
- else:
- raise ValueError('method not managed')
- profile = fct(data[:, max(0, start):min(end, height), :], axis=1).astype(numpy.float32)
- else:
- profile = numpy.zeros((nimages, width), dtype=numpy.float32)
-
- # Compute effective ROI in plot coords
- profileBounds = numpy.array(
- (0, width, width, 0),
- dtype=numpy.float32) * scale[axis] + origin[axis]
- roiBounds = numpy.array(
- (start, start, end, end),
- dtype=numpy.float32) * scale[1 - axis] + origin[1 - axis]
-
- if axis == 0: # Horizontal profile
- area = profileBounds, roiBounds
- else: # vertical profile
- area = roiBounds, profileBounds
-
- return profile, area
-
-
-def _alignedPartialProfile(data, rowRange, colRange, axis, method):
- """Mean of a rectangular region (ROI) of a stack of images
- along a given axis.
-
- Returned values and all parameters are in image coordinates.
-
- :param numpy.ndarray data: 3D volume (stack of 2D images)
- The first dimension is the image index.
- :param rowRange: [min, max[ of ROI rows (upper bound excluded).
- :type rowRange: 2-tuple of int (min, max) with min < max
- :param colRange: [min, max[ of ROI columns (upper bound excluded).
- :type colRange: 2-tuple of int (min, max) with min < max
- :param int axis: The axis along which to take the profile of the ROI.
- 0: Sum rows along columns.
- 1: Sum columns along rows.
- :param str method: method to compute the profile. Can be 'mean' or 'sum'
- :return: Profile image along the ROI as the mean of the intersection
- of the ROI and the image.
- """
- assert axis in (0, 1)
- assert len(data.shape) == 3
- assert rowRange[0] < rowRange[1]
- assert colRange[0] < colRange[1]
- assert method in ('mean', 'sum')
-
- nimages, height, width = data.shape
-
- # Range aligned with the integration direction
- profileRange = colRange if axis == 0 else rowRange
-
- profileLength = abs(profileRange[1] - profileRange[0])
-
- # Subset of the image to use as intersection of ROI and image
- rowStart = min(max(0, rowRange[0]), height)
- rowEnd = min(max(0, rowRange[1]), height)
- colStart = min(max(0, colRange[0]), width)
- colEnd = min(max(0, colRange[1]), width)
-
- if method == 'mean':
- _fct = numpy.mean
- elif method == 'sum':
- _fct = numpy.sum
- else:
- raise ValueError('method not managed')
-
- imgProfile = _fct(data[:, rowStart:rowEnd, colStart:colEnd], axis=axis + 1,
- dtype=numpy.float32)
-
- # Profile including out of bound area
- profile = numpy.zeros((nimages, profileLength), dtype=numpy.float32)
-
- # Place imgProfile in full profile
- offset = - min(0, profileRange[0])
- profile[:, offset:offset + imgProfile.shape[1]] = imgProfile
-
- return profile
-
-
-def createProfile(roiInfo, currentData, origin, scale, lineWidth, method):
- """Create the profile line for the the given image.
-
- :param roiInfo: information about the ROI: start point, end point and
- type ("X", "Y", "D")
- :param numpy.ndarray currentData: the 2D image or the 3D stack of images
- on which we compute the profile.
- :param origin: (ox, oy) the offset from origin
- :type origin: 2-tuple of float
- :param scale: (sx, sy) the scale to use
- :type scale: 2-tuple of float
- :param int lineWidth: width of the profile line
- :param str method: method to compute the profile. Can be 'mean' or 'sum'
- or 'none': to compute everything except the profile
- :return: `coords, profile, area, profileName, xLabel`, where:
- - coords is the X coordinate to use to display the profile
- - profile is a 2D array of the profiles of the stack of images.
- For a single image, the profile is a curve, so this parameter
- has a shape *(1, len(curve))*
- - area is a tuple of two 1D arrays with 4 values each. They represent
- the effective ROI area corners in plot coords.
- - profileName is a string describing the ROI, meant to be used as
- title of the profile plot
- - xLabel the label for X in the profile window
-
- :rtype: tuple(ndarray,ndarray,(ndarray,ndarray),str)
- """
- if currentData is None or roiInfo is None or lineWidth is None:
- raise ValueError("createProfile called with invalide arguments")
-
- # force 3D data (stack of images)
- if len(currentData.shape) == 2:
- currentData3D = currentData.reshape((1,) + currentData.shape)
- elif len(currentData.shape) == 3:
- currentData3D = currentData
-
- roiWidth = max(1, lineWidth)
- roiStart, roiEnd, lineProjectionMode = roiInfo
-
- if lineProjectionMode == 'X': # Horizontal profile on the whole image
- profile, area = _alignedFullProfile(currentData3D,
- origin, scale,
- roiStart[1], roiWidth,
- axis=0,
- method=method)
-
- if method == 'none':
- coords = None
- else:
- coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
- coords = coords * scale[0] + origin[0]
-
- yMin, yMax = min(area[1]), max(area[1]) - 1
- if roiWidth <= 1:
- profileName = '{ylabel} = %g' % yMin
- else:
- profileName = '{ylabel} = [%g, %g]' % (yMin, yMax)
- xLabel = '{xlabel}'
-
- elif lineProjectionMode == 'Y': # Vertical profile on the whole image
- profile, area = _alignedFullProfile(currentData3D,
- origin, scale,
- roiStart[0], roiWidth,
- axis=1,
- method=method)
-
- if method == 'none':
- coords = None
- else:
- coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
- coords = coords * scale[1] + origin[1]
-
- xMin, xMax = min(area[0]), max(area[0]) - 1
- if roiWidth <= 1:
- profileName = '{xlabel} = %g' % xMin
- else:
- profileName = '{xlabel} = [%g, %g]' % (xMin, xMax)
- xLabel = '{ylabel}'
-
- else: # Free line profile
-
- # Convert start and end points in image coords as (row, col)
- startPt = ((roiStart[1] - origin[1]) / scale[1],
- (roiStart[0] - origin[0]) / scale[0])
- endPt = ((roiEnd[1] - origin[1]) / scale[1],
- (roiEnd[0] - origin[0]) / scale[0])
-
- if (int(startPt[0]) == int(endPt[0]) or
- int(startPt[1]) == int(endPt[1])):
- # Profile is aligned with one of the axes
-
- # Convert to int
- startPt = int(startPt[0]), int(startPt[1])
- endPt = int(endPt[0]), int(endPt[1])
-
- # Ensure startPt <= endPt
- if startPt[0] > endPt[0] or startPt[1] > endPt[1]:
- startPt, endPt = endPt, startPt
-
- if startPt[0] == endPt[0]: # Row aligned
- rowRange = (int(startPt[0] + 0.5 - 0.5 * roiWidth),
- int(startPt[0] + 0.5 + 0.5 * roiWidth))
- colRange = startPt[1], endPt[1] + 1
- if method == 'none':
- profile = None
- else:
- profile = _alignedPartialProfile(currentData3D,
- rowRange, colRange,
- axis=0,
- method=method)
-
- else: # Column aligned
- rowRange = startPt[0], endPt[0] + 1
- colRange = (int(startPt[1] + 0.5 - 0.5 * roiWidth),
- int(startPt[1] + 0.5 + 0.5 * roiWidth))
- if method == 'none':
- profile = None
- else:
- profile = _alignedPartialProfile(currentData3D,
- rowRange, colRange,
- axis=1,
- method=method)
- # Convert ranges to plot coords to draw ROI area
- area = (
- numpy.array(
- (colRange[0], colRange[1], colRange[1], colRange[0]),
- dtype=numpy.float32) * scale[0] + origin[0],
- numpy.array(
- (rowRange[0], rowRange[0], rowRange[1], rowRange[1]),
- dtype=numpy.float32) * scale[1] + origin[1])
-
- else: # General case: use bilinear interpolation
-
- # Ensure startPt <= endPt
- if (startPt[1] > endPt[1] or (
- startPt[1] == endPt[1] and startPt[0] > endPt[0])):
- startPt, endPt = endPt, startPt
-
- if method == 'none':
- profile = None
- else:
- profile = []
- for slice_idx in range(currentData3D.shape[0]):
- bilinear = BilinearImage(currentData3D[slice_idx, :, :])
-
- profile.append(bilinear.profile_line(
- (startPt[0] - 0.5, startPt[1] - 0.5),
- (endPt[0] - 0.5, endPt[1] - 0.5),
- roiWidth,
- method=method))
- profile = numpy.array(profile)
-
- # Extend ROI with half a pixel on each end, and
- # Convert back to plot coords (x, y)
- length = numpy.sqrt((endPt[0] - startPt[0]) ** 2 +
- (endPt[1] - startPt[1]) ** 2)
- dRow = (endPt[0] - startPt[0]) / length
- dCol = (endPt[1] - startPt[1]) / length
-
- # Extend ROI with half a pixel on each end
- roiStartPt = startPt[0] - 0.5 * dRow, startPt[1] - 0.5 * dCol
- roiEndPt = endPt[0] + 0.5 * dRow, endPt[1] + 0.5 * dCol
-
- # Rotate deltas by 90 degrees to apply line width
- dRow, dCol = dCol, -dRow
-
- area = (
- numpy.array((roiStartPt[1] - 0.5 * roiWidth * dCol,
- roiStartPt[1] + 0.5 * roiWidth * dCol,
- roiEndPt[1] + 0.5 * roiWidth * dCol,
- roiEndPt[1] - 0.5 * roiWidth * dCol),
- dtype=numpy.float32) * scale[0] + origin[0],
- numpy.array((roiStartPt[0] - 0.5 * roiWidth * dRow,
- roiStartPt[0] + 0.5 * roiWidth * dRow,
- roiEndPt[0] + 0.5 * roiWidth * dRow,
- roiEndPt[0] - 0.5 * roiWidth * dRow),
- dtype=numpy.float32) * scale[1] + origin[1])
-
- # Convert start and end points back to plot coords
- y0 = startPt[0] * scale[1] + origin[1]
- x0 = startPt[1] * scale[0] + origin[0]
- y1 = endPt[0] * scale[1] + origin[1]
- x1 = endPt[1] * scale[0] + origin[0]
-
- if startPt[1] == endPt[1]:
- profileName = '{xlabel} = %g; {ylabel} = [%g, %g]' % (x0, y0, y1)
- if method == 'none':
- coords = None
- else:
- coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
- coords = coords * scale[1] + y0
- xLabel = '{ylabel}'
-
- elif startPt[0] == endPt[0]:
- profileName = '{ylabel} = %g; {xlabel} = [%g, %g]' % (y0, x0, x1)
- if method == 'none':
- coords = None
- else:
- coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
- coords = coords * scale[0] + x0
- xLabel = '{xlabel}'
-
- else:
- m = (y1 - y0) / (x1 - x0)
- b = y0 - m * x0
- profileName = '{ylabel} = %g * {xlabel} %+g' % (m, b)
- if method == 'none':
- coords = None
- else:
- coords = numpy.linspace(x0, x1, len(profile[0]),
- endpoint=True,
- dtype=numpy.float32)
- xLabel = '{xlabel}'
-
- return coords, profile, area, profileName, xLabel
diff --git a/silx/gui/plot/tools/profile/editors.py b/silx/gui/plot/tools/profile/editors.py
deleted file mode 100644
index 80e0452..0000000
--- a/silx/gui/plot/tools/profile/editors.py
+++ /dev/null
@@ -1,307 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 editors which are used to custom profile ROI properties.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-import logging
-
-from silx.gui import qt
-
-from silx.gui.utils import blockSignals
-from silx.gui.plot.PlotToolButtons import ProfileOptionToolButton
-from silx.gui.plot.PlotToolButtons import ProfileToolButton
-from . import rois
-from . import core
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _NoProfileRoiEditor(qt.QWidget):
-
- sigDataCommited = qt.Signal()
-
- def setEditorData(self, roi):
- pass
-
- def setRoiData(self, roi):
- pass
-
-
-class _DefaultImageProfileRoiEditor(qt.QWidget):
-
- sigDataCommited = qt.Signal()
-
- def __init__(self, parent=None):
- qt.QWidget.__init__(self, parent=parent)
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- self._initLayout(layout)
-
- def _initLayout(self, layout):
- self._lineWidth = qt.QSpinBox(self)
- self._lineWidth.setRange(1, 1000)
- self._lineWidth.setValue(1)
- self._lineWidth.valueChanged[int].connect(self._widgetChanged)
-
- self._methodsButton = ProfileOptionToolButton(parent=self, plot=None)
- self._methodsButton.sigMethodChanged.connect(self._widgetChanged)
-
- label = qt.QLabel('W:')
- label.setToolTip("Line width in pixels")
- layout.addWidget(label)
- layout.addWidget(self._lineWidth)
- layout.addWidget(self._methodsButton)
-
- def _widgetChanged(self, value=None):
- self.commitData()
-
- def commitData(self):
- self.sigDataCommited.emit()
-
- def setEditorData(self, roi):
- with blockSignals(self._lineWidth):
- self._lineWidth.setValue(roi.getProfileLineWidth())
- with blockSignals(self._methodsButton):
- method = roi.getProfileMethod()
- self._methodsButton.setMethod(method)
-
- def setRoiData(self, roi):
- lineWidth = self._lineWidth.value()
- roi.setProfileLineWidth(lineWidth)
- method = self._methodsButton.getMethod()
- roi.setProfileMethod(method)
-
-
-class _DefaultImageStackProfileRoiEditor(_DefaultImageProfileRoiEditor):
-
- def _initLayout(self, layout):
- super(_DefaultImageStackProfileRoiEditor, self)._initLayout(layout)
- self._profileDim = ProfileToolButton(parent=self, plot=None)
- self._profileDim.sigDimensionChanged.connect(self._widgetChanged)
- layout.addWidget(self._profileDim)
-
- def setEditorData(self, roi):
- super(_DefaultImageStackProfileRoiEditor, self).setEditorData(roi)
- with blockSignals(self._profileDim):
- kind = roi.getProfileType()
- dim = {"1D": 1, "2D": 2}[kind]
- self._profileDim.setDimension(dim)
-
- def setRoiData(self, roi):
- super(_DefaultImageStackProfileRoiEditor, self).setRoiData(roi)
- dim = self._profileDim.getDimension()
- kind = {1: "1D", 2: "2D"}[dim]
- roi.setProfileType(kind)
-
-
-class _DefaultScatterProfileRoiEditor(qt.QWidget):
-
- sigDataCommited = qt.Signal()
-
- def __init__(self, parent=None):
- qt.QWidget.__init__(self, parent=parent)
-
- self._nPoints = qt.QSpinBox(self)
- self._nPoints.setRange(1, 9999)
- self._nPoints.setValue(1024)
- self._nPoints.valueChanged[int].connect(self.__widgetChanged)
-
- layout = qt.QHBoxLayout(self)
- layout.setContentsMargins(0, 0, 0, 0)
- label = qt.QLabel('Samples:')
- label.setToolTip("Number of sample points of the profile")
- layout.addWidget(label)
- layout.addWidget(self._nPoints)
-
- def __widgetChanged(self, value=None):
- self.commitData()
-
- def commitData(self):
- self.sigDataCommited.emit()
-
- def setEditorData(self, roi):
- with blockSignals(self._nPoints):
- self._nPoints.setValue(roi.getNPoints())
-
- def setRoiData(self, roi):
- nPoints = self._nPoints.value()
- roi.setNPoints(nPoints)
-
-
-class ProfileRoiEditorAction(qt.QWidgetAction):
- """
- Action displaying GUI to edit the selected ROI.
-
- :param qt.QWidget parent: Parent widget
- """
- def __init__(self, parent=None):
- super(ProfileRoiEditorAction, self).__init__(parent)
- self.__roiManager = None
- self.__roi = None
- self.__inhibiteReentance = None
-
- def createWidget(self, parent):
- """Inherit the method to create a new editor"""
- widget = qt.QWidget(parent)
- layout = qt.QHBoxLayout(widget)
- if isinstance(parent, qt.QMenu):
- margins = layout.contentsMargins()
- layout.setContentsMargins(margins.left(), 0, margins.right(), 0)
- else:
- layout.setContentsMargins(0, 0, 0, 0)
-
- editorClass = self.getEditorClass(self.__roi)
- editor = editorClass(parent)
- editor.setEditorData(self.__roi)
- self.__setEditor(widget, editor)
- return widget
-
- def deleteWidget(self, widget):
- """Inherit the method to delete an editor"""
- self.__setEditor(widget, None)
- return qt.QWidgetAction.deleteWidget(self, widget)
-
- def _getEditor(self, widget):
- """Returns the editor contained in the widget holder"""
- layout = widget.layout()
- if layout.count() == 0:
- return None
- return layout.itemAt(0).widget()
-
- def setRoiManager(self, roiManager):
- """
- Connect this action to a ROI manager.
-
- :param RegionOfInterestManager roiManager: A ROI manager
- """
- if self.__roiManager is roiManager:
- return
- if self.__roiManager is not None:
- self.__roiManager.sigCurrentRoiChanged.disconnect(self.__currentRoiChanged)
- self.__roiManager = roiManager
- if self.__roiManager is not None:
- self.__roiManager.sigCurrentRoiChanged.connect(self.__currentRoiChanged)
- self.__currentRoiChanged(roiManager.getCurrentRoi())
-
- def __currentRoiChanged(self, roi):
- """Handle changes of the selected ROI"""
- if roi is not None and not isinstance(roi, core.ProfileRoiMixIn):
- return
- self.setProfileRoi(roi)
-
- def setProfileRoi(self, roi):
- """Set a profile ROI to edit.
-
- :param ProfileRoiMixIn roi: A profile ROI
- """
- if self.__roi is roi:
- return
- if self.__roi is not None:
- self.__roi.sigProfilePropertyChanged.disconnect(self.__roiPropertyChanged)
- self.__roi = roi
- if self.__roi is not None:
- self.__roi.sigProfilePropertyChanged.connect(self.__roiPropertyChanged)
- self._updateWidgets()
-
- def getRoiProfile(self):
- """Returns the edited profile ROI.
-
- :rtype: ProfileRoiMixIn
- """
- return self.__roi
-
- def __roiPropertyChanged(self):
- """Handle changes on the property defining the ROI.
- """
- self._updateWidgetValues()
-
- def __setEditor(self, widget, editor):
- """Set the editor to display.
-
- :param qt.QWidget editor: The editor to display
- """
- previousEditor = self._getEditor(widget)
- if previousEditor is editor:
- return
- layout = widget.layout()
- if previousEditor is not None:
- previousEditor.sigDataCommited.disconnect(self._editorDataCommited)
- layout.removeWidget(previousEditor)
- previousEditor.deleteLater()
- if editor is not None:
- editor.sigDataCommited.connect(self._editorDataCommited)
- layout.addWidget(editor)
-
- def getEditorClass(self, roi):
- """Returns the editor class to use according to the ROI."""
- if roi is None:
- editorClass = _NoProfileRoiEditor
- elif isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn,
- rois.ProfileImageStackCrossROI)):
- # Must be done before the default image ROI
- # Cause ImageStack ROIs inherit from Image ROIs
- editorClass = _DefaultImageStackProfileRoiEditor
- elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn,
- rois.ProfileImageCrossROI)):
- editorClass = _DefaultImageProfileRoiEditor
- elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn,
- rois.ProfileScatterCrossROI)):
- editorClass = _DefaultScatterProfileRoiEditor
- else:
- # Unsupported
- editorClass = _NoProfileRoiEditor
- return editorClass
-
- def _updateWidgets(self):
- """Update the kind of editor to display, according to the selected
- profile ROI."""
- parent = self.parent()
- editorClass = self.getEditorClass(self.__roi)
- for widget in self.createdWidgets():
- editor = editorClass(parent)
- editor.setEditorData(self.__roi)
- self.__setEditor(widget, editor)
-
- def _updateWidgetValues(self):
- """Update the content of the displayed editor, according to the
- selected profile ROI."""
- for widget in self.createdWidgets():
- editor = self._getEditor(widget)
- if self.__inhibiteReentance is editor:
- continue
- editor.setEditorData(self.__roi)
-
- def _editorDataCommited(self):
- """Handle changes from the editor."""
- editor = self.sender()
- if self.__roi is not None:
- self.__inhibiteReentance = editor
- editor.setRoiData(self.__roi)
- self.__inhibiteReentance = None
diff --git a/silx/gui/plot/tools/profile/manager.py b/silx/gui/plot/tools/profile/manager.py
deleted file mode 100644
index 68db9a6..0000000
--- a/silx/gui/plot/tools/profile/manager.py
+++ /dev/null
@@ -1,1076 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2021 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 manager to compute and display profiles.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-import logging
-import weakref
-
-from silx.gui import qt
-from silx.gui import colors
-from silx.gui import utils
-
-from silx.utils.weakref import WeakMethodProxy
-from silx.gui import icons
-from silx.gui.plot import PlotWidget
-from silx.gui.plot.tools.roi import RegionOfInterestManager
-from silx.gui.plot.tools.roi import CreateRoiModeAction
-from silx.gui.plot import items
-from silx.gui.qt import silxGlobalThreadPool
-from silx.gui.qt import inspect
-from . import rois
-from . import core
-from . import editors
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _RunnableComputeProfile(qt.QRunnable):
- """Runner to process profiles
-
- :param qt.QThreadPool threadPool: The thread which will be used to
- execute this runner. It is used to update the used signals
- :param ~silx.gui.plot.items.Item item: Item in which the profile is
- computed
- :param ~silx.gui.plot.tools.profile.core.ProfileRoiMixIn roi: ROI
- defining the profile shape and other characteristics
- """
-
- class _Signals(qt.QObject):
- """Signal holder"""
- resultReady = qt.Signal(object, object)
- runnerFinished = qt.Signal(object)
-
- def __init__(self, threadPool, item, roi):
- """Constructor
- """
- super(_RunnableComputeProfile, self).__init__()
- self._signals = self._Signals()
- self._signals.moveToThread(threadPool.thread())
- self._item = item
- self._roi = roi
- self._cancelled = False
-
- def _lazyCancel(self):
- """Cancel the runner if it is not yet started.
-
- The threadpool will still execute the runner, but this will process
- nothing.
-
- This is only used with Qt<5.9 where QThreadPool.tryTake is not available.
- """
- self._cancelled = True
-
- def autoDelete(self):
- return False
-
- def getRoi(self):
- """Returns the ROI in which the runner will compute a profile.
-
- :rtype: ~silx.gui.plot.tools.profile.core.ProfileRoiMixIn
- """
- return self._roi
-
- @property
- def resultReady(self):
- """Signal emitted when the result of the computation is available.
-
- This signal provides 2 values: The ROI, and the computation result.
- """
- return self._signals.resultReady
-
- @property
- def runnerFinished(self):
- """Signal emitted when runner have finished.
-
- This signal provides a single value: the runner itself.
- """
- return self._signals.runnerFinished
-
- def run(self):
- """Process the profile computation.
- """
- if not self._cancelled:
- try:
- profileData = self._roi.computeProfile(self._item)
- except Exception:
- _logger.error("Error while computing profile", exc_info=True)
- else:
- self.resultReady.emit(self._roi, profileData)
- self.runnerFinished.emit(self)
-
-
-class ProfileWindow(qt.QMainWindow):
- """
- Display a computed profile.
-
- The content can be described using :meth:`setRoiProfile` if the source of
- the profile is a profile ROI, and :meth:`setProfile` for the data content.
- """
-
- sigClose = qt.Signal()
- """Emitted by :meth:`closeEvent` (e.g. when the window is closed
- through the window manager's close icon)."""
-
- def __init__(self, parent=None, backend=None):
- qt.QMainWindow.__init__(self, parent=parent, flags=qt.Qt.Dialog)
-
- self.setWindowTitle('Profile window')
- self._plot1D = None
- self._plot2D = None
- self._backend = backend
- self._data = None
-
- widget = qt.QWidget()
- self._layout = qt.QStackedLayout(widget)
- self._layout.setContentsMargins(0, 0, 0, 0)
- self.setCentralWidget(widget)
-
- def prepareWidget(self, roi):
- """Called before the show to prepare the window to use with
- a specific ROI."""
- if isinstance(roi, rois._DefaultImageStackProfileRoiMixIn):
- profileType = roi.getProfileType()
- else:
- profileType = "1D"
- if profileType == "1D":
- self.getPlot1D()
- elif profileType == "2D":
- self.getPlot2D()
-
- def createPlot1D(self, parent, backend):
- """Inherit this function to create your own plot to render 1D
- profiles. The default value is a `Plot1D`.
-
- :param parent: The parent of this widget or None.
- :param backend: The backend to use for the plot.
- See :class:`PlotWidget` for the list of supported backend.
- :rtype: PlotWidget
- """
- # import here to avoid circular import
- from ...PlotWindow import Plot1D
- plot = Plot1D(parent=parent, backend=backend)
- plot.setDataMargins(yMinMargin=0.1, yMaxMargin=0.1)
- plot.setGraphYLabel('Profile')
- plot.setGraphXLabel('')
- return plot
-
- def createPlot2D(self, parent, backend):
- """Inherit this function to create your own plot to render 2D
- profiles. The default value is a `Plot2D`.
-
- :param parent: The parent of this widget or None.
- :param backend: The backend to use for the plot.
- See :class:`PlotWidget` for the list of supported backend.
- :rtype: PlotWidget
- """
- # import here to avoid circular import
- from ...PlotWindow import Plot2D
- return Plot2D(parent=parent, backend=backend)
-
- def getPlot1D(self, init=True):
- """Return the current plot used to display curves and create it if it
- does not yet exists and `init` is True. Else returns None."""
- if not init:
- return self._plot1D
- if self._plot1D is None:
- self._plot1D = self.createPlot1D(self, self._backend)
- self._layout.addWidget(self._plot1D)
- return self._plot1D
-
- def _showPlot1D(self):
- plot = self.getPlot1D()
- self._layout.setCurrentWidget(plot)
-
- def getPlot2D(self, init=True):
- """Return the current plot used to display image and create it if it
- does not yet exists and `init` is True. Else returns None."""
- if not init:
- return self._plot2D
- if self._plot2D is None:
- self._plot2D = self.createPlot2D(parent=self, backend=self._backend)
- self._layout.addWidget(self._plot2D)
- return self._plot2D
-
- def _showPlot2D(self):
- plot = self.getPlot2D()
- self._layout.setCurrentWidget(plot)
-
- def getCurrentPlotWidget(self):
- return self._layout.currentWidget()
-
- def closeEvent(self, qCloseEvent):
- self.sigClose.emit()
- qCloseEvent.accept()
-
- def setRoiProfile(self, roi):
- """Set the profile ROI which it the source of the following data
- to display.
-
- :param ProfileRoiMixIn roi: The profile ROI data source
- """
- if roi is None:
- return
- self.__color = colors.rgba(roi.getColor())
-
- def _setImageProfile(self, data):
- """
- Setup the window to display a new profile data which is represented
- by an image.
-
- :param core.ImageProfileData data: Computed data profile
- """
- plot = self.getPlot2D()
-
- plot.clear()
- plot.setGraphTitle(data.title)
- plot.getXAxis().setLabel(data.xLabel)
-
-
- coords = data.coords
- colormap = data.colormap
- profileScale = (coords[-1] - coords[0]) / data.profile.shape[1], 1
- plot.addImage(data.profile,
- legend="profile",
- colormap=colormap,
- origin=(coords[0], 0),
- scale=profileScale)
- plot.getYAxis().setLabel("Frame index (depth)")
-
- self._showPlot2D()
-
- def _setCurveProfile(self, data):
- """
- Setup the window to display a new profile data which is represented
- by a curve.
-
- :param core.CurveProfileData data: Computed data profile
- """
- plot = self.getPlot1D()
-
- plot.clear()
- plot.setGraphTitle(data.title)
- plot.getXAxis().setLabel(data.xLabel)
- plot.getYAxis().setLabel(data.yLabel)
-
- plot.addCurve(data.coords,
- data.profile,
- legend="level",
- color=self.__color)
-
- self._showPlot1D()
-
- def _setRgbaProfile(self, data):
- """
- Setup the window to display a new profile data which is represented
- by a curve.
-
- :param core.RgbaProfileData data: Computed data profile
- """
- plot = self.getPlot1D()
-
- plot.clear()
- plot.setGraphTitle(data.title)
- plot.getXAxis().setLabel(data.xLabel)
- plot.getYAxis().setLabel(data.yLabel)
-
- self._showPlot1D()
-
- plot.addCurve(data.coords, data.profile,
- legend="level", color="black")
- plot.addCurve(data.coords, data.profile_r,
- legend="red", color="red")
- plot.addCurve(data.coords, data.profile_g,
- legend="green", color="green")
- plot.addCurve(data.coords, data.profile_b,
- legend="blue", color="blue")
- if data.profile_a is not None:
- plot.addCurve(data.coords, data.profile_a, legend="alpha", color="gray")
-
- def clear(self):
- """Clear the window profile"""
- plot = self.getPlot1D(init=False)
- if plot is not None:
- plot.clear()
- plot = self.getPlot2D(init=False)
- if plot is not None:
- plot.clear()
-
- def getProfile(self):
- """Returns the profile data which is displayed"""
- return self.__data
-
- def setProfile(self, data):
- """
- Setup the window to display a new profile data.
-
- This method dispatch the result to a specific method according to the
- data type.
-
- :param data: Computed data profile
- """
- self.__data = data
- if data is None:
- self.clear()
- elif isinstance(data, core.ImageProfileData):
- self._setImageProfile(data)
- elif isinstance(data, core.RgbaProfileData):
- self._setRgbaProfile(data)
- elif isinstance(data, core.CurveProfileData):
- self._setCurveProfile(data)
- else:
- raise TypeError("Unsupported type %s" % type(data))
-
-
-class _ClearAction(qt.QAction):
- """Action to clear the profile manager
-
- The action is only enabled if something can be cleaned up.
- """
-
- def __init__(self, parent, profileManager):
- super(_ClearAction, self).__init__(parent)
- self.__profileManager = weakref.ref(profileManager)
- icon = icons.getQIcon('profile-clear')
- self.setIcon(icon)
- self.setText('Clear profile')
- self.setToolTip('Clear the profiles')
- self.setCheckable(False)
- self.setEnabled(False)
- self.triggered.connect(profileManager.clearProfile)
- plot = profileManager.getPlotWidget()
- roiManager = profileManager.getRoiManager()
- plot.sigInteractiveModeChanged.connect(self.__modeUpdated)
- roiManager.sigRoiChanged.connect(self.__roiListUpdated)
-
- def getProfileManager(self):
- return self.__profileManager()
-
- def __roiListUpdated(self):
- self.__update()
-
- def __modeUpdated(self, source):
- self.__update()
-
- def __update(self):
- profileManager = self.getProfileManager()
- if profileManager is None:
- return
- roiManager = profileManager.getRoiManager()
- if roiManager is None:
- return
- enabled = roiManager.isStarted() or len(roiManager.getRois()) > 0
- self.setEnabled(enabled)
-
-
-class _StoreLastParamBehavior(qt.QObject):
- """This object allow to store and restore the properties of the ROI
- profiles"""
-
- def __init__(self, parent):
- assert isinstance(parent, ProfileManager)
- super(_StoreLastParamBehavior, self).__init__(parent=parent)
- self.__properties = {}
- self.__profileRoi = None
- self.__filter = utils.LockReentrant()
-
- def _roi(self):
- """Return the spied ROI"""
- if self.__profileRoi is None:
- return None
- roi = self.__profileRoi()
- if roi is None:
- self.__profileRoi = None
- return roi
-
- def setProfileRoi(self, roi):
- """Set a profile ROI to spy.
-
- :param ProfileRoiMixIn roi: A profile ROI
- """
- previousRoi = self._roi()
- if previousRoi is roi:
- return
- if previousRoi is not None:
- previousRoi.sigProfilePropertyChanged.disconnect(self._profilePropertyChanged)
- self.__profileRoi = None if roi is None else weakref.ref(roi)
- if roi is not None:
- roi.sigProfilePropertyChanged.connect(self._profilePropertyChanged)
-
- def _profilePropertyChanged(self):
- """Handle changes on the properties defining the profile ROI.
- """
- if self.__filter.locked():
- return
- roi = self.sender()
- self.storeProperties(roi)
-
- def storeProperties(self, roi):
- if isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn,
- rois.ProfileImageStackCrossROI)):
- self.__properties["method"] = roi.getProfileMethod()
- self.__properties["line-width"] = roi.getProfileLineWidth()
- self.__properties["type"] = roi.getProfileType()
- elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn,
- rois.ProfileImageCrossROI)):
- self.__properties["method"] = roi.getProfileMethod()
- self.__properties["line-width"] = roi.getProfileLineWidth()
- elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn,
- rois.ProfileScatterCrossROI)):
- self.__properties["npoints"] = roi.getNPoints()
-
- def restoreProperties(self, roi):
- with self.__filter:
- if isinstance(roi, (rois._DefaultImageStackProfileRoiMixIn,
- rois.ProfileImageStackCrossROI)):
- value = self.__properties.get("method", None)
- if value is not None:
- roi.setProfileMethod(value)
- value = self.__properties.get("line-width", None)
- if value is not None:
- roi.setProfileLineWidth(value)
- value = self.__properties.get("type", None)
- if value is not None:
- roi.setProfileType(value)
- elif isinstance(roi, (rois._DefaultImageProfileRoiMixIn,
- rois.ProfileImageCrossROI)):
- value = self.__properties.get("method", None)
- if value is not None:
- roi.setProfileMethod(value)
- value = self.__properties.get("line-width", None)
- if value is not None:
- roi.setProfileLineWidth(value)
- elif isinstance(roi, (rois._DefaultScatterProfileRoiMixIn,
- rois.ProfileScatterCrossROI)):
- value = self.__properties.get("npoints", None)
- if value is not None:
- roi.setNPoints(value)
-
-
-class ProfileManager(qt.QObject):
- """Base class for profile management tools
-
- :param plot: :class:`~silx.gui.plot.PlotWidget` on which to operate.
- :param plot: :class:`~silx.gui.plot.tools.roi.RegionOfInterestManager`
- on which to operate.
- """
- def __init__(self, parent=None, plot=None, roiManager=None):
- super(ProfileManager, self).__init__(parent)
-
- assert isinstance(plot, PlotWidget)
- self._plotRef = weakref.ref(
- plot, WeakMethodProxy(self.__plotDestroyed))
-
- # Set-up interaction manager
- if roiManager is None:
- roiManager = RegionOfInterestManager(plot)
-
- self._roiManagerRef = weakref.ref(roiManager)
- self._rois = []
- self._pendingRunners = []
- """List of ROIs which have to be updated"""
-
- self.__reentrantResults = {}
- """Store reentrant result to avoid to skip some of them
- cause the implementation uses a QEventLoop."""
-
- self._profileWindowClass = ProfileWindow
- """Class used to display the profile results"""
-
- self._computedProfiles = 0
- """Statistics for tests"""
-
- self.__itemTypes = []
- """Kind of items to use"""
-
- self.__tracking = False
- """Is the plot active items are tracked"""
-
- self.__useColorFromCursor = True
- """If true, force the ROI color with the colormap marker color"""
-
- self._item = None
- """The selected item"""
-
- self.__singleProfileAtATime = True
- """When it's true, only a single profile is displayed at a time."""
-
- self._previousWindowGeometry = []
-
- self._storeProperties = _StoreLastParamBehavior(self)
- """If defined the profile properties of the last ROI are reused to the
- new created ones"""
-
- # Listen to plot limits changed
- plot.getXAxis().sigLimitsChanged.connect(self.requestUpdateAllProfile)
- plot.getYAxis().sigLimitsChanged.connect(self.requestUpdateAllProfile)
-
- roiManager.sigInteractiveModeFinished.connect(self.__interactionFinished)
- roiManager.sigInteractiveRoiCreated.connect(self.__roiCreated)
- roiManager.sigRoiAdded.connect(self.__roiAdded)
- roiManager.sigRoiAboutToBeRemoved.connect(self.__roiRemoved)
-
- def setSingleProfile(self, enable):
- """
- Enable or disable the single profile mode.
-
- In single mode, the manager enforce a single ROI at the same
- time. A new one will remove the previous one.
-
- If this mode is not enabled, many ROIs can be created, and many
- profile windows will be displayed.
- """
- self.__singleProfileAtATime = enable
-
- def isSingleProfile(self):
- """
- Returns true if the manager is in a single profile mode.
-
- :rtype: bool
- """
- return self.__singleProfileAtATime
-
- def __interactionFinished(self):
- """Handle end of interactive mode"""
- pass
-
- def __roiAdded(self, roi):
- """Handle new ROI"""
- # Filter out non profile ROIs
- if not isinstance(roi, core.ProfileRoiMixIn):
- return
- self.__addProfile(roi)
-
- def __roiRemoved(self, roi):
- """Handle removed ROI"""
- # Filter out non profile ROIs
- if not isinstance(roi, core.ProfileRoiMixIn):
- return
- self.__removeProfile(roi)
-
- def createProfileAction(self, profileRoiClass, parent=None):
- """Create an action from a class of ProfileRoi
-
- :param core.ProfileRoiMixIn profileRoiClass: A class of a profile ROI
- :param qt.QObject parent: The parent of the created action.
- :rtype: qt.QAction
- """
- if not issubclass(profileRoiClass, core.ProfileRoiMixIn):
- raise TypeError("Type %s not expected" % type(profileRoiClass))
- roiManager = self.getRoiManager()
- action = CreateRoiModeAction(parent, roiManager, profileRoiClass)
- if hasattr(profileRoiClass, "ICON"):
- action.setIcon(icons.getQIcon(profileRoiClass.ICON))
- if hasattr(profileRoiClass, "NAME"):
- def articulify(word):
- """Add an an/a article in the front of the word"""
- first = word[1] if word[0] == 'h' else word[0]
- if first in "aeiou":
- return "an " + word
- return "a " + word
- action.setText('Define %s' % articulify(profileRoiClass.NAME))
- action.setToolTip('Enables %s selection mode' % profileRoiClass.NAME)
- action.setSingleShot(True)
- return action
-
- def createClearAction(self, parent):
- """Create an action to clean up the plot from the profile ROIs.
-
- :param qt.QObject parent: The parent of the created action.
- :rtype: qt.QAction
- """
- action = _ClearAction(parent, self)
- return action
-
- def createImageActions(self, parent):
- """Create actions designed for image items. This actions created
- new ROIs.
-
- :param qt.QObject parent: The parent of the created action.
- :rtype: List[qt.QAction]
- """
- profileClasses = [
- rois.ProfileImageHorizontalLineROI,
- rois.ProfileImageVerticalLineROI,
- rois.ProfileImageLineROI,
- rois.ProfileImageDirectedLineROI,
- rois.ProfileImageCrossROI,
- ]
- return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
-
- def createScatterActions(self, parent):
- """Create actions designed for scatter items. This actions created
- new ROIs.
-
- :param qt.QObject parent: The parent of the created action.
- :rtype: List[qt.QAction]
- """
- profileClasses = [
- rois.ProfileScatterHorizontalLineROI,
- rois.ProfileScatterVerticalLineROI,
- rois.ProfileScatterLineROI,
- rois.ProfileScatterCrossROI,
- ]
- return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
-
- def createScatterSliceActions(self, parent):
- """Create actions designed for regular scatter items. This actions
- created new ROIs.
-
- This ROIs was designed to use the input data without interpolation,
- like you could do with an image.
-
- :param qt.QObject parent: The parent of the created action.
- :rtype: List[qt.QAction]
- """
- profileClasses = [
- rois.ProfileScatterHorizontalSliceROI,
- rois.ProfileScatterVerticalSliceROI,
- rois.ProfileScatterCrossSliceROI,
- ]
- return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
-
- def createImageStackActions(self, parent):
- """Create actions designed for stack image items. This actions
- created new ROIs.
-
- This ROIs was designed to create both profile on the displayed image
- and profile on the full stack (2D result).
-
- :param qt.QObject parent: The parent of the created action.
- :rtype: List[qt.QAction]
- """
- profileClasses = [
- rois.ProfileImageStackHorizontalLineROI,
- rois.ProfileImageStackVerticalLineROI,
- rois.ProfileImageStackLineROI,
- rois.ProfileImageStackCrossROI,
- ]
- return [self.createProfileAction(pc, parent=parent) for pc in profileClasses]
-
- def createEditorAction(self, parent):
- """Create an action containing GUI to edit the selected profile ROI.
-
- :param qt.QObject parent: The parent of the created action.
- :rtype: qt.QAction
- """
- action = editors.ProfileRoiEditorAction(parent)
- action.setRoiManager(self.getRoiManager())
- return action
-
- def setItemType(self, image=False, scatter=False):
- """Set the item type to use and select the active one.
-
- :param bool image: Image item are allowed
- :param bool scatter: Scatter item are allowed
- """
- self.__itemTypes = []
- plot = self.getPlotWidget()
- item = None
- if image:
- self.__itemTypes.append("image")
- item = plot.getActiveImage()
- if scatter:
- self.__itemTypes.append("scatter")
- if item is None:
- item = plot.getActiveScatter()
- self.setPlotItem(item)
-
- def setProfileWindowClass(self, profileWindowClass):
- """Set the class which will be instantiated to display profile result.
- """
- self._profileWindowClass = profileWindowClass
-
- def setActiveItemTracking(self, tracking):
- """Enable/disable the tracking of the active item of the plot.
-
- :param bool tracking: Tracking mode
- """
- if self.__tracking == tracking:
- return
- plot = self.getPlotWidget()
- if self.__tracking:
- plot.sigActiveImageChanged.disconnect(self._activeImageChanged)
- plot.sigActiveScatterChanged.disconnect(self._activeScatterChanged)
- self.__tracking = tracking
- if self.__tracking:
- plot.sigActiveImageChanged.connect(self.__activeImageChanged)
- plot.sigActiveScatterChanged.connect(self.__activeScatterChanged)
-
- def setDefaultColorFromCursorColor(self, enabled):
- """Enabled/disable the use of the colormap cursor color to display the
- ROIs.
-
- If set, the manager will update the color of the profile ROIs using the
- current colormap cursor color from the selected item.
- """
- self.__useColorFromCursor = enabled
-
- def __activeImageChanged(self, previous, legend):
- """Handle plot item selection"""
- if "image" in self.__itemTypes:
- plot = self.getPlotWidget()
- item = plot.getImage(legend)
- self.setPlotItem(item)
-
- def __activeScatterChanged(self, previous, legend):
- """Handle plot item selection"""
- if "scatter" in self.__itemTypes:
- plot = self.getPlotWidget()
- item = plot.getScatter(legend)
- self.setPlotItem(item)
-
- def __roiCreated(self, roi):
- """Handle ROI creation"""
- # Filter out non profile ROIs
- if isinstance(roi, core.ProfileRoiMixIn):
- if self._storeProperties is not None:
- # Initialize the properties with the previous ones
- self._storeProperties.restoreProperties(roi)
-
- def __addProfile(self, profileRoi):
- """Add a new ROI to the manager."""
- if profileRoi.getFocusProxy() is None:
- if self._storeProperties is not None:
- # Follow changes on properties
- self._storeProperties.setProfileRoi(profileRoi)
- if self.__singleProfileAtATime:
- # FIXME: It would be good to reuse the windows to avoid blinking
- self.clearProfile()
-
- profileRoi._setProfileManager(self)
- self._updateRoiColor(profileRoi)
- self._rois.append(profileRoi)
- self.requestUpdateProfile(profileRoi)
-
- def __removeProfile(self, profileRoi):
- """Remove a ROI from the manager."""
- window = self._disconnectProfileWindow(profileRoi)
- if window is not None:
- geometry = window.geometry()
- if not geometry.isEmpty():
- self._previousWindowGeometry.append(geometry)
- self.clearProfileWindow(window)
- if profileRoi in self._rois:
- self._rois.remove(profileRoi)
-
- def _disconnectProfileWindow(self, profileRoi):
- """Handle profile window close."""
- window = profileRoi.getProfileWindow()
- profileRoi.setProfileWindow(None)
- return window
-
- def clearProfile(self):
- """Clear the associated ROI profile"""
- roiManager = self.getRoiManager()
- for roi in list(self._rois):
- if roi.getFocusProxy() is not None:
- # Skip sub ROIs, it will be removed by their parents
- continue
- roiManager.removeRoi(roi)
-
- if not roiManager.isDrawing():
- # Clean the selected mode
- roiManager.stop()
-
- def hasPendingOperations(self):
- """Returns true if a thread is still computing or displaying a profile.
-
- :rtype: bool
- """
- return len(self.__reentrantResults) > 0 or len(self._pendingRunners) > 0
-
- def requestUpdateAllProfile(self):
- """Request to update the profile of all the managed ROIs.
- """
- for roi in self._rois:
- self.requestUpdateProfile(roi)
-
- def requestUpdateProfile(self, profileRoi):
- """Request to update a specific profile ROI.
-
- :param ~core.ProfileRoiMixIn profileRoi:
- """
- if profileRoi.computeProfile is None:
- return
- threadPool = silxGlobalThreadPool()
-
- # Clean up deprecated runners
- for runner in list(self._pendingRunners):
- if not inspect.isValid(runner):
- self._pendingRunners.remove(runner)
- continue
- if runner.getRoi() is profileRoi:
- if hasattr(threadPool, "tryTake"):
- if threadPool.tryTake(runner):
- self._pendingRunners.remove(runner)
- else: # Support Qt<5.9
- runner._lazyCancel()
-
- item = self.getPlotItem()
- if item is None or not isinstance(item, profileRoi.ITEM_KIND):
- # This item is not compatible with this profile
- profileRoi._setPlotItem(None)
- profileWindow = profileRoi.getProfileWindow()
- if profileWindow is not None:
- profileWindow.setProfile(None)
- return
-
- profileRoi._setPlotItem(item)
- runner = _RunnableComputeProfile(threadPool, item, profileRoi)
- runner.runnerFinished.connect(self.__cleanUpRunner)
- runner.resultReady.connect(self.__displayResult)
- self._pendingRunners.append(runner)
- threadPool.start(runner)
-
- def __cleanUpRunner(self, runner):
- """Remove a thread pool runner from the list of hold tasks.
-
- Called at the termination of the runner.
- """
- if runner in self._pendingRunners:
- self._pendingRunners.remove(runner)
-
- def __displayResult(self, roi, profileData):
- """Display the result of a ROI.
-
- :param ~core.ProfileRoiMixIn profileRoi: A managed ROI
- :param ~core.CurveProfileData profileData: Computed data profile
- """
- if roi in self.__reentrantResults:
- # Store the data to process it in the main loop
- # And not a sub loop created by initProfileWindow
- # This also remove the duplicated requested
- self.__reentrantResults[roi] = profileData
- return
-
- self.__reentrantResults[roi] = profileData
- self._computedProfiles = self._computedProfiles + 1
- window = roi.getProfileWindow()
- if window is None:
- plot = self.getPlotWidget()
- window = self.createProfileWindow(plot, roi)
- # roi.profileWindow have to be set before initializing the window
- # Cause the initialization is using QEventLoop
- roi.setProfileWindow(window)
- self.initProfileWindow(window, roi)
- window.show()
-
- lastData = self.__reentrantResults.pop(roi)
- window.setProfile(lastData)
-
- def __plotDestroyed(self, ref):
- """Handle finalization of PlotWidget
-
- :param ref: weakref to the plot
- """
- self._plotRef = None
- self._roiManagerRef = None
- self._pendingRunners = []
-
- def setPlotItem(self, item):
- """Set the plot item focused by the profile manager.
-
- :param ~silx.gui.plot.items.Item item: A plot item
- """
- previous = self.getPlotItem()
- if previous is item:
- return
- if item is None:
- self._item = None
- else:
- item.sigItemChanged.connect(self.__itemChanged)
- self._item = weakref.ref(item)
- self._updateRoiColors()
- self.requestUpdateAllProfile()
-
- def getDefaultColor(self, item):
- """Returns the default ROI color to use according to the given item.
-
- :param ~silx.gui.plot.items.item.Item item: AN item
- :rtype: qt.QColor
- """
- color = 'pink'
- if isinstance(item, items.ColormapMixIn):
- colormap = item.getColormap()
- name = colormap.getName()
- if name is not None:
- color = colors.cursorColorForColormap(name)
- color = colors.asQColor(color)
- return color
-
- def _updateRoiColors(self):
- """Update ROI color according to the item selection"""
- if not self.__useColorFromCursor:
- return
- item = self.getPlotItem()
- color = self.getDefaultColor(item)
- for roi in self._rois:
- roi.setColor(color)
-
- def _updateRoiColor(self, roi):
- """Update a specific ROI according to the current selected item.
-
- :param RegionOfInterest roi: The ROI to update
- """
- if not self.__useColorFromCursor:
- return
- item = self.getPlotItem()
- color = self.getDefaultColor(item)
- roi.setColor(color)
-
- def __itemChanged(self, changeType):
- """Handle item changes.
- """
- if changeType in (items.ItemChangedType.DATA,
- items.ItemChangedType.MASK,
- items.ItemChangedType.POSITION,
- items.ItemChangedType.SCALE):
- self.requestUpdateAllProfile()
- elif changeType == (items.ItemChangedType.COLORMAP):
- self._updateRoiColors()
-
- def getPlotItem(self):
- """Returns the item focused by the profile manager.
-
- :rtype: ~silx.gui.plot.items.Item
- """
- if self._item is None:
- return None
- item = self._item()
- if item is None:
- self._item = None
- return item
-
- def getPlotWidget(self):
- """The plot associated to the profile manager.
-
- :rtype: ~silx.gui.plot.PlotWidget
- """
- if self._plotRef is None:
- return None
- plot = self._plotRef()
- if plot is None:
- self._plotRef = None
- return plot
-
- def getCurrentRoi(self):
- """Returns the currently selected ROI, else None.
-
- :rtype: core.ProfileRoiMixIn
- """
- roiManager = self.getRoiManager()
- if roiManager is None:
- return None
- roi = roiManager.getCurrentRoi()
- if not isinstance(roi, core.ProfileRoiMixIn):
- return None
- return roi
-
- def getRoiManager(self):
- """Returns the used ROI manager
-
- :rtype: RegionOfInterestManager
- """
- return self._roiManagerRef()
-
- def createProfileWindow(self, plot, roi):
- """Create a new profile window.
-
- :param ~core.ProfileRoiMixIn roi: The plot containing the raw data
- :param ~core.ProfileRoiMixIn roi: A managed ROI
- :rtype: ~ProfileWindow
- """
- return self._profileWindowClass(plot)
-
- def initProfileWindow(self, profileWindow, roi):
- """This function is called just after the profile window creation in
- order to initialize the window location.
-
- :param ~ProfileWindow profileWindow:
- The profile window to initialize.
- """
- # Enforce the use of one of the widgets
- # To have the correct window size
- profileWindow.prepareWidget(roi)
- profileWindow.adjustSize()
-
- # Trick to avoid blinking while retrieving the right window size
- # Display the window, hide it and wait for some event loops
- profileWindow.show()
- profileWindow.hide()
- eventLoop = qt.QEventLoop(self)
- for _ in range(10):
- if not eventLoop.processEvents():
- break
-
- profileWindow.show()
- if len(self._previousWindowGeometry) > 0:
- geometry = self._previousWindowGeometry.pop()
- profileWindow.setGeometry(geometry)
- return
-
- window = self.getPlotWidget().window()
- winGeom = window.frameGeometry()
- qapp = qt.QApplication.instance()
- desktop = qapp.desktop()
- screenGeom = desktop.availableGeometry(window)
- spaceOnLeftSide = winGeom.left()
- spaceOnRightSide = screenGeom.width() - winGeom.right()
-
- profileGeom = profileWindow.frameGeometry()
- profileWidth = profileGeom.width()
-
- # Align vertically to the center of the window
- top = winGeom.top() + (winGeom.height() - profileGeom.height()) // 2
-
- margin = 5
- if profileWidth < spaceOnRightSide:
- # Place profile on the right
- left = winGeom.right() + margin
- elif profileWidth < spaceOnLeftSide:
- # Place profile on the left
- left = max(0, winGeom.left() - profileWidth - margin)
- else:
- # Move it as much as possible where there is more space
- if spaceOnLeftSide > spaceOnRightSide:
- left = 0
- else:
- left = screenGeom.width() - profileGeom.width()
- profileWindow.move(left, top)
-
-
- def clearProfileWindow(self, profileWindow):
- """Called when a profile window is not anymore needed.
-
- By default the window will be closed. But it can be
- inherited to change this behavior.
- """
- profileWindow.deleteLater()
diff --git a/silx/gui/plot/tools/profile/rois.py b/silx/gui/plot/tools/profile/rois.py
deleted file mode 100644
index eb7e975..0000000
--- a/silx/gui/plot/tools/profile/rois.py
+++ /dev/null
@@ -1,1156 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2021 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 define ROIs for profile tools.
-
-.. inheritance-diagram::
- silx.gui.plot.tools.profile.rois
- :top-classes: silx.gui.plot.tools.profile.core.ProfileRoiMixIn, silx.gui.plot.items.roi.RegionOfInterest
- :parts: 1
- :private-bases:
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "01/12/2020"
-
-import numpy
-import weakref
-from concurrent.futures import CancelledError
-
-from silx.gui import colors
-
-from silx.gui.plot import items
-from silx.gui.plot.items import roi as roi_items
-from . import core
-from silx.gui import utils
-from .....utils.proxy import docstring
-
-
-def _relabelAxes(plot, text):
- """Relabel {xlabel} and {ylabel} from this text using the corresponding
- plot axis label. If the axis label is empty, label it with "X" and "Y".
-
- :rtype: str
- """
- xLabel = plot.getXAxis().getLabel()
- if not xLabel:
- xLabel = "X"
- yLabel = plot.getYAxis().getLabel()
- if not yLabel:
- yLabel = "Y"
- return text.format(xlabel=xLabel, ylabel=yLabel)
-
-
-def _lineProfileTitle(x0, y0, x1, y1):
- """Compute corresponding plot title
-
- This can be overridden to change title behavior.
-
- :param float x0: Profile start point X coord
- :param float y0: Profile start point Y coord
- :param float x1: Profile end point X coord
- :param float y1: Profile end point Y coord
- :return: Title to use
- :rtype: str
- """
- if x0 == x1:
- title = '{xlabel} = %g; {ylabel} = [%g, %g]' % (x0, y0, y1)
- elif y0 == y1:
- title = '{ylabel} = %g; {xlabel} = [%g, %g]' % (y0, x0, x1)
- else:
- m = (y1 - y0) / (x1 - x0)
- b = y0 - m * x0
- title = '{ylabel} = %g * {xlabel} %+g' % (m, b)
-
- return title
-
-
-class _ImageProfileArea(items.Shape):
- """This shape displays the location of pixels used to compute the
- profile."""
-
- def __init__(self, parentRoi):
- items.Shape.__init__(self, "polygon")
- color = colors.rgba(parentRoi.getColor())
- self.setColor(color)
- self.setFill(True)
- self.setOverlay(True)
- self.setPoints([[0, 0], [0, 0]]) # Else it segfault
-
- self.__parentRoi = weakref.ref(parentRoi)
- parentRoi.sigItemChanged.connect(self._updateAreaProperty)
- parentRoi.sigRegionChanged.connect(self._updateArea)
- parentRoi.sigProfilePropertyChanged.connect(self._updateArea)
- parentRoi.sigPlotItemChanged.connect(self._updateArea)
-
- def getParentRoi(self):
- if self.__parentRoi is None:
- return None
- parentRoi = self.__parentRoi()
- if parentRoi is None:
- self.__parentRoi = None
- return parentRoi
-
- def _updateAreaProperty(self, event=None, checkVisibility=True):
- parentRoi = self.sender()
- if event == items.ItemChangedType.COLOR:
- parentRoi._updateItemProperty(event, parentRoi, self)
- elif event == items.ItemChangedType.VISIBLE:
- if self.getPlotItem() is not None:
- parentRoi._updateItemProperty(event, parentRoi, self)
-
- def _updateArea(self):
- roi = self.getParentRoi()
- item = roi.getPlotItem()
- if item is None:
- self.setVisible(False)
- return
- polygon = self._computePolygon(item)
- self.setVisible(True)
- polygon = numpy.array(polygon).T
- self.setLineStyle("--")
- self.setPoints(polygon, copy=False)
-
- def _computePolygon(self, item):
- if not isinstance(item, items.ImageBase):
- raise TypeError("Unexpected class %s" % type(item))
-
- currentData = item.getValueData(copy=False)
-
- roi = self.getParentRoi()
- origin = item.getOrigin()
- scale = item.getScale()
- _coords, _profile, area, _profileName, _xLabel = core.createProfile(
- roiInfo=roi._getRoiInfo(),
- currentData=currentData,
- origin=origin,
- scale=scale,
- lineWidth=roi.getProfileLineWidth(),
- method="none")
- return area
-
-
-class _SliceProfileArea(items.Shape):
- """This shape displays the location a profile in a scatter.
-
- Each point used to compute the slice are linked together.
- """
-
- def __init__(self, parentRoi):
- items.Shape.__init__(self, "polygon")
- color = colors.rgba(parentRoi.getColor())
- self.setColor(color)
- self.setFill(True)
- self.setOverlay(True)
- self.setPoints([[0, 0], [0, 0]]) # Else it segfault
-
- self.__parentRoi = weakref.ref(parentRoi)
- parentRoi.sigItemChanged.connect(self._updateAreaProperty)
- parentRoi.sigRegionChanged.connect(self._updateArea)
- parentRoi.sigProfilePropertyChanged.connect(self._updateArea)
- parentRoi.sigPlotItemChanged.connect(self._updateArea)
-
- def getParentRoi(self):
- if self.__parentRoi is None:
- return None
- parentRoi = self.__parentRoi()
- if parentRoi is None:
- self.__parentRoi = None
- return parentRoi
-
- def _updateAreaProperty(self, event=None, checkVisibility=True):
- parentRoi = self.sender()
- if event == items.ItemChangedType.COLOR:
- parentRoi._updateItemProperty(event, parentRoi, self)
- elif event == items.ItemChangedType.VISIBLE:
- if self.getPlotItem() is not None:
- parentRoi._updateItemProperty(event, parentRoi, self)
-
- def _updateArea(self):
- roi = self.getParentRoi()
- item = roi.getPlotItem()
- if item is None:
- self.setVisible(False)
- return
- polylines = self._computePolylines(roi, item)
- if polylines is None:
- self.setVisible(False)
- return
- self.setVisible(True)
- self.setLineStyle("--")
- self.setPoints(polylines, copy=False)
-
- def _computePolylines(self, roi, item):
- slicing = roi._getSlice(item)
- if slicing is None:
- return None
- xx, yy, _values, _xx_error, _yy_error = item.getData(copy=False)
- xx, yy = xx[slicing], yy[slicing]
- polylines = numpy.array((xx, yy)).T
- if len(polylines) == 0:
- return None
- return polylines
-
-
-class _DefaultImageProfileRoiMixIn(core.ProfileRoiMixIn):
- """Provide common behavior for silx default image profile ROI.
- """
-
- ITEM_KIND = items.ImageBase
-
- def __init__(self, parent=None):
- core.ProfileRoiMixIn.__init__(self, parent=parent)
- self.__method = "mean"
- self.__width = 1
- self.sigRegionChanged.connect(self.__regionChanged)
- self.sigPlotItemChanged.connect(self.__updateArea)
- self.__area = _ImageProfileArea(self)
- self.addItem(self.__area)
-
- def __regionChanged(self):
- self.invalidateProfile()
- self.__updateArea()
-
- def setProfileMethod(self, method):
- """
- :param str method: method to compute the profile. Can be 'mean' or 'sum'
- """
- if self.__method == method:
- return
- self.__method = method
- self.invalidateProperties()
- self.invalidateProfile()
-
- def getProfileMethod(self):
- return self.__method
-
- def setProfileLineWidth(self, width):
- if self.__width == width:
- return
- self.__width = width
- self.__updateArea()
- self.invalidateProperties()
- self.invalidateProfile()
-
- def getProfileLineWidth(self):
- return self.__width
-
- def __updateArea(self):
- plotItem = self.getPlotItem()
- if plotItem is None:
- self.setLineStyle("-")
- else:
- self.setLineStyle("--")
-
- def _getRoiInfo(self):
- """Wrapper to allow to reuse the previous Profile code.
-
- It would be good to remove it at one point.
- """
- if isinstance(self, roi_items.HorizontalLineROI):
- lineProjectionMode = 'X'
- y = self.getPosition()
- roiStart = (0, y)
- roiEnd = (1, y)
- elif isinstance(self, roi_items.VerticalLineROI):
- lineProjectionMode = 'Y'
- x = self.getPosition()
- roiStart = (x, 0)
- roiEnd = (x, 1)
- elif isinstance(self, roi_items.LineROI):
- lineProjectionMode = 'D'
- roiStart, roiEnd = self.getEndPoints()
- else:
- assert False
-
- return roiStart, roiEnd, lineProjectionMode
-
- def computeProfile(self, item):
- if not isinstance(item, items.ImageBase):
- raise TypeError("Unexpected class %s" % type(item))
-
- origin = item.getOrigin()
- scale = item.getScale()
- method = self.getProfileMethod()
- lineWidth = self.getProfileLineWidth()
-
- def createProfile2(currentData):
- coords, profile, _area, profileName, xLabel = core.createProfile(
- roiInfo=self._getRoiInfo(),
- currentData=currentData,
- origin=origin,
- scale=scale,
- lineWidth=lineWidth,
- method=method)
- return coords, profile, profileName, xLabel
-
- currentData = item.getValueData(copy=False)
-
- yLabel = "%s" % str(method).capitalize()
- coords, profile, title, xLabel = createProfile2(currentData)
- title = title + "; width = %d" % lineWidth
-
- # Use the axis names from the original plot
- profileManager = self.getProfileManager()
- plot = profileManager.getPlotWidget()
- title = _relabelAxes(plot, title)
- xLabel = _relabelAxes(plot, xLabel)
-
- if isinstance(item, items.ImageRgba):
- rgba = item.getData(copy=False)
- _coords, r, _profileName, _xLabel = createProfile2(rgba[..., 0])
- _coords, g, _profileName, _xLabel = createProfile2(rgba[..., 1])
- _coords, b, _profileName, _xLabel = createProfile2(rgba[..., 2])
- if rgba.shape[-1] == 4:
- _coords, a, _profileName, _xLabel = createProfile2(rgba[..., 3])
- else:
- a = [None]
- data = core.RgbaProfileData(
- coords=coords,
- profile=profile[0],
- profile_r=r[0],
- profile_g=g[0],
- profile_b=b[0],
- profile_a=a[0],
- title=title,
- xLabel=xLabel,
- yLabel=yLabel,
- )
- else:
- data = core.CurveProfileData(
- coords=coords,
- profile=profile[0],
- title=title,
- xLabel=xLabel,
- yLabel=yLabel,
- )
- return data
-
-
-class ProfileImageHorizontalLineROI(roi_items.HorizontalLineROI,
- _DefaultImageProfileRoiMixIn):
- """ROI for an horizontal profile at a location of an image"""
-
- ICON = 'shape-horizontal'
- NAME = 'horizontal line profile'
-
- def __init__(self, parent=None):
- roi_items.HorizontalLineROI.__init__(self, parent=parent)
- _DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileImageVerticalLineROI(roi_items.VerticalLineROI,
- _DefaultImageProfileRoiMixIn):
- """ROI for a vertical profile at a location of an image"""
-
- ICON = 'shape-vertical'
- NAME = 'vertical line profile'
-
- def __init__(self, parent=None):
- roi_items.VerticalLineROI.__init__(self, parent=parent)
- _DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileImageLineROI(roi_items.LineROI,
- _DefaultImageProfileRoiMixIn):
- """ROI for an image profile between 2 points.
-
- The X profile of this ROI is the projecting into one of the x/y axes,
- using its scale and its orientation.
- """
-
- ICON = 'shape-diagonal'
- NAME = 'line profile'
-
- def __init__(self, parent=None):
- roi_items.LineROI.__init__(self, parent=parent)
- _DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileImageDirectedLineROI(roi_items.LineROI,
- _DefaultImageProfileRoiMixIn):
- """ROI for an image profile between 2 points.
-
- The X profile of the line is displayed projected into the line itself,
- using its scale and its orientation. It's the distance from the origin.
- """
-
- ICON = 'shape-diagonal-directed'
- NAME = 'directed line profile'
-
- def __init__(self, parent=None):
- roi_items.LineROI.__init__(self, parent=parent)
- _DefaultImageProfileRoiMixIn.__init__(self, parent=parent)
- self._handleStart.setSymbol('o')
-
- def computeProfile(self, item):
- if not isinstance(item, items.ImageBase):
- raise TypeError("Unexpected class %s" % type(item))
-
- from silx.image.bilinear import BilinearImage
-
- origin = item.getOrigin()
- scale = item.getScale()
- method = self.getProfileMethod()
- lineWidth = self.getProfileLineWidth()
- currentData = item.getValueData(copy=False)
-
- roiInfo = self._getRoiInfo()
- roiStart, roiEnd, _lineProjectionMode = roiInfo
-
- startPt = ((roiStart[1] - origin[1]) / scale[1],
- (roiStart[0] - origin[0]) / scale[0])
- endPt = ((roiEnd[1] - origin[1]) / scale[1],
- (roiEnd[0] - origin[0]) / scale[0])
-
- if numpy.array_equal(startPt, endPt):
- return None
-
- bilinear = BilinearImage(currentData)
- profile = bilinear.profile_line(
- (startPt[0] - 0.5, startPt[1] - 0.5),
- (endPt[0] - 0.5, endPt[1] - 0.5),
- lineWidth,
- method=method)
-
- # Compute the line size
- lineSize = numpy.sqrt((roiEnd[1] - roiStart[1]) ** 2 +
- (roiEnd[0] - roiStart[0]) ** 2)
- coords = numpy.linspace(0, lineSize, len(profile),
- endpoint=True,
- dtype=numpy.float32)
-
- title = _lineProfileTitle(*roiStart, *roiEnd)
- title = title + "; width = %d" % lineWidth
- xLabel = "√({xlabel}²+{ylabel}²)"
- yLabel = str(method).capitalize()
-
- # Use the axis names from the original plot
- profileManager = self.getProfileManager()
- plot = profileManager.getPlotWidget()
- xLabel = _relabelAxes(plot, xLabel)
- title = _relabelAxes(plot, title)
-
- data = core.CurveProfileData(
- coords=coords,
- profile=profile,
- title=title,
- xLabel=xLabel,
- yLabel=yLabel,
- )
- return data
-
-
-class _ProfileCrossROI(roi_items.HandleBasedROI, core.ProfileRoiMixIn):
-
- """ROI to manage a cross of profiles
-
- It is managed using 2 sub ROIs for vertical and horizontal.
- """
-
- _kind = "Cross"
- """Label for this kind of ROI"""
-
- _plotShape = "point"
- """Plot shape which is used for the first interaction"""
-
- def __init__(self, parent=None):
- roi_items.HandleBasedROI.__init__(self, parent=parent)
- core.ProfileRoiMixIn.__init__(self, parent=parent)
- self.sigRegionChanged.connect(self.__regionChanged)
- self.sigAboutToBeRemoved.connect(self.__aboutToBeRemoved)
- self.__position = 0, 0
- self.__vline = None
- self.__hline = None
- self.__handle = self.addHandle()
- self.__handleLabel = self.addLabelHandle()
- self.__handleLabel.setText(self.getName())
- self.__inhibitReentance = utils.LockReentrant()
- self.computeProfile = None
- self.sigItemChanged.connect(self.__updateLineProperty)
-
- # Make sure the marker is over the ROIs
- self.__handle.setZValue(1)
- # Create the vline and the hline
- self._createSubRois()
-
- @docstring(roi_items.HandleBasedROI)
- def contains(self, position):
- roiPos = self.getPosition()
- return position[0] == roiPos[0] or position[1] == roiPos[1]
-
- def setFirstShapePoints(self, points):
- pos = points[0]
- self.setPosition(pos)
-
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
- return self.__position
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param numpy.ndarray pos: 2d-coordinate of this point
- """
- self.__position = pos
- with utils.blockSignals(self.__handle):
- self.__handle.setPosition(*pos)
- with utils.blockSignals(self.__handleLabel):
- self.__handleLabel.setPosition(*pos)
- self.sigRegionChanged.emit()
-
- def handleDragUpdated(self, handle, origin, previous, current):
- if handle is self.__handle:
- self.setPosition(current)
-
- def __updateLineProperty(self, event=None, checkVisibility=True):
- if event == items.ItemChangedType.NAME:
- self.__handleLabel.setText(self.getName())
- elif event in [items.ItemChangedType.COLOR,
- items.ItemChangedType.VISIBLE]:
- lines = []
- if self.__vline:
- lines.append(self.__vline)
- if self.__hline:
- lines.append(self.__hline)
- self._updateItemProperty(event, self, lines)
-
- def _createLines(self, parent):
- """Inherit this function to return 2 ROI objects for respectivly
- the horizontal, and the vertical lines."""
- raise NotImplementedError()
-
- def _setProfileManager(self, profileManager):
- core.ProfileRoiMixIn._setProfileManager(self, profileManager)
- # Connecting the vline and the hline
- roiManager = profileManager.getRoiManager()
- roiManager.addRoi(self.__vline)
- roiManager.addRoi(self.__hline)
-
- def _createSubRois(self):
- hline, vline = self._createLines(parent=None)
- for i, line in enumerate([vline, hline]):
- line.setPosition(self.__position[i])
- line.setEditable(True)
- line.setSelectable(True)
- line.setFocusProxy(self)
- line.setName("")
- self.__vline = vline
- self.__hline = hline
- vline.sigAboutToBeRemoved.connect(self.__vlineRemoved)
- vline.sigRegionChanged.connect(self.__vlineRegionChanged)
- hline.sigAboutToBeRemoved.connect(self.__hlineRemoved)
- hline.sigRegionChanged.connect(self.__hlineRegionChanged)
-
- def _getLines(self):
- return self.__hline, self.__vline
-
- def __regionChanged(self):
- if self.__inhibitReentance.locked():
- return
- x, y = self.getPosition()
- hline, vline = self._getLines()
- if hline is None:
- return
- with self.__inhibitReentance:
- hline.setPosition(y)
- vline.setPosition(x)
-
- def __vlineRegionChanged(self):
- if self.__inhibitReentance.locked():
- return
- pos = self.getPosition()
- vline = self.__vline
- pos = vline.getPosition(), pos[1]
- with self.__inhibitReentance:
- self.setPosition(pos)
-
- def __hlineRegionChanged(self):
- if self.__inhibitReentance.locked():
- return
- pos = self.getPosition()
- hline = self.__hline
- pos = pos[0], hline.getPosition()
- with self.__inhibitReentance:
- self.setPosition(pos)
-
- def __aboutToBeRemoved(self):
- vline = self.__vline
- hline = self.__hline
- # Avoid side remove signals
- if hline is not None:
- hline.sigAboutToBeRemoved.disconnect(self.__hlineRemoved)
- hline.sigRegionChanged.disconnect(self.__hlineRegionChanged)
- if vline is not None:
- vline.sigAboutToBeRemoved.disconnect(self.__vlineRemoved)
- vline.sigRegionChanged.disconnect(self.__vlineRegionChanged)
- # Clean up the child
- profileManager = self.getProfileManager()
- roiManager = profileManager.getRoiManager()
- if hline is not None:
- roiManager.removeRoi(hline)
- self.__hline = None
- if vline is not None:
- roiManager.removeRoi(vline)
- self.__vline = None
-
- def __hlineRemoved(self):
- self.__lineRemoved(isHline=True)
-
- def __vlineRemoved(self):
- self.__lineRemoved(isHline=False)
-
- def __lineRemoved(self, isHline):
- """If any of the lines is removed: disconnect this objects, and let the
- other one persist"""
- hline, vline = self._getLines()
-
- hline.sigAboutToBeRemoved.disconnect(self.__hlineRemoved)
- vline.sigAboutToBeRemoved.disconnect(self.__vlineRemoved)
- hline.sigRegionChanged.disconnect(self.__hlineRegionChanged)
- vline.sigRegionChanged.disconnect(self.__vlineRegionChanged)
-
- self.__hline = None
- self.__vline = None
- profileManager = self.getProfileManager()
- roiManager = profileManager.getRoiManager()
- if isHline:
- self.__releaseLine(vline)
- else:
- self.__releaseLine(hline)
- roiManager.removeRoi(self)
-
- def __releaseLine(self, line):
- """Release the line in order to make it independent"""
- line.setFocusProxy(None)
- line.setName(self.getName())
- line.setEditable(self.isEditable())
- line.setSelectable(self.isSelectable())
-
-
-class ProfileImageCrossROI(_ProfileCrossROI):
- """ROI to manage a cross of profiles
-
- It is managed using 2 sub ROIs for vertical and horizontal.
- """
-
- ICON = 'shape-cross'
- NAME = 'cross profile'
- ITEM_KIND = items.ImageBase
-
- def _createLines(self, parent):
- vline = ProfileImageVerticalLineROI(parent=parent)
- hline = ProfileImageHorizontalLineROI(parent=parent)
- return hline, vline
-
- def setProfileMethod(self, method):
- """
- :param str method: method to compute the profile. Can be 'mean' or 'sum'
- """
- hline, vline = self._getLines()
- hline.setProfileMethod(method)
- vline.setProfileMethod(method)
- self.invalidateProperties()
-
- def getProfileMethod(self):
- hline, _vline = self._getLines()
- return hline.getProfileMethod()
-
- def setProfileLineWidth(self, width):
- hline, vline = self._getLines()
- hline.setProfileLineWidth(width)
- vline.setProfileLineWidth(width)
- self.invalidateProperties()
-
- def getProfileLineWidth(self):
- hline, _vline = self._getLines()
- return hline.getProfileLineWidth()
-
-
-class _DefaultScatterProfileRoiMixIn(core.ProfileRoiMixIn):
- """Provide common behavior for silx default scatter profile ROI.
- """
-
- ITEM_KIND = items.Scatter
-
- def __init__(self, parent=None):
- core.ProfileRoiMixIn.__init__(self, parent=parent)
- self.__nPoints = 1024
- self.sigRegionChanged.connect(self.__regionChanged)
-
- def __regionChanged(self):
- self.invalidateProfile()
-
- # Number of points
-
- def getNPoints(self):
- """Returns the number of points of the profiles
-
- :rtype: int
- """
- return self.__nPoints
-
- def setNPoints(self, npoints):
- """Set the number of points of the profiles
-
- :param int npoints:
- """
- npoints = int(npoints)
- if npoints < 1:
- raise ValueError("Unsupported number of points: %d" % npoints)
- elif npoints != self.__nPoints:
- self.__nPoints = npoints
- self.invalidateProperties()
- self.invalidateProfile()
-
- def _computeProfile(self, scatter, x0, y0, x1, y1):
- """Compute corresponding profile
-
- :param float x0: Profile start point X coord
- :param float y0: Profile start point Y coord
- :param float x1: Profile end point X coord
- :param float y1: Profile end point Y coord
- :return: (points, values) profile data or None
- """
- future = scatter._getInterpolator()
- try:
- interpolator = future.result()
- except CancelledError:
- return None
- if interpolator is None:
- return None # Cannot init an interpolator
-
- nPoints = self.getNPoints()
- points = numpy.transpose((
- numpy.linspace(x0, x1, nPoints, endpoint=True),
- numpy.linspace(y0, y1, nPoints, endpoint=True)))
-
- values = interpolator(points)
-
- if not numpy.any(numpy.isfinite(values)):
- return None # Profile outside convex hull
-
- return points, values
-
- def computeProfile(self, item):
- """Update profile according to current ROI"""
- if not isinstance(item, items.Scatter):
- raise TypeError("Unexpected class %s" % type(item))
-
- # Get end points
- if isinstance(self, roi_items.LineROI):
- points = self.getEndPoints()
- x0, y0 = points[0]
- x1, y1 = points[1]
- elif isinstance(self, (roi_items.VerticalLineROI, roi_items.HorizontalLineROI)):
- profileManager = self.getProfileManager()
- plot = profileManager.getPlotWidget()
-
- if isinstance(self, roi_items.HorizontalLineROI):
- x0, x1 = plot.getXAxis().getLimits()
- y0 = y1 = self.getPosition()
-
- elif isinstance(self, roi_items.VerticalLineROI):
- x0 = x1 = self.getPosition()
- y0, y1 = plot.getYAxis().getLimits()
- else:
- raise RuntimeError('Unsupported ROI for profile: {}'.format(self.__class__))
-
- if x1 < x0 or (x1 == x0 and y1 < y0):
- # Invert points
- x0, y0, x1, y1 = x1, y1, x0, y0
-
- profile = self._computeProfile(item, x0, y0, x1, y1)
- if profile is None:
- return None
-
- title = _lineProfileTitle(x0, y0, x1, y1)
- points = profile[0]
- values = profile[1]
-
- if (numpy.abs(points[-1, 0] - points[0, 0]) >
- numpy.abs(points[-1, 1] - points[0, 1])):
- xProfile = points[:, 0]
- xLabel = '{xlabel}'
- else:
- xProfile = points[:, 1]
- xLabel = '{ylabel}'
-
- # Use the axis names from the original
- profileManager = self.getProfileManager()
- plot = profileManager.getPlotWidget()
- title = _relabelAxes(plot, title)
- xLabel = _relabelAxes(plot, xLabel)
-
- data = core.CurveProfileData(
- coords=xProfile,
- profile=values,
- title=title,
- xLabel=xLabel,
- yLabel='Profile',
- )
- return data
-
-
-class ProfileScatterHorizontalLineROI(roi_items.HorizontalLineROI,
- _DefaultScatterProfileRoiMixIn):
- """ROI for an horizontal profile at a location of a scatter"""
-
- ICON = 'shape-horizontal'
- NAME = 'horizontal line profile'
-
- def __init__(self, parent=None):
- roi_items.HorizontalLineROI.__init__(self, parent=parent)
- _DefaultScatterProfileRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileScatterVerticalLineROI(roi_items.VerticalLineROI,
- _DefaultScatterProfileRoiMixIn):
- """ROI for an horizontal profile at a location of a scatter"""
-
- ICON = 'shape-vertical'
- NAME = 'vertical line profile'
-
- def __init__(self, parent=None):
- roi_items.VerticalLineROI.__init__(self, parent=parent)
- _DefaultScatterProfileRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileScatterLineROI(roi_items.LineROI,
- _DefaultScatterProfileRoiMixIn):
- """ROI for an horizontal profile at a location of a scatter"""
-
- ICON = 'shape-diagonal'
- NAME = 'line profile'
-
- def __init__(self, parent=None):
- roi_items.LineROI.__init__(self, parent=parent)
- _DefaultScatterProfileRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileScatterCrossROI(_ProfileCrossROI):
- """ROI to manage a cross of profiles for scatters.
- """
-
- ICON = 'shape-cross'
- NAME = 'cross profile'
- ITEM_KIND = items.Scatter
-
- def _createLines(self, parent):
- vline = ProfileScatterVerticalLineROI(parent=parent)
- hline = ProfileScatterHorizontalLineROI(parent=parent)
- return hline, vline
-
- def getNPoints(self):
- """Returns the number of points of the profiles
-
- :rtype: int
- """
- hline, _vline = self._getLines()
- return hline.getNPoints()
-
- def setNPoints(self, npoints):
- """Set the number of points of the profiles
-
- :param int npoints:
- """
- hline, vline = self._getLines()
- hline.setNPoints(npoints)
- vline.setNPoints(npoints)
- self.invalidateProperties()
-
-
-class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn):
- """Default ROI to allow to slice in the scatter data."""
-
- ITEM_KIND = items.Scatter
-
- def __init__(self, parent=None):
- core.ProfileRoiMixIn.__init__(self, parent=parent)
- self.__area = _SliceProfileArea(self)
- self.addItem(self.__area)
- self.sigRegionChanged.connect(self._regionChanged)
- self.sigPlotItemChanged.connect(self._updateArea)
-
- def _regionChanged(self):
- self.invalidateProfile()
- self._updateArea()
-
- def _updateArea(self):
- plotItem = self.getPlotItem()
- if plotItem is None:
- self.setLineStyle("-")
- else:
- self.setLineStyle("--")
-
- def _getSlice(self, item):
- position = self.getPosition()
- bounds = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_BOUNDS)
- if isinstance(self, roi_items.HorizontalLineROI):
- axis = 1
- elif isinstance(self, roi_items.VerticalLineROI):
- axis = 0
- else:
- assert False
- if position < bounds[0][axis] or position > bounds[1][axis]:
- # ROI outside of the scatter bound
- return None
-
- major_order = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_MAJOR_ORDER)
- assert major_order == 'row'
- max_grid_yy, max_grid_xx = item.getCurrentVisualizationParameter(items.Scatter.VisualizationParameter.GRID_SHAPE)
-
- xx, yy, _values, _xx_error, _yy_error = item.getData(copy=False)
- if isinstance(self, roi_items.HorizontalLineROI):
- axis = yy
- max_grid_first = max_grid_yy
- max_grid_second = max_grid_xx
- major_axis = major_order == 'column'
- elif isinstance(self, roi_items.VerticalLineROI):
- axis = xx
- max_grid_first = max_grid_xx
- max_grid_second = max_grid_yy
- major_axis = major_order == 'row'
- else:
- assert False
-
- def argnearest(array, value):
- array = numpy.abs(array - value)
- return numpy.argmin(array)
-
- if major_axis:
- # slice in the middle of the scatter
- start = max_grid_second // 2 * max_grid_first
- vslice = axis[start:start + max_grid_second]
- index = argnearest(vslice, position)
- slicing = slice(index, None, max_grid_first)
- else:
- # slice in the middle of the scatter
- vslice = axis[max_grid_second // 2::max_grid_second]
- index = argnearest(vslice, position)
- start = index * max_grid_second
- slicing = slice(start, start + max_grid_second)
-
- return slicing
-
- def computeProfile(self, item):
- if not isinstance(item, items.Scatter):
- raise TypeError("Unsupported %s item" % type(item))
-
- slicing = self._getSlice(item)
- if slicing is None:
- # ROI out of bounds
- return None
-
- _xx, _yy, values, _xx_error, _yy_error = item.getData(copy=False)
- profile = values[slicing]
-
- if isinstance(self, roi_items.HorizontalLineROI):
- title = "Horizontal slice"
- xLabel = "{xlabel} index"
- elif isinstance(self, roi_items.VerticalLineROI):
- title = "Vertical slice"
- xLabel = "{ylabel} index"
- else:
- assert False
-
- # Use the axis names from the original plot
- profileManager = self.getProfileManager()
- plot = profileManager.getPlotWidget()
- xLabel = _relabelAxes(plot, xLabel)
-
- data = core.CurveProfileData(
- coords=numpy.arange(len(profile)),
- profile=profile,
- title=title,
- xLabel=xLabel,
- yLabel="Profile",
- )
- return data
-
-
-class ProfileScatterHorizontalSliceROI(roi_items.HorizontalLineROI,
- _DefaultScatterProfileSliceRoiMixIn):
- """ROI for an horizontal profile at a location of a scatter
- using data slicing.
- """
-
- ICON = 'slice-horizontal'
- NAME = 'horizontal data slice profile'
-
- def __init__(self, parent=None):
- roi_items.HorizontalLineROI.__init__(self, parent=parent)
- _DefaultScatterProfileSliceRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileScatterVerticalSliceROI(roi_items.VerticalLineROI,
- _DefaultScatterProfileSliceRoiMixIn):
- """ROI for a vertical profile at a location of a scatter
- using data slicing.
- """
-
- ICON = 'slice-vertical'
- NAME = 'vertical data slice profile'
-
- def __init__(self, parent=None):
- roi_items.VerticalLineROI.__init__(self, parent=parent)
- _DefaultScatterProfileSliceRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileScatterCrossSliceROI(_ProfileCrossROI):
- """ROI to manage a cross of slicing profiles on scatters.
- """
-
- ICON = 'slice-cross'
- NAME = 'cross data slice profile'
- ITEM_KIND = items.Scatter
-
- def _createLines(self, parent):
- vline = ProfileScatterVerticalSliceROI(parent=parent)
- hline = ProfileScatterHorizontalSliceROI(parent=parent)
- return hline, vline
-
-
-class _DefaultImageStackProfileRoiMixIn(_DefaultImageProfileRoiMixIn):
-
- ITEM_KIND = items.ImageStack
-
- def __init__(self, parent=None):
- super(_DefaultImageStackProfileRoiMixIn, self).__init__(parent=parent)
- self.__profileType = "1D"
- """Kind of profile"""
-
- def getProfileType(self):
- return self.__profileType
-
- def setProfileType(self, kind):
- assert kind in ["1D", "2D"]
- if self.__profileType == kind:
- return
- self.__profileType = kind
- self.invalidateProperties()
- self.invalidateProfile()
-
- def computeProfile(self, item):
- if not isinstance(item, items.ImageStack):
- raise TypeError("Unexpected class %s" % type(item))
-
- kind = self.getProfileType()
- if kind == "1D":
- result = _DefaultImageProfileRoiMixIn.computeProfile(self, item)
- # z = item.getStackPosition()
- return result
-
- assert kind == "2D"
-
- def createProfile2(currentData):
- coords, profile, _area, profileName, xLabel = core.createProfile(
- roiInfo=self._getRoiInfo(),
- currentData=currentData,
- origin=origin,
- scale=scale,
- lineWidth=self.getProfileLineWidth(),
- method=method)
- return coords, profile, profileName, xLabel
-
- currentData = numpy.array(item.getStackData(copy=False))
- origin = item.getOrigin()
- scale = item.getScale()
- colormap = item.getColormap()
- method = self.getProfileMethod()
-
- coords, profile, profileName, xLabel = createProfile2(currentData)
-
- data = core.ImageProfileData(
- coords=coords,
- profile=profile,
- title=profileName,
- xLabel=xLabel,
- yLabel="Profile",
- colormap=colormap,
- )
- return data
-
-
-class ProfileImageStackHorizontalLineROI(roi_items.HorizontalLineROI,
- _DefaultImageStackProfileRoiMixIn):
- """ROI for an horizontal profile at a location of a stack of images"""
-
- ICON = 'shape-horizontal'
- NAME = 'horizontal line profile'
-
- def __init__(self, parent=None):
- roi_items.HorizontalLineROI.__init__(self, parent=parent)
- _DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileImageStackVerticalLineROI(roi_items.VerticalLineROI,
- _DefaultImageStackProfileRoiMixIn):
- """ROI for an vertical profile at a location of a stack of images"""
-
- ICON = 'shape-vertical'
- NAME = 'vertical line profile'
-
- def __init__(self, parent=None):
- roi_items.VerticalLineROI.__init__(self, parent=parent)
- _DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileImageStackLineROI(roi_items.LineROI,
- _DefaultImageStackProfileRoiMixIn):
- """ROI for an vertical profile at a location of a stack of images"""
-
- ICON = 'shape-diagonal'
- NAME = 'line profile'
-
- def __init__(self, parent=None):
- roi_items.LineROI.__init__(self, parent=parent)
- _DefaultImageStackProfileRoiMixIn.__init__(self, parent=parent)
-
-
-class ProfileImageStackCrossROI(ProfileImageCrossROI):
- """ROI for an vertical profile at a location of a stack of images"""
-
- ICON = 'shape-cross'
- NAME = 'cross profile'
- ITEM_KIND = items.ImageStack
-
- def _createLines(self, parent):
- vline = ProfileImageStackVerticalLineROI(parent=parent)
- hline = ProfileImageStackHorizontalLineROI(parent=parent)
- return hline, vline
-
- def getProfileType(self):
- hline, _vline = self._getLines()
- return hline.getProfileType()
-
- def setProfileType(self, kind):
- hline, vline = self._getLines()
- hline.setProfileType(kind)
- vline.setProfileType(kind)
- self.invalidateProperties()
diff --git a/silx/gui/plot/tools/profile/toolbar.py b/silx/gui/plot/tools/profile/toolbar.py
deleted file mode 100644
index 4a9a195..0000000
--- a/silx/gui/plot/tools/profile/toolbar.py
+++ /dev/null
@@ -1,172 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2019 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 tool bar helper.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import logging
-import weakref
-
-from silx.gui import qt
-from silx.gui.widgets.MultiModeAction import MultiModeAction
-from . import manager
-from .. import roi as roi_mdl
-from silx.gui.plot import items
-
-
-_logger = logging.getLogger(__name__)
-
-
-class ProfileToolBar(qt.QToolBar):
- """Tool bar to provide profile for a plot.
-
- It is an helper class. For a dedicated application it would be better to
- use an own tool bar in order in order have more flexibility.
- """
- def __init__(self, parent=None, plot=None):
- super(ProfileToolBar, self).__init__(parent=parent)
- self.__scheme = None
- self.__manager = None
- self.__plot = weakref.ref(plot)
- self.__multiAction = None
-
- def getPlotWidget(self):
- """The :class:`~silx.gui.plot.PlotWidget` associated to the toolbar.
-
- :rtype: Union[~silx.gui.plot.PlotWidget,None]
- """
- if self.__plot is None:
- return None
- plot = self.__plot()
- if self.__plot is None:
- self.__plot = None
- return plot
-
- def setScheme(self, scheme):
- """Initialize the tool bar using a configuration scheme.
-
- It have to be done once and only once.
-
- :param str scheme: One of "scatter", "image", "imagestack"
- """
- assert self.__scheme is None
- self.__scheme = scheme
-
- plot = self.getPlotWidget()
- self.__manager = manager.ProfileManager(self, plot)
-
- if scheme == "image":
- self.__manager.setItemType(image=True)
- self.__manager.setActiveItemTracking(True)
-
- multiAction = MultiModeAction(self)
- self.addAction(multiAction)
- for action in self.__manager.createImageActions(self):
- multiAction.addAction(action)
- self.__multiAction = multiAction
-
- cleanAction = self.__manager.createClearAction(self)
- self.addAction(cleanAction)
- editorAction = self.__manager.createEditorAction(self)
- self.addAction(editorAction)
-
- plot.sigActiveImageChanged.connect(self._activeImageChanged)
- self._activeImageChanged()
-
- elif scheme == "scatter":
- self.__manager.setItemType(scatter=True)
- self.__manager.setActiveItemTracking(True)
-
- multiAction = MultiModeAction(self)
- self.addAction(multiAction)
- for action in self.__manager.createScatterActions(self):
- multiAction.addAction(action)
- for action in self.__manager.createScatterSliceActions(self):
- multiAction.addAction(action)
- self.__multiAction = multiAction
-
- cleanAction = self.__manager.createClearAction(self)
- self.addAction(cleanAction)
- editorAction = self.__manager.createEditorAction(self)
- self.addAction(editorAction)
-
- plot.sigActiveScatterChanged.connect(self._activeScatterChanged)
- self._activeScatterChanged()
-
- elif scheme == "imagestack":
- self.__manager.setItemType(image=True)
- self.__manager.setActiveItemTracking(True)
-
- multiAction = MultiModeAction(self)
- self.addAction(multiAction)
- for action in self.__manager.createImageStackActions(self):
- multiAction.addAction(action)
- self.__multiAction = multiAction
-
- cleanAction = self.__manager.createClearAction(self)
- self.addAction(cleanAction)
- editorAction = self.__manager.createEditorAction(self)
- self.addAction(editorAction)
-
- plot.sigActiveImageChanged.connect(self._activeImageChanged)
- self._activeImageChanged()
-
- else:
- raise ValueError("Toolbar scheme %s unsupported" % scheme)
-
- def _setRoiActionEnabled(self, itemKind, enabled):
- for action in self.__multiAction.getMenu().actions():
- if not isinstance(action, roi_mdl.CreateRoiModeAction):
- continue
- roiClass = action.getRoiClass()
- if issubclass(itemKind, roiClass.ITEM_KIND):
- action.setEnabled(enabled)
-
- def _activeImageChanged(self, previous=None, legend=None):
- """Handle active image change to toggle actions"""
- if legend is None:
- self._setRoiActionEnabled(items.ImageStack, False)
- self._setRoiActionEnabled(items.ImageBase, False)
- else:
- plot = self.getPlotWidget()
- image = plot.getActiveImage()
- # Disable for empty image
- enabled = image.getData(copy=False).size > 0
- self._setRoiActionEnabled(type(image), enabled)
-
- def _activeScatterChanged(self, previous=None, legend=None):
- """Handle active scatter change to toggle actions"""
- if legend is None:
- self._setRoiActionEnabled(items.Scatter, False)
- else:
- plot = self.getPlotWidget()
- scatter = plot.getActiveScatter()
- # Disable for empty image
- enabled = scatter.getValueData(copy=False).size > 0
- self._setRoiActionEnabled(type(scatter), enabled)
diff --git a/silx/gui/plot/tools/roi.py b/silx/gui/plot/tools/roi.py
deleted file mode 100644
index 4e2d6db..0000000
--- a/silx/gui/plot/tools/roi.py
+++ /dev/null
@@ -1,1417 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 ROI interaction for :class:`~silx.gui.plot.PlotWidget`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import enum
-import logging
-import time
-import weakref
-import functools
-
-import numpy
-
-from ... import qt, icons
-from ...utils import blockSignals
-from ...utils import LockReentrant
-from .. import PlotWidget
-from ..items import roi as roi_items
-
-from ...colors import rgba
-
-
-logger = logging.getLogger(__name__)
-
-
-class CreateRoiModeAction(qt.QAction):
- """
- This action is a plot mode which allows to create new ROIs using a ROI
- manager.
-
- A ROI is created using a specific `roiClass`. `initRoi` and `finalizeRoi`
- can be inherited to custom the ROI initialization.
-
- :param class roiClass: The ROI class which will be created by this action.
- :param qt.QObject parent: The action parent
- :param RegionOfInterestManager roiManager: The ROI manager
- """
-
- def __init__(self, parent, roiManager, roiClass):
- assert roiManager is not None
- assert roiClass is not None
- qt.QAction.__init__(self, parent=parent)
- self._roiManager = weakref.ref(roiManager)
- self._roiClass = roiClass
- self._singleShot = False
- self._initAction()
- self.triggered[bool].connect(self._actionTriggered)
-
- def _initAction(self):
- """Default initialization of the action"""
- roiClass = self._roiClass
-
- name = None
- iconName = None
- if hasattr(roiClass, "NAME"):
- name = roiClass.NAME
- if hasattr(roiClass, "ICON"):
- iconName = roiClass.ICON
-
- if iconName is None:
- iconName = "add-shape-unknown"
- if name is None:
- name = roiClass.__name__
- text = 'Add %s' % name
- self.setIcon(icons.getQIcon(iconName))
- self.setText(text)
- self.setCheckable(True)
- self.setToolTip(text)
-
- def getRoiClass(self):
- """Return the ROI class used by this action to create ROIs"""
- return self._roiClass
-
- def getRoiManager(self):
- return self._roiManager()
-
- def setSingleShot(self, singleShot):
- """Set it to True to deactivate the action after the first creation
- of a ROI.
-
- :param bool singleShot: New single short state
- """
- self._singleShot = singleShot
-
- def getSingleShot(self):
- """If True, after the first creation of a ROI with this mode,
- the mode is deactivated.
-
- :rtype: bool
- """
- return self._singleShot
-
- def _actionTriggered(self, checked):
- """Handle mode actions being checked by the user
-
- :param bool checked:
- :param str kind: Corresponding shape kind
- """
- roiManager = self.getRoiManager()
- if roiManager is None:
- return
-
- if checked:
- roiManager.start(self._roiClass, self)
- self.__interactiveModeStarted(roiManager)
- else:
- source = roiManager.getInteractionSource()
- if source is self:
- roiManager.stop()
-
- def __interactiveModeStarted(self, roiManager):
- roiManager.sigInteractiveRoiCreated.connect(self.initRoi)
- roiManager.sigInteractiveRoiFinalized.connect(self.__finalizeRoi)
- roiManager.sigInteractiveModeFinished.connect(self.__interactiveModeFinished)
-
- def __interactiveModeFinished(self):
- roiManager = self.getRoiManager()
- if roiManager is not None:
- roiManager.sigInteractiveRoiCreated.disconnect(self.initRoi)
- roiManager.sigInteractiveRoiFinalized.disconnect(self.__finalizeRoi)
- roiManager.sigInteractiveModeFinished.disconnect(self.__interactiveModeFinished)
- self.setChecked(False)
-
- def initRoi(self, roi):
- """Inherit it to custom the new ROI at it's creation during the
- interaction."""
- pass
-
- def __finalizeRoi(self, roi):
- self.finalizeRoi(roi)
- if self._singleShot:
- roiManager = self.getRoiManager()
- if roiManager is not None:
- roiManager.stop()
-
- def finalizeRoi(self, roi):
- """Inherit it to custom the new ROI after it's creation when the
- interaction is finalized."""
- pass
-
-
-class RoiModeSelector(qt.QWidget):
- def __init__(self, parent=None):
- super(RoiModeSelector, self).__init__(parent=parent)
- self.__roi = None
- self.__reentrant = LockReentrant()
-
- layout = qt.QHBoxLayout(self)
- if isinstance(parent, qt.QMenu):
- margins = layout.contentsMargins()
- layout.setContentsMargins(margins.left(), 0, margins.right(), 0)
- else:
- layout.setContentsMargins(0, 0, 0, 0)
-
- self._label = qt.QLabel(self)
- self._label.setText("Mode:")
- self._label.setToolTip("Select a specific interaction to edit the ROI")
- self._combo = qt.QComboBox(self)
- self._combo.currentIndexChanged.connect(self._modeSelected)
- layout.addWidget(self._label)
- layout.addWidget(self._combo)
- self._updateAvailableModes()
-
- def getRoi(self):
- """Returns the edited ROI.
-
- :rtype: roi_items.RegionOfInterest
- """
- return self.__roi
-
- def setRoi(self, roi):
- """Returns the edited ROI.
-
- :rtype: roi_items.RegionOfInterest
- """
- if self.__roi is roi:
- return
- if not isinstance(roi, roi_items.InteractionModeMixIn):
- self.__roi = None
- self._updateAvailableModes()
- return
-
- if self.__roi is not None:
- self.__roi.sigInteractionModeChanged.disconnect(self._modeChanged)
- self.__roi = roi
- if self.__roi is not None:
- self.__roi.sigInteractionModeChanged.connect(self._modeChanged)
- self._updateAvailableModes()
-
- def isEmpty(self):
- return not self._label.isVisibleTo(self)
-
- def _updateAvailableModes(self):
- roi = self.getRoi()
- if isinstance(roi, roi_items.InteractionModeMixIn):
- modes = roi.availableInteractionModes()
- else:
- modes = []
- if len(modes) <= 1:
- self._label.setVisible(False)
- self._combo.setVisible(False)
- else:
- self._label.setVisible(True)
- self._combo.setVisible(True)
- with blockSignals(self._combo):
- self._combo.clear()
- for im, m in enumerate(modes):
- self._combo.addItem(m.label, m)
- self._combo.setItemData(im, m.description, qt.Qt.ToolTipRole)
- mode = roi.getInteractionMode()
- self._modeChanged(mode)
- index = modes.index(mode)
- self._combo.setCurrentIndex(index)
-
- def _modeChanged(self, mode):
- """Triggered when the ROI interaction mode was changed externally"""
- if self.__reentrant.locked():
- # This event was initialised by the widget
- return
- roi = self.__roi
- modes = roi.availableInteractionModes()
- index = modes.index(mode)
- with blockSignals(self._combo):
- self._combo.setCurrentIndex(index)
-
- def _modeSelected(self):
- """Triggered when the ROI interaction mode was selected in the widget"""
- index = self._combo.currentIndex()
- if index == -1:
- return
- roi = self.getRoi()
- if roi is not None:
- mode = self._combo.itemData(index, qt.Qt.UserRole)
- with self.__reentrant:
- roi.setInteractionMode(mode)
-
-
-class RoiModeSelectorAction(qt.QWidgetAction):
- """Display the selected mode of a ROI and allow to change it"""
-
- def __init__(self, parent=None):
- super(RoiModeSelectorAction, self).__init__(parent)
- self.__roiManager = None
-
- def createWidget(self, parent):
- """Inherit the method to create a new widget"""
- widget = RoiModeSelector(parent)
- manager = self.__roiManager
- if manager is not None:
- roi = manager.getCurrentRoi()
- widget.setRoi(roi)
- self.setVisible(not widget.isEmpty())
- return widget
-
- def deleteWidget(self, widget):
- """Inherit the method to delete a widget"""
- widget.setRoi(None)
- return qt.QWidgetAction.deleteWidget(self, widget)
-
- def setRoiManager(self, roiManager):
- """
- Connect this action to a ROI manager.
-
- :param RegionOfInterestManager roiManager: A ROI manager
- """
- if self.__roiManager is roiManager:
- return
- if self.__roiManager is not None:
- self.__roiManager.sigCurrentRoiChanged.disconnect(self.__currentRoiChanged)
- self.__roiManager = roiManager
- if self.__roiManager is not None:
- self.__roiManager.sigCurrentRoiChanged.connect(self.__currentRoiChanged)
- self.__currentRoiChanged(roiManager.getCurrentRoi())
-
- def __currentRoiChanged(self, roi):
- """Handle changes of the selected ROI"""
- self.setRoi(roi)
-
- def setRoi(self, roi):
- """Set a profile ROI to edit.
-
- :param ProfileRoiMixIn roi: A profile ROI
- """
- widget = None
- for widget in self.createdWidgets():
- widget.setRoi(roi)
- if widget is not None:
- self.setVisible(not widget.isEmpty())
-
-
-class RegionOfInterestManager(qt.QObject):
- """Class handling ROI interaction on a PlotWidget.
-
- It supports the multiple ROIs: points, rectangles, polygons,
- lines, horizontal and vertical lines.
-
- See ``plotInteractiveImageROI.py`` sample code (:ref:`sample-code`).
-
- :param silx.gui.plot.PlotWidget parent:
- The plot widget in which to control the ROIs.
- """
-
- sigRoiAdded = qt.Signal(roi_items.RegionOfInterest)
- """Signal emitted when a new ROI has been added.
-
- It provides the newly add :class:`RegionOfInterest` object.
- """
-
- sigRoiAboutToBeRemoved = qt.Signal(roi_items.RegionOfInterest)
- """Signal emitted just before a ROI is removed.
-
- It provides the :class:`RegionOfInterest` object that is about to be removed.
- """
-
- sigRoiChanged = qt.Signal()
- """Signal emitted whenever the ROIs have changed."""
-
- sigCurrentRoiChanged = qt.Signal(object)
- """Signal emitted whenever a ROI is selected."""
-
- sigInteractiveModeStarted = qt.Signal(object)
- """Signal emitted when switching to ROI drawing interactive mode.
-
- It provides the class of the ROI which will be created by the interactive
- mode.
- """
-
- sigInteractiveRoiCreated = qt.Signal(object)
- """Signal emitted when a ROI is created during the interaction.
- The interaction is still incomplete and can be aborted.
-
- It provides the ROI object which was just been created.
- """
-
- sigInteractiveRoiFinalized = qt.Signal(object)
- """Signal emitted when a ROI creation is complet.
-
- It provides the ROI object which was just been created.
- """
-
- sigInteractiveModeFinished = qt.Signal()
- """Signal emitted when leaving interactive ROI drawing mode.
- """
-
- ROI_CLASSES = (
- roi_items.PointROI,
- roi_items.CrossROI,
- roi_items.RectangleROI,
- roi_items.CircleROI,
- roi_items.EllipseROI,
- roi_items.PolygonROI,
- roi_items.LineROI,
- roi_items.HorizontalLineROI,
- roi_items.VerticalLineROI,
- roi_items.ArcROI,
- roi_items.HorizontalRangeROI,
- )
-
- def __init__(self, parent):
- assert isinstance(parent, PlotWidget)
- super(RegionOfInterestManager, self).__init__(parent)
- self._rois = [] # List of ROIs
- self._drawnROI = None # New ROI being currently drawn
-
- self._roiClass = None
- self._source = None
- self._color = rgba('red')
-
- self._label = "__RegionOfInterestManager__%d" % id(self)
-
- self._currentRoi = None
- """Hold currently selected ROI"""
-
- self._eventLoop = None
-
- self._modeActions = {}
-
- parent.sigPlotSignal.connect(self._plotSignals)
-
- parent.sigInteractiveModeChanged.connect(
- self._plotInteractiveModeChanged)
-
- parent.sigItemRemoved.connect(self._itemRemoved)
-
- parent._sigDefaultContextMenu.connect(self._feedContextMenu)
-
- @classmethod
- def getSupportedRoiClasses(cls):
- """Returns the default available ROI classes
-
- :rtype: List[class]
- """
- return tuple(cls.ROI_CLASSES)
-
- # Associated QActions
-
- def getInteractionModeAction(self, roiClass):
- """Returns the QAction corresponding to a kind of ROI
-
- The QAction allows to enable the corresponding drawing
- interactive mode.
-
- :param class roiClass: The ROI class which will be created by this action.
- :rtype: QAction
- :raise ValueError: If kind is not supported
- """
- if not issubclass(roiClass, roi_items.RegionOfInterest):
- raise ValueError('Unsupported ROI class %s' % roiClass)
-
- action = self._modeActions.get(roiClass, None)
- if action is None: # Lazy-loading
- action = CreateRoiModeAction(self, self, roiClass)
- self._modeActions[roiClass] = action
- return action
-
- # PlotWidget eventFilter and listeners
-
- def _plotInteractiveModeChanged(self, source):
- """Handle change of interactive mode in the plot"""
- if source is not self:
- self.__roiInteractiveModeEnded()
-
- def _getRoiFromItem(self, item):
- """Returns the ROI which own this item, else None
- if this manager do not have knowledge of this ROI."""
- for roi in self._rois:
- if isinstance(roi, roi_items.RegionOfInterest):
- for child in roi.getItems():
- if child is item:
- return roi
- return None
-
- def _itemRemoved(self, item):
- """Called after an item was removed from the plot."""
- if not hasattr(item, "_roiGroup"):
- # Early break to avoid to use _getRoiFromItem
- # And to avoid reentrant signal when the ROI remove the item itself
- return
- roi = self._getRoiFromItem(item)
- if roi is not None:
- self.removeRoi(roi)
-
- # Handle ROI interaction
-
- def _handleInteraction(self, event):
- """Handle mouse interaction for ROI addition"""
- roiClass = self.getCurrentInteractionModeRoiClass()
- if roiClass is None:
- return # Should not happen
-
- kind = roiClass.getFirstInteractionShape()
- if kind == 'point':
- if event['event'] == 'mouseClicked' and event['button'] == 'left':
- points = numpy.array([(event['x'], event['y'])],
- dtype=numpy.float64)
- # Not an interactive creation
- roi = self._createInteractiveRoi(roiClass, points=points)
- roi.creationFinalized()
- self.sigInteractiveRoiFinalized.emit(roi)
- else: # other shapes
- if (event['event'] in ('drawingProgress', 'drawingFinished') and
- event['parameters']['label'] == self._label):
- points = numpy.array((event['xdata'], event['ydata']),
- dtype=numpy.float64).T
-
- if self._drawnROI is None: # Create new ROI
- # NOTE: Set something before createRoi, so isDrawing is True
- self._drawnROI = object()
- self._drawnROI = self._createInteractiveRoi(roiClass, points=points)
- else:
- self._drawnROI.setFirstShapePoints(points)
-
- if event['event'] == 'drawingFinished':
- if kind == 'polygon' and len(points) > 1:
- self._drawnROI.setFirstShapePoints(points[:-1])
- roi = self._drawnROI
- self._drawnROI = None # Stop drawing
- roi.creationFinalized()
- self.sigInteractiveRoiFinalized.emit(roi)
-
- # RegionOfInterest selection
-
- def __getRoiFromMarker(self, marker):
- """Returns a ROI from a marker, else None"""
- # This should be speed up
- for roi in self._rois:
- if isinstance(roi, roi_items.HandleBasedROI):
- for m in roi.getHandles():
- if m is marker:
- return roi
- else:
- for m in roi.getItems():
- if m is marker:
- return roi
- return None
-
- def setCurrentRoi(self, roi):
- """Set the currently selected ROI, and emit a signal.
-
- :param Union[RegionOfInterest,None] roi: The ROI to select
- """
- if self._currentRoi is roi:
- return
- if roi is not None:
- # Note: Fixed range to avoid infinite loops
- for _ in range(10):
- target = roi.getFocusProxy()
- if target is None:
- break
- roi = target
- else:
- raise RuntimeError("Max selection proxy depth (10) reached.")
-
- if self._currentRoi is not None:
- self._currentRoi.setHighlighted(False)
- self._currentRoi = roi
- if self._currentRoi is not None:
- self._currentRoi.setHighlighted(True)
- self.sigCurrentRoiChanged.emit(roi)
-
- def getCurrentRoi(self):
- """Returns the currently selected ROI, else None.
-
- :rtype: Union[RegionOfInterest,None]
- """
- return self._currentRoi
-
- def _plotSignals(self, event):
- """Handle mouse interaction for ROI addition"""
- clicked = False
- roi = None
- if event["event"] in ("markerClicked", "markerMoving"):
- plot = self.parent()
- legend = event["label"]
- marker = plot._getMarker(legend=legend)
- roi = self.__getRoiFromMarker(marker)
- elif event["event"] == "mouseClicked" and event["button"] == "left":
- # Marker click is only for dnd
- # This also can click on a marker
- clicked = True
- plot = self.parent()
- marker = plot._getMarkerAt(event["xpixel"], event["ypixel"])
- roi = self.__getRoiFromMarker(marker)
- else:
- return
-
- if roi not in self._rois:
- # The ROI is not own by this manager
- return
-
- if roi is not None:
- currentRoi = self.getCurrentRoi()
- if currentRoi is roi:
- if clicked:
- self.__updateMode(roi)
- elif roi.isSelectable():
- self.setCurrentRoi(roi)
- else:
- self.setCurrentRoi(None)
-
- def __updateMode(self, roi):
- if isinstance(roi, roi_items.InteractionModeMixIn):
- available = roi.availableInteractionModes()
- mode = roi.getInteractionMode()
- imode = available.index(mode)
- mode = available[(imode + 1) % len(available)]
- roi.setInteractionMode(mode)
-
- def _feedContextMenu(self, menu):
- """Called wen the default plot context menu is about to be displayed"""
- roi = self.getCurrentRoi()
- if roi is not None:
- if roi.isEditable():
- # Filter by data position
- # FIXME: It would be better to use GUI coords for it
- plot = self.parent()
- pos = plot.getWidgetHandle().mapFromGlobal(qt.QCursor.pos())
- data = plot.pixelToData(pos.x(), pos.y())
- if roi.contains(data):
- if isinstance(roi, roi_items.InteractionModeMixIn):
- self._contextMenuForInteractionMode(menu, roi)
-
- removeAction = qt.QAction(menu)
- removeAction.setText("Remove %s" % roi.getName())
- callback = functools.partial(self.removeRoi, roi)
- removeAction.triggered.connect(callback)
- menu.addAction(removeAction)
-
- def _contextMenuForInteractionMode(self, menu, roi):
- availableModes = roi.availableInteractionModes()
- currentMode = roi.getInteractionMode()
- submenu = qt.QMenu(menu)
- modeGroup = qt.QActionGroup(menu)
- modeGroup.setExclusive(True)
- for mode in availableModes:
- action = qt.QAction(menu)
- action.setText(mode.label)
- action.setToolTip(mode.description)
- action.setCheckable(True)
- if mode is currentMode:
- action.setChecked(True)
- else:
- callback = functools.partial(roi.setInteractionMode, mode)
- action.triggered.connect(callback)
- modeGroup.addAction(action)
- submenu.addAction(action)
- action = qt.QAction(menu)
- action.setMenu(submenu)
- action.setText("%s interaction mode" % roi.getName())
- menu.addAction(action)
-
- # RegionOfInterest API
-
- def getRois(self):
- """Returns the list of ROIs.
-
- It returns an empty tuple if there is currently no ROI.
-
- :return: Tuple of arrays of objects describing the ROIs
- :rtype: List[RegionOfInterest]
- """
- return tuple(self._rois)
-
- def clear(self):
- """Reset current ROIs
-
- :return: True if ROIs were reset.
- :rtype: bool
- """
- if self.getRois(): # Something to reset
- for roi in self._rois:
- roi.sigRegionChanged.disconnect(
- self._regionOfInterestChanged)
- roi.setParent(None)
- self._rois = []
- self._roisUpdated()
- return True
-
- else:
- return False
-
- def _regionOfInterestChanged(self, event=None):
- """Handle ROI object changed"""
- self.sigRoiChanged.emit()
-
- def _createInteractiveRoi(self, roiClass, points, label=None, index=None):
- """Create a new ROI with interactive creation.
-
- :param class roiClass: The class of the ROI to create
- :param numpy.ndarray points: The first shape used to create the ROI
- :param str label: The label to display along with the ROI.
- :param int index: The position where to insert the ROI.
- By default it is appended to the end of the list.
- :return: The created ROI object
- :rtype: roi_items.RegionOfInterest
- :raise RuntimeError: When ROI cannot be added because the maximum
- number of ROIs has been reached.
- """
- roi = roiClass(parent=None)
- if label is not None:
- roi.setName(str(label))
- roi.creationStarted()
- roi.setFirstShapePoints(points)
-
- self.addRoi(roi, index)
- if roi.isSelectable():
- self.setCurrentRoi(roi)
- self.sigInteractiveRoiCreated.emit(roi)
- return roi
-
- def containsRoi(self, roi):
- """Returns true if the ROI is part of this manager.
-
- :param roi_items.RegionOfInterest roi: The ROI to add
- :rtype: bool
- """
- return roi in self._rois
-
- def addRoi(self, roi, index=None, useManagerColor=True):
- """Add the ROI to the list of ROIs.
-
- :param roi_items.RegionOfInterest roi: The ROI to add
- :param int index: The position where to insert the ROI,
- By default it is appended to the end of the list of ROIs
- :param bool useManagerColor:
- Whether to set the ROI color to the default one of the manager or not.
- (Default: True).
- :raise RuntimeError: When ROI cannot be added because the maximum
- number of ROIs has been reached.
- """
- plot = self.parent()
- if plot is None:
- raise RuntimeError(
- 'Cannot add ROI: PlotWidget no more available')
-
- roi.setParent(self)
-
- if useManagerColor:
- roi.setColor(self.getColor())
-
- roi.sigRegionChanged.connect(self._regionOfInterestChanged)
- roi.sigItemChanged.connect(self._regionOfInterestChanged)
-
- if index is None:
- self._rois.append(roi)
- else:
- self._rois.insert(index, roi)
- self.sigRoiAdded.emit(roi)
- self._roisUpdated()
-
- def removeRoi(self, roi):
- """Remove a ROI from the list of ROIs.
-
- :param roi_items.RegionOfInterest roi: The ROI to remove
- :raise ValueError: When ROI does not belong to this object
- """
- if not (isinstance(roi, roi_items.RegionOfInterest) and
- roi.parent() is self and
- roi in self._rois):
- raise ValueError(
- 'RegionOfInterest does not belong to this instance')
-
- roi.sigAboutToBeRemoved.emit()
- self.sigRoiAboutToBeRemoved.emit(roi)
-
- if roi is self._currentRoi:
- self.setCurrentRoi(None)
-
- mustRestart = False
- if roi is self._drawnROI:
- self._drawnROI = None
- mustRestart = True
- self._rois.remove(roi)
- roi.sigRegionChanged.disconnect(self._regionOfInterestChanged)
- roi.sigItemChanged.disconnect(self._regionOfInterestChanged)
- roi.setParent(None)
- self._roisUpdated()
-
- if mustRestart:
- self._restart()
-
- def _roisUpdated(self):
- """Handle update of the ROI list"""
- self.sigRoiChanged.emit()
-
- # RegionOfInterest parameters
-
- def getColor(self):
- """Return the default color of created ROIs
-
- :rtype: QColor
- """
- return qt.QColor.fromRgbF(*self._color)
-
- def setColor(self, color):
- """Set the default color to use when creating ROIs.
-
- Existing ROIs are not affected.
-
- :param color: The color to use for displaying ROIs as
- either a color name, a QColor, a list of uint8 or float in [0, 1].
- """
- self._color = rgba(color)
-
- # Control ROI
-
- def getCurrentInteractionModeRoiClass(self):
- """Returns the current ROI class used by the interactive drawing mode.
-
- Returns None if the ROI manager is not in an interactive mode.
-
- :rtype: Union[class,None]
- """
- return self._roiClass
-
- def getInteractionSource(self):
- """Returns the object which have requested the ROI creation.
-
- Returns None if the ROI manager is not in an interactive mode.
-
- :rtype: Union[object,None]
- """
- return self._source
-
- def isStarted(self):
- """Returns True if an interactive ROI drawing mode is active.
-
- :rtype: bool
- """
- return self._roiClass is not None
-
- def isDrawing(self):
- """Returns True if an interactive ROI is drawing.
-
- :rtype: bool
- """
- return self._drawnROI is not None
-
- def start(self, roiClass, source=None):
- """Start an interactive ROI drawing mode.
-
- :param class roiClass: The ROI class to create. It have to inherite from
- `roi_items.RegionOfInterest`.
- :param object source: SOurce of the ROI interaction.
- :return: True if interactive ROI drawing was started, False otherwise
- :rtype: bool
- :raise ValueError: If roiClass is not supported
- """
- self.stop()
-
- if not issubclass(roiClass, roi_items.RegionOfInterest):
- raise ValueError('Unsupported ROI class %s' % roiClass)
-
- plot = self.parent()
- if plot is None:
- return False
-
- self._roiClass = roiClass
- self._source = source
-
- self._restart()
-
- plot.sigPlotSignal.connect(self._handleInteraction)
-
- self.sigInteractiveModeStarted.emit(roiClass)
-
- return True
-
- def _restart(self):
- """Restart the plot interaction without changing the
- source or the ROI class.
- """
- roiClass = self._roiClass
- plot = self.parent()
- firstInteractionShapeKind = roiClass.getFirstInteractionShape()
-
- if firstInteractionShapeKind == 'point':
- plot.setInteractiveMode(mode='select', source=self)
- else:
- if roiClass.showFirstInteractionShape():
- color = rgba(self.getColor())
- else:
- color = None
- plot.setInteractiveMode(mode='select-draw',
- source=self,
- shape=firstInteractionShapeKind,
- color=color,
- label=self._label)
-
- def __roiInteractiveModeEnded(self):
- """Handle end of ROI draw interactive mode"""
- if self.isStarted():
- self._roiClass = None
- self._source = None
-
- if self._drawnROI is not None:
- # Cancel ROI create
- roi = self._drawnROI
- self._drawnROI = None
- self.removeRoi(roi)
-
- plot = self.parent()
- if plot is not None:
- plot.sigPlotSignal.disconnect(self._handleInteraction)
-
- self.sigInteractiveModeFinished.emit()
-
- def stop(self):
- """Stop interactive ROI drawing mode.
-
- :return: True if an interactive ROI drawing mode was actually stopped
- :rtype: bool
- """
- if not self.isStarted():
- return False
-
- plot = self.parent()
- if plot is not None:
- # This leads to call __roiInteractiveModeEnded through
- # interactive mode changed signal
- plot.resetInteractiveMode()
- else: # Fallback
- self.__roiInteractiveModeEnded()
-
- return True
-
- def exec_(self, roiClass):
- """Block until :meth:`quit` is called.
-
- :param class kind: The class of the ROI which have to be created.
- See `silx.gui.plot.items.roi`.
- :return: The list of ROIs
- :rtype: tuple
- """
- self.start(roiClass)
-
- plot = self.parent()
- plot.show()
- plot.raise_()
-
- self._eventLoop = qt.QEventLoop()
- self._eventLoop.exec_()
- self._eventLoop = None
-
- self.stop()
-
- rois = self.getRois()
- self.clear()
- return rois
-
- def quit(self):
- """Stop a blocking :meth:`exec_` and call :meth:`stop`"""
- if self._eventLoop is not None:
- self._eventLoop.quit()
- self._eventLoop = None
- self.stop()
-
-
-class InteractiveRegionOfInterestManager(RegionOfInterestManager):
- """RegionOfInterestManager with features for use from interpreter.
-
- It is meant to be used through the :meth:`exec_`.
- It provides some messages to display in a status bar and
- different modes to end blocking calls to :meth:`exec_`.
-
- :param parent: See QObject
- """
-
- sigMessageChanged = qt.Signal(str)
- """Signal emitted when a new message should be displayed to the user
-
- It provides the message as a str.
- """
-
- def __init__(self, parent):
- super(InteractiveRegionOfInterestManager, self).__init__(parent)
- self._maxROI = None
- self.__timeoutEndTime = None
- self.__message = ''
- self.__validationMode = self.ValidationMode.ENTER
- self.__execClass = None
-
- self.sigRoiAdded.connect(self.__added)
- self.sigRoiAboutToBeRemoved.connect(self.__aboutToBeRemoved)
- self.sigInteractiveModeStarted.connect(self.__started)
- self.sigInteractiveModeFinished.connect(self.__finished)
-
- # Max ROI
-
- def getMaxRois(self):
- """Returns the maximum number of ROIs or None if no limit.
-
- :rtype: Union[int,None]
- """
- return self._maxROI
-
- def setMaxRois(self, max_):
- """Set the maximum number of ROIs.
-
- :param Union[int,None] max_: The max limit or None for no limit.
- :raise ValueError: If there is more ROIs than max value
- """
- if max_ is not None:
- max_ = int(max_)
- if max_ <= 0:
- raise ValueError('Max limit must be strictly positive')
-
- if len(self.getRois()) > max_:
- raise ValueError(
- 'Cannot set max limit: Already too many ROIs')
-
- self._maxROI = max_
-
- def isMaxRois(self):
- """Returns True if the maximum number of ROIs is reached.
-
- :rtype: bool
- """
- max_ = self.getMaxRois()
- return max_ is not None and len(self.getRois()) >= max_
-
- # Validation mode
-
- @enum.unique
- class ValidationMode(enum.Enum):
- """Mode of validation to leave blocking :meth:`exec_`"""
-
- AUTO = 'auto'
- """Automatically ends the interactive mode once
- the user terminates the last ROI shape."""
-
- ENTER = 'enter'
- """Ends the interactive mode when the *Enter* key is pressed."""
-
- AUTO_ENTER = 'auto_enter'
- """Ends the interactive mode when reaching max ROIs or
- when the *Enter* key is pressed.
- """
-
- NONE = 'none'
- """Do not provide the user a way to end the interactive mode.
-
- The end of :meth:`exec_` is done through :meth:`quit` or timeout.
- """
-
- def getValidationMode(self):
- """Returns the interactive mode validation in use.
-
- :rtype: ValidationMode
- """
- return self.__validationMode
-
- def setValidationMode(self, mode):
- """Set the way to perform interactive mode validation.
-
- See :class:`ValidationMode` enumeration for the supported
- validation modes.
-
- :param ValidationMode mode: The interactive mode validation to use.
- """
- assert isinstance(mode, self.ValidationMode)
- if mode != self.__validationMode:
- self.__validationMode = mode
-
- if self.isExec():
- if (self.isMaxRois() and self.getValidationMode() in
- (self.ValidationMode.AUTO,
- self.ValidationMode.AUTO_ENTER)):
- self.quit()
-
- self.__updateMessage()
-
- def eventFilter(self, obj, event):
- if event.type() == qt.QEvent.Hide:
- self.quit()
-
- if event.type() == qt.QEvent.KeyPress:
- key = event.key()
- if (key in (qt.Qt.Key_Return, qt.Qt.Key_Enter) and
- self.getValidationMode() in (
- self.ValidationMode.ENTER,
- self.ValidationMode.AUTO_ENTER)):
- # Stop on return key pressed
- self.quit()
- return True # Stop further handling of this keys
-
- if (key in (qt.Qt.Key_Delete, qt.Qt.Key_Backspace) or (
- key == qt.Qt.Key_Z and
- event.modifiers() & qt.Qt.ControlModifier)):
- rois = self.getRois()
- if rois: # Something to undo
- self.removeRoi(rois[-1])
- # Stop further handling of keys if something was undone
- return True
-
- return super(InteractiveRegionOfInterestManager, self).eventFilter(obj, event)
-
- # Message API
-
- def getMessage(self):
- """Returns the current status message.
-
- This message is meant to be displayed in a status bar.
-
- :rtype: str
- """
- if self.__timeoutEndTime is None:
- return self.__message
- else:
- remaining = self.__timeoutEndTime - time.time()
- return self.__message + (' - %d seconds remaining' %
- max(1, int(remaining)))
-
- # Listen to ROI updates
-
- def __added(self, *args, **kwargs):
- """Handle new ROI added"""
- max_ = self.getMaxRois()
- if max_ is not None:
- # When reaching max number of ROIs, redo last one
- while len(self.getRois()) > max_:
- self.removeRoi(self.getRois()[-2])
-
- self.__updateMessage()
- if (self.isMaxRois() and
- self.getValidationMode() in (self.ValidationMode.AUTO,
- self.ValidationMode.AUTO_ENTER)):
- self.quit()
-
- def __aboutToBeRemoved(self, *args, **kwargs):
- """Handle removal of a ROI"""
- # RegionOfInterest not removed yet
- self.__updateMessage(nbrois=len(self.getRois()) - 1)
-
- def __started(self, roiKind):
- """Handle interactive mode started"""
- self.__updateMessage()
-
- def __finished(self):
- """Handle interactive mode finished"""
- self.__updateMessage()
-
- def __updateMessage(self, nbrois=None):
- """Update message"""
- if not self.isExec():
- message = 'Done'
-
- elif not self.isStarted():
- message = 'Use %s ROI edition mode' % self.__execClass
-
- else:
- if nbrois is None:
- nbrois = len(self.getRois())
-
- name = self.__execClass._getShortName()
-
- max_ = self.getMaxRois()
- if max_ is None:
- message = 'Select %ss (%d selected)' % (name, nbrois)
-
- elif max_ <= 1:
- message = 'Select a %s' % name
- else:
- message = 'Select %d/%d %ss' % (nbrois, max_, name)
-
- if (self.getValidationMode() == self.ValidationMode.ENTER and
- self.isMaxRois()):
- message += ' - Press Enter to confirm'
-
- if message != self.__message:
- self.__message = message
- # Use getMessage to add timeout message
- self.sigMessageChanged.emit(self.getMessage())
-
- # Handle blocking call
-
- def __timeoutUpdate(self):
- """Handle update of timeout"""
- if (self.__timeoutEndTime is not None and
- (self.__timeoutEndTime - time.time()) > 0):
- self.sigMessageChanged.emit(self.getMessage())
- else: # Stop interactive mode and message timer
- timer = self.sender()
- if timer is not None:
- timer.stop()
- self.__timeoutEndTime = None
- self.quit()
-
- def isExec(self):
- """Returns True if :meth:`exec_` is currently running.
-
- :rtype: bool"""
- return self.__execClass is not None
-
- def exec_(self, roiClass, timeout=0):
- """Block until ROI selection is done or timeout is elapsed.
-
- :meth:`quit` also ends this blocking call.
-
- :param class roiClass: The class of the ROI which have to be created.
- See `silx.gui.plot.items.roi`.
- :param int timeout: Maximum duration in seconds to block.
- Default: No timeout
- :return: The list of ROIs
- :rtype: List[RegionOfInterest]
- """
- plot = self.parent()
- if plot is None:
- return
-
- self.__execClass = roiClass
-
- plot.installEventFilter(self)
-
- if timeout > 0:
- self.__timeoutEndTime = time.time() + timeout
- timer = qt.QTimer(self)
- timer.timeout.connect(self.__timeoutUpdate)
- timer.start(1000)
-
- rois = super(InteractiveRegionOfInterestManager, self).exec_(roiClass)
-
- timer.stop()
- self.__timeoutEndTime = None
-
- else:
- rois = super(InteractiveRegionOfInterestManager, self).exec_(roiClass)
-
- plot.removeEventFilter(self)
-
- self.__execClass = None
- self.__updateMessage()
-
- return rois
-
-
-class _DeleteRegionOfInterestToolButton(qt.QToolButton):
- """Tool button deleting a ROI object
-
- :param parent: See QWidget
- :param RegionOfInterest roi: The ROI to delete
- """
-
- def __init__(self, parent, roi):
- super(_DeleteRegionOfInterestToolButton, self).__init__(parent)
- self.setIcon(icons.getQIcon('remove'))
- self.setToolTip("Remove this ROI")
- self.__roiRef = roi if roi is None else weakref.ref(roi)
- self.clicked.connect(self.__clicked)
-
- def __clicked(self, checked):
- """Handle button clicked"""
- roi = None if self.__roiRef is None else self.__roiRef()
- if roi is not None:
- manager = roi.parent()
- if manager is not None:
- manager.removeRoi(roi)
- self.__roiRef = None
-
-
-class RegionOfInterestTableWidget(qt.QTableWidget):
- """Widget displaying the ROIs of a :class:`RegionOfInterestManager`"""
-
- def __init__(self, parent=None):
- super(RegionOfInterestTableWidget, self).__init__(parent)
- self._roiManagerRef = None
-
- headers = ['Label', 'Edit', 'Kind', 'Coordinates', '']
- self.setColumnCount(len(headers))
- self.setHorizontalHeaderLabels(headers)
-
- horizontalHeader = self.horizontalHeader()
- horizontalHeader.setDefaultAlignment(qt.Qt.AlignLeft)
- if hasattr(horizontalHeader, 'setResizeMode'): # Qt 4
- setSectionResizeMode = horizontalHeader.setResizeMode
- else: # Qt5
- setSectionResizeMode = horizontalHeader.setSectionResizeMode
-
- setSectionResizeMode(0, qt.QHeaderView.Interactive)
- setSectionResizeMode(1, qt.QHeaderView.ResizeToContents)
- setSectionResizeMode(2, qt.QHeaderView.ResizeToContents)
- setSectionResizeMode(3, qt.QHeaderView.Stretch)
- setSectionResizeMode(4, qt.QHeaderView.ResizeToContents)
-
- verticalHeader = self.verticalHeader()
- verticalHeader.setVisible(False)
-
- self.setSelectionMode(qt.QAbstractItemView.NoSelection)
- self.setFocusPolicy(qt.Qt.NoFocus)
-
- self.itemChanged.connect(self.__itemChanged)
-
- def __itemChanged(self, item):
- """Handle item updates"""
- column = item.column()
- index = item.data(qt.Qt.UserRole)
-
- if index is not None:
- manager = self.getRegionOfInterestManager()
- roi = manager.getRois()[index]
- else:
- return
-
- if column == 0:
- # First collect information from item, then update ROI
- # Otherwise, this causes issues issues
- checked = item.checkState() == qt.Qt.Checked
- text= item.text()
- roi.setVisible(checked)
- roi.setName(text)
- elif column == 1:
- roi.setEditable(item.checkState() == qt.Qt.Checked)
- elif column in (2, 3, 4):
- pass # TODO
- else:
- logger.error('Unhandled column %d', column)
-
- def setRegionOfInterestManager(self, manager):
- """Set the :class:`RegionOfInterestManager` object to sync with
-
- :param RegionOfInterestManager manager:
- """
- assert manager is None or isinstance(manager, RegionOfInterestManager)
-
- previousManager = self.getRegionOfInterestManager()
-
- if previousManager is not None:
- previousManager.sigRoiChanged.disconnect(self._sync)
- self.setRowCount(0)
-
- self._roiManagerRef = weakref.ref(manager)
-
- self._sync()
-
- if manager is not None:
- manager.sigRoiChanged.connect(self._sync)
-
- def _getReadableRoiDescription(self, roi):
- """Returns modelisation of a ROI as a readable sequence of values.
-
- :rtype: str
- """
- text = str(roi)
- try:
- # Extract the params from syntax "CLASSNAME(PARAMS)"
- elements = text.split("(", 1)
- if len(elements) != 2:
- return text
- result = elements[1]
- result = result.strip()
- if not result.endswith(")"):
- return text
- result = result[0:-1]
- # Capitalize each words
- result = result.title()
- return result
- except Exception:
- logger.debug("Backtrace", exc_info=True)
- return text
-
- def _sync(self):
- """Update widget content according to ROI manger"""
- manager = self.getRegionOfInterestManager()
-
- if manager is None:
- self.setRowCount(0)
- return
-
- rois = manager.getRois()
-
- self.setRowCount(len(rois))
- for index, roi in enumerate(rois):
- baseFlags = qt.Qt.ItemIsSelectable | qt.Qt.ItemIsEnabled
-
- # Label and visible
- label = roi.getName()
- item = qt.QTableWidgetItem(label)
- item.setFlags(baseFlags | qt.Qt.ItemIsEditable | qt.Qt.ItemIsUserCheckable)
- item.setData(qt.Qt.UserRole, index)
- item.setCheckState(
- qt.Qt.Checked if roi.isVisible() else qt.Qt.Unchecked)
- self.setItem(index, 0, item)
-
- # Editable
- item = qt.QTableWidgetItem()
- item.setFlags(baseFlags | qt.Qt.ItemIsUserCheckable)
- item.setData(qt.Qt.UserRole, index)
- item.setCheckState(
- qt.Qt.Checked if roi.isEditable() else qt.Qt.Unchecked)
- self.setItem(index, 1, item)
- item.setTextAlignment(qt.Qt.AlignCenter)
- item.setText(None)
-
- # Kind
- label = roi._getShortName()
- if label is None:
- # Default value if kind is not overrided
- label = roi.__class__.__name__
- item = qt.QTableWidgetItem(label.capitalize())
- item.setFlags(baseFlags)
- self.setItem(index, 2, item)
-
- item = qt.QTableWidgetItem()
- item.setFlags(baseFlags)
-
- # Coordinates
- text = self._getReadableRoiDescription(roi)
- item.setText(text)
- self.setItem(index, 3, item)
-
- # Delete
- delBtn = _DeleteRegionOfInterestToolButton(None, roi)
- widget = qt.QWidget(self)
- layout = qt.QHBoxLayout()
- layout.setContentsMargins(2, 2, 2, 2)
- layout.setSpacing(0)
- widget.setLayout(layout)
- layout.addStretch(1)
- layout.addWidget(delBtn)
- layout.addStretch(1)
- self.setCellWidget(index, 4, widget)
-
- def getRegionOfInterestManager(self):
- """Returns the :class:`RegionOfInterestManager` this widget supervise.
-
- It returns None if not sync with an :class:`RegionOfInterestManager`.
-
- :rtype: RegionOfInterestManager
- """
- return None if self._roiManagerRef is None else self._roiManagerRef()
diff --git a/silx/gui/plot/tools/test/__init__.py b/silx/gui/plot/tools/test/__init__.py
deleted file mode 100644
index 1429545..0000000
--- a/silx/gui/plot/tools/test/__init__.py
+++ /dev/null
@@ -1,52 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018 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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "26/03/2018"
-
-
-import unittest
-
-from . import testROI
-from . import testTools
-from . import testScatterProfileToolBar
-from . import testCurveLegendsWidget
-from . import testProfile
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTests(
- [testROI.suite(),
- testTools.suite(),
- testScatterProfileToolBar.suite(),
- testCurveLegendsWidget.suite(),
- testProfile.suite(),
- ])
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/tools/test/testCurveLegendsWidget.py b/silx/gui/plot/tools/test/testCurveLegendsWidget.py
deleted file mode 100644
index 4824dd7..0000000
--- a/silx/gui/plot/tools/test/testCurveLegendsWidget.py
+++ /dev/null
@@ -1,125 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018 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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "02/08/2018"
-
-
-import unittest
-
-from silx.gui import qt
-from silx.utils.testutils import ParametricTestCase
-from silx.gui.utils.testutils import TestCaseQt
-from silx.gui.plot import PlotWindow
-from silx.gui.plot.tools import CurveLegendsWidget
-
-
-class TestCurveLegendsWidget(TestCaseQt, ParametricTestCase):
- """Tests for CurveLegendsWidget class"""
-
- def setUp(self):
- super(TestCurveLegendsWidget, self).setUp()
- self.plot = PlotWindow()
-
- self.legends = CurveLegendsWidget.CurveLegendsWidget()
- self.legends.setPlotWidget(self.plot)
-
- dock = qt.QDockWidget()
- dock.setWindowTitle('Curve Legends')
- dock.setWidget(self.legends)
- self.plot.addTabbedDockWidget(dock)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- def tearDown(self):
- del self.legends
- self.qapp.processEvents()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- super(TestCurveLegendsWidget, self).tearDown()
-
- def _assertNbLegends(self, count):
- """Check the number of legends in the CurveLegendsWidget"""
- children = self.legends.findChildren(CurveLegendsWidget._LegendWidget)
- self.assertEqual(len(children), count)
-
- def testAddRemoveCurves(self):
- """Test CurveLegendsWidget while adding/removing curves"""
- self.plot.addCurve((0, 1), (1, 2), legend='a')
- self._assertNbLegends(1)
- self.plot.addCurve((0, 1), (2, 3), legend='b')
- self._assertNbLegends(2)
-
- # Detached/attach
- self.legends.setPlotWidget(None)
- self._assertNbLegends(0)
-
- self.legends.setPlotWidget(self.plot)
- self._assertNbLegends(2)
-
- self.plot.clear()
- self._assertNbLegends(0)
-
- def testUpdateCurves(self):
- """Test CurveLegendsWidget while updating curves """
- self.plot.addCurve((0, 1), (1, 2), legend='a')
- self._assertNbLegends(1)
- self.plot.addCurve((0, 1), (2, 3), legend='b')
- self._assertNbLegends(2)
-
- # Activate curve
- self.plot.setActiveCurve('a')
- self.qapp.processEvents()
- self.plot.setActiveCurve('b')
- self.qapp.processEvents()
-
- # Change curve style
- curve = self.plot.getCurve('a')
- curve.setLineWidth(2)
- for linestyle in (':', '', '--', '-'):
- with self.subTest(linestyle=linestyle):
- curve.setLineStyle(linestyle)
- self.qapp.processEvents()
- self.qWait(1000)
-
- for symbol in ('o', 'd', '', 's'):
- with self.subTest(symbol=symbol):
- curve.setSymbol(symbol)
- self.qapp.processEvents()
- self.qWait(1000)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(
- TestCurveLegendsWidget))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/tools/test/testProfile.py b/silx/gui/plot/tools/test/testProfile.py
deleted file mode 100644
index 444cfe0..0000000
--- a/silx/gui/plot/tools/test/testProfile.py
+++ /dev/null
@@ -1,673 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018 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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import unittest
-import contextlib
-import numpy
-import logging
-
-from silx.gui import qt
-from silx.utils import deprecation
-from silx.utils import testutils
-
-from silx.gui.utils.testutils import TestCaseQt
-from silx.utils.testutils import ParametricTestCase
-from silx.gui.plot import PlotWindow, Plot1D, Plot2D, Profile
-from silx.gui.plot.StackView import StackView
-from silx.gui.plot.tools.profile import rois
-from silx.gui.plot.tools.profile import editors
-from silx.gui.plot.items import roi as roi_items
-from silx.gui.plot.tools.profile import manager
-from silx.gui import plot as silx_plot
-
-_logger = logging.getLogger(__name__)
-
-
-class TestRois(TestCaseQt):
-
- def test_init(self):
- """Check that the constructor is not called twice"""
- roi = rois.ProfileImageVerticalLineROI()
- if qt.BINDING not in ["PySide", "PySide2"]:
- # the profile ROI + the shape
- self.assertEqual(roi.receivers(roi.sigRegionChanged), 2)
-
-
-class TestInteractions(TestCaseQt):
-
- @contextlib.contextmanager
- def defaultPlot(self):
- try:
- widget = silx_plot.PlotWidget()
- widget.show()
- self.qWaitForWindowExposed(widget)
- yield widget
- finally:
- widget.close()
- widget = None
- self.qWait()
-
- @contextlib.contextmanager
- def imagePlot(self):
- try:
- widget = silx_plot.Plot2D()
- image = numpy.arange(10 * 10).reshape(10, -1)
- widget.addImage(image)
- widget.show()
- self.qWaitForWindowExposed(widget)
- yield widget
- finally:
- widget.close()
- widget = None
- self.qWait()
-
- @contextlib.contextmanager
- def scatterPlot(self):
- try:
- widget = silx_plot.ScatterView()
-
- nbX, nbY = 7, 5
- yy = numpy.atleast_2d(numpy.ones(nbY)).T
- xx = numpy.atleast_2d(numpy.ones(nbX))
- positionX = numpy.linspace(10, 50, nbX) * yy
- positionX = positionX.reshape(nbX * nbY)
- positionY = numpy.atleast_2d(numpy.linspace(20, 60, nbY)).T * xx
- positionY = positionY.reshape(nbX * nbY)
- values = numpy.arange(nbX * nbY)
-
- widget.setData(positionX, positionY, values)
- widget.resetZoom()
- widget.show()
- self.qWaitForWindowExposed(widget)
- yield widget.getPlotWidget()
- finally:
- widget.close()
- widget = None
- self.qWait()
-
- @contextlib.contextmanager
- def stackPlot(self):
- try:
- widget = silx_plot.StackView()
- image = numpy.arange(10 * 10).reshape(10, -1)
- cube = numpy.array([image, image, image])
- widget.setStack(cube)
- widget.resetZoom()
- widget.show()
- self.qWaitForWindowExposed(widget)
- yield widget.getPlotWidget()
- finally:
- widget.close()
- widget = None
- self.qWait()
-
- def waitPendingOperations(self, proflie):
- for _ in range(10):
- if not proflie.hasPendingOperations():
- return
- self.qWait(100)
- _logger.error("The profile manager still have pending operations")
-
- def genericRoiTest(self, plot, roiClass):
- profileManager = manager.ProfileManager(plot, plot)
- profileManager.setItemType(image=True, scatter=True)
-
- try:
- action = profileManager.createProfileAction(roiClass, plot)
- action.triggered[bool].emit(True)
- widget = plot.getWidgetHandle()
-
- # Do the mouse interaction
- pos1 = widget.width() * 0.4, widget.height() * 0.4
- self.mouseMove(widget, pos=pos1)
- self.mouseClick(widget, qt.Qt.LeftButton, pos=pos1)
-
- if issubclass(roiClass, roi_items.LineROI):
- pos2 = widget.width() * 0.6, widget.height() * 0.6
- self.mouseMove(widget, pos=pos2)
- self.mouseClick(widget, qt.Qt.LeftButton, pos=pos2)
-
- self.waitPendingOperations(profileManager)
-
- # Test that something was computed
- if issubclass(roiClass, rois._ProfileCrossROI):
- self.assertEqual(profileManager._computedProfiles, 2)
- elif issubclass(roiClass, roi_items.LineROI):
- self.assertGreaterEqual(profileManager._computedProfiles, 1)
- else:
- self.assertEqual(profileManager._computedProfiles, 1)
-
- # Test the created ROIs
- profileRois = profileManager.getRoiManager().getRois()
- if issubclass(roiClass, rois._ProfileCrossROI):
- self.assertEqual(len(profileRois), 3)
- else:
- self.assertEqual(len(profileRois), 1)
- # The first one should be the expected one
- roi = profileRois[0]
-
- # Test that something was displayed
- if issubclass(roiClass, rois._ProfileCrossROI):
- profiles = roi._getLines()
- window = profiles[0].getProfileWindow()
- self.assertIsNotNone(window)
- window = profiles[1].getProfileWindow()
- self.assertIsNotNone(window)
- else:
- window = roi.getProfileWindow()
- self.assertIsNotNone(window)
- finally:
- profileManager.clearProfile()
-
- def testImageActions(self):
- roiClasses = [
- rois.ProfileImageHorizontalLineROI,
- rois.ProfileImageVerticalLineROI,
- rois.ProfileImageLineROI,
- rois.ProfileImageCrossROI,
- ]
- with self.imagePlot() as plot:
- for roiClass in roiClasses:
- with self.subTest(roiClass=roiClass):
- self.genericRoiTest(plot, roiClass)
-
- def testScatterActions(self):
- roiClasses = [
- rois.ProfileScatterHorizontalLineROI,
- rois.ProfileScatterVerticalLineROI,
- rois.ProfileScatterLineROI,
- rois.ProfileScatterCrossROI,
- rois.ProfileScatterHorizontalSliceROI,
- rois.ProfileScatterVerticalSliceROI,
- rois.ProfileScatterCrossSliceROI,
- ]
- with self.scatterPlot() as plot:
- for roiClass in roiClasses:
- with self.subTest(roiClass=roiClass):
- self.genericRoiTest(plot, roiClass)
-
- def testStackActions(self):
- roiClasses = [
- rois.ProfileImageStackHorizontalLineROI,
- rois.ProfileImageStackVerticalLineROI,
- rois.ProfileImageStackLineROI,
- rois.ProfileImageStackCrossROI,
- ]
- with self.stackPlot() as plot:
- for roiClass in roiClasses:
- with self.subTest(roiClass=roiClass):
- self.genericRoiTest(plot, roiClass)
-
- def genericEditorTest(self, plot, roi, editor):
- if isinstance(editor, editors._NoProfileRoiEditor):
- pass
- elif isinstance(editor, editors._DefaultImageStackProfileRoiEditor):
- # GUI to ROI
- editor._lineWidth.setValue(2)
- self.assertEqual(roi.getProfileLineWidth(), 2)
- editor._methodsButton.setMethod("sum")
- self.assertEqual(roi.getProfileMethod(), "sum")
- editor._profileDim.setDimension(1)
- self.assertEqual(roi.getProfileType(), "1D")
- # ROI to GUI
- roi.setProfileLineWidth(3)
- self.assertEqual(editor._lineWidth.value(), 3)
- roi.setProfileMethod("mean")
- self.assertEqual(editor._methodsButton.getMethod(), "mean")
- roi.setProfileType("2D")
- self.assertEqual(editor._profileDim.getDimension(), 2)
- elif isinstance(editor, editors._DefaultImageProfileRoiEditor):
- # GUI to ROI
- editor._lineWidth.setValue(2)
- self.assertEqual(roi.getProfileLineWidth(), 2)
- editor._methodsButton.setMethod("sum")
- self.assertEqual(roi.getProfileMethod(), "sum")
- # ROI to GUI
- roi.setProfileLineWidth(3)
- self.assertEqual(editor._lineWidth.value(), 3)
- roi.setProfileMethod("mean")
- self.assertEqual(editor._methodsButton.getMethod(), "mean")
- elif isinstance(editor, editors._DefaultScatterProfileRoiEditor):
- # GUI to ROI
- editor._nPoints.setValue(100)
- self.assertEqual(roi.getNPoints(), 100)
- # ROI to GUI
- roi.setNPoints(200)
- self.assertEqual(editor._nPoints.value(), 200)
- else:
- assert False
-
- def testEditors(self):
- roiClasses = [
- (rois.ProfileImageHorizontalLineROI, editors._DefaultImageProfileRoiEditor),
- (rois.ProfileImageVerticalLineROI, editors._DefaultImageProfileRoiEditor),
- (rois.ProfileImageLineROI, editors._DefaultImageProfileRoiEditor),
- (rois.ProfileImageCrossROI, editors._DefaultImageProfileRoiEditor),
- (rois.ProfileScatterHorizontalLineROI, editors._DefaultScatterProfileRoiEditor),
- (rois.ProfileScatterVerticalLineROI, editors._DefaultScatterProfileRoiEditor),
- (rois.ProfileScatterLineROI, editors._DefaultScatterProfileRoiEditor),
- (rois.ProfileScatterCrossROI, editors._DefaultScatterProfileRoiEditor),
- (rois.ProfileScatterHorizontalSliceROI, editors._NoProfileRoiEditor),
- (rois.ProfileScatterVerticalSliceROI, editors._NoProfileRoiEditor),
- (rois.ProfileScatterCrossSliceROI, editors._NoProfileRoiEditor),
- (rois.ProfileImageStackHorizontalLineROI, editors._DefaultImageStackProfileRoiEditor),
- (rois.ProfileImageStackVerticalLineROI, editors._DefaultImageStackProfileRoiEditor),
- (rois.ProfileImageStackLineROI, editors._DefaultImageStackProfileRoiEditor),
- (rois.ProfileImageStackCrossROI, editors._DefaultImageStackProfileRoiEditor),
- ]
- with self.defaultPlot() as plot:
- profileManager = manager.ProfileManager(plot, plot)
- editorAction = profileManager.createEditorAction(parent=plot)
- for roiClass, editorClass in roiClasses:
- with self.subTest(roiClass=roiClass):
- roi = roiClass()
- roi._setProfileManager(profileManager)
- try:
- # Force widget creation
- menu = qt.QMenu(plot)
- menu.addAction(editorAction)
- widgets = editorAction.createdWidgets()
- self.assertGreater(len(widgets), 0)
-
- editorAction.setProfileRoi(roi)
- editorWidget = editorAction._getEditor(widgets[0])
- self.assertIsInstance(editorWidget, editorClass)
- self.genericEditorTest(plot, roi, editorWidget)
- finally:
- editorAction.setProfileRoi(None)
- menu.deleteLater()
- menu = None
- self.qapp.processEvents()
-
-
-class TestProfileToolBar(TestCaseQt, ParametricTestCase):
- """Tests for ProfileToolBar widget."""
-
- def setUp(self):
- super(TestProfileToolBar, self).setUp()
- self.plot = PlotWindow()
- self.toolBar = Profile.ProfileToolBar(plot=self.plot)
- self.plot.addToolBar(self.toolBar)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- self.mouseMove(self.plot) # Move to center
- self.qapp.processEvents()
- deprecation.FORCE = True
-
- def tearDown(self):
- deprecation.FORCE = False
- self.qapp.processEvents()
- profileManager = self.toolBar.getProfileManager()
- profileManager.clearProfile()
- profileManager = None
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- del self.toolBar
-
- super(TestProfileToolBar, self).tearDown()
-
- def testAlignedProfile(self):
- """Test horizontal and vertical profile, without and with image"""
- # Use Plot backend widget to submit mouse events
- widget = self.plot.getWidgetHandle()
- for method in ('sum', 'mean'):
- with self.subTest(method=method):
- # 2 positions to use for mouse events
- pos1 = widget.width() * 0.4, widget.height() * 0.4
- pos2 = widget.width() * 0.6, widget.height() * 0.6
-
- for action in (self.toolBar.hLineAction, self.toolBar.vLineAction):
- with self.subTest(mode=action.text()):
- # Trigger tool button for mode
- action.trigger()
- # Without image
- self.mouseMove(widget, pos=pos1)
- self.mouseClick(widget, qt.Qt.LeftButton, pos=pos1)
-
- # with image
- self.plot.addImage(
- numpy.arange(100 * 100).reshape(100, -1))
- self.mousePress(widget, qt.Qt.LeftButton, pos=pos1)
- self.mouseMove(widget, pos=pos2)
- self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2)
-
- self.mouseMove(widget)
- self.mouseClick(widget, qt.Qt.LeftButton)
-
- manager = self.toolBar.getProfileManager()
- for _ in range(20):
- self.qWait(200)
- if not manager.hasPendingOperations():
- break
-
- @testutils.test_logging(deprecation.depreclog.name, warning=4)
- def testDiagonalProfile(self):
- """Test diagonal profile, without and with image"""
- # Use Plot backend widget to submit mouse events
- widget = self.plot.getWidgetHandle()
-
- for method in ('sum', 'mean'):
- for image in (False, True):
- with self.subTest(method=method, image=image):
- # 2 positions to use for mouse events
- pos1 = widget.width() * 0.4, widget.height() * 0.4
- pos2 = widget.width() * 0.6, widget.height() * 0.6
-
- if image:
- self.plot.addImage(
- numpy.arange(100 * 100).reshape(100, -1))
-
- # Trigger tool button for diagonal profile mode
- self.toolBar.lineAction.trigger()
-
- # draw profile line
- widget.setFocus(qt.Qt.OtherFocusReason)
- self.mouseMove(widget, pos=pos1)
- self.qWait(100)
- self.mousePress(widget, qt.Qt.LeftButton, pos=pos1)
- self.qWait(100)
- self.mouseMove(widget, pos=pos2)
- self.qWait(100)
- self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2)
- self.qWait(100)
-
- manager = self.toolBar.getProfileManager()
-
- for _ in range(20):
- self.qWait(200)
- if not manager.hasPendingOperations():
- break
-
- roi = manager.getCurrentRoi()
- self.assertIsNotNone(roi)
- roi.setProfileLineWidth(3)
- roi.setProfileMethod(method)
-
- for _ in range(20):
- self.qWait(200)
- if not manager.hasPendingOperations():
- break
-
- if image is True:
- curveItem = self.toolBar.getProfilePlot().getAllCurves()[0]
- if method == 'sum':
- self.assertTrue(curveItem.getData()[1].max() > 10000)
- elif method == 'mean':
- self.assertTrue(curveItem.getData()[1].max() < 10000)
-
- # Remove the ROI so the profile window is also removed
- roiManager = manager.getRoiManager()
- roiManager.removeRoi(roi)
- self.qWait(100)
-
-
-class TestDeprecatedProfileToolBar(TestCaseQt):
- """Tests old features of the ProfileToolBar widget."""
-
- def setUp(self):
- self.plot = None
- super(TestDeprecatedProfileToolBar, self).setUp()
-
- def tearDown(self):
- if self.plot is not None:
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- self.plot = None
- self.qWait()
-
- super(TestDeprecatedProfileToolBar, self).tearDown()
-
- @testutils.test_logging(deprecation.depreclog.name, warning=2)
- def testCustomProfileWindow(self):
- from silx.gui.plot import ProfileMainWindow
-
- self.plot = PlotWindow()
- profileWindow = ProfileMainWindow.ProfileMainWindow(self.plot)
- toolBar = Profile.ProfileToolBar(parent=self.plot,
- plot=self.plot,
- profileWindow=profileWindow)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
- profileWindow.show()
- self.qWaitForWindowExposed(profileWindow)
- self.qapp.processEvents()
-
- self.plot.addImage(numpy.arange(10 * 10).reshape(10, -1))
- profile = rois.ProfileImageHorizontalLineROI()
- profile.setPosition(5)
- toolBar.getProfileManager().getRoiManager().addRoi(profile)
- toolBar.getProfileManager().getRoiManager().setCurrentRoi(profile)
-
- for _ in range(20):
- self.qWait(200)
- if not toolBar.getProfileManager().hasPendingOperations():
- break
-
- # There is a displayed profile
- self.assertIsNotNone(profileWindow.getProfile())
- self.assertIs(toolBar.getProfileMainWindow(), profileWindow)
-
- # There is nothing anymore but the window is still there
- toolBar.getProfileManager().clearProfile()
- self.qapp.processEvents()
- self.assertIsNone(profileWindow.getProfile())
-
-
-class TestProfile3DToolBar(TestCaseQt):
- """Tests for Profile3DToolBar widget.
- """
- def setUp(self):
- super(TestProfile3DToolBar, self).setUp()
- self.plot = StackView()
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- self.plot.setStack(numpy.array([
- [[0, 1, 2], [3, 4, 5]],
- [[6, 7, 8], [9, 10, 11]],
- [[12, 13, 14], [15, 16, 17]]
- ]))
- deprecation.FORCE = True
-
- def tearDown(self):
- deprecation.FORCE = False
- profileManager = self.plot.getProfileToolbar().getProfileManager()
- profileManager.clearProfile()
- profileManager = None
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- self.plot = None
-
- super(TestProfile3DToolBar, self).tearDown()
-
- @testutils.test_logging(deprecation.depreclog.name, warning=2)
- def testMethodProfile2D(self):
- """Test that the profile can have a different method if we want to
- compute then in 1D or in 2D"""
-
- toolBar = self.plot.getProfileToolbar()
-
- toolBar.vLineAction.trigger()
- plot2D = self.plot.getPlotWidget().getWidgetHandle()
- pos1 = plot2D.width() * 0.5, plot2D.height() * 0.5
- self.mouseClick(plot2D, qt.Qt.LeftButton, pos=pos1)
-
- manager = toolBar.getProfileManager()
- roi = manager.getCurrentRoi()
- roi.setProfileMethod("mean")
- roi.setProfileType("2D")
- roi.setProfileLineWidth(3)
-
- for _ in range(20):
- self.qWait(200)
- if not manager.hasPendingOperations():
- break
-
- # check 2D 'mean' profile
- profilePlot = toolBar.getProfilePlot()
- data = profilePlot.getAllImages()[0].getData()
- expected = numpy.array([[1, 4], [7, 10], [13, 16]])
- numpy.testing.assert_almost_equal(data, expected)
-
- @testutils.test_logging(deprecation.depreclog.name, warning=2)
- def testMethodSumLine(self):
- """Simple interaction test to make sure the sum is correctly computed
- """
- toolBar = self.plot.getProfileToolbar()
-
- toolBar.lineAction.trigger()
- plot2D = self.plot.getPlotWidget().getWidgetHandle()
- pos1 = plot2D.width() * 0.5, plot2D.height() * 0.2
- pos2 = plot2D.width() * 0.5, plot2D.height() * 0.8
-
- self.mouseMove(plot2D, pos=pos1)
- self.mousePress(plot2D, qt.Qt.LeftButton, pos=pos1)
- self.mouseMove(plot2D, pos=pos2)
- self.mouseRelease(plot2D, qt.Qt.LeftButton, pos=pos2)
-
- manager = toolBar.getProfileManager()
- roi = manager.getCurrentRoi()
- roi.setProfileMethod("sum")
- roi.setProfileType("2D")
- roi.setProfileLineWidth(3)
-
- for _ in range(20):
- self.qWait(200)
- if not manager.hasPendingOperations():
- break
-
- # check 2D 'sum' profile
- profilePlot = toolBar.getProfilePlot()
- data = profilePlot.getAllImages()[0].getData()
- expected = numpy.array([[3, 12], [21, 30], [39, 48]])
- numpy.testing.assert_almost_equal(data, expected)
-
-
-class TestGetProfilePlot(TestCaseQt):
-
- def setUp(self):
- self.plot = None
- super(TestGetProfilePlot, self).setUp()
-
- def tearDown(self):
- if self.plot is not None:
- manager = self.plot.getProfileToolbar().getProfileManager()
- manager.clearProfile()
- manager = None
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- self.plot = None
-
- super(TestGetProfilePlot, self).tearDown()
-
- def testProfile1D(self):
- self.plot = Plot2D()
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
- self.plot.addImage([[0, 1], [2, 3]])
-
- toolBar = self.plot.getProfileToolbar()
-
- manager = toolBar.getProfileManager()
- roiManager = manager.getRoiManager()
-
- roi = rois.ProfileImageHorizontalLineROI()
- roi.setPosition(0.5)
- roiManager.addRoi(roi)
- roiManager.setCurrentRoi(roi)
-
- for _ in range(20):
- self.qWait(200)
- if not manager.hasPendingOperations():
- break
-
- profileWindow = roi.getProfileWindow()
- self.assertIsInstance(roi.getProfileWindow(), qt.QMainWindow)
- self.assertIsInstance(profileWindow.getCurrentPlotWidget(), Plot1D)
-
- def testProfile2D(self):
- """Test that the profile plot associated to a stack view is either a
- Plot1D or a plot 2D instance."""
- self.plot = StackView()
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- self.plot.setStack(numpy.array([[[0, 1], [2, 3]],
- [[4, 5], [6, 7]]]))
-
- toolBar = self.plot.getProfileToolbar()
-
- manager = toolBar.getProfileManager()
- roiManager = manager.getRoiManager()
-
- roi = rois.ProfileImageStackHorizontalLineROI()
- roi.setPosition(0.5)
- roi.setProfileType("2D")
- roiManager.addRoi(roi)
- roiManager.setCurrentRoi(roi)
-
- for _ in range(20):
- self.qWait(200)
- if not manager.hasPendingOperations():
- break
-
- profileWindow = roi.getProfileWindow()
- self.assertIsInstance(roi.getProfileWindow(), qt.QMainWindow)
- self.assertIsInstance(profileWindow.getCurrentPlotWidget(), Plot2D)
-
- roi.setProfileType("1D")
-
- for _ in range(20):
- self.qWait(200)
- if not manager.hasPendingOperations():
- break
-
- profileWindow = roi.getProfileWindow()
- self.assertIsInstance(roi.getProfileWindow(), qt.QMainWindow)
- self.assertIsInstance(profileWindow.getCurrentPlotWidget(), Plot1D)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loadTests(TestRois))
- test_suite.addTest(loadTests(TestInteractions))
- test_suite.addTest(loadTests(TestProfileToolBar))
- test_suite.addTest(loadTests(TestGetProfilePlot))
- test_suite.addTest(loadTests(TestProfile3DToolBar))
- test_suite.addTest(loadTests(TestDeprecatedProfileToolBar))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/tools/test/testROI.py b/silx/gui/plot/tools/test/testROI.py
deleted file mode 100644
index 8a00073..0000000
--- a/silx/gui/plot/tools/test/testROI.py
+++ /dev/null
@@ -1,694 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import unittest
-import numpy.testing
-
-from silx.gui import qt
-from silx.utils.testutils import ParametricTestCase
-from silx.gui.utils.testutils import TestCaseQt, SignalListener
-from silx.gui.plot import PlotWindow
-import silx.gui.plot.items.roi as roi_items
-from silx.gui.plot.tools import roi
-
-
-class TestRoiItems(TestCaseQt):
-
- def testLine_geometry(self):
- item = roi_items.LineROI()
- startPoint = numpy.array([1, 2])
- endPoint = numpy.array([3, 4])
- item.setEndPoints(startPoint, endPoint)
- numpy.testing.assert_allclose(item.getEndPoints()[0], startPoint)
- numpy.testing.assert_allclose(item.getEndPoints()[1], endPoint)
-
- def testHLine_geometry(self):
- item = roi_items.HorizontalLineROI()
- item.setPosition(15)
- self.assertEqual(item.getPosition(), 15)
-
- def testVLine_geometry(self):
- item = roi_items.VerticalLineROI()
- item.setPosition(15)
- self.assertEqual(item.getPosition(), 15)
-
- def testPoint_geometry(self):
- point = numpy.array([1, 2])
- item = roi_items.PointROI()
- item.setPosition(point)
- numpy.testing.assert_allclose(item.getPosition(), point)
-
- def testRectangle_originGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- center = numpy.array([5, 10])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- numpy.testing.assert_allclose(item.getOrigin(), origin)
- numpy.testing.assert_allclose(item.getSize(), size)
- numpy.testing.assert_allclose(item.getCenter(), center)
-
- def testRectangle_centerGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- center = numpy.array([5, 10])
- item = roi_items.RectangleROI()
- item.setGeometry(center=center, size=size)
- numpy.testing.assert_allclose(item.getOrigin(), origin)
- numpy.testing.assert_allclose(item.getSize(), size)
- numpy.testing.assert_allclose(item.getCenter(), center)
-
- def testRectangle_setCenterGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- newCenter = numpy.array([0, 0])
- item.setCenter(newCenter)
- expectedOrigin = numpy.array([-5, -10])
- numpy.testing.assert_allclose(item.getOrigin(), expectedOrigin)
- numpy.testing.assert_allclose(item.getCenter(), newCenter)
- numpy.testing.assert_allclose(item.getSize(), size)
-
- def testRectangle_setOriginGeometry(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- newOrigin = numpy.array([10, 10])
- item.setOrigin(newOrigin)
- expectedCenter = numpy.array([15, 20])
- numpy.testing.assert_allclose(item.getOrigin(), newOrigin)
- numpy.testing.assert_allclose(item.getCenter(), expectedCenter)
- numpy.testing.assert_allclose(item.getSize(), size)
-
- def testCircle_geometry(self):
- center = numpy.array([0, 0])
- radius = 10.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- numpy.testing.assert_allclose(item.getCenter(), center)
- numpy.testing.assert_allclose(item.getRadius(), radius)
-
- def testCircle_setCenter(self):
- center = numpy.array([0, 0])
- radius = 10.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- newCenter = numpy.array([-10, 0])
- item.setCenter(newCenter)
- numpy.testing.assert_allclose(item.getCenter(), newCenter)
- numpy.testing.assert_allclose(item.getRadius(), radius)
-
- def testCircle_setRadius(self):
- center = numpy.array([0, 0])
- radius = 10.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- newRadius = 5.1
- item.setRadius(newRadius)
- numpy.testing.assert_allclose(item.getCenter(), center)
- numpy.testing.assert_allclose(item.getRadius(), newRadius)
-
- def testCircle_contains(self):
- center = numpy.array([2, -1])
- radius = 1.
- item = roi_items.CircleROI()
- item.setGeometry(center=center, radius=radius)
- self.assertTrue(item.contains([1, -1]))
- self.assertFalse(item.contains([0, 0]))
- self.assertTrue(item.contains([2, 0]))
- self.assertFalse(item.contains([3.01, -1]))
-
- def testEllipse_contains(self):
- center = numpy.array([-2, 0])
- item = roi_items.EllipseROI()
- item.setCenter(center)
- item.setOrientation(numpy.pi / 4.0)
- item.setMajorRadius(2)
- item.setMinorRadius(1)
- print(item.getMinorRadius(), item.getMajorRadius())
- self.assertFalse(item.contains([0, 0]))
- self.assertTrue(item.contains([-1, 1]))
- self.assertTrue(item.contains([-3, 0]))
- self.assertTrue(item.contains([-2, 0]))
- self.assertTrue(item.contains([-2, 1]))
- self.assertFalse(item.contains([-4, 1]))
-
- def testRectangle_isIn(self):
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin, size=size)
- self.assertTrue(item.contains(position=(0, 0)))
- self.assertTrue(item.contains(position=(2, 14)))
- self.assertFalse(item.contains(position=(14, 12)))
-
- def testPolygon_emptyGeometry(self):
- points = numpy.empty((0, 2))
- item = roi_items.PolygonROI()
- item.setPoints(points)
- numpy.testing.assert_allclose(item.getPoints(), points)
-
- def testPolygon_geometry(self):
- points = numpy.array([[10, 10], [12, 10], [50, 1]])
- item = roi_items.PolygonROI()
- item.setPoints(points)
- numpy.testing.assert_allclose(item.getPoints(), points)
-
- def testPolygon_isIn(self):
- points = numpy.array([[0, 0], [0, 10], [5, 10]])
- item = roi_items.PolygonROI()
- item.setPoints(points)
- self.assertTrue(item.contains((0, 0)))
- self.assertFalse(item.contains((6, 2)))
- self.assertFalse(item.contains((-2, 5)))
- self.assertFalse(item.contains((2, -1)))
- self.assertFalse(item.contains((8, 1)))
- self.assertTrue(item.contains((1, 8)))
-
- def testArc_getToSetGeometry(self):
- """Test that we can use getGeometry as input to setGeometry"""
- item = roi_items.ArcROI()
- item.setFirstShapePoints(numpy.array([[5, 10], [50, 100]]))
- item.setGeometry(*item.getGeometry())
-
- def testArc_degenerated_point(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 0, 0, 0
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
-
- def testArc_degenerated_line(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
-
- def testArc_special_circle(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, 3 * numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), item.getEndAngle() - numpy.pi * 2.0)
- self.assertTrue(item.isClosed())
-
- def testArc_special_donut(self):
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi, 3 * numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), item.getEndAngle() - numpy.pi * 2.0)
- self.assertTrue(item.isClosed())
-
- def testArc_clockwiseGeometry(self):
- """Test that we can use getGeometry as input to setGeometry"""
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), startAngle)
- self.assertAlmostEqual(item.getEndAngle(), endAngle)
- self.assertAlmostEqual(item.isClosed(), False)
-
- def testArc_anticlockwiseGeometry(self):
- """Test that we can use getGeometry as input to setGeometry"""
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, -numpy.pi * 0.5
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- numpy.testing.assert_allclose(item.getCenter(), center)
- self.assertAlmostEqual(item.getInnerRadius(), innerRadius)
- self.assertAlmostEqual(item.getOuterRadius(), outerRadius)
- self.assertAlmostEqual(item.getStartAngle(), startAngle)
- self.assertAlmostEqual(item.getEndAngle(), endAngle)
- self.assertAlmostEqual(item.isClosed(), False)
-
- def testHRange_geometry(self):
- item = roi_items.HorizontalRangeROI()
- vmin = 1
- vmax = 3
- item.setRange(vmin, vmax)
- self.assertAlmostEqual(item.getMin(), vmin)
- self.assertAlmostEqual(item.getMax(), vmax)
- self.assertAlmostEqual(item.getCenter(), 2)
-
-
-class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase):
- """Tests for RegionOfInterestManager class"""
-
- def setUp(self):
- super(TestRegionOfInterestManager, self).setUp()
- self.plot = PlotWindow()
-
- self.roiTableWidget = roi.RegionOfInterestTableWidget()
- dock = qt.QDockWidget()
- dock.setWidget(self.roiTableWidget)
- self.plot.addDockWidget(qt.Qt.BottomDockWidgetArea, dock)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- def tearDown(self):
- del self.roiTableWidget
- self.qapp.processEvents()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- super(TestRegionOfInterestManager, self).tearDown()
-
- def test(self):
- """Test ROI of different shapes"""
- tests = ( # shape, points=[list of (x, y), list of (x, y)]
- (roi_items.PointROI, numpy.array(([(10., 15.)], [(20., 25.)]))),
- (roi_items.RectangleROI,
- numpy.array((((1., 10.), (11., 20.)),
- ((2., 3.), (12., 13.))))),
- (roi_items.PolygonROI,
- numpy.array((((0., 1.), (0., 10.), (10., 0.)),
- ((5., 6.), (5., 16.), (15., 6.))))),
- (roi_items.LineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
- (roi_items.HorizontalLineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
- (roi_items.VerticalLineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
- (roi_items.HorizontalLineROI,
- numpy.array((((10., 20.), (10., 30.)),
- ((30., 40.), (30., 50.))))),
- )
-
- for roiClass, points in tests:
- with self.subTest(roiClass=roiClass):
- manager = roi.RegionOfInterestManager(self.plot)
- self.roiTableWidget.setRegionOfInterestManager(manager)
- manager.start(roiClass)
-
- self.assertEqual(manager.getRois(), ())
-
- finishListener = SignalListener()
- manager.sigInteractiveModeFinished.connect(finishListener)
-
- changedListener = SignalListener()
- manager.sigRoiChanged.connect(changedListener)
-
- # Add a point
- r = roiClass()
- r.setFirstShapePoints(points[0])
- manager.addRoi(r)
- self.qapp.processEvents()
- self.assertTrue(len(manager.getRois()), 1)
- self.assertEqual(changedListener.callCount(), 1)
-
- # Remove it
- manager.removeRoi(manager.getRois()[0])
- self.assertEqual(manager.getRois(), ())
- self.assertEqual(changedListener.callCount(), 2)
-
- # Add two point
- r = roiClass()
- r.setFirstShapePoints(points[0])
- manager.addRoi(r)
- self.qapp.processEvents()
- r = roiClass()
- r.setFirstShapePoints(points[1])
- manager.addRoi(r)
- self.qapp.processEvents()
- self.assertTrue(len(manager.getRois()), 2)
- self.assertEqual(changedListener.callCount(), 4)
-
- # Reset it
- result = manager.clear()
- self.assertTrue(result)
- self.assertEqual(manager.getRois(), ())
- self.assertEqual(changedListener.callCount(), 5)
-
- changedListener.clear()
-
- # Add two point
- r = roiClass()
- r.setFirstShapePoints(points[0])
- manager.addRoi(r)
- self.qapp.processEvents()
- r = roiClass()
- r.setFirstShapePoints(points[1])
- manager.addRoi(r)
- self.qapp.processEvents()
- self.assertTrue(len(manager.getRois()), 2)
- self.assertEqual(changedListener.callCount(), 2)
-
- # stop
- result = manager.stop()
- self.assertTrue(result)
- self.assertTrue(len(manager.getRois()), 1)
- self.qapp.processEvents()
- self.assertEqual(finishListener.callCount(), 1)
-
- manager.clear()
-
- def testRoiDisplay(self):
- rois = []
-
- # Line
- item = roi_items.LineROI()
- startPoint = numpy.array([1, 2])
- endPoint = numpy.array([3, 4])
- item.setEndPoints(startPoint, endPoint)
- rois.append(item)
- # Horizontal line
- item = roi_items.HorizontalLineROI()
- item.setPosition(15)
- rois.append(item)
- # Vertical line
- item = roi_items.VerticalLineROI()
- item.setPosition(15)
- rois.append(item)
- # Point
- item = roi_items.PointROI()
- point = numpy.array([1, 2])
- item.setPosition(point)
- rois.append(item)
- # Rectangle
- item = roi_items.RectangleROI()
- origin = numpy.array([0, 0])
- size = numpy.array([10, 20])
- item.setGeometry(origin=origin, size=size)
- rois.append(item)
- # Polygon
- item = roi_items.PolygonROI()
- points = numpy.array([[10, 10], [12, 10], [50, 1]])
- item.setPoints(points)
- rois.append(item)
- # Degenerated polygon: No points
- item = roi_items.PolygonROI()
- points = numpy.empty((0, 2))
- item.setPoints(points)
- rois.append(item)
- # Degenerated polygon: A single point
- item = roi_items.PolygonROI()
- points = numpy.array([[5, 10]])
- item.setPoints(points)
- rois.append(item)
- # Degenerated arc: it's a point
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 0, 0, 0
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- rois.append(item)
- # Degenerated arc: it's a line
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 0, 100, numpy.pi, numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- rois.append(item)
- # Special arc: it's a donut
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi, 3 * numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- rois.append(item)
- # Arc
- item = roi_items.ArcROI()
- center = numpy.array([10, 20])
- innerRadius, outerRadius, startAngle, endAngle = 1, 100, numpy.pi * 0.5, numpy.pi
- item.setGeometry(center, innerRadius, outerRadius, startAngle, endAngle)
- rois.append(item)
- # Horizontal Range
- item = roi_items.HorizontalRangeROI()
- item.setRange(-1, 3)
- rois.append(item)
-
- manager = roi.RegionOfInterestManager(self.plot)
- self.roiTableWidget.setRegionOfInterestManager(manager)
- for item in rois:
- with self.subTest(roi=str(item)):
- manager.addRoi(item)
- self.qapp.processEvents()
- item.setEditable(True)
- self.qapp.processEvents()
- item.setEditable(False)
- self.qapp.processEvents()
- manager.removeRoi(item)
- self.qapp.processEvents()
-
- def testSelectionProxy(self):
- item1 = roi_items.PointROI()
- item1.setSelectable(True)
- item2 = roi_items.PointROI()
- item2.setSelectable(True)
- item1.setFocusProxy(item2)
- manager = roi.RegionOfInterestManager(self.plot)
- manager.setCurrentRoi(item1)
- self.assertIs(manager.getCurrentRoi(), item2)
-
- def testRemovedSelection(self):
- item1 = roi_items.PointROI()
- item1.setSelectable(True)
- manager = roi.RegionOfInterestManager(self.plot)
- manager.addRoi(item1)
- manager.setCurrentRoi(item1)
- manager.removeRoi(item1)
- self.assertIs(manager.getCurrentRoi(), None)
-
- def testMaxROI(self):
- """Test Max ROI"""
- origin1 = numpy.array([1., 10.])
- size1 = numpy.array([10., 10.])
- origin2 = numpy.array([2., 3.])
- size2 = numpy.array([10., 10.])
-
- manager = roi.InteractiveRegionOfInterestManager(self.plot)
- self.roiTableWidget.setRegionOfInterestManager(manager)
- self.assertEqual(manager.getRois(), ())
-
- changedListener = SignalListener()
- manager.sigRoiChanged.connect(changedListener)
-
- # Add two point
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin1, size=size1)
- manager.addRoi(item)
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin2, size=size2)
- manager.addRoi(item)
- self.qapp.processEvents()
- self.assertEqual(changedListener.callCount(), 2)
- self.assertEqual(len(manager.getRois()), 2)
-
- # Try to set max ROI to 1 while there is 2 ROIs
- with self.assertRaises(ValueError):
- manager.setMaxRois(1)
-
- manager.clear()
- self.assertEqual(len(manager.getRois()), 0)
- self.assertEqual(changedListener.callCount(), 3)
-
- # Set max limit to 1
- manager.setMaxRois(1)
-
- # Add a point
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin1, size=size1)
- manager.addRoi(item)
- self.qapp.processEvents()
- self.assertEqual(changedListener.callCount(), 4)
-
- # Add a 2nd point while max ROI is 1
- item = roi_items.RectangleROI()
- item.setGeometry(origin=origin1, size=size1)
- manager.addRoi(item)
- self.qapp.processEvents()
- self.assertEqual(changedListener.callCount(), 6)
- self.assertEqual(len(manager.getRois()), 1)
-
- def testChangeInteractionMode(self):
- """Test change of interaction mode"""
- manager = roi.RegionOfInterestManager(self.plot)
- self.roiTableWidget.setRegionOfInterestManager(manager)
- manager.start(roi_items.PointROI)
-
- interactiveModeToolBar = self.plot.getInteractiveModeToolBar()
- panAction = interactiveModeToolBar.getPanModeAction()
-
- for roiClass in manager.getSupportedRoiClasses():
- with self.subTest(roiClass=roiClass):
- # Change to pan mode
- panAction.trigger()
-
- # Change to interactive ROI mode
- action = manager.getInteractionModeAction(roiClass)
- action.trigger()
-
- self.assertEqual(roiClass, manager.getCurrentInteractionModeRoiClass())
-
- manager.clear()
-
- def testLineInteraction(self):
- """This test make sure that a ROI based on handles can be edited with
- the mouse."""
- xlimit = self.plot.getXAxis().getLimits()
- ylimit = self.plot.getYAxis().getLimits()
- points = numpy.array([xlimit, ylimit]).T
- center = numpy.mean(points, axis=0)
-
- # Create the line
- manager = roi.RegionOfInterestManager(self.plot)
- item = roi_items.LineROI()
- item.setEndPoints(points[0], points[1])
- item.setEditable(True)
- manager.addRoi(item)
- self.qapp.processEvents()
-
- # Drag the center
- widget = self.plot.getWidgetHandle()
- mx, my = self.plot.dataToPixel(*center)
- self.mouseMove(widget, pos=(mx, my))
- self.mousePress(widget, qt.Qt.LeftButton, pos=(mx, my))
- self.mouseMove(widget, pos=(mx, my+25))
- self.mouseMove(widget, pos=(mx, my+50))
- self.mouseRelease(widget, qt.Qt.LeftButton, pos=(mx, my+50))
-
- result = numpy.array(item.getEndPoints())
- # x location is still the same
- numpy.testing.assert_allclose(points[:, 0], result[:, 0], atol=0.5)
- # size is still the same
- numpy.testing.assert_allclose(points[1] - points[0],
- result[1] - result[0], atol=0.5)
- # But Y is not the same
- self.assertNotEqual(points[0, 1], result[0, 1])
- self.assertNotEqual(points[1, 1], result[1, 1])
- item = None
- manager.clear()
- self.qapp.processEvents()
-
- def testPlotWhenCleared(self):
- """PlotWidget.clear should clean up the available ROIs"""
- manager = roi.RegionOfInterestManager(self.plot)
- item = roi_items.LineROI()
- item.setEndPoints((0, 0), (1, 1))
- item.setEditable(True)
- manager.addRoi(item)
- self.qWait()
- try:
- # Make sure the test setup is fine
- self.assertNotEqual(len(manager.getRois()), 0)
- self.assertNotEqual(len(self.plot.getItems()), 0)
-
- # Call clear and test the expected state
- self.plot.clear()
- self.assertEqual(len(manager.getRois()), 0)
- self.assertEqual(len(self.plot.getItems()), 0)
- finally:
- # Clean up
- manager.clear()
-
- def testPlotWhenRoiRemoved(self):
- """Make sure there is no remaining items in the plot when a ROI is removed"""
- manager = roi.RegionOfInterestManager(self.plot)
- item = roi_items.LineROI()
- item.setEndPoints((0, 0), (1, 1))
- item.setEditable(True)
- manager.addRoi(item)
- self.qWait()
- try:
- # Make sure the test setup is fine
- self.assertNotEqual(len(manager.getRois()), 0)
- self.assertNotEqual(len(self.plot.getItems()), 0)
-
- # Call clear and test the expected state
- manager.removeRoi(item)
- self.assertEqual(len(manager.getRois()), 0)
- self.assertEqual(len(self.plot.getItems()), 0)
- finally:
- # Clean up
- manager.clear()
-
- def testArcRoiSwitchMode(self):
- """Make sure we can switch mode by clicking on the ROI"""
- xlimit = self.plot.getXAxis().getLimits()
- ylimit = self.plot.getYAxis().getLimits()
- points = numpy.array([xlimit, ylimit]).T
- center = numpy.mean(points, axis=0)
- size = numpy.abs(points[1] - points[0])
-
- # Create the line
- manager = roi.RegionOfInterestManager(self.plot)
- item = roi_items.ArcROI()
- item.setGeometry(center, size[1] / 10, size[1] / 2, 0, 3)
- item.setEditable(True)
- item.setSelectable(True)
- manager.addRoi(item)
- self.qapp.processEvents()
-
- # Initial state
- self.assertIs(item.getInteractionMode(), roi_items.ArcROI.ThreePointMode)
- self.qWait(500)
-
- # Click on the center
- widget = self.plot.getWidgetHandle()
- mx, my = self.plot.dataToPixel(*center)
-
- # Select the ROI
- self.mouseMove(widget, pos=(mx, my))
- self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my))
- self.qWait(500)
- self.assertIs(item.getInteractionMode(), roi_items.ArcROI.ThreePointMode)
-
- # Change the mode
- self.mouseMove(widget, pos=(mx, my))
- self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my))
- self.qWait(500)
- self.assertIs(item.getInteractionMode(), roi_items.ArcROI.PolarMode)
-
- manager.clear()
- self.qapp.processEvents()
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loadTests(TestRoiItems))
- test_suite.addTest(loadTests(TestRegionOfInterestManager))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/tools/test/testScatterProfileToolBar.py b/silx/gui/plot/tools/test/testScatterProfileToolBar.py
deleted file mode 100644
index b9f4885..0000000
--- a/silx/gui/plot/tools/test/testScatterProfileToolBar.py
+++ /dev/null
@@ -1,196 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018 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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import unittest
-import numpy
-
-from silx.gui import qt
-from silx.utils.testutils import ParametricTestCase
-from silx.gui.utils.testutils import TestCaseQt
-from silx.gui.plot import PlotWindow
-from silx.gui.plot.tools.profile import manager
-from silx.gui.plot.tools.profile import core
-from silx.gui.plot.tools.profile import rois
-
-
-class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase):
- """Tests for ScatterProfileToolBar class"""
-
- def setUp(self):
- super(TestScatterProfileToolBar, self).setUp()
- self.plot = PlotWindow()
-
- self.manager = manager.ProfileManager(plot=self.plot)
- self.manager.setItemType(scatter=True)
- self.manager.setActiveItemTracking(True)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- def tearDown(self):
- del self.manager
- self.qapp.processEvents()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- super(TestScatterProfileToolBar, self).tearDown()
-
- def testHorizontalProfile(self):
- """Test ScatterProfileToolBar horizontal profile"""
-
- roiManager = self.manager.getRoiManager()
-
- # Add a scatter plot
- self.plot.addScatter(
- x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.))
- self.plot.resetZoom(dataMargins=(.1, .1, .1, .1))
- self.qapp.processEvents()
-
- # Set a ROI profile
- roi = rois.ProfileScatterHorizontalLineROI()
- roi.setPosition(0.5)
- roi.setNPoints(8)
- roiManager.addRoi(roi)
-
- # Wait for async interpolator init
- for _ in range(20):
- self.qWait(200)
- if not self.manager.hasPendingOperations():
- break
- self.qapp.processEvents()
-
- window = roi.getProfileWindow()
- self.assertIsNotNone(window)
- data = window.getProfile()
- self.assertIsInstance(data, core.CurveProfileData)
- self.assertEqual(len(data.coords), 8)
-
- # Check that profile has same limits than Plot
- xLimits = self.plot.getXAxis().getLimits()
- self.assertEqual(data.coords[0], xLimits[0])
- self.assertEqual(data.coords[-1], xLimits[1])
-
- # Clear the profile
- self.manager.clearProfile()
- self.qapp.processEvents()
- self.assertIsNone(roi.getProfileWindow())
-
- def testVerticalProfile(self):
- """Test ScatterProfileToolBar vertical profile"""
-
- roiManager = self.manager.getRoiManager()
-
- # Add a scatter plot
- self.plot.addScatter(
- x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.))
- self.plot.resetZoom(dataMargins=(.1, .1, .1, .1))
- self.qapp.processEvents()
-
- # Set a ROI profile
- roi = rois.ProfileScatterVerticalLineROI()
- roi.setPosition(0.5)
- roi.setNPoints(8)
- roiManager.addRoi(roi)
-
- # Wait for async interpolator init
- for _ in range(10):
- self.qWait(200)
- if not self.manager.hasPendingOperations():
- break
-
- window = roi.getProfileWindow()
- self.assertIsNotNone(window)
- data = window.getProfile()
- self.assertIsInstance(data, core.CurveProfileData)
- self.assertEqual(len(data.coords), 8)
-
- # Check that profile has same limits than Plot
- yLimits = self.plot.getYAxis().getLimits()
- self.assertEqual(data.coords[0], yLimits[0])
- self.assertEqual(data.coords[-1], yLimits[1])
-
- # Check that profile limits are updated when changing limits
- self.plot.getYAxis().setLimits(yLimits[0] + 1, yLimits[1] + 10)
-
- # Wait for async interpolator init
- for _ in range(10):
- self.qWait(200)
- if not self.manager.hasPendingOperations():
- break
-
- yLimits = self.plot.getYAxis().getLimits()
- data = window.getProfile()
- self.assertEqual(data.coords[0], yLimits[0])
- self.assertEqual(data.coords[-1], yLimits[1])
-
- # Clear the profile
- self.manager.clearProfile()
- self.qapp.processEvents()
- self.assertIsNone(roi.getProfileWindow())
-
- def testLineProfile(self):
- """Test ScatterProfileToolBar line profile"""
-
- roiManager = self.manager.getRoiManager()
-
- # Add a scatter plot
- self.plot.addScatter(
- x=(0., 1., 1., 0.), y=(0., 0., 1., 1.), value=(0., 1., 2., 3.))
- self.plot.resetZoom(dataMargins=(.1, .1, .1, .1))
- self.qapp.processEvents()
-
- # Set a ROI profile
- roi = rois.ProfileScatterLineROI()
- roi.setEndPoints(numpy.array([0., 0.]), numpy.array([1., 1.]))
- roi.setNPoints(8)
- roiManager.addRoi(roi)
-
- # Wait for async interpolator init
- for _ in range(10):
- self.qWait(200)
- if not self.manager.hasPendingOperations():
- break
-
- window = roi.getProfileWindow()
- self.assertIsNotNone(window)
- data = window.getProfile()
- self.assertIsInstance(data, core.CurveProfileData)
- self.assertEqual(len(data.coords), 8)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(
- TestScatterProfileToolBar))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/tools/test/testTools.py b/silx/gui/plot/tools/test/testTools.py
deleted file mode 100644
index 70c8105..0000000
--- a/silx/gui/plot/tools/test/testTools.py
+++ /dev/null
@@ -1,147 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2019 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.
-#
-# ###########################################################################*/
-"""Basic tests for silx.gui.plot.tools package"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "02/03/2018"
-
-
-import functools
-import unittest
-import numpy
-
-from silx.utils.testutils import TestLogging
-from silx.gui.utils.testutils import qWaitForWindowExposedAndActivate
-from silx.gui import qt
-from silx.gui.plot import PlotWindow
-from silx.gui.plot import tools
-from silx.gui.plot.test.utils import PlotWidgetTestCase
-
-
-class TestPositionInfo(PlotWidgetTestCase):
- """Tests for PositionInfo widget."""
-
- def _createPlot(self):
- return PlotWindow()
-
- def setUp(self):
- super(TestPositionInfo, self).setUp()
- self.mouseMove(self.plot, pos=(0, 0))
- self.qapp.processEvents()
- self.qWait(100)
-
- def tearDown(self):
- super(TestPositionInfo, self).tearDown()
-
- def _test(self, positionWidget, converterNames, **kwargs):
- """General test of PositionInfo.
-
- - Add it to a toolbar and
- - Move mouse around the center of the PlotWindow.
- """
- toolBar = qt.QToolBar()
- self.plot.addToolBar(qt.Qt.BottomToolBarArea, toolBar)
-
- toolBar.addWidget(positionWidget)
-
- converters = positionWidget.getConverters()
- self.assertEqual(len(converters), len(converterNames))
- for index, name in enumerate(converterNames):
- self.assertEqual(converters[index][0], name)
-
- with TestLogging(tools.__name__, **kwargs):
- # Move mouse to center
- center = self.plot.size() / 2
- self.mouseMove(self.plot, pos=(center.width(), center.height()))
- # Move out
- self.mouseMove(self.plot, pos=(1, 1))
-
- def testDefaultConverters(self):
- """Test PositionInfo with default converters"""
- positionWidget = tools.PositionInfo(plot=self.plot)
- self._test(positionWidget, ('X', 'Y'))
-
- def testCustomConverters(self):
- """Test PositionInfo with custom converters"""
- converters = [
- ('Coords', lambda x, y: (int(x), int(y))),
- ('Radius', lambda x, y: numpy.sqrt(x * x + y * y)),
- ('Angle', lambda x, y: numpy.degrees(numpy.arctan2(y, x)))
- ]
- positionWidget = tools.PositionInfo(plot=self.plot,
- converters=converters)
- self._test(positionWidget, ('Coords', 'Radius', 'Angle'))
-
- def testFailingConverters(self):
- """Test PositionInfo with failing custom converters"""
- def raiseException(x, y):
- raise RuntimeError()
-
- positionWidget = tools.PositionInfo(
- plot=self.plot,
- converters=[('Exception', raiseException)])
- self._test(positionWidget, ['Exception'], error=2)
-
- def testUpdate(self):
- """Test :meth:`PositionInfo.updateInfo`"""
- calls = []
-
- def update(calls, x, y): # Get number of calls
- calls.append((x, y))
- return len(calls)
-
- positionWidget = tools.PositionInfo(
- plot=self.plot,
- converters=[('Call count', functools.partial(update, calls))])
-
- positionWidget.updateInfo()
- self.assertEqual(len(calls), 1)
-
-
-class TestPlotToolsToolbars(PlotWidgetTestCase):
- """Tests toolbars from silx.gui.plot.tools"""
-
- def test(self):
- """"Add all toolbars"""
- for tbClass in (tools.InteractiveModeToolBar,
- tools.ImageToolBar,
- tools.CurveToolBar,
- tools.OutputToolBar):
- tb = tbClass(parent=self.plot, plot=self.plot)
- self.plot.addToolBar(tb)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- # test_suite.addTest(positionInfoTestSuite)
- for testClass in (TestPositionInfo, TestPlotToolsToolbars):
- test_suite.addTest(unittest.defaultTestLoader.loadTestsFromTestCase(
- testClass))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/tools/toolbars.py b/silx/gui/plot/tools/toolbars.py
deleted file mode 100644
index 3df7d06..0000000
--- a/silx/gui/plot/tools/toolbars.py
+++ /dev/null
@@ -1,362 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2018-2020 European Synchrotron Radiation Facility
-#
-# Permission is hereby granted, free of charge, to any person obtaining a copy
-# of this software and associated documentation files (the "Software"), to deal
-# 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 toolbars that work with :class:`PlotWidget`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "01/03/2018"
-
-
-from ... import qt
-from .. import actions
-from ..PlotWidget import PlotWidget
-from .. import PlotToolButtons
-from ....utils.deprecation import deprecated
-
-
-class InteractiveModeToolBar(qt.QToolBar):
- """Toolbar with interactive mode actions
-
- :param parent: See :class:`QWidget`
- :param silx.gui.plot.PlotWidget plot: PlotWidget to control
- :param str title: Title of the toolbar.
- """
-
- def __init__(self, parent=None, plot=None, title='Plot Interaction'):
- super(InteractiveModeToolBar, self).__init__(title, parent)
-
- assert isinstance(plot, PlotWidget)
-
- self._zoomModeAction = actions.mode.ZoomModeAction(
- parent=self, plot=plot)
- self.addAction(self._zoomModeAction)
-
- self._panModeAction = actions.mode.PanModeAction(
- parent=self, plot=plot)
- self.addAction(self._panModeAction)
-
- def getZoomModeAction(self):
- """Returns the zoom mode QAction.
-
- :rtype: PlotAction
- """
- return self._zoomModeAction
-
- def getPanModeAction(self):
- """Returns the pan mode QAction
-
- :rtype: PlotAction
- """
- return self._panModeAction
-
-
-class OutputToolBar(qt.QToolBar):
- """Toolbar providing icons to copy, save and print a PlotWidget
-
- :param parent: See :class:`QWidget`
- :param silx.gui.plot.PlotWidget plot: PlotWidget to control
- :param str title: Title of the toolbar.
- """
-
- def __init__(self, parent=None, plot=None, title='Plot Output'):
- super(OutputToolBar, self).__init__(title, parent)
-
- assert isinstance(plot, PlotWidget)
-
- self._copyAction = actions.io.CopyAction(parent=self, plot=plot)
- self.addAction(self._copyAction)
-
- self._saveAction = actions.io.SaveAction(parent=self, plot=plot)
- self.addAction(self._saveAction)
-
- self._printAction = actions.io.PrintAction(parent=self, plot=plot)
- self.addAction(self._printAction)
-
- def getCopyAction(self):
- """Returns the QAction performing copy to clipboard of the PlotWidget
-
- :rtype: PlotAction
- """
- return self._copyAction
-
- def getSaveAction(self):
- """Returns the QAction performing save to file of the PlotWidget
-
- :rtype: PlotAction
- """
- return self._saveAction
-
- def getPrintAction(self):
- """Returns the QAction performing printing of the PlotWidget
-
- :rtype: PlotAction
- """
- return self._printAction
-
-
-class ImageToolBar(qt.QToolBar):
- """Toolbar providing PlotAction suited when displaying images
-
- :param parent: See :class:`QWidget`
- :param silx.gui.plot.PlotWidget plot: PlotWidget to control
- :param str title: Title of the toolbar.
- """
-
- def __init__(self, parent=None, plot=None, title='Image'):
- super(ImageToolBar, self).__init__(title, parent)
-
- assert isinstance(plot, PlotWidget)
-
- self._resetZoomAction = actions.control.ResetZoomAction(
- parent=self, plot=plot)
- self.addAction(self._resetZoomAction)
-
- self._colormapAction = actions.control.ColormapAction(
- parent=self, plot=plot)
- self.addAction(self._colormapAction)
-
- self._keepDataAspectRatioButton = PlotToolButtons.AspectToolButton(
- parent=self, plot=plot)
- self.addWidget(self._keepDataAspectRatioButton)
-
- self._yAxisInvertedButton = PlotToolButtons.YAxisOriginToolButton(
- parent=self, plot=plot)
- self.addWidget(self._yAxisInvertedButton)
-
- def getResetZoomAction(self):
- """Returns the QAction to reset the zoom.
-
- :rtype: PlotAction
- """
- return self._resetZoomAction
-
- def getColormapAction(self):
- """Returns the QAction to control the colormap.
-
- :rtype: PlotAction
- """
- return self._colormapAction
-
- def getKeepDataAspectRatioButton(self):
- """Returns the QToolButton controlling data aspect ratio.
-
- :rtype: QToolButton
- """
- return self._keepDataAspectRatioButton
-
- def getYAxisInvertedButton(self):
- """Returns the QToolButton controlling Y axis orientation.
-
- :rtype: QToolButton
- """
- return self._yAxisInvertedButton
-
-
-class CurveToolBar(qt.QToolBar):
- """Toolbar providing PlotAction suited when displaying curves
-
- :param parent: See :class:`QWidget`
- :param silx.gui.plot.PlotWidget plot: PlotWidget to control
- :param str title: Title of the toolbar.
- """
-
- def __init__(self, parent=None, plot=None, title='Image'):
- super(CurveToolBar, self).__init__(title, parent)
-
- assert isinstance(plot, PlotWidget)
-
- self._resetZoomAction = actions.control.ResetZoomAction(
- parent=self, plot=plot)
- self.addAction(self._resetZoomAction)
-
- self._xAxisAutoScaleAction = actions.control.XAxisAutoScaleAction(
- parent=self, plot=plot)
- self.addAction(self._xAxisAutoScaleAction)
-
- self._yAxisAutoScaleAction = actions.control.YAxisAutoScaleAction(
- parent=self, plot=plot)
- self.addAction(self._yAxisAutoScaleAction)
-
- self._xAxisLogarithmicAction = actions.control.XAxisLogarithmicAction(
- parent=self, plot=plot)
- self.addAction(self._xAxisLogarithmicAction)
-
- self._yAxisLogarithmicAction = actions.control.YAxisLogarithmicAction(
- parent=self, plot=plot)
- self.addAction(self._yAxisLogarithmicAction)
-
- self._gridAction = actions.control.GridAction(
- parent=self, plot=plot)
- self.addAction(self._gridAction)
-
- self._curveStyleAction = actions.control.CurveStyleAction(
- parent=self, plot=plot)
- self.addAction(self._curveStyleAction)
-
- def getResetZoomAction(self):
- """Returns the QAction to reset the zoom.
-
- :rtype: PlotAction
- """
- return self._resetZoomAction
-
- def getXAxisAutoScaleAction(self):
- """Returns the QAction to toggle X axis autoscale.
-
- :rtype: PlotAction
- """
- return self._xAxisAutoScaleAction
-
- def getYAxisAutoScaleAction(self):
- """Returns the QAction to toggle Y axis autoscale.
-
- :rtype: PlotAction
- """
- return self._yAxisAutoScaleAction
-
- def getXAxisLogarithmicAction(self):
- """Returns the QAction to toggle X axis log/linear scale.
-
- :rtype: PlotAction
- """
- return self._xAxisLogarithmicAction
-
- def getYAxisLogarithmicAction(self):
- """Returns the QAction to toggle Y axis log/linear scale.
-
- :rtype: PlotAction
- """
- return self._yAxisLogarithmicAction
-
- def getGridAction(self):
- """Returns the action to toggle the plot grid.
-
- :rtype: PlotAction
- """
- return self._gridAction
-
- def getCurveStyleAction(self):
- """Returns the QAction to change the style of all curves.
-
- :rtype: PlotAction
- """
- return self._curveStyleAction
-
-
-class ScatterToolBar(qt.QToolBar):
- """Toolbar providing PlotAction suited when displaying scatter plot
-
- :param parent: See :class:`QWidget`
- :param silx.gui.plot.PlotWidget plot: PlotWidget to control
- :param str title: Title of the toolbar.
- """
-
- def __init__(self, parent=None, plot=None, title='Scatter Tools'):
- super(ScatterToolBar, self).__init__(title, parent)
-
- assert isinstance(plot, PlotWidget)
-
- self._resetZoomAction = actions.control.ResetZoomAction(
- parent=self, plot=plot)
- self.addAction(self._resetZoomAction)
-
- self._xAxisLogarithmicAction = actions.control.XAxisLogarithmicAction(
- parent=self, plot=plot)
- self.addAction(self._xAxisLogarithmicAction)
-
- self._yAxisLogarithmicAction = actions.control.YAxisLogarithmicAction(
- parent=self, plot=plot)
- self.addAction(self._yAxisLogarithmicAction)
-
- self._keepDataAspectRatioButton = PlotToolButtons.AspectToolButton(
- parent=self, plot=plot)
- self.addWidget(self._keepDataAspectRatioButton)
-
- self._gridAction = actions.control.GridAction(
- parent=self, plot=plot)
- self.addAction(self._gridAction)
-
- self._colormapAction = actions.control.ColormapAction(
- parent=self, plot=plot)
- self.addAction(self._colormapAction)
-
- self._visualizationToolButton = \
- PlotToolButtons.ScatterVisualizationToolButton(parent=self, plot=plot)
- self.addWidget(self._visualizationToolButton)
-
- def getResetZoomAction(self):
- """Returns the QAction to reset the zoom.
-
- :rtype: PlotAction
- """
- return self._resetZoomAction
-
- def getXAxisLogarithmicAction(self):
- """Returns the QAction to toggle X axis log/linear scale.
-
- :rtype: PlotAction
- """
- return self._xAxisLogarithmicAction
-
- def getYAxisLogarithmicAction(self):
- """Returns the QAction to toggle Y axis log/linear scale.
-
- :rtype: PlotAction
- """
- return self._yAxisLogarithmicAction
-
- def getGridAction(self):
- """Returns the action to toggle the plot grid.
-
- :rtype: PlotAction
- """
- return self._gridAction
-
- def getColormapAction(self):
- """Returns the QAction to control the colormap.
-
- :rtype: PlotAction
- """
- return self._colormapAction
-
- def getKeepDataAspectRatioButton(self):
- """Returns the QToolButton controlling data aspect ratio.
-
- :rtype: QToolButton
- """
- return self._keepDataAspectRatioButton
-
- def getScatterVisualizationToolButton(self):
- """Returns the QToolButton controlling the visualization mode.
-
- :rtype: ScatterVisualizationToolButton
- """
- return self._visualizationToolButton
-
- @deprecated(replacement='getScatterVisualizationToolButton',
- since_version='0.11.0')
- def getSymbolToolButton(self):
- return self.getScatterVisualizationToolButton()
diff --git a/silx/gui/plot/utils/__init__.py b/silx/gui/plot/utils/__init__.py
deleted file mode 100644
index 3187f6b..0000000
--- a/silx/gui/plot/utils/__init__.py
+++ /dev/null
@@ -1,30 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""Utils module for plot.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "29/06/2017"
diff --git a/silx/gui/plot/utils/axis.py b/silx/gui/plot/utils/axis.py
deleted file mode 100644
index 693e8eb..0000000
--- a/silx/gui/plot/utils/axis.py
+++ /dev/null
@@ -1,403 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2018 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 contains utils class for axes management.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "20/11/2018"
-
-import functools
-import logging
-from contextlib import contextmanager
-import weakref
-import silx.utils.weakref as silxWeakref
-from silx.gui.plot.items.axis import Axis, XAxis, YAxis
-
-try:
- from ...qt.inspect import isValid as _isQObjectValid
-except ImportError: # PySide(1) fallback
- def _isQObjectValid(obj):
- return True
-
-
-_logger = logging.getLogger(__name__)
-
-
-class SyncAxes(object):
- """Synchronize a set of plot axes together.
-
- It is created with the expected axes and starts to synchronize them.
-
- It can be customized to synchronize limits, scale, and direction of axes
- together. By default everything is synchronized.
-
- The API :meth:`start` and :meth:`stop` can be used to enable/disable the
- synchronization while this object is still alive.
-
- If this object is destroyed the synchronization stop.
-
- .. versionadded:: 0.6
- """
-
- def __init__(self, axes,
- syncLimits=True,
- syncScale=True,
- syncDirection=True,
- syncCenter=False,
- syncZoom=False,
- filterHiddenPlots=False
- ):
- """
- Constructor
-
- :param list(Axis) axes: A list of axes to synchronize together
- :param bool syncLimits: Synchronize axes limits
- :param bool syncScale: Synchronize axes scale
- :param bool syncDirection: Synchronize axes direction
- :param bool syncCenter: Synchronize the center of the axes in the center
- of the plots
- :param bool syncZoom: Synchronize the zoom of the plot
- :param bool filterHiddenPlots: True to avoid updating hidden plots.
- Default: False.
- """
- object.__init__(self)
-
- def implies(x, y): return bool(y ** x)
-
- assert(implies(syncZoom, not syncLimits))
- assert(implies(syncCenter, not syncLimits))
- assert(implies(syncLimits, not syncCenter))
- assert(implies(syncLimits, not syncZoom))
-
- self.__filterHiddenPlots = filterHiddenPlots
- self.__locked = False
- self.__axisRefs = []
- self.__syncLimits = syncLimits
- self.__syncScale = syncScale
- self.__syncDirection = syncDirection
- self.__syncCenter = syncCenter
- self.__syncZoom = syncZoom
- self.__callbacks = None
- self.__lastMainAxis = None
-
- for axis in axes:
- self.addAxis(axis)
-
- self.start()
-
- def start(self):
- """Start synchronizing axes together.
-
- The first axis is used as the reference for the first synchronization.
- After that, any changes to any axes will be used to synchronize other
- axes.
- """
- if self.isSynchronizing():
- raise RuntimeError("Axes already synchronized")
- self.__callbacks = {}
-
- axes = self.__getAxes()
-
- # register callback for further sync
- for axis in axes:
- self.__connectAxes(axis)
- self.synchronize()
-
- def isSynchronizing(self):
- """Returns true if events are connected to the axes to synchronize them
- all together
-
- :rtype: bool
- """
- return self.__callbacks is not None
-
- def __connectAxes(self, axis):
- refAxis = weakref.ref(axis)
- callbacks = []
- if self.__syncLimits:
- # the weakref is needed to be able ignore self references
- callback = silxWeakref.WeakMethodProxy(self.__axisLimitsChanged)
- callback = functools.partial(callback, refAxis)
- sig = axis.sigLimitsChanged
- sig.connect(callback)
- callbacks.append(("sigLimitsChanged", callback))
- elif self.__syncCenter and self.__syncZoom:
- # the weakref is needed to be able ignore self references
- callback = silxWeakref.WeakMethodProxy(self.__axisCenterAndZoomChanged)
- callback = functools.partial(callback, refAxis)
- sig = axis.sigLimitsChanged
- sig.connect(callback)
- callbacks.append(("sigLimitsChanged", callback))
- elif self.__syncZoom:
- raise NotImplementedError()
- elif self.__syncCenter:
- # the weakref is needed to be able ignore self references
- callback = silxWeakref.WeakMethodProxy(self.__axisCenterChanged)
- callback = functools.partial(callback, refAxis)
- sig = axis.sigLimitsChanged
- sig.connect(callback)
- callbacks.append(("sigLimitsChanged", callback))
- if self.__syncScale:
- # the weakref is needed to be able ignore self references
- callback = silxWeakref.WeakMethodProxy(self.__axisScaleChanged)
- callback = functools.partial(callback, refAxis)
- sig = axis.sigScaleChanged
- sig.connect(callback)
- callbacks.append(("sigScaleChanged", callback))
- if self.__syncDirection:
- # the weakref is needed to be able ignore self references
- callback = silxWeakref.WeakMethodProxy(self.__axisInvertedChanged)
- callback = functools.partial(callback, refAxis)
- sig = axis.sigInvertedChanged
- sig.connect(callback)
- callbacks.append(("sigInvertedChanged", callback))
-
- if self.__filterHiddenPlots:
- # the weakref is needed to be able ignore self references
- callback = silxWeakref.WeakMethodProxy(self.__axisVisibilityChanged)
- callback = functools.partial(callback, refAxis)
- plot = axis._getPlot()
- plot.sigVisibilityChanged.connect(callback)
- callbacks.append(("sigVisibilityChanged", callback))
-
- self.__callbacks[refAxis] = callbacks
-
- def __disconnectAxes(self, axis):
- if axis is not None and _isQObjectValid(axis):
- ref = weakref.ref(axis)
- callbacks = self.__callbacks.pop(ref)
- for sigName, callback in callbacks:
- if sigName == "sigVisibilityChanged":
- obj = axis._getPlot()
- else:
- obj = axis
- if obj is not None:
- sig = getattr(obj, sigName)
- sig.disconnect(callback)
-
- def addAxis(self, axis):
- """Add a new axes to synchronize.
-
- :param ~silx.gui.plot.items.Axis axis: The axis to synchronize
- """
- self.__axisRefs.append(weakref.ref(axis))
- if self.isSynchronizing():
- self.__connectAxes(axis)
- # This could be done faster as only this axis have to be fixed
- self.synchronize()
-
- def removeAxis(self, axis):
- """Remove an axis from the synchronized axes.
-
- :param ~silx.gui.plot.items.Axis axis: The axis to remove
- """
- ref = weakref.ref(axis)
- self.__axisRefs.remove(ref)
- if self.isSynchronizing():
- self.__disconnectAxes(axis)
-
- def synchronize(self, mainAxis=None):
- """Synchronize programatically all the axes.
-
- :param ~silx.gui.plot.items.Axis mainAxis:
- The axis to take as reference (Default: the first axis).
- """
- # sync the current state
- axes = self.__getAxes()
- if len(axes) == 0:
- return
-
- if mainAxis is None:
- mainAxis = axes[0]
-
- refMainAxis = weakref.ref(mainAxis)
- if self.__syncLimits:
- self.__axisLimitsChanged(refMainAxis, *mainAxis.getLimits())
- elif self.__syncCenter and self.__syncZoom:
- self.__axisCenterAndZoomChanged(refMainAxis, *mainAxis.getLimits())
- elif self.__syncCenter:
- self.__axisCenterChanged(refMainAxis, *mainAxis.getLimits())
- if self.__syncScale:
- self.__axisScaleChanged(refMainAxis, mainAxis.getScale())
- if self.__syncDirection:
- self.__axisInvertedChanged(refMainAxis, mainAxis.isInverted())
-
- def stop(self):
- """Stop the synchronization of the axes"""
- if not self.isSynchronizing():
- raise RuntimeError("Axes not synchronized")
- for ref in list(self.__callbacks.keys()):
- axis = ref()
- self.__disconnectAxes(axis)
- self.__callbacks = None
-
- def __del__(self):
- """Destructor"""
- # clean up references
- if self.__callbacks is not None:
- self.stop()
-
- def __getAxes(self):
- """Returns list of existing axes.
-
- :rtype: List[Axis]
- """
- axes = [ref() for ref in self.__axisRefs]
- return [axis for axis in axes if axis is not None]
-
- @contextmanager
- def __inhibitSignals(self):
- self.__locked = True
- yield
- self.__locked = False
-
- def __axesToUpdate(self, changedAxis):
- for axis in self.__getAxes():
- if axis is changedAxis:
- continue
- if self.__filterHiddenPlots:
- plot = axis._getPlot()
- if not plot.isVisible():
- continue
- yield axis
-
- def __axisVisibilityChanged(self, changedAxis, isVisible):
- if not isVisible:
- return
- if self.__locked:
- return
- changedAxis = changedAxis()
- if self.__lastMainAxis is None:
- self.__lastMainAxis = self.__axisRefs[0]
- mainAxis = self.__lastMainAxis
- mainAxis = mainAxis()
- self.synchronize(mainAxis=mainAxis)
- # force back the main axis
- self.__lastMainAxis = weakref.ref(mainAxis)
-
- def __getAxesCenter(self, axis, vmin, vmax):
- """Returns the value displayed in the center of this axis range.
-
- :rtype: float
- """
- scale = axis.getScale()
- if scale == Axis.LINEAR:
- center = (vmin + vmax) * 0.5
- else:
- raise NotImplementedError("Log scale not implemented")
- return center
-
- def __getRangeInPixel(self, axis):
- """Returns the size of the axis in pixel"""
- bounds = axis._getPlot().getPlotBoundsInPixels()
- # bounds: left, top, width, height
- if isinstance(axis, XAxis):
- return bounds[2]
- elif isinstance(axis, YAxis):
- return bounds[3]
- else:
- assert(False)
-
- def __getLimitsFromCenter(self, axis, pos, pixelSize=None):
- """Returns the limits to apply to this axis to move the `pos` into the
- center of this axis.
-
- :param Axis axis:
- :param float pos: Position in the center of the computed limits
- :param Union[None,float] pixelSize: Pixel size to apply to compute the
- limits. If `None` the current pixel size is applyed.
- """
- scale = axis.getScale()
- if scale == Axis.LINEAR:
- if pixelSize is None:
- # Use the current pixel size of the axis
- limits = axis.getLimits()
- valueRange = limits[0] - limits[1]
- a = pos - valueRange * 0.5
- b = pos + valueRange * 0.5
- else:
- pixelRange = self.__getRangeInPixel(axis)
- a = pos - pixelRange * 0.5 * pixelSize
- b = pos + pixelRange * 0.5 * pixelSize
-
- else:
- raise NotImplementedError("Log scale not implemented")
- if a > b:
- return b, a
- return a, b
-
- def __axisLimitsChanged(self, changedAxis, vmin, vmax):
- if self.__locked:
- return
- self.__lastMainAxis = changedAxis
- changedAxis = changedAxis()
- with self.__inhibitSignals():
- for axis in self.__axesToUpdate(changedAxis):
- axis.setLimits(vmin, vmax)
-
- def __axisCenterAndZoomChanged(self, changedAxis, vmin, vmax):
- if self.__locked:
- return
- self.__lastMainAxis = changedAxis
- changedAxis = changedAxis()
- with self.__inhibitSignals():
- center = self.__getAxesCenter(changedAxis, vmin, vmax)
- pixelRange = self.__getRangeInPixel(changedAxis)
- if pixelRange == 0:
- return
- pixelSize = (vmax - vmin) / pixelRange
- for axis in self.__axesToUpdate(changedAxis):
- vmin, vmax = self.__getLimitsFromCenter(axis, center, pixelSize)
- axis.setLimits(vmin, vmax)
-
- def __axisCenterChanged(self, changedAxis, vmin, vmax):
- if self.__locked:
- return
- self.__lastMainAxis = changedAxis
- changedAxis = changedAxis()
- with self.__inhibitSignals():
- center = self.__getAxesCenter(changedAxis, vmin, vmax)
- for axis in self.__axesToUpdate(changedAxis):
- vmin, vmax = self.__getLimitsFromCenter(axis, center)
- axis.setLimits(vmin, vmax)
-
- def __axisScaleChanged(self, changedAxis, scale):
- if self.__locked:
- return
- self.__lastMainAxis = changedAxis
- changedAxis = changedAxis()
- with self.__inhibitSignals():
- for axis in self.__axesToUpdate(changedAxis):
- axis.setScale(scale)
-
- def __axisInvertedChanged(self, changedAxis, isInverted):
- if self.__locked:
- return
- self.__lastMainAxis = changedAxis
- changedAxis = changedAxis()
- with self.__inhibitSignals():
- for axis in self.__axesToUpdate(changedAxis):
- axis.setInverted(isInverted)
diff --git a/silx/gui/plot/utils/intersections.py b/silx/gui/plot/utils/intersections.py
deleted file mode 100644
index 53f2546..0000000
--- a/silx/gui/plot/utils/intersections.py
+++ /dev/null
@@ -1,101 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2017-2018 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 contains utils class for axes management.
-"""
-
-__authors__ = ["H. Payno", ]
-__license__ = "MIT"
-__date__ = "18/05/2020"
-
-
-import numpy
-
-
-def lines_intersection(line1_pt1, line1_pt2, line2_pt1, line2_pt2):
- """
- line segment intersection using vectors (Computer Graphics by F.S. Hill)
-
- :param tuple line1_pt1:
- :param tuple line1_pt2:
- :param tuple line2_pt1:
- :param tuple line2_pt2:
- :return: Union[None,numpy.array]
- """
- dir_line1 = line1_pt2[0] - line1_pt1[0], line1_pt2[1] - line1_pt1[1]
- dir_line2 = line2_pt2[0] - line2_pt1[0], line2_pt2[1] - line2_pt1[1]
- dp = line1_pt1 - line2_pt1
-
- def perp(a):
- b = numpy.empty_like(a)
- b[0] = -a[1]
- b[1] = a[0]
- return b
-
- dap = perp(dir_line1)
- denom = numpy.dot(dap, dir_line2)
- num = numpy.dot(dap, dp)
- if denom == 0:
- return None
- return (
- (num / denom.astype(float)) * dir_line2[0] + line2_pt1[0],
- (num / denom.astype(float)) * dir_line2[1] + line2_pt1[1])
-
-
-def segments_intersection(seg1_start_pt, seg1_end_pt, seg2_start_pt,
- seg2_end_pt):
- """
- Compute intersection between two segments
-
- :param seg1_start_pt:
- :param seg1_end_pt:
- :param seg2_start_pt:
- :param seg2_end_pt:
- :return: numpy.array if an intersection exists, else None
- :rtype: Union[None,numpy.array]
- """
- intersection = lines_intersection(line1_pt1=seg1_start_pt,
- line1_pt2=seg1_end_pt,
- line2_pt1=seg2_start_pt,
- line2_pt2=seg2_end_pt)
- if intersection is not None:
- max_x_seg1 = max(seg1_start_pt[0], seg1_end_pt[0])
- max_x_seg2 = max(seg2_start_pt[0], seg2_end_pt[0])
- max_y_seg1 = max(seg1_start_pt[1], seg1_end_pt[1])
- max_y_seg2 = max(seg2_start_pt[1], seg2_end_pt[1])
-
- min_x_seg1 = min(seg1_start_pt[0], seg1_end_pt[0])
- min_x_seg2 = min(seg2_start_pt[0], seg2_end_pt[0])
- min_y_seg1 = min(seg1_start_pt[1], seg1_end_pt[1])
- min_y_seg2 = min(seg2_start_pt[1], seg2_end_pt[1])
-
- min_tmp_x = max(min_x_seg1, min_x_seg2)
- max_tmp_x = min(max_x_seg1, max_x_seg2)
- min_tmp_y = max(min_y_seg1, min_y_seg2)
- max_tmp_y = min(max_y_seg1, max_y_seg2)
- if (min_tmp_x <= intersection[0] <= max_tmp_x and
- min_tmp_y <= intersection[1] <= max_tmp_y):
- return intersection
- else:
- return None