Add "left" and "right" step Modes (#1360)

* Add "lstep" and "rstep" step Modes

stepMode is currently either True or False. If it is True,
it requires the user to make len(x) = len(y)+1. This is
inconvenient because it makes it difficult to change the
stepMode on a given curve (just as one would change, e.g.,
its color).

This commit extends the current situation by introducing
two more step modes: "lstep" and "rstep", which do not require
passing an extra x value. In turn, this modes associate each
y value to either the left or the right boundary of the step.

For example, the "rstep" mode is handy when plotting "life"
digital signals in which x,y data pairs are appended as they
are read.

This commit does not modify the behaviour in case of stepMode=True

* Replace step mode names: lstep,rstep -> left,right

* Improve docs for stepMode

Reword docstring and add it to PlotDataItem class too

* Document left and right stepModes as added in v 0.12.0

TODO: confirm the exact version number to use here

* Add comments stress the need for "is True"

Some conditional statements in the code regarding stepMode are
done with "is True". This is actually required since other
possible values such as "left" also evaluate as true but should
not be caught.

* Deprecate boolean API for stepMode

Introduce stepMode="mid" as a replacement of stepMode=True,
but keeping full backwards compatibility with the old API.
Adapt docs, examples and tests accordingly.

* Raise ValueError on unsupported stepMode values

* Rename "mid" step mode to "center"

* Remove "added in 0.12.0" note

See https://github.com/pyqtgraph/pyqtgraph/pull/1360#discussion_r502746919

* Add deprecation warning when stepMode=True

Issue a DeprecationWarning if stepMode=True is being passed to the
constructor or setData() of PlotDataItem or PlotCurveItem.

Note: warnings module is imported locally so that it is esier to
remove once this check is no longer needed.

* Fix wrong syntax in last commit

Fix usage of "default" kwarg in dict.get()
This commit is contained in:
Carlos Pascual 2020-10-13 17:52:07 +02:00 committed by GitHub
parent 325a15a6ef
commit 23a46b5fb9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 54 additions and 17 deletions

View File

@ -20,9 +20,9 @@ vals = np.hstack([np.random.normal(size=500), np.random.normal(size=260, loc=4)]
## compute standard histogram ## compute standard histogram
y,x = np.histogram(vals, bins=np.linspace(-3, 8, 40)) y,x = np.histogram(vals, bins=np.linspace(-3, 8, 40))
## Using stepMode=True causes the plot to draw two lines for each sample. ## Using stepMode="center" causes the plot to draw two lines for each sample.
## notice that len(x) == len(y)+1 ## notice that len(x) == len(y)+1
plt1.plot(x, y, stepMode=True, fillLevel=0, fillOutline=True, brush=(0,0,255,150)) plt1.plot(x, y, stepMode="center", fillLevel=0, fillOutline=True, brush=(0,0,255,150))
## Now draw all points as a nicely-spaced scatter plot ## Now draw all points as a nicely-spaced scatter plot
y = pg.pseudoScatter(vals, spacing=0.15) y = pg.pseudoScatter(vals, spacing=0.15)

View File

@ -28,7 +28,7 @@ def test_CSVExporter():
y3 = [1,5,2,3,4,6,1,2,4,2,3,5,3] y3 = [1,5,2,3,4,6,1,2,4,2,3,5,3]
x3 = pg.np.linspace(0, 1.0, len(y3)+1) x3 = pg.np.linspace(0, 1.0, len(y3)+1)
plt.plot(x=x3, y=y3, stepMode=True) plt.plot(x=x3, y=y3, stepMode="center")
ex = pg.exporters.CSVExporter(plt.plotItem) ex = pg.exporters.CSVExporter(plt.plotItem)
ex.export(fileName=tempfilename) ex.export(fileName=tempfilename)

View File

@ -62,7 +62,7 @@ class PlotCurveItem(GraphicsObject):
'fillLevel': None, 'fillLevel': None,
'fillOutline': False, 'fillOutline': False,
'brush': None, 'brush': None,
'stepMode': False, 'stepMode': None,
'name': None, 'name': None,
'antialias': getConfigOption('antialias'), 'antialias': getConfigOption('antialias'),
'connect': 'all', 'connect': 'all',
@ -315,9 +315,17 @@ class PlotCurveItem(GraphicsObject):
by :func:`mkBrush <pyqtgraph.mkBrush>` is allowed. by :func:`mkBrush <pyqtgraph.mkBrush>` is allowed.
antialias (bool) Whether to use antialiasing when drawing. This antialias (bool) Whether to use antialiasing when drawing. This
is disabled by default because it decreases performance. is disabled by default because it decreases performance.
stepMode If True, two orthogonal lines are drawn for each sample stepMode (str or None) If "center", a step is drawn using the x
as steps. This is commonly used when drawing histograms. values as boundaries and the given y values are
Note that in this case, len(x) == len(y) + 1 associated to the mid-points between the boundaries of
each step. This is commonly used when drawing
histograms. Note that in this case, len(x) == len(y) + 1
If "left" or "right", the step is drawn assuming that
the y value is associated to the left or right boundary,
respectively. In this case len(x) == len(y)
If not passed or an empty string or None is passed, the
step mode is not enabled.
Passing True is a deprecated equivalent to "center".
connect Argument specifying how vertexes should be connected connect Argument specifying how vertexes should be connected
by line segments. Default is "all", indicating full by line segments. Default is "all", indicating full
connection. "pairs" causes only even-numbered segments connection. "pairs" causes only even-numbered segments
@ -379,7 +387,10 @@ class PlotCurveItem(GraphicsObject):
if 'stepMode' in kargs: if 'stepMode' in kargs:
self.opts['stepMode'] = kargs['stepMode'] self.opts['stepMode'] = kargs['stepMode']
if self.opts['stepMode'] in ("center", True): ## check against True for backwards compatibility
if self.opts['stepMode'] is True: if self.opts['stepMode'] is True:
import warnings
warnings.warn('stepMode=True is deprecated, use stepMode="center" instead', DeprecationWarning, stacklevel=3)
if len(self.xData) != len(self.yData)+1: ## allow difference of 1 for step mode plots if len(self.xData) != len(self.yData)+1: ## allow difference of 1 for step mode plots
raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (self.xData.shape, self.yData.shape)) raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (self.xData.shape, self.yData.shape))
else: else:
@ -413,10 +424,22 @@ class PlotCurveItem(GraphicsObject):
profiler('emit') profiler('emit')
def generatePath(self, x, y): def generatePath(self, x, y):
if self.opts['stepMode']: stepMode = self.opts['stepMode']
if stepMode:
## each value in the x/y arrays generates 2 points. ## each value in the x/y arrays generates 2 points.
if stepMode == "right":
x2 = np.empty((len(x) + 1, 2), dtype=x.dtype)
x2[:-1] = x[:, np.newaxis]
x2[-1] = x2[-2]
elif stepMode == "left":
x2 = np.empty((len(x) + 1, 2), dtype=x.dtype)
x2[1:] = x[:, np.newaxis]
x2[0] = x2[1]
elif stepMode in ("center", True): ## support True for back-compat
x2 = np.empty((len(x),2), dtype=x.dtype) x2 = np.empty((len(x),2), dtype=x.dtype)
x2[:] = x[:, np.newaxis] x2[:] = x[:, np.newaxis]
else:
raise ValueError("Unsupported stepMode %s" % stepMode)
if self.opts['fillLevel'] is None: if self.opts['fillLevel'] is None:
x = x2.reshape(x2.size)[1:-1] x = x2.reshape(x2.size)[1:-1]
y2 = np.empty((len(y),2), dtype=y.dtype) y2 = np.empty((len(y),2), dtype=y.dtype)

View File

@ -76,9 +76,17 @@ class PlotDataItem(GraphicsObject):
fillOutline (bool) If True, an outline surrounding the *fillLevel* area is drawn. fillOutline (bool) If True, an outline surrounding the *fillLevel* area is drawn.
fillBrush Fill to use when fillLevel is specified. fillBrush Fill to use when fillLevel is specified.
May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>` May be any single argument accepted by :func:`mkBrush() <pyqtgraph.mkBrush>`
stepMode If True, two orthogonal lines are drawn for each sample stepMode (str or None) If "center", a step is drawn using the x
as steps. This is commonly used when drawing histograms. values as boundaries and the given y values are
Note that in this case, ``len(x) == len(y) + 1`` associated to the mid-points between the boundaries of
each step. This is commonly used when drawing
histograms. Note that in this case, len(x) == len(y) + 1
If "left" or "right", the step is drawn assuming that
the y value is associated to the left or right boundary,
respectively. In this case len(x) == len(y)
If not passed or an empty string or None is passed, the
step mode is not enabled.
Passing True is a deprecated equivalent to "center".
(added in version 0.9.9) (added in version 0.9.9)
============ ============================================================================== ============ ==============================================================================
@ -376,6 +384,12 @@ class PlotDataItem(GraphicsObject):
See :func:`__init__() <pyqtgraph.PlotDataItem.__init__>` for details; it accepts the same arguments. See :func:`__init__() <pyqtgraph.PlotDataItem.__init__>` for details; it accepts the same arguments.
""" """
#self.clear() #self.clear()
if kargs.get("stepMode", None) is True:
import warnings
warnings.warn(
'stepMode=True is deprecated, use stepMode="center" instead',
DeprecationWarning, stacklevel=3
)
profiler = debug.Profiler() profiler = debug.Profiler()
y = None y = None
x = None x = None
@ -520,8 +534,8 @@ class PlotDataItem(GraphicsObject):
self.curve.hide() self.curve.hide()
if scatterArgs['symbol'] is not None: if scatterArgs['symbol'] is not None:
## check against `True` too for backwards compatibility
if self.opts.get('stepMode', False) is True: if self.opts.get('stepMode', False) in ("center", True):
x = 0.5 * (x[:-1] + x[1:]) x = 0.5 * (x[:-1] + x[1:])
self.scatter.setData(x=x, y=y, **scatterArgs) self.scatter.setData(x=x, y=y, **scatterArgs)
self.scatter.show() self.scatter.show()

View File

@ -61,7 +61,7 @@ def test_clear():
def test_clear_in_step_mode(): def test_clear_in_step_mode():
w = pg.PlotWidget() w = pg.PlotWidget()
c = pg.PlotDataItem([1,4,2,3], [5,7,6], stepMode=True) c = pg.PlotDataItem([1,4,2,3], [5,7,6], stepMode="center")
w.addItem(c) w.addItem(c)
c.clear() c.clear()