From 4099b53ea2f80048f26304f7a06ae3919fa30787 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 9 Jun 2022 17:07:31 -0400 Subject: [PATCH 1/4] Add `Flow.ds_graphics': a downsample curve ref Allows for optionally updating a "downsampled" graphics type which is currently necessary in the `BarItems` -> `FlattenedOHLC` curve switching case; we don't want to be needlessly redrawing the `Flow.graphics` object (which will be an OHLC curve) when in flattened curve mode. Further add a `only_last_uppx: bool` flag to `.draw_last()` to allow forcing a "last uppx's worth of data max/min" style interpolating line as needed. --- piker/ui/_flows.py | 70 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 53 insertions(+), 17 deletions(-) diff --git a/piker/ui/_flows.py b/piker/ui/_flows.py index 01bbbece..01e7159b 100644 --- a/piker/ui/_flows.py +++ b/piker/ui/_flows.py @@ -175,6 +175,7 @@ def render_baritems( name=f'{flow.name}_ds_ohlc', color=bars._color, ) + flow.ds_graphics = curve curve.hide() self.plot.addItem(curve) @@ -192,18 +193,20 @@ def render_baritems( uppx = curve.x_uppx() in_line = should_line = curve.isVisible() if ( - should_line + in_line and uppx < x_gt ): # print('FLIPPING TO BARS') should_line = False + flow._in_ds = False elif ( - not should_line + not in_line and uppx >= x_gt ): # print('FLIPPING TO LINE') should_line = True + flow._in_ds = True profiler(f'ds logic complete line={should_line}') @@ -333,7 +336,13 @@ class Flow(msgspec.Struct): # , frozen=True): ''' name: str plot: pg.PlotItem - graphics: Curve + graphics: Union[Curve, BarItems] + + # in some cases a flow may want to change its + # graphical "type" or, "form" when downsampling, + # normally this is just a plain line. + ds_graphics: Optional[Curve] = None + _shm: ShmArray is_ohlc: bool = False @@ -540,6 +549,7 @@ class Flow(msgspec.Struct): # , frozen=True): should_redraw: bool = False rkwargs = {} + should_line = False if isinstance(graphics, BarItems): # XXX: special case where we change out graphics # to a line after a certain uppx threshold. @@ -556,8 +566,8 @@ class Flow(msgspec.Struct): # , frozen=True): profiler, **kwargs, ) - # bars = True should_redraw = changed_to_line or not should_line + self._in_ds = should_line else: r = self._src_r @@ -661,6 +671,17 @@ class Flow(msgspec.Struct): # , frozen=True): # assign output paths to graphicis obj graphics.path = r.path graphics.fast_path = r.fast_path + + # XXX: we don't need this right? + # graphics.draw_last_datum( + # path, + # src_array, + # data, + # reset, + # array_key, + # ) + # graphics.update() + # profiler('.update()') else: # assign output paths to graphicis obj graphics.path = r.path @@ -673,16 +694,15 @@ class Flow(msgspec.Struct): # , frozen=True): reset, array_key, ) - - # TODO: is this ever better? - # graphics.prepareGeometryChange() - # profiler('.prepareGeometryChange()') + graphics.update() + profiler('.update()') # 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('.update()') + # TODO: is this ever better? + # graphics.prepareGeometryChange() + # profiler('.prepareGeometryChange()') # track downsampled state self._in_ds = r._in_ds @@ -692,6 +712,7 @@ class Flow(msgspec.Struct): # , frozen=True): def draw_last( self, array_key: Optional[str] = None, + only_last_uppx: bool = False, ) -> None: @@ -711,19 +732,34 @@ class Flow(msgspec.Struct): # , frozen=True): array_key, ) - if self._in_ds: - # we only care about the last pixel's - # worth of data since that's all the screen - # can represent on the last column where - # the most recent datum is being drawn. + # the renderer is downsampling we choose + # to always try and updadte a single (interpolating) + # line segment that spans and tries to display + # the las uppx's worth of datums. + # we only care about the last pixel's + # worth of data since that's all the screen + # can represent on the last column where + # the most recent datum is being drawn. + if self._in_ds or only_last_uppx: + dsg = self.ds_graphics or self.graphics + + # XXX: pretty sure we don't need this? + # if isinstance(g, Curve): + # with dsg.reset_cache(): uppx = self._last_uppx y = y[-uppx:] ymn, ymx = y.min(), y.max() # print(f'drawing uppx={uppx} mxmn line: {ymn}, {ymx}') - g._last_line = QLineF( - x[-2], ymn, + dsg._last_line = QLineF( + x[-uppx], ymn, x[-1], ymx, ) + # print(f'updating DS curve {self.name}') + dsg.update() + + else: + # print(f'updating NOT DS curve {self.name}') + g.update() def by_index_and_key( From 30747736622cdea49d24c5de9a7f1fc4d16f0439 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 9 Jun 2022 17:46:55 -0400 Subject: [PATCH 2/4] Fix 'last datum line is uppx's worth of data' rendering This was introduced in #302 but after thorough testing was clear to be not working XD. Adjust the display loop to update the last graphics segment on both the OHLC and vlm charts (as well as all deriving fsp flows) whenever the uppx >= 1 and there is no current path append taking place (since more datums are needed to span an x-pixel in view). Summary of tweaks: - move vlm chart update code to be at the end of the cycle routine and have that block include the tests for a "interpolated last datum in view" line. - make `do_append: bool` compare with a floor of the uppx value (i.e. appends should happen when we're just fractionally over a pixel of x units). - never update the "volume" chart. --- piker/ui/_display.py | 196 ++++++++++++++++++++++++------------------- 1 file changed, 109 insertions(+), 87 deletions(-) diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 415827fb..031806e7 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -24,6 +24,7 @@ graphics update methods via our custom ``pyqtgraph`` charting api. from dataclasses import dataclass from functools import partial import time +from math import floor from typing import Optional, Any, Callable import numpy as np @@ -367,7 +368,7 @@ def graphics_update_cycle( # such unit steps per pixel (aka uppx). Iow, if the zoom level # is such that a datum(s) update to graphics wouldn't span # to a new pixel, we don't update yet. - do_append = (append_diff >= uppx) + do_append = (append_diff >= floor(uppx)) if do_append: vars['i_last_append'] = i_step @@ -426,71 +427,6 @@ def graphics_update_cycle( profiler('view incremented') - if vlm_chart: - # always update y-label - ds.vlm_sticky.update_from_data( - *array[-1][['index', 'volume']] - ) - - if ( - ( - do_rt_update - or do_append - and liv - ) - or trigger_all - ): - # TODO: make it so this doesn't have to be called - # once the $vlm is up? - vlm_chart.update_graphics_from_flow( - 'volume', - # UGGGh, see ``maxmin()`` impl in `._fsp` for - # the overlayed plotitems... we need a better - # bay to invoke a maxmin per overlay.. - render=False, - # XXX: ^^^^ THIS IS SUPER IMPORTANT! ^^^^ - # without this, since we disable the - # 'volume' (units) chart after the $vlm starts - # up we need to be sure to enable this - # auto-ranging otherwise there will be no handler - # connected to update accompanying overlay - # graphics.. - ) - profiler('`vlm_chart.update_graphics_from_flow()`') - - if ( - mx_vlm_in_view != vars['last_mx_vlm'] - ): - yrange = (0, mx_vlm_in_view * 1.375) - vlm_chart.view._set_yrange( - yrange=yrange, - ) - profiler('`vlm_chart.view._set_yrange()`') - # print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}') - vars['last_mx_vlm'] = mx_vlm_in_view - - for curve_name, flow in vlm_chart._flows.items(): - - if not flow.render: - continue - - update_fsp_chart( - vlm_chart, - flow, - curve_name, - array_key=curve_name, - # do_append=uppx < update_uppx, - do_append=do_append, - ) - # is this even doing anything? - # (pretty sure it's the real-time - # resizing from last quote?) - fvb = flow.plot.vb - fvb._set_yrange( - # autoscale_linked_plots=False, - name=curve_name, - ) - ticks_frame = quote.get('ticks', ()) frames_by_type: dict[str, dict] = {} @@ -540,15 +476,16 @@ def graphics_update_cycle( or do_append or trigger_all ): - # TODO: we should always update the "last" datum - # since the current range should at least be updated - # to it's max/min on the last pixel. chart.update_graphics_from_flow( chart.name, # do_append=uppx < update_uppx, do_append=do_append, ) + # NOTE: we always update the "last" datum + # since the current range should at least be updated + # to it's max/min on the last pixel. + # iterate in FIFO order per tick-frame for typ, tick in lasts.items(): @@ -653,30 +590,115 @@ def graphics_update_cycle( vars['last_mx'], vars['last_mn'] = mx, mn # run synchronous update on all linked flows + # TODO: should the "main" (aka source) flow be special? for curve_name, flow in chart._flows.items(): + # update any overlayed fsp flows + if curve_name != chart.data_key: + update_fsp_chart( + chart, + flow, + curve_name, + array_key=curve_name, + ) + + # even if we're downsampled bigly + # draw the last datum in the final + # px column to give the user the mx/mn + # range of that set. + if ( + not do_append + # and not do_rt_update + and liv + ): + flow.draw_last( + array_key=curve_name, + only_last_uppx=True, + ) + + # volume chart logic.. + # TODO: can we unify this with the above loop? + if vlm_chart: + # always update y-label + ds.vlm_sticky.update_from_data( + *array[-1][['index', 'volume']] + ) if ( - not (do_rt_update or do_append) - and liv - # even if we're downsampled bigly - # draw the last datum in the final - # px column to give the user the mx/mn - # range of that set. + ( + do_rt_update + or do_append + and liv + ) + or trigger_all ): - # always update the last datum-element - # graphic for all flows - flow.draw_last(array_key=curve_name) + # TODO: make it so this doesn't have to be called + # once the $vlm is up? + vlm_chart.update_graphics_from_flow( + 'volume', + # UGGGh, see ``maxmin()`` impl in `._fsp` for + # the overlayed plotitems... we need a better + # bay to invoke a maxmin per overlay.. + render=False, + # XXX: ^^^^ THIS IS SUPER IMPORTANT! ^^^^ + # without this, since we disable the + # 'volume' (units) chart after the $vlm starts + # up we need to be sure to enable this + # auto-ranging otherwise there will be no handler + # connected to update accompanying overlay + # graphics.. + ) + profiler('`vlm_chart.update_graphics_from_flow()`') - # TODO: should the "main" (aka source) flow be special? - if curve_name == chart.data_key: - continue + if ( + mx_vlm_in_view != vars['last_mx_vlm'] + ): + yrange = (0, mx_vlm_in_view * 1.375) + vlm_chart.view._set_yrange( + yrange=yrange, + ) + profiler('`vlm_chart.view._set_yrange()`') + # print(f'mx vlm: {last_mx_vlm} -> {mx_vlm_in_view}') + vars['last_mx_vlm'] = mx_vlm_in_view - update_fsp_chart( - chart, - flow, - curve_name, - array_key=curve_name, - ) + for curve_name, flow in vlm_chart._flows.items(): + + if ( + curve_name != 'volume' and + flow.render and ( + liv and + do_rt_update or do_append + ) + ): + update_fsp_chart( + vlm_chart, + flow, + curve_name, + array_key=curve_name, + # do_append=uppx < update_uppx, + do_append=do_append, + ) + # is this even doing anything? + # (pretty sure it's the real-time + # resizing from last quote?) + fvb = flow.plot.vb + fvb._set_yrange( + name=curve_name, + ) + + elif ( + curve_name != 'volume' + and not do_append + and liv + and uppx >= 1 + # even if we're downsampled bigly + # draw the last datum in the final + # px column to give the user the mx/mn + # range of that set. + ): + # always update the last datum-element + # graphic for all flows + # print(f'drawing last {flow.name}') + flow.draw_last(array_key=curve_name) async def display_symbol_data( From da5dea9f992958f9a48360b1196a08ef895ee9a0 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 9 Jun 2022 16:38:04 -0400 Subject: [PATCH 3/4] Drop cache reset from `Curve.draw_last_datum()` --- piker/ui/_curve.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/piker/ui/_curve.py b/piker/ui/_curve.py index ac967bf7..ac5d12ca 100644 --- a/piker/ui/_curve.py +++ b/piker/ui/_curve.py @@ -379,17 +379,17 @@ class Curve(pg.GraphicsObject): ) -> None: # default line draw last call - with self.reset_cache(): - x = render_data['index'] - y = render_data[array_key] + # with self.reset_cache(): + x = render_data['index'] + y = render_data[array_key] - # 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], - ) + # 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], + ) return x, y From 8eb4a427dacd06298f8dcb9df7b08103feb5be9b Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 10 Jun 2022 07:02:32 -0400 Subject: [PATCH 4/4] Revert uppx flooring, causes shift issues --- piker/ui/_display.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 031806e7..9ad59e30 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -24,7 +24,6 @@ graphics update methods via our custom ``pyqtgraph`` charting api. from dataclasses import dataclass from functools import partial import time -from math import floor from typing import Optional, Any, Callable import numpy as np @@ -368,7 +367,7 @@ def graphics_update_cycle( # such unit steps per pixel (aka uppx). Iow, if the zoom level # is such that a datum(s) update to graphics wouldn't span # to a new pixel, we don't update yet. - do_append = (append_diff >= floor(uppx)) + do_append = (append_diff >= uppx) if do_append: vars['i_last_append'] = i_step