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
parent
02b7d6cd19
commit
2568a2d2e9
|
@ -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(
|
||||
"<b>i</b>:{index}<br/>"
|
||||
"<b>O</b>:{}<br/>"
|
||||
"<b>H</b>:{}<br/>"
|
||||
"<b>L</b>:{}<br/>"
|
||||
"<b>C</b>:{}<br/>"
|
||||
"<b>V</b>:{}".format(
|
||||
# *self._array[index].item()[2:8],
|
||||
*array[index].item()[2:8],
|
||||
"<b>V</b>:{}<br/>"
|
||||
"<b>wap</b>:{}".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
|
||||
|
|
Loading…
Reference in New Issue