WIP incremental render apis

incremental_update_paths
Tyler Goodlet 2022-04-16 15:22:11 -04:00
parent d0af280a59
commit 5a9bab0b69
4 changed files with 198 additions and 51 deletions

View File

@ -290,9 +290,9 @@ class FastAppendCurve(pg.GraphicsObject):
# XXX: lol brutal, the internals of `CurvePoint` (inherited by # XXX: lol brutal, the internals of `CurvePoint` (inherited by
# our `LineDot`) required ``.getData()`` to work.. # our `LineDot`) required ``.getData()`` to work..
self.xData = x # self.xData = x
self.yData = y # self.yData = y
self._x, self._y = x, y # self._x, self._y = x, y
if view_range: if view_range:
profiler(f'view range slice {view_range}') profiler(f'view range slice {view_range}')
@ -328,7 +328,7 @@ class FastAppendCurve(pg.GraphicsObject):
# x_last = x_iv[-1] # x_last = x_iv[-1]
# y_last = y_iv[-1] # y_last = y_iv[-1]
self._last_vr = view_range # self._last_vr = view_range
# self.disable_cache() # self.disable_cache()
# flip_cache = True # flip_cache = True

View File

@ -437,7 +437,6 @@ def graphics_update_cycle(
for curve_name, flow in vlm_chart._flows.items(): for curve_name, flow in vlm_chart._flows.items():
if not flow.render: if not flow.render:
print(f'skipping flow {curve_name}?')
continue continue
update_fsp_chart( update_fsp_chart(

View File

@ -22,6 +22,7 @@ graphics primitives with underlying FSP related data structures for fast
incremental update. incremental update.
''' '''
from __future__ import annotations
from typing import ( from typing import (
Optional, Optional,
Callable, Callable,
@ -36,8 +37,16 @@ from ..data._sharedmem import (
ShmArray, ShmArray,
# attach_shm_array # attach_shm_array
) )
from ._ohlc import BarItems from ._ohlc import (
BarItems,
gen_qpath,
)
from ._curve import (
FastAppendCurve,
)
from ._compression import (
ohlc_flatten,
)
# class FlowsTable(msgspec.Struct): # class FlowsTable(msgspec.Struct):
# ''' # '''
@ -48,6 +57,20 @@ from ._ohlc import BarItems
# ''' # '''
# flows: dict[str, np.ndarray] = {} # flows: dict[str, np.ndarray] = {}
# @classmethod
# def from_token(
# cls,
# shm_token: tuple[
# str,
# str,
# tuple[str, str],
# ],
# ) -> Renderer:
# shm = attach_shm_array(token)
# return cls(shm)
class Flow(msgspec.Struct): # , frozen=True): class Flow(msgspec.Struct): # , frozen=True):
''' '''
@ -61,16 +84,28 @@ class Flow(msgspec.Struct): # , frozen=True):
''' '''
name: str name: str
plot: pg.PlotItem plot: pg.PlotItem
graphics: pg.GraphicsObject
_shm: ShmArray
is_ohlc: bool = False is_ohlc: bool = False
render: bool = True # toggle for display loop render: bool = True # toggle for display loop
graphics: pg.GraphicsObject _last_uppx: float = 0
_in_ds: bool = False
_graphics_tranform_fn: Optional[Callable[ShmArray, np.ndarray]] = None
# map from uppx -> (downsampled data, incremental graphics)
_render_table: dict[
Optional[int],
tuple[Renderer, pg.GraphicsItem],
] = {}
# TODO: hackery to be able to set a shm later # TODO: hackery to be able to set a shm later
# but whilst also allowing this type to hashable, # but whilst also allowing this type to hashable,
# likely will require serializable token that is used to attach # likely will require serializable token that is used to attach
# to the underlying shm ref after startup? # to the underlying shm ref after startup?
_shm: Optional[ShmArray] = None # currently, may be filled in "later" # _shm: Optional[ShmArray] = None # currently, may be filled in "later"
# last read from shm (usually due to an update call) # last read from shm (usually due to an update call)
_last_read: Optional[np.ndarray] = None _last_read: Optional[np.ndarray] = None
@ -219,7 +254,7 @@ class Flow(msgspec.Struct): # , frozen=True):
''' '''
# shm read and slice to view # shm read and slice to view
xfirst, xlast, array, ivl, ivr, in_view = self.read() read = xfirst, xlast, array, ivl, ivr, in_view = self.read()
if ( if (
not in_view.size not in_view.size
@ -227,10 +262,48 @@ class Flow(msgspec.Struct): # , frozen=True):
): ):
return self.graphics return self.graphics
array_key = array_key or self.name
graphics = self.graphics graphics = self.graphics
if isinstance(graphics, BarItems): if isinstance(graphics, BarItems):
# ugh, not luvin dis, should we have just a designated
# instance var?
r = self._render_table.get('src')
if not r:
r = Renderer(
flow=self,
draw=gen_qpath, # TODO: rename this to something with ohlc
last_read=read,
)
self._render_table['src'] = (r, graphics)
ds_curve_r = Renderer(
flow=self,
draw=gen_qpath, # TODO: rename this to something with ohlc
last_read=read,
prerender_fn=ohlc_flatten,
)
# 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,
),
)
# 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
# update our pre-downsample-ready data and then pass that
# new data the downsampler algo for incremental update.
else:
# do incremental update
graphics.update_from_array( graphics.update_from_array(
array, array,
in_view, in_view,
@ -239,7 +312,55 @@ class Flow(msgspec.Struct): # , frozen=True):
**kwargs, **kwargs,
) )
# generate and apply path to graphics obj
graphics.path, last = r.render(only_in_view=True)
graphics.draw_last(last)
else: 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
array_key = array_key or self.name
graphics.update_from_array( graphics.update_from_array(
x=array['index'], x=array['index'],
y=array[array_key], y=array[array_key],
@ -253,51 +374,80 @@ class Flow(msgspec.Struct): # , frozen=True):
return graphics return graphics
# @classmethod
# def from_token(
# cls,
# shm_token: tuple[
# str,
# str,
# tuple[str, str],
# ],
# ) -> PathRenderer: class Renderer(msgspec.Struct):
# shm = attach_shm_array(token) flow: Flow
# return cls(shm)
# called to render path graphics
draw: Callable[np.ndarray, QPainterPath]
class PathRenderer(msgspec.Struct): # called on input data but before
prerender_fn: Optional[Callable[ShmArray, np.ndarray]] = None
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
path: Optional[QPainterPath] = None path: Optional[QPainterPath] = None
last_read_src_array: np.ndarray # def diff(
# called on input data but before # self,
prerender_fn: Callable[ShmArray, np.ndarray] # latest_read: tuple[np.ndarray],
def diff(
self,
) -> dict[str, np.ndarray]:
...
def update(self) -> QPainterPath:
'''
Incrementally update the internal path graphics from
updates in shm data and deliver the new (sub)-path
generated.
'''
...
# ) -> tuple[np.ndarray]:
# # blah blah blah
# # do diffing for prepend, append and last entry
# return (
# to_prepend
# to_append
# last,
# )
def render( def render(
self, self,
# only render datums "in view" of the ``ChartView``
only_in_view: bool = True,
) -> list[QPainterPath]: ) -> list[QPainterPath]:
''' '''
Render the current graphics path(s) Render the current graphics path(s)
''' '''
... # do full source data render to path
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
if self.path is None or 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)
hist, last = array[:-1], array[-1]
# call path render func on history
self.path = self.draw(hist)
elif self.path:
print(f'inremental update not supported yet {self.flow.name}')
# TODO: do incremental update
# prepend, append, last = self.diff(self.flow.read())
# do path generation for each segment
# and then push into graphics object.
return self.path, last

View File

@ -46,7 +46,7 @@ log = get_logger(__name__)
def bar_from_ohlc_row( def bar_from_ohlc_row(
row: np.ndarray, row: np.ndarray,
w: float w: float = 0.43
) -> tuple[QLineF]: ) -> tuple[QLineF]:
''' '''
@ -158,8 +158,9 @@ def path_arrays_from_ohlc(
def gen_qpath( def gen_qpath(
data: np.ndarray, data: np.ndarray,
start: int, # XXX: do we need this? start: int = 0, # XXX: do we need this?
w: float, # 0.5 is no overlap between arms, 1.0 is full overlap
w: float = 0.43,
path: Optional[QtGui.QPainterPath] = None, path: Optional[QtGui.QPainterPath] = None,
) -> QtGui.QPainterPath: ) -> QtGui.QPainterPath:
@ -310,7 +311,7 @@ class BarItems(pg.GraphicsObject):
self._pi.addItem(curve) self._pi.addItem(curve)
self._ds_line = curve self._ds_line = curve
self._ds_xrange = (index[0], index[-1]) # self._ds_xrange = (index[0], index[-1])
# trigger render # trigger render
# https://doc.qt.io/qt-5/qgraphicsitem.html#update # https://doc.qt.io/qt-5/qgraphicsitem.html#update
@ -358,7 +359,7 @@ class BarItems(pg.GraphicsObject):
# index = self.start_index # index = self.start_index
istart, istop = self._xrange istart, istop = self._xrange
ds_istart, ds_istop = self._ds_xrange # ds_istart, ds_istop = self._ds_xrange
index = ohlc['index'] index = ohlc['index']
first_index, last_index = index[0], index[-1] first_index, last_index = index[0], index[-1]
@ -435,9 +436,6 @@ class BarItems(pg.GraphicsObject):
# stop here since we don't need to update bars path any more # stop here since we don't need to update bars path any more
# as we delegate to the downsample line with updates. # as we delegate to the downsample line with updates.
profiler.finish()
# print('terminating early')
return
else: else:
# we should be in bars mode # we should be in bars mode
@ -606,7 +604,7 @@ class BarItems(pg.GraphicsObject):
if flip_cache: if flip_cache:
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
profiler.finish() # profiler.finish()
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