More WIP, implement `BarItems` rendering in `Flow.update_graphics()`
parent
789c77f9b2
commit
44c7ff253f
|
@ -23,6 +23,8 @@ incremental update.
|
|||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from functools import partial
|
||||
import time
|
||||
from typing import (
|
||||
Optional,
|
||||
Callable,
|
||||
|
@ -30,6 +32,7 @@ from typing import (
|
|||
|
||||
import msgspec
|
||||
import numpy as np
|
||||
from numpy.lib import recfunctions as rfn
|
||||
import pyqtgraph as pg
|
||||
from PyQt5.QtGui import QPainterPath
|
||||
|
||||
|
@ -37,6 +40,7 @@ from ..data._sharedmem import (
|
|||
ShmArray,
|
||||
# attach_shm_array
|
||||
)
|
||||
from .._profile import pg_profile_enabled, ms_slower_then
|
||||
from ._ohlc import (
|
||||
BarItems,
|
||||
gen_qpath,
|
||||
|
@ -46,7 +50,12 @@ from ._curve import (
|
|||
)
|
||||
from ._compression import (
|
||||
ohlc_flatten,
|
||||
ds_m4,
|
||||
)
|
||||
from ..log import get_logger
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
# class FlowsTable(msgspec.Struct):
|
||||
# '''
|
||||
|
@ -72,11 +81,63 @@ from ._compression import (
|
|||
# return cls(shm)
|
||||
|
||||
|
||||
def rowarr_to_path(
|
||||
rows_array: np.ndarray,
|
||||
x_basis: np.ndarray,
|
||||
flow: Flow,
|
||||
|
||||
) -> QPainterPath:
|
||||
|
||||
# TODO: we could in theory use ``numba`` to flatten
|
||||
# if needed?
|
||||
|
||||
# to 1d
|
||||
y = rows_array.flatten()
|
||||
|
||||
return pg.functions.arrayToQPath(
|
||||
# these get passed at render call time
|
||||
x=x_basis[:y.size],
|
||||
y=y,
|
||||
connect='all',
|
||||
finiteCheck=False,
|
||||
path=flow.path,
|
||||
)
|
||||
|
||||
|
||||
def ohlc_flat_view(
|
||||
ohlc_shm: ShmArray,
|
||||
|
||||
# XXX: we bind this in currently..
|
||||
x_basis: np.ndarray,
|
||||
|
||||
# vr: Optional[slice] = None,
|
||||
|
||||
) -> np.ndarray:
|
||||
'''
|
||||
Return flattened-non-copy view into an OHLC shm array.
|
||||
|
||||
'''
|
||||
ohlc = ohlc_shm._array[['open', 'high', 'low', 'close']]
|
||||
# if vr:
|
||||
# ohlc = ohlc[vr]
|
||||
# x = x_basis[vr]
|
||||
|
||||
unstructured = rfn.structured_to_unstructured(
|
||||
ohlc,
|
||||
copy=False,
|
||||
)
|
||||
# breakpoint()
|
||||
y = unstructured.flatten()
|
||||
x = x_basis[:y.size]
|
||||
return x, y
|
||||
|
||||
|
||||
class Flow(msgspec.Struct): # , frozen=True):
|
||||
'''
|
||||
(FinancialSignal-)Flow compound type which wraps a real-time
|
||||
graphics (curve) and its backing data stream together for high level
|
||||
access and control.
|
||||
(Financial Signal-)Flow compound type which wraps a real-time
|
||||
shm array stream with displayed graphics (curves, charts)
|
||||
for high level access and control as well as efficient incremental
|
||||
update.
|
||||
|
||||
The intention is for this type to eventually be capable of shm-passing
|
||||
of incrementally updated graphics stream data between actors.
|
||||
|
@ -89,6 +150,8 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
|
||||
is_ohlc: bool = False
|
||||
render: bool = True # toggle for display loop
|
||||
flat: Optional[ShmArray] = None
|
||||
x_basis: Optional[np.ndarray] = None
|
||||
|
||||
_last_uppx: float = 0
|
||||
_in_ds: bool = False
|
||||
|
@ -96,6 +159,7 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
_graphics_tranform_fn: Optional[Callable[ShmArray, np.ndarray]] = None
|
||||
|
||||
# map from uppx -> (downsampled data, incremental graphics)
|
||||
_src_r: Optional[Renderer] = None
|
||||
_render_table: dict[
|
||||
Optional[int],
|
||||
tuple[Renderer, pg.GraphicsItem],
|
||||
|
@ -215,7 +279,9 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
int, int, np.ndarray,
|
||||
int, int, np.ndarray,
|
||||
]:
|
||||
# read call
|
||||
array = self.shm.array
|
||||
|
||||
indexes = array['index']
|
||||
ifirst = indexes[0]
|
||||
ilast = indexes[-1]
|
||||
|
@ -245,6 +311,8 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
render: bool = True,
|
||||
array_key: Optional[str] = None,
|
||||
|
||||
profiler=None,
|
||||
|
||||
**kwargs,
|
||||
|
||||
) -> pg.GraphicsObject:
|
||||
|
@ -253,8 +321,19 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
render to graphics.
|
||||
|
||||
'''
|
||||
|
||||
profiler = profiler or pg.debug.Profiler(
|
||||
msg=f'Flow.update_graphics() for {self.name}',
|
||||
disabled=not pg_profile_enabled(),
|
||||
gt=ms_slower_then,
|
||||
delayed=True,
|
||||
)
|
||||
# shm read and slice to view
|
||||
read = xfirst, xlast, array, ivl, ivr, in_view = self.read()
|
||||
read = (
|
||||
xfirst, xlast, array,
|
||||
ivl, ivr, in_view,
|
||||
) = self.read()
|
||||
profiler('read src shm data')
|
||||
|
||||
if (
|
||||
not in_view.size
|
||||
|
@ -265,100 +344,182 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
graphics = self.graphics
|
||||
if isinstance(graphics, BarItems):
|
||||
|
||||
# ugh, not luvin dis, should we have just a designated
|
||||
# instance var?
|
||||
r = self._render_table.get('src')
|
||||
# if no source data renderer exists create one.
|
||||
r = self._src_r
|
||||
if not r:
|
||||
r = Renderer(
|
||||
# OHLC bars path renderer
|
||||
r = self._src_r = Renderer(
|
||||
flow=self,
|
||||
draw=gen_qpath, # TODO: rename this to something with ohlc
|
||||
# TODO: rename this to something with ohlc
|
||||
draw_path=gen_qpath,
|
||||
last_read=read,
|
||||
)
|
||||
self._render_table['src'] = (r, graphics)
|
||||
|
||||
# create a flattened view onto the OHLC array
|
||||
# which can be read as a line-style format
|
||||
# shm = self.shm
|
||||
# self.flat = shm.unstruct_view(['open', 'high', 'low', 'close'])
|
||||
# import pdbpp
|
||||
# pdbpp.set_trace()
|
||||
# x = self.x_basis = (
|
||||
# np.broadcast_to(
|
||||
# shm._array['index'][:, None],
|
||||
# # self.flat._array.shape,
|
||||
# self.flat.shape,
|
||||
# ) + np.array([-0.5, 0, 0, 0.5])
|
||||
# )
|
||||
|
||||
ds_curve_r = Renderer(
|
||||
flow=self,
|
||||
draw=gen_qpath, # TODO: rename this to something with ohlc
|
||||
|
||||
# just swap in the flat view
|
||||
data_t=lambda array: self.flat.array,
|
||||
# data_t=partial(
|
||||
# ohlc_flat_view,
|
||||
# self.shm,
|
||||
# ),
|
||||
last_read=read,
|
||||
prerender_fn=ohlc_flatten,
|
||||
draw_path=partial(
|
||||
rowarr_to_path,
|
||||
x_basis=None,
|
||||
),
|
||||
|
||||
)
|
||||
curve = FastAppendCurve(
|
||||
# y=y,
|
||||
# x=x,
|
||||
name='OHLC',
|
||||
color=graphics._color,
|
||||
)
|
||||
curve.hide()
|
||||
self.plot.addItem(curve)
|
||||
|
||||
# baseline "line" downsampled OHLC curve that should
|
||||
# kick on only when we reach a certain uppx threshold.
|
||||
self._render_table[0] = (
|
||||
ds_curve_r,
|
||||
FastAppendCurve(
|
||||
y=y,
|
||||
x=x,
|
||||
name='OHLC',
|
||||
color=self._color,
|
||||
),
|
||||
curve,
|
||||
)
|
||||
|
||||
dsc_r, curve = self._render_table[0]
|
||||
|
||||
# do checks for whether or not we require downsampling:
|
||||
# - if we're **not** downsampling then we simply want to
|
||||
# render the bars graphics curve and update..
|
||||
# - if insteam we are in a downsamplig state then we to
|
||||
x_gt = 8
|
||||
uppx = curve.x_uppx()
|
||||
in_line = should_line = curve.isVisible()
|
||||
if (
|
||||
should_line
|
||||
and uppx < x_gt
|
||||
):
|
||||
should_line = False
|
||||
|
||||
elif (
|
||||
not should_line
|
||||
and uppx >= x_gt
|
||||
):
|
||||
should_line = True
|
||||
|
||||
profiler(f'ds logic complete line={should_line}')
|
||||
|
||||
# do graphics updates
|
||||
if should_line:
|
||||
# start = time.time()
|
||||
# y = self.shm.unstruct_view(
|
||||
# ['open', 'high', 'low', 'close'],
|
||||
# )
|
||||
# print(f'unstruct diff: {time.time() - start}')
|
||||
# profiler('read unstr view bars to line')
|
||||
# # start = self.flat._first.value
|
||||
|
||||
# x = self.x_basis[:y.size].flatten()
|
||||
# y = y.flatten()
|
||||
# profiler('flattening bars to line')
|
||||
# path, last = dsc_r.render(read)
|
||||
# x, flat = ohlc_flat_view(
|
||||
# ohlc_shm=self.shm,
|
||||
# x_basis=x_basis,
|
||||
# )
|
||||
# y = y.flatten()
|
||||
# y_iv = y[ivl:ivr].flatten()
|
||||
# x_iv = x[ivl:ivr].flatten()
|
||||
# assert y.size == x.size
|
||||
|
||||
x, y = self.flat = ohlc_flatten(array)
|
||||
x_iv, y_iv = ohlc_flatten(in_view)
|
||||
profiler('flattened OHLC data')
|
||||
|
||||
curve.update_from_array(
|
||||
x,
|
||||
y,
|
||||
x_iv=x_iv,
|
||||
y_iv=y_iv,
|
||||
view_range=None, # hack
|
||||
profiler=profiler,
|
||||
)
|
||||
profiler('updated ds curve')
|
||||
|
||||
else:
|
||||
# render incremental or in-view update
|
||||
# and apply ouput (path) to graphics.
|
||||
path, last = r.render(
|
||||
read,
|
||||
only_in_view=True,
|
||||
)
|
||||
|
||||
graphics.path = path
|
||||
graphics.draw_last(last)
|
||||
|
||||
# NOTE: on appends we used to have to flip the coords
|
||||
# cache thought it doesn't seem to be required any more?
|
||||
# graphics.setCacheMode(QtWidgets.QGraphicsItem.NoCache)
|
||||
# graphics.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
||||
|
||||
graphics.prepareGeometryChange()
|
||||
graphics.update()
|
||||
|
||||
if (
|
||||
not in_line
|
||||
and should_line
|
||||
):
|
||||
# change to line graphic
|
||||
|
||||
log.info(
|
||||
f'downsampling to line graphic {self.name}'
|
||||
)
|
||||
graphics.hide()
|
||||
# graphics.update()
|
||||
curve.show()
|
||||
curve.update()
|
||||
|
||||
elif in_line and not should_line:
|
||||
log.info(f'showing bars graphic {self.name}')
|
||||
curve.hide()
|
||||
graphics.show()
|
||||
graphics.update()
|
||||
|
||||
# update our pre-downsample-ready data and then pass that
|
||||
# new data the downsampler algo for incremental update.
|
||||
else:
|
||||
pass
|
||||
# do incremental update
|
||||
|
||||
graphics.update_from_array(
|
||||
array,
|
||||
in_view,
|
||||
view_range=(ivl, ivr) if use_vr else None,
|
||||
# graphics.update_from_array(
|
||||
# array,
|
||||
# in_view,
|
||||
# view_range=(ivl, ivr) if use_vr else None,
|
||||
|
||||
**kwargs,
|
||||
)
|
||||
# **kwargs,
|
||||
# )
|
||||
|
||||
# generate and apply path to graphics obj
|
||||
graphics.path, last = r.render(only_in_view=True)
|
||||
graphics.draw_last(last)
|
||||
# generate and apply path to graphics obj
|
||||
# graphics.path, last = r.render(
|
||||
# read,
|
||||
# only_in_view=True,
|
||||
# )
|
||||
# graphics.draw_last(last)
|
||||
|
||||
else:
|
||||
# should_ds = False
|
||||
# should_redraw = False
|
||||
|
||||
# # downsampling incremental state checking
|
||||
# uppx = bars.x_uppx()
|
||||
# px_width = bars.px_width()
|
||||
# uppx_diff = (uppx - self._last_uppx)
|
||||
|
||||
# if self.renderer is None:
|
||||
# self.renderer = Renderer(
|
||||
# flow=self,
|
||||
|
||||
# if not self._in_ds:
|
||||
# # in not currently marked as downsampling graphics
|
||||
# # then only draw the full bars graphic for datums "in
|
||||
# # view".
|
||||
|
||||
# # check for downsampling conditions
|
||||
# if (
|
||||
# # std m4 downsample conditions
|
||||
# px_width
|
||||
# and uppx_diff >= 4
|
||||
# or uppx_diff <= -3
|
||||
# or self._step_mode and abs(uppx_diff) >= 4
|
||||
|
||||
# ):
|
||||
# log.info(
|
||||
# f'{self._name} sampler change: {self._last_uppx} -> {uppx}'
|
||||
# )
|
||||
# self._last_uppx = uppx
|
||||
# should_ds = True
|
||||
|
||||
# elif (
|
||||
# uppx <= 2
|
||||
# and self._in_ds
|
||||
# ):
|
||||
# # we should de-downsample back to our original
|
||||
# # source data so we clear our path data in prep
|
||||
# # to generate a new one from original source data.
|
||||
# should_redraw = True
|
||||
# should_ds = False
|
||||
# ``FastAppendCurve`` case:
|
||||
|
||||
array_key = array_key or self.name
|
||||
|
||||
|
@ -376,23 +537,64 @@ class Flow(msgspec.Struct): # , frozen=True):
|
|||
return graphics
|
||||
|
||||
|
||||
def xy_downsample(
|
||||
x,
|
||||
y,
|
||||
px_width,
|
||||
uppx,
|
||||
|
||||
x_spacer: float = 0.5,
|
||||
|
||||
) -> tuple[np.ndarray, np.ndarray]:
|
||||
|
||||
# downsample whenever more then 1 pixels per datum can be shown.
|
||||
# always refresh data bounds until we get diffing
|
||||
# working properly, see above..
|
||||
bins, x, y = ds_m4(
|
||||
x,
|
||||
y,
|
||||
px_width=px_width,
|
||||
uppx=uppx,
|
||||
log_scale=bool(uppx)
|
||||
)
|
||||
|
||||
# flatten output to 1d arrays suitable for path-graphics generation.
|
||||
x = np.broadcast_to(x[:, None], y.shape)
|
||||
x = (x + np.array(
|
||||
[-x_spacer, 0, 0, x_spacer]
|
||||
)).flatten()
|
||||
y = y.flatten()
|
||||
|
||||
return x, y
|
||||
|
||||
|
||||
class Renderer(msgspec.Struct):
|
||||
|
||||
flow: Flow
|
||||
|
||||
# called to render path graphics
|
||||
draw: Callable[np.ndarray, QPainterPath]
|
||||
draw_path: Callable[np.ndarray, QPainterPath]
|
||||
|
||||
# called on input data but before
|
||||
prerender_fn: Optional[Callable[ShmArray, np.ndarray]] = None
|
||||
# called on input data but before any graphics format
|
||||
# conversions or processing.
|
||||
data_t: Optional[Callable[ShmArray, np.ndarray]] = None
|
||||
data_t_shm: Optional[ShmArray] = None
|
||||
|
||||
# called on the final data (transform) output to convert
|
||||
# to "graphical data form" a format that can be passed to
|
||||
# the ``.draw()`` implementation.
|
||||
graphics_t: Optional[Callable[ShmArray, np.ndarray]] = None
|
||||
graphics_t_shm: Optional[ShmArray] = None
|
||||
|
||||
# path graphics update implementation methods
|
||||
prepend_fn: Optional[Callable[QPainterPath, QPainterPath]] = None
|
||||
append_fn: Optional[Callable[QPainterPath, QPainterPath]] = None
|
||||
|
||||
# last array view read
|
||||
last_read: Optional[np.ndarray] = None
|
||||
|
||||
# output graphics rendering
|
||||
# output graphics rendering, the main object
|
||||
# processed in ``QGraphicsObject.paint()``
|
||||
path: Optional[QPainterPath] = None
|
||||
|
||||
# def diff(
|
||||
|
@ -411,8 +613,10 @@ class Renderer(msgspec.Struct):
|
|||
def render(
|
||||
self,
|
||||
|
||||
new_read,
|
||||
|
||||
# only render datums "in view" of the ``ChartView``
|
||||
only_in_view: bool = True,
|
||||
only_in_view: bool = False,
|
||||
|
||||
) -> list[QPainterPath]:
|
||||
'''
|
||||
|
@ -428,28 +632,42 @@ class Renderer(msgspec.Struct):
|
|||
|
||||
'''
|
||||
# do full source data render to path
|
||||
xfirst, xlast, array, ivl, ivr, in_view = self.last_read
|
||||
last_read = (
|
||||
xfirst, xlast, array,
|
||||
ivl, ivr, in_view,
|
||||
) = self.last_read
|
||||
|
||||
if only_in_view:
|
||||
# get latest data from flow shm
|
||||
self.last_read = (
|
||||
xfirst, xlast, array, ivl, ivr, in_view
|
||||
) = self.flow.read()
|
||||
|
||||
array = in_view
|
||||
# # get latest data from flow shm
|
||||
# self.last_read = (
|
||||
# xfirst, xlast, array, ivl, ivr, in_view
|
||||
# ) = new_read
|
||||
|
||||
if self.path is None or in_view:
|
||||
if self.path is None or only_in_view:
|
||||
# redraw the entire source data if we have either of:
|
||||
# - no prior path graphic rendered or,
|
||||
# - we always intend to re-render the data only in view
|
||||
|
||||
if self.prerender_fn:
|
||||
array = self.prerender_fn(array)
|
||||
# data transform: convert source data to a format
|
||||
# expected to be incrementally updates and later rendered
|
||||
# to a more graphics native format.
|
||||
if self.data_t:
|
||||
array = self.data_t(array)
|
||||
|
||||
hist, last = array[:-1], array[-1]
|
||||
# maybe allocate shm for data transform output
|
||||
# if self.data_t_shm is None:
|
||||
# fshm = self.flow.shm
|
||||
|
||||
# call path render func on history
|
||||
self.path = self.draw(hist)
|
||||
# shm, opened = maybe_open_shm_array(
|
||||
# f'{self.flow.name}_data_t',
|
||||
# # TODO: create entry for each time frame
|
||||
# dtype=array.dtype,
|
||||
# readonly=False,
|
||||
# )
|
||||
# assert opened
|
||||
# shm.push(array)
|
||||
# self.data_t_shm = shm
|
||||
|
||||
elif self.path:
|
||||
print(f'inremental update not supported yet {self.flow.name}')
|
||||
|
@ -459,4 +677,10 @@ class Renderer(msgspec.Struct):
|
|||
# do path generation for each segment
|
||||
# and then push into graphics object.
|
||||
|
||||
hist, last = array[:-1], array[-1]
|
||||
|
||||
# call path render func on history
|
||||
self.path = self.draw_path(hist)
|
||||
|
||||
self.last_read = new_read
|
||||
return self.path, last
|
||||
|
|
Loading…
Reference in New Issue