# 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__)
[docs]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
[docs] def getLimits(self):
"""Get the limits of this axis.
:return: Minimum and maximum values of this axis as tuple
"""
return self._internalGetLimits()
[docs] 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
[docs] 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
[docs] 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()
[docs] 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
[docs] 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)
[docs] def getScale(self):
"""Return the name of the scale used by this axis.
:rtype: str
"""
return self._scale
[docs] 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)
[docs] def isAutoScale(self):
"""Return True if axis is automatically adjusting its limits.
:rtype: bool
"""
return self._isAutoScale
[docs] 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()
[docs] 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()
[docs] 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)