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.
to_qpainterpath_and_beyond
Tyler Goodlet 2020-12-10 14:30:40 -05:00
parent 02b7d6cd19
commit 2568a2d2e9
1 changed files with 182 additions and 56 deletions

View File

@ -33,14 +33,15 @@ from ._style import (
_xaxis_at, _xaxis_at,
hcolor, hcolor,
_font, _font,
_down_2_font_inches_we_like,
) )
from ._axes import YAxisLabel, XAxisLabel, YSticky from ._axes import YAxisLabel, XAxisLabel, YSticky
# XXX: these settings seem to result in really decent mouse scroll # XXX: these settings seem to result in really decent mouse scroll
# latency (in terms of perceived lag in cross hair) so really be sure # latency (in terms of perceived lag in cross hair) so really be sure
# there's an improvement if you want to change it. # there's an improvement if you want to change it!
_mouse_rate_limit = 60 # calc current screen refresh rate? _mouse_rate_limit = 60 # TODO; should we calc current screen refresh rate?
_debounce_delay = 1 / 2e3 _debounce_delay = 1 / 2e3
_ch_label_opac = 1 _ch_label_opac = 1
@ -53,6 +54,7 @@ class LineDot(pg.CurvePoint):
self, self,
curve: pg.PlotCurveItem, curve: pg.PlotCurveItem,
index: int, index: int,
plot: 'ChartPlotWidget',
pos=None, pos=None,
size: int = 2, # in pxs size: int = 2, # in pxs
color: str = 'default_light', color: str = 'default_light',
@ -64,6 +66,7 @@ class LineDot(pg.CurvePoint):
pos=pos, pos=pos,
rotate=False, rotate=False,
) )
self._plot = plot
# TODO: get pen from curve if not defined? # TODO: get pen from curve if not defined?
cdefault = hcolor(color) cdefault = hcolor(color)
@ -83,6 +86,31 @@ class LineDot(pg.CurvePoint):
# keep a static size # keep a static size
self.setFlag(self.ItemIgnoresTransformations) 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 = { _corner_anchors = {
'top': 0, 'top': 0,
@ -94,8 +122,10 @@ _corner_anchors = {
_corner_margins = { _corner_margins = {
('top', 'left'): (-4, -5), ('top', 'left'): (-4, -5),
('top', 'right'): (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, array: np.ndarray,
) -> None: ) -> None:
# this being "html" is the dumbest shit :eyeroll: # this being "html" is the dumbest shit :eyeroll:
first = array[0]['index']
self.setText( self.setText(
"<b>i</b>:{index}<br/>" "<b>i</b>:{index}<br/>"
"<b>O</b>:{}<br/>" "<b>O</b>:{}<br/>"
"<b>H</b>:{}<br/>" "<b>H</b>:{}<br/>"
"<b>L</b>:{}<br/>" "<b>L</b>:{}<br/>"
"<b>C</b>:{}<br/>" "<b>C</b>:{}<br/>"
"<b>V</b>:{}".format( "<b>V</b>:{}<br/>"
# *self._array[index].item()[2:8], "<b>wap</b>:{}".format(
*array[index].item()[2:8], *array[index - first][
['open', 'high', 'low', 'close', 'volume', 'bar_wap']
],
name=name, name=name,
index=index, index=index,
) )
@ -152,8 +186,9 @@ class ContentsLabel(pg.LabelItem):
index: int, index: int,
array: np.ndarray, array: np.ndarray,
) -> None: ) -> None:
if index < len(array): first = array[0]['index']
data = array[index][name] if index < array[-1]['index'] and index > first:
data = array[index - first][name]
self.setText(f"{name}: {data:.2f}") self.setText(f"{name}: {data:.2f}")
@ -250,7 +285,7 @@ class CrossHair(pg.GraphicsObject):
) -> LineDot: ) -> LineDot:
# if this plot contains curves add line dot "cursors" to denote # if this plot contains curves add line dot "cursors" to denote
# the current sample under the mouse # 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) plot.addItem(cursor)
self.graphics[plot].setdefault('cursors', []).append(cursor) self.graphics[plot].setdefault('cursors', []).append(cursor)
return cursor return cursor
@ -312,8 +347,9 @@ class CrossHair(pg.GraphicsObject):
plot.update_contents_labels(ix) plot.update_contents_labels(ix)
# update all subscribed curve dots # update all subscribed curve dots
# first = plot._ohlc[0]['index']
for cursor in opts.get('cursors', ()): for cursor in opts.get('cursors', ()):
cursor.setIndex(ix) cursor.setIndex(ix) # - first)
# update the label on the bottom of the crosshair # update the label on the bottom of the crosshair
self.xaxis_label.update_label( self.xaxis_label.update_label(
@ -375,7 +411,7 @@ def lines_from_ohlc(row: np.ndarray, w: float) -> Tuple[QLineF]:
return [hl, o, c] return [hl, o, c]
# @timeit @timeit
@jit( @jit(
# TODO: for now need to construct this manually for readonly arrays, see # TODO: for now need to construct this manually for readonly arrays, see
# https://github.com/numba/numba/issues/4511 # https://github.com/numba/numba/issues/4511
@ -450,7 +486,7 @@ def path_arrays_from_ohlc(
@timeit @timeit
def gen_qpath( def gen_qpath(
data, data,
start, start, # XXX: do we need this?
w, w,
) -> QtGui.QPainterPath: ) -> QtGui.QPainterPath:
@ -478,13 +514,16 @@ class BarItems(pg.GraphicsObject):
super().__init__() super().__init__()
self.last_bar = QtGui.QPicture() self.last_bar = QtGui.QPicture()
self.history = QtGui.QPicture() # self.history = QtGui.QPicture()
self.path = QtGui.QPainterPath() self.path = QtGui.QPainterPath()
self._h_path = QtGui.QGraphicsPathItem(self.path) # self._h_path = QtGui.QGraphicsPathItem(self.path)
self._pi = plotitem self._pi = plotitem
self._xrange: Tuple[int, int]
self._yrange: Tuple[float, float]
# XXX: not sure this actually needs to be an array other # XXX: not sure this actually needs to be an array other
# then for the old tina mode calcs for up/down bars below? # then for the old tina mode calcs for up/down bars below?
# lines container # lines container
@ -495,14 +534,15 @@ class BarItems(pg.GraphicsObject):
self._last_bar_lines: Optional[Tuple[QLineF, ...]] = None self._last_bar_lines: Optional[Tuple[QLineF, ...]] = None
# track the current length of drawable lines within the larger array # 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( def draw_from_data(
self, self,
data: np.ndarray, data: np.ndarray,
start: int = 0, start: int = 0,
): ) -> QtGui.QPainterPath:
"""Draw OHLC datum graphics from a ``np.ndarray``. """Draw OHLC datum graphics from a ``np.ndarray``.
This routine is usually only called to draw the initial history. 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 # save graphics for later reference and keep track
# of current internal "last index" # 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 # up to last to avoid double draw of last bar
self._last_bar_lines = lines_from_ohlc(data[-1], self.w) self._last_bar_lines = lines_from_ohlc(data[-1], self.w)
# create pics # create pics
self.draw_history() # self.draw_history()
self.draw_last_bar() self.draw_last_bar()
# trigger render # trigger render
# https://doc.qt.io/qt-5/qgraphicsitem.html#update # https://doc.qt.io/qt-5/qgraphicsitem.html#update
self.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: def draw_last_bar(self) -> None:
"""Currently this draws lines to a cached ``QPicture`` which """Currently this draws lines to a cached ``QPicture`` which
is supposed to speed things up on ``.paint()`` calls (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.drawLines(*tuple(filter(bool, self._last_bar_lines)))
p.end() p.end()
@timeit # @timeit
def draw_history(self) -> None: # def draw_history(self) -> None:
# TODO: avoid having to use a ```QPicture` to calc the # # TODO: avoid having to use a ```QPicture` to calc the
# ``.boundingRect()``, use ``QGraphicsPathItem`` instead? # # ``.boundingRect()``, use ``QGraphicsPathItem`` instead?
# https://doc.qt.io/qt-5/qgraphicspathitem.html # # https://doc.qt.io/qt-5/qgraphicspathitem.html
# self._h_path.setPath(self.path) # # self._h_path.setPath(self.path)
p = QtGui.QPainter(self.history) # p = QtGui.QPainter(self.history)
p.setPen(self.bars_pen) # p.setPen(self.bars_pen)
p.drawPath(self.path) # p.drawPath(self.path)
p.end() # p.end()
@timeit @timeit
def update_from_array( def update_from_array(
@ -564,14 +622,42 @@ class BarItems(pg.GraphicsObject):
This routine should be made (transitively) as fast as possible. This routine should be made (transitively) as fast as possible.
""" """
index = self.index # index = self.start_index
length = len(array) istart, istop = self._xrange
extra = length - index
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 # TODO: allow mapping only a range of lines thus
# only drawing as many bars as exactly specified. # 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" # generate new lines objects for updatable "current bar"
self._last_bar_lines = lines_from_ohlc(array[-1], self.w) self._last_bar_lines = lines_from_ohlc(array[-1], self.w)
self.draw_last_bar() self.draw_last_bar()
@ -580,19 +666,50 @@ class BarItems(pg.GraphicsObject):
# path appending logic: # path appending logic:
# we need to get the previous "current bar(s)" for the time step # 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 # and convert it to a sub-path to append to the historical set
new_history_istart = length - 2 # new_bars = array[istop - 1:istop + append_length - 1]
to_history = array[new_history_istart:new_history_istart + extra] new_bars = array[-append_length - 1:-1]
new_history_qpath = gen_qpath(to_history, 0, self.w) 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 # self.draw_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.index += extra self._xrange = first_index, last_index
self.draw_history() # if extra > 0:
# index = array['index']
# first, last = index[0], indext[-1]
# # 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: if just_history:
self.update() self.update()
@ -602,7 +719,8 @@ class BarItems(pg.GraphicsObject):
i, o, h, l, last, v = array[-1][ i, o, h, l, last, v = array[-1][
['index', 'open', 'high', 'low', 'close', 'volume'] ['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 body, larm, rarm = self._last_bar_lines
# XXX: is there a faster way to modify this? # XXX: is there a faster way to modify this?
@ -660,12 +778,14 @@ class BarItems(pg.GraphicsObject):
# @timeit # @timeit
def boundingRect(self): 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 # like `pg.PlotCurveItem` does? In theory it's just
# computing max/min stuff again like we do in the udpate loop # 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 # boundingRect _must_ indicate the entire area that will be
# drawn on or else we will get artifacts and possibly crashing. # drawn on or else we will get artifacts and possibly crashing.
# (in this case, QPicture does all the work of computing the # (in this case, QPicture does all the work of computing the
@ -673,15 +793,15 @@ class BarItems(pg.GraphicsObject):
# compute aggregate bounding rectangle # compute aggregate bounding rectangle
lb = self.last_bar.boundingRect() lb = self.last_bar.boundingRect()
hb = self.history.boundingRect() hb = self.path.boundingRect()
# hb = self._h_path.boundingRect() # hb = self._h_path.boundingRect()
return QtCore.QRectF( return QtCore.QRectF(
# top left # top left
QtCore.QPointF(hb.topLeft()), QtCore.QPointF(hb.topLeft()),
# total size # total size
# QtCore.QSizeF(QtCore.QSizeF(lb.size()) + hb.size()) QtCore.QSizeF(QtCore.QSizeF(lb.size()) + hb.size())
QtCore.QSizeF(lb.size() + hb.size()) # QtCore.QSizeF(lb.size() + hb.size())
) )
@ -834,7 +954,7 @@ class L1Labels:
chart: 'ChartPlotWidget', # noqa chart: 'ChartPlotWidget', # noqa
digits: int = 2, digits: int = 2,
size_digits: int = 0, size_digits: int = 0,
font_size_inches: float = 4 / 53., font_size_inches: float = _down_2_font_inches_we_like,
) -> None: ) -> None:
self.chart = chart self.chart = chart
@ -888,7 +1008,9 @@ def level_line(
digits: int = 1, digits: int = 1,
# size 4 font on 4k screen scaled down, so small-ish. # 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 **linelabelkwargs
) -> LevelLine: ) -> LevelLine:
@ -908,6 +1030,7 @@ def level_line(
**linelabelkwargs **linelabelkwargs
) )
label.update_from_data(0, level) label.update_from_data(0, level)
# TODO: can we somehow figure out a max value from the parent axis? # TODO: can we somehow figure out a max value from the parent axis?
label._size_br_from_str(label.label_str) label._size_br_from_str(label.label_str)
@ -923,4 +1046,7 @@ def level_line(
chart.plotItem.addItem(line) chart.plotItem.addItem(line)
if not show_label:
label.hide()
return line return line