From 61f3ce43b32cd6c562a073b315a87508c61d4732 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 16 Sep 2021 19:21:59 -0400 Subject: [PATCH 01/12] Toss in references step mode impl --- piker/ui/_curve.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/piker/ui/_curve.py b/piker/ui/_curve.py index 6b38420a..b0c10ef3 100644 --- a/piker/ui/_curve.py +++ b/piker/ui/_curve.py @@ -59,6 +59,19 @@ class FastAppendCurve(pg.PlotCurveItem): prepend_length = istart - x[0] append_length = x[-1] - istop + # TODO: step mode support + # if self.stepMode in ("center", True): ## support True for back-compat + # x2 = np.empty((len(x),2), dtype=x.dtype) + # x2[:] = x[:, np.newaxis] + + # ## If we have a fill level, add two extra points at either end + # x = x2.reshape(x2.size) + # y2 = np.empty((len(y)+2,2), dtype=y.dtype) + # y2[1:-1] = y[:,np.newaxis] + # y = y2.reshape(y2.size)[1:-1] + # y[0] = self.opts['fillLevel'] + # y[-1] = self.opts['fillLevel'] + if self.path is None or prepend_length: self.path = pg.functions.arrayToQPath( x[:-1], From 4cf51ffb1eb917941d42970ce152eba7ceeaaf0a Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 17 Sep 2021 16:01:28 -0400 Subject: [PATCH 02/12] Draft 'step' curve; couldn't get pg builtin to work --- piker/ui/_curve.py | 65 ++++++++++++++++++++++++++++++++++++---------- 1 file changed, 51 insertions(+), 14 deletions(-) diff --git a/piker/ui/_curve.py b/piker/ui/_curve.py index b0c10ef3..b4592923 100644 --- a/piker/ui/_curve.py +++ b/piker/ui/_curve.py @@ -20,6 +20,7 @@ Fast, smooth, sexy curves. """ from typing import Tuple +import numpy as np import pyqtgraph as pg from PyQt5 import QtCore, QtGui, QtWidgets @@ -29,7 +30,12 @@ from .._profile import pg_profile_enabled # TODO: got a feeling that dropping this inheritance gets us even more speedups class FastAppendCurve(pg.PlotCurveItem): - def __init__(self, *args, **kwargs): + def __init__( + self, + *args, + step_mode: bool = False, + **kwargs + ) -> None: # TODO: we can probably just dispense with the parent since # we're basically only using the pen setting now... @@ -37,6 +43,7 @@ class FastAppendCurve(pg.PlotCurveItem): self._last_line: QtCore.QLineF = None self._xrange: Tuple[int, int] = self.dataBounds(ax=0) + self._step_mode: bool = step_mode # TODO: one question still remaining is if this makes trasform # interactions slower (such as zooming) and if so maybe if/when @@ -59,23 +66,53 @@ class FastAppendCurve(pg.PlotCurveItem): prepend_length = istart - x[0] append_length = x[-1] - istop - # TODO: step mode support - # if self.stepMode in ("center", True): ## support True for back-compat - # x2 = np.empty((len(x),2), dtype=x.dtype) - # x2[:] = x[:, np.newaxis] + # step mode: draw flat top discrete "step" + # over the index space for each datum. + if self._step_mode: + y_out = y.copy() + x_out = x.copy() + x2 = np.empty( + # the data + 2 endpoints on either end for + # "termination of the path". + (len(x) + 1, 2), + # we want to align with OHLC or other sampling style + # bars likely so we need fractinal values + dtype=float, + ) + x2[0] = x[0] - 0.5 + x2[1] = x[0] + 0.5 + x2[1:] = x[:, np.newaxis] + 0.5 + + # flatten to 1-d + x_out = x2.reshape(x2.size) + + # we create a 1d with 2 extra indexes to + # hold the start and (current) end value for the steps + # on either end + y_out = np.empty( + 2*len(y) + 2, + dtype=y.dtype + ) + y2 = np.empty((len(y), 2), dtype=y.dtype) + y2[:] = y[:,np.newaxis] + # flatten + y_out[1:-1] = y2.reshape(y2.size) + y_out[0] = 0 + y_out[-1] = 0 + + # TODO: see ``painter.fillPath()`` call + # inside parent's ``.paint()`` to get + # a solid brush under the curve. + + else: + # by default we only pull data up to the last (current) index + x_out, y_out = x[:-1], y[:-1] - # ## If we have a fill level, add two extra points at either end - # x = x2.reshape(x2.size) - # y2 = np.empty((len(y)+2,2), dtype=y.dtype) - # y2[1:-1] = y[:,np.newaxis] - # y = y2.reshape(y2.size)[1:-1] - # y[0] = self.opts['fillLevel'] - # y[-1] = self.opts['fillLevel'] if self.path is None or prepend_length: self.path = pg.functions.arrayToQPath( - x[:-1], - y[:-1], + x_out, + y_out, connect='all' ) profiler('generate fresh path') From e4e1b4d64aa91dca889d24ce52a39b27191e2c86 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 17 Sep 2021 16:30:56 -0400 Subject: [PATCH 03/12] Invert 'c' (connection) array In latest `pyqtgraph` it seems there's a discrepancy since `function.arrayToQPath()` was reworked and now we need to *not* connect the last point for each bar. --- piker/ui/_ohlc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piker/ui/_ohlc.py b/piker/ui/_ohlc.py index 7ae010fa..db6a343a 100644 --- a/piker/ui/_ohlc.py +++ b/piker/ui/_ohlc.py @@ -146,7 +146,7 @@ def path_arrays_from_ohlc( # specifies that the first edge is never connected to the # prior bars last edge thus providing a small "gap"/"space" # between bars determined by ``bar_gap``. - c[istart:istop] = (0, 1, 1, 1, 1, 1) + c[istart:istop] = (1, 1, 1, 1, 1, 0) return x, y, c From c378a56b29a309de4ad2030c5d8fb23565922157 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 19 Sep 2021 15:56:02 -0400 Subject: [PATCH 04/12] Add last step updates and path fill support --- piker/ui/_curve.py | 159 ++++++++++++++++++++++++++++++++------------- 1 file changed, 115 insertions(+), 44 deletions(-) diff --git a/piker/ui/_curve.py b/piker/ui/_curve.py index b4592923..53a85980 100644 --- a/piker/ui/_curve.py +++ b/piker/ui/_curve.py @@ -22,9 +22,87 @@ from typing import Tuple import numpy as np import pyqtgraph as pg -from PyQt5 import QtCore, QtGui, QtWidgets +from PyQt5 import QtGui, QtWidgets +from PyQt5.QtCore import ( + QLineF, + QSizeF, + QRectF, + QPointF, +) from .._profile import pg_profile_enabled +from ._style import hcolor + + +def step_path_arrays_from_1d( + x: np.ndarray, + y: np.ndarray, + +) -> (np.ndarray, np.ndarray): + '''Generate a "step mode" curve aligned with OHLC style bars + such that each segment spans each bar (aka "centered" style). + + ''' + y_out = y.copy() + x_out = x.copy() + x2 = np.empty( + # the data + 2 endpoints on either end for + # "termination of the path". + (len(x) + 1, 2), + # we want to align with OHLC or other sampling style + # bars likely so we need fractinal values + dtype=float, + ) + x2[0] = x[0] - 0.5 + x2[1] = x[0] + 0.5 + x2[1:] = x[:, np.newaxis] + 0.5 + + # flatten to 1-d + x_out = x2.reshape(x2.size) + + # we create a 1d with 2 extra indexes to + # hold the start and (current) end value for the steps + # on either end + y_out = np.empty( + 2*len(y) + 2, + dtype=y.dtype + ) + y2 = np.empty((len(y), 2), dtype=y.dtype) + y2[:] = y[:, np.newaxis] + + # flatten and set 0 endpoints + y_out[1:-1] = y2.reshape(y2.size) + y_out[0] = 0 + y_out[-1] = 0 + + return x_out, y_out + + +def step_lines_from_point( + index: float, + level: float, + +) -> Tuple[QLineF]: + + # TODO: maybe consider using `QGraphicsLineItem` ?? + # gives us a ``.boundingRect()`` on the objects which may make + # computing the composite bounding rect of the last bars + the + # history path faster since it's done in C++: + # https://doc.qt.io/qt-5/qgraphicslineitem.html + + # index = x[0] + # level = y[0] + + # (x0 - 0.5, 0) -> (x0 - 0.5, y0) + left = QLineF(index - 0.5, 0, index - 0.5, level) + + # (x0 - 0.5, y0) -> (x1 + 0.5, y1) + top = QLineF(index - 0.5, level, index + 0.5, level) + + # (x1 + 0.5, y1 -> (x1 + 0.5, 0) + right = QLineF(index + 0.5, level, index + 0.5, 0) + + return [left, top, right] # TODO: got a feeling that dropping this inheritance gets us even more speedups @@ -41,10 +119,13 @@ class FastAppendCurve(pg.PlotCurveItem): # we're basically only using the pen setting now... super().__init__(*args, **kwargs) - self._last_line: QtCore.QLineF = None + self._last_line: QLineF = None self._xrange: Tuple[int, int] = self.dataBounds(ax=0) self._step_mode: bool = step_mode + self.setBrush(hcolor('bracket')) + + breakpoint() # TODO: one question still remaining is if this makes trasform # interactions slower (such as zooming) and if so maybe if/when # we implement a "history" mode for the view we disable this in @@ -53,8 +134,9 @@ class FastAppendCurve(pg.PlotCurveItem): def update_from_array( self, - x, - y, + x: np.ndarray, + y: np.ndarray, + ) -> QtGui.QPainterPath: profiler = pg.debug.Profiler(disabled=not pg_profile_enabled()) @@ -69,37 +151,7 @@ class FastAppendCurve(pg.PlotCurveItem): # step mode: draw flat top discrete "step" # over the index space for each datum. if self._step_mode: - y_out = y.copy() - x_out = x.copy() - x2 = np.empty( - # the data + 2 endpoints on either end for - # "termination of the path". - (len(x) + 1, 2), - # we want to align with OHLC or other sampling style - # bars likely so we need fractinal values - dtype=float, - ) - x2[0] = x[0] - 0.5 - x2[1] = x[0] + 0.5 - x2[1:] = x[:, np.newaxis] + 0.5 - - # flatten to 1-d - x_out = x2.reshape(x2.size) - - # we create a 1d with 2 extra indexes to - # hold the start and (current) end value for the steps - # on either end - y_out = np.empty( - 2*len(y) + 2, - dtype=y.dtype - ) - y2 = np.empty((len(y), 2), dtype=y.dtype) - y2[:] = y[:,np.newaxis] - # flatten - y_out[1:-1] = y2.reshape(y2.size) - y_out[0] = 0 - y_out[-1] = 0 - + x_out, y_out = step_path_arrays_from_1d(x[:-1], y[:-1]) # TODO: see ``painter.fillPath()`` call # inside parent's ``.paint()`` to get # a solid brush under the curve. @@ -108,7 +160,6 @@ class FastAppendCurve(pg.PlotCurveItem): # by default we only pull data up to the last (current) index x_out, y_out = x[:-1], y[:-1] - if self.path is None or prepend_length: self.path = pg.functions.arrayToQPath( x_out, @@ -139,6 +190,11 @@ class FastAppendCurve(pg.PlotCurveItem): new_y = y[-append_length - 2:-1] # print((new_x, new_y)) + if self._step_mode: + new_x, new_y = step_path_arrays_from_1d(new_x, new_y) + new_x = new_x[2:] + new_y = new_y[2:] + append_path = pg.functions.arrayToQPath( new_x, new_y, @@ -148,6 +204,7 @@ class FastAppendCurve(pg.PlotCurveItem): # self.path.moveTo(new_x[0], new_y[0]) # self.path.connectPath(append_path) self.path.connectPath(append_path) + # self.fill_path.connectPath( # XXX: pretty annoying but, without this there's little # artefacts on the append updates to the curve... @@ -163,7 +220,10 @@ class FastAppendCurve(pg.PlotCurveItem): self.yData = y self._xrange = x[0], x[-1] - self._last_line = QtCore.QLineF(x[-2], y[-2], x[-1], y[-1]) + if self._step_mode: + self._last_step_lines = step_lines_from_point(x[-1], y[-1]) + else: + self._last_line = QLineF(x[-2], y[-2], x[-1], y[-1]) # trigger redraw of path # do update before reverting to cache mode @@ -193,13 +253,13 @@ class FastAppendCurve(pg.PlotCurveItem): w = hb_size.width() + 1 h = hb_size.height() + 1 - br = QtCore.QRectF( + br = QRectF( # top left - QtCore.QPointF(hb.topLeft()), + QPointF(hb.topLeft()), # total size - QtCore.QSizeF(w, h) + QSizeF(w, h) ) # print(f'bounding rect: {br}') return br @@ -215,8 +275,19 @@ class FastAppendCurve(pg.PlotCurveItem): # p.setRenderHint(p.Antialiasing, True) p.setPen(self.opts['pen']) - p.drawLine(self._last_line) - profiler('.drawLine()') - p.drawPath(self.path) - profiler('.drawPath()') + if self._step_mode: + p.drawLines(*tuple(filter(bool, self._last_step_lines))) + + # fill_path = QtGui.QPainterPath(self.path) + self.path.closeSubpath() + p.fillPath(self.path, self.opts['brush']) + p.drawPath(self.path) + profiler('.drawPath()') + + else: + p.drawLine(self._last_line) + profiler('.drawLine()') + + p.drawPath(self.path) + profiler('.drawPath()') From 0876d2f4fe43df5d40e363b86a6306fb9d501fce Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 Sep 2021 08:39:44 -0400 Subject: [PATCH 05/12] Bleh, try a bunch of stuff for step filling Turns out the performance of updating and refilling step curves > 1k ish points is super slow :sadkek:. Disabling the fill basically returns normal performance, so it seems maybe we'll stick with unfilled volume "bars" for now. The other tricky bit is getting the path to extend and fill which is particularly slow if you use the `QPainterPath.united()` (what `+` set op does) operation which seems to require an entire redraw of the curve each paint iteration. Removing the pixel buffer cache makes things that much worse too.. One technique i tried was only setting a `._fill` flag when so many datums are in view (< 1k as determined by the chart widget), and this helps, but under high load (trade rates) you still see more lag then without the fill which makes me say screw it and let's stick with unfilled bars for now. Trying go to get performant filled curves will be an exercise for an aspiring graphics eng :P --- piker/ui/_curve.py | 71 ++++++++++++++++++++++++++++++++-------------- 1 file changed, 50 insertions(+), 21 deletions(-) diff --git a/piker/ui/_curve.py b/piker/ui/_curve.py index 53a85980..ae24cc07 100644 --- a/piker/ui/_curve.py +++ b/piker/ui/_curve.py @@ -122,10 +122,12 @@ class FastAppendCurve(pg.PlotCurveItem): self._last_line: QLineF = None self._xrange: Tuple[int, int] = self.dataBounds(ax=0) self._step_mode: bool = step_mode + self._fill = False - self.setBrush(hcolor('bracket')) + color = hcolor('davies') + self.setBrush(color) + self.setPen(color) - breakpoint() # TODO: one question still remaining is if this makes trasform # interactions slower (such as zooming) and if so maybe if/when # we implement a "history" mode for the view we disable this in @@ -152,9 +154,6 @@ class FastAppendCurve(pg.PlotCurveItem): # over the index space for each datum. if self._step_mode: x_out, y_out = step_path_arrays_from_1d(x[:-1], y[:-1]) - # TODO: see ``painter.fillPath()`` call - # inside parent's ``.paint()`` to get - # a solid brush under the curve. else: # by default we only pull data up to the last (current) index @@ -164,10 +163,14 @@ class FastAppendCurve(pg.PlotCurveItem): self.path = pg.functions.arrayToQPath( x_out, y_out, - connect='all' + connect='all', + finiteCheck=False, ) profiler('generate fresh path') + if self._step_mode: + self.path.closeSubpath() + # TODO: get this working - right now it's giving heck on vwap... # if prepend_length: # breakpoint() @@ -185,25 +188,44 @@ class FastAppendCurve(pg.PlotCurveItem): # self.path.connectPath(old_path) if append_length: - # print(f"append_length: {append_length}") - new_x = x[-append_length - 2:-1] - new_y = y[-append_length - 2:-1] - # print((new_x, new_y)) - if self._step_mode: + new_x = x[-append_length - 2:-1] + new_y = y[-append_length - 2:-1] new_x, new_y = step_path_arrays_from_1d(new_x, new_y) - new_x = new_x[2:] - new_y = new_y[2:] + # new_x = new_x[3:] + # new_y = new_y[3:] + + else: + # print(f"append_length: {append_length}") + new_x = x[-append_length - 2:-1] + new_y = y[-append_length - 2:-1] + # print((new_x, new_y)) append_path = pg.functions.arrayToQPath( new_x, new_y, - connect='all' + connect='all', + finiteCheck=False, ) - # print(f"append_path br: {append_path.boundingRect()}") - # self.path.moveTo(new_x[0], new_y[0]) - # self.path.connectPath(append_path) - self.path.connectPath(append_path) + + if self._step_mode: + if self._fill: + path = self.path + # self.path = self.path.united(append_path).simplified() + path.addPath(append_path.simplified()) + # path.connectPath(append_path.simplified()) + path.closeSubpath() + # path.simplified() + else: + self.path.connectPath(append_path.simplified()) + else: + # print(f"append_path br: {append_path.boundingRect()}") + # self.path.moveTo(new_x[0], new_y[0]) + # self.path.connectPath(append_path) + self.path.connectPath(append_path.simplified()) + + # if self._step_mode: + # self.path.closeSubpath() # self.fill_path.connectPath( # XXX: pretty annoying but, without this there's little @@ -221,6 +243,7 @@ class FastAppendCurve(pg.PlotCurveItem): self._xrange = x[0], x[-1] if self._step_mode: + # TODO: use a ``QRectF`` and ``QPainterPath.addRect()`` self._last_step_lines = step_lines_from_point(x[-1], y[-1]) else: self._last_line = QLineF(x[-2], y[-2], x[-1], y[-1]) @@ -277,12 +300,18 @@ class FastAppendCurve(pg.PlotCurveItem): p.setPen(self.opts['pen']) if self._step_mode: + p.drawLines(*tuple(filter(bool, self._last_step_lines))) - # fill_path = QtGui.QPainterPath(self.path) - self.path.closeSubpath() - p.fillPath(self.path, self.opts['brush']) p.drawPath(self.path) + + # fill_path = QtGui.QPainterPath(self.path) + # if self._fill: + # self.path.closeSubpath() + if self._fill: + print('FILLED') + p.fillPath(self.path, self.opts['brush']) + profiler('.drawPath()') else: From 5bf8e6a90e73ac64b4f3cafaed9b1721fbe0b1ee Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 Sep 2021 13:38:12 -0400 Subject: [PATCH 06/12] Use filled rect for current step A `QRectF` is easier to make and draw (i think?) so use that and fill it on volume events for decent sleek real-time look. Adjust the step array generator to allow for an endpoints flag. Comment and/or clean out all the old path filling calls that gave us perf issues.. --- piker/ui/_curve.py | 104 ++++++++++++++++++--------------------------- 1 file changed, 42 insertions(+), 62 deletions(-) diff --git a/piker/ui/_curve.py b/piker/ui/_curve.py index ae24cc07..78d4cba1 100644 --- a/piker/ui/_curve.py +++ b/piker/ui/_curve.py @@ -18,7 +18,7 @@ Fast, smooth, sexy curves. """ -from typing import Tuple +from typing import Optional import numpy as np import pyqtgraph as pg @@ -37,6 +37,7 @@ from ._style import hcolor def step_path_arrays_from_1d( x: np.ndarray, y: np.ndarray, + include_endpoints: bool = False, ) -> (np.ndarray, np.ndarray): '''Generate a "step mode" curve aligned with OHLC style bars @@ -63,46 +64,24 @@ def step_path_arrays_from_1d( # we create a 1d with 2 extra indexes to # hold the start and (current) end value for the steps # on either end + y2 = np.empty((len(y), 2), dtype=y.dtype) + y2[:] = y[:, np.newaxis] + y_out = np.empty( 2*len(y) + 2, dtype=y.dtype ) - y2 = np.empty((len(y), 2), dtype=y.dtype) - y2[:] = y[:, np.newaxis] # flatten and set 0 endpoints y_out[1:-1] = y2.reshape(y2.size) y_out[0] = 0 y_out[-1] = 0 - return x_out, y_out + if not include_endpoints: + return x_out[:-1], y_out[:-1] - -def step_lines_from_point( - index: float, - level: float, - -) -> Tuple[QLineF]: - - # TODO: maybe consider using `QGraphicsLineItem` ?? - # gives us a ``.boundingRect()`` on the objects which may make - # computing the composite bounding rect of the last bars + the - # history path faster since it's done in C++: - # https://doc.qt.io/qt-5/qgraphicslineitem.html - - # index = x[0] - # level = y[0] - - # (x0 - 0.5, 0) -> (x0 - 0.5, y0) - left = QLineF(index - 0.5, 0, index - 0.5, level) - - # (x0 - 0.5, y0) -> (x1 + 0.5, y1) - top = QLineF(index - 0.5, level, index + 0.5, level) - - # (x1 + 0.5, y1 -> (x1 + 0.5, 0) - right = QLineF(index + 0.5, level, index + 0.5, 0) - - return [left, top, right] + else: + return x_out, y_out # TODO: got a feeling that dropping this inheritance gets us even more speedups @@ -112,7 +91,11 @@ class FastAppendCurve(pg.PlotCurveItem): self, *args, step_mode: bool = False, + color: str = 'default_lightest', + fill_color: Optional[str] = None, + **kwargs + ) -> None: # TODO: we can probably just dispense with the parent since @@ -120,13 +103,12 @@ class FastAppendCurve(pg.PlotCurveItem): super().__init__(*args, **kwargs) self._last_line: QLineF = None - self._xrange: Tuple[int, int] = self.dataBounds(ax=0) + self._xrange: tuple[int, int] = self.dataBounds(ax=0) self._step_mode: bool = step_mode self._fill = False - color = hcolor('davies') - self.setBrush(color) - self.setPen(color) + self.setBrush(hcolor(fill_color or color)) + self.setPen(hcolor(color)) # TODO: one question still remaining is if this makes trasform # interactions slower (such as zooming) and if so maybe if/when @@ -168,8 +150,8 @@ class FastAppendCurve(pg.PlotCurveItem): ) profiler('generate fresh path') - if self._step_mode: - self.path.closeSubpath() + # if self._step_mode: + # self.path.closeSubpath() # TODO: get this working - right now it's giving heck on vwap... # if prepend_length: @@ -187,13 +169,14 @@ class FastAppendCurve(pg.PlotCurveItem): # # self.path.moveTo(new_x[0], new_y[0]) # self.path.connectPath(old_path) - if append_length: + elif append_length: if self._step_mode: - new_x = x[-append_length - 2:-1] - new_y = y[-append_length - 2:-1] - new_x, new_y = step_path_arrays_from_1d(new_x, new_y) - # new_x = new_x[3:] - # new_y = new_y[3:] + new_x, new_y = step_path_arrays_from_1d( + x[-append_length - 2:-1], + y[-append_length - 2:-1], + ) + new_x = new_x[1:] + new_y = new_y[1:] else: # print(f"append_length: {append_length}") @@ -205,28 +188,26 @@ class FastAppendCurve(pg.PlotCurveItem): new_x, new_y, connect='all', - finiteCheck=False, + # finiteCheck=False, ) + path = self.path if self._step_mode: + if self._fill: - path = self.path - # self.path = self.path.united(append_path).simplified() - path.addPath(append_path.simplified()) - # path.connectPath(append_path.simplified()) - path.closeSubpath() - # path.simplified() + # XXX: super slow set "union" op + self.path = self.path.united(append_path).simplified() + + # path.addPath(append_path) + # path.closeSubpath() else: - self.path.connectPath(append_path.simplified()) + # path.addPath(append_path) + self.path.connectPath(append_path) else: # print(f"append_path br: {append_path.boundingRect()}") # self.path.moveTo(new_x[0], new_y[0]) # self.path.connectPath(append_path) - self.path.connectPath(append_path.simplified()) - - # if self._step_mode: - # self.path.closeSubpath() - # self.fill_path.connectPath( + path.connectPath(append_path) # XXX: pretty annoying but, without this there's little # artefacts on the append updates to the curve... @@ -243,8 +224,7 @@ class FastAppendCurve(pg.PlotCurveItem): self._xrange = x[0], x[-1] if self._step_mode: - # TODO: use a ``QRectF`` and ``QPainterPath.addRect()`` - self._last_step_lines = step_lines_from_point(x[-1], y[-1]) + self._last_step_rect = QRectF(x[-1] - 0.5, 0, x[-1] + 0.5, y[-1]) else: self._last_line = QLineF(x[-2], y[-2], x[-1], y[-1]) @@ -301,16 +281,16 @@ class FastAppendCurve(pg.PlotCurveItem): if self._step_mode: - p.drawLines(*tuple(filter(bool, self._last_step_lines))) + brush = self.opts['brush'] + # p.drawLines(*tuple(filter(bool, self._last_step_lines))) + # p.drawRect(self._last_step_rect) + p.fillRect(self._last_step_rect, brush) p.drawPath(self.path) - # fill_path = QtGui.QPainterPath(self.path) - # if self._fill: - # self.path.closeSubpath() if self._fill: print('FILLED') - p.fillPath(self.path, self.opts['brush']) + p.fillPath(self.path, brush) profiler('.drawPath()') From 43666a1a8efdab2bafce57a0b2f81722438fd765 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 20 Sep 2021 13:41:53 -0400 Subject: [PATCH 07/12] Increase current bar's pen size by a px --- piker/ui/_ohlc.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/piker/ui/_ohlc.py b/piker/ui/_ohlc.py index db6a343a..1f383d02 100644 --- a/piker/ui/_ohlc.py +++ b/piker/ui/_ohlc.py @@ -182,12 +182,14 @@ class BarItems(pg.GraphicsObject): # scene: 'QGraphicsScene', # noqa plotitem: 'pg.PlotItem', # noqa pen_color: str = 'bracket', + last_bar_color: str = 'bracket', ) -> None: super().__init__() # XXX: for the mega-lulz increasing width here increases draw latency... # so probably don't do it until we figure that out. self.bars_pen = pg.mkPen(hcolor(pen_color), width=1) + self.last_bar_pen = pg.mkPen(hcolor(last_bar_color), width=2) # NOTE: this prevents redraws on mouse interaction which is # a huge boon for avg interaction latency. @@ -364,7 +366,6 @@ class BarItems(pg.GraphicsObject): profiler = pg.debug.Profiler(disabled=not pg_profile_enabled()) # p.setCompositionMode(0) - p.setPen(self.bars_pen) # TODO: one thing we could try here is pictures being drawn of # a fixed count of bars such that based on the viewbox indices we @@ -372,9 +373,11 @@ class BarItems(pg.GraphicsObject): # as is necesarry for what's in "view". Not sure if this will # lead to any perf gains other then when zoomed in to less bars # in view. + p.setPen(self.last_bar_pen) p.drawLines(*tuple(filter(bool, self._last_bar_lines))) profiler('draw last bar') + p.setPen(self.bars_pen) p.drawPath(self.path) profiler('draw history path') From 95bf522b48d8705b3db37df17281a0312210eabf Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 21 Sep 2021 15:27:22 -0400 Subject: [PATCH 08/12] Always draw a last step line with px width=2 --- piker/ui/_curve.py | 62 +++++++++++++++++++++++++++++++--------------- 1 file changed, 42 insertions(+), 20 deletions(-) diff --git a/piker/ui/_curve.py b/piker/ui/_curve.py index 78d4cba1..448a2cdc 100644 --- a/piker/ui/_curve.py +++ b/piker/ui/_curve.py @@ -102,14 +102,22 @@ class FastAppendCurve(pg.PlotCurveItem): # we're basically only using the pen setting now... super().__init__(*args, **kwargs) - self._last_line: QLineF = None self._xrange: tuple[int, int] = self.dataBounds(ax=0) - self._step_mode: bool = step_mode - self._fill = False - self.setBrush(hcolor(fill_color or color)) + # all history of curve is drawn in single px thickness self.setPen(hcolor(color)) + # last segment is drawn in 2px thickness for emphasis + self.last_step_pen = pg.mkPen(hcolor(color), width=2) + self._last_line: QLineF = None + self._last_step_rect: QRectF = None + + # flat-top style histogram-like discrete curve + self._step_mode: bool = step_mode + + self._fill = False + self.setBrush(hcolor(fill_color or color)) + # TODO: one question still remaining is if this makes trasform # interactions slower (such as zooming) and if so maybe if/when # we implement a "history" mode for the view we disable this in @@ -192,8 +200,10 @@ class FastAppendCurve(pg.PlotCurveItem): ) path = self.path - if self._step_mode: + # other merging ideas: + # https://stackoverflow.com/questions/8936225/how-to-merge-qpainterpaths + if self._step_mode: if self._fill: # XXX: super slow set "union" op self.path = self.path.united(append_path).simplified() @@ -222,11 +232,23 @@ class FastAppendCurve(pg.PlotCurveItem): self.xData = x self.yData = y - self._xrange = x[0], x[-1] + x0, x_last = self._xrange = x[0], x[-1] + y_last = y[-1] + if self._step_mode: - self._last_step_rect = QRectF(x[-1] - 0.5, 0, x[-1] + 0.5, y[-1]) + self._last_line = QLineF( + x_last - 0.5, 0, + x_last + 0.5, 0, + ) + self._last_step_rect = QRectF( + x_last - 0.5, 0, + x_last + 0.5, y_last + ) else: - self._last_line = QLineF(x[-2], y[-2], x[-1], y[-1]) + self._last_line = QLineF( + x[-2], y[-2], + x[-1], y_last + ) # trigger redraw of path # do update before reverting to cache mode @@ -277,8 +299,6 @@ class FastAppendCurve(pg.PlotCurveItem): profiler = pg.debug.Profiler(disabled=not pg_profile_enabled()) # p.setRenderHint(p.Antialiasing, True) - p.setPen(self.opts['pen']) - if self._step_mode: brush = self.opts['brush'] @@ -286,17 +306,19 @@ class FastAppendCurve(pg.PlotCurveItem): # p.drawRect(self._last_step_rect) p.fillRect(self._last_step_rect, brush) - p.drawPath(self.path) + # p.drawPath(self.path) - if self._fill: - print('FILLED') - p.fillPath(self.path, brush) + # profiler('.drawPath()') - profiler('.drawPath()') + # else: + p.setPen(self.last_step_pen) + p.drawLine(self._last_line) + profiler('.drawLine()') - else: - p.drawLine(self._last_line) - profiler('.drawLine()') + p.setPen(self.opts['pen']) + p.drawPath(self.path) + profiler('.drawPath()') - p.drawPath(self.path) - profiler('.drawPath()') + if self._fill: + print('FILLED') + p.fillPath(self.path, brush) From 739399d5a9dd6d38ea0b152deb491ec6b975a3ec Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 21 Sep 2021 15:27:45 -0400 Subject: [PATCH 09/12] Make `.paint()` method always the last --- piker/ui/_ohlc.py | 54 +++++++++++++++++++++++------------------------ 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/piker/ui/_ohlc.py b/piker/ui/_ohlc.py index 1f383d02..4331f036 100644 --- a/piker/ui/_ohlc.py +++ b/piker/ui/_ohlc.py @@ -186,8 +186,8 @@ class BarItems(pg.GraphicsObject): ) -> None: super().__init__() - # XXX: for the mega-lulz increasing width here increases draw latency... - # so probably don't do it until we figure that out. + # XXX: for the mega-lulz increasing width here increases draw + # latency... so probably don't do it until we figure that out. self.bars_pen = pg.mkPen(hcolor(pen_color), width=1) self.last_bar_pen = pg.mkPen(hcolor(last_bar_color), width=2) @@ -356,31 +356,6 @@ class BarItems(pg.GraphicsObject): if flip_cache: self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) - def paint( - self, - p: QtGui.QPainter, - opt: QtWidgets.QStyleOptionGraphicsItem, - w: QtWidgets.QWidget - ) -> None: - - profiler = pg.debug.Profiler(disabled=not pg_profile_enabled()) - - # p.setCompositionMode(0) - - # TODO: one thing we could try here is pictures being drawn of - # a fixed count of bars such that based on the viewbox indices we - # only draw the "rounded up" number of "pictures worth" of bars - # as is necesarry for what's in "view". Not sure if this will - # lead to any perf gains other then when zoomed in to less bars - # in view. - p.setPen(self.last_bar_pen) - p.drawLines(*tuple(filter(bool, self._last_bar_lines))) - profiler('draw last bar') - - p.setPen(self.bars_pen) - p.drawPath(self.path) - profiler('draw history path') - def boundingRect(self): # Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect @@ -424,3 +399,28 @@ class BarItems(pg.GraphicsObject): ) ) + + def paint( + self, + p: QtGui.QPainter, + opt: QtWidgets.QStyleOptionGraphicsItem, + w: QtWidgets.QWidget + ) -> None: + + profiler = pg.debug.Profiler(disabled=not pg_profile_enabled()) + + # p.setCompositionMode(0) + + # TODO: one thing we could try here is pictures being drawn of + # a fixed count of bars such that based on the viewbox indices we + # only draw the "rounded up" number of "pictures worth" of bars + # as is necesarry for what's in "view". Not sure if this will + # lead to any perf gains other then when zoomed in to less bars + # in view. + p.setPen(self.last_bar_pen) + p.drawLines(*tuple(filter(bool, self._last_bar_lines))) + profiler('draw last bar') + + p.setPen(self.bars_pen) + p.drawPath(self.path) + profiler('draw history path') From a2659d1fde6f53a62f9f611888b9c428ccff5724 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 2 Nov 2021 14:04:44 -0400 Subject: [PATCH 10/12] Only update curve lengths on non-negative index diffs --- piker/ui/_curve.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/piker/ui/_curve.py b/piker/ui/_curve.py index 448a2cdc..4a35bee2 100644 --- a/piker/ui/_curve.py +++ b/piker/ui/_curve.py @@ -137,6 +137,9 @@ class FastAppendCurve(pg.PlotCurveItem): # print(f"xrange: {self._xrange}") istart, istop = self._xrange + # compute the length diffs between the first/last index entry in + # the input data and the last indexes we have on record from the + # last time we updated the curve index. prepend_length = istart - x[0] append_length = x[-1] - istop @@ -149,7 +152,7 @@ class FastAppendCurve(pg.PlotCurveItem): # by default we only pull data up to the last (current) index x_out, y_out = x[:-1], y[:-1] - if self.path is None or prepend_length: + if self.path is None or prepend_length > 0: self.path = pg.functions.arrayToQPath( x_out, y_out, @@ -177,7 +180,7 @@ class FastAppendCurve(pg.PlotCurveItem): # # self.path.moveTo(new_x[0], new_y[0]) # self.path.connectPath(old_path) - elif append_length: + elif append_length > 0: if self._step_mode: new_x, new_y = step_path_arrays_from_1d( x[-append_length - 2:-1], From 4f899edcef04f87e219309a1b154b73f7d08d20d Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 7 Dec 2021 15:11:00 -0500 Subject: [PATCH 11/12] Use a `pyqtgraph` dev branch pin (again) There's lotsa movement on the project these days with stuff getting improved, borked, fixed, rinse repeat. Might as well use a pin on our fork so we can more easily hack on it and pull in latest features piece-wise for testing. --- requirements.txt | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/requirements.txt b/requirements.txt index 96f37cab..ca910e01 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,3 +2,8 @@ # are often untested in tractor's CI and/or being tested by us # first before committing as core features in tractor's base. -e git+git://github.com/goodboy/tractor.git@piker_pin#egg=tractor + +# `pyqtgraph` peeps keep breaking, fixing, improving so might as well +# pin this to a dev branch that we have more control over especially +# as more graphics stuff gets hashed out. +-e git+git://github.com/pikers/pyqtgraph.git@piker_pin#egg=pyqtgraph From 18859e1b8ceb03c4af3645333d142dcd5ce722b6 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 7 Dec 2021 16:09:47 -0500 Subject: [PATCH 12/12] Add detailed comments, comment out fill mode --- piker/ui/_curve.py | 60 +++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/piker/ui/_curve.py b/piker/ui/_curve.py index 4a35bee2..161332d5 100644 --- a/piker/ui/_curve.py +++ b/piker/ui/_curve.py @@ -40,7 +40,8 @@ def step_path_arrays_from_1d( include_endpoints: bool = False, ) -> (np.ndarray, np.ndarray): - '''Generate a "step mode" curve aligned with OHLC style bars + ''' + Generate a "step mode" curve aligned with OHLC style bars such that each segment spans each bar (aka "centered" style). ''' @@ -86,6 +87,18 @@ def step_path_arrays_from_1d( # TODO: got a feeling that dropping this inheritance gets us even more speedups class FastAppendCurve(pg.PlotCurveItem): + ''' + A faster, append friendly version of ``pyqtgraph.PlotCurveItem`` + built for real-time data updates. + + The main difference is avoiding regeneration of the entire + historical path where possible and instead only updating the "new" + segment(s) via a ``numpy`` array diff calc. Further the "last" + graphic segment is drawn independently such that near-term (high + frequency) discrete-time-sampled style updates don't trigger a full + path redraw. + + ''' def __init__( self, @@ -115,7 +128,7 @@ class FastAppendCurve(pg.PlotCurveItem): # flat-top style histogram-like discrete curve self._step_mode: bool = step_mode - self._fill = False + # self._fill = True self.setBrush(hcolor(fill_color or color)) # TODO: one question still remaining is if this makes trasform @@ -134,8 +147,8 @@ class FastAppendCurve(pg.PlotCurveItem): profiler = pg.debug.Profiler(disabled=not pg_profile_enabled()) flip_cache = False - # print(f"xrange: {self._xrange}") istart, istop = self._xrange + # print(f"xrange: {self._xrange}") # compute the length diffs between the first/last index entry in # the input data and the last indexes we have on record from the @@ -164,7 +177,8 @@ class FastAppendCurve(pg.PlotCurveItem): # if self._step_mode: # self.path.closeSubpath() - # TODO: get this working - right now it's giving heck on vwap... + # TODO: get this piecewise prepend working - right now it's + # giving heck on vwap... # if prepend_length: # breakpoint() @@ -186,6 +200,10 @@ class FastAppendCurve(pg.PlotCurveItem): x[-append_length - 2:-1], y[-append_length - 2:-1], ) + # [1:] since we don't need the vertical line normally at + # the beginning of the step curve taking the first (x, + # y) poing down to the x-axis **because** this is an + # appended path graphic. new_x = new_x[1:] new_y = new_y[1:] @@ -207,15 +225,20 @@ class FastAppendCurve(pg.PlotCurveItem): # other merging ideas: # https://stackoverflow.com/questions/8936225/how-to-merge-qpainterpaths if self._step_mode: - if self._fill: - # XXX: super slow set "union" op - self.path = self.path.united(append_path).simplified() + # path.addPath(append_path) + self.path.connectPath(append_path) + + # TODO: try out new work from `pyqtgraph` main which + # should repair horrid perf: + # https://github.com/pyqtgraph/pyqtgraph/pull/2032 + # ok, nope still horrible XD + # if self._fill: + # # XXX: super slow set "union" op + # self.path = self.path.united(append_path).simplified() + + # # path.addPath(append_path) + # # path.closeSubpath() - # path.addPath(append_path) - # path.closeSubpath() - else: - # path.addPath(append_path) - self.path.connectPath(append_path) else: # print(f"append_path br: {append_path.boundingRect()}") # self.path.moveTo(new_x[0], new_y[0]) @@ -238,6 +261,8 @@ class FastAppendCurve(pg.PlotCurveItem): x0, x_last = self._xrange = x[0], x[-1] y_last = y[-1] + # draw the "current" step graphic segment so it lines up with + # the "middle" of the current (OHLC) sample. if self._step_mode: self._last_line = QLineF( x_last - 0.5, 0, @@ -259,6 +284,7 @@ class FastAppendCurve(pg.PlotCurveItem): self.update() if flip_cache: + # XXX: seems to be needed to avoid artifacts (see above). self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) def boundingRect(self): @@ -310,7 +336,6 @@ class FastAppendCurve(pg.PlotCurveItem): p.fillRect(self._last_step_rect, brush) # p.drawPath(self.path) - # profiler('.drawPath()') # else: @@ -322,6 +347,9 @@ class FastAppendCurve(pg.PlotCurveItem): p.drawPath(self.path) profiler('.drawPath()') - if self._fill: - print('FILLED') - p.fillPath(self.path, brush) + # TODO: try out new work from `pyqtgraph` main which + # should repair horrid perf: + # https://github.com/pyqtgraph/pyqtgraph/pull/2032 + # if self._fill: + # brush = self.opts['brush'] + # p.fillPath(self.path, brush)