343 lines
10 KiB
Python
343 lines
10 KiB
Python
# piker: trading gear for hackers
|
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
|
|
|
# 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/>.
|
|
|
|
'''
|
|
High level streaming graphics primitives.
|
|
|
|
This is an intermediate layer which associates real-time low latency
|
|
graphics primitives with underlying stream/flow related data structures
|
|
for fast incremental update.
|
|
|
|
'''
|
|
from __future__ import annotations
|
|
from typing import (
|
|
TYPE_CHECKING,
|
|
)
|
|
|
|
import msgspec
|
|
import numpy as np
|
|
import pyqtgraph as pg
|
|
|
|
from piker.ui.qt import QPainterPath
|
|
from ..data._formatters import (
|
|
IncrementalFormatter,
|
|
)
|
|
from ..data._pathops import (
|
|
xy_downsample,
|
|
)
|
|
from ..log import get_logger
|
|
from ..toolz import (
|
|
Profiler,
|
|
)
|
|
|
|
if TYPE_CHECKING:
|
|
from ._dataviz import Viz
|
|
|
|
|
|
log = get_logger(__name__)
|
|
|
|
|
|
class Renderer(msgspec.Struct):
|
|
'''
|
|
Low(er) level interface for converting a source, real-time updated,
|
|
data buffer (usually held in a ``ShmArray``) to a graphics data
|
|
format usable by `Qt`.
|
|
|
|
A renderer reads in context-specific source data using a ``Viz``,
|
|
formats that data to a 2D-xy pre-graphics format using
|
|
a ``IncrementalFormatter``, then renders that data to a set of
|
|
output graphics objects normally a ``.ui._curve.FlowGraphics``
|
|
sub-type to which the ``Renderer.path`` is applied and further "last
|
|
datum" graphics are updated from the source buffer's latest
|
|
sample(s).
|
|
|
|
'''
|
|
viz: Viz
|
|
fmtr: IncrementalFormatter
|
|
|
|
# output graphics rendering, the main object
|
|
# processed in ``QGraphicsObject.paint()``
|
|
path: QPainterPath | None = None
|
|
fast_path: QPainterPath | None = None
|
|
|
|
# downsampling state
|
|
_last_uppx: float = 0
|
|
_in_ds: bool = False
|
|
|
|
def draw_path(
|
|
self,
|
|
x: np.ndarray,
|
|
y: np.ndarray,
|
|
connect: str | np.ndarray = 'all',
|
|
path: QPainterPath | None = None,
|
|
redraw: bool = False,
|
|
|
|
) -> QPainterPath:
|
|
|
|
path_was_none = path is None
|
|
|
|
if redraw and path:
|
|
path.clear()
|
|
|
|
# TODO: avoid this?
|
|
if self.fast_path:
|
|
self.fast_path.clear()
|
|
|
|
path = pg.functions.arrayToQPath(
|
|
x,
|
|
y,
|
|
connect=connect,
|
|
finiteCheck=False,
|
|
|
|
# reserve mem allocs see:
|
|
# - https://doc.qt.io/qt-5/qpainterpath.html#reserve
|
|
# - https://doc.qt.io/qt-5/qpainterpath.html#capacity
|
|
# - https://doc.qt.io/qt-5/qpainterpath.html#clear
|
|
# XXX: right now this is based on ad-hoc checks on a
|
|
# hidpi 3840x2160 4k monitor but we should optimize for
|
|
# the target display(s) on the sys.
|
|
# if no_path_yet:
|
|
# graphics.path.reserve(int(500e3))
|
|
# path=path, # path re-use / reserving
|
|
)
|
|
|
|
# avoid mem allocs if possible
|
|
if path_was_none:
|
|
path.reserve(path.capacity())
|
|
|
|
return path
|
|
|
|
def render(
|
|
self,
|
|
|
|
new_read,
|
|
array_key: str,
|
|
profiler: Profiler,
|
|
uppx: float = 1,
|
|
|
|
# redraw and ds flags
|
|
should_redraw: bool = False,
|
|
new_sample_rate: bool = False,
|
|
should_ds: bool = False,
|
|
showing_src_data: bool = True,
|
|
|
|
do_append: bool = True,
|
|
use_fpath: bool = True,
|
|
force_reformat: bool = False,
|
|
|
|
# only render datums "in view" of the ``ChartView``
|
|
use_vr: bool = True,
|
|
|
|
) -> tuple[QPainterPath, bool]:
|
|
'''
|
|
Render the current graphics path(s)
|
|
|
|
There are (at least) 3 stages from source data to graphics data:
|
|
- a data transform (which can be stored in additional shm)
|
|
- a graphics transform which converts discrete basis data to
|
|
a `float`-basis view-coords graphics basis. (eg. ``ohlc_flatten()``,
|
|
``step_path_arrays_from_1d()``, etc.)
|
|
|
|
- blah blah blah (from notes)
|
|
|
|
'''
|
|
# TODO: can the renderer just call ``Viz.read()`` directly?
|
|
# unpack latest source data read
|
|
fmtr = self.fmtr
|
|
|
|
(
|
|
_,
|
|
_,
|
|
array,
|
|
ivl,
|
|
ivr,
|
|
in_view,
|
|
) = new_read
|
|
|
|
# xy-path data transform: convert source data to a format
|
|
# able to be passed to a `QPainterPath` rendering routine.
|
|
fmt_out = fmtr.format_to_1d(
|
|
new_read,
|
|
array_key,
|
|
profiler,
|
|
|
|
slice_to_inview=use_vr,
|
|
force_full_realloc=force_reformat,
|
|
)
|
|
|
|
# no history in view case
|
|
if not fmt_out:
|
|
# XXX: this might be why the profiler only has exits?
|
|
return
|
|
|
|
(
|
|
x_1d,
|
|
y_1d,
|
|
connect,
|
|
prepend_length,
|
|
append_length,
|
|
view_changed,
|
|
# append_tres,
|
|
|
|
) = fmt_out
|
|
|
|
if not x_1d.size:
|
|
log.warning(f'{array_key} has no `.size`?')
|
|
return
|
|
|
|
# redraw conditions
|
|
if (
|
|
prepend_length > 0
|
|
or new_sample_rate
|
|
or view_changed
|
|
|
|
# NOTE: comment this to try and make "append paths"
|
|
# work below..
|
|
or append_length > 0
|
|
):
|
|
should_redraw = True
|
|
|
|
path: QPainterPath = self.path
|
|
fast_path: QPainterPath = self.fast_path
|
|
reset: bool = False
|
|
|
|
self.viz.ds_yrange = None
|
|
|
|
# redraw the entire source data if we have either of:
|
|
# - no prior path graphic rendered or,
|
|
# - we always intend to re-render the data only in view
|
|
if (
|
|
path is None
|
|
or should_redraw
|
|
):
|
|
# print(f"{self.viz.name} -> REDRAWING BRUH")
|
|
if (
|
|
new_sample_rate
|
|
and showing_src_data
|
|
):
|
|
log.info(f'DE-downsampling -> {array_key}')
|
|
self._in_ds = False
|
|
|
|
elif should_ds and uppx > 1:
|
|
|
|
ds_out = xy_downsample(
|
|
x_1d,
|
|
y_1d,
|
|
uppx,
|
|
)
|
|
if ds_out is not None:
|
|
x_1d, y_1d, ymn, ymx = ds_out
|
|
self.viz.ds_yrange = ymn, ymx
|
|
# print(f'{self.viz.name} post ds: ymn, ymx: {ymn},{ymx}')
|
|
|
|
reset = True
|
|
profiler(f'FULL PATH downsample redraw={should_ds}')
|
|
self._in_ds = True
|
|
|
|
path = self.draw_path(
|
|
x=x_1d,
|
|
y=y_1d,
|
|
connect=connect,
|
|
path=path,
|
|
redraw=True,
|
|
)
|
|
|
|
profiler(
|
|
'generated fresh path. '
|
|
f'(should_redraw: {should_redraw} '
|
|
f'should_ds: {should_ds} new_sample_rate: {new_sample_rate})'
|
|
)
|
|
|
|
# TODO: get this piecewise prepend working - right now it's
|
|
# giving heck on vwap...
|
|
# elif prepend_length:
|
|
|
|
# prepend_path = pg.functions.arrayToQPath(
|
|
# x[0:prepend_length],
|
|
# y[0:prepend_length],
|
|
# connect='all'
|
|
# )
|
|
|
|
# # swap prepend path in "front"
|
|
# old_path = graphics.path
|
|
# graphics.path = prepend_path
|
|
# # graphics.path.moveTo(new_x[0], new_y[0])
|
|
# graphics.path.connectPath(old_path)
|
|
|
|
elif (
|
|
append_length > 0
|
|
and do_append
|
|
):
|
|
profiler(f'sliced append path {append_length}')
|
|
# (
|
|
# x_1d,
|
|
# y_1d,
|
|
# connect,
|
|
# ) = append_tres
|
|
|
|
profiler(
|
|
f'diffed array input, append_length={append_length}'
|
|
)
|
|
|
|
# if should_ds and uppx > 1:
|
|
# new_x, new_y = xy_downsample(
|
|
# new_x,
|
|
# new_y,
|
|
# uppx,
|
|
# )
|
|
# profiler(f'fast path downsample redraw={should_ds}')
|
|
|
|
append_path = self.draw_path(
|
|
x=x_1d,
|
|
y=y_1d,
|
|
connect=connect,
|
|
path=fast_path,
|
|
)
|
|
profiler('generated append qpath')
|
|
|
|
if use_fpath:
|
|
# an attempt at trying to make append-updates faster..
|
|
if fast_path is None:
|
|
fast_path = append_path
|
|
# fast_path.reserve(int(6e3))
|
|
else:
|
|
# print(
|
|
# f'{self.viz.name}: FAST PATH\n'
|
|
# f"append_path br: {append_path.boundingRect()}\n"
|
|
# f"path size: {size}\n"
|
|
# f"append_path len: {append_path.length()}\n"
|
|
# f"fast_path len: {fast_path.length()}\n"
|
|
# )
|
|
|
|
fast_path.connectPath(append_path)
|
|
size = fast_path.capacity()
|
|
profiler(f'connected fast path w size: {size}')
|
|
|
|
# graphics.path.moveTo(new_x[0], new_y[0])
|
|
# path.connectPath(append_path)
|
|
|
|
# XXX: lol this causes a hang..
|
|
# graphics.path = graphics.path.simplified()
|
|
else:
|
|
size = path.capacity()
|
|
profiler(f'connected history path w size: {size}')
|
|
path.connectPath(append_path)
|
|
|
|
self.path = path
|
|
self.fast_path = fast_path
|
|
|
|
return self.path, reset
|