commit
c808965a6f
|
@ -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)
|
||||||
|
|
|
@ -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')
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue