Move contents labels management to cursor mod

Add a new type/api to manage "contents labels" (labels that sit in
a view and display info about viewed data) since it's mostly used by
the linked charts cursor. Make `LinkedSplits.cursor` the new and only
instance var for the cursor such that charts can look it up from that
common class. Drop the `ChartPlotWidget._ohlc` array, just add
a `'ohlc'` entry to `._arrays`.
asyncify_input_modes
Tyler Goodlet 2021-06-21 16:45:27 -04:00
parent d3d5d4ad06
commit b6eeed1ae0
2 changed files with 164 additions and 164 deletions

View File

@ -20,7 +20,7 @@ High level Qt chart widgets.
"""
import time
from contextlib import AsyncExitStack
from typing import Tuple, Dict, Any, Optional, Callable
from typing import Tuple, Dict, Any, Optional
from types import ModuleType
from functools import partial
@ -261,7 +261,7 @@ class LinkedSplits(QtGui.QWidget):
super().__init__()
# self.signals_visible: bool = False
self._cursor: Cursor = None # crosshair graphics
self.cursor: Cursor = None # crosshair graphics
self.godwidget = godwidget
self.chart: ChartPlotWidget = None # main (ohlc) chart
@ -326,7 +326,7 @@ class LinkedSplits(QtGui.QWidget):
The data input struct array must include OHLC fields.
"""
# add crosshairs
self._cursor = Cursor(
self.cursor = Cursor(
linkedsplits=self,
digits=symbol.digits(),
)
@ -338,7 +338,7 @@ class LinkedSplits(QtGui.QWidget):
_is_main=True,
)
# add crosshair graphic
self.chart.addItem(self._cursor)
self.chart.addItem(self.cursor)
# axis placement
if _xaxis_at == 'bottom':
@ -392,7 +392,7 @@ class LinkedSplits(QtGui.QWidget):
'left': PriceAxis(linkedsplits=self, orientation='left'),
},
viewBox=cv,
cursor=self._cursor,
# cursor=self.cursor,
**cpw_kwargs,
)
print(f'xaxis ps: {xaxis.pos()}')
@ -412,7 +412,7 @@ class LinkedSplits(QtGui.QWidget):
cpw.setXLink(self.chart)
# add to cross-hair's known plots
self._cursor.add_plot(cpw)
self.cursor.add_plot(cpw)
# draw curve graphics
if style == 'bar':
@ -493,15 +493,19 @@ class ChartPlotWidget(pg.PlotWidget):
)
self.name = name
self._lc = linkedsplits
self.linked = linkedsplits
# scene-local placeholder for book graphics
# sizing to avoid overlap with data contents
self._max_l1_line_len: float = 0
# self.setViewportMargins(0, 0, 0, 0)
self._ohlc = array # readonly view of ohlc data
# self._ohlc = array # readonly view of ohlc data
self._arrays = {} # readonly view of overlays
# readonly view of data arrays
self._arrays = {
'ohlc': array,
}
self._graphics = {} # registry of underlying graphics
self._overlays = set() # registry of overlay curve names
@ -510,9 +514,7 @@ class ChartPlotWidget(pg.PlotWidget):
self._vb = self.plotItem.vb
self._static_yrange = static_yrange # for "known y-range style"
self._view_mode: str = 'follow'
self._cursor = cursor # placehold for mouse
# show only right side axes
self.hideAxis('left')
@ -539,25 +541,10 @@ class ChartPlotWidget(pg.PlotWidget):
self._vb.setFocus()
def last_bar_in_view(self) -> int:
self._ohlc[-1]['index']
self._arrays['ohlc'][-1]['index']
def update_contents_labels(
self,
index: int,
# array_name: str,
) -> None:
if index >= 0 and index < self._ohlc[-1]['index']:
for name, (label, update) in self._labels.items():
if name is self.name:
array = self._ohlc
else:
array = self._arrays[name]
try:
update(index, array)
except IndexError:
log.exception(f"Failed to update label: {name}")
def is_valid_index(self, index: int) -> bool:
return index >= 0 and index < self._arrays['ohlc'][-1]['index']
def _set_xlimits(
self,
@ -581,11 +568,11 @@ class ChartPlotWidget(pg.PlotWidget):
"""Return a range tuple for the bars present in view.
"""
l, r = self.view_range()
a = self._ohlc
a = self._arrays['ohlc']
lbar = max(l, a[0]['index'])
rbar = min(r, a[-1]['index'])
# lbar = max(l, 0)
# rbar = min(r, len(self._ohlc))
# rbar = min(r, len(self._arrays['ohlc']))
return l, lbar, rbar, r
def default_view(
@ -595,7 +582,7 @@ class ChartPlotWidget(pg.PlotWidget):
"""Set the view box to the "default" startup view of the scene.
"""
xlast = self._ohlc[index]['index']
xlast = self._arrays['ohlc'][index]['index']
begin = xlast - _bars_to_left_in_follow_mode
end = xlast + _bars_from_right_in_follow_mode
@ -650,12 +637,12 @@ class ChartPlotWidget(pg.PlotWidget):
self._graphics[name] = graphics
self.add_contents_label(
name,
self.linked.cursor.contents_labels.add_label(
self,
'ohlc',
anchor_at=('top', 'left'),
update_func=ContentsLabel.update_from_ohlc,
)
self.update_contents_labels(len(data) - 1)
self._add_sticky(name)
@ -727,32 +714,18 @@ class ChartPlotWidget(pg.PlotWidget):
# (we need something that avoids clutter on x-axis).
self._add_sticky(name, bg_color='default_light')
if add_label:
self.add_contents_label(name, anchor_at=anchor_at)
self.update_contents_labels(len(data) - 1)
if self.linked.cursor:
self.linked.cursor.add_curve_cursor(self, curve)
if self._cursor:
self._cursor.add_curve_cursor(self, curve)
if add_label:
self.linked.cursor.contents_labels.add_label(
self,
name,
anchor_at=anchor_at
)
return curve
def add_contents_label(
self,
name: str,
anchor_at: Tuple[str, str] = ('top', 'left'),
update_func: Callable = ContentsLabel.update_from_value,
) -> ContentsLabel:
label = ContentsLabel(chart=self, anchor_at=anchor_at)
self._labels[name] = (
# calls class method on instance
label,
partial(update_func, label, name)
)
label.show()
return label
def _add_sticky(
self,
name: str,
@ -787,7 +760,7 @@ class ChartPlotWidget(pg.PlotWidget):
"""Update the named internal graphics from ``array``.
"""
self._ohlc = array
self._arrays['ohlc'] = array
graphics = self._graphics[name]
graphics.update_from_array(array, **kwargs)
return graphics
@ -803,7 +776,7 @@ class ChartPlotWidget(pg.PlotWidget):
"""
if name not in self._overlays:
self._ohlc = array
self._arrays['ohlc'] = array
else:
self._arrays[name] = array
@ -857,10 +830,10 @@ class ChartPlotWidget(pg.PlotWidget):
# TODO: logic to check if end of bars in view
# extra = view_len - _min_points_to_show
# begin = self._ohlc[0]['index'] - extra
# begin = self._arrays['ohlc'][0]['index'] - extra
# # end = len(self._ohlc) - 1 + extra
# end = self._ohlc[-1]['index'] - 1 + extra
# # end = len(self._arrays['ohlc']) - 1 + extra
# end = self._arrays['ohlc'][-1]['index'] - 1 + extra
# XXX: test code for only rendering lines for the bars in view.
# This turns out to be very very poor perf when scaling out to
@ -879,9 +852,9 @@ class ChartPlotWidget(pg.PlotWidget):
# self._set_xlimits(begin, end)
# TODO: this should be some kind of numpy view api
# bars = self._ohlc[lbar:rbar]
# bars = self._arrays['ohlc'][lbar:rbar]
a = self._ohlc
a = self._arrays['ohlc']
ifirst = a[0]['index']
bars = a[lbar - ifirst:rbar - ifirst + 1]
@ -952,84 +925,6 @@ class ChartPlotWidget(pg.PlotWidget):
self.scene().leaveEvent(ev)
async def test_bed(
ohlcv,
chart,
lc,
):
from ._graphics._lines import order_line
sleep = 6
# from PyQt5.QtCore import QPointF
vb = chart._vb
# scene = vb.scene()
# raxis = chart.getAxis('right')
# vb_right = vb.boundingRect().right()
last, i_end = ohlcv.array[-1][['close', 'index']]
line = order_line(
chart,
level=last,
level_digits=2
)
# eps = line.getEndpoints()
# llabel = line._labels[1][1]
line.update_labels({'level': last})
return
# rl = eps[1]
# rlabel.setPos(rl)
# ti = pg.TextItem(text='Fuck you')
# ti.setPos(pg.Point(i_end, last))
# ti.setParentItem(line)
# ti.setAnchor(pg.Point(1, 1))
# vb.addItem(ti)
# chart.plotItem.addItem(ti)
from ._label import Label
txt = Label(
vb,
fmt_str='fuck {it}',
)
txt.format(it='boy')
txt.place_on_scene('left')
txt.set_view_y(last)
# txt = QtGui.QGraphicsTextItem()
# txt.setPlainText("FUCK YOU")
# txt.setFont(_font.font)
# txt.setDefaultTextColor(pg.mkColor(hcolor('bracket')))
# # txt.setParentItem(vb)
# w = txt.boundingRect().width()
# scene.addItem(txt)
# txt.setParentItem(line)
# d_coords = vb.mapFromView(QPointF(i_end, last))
# txt.setPos(vb_right - w, d_coords.y())
# txt.show()
# txt.update()
# rlabel.setPos(vb_right - 2*w, d_coords.y())
# rlabel.show()
i = 0
while True:
await trio.sleep(sleep)
await tractor.breakpoint()
txt.format(it=f'dog_{i}')
# d_coords = vb.mapFromView(QPointF(i_end, last))
# txt.setPos(vb_right - w, d_coords.y())
# txt.setPlainText(f"FUCK YOU {i}")
i += 1
_clear_throttle_rate: int = 60 # Hz
_book_throttle_rate: int = 16 # Hz
@ -1065,7 +960,7 @@ async def chart_from_quotes(
# https://arxiv.org/abs/cs/0610046
# https://github.com/lemire/pythonmaxmin
array = chart._ohlc
array = chart._arrays['ohlc']
ifirst = array[0]['index']
last_bars_range = chart.bars_range()
@ -1385,7 +1280,7 @@ async def run_fsp(
)
# display contents labels asap
chart.update_contents_labels(
chart.linked.cursor.contents_labels.update_labels(
len(shm.array) - 1,
# fsp_func_name
)

View File

@ -18,8 +18,8 @@
Mouse interaction graphics
"""
import math
from typing import Optional, Tuple, Set, Dict
from functools import partial
from typing import Optional, Callable
import inspect
import numpy as np
@ -30,7 +30,6 @@ from PyQt5.QtCore import QPointF, QRectF
from .._style import (
_xaxis_at,
hcolor,
_font,
_font_small,
)
from .._axes import YAxisLabel, XAxisLabel
@ -98,7 +97,7 @@ class LineDot(pg.CurvePoint):
(x, y) = self.curve().getData()
index = self.property('index')
# first = self._plot._ohlc[0]['index']
# first = self._plot._arrays['ohlc'][0]['index']
# first = x[0]
# i = index - first
i = index - x[0]
@ -133,11 +132,15 @@ class ContentsLabel(pg.LabelItem):
}
def __init__(
self,
chart: 'ChartPlotWidget', # noqa
# chart: 'ChartPlotWidget', # noqa
view: pg.ViewBox,
anchor_at: str = ('top', 'right'),
justify_text: str = 'left',
font_size: Optional[int] = None,
) -> None:
font_size = font_size or _font_small.px_size
@ -148,9 +151,10 @@ class ContentsLabel(pg.LabelItem):
)
# anchor to viewbox
self.setParentItem(chart._vb)
chart.scene().addItem(self)
self.chart = chart
self.setParentItem(view)
self.vb = view
view.scene().addItem(self)
v, h = anchor_at
index = (self._corner_anchors[h], self._corner_anchors[v])
@ -163,10 +167,12 @@ class ContentsLabel(pg.LabelItem):
self.anchor(itemPos=index, parentPos=index, offset=margins)
def update_from_ohlc(
self,
name: str,
index: int,
array: np.ndarray,
) -> None:
# this being "html" is the dumbest shit :eyeroll:
first = array[0]['index']
@ -188,25 +194,111 @@ class ContentsLabel(pg.LabelItem):
)
def update_from_value(
self,
name: str,
index: int,
array: np.ndarray,
) -> None:
first = array[0]['index']
if index < array[-1]['index'] and index > first:
data = array[index - first][name]
self.setText(f"{name}: {data:.2f}")
class ContentsLabels:
'''Collection of labels that span a ``LinkedSplits`` set of chart plots
and can be updated from the underlying data from an x-index value sent
as input from a cursor or other query mechanism.
'''
def __init__(
self,
linkedsplits: 'LinkedSplits', # type: ignore # noqa
) -> None:
self.linkedsplits = linkedsplits
self._labels: list[(
'CharPlotWidget', # type: ignore # noqa
str,
ContentsLabel,
Callable
)] = []
def update_labels(
self,
index: int,
# array_name: str,
) -> None:
# for name, (label, update) in self._labels.items():
for chart, name, label, update in self._labels:
if not (index >= 0 and index < chart._arrays['ohlc'][-1]['index']):
# out of range
continue
array = chart._arrays[name]
# call provided update func with data point
try:
label.show()
update(index, array)
except IndexError:
log.exception(f"Failed to update label: {name}")
def hide(self) -> None:
for chart, name, label, update in self._labels:
label.hide()
def add_label(
self,
chart: 'ChartPlotWidget', # type: ignore # noqa
name: str,
anchor_at: tuple[str, str] = ('top', 'left'),
update_func: Callable = ContentsLabel.update_from_value,
) -> ContentsLabel:
label = ContentsLabel(
view=chart._vb,
anchor_at=anchor_at,
)
self._labels.append(
(chart, name, label, partial(update_func, label, name))
)
# label.hide()
return label
class Cursor(pg.GraphicsObject):
def __init__(
self,
linkedsplits: 'LinkedSplits', # noqa
digits: int = 0
) -> None:
super().__init__()
self.linked = linkedsplits
self.graphics: dict[str, pg.GraphicsObject] = {}
self.plots: List['PlotChartWidget'] = [] # type: ignore # noqa
self.active_plot = None
self.digits: int = digits
self._datum_xy: tuple[int, float] = (0, 0)
self._hovered: set[pg.GraphicsObject] = set()
self._trackers: set[pg.GraphicsObject] = set()
# XXX: not sure why these are instance variables?
# It's not like we can change them on the fly..?
self.pen = pg.mkPen(
@ -217,19 +309,10 @@ class Cursor(pg.GraphicsObject):
color=hcolor('davies'),
style=QtCore.Qt.DashLine,
)
self.lsc = linkedsplits
self.graphics: Dict[str, pg.GraphicsObject] = {}
self.plots: List['PlotChartWidget'] = [] # type: ignore # noqa
self.active_plot = None
self.digits: int = digits
self._datum_xy: Tuple[int, float] = (0, 0)
self._hovered: Set[pg.GraphicsObject] = set()
self._trackers: Set[pg.GraphicsObject] = set()
# value used for rounding y-axis discreet tick steps
# computing once, up front, here cuz why not
self._y_incr_mult = 1 / self.lsc._symbol.tick_size
self._y_incr_mult = 1 / self.linked._symbol.tick_size
# line width in view coordinates
self._lw = self.pixelWidth() * self.lines_pen.width()
@ -239,6 +322,22 @@ class Cursor(pg.GraphicsObject):
self._y_label_update: bool = True
self.contents_labels = ContentsLabels(self.linked)
self._in_query_mode: bool = False
@property
def in_query_mode(self) -> bool:
return self._in_query_mode
@in_query_mode.setter
def in_query_mode(self, value: bool) -> None:
if self._in_query_mode and not value:
# edge trigger hide all labels
self.contents_labels.hide()
self._in_query_mode = value
def add_hovered(
self,
item: pg.GraphicsObject,
@ -320,7 +419,7 @@ class Cursor(pg.GraphicsObject):
# the current sample under the mouse
cursor = LineDot(
curve,
index=plot._ohlc[-1]['index'],
index=plot._arrays['ohlc'][-1]['index'],
plot=plot
)
plot.addItem(cursor)
@ -344,7 +443,7 @@ class Cursor(pg.GraphicsObject):
def mouseMoved(
self,
evt: 'Tuple[QMouseEvent]', # noqa
evt: 'tuple[QMouseEvent]', # noqa
) -> None: # noqa
"""Update horizonal and vertical lines when mouse moves inside
either the main chart or any indicator subplot.
@ -392,10 +491,16 @@ class Cursor(pg.GraphicsObject):
item.on_tracked_source(ix, iy)
if ix != last_ix:
if self.in_query_mode:
# show contents labels on all linked charts and update
# with cursor movement
self.contents_labels.update_labels(ix)
for plot, opts in self.graphics.items():
# update the chart's "contents" label
plot.update_contents_labels(ix)
# plot.update_contents_labels(ix)
# move the vertical line to the current "center of bar"
opts['vl'].setX(ix + line_offset)