Add `Curve` sub-types with new custom graphics API
Instead of using a bunch of internal logic to modify low level paint-able elements create a `Curve` lineage that allows for graphics "style" customization via a small set of public methods: - `Curve.declare_paintables()` to allow setup of state/elements to be drawn in later methods. - `.sub_paint()` to allow painting additional elements along with the defaults. - `.sub_br()` to customize the `.boundingRect()` dimensions. - `.draw_last_datum()` which is expected to produce the paintable elements which will show the last datum in view. Introduce the new sub-types and load as necessary in `ChartPlotWidget.draw_curve()`: - `FlattenedOHLC` - `StepCurve` Reimplement all `.draw_last()` routines as a `Curve` method and call it the same way from `Flow.update_graphics()`incremental_update_paths
parent
55772efb34
commit
a66934a49d
|
@ -50,7 +50,10 @@ from ._cursor import (
|
|||
from ..data._sharedmem import ShmArray
|
||||
from ._l1 import L1Labels
|
||||
from ._ohlc import BarItems
|
||||
from ._curve import Curve
|
||||
from ._curve import (
|
||||
Curve,
|
||||
StepCurve,
|
||||
)
|
||||
from ._style import (
|
||||
hcolor,
|
||||
CHART_MARGINS,
|
||||
|
@ -1051,6 +1054,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
color: Optional[str] = None,
|
||||
add_label: bool = True,
|
||||
pi: Optional[pg.PlotItem] = None,
|
||||
step_mode: bool = False,
|
||||
|
||||
**pdi_kwargs,
|
||||
|
||||
|
@ -1067,29 +1071,18 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
data_key = array_key or name
|
||||
|
||||
# yah, we wrote our own B)
|
||||
data = shm.array
|
||||
curve = Curve(
|
||||
# antialias=True,
|
||||
curve_type = {
|
||||
None: Curve,
|
||||
'step': StepCurve,
|
||||
# TODO:
|
||||
# 'bars': BarsItems
|
||||
}['step' if step_mode else None]
|
||||
|
||||
curve = curve_type(
|
||||
name=name,
|
||||
|
||||
# XXX: pretty sure this is just more overhead
|
||||
# on data reads and makes graphics rendering no faster
|
||||
# clipToView=True,
|
||||
|
||||
**pdi_kwargs,
|
||||
)
|
||||
|
||||
# XXX: see explanation for different caching modes:
|
||||
# https://stackoverflow.com/a/39410081
|
||||
# seems to only be useful if we don't re-generate the entire
|
||||
# QPainterPath every time
|
||||
# curve.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
||||
|
||||
# don't ever use this - it's a colossal nightmare of artefacts
|
||||
# and is disastrous for performance.
|
||||
# curve.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache)
|
||||
|
||||
pi = pi or self.plotItem
|
||||
|
||||
self._flows[data_key] = Flow(
|
||||
|
|
|
@ -19,20 +19,24 @@ Fast, smooth, sexy curves.
|
|||
|
||||
"""
|
||||
from contextlib import contextmanager as cm
|
||||
from typing import Optional
|
||||
from typing import Optional, Callable
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
from PyQt5 import QtWidgets
|
||||
from PyQt5.QtWidgets import QGraphicsItem
|
||||
from PyQt5.QtCore import (
|
||||
Qt,
|
||||
QLineF,
|
||||
QSizeF,
|
||||
QRectF,
|
||||
# QRect,
|
||||
QPointF,
|
||||
)
|
||||
|
||||
from PyQt5.QtGui import (
|
||||
QPainter,
|
||||
QPainterPath,
|
||||
)
|
||||
from .._profile import pg_profile_enabled, ms_slower_then
|
||||
from ._style import hcolor
|
||||
# from ._compression import (
|
||||
|
@ -59,10 +63,12 @@ class Curve(pg.GraphicsObject):
|
|||
``pyqtgraph.PlotCurveItem`` built for highly customizable real-time
|
||||
updates.
|
||||
|
||||
This type is a much stripped down version of a ``pyqtgraph`` style "graphics object" in
|
||||
the sense that the internal lower level graphics which are drawn in the ``.paint()`` method
|
||||
are actually rendered outside of this class entirely and instead are assigned as state
|
||||
(instance vars) here and then drawn during a Qt graphics cycle.
|
||||
This type is a much stripped down version of a ``pyqtgraph`` style
|
||||
"graphics object" in the sense that the internal lower level
|
||||
graphics which are drawn in the ``.paint()`` method are actually
|
||||
rendered outside of this class entirely and instead are assigned as
|
||||
state (instance vars) here and then drawn during a Qt graphics
|
||||
cycle.
|
||||
|
||||
The main motivation for this more modular, composed design is that
|
||||
lower level graphics data can be rendered in different threads and
|
||||
|
@ -72,13 +78,20 @@ class Curve(pg.GraphicsObject):
|
|||
level path generation and incremental update. The main differences in
|
||||
the path generation code include:
|
||||
|
||||
- avoiding regeneration of the entire historical path where possible and instead
|
||||
only updating the "new" segment(s) via a ``numpy`` array diff calc.
|
||||
- avoiding regeneration of the entire historical path where possible
|
||||
and instead only updating the "new" segment(s) via a ``numpy``
|
||||
array diff calc.
|
||||
- here, the "last" graphics datum-segment is drawn independently
|
||||
such that near-term (high frequency) discrete-time-sampled style
|
||||
updates don't trigger a full path redraw.
|
||||
|
||||
'''
|
||||
|
||||
# sub-type customization methods
|
||||
sub_br: Optional[Callable] = None
|
||||
sub_paint: Optional[Callable] = None
|
||||
declare_paintables: Optional[Callable] = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
|
@ -94,19 +107,20 @@ class Curve(pg.GraphicsObject):
|
|||
|
||||
) -> None:
|
||||
|
||||
self._name = name
|
||||
|
||||
# brutaaalll, see comments within..
|
||||
self.yData = None
|
||||
self.xData = None
|
||||
self._last_cap: int = 0
|
||||
|
||||
self._name = name
|
||||
self.path: Optional[QtGui.QPainterPath] = None
|
||||
# self._last_cap: int = 0
|
||||
self.path: Optional[QPainterPath] = None
|
||||
|
||||
# additional path used for appends which tries to avoid
|
||||
# triggering an update/redraw of the presumably larger
|
||||
# historical ``.path`` above.
|
||||
self.use_fpath = use_fpath
|
||||
self.fast_path: Optional[QtGui.QPainterPath] = None
|
||||
self.fast_path: Optional[QPainterPath] = None
|
||||
|
||||
# TODO: we can probably just dispense with the parent since
|
||||
# we're basically only using the pen setting now...
|
||||
|
@ -125,12 +139,12 @@ class Curve(pg.GraphicsObject):
|
|||
# self.last_step_pen = pg.mkPen(hcolor(color), width=2)
|
||||
self.last_step_pen = pg.mkPen(pen, width=2)
|
||||
|
||||
self._last_line: Optional[QLineF] = None
|
||||
self._last_step_rect: Optional[QRectF] = None
|
||||
# self._last_line: Optional[QLineF] = None
|
||||
self._last_line = QLineF()
|
||||
self._last_w: float = 1
|
||||
|
||||
# flat-top style histogram-like discrete curve
|
||||
self._step_mode: bool = step_mode
|
||||
# self._step_mode: bool = step_mode
|
||||
|
||||
# self._fill = True
|
||||
self._brush = pg.functions.mkBrush(hcolor(fill_color or color))
|
||||
|
@ -148,6 +162,21 @@ class Curve(pg.GraphicsObject):
|
|||
# endpoint (something we saw on trade rate curves)
|
||||
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
|
||||
|
||||
# XXX: see explanation for different caching modes:
|
||||
# https://stackoverflow.com/a/39410081
|
||||
# seems to only be useful if we don't re-generate the entire
|
||||
# QPainterPath every time
|
||||
# curve.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
||||
|
||||
# don't ever use this - it's a colossal nightmare of artefacts
|
||||
# and is disastrous for performance.
|
||||
# curve.setCacheMode(QtWidgets.QGraphicsItem.ItemCoordinateCache)
|
||||
|
||||
# allow sub-type customization
|
||||
declare = self.declare_paintables
|
||||
if declare:
|
||||
declare()
|
||||
|
||||
# TODO: probably stick this in a new parent
|
||||
# type which will contain our own version of
|
||||
# what ``PlotCurveItem`` had in terms of base
|
||||
|
@ -215,7 +244,7 @@ class Curve(pg.GraphicsObject):
|
|||
Compute and then cache our rect.
|
||||
'''
|
||||
if self.path is None:
|
||||
return QtGui.QPainterPath().boundingRect()
|
||||
return QPainterPath().boundingRect()
|
||||
else:
|
||||
# dynamically override this method after initial
|
||||
# path is created to avoid requiring the above None check
|
||||
|
@ -227,14 +256,15 @@ class Curve(pg.GraphicsObject):
|
|||
Post init ``.boundingRect()```.
|
||||
|
||||
'''
|
||||
hb = self.path.controlPointRect()
|
||||
# hb = self.path.boundingRect()
|
||||
hb = self.path.controlPointRect()
|
||||
hb_size = hb.size()
|
||||
|
||||
fp = self.fast_path
|
||||
if fp:
|
||||
fhb = fp.controlPointRect()
|
||||
hb_size = fhb.size() + hb_size
|
||||
|
||||
# print(f'hb_size: {hb_size}')
|
||||
|
||||
# if self._last_step_rect:
|
||||
|
@ -255,7 +285,13 @@ class Curve(pg.GraphicsObject):
|
|||
w = hb_size.width()
|
||||
h = hb_size.height()
|
||||
|
||||
if not self._last_step_rect:
|
||||
sbr = self.sub_br
|
||||
if sbr:
|
||||
w, h = self.sub_br(w, h)
|
||||
else:
|
||||
# assume plain line graphic and use
|
||||
# default unit step in each direction.
|
||||
|
||||
# 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
|
||||
|
@ -289,7 +325,7 @@ class Curve(pg.GraphicsObject):
|
|||
|
||||
def paint(
|
||||
self,
|
||||
p: QtGui.QPainter,
|
||||
p: QPainter,
|
||||
opt: QtWidgets.QStyleOptionGraphicsItem,
|
||||
w: QtWidgets.QWidget
|
||||
|
||||
|
@ -301,25 +337,16 @@ class Curve(pg.GraphicsObject):
|
|||
ms_threshold=ms_slower_then,
|
||||
)
|
||||
|
||||
if (
|
||||
self._step_mode
|
||||
and self._last_step_rect
|
||||
):
|
||||
brush = self._brush
|
||||
sub_paint = self.sub_paint
|
||||
if sub_paint:
|
||||
sub_paint(p, profiler)
|
||||
|
||||
# p.drawLines(*tuple(filter(bool, self._last_step_lines)))
|
||||
# p.drawRect(self._last_step_rect)
|
||||
p.fillRect(self._last_step_rect, brush)
|
||||
profiler('.fillRect()')
|
||||
|
||||
if self._last_line:
|
||||
p.setPen(self.last_step_pen)
|
||||
p.drawLine(self._last_line)
|
||||
profiler('.drawLine()')
|
||||
p.setPen(self._pen)
|
||||
p.setPen(self.last_step_pen)
|
||||
p.drawLine(self._last_line)
|
||||
profiler('.drawLine()')
|
||||
p.setPen(self._pen)
|
||||
|
||||
path = self.path
|
||||
|
||||
# cap = path.capacity()
|
||||
# if cap != self._last_cap:
|
||||
# print(f'NEW CAPACITY: {self._last_cap} -> {cap}')
|
||||
|
@ -341,3 +368,116 @@ class Curve(pg.GraphicsObject):
|
|||
# if self._fill:
|
||||
# brush = self.opts['brush']
|
||||
# p.fillPath(self.path, brush)
|
||||
|
||||
def draw_last_datum(
|
||||
self,
|
||||
path: QPainterPath,
|
||||
src_data: np.ndarray,
|
||||
render_data: np.ndarray,
|
||||
reset: bool,
|
||||
array_key: str,
|
||||
|
||||
) -> None:
|
||||
# default line draw last call
|
||||
with self.reset_cache():
|
||||
x = render_data['index']
|
||||
y = render_data[array_key]
|
||||
|
||||
x_last = x[-1]
|
||||
y_last = y[-1]
|
||||
|
||||
# draw the "current" step graphic segment so it
|
||||
# lines up with the "middle" of the current
|
||||
# (OHLC) sample.
|
||||
self._last_line = QLineF(
|
||||
x[-2], y[-2],
|
||||
x_last, y_last
|
||||
)
|
||||
|
||||
|
||||
# TODO: this should probably be a "downsampled" curve type
|
||||
# that draws a bar-style (but for the px column) last graphics
|
||||
# element such that the current datum in view can be shown
|
||||
# (via it's max / min) even when highly zoomed out.
|
||||
class FlattenedOHLC(Curve):
|
||||
|
||||
def draw_last_datum(
|
||||
self,
|
||||
path: QPainterPath,
|
||||
src_data: np.ndarray,
|
||||
render_data: np.ndarray,
|
||||
reset: bool,
|
||||
array_key: str,
|
||||
|
||||
) -> None:
|
||||
lasts = src_data[-2:]
|
||||
x = lasts['index']
|
||||
y = lasts['close']
|
||||
|
||||
# draw the "current" step graphic segment so it
|
||||
# lines up with the "middle" of the current
|
||||
# (OHLC) sample.
|
||||
self._last_line = QLineF(
|
||||
x[-2], y[-2],
|
||||
x[-1], y[-1]
|
||||
)
|
||||
|
||||
|
||||
class StepCurve(Curve):
|
||||
|
||||
def declare_paintables(
|
||||
self,
|
||||
) -> None:
|
||||
self._last_step_rect = QRectF()
|
||||
|
||||
def draw_last_datum(
|
||||
self,
|
||||
path: QPainterPath,
|
||||
src_data: np.ndarray,
|
||||
render_data: np.ndarray,
|
||||
reset: bool,
|
||||
array_key: str,
|
||||
|
||||
w: float = 0.5,
|
||||
|
||||
) -> None:
|
||||
|
||||
# TODO: remove this and instead place all step curve
|
||||
# updating into pre-path data render callbacks.
|
||||
# full input data
|
||||
x = src_data['index']
|
||||
y = src_data[array_key]
|
||||
|
||||
x_last = x[-1]
|
||||
y_last = y[-1]
|
||||
|
||||
# lol, commenting this makes step curves
|
||||
# all "black" for me :eyeroll:..
|
||||
self._last_line = QLineF(
|
||||
x_last - w, 0,
|
||||
x_last + w, 0,
|
||||
)
|
||||
self._last_step_rect = QRectF(
|
||||
x_last - w, 0,
|
||||
x_last + w, y_last,
|
||||
)
|
||||
|
||||
def sub_paint(
|
||||
self,
|
||||
p: QPainter,
|
||||
profiler: pg.debug.Profiler,
|
||||
|
||||
) -> None:
|
||||
# p.drawLines(*tuple(filter(bool, self._last_step_lines)))
|
||||
# p.drawRect(self._last_step_rect)
|
||||
p.fillRect(self._last_step_rect, self._brush)
|
||||
profiler('.fillRect()')
|
||||
|
||||
def sub_br(
|
||||
self,
|
||||
path_w: float,
|
||||
path_h: float,
|
||||
|
||||
) -> (float, float):
|
||||
# passthrough
|
||||
return path_w, path_h
|
||||
|
|
|
@ -34,13 +34,6 @@ import numpy as np
|
|||
from numpy.lib import recfunctions as rfn
|
||||
import pyqtgraph as pg
|
||||
from PyQt5.QtGui import QPainterPath
|
||||
from PyQt5.QtCore import (
|
||||
# Qt,
|
||||
QLineF,
|
||||
# QSizeF,
|
||||
QRectF,
|
||||
# QPointF,
|
||||
)
|
||||
|
||||
from ..data._sharedmem import (
|
||||
ShmArray,
|
||||
|
@ -57,10 +50,12 @@ from ._pathops import (
|
|||
)
|
||||
from ._ohlc import (
|
||||
BarItems,
|
||||
bar_from_ohlc_row,
|
||||
# bar_from_ohlc_row,
|
||||
)
|
||||
from ._curve import (
|
||||
Curve,
|
||||
StepCurve,
|
||||
FlattenedOHLC,
|
||||
)
|
||||
from ..log import get_logger
|
||||
|
||||
|
@ -175,7 +170,7 @@ def render_baritems(
|
|||
format_xy=ohlc_flat_to_xy,
|
||||
)
|
||||
|
||||
curve = Curve(
|
||||
curve = FlattenedOHLC(
|
||||
name=f'{flow.name}_ds_ohlc',
|
||||
color=bars._color,
|
||||
)
|
||||
|
@ -244,84 +239,10 @@ def render_baritems(
|
|||
bars.show()
|
||||
bars.update()
|
||||
|
||||
draw_last = False
|
||||
|
||||
if should_line:
|
||||
|
||||
def draw_last_flattened_ohlc_line(
|
||||
graphics: pg.GraphicsObject,
|
||||
path: QPainterPath,
|
||||
src_data: np.ndarray,
|
||||
render_data: np.ndarray,
|
||||
reset: bool,
|
||||
|
||||
) -> None:
|
||||
lasts = src_data[-2:]
|
||||
x = lasts['index']
|
||||
y = lasts['close']
|
||||
|
||||
# draw the "current" step graphic segment so it
|
||||
# lines up with the "middle" of the current
|
||||
# (OHLC) sample.
|
||||
graphics._last_line = QLineF(
|
||||
x[-2], y[-2],
|
||||
x[-1], y[-1]
|
||||
)
|
||||
|
||||
draw_last = draw_last_flattened_ohlc_line
|
||||
|
||||
else:
|
||||
def draw_last_ohlc_bar(
|
||||
graphics: pg.GraphicsObject,
|
||||
path: QPainterPath,
|
||||
src_data: np.ndarray,
|
||||
render_data: np.ndarray,
|
||||
reset: bool,
|
||||
|
||||
) -> None:
|
||||
last = src_data[-1]
|
||||
|
||||
# generate new lines objects for updatable "current bar"
|
||||
graphics._last_bar_lines = bar_from_ohlc_row(last)
|
||||
|
||||
# last bar update
|
||||
i, o, h, l, last, v = last[
|
||||
['index', 'open', 'high', 'low', 'close', 'volume']
|
||||
]
|
||||
# assert i == graphics.start_index - 1
|
||||
# assert i == last_index
|
||||
body, larm, rarm = graphics._last_bar_lines
|
||||
|
||||
# XXX: is there a faster way to modify this?
|
||||
rarm.setLine(rarm.x1(), last, rarm.x2(), last)
|
||||
|
||||
# writer is responsible for changing open on "first" volume of bar
|
||||
larm.setLine(larm.x1(), o, larm.x2(), o)
|
||||
|
||||
if l != h: # noqa
|
||||
|
||||
if body is None:
|
||||
body = graphics._last_bar_lines[0] = QLineF(i, l, i, h)
|
||||
else:
|
||||
# update body
|
||||
body.setLine(i, l, i, h)
|
||||
|
||||
# XXX: pretty sure this is causing an issue where the
|
||||
# bar has a large upward move right before the next
|
||||
# sample and the body is getting set to None since the
|
||||
# next bar is flat but the shm array index update wasn't
|
||||
# read by the time this code runs. Iow we're doing this
|
||||
# removal of the body for a bar index that is now out of
|
||||
# date / from some previous sample. It's weird though
|
||||
# because i've seen it do this to bars i - 3 back?
|
||||
|
||||
draw_last = draw_last_ohlc_bar
|
||||
|
||||
return (
|
||||
graphics,
|
||||
r,
|
||||
{'read_from_key': False},
|
||||
draw_last,
|
||||
should_line,
|
||||
changed_to_line,
|
||||
)
|
||||
|
@ -411,10 +332,10 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
'''
|
||||
name: str
|
||||
plot: pg.PlotItem
|
||||
graphics: pg.GraphicsObject
|
||||
graphics: Curve
|
||||
_shm: ShmArray
|
||||
|
||||
draw_last_datum: Optional[
|
||||
draw_last: Optional[
|
||||
Callable[
|
||||
[np.ndarray, str],
|
||||
tuple[np.ndarray]
|
||||
|
@ -597,12 +518,9 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
render to graphics.
|
||||
|
||||
'''
|
||||
|
||||
# profiler = profiler or pg.debug.Profiler(
|
||||
profiler = pg.debug.Profiler(
|
||||
msg=f'Flow.update_graphics() for {self.name}',
|
||||
disabled=not pg_profile_enabled(),
|
||||
# disabled=False,
|
||||
ms_threshold=4,
|
||||
# ms_threshold=ms_slower_then,
|
||||
)
|
||||
|
@ -623,13 +541,9 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
# print('exiting early')
|
||||
return graphics
|
||||
|
||||
draw_last: bool = True
|
||||
slice_to_head: int = -1
|
||||
|
||||
should_redraw: bool = False
|
||||
|
||||
rkwargs = {}
|
||||
bars = False
|
||||
|
||||
if isinstance(graphics, BarItems):
|
||||
# XXX: special case where we change out graphics
|
||||
|
@ -638,7 +552,6 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
graphics,
|
||||
r,
|
||||
rkwargs,
|
||||
draw_last,
|
||||
should_line,
|
||||
changed_to_line,
|
||||
) = render_baritems(
|
||||
|
@ -648,7 +561,7 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
profiler,
|
||||
**kwargs,
|
||||
)
|
||||
bars = True
|
||||
# bars = True
|
||||
should_redraw = changed_to_line or not should_line
|
||||
|
||||
else:
|
||||
|
@ -661,7 +574,7 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
last_read=read,
|
||||
)
|
||||
|
||||
# ``Curve`` case:
|
||||
# ``Curve`` derivative case(s):
|
||||
array_key = array_key or self.name
|
||||
# print(array_key)
|
||||
|
||||
|
@ -670,20 +583,19 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
should_ds: bool = r._in_ds
|
||||
showing_src_data: bool = not r._in_ds
|
||||
|
||||
step_mode = getattr(graphics, '_step_mode', False)
|
||||
# step_mode = getattr(graphics, '_step_mode', False)
|
||||
step_mode = isinstance(graphics, StepCurve)
|
||||
if step_mode:
|
||||
|
||||
r.allocate_xy = to_step_format
|
||||
r.update_xy = update_step_xy
|
||||
r.format_xy = step_to_xy
|
||||
|
||||
slice_to_head = -2
|
||||
|
||||
# TODO: append logic inside ``.render()`` isn't
|
||||
# corrent yet for step curves.. remove this to see it.
|
||||
# correct yet for step curves.. remove this to see it.
|
||||
should_redraw = True
|
||||
|
||||
draw_last = True
|
||||
# draw_last = True
|
||||
slice_to_head = -2
|
||||
|
||||
# downsampling incremental state checking
|
||||
# check for and set std m4 downsample conditions
|
||||
|
@ -760,77 +672,23 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
graphics.path = r.path
|
||||
graphics.fast_path = r.fast_path
|
||||
|
||||
if draw_last and not bars:
|
||||
graphics.draw_last_datum(
|
||||
path,
|
||||
src_array,
|
||||
data,
|
||||
reset,
|
||||
array_key,
|
||||
)
|
||||
|
||||
if not step_mode:
|
||||
# TODO: is this ever better?
|
||||
# graphics.prepareGeometryChange()
|
||||
# profiler('.prepareGeometryChange()')
|
||||
|
||||
def draw_last_line(
|
||||
graphics: pg.GraphicsObject,
|
||||
path: QPainterPath,
|
||||
src_data: np.ndarray,
|
||||
render_data: np.ndarray,
|
||||
reset: bool,
|
||||
|
||||
) -> None:
|
||||
# default line draw last call
|
||||
with graphics.reset_cache():
|
||||
x = render_data['index']
|
||||
y = render_data[array_key]
|
||||
x_last = x[-1]
|
||||
y_last = y[-1]
|
||||
|
||||
# draw the "current" step graphic segment so it
|
||||
# lines up with the "middle" of the current
|
||||
# (OHLC) sample.
|
||||
graphics._last_line = QLineF(
|
||||
x[-2], y[-2],
|
||||
x_last, y_last
|
||||
)
|
||||
|
||||
draw_last_line(graphics, path, src_array, data, reset)
|
||||
|
||||
else:
|
||||
|
||||
def draw_last_step(
|
||||
graphics: pg.GraphicsObject,
|
||||
path: QPainterPath,
|
||||
src_data: np.ndarray,
|
||||
render_data: np.ndarray,
|
||||
reset: bool,
|
||||
|
||||
) -> None:
|
||||
w = 0.5
|
||||
# TODO: remove this and instead place all step curve
|
||||
# updating into pre-path data render callbacks.
|
||||
# full input data
|
||||
x = src_array['index']
|
||||
y = src_array[array_key]
|
||||
x_last = x[-1]
|
||||
y_last = y[-1]
|
||||
|
||||
# lol, commenting this makes step curves
|
||||
# all "black" for me :eyeroll:..
|
||||
graphics._last_line = QLineF(
|
||||
x_last - w, 0,
|
||||
x_last + w, 0,
|
||||
)
|
||||
graphics._last_step_rect = QRectF(
|
||||
x_last - w, 0,
|
||||
x_last + w, y_last,
|
||||
)
|
||||
|
||||
draw_last_step(graphics, path, src_array, data, reset)
|
||||
|
||||
# TODO: does this actuallly help us in any way (prolly should
|
||||
# look at the source / ask ogi). I think it avoid artifacts on
|
||||
# wheel-scroll downsampling curve updates?
|
||||
graphics.update()
|
||||
profiler('.prepareGeometryChange()')
|
||||
|
||||
elif bars and draw_last:
|
||||
draw_last(graphics, path, src_array, data, reset)
|
||||
graphics.update()
|
||||
profiler('.update()')
|
||||
# TODO: does this actuallly help us in any way (prolly should
|
||||
# look at the source / ask ogi). I think it avoid artifacts on
|
||||
# wheel-scroll downsampling curve updates?
|
||||
graphics.update()
|
||||
profiler('.update()')
|
||||
|
||||
return graphics
|
||||
|
||||
|
|
|
@ -27,6 +27,7 @@ import numpy as np
|
|||
import pyqtgraph as pg
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from PyQt5.QtCore import QLineF, QPointF
|
||||
from PyQt5.QtGui import QPainterPath
|
||||
|
||||
from .._profile import pg_profile_enabled, ms_slower_then
|
||||
from ._style import hcolor
|
||||
|
@ -85,8 +86,6 @@ class BarItems(pg.GraphicsObject):
|
|||
"Price range" bars graphics rendered from a OHLC sampled sequence.
|
||||
|
||||
'''
|
||||
sigPlotChanged = QtCore.pyqtSignal(object)
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
linked: LinkedSplits,
|
||||
|
@ -107,7 +106,7 @@ class BarItems(pg.GraphicsObject):
|
|||
self._name = name
|
||||
|
||||
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
||||
self.path = QtGui.QPainterPath()
|
||||
self.path = QPainterPath()
|
||||
self._last_bar_lines: Optional[tuple[QLineF, ...]] = None
|
||||
|
||||
def x_uppx(self) -> int:
|
||||
|
@ -192,3 +191,48 @@ class BarItems(pg.GraphicsObject):
|
|||
p.setPen(self.bars_pen)
|
||||
p.drawPath(self.path)
|
||||
profiler(f'draw history path: {self.path.capacity()}')
|
||||
|
||||
def draw_last_datum(
|
||||
self,
|
||||
path: QPainterPath,
|
||||
src_data: np.ndarray,
|
||||
render_data: np.ndarray,
|
||||
reset: bool,
|
||||
array_key: str,
|
||||
|
||||
) -> None:
|
||||
last = src_data[-1]
|
||||
|
||||
# generate new lines objects for updatable "current bar"
|
||||
self._last_bar_lines = bar_from_ohlc_row(last)
|
||||
|
||||
# last bar update
|
||||
i, o, h, l, last, v = last[
|
||||
['index', 'open', 'high', 'low', 'close', 'volume']
|
||||
]
|
||||
# assert i == graphics.start_index - 1
|
||||
# assert i == last_index
|
||||
body, larm, rarm = self._last_bar_lines
|
||||
|
||||
# XXX: is there a faster way to modify this?
|
||||
rarm.setLine(rarm.x1(), last, rarm.x2(), last)
|
||||
|
||||
# writer is responsible for changing open on "first" volume of bar
|
||||
larm.setLine(larm.x1(), o, larm.x2(), o)
|
||||
|
||||
if l != h: # noqa
|
||||
|
||||
if body is None:
|
||||
body = self._last_bar_lines[0] = QLineF(i, l, i, h)
|
||||
else:
|
||||
# update body
|
||||
body.setLine(i, l, i, h)
|
||||
|
||||
# XXX: pretty sure this is causing an issue where the
|
||||
# bar has a large upward move right before the next
|
||||
# sample and the body is getting set to None since the
|
||||
# next bar is flat but the shm array index update wasn't
|
||||
# read by the time this code runs. Iow we're doing this
|
||||
# removal of the body for a bar index that is now out of
|
||||
# date / from some previous sample. It's weird though
|
||||
# because i've seen it do this to bars i - 3 back?
|
||||
|
|
Loading…
Reference in New Issue