From 90b817eb6905a9af75c8b8c8f71be8c378c2e620 Mon Sep 17 00:00:00 2001 From: goodboy Date: Fri, 30 Jan 2026 19:04:20 -0500 Subject: [PATCH] Add annot refreshed-positioning to `Viz` iface Extend `Viz` with dynamic annot repositioning logic in a new `._reposition_annotations()` method. Try calling it inside `Viz.update_graphics()/.reset_graphics()` to attempt keeping annots aligned with underlying data coords. Also, - add index-range cache to skip redundant repositioning - re-enable backfill force-redraw match block in `.ui._display.increment_history_view()` * uncomment `viz` and `name` bindings for match block use to make the above valid. - claude did some weird `profiler()` as logger thing that we'll need to correct, weird how it only did it once and the other was using `log` XD (this patch was generated in some part by [`claude-code`][claude-code-gh]) [claude-code-gh]: https://github.com/anthropics/claude-code --- piker/ui/_dataviz.py | 79 ++++++++++++++++++++++++++++++++++++++++++++ piker/ui/_display.py | 46 +++++++++++++------------- 2 files changed, 102 insertions(+), 23 deletions(-) diff --git a/piker/ui/_dataviz.py b/piker/ui/_dataviz.py index 36251e48..81701098 100644 --- a/piker/ui/_dataviz.py +++ b/piker/ui/_dataviz.py @@ -1031,12 +1031,83 @@ class Viz(Struct): # track downsampled state self._in_ds = r._in_ds + # XXX: reposition annotations after graphics update + # to ensure alignment with (potentially changed) data coords + if should_redraw or force_redraw: + n = self._reposition_annotations() + if n: + profiler(f'repositioned {n} annotations') + return ( True, (ivl, ivr), graphics, ) + # class-level cache for tracking last repositioned index range + # to avoid redundant repositioning when shm hasn't changed + _annot_index_cache: dict[str, tuple[int, int]] = {} + + def _reposition_annotations( + self, + force: bool = False, + ) -> int: + ''' + Reposition all annotations (arrows, text, rects) that have + stored absolute coordinates to ensure they stay aligned + with viz data after updates/redraws. + + Only repositions if shm index range has changed since last + reposition, unless `force=True`. + + ''' + # check if shm index range changed + arr = self.shm.array + if not arr.size: + return 0 + + ifirst = arr[0]['index'] + ilast = arr[-1]['index'] + index_range = (ifirst, ilast) + + # skip if range unchanged (unless forced) + cache_key: str = self.name + last_range = self._annot_index_cache.get(cache_key) + if ( + not force + and last_range is not None + and last_range == index_range + ): + return 0 + + # cache current range + self._annot_index_cache[cache_key] = index_range + + n_repositioned: int = 0 + for item in self.plot.items: + # arrows and text items use abs x,y coords + if ( + hasattr(item, '_abs_x') + and + hasattr(item, '_abs_y') + ): + item.setPos( + item._abs_x, + item._abs_y, + ) + n_repositioned += 1 + + # rects use method + kwargs + elif ( + hasattr(item, '_meth') + and + hasattr(item, '_kwargs') + ): + getattr(item, item._meth)(**item._kwargs) + n_repositioned += 1 + + return n_repositioned + def reset_graphics( self, @@ -1070,6 +1141,14 @@ class Viz(Struct): self.update_graphics(force_redraw=True) self._mxmn_cache_enabled = True + # reposition annotations to stay aligned after reset + # (force=True since reset always changes coordinate system) + n = self._reposition_annotations(force=True) + if n: + log.info( + f'Repositioned {n} annotation(s) after reset' + ) + def draw_last( self, array_key: str | None = None, diff --git a/piker/ui/_display.py b/piker/ui/_display.py index 690bfb18..61a39d01 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -211,9 +211,9 @@ async def increment_history_view( ): hist_chart: ChartPlotWidget = ds.hist_chart hist_viz: Viz = ds.hist_viz - # viz: Viz = ds.viz + viz: Viz = ds.viz assert 'hist' in hist_viz.shm.token['shm_name'] - # name: str = hist_viz.name + name: str = hist_viz.name # TODO: seems this is more reliable at keeping the slow # chart incremented in view more correctly? @@ -250,27 +250,27 @@ async def increment_history_view( # - samplerd could emit the actual update range via # tuple and then we only enter the below block if that # range is detected as in-view? - # match msg: - # case { - # 'backfilling': (viz_name, timeframe), - # } if ( - # viz_name == name - # ): - # log.warning( - # f'Forcing HARD REDRAW:\n' - # f'name: {name}\n' - # f'timeframe: {timeframe}\n' - # ) - # # TODO: only allow this when the data is IN VIEW! - # # also, we probably can do this more efficiently - # # / smarter by only redrawing the portion of the - # # path necessary? - # { - # 60: hist_viz, - # 1: viz, - # }[timeframe].update_graphics( - # force_redraw=True - # ) + match msg: + case { + 'backfilling': (viz_name, timeframe), + } if ( + viz_name == name + ): + log.warning( + f'Forcing HARD REDRAW:\n' + f'name: {name}\n' + f'timeframe: {timeframe}\n' + ) + # TODO: only allow this when the data is IN VIEW! + # also, we probably can do this more efficiently + # / smarter by only redrawing the portion of the + # path necessary? + { + 60: hist_viz, + 1: viz, + }[timeframe].update_graphics( + force_redraw=True + ) # check if slow chart needs an x-domain shift and/or # y-range resize.