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 ..data._sharedmem import ShmArray
|
||||||
from ._l1 import L1Labels
|
from ._l1 import L1Labels
|
||||||
from ._ohlc import BarItems
|
from ._ohlc import BarItems
|
||||||
from ._curve import Curve
|
from ._curve import (
|
||||||
|
Curve,
|
||||||
|
StepCurve,
|
||||||
|
)
|
||||||
from ._style import (
|
from ._style import (
|
||||||
hcolor,
|
hcolor,
|
||||||
CHART_MARGINS,
|
CHART_MARGINS,
|
||||||
|
@ -1051,6 +1054,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
color: Optional[str] = None,
|
color: Optional[str] = None,
|
||||||
add_label: bool = True,
|
add_label: bool = True,
|
||||||
pi: Optional[pg.PlotItem] = None,
|
pi: Optional[pg.PlotItem] = None,
|
||||||
|
step_mode: bool = False,
|
||||||
|
|
||||||
**pdi_kwargs,
|
**pdi_kwargs,
|
||||||
|
|
||||||
|
@ -1067,29 +1071,18 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
data_key = array_key or name
|
data_key = array_key or name
|
||||||
|
|
||||||
# yah, we wrote our own B)
|
curve_type = {
|
||||||
data = shm.array
|
None: Curve,
|
||||||
curve = Curve(
|
'step': StepCurve,
|
||||||
# antialias=True,
|
# TODO:
|
||||||
|
# 'bars': BarsItems
|
||||||
|
}['step' if step_mode else None]
|
||||||
|
|
||||||
|
curve = curve_type(
|
||||||
name=name,
|
name=name,
|
||||||
|
|
||||||
# XXX: pretty sure this is just more overhead
|
|
||||||
# on data reads and makes graphics rendering no faster
|
|
||||||
# clipToView=True,
|
|
||||||
|
|
||||||
**pdi_kwargs,
|
**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
|
pi = pi or self.plotItem
|
||||||
|
|
||||||
self._flows[data_key] = Flow(
|
self._flows[data_key] = Flow(
|
||||||
|
|
|
@ -19,20 +19,24 @@ Fast, smooth, sexy curves.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from contextlib import contextmanager as cm
|
from contextlib import contextmanager as cm
|
||||||
from typing import Optional
|
from typing import Optional, Callable
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtGui, QtWidgets
|
from PyQt5 import QtWidgets
|
||||||
from PyQt5.QtWidgets import QGraphicsItem
|
from PyQt5.QtWidgets import QGraphicsItem
|
||||||
from PyQt5.QtCore import (
|
from PyQt5.QtCore import (
|
||||||
Qt,
|
Qt,
|
||||||
QLineF,
|
QLineF,
|
||||||
QSizeF,
|
QSizeF,
|
||||||
QRectF,
|
QRectF,
|
||||||
|
# QRect,
|
||||||
QPointF,
|
QPointF,
|
||||||
)
|
)
|
||||||
|
from PyQt5.QtGui import (
|
||||||
|
QPainter,
|
||||||
|
QPainterPath,
|
||||||
|
)
|
||||||
from .._profile import pg_profile_enabled, ms_slower_then
|
from .._profile import pg_profile_enabled, ms_slower_then
|
||||||
from ._style import hcolor
|
from ._style import hcolor
|
||||||
# from ._compression import (
|
# from ._compression import (
|
||||||
|
@ -59,10 +63,12 @@ class Curve(pg.GraphicsObject):
|
||||||
``pyqtgraph.PlotCurveItem`` built for highly customizable real-time
|
``pyqtgraph.PlotCurveItem`` built for highly customizable real-time
|
||||||
updates.
|
updates.
|
||||||
|
|
||||||
This type is a much stripped down version of a ``pyqtgraph`` style "graphics object" in
|
This type is a much stripped down version of a ``pyqtgraph`` style
|
||||||
the sense that the internal lower level graphics which are drawn in the ``.paint()`` method
|
"graphics object" in the sense that the internal lower level
|
||||||
are actually rendered outside of this class entirely and instead are assigned as state
|
graphics which are drawn in the ``.paint()`` method are actually
|
||||||
(instance vars) here and then drawn during a Qt graphics cycle.
|
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
|
The main motivation for this more modular, composed design is that
|
||||||
lower level graphics data can be rendered in different threads and
|
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
|
level path generation and incremental update. The main differences in
|
||||||
the path generation code include:
|
the path generation code include:
|
||||||
|
|
||||||
- avoiding regeneration of the entire historical path where possible and instead
|
- avoiding regeneration of the entire historical path where possible
|
||||||
only updating the "new" segment(s) via a ``numpy`` array diff calc.
|
and instead only updating the "new" segment(s) via a ``numpy``
|
||||||
|
array diff calc.
|
||||||
- here, the "last" graphics datum-segment is drawn independently
|
- here, the "last" graphics datum-segment is drawn independently
|
||||||
such that near-term (high frequency) discrete-time-sampled style
|
such that near-term (high frequency) discrete-time-sampled style
|
||||||
updates don't trigger a full path redraw.
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
*args,
|
*args,
|
||||||
|
@ -94,19 +107,20 @@ class Curve(pg.GraphicsObject):
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
|
self._name = name
|
||||||
|
|
||||||
# brutaaalll, see comments within..
|
# brutaaalll, see comments within..
|
||||||
self.yData = None
|
self.yData = None
|
||||||
self.xData = None
|
self.xData = None
|
||||||
self._last_cap: int = 0
|
|
||||||
|
|
||||||
self._name = name
|
# self._last_cap: int = 0
|
||||||
self.path: Optional[QtGui.QPainterPath] = None
|
self.path: Optional[QPainterPath] = None
|
||||||
|
|
||||||
# additional path used for appends which tries to avoid
|
# additional path used for appends which tries to avoid
|
||||||
# triggering an update/redraw of the presumably larger
|
# triggering an update/redraw of the presumably larger
|
||||||
# historical ``.path`` above.
|
# historical ``.path`` above.
|
||||||
self.use_fpath = use_fpath
|
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
|
# 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...
|
||||||
|
@ -125,12 +139,12 @@ 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: Optional[QLineF] = None
|
||||||
self._last_step_rect: Optional[QRectF] = None
|
self._last_line = QLineF()
|
||||||
self._last_w: float = 1
|
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
|
||||||
|
|
||||||
# self._fill = True
|
# self._fill = True
|
||||||
self._brush = pg.functions.mkBrush(hcolor(fill_color or color))
|
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)
|
# endpoint (something we saw on trade rate curves)
|
||||||
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
|
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
|
# TODO: probably stick this in a new parent
|
||||||
# type which will contain our own version of
|
# type which will contain our own version of
|
||||||
# what ``PlotCurveItem`` had in terms of base
|
# what ``PlotCurveItem`` had in terms of base
|
||||||
|
@ -215,7 +244,7 @@ class Curve(pg.GraphicsObject):
|
||||||
Compute and then cache our rect.
|
Compute and then cache our rect.
|
||||||
'''
|
'''
|
||||||
if self.path is None:
|
if self.path is None:
|
||||||
return QtGui.QPainterPath().boundingRect()
|
return QPainterPath().boundingRect()
|
||||||
else:
|
else:
|
||||||
# dynamically override this method after initial
|
# dynamically override this method after initial
|
||||||
# path is created to avoid requiring the above None check
|
# path is created to avoid requiring the above None check
|
||||||
|
@ -227,14 +256,15 @@ class Curve(pg.GraphicsObject):
|
||||||
Post init ``.boundingRect()```.
|
Post init ``.boundingRect()```.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
hb = self.path.controlPointRect()
|
|
||||||
# hb = self.path.boundingRect()
|
# hb = self.path.boundingRect()
|
||||||
|
hb = self.path.controlPointRect()
|
||||||
hb_size = hb.size()
|
hb_size = hb.size()
|
||||||
|
|
||||||
fp = self.fast_path
|
fp = self.fast_path
|
||||||
if fp:
|
if fp:
|
||||||
fhb = fp.controlPointRect()
|
fhb = fp.controlPointRect()
|
||||||
hb_size = fhb.size() + hb_size
|
hb_size = fhb.size() + hb_size
|
||||||
|
|
||||||
# print(f'hb_size: {hb_size}')
|
# print(f'hb_size: {hb_size}')
|
||||||
|
|
||||||
# if self._last_step_rect:
|
# if self._last_step_rect:
|
||||||
|
@ -255,7 +285,13 @@ class Curve(pg.GraphicsObject):
|
||||||
w = hb_size.width()
|
w = hb_size.width()
|
||||||
h = hb_size.height()
|
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
|
# only on a plane line do we include
|
||||||
# and extra index step's worth of width
|
# and extra index step's worth of width
|
||||||
# since in the step case the end of the curve
|
# since in the step case the end of the curve
|
||||||
|
@ -289,7 +325,7 @@ class Curve(pg.GraphicsObject):
|
||||||
|
|
||||||
def paint(
|
def paint(
|
||||||
self,
|
self,
|
||||||
p: QtGui.QPainter,
|
p: QPainter,
|
||||||
opt: QtWidgets.QStyleOptionGraphicsItem,
|
opt: QtWidgets.QStyleOptionGraphicsItem,
|
||||||
w: QtWidgets.QWidget
|
w: QtWidgets.QWidget
|
||||||
|
|
||||||
|
@ -301,25 +337,16 @@ class Curve(pg.GraphicsObject):
|
||||||
ms_threshold=ms_slower_then,
|
ms_threshold=ms_slower_then,
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
sub_paint = self.sub_paint
|
||||||
self._step_mode
|
if sub_paint:
|
||||||
and self._last_step_rect
|
sub_paint(p, profiler)
|
||||||
):
|
|
||||||
brush = self._brush
|
|
||||||
|
|
||||||
# p.drawLines(*tuple(filter(bool, self._last_step_lines)))
|
p.setPen(self.last_step_pen)
|
||||||
# p.drawRect(self._last_step_rect)
|
p.drawLine(self._last_line)
|
||||||
p.fillRect(self._last_step_rect, brush)
|
profiler('.drawLine()')
|
||||||
profiler('.fillRect()')
|
p.setPen(self._pen)
|
||||||
|
|
||||||
if self._last_line:
|
|
||||||
p.setPen(self.last_step_pen)
|
|
||||||
p.drawLine(self._last_line)
|
|
||||||
profiler('.drawLine()')
|
|
||||||
p.setPen(self._pen)
|
|
||||||
|
|
||||||
path = self.path
|
path = self.path
|
||||||
|
|
||||||
# cap = path.capacity()
|
# cap = path.capacity()
|
||||||
# if cap != self._last_cap:
|
# if cap != self._last_cap:
|
||||||
# print(f'NEW CAPACITY: {self._last_cap} -> {cap}')
|
# print(f'NEW CAPACITY: {self._last_cap} -> {cap}')
|
||||||
|
@ -341,3 +368,116 @@ class Curve(pg.GraphicsObject):
|
||||||
# if self._fill:
|
# if self._fill:
|
||||||
# brush = self.opts['brush']
|
# brush = self.opts['brush']
|
||||||
# p.fillPath(self.path, 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
|
from numpy.lib import recfunctions as rfn
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5.QtGui import QPainterPath
|
from PyQt5.QtGui import QPainterPath
|
||||||
from PyQt5.QtCore import (
|
|
||||||
# Qt,
|
|
||||||
QLineF,
|
|
||||||
# QSizeF,
|
|
||||||
QRectF,
|
|
||||||
# QPointF,
|
|
||||||
)
|
|
||||||
|
|
||||||
from ..data._sharedmem import (
|
from ..data._sharedmem import (
|
||||||
ShmArray,
|
ShmArray,
|
||||||
|
@ -57,10 +50,12 @@ from ._pathops import (
|
||||||
)
|
)
|
||||||
from ._ohlc import (
|
from ._ohlc import (
|
||||||
BarItems,
|
BarItems,
|
||||||
bar_from_ohlc_row,
|
# bar_from_ohlc_row,
|
||||||
)
|
)
|
||||||
from ._curve import (
|
from ._curve import (
|
||||||
Curve,
|
Curve,
|
||||||
|
StepCurve,
|
||||||
|
FlattenedOHLC,
|
||||||
)
|
)
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
|
||||||
|
@ -175,7 +170,7 @@ def render_baritems(
|
||||||
format_xy=ohlc_flat_to_xy,
|
format_xy=ohlc_flat_to_xy,
|
||||||
)
|
)
|
||||||
|
|
||||||
curve = Curve(
|
curve = FlattenedOHLC(
|
||||||
name=f'{flow.name}_ds_ohlc',
|
name=f'{flow.name}_ds_ohlc',
|
||||||
color=bars._color,
|
color=bars._color,
|
||||||
)
|
)
|
||||||
|
@ -244,84 +239,10 @@ def render_baritems(
|
||||||
bars.show()
|
bars.show()
|
||||||
bars.update()
|
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 (
|
return (
|
||||||
graphics,
|
graphics,
|
||||||
r,
|
r,
|
||||||
{'read_from_key': False},
|
{'read_from_key': False},
|
||||||
draw_last,
|
|
||||||
should_line,
|
should_line,
|
||||||
changed_to_line,
|
changed_to_line,
|
||||||
)
|
)
|
||||||
|
@ -411,10 +332,10 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
'''
|
'''
|
||||||
name: str
|
name: str
|
||||||
plot: pg.PlotItem
|
plot: pg.PlotItem
|
||||||
graphics: pg.GraphicsObject
|
graphics: Curve
|
||||||
_shm: ShmArray
|
_shm: ShmArray
|
||||||
|
|
||||||
draw_last_datum: Optional[
|
draw_last: Optional[
|
||||||
Callable[
|
Callable[
|
||||||
[np.ndarray, str],
|
[np.ndarray, str],
|
||||||
tuple[np.ndarray]
|
tuple[np.ndarray]
|
||||||
|
@ -597,12 +518,9 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
render to graphics.
|
render to graphics.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
|
|
||||||
# profiler = profiler or pg.debug.Profiler(
|
|
||||||
profiler = pg.debug.Profiler(
|
profiler = pg.debug.Profiler(
|
||||||
msg=f'Flow.update_graphics() for {self.name}',
|
msg=f'Flow.update_graphics() for {self.name}',
|
||||||
disabled=not pg_profile_enabled(),
|
disabled=not pg_profile_enabled(),
|
||||||
# disabled=False,
|
|
||||||
ms_threshold=4,
|
ms_threshold=4,
|
||||||
# ms_threshold=ms_slower_then,
|
# ms_threshold=ms_slower_then,
|
||||||
)
|
)
|
||||||
|
@ -623,13 +541,9 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
# print('exiting early')
|
# print('exiting early')
|
||||||
return graphics
|
return graphics
|
||||||
|
|
||||||
draw_last: bool = True
|
|
||||||
slice_to_head: int = -1
|
slice_to_head: int = -1
|
||||||
|
|
||||||
should_redraw: bool = False
|
should_redraw: bool = False
|
||||||
|
|
||||||
rkwargs = {}
|
rkwargs = {}
|
||||||
bars = False
|
|
||||||
|
|
||||||
if isinstance(graphics, BarItems):
|
if isinstance(graphics, BarItems):
|
||||||
# XXX: special case where we change out graphics
|
# XXX: special case where we change out graphics
|
||||||
|
@ -638,7 +552,6 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
graphics,
|
graphics,
|
||||||
r,
|
r,
|
||||||
rkwargs,
|
rkwargs,
|
||||||
draw_last,
|
|
||||||
should_line,
|
should_line,
|
||||||
changed_to_line,
|
changed_to_line,
|
||||||
) = render_baritems(
|
) = render_baritems(
|
||||||
|
@ -648,7 +561,7 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
profiler,
|
profiler,
|
||||||
**kwargs,
|
**kwargs,
|
||||||
)
|
)
|
||||||
bars = True
|
# bars = True
|
||||||
should_redraw = changed_to_line or not should_line
|
should_redraw = changed_to_line or not should_line
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
@ -661,7 +574,7 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
last_read=read,
|
last_read=read,
|
||||||
)
|
)
|
||||||
|
|
||||||
# ``Curve`` case:
|
# ``Curve`` derivative case(s):
|
||||||
array_key = array_key or self.name
|
array_key = array_key or self.name
|
||||||
# print(array_key)
|
# print(array_key)
|
||||||
|
|
||||||
|
@ -670,20 +583,19 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
should_ds: bool = r._in_ds
|
should_ds: bool = r._in_ds
|
||||||
showing_src_data: bool = not 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:
|
if step_mode:
|
||||||
|
|
||||||
r.allocate_xy = to_step_format
|
r.allocate_xy = to_step_format
|
||||||
r.update_xy = update_step_xy
|
r.update_xy = update_step_xy
|
||||||
r.format_xy = step_to_xy
|
r.format_xy = step_to_xy
|
||||||
|
|
||||||
slice_to_head = -2
|
|
||||||
|
|
||||||
# TODO: append logic inside ``.render()`` isn't
|
# 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
|
should_redraw = True
|
||||||
|
# draw_last = True
|
||||||
draw_last = True
|
slice_to_head = -2
|
||||||
|
|
||||||
# downsampling incremental state checking
|
# downsampling incremental state checking
|
||||||
# check for and set std m4 downsample conditions
|
# check for and set std m4 downsample conditions
|
||||||
|
@ -760,77 +672,23 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
graphics.path = r.path
|
graphics.path = r.path
|
||||||
graphics.fast_path = r.fast_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(
|
# TODO: does this actuallly help us in any way (prolly should
|
||||||
graphics: pg.GraphicsObject,
|
# look at the source / ask ogi). I think it avoid artifacts on
|
||||||
path: QPainterPath,
|
# wheel-scroll downsampling curve updates?
|
||||||
src_data: np.ndarray,
|
graphics.update()
|
||||||
render_data: np.ndarray,
|
profiler('.update()')
|
||||||
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()')
|
|
||||||
|
|
||||||
return graphics
|
return graphics
|
||||||
|
|
||||||
|
|
|
@ -27,6 +27,7 @@ import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||||
from PyQt5.QtCore import QLineF, QPointF
|
from PyQt5.QtCore import QLineF, QPointF
|
||||||
|
from PyQt5.QtGui import QPainterPath
|
||||||
|
|
||||||
from .._profile import pg_profile_enabled, ms_slower_then
|
from .._profile import pg_profile_enabled, ms_slower_then
|
||||||
from ._style import hcolor
|
from ._style import hcolor
|
||||||
|
@ -85,8 +86,6 @@ class BarItems(pg.GraphicsObject):
|
||||||
"Price range" bars graphics rendered from a OHLC sampled sequence.
|
"Price range" bars graphics rendered from a OHLC sampled sequence.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
sigPlotChanged = QtCore.pyqtSignal(object)
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
linked: LinkedSplits,
|
linked: LinkedSplits,
|
||||||
|
@ -107,7 +106,7 @@ class BarItems(pg.GraphicsObject):
|
||||||
self._name = name
|
self._name = name
|
||||||
|
|
||||||
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
||||||
self.path = QtGui.QPainterPath()
|
self.path = QPainterPath()
|
||||||
self._last_bar_lines: Optional[tuple[QLineF, ...]] = None
|
self._last_bar_lines: Optional[tuple[QLineF, ...]] = None
|
||||||
|
|
||||||
def x_uppx(self) -> int:
|
def x_uppx(self) -> int:
|
||||||
|
@ -192,3 +191,48 @@ class BarItems(pg.GraphicsObject):
|
||||||
p.setPen(self.bars_pen)
|
p.setPen(self.bars_pen)
|
||||||
p.drawPath(self.path)
|
p.drawPath(self.path)
|
||||||
profiler(f'draw history path: {self.path.capacity()}')
|
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