Merge pull request #490 from pikers/log_linearized_curve_overlays
Log linearized curve overlaysbinance_ws_ep_update
commit
f3b04f27e6
|
@ -295,7 +295,7 @@ def slice_from_time(
|
|||
arr: np.ndarray,
|
||||
start_t: float,
|
||||
stop_t: float,
|
||||
step: int | None = None,
|
||||
step: float, # sampler period step-diff
|
||||
|
||||
) -> slice:
|
||||
'''
|
||||
|
@ -324,12 +324,6 @@ def slice_from_time(
|
|||
# end of the input array.
|
||||
read_i_max = arr.shape[0]
|
||||
|
||||
# TODO: require this is always passed in?
|
||||
if step is None:
|
||||
step = round(t_last - times[-2])
|
||||
if step == 0:
|
||||
step = 1
|
||||
|
||||
# compute (presumed) uniform-time-step index offsets
|
||||
i_start_t = floor(start_t)
|
||||
read_i_start = floor(((i_start_t - t_first) // step)) - 1
|
||||
|
@ -395,7 +389,7 @@ def slice_from_time(
|
|||
# f'diff: {t_diff}\n'
|
||||
# f'REMAPPED START i: {read_i_start} -> {new_read_i_start}\n'
|
||||
# )
|
||||
read_i_start = new_read_i_start - 1
|
||||
read_i_start = new_read_i_start
|
||||
|
||||
t_iv_stop = times[read_i_stop - 1]
|
||||
if (
|
||||
|
@ -412,7 +406,7 @@ def slice_from_time(
|
|||
times[read_i_start:],
|
||||
# times,
|
||||
i_stop_t,
|
||||
side='left',
|
||||
side='right',
|
||||
)
|
||||
|
||||
if (
|
||||
|
|
|
@ -87,7 +87,6 @@ class Sampler:
|
|||
# holds all the ``tractor.Context`` remote subscriptions for
|
||||
# a particular sample period increment event: all subscribers are
|
||||
# notified on a step.
|
||||
# subscribers: dict[int, list[tractor.MsgStream]] = {}
|
||||
subscribers: defaultdict[
|
||||
float,
|
||||
list[
|
||||
|
@ -240,8 +239,11 @@ class Sampler:
|
|||
subscribers for a given sample period.
|
||||
|
||||
'''
|
||||
pair: list[float, set]
|
||||
pair = self.subscribers[period_s]
|
||||
|
||||
last_ts: float
|
||||
subs: set
|
||||
last_ts, subs = pair
|
||||
|
||||
task = trio.lowlevel.current_task()
|
||||
|
@ -253,25 +255,35 @@ class Sampler:
|
|||
# f'consumers: {subs}'
|
||||
)
|
||||
borked: set[tractor.MsgStream] = set()
|
||||
for stream in subs:
|
||||
sent: set[tractor.MsgStream] = set()
|
||||
while True:
|
||||
try:
|
||||
await stream.send({
|
||||
'index': time_stamp or last_ts,
|
||||
'period': period_s,
|
||||
})
|
||||
except (
|
||||
trio.BrokenResourceError,
|
||||
trio.ClosedResourceError
|
||||
):
|
||||
log.error(
|
||||
f'{stream._ctx.chan.uid} dropped connection'
|
||||
)
|
||||
borked.add(stream)
|
||||
for stream in (subs - sent):
|
||||
try:
|
||||
await stream.send({
|
||||
'index': time_stamp or last_ts,
|
||||
'period': period_s,
|
||||
})
|
||||
sent.add(stream)
|
||||
|
||||
except (
|
||||
trio.BrokenResourceError,
|
||||
trio.ClosedResourceError
|
||||
):
|
||||
log.error(
|
||||
f'{stream._ctx.chan.uid} dropped connection'
|
||||
)
|
||||
borked.add(stream)
|
||||
else:
|
||||
break
|
||||
except RuntimeError:
|
||||
log.warning(f'Client subs {subs} changed while broadcasting')
|
||||
continue
|
||||
|
||||
for stream in borked:
|
||||
try:
|
||||
subs.remove(stream)
|
||||
except ValueError:
|
||||
except KeyError:
|
||||
log.warning(
|
||||
f'{stream._ctx.chan.uid} sub already removed!?'
|
||||
)
|
||||
|
@ -419,7 +431,7 @@ async def maybe_open_samplerd(
|
|||
loglevel: str | None = None,
|
||||
**kwargs,
|
||||
|
||||
) -> tractor._portal.Portal: # noqa
|
||||
) -> tractor.Portal: # noqa
|
||||
'''
|
||||
Client-side helper to maybe startup the ``samplerd`` service
|
||||
under the ``pikerd`` tree.
|
||||
|
@ -609,6 +621,14 @@ async def sample_and_broadcast(
|
|||
fqsn = f'{broker_symbol}.{brokername}'
|
||||
lags: int = 0
|
||||
|
||||
# TODO: speed up this loop in an AOT compiled lang (like
|
||||
# rust or nim or zig) and/or instead of doing a fan out to
|
||||
# TCP sockets here, we add a shm-style tick queue which
|
||||
# readers can pull from instead of placing the burden of
|
||||
# broadcast on solely on this `brokerd` actor. see issues:
|
||||
# - https://github.com/pikers/piker/issues/98
|
||||
# - https://github.com/pikers/piker/issues/107
|
||||
|
||||
for (stream, tick_throttle) in subs.copy():
|
||||
try:
|
||||
with trio.move_on_after(0.2) as cs:
|
||||
|
@ -738,9 +758,6 @@ def frame_ticks(
|
|||
ticks_by_type[ttype].append(tick)
|
||||
|
||||
|
||||
# TODO: a less naive throttler, here's some snippets:
|
||||
# token bucket by njs:
|
||||
# https://gist.github.com/njsmith/7ea44ec07e901cb78ebe1dd8dd846cb9
|
||||
async def uniform_rate_send(
|
||||
|
||||
rate: float,
|
||||
|
@ -750,8 +767,22 @@ async def uniform_rate_send(
|
|||
task_status: TaskStatus = trio.TASK_STATUS_IGNORED,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Throttle a real-time (presumably tick event) stream to a uniform
|
||||
transmissiom rate, normally for the purposes of throttling a data
|
||||
flow being consumed by a graphics rendering actor which itself is limited
|
||||
by a fixed maximum display rate.
|
||||
|
||||
# try not to error-out on overruns of the subscribed (chart) client
|
||||
Though this function isn't documented (nor was intentially written
|
||||
to be) a token-bucket style algo, it effectively operates as one (we
|
||||
think?).
|
||||
|
||||
TODO: a less naive throttler, here's some snippets:
|
||||
token bucket by njs:
|
||||
https://gist.github.com/njsmith/7ea44ec07e901cb78ebe1dd8dd846cb9
|
||||
|
||||
'''
|
||||
# try not to error-out on overruns of the subscribed client
|
||||
stream._ctx._backpressure = True
|
||||
|
||||
# TODO: compute the approx overhead latency per cycle
|
||||
|
@ -848,6 +879,16 @@ async def uniform_rate_send(
|
|||
# rate timing exactly lul
|
||||
try:
|
||||
await stream.send({sym: first_quote})
|
||||
except tractor.RemoteActorError as rme:
|
||||
if rme.type is not tractor._exceptions.StreamOverrun:
|
||||
raise
|
||||
ctx = stream._ctx
|
||||
chan = ctx.chan
|
||||
log.warning(
|
||||
'Throttled quote-stream overrun!\n'
|
||||
f'{sym}:{ctx.cid}@{chan.uid}'
|
||||
)
|
||||
|
||||
except (
|
||||
# NOTE: any of these can be raised by ``tractor``'s IPC
|
||||
# transport-layer and we want to be highly resilient
|
||||
|
|
|
@ -1589,6 +1589,9 @@ async def open_feed(
|
|||
(brokermod, bfqsns),
|
||||
) in zip(ctxs, providers.items()):
|
||||
|
||||
# NOTE: do it asap to avoid overruns during multi-feed setup?
|
||||
ctx._backpressure = backpressure
|
||||
|
||||
for fqsn, flume_msg in flumes_msg_dict.items():
|
||||
flume = Flume.from_msg(flume_msg)
|
||||
assert flume.symbol.fqsn == fqsn
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
Annotations for ur faces.
|
||||
|
||||
"""
|
||||
from typing import Callable, Optional
|
||||
from typing import Callable
|
||||
|
||||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from PyQt5.QtCore import QPointF, QRectF
|
||||
|
@ -105,7 +105,7 @@ class LevelMarker(QGraphicsPathItem):
|
|||
get_level: Callable[..., float],
|
||||
size: float = 20,
|
||||
keep_in_view: bool = True,
|
||||
on_paint: Optional[Callable] = None,
|
||||
on_paint: Callable | None = None,
|
||||
|
||||
) -> None:
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# piker: trading gear for hackers
|
||||
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
||||
# 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
|
||||
|
@ -20,7 +20,7 @@ Chart axes graphics and behavior.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
from functools import lru_cache
|
||||
from typing import Optional, Callable
|
||||
from typing import Callable
|
||||
from math import floor
|
||||
|
||||
import numpy as np
|
||||
|
@ -60,7 +60,8 @@ class Axis(pg.AxisItem):
|
|||
**kwargs
|
||||
)
|
||||
|
||||
# XXX: pretty sure this makes things slower
|
||||
# XXX: pretty sure this makes things slower!
|
||||
# no idea why given we only move labels for the most part?
|
||||
# self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
|
||||
|
||||
self.pi = plotitem
|
||||
|
@ -190,7 +191,7 @@ class PriceAxis(Axis):
|
|||
*args,
|
||||
min_tick: int = 2,
|
||||
title: str = '',
|
||||
formatter: Optional[Callable[[float], str]] = None,
|
||||
formatter: Callable[[float], str] | None = None,
|
||||
**kwargs
|
||||
|
||||
) -> None:
|
||||
|
@ -202,8 +203,8 @@ class PriceAxis(Axis):
|
|||
def set_title(
|
||||
self,
|
||||
title: str,
|
||||
view: Optional[ChartView] = None,
|
||||
color: Optional[str] = None,
|
||||
view: ChartView | None = None,
|
||||
color: str | None = None,
|
||||
|
||||
) -> Label:
|
||||
'''
|
||||
|
@ -303,8 +304,9 @@ class DynamicDateAxis(Axis):
|
|||
viz = chart._vizs[chart.name]
|
||||
shm = viz.shm
|
||||
array = shm.array
|
||||
times = array['time']
|
||||
i_0, i_l = times[0], times[-1]
|
||||
ifield = viz.index_field
|
||||
index = array[ifield]
|
||||
i_0, i_l = index[0], index[-1]
|
||||
|
||||
# edge cases
|
||||
if (
|
||||
|
@ -316,11 +318,13 @@ class DynamicDateAxis(Axis):
|
|||
(indexes[0] > i_0
|
||||
and indexes[-1] > i_l)
|
||||
):
|
||||
# print(f"x-label indexes empty edge case: {indexes}")
|
||||
return []
|
||||
|
||||
if viz.index_field == 'index':
|
||||
arr_len = times.shape[0]
|
||||
if ifield == 'index':
|
||||
arr_len = index.shape[0]
|
||||
first = shm._first.value
|
||||
times = array['time']
|
||||
epochs = times[
|
||||
list(
|
||||
map(
|
||||
|
|
|
@ -19,9 +19,12 @@ High level chart-widget apis.
|
|||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from contextlib import (
|
||||
contextmanager as cm,
|
||||
ExitStack,
|
||||
)
|
||||
from typing import (
|
||||
Iterator,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
|
@ -102,7 +105,7 @@ class GodWidget(QWidget):
|
|||
|
||||
super().__init__(parent)
|
||||
|
||||
self.search: Optional[SearchWidget] = None
|
||||
self.search: SearchWidget | None = None
|
||||
|
||||
self.hbox = QHBoxLayout(self)
|
||||
self.hbox.setContentsMargins(0, 0, 0, 0)
|
||||
|
@ -116,22 +119,14 @@ class GodWidget(QWidget):
|
|||
|
||||
self.hbox.addLayout(self.vbox)
|
||||
|
||||
# self.toolbar_layout = QHBoxLayout()
|
||||
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# self.vbox.addLayout(self.toolbar_layout)
|
||||
|
||||
# self.init_timeframes_ui()
|
||||
# self.init_strategy_ui()
|
||||
# self.vbox.addLayout(self.hbox)
|
||||
|
||||
self._chart_cache: dict[
|
||||
str,
|
||||
tuple[LinkedSplits, LinkedSplits],
|
||||
] = {}
|
||||
|
||||
self.hist_linked: Optional[LinkedSplits] = None
|
||||
self.rt_linked: Optional[LinkedSplits] = None
|
||||
self._active_cursor: Optional[Cursor] = None
|
||||
self.hist_linked: LinkedSplits | None = None
|
||||
self.rt_linked: LinkedSplits | None = None
|
||||
self._active_cursor: Cursor | None = None
|
||||
|
||||
# assigned in the startup func `_async_main()`
|
||||
self._root_n: trio.Nursery = None
|
||||
|
@ -143,15 +138,18 @@ class GodWidget(QWidget):
|
|||
# and the window does not? Never right?!
|
||||
# self.reg_for_resize(self)
|
||||
|
||||
# TODO: strat loader/saver that we don't need yet.
|
||||
# def init_strategy_ui(self):
|
||||
# self.toolbar_layout = QHBoxLayout()
|
||||
# self.toolbar_layout.setContentsMargins(0, 0, 0, 0)
|
||||
# self.vbox.addLayout(self.toolbar_layout)
|
||||
# self.strategy_box = StrategyBoxWidget(self)
|
||||
# self.toolbar_layout.addWidget(self.strategy_box)
|
||||
|
||||
@property
|
||||
def linkedsplits(self) -> LinkedSplits:
|
||||
return self.rt_linked
|
||||
|
||||
# XXX: strat loader/saver that we don't need yet.
|
||||
# def init_strategy_ui(self):
|
||||
# self.strategy_box = StrategyBoxWidget(self)
|
||||
# self.toolbar_layout.addWidget(self.strategy_box)
|
||||
|
||||
def set_chart_symbols(
|
||||
self,
|
||||
group_key: tuple[str], # of form <fqsn>.<providername>
|
||||
|
@ -263,7 +261,9 @@ class GodWidget(QWidget):
|
|||
# last had the xlast in view, if so then shift so it's
|
||||
# still in view, if the user was viewing history then
|
||||
# do nothing yah?
|
||||
self.rt_linked.chart.default_view()
|
||||
self.rt_linked.chart.main_viz.default_view(
|
||||
do_min_bars=True,
|
||||
)
|
||||
|
||||
# if a history chart instance is already up then
|
||||
# set the search widget as its sidepane.
|
||||
|
@ -372,7 +372,7 @@ class ChartnPane(QFrame):
|
|||
'''
|
||||
sidepane: FieldsForm | SearchWidget
|
||||
hbox: QHBoxLayout
|
||||
chart: Optional[ChartPlotWidget] = None
|
||||
chart: ChartPlotWidget | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
|
@ -432,7 +432,7 @@ class LinkedSplits(QWidget):
|
|||
|
||||
self.godwidget = godwidget
|
||||
self.chart: ChartPlotWidget = None # main (ohlc) chart
|
||||
self.subplots: dict[tuple[str, ...], ChartPlotWidget] = {}
|
||||
self.subplots: dict[str, ChartPlotWidget] = {}
|
||||
|
||||
self.godwidget = godwidget
|
||||
# placeholder for last appended ``PlotItem``'s bottom axis.
|
||||
|
@ -450,7 +450,7 @@ class LinkedSplits(QWidget):
|
|||
# chart-local graphics state that can be passed to
|
||||
# a ``graphic_update_cycle()`` call by any task wishing to
|
||||
# update the UI for a given "chart instance".
|
||||
self.display_state: Optional[DisplayState] = None
|
||||
self.display_state: DisplayState | None = None
|
||||
|
||||
self._symbol: Symbol = None
|
||||
|
||||
|
@ -480,7 +480,7 @@ class LinkedSplits(QWidget):
|
|||
|
||||
def set_split_sizes(
|
||||
self,
|
||||
prop: Optional[float] = None,
|
||||
prop: float | None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -494,7 +494,7 @@ class LinkedSplits(QWidget):
|
|||
prop = 3/8
|
||||
|
||||
h = self.height()
|
||||
histview_h = h * (6/16)
|
||||
histview_h = h * (4/11)
|
||||
h = h - histview_h
|
||||
|
||||
major = 1 - prop
|
||||
|
@ -574,11 +574,11 @@ class LinkedSplits(QWidget):
|
|||
shm: ShmArray,
|
||||
flume: Flume,
|
||||
|
||||
array_key: Optional[str] = None,
|
||||
array_key: str | None = None,
|
||||
style: str = 'line',
|
||||
_is_main: bool = False,
|
||||
|
||||
sidepane: Optional[QWidget] = None,
|
||||
sidepane: QWidget | None = None,
|
||||
draw_kwargs: dict = {},
|
||||
|
||||
**cpw_kwargs,
|
||||
|
@ -634,6 +634,7 @@ class LinkedSplits(QWidget):
|
|||
axis.pi = cpw.plotItem
|
||||
|
||||
cpw.hideAxis('left')
|
||||
# cpw.removeAxis('left')
|
||||
cpw.hideAxis('bottom')
|
||||
|
||||
if (
|
||||
|
@ -750,12 +751,12 @@ class LinkedSplits(QWidget):
|
|||
|
||||
# NOTE: back-link the new sub-chart to trigger y-autoranging in
|
||||
# the (ohlc parent) main chart for this linked set.
|
||||
if self.chart:
|
||||
main_viz = self.chart.get_viz(self.chart.name)
|
||||
self.chart.view.enable_auto_yrange(
|
||||
src_vb=cpw.view,
|
||||
viz=main_viz,
|
||||
)
|
||||
# if self.chart:
|
||||
# main_viz = self.chart.get_viz(self.chart.name)
|
||||
# self.chart.view.enable_auto_yrange(
|
||||
# src_vb=cpw.view,
|
||||
# viz=main_viz,
|
||||
# )
|
||||
|
||||
graphics = viz.graphics
|
||||
data_key = viz.name
|
||||
|
@ -793,7 +794,7 @@ class LinkedSplits(QWidget):
|
|||
|
||||
def resize_sidepanes(
|
||||
self,
|
||||
from_linked: Optional[LinkedSplits] = None,
|
||||
from_linked: LinkedSplits | None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -816,11 +817,17 @@ class LinkedSplits(QWidget):
|
|||
self.chart.sidepane.setMinimumWidth(sp_w)
|
||||
|
||||
|
||||
# TODO: we should really drop using this type and instead just
|
||||
# write our own wrapper around `PlotItem`..
|
||||
# TODO: a general rework of this widget-interface:
|
||||
# - we should really drop using this type and instead just lever our
|
||||
# own override of `PlotItem`..
|
||||
# - possibly rename to class -> MultiChart(pg.PlotWidget):
|
||||
# where the widget is responsible for containing management
|
||||
# harness for multi-Viz "view lists" and their associated mode-panes
|
||||
# (fsp chain, order ctl, feed queue-ing params, actor ctl, etc).
|
||||
|
||||
class ChartPlotWidget(pg.PlotWidget):
|
||||
'''
|
||||
``GraphicsView`` subtype containing a ``.plotItem: PlotItem`` as well
|
||||
``PlotWidget`` subtype containing a ``.plotItem: PlotItem`` as well
|
||||
as a `.pi_overlay: PlotItemOverlay`` which helps manage and overlay flow
|
||||
graphics view multiple compose view boxes.
|
||||
|
||||
|
@ -861,7 +868,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# TODO: load from config
|
||||
use_open_gl: bool = False,
|
||||
|
||||
static_yrange: Optional[tuple[float, float]] = None,
|
||||
static_yrange: tuple[float, float] | None = None,
|
||||
|
||||
parent=None,
|
||||
**kwargs,
|
||||
|
@ -876,7 +883,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
|
||||
# NOTE: must be set bfore calling ``.mk_vb()``
|
||||
self.linked = linkedsplits
|
||||
self.sidepane: Optional[FieldsForm] = None
|
||||
self.sidepane: FieldsForm | None = None
|
||||
|
||||
# source of our custom interactions
|
||||
self.cv = self.mk_vb(name)
|
||||
|
@ -1010,36 +1017,10 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# )
|
||||
return line_end, marker_right, r_axis_x
|
||||
|
||||
def default_view(
|
||||
self,
|
||||
bars_from_y: int = int(616 * 3/8),
|
||||
y_offset: int = 0,
|
||||
do_ds: bool = True,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Set the view box to the "default" startup view of the scene.
|
||||
|
||||
'''
|
||||
viz = self.get_viz(self.name)
|
||||
|
||||
if not viz:
|
||||
log.warning(f'`Viz` for {self.name} not loaded yet?')
|
||||
return
|
||||
|
||||
viz.default_view(
|
||||
bars_from_y,
|
||||
y_offset,
|
||||
do_ds,
|
||||
)
|
||||
|
||||
if do_ds:
|
||||
self.linked.graphics_cycle()
|
||||
|
||||
def increment_view(
|
||||
self,
|
||||
datums: int = 1,
|
||||
vb: Optional[ChartView] = None,
|
||||
vb: ChartView | None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -1057,6 +1038,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
# breakpoint()
|
||||
return
|
||||
|
||||
# should trigger broadcast on all overlays right?
|
||||
view.setXRange(
|
||||
min=l + x_shift,
|
||||
max=r + x_shift,
|
||||
|
@ -1069,8 +1051,8 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
def overlay_plotitem(
|
||||
self,
|
||||
name: str,
|
||||
index: Optional[int] = None,
|
||||
axis_title: Optional[str] = None,
|
||||
index: int | None = None,
|
||||
axis_title: str | None = None,
|
||||
axis_side: str = 'right',
|
||||
axis_kwargs: dict = {},
|
||||
|
||||
|
@ -1119,6 +1101,15 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
link_axes=(0,),
|
||||
)
|
||||
|
||||
# hide all axes not named by ``axis_side``
|
||||
for axname in (
|
||||
({'bottom'} | allowed_sides) - {axis_side}
|
||||
):
|
||||
try:
|
||||
pi.hideAxis(axname)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# add axis title
|
||||
# TODO: do we want this API to still work?
|
||||
# raxis = pi.getAxis('right')
|
||||
|
@ -1134,11 +1125,11 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
shm: ShmArray,
|
||||
flume: Flume,
|
||||
|
||||
array_key: Optional[str] = None,
|
||||
array_key: str | None = None,
|
||||
overlay: bool = False,
|
||||
color: Optional[str] = None,
|
||||
color: str | None = None,
|
||||
add_label: bool = True,
|
||||
pi: Optional[pg.PlotItem] = None,
|
||||
pi: pg.PlotItem | None = None,
|
||||
step_mode: bool = False,
|
||||
is_ohlc: bool = False,
|
||||
add_sticky: None | str = 'right',
|
||||
|
@ -1197,6 +1188,10 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
)
|
||||
|
||||
pi.viz = viz
|
||||
# so that viewboxes are associated 1-to-1 with
|
||||
# their parent plotitem
|
||||
pi.vb._viz = viz
|
||||
|
||||
assert isinstance(viz.shm, ShmArray)
|
||||
|
||||
# TODO: this probably needs its own method?
|
||||
|
@ -1209,17 +1204,21 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
pi = overlay
|
||||
|
||||
if add_sticky:
|
||||
axis = pi.getAxis(add_sticky)
|
||||
if pi.name not in axis._stickies:
|
||||
|
||||
if pi is not self.plotItem:
|
||||
overlay = self.pi_overlay
|
||||
assert pi in overlay.overlays
|
||||
overlay_axis = overlay.get_axis(
|
||||
pi,
|
||||
add_sticky,
|
||||
)
|
||||
assert overlay_axis is axis
|
||||
if pi is not self.plotItem:
|
||||
# overlay = self.pi_overlay
|
||||
# assert pi in overlay.overlays
|
||||
overlay = self.pi_overlay
|
||||
assert pi in overlay.overlays
|
||||
axis = overlay.get_axis(
|
||||
pi,
|
||||
add_sticky,
|
||||
)
|
||||
|
||||
else:
|
||||
axis = pi.getAxis(add_sticky)
|
||||
|
||||
if pi.name not in axis._stickies:
|
||||
|
||||
# TODO: UGH! just make this not here! we should
|
||||
# be making the sticky from code which has access
|
||||
|
@ -1263,7 +1262,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
shm: ShmArray,
|
||||
flume: Flume,
|
||||
|
||||
array_key: Optional[str] = None,
|
||||
array_key: str | None = None,
|
||||
**draw_curve_kwargs,
|
||||
|
||||
) -> Viz:
|
||||
|
@ -1280,24 +1279,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
**draw_curve_kwargs,
|
||||
)
|
||||
|
||||
def update_graphics_from_flow(
|
||||
self,
|
||||
graphics_name: str,
|
||||
array_key: Optional[str] = None,
|
||||
|
||||
**kwargs,
|
||||
|
||||
) -> pg.GraphicsObject:
|
||||
'''
|
||||
Update the named internal graphics from ``array``.
|
||||
|
||||
'''
|
||||
viz = self._vizs[array_key or graphics_name]
|
||||
return viz.update_graphics(
|
||||
array_key=array_key,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
# TODO: pretty sure we can just call the cursor
|
||||
# directly not? i don't wee why we need special "signal proxies"
|
||||
# for this lul..
|
||||
|
@ -1310,43 +1291,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
self.sig_mouse_leave.emit(self)
|
||||
self.scene().leaveEvent(ev)
|
||||
|
||||
def maxmin(
|
||||
self,
|
||||
name: Optional[str] = None,
|
||||
bars_range: Optional[tuple[
|
||||
int, int, int, int, int, int
|
||||
]] = None,
|
||||
|
||||
) -> tuple[float, float]:
|
||||
'''
|
||||
Return the max and min y-data values "in view".
|
||||
|
||||
If ``bars_range`` is provided use that range.
|
||||
|
||||
'''
|
||||
# TODO: here we should instead look up the ``Viz.shm.array``
|
||||
# and read directly from shm to avoid copying to memory first
|
||||
# and then reading it again here.
|
||||
viz_key = name or self.name
|
||||
viz = self._vizs.get(viz_key)
|
||||
if viz is None:
|
||||
log.error(f"viz {viz_key} doesn't exist in chart {self.name} !?")
|
||||
return 0, 0
|
||||
|
||||
res = viz.maxmin()
|
||||
|
||||
if (
|
||||
res is None
|
||||
):
|
||||
mxmn = 0, 0
|
||||
if not self._on_screen:
|
||||
self.default_view(do_ds=False)
|
||||
self._on_screen = True
|
||||
else:
|
||||
x_range, read_slc, mxmn = res
|
||||
|
||||
return mxmn
|
||||
|
||||
def get_viz(
|
||||
self,
|
||||
key: str,
|
||||
|
@ -1360,3 +1304,32 @@ class ChartPlotWidget(pg.PlotWidget):
|
|||
@property
|
||||
def main_viz(self) -> Viz:
|
||||
return self.get_viz(self.name)
|
||||
|
||||
def iter_vizs(self) -> Iterator[Viz]:
|
||||
return iter(self._vizs.values())
|
||||
|
||||
@cm
|
||||
def reset_graphics_caches(self) -> None:
|
||||
'''
|
||||
Reset all managed ``Viz`` (flow) graphics objects
|
||||
Qt cache modes (to ``NoCache`` mode) on enter and
|
||||
restore on exit.
|
||||
|
||||
'''
|
||||
with ExitStack() as stack:
|
||||
for viz in self.iter_vizs():
|
||||
stack.enter_context(
|
||||
viz.graphics.reset_cache(),
|
||||
)
|
||||
|
||||
# also reset any downsampled alt-graphics objects which
|
||||
# might be active.
|
||||
dsg = viz.ds_graphics
|
||||
if dsg:
|
||||
stack.enter_context(
|
||||
dsg.reset_cache(),
|
||||
)
|
||||
try:
|
||||
yield
|
||||
finally:
|
||||
stack.close()
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# piker: trading gear for hackers
|
||||
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
||||
# 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
|
||||
|
@ -21,7 +21,6 @@ Mouse interaction graphics
|
|||
from __future__ import annotations
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Optional,
|
||||
Callable,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
@ -38,7 +37,10 @@ from ._style import (
|
|||
_font_small,
|
||||
_font,
|
||||
)
|
||||
from ._axes import YAxisLabel, XAxisLabel
|
||||
from ._axes import (
|
||||
YAxisLabel,
|
||||
XAxisLabel,
|
||||
)
|
||||
from ..log import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
|
@ -167,7 +169,7 @@ class ContentsLabel(pg.LabelItem):
|
|||
|
||||
anchor_at: str = ('top', 'right'),
|
||||
justify_text: str = 'left',
|
||||
font_size: Optional[int] = None,
|
||||
font_size: int | None = None,
|
||||
|
||||
) -> None:
|
||||
|
||||
|
@ -338,7 +340,7 @@ class Cursor(pg.GraphicsObject):
|
|||
|
||||
self.linked = linkedsplits
|
||||
self.graphics: dict[str, pg.GraphicsObject] = {}
|
||||
self.xaxis_label: Optional[XAxisLabel] = None
|
||||
self.xaxis_label: XAxisLabel | None = None
|
||||
self.always_show_xlabel: bool = True
|
||||
self.plots: list['PlotChartWidget'] = [] # type: ignore # noqa
|
||||
self.active_plot = None
|
||||
|
|
|
@ -19,7 +19,7 @@ Fast, smooth, sexy curves.
|
|||
|
||||
"""
|
||||
from contextlib import contextmanager as cm
|
||||
from typing import Optional, Callable
|
||||
from typing import Callable
|
||||
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
|
@ -86,7 +86,7 @@ class FlowGraphic(pg.GraphicsObject):
|
|||
# line styling
|
||||
color: str = 'bracket',
|
||||
last_step_color: str | None = None,
|
||||
fill_color: Optional[str] = None,
|
||||
fill_color: str | None = None,
|
||||
style: str = 'solid',
|
||||
|
||||
**kwargs
|
||||
|
@ -158,14 +158,37 @@ class FlowGraphic(pg.GraphicsObject):
|
|||
drawn yet, ``None``.
|
||||
|
||||
'''
|
||||
return self._last_line.x1() if self._last_line else None
|
||||
if self._last_line:
|
||||
return self._last_line.x1()
|
||||
|
||||
return None
|
||||
|
||||
# XXX: due to a variety of weird jitter bugs and "smearing"
|
||||
# artifacts when click-drag panning and viewing history time series,
|
||||
# we offer this ctx-mngr interface to allow temporarily disabling
|
||||
# Qt's graphics caching mode; this is now currently used from
|
||||
# ``ChartView.start/signal_ic()`` methods which also disable the
|
||||
# rt-display loop when the user is moving around a view.
|
||||
@cm
|
||||
def reset_cache(self) -> None:
|
||||
try:
|
||||
none = QGraphicsItem.NoCache
|
||||
log.debug(
|
||||
f'{self._name} -> CACHE DISABLE: {none}'
|
||||
)
|
||||
self.setCacheMode(none)
|
||||
yield
|
||||
finally:
|
||||
mode = self.cache_mode
|
||||
log.debug(f'{self._name} -> CACHE ENABLE {mode}')
|
||||
self.setCacheMode(mode)
|
||||
|
||||
|
||||
class Curve(FlowGraphic):
|
||||
'''
|
||||
A faster, simpler, append friendly version of
|
||||
``pyqtgraph.PlotCurveItem`` built for highly customizable real-time
|
||||
updates.
|
||||
updates; a graphics object to render a simple "line" plot.
|
||||
|
||||
This type is a much stripped down version of a ``pyqtgraph`` style
|
||||
"graphics object" in the sense that the internal lower level
|
||||
|
@ -191,14 +214,14 @@ class Curve(FlowGraphic):
|
|||
|
||||
'''
|
||||
# TODO: can we remove this?
|
||||
# sub_br: Optional[Callable] = None
|
||||
# sub_br: Callable | None = None
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
|
||||
# color: str = 'default_lightest',
|
||||
# fill_color: Optional[str] = None,
|
||||
# fill_color: str | None = None,
|
||||
# style: str = 'solid',
|
||||
|
||||
**kwargs
|
||||
|
@ -248,12 +271,6 @@ class Curve(FlowGraphic):
|
|||
self.fast_path.clear()
|
||||
# self.fast_path = None
|
||||
|
||||
@cm
|
||||
def reset_cache(self) -> None:
|
||||
self.setCacheMode(QtWidgets.QGraphicsItem.NoCache)
|
||||
yield
|
||||
self.setCacheMode(QGraphicsItem.DeviceCoordinateCache)
|
||||
|
||||
def boundingRect(self):
|
||||
'''
|
||||
Compute and then cache our rect.
|
||||
|
@ -378,7 +395,6 @@ class Curve(FlowGraphic):
|
|||
|
||||
) -> None:
|
||||
# default line draw last call
|
||||
# with self.reset_cache():
|
||||
x = src_data[index_field]
|
||||
y = src_data[array_key]
|
||||
|
||||
|
@ -406,10 +422,20 @@ class Curve(FlowGraphic):
|
|||
# element such that the current datum in view can be shown
|
||||
# (via it's max / min) even when highly zoomed out.
|
||||
class FlattenedOHLC(Curve):
|
||||
'''
|
||||
More or less the exact same as a standard line ``Curve`` above
|
||||
but meant to handle a traced-and-downsampled OHLC time series.
|
||||
_
|
||||
_| | _
|
||||
|_ | |_ | |
|
||||
_| => |_| |
|
||||
| |
|
||||
|_ |_
|
||||
|
||||
# avoids strange dragging/smearing artifacts when panning..
|
||||
cache_mode: int = QGraphicsItem.NoCache
|
||||
The main implementation different is that ``.draw_last_datum()``
|
||||
expects an underlying OHLC array for the ``src_data`` input.
|
||||
|
||||
'''
|
||||
def draw_last_datum(
|
||||
self,
|
||||
path: QPainterPath,
|
||||
|
@ -434,7 +460,19 @@ class FlattenedOHLC(Curve):
|
|||
|
||||
|
||||
class StepCurve(Curve):
|
||||
'''
|
||||
A familiar rectangle-with-y-height-per-datum type curve:
|
||||
|
||||
||
|
||||
|| ||
|
||||
|| || ||||
|
||||
_||_||_||_||||_ where each datum's y-value is drawn as
|
||||
a nearly full rectangle, each "level" spans some x-step size.
|
||||
|
||||
This is most often used for vlm and option OI style curves and/or
|
||||
the very popular "bar chart".
|
||||
|
||||
'''
|
||||
def declare_paintables(
|
||||
self,
|
||||
) -> None:
|
||||
|
|
|
@ -19,17 +19,20 @@ Data vizualization APIs
|
|||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from functools import lru_cache
|
||||
from math import (
|
||||
ceil,
|
||||
floor,
|
||||
)
|
||||
from typing import (
|
||||
Optional,
|
||||
Literal,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import msgspec
|
||||
from msgspec import (
|
||||
Struct,
|
||||
field,
|
||||
)
|
||||
import numpy as np
|
||||
import pyqtgraph as pg
|
||||
from PyQt5.QtCore import QLineF
|
||||
|
@ -225,15 +228,51 @@ def render_baritems(
|
|||
_sample_rates: set[float] = {1, 60}
|
||||
|
||||
|
||||
class Viz(msgspec.Struct): # , frozen=True):
|
||||
class ViewState(Struct):
|
||||
'''
|
||||
Indexing objects representing the current view x-range -> y-range.
|
||||
|
||||
'''
|
||||
# (xl, xr) "input" view range in x-domain
|
||||
xrange: tuple[
|
||||
float | int,
|
||||
float | int
|
||||
] | None = None
|
||||
|
||||
# TODO: cache the (ixl, ixr) read_slc-into-.array style slice index?
|
||||
|
||||
# (ymn, ymx) "output" min and max in viewed y-codomain
|
||||
yrange: tuple[
|
||||
float | int,
|
||||
float | int
|
||||
] | None = None
|
||||
|
||||
# last in view ``ShmArray.array[read_slc]`` data
|
||||
in_view: np.ndarray | None = None
|
||||
|
||||
|
||||
class Viz(Struct):
|
||||
'''
|
||||
(Data) "Visualization" compound type which wraps a real-time
|
||||
shm array stream with displayed graphics (curves, charts)
|
||||
for high level access and control as well as efficient incremental
|
||||
update.
|
||||
update, oriented around the idea of a "view state".
|
||||
|
||||
The intention is for this type to eventually be capable of shm-passing
|
||||
of incrementally updated graphics stream data between actors.
|
||||
The (backend) intention is for this interface and type is to
|
||||
eventually be capable of shm-passing of incrementally updated
|
||||
graphics stream data, thus providing a cross-actor solution to
|
||||
sharing UI-related update state potentionally in a (compressed)
|
||||
binary-interchange format.
|
||||
|
||||
Further, from an interaction-triggers-view-in-UI perspective, this type
|
||||
operates as a transform:
|
||||
(x_left, x_right) -> output metrics {ymn, ymx, uppx, ...}
|
||||
|
||||
wherein each x-domain range maps to some output set of (graphics
|
||||
related) vizualization metrics. In further documentation we often
|
||||
refer to this abstraction as a vizualization curve: Ci. Each Ci is
|
||||
considered a function which maps an x-range (input view range) to
|
||||
a multi-variate (metrics) output.
|
||||
|
||||
'''
|
||||
name: str
|
||||
|
@ -242,13 +281,17 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
flume: Flume
|
||||
graphics: Curve | BarItems
|
||||
|
||||
# for tracking y-mn/mx for y-axis auto-ranging
|
||||
yrange: tuple[float, float] = None
|
||||
vs: ViewState = field(default_factory=ViewState)
|
||||
|
||||
# last calculated y-mn/mx from m4 downsample code, this
|
||||
# is updated in the body of `Renderer.render()`.
|
||||
ds_yrange: tuple[float, float] | None = None
|
||||
yrange: tuple[float, float] | None = None
|
||||
|
||||
# in some cases a viz may want to change its
|
||||
# graphical "type" or, "form" when downsampling, to
|
||||
# start this is only ever an interpolation line.
|
||||
ds_graphics: Optional[Curve] = None
|
||||
ds_graphics: Curve | None = None
|
||||
|
||||
is_ohlc: bool = False
|
||||
render: bool = True # toggle for display loop
|
||||
|
@ -264,7 +307,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
|
||||
] = 'time'
|
||||
|
||||
# downsampling state
|
||||
# TODO: maybe compound this into a downsampling state type?
|
||||
_last_uppx: float = 0
|
||||
_in_ds: bool = False
|
||||
_index_step: float | None = None
|
||||
|
@ -282,20 +325,44 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
tuple[float, float],
|
||||
] = {}
|
||||
|
||||
# cache of median calcs from input read slice hashes
|
||||
# see `.median()`
|
||||
_meds: dict[
|
||||
int,
|
||||
float,
|
||||
] = {}
|
||||
|
||||
# to make lru_cache-ing work, see
|
||||
# https://docs.python.org/3/faq/programming.html#how-do-i-cache-method-calls
|
||||
def __eq__(self, other):
|
||||
return self._shm._token == other._shm._token
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self._shm._token)
|
||||
|
||||
@property
|
||||
def shm(self) -> ShmArray:
|
||||
return self._shm
|
||||
|
||||
@property
|
||||
def index_field(self) -> str:
|
||||
'''
|
||||
The column name as ``str`` in the underlying ``._shm: ShmArray``
|
||||
which will deliver the "index" array.
|
||||
|
||||
'''
|
||||
return self._index_field
|
||||
|
||||
def index_step(
|
||||
self,
|
||||
reset: bool = False,
|
||||
|
||||
) -> float:
|
||||
'''
|
||||
Return the size between sample steps in the units of the
|
||||
x-domain, normally either an ``int`` array index size or an
|
||||
epoch time in seconds.
|
||||
|
||||
'''
|
||||
# attempt to dectect the best step size by scanning a sample of
|
||||
# the source data.
|
||||
if self._index_step is None:
|
||||
|
@ -378,7 +445,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
|
||||
# TODO: hash the slice instead maybe?
|
||||
# https://stackoverflow.com/a/29980872
|
||||
lbar, rbar = ixrng = round(x_range[0]), round(x_range[1])
|
||||
ixrng = lbar, rbar = round(x_range[0]), round(x_range[1])
|
||||
|
||||
if use_caching:
|
||||
cached_result = self._mxmns.get(ixrng)
|
||||
|
@ -389,6 +456,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
f'{ixrng} -> {cached_result}'
|
||||
)
|
||||
read_slc, mxmn = cached_result
|
||||
self.vs.yrange = mxmn
|
||||
return (
|
||||
ixrng,
|
||||
read_slc,
|
||||
|
@ -421,8 +489,8 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
)
|
||||
return None
|
||||
|
||||
elif self.yrange:
|
||||
mxmn = self.yrange
|
||||
elif self.ds_yrange:
|
||||
mxmn = self.ds_yrange
|
||||
if do_print:
|
||||
print(
|
||||
f'{self.name} M4 maxmin:\n'
|
||||
|
@ -455,6 +523,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
# cache result for input range
|
||||
assert mxmn
|
||||
self._mxmns[ixrng] = (read_slc, mxmn)
|
||||
self.vs.yrange = mxmn
|
||||
profiler(f'yrange mxmn cacheing: {x_range} -> {mxmn}')
|
||||
return (
|
||||
ixrng,
|
||||
|
@ -473,20 +542,11 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
vr.right(),
|
||||
)
|
||||
|
||||
def bars_range(self) -> tuple[int, int, int, int]:
|
||||
'''
|
||||
Return a range tuple for the left-view, left-datum, right-datum
|
||||
and right-view x-indices.
|
||||
|
||||
'''
|
||||
l, start, datum_start, datum_stop, stop, r = self.datums_range()
|
||||
return l, datum_start, datum_stop, r
|
||||
|
||||
def datums_range(
|
||||
self,
|
||||
view_range: None | tuple[float, float] = None,
|
||||
index_field: str | None = None,
|
||||
array: None | np.ndarray = None,
|
||||
array: np.ndarray | None = None,
|
||||
|
||||
) -> tuple[
|
||||
int, int, int, int, int, int
|
||||
|
@ -499,42 +559,47 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
|
||||
index_field: str = index_field or self.index_field
|
||||
if index_field == 'index':
|
||||
l, r = round(l), round(r)
|
||||
l: int = round(l)
|
||||
r: int = round(r)
|
||||
|
||||
if array is None:
|
||||
array = self.shm.array
|
||||
|
||||
index = array[index_field]
|
||||
first = floor(index[0])
|
||||
last = ceil(index[-1])
|
||||
|
||||
# first and last datums in view determined by
|
||||
# l / r view range.
|
||||
leftmost = floor(l)
|
||||
rightmost = ceil(r)
|
||||
first: int = floor(index[0])
|
||||
last: int = ceil(index[-1])
|
||||
|
||||
# invalid view state
|
||||
if (
|
||||
r < l
|
||||
or l < 0
|
||||
or r < 0
|
||||
or (l > last and r > last)
|
||||
or (
|
||||
l > last
|
||||
and r > last
|
||||
)
|
||||
):
|
||||
leftmost = first
|
||||
rightmost = last
|
||||
leftmost: int = first
|
||||
rightmost: int = last
|
||||
|
||||
else:
|
||||
# determine first and last datums in view determined by
|
||||
# l -> r view range.
|
||||
rightmost = max(
|
||||
min(last, rightmost),
|
||||
min(last, ceil(r)),
|
||||
first,
|
||||
)
|
||||
|
||||
leftmost = min(
|
||||
max(first, leftmost),
|
||||
max(first, floor(l)),
|
||||
last,
|
||||
rightmost - 1,
|
||||
)
|
||||
|
||||
assert leftmost < rightmost
|
||||
# sanity
|
||||
# assert leftmost < rightmost
|
||||
|
||||
self.vs.xrange = leftmost, rightmost
|
||||
|
||||
return (
|
||||
l, # left x-in-view
|
||||
|
@ -547,7 +612,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
|
||||
def read(
|
||||
self,
|
||||
array_field: Optional[str] = None,
|
||||
array_field: str | None = None,
|
||||
index_field: str | None = None,
|
||||
profiler: None | Profiler = None,
|
||||
|
||||
|
@ -563,11 +628,9 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
|
||||
'''
|
||||
index_field: str = index_field or self.index_field
|
||||
vr = l, r = self.view_range()
|
||||
|
||||
# readable data
|
||||
array = self.shm.array
|
||||
|
||||
if profiler:
|
||||
profiler('self.shm.array READ')
|
||||
|
||||
|
@ -579,7 +642,6 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
ilast,
|
||||
r,
|
||||
) = self.datums_range(
|
||||
view_range=vr,
|
||||
index_field=index_field,
|
||||
array=array,
|
||||
)
|
||||
|
@ -595,17 +657,21 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
array,
|
||||
start_t=lbar,
|
||||
stop_t=rbar,
|
||||
step=self.index_step(),
|
||||
)
|
||||
|
||||
# TODO: maybe we should return this from the slicer call
|
||||
# above?
|
||||
in_view = array[read_slc]
|
||||
if in_view.size:
|
||||
self.vs.in_view = in_view
|
||||
abs_indx = in_view['index']
|
||||
abs_slc = slice(
|
||||
int(abs_indx[0]),
|
||||
int(abs_indx[-1]),
|
||||
)
|
||||
else:
|
||||
self.vs.in_view = None
|
||||
|
||||
if profiler:
|
||||
profiler(
|
||||
|
@ -626,10 +692,11 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
# BUT the ``in_view`` slice DOES..
|
||||
read_slc = slice(lbar_i, rbar_i)
|
||||
in_view = array[lbar_i: rbar_i + 1]
|
||||
self.vs.in_view = in_view
|
||||
# in_view = array[lbar_i-1: rbar_i+1]
|
||||
|
||||
# XXX: same as ^
|
||||
# to_draw = array[lbar - ifirst:(rbar - ifirst) + 1]
|
||||
|
||||
if profiler:
|
||||
profiler('index arithmetic for slicing')
|
||||
|
||||
|
@ -664,8 +731,8 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
pg.GraphicsObject,
|
||||
]:
|
||||
'''
|
||||
Read latest datums from shm and render to (incrementally)
|
||||
render to graphics.
|
||||
Read latest datums from shm and (incrementally) render to
|
||||
graphics.
|
||||
|
||||
'''
|
||||
profiler = Profiler(
|
||||
|
@ -955,9 +1022,11 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
|
||||
def default_view(
|
||||
self,
|
||||
bars_from_y: int = int(616 * 3/8),
|
||||
min_bars_from_y: int = int(616 * 4/11),
|
||||
y_offset: int = 0, # in datums
|
||||
|
||||
do_ds: bool = True,
|
||||
do_min_bars: bool = False,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -1013,12 +1082,10 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
data_diff = last_datum - first_datum
|
||||
rl_diff = vr - vl
|
||||
rescale_to_data: bool = False
|
||||
# new_uppx: float = 1
|
||||
|
||||
if rl_diff > data_diff:
|
||||
rescale_to_data = True
|
||||
rl_diff = data_diff
|
||||
new_uppx: float = data_diff / self.px_width()
|
||||
|
||||
# orient by offset from the y-axis including
|
||||
# space to compensate for the L1 labels.
|
||||
|
@ -1027,17 +1094,29 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
|
||||
offset = l1_offset
|
||||
|
||||
if (
|
||||
rescale_to_data
|
||||
):
|
||||
if rescale_to_data:
|
||||
new_uppx: float = data_diff / self.px_width()
|
||||
offset = (offset / uppx) * new_uppx
|
||||
|
||||
else:
|
||||
offset = (y_offset * step) + uppx*step
|
||||
|
||||
# NOTE: if we are in the midst of start-up and a bunch of
|
||||
# widgets are spawning/rendering concurrently, it's likely the
|
||||
# label size above `l1_offset` won't have yet fully rendered.
|
||||
# Here we try to compensate for that ensure at least a static
|
||||
# bar gap between the last datum and the y-axis.
|
||||
if (
|
||||
do_min_bars
|
||||
and offset <= (6 * step)
|
||||
):
|
||||
offset = 6 * step
|
||||
|
||||
# align right side of view to the rightmost datum + the selected
|
||||
# offset from above.
|
||||
r_reset = (self.graphics.x_last() or last_datum) + offset
|
||||
r_reset = (
|
||||
self.graphics.x_last() or last_datum
|
||||
) + offset
|
||||
|
||||
# no data is in view so check for the only 2 sane cases:
|
||||
# - entire view is LEFT of data
|
||||
|
@ -1062,12 +1141,20 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
else:
|
||||
log.warning(f'Unknown view state {vl} -> {vr}')
|
||||
return
|
||||
# raise RuntimeError(f'Unknown view state {vl} -> {vr}')
|
||||
|
||||
else:
|
||||
# maintain the l->r view distance
|
||||
l_reset = r_reset - rl_diff
|
||||
|
||||
if (
|
||||
do_min_bars
|
||||
and (r_reset - l_reset) < min_bars_from_y
|
||||
):
|
||||
l_reset = (
|
||||
(r_reset + offset)
|
||||
-
|
||||
min_bars_from_y * step
|
||||
)
|
||||
|
||||
# remove any custom user yrange setttings
|
||||
if chartw._static_yrange == 'axis':
|
||||
chartw._static_yrange = None
|
||||
|
@ -1079,9 +1166,7 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
)
|
||||
|
||||
if do_ds:
|
||||
# view.interaction_graphics_cycle()
|
||||
view.maybe_downsample_graphics()
|
||||
view._set_yrange(viz=self)
|
||||
view.interact_graphics_cycle()
|
||||
|
||||
def incr_info(
|
||||
self,
|
||||
|
@ -1236,3 +1321,152 @@ class Viz(msgspec.Struct): # , frozen=True):
|
|||
vr, 0,
|
||||
)
|
||||
).length()
|
||||
|
||||
@lru_cache(maxsize=6116)
|
||||
def median_from_range(
|
||||
self,
|
||||
start: int,
|
||||
stop: int,
|
||||
|
||||
) -> float:
|
||||
in_view = self.shm.array[start:stop]
|
||||
if self.is_ohlc:
|
||||
return np.median(in_view['close'])
|
||||
else:
|
||||
return np.median(in_view[self.name])
|
||||
|
||||
@lru_cache(maxsize=6116)
|
||||
def _dispersion(
|
||||
self,
|
||||
# xrange: tuple[float, float],
|
||||
ymn: float,
|
||||
ymx: float,
|
||||
yref: float,
|
||||
|
||||
) -> tuple[float, float]:
|
||||
return (
|
||||
(ymx - yref) / yref,
|
||||
(ymn - yref) / yref,
|
||||
)
|
||||
|
||||
def disp_from_range(
|
||||
self,
|
||||
xrange: tuple[float, float] | None = None,
|
||||
yref: float | None = None,
|
||||
method: Literal[
|
||||
'up',
|
||||
'down',
|
||||
'full', # both sides
|
||||
'both', # both up and down as separate scalars
|
||||
|
||||
] = 'full',
|
||||
|
||||
) -> float | tuple[float, float] | None:
|
||||
'''
|
||||
Return a dispersion metric referenced from an optionally
|
||||
provided ``yref`` or the left-most datum level by default.
|
||||
|
||||
'''
|
||||
vs = self.vs
|
||||
yrange = vs.yrange
|
||||
if yrange is None:
|
||||
return None
|
||||
|
||||
ymn, ymx = yrange
|
||||
key = 'open' if self.is_ohlc else self.name
|
||||
yref = yref or vs.in_view[0][key]
|
||||
# xrange = xrange or vs.xrange
|
||||
|
||||
# call into the lru_cache-d sigma calculator method
|
||||
r_up, r_down = self._dispersion(ymn, ymx, yref)
|
||||
match method:
|
||||
case 'full':
|
||||
return r_up - r_down
|
||||
case 'up':
|
||||
return r_up
|
||||
case 'down':
|
||||
return r_up
|
||||
case 'both':
|
||||
return r_up, r_down
|
||||
|
||||
# @lru_cache(maxsize=6116)
|
||||
def i_from_t(
|
||||
self,
|
||||
t: float,
|
||||
return_y: bool = False,
|
||||
|
||||
) -> int | tuple[int, float]:
|
||||
|
||||
istart = slice_from_time(
|
||||
self.vs.in_view,
|
||||
start_t=t,
|
||||
stop_t=t,
|
||||
step=self.index_step(),
|
||||
).start
|
||||
|
||||
if not return_y:
|
||||
return istart
|
||||
|
||||
vs = self.vs
|
||||
arr = vs.in_view
|
||||
key = 'open' if self.is_ohlc else self.name
|
||||
yref = arr[istart][key]
|
||||
return istart, yref
|
||||
|
||||
def scalars_from_index(
|
||||
self,
|
||||
xref: float | None = None,
|
||||
|
||||
) -> tuple[
|
||||
int,
|
||||
float,
|
||||
float,
|
||||
float,
|
||||
] | None:
|
||||
'''
|
||||
Calculate and deliver the log-returns scalars specifically
|
||||
according to y-data supported on this ``Viz``'s underlying
|
||||
x-domain data range from ``xref`` -> ``.vs.xrange[1]``.
|
||||
|
||||
The main use case for this method (currently) is to generate
|
||||
scalars which will allow calculating the required y-range for
|
||||
some "pinned" curve to be aligned *from* the ``xref`` time
|
||||
stamped datum *to* the curve rendered by THIS viz.
|
||||
|
||||
'''
|
||||
vs = self.vs
|
||||
arr = vs.in_view
|
||||
|
||||
# TODO: make this work by parametrizing over input
|
||||
# .vs.xrange input for caching?
|
||||
# read_slc_start = self.i_from_t(xref)
|
||||
|
||||
read_slc = slice_from_time(
|
||||
arr=self.vs.in_view,
|
||||
start_t=xref,
|
||||
stop_t=vs.xrange[1],
|
||||
step=self.index_step(),
|
||||
)
|
||||
key = 'open' if self.is_ohlc else self.name
|
||||
|
||||
# NOTE: old code, it's no faster right?
|
||||
# read_slc_start = read_slc.start
|
||||
# yref = arr[read_slc_start][key]
|
||||
|
||||
read = arr[read_slc][key]
|
||||
if not read.size:
|
||||
return None
|
||||
|
||||
yref = read[0]
|
||||
ymn, ymx = self.vs.yrange
|
||||
# print(
|
||||
# f'Viz[{self.name}].scalars_from_index(xref={xref})\n'
|
||||
# f'read_slc: {read_slc}\n'
|
||||
# f'ymnmx: {(ymn, ymx)}\n'
|
||||
# )
|
||||
return (
|
||||
read_slc.start,
|
||||
yref,
|
||||
(ymx - yref) / yref,
|
||||
(ymn - yref) / yref,
|
||||
)
|
||||
|
|
|
@ -21,18 +21,18 @@ this module ties together quote and computational (fsp) streams with
|
|||
graphics update methods via our custom ``pyqtgraph`` charting api.
|
||||
|
||||
'''
|
||||
from functools import partial
|
||||
import itertools
|
||||
from math import floor
|
||||
import time
|
||||
from typing import (
|
||||
Optional,
|
||||
Any,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import tractor
|
||||
import trio
|
||||
import pyqtgraph as pg
|
||||
# import pendulum
|
||||
|
||||
from msgspec import field
|
||||
|
||||
|
@ -82,6 +82,9 @@ from .._profile import (
|
|||
from ..log import get_logger
|
||||
from .._profile import Profiler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._interaction import ChartView
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
|
@ -146,12 +149,11 @@ def multi_maxmin(
|
|||
profiler(f'vlm_viz.maxmin({read_slc})')
|
||||
|
||||
return (
|
||||
mx,
|
||||
|
||||
# enforcing price can't be negative?
|
||||
# TODO: do we even need this?
|
||||
max(mn, 0),
|
||||
|
||||
mx,
|
||||
mx_vlm_in_view, # vlm max
|
||||
)
|
||||
|
||||
|
@ -183,29 +185,23 @@ class DisplayState(Struct):
|
|||
# misc state tracking
|
||||
vars: dict[str, Any] = field(
|
||||
default_factory=lambda: {
|
||||
'tick_margin': 0,
|
||||
'i_last': 0,
|
||||
'i_last_append': 0,
|
||||
'last_mx_vlm': 0,
|
||||
'last_mx': 0,
|
||||
'last_mn': 0,
|
||||
}
|
||||
)
|
||||
hist_vars: dict[str, Any] = field(
|
||||
default_factory=lambda: {
|
||||
'tick_margin': 0,
|
||||
'i_last': 0,
|
||||
'i_last_append': 0,
|
||||
'last_mx_vlm': 0,
|
||||
'last_mx': 0,
|
||||
'last_mn': 0,
|
||||
}
|
||||
)
|
||||
|
||||
globalz: None | dict[str, Any] = None
|
||||
|
||||
vlm_chart: Optional[ChartPlotWidget] = None
|
||||
vlm_sticky: Optional[YAxisLabel] = None
|
||||
vlm_chart: ChartPlotWidget | None = None
|
||||
vlm_sticky: YAxisLabel | None = None
|
||||
wap_in_history: bool = False
|
||||
|
||||
|
||||
|
@ -261,7 +257,10 @@ async def increment_history_view(
|
|||
profiler('`hist Viz.update_graphics()` call')
|
||||
|
||||
if liv:
|
||||
hist_viz.plot.vb._set_yrange(viz=hist_viz)
|
||||
hist_viz.plot.vb.interact_graphics_cycle(
|
||||
do_linked_charts=False,
|
||||
do_overlay_scaling=True, # always overlayT slow chart
|
||||
)
|
||||
profiler('hist chart yrange view')
|
||||
|
||||
# check if tread-in-place view x-shift is needed
|
||||
|
@ -351,8 +350,8 @@ async def graphics_update_loop(
|
|||
vlm_viz = vlm_chart._vizs.get('volume') if vlm_chart else None
|
||||
|
||||
(
|
||||
last_mx,
|
||||
last_mn,
|
||||
last_mx,
|
||||
last_mx_vlm,
|
||||
) = multi_maxmin(
|
||||
None,
|
||||
|
@ -379,9 +378,6 @@ async def graphics_update_loop(
|
|||
# levels this might be dark volume we need to
|
||||
# present differently -> likely dark vlm
|
||||
|
||||
tick_size = symbol.tick_size
|
||||
tick_margin = 3 * tick_size
|
||||
|
||||
fast_chart.show()
|
||||
last_quote_s = time.time()
|
||||
|
||||
|
@ -389,7 +385,6 @@ async def graphics_update_loop(
|
|||
'fqsn': fqsn,
|
||||
'godwidget': godwidget,
|
||||
'quotes': {},
|
||||
# 'maxmin': maxmin,
|
||||
|
||||
'flume': flume,
|
||||
|
||||
|
@ -406,12 +401,11 @@ async def graphics_update_loop(
|
|||
'l1': l1,
|
||||
|
||||
'vars': {
|
||||
'tick_margin': tick_margin,
|
||||
'i_last': 0,
|
||||
'i_last_append': 0,
|
||||
'last_mx_vlm': last_mx_vlm,
|
||||
'last_mx': last_mx,
|
||||
'last_mn': last_mn,
|
||||
# 'last_mx': last_mx,
|
||||
# 'last_mn': last_mn,
|
||||
},
|
||||
'globalz': globalz,
|
||||
})
|
||||
|
@ -422,7 +416,9 @@ async def graphics_update_loop(
|
|||
ds.vlm_chart = vlm_chart
|
||||
ds.vlm_sticky = vlm_sticky
|
||||
|
||||
fast_chart.default_view()
|
||||
fast_chart.main_viz.default_view(
|
||||
do_min_bars=True,
|
||||
)
|
||||
|
||||
# ds.hist_vars.update({
|
||||
# 'i_last_append': 0,
|
||||
|
@ -474,7 +470,7 @@ async def graphics_update_loop(
|
|||
fast_chart.pause_all_feeds()
|
||||
continue
|
||||
|
||||
ic = fast_chart.view._ic
|
||||
ic = fast_chart.view._in_interact
|
||||
if ic:
|
||||
fast_chart.pause_all_feeds()
|
||||
print(f'{fqsn} PAUSING DURING INTERACTION')
|
||||
|
@ -494,7 +490,7 @@ def graphics_update_cycle(
|
|||
|
||||
wap_in_history: bool = False,
|
||||
trigger_all: bool = False, # flag used by prepend history updates
|
||||
prepend_update_index: Optional[int] = None,
|
||||
prepend_update_index: int | None = None,
|
||||
|
||||
) -> None:
|
||||
|
||||
|
@ -517,7 +513,7 @@ def graphics_update_cycle(
|
|||
chart = ds.chart
|
||||
vlm_chart = ds.vlm_chart
|
||||
|
||||
varz = ds.vars
|
||||
# varz = ds.vars
|
||||
l1 = ds.l1
|
||||
flume = ds.flume
|
||||
ohlcv = flume.rt_shm
|
||||
|
@ -527,8 +523,6 @@ def graphics_update_cycle(
|
|||
main_viz = ds.viz
|
||||
index_field = main_viz.index_field
|
||||
|
||||
tick_margin = varz['tick_margin']
|
||||
|
||||
(
|
||||
uppx,
|
||||
liv,
|
||||
|
@ -547,35 +541,37 @@ def graphics_update_cycle(
|
|||
# them as an additional graphic.
|
||||
clear_types = _tick_groups['clears']
|
||||
|
||||
mx = varz['last_mx']
|
||||
mn = varz['last_mn']
|
||||
mx_vlm_in_view = varz['last_mx_vlm']
|
||||
# TODO: fancier y-range sorting..
|
||||
# https://github.com/pikers/piker/issues/325
|
||||
# - a proper streaming mxmn algo as per above issue.
|
||||
# - we should probably scale the view margin based on the size of
|
||||
# the true range? This way you can slap in orders outside the
|
||||
# current L1 (only) book range.
|
||||
main_vb: ChartView = main_viz.plot.vb
|
||||
this_viz: Viz = chart._vizs[fqsn]
|
||||
this_vb: ChartView = this_viz.plot.vb
|
||||
this_yr = this_vb._yrange
|
||||
if this_yr:
|
||||
lmn, lmx = this_yr
|
||||
else:
|
||||
lmn = lmx = 0
|
||||
|
||||
mn: float = lmn
|
||||
mx: float = lmx
|
||||
mx_vlm_in_view: float | None = None
|
||||
yrange_margin = 0.09
|
||||
|
||||
# update ohlc sampled price bars
|
||||
if (
|
||||
# do_rt_update
|
||||
# or do_px_step
|
||||
(liv and do_px_step)
|
||||
or trigger_all
|
||||
):
|
||||
# TODO: i think we're double calling this right now
|
||||
# since .interact_graphics_cycle() also calls it?
|
||||
# I guess we can add a guard in there?
|
||||
_, i_read_range, _ = main_viz.update_graphics()
|
||||
profiler('`Viz.update_graphics()` call')
|
||||
|
||||
(
|
||||
mx_in_view,
|
||||
mn_in_view,
|
||||
mx_vlm_in_view,
|
||||
) = multi_maxmin(
|
||||
i_read_range,
|
||||
main_viz,
|
||||
ds.vlm_viz,
|
||||
profiler,
|
||||
)
|
||||
|
||||
mx = mx_in_view + tick_margin
|
||||
mn = mn_in_view - tick_margin
|
||||
profiler('{fqsdn} `multi_maxmin()` call')
|
||||
|
||||
# don't real-time "shift" the curve to the
|
||||
# left unless we get one of the following:
|
||||
if (
|
||||
|
@ -583,7 +579,6 @@ def graphics_update_cycle(
|
|||
or trigger_all
|
||||
):
|
||||
chart.increment_view(datums=append_diff)
|
||||
# main_viz.plot.vb._set_yrange(viz=main_viz)
|
||||
|
||||
# NOTE: since vlm and ohlc charts are axis linked now we don't
|
||||
# need the double increment request?
|
||||
|
@ -592,6 +587,21 @@ def graphics_update_cycle(
|
|||
|
||||
profiler('view incremented')
|
||||
|
||||
# NOTE: do this **after** the tread to ensure we take the yrange
|
||||
# from the most current view x-domain.
|
||||
(
|
||||
mn,
|
||||
mx,
|
||||
mx_vlm_in_view,
|
||||
) = multi_maxmin(
|
||||
i_read_range,
|
||||
main_viz,
|
||||
ds.vlm_viz,
|
||||
profiler,
|
||||
)
|
||||
|
||||
profiler(f'{fqsn} `multi_maxmin()` call')
|
||||
|
||||
# iterate frames of ticks-by-type such that we only update graphics
|
||||
# using the last update per type where possible.
|
||||
ticks_by_type = quote.get('tbt', {})
|
||||
|
@ -613,8 +623,22 @@ def graphics_update_cycle(
|
|||
# TODO: make sure IB doesn't send ``-1``!
|
||||
and price > 0
|
||||
):
|
||||
mx = max(price + tick_margin, mx)
|
||||
mn = min(price - tick_margin, mn)
|
||||
if (
|
||||
price < mn
|
||||
):
|
||||
mn = price
|
||||
yrange_margin = 0.16
|
||||
# # print(f'{this_viz.name} new MN from TICK {mn}')
|
||||
|
||||
if (
|
||||
price > mx
|
||||
):
|
||||
mx = price
|
||||
yrange_margin = 0.16
|
||||
# # print(f'{this_viz.name} new MX from TICK {mx}')
|
||||
|
||||
# mx = max(price, mx)
|
||||
# mn = min(price, mn)
|
||||
|
||||
# clearing price update:
|
||||
# generally, we only want to update grahpics from the *last*
|
||||
|
@ -677,14 +701,16 @@ def graphics_update_cycle(
|
|||
|
||||
# Y-autoranging: adjust y-axis limits based on state tracking
|
||||
# of previous "last" L1 values which are in view.
|
||||
lmx = varz['last_mx']
|
||||
lmn = varz['last_mn']
|
||||
mx_diff = mx - lmx
|
||||
mn_diff = mn - lmn
|
||||
mx_diff = mx - lmx
|
||||
|
||||
if (
|
||||
mx_diff
|
||||
or mn_diff
|
||||
mn_diff or mx_diff # covers all cases below?
|
||||
# (mx - lmx) > 0 # upward expansion
|
||||
# or (mn - lmn) < 0 # downward expansion
|
||||
|
||||
# or (lmx - mx) > 0 # upward contraction
|
||||
# or (lmn - mn) < 0 # downward contraction
|
||||
):
|
||||
# complain about out-of-range outliers which can show up
|
||||
# in certain annoying feeds (like ib)..
|
||||
|
@ -703,53 +729,77 @@ def graphics_update_cycle(
|
|||
f'mn_diff: {mn_diff}\n'
|
||||
)
|
||||
|
||||
# FAST CHART resize case
|
||||
# TODO: track local liv maxmin without doing a recompute all the
|
||||
# time..plus, just generally the user is more likely to be
|
||||
# zoomed out enough on the slow chart that this is never an
|
||||
# issue (the last datum going out of y-range).
|
||||
|
||||
# FAST CHART y-auto-range resize case
|
||||
elif (
|
||||
liv
|
||||
and not chart._static_yrange == 'axis'
|
||||
):
|
||||
main_vb = main_viz.plot.vb
|
||||
# NOTE: this auto-yranging approach is a sort of, hybrid,
|
||||
# between always aligning overlays to the their common ref
|
||||
# sample and not updating at all:
|
||||
# - whenever an interaction happens the overlays are scaled
|
||||
# to one another and thus are ref-point aligned and
|
||||
# scaled.
|
||||
# - on treads and range updates due to new mn/mx from last
|
||||
# datum, we don't scale to the overlayT instead only
|
||||
# adjusting when the latest datum is outside the previous
|
||||
# dispersion range.
|
||||
mn = min(mn, lmn)
|
||||
mx = max(mx, lmx)
|
||||
|
||||
if (
|
||||
main_vb._ic is None
|
||||
or not main_vb._ic.is_set()
|
||||
main_vb._in_interact is None
|
||||
or not main_vb._in_interact.is_set()
|
||||
):
|
||||
yr = (mn, mx)
|
||||
# print(
|
||||
# f'MAIN VIZ yrange update\n'
|
||||
# f'{fqsn}: {yr}'
|
||||
# )
|
||||
|
||||
main_vb._set_yrange(
|
||||
# TODO: we should probably scale
|
||||
# the view margin based on the size
|
||||
# of the true range? This way you can
|
||||
# slap in orders outside the current
|
||||
# L1 (only) book range.
|
||||
# range_margin=0.1,
|
||||
yrange=yr
|
||||
# print(f'SETTING Y-mnmx -> {main_viz.name}: {(mn, mx)}')
|
||||
this_vb.interact_graphics_cycle(
|
||||
do_linked_charts=False,
|
||||
# TODO: we could optionally offer always doing this
|
||||
# on treads thus always keeping fast-chart overlays
|
||||
# aligned by their LHS datum?
|
||||
do_overlay_scaling=False,
|
||||
yrange_kwargs={
|
||||
this_viz: {
|
||||
'yrange': (mn, mx),
|
||||
'range_margin': yrange_margin,
|
||||
},
|
||||
}
|
||||
)
|
||||
profiler('main vb y-autorange')
|
||||
|
||||
# SLOW CHART resize case
|
||||
(
|
||||
_,
|
||||
hist_liv,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
) = hist_viz.incr_info(
|
||||
ds=ds,
|
||||
is_1m=True,
|
||||
)
|
||||
profiler('hist `Viz.incr_info()`')
|
||||
# SLOW CHART y-auto-range resize casd
|
||||
# (NOTE: still is still inside the y-range
|
||||
# guard block above!)
|
||||
# (
|
||||
# _,
|
||||
# hist_liv,
|
||||
# _,
|
||||
# _,
|
||||
# _,
|
||||
# _,
|
||||
# _,
|
||||
# ) = hist_viz.incr_info(
|
||||
# ds=ds,
|
||||
# is_1m=True,
|
||||
# )
|
||||
|
||||
# if hist_liv:
|
||||
# times = hist_viz.shm.array['time']
|
||||
# last_t = times[-1]
|
||||
# dt = pendulum.from_timestamp(last_t)
|
||||
# log.info(
|
||||
# f'{hist_viz.name} TIMESTEP:'
|
||||
# f'epoch: {last_t}\n'
|
||||
# f'datetime: {dt}\n'
|
||||
# )
|
||||
|
||||
# profiler('hist `Viz.incr_info()`')
|
||||
|
||||
# TODO: track local liv maxmin without doing a recompute all the
|
||||
# time..plut, just generally the user is more likely to be
|
||||
# zoomed out enough on the slow chart that this is never an
|
||||
# issue (the last datum going out of y-range).
|
||||
# hist_chart = ds.hist_chart
|
||||
# if (
|
||||
# hist_liv
|
||||
|
@ -764,7 +814,8 @@ def graphics_update_cycle(
|
|||
# XXX: update this every draw cycle to ensure y-axis auto-ranging
|
||||
# only adjusts when the in-view data co-domain actually expands or
|
||||
# contracts.
|
||||
varz['last_mx'], varz['last_mn'] = mx, mn
|
||||
# varz['last_mn'] = mn
|
||||
# varz['last_mx'] = mx
|
||||
|
||||
# TODO: a similar, only-update-full-path-on-px-step approach for all
|
||||
# fsp overlays and vlm stuff..
|
||||
|
@ -772,10 +823,12 @@ def graphics_update_cycle(
|
|||
# run synchronous update on all `Viz` overlays
|
||||
for curve_name, viz in chart._vizs.items():
|
||||
|
||||
if viz.is_ohlc:
|
||||
continue
|
||||
|
||||
# update any overlayed fsp flows
|
||||
if (
|
||||
curve_name != fqsn
|
||||
and not viz.is_ohlc
|
||||
):
|
||||
update_fsp_chart(
|
||||
viz,
|
||||
|
@ -788,8 +841,7 @@ def graphics_update_cycle(
|
|||
# px column to give the user the mx/mn
|
||||
# range of that set.
|
||||
if (
|
||||
curve_name != fqsn
|
||||
and liv
|
||||
liv
|
||||
# and not do_px_step
|
||||
# and not do_rt_update
|
||||
):
|
||||
|
@ -809,8 +861,14 @@ def graphics_update_cycle(
|
|||
# TODO: can we unify this with the above loop?
|
||||
if vlm_chart:
|
||||
vlm_vizs = vlm_chart._vizs
|
||||
|
||||
main_vlm_viz = vlm_vizs['volume']
|
||||
main_vlm_vb = main_vlm_viz.plot.vb
|
||||
|
||||
# TODO: we should probably read this
|
||||
# from the `Viz.vs: ViewState`!
|
||||
vlm_yr = main_vlm_vb._yrange
|
||||
if vlm_yr:
|
||||
(_, vlm_ymx) = vlm_yrange = vlm_yr
|
||||
|
||||
# always update y-label
|
||||
ds.vlm_sticky.update_from_data(
|
||||
|
@ -848,16 +906,30 @@ def graphics_update_cycle(
|
|||
profiler('`main_vlm_viz.update_graphics()`')
|
||||
|
||||
if (
|
||||
mx_vlm_in_view != varz['last_mx_vlm']
|
||||
mx_vlm_in_view
|
||||
and vlm_yr
|
||||
and mx_vlm_in_view != vlm_ymx
|
||||
):
|
||||
varz['last_mx_vlm'] = mx_vlm_in_view
|
||||
# vlm_yr = (0, mx_vlm_in_view * 1.375)
|
||||
# vlm_chart.view._set_yrange(yrange=vlm_yr)
|
||||
# profiler('`vlm_chart.view._set_yrange()`')
|
||||
# in this case we want to scale all overlays in the
|
||||
# sub-chart but only incrementally update the vlm since
|
||||
# we already calculated the new range above.
|
||||
# TODO: in theory we can incrementally update all
|
||||
# overlays as well though it will require iteration of
|
||||
# them here in the display loop right?
|
||||
main_vlm_viz.plot.vb.interact_graphics_cycle(
|
||||
do_overlay_scaling=True,
|
||||
do_linked_charts=False,
|
||||
yrange_kwargs={
|
||||
main_vlm_viz: {
|
||||
'yrange': vlm_yrange,
|
||||
# 'range_margin': yrange_margin,
|
||||
},
|
||||
},
|
||||
)
|
||||
profiler('`vlm_chart.view.interact_graphics_cycle()`')
|
||||
|
||||
# update all downstream FSPs
|
||||
for curve_name, viz in vlm_vizs.items():
|
||||
|
||||
if curve_name == 'volume':
|
||||
continue
|
||||
|
||||
|
@ -882,10 +954,13 @@ def graphics_update_cycle(
|
|||
# XXX: without this we get completely
|
||||
# mangled/empty vlm display subchart..
|
||||
# fvb = viz.plot.vb
|
||||
# fvb._set_yrange(
|
||||
# viz=viz,
|
||||
# fvb.interact_graphics_cycle(
|
||||
# do_linked_charts=False,
|
||||
# do_overlay_scaling=False,
|
||||
# )
|
||||
profiler(f'vlm `Viz[{viz.name}].plot.vb._set_yrange()`')
|
||||
profiler(
|
||||
f'Viz[{viz.name}].plot.vb.interact_graphics_cycle()`'
|
||||
)
|
||||
|
||||
# even if we're downsampled bigly
|
||||
# draw the last datum in the final
|
||||
|
@ -1224,6 +1299,9 @@ async def display_symbol_data(
|
|||
# to avoid internal pane creation.
|
||||
# sidepane=False,
|
||||
sidepane=godwidget.search,
|
||||
draw_kwargs={
|
||||
'last_step_color': 'original',
|
||||
},
|
||||
)
|
||||
|
||||
# ensure the last datum graphic is generated
|
||||
|
@ -1242,6 +1320,9 @@ async def display_symbol_data(
|
|||
# in the case of history chart we explicitly set `False`
|
||||
# to avoid internal pane creation.
|
||||
sidepane=pp_pane,
|
||||
draw_kwargs={
|
||||
'last_step_color': 'original',
|
||||
},
|
||||
)
|
||||
rt_viz = rt_chart.get_viz(fqsn)
|
||||
pis.setdefault(fqsn, [None, None])[0] = rt_chart.plotItem
|
||||
|
@ -1308,13 +1389,6 @@ async def display_symbol_data(
|
|||
name=fqsn,
|
||||
axis_title=fqsn,
|
||||
)
|
||||
# only show a singleton bottom-bottom axis by default.
|
||||
hist_pi.hideAxis('bottom')
|
||||
|
||||
# XXX: TODO: THIS WILL CAUSE A GAP ON OVERLAYS,
|
||||
# i think it needs to be "removed" instead when there
|
||||
# are none?
|
||||
hist_pi.hideAxis('left')
|
||||
|
||||
hist_viz = hist_chart.draw_curve(
|
||||
fqsn,
|
||||
|
@ -1333,10 +1407,6 @@ async def display_symbol_data(
|
|||
# for zoom-interaction purposes.
|
||||
hist_viz.draw_last(array_key=fqsn)
|
||||
|
||||
hist_pi.vb.maxmin = partial(
|
||||
hist_chart.maxmin,
|
||||
name=fqsn,
|
||||
)
|
||||
# TODO: we need a better API to do this..
|
||||
# specially store ref to shm for lookup in display loop
|
||||
# since only a placeholder of `None` is entered in
|
||||
|
@ -1350,9 +1420,6 @@ async def display_symbol_data(
|
|||
axis_title=fqsn,
|
||||
)
|
||||
|
||||
rt_pi.hideAxis('left')
|
||||
rt_pi.hideAxis('bottom')
|
||||
|
||||
rt_viz = rt_chart.draw_curve(
|
||||
fqsn,
|
||||
ohlcv,
|
||||
|
@ -1365,10 +1432,6 @@ async def display_symbol_data(
|
|||
color=bg_chart_color,
|
||||
last_step_color=bg_last_bar_color,
|
||||
)
|
||||
rt_pi.vb.maxmin = partial(
|
||||
rt_chart.maxmin,
|
||||
name=fqsn,
|
||||
)
|
||||
|
||||
# TODO: we need a better API to do this..
|
||||
# specially store ref to shm for lookup in display loop
|
||||
|
@ -1395,7 +1458,9 @@ async def display_symbol_data(
|
|||
for fqsn, flume in feed.flumes.items():
|
||||
|
||||
# size view to data prior to order mode init
|
||||
rt_chart.default_view()
|
||||
rt_chart.main_viz.default_view(
|
||||
do_min_bars=True,
|
||||
)
|
||||
rt_linked.graphics_cycle()
|
||||
|
||||
# TODO: look into this because not sure why it was
|
||||
|
@ -1406,7 +1471,9 @@ async def display_symbol_data(
|
|||
# determine if auto-range adjustements should be made.
|
||||
# rt_linked.subplots.pop('volume', None)
|
||||
|
||||
hist_chart.default_view()
|
||||
hist_chart.main_viz.default_view(
|
||||
do_min_bars=True,
|
||||
)
|
||||
hist_linked.graphics_cycle()
|
||||
|
||||
godwidget.resize_all()
|
||||
|
@ -1449,10 +1516,14 @@ async def display_symbol_data(
|
|||
|
||||
# default view adjuments and sidepane alignment
|
||||
# as final default UX touch.
|
||||
rt_chart.default_view()
|
||||
rt_chart.main_viz.default_view(
|
||||
do_min_bars=True,
|
||||
)
|
||||
await trio.sleep(0)
|
||||
|
||||
hist_chart.default_view()
|
||||
hist_chart.main_viz.default_view(
|
||||
do_min_bars=True,
|
||||
)
|
||||
hist_viz = hist_chart.get_viz(fqsn)
|
||||
await trio.sleep(0)
|
||||
|
||||
|
|
|
@ -21,7 +21,6 @@ Higher level annotation editors.
|
|||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from typing import (
|
||||
Optional,
|
||||
TYPE_CHECKING
|
||||
)
|
||||
|
||||
|
@ -67,7 +66,7 @@ class ArrowEditor(Struct):
|
|||
x: float,
|
||||
y: float,
|
||||
color='default',
|
||||
pointing: Optional[str] = None,
|
||||
pointing: str | None = None,
|
||||
|
||||
) -> pg.ArrowItem:
|
||||
'''
|
||||
|
@ -221,7 +220,7 @@ class LineEditor(Struct):
|
|||
line: LevelLine = None,
|
||||
uuid: str = None,
|
||||
|
||||
) -> Optional[LevelLine]:
|
||||
) -> LevelLine | None:
|
||||
'''Remove a line by refernce or uuid.
|
||||
|
||||
If no lines or ids are provided remove all lines under the
|
||||
|
|
|
@ -23,7 +23,9 @@ from contextlib import asynccontextmanager
|
|||
from functools import partial
|
||||
from math import floor
|
||||
from typing import (
|
||||
Optional, Any, Callable, Awaitable
|
||||
Any,
|
||||
Callable,
|
||||
Awaitable,
|
||||
)
|
||||
|
||||
import trio
|
||||
|
@ -263,7 +265,7 @@ class Selection(QComboBox):
|
|||
def set_icon(
|
||||
self,
|
||||
key: str,
|
||||
icon_name: Optional[str],
|
||||
icon_name: str | None,
|
||||
|
||||
) -> None:
|
||||
self.setItemIcon(
|
||||
|
@ -344,7 +346,7 @@ class FieldsForm(QWidget):
|
|||
|
||||
name: str,
|
||||
|
||||
font_size: Optional[int] = None,
|
||||
font_size: int | None = None,
|
||||
font_color: str = 'default_lightest',
|
||||
|
||||
) -> QtGui.QLabel:
|
||||
|
@ -469,7 +471,7 @@ def mk_form(
|
|||
|
||||
parent: QWidget,
|
||||
fields_schema: dict,
|
||||
font_size: Optional[int] = None,
|
||||
font_size: int | None = None,
|
||||
|
||||
) -> FieldsForm:
|
||||
|
||||
|
@ -628,7 +630,7 @@ def mk_fill_status_bar(
|
|||
parent_pane: QWidget,
|
||||
form: FieldsForm,
|
||||
pane_vbox: QVBoxLayout,
|
||||
label_font_size: Optional[int] = None,
|
||||
label_font_size: int | None = None,
|
||||
|
||||
) -> (
|
||||
# TODO: turn this into a composite?
|
||||
|
@ -738,7 +740,7 @@ def mk_fill_status_bar(
|
|||
def mk_order_pane_layout(
|
||||
|
||||
parent: QWidget,
|
||||
# accounts: dict[str, Optional[str]],
|
||||
# accounts: dict[str, str | None],
|
||||
|
||||
) -> FieldsForm:
|
||||
|
||||
|
|
|
@ -24,7 +24,10 @@ from contextlib import asynccontextmanager as acm
|
|||
from functools import partial
|
||||
import inspect
|
||||
from itertools import cycle
|
||||
from typing import Optional, AsyncGenerator, Any
|
||||
from typing import (
|
||||
AsyncGenerator,
|
||||
Any,
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
import msgspec
|
||||
|
@ -80,7 +83,7 @@ def has_vlm(ohlcv: ShmArray) -> bool:
|
|||
def update_fsp_chart(
|
||||
viz,
|
||||
graphics_name: str,
|
||||
array_key: Optional[str],
|
||||
array_key: str | None,
|
||||
**kwargs,
|
||||
|
||||
) -> None:
|
||||
|
@ -476,7 +479,7 @@ class FspAdmin:
|
|||
target: Fsp,
|
||||
conf: dict[str, dict[str, Any]],
|
||||
|
||||
worker_name: Optional[str] = None,
|
||||
worker_name: str | None = None,
|
||||
loglevel: str = 'info',
|
||||
|
||||
) -> (Flume, trio.Event):
|
||||
|
@ -608,10 +611,11 @@ async def open_vlm_displays(
|
|||
linked: LinkedSplits,
|
||||
flume: Flume,
|
||||
dvlm: bool = True,
|
||||
loglevel: str = 'info',
|
||||
|
||||
task_status: TaskStatus[ChartPlotWidget] = trio.TASK_STATUS_IGNORED,
|
||||
|
||||
) -> ChartPlotWidget:
|
||||
) -> None:
|
||||
'''
|
||||
Volume subchart displays.
|
||||
|
||||
|
@ -666,7 +670,6 @@ async def open_vlm_displays(
|
|||
# built-in vlm which we plot ASAP since it's
|
||||
# usually data provided directly with OHLC history.
|
||||
shm = ohlcv
|
||||
# ohlc_chart = linked.chart
|
||||
|
||||
vlm_chart = linked.add_plot(
|
||||
name='volume',
|
||||
|
@ -690,7 +693,14 @@ async def open_vlm_displays(
|
|||
# the axis on the left it's totally not lined up...
|
||||
# show volume units value on LHS (for dinkus)
|
||||
# vlm_chart.hideAxis('right')
|
||||
# vlm_chart.showAxis('left')
|
||||
vlm_chart.hideAxis('left')
|
||||
|
||||
# TODO: is it worth being able to remove axes (from i guess
|
||||
# a perf perspective) enough that we can actually do this and
|
||||
# other axis related calls (for eg. label upddates in the
|
||||
# display loop) don't raise when a the axis can't be loaded and
|
||||
# thus would normally cause many label related calls to crash?
|
||||
# axis = vlm_chart.removeAxis('left')
|
||||
|
||||
# send back new chart to caller
|
||||
task_status.started(vlm_chart)
|
||||
|
@ -704,17 +714,9 @@ async def open_vlm_displays(
|
|||
|
||||
# read from last calculated value
|
||||
value = shm.array['volume'][-1]
|
||||
|
||||
last_val_sticky.update_from_data(-1, value)
|
||||
|
||||
_, _, vlm_curve = vlm_chart.update_graphics_from_flow(
|
||||
'volume',
|
||||
)
|
||||
|
||||
# size view to data once at outset
|
||||
vlm_chart.view._set_yrange(
|
||||
viz=vlm_viz
|
||||
)
|
||||
_, _, vlm_curve = vlm_viz.update_graphics()
|
||||
|
||||
# add axis title
|
||||
axis = vlm_chart.getAxis('right')
|
||||
|
@ -722,7 +724,6 @@ async def open_vlm_displays(
|
|||
|
||||
if dvlm:
|
||||
|
||||
tasks_ready = []
|
||||
# spawn and overlay $ vlm on the same subchart
|
||||
dvlm_flume, started = await admin.start_engine_task(
|
||||
dolla_vlm,
|
||||
|
@ -736,22 +737,8 @@ async def open_vlm_displays(
|
|||
},
|
||||
},
|
||||
},
|
||||
# loglevel,
|
||||
loglevel,
|
||||
)
|
||||
tasks_ready.append(started)
|
||||
|
||||
# FIXME: we should error on starting the same fsp right
|
||||
# since it might collide with existing shm.. or wait we
|
||||
# had this before??
|
||||
# dolla_vlm
|
||||
|
||||
tasks_ready.append(started)
|
||||
# profiler(f'created shm for fsp actor: {display_name}')
|
||||
|
||||
# wait for all engine tasks to startup
|
||||
async with trio.open_nursery() as n:
|
||||
for event in tasks_ready:
|
||||
n.start_soon(event.wait)
|
||||
|
||||
# dolla vlm overlay
|
||||
# XXX: the main chart already contains a vlm "units" axis
|
||||
|
@ -774,10 +761,6 @@ async def open_vlm_displays(
|
|||
},
|
||||
)
|
||||
|
||||
# TODO: should this maybe be implicit based on input args to
|
||||
# `.overlay_plotitem()` above?
|
||||
dvlm_pi.hideAxis('bottom')
|
||||
|
||||
# all to be overlayed curve names
|
||||
dvlm_fields = [
|
||||
'dolla_vlm',
|
||||
|
@ -827,6 +810,7 @@ async def open_vlm_displays(
|
|||
)
|
||||
assert viz.plot is pi
|
||||
|
||||
await started.wait()
|
||||
chart_curves(
|
||||
dvlm_fields,
|
||||
dvlm_pi,
|
||||
|
@ -835,19 +819,17 @@ async def open_vlm_displays(
|
|||
step_mode=True,
|
||||
)
|
||||
|
||||
# spawn flow rates fsp **ONLY AFTER** the 'dolla_vlm' fsp is
|
||||
# up since this one depends on it.
|
||||
|
||||
# NOTE: spawn flow rates fsp **ONLY AFTER** the 'dolla_vlm' fsp is
|
||||
# up since calculating vlm "rates" obvs first requires the
|
||||
# underlying vlm event feed ;)
|
||||
fr_flume, started = await admin.start_engine_task(
|
||||
flow_rates,
|
||||
{ # fsp engine conf
|
||||
'func_name': 'flow_rates',
|
||||
'zero_on_step': True,
|
||||
},
|
||||
# loglevel,
|
||||
loglevel,
|
||||
)
|
||||
await started.wait()
|
||||
|
||||
# chart_curves(
|
||||
# dvlm_rate_fields,
|
||||
# dvlm_pi,
|
||||
|
@ -859,13 +841,15 @@ async def open_vlm_displays(
|
|||
# hide the original vlm curve since the $vlm one is now
|
||||
# displayed and the curves are effectively the same minus
|
||||
# liquidity events (well at least on low OHLC periods - 1s).
|
||||
vlm_curve.hide()
|
||||
# vlm_curve.hide()
|
||||
vlm_chart.removeItem(vlm_curve)
|
||||
vlm_viz = vlm_chart._vizs['volume']
|
||||
vlm_viz.render = False
|
||||
|
||||
# avoid range sorting on volume once disabled
|
||||
vlm_chart.view.disable_auto_yrange()
|
||||
# NOTE: DON'T DO THIS.
|
||||
# WHY: we want range sorting on volume for the RHS label!
|
||||
# -> if you don't want that then use this but likely you
|
||||
# only will if we decide to drop unit vlm..
|
||||
# vlm_viz.render = False
|
||||
|
||||
# Trade rate overlay
|
||||
# XXX: requires an additional overlay for
|
||||
|
@ -888,8 +872,8 @@ async def open_vlm_displays(
|
|||
},
|
||||
|
||||
)
|
||||
tr_pi.hideAxis('bottom')
|
||||
|
||||
await started.wait()
|
||||
chart_curves(
|
||||
trade_rate_fields,
|
||||
tr_pi,
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
# piker: trading gear for hackers
|
||||
# Copyright (C) Tyler Goodlet (in stewardship for piker0)
|
||||
# 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
|
||||
|
@ -14,16 +14,17 @@
|
|||
# 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/>.
|
||||
|
||||
"""
|
||||
'''
|
||||
Chart view box primitives
|
||||
|
||||
"""
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from contextlib import asynccontextmanager
|
||||
from functools import partial
|
||||
from contextlib import (
|
||||
asynccontextmanager,
|
||||
ExitStack,
|
||||
)
|
||||
import time
|
||||
from typing import (
|
||||
Optional,
|
||||
Callable,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
@ -40,6 +41,7 @@ import trio
|
|||
from ..log import get_logger
|
||||
from .._profile import Profiler
|
||||
from .._profile import pg_profile_enabled, ms_slower_then
|
||||
from .view_mode import overlay_viewlists
|
||||
# from ._style import _min_points_to_show
|
||||
from ._editors import SelectRect
|
||||
from . import _event
|
||||
|
@ -73,7 +75,7 @@ ORDER_MODE = {
|
|||
|
||||
async def handle_viewmode_kb_inputs(
|
||||
|
||||
view: 'ChartView',
|
||||
view: ChartView,
|
||||
recv_chan: trio.abc.ReceiveChannel,
|
||||
|
||||
) -> None:
|
||||
|
@ -87,7 +89,7 @@ async def handle_viewmode_kb_inputs(
|
|||
last = time.time()
|
||||
action: str
|
||||
|
||||
on_next_release: Optional[Callable] = None
|
||||
on_next_release: Callable | None = None
|
||||
|
||||
# for quick key sequence-combo pattern matching
|
||||
# we have a min_tap period and these should not
|
||||
|
@ -142,6 +144,23 @@ async def handle_viewmode_kb_inputs(
|
|||
if mods == Qt.ControlModifier:
|
||||
ctrl = True
|
||||
|
||||
# UI REPL-shell
|
||||
if (
|
||||
ctrl and key in {
|
||||
Qt.Key_U,
|
||||
}
|
||||
):
|
||||
import tractor
|
||||
god = order_mode.godw # noqa
|
||||
feed = order_mode.feed # noqa
|
||||
chart = order_mode.chart # noqa
|
||||
viz = chart.main_viz # noqa
|
||||
vlm_chart = chart.linked.subplots['volume'] # noqa
|
||||
vlm_viz = vlm_chart.main_viz # noqa
|
||||
dvlm_pi = vlm_chart._vizs['dolla_vlm'].plot # noqa
|
||||
await tractor.breakpoint()
|
||||
view.interact_graphics_cycle()
|
||||
|
||||
# SEARCH MODE #
|
||||
# ctlr-<space>/<l> for "lookup", "search" -> open search tree
|
||||
if (
|
||||
|
@ -169,9 +188,13 @@ async def handle_viewmode_kb_inputs(
|
|||
# View modes
|
||||
if key == Qt.Key_R:
|
||||
|
||||
# TODO: set this for all subplots
|
||||
# edge triggered default view activation
|
||||
view.chart.default_view()
|
||||
# NOTE: seems that if we don't yield a Qt render
|
||||
# cycle then the m4 downsampled curves will show here
|
||||
# without another reset..
|
||||
view._viz.default_view()
|
||||
view.interact_graphics_cycle()
|
||||
await trio.sleep(0)
|
||||
view.interact_graphics_cycle()
|
||||
|
||||
if len(fast_key_seq) > 1:
|
||||
# begin matches against sequences
|
||||
|
@ -313,7 +336,7 @@ async def handle_viewmode_kb_inputs(
|
|||
|
||||
async def handle_viewmode_mouse(
|
||||
|
||||
view: 'ChartView',
|
||||
view: ChartView,
|
||||
recv_chan: trio.abc.ReceiveChannel,
|
||||
|
||||
) -> None:
|
||||
|
@ -359,7 +382,7 @@ class ChartView(ViewBox):
|
|||
name: str,
|
||||
|
||||
parent: pg.PlotItem = None,
|
||||
static_yrange: Optional[tuple[float, float]] = None,
|
||||
static_yrange: tuple[float, float] | None = None,
|
||||
**kwargs,
|
||||
|
||||
):
|
||||
|
@ -392,8 +415,13 @@ class ChartView(ViewBox):
|
|||
self.order_mode: bool = False
|
||||
|
||||
self.setFocusPolicy(QtCore.Qt.StrongFocus)
|
||||
self._ic = None
|
||||
self._yranger: Callable | None = None
|
||||
self._in_interact: trio.Event | None = None
|
||||
self._interact_stack: ExitStack = ExitStack()
|
||||
|
||||
# TODO: probably just assign this whenever a new `PlotItem` is
|
||||
# allocated since they're 1to1 with views..
|
||||
self._viz: Viz | None = None
|
||||
self._yrange: tuple[float, float] | None = None
|
||||
|
||||
def start_ic(
|
||||
self,
|
||||
|
@ -403,10 +431,15 @@ class ChartView(ViewBox):
|
|||
to any interested task waiters.
|
||||
|
||||
'''
|
||||
if self._ic is None:
|
||||
if self._in_interact is None:
|
||||
chart = self.chart
|
||||
try:
|
||||
self.chart.pause_all_feeds()
|
||||
self._ic = trio.Event()
|
||||
self._in_interact = trio.Event()
|
||||
|
||||
chart.pause_all_feeds()
|
||||
self._interact_stack.enter_context(
|
||||
chart.reset_graphics_caches()
|
||||
)
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
|
@ -420,11 +453,13 @@ class ChartView(ViewBox):
|
|||
to any waiters.
|
||||
|
||||
'''
|
||||
if self._ic:
|
||||
if self._in_interact:
|
||||
try:
|
||||
self._ic.set()
|
||||
self._ic = None
|
||||
self._interact_stack.close()
|
||||
self.chart.resume_all_feeds()
|
||||
|
||||
self._in_interact.set()
|
||||
self._in_interact = None
|
||||
except RuntimeError:
|
||||
pass
|
||||
|
||||
|
@ -432,7 +467,7 @@ class ChartView(ViewBox):
|
|||
async def open_async_input_handler(
|
||||
self,
|
||||
|
||||
) -> 'ChartView':
|
||||
) -> ChartView:
|
||||
|
||||
async with (
|
||||
_event.open_handlers(
|
||||
|
@ -492,7 +527,7 @@ class ChartView(ViewBox):
|
|||
|
||||
# don't zoom more then the min points setting
|
||||
viz = chart.get_viz(chart.name)
|
||||
vl, lbar, rbar, vr = viz.bars_range()
|
||||
_, vl, lbar, rbar, vr, r = viz.datums_range()
|
||||
|
||||
# TODO: max/min zoom limits incorporating time step size.
|
||||
# rl = vr - vl
|
||||
|
@ -507,7 +542,7 @@ class ChartView(ViewBox):
|
|||
# return
|
||||
|
||||
# actual scaling factor
|
||||
s = 1.015 ** (ev.delta() * -1 / 20) # self.state['wheelScaleFactor'])
|
||||
s = 1.016 ** (ev.delta() * -1 / 20) # self.state['wheelScaleFactor'])
|
||||
s = [(None if m is False else s) for m in mask]
|
||||
|
||||
if (
|
||||
|
@ -533,12 +568,13 @@ class ChartView(ViewBox):
|
|||
# scale_y = 1.3 ** (center.y() * -1 / 20)
|
||||
self.scaleBy(s, center)
|
||||
|
||||
# zoom in view-box area
|
||||
else:
|
||||
# use right-most point of current curve graphic
|
||||
xl = viz.graphics.x_last()
|
||||
focal = min(
|
||||
xl,
|
||||
vr,
|
||||
r,
|
||||
)
|
||||
|
||||
self._resetTarget()
|
||||
|
@ -552,7 +588,7 @@ class ChartView(ViewBox):
|
|||
# update, but i gotta feelin that because this one is signal
|
||||
# based (and thus not necessarily sync invoked right away)
|
||||
# that calling the resize method manually might work better.
|
||||
self.sigRangeChangedManually.emit(mask)
|
||||
# self.sigRangeChangedManually.emit(mask)
|
||||
|
||||
# XXX: without this is seems as though sometimes
|
||||
# when zooming in from far out (and maybe vice versa?)
|
||||
|
@ -562,14 +598,15 @@ class ChartView(ViewBox):
|
|||
# that never seems to happen? Only question is how much this
|
||||
# "double work" is causing latency when these missing event
|
||||
# fires don't happen?
|
||||
self.maybe_downsample_graphics()
|
||||
self.interact_graphics_cycle()
|
||||
self.interact_graphics_cycle()
|
||||
|
||||
ev.accept()
|
||||
|
||||
def mouseDragEvent(
|
||||
self,
|
||||
ev,
|
||||
axis: Optional[int] = None,
|
||||
axis: int | None = None,
|
||||
|
||||
) -> None:
|
||||
pos = ev.pos()
|
||||
|
@ -581,7 +618,10 @@ class ChartView(ViewBox):
|
|||
button = ev.button()
|
||||
|
||||
# Ignore axes if mouse is disabled
|
||||
mouseEnabled = np.array(self.state['mouseEnabled'], dtype=np.float)
|
||||
mouseEnabled = np.array(
|
||||
self.state['mouseEnabled'],
|
||||
dtype=np.float,
|
||||
)
|
||||
mask = mouseEnabled.copy()
|
||||
if axis is not None:
|
||||
mask[1-axis] = 0.0
|
||||
|
@ -645,9 +685,6 @@ class ChartView(ViewBox):
|
|||
self.start_ic()
|
||||
except RuntimeError:
|
||||
pass
|
||||
# if self._ic is None:
|
||||
# self.chart.pause_all_feeds()
|
||||
# self._ic = trio.Event()
|
||||
|
||||
if axis == 1:
|
||||
self.chart._static_yrange = 'axis'
|
||||
|
@ -664,16 +701,19 @@ class ChartView(ViewBox):
|
|||
if x is not None or y is not None:
|
||||
self.translateBy(x=x, y=y)
|
||||
|
||||
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
||||
# self.sigRangeChangedManually.emit(mask)
|
||||
# self.state['mouseEnabled']
|
||||
# )
|
||||
self.interact_graphics_cycle()
|
||||
|
||||
if ev.isFinish():
|
||||
self.signal_ic()
|
||||
# self._ic.set()
|
||||
# self._ic = None
|
||||
# self._in_interact.set()
|
||||
# self._in_interact = None
|
||||
# self.chart.resume_all_feeds()
|
||||
|
||||
# XXX: WHY
|
||||
ev.accept()
|
||||
# # XXX: WHY
|
||||
# ev.accept()
|
||||
|
||||
# WEIRD "RIGHT-CLICK CENTER ZOOM" MODE
|
||||
elif button & QtCore.Qt.RightButton:
|
||||
|
@ -695,10 +735,12 @@ class ChartView(ViewBox):
|
|||
center = Point(tr.map(ev.buttonDownPos(QtCore.Qt.RightButton)))
|
||||
self._resetTarget()
|
||||
self.scaleBy(x=x, y=y, center=center)
|
||||
self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
||||
|
||||
# XXX: WHY
|
||||
ev.accept()
|
||||
# self.sigRangeChangedManually.emit(self.state['mouseEnabled'])
|
||||
self.interact_graphics_cycle()
|
||||
|
||||
# XXX: WHY
|
||||
ev.accept()
|
||||
|
||||
# def mouseClickEvent(self, event: QtCore.QEvent) -> None:
|
||||
# '''This routine is rerouted to an async handler.
|
||||
|
@ -719,19 +761,19 @@ class ChartView(ViewBox):
|
|||
self,
|
||||
*,
|
||||
|
||||
yrange: Optional[tuple[float, float]] = None,
|
||||
yrange: tuple[float, float] | None = None,
|
||||
viz: Viz | None = None,
|
||||
|
||||
# NOTE: this value pairs (more or less) with L1 label text
|
||||
# height offset from from the bid/ask lines.
|
||||
range_margin: float = 0.09,
|
||||
range_margin: float | None = 0.06,
|
||||
|
||||
bars_range: Optional[tuple[int, int, int, int]] = None,
|
||||
bars_range: tuple[int, int, int, int] | None = None,
|
||||
|
||||
# flag to prevent triggering sibling charts from the same linked
|
||||
# set from recursion errors.
|
||||
autoscale_linked_plots: bool = False,
|
||||
name: Optional[str] = None,
|
||||
name: str | None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -743,14 +785,13 @@ class ChartView(ViewBox):
|
|||
|
||||
'''
|
||||
name = self.name
|
||||
# print(f'YRANGE ON {name}')
|
||||
# print(f'YRANGE ON {name} -> yrange{yrange}')
|
||||
profiler = Profiler(
|
||||
msg=f'`ChartView._set_yrange()`: `{name}`',
|
||||
disabled=not pg_profile_enabled(),
|
||||
ms_threshold=ms_slower_then,
|
||||
delayed=True,
|
||||
)
|
||||
set_range = True
|
||||
chart = self._chart
|
||||
|
||||
# view has been set in 'axis' mode
|
||||
|
@ -759,8 +800,8 @@ class ChartView(ViewBox):
|
|||
# - disable autoranging
|
||||
# - remove any y range limits
|
||||
if chart._static_yrange == 'axis':
|
||||
set_range = False
|
||||
self.setLimits(yMin=None, yMax=None)
|
||||
return
|
||||
|
||||
# static y-range has been set likely by
|
||||
# a specialized FSP configuration.
|
||||
|
@ -773,54 +814,72 @@ class ChartView(ViewBox):
|
|||
elif yrange is not None:
|
||||
ylow, yhigh = yrange
|
||||
|
||||
if set_range:
|
||||
# XXX: only compute the mxmn range
|
||||
# if none is provided as input!
|
||||
if not yrange:
|
||||
|
||||
# XXX: only compute the mxmn range
|
||||
# if none is provided as input!
|
||||
if not yrange:
|
||||
if not viz:
|
||||
breakpoint()
|
||||
|
||||
if not viz:
|
||||
breakpoint()
|
||||
out = viz.maxmin()
|
||||
if out is None:
|
||||
log.warning(f'No yrange provided for {name}!?')
|
||||
return
|
||||
(
|
||||
ixrng,
|
||||
_,
|
||||
yrange
|
||||
) = out
|
||||
|
||||
out = viz.maxmin()
|
||||
if out is None:
|
||||
log.warning(f'No yrange provided for {name}!?')
|
||||
return
|
||||
(
|
||||
ixrng,
|
||||
_,
|
||||
yrange
|
||||
) = out
|
||||
profiler(f'`{self.name}:Viz.maxmin()` -> {ixrng}=>{yrange}')
|
||||
|
||||
profiler(f'`{self.name}:Viz.maxmin()` -> {ixrng}=>{yrange}')
|
||||
|
||||
if yrange is None:
|
||||
log.warning(f'No yrange provided for {name}!?')
|
||||
return
|
||||
if yrange is None:
|
||||
log.warning(f'No yrange provided for {name}!?')
|
||||
return
|
||||
|
||||
ylow, yhigh = yrange
|
||||
|
||||
# view margins: stay within a % of the "true range"
|
||||
# always stash last range for diffing by
|
||||
# incremental update calculations BEFORE adding
|
||||
# margin.
|
||||
self._yrange = ylow, yhigh
|
||||
|
||||
# view margins: stay within a % of the "true range"
|
||||
if range_margin is not None:
|
||||
diff = yhigh - ylow
|
||||
ylow = ylow - (diff * range_margin)
|
||||
yhigh = yhigh + (diff * range_margin)
|
||||
|
||||
# XXX: this often needs to be unset
|
||||
# to get different view modes to operate
|
||||
# correctly!
|
||||
self.setLimits(
|
||||
yMin=ylow,
|
||||
yMax=yhigh,
|
||||
ylow = max(
|
||||
ylow - (diff * range_margin),
|
||||
0,
|
||||
)
|
||||
yhigh = min(
|
||||
yhigh + (diff * range_margin),
|
||||
yhigh * (1 + range_margin),
|
||||
)
|
||||
self.setYRange(ylow, yhigh)
|
||||
profiler(f'set limits: {(ylow, yhigh)}')
|
||||
|
||||
# print(
|
||||
# f'set limits {self.name}:\n'
|
||||
# f'ylow: {ylow}\n'
|
||||
# f'yhigh: {yhigh}\n'
|
||||
# )
|
||||
self.setYRange(
|
||||
ylow,
|
||||
yhigh,
|
||||
padding=0,
|
||||
)
|
||||
self.setLimits(
|
||||
yMin=ylow,
|
||||
yMax=yhigh,
|
||||
)
|
||||
self.update()
|
||||
|
||||
# LOL: yet anothercucking pg buggg..
|
||||
# can't use `msg=f'setYRange({ylow}, {yhigh}')`
|
||||
profiler.finish()
|
||||
|
||||
def enable_auto_yrange(
|
||||
self,
|
||||
viz: Viz,
|
||||
src_vb: Optional[ChartView] = None,
|
||||
src_vb: ChartView | None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -831,18 +890,6 @@ class ChartView(ViewBox):
|
|||
if src_vb is None:
|
||||
src_vb = self
|
||||
|
||||
if self._yranger is None:
|
||||
self._yranger = partial(
|
||||
self._set_yrange,
|
||||
viz=viz,
|
||||
)
|
||||
|
||||
# widget-UIs/splitter(s) resizing
|
||||
src_vb.sigResized.connect(self._yranger)
|
||||
|
||||
# mouse wheel doesn't emit XRangeChanged
|
||||
src_vb.sigRangeChangedManually.connect(self._yranger)
|
||||
|
||||
# re-sampling trigger:
|
||||
# TODO: a smarter way to avoid calling this needlessly?
|
||||
# 2 things i can think of:
|
||||
|
@ -850,23 +897,20 @@ class ChartView(ViewBox):
|
|||
# iterate those.
|
||||
# - only register this when certain downsample-able graphics are
|
||||
# "added to scene".
|
||||
src_vb.sigRangeChangedManually.connect(
|
||||
self.maybe_downsample_graphics
|
||||
# src_vb.sigRangeChangedManually.connect(
|
||||
# self.interact_graphics_cycle
|
||||
# )
|
||||
|
||||
# widget-UIs/splitter(s) resizing
|
||||
src_vb.sigResized.connect(
|
||||
self.interact_graphics_cycle
|
||||
)
|
||||
|
||||
def disable_auto_yrange(self) -> None:
|
||||
|
||||
# XXX: not entirely sure why we can't de-reg this..
|
||||
self.sigResized.disconnect(
|
||||
self._yranger,
|
||||
)
|
||||
|
||||
self.sigRangeChangedManually.disconnect(
|
||||
self._yranger,
|
||||
)
|
||||
|
||||
self.sigRangeChangedManually.disconnect(
|
||||
self.maybe_downsample_graphics
|
||||
self.interact_graphics_cycle
|
||||
)
|
||||
|
||||
def x_uppx(self) -> float:
|
||||
|
@ -887,57 +931,54 @@ class ChartView(ViewBox):
|
|||
else:
|
||||
return 0
|
||||
|
||||
def maybe_downsample_graphics(
|
||||
def interact_graphics_cycle(
|
||||
self,
|
||||
autoscale_overlays: bool = False,
|
||||
*args, # capture Qt signal (slot) inputs
|
||||
|
||||
# debug_print: bool = False,
|
||||
do_linked_charts: bool = True,
|
||||
do_overlay_scaling: bool = True,
|
||||
|
||||
yrange_kwargs: dict[
|
||||
str,
|
||||
tuple[float, float],
|
||||
] | None = None,
|
||||
|
||||
):
|
||||
profiler = Profiler(
|
||||
msg=f'ChartView.maybe_downsample_graphics() for {self.name}',
|
||||
msg=f'ChartView.interact_graphics_cycle() for {self.name}',
|
||||
disabled=not pg_profile_enabled(),
|
||||
ms_threshold=ms_slower_then,
|
||||
|
||||
# XXX: important to avoid not seeing underlying
|
||||
# ``.update_graphics_from_flow()`` nested profiling likely
|
||||
# ``Viz.update_graphics()`` nested profiling likely
|
||||
# due to the way delaying works and garbage collection of
|
||||
# the profiler in the delegated method calls.
|
||||
ms_threshold=6,
|
||||
# ms_threshold=ms_slower_then,
|
||||
delayed=True,
|
||||
|
||||
# for hardcore latency checking, comment these flags above.
|
||||
# disabled=False,
|
||||
# ms_threshold=4,
|
||||
)
|
||||
|
||||
# TODO: a faster single-loop-iterator way of doing this XD
|
||||
chart = self._chart
|
||||
plots = {chart.name: chart}
|
||||
|
||||
linked = self.linked
|
||||
if linked:
|
||||
if (
|
||||
do_linked_charts
|
||||
and linked
|
||||
):
|
||||
plots = {linked.chart.name: linked.chart}
|
||||
plots |= linked.subplots
|
||||
|
||||
for chart_name, chart in plots.items():
|
||||
for name, flow in chart._vizs.items():
|
||||
else:
|
||||
chart = self._chart
|
||||
plots = {chart.name: chart}
|
||||
|
||||
if (
|
||||
not flow.render
|
||||
|
||||
# XXX: super important to be aware of this.
|
||||
# or not flow.graphics.isVisible()
|
||||
):
|
||||
# print(f'skipping {flow.name}')
|
||||
continue
|
||||
|
||||
# pass in no array which will read and render from the last
|
||||
# passed array (normally provided by the display loop.)
|
||||
chart.update_graphics_from_flow(name)
|
||||
|
||||
# for each overlay on this chart auto-scale the
|
||||
# y-range to max-min values.
|
||||
# if autoscale_overlays:
|
||||
# overlay = chart.pi_overlay
|
||||
# if overlay:
|
||||
# for pi in overlay.overlays:
|
||||
# pi.vb._set_yrange(
|
||||
# # TODO: get the range once up front...
|
||||
# # bars_range=br,
|
||||
# viz=pi.viz,
|
||||
# )
|
||||
# profiler('autoscaled linked plots')
|
||||
|
||||
profiler(f'<{chart_name}>.update_graphics_from_flow({name})')
|
||||
# TODO: a faster single-loop-iterator way of doing this?
|
||||
return overlay_viewlists(
|
||||
self._viz,
|
||||
plots,
|
||||
profiler,
|
||||
do_overlay_scaling=do_overlay_scaling,
|
||||
do_linked_charts=do_linked_charts,
|
||||
yrange_kwargs=yrange_kwargs,
|
||||
)
|
||||
|
|
|
@ -19,7 +19,10 @@ Non-shitty labels that don't re-invent the wheel.
|
|||
|
||||
"""
|
||||
from inspect import isfunction
|
||||
from typing import Callable, Optional, Any
|
||||
from typing import (
|
||||
Callable,
|
||||
Any,
|
||||
)
|
||||
|
||||
import pyqtgraph as pg
|
||||
from PyQt5 import QtGui, QtWidgets
|
||||
|
@ -70,9 +73,7 @@ class Label:
|
|||
self._fmt_str = fmt_str
|
||||
self._view_xy = QPointF(0, 0)
|
||||
|
||||
self.scene_anchor: Optional[
|
||||
Callable[..., QPointF]
|
||||
] = None
|
||||
self.scene_anchor: Callable[..., QPointF] | None = None
|
||||
|
||||
self._x_offset = x_offset
|
||||
|
||||
|
@ -164,7 +165,7 @@ class Label:
|
|||
self,
|
||||
|
||||
y: float,
|
||||
x: Optional[float] = None,
|
||||
x: float | None = None,
|
||||
|
||||
) -> None:
|
||||
|
||||
|
|
|
@ -22,7 +22,6 @@ from __future__ import annotations
|
|||
from functools import partial
|
||||
from math import floor
|
||||
from typing import (
|
||||
Optional,
|
||||
Callable,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
@ -32,7 +31,7 @@ from pyqtgraph import Point, functions as fn
|
|||
from PyQt5 import QtCore, QtGui, QtWidgets
|
||||
from PyQt5.QtCore import QPointF
|
||||
|
||||
from ._annotate import qgo_draw_markers, LevelMarker
|
||||
from ._annotate import LevelMarker
|
||||
from ._anchors import (
|
||||
vbr_left,
|
||||
right_axis,
|
||||
|
@ -295,7 +294,7 @@ class LevelLine(pg.InfiniteLine):
|
|||
# show y-crosshair again
|
||||
cursor.show_xhair()
|
||||
|
||||
def get_cursor(self) -> Optional[Cursor]:
|
||||
def get_cursor(self) -> Cursor | None:
|
||||
|
||||
chart = self._chart
|
||||
cur = chart.linked.cursor
|
||||
|
@ -610,11 +609,11 @@ def order_line(
|
|||
|
||||
chart,
|
||||
level: float,
|
||||
action: Optional[str] = 'buy', # buy or sell
|
||||
action: str | None = 'buy', # buy or sell
|
||||
|
||||
marker_style: Optional[str] = None,
|
||||
level_digits: Optional[float] = 3,
|
||||
size: Optional[int] = 1,
|
||||
marker_style: str | None = None,
|
||||
level_digits: float | None = 3,
|
||||
size: int | None = 1,
|
||||
size_digits: int = 1,
|
||||
show_markers: bool = False,
|
||||
submit_price: float = None,
|
||||
|
|
|
@ -21,7 +21,6 @@ Notifications utils.
|
|||
import os
|
||||
import platform
|
||||
import subprocess
|
||||
from typing import Optional
|
||||
|
||||
import trio
|
||||
|
||||
|
@ -33,7 +32,7 @@ from ..clearing._messages import (
|
|||
log = get_logger(__name__)
|
||||
|
||||
|
||||
_dbus_uid: Optional[str] = ''
|
||||
_dbus_uid: str | None = ''
|
||||
|
||||
|
||||
async def notify_from_ems_status_msg(
|
||||
|
|
|
@ -28,7 +28,6 @@ from PyQt5.QtCore import (
|
|||
QLineF,
|
||||
QRectF,
|
||||
)
|
||||
from PyQt5.QtWidgets import QGraphicsItem
|
||||
from PyQt5.QtGui import QPainterPath
|
||||
|
||||
from ._curve import FlowGraphic
|
||||
|
@ -91,10 +90,6 @@ class BarItems(FlowGraphic):
|
|||
"Price range" bars graphics rendered from a OHLC sampled sequence.
|
||||
|
||||
'''
|
||||
# XXX: causes this weird jitter bug when click-drag panning
|
||||
# where the path curve will awkwardly flicker back and forth?
|
||||
cache_mode: int = QGraphicsItem.NoCache
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*args,
|
||||
|
@ -113,9 +108,10 @@ class BarItems(FlowGraphic):
|
|||
'''
|
||||
if self._last_bar_lines:
|
||||
close_arm_line = self._last_bar_lines[-1]
|
||||
return close_arm_line.x2() if close_arm_line else None
|
||||
else:
|
||||
return None
|
||||
if close_arm_line:
|
||||
return close_arm_line.x2()
|
||||
|
||||
return None
|
||||
|
||||
# Qt docs: https://doc.qt.io/qt-5/qgraphicsitem.html#boundingRect
|
||||
def boundingRect(self):
|
||||
|
|
|
@ -20,8 +20,9 @@ micro-ORM for coupling ``pydantic`` models with Qt input/output widgets.
|
|||
"""
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Optional, Generic,
|
||||
TypeVar, Callable,
|
||||
Generic,
|
||||
TypeVar,
|
||||
Callable,
|
||||
)
|
||||
|
||||
# from pydantic import BaseModel, validator
|
||||
|
@ -42,13 +43,11 @@ DataType = TypeVar('DataType')
|
|||
|
||||
|
||||
class Field(GenericModel, Generic[DataType]):
|
||||
widget_factory: Optional[
|
||||
Callable[
|
||||
[QWidget, 'Field'],
|
||||
QWidget
|
||||
]
|
||||
]
|
||||
value: Optional[DataType] = None
|
||||
widget_factory: Callable[
|
||||
[QWidget, 'Field'],
|
||||
QWidget
|
||||
] | None = None
|
||||
value: DataType | None = None
|
||||
|
||||
|
||||
class Selection(Field[DataType], Generic[DataType]):
|
||||
|
|
|
@ -22,7 +22,6 @@ from collections import defaultdict
|
|||
from functools import partial
|
||||
from typing import (
|
||||
Callable,
|
||||
Optional,
|
||||
)
|
||||
|
||||
from pyqtgraph.graphicsItems.AxisItem import AxisItem
|
||||
|
@ -116,6 +115,7 @@ class ComposedGridLayout:
|
|||
|
||||
layout.setContentsMargins(0, 0, 0, 0)
|
||||
layout.setSpacing(0)
|
||||
layout.setMinimumWidth(0)
|
||||
|
||||
if name in ('top', 'bottom'):
|
||||
orient = Qt.Vertical
|
||||
|
@ -125,7 +125,11 @@ class ComposedGridLayout:
|
|||
|
||||
layout.setOrientation(orient)
|
||||
|
||||
self.insert_plotitem(0, pi)
|
||||
self.insert_plotitem(
|
||||
0,
|
||||
pi,
|
||||
remove_axes=False,
|
||||
)
|
||||
|
||||
# insert surrounding linear layouts into the parent pi's layout
|
||||
# such that additional axes can be appended arbitrarily without
|
||||
|
@ -140,7 +144,9 @@ class ComposedGridLayout:
|
|||
assert linlayout.itemAt(0) is axis
|
||||
|
||||
# XXX: see comment in ``.insert_plotitem()``...
|
||||
# our `PlotItem.removeAxis()` does this internally.
|
||||
# pi.layout.removeItem(axis)
|
||||
|
||||
pi.layout.addItem(linlayout, *index)
|
||||
layout = pi.layout.itemAt(*index)
|
||||
assert layout is linlayout
|
||||
|
@ -165,6 +171,8 @@ class ComposedGridLayout:
|
|||
index: int,
|
||||
plotitem: PlotItem,
|
||||
|
||||
remove_axes: bool = False,
|
||||
|
||||
) -> tuple[int, list[AxisItem]]:
|
||||
'''
|
||||
Place item at index by inserting all axes into the grid
|
||||
|
@ -193,25 +201,19 @@ class ComposedGridLayout:
|
|||
axis_view = axis.linkedView()
|
||||
assert axis_view is plotitem.vb
|
||||
|
||||
if (
|
||||
not axis.isVisible()
|
||||
# if (
|
||||
# not axis.isVisible()
|
||||
|
||||
# XXX: we never skip moving the axes for the *root*
|
||||
# plotitem inserted (even if not shown) since we need to
|
||||
# move all the hidden axes into linear sub-layouts for
|
||||
# that "central" plot in the overlay. Also if we don't
|
||||
# do it there's weird geomoetry calc offsets that make
|
||||
# view coords slightly off somehow .. smh
|
||||
and not len(self.pitems) == 0
|
||||
):
|
||||
continue
|
||||
|
||||
# XXX: Remove old axis?
|
||||
# No, turns out we don't need this?
|
||||
# DON'T UNLINK IT since we need the original ``ViewBox`` to
|
||||
# still drive it with events/handlers B)
|
||||
# popped = plotitem.removeAxis(name, unlink=False)
|
||||
# assert axis is popped
|
||||
# # XXX: we never skip moving the axes for the *root*
|
||||
# # plotitem inserted (even if not shown) since we need to
|
||||
# # move all the hidden axes into linear sub-layouts for
|
||||
# # that "central" plot in the overlay. Also if we don't
|
||||
# # do it there's weird geomoetry calc offsets that make
|
||||
# # view coords slightly off somehow .. smh
|
||||
# and not len(self.pitems) == 0
|
||||
# ):
|
||||
# print(f'SKIPPING MOVE: {plotitem.name}:{name} -> {axis}')
|
||||
# continue
|
||||
|
||||
# invert insert index for layouts which are
|
||||
# not-left-to-right, top-to-bottom insert oriented
|
||||
|
@ -225,6 +227,16 @@ class ComposedGridLayout:
|
|||
|
||||
self._register_item(index, plotitem)
|
||||
|
||||
if remove_axes:
|
||||
for name, axis_info in plotitem.axes.copy().items():
|
||||
axis = axis_info['item']
|
||||
# XXX: Remove old axis?
|
||||
# No, turns out we don't need this?
|
||||
# DON'T UNLINK IT since we need the original ``ViewBox`` to
|
||||
# still drive it with events/handlers B)
|
||||
popped = plotitem.removeAxis(name, unlink=False)
|
||||
assert axis is popped
|
||||
|
||||
return (index, inserted_axes)
|
||||
|
||||
def append_plotitem(
|
||||
|
@ -246,7 +258,7 @@ class ComposedGridLayout:
|
|||
plot: PlotItem,
|
||||
name: str,
|
||||
|
||||
) -> Optional[AxisItem]:
|
||||
) -> AxisItem | None:
|
||||
'''
|
||||
Retrieve the named axis for overlayed ``plot`` or ``None``
|
||||
if axis for that name is not shown.
|
||||
|
@ -321,7 +333,7 @@ class PlotItemOverlay:
|
|||
def add_plotitem(
|
||||
self,
|
||||
plotitem: PlotItem,
|
||||
index: Optional[int] = None,
|
||||
index: int | None = None,
|
||||
|
||||
# event/signal names which will be broadcasted to all added
|
||||
# (relayee) ``PlotItem``s (eg. ``ViewBox.mouseDragEvent``).
|
||||
|
@ -376,7 +388,7 @@ class PlotItemOverlay:
|
|||
|
||||
# TODO: drop this viewbox specific input and
|
||||
# allow a predicate to be passed in by user.
|
||||
axis: 'Optional[int]' = None,
|
||||
axis: int | None = None,
|
||||
|
||||
*,
|
||||
|
||||
|
@ -487,10 +499,10 @@ class PlotItemOverlay:
|
|||
else:
|
||||
insert_index, axes = self.layout.insert_plotitem(index, plotitem)
|
||||
|
||||
plotitem.setGeometry(root.vb.sceneBoundingRect())
|
||||
plotitem.vb.setGeometry(root.vb.sceneBoundingRect())
|
||||
|
||||
def size_to_viewbox(vb: 'ViewBox'):
|
||||
plotitem.setGeometry(vb.sceneBoundingRect())
|
||||
plotitem.vb.setGeometry(root.vb.sceneBoundingRect())
|
||||
|
||||
root.vb.sigResized.connect(size_to_viewbox)
|
||||
|
||||
|
|
|
@ -22,8 +22,6 @@ Generally, our does not require "scentific precision" for pixel perfect
|
|||
view transforms.
|
||||
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
import pyqtgraph as pg
|
||||
|
||||
from ._axes import Axis
|
||||
|
@ -47,9 +45,10 @@ def invertQTransform(tr):
|
|||
|
||||
|
||||
def _do_overrides() -> None:
|
||||
"""Dooo eeet.
|
||||
'''
|
||||
Dooo eeet.
|
||||
|
||||
"""
|
||||
'''
|
||||
# we don't care about potential fp issues inside Qt
|
||||
pg.functions.invertQTransform = invertQTransform
|
||||
pg.PlotItem = PlotItem
|
||||
|
@ -91,7 +90,7 @@ class PlotItem(pg.PlotItem):
|
|||
title=None,
|
||||
viewBox=None,
|
||||
axisItems=None,
|
||||
default_axes=['left', 'bottom'],
|
||||
default_axes=['right', 'bottom'],
|
||||
enableMenu=True,
|
||||
**kargs
|
||||
):
|
||||
|
@ -119,7 +118,7 @@ class PlotItem(pg.PlotItem):
|
|||
name: str,
|
||||
unlink: bool = True,
|
||||
|
||||
) -> Optional[pg.AxisItem]:
|
||||
) -> pg.AxisItem | None:
|
||||
"""
|
||||
Remove an axis from the contained axis items
|
||||
by ```name: str```.
|
||||
|
@ -130,7 +129,7 @@ class PlotItem(pg.PlotItem):
|
|||
|
||||
If the ``unlink: bool`` is set to ``False`` then the axis will
|
||||
stay linked to its view and will only be removed from the
|
||||
layoutonly be removed from the layout.
|
||||
layout.
|
||||
|
||||
If no axis with ``name: str`` is found then this is a noop.
|
||||
|
||||
|
@ -144,7 +143,10 @@ class PlotItem(pg.PlotItem):
|
|||
|
||||
axis = entry['item']
|
||||
self.layout.removeItem(axis)
|
||||
axis.scene().removeItem(axis)
|
||||
scn = axis.scene()
|
||||
if scn:
|
||||
scn.removeItem(axis)
|
||||
|
||||
if unlink:
|
||||
axis.unlinkFromView()
|
||||
|
||||
|
@ -166,14 +168,14 @@ class PlotItem(pg.PlotItem):
|
|||
def setAxisItems(
|
||||
self,
|
||||
# XXX: yeah yeah, i know we can't use type annots like this yet.
|
||||
axisItems: Optional[dict[str, pg.AxisItem]] = None,
|
||||
axisItems: dict[str, pg.AxisItem] | None = None,
|
||||
add_to_layout: bool = True,
|
||||
default_axes: list[str] = ['left', 'bottom'],
|
||||
):
|
||||
"""
|
||||
Override axis item setting to only
|
||||
'''
|
||||
Override axis item setting to only what is passed in.
|
||||
|
||||
"""
|
||||
'''
|
||||
axisItems = axisItems or {}
|
||||
|
||||
# XXX: wth is is this even saying?!?
|
||||
|
|
|
@ -25,7 +25,6 @@ from functools import partial
|
|||
from math import floor, copysign
|
||||
from typing import (
|
||||
Callable,
|
||||
Optional,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
|
@ -170,12 +169,12 @@ class SettingsPane:
|
|||
limit_label: QLabel
|
||||
|
||||
# encompasing high level namespace
|
||||
order_mode: Optional['OrderMode'] = None # typing: ignore # noqa
|
||||
order_mode: OrderMode | None = None # typing: ignore # noqa
|
||||
|
||||
def set_accounts(
|
||||
self,
|
||||
names: list[str],
|
||||
sizes: Optional[list[float]] = None,
|
||||
sizes: list[float] | None = None,
|
||||
) -> None:
|
||||
|
||||
combo = self.form.fields['account']
|
||||
|
@ -540,8 +539,8 @@ class Nav(Struct):
|
|||
charts: dict[int, ChartPlotWidget]
|
||||
pp_labels: dict[str, Label] = {}
|
||||
size_labels: dict[str, Label] = {}
|
||||
lines: dict[str, Optional[LevelLine]] = {}
|
||||
level_markers: dict[str, Optional[LevelMarker]] = {}
|
||||
lines: dict[str, LevelLine | None] = {}
|
||||
level_markers: dict[str, LevelMarker | None] = {}
|
||||
color: str = 'default_lightest'
|
||||
|
||||
def update_ui(
|
||||
|
@ -550,7 +549,7 @@ class Nav(Struct):
|
|||
price: float,
|
||||
size: float,
|
||||
slots_used: float,
|
||||
size_digits: Optional[int] = None,
|
||||
size_digits: int | None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -847,7 +846,7 @@ class PositionTracker:
|
|||
|
||||
def update_from_pp(
|
||||
self,
|
||||
position: Optional[Position] = None,
|
||||
position: Position | None = None,
|
||||
set_as_startup: bool = False,
|
||||
|
||||
) -> None:
|
||||
|
|
|
@ -51,7 +51,20 @@ 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
|
||||
|
||||
|
@ -179,6 +192,10 @@ class Renderer(msgspec.Struct):
|
|||
|
||||
) = fmt_out
|
||||
|
||||
if not x_1d.size:
|
||||
log.warning(f'{array_key} has no `.size`?')
|
||||
return
|
||||
|
||||
# redraw conditions
|
||||
if (
|
||||
prepend_length > 0
|
||||
|
@ -195,7 +212,7 @@ class Renderer(msgspec.Struct):
|
|||
fast_path: QPainterPath = self.fast_path
|
||||
reset: bool = False
|
||||
|
||||
self.viz.yrange = None
|
||||
self.viz.ds_yrange = None
|
||||
|
||||
# redraw the entire source data if we have either of:
|
||||
# - no prior path graphic rendered or,
|
||||
|
@ -218,7 +235,7 @@ class Renderer(msgspec.Struct):
|
|||
)
|
||||
if ds_out is not None:
|
||||
x_1d, y_1d, ymn, ymx = ds_out
|
||||
self.viz.yrange = ymn, ymx
|
||||
self.viz.ds_yrange = ymn, ymx
|
||||
# print(f'{self.viz.name} post ds: ymn, ymx: {ymn},{ymx}')
|
||||
|
||||
reset = True
|
||||
|
|
|
@ -35,7 +35,6 @@ from collections import defaultdict
|
|||
from contextlib import asynccontextmanager
|
||||
from functools import partial
|
||||
from typing import (
|
||||
Optional,
|
||||
Callable,
|
||||
Awaitable,
|
||||
Sequence,
|
||||
|
@ -178,8 +177,8 @@ class CompleterView(QTreeView):
|
|||
|
||||
def resize_to_results(
|
||||
self,
|
||||
w: Optional[float] = 0,
|
||||
h: Optional[float] = None,
|
||||
w: float | None = 0,
|
||||
h: float | None = None,
|
||||
|
||||
) -> None:
|
||||
model = self.model()
|
||||
|
@ -380,7 +379,7 @@ class CompleterView(QTreeView):
|
|||
self,
|
||||
section: str,
|
||||
|
||||
) -> Optional[QModelIndex]:
|
||||
) -> QModelIndex | None:
|
||||
'''
|
||||
Find the *first* depth = 1 section matching ``section`` in
|
||||
the tree and return its index.
|
||||
|
@ -504,7 +503,7 @@ class CompleterView(QTreeView):
|
|||
|
||||
def show_matches(
|
||||
self,
|
||||
wh: Optional[tuple[float, float]] = None,
|
||||
wh: tuple[float, float] | None = None,
|
||||
|
||||
) -> None:
|
||||
|
||||
|
@ -529,7 +528,7 @@ class SearchBar(Edit):
|
|||
self,
|
||||
parent: QWidget,
|
||||
godwidget: QWidget,
|
||||
view: Optional[CompleterView] = None,
|
||||
view: CompleterView | None = None,
|
||||
**kwargs,
|
||||
|
||||
) -> None:
|
||||
|
@ -708,7 +707,7 @@ class SearchWidget(QtWidgets.QWidget):
|
|||
self,
|
||||
clear_to_cache: bool = True,
|
||||
|
||||
) -> Optional[str]:
|
||||
) -> str | None:
|
||||
'''
|
||||
Attempt to load and switch the current selected
|
||||
completion result to the affiliated chart app.
|
||||
|
@ -1167,7 +1166,7 @@ async def register_symbol_search(
|
|||
|
||||
provider_name: str,
|
||||
search_routine: Callable,
|
||||
pause_period: Optional[float] = None,
|
||||
pause_period: float | None = None,
|
||||
|
||||
) -> AsyncIterator[dict]:
|
||||
|
||||
|
|
|
@ -18,7 +18,7 @@
|
|||
Qt UI styling.
|
||||
|
||||
'''
|
||||
from typing import Optional, Dict
|
||||
from typing import Dict
|
||||
import math
|
||||
|
||||
import pyqtgraph as pg
|
||||
|
@ -52,7 +52,7 @@ class DpiAwareFont:
|
|||
# TODO: move to config
|
||||
name: str = 'Hack',
|
||||
font_size: str = 'default',
|
||||
# size_in_inches: Optional[float] = None,
|
||||
|
||||
) -> None:
|
||||
self.name = name
|
||||
self._qfont = QtGui.QFont(name)
|
||||
|
@ -91,13 +91,14 @@ class DpiAwareFont:
|
|||
def px_size(self) -> int:
|
||||
return self._qfont.pixelSize()
|
||||
|
||||
def configure_to_dpi(self, screen: Optional[QtGui.QScreen] = None):
|
||||
"""Set an appropriately sized font size depending on the screen DPI.
|
||||
def configure_to_dpi(self, screen: QtGui.QScreen | None = None):
|
||||
'''
|
||||
Set an appropriately sized font size depending on the screen DPI.
|
||||
|
||||
If we end up needing to generalize this more here there are resources
|
||||
listed in the script in ``snippets/qt_screen_info.py``.
|
||||
|
||||
"""
|
||||
'''
|
||||
if screen is None:
|
||||
screen = self.screen
|
||||
|
||||
|
|
|
@ -23,7 +23,6 @@ import signal
|
|||
import time
|
||||
from typing import (
|
||||
Callable,
|
||||
Optional,
|
||||
Union,
|
||||
)
|
||||
import uuid
|
||||
|
@ -64,9 +63,9 @@ class MultiStatus:
|
|||
|
||||
self,
|
||||
msg: str,
|
||||
final_msg: Optional[str] = None,
|
||||
final_msg: str | None = None,
|
||||
clear_on_next: bool = False,
|
||||
group_key: Optional[Union[bool, str]] = False,
|
||||
group_key: Union[bool, str] | None = False,
|
||||
|
||||
) -> Union[Callable[..., None], str]:
|
||||
'''
|
||||
|
@ -178,11 +177,11 @@ class MainWindow(QMainWindow):
|
|||
self.setWindowTitle(self.title)
|
||||
|
||||
# set by runtime after `trio` is engaged.
|
||||
self.godwidget: Optional[GodWidget] = None
|
||||
self.godwidget: GodWidget | None = None
|
||||
|
||||
self._status_bar: QStatusBar = None
|
||||
self._status_label: QLabel = None
|
||||
self._size: Optional[tuple[int, int]] = None
|
||||
self._size: tuple[int, int] | None = None
|
||||
|
||||
@property
|
||||
def mode_label(self) -> QLabel:
|
||||
|
@ -289,7 +288,7 @@ class MainWindow(QMainWindow):
|
|||
|
||||
def configure_to_desktop(
|
||||
self,
|
||||
size: Optional[tuple[int, int]] = None,
|
||||
size: tuple[int, int] | None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
|
@ -25,7 +25,6 @@ from functools import partial
|
|||
from pprint import pformat
|
||||
import time
|
||||
from typing import (
|
||||
Optional,
|
||||
Callable,
|
||||
Any,
|
||||
TYPE_CHECKING,
|
||||
|
@ -129,7 +128,7 @@ class OrderMode:
|
|||
trackers: dict[str, PositionTracker]
|
||||
|
||||
# switched state, the current position
|
||||
current_pp: Optional[PositionTracker] = None
|
||||
current_pp: PositionTracker | None = None
|
||||
active: bool = False
|
||||
name: str = 'order'
|
||||
dialogs: dict[str, Dialog] = field(default_factory=dict)
|
||||
|
@ -139,7 +138,7 @@ class OrderMode:
|
|||
'buy': 'buy_green',
|
||||
'sell': 'sell_red',
|
||||
}
|
||||
_staged_order: Optional[Order] = None
|
||||
_staged_order: Order | None = None
|
||||
|
||||
def on_level_change_update_next_order_info(
|
||||
self,
|
||||
|
@ -180,7 +179,7 @@ class OrderMode:
|
|||
def new_line_from_order(
|
||||
self,
|
||||
order: Order,
|
||||
chart: Optional[ChartPlotWidget] = None,
|
||||
chart: ChartPlotWidget | None = None,
|
||||
**line_kwargs,
|
||||
|
||||
) -> LevelLine:
|
||||
|
@ -340,7 +339,7 @@ class OrderMode:
|
|||
def submit_order(
|
||||
self,
|
||||
send_msg: bool = True,
|
||||
order: Optional[Order] = None,
|
||||
order: Order | None = None,
|
||||
|
||||
) -> Dialog:
|
||||
'''
|
||||
|
@ -452,7 +451,7 @@ class OrderMode:
|
|||
def on_submit(
|
||||
self,
|
||||
uuid: str,
|
||||
order: Optional[Order] = None,
|
||||
order: Order | None = None,
|
||||
|
||||
) -> Dialog:
|
||||
'''
|
||||
|
@ -496,7 +495,7 @@ class OrderMode:
|
|||
price: float,
|
||||
time_s: float,
|
||||
|
||||
pointing: Optional[str] = None,
|
||||
pointing: str | None = None,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
|
|
@ -0,0 +1,899 @@
|
|||
# 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/>.
|
||||
|
||||
'''
|
||||
Overlay (aka multi-chart) UX machinery.
|
||||
|
||||
'''
|
||||
from __future__ import annotations
|
||||
from typing import (
|
||||
Any,
|
||||
Literal,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import numpy as np
|
||||
import pendulum
|
||||
import pyqtgraph as pg
|
||||
|
||||
from ..data.types import Struct
|
||||
from ..data._pathops import slice_from_time
|
||||
from ..log import get_logger
|
||||
from .._profile import Profiler
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._chart import ChartPlotWidget
|
||||
from ._dataviz import Viz
|
||||
from ._interaction import ChartView
|
||||
|
||||
|
||||
log = get_logger(__name__)
|
||||
|
||||
|
||||
class OverlayT(Struct):
|
||||
'''
|
||||
An overlay co-domain range transformer.
|
||||
|
||||
Used to translate and apply a range from one y-range
|
||||
to another based on a returns logarithm:
|
||||
|
||||
R(ymn, ymx, yref) = (ymx - yref)/yref
|
||||
|
||||
which gives the log-scale multiplier, and
|
||||
|
||||
ymx_t = yref * (1 + R)
|
||||
|
||||
which gives the inverse to translate to the same value
|
||||
in the target co-domain.
|
||||
|
||||
'''
|
||||
viz: Viz | None = None
|
||||
start_t: float | None = None
|
||||
|
||||
# % "range" computed from some ref value to the mn/mx
|
||||
rng: float | None = None
|
||||
in_view: np.ndarray | None = None
|
||||
|
||||
# pinned-minor curve modified mn and max for the major dispersion
|
||||
# curve due to one series being shorter and the pin + scaling from
|
||||
# that pin point causing the original range to have to increase.
|
||||
y_val: float | None = None
|
||||
|
||||
def apply_r(
|
||||
self,
|
||||
y_ref: float, # reference value for dispersion metric
|
||||
|
||||
) -> float:
|
||||
return y_ref * (1 + self.rng)
|
||||
|
||||
|
||||
def intersect_from_longer(
|
||||
start_t_first: float,
|
||||
in_view_first: np.ndarray,
|
||||
|
||||
start_t_second: float,
|
||||
in_view_second: np.ndarray,
|
||||
step: float,
|
||||
|
||||
) -> np.ndarray:
|
||||
|
||||
tdiff = start_t_first - start_t_second
|
||||
|
||||
if tdiff == 0:
|
||||
return False
|
||||
|
||||
i: int = 0
|
||||
|
||||
# first time series has an "earlier" first time stamp then the 2nd.
|
||||
# aka 1st is "shorter" then the 2nd.
|
||||
if tdiff > 0:
|
||||
longer = in_view_second
|
||||
find_t = start_t_first
|
||||
i = 1
|
||||
|
||||
# second time series has an "earlier" first time stamp then the 1st.
|
||||
# aka 2nd is "shorter" then the 1st.
|
||||
elif tdiff < 0:
|
||||
longer = in_view_first
|
||||
find_t = start_t_second
|
||||
i = 0
|
||||
|
||||
slc = slice_from_time(
|
||||
arr=longer,
|
||||
start_t=find_t,
|
||||
stop_t=find_t,
|
||||
step=step,
|
||||
)
|
||||
return (
|
||||
longer[slc.start],
|
||||
find_t,
|
||||
i,
|
||||
)
|
||||
|
||||
|
||||
def _maybe_calc_yrange(
|
||||
viz: Viz,
|
||||
yrange_kwargs: dict[Viz, dict[str, Any]],
|
||||
profiler: Profiler,
|
||||
chart_name: str,
|
||||
|
||||
) -> tuple[
|
||||
slice,
|
||||
dict,
|
||||
] | None:
|
||||
|
||||
if not viz.render:
|
||||
return
|
||||
|
||||
# pass in no array which will read and render from the last
|
||||
# passed array (normally provided by the display loop.)
|
||||
in_view, i_read_range, _ = viz.update_graphics()
|
||||
|
||||
if not in_view:
|
||||
return
|
||||
|
||||
profiler(f'{viz.name}@{chart_name} `Viz.update_graphics()`')
|
||||
|
||||
# check if explicit yrange (kwargs) was passed in by the caller
|
||||
yrange_kwargs = yrange_kwargs.get(viz) if yrange_kwargs else None
|
||||
if yrange_kwargs is not None:
|
||||
read_slc = slice(*i_read_range)
|
||||
|
||||
else:
|
||||
out = viz.maxmin(i_read_range=i_read_range)
|
||||
if out is None:
|
||||
log.warning(f'No yrange provided for {viz.name}!?')
|
||||
return
|
||||
(
|
||||
_, # ixrng,
|
||||
read_slc,
|
||||
yrange
|
||||
) = out
|
||||
profiler(f'{viz.name}@{chart_name} `Viz.maxmin()`')
|
||||
yrange_kwargs = {'yrange': yrange}
|
||||
|
||||
return (
|
||||
read_slc,
|
||||
yrange_kwargs,
|
||||
)
|
||||
|
||||
|
||||
def overlay_viewlists(
|
||||
active_viz: Viz,
|
||||
plots: dict[str, ChartPlotWidget],
|
||||
profiler: Profiler,
|
||||
|
||||
# public config ctls
|
||||
do_linked_charts: bool = True,
|
||||
do_overlay_scaling: bool = True,
|
||||
yrange_kwargs: dict[
|
||||
str,
|
||||
tuple[float, float],
|
||||
] | None = None,
|
||||
|
||||
method: Literal[
|
||||
'loglin_ref_to_curve',
|
||||
'loglin_ref_to_first',
|
||||
'mxmn',
|
||||
'solo',
|
||||
|
||||
] = 'loglin_ref_to_curve',
|
||||
|
||||
# internal debug
|
||||
debug_print: bool = False,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
Calculate and apply y-domain (axis y-range) multi-curve overlay adjustments
|
||||
a set of ``plots`` based on the requested ``method``.
|
||||
|
||||
'''
|
||||
chart_name: str
|
||||
chart: ChartPlotWidget
|
||||
for chart_name, chart in plots.items():
|
||||
|
||||
overlay_viz_items = chart._vizs.items()
|
||||
|
||||
# Common `PlotItem` maxmin table; presumes that some path
|
||||
# graphics (and thus their backing data sets) are in the
|
||||
# same co-domain and view box (since the were added
|
||||
# a separate graphics objects to a common plot) and thus can
|
||||
# be sorted as one set per plot.
|
||||
mxmns_by_common_pi: dict[
|
||||
pg.PlotItem,
|
||||
tuple[float, float],
|
||||
] = {}
|
||||
|
||||
# proportional group auto-scaling per overlay set.
|
||||
# -> loop through overlays on each multi-chart widget
|
||||
# and scale all y-ranges based on autoscale config.
|
||||
# -> for any "group" overlay we want to dispersion normalize
|
||||
# and scale minor charts onto the major chart: the chart
|
||||
# with the most dispersion in the set.
|
||||
|
||||
# ONLY auto-yrange the viz mapped to THIS view box
|
||||
if (
|
||||
not do_overlay_scaling
|
||||
or len(overlay_viz_items) < 2
|
||||
):
|
||||
viz = active_viz
|
||||
out = _maybe_calc_yrange(
|
||||
viz,
|
||||
yrange_kwargs,
|
||||
profiler,
|
||||
chart_name,
|
||||
)
|
||||
|
||||
if out is None:
|
||||
continue
|
||||
|
||||
read_slc, yrange_kwargs = out
|
||||
viz.plot.vb._set_yrange(**yrange_kwargs)
|
||||
profiler(f'{viz.name}@{chart_name} single curve yrange')
|
||||
|
||||
if debug_print:
|
||||
print(f'ONLY ranging THIS viz: {viz.name}')
|
||||
|
||||
# don't iterate overlays, just move to next chart
|
||||
continue
|
||||
|
||||
if debug_print:
|
||||
divstr = '#'*46
|
||||
print(
|
||||
f'BEGIN UX GRAPHICS CYCLE: @{chart_name}\n'
|
||||
+
|
||||
divstr
|
||||
+
|
||||
'\n'
|
||||
)
|
||||
|
||||
# create a group overlay log-linearized y-range transform to
|
||||
# track and eventually inverse transform all overlay curves
|
||||
# to a common target max dispersion range.
|
||||
dnt = OverlayT()
|
||||
upt = OverlayT()
|
||||
|
||||
# collect certain flows have grapics objects **in seperate
|
||||
# plots/viewboxes** into groups and do a common calc to
|
||||
# determine auto-ranging input for `._set_yrange()`.
|
||||
# this is primarly used for our so called "log-linearized
|
||||
# multi-plot" overlay technique.
|
||||
overlay_table: dict[
|
||||
float,
|
||||
tuple[
|
||||
ChartView,
|
||||
Viz,
|
||||
float, # y start
|
||||
float, # y min
|
||||
float, # y max
|
||||
float, # y median
|
||||
slice, # in-view array slice
|
||||
np.ndarray, # in-view array
|
||||
float, # returns up scalar
|
||||
float, # return down scalar
|
||||
],
|
||||
] = {}
|
||||
|
||||
# multi-curve overlay processing stage
|
||||
for name, viz in overlay_viz_items:
|
||||
|
||||
out = _maybe_calc_yrange(
|
||||
viz,
|
||||
yrange_kwargs,
|
||||
profiler,
|
||||
chart_name,
|
||||
)
|
||||
if out is None:
|
||||
continue
|
||||
|
||||
read_slc, yrange_kwargs = out
|
||||
yrange = yrange_kwargs['yrange']
|
||||
pi = viz.plot
|
||||
|
||||
# handle multiple graphics-objs per viewbox cases
|
||||
mxmn = mxmns_by_common_pi.get(pi)
|
||||
if mxmn:
|
||||
yrange = mxmns_by_common_pi[pi] = (
|
||||
min(yrange[0], mxmn[0]),
|
||||
max(yrange[1], mxmn[1]),
|
||||
)
|
||||
|
||||
else:
|
||||
mxmns_by_common_pi[pi] = yrange
|
||||
|
||||
profiler(f'{viz.name}@{chart_name} common pi sort')
|
||||
|
||||
# non-overlay group case
|
||||
if (
|
||||
not viz.is_ohlc
|
||||
or method == 'solo'
|
||||
):
|
||||
pi.vb._set_yrange(yrange=yrange)
|
||||
profiler(
|
||||
f'{viz.name}@{chart_name} simple std `._set_yrange()`'
|
||||
)
|
||||
continue
|
||||
|
||||
# handle overlay log-linearized group scaling cases
|
||||
# TODO: a better predicate here, likely something
|
||||
# to do with overlays and their settings..
|
||||
# TODO: we probably eventually might want some other
|
||||
# charts besides OHLC?
|
||||
else:
|
||||
ymn, ymx = yrange
|
||||
|
||||
# determine start datum in view
|
||||
in_view = viz.vs.in_view
|
||||
if in_view.size < 2:
|
||||
if debug_print:
|
||||
print(f'{viz.name} not in view?')
|
||||
continue
|
||||
|
||||
row_start = in_view[0]
|
||||
if viz.is_ohlc:
|
||||
y_ref = row_start['open']
|
||||
else:
|
||||
y_ref = row_start[viz.name]
|
||||
|
||||
profiler(f'{viz.name}@{chart_name} MINOR curve median')
|
||||
|
||||
key = 'open' if viz.is_ohlc else viz.name
|
||||
start_t = row_start['time']
|
||||
|
||||
# returns scalars
|
||||
r_up = (ymx - y_ref) / y_ref
|
||||
r_down = (ymn - y_ref) / y_ref
|
||||
disp = r_up - r_down
|
||||
|
||||
msg = (
|
||||
f'Viz[{viz.name}][{key}]: @{chart_name}\n'
|
||||
f' .yrange = {viz.vs.yrange}\n'
|
||||
f' .xrange = {viz.vs.xrange}\n\n'
|
||||
f'start_t: {start_t}\n'
|
||||
f'y_ref: {y_ref}\n'
|
||||
f'ymn: {ymn}\n'
|
||||
f'ymx: {ymx}\n'
|
||||
f'r_up: {r_up}\n'
|
||||
f'r_down: {r_down}\n'
|
||||
f'(full) disp: {disp}\n'
|
||||
)
|
||||
profiler(msg)
|
||||
if debug_print:
|
||||
print(msg)
|
||||
|
||||
# track the "major" curve as the curve with most
|
||||
# dispersion.
|
||||
if (
|
||||
dnt.rng is None
|
||||
or (
|
||||
r_down < dnt.rng
|
||||
and r_down < 0
|
||||
)
|
||||
):
|
||||
dnt.viz = viz
|
||||
dnt.rng = r_down
|
||||
dnt.in_view = in_view
|
||||
dnt.start_t = in_view[0]['time']
|
||||
dnt.y_val = ymn
|
||||
|
||||
profiler(f'NEW DOWN: {viz.name}@{chart_name} r: {r_down}')
|
||||
else:
|
||||
# minor in the down swing range so check that if
|
||||
# we apply the current rng to the minor that it
|
||||
# doesn't go outside the current range for the major
|
||||
# otherwise we recompute the minor's range (when
|
||||
# adjusted for it's intersect point to be the new
|
||||
# major's range.
|
||||
intersect = intersect_from_longer(
|
||||
dnt.start_t,
|
||||
dnt.in_view,
|
||||
start_t,
|
||||
in_view,
|
||||
viz.index_step(),
|
||||
)
|
||||
profiler(f'{viz.name}@{chart_name} intersect by t')
|
||||
|
||||
if intersect:
|
||||
longer_in_view, _t, i = intersect
|
||||
|
||||
scaled_mn = dnt.apply_r(y_ref)
|
||||
if scaled_mn > ymn:
|
||||
# after major curve scaling we detected
|
||||
# the minor curve is still out of range
|
||||
# so we need to adjust the major's range
|
||||
# to include the new composed range.
|
||||
y_maj_ref = longer_in_view[key]
|
||||
new_major_ymn = y_maj_ref * (1 + r_down)
|
||||
|
||||
# rewrite the major range to the new
|
||||
# minor-pinned-to-major range and mark
|
||||
# the transform as "virtual".
|
||||
msg = (
|
||||
f'EXPAND DOWN bc {viz.name}@{chart_name}\n'
|
||||
f'y_start epoch time @ {_t}:\n'
|
||||
f'y_maj_ref @ {_t}: {y_maj_ref}\n'
|
||||
f'R: {dnt.rng} -> {r_down}\n'
|
||||
f'MN: {dnt.y_val} -> {new_major_ymn}\n'
|
||||
)
|
||||
dnt.rng = r_down
|
||||
dnt.y_val = new_major_ymn
|
||||
profiler(msg)
|
||||
if debug_print:
|
||||
print(msg)
|
||||
|
||||
# is the current up `OverlayT` not yet defined or
|
||||
# the current `r_up` greater then the previous max.
|
||||
if (
|
||||
upt.rng is None
|
||||
or (
|
||||
r_up > upt.rng
|
||||
and r_up > 0
|
||||
)
|
||||
):
|
||||
upt.rng = r_up
|
||||
upt.viz = viz
|
||||
upt.in_view = in_view
|
||||
upt.start_t = in_view[0]['time']
|
||||
upt.y_val = ymx
|
||||
profiler(f'NEW UP: {viz.name}@{chart_name} r: {r_up}')
|
||||
|
||||
else:
|
||||
intersect = intersect_from_longer(
|
||||
upt.start_t,
|
||||
upt.in_view,
|
||||
start_t,
|
||||
in_view,
|
||||
viz.index_step(),
|
||||
)
|
||||
profiler(f'{viz.name}@{chart_name} intersect by t')
|
||||
|
||||
if intersect:
|
||||
longer_in_view, _t, i = intersect
|
||||
|
||||
# after major curve scaling we detect if
|
||||
# the minor curve is still out of range
|
||||
# so we need to adjust the major's range
|
||||
# to include the new composed range.
|
||||
scaled_mx = upt.apply_r(y_ref)
|
||||
if scaled_mx < ymx:
|
||||
y_maj_ref = longer_in_view[key]
|
||||
new_major_ymx = y_maj_ref * (1 + r_up)
|
||||
|
||||
# rewrite the major range to the new
|
||||
# minor-pinned-to-major range and mark
|
||||
# the transform as "virtual".
|
||||
msg = (
|
||||
f'EXPAND UP bc {viz.name}@{chart_name}:\n'
|
||||
f'y_maj_ref @ {_t}: {y_maj_ref}\n'
|
||||
f'R: {upt.rng} -> {r_up}\n'
|
||||
f'MX: {upt.y_val} -> {new_major_ymx}\n'
|
||||
)
|
||||
upt.rng = r_up
|
||||
upt.y_val = new_major_ymx
|
||||
profiler(msg)
|
||||
print(msg)
|
||||
|
||||
# register curves by a "full" dispersion metric for
|
||||
# later sort order in the overlay (technique
|
||||
# ) application loop below.
|
||||
overlay_table[disp] = (
|
||||
viz.plot.vb,
|
||||
viz,
|
||||
y_ref,
|
||||
ymn,
|
||||
ymx,
|
||||
read_slc,
|
||||
in_view,
|
||||
r_up,
|
||||
r_down,
|
||||
)
|
||||
profiler(f'{viz.name}@{chart_name} yrange scan complete')
|
||||
|
||||
# __ END OF scan phase (loop) __
|
||||
|
||||
# NOTE: if no there were no overlay charts
|
||||
# detected/collected (could be either no group detected or
|
||||
# chart with a single symbol, thus a single viz/overlay)
|
||||
# then we ONLY set the mone chart's (viz) yrange and short
|
||||
# circuit to the next chart in the linked charts loop. IOW
|
||||
# there's no reason to go through the overlay dispersion
|
||||
# scaling in the next loop below when only one curve is
|
||||
# detected.
|
||||
if (
|
||||
not mxmns_by_common_pi
|
||||
and len(overlay_table) < 2
|
||||
):
|
||||
if debug_print:
|
||||
print(f'ONLY ranging major: {viz.name}')
|
||||
|
||||
out = _maybe_calc_yrange(
|
||||
viz,
|
||||
yrange_kwargs,
|
||||
profiler,
|
||||
chart_name,
|
||||
)
|
||||
if out is None:
|
||||
continue
|
||||
|
||||
read_slc, yrange_kwargs = out
|
||||
viz.plot.vb._set_yrange(**yrange_kwargs)
|
||||
profiler(f'{viz.name}@{chart_name} single curve yrange')
|
||||
|
||||
# move to next chart in linked set since
|
||||
# no overlay transforming is needed.
|
||||
continue
|
||||
|
||||
elif (
|
||||
mxmns_by_common_pi
|
||||
and not overlay_table
|
||||
):
|
||||
# move to next chart in linked set since
|
||||
# no overlay transforming is needed.
|
||||
continue
|
||||
|
||||
profiler('`Viz` curve (first) scan phase complete\n')
|
||||
|
||||
r_up_mx: float
|
||||
r_dn_mn: float
|
||||
mx_disp = max(overlay_table)
|
||||
|
||||
if debug_print:
|
||||
# print overlay table in descending dispersion order
|
||||
msg = 'overlays in dispersion order:\n'
|
||||
for i, disp in enumerate(reversed(overlay_table)):
|
||||
entry = overlay_table[disp]
|
||||
msg += f' [{i}] {disp}: {entry[1].name}\n'
|
||||
|
||||
print(
|
||||
'TRANSFORM PHASE' + '-'*100 + '\n\n'
|
||||
+
|
||||
msg
|
||||
)
|
||||
|
||||
if method == 'loglin_ref_to_curve':
|
||||
mx_entry = overlay_table.pop(mx_disp)
|
||||
else:
|
||||
# TODO: for pin to first-in-view we need to no pop this from the
|
||||
# table, but can we simplify below code even more?
|
||||
mx_entry = overlay_table[mx_disp]
|
||||
|
||||
(
|
||||
mx_view, # viewbox
|
||||
mx_viz, # viz
|
||||
_, # y_ref
|
||||
mx_ymn,
|
||||
mx_ymx,
|
||||
_, # read_slc
|
||||
mx_in_view, # in_view array
|
||||
r_up_mx,
|
||||
r_dn_mn,
|
||||
) = mx_entry
|
||||
mx_time = mx_in_view['time']
|
||||
mx_xref = mx_time[0]
|
||||
|
||||
# conduct "log-linearized multi-plot" range transform
|
||||
# calculations for curves detected as overlays in the previous
|
||||
# loop:
|
||||
# -> iterate all curves Ci in dispersion-measure sorted order
|
||||
# going from smallest swing to largest via the
|
||||
# ``overlay_table: dict``,
|
||||
# -> match on overlay ``method: str`` provided by caller,
|
||||
# -> calc y-ranges from each curve's time series and store in
|
||||
# a final table ``scaled: dict`` for final application in the
|
||||
# scaling loop; the final phase.
|
||||
scaled: dict[
|
||||
float,
|
||||
tuple[Viz, float, float, float, float]
|
||||
] = {}
|
||||
|
||||
for full_disp in reversed(overlay_table):
|
||||
(
|
||||
view,
|
||||
viz,
|
||||
y_start,
|
||||
y_min,
|
||||
y_max,
|
||||
read_slc,
|
||||
minor_in_view,
|
||||
r_up,
|
||||
r_dn,
|
||||
) = overlay_table[full_disp]
|
||||
|
||||
key = 'open' if viz.is_ohlc else viz.name
|
||||
xref = minor_in_view[0]['time']
|
||||
match method:
|
||||
# Pin this curve to the "major dispersion" (or other
|
||||
# target) curve:
|
||||
#
|
||||
# - find the intersect datum and then scaling according
|
||||
# to the returns log-lin tranform 'at that intersect
|
||||
# reference data'.
|
||||
# - if the pinning/log-returns-based transform scaling
|
||||
# results in this minor/pinned curve being out of
|
||||
# view, adjust the scalars to match **this** curve's
|
||||
# y-range to stay in view and then backpropagate that
|
||||
# scaling to all curves, including the major-target,
|
||||
# which were previously scaled before.
|
||||
case 'loglin_ref_to_curve':
|
||||
|
||||
# calculate y-range scalars from the earliest
|
||||
# "intersect" datum with the target-major
|
||||
# (dispersion) curve so as to "pin" the curves
|
||||
# in the y-domain at that spot.
|
||||
# NOTE: there are 2 cases for un-matched support
|
||||
# in x-domain (where one series is shorter then the
|
||||
# other):
|
||||
# => major is longer then minor:
|
||||
# - need to scale the minor *from* the first
|
||||
# supported datum in both series.
|
||||
#
|
||||
# => major is shorter then minor:
|
||||
# - need to scale the minor *from* the first
|
||||
# supported datum in both series (the
|
||||
# intersect x-value) but using the
|
||||
# intersecting point from the minor **not**
|
||||
# its first value in view!
|
||||
yref = y_start
|
||||
|
||||
if mx_xref > xref:
|
||||
(
|
||||
xref_pin,
|
||||
yref,
|
||||
) = viz.i_from_t(
|
||||
mx_xref,
|
||||
return_y=True,
|
||||
)
|
||||
xref_pin_dt = pendulum.from_timestamp(xref_pin)
|
||||
xref = mx_xref
|
||||
|
||||
if debug_print:
|
||||
print(
|
||||
'MAJOR SHORTER!!!\n'
|
||||
f'xref: {xref}\n'
|
||||
f'xref_pin: {xref_pin}\n'
|
||||
f'xref_pin-dt: {xref_pin_dt}\n'
|
||||
f'yref@xref_pin: {yref}\n'
|
||||
)
|
||||
|
||||
# XXX: we need to handle not-in-view cases?
|
||||
# still not sure why or when tf this happens..
|
||||
mx_scalars = mx_viz.scalars_from_index(xref)
|
||||
if mx_scalars is None:
|
||||
continue
|
||||
(
|
||||
i_start,
|
||||
y_ref_major,
|
||||
r_up_from_major_at_xref,
|
||||
r_down_from_major_at_xref,
|
||||
) = mx_scalars
|
||||
|
||||
if debug_print:
|
||||
print(
|
||||
'MAJOR PIN SCALING\n'
|
||||
f'mx_xref: {mx_xref}\n'
|
||||
f'major i_start: {i_start}\n'
|
||||
f'y_ref_major: {y_ref_major}\n'
|
||||
f'r_up_from_major_at_xref '
|
||||
f'{r_up_from_major_at_xref}\n'
|
||||
f'r_down_from_major_at_xref: '
|
||||
f'{r_down_from_major_at_xref}\n'
|
||||
f'-----to minor-----\n'
|
||||
f'xref: {xref}\n'
|
||||
f'y_start: {y_start}\n'
|
||||
f'yref: {yref}\n'
|
||||
)
|
||||
ymn = yref * (1 + r_down_from_major_at_xref)
|
||||
ymx = yref * (1 + r_up_from_major_at_xref)
|
||||
|
||||
# if this curve's y-range is detected as **not
|
||||
# being in view** after applying the
|
||||
# target-major's transform, adjust the
|
||||
# target-major curve's range to (log-linearly)
|
||||
# include it (the extra missing range) by
|
||||
# adjusting the y-mxmn to this new y-range and
|
||||
# applying the inverse transform of the minor
|
||||
# back on the target-major (and possibly any
|
||||
# other previously-scaled-to-target/major, minor
|
||||
# curves).
|
||||
if ymn >= y_min:
|
||||
ymn = y_min
|
||||
r_dn_minor = (ymn - yref) / yref
|
||||
|
||||
# rescale major curve's y-max to include new
|
||||
# range increase required by **this minor**.
|
||||
mx_ymn = y_ref_major * (1 + r_dn_minor)
|
||||
mx_viz.vs.yrange = mx_ymn, mx_viz.vs.yrange[1]
|
||||
|
||||
if debug_print:
|
||||
print(
|
||||
f'RESCALE {mx_viz.name} DUE TO {viz.name} '
|
||||
f'ymn -> {y_min}\n'
|
||||
f'-> MAJ ymn (w r_down: {r_dn_minor}) '
|
||||
f'-> {mx_ymn}\n\n'
|
||||
)
|
||||
# rescale all already scaled curves to new
|
||||
# increased range for this side as
|
||||
# determined by ``y_min`` staying in view;
|
||||
# re-set the `scaled: dict` entry to
|
||||
# ensure that this minor curve will be
|
||||
# entirely in view.
|
||||
# TODO: re updating already-scaled minor curves
|
||||
# - is there a faster way to do this by
|
||||
# mutating state on some object instead?
|
||||
for _view in scaled:
|
||||
_viz, _yref, _ymn, _ymx, _xref = scaled[_view]
|
||||
(
|
||||
_,
|
||||
_,
|
||||
_,
|
||||
r_down_from_out_of_range,
|
||||
) = mx_viz.scalars_from_index(_xref)
|
||||
|
||||
new_ymn = _yref * (1 + r_down_from_out_of_range)
|
||||
|
||||
scaled[_view] = (
|
||||
_viz, _yref, new_ymn, _ymx, _xref)
|
||||
|
||||
if debug_print:
|
||||
print(
|
||||
f'RESCALE {_viz.name} ymn -> {new_ymn}'
|
||||
f'RESCALE MAJ ymn -> {mx_ymn}'
|
||||
)
|
||||
|
||||
# same as above but for minor being out-of-range
|
||||
# on the upside.
|
||||
if ymx <= y_max:
|
||||
ymx = y_max
|
||||
r_up_minor = (ymx - yref) / yref
|
||||
mx_ymx = y_ref_major * (1 + r_up_minor)
|
||||
mx_viz.vs.yrange = mx_viz.vs.yrange[0], mx_ymx
|
||||
|
||||
if debug_print:
|
||||
print(
|
||||
f'RESCALE {mx_viz.name} DUE TO {viz.name} '
|
||||
f'ymx -> {y_max}\n'
|
||||
f'-> MAJ ymx (r_up: {r_up_minor} '
|
||||
f'-> {mx_ymx}\n\n'
|
||||
)
|
||||
|
||||
for _view in scaled:
|
||||
_viz, _yref, _ymn, _ymx, _xref = scaled[_view]
|
||||
(
|
||||
_,
|
||||
_,
|
||||
r_up_from_out_of_range,
|
||||
_,
|
||||
) = mx_viz.scalars_from_index(_xref)
|
||||
|
||||
new_ymx = _yref * (1 + r_up_from_out_of_range)
|
||||
scaled[_view] = (
|
||||
_viz, _yref, _ymn, new_ymx, _xref)
|
||||
|
||||
if debug_print:
|
||||
print(
|
||||
f'RESCALE {_viz.name} ymn -> {new_ymx}'
|
||||
)
|
||||
|
||||
# register all overlays for a final pass where we
|
||||
# apply all pinned-curve y-range transform scalings.
|
||||
scaled[view] = (viz, yref, ymn, ymx, xref)
|
||||
|
||||
if debug_print:
|
||||
print(
|
||||
f'Viz[{viz.name}]: @ {chart_name}\n'
|
||||
f' .yrange = {viz.vs.yrange}\n'
|
||||
f' .xrange = {viz.vs.xrange}\n\n'
|
||||
f'xref: {xref}\n'
|
||||
f'xref-dt: {pendulum.from_timestamp(xref)}\n'
|
||||
f'y_min: {y_min}\n'
|
||||
f'y_max: {y_max}\n'
|
||||
f'RESCALING\n'
|
||||
f'r dn: {r_down_from_major_at_xref}\n'
|
||||
f'r up: {r_up_from_major_at_xref}\n'
|
||||
f'ymn: {ymn}\n'
|
||||
f'ymx: {ymx}\n'
|
||||
)
|
||||
|
||||
# Pin all curves by their first datum in view to all
|
||||
# others such that each curve's earliest datum provides the
|
||||
# reference point for returns vs. every other curve in
|
||||
# view.
|
||||
case 'loglin_ref_to_first':
|
||||
ymn = dnt.apply_r(y_start)
|
||||
ymx = upt.apply_r(y_start)
|
||||
view._set_yrange(yrange=(ymn, ymx))
|
||||
|
||||
# Do not pin curves by log-linearizing their y-ranges,
|
||||
# instead allow each curve to fully scale to the
|
||||
# time-series in view's min and max y-values.
|
||||
case 'mxmn':
|
||||
view._set_yrange(yrange=(y_min, y_max))
|
||||
|
||||
case _:
|
||||
raise RuntimeError(
|
||||
f'overlay ``method`` is invalid `{method}'
|
||||
)
|
||||
|
||||
# __ END OF transform calc phase (loop) __
|
||||
|
||||
# finally, scale the major target/dispersion curve to
|
||||
# the (possibly re-scaled/modified) values were set in
|
||||
# transform phase loop.
|
||||
mx_view._set_yrange(yrange=(mx_ymn, mx_ymx))
|
||||
|
||||
if scaled:
|
||||
if debug_print:
|
||||
print(
|
||||
'SCALING PHASE' + '-'*100 + '\n\n'
|
||||
'_________MAJOR INFO___________\n'
|
||||
f'SIGMA MAJOR C: {mx_viz.name} -> {mx_disp}\n'
|
||||
f'UP MAJOR C: {upt.viz.name} with disp: {upt.rng}\n'
|
||||
f'DOWN MAJOR C: {dnt.viz.name} with disp: {dnt.rng}\n'
|
||||
f'xref: {mx_xref}\n'
|
||||
f'xref-dt: {pendulum.from_timestamp(mx_xref)}\n'
|
||||
f'dn: {r_dn_mn}\n'
|
||||
f'up: {r_up_mx}\n'
|
||||
f'mx_ymn: {mx_ymn}\n'
|
||||
f'mx_ymx: {mx_ymx}\n'
|
||||
'------------------------------'
|
||||
)
|
||||
|
||||
for (
|
||||
view,
|
||||
(viz, yref, ymn, ymx, xref)
|
||||
) in scaled.items():
|
||||
|
||||
# NOTE XXX: we have to set each curve's range once (and
|
||||
# ONLY ONCE) here since we're doing this entire routine
|
||||
# inside of a single render cycle (and apparently calling
|
||||
# `ViewBox.setYRange()` multiple times within one only takes
|
||||
# the first call as serious...) XD
|
||||
view._set_yrange(yrange=(ymn, ymx))
|
||||
profiler(f'{viz.name}@{chart_name} log-SCALE minor')
|
||||
|
||||
if debug_print:
|
||||
print(
|
||||
'_________MINOR INFO___________\n'
|
||||
f'Viz[{viz.name}]: @ {chart_name}\n'
|
||||
f' .yrange = {viz.vs.yrange}\n'
|
||||
f' .xrange = {viz.vs.xrange}\n\n'
|
||||
f'xref: {xref}\n'
|
||||
f'xref-dt: {pendulum.from_timestamp(xref)}\n'
|
||||
f'y_start: {y_start}\n'
|
||||
f'y min: {y_min}\n'
|
||||
f'y max: {y_max}\n'
|
||||
f'T scaled ymn: {ymn}\n'
|
||||
f'T scaled ymx: {ymx}\n\n'
|
||||
'--------------------------------\n'
|
||||
)
|
||||
|
||||
# __ END OF overlay scale phase (loop) __
|
||||
|
||||
if debug_print:
|
||||
print(
|
||||
f'END UX GRAPHICS CYCLE: @{chart_name}\n'
|
||||
+
|
||||
divstr
|
||||
+
|
||||
'\n'
|
||||
)
|
||||
|
||||
profiler(f'<{chart_name}>.interact_graphics_cycle()')
|
||||
|
||||
if not do_linked_charts:
|
||||
break
|
||||
|
||||
profiler.finish()
|
Loading…
Reference in New Issue