Repository: silx-kit/silx
Issue: [Plot] Right axis range not updated.

The behaviour of the right Y axis (and all plots associated with it) is wrong.

``` python
import numpy as np
from silx.gui import qt

from silx.gui.plot import PlotWindow

app = qt.QApplication([])

plotWin = PlotWindow()

plotWin.addCurve(np.arange(100), np.arange(100), legend='p1', yaxis='left')
plotWin.addCurve(2*np.arange(100), np.arange(100), legend='p2', yaxis='right')

plotWin.setKeepDataAspectRatio(True)
plotWin.show()
app.exec_()
```


Changed files:
silx/gui/plot/BackendMatplotlib.py, silx/gui/plot/test/testPlot.py, silx/gui/plot/_utils.py, silx/gui/plot/Plot.py, silx/gui/plot/BackendBase.py

diff --git a/silx/gui/plot/BackendBase.py b/silx/gui/plot/BackendBase.py
index 939355eec..445f3f850 100644
--- a/silx/gui/plot/BackendBase.py
+++ b/silx/gui/plot/BackendBase.py
@@ -473,7 +473,7 @@ class BackendBase(object):
         """
         raise NotImplementedError()
 
-    def pixelToData(self, x, y, axis):
+    def pixelToData(self, x, y, axis, check=True):
         """Convert a position in pixels in the widget to a position in
         the data space.
 
@@ -481,6 +481,8 @@ class BackendBase(object):
         :param float y: The Y coordinate in pixels.
         :param str axis: The Y axis to use for the conversion
                          ('left' or 'right').
+        :param bool check: True to check if the coordinates are in the
+                           plot area.
         :returns: The corresponding position in data space or
                   None if the pixel position is not in the plot area.
         :rtype: A tuple of 2 floats: (xData, yData) or None.
diff --git a/silx/gui/plot/BackendMatplotlib.py b/silx/gui/plot/BackendMatplotlib.py
index 45e34c55e..bdb4221eb 100644
--- a/silx/gui/plot/BackendMatplotlib.py
+++ b/silx/gui/plot/BackendMatplotlib.py
@@ -24,6 +24,8 @@
 # ###########################################################################*/
 """Matplotlib Plot backend."""
 
+from __future__ import division
+
 __authors__ = ["V.A. Sole", "T. Vincent"]
 __license__ = "MIT"
 __date__ = "15/09/2016"
@@ -206,19 +208,6 @@ class BackendMatplotlib(BackendBase.BackendBase):
                     axes.fill_between(x, 1.0e-8, y,
                                       facecolor=color, linewidth=0))
 
-        # This complies with _getDataLimits
-        artists[-1]._plot_info = {
-            'axes': yaxis,
-            # this is needed for scatter plots because I do not know
-            # how to recover the data yet, it can speed up limits too
-            'xmin': numpy.nanmin(x),
-            'xmax': numpy.nanmax(x),
-            'ymin': numpy.nanmin(y),
-            'ymax': numpy.nanmax(y),
-        }
-
-        artists[-1].axes = axes
-
         for artist in artists:
             artist.set_zorder(z)
 
@@ -239,8 +228,12 @@ class BackendMatplotlib(BackendBase.BackendBase):
         h, w = data.shape[0:2]
         xmin = origin[0]
         xmax = xmin + scale[0] * w
+        if scale[0] < 0.:
+            xmin, xmax = xmax, xmin
         ymin = origin[1]
         ymax = ymin + scale[1] * h
+        if scale[1] < 0.:
+            ymin, ymax = ymax, ymin
         extent = (xmin, xmax, ymax, ymin)
 
         picker = (selectable or draggable)
@@ -632,178 +625,52 @@ class BackendMatplotlib(BackendBase.BackendBase):
             yLimits = self.getGraphYLimits(axis='left')
             y2Limits = self.getGraphYLimits(axis='right')
 
-            xmin, xmax, ymin, ymax = self._getDataLimits('left')
-            if hasattr(self.ax2, "get_visible"):
-                if self.ax2.get_visible():
-                    xmin2, xmax2, ymin2, ymax2 = self._getDataLimits('right')
-                else:
-                    xmin2, xmax2, ymin2, ymax2 = None, None, None, None
+            # Get data range
+            ranges = self._plot.getDataRange()
+            xmin, xmax = (1., 100.) if ranges.x is None else ranges.x
+            ymin, ymax = (1., 100.) if ranges.y is None else ranges.y
+            if ranges.yright is None:
+                ymin2, ymax2 = None, None
             else:
-                xmin2, xmax2, ymin2, ymax2 = self._getDataLimits('right')
-
-            if (xmin2 is not None) and ((xmin2 != 0) or (xmax2 != 1)):
-                xmin = min(xmin, xmin2)
-                xmax = max(xmax, xmax2)
+                ymin2, ymax2 = ranges.yright
 
             # Add margins around data inside the plot area
-            newLimits = _utils.addMarginsToLimits(
+            newLimits = list(_utils.addMarginsToLimits(
                 dataMargins,
                 self.ax.get_xscale() == 'log',
                 self.ax.get_yscale() == 'log',
-                xmin, xmax, ymin, ymax, ymin2, ymax2)
+                xmin, xmax, ymin, ymax, ymin2, ymax2))
+
+            if self.isKeepDataAspectRatio():
+                # Compute bbox wth figure aspect ratio
+                figW, figH = self.fig.get_size_inches()
+                figureRatio = figH / figW
+
+                dataRatio = (ymax - ymin) / (xmax - xmin)
+                if dataRatio < figureRatio:
+                    # Increase y range
+                    ycenter = 0.5 * (newLimits[3] - newLimits[2])
+                    yrange = (xmax - xmin) * figureRatio
+                    newLimits[2] = ycenter - 0.5 * yrange
+                    newLimits[3] = ycenter + 0.5 * yrange
+
+                elif dataRatio > figureRatio:
+                    # Increase x range
+                    xcenter = 0.5 * (newLimits[1] - newLimits[0])
+                    xrange_ = (ymax - ymin) / figureRatio
+                    newLimits[0] = xcenter - 0.5 * xrange_
+                    newLimits[1] = xcenter + 0.5 * xrange_
 
             self.setLimits(*newLimits)
 
             if not xAuto and yAuto:
                 self.setGraphXLimits(*xLimits)
             elif xAuto and not yAuto:
-                self.setGraphYLimits(y2Limits[0], y2Limits[1], axis='right')
-                self.setGraphYLimits(yLimits[0], yLimits[1], axis='left')
-
-    def _getDataLimits(self, axesLabel='left'):
-        """Returns the bounds of the data.
-
-        :param str axesLabel: The Y axis to consider in 'left', 'right'
-        :return: The data bounds
-        :rtype: 4-tuple of float (xmin, xmax, ymin, ymax)
-        """
-        if axesLabel == 'right':
-            axes = self.ax2
-        else:
-            axes = self.ax
-        _logger.debug("CALCULATING limits %s", axes.get_label())
-        xmin = None
-
-        for line2d in axes.lines:
-            label = line2d.get_label()
-            if label.startswith("__MARKER__"):
-                # it is a marker
-                continue
-            lineXMin = None
-            if hasattr(line2d, "_plot_info"):
-                if line2d._plot_info["axes"] != axesLabel:
-                    continue
-                if "xmin" in line2d._plot_info:
-                    lineXMin = line2d._plot_info["xmin"]
-                    lineXMax = line2d._plot_info["xmax"]
-                    lineYMin = line2d._plot_info["ymin"]
-                    lineYMax = line2d._plot_info["ymax"]
-            if lineXMin is None:
-                x = line2d.get_xdata()
-                y = line2d.get_ydata()
-                if not len(x) or not len(y):
-                    continue
-                lineXMin = numpy.nanmin(x)
-                lineXMax = numpy.nanmax(x)
-                lineYMin = numpy.nanmin(y)
-                lineYMax = numpy.nanmax(y)
-            if xmin is None:
-                xmin = lineXMin
-                xmax = lineXMax
-                ymin = lineYMin
-                ymax = lineYMax
-                continue
-            xmin = min(xmin, lineXMin)
-            xmax = max(xmax, lineXMax)
-            ymin = min(ymin, lineYMin)
-            ymax = max(ymax, lineYMax)
-
-        for line2d in axes.collections:
-            label = line2d.get_label()
-            if label.startswith("__MARKER__"):
-                # it is a marker
-                continue
-            lineXMin = None
-            if hasattr(line2d, "_plot_info"):
-                if line2d._plot_info["axes"] != axesLabel:
-                    continue
-                if "xmin" in line2d._plot_info:
-                    lineXMin = line2d._plot_info["xmin"]
-                    lineXMax = line2d._plot_info["xmax"]
-                    lineYMin = line2d._plot_info["ymin"]
-                    lineYMax = line2d._plot_info["ymax"]
-            if lineXMin is None:
-                _logger.debug("CANNOT CALCULATE LIMITS")
-                continue
-            if xmin is None:
-                xmin = lineXMin
-                xmax = lineXMax
-                ymin = lineYMin
-                ymax = lineYMax
-                continue
-            xmin = min(xmin, lineXMin)
-            xmax = max(xmax, lineXMax)
-            ymin = min(ymin, lineYMin)
-            ymax = max(ymax, lineYMax)
-
-        for artist in axes.images:
-            x0, x1, y0, y1 = artist.get_extent()
-            if (xmin is None):
-                xmin = x0
-                xmax = x1
-                ymin = min(y0, y1)
-                ymax = max(y0, y1)
-            xmin = min(xmin, x0)
-            xmax = max(xmax, x1)
-            ymin = min(ymin, y0)
-            ymax = max(ymax, y1)
-
-        for artist in axes.artists:
-            label = artist.get_label()
-            if label.startswith("__IMAGE__"):
-                if hasattr(artist, 'get_image_extent'):
-                    x0, x1, y0, y1 = artist.get_image_extent()
-                else:
-                    x0, x1, y0, y1 = artist.get_extent()
-                if (xmin is None):
-                    xmin = x0
-                    xmax = x1
-                    ymin = min(y0, y1)
-                    ymax = max(y0, y1)
-                ymin = min(ymin, y0, y1)
-                ymax = max(ymax, y1, y0)
-                xmin = min(xmin, x0)
-                xmax = max(xmax, x1)
-
-        if xmin is None:
-            _logger.debug('Did not found any limits, set to default')
-            xmin = 1
-            xmax = 100
-            ymin = 1
-            ymax = 100
-            if axesLabel == 'right':
-                _logger.debug('Returning None')
-                return None, None, None, None
-
-        xSize = float(xmax - xmin)
-        ySize = float(ymax - ymin)
-        A = self.ax.get_aspect()
-        if A != 'auto':
-            figW, figH = self.fig.get_size_inches()
-            figAspect = figH / figW
-
-            dataRatio = (ySize / xSize) * A
-
-            y_expander = dataRatio - figAspect
-            # If y_expander > 0, the dy/dx viewLim ratio needs to increase
-            if abs(y_expander) < 0.005:
-                # good enough
-                pass
-            else:
-                # this works for any data ratio
-                if y_expander < 0:
-                    deltaY = xSize * (figAspect / A) - ySize
-                    yc = 0.5 * (ymin + ymax)
-                    ymin = yc - (ySize + deltaY) * 0.5
-                    ymax = yc + (ySize + deltaY) * 0.5
-                else:
-                    deltaX = ySize * (A / figAspect) - xSize
-                    xc = 0.5 * (xmin + xmax)
-                    xmin = xc - (xSize + deltaX) * 0.5
-                    xmax = xc + (xSize + deltaX) * 0.5
-        _logger.debug(
-            "CALCULATED LIMITS = %f %f %f %f", xmin, xmax, ymin, ymax)
-        return xmin, xmax, ymin, ymax
+                if y2Limits is not None:
+                    self.setGraphYLimits(
+                        y2Limits[0], y2Limits[1], axis='right')
+                if yLimits is not None:
+                    self.setGraphYLimits(yLimits[0], yLimits[1], axis='left')
 
     def setLimits(self, xmin, xmax, ymin, ymax, y2min=None, y2max=None):
         # Let matplotlib taking care of keep aspect ratio if any
@@ -812,9 +679,9 @@ class BackendMatplotlib(BackendBase.BackendBase):
 
         if y2min is not None and y2max is not None:
             if not self.isYAxisInverted():
-                self.ax.set_ylim(min(y2min, y2max), max(y2min, y2max))
+                self.ax2.set_ylim(min(y2min, y2max), max(y2min, y2max))
             else:
-                self.ax.set_ylim(max(y2min, y2max), min(y2min, y2max))
+                self.ax2.set_ylim(max(y2min, y2max), min(y2min, y2max))
 
         if not self.isYAxisInverted():
             self.ax.set_ylim(min(ymin, ymax), max(ymin, ymax))
@@ -888,6 +755,7 @@ class BackendMatplotlib(BackendBase.BackendBase):
 
     def setKeepDataAspectRatio(self, flag):
         self.ax.set_aspect(1.0 if flag else 'auto')
+        self.ax2.set_aspect(1.0 if flag else 'auto')
 
     def setGraphGrid(self, which):
         self.ax.grid(False, which='both')  # Disable all grid first
@@ -970,23 +838,24 @@ class BackendMatplotlib(BackendBase.BackendBase):
     # Data <-> Pixel coordinates conversion
 
     def dataToPixel(self, x, y, axis):
-        ax = self.ax2 if "axis" == "right" else self.ax
+        ax = self.ax2 if axis == "right" else self.ax
 
         pixels = ax.transData.transform_point((x, y))
         xPixel, yPixel = pixels.T
         return xPixel, yPixel
 
     def pixelToData(self, x, y, axis, check):
-        ax = self.ax2 if "axis" == "right" else self.ax
+        ax = self.ax2 if axis == "right" else self.ax
 
         inv = ax.transData.inverted()
         x, y = inv.transform_point((x, y))
 
-        xmin, xmax = self.getGraphXLimits()
-        ymin, ymax = self.getGraphYLimits(axis=axis)
+        if check:
+            xmin, xmax = self.getGraphXLimits()
+            ymin, ymax = self.getGraphYLimits(axis=axis)
 
-        if check and (x > xmax or x < xmin or y > ymax or y < ymin):
-            return None  # (x, y) is out of plot area
+            if x > xmax or x < xmin or y > ymax or y < ymin:
+                return None  # (x, y) is out of plot area
 
         return x, y
 
diff --git a/silx/gui/plot/Plot.py b/silx/gui/plot/Plot.py
index 42a974f50..72e65a5a1 100644
--- a/silx/gui/plot/Plot.py
+++ b/silx/gui/plot/Plot.py
@@ -321,9 +321,16 @@ class Plot(object):
         self._yAutoScale = True
         self._grid = None
 
+        # Store default labels provided to setGraph[X|Y]Label
+        self._defaultLabels = {'x': '', 'y': '', 'yright': ''}
+        # Store currently displayed labels
+        # Current label can differ from input one with active curve handling
+        self._currentLabels = {'x': '', 'y': '', 'yright': ''}
+
         self.setGraphTitle()
         self.setGraphXLabel()
         self.setGraphYLabel()
+        self.setGraphYLabel('', axis='right')
 
         self.setDefaultColormap()  # Init default colormap
 
@@ -392,24 +399,37 @@ class Plot(object):
         for curve, info in self._curves.items():
             # using numpy's separate min and max is faster than
             # a pure python minmax.
-            xMin = numpy.nanmin([xMin, info['xmin']])
-            xMax = numpy.nanmax([xMax, info['xmax']])
+            if info['xmin'] is not None:
+                xMin = numpy.nanmin([xMin, info['xmin']])
+            if info['xmax'] is not None:
+                xMax = numpy.nanmax([xMax, info['xmax']])
+
             if info['params']['yaxis'] == 'left':
-                yMinLeft = numpy.nanmin([yMinLeft, info['ymin']])
-                yMaxLeft = numpy.nanmax([yMaxLeft, info['ymax']])
+                if info['ymin'] is not None:
+                    yMinLeft = numpy.nanmin([yMinLeft, info['ymin']])
+                if info['ymax'] is not None:
+                    yMaxLeft = numpy.nanmax([yMaxLeft, info['ymax']])
             else:
-                yMinRight = numpy.nanmin([yMinRight, info['ymin']])
-                yMaxRight = numpy.nanmax([yMaxRight, info['ymax']])
-
-        for image, info in self._images.items():
-            height, width = info['data'].shape
-            params = info['params']
-            origin = params['origin']
-            scale = params['scale']
-            xMin = numpy.nanmin([xMin, origin[0]])
-            xMax = numpy.nanmax([xMax, origin[0] + width * scale[0]])
-            yMinLeft = numpy.nanmin([yMinLeft, origin[1]])
-            yMaxLeft = numpy.nanmax([yMaxLeft, origin[1] + height * scale[1]])
+                if info['ymin'] is not None:
+                    yMinRight = numpy.nanmin([yMinRight, info['ymin']])
+                if info['ymax'] is not None:
+                    yMaxRight = numpy.nanmax([yMaxRight, info['ymax']])
+
+        if not self.isXAxisLogarithmic() and not self.isYAxisLogarithmic():
+            for image, info in self._images.items():
+                if info['data'] is not None:
+                    height, width = info['data'].shape[:2]
+                    params = info['params']
+                    origin = params['origin']
+                    scale = params['scale']
+                    # Taking care of scale might be < 0
+                    xCoords = [origin[0], origin[0] + width * scale[0]]
+                    xMin = numpy.nanmin([xMin] + xCoords)
+                    xMax = numpy.nanmax([xMax] + xCoords)
+                    # Taking care of scale might be < 0
+                    yCoords = [origin[1], origin[1] + height * scale[1]]
+                    yMinLeft = numpy.nanmin([yMinLeft] + yCoords)
+                    yMaxLeft = numpy.nanmax([yMaxLeft] + yCoords)
 
         lGetRange = (lambda x, y:
                      None if numpy.isnan(x) and numpy.isnan(y) else (x, y))
@@ -633,14 +653,17 @@ class Plot(object):
                                             selectable=params['selectable'],
                                             fill=params['fill'])
             self._setDirtyPlot()
-        else:
-            handle = None  # The curve has no points or is hidden
 
-        # caching the min and max values for the getDataRange method.
-        xMin = numpy.nanmin(x)
-        xMax = numpy.nanmax(x)
-        yMin = numpy.nanmin(y)
-        yMax = numpy.nanmax(y)
+            # caching the min and max values for the getDataRange method.
+            xMin = numpy.nanmin(xFiltered)
+            xMax = numpy.nanmax(xFiltered)
+            yMin = numpy.nanmin(yFiltered)
+            yMax = numpy.nanmax(yFiltered)
+
+        else:
+            # The curve has no points or is hidden
+            handle = None
+            xMin, xMax, yMin, yMax = None, None, None, None
 
         self._curves[legend] = {
             'handle': handle, 'x': x, 'y': y, 'params': params,
@@ -1500,8 +1523,9 @@ class Plot(object):
         if not self.isActiveCurveHandling():
             return
 
-        xLabel = self._xLabel
-        yLabel = self._yLabel
+        xLabel = self._defaultLabels['x']
+        yLabel = self._defaultLabels['y']
+        yRightLabel = self._defaultLabels['yright']
 
         oldActiveCurveLegend = self.getActiveCurve(just_legend=True)
         if oldActiveCurveLegend:  # Reset previous active curve
@@ -1529,14 +1553,19 @@ class Plot(object):
                 if curveParams['xlabel'] is not None:
                     xLabel = curveParams['xlabel']
                 if curveParams['ylabel'] is not None:
-                    yLabel = curveParams['ylabel']
-                # TODO y2 axis case
+                    if curveParams['yaxis'] == 'left':
+                        yLabel = curveParams['ylabel']
+                    else:
+                        yRightLabel = curveParams['ylabel']
 
         # Store current labels and update plot
-        self._currentXLabel = xLabel
+        self._currentLabels['x'] = xLabel
+        self._currentLabels['y'] = yLabel
+        self._currentLabels['yright'] = yRightLabel
+
         self._backend.setGraphXLabel(xLabel)
-        self._currentYLabel = yLabel
-        self._backend.setGraphYLabel(yLabel, axis='left')  # TODO y2 axis
+        self._backend.setGraphYLabel(yLabel, axis='left')
+        self._backend.setGraphYLabel(yRightLabel, axis='right')
 
         self._setDirtyPlot()
 
@@ -1588,8 +1617,8 @@ class Plot(object):
         if replot is not None:
             _logger.warning('setActiveImage deprecated replot parameter')
 
-        xLabel = self._xLabel
-        yLabel = self._yLabel
+        xLabel = self._defaultLabels['x']
+        yLabel = self._defaultLabels['y']
 
         oldActiveImageLegend = self.getActiveImage(just_legend=True)
 
@@ -1612,9 +1641,10 @@ class Plot(object):
                     yLabel = imageParams['ylabel']
 
         # Store current labels and update plot
-        self._currentXLabel = xLabel
+        self._currentLabels['x'] = xLabel
+        self._currentLabels['y'] = yLabel
+
         self._backend.setGraphXLabel(xLabel)
-        self._currentYLabel = yLabel
         self._backend.setGraphYLabel(yLabel, axis='left')
 
         if oldActiveImageLegend is not None or self._activeImage is not None:
@@ -1847,7 +1877,7 @@ class Plot(object):
 
     def getGraphXLabel(self):
         """Return the current X axis label as a str."""
-        return self._currentXLabel
+        return self._defaultLabels['x']
 
     def setGraphXLabel(self, label="X"):
         """Set the plot X axis label.
@@ -1857,28 +1887,39 @@ class Plot(object):
 
         :param str label: The X axis label (default: 'X')
         """
-        self._xLabel = label
-        # Current label can differ from input one with active curve handling
-        self._currentXLabel = label
+        self._defaultLabels['x'] = label
+        self._currentLabels['x'] = label
         self._backend.setGraphXLabel(label)
         self._setDirtyPlot()
 
-    def getGraphYLabel(self):
-        """Return the current Y axis label as a str."""
-        return self._currentYLabel
+    def getGraphYLabel(self, axis='left'):
+        """Return the current Y axis label as a str.
 
-    def setGraphYLabel(self, label="Y"):
+        :param str axis: The Y axis for which to get the label (left or right)
+        """
+        assert axis in ('left', 'right')
+
+        return self._currentLabels['y' if axis == 'left' else 'yright']
+
+    def setGraphYLabel(self, label="Y", axis='left'):
         """Set the plot Y axis label.
 
         The provided label can be temporarily replaced by the Y label of the
         active curve if any.
 
         :param str label: The Y axis label (default: 'Y')
+        :param str axis: The Y axis for which to set the label (left or right)
         """
-        self._yLabel = label
-        # Current label can differ from input one with active curve handling
-        self._currentYLabel = label
-        self._backend.setGraphYLabel(label, axis='left')
+        assert axis in ('left', 'right')
+
+        if axis == 'left':
+            self._defaultLabels['y'] = label
+            self._currentLabels['y'] = label
+        else:
+            self._defaultLabels['yright'] = label
+            self._currentLabels['yright'] = label
+
+        self._backend.setGraphYLabel(label, axis=axis)
         self._setDirtyPlot()
 
     # Axes
@@ -1937,6 +1978,7 @@ class Plot(object):
                 self._backend.setXAxisLogarithmic(self._logX)
                 self._update()
 
+        self._invalidateDataRange()
         self._setDirtyPlot()
         self.resetZoom()
         self.notify('setXAxisLogarithmic', state=self._logX)
@@ -1980,6 +2022,7 @@ class Plot(object):
                 self._backend.setYAxisLogarithmic(self._logY)
                 self._update()
 
+        self._invalidateDataRange()
         self._setDirtyPlot()
         self.resetZoom()
         self.notify('setYAxisLogarithmic', state=self._logY)
diff --git a/silx/gui/plot/_utils.py b/silx/gui/plot/_utils.py
index 843188c8f..308862e51 100644
--- a/silx/gui/plot/_utils.py
+++ b/silx/gui/plot/_utils.py
@@ -147,7 +147,7 @@ def applyZoomToPlot(plot, scaleF, center=None):
 
     :param plot: The plot on which to apply zoom.
     :param float scaleF: Scale factor of zoom.
-    :param center: (x, y) coords in data coordinates of the zoom center.
+    :param center: (x, y) coords in pixel coordinates of the zoom center.
     :type center: 2-tuple of float
     """
     xMin, xMax = plot.getGraphXLimits()
diff --git a/silx/gui/plot/test/testPlot.py b/silx/gui/plot/test/testPlot.py
index b8b2f3dd7..1e9a36931 100644
--- a/silx/gui/plot/test/testPlot.py
+++ b/silx/gui/plot/test/testPlot.py
@@ -30,6 +30,8 @@ __date__ = "02/03/2016"
 
 
 import unittest
+from functools import reduce
+from silx.testutils import ParametricTestCase
 
 import numpy
 
@@ -63,64 +65,176 @@ class TestPlot(unittest.TestCase):
             numpy.array((1., 10.)), numpy.array((10., 10.)), shape="rectangle")
         plot.addXMarker(10.)
 
+
+class TestPlotRanges(ParametricTestCase):
+    """Basic tests of Plot data ranges without backend"""
+
+    _getValidValues = {True: lambda ar: ar > 0,
+                       False: lambda ar: numpy.ones(shape=ar.shape,
+                                                    dtype=bool)}
+
+    @staticmethod
+    def _getRanges(arrays, are_logs):
+        gen = (TestPlotRanges._getValidValues[is_log](ar)
+               for (ar, is_log) in zip(arrays, are_logs))
+        indices = numpy.where(reduce(numpy.logical_and, gen))[0]
+        if len(indices) > 0:
+            ranges = [(ar[indices[0]], ar[indices[-1]]) for ar in arrays]
+        else:
+            ranges = [None] * len(arrays)
+
+        return ranges
+
+    @staticmethod
+    def _getRangesMinmax(ranges):
+        # TODO : error if None in ranges.
+        rangeMin = numpy.min([rng[0] for rng in ranges])
+        rangeMax = numpy.max([rng[1] for rng in ranges])
+        return rangeMin, rangeMax
+
     def testDataRangeNoPlot(self):
         """empty plot data range"""
 
         plot = Plot(backend='none')
-        dataRange = plot.getDataRange()
-        self.assertIsNone(dataRange.x)
-        self.assertIsNone(dataRange.y)
-        self.assertIsNone(dataRange.yright)
+
+        for logX, logY in ((False, False),
+                           (True, False),
+                           (True, True),
+                           (False, True),
+                           (False, False)):
+            with self.subTest(logX=logX, logY=logY):
+                plot.setXAxisLogarithmic(logX)
+                plot.setYAxisLogarithmic(logY)
+                dataRange = plot.getDataRange()
+                self.assertIsNone(dataRange.x)
+                self.assertIsNone(dataRange.y)
+                self.assertIsNone(dataRange.yright)
 
     def testDataRangeLeft(self):
         """left axis range"""
 
         plot = Plot(backend='none')
-        plot.addCurve(x=numpy.arange(10) - 5., y=numpy.arange(10) - 7.,
-                      legend='plot_0', yaxis='left')
 
-        dataRange = plot.getDataRange()
-        self.assertEqual(dataRange.x, (-5., 4.))
-        self.assertEqual(dataRange.y, (-7., 2.))
-        self.assertIsNone(dataRange.yright)
+        xData = numpy.arange(10) - 4.9  # range : -4.9 , 4.1
+        yData = numpy.arange(10) - 6.9  # range : -6.9 , 2.1
+
+        plot.addCurve(x=xData,
+                      y=yData,
+                      legend='plot_0',
+                      yaxis='left')
+
+        for logX, logY in ((False, False),
+                           (True, False),
+                           (True, True),
+                           (False, True),
+                           (False, False)):
+            with self.subTest(logX=logX, logY=logY):
+                plot.setXAxisLogarithmic(logX)
+                plot.setYAxisLogarithmic(logY)
+                dataRange = plot.getDataRange()
+                xRange, yRange = self._getRanges([xData, yData],
+                                                 [logX, logY])
+                self.assertSequenceEqual(dataRange.x, xRange)
+                self.assertSequenceEqual(dataRange.y, yRange)
+                self.assertIsNone(dataRange.yright)
 
     def testDataRangeRight(self):
         """right axis range"""
 
         plot = Plot(backend='none')
-        plot.addCurve(x=numpy.arange(10) - 5., y=numpy.arange(10) - 7.,
-                      legend='plot_0', yaxis='right')
-
-        dataRange = plot.getDataRange()
-        self.assertEqual(dataRange.x, (-5., 4.))
-        self.assertIsNone(dataRange.y)
-        self.assertEqual(dataRange.yright, (-7., 2.))
+        xData = numpy.arange(10) - 4.9  # range : -4.9 , 4.1
+        yData = numpy.arange(10) - 6.9  # range : -6.9 , 2.1
+        plot.addCurve(x=xData,
+                      y=yData,
+                      legend='plot_0',
+                      yaxis='right')
+
+        for logX, logY in ((False, False),
+                           (True, False),
+                           (True, True),
+                           (False, True),
+                           (False, False)):
+            with self.subTest(logX=logX, logY=logY):
+                plot.setXAxisLogarithmic(logX)
+                plot.setYAxisLogarithmic(logY)
+                dataRange = plot.getDataRange()
+                xRange, yRange = self._getRanges([xData, yData],
+                                                 [logX, logY])
+                self.assertSequenceEqual(dataRange.x, xRange)
+                self.assertIsNone(dataRange.y)
+                self.assertSequenceEqual(dataRange.yright, yRange)
 
     def testDataRangeImage(self):
         """image data range"""
 
-        plot = Plot(backend='none')
-        plot.addImage(numpy.arange(100.).reshape(20, 5),
-                      origin=(-10, 25), scale=(3., 8.))
+        origin = (-10, 25)
+        scale = (3., 8.)
+        image = numpy.arange(100.).reshape(20, 5)
 
-        dataRange = plot.getDataRange()
-        self.assertEqual(dataRange.x, (-10., 5.))
-        self.assertEqual(dataRange.y, (25., 185.))
-        self.assertIsNone(dataRange.yright)
+        plot = Plot(backend='none')
+        plot.addImage(image,
+                      origin=origin, scale=scale)
+
+        xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
+        yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
+
+        ranges = {(False, False): (xRange, yRange),
+                  (True, False): (None, None),
+                  (True, True): (None, None),
+                  (False, True): (None, None)}
+
+        for logX, logY in ((False, False),
+                           (True, False),
+                           (True, True),
+                           (False, True),
+                           (False, False)):
+            with self.subTest(logX=logX, logY=logY):
+                plot.setXAxisLogarithmic(logX)
+                plot.setYAxisLogarithmic(logY)
+                dataRange = plot.getDataRange()
+                xRange, yRange = ranges[logX, logY]
+                self.assertTrue(numpy.array_equal(dataRange.x, xRange),
+                                msg='{0} != {1}'.format(dataRange.x, xRange))
+                self.assertTrue(numpy.array_equal(dataRange.y, yRange),
+                                msg='{0} != {1}'.format(dataRange.y, yRange))
+                self.assertIsNone(dataRange.yright)
 
     def testDataRangeLeftRight(self):
         """right+left axis range"""
 
         plot = Plot(backend='none')
-        plot.addCurve(x=numpy.arange(10) - 1., y=numpy.arange(10) - 2.,
-                      legend='plot_left', yaxis='left')
-        plot.addCurve(x=numpy.arange(10) - 5., y=numpy.arange(10) - 7.,
-                      legend='plot_right', yaxis='right')
 
-        dataRange = plot.getDataRange()
-        self.assertEqual(dataRange.x, (-5., 8.))
-        self.assertEqual(dataRange.y, (-2, 7.))
-        self.assertEqual(dataRange.yright, (-7., 2.))
+        xData_l = numpy.arange(10) - 0.9  # range : -0.9 , 8.1
+        yData_l = numpy.arange(10) - 1.9  # range : -1.9 , 7.1
+        plot.addCurve(x=xData_l,
+                      y=yData_l,
+                      legend='plot_l',
+                      yaxis='left')
+
+        xData_r = numpy.arange(10) - 4.9  # range : -4.9 , 4.1
+        yData_r = numpy.arange(10) - 6.9  # range : -6.9 , 2.1
+        plot.addCurve(x=xData_r,
+                      y=yData_r,
+                      legend='plot_r',
+                      yaxis='right')
+
+        for logX, logY in ((False, False),
+                           (True, False),
+                           (True, True),
+                           (False, True),
+                           (False, False)):
+            with self.subTest(logX=logX, logY=logY):
+                plot.setXAxisLogarithmic(logX)
+                plot.setYAxisLogarithmic(logY)
+                dataRange = plot.getDataRange()
+                xRangeL, yRangeL = self._getRanges([xData_l, yData_l],
+                                                   [logX, logY])
+                xRangeR, yRangeR = self._getRanges([xData_r, yData_r],
+                                                   [logX, logY])
+                xRangeLR = self._getRangesMinmax([xRangeL, xRangeR])
+                self.assertSequenceEqual(dataRange.x, xRangeLR)
+                self.assertSequenceEqual(dataRange.y, yRangeL)
+                self.assertSequenceEqual(dataRange.yright, yRangeR)
 
     def testDataRangeCurveImage(self):
         """right+left+image axis range"""
@@ -130,23 +244,135 @@ class TestPlot(unittest.TestCase):
         # plot_left sets y min
         # plot_right sets x max (and yright)
         plot = Plot(backend='none')
-        plot.addImage(numpy.arange(100.).reshape(20, 5),
-                      origin=(-10, 5), scale=(3., 8.), legend='image')
-        plot.addCurve(x=numpy.arange(10) - 1., y=numpy.arange(10) - 2.,
-                      legend='plot_left', yaxis='left')
-        plot.addCurve(x=numpy.arange(10) + 5., y=numpy.arange(10) - 1.,
-                      legend='plot_right', yaxis='right')
 
-        dataRange = plot.getDataRange()
-        self.assertEqual(dataRange.x, (-10., 14.))
-        self.assertEqual(dataRange.y, (-2, 165.))
-        self.assertEqual(dataRange.yright, (-1., 8.))
+        origin = (-10, 5)
+        scale = (3., 8.)
+        image = numpy.arange(100.).reshape(20, 5)
+
+        plot.addImage(image,
+                      origin=origin, scale=scale, legend='image')
+
+        xData_l = numpy.arange(10) - 0.9  # range : -0.9 , 8.1
+        yData_l = numpy.arange(10) - 1.9  # range : -1.9 , 7.1
+        plot.addCurve(x=xData_l,
+                      y=yData_l,
+                      legend='plot_l',
+                      yaxis='left')
+
+        xData_r = numpy.arange(10) + 4.1  # range : 4.1 , 13.1
+        yData_r = numpy.arange(10) - 0.9  # range : -0.9 , 8.1
+        plot.addCurve(x=xData_r,
+                      y=yData_r,
+                      legend='plot_r',
+                      yaxis='right')
+
+        imgXRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
+        imgYRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
+
+        for logX, logY in ((False, False),
+                           (True, False),
+                           (True, True),
+                           (False, True),
+                           (False, False)):
+            with self.subTest(logX=logX, logY=logY):
+                plot.setXAxisLogarithmic(logX)
+                plot.setYAxisLogarithmic(logY)
+                dataRange = plot.getDataRange()
+                xRangeL, yRangeL = self._getRanges([xData_l, yData_l],
+                                                   [logX, logY])
+                xRangeR, yRangeR = self._getRanges([xData_r, yData_r],
+                                                   [logX, logY])
+                if logX or logY:
+                    xRangeLR = self._getRangesMinmax([xRangeL, xRangeR])
+                else:
+                    xRangeLR = self._getRangesMinmax([xRangeL,
+                                                      xRangeR,
+                                                      imgXRange])
+                    yRangeL = self._getRangesMinmax([yRangeL, imgYRange])
+                self.assertSequenceEqual(dataRange.x, xRangeLR)
+                self.assertSequenceEqual(dataRange.y, yRangeL)
+                self.assertSequenceEqual(dataRange.yright, yRangeR)
+
+    def testDataRangeImageNegativeScaleX(self):
+        """image data range, negative scale"""
+
+        origin = (-10, 25)
+        scale = (-3., 8.)
+        image = numpy.arange(100.).reshape(20, 5)
+
+        plot = Plot(backend='none')
+        plot.addImage(image,
+                      origin=origin, scale=scale)
+
+        xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
+        xRange.sort()  # negative scale!
+        yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
+
+        ranges = {(False, False): (xRange, yRange),
+                  (True, False): (None, None),
+                  (True, True): (None, None),
+                  (False, True): (None, None)}
+
+        for logX, logY in ((False, False),
+                           (True, False),
+                           (True, True),
+                           (False, True),
+                           (False, False)):
+            with self.subTest(logX=logX, logY=logY):
+                plot.setXAxisLogarithmic(logX)
+                plot.setYAxisLogarithmic(logY)
+                dataRange = plot.getDataRange()
+                xRange, yRange = ranges[logX, logY]
+                self.assertTrue(numpy.array_equal(dataRange.x, xRange),
+                                msg='{0} != {1}'.format(dataRange.x, xRange))
+                self.assertTrue(numpy.array_equal(dataRange.y, yRange),
+                                msg='{0} != {1}'.format(dataRange.y, yRange))
+                self.assertIsNone(dataRange.yright)
+
+
+    def testDataRangeImageNegativeScaleY(self):
+        """image data range, negative scale"""
+
+        origin = (-10, 25)
+        scale = (3., -8.)
+        image = numpy.arange(100.).reshape(20, 5)
+
+        plot = Plot(backend='none')
+        plot.addImage(image,
+                      origin=origin, scale=scale)
+
+        xRange = numpy.array([0., image.shape[1] * scale[0]]) + origin[0]
+        yRange = numpy.array([0., image.shape[0] * scale[1]]) + origin[1]
+        yRange.sort()  # negative scale!
+
+        ranges = {(False, False): (xRange, yRange),
+                  (True, False): (None, None),
+                  (True, True): (None, None),
+                  (False, True): (None, None)}
+
+        for logX, logY in ((False, False),
+                           (True, False),
+                           (True, True),
+                           (False, True),
+                           (False, False)):
+            with self.subTest(logX=logX, logY=logY):
+                plot.setXAxisLogarithmic(logX)
+                plot.setYAxisLogarithmic(logY)
+                dataRange = plot.getDataRange()
+                xRange, yRange = ranges[logX, logY]
+                self.assertTrue(numpy.array_equal(dataRange.x, xRange),
+                                msg='{0} != {1}'.format(dataRange.x, xRange))
+                self.assertTrue(numpy.array_equal(dataRange.y, yRange),
+                                msg='{0} != {1}'.format(dataRange.y, yRange))
+                self.assertIsNone(dataRange.yright)
 
 
 def suite():
     test_suite = unittest.TestSuite()
     test_suite.addTest(
         unittest.defaultTestLoader.loadTestsFromTestCase(TestPlot))
+    test_suite.addTest(
+        unittest.defaultTestLoader.loadTestsFromTestCase(TestPlotRanges))
     return test_suite