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`.backup_asyncify_input_modes
parent
3a041e4f47
commit
d2c3b03513
|
@ -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
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue