Add `Viz.view_state: ViewState`
Adds a small struct which is used to track the most recently viewed data's x/y ranges as well as the last `Viz.read()` "in view" array data for fast access by chart related graphics processing code, namely view mode overlay handling. Also adds new `Viz` interfaces: - `Viz.ds_yrange: tuple[float, float]' which replaces the previous `.yrange` (now set by `.datums_range()` on manual y-range calcs) so that the m4 downsampler can set this field specifically and then it get used (when available) by `Viz.maxmin()`. - `Viz.scalars_from_index()` a new returns-scalar generator which can be used to calc the up and down returns values (used for scaling overlay y-ranges) from an input `xref` x-domain index which maps to some `Ci(xref) = yref`.log_linearized_curve_overlays
parent
26690b061b
commit
62e0889bf5
|
@ -29,7 +29,10 @@ from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
import msgspec
|
from msgspec import (
|
||||||
|
Struct,
|
||||||
|
field,
|
||||||
|
)
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from PyQt5.QtCore import QLineF
|
from PyQt5.QtCore import QLineF
|
||||||
|
@ -225,15 +228,49 @@ def render_baritems(
|
||||||
_sample_rates: set[float] = {1, 60}
|
_sample_rates: set[float] = {1, 60}
|
||||||
|
|
||||||
|
|
||||||
class Viz(msgspec.Struct): # , frozen=True):
|
class ViewState(Struct):
|
||||||
|
'''
|
||||||
|
Indexing objects representing the current view x-range -> y-range.
|
||||||
|
|
||||||
|
'''
|
||||||
|
# (xl, xr) "input" view range in x-domain
|
||||||
|
xrange: tuple[
|
||||||
|
float | int,
|
||||||
|
float | int
|
||||||
|
] | None = None
|
||||||
|
|
||||||
|
# (ymn, ymx) "output" min and max in viewed y-codomain
|
||||||
|
yrange: tuple[
|
||||||
|
float | int,
|
||||||
|
float | int
|
||||||
|
] | None = None
|
||||||
|
|
||||||
|
# last in view ``ShmArray.array[read_slc]`` data
|
||||||
|
in_view: np.ndarray | None = None
|
||||||
|
|
||||||
|
|
||||||
|
class Viz(Struct): # , frozen=True):
|
||||||
'''
|
'''
|
||||||
(Data) "Visualization" compound type which wraps a real-time
|
(Data) "Visualization" compound type which wraps a real-time
|
||||||
shm array stream with displayed graphics (curves, charts)
|
shm array stream with displayed graphics (curves, charts)
|
||||||
for high level access and control as well as efficient incremental
|
for high level access and control as well as efficient incremental
|
||||||
update.
|
update, oriented around the idea of a "view state".
|
||||||
|
|
||||||
The intention is for this type to eventually be capable of shm-passing
|
The (backend) intention is for this interface and type is to
|
||||||
of incrementally updated graphics stream data between actors.
|
eventually be capable of shm-passing of incrementally updated
|
||||||
|
graphics stream data, thus providing a cross-actor solution to
|
||||||
|
sharing UI-related update state potentionally in a (compressed)
|
||||||
|
binary-interchange format.
|
||||||
|
|
||||||
|
Further, from an interaction-triggers-view-in-UI perspective, this type
|
||||||
|
operates as a transform:
|
||||||
|
(x_left, x_right) -> output metrics {ymn, ymx, uppx, ...}
|
||||||
|
|
||||||
|
wherein each x-domain range maps to some output set of (graphics
|
||||||
|
related) vizualization metrics. In further documentation we often
|
||||||
|
refer to this abstraction as a vizualization curve: Ci. Each Ci is
|
||||||
|
considered a function which maps an x-range (input view range) to
|
||||||
|
a multi-variate (metrics) output.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
name: str
|
name: str
|
||||||
|
@ -242,8 +279,12 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
flume: Flume
|
flume: Flume
|
||||||
graphics: Curve | BarItems
|
graphics: Curve | BarItems
|
||||||
|
|
||||||
# for tracking y-mn/mx for y-axis auto-ranging
|
view_state: ViewState = field(default_factory=ViewState)
|
||||||
yrange: tuple[float, float] = None
|
|
||||||
|
# last calculated y-mn/mx from m4 downsample code, this
|
||||||
|
# is updated in the body of `Renderer.render()`.
|
||||||
|
ds_yrange: tuple[float, float] | None = None
|
||||||
|
yrange: tuple[float, float] | None = None
|
||||||
|
|
||||||
# in some cases a viz may want to change its
|
# in some cases a viz may want to change its
|
||||||
# graphical "type" or, "form" when downsampling, to
|
# graphical "type" or, "form" when downsampling, to
|
||||||
|
@ -264,7 +305,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
] = 'time'
|
] = 'time'
|
||||||
|
|
||||||
# downsampling state
|
# TODO: maybe compound this into a downsampling state type?
|
||||||
_last_uppx: float = 0
|
_last_uppx: float = 0
|
||||||
_in_ds: bool = False
|
_in_ds: bool = False
|
||||||
_index_step: float | None = None
|
_index_step: float | None = None
|
||||||
|
@ -303,14 +344,23 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def index_field(self) -> str:
|
def index_field(self) -> str:
|
||||||
|
'''
|
||||||
|
The column name as ``str`` in the underlying ``._shm: ShmArray``
|
||||||
|
which will deliver the "index" array.
|
||||||
|
|
||||||
|
'''
|
||||||
return self._index_field
|
return self._index_field
|
||||||
|
|
||||||
def index_step(
|
def index_step(
|
||||||
self,
|
self,
|
||||||
reset: bool = False,
|
reset: bool = False,
|
||||||
|
|
||||||
) -> float:
|
) -> float:
|
||||||
|
'''
|
||||||
|
Return the size between sample steps in the units of the
|
||||||
|
x-domain, normally either an ``int`` array index size or an
|
||||||
|
epoch time in seconds.
|
||||||
|
|
||||||
|
'''
|
||||||
# attempt to dectect the best step size by scanning a sample of
|
# attempt to dectect the best step size by scanning a sample of
|
||||||
# the source data.
|
# the source data.
|
||||||
if self._index_step is None:
|
if self._index_step is None:
|
||||||
|
@ -393,7 +443,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
# TODO: hash the slice instead maybe?
|
# TODO: hash the slice instead maybe?
|
||||||
# https://stackoverflow.com/a/29980872
|
# https://stackoverflow.com/a/29980872
|
||||||
lbar, rbar = ixrng = round(x_range[0]), round(x_range[1])
|
ixrng = lbar, rbar = round(x_range[0]), round(x_range[1])
|
||||||
|
|
||||||
if use_caching:
|
if use_caching:
|
||||||
cached_result = self._mxmns.get(ixrng)
|
cached_result = self._mxmns.get(ixrng)
|
||||||
|
@ -436,8 +486,8 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
)
|
)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
elif self.yrange:
|
elif self.ds_yrange:
|
||||||
mxmn = self.yrange
|
mxmn = self.ds_yrange
|
||||||
if do_print:
|
if do_print:
|
||||||
print(
|
print(
|
||||||
f'{self.name} M4 maxmin:\n'
|
f'{self.name} M4 maxmin:\n'
|
||||||
|
@ -477,19 +527,6 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
mxmn,
|
mxmn,
|
||||||
)
|
)
|
||||||
|
|
||||||
@lru_cache(maxsize=6116)
|
|
||||||
def median_from_range(
|
|
||||||
self,
|
|
||||||
start: int,
|
|
||||||
stop: int,
|
|
||||||
|
|
||||||
) -> float:
|
|
||||||
in_view = self.shm.array[start:stop]
|
|
||||||
if self.is_ohlc:
|
|
||||||
return np.median(in_view['close'])
|
|
||||||
else:
|
|
||||||
return np.median(in_view[self.name])
|
|
||||||
|
|
||||||
def view_range(self) -> tuple[int, int]:
|
def view_range(self) -> tuple[int, int]:
|
||||||
'''
|
'''
|
||||||
Return the start and stop x-indexes for the managed ``ViewBox``.
|
Return the start and stop x-indexes for the managed ``ViewBox``.
|
||||||
|
@ -514,7 +551,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
self,
|
self,
|
||||||
view_range: None | tuple[float, float] = None,
|
view_range: None | tuple[float, float] = None,
|
||||||
index_field: str | None = None,
|
index_field: str | None = None,
|
||||||
array: None | np.ndarray = None,
|
array: np.ndarray | None = None,
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
int, int, int, int, int, int
|
int, int, int, int, int, int
|
||||||
|
@ -527,29 +564,34 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
index_field: str = index_field or self.index_field
|
index_field: str = index_field or self.index_field
|
||||||
if index_field == 'index':
|
if index_field == 'index':
|
||||||
l, r = round(l), round(r)
|
l: int = round(l)
|
||||||
|
r: int = round(r)
|
||||||
|
|
||||||
if array is None:
|
if array is None:
|
||||||
array = self.shm.array
|
array = self.shm.array
|
||||||
|
|
||||||
index = array[index_field]
|
index = array[index_field]
|
||||||
first = floor(index[0])
|
first: int = floor(index[0])
|
||||||
last = ceil(index[-1])
|
last: int = ceil(index[-1])
|
||||||
|
|
||||||
# first and last datums in view determined by
|
# first and last datums in view determined by
|
||||||
# l / r view range.
|
# l -> r view range.
|
||||||
leftmost = floor(l)
|
leftmost: int = floor(l)
|
||||||
rightmost = ceil(r)
|
rightmost: int = ceil(r)
|
||||||
|
|
||||||
# invalid view state
|
# invalid view state
|
||||||
if (
|
if (
|
||||||
r < l
|
r < l
|
||||||
or l < 0
|
or l < 0
|
||||||
or r < 0
|
or r < 0
|
||||||
or (l > last and r > last)
|
or (
|
||||||
|
l > last
|
||||||
|
and r > last
|
||||||
|
)
|
||||||
):
|
):
|
||||||
leftmost = first
|
leftmost: int = first
|
||||||
rightmost = last
|
rightmost: int = last
|
||||||
|
|
||||||
else:
|
else:
|
||||||
rightmost = max(
|
rightmost = max(
|
||||||
min(last, rightmost),
|
min(last, rightmost),
|
||||||
|
@ -562,7 +604,10 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
rightmost - 1,
|
rightmost - 1,
|
||||||
)
|
)
|
||||||
|
|
||||||
assert leftmost < rightmost
|
# sanity
|
||||||
|
# assert leftmost < rightmost
|
||||||
|
|
||||||
|
self.view_state.xrange = leftmost, rightmost
|
||||||
|
|
||||||
return (
|
return (
|
||||||
l, # left x-in-view
|
l, # left x-in-view
|
||||||
|
@ -591,11 +636,9 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
'''
|
'''
|
||||||
index_field: str = index_field or self.index_field
|
index_field: str = index_field or self.index_field
|
||||||
vr = l, r = self.view_range()
|
|
||||||
|
|
||||||
# readable data
|
# readable data
|
||||||
array = self.shm.array
|
array = self.shm.array
|
||||||
|
|
||||||
if profiler:
|
if profiler:
|
||||||
profiler('self.shm.array READ')
|
profiler('self.shm.array READ')
|
||||||
|
|
||||||
|
@ -607,7 +650,6 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
ilast,
|
ilast,
|
||||||
r,
|
r,
|
||||||
) = self.datums_range(
|
) = self.datums_range(
|
||||||
view_range=vr,
|
|
||||||
index_field=index_field,
|
index_field=index_field,
|
||||||
array=array,
|
array=array,
|
||||||
)
|
)
|
||||||
|
@ -629,11 +671,14 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
# above?
|
# above?
|
||||||
in_view = array[read_slc]
|
in_view = array[read_slc]
|
||||||
if in_view.size:
|
if in_view.size:
|
||||||
|
self.view_state.in_view = in_view
|
||||||
abs_indx = in_view['index']
|
abs_indx = in_view['index']
|
||||||
abs_slc = slice(
|
abs_slc = slice(
|
||||||
int(abs_indx[0]),
|
int(abs_indx[0]),
|
||||||
int(abs_indx[-1]),
|
int(abs_indx[-1]),
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
self.view_state.in_view = None
|
||||||
|
|
||||||
if profiler:
|
if profiler:
|
||||||
profiler(
|
profiler(
|
||||||
|
@ -654,10 +699,11 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
# BUT the ``in_view`` slice DOES..
|
# BUT the ``in_view`` slice DOES..
|
||||||
read_slc = slice(lbar_i, rbar_i)
|
read_slc = slice(lbar_i, rbar_i)
|
||||||
in_view = array[lbar_i: rbar_i + 1]
|
in_view = array[lbar_i: rbar_i + 1]
|
||||||
|
self.view_state.in_view = in_view
|
||||||
# in_view = array[lbar_i-1: rbar_i+1]
|
# in_view = array[lbar_i-1: rbar_i+1]
|
||||||
|
|
||||||
# XXX: same as ^
|
# XXX: same as ^
|
||||||
# to_draw = array[lbar - ifirst:(rbar - ifirst) + 1]
|
# to_draw = array[lbar - ifirst:(rbar - ifirst) + 1]
|
||||||
|
|
||||||
if profiler:
|
if profiler:
|
||||||
profiler('index arithmetic for slicing')
|
profiler('index arithmetic for slicing')
|
||||||
|
|
||||||
|
@ -692,8 +738,8 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
pg.GraphicsObject,
|
pg.GraphicsObject,
|
||||||
]:
|
]:
|
||||||
'''
|
'''
|
||||||
Read latest datums from shm and render to (incrementally)
|
Read latest datums from shm and (incrementally) render to
|
||||||
render to graphics.
|
graphics.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
profiler = Profiler(
|
profiler = Profiler(
|
||||||
|
@ -1262,3 +1308,42 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
vr, 0,
|
vr, 0,
|
||||||
)
|
)
|
||||||
).length()
|
).length()
|
||||||
|
|
||||||
|
@lru_cache(maxsize=6116)
|
||||||
|
def median_from_range(
|
||||||
|
self,
|
||||||
|
start: int,
|
||||||
|
stop: int,
|
||||||
|
|
||||||
|
) -> float:
|
||||||
|
in_view = self.shm.array[start:stop]
|
||||||
|
if self.is_ohlc:
|
||||||
|
return np.median(in_view['close'])
|
||||||
|
else:
|
||||||
|
return np.median(in_view[self.name])
|
||||||
|
|
||||||
|
@lru_cache(maxsize=6116)
|
||||||
|
def dispersion(
|
||||||
|
start: int,
|
||||||
|
stop: int,
|
||||||
|
|
||||||
|
) -> float:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def scalars_from_index(
|
||||||
|
self,
|
||||||
|
xref: float,
|
||||||
|
|
||||||
|
) -> tuple[float, float]:
|
||||||
|
arr = self.view_state.in_view
|
||||||
|
slc = slice_from_time(
|
||||||
|
arr=self.view_state.in_view,
|
||||||
|
start_t=xref,
|
||||||
|
stop_t=xref,
|
||||||
|
)
|
||||||
|
yref = arr[slc.start]
|
||||||
|
ymn, ymx = self.view_state.yrange
|
||||||
|
return (
|
||||||
|
(ymn - yref) / yref,
|
||||||
|
(ymx - yref) / yref,
|
||||||
|
)
|
||||||
|
|
Loading…
Reference in New Issue