Add for a `BarItems` to display a line on high uppx

When a bars graphic is zoomed out enough you get a high uppx, datum
units-per-pixel, and there is no point in drawing the 6-lines in each
bar element-graphic if you can't see them on the screen/display device.

Instead here we offer converting to a `FastAppendCurve` which traces
the high-low outline and instead display that when it's impossible to see the
details of bars - approximately when the uppx >= 2.

There is also some draft-commented code in here for downsampling the
outlines as zoom level increases but it's not fully working and should
likely be factored out into a higher level api anyway.
marketstore_backup
Tyler Goodlet 2022-03-11 14:40:17 -05:00
parent 35fd39d769
commit 8c9ffb2c22
1 changed files with 159 additions and 50 deletions

View File

@ -34,6 +34,8 @@ from PyQt5.QtCore import QLineF, QPointF
from .._profile import pg_profile_enabled from .._profile import pg_profile_enabled
from ._style import hcolor from ._style import hcolor
from ..log import get_logger from ..log import get_logger
from ._curve import FastAppendCurve
from ._compression import hl2mxmn
if TYPE_CHECKING: if TYPE_CHECKING:
from ._chart import LinkedSplits from ._chart import LinkedSplits
@ -46,10 +48,12 @@ def _mk_lines_array(
data: list, data: list,
size: int, size: int,
elements_step: int = 6, elements_step: int = 6,
) -> np.ndarray:
"""Create an ndarray to hold lines graphics info.
""" ) -> np.ndarray:
'''
Create an ndarray to hold lines graphics info.
'''
return np.zeros_like( return np.zeros_like(
data, data,
shape=(int(size), elements_step), shape=(int(size), elements_step),
@ -107,10 +111,12 @@ def path_arrays_from_ohlc(
data: np.ndarray, data: np.ndarray,
start: int64, start: int64,
bar_gap: float64 = 0.43, bar_gap: float64 = 0.43,
) -> np.ndarray:
"""Generate an array of lines objects from input ohlc data.
""" ) -> np.ndarray:
'''
Generate an array of lines objects from input ohlc data.
'''
size = int(data.shape[0] * 6) size = int(data.shape[0] * 6)
x = np.zeros( x = np.zeros(
@ -220,13 +226,12 @@ class BarItems(pg.GraphicsObject):
# that mode? # that mode?
self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
# not sure if this is actually impoving anything but figured it
# was worth a shot:
# self.path.reserve(int(100e3 * 6))
self.path = QtGui.QPainterPath()
self._pi = plotitem self._pi = plotitem
self.path = QtGui.QPainterPath()
# not sure if this is actually impoving anything but figured it
# was worth a shot:
self.path.reserve(int(100e3 * 6))
self._xrange: tuple[int, int] self._xrange: tuple[int, int]
self._yrange: tuple[float, float] self._yrange: tuple[float, float]
@ -239,11 +244,15 @@ class BarItems(pg.GraphicsObject):
self.start_index: int = 0 self.start_index: int = 0
self.stop_index: int = 0 self.stop_index: int = 0
# downsampler-line state
self._in_ds: bool = False self._in_ds: bool = False
self._ds_lines: dict[int, FastAppendCurve] = {}
self._ds_line: Optional[FastAppendCurve] = None
self._ds: int = 0
def draw_from_data( def draw_from_data(
self, self,
data: np.ndarray, ohlc: np.ndarray,
start: int = 0, start: int = 0,
) -> QtGui.QPainterPath: ) -> QtGui.QPainterPath:
@ -253,18 +262,18 @@ class BarItems(pg.GraphicsObject):
This routine is usually only called to draw the initial history. This routine is usually only called to draw the initial history.
''' '''
hist, last = data[:-1], data[-1] hist, last = ohlc[:-1], ohlc[-1]
self.path = gen_qpath(hist, start, self.w) self.path = gen_qpath(hist, start, self.w)
# 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.start_index = len(data) # self.start_index = len(ohlc)
index = data['index'] index = ohlc['index']
self._xrange = (index[0], index[-1]) self._xrange = (index[0], index[-1])
self._yrange = ( self._yrange = (
np.nanmax(data['high']), np.nanmax(ohlc['high']),
np.nanmin(data['low']), np.nanmin(ohlc['low']),
) )
# up to last to avoid double draw of last bar # up to last to avoid double draw of last bar
@ -274,23 +283,86 @@ class BarItems(pg.GraphicsObject):
# https://doc.qt.io/qt-5/qgraphicsitem.html#update # https://doc.qt.io/qt-5/qgraphicsitem.html#update
self.update() self.update()
from ._curve import FastAppendCurve self.update_ds_line(ohlc)
self._ds_line = FastAppendCurve( assert self._ds_line
y=data['close'],
x=data['index'],
name='ohlc_ds_line',
color=self._color,
# use_polyline=True, # pretty sure this is slower?
)
self.update_from_array(data)
self._pi.addItem(self._ds_line)
self._ds_line.hide() self._ds_line.hide()
return self.path return self.path
def get_ds_line(
self,
ds: Optional[int] = None,
) -> tuple[FastAppendCurve, int]:
if ds is None:
px_vecs = self.pixelVectors()[0]
if px_vecs:
xs_in_px = px_vecs.x()
ds = round(xs_in_px)
else:
ds = 0
# print(f'ds is {ds}')
return self._ds_lines.get(ds), ds
def update_ds_line(
self,
ohlc: np.ndarray,
use_ds: bool = False,
) -> int:
if not use_ds:
ds = 0
else:
ds = None
# determine current potential downsampling value (based on pixel
# scaling) and return any existing curve for it.
curve, ds = self.get_ds_line(ds=ds)
# curve = self._ds_lines.get(ds)
# if current and current != curve:
# current.hide()
# if no curve for this downsample rate yet, allowcate a new one
if not curve:
mxmn, x = hl2mxmn(ohlc, downsample_by=ds)
curve = FastAppendCurve(
y=mxmn,
x=x,
name='ohlc_ds_line',
color=self._color,
# color='dad_blue',
# use_polyline=True, # pretty sure this is slower?
)
# self._pi.addItem(curve)
self._ds_lines[ds] = curve
self._ds_line = curve
# elif ds != self._ds:
# print(f'ds changed {self._ds} -> {ds}')
# TODO: we should be diffing the amount of new data which
# needs to be downsampled. Ideally we actually are just
# doing all the ds-ing in sibling actors so that the data
# can just be read and rendered to graphics on events of our
# choice.
# diff = do_diff(ohlc, new_bit)
mxmn, x = hl2mxmn(ohlc, downsample_by=ds)
curve.update_from_array(
y=mxmn,
x=x,
)
self._ds = ds
return curve, ds
def update_from_array( def update_from_array(
self, self,
array: np.ndarray, ohlc: np.ndarray,
just_history=False, just_history=False,
) -> None: ) -> None:
@ -309,19 +381,16 @@ class BarItems(pg.GraphicsObject):
''' '''
# XXX: always do this? # XXX: always do this?
if self._in_ds: if self._in_ds:
self._ds_line.update_from_array( curve, ds = self.update_ds_line(ohlc)
x=array['index'],
y=array['close'],
)
return return
# index = self.start_index # index = self.start_index
istart, istop = self._xrange istart, istop = self._xrange
index = array['index'] index = ohlc['index']
first_index, last_index = index[0], index[-1] first_index, last_index = index[0], index[-1]
# length = len(array) # length = len(ohlc)
prepend_length = istart - first_index prepend_length = istart - first_index
append_length = last_index - istop append_length = last_index - istop
@ -333,12 +402,12 @@ class BarItems(pg.GraphicsObject):
if prepend_length: if prepend_length:
# new history was added and we need to render a new path # new history was added and we need to render a new path
new_bars = array[:prepend_length] new_bars = ohlc[:prepend_length]
prepend_path = gen_qpath(new_bars, 0, self.w) prepend_path = gen_qpath(new_bars, 0, self.w)
# XXX: SOMETHING IS MAYBE FISHY HERE what with the old_path # XXX: SOMETHING IS MAYBE FISHY HERE what with the old_path
# y value not matching the first value from # y value not matching the first value from
# array[prepend_length + 1] ??? # ohlc[prepend_length + 1] ???
# update path # update path
old_path = self.path old_path = self.path
@ -350,14 +419,14 @@ class BarItems(pg.GraphicsObject):
if append_length: 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(ohlc[-1], self.w)
# generate new graphics to match provided array # generate new graphics to match provided array
# 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_bars = array[istop - 1:istop + append_length - 1] # new_bars = ohlc[istop - 1:istop + append_length - 1]
new_bars = array[-append_length - 1:-1] new_bars = ohlc[-append_length - 1:-1]
append_path = gen_qpath(new_bars, 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.moveTo(float(istop - self.w), float(new_bars[0]['open']))
self.path.addPath(append_path) self.path.addPath(append_path)
@ -370,7 +439,7 @@ class BarItems(pg.GraphicsObject):
self._xrange = first_index, last_index self._xrange = first_index, last_index
# last bar update # last bar update
i, o, h, l, last, v = array[-1][ i, o, h, l, last, v = ohlc[-1][
['index', 'open', 'high', 'low', 'close', 'volume'] ['index', 'open', 'high', 'low', 'close', 'volume']
] ]
# assert i == self.start_index - 1 # assert i == self.start_index - 1
@ -462,36 +531,76 @@ class BarItems(pg.GraphicsObject):
''' '''
# this is the ``float`` value of the "number of x units" (in # this is the ``float`` value of the "number of x units" (in
# view coords) that a pixel spans. # view coords) that a pixel spans.
xs_in_px = self.pixelVectors()[0].x() xvec = self.pixelVectors()[0]
if xvec:
xs_in_px = xvec.x()
else:
xs_in_px = self._ds_line.pixelVectors()[0].x()
linked = self.linked
if ( if (
not self._in_ds not self._in_ds
and xs_in_px >= x_gt and xs_in_px >= x_gt
): ):
linked = self.linked
# TODO: a `.ui()` log level? # TODO: a `.ui()` log level?
log.info(f'downsampling to line graphic {linked.symbol.key}') log.info(f'downsampling to line graphic {linked.symbol.key}')
self._in_ds = True
self.hide() self.hide()
self._pi.addItem(self._ds_line) # XXX: is this actually any faster?
self._ds_line.show() self._pi.removeItem(self)
self._ds_line.update()
curve, ds = self.get_ds_line(ds=0)
last_curve = self._ds_line
assert last_curve is curve
self._pi.addItem(curve)
curve.show()
curve.update()
linked.graphics_cycle() linked.graphics_cycle()
self._in_ds = True
return True return True
elif ( elif (
self._in_ds self._in_ds
and xs_in_px < x_gt and xs_in_px < x_gt
): ):
linked = self.linked
log.info(f'showing bars graphic {linked.symbol.key}') log.info(f'showing bars graphic {linked.symbol.key}')
self._in_ds = False
curve, ds = self.get_ds_line(ds=0)
last_curve = self._ds_line
assert last_curve is curve
curve.hide()
self._pi.removeItem(curve)
# XXX: is this actually any faster?
self._pi.addItem(self)
self.show() self.show()
self.update() self.update()
self._ds_line.hide()
self._pi.removeItem(self._ds_line) self._in_ds = False
linked.graphics_cycle() linked.graphics_cycle()
return True return True
# elif (
# self._in_ds
# and self._ds != ds
# ):
# # curve = self._ds_lines.get(ds)
# # assert self._ds_line is not curve
# if self._ds_line and self._ds_line is not curve:
# self._ds_line.hide()
# if curve:
# # self._pi.removeItem(curve)
# curve.show()
# curve.update()
# self._ds_line = curve
# self._ds = ds
# linked.graphics_cycle()
# return True
# no curve change # no curve change
return False return False