917 lines
32 KiB
Python
917 lines
32 KiB
Python
# piker: trading gear for hackers
|
|
# Copyright (C) Tyler Goodlet (in stewardship for pikers)
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
# it under the terms of the GNU Affero General Public License as published by
|
|
# the Free Software Foundation, either version 3 of the License, or
|
|
# (at your option) any later version.
|
|
|
|
# This program is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
# GNU Affero General Public License for more details.
|
|
|
|
# You should have received a copy of the GNU Affero General Public License
|
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
'''
|
|
Overlay (aka multi-chart) UX machinery.
|
|
|
|
'''
|
|
from __future__ import annotations
|
|
from operator import itemgetter
|
|
from typing import (
|
|
Any,
|
|
Literal,
|
|
TYPE_CHECKING,
|
|
)
|
|
|
|
import numpy as np
|
|
import pendulum
|
|
import pyqtgraph as pg
|
|
|
|
from piker.types import Struct
|
|
from ..tsp import slice_from_time
|
|
from ..log import get_logger
|
|
from ..toolz 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: dict = chart._vizs
|
|
|
|
# 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.
|
|
# vizs_by_disp: list[tuple[float, Viz]] = []
|
|
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.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: float = (ymx - y_ref) / y_ref
|
|
r_down: float = (ymn - y_ref) / y_ref
|
|
disp: float = 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)
|
|
if debug_print:
|
|
print(msg)
|
|
|
|
# register curves by a "full" dispersion metric for
|
|
# later sort order in the overlay (technique
|
|
# ) application loop below.
|
|
pair: tuple[float, Viz] = (disp, viz)
|
|
|
|
# time series are so similar they have same
|
|
# dispersion with `float` precision..
|
|
if entry := overlay_table.get(pair):
|
|
raise RuntimeError('Duplicate entry!? -> {entry}')
|
|
|
|
# vizs_by_disp.append(pair)
|
|
overlay_table[pair] = (
|
|
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_pair: tuple = max(overlay_table, key=itemgetter(0))
|
|
|
|
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_pair)
|
|
else:
|
|
# TODO: for pin to first-in-view we need to NOT pop this from the
|
|
# table, but can we simplify below code even more?
|
|
mx_entry = overlay_table[mx_pair]
|
|
|
|
(
|
|
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 pair in sorted(
|
|
overlay_table,
|
|
key=itemgetter(0),
|
|
reverse=True,
|
|
):
|
|
(
|
|
view,
|
|
viz,
|
|
y_start,
|
|
y_min,
|
|
y_max,
|
|
read_slc,
|
|
minor_in_view,
|
|
r_up,
|
|
r_dn,
|
|
) = overlay_table[pair]
|
|
|
|
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_pair[0]}\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()
|