commit
						c808965a6f
					
				| 
						 | 
				
			
			@ -18,25 +18,118 @@
 | 
			
		|||
Fast, smooth, sexy curves.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
from typing import Tuple
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
    include_endpoints: bool = False,
 | 
			
		||||
 | 
			
		||||
) -> (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
 | 
			
		||||
    y2 = np.empty((len(y), 2), dtype=y.dtype)
 | 
			
		||||
    y2[:] = y[:, np.newaxis]
 | 
			
		||||
 | 
			
		||||
    y_out = np.empty(
 | 
			
		||||
        2*len(y) + 2,
 | 
			
		||||
        dtype=y.dtype
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # flatten and set 0 endpoints
 | 
			
		||||
    y_out[1:-1] = y2.reshape(y2.size)
 | 
			
		||||
    y_out[0] = 0
 | 
			
		||||
    y_out[-1] = 0
 | 
			
		||||
 | 
			
		||||
    if not include_endpoints:
 | 
			
		||||
        return x_out[:-1], y_out[:-1]
 | 
			
		||||
 | 
			
		||||
    else:
 | 
			
		||||
        return x_out, y_out
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# 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.
 | 
			
		||||
 | 
			
		||||
    def __init__(self, *args, **kwargs):
 | 
			
		||||
    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,
 | 
			
		||||
        *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
 | 
			
		||||
        # we're basically only using the pen setting now...
 | 
			
		||||
        super().__init__(*args, **kwargs)
 | 
			
		||||
 | 
			
		||||
        self._last_line: QtCore.QLineF = None
 | 
			
		||||
        self._xrange: Tuple[int, int] = self.dataBounds(ax=0)
 | 
			
		||||
        self._xrange: tuple[int, int] = self.dataBounds(ax=0)
 | 
			
		||||
 | 
			
		||||
        # 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 = True
 | 
			
		||||
        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
 | 
			
		||||
| 
						 | 
				
			
			@ -46,28 +139,46 @@ 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())
 | 
			
		||||
        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
 | 
			
		||||
        # last time we updated the curve index.
 | 
			
		||||
        prepend_length = istart - x[0]
 | 
			
		||||
        append_length = x[-1] - istop
 | 
			
		||||
 | 
			
		||||
        if self.path is None or prepend_length:
 | 
			
		||||
        # step mode: draw flat top discrete "step"
 | 
			
		||||
        # over the index space for each datum.
 | 
			
		||||
        if self._step_mode:
 | 
			
		||||
            x_out, y_out = step_path_arrays_from_1d(x[:-1], y[:-1])
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            # 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 > 0:
 | 
			
		||||
            self.path = pg.functions.arrayToQPath(
 | 
			
		||||
                x[:-1],
 | 
			
		||||
                y[:-1],
 | 
			
		||||
                connect='all'
 | 
			
		||||
                x_out,
 | 
			
		||||
                y_out,
 | 
			
		||||
                connect='all',
 | 
			
		||||
                finiteCheck=False,
 | 
			
		||||
            )
 | 
			
		||||
            profiler('generate fresh path')
 | 
			
		||||
 | 
			
		||||
        # TODO: get this working - right now it's giving heck on vwap...
 | 
			
		||||
            # if self._step_mode:
 | 
			
		||||
            #     self.path.closeSubpath()
 | 
			
		||||
 | 
			
		||||
        # TODO: get this piecewise prepend working - right now it's
 | 
			
		||||
        # giving heck on vwap...
 | 
			
		||||
        # if prepend_length:
 | 
			
		||||
        #     breakpoint()
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -83,21 +194,56 @@ class FastAppendCurve(pg.PlotCurveItem):
 | 
			
		|||
        #     # self.path.moveTo(new_x[0], new_y[0])
 | 
			
		||||
        #     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))
 | 
			
		||||
        elif append_length > 0:
 | 
			
		||||
            if self._step_mode:
 | 
			
		||||
                new_x, new_y = step_path_arrays_from_1d(
 | 
			
		||||
                    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:]
 | 
			
		||||
 | 
			
		||||
            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)
 | 
			
		||||
 | 
			
		||||
            path = self.path
 | 
			
		||||
 | 
			
		||||
            # other merging ideas:
 | 
			
		||||
            # https://stackoverflow.com/questions/8936225/how-to-merge-qpainterpaths
 | 
			
		||||
            if self._step_mode:
 | 
			
		||||
                # 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()
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                # print(f"append_path br: {append_path.boundingRect()}")
 | 
			
		||||
                # self.path.moveTo(new_x[0], new_y[0])
 | 
			
		||||
                # self.path.connectPath(append_path)
 | 
			
		||||
                path.connectPath(append_path)
 | 
			
		||||
 | 
			
		||||
            # XXX: pretty annoying but, without this there's little
 | 
			
		||||
            # artefacts on the append updates to the curve...
 | 
			
		||||
| 
						 | 
				
			
			@ -112,8 +258,25 @@ class FastAppendCurve(pg.PlotCurveItem):
 | 
			
		|||
        self.xData = x
 | 
			
		||||
        self.yData = y
 | 
			
		||||
 | 
			
		||||
        self._xrange = x[0], x[-1]
 | 
			
		||||
        self._last_line = QtCore.QLineF(x[-2], y[-2], x[-1], y[-1])
 | 
			
		||||
        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,
 | 
			
		||||
                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_last
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # trigger redraw of path
 | 
			
		||||
        # do update before reverting to cache mode
 | 
			
		||||
| 
						 | 
				
			
			@ -121,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):
 | 
			
		||||
| 
						 | 
				
			
			@ -143,13 +307,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
 | 
			
		||||
| 
						 | 
				
			
			@ -164,9 +328,28 @@ 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']
 | 
			
		||||
            # 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)
 | 
			
		||||
            # profiler('.drawPath()')
 | 
			
		||||
 | 
			
		||||
        # else:
 | 
			
		||||
        p.setPen(self.last_step_pen)
 | 
			
		||||
        p.drawLine(self._last_line)
 | 
			
		||||
        profiler('.drawLine()')
 | 
			
		||||
 | 
			
		||||
        p.setPen(self.opts['pen'])
 | 
			
		||||
        p.drawPath(self.path)
 | 
			
		||||
        profiler('.drawPath()')
 | 
			
		||||
 | 
			
		||||
        # 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)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
        # 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.
 | 
			
		||||
| 
						 | 
				
			
			@ -354,30 +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)
 | 
			
		||||
        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
 | 
			
		||||
        # 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.drawLines(*tuple(filter(bool, self._last_bar_lines)))
 | 
			
		||||
        profiler('draw last bar')
 | 
			
		||||
 | 
			
		||||
        p.drawPath(self.path)
 | 
			
		||||
        profiler('draw history path')
 | 
			
		||||
 | 
			
		||||
    def boundingRect(self):
 | 
			
		||||
        # Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -421,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')
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue