Right, handle y-ranging multiple paths per plot

We were hacking this before using the whole `ChartView._maxmin()`
setting stuff since in some cases you might want similarly ranged paths
on the same view, but of course you need to max/min them together..

This adds that group sorting by using a table of `dict[PlotItem,
tuple[float, float]` and taking the abs highest/lowest value for each
plot in the viz interaction update loop.

Also removes the now commented signal registry calls and thus
`._yranger`, drops the `set_range: bool` from `._set_yrange` and adds
and extra `.maybe_downsample_graphics()` to the mouse wheel handler to
avoid a weird slow debounce where ds-ing is delayed until a further
interaction.
multichartz
Tyler Goodlet 2023-01-19 10:20:29 -05:00
parent f5b15aba11
commit 99fbce3231
1 changed files with 99 additions and 71 deletions

View File

@ -20,7 +20,6 @@ Chart view box primitives
""" """
from __future__ import annotations from __future__ import annotations
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from functools import partial
import time import time
from typing import ( from typing import (
Optional, Optional,
@ -393,7 +392,6 @@ class ChartView(ViewBox):
self.setFocusPolicy(QtCore.Qt.StrongFocus) self.setFocusPolicy(QtCore.Qt.StrongFocus)
self._ic = None self._ic = None
self._yranger: Callable | None = None
# TODO: probably just assign this whenever a new `PlotItem` is # TODO: probably just assign this whenever a new `PlotItem` is
# allocated since they're 1to1 with views.. # allocated since they're 1to1 with views..
@ -568,6 +566,7 @@ class ChartView(ViewBox):
# "double work" is causing latency when these missing event # "double work" is causing latency when these missing event
# fires don't happen? # fires don't happen?
self.maybe_downsample_graphics() self.maybe_downsample_graphics()
self.maybe_downsample_graphics()
ev.accept() ev.accept()
@ -763,7 +762,6 @@ class ChartView(ViewBox):
ms_threshold=ms_slower_then, ms_threshold=ms_slower_then,
delayed=True, delayed=True,
) )
set_range = True
chart = self._chart chart = self._chart
# view has been set in 'axis' mode # view has been set in 'axis' mode
@ -772,8 +770,8 @@ class ChartView(ViewBox):
# - disable autoranging # - disable autoranging
# - remove any y range limits # - remove any y range limits
if chart._static_yrange == 'axis': if chart._static_yrange == 'axis':
set_range = False
self.setLimits(yMin=None, yMax=None) self.setLimits(yMin=None, yMax=None)
return
# static y-range has been set likely by # static y-range has been set likely by
# a specialized FSP configuration. # a specialized FSP configuration.
@ -786,48 +784,63 @@ class ChartView(ViewBox):
elif yrange is not None: elif yrange is not None:
ylow, yhigh = yrange 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 not viz:
# if none is provided as input! breakpoint()
if not yrange:
if not viz: out = viz.maxmin()
breakpoint() if out is None:
log.warning(f'No yrange provided for {name}!?')
return
(
ixrng,
_,
yrange
) = out
out = viz.maxmin() profiler(f'`{self.name}:Viz.maxmin()` -> {ixrng}=>{yrange}')
if out is None:
log.warning(f'No yrange provided for {name}!?')
return
(
ixrng,
_,
yrange
) = out
profiler(f'`{self.name}:Viz.maxmin()` -> {ixrng}=>{yrange}') if yrange is None:
log.warning(f'No yrange provided for {name}!?')
if yrange is None: return
log.warning(f'No yrange provided for {name}!?')
return
ylow, yhigh = yrange ylow, yhigh = yrange
# view margins: stay within a % of the "true range" # view margins: stay within a % of the "true range"
diff = yhigh - ylow diff = yhigh - ylow
ylow = ylow - (diff * range_margin) ylow = max(
yhigh = yhigh + (diff * range_margin) ylow - (diff * range_margin),
0,
)
yhigh = min(
yhigh + (diff * range_margin),
yhigh * (1 + range_margin),
)
# XXX: this often needs to be unset # XXX: this often needs to be unset
# to get different view modes to operate # to get different view modes to operate
# correctly! # correctly!
self.setLimits(
yMin=ylow,
yMax=yhigh,
)
self.setYRange(ylow, yhigh)
profiler(f'set limits: {(ylow, yhigh)}')
profiler(
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,
)
# LOL: yet anothercucking pg buggg..
# can't use `msg=f'setYRange({ylow}, {yhigh}')`
profiler.finish() profiler.finish()
def enable_auto_yrange( def enable_auto_yrange(
@ -844,12 +857,6 @@ class ChartView(ViewBox):
if src_vb is None: if src_vb is None:
src_vb = self src_vb = self
if self._yranger is None:
self._yranger = partial(
self._set_yrange,
viz=viz,
)
# re-sampling trigger: # re-sampling trigger:
# TODO: a smarter way to avoid calling this needlessly? # TODO: a smarter way to avoid calling this needlessly?
# 2 things i can think of: # 2 things i can think of:
@ -866,25 +873,13 @@ class ChartView(ViewBox):
self.maybe_downsample_graphics self.maybe_downsample_graphics
) )
# mouse wheel doesn't emit XRangeChanged
# src_vb.sigRangeChangedManually.connect(self._yranger)
def disable_auto_yrange(self) -> None: def disable_auto_yrange(self) -> None:
# XXX: not entirely sure why we can't de-reg this.. # XXX: not entirely sure why we can't de-reg this..
self.sigResized.disconnect( self.sigResized.disconnect(
# self._yranger,
self.maybe_downsample_graphics self.maybe_downsample_graphics
) )
# self.sigRangeChangedManually.disconnect(
# self._yranger,
# )
# self.sigRangeChangedManually.disconnect(
# self.maybe_downsample_graphics
# )
def x_uppx(self) -> float: def x_uppx(self) -> float:
''' '''
Return the "number of x units" within a single Return the "number of x units" within a single
@ -908,14 +903,18 @@ class ChartView(ViewBox):
): ):
profiler = Profiler( profiler = Profiler(
msg=f'ChartView.maybe_downsample_graphics() for {self.name}', msg=f'ChartView.maybe_downsample_graphics() for {self.name}',
disabled=not pg_profile_enabled(), # disabled=not pg_profile_enabled(),
# ms_threshold=ms_slower_then,
disabled=True,
ms_threshold=4,
# XXX: important to avoid not seeing underlying # XXX: important to avoid not seeing underlying
# ``.update_graphics_from_flow()`` nested profiling likely # ``.update_graphics_from_flow()`` nested profiling likely
# due to the way delaying works and garbage collection of # due to the way delaying works and garbage collection of
# the profiler in the delegated method calls. # the profiler in the delegated method calls.
ms_threshold=6, delayed=True,
# ms_threshold=ms_slower_then,
) )
# TODO: a faster single-loop-iterator way of doing this XD # TODO: a faster single-loop-iterator way of doing this XD
@ -927,20 +926,22 @@ class ChartView(ViewBox):
plots |= linked.subplots plots |= linked.subplots
for chart_name, chart in plots.items(): for chart_name, chart in plots.items():
mxmns: dict[
pg.PlotItem,
tuple[float, float],
] = {}
for name, viz in chart._vizs.items(): for name, viz in chart._vizs.items():
if not viz.render:
if (
not viz.render
# XXX: super important to be aware of this.
# or not flow.graphics.isVisible()
):
# print(f'skipping {flow.name}') # print(f'skipping {flow.name}')
continue continue
# pass in no array which will read and render from the last # pass in no array which will read and render from the last
# passed array (normally provided by the display loop.) # passed array (normally provided by the display loop.)
i_read_range, _ = viz.update_graphics() i_read_range, _ = viz.update_graphics()
profiler(f'{viz.name}@{chart_name} `Viz.update_graphics()`')
out = viz.maxmin(i_read_range=i_read_range) out = viz.maxmin(i_read_range=i_read_range)
if out is None: if out is None:
log.warning(f'No yrange provided for {name}!?') log.warning(f'No yrange provided for {name}!?')
@ -951,13 +952,40 @@ class ChartView(ViewBox):
yrange yrange
) = out ) = out
# print( pi = viz.plot
# f'i_read_range: {i_read_range}\n' mxmn = mxmns.get(pi)
# f'ixrng: {ixrng}\n' if mxmn:
# f'yrange: {yrange}\n' yrange = mxmns[pi] = (
# ) min(yrange[0], mxmn[0]),
viz.plot.vb._set_yrange(yrange=yrange) max(yrange[1], mxmn[1]),
)
else:
mxmns[viz.plot] = yrange
pi.vb._set_yrange(yrange=yrange)
profiler(
f'{viz.name}@{chart_name} `Viz.plot.vb._set_yrange()`'
)
# if 'dolla_vlm' in viz.name:
# print(
# f'AUTO-Y-RANGING: {viz.name}\n'
# f'i_read_range: {i_read_range}\n'
# f'ixrng: {ixrng}\n'
# f'yrange: {yrange}\n'
# )
# (
# view_xrange,
# view_yrange,
# ) = viz.plot.vb.viewRange()
# print(
# f'{viz.name}@{chart_name}\n'
# f' xRange -> {view_xrange}\n'
# f' yRange -> {view_yrange}\n'
# )
profiler(f'autoscaled overlays {chart_name}') profiler(f'autoscaled overlays {chart_name}')
profiler(f'<{chart_name}>.update_graphics_from_flow({name})') profiler(f'<{chart_name}>.update_graphics_from_flow({name})')
profiler.finish()