Compare commits

..

No commits in common. "c58a56fca7bbc94b44358fe8a6cf4ae65c65287c" and "56f9ddb880774b7837ee7c72db24be6acd7e3b7f" have entirely different histories.

9 changed files with 97 additions and 157 deletions

View File

@ -1,5 +1,5 @@
# piker: trading gear for hackers
# Copyright (C) Tyler Goodlet (in stewardship of pikers)
# Copyright (C) 2018-present Tyler Goodlet (in stewardship of piker0)
# 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,8 +287,6 @@ class AxisLabel(pg.GraphicsObject):
self.path = None
self.rect = None
self._pw = self.pixelWidth()
def paint(
self,
p: QtGui.QPainter,
@ -421,7 +419,6 @@ 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)])
@ -436,19 +433,17 @@ class XAxisLabel(AxisLabel):
w = self.boundingRect().width()
self.setPos(
QPointF(
abs_pos.x() - w/2 - self._pw,
y_offset/2,
)
)
self.setPos(QPointF(
abs_pos.x() - w/2,
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 - self._pw * 0.5
middle = w/2 - 0.5
aw = h/2
left = middle - aw
right = middle + aw
@ -518,12 +513,10 @@ class YAxisLabel(AxisLabel):
br = self.boundingRect()
h = br.height()
self.setPos(
QPointF(
x_offset,
abs_pos.y() - h / 2 - self._pw,
)
)
self.setPos(QPointF(
x_offset,
abs_pos.y() - h / 2 - self._y_margin / 2
))
self.update()
def update_on_resize(self, vr, r):
@ -560,7 +553,7 @@ class YAxisLabel(AxisLabel):
path = QtGui.QPainterPath()
h = self.rect.height()
path.moveTo(0, 0)
path.lineTo(-x_offset - h/4, h/2. - self._pw/2)
path.lineTo(-x_offset - h/4, h/2.)
path.lineTo(0, h)
path.closeSubpath()
self.path = path

View File

@ -479,20 +479,14 @@ 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
@ -732,6 +726,11 @@ 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)
@ -863,58 +862,55 @@ 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
allowed_sides = {'left', 'right'}
if axis_side not in allowed_sides:
raise ValueError(f'``axis_side``` must be in {allowed_sides}')
# xaxis = DynamicDateAxis(
# orientation='bottom',
# linkedsplits=self.linked,
# )
yaxis = PriceAxis(
orientation=axis_side,
orientation='right',
linkedsplits=self.linked,
**axis_kwargs,
)
pi = pg.PlotItem(
plotitem = pg.PlotItem(
parent=self.plotItem,
name=name,
enableMenu=False,
viewBox=cv,
axisItems={
# 'bottom': xaxis,
axis_side: yaxis,
'right': yaxis,
},
default_axes=[],
)
pi.hideButtons()
# plotitem.setAxisItems(
# add_to_layout=False,
# axisItems={
# 'bottom': xaxis,
# 'right': yaxis,
# },
# )
# plotite.hideAxis('right')
# plotite.hideAxis('bottom')
# plotitem.addItem(curve)
cv.enable_auto_yrange()
# 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(
pi,
index=index,
# plotitem.enableAutoRange(axis='y')
plotitem.hideButtons()
self.pi_overlay.add_plotitem(
plotitem,
# only link x-axes,
link_axes=(0,),
)
# 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
return plotitem
def draw_curve(
self,
@ -1020,8 +1016,7 @@ class ChartPlotWidget(pg.PlotWidget):
# add y-axis "last" value label
last = self._ysticks[name] = YAxisLabel(
chart=self,
# parent=self.getAxis('right'),
parent=self.pi_overlay.get_axis(self.plotItem, 'right'),
parent=self.getAxis('right'),
# TODO: pass this from symbol data
digits=digits,
opacity=1,

View File

@ -369,13 +369,7 @@ 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
@ -388,8 +382,7 @@ class Cursor(pg.GraphicsObject):
yl = YAxisLabel(
chart=plot,
# parent=plot.getAxis('right'),
parent=plot.pi_overlay.get_axis(plot.plotItem, 'right'),
parent=plot.getAxis('right'),
digits=digits or self.digits,
opacity=_ch_label_opac,
bg_color=self.label_color,
@ -431,25 +424,19 @@ 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:
if plot.linked.xaxis_chart is plot:
xlabel = self.xaxis_label = XAxisLabel(
if 'bottom' in plot.plotItem.axes:
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
xlabel.setPos(
self.plots[0].mapFromView(QPointF(0, 0))
)
xlabel.show()
self.xaxis_label.setPos(self.plots[0].mapFromView(QPointF(0, 0)))
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
@ -506,27 +493,24 @@ 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
vl_y = iy - line_offset
# px perfect...
line_offset = self._lw / 2
# 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, vl_y)),
abs_pos=plot.mapFromView(QPointF(ix, iy)),
value=iy
)
# only update horizontal xhair line if label is enabled
# self.graphics[plot]['hl'].setY(iy)
self.graphics[plot]['hl'].setY(vl_y)
self.graphics[plot]['hl'].setY(iy)
# update all trackers
for item in self._trackers:
@ -557,18 +541,21 @@ 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()
# 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,
)
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,
)
self._datum_xy = ix, iy

View File

@ -115,14 +115,13 @@ 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 (some might be done already, can't member):
# TODO: bunch of stuff:
# - 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.
@ -182,34 +181,13 @@ 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
@ -218,8 +196,7 @@ 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 = time.time()
last_quote = now
# chart isn't active/shown so skip render cycle and pause feed(s)
if chart.linked.isHidden():
@ -644,15 +621,9 @@ 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.. bby
# let the app run.
await trio.sleep_forever()

View File

@ -655,7 +655,7 @@ async def open_vlm_displays(
last_val_sticky.update_from_data(-1, value)
vlm_curve = chart.update_curve_from_array(
chart.update_curve_from_array(
'volume',
shm.array,
)
@ -690,11 +690,10 @@ 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={
'typical_max_str': ' 100.0 M ',
# 'humanize': True,
# 'text': 'dvlm',
'typical_max_str': ' 99.9 M ',
'formatter': partial(
humanize,
digits=2,
@ -703,6 +702,10 @@ 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')
@ -713,18 +716,13 @@ async def open_vlm_displays(
array_key='dolla_vlm',
overlay=pi,
# color='bracket',
# TODO: this color or dark volume
# color='charcoal',
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()
# 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,8 +50,10 @@ 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,
@ -61,7 +63,6 @@ class Label:
font_size: str = 'small',
opacity: float = 1,
fields: dict = {},
parent: pg.GraphicsObject = None,
update_on_range_change: bool = True,
) -> None:
@ -70,13 +71,11 @@ 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(parent=parent)
txt = self.txt = QtWidgets.QGraphicsTextItem()
txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
vb.scene().addItem(txt)
@ -87,6 +86,7 @@ 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) -> str:
def color(self):
return self._hcolor
@color.setter
@ -118,10 +118,9 @@ 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
@ -235,8 +234,7 @@ 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__(
@ -275,8 +273,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,11 +334,10 @@ 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,7 +16,6 @@
"""
Qt UI styling.
"""
from typing import Optional, Dict
import math
@ -142,7 +141,7 @@ class DpiAwareFont:
self._font_inches = inches
font_size = math.floor(inches * dpi)
log.debug(
log.info(
f"screen:{screen.name()}]\n"
f"pDPI: {pdpi}, lDPI: {ldpi}, scale: {scale}\n"
f"\nOur best guess font size is {font_size}\n"