From 34fac364fdf8c4e0523fff391e79593be1df5521 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 13 Nov 2022 18:23:33 -0500 Subject: [PATCH 01/12] Add default YAxisLable.x_offset: int` --- piker/ui/_axes.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/piker/ui/_axes.py b/piker/ui/_axes.py index 3ed5b420..6753aaa1 100644 --- a/piker/ui/_axes.py +++ b/piker/ui/_axes.py @@ -522,7 +522,7 @@ class XAxisLabel(AxisLabel): class YAxisLabel(AxisLabel): - _y_margin = 4 + _y_margin: int = 4 text_flags = ( QtCore.Qt.AlignLeft @@ -546,6 +546,7 @@ class YAxisLabel(AxisLabel): self._last_datum = (None, None) + self.x_offset = 0 # pull text offset from axis from parent axis if getattr(self._parent, 'txt_offsets', False): self.x_offset, y_offset = self._parent.txt_offsets() @@ -564,7 +565,8 @@ class YAxisLabel(AxisLabel): value: float, # data for text # on odd dimension and/or adds nice black line - x_offset: Optional[int] = None + x_offset: int = 0, + ) -> None: # this is read inside ``.paint()`` From 31af7a2c997efe1451cf213a2129d5cb1ea95ce9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 14 Nov 2022 15:09:00 -0500 Subject: [PATCH 02/12] Add `Axis.add_sticky()` for creating axis labels We have this method on our `ChartPlotWidget` but it makes more sense to directly associate axis-labels with, well, the label's parent axis XD. We add `._stickies: dict[str, YAxisLabel]` to replace `ChartPlotWidget._ysticks` and pass in the `pg.PlotItem` to each axis instance, stored as `Axis.pi` instead of handing around linked split references (which are way out of scope for a single axis). More work needs to be done to remove dependence on `.chart: ChartPlotWidget` references in the date axis type as per comments. --- piker/ui/_axes.py | 77 +++++++++++++++++++++++++++++++++++++---------- 1 file changed, 61 insertions(+), 16 deletions(-) diff --git a/piker/ui/_axes.py b/piker/ui/_axes.py index 6753aaa1..2ee60bb9 100644 --- a/piker/ui/_axes.py +++ b/piker/ui/_axes.py @@ -18,6 +18,7 @@ Chart axes graphics and behavior. """ +from __future__ import annotations from functools import lru_cache from typing import Optional, Callable from math import floor @@ -27,6 +28,7 @@ import pyqtgraph as pg from PyQt5 import QtCore, QtGui, QtWidgets from PyQt5.QtCore import QPointF +from . import _pg_overrides as pgo from ..data._source import float_digits from ._label import Label from ._style import DpiAwareFont, hcolor, _font @@ -46,7 +48,7 @@ class Axis(pg.AxisItem): ''' def __init__( self, - linkedsplits, + plotitem: pgo.PlotItem, typical_max_str: str = '100 000.000', text_color: str = 'bracket', lru_cache_tick_strings: bool = True, @@ -61,27 +63,32 @@ class Axis(pg.AxisItem): # XXX: pretty sure this makes things slower # self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) - self.linkedsplits = linkedsplits + self.pi = plotitem self._dpi_font = _font self.setTickFont(_font.font) font_size = self._dpi_font.font.pixelSize() + style_conf = { + 'textFillLimits': [(0, 0.5)], + 'tickFont': self._dpi_font.font, + + } + text_offset = None if self.orientation in ('bottom',): text_offset = floor(0.25 * font_size) elif self.orientation in ('left', 'right'): text_offset = floor(font_size / 2) - self.setStyle(**{ - 'textFillLimits': [(0, 0.5)], - 'tickFont': self._dpi_font.font, - - # offset of text *away from* axis line in px - # use approx. half the font pixel size (height) - 'tickTextOffset': text_offset, - }) + if text_offset: + style_conf.update({ + # offset of text *away from* axis line in px + # use approx. half the font pixel size (height) + 'tickTextOffset': text_offset, + }) + self.setStyle(**style_conf) self.setTickFont(_font.font) # NOTE: this is for surrounding "border" @@ -102,6 +109,9 @@ class Axis(pg.AxisItem): maxsize=2**20 )(self.tickStrings) + # axis "sticky" labels + self._stickies: dict[str, YAxisLabel] = {} + # NOTE: only overriden to cast tick values entries into tuples # for use with the lru caching. def tickValues( @@ -139,6 +149,40 @@ class Axis(pg.AxisItem): def txt_offsets(self) -> tuple[int, int]: return tuple(self.style['tickTextOffset']) + def add_sticky( + self, + pi: pgo.PlotItem, + name: None | str = None, + digits: None | int = 2, + # axis_name: str = 'right', + bg_color='bracket', + + ) -> YAxisLabel: + + # if the sticky is for our symbol + # use the tick size precision for display + name = name or pi.name + digits = digits or 2 + + # TODO: ``._ysticks`` should really be an attr on each + # ``PlotItem`` no instead of the (containing because of + # overlays) widget? + + # add y-axis "last" value label + sticky = self._stickies[name] = YAxisLabel( + pi=pi, + parent=self, + # TODO: pass this from symbol data + digits=digits, + opacity=1, + bg_color=bg_color, + ) + + pi.sigRangeChanged.connect(sticky.update_on_resize) + # pi.addItem(sticky) + # pi.addItem(last) + return sticky + class PriceAxis(Axis): @@ -255,7 +299,9 @@ class DynamicDateAxis(Axis): ) -> list[str]: - chart = self.linkedsplits.chart + # XX: ARGGGGG AG:LKSKDJF:LKJSDFD + chart = self.pi.chart_widget + flow = chart._flows[chart.name] shm = flow.shm bars = shm.array @@ -533,16 +579,15 @@ class YAxisLabel(AxisLabel): def __init__( self, - chart, + pi: pgo.PlotItem, *args, **kwargs ) -> None: super().__init__(*args, **kwargs) - self._chart = chart - - chart.sigRangeChanged.connect(self.update_on_resize) + self._pi = pi + pi.sigRangeChanged.connect(self.update_on_resize) self._last_datum = (None, None) @@ -612,7 +657,7 @@ class YAxisLabel(AxisLabel): self._last_datum = (index, value) self.update_label( - self._chart.mapFromView(QPointF(index, value)), + self._pi.mapFromView(QPointF(index, value)), value ) From 92176107348b6add405b871d4a700befa5c91dbe Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 14 Nov 2022 16:17:24 -0500 Subject: [PATCH 03/12] Simplify OHLC graphic color instance var name --- piker/ui/_ohlc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/piker/ui/_ohlc.py b/piker/ui/_ohlc.py index b2ff6e10..a8519d90 100644 --- a/piker/ui/_ohlc.py +++ b/piker/ui/_ohlc.py @@ -98,7 +98,7 @@ class BarItems(pg.GraphicsObject): self, linked: LinkedSplits, plotitem: 'pg.PlotItem', # noqa - pen_color: str = 'bracket', + color: str = 'bracket', last_bar_color: str = 'bracket', name: Optional[str] = None, @@ -108,8 +108,8 @@ class BarItems(pg.GraphicsObject): self.linked = linked # XXX: for the mega-lulz increasing width here increases draw # latency... so probably don't do it until we figure that out. - self._color = pen_color - self.bars_pen = pg.mkPen(hcolor(pen_color), width=1) + self._color = color + self.bars_pen = pg.mkPen(hcolor(color), width=1) self.last_bar_pen = pg.mkPen(hcolor(last_bar_color), width=2) self._name = name From 00be100e71c93568e3c385a3f7fce32c5efbacc4 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 14 Nov 2022 16:25:19 -0500 Subject: [PATCH 04/12] 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, From a39c980266f50ce7ea3546e3af6dbd749771fee2 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 14 Nov 2022 16:52:48 -0500 Subject: [PATCH 05/12] Allocate our internal `Axis` subtype in our `PlotItem` override --- piker/ui/_pg_overrides.py | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/piker/ui/_pg_overrides.py b/piker/ui/_pg_overrides.py index a961e567..397954fd 100644 --- a/piker/ui/_pg_overrides.py +++ b/piker/ui/_pg_overrides.py @@ -26,6 +26,8 @@ from typing import Optional import pyqtgraph as pg +from ._axes import Axis + def invertQTransform(tr): """Return a QTransform that is the inverse of *tr*. @@ -62,6 +64,20 @@ class PlotItem(pg.PlotItem): Overrides for the core plot object mostly pertaining to overlayed multi-view management as it relates to multi-axis managment. + This object is the combination of a ``ViewBox`` and multiple + ``AxisItem``s and so far we've added additional functionality and + APIs for: + - removal of axes + + --- + + From ``pyqtgraph`` super type docs: + - Manage placement of ViewBox, AxisItems, and LabelItems + - Create and manage a list of PlotDataItems displayed inside the + ViewBox + - Implement a context menu with commonly used display and analysis + options + ''' def __init__( self, @@ -86,6 +102,8 @@ class PlotItem(pg.PlotItem): enableMenu=enableMenu, kargs=kargs, ) + self.name = name + self.chart_widget = None # self.setAxisItems( # axisItems, # default_axes=default_axes, @@ -209,7 +227,12 @@ class PlotItem(pg.PlotItem): # adding this is without it there's some weird # ``ViewBox`` geometry bug.. where a gap for the # 'bottom' axis is somehow left in? - axis = pg.AxisItem(orientation=name, parent=self) + # axis = pg.AxisItem(orientation=name, parent=self) + axis = Axis( + self, + orientation=name, + parent=self, + ) axis.linkToView(self.vb) From 727c7ce2b1e9d419940e6aa1dbe249afa35d5bf6 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 14 Nov 2022 16:53:44 -0500 Subject: [PATCH 06/12] Adjust L1 labels to expect `.pi: PlotItem` --- piker/ui/_l1.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/piker/ui/_l1.py b/piker/ui/_l1.py index bfa0551e..0c6e4e1a 100644 --- a/piker/ui/_l1.py +++ b/piker/ui/_l1.py @@ -26,6 +26,7 @@ from PyQt5.QtCore import QPointF from ._axes import YAxisLabel from ._style import hcolor +from ._pg_overrides import PlotItem class LevelLabel(YAxisLabel): @@ -132,7 +133,7 @@ class LevelLabel(YAxisLabel): level = self.fields['level'] # map "level" to local coords - abs_xy = self._chart.mapFromView(QPointF(0, level)) + abs_xy = self._pi.mapFromView(QPointF(0, level)) self.update_label( abs_xy, @@ -149,7 +150,7 @@ class LevelLabel(YAxisLabel): h, w = self.set_label_str(fields) if self._adjust_to_l1: - self._x_offset = self._chart._max_l1_line_len + self._x_offset = self._pi.chart_widget._max_l1_line_len self.setPos(QPointF( self._h_shift * (w + self._x_offset), @@ -236,10 +237,10 @@ class L1Label(LevelLabel): # Set a global "max L1 label length" so we can # look it up on order lines and adjust their # labels not to overlap with it. - chart = self._chart + chart = self._pi.chart_widget chart._max_l1_line_len: float = max( chart._max_l1_line_len, - w + w, ) return h, w @@ -251,17 +252,17 @@ class L1Labels: """ def __init__( self, - chart: 'ChartPlotWidget', # noqa + plotitem: PlotItem, digits: int = 2, size_digits: int = 3, font_size: str = 'small', ) -> None: - self.chart = chart + chart = self.chart = plotitem.chart_widget - raxis = chart.getAxis('right') + raxis = plotitem.getAxis('right') kwargs = { - 'chart': chart, + 'chart': plotitem, 'parent': raxis, 'opacity': 1, From ae0f3118f4b4a4176209fbbf6c24a912613841db Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 14 Nov 2022 17:06:18 -0500 Subject: [PATCH 07/12] Pass plotitem to axis from cursor --- piker/ui/_cursor.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/piker/ui/_cursor.py b/piker/ui/_cursor.py index a27aca8c..fd00c380 100644 --- a/piker/ui/_cursor.py +++ b/piker/ui/_cursor.py @@ -418,7 +418,7 @@ class Cursor(pg.GraphicsObject): hl.hide() yl = YAxisLabel( - chart=plot, + pi=plot.plotItem, # parent=plot.getAxis('right'), parent=plot.pi_overlay.get_axis(plot.plotItem, 'right'), digits=digits or self.digits, From 36a81cb2de56f9da10cd3208540223f9a61a8814 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 15 Nov 2022 15:04:28 -0500 Subject: [PATCH 08/12] Only add plot to cursor set if not an overlay --- piker/ui/_chart.py | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index 24ec70c3..e3967f81 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -483,7 +483,11 @@ class LinkedSplits(QWidget): from . import _display ds = self.display_state if ds: - return _display.graphics_update_cycle(ds, **kwargs) + return _display.graphics_update_cycle( + ds, + ds.quotes, + **kwargs, + ) @property def symbol(self) -> Symbol: @@ -536,7 +540,7 @@ class LinkedSplits(QWidget): shm: ShmArray, sidepane: FieldsForm, - style: str = 'bar', + style: str = 'ohlc_bar', ) -> ChartPlotWidget: ''' @@ -699,7 +703,7 @@ class LinkedSplits(QWidget): anchor_at = ('top', 'left') # draw curve graphics - if style == 'bar': + if style == 'ohlc_bar': graphics, data_key = cpw.draw_ohlc( name, @@ -736,30 +740,33 @@ class LinkedSplits(QWidget): else: raise ValueError(f"Chart style {style} is currently unsupported") - if not _is_main: + if _is_main: + assert style == 'ohlc_bar', 'main chart must be OHLC' + else: # track by name self.subplots[name] = cpw if qframe is not None: self.splitter.addWidget(qframe) - else: - assert style == 'bar', 'main chart must be OHLC' - # add to cross-hair's known plots # NOTE: add **AFTER** creating the underlying ``PlotItem``s # since we require that global (linked charts wide) axes have # been created! - self.cursor.add_plot(cpw) + if self.cursor: + if ( + _is_main + or style != 'ohlc_bar' + ): + self.cursor.add_plot(cpw) + if style != 'ohlc_bar': + self.cursor.add_curve_cursor(cpw, graphics) - if self.cursor and style != 'bar': - self.cursor.add_curve_cursor(cpw, graphics) - - if add_label: - self.cursor.contents_labels.add_label( - cpw, - data_key, - anchor_at=anchor_at, - ) + if add_label: + self.cursor.contents_labels.add_label( + cpw, + data_key, + anchor_at=anchor_at, + ) self.resize_sidepanes() return cpw @@ -1150,6 +1157,7 @@ class ChartPlotWidget(pg.PlotWidget): }, default_axes=[], ) + # pi.vb.background.setOpacity(0) yaxis.pi = pi pi.chart_widget = self pi.hideButtons() @@ -1210,10 +1218,6 @@ class ChartPlotWidget(pg.PlotWidget): ''' color = color or self.pen_color or 'default_light' - # graphics_kwargs.update({ - # 'color': color - # }) - data_key = array_key or name pi = pi or self.plotItem From 58b42d629fa5594f4683116433745efd671420ff Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 14 Nov 2022 16:50:41 -0500 Subject: [PATCH 09/12] Passthrough fqsns list directly to `.load_symbols()` --- piker/ui/_app.py | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/piker/ui/_app.py b/piker/ui/_app.py index 23a9d2ed..2743103e 100644 --- a/piker/ui/_app.py +++ b/piker/ui/_app.py @@ -118,17 +118,10 @@ async def _async_main( # godwidget.hbox.addWidget(search) godwidget.search = search - symbols: list[str] = [] - - for sym in syms: - symbol, _, provider = sym.rpartition('.') - symbols.append(symbol) - # this internally starts a ``display_symbol_data()`` task above order_mode_ready = await godwidget.load_symbols( - provider, - symbols, - loglevel + fqsns=syms, + loglevel=loglevel, ) # spin up a search engine for the local cached symbol set From d57bc6c6d90496836896218f2b03f3aa9d5cb7a7 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 31 Jan 2023 15:15:56 -0500 Subject: [PATCH 10/12] Adjust to using `PlotItem`s for axis sticky mgmt --- piker/ui/_display.py | 15 +++++++++------ piker/ui/_fsp.py | 6 ++++-- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/piker/ui/_display.py b/piker/ui/_display.py index c7ed9299..babbfa7a 100644 --- a/piker/ui/_display.py +++ b/piker/ui/_display.py @@ -260,12 +260,14 @@ async def graphics_update_loop( hist_ohlcv = flume.hist_shm # update last price sticky - last_price_sticky = fast_chart._ysticks[fast_chart.name] + last_price_sticky = fast_chart.plotItem.getAxis( + 'right')._stickies.get(fast_chart.name) last_price_sticky.update_from_data( *ohlcv.array[-1][['index', 'close']] ) - hist_last_price_sticky = hist_chart._ysticks[hist_chart.name] + hist_last_price_sticky = hist_chart.plotItem.getAxis( + 'right')._stickies.get(hist_chart.name) hist_last_price_sticky.update_from_data( *hist_ohlcv.array[-1][['index', 'close']] ) @@ -289,7 +291,7 @@ async def graphics_update_loop( symbol = fast_chart.linked.symbol l1 = L1Labels( - fast_chart, + fast_chart.plotItem, # determine precision/decimal lengths digits=symbol.tick_size_digits, size_digits=symbol.lot_size_digits, @@ -333,7 +335,8 @@ async def graphics_update_loop( }) if vlm_chart: - vlm_sticky = vlm_chart._ysticks['volume'] + vlm_sticky = vlm_chart.plotItem.getAxis( + 'right')._stickies.get('volume') ds.vlm_chart = vlm_chart ds.vlm_sticky = vlm_sticky @@ -947,7 +950,6 @@ async def link_views_with_region( async def display_symbol_data( godwidget: GodWidget, - provider: str, fqsns: list[str], loglevel: str, order_mode_started: trio.Event, @@ -999,6 +1001,7 @@ async def display_symbol_data( symbol = flume.symbol fqsn = symbol.fqsn + brokername = symbol.brokers[0] step_size_s = 1 tf_key = tf_in_1s[step_size_s] @@ -1082,7 +1085,7 @@ async def display_symbol_data( # if available load volume related built-in display(s) if ( - not symbol.broker_info[provider].get('no_vlm', False) + not symbol.broker_info[brokername].get('no_vlm', False) and has_vlm(ohlcv) ): vlm_chart = await ln.start( diff --git a/piker/ui/_fsp.py b/piker/ui/_fsp.py index 9e05f545..8c2e64a1 100644 --- a/piker/ui/_fsp.py +++ b/piker/ui/_fsp.py @@ -110,7 +110,8 @@ def update_fsp_chart( # sub-charts reference it under different 'named charts'. # read from last calculated value and update any label - last_val_sticky = chart._ysticks.get(graphics_name) + last_val_sticky = chart.plotItem.getAxis( + 'right')._stickies.get(chart.name) if last_val_sticky: last = last_row[array_key] last_val_sticky.update_from_data(-1, last) @@ -685,7 +686,8 @@ async def open_vlm_displays( assert chart.name != linked.chart.name # sticky only on sub-charts atm - last_val_sticky = chart._ysticks[chart.name] + last_val_sticky = chart.plotItem.getAxis( + 'right')._stickies.get(chart.name) # read from last calculated value value = shm.array['volume'][-1] From 6100bd19c738976728181da25aad8db74ec7e0d9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Tue, 15 Nov 2022 11:22:08 -0500 Subject: [PATCH 11/12] Adjust search to handle multi-sym results --- piker/ui/_search.py | 115 ++++++++++++++++++++++++++++++++------------ 1 file changed, 83 insertions(+), 32 deletions(-) diff --git a/piker/ui/_search.py b/piker/ui/_search.py index 6c7c6fd8..36b16132 100644 --- a/piker/ui/_search.py +++ b/piker/ui/_search.py @@ -416,12 +416,26 @@ class CompleterView(QTreeView): section: str, values: Sequence[str], clear_all: bool = False, + reverse: bool = False, ) -> None: ''' Set result-rows for depth = 1 tree section ``section``. ''' + if ( + values + and not isinstance(values[0], str) + ): + flattened: list[str] = [] + for val in values: + flattened.extend(val) + + values = flattened + + if reverse: + values = reversed(values) + model = self.model() if clear_all: # XXX: rewrite the model from scratch if caller requests it @@ -598,22 +612,34 @@ class SearchWidget(QtWidgets.QWidget): self.show() self.bar.focus() - def show_only_cache_entries(self) -> None: + def show_cache_entries( + self, + only: bool = False, + ) -> None: ''' Clear the search results view and show only cached (aka recently loaded with active data) feeds in the results section. ''' godw = self.godwidget + + # first entry in the cache is the current symbol(s) + fqsns = [] + + for multi_fqsns in list(godw._chart_cache): + fqsns.extend(list(multi_fqsns)) + self.view.set_section_entries( 'cache', - list(reversed(godw._chart_cache)), + list(fqsns), # remove all other completion results except for cache - clear_all=True, + clear_all=only, + reverse=True, ) def get_current_item(self) -> Optional[tuple[str, str]]: - '''Return the current completer tree selection as + ''' + Return the current completer tree selection as a tuple ``(parent: str, child: str)`` if valid, else ``None``. ''' @@ -663,12 +689,13 @@ class SearchWidget(QtWidgets.QWidget): provider, symbol = value godw = self.godwidget - log.info(f'Requesting symbol: {symbol}.{provider}') + fqsn = f'{symbol}.{provider}' + log.info(f'Requesting symbol: {fqsn}') + # assert provider in symbol await godw.load_symbols( - provider, - [symbol], - 'info', + fqsns=[fqsn], + loglevel='info', ) # fully qualified symbol name (SNS i guess is what we're @@ -682,13 +709,13 @@ class SearchWidget(QtWidgets.QWidget): # Re-order the symbol cache on the chart to display in # LIFO order. this is normally only done internally by # the chart on new symbols being loaded into memory - godw.set_chart_symbol( - fqsn, ( + godw.set_chart_symbols( + (fqsn,), ( godw.hist_linked, godw.rt_linked, ) ) - self.show_only_cache_entries() + self.show_cache_entries(only=True) self.bar.focus() return fqsn @@ -757,9 +784,10 @@ async def pack_matches( with trio.CancelScope() as cs: task_status.started(cs) # ensure ^ status is updated - results = await search(pattern) + results = list(await search(pattern)) - if provider != 'cache': # XXX: don't cache the cache results xD + # XXX: don't cache the cache results xD + if provider != 'cache': matches[(provider, pattern)] = results # print(f'results from {provider}: {results}') @@ -806,7 +834,7 @@ async def fill_results( has_results: defaultdict[str, set[str]] = defaultdict(set) # show cached feed list at startup - search.show_only_cache_entries() + search.show_cache_entries() search.on_resize() while True: @@ -860,8 +888,9 @@ async def fill_results( # it hasn't already been searched with the current # input pattern (in which case just look up the old # results). - if (period >= pause) and ( - provider not in already_has_results + if ( + period >= pause + and provider not in already_has_results ): # TODO: it may make more sense TO NOT search the @@ -869,7 +898,9 @@ async def fill_results( # cpu-bound. if provider != 'cache': view.clear_section( - provider, status_field='-> searchin..') + provider, + status_field='-> searchin..', + ) await n.start( pack_matches, @@ -890,11 +921,20 @@ async def fill_results( # re-searching it's ``dict`` since it's easier # but it also causes it to be slower then cached # results from other providers on occasion. - if results and provider != 'cache': - view.set_section_entries( - section=provider, - values=results, - ) + if ( + results + ): + if provider != 'cache': + view.set_section_entries( + section=provider, + values=results, + ) + else: + # if provider == 'cache': + # for the cache just show what we got + # that matches + search.show_cache_entries() + else: view.clear_section(provider) @@ -937,7 +977,7 @@ async def handle_keyboard_input( ) bar.focus() - search.show_only_cache_entries() + search.show_cache_entries() await trio.sleep(0) async for kbmsg in recv_chan: @@ -949,20 +989,21 @@ async def handle_keyboard_input( if mods == Qt.ControlModifier: ctl = True - if key in (Qt.Key_Enter, Qt.Key_Return): + if key in ( + Qt.Key_Enter, + Qt.Key_Return + ): _search_enabled = False await search.chart_current_item(clear_to_cache=True) - search.show_only_cache_entries() + search.show_cache_entries(only=True) view.show_matches() search.focus() elif not ctl and not bar.text(): - # if nothing in search text show the cache - view.set_section_entries( - 'cache', - list(reversed(godwidget._chart_cache)), - clear_all=True, - ) + + # TODO: really should factor this somewhere..bc + # we're doin it in another spot as well.. + search.show_cache_entries(only=True) continue # cancel and close @@ -1025,6 +1066,8 @@ async def handle_keyboard_input( if parent_item and parent_item.text() == 'cache': await search.chart_current_item(clear_to_cache=False) + # ACTUAL SEARCH BLOCK # + # where we fuzzy complete and fill out sections. elif not ctl: # relay to completer task _search_enabled = True @@ -1035,13 +1078,21 @@ async def handle_keyboard_input( async def search_simple_dict( text: str, source: dict, + ) -> dict[str, Any]: + tokens = [] + for key in source: + if not isinstance(key, str): + tokens.extend(key) + else: + tokens.append(key) + # search routine can be specified as a function such # as in the case of the current app's local symbol cache matches = fuzzy.extractBests( text, - source.keys(), + tokens, score_cutoff=90, ) From 50ad7370c75bd2ab203bbdaacf68a40256e697bd Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Sun, 5 Feb 2023 15:27:01 -0500 Subject: [PATCH 12/12] Adjust chart call to graphics cycle to not pass quotes Was breaking the `'r'` hotkey to reset the chart.. --- piker/ui/_chart.py | 1 - 1 file changed, 1 deletion(-) diff --git a/piker/ui/_chart.py b/piker/ui/_chart.py index e3967f81..6ae30a84 100644 --- a/piker/ui/_chart.py +++ b/piker/ui/_chart.py @@ -485,7 +485,6 @@ class LinkedSplits(QWidget): if ds: return _display.graphics_update_cycle( ds, - ds.quotes, **kwargs, )