Make `FastAppendCurve` optionally view range aware

As with the `BarItems` graphics, this makes it possible to pass in a "in
view" range of array data that can be *only* rendered improving
performance for large(r) data sets. All the other normal behaviour is
kept (i.e a persistent, (pre/ap)pendable path can still be maintained)
if a ``view_range`` is not provided.

Further updates,
- drop the `.should_ds_or_redraw()` and `.maybe_downsample()` predicates
 instead moving all that logic inside `.update_from_array()`.
- disable the "cache flipping", which doesn't seem to be needed to avoid
  artifacts any more?
- handle all redraw/dowsampling logic in `.update_from_array()`.
- even more profiling.
- drop path `.reserve()` stuff until we better figure out how it's
  supposed to work.
big_data_lines
Tyler Goodlet 2022-04-04 10:20:10 -04:00
parent fdd5aa33d2
commit b6f852e0ad
1 changed files with 131 additions and 113 deletions

View File

@ -214,6 +214,9 @@ class FastAppendCurve(pg.GraphicsObject):
vr = self.viewRect() vr = self.viewRect()
l, r = int(vr.left()), int(vr.right()) l, r = int(vr.left()), int(vr.right())
if not self._xrange:
return 0
start, stop = self._xrange start, stop = self._xrange
lbar = max(l, start) lbar = max(l, start)
rbar = min(r, stop) rbar = min(r, stop)
@ -222,45 +225,6 @@ class FastAppendCurve(pg.GraphicsObject):
QLineF(lbar, 0, rbar, 0) QLineF(lbar, 0, rbar, 0)
).length() ).length()
def should_ds_or_redraw(
self,
) -> tuple[bool, bool]:
uppx = self.x_uppx()
px_width = self.px_width()
# uppx_diff = abs(uppx - self._last_uppx)
uppx_diff = (uppx - self._last_uppx)
self._last_uppx = uppx
should_redraw: bool = False
should_ds: bool = False
# print(uppx_diff)
if (
uppx <= 8
):
# trigger redraw or original non-downsampled data
if self._in_ds:
print('REVERTING BACK TO SRC DATA')
# clear downsampled curve(s) and expect
# refresh of path segments.
should_redraw = True
elif (
uppx_diff >= 4
or uppx_diff <= -2
or self._step_mode and abs(uppx_diff) >= 1
):
log.info(
f'{self._name} downsampler change: {self._last_uppx} -> {uppx}'
)
should_ds = {'px_width': px_width, 'uppx': uppx}
should_redraw = True
return should_ds, should_redraw
def downsample( def downsample(
self, self,
x, x,
@ -289,23 +253,19 @@ class FastAppendCurve(pg.GraphicsObject):
self._in_ds = True self._in_ds = True
return x, y return x, y
def maybe_downsample(
self,
) -> None:
'''
Simple update call but with previously cached arrays data.
'''
# print('DS CALLED FROM INTERACTION?')
# presume this is a so called "interaction update", see
# ``ChartView.maybe_downsample_graphics()``.
self.update_from_array(self._x, self._y)
def update_from_array( def update_from_array(
self, self,
# full array input history
x: np.ndarray, x: np.ndarray,
y: np.ndarray, y: np.ndarray,
# pre-sliced array data that's "in view"
x_iv: np.ndarray,
y_iv: np.ndarray,
view_range: Optional[tuple[int, int]] = None,
) -> QtGui.QPainterPath: ) -> QtGui.QPainterPath:
''' '''
Update curve from input 2-d data. Update curve from input 2-d data.
@ -315,11 +275,11 @@ class FastAppendCurve(pg.GraphicsObject):
''' '''
profiler = pg.debug.Profiler( profiler = pg.debug.Profiler(
msg=f'{self._name}.update_from_array()', msg=f'FastAppendCurve.update_from_array(): `{self._name}`',
disabled=not pg_profile_enabled(), disabled=not pg_profile_enabled(),
gt=ms_slower_then, gt=ms_slower_then,
) )
flip_cache = False # flip_cache = False
if self._xrange: if self._xrange:
istart, istop = self._xrange istart, istop = self._xrange
@ -327,40 +287,110 @@ class FastAppendCurve(pg.GraphicsObject):
self._xrange = istart, istop = x[0], x[-1] self._xrange = istart, istop = x[0], x[-1]
# print(f"xrange: {self._xrange}") # print(f"xrange: {self._xrange}")
should_ds, should_redraw = self.should_ds_or_redraw() # XXX: lol brutal, the internals of `CurvePoint` (inherited by
# our `LineDot`) required ``.getData()`` to work..
self.xData = x
self.yData = y
self._x, self._y = x, y
if view_range:
profiler(f'view range slice {view_range}')
# downsampling incremental state checking
uppx = self.x_uppx()
px_width = self.px_width()
uppx_diff = (uppx - self._last_uppx)
should_ds = False
should_redraw = False
# if a view range is passed, plan to draw the
# source ouput that's "in view" of the chart.
if view_range and not self._in_ds:
# print(f'{self._name} vr: {view_range}')
# by default we only pull data up to the last (current) index
x_out, y_out = x_iv[:-1], y_iv[:-1]
# step mode: draw flat top discrete "step"
# over the index space for each datum.
if self._step_mode:
# TODO: numba this bish
x_out, y_out = step_path_arrays_from_1d(
x_out,
y_out
)
profiler('generated step arrays')
should_redraw = True
profiler('sliced in-view array history')
# x_last = x_iv[-1]
# y_last = y_iv[-1]
self._last_vr = view_range
# self.disable_cache()
# flip_cache = True
else:
self._xrange = x[0], x[-1]
x_last = x[-1]
y_last = y[-1]
# check for downsampling conditions
if (
# std m4 downsample conditions
uppx_diff >= 2
or uppx_diff <= -2
or self._step_mode and abs(uppx_diff) >= 2
):
log.info(
f'{self._name} sampler change: {self._last_uppx} -> {uppx}'
)
self._last_uppx = uppx
should_ds = {'px_width': px_width, 'uppx': uppx}
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
# 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 = int(istart - x[0]) prepend_length = int(istart - x[0])
append_length = int(x[-1] - istop) append_length = int(x[-1] - istop)
no_path_yet = self.path is None
# no_path_yet = self.path is None
if ( if (
should_redraw or should_ds self.path is None
or self.path is None or should_redraw
or should_ds
or prepend_length > 0 or prepend_length > 0
): ):
if (
not view_range
or self._in_ds
):
# by default we only pull data up to the last (current) index
x_out, y_out = x[:-1], y[:-1]
# step mode: draw flat top discrete "step" # step mode: draw flat top discrete "step"
# over the index space for each datum. # over the index space for each datum.
if self._step_mode: if self._step_mode:
x_out, y_out = step_path_arrays_from_1d( x_out, y_out = step_path_arrays_from_1d(
x[:-1], y[:-1]
)
profiler('generated step arrays')
else:
# by default we only pull data up to the last (current) index
x_out, y_out = x[:-1], y[:-1]
if should_ds:
x_out, y_out = self.downsample(
x_out, x_out,
y_out, y_out,
**should_ds,
) )
profiler(f'path downsample redraw={should_ds}') # TODO: numba this bish
self._in_ds = True profiler('generated step arrays')
if should_redraw: if should_redraw:
profiler('path reversion to non-ds') profiler('path reversion to non-ds')
@ -371,10 +401,20 @@ class FastAppendCurve(pg.GraphicsObject):
self.fast_path.clear() self.fast_path.clear()
if should_redraw and not should_ds: if should_redraw and not should_ds:
if self._in_ds:
log.info(f'DEDOWN -> {self._name}') log.info(f'DEDOWN -> {self._name}')
self._in_ds = False self._in_ds = False
# else: elif should_ds:
x_out, y_out = self.downsample(
x_out,
y_out,
**should_ds,
)
profiler(f'FULL PATH downsample redraw={should_ds}')
self._in_ds = True
self.path = pg.functions.arrayToQPath( self.path = pg.functions.arrayToQPath(
x_out, x_out,
y_out, y_out,
@ -382,6 +422,9 @@ class FastAppendCurve(pg.GraphicsObject):
finiteCheck=False, finiteCheck=False,
path=self.path, path=self.path,
) )
profiler('generated fresh path')
# profiler(f'DRAW PATH IN VIEW -> {self._name}')
# reserve mem allocs see: # reserve mem allocs see:
# - https://doc.qt.io/qt-5/qpainterpath.html#reserve # - 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#capacity
@ -389,17 +432,12 @@ class FastAppendCurve(pg.GraphicsObject):
# XXX: right now this is based on had hoc checks on a # XXX: right now this is based on had hoc checks on a
# hidpi 3840x2160 4k monitor but we should optimize for # hidpi 3840x2160 4k monitor but we should optimize for
# the target display(s) on the sys. # the target display(s) on the sys.
if no_path_yet: # if no_path_yet:
self.path.reserve(int(500e3)) # self.path.reserve(int(500e3))
profiler('generated fresh path')
# if self._step_mode:
# self.path.closeSubpath()
# TODO: get this piecewise prepend working - right now it's # TODO: get this piecewise prepend working - right now it's
# giving heck on vwap... # giving heck on vwap...
# if prepend_length: # elif prepend_length:
# breakpoint() # breakpoint()
# prepend_path = pg.functions.arrayToQPath( # prepend_path = pg.functions.arrayToQPath(
@ -416,11 +454,15 @@ class FastAppendCurve(pg.GraphicsObject):
elif ( elif (
append_length > 0 append_length > 0
and not view_range
): ):
new_x = x[-append_length - 2:-1]
new_y = y[-append_length - 2:-1]
if self._step_mode: if self._step_mode:
new_x, new_y = step_path_arrays_from_1d( new_x, new_y = step_path_arrays_from_1d(
x[-append_length - 2:-1], new_x,
y[-append_length - 2:-1], new_y,
) )
# [1:] since we don't need the vertical line normally at # [1:] since we don't need the vertical line normally at
# the beginning of the step curve taking the first (x, # the beginning of the step curve taking the first (x,
@ -429,12 +471,6 @@ class FastAppendCurve(pg.GraphicsObject):
new_x = new_x[1:] new_x = new_x[1:]
new_y = new_y[1:] new_y = new_y[1:]
else:
# print(f"append_length: {append_length}")
new_x = x[-append_length - 2:-1]
new_y = y[-append_length - 2:-1]
# print((new_x, new_y))
profiler('diffed append arrays') profiler('diffed append arrays')
if should_ds: if should_ds:
@ -490,21 +526,6 @@ class FastAppendCurve(pg.GraphicsObject):
# self.disable_cache() # self.disable_cache()
# flip_cache = True # flip_cache = True
# XXX: do we need this any more?
# if (
# self._step_mode
# ):
# self.disable_cache()
# flip_cache = True
# XXX: lol brutal, the internals of `CurvePoint` (inherited by
# our `LineDot`) required ``.getData()`` to work..
self.xData = x
self.yData = y
x0, x_last = self._xrange = x[0], x[-1]
y_last = y[-1]
# draw the "current" step graphic segment so it lines up with # draw the "current" step graphic segment so it lines up with
# the "middle" of the current (OHLC) sample. # the "middle" of the current (OHLC) sample.
if self._step_mode: if self._step_mode:
@ -522,7 +543,6 @@ class FastAppendCurve(pg.GraphicsObject):
# f"last rect br: {self._last_step_rect}", # f"last rect br: {self._last_step_rect}",
# ) # )
else: else:
# print((x[-1], y_last))
self._last_line = QLineF( self._last_line = QLineF(
x[-2], y[-2], x[-2], y[-2],
x[-1], y_last x[-1], y_last
@ -536,11 +556,9 @@ class FastAppendCurve(pg.GraphicsObject):
self.update() self.update()
profiler('.update()') profiler('.update()')
if flip_cache: # if flip_cache:
# XXX: seems to be needed to avoid artifacts (see above). # # XXX: seems to be needed to avoid artifacts (see above).
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache) # self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
self._x, self._y = x, y
# 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..