Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

silx.gui.plot.PlotWidget: Take errorbars into account for item bounds #3647

Merged
merged 3 commits into from
Sep 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions src/silx/gui/plot/PlotWidget.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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)
Expand Down
44 changes: 34 additions & 10 deletions src/silx/gui/plot/items/core.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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)])
Expand Down Expand Up @@ -1600,8 +1624,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),
Expand Down
6 changes: 3 additions & 3 deletions src/silx/gui/plot/items/scatter.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
Expand Down
22 changes: 21 additions & 1 deletion src/silx/gui/plot/test/testPlotWidget.py
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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()
Expand Down