From 052ce65682092e1e44b93c6ff652463c37c4cebb Mon Sep 17 00:00:00 2001 From: Tyler Goodlet Date: Fri, 20 Jan 2023 18:46:44 -0500 Subject: [PATCH] 3rdz the charm: log-linearize minor y-ranges to a major In very close manner to the original (gut instinct) attempt, this properly (y-axis-vertically) aligns and scales overlaid curves according to what we are calling a "log-linearized y-range multi-plot" B) The basic idea is that a simple returns measure (eg. `R = (p1 - p0) / p0`) applied to all curves gives a constant output `R` no matter the price co-domain in use and thus gives a constant returns over all assets in view styled scaling; a intuitive visual of returns correlation. The reference point is for now the left-most point in view (or highest common index available to all curves), though we can make this a parameter based on user needs. A slew of debug `print()`s are left in for now until we iron out the remaining edge cases to do with re-scaling a major (dispersion) curve based on a minor now requiring a larger log-linear y-range from that previous major' range. --- piker/ui/_interaction.py | 199 ++++++++++++++++++++++----------------- 1 file changed, 111 insertions(+), 88 deletions(-) diff --git a/piker/ui/_interaction.py b/piker/ui/_interaction.py index 5b15b979..08530bb0 100644 --- a/piker/ui/_interaction.py +++ b/piker/ui/_interaction.py @@ -20,6 +20,7 @@ Chart view box primitives """ from __future__ import annotations from contextlib import asynccontextmanager +import math import time from typing import ( Optional, @@ -988,6 +989,13 @@ class ChartView(ViewBox): profiler(f'<{chart_name}>.interact_graphics_cycle({name})') + # if no overlays, set lone chart's yrange and short circuit + if len(mxmn_groups) < 2: + viz.plot.vb._set_yrange( + yrange=yrange, + ) + return + # proportional group auto-scaling per overlay set. # -> loop through overlays on each multi-chart widget # and scale all y-ranges based on autoscale config. @@ -1008,6 +1016,7 @@ class ChartView(ViewBox): float, # y max float, # y median slice, # in-view array slice + np.ndarray, # in-view array ], ] = {} max_start: float = 0 @@ -1020,7 +1029,6 @@ class ChartView(ViewBox): (ymn, ymx), ) = out - x_start = ixrng[0] max_start = max(x_start, max_start) @@ -1032,27 +1040,28 @@ class ChartView(ViewBox): # row_stop = arr[read_slc.stop - 1] if viz.is_ohlc: - y_median = np.median(in_view['close']) + y_med = np.median(in_view['close']) y_start = row_start['open'] else: - y_median = np.median(in_view[viz.name]) + y_med = np.median(in_view[viz.name]) y_start = row_start[viz.name] # y_stop = row_stop[viz.name] + print( + f'{viz.name} -> (x_start: {x_start}, y_start: {y_start}\n' + ) start_datums[viz.plot.vb] = ( viz, y_start, ymn, ymx, - y_median, + y_med, read_slc, + in_view, ) - # compute directional (up/down) y-range % swing/dispersion - y_ref = y_median - up_rng = (ymx - y_ref) / y_ref - down_rng = (ymn - y_ref) / y_ref - disp = abs(ymx - ymn) / y_ref + # find curve with max dispersion + disp = abs(ymx - ymn) / y_med # track the "major" curve as the curve with most # dispersion. @@ -1062,17 +1071,26 @@ class ChartView(ViewBox): major_mn = ymn major_mx = ymx + # compute directional (up/down) y-range % swing/dispersion + y_ref = y_med + up_rng = (ymx - y_ref) / y_ref + down_rng = (ymn - y_ref) / y_ref + mx_up_rng = max(mx_up_rng, up_rng) mn_down_rng = min(mn_down_rng, down_rng) print( + '####################\n' f'{viz.name}@{chart_name} group mxmn calc\n' + '--------------------\nn' f'y_start: {y_start}\n' f'ymn: {ymn}\n' f'ymx: {ymx}\n' f'mx_disp: {mx_disp}\n' + '####################\n' f'up %: {up_rng * 100}\n' f'down %: {down_rng * 100}\n' + '####################\n' f'mx up %: {mx_up_rng * 100}\n' f'mn down %: {mn_down_rng * 100}\n' ) @@ -1084,109 +1102,114 @@ class ChartView(ViewBox): y_start, y_min, y_max, - y_median, + y_med, read_slc, + minor_in_view, ) ) in start_datums.items(): - # TODO: just use y_min / y_max directly for the major - # `Viz` instead of the below calc since it should be the - # same output.. - symn = y_median * (1 + mn_down_rng) - symx = y_median * (1 + mx_up_rng) - - if not (viz is major_viz): - - # compute dispersion normed offsets at the start - # index of the smaller dispersion curve. - maj_viz_arr = major_viz.shm.array + # we use the ymn/mx verbatim from the major curve + # (i.e. the curve measured to have the highest + # dispersion in view). + if viz is major_viz: + ymn = y_min + ymx = y_max + else: key = 'open' if viz.is_ohlc else viz.name - # handle case where major (dispersion) curve has - # a smaller domain then minor one(s). - istart = read_slc.start - if read_slc.start > maj_viz_arr.size: - istart = 0 + # handle case where major and minor curve(s) have + # a disjoint x-domain (one curve is smaller in + # length then the other): + # - find the highest (time) index common to both + # curves. + # - slice out the first "intersecting" y-value from + # both curves for use in log-linear scaling such + # that the intersecting y-value is used as the + # reference point for scaling minor curve's + # y-range based on the major curves y-range. + abs_ifirst = minor_in_view[0]['index'] + mshm = major_viz.shm + abs_i_start = max( + abs_ifirst, + mshm.array['index'][0], + ) + # get intersection point y-values for both curves + y_maj_intersect = mshm._array[abs_i_start][key] + y_min_intersect = minor_in_view[abs_i_start - abs_ifirst] - maj_start_y = maj_viz_arr[istart][key] + # TODO: probably write this as a compile cpython or + # numba func. - maj_start_offset = maj_start_y / major_mn - maj_max_offset = major_mx / major_mn + # compute directional (up/down) y-range + # % swing/dispersion starting at the reference index + # determined by the above indexing arithmetic. + y_ref = y_maj_intersect + assert y_ref + r_up = (major_mx - y_ref) / y_ref + r_down = (major_mn - y_ref) / y_ref + ymn = y_start * (1 + r_down) + ymx = y_start * (1 + r_up) - # XXX: or this? - # maj_start_offset = (maj_start_y - major_mn) / major_mn - # maj_max_offset = (major_mx - maj_start_y) / major_mn + # XXX: handle out of view cases where minor curve + # now is outside the range of the major curve. in + # this case we then re-scale the major curve to + # include the range missing now enforced by the + # minor (now new major for this *side*). Note this + # is side (up/down) specific. + new_maj_mxmn: None | tuple[float, float] = None + if y_max > ymx: + y_ref = y_min_intersect[key] + r_up_minor = (y_max - y_ref) / y_ref + new_maj_ymx = y_maj_intersect * (1 + r_up_minor) + new_maj_mxmn = (major_mn, new_maj_ymx) + ymx = y_max - # XXX: or this? - # major_disp_offset = ( - # (maj_viz_arr[istart][key] - major_mn) - # / - # major_mn - # ) - # minor_disp_offset_mn = ( - # (y_start - y_min) - # / - # y_min - # ) - # minor_disp_offset_mx = ( - # (ymx - y_start) - # / - # y_min + print( + f'{view.name} OUT OF RANGE:\n' + f'MAJOR is {major_viz.name}\n' + f'y_max:{y_max} > ymx:{ymx}\n' + ) - # normed_disp_ratio = minor_disp_offset - major_disp_offset + if y_min < ymn: + y_ref = y_min_intersect[key] + r_down_minor = (y_min - y_ref) / y_ref + new_maj_ymn = y_maj_intersect * (1 + r_down_minor) + new_maj_mxmn = ( + new_maj_ymn, + new_maj_ymx[1] if new_maj_mxmn else major_mx + ) + ymn = y_min + print( + f'{view.name} OUT OF RANGE:\n' + f'MAJOR is {major_viz.name}\n' + f'y_min:{y_min} < ymn:{ymn}\n' + ) - # adjust mxmn range to align curve start point in - # the minor overlay with the major one. - - # symn = symn * (1 + normed_disp_ratio) - # symx = symx * (1 + normed_disp_ratio) - - # symn = symn - (symn * normed_disp_ratio) - # symx = symx - (symn * normed_disp_ratio) - - # symn = y_min * maj_start_offset - # symx = y_min * maj_max_offset + if new_maj_mxmn: + major_viz.plot.vb._set_yrange( + yrange=new_maj_mxmn, + ) print( f'{view.name} APPLY group mxmn\n' - # f'disp offset ratio diff %: {normed_disp_ratio}\n' - # f'major disp offset %: {major_disp_offset}\n' - # f'minor disp offset %: {minor_disp_offset}\n' f'y_start: {y_start}\n' f'mn_down_rng: {mn_down_rng * 100}\n' f'mx_up_rng: {mx_up_rng * 100}\n' - f'scaled ymn: {symn}\n' - f'scaled ymx: {symx}\n' + f'scaled ymn: {ymn}\n' + f'scaled ymx: {ymx}\n' f'scaled mx_disp: {mx_disp}\n' ) + if ( + math.isinf(ymx) + or math.isinf(ymn) + ): + breakpoint() + view._set_yrange( - yrange=(symn, symx), - # range_margin=None, + yrange=(ymn, ymx), ) - # if 'mnq' 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() - # view_ymx = view_yrange[1] - # print( - # f'{viz.name}@{chart_name}\n' - # f' xRange -> {view_xrange}\n' - # f' yRange -> {view_yrange}\n' - # f' view y-max -> {view_ymx}\n' - # ) - - # if view_ymx != symx: - # breakpoint() - profiler.finish()