diff options
Diffstat (limited to 'src/silx/gui/plot/_utils/panzoom.py')
-rw-r--r-- | src/silx/gui/plot/_utils/panzoom.py | 136 |
1 files changed, 89 insertions, 47 deletions
diff --git a/src/silx/gui/plot/_utils/panzoom.py b/src/silx/gui/plot/_utils/panzoom.py index 8592ad0..cac591d 100644 --- a/src/silx/gui/plot/_utils/panzoom.py +++ b/src/silx/gui/plot/_utils/panzoom.py @@ -1,6 +1,6 @@ # /*########################################################################## # -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility +# Copyright (c) 2004-2023 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 @@ -23,6 +23,8 @@ # ###########################################################################*/ """Functions to apply pan and zoom on a Plot""" +from __future__ import annotations + __authors__ = ["T. Vincent", "V. Valls"] __license__ = "MIT" __date__ = "08/08/2017" @@ -30,6 +32,7 @@ __date__ = "08/08/2017" import logging import math +from typing import NamedTuple import numpy @@ -46,11 +49,11 @@ FLOAT32_SAFE_MAX = 1e37 # TODO double support -def checkAxisLimits(vmin, vmax, isLog: bool=False, name: str=""): +def checkAxisLimits(vmin: float, vmax: float, isLog: bool = False, name: str = ""): """Makes sure axis range is not empty and within supported range. - :param float vmin: Min axis value - :param float vmax: Max axis value + :param vmin: Min axis value + :param vmax: Max axis value :return: (min, max) making sure min < max :rtype: 2-tuple of float """ @@ -59,11 +62,11 @@ def checkAxisLimits(vmin, vmax, isLog: bool=False, name: str=""): vmin = numpy.clip(vmin, min_, FLOAT32_SAFE_MAX) if vmax < vmin: - _logger.debug('%s axis: max < min, inverting limits.', name) + _logger.debug("%s axis: max < min, inverting limits.", name) vmin, vmax = vmax, vmin elif vmax == vmin: - _logger.debug('%s axis: max == min, expanding limits.', name) - if vmin == 0.: + _logger.debug("%s axis: max == min, expanding limits.", name) + if vmin == 0.0: vmin, vmax = -0.1, 0.1 elif vmin < 0: vmax *= 0.9 @@ -75,26 +78,27 @@ def checkAxisLimits(vmin, vmax, isLog: bool=False, name: str=""): return vmin, vmax -def scale1DRange(min_, max_, center, scale, isLog): +def scale1DRange( + min_: float, max_: float, center: float, scale: float, isLog: bool +) -> tuple[float, float]: """Scale a 1D range given a scale factor and an center point. Keeps the values in a smaller range than float32. - :param float min_: The current min value of the range. - :param float max_: The current max value of the range. - :param float center: The center of the zoom (i.e., invariant point). - :param float scale: The scale to use for zoom - :param bool isLog: Whether using log scale or not. - :return: The zoomed range. - :rtype: tuple of 2 floats: (min, max) + :param min_: The current min value of the range. + :param max_: The current max value of the range. + :param center: The center of the zoom (i.e., invariant point). + :param scale: The scale to use for zoom + :param isLog: Whether using log scale or not. + :return: The zoomed range (min, max) """ if isLog: # Min and center can be < 0 when # autoscale is off and switch to log scale # max_ < 0 should not happen - min_ = numpy.log10(min_) if min_ > 0. else FLOAT32_MINPOS - center = numpy.log10(center) if center > 0. else FLOAT32_MINPOS - max_ = numpy.log10(max_) if max_ > 0. else FLOAT32_MINPOS + min_ = numpy.log10(min_) if min_ > 0.0 else FLOAT32_MINPOS + center = numpy.log10(center) if center > 0.0 else FLOAT32_MINPOS + max_ = numpy.log10(max_) if max_ > 0.0 else FLOAT32_MINPOS if min_ == max_: return min_, max_ @@ -102,12 +106,12 @@ def scale1DRange(min_, max_, center, scale, isLog): offset = (center - min_) / (max_ - min_) range_ = (max_ - min_) / scale newMin = center - offset * range_ - newMax = center + (1. - offset) * range_ + newMax = center + (1.0 - offset) * range_ if isLog: # No overflow as exponent is log10 of a float32 - newMin = pow(10., newMin) - newMax = pow(10., newMax) + newMin = pow(10.0, newMin) + newMax = pow(10.0, newMax) newMin = numpy.clip(newMin, FLOAT32_MINPOS, FLOAT32_SAFE_MAX) newMax = numpy.clip(newMax, FLOAT32_MINPOS, FLOAT32_SAFE_MAX) else: @@ -116,16 +120,34 @@ def scale1DRange(min_, max_, center, scale, isLog): return newMin, newMax -def applyZoomToPlot(plot, scaleF, center=None): +class EnabledAxes(NamedTuple): + """Toggle zoom for each axis""" + + xaxis: bool = True + yaxis: bool = True + y2axis: bool = True + + def isDisabled(self) -> bool: + """True only if all axes are disabled""" + return not (self.xaxis or self.yaxis or self.y2axis) + + +def applyZoomToPlot( + plot, + scale: float, + center: tuple[float, float] = None, + enabled: EnabledAxes = EnabledAxes(), +): """Zoom in/out plot given a scale and a center point. :param plot: The plot on which to apply zoom. - :param float scaleF: Scale factor of zoom. + :param scale: Scale factor of zoom. :param center: (x, y) coords in pixel coordinates of the zoom center. - :type center: 2-tuple of float + :param enabled: Toggle zoom for each axis independently """ xMin, xMax = plot.getXAxis().getLimits() yMin, yMax = plot.getYAxis().getLimits() + y2Min, y2Max = plot.getYAxis(axis="right").getLimits() if center is None: left, top, width, height = plot.getPlotBoundsInPixels() @@ -136,18 +158,23 @@ def applyZoomToPlot(plot, scaleF, center=None): dataCenterPos = plot.pixelToData(cx, cy) assert dataCenterPos is not None - xMin, xMax = scale1DRange(xMin, xMax, dataCenterPos[0], scaleF, - plot.getXAxis()._isLogarithmic()) + if enabled.xaxis: + xMin, xMax = scale1DRange( + xMin, xMax, dataCenterPos[0], scale, plot.getXAxis()._isLogarithmic() + ) - yMin, yMax = scale1DRange(yMin, yMax, dataCenterPos[1], scaleF, - plot.getYAxis()._isLogarithmic()) + if enabled.yaxis: + yMin, yMax = scale1DRange( + yMin, yMax, dataCenterPos[1], scale, plot.getYAxis()._isLogarithmic() + ) - dataPos = plot.pixelToData(cx, cy, axis="right") - assert dataPos is not None - y2Center = dataPos[1] - y2Min, y2Max = plot.getYAxis(axis="right").getLimits() - y2Min, y2Max = scale1DRange(y2Min, y2Max, y2Center, scaleF, - plot.getYAxis()._isLogarithmic()) + if enabled.y2axis: + dataPos = plot.pixelToData(cx, cy, axis="right") + assert dataPos is not None + y2Center = dataPos[1] + y2Min, y2Max = scale1DRange( + y2Min, y2Max, y2Center, scale, plot.getYAxis()._isLogarithmic() + ) plot.setLimits(xMin, xMax, yMin, yMax, y2Min, y2Max) @@ -166,15 +193,15 @@ def applyPan(min_, max_, panFactor, isLog10): :return: New min and max value with pan applied. :rtype: 2-tuple of float. """ - if isLog10 and min_ > 0.: + if isLog10 and min_ > 0.0: # Negative range and log scale can happen with matplotlib logMin, logMax = math.log10(min_), math.log10(max_) logOffset = panFactor * (logMax - logMin) - newMin = pow(10., logMin + logOffset) - newMax = pow(10., logMax + logOffset) + newMin = pow(10.0, logMin + logOffset) + newMax = pow(10.0, logMax + logOffset) # Takes care of out-of-range values - if newMin > 0. and newMax < float('inf'): + if newMin > 0.0 and newMax < float("inf"): min_, max_ = newMin, newMax else: @@ -182,13 +209,14 @@ def applyPan(min_, max_, panFactor, isLog10): newMin, newMax = min_ + offset, max_ + offset # Takes care of out-of-range values - if newMin > - float('inf') and newMax < float('inf'): + if newMin > -float("inf") and newMax < float("inf"): min_, max_ = newMin, newMax return min_, max_ class _Unset(object): """To be able to have distinction between None and unset""" + pass @@ -203,10 +231,17 @@ class ViewConstraints(object): self._minRange = [None, None] self._maxRange = [None, None] - def update(self, xMin=_Unset, xMax=_Unset, - yMin=_Unset, yMax=_Unset, - minXRange=_Unset, maxXRange=_Unset, - minYRange=_Unset, maxYRange=_Unset): + def update( + self, + xMin=_Unset, + xMax=_Unset, + yMin=_Unset, + yMax=_Unset, + minXRange=_Unset, + maxXRange=_Unset, + minYRange=_Unset, + maxYRange=_Unset, + ): """ Update the constraints managed by the object @@ -238,7 +273,6 @@ class ViewConstraints(object): maxPos = [xMax, yMax] for axis in range(2): - value = minPos[axis] if value is not _Unset and value != self._min[axis]: self._min[axis] = value @@ -262,7 +296,11 @@ class ViewConstraints(object): # Sanity checks for axis in range(2): - if self._maxRange[axis] is not None and self._min[axis] is not None and self._max[axis] is not None: + if ( + self._maxRange[axis] is not None + and self._min[axis] is not None + and self._max[axis] is not None + ): # max range cannot be larger than bounds diff = self._max[axis] - self._min[axis] self._maxRange[axis] = min(self._maxRange[axis], diff) @@ -298,8 +336,12 @@ class ViewConstraints(object): viewRange[axis][1] += delta * 0.5 # clamp min and max positions - outMin = self._min[axis] is not None and viewRange[axis][0] < self._min[axis] - outMax = self._max[axis] is not None and viewRange[axis][1] > self._max[axis] + outMin = ( + self._min[axis] is not None and viewRange[axis][0] < self._min[axis] + ) + outMax = ( + self._max[axis] is not None and viewRange[axis][1] > self._max[axis] + ) if outMin and outMax: if allow_scaling: |