Merge pull request #240 from pikers/fast_step_curve

Fast step curve
simpler_quote_throttle_logic
goodboy 2021-12-07 16:12:47 -05:00 committed by GitHub
commit c808965a6f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 247 additions and 56 deletions

View File

@ -18,25 +18,118 @@
Fast, smooth, sexy curves. Fast, smooth, sexy curves.
""" """
from typing import Tuple from typing import Optional
import numpy as np
import pyqtgraph as pg 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 .._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 # TODO: got a feeling that dropping this inheritance gets us even more speedups
class FastAppendCurve(pg.PlotCurveItem): 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 # TODO: we can probably just dispense with the parent since
# we're basically only using the pen setting now... # we're basically only using the pen setting now...
super().__init__(*args, **kwargs) 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 # TODO: one question still remaining is if this makes trasform
# interactions slower (such as zooming) and if so maybe if/when # interactions slower (such as zooming) and if so maybe if/when
@ -46,28 +139,46 @@ class FastAppendCurve(pg.PlotCurveItem):
def update_from_array( def update_from_array(
self, self,
x, x: np.ndarray,
y, y: np.ndarray,
) -> QtGui.QPainterPath: ) -> QtGui.QPainterPath:
profiler = pg.debug.Profiler(disabled=not pg_profile_enabled()) profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
flip_cache = False flip_cache = False
# print(f"xrange: {self._xrange}")
istart, istop = 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] prepend_length = istart - x[0]
append_length = x[-1] - istop 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( self.path = pg.functions.arrayToQPath(
x[:-1], x_out,
y[:-1], y_out,
connect='all' connect='all',
finiteCheck=False,
) )
profiler('generate fresh path') 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: # if prepend_length:
# breakpoint() # breakpoint()
@ -83,21 +194,56 @@ class FastAppendCurve(pg.PlotCurveItem):
# # self.path.moveTo(new_x[0], new_y[0]) # # self.path.moveTo(new_x[0], new_y[0])
# self.path.connectPath(old_path) # self.path.connectPath(old_path)
if append_length: elif append_length > 0:
# print(f"append_length: {append_length}") if self._step_mode:
new_x = x[-append_length - 2:-1] new_x, new_y = step_path_arrays_from_1d(
new_y = y[-append_length - 2:-1] x[-append_length - 2:-1],
# print((new_x, new_y)) 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( append_path = pg.functions.arrayToQPath(
new_x, new_x,
new_y, 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]) path = self.path
# self.path.connectPath(append_path)
self.path.connectPath(append_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 # XXX: pretty annoying but, without this there's little
# artefacts on the append updates to the curve... # artefacts on the append updates to the curve...
@ -112,8 +258,25 @@ class FastAppendCurve(pg.PlotCurveItem):
self.xData = x self.xData = x
self.yData = y self.yData = y
self._xrange = x[0], x[-1] x0, x_last = self._xrange = x[0], x[-1]
self._last_line = QtCore.QLineF(x[-2], y[-2], x[-1], y[-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 # trigger redraw of path
# do update before reverting to cache mode # do update before reverting to cache mode
@ -121,6 +284,7 @@ class FastAppendCurve(pg.PlotCurveItem):
self.update() self.update()
if flip_cache: if flip_cache:
# XXX: seems to be needed to avoid artifacts (see above).
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
def boundingRect(self): def boundingRect(self):
@ -143,13 +307,13 @@ class FastAppendCurve(pg.PlotCurveItem):
w = hb_size.width() + 1 w = hb_size.width() + 1
h = hb_size.height() + 1 h = hb_size.height() + 1
br = QtCore.QRectF( br = QRectF(
# top left # top left
QtCore.QPointF(hb.topLeft()), QPointF(hb.topLeft()),
# total size # total size
QtCore.QSizeF(w, h) QSizeF(w, h)
) )
# print(f'bounding rect: {br}') # print(f'bounding rect: {br}')
return br return br
@ -164,9 +328,28 @@ class FastAppendCurve(pg.PlotCurveItem):
profiler = pg.debug.Profiler(disabled=not pg_profile_enabled()) profiler = pg.debug.Profiler(disabled=not pg_profile_enabled())
# p.setRenderHint(p.Antialiasing, True) # 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) p.drawLine(self._last_line)
profiler('.drawLine()') profiler('.drawLine()')
p.setPen(self.opts['pen'])
p.drawPath(self.path) p.drawPath(self.path)
profiler('.drawPath()') 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)

View File

@ -146,7 +146,7 @@ def path_arrays_from_ohlc(
# specifies that the first edge is never connected to the # specifies that the first edge is never connected to the
# prior bars last edge thus providing a small "gap"/"space" # prior bars last edge thus providing a small "gap"/"space"
# between bars determined by ``bar_gap``. # 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 return x, y, c
@ -182,12 +182,14 @@ class BarItems(pg.GraphicsObject):
# scene: 'QGraphicsScene', # noqa # scene: 'QGraphicsScene', # noqa
plotitem: 'pg.PlotItem', # noqa plotitem: 'pg.PlotItem', # noqa
pen_color: str = 'bracket', pen_color: str = 'bracket',
last_bar_color: str = 'bracket',
) -> None: ) -> None:
super().__init__() super().__init__()
# XXX: for the mega-lulz increasing width here increases draw latency... # XXX: for the mega-lulz increasing width here increases draw
# so probably don't do it until we figure that out. # latency... so probably don't do it until we figure that out.
self.bars_pen = pg.mkPen(hcolor(pen_color), width=1) 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 # NOTE: this prevents redraws on mouse interaction which is
# a huge boon for avg interaction latency. # a huge boon for avg interaction latency.
@ -354,30 +356,6 @@ class BarItems(pg.GraphicsObject):
if flip_cache: if flip_cache:
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) 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): def boundingRect(self):
# Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect # 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')

View File

@ -2,3 +2,8 @@
# are often untested in tractor's CI and/or being tested by us # are often untested in tractor's CI and/or being tested by us
# first before committing as core features in tractor's base. # first before committing as core features in tractor's base.
-e git+git://github.com/goodboy/tractor.git@piker_pin#egg=tractor -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