From f46666aceec4067c87b885be3bf6125133c0ca41 Mon Sep 17 00:00:00 2001 From: Thomas VINCENT Date: Wed, 13 Mar 2024 09:46:20 +0100 Subject: [PATCH] fix bounding box with error bars going < 0 --- src/silx/gui/plot/items/core.py | 98 +++++++++++++++++++++------------ 1 file changed, 63 insertions(+), 35 deletions(-) diff --git a/src/silx/gui/plot/items/core.py b/src/silx/gui/plot/items/core.py index 1e76ddddbc..8d61c4daa8 100644 --- a/src/silx/gui/plot/items/core.py +++ b/src/silx/gui/plot/items/core.py @@ -840,7 +840,12 @@ def setSymbolSize(self, size): str, Tuple[Union[float, int], None], Tuple[Union[float, int], Tuple[Union[float, int], Union[float, int]]], - Tuple[Union[float, int], Tuple[Union[float, int], Union[float, int], Union[float, int], Union[float, int]]], + Tuple[ + Union[float, int], + Tuple[ + Union[float, int], Union[float, int], Union[float, int], Union[float, int] + ], + ], ] """Type for :class:`LineMixIn`'s line style""" @@ -1448,48 +1453,66 @@ def __init__(self): self._boundsCache = {} @staticmethod - def _logFilterError(value, error): + def _errorAs2DArray( + error: numpy.ndarray | numbers.Number, + length: int, + ) -> numpy.ndarray: + """Convert error to a 2D array + + :param error: The error argument to convert to an array + :param length: Expected size of error array + :returns: 2D array of errors + """ + # Convert scalar to array + if not isinstance(error, numpy.ndarray): + error = numpy.full((length,), error, dtype=numpy.float64) + + # Convert Nx1 to N array + if error.ndim == 2 and error.shape[1] == 1 and length != 1: + error = numpy.ravel(error) + + # Return 2D array + return numpy.atleast_2d(error) + + @classmethod + def _logFilterError( + cls, + value: numpy.ndarray, + error: numpy.ndarray | float | int | None, + ) -> numpy.ndarray | None: """Filter/convert error values if they go <= 0. Replace error leading to negative values by nan - :param numpy.ndarray value: 1D array of values - :param numpy.ndarray error: + :param value: 1D array of values + :param error: Array of errors: scalar, N, Nx1 or 2xN or None. :return: Filtered error so error bars are never negative """ - if error is not None: - # Convert Nx1 to N - if error.ndim == 2 and error.shape[1] == 1 and len(value) != 1: - error = numpy.ravel(error) - - # Supports error being scalar, N or 2xN array - valueMinusError = value - numpy.atleast_2d(error)[0] - errorClipped = numpy.isnan(valueMinusError) - mask = numpy.logical_not(errorClipped) - errorClipped[mask] = valueMinusError[mask] <= 0 - - if numpy.any(errorClipped): # Need filtering - # expand errorbars to 2xN - if error.size == 1: # Scalar - error = numpy.full((2, len(value)), error, dtype=numpy.float64) + if error is None: + return None - elif error.ndim == 1: # N array - newError = numpy.empty((2, len(value)), dtype=numpy.float64) - newError[0, :] = error - newError[1, :] = error - error = newError + errorArray = cls._errorAs2DArray(error, len(value)) - elif error.size == 2 * len(value): # 2xN array - error = numpy.array(error, copy=True, dtype=numpy.float64) + # Supports error being scalar, N or 2xN array + valueMinusError = value - errorArray[0] + errorClipped = numpy.isnan(valueMinusError) + mask = numpy.logical_not(errorClipped) + errorClipped[mask] = valueMinusError[mask] <= 0 - else: - _logger.error("Unhandled error array") - return error + if not numpy.any(errorClipped): # No filtering + return error - error[0, errorClipped] = numpy.nan + # expand errorbars to 2xN + if len(errorArray) == 1: + filteredError = numpy.empty((2, len(value)), dtype=numpy.float64) + filteredError[0, :] = errorArray[0] + filteredError[1, :] = errorArray[0] + else: # 2xN array + filteredError = numpy.array(errorArray, copy=True, dtype=numpy.float64) - return error + filteredError[0, errorClipped] = numpy.nan + return filteredError def _getClippingBoolArray(self, xPositive, yPositive): """Compute a boolean array to filter out points with negative @@ -1563,8 +1586,9 @@ def _filterData(self, xPositive, yPositive): return x, y, xerror, yerror - @staticmethod + @classmethod def __minMaxDataWithError( + cls, data: numpy.ndarray, error: Optional[Union[float, numpy.ndarray]], positiveOnly: bool, @@ -1573,14 +1597,18 @@ def __minMaxDataWithError( min_, max_ = min_max(data, finite=True) return min_, max_ - # float, 1D or 2D array - dataMinusError = data - numpy.atleast_2d(error)[0] + errorArray = cls._errorAs2DArray(error, len(data)) + isNanError = numpy.isnan(errorArray) + + dataMinusError = data - errorArray[0] + dataMinusError[isNanError[0]] = data[isNanError[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 = data + errorArray[-1] + dataPlusError[isNanError[-1]] = data[isNanError[-1]] dataPlusError = dataPlusError[numpy.isfinite(dataPlusError)] if positiveOnly: dataPlusError = dataPlusError[dataPlusError > 0]