2020-12-28 22:31:58 +00:00
|
|
|
# piker: trading gear for hackers
|
2022-03-09 19:48:00 +00:00
|
|
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
2020-12-28 22:31:58 +00:00
|
|
|
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
|
|
# (at your option) any later version.
|
|
|
|
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
|
|
|
|
"""
|
|
|
|
Fast, smooth, sexy curves.
|
2021-03-12 02:42:38 +00:00
|
|
|
|
2020-12-28 22:31:58 +00:00
|
|
|
"""
|
2021-09-20 17:38:12 +00:00
|
|
|
from typing import Optional
|
2020-12-28 22:31:58 +00:00
|
|
|
|
2021-09-17 20:01:28 +00:00
|
|
|
import numpy as np
|
2020-12-28 22:31:58 +00:00
|
|
|
import pyqtgraph as pg
|
2021-09-19 19:56:02 +00:00
|
|
|
from PyQt5 import QtGui, QtWidgets
|
2022-03-09 16:07:53 +00:00
|
|
|
from PyQt5.QtWidgets import QGraphicsItem
|
2021-09-19 19:56:02 +00:00
|
|
|
from PyQt5.QtCore import (
|
2022-02-02 19:02:21 +00:00
|
|
|
Qt,
|
2021-09-19 19:56:02 +00:00
|
|
|
QLineF,
|
|
|
|
QSizeF,
|
|
|
|
QRectF,
|
|
|
|
QPointF,
|
|
|
|
)
|
2020-12-28 22:31:58 +00:00
|
|
|
|
2022-04-01 17:46:37 +00:00
|
|
|
from .._profile import pg_profile_enabled, ms_slower_then
|
2021-09-19 19:56:02 +00:00
|
|
|
from ._style import hcolor
|
2022-05-15 19:15:14 +00:00
|
|
|
# from ._compression import (
|
|
|
|
# # ohlc_to_m4_line,
|
|
|
|
# ds_m4,
|
|
|
|
# )
|
|
|
|
from ._pathops import xy_downsample
|
2022-03-31 23:04:52 +00:00
|
|
|
from ..log import get_logger
|
|
|
|
|
|
|
|
|
|
|
|
log = get_logger(__name__)
|
2021-09-19 19:56:02 +00:00
|
|
|
|
|
|
|
|
2022-02-02 19:02:21 +00:00
|
|
|
_line_styles: dict[str, int] = {
|
|
|
|
'solid': Qt.PenStyle.SolidLine,
|
|
|
|
'dash': Qt.PenStyle.DashLine,
|
|
|
|
'dot': Qt.PenStyle.DotLine,
|
|
|
|
'dashdot': Qt.PenStyle.DashDotLine,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
2022-03-30 19:43:14 +00:00
|
|
|
class FastAppendCurve(pg.GraphicsObject):
|
2021-12-07 21:09:47 +00:00
|
|
|
'''
|
|
|
|
A faster, append friendly version of ``pyqtgraph.PlotCurveItem``
|
|
|
|
built for real-time data updates.
|
|
|
|
|
|
|
|
The main difference is avoiding regeneration of the entire
|
|
|
|
historical path where possible and instead only updating the "new"
|
|
|
|
segment(s) via a ``numpy`` array diff calc. Further the "last"
|
|
|
|
graphic segment is drawn independently such that near-term (high
|
|
|
|
frequency) discrete-time-sampled style updates don't trigger a full
|
|
|
|
path redraw.
|
|
|
|
|
|
|
|
'''
|
2021-09-17 20:01:28 +00:00
|
|
|
def __init__(
|
|
|
|
self,
|
|
|
|
*args,
|
2022-03-30 19:43:14 +00:00
|
|
|
|
2021-09-17 20:01:28 +00:00
|
|
|
step_mode: bool = False,
|
2021-09-20 17:38:12 +00:00
|
|
|
color: str = 'default_lightest',
|
|
|
|
fill_color: Optional[str] = None,
|
2022-02-02 19:02:21 +00:00
|
|
|
style: str = 'solid',
|
2022-02-10 13:08:42 +00:00
|
|
|
name: Optional[str] = None,
|
2022-03-31 23:04:52 +00:00
|
|
|
use_fpath: bool = True,
|
2021-09-20 17:38:12 +00:00
|
|
|
|
2021-09-17 20:01:28 +00:00
|
|
|
**kwargs
|
2021-09-20 17:38:12 +00:00
|
|
|
|
2021-09-17 20:01:28 +00:00
|
|
|
) -> None:
|
2020-12-28 22:31:58 +00:00
|
|
|
|
2022-03-30 19:43:14 +00:00
|
|
|
# brutaaalll, see comments within..
|
2022-05-16 21:58:44 +00:00
|
|
|
self.yData = None
|
|
|
|
self.xData = None
|
2022-05-18 13:08:08 +00:00
|
|
|
# self._vr: Optional[tuple] = None
|
|
|
|
# self._avr: Optional[tuple] = None
|
2022-05-17 23:14:49 +00:00
|
|
|
self._last_cap: int = 0
|
2022-03-30 19:43:14 +00:00
|
|
|
|
2022-03-09 16:07:53 +00:00
|
|
|
self._name = name
|
2022-03-23 16:32:55 +00:00
|
|
|
self.path: Optional[QtGui.QPainterPath] = None
|
2022-03-09 16:07:53 +00:00
|
|
|
|
2022-03-31 23:04:52 +00:00
|
|
|
self.use_fpath = use_fpath
|
|
|
|
self.fast_path: Optional[QtGui.QPainterPath] = None
|
|
|
|
|
2020-12-28 22:31:58 +00:00
|
|
|
# TODO: we can probably just dispense with the parent since
|
|
|
|
# we're basically only using the pen setting now...
|
|
|
|
super().__init__(*args, **kwargs)
|
2022-03-30 19:43:14 +00:00
|
|
|
|
|
|
|
# self._xrange: tuple[int, int] = self.dataBounds(ax=0)
|
2022-05-18 13:08:08 +00:00
|
|
|
# self._xrange: Optional[tuple[int, int]] = None
|
2022-04-26 12:34:53 +00:00
|
|
|
# self._x_iv_range = None
|
2021-09-21 19:27:22 +00:00
|
|
|
|
2022-03-09 16:07:53 +00:00
|
|
|
# self._last_draw = time.time()
|
2022-05-18 13:08:08 +00:00
|
|
|
# self._in_ds: bool = False
|
|
|
|
# self._last_uppx: float = 0
|
2022-03-09 16:07:53 +00:00
|
|
|
|
2021-09-21 19:27:22 +00:00
|
|
|
# all history of curve is drawn in single px thickness
|
2022-02-02 19:02:21 +00:00
|
|
|
pen = pg.mkPen(hcolor(color))
|
|
|
|
pen.setStyle(_line_styles[style])
|
2022-02-08 20:57:02 +00:00
|
|
|
|
2022-02-02 19:02:21 +00:00
|
|
|
if 'dash' in style:
|
2022-02-08 20:57:02 +00:00
|
|
|
pen.setDashPattern([8, 3])
|
|
|
|
|
2022-03-30 19:43:14 +00:00
|
|
|
self._pen = pen
|
2021-09-21 19:27:22 +00:00
|
|
|
|
|
|
|
# last segment is drawn in 2px thickness for emphasis
|
2022-02-10 13:08:42 +00:00
|
|
|
# self.last_step_pen = pg.mkPen(hcolor(color), width=2)
|
|
|
|
self.last_step_pen = pg.mkPen(pen, width=2)
|
|
|
|
|
2022-03-06 22:15:43 +00:00
|
|
|
self._last_line: Optional[QLineF] = None
|
|
|
|
self._last_step_rect: Optional[QRectF] = None
|
2021-09-21 19:27:22 +00:00
|
|
|
|
|
|
|
# flat-top style histogram-like discrete curve
|
2021-09-17 20:01:28 +00:00
|
|
|
self._step_mode: bool = step_mode
|
2020-12-28 22:31:58 +00:00
|
|
|
|
2021-12-07 21:09:47 +00:00
|
|
|
# self._fill = True
|
2022-03-30 19:43:14 +00:00
|
|
|
self._brush = pg.functions.mkBrush(hcolor(fill_color or color))
|
2021-09-19 19:56:02 +00:00
|
|
|
|
2020-12-28 22:31:58 +00:00
|
|
|
# TODO: one question still remaining is if this makes trasform
|
|
|
|
# interactions slower (such as zooming) and if so maybe if/when
|
|
|
|
# we implement a "history" mode for the view we disable this in
|
|
|
|
# that mode?
|
2022-04-22 23:02:22 +00:00
|
|
|
# if step_mode:
|
|
|
|
# don't enable caching by default for the case where the
|
|
|
|
# only thing drawn is the "last" line segment which can
|
|
|
|
# have a weird artifact where it won't be fully drawn to its
|
|
|
|
# endpoint (something we saw on trade rate curves)
|
2022-04-23 21:22:02 +00:00
|
|
|
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
|
2022-03-30 19:43:14 +00:00
|
|
|
|
2022-04-01 17:46:37 +00:00
|
|
|
# TODO: probably stick this in a new parent
|
|
|
|
# type which will contain our own version of
|
|
|
|
# what ``PlotCurveItem`` had in terms of base
|
|
|
|
# functionality? A `FlowGraphic` maybe?
|
2022-03-31 23:04:52 +00:00
|
|
|
def x_uppx(self) -> int:
|
|
|
|
|
|
|
|
px_vecs = self.pixelVectors()[0]
|
|
|
|
if px_vecs:
|
|
|
|
xs_in_px = px_vecs.x()
|
|
|
|
return round(xs_in_px)
|
|
|
|
else:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
def px_width(self) -> float:
|
|
|
|
|
|
|
|
vb = self.getViewBox()
|
|
|
|
if not vb:
|
|
|
|
return 0
|
|
|
|
|
|
|
|
vr = self.viewRect()
|
|
|
|
l, r = int(vr.left()), int(vr.right())
|
|
|
|
|
2022-05-18 13:08:08 +00:00
|
|
|
# if not self._xrange:
|
|
|
|
# return 0
|
2022-04-04 14:20:10 +00:00
|
|
|
|
2022-03-31 23:04:52 +00:00
|
|
|
start, stop = self._xrange
|
|
|
|
lbar = max(l, start)
|
|
|
|
rbar = min(r, stop)
|
|
|
|
|
|
|
|
return vb.mapViewToDevice(
|
|
|
|
QLineF(lbar, 0, rbar, 0)
|
|
|
|
).length()
|
|
|
|
|
2022-04-20 16:13:18 +00:00
|
|
|
def draw_last(
|
|
|
|
self,
|
|
|
|
x: np.ndarray,
|
|
|
|
y: np.ndarray,
|
|
|
|
|
|
|
|
) -> None:
|
|
|
|
x_last = x[-1]
|
|
|
|
y_last = y[-1]
|
|
|
|
|
2021-12-07 21:09:47 +00:00
|
|
|
# draw the "current" step graphic segment so it lines up with
|
|
|
|
# the "middle" of the current (OHLC) sample.
|
2021-09-19 19:56:02 +00:00
|
|
|
if self._step_mode:
|
2021-09-21 19:27:22 +00:00
|
|
|
self._last_line = QLineF(
|
|
|
|
x_last - 0.5, 0,
|
|
|
|
x_last + 0.5, 0,
|
2022-04-26 12:34:53 +00:00
|
|
|
# x_last, 0,
|
|
|
|
# x_last, 0,
|
2021-09-21 19:27:22 +00:00
|
|
|
)
|
|
|
|
self._last_step_rect = QRectF(
|
|
|
|
x_last - 0.5, 0,
|
|
|
|
x_last + 0.5, y_last
|
2022-04-26 12:34:53 +00:00
|
|
|
# x_last, 0,
|
|
|
|
# x_last, y_last
|
2021-09-21 19:27:22 +00:00
|
|
|
)
|
2022-03-31 23:04:52 +00:00
|
|
|
# print(
|
|
|
|
# f"path br: {self.path.boundingRect()}",
|
|
|
|
# f"fast path br: {self.fast_path.boundingRect()}",
|
|
|
|
# f"last rect br: {self._last_step_rect}",
|
|
|
|
# )
|
2021-09-19 19:56:02 +00:00
|
|
|
else:
|
2021-09-21 19:27:22 +00:00
|
|
|
self._last_line = QLineF(
|
|
|
|
x[-2], y[-2],
|
2022-04-20 16:13:18 +00:00
|
|
|
x_last, y_last
|
2021-09-21 19:27:22 +00:00
|
|
|
)
|
2020-12-28 22:31:58 +00:00
|
|
|
|
2022-03-30 19:43:14 +00:00
|
|
|
# XXX: lol brutal, the internals of `CurvePoint` (inherited by
|
|
|
|
# our `LineDot`) required ``.getData()`` to work..
|
|
|
|
def getData(self):
|
2022-05-16 21:58:44 +00:00
|
|
|
return self.xData, self.yData
|
2022-03-31 23:04:52 +00:00
|
|
|
|
2022-03-23 16:32:55 +00:00
|
|
|
def clear(self):
|
|
|
|
'''
|
|
|
|
Clear internal graphics making object ready for full re-draw.
|
|
|
|
|
|
|
|
'''
|
|
|
|
# NOTE: original code from ``pg.PlotCurveItem``
|
2022-03-30 19:43:14 +00:00
|
|
|
self.xData = None
|
2022-03-23 16:32:55 +00:00
|
|
|
self.yData = None
|
|
|
|
|
2022-03-31 23:04:52 +00:00
|
|
|
# XXX: previously, if not trying to leverage `.reserve()` allocs
|
|
|
|
# then you might as well create a new one..
|
|
|
|
# self.path = None
|
|
|
|
|
2022-03-23 16:32:55 +00:00
|
|
|
# path reservation aware non-mem de-alloc cleaning
|
|
|
|
if self.path:
|
|
|
|
self.path.clear()
|
|
|
|
|
2022-03-31 23:04:52 +00:00
|
|
|
if self.fast_path:
|
|
|
|
# self.fast_path.clear()
|
|
|
|
self.fast_path = None
|
|
|
|
|
|
|
|
# self.disable_cache()
|
|
|
|
# self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
|
2022-03-23 21:29:56 +00:00
|
|
|
|
2022-04-24 16:33:25 +00:00
|
|
|
def reset_cache(self) -> None:
|
|
|
|
self.disable_cache()
|
|
|
|
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
|
|
|
|
|
2022-02-10 13:08:42 +00:00
|
|
|
def disable_cache(self) -> None:
|
2022-02-11 15:41:47 +00:00
|
|
|
'''
|
|
|
|
Disable the use of the pixel coordinate cache and trigger a geo event.
|
|
|
|
|
|
|
|
'''
|
2022-02-10 13:08:42 +00:00
|
|
|
# XXX: pretty annoying but, without this there's little
|
|
|
|
# artefacts on the append updates to the curve...
|
|
|
|
self.setCacheMode(QtWidgets.QGraphicsItem.NoCache)
|
2022-04-26 12:34:53 +00:00
|
|
|
# self.prepareGeometryChange()
|
2022-02-10 13:08:42 +00:00
|
|
|
|
2020-12-28 22:31:58 +00:00
|
|
|
def boundingRect(self):
|
2022-02-11 15:41:47 +00:00
|
|
|
'''
|
|
|
|
Compute and then cache our rect.
|
|
|
|
'''
|
2022-03-31 23:04:52 +00:00
|
|
|
if self.path is None:
|
|
|
|
return QtGui.QPainterPath().boundingRect()
|
2021-01-01 18:23:05 +00:00
|
|
|
else:
|
2022-03-31 23:04:52 +00:00
|
|
|
# dynamically override this method after initial
|
|
|
|
# path is created to avoid requiring the above None check
|
|
|
|
self.boundingRect = self._path_br
|
|
|
|
return self._path_br()
|
2021-01-01 18:23:05 +00:00
|
|
|
|
2022-03-09 16:07:53 +00:00
|
|
|
def _path_br(self):
|
2022-02-11 15:41:47 +00:00
|
|
|
'''
|
|
|
|
Post init ``.boundingRect()```.
|
2021-01-01 18:23:05 +00:00
|
|
|
|
2022-02-11 15:41:47 +00:00
|
|
|
'''
|
2020-12-28 22:31:58 +00:00
|
|
|
hb = self.path.controlPointRect()
|
2022-04-26 12:34:53 +00:00
|
|
|
# hb = self.path.boundingRect()
|
2020-12-28 22:31:58 +00:00
|
|
|
hb_size = hb.size()
|
2022-03-31 23:04:52 +00:00
|
|
|
|
|
|
|
fp = self.fast_path
|
|
|
|
if fp:
|
|
|
|
fhb = fp.controlPointRect()
|
|
|
|
hb_size = fhb.size() + hb_size
|
2020-12-28 22:31:58 +00:00
|
|
|
# print(f'hb_size: {hb_size}')
|
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
# if self._last_step_rect:
|
|
|
|
# hb_size += self._last_step_rect.size()
|
|
|
|
|
|
|
|
# if self._line:
|
|
|
|
# br = self._last_step_rect.bottomRight()
|
|
|
|
|
|
|
|
# tl = QPointF(
|
|
|
|
# # self._vr[0],
|
|
|
|
# # hb.topLeft().y(),
|
|
|
|
# # 0,
|
|
|
|
# # hb_size.height() + 1
|
|
|
|
# )
|
|
|
|
|
|
|
|
# if self._last_step_rect:
|
|
|
|
# br = self._last_step_rect.bottomRight()
|
|
|
|
|
|
|
|
# else:
|
|
|
|
# hb_size += QSizeF(1, 1)
|
2020-12-28 22:31:58 +00:00
|
|
|
w = hb_size.width() + 1
|
|
|
|
h = hb_size.height() + 1
|
2021-02-12 04:41:40 +00:00
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
# br = QPointF(
|
|
|
|
# self._vr[-1],
|
|
|
|
# # tl.x() + w,
|
|
|
|
# tl.y() + h,
|
|
|
|
# )
|
|
|
|
|
2021-09-19 19:56:02 +00:00
|
|
|
br = QRectF(
|
2020-12-28 22:31:58 +00:00
|
|
|
|
|
|
|
# top left
|
2022-04-26 12:34:53 +00:00
|
|
|
# hb.topLeft()
|
|
|
|
# tl,
|
2021-09-19 19:56:02 +00:00
|
|
|
QPointF(hb.topLeft()),
|
2020-12-28 22:31:58 +00:00
|
|
|
|
2022-04-26 12:34:53 +00:00
|
|
|
# br,
|
2020-12-28 22:31:58 +00:00
|
|
|
# total size
|
2022-04-26 12:34:53 +00:00
|
|
|
# QSizeF(hb_size)
|
|
|
|
# hb_size,
|
2021-09-19 19:56:02 +00:00
|
|
|
QSizeF(w, h)
|
2020-12-28 22:31:58 +00:00
|
|
|
)
|
|
|
|
# print(f'bounding rect: {br}')
|
|
|
|
return br
|
|
|
|
|
2020-12-29 17:55:56 +00:00
|
|
|
def paint(
|
|
|
|
self,
|
|
|
|
p: QtGui.QPainter,
|
|
|
|
opt: QtWidgets.QStyleOptionGraphicsItem,
|
|
|
|
w: QtWidgets.QWidget
|
2022-02-08 20:57:02 +00:00
|
|
|
|
2020-12-29 17:55:56 +00:00
|
|
|
) -> None:
|
2020-12-28 22:31:58 +00:00
|
|
|
|
2022-03-09 16:07:53 +00:00
|
|
|
profiler = pg.debug.Profiler(
|
2022-04-01 17:46:37 +00:00
|
|
|
msg=f'FastAppendCurve.paint(): `{self._name}`',
|
2022-03-14 10:04:18 +00:00
|
|
|
disabled=not pg_profile_enabled(),
|
2022-05-13 20:04:31 +00:00
|
|
|
ms_threshold=ms_slower_then,
|
2022-03-09 16:07:53 +00:00
|
|
|
)
|
2022-04-26 12:34:53 +00:00
|
|
|
self.prepareGeometryChange()
|
2020-12-28 22:31:58 +00:00
|
|
|
|
2022-01-13 21:05:05 +00:00
|
|
|
if (
|
|
|
|
self._step_mode
|
|
|
|
and self._last_step_rect
|
|
|
|
):
|
2022-03-30 19:43:14 +00:00
|
|
|
brush = self._brush
|
|
|
|
|
2021-09-20 17:38:12 +00:00
|
|
|
# p.drawLines(*tuple(filter(bool, self._last_step_lines)))
|
|
|
|
# p.drawRect(self._last_step_rect)
|
|
|
|
p.fillRect(self._last_step_rect, brush)
|
2022-04-01 17:46:37 +00:00
|
|
|
profiler('.fillRect()')
|
2021-09-19 19:56:02 +00:00
|
|
|
|
2022-03-06 22:15:43 +00:00
|
|
|
if self._last_line:
|
|
|
|
p.setPen(self.last_step_pen)
|
|
|
|
p.drawLine(self._last_line)
|
|
|
|
profiler('.drawLine()')
|
2022-03-30 19:43:14 +00:00
|
|
|
p.setPen(self._pen)
|
2021-09-19 19:56:02 +00:00
|
|
|
|
2022-03-14 10:04:18 +00:00
|
|
|
path = self.path
|
|
|
|
|
2022-05-17 23:14:49 +00:00
|
|
|
# cap = path.capacity()
|
|
|
|
# if cap != self._last_cap:
|
|
|
|
# print(f'NEW CAPACITY: {self._last_cap} -> {cap}')
|
|
|
|
# self._last_cap = cap
|
|
|
|
|
2022-03-31 23:04:52 +00:00
|
|
|
if path:
|
2022-03-14 10:04:18 +00:00
|
|
|
p.drawPath(path)
|
2022-04-22 17:59:20 +00:00
|
|
|
profiler(f'.drawPath(path): {path.capacity()}')
|
2022-03-31 23:04:52 +00:00
|
|
|
|
|
|
|
fp = self.fast_path
|
|
|
|
if fp:
|
|
|
|
p.drawPath(fp)
|
|
|
|
profiler('.drawPath(fast_path)')
|
2021-09-19 19:56:02 +00:00
|
|
|
|
2022-03-09 16:07:53 +00:00
|
|
|
# TODO: try out new work from `pyqtgraph` main which should
|
|
|
|
# repair horrid perf (pretty sure i did and it was still
|
|
|
|
# horrible?):
|
2021-12-07 21:09:47 +00:00
|
|
|
# https://github.com/pyqtgraph/pyqtgraph/pull/2032
|
|
|
|
# if self._fill:
|
|
|
|
# brush = self.opts['brush']
|
|
|
|
# p.fillPath(self.path, brush)
|