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
parent
d3d5d4ad06
commit
b6eeed1ae0
|
@ -20,7 +20,7 @@ High level Qt chart widgets.
|
||||||
"""
|
"""
|
||||||
import time
|
import time
|
||||||
from contextlib import AsyncExitStack
|
from contextlib import AsyncExitStack
|
||||||
from typing import Tuple, Dict, Any, Optional, Callable
|
from typing import Tuple, Dict, Any, Optional
|
||||||
from types import ModuleType
|
from types import ModuleType
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
|
@ -261,7 +261,7 @@ class LinkedSplits(QtGui.QWidget):
|
||||||
super().__init__()
|
super().__init__()
|
||||||
|
|
||||||
# self.signals_visible: bool = False
|
# self.signals_visible: bool = False
|
||||||
self._cursor: Cursor = None # crosshair graphics
|
self.cursor: Cursor = None # crosshair graphics
|
||||||
|
|
||||||
self.godwidget = godwidget
|
self.godwidget = godwidget
|
||||||
self.chart: ChartPlotWidget = None # main (ohlc) chart
|
self.chart: ChartPlotWidget = None # main (ohlc) chart
|
||||||
|
@ -326,7 +326,7 @@ class LinkedSplits(QtGui.QWidget):
|
||||||
The data input struct array must include OHLC fields.
|
The data input struct array must include OHLC fields.
|
||||||
"""
|
"""
|
||||||
# add crosshairs
|
# add crosshairs
|
||||||
self._cursor = Cursor(
|
self.cursor = Cursor(
|
||||||
linkedsplits=self,
|
linkedsplits=self,
|
||||||
digits=symbol.digits(),
|
digits=symbol.digits(),
|
||||||
)
|
)
|
||||||
|
@ -338,7 +338,7 @@ class LinkedSplits(QtGui.QWidget):
|
||||||
_is_main=True,
|
_is_main=True,
|
||||||
)
|
)
|
||||||
# add crosshair graphic
|
# add crosshair graphic
|
||||||
self.chart.addItem(self._cursor)
|
self.chart.addItem(self.cursor)
|
||||||
|
|
||||||
# axis placement
|
# axis placement
|
||||||
if _xaxis_at == 'bottom':
|
if _xaxis_at == 'bottom':
|
||||||
|
@ -392,7 +392,7 @@ class LinkedSplits(QtGui.QWidget):
|
||||||
'left': PriceAxis(linkedsplits=self, orientation='left'),
|
'left': PriceAxis(linkedsplits=self, orientation='left'),
|
||||||
},
|
},
|
||||||
viewBox=cv,
|
viewBox=cv,
|
||||||
cursor=self._cursor,
|
# cursor=self.cursor,
|
||||||
**cpw_kwargs,
|
**cpw_kwargs,
|
||||||
)
|
)
|
||||||
print(f'xaxis ps: {xaxis.pos()}')
|
print(f'xaxis ps: {xaxis.pos()}')
|
||||||
|
@ -412,7 +412,7 @@ class LinkedSplits(QtGui.QWidget):
|
||||||
cpw.setXLink(self.chart)
|
cpw.setXLink(self.chart)
|
||||||
|
|
||||||
# add to cross-hair's known plots
|
# add to cross-hair's known plots
|
||||||
self._cursor.add_plot(cpw)
|
self.cursor.add_plot(cpw)
|
||||||
|
|
||||||
# draw curve graphics
|
# draw curve graphics
|
||||||
if style == 'bar':
|
if style == 'bar':
|
||||||
|
@ -493,15 +493,19 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
)
|
)
|
||||||
self.name = name
|
self.name = name
|
||||||
self._lc = linkedsplits
|
self._lc = linkedsplits
|
||||||
|
self.linked = linkedsplits
|
||||||
|
|
||||||
# scene-local placeholder for book graphics
|
# scene-local placeholder for book graphics
|
||||||
# sizing to avoid overlap with data contents
|
# sizing to avoid overlap with data contents
|
||||||
self._max_l1_line_len: float = 0
|
self._max_l1_line_len: float = 0
|
||||||
|
|
||||||
# self.setViewportMargins(0, 0, 0, 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._graphics = {} # registry of underlying graphics
|
||||||
self._overlays = set() # registry of overlay curve names
|
self._overlays = set() # registry of overlay curve names
|
||||||
|
|
||||||
|
@ -510,9 +514,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
self._vb = self.plotItem.vb
|
self._vb = self.plotItem.vb
|
||||||
self._static_yrange = static_yrange # for "known y-range style"
|
self._static_yrange = static_yrange # for "known y-range style"
|
||||||
|
|
||||||
self._view_mode: str = 'follow'
|
self._view_mode: str = 'follow'
|
||||||
self._cursor = cursor # placehold for mouse
|
|
||||||
|
|
||||||
# show only right side axes
|
# show only right side axes
|
||||||
self.hideAxis('left')
|
self.hideAxis('left')
|
||||||
|
@ -539,25 +541,10 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self._vb.setFocus()
|
self._vb.setFocus()
|
||||||
|
|
||||||
def last_bar_in_view(self) -> int:
|
def last_bar_in_view(self) -> int:
|
||||||
self._ohlc[-1]['index']
|
self._arrays['ohlc'][-1]['index']
|
||||||
|
|
||||||
def update_contents_labels(
|
def is_valid_index(self, index: int) -> bool:
|
||||||
self,
|
return index >= 0 and index < self._arrays['ohlc'][-1]['index']
|
||||||
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 _set_xlimits(
|
def _set_xlimits(
|
||||||
self,
|
self,
|
||||||
|
@ -581,11 +568,11 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""Return a range tuple for the bars present in view.
|
"""Return a range tuple for the bars present in view.
|
||||||
"""
|
"""
|
||||||
l, r = self.view_range()
|
l, r = self.view_range()
|
||||||
a = self._ohlc
|
a = self._arrays['ohlc']
|
||||||
lbar = max(l, a[0]['index'])
|
lbar = max(l, a[0]['index'])
|
||||||
rbar = min(r, a[-1]['index'])
|
rbar = min(r, a[-1]['index'])
|
||||||
# lbar = max(l, 0)
|
# lbar = max(l, 0)
|
||||||
# rbar = min(r, len(self._ohlc))
|
# rbar = min(r, len(self._arrays['ohlc']))
|
||||||
return l, lbar, rbar, r
|
return l, lbar, rbar, r
|
||||||
|
|
||||||
def default_view(
|
def default_view(
|
||||||
|
@ -595,7 +582,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""Set the view box to the "default" startup view of the scene.
|
"""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
|
begin = xlast - _bars_to_left_in_follow_mode
|
||||||
end = xlast + _bars_from_right_in_follow_mode
|
end = xlast + _bars_from_right_in_follow_mode
|
||||||
|
|
||||||
|
@ -650,12 +637,12 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
|
|
||||||
self._graphics[name] = graphics
|
self._graphics[name] = graphics
|
||||||
|
|
||||||
self.add_contents_label(
|
self.linked.cursor.contents_labels.add_label(
|
||||||
name,
|
self,
|
||||||
|
'ohlc',
|
||||||
anchor_at=('top', 'left'),
|
anchor_at=('top', 'left'),
|
||||||
update_func=ContentsLabel.update_from_ohlc,
|
update_func=ContentsLabel.update_from_ohlc,
|
||||||
)
|
)
|
||||||
self.update_contents_labels(len(data) - 1)
|
|
||||||
|
|
||||||
self._add_sticky(name)
|
self._add_sticky(name)
|
||||||
|
|
||||||
|
@ -727,32 +714,18 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# (we need something that avoids clutter on x-axis).
|
# (we need something that avoids clutter on x-axis).
|
||||||
self._add_sticky(name, bg_color='default_light')
|
self._add_sticky(name, bg_color='default_light')
|
||||||
|
|
||||||
if add_label:
|
if self.linked.cursor:
|
||||||
self.add_contents_label(name, anchor_at=anchor_at)
|
self.linked.cursor.add_curve_cursor(self, curve)
|
||||||
self.update_contents_labels(len(data) - 1)
|
|
||||||
|
|
||||||
if self._cursor:
|
if add_label:
|
||||||
self._cursor.add_curve_cursor(self, curve)
|
self.linked.cursor.contents_labels.add_label(
|
||||||
|
self,
|
||||||
|
name,
|
||||||
|
anchor_at=anchor_at
|
||||||
|
)
|
||||||
|
|
||||||
return curve
|
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(
|
def _add_sticky(
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
|
@ -787,7 +760,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""Update the named internal graphics from ``array``.
|
"""Update the named internal graphics from ``array``.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self._ohlc = array
|
self._arrays['ohlc'] = array
|
||||||
graphics = self._graphics[name]
|
graphics = self._graphics[name]
|
||||||
graphics.update_from_array(array, **kwargs)
|
graphics.update_from_array(array, **kwargs)
|
||||||
return graphics
|
return graphics
|
||||||
|
@ -803,7 +776,7 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if name not in self._overlays:
|
if name not in self._overlays:
|
||||||
self._ohlc = array
|
self._arrays['ohlc'] = array
|
||||||
else:
|
else:
|
||||||
self._arrays[name] = array
|
self._arrays[name] = array
|
||||||
|
|
||||||
|
@ -857,10 +830,10 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
# TODO: logic to check if end of bars in view
|
# TODO: logic to check if end of bars in view
|
||||||
# extra = view_len - _min_points_to_show
|
# 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 = len(self._arrays['ohlc']) - 1 + extra
|
||||||
# end = self._ohlc[-1]['index'] - 1 + extra
|
# end = self._arrays['ohlc'][-1]['index'] - 1 + extra
|
||||||
|
|
||||||
# XXX: test code for only rendering lines for the bars in view.
|
# 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
|
# 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)
|
# self._set_xlimits(begin, end)
|
||||||
|
|
||||||
# TODO: this should be some kind of numpy view api
|
# 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']
|
ifirst = a[0]['index']
|
||||||
bars = a[lbar - ifirst:rbar - ifirst + 1]
|
bars = a[lbar - ifirst:rbar - ifirst + 1]
|
||||||
|
|
||||||
|
@ -952,84 +925,6 @@ class ChartPlotWidget(pg.PlotWidget):
|
||||||
self.scene().leaveEvent(ev)
|
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
|
_clear_throttle_rate: int = 60 # Hz
|
||||||
_book_throttle_rate: int = 16 # Hz
|
_book_throttle_rate: int = 16 # Hz
|
||||||
|
|
||||||
|
@ -1065,7 +960,7 @@ async def chart_from_quotes(
|
||||||
# https://arxiv.org/abs/cs/0610046
|
# https://arxiv.org/abs/cs/0610046
|
||||||
# https://github.com/lemire/pythonmaxmin
|
# https://github.com/lemire/pythonmaxmin
|
||||||
|
|
||||||
array = chart._ohlc
|
array = chart._arrays['ohlc']
|
||||||
ifirst = array[0]['index']
|
ifirst = array[0]['index']
|
||||||
|
|
||||||
last_bars_range = chart.bars_range()
|
last_bars_range = chart.bars_range()
|
||||||
|
@ -1385,7 +1280,7 @@ async def run_fsp(
|
||||||
)
|
)
|
||||||
|
|
||||||
# display contents labels asap
|
# display contents labels asap
|
||||||
chart.update_contents_labels(
|
chart.linked.cursor.contents_labels.update_labels(
|
||||||
len(shm.array) - 1,
|
len(shm.array) - 1,
|
||||||
# fsp_func_name
|
# fsp_func_name
|
||||||
)
|
)
|
||||||
|
|
|
@ -18,8 +18,8 @@
|
||||||
Mouse interaction graphics
|
Mouse interaction graphics
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import math
|
from functools import partial
|
||||||
from typing import Optional, Tuple, Set, Dict
|
from typing import Optional, Callable
|
||||||
|
|
||||||
import inspect
|
import inspect
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
@ -30,7 +30,6 @@ from PyQt5.QtCore import QPointF, QRectF
|
||||||
from .._style import (
|
from .._style import (
|
||||||
_xaxis_at,
|
_xaxis_at,
|
||||||
hcolor,
|
hcolor,
|
||||||
_font,
|
|
||||||
_font_small,
|
_font_small,
|
||||||
)
|
)
|
||||||
from .._axes import YAxisLabel, XAxisLabel
|
from .._axes import YAxisLabel, XAxisLabel
|
||||||
|
@ -98,7 +97,7 @@ class LineDot(pg.CurvePoint):
|
||||||
|
|
||||||
(x, y) = self.curve().getData()
|
(x, y) = self.curve().getData()
|
||||||
index = self.property('index')
|
index = self.property('index')
|
||||||
# first = self._plot._ohlc[0]['index']
|
# first = self._plot._arrays['ohlc'][0]['index']
|
||||||
# first = x[0]
|
# first = x[0]
|
||||||
# i = index - first
|
# i = index - first
|
||||||
i = index - x[0]
|
i = index - x[0]
|
||||||
|
@ -133,11 +132,15 @@ class ContentsLabel(pg.LabelItem):
|
||||||
}
|
}
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
chart: 'ChartPlotWidget', # noqa
|
# chart: 'ChartPlotWidget', # noqa
|
||||||
|
view: pg.ViewBox,
|
||||||
|
|
||||||
anchor_at: str = ('top', 'right'),
|
anchor_at: str = ('top', 'right'),
|
||||||
justify_text: str = 'left',
|
justify_text: str = 'left',
|
||||||
font_size: Optional[int] = None,
|
font_size: Optional[int] = None,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
font_size = font_size or _font_small.px_size
|
font_size = font_size or _font_small.px_size
|
||||||
|
@ -148,9 +151,10 @@ class ContentsLabel(pg.LabelItem):
|
||||||
)
|
)
|
||||||
|
|
||||||
# anchor to viewbox
|
# anchor to viewbox
|
||||||
self.setParentItem(chart._vb)
|
self.setParentItem(view)
|
||||||
chart.scene().addItem(self)
|
|
||||||
self.chart = chart
|
self.vb = view
|
||||||
|
view.scene().addItem(self)
|
||||||
|
|
||||||
v, h = anchor_at
|
v, h = anchor_at
|
||||||
index = (self._corner_anchors[h], self._corner_anchors[v])
|
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)
|
self.anchor(itemPos=index, parentPos=index, offset=margins)
|
||||||
|
|
||||||
def update_from_ohlc(
|
def update_from_ohlc(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
index: int,
|
index: int,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
# this being "html" is the dumbest shit :eyeroll:
|
# this being "html" is the dumbest shit :eyeroll:
|
||||||
first = array[0]['index']
|
first = array[0]['index']
|
||||||
|
@ -188,25 +194,111 @@ class ContentsLabel(pg.LabelItem):
|
||||||
)
|
)
|
||||||
|
|
||||||
def update_from_value(
|
def update_from_value(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
name: str,
|
name: str,
|
||||||
index: int,
|
index: int,
|
||||||
array: np.ndarray,
|
array: np.ndarray,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
first = array[0]['index']
|
first = array[0]['index']
|
||||||
if index < array[-1]['index'] and index > first:
|
if index < array[-1]['index'] and index > first:
|
||||||
data = array[index - first][name]
|
data = array[index - first][name]
|
||||||
self.setText(f"{name}: {data:.2f}")
|
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):
|
class Cursor(pg.GraphicsObject):
|
||||||
|
|
||||||
def __init__(
|
def __init__(
|
||||||
|
|
||||||
self,
|
self,
|
||||||
linkedsplits: 'LinkedSplits', # noqa
|
linkedsplits: 'LinkedSplits', # noqa
|
||||||
digits: int = 0
|
digits: int = 0
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
|
|
||||||
super().__init__()
|
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?
|
# XXX: not sure why these are instance variables?
|
||||||
# It's not like we can change them on the fly..?
|
# It's not like we can change them on the fly..?
|
||||||
self.pen = pg.mkPen(
|
self.pen = pg.mkPen(
|
||||||
|
@ -217,19 +309,10 @@ class Cursor(pg.GraphicsObject):
|
||||||
color=hcolor('davies'),
|
color=hcolor('davies'),
|
||||||
style=QtCore.Qt.DashLine,
|
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
|
# value used for rounding y-axis discreet tick steps
|
||||||
# computing once, up front, here cuz why not
|
# 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
|
# line width in view coordinates
|
||||||
self._lw = self.pixelWidth() * self.lines_pen.width()
|
self._lw = self.pixelWidth() * self.lines_pen.width()
|
||||||
|
@ -239,6 +322,22 @@ class Cursor(pg.GraphicsObject):
|
||||||
|
|
||||||
self._y_label_update: bool = True
|
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(
|
def add_hovered(
|
||||||
self,
|
self,
|
||||||
item: pg.GraphicsObject,
|
item: pg.GraphicsObject,
|
||||||
|
@ -320,7 +419,7 @@ class Cursor(pg.GraphicsObject):
|
||||||
# the current sample under the mouse
|
# the current sample under the mouse
|
||||||
cursor = LineDot(
|
cursor = LineDot(
|
||||||
curve,
|
curve,
|
||||||
index=plot._ohlc[-1]['index'],
|
index=plot._arrays['ohlc'][-1]['index'],
|
||||||
plot=plot
|
plot=plot
|
||||||
)
|
)
|
||||||
plot.addItem(cursor)
|
plot.addItem(cursor)
|
||||||
|
@ -344,7 +443,7 @@ class Cursor(pg.GraphicsObject):
|
||||||
|
|
||||||
def mouseMoved(
|
def mouseMoved(
|
||||||
self,
|
self,
|
||||||
evt: 'Tuple[QMouseEvent]', # noqa
|
evt: 'tuple[QMouseEvent]', # noqa
|
||||||
) -> None: # noqa
|
) -> None: # noqa
|
||||||
"""Update horizonal and vertical lines when mouse moves inside
|
"""Update horizonal and vertical lines when mouse moves inside
|
||||||
either the main chart or any indicator subplot.
|
either the main chart or any indicator subplot.
|
||||||
|
@ -392,10 +491,16 @@ class Cursor(pg.GraphicsObject):
|
||||||
item.on_tracked_source(ix, iy)
|
item.on_tracked_source(ix, iy)
|
||||||
|
|
||||||
if ix != last_ix:
|
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():
|
for plot, opts in self.graphics.items():
|
||||||
|
|
||||||
# update the chart's "contents" label
|
# 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"
|
# move the vertical line to the current "center of bar"
|
||||||
opts['vl'].setX(ix + line_offset)
|
opts['vl'].setX(ix + line_offset)
|
||||||
|
|
Loading…
Reference in New Issue