commit
						8fe2bd6614
					
				| 
						 | 
					@ -1,5 +1,5 @@
 | 
				
			||||||
# piker: trading gear for hackers
 | 
					# piker: trading gear for hackers
 | 
				
			||||||
# Copyright (C) 2018-present  Tyler Goodlet (in stewardship of piker0)
 | 
					# Copyright (C) Tyler Goodlet (in stewardship of pikers)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# This program is free software: you can redistribute it and/or modify
 | 
					# 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
 | 
					# it under the terms of the GNU Affero General Public License as published by
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,21 +18,26 @@
 | 
				
			||||||
Anchor funtions for UI placement of annotions.
 | 
					Anchor funtions for UI placement of annotions.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
'''
 | 
					'''
 | 
				
			||||||
from typing import Callable
 | 
					from __future__ import annotations
 | 
				
			||||||
 | 
					from typing import Callable, TYPE_CHECKING
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from PyQt5.QtCore import QPointF
 | 
					from PyQt5.QtCore import QPointF
 | 
				
			||||||
from PyQt5.QtWidgets import QGraphicsPathItem
 | 
					from PyQt5.QtWidgets import QGraphicsPathItem
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					if TYPE_CHECKING:
 | 
				
			||||||
 | 
					    from ._axes import PriceAxis
 | 
				
			||||||
 | 
					    from ._chart import ChartPlotWidget
 | 
				
			||||||
    from ._label import Label
 | 
					    from ._label import Label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def marker_right_points(
 | 
					def marker_right_points(
 | 
				
			||||||
 | 
					    chart: ChartPlotWidget,  # noqa
 | 
				
			||||||
    chart: 'ChartPlotWidget',  # noqa
 | 
					 | 
				
			||||||
    marker_size: int = 20,
 | 
					    marker_size: int = 20,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> (float, float, float):
 | 
					) -> (float, float, float):
 | 
				
			||||||
    '''Return x-dimension, y-axis-aware, level-line marker oriented scene values.
 | 
					    '''
 | 
				
			||||||
 | 
					    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
 | 
					    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"
 | 
					    a paried level line marker, and the right most side of the "right"
 | 
				
			||||||
| 
						 | 
					@ -57,16 +62,17 @@ def vbr_left(
 | 
				
			||||||
    label: Label,
 | 
					    label: Label,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> Callable[..., float]:
 | 
					) -> Callable[..., float]:
 | 
				
			||||||
    """Return a closure which gives the scene x-coordinate for the
 | 
					    '''
 | 
				
			||||||
    leftmost point of the containing view box.
 | 
					    Return a closure which gives the scene x-coordinate for the leftmost
 | 
				
			||||||
 | 
					    point of the containing view box.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    """
 | 
					    '''
 | 
				
			||||||
    return label.vbr().left
 | 
					    return label.vbr().left
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def right_axis(
 | 
					def right_axis(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    chart: 'ChartPlotWidget',  # noqa
 | 
					    chart: ChartPlotWidget,  # noqa
 | 
				
			||||||
    label: Label,
 | 
					    label: Label,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    side: str = 'left',
 | 
					    side: str = 'left',
 | 
				
			||||||
| 
						 | 
					@ -141,13 +147,13 @@ def gpath_pin(
 | 
				
			||||||
        return path_br.bottomRight() - QPointF(label.w, label.h / 6)
 | 
					        return path_br.bottomRight() - QPointF(label.w, label.h / 6)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					 | 
				
			||||||
def pp_tight_and_right(
 | 
					def pp_tight_and_right(
 | 
				
			||||||
    label: Label
 | 
					    label: Label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> QPointF:
 | 
					) -> QPointF:
 | 
				
			||||||
    '''Place *just* right of the pp label.
 | 
					    '''
 | 
				
			||||||
 | 
					    Place *just* right of the pp label.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    txt = label.txt
 | 
					    # txt = label.txt
 | 
				
			||||||
    return label.txt.pos() + QPointF(label.w - label.h/3, 0)
 | 
					    return label.txt.pos() + QPointF(label.w - label.h/3, 0)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,8 +18,8 @@
 | 
				
			||||||
Chart axes graphics and behavior.
 | 
					Chart axes graphics and behavior.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
"""
 | 
					"""
 | 
				
			||||||
import functools
 | 
					from functools import lru_cache
 | 
				
			||||||
from typing import List, Tuple, Optional
 | 
					from typing import List, Tuple, Optional, Callable
 | 
				
			||||||
from math import floor
 | 
					from math import floor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import pandas as pd
 | 
					import pandas as pd
 | 
				
			||||||
| 
						 | 
					@ -27,8 +27,10 @@ import pyqtgraph as pg
 | 
				
			||||||
from PyQt5 import QtCore, QtGui, QtWidgets
 | 
					from PyQt5 import QtCore, QtGui, QtWidgets
 | 
				
			||||||
from PyQt5.QtCore import QPointF
 | 
					from PyQt5.QtCore import QPointF
 | 
				
			||||||
 | 
					
 | 
				
			||||||
from ._style import DpiAwareFont, hcolor, _font
 | 
					 | 
				
			||||||
from ..data._source import float_digits
 | 
					from ..data._source import float_digits
 | 
				
			||||||
 | 
					from ._label import Label
 | 
				
			||||||
 | 
					from ._style import DpiAwareFont, hcolor, _font
 | 
				
			||||||
 | 
					from ._interaction import ChartView
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_axis_pen = pg.mkPen(hcolor('bracket'))
 | 
					_axis_pen = pg.mkPen(hcolor('bracket'))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -42,7 +44,6 @@ class Axis(pg.AxisItem):
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        linkedsplits,
 | 
					        linkedsplits,
 | 
				
			||||||
        typical_max_str: str = '100 000.000',
 | 
					        typical_max_str: str = '100 000.000',
 | 
				
			||||||
        min_tick: int = 2,
 | 
					 | 
				
			||||||
        **kwargs
 | 
					        **kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
| 
						 | 
					@ -52,7 +53,6 @@ class Axis(pg.AxisItem):
 | 
				
			||||||
        # self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
 | 
					        # self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.linkedsplits = linkedsplits
 | 
					        self.linkedsplits = linkedsplits
 | 
				
			||||||
        self._min_tick = min_tick
 | 
					 | 
				
			||||||
        self._dpi_font = _font
 | 
					        self._dpi_font = _font
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.setTickFont(_font.font)
 | 
					        self.setTickFont(_font.font)
 | 
				
			||||||
| 
						 | 
					@ -74,7 +74,10 @@ class Axis(pg.AxisItem):
 | 
				
			||||||
        })
 | 
					        })
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.setTickFont(_font.font)
 | 
					        self.setTickFont(_font.font)
 | 
				
			||||||
 | 
					        # NOTE: this is for surrounding "border"
 | 
				
			||||||
        self.setPen(_axis_pen)
 | 
					        self.setPen(_axis_pen)
 | 
				
			||||||
 | 
					        # this is the text color
 | 
				
			||||||
 | 
					        self.setTextPen(_axis_pen)
 | 
				
			||||||
        self.typical_br = _font._qfm.boundingRect(typical_max_str)
 | 
					        self.typical_br = _font._qfm.boundingRect(typical_max_str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # size the pertinent axis dimension to a "typical value"
 | 
					        # size the pertinent axis dimension to a "typical value"
 | 
				
			||||||
| 
						 | 
					@ -83,40 +86,102 @@ class Axis(pg.AxisItem):
 | 
				
			||||||
    def size_to_values(self) -> None:
 | 
					    def size_to_values(self) -> None:
 | 
				
			||||||
        pass
 | 
					        pass
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def set_min_tick(self, size: int) -> None:
 | 
					 | 
				
			||||||
        self._min_tick = size
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def txt_offsets(self) -> Tuple[int, int]:
 | 
					    def txt_offsets(self) -> Tuple[int, int]:
 | 
				
			||||||
        return tuple(self.style['tickTextOffset'])
 | 
					        return tuple(self.style['tickTextOffset'])
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class PriceAxis(Axis):
 | 
					class PriceAxis(Axis):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def __init__(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        *args,
 | 
				
			||||||
 | 
					        min_tick: int = 2,
 | 
				
			||||||
 | 
					        title: str = '',
 | 
				
			||||||
 | 
					        formatter: Optional[Callable[[float], str]] = None,
 | 
				
			||||||
 | 
					        **kwargs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ) -> None:
 | 
				
			||||||
 | 
					        super().__init__(*args, **kwargs)
 | 
				
			||||||
 | 
					        self.formatter = formatter
 | 
				
			||||||
 | 
					        self._min_tick: int = min_tick
 | 
				
			||||||
 | 
					        self.title = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def set_title(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        title: str,
 | 
				
			||||||
 | 
					        view: Optional[ChartView] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    ) -> Label:
 | 
				
			||||||
 | 
					        '''
 | 
				
			||||||
 | 
					        Set a sane UX label using our built-in ``Label``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        '''
 | 
				
			||||||
 | 
					        # XXX: built-in labels but they're huge, and placed weird..
 | 
				
			||||||
 | 
					        # self.setLabel(title)
 | 
				
			||||||
 | 
					        # self.showLabel()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        label = self.title = Label(
 | 
				
			||||||
 | 
					            view=view or self.linkedView(),
 | 
				
			||||||
 | 
					            fmt_str=title,
 | 
				
			||||||
 | 
					            color='bracket',
 | 
				
			||||||
 | 
					            parent=self,
 | 
				
			||||||
 | 
					            # update_on_range_change=False,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        def below_axis() -> QPointF:
 | 
				
			||||||
 | 
					            return QPointF(
 | 
				
			||||||
 | 
					                0,
 | 
				
			||||||
 | 
					                self.size().height(),
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # XXX: doesn't work? have to pass it above
 | 
				
			||||||
 | 
					        # label.txt.setParent(self)
 | 
				
			||||||
 | 
					        label.scene_anchor = below_axis
 | 
				
			||||||
 | 
					        label.render()
 | 
				
			||||||
 | 
					        label.show()
 | 
				
			||||||
 | 
					        label.update()
 | 
				
			||||||
 | 
					        return label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    def set_min_tick(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        size: int
 | 
				
			||||||
 | 
					    ) -> None:
 | 
				
			||||||
 | 
					        self._min_tick = size
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def size_to_values(self) -> None:
 | 
					    def size_to_values(self) -> None:
 | 
				
			||||||
 | 
					        # self.typical_br = _font._qfm.boundingRect(typical_max_str)
 | 
				
			||||||
        self.setWidth(self.typical_br.width())
 | 
					        self.setWidth(self.typical_br.width())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    # XXX: drop for now since it just eats up h space
 | 
					    # XXX: drop for now since it just eats up h space
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def tickStrings(
 | 
					    def tickStrings(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        vals,
 | 
					        vals: tuple[float],
 | 
				
			||||||
        scale,
 | 
					        scale: float,
 | 
				
			||||||
        spacing,
 | 
					        spacing: float,
 | 
				
			||||||
    ):
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # TODO: figure out how to enforce min tick spacing by passing
 | 
					    ) -> list[str]:
 | 
				
			||||||
        # it into the parent type
 | 
					        # TODO: figure out how to enforce min tick spacing by passing it
 | 
				
			||||||
        digits = max(float_digits(spacing * scale), self._min_tick)
 | 
					        # into the parent type
 | 
				
			||||||
 | 
					        digits = max(
 | 
				
			||||||
 | 
					            float_digits(spacing * scale),
 | 
				
			||||||
 | 
					            self._min_tick,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					        if self.title:
 | 
				
			||||||
 | 
					            self.title.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # print(f'vals: {vals}\nscale: {scale}\nspacing: {spacing}')
 | 
					        # print(f'vals: {vals}\nscale: {scale}\nspacing: {spacing}')
 | 
				
			||||||
        # print(f'digits: {digits}')
 | 
					        # print(f'digits: {digits}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if not self.formatter:
 | 
				
			||||||
            return [
 | 
					            return [
 | 
				
			||||||
                ('{value:,.{digits}f}').format(
 | 
					                ('{value:,.{digits}f}').format(
 | 
				
			||||||
                    digits=digits,
 | 
					                    digits=digits,
 | 
				
			||||||
                    value=v,
 | 
					                    value=v,
 | 
				
			||||||
                ).replace(',', ' ') for v in vals
 | 
					                ).replace(',', ' ') for v in vals
 | 
				
			||||||
            ]
 | 
					            ]
 | 
				
			||||||
 | 
					        else:
 | 
				
			||||||
 | 
					            return list(map(self.formatter, vals))
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class DynamicDateAxis(Axis):
 | 
					class DynamicDateAxis(Axis):
 | 
				
			||||||
| 
						 | 
					@ -136,6 +201,7 @@ class DynamicDateAxis(Axis):
 | 
				
			||||||
    def _indexes_to_timestrs(
 | 
					    def _indexes_to_timestrs(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        indexes: List[int],
 | 
					        indexes: List[int],
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> List[str]:
 | 
					    ) -> List[str]:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        chart = self.linkedsplits.chart
 | 
					        chart = self.linkedsplits.chart
 | 
				
			||||||
| 
						 | 
					@ -165,9 +231,10 @@ class DynamicDateAxis(Axis):
 | 
				
			||||||
    def tickStrings(
 | 
					    def tickStrings(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        values: tuple[float],
 | 
					        values: tuple[float],
 | 
				
			||||||
        scale,
 | 
					        scale: float,
 | 
				
			||||||
        spacing,
 | 
					        spacing: float,
 | 
				
			||||||
    ):
 | 
					
 | 
				
			||||||
 | 
					    ) -> list[str]:
 | 
				
			||||||
        # info = self.tickStrings.cache_info()
 | 
					        # info = self.tickStrings.cache_info()
 | 
				
			||||||
        # print(info)
 | 
					        # print(info)
 | 
				
			||||||
        return self._indexes_to_timestrs(values)
 | 
					        return self._indexes_to_timestrs(values)
 | 
				
			||||||
| 
						 | 
					@ -220,6 +287,8 @@ class AxisLabel(pg.GraphicsObject):
 | 
				
			||||||
        self.path = None
 | 
					        self.path = None
 | 
				
			||||||
        self.rect = None
 | 
					        self.rect = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self._pw = self.pixelWidth()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def paint(
 | 
					    def paint(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        p: QtGui.QPainter,
 | 
					        p: QtGui.QPainter,
 | 
				
			||||||
| 
						 | 
					@ -269,9 +338,10 @@ class AxisLabel(pg.GraphicsObject):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def boundingRect(self):  # noqa
 | 
					    def boundingRect(self):  # noqa
 | 
				
			||||||
        """Size the graphics space from the text contents.
 | 
					        '''
 | 
				
			||||||
 | 
					        Size the graphics space from the text contents.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        """
 | 
					        '''
 | 
				
			||||||
        if self.label_str:
 | 
					        if self.label_str:
 | 
				
			||||||
            self._size_br_from_str(self.label_str)
 | 
					            self._size_br_from_str(self.label_str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -287,23 +357,32 @@ class AxisLabel(pg.GraphicsObject):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return QtCore.QRectF()
 | 
					        return QtCore.QRectF()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # return self.rect or QtCore.QRectF()
 | 
					    # TODO: but the input probably needs to be the "len" of
 | 
				
			||||||
 | 
					    # the current text value:
 | 
				
			||||||
 | 
					    @lru_cache
 | 
				
			||||||
 | 
					    def _size_br_from_str(
 | 
				
			||||||
 | 
					        self,
 | 
				
			||||||
 | 
					        value: str
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _size_br_from_str(self, value: str) -> None:
 | 
					    ) -> tuple[float, float]:
 | 
				
			||||||
        """Do our best to render the bounding rect to a set margin
 | 
					        '''
 | 
				
			||||||
 | 
					        Do our best to render the bounding rect to a set margin
 | 
				
			||||||
        around provided string contents.
 | 
					        around provided string contents.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        """
 | 
					        '''
 | 
				
			||||||
        # size the filled rect to text and/or parent axis
 | 
					        # size the filled rect to text and/or parent axis
 | 
				
			||||||
        # if not self._txt_br:
 | 
					        # if not self._txt_br:
 | 
				
			||||||
        #     # XXX: this can't be c
 | 
					        #     # XXX: this can't be called until stuff is rendered?
 | 
				
			||||||
        #     self._txt_br = self._dpifont.boundingRect(value)
 | 
					        #     self._txt_br = self._dpifont.boundingRect(value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        txt_br = self._txt_br = self._dpifont.boundingRect(value)
 | 
					        txt_br = self._txt_br = self._dpifont.boundingRect(value)
 | 
				
			||||||
        txt_h, txt_w = txt_br.height(), txt_br.width()
 | 
					        txt_h, txt_w = txt_br.height(), txt_br.width()
 | 
				
			||||||
 | 
					        # print(f'wsw: {self._dpifont.boundingRect(" ")}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # allow subtypes to specify a static width and height
 | 
					        # allow subtypes to specify a static width and height
 | 
				
			||||||
        h, w = self.size_hint()
 | 
					        h, w = self.size_hint()
 | 
				
			||||||
 | 
					        # print(f'axis size: {self._parent.size()}')
 | 
				
			||||||
 | 
					        # print(f'axis geo: {self._parent.geometry()}')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.rect = QtCore.QRectF(
 | 
					        self.rect = QtCore.QRectF(
 | 
				
			||||||
            0, 0,
 | 
					            0, 0,
 | 
				
			||||||
| 
						 | 
					@ -314,7 +393,7 @@ class AxisLabel(pg.GraphicsObject):
 | 
				
			||||||
        # hb = self.path.controlPointRect()
 | 
					        # hb = self.path.controlPointRect()
 | 
				
			||||||
        # hb_size = hb.size()
 | 
					        # hb_size = hb.size()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return self.rect
 | 
					        return (self.rect.width(), self.rect.height())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# _common_text_flags = (
 | 
					# _common_text_flags = (
 | 
				
			||||||
#     QtCore.Qt.TextDontClip |
 | 
					#     QtCore.Qt.TextDontClip |
 | 
				
			||||||
| 
						 | 
					@ -342,6 +421,7 @@ class XAxisLabel(AxisLabel):
 | 
				
			||||||
        abs_pos: QPointF,  # scene coords
 | 
					        abs_pos: QPointF,  # scene coords
 | 
				
			||||||
        value: float,  # data for text
 | 
					        value: float,  # data for text
 | 
				
			||||||
        offset: int = 0  # if have margins, k?
 | 
					        offset: int = 0  # if have margins, k?
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        timestrs = self._parent._indexes_to_timestrs([int(value)])
 | 
					        timestrs = self._parent._indexes_to_timestrs([int(value)])
 | 
				
			||||||
| 
						 | 
					@ -356,17 +436,19 @@ class XAxisLabel(AxisLabel):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        w = self.boundingRect().width()
 | 
					        w = self.boundingRect().width()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.setPos(QPointF(
 | 
					        self.setPos(
 | 
				
			||||||
            abs_pos.x() - w/2,
 | 
					            QPointF(
 | 
				
			||||||
 | 
					                abs_pos.x() - w/2 - self._pw,
 | 
				
			||||||
                y_offset/2,
 | 
					                y_offset/2,
 | 
				
			||||||
        ))
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        self.update()
 | 
					        self.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def _draw_arrow_path(self):
 | 
					    def _draw_arrow_path(self):
 | 
				
			||||||
        y_offset = self._parent.style['tickTextOffset'][1]
 | 
					        y_offset = self._parent.style['tickTextOffset'][1]
 | 
				
			||||||
        path = QtGui.QPainterPath()
 | 
					        path = QtGui.QPainterPath()
 | 
				
			||||||
        h, w = self.rect.height(), self.rect.width()
 | 
					        h, w = self.rect.height(), self.rect.width()
 | 
				
			||||||
        middle = w/2 - 0.5
 | 
					        middle = w/2 - self._pw * 0.5
 | 
				
			||||||
        aw = h/2
 | 
					        aw = h/2
 | 
				
			||||||
        left = middle - aw
 | 
					        left = middle - aw
 | 
				
			||||||
        right = middle + aw
 | 
					        right = middle + aw
 | 
				
			||||||
| 
						 | 
					@ -410,8 +492,12 @@ class YAxisLabel(AxisLabel):
 | 
				
			||||||
            self.x_offset, y_offset = self._parent.txt_offsets()
 | 
					            self.x_offset, y_offset = self._parent.txt_offsets()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def size_hint(self) -> Tuple[float, float]:
 | 
					    def size_hint(self) -> Tuple[float, float]:
 | 
				
			||||||
        # size to parent axis width
 | 
					        # size to parent axis width(-ish)
 | 
				
			||||||
        return None, self._parent.width()
 | 
					        wsh = self._dpifont.boundingRect(' ').height() / 2
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					            None,
 | 
				
			||||||
 | 
					            self._parent.size().width() - wsh,
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_label(
 | 
					    def update_label(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
| 
						 | 
					@ -432,16 +518,19 @@ class YAxisLabel(AxisLabel):
 | 
				
			||||||
        br = self.boundingRect()
 | 
					        br = self.boundingRect()
 | 
				
			||||||
        h = br.height()
 | 
					        h = br.height()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.setPos(QPointF(
 | 
					        self.setPos(
 | 
				
			||||||
 | 
					            QPointF(
 | 
				
			||||||
                x_offset,
 | 
					                x_offset,
 | 
				
			||||||
            abs_pos.y() - h / 2 - self._y_margin / 2
 | 
					                abs_pos.y() - h / 2 - self._pw,
 | 
				
			||||||
        ))
 | 
					            )
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
        self.update()
 | 
					        self.update()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update_on_resize(self, vr, r):
 | 
					    def update_on_resize(self, vr, r):
 | 
				
			||||||
        """Tiis is a ``.sigRangeChanged()`` handler.
 | 
					        '''
 | 
				
			||||||
 | 
					        This is a ``.sigRangeChanged()`` handler.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        """
 | 
					        '''
 | 
				
			||||||
        index, last = self._last_datum
 | 
					        index, last = self._last_datum
 | 
				
			||||||
        if index is not None:
 | 
					        if index is not None:
 | 
				
			||||||
            self.update_from_data(index, last)
 | 
					            self.update_from_data(index, last)
 | 
				
			||||||
| 
						 | 
					@ -451,11 +540,13 @@ class YAxisLabel(AxisLabel):
 | 
				
			||||||
        index: int,
 | 
					        index: int,
 | 
				
			||||||
        value: float,
 | 
					        value: float,
 | 
				
			||||||
        _save_last: bool = True,
 | 
					        _save_last: bool = True,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
        """Update the label's text contents **and** position from
 | 
					        '''
 | 
				
			||||||
 | 
					        Update the label's text contents **and** position from
 | 
				
			||||||
        a view box coordinate datum.
 | 
					        a view box coordinate datum.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        """
 | 
					        '''
 | 
				
			||||||
        if _save_last:
 | 
					        if _save_last:
 | 
				
			||||||
            self._last_datum = (index, value)
 | 
					            self._last_datum = (index, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -469,7 +560,7 @@ class YAxisLabel(AxisLabel):
 | 
				
			||||||
        path = QtGui.QPainterPath()
 | 
					        path = QtGui.QPainterPath()
 | 
				
			||||||
        h = self.rect.height()
 | 
					        h = self.rect.height()
 | 
				
			||||||
        path.moveTo(0, 0)
 | 
					        path.moveTo(0, 0)
 | 
				
			||||||
        path.lineTo(-x_offset - h/4, h/2.)
 | 
					        path.lineTo(-x_offset - h/4, h/2. - self._pw/2)
 | 
				
			||||||
        path.lineTo(0, h)
 | 
					        path.lineTo(0, h)
 | 
				
			||||||
        path.closeSubpath()
 | 
					        path.closeSubpath()
 | 
				
			||||||
        self.path = path
 | 
					        self.path = path
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -479,14 +479,20 @@ class LinkedSplits(QWidget):
 | 
				
			||||||
            axisItems=axes,
 | 
					            axisItems=axes,
 | 
				
			||||||
            **cpw_kwargs,
 | 
					            **cpw_kwargs,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					        cpw.hideAxis('left')
 | 
				
			||||||
 | 
					        cpw.hideAxis('bottom')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.xaxis_chart:
 | 
					        if self.xaxis_chart:
 | 
				
			||||||
 | 
					            self.xaxis_chart.hideAxis('bottom')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # presuming we only want it at the true bottom of all charts.
 | 
					            # presuming we only want it at the true bottom of all charts.
 | 
				
			||||||
            # XXX: uses new api from our ``pyqtgraph`` fork.
 | 
					            # XXX: uses new api from our ``pyqtgraph`` fork.
 | 
				
			||||||
            # https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master
 | 
					            # https://github.com/pikers/pyqtgraph/tree/plotitemoverlay_onto_pg_master
 | 
				
			||||||
            _ = self.xaxis_chart.removeAxis('bottom', unlink=False)
 | 
					            # _ = self.xaxis_chart.removeAxis('bottom', unlink=False)
 | 
				
			||||||
            assert 'bottom' not in self.xaxis_chart.plotItem.axes
 | 
					            # assert 'bottom' not in self.xaxis_chart.plotItem.axes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            self.xaxis_chart = cpw
 | 
					            self.xaxis_chart = cpw
 | 
				
			||||||
 | 
					            cpw.showAxis('bottom')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if self.xaxis_chart is None:
 | 
					        if self.xaxis_chart is None:
 | 
				
			||||||
            self.xaxis_chart = cpw
 | 
					            self.xaxis_chart = cpw
 | 
				
			||||||
| 
						 | 
					@ -726,11 +732,6 @@ class ChartPlotWidget(pg.PlotWidget):
 | 
				
			||||||
        self._static_yrange = static_yrange  # for "known y-range style"
 | 
					        self._static_yrange = static_yrange  # for "known y-range style"
 | 
				
			||||||
        self._view_mode: str = 'follow'
 | 
					        self._view_mode: str = 'follow'
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # show only right side axes
 | 
					 | 
				
			||||||
        self.hideAxis('left')
 | 
					 | 
				
			||||||
        self.showAxis('right')
 | 
					 | 
				
			||||||
        # self.showAxis('left')
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        # show background grid
 | 
					        # show background grid
 | 
				
			||||||
        self.showGrid(x=False, y=True, alpha=0.3)
 | 
					        self.showGrid(x=False, y=True, alpha=0.3)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -862,53 +863,58 @@ class ChartPlotWidget(pg.PlotWidget):
 | 
				
			||||||
    def overlay_plotitem(
 | 
					    def overlay_plotitem(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        name: str,
 | 
					        name: str,
 | 
				
			||||||
 | 
					        index: Optional[int] = None,
 | 
				
			||||||
 | 
					        axis_title: Optional[str] = None,
 | 
				
			||||||
 | 
					        axis_side: str = 'right',
 | 
				
			||||||
 | 
					        axis_kwargs: dict = {},
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> pg.PlotItem:
 | 
					    ) -> pg.PlotItem:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Custom viewbox impl
 | 
					        # Custom viewbox impl
 | 
				
			||||||
        cv = self.mk_vb(name)
 | 
					        cv = self.mk_vb(name)
 | 
				
			||||||
        cv.chart = self
 | 
					        cv.chart = self
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # xaxis = DynamicDateAxis(
 | 
					        allowed_sides = {'left', 'right'}
 | 
				
			||||||
        #     orientation='bottom',
 | 
					        if axis_side not in allowed_sides:
 | 
				
			||||||
        #     linkedsplits=self.linked,
 | 
					            raise ValueError(f'``axis_side``` must be in {allowed_sides}')
 | 
				
			||||||
        # )
 | 
					 | 
				
			||||||
        yaxis = PriceAxis(
 | 
					        yaxis = PriceAxis(
 | 
				
			||||||
            orientation='right',
 | 
					            orientation=axis_side,
 | 
				
			||||||
            linkedsplits=self.linked,
 | 
					            linkedsplits=self.linked,
 | 
				
			||||||
 | 
					            **axis_kwargs,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        plotitem = pg.PlotItem(
 | 
					        pi = pg.PlotItem(
 | 
				
			||||||
            parent=self.plotItem,
 | 
					            parent=self.plotItem,
 | 
				
			||||||
            name=name,
 | 
					            name=name,
 | 
				
			||||||
            enableMenu=False,
 | 
					            enableMenu=False,
 | 
				
			||||||
            viewBox=cv,
 | 
					            viewBox=cv,
 | 
				
			||||||
            axisItems={
 | 
					            axisItems={
 | 
				
			||||||
                # 'bottom': xaxis,
 | 
					                # 'bottom': xaxis,
 | 
				
			||||||
                'right': yaxis,
 | 
					                axis_side: yaxis,
 | 
				
			||||||
            },
 | 
					            },
 | 
				
			||||||
            default_axes=[],
 | 
					            default_axes=[],
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        # plotitem.setAxisItems(
 | 
					        pi.hideButtons()
 | 
				
			||||||
        #     add_to_layout=False,
 | 
					
 | 
				
			||||||
        #     axisItems={
 | 
					 | 
				
			||||||
        #         'bottom': xaxis,
 | 
					 | 
				
			||||||
        #         'right': yaxis,
 | 
					 | 
				
			||||||
        #     },
 | 
					 | 
				
			||||||
        # )
 | 
					 | 
				
			||||||
        # plotite.hideAxis('right')
 | 
					 | 
				
			||||||
        # plotite.hideAxis('bottom')
 | 
					 | 
				
			||||||
        # plotitem.addItem(curve)
 | 
					 | 
				
			||||||
        cv.enable_auto_yrange()
 | 
					        cv.enable_auto_yrange()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # plotitem.enableAutoRange(axis='y')
 | 
					        # compose this new plot's graphics with the current chart's
 | 
				
			||||||
        plotitem.hideButtons()
 | 
					        # existing one but with separate axes as neede and specified.
 | 
				
			||||||
 | 
					 | 
				
			||||||
        self.pi_overlay.add_plotitem(
 | 
					        self.pi_overlay.add_plotitem(
 | 
				
			||||||
            plotitem,
 | 
					            pi,
 | 
				
			||||||
 | 
					            index=index,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # only link x-axes,
 | 
					            # only link x-axes,
 | 
				
			||||||
            link_axes=(0,),
 | 
					            link_axes=(0,),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        return plotitem
 | 
					
 | 
				
			||||||
 | 
					        # add axis title
 | 
				
			||||||
 | 
					        # TODO: do we want this API to still work?
 | 
				
			||||||
 | 
					        # raxis = pi.getAxis('right')
 | 
				
			||||||
 | 
					        axis = self.pi_overlay.get_axis(pi, axis_side)
 | 
				
			||||||
 | 
					        axis.set_title(axis_title or name, view=pi.getViewBox())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return pi
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def draw_curve(
 | 
					    def draw_curve(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
| 
						 | 
					@ -1014,7 +1020,8 @@ class ChartPlotWidget(pg.PlotWidget):
 | 
				
			||||||
        # add y-axis "last" value label
 | 
					        # add y-axis "last" value label
 | 
				
			||||||
        last = self._ysticks[name] = YAxisLabel(
 | 
					        last = self._ysticks[name] = YAxisLabel(
 | 
				
			||||||
            chart=self,
 | 
					            chart=self,
 | 
				
			||||||
            parent=self.getAxis('right'),
 | 
					            # parent=self.getAxis('right'),
 | 
				
			||||||
 | 
					            parent=self.pi_overlay.get_axis(self.plotItem, 'right'),
 | 
				
			||||||
            # TODO: pass this from symbol data
 | 
					            # TODO: pass this from symbol data
 | 
				
			||||||
            digits=digits,
 | 
					            digits=digits,
 | 
				
			||||||
            opacity=1,
 | 
					            opacity=1,
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,8 +43,8 @@ log = get_logger(__name__)
 | 
				
			||||||
# latency (in terms of perceived lag in cross hair) so really be sure
 | 
					# latency (in terms of perceived lag in cross hair) so really be sure
 | 
				
			||||||
# there's an improvement if you want to change it!
 | 
					# there's an improvement if you want to change it!
 | 
				
			||||||
 | 
					
 | 
				
			||||||
_mouse_rate_limit = 58  # TODO; should we calc current screen refresh rate?
 | 
					_mouse_rate_limit = 120  # TODO; should we calc current screen refresh rate?
 | 
				
			||||||
_debounce_delay = 1 / 60
 | 
					_debounce_delay = 1 / 40
 | 
				
			||||||
_ch_label_opac = 1
 | 
					_ch_label_opac = 1
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -369,7 +369,13 @@ class Cursor(pg.GraphicsObject):
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        plot: 'ChartPlotWidget',  # noqa
 | 
					        plot: 'ChartPlotWidget',  # noqa
 | 
				
			||||||
        digits: int = 0,
 | 
					        digits: int = 0,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
 | 
					        '''
 | 
				
			||||||
 | 
					        Add chart to tracked set such that a cross-hair and possibly
 | 
				
			||||||
 | 
					        curve tracking cursor can be drawn on the plot.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        '''
 | 
				
			||||||
        # add ``pg.graphicsItems.InfiniteLine``s
 | 
					        # add ``pg.graphicsItems.InfiniteLine``s
 | 
				
			||||||
        # vertical and horizonal lines and a y-axis label
 | 
					        # vertical and horizonal lines and a y-axis label
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -382,7 +388,8 @@ class Cursor(pg.GraphicsObject):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        yl = YAxisLabel(
 | 
					        yl = YAxisLabel(
 | 
				
			||||||
            chart=plot,
 | 
					            chart=plot,
 | 
				
			||||||
            parent=plot.getAxis('right'),
 | 
					            # parent=plot.getAxis('right'),
 | 
				
			||||||
 | 
					            parent=plot.pi_overlay.get_axis(plot.plotItem, 'right'),
 | 
				
			||||||
            digits=digits or self.digits,
 | 
					            digits=digits or self.digits,
 | 
				
			||||||
            opacity=_ch_label_opac,
 | 
					            opacity=_ch_label_opac,
 | 
				
			||||||
            bg_color=self.label_color,
 | 
					            bg_color=self.label_color,
 | 
				
			||||||
| 
						 | 
					@ -424,19 +431,25 @@ class Cursor(pg.GraphicsObject):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # ONLY create an x-axis label for the cursor
 | 
					        # ONLY create an x-axis label for the cursor
 | 
				
			||||||
        # if this plot owns the 'bottom' axis.
 | 
					        # if this plot owns the 'bottom' axis.
 | 
				
			||||||
        if 'bottom' in plot.plotItem.axes:
 | 
					        # if 'bottom' in plot.plotItem.axes:
 | 
				
			||||||
            self.xaxis_label = XAxisLabel(
 | 
					        if plot.linked.xaxis_chart is plot:
 | 
				
			||||||
 | 
					            xlabel = self.xaxis_label = XAxisLabel(
 | 
				
			||||||
                parent=self.plots[plot_index].getAxis('bottom'),
 | 
					                parent=self.plots[plot_index].getAxis('bottom'),
 | 
				
			||||||
 | 
					                # parent=self.plots[plot_index].pi_overlay.get_axis(plot.plotItem, 'bottom'),
 | 
				
			||||||
                opacity=_ch_label_opac,
 | 
					                opacity=_ch_label_opac,
 | 
				
			||||||
                bg_color=self.label_color,
 | 
					                bg_color=self.label_color,
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            # place label off-screen during startup
 | 
					            # place label off-screen during startup
 | 
				
			||||||
            self.xaxis_label.setPos(self.plots[0].mapFromView(QPointF(0, 0)))
 | 
					            xlabel.setPos(
 | 
				
			||||||
 | 
					                self.plots[0].mapFromView(QPointF(0, 0))
 | 
				
			||||||
 | 
					            )
 | 
				
			||||||
 | 
					            xlabel.show()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def add_curve_cursor(
 | 
					    def add_curve_cursor(
 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        plot: 'ChartPlotWidget',  # noqa
 | 
					        plot: 'ChartPlotWidget',  # noqa
 | 
				
			||||||
        curve: 'PlotCurveItem',  # noqa
 | 
					        curve: 'PlotCurveItem',  # noqa
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> LineDot:
 | 
					    ) -> LineDot:
 | 
				
			||||||
        # if this plot contains curves add line dot "cursors" to denote
 | 
					        # if this plot contains curves add line dot "cursors" to denote
 | 
				
			||||||
        # the current sample under the mouse
 | 
					        # the current sample under the mouse
 | 
				
			||||||
| 
						 | 
					@ -493,24 +506,27 @@ class Cursor(pg.GraphicsObject):
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ix = round(x)  # since bars are centered around index
 | 
					        ix = round(x)  # since bars are centered around index
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # px perfect...
 | 
				
			||||||
 | 
					        line_offset = self._lw / 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # round y value to nearest tick step
 | 
					        # round y value to nearest tick step
 | 
				
			||||||
        m = self._y_incr_mult
 | 
					        m = self._y_incr_mult
 | 
				
			||||||
        iy = round(y * m) / m
 | 
					        iy = round(y * m) / m
 | 
				
			||||||
 | 
					        vl_y = iy - line_offset
 | 
				
			||||||
        # px perfect...
 | 
					 | 
				
			||||||
        line_offset = self._lw / 2
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # update y-range items
 | 
					        # update y-range items
 | 
				
			||||||
        if iy != last_iy:
 | 
					        if iy != last_iy:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if self._y_label_update:
 | 
					            if self._y_label_update:
 | 
				
			||||||
                self.graphics[self.active_plot]['yl'].update_label(
 | 
					                self.graphics[self.active_plot]['yl'].update_label(
 | 
				
			||||||
                    abs_pos=plot.mapFromView(QPointF(ix, iy)),
 | 
					                    # abs_pos=plot.mapFromView(QPointF(ix, iy)),
 | 
				
			||||||
 | 
					                    abs_pos=plot.mapFromView(QPointF(ix, vl_y)),
 | 
				
			||||||
                    value=iy
 | 
					                    value=iy
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # only update horizontal xhair line if label is enabled
 | 
					                # only update horizontal xhair line if label is enabled
 | 
				
			||||||
                self.graphics[plot]['hl'].setY(iy)
 | 
					                # self.graphics[plot]['hl'].setY(iy)
 | 
				
			||||||
 | 
					                self.graphics[plot]['hl'].setY(vl_y)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # update all trackers
 | 
					            # update all trackers
 | 
				
			||||||
            for item in self._trackers:
 | 
					            for item in self._trackers:
 | 
				
			||||||
| 
						 | 
					@ -541,9 +557,6 @@ class Cursor(pg.GraphicsObject):
 | 
				
			||||||
                # left axis offset width for calcuating
 | 
					                # left axis offset width for calcuating
 | 
				
			||||||
                # absolute x-axis label placement.
 | 
					                # absolute x-axis label placement.
 | 
				
			||||||
                left_axis_width = 0
 | 
					                left_axis_width = 0
 | 
				
			||||||
 | 
					 | 
				
			||||||
                if 'bottom' in axes:
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                left = axes.get('left')
 | 
					                left = axes.get('left')
 | 
				
			||||||
                if left:
 | 
					                if left:
 | 
				
			||||||
                    left_axis_width = left['item'].width()
 | 
					                    left_axis_width = left['item'].width()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -115,13 +115,14 @@ async def update_linked_charts_graphics(
 | 
				
			||||||
    vlm_chart: Optional[ChartPlotWidget] = None,
 | 
					    vlm_chart: Optional[ChartPlotWidget] = None,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
) -> None:
 | 
					) -> None:
 | 
				
			||||||
    '''The 'main' (price) chart real-time update loop.
 | 
					    '''
 | 
				
			||||||
 | 
					    The 'main' (price) chart real-time update loop.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    Receive from the primary instrument quote stream and update the OHLC
 | 
					    Receive from the primary instrument quote stream and update the OHLC
 | 
				
			||||||
    chart.
 | 
					    chart.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    # TODO: bunch of stuff:
 | 
					    # TODO: bunch of stuff (some might be done already, can't member):
 | 
				
			||||||
    # - I'm starting to think all this logic should be
 | 
					    # - I'm starting to think all this logic should be
 | 
				
			||||||
    #   done in one place and "graphics update routines"
 | 
					    #   done in one place and "graphics update routines"
 | 
				
			||||||
    #   should not be doing any length checking and array diffing.
 | 
					    #   should not be doing any length checking and array diffing.
 | 
				
			||||||
| 
						 | 
					@ -181,13 +182,34 @@ async def update_linked_charts_graphics(
 | 
				
			||||||
    view = chart.view
 | 
					    view = chart.view
 | 
				
			||||||
    last_quote = time.time()
 | 
					    last_quote = time.time()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # async def iter_drain_quotes():
 | 
				
			||||||
 | 
					    #     # NOTE: all code below this loop is expected to be synchronous
 | 
				
			||||||
 | 
					    #     # and thus draw instructions are not picked up jntil the next
 | 
				
			||||||
 | 
					    #     # wait / iteration.
 | 
				
			||||||
 | 
					    #     async for quotes in stream:
 | 
				
			||||||
 | 
					    #         while True:
 | 
				
			||||||
 | 
					    #             try:
 | 
				
			||||||
 | 
					    #                 moar = stream.receive_nowait()
 | 
				
			||||||
 | 
					    #             except trio.WouldBlock:
 | 
				
			||||||
 | 
					    #                 yield quotes
 | 
				
			||||||
 | 
					    #                 break
 | 
				
			||||||
 | 
					    #             else:
 | 
				
			||||||
 | 
					    #                 for sym, quote in moar.items():
 | 
				
			||||||
 | 
					    #                     ticks_frame = quote.get('ticks')
 | 
				
			||||||
 | 
					    #                     if ticks_frame:
 | 
				
			||||||
 | 
					    #                         quotes[sym].setdefault(
 | 
				
			||||||
 | 
					    #                             'ticks', []).extend(ticks_frame)
 | 
				
			||||||
 | 
					    #                     print('pulled extra')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    #                 yield quotes
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # async for quotes in iter_drain_quotes():
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    async for quotes in stream:
 | 
					    async for quotes in stream:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        now = time.time()
 | 
					 | 
				
			||||||
        quote_period = time.time() - last_quote
 | 
					        quote_period = time.time() - last_quote
 | 
				
			||||||
        quote_rate = round(
 | 
					        quote_rate = round(
 | 
				
			||||||
            1/quote_period, 1) if quote_period > 0 else float('inf')
 | 
					            1/quote_period, 1) if quote_period > 0 else float('inf')
 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            quote_period <= 1/_quote_throttle_rate
 | 
					            quote_period <= 1/_quote_throttle_rate
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -196,7 +218,8 @@ async def update_linked_charts_graphics(
 | 
				
			||||||
            and quote_rate >= _quote_throttle_rate * 1.5
 | 
					            and quote_rate >= _quote_throttle_rate * 1.5
 | 
				
			||||||
        ):
 | 
					        ):
 | 
				
			||||||
            log.warning(f'High quote rate {symbol.key}: {quote_rate}')
 | 
					            log.warning(f'High quote rate {symbol.key}: {quote_rate}')
 | 
				
			||||||
        last_quote = now
 | 
					
 | 
				
			||||||
 | 
					        last_quote = time.time()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # chart isn't active/shown so skip render cycle and pause feed(s)
 | 
					        # chart isn't active/shown so skip render cycle and pause feed(s)
 | 
				
			||||||
        if chart.linked.isHidden():
 | 
					        if chart.linked.isHidden():
 | 
				
			||||||
| 
						 | 
					@ -621,9 +644,15 @@ async def display_symbol_data(
 | 
				
			||||||
                await trio.sleep(0)
 | 
					                await trio.sleep(0)
 | 
				
			||||||
                linkedsplits.resize_sidepanes()
 | 
					                linkedsplits.resize_sidepanes()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                # NOTE: we pop the volume chart from the subplots set so
 | 
				
			||||||
 | 
					                # that it isn't double rendered in the display loop
 | 
				
			||||||
 | 
					                # above since we do a maxmin calc on the volume data to
 | 
				
			||||||
 | 
					                # determine if auto-range adjustements should be made.
 | 
				
			||||||
 | 
					                linkedsplits.subplots.pop('volume', None)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # TODO: make this not so shit XD
 | 
					                # TODO: make this not so shit XD
 | 
				
			||||||
                # close group status
 | 
					                # close group status
 | 
				
			||||||
                sbar._status_groups[loading_sym_key][1]()
 | 
					                sbar._status_groups[loading_sym_key][1]()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # let the app run.
 | 
					                # let the app run.. bby
 | 
				
			||||||
                await trio.sleep_forever()
 | 
					                await trio.sleep_forever()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -33,7 +33,9 @@ import pyqtgraph as pg
 | 
				
			||||||
import trio
 | 
					import trio
 | 
				
			||||||
from trio_typing import TaskStatus
 | 
					from trio_typing import TaskStatus
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					from ._axes import PriceAxis
 | 
				
			||||||
from .._cacheables import maybe_open_context
 | 
					from .._cacheables import maybe_open_context
 | 
				
			||||||
 | 
					from ..calc import humanize
 | 
				
			||||||
from ..data._sharedmem import (
 | 
					from ..data._sharedmem import (
 | 
				
			||||||
    ShmArray,
 | 
					    ShmArray,
 | 
				
			||||||
    maybe_open_shm_array,
 | 
					    maybe_open_shm_array,
 | 
				
			||||||
| 
						 | 
					@ -653,7 +655,7 @@ async def open_vlm_displays(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        last_val_sticky.update_from_data(-1, value)
 | 
					        last_val_sticky.update_from_data(-1, value)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        chart.update_curve_from_array(
 | 
					        vlm_curve = chart.update_curve_from_array(
 | 
				
			||||||
            'volume',
 | 
					            'volume',
 | 
				
			||||||
            shm.array,
 | 
					            shm.array,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
| 
						 | 
					@ -661,8 +663,11 @@ async def open_vlm_displays(
 | 
				
			||||||
        # size view to data once at outset
 | 
					        # size view to data once at outset
 | 
				
			||||||
        chart.view._set_yrange()
 | 
					        chart.view._set_yrange()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if not dvlm:
 | 
					        # add axis title
 | 
				
			||||||
            return
 | 
					        axis = chart.getAxis('right')
 | 
				
			||||||
 | 
					        axis.set_title(' vlm')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if dvlm:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # spawn and overlay $ vlm on the same subchart
 | 
					            # spawn and overlay $ vlm on the same subchart
 | 
				
			||||||
            shm, started = await admin.start_engine_task(
 | 
					            shm, started = await admin.start_engine_task(
 | 
				
			||||||
| 
						 | 
					@ -685,7 +690,19 @@ async def open_vlm_displays(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            pi = chart.overlay_plotitem(
 | 
					            pi = chart.overlay_plotitem(
 | 
				
			||||||
                'dolla_vlm',
 | 
					                'dolla_vlm',
 | 
				
			||||||
 | 
					                index=0,  # place axis on inside (nearest to chart)
 | 
				
			||||||
 | 
					                axis_title=' $vlm',
 | 
				
			||||||
 | 
					                axis_side='right',
 | 
				
			||||||
 | 
					                axis_kwargs={
 | 
				
			||||||
 | 
					                    'typical_max_str': ' 100.0 M ',
 | 
				
			||||||
 | 
					                    'formatter': partial(
 | 
				
			||||||
 | 
					                        humanize,
 | 
				
			||||||
 | 
					                        digits=2,
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # add custom auto range handler
 | 
					            # add custom auto range handler
 | 
				
			||||||
            pi.vb._maxmin = partial(maxmin, name='dolla_vlm')
 | 
					            pi.vb._maxmin = partial(maxmin, name='dolla_vlm')
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -696,13 +713,18 @@ async def open_vlm_displays(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                array_key='dolla_vlm',
 | 
					                array_key='dolla_vlm',
 | 
				
			||||||
                overlay=pi,
 | 
					                overlay=pi,
 | 
				
			||||||
            color='charcoal',
 | 
					                # color='bracket',
 | 
				
			||||||
 | 
					                # TODO: this color or dark volume
 | 
				
			||||||
 | 
					                # color='charcoal',
 | 
				
			||||||
                step_mode=True,
 | 
					                step_mode=True,
 | 
				
			||||||
                # **conf.get('chart_kwargs', {})
 | 
					                # **conf.get('chart_kwargs', {})
 | 
				
			||||||
            )
 | 
					            )
 | 
				
			||||||
            # TODO: is there a way to "sync" the dual axes such that only
 | 
					            # TODO: is there a way to "sync" the dual axes such that only
 | 
				
			||||||
            # one curve is needed?
 | 
					            # one curve is needed?
 | 
				
			||||||
        # curve.hide()
 | 
					            # hide the original vlm curve since the $vlm one is now
 | 
				
			||||||
 | 
					            # displayed and the curves are effectively the same minus
 | 
				
			||||||
 | 
					            # liquidity events (well at least on low OHLC periods - 1s).
 | 
				
			||||||
 | 
					            vlm_curve.hide()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # TODO: we need a better API to do this..
 | 
					            # TODO: we need a better API to do this..
 | 
				
			||||||
            # specially store ref to shm for lookup in display loop
 | 
					            # specially store ref to shm for lookup in display loop
 | 
				
			||||||
| 
						 | 
					@ -710,7 +732,8 @@ async def open_vlm_displays(
 | 
				
			||||||
            # ``.draw_curve()``.
 | 
					            # ``.draw_curve()``.
 | 
				
			||||||
            chart._overlays['dolla_vlm'] = shm
 | 
					            chart._overlays['dolla_vlm'] = shm
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # XXX: old dict-style config before it was moved into the helper task
 | 
					            # XXX: old dict-style config before it was moved into the
 | 
				
			||||||
 | 
					            # helper task
 | 
				
			||||||
            #     'dolla_vlm': {
 | 
					            #     'dolla_vlm': {
 | 
				
			||||||
            #         'func_name': 'dolla_vlm',
 | 
					            #         'func_name': 'dolla_vlm',
 | 
				
			||||||
            #         'zero_on_step': True,
 | 
					            #         'zero_on_step': True,
 | 
				
			||||||
| 
						 | 
					@ -729,6 +752,12 @@ async def open_vlm_displays(
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # }
 | 
					            # }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            for name, axis_info in pi.axes.items():
 | 
				
			||||||
 | 
					                # lol this sux XD
 | 
				
			||||||
 | 
					                axis = axis_info['item']
 | 
				
			||||||
 | 
					                if isinstance(axis, PriceAxis):
 | 
				
			||||||
 | 
					                    axis.size_to_values()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # built-in vlm fsps
 | 
					        # built-in vlm fsps
 | 
				
			||||||
        for display_name, conf in {
 | 
					        for display_name, conf in {
 | 
				
			||||||
            'vwap': {
 | 
					            'vwap': {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -34,7 +34,7 @@ from ._style import (
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class Label:
 | 
					class Label:
 | 
				
			||||||
    """
 | 
					    '''
 | 
				
			||||||
    A plain ol' "scene label" using an underlying ``QGraphicsTextItem``.
 | 
					    A plain ol' "scene label" using an underlying ``QGraphicsTextItem``.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    After hacking for many days on multiple "label" systems inside
 | 
					    After hacking for many days on multiple "label" systems inside
 | 
				
			||||||
| 
						 | 
					@ -50,10 +50,8 @@ class Label:
 | 
				
			||||||
    small, re-usable label components that can actually be used to build
 | 
					    small, re-usable label components that can actually be used to build
 | 
				
			||||||
    production grade UIs...
 | 
					    production grade UIs...
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    """
 | 
					    '''
 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __init__(
 | 
					    def __init__(
 | 
				
			||||||
 | 
					 | 
				
			||||||
        self,
 | 
					        self,
 | 
				
			||||||
        view: pg.ViewBox,
 | 
					        view: pg.ViewBox,
 | 
				
			||||||
        fmt_str: str,
 | 
					        fmt_str: str,
 | 
				
			||||||
| 
						 | 
					@ -63,6 +61,7 @@ class Label:
 | 
				
			||||||
        font_size: str = 'small',
 | 
					        font_size: str = 'small',
 | 
				
			||||||
        opacity: float = 1,
 | 
					        opacity: float = 1,
 | 
				
			||||||
        fields: dict = {},
 | 
					        fields: dict = {},
 | 
				
			||||||
 | 
					        parent: pg.GraphicsObject = None,
 | 
				
			||||||
        update_on_range_change: bool = True,
 | 
					        update_on_range_change: bool = True,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
| 
						 | 
					@ -71,11 +70,13 @@ class Label:
 | 
				
			||||||
        self._fmt_str = fmt_str
 | 
					        self._fmt_str = fmt_str
 | 
				
			||||||
        self._view_xy = QPointF(0, 0)
 | 
					        self._view_xy = QPointF(0, 0)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.scene_anchor: Optional[Callable[..., QPointF]] = None
 | 
					        self.scene_anchor: Optional[
 | 
				
			||||||
 | 
					            Callable[..., QPointF]
 | 
				
			||||||
 | 
					        ] = None
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self._x_offset = x_offset
 | 
					        self._x_offset = x_offset
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        txt = self.txt = QtWidgets.QGraphicsTextItem()
 | 
					        txt = self.txt = QtWidgets.QGraphicsTextItem(parent=parent)
 | 
				
			||||||
        txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
 | 
					        txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        vb.scene().addItem(txt)
 | 
					        vb.scene().addItem(txt)
 | 
				
			||||||
| 
						 | 
					@ -86,7 +87,6 @@ class Label:
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        dpi_font.configure_to_dpi()
 | 
					        dpi_font.configure_to_dpi()
 | 
				
			||||||
        txt.setFont(dpi_font.font)
 | 
					        txt.setFont(dpi_font.font)
 | 
				
			||||||
 | 
					 | 
				
			||||||
        txt.setOpacity(opacity)
 | 
					        txt.setOpacity(opacity)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # register viewbox callbacks
 | 
					        # register viewbox callbacks
 | 
				
			||||||
| 
						 | 
					@ -109,7 +109,7 @@ class Label:
 | 
				
			||||||
        # self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction)
 | 
					        # self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @property
 | 
					    @property
 | 
				
			||||||
    def color(self):
 | 
					    def color(self) -> str:
 | 
				
			||||||
        return self._hcolor
 | 
					        return self._hcolor
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    @color.setter
 | 
					    @color.setter
 | 
				
			||||||
| 
						 | 
					@ -118,9 +118,10 @@ class Label:
 | 
				
			||||||
        self._hcolor = color
 | 
					        self._hcolor = color
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    def update(self) -> None:
 | 
					    def update(self) -> None:
 | 
				
			||||||
        '''Update this label either by invoking its
 | 
					        '''
 | 
				
			||||||
        user defined anchoring function, or by positioning
 | 
					        Update this label either by invoking its user defined anchoring
 | 
				
			||||||
        to the last recorded data view coordinates.
 | 
					        function, or by positioning to the last recorded data view
 | 
				
			||||||
 | 
					        coordinates.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        '''
 | 
					        '''
 | 
				
			||||||
        # move label in scene coords to desired position
 | 
					        # move label in scene coords to desired position
 | 
				
			||||||
| 
						 | 
					@ -234,7 +235,8 @@ class Label:
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class FormatLabel(QLabel):
 | 
					class FormatLabel(QLabel):
 | 
				
			||||||
    '''Kinda similar to above but using the widget apis.
 | 
					    '''
 | 
				
			||||||
 | 
					    Kinda similar to above but using the widget apis.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    '''
 | 
					    '''
 | 
				
			||||||
    def __init__(
 | 
					    def __init__(
 | 
				
			||||||
| 
						 | 
					@ -273,8 +275,8 @@ class FormatLabel(QLabel):
 | 
				
			||||||
            QSizePolicy.Expanding,
 | 
					            QSizePolicy.Expanding,
 | 
				
			||||||
            QSizePolicy.Expanding,
 | 
					            QSizePolicy.Expanding,
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.setAlignment(Qt.AlignVCenter
 | 
					        self.setAlignment(
 | 
				
			||||||
            | Qt.AlignLeft
 | 
					            Qt.AlignVCenter | Qt.AlignLeft
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        self.setText(self.fmt_str)
 | 
					        self.setText(self.fmt_str)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -334,10 +334,11 @@ class LevelLine(pg.InfiniteLine):
 | 
				
			||||||
        w: QtWidgets.QWidget
 | 
					        w: QtWidgets.QWidget
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ) -> None:
 | 
					    ) -> None:
 | 
				
			||||||
        """Core paint which we override (yet again)
 | 
					        '''
 | 
				
			||||||
 | 
					        Core paint which we override (yet again)
 | 
				
			||||||
        from pg..
 | 
					        from pg..
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        """
 | 
					        '''
 | 
				
			||||||
        p.setRenderHint(p.Antialiasing)
 | 
					        p.setRenderHint(p.Antialiasing)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # these are in viewbox coords
 | 
					        # these are in viewbox coords
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
		Loading…
	
		Reference in New Issue