Make curve graphics timeframe agnostic

Ensure `.boundingRect()` calcs and `.draw_last_datum()` do geo-sizing
based on source data instead of presuming some `1.0` unit steps in some
spots; we need this to support an epoch index as is needed for overlays.

Further, clean out a bunch of old bounding rect calc code and add some
commented code for trying out `QRectF.united()` on the path + last datum
curve segment. Turns out that approach is slower as per eyeballing the
added profiler points.
epoch_index
Tyler Goodlet 2022-11-23 13:58:01 -05:00
parent ab1f15506d
commit 9d16299f60
2 changed files with 148 additions and 110 deletions

View File

@ -28,10 +28,7 @@ from PyQt5.QtWidgets import QGraphicsItem
from PyQt5.QtCore import ( from PyQt5.QtCore import (
Qt, Qt,
QLineF, QLineF,
QSizeF,
QRectF, QRectF,
# QRect,
QPointF,
) )
from PyQt5.QtGui import ( from PyQt5.QtGui import (
QPainter, QPainter,
@ -89,9 +86,9 @@ class Curve(pg.GraphicsObject):
''' '''
# sub-type customization methods # sub-type customization methods
sub_br: Optional[Callable] = None
sub_paint: Optional[Callable] = None
declare_paintables: Optional[Callable] = None declare_paintables: Optional[Callable] = None
sub_paint: Optional[Callable] = None
# sub_br: Optional[Callable] = None
def __init__( def __init__(
self, self,
@ -140,9 +137,7 @@ class Curve(pg.GraphicsObject):
# self.last_step_pen = pg.mkPen(hcolor(color), width=2) # self.last_step_pen = pg.mkPen(hcolor(color), width=2)
self.last_step_pen = pg.mkPen(pen, width=2) self.last_step_pen = pg.mkPen(pen, width=2)
# self._last_line: Optional[QLineF] = None
self._last_line = QLineF() self._last_line = QLineF()
self._last_w: float = 1
# flat-top style histogram-like discrete curve # flat-top style histogram-like discrete curve
# self._step_mode: bool = step_mode # self._step_mode: bool = step_mode
@ -231,8 +226,8 @@ class Curve(pg.GraphicsObject):
self.path.clear() self.path.clear()
if self.fast_path: if self.fast_path:
# self.fast_path.clear() self.fast_path.clear()
self.fast_path = None # self.fast_path = None
@cm @cm
def reset_cache(self) -> None: def reset_cache(self) -> None:
@ -252,77 +247,81 @@ class Curve(pg.GraphicsObject):
self.boundingRect = self._path_br self.boundingRect = self._path_br
return self._path_br() return self._path_br()
# Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect
def _path_br(self): def _path_br(self):
''' '''
Post init ``.boundingRect()```. Post init ``.boundingRect()```.
''' '''
# hb = self.path.boundingRect() profiler = Profiler(
hb = self.path.controlPointRect() msg=f'Curve.boundingRect(): `{self._name}`',
hb_size = hb.size() disabled=not pg_profile_enabled(),
ms_threshold=ms_slower_then,
)
pr = self.path.controlPointRect()
hb_tl, hb_br = (
pr.topLeft(),
pr.bottomRight(),
)
mn_y = hb_tl.y()
mx_y = hb_br.y()
most_left = hb_tl.x()
most_right = hb_br.x()
profiler('calc path vertices')
fp = self.fast_path # TODO: if/when we get fast path appends working in the
if fp: # `Renderer`, then we might need to actually use this..
fhb = fp.controlPointRect() # fp = self.fast_path
hb_size = fhb.size() + hb_size # if fp:
# fhb = fp.controlPointRect()
# # hb_size = fhb.size() + hb_size
# br = pr.united(fhb)
# print(f'hb_size: {hb_size}') # XXX: *was* a way to allow sub-types to extend the
# boundingrect calc, but in the one use case for a step curve
# doesn't seem like we need it as long as the last line segment
# is drawn as it is?
# if self._last_step_rect: # sbr = self.sub_br
# hb_size += self._last_step_rect.size() # if sbr:
# # w, h = self.sub_br(w, h)
# sub_br = sbr()
# br = br.united(sub_br)
# if self._line:
# br = self._last_step_rect.bottomRight()
# tl = QPointF(
# # self._vr[0],
# # hb.topLeft().y(),
# # 0,
# # hb_size.height() + 1
# )
# br = self._last_step_rect.bottomRight()
w = hb_size.width()
h = hb_size.height()
sbr = self.sub_br
if sbr:
w, h = self.sub_br(w, h)
else:
# assume plain line graphic and use # assume plain line graphic and use
# default unit step in each direction. # default unit step in each direction.
ll = self._last_line
y1, y2 = ll.y1(), ll.y2()
x1, x2 = ll.x1(), ll.x2()
# only on a plane line do we include ymn = min(y1, y2, mn_y)
# and extra index step's worth of width ymx = max(y1, y2, mx_y)
# since in the step case the end of the curve most_left = min(x1, x2, most_left)
# actually terminates earlier so we don't need most_right = max(x1, x2, most_right)
# this for the last step.
w += self._last_w
# ll = self._last_line
h += 1 # ll.y2() - ll.y1()
# br = QPointF( profiler('calc last line vertices')
# self._vr[-1], # ll_br = QRectF(
# # tl.x() + w, # x1,
# tl.y() + h, # ymn,
# # NOTE: a legacy snippet, not sure if it still applies?
# # only on a plane line do we include
# # and extra index step's worth of width
# # since in the step case the end of the curve
# # actually terminates earlier so we don't need
# # this for the last step.
# x2 - x1 + 1,
# ymx,
# ) # )
# br = br.united(ll_br)
# profiler('calc united rects')
# return br
br = QRectF( return QRectF(
most_left,
# top left ymn,
# hb.topLeft() most_right - most_left + 1,
# tl, ymx,
QPointF(hb.topLeft()),
# br,
# total size
# QSizeF(hb_size)
# hb_size,
QSizeF(w, h)
) )
# print(f'bounding rect: {br}')
return br
def paint( def paint(
self, self,
@ -359,6 +358,7 @@ class Curve(pg.GraphicsObject):
fp = self.fast_path fp = self.fast_path
if fp: if fp:
# print("DRAWING PATH")
p.drawPath(fp) p.drawPath(fp)
profiler('.drawPath(fast_path)') profiler('.drawPath(fast_path)')
@ -450,17 +450,20 @@ class StepCurve(Curve):
y = src_data[array_key] y = src_data[array_key]
x_last = x[-1] x_last = x[-1]
x_2last = x[-2]
y_last = y[-1] y_last = y[-1]
step_size = x_last - x_2last
half_step = step_size / 2
# lol, commenting this makes step curves # lol, commenting this makes step curves
# all "black" for me :eyeroll:.. # all "black" for me :eyeroll:..
self._last_line = QLineF( self._last_line = QLineF(
x_last - w, 0, x_2last, 0,
x_last + w, 0, x_last, 0,
) )
self._last_step_rect = QRectF( self._last_step_rect = QRectF(
x_last - w, 0, x_last - half_step, 0,
x_last + w, y_last, step_size, y_last,
) )
return x, y return x, y
@ -475,11 +478,8 @@ class StepCurve(Curve):
p.fillRect(self._last_step_rect, self._brush) p.fillRect(self._last_step_rect, self._brush)
profiler('.fillRect()') profiler('.fillRect()')
def sub_br( # def sub_br(
self, # self,
path_w: float, # parent_br: QRectF | None = None,
path_h: float, # ) -> QRectF:
# return self._last_step_rect
) -> (float, float):
# passthrough
return path_w, path_h

View File

@ -25,8 +25,15 @@ from typing import (
import numpy as np import numpy as np
import pyqtgraph as pg import pyqtgraph as pg
from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5 import (
from PyQt5.QtCore import QLineF, QPointF QtGui,
QtWidgets,
)
from PyQt5.QtCore import (
QLineF,
QRectF,
)
from PyQt5.QtGui import QPainterPath from PyQt5.QtGui import QPainterPath
from .._profile import pg_profile_enabled, ms_slower_then from .._profile import pg_profile_enabled, ms_slower_then
@ -114,8 +121,13 @@ class BarItems(pg.GraphicsObject):
# we expect the downsample curve report this. # we expect the downsample curve report this.
return 0 return 0
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
def boundingRect(self):
profiler = Profiler(
msg=f'BarItems.boundingRect(): `{self._name}`',
disabled=not pg_profile_enabled(),
ms_threshold=ms_slower_then,
)
# TODO: Can we do rect caching to make this faster # TODO: Can we do rect caching to make this faster
# like `pg.PlotCurveItem` does? In theory it's just # like `pg.PlotCurveItem` does? In theory it's just
@ -135,32 +147,53 @@ class BarItems(pg.GraphicsObject):
hb.topLeft(), hb.topLeft(),
hb.bottomRight(), hb.bottomRight(),
) )
mn_y = hb_tl.y()
mx_y = hb_br.y()
most_left = hb_tl.x()
most_right = hb_br.x()
profiler('calc path vertices')
# need to include last bar height or BR will be off # need to include last bar height or BR will be off
mx_y = hb_br.y() # OHLC line segments: [hl, o, c]
mn_y = hb_tl.y() last_lines: tuple[QLineF] | None = self._last_bar_lines
last_lines = self._last_bar_lines
if last_lines: if last_lines:
body_line = self._last_bar_lines[0] (
if body_line: hl,
mx_y = max(mx_y, max(body_line.y1(), body_line.y2())) o,
mn_y = min(mn_y, min(body_line.y1(), body_line.y2())) c,
) = last_lines
most_right = c.x2() + 1
ymx = ymn = c.y2()
return QtCore.QRectF( if hl:
y1, y2 = hl.y1(), hl.y2()
ymn = min(y1, y2)
ymx = max(y1, y2)
mx_y = max(ymx, mx_y)
mn_y = min(ymn, mn_y)
# top left profiler('calc last bar vertices')
QPointF( # TODO: see if this br uniting works faster?
hb_tl.x(), # last_bar_rect = QRectF(
# o.x1(),
# ymn,
# c.x2() - o.x1() + 1,
# ymx,
# )
# tot_br = hb.united(last_bar_rect)
# print(
# f'last datum bar br: {last_bar_rect}\n'
# f'path br: {hb}\n'
# f'sum br: {tot_br}\n'
# )
# profiler('calc united rects')
# return tot_br
return QRectF(
most_left,
mn_y, mn_y,
), most_right - most_left + 1,
mx_y - mn_y,
# bottom right
QPointF(
hb_br.x() + 1,
mx_y,
)
) )
def paint( def paint(
@ -213,11 +246,15 @@ class BarItems(pg.GraphicsObject):
# relevant fields # relevant fields
ohlc = src_data[fields] ohlc = src_data[fields]
last_row = ohlc[-1:] # last_row = ohlc[-1:]
# individual values # individual values
last_row = i, o, h, l, last = ohlc[-1] last_row = i, o, h, l, last = ohlc[-1]
# times = src_data['time']
# if times[-1] - times[-2]:
# breakpoint()
# generate new lines objects for updatable "current bar" # generate new lines objects for updatable "current bar"
self._last_bar_lines = bar_from_ohlc_row(last_row) self._last_bar_lines = bar_from_ohlc_row(last_row)
@ -248,4 +285,5 @@ class BarItems(pg.GraphicsObject):
# date / from some previous sample. It's weird though # date / from some previous sample. It's weird though
# because i've seen it do this to bars i - 3 back? # because i've seen it do this to bars i - 3 back?
# return ohlc['time'], ohlc['close']
return ohlc['index'], ohlc['close'] return ohlc['index'], ohlc['close']