Source code for silx.gui.plot.PlotToolButtons

# /*##########################################################################
#
# Copyright (c) 2004-2020 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
# ###########################################################################*/
"""This module provides a set of QToolButton to use with
:class:`~silx.gui.plot.PlotWidget`.

The following QToolButton are available:

- :class:`.AspectToolButton`
- :class:`.YAxisOriginToolButton`
- :class:`.ProfileToolButton`
- :class:`.SymbolToolButton`

"""

__authors__ = ["V. Valls", "H. Payno"]
__license__ = "MIT"
__date__ = "27/06/2017"


import functools
import logging
import weakref

from .. import icons
from .. import qt
from ... import config

from .items import SymbolMixIn, Scatter


_logger = logging.getLogger(__name__)


[docs]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)
[docs] def plot(self): """ Returns the plot connected to the widget. """ return None if self._plotRef is None else self._plotRef()
[docs] 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
[docs]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)
[docs] 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)
[docs] 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)
[docs]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)
[docs] 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)
[docs] 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)
[docs]class ProfileOptionToolButton(PlotToolButton): """Button to define option on the profile""" sigMethodChanged = qt.Signal(str) def __init__(self, parent=None, plot=None): PlotToolButton.__init__(self, parent=parent, plot=plot) self.STATE = {} # is down self.STATE['sum', "icon"] = icons.getQIcon('math-sigma') self.STATE['sum', "state"] = "Compute profile sum" self.STATE['sum', "action"] = "Compute profile sum" # keep ration self.STATE['mean', "icon"] = icons.getQIcon('math-mean') self.STATE['mean', "state"] = "Compute profile mean" self.STATE['mean', "action"] = "Compute profile mean" self.sumAction = self._createAction('sum') self.sumAction.triggered.connect(self.setSum) self.sumAction.setIconVisibleInMenu(True) self.sumAction.setCheckable(True) self.sumAction.setChecked(True) self.meanAction = self._createAction('mean') self.meanAction.triggered.connect(self.setMean) self.meanAction.setIconVisibleInMenu(True) self.meanAction.setCheckable(True) menu = qt.QMenu(self) menu.addAction(self.sumAction) menu.addAction(self.meanAction) self.setMenu(menu) self.setPopupMode(qt.QToolButton.InstantPopup) self._method = 'mean' self._update() def _createAction(self, method): icon = self.STATE[method, "icon"] text = self.STATE[method, "action"] return qt.QAction(icon, text, self) def setSum(self): self.setMethod('sum') def _update(self): icon = self.STATE[self._method, "icon"] toolTip = self.STATE[self._method, "state"] self.setIcon(icon) self.setToolTip(toolTip) self.sumAction.setChecked(self._method == "sum") self.meanAction.setChecked(self._method == "mean") def setMean(self): self.setMethod('mean')
[docs] def setMethod(self, method): """Set the method to use. :param str method: Either 'sum' or 'mean' """ if method != self._method: if method in ('sum', 'mean'): self._method = method self.sigMethodChanged.emit(self._method) self._update() else: _logger.warning( "Unsupported method '%s'. Setting ignored.", method)
[docs] def getMethod(self): """Returns the current method in use (See :meth:`setMethod`). :rtype: str """ return self._method
[docs]class ProfileToolButton(PlotToolButton): """Button used in Profile3DToolbar to switch between 2D profile and 1D profile.""" STATE = None """Lazy loaded states used to feed ProfileToolButton""" sigDimensionChanged = qt.Signal(int) def __init__(self, parent=None, plot=None): if self.STATE is None: self.STATE = { (1, "icon"): icons.getQIcon('profile1D'), (1, "state"): "1D profile is computed on visible image", (1, "action"): "1D profile on visible image", (2, "icon"): icons.getQIcon('profile2D'), (2, "state"): "2D profile is computed, one 1D profile for each image in the stack", (2, "action"): "2D profile on image stack"} # Compute 1D profile # Compute 2D profile super(ProfileToolButton, self).__init__(parent=parent, plot=plot) self._dimension = 1 profile1DAction = self._createAction(1) profile1DAction.triggered.connect(self.computeProfileIn1D) profile1DAction.setIconVisibleInMenu(True) profile1DAction.setCheckable(True) profile1DAction.setChecked(True) self._profile1DAction = profile1DAction profile2DAction = self._createAction(2) profile2DAction.triggered.connect(self.computeProfileIn2D) profile2DAction.setIconVisibleInMenu(True) profile2DAction.setCheckable(True) self._profile2DAction = profile2DAction menu = qt.QMenu(self) menu.addAction(profile1DAction) menu.addAction(profile2DAction) self.setMenu(menu) self.setPopupMode(qt.QToolButton.InstantPopup) menu.setTitle('Select profile dimension') self.computeProfileIn1D() def _createAction(self, profileDimension): icon = self.STATE[profileDimension, "icon"] text = self.STATE[profileDimension, "action"] return qt.QAction(icon, text, self) def _profileDimensionChanged(self, profileDimension): """Update icon in toolbar, emit number of dimensions for profile""" self.setIcon(self.STATE[profileDimension, "icon"]) self.setToolTip(self.STATE[profileDimension, "state"]) self._dimension = profileDimension self.sigDimensionChanged.emit(profileDimension) self._profile1DAction.setChecked(profileDimension == 1) self._profile2DAction.setChecked(profileDimension == 2) def computeProfileIn1D(self): self._profileDimensionChanged(1) def computeProfileIn2D(self): self._profileDimensionChanged(2)
[docs] def setDimension(self, dimension): """Set the selected dimension""" assert dimension in [1, 2] if self._dimension == dimension: return if dimension == 1: self.computeProfileIn1D() elif dimension == 2: self.computeProfileIn2D() else: _logger.warning("Unsupported dimension '%s'. Setting ignored.", dimension)
[docs] def getDimension(self): """Get the selected dimension. :rtype: int (1 or 2) """ return self._dimension
class _SymbolToolButtonBase(PlotToolButton): """Base class for PlotToolButton setting marker and size. :param parent: See QWidget :param plot: The `~silx.gui.plot.PlotWidget` to control """ def __init__(self, parent=None, plot=None): super(_SymbolToolButtonBase, self).__init__(parent=parent, plot=plot) def _addSizeSliderToMenu(self, menu): """Add a slider to set size to the given menu :param QMenu menu: """ slider = qt.QSlider(qt.Qt.Horizontal) slider.setRange(1, 20) slider.setValue(int(config.DEFAULT_PLOT_SYMBOL_SIZE)) slider.setTracking(False) slider.valueChanged.connect(self._sizeChanged) widgetAction = qt.QWidgetAction(menu) widgetAction.setDefaultWidget(slider) menu.addAction(widgetAction) def _addSymbolsToMenu(self, menu): """Add symbols to the given menu :param QMenu menu: """ for marker, name in zip(SymbolMixIn.getSupportedSymbols(), SymbolMixIn.getSupportedSymbolNames()): action = qt.QAction(name, menu) action.setCheckable(False) action.triggered.connect( functools.partial(self._markerChanged, marker)) menu.addAction(action) def _sizeChanged(self, value): """Manage slider value changed :param int value: Marker size """ plot = self.plot() if plot is None: return for item in plot.getItems(): if isinstance(item, SymbolMixIn): item.setSymbolSize(value) def _markerChanged(self, marker): """Manage change of marker. :param str marker: Letter describing the marker """ plot = self.plot() if plot is None: return for item in plot.getItems(): if isinstance(item, SymbolMixIn): item.setSymbol(marker)
[docs]class SymbolToolButton(_SymbolToolButtonBase): """A tool button with a drop-down menu to control symbol size and marker. :param parent: See QWidget :param plot: The `~silx.gui.plot.PlotWidget` to control """ def __init__(self, parent=None, plot=None): super(SymbolToolButton, self).__init__(parent=parent, plot=plot) self.setToolTip('Set symbol size and marker') self.setIcon(icons.getQIcon('plot-symbols')) menu = qt.QMenu(self) self._addSizeSliderToMenu(menu) menu.addSeparator() self._addSymbolsToMenu(menu) self.setMenu(menu) self.setPopupMode(qt.QToolButton.InstantPopup)
[docs]class ScatterVisualizationToolButton(_SymbolToolButtonBase): """QToolButton to select the visualization mode of scatter plot :param parent: See QWidget :param plot: The `~silx.gui.plot.PlotWidget` to control """ def __init__(self, parent=None, plot=None): super(ScatterVisualizationToolButton, self).__init__( parent=parent, plot=plot) self.setToolTip( 'Set scatter visualization mode, symbol marker and size') self.setIcon(icons.getQIcon('eye')) menu = qt.QMenu(self) # Add visualization modes for mode in Scatter.supportedVisualizations(): if mode is not Scatter.Visualization.BINNED_STATISTIC: name = mode.value.capitalize() action = qt.QAction(name, menu) action.setCheckable(False) action.triggered.connect( functools.partial(self._visualizationChanged, mode, None)) menu.addAction(action) if Scatter.Visualization.BINNED_STATISTIC in Scatter.supportedVisualizations(): reductions = Scatter.supportedVisualizationParameterValues( Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION) if reductions: submenu = menu.addMenu('Binned Statistic') for reduction in reductions: name = reduction.capitalize() action = qt.QAction(name, menu) action.setCheckable(False) action.triggered.connect(functools.partial( self._visualizationChanged, Scatter.Visualization.BINNED_STATISTIC, {Scatter.VisualizationParameter.BINNED_STATISTIC_FUNCTION: reduction})) submenu.addAction(action) submenu.addSeparator() binsmenu = submenu.addMenu('N Bins') slider = qt.QSlider(qt.Qt.Horizontal) slider.setRange(10, 1000) slider.setValue(100) slider.setTracking(False) slider.valueChanged.connect(self._binningChanged) widgetAction = qt.QWidgetAction(binsmenu) widgetAction.setDefaultWidget(slider) binsmenu.addAction(widgetAction) menu.addSeparator() submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol") self._addSymbolsToMenu(submenu) submenu = menu.addMenu(icons.getQIcon('plot-symbols'), "Symbol Size") self._addSizeSliderToMenu(submenu) self.setMenu(menu) self.setPopupMode(qt.QToolButton.InstantPopup) def _visualizationChanged(self, mode, parameters=None): """Handle change of visualization mode. :param ScatterVisualizationMixIn.Visualization mode: The visualization mode to use for scatter :param Union[dict,None] parameters: Dict of VisualizationParameter: parameter_value to set with the visualization. """ plot = self.plot() if plot is None: return for item in plot.getItems(): if isinstance(item, Scatter): if parameters: for parameter, value in parameters.items(): item.setVisualizationParameter(parameter, value) item.setVisualization(mode) def _binningChanged(self, value): """Handle change of binning. :param int value: The number of bin on each dimension. """ plot = self.plot() if plot is None: return for item in plot.getItems(): if isinstance(item, Scatter): item.setVisualizationParameter( Scatter.VisualizationParameter.BINNED_STATISTIC_SHAPE, (value, value)) item.setVisualization(Scatter.Visualization.BINNED_STATISTIC)