# 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/>.

'''
High level chart-widget apis.

'''
from __future__ import annotations
from contextlib import (
    contextmanager as cm,
    ExitStack,
)
from typing import (
    Iterator,
    TYPE_CHECKING,
)

import pyqtgraph as pg

from piker.ui.qt import (
    QtCore,
    Qt,
    QLineF,
    QFrame,
    QWidget,
    QHBoxLayout,
    QVBoxLayout,
    QSplitter,
)
from ._widget import GodWidget
from ._axes import (
    DynamicDateAxis,
    PriceAxis,
)
from ._cursor import (
    Cursor,
    ContentsLabel,
)
from tractor.ipc._shm import ShmArray
from ._ohlc import BarItems
from ._curve import (
    Curve,
    StepCurve,
)
from ._style import (
    hcolor,
    CHART_MARGINS,
    _xaxis_at,
    # _min_points_to_show,
)
from ..accounting import (
    MktPair,
)
from ..log import get_logger
from ._interaction import ChartView
from ._forms import FieldsForm
from ._overlay import PlotItemOverlay
from ._dataviz import Viz
from ._search import SearchWidget
from . import _pg_overrides as pgo

if TYPE_CHECKING:
    from ._display import DisplayState
    from ..data.flows import Flume
    from ..data.feed import Feed

log = get_logger(__name__)


class ChartnPane(QFrame):
    '''
    One-off ``QFrame`` composite which pairs a chart
    + sidepane (often a ``FieldsForm`` + other widgets if
    provided) forming a, sort of, "chart row" with a side panel
    for configuration and display of off-chart data.

    See composite widgets docs for deats:
    https://doc.qt.io/qt-5/qwidget.html#composite-widgets

    '''
    sidepane: FieldsForm|SearchWidget
    hbox: QHBoxLayout
    chart: ChartPlotWidget|None = None

    def __init__(
        self,

        sidepane: FieldsForm,
        parent=None,

    ) -> None:

        super().__init__(parent)

        self._sidepane = sidepane
        self.chart = None

        hbox = self.hbox = QHBoxLayout(self)
        hbox.setAlignment(Qt.AlignTop|Qt.AlignLeft)
        hbox.setContentsMargins(0, 0, 0, 0)
        hbox.setSpacing(3)

    def set_sidepane(
        self,
        sidepane: FieldsForm|SearchWidget,
    ) -> None:

        # add sidepane **after** chart; place it on axis side
        self.hbox.addWidget(
            sidepane,
            alignment=Qt.AlignTop
        )
        self._sidepane = sidepane

    @property
    def sidepane(self) -> FieldsForm|SearchWidget:
        return self._sidepane


class LinkedSplits(QWidget):
    '''
    Composite that holds a central chart plus a set of (derived)
    subcharts (usually computed from the original data) arranged in
    a splitter for resizing.

    A single internal references to the data is maintained
    for each chart and can be updated externally.

    '''
    def __init__(
        self,
        godwidget: GodWidget,

    ) -> None:

        super().__init__()

        # self.signals_visible: bool = False
        self.cursor: Cursor = None  # crosshair graphics

        self.godwidget = godwidget
        self.chart: ChartPlotWidget = None  # main (ohlc) chart
        self.subplots: dict[str, ChartPlotWidget] = {}

        self.godwidget = godwidget
        # placeholder for last appended ``PlotItem``'s bottom axis.
        self.xaxis_chart = None

        self.splitter = QSplitter(QtCore.Qt.Vertical)
        self.splitter.setMidLineWidth(0)
        self.splitter.setHandleWidth(2)
        self.splitter.splitterMoved.connect(self.on_splitter_adjust)

        self.layout = QVBoxLayout(self)
        self.layout.setContentsMargins(0, 0, 0, 0)
        self.layout.addWidget(self.splitter)

        # chart-local graphics state that can be passed to
        # a ``graphic_update_cycle()`` call by any task wishing to
        # update the UI for a given "chart instance".
        self.display_state: DisplayState|None = None

        self._mkt: MktPair = None

    def on_splitter_adjust(
        self,
        pos: int,
        index: int,
    ) -> None:
        # print(f'splitter moved pos:{pos}, index:{index}')
        godw = self.godwidget
        if self is godw.rt_linked:
            godw.search.on_resize()

    def graphics_cycle(self, **kwargs) -> None:
        from . import _display
        ds = self.display_state
        if ds:
            return _display.graphics_update_cycle(
                ds,
                ds.quotes,
                **kwargs,
            )

    def set_mkt_info(
        self,
        mkt: MktPair,
    ) -> None:
        self._mkt = mkt

    @property
    def mkt(self) -> MktPair:
        return self._mkt

    def set_split_sizes(
        self,
        prop: float|None = None,

    ) -> None:
        '''
        Set the proportion of space allocated for linked subcharts.

        '''
        ln: int = len(self.subplots) or 1

        # proportion allocated to consumer subcharts
        if not prop:
            prop = 3/8

        h = self.height()
        histview_h = h * (4/11)
        h = h - histview_h

        major = 1 - prop
        min_h_ind = int((h * prop) / ln)
        sizes = [
            int(histview_h),
            int(h * major),
        ]

        # give all subcharts the same remaining proportional height
        sizes.extend([min_h_ind] * ln)

        if self.godwidget.rt_linked is self:
            self.splitter.setSizes(sizes)

    def focus(self) -> None:
        if self.chart is not None:
            self.chart.focus()

    def unfocus(self) -> None:
        if self.chart is not None:
            self.chart.clearFocus()

    def plot_ohlc_main(
        self,

        mkt: MktPair,
        shm: ShmArray,
        flume: Flume,
        sidepane: FieldsForm,

        style: str = 'ohlc_bar',

        **add_plot_kwargs,

    ) -> ChartPlotWidget:
        '''
        Start up and show main (price) chart and all linked subcharts.

        The data input struct array must include OHLC fields.

        '''
        # add crosshairs
        self.cursor = Cursor(
            linkedsplits=self,
            digits=mkt.price_tick_digits,
        )

        # NOTE: atm the first (and only) OHLC price chart for the symbol
        # is given a special reference but in the future there shouldn't
        # be no distinction since we will have multiple symbols per
        # view as part of "aggregate feeds".
        self.chart = self.add_plot(
            name=mkt.fqme,
            shm=shm,
            flume=flume,
            style=style,
            _is_main=True,
            sidepane=sidepane,
            **add_plot_kwargs,
        )
        # add crosshair graphic
        self.chart.addItem(self.cursor)

        # style?
        self.chart.setFrameStyle(
            QFrame.Shape.StyledPanel
            |QFrame.Shadow.Plain
        )

        return self.chart

    def add_plot(
        self,

        name: str,
        shm: ShmArray,
        flume: Flume,

        array_key: str|None = None,
        style: str = 'line',
        _is_main: bool = False,

        sidepane: QWidget|None = None,
        draw_kwargs: dict = {},

        **cpw_kwargs,

    ) -> ChartPlotWidget:
        '''
        Add (sub)plots to chart widget by key.

        '''
        if self.chart is None and not _is_main:
            raise RuntimeError(
                "A main plot must be created first with `.plot_ohlc_main()`")

        # use "indicator axis" by default

        # TODO: we gotta possibly assign this back
        # to the last subplot on removal of some last subplot
        xaxis = DynamicDateAxis(
            None,
            orientation='bottom',
            linkedsplits=self
        )
        axes = {
            'right': PriceAxis(None, orientation='right'),
            'left': PriceAxis(None, orientation='left'),
            'bottom': xaxis,
        }

        if sidepane is not False:
            parent = qframe = ChartnPane(
                sidepane=sidepane,
                parent=self.splitter,
            )
        else:
            parent = self.splitter
            qframe = None

        cpw = ChartPlotWidget(

            # this name will be used to register the primary
            # graphics curve managed by the subchart
            name=name,
            data_key=array_key or name,

            parent=parent,
            linkedsplits=self,
            axisItems=axes,
            **cpw_kwargs,
        )
        # TODO: wow i can't believe how confusing garbage all this axes
        # stuff iss..
        for axis in axes.values():
            axis.pi = cpw.plotItem

        cpw.hideAxis('left')
        # cpw.removeAxis('left')
        cpw.hideAxis('bottom')

        if (
            _xaxis_at == 'bottom'
            and (
                self.xaxis_chart
                or (
                    not self.subplots
                    and self.xaxis_chart is None
                )
            )
        ):
            # hide the previous x-axis chart's bottom axis since we're
            # presumably being appended to the bottom subplot.
            if self.xaxis_chart:
                self.xaxis_chart.hideAxis('bottom')

            # presuming we only want it at the true bottom of all charts.
            # XXX: uses new api from our ``pyqtgraph`` fork.
            # https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master
            # _ = self.xaxis_chart.removeAxis('bottom', unlink=False)
            # assert 'bottom' not in self.xaxis_chart.plotItem.axes
            self.xaxis_chart = cpw
            cpw.showAxis('bottom')

        if qframe is not None:
            qframe.chart = cpw
            qframe.hbox.addWidget(cpw)

            # so we can look this up and add back to the splitter
            # on a symbol switch
            cpw.qframe = qframe
            assert cpw.parent() == qframe

            # add sidepane **after** chart; place it on axis side
            qframe.set_sidepane(sidepane)
            # qframe.hbox.addWidget(
            #     sidepane,
            #     alignment=Qt.AlignTop
            # )

            cpw.sidepane = sidepane

        cpw.plotItem.vb.linked = self
        cpw.setFrameStyle(
            QFrame.Shape.StyledPanel
            # |QFrame.Shadow.Plain
        )

        # don't show the little "autoscale" A label.
        cpw.hideButtons()

        # XXX: gives us outline on backside of y-axis
        cpw.getPlotItem().setContentsMargins(*CHART_MARGINS)

        # link chart x-axis to main chart
        # this is 1/2 of where the `Link` in ``LinkedSplit``
        # comes from ;)
        cpw.cv.setXLink(self.chart)

        # NOTE: above is the same as the following,
        # link this subchart's axes to the main top level chart.
        # if self.chart:
        #     cpw.cv.linkView(0, self.chart.cv)

        add_label = False
        anchor_at = ('top', 'left')

        # draw curve graphics
        if style == 'ohlc_bar':

            viz = cpw.draw_ohlc(
                name,
                shm,
                flume=flume,
                array_key=array_key,
                **draw_kwargs,
            )
            self.cursor.contents_labels.add_label(
                cpw,
                name,
                anchor_at=('top', 'left'),
                update_func=ContentsLabel.update_from_ohlc,
            )

        elif style == 'line':
            add_label = True
            # graphics, data_key = cpw.draw_curve(
            viz = cpw.draw_curve(
                name,
                shm,
                flume,
                array_key=array_key,
                color='default_light',
                **draw_kwargs,
            )

        elif style == 'step':
            add_label = True
            # graphics, data_key = cpw.draw_curve(
            viz = cpw.draw_curve(
                name,
                shm,
                flume,
                array_key=array_key,
                step_mode=True,
                color='davies',
                fill_color='davies',
                **draw_kwargs,
            )

        else:
            raise ValueError(f"Chart style {style} is currently unsupported")

        # NOTE: back-link the new sub-chart to trigger y-autoranging in
        # the (ohlc parent) main chart for this linked set.
        # if self.chart:
        #     main_viz = self.chart.get_viz(self.chart.name)
        #     self.chart.view.enable_auto_yrange(
        #         src_vb=cpw.view,
        #         viz=main_viz,
        #     )

        graphics = viz.graphics
        data_key = viz.name

        if _is_main:
            assert style == 'ohlc_bar', 'main chart must be OHLC'
        else:
            # track by name
            self.subplots[name] = cpw
            if qframe is not None:
                self.splitter.addWidget(qframe)

        # add to cross-hair's known plots
        # NOTE: add **AFTER** creating the underlying ``PlotItem``s
        # since we require that global (linked charts wide) axes have
        # been created!
        if self.cursor:
            if (
                _is_main
                or style != 'ohlc_bar'
            ):
                self.cursor.add_plot(cpw)
                if style != 'ohlc_bar':
                    self.cursor.add_curve_cursor(cpw, graphics)

                if add_label:
                    self.cursor.contents_labels.add_label(
                        cpw,
                        data_key,
                        anchor_at=anchor_at,
                    )

        self.resize_sidepanes()
        return cpw

    def resize_sidepanes(
        self,
        from_linked: LinkedSplits|None = None,

    ) -> None:
        '''
        Size all sidepanes based on the OHLC "main" plot and its
        sidepane width.

        '''
        if from_linked:
            main_chart = from_linked.chart
        else:
            main_chart = self.chart

        if main_chart and main_chart.sidepane:
            sp_w = main_chart.sidepane.width()
            for name, cpw in self.subplots.items():
                cpw.sidepane.setMinimumWidth(sp_w)
                cpw.sidepane.setMaximumWidth(sp_w)

            if from_linked:
                self.chart.sidepane.setMinimumWidth(sp_w)


# TODO: a general rework of this widget-interface:
# - we should really drop using this type and instead just lever our
#   own override of `PlotItem`..
# - possibly rename to class -> MultiChart(pg.PlotWidget):
#   where the widget is responsible for containing management
#   harness for multi-Viz "view lists" and their associated mode-panes
#   (fsp chain, order ctl, feed queue-ing params, actor ctl, etc).

class ChartPlotWidget(pg.PlotWidget):
    '''
    ``PlotWidget`` subtype containing a ``.plotItem: PlotItem`` as well
    as a `.pi_overlay: PlotItemOverlay`` which helps manage and overlay flow
    graphics view multiple compose view boxes.

    - The added methods allow for plotting OHLC sequences from
      ``np.ndarray``s with appropriate field names.
    - Overrides a ``pyqtgraph.PlotWidget`` (a ``GraphicsView`` containing
      a single ``PlotItem``) to intercept and and re-emit mouse enter/exit
      events.

    (Could be replaced with a ``pg.GraphicsLayoutWidget`` if we
    eventually want multiple plots managed together?)

    '''
    sig_mouse_leave = QtCore.pyqtSignal(object)
    sig_mouse_enter = QtCore.pyqtSignal(object)

    mode_name: str = 'view'

    # TODO: can take a ``background`` color setting - maybe there's
    # a better one?
    def mk_vb(self, name: str) -> ChartView:
        cv = ChartView(name)
        # link new view to chart's view set
        cv.linked = self.linked
        return cv

    def __init__(
        self,

        # the "data view" we generate graphics from
        name: str,
        data_key: str,
        linkedsplits: LinkedSplits,

        view_color: str = 'papas_special',
        pen_color: str = 'bracket',

        # TODO: load from config
        use_open_gl: bool = False,

        static_yrange: tuple[float, float]|None = None,

        parent=None,
        **kwargs,
    ):
        '''
        Configure initial display settings and connect view callback
        handlers.

        '''
        self.view_color = view_color
        self.pen_color = pen_color

        # NOTE: must be set bfore calling ``.mk_vb()``
        self.linked = linkedsplits
        self.sidepane: FieldsForm|None = None

        # source of our custom interactions
        self.cv = self.mk_vb(name)

        pi = pgo.PlotItem(
            viewBox=self.cv,
            name=name,
            **kwargs,
        )
        pi.chart_widget = self
        super().__init__(
            background=hcolor(view_color),
            viewBox=self.cv,
            # parent=None,
            # plotItem=None,
            # antialias=True,
            parent=parent,
            plotItem=pi,
            **kwargs
        )
        # give viewbox as reference to chart
        # allowing for kb controls and interactions on **this** widget
        # (see our custom view mode in `._interactions.py`)
        self.cv.chart = self

        self.pi_overlay: PlotItemOverlay = PlotItemOverlay(self.plotItem)

        # ensure internal pi matches
        assert self.cv is self.plotItem.vb

        self.useOpenGL(use_open_gl)
        self.name = name
        self.data_key = data_key or name
        self.qframe: ChartnPane|None = None

        # scene-local placeholder for book graphics
        # sizing to avoid overlap with data contents
        self._max_l1_line_len: float = 0

        # self.setViewportMargins(0, 0, 0, 0)

        # registry of overlay curve names
        self._vizs: dict[str, Viz] = {}

        self.feed: Feed|None = None

        self._labels = {}  # registry of underlying graphics
        self._ysticks = {}  # registry of underlying graphics

        self._static_yrange = static_yrange  # for "known y-range style"
        self._view_mode: str = 'follow'

        # show background grid
        self.showGrid(x=False, y=True, alpha=0.3)

        # indempotent startup flag for auto-yrange subsys
        # to detect the "first time" y-domain graphics begin
        # to be shown in the (main) graphics view.
        self._on_screen: bool = False

    def resume_all_feeds(self):
        feed = self.feed
        if feed:
            try:
                self.linked.godwidget._root_n.start_soon(feed.resume)
            except RuntimeError:
                # TODO: cancel the qtractor runtime here?
                raise

    def pause_all_feeds(self):
        feed = self.feed
        if feed:
            self.linked.godwidget._root_n.start_soon(feed.pause)

    @property
    def view(self) -> ChartView:
        return self.plotItem.vb

    def focus(self) -> None:
        self.view.setFocus()

    def pre_l1_xs(self) -> tuple[float, float]:
        '''
        Return the view x-coord for the value just before
        the L1 labels on the y-axis as well as the length
        of that L1 label from the y-axis.

        '''
        line_end, marker_right, yaxis_x = self.marker_right_points()
        line = self.view.mapToView(
            QLineF(line_end, 0, yaxis_x, 0)
        )
        linex, linelen = line.x1(), line.length()
        # print(
        #     f'line: {line}\n'
        #     f'linex: {linex}\n'
        #     f'linelen: {linelen}\n'
        # )
        return linex, linelen

    def marker_right_points(
        self,
        marker_size: int = 20,

    ) -> (float, float, float):
        '''
        Return x-dimension, y-axis-aware, level-line marker oriented scene
        values.

        X values correspond to set the end of a level line, end of
        a paried level line marker, and the right most side of the "right"
        axis respectively.

        '''
        # TODO: compute some sensible maximum value here
        # and use a humanized scheme to limit to that length.
        from ._l1 import L1Label
        l1_len = abs(L1Label._x_br_offset)
        ryaxis = self.getAxis('right')

        r_axis_x = ryaxis.pos().x()
        up_to_l1_sc = r_axis_x - l1_len
        marker_right = up_to_l1_sc - (1.375 * 2 * marker_size)
        # line_end = marker_right - (6/16 * marker_size)
        line_end = marker_right - marker_size

        # print(
        #     f'r_axis_x: {r_axis_x}\n'
        #     f'up_to_l1_sc: {up_to_l1_sc}\n'
        #     f'marker_right: {marker_right}\n'
        #     f'line_end: {line_end}\n'
        # )
        return line_end, marker_right, r_axis_x

    def increment_view(
        self,
        datums: int = 1,
        vb: ChartView|None = None,

    ) -> None:
        '''
        Increment the data view `datums`` steps toward y-axis thus
        "following" the current time slot/step/bar.

        '''
        view = vb or self.view
        viz = self.main_viz
        left, right = viz.view_range()
        x_shift = viz.index_step() * datums

        if datums >= 300:
            log.warning('FUCKING FIX THE GLOBAL STEP BULLSHIT')
            # breakpoint()
            return

        # should trigger broadcast on all overlays right?
        view.setXRange(
            min=left + x_shift,
            max=right + x_shift,

            # TODO: holy shit, wtf dude... why tf would this not be 0 by
            # default... speechless.
            padding=0,
        )

    def overlay_plotitem(
        self,
        name: str,
        index: int|None = None,
        axis_title: str|None = None,
        axis_side: str = 'right',
        axis_kwargs: dict = {},

    ) -> pgo.PlotItem:

        # Custom viewbox impl
        cv = self.mk_vb(name)
        cv.chart = self

        allowed_sides = {'left', 'right'}
        if axis_side not in allowed_sides:
            raise ValueError(f'``axis_side``` must be in {allowed_sides}')

        yaxis = PriceAxis(
            plotitem=None,
            orientation=axis_side,
            **axis_kwargs,
        )

        pi = pgo.PlotItem(
            parent=self.plotItem,
            name=name,
            enableMenu=False,
            viewBox=cv,
            axisItems={
                # 'bottom': xaxis,
                axis_side: yaxis,
            },
            default_axes=[],
        )
        # pi.vb.background.setOpacity(0)
        yaxis.pi = pi
        pi.chart_widget = self
        pi.hideButtons()

        # compose this new plot's graphics with the current chart's
        # existing one but with separate axes as neede and specified.
        self.pi_overlay.add_plotitem(
            pi,
            index=index,

            # only link x-axes and
            # don't relay any ``ViewBox`` derived event
            # handlers since we only care about keeping charts
            # x-synced on interaction (at least for now).
            link_axes=(0,),
        )

        # hide all axes not named by ``axis_side``
        for axname in (
            ({'bottom'} | allowed_sides) - {axis_side}
        ):
            try:
                pi.hideAxis(axname)
            except Exception:
                pass

        # add axis title
        # TODO: do we want this API to still work?
        # raxis = pi.getAxis('right')
        overlay: PlotItemOverlay = self.pi_overlay

        # Whenever overlays exist always add a y-axis label to the
        # main axis as well!
        for name, axis_info in self.plotItem.axes.items():
            axis = axis_info['item']
            if isinstance(axis, PriceAxis):
                axis.set_title(self.linked.mkt.pair())

        axis: PriceAxis = overlay.get_axis(
            pi,
            axis_side,
        )
        axis.set_title(
            axis_title or name,
            view=pi.getViewBox(),
        )

        return pi

    def draw_curve(
        self,

        name: str,
        shm: ShmArray,
        flume: Flume,

        array_key: str|None = None,
        overlay: bool = False,
        color: str|None = None,
        add_label: bool = True,
        pi: pg.PlotItem|None = None,
        step_mode: bool = False,
        is_ohlc: bool = False,
        add_sticky: None|str = 'right',

        **graphics_kwargs,

    ) -> Viz:
        '''
        Draw a "curve" (line plot graphics) for the provided data in
        the input shm array ``shm``.

        '''
        color = color or self.pen_color or 'default_light'
        data_key = array_key or name

        pi = pi or self.plotItem

        if is_ohlc:
            graphics = BarItems(
                color=color,
                name=name,
                **graphics_kwargs,
            )

        else:
            curve_type = {
                None: Curve,
                'step': StepCurve,
                # TODO:
                # 'bars': BarsItems
            }['step' if step_mode else None]

            graphics = curve_type(
                name=name,
                color=color,
                **graphics_kwargs,
            )

        viz = self._vizs[data_key] = Viz(
            data_key,
            pi,
            shm,
            flume,

            is_ohlc=is_ohlc,
            # register curve graphics with this viz
            graphics=graphics,
        )

        # connect auto-yrange callbacks *from* this new
        # view **to** this parent and likewise *from* the
        # main/parent chart back *to* the created overlay.
        pi.vb.enable_auto_yrange(
            src_vb=self.view,
            viz=viz,
        )

        pi.viz = viz
        # so that viewboxes are associated 1-to-1 with
        # their parent plotitem
        pi.vb._viz = viz

        assert isinstance(viz.shm, ShmArray)

        # TODO: this probably needs its own method?
        if overlay:
            if isinstance(overlay, pgo.PlotItem):
                if overlay not in self.pi_overlay.overlays:
                    raise RuntimeError(
                            f'{overlay} must be from `.plotitem_overlay()`'
                        )
                pi = overlay

        if add_sticky:

            main_pi: pgo.PlotItem = self.plotItem
            if pi is not main_pi:
                # overlay = self.pi_overlay
                # assert pi in overlay.overlays
                overlay = self.pi_overlay
                assert pi in overlay.overlays
                assert main_pi is overlay.root_plotitem
                axis = overlay.get_axis(
                    pi,
                    add_sticky,
                )

            else:
                axis = pi.getAxis(add_sticky)

            if pi.name not in axis._stickies:

                # TODO: UGH! just make this not here! we should
                # be making the sticky from code which has access
                # to the ``MktPair`` instance..

                # if the sticky is for our symbol
                # use the tick size precision for display
                name = name or pi.name
                mkt: MktPair = self.linked.mkt
                digits: int|None = None
                if name in mkt.fqme:
                    digits = mkt.price_tick_digits

                # anchor_at = ('top', 'left')

                # TODO: something instead of stickies for overlays
                # (we need something that avoids clutter on x-axis).
                axis.add_sticky(
                    pi=pi,
                    fg_color='black',
                    # bg_color=color,
                    digits=digits,
                )

        # NOTE: this is more or less the RENDER call that tells Qt to
        # start showing the generated graphics-curves. This is kind of
        # of edge-triggered call where once added any
        # ``QGraphicsItem.update()`` calls are automatically displayed.
        # Our internal graphics objects have their own "update from
        # data" style method API that allows for real-time updates on
        # the next render cycle; just note a lot of the real-time
        # updates are implicit and require a bit of digging to
        # understand.
        pi.addItem(graphics)

        return viz

    def draw_ohlc(
        self,
        name: str,
        shm: ShmArray,
        flume: Flume,

        array_key: str|None = None,
        **draw_curve_kwargs,

    ) -> Viz:
        '''
        Draw OHLC datums to chart.

        '''
        return self.draw_curve(
            name,
            shm,
            flume,
            array_key=array_key,
            is_ohlc=True,
            **draw_curve_kwargs,
        )

    # TODO: pretty sure we can just call the cursor
    # directly not? i don't wee why we need special "signal proxies"
    # for this lul..
    def enterEvent(self, ev):  # noqa
        # pg.PlotWidget.enterEvent(self, ev)
        self.sig_mouse_enter.emit(self)

    def leaveEvent(self, ev):  # noqa
        # pg.PlotWidget.leaveEvent(self, ev)
        self.sig_mouse_leave.emit(self)
        self.scene().leaveEvent(ev)

    def get_viz(
        self,
        key: str,
    ) -> Viz:
        '''
        Try to get an underlying ``Viz`` by key.

        '''
        return self._vizs.get(key)

    @property
    def main_viz(self) -> Viz:
        return self.get_viz(self.name)

    def iter_vizs(self) -> Iterator[Viz]:
        return iter(self._vizs.values())

    @cm
    def reset_graphics_caches(self) -> None:
        '''
        Reset all managed ``Viz`` (flow) graphics objects
        Qt cache modes (to ``NoCache`` mode) on enter and
        restore on exit.

        '''
        with ExitStack() as stack:
            for viz in self.iter_vizs():
                stack.enter_context(
                    viz.graphics.reset_cache(),
                )

                # also reset any downsampled alt-graphics objects which
                # might be active.
                dsg = viz.ds_graphics
                if dsg:
                    stack.enter_context(
                        dsg.reset_cache(),
                    )
            try:
                yield
            finally:
                stack.close()
