From 23a46b5fb98b4d0cfadbb2cdb74a08c6b06f667d Mon Sep 17 00:00:00 2001 From: Carlos Pascual Date: Tue, 13 Oct 2020 17:52:07 +0200 Subject: [PATCH] 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() --- examples/histogram.py | 4 +- pyqtgraph/exporters/tests/test_csv.py | 2 +- pyqtgraph/graphicsItems/PlotCurveItem.py | 39 +++++++++++++++---- pyqtgraph/graphicsItems/PlotDataItem.py | 24 +++++++++--- .../graphicsItems/tests/test_PlotDataItem.py | 2 +- 5 files changed, 54 insertions(+), 17 deletions(-) diff --git a/examples/histogram.py b/examples/histogram.py index 85fbe3f0..53d4fdad 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -20,9 +20,9 @@ vals = np.hstack([np.random.normal(size=500), np.random.normal(size=260, loc=4)] ## compute standard histogram 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 -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 y = pg.pseudoScatter(vals, spacing=0.15) diff --git a/pyqtgraph/exporters/tests/test_csv.py b/pyqtgraph/exporters/tests/test_csv.py index d6da033b..9cffc64d 100644 --- a/pyqtgraph/exporters/tests/test_csv.py +++ b/pyqtgraph/exporters/tests/test_csv.py @@ -28,7 +28,7 @@ def test_CSVExporter(): y3 = [1,5,2,3,4,6,1,2,4,2,3,5,3] 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.export(fileName=tempfilename) diff --git a/pyqtgraph/graphicsItems/PlotCurveItem.py b/pyqtgraph/graphicsItems/PlotCurveItem.py index 0796f52c..f38934bd 100644 --- a/pyqtgraph/graphicsItems/PlotCurveItem.py +++ b/pyqtgraph/graphicsItems/PlotCurveItem.py @@ -62,7 +62,7 @@ class PlotCurveItem(GraphicsObject): 'fillLevel': None, 'fillOutline': False, 'brush': None, - 'stepMode': False, + 'stepMode': None, 'name': None, 'antialias': getConfigOption('antialias'), 'connect': 'all', @@ -315,9 +315,17 @@ class PlotCurveItem(GraphicsObject): by :func:`mkBrush ` is allowed. antialias (bool) Whether to use antialiasing when drawing. This is disabled by default because it decreases performance. - stepMode If True, two orthogonal lines are drawn for each sample - as steps. This is commonly used when drawing histograms. - Note that in this case, len(x) == len(y) + 1 + stepMode (str or None) If "center", a step is drawn using the x + values as boundaries and the given y values are + 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 by line segments. Default is "all", indicating full connection. "pairs" causes only even-numbered segments @@ -379,7 +387,10 @@ class PlotCurveItem(GraphicsObject): if 'stepMode' in kargs: self.opts['stepMode'] = kargs['stepMode'] - if self.opts['stepMode'] is True: + if self.opts['stepMode'] in ("center", True): ## check against True for backwards compatibility + 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 raise Exception("len(X) must be len(Y)+1 since stepMode=True (got %s and %s)" % (self.xData.shape, self.yData.shape)) else: @@ -413,10 +424,22 @@ class PlotCurveItem(GraphicsObject): profiler('emit') 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. - x2 = np.empty((len(x),2), dtype=x.dtype) - x2[:] = x[:,np.newaxis] + 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[:] = x[:, np.newaxis] + else: + raise ValueError("Unsupported stepMode %s" % stepMode) if self.opts['fillLevel'] is None: x = x2.reshape(x2.size)[1:-1] y2 = np.empty((len(y),2), dtype=y.dtype) diff --git a/pyqtgraph/graphicsItems/PlotDataItem.py b/pyqtgraph/graphicsItems/PlotDataItem.py index 4bc5a6d1..449f50a8 100644 --- a/pyqtgraph/graphicsItems/PlotDataItem.py +++ b/pyqtgraph/graphicsItems/PlotDataItem.py @@ -76,9 +76,17 @@ class PlotDataItem(GraphicsObject): fillOutline (bool) If True, an outline surrounding the *fillLevel* area is drawn. fillBrush Fill to use when fillLevel is specified. May be any single argument accepted by :func:`mkBrush() ` - stepMode If True, two orthogonal lines are drawn for each sample - as steps. This is commonly used when drawing histograms. - Note that in this case, ``len(x) == len(y) + 1`` + stepMode (str or None) If "center", a step is drawn using the x + values as boundaries and the given y values are + 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) ============ ============================================================================== @@ -376,6 +384,12 @@ class PlotDataItem(GraphicsObject): See :func:`__init__() ` for details; it accepts the same arguments. """ #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() y = None x = None @@ -520,8 +534,8 @@ class PlotDataItem(GraphicsObject): self.curve.hide() if scatterArgs['symbol'] is not None: - - if self.opts.get('stepMode', False) is True: + ## check against `True` too for backwards compatibility + if self.opts.get('stepMode', False) in ("center", True): x = 0.5 * (x[:-1] + x[1:]) self.scatter.setData(x=x, y=y, **scatterArgs) self.scatter.show() diff --git a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py index 894afc74..926b6098 100644 --- a/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py +++ b/pyqtgraph/graphicsItems/tests/test_PlotDataItem.py @@ -61,7 +61,7 @@ def test_clear(): def test_clear_in_step_mode(): 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) c.clear()