Move graphics update logic into `Renderer.render()`
Finally this gets us much closer to a generic incremental update system for graphics wherein the input array diffing, pre-graphical format data processing, downsampler activation and incremental update and storage of any of these data flow stages can be managed in one modular sub-system :surfer_boi:. Dirty deatz: - reorg and move all path logic into `Renderer.render()` and have it take in pretty much the same flags as the old `FastAppendCurve.update_from_array()` and instead storing all update state vars (even copies of the downsampler related ones) on the renderer instance: - new state vars: `._last_uppx, ._in_ds, ._vr, ._avr` - `.render()` input bools: `new_sample_rate, should_redraw, should_ds, showing_src_data` - add a hack-around for passing in incremental update data (for now) via a `input_data: tuple` of numpy arrays - a default `uppx: float = 1` - add new render interface attrs: - `.format_xy()` which takes in the source data array and produces out x, y arrays (and maybe a `connect` array) that can be passed to `.draw_path()` (the default for this is just to slice out the index and `array_key: str` columns from the input struct array), - `.draw_path()` which takes in the x, y, connect arrays and generates a `QPainterPath` - `.fast_path`, for "appendable" updates like there was on the fast append curve - move redraw (aka `.clear()` calls) into `.draw_path()` and trigger via `redraw: bool` flag. - our graphics objects no longer set their own `.path` state, it's done by the `Flow.update_graphics()` method using output from `Renderer.render()` (and it's state if necessary)incremental_update_paths
parent
aa0efe1523
commit
167ae96566
|
@ -27,6 +27,7 @@ from __future__ import annotations
|
||||||
from typing import (
|
from typing import (
|
||||||
Optional,
|
Optional,
|
||||||
Callable,
|
Callable,
|
||||||
|
Union,
|
||||||
)
|
)
|
||||||
|
|
||||||
import msgspec
|
import msgspec
|
||||||
|
@ -144,12 +145,13 @@ def render_baritems(
|
||||||
# if no source data renderer exists create one.
|
# if no source data renderer exists create one.
|
||||||
self = flow
|
self = flow
|
||||||
r = self._src_r
|
r = self._src_r
|
||||||
|
show_bars: bool = False
|
||||||
if not r:
|
if not r:
|
||||||
|
show_bars = True
|
||||||
# OHLC bars path renderer
|
# OHLC bars path renderer
|
||||||
r = self._src_r = Renderer(
|
r = self._src_r = Renderer(
|
||||||
flow=self,
|
flow=self,
|
||||||
# TODO: rename this to something with ohlc
|
format_xy=gen_ohlc_qpath,
|
||||||
draw_path=gen_ohlc_qpath,
|
|
||||||
last_read=read,
|
last_read=read,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -292,20 +294,28 @@ def render_baritems(
|
||||||
# # do_append=uppx < 16,
|
# # do_append=uppx < 16,
|
||||||
# **kwargs,
|
# **kwargs,
|
||||||
# )
|
# )
|
||||||
# curve.draw_last(x, y)
|
curve.draw_last(x, y)
|
||||||
curve.show()
|
curve.show()
|
||||||
profiler('updated ds curve')
|
profiler('updated ds curve')
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# render incremental or in-view update
|
# render incremental or in-view update
|
||||||
# and apply ouput (path) to graphics.
|
# and apply ouput (path) to graphics.
|
||||||
path, last = r.render(
|
path, data = r.render(
|
||||||
read,
|
read,
|
||||||
only_in_view=True,
|
'ohlc',
|
||||||
|
profiler=profiler,
|
||||||
|
# uppx=1,
|
||||||
|
use_vr=True,
|
||||||
|
# graphics=graphics,
|
||||||
|
# should_redraw=True, # always
|
||||||
)
|
)
|
||||||
|
assert path
|
||||||
|
|
||||||
graphics.path = path
|
graphics.path = path
|
||||||
graphics.draw_last(last)
|
graphics.draw_last(data[-1])
|
||||||
|
if show_bars:
|
||||||
|
graphics.show()
|
||||||
|
|
||||||
# NOTE: on appends we used to have to flip the coords
|
# NOTE: on appends we used to have to flip the coords
|
||||||
# cache thought it doesn't seem to be required any more?
|
# cache thought it doesn't seem to be required any more?
|
||||||
|
@ -699,6 +709,7 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
xfirst, xlast, array,
|
xfirst, xlast, array,
|
||||||
ivl, ivr, in_view,
|
ivl, ivr, in_view,
|
||||||
) = self.read()
|
) = self.read()
|
||||||
|
|
||||||
profiler('read src shm data')
|
profiler('read src shm data')
|
||||||
|
|
||||||
graphics = self.graphics
|
graphics = self.graphics
|
||||||
|
@ -709,8 +720,13 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
):
|
):
|
||||||
return graphics
|
return graphics
|
||||||
|
|
||||||
|
draw_last: bool = True
|
||||||
|
slice_to_head: int = -1
|
||||||
|
input_data = None
|
||||||
|
|
||||||
out: Optional[tuple] = None
|
out: Optional[tuple] = None
|
||||||
if isinstance(graphics, BarItems):
|
if isinstance(graphics, BarItems):
|
||||||
|
draw_last = False
|
||||||
# XXX: special case where we change out graphics
|
# XXX: special case where we change out graphics
|
||||||
# to a line after a certain uppx threshold.
|
# to a line after a certain uppx threshold.
|
||||||
# render_baritems(
|
# render_baritems(
|
||||||
|
@ -741,14 +757,6 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
array_key = array_key or self.name
|
array_key = array_key or self.name
|
||||||
shm = self.shm
|
shm = self.shm
|
||||||
|
|
||||||
# update config
|
|
||||||
new_sample_rate = False
|
|
||||||
should_ds = self._in_ds
|
|
||||||
showing_src_data = self._in_ds
|
|
||||||
draw_last: bool = True
|
|
||||||
slice_to_head: int = -1
|
|
||||||
should_redraw: bool = False
|
|
||||||
|
|
||||||
if out is not None:
|
if out is not None:
|
||||||
# hack to handle ds curve from bars above
|
# hack to handle ds curve from bars above
|
||||||
(
|
(
|
||||||
|
@ -758,32 +766,60 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
x_iv,
|
x_iv,
|
||||||
y_iv,
|
y_iv,
|
||||||
) = out
|
) = out
|
||||||
|
input_data = out[1:]
|
||||||
|
# breakpoint()
|
||||||
|
|
||||||
else:
|
# ds update config
|
||||||
|
new_sample_rate: bool = False
|
||||||
|
should_redraw: bool = False
|
||||||
|
should_ds: bool = r._in_ds
|
||||||
|
showing_src_data: bool = not r._in_ds
|
||||||
|
|
||||||
|
# downsampling incremental state checking
|
||||||
|
# check for and set std m4 downsample conditions
|
||||||
|
uppx = graphics.x_uppx()
|
||||||
|
uppx_diff = (uppx - self._last_uppx)
|
||||||
|
profiler(f'diffed uppx {uppx}')
|
||||||
|
if (
|
||||||
|
uppx > 1
|
||||||
|
and abs(uppx_diff) >= 1
|
||||||
|
):
|
||||||
|
log.info(
|
||||||
|
f'{array_key} sampler change: {self._last_uppx} -> {uppx}'
|
||||||
|
)
|
||||||
|
self._last_uppx = uppx
|
||||||
|
new_sample_rate = True
|
||||||
|
showing_src_data = False
|
||||||
|
should_redraw = True
|
||||||
|
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
|
||||||
|
new_sample_rate = True
|
||||||
|
should_ds = False
|
||||||
|
showing_src_data = True
|
||||||
|
|
||||||
|
if graphics._step_mode:
|
||||||
|
slice_to_head = -2
|
||||||
|
|
||||||
|
# TODO: remove this and instead place all step curve
|
||||||
|
# updating into pre-path data render callbacks.
|
||||||
# full input data
|
# full input data
|
||||||
x = array['index']
|
x = array['index']
|
||||||
y = array[array_key]
|
y = array[array_key]
|
||||||
|
x_last = x[-1]
|
||||||
|
y_last = y[-1]
|
||||||
|
|
||||||
# inview data
|
# inview data
|
||||||
x_iv = in_view['index']
|
x_iv = in_view['index']
|
||||||
y_iv = in_view[array_key]
|
y_iv = in_view[array_key]
|
||||||
|
|
||||||
# downsampling incremental state checking
|
|
||||||
uppx = graphics.x_uppx()
|
|
||||||
# px_width = graphics.px_width()
|
|
||||||
uppx_diff = (uppx - self._last_uppx)
|
|
||||||
profiler(f'diffed uppx {uppx}')
|
|
||||||
|
|
||||||
x_last = x[-1]
|
|
||||||
y_last = y[-1]
|
|
||||||
|
|
||||||
slice_to_head = -1
|
|
||||||
|
|
||||||
profiler('sliced input arrays')
|
|
||||||
|
|
||||||
if graphics._step_mode:
|
|
||||||
slice_to_head = -2
|
|
||||||
|
|
||||||
if self.gy is None:
|
if self.gy is None:
|
||||||
(
|
(
|
||||||
self._iflat_first,
|
self._iflat_first,
|
||||||
|
@ -824,261 +860,104 @@ class Flow(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
should_redraw = bool(append_diff)
|
should_redraw = bool(append_diff)
|
||||||
draw_last = False
|
draw_last = False
|
||||||
|
input_data = (
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
x_iv,
|
||||||
|
y_iv,
|
||||||
|
)
|
||||||
|
|
||||||
# compute the length diffs between the first/last index entry in
|
# compute the length diffs between the first/last index entry in
|
||||||
# the input data and the last indexes we have on record from the
|
# the input data and the last indexes we have on record from the
|
||||||
# last time we updated the curve index.
|
# last time we updated the curve index.
|
||||||
prepend_length, append_length = r.diff(read)
|
# prepend_length, append_length = r.diff(read)
|
||||||
|
|
||||||
# MAIN RENDER LOGIC:
|
# MAIN RENDER LOGIC:
|
||||||
# - determine in view data and redraw on range change
|
# - determine in view data and redraw on range change
|
||||||
# - determine downsampling ops if needed
|
# - determine downsampling ops if needed
|
||||||
# - (incrementally) update ``QPainterPath``
|
# - (incrementally) update ``QPainterPath``
|
||||||
|
|
||||||
path = graphics.path
|
# path = graphics.path
|
||||||
fast_path = graphics.fast_path
|
# fast_path = graphics.fast_path
|
||||||
|
|
||||||
if (
|
path, data = r.render(
|
||||||
use_vr
|
read,
|
||||||
):
|
array_key,
|
||||||
|
profiler,
|
||||||
|
uppx=uppx,
|
||||||
|
input_data=input_data,
|
||||||
|
# use_vr=True,
|
||||||
|
|
||||||
# if a view range is passed, plan to draw the
|
# TODO: better way to detect and pass this?
|
||||||
# source ouput that's "in view" of the chart.
|
# if we want to eventually cache renderers for a given uppx
|
||||||
view_range = (ivl, ivr)
|
# we should probably use this as a key + state?
|
||||||
# print(f'{self._name} vr: {view_range}')
|
should_redraw=should_redraw,
|
||||||
|
new_sample_rate=new_sample_rate,
|
||||||
|
should_ds=should_ds,
|
||||||
|
showing_src_data=showing_src_data,
|
||||||
|
|
||||||
# by default we only pull data up to the last (current) index
|
slice_to_head=slice_to_head,
|
||||||
x_out = x_iv[:slice_to_head]
|
do_append=do_append,
|
||||||
y_out = y_iv[:slice_to_head]
|
graphics=graphics,
|
||||||
profiler(f'view range slice {view_range}')
|
|
||||||
|
|
||||||
vl, vr = view_range
|
|
||||||
|
|
||||||
zoom_or_append = False
|
|
||||||
last_vr = self._vr
|
|
||||||
last_ivr = self._avr
|
|
||||||
|
|
||||||
# incremental in-view data update.
|
|
||||||
if last_vr:
|
|
||||||
# relative slice indices
|
|
||||||
lvl, lvr = last_vr
|
|
||||||
# abs slice indices
|
|
||||||
al, ar = last_ivr
|
|
||||||
|
|
||||||
# left_change = abs(x_iv[0] - al) >= 1
|
|
||||||
# right_change = abs(x_iv[-1] - ar) >= 1
|
|
||||||
|
|
||||||
if (
|
|
||||||
# likely a zoom view change
|
|
||||||
(vr - lvr) > 2 or vl < lvl
|
|
||||||
# append / prepend update
|
|
||||||
# we had an append update where the view range
|
|
||||||
# didn't change but the data-viewed (shifted)
|
|
||||||
# underneath, so we need to redraw.
|
|
||||||
# or left_change and right_change and last_vr == view_range
|
|
||||||
|
|
||||||
# not (left_change and right_change) and ivr
|
|
||||||
# (
|
|
||||||
# or abs(x_iv[ivr] - livr) > 1
|
|
||||||
):
|
|
||||||
zoom_or_append = True
|
|
||||||
|
|
||||||
if (
|
|
||||||
view_range != last_vr
|
|
||||||
and (
|
|
||||||
append_length > 1
|
|
||||||
or zoom_or_append
|
|
||||||
)
|
)
|
||||||
):
|
# graphics.prepareGeometryChange()
|
||||||
should_redraw = True
|
# assign output paths to graphicis obj
|
||||||
# print("REDRAWING BRUH")
|
graphics.path = r.path
|
||||||
|
graphics.fast_path = r.fast_path
|
||||||
self._vr = view_range
|
|
||||||
self._avr = x_iv[0], x_iv[slice_to_head]
|
|
||||||
|
|
||||||
if prepend_length > 0:
|
|
||||||
should_redraw = True
|
|
||||||
|
|
||||||
# check for and set std m4 downsample conditions
|
|
||||||
if (
|
|
||||||
abs(uppx_diff) >= 1
|
|
||||||
):
|
|
||||||
log.info(
|
|
||||||
f'{array_key} sampler change: {self._last_uppx} -> {uppx}'
|
|
||||||
)
|
|
||||||
self._last_uppx = uppx
|
|
||||||
new_sample_rate = True
|
|
||||||
showing_src_data = False
|
|
||||||
should_redraw = True
|
|
||||||
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
|
|
||||||
new_sample_rate = True
|
|
||||||
should_ds = False
|
|
||||||
showing_src_data = True
|
|
||||||
|
|
||||||
if (
|
|
||||||
path is None
|
|
||||||
or should_redraw
|
|
||||||
or new_sample_rate
|
|
||||||
or prepend_length > 0
|
|
||||||
):
|
|
||||||
if should_redraw:
|
|
||||||
if path:
|
|
||||||
path.clear()
|
|
||||||
profiler('cleared paths due to `should_redraw=True`')
|
|
||||||
|
|
||||||
if fast_path:
|
|
||||||
fast_path.clear()
|
|
||||||
|
|
||||||
profiler('cleared paths due to `should_redraw` set')
|
|
||||||
|
|
||||||
if new_sample_rate and showing_src_data:
|
|
||||||
# if self._in_ds:
|
|
||||||
log.info(f'DEDOWN -> {self.name}')
|
|
||||||
|
|
||||||
self._in_ds = False
|
|
||||||
|
|
||||||
elif should_ds and uppx > 1:
|
|
||||||
|
|
||||||
x_out, y_out = xy_downsample(
|
|
||||||
x_out,
|
|
||||||
y_out,
|
|
||||||
uppx,
|
|
||||||
)
|
|
||||||
profiler(f'FULL PATH downsample redraw={should_ds}')
|
|
||||||
self._in_ds = True
|
|
||||||
|
|
||||||
path = pg.functions.arrayToQPath(
|
|
||||||
x_out,
|
|
||||||
y_out,
|
|
||||||
connect='all',
|
|
||||||
finiteCheck=False,
|
|
||||||
path=path,
|
|
||||||
)
|
|
||||||
graphics.prepareGeometryChange()
|
|
||||||
profiler(
|
|
||||||
'generated fresh path. '
|
|
||||||
f'(should_redraw: {should_redraw} '
|
|
||||||
f'should_ds: {should_ds} new_sample_rate: {new_sample_rate})'
|
|
||||||
)
|
|
||||||
# profiler(f'DRAW PATH IN VIEW -> {self.name}')
|
|
||||||
|
|
||||||
# reserve mem allocs see:
|
|
||||||
# - https://doc.qt.io/qt-5/qpainterpath.html#reserve
|
|
||||||
# - https://doc.qt.io/qt-5/qpainterpath.html#capacity
|
|
||||||
# - https://doc.qt.io/qt-5/qpainterpath.html#clear
|
|
||||||
# XXX: right now this is based on had hoc checks on a
|
|
||||||
# hidpi 3840x2160 4k monitor but we should optimize for
|
|
||||||
# the target display(s) on the sys.
|
|
||||||
# if no_path_yet:
|
|
||||||
# graphics.path.reserve(int(500e3))
|
|
||||||
|
|
||||||
# TODO: get this piecewise prepend working - right now it's
|
|
||||||
# giving heck on vwap...
|
|
||||||
# elif prepend_length:
|
|
||||||
# breakpoint()
|
|
||||||
|
|
||||||
# prepend_path = pg.functions.arrayToQPath(
|
|
||||||
# x[0:prepend_length],
|
|
||||||
# y[0:prepend_length],
|
|
||||||
# connect='all'
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # swap prepend path in "front"
|
|
||||||
# old_path = graphics.path
|
|
||||||
# graphics.path = prepend_path
|
|
||||||
# # graphics.path.moveTo(new_x[0], new_y[0])
|
|
||||||
# graphics.path.connectPath(old_path)
|
|
||||||
|
|
||||||
elif (
|
|
||||||
append_length > 0
|
|
||||||
and do_append
|
|
||||||
and not should_redraw
|
|
||||||
):
|
|
||||||
print(f'{self.name} append len: {append_length}')
|
|
||||||
new_x = x[-append_length - 2:slice_to_head]
|
|
||||||
new_y = y[-append_length - 2:slice_to_head]
|
|
||||||
profiler('sliced append path')
|
|
||||||
|
|
||||||
profiler(
|
|
||||||
f'diffed array input, append_length={append_length}'
|
|
||||||
)
|
|
||||||
|
|
||||||
# if should_ds:
|
|
||||||
# new_x, new_y = xy_downsample(
|
|
||||||
# new_x,
|
|
||||||
# new_y,
|
|
||||||
# px_width,
|
|
||||||
# uppx,
|
|
||||||
# )
|
|
||||||
# profiler(f'fast path downsample redraw={should_ds}')
|
|
||||||
|
|
||||||
append_path = pg.functions.arrayToQPath(
|
|
||||||
new_x,
|
|
||||||
new_y,
|
|
||||||
connect='all',
|
|
||||||
finiteCheck=False,
|
|
||||||
path=fast_path,
|
|
||||||
)
|
|
||||||
profiler('generated append qpath')
|
|
||||||
|
|
||||||
if graphics.use_fpath:
|
|
||||||
print("USING FPATH")
|
|
||||||
# an attempt at trying to make append-updates faster..
|
|
||||||
if fast_path is None:
|
|
||||||
fast_path = append_path
|
|
||||||
# fast_path.reserve(int(6e3))
|
|
||||||
else:
|
|
||||||
fast_path.connectPath(append_path)
|
|
||||||
size = fast_path.capacity()
|
|
||||||
profiler(f'connected fast path w size: {size}')
|
|
||||||
|
|
||||||
# print(f"append_path br: {append_path.boundingRect()}")
|
|
||||||
# graphics.path.moveTo(new_x[0], new_y[0])
|
|
||||||
# path.connectPath(append_path)
|
|
||||||
|
|
||||||
# XXX: lol this causes a hang..
|
|
||||||
# graphics.path = graphics.path.simplified()
|
|
||||||
else:
|
|
||||||
size = graphics.path.capacity()
|
|
||||||
profiler(f'connected history path w size: {size}')
|
|
||||||
graphics.path.connectPath(append_path)
|
|
||||||
|
|
||||||
if draw_last:
|
if draw_last:
|
||||||
|
x = data['index']
|
||||||
|
y = data[array_key]
|
||||||
graphics.draw_last(x, y)
|
graphics.draw_last(x, y)
|
||||||
profiler('draw last segment')
|
profiler('draw last segment')
|
||||||
|
|
||||||
graphics.update()
|
graphics.update()
|
||||||
profiler('.update()')
|
profiler('.update()')
|
||||||
|
|
||||||
# assign output paths to graphicis obj
|
|
||||||
graphics.path = path
|
|
||||||
graphics.fast_path = fast_path
|
|
||||||
|
|
||||||
profiler('`graphics.update_from_array()` complete')
|
profiler('`graphics.update_from_array()` complete')
|
||||||
return graphics
|
return graphics
|
||||||
|
|
||||||
|
|
||||||
|
def by_index_and_key(
|
||||||
|
array: np.ndarray,
|
||||||
|
array_key: str,
|
||||||
|
|
||||||
|
) -> tuple[
|
||||||
|
np.ndarray,
|
||||||
|
np.ndarray,
|
||||||
|
np.ndarray,
|
||||||
|
]:
|
||||||
|
# full input data
|
||||||
|
x = array['index']
|
||||||
|
y = array[array_key]
|
||||||
|
|
||||||
|
# # inview data
|
||||||
|
# x_iv = in_view['index']
|
||||||
|
# y_iv = in_view[array_key]
|
||||||
|
|
||||||
|
return tuple({
|
||||||
|
'x': x,
|
||||||
|
'y': y,
|
||||||
|
# 'x_iv': x_iv,
|
||||||
|
# 'y_iv': y_iv,
|
||||||
|
'connect': 'all',
|
||||||
|
}.values())
|
||||||
|
|
||||||
|
|
||||||
class Renderer(msgspec.Struct):
|
class Renderer(msgspec.Struct):
|
||||||
|
|
||||||
flow: Flow
|
flow: Flow
|
||||||
# last array view read
|
# last array view read
|
||||||
last_read: Optional[tuple] = None
|
last_read: Optional[tuple] = None
|
||||||
|
format_xy: Callable[np.ndarray, tuple[np.ndarray]] = by_index_and_key
|
||||||
|
|
||||||
# called to render path graphics
|
# called to render path graphics
|
||||||
draw_path: Optional[Callable[np.ndarray, QPainterPath]] = None
|
# draw_path: Optional[Callable[np.ndarray, QPainterPath]] = None
|
||||||
|
|
||||||
# output graphics rendering, the main object
|
# output graphics rendering, the main object
|
||||||
# processed in ``QGraphicsObject.paint()``
|
# processed in ``QGraphicsObject.paint()``
|
||||||
path: Optional[QPainterPath] = None
|
path: Optional[QPainterPath] = None
|
||||||
|
fast_path: Optional[QPainterPath] = None
|
||||||
|
|
||||||
# called on input data but before any graphics format
|
# called on input data but before any graphics format
|
||||||
# conversions or processing.
|
# conversions or processing.
|
||||||
|
@ -1095,12 +974,18 @@ class Renderer(msgspec.Struct):
|
||||||
prepend_fn: Optional[Callable[QPainterPath, QPainterPath]] = None
|
prepend_fn: Optional[Callable[QPainterPath, QPainterPath]] = None
|
||||||
append_fn: Optional[Callable[QPainterPath, QPainterPath]] = None
|
append_fn: Optional[Callable[QPainterPath, QPainterPath]] = None
|
||||||
|
|
||||||
|
# downsampling state
|
||||||
|
_last_uppx: float = 0
|
||||||
|
_in_ds: bool = False
|
||||||
|
|
||||||
# incremental update state(s)
|
# incremental update state(s)
|
||||||
# _in_ds: bool = False
|
|
||||||
# _last_uppx: float = 0
|
|
||||||
_last_vr: Optional[tuple[float, float]] = None
|
_last_vr: Optional[tuple[float, float]] = None
|
||||||
_last_ivr: Optional[tuple[float, float]] = None
|
_last_ivr: Optional[tuple[float, float]] = None
|
||||||
|
|
||||||
|
# view-range incremental state
|
||||||
|
_vr: Optional[tuple] = None
|
||||||
|
_avr: Optional[tuple] = None
|
||||||
|
|
||||||
def diff(
|
def diff(
|
||||||
self,
|
self,
|
||||||
new_read: tuple[np.ndarray],
|
new_read: tuple[np.ndarray],
|
||||||
|
@ -1146,23 +1031,80 @@ class Renderer(msgspec.Struct):
|
||||||
# last,
|
# last,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# def gen_path_data(
|
||||||
|
# self,
|
||||||
|
# redraw: bool = False,
|
||||||
|
# ) -> np.ndarray:
|
||||||
|
# ...
|
||||||
|
|
||||||
def draw_path(
|
def draw_path(
|
||||||
self,
|
self,
|
||||||
should_redraw: bool = False,
|
x: np.ndarray,
|
||||||
|
y: np.ndarray,
|
||||||
|
connect: Union[str, np.ndarray] = 'all',
|
||||||
|
path: Optional[QPainterPath] = None,
|
||||||
|
redraw: bool = False,
|
||||||
|
|
||||||
) -> QPainterPath:
|
) -> QPainterPath:
|
||||||
|
|
||||||
if should_redraw:
|
path_was_none = path is None
|
||||||
if self.path:
|
|
||||||
self.path.clear()
|
if redraw and path:
|
||||||
|
path.clear()
|
||||||
|
|
||||||
|
# TODO: avoid this?
|
||||||
|
if self.fast_path:
|
||||||
|
self.fast_path.clear()
|
||||||
|
|
||||||
# profiler('cleared paths due to `should_redraw=True`')
|
# profiler('cleared paths due to `should_redraw=True`')
|
||||||
|
|
||||||
|
path = pg.functions.arrayToQPath(
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
connect=connect,
|
||||||
|
finiteCheck=False,
|
||||||
|
|
||||||
|
# reserve mem allocs see:
|
||||||
|
# - https://doc.qt.io/qt-5/qpainterpath.html#reserve
|
||||||
|
# - https://doc.qt.io/qt-5/qpainterpath.html#capacity
|
||||||
|
# - https://doc.qt.io/qt-5/qpainterpath.html#clear
|
||||||
|
# XXX: right now this is based on had hoc checks on a
|
||||||
|
# hidpi 3840x2160 4k monitor but we should optimize for
|
||||||
|
# the target display(s) on the sys.
|
||||||
|
# if no_path_yet:
|
||||||
|
# graphics.path.reserve(int(500e3))
|
||||||
|
path=path, # path re-use / reserving
|
||||||
|
)
|
||||||
|
|
||||||
|
# avoid mem allocs if possible
|
||||||
|
if path_was_none:
|
||||||
|
path.reserve(path.capacity())
|
||||||
|
|
||||||
|
return path
|
||||||
|
|
||||||
def render(
|
def render(
|
||||||
self,
|
self,
|
||||||
|
|
||||||
new_read,
|
new_read,
|
||||||
|
array_key: str,
|
||||||
|
profiler: pg.debug.Profiler,
|
||||||
|
uppx: float = 1,
|
||||||
|
|
||||||
|
input_data: Optional[tuple[np.ndarray]] = None,
|
||||||
|
|
||||||
|
# redraw and ds flags
|
||||||
|
should_redraw: bool = True,
|
||||||
|
new_sample_rate: bool = False,
|
||||||
|
should_ds: bool = False,
|
||||||
|
showing_src_data: bool = True,
|
||||||
|
|
||||||
|
do_append: bool = True,
|
||||||
|
slice_to_head: int = -1,
|
||||||
|
use_fpath: bool = True,
|
||||||
|
|
||||||
# only render datums "in view" of the ``ChartView``
|
# only render datums "in view" of the ``ChartView``
|
||||||
only_in_view: bool = False,
|
use_vr: bool = True,
|
||||||
|
graphics: Optional[pg.GraphicObject] = None,
|
||||||
|
|
||||||
) -> list[QPainterPath]:
|
) -> list[QPainterPath]:
|
||||||
'''
|
'''
|
||||||
|
@ -1177,8 +1119,6 @@ class Renderer(msgspec.Struct):
|
||||||
- blah blah blah (from notes)
|
- blah blah blah (from notes)
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# get graphics info
|
|
||||||
|
|
||||||
# TODO: can the renderer just call ``Flow.read()`` directly?
|
# TODO: can the renderer just call ``Flow.read()`` directly?
|
||||||
# unpack latest source data read
|
# unpack latest source data read
|
||||||
(
|
(
|
||||||
|
@ -1190,29 +1130,268 @@ class Renderer(msgspec.Struct):
|
||||||
in_view,
|
in_view,
|
||||||
) = new_read
|
) = new_read
|
||||||
|
|
||||||
|
if use_vr:
|
||||||
|
array = in_view
|
||||||
|
|
||||||
|
if input_data:
|
||||||
|
# allow input data passing for now from alt curve updaters.
|
||||||
|
(
|
||||||
|
x_out,
|
||||||
|
y_out,
|
||||||
|
x_iv,
|
||||||
|
y_iv,
|
||||||
|
) = input_data
|
||||||
|
connect = 'all'
|
||||||
|
|
||||||
|
if use_vr:
|
||||||
|
x_out = x_iv
|
||||||
|
y_out = y_iv
|
||||||
|
|
||||||
|
# last = y_out[slice_to_head]
|
||||||
|
|
||||||
|
else:
|
||||||
|
hist = array[:slice_to_head]
|
||||||
|
# last = array[slice_to_head]
|
||||||
|
|
||||||
|
(
|
||||||
|
x_out,
|
||||||
|
y_out,
|
||||||
|
# x_iv,
|
||||||
|
# y_iv,
|
||||||
|
connect,
|
||||||
|
) = self.format_xy(hist, array_key)
|
||||||
|
|
||||||
|
# print(f'{array_key} len x,y: {(len(x_out), len(y_out))}')
|
||||||
|
# # full input data
|
||||||
|
# x = array['index']
|
||||||
|
# y = array[array_key]
|
||||||
|
|
||||||
|
# # inview data
|
||||||
|
# x_iv = in_view['index']
|
||||||
|
# y_iv = in_view[array_key]
|
||||||
|
|
||||||
|
profiler('sliced input arrays')
|
||||||
|
|
||||||
(
|
(
|
||||||
prepend_length,
|
prepend_length,
|
||||||
append_length,
|
append_length,
|
||||||
) = self.diff(new_read)
|
) = self.diff(new_read)
|
||||||
|
|
||||||
# do full source data render to path
|
if (
|
||||||
|
use_vr
|
||||||
|
):
|
||||||
|
# if a view range is passed, plan to draw the
|
||||||
|
# source ouput that's "in view" of the chart.
|
||||||
|
view_range = (ivl, ivr)
|
||||||
|
# print(f'{self._name} vr: {view_range}')
|
||||||
|
|
||||||
# x = array['index']
|
# by default we only pull data up to the last (current) index
|
||||||
# y = array#[array_key]
|
# x_out = x_iv[:slice_to_head]
|
||||||
# x_iv = in_view['index']
|
# y_out = y_iv[:slice_to_head]
|
||||||
# y_iv = in_view#[array_key]
|
|
||||||
|
|
||||||
if only_in_view:
|
profiler(f'view range slice {view_range}')
|
||||||
array = in_view
|
|
||||||
|
vl, vr = view_range
|
||||||
|
|
||||||
|
zoom_or_append = False
|
||||||
|
last_vr = self._vr
|
||||||
|
last_ivr = self._avr
|
||||||
|
|
||||||
|
# incremental in-view data update.
|
||||||
|
if last_vr:
|
||||||
|
# relative slice indices
|
||||||
|
lvl, lvr = last_vr
|
||||||
|
# abs slice indices
|
||||||
|
al, ar = last_ivr
|
||||||
|
|
||||||
|
# left_change = abs(x_iv[0] - al) >= 1
|
||||||
|
# right_change = abs(x_iv[-1] - ar) >= 1
|
||||||
|
|
||||||
|
if (
|
||||||
|
# likely a zoom view change
|
||||||
|
(vr - lvr) > 2 or vl < lvl
|
||||||
|
# append / prepend update
|
||||||
|
# we had an append update where the view range
|
||||||
|
# didn't change but the data-viewed (shifted)
|
||||||
|
# underneath, so we need to redraw.
|
||||||
|
# or left_change and right_change and last_vr == view_range
|
||||||
|
|
||||||
|
# not (left_change and right_change) and ivr
|
||||||
|
# (
|
||||||
|
# or abs(x_iv[ivr] - livr) > 1
|
||||||
|
):
|
||||||
|
zoom_or_append = True
|
||||||
|
|
||||||
|
if (
|
||||||
|
view_range != last_vr
|
||||||
|
and (
|
||||||
|
append_length > 1
|
||||||
|
or zoom_or_append
|
||||||
|
)
|
||||||
|
):
|
||||||
|
should_redraw = True
|
||||||
|
# print("REDRAWING BRUH")
|
||||||
|
|
||||||
|
self._vr = view_range
|
||||||
|
if len(x_out):
|
||||||
|
self._avr = x_out[0], x_out[slice_to_head]
|
||||||
|
|
||||||
|
if prepend_length > 0:
|
||||||
|
should_redraw = True
|
||||||
|
|
||||||
|
# # last datums
|
||||||
|
# x_last = x_out[-1]
|
||||||
|
# y_last = y_out[-1]
|
||||||
|
|
||||||
|
path = self.path
|
||||||
|
fast_path = self.fast_path
|
||||||
|
|
||||||
|
if (
|
||||||
|
path is None
|
||||||
|
or should_redraw
|
||||||
|
or new_sample_rate
|
||||||
|
or prepend_length > 0
|
||||||
|
):
|
||||||
|
# if should_redraw:
|
||||||
|
# if path:
|
||||||
|
# path.clear()
|
||||||
|
# profiler('cleared paths due to `should_redraw=True`')
|
||||||
|
|
||||||
|
# if fast_path:
|
||||||
|
# fast_path.clear()
|
||||||
|
|
||||||
|
# profiler('cleared paths due to `should_redraw` set')
|
||||||
|
|
||||||
|
if new_sample_rate and showing_src_data:
|
||||||
|
# if self._in_ds:
|
||||||
|
log.info(f'DEDOWN -> {array_key}')
|
||||||
|
|
||||||
|
self._in_ds = False
|
||||||
|
|
||||||
|
elif should_ds and uppx > 1:
|
||||||
|
|
||||||
|
x_out, y_out = xy_downsample(
|
||||||
|
x_out,
|
||||||
|
y_out,
|
||||||
|
uppx,
|
||||||
|
)
|
||||||
|
profiler(f'FULL PATH downsample redraw={should_ds}')
|
||||||
|
self._in_ds = True
|
||||||
|
# else:
|
||||||
|
# print(f"NOT DOWNSAMPLING {array_key}")
|
||||||
|
|
||||||
|
path = self.draw_path(
|
||||||
|
x=x_out,
|
||||||
|
y=y_out,
|
||||||
|
connect=connect,
|
||||||
|
path=path,
|
||||||
|
redraw=True,
|
||||||
|
)
|
||||||
|
# path = pg.functions.arrayToQPath(
|
||||||
|
# x_out,
|
||||||
|
# y_out,
|
||||||
|
# connect='all',
|
||||||
|
# finiteCheck=False,
|
||||||
|
# path=path,
|
||||||
|
# )
|
||||||
|
if graphics:
|
||||||
|
graphics.prepareGeometryChange()
|
||||||
|
|
||||||
|
profiler(
|
||||||
|
'generated fresh path. '
|
||||||
|
f'(should_redraw: {should_redraw} '
|
||||||
|
f'should_ds: {should_ds} new_sample_rate: {new_sample_rate})'
|
||||||
|
)
|
||||||
|
# profiler(f'DRAW PATH IN VIEW -> {self.name}')
|
||||||
|
|
||||||
|
# TODO: get this piecewise prepend working - right now it's
|
||||||
|
# giving heck on vwap...
|
||||||
|
# elif prepend_length:
|
||||||
|
# breakpoint()
|
||||||
|
|
||||||
|
# prepend_path = pg.functions.arrayToQPath(
|
||||||
|
# x[0:prepend_length],
|
||||||
|
# y[0:prepend_length],
|
||||||
|
# connect='all'
|
||||||
|
# )
|
||||||
|
|
||||||
|
# # swap prepend path in "front"
|
||||||
|
# old_path = graphics.path
|
||||||
|
# graphics.path = prepend_path
|
||||||
|
# # graphics.path.moveTo(new_x[0], new_y[0])
|
||||||
|
# graphics.path.connectPath(old_path)
|
||||||
|
|
||||||
|
elif (
|
||||||
|
append_length > 0
|
||||||
|
and do_append
|
||||||
|
and not should_redraw
|
||||||
|
):
|
||||||
|
# print(f'{self.name} append len: {append_length}')
|
||||||
|
print(f'{array_key} append len: {append_length}')
|
||||||
|
new_x = x_out[-append_length - 2:] # slice_to_head]
|
||||||
|
new_y = y_out[-append_length - 2:] # slice_to_head]
|
||||||
|
profiler('sliced append path')
|
||||||
|
|
||||||
|
profiler(
|
||||||
|
f'diffed array input, append_length={append_length}'
|
||||||
|
)
|
||||||
|
|
||||||
|
# if should_ds:
|
||||||
|
# new_x, new_y = xy_downsample(
|
||||||
|
# new_x,
|
||||||
|
# new_y,
|
||||||
|
# uppx,
|
||||||
|
# )
|
||||||
|
# profiler(f'fast path downsample redraw={should_ds}')
|
||||||
|
|
||||||
|
append_path = self.draw_path(
|
||||||
|
x=new_x,
|
||||||
|
y=new_y,
|
||||||
|
connect=connect,
|
||||||
|
# path=fast_path,
|
||||||
|
)
|
||||||
|
|
||||||
|
# append_path = pg.functions.arrayToQPath(
|
||||||
|
# connect='all',
|
||||||
|
# finiteCheck=False,
|
||||||
|
# path=fast_path,
|
||||||
|
# )
|
||||||
|
profiler('generated append qpath')
|
||||||
|
|
||||||
|
# if graphics.use_fpath:
|
||||||
|
if use_fpath:
|
||||||
|
print("USING FPATH")
|
||||||
|
# an attempt at trying to make append-updates faster..
|
||||||
|
if fast_path is None:
|
||||||
|
fast_path = append_path
|
||||||
|
# fast_path.reserve(int(6e3))
|
||||||
|
else:
|
||||||
|
fast_path.connectPath(append_path)
|
||||||
|
size = fast_path.capacity()
|
||||||
|
profiler(f'connected fast path w size: {size}')
|
||||||
|
|
||||||
|
# print(f"append_path br: {append_path.boundingRect()}")
|
||||||
|
# graphics.path.moveTo(new_x[0], new_y[0])
|
||||||
|
# path.connectPath(append_path)
|
||||||
|
|
||||||
|
# XXX: lol this causes a hang..
|
||||||
|
# graphics.path = graphics.path.simplified()
|
||||||
|
else:
|
||||||
|
size = path.capacity()
|
||||||
|
profiler(f'connected history path w size: {size}')
|
||||||
|
path.connectPath(append_path)
|
||||||
|
|
||||||
|
# if use_vr:
|
||||||
|
# array = in_view
|
||||||
# # get latest data from flow shm
|
# # get latest data from flow shm
|
||||||
# self.last_read = (
|
# self.last_read = (
|
||||||
# xfirst, xlast, array, ivl, ivr, in_view
|
# xfirst, xlast, array, ivl, ivr, in_view
|
||||||
# ) = new_read
|
# ) = new_read
|
||||||
|
|
||||||
if (
|
# if (
|
||||||
self.path is None
|
# self.path is None
|
||||||
or only_in_view
|
# or use_vr
|
||||||
):
|
# ):
|
||||||
# redraw the entire source data if we have either of:
|
# redraw the entire source data if we have either of:
|
||||||
# - no prior path graphic rendered or,
|
# - no prior path graphic rendered or,
|
||||||
# - we always intend to re-render the data only in view
|
# - we always intend to re-render the data only in view
|
||||||
|
@ -1220,8 +1399,8 @@ class Renderer(msgspec.Struct):
|
||||||
# data transform: convert source data to a format
|
# data transform: convert source data to a format
|
||||||
# expected to be incrementally updates and later rendered
|
# expected to be incrementally updates and later rendered
|
||||||
# to a more graphics native format.
|
# to a more graphics native format.
|
||||||
if self.data_t:
|
# if self.data_t:
|
||||||
array = self.data_t(array)
|
# array = self.data_t(array)
|
||||||
|
|
||||||
# maybe allocate shm for data transform output
|
# maybe allocate shm for data transform output
|
||||||
# if self.data_t_shm is None:
|
# if self.data_t_shm is None:
|
||||||
|
@ -1237,18 +1416,18 @@ class Renderer(msgspec.Struct):
|
||||||
# shm.push(array)
|
# shm.push(array)
|
||||||
# self.data_t_shm = shm
|
# self.data_t_shm = shm
|
||||||
|
|
||||||
elif self.path:
|
# elif self.path:
|
||||||
print(f'inremental update not supported yet {self.flow.name}')
|
# print(f'inremental update not supported yet {self.flow.name}')
|
||||||
# TODO: do incremental update
|
# TODO: do incremental update
|
||||||
# prepend, append, last = self.diff(self.flow.read())
|
# prepend, append, last = self.diff(self.flow.read())
|
||||||
|
|
||||||
# do path generation for each segment
|
# do path generation for each segment
|
||||||
# and then push into graphics object.
|
# and then push into graphics object.
|
||||||
|
|
||||||
hist, last = array[:-1], array[-1]
|
|
||||||
|
|
||||||
# call path render func on history
|
# call path render func on history
|
||||||
self.path = self.draw_path(hist)
|
# self.path = self.draw_path(hist)
|
||||||
|
self.path = path
|
||||||
|
self.fast_path = fast_path
|
||||||
|
|
||||||
self.last_read = new_read
|
self.last_read = new_read
|
||||||
return self.path, last
|
return self.path, array
|
||||||
|
|
Loading…
Reference in New Issue