Finally fix our `SelectRect` for use with cursor..
Turns out using the `.setRect()` method was the main cause of the issue (though still don't really understand how or why) and this instead adopts verbatim the code from `pg.ViewBox.updateScaleBox()` which uses a scaling transform to set the rect for the "zoom scale box" thingy. Further add a shite ton more improvements and interface tweaks in support of the new remote-annotation control msging subsys: - re-impl `.set_scen_pos()` to expect `QGraphicsScene` coordinates (i.e. passed from the interaction loop and pass scene `QPointF`s from `ViewBox.mouseDragEvent()` using the `MouseDragEvent.scenePos()` and friends; this is required to properly use the transform setting approach to resize the select-rect as mentioned above. - add `as_point()` converter to maybe-cast python `tuple[float, float]` inputs (prolly from IPC msgs) to equivalent `QPointF`s. - add a ton more detailed Qt-obj-related typing throughout our deriv. - call `.add_to_view()` from init so that wtv view is passed in during instantiation is always set as the `.vb` after creation. - factor the (proxy widget) label creation into a new `.init_label()` so that both the `set_scen/view_pos()` methods can call it and just generally decouple rect-pos mods from label content mods.distribute_dis
parent
31fa0b02f5
commit
69368f20c2
|
@ -21,7 +21,8 @@ Higher level annotation editors.
|
|||
from __future__ import annotations
|
||||
from collections import defaultdict
|
||||
from typing import (
|
||||
TYPE_CHECKING
|
||||
Sequence,
|
||||
TYPE_CHECKING,
|
||||
)
|
||||
|
||||
import pyqtgraph as pg
|
||||
|
@ -31,24 +32,36 @@ from pyqtgraph import (
|
|||
QtCore,
|
||||
QtWidgets,
|
||||
)
|
||||
from PyQt5.QtCore import (
|
||||
QPointF,
|
||||
QRectF,
|
||||
)
|
||||
from PyQt5.QtGui import (
|
||||
QColor,
|
||||
QTransform,
|
||||
)
|
||||
from PyQt5.QtWidgets import (
|
||||
QGraphicsProxyWidget,
|
||||
QGraphicsScene,
|
||||
QLabel,
|
||||
)
|
||||
|
||||
from pyqtgraph import functions as fn
|
||||
from PyQt5.QtCore import QPointF
|
||||
import numpy as np
|
||||
|
||||
from piker.types import Struct
|
||||
from ._style import hcolor, _font
|
||||
from ._style import (
|
||||
hcolor,
|
||||
_font,
|
||||
)
|
||||
from ._lines import LevelLine
|
||||
from ..log import get_logger
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from ._chart import GodWidget
|
||||
from ._chart import (
|
||||
GodWidget,
|
||||
ChartPlotWidget,
|
||||
)
|
||||
from ._interaction import ChartView
|
||||
|
||||
|
||||
|
@ -66,7 +79,7 @@ class ArrowEditor(Struct):
|
|||
uid: str,
|
||||
x: float,
|
||||
y: float,
|
||||
color='default',
|
||||
color: str = 'default',
|
||||
pointing: str | None = None,
|
||||
|
||||
) -> pg.ArrowItem:
|
||||
|
@ -252,27 +265,56 @@ class LineEditor(Struct):
|
|||
return lines
|
||||
|
||||
|
||||
class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||
def as_point(
|
||||
pair: Sequence[float, float] | QPointF,
|
||||
) -> list[QPointF, QPointF]:
|
||||
'''
|
||||
Case any input tuple of floats to a a list of `QPoint` objects
|
||||
for use in Qt geometry routines.
|
||||
|
||||
'''
|
||||
if isinstance(pair, QPointF):
|
||||
return pair
|
||||
|
||||
return QPointF(pair[0], pair[1])
|
||||
|
||||
|
||||
# TODO: maybe implement better, something something RectItemProxy??
|
||||
# -[ ] dig into details of how proxy's work?
|
||||
# https://doc.qt.io/qt-5/qgraphicsscene.html#addWidget
|
||||
# -[ ] consider using `.addRect()` maybe?
|
||||
|
||||
class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||
'''
|
||||
A data-view "selection rectangle": the most fundamental
|
||||
geometry for annotating data views.
|
||||
|
||||
- https://doc.qt.io/qt-5/qgraphicsrectitem.html
|
||||
- https://doc.qt.io/qt-6/qgraphicsrectitem.html
|
||||
|
||||
'''
|
||||
def __init__(
|
||||
self,
|
||||
viewbox: ViewBox,
|
||||
color: str = 'dad_blue',
|
||||
color: str | None = None,
|
||||
) -> None:
|
||||
super().__init__(0, 0, 1, 1)
|
||||
|
||||
# self.rbScaleBox = QGraphicsRectItem(0, 0, 1, 1)
|
||||
self.vb: ViewBox = viewbox
|
||||
self._chart: 'ChartPlotWidget' = None # noqa
|
||||
|
||||
# override selection box color
|
||||
self._chart: ChartPlotWidget | None = None # noqa
|
||||
|
||||
# TODO: maybe allow this to be dynamic via a method?
|
||||
#l override selection box color
|
||||
color: str = color or 'dad_blue'
|
||||
color = QColor(hcolor(color))
|
||||
|
||||
self.setPen(fn.mkPen(color, width=1))
|
||||
color.setAlpha(66)
|
||||
self.setBrush(fn.mkBrush(color))
|
||||
self.setZValue(1e9)
|
||||
self.hide()
|
||||
self._label = None
|
||||
|
||||
label = self._label = QLabel()
|
||||
label.setTextFormat(0) # markdown
|
||||
|
@ -282,13 +324,15 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
QtCore.Qt.AlignLeft
|
||||
# | QtCore.Qt.AlignVCenter
|
||||
)
|
||||
label.hide() # always right after init
|
||||
|
||||
# proxy is created after containing scene is initialized
|
||||
self._label_proxy = None
|
||||
self._abs_top_right = None
|
||||
self._label_proxy: QGraphicsProxyWidget | None = None
|
||||
self._abs_top_right: Point | None = None
|
||||
|
||||
# TODO: "swing %" might be handy here (data's max/min # % change)
|
||||
self._contents = [
|
||||
# TODO: "swing %" might be handy here (data's max/min
|
||||
# # % change)?
|
||||
self._contents: list[str] = [
|
||||
'change: {pchng:.2f} %',
|
||||
'range: {rng:.2f}',
|
||||
'bars: {nbars}',
|
||||
|
@ -298,25 +342,30 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
'sigma: {std:.2f}',
|
||||
]
|
||||
|
||||
self.add_to_view(viewbox)
|
||||
|
||||
def add_to_view(
|
||||
self,
|
||||
view: ChartView,
|
||||
) -> None:
|
||||
'''
|
||||
Self-defined view hookup impl.
|
||||
Self-defined view hookup impl which will
|
||||
also re-assign the internal ref.
|
||||
|
||||
'''
|
||||
view.addItem(
|
||||
self,
|
||||
ignoreBounds=True,
|
||||
)
|
||||
if self.vb is not view:
|
||||
self.vb = view
|
||||
|
||||
@property
|
||||
def chart(self) -> 'ChartPlotWidget': # noqa
|
||||
def chart(self) -> ChartPlotWidget: # noqa
|
||||
return self._chart
|
||||
|
||||
@chart.setter
|
||||
def chart(self, chart: 'ChartPlotWidget') -> None: # noqa
|
||||
def chart(self, chart: ChartPlotWidget) -> None: # noqa
|
||||
self._chart = chart
|
||||
chart.sigRangeChanged.connect(self.update_on_resize)
|
||||
palette = self._label.palette()
|
||||
|
@ -340,8 +389,10 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
|
||||
def set_scen_pos(
|
||||
self,
|
||||
p1: QPointF,
|
||||
p2: QPointF
|
||||
scen_p1: QPointF,
|
||||
scen_p2: QPointF,
|
||||
|
||||
update_label: bool = True,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -350,16 +401,50 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
match.
|
||||
|
||||
'''
|
||||
# map to view coords
|
||||
self.set_view_pos(
|
||||
self.vb.mapToView(p1),
|
||||
self.vb.mapToView(p2),
|
||||
# NOTE XXX: apparently just setting it doesn't work!?
|
||||
# i have no idea why but it's pretty weird we have to do
|
||||
# this transform thing which was basically pulled verbatim
|
||||
# from the `pg.ViewBox.updateScaleBox()` method.
|
||||
view_rect: QRectF = self.vb.childGroup.mapRectFromScene(
|
||||
QRectF(
|
||||
scen_p1,
|
||||
scen_p2,
|
||||
)
|
||||
)
|
||||
self.setPos(view_rect.topLeft())
|
||||
# XXX: does not work..!?!?
|
||||
# https://doc.qt.io/qt-5/qgraphicsrectitem.html#setRect
|
||||
# self.setRect(view_rect)
|
||||
|
||||
tr = QTransform.fromScale(
|
||||
view_rect.width(),
|
||||
view_rect.height(),
|
||||
)
|
||||
self.setTransform(tr)
|
||||
|
||||
# XXX: never got this working, was always offset
|
||||
# / transformed completely wrong (and off to the far right
|
||||
# from the cursor?)
|
||||
# self.set_view_pos(
|
||||
# view_rect=view_rect,
|
||||
# # self.vwqpToView(p1),
|
||||
# # self.vb.mapToView(p2),
|
||||
# # start_pos=self.vb.mapToScene(p1),
|
||||
# # end_pos=self.vb.mapToScene(p2),
|
||||
# )
|
||||
self.show()
|
||||
|
||||
if update_label:
|
||||
self.init_label(view_rect)
|
||||
|
||||
def set_view_pos(
|
||||
self,
|
||||
start_pos: QPointF,
|
||||
end_pos: QPointF,
|
||||
|
||||
start_pos: QPointF | Sequence[float, float] | None = None,
|
||||
end_pos: QPointF | Sequence[float, float] | None = None,
|
||||
view_rect: QRectF | None = None,
|
||||
|
||||
update_label: bool = True,
|
||||
|
||||
) -> None:
|
||||
'''
|
||||
|
@ -368,27 +453,80 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
moved to match).
|
||||
|
||||
'''
|
||||
# https://doc.qt.io/qt-5/qgraphicsproxywidget.html
|
||||
if self._label_proxy is None:
|
||||
self._label_proxy = self.vb.scene(
|
||||
).addWidget(self._label)
|
||||
if self._chart is None:
|
||||
raise RuntimeError(
|
||||
'You MUST assign a `SelectRect.chart: ChartPlotWidget`!'
|
||||
)
|
||||
|
||||
# map to view coords and update area
|
||||
r = QtCore.QRectF(start_pos, end_pos)
|
||||
if view_rect is None:
|
||||
# ensure point casting
|
||||
start_pos: QPointF = as_point(start_pos)
|
||||
end_pos: QPointF = as_point(end_pos)
|
||||
|
||||
# old way; don't need right?
|
||||
# lr = QtCore.QRectF(p1, p2)
|
||||
# r = self.vb.childGroup.mapRectFromParent(lr)
|
||||
# map to view coords and update area
|
||||
view_rect = QtCore.QRectF(
|
||||
start_pos,
|
||||
end_pos,
|
||||
)
|
||||
|
||||
self.setPos(r.topLeft())
|
||||
self.resetTransform()
|
||||
self.setRect(r)
|
||||
self.setPos(view_rect.topLeft())
|
||||
|
||||
# NOTE: SERIOUSLY NO IDEA WHY THIS WORKS...
|
||||
# but it does and all the other commented stuff above
|
||||
# dint, dawg..
|
||||
|
||||
# self.resetTransform()
|
||||
# self.setRect(view_rect)
|
||||
|
||||
tr = QTransform.fromScale(
|
||||
view_rect.width(),
|
||||
view_rect.height(),
|
||||
)
|
||||
self.setTransform(tr)
|
||||
|
||||
if update_label:
|
||||
self.init_label(view_rect)
|
||||
|
||||
print(
|
||||
'SelectRect modify:\n'
|
||||
f'QRectF: {view_rect}\n'
|
||||
f'start_pos: {start_pos}\n'
|
||||
f'end_pos: {end_pos}\n'
|
||||
)
|
||||
self.show()
|
||||
|
||||
y1, y2 = start_pos.y(), end_pos.y()
|
||||
x1, x2 = start_pos.x(), end_pos.x()
|
||||
def init_label(
|
||||
self,
|
||||
view_rect: QRectF,
|
||||
) -> QLabel:
|
||||
|
||||
# TODO: heh, could probably use a max-min streamin algo here too
|
||||
# should be init-ed in `.__init__()`
|
||||
label: QLabel = self._label
|
||||
cv: ChartView = self.vb
|
||||
|
||||
# https://doc.qt.io/qt-5/qgraphicsproxywidget.html
|
||||
if self._label_proxy is None:
|
||||
scen: QGraphicsScene = cv.scene()
|
||||
# NOTE: specifically this is passing a widget
|
||||
# pointer to the scene's `.addWidget()` as per,
|
||||
# https://doc.qt.io/qt-5/qgraphicsproxywidget.html#embedding-a-widget-with-qgraphicsproxywidget
|
||||
self._label_proxy: QGraphicsProxyWidget = scen.addWidget(label)
|
||||
|
||||
# get label startup coords
|
||||
tl: QPointF = view_rect.topLeft()
|
||||
br: QPointF = view_rect.bottomRight()
|
||||
|
||||
x1, y1 = tl.x(), tl.y()
|
||||
x2, y2 = br.x(), br.y()
|
||||
|
||||
# TODO: to remove, previous label corner point unpacking
|
||||
# x1, y1 = start_pos.x(), start_pos.y()
|
||||
# x2, y2 = end_pos.x(), end_pos.y()
|
||||
# y1, y2 = start_pos.y(), end_pos.y()
|
||||
# x1, x2 = start_pos.x(), end_pos.x()
|
||||
|
||||
# TODO: heh, could probably use a max-min streamin algo
|
||||
# here too?
|
||||
_, xmn = min(y1, y2), min(x1, x2)
|
||||
ymx, xmx = max(y1, y2), max(x1, x2)
|
||||
|
||||
|
@ -398,26 +536,35 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
ixmn, ixmx = round(xmn), round(xmx)
|
||||
nbars = ixmx - ixmn + 1
|
||||
|
||||
chart = self._chart
|
||||
data = chart.get_viz(chart.name).shm.array[ixmn:ixmx]
|
||||
chart: ChartPlotWidget = self._chart
|
||||
data: np.ndarray = chart.get_viz(
|
||||
chart.name
|
||||
).shm.array[ixmn:ixmx]
|
||||
|
||||
if len(data):
|
||||
std = data['close'].std()
|
||||
dmx = data['high'].max()
|
||||
dmn = data['low'].min()
|
||||
std: float = data['close'].std()
|
||||
dmx: float = data['high'].max()
|
||||
dmn: float = data['low'].min()
|
||||
else:
|
||||
dmn = dmx = std = np.nan
|
||||
|
||||
# update label info
|
||||
self._label.setText('\n'.join(self._contents).format(
|
||||
pchng=pchng, rng=rng, nbars=nbars,
|
||||
std=std, dmx=dmx, dmn=dmn,
|
||||
label.setText('\n'.join(self._contents).format(
|
||||
pchng=pchng,
|
||||
rng=rng,
|
||||
nbars=nbars,
|
||||
std=std,
|
||||
dmx=dmx,
|
||||
dmn=dmn,
|
||||
))
|
||||
|
||||
# print(f'x2, y2: {(x2, y2)}')
|
||||
# print(f'xmn, ymn: {(xmn, ymx)}')
|
||||
|
||||
label_anchor = Point(xmx + 2, ymx)
|
||||
label_anchor = Point(
|
||||
xmx + 2,
|
||||
ymx,
|
||||
)
|
||||
|
||||
# XXX: in the drag bottom-right -> top-left case we don't
|
||||
# want the label to overlay the box.
|
||||
|
@ -426,9 +573,11 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
|||
# # label_anchor = Point(x2, y2 + self._label.height())
|
||||
# label_anchor = Point(xmn, ymn)
|
||||
|
||||
self._abs_top_right = label_anchor
|
||||
self._label_proxy.setPos(self.vb.mapFromView(label_anchor))
|
||||
# self._label.show()
|
||||
self._abs_top_right: Point = label_anchor
|
||||
self._label_proxy.setPos(
|
||||
cv.mapFromView(label_anchor)
|
||||
)
|
||||
label.show()
|
||||
|
||||
def clear(self):
|
||||
'''
|
||||
|
|
Loading…
Reference in New Issue