# 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()
