Support size fields on order lines; avoid overlap with L1 lines
parent
990c3a1eac
commit
b9d9dbfc4a
|
@ -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()
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Reference in New Issue