diff options
Diffstat (limited to 'silx/gui/plot/actions')
-rw-r--r-- | silx/gui/plot/actions/PlotAction.py | 78 | ||||
-rw-r--r-- | silx/gui/plot/actions/PlotToolAction.py | 150 | ||||
-rw-r--r-- | silx/gui/plot/actions/__init__.py | 42 | ||||
-rwxr-xr-x | silx/gui/plot/actions/control.py | 694 | ||||
-rw-r--r-- | silx/gui/plot/actions/fit.py | 403 | ||||
-rw-r--r-- | silx/gui/plot/actions/histogram.py | 392 | ||||
-rw-r--r-- | silx/gui/plot/actions/io.py | 818 | ||||
-rw-r--r-- | silx/gui/plot/actions/medfilt.py | 147 | ||||
-rw-r--r-- | silx/gui/plot/actions/mode.py | 104 |
9 files changed, 0 insertions, 2828 deletions
diff --git a/silx/gui/plot/actions/PlotAction.py b/silx/gui/plot/actions/PlotAction.py deleted file mode 100644 index 2983775..0000000 --- a/silx/gui/plot/actions/PlotAction.py +++ /dev/null @@ -1,78 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -The class :class:`.PlotAction` help the creation of a qt.QAction associated -with a :class:`.PlotWidget`. -""" - -from __future__ import division - - -__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] -__license__ = "MIT" -__date__ = "03/01/2018" - - -import weakref -from silx.gui import icons -from silx.gui import qt - - -class PlotAction(qt.QAction): - """Base class for QAction that operates on a PlotWidget. - - :param plot: :class:`.PlotWidget` instance on which to operate. - :param icon: QIcon or str name of icon to use - :param str text: The name of this action to be used for menu label - :param str tooltip: The text of the tooltip - :param triggered: The callback to connect to the action's triggered - signal or None for no callback. - :param bool checkable: True for checkable action, False otherwise (default) - :param parent: See :class:`QAction`. - """ - - def __init__(self, plot, icon, text, tooltip=None, - triggered=None, checkable=False, parent=None): - assert plot is not None - self._plotRef = weakref.ref(plot) - - if not isinstance(icon, qt.QIcon): - # Try with icon as a string and load corresponding icon - icon = icons.getQIcon(icon) - - super(PlotAction, self).__init__(icon, text, parent) - - if tooltip is not None: - self.setToolTip(tooltip) - - self.setCheckable(checkable) - - if triggered is not None: - self.triggered[bool].connect(triggered) - - @property - def plot(self): - """The :class:`.PlotWidget` this action group is controlling.""" - return self._plotRef() diff --git a/silx/gui/plot/actions/PlotToolAction.py b/silx/gui/plot/actions/PlotToolAction.py deleted file mode 100644 index fbb0b0f..0000000 --- a/silx/gui/plot/actions/PlotToolAction.py +++ /dev/null @@ -1,150 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -The class :class:`.PlotToolAction` help the creation of a qt.QAction associating -a tool window with a :class:`.PlotWidget`. -""" - -from __future__ import division - - -__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] -__license__ = "MIT" -__date__ = "10/10/2018" - - -import weakref - -from .PlotAction import PlotAction -from silx.gui import qt - - -class PlotToolAction(PlotAction): - """Base class for QAction that maintain a tool window operating on a - PlotWidget.""" - - def __init__(self, plot, icon, text, tooltip=None, - triggered=None, checkable=False, parent=None): - PlotAction.__init__(self, - plot=plot, - icon=icon, - text=text, - tooltip=tooltip, - triggered=self._triggered, - parent=parent, - checkable=True) - self._previousGeometry = None - self._toolWindow = None - - def _triggered(self, checked): - """Update the plot of the histogram visibility status - - :param bool checked: status of the action button - """ - self._setToolWindowVisible(checked) - - def _setToolWindowVisible(self, visible): - """Set the tool window visible or hidden.""" - tool = self._getToolWindow() - if tool.isVisible() == visible: - # Nothing to do - return - - if visible: - self._connectPlot(tool) - tool.show() - if self._previousGeometry is not None: - # Restore the geometry - tool.setGeometry(self._previousGeometry) - else: - self._disconnectPlot(tool) - # Save the geometry - self._previousGeometry = tool.geometry() - tool.hide() - - def _connectPlot(self, window): - """Called if the tool is visible and have to be updated according to - event of the plot. - - :param qt.QWidget window: The tool window - """ - pass - - def _disconnectPlot(self, window): - """Called if the tool is not visible and dont have anymore to be updated - according to event of the plot. - - :param qt.QWidget window: The tool window - """ - pass - - def _isWindowInUse(self): - """Returns true if the tool window is currently in use.""" - if not self.isChecked(): - return False - return self._toolWindow is not None - - def _ownerVisibilityChanged(self, isVisible): - """Called when the visibility of the parent of the tool window changes - - :param bool isVisible: True if the parent became visible - """ - if self._isWindowInUse(): - self._setToolWindowVisible(isVisible) - - def eventFilter(self, qobject, event): - """Observe when the close event is emitted then - simply uncheck the action button - - :param qobject: the object observe - :param event: the event received by qobject - """ - if event.type() == qt.QEvent.Close: - if self._toolWindow is not None: - window = self._toolWindow() - self._previousGeometry = window.geometry() - window.hide() - self.setChecked(False) - - return PlotAction.eventFilter(self, qobject, event) - - def _getToolWindow(self): - """Returns the window containing the tool. - - It uses lazy loading to create this tool.. - """ - if self._toolWindow is None: - window = self._createToolWindow() - if self._previousGeometry is not None: - window.setGeometry(self._previousGeometry) - window.installEventFilter(self) - plot = self.plot - plot.sigVisibilityChanged.connect(self._ownerVisibilityChanged) - self._toolWindow = weakref.ref(window) - return self._toolWindow() - - def _createToolWindow(self): - """Create the tool window managing the plot.""" - raise NotImplementedError() diff --git a/silx/gui/plot/actions/__init__.py b/silx/gui/plot/actions/__init__.py deleted file mode 100644 index 930c728..0000000 --- a/silx/gui/plot/actions/__init__.py +++ /dev/null @@ -1,42 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This package provides a set of QAction to use with -:class:`~silx.gui.plot.PlotWidget` - -Those actions are useful to add menu items or toolbar items -that interact with a :class:`~silx.gui.plot.PlotWidget`. - -It provides a base class used to define new plot actions: -:class:`~silx.gui.plot.actions.PlotAction`. -""" - -__authors__ = ["H. Payno"] -__license__ = "MIT" -__date__ = "16/08/2017" - -from .PlotAction import PlotAction -from . import control -from . import mode -from . import io diff --git a/silx/gui/plot/actions/control.py b/silx/gui/plot/actions/control.py deleted file mode 100755 index 439985e..0000000 --- a/silx/gui/plot/actions/control.py +++ /dev/null @@ -1,694 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -:mod:`silx.gui.plot.actions.control` provides a set of QAction relative to control -of a :class:`.PlotWidget`. - -The following QAction are available: - -- :class:`ColormapAction` -- :class:`CrosshairAction` -- :class:`CurveStyleAction` -- :class:`GridAction` -- :class:`KeepAspectRatioAction` -- :class:`PanWithArrowKeysAction` -- :class:`ResetZoomAction` -- :class:`ShowAxisAction` -- :class:`XAxisLogarithmicAction` -- :class:`XAxisAutoScaleAction` -- :class:`YAxisInvertedAction` -- :class:`YAxisLogarithmicAction` -- :class:`YAxisAutoScaleAction` -- :class:`ZoomBackAction` -- :class:`ZoomInAction` -- :class:`ZoomOutAction` -""" - -from __future__ import division - -__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] -__license__ = "MIT" -__date__ = "27/11/2020" - -from . import PlotAction -import logging -from silx.gui.plot import items -from silx.gui.plot._utils import applyZoomToPlot as _applyZoomToPlot -from silx.gui import qt -from silx.gui import icons - -_logger = logging.getLogger(__name__) - - -class ResetZoomAction(PlotAction): - """QAction controlling reset zoom on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(ResetZoomAction, self).__init__( - plot, icon='zoom-original', text='Reset Zoom', - tooltip='Auto-scale the graph', - triggered=self._actionTriggered, - checkable=False, parent=parent) - self._autoscaleChanged(True) - plot.getXAxis().sigAutoScaleChanged.connect(self._autoscaleChanged) - plot.getYAxis().sigAutoScaleChanged.connect(self._autoscaleChanged) - - def _autoscaleChanged(self, enabled): - xAxis = self.plot.getXAxis() - yAxis = self.plot.getYAxis() - self.setEnabled(xAxis.isAutoScale() or yAxis.isAutoScale()) - - if xAxis.isAutoScale() and yAxis.isAutoScale(): - tooltip = 'Auto-scale the graph' - elif xAxis.isAutoScale(): # And not Y axis - tooltip = 'Auto-scale the x-axis of the graph only' - elif yAxis.isAutoScale(): # And not X axis - tooltip = 'Auto-scale the y-axis of the graph only' - else: # no axis in autoscale - tooltip = 'Auto-scale the graph' - self.setToolTip(tooltip) - - def _actionTriggered(self, checked=False): - self.plot.resetZoom() - - -class ZoomBackAction(PlotAction): - """QAction performing a zoom-back in :class:`.PlotWidget` limits history. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(ZoomBackAction, self).__init__( - plot, icon='zoom-back', text='Zoom Back', - tooltip='Zoom back the plot', - triggered=self._actionTriggered, - checkable=False, parent=parent) - self.setShortcutContext(qt.Qt.WidgetShortcut) - - def _actionTriggered(self, checked=False): - self.plot.getLimitsHistory().pop() - - -class ZoomInAction(PlotAction): - """QAction performing a zoom-in on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(ZoomInAction, self).__init__( - plot, icon='zoom-in', text='Zoom In', - tooltip='Zoom in the plot', - triggered=self._actionTriggered, - checkable=False, parent=parent) - self.setShortcut(qt.QKeySequence.ZoomIn) - self.setShortcutContext(qt.Qt.WidgetShortcut) - - def _actionTriggered(self, checked=False): - _applyZoomToPlot(self.plot, 1.1) - - -class ZoomOutAction(PlotAction): - """QAction performing a zoom-out on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(ZoomOutAction, self).__init__( - plot, icon='zoom-out', text='Zoom Out', - tooltip='Zoom out the plot', - triggered=self._actionTriggered, - checkable=False, parent=parent) - self.setShortcut(qt.QKeySequence.ZoomOut) - self.setShortcutContext(qt.Qt.WidgetShortcut) - - def _actionTriggered(self, checked=False): - _applyZoomToPlot(self.plot, 1. / 1.1) - - -class XAxisAutoScaleAction(PlotAction): - """QAction controlling X axis autoscale on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(XAxisAutoScaleAction, self).__init__( - plot, icon='plot-xauto', text='X Autoscale', - tooltip='Enable x-axis auto-scale when checked.\n' - 'If unchecked, x-axis does not change when reseting zoom.', - triggered=self._actionTriggered, - checkable=True, parent=parent) - self.setChecked(plot.getXAxis().isAutoScale()) - plot.getXAxis().sigAutoScaleChanged.connect(self.setChecked) - - def _actionTriggered(self, checked=False): - self.plot.getXAxis().setAutoScale(checked) - if checked: - self.plot.resetZoom() - - -class YAxisAutoScaleAction(PlotAction): - """QAction controlling Y axis autoscale on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(YAxisAutoScaleAction, self).__init__( - plot, icon='plot-yauto', text='Y Autoscale', - tooltip='Enable y-axis auto-scale when checked.\n' - 'If unchecked, y-axis does not change when reseting zoom.', - triggered=self._actionTriggered, - checkable=True, parent=parent) - self.setChecked(plot.getYAxis().isAutoScale()) - plot.getYAxis().sigAutoScaleChanged.connect(self.setChecked) - - def _actionTriggered(self, checked=False): - self.plot.getYAxis().setAutoScale(checked) - if checked: - self.plot.resetZoom() - - -class XAxisLogarithmicAction(PlotAction): - """QAction controlling X axis log scale on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(XAxisLogarithmicAction, self).__init__( - plot, icon='plot-xlog', text='X Log. scale', - tooltip='Logarithmic x-axis when checked', - triggered=self._actionTriggered, - checkable=True, parent=parent) - self.axis = plot.getXAxis() - self.setChecked(self.axis.getScale() == self.axis.LOGARITHMIC) - self.axis.sigScaleChanged.connect(self._setCheckedIfLogScale) - - def _setCheckedIfLogScale(self, scale): - self.setChecked(scale == self.axis.LOGARITHMIC) - - def _actionTriggered(self, checked=False): - scale = self.axis.LOGARITHMIC if checked else self.axis.LINEAR - self.axis.setScale(scale) - - -class YAxisLogarithmicAction(PlotAction): - """QAction controlling Y axis log scale on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(YAxisLogarithmicAction, self).__init__( - plot, icon='plot-ylog', text='Y Log. scale', - tooltip='Logarithmic y-axis when checked', - triggered=self._actionTriggered, - checkable=True, parent=parent) - self.axis = plot.getYAxis() - self.setChecked(self.axis.getScale() == self.axis.LOGARITHMIC) - self.axis.sigScaleChanged.connect(self._setCheckedIfLogScale) - - def _setCheckedIfLogScale(self, scale): - self.setChecked(scale == self.axis.LOGARITHMIC) - - def _actionTriggered(self, checked=False): - scale = self.axis.LOGARITHMIC if checked else self.axis.LINEAR - self.axis.setScale(scale) - - -class GridAction(PlotAction): - """QAction controlling grid mode on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param str gridMode: The grid mode to use in 'both', 'major'. - See :meth:`.PlotWidget.setGraphGrid` - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, gridMode='both', parent=None): - assert gridMode in ('both', 'major') - self._gridMode = gridMode - - super(GridAction, self).__init__( - plot, icon='plot-grid', text='Grid', - tooltip='Toggle grid (on/off)', - triggered=self._actionTriggered, - checkable=True, parent=parent) - self.setChecked(plot.getGraphGrid() is not None) - plot.sigSetGraphGrid.connect(self._gridChanged) - - def _gridChanged(self, which): - """Slot listening for PlotWidget grid mode change.""" - self.setChecked(which != 'None') - - def _actionTriggered(self, checked=False): - self.plot.setGraphGrid(self._gridMode if checked else None) - - -class CurveStyleAction(PlotAction): - """QAction controlling curve style on a :class:`.PlotWidget`. - - It changes the default line and markers style which updates all - curves on the plot. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(CurveStyleAction, self).__init__( - plot, icon='plot-toggle-points', text='Curve style', - tooltip='Change curve line and markers style', - triggered=self._actionTriggered, - checkable=False, parent=parent) - - def _actionTriggered(self, checked=False): - currentState = (self.plot.isDefaultPlotLines(), - self.plot.isDefaultPlotPoints()) - - if currentState == (False, False): - newState = True, False - else: - # line only, line and symbol, symbol only - states = (True, False), (True, True), (False, True) - newState = states[(states.index(currentState) + 1) % 3] - - self.plot.setDefaultPlotLines(newState[0]) - self.plot.setDefaultPlotPoints(newState[1]) - - -class ColormapAction(PlotAction): - """QAction opening a ColormapDialog to update the colormap. - - Both the active image colormap and the default colormap are updated. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - self._dialog = None # To store an instance of ColormapDialog - super(ColormapAction, self).__init__( - plot, icon='colormap', text='Colormap', - tooltip="Change colormap", - triggered=self._actionTriggered, - checkable=True, parent=parent) - self.plot.sigActiveImageChanged.connect(self._updateColormap) - self.plot.sigActiveScatterChanged.connect(self._updateColormap) - - def setColorDialog(self, colorDialog): - """Set a specific color dialog instead of using the default dialog.""" - assert(colorDialog is not None) - assert(self._dialog is None) - self._dialog = colorDialog - self._dialog.visibleChanged.connect(self._dialogVisibleChanged) - self.setChecked(self._dialog.isVisible()) - - @staticmethod - def _createDialog(parent): - """Create the dialog if not already existing - - :parent QWidget parent: Parent of the new colormap - :rtype: ColormapDialog - """ - from silx.gui.dialog.ColormapDialog import ColormapDialog - dialog = ColormapDialog(parent=parent) - dialog.setModal(False) - return dialog - - def _actionTriggered(self, checked=False): - """Create a cmap dialog and update active image and default cmap.""" - if self._dialog is None: - self._dialog = self._createDialog(self.plot) - self._dialog.visibleChanged.connect(self._dialogVisibleChanged) - - # Run the dialog listening to colormap change - if checked is True: - self._updateColormap() - self._dialog.show() - else: - self._dialog.hide() - - def _dialogVisibleChanged(self, isVisible): - self.setChecked(isVisible) - - def _updateColormap(self): - if self._dialog is None: - return - image = self.plot.getActiveImage() - - if isinstance(image, items.ColormapMixIn): - # Set dialog from active image - colormap = image.getColormap() - # Set histogram and range if any - self._dialog.setItem(image) - - else: - # No active image or active image is RGBA, - # Check for active scatter plot - scatter = self.plot._getActiveItem(kind='scatter') - if scatter is not None: - colormap = scatter.getColormap() - self._dialog.setItem(scatter) - - else: - # No active data image nor scatter, - # set dialog from default info - colormap = self.plot.getDefaultColormap() - # Reset histogram and range if any - self._dialog.setData(None) - - self._dialog.setColormap(colormap) - - -class ColorBarAction(PlotAction): - """QAction opening the ColorBarWidget of the specified plot. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - self._dialog = None # To store an instance of ColorBar - super(ColorBarAction, self).__init__( - plot, icon='colorbar', text='Colorbar', - tooltip="Show/Hide the colorbar", - triggered=self._actionTriggered, - checkable=True, parent=parent) - colorBarWidget = self.plot.getColorBarWidget() - old = self.blockSignals(True) - self.setChecked(colorBarWidget.isVisibleTo(self.plot)) - self.blockSignals(old) - colorBarWidget.sigVisibleChanged.connect(self._widgetVisibleChanged) - - def _widgetVisibleChanged(self, isVisible): - """Callback when the colorbar `visible` property change.""" - if self.isChecked() == isVisible: - return - self.setChecked(isVisible) - - def _actionTriggered(self, checked=False): - """Create a cmap dialog and update active image and default cmap.""" - colorBarWidget = self.plot.getColorBarWidget() - if not colorBarWidget.isHidden() == checked: - return - self.plot.getColorBarWidget().setVisible(checked) - - -class KeepAspectRatioAction(PlotAction): - """QAction controlling aspect ratio on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - # Uses two images for checked/unchecked states - self._states = { - False: (icons.getQIcon('shape-circle-solid'), - "Keep data aspect ratio"), - True: (icons.getQIcon('shape-ellipse-solid'), - "Do no keep data aspect ratio") - } - - icon, tooltip = self._states[plot.isKeepDataAspectRatio()] - super(KeepAspectRatioAction, self).__init__( - plot, - icon=icon, - text='Toggle keep aspect ratio', - tooltip=tooltip, - triggered=self._actionTriggered, - checkable=False, - parent=parent) - plot.sigSetKeepDataAspectRatio.connect( - self._keepDataAspectRatioChanged) - - def _keepDataAspectRatioChanged(self, aspectRatio): - """Handle Plot set keep aspect ratio signal""" - icon, tooltip = self._states[aspectRatio] - self.setIcon(icon) - self.setToolTip(tooltip) - - def _actionTriggered(self, checked=False): - # This will trigger _keepDataAspectRatioChanged - self.plot.setKeepDataAspectRatio(not self.plot.isKeepDataAspectRatio()) - - -class YAxisInvertedAction(PlotAction): - """QAction controlling Y orientation on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - # Uses two images for checked/unchecked states - self._states = { - False: (icons.getQIcon('plot-ydown'), - "Orient Y axis downward"), - True: (icons.getQIcon('plot-yup'), - "Orient Y axis upward"), - } - - icon, tooltip = self._states[plot.getYAxis().isInverted()] - super(YAxisInvertedAction, self).__init__( - plot, - icon=icon, - text='Invert Y Axis', - tooltip=tooltip, - triggered=self._actionTriggered, - checkable=False, - parent=parent) - plot.getYAxis().sigInvertedChanged.connect(self._yAxisInvertedChanged) - - def _yAxisInvertedChanged(self, inverted): - """Handle Plot set y axis inverted signal""" - icon, tooltip = self._states[inverted] - self.setIcon(icon) - self.setToolTip(tooltip) - - def _actionTriggered(self, checked=False): - # This will trigger _yAxisInvertedChanged - yAxis = self.plot.getYAxis() - yAxis.setInverted(not yAxis.isInverted()) - - -class CrosshairAction(PlotAction): - """QAction toggling crosshair cursor on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param str color: Color to use to draw the crosshair - :param int linewidth: Width of the crosshair cursor - :param str linestyle: Style of line. See :meth:`.Plot.setGraphCursor` - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, color='black', linewidth=1, linestyle='-', - parent=None): - self.color = color - """Color used to draw the crosshair (str).""" - - self.linewidth = linewidth - """Width of the crosshair cursor (int).""" - - self.linestyle = linestyle - """Style of line of the cursor (str).""" - - super(CrosshairAction, self).__init__( - plot, icon='crosshair', text='Crosshair Cursor', - tooltip='Enable crosshair cursor when checked', - triggered=self._actionTriggered, - checkable=True, parent=parent) - self.setChecked(plot.getGraphCursor() is not None) - plot.sigSetGraphCursor.connect(self.setChecked) - - def _actionTriggered(self, checked=False): - self.plot.setGraphCursor(checked, - color=self.color, - linestyle=self.linestyle, - linewidth=self.linewidth) - - -class PanWithArrowKeysAction(PlotAction): - """QAction toggling pan with arrow keys on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - - super(PanWithArrowKeysAction, self).__init__( - plot, icon='arrow-keys', text='Pan with arrow keys', - tooltip='Enable pan with arrow keys when checked', - triggered=self._actionTriggered, - checkable=True, parent=parent) - self.setChecked(plot.isPanWithArrowKeys()) - plot.sigSetPanWithArrowKeys.connect(self.setChecked) - - def _actionTriggered(self, checked=False): - self.plot.setPanWithArrowKeys(checked) - - -class ShowAxisAction(PlotAction): - """QAction controlling axis visibility on a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - tooltip = 'Show plot axis when checked, otherwise hide them' - PlotAction.__init__(self, - plot, - icon='axis', - text='show axis', - tooltip=tooltip, - triggered=self._actionTriggered, - checkable=True, - parent=parent) - self.setChecked(self.plot.isAxesDisplayed()) - plot._sigAxesVisibilityChanged.connect(self.setChecked) - - def _actionTriggered(self, checked=False): - self.plot.setAxesDisplayed(checked) - - -class ClosePolygonInteractionAction(PlotAction): - """QAction controlling closure of a polygon in draw interaction mode - if the :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - tooltip = 'Close the current polygon drawn' - PlotAction.__init__(self, - plot, - icon='add-shape-polygon', - text='Close the polygon', - tooltip=tooltip, - triggered=self._actionTriggered, - checkable=True, - parent=parent) - self.plot.sigInteractiveModeChanged.connect(self._modeChanged) - self._modeChanged(None) - - def _modeChanged(self, source): - mode = self.plot.getInteractiveMode() - enabled = "shape" in mode and mode["shape"] == "polygon" - self.setEnabled(enabled) - - def _actionTriggered(self, checked=False): - self.plot._eventHandler.validate() - - -class OpenGLAction(PlotAction): - """QAction controlling rendering of a :class:`.PlotWidget`. - - For now it can enable or not the OpenGL backend. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - # Uses two images for checked/unchecked states - self._states = { - "opengl": (icons.getQIcon('backend-opengl'), - "OpenGL rendering (fast)\nClick to disable OpenGL"), - "matplotlib": (icons.getQIcon('backend-opengl'), - "Matplotlib rendering (safe)\nClick to enable OpenGL"), - "unknown": (icons.getQIcon('backend-opengl'), - "Custom rendering") - } - - name = self._getBackendName(plot) - self.__state = name - icon, tooltip = self._states[name] - super(OpenGLAction, self).__init__( - plot, - icon=icon, - text='Enable/disable OpenGL rendering', - tooltip=tooltip, - triggered=self._actionTriggered, - checkable=True, - parent=parent) - - def _backendUpdated(self): - name = self._getBackendName(self.plot) - self.__state = name - icon, tooltip = self._states[name] - self.setIcon(icon) - self.setToolTip(tooltip) - self.setChecked(name == "opengl") - - def _getBackendName(self, plot): - backend = plot.getBackend() - name = type(backend).__name__.lower() - if "opengl" in name: - return "opengl" - elif "matplotlib" in name: - return "matplotlib" - else: - return "unknown" - - def _actionTriggered(self, checked=False): - plot = self.plot - name = self._getBackendName(self.plot) - if self.__state != name: - # THere is no event to know the backend was updated - # So here we check if there is a mismatch between the displayed state - # and the real state of the widget - self._backendUpdated() - return - if name != "opengl": - from silx.gui.utils import glutils - result = glutils.isOpenGLAvailable() - if not result: - qt.QMessageBox.critical(plot, "OpenGL rendering not available", result.error) - # Uncheck if needed - self._backendUpdated() - return - plot.setBackend("opengl") - else: - plot.setBackend("matplotlib") - self._backendUpdated() diff --git a/silx/gui/plot/actions/fit.py b/silx/gui/plot/actions/fit.py deleted file mode 100644 index f3c9e1c..0000000 --- a/silx/gui/plot/actions/fit.py +++ /dev/null @@ -1,403 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -:mod:`silx.gui.plot.actions.fit` module provides actions relative to fit. - -The following QAction are available: - -- :class:`.FitAction` - -.. autoclass:`.FitAction` -""" - -from __future__ import division - -__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] -__license__ = "MIT" -__date__ = "10/10/2018" - -import logging - -import numpy - -from .PlotToolAction import PlotToolAction -from .. import items -from ....utils.deprecation import deprecated -from silx.gui import qt -from silx.gui.plot.ItemsSelectionDialog import ItemsSelectionDialog - -_logger = logging.getLogger(__name__) - - -def _getUniqueCurveOrHistogram(plot): - """Returns unique :class:`Curve` or :class:`Histogram` in a `PlotWidget`. - - If there is an active curve, returns it, else return curve or histogram - only if alone in the plot. - - :param PlotWidget plot: - :rtype: Union[None,~silx.gui.plot.items.Curve,~silx.gui.plot.items.Histogram] - """ - curve = plot.getActiveCurve() - if curve is not None: - return curve - - histograms = [item for item in plot.getItems() - if isinstance(item, items.Histogram) and item.isVisible()] - curves = [item for item in plot.getItems() - if isinstance(item, items.Curve) and item.isVisible()] - - if len(histograms) == 1 and len(curves) == 0: - return histograms[0] - elif len(curves) == 1 and len(histograms) == 0: - return curves[0] - else: - return None - - -class FitAction(PlotToolAction): - """QAction to open a :class:`FitWidget` and set its data to the - active curve if any, or to the first curve. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - self.__item = None - self.__activeCurveSynchroEnabled = False - self.__range = 0, 1 - self.__rangeAutoUpdate = False - self.__x, self.__y = None, None # Data to fit - self.__curveParams = {} # Store curve parameters to use for fit result - self.__legend = None - - super(FitAction, self).__init__( - plot, icon='math-fit', text='Fit curve', - tooltip='Open a fit dialog', - parent=parent) - - @property - @deprecated(replacement='getXRange()[0]', since_version='0.13.0') - def xmin(self): - return self.getXRange()[0] - - @property - @deprecated(replacement='getXRange()[1]', since_version='0.13.0') - def xmax(self): - return self.getXRange()[1] - - @property - @deprecated(replacement='getXData()', since_version='0.13.0') - def x(self): - return self.getXData() - - @property - @deprecated(replacement='getYData()', since_version='0.13.0') - def y(self): - return self.getYData() - - @property - @deprecated(since_version='0.13.0') - def xlabel(self): - return self.__curveParams.get('xlabel', None) - - @property - @deprecated(since_version='0.13.0') - def ylabel(self): - return self.__curveParams.get('ylabel', None) - - @property - @deprecated(since_version='0.13.0') - def legend(self): - return self.__legend - - def _createToolWindow(self): - # import done here rather than at module level to avoid circular import - # FitWidget -> BackgroundWidget -> PlotWindow -> actions -> fit -> FitWidget - from ...fit.FitWidget import FitWidget - - window = FitWidget(parent=self.plot) - window.setWindowFlags(qt.Qt.Dialog) - window.sigFitWidgetSignal.connect(self.handle_signal) - return window - - def _connectPlot(self, window): - if self.isXRangeUpdatedOnZoom(): - self.__setAutoXRangeEnabled(True) - else: - plot = self.plot - if plot is None: - _logger.error("No associated PlotWidget") - return - self._setXRange(*plot.getXAxis().getLimits()) - - if self.isFittedItemUpdatedFromActiveCurve(): - self.__setFittedItemAutoUpdateEnabled(True) - else: - # Wait for the next iteration, else the plot is not yet initialized - # No curve available - qt.QTimer.singleShot(10, self._initFit) - - def _disconnectPlot(self, window): - if self.isXRangeUpdatedOnZoom(): - self.__setAutoXRangeEnabled(False) - - if self.isFittedItemUpdatedFromActiveCurve(): - self.__setFittedItemAutoUpdateEnabled(False) - - def _initFit(self): - plot = self.plot - if plot is None: - _logger.error("No associated PlotWidget") - return - - item = _getUniqueCurveOrHistogram(plot) - if item is None: - # ambiguous case, we need to ask which plot item to fit - isd = ItemsSelectionDialog(parent=plot, plot=plot) - isd.setWindowTitle("Select item to be fitted") - isd.setItemsSelectionMode(qt.QTableWidget.SingleSelection) - isd.setAvailableKinds(["curve", "histogram"]) - isd.selectAllKinds() - - if not isd.exec_(): # Cancel - self._getToolWindow().setVisible(False) - else: - selectedItems = isd.getSelectedItems() - item = selectedItems[0] if len(selectedItems) == 1 else None - - self._setXRange(*plot.getXAxis().getLimits()) - self._setFittedItem(item) - - def __updateFitWidget(self): - """Update the data/range used by the FitWidget""" - fitWidget = self._getToolWindow() - - item = self._getFittedItem() - xdata = self.getXData(copy=False) - ydata = self.getYData(copy=False) - if item is None or xdata is None or ydata is None: - fitWidget.setData(y=None) - fitWidget.setWindowTitle("No curve selected") - - else: - xmin, xmax = self.getXRange() - fitWidget.setData( - xdata, ydata, xmin=xmin, xmax=xmax) - fitWidget.setWindowTitle( - "Fitting " + item.getName() + - " on x range %f-%f" % (xmin, xmax)) - - # X Range management - - def getXRange(self): - """Returns the range on the X axis on which to perform the fit.""" - return self.__range - - def _setXRange(self, xmin, xmax): - """Set the range on which the fit is done. - - :param float xmin: - :param float xmax: - """ - range_ = float(xmin), float(xmax) - if self.__range != range_: - self.__range = range_ - self.__updateFitWidget() - - def __setAutoXRangeEnabled(self, enabled): - """Implement the change of update mode of the X range. - - :param bool enabled: - """ - plot = self.plot - if plot is None: - _logger.error("No associated PlotWidget") - return - - if enabled: - self._setXRange(*plot.getXAxis().getLimits()) - plot.getXAxis().sigLimitsChanged.connect(self._setXRange) - else: - plot.getXAxis().sigLimitsChanged.disconnect(self._setXRange) - - def setXRangeUpdatedOnZoom(self, enabled): - """Set whether or not to update the X range on zoom change. - - :param bool enabled: - """ - if enabled != self.__rangeAutoUpdate: - self.__rangeAutoUpdate = enabled - if self._getToolWindow().isVisible(): - self.__setAutoXRangeEnabled(enabled) - - def isXRangeUpdatedOnZoom(self): - """Returns the current mode of fitted data X range update. - - :rtype: bool - """ - return self.__rangeAutoUpdate - - # Fitted item update - - def getXData(self, copy=True): - """Returns the X data used for the fit or None if undefined. - - :param bool copy: - True to get a copy of the data, False to get the internal data. - :rtype: Union[numpy.ndarray,None] - """ - return None if self.__x is None else numpy.array(self.__x, copy=copy) - - def getYData(self, copy=True): - """Returns the Y data used for the fit or None if undefined. - - :param bool copy: - True to get a copy of the data, False to get the internal data. - :rtype: Union[numpy.ndarray,None] - """ - return None if self.__y is None else numpy.array(self.__y, copy=copy) - - def _getFittedItem(self): - """Returns the current item used for the fit - - :rtype: Union[~silx.gui.plot.items.Curve,~silx.gui.plot.items.Histogram,None] - """ - return self.__item - - def _setFittedItem(self, item): - """Set the curve to use for fitting. - - :param Union[~silx.gui.plot.items.Curve,~silx.gui.plot.items.Histogram,None] item: - """ - plot = self.plot - if plot is None: - _logger.error("No associated PlotWidget") - - if plot is None or item is None: - self.__item = None - self.__curveParams = {} - self.__updateFitWidget() - return - - axis = item.getYAxis() if isinstance(item, items.YAxisMixIn) else 'left' - self.__curveParams = { - 'yaxis': axis, - 'xlabel': plot.getXAxis().getLabel(), - 'ylabel': plot.getYAxis(axis).getLabel(), - } - self.__legend = item.getName() - - if isinstance(item, items.Histogram): - bin_edges = item.getBinEdgesData(copy=False) - # take the middle coordinate between adjacent bin edges - self.__x = (bin_edges[1:] + bin_edges[:-1]) / 2 - self.__y = item.getValueData(copy=False) - # else take the active curve, or else the unique curve - elif isinstance(item, items.Curve): - self.__x = item.getXData(copy=False) - self.__y = item.getYData(copy=False) - - self.__item = item - self.__updateFitWidget() - - def __activeCurveChanged(self, previous, current): - """Handle change of active curve in the PlotWidget - """ - if current is None: - self._setFittedItem(None) - else: - item = self.plot.getCurve(current) - self._setFittedItem(item) - - def __setFittedItemAutoUpdateEnabled(self, enabled): - """Implement the change of fitted item update mode - - :param bool enabled: - """ - plot = self.plot - if plot is None: - _logger.error("No associated PlotWidget") - return - - if enabled: - self._setFittedItem(plot.getActiveCurve()) - plot.sigActiveCurveChanged.connect(self.__activeCurveChanged) - - else: - plot.sigActiveCurveChanged.disconnect( - self.__activeCurveChanged) - - def setFittedItemUpdatedFromActiveCurve(self, enabled): - """Toggle fitted data synchronization with plot active curve. - - :param bool enabled: - """ - enabled = bool(enabled) - if enabled != self.__activeCurveSynchroEnabled: - self.__activeCurveSynchroEnabled = enabled - if self._getToolWindow().isVisible(): - self.__setFittedItemAutoUpdateEnabled(enabled) - - def isFittedItemUpdatedFromActiveCurve(self): - """Returns True if fitted data is synchronized with plot. - - :rtype: bool - """ - return self.__activeCurveSynchroEnabled - - # Handle fit completed - - def handle_signal(self, ddict): - xdata = self.getXData(copy=False) - if xdata is None: - _logger.error("No reference data to display fit result for") - return - - xmin, xmax = self.getXRange() - x_fit = xdata[xmin <= xdata] - x_fit = x_fit[x_fit <= xmax] - fit_legend = "Fit <%s>" % self.__legend - fit_curve = self.plot.getCurve(fit_legend) - - if ddict["event"] == "FitFinished": - fit_widget = self._getToolWindow() - if fit_widget is None: - return - y_fit = fit_widget.fitmanager.gendata() - if fit_curve is None: - self.plot.addCurve(x_fit, y_fit, - fit_legend, - resetzoom=False, - **self.__curveParams) - else: - fit_curve.setData(x_fit, y_fit) - fit_curve.setVisible(True) - fit_curve.setYAxis(self.__curveParams.get('yaxis', 'left')) - - if ddict["event"] in ["FitStarted", "FitFailed"]: - if fit_curve is not None: - fit_curve.setVisible(False) diff --git a/silx/gui/plot/actions/histogram.py b/silx/gui/plot/actions/histogram.py deleted file mode 100644 index 0bba558..0000000 --- a/silx/gui/plot/actions/histogram.py +++ /dev/null @@ -1,392 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -:mod:`silx.gui.plot.actions.histogram` provides actions relative to histograms -for :class:`.PlotWidget`. - -The following QAction are available: - -- :class:`PixelIntensitiesHistoAction` -""" - -from __future__ import division - -__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] -__date__ = "01/12/2020" -__license__ = "MIT" - -import numpy -import logging -import typing -import weakref - -from .PlotToolAction import PlotToolAction - -from silx.math.histogram import Histogramnd -from silx.math.combo import min_max -from silx.gui import qt -from silx.gui.plot import items -from silx.gui.widgets.ElidedLabel import ElidedLabel -from silx.utils.deprecation import deprecated - -_logger = logging.getLogger(__name__) - - -class _ElidedLabel(ElidedLabel): - """QLabel with a default size larger than what is displayed.""" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setTextInteractionFlags(qt.Qt.TextSelectableByMouse) - - def sizeHint(self): - hint = super().sizeHint() - nbchar = max(len(self.getText()), 12) - width = self.fontMetrics().boundingRect('#' * nbchar).width() - return qt.QSize(max(hint.width(), width), hint.height()) - - -class _StatWidget(qt.QWidget): - """Widget displaying a name and a value - - :param parent: - :param name: - """ - - def __init__(self, parent=None, name: str=''): - super().__init__(parent) - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - - keyWidget = qt.QLabel(parent=self) - keyWidget.setText("<b>" + name.capitalize() + ":<b>") - layout.addWidget(keyWidget) - self.__valueWidget = _ElidedLabel(parent=self) - self.__valueWidget.setText("-") - self.__valueWidget.setTextInteractionFlags( - qt.Qt.TextSelectableByMouse | qt.Qt.TextSelectableByKeyboard) - layout.addWidget(self.__valueWidget) - - def setValue(self, value: typing.Optional[float]): - """Set the displayed value - - :param value: - """ - self.__valueWidget.setText( - "-" if value is None else "{:.5g}".format(value)) - - -class HistogramWidget(qt.QWidget): - """Widget displaying a histogram and some statistic indicators""" - - _SUPPORTED_ITEM_CLASS = items.ImageBase, items.Scatter - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.setWindowTitle('Histogram') - - self.__itemRef = None # weakref on the item to track - - layout = qt.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - # Plot - # Lazy import to avoid circular dependencies - from silx.gui.plot.PlotWindow import Plot1D - self.__plot = Plot1D(self) - layout.addWidget(self.__plot) - - self.__plot.setDataMargins(0.1, 0.1, 0.1, 0.1) - self.__plot.getXAxis().setLabel("Value") - self.__plot.getYAxis().setLabel("Count") - posInfo = self.__plot.getPositionInfoWidget() - posInfo.setSnappingMode(posInfo.SNAPPING_CURVE) - - # Stats display - statsWidget = qt.QWidget(self) - layout.addWidget(statsWidget) - statsLayout = qt.QHBoxLayout(statsWidget) - statsLayout.setContentsMargins(4, 4, 4, 4) - - self.__statsWidgets = dict( - (name, _StatWidget(parent=statsWidget, name=name)) - for name in ("min", "max", "mean", "std", "sum")) - - for widget in self.__statsWidgets.values(): - statsLayout.addWidget(widget) - statsLayout.addStretch(1) - - def getPlotWidget(self): - """Returns :class:`PlotWidget` use to display the histogram""" - return self.__plot - - def resetZoom(self): - """Reset PlotWidget zoom""" - self.getPlotWidget().resetZoom() - - def reset(self): - """Clear displayed information""" - self.getPlotWidget().clear() - self.setStatistics() - - def getItem(self) -> typing.Optional[items.Item]: - """Returns item used to display histogram and statistics.""" - return None if self.__itemRef is None else self.__itemRef() - - def setItem(self, item: typing.Optional[items.Item]): - """Set item from which to display histogram and statistics. - - :param item: - """ - previous = self.getItem() - if previous is not None: - previous.sigItemChanged.disconnect(self.__itemChanged) - - self.__itemRef = None if item is None else weakref.ref(item) - if item is not None: - if isinstance(item, self._SUPPORTED_ITEM_CLASS): - # Only listen signal for supported items - item.sigItemChanged.connect(self.__itemChanged) - self._updateFromItem() - - def __itemChanged(self, event): - """Handle update of the item""" - if event in (items.ItemChangedType.DATA, items.ItemChangedType.MASK): - self._updateFromItem() - - def _updateFromItem(self): - """Update histogram and stats from the item""" - item = self.getItem() - - if item is None: - self.reset() - return - - if not isinstance(item, self._SUPPORTED_ITEM_CLASS): - _logger.error("Unsupported item", item) - self.reset() - return - - # Compute histogram and stats - array = item.getValueData(copy=False) - - if array.size == 0: - self.reset() - return - - xmin, xmax = min_max(array, min_positive=False, finite=True) - nbins = min(1024, int(numpy.sqrt(array.size))) - data_range = xmin, xmax - - # bad hack: get 256 bins in the case we have a B&W - if numpy.issubdtype(array.dtype, numpy.integer): - if nbins > xmax - xmin: - nbins = xmax - xmin - - nbins = max(2, nbins) - - data = array.ravel().astype(numpy.float32) - histogram = Histogramnd(data, n_bins=nbins, histo_range=data_range) - if len(histogram.edges) != 1: - _logger.error("Error while computing the histogram") - self.reset() - return - - self.setHistogram(histogram.histo, histogram.edges[0]) - self.resetZoom() - self.setStatistics( - min_=xmin, - max_=xmax, - mean=numpy.nanmean(array), - std=numpy.nanstd(array), - sum_=numpy.nansum(array)) - - def setHistogram(self, histogram, edges): - """Set displayed histogram - - :param histogram: Bin values (N) - :param edges: Bin edges (N+1) - """ - self.getPlotWidget().addHistogram( - histogram=histogram, - edges=edges, - legend='histogram', - fill=True, - color='#66aad7', - resetzoom=False) - - def getHistogram(self, copy: bool=True): - """Returns currently displayed histogram. - - :param copy: True to get a copy, - False to get internal representation (Do not modify!) - :return: (histogram, edges) or None - """ - for item in self.getPlotWidget().getItems(): - if item.getName() == 'histogram': - return (item.getValueData(copy=copy), - item.getBinEdgesData(copy=copy)) - else: - return None - - def setStatistics(self, - min_: typing.Optional[float] = None, - max_: typing.Optional[float] = None, - mean: typing.Optional[float] = None, - std: typing.Optional[float] = None, - sum_: typing.Optional[float] = None): - """Set displayed statistic indicators.""" - self.__statsWidgets['min'].setValue(min_) - self.__statsWidgets['max'].setValue(max_) - self.__statsWidgets['mean'].setValue(mean) - self.__statsWidgets['std'].setValue(std) - self.__statsWidgets['sum'].setValue(sum_) - - -class _LastActiveItem(qt.QObject): - - sigActiveItemChanged = qt.Signal(object, object) - """Emitted when the active plot item have changed""" - - def __init__(self, parent, plot): - assert plot is not None - super(_LastActiveItem, self).__init__(parent=parent) - self.__plot = weakref.ref(plot) - self.__item = None - item = self.__findActiveItem() - self.setActiveItem(item) - plot.sigActiveImageChanged.connect(self._activeImageChanged) - plot.sigActiveScatterChanged.connect(self._activeScatterChanged) - - def getPlotWidget(self): - return self.__plot() - - def __findActiveItem(self): - plot = self.getPlotWidget() - image = plot.getActiveImage() - if image is not None: - return image - scatter = plot.getActiveScatter() - if scatter is not None: - return scatter - - def getActiveItem(self): - if self.__item is None: - return None - item = self.__item() - if item is None: - self.__item = None - return item - - def setActiveItem(self, item): - previous = self.getActiveItem() - if previous is item: - return - if item is None: - self.__item = None - else: - self.__item = weakref.ref(item) - self.sigActiveItemChanged.emit(previous, item) - - def _activeImageChanged(self, previous, current): - """Handle active image change""" - plot = self.getPlotWidget() - if current is None: # Fall-back to active scatter if any - self.setActiveItem(plot.getActiveScatter()) - else: - item = plot.getImage(current) - if item is None: - self.setActiveItem(None) - elif isinstance(item, items.ImageBase): - self.setActiveItem(item) - else: - # Do not touch anything, which is consistent with silx v0.12 behavior - pass - - def _activeScatterChanged(self, previous, current): - """Handle active scatter change""" - plot = self.getPlotWidget() - if current is None: # Fall-back to active image if any - self.setActiveItem(plot.getActiveImage()) - else: - item = plot.getScatter(current) - self.setActiveItem(item) - - -class PixelIntensitiesHistoAction(PlotToolAction): - """QAction to plot the pixels intensities diagram - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - PlotToolAction.__init__(self, - plot, - icon='pixel-intensities', - text='pixels intensity', - tooltip='Compute image intensity distribution', - parent=parent) - self._lastItemFilter = _LastActiveItem(self, plot) - - def _connectPlot(self, window): - self._lastItemFilter.sigActiveItemChanged.connect(self._activeItemChanged) - item = self._lastItemFilter.getActiveItem() - self.getHistogramWidget().setItem(item) - PlotToolAction._connectPlot(self, window) - - def _disconnectPlot(self, window): - self._lastItemFilter.sigActiveItemChanged.disconnect(self._activeItemChanged) - PlotToolAction._disconnectPlot(self, window) - self.getHistogramWidget().setItem(None) - - def _activeItemChanged(self, previous, current): - if self._isWindowInUse(): - self.getHistogramWidget().setItem(current) - - @deprecated(since_version='0.15.0') - def computeIntensityDistribution(self): - self.getHistogramWidget()._updateFromItem() - - def getHistogramWidget(self): - """Returns the widget displaying the histogram""" - return self._getToolWindow() - - @deprecated(since_version='0.15.0', - replacement='getHistogramWidget().getPlotWidget()') - def getHistogramPlotWidget(self): - return self._getToolWindow().getPlotWidget() - - def _createToolWindow(self): - return HistogramWidget(self.plot, qt.Qt.Window) - - def getHistogram(self) -> typing.Optional[numpy.ndarray]: - """Return the last computed histogram - - :return: the histogram displayed in the HistogramWidget - """ - histogram = self.getHistogramWidget().getHistogram() - return None if histogram is None else histogram[0] diff --git a/silx/gui/plot/actions/io.py b/silx/gui/plot/actions/io.py deleted file mode 100644 index f728b7a..0000000 --- a/silx/gui/plot/actions/io.py +++ /dev/null @@ -1,818 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -:mod:`silx.gui.plot.actions.io` provides a set of QAction relative of inputs -and outputs for a :class:`.PlotWidget`. - -The following QAction are available: - -- :class:`CopyAction` -- :class:`PrintAction` -- :class:`SaveAction` -""" - -from __future__ import division - -__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] -__license__ = "MIT" -__date__ = "25/09/2020" - -from . import PlotAction -from silx.io.utils import save1D, savespec, NEXUS_HDF5_EXT -from silx.io.nxdata import save_NXdata -import logging -import sys -import os.path -from collections import OrderedDict -import traceback -import numpy -from silx.utils.deprecation import deprecated -from silx.gui import qt, printer -from silx.gui.dialog.GroupDialog import GroupDialog -from silx.third_party.EdfFile import EdfFile -from silx.third_party.TiffIO import TiffIO -from ...utils.image import convertArrayToQImage -if sys.version_info[0] == 3: - from io import BytesIO -else: - import cStringIO as _StringIO - BytesIO = _StringIO.StringIO - -_logger = logging.getLogger(__name__) - -_NEXUS_HDF5_EXT_STR = ' '.join(['*' + ext for ext in NEXUS_HDF5_EXT]) - - -def selectOutputGroup(h5filename): - """Open a dialog to prompt the user to select a group in - which to output data. - - :param str h5filename: name of an existing HDF5 file - :rtype: str - :return: Name of output group, or None if the dialog was cancelled - """ - dialog = GroupDialog() - dialog.addFile(h5filename) - dialog.setWindowTitle("Select an output group") - if not dialog.exec_(): - return None - return dialog.getSelectedDataUrl().data_path() - - -class SaveAction(PlotAction): - """QAction for saving Plot content. - - It opens a Save as... dialog. - - :param plot: :class:`.PlotWidget` instance on which to operate. - :param parent: See :class:`QAction`. - """ - - SNAPSHOT_FILTER_SVG = 'Plot Snapshot as SVG (*.svg)' - SNAPSHOT_FILTER_PNG = 'Plot Snapshot as PNG (*.png)' - - DEFAULT_ALL_FILTERS = (SNAPSHOT_FILTER_PNG, SNAPSHOT_FILTER_SVG) - - # Dict of curve filters with CSV-like format - # Using ordered dict to guarantee filters order - # Note: '%.18e' is numpy.savetxt default format - CURVE_FILTERS_TXT = OrderedDict(( - ('Curve as Raw ASCII (*.txt)', - {'fmt': '%.18e', 'delimiter': ' ', 'header': False}), - ('Curve as ";"-separated CSV (*.csv)', - {'fmt': '%.18e', 'delimiter': ';', 'header': True}), - ('Curve as ","-separated CSV (*.csv)', - {'fmt': '%.18e', 'delimiter': ',', 'header': True}), - ('Curve as tab-separated CSV (*.csv)', - {'fmt': '%.18e', 'delimiter': '\t', 'header': True}), - ('Curve as OMNIC CSV (*.csv)', - {'fmt': '%.7E', 'delimiter': ',', 'header': False}), - ('Curve as SpecFile (*.dat)', - {'fmt': '%.10g', 'delimiter': '', 'header': False}) - )) - - CURVE_FILTER_NPY = 'Curve as NumPy binary file (*.npy)' - - CURVE_FILTER_NXDATA = 'Curve as NXdata (%s)' % _NEXUS_HDF5_EXT_STR - - DEFAULT_CURVE_FILTERS = list(CURVE_FILTERS_TXT.keys()) + [ - CURVE_FILTER_NPY, CURVE_FILTER_NXDATA] - - DEFAULT_ALL_CURVES_FILTERS = ("All curves as SpecFile (*.dat)",) - - IMAGE_FILTER_EDF = 'Image data as EDF (*.edf)' - IMAGE_FILTER_TIFF = 'Image data as TIFF (*.tif)' - IMAGE_FILTER_NUMPY = 'Image data as NumPy binary file (*.npy)' - IMAGE_FILTER_ASCII = 'Image data as ASCII (*.dat)' - IMAGE_FILTER_CSV_COMMA = 'Image data as ,-separated CSV (*.csv)' - IMAGE_FILTER_CSV_SEMICOLON = 'Image data as ;-separated CSV (*.csv)' - IMAGE_FILTER_CSV_TAB = 'Image data as tab-separated CSV (*.csv)' - IMAGE_FILTER_RGB_PNG = 'Image as PNG (*.png)' - IMAGE_FILTER_NXDATA = 'Image as NXdata (%s)' % _NEXUS_HDF5_EXT_STR - - DEFAULT_IMAGE_FILTERS = (IMAGE_FILTER_EDF, - IMAGE_FILTER_TIFF, - IMAGE_FILTER_NUMPY, - IMAGE_FILTER_ASCII, - IMAGE_FILTER_CSV_COMMA, - IMAGE_FILTER_CSV_SEMICOLON, - IMAGE_FILTER_CSV_TAB, - IMAGE_FILTER_RGB_PNG, - IMAGE_FILTER_NXDATA) - - SCATTER_FILTER_NXDATA = 'Scatter as NXdata (%s)' % _NEXUS_HDF5_EXT_STR - DEFAULT_SCATTER_FILTERS = (SCATTER_FILTER_NXDATA,) - - # filters for which we don't want an "overwrite existing file" warning - DEFAULT_APPEND_FILTERS = (CURVE_FILTER_NXDATA, IMAGE_FILTER_NXDATA, - SCATTER_FILTER_NXDATA) - - def __init__(self, plot, parent=None): - self._filters = { - 'all': OrderedDict(), - 'curve': OrderedDict(), - 'curves': OrderedDict(), - 'image': OrderedDict(), - 'scatter': OrderedDict()} - - self._appendFilters = list(self.DEFAULT_APPEND_FILTERS) - - # Initialize filters - for nameFilter in self.DEFAULT_ALL_FILTERS: - self.setFileFilter( - dataKind='all', nameFilter=nameFilter, func=self._saveSnapshot) - - for nameFilter in self.DEFAULT_CURVE_FILTERS: - self.setFileFilter( - dataKind='curve', nameFilter=nameFilter, func=self._saveCurve) - - for nameFilter in self.DEFAULT_ALL_CURVES_FILTERS: - self.setFileFilter( - dataKind='curves', nameFilter=nameFilter, func=self._saveCurves) - - for nameFilter in self.DEFAULT_IMAGE_FILTERS: - self.setFileFilter( - dataKind='image', nameFilter=nameFilter, func=self._saveImage) - - for nameFilter in self.DEFAULT_SCATTER_FILTERS: - self.setFileFilter( - dataKind='scatter', nameFilter=nameFilter, func=self._saveScatter) - - super(SaveAction, self).__init__( - plot, icon='document-save', text='Save as...', - tooltip='Save curve/image/plot snapshot dialog', - triggered=self._actionTriggered, - checkable=False, parent=parent) - self.setShortcut(qt.QKeySequence.Save) - self.setShortcutContext(qt.Qt.WidgetShortcut) - - @staticmethod - def _errorMessage(informativeText='', parent=None): - """Display an error message.""" - # TODO issue with QMessageBox size fixed and too small - msg = qt.QMessageBox(parent) - msg.setIcon(qt.QMessageBox.Critical) - msg.setInformativeText(informativeText + ' ' + str(sys.exc_info()[1])) - msg.setDetailedText(traceback.format_exc()) - msg.exec_() - - def _saveSnapshot(self, plot, filename, nameFilter): - """Save a snapshot of the :class:`PlotWindow` widget. - - :param str filename: The name of the file to write - :param str nameFilter: The selected name filter - :return: False if format is not supported or save failed, - True otherwise. - """ - if nameFilter == self.SNAPSHOT_FILTER_PNG: - fileFormat = 'png' - elif nameFilter == self.SNAPSHOT_FILTER_SVG: - fileFormat = 'svg' - else: # Format not supported - _logger.error( - 'Saving plot snapshot failed: format not supported') - return False - - plot.saveGraph(filename, fileFormat=fileFormat) - return True - - def _getAxesLabels(self, item): - # If curve has no associated label, get the default from the plot - xlabel = item.getXLabel() or self.plot.getXAxis().getLabel() - ylabel = item.getYLabel() or self.plot.getYAxis().getLabel() - return xlabel, ylabel - - def _get1dData(self, item): - "provide xdata, [ydata], xlabel, [ylabel] and manages error bars" - xlabel, ylabel = self._getAxesLabels(item) - x_data = item.getXData(copy=False) - y_data = item.getYData(copy=False) - x_err = item.getXErrorData(copy=False) - y_err = item.getYErrorData(copy=False) - labels = [ylabel] - data = [y_data] - - if x_err is not None: - if numpy.isscalar(x_err): - data.append(numpy.zeros_like(y_data) + x_err) - labels.append(xlabel + "_errors") - elif x_err.ndim == 1: - data.append(x_err) - labels.append(xlabel + "_errors") - elif x_err.ndim == 2: - data.append(x_err[0]) - labels.append(xlabel + "_errors_below") - data.append(x_err[1]) - labels.append(xlabel + "_errors_above") - - if y_err is not None: - if numpy.isscalar(y_err): - data.append(numpy.zeros_like(y_data) + y_err) - labels.append(ylabel + "_errors") - elif y_err.ndim == 1: - data.append(y_err) - labels.append(ylabel + "_errors") - elif y_err.ndim == 2: - data.append(y_err[0]) - labels.append(ylabel + "_errors_below") - data.append(y_err[1]) - labels.append(ylabel + "_errors_above") - return x_data, data, xlabel, labels - - @staticmethod - def _selectWriteableOutputGroup(filename, parent): - if os.path.exists(filename) and os.path.isfile(filename) \ - and os.access(filename, os.W_OK): - entryPath = selectOutputGroup(filename) - if entryPath is None: - _logger.info("Save operation cancelled") - return None - return entryPath - elif not os.path.exists(filename): - # create new entry in new file - return "/entry" - else: - SaveAction._errorMessage('Save failed (file access issue)\n', parent=parent) - return None - - def _saveCurveAsNXdata(self, curve, filename): - entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot) - if entryPath is None: - return False - - xlabel, ylabel = self._getAxesLabels(curve) - - return save_NXdata( - filename, - nxentry_name=entryPath, - signal=curve.getYData(copy=False), - axes=[curve.getXData(copy=False)], - signal_name="y", - axes_names=["x"], - signal_long_name=ylabel, - axes_long_names=[xlabel], - signal_errors=curve.getYErrorData(copy=False), - axes_errors=[curve.getXErrorData(copy=True)], - title=self.plot.getGraphTitle()) - - def _saveCurve(self, plot, filename, nameFilter): - """Save a curve from the plot. - - :param str filename: The name of the file to write - :param str nameFilter: The selected name filter - :return: False if format is not supported or save failed, - True otherwise. - """ - if nameFilter not in self.DEFAULT_CURVE_FILTERS: - return False - - # Check if a curve is to be saved - curve = plot.getActiveCurve() - # before calling _saveCurve, if there is no selected curve, we - # make sure there is only one curve on the graph - if curve is None: - curves = plot.getAllCurves() - if not curves: - self._errorMessage("No curve to be saved", parent=self.plot) - return False - curve = curves[0] - - if nameFilter in self.CURVE_FILTERS_TXT: - filter_ = self.CURVE_FILTERS_TXT[nameFilter] - fmt = filter_['fmt'] - csvdelim = filter_['delimiter'] - autoheader = filter_['header'] - else: - # .npy or nxdata - fmt, csvdelim, autoheader = ("", "", False) - - if nameFilter == self.CURVE_FILTER_NXDATA: - return self._saveCurveAsNXdata(curve, filename) - - xdata, data, xlabel, labels = self._get1dData(curve) - - try: - save1D(filename, - xdata, data, - xlabel, labels, - fmt=fmt, csvdelim=csvdelim, - autoheader=autoheader) - except IOError: - self._errorMessage('Save failed\n', parent=self.plot) - return False - - return True - - def _saveCurves(self, plot, filename, nameFilter): - """Save all curves from the plot. - - :param str filename: The name of the file to write - :param str nameFilter: The selected name filter - :return: False if format is not supported or save failed, - True otherwise. - """ - if nameFilter not in self.DEFAULT_ALL_CURVES_FILTERS: - return False - - curves = plot.getAllCurves() - if not curves: - self._errorMessage("No curves to be saved", parent=self.plot) - return False - - curve = curves[0] - scanno = 1 - try: - xdata, data, xlabel, labels = self._get1dData(curve) - - specfile = savespec(filename, - xdata, data, - xlabel, labels, - fmt="%.7g", scan_number=1, mode="w", - write_file_header=True, - close_file=False) - except IOError: - self._errorMessage('Save failed\n', parent=self.plot) - return False - - for curve in curves[1:]: - try: - scanno += 1 - xdata, data, xlabel, labels = self._get1dData(curve) - specfile = savespec(specfile, - xdata, data, - xlabel, labels, - fmt="%.7g", scan_number=scanno, - write_file_header=False, - close_file=False) - except IOError: - self._errorMessage('Save failed\n', parent=self.plot) - return False - specfile.close() - - return True - - def _saveImage(self, plot, filename, nameFilter): - """Save an image from the plot. - - :param str filename: The name of the file to write - :param str nameFilter: The selected name filter - :return: False if format is not supported or save failed, - True otherwise. - """ - if nameFilter not in self.DEFAULT_IMAGE_FILTERS: - return False - - image = plot.getActiveImage() - if image is None: - qt.QMessageBox.warning( - plot, "No Data", "No image to be saved") - return False - - data = image.getData(copy=False) - - # TODO Use silx.io for writing files - if nameFilter == self.IMAGE_FILTER_EDF: - edfFile = EdfFile(filename, access="w+") - edfFile.WriteImage({}, data, Append=0) - return True - - elif nameFilter == self.IMAGE_FILTER_TIFF: - tiffFile = TiffIO(filename, mode='w') - tiffFile.writeImage(data, software='silx') - return True - - elif nameFilter == self.IMAGE_FILTER_NUMPY: - try: - numpy.save(filename, data) - except IOError: - self._errorMessage('Save failed\n', parent=self.plot) - return False - return True - - elif nameFilter == self.IMAGE_FILTER_NXDATA: - entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot) - if entryPath is None: - return False - xorigin, yorigin = image.getOrigin() - xscale, yscale = image.getScale() - xaxis = xorigin + xscale * numpy.arange(data.shape[1]) - yaxis = yorigin + yscale * numpy.arange(data.shape[0]) - xlabel, ylabel = self._getAxesLabels(image) - interpretation = "image" if len(data.shape) == 2 else "rgba-image" - - return save_NXdata(filename, - nxentry_name=entryPath, - signal=data, - axes=[yaxis, xaxis], - signal_name="image", - axes_names=["y", "x"], - axes_long_names=[ylabel, xlabel], - title=plot.getGraphTitle(), - interpretation=interpretation) - - elif nameFilter in (self.IMAGE_FILTER_ASCII, - self.IMAGE_FILTER_CSV_COMMA, - self.IMAGE_FILTER_CSV_SEMICOLON, - self.IMAGE_FILTER_CSV_TAB): - csvdelim, filetype = { - self.IMAGE_FILTER_ASCII: (' ', 'txt'), - self.IMAGE_FILTER_CSV_COMMA: (',', 'csv'), - self.IMAGE_FILTER_CSV_SEMICOLON: (';', 'csv'), - self.IMAGE_FILTER_CSV_TAB: ('\t', 'csv'), - }[nameFilter] - - height, width = data.shape - rows, cols = numpy.mgrid[0:height, 0:width] - try: - save1D(filename, rows.ravel(), (cols.ravel(), data.ravel()), - filetype=filetype, - xlabel='row', - ylabels=['column', 'value'], - csvdelim=csvdelim, - autoheader=True) - - except IOError: - self._errorMessage('Save failed\n', parent=self.plot) - return False - return True - - elif nameFilter == self.IMAGE_FILTER_RGB_PNG: - # Get displayed image - rgbaImage = image.getRgbaImageData(copy=False) - # Convert RGB QImage - qimage = convertArrayToQImage(rgbaImage[:, :, :3]) - - if qimage.save(filename, 'PNG'): - return True - else: - _logger.error('Failed to save image as %s', filename) - qt.QMessageBox.critical( - self.parent(), - 'Save image as', - 'Failed to save image') - - return False - - def _saveScatter(self, plot, filename, nameFilter): - """Save an image from the plot. - - :param str filename: The name of the file to write - :param str nameFilter: The selected name filter - :return: False if format is not supported or save failed, - True otherwise. - """ - if nameFilter not in self.DEFAULT_SCATTER_FILTERS: - return False - - if nameFilter == self.SCATTER_FILTER_NXDATA: - entryPath = self._selectWriteableOutputGroup(filename, parent=self.plot) - if entryPath is None: - return False - scatter = plot.getScatter() - - x = scatter.getXData(copy=False) - y = scatter.getYData(copy=False) - z = scatter.getValueData(copy=False) - - xerror = scatter.getXErrorData(copy=False) - if isinstance(xerror, float): - xerror = xerror * numpy.ones(x.shape, dtype=numpy.float32) - - yerror = scatter.getYErrorData(copy=False) - if isinstance(yerror, float): - yerror = yerror * numpy.ones(x.shape, dtype=numpy.float32) - - xlabel = plot.getGraphXLabel() - ylabel = plot.getGraphYLabel() - - return save_NXdata( - filename, - nxentry_name=entryPath, - signal=z, - axes=[x, y], - signal_name="values", - axes_names=["x", "y"], - axes_long_names=[xlabel, ylabel], - axes_errors=[xerror, yerror], - title=plot.getGraphTitle()) - - def setFileFilter(self, dataKind, nameFilter, func, index=None, appendToFile=False): - """Set a name filter to add/replace a file format support - - :param str dataKind: - The kind of data for which the provided filter is valid. - One of: 'all', 'curve', 'curves', 'image', 'scatter' - :param str nameFilter: The name filter in the QFileDialog. - See :meth:`QFileDialog.setNameFilters`. - :param callable func: The function to call to perform saving. - Expected signature is: - bool func(PlotWidget plot, str filename, str nameFilter) - :param bool appendToFile: True to append the data into the selected - file. - :param integer index: Index of the filter in the final list (or None) - """ - assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter') - - if appendToFile: - self._appendFilters.append(nameFilter) - - # first append or replace the new filter to prevent colissions - self._filters[dataKind][nameFilter] = func - if index is None: - # we are already done - return - - # get the current ordered list of keys - keyList = list(self._filters[dataKind].keys()) - - # deal with negative indices - if index < 0: - index = len(keyList) + index - if index < 0: - index = 0 - - if index >= len(keyList): - # nothing to be done, already at the end - txt = 'Requested index %d impossible, already at the end' % index - _logger.info(txt) - return - - # get the new ordered list - oldIndex = keyList.index(nameFilter) - del keyList[oldIndex] - keyList.insert(index, nameFilter) - - # build the new filters - newFilters = OrderedDict() - for key in keyList: - newFilters[key] = self._filters[dataKind][key] - - # and update the filters - self._filters[dataKind] = newFilters - return - - def getFileFilters(self, dataKind): - """Returns the nameFilter and associated function for a kind of data. - - :param str dataKind: - The kind of data for which the provided filter is valid. - On of: 'all', 'curve', 'curves', 'image', 'scatter' - :return: {nameFilter: function} associations. - :rtype: collections.OrderedDict - """ - assert dataKind in ('all', 'curve', 'curves', 'image', 'scatter') - - return self._filters[dataKind].copy() - - def _actionTriggered(self, checked=False): - """Handle save action.""" - # Set-up filters - filters = OrderedDict() - - # Add image filters if there is an active image - if self.plot.getActiveImage() is not None: - filters.update(self._filters['image'].items()) - - # Add curve filters if there is a curve to save - if (self.plot.getActiveCurve() is not None or - len(self.plot.getAllCurves()) == 1): - filters.update(self._filters['curve'].items()) - if len(self.plot.getAllCurves()) >= 1: - filters.update(self._filters['curves'].items()) - - # Add scatter filters if there is a scatter - # todo: CSV - if self.plot.getScatter() is not None: - filters.update(self._filters['scatter'].items()) - - filters.update(self._filters['all'].items()) - - # Create and run File dialog - dialog = qt.QFileDialog(self.plot) - dialog.setOption(dialog.DontUseNativeDialog) - dialog.setWindowTitle("Output File Selection") - dialog.setModal(1) - dialog.setNameFilters(list(filters.keys())) - - dialog.setFileMode(dialog.AnyFile) - dialog.setAcceptMode(dialog.AcceptSave) - - def onFilterSelection(filt_): - # disable overwrite confirmation for NXdata types, - # because we append the data to existing files - if filt_ in self._appendFilters: - dialog.setOption(dialog.DontConfirmOverwrite) - else: - dialog.setOption(dialog.DontConfirmOverwrite, False) - - dialog.filterSelected.connect(onFilterSelection) - - if not dialog.exec_(): - return False - - nameFilter = dialog.selectedNameFilter() - filename = dialog.selectedFiles()[0] - dialog.close() - - if '(' in nameFilter and ')' == nameFilter.strip()[-1]: - # Check for correct file extension - # Extract file extensions as .something - extensions = [ext[ext.find('.'):] for ext in - nameFilter[nameFilter.find('(') + 1:-1].split()] - for ext in extensions: - if (len(filename) > len(ext) and - filename[-len(ext):].lower() == ext.lower()): - break - else: # filename has no extension supported in nameFilter, add one - if len(extensions) >= 1: - filename += extensions[0] - - # Handle save - func = filters.get(nameFilter, None) - if func is not None: - return func(self.plot, filename, nameFilter) - else: - _logger.error('Unsupported file filter: %s', nameFilter) - return False - - -def _plotAsPNG(plot): - """Save a :class:`Plot` as PNG and return the payload. - - :param plot: The :class:`Plot` to save - """ - pngFile = BytesIO() - plot.saveGraph(pngFile, fileFormat='png') - pngFile.flush() - pngFile.seek(0) - data = pngFile.read() - pngFile.close() - return data - - -class PrintAction(PlotAction): - """QAction for printing the plot. - - It opens a Print dialog. - - Current implementation print a bitmap of the plot area and not vector - graphics, so printing quality is not great. - - :param plot: :class:`.PlotWidget` instance on which to operate. - :param parent: See :class:`QAction`. - """ - - def __init__(self, plot, parent=None): - super(PrintAction, self).__init__( - plot, icon='document-print', text='Print...', - tooltip='Open print dialog', - triggered=self.printPlot, - checkable=False, parent=parent) - self.setShortcut(qt.QKeySequence.Print) - self.setShortcutContext(qt.Qt.WidgetShortcut) - - def getPrinter(self): - """The QPrinter instance used by the PrintAction. - - :rtype: QPrinter - """ - return printer.getDefaultPrinter() - - @property - @deprecated(replacement="getPrinter()", since_version="0.8.0") - def printer(self): - return self.getPrinter() - - def printPlotAsWidget(self): - """Open the print dialog and print the plot. - - Use :meth:`QWidget.render` to print the plot - - :return: True if successful - """ - dialog = qt.QPrintDialog(self.getPrinter(), self.plot) - dialog.setWindowTitle('Print Plot') - if not dialog.exec_(): - return False - - # Print a snapshot of the plot widget at the top of the page - widget = self.plot.centralWidget() - - painter = qt.QPainter() - if not painter.begin(self.getPrinter()): - return False - - pageRect = self.getPrinter().pageRect() - xScale = pageRect.width() / widget.width() - yScale = pageRect.height() / widget.height() - scale = min(xScale, yScale) - - painter.translate(pageRect.width() / 2., 0.) - painter.scale(scale, scale) - painter.translate(-widget.width() / 2., 0.) - widget.render(painter) - painter.end() - - return True - - def printPlot(self): - """Open the print dialog and print the plot. - - Use :meth:`Plot.saveGraph` to print the plot. - - :return: True if successful - """ - # Init printer and start printer dialog - dialog = qt.QPrintDialog(self.getPrinter(), self.plot) - dialog.setWindowTitle('Print Plot') - if not dialog.exec_(): - return False - - # Save Plot as PNG and make a pixmap from it with default dpi - pngData = _plotAsPNG(self.plot) - - pixmap = qt.QPixmap() - pixmap.loadFromData(pngData, 'png') - - xScale = self.getPrinter().pageRect().width() / pixmap.width() - yScale = self.getPrinter().pageRect().height() / pixmap.height() - scale = min(xScale, yScale) - - # Draw pixmap with painter - painter = qt.QPainter() - if not painter.begin(self.getPrinter()): - return False - - painter.drawPixmap(0, 0, - pixmap.width() * scale, - pixmap.height() * scale, - pixmap) - painter.end() - - return True - - -class CopyAction(PlotAction): - """QAction to copy :class:`.PlotWidget` content to clipboard. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(CopyAction, self).__init__( - plot, icon='edit-copy', text='Copy plot', - tooltip='Copy a snapshot of the plot into the clipboard', - triggered=self.copyPlot, - checkable=False, parent=parent) - self.setShortcut(qt.QKeySequence.Copy) - self.setShortcutContext(qt.Qt.WidgetShortcut) - - def copyPlot(self): - """Copy plot content to the clipboard as a bitmap.""" - # Save Plot as PNG and make a QImage from it with default dpi - pngData = _plotAsPNG(self.plot) - image = qt.QImage.fromData(pngData, 'png') - qt.QApplication.clipboard().setImage(image) diff --git a/silx/gui/plot/actions/medfilt.py b/silx/gui/plot/actions/medfilt.py deleted file mode 100644 index f86a377..0000000 --- a/silx/gui/plot/actions/medfilt.py +++ /dev/null @@ -1,147 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -:mod:`silx.gui.plot.actions.medfilt` provides a set of QAction to apply filter -on data contained in a :class:`.PlotWidget`. - -The following QAction are available: - -- :class:`MedianFilterAction` -- :class:`MedianFilter1DAction` -- :class:`MedianFilter2DAction` - -""" - -from __future__ import division - -__authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] -__license__ = "MIT" - -__date__ = "10/10/2018" - -from .PlotToolAction import PlotToolAction -from silx.gui.widgets.MedianFilterDialog import MedianFilterDialog -from silx.math.medianfilter import medfilt2d -import logging - -_logger = logging.getLogger(__name__) - - -class MedianFilterAction(PlotToolAction): - """QAction to plot the pixels intensities diagram - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - PlotToolAction.__init__(self, - plot, - icon='median-filter', - text='median filter', - tooltip='Apply a median filter on the image', - parent=parent) - self._originalImage = None - self._legend = None - self._filteredImage = None - - def _createToolWindow(self): - popup = MedianFilterDialog(parent=self.plot) - popup.sigFilterOptChanged.connect(self._updateFilter) - return popup - - def _connectPlot(self, window): - PlotToolAction._connectPlot(self, window) - self.plot.sigActiveImageChanged.connect(self._updateActiveImage) - self._updateActiveImage() - - def _disconnectPlot(self, window): - PlotToolAction._disconnectPlot(self, window) - self.plot.sigActiveImageChanged.disconnect(self._updateActiveImage) - - def _updateActiveImage(self): - """Set _activeImageLegend and _originalImage from the active image""" - self._activeImageLegend = self.plot.getActiveImage(just_legend=True) - if self._activeImageLegend is None: - self._originalImage = None - self._legend = None - else: - self._originalImage = self.plot.getImage(self._activeImageLegend).getData(copy=False) - self._legend = self.plot.getImage(self._activeImageLegend).getName() - - def _updateFilter(self, kernelWidth, conditional=False): - if self._originalImage is None: - return - - self.plot.sigActiveImageChanged.disconnect(self._updateActiveImage) - filteredImage = self._computeFilteredImage(kernelWidth, conditional) - self.plot.addImage(data=filteredImage, - legend=self._legend, - replace=True) - self.plot.sigActiveImageChanged.connect(self._updateActiveImage) - - def _computeFilteredImage(self, kernelWidth, conditional): - raise NotImplementedError('MedianFilterAction is a an abstract class') - - def getFilteredImage(self): - """ - :return: the image with the median filter apply on""" - return self._filteredImage - - -class MedianFilter1DAction(MedianFilterAction): - """Define the MedianFilterAction for 1D - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - def __init__(self, plot, parent=None): - MedianFilterAction.__init__(self, - plot, - parent=parent) - - def _computeFilteredImage(self, kernelWidth, conditional): - assert(self.plot is not None) - return medfilt2d(self._originalImage, - (kernelWidth, 1), - conditional) - - -class MedianFilter2DAction(MedianFilterAction): - """Define the MedianFilterAction for 2D - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - def __init__(self, plot, parent=None): - MedianFilterAction.__init__(self, - plot, - parent=parent) - - def _computeFilteredImage(self, kernelWidth, conditional): - assert(self.plot is not None) - return medfilt2d(self._originalImage, - (kernelWidth, kernelWidth), - conditional) diff --git a/silx/gui/plot/actions/mode.py b/silx/gui/plot/actions/mode.py deleted file mode 100644 index ee05256..0000000 --- a/silx/gui/plot/actions/mode.py +++ /dev/null @@ -1,104 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -:mod:`silx.gui.plot.actions.mode` provides a set of QAction relative to mouse -mode of a :class:`.PlotWidget`. - -The following QAction are available: - -- :class:`ZoomModeAction` -- :class:`PanModeAction` -""" - -from __future__ import division - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "16/08/2017" - -from . import PlotAction -import logging - -_logger = logging.getLogger(__name__) - - -class ZoomModeAction(PlotAction): - """QAction controlling the zoom mode of a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(ZoomModeAction, self).__init__( - plot, icon='zoom', text='Zoom mode', - tooltip='Zoom in or out', - triggered=self._actionTriggered, - checkable=True, parent=parent) - # Listen to mode change - self.plot.sigInteractiveModeChanged.connect(self._modeChanged) - # Init the state - self._modeChanged(None) - - def _modeChanged(self, source): - modeDict = self.plot.getInteractiveMode() - old = self.blockSignals(True) - self.setChecked(modeDict["mode"] == "zoom") - self.blockSignals(old) - - def _actionTriggered(self, checked=False): - plot = self.plot - if plot is not None: - plot.setInteractiveMode('zoom', source=self) - - -class PanModeAction(PlotAction): - """QAction controlling the pan mode of a :class:`.PlotWidget`. - - :param plot: :class:`.PlotWidget` instance on which to operate - :param parent: See :class:`QAction` - """ - - def __init__(self, plot, parent=None): - super(PanModeAction, self).__init__( - plot, icon='pan', text='Pan mode', - tooltip='Pan the view', - triggered=self._actionTriggered, - checkable=True, parent=parent) - # Listen to mode change - self.plot.sigInteractiveModeChanged.connect(self._modeChanged) - # Init the state - self._modeChanged(None) - - def _modeChanged(self, source): - modeDict = self.plot.getInteractiveMode() - old = self.blockSignals(True) - self.setChecked(modeDict["mode"] == "pan") - self.blockSignals(old) - - def _actionTriggered(self, checked=False): - plot = self.plot - if plot is not None: - plot.setInteractiveMode('pan', source=self) |