From 00be100e71c93568e3c385a3f7fce32c5efbacc4 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 14 Nov 2022 16:25:19 -0500 Subject: [PATCH] Initial chart widget adjustments for agg feeds Main "public" API change is to make `GodWidget.get/set_chart_symbol()` accept and cache-on fqsn tuples to allow handling overlayed chart groups and adjust method names to be plural to match. Wrt `LinkedSplits`, - create all chart widget axes with a `None` plotitem argument and set the `.pi` field after axis creation (since apparently we have another object reference causality dilemma..) - set a monkeyed `PlotItem.chart_widget` for use in axes that still need the widget reference. - drop feed pause/resume for now since it's leaking feed tasks on the `brokerd` side and we probably don't really need it any more, and if we still do it should be done on the feed not the flume. Wrt `ChartPlotItem`, - drop `._add_sticky()` and use the `Axis` method instead and add some overlay + axis sanity checks. - refactor `.draw_ohlc()` to be a lighter wrapper around a call to `.add_plot()`. --- piker/ui/_chart.py | 250 +++++++++++++++++++++++---------------------- 1 file changed, 126 insertions(+), 124 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index bad82544..24ec70c3 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -45,7 +45,6 @@ import trio from ._axes import ( DynamicDateAxis, PriceAxis, - YAxisLabel, ) from ._cursor import ( Cursor, @@ -168,18 +167,18 @@ class GodWidget(QWidget): # self.strategy_box = StrategyBoxWidget(self) # self.toolbar_layout.addWidget(self.strategy_box) - def set_chart_symbol( + def set_chart_symbols( self, - symbol_key: str, # of form . + group_key: tuple[str], # of form . all_linked: tuple[LinkedSplits, LinkedSplits], # type: ignore ) -> None: # re-sort org cache symbol list in LIFO order cache = self._chart_cache - cache.pop(symbol_key, None) - cache[symbol_key] = all_linked + cache.pop(group_key, None) + cache[group_key] = all_linked - def get_chart_symbol( + def get_chart_symbols( self, symbol_key: str, @@ -188,8 +187,7 @@ class GodWidget(QWidget): async def load_symbols( self, - providername: str, - symbol_keys: list[str], + fqsns: list[str], loglevel: str, reset: bool = False, @@ -200,20 +198,11 @@ class GodWidget(QWidget): Expects a ``numpy`` structured array containing all the ohlcv fields. ''' - fqsns: list[str] = [] - - # our symbol key style is always lower case - for key in list(map(str.lower, symbol_keys)): - - # fully qualified symbol name (SNS i guess is what we're making?) - fqsn = '.'.join([key, providername]) - fqsns.append(fqsn) - # NOTE: for now we use the first symbol in the set as the "key" # for the overlay of feeds on the chart. - group_key = fqsns[0] + group_key: tuple[str] = tuple(fqsns) - all_linked = self.get_chart_symbol(group_key) + all_linked = self.get_chart_symbols(group_key) order_mode_started = trio.Event() if not self.vbox.isEmpty(): @@ -245,7 +234,6 @@ class GodWidget(QWidget): self._root_n.start_soon( display_symbol_data, self, - providername, fqsns, loglevel, order_mode_started, @@ -253,8 +241,8 @@ class GodWidget(QWidget): # self.vbox.addWidget(hist_charts) self.vbox.addWidget(rt_charts) - self.set_chart_symbol( - fqsn, + self.set_chart_symbols( + group_key, (hist_charts, rt_charts), ) @@ -568,12 +556,10 @@ class LinkedSplits(QWidget): # be no distinction since we will have multiple symbols per # view as part of "aggregate feeds". self.chart = self.add_plot( - - name=symbol.key, + name=symbol.fqsn, shm=shm, style=style, _is_main=True, - sidepane=sidepane, ) # add crosshair graphic @@ -615,12 +601,13 @@ class LinkedSplits(QWidget): # TODO: we gotta possibly assign this back # to the last subplot on removal of some last subplot xaxis = DynamicDateAxis( + None, orientation='bottom', linkedsplits=self ) axes = { - 'right': PriceAxis(linkedsplits=self, orientation='right'), - 'left': PriceAxis(linkedsplits=self, orientation='left'), + 'right': PriceAxis(None, orientation='right'), + 'left': PriceAxis(None, orientation='left'), 'bottom': xaxis, } @@ -645,6 +632,11 @@ class LinkedSplits(QWidget): axisItems=axes, **cpw_kwargs, ) + # TODO: wow i can't believe how confusing garbage all this axes + # stuff iss.. + for axis in axes.values(): + axis.pi = cpw.plotItem + cpw.hideAxis('left') cpw.hideAxis('bottom') @@ -860,7 +852,12 @@ class ChartPlotWidget(pg.PlotWidget): # source of our custom interactions self.cv = cv = self.mk_vb(name) - pi = pgo.PlotItem(viewBox=cv, **kwargs) + pi = pgo.PlotItem( + viewBox=cv, + name=name, + **kwargs, + ) + pi.chart_widget = self super().__init__( background=hcolor(view_color), viewBox=cv, @@ -913,18 +910,20 @@ class ChartPlotWidget(pg.PlotWidget): self._on_screen: bool = False def resume_all_feeds(self): - try: - for feed in self._feeds.values(): - for flume in feed.flumes.values(): - self.linked.godwidget._root_n.start_soon(feed.resume) - except RuntimeError: - # TODO: cancel the qtractor runtime here? - raise + ... + # try: + # for feed in self._feeds.values(): + # for flume in feed.flumes.values(): + # self.linked.godwidget._root_n.start_soon(flume.resume) + # except RuntimeError: + # # TODO: cancel the qtractor runtime here? + # raise def pause_all_feeds(self): - for feed in self._feeds.values(): - for flume in feed.flumes.values(): - self.linked.godwidget._root_n.start_soon(feed.pause) + ... + # for feed in self._feeds.values(): + # for flume in feed.flumes.values(): + # self.linked.godwidget._root_n.start_soon(flume.pause) @property def view(self) -> ChartView: @@ -1116,43 +1115,6 @@ class ChartPlotWidget(pg.PlotWidget): padding=0, ) - def draw_ohlc( - self, - name: str, - shm: ShmArray, - - array_key: Optional[str] = None, - - ) -> (pg.GraphicsObject, str): - ''' - Draw OHLC datums to chart. - - ''' - graphics = BarItems( - self.linked, - self.plotItem, - pen_color=self.pen_color, - name=name, - ) - - # adds all bar/candle graphics objects for each data point in - # the np array buffer to be drawn on next render cycle - self.plotItem.addItem(graphics) - - data_key = array_key or name - - self._flows[data_key] = Flow( - name=name, - plot=self.plotItem, - _shm=shm, - is_ohlc=True, - graphics=graphics, - ) - - self._add_sticky(name, bg_color='davies') - - return graphics, data_key - def overlay_plotitem( self, name: str, @@ -1172,8 +1134,8 @@ class ChartPlotWidget(pg.PlotWidget): raise ValueError(f'``axis_side``` must be in {allowed_sides}') yaxis = PriceAxis( + plotitem=None, orientation=axis_side, - linkedsplits=self.linked, **axis_kwargs, ) @@ -1188,6 +1150,8 @@ class ChartPlotWidget(pg.PlotWidget): }, default_axes=[], ) + yaxis.pi = pi + pi.chart_widget = self pi.hideButtons() # compose this new plot's graphics with the current chart's @@ -1231,43 +1195,60 @@ class ChartPlotWidget(pg.PlotWidget): add_label: bool = True, pi: Optional[pg.PlotItem] = None, step_mode: bool = False, + is_ohlc: bool = False, + add_sticky: None | str = 'right', - **pdi_kwargs, + **graphics_kwargs, - ) -> (pg.PlotDataItem, str): + ) -> tuple[ + pg.GraphicsObject, + str, + ]: ''' Draw a "curve" (line plot graphics) for the provided data in the input shm array ``shm``. ''' color = color or self.pen_color or 'default_light' - pdi_kwargs.update({ - 'color': color - }) + # graphics_kwargs.update({ + # 'color': color + # }) data_key = array_key or name - curve_type = { - None: Curve, - 'step': StepCurve, - # TODO: - # 'bars': BarsItems - }['step' if step_mode else None] - - curve = curve_type( - name=name, - **pdi_kwargs, - ) - pi = pi or self.plotItem + if is_ohlc: + graphics = BarItems( + linked=self.linked, + plotitem=pi, + # pen_color=self.pen_color, + color=color, + name=name, + **graphics_kwargs, + ) + + else: + curve_type = { + None: Curve, + 'step': StepCurve, + # TODO: + # 'bars': BarsItems + }['step' if step_mode else None] + + graphics = curve_type( + name=name, + color=color, + **graphics_kwargs, + ) + self._flows[data_key] = Flow( name=name, plot=pi, _shm=shm, - is_ohlc=False, + is_ohlc=is_ohlc, # register curve graphics with this flow - graphics=curve, + graphics=graphics, ) # TODO: this probably needs its own method? @@ -1278,12 +1259,41 @@ class ChartPlotWidget(pg.PlotWidget): f'{overlay} must be from `.plotitem_overlay()`' ) pi = overlay - else: - # anchor_at = ('top', 'left') - # TODO: something instead of stickies for overlays - # (we need something that avoids clutter on x-axis). - self._add_sticky(name, bg_color=color) + if add_sticky: + axis = pi.getAxis(add_sticky) + if pi.name not in axis._stickies: + + if pi is not self.plotItem: + overlay = self.pi_overlay + assert pi in overlay.overlays + overlay_axis = overlay.get_axis( + pi, + add_sticky, + ) + assert overlay_axis is axis + + # TODO: UGH! just make this not here! we should + # be making the sticky from code which has access + # to the ``Symbol`` instance.. + + # if the sticky is for our symbol + # use the tick size precision for display + name = name or pi.name + sym = self.linked.symbol + digits = None + if name == sym.key: + digits = sym.tick_size_digits + + # anchor_at = ('top', 'left') + + # TODO: something instead of stickies for overlays + # (we need something that avoids clutter on x-axis). + axis.add_sticky( + pi=pi, + bg_color=color, + digits=digits, + ) # NOTE: this is more or less the RENDER call that tells Qt to # start showing the generated graphics-curves. This is kind of @@ -1294,38 +1304,30 @@ class ChartPlotWidget(pg.PlotWidget): # the next render cycle; just note a lot of the real-time # updates are implicit and require a bit of digging to # understand. - pi.addItem(curve) + pi.addItem(graphics) - return curve, data_key + return graphics, data_key - # TODO: make this a ctx mngr - def _add_sticky( + def draw_ohlc( self, - name: str, - bg_color='bracket', + shm: ShmArray, - ) -> YAxisLabel: + array_key: Optional[str] = None, + **draw_curve_kwargs, - # if the sticky is for our symbol - # use the tick size precision for display - sym = self.linked.symbol - if name == sym.key: - digits = sym.tick_size_digits - else: - digits = 2 + ) -> (pg.GraphicsObject, str): + ''' + Draw OHLC datums to chart. - # add y-axis "last" value label - last = self._ysticks[name] = YAxisLabel( - chart=self, - # parent=self.getAxis('right'), - parent=self.pi_overlay.get_axis(self.plotItem, 'right'), - # TODO: pass this from symbol data - digits=digits, - opacity=1, - bg_color=bg_color, + ''' + return self.draw_curve( + name=name, + shm=shm, + array_key=array_key, + is_ohlc=True, + **draw_curve_kwargs, ) - return last def update_graphics_from_flow( self,