# 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 . ''' 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