Get `QPainterPath` "append" working
Pertains further to #109. Instead of redrawing the entire `QPainterPath` every time there's a historical bars update just use `.addPath()` to slap in latest history. It seems to work and is fast. This also seems like it will be a great strategy for filling in earlier data, woot!to_qpainterpath_and_beyond
parent
413c703e34
commit
f083f537b1
|
@ -18,12 +18,11 @@
|
||||||
Chart graphics for displaying a slew of different data types.
|
Chart graphics for displaying a slew of different data types.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import time
|
|
||||||
from typing import List, Optional, Tuple
|
from typing import List, Optional, Tuple
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
from numba import jit, float64, optional, int64
|
from numba import jit, float64, int64
|
||||||
from PyQt5 import QtCore, QtGui
|
from PyQt5 import QtCore, QtGui
|
||||||
from PyQt5.QtCore import QLineF, QPointF
|
from PyQt5.QtCore import QLineF, QPointF
|
||||||
|
|
||||||
|
@ -335,15 +334,6 @@ class CrossHair(pg.GraphicsObject):
|
||||||
return self.plots[0].boundingRect()
|
return self.plots[0].boundingRect()
|
||||||
|
|
||||||
|
|
||||||
# @jit(
|
|
||||||
# # float64[:](
|
|
||||||
# # float64[:],
|
|
||||||
# # optional(float64),
|
|
||||||
# # optional(int16)
|
|
||||||
# # ),
|
|
||||||
# nopython=True,
|
|
||||||
# nogil=True
|
|
||||||
# )
|
|
||||||
def _mk_lines_array(
|
def _mk_lines_array(
|
||||||
data: List,
|
data: List,
|
||||||
size: int,
|
size: int,
|
||||||
|
@ -382,77 +372,6 @@ def lines_from_ohlc(row: np.ndarray, w: float) -> Tuple[QLineF]:
|
||||||
|
|
||||||
return [hl, o, c]
|
return [hl, o, c]
|
||||||
|
|
||||||
# TODO: `numba` this?
|
|
||||||
# @jit(
|
|
||||||
# # float64[:](
|
|
||||||
# # float64[:],
|
|
||||||
# # optional(float64),
|
|
||||||
# # optional(int16)
|
|
||||||
# # ),
|
|
||||||
# nopython=True,
|
|
||||||
# nogil=True
|
|
||||||
# )
|
|
||||||
def bars_from_ohlc(
|
|
||||||
data: np.ndarray,
|
|
||||||
w: float,
|
|
||||||
start: int = 0,
|
|
||||||
) -> np.ndarray:
|
|
||||||
"""Generate an array of lines objects from input ohlc data.
|
|
||||||
|
|
||||||
"""
|
|
||||||
lines = _mk_lines_array(data, data.shape[0], 3)
|
|
||||||
|
|
||||||
for i, q in enumerate(data[start:], start=start):
|
|
||||||
open, high, low, close, index = q[
|
|
||||||
['open', 'high', 'low', 'close', 'index']]
|
|
||||||
|
|
||||||
# high -> low vertical (body) line
|
|
||||||
if low != high:
|
|
||||||
hl = QLineF(index, low, index, high)
|
|
||||||
else:
|
|
||||||
# XXX: if we don't do it renders a weird rectangle?
|
|
||||||
# see below for filtering this later...
|
|
||||||
hl = None
|
|
||||||
|
|
||||||
# NOTE: place the x-coord start as "middle" of the drawing range such
|
|
||||||
# that the open arm line-graphic is at the left-most-side of
|
|
||||||
# the index's range according to the view mapping.
|
|
||||||
|
|
||||||
# open line
|
|
||||||
o = QLineF(index - w, open, index, open)
|
|
||||||
# close line
|
|
||||||
c = QLineF(index, close, index + w, close)
|
|
||||||
|
|
||||||
# indexing here is as per the below comments
|
|
||||||
lines[i] = (hl, o, c)
|
|
||||||
|
|
||||||
# XXX: in theory we could get a further speedup by using a flat
|
|
||||||
# array and avoiding the call to `np.ravel()` below?
|
|
||||||
# lines[3*i:3*i+3] = (hl, o, c)
|
|
||||||
|
|
||||||
# XXX: legacy code from candles custom graphics:
|
|
||||||
# if not _tina_mode:
|
|
||||||
# else _tina_mode:
|
|
||||||
# self.lines = lines = np.concatenate(
|
|
||||||
# [high_to_low, open_sticks, close_sticks])
|
|
||||||
# use traditional up/down green/red coloring
|
|
||||||
# long_bars = np.resize(Quotes.close > Quotes.open, len(lines))
|
|
||||||
# short_bars = np.resize(
|
|
||||||
# Quotes.close < Quotes.open, len(lines))
|
|
||||||
|
|
||||||
# ups = lines[long_bars]
|
|
||||||
# downs = lines[short_bars]
|
|
||||||
|
|
||||||
# # draw "up" bars
|
|
||||||
# p.setPen(self.bull_brush)
|
|
||||||
# p.drawLines(*ups)
|
|
||||||
|
|
||||||
# # draw "down" bars
|
|
||||||
# p.setPen(self.bear_brush)
|
|
||||||
# p.drawLines(*downs)
|
|
||||||
|
|
||||||
return lines
|
|
||||||
|
|
||||||
|
|
||||||
# @timeit
|
# @timeit
|
||||||
@jit(
|
@jit(
|
||||||
|
@ -490,9 +409,8 @@ def path_arrays_from_ohlc(
|
||||||
dtype=float64,
|
dtype=float64,
|
||||||
)
|
)
|
||||||
|
|
||||||
# TODO: report bug for assert
|
# TODO: report bug for assert @
|
||||||
# @ /home/goodboy/repos/piker/env/lib/python3.8/site-packages/numba/core/typing/builtins.py:991
|
# /home/goodboy/repos/piker/env/lib/python3.8/site-packages/numba/core/typing/builtins.py:991
|
||||||
# for i, q in enumerate(data[start:], start):
|
|
||||||
for i, q in enumerate(data[start:], start):
|
for i, q in enumerate(data[start:], start):
|
||||||
|
|
||||||
# TODO: ask numba why this doesn't work..
|
# TODO: ask numba why this doesn't work..
|
||||||
|
@ -541,7 +459,6 @@ def gen_qpath(
|
||||||
return pg.functions.arrayToQPath(x, y, connect=c)
|
return pg.functions.arrayToQPath(x, y, connect=c)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class BarItems(pg.GraphicsObject):
|
class BarItems(pg.GraphicsObject):
|
||||||
"""Price range bars graphics rendered from a OHLC sequence.
|
"""Price range bars graphics rendered from a OHLC sequence.
|
||||||
"""
|
"""
|
||||||
|
@ -554,10 +471,6 @@ class BarItems(pg.GraphicsObject):
|
||||||
# so probably don't do it until we figure that out.
|
# so probably don't do it until we figure that out.
|
||||||
bars_pen = pg.mkPen(hcolor('bracket'))
|
bars_pen = pg.mkPen(hcolor('bracket'))
|
||||||
|
|
||||||
# XXX: tina mode, see below
|
|
||||||
# bull_brush = pg.mkPen('#00cc00')
|
|
||||||
# bear_brush = pg.mkPen('#fa0000')
|
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
# scene: 'QGraphicsScene', # noqa
|
# scene: 'QGraphicsScene', # noqa
|
||||||
|
@ -567,22 +480,18 @@ class BarItems(pg.GraphicsObject):
|
||||||
|
|
||||||
self.last_bar = QtGui.QPicture()
|
self.last_bar = QtGui.QPicture()
|
||||||
self.history = QtGui.QPicture()
|
self.history = QtGui.QPicture()
|
||||||
|
|
||||||
# TODO: implement updateable pixmap solution
|
# TODO: implement updateable pixmap solution
|
||||||
self._pi = plotitem
|
self._pi = plotitem
|
||||||
# self._scene = plotitem.vb.scene()
|
|
||||||
# self.picture = QtGui.QPixmap(1000, 300)
|
|
||||||
# plotitem.addItem(self.picture)
|
|
||||||
# self._pmi = None
|
|
||||||
# self._pmi = self._scene.addPixmap(self.picture)
|
|
||||||
|
|
||||||
# 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
|
||||||
day_in_s = 60 * 60 * 12
|
|
||||||
self.lines = _mk_lines_array([], 50e3, 6)
|
self.lines = _mk_lines_array([], 50e3, 6)
|
||||||
|
|
||||||
# TODO: don't render the full backing array each time
|
# TODO: don't render the full backing array each time
|
||||||
# self._path_data = None
|
# self._path_data = None
|
||||||
self._last_bar_lines = 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.index: int = 0
|
||||||
|
@ -597,32 +506,13 @@ 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.
|
||||||
"""
|
"""
|
||||||
# start_lines = time.time()
|
|
||||||
|
|
||||||
# lines = bars_from_ohlc(data, self.w, start=start)
|
|
||||||
|
|
||||||
# start_path = time.time()
|
|
||||||
# assert len(data) == 2000
|
|
||||||
|
|
||||||
self.path = gen_qpath(data, self.w, start=start)
|
self.path = gen_qpath(data, self.w, start=start)
|
||||||
|
|
||||||
# end = time.time()
|
|
||||||
# print(f"paths took {end - start_path}\n lines took {start_path - start_lines}")
|
|
||||||
|
|
||||||
# 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"
|
||||||
# index = len(lines)
|
|
||||||
# index = len(data)
|
|
||||||
# self.lines[:index] = lines
|
|
||||||
# lines = bars_from_ohlc(data[-1:], self.w, start=start)
|
|
||||||
|
|
||||||
self.index = len(data)
|
self.index = len(data)
|
||||||
|
|
||||||
# up to last to avoid double draw of last bar
|
# up to last to avoid double draw of last bar
|
||||||
# self.draw_lines(just_history=True, iend=self.index - 1, path=self.path)
|
|
||||||
|
|
||||||
# self.draw_lines(iend=self.index)
|
|
||||||
|
|
||||||
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
|
||||||
|
@ -634,99 +524,23 @@ class BarItems(pg.GraphicsObject):
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
def draw_last_bar(self) -> 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
|
||||||
|
is a call to ``QPainter.drawPicture()`` but I'm not so sure.
|
||||||
|
|
||||||
# pic = self.last_bar
|
"""
|
||||||
|
|
||||||
# pre-computing a QPicture object allows paint() to run much
|
|
||||||
# more quickly, rather than re-drawing the shapes every time.
|
|
||||||
p = QtGui.QPainter(self.last_bar)
|
p = QtGui.QPainter(self.last_bar)
|
||||||
p.setPen(self.bars_pen)
|
p.setPen(self.bars_pen)
|
||||||
|
|
||||||
# print(self._last_bar_lines)
|
|
||||||
p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
|
p.drawLines(*tuple(filter(bool, self._last_bar_lines)))
|
||||||
p.end()
|
p.end()
|
||||||
|
|
||||||
# trigger re-render
|
|
||||||
# https://doc.qt.io/qt-5/qgraphicsitem.html#update
|
|
||||||
# self.update()
|
|
||||||
|
|
||||||
def draw_history(self) -> None:
|
def draw_history(self) -> None:
|
||||||
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()
|
||||||
|
|
||||||
# self.update()
|
|
||||||
|
|
||||||
@timeit
|
@timeit
|
||||||
def draw_lines(
|
|
||||||
self,
|
|
||||||
iend=None,
|
|
||||||
just_history=False,
|
|
||||||
istart=0,
|
|
||||||
path: QtGui.QPainterPath = None,
|
|
||||||
|
|
||||||
# TODO: could get even fancier and only update the single close line?
|
|
||||||
lines=None,
|
|
||||||
) -> None:
|
|
||||||
"""Draw the current line set using the painter.
|
|
||||||
|
|
||||||
Currently this draws lines to a cached ``QPicture`` which
|
|
||||||
is supposed to speed things up on ``.paint()`` calls (which
|
|
||||||
is a call to ``QPainter.drawPicture()`` but I'm not so sure.
|
|
||||||
"""
|
|
||||||
# if path is None:
|
|
||||||
# if just_history:
|
|
||||||
# raise RuntimeError
|
|
||||||
# # draw bars for the "history" picture
|
|
||||||
# iend = iend or self.index - 1
|
|
||||||
# pic = self.history
|
|
||||||
# else:
|
|
||||||
# # draw the last bar
|
|
||||||
# istart = self.index - 1
|
|
||||||
# iend = iend or self.index
|
|
||||||
|
|
||||||
# pic = self.last_bar
|
|
||||||
|
|
||||||
# # use 2d array of lines objects, see conlusion on speed:
|
|
||||||
# # https://stackoverflow.com/a/60089929
|
|
||||||
# flat = np.ravel(self.lines[istart:iend])
|
|
||||||
|
|
||||||
# # TODO: do this with numba for speed gain:
|
|
||||||
# # https://stackoverflow.com/questions/58422690/filtering-a-numpy-array-what-is-the-best-approach
|
|
||||||
# to_draw = flat[np.where(flat != None)] # noqa
|
|
||||||
|
|
||||||
# else:
|
|
||||||
# pic = self.history
|
|
||||||
|
|
||||||
pic = self.last_bar
|
|
||||||
|
|
||||||
# pre-computing a QPicture object allows paint() to run much
|
|
||||||
# more quickly, rather than re-drawing the shapes every time.
|
|
||||||
p = QtGui.QPainter(pic)
|
|
||||||
p.setPen(self.bars_pen)
|
|
||||||
|
|
||||||
p.drawLines(*self._last_bar_lines)
|
|
||||||
p.end()
|
|
||||||
|
|
||||||
# trigger re-render
|
|
||||||
# https://doc.qt.io/qt-5/qgraphicsitem.html#update
|
|
||||||
self.update()
|
|
||||||
|
|
||||||
# TODO: is there any way to not have to pass all the lines every
|
|
||||||
# iteration? It seems they won't draw unless it's done this way..
|
|
||||||
# if path is None:
|
|
||||||
# # p.drawLines(*to_draw)
|
|
||||||
# p.drawLines(*self._last_bars_lines)
|
|
||||||
# else:
|
|
||||||
# p.drawPath(path)
|
|
||||||
|
|
||||||
# p.end()
|
|
||||||
|
|
||||||
# trigger re-render
|
|
||||||
# https://doc.qt.io/qt-5/qgraphicsitem.html#update
|
|
||||||
# self.update()
|
|
||||||
|
|
||||||
def update_from_array(
|
def update_from_array(
|
||||||
self,
|
self,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
|
@ -740,57 +554,49 @@ class BarItems(pg.GraphicsObject):
|
||||||
graphics object, and then update/rerender, but here we're
|
graphics object, and then update/rerender, but here we're
|
||||||
assuming the prior graphics havent changed (OHLC history rarely
|
assuming the prior graphics havent changed (OHLC history rarely
|
||||||
does) so this "should" be simpler and faster.
|
does) so this "should" be simpler and faster.
|
||||||
|
|
||||||
|
This routine should be made (transitively) as fast as possible.
|
||||||
"""
|
"""
|
||||||
index = self.index
|
index = self.index
|
||||||
length = len(array)
|
length = len(array)
|
||||||
extra = length - index
|
extra = length - index
|
||||||
|
|
||||||
# start_bar_to_update = index - 100
|
|
||||||
|
|
||||||
# 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 extra > 0:
|
||||||
# generate new graphics to match provided array
|
|
||||||
|
|
||||||
# lines = bars_from_ohlc(new, self.w)
|
# generate new lines objects for updatable "current bar"
|
||||||
# lines = bars_from_ohlc(array[-1:], self.w)
|
|
||||||
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()
|
||||||
|
|
||||||
# TODO: only draw these new bars to the backing binary
|
# generate new graphics to match provided array
|
||||||
# path array and then call arrayToQpath() on the whole
|
# path appending logic:
|
||||||
# -> will avoid multiple passes for path data we've already
|
# we need to get the previous "current bar(s)" for the time step
|
||||||
# already generated
|
# and convert it to a path to append to the historical set
|
||||||
new = array[index:index + extra]
|
new_history_istart = length - 2
|
||||||
|
to_history = array[new_history_istart:new_history_istart + extra]
|
||||||
|
# generate a new sub-path for this now-ready-for-history bar set
|
||||||
|
new_history_qpath = gen_qpath(to_history, self.w, 0)
|
||||||
|
|
||||||
self.path = gen_qpath(array[:-1], self.w, start=0)
|
# move to position of placement for the next bar in history
|
||||||
|
# and append new sub-path
|
||||||
# self.path.connectPath(path)
|
new_bars = array[index:index + extra]
|
||||||
|
self.path.moveTo(float(index - self.w), float(new_bars[0]['open']))
|
||||||
# bars_added = len(new)
|
self.path.addPath(new_history_qpath)
|
||||||
# bars_added = extra
|
|
||||||
# self.lines[index:index + bars_added] = lines
|
|
||||||
|
|
||||||
self.index += extra
|
self.index += extra
|
||||||
|
|
||||||
# start_bar_to_update = index - bars_added
|
|
||||||
# self.draw_lines(just_history=True, path=self.path)
|
|
||||||
# self.update()
|
|
||||||
|
|
||||||
self.draw_history()
|
self.draw_history()
|
||||||
|
|
||||||
if just_history:
|
if just_history:
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# last bar update
|
# last bar update
|
||||||
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.index - 1
|
||||||
|
|
||||||
# body, larm, rarm = self.lines[i]
|
|
||||||
# body, larm, rarm = self._bars
|
|
||||||
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?
|
||||||
|
@ -800,7 +606,6 @@ class BarItems(pg.GraphicsObject):
|
||||||
|
|
||||||
if l != h: # noqa
|
if l != h: # noqa
|
||||||
if body is None:
|
if body is None:
|
||||||
# body = self.lines[index - 1][0] = QLineF(i, l, i, h)
|
|
||||||
body = self._last_bar_lines[0] = QLineF(i, l, i, h)
|
body = self._last_bar_lines[0] = QLineF(i, l, i, h)
|
||||||
else:
|
else:
|
||||||
# update body
|
# update body
|
||||||
|
@ -819,7 +624,6 @@ class BarItems(pg.GraphicsObject):
|
||||||
# if body is not None:
|
# if body is not None:
|
||||||
# body = self.lines[index - 1][0] = None
|
# body = self.lines[index - 1][0] = None
|
||||||
|
|
||||||
# self.draw_lines(just_history=False)
|
|
||||||
self.draw_last_bar()
|
self.draw_last_bar()
|
||||||
self.update()
|
self.update()
|
||||||
|
|
||||||
|
@ -842,15 +646,9 @@ class BarItems(pg.GraphicsObject):
|
||||||
# p.drawPicture(0, 0, self.history)
|
# p.drawPicture(0, 0, self.history)
|
||||||
p.drawPicture(0, 0, self.last_bar)
|
p.drawPicture(0, 0, self.last_bar)
|
||||||
|
|
||||||
# p = QtGui.QPainter(pic)
|
|
||||||
p.setPen(self.bars_pen)
|
p.setPen(self.bars_pen)
|
||||||
# p.drawLines(*self._last_bar_lines)
|
|
||||||
|
|
||||||
# TODO: is there any way to not have to pass all the lines every
|
|
||||||
# iteration? It seems they won't draw unless it's done this way..
|
|
||||||
p.drawPath(self.path)
|
p.drawPath(self.path)
|
||||||
|
|
||||||
|
|
||||||
# TODO: if we can ever make pixmaps work...
|
# TODO: if we can ever make pixmaps work...
|
||||||
# p.drawPixmap(0, 0, self.picture)
|
# p.drawPixmap(0, 0, self.picture)
|
||||||
# self._pmi.setPixmap(self.picture)
|
# self._pmi.setPixmap(self.picture)
|
||||||
|
@ -860,7 +658,10 @@ class BarItems(pg.GraphicsObject):
|
||||||
|
|
||||||
# @timeit
|
# @timeit
|
||||||
def boundingRect(self):
|
def boundingRect(self):
|
||||||
# TODO: can we do rect caching to make this faster?
|
# 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.
|
||||||
|
|
||||||
# Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect
|
# 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
|
||||||
|
|
Loading…
Reference in New Issue