Compare commits

...

10 Commits

Author SHA1 Message Date
Tyler Goodlet 760c752641 Set a `PlotItem.viz` for interaction lookup
Inside `._interaction` routines we need access to `Viz` instances.
Instead of doing `CharPlotWidget._vizs: dict` lookups this ensures each
plot can lookup it's (parent) viz without error.

Also, adjusts `Viz.maxmin()` output parsing to new signature.
2023-01-15 23:53:57 -05:00
Tyler Goodlet 9826ddaa9a Always cache `read_slc` alongside y-mnmx values 2023-01-15 23:15:11 -05:00
Tyler Goodlet eba8488926 Add first-draft `PlotItemOverlay.group_maxmin()`
Computes the maxmin values for each underlying plot's in-view range as
well as the max up/down swing (in percentage terms) from the plot with
most dispersion and returns a all these values plus a `dict` of plots to
their ranges as part of output.
2023-01-15 13:32:22 -05:00
Tyler Goodlet 4efe875f1b Add back coord-caching to ohlc graphic 2023-01-15 13:23:31 -05:00
Tyler Goodlet 4568be884b Use (modern) literal type annots in view code 2023-01-14 16:25:02 -05:00
Tyler Goodlet 9d3de6ec02 Drop x-range query from `ChartPlotWidget.maxmin()`
Move the `Viz.datums_range()` call into `Viz.maxmin()` itself thus
minimizing the chart `.maxmin()` method to an ultra light wrapper around
the viz call. Also move all profiling into the `Viz` method.

Adjust `Viz.maxmin()` to return both the (rounded) x-range values which
correspond to the range containing the y-domain min and max so that
it can be used for up and coming overlay group maxmin calcs.
2023-01-14 16:11:25 -05:00
Tyler Goodlet 53c9332e60 Drop multi mxmn from display mod 2023-01-14 13:54:19 -05:00
Tyler Goodlet e57a2649d1 Only handle hist discrepancies when market is open
We obviously don't want to be debugging a sample-index issue if/when the
market for the asset is closed (since we'll be guaranteed to have
a mismatch, lul). Pass in the `feed_is_live: trio.Event` throughout the
backfilling routines to allow first checking for the live feed being active
so as to avoid breakpointing on false +ves. Also, add a detailed warning
log message for when *actually* investigating a mismatch.
2023-01-13 18:57:20 -05:00
Tyler Goodlet 23e1ecbb04 Passthrough `tractor` kwargs directly 2023-01-13 18:51:04 -05:00
Tyler Goodlet 664a15e02d Fix `open_trade_ledger()` enter value type annot 2023-01-13 18:50:25 -05:00
9 changed files with 250 additions and 117 deletions

View File

@ -257,7 +257,7 @@ async def open_piker_runtime(
# and spawn the service tree distributed per that. # and spawn the service tree distributed per that.
start_method: str = 'trio', start_method: str = 'trio',
tractor_kwargs: dict = {}, **tractor_kwargs,
) -> tuple[ ) -> tuple[
tractor.Actor, tractor.Actor,

View File

@ -207,7 +207,7 @@ def get_feed_bus(
) -> _FeedsBus: ) -> _FeedsBus:
''' '''
Retreive broker-daemon-local data feeds bus from process global Retrieve broker-daemon-local data feeds bus from process global
scope. Serialize task access to lock. scope. Serialize task access to lock.
''' '''
@ -250,6 +250,7 @@ async def start_backfill(
shm: ShmArray, shm: ShmArray,
timeframe: float, timeframe: float,
sampler_stream: tractor.MsgStream, sampler_stream: tractor.MsgStream,
feed_is_live: trio.Event,
last_tsdb_dt: Optional[datetime] = None, last_tsdb_dt: Optional[datetime] = None,
storage: Optional[Storage] = None, storage: Optional[Storage] = None,
@ -281,9 +282,30 @@ async def start_backfill(
- pendulum.from_timestamp(times[-2]) - pendulum.from_timestamp(times[-2])
).seconds ).seconds
if step_size_s == 60: # if the market is open (aka we have a live feed) but the
# history sample step index seems off we report the surrounding
# data and drop into a bp. this case shouldn't really ever
# happen if we're doing history retrieval correctly.
if (
step_size_s == 60
and feed_is_live.is_set()
):
inow = round(time.time()) inow = round(time.time())
if (inow - times[-1]) > 60: diff = inow - times[-1]
if abs(diff) > 60:
surr = array[-6:]
diff_in_mins = round(diff/60., ndigits=2)
log.warning(
f'STEP ERROR `{bfqsn}` for period {step_size_s}s:\n'
f'Off by `{diff}` seconds (or `{diff_in_mins}` mins)\n'
'Surrounding 6 time stamps:\n'
f'{list(surr["time"])}\n'
'Here is surrounding 6 samples:\n'
f'{surr}\nn'
)
# for now we expect a hacker to investigate this case
# manually..
await tractor.breakpoint() await tractor.breakpoint()
# frame's worth of sample-period-steps, in seconds # frame's worth of sample-period-steps, in seconds
@ -485,6 +507,7 @@ async def basic_backfill(
bfqsn: str, bfqsn: str,
shms: dict[int, ShmArray], shms: dict[int, ShmArray],
sampler_stream: tractor.MsgStream, sampler_stream: tractor.MsgStream,
feed_is_live: trio.Event,
) -> None: ) -> None:
@ -504,6 +527,7 @@ async def basic_backfill(
shm, shm,
timeframe, timeframe,
sampler_stream, sampler_stream,
feed_is_live,
) )
) )
except DataUnavailable: except DataUnavailable:
@ -520,6 +544,7 @@ async def tsdb_backfill(
bfqsn: str, bfqsn: str,
shms: dict[int, ShmArray], shms: dict[int, ShmArray],
sampler_stream: tractor.MsgStream, sampler_stream: tractor.MsgStream,
feed_is_live: trio.Event,
task_status: TaskStatus[ task_status: TaskStatus[
tuple[ShmArray, ShmArray] tuple[ShmArray, ShmArray]
@ -554,6 +579,8 @@ async def tsdb_backfill(
shm, shm,
timeframe, timeframe,
sampler_stream, sampler_stream,
feed_is_live,
last_tsdb_dt=last_tsdb_dt, last_tsdb_dt=last_tsdb_dt,
tsdb_is_up=True, tsdb_is_up=True,
storage=storage, storage=storage,
@ -856,6 +883,7 @@ async def manage_history(
60: hist_shm, 60: hist_shm,
}, },
sample_stream, sample_stream,
feed_is_live,
) )
# yield back after client connect with filled shm # yield back after client connect with filled shm
@ -890,6 +918,7 @@ async def manage_history(
60: hist_shm, 60: hist_shm,
}, },
sample_stream, sample_stream,
feed_is_live,
) )
task_status.started(( task_status.started((
hist_zero_index, hist_zero_index,
@ -1051,7 +1080,10 @@ async def allocate_persistent_feed(
# seed the buffer with a history datum - this is most handy # seed the buffer with a history datum - this is most handy
# for many backends which don't sample @ 1s OHLC but do have # for many backends which don't sample @ 1s OHLC but do have
# slower data such as 1m OHLC. # slower data such as 1m OHLC.
if not len(rt_shm.array): if (
not len(rt_shm.array)
and hist_shm.array.size
):
rt_shm.push(hist_shm.array[-3:-1]) rt_shm.push(hist_shm.array[-3:-1])
ohlckeys = ['open', 'high', 'low', 'close'] ohlckeys = ['open', 'high', 'low', 'close']
rt_shm.array[ohlckeys][-2:] = hist_shm.array['close'][-1] rt_shm.array[ohlckeys][-2:] = hist_shm.array['close'][-1]
@ -1062,6 +1094,9 @@ async def allocate_persistent_feed(
rt_shm.array['time'][0] = ts rt_shm.array['time'][0] = ts
rt_shm.array['time'][1] = ts + 1 rt_shm.array['time'][1] = ts + 1
elif hist_shm.array.size == 0:
await tractor.breakpoint()
# wait the spawning parent task to register its subscriber # wait the spawning parent task to register its subscriber
# send-stream entry before we start the sample loop. # send-stream entry before we start the sample loop.
await sub_registered.wait() await sub_registered.wait()

View File

@ -54,7 +54,7 @@ def open_trade_ledger(
broker: str, broker: str,
account: str, account: str,
) -> str: ) -> dict:
''' '''
Indempotently create and read in a trade log file from the Indempotently create and read in a trade log file from the
``<configuration_dir>/ledgers/`` directory. ``<configuration_dir>/ledgers/`` directory.

View File

@ -50,7 +50,6 @@ from ._cursor import (
ContentsLabel, ContentsLabel,
) )
from ..data._sharedmem import ShmArray from ..data._sharedmem import ShmArray
from ._l1 import L1Labels
from ._ohlc import BarItems from ._ohlc import BarItems
from ._curve import ( from ._curve import (
Curve, Curve,
@ -70,12 +69,10 @@ from ..data._source import Symbol
from ..log import get_logger from ..log import get_logger
from ._interaction import ChartView from ._interaction import ChartView
from ._forms import FieldsForm from ._forms import FieldsForm
from .._profile import pg_profile_enabled, ms_slower_then
from ._overlay import PlotItemOverlay from ._overlay import PlotItemOverlay
from ._dataviz import Viz from ._dataviz import Viz
from ._search import SearchWidget from ._search import SearchWidget
from . import _pg_overrides as pgo from . import _pg_overrides as pgo
from .._profile import Profiler
if TYPE_CHECKING: if TYPE_CHECKING:
from ._display import DisplayState from ._display import DisplayState
@ -632,11 +629,13 @@ class LinkedSplits(QWidget):
for axis in axes.values(): for axis in axes.values():
axis.pi = cpw.plotItem axis.pi = cpw.plotItem
cpw.hideAxis('left') cpw.hideAxis('left')
cpw.hideAxis('bottom') cpw.hideAxis('bottom')
if ( if (
_xaxis_at == 'bottom' and ( _xaxis_at == 'bottom'
and (
self.xaxis_chart self.xaxis_chart
or ( or (
not self.subplots not self.subplots
@ -644,6 +643,8 @@ class LinkedSplits(QWidget):
) )
) )
): ):
# hide the previous x-axis chart's bottom axis since we're
# presumably being appended to the bottom subplot.
if self.xaxis_chart: if self.xaxis_chart:
self.xaxis_chart.hideAxis('bottom') self.xaxis_chart.hideAxis('bottom')
@ -688,7 +689,12 @@ class LinkedSplits(QWidget):
# link chart x-axis to main chart # link chart x-axis to main chart
# this is 1/2 of where the `Link` in ``LinkedSplit`` # this is 1/2 of where the `Link` in ``LinkedSplit``
# comes from ;) # comes from ;)
cpw.setXLink(self.chart) cpw.cv.setXLink(self.chart)
# NOTE: above is the same as the following,
# link this subchart's axes to the main top level chart.
# if self.chart:
# cpw.cv.linkView(0, self.chart.cv)
add_label = False add_label = False
anchor_at = ('top', 'left') anchor_at = ('top', 'left')
@ -800,7 +806,9 @@ class LinkedSplits(QWidget):
# write our own wrapper around `PlotItem`.. # write our own wrapper around `PlotItem`..
class ChartPlotWidget(pg.PlotWidget): class ChartPlotWidget(pg.PlotWidget):
''' '''
``GraphicsView`` subtype containing a single ``PlotItem``. ``GraphicsView`` subtype containing a ``.plotItem: PlotItem`` as well
as a `.pi_overlay: PlotItemOverlay`` which helps manage and overlay flow
graphics view multiple compose view boxes.
- The added methods allow for plotting OHLC sequences from - The added methods allow for plotting OHLC sequences from
``np.ndarray``s with appropriate field names. ``np.ndarray``s with appropriate field names.
@ -857,17 +865,17 @@ class ChartPlotWidget(pg.PlotWidget):
self.sidepane: Optional[FieldsForm] = None self.sidepane: Optional[FieldsForm] = None
# source of our custom interactions # source of our custom interactions
self.cv = cv = self.mk_vb(name) self.cv = self.mk_vb(name)
pi = pgo.PlotItem( pi = pgo.PlotItem(
viewBox=cv, viewBox=self.cv,
name=name, name=name,
**kwargs, **kwargs,
) )
pi.chart_widget = self pi.chart_widget = self
super().__init__( super().__init__(
background=hcolor(view_color), background=hcolor(view_color),
viewBox=cv, viewBox=self.cv,
# parent=None, # parent=None,
# plotItem=None, # plotItem=None,
# antialias=True, # antialias=True,
@ -878,7 +886,9 @@ class ChartPlotWidget(pg.PlotWidget):
# give viewbox as reference to chart # give viewbox as reference to chart
# allowing for kb controls and interactions on **this** widget # allowing for kb controls and interactions on **this** widget
# (see our custom view mode in `._interactions.py`) # (see our custom view mode in `._interactions.py`)
cv.chart = self self.cv.chart = self
self.pi_overlay: PlotItemOverlay = PlotItemOverlay(self.plotItem)
# ensure internal pi matches # ensure internal pi matches
assert self.cv is self.plotItem.vb assert self.cv is self.plotItem.vb
@ -907,8 +917,6 @@ class ChartPlotWidget(pg.PlotWidget):
# show background grid # show background grid
self.showGrid(x=False, y=True, alpha=0.3) self.showGrid(x=False, y=True, alpha=0.3)
self.pi_overlay: PlotItemOverlay = PlotItemOverlay(self.plotItem)
# indempotent startup flag for auto-yrange subsys # indempotent startup flag for auto-yrange subsys
# to detect the "first time" y-domain graphics begin # to detect the "first time" y-domain graphics begin
# to be shown in the (main) graphics view. # to be shown in the (main) graphics view.
@ -1101,6 +1109,7 @@ class ChartPlotWidget(pg.PlotWidget):
# view **to** this parent and likewise *from* the # view **to** this parent and likewise *from* the
# main/parent chart back *to* the created overlay. # main/parent chart back *to* the created overlay.
cv.enable_auto_yrange(src_vb=self.view) cv.enable_auto_yrange(src_vb=self.view)
# makes it so that interaction on the new overlay will reflect # makes it so that interaction on the new overlay will reflect
# back on the main chart (which overlay was added to). # back on the main chart (which overlay was added to).
self.view.enable_auto_yrange(src_vb=cv) self.view.enable_auto_yrange(src_vb=cv)
@ -1175,6 +1184,7 @@ class ChartPlotWidget(pg.PlotWidget):
# register curve graphics with this viz # register curve graphics with this viz
graphics=graphics, graphics=graphics,
) )
pi.viz = viz
assert isinstance(viz.shm, ShmArray) assert isinstance(viz.shm, ShmArray)
# TODO: this probably needs its own method? # TODO: this probably needs its own method?
@ -1302,13 +1312,6 @@ class ChartPlotWidget(pg.PlotWidget):
If ``bars_range`` is provided use that range. If ``bars_range`` is provided use that range.
''' '''
profiler = Profiler(
msg=f'`{str(self)}.maxmin(name={name})`: `{self.name}`',
disabled=not pg_profile_enabled(),
ms_threshold=ms_slower_then,
delayed=True,
)
# TODO: here we should instead look up the ``Viz.shm.array`` # TODO: here we should instead look up the ``Viz.shm.array``
# and read directly from shm to avoid copying to memory first # and read directly from shm to avoid copying to memory first
# and then reading it again here. # and then reading it again here.
@ -1316,36 +1319,21 @@ class ChartPlotWidget(pg.PlotWidget):
viz = self._vizs.get(viz_key) viz = self._vizs.get(viz_key)
if viz is None: if viz is None:
log.error(f"viz {viz_key} doesn't exist in chart {self.name} !?") log.error(f"viz {viz_key} doesn't exist in chart {self.name} !?")
key = res = 0, 0 return 0, 0
else: res = viz.maxmin()
(
l,
_,
lbar,
rbar,
_,
r,
) = bars_range or viz.datums_range()
profiler(f'{self.name} got bars range')
key = lbar, rbar
res = viz.maxmin(*key)
if ( if (
res is None res is None
): ):
log.warning( mxmn = 0, 0
f"{viz_key} no mxmn for bars_range => {key} !?"
)
res = 0, 0
if not self._on_screen: if not self._on_screen:
self.default_view(do_ds=False) self.default_view(do_ds=False)
self._on_screen = True self._on_screen = True
else:
x_range, read_slc, mxmn = res
profiler(f'yrange mxmn: {key} -> {res}') return mxmn
# print(f'{viz_key} yrange mxmn: {key} -> {res}')
return res
def get_viz( def get_viz(
self, self,

View File

@ -60,6 +60,7 @@ from ..log import get_logger
from .._profile import ( from .._profile import (
Profiler, Profiler,
pg_profile_enabled, pg_profile_enabled,
ms_slower_then,
) )
@ -276,7 +277,10 @@ class Viz(msgspec.Struct): # , frozen=True):
] = (None, None) ] = (None, None)
# cache of y-range values per x-range input. # cache of y-range values per x-range input.
_mxmns: dict[tuple[int, int], tuple[float, float]] = {} _mxmns: dict[
tuple[int, int],
tuple[float, float],
] = {}
@property @property
def shm(self) -> ShmArray: def shm(self) -> ShmArray:
@ -324,32 +328,29 @@ class Viz(msgspec.Struct): # , frozen=True):
def maxmin( def maxmin(
self, self,
lbar: int,
rbar: int,
# TODO: drop this right?
bars_range: Optional[tuple[
int, int, int, int, int, int
]] = None,
x_range: slice | tuple[int, int] | None = None,
use_caching: bool = True, use_caching: bool = True,
) -> Optional[tuple[float, float]]: ) -> tuple[float, float] | None:
''' '''
Compute the cached max and min y-range values for a given Compute the cached max and min y-range values for a given
x-range determined by ``lbar`` and ``rbar`` or ``None`` x-range determined by ``lbar`` and ``rbar`` or ``None``
if no range can be determined (yet). if no range can be determined (yet).
''' '''
# TODO: hash the slice instead maybe? name = self.name
# https://stackoverflow.com/a/29980872 profiler = Profiler(
rkey = (round(lbar), round(rbar)) msg=f'{name} -> `{str(self)}.maxmin()`',
disabled=not pg_profile_enabled(),
do_print: bool = False ms_threshold=ms_slower_then,
if use_caching: delayed=True,
cached_result = self._mxmns.get(rkey)
if cached_result:
if do_print:
print(
f'{self.name} CACHED maxmin\n'
f'{rkey} -> {cached_result}'
) )
return cached_result
shm = self.shm shm = self.shm
if shm is None: if shm is None:
@ -357,6 +358,43 @@ class Viz(msgspec.Struct): # , frozen=True):
arr = shm.array arr = shm.array
if x_range is None:
(
l,
_,
lbar,
rbar,
_,
r,
) = (
# TODO: drop this yah?
bars_range
or self.datums_range()
)
profiler(f'{self.name} got bars range')
x_range = lbar, rbar
# TODO: hash the slice instead maybe?
# https://stackoverflow.com/a/29980872
ixrng = (round(lbar), round(rbar))
do_print: bool = False
if use_caching:
cached_result = self._mxmns.get(ixrng)
if cached_result:
if do_print:
print(
f'{self.name} CACHED maxmin\n'
f'{ixrng} -> {cached_result}'
)
read_slc, mxmn = cached_result
return (
ixrng,
read_slc,
mxmn,
)
# get relative slice indexes into array # get relative slice indexes into array
if self.index_field == 'time': if self.index_field == 'time':
read_slc = slice_from_time( read_slc = slice_from_time(
@ -376,7 +414,10 @@ class Viz(msgspec.Struct): # , frozen=True):
slice_view = arr[read_slc] slice_view = arr[read_slc]
if not slice_view.size: if not slice_view.size:
log.warning(f'{self.name} no maxmin in view?') log.warning(
f'{self.name} no maxmin in view?\n'
f"{name} no mxmn for bars_range => {ixrng} !?"
)
return None return None
elif self.yrange: elif self.yrange:
@ -384,9 +425,8 @@ class Viz(msgspec.Struct): # , frozen=True):
if do_print: if do_print:
print( print(
f'{self.name} M4 maxmin:\n' f'{self.name} M4 maxmin:\n'
f'{rkey} -> {mxmn}' f'{ixrng} -> {mxmn}'
) )
else: else:
if self.is_ohlc: if self.is_ohlc:
ylow = np.min(slice_view['low']) ylow = np.min(slice_view['low'])
@ -404,7 +444,7 @@ class Viz(msgspec.Struct): # , frozen=True):
s = 3 s = 3
print( print(
f'{self.name} MANUAL ohlc={self.is_ohlc} maxmin:\n' f'{self.name} MANUAL ohlc={self.is_ohlc} maxmin:\n'
f'{rkey} -> {mxmn}\n' f'{ixrng} -> {mxmn}\n'
f'read_slc: {read_slc}\n' f'read_slc: {read_slc}\n'
# f'abs_slc: {slice_view["index"]}\n' # f'abs_slc: {slice_view["index"]}\n'
f'first {s}:\n{slice_view[:s]}\n' f'first {s}:\n{slice_view[:s]}\n'
@ -413,9 +453,13 @@ class Viz(msgspec.Struct): # , frozen=True):
# cache result for input range # cache result for input range
assert mxmn assert mxmn
self._mxmns[rkey] = mxmn self._mxmns[ixrng] = (read_slc, mxmn)
profiler(f'yrange mxmn cacheing: {x_range} -> {mxmn}')
return mxmn return (
ixrng,
read_slc,
mxmn,
)
def view_range(self) -> tuple[int, int]: def view_range(self) -> tuple[int, int]:
''' '''
@ -985,12 +1029,10 @@ class Viz(msgspec.Struct): # , frozen=True):
) )
if do_ds: if do_ds:
# view.interaction_graphics_update_cycle()
view.maybe_downsample_graphics() view.maybe_downsample_graphics()
view._set_yrange() view._set_yrange()
# caller should do this!
# self.linked.graphics_cycle()
def incr_info( def incr_info(
self, self,
ds: DisplayState, ds: DisplayState,

View File

@ -986,32 +986,6 @@ async def link_views_with_region(
# region.sigRegionChangeFinished.connect(update_pi_from_region) # region.sigRegionChangeFinished.connect(update_pi_from_region)
# force 0 to always be in view
def multi_maxmin(
chart: ChartPlotWidget,
names: list[str],
) -> tuple[float, float]:
'''
Viz "group" maxmin loop; assumes all named vizs
are in the same co-domain and thus can be sorted
as one set.
Iterates all the named vizs and calls the chart
api to find their range values and return.
TODO: really we should probably have a more built-in API
for this?
'''
mx = 0
for name in names:
ymn, ymx = chart.maxmin(name=name)
mx = max(mx, ymx)
return 0, mx
_quote_throttle_rate: int = 60 - 6 _quote_throttle_rate: int = 60 - 6

View File

@ -21,7 +21,11 @@ Chart view box primitives
from __future__ import annotations from __future__ import annotations
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import time import time
from typing import Optional, Callable from typing import (
Optional,
Callable,
TYPE_CHECKING,
)
import pyqtgraph as pg import pyqtgraph as pg
# from pyqtgraph.GraphicsScene import mouseEvents # from pyqtgraph.GraphicsScene import mouseEvents
@ -39,6 +43,9 @@ from .._profile import pg_profile_enabled, ms_slower_then
from ._editors import SelectRect from ._editors import SelectRect
from . import _event from . import _event
if TYPE_CHECKING:
from ._chart import ChartPlotWidget
log = get_logger(__name__) log = get_logger(__name__)
@ -374,7 +381,7 @@ class ChartView(ViewBox):
) )
self.linked = None self.linked = None
self._chart: 'ChartPlotWidget' = None # noqa self._chart: ChartPlotWidget | None = None # noqa
# add our selection box annotator # add our selection box annotator
self.select_box = SelectRect(self) self.select_box = SelectRect(self)
@ -445,11 +452,11 @@ class ChartView(ViewBox):
yield self yield self
@property @property
def chart(self) -> 'ChartPlotWidget': # type: ignore # noqa def chart(self) -> ChartPlotWidget: # type: ignore # noqa
return self._chart return self._chart
@chart.setter @chart.setter
def chart(self, chart: 'ChartPlotWidget') -> None: # type: ignore # noqa def chart(self, chart: ChartPlotWidget) -> None: # type: ignore # noqa
self._chart = chart self._chart = chart
self.select_box.chart = chart self.select_box.chart = chart
if self._maxmin is None: if self._maxmin is None:
@ -783,11 +790,9 @@ class ChartView(ViewBox):
if yrange is None: if yrange is None:
log.warning(f'No yrange provided for {name}!?') log.warning(f'No yrange provided for {name}!?')
print(f"WTF NO YRANGE {name}")
return return
ylow, yhigh = yrange ylow, yhigh = yrange
profiler(f'callback ._maxmin(): {yrange}') profiler(f'callback ._maxmin(): {yrange}')
# view margins: stay within a % of the "true range" # view margins: stay within a % of the "true range"

View File

@ -121,7 +121,7 @@ class BarItems(FlowGraphic):
# XXX: causes this weird jitter bug when click-drag panning # XXX: causes this weird jitter bug when click-drag panning
# where the path curve will awkwardly flicker back and forth? # where the path curve will awkwardly flicker back and forth?
# self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
self.path = QPainterPath() self.path = QPainterPath()
self._last_bar_lines: tuple[QLineF, ...] | None = None self._last_bar_lines: tuple[QLineF, ...] | None = None

View File

@ -22,7 +22,6 @@ from collections import defaultdict
from functools import partial from functools import partial
from typing import ( from typing import (
Callable, Callable,
Optional,
) )
from pyqtgraph.graphicsItems.AxisItem import AxisItem from pyqtgraph.graphicsItems.AxisItem import AxisItem
@ -246,7 +245,7 @@ class ComposedGridLayout:
plot: PlotItem, plot: PlotItem,
name: str, name: str,
) -> Optional[AxisItem]: ) -> AxisItem | None:
''' '''
Retrieve the named axis for overlayed ``plot`` or ``None`` Retrieve the named axis for overlayed ``plot`` or ``None``
if axis for that name is not shown. if axis for that name is not shown.
@ -321,7 +320,7 @@ class PlotItemOverlay:
def add_plotitem( def add_plotitem(
self, self,
plotitem: PlotItem, plotitem: PlotItem,
index: Optional[int] = None, index: int | None = None,
# event/signal names which will be broadcasted to all added # event/signal names which will be broadcasted to all added
# (relayee) ``PlotItem``s (eg. ``ViewBox.mouseDragEvent``). # (relayee) ``PlotItem``s (eg. ``ViewBox.mouseDragEvent``).
@ -376,7 +375,7 @@ class PlotItemOverlay:
# TODO: drop this viewbox specific input and # TODO: drop this viewbox specific input and
# allow a predicate to be passed in by user. # allow a predicate to be passed in by user.
axis: 'Optional[int]' = None, axis: int | None = None,
*, *,
@ -578,3 +577,93 @@ class PlotItemOverlay:
# ''' # '''
# ... # ...
def group_maxmin(
self,
focus_around: str | None = None,
force_min: float | None = None,
) -> tuple[
float, # mn
float, # mx
float, # max range in % terms of highest sigma plot's y-range
PlotItem, # front/selected plot
]:
'''
Overlay "group" maxmin sorting.
Assumes all named flows are in the same co-domain and thus can
be sorted as one set.
Iterates all the named flows and calls the chart api to find
their range values and return.
TODO: really we should probably have a more built-in API for
this?
'''
# TODO:
# - use this in the ``.ui._fsp`` mutli-maxmin stuff
# -
# force 0 to always be in view
group_mx: float = 0
group_mn: float = 0
mx_up_rng: float = 0
mn_down_rng: float = 0
pis2ranges: dict[
PlotItem,
tuple[float, float],
] = {}
for pi in self.overlays:
# TODO: can we remove this from the widget
# and place somewhere more related to UX/Viz?
# name = pi.name
# chartw = pi.chart_widget
viz = pi.viz
# viz = chartw._vizs[name]
out = viz.maxmin()
if out is None:
return None
(
(x_start, x_stop),
read_slc,
(ymn, ymx),
) = out
arr = viz.shm.array
y_start = arr[read_slc.start - 1]
y_stop = arr[read_slc.stop - 1]
if viz.is_ohlc:
y_start = y_start['open']
y_stop = y_stop['close']
else:
y_start = y_start[viz.name]
y_stop = y_stop[viz.name]
# update max for group
up_rng = (ymx - y_start) / y_start
down_rng = (y_stop - ymn) / y_stop
# compute directional (up/down) y-range % swing/dispersion
mx_up_rng = max(mx_up_rng, up_rng)
mn_down_rng = min(mn_down_rng, down_rng)
pis2ranges[pi] = (ymn, ymx)
group_mx = max(group_mx, ymx)
if force_min is None:
group_mn = min(group_mn, ymn)
return (
group_mn if force_min is None else force_min,
group_mx,
mn_down_rng,
mx_up_rng,
pis2ranges,
)