Support size fields on order lines; avoid overlap with L1 lines

basic_orders
Tyler Goodlet 2021-01-26 11:27:50 -05:00
parent 990c3a1eac
commit b9d9dbfc4a
2 changed files with 136 additions and 41 deletions

View File

@ -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 for piker0)
# 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
@ -166,6 +166,7 @@ class AxisLabel(pg.GraphicsObject):
super().__init__(parent) super().__init__(parent)
self.setFlag(self.ItemIgnoresTransformations) self.setFlag(self.ItemIgnoresTransformations)
# XXX: pretty sure this is faster # XXX: pretty sure this is faster
self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache) self.setCacheMode(QtGui.QGraphicsItem.DeviceCoordinateCache)
@ -177,7 +178,7 @@ class AxisLabel(pg.GraphicsObject):
self._txt_br: QtCore.QRect = None self._txt_br: QtCore.QRect = None
self._dpifont = DpiAwareFont(size_in_inches=font_size_inches) self._dpifont = DpiAwareFont(size_in_inches=font_size_inches)
self._dpifont.configure_to_dpi(_font._screen) self._dpifont.configure_to_dpi()
self.bg_color = pg.mkColor(hcolor(bg_color)) self.bg_color = pg.mkColor(hcolor(bg_color))
self.fg_color = pg.mkColor(hcolor(fg_color)) self.fg_color = pg.mkColor(hcolor(fg_color))
@ -232,8 +233,11 @@ class AxisLabel(pg.GraphicsObject):
""" """
# size the filled rect to text and/or parent axis # size the filled rect to text and/or parent axis
br = self._txt_br = self._dpifont.boundingRect(value) # if not self._txt_br:
# # XXX: this can't be c
# self._txt_br = self._dpifont.boundingRect(value)
br = self._txt_br = self._dpifont.boundingRect(value)
txt_h, txt_w = br.height(), br.width() txt_h, txt_w = br.height(), br.width()
h, w = self.size_hint() h, w = self.size_hint()

View File

@ -18,7 +18,7 @@
Lines for orders, alerts, L2. Lines for orders, alerts, L2.
""" """
from typing import Tuple from typing import Tuple, Dict, Any, Optional
import pyqtgraph as pg import pyqtgraph as pg
from PyQt5 import QtCore, QtGui from PyQt5 import QtCore, QtGui
@ -27,6 +27,8 @@ from PyQt5.QtCore import QPointF
from .._style import ( from .._style import (
hcolor, hcolor,
_down_2_font_inches_we_like, _down_2_font_inches_we_like,
# _font,
# DpiAwareFont
) )
from .._axes import YSticky from .._axes import YSticky
@ -35,7 +37,15 @@ class LevelLabel(YSticky):
_w_margin = 4 _w_margin = 4
_h_margin = 3 _h_margin = 3
level: float = 0
# adjustment "further away from" parent axis
_x_offset = 0
# fields to be displayed
level: float = 0.0
size: float = 2.0
size_digits: int = int(2.0)
def __init__( def __init__(
self, self,
@ -65,6 +75,9 @@ class LevelLabel(YSticky):
'left': -1., 'right': 0 'left': -1., 'right': 0
}[orient_h] }[orient_h]
self._fmt_fields: Dict[str, Dict[str, Any]] = {}
self._use_extra_fields: bool = False
@property @property
def color(self): def color(self):
return self._hcolor return self._hcolor
@ -82,29 +95,39 @@ class LevelLabel(YSticky):
) -> None: ) -> None:
# write contents, type specific # write contents, type specific
self.set_label_str(level) h, w = self.set_label_str(level)
br = self.boundingRect() # this triggers ``.paint()`` implicitly or no?
h, w = br.height(), br.width()
# this triggers ``.paint()`` implicitly?
self.setPos(QPointF( self.setPos(QPointF(
self._h_shift * w - offset, self._h_shift * w - self._x_offset,
abs_pos.y() - (self._v_shift * h) - offset abs_pos.y() - (self._v_shift * h) - offset
)) ))
# trigger .paint()
self.update() self.update()
self.level = level self.level = level
def set_label_str(self, level: float): def set_label_str(self, level: float):
# self.label_str = '{size} x {level:.{digits}f}'.format( # use space as e3 delim
# size=self._size, label_str = (f'{level:,.{self.digits}f}').replace(',', ' ')
# XXX: not huge on this approach but we need a more formal
# way to define "label fields" that i don't have the brain space
# for atm.. it's at least a **lot** better then the wacky
# internals of InfLinelabel or wtv.
# mutate label to contain any extra defined format fields
if self._use_extra_fields:
for fmt_str, fields in self._fmt_fields.items():
label_str = fmt_str.format(
**{f: getattr(self, f) for f in fields}) + label_str
self.label_str = label_str
br = self.boundingRect()
h, w = br.height(), br.width()
return h, w
# this is read inside ``.paint()``
self.label_str = '{level:.{digits}f}'.format(
digits=self.digits,
level=level
).replace(',', ' ')
def size_hint(self) -> Tuple[None, None]: def size_hint(self) -> Tuple[None, None]:
return None, None return None, None
@ -119,6 +142,7 @@ class LevelLabel(YSticky):
if self._orient_v == 'bottom': if self._orient_v == 'bottom':
lp, rp = rect.topLeft(), rect.topRight() lp, rp = rect.topLeft(), rect.topRight()
# p.drawLine(rect.topLeft(), rect.topRight()) # p.drawLine(rect.topLeft(), rect.topRight())
elif self._orient_v == 'top': elif self._orient_v == 'top':
lp, rp = rect.bottomLeft(), rect.bottomRight() lp, rp = rect.bottomLeft(), rect.bottomRight()
@ -133,10 +157,15 @@ class LevelLabel(YSticky):
self.update() self.update()
# global for now but probably should be
# attached to chart instance?
_max_l1_line_len: float = 0
class L1Label(LevelLabel): class L1Label(LevelLabel):
size: float = 0 size: float = 0
size_digits: float = 3 size_digits: int = 3
text_flags = ( text_flags = (
QtCore.Qt.TextDontClip QtCore.Qt.TextDontClip
@ -148,12 +177,14 @@ class L1Label(LevelLabel):
size in the text, eg. 100 x 323.3. size in the text, eg. 100 x 323.3.
""" """
self.label_str = '{size:.{size_digits}f} x {level:,.{digits}f}'.format( h, w = super().set_label_str(level)
size_digits=self.size_digits,
size=self.size or 2, # Set a global "max L1 label length" so we can look it up
digits=self.digits, # on order lines and adjust their labels not to overlap with it.
level=level global _max_l1_line_len
).replace(',', ' ') _max_l1_line_len = max(_max_l1_line_len, w)
return h, w
class L1Labels: class L1Labels:
@ -200,6 +231,14 @@ class L1Labels:
self.ask_label.size_digits = size_digits self.ask_label.size_digits = size_digits
self.ask_label._size_br_from_str(self.max_value) self.ask_label._size_br_from_str(self.max_value)
self.bid_label._use_extra_fields = True
self.ask_label._use_extra_fields = True
self.bid_label._fmt_fields['{size:.{size_digits}f} x '] = {
'size', 'size_digits'}
self.ask_label._fmt_fields['{size:.{size_digits}f} x '] = {
'size', 'size_digits'}
# TODO: probably worth investigating if we can # TODO: probably worth investigating if we can
# make .boundingRect() faster: # make .boundingRect() faster:
@ -217,6 +256,8 @@ class LevelLine(pg.InfiniteLine):
highlight_color: str = 'default_light', highlight_color: str = 'default_light',
hl_on_hover: bool = True, hl_on_hover: bool = True,
dotted: bool = False, dotted: bool = False,
adjust_to_l1: bool = False,
always_show_label: bool = False,
**kwargs, **kwargs,
) -> None: ) -> None:
@ -234,6 +275,8 @@ class LevelLine(pg.InfiniteLine):
# TODO: for when we want to move groups of lines? # TODO: for when we want to move groups of lines?
self._track_cursor: bool = False self._track_cursor: bool = False
self._adjust_to_l1 = adjust_to_l1
self._always_show_label = always_show_label
# testing markers # testing markers
# self.addMarker('<|', 0.1, 3) # self.addMarker('<|', 0.1, 3)
@ -267,14 +310,24 @@ class LevelLine(pg.InfiniteLine):
hoverpen.setWidth(2) hoverpen.setWidth(2)
self.hoverPen = hoverpen self.hoverPen = hoverpen
def set_level(self, value: float) -> None: def set_level(self) -> None:
self.label.update_from_data(0, self.value())
label = self.label
# TODO: a better way to accomplish this...
if self._adjust_to_l1:
label._x_offset = _max_l1_line_len
label.update_from_data(0, self.value())
def on_tracked_source( def on_tracked_source(
self, self,
x: int, x: int,
y: float y: float
) -> None: ) -> None:
# XXX: this is called by our ``Cursor`` type once this
# line is set to track the cursor: for every movement
# this callback is invoked to reposition the line
self.movable = True self.movable = True
self.setPos(y) # implictly calls ``.set_level()`` self.setPos(y) # implictly calls ``.set_level()``
self.update() self.update()
@ -300,19 +353,23 @@ class LevelLine(pg.InfiniteLine):
# add us to cursor state # add us to cursor state
chart._cursor.add_hovered(self) chart._cursor.add_hovered(self)
self.label.show()
# TODO: hide y-crosshair? # TODO: hide y-crosshair?
# chart._cursor.graphics[chart]['hl'].hide() # chart._cursor.graphics[chart]['hl'].hide()
# self.setCursor(QtCore.Qt.OpenHandCursor)
# self.setCursor(QtCore.Qt.DragMoveCursor)
else: else:
self.currentPen = self.pen self.currentPen = self.pen
self.label.unhighlight() self.label.unhighlight()
chart._cursor._hovered.remove(self) chart._cursor._hovered.remove(self)
if not self._always_show_label:
self.label.hide()
# highlight any attached label # highlight any attached label
# self.setCursor(QtCore.Qt.OpenHandCursor)
# self.setCursor(QtCore.Qt.DragMoveCursor)
self.update() self.update()
def mouseDragEvent(self, ev): def mouseDragEvent(self, ev):
@ -339,13 +396,6 @@ class LevelLine(pg.InfiniteLine):
) -> None: ) -> None:
print(f'double click {ev}') print(f'double click {ev}')
# def mouseMoved(
# self,
# ev: Tuple[QtGui.QMouseEvent],
# ) -> None:
# pos = evt[0]
# print(pos)
def delete(self) -> None: def delete(self) -> None:
"""Remove this line from containing chart/view/scene. """Remove this line from containing chart/view/scene.
@ -357,6 +407,20 @@ class LevelLine(pg.InfiniteLine):
self._chart.plotItem.removeItem(self) self._chart.plotItem.removeItem(self)
def getEndpoints(self):
"""Get line endpoints at view edges.
Stolen from InfLineLabel.
"""
# calculate points where line intersects view box
# (in line coordinates)
lr = self.boundingRect()
pt1 = pg.Point(lr.left(), 0)
pt2 = pg.Point(lr.right(), 0)
return pt1, pt2
def level_line( def level_line(
chart: 'ChartPlogWidget', # noqa chart: 'ChartPlogWidget', # noqa
@ -367,8 +431,6 @@ def level_line(
# size 4 font on 4k screen scaled down, so small-ish. # size 4 font on 4k screen scaled down, so small-ish.
font_size_inches: float = _down_2_font_inches_we_like, font_size_inches: float = _down_2_font_inches_we_like,
show_label: bool = True,
# whether or not the line placed in view should highlight # whether or not the line placed in view should highlight
# when moused over (aka "hovered") # when moused over (aka "hovered")
hl_on_hover: bool = True, hl_on_hover: bool = True,
@ -376,6 +438,10 @@ def level_line(
# line style # line style
dotted: bool = False, dotted: bool = False,
adjust_to_l1: bool = False,
always_show_label: bool = False,
**linelabelkwargs **linelabelkwargs
) -> LevelLine: ) -> LevelLine:
"""Convenience routine to add a styled horizontal line to a plot. """Convenience routine to add a styled horizontal line to a plot.
@ -396,6 +462,7 @@ def level_line(
**linelabelkwargs **linelabelkwargs
) )
label.update_from_data(0, level) label.update_from_data(0, level)
label.hide()
# TODO: can we somehow figure out a max value from the parent axis? # TODO: can we somehow figure out a max value from the parent axis?
label._size_br_from_str(label.label_str) label._size_br_from_str(label.label_str)
@ -410,15 +477,39 @@ def level_line(
angle=0, angle=0,
hl_on_hover=hl_on_hover, hl_on_hover=hl_on_hover,
dotted=dotted, dotted=dotted,
adjust_to_l1=adjust_to_l1,
always_show_label=always_show_label,
) )
line.setValue(level)
# activate/draw label # activate/draw label
line.setValue(level) line.setValue(level)
line.set_level()
chart.plotItem.addItem(line) chart.plotItem.addItem(line)
if not show_label: return line
label.hide()
def order_line(
*args,
size: Optional[int] = None,
size_digits: int = 0,
**kwargs,
) -> LevelLine:
"""Convenience routine to add a line graphic representing an order execution
submitted to the EMS via the chart's "order mode".
"""
line = level_line(*args, adjust_to_l1=True, **kwargs)
line.label._fmt_fields['{size:.{size_digits}f} x '] = {
'size', 'size_digits'}
if size is not None:
line.label._use_extra_fields = True
line.label.size = size
line.label.size_digits = size_digits
line.label.hide()
return line return line