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()')