# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal # in the Software without restriction, including without limitation the rights # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell # copies of the Software, and to permit persons to whom the Software is # furnished to do so, subject to the following conditions: # # The above copyright notice and this permission notice shall be included in # all copies or substantial portions of the Software. # # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. # # ###########################################################################*/ """This module provides the class for axes of the :class:`PlotWidget`. """ __authors__ = ["V. Valls"] __license__ = "MIT" __date__ = "30/08/2017" import logging from ... import qt _logger = logging.getLogger(__name__) class Axis(qt.QObject): """This class describes and controls a plot axis. Note: This is an abstract class. """ # States are half-stored on the backend of the plot, and half-stored on this # object. # TODO It would be good to store all the states of an axis in this object. # i.e. vmin and vmax LINEAR = "linear" """Constant defining a linear scale""" LOGARITHMIC = "log" """Constant defining a logarithmic scale""" _SCALES = set([LINEAR, LOGARITHMIC]) sigInvertedChanged = qt.Signal(bool) """Signal emitted when axis orientation has changed""" sigScaleChanged = qt.Signal(str) """Signal emitted when axis scale has changed""" _sigLogarithmicChanged = qt.Signal(bool) """Signal emitted when axis scale has changed to or from logarithmic""" sigAutoScaleChanged = qt.Signal(bool) """Signal emitted when axis autoscale has changed""" sigLimitsChanged = qt.Signal(float, float) """Signal emitted when axis autoscale has changed""" def __init__(self, plot): """Constructor :param silx.gui.plot.PlotWidget.PlotWidget plot: Parent plot of this axis """ qt.QObject.__init__(self, parent=plot) self._scale = self.LINEAR self._isAutoScale = True # Store default labels provided to setGraph[X|Y]Label self._defaultLabel = '' # Store currently displayed labels # Current label can differ from input one with active curve handling self._currentLabel = '' self._plot = plot def getLimits(self): """Get the limits of this axis. :return: Minimum and maximum values of this axis as tuple """ return self._internalGetLimits() def setLimits(self, vmin, vmax): """Set this axis limits. :param float vmin: minimum axis value :param float vmax: maximum axis value """ vmin, vmax = self._checkLimits(vmin, vmax) if self.getLimits() == (vmin, vmax): return self._internalSetLimits(vmin, vmax) self._plot._setDirtyPlot() self._emitLimitsChanged() def _emitLimitsChanged(self): """Emit axis sigLimitsChanged and PlotWidget limitsChanged event""" vmin, vmax = self.getLimits() self.sigLimitsChanged.emit(vmin, vmax) self._plot._notifyLimitsChanged(emitSignal=False) def _checkLimits(self, vmin, vmax): """Makes sure axis range is not empty :param float vmin: Min axis value :param float vmax: Max axis value :return: (min, max) making sure min < max :rtype: 2-tuple of float """ if vmax < vmin: _logger.debug('%s axis: max < min, inverting limits.', self._defaultLabel) vmin, vmax = vmax, vmin elif vmax == vmin: _logger.debug('%s axis: max == min, expanding limits.', self._defaultLabel) if vmin == 0.: vmin, vmax = -0.1, 0.1 elif vmin < 0: vmin, vmax = vmin * 1.1, vmin * 0.9 else: # xmin > 0 vmin, vmax = vmin * 0.9, vmin * 1.1 return vmin, vmax def isInverted(self): """Return True if the axis is inverted (top to bottom for the y-axis), False otherwise. It is always False for the X axis. :rtype: bool """ return False def setInverted(self, isInverted): """Set the axis orientation. This is only available for the Y axis. :param bool flag: True for Y axis going from top to bottom, False for Y axis going from bottom to top """ if isInverted == self.isInverted(): return raise NotImplementedError() def getLabel(self): """Return the current displayed label of this axis. :param str axis: The Y axis for which to get the label (left or right) :rtype: str """ return self._currentLabel def setLabel(self, label): """Set the label displayed on the plot for this axis. The provided label can be temporarily replaced by the label of the active curve if any. :param str label: The axis label """ self._defaultLabel = label self._setCurrentLabel(label) self._plot._setDirtyPlot() def _setCurrentLabel(self, label): """Define the label currently displayed. If the label is None or empty the default label is used. :param str label: Currently displayed label """ if label is None or label == '': label = self._defaultLabel if label is None: label = '' self._currentLabel = label self._internalSetCurrentLabel(label) def getScale(self): """Return the name of the scale used by this axis. :rtype: str """ return self._scale def setScale(self, scale): """Set the scale to be used by this axis. :param str scale: Name of the scale ("log", or "linear") """ assert(scale in self._SCALES) if self._scale == scale: return # For the backward compatibility signal emitLog = self._scale == self.LOGARITHMIC or scale == self.LOGARITHMIC if scale == self.LOGARITHMIC: self._internalSetLogarithmic(True) elif scale == self.LINEAR: self._internalSetLogarithmic(False) else: raise ValueError("Scale %s unsupported" % scale) self._scale = scale # TODO hackish way of forcing update of curves and images for item in self._plot._getItems(withhidden=True): item._updated() self._plot._invalidateDataRange() self._plot.resetZoom() self.sigScaleChanged.emit(self._scale) if emitLog: self._sigLogarithmicChanged.emit(self._scale == self.LOGARITHMIC) def _isLogarithmic(self): """Return True if this axis scale is logarithmic, False if linear. :rtype: bool """ return self._scale == self.LOGARITHMIC def _setLogarithmic(self, flag): """Set the scale of this axes (either linear or logarithmic). :param bool flag: True to use a logarithmic scale, False for linear. """ flag = bool(flag) self.setScale(self.LOGARITHMIC if flag else self.LINEAR) def isAutoScale(self): """Return True if axis is automatically adjusting its limits. :rtype: bool """ return self._isAutoScale def setAutoScale(self, flag=True): """Set the axis limits adjusting behavior of :meth:`resetZoom`. :param bool flag: True to resize limits automatically, False to disable it. """ self._isAutoScale = bool(flag) self.sigAutoScaleChanged.emit(self._isAutoScale) def _setLimitsConstraints(self, minPos=None, maxPos=None): raise NotImplementedError() def setLimitsConstraints(self, minPos=None, maxPos=None): """ Set a constaints on the position of the axes. :param float minPos: Minimum allowed axis value. :param float maxPos: Maximum allowed axis value. :return: True if the constaints was updated :rtype: bool """ updated = self._setLimitsConstraints(minPos, maxPos) if updated: plot = self._plot xMin, xMax = plot.getXAxis().getLimits() yMin, yMax = plot.getYAxis().getLimits() y2Min, y2Max = plot.getYAxis('right').getLimits() plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max) return updated def _setRangeConstraints(self, minRange=None, maxRange=None): raise NotImplementedError() def setRangeConstraints(self, minRange=None, maxRange=None): """ Set a constaints on the position of the axes. :param float minRange: Minimum allowed left-to-right span across the view :param float maxRange: Maximum allowed left-to-right span across the view :return: True if the constaints was updated :rtype: bool """ updated = self._setRangeConstraints(minRange, maxRange) if updated: plot = self._plot xMin, xMax = plot.getXAxis().getLimits() yMin, yMax = plot.getYAxis().getLimits() y2Min, y2Max = plot.getYAxis('right').getLimits() plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max) return updated class XAxis(Axis): """Axis class defining primitives for the X axis""" # TODO With some changes on the backend, it will be able to remove all this # specialised implementations (prefixel by '_internal') def _internalSetCurrentLabel(self, label): self._plot._backend.setGraphXLabel(label) def _internalGetLimits(self): return self._plot._backend.getGraphXLimits() def _internalSetLimits(self, xmin, xmax): self._plot._backend.setGraphXLimits(xmin, xmax) def _internalSetLogarithmic(self, flag): self._plot._backend.setXAxisLogarithmic(flag) def _setLimitsConstraints(self, minPos=None, maxPos=None): constrains = self._plot._getViewConstraints() updated = constrains.update(xMin=minPos, xMax=maxPos) return updated def _setRangeConstraints(self, minRange=None, maxRange=None): constrains = self._plot._getViewConstraints() updated = constrains.update(minXRange=minRange, maxXRange=maxRange) return updated class YAxis(Axis): """Axis class defining primitives for the Y axis""" # TODO With some changes on the backend, it will be able to remove all this # specialised implementations (prefixel by '_internal') def _internalSetCurrentLabel(self, label): self._plot._backend.setGraphYLabel(label, axis='left') def _internalGetLimits(self): return self._plot._backend.getGraphYLimits(axis='left') def _internalSetLimits(self, ymin, ymax): self._plot._backend.setGraphYLimits(ymin, ymax, axis='left') def _internalSetLogarithmic(self, flag): self._plot._backend.setYAxisLogarithmic(flag) def setInverted(self, flag=True): """Set the axis orientation. This is only available for the Y axis. :param bool flag: True for Y axis going from top to bottom, False for Y axis going from bottom to top """ flag = bool(flag) self._plot._backend.setYAxisInverted(flag) self._plot._setDirtyPlot() self.sigInvertedChanged.emit(flag) def isInverted(self): """Return True if the axis is inverted (top to bottom for the y-axis), False otherwise. It is always False for the X axis. :rtype: bool """ return self._plot._backend.isYAxisInverted() def _setLimitsConstraints(self, minPos=None, maxPos=None): constrains = self._plot._getViewConstraints() updated = constrains.update(yMin=minPos, yMax=maxPos) return updated def _setRangeConstraints(self, minRange=None, maxRange=None): constrains = self._plot._getViewConstraints() updated = constrains.update(minYRange=minRange, maxYRange=maxRange) return updated class YRightAxis(Axis): """Proxy axis for the secondary Y axes. It manages it own label and limit but share the some state like scale and direction with the main axis.""" # TODO With some changes on the backend, it will be able to remove all this # specialised implementations (prefixel by '_internal') def __init__(self, plot, mainAxis): """Constructor :param silx.gui.plot.PlotWidget.PlotWidget plot: Parent plot of this axis :param Axis mainAxis: Axis which sharing state with this axis """ Axis.__init__(self, plot) self.__mainAxis = mainAxis @property def sigInvertedChanged(self): """Signal emitted when axis orientation has changed""" return self.__mainAxis.sigInvertedChanged @property def sigScaleChanged(self): """Signal emitted when axis scale has changed""" return self.__mainAxis.sigScaleChanged @property def _sigLogarithmicChanged(self): """Signal emitted when axis scale has changed to or from logarithmic""" return self.__mainAxis._sigLogarithmicChanged @property def sigAutoScaleChanged(self): """Signal emitted when axis autoscale has changed""" return self.__mainAxis.sigAutoScaleChanged def _internalSetCurrentLabel(self, label): self._plot._backend.setGraphYLabel(label, axis='right') def _internalGetLimits(self): return self._plot._backend.getGraphYLimits(axis='right') def _internalSetLimits(self, ymin, ymax): self._plot._backend.setGraphYLimits(ymin, ymax, axis='right') def setInverted(self, flag=True): """Set the Y axis orientation. :param bool flag: True for Y axis going from top to bottom, False for Y axis going from bottom to top """ return self.__mainAxis.setInverted(flag) def isInverted(self): """Return True if Y axis goes from top to bottom, False otherwise.""" return self.__mainAxis.isInverted() def getScale(self): """Return the name of the scale used by this axis. :rtype: str """ return self.__mainAxis.getScale() def setScale(self, scale): """Set the scale to be used by this axis. :param str scale: Name of the scale ("log", or "linear") """ self.__mainAxis.setScale(scale) def _isLogarithmic(self): """Return True if Y axis scale is logarithmic, False if linear.""" return self.__mainAxis._isLogarithmic() def _setLogarithmic(self, flag): """Set the Y axes scale (either linear or logarithmic). :param bool flag: True to use a logarithmic scale, False for linear. """ return self.__mainAxis._setLogarithmic(flag) def isAutoScale(self): """Return True if Y axes are automatically adjusting its limits.""" return self.__mainAxis.isAutoScale() def setAutoScale(self, flag=True): """Set the Y axis limits adjusting behavior of :meth:`PlotWidget.resetZoom`. :param bool flag: True to resize limits automatically, False to disable it. """ return self.__mainAxis.setAutoScale(flag)