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 typing import ( | ||||
|     Optional, | ||||
|     TYPE_CHECKING, | ||||
| ) | ||||
| 
 | ||||
| import msgspec | ||||
|  | @ -49,7 +50,6 @@ from ..data._pathops import ( | |||
| ) | ||||
| from ._ohlc import ( | ||||
|     BarItems, | ||||
|     # bar_from_ohlc_row, | ||||
| ) | ||||
| from ._curve import ( | ||||
|     Curve, | ||||
|  | @ -63,6 +63,11 @@ from .._profile import ( | |||
| ) | ||||
| 
 | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from ._interaction import ChartView | ||||
|     from ._chart import ChartPlotWidget | ||||
| 
 | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
| 
 | ||||
| 
 | ||||
|  | @ -231,11 +236,13 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
|     is_ohlc: bool = False | ||||
|     render: bool = True  # toggle for display loop | ||||
| 
 | ||||
|     # _index_field: str = 'index' | ||||
|     _index_field: str = 'time' | ||||
| 
 | ||||
|     # downsampling state | ||||
|     _last_uppx: float = 0 | ||||
|     _in_ds: bool = False | ||||
|     _index_step: float | None = None | ||||
| 
 | ||||
|     # map from uppx -> (downsampled data, incremental graphics) | ||||
|     _src_r: Optional[Renderer] = None | ||||
|  | @ -244,12 +251,6 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
|         tuple[Renderer, pg.GraphicsItem], | ||||
|     ] = (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. | ||||
|     _mxmns: dict[tuple[int, int], tuple[float, float]] = {} | ||||
| 
 | ||||
|  | @ -261,6 +262,17 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
|     def index_field(self) -> str: | ||||
|         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( | ||||
|         self, | ||||
|         lbar: int, | ||||
|  | @ -275,23 +287,35 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
|         ''' | ||||
|         # TODO: hash the slice instead maybe? | ||||
|         # https://stackoverflow.com/a/29980872 | ||||
|         rkey = (lbar, rbar) | ||||
|         rkey = (round(lbar), round(rbar)) | ||||
|         cached_result = self._mxmns.get(rkey) | ||||
|         do_print = 'btc' in self.name | ||||
|         if cached_result: | ||||
| 
 | ||||
|             # if do_print: | ||||
|             #     print( | ||||
|             #         f'{self.name} CACHED maxmin\n' | ||||
|             #         f'{rkey} -> {cached_result}' | ||||
|             #     ) | ||||
|             return cached_result | ||||
| 
 | ||||
|         shm = self.shm | ||||
|         if shm is None: | ||||
|             breakpoint() | ||||
|             return None | ||||
| 
 | ||||
|         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 | ||||
|         if self.index_field == 'time': | ||||
|             ( | ||||
|                 abs_slc, | ||||
|                 read_slc, | ||||
|             ) = slice_from_time( | ||||
|             read_slc = slice_from_time( | ||||
|                 arr, | ||||
|                 start_t=lbar, | ||||
|                 stop_t=rbar, | ||||
|  | @ -306,11 +330,17 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
|             ] | ||||
| 
 | ||||
|         if not slice_view.size: | ||||
|             log.warning(f'{self.name} no maxmin in view?') | ||||
|             # breakpoint() | ||||
|             return None | ||||
| 
 | ||||
|         elif 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: | ||||
|             if self.is_ohlc: | ||||
|  | @ -323,7 +353,11 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
|                 yhigh = np.max(view) | ||||
| 
 | ||||
|             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 | ||||
|         assert mxmn | ||||
|  | @ -333,8 +367,7 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
| 
 | ||||
|     def view_range(self) -> tuple[int, int]: | ||||
|         ''' | ||||
|         Return the indexes in view for the associated | ||||
|         plot displaying this viz's data. | ||||
|         Return the start and stop x-indexes for the managed ``ViewBox``. | ||||
| 
 | ||||
|         ''' | ||||
|         vr = self.plot.viewRect() | ||||
|  | @ -345,15 +378,18 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
| 
 | ||||
|     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 | ||||
| 
 | ||||
|     def datums_range( | ||||
|         self, | ||||
|         view_range: None | tuple[float, float] = None, | ||||
|         index_field: str | None = None, | ||||
|         array: None | np.ndarray = None, | ||||
| 
 | ||||
|     ) -> tuple[ | ||||
|         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. | ||||
| 
 | ||||
|         ''' | ||||
|         l, r = self.view_range() | ||||
|         l, r = view_range or self.view_range() | ||||
| 
 | ||||
|         index_field: str = index_field or self.index_field | ||||
|         if index_field == 'index': | ||||
|             l, r = round(l), round(r) | ||||
| 
 | ||||
|         array = self.shm.array | ||||
|         if array is None: | ||||
|             array = self.shm.array | ||||
| 
 | ||||
|         index = array[index_field] | ||||
|         start = index[0] | ||||
|         stop = index[-1] | ||||
|         first = round(index[0]) | ||||
|         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 ( | ||||
|             l < 0 | ||||
|             or r < l | ||||
|             or l < start | ||||
|             r < l | ||||
|             or l < 0 | ||||
|             or r < 0 | ||||
|             or (l > last and r > last) | ||||
|         ): | ||||
|             datum_start = start | ||||
|             datum_stop = stop | ||||
|             leftmost = first | ||||
|             rightmost = last | ||||
|         else: | ||||
|             datum_start = max(l, start) | ||||
|             datum_stop = r | ||||
|             if l < stop: | ||||
|                 datum_stop = min(r, stop) | ||||
|             rightmost = max( | ||||
|                 min(last, rightmost), | ||||
|                 first, | ||||
|             ) | ||||
| 
 | ||||
|         assert datum_start < datum_stop | ||||
|             leftmost = min( | ||||
|                 max(first, leftmost), | ||||
|                 last, | ||||
|                 rightmost - 1, | ||||
|             ) | ||||
| 
 | ||||
|             assert leftmost < rightmost | ||||
| 
 | ||||
|         return ( | ||||
|             start, | ||||
|             l,  # left x-in-view | ||||
|             datum_start, | ||||
|             datum_stop, | ||||
|             first,  # first datum | ||||
|             leftmost, | ||||
|             rightmost, | ||||
|             last,  # last_datum | ||||
|             r,  # right-x-in-view | ||||
|             stop, | ||||
|         ) | ||||
| 
 | ||||
|     def read( | ||||
|  | @ -415,6 +466,7 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
| 
 | ||||
|         ''' | ||||
|         index_field: str = index_field or self.index_field | ||||
|         vr = l, r = self.view_range() | ||||
| 
 | ||||
|         # readable data | ||||
|         array = self.shm.array | ||||
|  | @ -423,13 +475,17 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
|             profiler('self.shm.array READ') | ||||
| 
 | ||||
|         ( | ||||
|             ifirst, | ||||
|             l, | ||||
|             ifirst, | ||||
|             lbar, | ||||
|             rbar, | ||||
|             r, | ||||
|             ilast, | ||||
|         ) = self.datums_range(index_field=index_field) | ||||
|             r, | ||||
|         ) = self.datums_range( | ||||
|             view_range=vr, | ||||
|             index_field=index_field, | ||||
|             array=array, | ||||
|         ) | ||||
|         # if rbar < lbar: | ||||
|         #     breakpoint() | ||||
| 
 | ||||
|  | @ -440,26 +496,22 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
| 
 | ||||
|         # TODO: support time slicing | ||||
|         if index_field == 'time': | ||||
|             ( | ||||
|                 abs_slc, | ||||
|                 read_slc, | ||||
|             ) = slice_from_time( | ||||
|             read_slc = slice_from_time( | ||||
|                 array, | ||||
|                 start_t=lbar, | ||||
|                 stop_t=rbar, | ||||
|             ) | ||||
|             in_view = array[read_slc] | ||||
| 
 | ||||
|             # diff = rbar - lbar | ||||
|             # if ( | ||||
|             #     'btc' in self.name | ||||
|             #     and 'hist' not in self.shm.token | ||||
|             # ): | ||||
|             #     print( | ||||
|             #         f'{self.name}: len(iv) = {len(in_view)}\n' | ||||
|             #         f'start/stop: {lbar},{rbar}\n', | ||||
|             #         f'diff: {diff}\n', | ||||
|             #     ) | ||||
|             # TODO: maybe we should return this from the slicer call | ||||
|             # above? | ||||
|             in_view = array[read_slc] | ||||
|             if in_view.size: | ||||
|                 abs_indx = in_view['index'] | ||||
|                 abs_slc = slice( | ||||
|                     int(abs_indx[0]), | ||||
|                     int(abs_indx[-1]), | ||||
|                 ) | ||||
| 
 | ||||
|             if profiler: | ||||
|                 profiler( | ||||
|                     '`slice_from_time(' | ||||
|  | @ -635,8 +687,6 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
|             should_redraw = True | ||||
| 
 | ||||
|             showing_src_data = True | ||||
|             # reset yrange to be computed from source data | ||||
|             self.yrange = None | ||||
| 
 | ||||
|         # MAIN RENDER LOGIC: | ||||
|         # - determine in view data and redraw on range change | ||||
|  | @ -662,10 +712,6 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
| 
 | ||||
|             **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: | ||||
|             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 | ||||
|         # graphics that don't update until you downsampler again.. | ||||
|         # reset = False | ||||
|         # if reset: | ||||
|         #     with graphics.reset_cache(): | ||||
|         #         # assign output paths to graphicis obj | ||||
|  | @ -798,6 +843,129 @@ class Viz(msgspec.Struct):  # , frozen=True): | |||
|             ) | ||||
|         ).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): | ||||
| 
 | ||||
|  | @ -959,6 +1127,8 @@ class Renderer(msgspec.Struct): | |||
|         fast_path = self.fast_path | ||||
|         reset = False | ||||
| 
 | ||||
|         self.viz.yrange = None | ||||
| 
 | ||||
|         # redraw the entire source data if we have either of: | ||||
|         # - no prior path graphic rendered or, | ||||
|         # - we always intend to re-render the data only in view | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue