Proper slow chart auto y-range support

The slow (history) chart requires it's own y-range checker logic which
needs to be run in 2 cases:
- the last datum is in view and goes outside the previous mx/mn in view
- the chart is incremented a step

Since we need this duplicate logic this patch also factors the incremental
graphics update info "reading" into a new `DisplayState.incr_info()`
method that can be configured to a chart and input state and returns all
relevant "graphics update measure" in a tuple (for now).

Use this method throughout the rest of the display loop for both fast
and slow chart checks and in the `increment_history_view()` slow chart
task.
history_view
Tyler Goodlet 2022-09-02 16:42:48 -04:00
parent 7958d8ad4f
commit 10c1944de5
1 changed files with 133 additions and 88 deletions

View File

@ -21,7 +21,6 @@ this module ties together quote and computational (fsp) streams with
graphics update methods via our custom ``pyqtgraph`` charting api. graphics update methods via our custom ``pyqtgraph`` charting api.
''' '''
from dataclasses import dataclass
from functools import partial from functools import partial
import time import time
from typing import Optional, Any, Callable from typing import Optional, Any, Callable
@ -35,6 +34,7 @@ from ..data.feed import (
open_feed, open_feed,
Feed, Feed,
) )
from ..data.types import Struct
from ._axes import YAxisLabel from ._axes import YAxisLabel
from ._chart import ( from ._chart import (
ChartPlotWidget, ChartPlotWidget,
@ -124,8 +124,7 @@ def chart_maxmin(
) )
@dataclass class DisplayState(Struct):
class DisplayState:
''' '''
Chart-local real-time graphics state container. Chart-local real-time graphics state container.
@ -146,12 +145,77 @@ class DisplayState:
hist_last_price_sticky: YAxisLabel hist_last_price_sticky: YAxisLabel
# misc state tracking # misc state tracking
vars: dict[str, Any] vars: dict[str, Any] = {
'tick_margin': 0,
'i_last': 0,
'i_last_append': 0,
'last_mx_vlm': 0,
'last_mx': 0,
'last_mn': 0,
}
vlm_chart: Optional[ChartPlotWidget] = None vlm_chart: Optional[ChartPlotWidget] = None
vlm_sticky: Optional[YAxisLabel] = None vlm_sticky: Optional[YAxisLabel] = None
wap_in_history: bool = False wap_in_history: bool = False
def incr_info(
self,
chart: Optional[ChartPlotWidget] = None,
shm: Optional[ShmArray] = None,
state: Optional[dict] = None, # pass in a copy if you don't
update_state: bool = True,
update_uppx: float = 16,
) -> tuple:
shm = shm or self.ohlcv
chart = chart or self.chart
state = state or self.vars
if not update_state:
state = state.copy()
# compute the first available graphic's x-units-per-pixel
uppx = chart.view.x_uppx()
# NOTE: this used to be implemented in a dedicated
# "increment task": ``check_for_new_bars()`` but it doesn't
# make sense to do a whole task switch when we can just do
# this simple index-diff and all the fsp sub-curve graphics
# are diffed on each draw cycle anyway; so updates to the
# "curve" length is already automatic.
# increment the view position by the sample offset.
i_step = shm.index
i_diff = i_step - state['i_last']
state['i_last'] = i_step
append_diff = i_step - state['i_last_append']
# update the "last datum" (aka extending the flow graphic with
# new data) only if the number of unit steps is >= the number of
# 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)
if do_append:
state['i_last_append'] = i_step
do_rt_update = uppx < update_uppx
_, _, _, r = chart.bars_range()
liv = r >= i_step
# TODO: pack this into a struct
return (
uppx,
liv,
do_append,
i_diff,
append_diff,
do_rt_update,
)
async def graphics_update_loop( async def graphics_update_loop(
@ -271,7 +335,11 @@ async def graphics_update_loop(
# API that can be reused at least in terms of pulling view # API that can be reused at least in terms of pulling view
# params (eg ``.bars_range()``). # params (eg ``.bars_range()``).
async def increment_history_view(): async def increment_history_view():
i_last_append = i_last = hist_ohlcv.index i_last = hist_ohlcv.index
state = ds.vars.copy() | {
'i_last_append': i_last,
'i_last': i_last,
}
_, hist_step_size_s, _ = feed.get_ds_info() _, hist_step_size_s, _ = feed.get_ds_info()
async with feed.index_stream( async with feed.index_stream(
@ -279,27 +347,26 @@ async def graphics_update_loop(
) as istream: ) as istream:
async for msg in istream: async for msg in istream:
# increment the view position by the sample offset. # check if slow chart needs a resize
uppx = hist_chart.view.x_uppx() (
l, lbar, rbar, r = hist_chart.bars_range() uppx,
liv,
i_step = hist_ohlcv.index do_append,
i_diff = i_step - i_last i_diff,
i_last = i_step append_diff,
liv = r >= i_step do_rt_update,
append_diff = i_step - i_last_append ) = ds.incr_info(
do_append = (append_diff >= uppx) chart=hist_chart,
shm=ds.hist_ohlcv,
if do_append: state=state,
i_last_append = i_step # update_state=False,
)
if ( if (
# i_diff > 0 # no new sample step
do_append do_append
# and uppx < 4 # chart is zoomed out very far
and liv and liv
): ):
hist_chart.increment_view(steps=i_diff) hist_chart.increment_view(steps=i_diff)
hist_chart.view._set_yrange(yrange=hist_chart.maxmin())
nurse.start_soon(increment_history_view) nurse.start_soon(increment_history_view)
@ -374,47 +441,15 @@ def graphics_update_cycle(
vars = ds.vars vars = ds.vars
tick_margin = vars['tick_margin'] tick_margin = vars['tick_margin']
update_uppx = 16
for sym, quote in ds.quotes.items(): for sym, quote in ds.quotes.items():
(
# compute the first available graphic's x-units-per-pixel uppx,
uppx = chart.view.x_uppx() liv,
do_append,
# NOTE: vlm may be written by the ``brokerd`` backend i_diff,
# event though a tick sample is not emitted. append_diff,
# TODO: show dark trades differently do_rt_update,
# https://github.com/pikers/piker/issues/116 ) = ds.incr_info()
# NOTE: this used to be implemented in a dedicated
# "increment task": ``check_for_new_bars()`` but it doesn't
# make sense to do a whole task switch when we can just do
# this simple index-diff and all the fsp sub-curve graphics
# are diffed on each draw cycle anyway; so updates to the
# "curve" length is already automatic.
# increment the view position by the sample offset.
i_step = ohlcv.index
i_diff = i_step - vars['i_last']
vars['i_last'] = i_step
append_diff = i_step - vars['i_last_append']
# update the "last datum" (aka extending the flow graphic with
# new data) only if the number of unit steps is >= the number of
# 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)
if do_append:
vars['i_last_append'] = i_step
do_rt_update = uppx < update_uppx
# print(
# f'append_diff:{append_diff}\n'
# f'uppx:{uppx}\n'
# f'do_append: {do_append}'
# )
# TODO: we should only run mxmn when we know # TODO: we should only run mxmn when we know
# an update is due via ``do_append`` above. # an update is due via ``do_append`` above.
@ -430,8 +465,6 @@ def graphics_update_cycle(
profiler('`ds.maxmin()` call') profiler('`ds.maxmin()` call')
liv = r >= i_step # the last datum is in view
if ( if (
prepend_update_index is not None prepend_update_index is not None
and lbar > prepend_update_index and lbar > prepend_update_index
@ -446,16 +479,10 @@ def graphics_update_cycle(
# don't real-time "shift" the curve to the # don't real-time "shift" the curve to the
# left unless we get one of the following: # left unless we get one of the following:
if ( if (
( (do_append and liv)
# i_diff > 0 # no new sample step
do_append
# and uppx < 4 # chart is zoomed out very far
and liv
)
or trigger_all or trigger_all
): ):
chart.increment_view(steps=i_diff) chart.increment_view(steps=i_diff)
# chart.increment_view(steps=i_diff + round(append_diff - uppx))
if vlm_chart: if vlm_chart:
vlm_chart.increment_view(steps=i_diff) vlm_chart.increment_view(steps=i_diff)
@ -606,10 +633,12 @@ def graphics_update_cycle(
l1.bid_label.update_fields({'level': price, 'size': size}) l1.bid_label.update_fields({'level': price, 'size': size})
# check for y-range re-size # check for y-range re-size
if (mx > vars['last_mx']) or (mn < vars['last_mn']):
# fast chart resize case
if ( if (
(mx > vars['last_mx']) or (mn < vars['last_mn']) liv
and not chart._static_yrange == 'axis' and not chart._static_yrange == 'axis'
and liv
): ):
main_vb = chart.view main_vb = chart.view
if ( if (
@ -627,6 +656,22 @@ def graphics_update_cycle(
yrange=(mn, mx), yrange=(mn, mx),
) )
# check if slow chart needs a resize
(
_,
hist_liv,
_,
_,
_,
_,
) = ds.incr_info(
chart=hist_chart,
shm=ds.hist_ohlcv,
update_state=False,
)
if hist_liv:
hist_chart.view._set_yrange(yrange=hist_chart.maxmin())
# XXX: update this every draw cycle to make L1-always-in-view work. # XXX: update this every draw cycle to make L1-always-in-view work.
vars['last_mx'], vars['last_mn'] = mx, mn vars['last_mx'], vars['last_mn'] = mx, mn