summaryrefslogtreecommitdiff
path: root/silx/gui/plot
diff options
context:
space:
mode:
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.py44
-rw-r--r--silx/gui/plot/ColormapDialog.py43
-rw-r--r--silx/gui/plot/Colors.py90
-rw-r--r--silx/gui/plot/CompareImages.py1190
-rw-r--r--silx/gui/plot/ComplexImageView.py492
-rw-r--r--silx/gui/plot/CurvesROIWidget.py1044
-rw-r--r--silx/gui/plot/ImageView.py871
-rw-r--r--silx/gui/plot/Interaction.py300
-rw-r--r--silx/gui/plot/ItemsSelectionDialog.py282
-rw-r--r--silx/gui/plot/LegendSelector.py1193
-rw-r--r--silx/gui/plot/LimitsHistory.py83
-rw-r--r--silx/gui/plot/MaskToolsWidget.py774
-rw-r--r--silx/gui/plot/PlotActions.py67
-rw-r--r--silx/gui/plot/PlotEvents.py166
-rw-r--r--silx/gui/plot/PlotInteraction.py1603
-rw-r--r--silx/gui/plot/PlotToolButtons.py419
-rw-r--r--silx/gui/plot/PlotTools.py43
-rw-r--r--silx/gui/plot/PlotWidget.py3228
-rw-r--r--silx/gui/plot/PlotWindow.py948
-rw-r--r--silx/gui/plot/PrintPreviewToolButton.py351
-rw-r--r--silx/gui/plot/Profile.py810
-rw-r--r--silx/gui/plot/ProfileMainWindow.py115
-rw-r--r--silx/gui/plot/ScatterMaskToolsWidget.py565
-rw-r--r--silx/gui/plot/ScatterView.py355
-rw-r--r--silx/gui/plot/StackView.py1240
-rw-r--r--silx/gui/plot/StatsWidget.py582
-rw-r--r--silx/gui/plot/_BaseMaskToolsWidget.py1167
-rw-r--r--silx/gui/plot/__init__.py71
-rw-r--r--silx/gui/plot/_utils/__init__.py93
-rw-r--r--silx/gui/plot/_utils/dtime_ticklayout.py438
-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
-rw-r--r--silx/gui/plot/actions/control.py604
-rw-r--r--silx/gui/plot/actions/fit.py186
-rw-r--r--silx/gui/plot/actions/histogram.py146
-rw-r--r--silx/gui/plot/actions/io.py743
-rw-r--r--silx/gui/plot/actions/medfilt.py147
-rw-r--r--silx/gui/plot/actions/mode.py104
-rw-r--r--silx/gui/plot/backends/BackendBase.py548
-rw-r--r--silx/gui/plot/backends/BackendMatplotlib.py1139
-rw-r--r--silx/gui/plot/backends/BackendOpenGL.py1725
-rw-r--r--silx/gui/plot/backends/__init__.py29
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotCurve.py1151
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotFrame.py1116
-rw-r--r--silx/gui/plot/backends/glutils/GLPlotImage.py674
-rw-r--r--silx/gui/plot/backends/glutils/GLSupport.py201
-rw-r--r--silx/gui/plot/backends/glutils/GLText.py270
-rw-r--r--silx/gui/plot/backends/glutils/GLTexture.py239
-rw-r--r--silx/gui/plot/backends/glutils/PlotImageFile.py153
-rw-r--r--silx/gui/plot/backends/glutils/__init__.py44
-rw-r--r--silx/gui/plot/items/__init__.py49
-rw-r--r--silx/gui/plot/items/axis.py567
-rw-r--r--silx/gui/plot/items/complex.py356
-rw-r--r--silx/gui/plot/items/core.py1036
-rw-r--r--silx/gui/plot/items/curve.py362
-rw-r--r--silx/gui/plot/items/histogram.py332
-rw-r--r--silx/gui/plot/items/image.py421
-rw-r--r--silx/gui/plot/items/marker.py261
-rw-r--r--silx/gui/plot/items/roi.py1416
-rw-r--r--silx/gui/plot/items/scatter.py193
-rw-r--r--silx/gui/plot/items/shape.py121
-rw-r--r--silx/gui/plot/matplotlib/Colormap.py232
-rw-r--r--silx/gui/plot/matplotlib/__init__.py101
-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.py491
-rw-r--r--silx/gui/plot/stats/statshandler.py190
-rw-r--r--silx/gui/plot/test/__init__.py90
-rw-r--r--silx/gui/plot/test/testAlphaSlider.py221
-rw-r--r--silx/gui/plot/test/testColorBar.py351
-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.py183
-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.py249
-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.py294
-rw-r--r--silx/gui/plot/test/testPixelIntensityHistoAction.py104
-rw-r--r--silx/gui/plot/test/testPlotInteraction.py168
-rw-r--r--silx/gui/plot/test/testPlotWidget.py1539
-rw-r--r--silx/gui/plot/test/testPlotWidgetNoBackend.py633
-rw-r--r--silx/gui/plot/test/testPlotWindow.py138
-rw-r--r--silx/gui/plot/test/testProfile.py291
-rw-r--r--silx/gui/plot/test/testSaveAction.py125
-rw-r--r--silx/gui/plot/test/testScatterMaskToolsWidget.py313
-rw-r--r--silx/gui/plot/test/testScatterView.py134
-rw-r--r--silx/gui/plot/test/testStackView.py252
-rw-r--r--silx/gui/plot/test/testStats.py562
-rw-r--r--silx/gui/plot/test/testUtilsAxis.py167
-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.py347
-rw-r--r--silx/gui/plot/tools/__init__.py50
-rw-r--r--silx/gui/plot/tools/profile/ScatterProfileToolBar.py431
-rw-r--r--silx/gui/plot/tools/profile/_BaseProfileToolBar.py430
-rw-r--r--silx/gui/plot/tools/profile/__init__.py38
-rw-r--r--silx/gui/plot/tools/roi.py934
-rw-r--r--silx/gui/plot/tools/test/__init__.py50
-rw-r--r--silx/gui/plot/tools/test/testCurveLegendsWidget.py125
-rw-r--r--silx/gui/plot/tools/test/testROI.py456
-rw-r--r--silx/gui/plot/tools/test/testScatterProfileToolBar.py216
-rw-r--r--silx/gui/plot/tools/test/testTools.py175
-rw-r--r--silx/gui/plot/tools/toolbars.py356
-rw-r--r--silx/gui/plot/utils/__init__.py30
-rw-r--r--silx/gui/plot/utils/axis.py199
117 files changed, 0 insertions, 47208 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 fd4d34e..0000000
--- a/silx/gui/plot/ColorBar.py
+++ /dev/null
@@ -1,881 +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.
-#
-# ###########################################################################*/
-"""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 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"""
- plot = self.getPlot()
- if plot is not None and self._isConnected:
- self._isConnected = False
- 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):
- # isHidden looks to be always synchronized, while isVisible is not
- wasHidden = self.isHidden()
- qt.QWidget.setVisible(self, isVisible)
- if wasHidden != self.isHidden():
- self.sigVisibleChanged.emit(not self.isHidden())
-
- 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 numpy.ndarray data: the data to display, needed if the colormap
- require an autoscale
- """
- self._data = data
- self.getColorScaleBar().setColormap(colormap=colormap,
- data=data)
- if self._colormap is not None:
- self._colormap.sigChanged.disconnect(self._colormapHasChanged)
- self._colormap = colormap
- if self._colormap is 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
- activeScatter = plot._getActiveItem(kind='scatter')
-
- self.setColormap(colormap=activeScatter.getColormap(),
- data=activeScatter.getValueData(copy=False))
-
- 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().getData(copy=False)
-
- # RGB(A) image, display default colormap
- if image.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=plot.getActiveImage().getColormap(),
- data=image)
-
- def _defaultColormapChanged(self, event):
- """Handle plot default colormap changed"""
- if (event['event'] == 'defaultColormapChanged' and
- self.getPlot().getActiveImage() is None):
- # No active image, take default colormap update into account
- self._syncWithDefaultColormap()
-
- def _syncWithDefaultColormap(self, data=None):
- """Update colorbar according to plot default colormap"""
- self.setColormap(self.getPlot().getDefaultColormap(), data)
-
- 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)
- else:
- vmin, vmax = colors.DEFAULT_MIN_LIN, colors.DEFAULT_MAX_LIN
-
- norm = colormap.getNormalization() if colormap else colors.Colormap.LINEAR
- self.tickbar = _TickBar(vmin=vmin,
- vmax=vmax,
- norm=norm,
- parent=self,
- displayValues=displayTicksValues,
- margin=ColorScaleBar._TEXT_MARGIN)
-
- self.layout().addWidget(self.tickbar, 1, 0, 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 numpy.ndarray data: the data to display, needed if the colormap
- require an autoscale
- """
- self.colorScale.setColormap(colormap, data)
-
- if colormap is not None:
- vmin, vmax = colormap.getColormapRange(data)
- norm = colormap.getNormalization()
- else:
- vmin, vmax = None, None
- norm = None
-
- self.tickbar.update(vmin=vmin,
- vmax=vmax,
- norm=norm)
- self._setMinMaxLabels(vmin, vmax)
-
- def setMinMaxVisible(self, val=True):
- """Change visibility of the min label and the max label
-
- :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.
-
- .. 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 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 = max(0.0, value)
- value = min(value, 1.0)
-
- vmin = self.vmin
- vmax = self.vmax
- if colormap.getNormalization() == colors.Colormap.LINEAR:
- return vmin + (vmax - vmin) * value
- elif colormap.getNormalization() == colors.Colormap.LOGARITHM:
- rpos = (numpy.log10(vmax) - numpy.log10(vmin)) * value + numpy.log10(vmin)
- return numpy.power(10., rpos)
- else:
- err = "normalization type (%s) is not managed by the _ColorScale Widget" % colormap['normalization']
- raise ValueError(err)
-
- 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 = 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 str norm: normalization type to be displayed. Valid values are
- 'linear' and 'log'
- :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, norm, parent=None, displayValues=True,
- nticks=None, margin=5):
- super(_TickBar, self).__init__(parent)
- self.margin = margin
- self._nticks = None
- self.ticks = ()
- self.subTicks = ()
- self._forcedDisplayType = None
- self.ticksDensity = _TickBar.DEFAULT_TICK_DENSITY
-
- self._vmin = vmin
- self._vmax = vmax
- self._norm = norm
- 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, norm):
- self._vmin = vmin
- self._vmax = vmax
- self._norm = norm
- 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 self._norm == colors.Colormap.LOGARITHM:
- self._computeTicksLog(nticks)
- elif self._norm == colors.Colormap.LINEAR:
- self._computeTicksLin(nticks)
- else:
- err = 'TickBar - Wrong normalization %s' % self._norm
- raise ValueError(err)
- # 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._norm == colors.Colormap.LINEAR:
- return 1 - (val - self._vmin) / (self._vmax - self._vmin)
- elif self._norm == colors.Colormap.LOGARITHM:
- return 1 - (numpy.log10(val) - numpy.log10(self._vmin)) / (numpy.log10(self._vmax) - numpy.log(self._vmin))
- else:
- raise ValueError('Norm is not recognized')
-
- 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 = viewportHeight * relativePos
- height += self.margin
- lineWidth = _TickBar._LINE_WIDTH
- if majorTick is False:
- lineWidth /= 2
-
- painter.drawLine(qt.QLine(self.width() - lineWidth,
- height,
- self.width(),
- height))
-
- if self.displayValues and majorTick is True:
- painter.drawText(qt.QPoint(0.0, 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 is 'std':
- return self._getStandardFormat()
- elif self._forcedDisplayType is '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 want to use durint the painting
- """
- form = self._getStandardFormat()
-
- fm = qt.QFontMetrics(font)
- width = 0
- for tick in self.ticks:
- width = max(fm.width(form.format(tick)), width)
-
- # if the length of the string are too long we are mooving 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 e797d89..0000000
--- a/silx/gui/plot/Colormap.py
+++ /dev/null
@@ -1,44 +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
-"""
-
-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.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 88b257d..0000000
--- a/silx/gui/plot/CompareImages.py
+++ /dev/null
@@ -1,1190 +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.
-#
-# ###########################################################################*/
-"""A widget dedicated to compare 2 images.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "23/07/2018"
-
-
-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
-from silx.third_party import enum
-
-_logger = logging.getLogger(__name__)
-
-from silx.opencl import ocl
-if ocl is not None:
- from silx.opencl import sift
-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"
-
-
-@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)
-
- 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)
-
- 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.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 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()
- 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()
- 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()
- 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],
- legend="keypoints",
- colormap=Colormap("spring"))
-
- def __updateData(self):
- """Compute aligned image when the alignement 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.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 = Colormap(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])
- 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
diff --git a/silx/gui/plot/ComplexImageView.py b/silx/gui/plot/ComplexImageView.py
deleted file mode 100644
index bbcb0a5..0000000
--- a/silx/gui/plot/ComplexImageView.py
+++ /dev/null
@@ -1,492 +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 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 .. 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.Mode.ABSOLUTE, ('math-amplitude', 'Amplitude')),
- (ImageComplexData.Mode.SQUARE_AMPLITUDE,
- ('math-square-amplitude', 'Square amplitude')),
- (ImageComplexData.Mode.PHASE, ('math-phase', 'Phase')),
- (ImageComplexData.Mode.REAL, ('math-real', 'Real part')),
- (ImageComplexData.Mode.IMAGINARY,
- ('math-imaginary', 'Imaginary part')),
- (ImageComplexData.Mode.AMPLITUDE_PHASE,
- ('math-phase-color', 'Amplitude and Phase')),
- (ImageComplexData.Mode.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.getVisualizationMode())
- 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.Mode.LOG10_AMPLITUDE_PHASE)
-
- def _triggered(self, action):
- """Handle triggering of menu actions"""
- actionText = action.text()
-
- if actionText == self._RANGE_DIALOG_TEXT: # Show dialog
- # Get amplitude range
- data = self._plot2DComplex.getData(copy=False)
-
- if data.size > 0:
- absolute = numpy.absolute(data)
- dataRange = (numpy.nanmin(absolute), numpy.nanmax(absolute))
- else:
- dataRange = None
-
- # Show dialog
- dialog = _AmplitudeRangeDialog(
- parent=self,
- amplitudeRange=dataRange,
- displayedRange=self._plot2DComplex._getAmplitudeRangeInfo())
- dialog.sigRangeChanged.connect(self._rangeChanged)
- dialog.exec_()
- dialog.sigRangeChanged.disconnect(self._rangeChanged)
-
- else: # update mode
- mode = action.data()
- if isinstance(mode, ImageComplexData.Mode):
- self._plot2DComplex.setVisualizationMode(mode)
-
- def _rangeChanged(self, range_):
- """Handle updates of range in the dialog"""
- self._plot2DComplex._setAmplitudeRangeInfo(*range_)
-
-
-class ComplexImageView(qt.QWidget):
- """Display an image of complex data and allow to choose the visualization.
-
- :param parent: See :class:`QMainWindow`
- """
-
- Mode = ImageComplexData.Mode
- """Also expose the modes inside the class"""
-
- 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._setLegend('__ComplexImageView__complex_image__')
- self._plotImage.sigItemChanged.connect(self._itemChanged)
- self._plot2D._add(self._plotImage)
- self._plot2D.setActiveImage(self._plotImage.getLegend())
-
- toolBar = qt.QToolBar('Complex', self)
- toolBar.addWidget(
- _ComplexDataToolButton(parent=self, plot=self))
-
- self._plot2D.insertToolBar(self._plot2D.getProfileToolbar(), toolBar)
-
- def _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.getVisualizationMode()
- 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.complex)
-
- 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.getVisualizationMode()
- if mode in (self.Mode.AMPLITUDE_PHASE,
- self.Mode.LOG10_AMPLITUDE_PHASE):
- return self._plotImage.getRgbaImageData(copy=copy)
- else:
- return self._plotImage.getData(copy=copy)
-
- @staticmethod
- def getSupportedVisualizationModes():
- """Returns the supported visualization modes.
-
- Supported visualization modes are:
-
- - amplitude: The absolute value provided by numpy.absolute
- - phase: The phase (or argument) provided by numpy.angle
- - real: Real part
- - imaginary: Imaginary part
- - amplitude_phase: Color-coded phase with amplitude as alpha.
- - log10_amplitude_phase:
- Color-coded phase with log10(amplitude) as alpha.
-
- :rtype: tuple of str
- """
- return tuple(ImageComplexData.Mode)
-
- def setVisualizationMode(self, mode):
- """Set the mode of visualization of the complex data.
-
- See :meth:`getSupportedVisualizationModes` for the list of
- supported modes.
-
- :param str mode: The mode to use.
- """
- self._plotImage.setVisualizationMode(mode)
-
- def getVisualizationMode(self):
- """Get the current visualization mode of the complex data.
-
- :rtype: Mode
- """
- return self._plotImage.getVisualizationMode()
-
- 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()
-
- # Image item proxy
-
- 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 Mode 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 Mode 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 81e684e..0000000
--- a/silx/gui/plot/CurvesROIWidget.py
+++ /dev/null
@@ -1,1044 +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.
-#
-# ###########################################################################*/
-"""Widget to handle regions of interest (ROI) on curves displayed in a PlotWindow.
-
-This widget is meant to work with :class:`PlotWindow`.
-
-ROI are defined by :
-
-- A name (`ROI` column)
-- 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 (`from` column)
-- The x coordinate of the right limit (`to` column)
-- Raw counts: Sum of the curve's values in the defined Region Of Intereset.
-
- .. image:: img/rawCounts.png
-
-- Net counts: Raw counts minus background
-
- .. image:: img/netCounts.png
-"""
-
-__authors__ = ["V.A. Sole", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "13/11/2017"
-
-from collections import OrderedDict
-
-import logging
-import os
-import sys
-import weakref
-
-import numpy
-
-from silx.io import dictdump
-from silx.utils import deprecation
-
-from .. import icons, qt
-
-
-_logger = logging.getLogger(__name__)
-
-
-class CurvesROIWidget(qt.QWidget):
- """Widget displaying a table of ROI information.
-
- :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)
- assert plot is not None
- self._plotRef = weakref.ref(plot)
-
- 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)
- ##############
- self.roiTable = ROITable(self)
- rheight = self.roiTable.horizontalHeader().sizeHint().height()
- self.roiTable.setMinimumHeight(4 * rheight)
- self.fillFromROIDict = self.roiTable.fillFromROIDict
- self.getROIListAndDict = self.roiTable.getROIListAndDict
- layout.addWidget(self.roiTable)
- self._roiFileDir = qt.QDir.home().absolutePath()
- #################
-
- 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)
-
- 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.sigROITableSignal.connect(self._forward)
-
- self.currentROI = None
- self._middleROIMarkerFlag = False
- self._isConnected = False # True if connected to plot signals
- self._isInit = False
-
- 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)
-
- def hideEvent(self, event):
- self._visibilityChangedHandler(visible=False)
- qt.QWidget.hideEvent(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, roidict, 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.
- """
- if order is None or order.lower() == "none":
- roilist = list(roidict.keys())
- else:
- assert order in ["from", "to", "type"]
- roilist = sorted(roidict.keys(),
- key=lambda roi_name: roidict[roi_name].get(order))
-
- return self.roiTable.fillFromROIDict(roilist, roidict)
-
- def getRois(self, order=None):
- """Return the currently defined ROIs, as an ordered dict.
-
- 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 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
- """
- roilist, roidict = self.roiTable.getROIListAndDict()
- if order is None or order.lower() == "none":
- ordered_roilist = roilist
- else:
- assert order in ["from", "to", "type", "netcounts", "rawcounts"]
- ordered_roilist = sorted(roidict.keys(),
- key=lambda roi_name: roidict[roi_name].get(order))
-
- return OrderedDict([(name, roidict[name]) for name in ordered_roilist])
-
- 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
- """
- if flag:
- self._middleROIMarkerFlag = True
- else:
- self._middleROIMarkerFlag = False
-
- def _add(self):
- """Add button clicked handler"""
- ddict = {}
- ddict['event'] = "AddROI"
- roilist, roidict = self.roiTable.getROIListAndDict()
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
- self.sigROIWidgetSignal.emit(ddict)
-
- def _del(self):
- """Delete button clicked handler"""
- row = self.roiTable.currentRow()
- if row >= 0:
- index = self.roiTable.labels.index('Type')
- text = str(self.roiTable.item(row, index).text())
- if text.upper() != 'DEFAULT':
- index = self.roiTable.labels.index('ROI')
- key = str(self.roiTable.item(row, index).text())
- else:
- # This is to prevent deleting ICR ROI, that is
- # usually initialized as "Default" type.
- return
- roilist, roidict = self.roiTable.getROIListAndDict()
- row = roilist.index(key)
- del roilist[row]
- del roidict[key]
- if len(roilist) > 0:
- currentroi = roilist[0]
- else:
- currentroi = None
-
- self.roiTable.fillFromROIDict(roilist=roilist,
- roidict=roidict,
- currentroi=currentroi)
- ddict = {}
- ddict['event'] = "DelROI"
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
- self.sigROIWidgetSignal.emit(ddict)
-
- def _forward(self, ddict):
- """Broadcast events from ROITable signal"""
- self.sigROIWidgetSignal.emit(ddict)
-
- def _reset(self):
- """Reset button clicked handler"""
- ddict = {}
- ddict['event'] = "ResetROI"
- roilist0, roidict0 = self.roiTable.getROIListAndDict()
- index = 0
- for key in roilist0:
- if roidict0[key]['type'].upper() == 'DEFAULT':
- index = roilist0.index(key)
- break
- roilist = []
- roidict = {}
- if len(roilist0):
- roilist.append(roilist0[index])
- roidict[roilist[0]] = {}
- roidict[roilist[0]].update(roidict0[roilist[0]])
- self.roiTable.fillFromROIDict(roilist=roilist, roidict=roidict)
- ddict['roilist'] = roilist
- ddict['roidict'] = roidict
- self.sigROIWidgetSignal.emit(ddict)
-
- 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.load(outputFile)
-
- 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
- """
- rois = dictdump.load(filename)
- currentROI = None
- if self.roiTable.rowCount():
- item = self.roiTable.item(self.roiTable.currentRow(), 0)
- if item is not None:
- currentROI = str(item.text())
-
- # Remove rawcounts and netcounts from ROIs
- for roi in rois['ROI']['roidict'].values():
- roi.pop('rawcounts', None)
- roi.pop('netcounts', None)
-
- self.roiTable.fillFromROIDict(roilist=rois['ROI']['roilist'],
- roidict=rois['ROI']['roidict'],
- currentroi=currentROI)
-
- roilist, roidict = self.roiTable.getROIListAndDict()
- event = {'event': 'LoadROI', 'roilist': roilist, 'roidict': roidict}
- self.sigROIWidgetSignal.emit(event)
-
- 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
- """
- roilist, roidict = self.roiTable.getROIListAndDict()
- datadict = {'ROI': {'roilist': roilist, 'roidict': roidict}}
- dictdump.dump(datadict, filename)
-
- def setHeader(self, text='ROIs'):
- """Set the header text of this widget"""
- self.headerLabel.setText("<b>%s<\b>" % text)
-
- def _roiSignal(self, ddict):
- """Handle ROI widget signal"""
- _logger.debug("CurvesROIWidget._roiSignal %s", str(ddict))
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- if ddict['event'] == "AddROI":
- xmin, xmax = plot.getXAxis().getLimits()
- fromdata = xmin + 0.25 * (xmax - xmin)
- todata = xmin + 0.75 * (xmax - xmin)
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if self._middleROIMarkerFlag:
- plot.remove('ROI middle', kind='marker')
- roiList, roiDict = self.roiTable.getROIListAndDict()
- nrois = len(roiList)
- if nrois == 0:
- newroi = "ICR"
- fromdata, dummy0, todata, dummy1 = self._getAllLimits()
- draggable = False
- color = 'black'
- else:
- # find the next index free for newroi.
- for i in range(nrois):
- i += 1
- newroi = "newroi %d" % i
- if newroi not in roiList:
- break
- color = 'blue'
- draggable = True
- plot.addXMarker(fromdata,
- legend='ROI min',
- text='ROI min',
- color=color,
- draggable=draggable)
- plot.addXMarker(todata,
- legend='ROI max',
- text='ROI max',
- color=color,
- draggable=draggable)
- if draggable and self._middleROIMarkerFlag:
- pos = 0.5 * (fromdata + todata)
- plot.addXMarker(pos,
- legend='ROI middle',
- text="",
- color='yellow',
- draggable=draggable)
- roiList.append(newroi)
- roiDict[newroi] = {}
- if newroi == "ICR":
- roiDict[newroi]['type'] = "Default"
- else:
- roiDict[newroi]['type'] = plot.getXAxis().getLabel()
- roiDict[newroi]['from'] = fromdata
- roiDict[newroi]['to'] = todata
- self.roiTable.fillFromROIDict(roilist=roiList,
- roidict=roiDict,
- currentroi=newroi)
- self.currentROI = newroi
- self.calculateRois()
- elif ddict['event'] in ['DelROI', "ResetROI"]:
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if self._middleROIMarkerFlag:
- plot.remove('ROI middle', kind='marker')
- roiList, roiDict = self.roiTable.getROIListAndDict()
- roiDictKeys = list(roiDict.keys())
- if len(roiDictKeys):
- currentroi = roiDictKeys[0]
- else:
- # create again the ICR
- ddict = {"event": "AddROI"}
- return self._roiSignal(ddict)
-
- self.roiTable.fillFromROIDict(roilist=roiList,
- roidict=roiDict,
- currentroi=currentroi)
- self.currentROI = currentroi
-
- elif ddict['event'] == 'LoadROI':
- self.calculateRois()
-
- elif ddict['event'] == 'selectionChanged':
- _logger.debug("Selection changed")
- self.roilist, self.roidict = self.roiTable.getROIListAndDict()
- fromdata = ddict['roi']['from']
- todata = ddict['roi']['to']
- plot.remove('ROI min', kind='marker')
- plot.remove('ROI max', kind='marker')
- if ddict['key'] == 'ICR':
- draggable = False
- color = 'black'
- else:
- draggable = True
- color = 'blue'
- plot.addXMarker(fromdata,
- legend='ROI min',
- text='ROI min',
- color=color,
- draggable=draggable)
- plot.addXMarker(todata,
- legend='ROI max',
- text='ROI max',
- color=color,
- draggable=draggable)
- if draggable and self._middleROIMarkerFlag:
- pos = 0.5 * (fromdata + todata)
- plot.addXMarker(pos,
- legend='ROI middle',
- text="",
- color='yellow',
- draggable=True)
- self.currentROI = ddict['key']
- if ddict['colheader'] in ['From', 'To']:
- dict0 = {}
- dict0['event'] = "SetActiveCurveEvent"
- dict0['legend'] = plot.getActiveCurve(just_legend=1)
- plot.setActiveCurve(dict0['legend'])
- elif ddict['colheader'] == 'Raw Counts':
- pass
- elif ddict['colheader'] == 'Net Counts':
- pass
- else:
- self._emitCurrentROISignal()
-
- else:
- _logger.debug("Unknown or ignored event %s", ddict['event'])
-
- 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
-
- @deprecation.deprecated(replacement="calculateRois",
- reason="CamelCase convention")
- def calculateROIs(self, *args, **kw):
- self.calculateRois(*args, **kw)
-
- def calculateRois(self, roiList=None, roiDict=None):
- """Compute ROI information"""
- if roiList is None or roiDict is None:
- roiList, roiDict = self.roiTable.getROIListAndDict()
-
- plot = self.getPlotWidget()
- if plot is None:
- activeCurve = None
- else:
- activeCurve = plot.getActiveCurve(just_legend=False)
-
- if activeCurve is None:
- xproc = None
- yproc = None
- self.setHeader()
- else:
- x = activeCurve.getXData(copy=False)
- y = activeCurve.getYData(copy=False)
- legend = activeCurve.getLegend()
- idx = numpy.argsort(x, kind='mergesort')
- xproc = numpy.take(x, idx)
- yproc = numpy.take(y, idx)
- self.setHeader('ROIs of %s' % legend)
-
- for key in roiList:
- if key == 'ICR':
- if xproc is not None:
- roiDict[key]['from'] = xproc.min()
- roiDict[key]['to'] = xproc.max()
- else:
- roiDict[key]['from'] = 0
- roiDict[key]['to'] = -1
- fromData = roiDict[key]['from']
- toData = roiDict[key]['to']
- if xproc is not None:
- idx = numpy.nonzero((fromData <= xproc) &
- (xproc <= toData))[0]
- if len(idx):
- xw = xproc[idx]
- yw = yproc[idx]
- rawCounts = yw.sum(dtype=numpy.float)
- 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.float))
- else:
- netCounts = 0.0
- else:
- rawCounts = 0.0
- netCounts = 0.0
- roiDict[key]['rawcounts'] = rawCounts
- roiDict[key]['netcounts'] = netCounts
- else:
- roiDict[key].pop('rawcounts', None)
- roiDict[key].pop('netcounts', None)
-
- self.roiTable.fillFromROIDict(
- roilist=roiList,
- roidict=roiDict,
- currentroi=self.currentROI if self.currentROI in roiList else None)
-
- def _emitCurrentROISignal(self):
- ddict = {}
- ddict['event'] = "currentROISignal"
- _roiList, roiDict = self.roiTable.getROIListAndDict()
- if self.currentROI in roiDict:
- ddict['ROI'] = roiDict[self.currentROI]
- else:
- self.currentROI = None
- ddict['current'] = self.currentROI
- self.sigROISignal.emit(ddict)
-
- def _handleROIMarkerEvent(self, ddict):
- """Handle plot signals related to marker events."""
- if ddict['event'] == 'markerMoved':
-
- label = ddict['label']
- if label not in ['ROI min', 'ROI max', 'ROI middle']:
- return
-
- roiList, roiDict = self.roiTable.getROIListAndDict()
- if self.currentROI is None:
- return
- if self.currentROI not in roiDict:
- return
-
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- x = ddict['x']
-
- if label == 'ROI min':
- roiDict[self.currentROI]['from'] = x
- if self._middleROIMarkerFlag:
- pos = 0.5 * (roiDict[self.currentROI]['to'] +
- roiDict[self.currentROI]['from'])
- plot.addXMarker(pos,
- legend='ROI middle',
- text='',
- color='yellow',
- draggable=True)
- elif label == 'ROI max':
- roiDict[self.currentROI]['to'] = x
- if self._middleROIMarkerFlag:
- pos = 0.5 * (roiDict[self.currentROI]['to'] +
- roiDict[self.currentROI]['from'])
- plot.addXMarker(pos,
- legend='ROI middle',
- text='',
- color='yellow',
- draggable=True)
- elif label == 'ROI middle':
- delta = x - 0.5 * (roiDict[self.currentROI]['from'] +
- roiDict[self.currentROI]['to'])
- roiDict[self.currentROI]['from'] += delta
- roiDict[self.currentROI]['to'] += delta
- plot.addXMarker(roiDict[self.currentROI]['from'],
- legend='ROI min',
- text='ROI min',
- color='blue',
- draggable=True)
- plot.addXMarker(roiDict[self.currentROI]['to'],
- legend='ROI max',
- text='ROI max',
- color='blue',
- draggable=True)
- else:
- return
- self.calculateRois(roiList, roiDict)
- self._emitCurrentROISignal()
-
- def _visibilityChangedHandler(self, visible):
- """Handle widget's visibility updates.
-
- It is connected to plot signals only when visible.
- """
- plot = self.getPlotWidget()
-
- if visible:
- if not self._isInit:
- # Deferred ROI widget init finalization
- self._finalizeInit()
-
- if not self._isConnected and plot is not None:
- plot.sigPlotSignal.connect(self._handleROIMarkerEvent)
- plot.sigActiveCurveChanged.connect(
- self._activeCurveChanged)
- self._isConnected = True
-
- self.calculateRois()
- else:
- if self._isConnected:
- if plot is not None:
- plot.sigPlotSignal.disconnect(self._handleROIMarkerEvent)
- plot.sigActiveCurveChanged.disconnect(
- self._activeCurveChanged)
- self._isConnected = False
-
- def _activeCurveChanged(self, *args):
- """Recompute ROIs when active curve changed."""
- self.calculateRois()
-
- def _finalizeInit(self):
- self._isInit = True
- self.sigROIWidgetSignal.connect(self._roiSignal)
- # initialize with the ICR if no ROi existing yet
- if len(self.getRois()) is 0:
- self._roiSignal({'event': "AddROI"})
-
-
-class ROITable(qt.QTableWidget):
- """Table widget displaying ROI information.
-
- See :class:`QTableWidget` for constructor arguments.
- """
-
- sigROITableSignal = qt.Signal(object)
- """Signal of ROI table modifications.
- """
-
- def __init__(self, *args, **kwargs):
- super(ROITable, self).__init__(*args, **kwargs)
- self.setRowCount(1)
- self.labels = 'ROI', 'Type', 'From', 'To', 'Raw Counts', 'Net Counts'
- self.setColumnCount(len(self.labels))
- self.setSortingEnabled(False)
-
- for index, label in enumerate(self.labels):
- item = self.horizontalHeaderItem(index)
- if item is None:
- item = qt.QTableWidgetItem(label,
- qt.QTableWidgetItem.Type)
- item.setText(label)
- self.setHorizontalHeaderItem(index, item)
-
- self.roidict = {}
- self.roilist = []
-
- self.building = False
- self.fillFromROIDict(roilist=self.roilist, roidict=self.roidict)
-
- self.cellClicked[(int, int)].connect(self._cellClickedSlot)
- self.cellChanged[(int, int)].connect(self._cellChangedSlot)
- verticalHeader = self.verticalHeader()
- verticalHeader.sectionClicked[int].connect(self._rowChangedSlot)
-
- self.__setTooltip()
-
- def __setTooltip(self):
- assert(self.labels[0] == 'ROI')
- self.horizontalHeaderItem(0).setToolTip('Region of interest identifier')
- assert(self.labels[1] == 'Type')
- self.horizontalHeaderItem(1).setToolTip('Type of the ROI')
- assert(self.labels[2] == 'From')
- self.horizontalHeaderItem(2).setToolTip('X-value of the min point')
- assert(self.labels[3] == 'To')
- self.horizontalHeaderItem(3).setToolTip('X-value of the max point')
- assert(self.labels[4] == 'Raw Counts')
- self.horizontalHeaderItem(4).setToolTip('Estimation of the integral \
- between y=0 and the selected curve')
- assert(self.labels[5] == 'Net Counts')
- self.horizontalHeaderItem(5).setToolTip('Estimation of the integral \
- between the segment [maxPt, minPt] and the selected curve')
-
- def fillFromROIDict(self, roilist=(), roidict=None, currentroi=None):
- """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 None:
- roidict = {}
-
- self.building = True
- line0 = 0
- self.roilist = []
- self.roidict = {}
- for key in roilist:
- if key in roidict.keys():
- roi = roidict[key]
- self.roilist.append(key)
- self.roidict[key] = {}
- self.roidict[key].update(roi)
- line0 = line0 + 1
- nlines = self.rowCount()
- if (line0 > nlines):
- self.setRowCount(line0)
- line = line0 - 1
- self.roidict[key]['line'] = line
- ROI = key
- roitype = "%s" % roi['type']
- fromdata = "%6g" % (roi['from'])
- todata = "%6g" % (roi['to'])
- if 'rawcounts' in roi:
- rawcounts = "%6g" % (roi['rawcounts'])
- else:
- rawcounts = " ?????? "
- if 'netcounts' in roi:
- netcounts = "%6g" % (roi['netcounts'])
- else:
- netcounts = " ?????? "
- fields = [ROI, roitype, fromdata, todata, rawcounts, netcounts]
- col = 0
- for field in fields:
- key2 = self.item(line, col)
- if key2 is None:
- key2 = qt.QTableWidgetItem(field,
- qt.QTableWidgetItem.Type)
- self.setItem(line, col, key2)
- else:
- key2.setText(field)
- if (ROI.upper() == 'ICR') or (ROI.upper() == 'DEFAULT'):
- key2.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled)
- else:
- if col in [0, 2, 3]:
- key2.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled |
- qt.Qt.ItemIsEditable)
- else:
- key2.setFlags(qt.Qt.ItemIsSelectable |
- qt.Qt.ItemIsEnabled)
- col = col + 1
- self.setRowCount(line0)
- i = 0
- for _label in self.labels:
- self.resizeColumnToContents(i)
- i = i + 1
- self.sortByColumn(2, qt.Qt.AscendingOrder)
- for i in range(len(self.roilist)):
- key = str(self.item(i, 0).text())
- self.roilist[i] = key
- self.roidict[key]['line'] = i
- if len(self.roilist) == 1:
- self.selectRow(0)
- else:
- if currentroi in self.roidict.keys():
- self.selectRow(self.roidict[currentroi]['line'])
- _logger.debug("Qt4 ensureCellVisible to be implemented")
- self.building = False
-
- def getROIListAndDict(self):
- """Return the currently defined ROIs, as a 2-tuple
- ``(roiList, roiDict)``
-
- ``roiList`` is a list of ROI names.
- ``roiDict`` is a dictionary of ROI info.
-
- 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
- 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")
-
-
- :return: ordered dict as a tuple of (list of ROI names, dict of info)
- """
- return self.roilist, self.roidict
-
- def _cellClickedSlot(self, *var, **kw):
- # selection changed event, get the current selection
- row = self.currentRow()
- col = self.currentColumn()
- if row >= 0 and row < len(self.roilist):
- item = self.item(row, 0)
- text = '' if item is None else str(item.text())
- self.roilist[row] = text
- self._emitSelectionChangedSignal(row, col)
-
- def _rowChangedSlot(self, row):
- self._emitSelectionChangedSignal(row, 0)
-
- def _cellChangedSlot(self, row, col):
- _logger.debug("_cellChangedSlot(%d, %d)", row, col)
- if self.building:
- return
- if col == 0:
- self.nameSlot(row, col)
- else:
- self._valueChanged(row, col)
-
- def _valueChanged(self, row, col):
- if col not in [2, 3]:
- return
- item = self.item(row, col)
- if item is None:
- return
- text = str(item.text())
- try:
- value = float(text)
- except:
- return
- if row >= len(self.roilist):
- _logger.debug("deleting???")
- return
- item = self.item(row, 0)
- if item is None:
- text = ""
- else:
- text = str(item.text())
- if not len(text):
- return
- if col == 2:
- self.roidict[text]['from'] = value
- elif col == 3:
- self.roidict[text]['to'] = value
- self._emitSelectionChangedSignal(row, col)
-
- def nameSlot(self, row, col):
- if col != 0:
- return
- if row >= len(self.roilist):
- _logger.debug("deleting???")
- return
- item = self.item(row, col)
- if item is None:
- text = ""
- else:
- text = str(item.text())
- if len(text) and (text not in self.roilist):
- old = self.roilist[row]
- self.roilist[row] = text
- self.roidict[text] = {}
- self.roidict[text].update(self.roidict[old])
- del self.roidict[old]
- self._emitSelectionChangedSignal(row, col)
-
- def _emitSelectionChangedSignal(self, row, col):
- ddict = {}
- ddict['event'] = "selectionChanged"
- ddict['row'] = row
- ddict['col'] = col
- ddict['roi'] = self.roidict[self.roilist[row]]
- ddict['key'] = self.roilist[row]
- ddict['colheader'] = self.labels[col]
- ddict['rowheader'] = "%d" % row
- self.sigROITableSignal.emit(ddict)
-
-
-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)
-
- 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.currentROI = self.roiWidget.currentROI
-
- self.layout().setContentsMargins(0, 0, 0, 0)
- self.setWidget(self.roiWidget)
-
- 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)
diff --git a/silx/gui/plot/ImageView.py b/silx/gui/plot/ImageView.py
deleted file mode 100644
index eba9bc6..0000000
--- a/silx/gui/plot/ImageView.py
+++ /dev/null
@@ -1,871 +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 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 silx
-from .. import qt
-
-from . import items, PlotWindow, PlotWidget, actions
-from ..colors import Colormap
-from ..colors import cursorColorForColormap
-from .tools import LimitsToolBar
-from .Profile import ProfileToolBar
-
-
-_logger = logging.getLogger(__name__)
-
-
-# RadarView ###################################################################
-
-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'))
- _VISIBLE_PEN = qt.QPen(qt.QColor('red'))
- _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
-
- class _DraggableRectItem(qt.QGraphicsRectItem):
- """RectItem which signals its change through visibleRectDragged."""
- def __init__(self, *args, **kwargs):
- super(RadarView._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(RadarView._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(RadarView._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(RadarView._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
-
- def __init__(self, parent=None):
- self._scene = qt.QGraphicsScene()
- self._dataRect = self._scene.addRect(0, 0, 1, 1,
- self._DATA_PEN,
- self._DATA_BRUSH)
- self._visibleRect = self._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)
-
- 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.setRect(0, 0, width, height)
- self._visibleRect.setPos(left, top)
- self.fitInView(self._scene.itemsBoundingRect(), qt.Qt.KeepAspectRatio)
-
-
-# ImageView ###################################################################
-
-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.
- """
-
- def __init__(self, parent=None, backend=None):
- self._imageLegend = '__ImageView__image' + str(id(self))
- self._cache = None # Store currently visible data information
- self._updatingLimits = False
-
- 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)
- 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.profile = ProfileToolBar(plot=self)
- """"Profile tools attached to this plot.
-
- See :class:`silx.gui.plot.PlotTools.ProfileToolBar`
- """
-
- self.addToolBar(self.profile)
-
- # Sync PlotBackend and ImageView
- self._updateYAxisInverted()
-
- def _initWidgets(self, backend):
- """Set-up layout and plots."""
- self._histoHPlot = PlotWidget(backend=backend, parent=self)
- self._histoHPlot.getWidgetHandle().setMinimumHeight(
- self.HISTOGRAMS_HEIGHT)
- self._histoHPlot.getWidgetHandle().setMaximumHeight(
- self.HISTOGRAMS_HEIGHT)
- self._histoHPlot.setInteractiveMode('zoom')
- self._histoHPlot.sigPlotSignal.connect(self._histoHPlotCB)
-
- self.setPanWithArrowKeys(True)
-
- self.setInteractiveMode('zoom') # Color set in setColormap
- self.sigPlotSignal.connect(self._imagePlotCB)
- self.getYAxis().sigInvertedChanged.connect(self._updateYAxisInverted)
- self.sigActiveImageChanged.connect(self._activeImageChangedSlot)
-
- self._histoVPlot = PlotWidget(backend=backend, parent=self)
- self._histoVPlot.getWidgetHandle().setMinimumWidth(
- self.HISTOGRAMS_HEIGHT)
- self._histoVPlot.getWidgetHandle().setMaximumWidth(
- self.HISTOGRAMS_HEIGHT)
- self._histoVPlot.setInteractiveMode('zoom')
- self._histoVPlot.sigPlotSignal.connect(self._histoVPlotCB)
-
- self._radarView = RadarView(parent=self)
- self._radarView.visibleRectDragged.connect(self._radarViewCB)
-
- 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)
-
- def _dirtyCache(self):
- self._cache = None
-
- def _updateHistograms(self):
- """Update histograms content using current active image."""
- activeImage = self.getActiveImage()
- if activeImage is not None:
- wasUpdatingLimits = self._updatingLimits
- self._updatingLimits = True
-
- data = activeImage.getData(copy=False)
- origin = activeImage.getOrigin()
- scale = activeImage.getScale()
- height, width = data.shape
-
- xMin, xMax = self.getXAxis().getLimits()
- yMin, yMax = self.getYAxis().getLimits()
-
- # Convert plot area limits to image coordinates
- # and work in image coordinates (i.e., in pixels)
- 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 and xMax >= 0 and
- yMin < height and yMax >= 0):
- # 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 (self._cache is None or
- subsetXMin != self._cache['dataXMin'] or
- subsetXMax != self._cache['dataXMax'] or
- subsetYMin != self._cache['dataYMin'] or
- subsetYMax != self._cache['dataYMax']):
- # The visible area of data has changed, update histograms
-
- # Rebuild histograms for visible area
- visibleData = data[subsetYMin:subsetYMax,
- subsetXMin:subsetXMax]
- histoHVisibleData = numpy.sum(visibleData, axis=0)
- histoVVisibleData = numpy.sum(visibleData, axis=1)
-
- self._cache = {
- 'dataXMin': subsetXMin,
- 'dataXMax': subsetXMax,
- 'dataYMin': subsetYMin,
- 'dataYMax': subsetYMax,
-
- 'histoH': histoHVisibleData,
- 'histoHMin': numpy.min(histoHVisibleData),
- 'histoHMax': numpy.max(histoHVisibleData),
-
- 'histoV': histoVVisibleData,
- 'histoVMin': numpy.min(histoVVisibleData),
- 'histoVMax': numpy.max(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)
- self._histoHPlot.addCurve(xCoords, xData,
- xlabel='', ylabel='',
- replace=False,
- color=self.HISTOGRAMS_COLOR,
- linestyle='-',
- selectable=False)
- vMin = self._cache['histoHMin']
- vMax = self._cache['histoHMax']
- vOffset = 0.1 * (vMax - vMin)
- if vOffset == 0.:
- vOffset = 1.
- self._histoHPlot.getYAxis().setLimits(vMin - vOffset,
- vMax + vOffset)
-
- coords = numpy.arange(2 * histoVVisibleData.size)
- yCoords = (coords + 1) // 2 + subsetYMin
- yCoords = origin[1] + scale[1] * yCoords
- yData = numpy.take(histoVVisibleData, coords // 2)
- self._histoVPlot.addCurve(yData, yCoords,
- xlabel='', ylabel='',
- replace=False,
- color=self.HISTOGRAMS_COLOR,
- linestyle='-',
- selectable=False)
- vMin = self._cache['histoVMin']
- vMax = self._cache['histoVMax']
- vOffset = 0.1 * (vMax - vMin)
- if vOffset == 0.:
- vOffset = 1.
- self._histoVPlot.getXAxis().setLimits(vMin - vOffset,
- vMax + vOffset)
- else:
- self._dirtyCache()
- self._histoHPlot.remove(kind='curve')
- self._histoVPlot.remove(kind='curve')
-
- self._updatingLimits = wasUpdatingLimits
-
- def _updateRadarView(self):
- """Update radar view visible area.
-
- Takes care of y coordinate conversion.
- """
- xMin, xMax = self.getXAxis().getLimits()
- yMin, yMax = self.getYAxis().getLimits()
- self._radarView.setVisibleRect(xMin, yMin, xMax - xMin, yMax - yMin)
-
- # 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._updateHistogramsLimits()
-
- def _updateHistogramsLimits(self):
- # Do not handle histograms limitsChanged while
- # updating their limits from here.
- self._updatingLimits = True
-
- # Refresh histograms
- self._updateHistograms()
-
- xMin, xMax = self.getXAxis().getLimits()
- yMin, yMax = self.getYAxis().getLimits()
-
- # Set horizontal histo limits
- self._histoHPlot.getXAxis().setLimits(xMin, xMax)
-
- # Set vertical histo limits
- self._histoVPlot.getYAxis().setLimits(yMin, yMax)
-
- self._updateRadarView()
-
- self._updatingLimits = False
-
- def _histoHPlotCB(self, eventDict):
- """Callback for horizontal histogram plot events."""
- if eventDict['event'] == 'mouseMoved':
- if self._cache is not None:
- activeImage = self.getActiveImage()
- if activeImage is not None:
- xOrigin = activeImage.getOrigin()[0]
- xScale = activeImage.getScale()[0]
-
- minValue = xOrigin + xScale * self._cache['dataXMin']
-
- if eventDict['x'] >= minValue:
- data = self._cache['histoH']
- column = int((eventDict['x'] - minValue) / xScale)
- if column >= 0 and column < data.shape[0]:
- self.valueChanged.emit(
- float('nan'),
- float(column + self._cache['dataXMin']),
- data[column])
-
- elif eventDict['event'] == 'limitsChanged':
- if (not self._updatingLimits and
- eventDict['xdata'] != self.getXAxis().getLimits()):
- xMin, xMax = eventDict['xdata']
- self.getXAxis().setLimits(xMin, xMax)
-
- def _histoVPlotCB(self, eventDict):
- """Callback for vertical histogram plot events."""
- if eventDict['event'] == 'mouseMoved':
- if self._cache is not None:
- activeImage = self.getActiveImage()
- if activeImage is not None:
- yOrigin = activeImage.getOrigin()[1]
- yScale = activeImage.getScale()[1]
-
- minValue = yOrigin + yScale * self._cache['dataYMin']
-
- if eventDict['y'] >= minValue:
- data = self._cache['histoV']
- row = int((eventDict['y'] - minValue) / yScale)
- if row >= 0 and row < data.shape[0]:
- self.valueChanged.emit(
- float(row + self._cache['dataYMin']),
- float('nan'),
- data[row])
-
- elif eventDict['event'] == 'limitsChanged':
- if (not self._updatingLimits and
- eventDict['ydata'] != self.getYAxis().getLimits()):
- yMin, yMax = eventDict['ydata']
- self.getYAxis().setLimits(yMin, yMax)
-
- def _radarViewCB(self, left, top, width, height):
- """Slot for radar view visible rectangle changes."""
- if not self._updatingLimits:
- # Takes care of Y axis conversion
- self.setLimits(left, left + width, top, top + height)
-
- def _updateYAxisInverted(self, inverted=None):
- """Sync image, vertical histogram and radar view axis orientation."""
- if inverted is None:
- # Do not perform this when called from plot signal
- inverted = self.getYAxis().isInverted()
-
- self._histoVPlot.getYAxis().setInverted(inverted)
-
- # 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._radarView.resetTransform()
- if not inverted:
- self._radarView.scale(1., -1.)
- self._updateRadarView()
-
- self._radarView.update()
-
- def _activeImageChangedSlot(self, previous, legend):
- """Handle Plot active image change.
-
- Resets side histograms cache
- """
- self._dirtyCache()
- self._updateHistograms()
-
- 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['dataXMin'], self._cache['dataXMax']))
- else:
- return dict(
- data=numpy.array(self._cache['histoV'], copy=True),
- extent=(self._cache['dataYMin'], self._cache['dataYMax']))
-
- 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.visibleRectDragged.disconnect(self._radarViewCB)
- self._radarView = radarView
- self._radarView.visibleRectDragged.connect(self._radarViewCB)
- self.centralWidget().layout().addWidget(self._radarView, 1, 1)
-
- self._updateYAxisInverted()
-
- # 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
- height, width = data.shape
-
- self.addImage(data,
- legend=self._imageLegend,
- origin=origin, scale=scale,
- colormap=self.getColormap(),
- resetzoom=False)
- self.setActiveImage(self._imageLegend)
- self._updateHistograms()
-
- self._radarView.setDataRect(origin[0],
- origin[1],
- width * scale[0],
- height * scale[1])
-
- if reset:
- self.resetZoom()
- else:
- self._updateHistogramsLimits()
-
-
-# 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))
-
- menu = self.menuBar().addMenu('Profile')
- menu.addAction(self.profile.hLineAction)
- menu.addAction(self.profile.vLineAction)
- menu.addAction(self.profile.lineAction)
- menu.addAction(self.profile.clearAction)
-
- # Connect to ImageView's signal
- self.valueChanged.connect(self._statusBarSlot)
-
- 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)
-
- def setImage(self, image, *args, **kwargs):
- """Set the displayed image.
-
- See :meth:`ImageView.setImage` for details.
- """
- 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 358af74..0000000
--- a/silx/gui/plot/Interaction.py
+++ /dev/null
@@ -1,300 +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 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
-
-
-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)
-
-
-# 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`.
- """
-
- DRAG_THRESHOLD_SQUARE_DIST = 5 ** 2
-
- class Idle(State):
- def onPress(self, x, y, btn):
- if btn == LEFT_BTN:
- self.goto('clickOrDrag', x, y)
- return True
- elif btn == RIGHT_BTN:
- self.goto('rightClick', x, y)
- return True
-
- class RightClick(State):
- def onMove(self, x, y):
- self.goto('idle')
-
- def onRelease(self, x, y, btn):
- if btn == RIGHT_BTN:
- self.machine.click(x, y, btn)
- self.goto('idle')
-
- class ClickOrDrag(State):
- def enterState(self, x, y):
- self.initPos = x, y
-
- 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))
-
- def onRelease(self, x, y, btn):
- if btn == LEFT_BTN:
- self.machine.click(x, y, btn)
- self.goto('idle')
-
- class Drag(State):
- def enterState(self, initPos, curPos):
- self.initPos = initPos
- self.machine.beginDrag(*initPos)
- self.machine.drag(*curPos)
-
- def onMove(self, x, y):
- self.machine.drag(x, y)
-
- def onRelease(self, x, y, btn):
- if btn == LEFT_BTN:
- self.machine.endDrag(self.initPos, (x, y))
- self.goto('idle')
-
- def __init__(self):
- states = {
- 'idle': ClickOrDrag.Idle,
- 'rightClick': ClickOrDrag.RightClick,
- 'clickOrDrag': ClickOrDrag.ClickOrDrag,
- 'drag': ClickOrDrag.Drag
- }
- super(ClickOrDrag, self).__init__(states, 'idle')
-
- def click(self, x, y, btn):
- """Called upon a left or right button click.
-
- To override in a subclass.
- """
- pass
-
- def beginDrag(self, x, y):
- """Called at the beginning of a drag gesture with left button
- pressed.
-
- To override in a subclass.
- """
- pass
-
- def drag(self, x, y):
- """Called on mouse moved during a drag gesture.
-
- To override in a subclass.
- """
- pass
-
- def endDrag(self, startPoint, endPoint):
- """Called at the end of a drag gesture when the left button is
- released.
-
- To override in a subclass.
- """
- pass
diff --git a/silx/gui/plot/ItemsSelectionDialog.py b/silx/gui/plot/ItemsSelectionDialog.py
deleted file mode 100644
index acb287a..0000000
--- a/silx/gui/plot/ItemsSelectionDialog.py
+++ /dev/null
@@ -1,282 +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 a dialog widget to select plot items.
-
-.. autoclass:: ItemsSelectionDialog
-
-"""
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "28/06/2017"
-
-import logging
-
-from silx.gui import qt
-from silx.gui.plot.PlotWidget import PlotWidget
-
-_logger = logging.getLogger(__name__)
-
-
-class KindsSelector(qt.QListWidget):
- """List widget allowing to select plot item kinds
- ("curve", "scatter", "image"...)
- """
- sigSelectedKindsChanged = qt.Signal(list)
-
- def __init__(self, parent=None, kinds=None):
- """
-
- :param parent: Parent QWidget or None
- :param tuple(str) kinds: Sequence of kinds. If None, the default
- behavior is to provide a checkbox for all possible item kinds.
- """
- qt.QListWidget.__init__(self, parent)
-
- self.plot_item_kinds = []
-
- self.setAvailableKinds(kinds if kinds is not None else PlotWidget.ITEM_KINDS)
-
- self.setSelectionMode(qt.QAbstractItemView.ExtendedSelection)
- self.selectAll()
-
- self.itemSelectionChanged.connect(self.emitSigKindsSelectionChanged)
-
- def emitSigKindsSelectionChanged(self):
- self.sigSelectedKindsChanged.emit(self.selectedKinds)
-
- @property
- def selectedKinds(self):
- """Tuple of all selected kinds (as strings)."""
- # check for updates when self.itemSelectionChanged
- return [item.text() for item in self.selectedItems()]
-
- def setAvailableKinds(self, kinds):
- """Set a list of kinds to be displayed.
-
- :param list[str] kinds: Sequence of kinds
- """
- self.plot_item_kinds = kinds
-
- self.clear()
- for kind in self.plot_item_kinds:
- item = qt.QListWidgetItem(self)
- item.setText(kind)
- self.addItem(item)
-
- def selectAll(self):
- """Select all available kinds."""
- if self.selectionMode() in [qt.QAbstractItemView.SingleSelection,
- qt.QAbstractItemView.NoSelection]:
- raise RuntimeError("selectAll requires a multiple selection mode")
- for i in range(self.count()):
- self.item(i).setSelected(True)
-
-
-class PlotItemsSelector(qt.QTableWidget):
- """Table widget displaying the legend and kind of all
- plot items corresponding to a list of specified kinds.
-
- Selected plot items are provided as property :attr:`selectedPlotItems`.
- You can be warned of selection changes by listening to signal
- :attr:`itemSelectionChanged`.
- """
- def __init__(self, parent=None, plot=None):
- if plot is None or not isinstance(plot, PlotWidget):
- raise AttributeError("parameter plot is required")
- self.plot = plot
- """:class:`PlotWidget` instance"""
-
- self.plot_item_kinds = None
- """List of plot item kinds (strings)"""
-
- qt.QTableWidget.__init__(self, parent)
-
- self.setColumnCount(2)
-
- self.setSelectionBehavior(qt.QTableWidget.SelectRows)
-
- def _clear(self):
- self.clear()
- self.setHorizontalHeaderLabels(["legend", "type"])
-
- def setAllKindsFilter(self):
- """Display all kinds of plot items."""
- self.setKindsFilter(PlotWidget.ITEM_KINDS)
-
- def setKindsFilter(self, kinds):
- """Set list of all kinds of plot items to be displayed.
-
- :param list[str] kinds: Sequence of kinds
- """
- if not set(kinds) <= set(PlotWidget.ITEM_KINDS):
- raise KeyError("Illegal plot item kinds: %s" %
- set(kinds) - set(PlotWidget.ITEM_KINDS))
- self.plot_item_kinds = kinds
-
- self.updatePlotItems()
-
- def updatePlotItems(self):
- self._clear()
-
- nrows = len(self.plot._getItems(kind=self.plot_item_kinds,
- just_legend=True))
- self.setRowCount(nrows)
-
- # respect order of kinds as set in method setKindsFilter
- i = 0
- for kind in self.plot_item_kinds:
- for plot_item in self.plot._getItems(kind=kind):
- legend_twitem = qt.QTableWidgetItem(plot_item.getLegend())
- self.setItem(i, 0, legend_twitem)
-
- kind_twitem = qt.QTableWidgetItem(kind)
- self.setItem(i, 1, kind_twitem)
- i += 1
-
- @property
- def selectedPlotItems(self):
- """List of all selected items"""
- selection_model = self.selectionModel()
- selected_rows_idx = selection_model.selectedRows()
- selected_rows = [idx.row() for idx in selected_rows_idx]
-
- items = []
- for row in selected_rows:
- legend = self.item(row, 0).text()
- kind = self.item(row, 1).text()
- items.append(self.plot._getItem(kind, legend))
-
- return items
-
-
-class ItemsSelectionDialog(qt.QDialog):
- """This widget is a modal dialog allowing to select one or more plot
- items, in a table displaying their legend and kind.
-
- Public methods:
-
- - :meth:`getSelectedItems`
- - :meth:`setAvailableKinds`
- - :meth:`setItemsSelectionMode`
-
- This widget inherits QDialog and therefore implements the usual
- dialog methods, e.g. :meth:`exec_`.
-
- A trivial usage example would be::
-
- isd = ItemsSelectionDialog(plot=my_plot_widget)
- isd.setItemsSelectionMode(qt.QTableWidget.SingleSelection)
- result = isd.exec_()
- if result:
- for item in isd.getSelectedItems():
- print(item.getLegend(), type(item))
- else:
- print("Selection cancelled")
- """
- def __init__(self, parent=None, plot=None):
- if plot is None or not isinstance(plot, PlotWidget):
- raise AttributeError("parameter plot is required")
- qt.QDialog.__init__(self, parent)
-
- self.setWindowTitle("Plot items selector")
-
- kind_selector_label = qt.QLabel("Filter item kinds:", self)
- item_selector_label = qt.QLabel("Select items:", self)
-
- self.kind_selector = KindsSelector(self)
- self.kind_selector.setToolTip(
- "select one or more item kinds to show them in the item list")
-
- self.item_selector = PlotItemsSelector(self, plot)
- self.item_selector.setToolTip("select items")
-
- self.item_selector.setKindsFilter(self.kind_selector.selectedKinds)
- self.kind_selector.sigSelectedKindsChanged.connect(
- self.item_selector.setKindsFilter
- )
-
- okb = qt.QPushButton("OK", self)
- okb.clicked.connect(self.accept)
-
- cancelb = qt.QPushButton("Cancel", self)
- cancelb.clicked.connect(self.reject)
-
- layout = qt.QGridLayout(self)
- layout.addWidget(kind_selector_label, 0, 0)
- layout.addWidget(item_selector_label, 0, 1)
- layout.addWidget(self.kind_selector, 1, 0)
- layout.addWidget(self.item_selector, 1, 1)
- layout.addWidget(okb, 2, 0)
- layout.addWidget(cancelb, 2, 1)
-
- self.setLayout(layout)
-
- def getSelectedItems(self):
- """Return a list of selected plot items
-
- :return: List of selected plot items
- :rtype: list[silx.gui.plot.items.Item]"""
- return self.item_selector.selectedPlotItems
-
- def setAvailableKinds(self, kinds):
- """Set a list of kinds to be displayed.
-
- :param list[str] kinds: Sequence of kinds
- """
- self.kind_selector.setAvailableKinds(kinds)
-
- def selectAllKinds(self):
- self.kind_selector.selectAll()
-
- def setItemsSelectionMode(self, mode):
- """Set selection mode for plot item (single item selection,
- multiple...).
-
- :param mode: One of :class:`QTableWidget` selection modes
- """
- if mode == self.item_selector.SingleSelection:
- self.item_selector.setToolTip(
- "Select one item by clicking on it.")
- elif mode == self.item_selector.MultiSelection:
- self.item_selector.setToolTip(
- "Select one or more items by clicking with the left mouse"
- " button.\nYou can unselect items by clicking them again.\n"
- "Multiple items can be toggled by dragging the mouse over them.")
- elif mode == self.item_selector.ExtendedSelection:
- self.item_selector.setToolTip(
- "Select one or more items. You can select multiple items "
- "by keeping the Ctrl key pushed when clicking.\nYou can "
- "select a range of items by clicking on the first and "
- "last while keeping the Shift key pushed.")
- elif mode == self.item_selector.ContiguousSelection:
- self.item_selector.setToolTip(
- "Select one item by clicking on it. If you press the Shift"
- " key while clicking on a second item,\nall items between "
- "the two will be selected.")
- elif mode == self.item_selector.NoSelection:
- raise ValueError("The NoSelection mode is not allowed "
- "in this context.")
- self.item_selector.setSelectionMode(mode)
diff --git a/silx/gui/plot/LegendSelector.py b/silx/gui/plot/LegendSelector.py
deleted file mode 100644
index b9d0fd3..0000000
--- a/silx/gui/plot/LegendSelector.py
+++ /dev/null
@@ -1,1193 +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.
-#
-# ###########################################################################*/
-"""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 . import items
-
-
-_logger = logging.getLogger(__name__)
-
-# Build all symbols
-# Courtesy of the pyqtgraph project
-Symbols = dict([(name, qt.QPainterPath())
- for name in ['o', 's', 't', 'd', '+', 'x', '.', ',']])
-Symbols['o'].addEllipse(qt.QRectF(.1, .1, .8, .8))
-Symbols['.'].addEllipse(qt.QRectF(.3, .3, .4, .4))
-Symbols[','].addEllipse(qt.QRectF(.4, .4, .2, .2))
-Symbols['s'].addRect(qt.QRectF(.1, .1, .8, .8))
-
-coords = {
- 't': [(0.5, 0.), (.1, .8), (.9, .8)],
- 'd': [(0.1, 0.5), (0.5, 0.), (0.9, 0.5), (0.5, 1.)],
- '+': [(0.0, 0.40), (0.40, 0.40), (0.40, 0.), (0.60, 0.),
- (0.60, 0.40), (1., 0.40), (1., 0.60), (0.60, 0.60),
- (0.60, 1.), (0.40, 1.), (0.40, 0.60), (0., 0.60)],
- 'x': [(0.0, 0.40), (0.40, 0.40), (0.40, 0.), (0.60, 0.),
- (0.60, 0.40), (1., 0.40), (1., 0.60), (0.60, 0.60),
- (0.60, 1.), (0.40, 1.), (0.40, 0.60), (0., 0.60)]
-}
-for s, c in coords.items():
- Symbols[s].moveTo(*c[0])
- for x, y in c[1:]:
- Symbols[s].lineTo(x, y)
- Symbols[s].closeSubpath()
-tr = qt.QTransform()
-tr.rotate(45)
-Symbols['x'].translate(qt.QPointF(-0.5, -0.5))
-Symbols['x'] = tr.map(Symbols['x'])
-Symbols['x'].translate(qt.QPointF(0.5, 0.5))
-
-NoSymbols = (None, 'None', 'none', '', ' ')
-"""List of values resulting in no symbol being displayed for a curve"""
-
-
-LineStyles = {
- None: qt.Qt.NoPen,
- 'None': qt.Qt.NoPen,
- 'none': qt.Qt.NoPen,
- '': qt.Qt.NoPen,
- ' ': qt.Qt.NoPen,
- '-': qt.Qt.SolidLine,
- '--': qt.Qt.DashLine,
- ':': qt.Qt.DotLine,
- '-.': qt.Qt.DashDotLine
-}
-"""Conversion from matplotlib-like linestyle to Qt"""
-
-NoLineStyle = (None, 'None', 'none', '', ' ')
-"""List of style values resulting in no line being displayed for a curve"""
-
-
-class LegendIcon(qt.QWidget):
- """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
-
- # Visibilities
- self.showLine = True
- self.showSymbol = True
-
- # Line attributes
- self.lineStyle = qt.Qt.NoPen
- self.lineWidth = 1.
- self.lineColor = qt.Qt.green
-
- self.symbol = ''
- # Symbol attributes
- self.symbolStyle = qt.Qt.SolidPattern
- self.symbolColor = qt.Qt.green
- self.symbolOutlineBrush = qt.QBrush(qt.Qt.white)
-
- # Control widget size: sizeHint "is the only acceptable
- # alternative, so the widget can never grow or shrink"
- # (c.f. Qt Doc, enum QSizePolicy::Policy)
- self.setSizePolicy(qt.QSizePolicy.Fixed,
- qt.QSizePolicy.Fixed)
-
- self.setCurve(curve)
-
- def sizeHint(self):
- return qt.QSize(50, 15)
-
- # Synchronize with a 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()
-
- # Modify Symbol
- def setSymbol(self, symbol):
- symbol = str(symbol)
- if symbol not in NoSymbols:
- if symbol not in Symbols:
- raise ValueError("Unknown symbol: <%s>" % symbol)
- self.symbol = symbol
- # self.update() after set...?
- # Does not seem necessary
-
- def setSymbolColor(self, color):
- """
- :param color: determines the symbol color
- :type style: qt.QColor
- """
- self.symbolColor = qt.QColor(color)
-
- # Modify Line
-
- def setLineColor(self, color):
- self.lineColor = qt.QColor(color)
-
- def setLineWidth(self, width):
- self.lineWidth = float(width)
-
- def setLineStyle(self, style):
- """Set the linestyle.
-
- Possible line styles:
-
- - '', ' ', 'None': No line
- - '-': solid
- - '--': dashed
- - ':': dotted
- - '-.': dash and dot
-
- :param str style: The linestyle to use
- """
- if style not in LineStyles:
- raise ValueError('Unknown style: %s', style)
- self.lineStyle = LineStyles[style]
-
- # Paint
-
- def paintEvent(self, event):
- """
- :param event: event
- :type event: QPaintEvent
- """
- painter = qt.QPainter(self)
- self.paint(painter, event.rect(), self.palette())
-
- def paint(self, painter, rect, palette):
- painter.save()
- painter.setRenderHint(qt.QPainter.Antialiasing)
- # Scale painter to the icon height
- # current -> width = 2.5, height = 1.0
- scale = float(self.height())
- ratio = float(self.width()) / scale
- painter.scale(scale,
- scale)
- symbolOffset = qt.QPointF(.5 * (ratio - 1.), 0.)
- # Determine and scale offset
- offset = qt.QPointF(float(rect.left()) / scale, float(rect.top()) / scale)
-
- # Override color when disabled
- if self.isEnabled():
- overrideColor = None
- else:
- overrideColor = palette.color(qt.QPalette.Disabled,
- qt.QPalette.WindowText)
-
- # Draw BG rectangle (for debugging)
- # bottomRight = qt.QPointF(
- # float(rect.right())/scale,
- # float(rect.bottom())/scale)
- # painter.fillRect(qt.QRectF(offset, bottomRight),
- # qt.QBrush(qt.Qt.green))
- llist = []
- if self.showLine:
- linePath = qt.QPainterPath()
- linePath.moveTo(0., 0.5)
- linePath.lineTo(ratio, 0.5)
- # linePath.lineTo(2.5, 0.5)
- lineBrush = qt.QBrush(
- self.lineColor if overrideColor is None else overrideColor)
- linePen = qt.QPen(
- lineBrush,
- (self.lineWidth / self.height()),
- self.lineStyle,
- qt.Qt.FlatCap
- )
- llist.append((linePath, linePen, lineBrush))
- if (self.showSymbol and len(self.symbol) and
- self.symbol not in NoSymbols):
- # PITFALL ahead: Let this be a warning to others
- # symbolPath = Symbols[self.symbol]
- # Copy before translate! Dict is a mutable type
- symbolPath = qt.QPainterPath(Symbols[self.symbol])
- symbolPath.translate(symbolOffset)
- symbolBrush = qt.QBrush(
- self.symbolColor if overrideColor is None else overrideColor,
- self.symbolStyle)
- symbolPen = qt.QPen(
- self.symbolOutlineBrush, # Brush
- 1. / self.height(), # Width
- qt.Qt.SolidLine # Style
- )
- llist.append((symbolPath,
- symbolPen,
- symbolBrush))
- # Draw
- for path, pen, brush in llist:
- path.translate(offset)
- painter.setPen(pen)
- painter.setBrush(brush)
- painter.drawPath(path)
- painter.restore()
-
-
-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 linestyle in NoLineStyle:
- # 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 symbol in NoSymbols:
- # 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):
- self.clear()
- if row is None:
- row = 0
- model = self.model()
- model.insertLegendList(row, legendList)
- _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 symbol in NoSymbols
- _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.getLegend(),
- 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.getLegend(),
- 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.getLegend(),
- 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.getLegend()
- # 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 990e479..0000000
--- a/silx/gui/plot/MaskToolsWidget.py
+++ /dev/null
@@ -1,774 +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.
-#
-# ###########################################################################*/
-"""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__ = "29/08/2018"
-
-
-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 silx.third_party.EdfFile import EdfFile
-from silx.third_party.TiffIO import TiffIO
-
-try:
- import fabio
-except ImportError:
- fabio = None
-
-
-_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+")
- edfFile.WriteImage({}, 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':
- if fabio is None:
- raise ImportError("Fit2d mask files can't be written: Fabio module is not available")
- 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
- 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 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
-
- 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
-
- # if mask has not changed, do nothing
- if numpy.array_equal(mask, self.getSelectionMask()):
- return mask.shape
-
- # ensure all mask attributes are synchronized with the active image
- # and connect listener
- activeImage = self.plot.getActiveImage()
- if activeImage is not None and activeImage.getLegend() != self._maskName:
- self._activeImageChanged()
- self.plot.sigActiveImageChanged.connect(self._activeImageChanged)
-
- if self._data.shape[0:2] == (0, 0) or mask.shape == self._data.shape[0:2]:
- self._mask.setMask(mask, copy=copy)
- self._mask.commit()
- 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._setLegend(self._maskName)
- # update the items
- maskItem.setData(mask, copy=False)
- maskItem.setColormap(self._colormap)
- maskItem.setOrigin(self._origin)
- maskItem.setScale(self._scale)
- maskItem.setZValue(self._z)
-
- if mustBeAdded:
- self.plot._add(maskItem)
-
- elif self.plot.getImage(self._maskName):
- self.plot.remove(self._maskName, kind='image')
-
- def showEvent(self, event):
- try:
- self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChangedAfterCare)
- except (RuntimeError, TypeError):
- pass
- self._activeImageChanged() # Init mask + enable/disable widget
- self.plot.sigActiveImageChanged.connect(self._activeImageChanged)
-
- def hideEvent(self, event):
- try:
- self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChanged)
- except (RuntimeError, TypeError):
- pass
- if self.isMaskInteractionActivated():
- # Disable drawing tool
- self.browseAction.trigger()
-
- if self.getSelectionMask(copy=False) is not None:
- self.plot.sigActiveImageChanged.connect(
- self._activeImageChangedAfterCare)
-
- 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.getLegend() == 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 _activeImageChanged(self, *args):
- """Update widget and mask according to active image changes"""
- activeImage = self.plot.getActiveImage()
- if (activeImage is None or activeImage.getLegend() == self._maskName or
- activeImage.getData(copy=False).size == 0):
- # No active image or active image is the mask or image has no data...
- self.setEnabled(False)
-
- self._data = numpy.zeros((0, 0), dtype=numpy.uint8)
- self._mask.reset()
- self._mask.commit()
-
- else: # There is an active image
- self.setEnabled(True)
-
- 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)
- self._mask.setDataItem(activeImage)
- 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()
-
- # 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":
- if fabio is None:
- raise ImportError("Fit2d mask files can't be read: Fabio module is not available")
- 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()
-
- 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:
- msg = qt.QMessageBox(self)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setText("Cannot save.\n"
- "Input Output Error: %s" % (sys.exc_info()[1]))
- msg.exec_()
- return
-
- self.maskFileDir = os.path.dirname(filename)
- try:
- self.save(filename, extension[1:])
- except Exception as e:
- raise
- msg = qt.QMessageBox(self)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setText("Cannot save file %s\n%s" % (filename, e.args[0]))
- 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' and
- 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 == 'polygon' and
- event['event'] == 'drawingFinished'):
- doMask = self._isMasking()
- # Convert from plot to array coords
- vertices = (event['points'] - self._origin) / self._scale
- vertices = vertices.astype(numpy.int)[:, (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
-
- def _loadRangeFromColormapTriggered(self):
- """Set range from active image colormap range"""
- activeImage = self.plot.getActiveImage()
- if (isinstance(activeImage, items.ColormapMixIn) and
- activeImage.getLegend() != 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 356bda6..0000000
--- a/silx/gui/plot/PlotInteraction.py
+++ /dev/null
@@ -1,1603 +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.
-#
-# ###########################################################################*/
-"""Implementation of the interaction for the :class:`Plot`."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-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,
- 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
-
- self.plot.addItem(points[:, 0], points[:, 1], legend=legend,
- replace=False,
- shape=shape, color=color, fill=fill,
- 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 ZoomIdle(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):
- """Init.
-
- :param plot: The plot to apply modifications to.
- """
- _PlotInteraction.__init__(self, plot)
-
- states = {
- 'idle': _ZoomOnWheel.ZoomIdle,
- 'rightClick': ClickOrDrag.RightClick,
- 'clickOrDrag': ClickOrDrag.ClickOrDrag,
- 'drag': ClickOrDrag.Drag
- }
- StateMachine.__init__(self, states, 'idle')
-
- self._lastClick = 0., None
-
-
-# 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):
- self._previousDataPos = self._pixelToData(x, y)
-
- def drag(self, x, y):
- 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):
- 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.
- """
-
- 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):
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
- self.x0, self.y0 = x, y
-
- def drag(self, x1, y1):
- 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 endDrag(self, startPos, endPos):
- x0, y0 = startPos
- x1, y1 = endPos
-
- if x0 != x1 or y0 != y1: # Avoid empty zoom area
- # 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)
-
- 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 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.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')
- 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 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):
- self._processEvent(x, y, isLast=False)
-
- def drag(self, x, y):
- self._processEvent(x, y, isLast=False)
-
- def endDrag(self, startPos, endPos):
- 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._pickMarker(x, y)
- if marker is not None:
- dataPos = self.machine.plot.pixelToData(x, y)
- assert dataPos is not None
- eventDict = prepareHoverSignal(
- marker.getLegend(), '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)
-
- return True
-
- def __init__(self, plot):
- _PlotInteraction.__init__(self, plot)
-
- states = {
- 'idle': ItemsInteraction.Idle,
- 'rightClick': ClickOrDrag.RightClick,
- 'clickOrDrag': ClickOrDrag.ClickOrDrag,
- 'drag': ClickOrDrag.Drag
- }
- StateMachine.__init__(self, states, 'idle')
-
- 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:
- marker = self.plot._pickMarker(
- x, y, lambda m: m.isSelectable())
- if marker is not None:
- xData, yData = marker.getPosition()
- if xData is None:
- xData = [0, 1]
- if yData is None:
- yData = [0, 1]
-
- eventDict = prepareMarkerSignal('markerClicked',
- 'left',
- marker.getLegend(),
- 'marker',
- marker.isDraggable(),
- marker.isSelectable(),
- (xData, yData),
- (x, y), None)
- return eventDict
-
- else:
- picked = self.plot._pickImageOrCurve(
- x, y, lambda item: item.isSelectable())
-
- if picked is None:
- pass
-
- elif picked[0] == 'curve':
- curve = picked[1]
- indices = picked[2]
-
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
-
- xData = curve.getXData(copy=False)
- yData = curve.getYData(copy=False)
-
- eventDict = prepareCurveSignal('left',
- curve.getLegend(),
- 'curve',
- xData[indices],
- yData[indices],
- dataPos[0], dataPos[1],
- x, y)
- return eventDict
-
- elif picked[0] == 'image':
- image = picked[1]
-
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
-
- # Get corresponding coordinate in image
- origin = image.getOrigin()
- scale = image.getScale()
- column = int((dataPos[0] - origin[0]) / float(scale[0]))
- row = int((dataPos[1] - origin[1]) / float(scale[1]))
- eventDict = prepareImageSignal('left',
- image.getLegend(),
- 'image',
- 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.getLegend(),
- 'marker',
- marker.isDraggable(),
- marker.isSelectable(),
- (xData, yData),
- (x, y),
- posDataCursor)
- self.plot.notify(**eventDict)
-
- def beginDrag(self, x, y):
- """Handle begining of drag interaction
-
- :param x: X position of the mouse in pixels
- :param y: Y position of the mouse in pixels
- :return: True if drag is catched by an item, False otherwise
- """
- self._lastPos = self.plot.pixelToData(x, y)
- assert self._lastPos is not None
-
- self.imageLegend = None
- self.markerLegend = None
- marker = self.plot._pickMarker(
- x, y, lambda m: m.isDraggable())
-
- if marker is not None:
- self.markerLegend = marker.getLegend()
- self._signalMarkerMovingEvent('markerMoving', marker, x, y)
- else:
- picked = self.plot._pickImageOrCurve(
- x,
- y,
- lambda item:
- hasattr(item, 'isDraggable') and item.isDraggable())
- if picked is None:
- self.imageLegend = None
- self.plot.setGraphCursorShape()
- return False
- else:
- assert picked[0] == 'image' # For now only drag images
- self.imageLegend = picked[1].getLegend()
- return True
-
- def drag(self, x, y):
- dataPos = self.plot.pixelToData(x, y)
- assert dataPos is not None
- xData, yData = dataPos
-
- if self.markerLegend is not None:
- marker = self.plot._getMarker(self.markerLegend)
- if marker is not None:
- marker.setPosition(xData, yData)
-
- self._signalMarkerMovingEvent(
- 'markerMoving', marker, x, y)
-
- if self.imageLegend is not None:
- image = self.plot.getImage(self.imageLegend)
- origin = image.getOrigin()
- xImage = origin[0] + xData - self._lastPos[0]
- yImage = origin[1] + yData - self._lastPos[1]
- image.setOrigin((xImage, yImage))
-
- self._lastPos = xData, yData
-
- def endDrag(self, startPos, endPos):
- if self.markerLegend is not None:
- marker = self.plot._getMarker(self.markerLegend)
- posData = list(marker.getPosition())
- if posData[0] is None:
- posData[0] = [0, 1]
- if posData[1] is None:
- posData[1] = [0, 1]
-
- eventDict = prepareMarkerSignal(
- 'markerMoved',
- 'left',
- marker.getLegend(),
- 'marker',
- marker.isDraggable(),
- marker.isSelectable(),
- posData)
- self.plot.notify(**eventDict)
-
- self.plot.setGraphCursorShape()
-
- del self.markerLegend
- del self.imageLegend
- del self._lastPos
-
- def cancel(self):
- self.plot.setGraphCursorShape()
-
-
-class ItemsInteractionForCombo(ItemsInteraction):
- """Interaction with items to combine through :class:`FocusManager`.
- """
-
- class Idle(ItemsInteraction.Idle):
- def onPress(self, x, y, btn):
- if btn == LEFT_BTN:
- def test(item):
- return (item.isSelectable() or
- (isinstance(item, items.DraggableMixIn) and
- item.isDraggable()))
-
- picked = self.machine.plot._pickMarker(x, y, test)
- if picked is not None:
- itemInteraction = True
-
- else:
- picked = self.machine.plot._pickImageOrCurve(x, y, test)
- itemInteraction = picked is not None
-
- if itemInteraction: # Request focus and handle interaction
- self.goto('clickOrDrag', x, y)
- return True
- else: # Do not request focus
- return False
-
- elif btn == RIGHT_BTN:
- self.goto('rightClick', x, y)
- return True
-
- def __init__(self, plot):
- _PlotInteraction.__init__(self, plot)
-
- states = {
- 'idle': ItemsInteractionForCombo.Idle,
- 'rightClick': ClickOrDrag.RightClick,
- 'clickOrDrag': ClickOrDrag.ClickOrDrag,
- 'drag': ClickOrDrag.Drag
- }
- StateMachine.__init__(self, states, 'idle')
-
-
-# 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):
- 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):
- 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 onPress(self, x, y, 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):
- 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):
- """Handle start drag and switching between zoom and item drag.
-
- :param x: X position in pixels
- :param y: Y position in pixels
- """
- self._doZoom = not super(ZoomAndSelect, self).beginDrag(x, y)
- if self._doZoom:
- self._zoom.beginDrag(x, y)
-
- def drag(self, x, y):
- """Handle drag, eventually forwarding to zoom.
-
- :param x: X position in pixels
- :param y: Y position in pixels
- """
- if self._doZoom:
- return self._zoom.drag(x, y)
- else:
- return super(ZoomAndSelect, self).drag(x, y)
-
- def endDrag(self, startPos, endPos):
- """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
- """
- if self._doZoom:
- return self._zoom.endDrag(startPos, endPos)
- else:
- return super(ZoomAndSelect, self).endDrag(startPos, endPos)
-
-
-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):
- """Handle start drag and switching between zoom and item drag.
-
- :param x: X position in pixels
- :param y: Y position in pixels
- """
- self._doPan = not super(PanAndSelect, self).beginDrag(x, y)
- if self._doPan:
- self._pan.beginDrag(x, y)
-
- def drag(self, x, y):
- """Handle drag, eventually forwarding to zoom.
-
- :param x: X position in pixels
- :param y: Y position in pixels
- """
- if self._doPan:
- return self._pan.drag(x, y)
- else:
- return super(PanAndSelect, self).drag(x, y)
-
- def endDrag(self, startPos, endPos):
- """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
- """
- if self._doPan:
- return self._pan.endDrag(startPos, endPos)
- else:
- return super(PanAndSelect, self).endDrag(startPos, endPos)
-
-
-# Interaction mode control ####################################################
-
-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,
- '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', '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, FocusManager):
- drawHandler = self._eventHandler.eventHandlers[1]
- if not isinstance(drawHandler, Select):
- raise RuntimeError('Unknown interactive mode')
-
- result = drawHandler.parameters.copy()
- result['mode'] = 'draw'
- return result
-
- elif isinstance(self._eventHandler, Select):
- result = self._eventHandler.parameters.copy()
- result['mode'] = 'draw'
- return result
-
- elif isinstance(self._eventHandler, PanAndSelect):
- return {'mode': 'pan'}
-
- else:
- return {'mode': 'select'}
-
- 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 color not in (None, 'video inverted'):
- color = colors.rgba(color)
-
- if mode in ('draw', 'select-draw'):
- assert shape in self._DRAW_MODES
- eventHandlerClass = self._DRAW_MODES[shape]
- parameters = {
- 'shape': shape,
- 'label': label,
- 'color': color,
- 'width': width,
- }
- eventHandler = eventHandlerClass(plot, parameters)
-
- self._eventHandler.cancel()
-
- if mode == 'draw':
- self._eventHandler = eventHandler
-
- else: # mode == 'select-draw'
- self._eventHandler = FocusManager(
- (ItemsInteractionForCombo(plot), eventHandler))
-
- 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 f6291b5..0000000
--- a/silx/gui/plot/PlotToolButtons.py
+++ /dev/null
@@ -1,419 +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.
-#
-# ###########################################################################*/
-"""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 .items import SymbolMixIn
-
-
-_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"
-
- sumAction = self._createAction('sum')
- sumAction.triggered.connect(self.setSum)
- sumAction.setIconVisibleInMenu(True)
-
- meanAction = self._createAction('mean')
- meanAction.triggered.connect(self.setMean)
- meanAction.setIconVisibleInMenu(True)
-
- menu = qt.QMenu(self)
- menu.addAction(sumAction)
- menu.addAction(meanAction)
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
- self.setMean()
-
- def _createAction(self, method):
- icon = self.STATE[method, "icon"]
- text = self.STATE[method, "action"]
- return qt.QAction(icon, text, self)
-
- def setSum(self):
- """Configure the plot to use y-axis upward"""
- self._method = 'sum'
- self.sigMethodChanged.emit(self._method)
- self._update()
-
- def _update(self):
- icon = self.STATE[self._method, "icon"]
- toolTip = self.STATE[self._method, "state"]
- self.setIcon(icon)
- self.setToolTip(toolTip)
-
- def setMean(self):
- """Configure the plot to use y-axis downward"""
- self._method = 'mean'
- self.sigMethodChanged.emit(self._method)
- self._update()
-
-
-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)
-
- profile1DAction = self._createAction(1)
- profile1DAction.triggered.connect(self.computeProfileIn1D)
- profile1DAction.setIconVisibleInMenu(True)
-
- profile2DAction = self._createAction(2)
- profile2DAction.triggered.connect(self.computeProfileIn2D)
- profile2DAction.setIconVisibleInMenu(True)
-
- menu = qt.QMenu(self)
- menu.addAction(profile1DAction)
- menu.addAction(profile2DAction)
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
- menu.setTitle('Select profile dimension')
-
- 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.sigDimensionChanged.emit(profileDimension)
-
- def computeProfileIn1D(self):
- self._profileDimensionChanged(1)
-
- def computeProfileIn2D(self):
- self._profileDimensionChanged(2)
-
-
-class SymbolToolButton(PlotToolButton):
- """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)
-
- # Size slider
-
- slider = qt.QSlider(qt.Qt.Horizontal)
- slider.setRange(1, 20)
- slider.setValue(SymbolMixIn._DEFAULT_SYMBOL_SIZE)
- slider.setTracking(False)
- slider.valueChanged.connect(self._sizeChanged)
- widgetAction = qt.QWidgetAction(menu)
- widgetAction.setDefaultWidget(slider)
- menu.addAction(widgetAction)
-
- menu.addSeparator()
-
- # Marker actions
-
- 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)
-
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
-
- 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(withhidden=True):
- 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(withhidden=True):
- if isinstance(item, SymbolMixIn):
- item.setSymbol(marker)
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 100644
index e023a21..0000000
--- a/silx/gui/plot/PlotWidget.py
+++ /dev/null
@@ -1,3228 +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.
-# ###########################################################################*/
-"""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__ = "12/10/2018"
-
-
-from collections import OrderedDict, namedtuple
-from contextlib import contextmanager
-import datetime as dt
-import itertools
-import logging
-
-import numpy
-
-import silx
-from silx.utils.weakref import WeakMethodProxy
-from silx.utils import deprecation
-from silx.utils.property import classproperty
-from silx.utils.deprecation import deprecated
-# Import matplotlib backend here to init matplotlib our way
-from .backends.BackendMatplotlib import BackendMatplotlibQt
-
-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
-
-_logger = logging.getLogger(__name__)
-
-
-_COLORDICT = colors.COLORDICT
-_COLORLIST = silx.config.DEFAULT_PLOT_CURVE_COLORS
-
-"""
-Object returned when requesting the data range.
-"""
-_PlotDataRange = namedtuple('PlotDataRange',
- ['x', 'y', 'yright'])
-
-
-class PlotWidget(qt.QMainWindow):
- """Qt Widget providing a 1D/2D plot.
-
- This widget is a QMainWindow.
- 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
- @deprecation.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.
- """
-
- 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.
- """
-
- def __init__(self, parent=None, backend=None,
- legends=False, callback=None, **kw):
- self._autoreplot = False
- self._dirty = False
- self._cursorInPlot = False
- self.__muteActiveItemChanged = False
-
- if kw:
- _logger.warning(
- 'deprecated: __init__ extra arguments: %s', str(kw))
- if legends:
- _logger.warning('deprecated: __init__ legend argument')
- if callback:
- _logger.warning('deprecated: __init__ callback argument')
-
- 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')
-
- if backend is None:
- backend = silx.config.DEFAULT_PLOT_BACKEND
-
- if hasattr(backend, "__call__"):
- self._backend = backend(self, parent)
-
- elif hasattr(backend, "lower"):
- lowerCaseString = backend.lower()
- if lowerCaseString in ("matplotlib", "mpl"):
- backendClass = BackendMatplotlibQt
- elif lowerCaseString in ('gl', 'opengl'):
- from .backends.BackendOpenGL import BackendOpenGL
- backendClass = BackendOpenGL
- elif lowerCaseString == 'none':
- from .backends.BackendBase import BackendBase as backendClass
- else:
- raise ValueError("Backend not supported %s" % backend)
- self._backend = backendClass(self, parent)
-
- else:
- raise ValueError("Backend not supported %s" % str(backend))
-
- self.setCallback() # set _callback
-
- # Items handling
- self._content = OrderedDict()
- self._contentToUpdate = [] # Used as an OrderedSet
-
- self._dataRange = None
-
- # line types
- self._styleList = ['-', '--', '-.', ':']
- self._colorIndex = 0
- self._styleIndex = 0
-
- self._activeCurveSelectionMode = "atmostone"
- self._activeCurveStyle = CurveStyle(color='#000000')
- self._activeLegend = {'curve': None, 'image': None,
- 'scatter': None}
-
- # default properties
- self._cursorConfiguration = None
-
- self._xAxis = items.XAxis(self)
- self._yAxis = items.YAxis(self)
- self._yRightAxis = items.YRightAxis(self, self._yAxis)
-
- self._grid = None
- self._graphTitle = ''
-
- self.setGraphTitle()
- self.setGraphXLabel()
- self.setGraphYLabel()
- self.setGraphYLabel('', axis='right')
-
- self.setDefaultColormap() # Init default colormap
-
- self.setDefaultPlotPoints(False)
- self.setDefaultPlotLines(True)
-
- self._limitsHistory = LimitsHistory(self)
-
- self._eventHandler = PlotInteraction.PlotInteraction(self)
- self._eventHandler.setInteractiveMode('zoom', color=(0., 0., 0., 1.))
-
- self._pressedButtons = [] # Currently pressed mouse buttons
-
- self._defaultDataMargins = (0., 0., 0., 0.)
-
- # Only activate autoreplot at the end
- # This avoids errors when loaded in Qt designer
- self._dirty = False
- self._autoreplot = True
-
- widget = self.getWidgetHandle()
- if widget is not None:
- self.setCentralWidget(widget)
- else:
- _logger.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')
-
- # TODO: Can be removed for silx 0.10
- @staticmethod
- @deprecation.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 _getDirtyPlot(self):
- """Return the plot dirty flag.
-
- If False, the plot has not changed since last replot.
- If True, the full plot need to be redrawn.
- If 'overlay', only the overlay has changed since last replot.
-
- It can be accessed by backend to check the dirty state.
-
- :return: False, True, 'overlay'
- """
- return self._dirty
-
- def _setDirtyPlot(self, overlayOnly=False):
- """Mark the plot as needing redraw
-
- :param bool overlayOnly: True to redraw only the overlay,
- False to redraw everything
- """
- wasDirty = self._dirty
-
- if not self._dirty and overlayOnly:
- self._dirty = 'overlay'
- else:
- self._dirty = True
-
- if self._autoreplot and not wasDirty and self.isVisible():
- self._backend.postRedisplay()
-
- 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._content.values():
- if item.isVisible():
- bounds = item.getBounds()
- if bounds is not None:
- xMin = numpy.nanmin([xMin, bounds[0]])
- xMax = numpy.nanmax([xMax, bounds[1]])
- # Take care of right axis
- if (isinstance(item, items.YAxisMixIn) and
- item.getYAxis() == 'right'):
- yMinRight = numpy.nanmin([yMinRight, bounds[2]])
- yMaxRight = numpy.nanmax([yMaxRight, bounds[3]])
- else:
- yMinLeft = numpy.nanmin([yMinLeft, bounds[2]])
- yMaxLeft = numpy.nanmax([yMaxLeft, bounds[3]])
-
- def lGetRange(x, y):
- return None if numpy.isnan(x) and numpy.isnan(y) else (x, y)
- xRange = lGetRange(xMin, xMax)
- yLeftRange = lGetRange(yMinLeft, yMaxLeft)
- yRightRange = lGetRange(yMinRight, yMaxRight)
-
- self._dataRange = _PlotDataRange(x=xRange,
- y=yLeftRange,
- yright=yRightRange)
-
- def getDataRange(self):
- """
- Returns this PlotWidget's data range.
-
- :return: a namedtuple with the following members :
- x, y (left y axis), yright. Each member is a tuple (min, max)
- or None if no data is associated with the axis.
- :rtype: namedtuple
- """
- if self._dataRange is None:
- self._updateDataRange()
- return self._dataRange
-
- # Content management
-
- @staticmethod
- def _itemKey(item):
- """Build the key of given :class:`Item` in the plot
-
- :param Item item: The item to make the key from
- :return: (legend, kind)
- :rtype: (str, str)
- """
- if isinstance(item, items.Curve):
- kind = 'curve'
- elif isinstance(item, items.ImageBase):
- kind = 'image'
- elif isinstance(item, items.Scatter):
- kind = 'scatter'
- elif isinstance(item, (items.Marker,
- items.XMarker, items.YMarker)):
- kind = 'marker'
- elif isinstance(item, items.Shape):
- kind = 'item'
- elif isinstance(item, items.Histogram):
- kind = 'histogram'
- else:
- raise ValueError('Unsupported item type %s' % type(item))
-
- return item.getLegend(), kind
-
- def _add(self, item):
- """Add the given :class:`Item` to the plot.
-
- :param Item item: The item to append to the plot content
- """
- key = self._itemKey(item)
- if key in self._content:
- raise RuntimeError('Item already in the plot')
-
- # Add item to plot
- self._content[key] = item
- item._setPlot(self)
- if item.isVisible():
- self._itemRequiresUpdate(item)
- if isinstance(item, items.DATA_ITEMS):
- self._invalidateDataRange() # TODO handle this automatically
-
- self._notifyContentChanged(item)
- self.sigItemAdded.emit(item)
-
- def _notifyContentChanged(self, item):
- legend, kind = self._itemKey(item)
- self.notify('contentChanged', action='add', kind=kind, legend=legend)
-
- def _remove(self, item):
- """Remove the given :class:`Item` from the plot.
-
- :param Item item: The item to remove from the plot content
- """
- key = self._itemKey(item)
- if key not in self._content:
- raise RuntimeError('Item not in the plot')
-
- self.sigItemAboutToBeRemoved.emit(item)
-
- legend, kind = key
-
- if kind in self._ACTIVE_ITEM_KINDS:
- if self._getActiveItem(kind) == item:
- # Reset active item
- self._setActiveItem(kind, None)
-
- # Remove item from plot
- self._content.pop(key)
- if item in self._contentToUpdate:
- self._contentToUpdate.remove(item)
- if item.isVisible():
- self._setDirtyPlot(overlayOnly=item.isOverlay())
- if item.getBounds() is not None:
- self._invalidateDataRange()
- item._removeBackendRenderer(self._backend)
- item._setPlot(None)
-
- if (kind == 'curve' and not self.getAllCurves(just_legend=True,
- withhidden=True)):
- self._resetColorAndStyle()
-
- self.notify('contentChanged', action='remove',
- kind=kind, legend=legend)
-
- def _itemRequiresUpdate(self, item):
- """Called by items in the plot for asynchronous update
-
- :param Item item: The item that required update
- """
- assert item.getPlot() == self
- # Pu item at the end of the list
- if item in self._contentToUpdate:
- self._contentToUpdate.remove(item)
- self._contentToUpdate.append(item)
- self._setDirtyPlot(overlayOnly=item.isOverlay())
-
- @contextmanager
- def _muteActiveItemChangedSignal(self):
- self.__muteActiveItemChanged = True
- yield
- self.__muteActiveItemChanged = False
-
- # Add
-
- # add * input arguments management:
- # If an arg is set, then use it.
- # Else:
- # If a curve with the same legend exists, then use its arg value
- # Else, use a default value.
- # Store used value.
- # This value is used when curve is updated either internally or by user.
-
- def addCurve(self, x, y, legend=None, info=None,
- replace=False, replot=None,
- color=None, symbol=None,
- linewidth=None, linestyle=None,
- xlabel=None, ylabel=None, yaxis=None,
- xerror=None, yerror=None, z=None, selectable=None,
- fill=None, resetzoom=True,
- histogram=None, copy=True, **kw):
- """Add a 1D curve given by x an y to the graph.
-
- Curves are uniquely identified by their legend.
- To add multiple curves, call :meth:`addCurve` multiple times with
- different legend argument.
- To replace an existing curve, call :meth:`addCurve` with the
- existing curve legend.
- If you want to display the curve values as an histogram see the
- histogram parameter or :meth:`addHistogram`.
-
- When curve parameters are not provided, if a curve with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- If you attempt to plot an histogram you can set edges values in x.
- In this case len(x) = len(y) + 1.
- 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 (the default) to delete already existing
- curves
- :param color: color(s) to be used
- :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or
- one of the predefined color names defined in colors.py
- :param str symbol: Symbol to be drawn at each (x, y) position::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross
- - 'x' x-cross
- - 'd' diamond
- - 's' square
- - None (the default) to use default symbol
-
- :param float linewidth: The width of the curve in pixels (Default: 1).
- :param str linestyle: Type of line::
-
- - ' ' no line
- - '-' solid line
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
- - None (the default) to use default line style
-
- :param str xlabel: Label to show on the X axis when the curve is active
- or None to keep default axis label.
- :param str ylabel: Label to show on the Y axis when the curve is active
- or None to keep default axis label.
- :param str yaxis: The Y axis this curve is attached to.
- Either 'left' (the default) or 'right'
- :param xerror: Values with the uncertainties on the x values
- :type xerror: A float, or a numpy.ndarray of float32.
- If it is an array, it can either be a 1D array of
- same length as the data or a 2D array with 2 rows
- of same length as the data: row 0 for positive errors,
- row 1 for negative errors.
- :param yerror: Values with the uncertainties on the y values
- :type yerror: A float, or a numpy.ndarray of float32. See xerror.
- :param int z: Layer on which to draw the curve (default: 1)
- This allows to control the overlay.
- :param bool selectable: Indicate if the curve can be selected.
- (Default: True)
- :param bool fill: True to fill the curve, False otherwise (default).
- :param bool resetzoom: True (the default) to reset the zoom.
- :param str histogram: if not None then the curve will be draw as an
- histogram. The step for each values of the curve can be set to the
- left, center or right of the original x curve values.
- If histogram is not None and len(x) == len(y)+1 then x is directly
- take as edges of the histogram.
- Type of histogram::
-
- - None (default)
- - 'left'
- - 'right'
- - 'center'
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- :returns: The key string identify this curve
- """
- # Deprecation warnings
- if replot is not None:
- _logger.warning(
- 'addCurve deprecated replot argument, use resetzoom instead')
- resetzoom = replot and resetzoom
-
- if kw:
- _logger.warning('addCurve: deprecated extra arguments')
-
- # This is an histogram, use addHistogram
- if histogram is not None:
- histoLegend = self.addHistogram(histogram=y,
- edges=x,
- legend=legend,
- color=color,
- fill=fill,
- align=histogram,
- copy=copy)
- histo = self.getHistogram(histoLegend)
-
- histo.setInfo(info)
- if linewidth is not None:
- histo.setLineWidth(linewidth)
- if linestyle is not None:
- histo.setLineStyle(linestyle)
- if xlabel is not None:
- _logger.warning(
- 'addCurve: Histogram does not support xlabel argument')
- if ylabel is not None:
- _logger.warning(
- 'addCurve: Histogram does not support ylabel argument')
- if yaxis is not None:
- histo.setYAxis(yaxis)
- if z is not None:
- histo.setZValue(z)
- if selectable is not None:
- _logger.warning(
- 'addCurve: Histogram does not support selectable argument')
-
- return
-
- legend = 'Unnamed curve 1.1' if legend is None else str(legend)
-
- # Check if curve was previously active
- wasActive = self.getActiveCurve(just_legend=True) == legend
-
- 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._setLegend(legend)
- # Set default color, linestyle and symbol
- default_color, default_linestyle = self._getColorAndStyle()
- curve.setColor(default_color)
- curve.setLineStyle(default_linestyle)
- curve.setSymbol(self._defaultPlotPoints)
-
- # Do not emit sigActiveCurveChanged,
- # it will be sent once with _setActiveItem
- with self._muteActiveItemChangedSignal():
- # Override previous/default values with provided ones
- curve.setInfo(info)
- if color is not None:
- curve.setColor(color)
- if symbol is not None:
- curve.setSymbol(symbol)
- if linewidth is not None:
- curve.setLineWidth(linewidth)
- if linestyle is not None:
- curve.setLineStyle(linestyle)
- if xlabel is not None:
- curve._setXLabel(xlabel)
- if ylabel is not None:
- curve._setYLabel(ylabel)
- if yaxis is not None:
- curve.setYAxis(yaxis)
- if z is not None:
- curve.setZValue(z)
- if selectable is not None:
- curve._setSelectable(selectable)
- if fill is not None:
- curve.setFill(fill)
-
- # Set curve data
- # If errors not provided, reuse previous ones
- # TODO: Issue if size of data change but not that of errors
- if xerror is None:
- xerror = curve.getXErrorData(copy=False)
- if yerror is None:
- yerror = curve.getYErrorData(copy=False)
-
- # 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, copy=copy)
-
- if replace: # Then remove all other curves
- for c in self.getAllCurves(withhidden=True):
- if c is not curve:
- self._remove(c)
-
- if mustBeAdded:
- self._add(curve)
- else:
- self._notifyContentChanged(curve)
-
- if wasActive:
- self.setActiveCurve(curve.getLegend())
- 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.getLegend())
-
- if resetzoom:
- # We ask for a zoom reset in order to handle the plot scaling
- # if the user does not want that, autoscale of the different
- # axes has to be set to off.
- self.resetZoom()
-
- return legend
-
- def addHistogram(self,
- histogram,
- edges,
- legend=None,
- color=None,
- fill=None,
- align='center',
- resetzoom=True,
- copy=True):
- """Add an histogram to the graph.
-
- This is NOT computing the histogram, this method takes as parameter
- already computed histogram values.
-
- Histogram are uniquely identified by their legend.
- To add multiple histograms, call :meth:`addHistogram` multiple times
- with different legend argument.
-
- When histogram parameters are not provided, if an histogram with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray histogram: The values of the histogram.
- :param numpy.ndarray edges:
- The bin edges of the histogram.
- If histogram and edges have the same length, the bin edges
- are computed according to the align parameter.
- :param str legend:
- The legend to be associated to the histogram (or None)
- :param color: color to be used
- :type color: str ("#RRGGBB") or RGB unsigned byte array or
- one of the predefined color names defined in colors.py
- :param bool fill: True to fill the curve, False otherwise (default).
- :param str align:
- In case histogram values and edges have the same length N,
- the N+1 bin edges are computed according to the alignment in:
- 'center' (default), 'left', 'right'.
- :param bool resetzoom: True (the default) to reset the zoom.
- :param bool copy: True make a copy of the data (default),
- False to use provided arrays.
- :returns: The key string identify this histogram
- """
- legend = 'Unnamed histogram' if legend is None else str(legend)
-
- # Create/Update histogram object
- histo = self.getHistogram(legend)
- mustBeAdded = histo is None
- if histo is None:
- # No previous histogram, create a default one and
- # add it to the plot
- histo = items.Histogram()
- histo._setLegend(legend)
- histo.setColor(self._getColorAndStyle()[0])
-
- # Override previous/default values with provided ones
- if color is not None:
- histo.setColor(color)
- if fill is not None:
- histo.setFill(fill)
-
- # Set histogram data
- histo.setData(histogram, edges, align=align, copy=copy)
-
- if mustBeAdded:
- self._add(histo)
- else:
- self._notifyContentChanged(histo)
-
- if resetzoom:
- # We ask for a zoom reset in order to handle the plot scaling
- # if the user does not want that, autoscale of the different
- # axes has to be set to off.
- self.resetZoom()
-
- return legend
-
- def addImage(self, data, legend=None, info=None,
- replace=False, replot=None,
- xScale=None, yScale=None, z=None,
- selectable=None, draggable=None,
- colormap=None, pixmap=None,
- xlabel=None, ylabel=None,
- origin=None, scale=None,
- resetzoom=True, copy=True, **kw):
- """Add a 2D dataset or an image to the plot.
-
- It displays either an array of data using a colormap or a RGB(A) image.
-
- Images are uniquely identified by their legend.
- To add multiple images, call :meth:`addImage` multiple times with
- different legend argument.
- To replace/update an existing image, call :meth:`addImage` with the
- existing image legend.
-
- When image parameters are not provided, if an image with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray data:
- (nrows, ncolumns) data or
- (nrows, ncolumns, RGBA) ubyte array
- Note: boolean values are converted to int8.
- :param str legend: The legend to be associated to the image (or None)
- :param info: User-defined information associated to the image
- :param bool replace:
- True 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
- """
- # Deprecation warnings
- if xScale is not None or yScale is not None:
- _logger.warning(
- 'addImage deprecated xScale and yScale arguments,'
- 'use origin, scale arguments instead.')
- if origin is None and scale is None:
- origin = xScale[0], yScale[0]
- scale = xScale[1], yScale[1]
- else:
- _logger.warning(
- 'addCurve: xScale, yScale and origin, scale arguments'
- ' are conflicting. xScale and yScale are ignored.'
- ' Use only origin, scale arguments.')
-
- if replot is not None:
- _logger.warning(
- 'addImage deprecated replot argument, use resetzoom instead')
- resetzoom = replot and resetzoom
-
- if kw:
- _logger.warning('addImage: deprecated extra arguments')
-
- legend = "Unnamed Image 1.1" if legend is None else str(legend)
-
- # Check if image was previously active
- wasActive = self.getActiveImage(just_legend=True) == legend
-
- data = numpy.array(data, copy=False)
- assert data.ndim in (2, 3)
-
- image = self.getImage(legend)
- if image is not None and image.getData(copy=False).ndim != data.ndim:
- # Update a data image with RGBA image or the other way around:
- # Remove previous image
- # In this case, we don't retrieve defaults from the previous image
- self._remove(image)
- image = None
-
- mustBeAdded = image is None
- if image is None:
- # No previous image, create a default one and add it to the plot
- if data.ndim == 2:
- image = items.ImageData()
- image.setColormap(self.getDefaultColormap())
- else:
- image = items.ImageRgba()
- image._setLegend(legend)
-
- # Do not emit sigActiveImageChanged,
- # it will be sent once with _setActiveItem
- with self._muteActiveItemChangedSignal():
- # Override previous/default values with provided ones
- image.setInfo(info)
- if origin is not None:
- image.setOrigin(origin)
- if scale is not None:
- image.setScale(scale)
- if z is not None:
- image.setZValue(z)
- if selectable is not None:
- image._setSelectable(selectable)
- if draggable is not None:
- image._setDraggable(draggable)
- if colormap is not None and isinstance(image, items.ColormapMixIn):
- if isinstance(colormap, dict):
- image.setColormap(Colormap._fromDict(colormap))
- else:
- assert isinstance(colormap, Colormap)
- image.setColormap(colormap)
- if xlabel is not None:
- image._setXLabel(xlabel)
- if ylabel is not None:
- image._setYLabel(ylabel)
-
- if data.ndim == 2:
- image.setData(data, alternative=pixmap, copy=copy)
- else: # RGB(A) image
- if pixmap is not None:
- _logger.warning(
- 'addImage: pixmap argument ignored when data is RGB(A)')
- image.setData(data, copy=copy)
-
- if replace:
- for img in self.getAllImages():
- if img is not image:
- self._remove(img)
-
- if mustBeAdded:
- self._add(image)
- else:
- self._notifyContentChanged(image)
-
- if len(self.getAllImages()) == 1 or wasActive:
- self.setActiveImage(legend)
-
- if resetzoom:
- # We ask for a zoom reset in order to handle the plot scaling
- # if the user does not want that, autoscale of the different
- # axes has to be set to off.
- self.resetZoom()
-
- return legend
-
- def addScatter(self, x, y, value, legend=None, colormap=None,
- info=None, symbol=None, xerror=None, yerror=None,
- z=None, copy=True):
- """Add a (x, y, value) scatter to the graph.
-
- Scatters are uniquely identified by their legend.
- To add multiple scatters, call :meth:`addScatter` multiple times with
- different legend argument.
- To replace/update an existing scatter, call :meth:`addScatter` with the
- existing scatter legend.
-
- When scatter parameters are not provided, if a scatter with the
- same legend is displayed in the plot, its parameters are used.
-
- :param numpy.ndarray x: The data corresponding to the x coordinates.
- :param numpy.ndarray y: The data corresponding to the y coordinates
- :param numpy.ndarray value: The data value associated with each point
- :param str legend: The legend to be associated to the scatter (or None)
- :param ~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._setLegend(legend)
- scatter.setColormap(self.getDefaultColormap())
-
- # Do not emit sigActiveScatterChanged,
- # it will be sent once with _setActiveItem
- with self._muteActiveItemChangedSignal():
- # Override previous/default values with provided ones
- scatter.setInfo(info)
- if symbol is not None:
- scatter.setSymbol(symbol)
- if z is not None:
- scatter.setZValue(z)
- if colormap is not None:
- if isinstance(colormap, dict):
- scatter.setColormap(Colormap._fromDict(colormap))
- else:
- assert isinstance(colormap, Colormap)
- scatter.setColormap(colormap)
-
- # Set scatter data
- # If errors not provided, reuse previous ones
- if xerror is None:
- xerror = scatter.getXErrorData(copy=False)
- if xerror is not None and len(xerror) != len(x):
- xerror = None
- if yerror is None:
- yerror = scatter.getYErrorData(copy=False)
- if yerror is not None and len(yerror) != len(y):
- yerror = None
-
- scatter.setData(x, y, value, xerror, yerror, copy=copy)
-
- if mustBeAdded:
- self._add(scatter)
- else:
- self._notifyContentChanged(scatter)
-
- if len(self._getItems(kind="scatter")) == 1 or wasActive:
- self._setActiveItem('scatter', scatter.getLegend())
-
- return legend
-
- def addItem(self, xdata, ydata, legend=None, info=None,
- replace=False,
- shape="polygon", color='black', fill=True,
- overlay=False, z=None, **kw):
- """Add an item (i.e. a shape) to the plot.
-
- Items are uniquely identified by their legend.
- To add multiple items, call :meth:`addItem` multiple times with
- different legend argument.
- To replace/update an existing item, call :meth:`addItem` with the
- existing item legend.
-
- :param numpy.ndarray xdata: The X coords of the points of the shape
- :param numpy.ndarray ydata: The Y coords of the points of the shape
- :param str legend: The legend to be associated to the item
- :param info: User-defined information associated to the item
- :param bool replace: True (default) to delete already existing images
- :param str shape: Type of item to be drawn in
- hline, polygon (the default), rectangle, vline,
- polylines
- :param str color: Color of the item, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool fill: True (the default) to fill the shape
- :param bool overlay: True if item is an overlay (Default: False).
- This allows for rendering optimization if this
- item is changed often.
- :param int z: Layer on which to draw the item (default: 2)
- :returns: The key string identify this item
- """
- # expected to receive the same parameters as the signal
-
- if kw:
- _logger.warning('addItem deprecated parameters: %s', str(kw))
-
- legend = "Unnamed Item 1.1" if legend is None else str(legend)
-
- z = int(z) if z is not None else 2
-
- if replace:
- self.remove(kind='item')
- else:
- self.remove(legend, kind='item')
-
- item = items.Shape(shape)
- item._setLegend(legend)
- item.setInfo(info)
- item.setColor(color)
- item.setFill(fill)
- item.setOverlay(overlay)
- item.setZValue(z)
- item.setPoints(numpy.array((xdata, ydata)).T)
-
- self._add(item)
-
- return legend
-
- def addXMarker(self, x, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- **kw):
- """Add a vertical line marker to the plot.
-
- Markers are uniquely identified by their legend.
- As opposed to curves, images and items, two calls to
- :meth:`addXMarker` without legend argument adds two markers with
- different identifying legends.
-
- :param float x: Position of the marker on the X axis in data
- coordinates
- :param str legend: Legend associated to the marker to identify it
- :param str text: Text to display on the marker.
- :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool selectable: Indicate if the marker can be selected.
- (default: False)
- :param bool draggable: Indicate if the marker can be moved.
- (default: False)
- :param constraint: A function filtering marker displacement by
- dragging operations or None for no filter.
- This function is called each time a marker is
- moved.
- This parameter is only used if draggable is True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- and that returns the filtered coordinates.
- :return: The key string identify this marker
- """
- if kw:
- _logger.warning(
- 'addXMarker deprecated extra parameters: %s', str(kw))
-
- return self._addMarker(x=x, y=None, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=None, constraint=constraint)
-
- def addYMarker(self, y,
- legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- constraint=None,
- **kw):
- """Add a horizontal line marker to the plot.
-
- Markers are uniquely identified by their legend.
- As opposed to curves, images and items, two calls to
- :meth:`addYMarker` without legend argument adds two markers with
- different identifying legends.
-
- :param float y: Position of the marker on the Y axis in data
- coordinates
- :param str legend: Legend associated to the marker to identify it
- :param str text: Text to display next to the marker.
- :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool selectable: Indicate if the marker can be selected.
- (default: False)
- :param bool draggable: Indicate if the marker can be moved.
- (default: False)
- :param constraint: A function filtering marker displacement by
- dragging operations or None for no filter.
- This function is called each time a marker is
- moved.
- This parameter is only used if draggable is True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- and that returns the filtered coordinates.
- :return: The key string identify this marker
- """
- if kw:
- _logger.warning(
- 'addYMarker deprecated extra parameters: %s', str(kw))
-
- return self._addMarker(x=None, y=y, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=None, constraint=constraint)
-
- def addMarker(self, x, y, legend=None,
- text=None,
- color=None,
- selectable=False,
- draggable=False,
- symbol='+',
- constraint=None,
- **kw):
- """Add a point marker to the plot.
-
- Markers are uniquely identified by their legend.
- As opposed to curves, images and items, two calls to
- :meth:`addMarker` without legend argument adds two markers with
- different identifying legends.
-
- :param float x: Position of the marker on the X axis in data
- coordinates
- :param float y: Position of the marker on the Y axis in data
- coordinates
- :param str legend: Legend associated to the marker to identify it
- :param str text: Text to display next to the marker
- :param str color: Color of the marker, e.g., 'blue', 'b', '#FF0000'
- (Default: 'black')
- :param bool selectable: Indicate if the marker can be selected.
- (default: False)
- :param bool draggable: Indicate if the marker can be moved.
- (default: False)
- :param str symbol: Symbol representing the marker in::
-
- - 'o' circle
- - '.' point
- - ',' pixel
- - '+' cross (the default)
- - 'x' x-cross
- - 'd' diamond
- - 's' square
-
- :param constraint: A function filtering marker displacement by
- dragging operations or None for no filter.
- This function is called each time a marker is
- moved.
- This parameter is only used if draggable is True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- and that returns the filtered coordinates.
- :return: The key string identify this marker
- """
- if kw:
- _logger.warning(
- 'addMarker deprecated extra parameters: %s', str(kw))
-
- if x is None:
- xmin, xmax = self._xAxis.getLimits()
- x = 0.5 * (xmax + xmin)
-
- if y is None:
- ymin, ymax = self._yAxis.getLimits()
- y = 0.5 * (ymax + ymin)
-
- return self._addMarker(x=x, y=y, legend=legend,
- text=text, color=color,
- selectable=selectable, draggable=draggable,
- symbol=symbol, constraint=constraint)
-
- def _addMarker(self, x, y, legend,
- text, color,
- selectable, draggable,
- symbol, constraint):
- """Common method for adding point, vline and hline marker.
-
- See :meth:`addMarker` for argument documentation.
- """
- assert (x, y) != (None, None)
-
- if legend is None: # Find an unused legend
- markerLegends = self._getAllMarkers(just_legend=True)
- for index in itertools.count():
- legend = "Unnamed Marker %d" % index
- if legend not in markerLegends:
- break # Keep this legend
- legend = str(legend)
-
- if x is None:
- markerClass = items.YMarker
- elif y is None:
- markerClass = items.XMarker
- else:
- markerClass = items.Marker
-
- # Create/Update marker object
- marker = self._getMarker(legend)
- if marker is not None and not isinstance(marker, markerClass):
- _logger.warning('Adding marker with same legend'
- ' but different type replaces it')
- self._remove(marker)
- marker = None
-
- mustBeAdded = marker is None
- if marker is None:
- # No previous marker, create one
- marker = markerClass()
- marker._setLegend(legend)
-
- if text is not None:
- marker.setText(text)
- if color is not None:
- marker.setColor(color)
- if selectable is not None:
- marker._setSelectable(selectable)
- if draggable is not None:
- marker._setDraggable(draggable)
- if symbol is not None:
- marker.setSymbol(symbol)
-
- # TODO to improve, but this ensure constraint is applied
- marker.setPosition(x, y)
- if constraint is not None:
- marker._setConstraint(constraint)
- marker.setPosition(x, y)
-
- if mustBeAdded:
- self._add(marker)
- else:
- self._notifyContentChanged(marker)
-
- return legend
-
- # Hide
-
- def isCurveHidden(self, legend):
- """Returns True if the curve associated to legend is hidden, else False
-
- :param str legend: The legend key identifying the curve
- :return: True if the associated curve is hidden, False otherwise
- """
- curve = self._getItem('curve', legend)
- return curve is not None and not curve.isVisible()
-
- def hideCurve(self, legend, flag=True, replot=None):
- """Show/Hide the curve associated to legend.
-
- Even when hidden, the curve is kept in the list of curves.
-
- :param str legend: The legend associated to the curve to be hidden
- :param bool flag: True (default) to hide the curve, False to show it
- """
- if replot is not None:
- _logger.warning('hideCurve deprecated replot parameter')
-
- curve = self._getItem('curve', legend)
- if curve is None:
- _logger.warning('Curve not in plot: %s', legend)
- return
-
- isVisible = not flag
- if isVisible != curve.isVisible():
- curve.setVisible(isVisible)
-
- # Remove
-
- ITEM_KINDS = 'curve', 'image', 'scatter', 'item', 'marker', 'histogram'
- """List of supported kind of items in the plot."""
-
- _ACTIVE_ITEM_KINDS = 'curve', 'scatter', 'image'
- """List of item's kind which have a active item."""
-
- def remove(self, legend=None, kind=ITEM_KINDS):
- """Remove one or all element(s) of the given legend and kind.
-
- Examples:
-
- - ``remove()`` clears the plot
- - ``remove(kind='curve')`` removes all curves from the plot
- - ``remove('myCurve', kind='curve')`` removes the curve with
- legend 'myCurve' from the plot.
- - ``remove('myImage, kind='image')`` removes the image with
- legend 'myImage' from the plot.
- - ``remove('myImage')`` removes elements (for instance curve, image,
- item and marker) with legend 'myImage'.
-
- :param str legend: The legend associated to the element to remove,
- or None to remove
- :param kind: The kind of elements to remove from the plot.
- See :attr:`ITEM_KINDS`.
- By default, it removes all kind of elements.
- :type kind: str or tuple of str to specify multiple kinds.
- """
- if kind is 'all': # Replace all by tuple of all kinds
- kind = self.ITEM_KINDS
-
- if kind in self.ITEM_KINDS: # Kind is a str, make it a tuple
- kind = (kind,)
-
- for aKind in kind:
- assert aKind in self.ITEM_KINDS
-
- if legend is None: # This is a clear
- # Clear each given kind
- for aKind in kind:
- for legend in self._getItems(
- kind=aKind, just_legend=True, withhidden=True):
- self.remove(legend=legend, kind=aKind)
-
- else: # This is removing a single element
- # Remove each given kind
- for aKind in kind:
- item = self._getItem(aKind, legend)
- if item is not None:
- self._remove(item)
-
- def removeCurve(self, legend):
- """Remove the curve associated to legend from the graph.
-
- :param str legend: The legend associated to the curve to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='curve')
-
- def removeImage(self, legend):
- """Remove the image associated to legend from the graph.
-
- :param str legend: The legend associated to the image to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='image')
-
- def removeItem(self, legend):
- """Remove the item associated to legend from the graph.
-
- :param str legend: The legend associated to the item to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='item')
-
- def removeMarker(self, legend):
- """Remove the marker associated to legend from the graph.
-
- :param str legend: The legend associated to the marker to be deleted
- """
- if legend is None:
- return
- self.remove(legend, kind='marker')
-
- # Clear
-
- def clear(self):
- """Remove everything from the plot."""
- self.remove()
-
- def clearCurves(self):
- """Remove all the curves from the plot."""
- self.remove(kind='curve')
-
- def clearImages(self):
- """Remove all the images from the plot."""
- self.remove(kind='image')
-
- def clearItems(self):
- """Remove all the items from the plot. """
- self.remove(kind='item')
-
- def clearMarkers(self):
- """Remove all the markers from the plot."""
- self.remove(kind='marker')
-
- # Interaction
-
- def getGraphCursor(self):
- """Returns the state of the crosshair cursor.
-
- See :meth:`setGraphCursor`.
-
- :return: None if the crosshair cursor is not active,
- else a tuple (color, linewidth, linestyle).
- """
- return self._cursorConfiguration
-
- def setGraphCursor(self, flag=False, color='black',
- linewidth=1, linestyle='-'):
- """Toggle the display of a crosshair cursor and set its attributes.
-
- :param bool flag: Toggle the display of a crosshair cursor.
- The crosshair cursor is hidden by default.
- :param color: The color to use for the crosshair.
- :type color: A string (either a predefined color name in colors.py
- or "#RRGGBB")) or a 4 columns unsigned byte array
- (Default: black).
- :param int linewidth: The width of the lines of the crosshair
- (Default: 1).
- :param str linestyle: Type of line::
-
- - ' ' no line
- - '-' solid line (the default)
- - '--' dashed line
- - '-.' dash-dot line
- - ':' dotted line
- """
- if flag:
- self._cursorConfiguration = color, linewidth, linestyle
- else:
- self._cursorConfiguration = None
-
- self._backend.setGraphCursor(flag=flag, color=color,
- linewidth=linewidth, linestyle=linestyle)
- self._setDirtyPlot()
- self.notify('setGraphCursor',
- state=self._cursorConfiguration is not None)
-
- def pan(self, direction, factor=0.1):
- """Pan the graph in the given direction by the given factor.
-
- Warning: Pan of right Y axis not implemented!
-
- :param str direction: One of 'up', 'down', 'left', 'right'.
- :param float factor: Proportion of the range used to pan the graph.
- Must be strictly positive.
- """
- assert direction in ('up', 'down', 'left', 'right')
- assert factor > 0.
-
- if direction in ('left', 'right'):
- xFactor = factor if direction == 'right' else - factor
- xMin, xMax = self._xAxis.getLimits()
-
- xMin, xMax = _utils.applyPan(xMin, xMax, xFactor,
- self._xAxis.getScale() == self._xAxis.LOGARITHMIC)
- self._xAxis.setLimits(xMin, xMax)
-
- else: # direction in ('up', 'down')
- sign = -1. if self._yAxis.isInverted() else 1.
- yFactor = sign * (factor if direction == 'up' else -factor)
- yMin, yMax = self._yAxis.getLimits()
- yIsLog = self._yAxis.getScale() == self._yAxis.LOGARITHMIC
-
- yMin, yMax = _utils.applyPan(yMin, yMax, yFactor, yIsLog)
- self._yAxis.setLimits(yMin, yMax)
-
- y2Min, y2Max = self._yRightAxis.getLimits()
-
- y2Min, y2Max = _utils.applyPan(y2Min, y2Max, yFactor, yIsLog)
- self._yRightAxis.setLimits(y2Min, y2Max)
-
- # Active Curve/Image
-
- def isActiveCurveHandling(self):
- """Returns True if active curve selection is enabled.
-
- :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, replot=None):
- """Make the curve associated to legend the active curve.
-
- :param legend: The legend associated to the curve
- or None to have no active curve.
- :type legend: str or None
- """
- if replot is not None:
- _logger.warning('setActiveCurve deprecated replot parameter')
-
- if not self.isActiveCurveHandling():
- return
- 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].getLegend())
-
- 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, replot=None):
- """Make the image associated to legend the active image.
-
- :param str legend: The legend associated to the image
- or None to have no active image.
- """
- if replot is not None:
- _logger.warning('setActiveImage deprecated replot parameter')
-
- return self._setActiveItem(kind='image', legend=legend)
-
- def _getActiveItem(self, kind, just_legend=False):
- """Return the currently active item of that kind if any
-
- :param str kind: Type of item: 'curve', 'scatter' or 'image'
- :param bool just_legend: True to get the legend,
- False (default) to get the item
- :return: legend or item or None if no active item
- """
- assert kind in self._ACTIVE_ITEM_KINDS
-
- if self._activeLegend[kind] is None:
- return None
-
- if (self._activeLegend[kind], kind) not in self._content:
- self._activeLegend[kind] = None
- return None
-
- if just_legend:
- return self._activeLegend[kind]
- else:
- return self._getItem(kind, self._activeLegend[kind])
-
- def _setActiveItem(self, kind, legend):
- """Make the curve associated to legend the active curve.
-
- :param str kind: Type of item: 'curve' or 'image'
- :param legend: The legend associated to the curve
- or None to have no active curve.
- :type legend: str or None
- """
- assert kind in self._ACTIVE_ITEM_KINDS
-
- xLabel = None
- yLabel = None
- yRightLabel = None
-
- oldActiveItem = self._getActiveItem(kind=kind)
-
- if oldActiveItem is not None: # Stop listening previous active image
- oldActiveItem.sigItemChanged.disconnect(self._activeItemChanged)
-
- # Curve specific: Reset highlight of previous active curve
- if kind == 'curve' and oldActiveItem is not None:
- oldActiveItem.setHighlighted(False)
-
- if legend is None:
- self._activeLegend[kind] = None
- else:
- legend = str(legend)
- item = self._getItem(kind, legend)
- if item is None:
- _logger.warning("This %s does not exist: %s", kind, legend)
- self._activeLegend[kind] = None
- else:
- self._activeLegend[kind] = legend
-
- # Curve specific: handle highlight
- if kind == 'curve':
- item.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.getLegend()
- self.notify(
- 'active' + kind[0].upper() + kind[1:] + 'Changed',
- updated=oldActiveLegend != activeLegend,
- previous=oldActiveLegend,
- legend=activeLegend)
-
- return activeLegend
-
- def _activeItemChanged(self, type_):
- """Listen for active item changed signal and broadcast signal
-
- :param item.ItemChangedType type_: The type of item change
- """
- if not self.__muteActiveItemChanged:
- item = self.sender()
- if item is not None:
- legend, kind = self._itemKey(item)
- self.notify(
- 'active' + kind[0].upper() + kind[1:] + 'Changed',
- updated=False,
- previous=legend,
- legend=legend)
-
- # Getters
-
- def getItems(self):
- """Returns the list of items in the plot
-
- :rtype: List[silx.gui.plot.items.Item]
- """
- return tuple(self._content.values())
-
- def getAllCurves(self, just_legend=False, withhidden=False):
- """Returns all curves legend or info and data.
-
- It returns an empty list in case of not having any curve.
-
- If just_legend is False, it returns a list of :class:`items.Curve`
- objects describing the curves.
- If just_legend is True, it returns a list of curves' legend.
-
- :param bool just_legend: True to get the legend of the curves,
- False (the default) to get the curves' data
- and info.
- :param bool withhidden: False (default) to skip hidden curves.
- :return: list of curves' legend or :class:`.items.Curve`
- :rtype: list of str or list of :class:`.items.Curve`
- """
- return self._getItems(kind='curve',
- just_legend=just_legend,
- withhidden=withhidden)
-
- def getCurve(self, legend=None):
- """Get the object describing a specific curve.
-
- It returns None in case no matching curve is found.
-
- :param str legend:
- The legend identifying the curve.
- If not provided or None (the default), the active curve is returned
- or if there is no active curve, the latest updated curve that is
- not hidden is returned if there are curves in the plot.
- :return: None or :class:`.items.Curve` object
- """
- return self._getItem(kind='curve', legend=legend)
-
- def getAllImages(self, just_legend=False):
- """Returns all images legend or objects.
-
- It returns an empty list in case of not having any image.
-
- If just_legend is False, it returns a list of :class:`items.ImageBase`
- objects describing the images.
- If just_legend is True, it returns a list of legends.
-
- :param bool just_legend: True to get the legend of the images,
- False (the default) to get the images'
- object.
- :return: list of images' legend or :class:`.items.ImageBase`
- :rtype: list of str or list of :class:`.items.ImageBase`
- """
- return self._getItems(kind='image',
- just_legend=just_legend,
- withhidden=True)
-
- def getImage(self, legend=None):
- """Get the object describing a specific image.
-
- It returns None in case no matching image is found.
-
- :param str legend:
- The legend identifying the image.
- If not provided or None (the default), the active image is returned
- or if there is no active image, the latest updated image
- is returned if there are images in the plot.
- :return: None or :class:`.items.ImageBase` object
- """
- return self._getItem(kind='image', legend=legend)
-
- def getScatter(self, legend=None):
- """Get the object describing a specific scatter.
-
- It returns None in case no matching scatter is found.
-
- :param str legend:
- The legend identifying the scatter.
- If not provided or None (the default), the active scatter is
- returned or if there is no active scatter, the latest updated
- scatter is returned if there are scatters in the plot.
- :return: None or :class:`.items.Scatter` object
- """
- return self._getItem(kind='scatter', legend=legend)
-
- def getHistogram(self, legend=None):
- """Get the object describing a specific histogram.
-
- It returns None in case no matching histogram is found.
-
- :param str legend:
- The legend identifying the histogram.
- If not provided or None (the default), the latest updated scatter
- is returned if there are histograms in the plot.
- :return: None or :class:`.items.Histogram` object
- """
- return self._getItem(kind='histogram', legend=legend)
-
- def _getItems(self, kind=ITEM_KINDS, just_legend=False, withhidden=False):
- """Retrieve all items of a kind in the plot
-
- :param kind: The kind of elements to retrieve from the plot.
- See :attr:`ITEM_KINDS`.
- By default, it removes all kind of elements.
- :type kind: str or tuple of str to specify multiple kinds.
- :param str kind: Type of item: 'curve' or 'image'
- :param bool just_legend: True to get the legend of the curves,
- False (the default) to get the curves' data
- and info.
- :param bool withhidden: False (default) to skip hidden curves.
- :return: list of legends or item objects
- """
- if kind is 'all': # Replace all by tuple of all kinds
- kind = self.ITEM_KINDS
-
- if kind in self.ITEM_KINDS: # Kind is a str, make it a tuple
- kind = (kind,)
-
- for aKind in kind:
- assert aKind in self.ITEM_KINDS
-
- output = []
- for (legend, type_), item in self._content.items():
- if type_ in kind and (withhidden or item.isVisible()):
- output.append(legend if just_legend else item)
- return output
-
- def _getItem(self, kind, legend=None):
- """Get an item from the plot: either an image or a curve.
-
- Returns None if no match found.
-
- :param str kind: Type of item to retrieve,
- see :attr:`ITEM_KINDS`.
- :param str legend: Legend of the item or
- None to get active or last item
- :return: Object describing the item or None
- """
- assert kind in self.ITEM_KINDS
-
- if legend is not None:
- return self._content.get((legend, kind), None)
- else:
- if kind in self._ACTIVE_ITEM_KINDS:
- item = self._getActiveItem(kind=kind)
- if item is not None: # Return active item if available
- return item
- # Return last visible item if any
- allItems = self._getItems(
- kind=kind, just_legend=False, withhidden=False)
- return allItems[-1] if allItems else None
-
- # Limits
-
- def _notifyLimitsChanged(self, emitSignal=True):
- """Send an event when plot area limits are changed."""
- xRange = self._xAxis.getLimits()
- yRange = self._yAxis.getLimits()
- y2Range = self._yRightAxis.getLimits()
- if emitSignal:
- axes = self.getXAxis(), self.getYAxis(), self.getYAxis(axis="right")
- ranges = xRange, yRange, y2Range
- for axis, limits in zip(axes, ranges):
- axis.sigLimitsChanged.emit(*limits)
- event = PlotEvents.prepareLimitsChangedSignal(
- id(self.getWidgetHandle()), xRange, yRange, y2Range)
- self.notify(**event)
-
- def getLimitsHistory(self):
- """Returns the object handling the history of limits of the plot"""
- return self._limitsHistory
-
- def getGraphXLimits(self):
- """Get the graph X (bottom) limits.
-
- :return: Minimum and maximum values of the X axis
- """
- return self._backend.getGraphXLimits()
-
- def setGraphXLimits(self, xmin, xmax, replot=None):
- """Set the graph X (bottom) limits.
-
- :param float xmin: minimum bottom axis value
- :param float xmax: maximum bottom axis value
- """
- if replot is not None:
- _logger.warning('setGraphXLimits deprecated replot parameter')
- self._xAxis.setLimits(xmin, xmax)
-
- def getGraphYLimits(self, axis='left'):
- """Get the graph Y limits.
-
- :param str axis: The axis for which to get the limits:
- Either 'left' or 'right'
- :return: Minimum and maximum values of the X axis
- """
- assert axis in ('left', 'right')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
- return yAxis.getLimits()
-
- def setGraphYLimits(self, ymin, ymax, axis='left', replot=None):
- """Set the graph Y limits.
-
- :param float ymin: minimum bottom axis value
- :param float ymax: maximum bottom axis value
- :param str axis: The axis for which to get the limits:
- Either 'left' or 'right'
- """
- if replot is not None:
- _logger.warning('setGraphYLimits deprecated replot parameter')
- assert axis in ('left', 'right')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
- return yAxis.setLimits(ymin, ymax)
-
- def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
- """Set the limits of the X and Y axes at once.
-
- If y2min or y2max is None, the right Y axis limits are not updated.
-
- :param float xmin: minimum bottom axis value
- :param float xmax: maximum bottom axis value
- :param float ymin: minimum left axis value
- :param float ymax: maximum left axis value
- :param float y2min: minimum right axis value or None (the default)
- :param float y2max: maximum right axis value or None (the default)
- """
- # Deal with incorrect values
- axis = self.getXAxis()
- xmin, xmax = axis._checkLimits(xmin, xmax)
- axis = self.getYAxis()
- ymin, ymax = axis._checkLimits(ymin, ymax)
-
- if y2min is None or y2max is None:
- # if one limit is None, both are ignored
- y2min, y2max = None, None
- else:
- axis = self.getYAxis(axis="right")
- y2min, y2max = axis._checkLimits(y2min, y2max)
-
- if self._viewConstrains:
- view = self._viewConstrains.normalize(xmin, xmax, ymin, ymax)
- xmin, xmax, ymin, ymax = view
-
- self._backend.setLimits(xmin, xmax, ymin, ymax, y2min, y2max)
- self._setDirtyPlot()
- self._notifyLimitsChanged()
-
- def _getViewConstraints(self):
- """Return the plot object managing constaints on the plot view.
-
- :rtype: ViewConstraints
- """
- if self._viewConstrains is None:
- self._viewConstrains = ViewConstraints()
- return self._viewConstrains
-
- # Title and labels
-
- def getGraphTitle(self):
- """Return the plot main title as a str."""
- return self._graphTitle
-
- def setGraphTitle(self, title=""):
- """Set the plot main title.
-
- :param str title: Main title of the plot (default: '')
- """
- self._graphTitle = str(title)
- self._backend.setGraphTitle(title)
- self._setDirtyPlot()
-
- def getGraphXLabel(self):
- """Return the current X axis label as a str."""
- return self._xAxis.getLabel()
-
- def setGraphXLabel(self, label="X"):
- """Set the plot X axis label.
-
- The provided label can be temporarily replaced by the X label of the
- active curve if any.
-
- :param str label: The X axis label (default: 'X')
- """
- self._xAxis.setLabel(label)
-
- def getGraphYLabel(self, axis='left'):
- """Return the current Y axis label as a str.
-
- :param str axis: The Y axis for which to get the label (left or right)
- """
- assert axis in ('left', 'right')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
- return yAxis.getLabel()
-
- def setGraphYLabel(self, label="Y", axis='left'):
- """Set the plot Y axis label.
-
- The provided label can be temporarily replaced by the Y label of the
- active curve if any.
-
- :param str label: The Y axis label (default: 'Y')
- :param str axis: The Y axis for which to set the label (left or right)
- """
- assert axis in ('left', 'right')
- yAxis = self._yAxis if axis == 'left' else self._yRightAxis
- return yAxis.setLabel(label)
-
- # Axes
-
- def getXAxis(self):
- """Returns the X axis
-
- .. versionadded:: 0.6
-
- :rtype: :class:`.items.Axis`
- """
- return self._xAxis
-
- def getYAxis(self, axis="left"):
- """Returns an Y axis
-
- .. versionadded:: 0.6
-
- :param str axis: The Y axis to return
- ('left' or 'right').
- :rtype: :class:`.items.Axis`
- """
- assert(axis in ["left", "right"])
- return self._yAxis if axis == "left" else self._yRightAxis
-
- def setAxesDisplayed(self, displayed):
- """Display or not the axes.
-
- :param bool displayed: If `True` axes are displayed. If `False` axes
- are not anymore visible and the margin used for them is removed.
- """
- self._backend.setAxesDisplayed(displayed)
- self._setDirtyPlot()
- self._sigAxesVisibilityChanged.emit(displayed)
-
- def _isAxesDisplayed(self):
- return self._backend.isAxesDisplayed()
-
- @property
- @deprecated(since_version='0.6')
- def sigSetYAxisInverted(self):
- """Signal emitted when Y axis orientation has changed"""
- return self._yAxis.sigInvertedChanged
-
- @property
- @deprecated(since_version='0.6')
- def sigSetXAxisLogarithmic(self):
- """Signal emitted when X axis scale has changed"""
- return self._xAxis._sigLogarithmicChanged
-
- @property
- @deprecated(since_version='0.6')
- def sigSetYAxisLogarithmic(self):
- """Signal emitted when Y axis scale has changed"""
- return self._yAxis._sigLogarithmicChanged
-
- @property
- @deprecated(since_version='0.6')
- def sigSetXAxisAutoScale(self):
- """Signal emitted when X axis autoscale has changed"""
- return self._xAxis.sigAutoScaleChanged
-
- @property
- @deprecated(since_version='0.6')
- def sigSetYAxisAutoScale(self):
- """Signal emitted when Y axis autoscale has changed"""
- return self._yAxis.sigAutoScaleChanged
-
- def setYAxisInverted(self, flag=True):
- """Set the Y axis orientation.
-
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- self._yAxis.setInverted(flag)
-
- def isYAxisInverted(self):
- """Return True if Y axis goes from top to bottom, False otherwise."""
- return self._yAxis.isInverted()
-
- def isXAxisLogarithmic(self):
- """Return True if X axis scale is logarithmic, False if linear."""
- return self._xAxis._isLogarithmic()
-
- def setXAxisLogarithmic(self, flag):
- """Set the bottom X axis scale (either linear or logarithmic).
-
- :param bool flag: True to use a logarithmic scale, False for linear.
- """
- self._xAxis._setLogarithmic(flag)
-
- def isYAxisLogarithmic(self):
- """Return True if Y axis scale is logarithmic, False if linear."""
- return self._yAxis._isLogarithmic()
-
- def setYAxisLogarithmic(self, flag):
- """Set the Y axes scale (either linear or logarithmic).
-
- :param bool flag: True to use a logarithmic scale, False for linear.
- """
- self._yAxis._setLogarithmic(flag)
-
- def isXAxisAutoScale(self):
- """Return True if X axis is automatically adjusting its limits."""
- return self._xAxis.isAutoScale()
-
- def setXAxisAutoScale(self, flag=True):
- """Set the X axis limits adjusting behavior of :meth:`resetZoom`.
-
- :param bool flag: True to resize limits automatically,
- False to disable it.
- """
- self._xAxis.setAutoScale(flag)
-
- def isYAxisAutoScale(self):
- """Return True if Y axes are automatically adjusting its limits."""
- return self._yAxis.isAutoScale()
-
- def setYAxisAutoScale(self, flag=True):
- """Set the Y axis limits adjusting behavior of :meth:`resetZoom`.
-
- :param bool flag: True to resize limits automatically,
- False to disable it.
- """
- self._yAxis.setAutoScale(flag)
-
- def isKeepDataAspectRatio(self):
- """Returns whether the plot is keeping data aspect ratio or not."""
- return self._backend.isKeepDataAspectRatio()
-
- def setKeepDataAspectRatio(self, flag=True):
- """Set whether the plot keeps data aspect ratio or not.
-
- :param bool flag: True to respect data aspect ratio
- """
- flag = bool(flag)
- self._backend.setKeepDataAspectRatio(flag=flag)
- self._setDirtyPlot()
- self._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 default Curve symbol is 'o', False for no symbol."""
- return self._defaultPlotPoints == 'o'
-
- def setDefaultPlotPoints(self, flag):
- """Set the default symbol of all curves.
-
- When called, this reset the symbol of all existing curves.
-
- :param bool flag: True to use 'o' as the default curve symbol,
- False to use no symbol.
- """
- self._defaultPlotPoints = 'o' if flag else ''
-
- # Reset symbol of all curves
- curves = self.getAllCurves(just_legend=False, withhidden=True)
-
- if curves:
- for curve in curves:
- curve.setSymbol(self._defaultPlotPoints)
-
- def isDefaultPlotLines(self):
- """Return True for line as default line style, False for no line."""
- return self._plotLines
-
- def setDefaultPlotLines(self, flag):
- """Toggle the use of lines as the default curve line style.
-
- :param bool flag: True to use a line as the default line style,
- False to use no line as the default line style.
- """
- self._plotLines = bool(flag)
-
- linestyle = '-' if self._plotLines else ' '
-
- # Reset linestyle of all curves
- curves = self.getAllCurves(withhidden=True)
-
- if curves:
- for curve in curves:
- curve.setLineStyle(linestyle)
-
- def getDefaultColormap(self):
- """Return the default colormap used by :meth:`addImage`.
-
- :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, **kw):
- """Save a snapshot of the plot.
-
- Supported file formats depends on the backend in use.
- The following file formats are always supported: "png", "svg".
- The matplotlib backend supports more formats:
- "pdf", "ps", "eps", "tiff", "jpeg", "jpg".
-
- :param filename: Destination
- :type filename: str, StringIO or BytesIO
- :param str fileFormat: String specifying the format
- :return: False if cannot save the plot, True otherwise
- """
- if kw:
- _logger.warning('Extra parameters ignored: %s', str(kw))
-
- if fileFormat is None:
- if not hasattr(filename, 'lower'):
- _logger.warning(
- 'saveGraph cancelled, cannot define file format.')
- return False
- else:
- fileFormat = (filename.split(".")[-1]).lower()
-
- supportedFormats = ("png", "svg", "pdf", "ps", "eps",
- "tif", "tiff", "jpeg", "jpg")
-
- if fileFormat not in supportedFormats:
- _logger.warning('Unsupported format %s', fileFormat)
- return False
- else:
- self._backend.saveGraph(filename,
- fileFormat=fileFormat,
- dpi=dpi)
- return True
-
- def getDataMargins(self):
- """Get the default data margin ratios, see :meth:`setDataMargins`.
-
- :return: The margin ratios for each side (xMin, xMax, yMin, yMax).
- :rtype: A 4-tuple of floats.
- """
- return self._defaultDataMargins
-
- def setDataMargins(self, xMinMargin=0., xMaxMargin=0.,
- yMinMargin=0., yMaxMargin=0.):
- """Set the default data margins to use in :meth:`resetZoom`.
-
- Set the default ratios of margins (as floats) to add around the data
- inside the plot area for each side.
- """
- self._defaultDataMargins = (xMinMargin, xMaxMargin,
- yMinMargin, yMaxMargin)
-
- def getAutoReplot(self):
- """Return True if replot is automatically handled, False otherwise.
-
- See :meth`setAutoReplot`.
- """
- return self._autoreplot
-
- def setAutoReplot(self, autoreplot=True):
- """Set automatic replot mode.
-
- When enabled, the plot is redrawn automatically when changed.
- When disabled, the plot is not redrawn when its content change.
- Instead, it :meth:`replot` must be called.
-
- :param bool autoreplot: True to enable it (default),
- False to disable it.
- """
- self._autoreplot = bool(autoreplot)
-
- # If the plot is dirty before enabling autoreplot,
- # then _backend.postRedisplay will never be called from _setDirtyPlot
- if self._autoreplot and self._getDirtyPlot():
- self._backend.postRedisplay()
-
- def replot(self):
- """Redraw the plot immediately."""
- for item in self._contentToUpdate:
- item._update(self._backend)
- self._contentToUpdate = []
- self._backend.replot()
- self._dirty = False # reset dirty flag
-
- def _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 = None, None
- else:
- ymin2, ymax2 = ranges.yright
-
- # Add margins around data inside the plot area
- newLimits = list(_utils.addMarginsToLimits(
- dataMargins,
- self._xAxis._isLogarithmic(),
- self._yAxis._isLogarithmic(),
- xmin, xmax, ymin, ymax, ymin2, ymax2))
-
- if self.isKeepDataAspectRatio():
- # Use limits with margins to keep ratio
- xmin, xmax, ymin, ymax = newLimits[:4]
-
- # Compute bbox wth figure aspect ratio
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
- 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")
- return self._backend.pixelToData(x, y, axis=axis, check=check)
-
- def getPlotBoundsInPixels(self):
- """Plot area bounds in widget coordinates in pixels.
-
- :return: bounds as a 4-tuple of int: (left, top, width, height)
- """
- return self._backend.getPlotBoundsInPixels()
-
- # Interaction support
-
- def setGraphCursorShape(self, cursor=None):
- """Set the cursor shape.
-
- :param str cursor: Name of the cursor shape
- """
- self._backend.setGraphCursorShape(cursor)
-
- def _pickMarker(self, x, y, test=None):
- """Pick a marker at the given position.
-
- To use for interaction implementation.
-
- :param float x: X position in pixels.
- :param float y: Y position in pixels.
- :param test: A callable to call for each picked marker to filter
- picked markers. If None (default), do not filter markers.
- """
- if test is None:
- def test(mark):
- return True
-
- markers = self._backend.pickItems(x, y, kinds=('marker',))
- legends = [m['legend'] for m in markers if m['kind'] == 'marker']
-
- for legend in reversed(legends):
- marker = self._getMarker(legend)
- if marker is not None and test(marker):
- return marker
- return None
-
- def _getAllMarkers(self, just_legend=False):
- """Returns all markers' legend or objects
-
- :param bool just_legend: True to get the legend of the markers,
- False (the default) to get marker objects.
- :return: list of legend of list of marker objects
- :rtype: list of str or list of marker objects
- """
- return self._getItems(
- kind='marker', just_legend=just_legend, withhidden=True)
-
- def _getMarker(self, legend=None):
- """Get the object describing a specific marker.
-
- It returns None in case no matching marker is found
-
- :param str legend: The legend of the marker to retrieve
- :rtype: None of marker object
- """
- return self._getItem(kind='marker', legend=legend)
-
- def _pickImageOrCurve(self, x, y, test=None):
- """Pick an image or a curve at the given position.
-
- To use for interaction implementation.
-
- :param float x: X position in pixels
- :param float y: Y position in pixels
- :param test: A callable to call for each picked item to filter
- picked items. If None (default), do not filter items.
- """
- if test is None:
- def test(i):
- return True
-
- allItems = self._backend.pickItems(x, y, kinds=('curve', 'image'))
- allItems = [item for item in allItems
- if item['kind'] in ['curve', 'image']]
-
- for item in reversed(allItems):
- kind, legend = item['kind'], item['legend']
- if kind == 'curve':
- curve = self.getCurve(legend)
- if curve is not None and test(curve):
- return kind, curve, item['indices']
-
- elif kind == 'image':
- image = self.getImage(legend)
- if image is not None and test(image):
- return kind, image, None
-
- else:
- _logger.warning('Unsupported kind: %s', kind)
-
- return None
-
- def _pick(self, x, y):
- """Pick items in the plot at given position.
-
- :param float x: X position in pixels
- :param float y: Y position in pixels
- :return: Iterable of (plot item, indices) at picked position.
- Items are ordered from back to front.
- """
- items = []
-
- # Convert backend result to plot items
- for itemInfo in self._backend.pickItems(
- x, y, kinds=('marker', 'curve', 'image')):
- kind, legend = itemInfo['kind'], itemInfo['legend']
-
- if kind in ('marker', 'image'):
- item = self._getItem(kind=kind, legend=legend)
- indices = None # TODO compute indices for images
-
- else: # backend kind == 'curve'
- for kind in ('curve', 'histogram', 'scatter'):
- item = self._getItem(kind=kind, legend=legend)
- if item is not None:
- indices = itemInfo['indices']
- break
- else:
- _logger.error(
- 'Cannot find corresponding picked item')
- continue
- items.append((item, indices))
-
- return tuple(items)
-
- # User event handling #
-
- def _isPositionInPlotArea(self, x, y):
- """Project position in pixel to the closest point in the plot area
-
- :param float x: X coordinate in widget coordinate (in pixel)
- :param float y: Y coordinate in widget coordinate (in pixel)
- :return: (x, y) in widget coord (in pixel) in the plot area
- """
- left, top, width, height = self.getPlotBoundsInPixels()
- xPlot = numpy.clip(x, left, left + width)
- yPlot = numpy.clip(y, top, top + height)
- return xPlot, yPlot
-
- def onMousePress(self, xPixel, yPixel, btn):
- """Handle mouse press event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- :param str btn: Mouse button in 'left', 'middle', 'right'
- """
- if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
- self._pressedButtons.append(btn)
- self._eventHandler.handleEvent('press', xPixel, yPixel, btn)
-
- def onMouseMove(self, xPixel, yPixel):
- """Handle mouse move event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- """
- inXPixel, inYPixel = self._isPositionInPlotArea(xPixel, yPixel)
- isCursorInPlot = inXPixel == xPixel and inYPixel == yPixel
-
- if self._cursorInPlot != isCursorInPlot:
- self._cursorInPlot = isCursorInPlot
- self._eventHandler.handleEvent(
- 'enter' if self._cursorInPlot else 'leave')
-
- if isCursorInPlot:
- # Signal mouse move event
- dataPos = self.pixelToData(inXPixel, inYPixel)
- assert dataPos is not None
-
- btn = self._pressedButtons[-1] if self._pressedButtons else None
- event = PlotEvents.prepareMouseSignal(
- 'mouseMoved', btn, dataPos[0], dataPos[1], xPixel, yPixel)
- self.notify(**event)
-
- # Either button was pressed in the plot or cursor is in the plot
- if isCursorInPlot or self._pressedButtons:
- self._eventHandler.handleEvent('move', inXPixel, inYPixel)
-
- def onMouseRelease(self, xPixel, yPixel, btn):
- """Handle mouse release event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- :param str btn: Mouse button in 'left', 'middle', 'right'
- """
- try:
- self._pressedButtons.remove(btn)
- except ValueError:
- pass
- else:
- xPixel, yPixel = self._isPositionInPlotArea(xPixel, yPixel)
- self._eventHandler.handleEvent('release', xPixel, yPixel, btn)
-
- def onMouseWheel(self, xPixel, yPixel, angleInDegrees):
- """Handle mouse wheel event.
-
- :param float xPixel: X mouse position in pixels
- :param float yPixel: Y mouse position in pixels
- :param float angleInDegrees: Angle corresponding to wheel motion.
- Positive for movement away from the user,
- negative for movement toward the user.
- """
- if self._isPositionInPlotArea(xPixel, yPixel) == (xPixel, yPixel):
- self._eventHandler.handleEvent(
- 'wheel', xPixel, yPixel, angleInDegrees)
-
- def onMouseLeaveWidget(self):
- """Handle mouse leave widget event."""
- if self._cursorInPlot:
- self._cursorInPlot = False
- self._eventHandler.handleEvent('leave')
-
- # Interaction modes #
-
- def getInteractiveMode(self):
- """Returns the current interactive mode as a dict.
-
- The returned dict contains at least the key 'mode'.
- Mode can be: 'draw', 'pan', 'select', 'zoom'.
- It can also contains extra keys (e.g., 'color') specific to a mode
- as provided to :meth:`setInteractiveMode`.
- """
- return self._eventHandler.getInteractiveMode()
-
- def setInteractiveMode(self, mode, color='black',
- shape='polygon', label=None,
- zoomOnWheel=True, source=None, width=None):
- """Switch the interactive mode.
-
- :param str mode: The name of the interactive mode.
- In 'draw', 'pan', 'select', '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
-
- self.notify(
- 'interactiveModeChanged', source=source)
-
- # Panning with arrow keys
-
- def isPanWithArrowKeys(self):
- """Returns whether or not panning the graph with arrow keys is enable.
-
- 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 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.
- 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)
-
- else:
- # Only call base class implementation when key is not handled.
- # See QWidget.keyPressEvent for details.
- super(PlotWidget, self).keyPressEvent(event)
-
- # Deprecated #
-
- def isDrawModeEnabled(self):
- """Deprecated, use :meth:`getInteractiveMode` instead.
-
- Return True if the current interactive state is drawing."""
- _logger.warning(
- 'isDrawModeEnabled deprecated, use getInteractiveMode instead')
- return self.getInteractiveMode()['mode'] == 'draw'
-
- def setDrawModeEnabled(self, flag=True, shape='polygon', label=None,
- color=None, **kwargs):
- """Deprecated, use :meth:`setInteractiveMode` instead.
-
- Set the drawing mode if flag is True and its parameters.
-
- If flag is False, only item selection is enabled.
-
- Warning: Zoom and drawing are not compatible and cannot be enabled
- simultaneously.
-
- :param bool flag: True to enable drawing and disable zoom and select.
- :param str shape: Type of item to be drawn in:
- hline, vline, rectangle, polygon (default)
- :param str label: Associated text for identifying draw signals
- :param color: The color to use to draw the selection area
- :type color: string ("#RRGGBB") or 4 column unsigned byte array or
- one of the predefined color names defined in colors.py
- """
- _logger.warning(
- 'setDrawModeEnabled deprecated, use setInteractiveMode instead')
-
- if kwargs:
- _logger.warning('setDrawModeEnabled ignores additional parameters')
-
- if color is None:
- color = 'black'
-
- if flag:
- self.setInteractiveMode('draw', shape=shape,
- label=label, color=color)
- elif self.getInteractiveMode()['mode'] == 'draw':
- self.setInteractiveMode('select')
-
- def getDrawMode(self):
- """Deprecated, use :meth:`getInteractiveMode` instead.
-
- Return the draw mode parameters as a dict of None.
-
- It returns None if the interactive mode is not a drawing mode,
- otherwise, it returns a dict containing the drawing mode parameters
- as provided to :meth:`setDrawModeEnabled`.
- """
- _logger.warning(
- 'getDrawMode deprecated, use getInteractiveMode instead')
- mode = self.getInteractiveMode()
- return mode if mode['mode'] == 'draw' else None
-
- def isZoomModeEnabled(self):
- """Deprecated, use :meth:`getInteractiveMode` instead.
-
- Return True if the current interactive state is zooming."""
- _logger.warning(
- 'isZoomModeEnabled deprecated, use getInteractiveMode instead')
- return self.getInteractiveMode()['mode'] == 'zoom'
-
- def setZoomModeEnabled(self, flag=True, color=None):
- """Deprecated, use :meth:`setInteractiveMode` instead.
-
- Set the zoom mode if flag is True, else item selection is enabled.
-
- Warning: Zoom and drawing are not compatible and cannot be enabled
- simultaneously
-
- :param bool flag: If True, enable zoom and select mode.
- :param color: The color to use to draw the selection area.
- (Default: 'black')
- :param color: The color to use to draw the selection area
- :type color: string ("#RRGGBB") or 4 column unsigned byte array or
- one of the predefined color names defined in colors.py
- """
- _logger.warning(
- 'setZoomModeEnabled deprecated, use setInteractiveMode instead')
- if color is None:
- color = 'black'
-
- if flag:
- self.setInteractiveMode('zoom', color=color)
- elif self.getInteractiveMode()['mode'] == 'zoom':
- self.setInteractiveMode('select')
-
- def insertMarker(self, *args, **kwargs):
- """Deprecated, use :meth:`addMarker` instead."""
- _logger.warning(
- 'insertMarker deprecated, use addMarker instead.')
- return self.addMarker(*args, **kwargs)
-
- def insertXMarker(self, *args, **kwargs):
- """Deprecated, use :meth:`addXMarker` instead."""
- _logger.warning(
- 'insertXMarker deprecated, use addXMarker instead.')
- return self.addXMarker(*args, **kwargs)
-
- def insertYMarker(self, *args, **kwargs):
- """Deprecated, use :meth:`addYMarker` instead."""
- _logger.warning(
- 'insertYMarker deprecated, use addYMarker instead.')
- return self.addYMarker(*args, **kwargs)
-
- def isActiveCurveHandlingEnabled(self):
- """Deprecated, use :meth:`isActiveCurveHandling` instead."""
- _logger.warning(
- 'isActiveCurveHandlingEnabled deprecated, '
- 'use isActiveCurveHandling instead.')
- return self.isActiveCurveHandling()
-
- def enableActiveCurveHandling(self, *args, **kwargs):
- """Deprecated, use :meth:`setActiveCurveHandling` instead."""
- _logger.warning(
- 'enableActiveCurveHandling deprecated, '
- 'use setActiveCurveHandling instead.')
- return self.setActiveCurveHandling(*args, **kwargs)
-
- def invertYAxis(self, *args, **kwargs):
- """Deprecated, use :meth:`Axis.setInverted` instead."""
- _logger.warning('invertYAxis deprecated, '
- 'use getYAxis().setInverted instead.')
- return self.getYAxis().setInverted(*args, **kwargs)
-
- def showGrid(self, flag=True):
- """Deprecated, use :meth:`setGraphGrid` instead."""
- _logger.warning("showGrid deprecated, use setGraphGrid instead")
- if flag in (0, False):
- flag = None
- elif flag in (1, True):
- flag = 'major'
- else:
- flag = 'both'
- return self.setGraphGrid(flag)
-
- def keepDataAspectRatio(self, *args, **kwargs):
- """Deprecated, use :meth:`setKeepDataAspectRatio`."""
- _logger.warning('keepDataAspectRatio deprecated,'
- 'use setKeepDataAspectRatio instead')
- return self.setKeepDataAspectRatio(*args, **kwargs)
diff --git a/silx/gui/plot/PlotWindow.py b/silx/gui/plot/PlotWindow.py
deleted file mode 100644
index 23ea399..0000000
--- a/silx/gui/plot/PlotWindow.py
+++ /dev/null
@@ -1,948 +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.
-#
-# ###########################################################################*/
-"""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/10/2018"
-
-import collections
-import logging
-import weakref
-
-import silx
-from silx.utils.weakref import WeakMethodProxy
-from silx.utils.deprecation import deprecated
-
-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._statsWidget = 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))
- self.resetZoomAction.setVisible(resetzoom)
- self.addAction(self.resetZoomAction)
-
- self.zoomInAction = actions.control.ZoomInAction(self)
- self.addAction(self.zoomInAction)
-
- self.zoomOutAction = actions.control.ZoomOutAction(self)
- self.addAction(self.zoomOutAction)
-
- self.xAxisAutoScaleAction = self.group.addAction(
- actions.control.XAxisAutoScaleAction(self))
- self.xAxisAutoScaleAction.setVisible(autoScale)
- self.addAction(self.xAxisAutoScaleAction)
-
- self.yAxisAutoScaleAction = self.group.addAction(
- actions.control.YAxisAutoScaleAction(self))
- self.yAxisAutoScaleAction.setVisible(autoScale)
- self.addAction(self.yAxisAutoScaleAction)
-
- self.xAxisLogarithmicAction = self.group.addAction(
- actions.control.XAxisLogarithmicAction(self))
- self.xAxisLogarithmicAction.setVisible(logScale)
- self.addAction(self.xAxisLogarithmicAction)
-
- self.yAxisLogarithmicAction = self.group.addAction(
- actions.control.YAxisLogarithmicAction(self))
- self.yAxisLogarithmicAction.setVisible(logScale)
- self.addAction(self.yAxisLogarithmicAction)
-
- self.gridAction = self.group.addAction(
- actions.control.GridAction(self, gridMode='both'))
- self.gridAction.setVisible(grid)
- self.addAction(self.gridAction)
-
- self.curveStyleAction = self.group.addAction(
- actions.control.CurveStyleAction(self))
- self.curveStyleAction.setVisible(curveStyle)
- self.addAction(self.curveStyleAction)
-
- self.colormapAction = self.group.addAction(
- actions.control.ColormapAction(self))
- self.colormapAction.setVisible(colormap)
- self.addAction(self.colormapAction)
-
- self.colorbarAction = self.group.addAction(
- actions_control.ColorBarAction(self, 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))
- self._intensityHistoAction.setVisible(False)
-
- self._medianFilter2DAction = self.group.addAction(
- actions_medfilt.MedianFilter2DAction(self))
- self._medianFilter2DAction.setVisible(False)
-
- self._medianFilter1DAction = self.group.addAction(
- actions_medfilt.MedianFilter1DAction(self))
- self._medianFilter1DAction.setVisible(False)
-
- self.fitAction = self.group.addAction(actions_fit.FitAction(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)
- palette = self._colorbar.palette()
- palette.setColor(qt.QPalette.Background, qt.Qt.white)
- palette.setColor(qt.QPalette.Window, qt.Qt.white)
- self._colorbar.setPalette(palette)
-
- gridLayout = qt.QGridLayout()
- gridLayout.setSpacing(0)
- gridLayout.setContentsMargins(0, 0, 0, 0)
- gridLayout.addWidget(self.getWidgetHandle(), 0, 0)
- gridLayout.addWidget(self._colorbar, 0, 1)
- gridLayout.setRowStretch(0, 1)
- gridLayout.setColumnStretch(0, 1)
- centralWidget = qt.QWidget(self)
- centralWidget.setLayout(gridLayout)
- self.setCentralWidget(centralWidget)
-
- self._positionWidget = None
-
- if control or position:
- hbox = qt.QHBoxLayout()
- hbox.setContentsMargins(0, 0, 0, 0)
-
- if control:
- 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)
-
- hbox.addWidget(self.controlButton)
-
- if position: # Add PositionInfo widget to the bottom of the plot
- if isinstance(position, collections.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)
-
- hbox.addWidget(self._positionWidget)
-
- hbox.addStretch(1)
- bottomBar = qt.QWidget(centralWidget)
- bottomBar.setLayout(hbox)
-
- gridLayout.addWidget(bottomBar, 1, 0, 1, -1)
-
- # 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=None)
- 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 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:
- it 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 getColorBarWidget(self):
- """Returns the embedded :class:`ColorBarWidget` widget.
-
- :rtype: ColorBarWidget
- """
- return self._colorbar
-
- # getters for dock widgets
- @property
- @deprecated(replacement="getLegendsDockWidget()", since_version="0.4.0")
- def legendsDockWidget(self):
- return self.getLegendsDockWidget()
-
- def getLegendsDockWidget(self):
- """DockWidget with Legend panel"""
- if self._legendsDockWidget is None:
- self._legendsDockWidget = LegendsDockWidget(plot=self)
- self._legendsDockWidget.hide()
- self.addTabbedDockWidget(self._legendsDockWidget)
- return self._legendsDockWidget
-
- @property
- @deprecated(replacement="getCurvesRoiWidget()", since_version="0.4.0")
- def curvesROIDockWidget(self):
- return self.getCurvesRoiDockWidget()
-
- 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.addTabbedDockWidget(self._curvesROIDockWidget)
- 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
-
- @property
- @deprecated(replacement="getMaskToolsDockWidget()", since_version="0.4.0")
- def maskToolsDockWidget(self):
- return self.getMaskToolsDockWidget()
-
- 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.addTabbedDockWidget(self._maskToolsDockWidget)
- return self._maskToolsDockWidget
-
- def getStatsWidget(self):
- """Returns a BasicStatsWidget connected to this plot
-
- :rtype: BasicStatsWidget
- """
- if self._statsWidget is None:
- dockWidget = qt.QDockWidget(parent=self)
- dockWidget.setWindowTitle("Curves stats")
- dockWidget.layout().setContentsMargins(0, 0, 0, 0)
- self._statsWidget = BasicStatsWidget(parent=self, plot=self)
- self._statsWidget.sigVisibilityChanged.connect(self.getStatsAction().setChecked)
- dockWidget.setWidget(self._statsWidget)
- dockWidget.hide()
- self.addTabbedDockWidget(dockWidget)
- return self._statsWidget
-
- # 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()
-
- @property
- @deprecated(replacement="getConsoleAction()", since_version="0.4.0")
- def consoleAction(self):
- return self.getConsoleAction()
-
- 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
-
- @property
- @deprecated(replacement="getCrosshairAction()", since_version="0.4.0")
- def crosshairAction(self):
- return self.getCrosshairAction()
-
- 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
-
- @property
- @deprecated(replacement="getMaskAction()", since_version="0.4.0")
- def maskAction(self):
- return self.getMaskAction()
-
- def getMaskAction(self):
- """QAction toggling image mask dock widget
-
- :rtype: QAction
- """
- return self.getMaskToolsDockWidget().toggleViewAction()
-
- @property
- @deprecated(replacement="getPanWithArrowKeysAction()",
- since_version="0.4.0")
- def panWithArrowKeysAction(self):
- return self.getPanWithArrowKeysAction()
-
- 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
-
- @property
- @deprecated(replacement="getRoiAction()", since_version="0.4.0")
- def roiAction(self):
- return self.getRoiAction()
-
- 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')
-
-
-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))]
-
- 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 '-'
- """
- value = '-'
- valueZ = -float('inf')
- mask = 0
- maskZ = -float('inf')
-
- for image in self.getAllImages():
- data = image.getData(copy=False)
- isMask = isinstance(image, items.MaskImageData)
- if isMask:
- zIndex = maskZ
- else:
- zIndex = valueZ
- if image.getZValue() >= zIndex:
- # This image is over the previous one
- ox, oy = image.getOrigin()
- sx, sy = image.getScale()
- row, col = (y - oy) / sy, (x - ox) / sx
- if row >= 0 and col >= 0:
- # Test positive before cast otherwise issue with int(-0.5) = 0
- row, col = int(row), int(col)
- if (row < data.shape[0] and col < data.shape[1]):
- v, z = data[row, col], image.getZValue()
- if not isMask:
- value = v
- valueZ = z
- else:
- mask = v
- maskZ = z
- if maskZ > valueZ and mask > 0:
- return value, "Masked"
- return value
-
- def getProfileToolbar(self):
- """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 b48505d..0000000
--- a/silx/gui/plot/PrintPreviewToolButton.py
+++ /dev/null
@@ -1,351 +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
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "18/07/2017"
-
-_logger = logging.getLogger(__name__)
-# _logger.setLevel(logging.DEBUG)
-
-
-class PrintPreviewToolButton(qt.QToolButton):
- """QToolButton to open a :class:`PrintPreviewDialog` (if not already open)
- and add the current plot to its page to be printed.
-
- :param parent: See :class:`QAction`
- :param plot: :class:`.PlotWidget` instance on which to operate
- """
- def __init__(self, parent=None, plot=None):
- super(PrintPreviewToolButton, self).__init__(parent)
-
- if not isinstance(plot, PlotWidget):
- raise TypeError("plot parameter must be a PlotWidget")
- self.plot = plot
-
- self.setIcon(icons.getQIcon('document-print'))
-
- printGeomAction = qt.QAction("Print geometry", self)
- printGeomAction.setToolTip("Define a print geometry prior to sending "
- "the plot to the print preview dialog")
- printGeomAction.setIcon(icons.getQIcon('shape-rectangle')) # fixme: icon not displayed in menu
- printGeomAction.triggered.connect(self._setPrintConfiguration)
-
- printPreviewAction = qt.QAction("Print preview", self)
- printPreviewAction.setToolTip("Send plot to the print preview dialog")
- printPreviewAction.setIcon(icons.getQIcon('document-print')) # fixme: icon not displayed
- printPreviewAction.triggered.connect(self._plotToPrintPreview)
-
- menu = qt.QMenu(self)
- menu.addAction(printGeomAction)
- menu.addAction(printPreviewAction)
- self.setMenu(menu)
- self.setPopupMode(qt.QToolButton.InstantPopup)
-
- self._printPreviewDialog = None
- self._printConfigurationDialog = None
-
- self._printGeometry = {"xOffset": 0.1,
- "yOffset": 0.1,
- "width": 0.9,
- "height": 0.9,
- "units": "page",
- "keepAspectRatio": True}
-
- @property
- def printPreviewDialog(self):
- """Lazy loaded :class:`PrintPreviewDialog`"""
- # if changes are made here, don't forget making them in
- # SingletonPrintPreviewToolButton.printPreviewDialog as well
- if self._printPreviewDialog is None:
- self._printPreviewDialog = PrintPreviewDialog(self.parent())
- return self._printPreviewDialog
-
- def _plotToPrintPreview(self):
- """Grab the plot widget and send it to the print preview dialog.
- Make sure the print preview dialog is shown and raised."""
- if not self.printPreviewDialog.ensurePrinterIsSet():
- return
-
- if qt.HAS_SVG:
- svgRenderer, viewBox = self._getSvgRendererAndViewbox()
- self.printPreviewDialog.addSvgItem(svgRenderer,
- viewBox=viewBox)
- else:
- _logger.warning("Missing QtSvg library, using a raster image")
- if qt.BINDING in ["PyQt4", "PySide"]:
- pixmap = qt.QPixmap.grabWidget(self.plot.centralWidget())
- else:
- # PyQt5 and hopefully PyQt6+
- pixmap = self.plot.centralWidget().grab()
- self.printPreviewDialog.addPixmap(pixmap)
- self.printPreviewDialog.show()
- self.printPreviewDialog.raise_()
-
- def _getSvgRendererAndViewbox(self):
- """Return a SVG renderer displaying the plot and its viewbox
- (interactively specified by the user the first time this is called).
-
- The size of the renderer is adjusted to the printer configuration
- and to the geometry configuration (width, height, ratio) specified
- by the user."""
- imgData = StringIO()
- assert self.plot.saveGraph(imgData, fileFormat="svg"), \
- "Unable to save graph"
- imgData.flush()
- imgData.seek(0)
- svgData = imgData.read()
-
- svgRenderer = qt.QSvgRenderer()
-
- viewbox = self._getViewBox()
-
- svgRenderer.setViewBox(viewbox)
-
- xml_stream = qt.QXmlStreamReader(svgData.encode(errors="replace"))
-
- # This is for PyMca compatibility, to share a print preview with PyMca plots
- svgRenderer._viewBox = viewbox
- svgRenderer._svgRawData = svgData.encode(errors="replace")
- svgRenderer._svgRendererData = xml_stream
-
- if not svgRenderer.load(xml_stream):
- raise RuntimeError("Cannot interpret svg data")
-
- return svgRenderer, viewbox
-
- def _getViewBox(self):
- """
- """
- printer = self.printPreviewDialog.printer
- dpix = printer.logicalDpiX()
- dpiy = printer.logicalDpiY()
- availableWidth = printer.width()
- availableHeight = printer.height()
-
- config = self._printGeometry
- width = config['width']
- height = config['height']
- xOffset = config['xOffset']
- yOffset = config['yOffset']
- units = config['units']
- keepAspectRatio = config['keepAspectRatio']
- aspectRatio = self._getPlotAspectRatio()
-
- # convert the offsets to dots
- if units.lower() in ['inch', 'inches']:
- xOffset = xOffset * dpix
- yOffset = yOffset * dpiy
- if width is not None:
- width = width * dpix
- if height is not None:
- height = height * dpiy
- elif units.lower() in ['cm', 'centimeters']:
- xOffset = (xOffset / 2.54) * dpix
- yOffset = (yOffset / 2.54) * dpiy
- if width is not None:
- width = (width / 2.54) * dpix
- if height is not None:
- height = (height / 2.54) * dpiy
- else:
- # page units
- xOffset = availableWidth * xOffset
- yOffset = availableHeight * yOffset
- if width is not None:
- width = availableWidth * width
- if height is not None:
- height = availableHeight * height
-
- availableWidth -= xOffset
- availableHeight -= yOffset
-
- if width is not None:
- if (availableWidth + 0.1) < width:
- txt = "Available width %f is less than requested width %f" % \
- (availableWidth, width)
- raise ValueError(txt)
- if height is not None:
- if (availableHeight + 0.1) < height:
- txt = "Available height %f is less than requested height %f" % \
- (availableHeight, height)
- raise ValueError(txt)
-
- if keepAspectRatio:
- bodyWidth = width or availableWidth
- bodyHeight = bodyWidth * aspectRatio
-
- if bodyHeight > availableHeight:
- bodyHeight = availableHeight
- bodyWidth = bodyHeight / aspectRatio
-
- else:
- bodyWidth = width or availableWidth
- bodyHeight = height or availableHeight
-
- return qt.QRectF(xOffset,
- yOffset,
- bodyWidth,
- bodyHeight)
-
- def _setPrintConfiguration(self):
- """Open a dialog to prompt the user to adjust print
- geometry parameters."""
- self.printPreviewDialog.ensurePrinterIsSet()
- if self._printConfigurationDialog is None:
- self._printConfigurationDialog = PrintGeometryDialog(self.parent())
-
- self._printConfigurationDialog.setPrintGeometry(self._printGeometry)
- if self._printConfigurationDialog.exec_():
- self._printGeometry = self._printConfigurationDialog.getPrintGeometry()
-
- def _getPlotAspectRatio(self):
- widget = self.plot.centralWidget()
- graphWidth = float(widget.width())
- graphHeight = float(widget.height())
- return graphHeight / graphWidth
-
-
-class SingletonPrintPreviewToolButton(PrintPreviewToolButton):
- """This class is similar to its parent class :class:`PrintPreviewToolButton`
- but it uses a singleton print preview widget.
-
- This allows for several plots to send their content to the
- same print page, and for users to arrange them."""
- def __init__(self, parent=None, plot=None):
- PrintPreviewToolButton.__init__(self, parent, plot)
-
- @property
- def printPreviewDialog(self):
- if self._printPreviewDialog is None:
- self._printPreviewDialog = SingletonPrintPreviewDialog(self.parent())
- return self._printPreviewDialog
-
-
-if __name__ == '__main__':
- import numpy
- app = qt.QApplication([])
-
- pw = PlotWidget()
- toolbar = qt.QToolBar(pw)
- toolbutton = PrintPreviewToolButton(parent=toolbar,
- plot=pw)
- pw.addToolBar(toolbar)
- toolbar.addWidget(toolbutton)
- pw.show()
-
- x = numpy.arange(1000)
- y = x / numpy.sin(x)
- pw.addCurve(x, y)
-
- app.exec_()
diff --git a/silx/gui/plot/Profile.py b/silx/gui/plot/Profile.py
deleted file mode 100644
index 182cf60..0000000
--- a/silx/gui/plot/Profile.py
+++ /dev/null
@@ -1,810 +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.
-#
-# ###########################################################################*/
-"""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__ = "24/07/2018"
-
-
-import weakref
-
-import numpy
-
-from silx.image.bilinear import BilinearImage
-
-from .. import icons
-from .. import qt
-from . import items
-from ..colors import cursorColorForColormap
-from . import actions
-from .PlotToolButtons import ProfileToolButton, ProfileOptionToolButton
-from .ProfileMainWindow import ProfileMainWindow
-
-from silx.utils.deprecation import deprecated
-
-
-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'
- :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')
-
- # 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 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'
- :return: `profile, area, profileName, xLabel`, where:
- - 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 is a string describing the meaning of the X axis on the
- profile plot ("rows", "columns", "distance")
-
- :rtype: tuple(ndarray, (ndarray, ndarray), str, 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)
-
- yMin, yMax = min(area[1]), max(area[1]) - 1
- if roiWidth <= 1:
- profileName = 'Y = %g' % yMin
- else:
- profileName = 'Y = [%g, %g]' % (yMin, yMax)
- xLabel = 'Columns'
-
- elif lineProjectionMode == 'Y': # Vertical profile on the whole image
- profile, area = _alignedFullProfile(currentData3D,
- origin, scale,
- roiStart[0], roiWidth,
- axis=1,
- method=method)
-
- xMin, xMax = min(area[0]), max(area[0]) - 1
- if roiWidth <= 1:
- profileName = 'X = %g' % xMin
- else:
- profileName = 'X = [%g, %g]' % (xMin, xMax)
- xLabel = 'Rows'
-
- 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
- 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))
- 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
-
- 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
- startPt = startPt[0] - 0.5 * dRow, startPt[1] - 0.5 * dCol
- endPt = 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((startPt[1] - 0.5 * roiWidth * dCol,
- startPt[1] + 0.5 * roiWidth * dCol,
- endPt[1] + 0.5 * roiWidth * dCol,
- endPt[1] - 0.5 * roiWidth * dCol),
- dtype=numpy.float32) * scale[0] + origin[0],
- numpy.array((startPt[0] - 0.5 * roiWidth * dRow,
- startPt[0] + 0.5 * roiWidth * dRow,
- endPt[0] + 0.5 * roiWidth * dRow,
- endPt[0] - 0.5 * roiWidth * dRow),
- dtype=numpy.float32) * scale[1] + origin[1])
-
- y0, x0 = startPt
- y1, x1 = endPt
- if x1 == x0 or y1 == y0:
- profileName = 'From (%g, %g) to (%g, %g)' % (x0, y0, x1, y1)
- else:
- m = (y1 - y0) / (x1 - x0)
- b = y0 - m * x0
- profileName = 'y = %g * x %+g ; width=%d' % (m, b, roiWidth)
- xLabel = 'Distance'
-
- return profile, area, profileName, xLabel
-
-
-# ProfileToolBar ##############################################################
-
-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`.
- """
- # TODO Make it a QActionGroup instead of a QToolBar
-
- _POLYGON_LEGEND = '__ProfileToolBar_ROI_Polygon'
-
- DEFAULT_PROF_METHOD = 'mean'
-
- def __init__(self, parent=None, plot=None, profileWindow=None,
- title='Profile Selection'):
- super(ProfileToolBar, self).__init__(title, parent)
- assert plot is not None
- self._plotRef = weakref.ref(plot)
-
- self._overlayColor = None
- self._defaultOverlayColor = 'red' # update when active image change
- self._method = self.DEFAULT_PROF_METHOD
-
- self._roiInfo = None # Store start and end points and type of ROI
-
- self._profileWindow = profileWindow
- """User provided plot widget in which the profile curve is plotted.
- None if no custom profile plot was provided."""
-
- self._profileMainWindow = None
- """Main window providing 2 profile plot widgets for 1D or 2D profiles.
- The window provides two public methods
- - :meth:`setProfileDimensions`
- - :meth:`getPlot`: return handle on the actual plot widget
- currently being used
- None if the user specified a custom profile plot window.
- """
-
- if self._profileWindow is None:
- self._profileMainWindow = ProfileMainWindow(self)
-
- # Actions
- self._browseAction = actions.mode.ZoomModeAction(self.plot, parent=self)
- self._browseAction.setVisible(False)
-
- self.hLineAction = qt.QAction(
- icons.getQIcon('shape-horizontal'),
- 'Horizontal Profile Mode', None)
- self.hLineAction.setToolTip(
- 'Enables horizontal profile selection mode')
- self.hLineAction.setCheckable(True)
- self.hLineAction.toggled[bool].connect(self._hLineActionToggled)
-
- self.vLineAction = qt.QAction(
- icons.getQIcon('shape-vertical'),
- 'Vertical Profile Mode', None)
- self.vLineAction.setToolTip(
- 'Enables vertical profile selection mode')
- self.vLineAction.setCheckable(True)
- self.vLineAction.toggled[bool].connect(self._vLineActionToggled)
-
- self.lineAction = qt.QAction(
- icons.getQIcon('shape-diagonal'),
- 'Free Line Profile Mode', None)
- self.lineAction.setToolTip(
- 'Enables line profile selection mode')
- self.lineAction.setCheckable(True)
- self.lineAction.toggled[bool].connect(self._lineActionToggled)
-
- self.clearAction = qt.QAction(
- icons.getQIcon('profile-clear'),
- 'Clear Profile', None)
- self.clearAction.setToolTip(
- 'Clear the profile Region of interest')
- self.clearAction.setCheckable(False)
- self.clearAction.triggered.connect(self.clearProfile)
-
- # 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)
-
- # Add actions to ToolBar
- self.addAction(self._browseAction)
- self.addAction(self.hLineAction)
- self.addAction(self.vLineAction)
- self.addAction(self.lineAction)
- self.addAction(self.clearAction)
-
- # Add width spin box to toolbar
- self.addWidget(qt.QLabel('W:'))
- self.lineWidthSpinBox = qt.QSpinBox(self)
- self.lineWidthSpinBox.setRange(1, 1000)
- self.lineWidthSpinBox.setValue(1)
- self.lineWidthSpinBox.valueChanged[int].connect(
- self._lineWidthSpinBoxValueChangedSlot)
- self.addWidget(self.lineWidthSpinBox)
-
- self.methodsButton = ProfileOptionToolButton(parent=self, plot=self)
- self.addWidget(self.methodsButton)
- # TODO: add connection with the signal
- self.methodsButton.sigMethodChanged.connect(self.setProfileMethod)
-
- self.plot.sigInteractiveModeChanged.connect(
- self._interactiveModeChanged)
-
- # Enable toolbar only if there is an active image
- self.setEnabled(self.plot.getActiveImage(just_legend=True) is not None)
- self.plot.sigActiveImageChanged.connect(
- self._activeImageChanged)
-
- # listen to the profile window signals to clear profile polygon on close
- if self.getProfileMainWindow() is not None:
- self.getProfileMainWindow().sigClose.connect(self.clearProfile)
-
- @property
- def plot(self):
- """The :class:`.PlotWidget` associated to the toolbar."""
- return self._plotRef()
-
- @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 getProfilePlot(self):
- """Return plot widget in which the profile curve or the
- profile image is plotted.
- """
- if self.getProfileMainWindow() is not None:
- return self.getProfileMainWindow().getPlot()
-
- # in case the user provided a custom plot for profiles
- return self._profileWindow
-
- def getProfileMainWindow(self):
- """Return window containing the profile curve widget.
- This can return *None* if a custom profile plot window was
- specified in the constructor.
- """
- return self._profileMainWindow
-
- def _activeImageChanged(self, previous, legend):
- """Handle active image change: toggle enabled toolbar, update curve"""
- if legend is None:
- self.setEnabled(False)
- else:
- activeImage = self.plot.getActiveImage()
-
- # Disable for empty image
- self.setEnabled(activeImage.getData(copy=False).size > 0)
-
- # Update default profile color
- if isinstance(activeImage, items.ColormapMixIn):
- self._defaultOverlayColor = cursorColorForColormap(
- activeImage.getColormap()['name'])
- else:
- self._defaultOverlayColor = 'black'
-
- self.updateProfile()
-
- def _lineWidthSpinBoxValueChangedSlot(self, value):
- """Listen to ROI width widget to refresh ROI and profile"""
- self.updateProfile()
-
- def _interactiveModeChanged(self, source):
- """Handle plot interactive mode changed:
-
- If changed from elsewhere, disable drawing tool
- """
- if source is not self:
- self.clearProfile()
-
- # Uncheck all drawing profile modes
- self.hLineAction.setChecked(False)
- self.vLineAction.setChecked(False)
- self.lineAction.setChecked(False)
-
- if self.getProfileMainWindow() is not None:
- self.getProfileMainWindow().hide()
-
- def _hLineActionToggled(self, checked):
- """Handle horizontal line profile action toggle"""
- if checked:
- self.plot.setInteractiveMode('draw', shape='hline',
- color=None, source=self)
- self.plot.sigPlotSignal.connect(self._plotWindowSlot)
- else:
- self.plot.sigPlotSignal.disconnect(self._plotWindowSlot)
-
- def _vLineActionToggled(self, checked):
- """Handle vertical line profile action toggle"""
- if checked:
- self.plot.setInteractiveMode('draw', shape='vline',
- color=None, source=self)
- self.plot.sigPlotSignal.connect(self._plotWindowSlot)
- else:
- self.plot.sigPlotSignal.disconnect(self._plotWindowSlot)
-
- def _lineActionToggled(self, checked):
- """Handle line profile action toggle"""
- if checked:
- self.plot.setInteractiveMode('draw', shape='line',
- color=None, source=self)
- self.plot.sigPlotSignal.connect(self._plotWindowSlot)
- else:
- self.plot.sigPlotSignal.disconnect(self._plotWindowSlot)
-
- def _plotWindowSlot(self, event):
- """Listen to Plot to handle drawing events to refresh ROI and profile.
- """
- if event['event'] not in ('drawingProgress', 'drawingFinished'):
- return
-
- checkedAction = self.actionGroup.checkedAction()
- if checkedAction == self.hLineAction:
- lineProjectionMode = 'X'
- elif checkedAction == self.vLineAction:
- lineProjectionMode = 'Y'
- elif checkedAction == self.lineAction:
- lineProjectionMode = 'D'
- else:
- return
-
- roiStart, roiEnd = event['points'][0], event['points'][1]
-
- self._roiInfo = roiStart, roiEnd, lineProjectionMode
- self.updateProfile()
-
- @property
- def overlayColor(self):
- """The 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.
- """
- return self._overlayColor or self._defaultOverlayColor
-
- @overlayColor.setter
- def overlayColor(self, color):
- self._overlayColor = color
- self.updateProfile()
-
- def clearProfile(self):
- """Remove profile curve and profile area."""
- self._roiInfo = None
- self.updateProfile()
-
- def updateProfile(self):
- """Update the displayed profile and profile ROI.
-
- This uses the current active image of the plot and the current ROI.
- """
- image = self.plot.getActiveImage()
- if image is None:
- return
-
- # Clean previous profile area, and previous curve
- self.plot.remove(self._POLYGON_LEGEND, kind='item')
- self.getProfilePlot().clear()
- self.getProfilePlot().setGraphTitle('')
- self.getProfilePlot().getXAxis().setLabel('X')
- self.getProfilePlot().getYAxis().setLabel('Y')
-
- self._createProfile(currentData=image.getData(copy=False),
- origin=image.getOrigin(),
- scale=image.getScale(),
- colormap=None, # Not used for 2D data
- z=image.getZValue(),
- method=self.getProfileMethod())
-
- def _createProfile(self, currentData, origin, scale, colormap, z, method):
- """Create the profile line for the the given image.
-
- :param numpy.ndarray currentData: the image or the 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 dict colormap: The colormap to use
- :param int z: The z layer of the image
- """
- if self._roiInfo is None:
- return
-
- profile, area, profileName, xLabel = createProfile(
- roiInfo=self._roiInfo,
- currentData=currentData,
- origin=origin,
- scale=scale,
- lineWidth=self.lineWidthSpinBox.value(),
- method=method)
-
- self.getProfilePlot().setGraphTitle(profileName)
-
- dataIs3D = len(currentData.shape) > 2
- if dataIs3D:
- self.getProfilePlot().addImage(profile,
- legend=profileName,
- xlabel=xLabel,
- ylabel="Frame index (depth)",
- colormap=colormap)
- else:
- coords = numpy.arange(len(profile[0]), dtype=numpy.float32)
- # Scale horizontal and vertical profile coordinates
- if self._roiInfo[2] == 'X':
- coords = coords * scale[0] + origin[0]
- elif self._roiInfo[2] == 'Y':
- coords = coords * scale[1] + origin[1]
-
- self.getProfilePlot().addCurve(coords,
- profile[0],
- legend=profileName,
- xlabel=xLabel,
- color=self.overlayColor)
-
- self.plot.addItem(area[0], area[1],
- legend=self._POLYGON_LEGEND,
- color=self.overlayColor,
- shape='polygon', fill=True,
- replace=False, z=z + 1)
-
- self._showProfileMainWindow()
-
- def _showProfileMainWindow(self):
- """If profile window was created by this toolbar,
- try to avoid overlapping with the toolbar's parent window.
- """
- profileMainWindow = self.getProfileMainWindow()
- if profileMainWindow is not None:
- winGeom = self.window().frameGeometry()
- qapp = qt.QApplication.instance()
- screenGeom = qapp.desktop().availableGeometry(self)
- spaceOnLeftSide = winGeom.left()
- spaceOnRightSide = screenGeom.width() - winGeom.right()
-
- profileWindowWidth = profileMainWindow.frameGeometry().width()
- if (profileWindowWidth < spaceOnRightSide):
- # Place profile on the right
- profileMainWindow.move(winGeom.right(), winGeom.top())
- elif(profileWindowWidth < spaceOnLeftSide):
- # Place profile on the left
- profileMainWindow.move(
- max(0, winGeom.left() - profileWindowWidth), winGeom.top())
-
- profileMainWindow.show()
- profileMainWindow.raise_()
- else:
- self.getProfilePlot().show()
- self.getProfilePlot().raise_()
-
- def hideProfileWindow(self):
- """Hide profile window.
- """
- # this method is currently only used by StackView when the perspective
- # is changed
- if self.getProfileMainWindow() is not None:
- self.getProfileMainWindow().hide()
-
- def setProfileMethod(self, method):
- assert method in ('sum', 'mean')
- self._method = method
- self.updateProfile()
-
- def getProfileMethod(self):
- return self._method
-
-
-class Profile3DToolBar(ProfileToolBar):
- def __init__(self, parent=None, stackview=None,
- title='Profile Selection'):
- """QToolBar providing profile tools for an image or a stack of images.
-
- :param parent: the parent QWidget
- :param 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.getPlot(),
- title=title)
- self.stackView = stackview
- """:class:`StackView` instance"""
-
- self.profile3dAction = ProfileToolButton(
- parent=self, plot=self.plot)
- self.profile3dAction.computeProfileIn2D()
- self.profile3dAction.setVisible(True)
- self.addWidget(self.profile3dAction)
- self.profile3dAction.sigDimensionChanged.connect(self._setProfileType)
-
- # create the 3D toolbar
- self._profileType = None
- self._setProfileType(2)
- self._method3D = 'sum'
-
- def _setProfileType(self, dimensions):
- """Set the profile type: "1D" for a curve (profile on a single image)
- or "2D" for an image (profile on a stack of images).
-
- :param int dimensions: 1 for a "1D" profile or 2 for a "2D" profile
- """
- # fixme this assumes that we created _profileMainWindow
- self._profileType = "1D" if dimensions == 1 else "2D"
- self.getProfileMainWindow().setProfileType(self._profileType)
- self.updateProfile()
-
- def updateProfile(self):
- """Method overloaded from :class:`ProfileToolBar`,
- to pass the stack of images instead of just the active image.
-
- In 1D profile mode, use the regular parent method.
- """
- if self._profileType == "1D":
- super(Profile3DToolBar, self).updateProfile()
- elif self._profileType == "2D":
- stackData = self.stackView.getCurrentView(copy=False,
- returnNumpyArray=True)
- if stackData is None:
- return
- self.plot.remove(self._POLYGON_LEGEND, kind='item')
- self.getProfilePlot().clear()
- self.getProfilePlot().setGraphTitle('')
- self.getProfilePlot().getXAxis().setLabel('X')
- self.getProfilePlot().getYAxis().setLabel('Y')
- self._createProfile(currentData=stackData[0],
- origin=stackData[1]['origin'],
- scale=stackData[1]['scale'],
- colormap=stackData[1]['colormap'],
- z=stackData[1]['z'],
- method=self.getProfileMethod())
- else:
- raise ValueError(
- "Profile type must be 1D or 2D, not %s" % self._profileType)
-
- def setProfileMethod(self, method):
- assert method in ('sum', 'mean')
- self._method3D = method
- self.updateProfile()
-
- def getProfileMethod(self):
- return self._method3D
diff --git a/silx/gui/plot/ProfileMainWindow.py b/silx/gui/plot/ProfileMainWindow.py
deleted file mode 100644
index caa076c..0000000
--- a/silx/gui/plot/ProfileMainWindow.py
+++ /dev/null
@@ -1,115 +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 a QMainWindow class used to display profile plots.
-"""
-from silx.gui import qt
-
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "21/02/2017"
-
-
-class ProfileMainWindow(qt.QMainWindow):
- """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.
- """
- 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.
- """
-
- sigClose = qt.Signal()
- """Emitted by :meth:`closeEvent` (e.g. when the window is closed
- through the window manager's close icon)."""
-
- sigProfileMethodChanged = qt.Signal(str)
- """Emitted when the method to compute the profile changed (for now can be
- sum or mean)"""
-
- def __init__(self, parent=None):
- qt.QMainWindow.__init__(self, parent=parent)
-
- self.setWindowTitle('Profile window')
- # plots are created on demand, in self.setProfileDimensions()
- self._plot1D = None
- self._plot2D = None
- # by default, profile is assumed to be a 1D curve
- self._profileType = None
- self.setProfileType("1D")
- self.setProfileMethod('sum')
-
- def setProfileType(self, profileType):
- """Set which profile plot widget (1D or 2D) is to be used
-
- :param str profileType: Type of profile data,
- "1D" for a curve or "2D" for an image
- """
- # import here to avoid circular import
- from .PlotWindow import Plot1D, Plot2D # noqa
- self._profileType = profileType
- if self._profileType == "1D":
- if self._plot2D is not None:
- self._plot2D.setParent(None) # necessary to avoid widget destruction
- if self._plot1D is None:
- self._plot1D = Plot1D()
- self._plot1D.setGraphYLabel('Profile')
- self._plot1D.setGraphXLabel('')
- self.setCentralWidget(self._plot1D)
- elif self._profileType == "2D":
- if self._plot1D is not None:
- self._plot1D.setParent(None) # necessary to avoid widget destruction
- if self._plot2D is None:
- self._plot2D = Plot2D()
- self.setCentralWidget(self._plot2D)
- 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.
- """
- if self._profileType == "2D":
- return self._plot2D
- else:
- return self._plot1D
-
- def closeEvent(self, qCloseEvent):
- self.sigClose.emit()
- qCloseEvent.accept()
-
- def setProfileMethod(self, method):
- """
-
- :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/ScatterMaskToolsWidget.py b/silx/gui/plot/ScatterMaskToolsWidget.py
deleted file mode 100644
index de645be..0000000
--- a/silx/gui/plot/ScatterMaskToolsWidget.py
+++ /dev/null
@@ -1,565 +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.
-#
-# ###########################################################################*/
-"""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__ = "24/04/2018"
-
-
-import math
-import logging
-import os
-import numpy
-import sys
-
-from .. import qt
-from ...math.combo import min_max
-from ...image import shapes
-
-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=numpy.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 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)
- elif self.plot._getItem(kind="scatter",
- legend=self._maskName) is not None:
- self.plot.remove(self._maskName, kind='scatter')
-
- # 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):
- self.plot.sigActiveScatterChanged.disconnect(self._activeScatterChanged)
- 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.getLegend() == 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.getLegend() == 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()
-
- 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:
- msg = qt.QMessageBox(self)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setText("Cannot save.\n"
- "Input Output Error: %s" % (sys.exc_info()[1]))
- msg.exec_()
- return
-
- self.maskFileDir = os.path.dirname(filename)
- try:
- self.save(filename, extension[1:])
- except Exception as e:
- msg = qt.QMessageBox(self)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setText("Cannot save file %s\n%s" % (filename, e.args[0]))
- 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' and
- 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 == 'polygon' and
- 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
-
- 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 ae79cf9..0000000
--- a/silx/gui/plot/ScatterView.py
+++ /dev/null
@@ -1,355 +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.
-#
-# ###########################################################################*/
-"""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 .tools.profile import ScatterProfileToolBar
-from .ColorBar import ColorBarWidget
-from .ScatterMaskToolsWidget import ScatterMaskToolsWidget
-
-from ..widgets.BoxLayoutDockWidget import BoxLayoutDockWidget
-from .. import qt, icons
-
-
-_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
- plot.addScatter(x=(), y=(), value=(), legend=self._SCATTER_LEGEND)
-
- # 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', lambda x, y: x),
- ('Y', lambda x, y: y),
- ('Data', lambda x, y: self._getScatterValue(x, y)),
- ('Index', lambda x, y: self._getScatterIndex(x, y))))
-
- # 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")
-
- # Create toolbars
- self._interactiveModeToolBar = tools.InteractiveModeToolBar(
- parent=self, plot=plot)
-
- self._scatterToolBar = tools.ScatterToolBar(
- parent=self, plot=plot)
- self._scatterToolBar.addAction(self._maskAction)
-
- 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 _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
- for item, indices in reversed(plot._pick(*pixelPos)):
- if isinstance(item, items.Scatter):
- # Get last index
- # with matplotlib it should be the top-most point
- dataIndex = indices[-1]
- self.__pickingCache = (
- dataIndex,
- item.getValueData(copy=False)[dataIndex])
- break
-
- return self.__pickingCache
-
- def _getScatterValue(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[1]
-
- def _getScatterIndex(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]
-
- _PICK_OFFSET = 3 # Offset in pixel used for picking
-
- 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)
-
- def getData(self, *args, **kwargs):
- return self.getScatterItem().getData(*args, **kwargs)
-
- getData.__doc__ = items.Scatter.getData.__doc__
-
- 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)
- plot.addScatter(x=(), y=(), value=(), legend=self._SCATTER_LEGEND)
- scatter = plot._getItem(
- kind='scatter', legend=self._SCATTER_LEGEND)
- return scatter
-
- # Convenient proxies
-
- def getXAxis(self, *args, **kwargs):
- return self.getPlotWidget().getXAxis(*args, **kwargs)
-
- getXAxis.__doc__ = PlotWidget.getXAxis.__doc__
-
- def getYAxis(self, *args, **kwargs):
- return self.getPlotWidget().getYAxis(*args, **kwargs)
-
- getYAxis.__doc__ = PlotWidget.getYAxis.__doc__
-
- def setGraphTitle(self, *args, **kwargs):
- return self.getPlotWidget().setGraphTitle(*args, **kwargs)
-
- setGraphTitle.__doc__ = PlotWidget.setGraphTitle.__doc__
-
- def getGraphTitle(self, *args, **kwargs):
- return self.getPlotWidget().getGraphTitle(*args, **kwargs)
-
- getGraphTitle.__doc__ = PlotWidget.getGraphTitle.__doc__
-
- def resetZoom(self, *args, **kwargs):
- return self.getPlotWidget().resetZoom(*args, **kwargs)
-
- resetZoom.__doc__ = PlotWidget.resetZoom.__doc__
diff --git a/silx/gui/plot/StackView.py b/silx/gui/plot/StackView.py
deleted file mode 100644
index 72b6cd4..0000000
--- a/silx/gui/plot/StackView.py
+++ /dev/null
@@ -1,1240 +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.
-#
-# ###########################################################################*/
-"""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 ..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.utils.array_like import DatasetView, ListOfImages
-from silx.math import calibration
-from silx.utils.deprecation import deprecated_warning
-
-try:
- import h5py
-except ImportError:
- def is_dataset(obj):
- return False
- h5py = None
-else:
- from silx.io.utils import is_dataset
-
-_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.
- """
-
- 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.__imageLegend = '__StackView__image' + str(id(self))
- 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.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._plot.profile = Profile3DToolBar(parent=self._plot,
- stackview=self)
- self._plot.addToolBar(self._plot.profile)
- self._plot.getXAxis().setLabel('Columns')
- self._plot.getYAxis().setLabel('Rows')
- self._plot.sigPlotSignal.connect(self._plotCallback)
-
- 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._plot.profile.getProfilePlot().clear)
- self.__planeSelection.sigPlaneSelectionChanged.connect(
- self._plot.profile.clearProfile)
-
- 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._plot.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)
-
- 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._plot.addImage(self.__transposed_view[index, :, :],
- origin=self._getImageOrigin(),
- scale=self._getImageScale(),
- legend=self.__imageLegend,
- resetzoom=False)
- 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)
-
- # This call to setColormap redefines the meaning of autoscale
- # for 3D volume: take global min/max rather than frame min/max
- if self.__autoscaleCmap:
- self.setColormap(autoscale=True)
-
- # init plot
- self._plot.addImage(self.__transposed_view[0, :, :],
- legend=self.__imageLegend,
- colormap=self.getColormap(),
- origin=self._getImageOrigin(),
- scale=self._getImageScale(),
- replace=True,
- resetzoom=False)
- self._plot.setActiveImage(self.__imageLegend)
- 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)
- """
- image = self._plot.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._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._plot.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 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)
-
- # Patch: since we don't apply this colormap to a single 2D data but
- # a 2D stack we have to deal manually with vmin, vmax
- if autoscale is None:
- # set default
- autoscale = False
- elif autoscale and is_dataset(self._stack):
- # h5py dataset has no min()/max() methods
- raise RuntimeError(
- "Cannot auto-scale colormap for a h5py dataset")
- else:
- autoscale = autoscale
- self.__autoscaleCmap = autoscale
-
- if autoscale and (self._stack is not None):
- _vmin, _vmax = _colormap.getColormapRange(data=self._stack)
- _colormap.setVRange(vmin=_vmin, vmax=_vmax)
- else:
- if vmin is None and self._stack is not None:
- _colormap.setVMin(self._stack.min())
- else:
- _colormap.setVMin(vmin)
- if vmax is None and self._stack is not None:
- _colormap.setVMax(self._stack.max())
- else:
- _colormap.setVMax(vmax)
-
- cursorColor = cursorColorForColormap(_colormap.getName())
- self._plot.setInteractiveMode('zoom', color=cursorColor)
-
- self._plot.setDefaultColormap(_colormap)
-
- # Update active image colormap
- activeImage = self._plot.getActiveImage()
- if isinstance(activeImage, items.ColormapMixIn):
- activeImage.setColormap(self.getColormap())
-
- def getPlot(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 getProfileWindow1D(self):
- """Plot window used to display 1D profile curve.
-
- :return: :class:`Plot1D`
- """
- return self._plot.profile.getProfileWindow1D()
-
- def getProfileWindow2D(self):
- """Plot window used to display 2D profile image.
-
- :return: :class:`Plot2D`
- """
- return self._plot.profile.getProfileWindow2D()
-
- def setOptionVisible(self, isVisible):
- """
- Set the visibility of the browsing options.
-
- :param bool isVisible: True to have the options visible, else False
- """
- self._browser.setVisible(isVisible)
- self.__planeSelection.setVisible(isVisible)
-
- # proxies to PlotWidget or PlotWindow methods
- def getProfileToolbar(self):
- """Profile tools attached to this plot
-
- See :class:`silx.gui.plot.Profile.Profile3DToolBar`
- """
- return self._plot.profile
-
- def getGraphTitle(self):
- """Return the plot main title as a str.
- """
- return self._plot.getGraphTitle()
-
- def setGraphTitle(self, title=""):
- """Set the plot main title.
-
- :param str title: Main title of the plot (default: '')
- """
- return self._plot.setGraphTitle(title)
-
- def getGraphXLabel(self):
- """Return the current horizontal axis label as a str.
- """
- return self._plot.getXAxis().getLabel()
-
- def setGraphXLabel(self, label=None):
- """Set the plot horizontal axis label.
-
- :param str label: The horizontal axis label
- """
- if label is None:
- label = self.__dimensionsLabels[1 if self._perspective == 2 else 2]
- self._plot.getXAxis().setLabel(label)
-
- def getGraphYLabel(self, axis='left'):
- """Return the current vertical axis label as a str.
-
- :param str axis: The Y axis for which to get the label (left or right)
- """
- return self._plot.getYAxis().getLabel(axis)
-
- def setGraphYLabel(self, label=None, axis='left'):
- """Set the vertical axis label on the plot.
-
- :param str label: The Y axis label
- :param str axis: The Y axis for which to set the label (left or right)
- """
- if label is None:
- label = self.__dimensionsLabels[1 if self._perspective == 0 else 0]
- self._plot.getYAxis(axis=axis).setLabel(label)
-
- def resetZoom(self):
- """Reset the plot limits to the bounds of the data and redraw the plot.
-
- This method is a simple proxy to the legacy :class:`PlotWidget` method
- of the same name. Using the object oriented approach is now
- preferred::
-
- stackview.getPlot().resetZoom()
- """
- self._plot.resetZoom()
-
- def setYAxisInverted(self, flag=True):
- """Set the Y axis orientation.
-
- This method is a simple proxy to the legacy :class:`PlotWidget` method
- of the same name. Using the object oriented approach is now
- preferred::
-
- stackview.getPlot().setYAxisInverted(flag)
-
- :param bool flag: True for Y axis going from top to bottom,
- False for Y axis going from bottom to top
- """
- self._plot.setYAxisInverted(flag)
-
- def isYAxisInverted(self):
- """Return True if Y axis goes from top to bottom, False otherwise.
-
- This method is a simple proxy to the legacy :class:`PlotWidget` method
- of the same name. Using the object oriented approach is now
- preferred::
-
- stackview.getPlot().isYAxisInverted()"""
- return self._plot.isYAxisInverted()
-
- def getSupportedColormaps(self):
- """Get the supported colormap names as a tuple of str.
-
- The list should at least contain and start by:
- ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue')
-
- This method is a simple proxy to the legacy :class:`PlotWidget` method
- of the same name. Using the object oriented approach is now
- preferred::
-
- stackview.getPlot().getSupportedColormaps()
- """
- return self._plot.getSupportedColormaps()
-
- def isKeepDataAspectRatio(self):
- """Returns whether the plot is keeping data aspect ratio or not.
-
- This method is a simple proxy to the legacy :class:`PlotWidget` method
- of the same name. Using the object oriented approach is now
- preferred::
-
- stackview.getPlot().isKeepDataAspectRatio()"""
- return self._plot.isKeepDataAspectRatio()
-
- def setKeepDataAspectRatio(self, flag=True):
- """Set whether the plot keeps data aspect ratio or not.
-
- This method is a simple proxy to the legacy :class:`PlotWidget` method
- of the same name. Using the object oriented approach is now
- preferred::
-
- stackview.getPlot().setKeepDataAspectRatio(flag)
-
- :param bool flag: True to respect data aspect ratio
- """
- self._plot.setKeepDataAspectRatio(flag)
-
- # kind of private methods, but needed by Profile
- def getActiveImage(self, just_legend=False):
- """Returns the currently active image object.
-
- It returns None in case of not having an active image.
-
- This method is a simple proxy to the legacy :class:`PlotWidget` method
- of the same name. Using the object oriented approach is now
- preferred::
-
- stackview.getPlot().getActiveImage()
-
- :param bool just_legend: True to get the legend of the image,
- False (the default) to get the image data and info.
- Note: :class:`StackView` uses the same legend for all frames.
- :return: legend or image object
- :rtype: str or list or None
- """
- return self._plot.getActiveImage(just_legend=just_legend)
-
- def 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)
-
- def addItem(self, *args, **kwargs):
- """
- See :meth:`Plot.Plot.addItem`
- """
- self._plot.addItem(*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')
- menu.addAction(self._plot.profile.hLineAction)
- menu.addAction(self._plot.profile.vLineAction)
- menu.addAction(self._plot.profile.lineAction)
- menu.addSeparator()
- menu.addAction(self._plot.profile.clearAction)
- self._plot.profile.profile3dAction.computeProfileIn2D()
- menu.addMenu(self._plot.profile.profile3dAction.menu())
-
- # 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 bb66613..0000000
--- a/silx/gui/plot/StatsWidget.py
+++ /dev/null
@@ -1,582 +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.
-#
-# ###########################################################################*/
-"""
-Module containing widgets displaying stats from items of a plot.
-"""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "24/07/2018"
-
-
-import functools
-import logging
-import numpy
-from collections import OrderedDict
-
-import silx.utils.weakref
-from silx.gui import qt
-from silx.gui import icons
-from silx.gui.plot.items.curve import Curve as CurveItem
-from silx.gui.plot.items.histogram import Histogram as HistogramItem
-from silx.gui.plot.items.image import ImageBase as ImageItem
-from silx.gui.plot.items.scatter import Scatter as ScatterItem
-from silx.gui.plot import stats as statsmdl
-from silx.gui.widgets.TableWidget import TableWidget
-from silx.gui.plot.stats.statshandler import StatsHandler, StatFormatter
-
-logger = logging.getLogger(__name__)
-
-
-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 parent: Qt parent
- :param plot: the plot containing items on which we want statistics.
- """
-
- sigVisibilityChanged = qt.Signal(bool)
-
- NUMBER_FORMAT = '{0:.3f}'
-
- class OptionsWidget(qt.QToolBar):
-
- def __init__(self, parent=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(True)
- 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)
-
- def isActiveItemMode(self):
- return self.itemSelection.checkedAction() is self.__displayActiveItems
-
- def isVisibleDataRangeMode(self):
- return self.dataRangeSelection.checkedAction() is self.__useVisibleData
-
- 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 = self.OptionsWidget(parent=self)
- self.layout().addWidget(self._options)
- self._statsTable = StatsTable(parent=self, plot=plot)
- self.setStats = self._statsTable.setStats
- self.setStats(stats)
-
- self.layout().addWidget(self._statsTable)
- self.setPlot = self._statsTable.setPlot
-
- self._options.itemSelection.triggered.connect(
- self._optSelectionChanged)
- self._options.dataRangeSelection.triggered.connect(
- self._optDataRangeChanged)
- self._optSelectionChanged()
- self._optDataRangeChanged()
-
- self.setDisplayOnlyActiveItem = self._statsTable.setDisplayOnlyActiveItem
- self.setStatsOnVisibleData = self._statsTable.setStatsOnVisibleData
-
- 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._statsTable.setDisplayOnlyActiveItem(self._options.isActiveItemMode())
-
- def _optDataRangeChanged(self, action=None):
- self._statsTable.setStatsOnVisibleData(self._options.isVisibleDataRangeMode())
-
-
-class BasicStatsWidget(StatsWidget):
- """
- Widget defining a simple set of :class:`Stat` to be displayed on a
- :class:`StatsWidget`.
-
- :param parent: Qt parent
- :param plot: the plot containing items on which we want statistics.
- """
-
- STATS = StatsHandler((
- (statsmdl.StatMin(), StatFormatter()),
- statsmdl.StatCoordMin(),
- (statsmdl.StatMax(), StatFormatter()),
- statsmdl.StatCoordMax(),
- (('std', numpy.std), StatFormatter()),
- (('mean', numpy.mean), StatFormatter()),
- statsmdl.StatCOM()
- ))
-
- def __init__(self, parent=None, plot=None):
- StatsWidget.__init__(self, parent=parent, plot=plot, stats=self.STATS)
-
-
-class StatsTable(TableWidget):
- """
- TableWidget displaying for each curves contained by the Plot some
- information:
-
- * legend
- * minimal value
- * maximal value
- * standard deviation (std)
-
- :param parent: The widget's parent.
- :param plot: :class:`.PlotWidget` instance on which to operate
- """
-
- COMPATIBLE_KINDS = {
- 'curve': CurveItem,
- 'image': ImageItem,
- 'scatter': ScatterItem,
- 'histogram': HistogramItem
- }
-
- COMPATIBLE_ITEMS = tuple(COMPATIBLE_KINDS.values())
-
- def __init__(self, parent=None, plot=None):
- TableWidget.__init__(self, parent)
- """Next freeID for the curve"""
- self.plot = None
- self._displayOnlyActItem = False
- self._statsOnVisibleData = False
- self._lgdAndKindToItems = {}
- """Associate to a tuple(legend, kind) the items legend"""
- self.callbackImage = None
- self.callbackScatter = None
- self.callbackCurve = None
- """Associate the curve legend to his first item"""
- self._statsHandler = None
- self._legendsSet = []
- """list of legends actually displayed"""
- self._resetColumns()
-
- self.setColumnCount(len(self._columns))
- self.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
- self.setPlot(plot)
- self.setSortingEnabled(True)
-
- def _resetColumns(self):
- self._columns_index = OrderedDict([('legend', 0), ('kind', 1)])
- self._columns = self._columns_index.keys()
- self.setColumnCount(len(self._columns))
-
- def setStats(self, statsHandler):
- """
-
- :param statsHandler: Set the statistics to be displayed and how to
- format them using
- :rtype: :class:`StatsHandler`
- """
- _statsHandler = statsHandler
- if statsHandler is None:
- _statsHandler = StatsHandler(statFormatters=())
- if isinstance(_statsHandler, (list, tuple)):
- _statsHandler = StatsHandler(_statsHandler)
- assert isinstance(_statsHandler, StatsHandler)
- self._resetColumns()
- self.clear()
-
- for statName, stat in list(_statsHandler.stats.items()):
- assert isinstance(stat, statsmdl.StatBase)
- self._columns_index[statName] = len(self._columns_index)
- self._statsHandler = _statsHandler
- self._columns = self._columns_index.keys()
- self.setColumnCount(len(self._columns))
-
- self._updateItemObserve()
- self._updateAllStats()
-
- def getStatsHandler(self):
- return self._statsHandler
-
- def _updateAllStats(self):
- for (legend, kind) in self._lgdAndKindToItems:
- self._updateStats(legend, kind)
-
- @staticmethod
- def _getKind(myItem):
- if isinstance(myItem, CurveItem):
- return 'curve'
- elif isinstance(myItem, ImageItem):
- return 'image'
- elif isinstance(myItem, ScatterItem):
- return 'scatter'
- elif isinstance(myItem, HistogramItem):
- return 'histogram'
- else:
- return None
-
- def setPlot(self, plot):
- """
- Define the plot to interact with
-
- :param plot: the plot containing the items on which statistics are
- applied
- :rtype: :class:`.PlotWidget`
- """
- if self.plot:
- self._dealWithPlotConnection(create=False)
- self.plot = plot
- self.clear()
- if self.plot:
- self._dealWithPlotConnection(create=True)
- self._updateItemObserve()
-
- def _updateItemObserve(self):
- if self.plot:
- self.clear()
- if self._displayOnlyActItem is True:
- activeCurve = self.plot.getActiveCurve(just_legend=False)
- activeScatter = self.plot._getActiveItem(kind='scatter',
- just_legend=False)
- activeImage = self.plot.getActiveImage(just_legend=False)
- if activeCurve:
- self._addItem(activeCurve)
- if activeImage:
- self._addItem(activeImage)
- if activeScatter:
- self._addItem(activeScatter)
- else:
- [self._addItem(curve) for curve in self.plot.getAllCurves()]
- [self._addItem(image) for image in self.plot.getAllImages()]
- scatters = self.plot._getItems(kind='scatter',
- just_legend=False,
- withhidden=True)
- [self._addItem(scatter) for scatter in scatters]
- histograms = self.plot._getItems(kind='histogram',
- just_legend=False,
- withhidden=True)
- [self._addItem(histogram) for histogram in histograms]
-
- def _dealWithPlotConnection(self, create=True):
- """
- Manage connection to plot signals
-
- Note: connection on Item are managed by the _removeItem function
- """
- if self.plot is None:
- return
- if self._displayOnlyActItem:
- if create is True:
- if self.callbackImage is None:
- self.callbackImage = functools.partial(self._activeItemChanged, 'image')
- self.callbackScatter = functools.partial(self._activeItemChanged, 'scatter')
- self.callbackCurve = functools.partial(self._activeItemChanged, 'curve')
- self.plot.sigActiveImageChanged.connect(self.callbackImage)
- self.plot.sigActiveScatterChanged.connect(self.callbackScatter)
- self.plot.sigActiveCurveChanged.connect(self.callbackCurve)
- else:
- if self.callbackImage is not None:
- self.plot.sigActiveImageChanged.disconnect(self.callbackImage)
- self.plot.sigActiveScatterChanged.disconnect(self.callbackScatter)
- self.plot.sigActiveCurveChanged.disconnect(self.callbackCurve)
- self.callbackImage = None
- self.callbackScatter = None
- self.callbackCurve = None
- else:
- if create is True:
- self.plot.sigContentChanged.connect(self._plotContentChanged)
- else:
- self.plot.sigContentChanged.disconnect(self._plotContentChanged)
- if create is True:
- self.plot.sigPlotSignal.connect(self._zoomPlotChanged)
- else:
- self.plot.sigPlotSignal.disconnect(self._zoomPlotChanged)
-
- def clear(self):
- """
- Clear all existing items
- """
- lgdsAndKinds = list(self._lgdAndKindToItems.keys())
- for lgdAndKind in lgdsAndKinds:
- self._removeItem(legend=lgdAndKind[0], kind=lgdAndKind[1])
- self._lgdAndKindToItems = {}
- qt.QTableWidget.clear(self)
- self.setRowCount(0)
-
- # It have to called befor3e accessing to the header items
- self.setHorizontalHeaderLabels(list(self._columns))
-
- if self._statsHandler is not None:
- for columnId, name in enumerate(self._columns):
- item = self.horizontalHeaderItem(columnId)
- if name in self._statsHandler.stats:
- stat = self._statsHandler.stats[name]
- text = stat.name[0].upper() + stat.name[1:]
- if stat.description is not None:
- tooltip = stat.description
- else:
- tooltip = ""
- else:
- text = name[0].upper() + name[1:]
- tooltip = ""
- item.setToolTip(tooltip)
- item.setText(text)
-
- if hasattr(self.horizontalHeader(), 'setSectionResizeMode'): # Qt5
- self.horizontalHeader().setSectionResizeMode(qt.QHeaderView.ResizeToContents)
- else: # Qt4
- self.horizontalHeader().setResizeMode(qt.QHeaderView.ResizeToContents)
- self.setColumnHidden(self._columns_index['kind'], True)
-
- def _addItem(self, item):
- assert isinstance(item, self.COMPATIBLE_ITEMS)
- if (item.getLegend(), self._getKind(item)) in self._lgdAndKindToItems:
- self._updateStats(item.getLegend(), self._getKind(item))
- return
-
- self.setRowCount(self.rowCount() + 1)
- indexTable = self.rowCount() - 1
- kind = self._getKind(item)
-
- self._lgdAndKindToItems[(item.getLegend(), kind)] = {}
-
- # the get item will manage the item creation of not existing
- _createItem = self._getItem
- for itemName in self._columns:
- _createItem(name=itemName, legend=item.getLegend(), kind=kind,
- indexTable=indexTable)
-
- self._updateStats(legend=item.getLegend(), kind=kind)
-
- callback = functools.partial(
- silx.utils.weakref.WeakMethodProxy(self._updateStats),
- item.getLegend(), kind)
- item.sigItemChanged.connect(callback)
- self.setColumnHidden(self._columns_index['kind'],
- item.getLegend() not in self._legendsSet)
- self._legendsSet.append(item.getLegend())
-
- def _getItem(self, name, legend, kind, indexTable):
- if (legend, kind) not in self._lgdAndKindToItems:
- self._lgdAndKindToItems[(legend, kind)] = {}
- if not (name in self._lgdAndKindToItems[(legend, kind)] and
- self._lgdAndKindToItems[(legend, kind)]):
- if name in ('legend', 'kind'):
- _item = qt.QTableWidgetItem(type=qt.QTableWidgetItem.Type)
- if name == 'legend':
- _item.setText(legend)
- else:
- assert name == 'kind'
- _item.setText(kind)
- else:
- if self._statsHandler.formatters[name]:
- _item = self._statsHandler.formatters[name].tabWidgetItemClass()
- else:
- _item = qt.QTableWidgetItem()
- tooltip = self._statsHandler.stats[name].getToolTip(kind=kind)
- if tooltip is not None:
- _item.setToolTip(tooltip)
-
- _item.setFlags(qt.Qt.ItemIsEnabled | qt.Qt.ItemIsSelectable)
- self.setItem(indexTable, self._columns_index[name], _item)
- self._lgdAndKindToItems[(legend, kind)][name] = _item
-
- return self._lgdAndKindToItems[(legend, kind)][name]
-
- def _removeItem(self, legend, kind):
- if (legend, kind) not in self._lgdAndKindToItems or not self.plot:
- return
-
- self.firstItem = self._lgdAndKindToItems[(legend, kind)]['legend']
- del self._lgdAndKindToItems[(legend, kind)]
- self.removeRow(self.firstItem.row())
- self._legendsSet.remove(legend)
- self.setColumnHidden(self._columns_index['kind'],
- legend not in self._legendsSet)
-
- def _updateCurrentStats(self):
- for lgdAndKind in self._lgdAndKindToItems:
- self._updateStats(lgdAndKind[0], lgdAndKind[1])
-
- def _updateStats(self, legend, kind, event=None):
- if self._statsHandler is None:
- return
-
- assert kind in ('curve', 'image', 'scatter', 'histogram')
- if kind == 'curve':
- item = self.plot.getCurve(legend)
- elif kind == 'image':
- item = self.plot.getImage(legend)
- elif kind == 'scatter':
- item = self.plot.getScatter(legend)
- elif kind == 'histogram':
- item = self.plot.getHistogram(legend)
- else:
- raise ValueError('kind not managed')
-
- if not item or (item.getLegend(), kind) not in self._lgdAndKindToItems:
- return
-
- assert isinstance(item, self.COMPATIBLE_ITEMS)
-
- statsValDict = self._statsHandler.calculate(item, self.plot,
- self._statsOnVisibleData)
-
- lgdItem = self._lgdAndKindToItems[(item.getLegend(), kind)]['legend']
- assert lgdItem
- rowStat = lgdItem.row()
-
- for statName, statVal in list(statsValDict.items()):
- assert statName in self._lgdAndKindToItems[(item.getLegend(), kind)]
- tableItem = self._getItem(name=statName, legend=item.getLegend(),
- kind=kind, indexTable=rowStat)
- tableItem.setText(str(statVal))
-
- def currentChanged(self, current, previous):
- if current.row() >= 0:
- legendItem = self.item(current.row(), self._columns_index['legend'])
- assert legendItem
- kindItem = self.item(current.row(), self._columns_index['kind'])
- kind = kindItem.text()
- if kind == 'curve':
- self.plot.setActiveCurve(legendItem.text())
- elif kind == 'image':
- self.plot.setActiveImage(legendItem.text())
- elif kind == 'scatter':
- self.plot._setActiveItem('scatter', legendItem.text())
- elif kind == 'histogram':
- # active histogram not managed by the plot actually
- pass
- else:
- raise ValueError('kind not managed')
- qt.QTableWidget.currentChanged(self, current, previous)
-
- def setDisplayOnlyActiveItem(self, displayOnlyActItem):
- """
-
- :param bool displayOnlyActItem: True if we want to only show active
- item
- """
- if self._displayOnlyActItem == displayOnlyActItem:
- return
- self._displayOnlyActItem = displayOnlyActItem
- self._dealWithPlotConnection(create=False)
- self._updateItemObserve()
- self._dealWithPlotConnection(create=True)
-
- def setStatsOnVisibleData(self, b):
- """
- .. 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._statsOnVisibleData = b
- self._updateCurrentStats()
-
- def _activeItemChanged(self, kind, previous, current):
- """Callback used when plotting only the active item"""
- assert kind in ('curve', 'image', 'scatter', 'histogram')
- self._updateItemObserve()
-
- def _plotContentChanged(self, action, kind, legend):
- """Callback used when plotting all the plot items"""
- if kind not in ('curve', 'image', 'scatter', 'histogram'):
- return
- if kind == 'curve':
- item = self.plot.getCurve(legend)
- elif kind == 'image':
- item = self.plot.getImage(legend)
- elif kind == 'scatter':
- item = self.plot.getScatter(legend)
- elif kind == 'histogram':
- item = self.plot.getHistogram(legend)
- else:
- raise ValueError('kind not managed')
-
- if action == 'add':
- if item is None:
- raise ValueError('Item from legend "%s" do not exists' % legend)
- self._addItem(item)
- elif action == 'remove':
- self._removeItem(legend, kind)
-
- def _zoomPlotChanged(self, event):
- if self._statsOnVisibleData is True:
- if 'event' in event and event['event'] == 'limitsChanged':
- self._updateCurrentStats()
diff --git a/silx/gui/plot/_BaseMaskToolsWidget.py b/silx/gui/plot/_BaseMaskToolsWidget.py
deleted file mode 100644
index e087354..0000000
--- a/silx/gui/plot/_BaseMaskToolsWidget.py
+++ /dev/null
@@ -1,1167 +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 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__ = "29/08/2018"
-
-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"""
-
- 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 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.all(numpy.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)
-
- 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)
-
- 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)
-
- # 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 disk 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 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=numpy.bool)
- # overlays colors set by the user
- self._overlayColors = numpy.zeros((self._maxLevelNumber + 1, 3), dtype=numpy.float32)
-
- # as parent have to be the first argument of the widget to fit
- # QtDesigner need but here plot can't be None by default.
- assert plot is not None
- self._plotRef = weakref.ref(plot)
- self._maskName = '__MASK_TOOLS_%d' % id(self) # Legend of the mask
-
- self._colormap = Colormap(name="",
- normalization='linear',
- vmin=0,
- vmax=self._maxLevelNumber,
- colors=None)
- self._defaultOverlayColor = rgba('gray') # Color of the mask
- self._setMaskColors(1, 0.5)
-
- if not isinstance(mask, BaseMask):
- raise TypeError("mask is not an instance of BaseMask")
- self._mask = mask
-
- self._mask.sigChanged.connect(self._updatePlotMask)
- self._mask.sigChanged.connect(self._emitSigMaskChanged)
-
- 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 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')
-
- @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.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(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()
-
- # Buttons group
- invertBtn = qt.QPushButton('Invert')
- invertBtn.setShortcut(qt.Qt.CTRL + qt.Qt.Key_I)
- invertBtn.setToolTip('Invert current mask <b>%s</b>' %
- invertBtn.shortcut().toString())
- invertBtn.clicked.connect(self._handleInvertMask)
-
- clearBtn = qt.QPushButton('Clear')
- clearBtn.setShortcut(qt.QKeySequence.Delete)
- clearBtn.setToolTip('Clear current mask level <b>%s</b>' %
- clearBtn.shortcut().toString())
- clearBtn.clicked.connect(self._handleClearMask)
-
- invertClearWidget = self._hboxWidget(
- invertBtn, clearBtn, stretch=False)
-
- undoBtn = qt.QPushButton('Undo')
- undoBtn.setShortcut(qt.QKeySequence.Undo)
- undoBtn.setToolTip('Undo last mask change <b>%s</b>' %
- undoBtn.shortcut().toString())
- self._mask.sigUndoable.connect(undoBtn.setEnabled)
- undoBtn.clicked.connect(self._mask.undo)
-
- redoBtn = qt.QPushButton('Redo')
- redoBtn.setShortcut(qt.QKeySequence.Redo)
- redoBtn.setToolTip('Redo last undone mask change <b>%s</b>' %
- redoBtn.shortcut().toString())
- self._mask.sigRedoable.connect(redoBtn.setEnabled)
- redoBtn.clicked.connect(self._mask.redo)
-
- undoRedoWidget = self._hboxWidget(undoBtn, redoBtn, stretch=False)
-
- self.clearAllBtn = qt.QPushButton('Clear all')
- self.clearAllBtn.setToolTip('Clear all mask levels')
- self.clearAllBtn.clicked.connect(self.resetSelectionMask)
-
- loadBtn = qt.QPushButton('Load...')
- loadBtn.clicked.connect(self._loadMask)
-
- saveBtn = qt.QPushButton('Save...')
- saveBtn.clicked.connect(self._saveMask)
-
- self.loadSaveWidget = self._hboxWidget(loadBtn, saveBtn, stretch=False)
-
- layout = qt.QVBoxLayout()
- layout.addWidget(self.levelWidget)
- layout.addWidget(self.transparencyWidget)
- layout.addWidget(invertClearWidget)
- layout.addWidget(undoRedoWidget)
- layout.addWidget(self.clearAllBtn)
- layout.addWidget(self.loadSaveWidget)
- 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', None)
- 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.polygonAction = qt.QAction(
- icons.getQIcon('shape-polygon'), 'Polygon selection', None)
- 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', None)
- 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.polygonAction)
- self.drawActionGroup.addAction(self.pencilAction)
-
- actions = (self.browseAction, self.rectAction,
- 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"""
- layout = qt.QVBoxLayout()
-
- # Thresholing
-
- self.belowThresholdAction = qt.QAction(
- icons.getQIcon('plot-roi-below'), 'Mask below threshold', None)
- self.belowThresholdAction.setToolTip(
- 'Mask image where values are below given threshold')
- self.belowThresholdAction.setCheckable(True)
- self.belowThresholdAction.triggered[bool].connect(
- self._belowThresholdActionTriggered)
-
- self.betweenThresholdAction = qt.QAction(
- icons.getQIcon('plot-roi-between'), 'Mask within range', None)
- self.betweenThresholdAction.setToolTip(
- 'Mask image where values are within given range')
- self.betweenThresholdAction.setCheckable(True)
- self.betweenThresholdAction.triggered[bool].connect(
- self._betweenThresholdActionTriggered)
-
- self.aboveThresholdAction = qt.QAction(
- icons.getQIcon('plot-roi-above'), 'Mask above threshold', None)
- self.aboveThresholdAction.setToolTip(
- 'Mask image where values are above given threshold')
- self.aboveThresholdAction.setCheckable(True)
- self.aboveThresholdAction.triggered[bool].connect(
- self._aboveThresholdActionTriggered)
-
- self.thresholdActionGroup = qt.QActionGroup(self)
- self.thresholdActionGroup.setExclusive(False)
- 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', None)
- 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()
- spacer.setSizePolicy(qt.QSizePolicy.Expanding,
- qt.QSizePolicy.Preferred)
- widgets.append(spacer)
-
- loadColormapRangeBtn = qt.QToolButton()
- loadColormapRangeBtn.setDefaultAction(self.loadColormapRangeAction)
- widgets.append(loadColormapRangeBtn)
-
- container = self._hboxWidget(*widgets, stretch=False)
- layout.addWidget(container)
-
- form = qt.QFormLayout()
-
- self.minLineEdit = FloatEdit(self, value=0)
- self.minLineEdit.setEnabled(False)
- form.addRow('Min:', self.minLineEdit)
-
- self.maxLineEdit = FloatEdit(self, value=0)
- self.maxLineEdit.setEnabled(False)
- form.addRow('Max:', self.maxLineEdit)
-
- self.applyMaskBtn = qt.QPushButton('Apply mask')
- self.applyMaskBtn.clicked.connect(self._maskBtnClicked)
- self.applyMaskBtn.setEnabled(False)
- form.addRow(self.applyMaskBtn)
-
- self.maskNanBtn = qt.QPushButton('Mask not finite values')
- self.maskNanBtn.setToolTip('Mask Not a Number and infinite values')
- self.maskNanBtn.clicked.connect(self._maskNotFiniteBtnClicked)
- form.addRow(self.maskNanBtn)
-
- thresholdWidget = qt.QWidget()
- thresholdWidget.setLayout(form)
- layout.addWidget(thresholdWidget)
-
- layout.addStretch(1)
-
- self.thresholdGroup = qt.QGroupBox('Threshold')
- self.thresholdGroup.setLayout(layout)
- return self.thresholdGroup
-
- # track widget visibility and plot active image changes
-
- 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
- """
- 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 == '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 _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 _belowThresholdActionTriggered(self, triggered):
- if triggered:
- self.minLineEdit.setEnabled(True)
- self.maxLineEdit.setEnabled(False)
- self.applyMaskBtn.setEnabled(True)
-
- def _betweenThresholdActionTriggered(self, triggered):
- if triggered:
- self.minLineEdit.setEnabled(True)
- self.maxLineEdit.setEnabled(True)
- self.applyMaskBtn.setEnabled(True)
-
- def _aboveThresholdActionTriggered(self, triggered):
- if triggered:
- self.minLineEdit.setEnabled(False)
- self.maxLineEdit.setEnabled(True)
- self.applyMaskBtn.setEnabled(True)
-
- def _thresholdActionGroupTriggered(self, triggeredAction):
- """Threshold action group listener."""
- if triggeredAction.isChecked():
- # Uncheck other actions
- for action in self.thresholdActionGroup.actions():
- if action is not triggeredAction and action.isChecked():
- action.setChecked(False)
- else:
- # Disable min/max edit
- self.minLineEdit.setEnabled(False)
- self.maxLineEdit.setEnabled(False)
- self.applyMaskBtn.setEnabled(False)
-
- 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/dtime_ticklayout.py b/silx/gui/plot/_utils/dtime_ticklayout.py
deleted file mode 100644
index 95fc235..0000000
--- a/silx/gui/plot/_utils/dtime_ticklayout.py
+++ /dev/null
@@ -1,438 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-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 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 logging
-import math
-import time
-
-import dateutil.tz
-
-from dateutil.relativedelta import relativedelta
-
-from silx.third_party import enum
-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 with the
-
- :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)
-
- delta = dMax - dMin
- lengthSec = delta.total_seconds()
- _logger.debug("findStartDate: {}, {} (duration = {} sec, {} days)"
- .format(dMin, dMax, lengthSec, lengthSec / SECONDS_PER_DAY))
-
- length, unit = bestUnit(delta.total_seconds())
- 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):
-
- # Month and years will be converted to integers
- assert int(step) > 0, "Integer value or tickstep is 0"
- 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 77e8be2..0000000
--- a/silx/gui/plot/actions/PlotToolAction.py
+++ /dev/null
@@ -1,150 +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:`.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 containg tohe 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 100644
index 10df130..0000000
--- a/silx/gui/plot/actions/control.py
+++ /dev/null
@@ -1,604 +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.control` provides a set of QAction relative to control
-of a :class:`.PlotWidget`.
-
-The following QAction are available:
-
-- :class:`ColormapAction`
-- :class:`CrosshairAction`
-- :class:`CurveStyleAction`
-- :class:`GridAction`
-- :class:`KeepAspectRatioAction`
-- :class:`PanWithArrowKeysAction`
-- :class:`ResetZoomAction`
-- :class:`XAxisLogarithmicAction`
-- :class:`XAxisAutoScaleAction`
-- :class:`YAxisInvertedAction`
-- :class:`YAxisLogarithmicAction`
-- :class:`YAxisAutoScaleAction`
-- :class:`ZoomBackAction`
-- :class:`ZoomInAction`
-- :class:`ZoomOutAction`
-- :class:'ShowAxisAction'
-"""
-
-from __future__ import division
-
-__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-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())
-
- # 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._dialog.show()
- self._updateColormap()
- 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.ImageComplexData):
- # Specific init for complex images
- colormap = image.getColormap()
-
- mode = image.getVisualizationMode()
- if mode in (items.ImageComplexData.Mode.AMPLITUDE_PHASE,
- items.ImageComplexData.Mode.LOG10_AMPLITUDE_PHASE):
- data = image.getData(
- copy=False, mode=items.ImageComplexData.Mode.PHASE)
- else:
- data = image.getData(copy=False)
-
- # Set histogram and range if any
- self._dialog.setData(data)
-
- elif isinstance(image, items.ColormapMixIn):
- # Set dialog from active image
- colormap = image.getColormap()
- data = image.getData(copy=False)
- # Set histogram and range if any
- self._dialog.setData(data)
-
- 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()
- data = scatter.getValueData(copy=False)
- self._dialog.setData(data)
-
- 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._backend.isAxesDisplayed())
- plot._sigAxesVisibilityChanged.connect(self.setChecked)
-
- def _actionTriggered(self, checked=False):
- self.plot.setAxesDisplayed(checked)
-
diff --git a/silx/gui/plot/actions/fit.py b/silx/gui/plot/actions/fit.py
deleted file mode 100644
index cb70733..0000000
--- a/silx/gui/plot/actions/fit.py
+++ /dev/null
@@ -1,186 +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.
-#
-# ###########################################################################*/
-"""
-: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"
-
-from .PlotToolAction import PlotToolAction
-import logging
-from silx.gui import qt
-from silx.gui.plot.ItemsSelectionDialog import ItemsSelectionDialog
-from silx.gui.plot.items import Curve, Histogram
-
-_logger = logging.getLogger(__name__)
-
-
-def _getUniqueCurve(plt):
- """Get a single curve from the plot.
- Get the active curve if any, else if a single curve is plotted
- get it, else return None.
-
- :param plt: :class:`.PlotWidget` instance on which to operate
-
- :return: return value of plt.getActiveCurve(), or plt.getAllCurves()[0],
- or None
- """
- curve = plt.getActiveCurve()
- if curve is not None:
- return curve
-
- curves = plt.getAllCurves()
- if len(curves) == 0:
- return None
-
- if len(curves) == 1 and len(plt._getItems(kind='histogram')) == 0:
- return curves[0]
-
- return None
-
-
-def _getUniqueHistogram(plt):
- """Return the histogram if there is a single histogram and no curve in
- the plot. In all other cases, return None.
-
- :param plt: :class:`.PlotWidget` instance on which to operate
- :return: histogram or None
- """
- histograms = plt._getItems(kind='histogram')
- if len(histograms) != 1:
- return None
- if plt.getAllCurves(just_legend=True):
- return None
- return histograms[0]
-
-
-class FitAction(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):
- super(FitAction, self).__init__(
- plot, icon='math-fit', text='Fit curve',
- tooltip='Open a fit dialog',
- parent=parent)
- self.fit_widget = None
-
- def _createToolWindow(self):
- window = qt.QMainWindow(parent=self.plot)
- # import done here rather than at module level to avoid circular import
- # FitWidget -> BackgroundWidget -> PlotWindow -> actions -> fit -> FitWidget
- from ...fit.FitWidget import FitWidget
- fit_widget = FitWidget(parent=window)
- window.setCentralWidget(fit_widget)
- fit_widget.guibuttons.DismissButton.clicked.connect(window.close)
- fit_widget.sigFitWidgetSignal.connect(self.handle_signal)
- self.fit_widget = fit_widget
- return window
-
- def _connectPlot(self, window):
- # Wait for the next iteration, else the plot is not yet initialized
- # No curve available
- qt.QTimer.singleShot(10, lambda: self._initFit(window))
-
- def _initFit(self, window):
- plot = self.plot
- self.xlabel = plot.getXAxis().getLabel()
- self.ylabel = plot.getYAxis().getLabel()
- self.xmin, self.xmax = plot.getXAxis().getLimits()
-
- histo = _getUniqueHistogram(self.plot)
- curve = _getUniqueCurve(self.plot)
-
- if histo is None and curve is None:
- # ambiguous case, we need to ask which plot item to fit
- isd = ItemsSelectionDialog(parent=plot, plot=self.plot)
- isd.setWindowTitle("Select item to be fitted")
- isd.setItemsSelectionMode(qt.QTableWidget.SingleSelection)
- isd.setAvailableKinds(["curve", "histogram"])
- isd.selectAllKinds()
-
- result = isd.exec_()
- if result and len(isd.getSelectedItems()) == 1:
- item = isd.getSelectedItems()[0]
- else:
- return
- elif histo is not None:
- # presence of a unique histo and no curve
- item = histo
- elif curve is not None:
- # presence of a unique or active curve
- item = curve
-
- self.legend = item.getLegend()
-
- if isinstance(item, Histogram):
- bin_edges = item.getBinEdgesData(copy=False)
- # take the middle coordinate between adjacent bin edges
- self.x = (bin_edges[1:] + bin_edges[:-1]) / 2
- self.y = item.getValueData(copy=False)
- # else take the active curve, or else the unique curve
- elif isinstance(item, Curve):
- self.x = item.getXData(copy=False)
- self.y = item.getYData(copy=False)
-
- self.fit_widget.setData(self.x, self.y,
- xmin=self.xmin, xmax=self.xmax)
- window.setWindowTitle(
- "Fitting " + self.legend +
- " on x range %f-%f" % (self.xmin, self.xmax))
-
- def handle_signal(self, ddict):
- x_fit = self.x[self.xmin <= self.x]
- x_fit = x_fit[x_fit <= self.xmax]
- fit_legend = "Fit <%s>" % self.legend
- fit_curve = self.plot.getCurve(fit_legend)
-
- if ddict["event"] == "FitFinished":
- y_fit = self.fit_widget.fitmanager.gendata()
- if fit_curve is None:
- self.plot.addCurve(x_fit, y_fit,
- fit_legend,
- xlabel=self.xlabel, ylabel=self.ylabel,
- resetzoom=False)
- else:
- fit_curve.setData(x_fit, y_fit)
- fit_curve.setVisible(True)
-
- if ddict["event"] in ["FitStarted", "FitFailed"]:
- if fit_curve is not None:
- fit_curve.setVisible(False)
diff --git a/silx/gui/plot/actions/histogram.py b/silx/gui/plot/actions/histogram.py
deleted file mode 100644
index 9181f53..0000000
--- a/silx/gui/plot/actions/histogram.py
+++ /dev/null
@@ -1,146 +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.
-#
-# ###########################################################################*/
-"""
-: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__ = "10/10/2018"
-__license__ = "MIT"
-
-from .PlotToolAction import PlotToolAction
-from silx.math.histogram import Histogramnd
-from silx.math.combo import min_max
-import numpy
-import logging
-from silx.gui import qt
-
-_logger = logging.getLogger(__name__)
-
-
-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._connectedToActiveImage = False
- self._histo = None
-
- def _connectPlot(self, window):
- if not self._connectedToActiveImage:
- self.plot.sigActiveImageChanged.connect(
- self._activeImageChanged)
- self._connectedToActiveImage = True
- self.computeIntensityDistribution()
- PlotToolAction._connectPlot(self, window)
-
- def _disconnectPlot(self, window):
- if self._connectedToActiveImage:
- self.plot.sigActiveImageChanged.disconnect(
- self._activeImageChanged)
- self._connectedToActiveImage = False
- PlotToolAction._disconnectPlot(self, window)
-
- def _activeImageChanged(self, previous, legend):
- """Handle active image change: toggle enabled toolbar, update curve"""
- if self._isWindowInUse():
- self.computeIntensityDistribution()
-
- def computeIntensityDistribution(self):
- """Get the active image and compute the image intensity distribution
- """
- activeImage = self.plot.getActiveImage()
-
- if activeImage is not None:
- image = activeImage.getData(copy=False)
- if image.ndim == 3: # RGB(A) images
- _logger.info('Converting current image from RGB(A) to grayscale\
- in order to compute the intensity distribution')
- image = (image[:, :, 0] * 0.299 +
- image[:, :, 1] * 0.587 +
- image[:, :, 2] * 0.114)
-
- xmin, xmax = min_max(image, min_positive=False, finite=True)
- nbins = min(1024, int(numpy.sqrt(image.size)))
- data_range = xmin, xmax
-
- # bad hack: get 256 bins in the case we have a B&W
- if numpy.issubdtype(image.dtype, numpy.integer):
- if nbins > xmax - xmin:
- nbins = xmax - xmin
-
- nbins = max(2, nbins)
-
- data = image.ravel().astype(numpy.float32)
- histogram = Histogramnd(data, n_bins=nbins, histo_range=data_range)
- assert len(histogram.edges) == 1
- self._histo = histogram.histo
- edges = histogram.edges[0]
- plot = self.getHistogramPlotWidget()
- plot.addHistogram(histogram=self._histo,
- edges=edges,
- legend='pixel intensity',
- fill=True,
- color='#66aad7')
- plot.resetZoom()
-
- def getHistogramPlotWidget(self):
- """Create the plot histogram if needed, otherwise create it
-
- :return: the PlotWidget showing the histogram of the pixel intensities
- """
- return self._getToolWindow()
-
- def _createToolWindow(self):
- from silx.gui.plot.PlotWindow import Plot1D
- window = Plot1D(parent=self.plot)
- window.setWindowFlags(qt.Qt.Window)
- window.setWindowTitle('Image Intensity Histogram')
- window.getXAxis().setLabel("Value")
- window.getYAxis().setLabel("Count")
- return window
-
- def getHistogram(self):
- """Return the last computed histogram
-
- :return: the histogram displayed in the HistogramPlotWiget
- """
- return self._histo
diff --git a/silx/gui/plot/actions/io.py b/silx/gui/plot/actions/io.py
deleted file mode 100644
index 97de527..0000000
--- a/silx/gui/plot/actions/io.py
+++ /dev/null
@@ -1,743 +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.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__ = "12/07/2018"
-
-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()}
-
- # 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)
-
- def _errorMessage(self, informativeText=''):
- """Display an error message."""
- # TODO issue with QMessageBox size fixed and too small
- msg = qt.QMessageBox(self.plot)
- msg.setIcon(qt.QMessageBox.Critical)
- msg.setInformativeText(informativeText + ' ' + str(sys.exc_info()[1]))
- msg.setDetailedText(traceback.format_exc())
- msg.exec_()
-
- def _saveSnapshot(self, 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 _selectWriteableOutputGroup(self, filename):
- 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:
- self._errorMessage('Save failed (file access issue)\n')
- return None
-
- def _saveCurveAsNXdata(self, curve, filename):
- entryPath = self._selectWriteableOutputGroup(filename)
- 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")
- 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)
-
- xlabel, ylabel = self._getAxesLabels(curve)
-
- if nameFilter == self.CURVE_FILTER_NXDATA:
- return self._saveCurveAsNXdata(curve, filename)
-
- try:
- save1D(filename,
- curve.getXData(copy=False),
- curve.getYData(copy=False),
- xlabel, [ylabel],
- fmt=fmt, csvdelim=csvdelim,
- autoheader=autoheader)
- except IOError:
- self._errorMessage('Save failed\n')
- return False
-
- return True
-
- def _saveCurves(self, 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")
- return False
-
- curve = curves[0]
- scanno = 1
- try:
- xlabel = curve.getXLabel() or plot.getGraphXLabel()
- ylabel = curve.getYLabel() or plot.getGraphYLabel(curve.getYAxis())
- specfile = savespec(filename,
- curve.getXData(copy=False),
- curve.getYData(copy=False),
- xlabel,
- ylabel,
- fmt="%.7g", scan_number=1, mode="w",
- write_file_header=True,
- close_file=False)
- except IOError:
- self._errorMessage('Save failed\n')
- return False
-
- for curve in curves[1:]:
- try:
- scanno += 1
- xlabel = curve.getXLabel() or plot.getGraphXLabel()
- ylabel = curve.getYLabel() or plot.getGraphYLabel(curve.getYAxis())
- specfile = savespec(specfile,
- curve.getXData(copy=False),
- curve.getYData(copy=False),
- xlabel,
- ylabel,
- fmt="%.7g", scan_number=scanno,
- write_file_header=False,
- close_file=False)
- except IOError:
- self._errorMessage('Save failed\n')
- 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')
- return False
- return True
-
- elif nameFilter == self.IMAGE_FILTER_NXDATA:
- entryPath = self._selectWriteableOutputGroup(filename)
- 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')
- 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)
- 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):
- """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)
- """
- assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter')
-
- self._filters[dataKind][nameFilter] = func
-
- 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.DEFAULT_APPEND_FILTERS:
- 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 276f970..0000000
--- a/silx/gui/plot/actions/medfilt.py
+++ /dev/null
@@ -1,147 +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.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).getLegend()
-
- def _updateFilter(self, kernelWidth, conditional=False):
- if self._originalImage is None:
- return
-
- self.plot.sigActiveImageChanged.disconnect(self._updateActiveImage)
- filteredImage = self._computeFilteredImage(kernelWidth, conditional)
- self.plot.addImage(data=filteredImage,
- legend=self._legend,
- replace=True)
- self.plot.sigActiveImageChanged.connect(self._updateActiveImage)
-
- def _computeFilteredImage(self, kernelWidth, conditional):
- raise 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 100644
index 7fb8be0..0000000
--- a/silx/gui/plot/backends/BackendBase.py
+++ /dev/null
@@ -1,548 +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.
-#
-# ############################################################################*/
-"""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__ = "24/04/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._xAxisTimeZone = None
- self._axesDisplayed = True
- # Store a weakref to get access to the plot state.
- self._setPlot(plot)
-
- self.__zoomBackAction = None
-
- @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)
-
- # Default Qt context menu
-
- def contextMenuEvent(self, event):
- """Override QWidget.contextMenuEvent to implement the context menu"""
- if self.__zoomBackAction is None:
- from ..actions.control import ZoomBackAction # Avoid cyclic import
- self.__zoomBackAction = ZoomBackAction(plot=self._plot,
- parent=self._plot)
- menu = qt.QMenu(self)
- menu.addAction(self.__zoomBackAction)
- menu.exec_(event.globalPos())
-
- # Add methods
-
- def addCurve(self, x, y, legend,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror, z, selectable,
- fill, alpha, symbolsize):
- """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 str legend: The legend to be associated to the curve
- :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 int z: Layer on which to draw the cuve
- :param bool selectable: indicate if the curve can be selected
- :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 legend
-
- def addImage(self, data, legend,
- origin, scale, z,
- selectable, draggable,
- colormap, alpha):
- """Add an image to the plot.
-
- :param numpy.ndarray data: (nrows, ncolumns) data or
- (nrows, ncolumns, RGBA) ubyte array
- :param str legend: The legend to be associated to the image
- :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 int z: Layer on which to draw the image
- :param bool selectable: indicate if the image can be selected
- :param bool draggable: indicate if the image can be moved
- :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 legend
-
- def addItem(self, x, y, legend, shape, color, fill, overlay, z):
- """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 legend: The legend to be associated to the item
- :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 int z: Layer on which to draw the item
- :returns: The handle used by the backend to univocally access the item
- """
- return legend
-
- def addMarker(self, x, y, legend, text, color,
- selectable, draggable,
- symbol, linestyle, linewidth, constraint):
- """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 legend: Legend associated to the marker
- :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 bool selectable: indicate if the marker can be selected
- :param bool draggable: indicate if the marker can be moved
- :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.
- This parameter is only used if draggable is True.
- :type constraint: None or a callable that takes the coordinates of
- the current cursor position in the plot as input
- and that returns the filtered coordinates.
- :return: Handle used by the backend to univocally access the marker
- """
- return legend
-
- # 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 pickItems(self, x, y, kinds):
- """Get a list of items at a pixel position.
-
- :param float x: The x pixel coord where to pick.
- :param float y: The y pixel coord where to pick.
- :param List[str] kind: List of item kinds to pick.
- Supported kinds: 'marker', 'curve', 'image'.
- :return: All picked items from back to front.
- One dict per item,
- with 'kind' key in 'curve', 'marker', 'image';
- 'legend' key, the item legend.
- and for curves, 'xdata' and 'ydata' keys storing picked
- position on the curve.
- :rtype: list of dict
- """
- return []
-
- # 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
- """
- raise NotImplementedError()
-
- 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.
- """
- raise NotImplementedError()
-
- 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, check):
- """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').
- :param bool check: True to check if the coordinates are in the
- plot area.
- :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 setAxesDisplayed(self, displayed):
- """Display or not the axes.
-
- :param bool displayed: If `True` axes are displayed. If `False` axes
- are not anymore visible and the margin used for them is removed.
- """
- self._axesDisplayed = displayed
-
- def isAxesDisplayed(self):
- """private because in some case it is possible that one of the two axes
- are displayed and not the other.
- This only check status set to axes from the public API
- """
- return self._axesDisplayed
diff --git a/silx/gui/plot/backends/BackendMatplotlib.py b/silx/gui/plot/backends/BackendMatplotlib.py
deleted file mode 100644
index 3b1d6dd..0000000
--- a/silx/gui/plot/backends/BackendMatplotlib.py
+++ /dev/null
@@ -1,1139 +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.
-#
-# ###########################################################################*/
-"""Matplotlib Plot backend."""
-
-from __future__ import division
-
-__authors__ = ["V.A. Sole", "T. Vincent, H. Payno"]
-__license__ = "MIT"
-__date__ = "01/08/2018"
-
-
-import logging
-import datetime as dt
-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 ..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.collections import PathCollection, LineCollection
-from matplotlib.ticker import Formatter, ScalarFormatter, Locator
-
-
-from ....third_party.modest_image import ModestImage
-from . import BackendBase
-from .._utils import FLOAT32_MINPOS
-from .._utils.dtime_ticklayout import calcTicks, bestFormatString, timestamp
-
-
-
-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 _MarkerContainer(Container):
- """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, x, y):
- self.line = artists[0]
- self.text = artists[1] if len(artists) > 1 else None
- self.x = x
- self.y = y
-
- Container.__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):
- """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 upprt limit
- """
- 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 None: # vertical line
- delta = abs(ymax - ymin)
- if ymin > ymax:
- ymax = ymin
- ymax -= 0.005 * delta
- self.text.set_y(ymax)
-
- if self.x is None and self.y is not None: # Horizontal line
- delta = abs(xmax - xmin)
- if xmin > xmax:
- xmax = xmin
- xmax -= 0.005 * delta
- self.text.set_x(xmax)
-
-
-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")
-
- # disable the use of offsets
- try:
- self.ax.get_yaxis().get_major_formatter().set_useOffset(False)
- self.ax.get_xaxis().get_major_formatter().set_useOffset(False)
- self.ax2.get_yaxis().get_major_formatter().set_useOffset(False)
- self.ax2.get_xaxis().get_major_formatter().set_useOffset(False)
- except:
- _logger.warning('Cannot disabled axes offsets in %s ' \
- % matplotlib.__version__)
-
- # critical for picking!!!!
- self.ax2.set_zorder(0)
- self.ax2.set_autoscaley_on(True)
- self.ax.set_zorder(1)
- # 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._overlays = set()
- self._background = None
-
- self._colormaps = {}
-
- self._graphCursor = tuple()
-
- self._enableAxis('right', False)
- self._isXAxisTimeSeries = False
-
- # Add methods
-
- def addCurve(self, x, y, legend,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror, z, selectable,
- fill, alpha, symbolsize):
- for parameter in (x, y, legend, color, symbol, linewidth, linestyle,
- yaxis, z, selectable, 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.float) / 255.
-
- if yaxis == "right":
- axes = self.ax2
- self._enableAxis("right", True)
- else:
- axes = self.ax
-
- picker = 3 if selectable else None
-
- 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
-
- # On Debian 7 at least, Nx1 array yerr does not seems supported
- if (isinstance(yerror, numpy.ndarray) and yerror.ndim == 2 and
- yerror.shape[1] == 1 and len(x) != 1):
- yerror = numpy.ravel(yerror)
-
- errorbars = axes.errorbar(x, y, label=legend,
- 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.float]:
- 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, label=legend,
- linestyle=linestyle,
- color=actualColor[0],
- linewidth=linewidth,
- picker=picker,
- marker=None)
- artists += list(curveList)
-
- scatter = axes.scatter(x, y,
- label=legend,
- color=actualColor,
- marker=symbol,
- picker=picker,
- s=symbolsize**2)
- artists.append(scatter)
-
- if fill:
- artists.append(axes.fill_between(
- x, FLOAT32_MINPOS, y, facecolor=actualColor[0], linestyle=''))
-
- else: # Curve
- curveList = axes.plot(x, y,
- label=legend,
- linestyle=linestyle,
- color=color,
- linewidth=linewidth,
- marker=symbol,
- picker=picker,
- markersize=symbolsize)
- artists += list(curveList)
-
- if fill:
- artists.append(
- axes.fill_between(x, FLOAT32_MINPOS, y, facecolor=color))
-
- for artist in artists:
- artist.set_zorder(z)
- if alpha < 1:
- artist.set_alpha(alpha)
-
- return Container(artists)
-
- def addImage(self, data, legend,
- origin, scale, z,
- selectable, draggable,
- 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, legend, origin, scale, z,
- selectable, draggable):
- 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]
-
- picker = (selectable or draggable)
-
- # 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 self._matplotlibVersion < _parse_version('1.2.0'):
- if (len(data.shape) == 2 and 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
- data = colors[data.reshape(-1)].reshape(
- data.shape + (4,))
- else:
- _logger.warning(
- 'matplotlib %s does not support transparent '
- 'colormap.', matplotlib.__version__)
-
- if ((height * width) > 5.0e5 and
- origin == (0., 0.) and scale == (1., 1.)):
- imageClass = ModestImage
- else:
- imageClass = AxesImage
-
- # All image are shown as RGBA image
- image = imageClass(self.ax,
- label="__IMAGE__" + legend,
- interpolation='nearest',
- picker=picker,
- zorder=z,
- origin='lower')
-
- 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 self._matplotlibVersion < _parse_version('2.1'):
- # matplotlib 1.4.2 do not support float128
- dtype = data.dtype
- if dtype.kind == "f" and dtype.itemsize >= 16:
- _logger.warning("Your matplotlib version do not support "
- "float128. Data converted to float64.")
- data = data.astype(numpy.float64)
-
- if data.ndim == 2: # Data image, convert to RGBA image
- data = colormap.applyToData(data)
-
- image.set_data(data)
-
- self.ax.add_artist(image)
-
- return image
-
- def addItem(self, x, y, legend, shape, color, fill, overlay, z):
- xView = numpy.array(x, copy=False)
- yView = numpy.array(y, copy=False)
-
- if shape == "line":
- item = self.ax.plot(x, y, label=legend, color=color,
- linestyle='-', marker=None)[0]
-
- elif shape == "hline":
- if hasattr(y, "__len__"):
- y = y[-1]
- item = self.ax.axhline(y, label=legend, color=color)
-
- elif shape == "vline":
- if hasattr(x, "__len__"):
- x = x[-1]
- item = self.ax.axvline(x, label=legend, color=color)
-
- 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)
- if fill:
- item.set_hatch('.')
-
- 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,
- label=legend,
- color=color)
- if fill and shape == 'polygon':
- item.set_hatch('/')
-
- self.ax.add_patch(item)
-
- else:
- raise NotImplementedError("Unsupported item shape %s" % shape)
-
- item.set_zorder(z)
-
- if overlay:
- item.set_animated(True)
- self._overlays.add(item)
-
- return item
-
- def addMarker(self, x, y, legend, text, color,
- selectable, draggable,
- symbol, linestyle, linewidth, constraint):
- legend = "__MARKER__" + legend
-
- textArtist = None
-
- xmin, xmax = self.getGraphXLimits()
- ymin, ymax = self.getGraphYLimits(axis='left')
-
- if x is not None and y is not None:
- line = self.ax.plot(x, y, label=legend,
- linestyle=" ",
- color=color,
- marker=symbol,
- markersize=10.)[-1]
-
- if text is not None:
- if symbol is None:
- valign = 'baseline'
- else:
- valign = 'top'
- text = " " + text
-
- textArtist = self.ax.text(x, y, text,
- color=color,
- horizontalalignment='left',
- verticalalignment=valign)
-
- elif x is not None:
- line = self.ax.axvline(x,
- label=legend,
- color=color,
- linewidth=linewidth,
- linestyle=linestyle)
- if text is not None:
- # Y position will be updated in updateMarkerText call
- textArtist = self.ax.text(x, 1., " " + text,
- color=color,
- horizontalalignment='left',
- verticalalignment='top')
-
- elif y is not None:
- line = self.ax.axhline(y,
- label=legend,
- color=color,
- linewidth=linewidth,
- linestyle=linestyle)
-
- if text is not None:
- # X position will be updated in updateMarkerText call
- textArtist = self.ax.text(1., y, " " + text,
- color=color,
- horizontalalignment='right',
- verticalalignment='top')
-
- else:
- raise RuntimeError('A marker must at least have one coordinate')
-
- if selectable or draggable:
- line.set_picker(5)
-
- # All markers are overlays
- line.set_animated(True)
- if textArtist is not None:
- textArtist.set_animated(True)
-
- artists = [line] if textArtist is None else [line, textArtist]
- container = _MarkerContainer(artists, x, y)
- container.updateMarkerText(xmin, xmax, ymin, ymax)
- self._overlays.add(container)
-
- return container
-
- def _updateMarkers(self):
- xmin, xmax = self.ax.get_xbound()
- ymin, ymax = self.ax.get_ybound()
- for item in self._overlays:
- if isinstance(item, _MarkerContainer):
- item.updateMarkerText(xmin, xmax, ymin, ymax)
-
- # Remove methods
-
- def remove(self, item):
- # Warning: It also needs to remove extra stuff if added as for markers
- self._overlays.discard(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 is not None:
- 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 saveGraph(self, fileName, fileFormat, dpi):
- # 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.replot() # makes sure we get the right limits
- 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.replot() # makes sure we get the right limits
-
- 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()
-
- 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 _mplQtYAxisCoordConversion(self, y):
- """Qt origin (top) to/from matplotlib origin (bottom) conversion.
-
- :rtype: float
- """
- height = self.fig.get_window_extent().height
- return height - y
-
- def dataToPixel(self, x, y, axis):
- ax = self.ax2 if axis == "right" else self.ax
-
- pixels = ax.transData.transform_point((x, y))
- xPixel, yPixel = pixels.T
-
- # Convert from matplotlib origin (bottom) to Qt origin (top)
- yPixel = self._mplQtYAxisCoordConversion(yPixel)
-
- return xPixel, yPixel
-
- def pixelToData(self, x, y, axis, check):
- ax = self.ax2 if axis == "right" else self.ax
-
- # Convert from Qt origin (top) to matplotlib origin (bottom)
- y = self._mplQtYAxisCoordConversion(y)
-
- inv = ax.transData.inverted()
- x, y = inv.transform_point((x, y))
-
- if check:
- xmin, xmax = self.getGraphXLimits()
- ymin, ymax = self.getGraphYLimits(axis=axis)
-
- if x > xmax or x < xmin or y > ymax or y < ymin:
- return None # (x, y) is out of plot area
-
- return x, y
-
- def getPlotBoundsInPixels(self):
- bbox = self.ax.get_window_extent()
- # Warning this is not returning int...
- return (bbox.xmin,
- self._mplQtYAxisCoordConversion(bbox.ymax),
- bbox.width,
- bbox.height)
-
- def setAxesDisplayed(self, displayed):
- """Display or not the axes.
-
- :param bool displayed: If `True` axes are displayed. If `False` axes
- are not anymore visible and the margin used for them is removed.
- """
- BackendBase.BackendBase.setAxesDisplayed(self, displayed)
- if displayed:
- # show axes and viewbox rect
- self.ax.set_axis_on()
- self.ax2.set_axis_on()
- # set the default margins
- self.ax.set_position([.15, .15, .75, .75])
- self.ax2.set_position([.15, .15, .75, .75])
- else:
- # hide axes and viewbox rect
- self.ax.set_axis_off()
- self.ax2.set_axis_off()
- # remove external margins
- self.ax.set_position([0, 0, 1, 1])
- self.ax2.set_position([0, 0, 1, 1])
- self._plot._setDirtyPlot()
-
-
-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 contextMenuEvent(self, event):
- """Override QWidget.contextMenuEvent to implement the context menu"""
- # Makes sure it is overridden (issue with PySide)
- BackendBase.BackendBase.contextMenuEvent(self, event)
-
- def postRedisplay(self):
- self._sigPostRedisplay.emit()
-
- # Mouse event forwarding
-
- _MPL_TO_PLOT_BUTTONS = {1: 'left', 2: 'middle', 3: 'right'}
-
- def _onMousePress(self, event):
- self._plot.onMousePress(
- event.x, self._mplQtYAxisCoordConversion(event.y),
- self._MPL_TO_PLOT_BUTTONS[event.button])
-
- def _onMouseMove(self, event):
- if self._graphCursor:
- lineh, linev = self._graphCursor
- if event.inaxes != self.ax and lineh.get_visible():
- lineh.set_visible(False)
- linev.set_visible(False)
- self._plot._setDirtyPlot(overlayOnly=True)
- else:
- linev.set_visible(True)
- linev.set_xdata((event.xdata, event.xdata))
- lineh.set_visible(True)
- lineh.set_ydata((event.ydata, event.ydata))
- self._plot._setDirtyPlot(overlayOnly=True)
- # onMouseMove must trigger replot if dirty flag is raised
-
- self._plot.onMouseMove(
- event.x, self._mplQtYAxisCoordConversion(event.y))
-
- def _onMouseRelease(self, event):
- self._plot.onMouseRelease(
- event.x, self._mplQtYAxisCoordConversion(event.y),
- self._MPL_TO_PLOT_BUTTONS[event.button])
-
- def _onMouseWheel(self, event):
- self._plot.onMouseWheel(
- event.x, self._mplQtYAxisCoordConversion(event.y), event.step)
-
- def leaveEvent(self, event):
- """QWidget event handler"""
- self._plot.onMouseLeaveWidget()
-
- # picking
-
- def _onPick(self, event):
- # TODO not very nice and fragile, find a better way?
- # Make a selection according to kind
- if self._picked is None:
- _logger.error('Internal picking error')
- return
-
- label = event.artist.get_label()
- if label.startswith('__MARKER__'):
- self._picked.append({'kind': 'marker', 'legend': label[10:]})
-
- elif label.startswith('__IMAGE__'):
- self._picked.append({'kind': 'image', 'legend': label[9:]})
-
- else: # it's a curve, item have no picker for now
- if not isinstance(event.artist, (PathCollection, Line2D)):
- _logger.info('Unsupported artist, ignored')
- return
-
- self._picked.append({'kind': 'curve', 'legend': label,
- 'indices': event.ind})
-
- def pickItems(self, x, y, kinds):
- self._picked = []
-
- # Weird way to do an explicit picking: Simulate a button press event
- mouseEvent = MouseEvent('button_press_event',
- self, x, self._mplQtYAxisCoordConversion(y))
- cid = self.mpl_connect('pick_event', self._onPick)
- self.fig.pick(mouseEvent)
- self.mpl_disconnect(cid)
-
- picked = [p for p in self._picked if p['kind'] in kinds]
- self._picked = None
-
- return picked
-
- # 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._overlays or self._graphCursor:
- # This is needed with matplotlib 1.5.x and 2.0.x
- self._plot._setDirtyPlot()
-
- def _drawOverlays(self):
- """Draw overlays if any."""
- if self._overlays or self._graphCursor:
- # There is some overlays or crosshair
-
- # This assume that items are only on left/bottom Axes
- for item in self._overlays:
- self.ax.draw_artist(item)
-
- for item in self._graphCursor:
- self.ax.draw_artist(item)
-
- 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.
- """
- # 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._overlays or self._graphCursor:
- # 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._overlays or self._graphCursor:
- 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 100644
index 9e2cb73..0000000
--- a/silx/gui/plot/backends/BackendOpenGL.py
+++ /dev/null
@@ -1,1725 +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.
-#
-# ############################################################################*/
-"""OpenGL Plot backend."""
-
-from __future__ import division
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "01/08/2018"
-
-from collections import OrderedDict, namedtuple
-from ctypes import c_void_p
-import logging
-
-import numpy
-
-from .._utils import FLOAT32_MINPOS
-from . import BackendBase
-from ... import colors
-from ... import qt
-
-from ..._glutils import gl
-from ... import _glutils as glu
-from .glutils import (
- GLPlotCurve2D, GLPlotColormap, GLPlotRGBAImage, GLPlotFrame2D,
- mat4Ortho, mat4Identity,
- LEFT, RIGHT, BOTTOM, TOP,
- Text2D, Shape2D)
-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
-
-# Bounds ######################################################################
-
-class Range(namedtuple('Range', ('min_', 'max_'))):
- """Describes a 1D range"""
-
- @property
- def range_(self):
- return self.max_ - self.min_
-
- @property
- def center(self):
- return 0.5 * (self.min_ + self.max_)
-
-
-class Bounds(object):
- """Describes plot bounds with 2 y axis"""
-
- def __init__(self, xMin, xMax, yMin, yMax, y2Min, y2Max):
- self._xAxis = Range(xMin, xMax)
- self._yAxis = Range(yMin, yMax)
- self._y2Axis = Range(y2Min, y2Max)
-
- def __repr__(self):
- return "x: %s, y: %s, y2: %s" % (repr(self._xAxis),
- repr(self._yAxis),
- repr(self._y2Axis))
-
- @property
- def xAxis(self):
- return self._xAxis
-
- @property
- def yAxis(self):
- return self._yAxis
-
- @property
- def y2Axis(self):
- return self._y2Axis
-
-
-# Content #####################################################################
-
-class PlotDataContent(object):
- """Manage plot data content: images and curves.
-
- This class is only meant to work with _OpenGLPlotCanvas.
- """
-
- _PRIMITIVE_TYPES = 'curve', 'image'
-
- def __init__(self):
- self._primitives = OrderedDict() # For images and curves
-
- def add(self, primitive):
- """Add a curve or image to the content dictionary.
-
- This function generates the key in the dict from the primitive.
-
- :param primitive: The primitive to add.
- :type primitive: Instance of GLPlotCurve2D, GLPlotColormap,
- GLPlotRGBAImage.
- """
- if isinstance(primitive, GLPlotCurve2D):
- primitiveType = 'curve'
- elif isinstance(primitive, (GLPlotColormap, GLPlotRGBAImage)):
- primitiveType = 'image'
- else:
- raise RuntimeError('Unsupported object type: %s', primitive)
-
- key = primitiveType, primitive.info['legend']
- self._primitives[key] = primitive
-
- def get(self, primitiveType, legend):
- """Get the corresponding primitive of given type with given legend.
-
- :param str primitiveType: Type of primitive ('curve' or 'image').
- :param str legend: The legend of the primitive to retrieve.
- :return: The corresponding curve or None if no such curve.
- """
- assert primitiveType in self._PRIMITIVE_TYPES
- return self._primitives.get((primitiveType, legend))
-
- def pop(self, primitiveType, key):
- """Pop the corresponding curve or return None if no such curve.
-
- :param str primitiveType:
- :param str key:
- :return:
- """
- assert primitiveType in self._PRIMITIVE_TYPES
- return self._primitives.pop((primitiveType, key), None)
-
- def zOrderedPrimitives(self, reverse=False):
- """List of primitives sorted according to their z order.
-
- It is a stable sort (as sorted):
- Original order is preserved when key is the same.
-
- :param bool reverse: Ascending (True, default) or descending (False).
- """
- return sorted(self._primitives.values(),
- key=lambda primitive: primitive.info['zOrder'],
- reverse=reverse)
-
- def primitives(self):
- """Iterator over all primitives."""
- return self._primitives.values()
-
- def primitiveKeys(self, primitiveType):
- """Iterator over primitives of a specific type."""
- assert primitiveType in self._PRIMITIVE_TYPES
- for type_, key in self._primitives.keys():
- if type_ == primitiveType:
- yield key
-
- def getBounds(self, xPositive=False, yPositive=False):
- """Bounds of the data.
-
- Can return strictly positive bounds (for log scale).
- In this case, curves are clipped to their smaller positive value
- and images with negative min are ignored.
-
- :param bool xPositive: True to get strictly positive range.
- :param bool yPositive: True to get strictly positive range.
- :return: The range of data for x, y and y2, or default (1., 100.)
- if no range found for one dimension.
- :rtype: Bounds
- """
- xMin, yMin, y2Min = float('inf'), float('inf'), float('inf')
- xMax = 0. if xPositive else -float('inf')
- if yPositive:
- yMax, y2Max = 0., 0.
- else:
- yMax, y2Max = -float('inf'), -float('inf')
-
- for item in self._primitives.values():
- # To support curve <= 0. and log and bypass images:
- # If positive only, uses x|yMinPos if available
- # and bypass other data with negative min bounds
- if xPositive:
- itemXMin = getattr(item, 'xMinPos', item.xMin)
- if itemXMin is None or itemXMin < FLOAT32_MINPOS:
- continue
- else:
- itemXMin = item.xMin
-
- if yPositive:
- itemYMin = getattr(item, 'yMinPos', item.yMin)
- if itemYMin is None or itemYMin < FLOAT32_MINPOS:
- continue
- else:
- itemYMin = item.yMin
-
- if itemXMin < xMin:
- xMin = itemXMin
- if item.xMax > xMax:
- xMax = item.xMax
-
- if item.info.get('yAxis') == 'right':
- if itemYMin < y2Min:
- y2Min = itemYMin
- if item.yMax > y2Max:
- y2Max = item.yMax
- else:
- if itemYMin < yMin:
- yMin = itemYMin
- if item.yMax > yMax:
- yMax = item.yMax
-
- # One of the limit has not been updated, return default range
- if xMin >= xMax:
- xMin, xMax = 1., 100.
- if yMin >= yMax:
- yMin, yMax = 1., 100.
- if y2Min >= y2Max:
- y2Min, y2Max = 1., 100.
-
- return Bounds(xMin, xMax, yMin, yMax, y2Min, y2Max)
-
-
-# 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 ###############################################################
-
-_current_context = None
-
-
-def _getContext():
- assert _current_context is not None
- return _current_context
-
-
-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.matScreenProj = mat4Identity()
-
- self._progBase = glu.Program(
- _baseVertShd, _baseFragShd, attrib0='position')
- self._progTex = glu.Program(
- _texVertShd, _texFragShd, attrib0='position')
- self._plotFBOs = {}
-
- self._keepDataAspectRatio = False
-
- self._crosshairCursor = None
- self._mousePosInPixels = None
-
- self._markers = OrderedDict()
- self._items = OrderedDict()
- self._plotContent = PlotDataContent() # For images and curves
- self._glGarbageCollector = []
-
- self._plotFrame = GLPlotFrame2D(
- margins={'left': 100, 'right': 50, 'top': 50, 'bottom': 50})
-
- # 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 contextMenuEvent(self, event):
- """Override QWidget.contextMenuEvent to implement the context menu"""
- # Makes sure it is overridden (issue with PySide)
- BackendBase.BackendBase.contextMenuEvent(self, event)
-
- def sizeHint(self):
- return qt.QSize(8 * 80, 6 * 80) # Mimic MatplotlibBackend
-
- def mousePressEvent(self, event):
- xPixel = event.x() * self.getDevicePixelRatio()
- yPixel = event.y() * self.getDevicePixelRatio()
- btn = self._MOUSE_BTNS[event.button()]
- self._plot.onMousePress(xPixel, yPixel, btn)
- event.accept()
-
- def mouseMoveEvent(self, event):
- xPixel = event.x() * self.getDevicePixelRatio()
- yPixel = event.y() * self.getDevicePixelRatio()
-
- # Handle crosshair
- inXPixel, inYPixel = self._mouseInPlotArea(xPixel, yPixel)
- isCursorInPlot = inXPixel == xPixel and inYPixel == yPixel
-
- previousMousePosInPixels = self._mousePosInPixels
- self._mousePosInPixels = (xPixel, yPixel) if isCursorInPlot else None
- 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(xPixel, yPixel)
- event.accept()
-
- def mouseReleaseEvent(self, event):
- xPixel = event.x() * self.getDevicePixelRatio()
- yPixel = event.y() * self.getDevicePixelRatio()
-
- btn = self._MOUSE_BTNS[event.button()]
- self._plot.onMouseRelease(xPixel, yPixel, btn)
- event.accept()
-
- def wheelEvent(self, event):
- xPixel = event.x() * self.getDevicePixelRatio()
- yPixel = event.y() * self.getDevicePixelRatio()
-
- if hasattr(event, 'angleDelta'): # Qt 5
- delta = event.angleDelta().y()
- else: # Qt 4 support
- delta = event.delta()
- angleInDegrees = delta / 8.
- self._plot.onMouseWheel(xPixel, yPixel, angleInDegrees)
- event.accept()
-
- def leaveEvent(self, _):
- self._plot.onMouseLeaveWidget()
-
- # OpenGLWidget API
-
- def initializeGL(self):
- gl.testGL()
-
- gl.glClearColor(1., 1., 1., 1.)
- 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._renderMarkersGL()
- self._renderOverlayGL()
-
- def _paintFBOGL(self):
- context = glu.getGLContext()
- plotFBOTex = self._plotFBOs.get(context)
- if (self._plot._getDirtyPlot() or self._plotFrame.isDirty or
- plotFBOTex is None):
- self._plotVertices = numpy.array(((-1., -1., 0., 0.),
- (1., -1., 1., 0.),
- (-1., 1., 0., 1.),
- (1., 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.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,
- mat4Identity().astype(numpy.float32))
-
- stride = self._plotVertices.shape[-1] * self._plotVertices.itemsize
- gl.glEnableVertexAttribArray(self._progTex.attributes['position'])
- gl.glVertexAttribPointer(self._progTex.attributes['position'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, self._plotVertices)
-
- texCoordsPtr = c_void_p(self._plotVertices.ctypes.data +
- 2 * self._plotVertices.itemsize) # Better way?
- gl.glEnableVertexAttribArray(self._progTex.attributes['texCoords'])
- gl.glVertexAttribPointer(self._progTex.attributes['texCoords'],
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- stride, texCoordsPtr)
-
- with plotFBOTex.texture:
- gl.glDrawArrays(gl.GL_TRIANGLE_STRIP, 0, len(self._plotVertices))
-
- self._renderMarkersGL()
- self._renderOverlayGL()
-
- def paintGL(self):
- global _current_context
- _current_context = self.context()
-
- glu.setGLContextGetter(_getContext)
-
- # Release OpenGL resources
- for item in self._glGarbageCollector:
- item.discard()
- self._glGarbageCollector = []
-
- gl.glClear(gl.GL_COLOR_BUFFER_BIT | gl.GL_STENCIL_BUFFER_BIT)
-
- # Check if window is large enough
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
- if plotWidth <= 2 or plotHeight <= 2:
- return
-
- # self._paintDirectGL()
- self._paintFBOGL()
-
- glu.setGLContextGetter()
- _current_context = None
-
- def _nonOrthoAxesLineMarkerPrimitives(self, marker, pixelOffset):
- """Generates the vertices and label for a line marker.
-
- :param dict marker: Description of a line marker
- :param int pixelOffset: Offset of text from borders in pixels
- :return: Line vertices and Text label or None
- :rtype: 2-tuple (2x2 numpy.array of float, Text2D)
- """
- label, vertices = None, None
-
- xCoord, yCoord = marker['x'], marker['y']
- assert xCoord is None or yCoord is None # Specific to line markers
-
- # Get plot corners in data coords
- plotLeft, plotTop, plotWidth, plotHeight = self.getPlotBoundsInPixels()
-
- corners = [(plotLeft, plotTop),
- (plotLeft, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop)]
- corners = numpy.array([self.pixelToData(x, y, axis='left', check=False)
- for (x, y) in corners])
-
- borders = {
- 'right': (corners[3], corners[2]),
- 'top': (corners[0], corners[3]),
- 'bottom': (corners[2], corners[1]),
- 'left': (corners[1], corners[0])
- }
-
- textLayouts = { # align, valign, offsets
- 'right': (RIGHT, BOTTOM, (-1., -1.)),
- 'top': (LEFT, TOP, (1., 1.)),
- 'bottom': (LEFT, BOTTOM, (1., -1.)),
- 'left': (LEFT, BOTTOM, (1., -1.))
- }
-
- if xCoord is None: # Horizontal line in data space
- if marker['text'] is not None:
- # Find intersection of hline with borders in data
- # Order is important as it stops at first intersection
- for border_name in ('right', 'top', 'bottom', 'left'):
- (x0, y0), (x1, y1) = borders[border_name]
-
- if min(y0, y1) <= yCoord < max(y0, y1):
- xIntersect = (yCoord - y0) * (x1 - x0) / (y1 - y0) + x0
-
- # Add text label
- pixelPos = self.dataToPixel(
- xIntersect, yCoord, axis='left', check=False)
-
- align, valign, offsets = textLayouts[border_name]
-
- x = pixelPos[0] + offsets[0] * pixelOffset
- y = pixelPos[1] + offsets[1] * pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
- bgColor=(1., 1., 1., 0.5),
- align=align, valign=valign)
- break # Stop at first intersection
-
- xMin, xMax = corners[:, 0].min(), corners[:, 0].max()
- vertices = numpy.array(
- ((xMin, yCoord), (xMax, yCoord)), dtype=numpy.float32)
-
- else: # yCoord is None: vertical line in data space
- if marker['text'] is not None:
- # Find intersection of hline with borders in data
- # Order is important as it stops at first intersection
- for border_name in ('top', 'bottom', 'right', 'left'):
- (x0, y0), (x1, y1) = borders[border_name]
- if min(x0, x1) <= xCoord < max(x0, x1):
- yIntersect = (xCoord - x0) * (y1 - y0) / (x1 - x0) + y0
-
- # Add text label
- pixelPos = self.dataToPixel(
- xCoord, yIntersect, axis='left', check=False)
-
- align, valign, offsets = textLayouts[border_name]
-
- x = pixelPos[0] + offsets[0] * pixelOffset
- y = pixelPos[1] + offsets[1] * pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
- bgColor=(1., 1., 1., 0.5),
- align=align, valign=valign)
- break # Stop at first intersection
-
- yMin, yMax = corners[:, 1].min(), corners[:, 1].max()
- vertices = numpy.array(
- ((xCoord, yMin), (xCoord, yMax)), dtype=numpy.float32)
-
- return vertices, label
-
- def _renderMarkersGL(self):
- if len(self._markers) == 0:
- return
-
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
-
- # Render in plot area
- gl.glScissor(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
- gl.glEnable(gl.GL_SCISSOR_TEST)
-
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
-
- # Prepare vertical and horizontal markers rendering
- 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.glUniform1i(self._progBase.uniforms['hatchStep'], 0)
- gl.glUniform1f(self._progBase.uniforms['tickLen'], 0.)
- posAttrib = self._progBase.attributes['position']
-
- labels = []
- pixelOffset = 3
-
- for marker in self._markers.values():
- xCoord, yCoord = marker['x'], marker['y']
-
- if ((self._plotFrame.xAxis.isLog and
- xCoord is not None and
- xCoord <= 0) or
- (self._plotFrame.yAxis.isLog 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 not self.isDefaultBaseVectors(): # Non-orthogonal axes
- vertices, label = self._nonOrthoAxesLineMarkerPrimitives(
- marker, pixelOffset)
- if label is not None:
- labels.append(label)
-
- else: # Orthogonal axes
- pixelPos = self.dataToPixel(
- xCoord, yCoord, axis='left', check=False)
-
- if xCoord is None: # Horizontal line in data space
- if marker['text'] is not None:
- x = self._plotFrame.size[0] - \
- self._plotFrame.margins.right - pixelOffset
- y = pixelPos[1] - pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
- bgColor=(1., 1., 1., 0.5),
- align=RIGHT, valign=BOTTOM)
- labels.append(label)
-
- width = self._plotFrame.size[0]
- vertices = numpy.array(((0, pixelPos[1]),
- (width, pixelPos[1])),
- dtype=numpy.float32)
-
- else: # yCoord is None: vertical line in data space
- if marker['text'] is not None:
- x = pixelPos[0] + pixelOffset
- y = self._plotFrame.margins.top + pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
- bgColor=(1., 1., 1., 0.5),
- align=LEFT, valign=TOP)
- labels.append(label)
-
- height = self._plotFrame.size[1]
- vertices = numpy.array(((pixelPos[0], 0),
- (pixelPos[0], height)),
- dtype=numpy.float32)
-
- self._progBase.use()
- gl.glUniform4f(self._progBase.uniforms['color'],
- *marker['color'])
-
- gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, vertices)
- gl.glLineWidth(1)
- gl.glDrawArrays(gl.GL_LINES, 0, len(vertices))
-
- else:
- pixelPos = self.dataToPixel(
- xCoord, yCoord, axis='left', check=True)
- if pixelPos is None:
- # Do not render markers outside visible plot area
- continue
-
- if marker['text'] is not None:
- x = pixelPos[0] + pixelOffset
- y = pixelPos[1] + pixelOffset
- label = Text2D(marker['text'], x, y,
- color=marker['color'],
- bgColor=(1., 1., 1., 0.5),
- align=LEFT, valign=TOP)
- labels.append(label)
-
- # For now simple implementation: using a curve for each marker
- # Should pack all markers to a single set of points
- markerCurve = GLPlotCurve2D(
- numpy.array((pixelPos[0],), dtype=numpy.float64),
- numpy.array((pixelPos[1],), dtype=numpy.float64),
- marker=marker['symbol'],
- markerColor=marker['color'],
- markerSize=11)
- markerCurve.render(self.matScreenProj, False, False)
- markerCurve.discard()
-
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
-
- # Render marker labels
- for label in labels:
- label.render(self.matScreenProj)
-
- gl.glDisable(gl.GL_SCISSOR_TEST)
-
- def _renderOverlayGL(self):
- # Render crosshair cursor
- if self._crosshairCursor is not None:
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
-
- # Scissor to plot area
- gl.glScissor(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
- gl.glEnable(gl.GL_SCISSOR_TEST)
-
- 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']
-
- # Render crosshair cursor in screen frame but with scissor
- if (self._crosshairCursor is not None and
- self._mousePosInPixels is not None):
- 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):
- plotWidth, plotHeight = self.getPlotBoundsInPixels()[2:]
-
- self._plotFrame.renderGrid()
-
- gl.glScissor(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
- gl.glEnable(gl.GL_SCISSOR_TEST)
-
- # Matrix
- trBounds = self._plotFrame.transformedDataRanges
- if trBounds.x[0] == trBounds.x[1] or \
- trBounds.y[0] == trBounds.y[1]:
- return
-
- isXLog = self._plotFrame.xAxis.isLog
- isYLog = self._plotFrame.yAxis.isLog
-
- gl.glViewport(self._plotFrame.margins.left,
- self._plotFrame.margins.bottom,
- plotWidth, plotHeight)
-
- # Render images and curves
- # sorted is stable: original order is preserved when key is the same
- for item in self._plotContent.zOrderedPrimitives():
- if item.info.get('yAxis') == 'right':
- item.render(self._plotFrame.transformedDataY2ProjMat,
- isXLog, isYLog)
- else:
- item.render(self._plotFrame.transformedDataProjMat,
- isXLog, isYLog)
-
- # Render Items
- gl.glViewport(0, 0, self._plotFrame.size[0], self._plotFrame.size[1])
-
- 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.)
-
- for item in self._items.values():
- 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
-
- closed = item['shape'] != 'polylines'
- points = [self.dataToPixel(x, y, axis='left', check=False)
- for (x, y) in zip(item['x'], item['y'])]
- shape2D = Shape2D(points,
- fill=item['fill'],
- fillColor=item['color'],
- stroke=True,
- strokeColor=item['color'],
- strokeClosed=closed)
-
- posAttrib = self._progBase.attributes['position']
- colorUnif = self._progBase.uniforms['color']
- hatchStepUnif = self._progBase.uniforms['hatchStep']
- shape2D.render(posAttrib, colorUnif, hatchStepUnif)
-
- 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 = 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, legend,
- color, symbol, linewidth, linestyle,
- yaxis,
- xerror, yerror, z, selectable,
- fill, alpha, symbolsize):
- for parameter in (x, y, legend, color, symbol, linewidth, linestyle,
- yaxis, z, selectable, 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
- 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
- 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
-
- behaviors = set()
- if selectable:
- behaviors.add('selectable')
-
- curve = GLPlotCurve2D(x, y, colorArray,
- xError=xerror,
- yError=yerror,
- lineStyle=linestyle,
- lineColor=color,
- lineWidth=linewidth,
- marker=symbol,
- markerColor=color,
- markerSize=symbolsize,
- fillColor=color if fill else None,
- isYLog=isYLog)
- curve.info = {
- 'legend': legend,
- 'zOrder': z,
- 'behaviors': behaviors,
- 'yAxis': 'left' if yaxis is None else yaxis,
- }
-
- if yaxis == "right":
- self._plotFrame.isY2Axis = True
-
- self._plotContent.add(curve)
-
- return legend, 'curve'
-
- def addImage(self, data, legend,
- origin, scale, z,
- selectable, draggable,
- colormap, alpha):
- for parameter in (data, legend, origin, scale, z,
- selectable, draggable):
- assert parameter is not None
-
- behaviors = set()
- if selectable:
- behaviors.add('selectable')
- if draggable:
- behaviors.add('draggable')
-
- if data.ndim == 2:
- # Ensure array is contiguous and eventually convert its type
- if data.dtype in (numpy.float32, numpy.uint8, numpy.uint16):
- 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')
-
- colormapIsLog = colormap.getNormalization() == 'log'
- cmapRange = colormap.getColormapRange(data=data)
- colormapLut = colormap.getNColors(nbColors=256)
-
- image = GLPlotColormap(data,
- origin,
- scale,
- colormapLut,
- colormapIsLog,
- cmapRange,
- alpha)
- image.info = {
- 'legend': legend,
- 'zOrder': z,
- 'behaviors': behaviors
- }
- self._plotContent.add(image)
-
- 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 numpy.issubdtype(data.dtype, numpy.integer):
- data = numpy.array(data, dtype=numpy.uint8, copy=False)
- else:
- raise ValueError('Unsupported data type')
-
- image = GLPlotRGBAImage(data, origin, scale, alpha)
-
- image.info = {
- 'legend': legend,
- 'zOrder': z,
- 'behaviors': behaviors
- }
-
- 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')
-
- self._plotContent.add(image)
-
- else:
- raise RuntimeError("Unsupported data shape {0}".format(data.shape))
-
- return legend, 'image'
-
- def addItem(self, x, y, legend, shape, color, fill, overlay, z):
- # TODO handle overlay
- 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))
-
- # 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')
-
- # Ignore fill for polylines to mimic matplotlib
- fill = fill if shape != 'polylines' else False
-
- self._items[legend] = {
- 'shape': shape,
- 'color': colors.rgba(color),
- 'fill': 'hatch' if fill else None,
- 'x': x,
- 'y': y
- }
-
- return legend, 'item'
-
- def addMarker(self, x, y, legend, text, color,
- selectable, draggable,
- symbol, linestyle, linewidth, constraint):
-
- if symbol is None:
- symbol = '+'
-
- if linestyle != '-' or linewidth != 1:
- _logger.warning(
- 'OpenGL backend does not support marker line style and width.')
-
- behaviors = set()
- if selectable:
- behaviors.add('selectable')
- if draggable:
- behaviors.add('draggable')
-
- # Apply constraint to provided position
- isConstraint = (draggable and constraint is not None and
- x is not None and y is not None)
- if isConstraint:
- x, y = constraint(x, y)
-
- self._markers[legend] = {
- 'x': x,
- 'y': y,
- 'legend': legend,
- 'text': text,
- 'color': colors.rgba(color),
- 'behaviors': behaviors,
- 'constraint': constraint if isConstraint else None,
- 'symbol': symbol,
- }
-
- return legend, 'marker'
-
- # Remove methods
-
- def remove(self, item):
- legend, kind = item
-
- if kind == 'curve':
- curve = self._plotContent.pop('curve', legend)
- if curve is not None:
- # Check if some curves remains on the right Y axis
- y2AxisItems = (item for item in self._plotContent.primitives()
- if item.info.get('yAxis', 'left') == 'right')
- self._plotFrame.isY2Axis = next(y2AxisItems, None) is not None
-
- self._glGarbageCollector.append(curve)
-
- elif kind == 'image':
- image = self._plotContent.pop('image', legend)
- if image is not None:
- self._glGarbageCollector.append(image)
-
- elif kind == 'marker':
- self._markers.pop(legend, False)
-
- elif kind == 'item':
- self._items.pop(legend, False)
-
- else:
- _logger.error('Unsupported kind: %s', str(kind))
-
- # 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 is not '-':
- _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):
- xPlot = numpy.clip(
- x, self._plotFrame.margins.left,
- self._plotFrame.size[0] - self._plotFrame.margins.right - 1)
- yPlot = numpy.clip(
- y, self._plotFrame.margins.top,
- self._plotFrame.size[1] - self._plotFrame.margins.bottom - 1)
- return xPlot, yPlot
-
- def pickItems(self, x, y, kinds):
- picked = []
-
- dataPos = self.pixelToData(x, y, axis='left', check=True)
- if dataPos is not None:
- # Pick markers
- if 'marker' in kinds:
- for marker in reversed(list(self._markers.values())):
- pixelPos = self.dataToPixel(
- marker['x'], marker['y'], axis='left', check=False)
- if pixelPos is None: # negative coord on a log axis
- continue
-
- if marker['x'] is None: # Horizontal line
- pt1 = self.pixelToData(
- x, y - self._PICK_OFFSET, axis='left', check=False)
- pt2 = self.pixelToData(
- x, y + self._PICK_OFFSET, axis='left', check=False)
- isPicked = (min(pt1[1], pt2[1]) <= marker['y'] <=
- max(pt1[1], pt2[1]))
-
- elif marker['y'] is None: # Vertical line
- pt1 = self.pixelToData(
- x - self._PICK_OFFSET, y, axis='left', check=False)
- pt2 = self.pixelToData(
- x + self._PICK_OFFSET, y, axis='left', check=False)
- isPicked = (min(pt1[0], pt2[0]) <= marker['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)
-
- if isPicked:
- picked.append(dict(kind='marker',
- legend=marker['legend']))
-
- # Pick image and curves
- if 'image' in kinds or 'curve' in kinds:
- for item in self._plotContent.zOrderedPrimitives(reverse=True):
- if ('image' in kinds and
- isinstance(item, (GLPlotColormap, GLPlotRGBAImage))):
- pickedPos = item.pick(*dataPos)
- if pickedPos is not None:
- picked.append(dict(kind='image',
- legend=item.info['legend']))
-
- elif 'curve' in kinds and isinstance(item, GLPlotCurve2D):
- offset = self._PICK_OFFSET
- if item.marker is not None:
- offset = max(item.markerSize / 2., offset)
- if item.lineStyle is not None:
- offset = max(item.lineWidth / 2., offset)
-
- yAxis = item.info['yAxis']
-
- inAreaPos = self._mouseInPlotArea(x - offset, y - offset)
- dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=yAxis, check=True)
- if dataPos is None:
- continue
- xPick0, yPick0 = dataPos
-
- inAreaPos = self._mouseInPlotArea(x + offset, y + offset)
- dataPos = self.pixelToData(inAreaPos[0], inAreaPos[1],
- axis=yAxis, check=True)
- if dataPos is None:
- continue
- 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 (yAxis == 'left' and self._plotFrame.yAxis.isLog) or (
- yAxis == 'right' and self._plotFrame.y2Axis.isLog):
- yPickMin = numpy.log10(yPickMin)
- yPickMax = numpy.log10(yPickMax)
-
- pickedIndices = item.pick(xPickMin, yPickMin,
- xPickMax, yPickMax)
- if pickedIndices:
- picked.append(dict(kind='curve',
- legend=item.info['legend'],
- indices=pickedIndices))
-
- return picked
-
- # 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
- if label:
- _logger.warning('Right axis label not implemented')
-
- # Non orthogonal axes
-
- def setBaseVectors(self, x=(1., 0.), y=(0., 1.)):
- """Set base vectors.
-
- Useful for non-orthogonal axes.
- If an axis is in log scale, skew is applied to log transformed values.
-
- Base vector does not work well with log axes, to investi
- """
- if x != (1., 0.) and y != (0., 1.):
- if self._plotFrame.xAxis.isLog:
- _logger.warning("setBaseVectors disables X axis logarithmic.")
- self.setXAxisLogarithmic(False)
- if self._plotFrame.yAxis.isLog:
- _logger.warning("setBaseVectors disables Y axis logarithmic.")
- self.setYAxisLogarithmic(False)
-
- if self.isKeepDataAspectRatio():
- _logger.warning("setBaseVectors disables keepDataAspectRatio.")
- self.keepDataAspectRatio(False)
-
- self._plotFrame.baseVectors = x, y
-
- def getBaseVectors(self):
- return self._plotFrame.baseVectors
-
- def isDefaultBaseVectors(self):
- return self._plotFrame.baseVectors == \
- self._plotFrame.DEFAULT_BASE_VECTORS
-
- # 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)
-
- if not self.isDefaultBaseVectors():
- # Update axes range with axes bounds in data coords
- plotLeft, plotTop, plotWidth, plotHeight = \
- self.getPlotBoundsInPixels()
-
- self._plotFrame.xAxis.dataRange = sorted([
- self.pixelToData(x, y, axis='left', check=False)[0]
- for (x, y) in ((plotLeft, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop + plotHeight))])
-
- self._plotFrame.yAxis.dataRange = sorted([
- self.pixelToData(x, y, axis='left', check=False)[1]
- for (x, y) in ((plotLeft, plotTop + plotHeight),
- (plotLeft, plotTop))])
-
- self._plotFrame.y2Axis.dataRange = sorted([
- self.pixelToData(x, y, axis='right', check=False)[1]
- for (x, y) in ((plotLeft + plotWidth, plotTop + plotHeight),
- (plotLeft + plotWidth, plotTop))])
-
- 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.getPlotBoundsInPixels()[2:]
- if plotWidth <= 2 or plotHeight <= 2:
- return
-
- if keepDim is None:
- dataBounds = self._plotContent.getBounds(
- self._plotFrame.xAxis.isLog, self._plotFrame.yAxis.isLog)
- if dataBounds.yAxis.range_ != 0.:
- dataRatio = dataBounds.xAxis.range_
- dataRatio /= float(dataBounds.yAxis.range_)
-
- 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")
-
- if flag and not self.isDefaultBaseVectors():
- _logger.warning(
- "setXAxisLogarithmic ignored because baseVectors are set")
- return
-
- 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")
-
- if flag and not self.isDefaultBaseVectors():
- _logger.warning(
- "setYAxisLogarithmic ignored because baseVectors are set")
- return
-
- 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")
- if flag and not self.isDefaultBaseVectors():
- _logger.warning(
- "keepDataAspectRatio ignored because baseVectors are set")
-
- 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, check=False):
- assert axis in ('left', 'right')
-
- if x is None or y is None:
- dataBounds = self._plotContent.getBounds(
- self._plotFrame.xAxis.isLog, self._plotFrame.yAxis.isLog)
-
- if x is None:
- x = dataBounds.xAxis.center
-
- if y is None:
- if axis == 'left':
- y = dataBounds.yAxis.center
- else:
- y = dataBounds.y2Axis.center
-
- result = self._plotFrame.dataToPixel(x, y, axis)
-
- if check and result is not None:
- xPixel, yPixel = result
- width, height = self._plotFrame.size
- if (xPixel < self._plotFrame.margins.left or
- xPixel > (width - self._plotFrame.margins.right) or
- yPixel < self._plotFrame.margins.top or
- yPixel > height - self._plotFrame.margins.bottom):
- return None # (x, y) is out of plot area
-
- return result
-
- def pixelToData(self, x, y, axis, check):
- assert axis in ("left", "right")
-
- if x is None:
- x = self._plotFrame.size[0] / 2.
- if y is None:
- y = self._plotFrame.size[1] / 2.
-
- if check and (x < self._plotFrame.margins.left or
- x > (self._plotFrame.size[0] -
- self._plotFrame.margins.right) or
- y < self._plotFrame.margins.top or
- y > (self._plotFrame.size[1] -
- self._plotFrame.margins.bottom)):
- return None # (x, y) is out of plot area
-
- return self._plotFrame.pixelToData(x, y, axis)
-
- def getPlotBoundsInPixels(self):
- return self._plotFrame.plotOrigin + self._plotFrame.plotSize
-
- def setAxesDisplayed(self, displayed):
- BackendBase.BackendBase.setAxesDisplayed(self, displayed)
- self._plotFrame.displayed = displayed
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 12b6bbe..0000000
--- a/silx/gui/plot/backends/glutils/GLPlotCurve.py
+++ /dev/null
@@ -1,1151 +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 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 warnings
-
-import numpy
-
-from silx.math.combo import min_max
-
-from ...._glutils import gl
-from ...._glutils import Program, vertexBuffer
-from .GLSupport import buildFillMaskIndices, mat4Identity, mat4Translate
-
-
-_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)) + 4 * len(slices)
- points = numpy.empty((nbPoints, 2), dtype=numpy.float32)
-
- offset = 0
- for start, end in slices:
- # Duplicate first point for connecting degenerated triangle
- points[offset:offset+2] = self.xData[start], self.baseline
-
- # 2nd point of the polygon is last point
- points[offset+2] = self.xData[end-1], self.baseline
-
- # Add all points from the data
- indices = start + buildFillMaskIndices(end - start)
-
- points[offset+3:offset+3+len(indices), 0] = self.xData[indices]
- points[offset+3:offset+3+len(indices), 1] = self.yData[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, matrix):
- """Perform rendering
-
- :param numpy.ndarray matrix: 4x4 transform matrix to use
- """
- 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(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._xFillVboData is not None:
- self._xFillVboData.vbo.discard()
-
- self._xFillVboData = None
- self._yFillVboData = None
-
-
-# line ########################################################################
-
-SOLID, DASHED, DASHDOT, DOTTED = '-', '--', '-.', ':'
-
-
-class _Lines2D(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;
-
- 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) {
- discard;
- }
- gl_FragColor = vColor;
- }
- """,
- attrib0='xPos')
-
- def __init__(self, xVboData=None, yVboData=None,
- colorVboData=None, distVboData=None,
- style=SOLID, color=(0., 0., 0., 1.),
- width=1, dashPeriod=20, drawMode=None,
- offset=(0., 0.)):
- self.xVboData = xVboData
- self.yVboData = yVboData
- self.distVboData = distVboData
- self.colorVboData = colorVboData
- self.useColorVboData = colorVboData is not None
-
- self.color = color
- 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, matrix):
- """Perform rendering
-
- :param numpy.ndarray matrix: 4x4 transform matrix to use
- """
- 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)
-
- if self.style == DOTTED:
- dash = (0.1 * self.dashPeriod,
- 0.6 * self.dashPeriod,
- 0.7 * self.dashPeriod,
- self.dashPeriod)
- elif self.style == DASHDOT:
- dash = (0.3 * self.dashPeriod,
- 0.5 * self.dashPeriod,
- 0.6 * self.dashPeriod,
- self.dashPeriod)
- else:
- dash = (0.5 * self.dashPeriod,
- self.dashPeriod,
- self.dashPeriod,
- self.dashPeriod)
-
- gl.glUniform4f(program.uniforms['dash'], *dash)
-
- distAttrib = program.attributes['distance']
- gl.glEnableVertexAttribArray(distAttrib)
- self.distVboData.setVertexAttrib(distAttrib)
-
- gl.glEnable(gl.GL_LINE_SMOOTH)
-
- matrix = numpy.dot(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)
- self.xVboData.setVertexAttrib(xPosAttrib)
-
- yPosAttrib = program.attributes['yPos']
- gl.glEnableVertexAttribArray(yPosAttrib)
- self.yVboData.setVertexAttrib(yPosAttrib)
-
- gl.glLineWidth(self.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
- """
- deltas = numpy.dstack((
- numpy.ediff1d(xData, to_begin=numpy.float32(0.)),
- numpy.ediff1d(yData, to_begin=numpy.float32(0.))))[0]
- return numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1)))
-
-
-# points ######################################################################
-
-DIAMOND, CIRCLE, SQUARE, PLUS, X_MARKER, POINT, PIXEL, ASTERISK = \
- 'd', 'o', 's', '+', 'x', '.', ',', '*'
-
-H_LINE, V_LINE = '_', '|'
-
-
-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)
- """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;
- }
- }
- """
- }
-
- _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, matrix):
- """Perform rendering
-
- :param numpy.ndarray matrix: 4x4 transform matrix to use
- """
- if self.marker is None:
- return
-
- program = self._getProgram(self.marker)
- program.use()
-
- matrix = numpy.dot(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
- 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:`_Lines2D` 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 = _Lines2D(
- 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, matrix):
- """Perform rendering
-
- :param numpy.ndarray matrix: 4x4 transform matrix to use
- """
- self.prepare()
-
- if self._attribs is not None:
- self._lines.render(matrix)
- self._xErrPoints.render(matrix)
- self._yErrPoints.render(matrix)
-
- def discard(self):
- """Release VBOs"""
- if self._attribs is not None:
- 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
-
-
-# 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(object):
- 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,
- isYLog=False):
-
- 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:
- # Use different baseline depending of Y log scale
- self.fill = _Fill2D(self.xData, self.yData,
- baseline=-38 if isYLog else 0,
- 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 = _Lines2D()
- 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"""
- _Lines2D.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, matrix, isXLog, isYLog):
- """Perform rendering
-
- :param numpy.ndarray matrix: 4x4 transform matrix to use
- :param bool isXLog:
- :param bool isYLog:
- """
- self.prepare()
- if self.fill is not None:
- self.fill.render(matrix)
- self._errorBars.render(matrix)
- self.lines.render(matrix)
- self.points.render(matrix)
-
- 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 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: list of int
- """
- 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 warnings.catch_warnings(): # Ignore NaN comparison warnings
- warnings.simplefilter('ignore', category=RuntimeWarning)
- 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 warnings.catch_warnings(): # Ignore NaN comparison warnings
- warnings.simplefilter('ignore', category=RuntimeWarning)
- indices = numpy.nonzero((self.xData >= xPickMin) &
- (self.xData <= xPickMax) &
- (self.yData >= yPickMin) &
- (self.yData <= yPickMax))[0].tolist()
-
- return indices
diff --git a/silx/gui/plot/backends/glutils/GLPlotFrame.py b/silx/gui/plot/backends/glutils/GLPlotFrame.py
deleted file mode 100644
index 4ad1547..0000000
--- a/silx/gui/plot/backends/glutils/GLPlotFrame.py
+++ /dev/null
@@ -1,1116 +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, plot,
- tickLength=(0., 0.),
- labelAlign=CENTER, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=CENTER,
- titleRotate=0, titleOffset=(0., 0.)):
- self._ticks = None
-
- self._plot = weakref.ref(plot)
-
- 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._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 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
-
- plot = self._plot()
- if plot is not None:
- plot._dirty()
-
- @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
- for (xPixel, yPixel), dataPos, text in self.ticks:
- if text is None:
- tickScale = 0.5
- else:
- tickScale = 1.
-
- label = Text2D(text=text,
- x=xPixel - xTickLength,
- y=yPixel - yTickLength,
- align=self._labelAlign,
- valign=self._labelVAlign)
-
- 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,
- x=xAxisCenter + xOffset,
- y=yAxisCenter + yOffset,
- align=self._titleAlign,
- valign=self._titleVAlign,
- rotate=self._titleRotate)
- labels.append(axisTitle)
-
- return vertices, labels
-
- def _dirtyTicks(self):
- """Mark ticks as dirty and notify listener (i.e., background)."""
- self._ticks = None
- plot = self._plot()
- if plot is not None:
- plot._dirty()
-
- @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))
-
- # 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, margins):
- """
- :param margins: The margins around plot area for axis and labels.
- :type margins: dict with 'left', 'right', 'top', 'bottom' keys and
- values as ints.
- """
- self._renderResources = None
-
- self._margins = self._Margins(**margins)
-
- self.axes = [] # List of PlotAxis to be updated by subclasses
-
- self._grid = False
- self._size = 0., 0.
- self._title = ''
- self._displayed = True
-
- @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 displayed(self):
- """Whether axes and their labels are displayed or not (bool)"""
- return self._displayed
-
- @displayed.setter
- def displayed(self, displayed):
- displayed = bool(displayed)
- if displayed != self._displayed:
- self._displayed = displayed
- self._dirty()
-
- @property
- def margins(self):
- """Margins in pixels around the plot."""
- if not self.displayed:
- return self._NoDisplayMargins
- else:
- return self._margins
-
- @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 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._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,
- x=xTitle,
- y=yTitle,
- align=CENTER,
- valign=BOTTOM))
-
- # 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 not self.displayed:
- 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'], 0., 0., 0., 1.)
- 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'], 0.7, 0.7, 0.7, 1.)
- 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, margins):
- """
- :param margins: The margins around plot area for axis and labels.
- :type margins: dict with 'left', 'right', 'top', 'bottom' keys and
- values as ints.
- """
- super(GLPlotFrame2D, self).__init__(margins)
- self.axes.append(PlotAxis(self,
- tickLength=(0., -5.),
- labelAlign=CENTER, labelVAlign=TOP,
- titleAlign=CENTER, titleVAlign=TOP,
- titleRotate=0,
- titleOffset=(0, self.margins.bottom // 2)))
-
- self._x2AxisCoords = ()
-
- self.axes.append(PlotAxis(self,
- tickLength=(5., 0.),
- labelAlign=RIGHT, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=BOTTOM,
- titleRotate=ROTATE_270,
- titleOffset=(-3 * self.margins.left // 4,
- 0)))
-
- self._y2Axis = PlotAxis(self,
- tickLength=(-5., 0.),
- labelAlign=LEFT, labelVAlign=CENTER,
- titleAlign=CENTER, titleVAlign=TOP,
- titleRotate=ROTATE_270,
- titleOffset=(3 * self.margins.right // 4,
- 0))
-
- 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()
-
- @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.
-
- # Non-orthogonal axes
- if self.baseVectors != self.DEFAULT_BASE_VECTORS:
- (xx, xy), (yx, yy) = self.baseVectors
- skew_mat = numpy.array(((xx, yx), (xy, yy)))
-
- corners = [(xMin, yMin), (xMin, yMax),
- (xMax, yMin), (xMax, yMax),
- (xMin, y2Min), (xMin, y2Max),
- (xMax, y2Min), (xMax, y2Max)]
-
- corners = numpy.array(
- [numpy.dot(skew_mat, corner) for corner in corners],
- dtype=numpy.float32)
- xMin, xMax = corners[:, 0].min(), corners[:, 0].max()
- yMin, yMax = corners[0:4, 1].min(), corners[0:4, 1].max()
- y2Min, y2Max = corners[4:, 1].min(), corners[4:, 1].max()
-
- 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)
-
- # Non-orthogonal axes
- if self.baseVectors != self.DEFAULT_BASE_VECTORS:
- (xx, xy), (yx, yy) = self.baseVectors
- mat = numpy.dot(mat, numpy.array((
- (xx, yx, 0., 0.),
- (xy, yy, 0., 0.),
- (0., 0., 1., 0.),
- (0., 0., 0., 1.)), dtype=numpy.float64))
-
- 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)
-
- # Non-orthogonal axes
- if self.baseVectors != self.DEFAULT_BASE_VECTORS:
- (xx, xy), (yx, yy) = self.baseVectors
- mat = numpy.dot(mat, numpy.matrix((
- (xx, yx, 0., 0.),
- (xy, yy, 0., 0.),
- (0., 0., 1., 0.),
- (0., 0., 0., 1.)), dtype=numpy.float64))
-
- 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)
diff --git a/silx/gui/plot/backends/glutils/GLPlotImage.py b/silx/gui/plot/backends/glutils/GLPlotImage.py
deleted file mode 100644
index 6f3c487..0000000
--- a/silx/gui/plot/backends/glutils/GLPlotImage.py
+++ /dev/null
@@ -1,674 +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 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
-
-
-class _GLPlotData2D(object):
- def __init__(self, data, origin, scale):
- 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 col, row
- 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
-
- def discard(self):
- pass
-
- def prepare(self):
- pass
-
- def render(self, matrix, isXLog, isYLog):
- pass
-
-
-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 struct {
- vec2 oneOverRange;
- vec2 originOverRange;
- } bounds;
-
- 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
-
- uniform sampler2D data;
- uniform struct {
- sampler2D texture;
- bool isLog;
- float min;
- float oneOverRange;
- } cmap;
- uniform float alpha;
-
- varying vec2 coords;
-
- %s
-
- const float oneOverLog10 = 0.43429448190325176;
-
- void main(void) {
- float value = texture2D(data, textureCoords()).r;
- if (cmap.isLog) {
- if (value > 0.) {
- value = clamp(cmap.oneOverRange *
- (oneOverLog10 * log(value) - cmap.min),
- 0., 1.);
- } else {
- value = 0.;
- }
- } else { /*Linear mapping*/
- value = clamp(cmap.oneOverRange * (value - cmap.min), 0., 1.);
- }
-
- 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,
- # 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')
-
- def __init__(self, data, origin, scale,
- colormap, cmapIsLog=False, cmapRange=None,
- alpha=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 bool cmapIsLog: If True, uses log10 of the data value
- :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)
- """
- assert data.dtype in self._INTERNAL_FORMATS
-
- super(GLPlotColormap, self).__init__(data, origin, scale)
- self.colormap = numpy.array(colormap, copy=False)
- self.cmapIsLog = cmapIsLog
- self._cmapRange = (1., 10.) # Colormap range
- self.cmapRange = cmapRange # Update _cmapRange
- self._alpha = numpy.clip(alpha, 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
-
- @property
- def cmapRange(self):
- if self.cmapIsLog:
- 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))
-
- 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
-
- 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.cmapIsLog:
- dataMin = math.log10(dataMin)
- dataMax = math.log10(dataMax)
-
- gl.glUniform1i(prog.uniforms['cmap.texture'],
- self._cmap_texture.texUnit)
- gl.glUniform1i(prog.uniforms['cmap.isLog'], self.cmapIsLog)
- 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)
-
- self._cmap_texture.bind()
-
- def _renderLinear(self, matrix):
- self.prepare()
-
- prog = self._linearProgram
- prog.use()
-
- gl.glUniform1i(prog.uniforms['data'], self._DATA_TEX_UNIT)
-
- mat = numpy.dot(numpy.dot(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, matrix, isXLog, isYLog):
- xMin, yMin = self.xMin, self.yMin
- if ((isXLog and xMin < FLOAT32_MINPOS) or
- (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,
- 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'], isXLog, 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, matrix, isXLog, isYLog):
- if any((isXLog, isYLog)):
- self._renderLog10(matrix, isXLog, isYLog)
- else:
- self._renderLinear(matrix)
-
- # 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 struct {
- vec2 oneOverRange;
- vec2 originOverRange;
- } bounds;
- 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))
-
- _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._texture is not None:
- self._texture.discard()
- self._texture = None
- self._textureIsDirty = False
-
- 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:
- format_ = gl.GL_RGBA if self.data.shape[2] == 4 else gl.GL_RGB
-
- self._texture = Image(format_,
- 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, matrix):
- self.prepare()
-
- prog = self._linearProgram
- prog.use()
-
- gl.glUniform1i(prog.uniforms['tex'], self._DATA_TEX_UNIT)
-
- mat = numpy.dot(numpy.dot(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, matrix, isXLog, isYLog):
- 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,
- 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'], isXLog, 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, matrix, isXLog, isYLog):
- if any((isXLog, isYLog)):
- self._renderLog(matrix, isXLog, isYLog)
- else:
- self._renderLinear(matrix)
diff --git a/silx/gui/plot/backends/glutils/GLSupport.py b/silx/gui/plot/backends/glutils/GLSupport.py
deleted file mode 100644
index 18c5eb7..0000000
--- a/silx/gui/plot/backends/glutils/GLSupport.py
+++ /dev/null
@@ -1,201 +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 Shape2D(object):
- _NO_HATCH = 0
- _HATCH_STEP = 20
-
- def __init__(self, points, fill='solid', stroke=True,
- fillColor=(0., 0., 0., 1.), strokeColor=(0., 0., 0., 1.),
- strokeClosed=True):
- self.vertices = numpy.array(points, dtype=numpy.float32, copy=False)
- self.strokeClosed = strokeClosed
-
- 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.fill = fill
- self.fillColor = fillColor
- self.stroke = stroke
- self.strokeColor = strokeColor
-
- @property
- def xMin(self):
- return self._xMin
-
- @property
- def xMax(self):
- return self._xMax
-
- @property
- def yMin(self):
- return self._yMin
-
- @property
- def yMax(self):
- return self._yMax
-
- def prepareFillMask(self, posAttrib):
- 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)
-
- def renderFill(self, posAttrib):
- self.prepareFillMask(posAttrib)
-
- 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)
-
- def renderStroke(self, posAttrib):
- gl.glEnableVertexAttribArray(posAttrib)
- gl.glVertexAttribPointer(posAttrib,
- 2,
- gl.GL_FLOAT,
- gl.GL_FALSE,
- 0, self.vertices)
- gl.glLineWidth(1)
- drawMode = gl.GL_LINE_LOOP if self.strokeClosed else gl.GL_LINE_STRIP
- gl.glDrawArrays(drawMode, 0, len(self.vertices))
-
- def render(self, posAttrib, colorUnif, hatchStepUnif):
- assert self.fill in ['hatch', 'solid', None]
- if self.fill is not None:
- gl.glUniform4f(colorUnif, *self.fillColor)
- step = self._HATCH_STEP if self.fill == 'hatch' else self._NO_HATCH
- gl.glUniform1i(hatchStepUnif, step)
- self.renderFill(posAttrib)
-
- if self.stroke:
- gl.glUniform4f(colorUnif, *self.strokeColor)
- gl.glUniform1i(hatchStepUnif, self._NO_HATCH)
- self.renderStroke(posAttrib)
-
-
-# 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 3d262bc..0000000
--- a/silx/gui/plot/backends/glutils/GLText.py
+++ /dev/null
@@ -1,270 +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 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 numpy
-
-from ...._glutils import font, gl, getGLContext, 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 = _Cache(callback=lambda key, value: value[0].discard())
- """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):
- 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):
- key = getGLContext(), text
-
- if key not in self._textures:
- image, offset = font.rasterText(text,
- font.getDefaultFontFamily())
- if text not in self._sizes:
- self._sizes[text] = image.shape[1], image.shape[0]
-
- self._textures[key] = (
- 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)),
- offset)
-
- return self._textures[key]
-
- @property
- def text(self):
- return self._text
-
- @property
- def size(self):
- if self.text not in self._sizes:
- image, offset = font.rasterText(self.text,
- font.getDefaultFontFamily())
- self._sizes[self.text] = image.shape[1], image.shape[0]
- return self._sizes[self.text]
-
- 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)
-
- 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 25dd9f1..0000000
--- a/silx/gui/plot/backends/glutils/GLTexture.py
+++ /dev/null
@@ -1,239 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-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 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)
- 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_,
- shape=(hData, wData),
- 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)
- 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)
- # 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 83c7ae0..0000000
--- a/silx/gui/plot/backends/glutils/PlotImageFile.py
+++ /dev/null
@@ -1,153 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-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.
-#
-# ############################################################################*/
-"""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.tostring() 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.tostring())
-
- 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 771de39..0000000
--- a/silx/gui/plot/backends/glutils/__init__.py
+++ /dev/null
@@ -1,44 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2014-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 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 .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 e7957ac..0000000
--- a/silx/gui/plot/items/__init__.py
+++ /dev/null
@@ -1,49 +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 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, LabelsMixIn, DraggableMixIn, ColormapMixIn, # noqa
- SymbolMixIn, ColorMixIn, YAxisMixIn, FillMixIn, # noqa
- AlphaMixIn, LineMixIn, ItemChangedType) # noqa
-from .complex import ImageComplexData # noqa
-from .curve import Curve # noqa
-from .histogram import Histogram # noqa
-from .image import ImageBase, ImageData, ImageRgba, MaskImageData # noqa
-from .shape import Shape # noqa
-from .scatter import Scatter # noqa
-from .marker import Marker, XMarker, YMarker # noqa
-from .axis import Axis, XAxis, YAxis, YRightAxis
-
-DATA_ITEMS = ImageComplexData, Curve, Histogram, ImageBase, Scatter
-"""Classes of items representing data and to consider to compute data bounds.
-"""
diff --git a/silx/gui/plot/items/axis.py b/silx/gui/plot/items/axis.py
deleted file mode 100644
index 3d9fe14..0000000
--- a/silx/gui/plot/items/axis.py
+++ /dev/null
@@ -1,567 +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 provides the class for axes of the :class:`PlotWidget`.
-"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "06/12/2017"
-
-import datetime as dt
-import logging
-
-import dateutil.tz
-
-from ... import qt
-
-from silx.third_party import enum
-
-_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(withhidden=True):
- 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)
- 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 535b0a9..0000000
--- a/silx/gui/plot/items/complex.py
+++ /dev/null
@@ -1,356 +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 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 silx.third_party import enum
-
-from ...colors import Colormap
-from .core import ColormapMixIn, 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):
- """Specific plot item to force colormap when using complex colormap.
-
- This is returning the specific colormap when displaying
- colored phase + amplitude.
- """
-
- class Mode(enum.Enum):
- """Identify available display mode for complex"""
- ABSOLUTE = 'absolute'
- PHASE = 'phase'
- REAL = 'real'
- IMAGINARY = 'imaginary'
- AMPLITUDE_PHASE = 'amplitude_phase'
- LOG10_AMPLITUDE_PHASE = 'log10_amplitude_phase'
- SQUARE_AMPLITUDE = 'square_amplitude'
-
- def __init__(self):
- ImageBase.__init__(self)
- ColormapMixIn.__init__(self)
- self._data = numpy.zeros((0, 0), dtype=numpy.complex64)
- self._dataByModesCache = {}
- self._mode = self.Mode.ABSOLUTE
- self._amplitudeRangeInfo = None, 2
-
- # Use default from ColormapMixIn
- colormap = super(ImageComplexData, self).getColormap()
-
- phaseColormap = Colormap(
- name='hsv',
- vmin=-numpy.pi,
- vmax=numpy.pi)
- phaseColormap.setEditable(False)
-
- self._colormaps = { # Default colormaps for all modes
- self.Mode.ABSOLUTE: colormap,
- self.Mode.PHASE: phaseColormap,
- self.Mode.REAL: colormap,
- self.Mode.IMAGINARY: colormap,
- self.Mode.AMPLITUDE_PHASE: phaseColormap,
- self.Mode.LOG10_AMPLITUDE_PHASE: phaseColormap,
- self.Mode.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.getVisualizationMode()
- if mode in (self.Mode.AMPLITUDE_PHASE,
- self.Mode.LOG10_AMPLITUDE_PHASE):
- # For those modes, compute RGBA image here
- colormap = None
- data = self.getRgbaImageData(copy=False)
- else:
- colormap = self.getColormap()
- data = self.getData(copy=False)
-
- if data.size == 0:
- return None # No data to display
-
- return backend.addImage(data,
- legend=self.getLegend(),
- origin=self.getOrigin(),
- scale=self.getScale(),
- z=self.getZValue(),
- selectable=self.isSelectable(),
- draggable=self.isDraggable(),
- colormap=colormap,
- alpha=self.getAlpha())
-
-
- def setVisualizationMode(self, mode):
- """Set the visualization mode to use.
-
- :param Mode mode:
- """
- assert isinstance(mode, self.Mode)
- assert mode in self._colormaps
-
- if mode != self._mode:
- self._mode = mode
-
- self._updated(ItemChangedType.VISUALIZATION_MODE)
-
- # Send data updated as value returned by getData has changed
- self._updated(ItemChangedType.DATA)
-
- # Update ColormapMixIn colormap
- colormap = self._colormaps[self._mode]
- if colormap is not super(ImageComplexData, self).getColormap():
- super(ImageComplexData, self).setColormap(colormap)
-
- def getVisualizationMode(self):
- """Returns the visualization mode in use.
-
- :rtype: Mode
- """
- return self._mode
-
- def _setAmplitudeRangeInfo(self, max_=None, delta=2):
- """Set the amplitude range to display for 'log10_amplitude_phase' mode.
-
- :param max_: Max of the amplitude range.
- If None it autoscales to data max.
- :param float delta: Delta range in log10 to display
- """
- self._amplitudeRangeInfo = max_, float(delta)
- 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 Mode mode:
- If specified, set the colormap of this specific mode.
- Default: current mode.
- """
- if mode is None:
- mode = self.getVisualizationMode()
-
- self._colormaps[mode] = colormap
- if mode is self.getVisualizationMode():
- super(ImageComplexData, self).setColormap(colormap)
- else:
- self._updated(ItemChangedType.COLORMAP)
-
- def getColormap(self, mode=None):
- """Get the colormap for the (current) mode.
-
- :param Mode mode:
- If specified, get the colormap of this specific mode.
- Default: current mode.
- :rtype: ~silx.gui.colors.Colormap
- """
- if mode is None:
- mode = self.getVisualizationMode()
-
- 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)
-
- self._data = data
- self._dataByModesCache = {}
-
- # TODO hackish data range implementation
- if self.isVisible():
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
-
- self._updated(ItemChangedType.DATA)
-
- 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 numpy.array(self._data, copy=copy)
-
- 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 Mode mode:
- If specified, get data corresponding to the mode.
- Default: Current mode.
- :rtype: numpy.ndarray of float
- """
- if mode is None:
- mode = self.getVisualizationMode()
-
- if mode not in self._dataByModesCache:
- # Compute data for mode and store it in cache
- complexData = self.getComplexData(copy=False)
- if mode is self.Mode.PHASE:
- data = numpy.angle(complexData)
- elif mode is self.Mode.REAL:
- data = numpy.real(complexData)
- elif mode is self.Mode.IMAGINARY:
- data = numpy.imag(complexData)
- elif mode in (self.Mode.ABSOLUTE,
- self.Mode.LOG10_AMPLITUDE_PHASE,
- self.Mode.AMPLITUDE_PHASE):
- data = numpy.absolute(complexData)
- elif mode is self.Mode.SQUARE_AMPLITUDE:
- data = numpy.absolute(complexData) ** 2
- else:
- _logger.error(
- 'Unsupported conversion mode: %s, fallback to absolute',
- str(mode))
- data = numpy.absolute(complexData)
-
- self._dataByModesCache[mode] = data
-
- 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 Mode 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.getVisualizationMode()
-
- colormap = self.getColormap(mode=mode)
- if mode is self.Mode.AMPLITUDE_PHASE:
- data = self.getComplexData(copy=False)
- return _complex2rgbalin(colormap, data)
- elif mode is self.Mode.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)
diff --git a/silx/gui/plot/items/core.py b/silx/gui/plot/items/core.py
deleted file mode 100644
index e000751..0000000
--- a/silx/gui/plot/items/core.py
+++ /dev/null
@@ -1,1036 +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 provides the base class for items of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "14/06/2018"
-
-import collections
-from copy import deepcopy
-import logging
-import warnings
-import weakref
-import numpy
-from silx.third_party import six, enum
-
-from ... import qt
-from ... import colors
-from ...colors import Colormap
-
-
-_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."""
-
- YAXIS = 'yAxisChanged'
- """Item's Y axis binding changed flag."""
-
- FILL = 'fillChanged'
- """Item's fill changed flag."""
-
- ALPHA = 'alphaChanged'
- """Item's transparency alpha changed flag."""
-
- DATA = 'dataChanged'
- """Item's data changed flag"""
-
- HIGHLIGHTED = 'highlightedChanged'
- """Item's highlight state changed flag."""
-
- HIGHLIGHTED_COLOR = 'highlightedColorChanged'
- """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."""
-
-
-class Item(qt.QObject):
- """Description of an item of the plot"""
-
- _DEFAULT_Z_LAYER = 0
- """Default layer for overlay rendering"""
-
- _DEFAULT_LEGEND = ''
- """Default legend of items"""
-
- _DEFAULT_SELECTABLE = False
- """Default selectable state of items"""
-
- sigItemChanged = qt.Signal(object)
- """Signal emitted when the item has changed.
-
- It provides a flag describing which property of the item has changed.
- See :class:`ItemChangedType` for flags description.
- """
-
- def __init__(self):
- qt.QObject.__init__(self)
- self._dirty = True
- self._plotRef = None
- self._visible = True
- self._legend = self._DEFAULT_LEGEND
- self._selectable = self._DEFAULT_SELECTABLE
- self._z = self._DEFAULT_Z_LAYER
- self._info = None
- self._xlabel = None
- self._ylabel = None
-
- self._backendRenderer = None
-
- def getPlot(self):
- """Returns Plot this item belongs to.
-
- :rtype: Plot or 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 Plot 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._plotRef = None if plot is None else weakref.ref(plot)
- 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 getLegend(self):
- """Returns the legend of this item (str)"""
- return self._legend
-
- def _setLegend(self, legend):
- """Set the legend.
-
- This is private as it is used by the plot as an identifier
-
- :param str legend: Item legend
- """
- legend = str(legend) if legend is not None else self._DEFAULT_LEGEND
- self._legend = 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 _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
-
-
-# Mix-in classes ##############################################################
-
-class ItemMixInBase(qt.QObject):
- """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)
-
-
-class ColormapMixIn(ItemMixInBase):
- """Mix-in class for items with colormap"""
-
- def __init__(self):
- self._colormap = Colormap()
- self._colormap.sigChanged.connect(self._colormapChanged)
-
- def getColormap(self):
- """Return the used colormap"""
- return self._colormap
-
- def setColormap(self, colormap):
- """Set the colormap of this image
-
- :param silx.gui.colors.Colormap colormap: colormap description
- """
- if isinstance(colormap, dict):
- colormap = Colormap._fromDict(colormap)
-
- if self._colormap is not None:
- self._colormap.sigChanged.disconnect(self._colormapChanged)
- self._colormap = colormap
- if self._colormap is not None:
- self._colormap.sigChanged.connect(self._colormapChanged)
- self._colormapChanged()
-
- def _colormapChanged(self):
- """Handle updates of the colormap"""
- self._updated(ItemChangedType.COLORMAP)
-
-
-class SymbolMixIn(ItemMixInBase):
- """Mix-in class for items with symbol type"""
-
- _DEFAULT_SYMBOL = ''
- """Default marker of the item"""
-
- _DEFAULT_SYMBOL_SIZE = 6.0
- """Default marker size of the item"""
-
- _SUPPORTED_SYMBOLS = collections.OrderedDict((
- ('o', 'Circle'),
- ('d', 'Diamond'),
- ('s', 'Square'),
- ('+', 'Plus'),
- ('x', 'Cross'),
- ('.', 'Point'),
- (',', 'Pixel'),
- ('', 'None')))
- """Dict of supported symbols"""
-
- def __init__(self):
- self._symbol = self._DEFAULT_SYMBOL
- 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)
- 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
- 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 Points(Item, 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):
- Item.__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.float)
-
- elif error.ndim == 1: # N array
- newError = numpy.empty((2, len(value)),
- dtype=numpy.float)
- newError[0, :] = error
- newError[1, :] = error
- error = newError
-
- elif error.size == 2 * len(value): # 2xN array
- error = numpy.array(
- error, copy=True, dtype=numpy.float)
-
- 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 warnings.catch_warnings(): # Ignore NaN warnings
- warnings.simplefilter('ignore', category=RuntimeWarning)
- xclipped = x <= 0
-
- if yPositive:
- y = self.getYData(copy=False)
- with warnings.catch_warnings(): # Ignore NaN warnings
- warnings.simplefilter('ignore', category=RuntimeWarning)
- 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.float)
- x[clipped] = numpy.nan
- y = numpy.array(y, copy=True, dtype=numpy.float)
- 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 = Points.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
-
- self._boundsCache[(xPositive, yPositive)] = (
- numpy.nanmin(x),
- numpy.nanmax(x),
- numpy.nanmin(y),
- numpy.nanmax(y)
- )
- 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
-
- if xerror is not None:
- if isinstance(xerror, collections.Iterable):
- xerror = numpy.array(xerror, copy=copy)
- else:
- xerror = float(xerror)
- if yerror is not None:
- if isinstance(yerror, collections.Iterable):
- yerror = numpy.array(yerror, copy=copy)
- else:
- yerror = float(yerror)
- # TODO checks on xerror, yerror
- self._x, self._y = x, y
- self._xerror, self._yerror = xerror, yerror
-
- self._boundsCache = {} # Reset cached bounds
- self._filteredCache = {} # Reset cached filtered data
- self._clippedCache = {} # Reset cached clipped bool array
-
- # TODO hackish data range implementation
- if self.isVisible():
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
- self._updated(ItemChangedType.DATA)
diff --git a/silx/gui/plot/items/curve.py b/silx/gui/plot/items/curve.py
deleted file mode 100644
index 80d9dea..0000000
--- a/silx/gui/plot/items/curve.py
+++ /dev/null
@@ -1,362 +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 provides the :class:`Curve` item of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "24/04/2018"
-
-
-import logging
-import numpy
-
-from silx.third_party import six
-from ....utils.deprecation import deprecated
-from ... import colors
-from .core import (Points, LabelsMixIn, ColorMixIn, YAxisMixIn,
- FillMixIn, LineMixIn, SymbolMixIn, ItemChangedType)
-
-
-_logger = logging.getLogger(__name__)
-
-
-class CurveStyle(object):
- """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(Points, ColorMixIn, YAxisMixIn, FillMixIn, LabelsMixIn, LineMixIn):
- """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"""
-
- def __init__(self):
- Points.__init__(self)
- ColorMixIn.__init__(self)
- YAxisMixIn.__init__(self)
- FillMixIn.__init__(self)
- LabelsMixIn.__init__(self)
- LineMixIn.__init__(self)
-
- self._highlightStyle = self._DEFAULT_HIGHLIGHT_STYLE
- self._highlighted = False
-
- self.sigItemChanged.connect(self.__itemChanged)
-
- def __itemChanged(self, event):
- if event == ItemChangedType.YAXIS:
- # TODO hackish data range implementation
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
-
- 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, self.getLegend(),
- color=style.getColor(),
- symbol=style.getSymbol(),
- linestyle=style.getLineStyle(),
- linewidth=style.getLineWidth(),
- yaxis=self.getYAxis(),
- xerror=xerror,
- yerror=yerror,
- z=self.getZValue(),
- selectable=self.isSelectable(),
- fill=self.isFill(),
- alpha=self.getAlpha(),
- symbolsize=style.getSymbolSize())
-
- 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.getLegend()
- 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))
-
- def setVisible(self, visible):
- """Set visibility of item.
-
- :param bool visible: True to display it, False otherwise
- """
- visible = bool(visible)
- # TODO hackish data range implementation
- if self.isVisible() != visible:
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
-
- super(Curve, self).setVisible(visible)
-
- def isHighlighted(self):
- """Returns True if curve is highlighted.
-
- :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, CurveStyle)
- self._highlightStyle = style
- self._updated(ItemChangedType.HIGHLIGHTED_STYLE)
-
- # Backward compatibility event
- if previous.getColor() != style.getColor():
- self._updated(ItemChangedType.HIGHLIGHTED_COLOR)
-
- @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()
diff --git a/silx/gui/plot/items/histogram.py b/silx/gui/plot/items/histogram.py
deleted file mode 100644
index 389e8a6..0000000
--- a/silx/gui/plot/items/histogram.py
+++ /dev/null
@@ -1,332 +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 the :class:`Histogram` item of the :class:`Plot`.
-"""
-
-__authors__ = ["H. Payno", "T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/08/2018"
-
-import logging
-
-import numpy
-
-from .core import (Item, AlphaMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn, ItemChangedType)
-
-_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 is 'left':
- width = 1
- if len(x) > 1:
- width = x[1] - x[0]
- edges = numpy.append(x[0] - width, edges)
- if histogramType is 'center':
- edges = _computeEdges(edges, 'right')
- widths = (edges[1:] - edges[0:-1]) / 2.0
- widths = numpy.append(widths, widths[-1])
- edges = edges - widths
- if histogramType is '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(Item, AlphaMixIn, ColorMixIn, FillMixIn,
- LineMixIn, YAxisMixIn):
- """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"""
-
- def __init__(self):
- Item.__init__(self)
- AlphaMixIn.__init__(self)
- ColorMixIn.__init__(self)
- FillMixIn.__init__(self)
- LineMixIn.__init__(self)
- YAxisMixIn.__init__(self)
-
- self._histogram = ()
- self._edges = ()
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- values, edges = 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.float)
- y = numpy.array(y, dtype=numpy.float)
- x[clipped] = numpy.nan
- y[clipped] = numpy.nan
-
- return backend.addCurve(x, y, self.getLegend(),
- color=self.getColor(),
- symbol='',
- linestyle=self.getLineStyle(),
- linewidth=self.getLineWidth(),
- yaxis=self.getYAxis(),
- xerror=None,
- yerror=None,
- z=self.getZValue(),
- selectable=self.isSelectable(),
- fill=self.isFill(),
- alpha=self.getAlpha(),
- symbolsize=1)
-
- def _getBounds(self):
- values, edges = 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.float)
-
- if xPositive:
- # Replace edges <= 0 by NaN and corresponding values by NaN
- clipped_edges = (edges <= 0)
- edges = numpy.array(edges, copy=True, dtype=numpy.float)
- edges[clipped_edges] = numpy.nan
- clipped_values = numpy.logical_or(clipped_edges[:-1],
- clipped_edges[1:])
- else:
- clipped_values = numpy.zeros_like(values, dtype=numpy.bool)
-
- if yPositive:
- # Replace values <= 0 by NaN, do not modify edges
- clipped_values = numpy.logical_or(clipped_values, values <= 0)
-
- values[clipped_values] = numpy.nan
-
- if xPositive or yPositive:
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- numpy.nanmin(values),
- numpy.nanmax(values))
-
- else: # No log scale, include 0 in bounds
- return (numpy.nanmin(edges),
- numpy.nanmax(edges),
- min(0, numpy.nanmin(values)),
- max(0, numpy.nanmax(values)))
-
- def setVisible(self, visible):
- """Set visibility of item.
-
- :param bool visible: True to display it, False otherwise
- """
- visible = bool(visible)
- # TODO hackish data range implementation
- if self.isVisible() != visible:
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
- super(Histogram, self).setVisible(visible)
-
- def getValueData(self, copy=True):
- """The values of the histogram
-
- :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._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 and the bin edges
-
- :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)
-
- def setData(self, histogram, edges, align='center', 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 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)
- assert numpy.all(edgesDiff >= 0) or numpy.all(edgesDiff <= 0)
-
- self._histogram = histogram
- self._edges = edges
- self._alignement = align
-
- if self.isVisible():
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
-
- 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 is 'left':
- return edges[1:]
- if histogramType is 'center':
- edges = (edges[1:] + edges[:-1]) / 2.0
- if histogramType is '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 99a916a..0000000
--- a/silx/gui/plot/items/image.py
+++ /dev/null
@@ -1,421 +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 the :class:`ImageData` and :class:`ImageRgba` items
-of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "20/10/2017"
-
-
-from collections import Sequence
-import logging
-
-import numpy
-
-from .core import (Item, 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(Item, LabelsMixIn, DraggableMixIn, AlphaMixIn):
- """Description of an image"""
-
- def __init__(self):
- Item.__init__(self)
- LabelsMixIn.__init__(self)
- DraggableMixIn.__init__(self)
- AlphaMixIn.__init__(self)
- self._data = numpy.zeros((0, 0, 4), dtype=numpy.uint8)
-
- 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.getLegend()
- 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 setVisible(self, visible):
- """Set visibility of item.
-
- :param bool visible: True to display it, False otherwise
- """
- visible = bool(visible)
- # TODO hackish data range implementation
- if self.isVisible() != visible:
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
- super(ImageBase, self).setVisible(visible)
-
- def _isPlotLinear(self, plot):
- """Return True if plot only uses linear scale for both of x and y
- axes."""
- linear = plot.getXAxis().LINEAR
- if plot.getXAxis().getScale() != linear:
- return False
- if plot.getYAxis().getScale() != linear:
- return False
- return True
-
- def _getBounds(self):
- if self.getData(copy=False).size == 0: # Empty data
- 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
-
- 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 getRgbaImageData(self, copy=True):
- """Get the displayed RGB(A) image
-
- :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, Sequence):
- origin = float(origin[0]), float(origin[1])
- else: # single value origin
- origin = float(origin), float(origin)
- if origin != self._origin:
- self._origin = origin
-
- # TODO hackish data range implementation
- if self.isVisible():
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
-
- 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, Sequence):
- scale = float(scale[0]), float(scale[1])
- else: # single value scale
- scale = float(scale), float(scale)
-
- if scale != self._scale:
- self._scale = scale
-
- # TODO hackish data range implementation
- if self.isVisible():
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
-
- self._updated(ItemChangedType.SCALE)
-
-
-class ImageData(ImageBase, ColormapMixIn):
- """Description of a data image with a colormap"""
-
- def __init__(self):
- ImageBase.__init__(self)
- ColormapMixIn.__init__(self)
- self._data = numpy.zeros((0, 0), dtype=numpy.float32)
- self._alternativeImage = 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:
- dataToUse = self.getAlternativeImageData(copy=False)
- else:
- dataToUse = self.getData(copy=False)
-
- if dataToUse.size == 0:
- return None # No data to display
-
- return backend.addImage(dataToUse,
- legend=self.getLegend(),
- origin=self.getOrigin(),
- scale=self.getScale(),
- z=self.getZValue(),
- selectable=self.isSelectable(),
- draggable=self.isDraggable(),
- colormap=self.getColormap(),
- 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: numpy.ndarray of uint8 of shape (height, width, 4)
- """
- if self._alternativeImage is not None:
- return _convertImageToRgba32(
- self.getAlternativeImageData(copy=False), copy=copy)
- else:
- # Apply colormap, in this case an new array is always returned
- colormap = self.getColormap()
- image = colormap.applyToData(self.getData(copy=False))
- return image
-
- def getAlternativeImageData(self, copy=True):
- """Get the optional RGBA image that is displayed instead of the data
-
- :param copy: True (Default) to get a copy,
- False to use internal representation (do not modify!)
- :returns: None or numpy.ndarray
- :rtype: numpy.ndarray or None
- """
- if self._alternativeImage is None:
- return None
- else:
- return numpy.array(self._alternativeImage, copy=copy)
-
- def setData(self, data, alternative=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: None or 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)
- self._data = 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
-
- # TODO hackish data range implementation
- if self.isVisible():
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
-
- self._updated(ItemChangedType.DATA)
-
-
-class ImageRgba(ImageBase):
- """Description of an RGB(A) image"""
-
- def __init__(self):
- ImageBase.__init__(self)
-
- 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,
- legend=self.getLegend(),
- origin=self.getOrigin(),
- scale=self.getScale(),
- z=self.getZValue(),
- selectable=self.isSelectable(),
- draggable=self.isDraggable(),
- 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)
- self._data = data
-
- # TODO hackish data range implementation
- if self.isVisible():
- plot = self.getPlot()
- if plot is not None:
- plot._invalidateDataRange()
-
- self._updated(ItemChangedType.DATA)
-
-
-class MaskImageData(ImageData):
- """Description of an image used as a mask.
-
- This class is used to flag mask items. This information is used to improve
- internal silx widgets.
- """
- pass
diff --git a/silx/gui/plot/items/marker.py b/silx/gui/plot/items/marker.py
deleted file mode 100644
index 09767a5..0000000
--- a/silx/gui/plot/items/marker.py
+++ /dev/null
@@ -1,261 +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 provides markers item of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "06/03/2017"
-
-
-import logging
-
-from .core import (Item, DraggableMixIn, ColorMixIn, LineMixIn, SymbolMixIn,
- ItemChangedType)
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _BaseMarker(Item, DraggableMixIn, ColorMixIn):
- """Base class for markers"""
-
- _DEFAULT_COLOR = (0., 0., 0., 1.)
- """Default color of the markers"""
-
- def __init__(self):
- Item.__init__(self)
- DraggableMixIn.__init__(self)
- ColorMixIn.__init__(self)
-
- self._text = ''
- self._x = None
- self._y = None
- self._constraint = self._defaultConstraint
-
- 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(),
- legend=self.getLegend(),
- text=self.getText(),
- color=self.getColor(),
- selectable=self.isSelectable(),
- draggable=self.isDraggable(),
- symbol=symbol,
- linestyle=linestyle,
- linewidth=linewidth,
- constraint=self.getConstraint())
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- raise NotImplementedError()
-
- def isOverlay(self):
- """Return true if marker is drawn as an overlay.
-
- A marker is an overlay if it is draggable.
-
- :rtype: bool
- """
- return self.isDraggable()
-
- 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
-
-
-class Marker(_BaseMarker, SymbolMixIn):
- """Description of a marker"""
-
- _DEFAULT_SYMBOL = '+'
- """Default symbol of the marker"""
-
- def __init__(self):
- _BaseMarker.__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(_BaseMarker, LineMixIn):
- """Base class for line markers"""
-
- def __init__(self):
- _BaseMarker.__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 f55ef91..0000000
--- a/silx/gui/plot/items/roi.py
+++ /dev/null
@@ -1,1416 +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 ROI item for the :class:`~silx.gui.plot.PlotWidget`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import functools
-import itertools
-import logging
-import collections
-import numpy
-
-from ....utils.weakref import WeakList
-from ... import qt
-from .. import items
-from ...colors import rgba
-
-
-logger = logging.getLogger(__name__)
-
-
-class RegionOfInterest(qt.QObject):
- """Object describing a region of interest in a plot.
-
- :param QObject parent:
- The RegionOfInterestManager that created this object
- """
-
- _kind = None
- """Label for this kind of ROI.
-
- Should be setted by inherited classes to custom the ROI manager widget.
- """
-
- sigRegionChanged = qt.Signal()
- """Signal emitted everytime the shape or position of the ROI changes"""
-
- def __init__(self, parent=None):
- # Avoid circular dependancy
- from ..tools import roi as roi_tools
- assert parent is None or isinstance(parent, roi_tools.RegionOfInterestManager)
- super(RegionOfInterest, self).__init__(parent)
- self._color = rgba('red')
- self._items = WeakList()
- self._editAnchors = WeakList()
- self._points = None
- self._label = ''
- self._labelItem = None
- self._editable = False
-
- def __del__(self):
- # Clean-up plot items
- self._removePlotItems()
-
- def setParent(self, parent):
- """Set the parent of the RegionOfInterest
-
- :param Union[None,RegionOfInterestManager] parent:
- """
- # Avoid circular dependancy
- from ..tools import roi as roi_tools
- if (parent is not None and not isinstance(parent, roi_tools.RegionOfInterestManager)):
- raise ValueError('Unsupported parent')
-
- self._removePlotItems()
- super(RegionOfInterest, self).setParent(parent)
- self._createPlotItems()
-
- @classmethod
- def _getKind(cls):
- """Return an human readable kind of ROI
-
- :rtype: str
- """
- return cls._kind
-
- def getColor(self):
- """Returns the color of this ROI
-
- :rtype: QColor
- """
- return qt.QColor.fromRgbF(*self._color)
-
- def _getAnchorColor(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 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
-
- # Update color of shape items in the plot
- rgbaColor = rgba(color)
- for item in list(self._items):
- if isinstance(item, items.ColorMixIn):
- item.setColor(rgbaColor)
- item = self._getLabelItem()
- if isinstance(item, items.ColorMixIn):
- item.setColor(rgbaColor)
-
- rgbaColor = self._getAnchorColor(rgbaColor)
- for item in list(self._editAnchors):
- if isinstance(item, items.ColorMixIn):
- item.setColor(rgbaColor)
-
- def getLabel(self):
- """Returns the label displayed for this ROI.
-
- :rtype: str
- """
- return self._label
-
- def setLabel(self, label):
- """Set the label displayed with this ROI.
-
- :param str label: The text label to display
- """
- label = str(label)
- if label != self._label:
- self._label = label
- self._updateLabelItem(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
- # Recreate plot items
- # This can be avoided once marker.setDraggable is public
- self._createPlotItems()
-
- def _getControlPoints(self):
- """Returns the current ROI control points.
-
- It returns an empty tuple if there is currently no ROI.
-
- :return: Array of (x, y) position in plot coordinates
- :rtype: numpy.ndarray
- """
- return None if self._points is None else numpy.array(self._points)
-
- @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 True
-
- @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 constains by the plot API and only supports few
- shapes.
- """
- points = self._createControlPointsFromFirstShape(points)
- self._setControlPoints(points)
-
- def _createControlPointsFromFirstShape(self, points):
- """Returns the list of control points from the very first shape
- provided.
-
- This shape is provided by the plot interaction and constained by the
- class of the ROI itself.
- """
- return points
-
- def _setControlPoints(self, points):
- """Set this ROI control points.
-
- :param points: Iterable of (x, y) control points
- """
- points = numpy.array(points)
-
- nbPointsChanged = (self._points is None or
- points.shape != self._points.shape)
-
- if nbPointsChanged or not numpy.all(numpy.equal(points, self._points)):
- self._points = points
-
- self._updateShape()
- if self._items and not nbPointsChanged: # Update plot items
- item = self._getLabelItem()
- if item is not None:
- markerPos = self._getLabelPosition()
- item.setPosition(*markerPos)
-
- if self._editAnchors: # Update anchors
- for anchor, point in zip(self._editAnchors, points):
- old = anchor.blockSignals(True)
- anchor.setPosition(*point)
- anchor.blockSignals(old)
-
- else: # No items or new point added
- # re-create plot items
- self._createPlotItems()
-
- self.sigRegionChanged.emit()
-
- def _updateShape(self):
- """Called when shape must be updated.
-
- Must be reimplemented if a shape item have to be updated.
- """
- return
-
- def _getLabelPosition(self):
- """Compute position of the label
-
- :return: (x, y) position of the marker
- """
- return None
-
- def _createPlotItems(self):
- """Create items displaying the ROI in the plot.
-
- It first removes any existing plot items.
- """
- roiManager = self.parent()
- if roiManager is None:
- return
- plot = roiManager.parent()
-
- self._removePlotItems()
-
- legendPrefix = "__RegionOfInterest-%d__" % id(self)
- itemIndex = 0
-
- controlPoints = self._getControlPoints()
-
- if self._labelItem is None:
- self._labelItem = self._createLabelItem()
- if self._labelItem is not None:
- self._labelItem._setLegend(legendPrefix + "label")
- plot._add(self._labelItem)
-
- self._items = WeakList()
- plotItems = self._createShapeItems(controlPoints)
- for item in plotItems:
- item._setLegend(legendPrefix + str(itemIndex))
- plot._add(item)
- self._items.append(item)
- itemIndex += 1
-
- self._editAnchors = WeakList()
- if self.isEditable():
- plotItems = self._createAnchorItems(controlPoints)
- color = rgba(self.getColor())
- color = self._getAnchorColor(color)
- for index, item in enumerate(plotItems):
- item._setLegend(legendPrefix + str(itemIndex))
- item.setColor(color)
- plot._add(item)
- item.sigItemChanged.connect(functools.partial(
- self._controlPointAnchorChanged, index))
- self._editAnchors.append(item)
- itemIndex += 1
-
- def _updateLabelItem(self, label):
- """Update the marker displaying the label.
-
- Inherite this method to custom the way the ROI display the label.
-
- :param str label: The new label to use
- """
- item = self._getLabelItem()
- if item is not None:
- item.setText(label)
-
- def _createLabelItem(self):
- """Returns a created marker which will be used to dipslay the label of
- this ROI.
-
- Inherite this method to return nothing if no new items have to be
- created, or your own marker.
-
- :rtype: Union[None,Marker]
- """
- # Add label marker
- markerPos = self._getLabelPosition()
- marker = items.Marker()
- marker.setPosition(*markerPos)
- marker.setText(self.getLabel())
- marker.setColor(rgba(self.getColor()))
- marker.setSymbol('')
- marker._setDraggable(False)
- return marker
-
- def _getLabelItem(self):
- """Returns the marker displaying the label of this ROI.
-
- Inherite this method to choose your own item. In case this item is also
- a control point.
- """
- return self._labelItem
-
- def _createShapeItems(self, points):
- """Create shape items from the current control points.
-
- :rtype: List[PlotItem]
- """
- return []
-
- def _createAnchorItems(self, points):
- """Create anchor items from the current control points.
-
- :rtype: List[Marker]
- """
- return []
-
- def _controlPointAnchorChanged(self, index, event):
- """Handle update of position of an edition anchor
-
- :param int index: Index of the anchor
- :param ItemChangedType event: Event type
- """
- if event == items.ItemChangedType.POSITION:
- anchor = self._editAnchors[index]
- previous = self._points[index].copy()
- current = anchor.getPosition()
- self._controlPointAnchorPositionChanged(index, current, previous)
-
- def _controlPointAnchorPositionChanged(self, index, current, previous):
- """Called when an anchor is manually edited.
-
- This function have to be inherited to change the behaviours of the
- control points. This function have to call :meth:`_getControlPoints` to
- reach the previous state of the control points. Updated the positions
- of the changed control points. Then call :meth:`_setControlPoints` to
- update the anchors and send signals.
- """
- points = self._getControlPoints()
- points[index] = current
- self._setControlPoints(points)
-
- def _removePlotItems(self):
- """Remove items from their plot."""
- for item in itertools.chain(list(self._items),
- list(self._editAnchors)):
-
- plot = item.getPlot()
- if plot is not None:
- plot._remove(item)
- self._items = WeakList()
- self._editAnchors = WeakList()
-
- if self._labelItem is not None:
- item = self._labelItem
- plot = item.getPlot()
- if plot is not None:
- plot._remove(item)
- self._labelItem = None
-
- def __str__(self):
- """Returns parameters of the ROI as a string."""
- points = self._getControlPoints()
- params = '; '.join('(%f; %f)' % (pt[0], pt[1]) for pt in points)
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class PointROI(RegionOfInterest):
- """A ROI identifying a point in a 2D plot."""
-
- _kind = "Point"
- """Label for this kind of ROI"""
-
- _plotShape = "point"
- """Plot shape which is used for the first interaction"""
-
- def getPosition(self):
- """Returns the position of this ROI
-
- :rtype: numpy.ndarray
- """
- return self._points[0].copy()
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param numpy.ndarray pos: 2d-coordinate of this point
- """
- controlPoints = numpy.array([pos])
- self._setControlPoints(controlPoints)
-
- def _createLabelItem(self):
- return None
-
- def _updateLabelItem(self, label):
- if self.isEditable():
- item = self._editAnchors[0]
- else:
- item = self._items[0]
- item.setText(label)
-
- def _createShapeItems(self, points):
- if self.isEditable():
- return []
- marker = items.Marker()
- marker.setPosition(points[0][0], points[0][1])
- marker.setText(self.getLabel())
- marker.setColor(rgba(self.getColor()))
- marker._setDraggable(False)
- return [marker]
-
- def _createAnchorItems(self, points):
- marker = items.Marker()
- marker.setPosition(points[0][0], points[0][1])
- marker.setText(self.getLabel())
- marker._setDraggable(self.isEditable())
- return [marker]
-
- def __str__(self):
- points = self._getControlPoints()
- params = '%f %f' % (points[0, 0], points[0, 1])
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class LineROI(RegionOfInterest):
- """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.
- """
-
- _kind = "Line"
- """Label for this kind of ROI"""
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- def _createControlPointsFromFirstShape(self, points):
- center = numpy.mean(points, axis=0)
- controlPoints = numpy.array([points[0], points[1], center])
- return controlPoints
-
- def setEndPoints(self, startPoint, endPoint):
- """Set this line location using the endding points
-
- :param numpy.ndarray startPoint: Staring bounding point of the line
- :param numpy.ndarray endPoint: Endding bounding point of the line
- """
- assert(startPoint.shape == (2,) and endPoint.shape == (2,))
- shapePoints = numpy.array([startPoint, endPoint])
- controlPoints = self._createControlPointsFromFirstShape(shapePoints)
- self._setControlPoints(controlPoints)
-
- def getEndPoints(self):
- """Returns bounding points of this ROI.
-
- :rtype: Tuple(numpy.ndarray,numpy.ndarray)
- """
- startPoint = self._points[0].copy()
- endPoint = self._points[1].copy()
- return (startPoint, endPoint)
-
- def _getLabelPosition(self):
- points = self._getControlPoints()
- return points[-1]
-
- def _updateShape(self):
- if len(self._items) == 0:
- return
- shape = self._items[0]
- points = self._getControlPoints()
- points = self._getShapeFromControlPoints(points)
- shape.setPoints(points)
-
- def _getShapeFromControlPoints(self, points):
- # Remove the center from the control points
- return points[0:2]
-
- def _createShapeItems(self, points):
- shapePoints = self._getShapeFromControlPoints(points)
- item = items.Shape("polylines")
- item.setPoints(shapePoints)
- item.setColor(rgba(self.getColor()))
- item.setFill(False)
- item.setOverlay(True)
- return [item]
-
- def _createAnchorItems(self, points):
- anchors = []
- for point in points[0:-1]:
- anchor = items.Marker()
- anchor.setPosition(*point)
- anchor.setText('')
- anchor.setSymbol('s')
- anchor._setDraggable(True)
- anchors.append(anchor)
-
- # Add an anchor to the center of the rectangle
- center = numpy.mean(points, axis=0)
- anchor = items.Marker()
- anchor.setPosition(*center)
- anchor.setText('')
- anchor.setSymbol('+')
- anchor._setDraggable(True)
- anchors.append(anchor)
-
- return anchors
-
- def _controlPointAnchorPositionChanged(self, index, current, previous):
- if index == len(self._editAnchors) - 1:
- # It is the center anchor
- points = self._getControlPoints()
- center = numpy.mean(points[0:-1], axis=0)
- offset = current - previous
- points[-1] = current
- points[0:-1] = points[0:-1] + offset
- self._setControlPoints(points)
- else:
- # Update the center
- points = self._getControlPoints()
- points[index] = current
- center = numpy.mean(points[0:-1], axis=0)
- points[-1] = center
- self._setControlPoints(points)
-
- def __str__(self):
- points = self._getControlPoints()
- params = points[0][0], points[0][1], points[1][0], points[1][1]
- params = 'start: %f %f; end: %f %f' % params
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class HorizontalLineROI(RegionOfInterest):
- """A ROI identifying an horizontal line in a 2D plot."""
-
- _kind = "HLine"
- """Label for this kind of ROI"""
-
- _plotShape = "hline"
- """Plot shape which is used for the first interaction"""
-
- def _createControlPointsFromFirstShape(self, points):
- points = numpy.array([(float('nan'), points[0, 1])],
- dtype=numpy.float64)
- return points
-
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
- return self._points[0, 1]
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param float pos: Horizontal position of this line
- """
- controlPoints = numpy.array([[float('nan'), pos]])
- self._setControlPoints(controlPoints)
-
- def _createLabelItem(self):
- return None
-
- def _updateLabelItem(self, label):
- if self.isEditable():
- item = self._editAnchors[0]
- else:
- item = self._items[0]
- item.setText(label)
-
- def _updateShape(self):
- if not self.isEditable():
- if len(self._items) > 0:
- controlPoints = self._getControlPoints()
- item = self._items[0]
- item.setPosition(*controlPoints[0])
-
- def _createShapeItems(self, points):
- if self.isEditable():
- return []
- marker = items.YMarker()
- marker.setPosition(points[0][0], points[0][1])
- marker.setText(self.getLabel())
- marker.setColor(rgba(self.getColor()))
- marker._setDraggable(False)
- return [marker]
-
- def _createAnchorItems(self, points):
- marker = items.YMarker()
- marker.setPosition(points[0][0], points[0][1])
- marker.setText(self.getLabel())
- marker._setDraggable(self.isEditable())
- return [marker]
-
- def __str__(self):
- points = self._getControlPoints()
- params = 'y: %f' % points[0, 1]
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class VerticalLineROI(RegionOfInterest):
- """A ROI identifying a vertical line in a 2D plot."""
-
- _kind = "VLine"
- """Label for this kind of ROI"""
-
- _plotShape = "vline"
- """Plot shape which is used for the first interaction"""
-
- def _createControlPointsFromFirstShape(self, points):
- points = numpy.array([(points[0, 0], float('nan'))],
- dtype=numpy.float64)
- return points
-
- def getPosition(self):
- """Returns the position of this line if the horizontal axis
-
- :rtype: float
- """
- return self._points[0, 0]
-
- def setPosition(self, pos):
- """Set the position of this ROI
-
- :param float pos: Horizontal position of this line
- """
- controlPoints = numpy.array([[pos, float('nan')]])
- self._setControlPoints(controlPoints)
-
- def _createLabelItem(self):
- return None
-
- def _updateLabelItem(self, label):
- if self.isEditable():
- item = self._editAnchors[0]
- else:
- item = self._items[0]
- item.setText(label)
-
- def _updateShape(self):
- if not self.isEditable():
- if len(self._items) > 0:
- controlPoints = self._getControlPoints()
- item = self._items[0]
- item.setPosition(*controlPoints[0])
-
- def _createShapeItems(self, points):
- if self.isEditable():
- return []
- marker = items.XMarker()
- marker.setPosition(points[0][0], points[0][1])
- marker.setText(self.getLabel())
- marker.setColor(rgba(self.getColor()))
- marker._setDraggable(False)
- return [marker]
-
- def _createAnchorItems(self, points):
- marker = items.XMarker()
- marker.setPosition(points[0][0], points[0][1])
- marker.setText(self.getLabel())
- marker._setDraggable(self.isEditable())
- return [marker]
-
- def __str__(self):
- points = self._getControlPoints()
- params = 'x: %f' % points[0, 0]
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class RectangleROI(RegionOfInterest):
- """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.
- """
-
- _kind = "Rectangle"
- """Label for this kind of ROI"""
-
- _plotShape = "rectangle"
- """Plot shape which is used for the first interaction"""
-
- def _createControlPointsFromFirstShape(self, points):
- point0 = points[0]
- point1 = points[1]
-
- # 4 corners
- controlPoints = numpy.array([
- point0[0], point0[1],
- point0[0], point1[1],
- point1[0], point1[1],
- point1[0], point0[1],
- ])
- # Central
- center = numpy.mean(points, axis=0)
- controlPoints = numpy.append(controlPoints, center)
- controlPoints.shape = -1, 2
- return controlPoints
-
- def getCenter(self):
- """Returns the central point of this rectangle
-
- :rtype: numpy.ndarray([float,float])
- """
- return numpy.mean(self._points, axis=0)
-
- def getOrigin(self):
- """Returns the corner point with the smaller coordinates
-
- :rtype: numpy.ndarray([float,float])
- """
- return numpy.min(self._points, axis=0)
-
- def getSize(self):
- """Returns the size of this rectangle
-
- :rtype: numpy.ndarray([float,float])
- """
- minPoint = numpy.min(self._points, axis=0)
- maxPoint = numpy.max(self._points, axis=0)
- return maxPoint - minPoint
-
- 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 not None:
- origin = numpy.array(origin)
- size = numpy.array(size)
- points = numpy.array([origin, origin + size])
- controlPoints = self._createControlPointsFromFirstShape(points)
- elif center is not None:
- center = numpy.array(center)
- size = numpy.array(size)
- points = numpy.array([center - size * 0.5, center + size * 0.5])
- controlPoints = self._createControlPointsFromFirstShape(points)
- else:
- raise ValueError("Origin or cengter expected")
- self._setControlPoints(controlPoints)
-
- def _getLabelPosition(self):
- points = self._getControlPoints()
- return points.min(axis=0)
-
- def _updateShape(self):
- if len(self._items) == 0:
- return
- shape = self._items[0]
- points = self._getControlPoints()
- points = self._getShapeFromControlPoints(points)
- shape.setPoints(points)
-
- def _getShapeFromControlPoints(self, points):
- minPoint = points.min(axis=0)
- maxPoint = points.max(axis=0)
- return numpy.array([minPoint, maxPoint])
-
- def _createShapeItems(self, points):
- shapePoints = self._getShapeFromControlPoints(points)
- item = items.Shape("rectangle")
- item.setPoints(shapePoints)
- item.setColor(rgba(self.getColor()))
- item.setFill(False)
- item.setOverlay(True)
- return [item]
-
- def _createAnchorItems(self, points):
- # Remove the center control point
- points = points[0:-1]
-
- anchors = []
- for point in points:
- anchor = items.Marker()
- anchor.setPosition(*point)
- anchor.setText('')
- anchor.setSymbol('s')
- anchor._setDraggable(True)
- anchors.append(anchor)
-
- # Add an anchor to the center of the rectangle
- center = numpy.mean(points, axis=0)
- anchor = items.Marker()
- anchor.setPosition(*center)
- anchor.setText('')
- anchor.setSymbol('+')
- anchor._setDraggable(True)
- anchors.append(anchor)
-
- return anchors
-
- def _controlPointAnchorPositionChanged(self, index, current, previous):
- if index == len(self._editAnchors) - 1:
- # It is the center anchor
- points = self._getControlPoints()
- center = numpy.mean(points[0:-1], axis=0)
- offset = current - previous
- points[-1] = current
- points[0:-1] = points[0:-1] + offset
- self._setControlPoints(points)
- else:
- # Fix other corners
- constrains = [(1, 3), (0, 2), (3, 1), (2, 0)]
- constrains = constrains[index]
- points = self._getControlPoints()
- points[index] = current
- points[constrains[0]][0] = current[0]
- points[constrains[1]][1] = current[1]
- # Update the center
- center = numpy.mean(points[0:-1], axis=0)
- points[-1] = center
- self._setControlPoints(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 PolygonROI(RegionOfInterest):
- """A ROI identifying a closed polygon in a 2D plot.
-
- This ROI provides 1 anchor for each point of the polygon.
- """
-
- _kind = "Polygon"
- """Label for this kind of ROI"""
-
- _plotShape = "polygon"
- """Plot shape which is used for the first interaction"""
-
- 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 len(points) > 0:
- controlPoints = numpy.array(points)
- else:
- controlPoints = numpy.empty((0, 2))
- self._setControlPoints(controlPoints)
-
- def _getLabelPosition(self):
- points = self._getControlPoints()
- if len(points) == 0:
- # FIXME: we should return none, this polygon have no location
- return numpy.array([0, 0])
- return points[numpy.argmin(points[:, 1])]
-
- def _updateShape(self):
- if len(self._items) == 0:
- return
- shape = self._items[0]
- points = self._getControlPoints()
- shape.setPoints(points)
-
- def _createShapeItems(self, points):
- if len(points) == 0:
- return []
- else:
- item = items.Shape("polygon")
- item.setPoints(points)
- item.setColor(rgba(self.getColor()))
- item.setFill(False)
- item.setOverlay(True)
- return [item]
-
- def _createAnchorItems(self, points):
- anchors = []
- for point in points:
- anchor = items.Marker()
- anchor.setPosition(*point)
- anchor.setText('')
- anchor.setSymbol('s')
- anchor._setDraggable(True)
- anchors.append(anchor)
- return anchors
-
- def __str__(self):
- points = self._getControlPoints()
- params = '; '.join('%f %f' % (pt[0], pt[1]) for pt in points)
- return "%s(%s)" % (self.__class__.__name__, params)
-
-
-class ArcROI(RegionOfInterest):
- """A ROI identifying an arc of a circle with a width.
-
- This ROI provides 3 anchors to control the curvature, 1 anchor to control
- the weigth, and 1 anchor to translate the shape.
- """
-
- _kind = "Arc"
- """Label for this kind of ROI"""
-
- _plotShape = "line"
- """Plot shape which is used for the first interaction"""
-
- _ArcGeometry = collections.namedtuple('ArcGeometry', ['center',
- 'startPoint', 'endPoint',
- 'radius', 'weight',
- 'startAngle', 'endAngle'])
-
- def __init__(self, parent=None):
- RegionOfInterest.__init__(self, parent=parent)
- self._geometry = None
-
- def _getInternalGeometry(self):
- """Returns the object storing the internal geometry of this ROI.
-
- This geometry is derived from the control points and cached for
- efficiency. Calling :meth:`_setControlPoints` invalidate the cache.
- """
- if self._geometry is None:
- controlPoints = self._getControlPoints()
- self._geometry = self._createGeometryFromControlPoint(controlPoints)
- return self._geometry
-
- @classmethod
- def showFirstInteractionShape(cls):
- return False
-
- def _getLabelPosition(self):
- points = self._getControlPoints()
- return points.min(axis=0)
-
- def _updateShape(self):
- if len(self._items) == 0:
- return
- shape = self._items[0]
- points = self._getControlPoints()
- points = self._getShapeFromControlPoints(points)
- shape.setPoints(points)
-
- def _controlPointAnchorPositionChanged(self, index, current, previous):
- controlPoints = self._getControlPoints()
- currentWeigth = numpy.linalg.norm(controlPoints[3] - controlPoints[1]) * 2
-
- if index in [0, 2]:
- # Moving start or end will maintain the same curvature
- # Then we have to custom the curvature control point
- startPoint = controlPoints[0]
- endPoint = controlPoints[2]
- center = (startPoint + endPoint) * 0.5
- normal = (endPoint - startPoint)
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- # Compute the coeficient which have to be constrained
- if distance != 0:
- normal /= distance
- midVector = controlPoints[1] - center
- constainedCoef = numpy.dot(midVector, normal) / distance
- else:
- constainedCoef = 1.0
-
- # Compute the location of the curvature point
- controlPoints[index] = current
- startPoint = controlPoints[0]
- endPoint = controlPoints[2]
- center = (startPoint + endPoint) * 0.5
- normal = (endPoint - startPoint)
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- if distance != 0:
- # BTW we dont need to divide by the distance here
- # Cause we compute normal * distance after all
- normal /= distance
- midPoint = center + normal * constainedCoef * distance
- controlPoints[1] = midPoint
-
- # The weight have to be fixed
- self._updateWeightControlPoint(controlPoints, currentWeigth)
- self._setControlPoints(controlPoints)
-
- elif index == 1:
- # The weight have to be fixed
- controlPoints[index] = current
- self._updateWeightControlPoint(controlPoints, currentWeigth)
- self._setControlPoints(controlPoints)
- else:
- super(ArcROI, self)._controlPointAnchorPositionChanged(index, current, previous)
-
- def _updateWeightControlPoint(self, controlPoints, weigth):
- startPoint = controlPoints[0]
- midPoint = controlPoints[1]
- endPoint = controlPoints[2]
- normal = (endPoint - startPoint)
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- if distance != 0:
- normal /= distance
- controlPoints[3] = midPoint + normal * weigth * 0.5
-
- def _createGeometryFromControlPoint(self, controlPoints):
- """Returns the geometry of the object"""
- weigth = numpy.linalg.norm(controlPoints[3] - controlPoints[1]) * 2
- if numpy.allclose(controlPoints[0], controlPoints[2]):
- # Special arc: It's a closed circle
- center = (controlPoints[0] + controlPoints[1]) * 0.5
- radius = numpy.linalg.norm(controlPoints[0] - center)
- v = controlPoints[0] - center
- startAngle = numpy.angle(complex(v[0], v[1]))
- endAngle = startAngle + numpy.pi * 2.0
- return self._ArcGeometry(center, controlPoints[0], controlPoints[2],
- radius, weigth, startAngle, endAngle)
-
- elif numpy.linalg.norm(
- numpy.cross(controlPoints[1] - controlPoints[0],
- controlPoints[2] - controlPoints[0])) < 1e-5:
- # Degenerated arc, it's a rectangle
- return self._ArcGeometry(None, controlPoints[0], controlPoints[2],
- None, weigth, None, None)
- else:
- center, radius = self._circleEquation(*controlPoints[:3])
- v = controlPoints[0] - center
- startAngle = numpy.angle(complex(v[0], v[1]))
- v = controlPoints[1] - center
- midAngle = numpy.angle(complex(v[0], v[1]))
- v = controlPoints[2] - center
- endAngle = numpy.angle(complex(v[0], v[1]))
- # Is it clockwise or anticlockwise
- if (midAngle - startAngle + 2 * numpy.pi) % (2 * numpy.pi) <= numpy.pi:
- if endAngle < startAngle:
- endAngle += 2 * numpy.pi
- else:
- if endAngle > startAngle:
- endAngle -= 2 * numpy.pi
-
- return self._ArcGeometry(center, controlPoints[0], controlPoints[2],
- radius, weigth, startAngle, endAngle)
-
- def _isCircle(self, geometry):
- """Returns True if the geometry is a closed circle"""
- delta = numpy.abs(geometry.endAngle - geometry.startAngle)
- return numpy.isclose(delta, numpy.pi * 2)
-
- def _getShapeFromControlPoints(self, controlPoints):
- geometry = self._createGeometryFromControlPoint(controlPoints)
- if geometry.center is None:
- # It is not an arc
- # but we can display it as an the intermediat 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])
- else:
- innerRadius = geometry.radius - geometry.weight * 0.5
- outerRadius = geometry.radius + geometry.weight * 0.5
-
- if numpy.isnan(geometry.startAngle):
- # Degenerated, it's a point
- # At least 2 points are expected
- return numpy.array([geometry.startPoint, geometry.startPoint])
-
- 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)
-
- isCircle = self._isCircle(geometry)
-
- if isCircle:
- if innerRadius <= 0:
- # 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)
- else:
- # 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")]))
- else:
- if innerRadius <= 0:
- # 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)
- else:
- # 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)
- points = numpy.array(points)
-
- return points
-
- def _setControlPoints(self, points):
- # Invalidate the geometry
- self._geometry = None
- RegionOfInterest._setControlPoints(self, points)
-
- def getGeometry(self):
- """Returns a tuple containing the geometry of this ROI
-
- It is a symetric fonction 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 representaed as section of
- a circle
- """
- geometry = self._getInternalGeometry()
- 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
- """
- geometry = self._getInternalGeometry()
- return self._isCircle(geometry)
-
- 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
- """
- geometry = self._getInternalGeometry()
- return 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
- """
- geometry = self._getInternalGeometry()
- return 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
- """
- geometry = self._getInternalGeometry()
- return geometry.endAngle
-
- def getInnerRadius(self):
- """Returns the radius of the smaller arc used to draw this ROI.
-
- :rtype: float
- """
- geometry = self._getInternalGeometry()
- 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._getInternalGeometry()
- 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.
- """
- assert(innerRadius <= outerRadius)
- assert(numpy.abs(startAngle - endAngle) <= 2 * numpy.pi)
- center = numpy.array(center)
- radius = (innerRadius + outerRadius) * 0.5
- weight = outerRadius - innerRadius
- geometry = self._ArcGeometry(center, None, None, radius, weight, startAngle, endAngle)
- controlPoints = self._createControlPointsFromGeometry(geometry)
- self._setControlPoints(controlPoints)
-
- def _createControlPointsFromGeometry(self, geometry):
- if geometry.startPoint or geometry.endPoint:
- # Duplication with the angles
- raise NotImplementedError("This general case is not implemented")
-
- angle = geometry.startAngle
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- startPoint = geometry.center + direction * geometry.radius
-
- angle = geometry.endAngle
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- endPoint = geometry.center + direction * geometry.radius
-
- angle = (geometry.startAngle + geometry.endAngle) * 0.5
- direction = numpy.array([numpy.cos(angle), numpy.sin(angle)])
- curvaturePoint = geometry.center + direction * geometry.radius
- weightPoint = curvaturePoint + direction * geometry.weight * 0.5
-
- return numpy.array([startPoint, curvaturePoint, endPoint, weightPoint])
-
- def _createControlPointsFromFirstShape(self, points):
- # The first shape is a line
- point0 = points[0]
- point1 = points[1]
-
- # Compute a non colineate point for the curvature
- center = (point1 + point0) * 0.5
- normal = point1 - center
- normal = numpy.array((normal[1], -normal[0]))
- defaultCurvature = numpy.pi / 5.0
- defaultWeight = 0.20 # percentage
- curvaturePoint = center - normal * defaultCurvature
- weightPoint = center - normal * defaultCurvature * (1.0 + defaultWeight)
-
- # 3 corners
- controlPoints = numpy.array([
- point0,
- curvaturePoint,
- point1,
- weightPoint
- ])
- return controlPoints
-
- def _createShapeItems(self, points):
- shapePoints = self._getShapeFromControlPoints(points)
- item = items.Shape("polygon")
- item.setPoints(shapePoints)
- item.setColor(rgba(self.getColor()))
- item.setFill(False)
- item.setOverlay(True)
- return [item]
-
- def _createAnchorItems(self, points):
- anchors = []
- symbols = ['o', 'o', 'o', 's']
-
- for index, point in enumerate(points):
- if index in [1, 3]:
- constraint = self._arcCurvatureMarkerConstraint
- else:
- constraint = None
- anchor = items.Marker()
- anchor.setPosition(*point)
- anchor.setText('')
- anchor.setSymbol(symbols[index])
- anchor._setDraggable(True)
- if constraint is not None:
- anchor._setConstraint(constraint)
- anchors.append(anchor)
-
- return anchors
-
- def _arcCurvatureMarkerConstraint(self, x, y):
- """Curvature marker remains on "mediatrice" """
- start = self._points[0]
- end = self._points[2]
- midPoint = (start + end) / 2.
- normal = (end - start)
- normal = numpy.array((normal[1], -normal[0]))
- distance = numpy.linalg.norm(normal)
- if distance != 0:
- normal /= distance
- v = numpy.dot(normal, (numpy.array((x, y)) - midPoint))
- x, y = midPoint + v * 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 ((-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/scatter.py b/silx/gui/plot/items/scatter.py
deleted file mode 100644
index acc74b4..0000000
--- a/silx/gui/plot/items/scatter.py
+++ /dev/null
@@ -1,193 +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 provides the :class:`Scatter` item of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "29/03/2017"
-
-
-import logging
-
-import numpy
-
-from .core import Points, ColormapMixIn
-
-
-_logger = logging.getLogger(__name__)
-
-
-class Scatter(Points, ColormapMixIn):
- """Description of a scatter"""
-
- _DEFAULT_SELECTABLE = True
- """Default selectable state for scatter plots"""
-
- _DEFAULT_SYMBOL = 'o'
- """Default symbol of the scatter plots"""
-
- def __init__(self):
- Points.__init__(self)
- ColormapMixIn.__init__(self)
- self._value = ()
- self.__alpha = None
-
- def _addBackendRenderer(self, backend):
- """Update backend renderer"""
- # Filter-out values <= 0
- xFiltered, yFiltered, valueFiltered, xerror, yerror = self.getData(
- copy=False, displayed=True)
-
- if len(xFiltered) == 0:
- return None # No data to display, do not add renderer to backend
-
- cmap = self.getColormap()
- rgbacolors = cmap.applyToData(self._value)
-
- if self.__alpha is not None:
- rgbacolors[:, -1] = (rgbacolors[:, -1] * self.__alpha).astype(numpy.uint8)
-
- return backend.addCurve(xFiltered, yFiltered, self.getLegend(),
- color=rgbacolors,
- symbol=self.getSymbol(),
- linewidth=0,
- linestyle="",
- yaxis='left',
- xerror=xerror,
- yerror=yerror,
- z=self.getZValue(),
- selectable=self.isSelectable(),
- fill=False,
- alpha=self.getAlpha(),
- symbolsize=self.getSymbolSize())
-
- 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 Points 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.float)
- value[clipped] = numpy.nan
-
- x, y, xerror, yerror = Points._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 Points 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)
-
- self._value = value
-
- 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()
- Points.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 65b26a1..0000000
--- a/silx/gui/plot/items/shape.py
+++ /dev/null
@@ -1,121 +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 the :class:`Shape` item of the :class:`Plot`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "17/05/2017"
-
-
-import logging
-
-import numpy
-
-from .core import (Item, ColorMixIn, FillMixIn, ItemChangedType)
-
-
-_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):
- """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)
- self._overlay = False
- assert type_ in ('hline', 'polygon', 'rectangle', 'vline', 'polylines')
- self._type = type_
- self._points = ()
-
- 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.addItem(x,
- y,
- legend=self.getLegend(),
- shape=self.getType(),
- color=self.getColor(),
- fill=self.isFill(),
- overlay=self.isOverlay(),
- z=self.getZValue())
-
- 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)
diff --git a/silx/gui/plot/matplotlib/Colormap.py b/silx/gui/plot/matplotlib/Colormap.py
deleted file mode 100644
index 772a473..0000000
--- a/silx/gui/plot/matplotlib/Colormap.py
+++ /dev/null
@@ -1,232 +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.
-#
-# ############################################################################*/
-"""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
-
-
-_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
-def magma():
- return getColormap('magma')
-
-
-@property
-def inferno():
- return getColormap('inferno')
-
-
-@property
-def plasma():
- return getColormap('plasma')
-
-
-@property
-def viridis():
- return getColormap('viridis')
-
-
-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)
-
-
-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)
- if colormap.getNormalization().startswith('log'):
- norm = matplotlib.colors.LogNorm(vmin, vmax)
- else: # Linear normalization
- norm = matplotlib.colors.Normalize(vmin, vmax)
-
- 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
-
-
-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 a4dc235..0000000
--- a/silx/gui/plot/matplotlib/__init__.py
+++ /dev/null
@@ -1,101 +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.
-#
-# ###########################################################################*/
-
-from __future__ import absolute_import
-
-"""This module inits matplotlib and setups the backend to use.
-
-It MUST be imported prior to any other import of matplotlib.
-
-It provides the matplotlib :class:`FigureCanvasQTAgg` class corresponding
-to the used backend.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "02/05/2018"
-
-
-import sys
-import logging
-
-
-_logger = logging.getLogger(__name__)
-
-_matplotlib_already_loaded = 'matplotlib' in sys.modules
-"""If true, matplotlib was already loaded"""
-
-import matplotlib
-from ... import qt
-
-
-def _configure(backend, backend_qt4=None, backend_qt5=None, check=False):
- """Configure matplotlib using a specific backend.
-
- It initialize `matplotlib.rcParams` using the requested backend, or check
- if it is already configured as requested.
-
- :param bool check: If true, the function only check that matplotlib
- is already initialized as request. If not a warning is emitted.
- If `check` is false, matplotlib is initialized.
- """
- if check:
- valid = matplotlib.rcParams['backend'] == backend
- if backend_qt4 is not None:
- valid = valid and matplotlib.rcParams['backend.qt4'] == backend_qt4
- if backend_qt5 is not None:
- valid = valid and matplotlib.rcParams['backend.qt5'] == backend_qt5
-
- if not valid:
- _logger.warning('matplotlib already loaded, setting its backend may not work')
- else:
- matplotlib.rcParams['backend'] = backend
- if backend_qt4 is not None:
- matplotlib.rcParams['backend.qt4'] = backend_qt4
- if backend_qt5 is not None:
- matplotlib.rcParams['backend.qt5'] = backend_qt5
-
-
-if qt.BINDING == 'PySide':
- _configure('Qt4Agg', backend_qt4='PySide', check=_matplotlib_already_loaded)
- import matplotlib.backends.backend_qt4agg as backend
-
-elif qt.BINDING == 'PyQt4':
- _configure('Qt4Agg', check=_matplotlib_already_loaded)
- import matplotlib.backends.backend_qt4agg as backend
-
-elif qt.BINDING == 'PySide2':
- _configure('Qt5Agg', backend_qt5="PySide2", check=_matplotlib_already_loaded)
- import matplotlib.backends.backend_qt5agg as backend
-
-elif qt.BINDING == 'PyQt5':
- _configure('Qt5Agg', check=_matplotlib_already_loaded)
- import matplotlib.backends.backend_qt5agg as backend
-
-else:
- backend = None
-
-if backend is not None:
- FigureCanvasQTAgg = backend.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 a753989..0000000
--- a/silx/gui/plot/stats/stats.py
+++ /dev/null
@@ -1,491 +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 provides the :class:`Scatter` item of the :class:`Plot`.
-"""
-
-__authors__ = ["H. Payno"]
-__license__ = "MIT"
-__date__ = "06/06/2018"
-
-
-import numpy
-from silx.gui.plot.items.curve import Curve as CurveItem
-from silx.gui.plot.items.image import ImageBase as ImageItem
-from silx.gui.plot.items.scatter import Scatter as ScatterItem
-from silx.gui.plot.items.histogram import Histogram as HistogramItem
-from silx.math.combo import min_max
-from collections import OrderedDict
-import logging
-
-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):
- """
- Call all :class:`Stat` object registred 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.
- :return dict: dictionary with :class:`Stat` name as ket and result
- of the calculation as value
- """
- res = {}
- if isinstance(item, CurveItem):
- context = _CurveContext(item, plot, onlimits)
- elif isinstance(item, ImageItem):
- context = _ImageContext(item, plot, onlimits)
- elif isinstance(item, ScatterItem):
- context = _ScatterContext(item, plot, onlimits)
- elif isinstance(item, HistogramItem):
- context = _HistogramContext(item, plot, onlimits)
- else:
- raise ValueError('Item type not managed')
- 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:
- 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):
- self.__setitem__(key=stat.name, value=stat)
-
-
-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.
- """
- def __init__(self, item, kind, plot, onlimits):
- assert item
- assert plot
- assert type(onlimits) is bool
- self.kind = kind
- self.min = None
- self.max = None
- self.data = None
- self.values = None
- self.createContext(item, plot, onlimits)
-
- def createContext(self, item, plot, onlimits):
- raise NotImplementedError("Base class")
-
-
-class _CurveContext(_StatsContext):
- """
- 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.
- """
- def __init__(self, item, plot, onlimits):
- _StatsContext.__init__(self, kind='curve', item=item,
- plot=plot, onlimits=onlimits)
-
- def createContext(self, item, plot, onlimits):
- xData, yData = item.getData(copy=True)[0:2]
-
- if onlimits:
- minX, maxX = plot.getXAxis().getLimits()
- yData = yData[(minX <= xData) & (xData <= maxX)]
- xData = xData[(minX <= xData) & (xData <= maxX)]
-
- self.xData = xData
- self.yData = yData
- if len(yData) > 0:
- self.min, self.max = min_max(yData)
- else:
- self.min, self.max = None, None
- self.data = (xData, yData)
- self.values = yData
-
-
-class _HistogramContext(_StatsContext):
- """
- 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.
- """
- def __init__(self, item, plot, onlimits):
- _StatsContext.__init__(self, kind='histogram', item=item,
- plot=plot, onlimits=onlimits)
-
- def createContext(self, item, plot, onlimits):
- xData, edges = item.getData(copy=True)[0:2]
- yData = item._revertComputeEdges(x=edges, histogramType=item.getAlignment())
- if onlimits:
- minX, maxX = plot.getXAxis().getLimits()
- yData = yData[(minX <= xData) & (xData <= maxX)]
- xData = xData[(minX <= xData) & (xData <= maxX)]
-
- self.xData = xData
- self.yData = yData
- if len(yData) > 0:
- self.min, self.max = min_max(yData)
- else:
- self.min, self.max = None, None
- self.data = (xData, yData)
- self.values = yData
-
-
-class _ScatterContext(_StatsContext):
- """
- StatsContext for :class:`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.
- """
- def __init__(self, item, plot, onlimits):
- _StatsContext.__init__(self, kind='scatter', item=item, plot=plot,
- onlimits=onlimits)
-
- def createContext(self, item, plot, onlimits):
- xData, yData, valueData, xerror, yerror = item.getData(copy=True)
- assert plot
- 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 len(valueData) > 0:
- self.min, self.max = min_max(valueData)
- else:
- self.min, self.max = None, None
- self.data = (xData, yData, valueData)
- self.values = valueData
-
-
-class _ImageContext(_StatsContext):
- """
- StatsContext for :class:`ImageBase`
-
- :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.
- """
- def __init__(self, item, plot, onlimits):
- _StatsContext.__init__(self, kind='image', item=item,
- plot=plot, onlimits=onlimits)
-
- def createContext(self, item, plot, onlimits):
- self.origin = item.getOrigin()
- self.scale = item.getScale()
- self.data = item.getData()
-
- 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 XMaxBound <= XMinBound or YMaxBound <= YMinBound:
- return self.noDataSelected()
- data = item.getData()
- self.data = data[YMinBound:YMaxBound + 1, XMinBound:XMaxBound + 1]
- else:
- self.data = item.getData()
-
- if self.data.size > 0:
- self.min, self.max = min_max(self.data)
- else:
- self.min, self.max = None, None
- self.values = self.data
-
-
-BASIC_COMPATIBLE_KINDS = {
- 'curve': CurveItem,
- 'image': ImageItem,
- 'scatter': ScatterItem,
- 'histogram': HistogramItem,
-}
-
-
-class StatBase(object):
- """
- Base class for defining a statistic.
-
- :param str name: the name of the statistic. Must be unique.
- :param compatibleKinds: the kind of items (curve, scatter...) for which
- the statistic apply.
- :rtype: List or tuple
- """
- 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 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 kinf: 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
-
- def calculate(self, context):
- if context.kind in self.compatibleKinds:
- return self._fct(context.values)
- else:
- raise ValueError('Kind %s not managed by %s'
- '' % (context.kind, self.name))
-
-
-class StatMin(StatBase):
- """
- Compute the minimal value on data
- """
- def __init__(self):
- StatBase.__init__(self, name='min')
-
- def calculate(self, context):
- return context.min
-
-
-class StatMax(StatBase):
- """
- Compute the maximal value on data
- """
- def __init__(self):
- StatBase.__init__(self, name='max')
-
- 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')
-
- def calculate(self, context):
- return context.max - context.min
-
-
-class StatCoordMin(StatBase):
- """
- Compute the first coordinates of the data minimal value
- """
- def __init__(self):
- StatBase.__init__(self, name='coords min')
-
- def calculate(self, context):
- if context.kind in ('curve', 'histogram'):
- return context.xData[numpy.argmin(context.yData)]
- elif context.kind == 'scatter':
- xData, yData, valueData = context.data
- return (xData[numpy.argmin(valueData)],
- yData[numpy.argmin(valueData)])
- elif context.kind == 'image':
- scaleX, scaleY = context.scale
- originX, originY = context.origin
- index1D = numpy.argmin(context.data)
- ySize = (context.data.shape[1])
- x = index1D % context.data.shape[1]
- y = (index1D - x) / ySize
- x = x * scaleX + originX
- y = y * scaleY + originY
- return (x, y)
- else:
- raise ValueError('kind not managed')
-
- def getToolTip(self, kind):
- if kind in ('scatter', 'image'):
- return '(x, y)'
- else:
- return None
-
-class StatCoordMax(StatBase):
- """
- Compute the first coordinates of the data minimal value
- """
- def __init__(self):
- StatBase.__init__(self, name='coords max')
-
- def calculate(self, context):
- if context.kind in ('curve', 'histogram'):
- return context.xData[numpy.argmax(context.yData)]
- elif context.kind == 'scatter':
- xData, yData, valueData = context.data
- return (xData[numpy.argmax(valueData)],
- yData[numpy.argmax(valueData)])
- elif context.kind == 'image':
- scaleX, scaleY = context.scale
- originX, originY = context.origin
- index1D = numpy.argmax(context.data)
- ySize = (context.data.shape[1])
- x = index1D % context.data.shape[1]
- y = (index1D - x) / ySize
- x = x * scaleX + originX
- y = y * scaleY + originY
- return (x, y)
- else:
- raise ValueError('kind not managed')
-
- def getToolTip(self, kind):
- if kind in ('scatter', 'image'):
- return '(x, y)'
- else:
- return None
-
-class StatCOM(StatBase):
- """
- Compute data center of mass
- """
- def __init__(self):
- StatBase.__init__(self, name='COM', description='Center of mass')
-
- def calculate(self, context):
- if context.kind in ('curve', 'histogram'):
- xData, yData = context.data
- deno = numpy.sum(yData).astype(numpy.float32)
- if deno == 0.:
- return numpy.nan
- else:
- return numpy.sum(xData * yData).astype(numpy.float32) / deno
- elif context.kind == 'scatter':
- xData, yData, values = context.data
- deno = numpy.sum(values).astype(numpy.float32)
- if deno == 0.:
- return numpy.nan, numpy.nan
- else:
- xcom = numpy.sum(xData * values).astype(numpy.float32) / deno
- ycom = numpy.sum(yData * values).astype(numpy.float32) / deno
- return (xcom, ycom)
- elif context.kind == 'image':
- yData = numpy.sum(context.data, axis=1)
- xData = numpy.sum(context.data, axis=0)
- dataXRange = range(context.data.shape[1])
- dataYRange = range(context.data.shape[0])
- xScale, yScale = context.scale
- xOrigin, yOrigin = context.origin
-
- denoY = numpy.sum(yData)
- if denoY == 0.:
- ycom = numpy.nan
- else:
- ycom = numpy.sum(yData * dataYRange) / denoY
- ycom = ycom * yScale + yOrigin
-
- denoX = numpy.sum(xData)
- if denoX == 0.:
- xcom = numpy.nan
- else:
- xcom = numpy.sum(xData * dataXRange) / denoX
- xcom = xcom * xScale + xOrigin
- return (xcom, ycom)
- else:
- raise ValueError('kind not managed')
-
- def getToolTip(self, kind):
- if kind in ('scatter', 'image'):
- return '(x, y)'
- else:
- return None
diff --git a/silx/gui/plot/stats/statshandler.py b/silx/gui/plot/stats/statshandler.py
deleted file mode 100644
index 0a62b31..0000000
--- a/silx/gui/plot/stats/statshandler.py
+++ /dev/null
@@ -1,190 +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.
-#
-# ###########################################################################*/
-"""
-"""
-
-__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):
- return float(self.text()) < float(other.text())
-
-
-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:
- helper = _StatHelper(elmt)
- self.add(stat=helper.stat, formatter=helper.statFormatter)
-
- def add(self, stat, formatter=None):
- 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 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):
- """
- compute all statistic registred 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
- :return: list of formatted statistics (as str)
- :rtype: dict
- """
- res = self.stats.calculate(item, plot, onlimits)
- for resName, resValue in list(res.items()):
- res[resName] = self.format(resName, res[resName])
- return res
-
-
-class _StatHelper(object):
- """
- Helper class to generated the requested StatBase instance and the
- associated StatFormatter
- """
- def __init__(self, arg):
- self.statFormatter = None
- self.stat = None
-
- if isinstance(arg, statsmdl.StatBase):
- self.stat = arg
- else:
- assert len(arg) > 0
- if isinstance(arg[0], statsmdl.StatBase):
- self.dealWithStatAndFormatter(arg)
- else:
- _arg = arg
- if isinstance(arg[0], tuple):
- _arg = arg[0]
- if len(arg) > 1:
- self.statFormatter = arg[1]
- self.createStatInstanceAndFormatter(_arg)
-
- def dealWithStatAndFormatter(self, arg):
- assert isinstance(arg[0], statsmdl.StatBase)
- self.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) is 2:
- assert isinstance(arg[1], (StatFormatter, type(None), str))
- self.statFormatter = arg[1]
-
- def createStatInstanceAndFormatter(self, arg):
- 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) is 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) is 2:
- self.stat = statsmdl.Stat(name=arg[0], fct=arg[1])
- else:
- self.stat = statsmdl.Stat(name=arg[0], fct=arg[1], kinds=arg[2])
diff --git a/silx/gui/plot/test/__init__.py b/silx/gui/plot/test/__init__.py
deleted file mode 100644
index 89c10c6..0000000
--- a/silx/gui/plot/test/__init__.py
+++ /dev/null
@@ -1,90 +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 testProfile
-from . import testStackView
-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
-
-
-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(),
- testProfile.suite(),
- testStackView.suite(),
- testItem.suite(),
- testUtilsAxis.suite(),
- testLimitConstraints.suite(),
- testComplexImageView.suite(),
- testImageView.suite(),
- testSaveAction.suite(),
- testScatterView.suite(),
- testPixelIntensityHistoAction.suite(),
- testCompareImages.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 63de441..0000000
--- a/silx/gui/plot/test/testAlphaSlider.py
+++ /dev/null
@@ -1,221 +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.
-#
-# ###########################################################################*/
-"""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
-
-# Makes sure a QApplication exists
-_qapp = qt.QApplication.instance() or qt.QApplication([])
-
-
-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 9a02e04..0000000
--- a/silx/gui/plot/test/testColorBar.py
+++ /dev/null
@@ -1,351 +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 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.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.assertTrue(val == 100.0)
-
- val = self.colorScaleWidget.getValueFromRelativePosition(0.5)
- self.assertTrue(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.assertTrue(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.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._norm == "linear")
-
- # 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.assertTrue(
- self.colorBar.getColorScaleBar().getTickBar()._norm == 'log')
-
- # 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 1933a95..0000000
--- a/silx/gui/plot/test/testComplexImageView.py
+++ /dev/null
@@ -1,95 +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.
-#
-# ###########################################################################*/
-"""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.complex)
- 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.getSupportedVisualizationModes()
- for mode in modes:
- with self.subTest(mode=mode):
- self.plot.setVisualizationMode(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.complex))
- self.qWait(100)
-
- # Test float data
- self.plot.setData(numpy.arange(100, dtype=numpy.float).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 0704779..0000000
--- a/silx/gui/plot/test/testCurvesROIWidget.py
+++ /dev/null
@@ -1,183 +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 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.test.utils import temp_dir
-from silx.gui.utils.testutils import TestCaseQt
-from silx.gui.plot import PlotWindow, CurvesROIWidget
-
-
-_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 = CurvesROIWidget.CurvesROIDockWidget(plot=self.plot, name='TEST')
- 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 testEmptyPlot(self):
- """Empty plot, display ROI widget"""
- pass
-
- 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.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton)
-
- # Change active curve
- self.plot.setActiveCurve(str(1))
-
- # Delete a ROI
- self.mouseClick(self.widget.roiWidget.delButton, qt.Qt.LeftButton)
-
- 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))
-
- # Reset ROIs
- self.mouseClick(self.widget.roiWidget.resetButton,
- qt.Qt.LeftButton)
-
- # Load ROIs
- self.widget.roiWidget.load(self.tmpFile)
-
- del self.tmpFile
-
- def testMiddleMarker(self):
- """Test with middle marker enabled"""
- self.widget.roiWidget.setMiddleROIMarkerFlag(True)
-
- # Add a ROI
- self.mouseClick(self.widget.roiWidget.addButton, qt.Qt.LeftButton)
-
- xleftMarker = self.plot._getMarker(legend='ROI min').getXPosition()
- xMiddleMarker = self.plot._getMarker(legend='ROI middle').getXPosition()
- xRightMarker = self.plot._getMarker(legend='ROI max').getXPosition()
- self.assertAlmostEqual(xMiddleMarker,
- xleftMarker + (xRightMarker - xleftMarker) / 2.)
-
- def testCalculation(self):
- 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
- ddict = {}
- ddict["positive"] = {"from": 10, "to": 20, "type":"X"}
- ddict["negative"] = {"from": -20, "to": -10, "type":"X"}
- self.widget.roiWidget.setRois(ddict)
-
- # And calculate the expected output
- self.widget.calculateROIs()
-
- output = self.widget.roiWidget.getRois()
- self.assertEqual(output["positive"]["rawcounts"],
- y[ddict["positive"]["from"]:ddict["positive"]["to"]+1].sum(),
- "Calculation failed on positive X coordinates")
-
- # Set the curve with negative X coordinates as active
- self.plot.setActiveCurve("negative")
-
- # the ROIs should have been automatically updated
- output = self.widget.roiWidget.getRois()
- selection = numpy.nonzero((-x >= output["negative"]["from"]) & \
- (-x <= output["negative"]["to"]))[0]
- self.assertEqual(output["negative"]["rawcounts"],
- y[selection].sum(), "Calculation failed on negative X coordinates")
-
- def testDeferedInit(self):
- 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.assertFalse(roiWidget._isInit)
- self.plot.getCurvesRoiDockWidget().setRois(roisDefs)
- self.assertTrue(len(roiWidget.getRois()) is len(roisDefs))
- self.plot.getCurvesRoiDockWidget().setVisible(True)
- self.assertTrue(len(roiWidget.getRois()) is len(roisDefs))
-
-
-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/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 074a7cd..0000000
--- a/silx/gui/plot/test/testInteraction.py
+++ /dev/null
@@ -1,89 +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.
-#
-# ###########################################################################*/
-"""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):
- events.append(('beginDrag', x, y))
-
- def drag(self, x, y):
- events.append(('drag', x, y))
-
- def endDrag(self, x, y):
- events.append(('endDrag', x, y))
-
- 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))
- self.assertEqual(events[1], ('drag', 15, 10))
- clickOrDrag.handleEvent('move', 20, 10)
- self.assertEqual(len(events), 3)
- self.assertEqual(events[-1], ('drag', 20, 10))
- clickOrDrag.handleEvent('release', 20, 10, Interaction.LEFT_BTN)
- self.assertEqual(len(events), 4)
- self.assertEqual(events[-1], ('endDrag', (10, 10), (20, 10)))
-
-
-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 993cce7..0000000
--- a/silx/gui/plot/test/testItem.py
+++ /dev/null
@@ -1,249 +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.
-#
-# ###########################################################################*/
-"""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 .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 Points 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.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')
- data2 = data + 10
-
- # Test of signals in Scatter class
- scatter.setData(data2, data2, data2)
-
- self.assertEqual(listener.arguments(),
- [(ItemChangedType.COLORMAP,),
- (ItemChangedType.DATA,)])
-
- def testShapeChanged(self):
- """Test sigItemChanged for shape"""
- data = numpy.array((1., 10.))
- self.plot.addItem(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)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loadTests = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loadTests(TestSigItemChangedSignal))
- test_suite.addTest(loadTests(TestSymbol))
- 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 6912ea3..0000000
--- a/silx/gui/plot/test/testMaskToolsWidget.py
+++ /dev/null
@@ -1,294 +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
-
-try:
- import fabio
-except ImportError:
- fabio = None
-
-
-_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.mouseClick(plot, qt.Qt.LeftButton, pos=pos0)
- self.mouseMove(plot, pos=(0, 0))
- self.mouseMove(plot, pos=pos1)
- self.mouseClick(plot, qt.Qt.LeftButton, pos=pos1)
-
- 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.mouseClick(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 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 origin, scale in tests:
- with self.subTest(origin=origin, scale=scale):
- self.plot.addImage(numpy.arange(1024**2).reshape(1024, 1024),
- legend='test',
- origin=origin,
- scale=scale)
- 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):
- """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):
- if fabio is None:
- self.skipTest("Fabio is missing")
- 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 20d1ea2..0000000
--- a/silx/gui/plot/test/testPixelIntensityHistoAction.py
+++ /dev/null
@@ -1,104 +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(100, 100)
- 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.getHistogramPlotWidget().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.getHistogramPlotWidget().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 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 335b1e4..0000000
--- a/silx/gui/plot/test/testPlotInteraction.py
+++ /dev/null
@@ -1,168 +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.mouseClick(plot, qt.Qt.LeftButton, pos=pos)
-
- 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 100644
index 857b9bc..0000000
--- a/silx/gui/plot/test/testPlotWidget.py
+++ /dev/null
@@ -1,1539 +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__ = "21/09/2018"
-
-
-import unittest
-import logging
-import numpy
-
-from silx.utils.testutils import ParametricTestCase, parameterize
-from silx.gui.utils.testutils import SignalListener
-from silx.gui.utils.testutils import TestCaseQt
-from silx.utils import testutils
-from silx.utils import deprecation
-
-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.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 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.addItem((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')
-
-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, 256))),
- dtype=numpy.uint8)
-
- self.plot.addImage(rgb, legend="rgb",
- origin=(0, 0), 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()
-
- 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 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=numpy.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)
-
-
-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 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')
-
-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()
-
-
-# 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'),
- ]
-
- # 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'),
- ]
-
- 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):
- self.plot.setGraphTitle('Item Fill')
-
- for legend, xList, yList, color in self.polygons:
- self.plot.addItem(xList, yList, legend=legend,
- replace=False,
- shape="polygon", fill=True, color=color)
- self.plot.resetZoom()
-
- def testPlotItemPolygonNoFill(self):
- self.plot.setGraphTitle('Item No Fill')
-
- for legend, xList, yList, color in self.polygons:
- self.plot.addItem(xList, yList, legend=legend,
- replace=False,
- shape="polygon", fill=False, color=color)
- self.plot.resetZoom()
-
- def testPlotItemRectangleFill(self):
- self.plot.setGraphTitle('Rectangle Fill')
-
- for legend, xList, yList, color in self.rectangles:
- self.plot.addItem(xList, yList, legend=legend,
- replace=False,
- shape="rectangle", fill=True, color=color)
- self.plot.resetZoom()
-
- def testPlotItemRectangleNoFill(self):
- self.plot.setGraphTitle('Rectangle No Fill')
-
- for legend, xList, yList, color in self.rectangles:
- self.plot.addItem(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)
-
- @testutils.test_logging(deprecation.depreclog.name)
- 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")
-
- listener = SignalListener()
- self.plot.sigSetXAxisLogarithmic.connect(listener.partial("x"))
- self.plot.sigSetYAxisLogarithmic.connect(listener.partial("y"))
-
- 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.assertEqual(listener.arguments(callIndex=-1), ("x", True))
-
- 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)
- self.assertEqual(listener.arguments(callIndex=-1), ("y", 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)
- self.assertEqual(listener.arguments(callIndex=-1), ("y", False))
-
- @testutils.test_logging(deprecation.depreclog.name)
- 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")
-
- listener = SignalListener()
- self.plot.sigSetXAxisAutoScale.connect(listener.partial("x"))
- self.plot.sigSetYAxisAutoScale.connect(listener.partial("y"))
-
- 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.assertEqual(listener.arguments(callIndex=-1), ("x", False))
-
- 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)
- self.assertEqual(listener.arguments(callIndex=-1), ("y", 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)
- self.assertEqual(listener.arguments(callIndex=-1), ("y", True))
-
- @testutils.test_logging(deprecation.depreclog.name)
- 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")
-
- listener = SignalListener()
- self.plot.sigSetYAxisInverted.connect(listener.partial("y"))
-
- 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)
- self.assertEqual(listener.arguments(callIndex=-1), ("y", 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)
- self.assertEqual(listener.arguments(callIndex=-1), ("y", 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)
-
-
-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 TestPlotItemLog(PlotWidgetTestCase):
- """Basic tests for items with log scale axes"""
-
- # 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'),
- ]
-
- # 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'),
- ]
-
- def setUp(self):
- super(TestPlotItemLog, 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., 100.)
- self.plot.getXAxis()._setLogarithmic(True)
- self.plot.getYAxis()._setLogarithmic(True)
-
- def testPlotItemPolygonLogFill(self):
- self.plot.setGraphTitle('Item Fill Log')
-
- for legend, xList, yList, color in self.polygons:
- self.plot.addItem(xList, yList, legend=legend,
- replace=False,
- shape="polygon", fill=True, color=color)
- self.plot.resetZoom()
-
- def testPlotItemPolygonLogNoFill(self):
- self.plot.setGraphTitle('Item No Fill Log')
-
- for legend, xList, yList, color in self.polygons:
- self.plot.addItem(xList, yList, legend=legend,
- replace=False,
- shape="polygon", fill=False, color=color)
- self.plot.resetZoom()
-
- def testPlotItemRectangleLogFill(self):
- self.plot.setGraphTitle('Rectangle Fill Log')
-
- for legend, xList, yList, color in self.rectangles:
- self.plot.addItem(xList, yList, legend=legend,
- replace=False,
- shape="rectangle", fill=True, color=color)
- self.plot.resetZoom()
-
- def testPlotItemRectangleLogNoFill(self):
- self.plot.setGraphTitle('Rectangle No Fill Log')
-
- for legend, xList, yList, color in self.rectangles:
- self.plot.addItem(xList, yList, legend=legend,
- replace=False,
- shape="rectangle", fill=False, color=color)
- self.plot.resetZoom()
-
-
-def suite():
- testClasses = (TestPlotWidget, TestPlotImage, TestPlotCurve,
- TestPlotMarker, TestPlotItem, TestPlotAxes,
- TestPlotActiveCurveImage,
- TestPlotEmptyLog, TestPlotCurveLog, TestPlotImageLog,
- TestPlotMarkerLog, TestPlotItemLog)
-
- test_suite = unittest.TestSuite()
-
- # Tests with matplotlib
- for testClass in testClasses:
- test_suite.addTest(parameterize(testClass, backend=None))
-
- if test_options.WITH_GL_TEST:
- # Tests with OpenGL backend
- for testClass in testClasses:
- test_suite.addTest(parameterize(testClass, backend='gl'))
-
- 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 cd7cbb3..0000000
--- a/silx/gui/plot/test/testPlotWidgetNoBackend.py
+++ /dev/null
@@ -1,633 +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 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.addItem(
- 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.getLegend(), 'curve 0')
- curve = plot.getCurve()
- self.assertEqual(curve.getLegend(), '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.getLegend(), 'curve 2') # Last added curve
-
- # Last curve hidden
- plot.hideCurve('curve 2', True)
- curve = plot.getCurve()
- self.assertEqual(curve.getLegend(), '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.getLegend(), 'image 0')
- image = plot.getImage()
- self.assertEqual(image.getLegend(), '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.getLegend(), 'image 2')
-
- # Active image
- plot.setActiveImage('image 1')
- active = plot.getActiveImage()
- self.assertEqual(active.getLegend(), 'image 1')
- image = plot.getImage()
- self.assertEqual(image.getLegend(), '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].getLegend(), '1')
- self.assertEqual(images[1].getLegend(), '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.getLegend(), '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.getLegend(), 'scatter 1')
-
- def testGetAllScatters(self):
- """PlotWidget.getAllImages test"""
-
- plot = PlotWidget(backend='none')
-
- scatters = plot._getItems(kind='scatter')
- self.assertEqual(len(scatters), 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')
-
- scatters = plot._getItems(kind='scatter')
- self.assertEqual(len(scatters), 3)
- self.assertEqual(scatters[0].getLegend(), 'scatter 0')
- self.assertEqual(scatters[2].getLegend(), 'scatter 2')
-
- scatters = plot._getItems(kind='scatter', just_legend=True)
- self.assertEqual(len(scatters), 3)
- self.assertEqual(list(scatters), ['scatter 0', 'scatter 1', '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 6d3eb8f..0000000
--- a/silx/gui/plot/test/testPlotWindow.py
+++ /dev/null
@@ -1,138 +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 PlotWindow"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "27/06/2017"
-
-
-import doctest
-import unittest
-
-from silx.gui.utils.testutils import TestCaseQt, getQToolButtonFromAction
-
-from silx.gui import qt
-from silx.gui.plot import PlotWindow
-
-
-# Test of the docstrings #
-
-# Makes sure a QApplication exists
-_qapp = qt.QApplication.instance() or qt.QApplication([])
-
-
-def _tearDownQt(docTest):
- """Tear down to use for test from docstring.
-
- Checks that plt widget is displayed
- """
- _qapp.processEvents()
- for obj in docTest.globs.values():
- if isinstance(obj, PlotWindow):
- # Commented out as it takes too long
- # qWaitForWindowExposedAndActivate(obj)
- obj.setAttribute(qt.Qt.WA_DeleteOnClose)
- obj.close()
- del obj
-
-
-plotWindowDocTestSuite = doctest.DocTestSuite('silx.gui.plot.PlotWindow',
- tearDown=_tearDownQt)
-"""Test suite of tests from the module's docstrings."""
-
-
-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 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 suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(plotWindowDocTestSuite)
- test_suite.addTest(
- unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotWindow))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/gui/plot/test/testProfile.py b/silx/gui/plot/test/testProfile.py
deleted file mode 100644
index 847f404..0000000
--- a/silx/gui/plot/test/testProfile.py
+++ /dev/null
@@ -1,291 +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 Profile"""
-
-__authors__ = ["T. Vincent", "P. Knobel"]
-__license__ = "MIT"
-__date__ = "17/01/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 PlotWindow, Plot1D, Plot2D, Profile
-from silx.gui.plot.StackView import StackView
-
-
-# Makes sure a QApplication exists
-_qapp = qt.QApplication.instance() or qt.QApplication([])
-
-
-class TestProfileToolBar(TestCaseQt, ParametricTestCase):
- """Tests for ProfileToolBar widget."""
-
- def setUp(self):
- super(TestProfileToolBar, self).setUp()
- profileWindow = PlotWindow()
- self.plot = PlotWindow()
- self.toolBar = Profile.ProfileToolBar(
- plot=self.plot, profileWindow=profileWindow)
- self.plot.addToolBar(self.toolBar)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
- profileWindow.show()
- self.qWaitForWindowExposed(profileWindow)
-
- 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.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
- toolButton = getQToolButtonFromAction(action)
- self.assertIsNot(toolButton, None)
- self.mouseMove(toolButton)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
-
- # 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)
-
- 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'):
- with self.subTest(method=method):
- self.toolBar.setProfileMethod(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 image in (False, True):
- with self.subTest(image=image):
- if image:
- self.plot.addImage(
- numpy.arange(100 * 100).reshape(100, -1))
-
- # Trigger tool button for diagonal profile mode
- toolButton = getQToolButtonFromAction(
- self.toolBar.lineAction)
- self.assertIsNot(toolButton, None)
- self.mouseMove(toolButton)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
- self.toolBar.lineWidthSpinBox.setValue(3)
-
- # draw profile line
- self.mouseMove(widget, pos=pos1)
- self.mousePress(widget, qt.Qt.LeftButton, pos=pos1)
- self.mouseMove(widget, pos=pos2)
- self.mouseRelease(widget, qt.Qt.LeftButton, pos=pos2)
-
- if image is True:
- profileCurve = self.toolBar.getProfilePlot().getAllCurves()[0]
- if method == 'sum':
- self.assertTrue(profileCurve.getData()[1].max() > 10000)
- elif method == 'mean':
- self.assertTrue(profileCurve.getData()[1].max() < 10000)
- self.plot.clear()
-
-
-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]]
- ]))
-
- def tearDown(self):
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- self.plot = None
-
- super(TestProfile3DToolBar, self).tearDown()
-
- def testMethodProfile1DAnd2D(self):
- """Test that the profile can have a different method if we want to
- compute then in 1D or in 2D"""
-
- _3DProfileToolbar = self.plot.getProfileToolbar()
- _2DProfilePlot = _3DProfileToolbar.getProfilePlot()
- self.plot.getProfileToolbar().setProfileMethod('mean')
- self.plot.getProfileToolbar().lineWidthSpinBox.setValue(3)
- self.assertTrue(_3DProfileToolbar.getProfileMethod() == 'mean')
-
- # check 2D 'mean' profile
- _3DProfileToolbar.profile3dAction.computeProfileIn2D()
- toolButton = getQToolButtonFromAction(_3DProfileToolbar.vLineAction)
- self.assertIsNot(toolButton, None)
- self.mouseMove(toolButton)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
- plot2D = self.plot.getPlot().getWidgetHandle()
- pos1 = plot2D.width() * 0.5, plot2D.height() * 0.5
- self.mouseClick(plot2D, qt.Qt.LeftButton, pos=pos1)
- self.assertTrue(numpy.array_equal(
- _2DProfilePlot.getActiveImage().getData(),
- numpy.array([[1, 4], [7, 10], [13, 16]])
- ))
-
- # check 1D 'sum' profile
- _2DProfileToolbar = _2DProfilePlot.getProfileToolbar()
- _2DProfileToolbar.setProfileMethod('sum')
- self.assertTrue(_2DProfileToolbar.getProfileMethod() == 'sum')
- _1DProfilePlot = _2DProfileToolbar.getProfilePlot()
-
- _2DProfileToolbar.lineWidthSpinBox.setValue(3)
- toolButton = getQToolButtonFromAction(_2DProfileToolbar.vLineAction)
- self.assertIsNot(toolButton, None)
- self.mouseMove(toolButton)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
- plot1D = _2DProfilePlot.getWidgetHandle()
- pos1 = plot1D.width() * 0.5, plot1D.height() * 0.5
- self.mouseClick(plot1D, qt.Qt.LeftButton, pos=pos1)
- self.assertTrue(numpy.array_equal(
- _1DProfilePlot.getAllCurves()[0].getData()[1],
- numpy.array([5, 17, 29])
- ))
-
- def testMethodSumLine(self):
- """Simple interaction test to make sure the sum is correctly computed
- """
- _3DProfileToolbar = self.plot.getProfileToolbar()
- _2DProfilePlot = _3DProfileToolbar.getProfilePlot()
- self.plot.getProfileToolbar().setProfileMethod('sum')
- self.plot.getProfileToolbar().lineWidthSpinBox.setValue(3)
- self.assertTrue(_3DProfileToolbar.getProfileMethod() == 'sum')
-
- # check 2D 'mean' profile
- _3DProfileToolbar.profile3dAction.computeProfileIn2D()
- toolButton = getQToolButtonFromAction(_3DProfileToolbar.lineAction)
- self.assertIsNot(toolButton, None)
- self.mouseMove(toolButton)
- self.mouseClick(toolButton, qt.Qt.LeftButton)
- plot2D = self.plot.getPlot().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)
- self.assertTrue(numpy.array_equal(
- _2DProfilePlot.getActiveImage().getData(),
- numpy.array([[3, 12], [21, 30], [39, 48]])
- ))
-
-
-class TestGetProfilePlot(TestCaseQt):
-
- def testProfile1D(self):
- plot = Plot2D()
- plot.show()
- self.qWaitForWindowExposed(plot)
- plot.addImage([[0, 1], [2, 3]])
- self.assertIsInstance(plot.getProfileToolbar().getProfileMainWindow(),
- qt.QMainWindow)
- self.assertIsInstance(plot.getProfilePlot(),
- Plot1D)
- plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- plot.close()
- del plot
-
- def testProfile2D(self):
- """Test that the profile plot associated to a stack view is either a
- Plot1D or a plot 2D instance."""
- plot = StackView()
- plot.show()
- self.qWaitForWindowExposed(plot)
-
- plot.setStack(numpy.array([[[0, 1], [2, 3]],
- [[4, 5], [6, 7]]]))
-
- self.assertIsInstance(plot.getProfileToolbar().getProfileMainWindow(),
- qt.QMainWindow)
-
- self.assertIsInstance(plot.getProfileToolbar().getProfilePlot(),
- Plot2D)
- plot.getProfileToolbar().profile3dAction.computeProfileIn1D()
- self.assertIsInstance(plot.getProfileToolbar().getProfilePlot(),
- Plot1D)
-
- plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- plot.close()
- del plot
-
-
-def suite():
- test_suite = unittest.TestSuite()
- for testClass in (TestProfileToolBar, TestGetProfilePlot,
- TestProfile3DToolBar):
- 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 85669bf..0000000
--- a/silx/gui/plot/test/testSaveAction.py
+++ /dev/null
@@ -1,125 +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.
-#
-# ###########################################################################*/
-"""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)
-
- # Update an existing file filter
- nameFilter = SaveAction.IMAGE_FILTER_EDF
- saveAction.setFileFilter('image', nameFilter, self._dummySaveFunction)
- self.assertEqual(saveAction.getFileFilters('image')[nameFilter],
- self._dummySaveFunction)
-
-
-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 a446911..0000000
--- a/silx/gui/plot/test/testScatterMaskToolsWidget.py
+++ /dev/null
@@ -1,313 +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
-
-try:
- import fabio
-except ImportError:
- fabio = None
-
-
-_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.mouseClick(plot, qt.Qt.LeftButton, pos=pos0)
- self.mouseMove(plot, pos=(0, 0))
- self.mouseMove(plot, pos=pos1)
- self.mouseClick(plot, qt.Qt.LeftButton, pos=pos1)
-
- 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.mouseClick(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 a5f649c..0000000
--- a/silx/gui/plot/test/testStackView.py
+++ /dev/null
@@ -1,252 +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 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
-
-
-# Makes sure a QApplication exists
-_qapp = qt.QApplication.instance() or qt.QApplication([])
-
-
-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 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 faedcff..0000000
--- a/silx/gui/plot/test/testStats.py
+++ /dev/null
@@ -1,562 +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 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
-from silx.gui.plot import Plot1D, Plot2D
-import unittest
-import logging
-import numpy
-
-_logger = logging.getLogger(__name__)
-
-
-class TestStats(TestCaseQt):
- """
- Test :class:`BaseClass` class and inheriting classes
- """
- def setUp(self):
- TestCaseQt.setUp(self)
- self.createCurveContext()
- self.createImageContext()
- self.createScatterContext()
-
- def tearDown(self):
- self.plot1d.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot1d.close()
- self.plot2d.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot2d.close()
- self.scatterPlot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.scatterPlot.close()
-
- 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)
-
- def createScatterContext(self):
- self.scatterPlot = Plot2D()
- lgd = 'scatter plot'
- self.xScatterData = numpy.array([0, 1, 2, 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
- )
-
- 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
- )
-
- 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()
- }
-
- def testBasicStatsCurve(self):
- """Test result for simple stats on a curve"""
- _stats = self.getBasicStats()
- xData = yData = numpy.array(range(20))
- self.assertTrue(_stats['min'].calculate(self.curveContext) == 0)
- self.assertTrue(_stats['max'].calculate(self.curveContext) == 19)
- self.assertTrue(_stats['minCoords'].calculate(self.curveContext) == [0])
- self.assertTrue(_stats['maxCoords'].calculate(self.curveContext) == [19])
- self.assertTrue(_stats['std'].calculate(self.curveContext) == numpy.std(yData))
- self.assertTrue(_stats['mean'].calculate(self.curveContext) == numpy.mean(yData))
- com = numpy.sum(xData * yData) / numpy.sum(yData)
- self.assertTrue(_stats['com'].calculate(self.curveContext) == com)
-
- def testBasicStatsImage(self):
- """Test result for simple stats on an image"""
- _stats = self.getBasicStats()
- self.assertTrue(_stats['min'].calculate(self.imageContext) == 0)
- self.assertTrue(_stats['max'].calculate(self.imageContext) == 128 * 32 - 1)
- self.assertTrue(_stats['minCoords'].calculate(self.imageContext) == (0, 0))
- self.assertTrue(_stats['maxCoords'].calculate(self.imageContext) == (127, 31))
- self.assertTrue(_stats['std'].calculate(self.imageContext) == numpy.std(self.imageData))
- self.assertTrue(_stats['mean'].calculate(self.imageContext) == numpy.mean(self.imageData))
-
- yData = numpy.sum(self.imageData, axis=1)
- xData = numpy.sum(self.imageData, 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.assertTrue(_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
- )
- _stats = self.getBasicStats()
- self.assertTrue(_stats['min'].calculate(image2Context) == 0)
- self.assertTrue(
- _stats['max'].calculate(image2Context) == 128 * 32 - 1)
- self.assertTrue(
- _stats['minCoords'].calculate(image2Context) == (100, 10))
- self.assertTrue(
- _stats['maxCoords'].calculate(image2Context) == (127*2. + 100,
- 31 * 0.5 + 10)
- )
- self.assertTrue(
- _stats['std'].calculate(image2Context) == numpy.std(
- self.imageData))
- self.assertTrue(
- _stats['mean'].calculate(image2Context) == numpy.mean(
- self.imageData))
-
- yData = numpy.sum(self.imageData, axis=1)
- xData = numpy.sum(self.imageData, axis=0)
- dataXRange = range(self.imageData.shape[1])
- dataYRange = range(self.imageData.shape[0])
-
- 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(
- _stats['com'].calculate(image2Context) == (xcom, ycom))
-
- def testBasicStatsScatter(self):
- """Test result for simple stats on a scatter"""
- _stats = self.getBasicStats()
- self.assertTrue(_stats['min'].calculate(self.scatterContext) == 5)
- self.assertTrue(_stats['max'].calculate(self.scatterContext) == 90)
- self.assertTrue(_stats['minCoords'].calculate(self.scatterContext) == (0, 2))
- self.assertTrue(_stats['maxCoords'].calculate(self.scatterContext) == (50, 69))
- self.assertTrue(_stats['std'].calculate(self.scatterContext) == numpy.std(self.valuesScatterData))
- self.assertTrue(_stats['mean'].calculate(self.scatterContext) == numpy.mean(self.valuesScatterData))
-
- comx = numpy.sum(self.xScatterData * self.valuesScatterData).astype(numpy.float32) / numpy.sum(
- self.valuesScatterData).astype(numpy.float32)
- comy = numpy.sum(self.yScatterData * self.valuesScatterData).astype(numpy.float32) / numpy.sum(
- self.valuesScatterData).astype(numpy.float32)
- self.assertTrue(numpy.all(
- numpy.equal(_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)
- self.assertTrue(stat.calculate(curveContextOnLimits) == 2)
-
- self.plot2d.getXAxis().setLimitsConstraints(minPos=32)
- imageContextOnLimits = stats._ImageContext(
- item=self.plot2d.getImage('test image'),
- plot=self.plot2d,
- onlimits=True)
- self.assertTrue(stat.calculate(imageContextOnLimits) == 32)
-
- self.scatterPlot.getXAxis().setLimitsConstraints(minPos=40)
- scatterContextOnLimits = stats._ScatterContext(
- item=self.scatterPlot.getScatter('scatter plot'),
- plot=self.scatterPlot,
- onlimits=True)
- self.assertTrue(stat.calculate(scatterContextOnLimits) == 20)
-
-
-class TestStatsFormatter(TestCaseQt):
- """Simple test to check usage of the :class:`StatsFormatter`"""
- def 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)
-
- self.stat = stats.StatMin()
-
- def tearDown(self):
- self.plot1d.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot1d.close()
-
- def testEmptyFormatter(self):
- """Make sure a formatter with no formatter definition will return a
- simple cast to str"""
- emptyFormatter = statshandler.StatFormatter()
- self.assertTrue(
- 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.assertTrue(
- formatter.format(self.stat.calculate(self.curveContext)) == '0.000')
-
-
-class TestStatsHandler(unittest.TestCase):
- """Make sure the StatHandler is correctly making the link between
- :class:`StatBase` and :class:`StatFormatter` and checking the API is valid
- """
- def 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):
- self.plot1d.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot1d.close()
-
- 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.assertTrue(res['min'] == '0')
- self.assertTrue('max' in res)
- self.assertTrue(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.assertTrue(res['min'] == '0')
- self.assertTrue('max' in res)
- self.assertTrue(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.assertTrue(res['min'] == '0')
- self.assertTrue('max' in res)
- self.assertTrue(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.assertTrue(res['amin'] == '0.000')
- self.assertTrue('amax' in res)
- self.assertTrue(res['amax'] == '19')
-
- with self.assertRaises(ValueError):
- statshandler.StatsHandler(('name'))
-
-
-class TestStatsWidgetWithCurves(TestCaseQt):
- """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.StatsTable(plot=self.plot)
-
- 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):
- 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 testInit(self):
- """Make sure all the curves are registred on initialization"""
- self.assertTrue(self.widget.rowCount() is 3)
-
- def testRemoveCurve(self):
- """Make sure the Curves stats take into account the curve removal from
- plot"""
- self.plot.removeCurve('curve2')
- self.assertTrue(self.widget.rowCount() is 2)
- for iRow in range(2):
- self.assertTrue(self.widget.item(iRow, 0).text() in ('curve0', 'curve1'))
-
- self.plot.removeCurve('curve0')
- self.assertTrue(self.widget.rowCount() is 1)
- self.plot.removeCurve('curve1')
- self.assertTrue(self.widget.rowCount() is 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.assertTrue(self.widget.rowCount() is 4)
-
- def testUpdateCurveFrmAddCurve(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.assertTrue(self.widget.rowCount() is 3)
- itemMax = self.widget._getItem(name='max', legend='curve0',
- kind='curve', indexTable=None)
- self.assertTrue(itemMax.text() == '9')
-
- def testUpdateCurveFrmCurveObj(self):
- self.plot.getCurve('curve0').setData(x=range(4), y=range(4))
- self.assertTrue(self.widget.rowCount() is 3)
- itemMax = self.widget._getItem(name='max', legend='curve0',
- kind='curve', indexTable=None)
- self.assertTrue(itemMax.text() == '3')
-
- def testSetAnotherPlot(self):
- plot2 = Plot1D()
- plot2.addCurve(x=range(26), y=range(26), legend='new curve')
- self.widget.setPlot(plot2)
- self.assertTrue(self.widget.rowCount() is 1)
- self.qapp.processEvents()
- plot2.setAttribute(qt.Qt.WA_DeleteOnClose)
- plot2.close()
- plot2 = None
-
-
-class TestStatsWidgetWithImages(TestCaseQt):
- """Basic test for StatsWidget with images"""
- def setUp(self):
- TestCaseQt.setUp(self)
- self.plot = Plot2D()
-
- self.plot.addImage(data=numpy.arange(128*128).reshape(128, 128),
- legend='test image', 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):
- 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):
- columnsIndex = self.widget._columns_index
- itemLegend = self.widget._lgdAndKindToItems[('test image', 'image')]['legend']
- itemMin = self.widget.item(itemLegend.row(), columnsIndex['min'])
- itemMax = self.widget.item(itemLegend.row(), columnsIndex['max'])
- itemDelta = self.widget.item(itemLegend.row(), columnsIndex['delta'])
- itemCoordsMin = self.widget.item(itemLegend.row(),
- columnsIndex['coords min'])
- itemCoordsMax = self.widget.item(itemLegend.row(),
- columnsIndex['coords max'])
- max = (128 * 128) - 1
- self.assertTrue(itemMin.text() == '0.000')
- self.assertTrue(itemMax.text() == '{0:.3f}'.format(max))
- self.assertTrue(itemDelta.text() == '{0:.3f}'.format(max))
- self.assertTrue(itemCoordsMin.text() == '0.0, 0.0')
- self.assertTrue(itemCoordsMax.text() == '127.0, 127.0')
-
-
-class TestStatsWidgetWithScatters(TestCaseQt):
- 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='scatter plot')
- 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):
- 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):
- columnsIndex = self.widget._columns_index
- itemLegend = self.widget._lgdAndKindToItems[('scatter plot', 'scatter')]['legend']
- itemMin = self.widget.item(itemLegend.row(), columnsIndex['min'])
- itemMax = self.widget.item(itemLegend.row(), columnsIndex['max'])
- itemDelta = self.widget.item(itemLegend.row(), columnsIndex['delta'])
- itemCoordsMin = self.widget.item(itemLegend.row(),
- columnsIndex['coords min'])
- itemCoordsMax = self.widget.item(itemLegend.row(),
- columnsIndex['coords max'])
- self.assertTrue(itemMin.text() == '5')
- self.assertTrue(itemMax.text() == '90')
- self.assertTrue(itemDelta.text() == '85')
- self.assertTrue(itemCoordsMin.text() == '0, 2')
- self.assertTrue(itemCoordsMax.text() == '50, 69')
-
-
-class TestEmptyStatsWidget(TestCaseQt):
- def test(self):
- widget = StatsWidget.StatsWidget()
- widget.show()
-
-
-def suite():
- test_suite = unittest.TestSuite()
- for TestClass in (TestStats, TestStatsHandler, TestStatsWidgetWithScatters,
- TestStatsWidgetWithImages, TestStatsWidgetWithCurves,
- TestStatsFormatter, TestEmptyStatsWidget):
- 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 016fafe..0000000
--- a/silx/gui/plot/test/testUtilsAxis.py
+++ /dev/null
@@ -1,167 +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__ = "14/02/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 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 7b63b29..0000000
--- a/silx/gui/plot/tools/CurveLegendsWidget.py
+++ /dev/null
@@ -1,247 +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 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.getLegend())
- 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.getLegend())
-
- def _itemRemoved(self, item):
- """Handle item removed from the plot content"""
- if isinstance(item, items.Curve):
- self._removeLegend(item.getLegend())
-
- 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 83b61bd..0000000
--- a/silx/gui/plot/tools/PositionInfo.py
+++ /dev/null
@@ -1,347 +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 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
-
-
-_logger = logging.getLogger(__name__)
-
-
-# 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 = qt.QLabel()
- contentWidget.setText('------')
- contentWidget.setTextInteractionFlags(qt.Qt.TextSelectableByMouse)
- contentWidget.setFixedWidth(
- contentWidget.fontMetrics().width('##############'))
- 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('curve')
- if snappingMode & self.SNAPPING_SCATTER:
- kinds.append('scatter')
- selectedItems = plot._getItems(kind=kinds)
-
- # 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 item.getSymbol()):
- # Only handled if item symbols are visible
- continue
-
- 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/__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 fd21515..0000000
--- a/silx/gui/plot/tools/profile/ScatterProfileToolBar.py
+++ /dev/null
@@ -1,431 +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 profile tools for scatter plots.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import logging
-import threading
-import time
-
-import numpy
-
-try:
- from scipy.interpolate import LinearNDInterpolator
-except ImportError:
- LinearNDInterpolator = None
-
- # Fallback using local Delaunay and matplotlib interpolator
- from silx.third_party.scipy_spatial import Delaunay
- import matplotlib.tri
-
-from ._BaseProfileToolBar import _BaseProfileToolBar
-from .... import qt
-from ... import items
-
-
-_logger = logging.getLogger(__name__)
-
-
-# TODO support log scale
-
-
-class _InterpolatorInitThread(qt.QThread):
- """Thread building a scatter interpolator
-
- This works in greedy mode in that the signal is only emitted
- when no other request is pending
- """
-
- sigInterpolatorReady = qt.Signal(object)
- """Signal emitted whenever an interpolator is ready
-
- It provides a 3-tuple (points, values, interpolator)
- """
-
- _RUNNING_THREADS_TO_DELETE = []
- """Store reference of no more used threads but still running"""
-
- def __init__(self):
- super(_InterpolatorInitThread, self).__init__()
- self._lock = threading.RLock()
- self._pendingData = None
- self._firstFallbackRun = True
-
- def discard(self, obj=None):
- """Wait for pending thread to complete and delete then
-
- Connect this to the destroyed signal of widget using this thread
- """
- if self.isRunning():
- self.cancel()
- self._RUNNING_THREADS_TO_DELETE.append(self) # Keep a reference
- self.finished.connect(self.__finished)
-
- def __finished(self):
- """Handle finished signal of threads to delete"""
- try:
- self._RUNNING_THREADS_TO_DELETE.remove(self)
- except ValueError:
- _logger.warning('Finished thread no longer in reference list')
-
- def request(self, points, values):
- """Request new initialisation of interpolator
-
- :param numpy.ndarray points: Point coordinates (N, D)
- :param numpy.ndarray values: Values the N points (1D array)
- """
- with self._lock:
- # Possibly replace already pending data
- self._pendingData = points, values
-
- if not self.isRunning():
- self.start()
-
- def cancel(self):
- """Cancel any running/pending requests"""
- with self._lock:
- self._pendingData = 'cancelled'
-
- def run(self):
- """Run the init of the scatter interpolator"""
- if LinearNDInterpolator is None:
- self.run_matplotlib()
- else:
- self.run_scipy()
-
- def run_matplotlib(self):
- """Run the init of the scatter interpolator"""
- if self._firstFallbackRun:
- self._firstFallbackRun = False
- _logger.warning(
- "scipy.spatial.LinearNDInterpolator not available: "
- "Scatter plot interpolator initialisation can freeze the GUI.")
-
- while True:
- with self._lock:
- data = self._pendingData
- self._pendingData = None
-
- if data in (None, 'cancelled'):
- return
-
- points, values = data
-
- startTime = time.time()
- try:
- delaunay = Delaunay(points)
- except:
- _logger.warning(
- "Cannot triangulate scatter data")
- else:
- with self._lock:
- data = self._pendingData
-
- if data is not None: # Break point
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
-
- x, y = points.T
- triangulation = matplotlib.tri.Triangulation(
- x, y, triangles=delaunay.simplices)
-
- interpolator = matplotlib.tri.LinearTriInterpolator(
- triangulation, values)
-
- with self._lock:
- data = self._pendingData
-
- if data is not None:
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
- # No other processing requested: emit the signal
- _logger.info("Interpolator initialised in %f s",
- time.time() - startTime)
-
- # Wrap interpolator to have same API as scipy's one
- def wrapper(points):
- return interpolator(*points.T)
-
- self.sigInterpolatorReady.emit(
- (points, values, wrapper))
-
- def run_scipy(self):
- """Run the init of the scatter interpolator"""
- while True:
- with self._lock:
- data = self._pendingData
- self._pendingData = None
-
- if data in (None, 'cancelled'):
- return
-
- points, values = data
-
- startTime = time.time()
- try:
- interpolator = LinearNDInterpolator(points, values)
- except:
- _logger.warning(
- "Cannot initialise scatter profile interpolator")
- else:
- with self._lock:
- data = self._pendingData
-
- if data is not None: # Break point
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
- # First call takes a while, do it here
- interpolator([(0., 0.)])
-
- with self._lock:
- data = self._pendingData
-
- if data is not None:
- _logger.info('Interpolator discarded after %f s',
- time.time() - startTime)
- else:
- # No other processing requested: emit the signal
- _logger.info("Interpolator initialised in %f s",
- time.time() - startTime)
- self.sigInterpolatorReady.emit(
- (points, values, interpolator))
-
-
-class ScatterProfileToolBar(_BaseProfileToolBar):
- """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='Scatter Profile'):
- super(ScatterProfileToolBar, self).__init__(parent, plot, title)
-
- self.__nPoints = 1024
- self.__interpolator = None
- self.__interpolatorCache = None # points, values, interpolator
-
- self.__initThread = _InterpolatorInitThread()
- self.destroyed.connect(self.__initThread.discard)
- self.__initThread.sigInterpolatorReady.connect(
- self.__interpolatorReady)
-
- roiManager = self._getRoiManager()
- if roiManager is None:
- _logger.error(
- "Error during scatter profile toolbar initialisation")
- else:
- roiManager.sigInteractiveModeStarted.connect(
- self.__interactionStarted)
- roiManager.sigInteractiveModeFinished.connect(
- self.__interactionFinished)
- if roiManager.isStarted():
- self.__interactionStarted(roiManager.getCurrentInteractionModeRoiClass())
-
- def __interactionStarted(self, roiClass):
- """Handle start of ROI interaction"""
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- plot.sigActiveScatterChanged.connect(self.__activeScatterChanged)
-
- scatter = plot._getActiveItem(kind='scatter')
- legend = None if scatter is None else scatter.getLegend()
- self.__activeScatterChanged(None, legend)
-
- def __interactionFinished(self):
- """Handle end of ROI interaction"""
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- plot.sigActiveScatterChanged.disconnect(self.__activeScatterChanged)
-
- scatter = plot._getActiveItem(kind='scatter')
- legend = None if scatter is None else scatter.getLegend()
- self.__activeScatterChanged(legend, None)
-
- def __activeScatterChanged(self, previous, legend):
- """Handle change of active scatter
-
- :param Union[str,None] previous:
- :param Union[str,None] legend:
- """
- self.__initThread.cancel()
-
- # Reset interpolator
- self.__interpolator = None
-
- plot = self.getPlotWidget()
- if plot is None:
- _logger.error("Associated PlotWidget no longer exists")
-
- else:
- if previous is not None: # Disconnect signal
- scatter = plot.getScatter(previous)
- if scatter is not None:
- scatter.sigItemChanged.disconnect(
- self.__scatterItemChanged)
-
- if legend is not None:
- scatter = plot.getScatter(legend)
- if scatter is None:
- _logger.error("Cannot retrieve active scatter")
-
- else:
- scatter.sigItemChanged.connect(self.__scatterItemChanged)
- points = numpy.transpose(numpy.array((
- scatter.getXData(copy=False),
- scatter.getYData(copy=False))))
- values = scatter.getValueData(copy=False)
-
- self.__updateInterpolator(points, values)
-
- # Refresh profile
- self.updateProfile()
-
- def __scatterItemChanged(self, event):
- """Handle update of active scatter plot item
-
- :param ItemChangedType event:
- """
- if event == items.ItemChangedType.DATA:
- self.__interpolator = None
- scatter = self.sender()
- if scatter is None:
- _logger.error("Cannot retrieve updated scatter item")
-
- else:
- points = numpy.transpose(numpy.array((
- scatter.getXData(copy=False),
- scatter.getYData(copy=False))))
- values = scatter.getValueData(copy=False)
-
- self.__updateInterpolator(points, values)
-
- # Handle interpolator init thread
-
- def __updateInterpolator(self, points, values):
- """Update used interpolator with new data"""
- if (self.__interpolatorCache is not None and
- len(points) == len(self.__interpolatorCache[0]) and
- numpy.all(numpy.equal(self.__interpolatorCache[0], points)) and
- numpy.all(numpy.equal(self.__interpolatorCache[1], values))):
- # Reuse previous interpolator
- _logger.info(
- 'Scatter changed: Reuse previous interpolator')
- self.__interpolator = self.__interpolatorCache[2]
-
- else:
- # Interpolator needs update: Start background processing
- _logger.info(
- 'Scatter changed: Rebuild interpolator')
- self.__interpolator = None
- self.__interpolatorCache = None
- self.__initThread.request(points, values)
-
- def __interpolatorReady(self, data):
- """Handle end of init interpolator thread"""
- points, values, interpolator = data
- self.__interpolator = interpolator
- self.__interpolatorCache = None if interpolator is None else data
- self.updateProfile()
-
- def hasPendingOperations(self):
- return self.__initThread.isRunning()
-
- # 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)
- else:
- self.__nPoints = npoints
-
- # Overridden methods
-
- def computeProfileTitle(self, x0, y0, x1, y1):
- """Compute corresponding plot title
-
- :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 self.hasPendingOperations():
- return 'Pre-processing data...'
-
- else:
- return super(ScatterProfileToolBar, self).computeProfileTitle(
- x0, y0, x1, y1)
-
- def computeProfile(self, 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
- """
- if self.__interpolator is None:
- return None
-
- nPoints = self.getNPoints()
-
- points = numpy.transpose((
- numpy.linspace(x0, x1, nPoints, endpoint=True),
- numpy.linspace(y0, y1, nPoints, endpoint=True)))
-
- values = self.__interpolator(points)
-
- if not numpy.any(numpy.isfinite(values)):
- return None # Profile outside convex hull
-
- return points, values
diff --git a/silx/gui/plot/tools/profile/_BaseProfileToolBar.py b/silx/gui/plot/tools/profile/_BaseProfileToolBar.py
deleted file mode 100644
index 6d9d6d4..0000000
--- a/silx/gui/plot/tools/profile/_BaseProfileToolBar.py
+++ /dev/null
@@ -1,430 +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 the base class for profile toolbars."""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import logging
-import weakref
-
-import numpy
-
-from silx.utils.weakref import WeakMethodProxy
-from silx.gui import qt, icons, colors
-from silx.gui.plot import PlotWidget, items
-from silx.gui.plot.ProfileMainWindow import ProfileMainWindow
-from silx.gui.plot.tools.roi import RegionOfInterestManager
-from silx.gui.plot.items import roi as roi_items
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _BaseProfileToolBar(qt.QToolBar):
- """Base class for QToolBar 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`.
- """
-
- sigProfileChanged = qt.Signal()
- """Signal emitted when the profile has changed"""
-
- def __init__(self, parent=None, plot=None, title=''):
- super(_BaseProfileToolBar, self).__init__(title, parent)
-
- self.__profile = None
- self.__profileTitle = ''
-
- assert isinstance(plot, PlotWidget)
- self._plotRef = weakref.ref(
- plot, WeakMethodProxy(self.__plotDestroyed))
-
- self._profileWindow = None
-
- # Set-up interaction manager
- roiManager = RegionOfInterestManager(plot)
- self._roiManagerRef = weakref.ref(roiManager)
-
- roiManager.sigInteractiveModeFinished.connect(self.__interactionFinished)
- roiManager.sigRoiChanged.connect(self.updateProfile)
- roiManager.sigRoiAdded.connect(self.__roiAdded)
-
- # Add interactive mode actions
- for kind, icon, tooltip in (
- (roi_items.HorizontalLineROI, 'shape-horizontal',
- 'Enables horizontal line profile selection mode'),
- (roi_items.VerticalLineROI, 'shape-vertical',
- 'Enables vertical line profile selection mode'),
- (roi_items.LineROI, 'shape-diagonal',
- 'Enables line profile selection mode')):
- action = roiManager.getInteractionModeAction(kind)
- action.setIcon(icons.getQIcon(icon))
- action.setToolTip(tooltip)
- self.addAction(action)
-
- # Add clear action
- action = qt.QAction(icons.getQIcon('profile-clear'),
- 'Clear Profile', self)
- action.setToolTip('Clear the profile')
- action.setCheckable(False)
- action.triggered.connect(self.clearProfile)
- self.addAction(action)
-
- # Initialize color
- self._color = None
- self.setColor('red')
-
- # Listen to plot limits changed
- plot.getXAxis().sigLimitsChanged.connect(self.updateProfile)
- plot.getYAxis().sigLimitsChanged.connect(self.updateProfile)
-
- # Listen to plot scale
- plot.getXAxis().sigScaleChanged.connect(self.__plotAxisScaleChanged)
- plot.getYAxis().sigScaleChanged.connect(self.__plotAxisScaleChanged)
-
- self.setDefaultProfileWindowEnabled(True)
-
- def getProfilePoints(self, copy=True):
- """Returns the profile sampling points as (x, y) or None
-
- :param bool copy: True to get a copy,
- False to get internal arrays (do not modify)
- :rtype: Union[numpy.ndarray,None]
- """
- if self.__profile is None:
- return None
- else:
- return numpy.array(self.__profile[0], copy=copy)
-
- def getProfileValues(self, copy=True):
- """Returns the values of the profile or None
-
- :param bool copy: True to get a copy,
- False to get internal arrays (do not modify)
- :rtype: Union[numpy.ndarray,None]
- """
- if self.__profile is None:
- return None
- else:
- return numpy.array(self.__profile[1], copy=copy)
-
- def getProfileTitle(self):
- """Returns the profile title
-
- :rtype: str
- """
- return self.__profileTitle
-
- # Handle plot reference
-
- def __plotDestroyed(self, ref):
- """Handle finalization of PlotWidget
-
- :param ref: weakref to the plot
- """
- self._plotRef = None
- self.setEnabled(False) # Profile is pointless
- for action in self.actions(): # TODO useful?
- self.removeAction(action)
-
- def getPlotWidget(self):
- """The :class:`~silx.gui.plot.PlotWidget` associated to the toolbar.
-
- :rtype: Union[~silx.gui.plot.PlotWidget,None]
- """
- return None if self._plotRef is None else self._plotRef()
-
- def _getRoiManager(self):
- """Returns the used ROI manager
-
- :rtype: RegionOfInterestManager
- """
- return self._roiManagerRef()
-
- # Profile Plot
-
- def isDefaultProfileWindowEnabled(self):
- """Returns True if the default floating profile window is used
-
- :rtype: bool
- """
- return self.getDefaultProfileWindow() is not None
-
- def setDefaultProfileWindowEnabled(self, enabled):
- """Set whether to use or not the default floating profile window.
-
- :param bool enabled: True to use, False to disable
- """
- if self.isDefaultProfileWindowEnabled() != enabled:
- if enabled:
- self._profileWindow = ProfileMainWindow(self)
- self._profileWindow.sigClose.connect(self.clearProfile)
- self.sigProfileChanged.connect(self.__updateDefaultProfilePlot)
-
- else:
- self.sigProfileChanged.disconnect(self.__updateDefaultProfilePlot)
- self._profileWindow.sigClose.disconnect(self.clearProfile)
- self._profileWindow.close()
- self._profileWindow = None
-
- def getDefaultProfileWindow(self):
- """Returns the default floating profile window if in use else None.
-
- See :meth:`isDefaultProfileWindowEnabled`
-
- :rtype: Union[ProfileMainWindow,None]
- """
- return self._profileWindow
-
- def __updateDefaultProfilePlot(self):
- """Update the plot of the default profile window"""
- profileWindow = self.getDefaultProfileWindow()
- if profileWindow is None:
- return
-
- profilePlot = profileWindow.getPlot()
- if profilePlot is None:
- return
-
- profilePlot.clear()
- profilePlot.setGraphTitle(self.getProfileTitle())
-
- points = self.getProfilePoints(copy=False)
- values = self.getProfileValues(copy=False)
-
- if points is not None and values is not None:
- if (numpy.abs(points[-1, 0] - points[0, 0]) >
- numpy.abs(points[-1, 1] - points[0, 1])):
- xProfile = points[:, 0]
- profilePlot.getXAxis().setLabel('X')
- else:
- xProfile = points[:, 1]
- profilePlot.getXAxis().setLabel('Y')
-
- profilePlot.addCurve(
- xProfile, values, legend='Profile', color=self._color)
-
- self._showDefaultProfileWindow()
-
- def _showDefaultProfileWindow(self):
- """If profile window was created by this toolbar,
- try to avoid overlapping with the toolbar's parent window.
- """
- profileWindow = self.getDefaultProfileWindow()
- roiManager = self._getRoiManager()
- if profileWindow is None or roiManager is None:
- return
-
- if roiManager.isStarted() and not profileWindow.isVisible():
- profileWindow.show()
- profileWindow.raise_()
-
- window = self.window()
- winGeom = window.frameGeometry()
- qapp = qt.QApplication.instance()
- desktop = qapp.desktop()
- screenGeom = desktop.availableGeometry(self)
- spaceOnLeftSide = winGeom.left()
- spaceOnRightSide = screenGeom.width() - winGeom.right()
-
- frameGeometry = profileWindow.frameGeometry()
- profileWindowWidth = frameGeometry.width()
- if profileWindowWidth < spaceOnRightSide:
- # Place profile on the right
- profileWindow.move(winGeom.right(), winGeom.top())
- elif profileWindowWidth < spaceOnLeftSide:
- # Place profile on the left
- profileWindow.move(
- max(0, winGeom.left() - profileWindowWidth), winGeom.top())
-
- # Handle plot in log scale
-
- def __plotAxisScaleChanged(self, scale):
- """Handle change of axis scale in the plot widget"""
- plot = self.getPlotWidget()
- if plot is None:
- return
-
- xScale = plot.getXAxis().getScale()
- yScale = plot.getYAxis().getScale()
-
- if xScale == items.Axis.LINEAR and yScale == items.Axis.LINEAR:
- self.setEnabled(True)
-
- else:
- roiManager = self._getRoiManager()
- if roiManager is not None:
- roiManager.stop() # Stop interactive mode
-
- self.clearProfile()
- self.setEnabled(False)
-
- # Profile color
-
- def getColor(self):
- """Returns the color used for the profile and ROI
-
- :rtype: QColor
- """
- return qt.QColor.fromRgbF(*self._color)
-
- def setColor(self, color):
- """Set the color to use for ROI and profile.
-
- :param color:
- Either a color name, a QColor, a list of uint8 or float in [0, 1].
- """
- self._color = colors.rgba(color)
- roiManager = self._getRoiManager()
- if roiManager is not None:
- roiManager.setColor(self._color)
- for roi in roiManager.getRois():
- roi.setColor(self._color)
- self.updateProfile()
-
- # Handle ROI manager
-
- def __interactionFinished(self):
- """Handle end of interactive mode"""
- self.clearProfile()
-
- profileWindow = self.getDefaultProfileWindow()
- if profileWindow is not None:
- profileWindow.hide()
-
- def __roiAdded(self, roi):
- """Handle new ROI"""
- roi.setLabel('Profile')
- roi.setEditable(True)
-
- # Remove any other ROI
- roiManager = self._getRoiManager()
- if roiManager is not None:
- for regionOfInterest in list(roiManager.getRois()):
- if regionOfInterest is not roi:
- roiManager.removeRoi(regionOfInterest)
-
- def computeProfile(self, x0, y0, x1, y1):
- """Compute corresponding profile
-
- Override in subclass to compute 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
- """
- return None
-
- def computeProfileTitle(self, 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 = 'X = %g; Y = [%g, %g]' % (x0, y0, y1)
- elif y0 == y1:
- title = 'Y = %g; X = [%g, %g]' % (y0, x0, x1)
- else:
- m = (y1 - y0) / (x1 - x0)
- b = y0 - m * x0
- title = 'Y = %g * X %+g' % (m, b)
-
- return title
-
- def updateProfile(self):
- """Update profile according to current ROI"""
- roiManager = self._getRoiManager()
- if roiManager is None:
- roi = None
- else:
- rois = roiManager.getRois()
- roi = None if len(rois) == 0 else rois[0]
-
- if roi is None:
- self._setProfile(profile=None, title='')
- return
-
- # Get end points
- if isinstance(roi, roi_items.LineROI):
- points = roi.getEndPoints()
- x0, y0 = points[0]
- x1, y1 = points[1]
- elif isinstance(roi, (roi_items.VerticalLineROI, roi_items.HorizontalLineROI)):
- plot = self.getPlotWidget()
- if plot is None:
- self._setProfile(profile=None, title='')
- return
-
- elif isinstance(roi, roi_items.HorizontalLineROI):
- x0, x1 = plot.getXAxis().getLimits()
- y0 = y1 = roi.getPosition()
-
- elif isinstance(roi, roi_items.VerticalLineROI):
- x0 = x1 = roi.getPosition()
- y0, y1 = plot.getYAxis().getLimits()
-
- else:
- raise RuntimeError('Unsupported ROI for profile: {}'.format(roi.__class__))
-
- if x1 < x0 or (x1 == x0 and y1 < y0):
- # Invert points
- x0, y0, x1, y1 = x1, y1, x0, y0
-
- profile = self.computeProfile(x0, y0, x1, y1)
- title = self.computeProfileTitle(x0, y0, x1, y1)
- self._setProfile(profile=profile, title=title)
-
- def _setProfile(self, profile=None, title=''):
- """Set profile data and emit signal.
-
- :param profile: points and profile values
- :param str title:
- """
- self.__profile = profile
- self.__profileTitle = title
-
- self.sigProfileChanged.emit()
-
- def clearProfile(self):
- """Clear the current line ROI and associated profile"""
- roiManager = self._getRoiManager()
- if roiManager is not None:
- roiManager.clear()
-
- self._setProfile(profile=None, title='')
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/roi.py b/silx/gui/plot/tools/roi.py
deleted file mode 100644
index d58c041..0000000
--- a/silx/gui/plot/tools/roi.py
+++ /dev/null
@@ -1,934 +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 ROI interaction for :class:`~silx.gui.plot.PlotWidget`.
-"""
-
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "28/06/2018"
-
-
-import collections
-import functools
-import logging
-import time
-import weakref
-
-import numpy
-
-from ....third_party import enum
-from ....utils.weakref import WeakMethodProxy
-from ... import qt, icons
-from .. import PlotWidget
-from ..items import roi as roi_items
-
-from ...colors import rgba
-
-
-logger = logging.getLogger(__name__)
-
-
-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."""
-
- 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.
- """
-
- sigInteractiveModeFinished = qt.Signal()
- """Signal emitted when leaving and interactive ROI drawing.
-
- It provides the list of ROIs.
- """
-
- _MODE_ACTIONS_PARAMS = collections.OrderedDict()
- # Interactive mode: (icon name, text)
- _MODE_ACTIONS_PARAMS[roi_items.PointROI] = 'add-shape-point', 'Add point markers'
- _MODE_ACTIONS_PARAMS[roi_items.RectangleROI] = 'add-shape-rectangle', 'Add rectangle ROI'
- _MODE_ACTIONS_PARAMS[roi_items.PolygonROI] = 'add-shape-polygon', 'Add polygon ROI'
- _MODE_ACTIONS_PARAMS[roi_items.LineROI] = 'add-shape-diagonal', 'Add line ROI'
- _MODE_ACTIONS_PARAMS[roi_items.HorizontalLineROI] = 'add-shape-horizontal', 'Add horizontal line ROI'
- _MODE_ACTIONS_PARAMS[roi_items.VerticalLineROI] = 'add-shape-vertical', 'Add vertical line ROI'
- _MODE_ACTIONS_PARAMS[roi_items.ArcROI] = 'add-shape-arc', 'Add arc ROI'
-
- 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._color = rgba('red')
-
- self._label = "__RegionOfInterestManager__%d" % id(self)
-
- self._eventLoop = None
-
- self._modeActions = {}
-
- parent.sigInteractiveModeChanged.connect(
- self._plotInteractiveModeChanged)
-
- @classmethod
- def getSupportedRoiClasses(cls):
- """Returns the default available ROI classes
-
- :rtype: List[class]
- """
- return tuple(cls._MODE_ACTIONS_PARAMS.keys())
-
- # 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 str roiClass: The ROI class which will be crated 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
- if roiClass in self._MODE_ACTIONS_PARAMS:
- iconName, text = self._MODE_ACTIONS_PARAMS[roiClass]
- else:
- iconName = "add-shape-unknown"
- name = roiClass._getKind()
- if name is None:
- name = roiClass.__name__
- text = 'Add %s' % name
- action = qt.QAction(self)
- action.setIcon(icons.getQIcon(iconName))
- action.setText(text)
- action.setCheckable(True)
- action.setChecked(self.getCurrentInteractionModeRoiClass() is roiClass)
- action.setToolTip(text)
-
- action.triggered[bool].connect(functools.partial(
- WeakMethodProxy(self._modeActionTriggered), roiClass=roiClass))
- self._modeActions[roiClass] = action
- return action
-
- def _modeActionTriggered(self, checked, roiClass):
- """Handle mode actions being checked by the user
-
- :param bool checked:
- :param str kind: Corresponding shape kind
- """
- if checked:
- self.start(roiClass)
- else: # Keep action checked
- action = self.sender()
- action.setChecked(True)
-
- def _updateModeActions(self):
- """Check/Uncheck action corresponding to current mode"""
- for roiClass, action in self._modeActions.items():
- action.setChecked(roiClass == self.getCurrentInteractionModeRoiClass())
-
- # PlotWidget eventFilter and listeners
-
- def _plotInteractiveModeChanged(self, source):
- """Handle change of interactive mode in the plot"""
- if source is not self:
- self.__roiInteractiveModeEnded()
-
- else: # Check the corresponding action
- self._updateModeActions()
-
- # 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)
- self.createRoi(roiClass, points=points)
-
- 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
- self._drawnROI = self.createRoi(roiClass, points=points)
- else:
- self._drawnROI.setFirstShapePoints(points)
-
- if event['event'] == 'drawingFinished':
- if kind == 'polygon' and len(points) > 1:
- self._drawnROI.setFirstShapePoints(points[:-1])
- self._drawnROI = None # Stop drawing
-
- # 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):
- """Handle ROI object changed"""
- self.sigRoiChanged.emit()
-
- def createRoi(self, roiClass, points, label='', index=None):
- """Create a new ROI and add it to list of ROIs.
-
- :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)
- roi.setLabel(str(label))
- roi.setFirstShapePoints(points)
-
- self.addRoi(roi, index)
- return roi
-
- 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
- :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)
-
- 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')
-
- self.sigRoiAboutToBeRemoved.emit(roi)
-
- self._rois.remove(roi)
- roi.sigRegionChanged.disconnect(self._regionOfInterestChanged)
- roi.setParent(None)
- self._roisUpdated()
-
- 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 isStarted(self):
- """Returns True if an interactive ROI drawing mode is active.
-
- :rtype: bool
- """
- return self._roiClass is not None
-
- def start(self, roiClass):
- """Start an interactive ROI drawing mode.
-
- :param class roiClass: The ROI class to create. It have to inherite from
- `roi_items.RegionOfInterest`.
- :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
- 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)
-
- plot.sigPlotSignal.connect(self._handleInteraction)
-
- self.sigInteractiveModeStarted.emit(roiClass)
-
- return True
-
- def __roiInteractiveModeEnded(self):
- """Handle end of ROI draw interactive mode"""
- if self.isStarted():
- self._roiClass = None
-
- if self._drawnROI is not None:
- # Cancel ROI create
- self.removeRoi(self._drawnROI)
- self._drawnROI = None
-
- plot = self.parent()
- if plot is not None:
- plot.sigPlotSignal.disconnect(self._handleInteraction)
-
- self._updateModeActions()
-
- 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.setInteractiveMode(mode='zoom', source=None)
- 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())
-
- kind = self.__execClass._getKind()
- max_ = self.getMaxRois()
-
- if max_ is None:
- message = 'Select %ss (%d selected)' % (kind, nbrois)
-
- elif max_ <= 1:
- message = 'Select a %s' % kind
- else:
- message = 'Select %d/%d %ss' % (nbrois, max_, kind)
-
- 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
-
- self.setColumnCount(5)
- self.setHorizontalHeaderLabels(
- ['Label', 'Edit', 'Kind', 'Coordinates', ''])
-
- 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)
-
- @staticmethod
- def __itemChanged(item):
- """Handle item updates"""
- column = item.column()
- roi = item.data(qt.Qt.UserRole)
- if column == 0:
- roi.setLabel(item.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
- label = roi.getLabel()
- item = qt.QTableWidgetItem(label)
- item.setFlags(baseFlags | qt.Qt.ItemIsEditable)
- item.setData(qt.Qt.UserRole, roi)
- self.setItem(index, 0, item)
-
- # Editable
- item = qt.QTableWidgetItem()
- item.setFlags(baseFlags | qt.Qt.ItemIsUserCheckable)
- item.setData(qt.Qt.UserRole, roi)
- 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._getKind()
- 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 9cede27..0000000
--- a/silx/gui/plot/tools/test/__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.
-#
-# ###########################################################################*/
-__authors__ = ["T. Vincent"]
-__license__ = "MIT"
-__date__ = "26/03/2018"
-
-
-import unittest
-
-from . import testROI
-from . import testTools
-from . import testScatterProfileToolBar
-from . import testCurveLegendsWidget
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTests(
- [testROI.suite(),
- testTools.suite(),
- testScatterProfileToolBar.suite(),
- testCurveLegendsWidget.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/testROI.py b/silx/gui/plot/tools/test/testROI.py
deleted file mode 100644
index 8aec1d9..0000000
--- a/silx/gui/plot/tools/test/testROI.py
+++ /dev/null
@@ -1,456 +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.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.VerticalLineROI()
- 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 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 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.assertAlmostEqual(item.isClosed(), True)
-
- 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.assertAlmostEqual(item.isClosed(), True)
-
- 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)
-
-
-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.))))),
- )
-
- 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
- manager.createRoi(roiClass, points[0])
- 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
- manager.createRoi(roiClass, points[0])
- self.qapp.processEvents()
- manager.createRoi(roiClass, points[1])
- 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
- manager.createRoi(roiClass, points[0])
- self.qapp.processEvents()
- manager.createRoi(roiClass, points[1])
- 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)
-
- 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 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 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 b99cac7..0000000
--- a/silx/gui/plot/tools/test/testScatterProfileToolBar.py
+++ /dev/null
@@ -1,216 +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 import profile
-import silx.gui.plot.items.roi as roi_items
-
-
-class TestScatterProfileToolBar(TestCaseQt, ParametricTestCase):
- """Tests for ScatterProfileToolBar class"""
-
- def setUp(self):
- super(TestScatterProfileToolBar, self).setUp()
- self.plot = PlotWindow()
-
- self.profile = profile.ScatterProfileToolBar(plot=self.plot)
-
- self.plot.addToolBar(self.profile)
-
- self.plot.show()
- self.qWaitForWindowExposed(self.plot)
-
- def tearDown(self):
- del self.profile
- self.qapp.processEvents()
- self.plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- self.plot.close()
- del self.plot
- super(TestScatterProfileToolBar, self).tearDown()
-
- def testNoProfile(self):
- """Test ScatterProfileToolBar without profile"""
- self.assertEqual(self.profile.getPlotWidget(), self.plot)
-
- # 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()
-
- # Check that there is no profile
- self.assertIsNone(self.profile.getProfileValues())
- self.assertIsNone(self.profile.getProfilePoints())
-
- def testHorizontalProfile(self):
- """Test ScatterProfileToolBar horizontal profile"""
- nPoints = 8
- self.profile.setNPoints(nPoints)
- self.assertEqual(self.profile.getNPoints(), nPoints)
-
- # 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()
-
- # Activate Horizontal profile
- hlineAction = self.profile.actions()[0]
- hlineAction.trigger()
- self.qapp.processEvents()
-
- # Set a ROI profile
- roi = roi_items.HorizontalLineROI()
- roi.setPosition(0.5)
- self.profile._getRoiManager().addRoi(roi)
-
- # Wait for async interpolator init
- for _ in range(10):
- self.qWait(200)
- if not self.profile.hasPendingOperations():
- break
-
- self.assertIsNotNone(self.profile.getProfileValues())
- points = self.profile.getProfilePoints()
- self.assertEqual(len(points), nPoints)
-
- # Check that profile has same limits than Plot
- xLimits = self.plot.getXAxis().getLimits()
- self.assertEqual(points[0, 0], xLimits[0])
- self.assertEqual(points[-1, 0], xLimits[1])
-
- # Clear the profile
- clearAction = self.profile.actions()[-1]
- clearAction.trigger()
- self.qapp.processEvents()
-
- self.assertIsNone(self.profile.getProfileValues())
- self.assertIsNone(self.profile.getProfilePoints())
- self.assertEqual(self.profile.getProfileTitle(), '')
-
- def testVerticalProfile(self):
- """Test ScatterProfileToolBar vertical profile"""
- nPoints = 8
- self.profile.setNPoints(nPoints)
- self.assertEqual(self.profile.getNPoints(), nPoints)
-
- # 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()
-
- # Activate vertical profile
- vlineAction = self.profile.actions()[1]
- vlineAction.trigger()
- self.qapp.processEvents()
-
- # Set a ROI profile
- roi = roi_items.VerticalLineROI()
- roi.setPosition(0.5)
- self.profile._getRoiManager().addRoi(roi)
-
- # Wait for async interpolator init
- for _ in range(10):
- self.qWait(200)
- if not self.profile.hasPendingOperations():
- break
-
- self.assertIsNotNone(self.profile.getProfileValues())
- points = self.profile.getProfilePoints()
- self.assertEqual(len(points), nPoints)
-
- # Check that profile has same limits than Plot
- yLimits = self.plot.getYAxis().getLimits()
- self.assertEqual(points[0, 1], yLimits[0])
- self.assertEqual(points[-1, 1], yLimits[1])
-
- # Check that profile limits are updated when changing limits
- self.plot.getYAxis().setLimits(yLimits[0] + 1, yLimits[1] + 10)
- self.qapp.processEvents()
- yLimits = self.plot.getYAxis().getLimits()
- points = self.profile.getProfilePoints()
- self.assertEqual(points[0, 1], yLimits[0])
- self.assertEqual(points[-1, 1], yLimits[1])
-
- # Clear the plot
- self.plot.clear()
- self.qapp.processEvents()
- self.assertIsNone(self.profile.getProfileValues())
- self.assertIsNone(self.profile.getProfilePoints())
-
- def testLineProfile(self):
- """Test ScatterProfileToolBar line profile"""
- nPoints = 8
- self.profile.setNPoints(nPoints)
- self.assertEqual(self.profile.getNPoints(), nPoints)
-
- # Activate line profile
- lineAction = self.profile.actions()[2]
- lineAction.trigger()
- self.qapp.processEvents()
-
- # 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 = roi_items.LineROI()
- roi.setEndPoints(numpy.array([0., 0.]), numpy.array([1., 1.]))
- self.profile._getRoiManager().addRoi(roi)
-
- # Wait for async interpolator init
- for _ in range(10):
- self.qWait(200)
- if not self.profile.hasPendingOperations():
- break
-
- self.assertIsNotNone(self.profile.getProfileValues())
- points = self.profile.getProfilePoints()
- self.assertEqual(len(points), nPoints)
-
-
-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 f4adda0..0000000
--- a/silx/gui/plot/tools/test/testTools.py
+++ /dev/null
@@ -1,175 +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 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
-
-
-# Makes sure a QApplication exists
-_qapp = qt.QApplication.instance() or qt.QApplication([])
-
-
-def _tearDownDocTest(docTest):
- """Tear down to use for test from docstring.
-
- Checks that plot widget is displayed
- """
- plot = docTest.globs['plot']
- qWaitForWindowExposedAndActivate(plot)
- plot.setAttribute(qt.Qt.WA_DeleteOnClose)
- plot.close()
- del plot
-
-# Disable doctest because of
-# "NameError: name 'numpy' is not defined"
-#
-# import doctest
-# positionInfoTestSuite = doctest.DocTestSuite(
-# PlotTools, tearDown=_tearDownDocTest,
-# optionflags=doctest.ELLIPSIS)
-# """Test suite of tests from PlotTools docstrings.
-#
-# Test PositionInfo and ProfileToolBar docstrings.
-# """
-
-
-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 28fb7f9..0000000
--- a/silx/gui/plot/tools/toolbars.py
+++ /dev/null
@@ -1,356 +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 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
-
-
-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._symbolToolButton = PlotToolButtons.SymbolToolButton(
- parent=self, plot=plot)
- self.addWidget(self._symbolToolButton)
-
- 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 getSymbolToolButton(self):
- """Returns the QToolButton controlling symbol size and marker.
-
- :rtype: SymbolToolButton
- """
- return self._symbolToolButton
-
- def getKeepDataAspectRatioButton(self):
- """Returns the QToolButton controlling data aspect ratio.
-
- :rtype: QToolButton
- """
- return self._keepDataAspectRatioButton
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 bd19996..0000000
--- a/silx/gui/plot/utils/axis.py
+++ /dev/null
@@ -1,199 +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__ = "23/02/2018"
-
-import functools
-import logging
-from contextlib import contextmanager
-import weakref
-import silx.utils.weakref as silxWeakref
-
-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):
- """
- 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
- """
- object.__init__(self)
- self.__locked = False
- self.__axisRefs = []
- self.__syncLimits = syncLimits
- self.__syncScale = syncScale
- self.__syncDirection = syncDirection
- self.__callbacks = None
-
- for axis in axes:
- self.__axisRefs.append(weakref.ref(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.__callbacks is not None:
- raise RuntimeError("Axes already synchronized")
- self.__callbacks = {}
-
- axes = self.__getAxes()
- if len(axes) == 0:
- raise RuntimeError('No axis to synchronize')
-
- # register callback for further sync
- for axis in axes:
- 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))
- 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))
-
- self.__callbacks[refAxis] = callbacks
-
- # sync the current state
- mainAxis = axes[0]
- refMainAxis = weakref.ref(mainAxis)
- if self.__syncLimits:
- self.__axisLimitsChanged(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 self.__callbacks is None:
- raise RuntimeError("Axes not synchronized")
- for ref, callbacks in self.__callbacks.items():
- axis = ref()
- if axis is not None and _isQObjectValid(axis):
- for sigName, callback in callbacks:
- sig = getattr(axis, sigName)
- sig.disconnect(callback)
- 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 __otherAxes(self, changedAxis):
- for axis in self.__getAxes():
- if axis is changedAxis:
- continue
- yield axis
-
- def __axisLimitsChanged(self, changedAxis, vmin, vmax):
- if self.__locked:
- return
- changedAxis = changedAxis()
- with self.__inhibitSignals():
- for axis in self.__otherAxes(changedAxis):
- axis.setLimits(vmin, vmax)
-
- def __axisScaleChanged(self, changedAxis, scale):
- if self.__locked:
- return
- changedAxis = changedAxis()
- with self.__inhibitSignals():
- for axis in self.__otherAxes(changedAxis):
- axis.setScale(scale)
-
- def __axisInvertedChanged(self, changedAxis, isInverted):
- if self.__locked:
- return
- changedAxis = changedAxis()
- with self.__inhibitSignals():
- for axis in self.__otherAxes(changedAxis):
- axis.setInverted(isInverted)