commit
						8fe2bd6614
					
				|  | @ -1,5 +1,5 @@ | |||
| # 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 | ||||
| # 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. | ||||
| 
 | ||||
| ''' | ||||
| from typing import Callable | ||||
| from __future__ import annotations | ||||
| from typing import Callable, TYPE_CHECKING | ||||
| 
 | ||||
| from PyQt5.QtCore import QPointF | ||||
| from PyQt5.QtWidgets import QGraphicsPathItem | ||||
| 
 | ||||
| from ._label import Label | ||||
| if TYPE_CHECKING: | ||||
|     from ._axes import PriceAxis | ||||
|     from ._chart import ChartPlotWidget | ||||
|     from ._label import Label | ||||
| 
 | ||||
| 
 | ||||
| def marker_right_points( | ||||
| 
 | ||||
|     chart: 'ChartPlotWidget',  # noqa | ||||
|     chart: ChartPlotWidget,  # noqa | ||||
|     marker_size: int = 20, | ||||
| 
 | ||||
| ) -> (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 | ||||
|     a paried level line marker, and the right most side of the "right" | ||||
|  | @ -57,16 +62,17 @@ def vbr_left( | |||
|     label: Label, | ||||
| 
 | ||||
| ) -> 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 | ||||
| 
 | ||||
| 
 | ||||
| def right_axis( | ||||
| 
 | ||||
|     chart: 'ChartPlotWidget',  # noqa | ||||
|     chart: ChartPlotWidget,  # noqa | ||||
|     label: Label, | ||||
| 
 | ||||
|     side: str = 'left', | ||||
|  | @ -141,13 +147,13 @@ def gpath_pin( | |||
|         return path_br.bottomRight() - QPointF(label.w, label.h / 6) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| def pp_tight_and_right( | ||||
|     label: Label | ||||
| 
 | ||||
| ) -> 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) | ||||
|  |  | |||
|  | @ -18,8 +18,8 @@ | |||
| Chart axes graphics and behavior. | ||||
| 
 | ||||
| """ | ||||
| import functools | ||||
| from typing import List, Tuple, Optional | ||||
| from functools import lru_cache | ||||
| from typing import List, Tuple, Optional, Callable | ||||
| from math import floor | ||||
| 
 | ||||
| import pandas as pd | ||||
|  | @ -27,8 +27,10 @@ import pyqtgraph as pg | |||
| from PyQt5 import QtCore, QtGui, QtWidgets | ||||
| from PyQt5.QtCore import QPointF | ||||
| 
 | ||||
| from ._style import DpiAwareFont, hcolor, _font | ||||
| 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')) | ||||
| 
 | ||||
|  | @ -42,7 +44,6 @@ class Axis(pg.AxisItem): | |||
|         self, | ||||
|         linkedsplits, | ||||
|         typical_max_str: str = '100 000.000', | ||||
|         min_tick: int = 2, | ||||
|         **kwargs | ||||
| 
 | ||||
|     ) -> None: | ||||
|  | @ -52,7 +53,6 @@ class Axis(pg.AxisItem): | |||
|         # self.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) | ||||
| 
 | ||||
|         self.linkedsplits = linkedsplits | ||||
|         self._min_tick = min_tick | ||||
|         self._dpi_font = _font | ||||
| 
 | ||||
|         self.setTickFont(_font.font) | ||||
|  | @ -74,7 +74,10 @@ class Axis(pg.AxisItem): | |||
|         }) | ||||
| 
 | ||||
|         self.setTickFont(_font.font) | ||||
|         # NOTE: this is for surrounding "border" | ||||
|         self.setPen(_axis_pen) | ||||
|         # this is the text color | ||||
|         self.setTextPen(_axis_pen) | ||||
|         self.typical_br = _font._qfm.boundingRect(typical_max_str) | ||||
| 
 | ||||
|         # size the pertinent axis dimension to a "typical value" | ||||
|  | @ -83,40 +86,102 @@ class Axis(pg.AxisItem): | |||
|     def size_to_values(self) -> None: | ||||
|         pass | ||||
| 
 | ||||
|     def set_min_tick(self, size: int) -> None: | ||||
|         self._min_tick = size | ||||
| 
 | ||||
|     def txt_offsets(self) -> Tuple[int, int]: | ||||
|         return tuple(self.style['tickTextOffset']) | ||||
| 
 | ||||
| 
 | ||||
| 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: | ||||
|         # self.typical_br = _font._qfm.boundingRect(typical_max_str) | ||||
|         self.setWidth(self.typical_br.width()) | ||||
| 
 | ||||
|     # XXX: drop for now since it just eats up h space | ||||
| 
 | ||||
|     def tickStrings( | ||||
|         self, | ||||
|         vals, | ||||
|         scale, | ||||
|         spacing, | ||||
|     ): | ||||
|         vals: tuple[float], | ||||
|         scale: float, | ||||
|         spacing: float, | ||||
| 
 | ||||
|         # TODO: figure out how to enforce min tick spacing by passing | ||||
|         # it into the parent type | ||||
|         digits = max(float_digits(spacing * scale), self._min_tick) | ||||
|     ) -> list[str]: | ||||
|         # TODO: figure out how to enforce min tick spacing by passing it | ||||
|         # 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'digits: {digits}') | ||||
| 
 | ||||
|         return [ | ||||
|             ('{value:,.{digits}f}').format( | ||||
|                 digits=digits, | ||||
|                 value=v, | ||||
|             ).replace(',', ' ') for v in vals | ||||
|         ] | ||||
|         if not self.formatter: | ||||
|             return [ | ||||
|                 ('{value:,.{digits}f}').format( | ||||
|                     digits=digits, | ||||
|                     value=v, | ||||
|                 ).replace(',', ' ') for v in vals | ||||
|             ] | ||||
|         else: | ||||
|             return list(map(self.formatter, vals)) | ||||
| 
 | ||||
| 
 | ||||
| class DynamicDateAxis(Axis): | ||||
|  | @ -136,6 +201,7 @@ class DynamicDateAxis(Axis): | |||
|     def _indexes_to_timestrs( | ||||
|         self, | ||||
|         indexes: List[int], | ||||
| 
 | ||||
|     ) -> List[str]: | ||||
| 
 | ||||
|         chart = self.linkedsplits.chart | ||||
|  | @ -165,9 +231,10 @@ class DynamicDateAxis(Axis): | |||
|     def tickStrings( | ||||
|         self, | ||||
|         values: tuple[float], | ||||
|         scale, | ||||
|         spacing, | ||||
|     ): | ||||
|         scale: float, | ||||
|         spacing: float, | ||||
| 
 | ||||
|     ) -> list[str]: | ||||
|         # info = self.tickStrings.cache_info() | ||||
|         # print(info) | ||||
|         return self._indexes_to_timestrs(values) | ||||
|  | @ -220,6 +287,8 @@ class AxisLabel(pg.GraphicsObject): | |||
|         self.path = None | ||||
|         self.rect = None | ||||
| 
 | ||||
|         self._pw = self.pixelWidth() | ||||
| 
 | ||||
|     def paint( | ||||
|         self, | ||||
|         p: QtGui.QPainter, | ||||
|  | @ -269,9 +338,10 @@ class AxisLabel(pg.GraphicsObject): | |||
| 
 | ||||
| 
 | ||||
|     def boundingRect(self):  # noqa | ||||
|         """Size the graphics space from the text contents. | ||||
|         ''' | ||||
|         Size the graphics space from the text contents. | ||||
| 
 | ||||
|         """ | ||||
|         ''' | ||||
|         if self.label_str: | ||||
|             self._size_br_from_str(self.label_str) | ||||
| 
 | ||||
|  | @ -287,23 +357,32 @@ class AxisLabel(pg.GraphicsObject): | |||
| 
 | ||||
|         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: | ||||
|         """Do our best to render the bounding rect to a set margin | ||||
|     ) -> tuple[float, float]: | ||||
|         ''' | ||||
|         Do our best to render the bounding rect to a set margin | ||||
|         around provided string contents. | ||||
| 
 | ||||
|         """ | ||||
|         ''' | ||||
|         # size the filled rect to text and/or parent axis | ||||
|         # 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) | ||||
| 
 | ||||
|         txt_br = self._txt_br = self._dpifont.boundingRect(value) | ||||
|         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 | ||||
|         h, w = self.size_hint() | ||||
|         # print(f'axis size: {self._parent.size()}') | ||||
|         # print(f'axis geo: {self._parent.geometry()}') | ||||
| 
 | ||||
|         self.rect = QtCore.QRectF( | ||||
|             0, 0, | ||||
|  | @ -314,7 +393,7 @@ class AxisLabel(pg.GraphicsObject): | |||
|         # hb = self.path.controlPointRect() | ||||
|         # hb_size = hb.size() | ||||
| 
 | ||||
|         return self.rect | ||||
|         return (self.rect.width(), self.rect.height()) | ||||
| 
 | ||||
| # _common_text_flags = ( | ||||
| #     QtCore.Qt.TextDontClip | | ||||
|  | @ -342,6 +421,7 @@ class XAxisLabel(AxisLabel): | |||
|         abs_pos: QPointF,  # scene coords | ||||
|         value: float,  # data for text | ||||
|         offset: int = 0  # if have margins, k? | ||||
| 
 | ||||
|     ) -> None: | ||||
| 
 | ||||
|         timestrs = self._parent._indexes_to_timestrs([int(value)]) | ||||
|  | @ -356,17 +436,19 @@ class XAxisLabel(AxisLabel): | |||
| 
 | ||||
|         w = self.boundingRect().width() | ||||
| 
 | ||||
|         self.setPos(QPointF( | ||||
|             abs_pos.x() - w/2, | ||||
|             y_offset/2, | ||||
|         )) | ||||
|         self.setPos( | ||||
|             QPointF( | ||||
|                 abs_pos.x() - w/2 - self._pw, | ||||
|                 y_offset/2, | ||||
|             ) | ||||
|         ) | ||||
|         self.update() | ||||
| 
 | ||||
|     def _draw_arrow_path(self): | ||||
|         y_offset = self._parent.style['tickTextOffset'][1] | ||||
|         path = QtGui.QPainterPath() | ||||
|         h, w = self.rect.height(), self.rect.width() | ||||
|         middle = w/2 - 0.5 | ||||
|         middle = w/2 - self._pw * 0.5 | ||||
|         aw = h/2 | ||||
|         left = middle - aw | ||||
|         right = middle + aw | ||||
|  | @ -410,8 +492,12 @@ class YAxisLabel(AxisLabel): | |||
|             self.x_offset, y_offset = self._parent.txt_offsets() | ||||
| 
 | ||||
|     def size_hint(self) -> Tuple[float, float]: | ||||
|         # size to parent axis width | ||||
|         return None, self._parent.width() | ||||
|         # size to parent axis width(-ish) | ||||
|         wsh = self._dpifont.boundingRect(' ').height() / 2 | ||||
|         return ( | ||||
|             None, | ||||
|             self._parent.size().width() - wsh, | ||||
|         ) | ||||
| 
 | ||||
|     def update_label( | ||||
|         self, | ||||
|  | @ -432,16 +518,19 @@ class YAxisLabel(AxisLabel): | |||
|         br = self.boundingRect() | ||||
|         h = br.height() | ||||
| 
 | ||||
|         self.setPos(QPointF( | ||||
|             x_offset, | ||||
|             abs_pos.y() - h / 2 - self._y_margin / 2 | ||||
|         )) | ||||
|         self.setPos( | ||||
|             QPointF( | ||||
|                 x_offset, | ||||
|                 abs_pos.y() - h / 2 - self._pw, | ||||
|             ) | ||||
|         ) | ||||
|         self.update() | ||||
| 
 | ||||
|     def update_on_resize(self, vr, r): | ||||
|         """Tiis is a ``.sigRangeChanged()`` handler. | ||||
|         ''' | ||||
|         This is a ``.sigRangeChanged()`` handler. | ||||
| 
 | ||||
|         """ | ||||
|         ''' | ||||
|         index, last = self._last_datum | ||||
|         if index is not None: | ||||
|             self.update_from_data(index, last) | ||||
|  | @ -451,11 +540,13 @@ class YAxisLabel(AxisLabel): | |||
|         index: int, | ||||
|         value: float, | ||||
|         _save_last: bool = True, | ||||
| 
 | ||||
|     ) -> None: | ||||
|         """Update the label's text contents **and** position from | ||||
|         ''' | ||||
|         Update the label's text contents **and** position from | ||||
|         a view box coordinate datum. | ||||
| 
 | ||||
|         """ | ||||
|         ''' | ||||
|         if _save_last: | ||||
|             self._last_datum = (index, value) | ||||
| 
 | ||||
|  | @ -469,7 +560,7 @@ class YAxisLabel(AxisLabel): | |||
|         path = QtGui.QPainterPath() | ||||
|         h = self.rect.height() | ||||
|         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.closeSubpath() | ||||
|         self.path = path | ||||
|  |  | |||
|  | @ -479,14 +479,20 @@ class LinkedSplits(QWidget): | |||
|             axisItems=axes, | ||||
|             **cpw_kwargs, | ||||
|         ) | ||||
|         cpw.hideAxis('left') | ||||
|         cpw.hideAxis('bottom') | ||||
| 
 | ||||
|         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.removeAxis('bottom', unlink=False) | ||||
|             # assert 'bottom' not in self.xaxis_chart.plotItem.axes | ||||
| 
 | ||||
|             self.xaxis_chart = cpw | ||||
|             cpw.showAxis('bottom') | ||||
| 
 | ||||
|         if self.xaxis_chart is None: | ||||
|             self.xaxis_chart = cpw | ||||
|  | @ -726,11 +732,6 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|         self._static_yrange = static_yrange  # for "known y-range style" | ||||
|         self._view_mode: str = 'follow' | ||||
| 
 | ||||
|         # show only right side axes | ||||
|         self.hideAxis('left') | ||||
|         self.showAxis('right') | ||||
|         # self.showAxis('left') | ||||
| 
 | ||||
|         # show background grid | ||||
|         self.showGrid(x=False, y=True, alpha=0.3) | ||||
| 
 | ||||
|  | @ -862,53 +863,58 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|     def overlay_plotitem( | ||||
|         self, | ||||
|         name: str, | ||||
|         index: Optional[int] = None, | ||||
|         axis_title: Optional[str] = None, | ||||
|         axis_side: str = 'right', | ||||
|         axis_kwargs: dict = {}, | ||||
| 
 | ||||
|     ) -> pg.PlotItem: | ||||
| 
 | ||||
|         # Custom viewbox impl | ||||
|         cv = self.mk_vb(name) | ||||
|         cv.chart = self | ||||
| 
 | ||||
|         # xaxis = DynamicDateAxis( | ||||
|         #     orientation='bottom', | ||||
|         #     linkedsplits=self.linked, | ||||
|         # ) | ||||
|         allowed_sides = {'left', 'right'} | ||||
|         if axis_side not in allowed_sides: | ||||
|             raise ValueError(f'``axis_side``` must be in {allowed_sides}') | ||||
|         yaxis = PriceAxis( | ||||
|             orientation='right', | ||||
|             orientation=axis_side, | ||||
|             linkedsplits=self.linked, | ||||
|             **axis_kwargs, | ||||
|         ) | ||||
| 
 | ||||
|         plotitem = pg.PlotItem( | ||||
|         pi = pg.PlotItem( | ||||
|             parent=self.plotItem, | ||||
|             name=name, | ||||
|             enableMenu=False, | ||||
|             viewBox=cv, | ||||
|             axisItems={ | ||||
|                 # 'bottom': xaxis, | ||||
|                 'right': yaxis, | ||||
|                 axis_side: yaxis, | ||||
|             }, | ||||
|             default_axes=[], | ||||
|         ) | ||||
|         # plotitem.setAxisItems( | ||||
|         #     add_to_layout=False, | ||||
|         #     axisItems={ | ||||
|         #         'bottom': xaxis, | ||||
|         #         'right': yaxis, | ||||
|         #     }, | ||||
|         # ) | ||||
|         # plotite.hideAxis('right') | ||||
|         # plotite.hideAxis('bottom') | ||||
|         # plotitem.addItem(curve) | ||||
|         pi.hideButtons() | ||||
| 
 | ||||
|         cv.enable_auto_yrange() | ||||
| 
 | ||||
|         # plotitem.enableAutoRange(axis='y') | ||||
|         plotitem.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( | ||||
|             plotitem, | ||||
|             pi, | ||||
|             index=index, | ||||
| 
 | ||||
|             # only link x-axes, | ||||
|             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( | ||||
|         self, | ||||
|  | @ -1014,7 +1020,8 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|         # add y-axis "last" value label | ||||
|         last = self._ysticks[name] = YAxisLabel( | ||||
|             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 | ||||
|             digits=digits, | ||||
|             opacity=1, | ||||
|  |  | |||
|  | @ -43,8 +43,8 @@ log = get_logger(__name__) | |||
| # latency (in terms of perceived lag in cross hair) so really be sure | ||||
| # there's an improvement if you want to change it! | ||||
| 
 | ||||
| _mouse_rate_limit = 58  # TODO; should we calc current screen refresh rate? | ||||
| _debounce_delay = 1 / 60 | ||||
| _mouse_rate_limit = 120  # TODO; should we calc current screen refresh rate? | ||||
| _debounce_delay = 1 / 40 | ||||
| _ch_label_opac = 1 | ||||
| 
 | ||||
| 
 | ||||
|  | @ -369,7 +369,13 @@ class Cursor(pg.GraphicsObject): | |||
|         self, | ||||
|         plot: 'ChartPlotWidget',  # noqa | ||||
|         digits: int = 0, | ||||
| 
 | ||||
|     ) -> 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 | ||||
|         # vertical and horizonal lines and a y-axis label | ||||
| 
 | ||||
|  | @ -382,7 +388,8 @@ class Cursor(pg.GraphicsObject): | |||
| 
 | ||||
|         yl = YAxisLabel( | ||||
|             chart=plot, | ||||
|             parent=plot.getAxis('right'), | ||||
|             # parent=plot.getAxis('right'), | ||||
|             parent=plot.pi_overlay.get_axis(plot.plotItem, 'right'), | ||||
|             digits=digits or self.digits, | ||||
|             opacity=_ch_label_opac, | ||||
|             bg_color=self.label_color, | ||||
|  | @ -424,19 +431,25 @@ class Cursor(pg.GraphicsObject): | |||
| 
 | ||||
|         # ONLY create an x-axis label for the cursor | ||||
|         # if this plot owns the 'bottom' axis. | ||||
|         if 'bottom' in plot.plotItem.axes: | ||||
|             self.xaxis_label = XAxisLabel( | ||||
|         # if 'bottom' in plot.plotItem.axes: | ||||
|         if plot.linked.xaxis_chart is plot: | ||||
|             xlabel = self.xaxis_label = XAxisLabel( | ||||
|                 parent=self.plots[plot_index].getAxis('bottom'), | ||||
|                 # parent=self.plots[plot_index].pi_overlay.get_axis(plot.plotItem, 'bottom'), | ||||
|                 opacity=_ch_label_opac, | ||||
|                 bg_color=self.label_color, | ||||
|             ) | ||||
|             # 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( | ||||
|         self, | ||||
|         plot: 'ChartPlotWidget',  # noqa | ||||
|         curve: 'PlotCurveItem',  # noqa | ||||
| 
 | ||||
|     ) -> LineDot: | ||||
|         # if this plot contains curves add line dot "cursors" to denote | ||||
|         # the current sample under the mouse | ||||
|  | @ -493,24 +506,27 @@ class Cursor(pg.GraphicsObject): | |||
| 
 | ||||
|         ix = round(x)  # since bars are centered around index | ||||
| 
 | ||||
|         # px perfect... | ||||
|         line_offset = self._lw / 2 | ||||
| 
 | ||||
|         # round y value to nearest tick step | ||||
|         m = self._y_incr_mult | ||||
|         iy = round(y * m) / m | ||||
| 
 | ||||
|         # px perfect... | ||||
|         line_offset = self._lw / 2 | ||||
|         vl_y = iy - line_offset | ||||
| 
 | ||||
|         # update y-range items | ||||
|         if iy != last_iy: | ||||
| 
 | ||||
|             if self._y_label_update: | ||||
|                 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 | ||||
|                 ) | ||||
| 
 | ||||
|                 # 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 | ||||
|             for item in self._trackers: | ||||
|  | @ -541,21 +557,18 @@ class Cursor(pg.GraphicsObject): | |||
|                 # left axis offset width for calcuating | ||||
|                 # absolute x-axis label placement. | ||||
|                 left_axis_width = 0 | ||||
|                 left = axes.get('left') | ||||
|                 if left: | ||||
|                     left_axis_width = left['item'].width() | ||||
| 
 | ||||
|                 if 'bottom' in axes: | ||||
| 
 | ||||
|                     left = axes.get('left') | ||||
|                     if left: | ||||
|                         left_axis_width = left['item'].width() | ||||
| 
 | ||||
|                     # map back to abs (label-local) coordinates | ||||
|                     self.xaxis_label.update_label( | ||||
|                         abs_pos=( | ||||
|                             plot.mapFromView(QPointF(vl_x, iy)) - | ||||
|                             QPointF(left_axis_width, 0) | ||||
|                         ), | ||||
|                         value=ix, | ||||
|                     ) | ||||
|                 # map back to abs (label-local) coordinates | ||||
|                 self.xaxis_label.update_label( | ||||
|                     abs_pos=( | ||||
|                         plot.mapFromView(QPointF(vl_x, iy)) - | ||||
|                         QPointF(left_axis_width, 0) | ||||
|                     ), | ||||
|                     value=ix, | ||||
|                 ) | ||||
| 
 | ||||
|         self._datum_xy = ix, iy | ||||
| 
 | ||||
|  |  | |||
|  | @ -115,13 +115,14 @@ async def update_linked_charts_graphics( | |||
|     vlm_chart: Optional[ChartPlotWidget] = 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 | ||||
|     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 | ||||
|     #   done in one place and "graphics update routines" | ||||
|     #   should not be doing any length checking and array diffing. | ||||
|  | @ -181,13 +182,34 @@ async def update_linked_charts_graphics( | |||
|     view = chart.view | ||||
|     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: | ||||
| 
 | ||||
|         now = time.time() | ||||
|         quote_period = time.time() - last_quote | ||||
|         quote_rate = round( | ||||
|             1/quote_period, 1) if quote_period > 0 else float('inf') | ||||
| 
 | ||||
|         if ( | ||||
|             quote_period <= 1/_quote_throttle_rate | ||||
| 
 | ||||
|  | @ -196,7 +218,8 @@ async def update_linked_charts_graphics( | |||
|             and quote_rate >= _quote_throttle_rate * 1.5 | ||||
|         ): | ||||
|             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) | ||||
|         if chart.linked.isHidden(): | ||||
|  | @ -621,9 +644,15 @@ async def display_symbol_data( | |||
|                 await trio.sleep(0) | ||||
|                 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 | ||||
|                 # close group status | ||||
|                 sbar._status_groups[loading_sym_key][1]() | ||||
| 
 | ||||
|                 # let the app run. | ||||
|                 # let the app run.. bby | ||||
|                 await trio.sleep_forever() | ||||
|  |  | |||
							
								
								
									
										143
									
								
								piker/ui/_fsp.py
								
								
								
								
							
							
						
						
									
										143
									
								
								piker/ui/_fsp.py
								
								
								
								
							|  | @ -33,7 +33,9 @@ import pyqtgraph as pg | |||
| import trio | ||||
| from trio_typing import TaskStatus | ||||
| 
 | ||||
| from ._axes import PriceAxis | ||||
| from .._cacheables import maybe_open_context | ||||
| from ..calc import humanize | ||||
| from ..data._sharedmem import ( | ||||
|     ShmArray, | ||||
|     maybe_open_shm_array, | ||||
|  | @ -653,7 +655,7 @@ async def open_vlm_displays( | |||
| 
 | ||||
|         last_val_sticky.update_from_data(-1, value) | ||||
| 
 | ||||
|         chart.update_curve_from_array( | ||||
|         vlm_curve = chart.update_curve_from_array( | ||||
|             'volume', | ||||
|             shm.array, | ||||
|         ) | ||||
|  | @ -661,73 +663,100 @@ async def open_vlm_displays( | |||
|         # size view to data once at outset | ||||
|         chart.view._set_yrange() | ||||
| 
 | ||||
|         if not dvlm: | ||||
|             return | ||||
|         # add axis title | ||||
|         axis = chart.getAxis('right') | ||||
|         axis.set_title(' vlm') | ||||
| 
 | ||||
|         # spawn and overlay $ vlm on the same subchart | ||||
|         shm, started = await admin.start_engine_task( | ||||
|             'dolla_vlm', | ||||
|             # linked.symbol.front_feed(),  # data-feed symbol key | ||||
|             {  # fsp engine conf | ||||
|                 'func_name': 'dolla_vlm', | ||||
|                 'zero_on_step': True, | ||||
|                 'params': { | ||||
|                     'price_func': { | ||||
|                         'default_value': 'chl3', | ||||
|         if dvlm: | ||||
| 
 | ||||
|             # spawn and overlay $ vlm on the same subchart | ||||
|             shm, started = await admin.start_engine_task( | ||||
|                 'dolla_vlm', | ||||
|                 # linked.symbol.front_feed(),  # data-feed symbol key | ||||
|                 {  # fsp engine conf | ||||
|                     'func_name': 'dolla_vlm', | ||||
|                     'zero_on_step': True, | ||||
|                     'params': { | ||||
|                         'price_func': { | ||||
|                             'default_value': 'chl3', | ||||
|                         }, | ||||
|                     }, | ||||
|                 }, | ||||
|             }, | ||||
|             # loglevel, | ||||
|         ) | ||||
|         # profiler(f'created shm for fsp actor: {display_name}') | ||||
|                 # loglevel, | ||||
|             ) | ||||
|             # profiler(f'created shm for fsp actor: {display_name}') | ||||
| 
 | ||||
|         await started.wait() | ||||
|             await started.wait() | ||||
| 
 | ||||
|         pi = chart.overlay_plotitem( | ||||
|             'dolla_vlm', | ||||
|         ) | ||||
|         # add custom auto range handler | ||||
|         pi.vb._maxmin = partial(maxmin, name='dolla_vlm') | ||||
|             pi = chart.overlay_plotitem( | ||||
|                 '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, | ||||
|                     ), | ||||
|                 }, | ||||
| 
 | ||||
|         curve, _ = chart.draw_curve( | ||||
|             ) | ||||
| 
 | ||||
|             name='dolla_vlm', | ||||
|             data=shm.array, | ||||
|             # add custom auto range handler | ||||
|             pi.vb._maxmin = partial(maxmin, name='dolla_vlm') | ||||
| 
 | ||||
|             array_key='dolla_vlm', | ||||
|             overlay=pi, | ||||
|             color='charcoal', | ||||
|             step_mode=True, | ||||
|             # **conf.get('chart_kwargs', {}) | ||||
|         ) | ||||
|         # TODO: is there a way to "sync" the dual axes such that only | ||||
|         # one curve is needed? | ||||
|         # curve.hide() | ||||
|             curve, _ = chart.draw_curve( | ||||
| 
 | ||||
|         # TODO: we need a better API to do this.. | ||||
|         # specially store ref to shm for lookup in display loop | ||||
|         # since only a placeholder of `None` is entered in | ||||
|         # ``.draw_curve()``. | ||||
|         chart._overlays['dolla_vlm'] = shm | ||||
|                 name='dolla_vlm', | ||||
|                 data=shm.array, | ||||
| 
 | ||||
|         # XXX: old dict-style config before it was moved into the helper task | ||||
|         #     'dolla_vlm': { | ||||
|         #         'func_name': 'dolla_vlm', | ||||
|         #         'zero_on_step': True, | ||||
|         #         'overlay': 'volume', | ||||
|         #         'separate_axes': True, | ||||
|         #         'params': { | ||||
|         #             'price_func': { | ||||
|         #                 'default_value': 'chl3', | ||||
|         #                 # tell target ``Edit`` widget to not allow | ||||
|         #                 # edits for now. | ||||
|         #                 'widget_kwargs': {'readonly': True}, | ||||
|         #             }, | ||||
|         #         }, | ||||
|         #         'chart_kwargs': {'step_mode': True} | ||||
|         #     }, | ||||
|                 array_key='dolla_vlm', | ||||
|                 overlay=pi, | ||||
|                 # color='bracket', | ||||
|                 # TODO: this color or dark volume | ||||
|                 # color='charcoal', | ||||
|                 step_mode=True, | ||||
|                 # **conf.get('chart_kwargs', {}) | ||||
|             ) | ||||
|             # TODO: is there a way to "sync" the dual axes such that only | ||||
|             # one curve is needed? | ||||
|             # 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.. | ||||
|             # specially store ref to shm for lookup in display loop | ||||
|             # since only a placeholder of `None` is entered in | ||||
|             # ``.draw_curve()``. | ||||
|             chart._overlays['dolla_vlm'] = shm | ||||
| 
 | ||||
|             # XXX: old dict-style config before it was moved into the | ||||
|             # helper task | ||||
|             #     'dolla_vlm': { | ||||
|             #         'func_name': 'dolla_vlm', | ||||
|             #         'zero_on_step': True, | ||||
|             #         'overlay': 'volume', | ||||
|             #         'separate_axes': True, | ||||
|             #         'params': { | ||||
|             #             'price_func': { | ||||
|             #                 'default_value': 'chl3', | ||||
|             #                 # tell target ``Edit`` widget to not allow | ||||
|             #                 # edits for now. | ||||
|             #                 'widget_kwargs': {'readonly': True}, | ||||
|             #             }, | ||||
|             #         }, | ||||
|             #         'chart_kwargs': {'step_mode': True} | ||||
|             #     }, | ||||
| 
 | ||||
|             # } | ||||
| 
 | ||||
|             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 | ||||
|         for display_name, conf in { | ||||
|  |  | |||
|  | @ -34,7 +34,7 @@ from ._style import ( | |||
| 
 | ||||
| 
 | ||||
| class Label: | ||||
|     """ | ||||
|     ''' | ||||
|     A plain ol' "scene label" using an underlying ``QGraphicsTextItem``. | ||||
| 
 | ||||
|     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 | ||||
|     production grade UIs... | ||||
| 
 | ||||
|     """ | ||||
| 
 | ||||
|     ''' | ||||
|     def __init__( | ||||
| 
 | ||||
|         self, | ||||
|         view: pg.ViewBox, | ||||
|         fmt_str: str, | ||||
|  | @ -63,6 +61,7 @@ class Label: | |||
|         font_size: str = 'small', | ||||
|         opacity: float = 1, | ||||
|         fields: dict = {}, | ||||
|         parent: pg.GraphicsObject = None, | ||||
|         update_on_range_change: bool = True, | ||||
| 
 | ||||
|     ) -> None: | ||||
|  | @ -71,11 +70,13 @@ class Label: | |||
|         self._fmt_str = fmt_str | ||||
|         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 | ||||
| 
 | ||||
|         txt = self.txt = QtWidgets.QGraphicsTextItem() | ||||
|         txt = self.txt = QtWidgets.QGraphicsTextItem(parent=parent) | ||||
|         txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache) | ||||
| 
 | ||||
|         vb.scene().addItem(txt) | ||||
|  | @ -86,7 +87,6 @@ class Label: | |||
|         ) | ||||
|         dpi_font.configure_to_dpi() | ||||
|         txt.setFont(dpi_font.font) | ||||
| 
 | ||||
|         txt.setOpacity(opacity) | ||||
| 
 | ||||
|         # register viewbox callbacks | ||||
|  | @ -109,7 +109,7 @@ class Label: | |||
|         # self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction) | ||||
| 
 | ||||
|     @property | ||||
|     def color(self): | ||||
|     def color(self) -> str: | ||||
|         return self._hcolor | ||||
| 
 | ||||
|     @color.setter | ||||
|  | @ -118,9 +118,10 @@ class Label: | |||
|         self._hcolor = color | ||||
| 
 | ||||
|     def update(self) -> None: | ||||
|         '''Update this label either by invoking its | ||||
|         user defined anchoring function, or by positioning | ||||
|         to the last recorded data view coordinates. | ||||
|         ''' | ||||
|         Update this label either by invoking its user defined anchoring | ||||
|         function, or by positioning to the last recorded data view | ||||
|         coordinates. | ||||
| 
 | ||||
|         ''' | ||||
|         # move label in scene coords to desired position | ||||
|  | @ -234,7 +235,8 @@ class Label: | |||
| 
 | ||||
| 
 | ||||
| class FormatLabel(QLabel): | ||||
|     '''Kinda similar to above but using the widget apis. | ||||
|     ''' | ||||
|     Kinda similar to above but using the widget apis. | ||||
| 
 | ||||
|     ''' | ||||
|     def __init__( | ||||
|  | @ -273,8 +275,8 @@ class FormatLabel(QLabel): | |||
|             QSizePolicy.Expanding, | ||||
|             QSizePolicy.Expanding, | ||||
|         ) | ||||
|         self.setAlignment(Qt.AlignVCenter | ||||
|             | Qt.AlignLeft | ||||
|         self.setAlignment( | ||||
|             Qt.AlignVCenter | Qt.AlignLeft | ||||
|         ) | ||||
|         self.setText(self.fmt_str) | ||||
| 
 | ||||
|  |  | |||
|  | @ -334,10 +334,11 @@ class LevelLine(pg.InfiniteLine): | |||
|         w: QtWidgets.QWidget | ||||
| 
 | ||||
|     ) -> None: | ||||
|         """Core paint which we override (yet again) | ||||
|         ''' | ||||
|         Core paint which we override (yet again) | ||||
|         from pg.. | ||||
| 
 | ||||
|         """ | ||||
|         ''' | ||||
|         p.setRenderHint(p.Antialiasing) | ||||
| 
 | ||||
|         # these are in viewbox coords | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue