From 8efa2d2fc752e588900a6efe493a4d0993ede4d9 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Thu, 10 Dec 2020 14:30:40 -0500 Subject: [PATCH] First draft, make graphics work on shm primary index This is a bit hacky (what with array indexing semantics being relative to the primary index's "start" value but it works. We'll likely want to somehow wrap this index finagling into an API soon. --- piker/ui/_graphics.py | 238 ++++++++++++++++++++++++++++++++---------- 1 file changed, 182 insertions(+), 56 deletions(-) diff --git a/piker/ui/_graphics.py b/piker/ui/_graphics.py index e1dbdaf2..83cbd5b5 100644 --- a/piker/ui/_graphics.py +++ b/piker/ui/_graphics.py @@ -33,14 +33,15 @@ from ._style import ( _xaxis_at, hcolor, _font, + _down_2_font_inches_we_like, ) from ._axes import YAxisLabel, XAxisLabel, YSticky # XXX: these settings seem to result in really decent mouse scroll # latency (in terms of perceived lag in cross hair) so really be sure -# there's an improvement if you want to change it. -_mouse_rate_limit = 60 # calc current screen refresh rate? +# there's an improvement if you want to change it! +_mouse_rate_limit = 60 # TODO; should we calc current screen refresh rate? _debounce_delay = 1 / 2e3 _ch_label_opac = 1 @@ -53,6 +54,7 @@ class LineDot(pg.CurvePoint): self, curve: pg.PlotCurveItem, index: int, + plot: 'ChartPlotWidget', pos=None, size: int = 2, # in pxs color: str = 'default_light', @@ -64,6 +66,7 @@ class LineDot(pg.CurvePoint): pos=pos, rotate=False, ) + self._plot = plot # TODO: get pen from curve if not defined? cdefault = hcolor(color) @@ -83,6 +86,31 @@ class LineDot(pg.CurvePoint): # keep a static size self.setFlag(self.ItemIgnoresTransformations) + def event( + self, + ev: QtCore.QEvent, + ) -> None: + # print((ev, type(ev))) + if not isinstance(ev, QtCore.QDynamicPropertyChangeEvent) or self.curve() is None: + return False + + # if ev.propertyName() == 'index': + # print(ev) + # # self.setProperty + + (x, y) = self.curve().getData() + index = self.property('index') + # first = self._plot._ohlc[0]['index'] + # first = x[0] + # i = index - first + i = index - x[0] + if i > 0: + newPos = (index, y[i]) + QtGui.QGraphicsItem.setPos(self, *newPos) + return True + + return False + _corner_anchors = { 'top': 0, @@ -94,8 +122,10 @@ _corner_anchors = { _corner_margins = { ('top', 'left'): (-4, -5), ('top', 'right'): (4, -5), - ('bottom', 'left'): (-4, 5), - ('bottom', 'right'): (4, 5), + + # TODO: pretty sure y here needs to be 2x font height + ('bottom', 'left'): (-4, 14), + ('bottom', 'right'): (4, 14), } @@ -132,15 +162,19 @@ class ContentsLabel(pg.LabelItem): array: np.ndarray, ) -> None: # this being "html" is the dumbest shit :eyeroll: + first = array[0]['index'] + self.setText( "i:{index}
" "O:{}
" "H:{}
" "L:{}
" "C:{}
" - "V:{}".format( - # *self._array[index].item()[2:8], - *array[index].item()[2:8], + "V:{}
" + "wap:{}".format( + *array[index - first][ + ['open', 'high', 'low', 'close', 'volume', 'bar_wap'] + ], name=name, index=index, ) @@ -152,8 +186,9 @@ class ContentsLabel(pg.LabelItem): index: int, array: np.ndarray, ) -> None: - if index < len(array): - data = array[index][name] + first = array[0]['index'] + if index < array[-1]['index'] and index > first: + data = array[index - first][name] self.setText(f"{name}: {data:.2f}") @@ -250,7 +285,7 @@ class CrossHair(pg.GraphicsObject): ) -> LineDot: # if this plot contains curves add line dot "cursors" to denote # the current sample under the mouse - cursor = LineDot(curve, index=len(plot._ohlc)) + cursor = LineDot(curve, index=plot._ohlc[-1]['index'], plot=plot) plot.addItem(cursor) self.graphics[plot].setdefault('cursors', []).append(cursor) return cursor @@ -312,8 +347,9 @@ class CrossHair(pg.GraphicsObject): plot.update_contents_labels(ix) # update all subscribed curve dots + # first = plot._ohlc[0]['index'] for cursor in opts.get('cursors', ()): - cursor.setIndex(ix) + cursor.setIndex(ix) # - first) # update the label on the bottom of the crosshair self.xaxis_label.update_label( @@ -375,7 +411,7 @@ def lines_from_ohlc(row: np.ndarray, w: float) -> Tuple[QLineF]: return [hl, o, c] -# @timeit +@timeit @jit( # TODO: for now need to construct this manually for readonly arrays, see # https://github.com/numba/numba/issues/4511 @@ -450,7 +486,7 @@ def path_arrays_from_ohlc( @timeit def gen_qpath( data, - start, + start, # XXX: do we need this? w, ) -> QtGui.QPainterPath: @@ -478,13 +514,16 @@ class BarItems(pg.GraphicsObject): super().__init__() self.last_bar = QtGui.QPicture() - self.history = QtGui.QPicture() + # self.history = QtGui.QPicture() self.path = QtGui.QPainterPath() - self._h_path = QtGui.QGraphicsPathItem(self.path) + # self._h_path = QtGui.QGraphicsPathItem(self.path) self._pi = plotitem + self._xrange: Tuple[int, int] + self._yrange: Tuple[float, float] + # XXX: not sure this actually needs to be an array other # then for the old tina mode calcs for up/down bars below? # lines container @@ -495,14 +534,15 @@ class BarItems(pg.GraphicsObject): self._last_bar_lines: Optional[Tuple[QLineF, ...]] = None # track the current length of drawable lines within the larger array - self.index: int = 0 + self.start_index: int = 0 + self.stop_index: int = 0 - # @timeit + @timeit def draw_from_data( self, data: np.ndarray, start: int = 0, - ): + ) -> QtGui.QPainterPath: """Draw OHLC datum graphics from a ``np.ndarray``. This routine is usually only called to draw the initial history. @@ -511,19 +551,37 @@ class BarItems(pg.GraphicsObject): # save graphics for later reference and keep track # of current internal "last index" - self.index = len(data) + # self.start_index = len(data) + index = data['index'] + self._xrange = (index[0], index[-1]) + self._yrange = ( + np.nanmax(data['high']), + np.nanmin(data['low']), + ) # up to last to avoid double draw of last bar self._last_bar_lines = lines_from_ohlc(data[-1], self.w) # create pics - self.draw_history() + # self.draw_history() self.draw_last_bar() # trigger render # https://doc.qt.io/qt-5/qgraphicsitem.html#update self.update() + return self.path + + # def update_ranges( + # self, + # xmn: int, + # xmx: int, + # ymn: float, + # ymx: float, + # ) -> None: + # ... + + def draw_last_bar(self) -> None: """Currently this draws lines to a cached ``QPicture`` which is supposed to speed things up on ``.paint()`` calls (which @@ -535,17 +593,17 @@ class BarItems(pg.GraphicsObject): p.drawLines(*tuple(filter(bool, self._last_bar_lines))) p.end() - @timeit - def draw_history(self) -> None: - # TODO: avoid having to use a ```QPicture` to calc the - # ``.boundingRect()``, use ``QGraphicsPathItem`` instead? - # https://doc.qt.io/qt-5/qgraphicspathitem.html - # self._h_path.setPath(self.path) + # @timeit + # def draw_history(self) -> None: + # # TODO: avoid having to use a ```QPicture` to calc the + # # ``.boundingRect()``, use ``QGraphicsPathItem`` instead? + # # https://doc.qt.io/qt-5/qgraphicspathitem.html + # # self._h_path.setPath(self.path) - p = QtGui.QPainter(self.history) - p.setPen(self.bars_pen) - p.drawPath(self.path) - p.end() + # p = QtGui.QPainter(self.history) + # p.setPen(self.bars_pen) + # p.drawPath(self.path) + # p.end() @timeit def update_from_array( @@ -564,14 +622,42 @@ class BarItems(pg.GraphicsObject): This routine should be made (transitively) as fast as possible. """ - index = self.index - length = len(array) - extra = length - index + # index = self.start_index + istart, istop = self._xrange + + index = array['index'] + first_index, last_index = index[0], index[-1] + + # length = len(array) + prepend_length = istart - first_index + append_length = last_index - istop # TODO: allow mapping only a range of lines thus # only drawing as many bars as exactly specified. - if extra > 0: + if prepend_length: + # breakpoint() + # new history was added and we need to render a new path + new_bars = array[:prepend_length] + prepend_path = gen_qpath(new_bars, 0, self.w) + + # XXX: SOMETHING IS FISHY HERE what with the old_path + # y value not matching the first value from + # array[prepend_length + 1] ??? + + # update path + old_path = self.path + self.path = prepend_path + # self.path.moveTo(float(index - self.w), float(new_bars[0]['open'])) + # self.path.moveTo( + # float(istart - self.w), + # # float(array[prepend_length + 1]['open']) + # float(array[prepend_length]['open']) + # ) + self.path.addPath(old_path) + # self.draw_history() + + if append_length: # generate new lines objects for updatable "current bar" self._last_bar_lines = lines_from_ohlc(array[-1], self.w) self.draw_last_bar() @@ -580,29 +666,61 @@ class BarItems(pg.GraphicsObject): # path appending logic: # we need to get the previous "current bar(s)" for the time step # and convert it to a sub-path to append to the historical set - new_history_istart = length - 2 - to_history = array[new_history_istart:new_history_istart + extra] - new_history_qpath = gen_qpath(to_history, 0, self.w) + # new_bars = array[istop - 1:istop + append_length - 1] + new_bars = array[-append_length - 1:-1] + append_path = gen_qpath(new_bars, 0, self.w) + self.path.moveTo(float(istop - self.w), float(new_bars[0]['open'])) + self.path.addPath(append_path) - # move to position of placement for the next bar in history - # and append new sub-path - new_bars = array[index:index + extra] - self.path.moveTo(float(index - self.w), float(new_bars[0]['open'])) - self.path.addPath(new_history_qpath) + # self.draw_history() - self.index += extra + self._xrange = first_index, last_index - self.draw_history() + # if extra > 0: + # index = array['index'] + # first, last = index[0], indext[-1] - if just_history: - self.update() - return + # # if first < self.start_index: + # # length = self.start_index - first + # # prepend_path = gen_qpath(array[:sef: + + # # generate new lines objects for updatable "current bar" + # self._last_bar_lines = lines_from_ohlc(array[-1], self.w) + # self.draw_last_bar() + + # # generate new graphics to match provided array + # # path appending logic: + # # we need to get the previous "current bar(s)" for the time step + # # and convert it to a sub-path to append to the historical set + # new_history_istart = length - 2 + + # to_history = array[new_history_istart:new_history_istart + extra] + + # new_history_qpath = gen_qpath(to_history, 0, self.w) + + # # move to position of placement for the next bar in history + # # and append new sub-path + # new_bars = array[index:index + extra] + + # # x, y coordinates for start of next open/left arm + # self.path.moveTo(float(index - self.w), float(new_bars[0]['open'])) + + # self.path.addPath(new_history_qpath) + + # self.start_index += extra + + # self.draw_history() + + if just_history: + self.update() + return # last bar update i, o, h, l, last, v = array[-1][ ['index', 'open', 'high', 'low', 'close', 'volume'] ] - assert i == self.index - 1 + # assert i == self.start_index - 1 + assert i == last_index body, larm, rarm = self._last_bar_lines # XXX: is there a faster way to modify this? @@ -660,12 +778,14 @@ class BarItems(pg.GraphicsObject): # @timeit def boundingRect(self): - # TODO: can we do rect caching to make this faster + # Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect + + # TODO: Can we do rect caching to make this faster # like `pg.PlotCurveItem` does? In theory it's just # computing max/min stuff again like we do in the udpate loop - # anyway. + # anyway. Not really sure it's necessary since profiling already + # shows this method is faf. - # Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect # boundingRect _must_ indicate the entire area that will be # drawn on or else we will get artifacts and possibly crashing. # (in this case, QPicture does all the work of computing the @@ -673,15 +793,15 @@ class BarItems(pg.GraphicsObject): # compute aggregate bounding rectangle lb = self.last_bar.boundingRect() - hb = self.history.boundingRect() + hb = self.path.boundingRect() # hb = self._h_path.boundingRect() return QtCore.QRectF( # top left QtCore.QPointF(hb.topLeft()), # total size - # QtCore.QSizeF(QtCore.QSizeF(lb.size()) + hb.size()) - QtCore.QSizeF(lb.size() + hb.size()) + QtCore.QSizeF(QtCore.QSizeF(lb.size()) + hb.size()) + # QtCore.QSizeF(lb.size() + hb.size()) ) @@ -834,7 +954,7 @@ class L1Labels: chart: 'ChartPlotWidget', # noqa digits: int = 2, size_digits: int = 0, - font_size_inches: float = 4 / 53., + font_size_inches: float = _down_2_font_inches_we_like, ) -> None: self.chart = chart @@ -888,7 +1008,9 @@ def level_line( digits: int = 1, # size 4 font on 4k screen scaled down, so small-ish. - font_size_inches: float = 4 / 53., + font_size_inches: float = _down_2_font_inches_we_like, + + show_label: bool = True, **linelabelkwargs ) -> LevelLine: @@ -908,6 +1030,7 @@ def level_line( **linelabelkwargs ) label.update_from_data(0, level) + # TODO: can we somehow figure out a max value from the parent axis? label._size_br_from_str(label.label_str) @@ -923,4 +1046,7 @@ def level_line( chart.plotItem.addItem(line) + if not show_label: + label.hide() + return line