Compare commits

..

10 Commits

Author SHA1 Message Date
Tyler Goodlet c58a56fca7 Comment stream drain idea
Likely it's not going to be that useful but keep it for reference for
the moment.
2022-01-25 08:11:20 -05:00
Tyler Goodlet 719187bf5a WIP idea: drain feed stream, doesn't do much.. 2022-01-25 08:11:06 -05:00
Tyler Goodlet c7436d5857 Drop dpi logging back to debug 2022-01-25 08:08:50 -05:00
Tyler Goodlet 82a9c62c07 Annoying doc string(s) 2022-01-25 08:00:45 -05:00
Tyler Goodlet edd227228c Fix bottom axis check logic for overlays, try out some px perfection 2022-01-25 08:00:45 -05:00
Tyler Goodlet 00b1b2a10c Allow passing in parent to `Label` 2022-01-25 08:00:45 -05:00
Tyler Goodlet 1a077c0553 Hide the unit vlm after the $vlm is up
Since more curves costs more processing and since the vlm and $vlm
curves are normally very close to the same (graphically) we hide the
unit volume curve once the dollar volume is up (after the fsp daemon-task is
spawned) and just expect the user to understand the diff in axes units.
Also, use the new `title=` api to `.overlay_plotitem()`.
2022-01-25 08:00:45 -05:00
Tyler Goodlet 26327e5462 Use overlay api to access multi-axes by name 2022-01-25 08:00:45 -05:00
Tyler Goodlet d600a2ca70 Make axes labels more pixel perfect 2022-01-25 08:00:45 -05:00
Tyler Goodlet e4bf3a5fe4 Pop vlm chart from subplots to avoid double render 2022-01-25 08:00:45 -05:00
9 changed files with 157 additions and 97 deletions

View File

@ -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

View File

@ -287,6 +287,8 @@ class AxisLabel(pg.GraphicsObject):
self.path = None
self.rect = None
self._pw = self.pixelWidth()
def paint(
self,
p: QtGui.QPainter,
@ -419,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)])
@ -433,17 +436,19 @@ class XAxisLabel(AxisLabel):
w = self.boundingRect().width()
self.setPos(QPointF(
abs_pos.x() - w/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
@ -513,10 +518,12 @@ class YAxisLabel(AxisLabel):
br = self.boundingRect()
h = br.height()
self.setPos(QPointF(
self.setPos(
QPointF(
x_offset,
abs_pos.y() - h / 2 - self._y_margin / 2
))
abs_pos.y() - h / 2 - self._pw,
)
)
self.update()
def update_on_resize(self, vr, r):
@ -553,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

View File

@ -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,55 +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,
@ -1016,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,

View File

@ -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,9 +557,6 @@ class Cursor(pg.GraphicsObject):
# left axis offset width for calcuating
# absolute x-axis label placement.
left_axis_width = 0
if 'bottom' in axes:
left = axes.get('left')
if left:
left_axis_width = left['item'].width()

View File

@ -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()

View File

@ -655,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,
)
@ -690,10 +690,11 @@ async def open_vlm_displays(
pi = chart.overlay_plotitem(
'dolla_vlm',
index=0, # place axis on inside (nearest to chart)
axis_title=' $vlm',
axis_side='right',
axis_kwargs={
# 'humanize': True,
# 'text': 'dvlm',
'typical_max_str': ' 99.9 M ',
'typical_max_str': ' 100.0 M ',
'formatter': partial(
humanize,
digits=2,
@ -702,10 +703,6 @@ async def open_vlm_displays(
)
# add axis title
raxis = pi.getAxis('right')
raxis.set_title(' $vlm', view=pi.getViewBox())
# add custom auto range handler
pi.vb._maxmin = partial(maxmin, name='dolla_vlm')
@ -716,13 +713,18 @@ async def open_vlm_displays(
array_key='dolla_vlm',
overlay=pi,
color='charcoal',
# 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?
# 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..
# specially store ref to shm for lookup in display loop

View File

@ -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)

View File

@ -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

View File

@ -16,6 +16,7 @@
"""
Qt UI styling.
"""
from typing import Optional, Dict
import math
@ -141,7 +142,7 @@ class DpiAwareFont:
self._font_inches = inches
font_size = math.floor(inches * dpi)
log.info(
log.debug(
f"screen:{screen.name()}]\n"
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
f"\nOur best guess font size is {font_size}\n"