From b118954bf7b68ef3ed673fc2eb76f372eeea7e56 Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Mon, 27 Feb 2023 15:55:37 -0500 Subject: [PATCH] Support "pin-to-target-curve" overlay method again Yah yah, i know it's the same as before (the N > 2 curves case with out-of-range-minor rescaling the previously scaled curves isn't fixed yet...) but, this is a much better and optional implementation in less code. Further we're now better leveraging various new cached properties and methods on `Viz`. We now handle different `overlay_technique: str` options using `match:` syntax in the 2ndary scaling loop, stash the returns scalars per curve in `overlay_table`, and store and iterate the curves by dispersion measure sort order. Further wrt "pin-to-target-curve" mode, which currently still pins to the largest measured dispersion curve in the overlay set: - pop major Ci overlay table entries at start for sub-calcs usage when handling the "minor requires major rescale after pin" case. - (finally) correctly rescale the major curve y-mxmn to whatever the latest minor overlay curve by calcing the inverse transform from the minor *at that point*: - the intersect point being that which the minor has starts support on the major's x-domain* using the new `Viz.scalars_from_index()` and, - checking that the minor is not out of range (versus what the major's transform calcs it to be, in which case, - calc the inverse transform from the current out-of-range minor and use it to project the new y-mxmn for the major/target based on the same intersect-reference point in the x-domain used by the minor. - always handle the target-major Ci specially by only setting the `mx_ymn` / `mx_ymn` value when iterating that entry in the overlay table. - add todos around also doing the last sub-sub bullet for all previously major-transform scaled minor overlays (this is coming next..i hope). - add a final 3rd overlay loop which goes through a final `scaled: dict` to apply all range values to each view; this is where we will eventually solve that last edge case of an out-of-range minor's scaling needing to be used to rescale already scaled minors XD --- piker/ui/view_mode.py | 209 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 173 insertions(+), 36 deletions(-) diff --git a/piker/ui/view_mode.py b/piker/ui/view_mode.py index 0e55a051..73051d06 100644 --- a/piker/ui/view_mode.py +++ b/piker/ui/view_mode.py @@ -62,8 +62,8 @@ class OverlayT(Struct): in the target co-domain. ''' - start_t: float | None = None viz: Viz | None = None + start_t: float | None = None # % "range" computed from some ref value to the mn/mx rng: float | None = None @@ -201,13 +201,13 @@ def overlay_viewlists( ] | None = None, overlay_technique: Literal[ - 'loglin_to_first', - 'loglin_to_sigma', - 'mnmx', + 'loglin_ref_to_curve', + 'loglin_ref_to_first', + 'mxmn', 'solo', - ] = 'loglin_to_first', + ] = 'loglin_ref_to_curve', - # internal instrumentation + # internal debug debug_print: bool = False, ) -> None: @@ -236,8 +236,6 @@ def overlay_viewlists( # -> 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. - major_sigma_viz: Viz = None - mx_disp: float = 0 # collect certain flows have grapics objects **in seperate # plots/viewboxes** into groups and do a common calc to @@ -245,8 +243,9 @@ def overlay_viewlists( # this is primarly used for our so called "log-linearized # multi-plot" overlay technique. overlay_table: dict[ - ChartView, + float, tuple[ + ChartView, Viz, float, # y start float, # y min @@ -254,6 +253,8 @@ def overlay_viewlists( float, # y median slice, # in-view array slice np.ndarray, # in-view array + float, # returns up scalar + float, # return down scalar ], ] = {} @@ -323,11 +324,15 @@ def overlay_viewlists( profiler(f'{viz.name}@{chart_name} common pi sort') # non-overlay group case - if not viz.is_ohlc: + if ( + not viz.is_ohlc + or overlay_technique == '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 @@ -338,15 +343,12 @@ def overlay_viewlists( ymn, ymx = yrange # determine start datum in view - arr = viz.shm.array - in_view = arr[read_slc] + in_view = viz.vs.in_view if not in_view.size: log.warning(f'{viz.name} not in view?') continue - # row_start = arr[read_slc.start - 1] - row_start = arr[read_slc.start] - + row_start = in_view[0] if viz.is_ohlc: y_ref = row_start['open'] else: @@ -355,9 +357,11 @@ def overlay_viewlists( profiler(f'{viz.name}@{chart_name} MINOR curve median') key = 'open' if viz.is_ohlc else viz.name - start_t = in_view[0]['time'] - r_down = (ymn - y_ref) / y_ref + start_t = row_start['time'] + + # returns scalars r_up = (ymx - y_ref) / y_ref + r_down = (ymn - y_ref) / y_ref msg = ( f'### {viz.name}@{chart_name} ###\n' @@ -431,6 +435,8 @@ def overlay_viewlists( 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 ( @@ -483,27 +489,37 @@ def overlay_viewlists( profiler(msg) print(msg) - # find curve with max dispersion - disp = abs(ymx - ymn) / y_ref - if disp > mx_disp: - major_sigma_viz = viz - mx_disp = disp + # disp = viz.disp_from_range(yref=y_ref) + # if disp is None: + # print(f'{viz.name}: WTF NO DISP') + # continue - overlay_table[viz.plot.vb] = ( + # r_up, r_dn = disp + disp = r_up - r_down + + # 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') # 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 lone chart's (viz) yrange and short + # 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 @@ -534,13 +550,20 @@ def overlay_viewlists( elif ( mxmns_by_common_pi - and not major_sigma_viz + # and not major_sigma_viz + and not overlay_table ): # move to next chart in linked set since # no overlay transforming is needed. continue - profiler(f'<{chart_name}>.interact_graphics_cycle({name})') + msg = ( + f'`Viz` curve first pass complete\n' + f'overlay_table: {overlay_table.keys()}\n' + ) + profiler(msg) + if debug_print: + print(msg) # if a minor curves scaling brings it "outside" the range of # the major curve (in major curve co-domain terms) then we @@ -548,21 +571,39 @@ def overlay_viewlists( # below placeholder denotes when this occurs. # group_mxmn: None | tuple[float, float] = None - # TODO: probably re-write this loop as a compiled cpython or - # numba func. + r_up_mx: float + r_dn_mn: float + mx_disp = max(overlay_table) + mx_entry = overlay_table[mx_disp] + ( + _, # viewbox + mx_viz, # viz + _, # y_ref + mx_ymn, + mx_ymx, + _, # read_slc + _, # in_view array + r_up_mx, + r_dn_mn, + ) = mx_entry + + scaled: dict[float, tuple[float, float, float]] = {} # conduct "log-linearized multi-plot" scalings for all groups - for ( - view, + # -> iterate all curves Ci in dispersion-measure sorted order + # going from smallest swing to largest. + for full_disp in sorted(overlay_table): ( + view, viz, y_start, y_min, y_max, read_slc, minor_in_view, - ) - ) in overlay_table.items(): + r_up, + r_dn, + ) = overlay_table[full_disp] key = 'open' if viz.is_ohlc else viz.name @@ -575,8 +616,99 @@ def overlay_viewlists( ) continue - ymn = dnt.apply_rng(y_start) - ymx = upt.apply_rng(y_start) + xref = minor_in_view[0]['time'] + match overlay_technique: + + # Pin this curve to the "major dispersion" (or other + # target) curve by finding the intersect datum and + # 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 + # + if viz is not mx_viz: + ( + i_start, + y_ref_major, + r_major_up_here, + r_major_down_here, + ) = 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) + if ymn > y_min: + ymn = y_min + r_dn_minor = (ymn - y_start) / y_start + + mx_ymn = y_ref_major * (1 + r_dn_minor) + + # TODO: rescale all already scaled curves to + # new increased range for this side. + # for ( + # view, + # (yref, ymn, ymx) + # ) in scaled.items(): + # pass + + ymx = y_start * (1 + r_major_up_here) + if ymx < y_max: + ymx = y_max + r_up_minor = (ymx - y_start) / y_start + mx_ymx = y_ref_major * (1 + r_up_minor) + + if debug_print: + print( + f'Minor SCALARS {viz.name}:\n' + f'xref: {xref}\n' + f'dn: {r_major_down_here}\n' + f'up: {r_major_up_here}\n' + ) + else: + if debug_print: + print( + f'MAJOR SCALARS {viz.name}:\n' + f'dn: {r_dn_mn}\n' + f'up: {r_up_mx}\n' + ) + # target/major curve's mxmn may have been + # reset by minor overlay steps above. + ymn = mx_ymn + ymx = mx_ymx + + # 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_rng(y_start) + ymx = upt.apply_rng(y_start) + + # 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': + ymn = y_min + ymx = y_max + + case _: + raise RuntimeError( + f'overlay_technique is invalid `{overlay_technique}' + ) + + scaled[view] = (y_start, ymn, ymx) + + for ( + view, + (yref, ymn, ymx) + + ) 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 @@ -594,6 +726,8 @@ def overlay_viewlists( f'LOGLIN SCALE CYCLE: {viz.name}@{chart_name}\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'disp: {disp}\n' + f'xref for MINOR: {xref}\n' f'y_start: {y_start}\n' f'y min: {y_min}\n' f'y max: {y_max}\n' @@ -614,7 +748,10 @@ def overlay_viewlists( + '\n' ) + + profiler(f'<{chart_name}>.interact_graphics_cycle()') + if not do_linked_charts: - return + break profiler.finish()