Re-implement `.default_view()` on `Viz`
Since we don't really need it defined on the "chart widget" move it to a viz method and rework it to hell: - always discard the invalid view l > r case. - use the graphic's UPPX to determine UI-to-scene coordinate scaling for the L1-label collision detection, if there is no L1 just offset by a few (index step scaled) datums; this allows us to drop the 2x x-range calls as was hacked previous. - handle no-data-in-view cases explicitly and error if we get any ostensibly impossible cases. - expect caller to trigger a graphics cycle if needed. Further support this includes a rework a slew of other important details: - add `Viz.index_step`, an idempotent computed, index (presumably uniform) step value which is needed for variable sample rate graphics displayed on an epoch (second) time index. - rework `Viz.datums_range()` to pass view x-endpoints as first and last elements in return `tuple`; tighten up snap-to-data edge case logic using `max()`/`min()` calls and better internal var naming. - adjust all calls to `slice_from_time()` to not expect an "abs" slice. - drop all `.yrange` resetting since we can just have the `Renderer` do it when necessary.epoch_indexing_and_dataviz_layer
parent
5ab4e5493e
commit
50209752c3
|
@ -25,6 +25,7 @@ incremental update.
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
from typing import (
|
from typing import (
|
||||||
Optional,
|
Optional,
|
||||||
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
import msgspec
|
import msgspec
|
||||||
|
@ -49,7 +50,6 @@ from ..data._pathops import (
|
||||||
)
|
)
|
||||||
from ._ohlc import (
|
from ._ohlc import (
|
||||||
BarItems,
|
BarItems,
|
||||||
# bar_from_ohlc_row,
|
|
||||||
)
|
)
|
||||||
from ._curve import (
|
from ._curve import (
|
||||||
Curve,
|
Curve,
|
||||||
|
@ -63,6 +63,11 @@ from .._profile import (
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from ._interaction import ChartView
|
||||||
|
from ._chart import ChartPlotWidget
|
||||||
|
|
||||||
|
|
||||||
log = get_logger(__name__)
|
log = get_logger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
@ -231,11 +236,13 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
is_ohlc: bool = False
|
is_ohlc: bool = False
|
||||||
render: bool = True # toggle for display loop
|
render: bool = True # toggle for display loop
|
||||||
|
|
||||||
|
# _index_field: str = 'index'
|
||||||
_index_field: str = 'time'
|
_index_field: str = 'time'
|
||||||
|
|
||||||
# downsampling state
|
# downsampling state
|
||||||
_last_uppx: float = 0
|
_last_uppx: float = 0
|
||||||
_in_ds: bool = False
|
_in_ds: bool = False
|
||||||
|
_index_step: float | None = None
|
||||||
|
|
||||||
# map from uppx -> (downsampled data, incremental graphics)
|
# map from uppx -> (downsampled data, incremental graphics)
|
||||||
_src_r: Optional[Renderer] = None
|
_src_r: Optional[Renderer] = None
|
||||||
|
@ -244,12 +251,6 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
tuple[Renderer, pg.GraphicsItem],
|
tuple[Renderer, pg.GraphicsItem],
|
||||||
] = (None, None)
|
] = (None, None)
|
||||||
|
|
||||||
# TODO: hackery to be able to set a shm later
|
|
||||||
# but whilst also allowing this type to hashable,
|
|
||||||
# likely will require serializable token that is used to attach
|
|
||||||
# to the underlying shm ref after startup?
|
|
||||||
# _shm: Optional[ShmArray] = None # currently, may be filled in "later"
|
|
||||||
|
|
||||||
# cache of y-range values per x-range input.
|
# cache of y-range values per x-range input.
|
||||||
_mxmns: dict[tuple[int, int], tuple[float, float]] = {}
|
_mxmns: dict[tuple[int, int], tuple[float, float]] = {}
|
||||||
|
|
||||||
|
@ -261,6 +262,17 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
def index_field(self) -> str:
|
def index_field(self) -> str:
|
||||||
return self._index_field
|
return self._index_field
|
||||||
|
|
||||||
|
def index_step(
|
||||||
|
self,
|
||||||
|
reset: bool = False,
|
||||||
|
|
||||||
|
) -> float:
|
||||||
|
if self._index_step is None:
|
||||||
|
index = self.shm.array[self.index_field]
|
||||||
|
self._index_step = index[-1] - index[-2]
|
||||||
|
|
||||||
|
return self._index_step
|
||||||
|
|
||||||
def maxmin(
|
def maxmin(
|
||||||
self,
|
self,
|
||||||
lbar: int,
|
lbar: int,
|
||||||
|
@ -275,23 +287,35 @@ 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
|
||||||
rkey = (lbar, rbar)
|
rkey = (round(lbar), round(rbar))
|
||||||
cached_result = self._mxmns.get(rkey)
|
cached_result = self._mxmns.get(rkey)
|
||||||
|
do_print = 'btc' in self.name
|
||||||
if cached_result:
|
if cached_result:
|
||||||
|
|
||||||
|
# if do_print:
|
||||||
|
# print(
|
||||||
|
# f'{self.name} CACHED maxmin\n'
|
||||||
|
# f'{rkey} -> {cached_result}'
|
||||||
|
# )
|
||||||
return cached_result
|
return cached_result
|
||||||
|
|
||||||
shm = self.shm
|
shm = self.shm
|
||||||
if shm is None:
|
if shm is None:
|
||||||
|
breakpoint()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
arr = shm.array
|
arr = shm.array
|
||||||
|
# times = arr['time']
|
||||||
|
# step = round(times[-1] - times[-2])
|
||||||
|
# if (
|
||||||
|
# do_print
|
||||||
|
# and step == 60
|
||||||
|
# ):
|
||||||
|
# breakpoint()
|
||||||
|
|
||||||
# get relative slice indexes into array
|
# get relative slice indexes into array
|
||||||
if self.index_field == 'time':
|
if self.index_field == 'time':
|
||||||
(
|
read_slc = slice_from_time(
|
||||||
abs_slc,
|
|
||||||
read_slc,
|
|
||||||
) = slice_from_time(
|
|
||||||
arr,
|
arr,
|
||||||
start_t=lbar,
|
start_t=lbar,
|
||||||
stop_t=rbar,
|
stop_t=rbar,
|
||||||
|
@ -306,11 +330,17 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
]
|
]
|
||||||
|
|
||||||
if not slice_view.size:
|
if not slice_view.size:
|
||||||
|
log.warning(f'{self.name} no maxmin in view?')
|
||||||
|
# breakpoint()
|
||||||
return None
|
return None
|
||||||
|
|
||||||
elif self.yrange:
|
elif self.yrange:
|
||||||
mxmn = self.yrange
|
mxmn = self.yrange
|
||||||
# print(f'{self.name} M4 maxmin: {mxmn}')
|
if do_print:
|
||||||
|
print(
|
||||||
|
f'{self.name} M4 maxmin:\n'
|
||||||
|
f'{rkey} -> {mxmn}'
|
||||||
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
if self.is_ohlc:
|
if self.is_ohlc:
|
||||||
|
@ -323,7 +353,11 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
yhigh = np.max(view)
|
yhigh = np.max(view)
|
||||||
|
|
||||||
mxmn = ylow, yhigh
|
mxmn = ylow, yhigh
|
||||||
# print(f'{self.name} MANUAL maxmin: {mxmn}')
|
if do_print:
|
||||||
|
print(
|
||||||
|
f'{self.name} MANUAL ohlc={self.is_ohlc} maxmin:\n'
|
||||||
|
f'{rkey} -> {mxmn}'
|
||||||
|
)
|
||||||
|
|
||||||
# cache result for input range
|
# cache result for input range
|
||||||
assert mxmn
|
assert mxmn
|
||||||
|
@ -333,8 +367,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
def view_range(self) -> tuple[int, int]:
|
def view_range(self) -> tuple[int, int]:
|
||||||
'''
|
'''
|
||||||
Return the indexes in view for the associated
|
Return the start and stop x-indexes for the managed ``ViewBox``.
|
||||||
plot displaying this viz's data.
|
|
||||||
|
|
||||||
'''
|
'''
|
||||||
vr = self.plot.viewRect()
|
vr = self.plot.viewRect()
|
||||||
|
@ -345,15 +378,18 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
def bars_range(self) -> tuple[int, int, int, int]:
|
def bars_range(self) -> tuple[int, int, int, int]:
|
||||||
'''
|
'''
|
||||||
Return a range tuple for the bars present in view.
|
Return a range tuple for the left-view, left-datum, right-datum
|
||||||
|
and right-view x-indices.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
start, l, datum_start, datum_stop, r, stop = self.datums_range()
|
l, start, datum_start, datum_stop, stop, r = self.datums_range()
|
||||||
return l, datum_start, datum_stop, r
|
return l, datum_start, datum_stop, r
|
||||||
|
|
||||||
def datums_range(
|
def datums_range(
|
||||||
self,
|
self,
|
||||||
|
view_range: None | tuple[float, float] = None,
|
||||||
index_field: str | None = None,
|
index_field: str | None = None,
|
||||||
|
array: None | np.ndarray = None,
|
||||||
|
|
||||||
) -> tuple[
|
) -> tuple[
|
||||||
int, int, int, int, int, int
|
int, int, int, int, int, int
|
||||||
|
@ -362,39 +398,54 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
Return a range tuple for the datums present in view.
|
Return a range tuple for the datums present in view.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
l, r = self.view_range()
|
l, r = view_range or self.view_range()
|
||||||
|
|
||||||
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, r = round(l), round(r)
|
||||||
|
|
||||||
array = self.shm.array
|
if array is None:
|
||||||
|
array = self.shm.array
|
||||||
|
|
||||||
index = array[index_field]
|
index = array[index_field]
|
||||||
start = index[0]
|
first = round(index[0])
|
||||||
stop = index[-1]
|
last = round(index[-1])
|
||||||
|
|
||||||
|
# first and last datums in view determined by
|
||||||
|
# l / r view range.
|
||||||
|
leftmost = round(l)
|
||||||
|
rightmost = round(r)
|
||||||
|
|
||||||
|
# invalid view state
|
||||||
if (
|
if (
|
||||||
l < 0
|
r < l
|
||||||
or r < l
|
or l < 0
|
||||||
or l < start
|
or r < 0
|
||||||
|
or (l > last and r > last)
|
||||||
):
|
):
|
||||||
datum_start = start
|
leftmost = first
|
||||||
datum_stop = stop
|
rightmost = last
|
||||||
else:
|
else:
|
||||||
datum_start = max(l, start)
|
rightmost = max(
|
||||||
datum_stop = r
|
min(last, rightmost),
|
||||||
if l < stop:
|
first,
|
||||||
datum_stop = min(r, stop)
|
)
|
||||||
|
|
||||||
assert datum_start < datum_stop
|
leftmost = min(
|
||||||
|
max(first, leftmost),
|
||||||
|
last,
|
||||||
|
rightmost - 1,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert leftmost < rightmost
|
||||||
|
|
||||||
return (
|
return (
|
||||||
start,
|
|
||||||
l, # left x-in-view
|
l, # left x-in-view
|
||||||
datum_start,
|
first, # first datum
|
||||||
datum_stop,
|
leftmost,
|
||||||
|
rightmost,
|
||||||
|
last, # last_datum
|
||||||
r, # right-x-in-view
|
r, # right-x-in-view
|
||||||
stop,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
def read(
|
def read(
|
||||||
|
@ -415,6 +466,7 @@ 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
|
||||||
|
@ -423,13 +475,17 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
profiler('self.shm.array READ')
|
profiler('self.shm.array READ')
|
||||||
|
|
||||||
(
|
(
|
||||||
ifirst,
|
|
||||||
l,
|
l,
|
||||||
|
ifirst,
|
||||||
lbar,
|
lbar,
|
||||||
rbar,
|
rbar,
|
||||||
r,
|
|
||||||
ilast,
|
ilast,
|
||||||
) = self.datums_range(index_field=index_field)
|
r,
|
||||||
|
) = self.datums_range(
|
||||||
|
view_range=vr,
|
||||||
|
index_field=index_field,
|
||||||
|
array=array,
|
||||||
|
)
|
||||||
# if rbar < lbar:
|
# if rbar < lbar:
|
||||||
# breakpoint()
|
# breakpoint()
|
||||||
|
|
||||||
|
@ -440,26 +496,22 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
# TODO: support time slicing
|
# TODO: support time slicing
|
||||||
if index_field == 'time':
|
if index_field == 'time':
|
||||||
(
|
read_slc = slice_from_time(
|
||||||
abs_slc,
|
|
||||||
read_slc,
|
|
||||||
) = slice_from_time(
|
|
||||||
array,
|
array,
|
||||||
start_t=lbar,
|
start_t=lbar,
|
||||||
stop_t=rbar,
|
stop_t=rbar,
|
||||||
)
|
)
|
||||||
in_view = array[read_slc]
|
|
||||||
|
|
||||||
# diff = rbar - lbar
|
# TODO: maybe we should return this from the slicer call
|
||||||
# if (
|
# above?
|
||||||
# 'btc' in self.name
|
in_view = array[read_slc]
|
||||||
# and 'hist' not in self.shm.token
|
if in_view.size:
|
||||||
# ):
|
abs_indx = in_view['index']
|
||||||
# print(
|
abs_slc = slice(
|
||||||
# f'{self.name}: len(iv) = {len(in_view)}\n'
|
int(abs_indx[0]),
|
||||||
# f'start/stop: {lbar},{rbar}\n',
|
int(abs_indx[-1]),
|
||||||
# f'diff: {diff}\n',
|
)
|
||||||
# )
|
|
||||||
if profiler:
|
if profiler:
|
||||||
profiler(
|
profiler(
|
||||||
'`slice_from_time('
|
'`slice_from_time('
|
||||||
|
@ -635,8 +687,6 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
should_redraw = True
|
should_redraw = True
|
||||||
|
|
||||||
showing_src_data = True
|
showing_src_data = True
|
||||||
# reset yrange to be computed from source data
|
|
||||||
self.yrange = None
|
|
||||||
|
|
||||||
# MAIN RENDER LOGIC:
|
# MAIN RENDER LOGIC:
|
||||||
# - determine in view data and redraw on range change
|
# - determine in view data and redraw on range change
|
||||||
|
@ -662,10 +712,6 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
**rkwargs,
|
**rkwargs,
|
||||||
)
|
)
|
||||||
if showing_src_data:
|
|
||||||
# print(f"{self.name} SHOWING SOURCE")
|
|
||||||
# reset yrange to be computed from source data
|
|
||||||
self.yrange = None
|
|
||||||
|
|
||||||
if not out:
|
if not out:
|
||||||
log.warning(f'{self.name} failed to render!?')
|
log.warning(f'{self.name} failed to render!?')
|
||||||
|
@ -678,7 +724,6 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
|
|
||||||
# XXX: SUPER UGGGHHH... without this we get stale cache
|
# XXX: SUPER UGGGHHH... without this we get stale cache
|
||||||
# graphics that don't update until you downsampler again..
|
# graphics that don't update until you downsampler again..
|
||||||
# reset = False
|
|
||||||
# if reset:
|
# if reset:
|
||||||
# with graphics.reset_cache():
|
# with graphics.reset_cache():
|
||||||
# # assign output paths to graphicis obj
|
# # assign output paths to graphicis obj
|
||||||
|
@ -798,6 +843,129 @@ class Viz(msgspec.Struct): # , frozen=True):
|
||||||
)
|
)
|
||||||
).length()
|
).length()
|
||||||
|
|
||||||
|
def default_view(
|
||||||
|
self,
|
||||||
|
bars_from_y: int = int(616 * 3/8),
|
||||||
|
y_offset: int = 0,
|
||||||
|
do_ds: bool = True,
|
||||||
|
|
||||||
|
) -> None:
|
||||||
|
'''
|
||||||
|
Set the plot's viewbox to a "default" startup setting where
|
||||||
|
we try to show the underlying data range sanely.
|
||||||
|
|
||||||
|
'''
|
||||||
|
shm: ShmArray = self.shm
|
||||||
|
array: np.ndarray = shm.array
|
||||||
|
view: ChartView = self.plot.vb
|
||||||
|
(
|
||||||
|
vl,
|
||||||
|
first_datum,
|
||||||
|
datum_start,
|
||||||
|
datum_stop,
|
||||||
|
last_datum,
|
||||||
|
vr,
|
||||||
|
) = self.datums_range(array=array)
|
||||||
|
|
||||||
|
# invalid case: view is not ordered correctly
|
||||||
|
# return and expect caller to sort it out.
|
||||||
|
if (
|
||||||
|
vl > vr
|
||||||
|
):
|
||||||
|
log.warning(
|
||||||
|
'Skipping `.default_view()` viewbox not initialized..\n'
|
||||||
|
f'l -> r: {vl} -> {vr}\n'
|
||||||
|
f'datum_start -> datum_stop: {datum_start} -> {datum_stop}\n'
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
chartw: ChartPlotWidget = self.plot.getViewWidget()
|
||||||
|
index_field = self.index_field
|
||||||
|
step = self.index_step()
|
||||||
|
|
||||||
|
if index_field == 'time':
|
||||||
|
# transform l -> r view range values into
|
||||||
|
# data index domain to determine how view
|
||||||
|
# should be reset to better match data.
|
||||||
|
read_slc = slice_from_time(
|
||||||
|
array,
|
||||||
|
start_t=vl,
|
||||||
|
stop_t=vr,
|
||||||
|
step=step,
|
||||||
|
)
|
||||||
|
|
||||||
|
index_iv = array[index_field][read_slc]
|
||||||
|
uppx: float = self.graphics.x_uppx() or 1
|
||||||
|
|
||||||
|
# l->r distance in scene units, no larger then data span
|
||||||
|
data_diff = last_datum - first_datum
|
||||||
|
rl_diff = min(vr - vl, data_diff)
|
||||||
|
|
||||||
|
# orient by offset from the y-axis including
|
||||||
|
# space to compensate for the L1 labels.
|
||||||
|
if not y_offset:
|
||||||
|
|
||||||
|
# we get the L1 spread label "length" in view coords and
|
||||||
|
# make sure it doesn't colide with the right-most datum in
|
||||||
|
# view.
|
||||||
|
_, l1_len = chartw.pre_l1_xs()
|
||||||
|
offset = l1_len/(uppx*step)
|
||||||
|
|
||||||
|
# if no L1 label is present just offset by a few datums
|
||||||
|
# from the y-axis.
|
||||||
|
if chartw._max_l1_line_len == 0:
|
||||||
|
offset += 3*step
|
||||||
|
else:
|
||||||
|
offset = (y_offset * step) + uppx*step
|
||||||
|
|
||||||
|
# align right side of view to the rightmost datum + the selected
|
||||||
|
# offset from above.
|
||||||
|
r_reset = last_datum + offset
|
||||||
|
|
||||||
|
# no data is in view so check for the only 2 sane cases:
|
||||||
|
# - entire view is LEFT of data
|
||||||
|
# - entire view is RIGHT of data
|
||||||
|
if index_iv.size == 0:
|
||||||
|
log.warning(f'No data in view for {vl} -> {vr}')
|
||||||
|
|
||||||
|
# 2 cases either the view is to the left or right of the
|
||||||
|
# data set.
|
||||||
|
if (
|
||||||
|
vl <= first_datum
|
||||||
|
and vr <= first_datum
|
||||||
|
):
|
||||||
|
l_reset = first_datum
|
||||||
|
|
||||||
|
elif (
|
||||||
|
vl >= last_datum
|
||||||
|
and vr >= last_datum
|
||||||
|
):
|
||||||
|
l_reset = r_reset - rl_diff
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise RuntimeError(f'Unknown view state {vl} -> {vr}')
|
||||||
|
|
||||||
|
else:
|
||||||
|
# maintain the l->r view distance
|
||||||
|
l_reset = r_reset - rl_diff
|
||||||
|
|
||||||
|
# remove any custom user yrange setttings
|
||||||
|
if chartw._static_yrange == 'axis':
|
||||||
|
chartw._static_yrange = None
|
||||||
|
|
||||||
|
view.setXRange(
|
||||||
|
min=l_reset,
|
||||||
|
max=r_reset,
|
||||||
|
padding=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
if do_ds:
|
||||||
|
view.maybe_downsample_graphics()
|
||||||
|
view._set_yrange()
|
||||||
|
|
||||||
|
# caller should do this!
|
||||||
|
# self.linked.graphics_cycle()
|
||||||
|
|
||||||
|
|
||||||
class Renderer(msgspec.Struct):
|
class Renderer(msgspec.Struct):
|
||||||
|
|
||||||
|
@ -959,6 +1127,8 @@ class Renderer(msgspec.Struct):
|
||||||
fast_path = self.fast_path
|
fast_path = self.fast_path
|
||||||
reset = False
|
reset = False
|
||||||
|
|
||||||
|
self.viz.yrange = None
|
||||||
|
|
||||||
# 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
|
||||||
|
|
Loading…
Reference in New Issue