Solve a final minor-should-rescale edge case

When the minor has the same scaling as the major in a given direction we
should still do back-scaling against the major-target and previous
minors to avoid strange edge cases where only the target-major might not
be shifted correctly to show an matched intersect point? More or less
this just meant making the y-mxmn checks interval-inclusive with
`>=`/`<=` operators.

Also adds a shite ton of detailed comments throughout the pin-to-target
method blocks and moves the final major y-range call outside the final
`scaled: dict` loop.
log_linearized_curve_overlays
Tyler Goodlet 2023-02-28 10:53:06 -05:00
parent 01ea706644
commit 45e97dd4c8
1 changed files with 136 additions and 81 deletions

View File

@ -81,8 +81,11 @@ class OverlayT(Struct):
) -> float: ) -> float:
return y_ref * (1 + self.rng) return y_ref * (1 + self.rng)
# def loglin_from_range( def scalars_from_index(
# self, self,
xref: float,
) -> tuple[float, float]:
pass
# y_ref: float, # reference value for dispersion metric # y_ref: float, # reference value for dispersion metric
# mn: float, # min y in target log-lin range # mn: float, # min y in target log-lin range
@ -359,6 +362,12 @@ def overlay_viewlists(
key = 'open' if viz.is_ohlc else viz.name key = 'open' if viz.is_ohlc else viz.name
start_t = row_start['time'] start_t = row_start['time']
# TODO: call `Viz.disp_from_range()` here!
# disp = viz.disp_from_range(yref=y_ref)
# if disp is None:
# print(f'{viz.name}: WTF NO DISP')
# continue
# returns scalars # returns scalars
r_up = (ymx - y_ref) / y_ref r_up = (ymx - y_ref) / y_ref
r_down = (ymn - y_ref) / y_ref r_down = (ymn - y_ref) / y_ref
@ -489,12 +498,6 @@ def overlay_viewlists(
profiler(msg) profiler(msg)
print(msg) print(msg)
# disp = viz.disp_from_range(yref=y_ref)
# if disp is None:
# print(f'{viz.name}: WTF NO DISP')
# continue
# r_up, r_dn = disp
disp = r_up - r_down disp = r_up - r_down
# register curves by a "full" dispersion metric for # register curves by a "full" dispersion metric for
@ -576,7 +579,7 @@ def overlay_viewlists(
mx_disp = max(overlay_table) mx_disp = max(overlay_table)
mx_entry = overlay_table[mx_disp] mx_entry = overlay_table[mx_disp]
( (
_, # viewbox mx_view, # viewbox
mx_viz, # viz mx_viz, # viz
_, # y_ref _, # y_ref
mx_ymn, mx_ymn,
@ -586,13 +589,17 @@ def overlay_viewlists(
r_up_mx, r_up_mx,
r_dn_mn, r_dn_mn,
) = mx_entry ) = mx_entry
mx_disp = r_up_mx - r_dn_mn
scaled: dict[float, tuple[float, float, float]] = {} scaled: dict[
float,
tuple[Viz, float, float, float, float]
] = {}
# conduct "log-linearized multi-plot" scalings for all groups # conduct "log-linearized multi-plot" scalings for all groups
# -> iterate all curves Ci in dispersion-measure sorted order # -> iterate all curves Ci in dispersion-measure sorted order
# going from smallest swing to largest. # going from smallest swing to largest.
for full_disp in sorted(overlay_table): for full_disp in reversed(overlay_table):
( (
view, view,
viz, viz,
@ -620,17 +627,24 @@ def overlay_viewlists(
match overlay_technique: match overlay_technique:
# Pin this curve to the "major dispersion" (or other # Pin this curve to the "major dispersion" (or other
# target) curve by finding the intersect datum and # target) curve:
# then scaling according to the returns log-lin transort
# 'at that intersect reference data'. If the pinning
# results in this (minor/pinned) curve being out of view
# adjust the returns scalars to match this curves min
# y-range to stay in view.
case 'loglin_ref_to_curve':
# TODO: technically we only need to do this here if
# #
# - 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':
if viz is not mx_viz: if viz is not mx_viz:
# 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.
( (
i_start, i_start,
y_ref_major, y_ref_major,
@ -638,46 +652,83 @@ def overlay_viewlists(
r_major_down_here, r_major_down_here,
) = mx_viz.scalars_from_index(xref) ) = mx_viz.scalars_from_index(xref)
# transform y-range scaling to be the same as the
# equivalent "intersect" datum on the major
# dispersion curve (or other target "pin to"
# equivalent).
ymn = y_start * (1 + r_major_down_here) ymn = y_start * (1 + r_major_down_here)
if ymn > y_min:
# 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 ymn = y_min
r_dn_minor = (ymn - y_start) / y_start r_dn_minor = (ymn - y_start) / y_start
# 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_ymn = y_ref_major * (1 + r_dn_minor)
mx_viz.vs.yrange = mx_ymn, mx_viz.vs.yrange[1]
# TODO: rescale all already scaled curves to # rescale all already scaled curves to new
# new increased range for this side. # 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: for _view in scaled:
_yref, _ymn, _ymx = scaled[_view] _viz, _yref, _ymn, _ymx, _xref = scaled[_view]
new_ymn = _yref * (1 + r_dn_minor) (
_,
_,
_,
r_major_down_here,
) = mx_viz.scalars_from_index(_xref)
# TODO: is there a faster way to do this new_ymn = _yref * (1 + r_major_down_here)
# by mutating state on some object
# instead? scaled[_view] = (
scaled[_view] = (_yref, new_ymn, _ymx) _viz, _yref, new_ymn, _ymx, _xref)
if debug_print:
print(
f'RESCALE {_viz.name} ymn -> {new_ymn}'
f'RESCALE MAJ ymn -> {mx_ymn}'
)
ymx = y_start * (1 + r_major_up_here) ymx = y_start * (1 + r_major_up_here)
if ymx < y_max:
# set the `scaled: dict` entry to ensure # same as above but for minor being out-of-range
# that this minor curve will be entirely in # on the upside.
# view. if ymx <= y_max:
ymx = y_max ymx = y_max
r_up_minor = (ymx - y_start) / y_start r_up_minor = (ymx - y_start) / y_start
# adjust the target-major curve's range to
# (log-linearly) include this extra range by
# applying the inverse transform of the
# minor.
mx_ymx = y_ref_major * (1 + r_up_minor) mx_ymx = y_ref_major * (1 + r_up_minor)
mx_viz.vs.yrange = mx_viz.vs.yrange[0], mx_ymx
for _view in scaled: for _view in scaled:
_yref, _ymn, _ymx = scaled[_view] _viz, _yref, _ymn, _ymx, _xref = scaled[_view]
new_ymx = _yref * (1 + r_up_minor) (
scaled[_view] = (_yref, _ymn, new_ymx) _,
_,
r_major_up_here,
_,
) = mx_viz.scalars_from_index(_xref)
new_ymx = _yref * (1 + r_major_up_here)
scaled[_view] = (
_viz, _yref, _ymn, new_ymx, _xref)
if debug_print:
print(
f'RESCALE {_viz.name} ymn -> {new_ymx}'
)
if debug_print: if debug_print:
print( print(
@ -686,6 +737,12 @@ def overlay_viewlists(
f'dn: {r_major_down_here}\n' f'dn: {r_major_down_here}\n'
f'up: {r_major_up_here}\n' f'up: {r_major_up_here}\n'
) )
# register all overlays for a final pass where we
# apply all pinned-curve y-range transform scalings.
scaled[view] = (viz, y_start, ymn, ymx, xref)
# target/dispersion MAJOR case
else: else:
if debug_print: if debug_print:
print( print(
@ -693,6 +750,7 @@ def overlay_viewlists(
f'dn: {r_dn_mn}\n' f'dn: {r_dn_mn}\n'
f'up: {r_up_mx}\n' f'up: {r_up_mx}\n'
) )
# target/major curve's mxmn may have been # target/major curve's mxmn may have been
# reset by minor overlay steps above. # reset by minor overlay steps above.
ymn = mx_ymn ymn = mx_ymn
@ -705,25 +763,23 @@ def overlay_viewlists(
case 'loglin_ref_to_first': case 'loglin_ref_to_first':
ymn = dnt.apply_rng(y_start) ymn = dnt.apply_rng(y_start)
ymx = upt.apply_rng(y_start) ymx = upt.apply_rng(y_start)
view._set_yrange(yrange=(ymn, ymx))
# Do not pin curves by log-linearizing their y-ranges, # Do not pin curves by log-linearizing their y-ranges,
# instead allow each curve to fully scale to the # instead allow each curve to fully scale to the
# time-series in view's min and max y-values. # time-series in view's min and max y-values.
case 'mxmn': case 'mxmn':
ymn = y_min view._set_yrange(yrange=(y_min, y_max))
ymx = y_max
case _: case _:
raise RuntimeError( raise RuntimeError(
f'overlay_technique is invalid `{overlay_technique}' f'overlay_technique is invalid `{overlay_technique}'
) )
scaled[view] = (y_start, ymn, ymx) if scaled:
for ( for (
view, view,
(yref, ymn, ymx) (viz, yref, ymn, ymx, xref)
) in scaled.items(): ) in scaled.items():
# NOTE XXX: we have to set each curve's range once (and # NOTE XXX: we have to set each curve's range once (and
@ -731,9 +787,7 @@ def overlay_viewlists(
# inside of a single render cycle (and apparently calling # inside of a single render cycle (and apparently calling
# `ViewBox.setYRange()` multiple times within one only takes # `ViewBox.setYRange()` multiple times within one only takes
# the first call as serious...) XD # the first call as serious...) XD
view._set_yrange( view._set_yrange(yrange=(ymn, ymx))
yrange=(ymn, ymx),
)
profiler(f'{viz.name}@{chart_name} log-SCALE minor') profiler(f'{viz.name}@{chart_name} log-SCALE minor')
if debug_print: if debug_print:
@ -742,7 +796,8 @@ def overlay_viewlists(
f'LOGLIN SCALE CYCLE: {viz.name}@{chart_name}\n' f'LOGLIN SCALE CYCLE: {viz.name}@{chart_name}\n'
f'UP MAJOR C: {upt.viz.name} with disp: {upt.rng}\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'DOWN MAJOR C: {dnt.viz.name} with disp: {dnt.rng}\n'
f'disp: {disp}\n' f'SIGMA MAJOR C: {mx_viz.name} -> {mx_disp}\n'
# f'disp: {disp}\n'
f'xref for MINOR: {xref}\n' f'xref for MINOR: {xref}\n'
f'y_start: {y_start}\n' f'y_start: {y_start}\n'
f'y min: {y_min}\n' f'y min: {y_min}\n'
@ -752,9 +807,9 @@ def overlay_viewlists(
'------------------------------\n' '------------------------------\n'
) )
# vrs = major_sigma_viz.plot.vb.viewRange() # finally, scale major curve to possibly re-scaled/modified
# if vrs[1][0] > major_mn: # values
# breakpoint() mx_view._set_yrange(yrange=(mx_ymn, mx_ymx))
if debug_print: if debug_print:
print( print(