From dff39b095bf1c18a23266d480ba3a4240469a5f7 Mon Sep 17 00:00:00 2001 From: Thomas VINCENT Date: Mon, 29 Aug 2022 12:11:45 +0200 Subject: [PATCH 1/3] Fix error bars 2xN array documentation --- src/silx/gui/plot/PlotWidget.py | 10 +++++----- src/silx/gui/plot/items/core.py | 6 +++--- src/silx/gui/plot/items/scatter.py | 6 +++--- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/silx/gui/plot/PlotWidget.py b/src/silx/gui/plot/PlotWidget.py index 6cb5ef579e..99168eacce 100755 --- a/src/silx/gui/plot/PlotWidget.py +++ b/src/silx/gui/plot/PlotWidget.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility +# Copyright (c) 2004-2022 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 @@ -1130,8 +1130,8 @@ def addCurve(self, x, y, legend=None, info=None, :type xerror: A float, or a numpy.ndarray of float32. If it is an array, it can either be a 1D array of same length as the data or a 2D array with 2 rows - of same length as the data: row 0 for positive errors, - row 1 for negative errors. + of same length as the data: row 0 for lower errors, + row 1 for upper errors. :param yerror: Values with the uncertainties on the y values :type yerror: A float, or a numpy.ndarray of float32. See xerror. :param int z: Layer on which to draw the curve (default: 1) @@ -1540,8 +1540,8 @@ def addScatter(self, x, y, value, legend=None, colormap=None, :type xerror: A float, or a numpy.ndarray of float32. If it is an array, it can either be a 1D array of same length as the data or a 2D array with 2 rows - of same length as the data: row 0 for positive errors, - row 1 for negative errors. + of same length as the data: row 0 for lower errors, + row 1 for upper errors. :param yerror: Values with the uncertainties on the y values :type yerror: A float, or a numpy.ndarray of float32. See xerror. :param int z: Layer on which to draw the scatter (default: 1) diff --git a/src/silx/gui/plot/items/core.py b/src/silx/gui/plot/items/core.py index fa3b8cfe29..3b36a2d754 100644 --- a/src/silx/gui/plot/items/core.py +++ b/src/silx/gui/plot/items/core.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 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 @@ -1600,8 +1600,8 @@ def setData(self, x, y, xerror=None, yerror=None, copy=True): :type xerror: A float, or a numpy.ndarray of float32. If it is an array, it can either be a 1D array of same length as the data or a 2D array with 2 rows - of same length as the data: row 0 for positive errors, - row 1 for negative errors. + of same length as the data: row 0 for lower errors, + row 1 for upper errors. :param yerror: Values with the uncertainties on the y values. :type yerror: A float, or a numpy.ndarray of float32. See xerror. :param bool copy: True make a copy of the data (default), diff --git a/src/silx/gui/plot/items/scatter.py b/src/silx/gui/plot/items/scatter.py index fdc66f77f4..93d8db5674 100644 --- a/src/silx/gui/plot/items/scatter.py +++ b/src/silx/gui/plot/items/scatter.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 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 @@ -950,8 +950,8 @@ def setData(self, x, y, value, xerror=None, yerror=None, alpha=None, copy=True): :type xerror: A float, or a numpy.ndarray of float32. If it is an array, it can either be a 1D array of same length as the data or a 2D array with 2 rows - of same length as the data: row 0 for positive errors, - row 1 for negative errors. + of same length as the data: row 0 for lower errors, + row 1 for upper errors. :param yerror: Values with the uncertainties on the y values :type yerror: A float, or a numpy.ndarray of float32. See xerror. :param alpha: Values with the transparency (between 0 and 1) From 379d04549ac9f16b0c64a03120da663f83379df6 Mon Sep 17 00:00:00 2001 From: Thomas VINCENT Date: Mon, 29 Aug 2022 12:12:55 +0200 Subject: [PATCH 2/3] Take errorbars into account for bounding box --- src/silx/gui/plot/items/core.py | 38 +++++++++++++++++++++++++++------ 1 file changed, 31 insertions(+), 7 deletions(-) diff --git a/src/silx/gui/plot/items/core.py b/src/silx/gui/plot/items/core.py index 3b36a2d754..a90bf0f4ca 100644 --- a/src/silx/gui/plot/items/core.py +++ b/src/silx/gui/plot/items/core.py @@ -37,8 +37,7 @@ from copy import deepcopy import logging import enum -from typing import Optional, Tuple -import warnings +from typing import Optional, Tuple, Union import weakref import numpy @@ -1479,6 +1478,31 @@ def _logFilterData(self, xPositive, yPositive): return x, y, xerror, yerror + @staticmethod + def __minMaxDataWithError( + data: numpy.ndarray, + error: Optional[Union[float, numpy.ndarray]], + positiveOnly: bool + ) -> Tuple[float]: + if error is None: + min_, max_ = min_max(data, finite=True) + return min_, max_ + + # float, 1D or 2D array + dataMinusError = data - numpy.atleast_2d(error)[0] + dataMinusError = dataMinusError[numpy.isfinite(dataMinusError)] + if positiveOnly: + dataMinusError = dataMinusError[dataMinusError > 0] + min_ = numpy.nan if dataMinusError.size == 0 else numpy.min(dataMinusError) + + dataPlusError = data + numpy.atleast_2d(error)[-1] + dataPlusError = dataPlusError[numpy.isfinite(dataPlusError)] + if positiveOnly: + dataPlusError = dataPlusError[dataPlusError > 0] + max_ = numpy.nan if dataPlusError.size == 0 else numpy.max(dataPlusError) + + return min_, max_ + def _getBounds(self): if self.getXData(copy=False).size == 0: # Empty data return None @@ -1491,7 +1515,6 @@ def _getBounds(self): xPositive = False yPositive = False - # TODO bounds do not take error bars into account if (xPositive, yPositive) not in self._boundsCache: # use the getData class method because instance method can be # overloaded to return additional arrays @@ -1500,12 +1523,13 @@ def _getBounds(self): # hack to avoid duplicating caching mechanism in Scatter # (happens when cached data is used, caching done using # Scatter._logFilterData) - x, y, _xerror, _yerror = data[0], data[1], data[3], data[4] + x, y, xerror, yerror = data[0], data[1], data[3], data[4] else: - x, y, _xerror, _yerror = data + x, y, xerror, yerror = data + + xmin, xmax = self.__minMaxDataWithError(x, xerror, xPositive) + ymin, ymax = self.__minMaxDataWithError(y, yerror, yPositive) - xmin, xmax = min_max(x, finite=True) - ymin, ymax = min_max(y, finite=True) self._boundsCache[(xPositive, yPositive)] = tuple([ (bound if bound is not None else numpy.nan) for bound in (xmin, xmax, ymin, ymax)]) From 4b91174c6fade85b5827474982e7a0fe527225e0 Mon Sep 17 00:00:00 2001 From: Thomas VINCENT Date: Mon, 29 Aug 2022 14:37:27 +0200 Subject: [PATCH 3/3] add a test of errors taken into account in plot bounds --- src/silx/gui/plot/test/testPlotWidget.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/silx/gui/plot/test/testPlotWidget.py b/src/silx/gui/plot/test/testPlotWidget.py index f6e108d187..1f87e7690a 100755 --- a/src/silx/gui/plot/test/testPlotWidget.py +++ b/src/silx/gui/plot/test/testPlotWidget.py @@ -1,7 +1,7 @@ # coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 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 @@ -1651,6 +1651,26 @@ def testPlotCurveErrorLogXY(self): self.qapp.processEvents() + if xError is None: + dataMin, dataMax = numpy.min(self.xData), numpy.max(self.xData) + else: + xMinusError = self.xData - numpy.atleast_2d(xError)[0] + dataMin = numpy.min(xMinusError[xMinusError > 0]) + xPlusError = self.xData + numpy.atleast_2d(xError)[-1] + dataMax = numpy.max(xPlusError[xPlusError > 0]) + plotMin, plotMax = self.plot.getXAxis().getLimits() + assert numpy.allclose((dataMin, dataMax), (plotMin, plotMax)) + + if yError is None: + dataMin, dataMax = numpy.min(self.yData), numpy.max(self.yData) + else: + yMinusError = self.yData - numpy.atleast_2d(yError)[0] + dataMin = numpy.min(yMinusError[yMinusError > 0]) + yPlusError = self.yData + numpy.atleast_2d(yError)[-1] + dataMax = numpy.max(yPlusError[yPlusError > 0]) + plotMin, plotMax = self.plot.getYAxis().getLimits() + assert numpy.allclose((dataMin, dataMax), (plotMin, plotMax)) + self.plot.clear() self.plot.resetZoom() self.qapp.processEvents()