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 __future__ import annotations
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING
|
Sequence,
|
||||||
|
TYPE_CHECKING,
|
||||||
)
|
)
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
@ -31,24 +32,36 @@ from pyqtgraph import (
|
||||||
QtCore,
|
QtCore,
|
||||||
QtWidgets,
|
QtWidgets,
|
||||||
)
|
)
|
||||||
|
from PyQt5.QtCore import (
|
||||||
|
QPointF,
|
||||||
|
QRectF,
|
||||||
|
)
|
||||||
from PyQt5.QtGui import (
|
from PyQt5.QtGui import (
|
||||||
QColor,
|
QColor,
|
||||||
|
QTransform,
|
||||||
)
|
)
|
||||||
from PyQt5.QtWidgets import (
|
from PyQt5.QtWidgets import (
|
||||||
|
QGraphicsProxyWidget,
|
||||||
|
QGraphicsScene,
|
||||||
QLabel,
|
QLabel,
|
||||||
)
|
)
|
||||||
|
|
||||||
from pyqtgraph import functions as fn
|
from pyqtgraph import functions as fn
|
||||||
from PyQt5.QtCore import QPointF
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
from piker.types import Struct
|
from piker.types import Struct
|
||||||
from ._style import hcolor, _font
|
from ._style import (
|
||||||
|
hcolor,
|
||||||
|
_font,
|
||||||
|
)
|
||||||
from ._lines import LevelLine
|
from ._lines import LevelLine
|
||||||
from ..log import get_logger
|
from ..log import get_logger
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from ._chart import GodWidget
|
from ._chart import (
|
||||||
|
GodWidget,
|
||||||
|
ChartPlotWidget,
|
||||||
|
)
|
||||||
from ._interaction import ChartView
|
from ._interaction import ChartView
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,7 +79,7 @@ class ArrowEditor(Struct):
|
||||||
uid: str,
|
uid: str,
|
||||||
x: float,
|
x: float,
|
||||||
y: float,
|
y: float,
|
||||||
color='default',
|
color: str = 'default',
|
||||||
pointing: str | None = None,
|
pointing: str | None = None,
|
||||||
|
|
||||||
) -> pg.ArrowItem:
|
) -> pg.ArrowItem:
|
||||||
|
@ -252,27 +265,56 @@ class LineEditor(Struct):
|
||||||
return lines
|
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__(
|
def __init__(
|
||||||
self,
|
self,
|
||||||
viewbox: ViewBox,
|
viewbox: ViewBox,
|
||||||
color: str = 'dad_blue',
|
color: str | None = None,
|
||||||
) -> None:
|
) -> None:
|
||||||
super().__init__(0, 0, 1, 1)
|
super().__init__(0, 0, 1, 1)
|
||||||
|
|
||||||
# self.rbScaleBox = QGraphicsRectItem(0, 0, 1, 1)
|
# self.rbScaleBox = QGraphicsRectItem(0, 0, 1, 1)
|
||||||
self.vb: ViewBox = viewbox
|
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))
|
color = QColor(hcolor(color))
|
||||||
|
|
||||||
self.setPen(fn.mkPen(color, width=1))
|
self.setPen(fn.mkPen(color, width=1))
|
||||||
color.setAlpha(66)
|
color.setAlpha(66)
|
||||||
self.setBrush(fn.mkBrush(color))
|
self.setBrush(fn.mkBrush(color))
|
||||||
self.setZValue(1e9)
|
self.setZValue(1e9)
|
||||||
self.hide()
|
self.hide()
|
||||||
self._label = None
|
|
||||||
|
|
||||||
label = self._label = QLabel()
|
label = self._label = QLabel()
|
||||||
label.setTextFormat(0) # markdown
|
label.setTextFormat(0) # markdown
|
||||||
|
@ -282,13 +324,15 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
QtCore.Qt.AlignLeft
|
QtCore.Qt.AlignLeft
|
||||||
# | QtCore.Qt.AlignVCenter
|
# | QtCore.Qt.AlignVCenter
|
||||||
)
|
)
|
||||||
|
label.hide() # always right after init
|
||||||
|
|
||||||
# proxy is created after containing scene is initialized
|
# proxy is created after containing scene is initialized
|
||||||
self._label_proxy = None
|
self._label_proxy: QGraphicsProxyWidget | None = None
|
||||||
self._abs_top_right = None
|
self._abs_top_right: Point | None = None
|
||||||
|
|
||||||
# TODO: "swing %" might be handy here (data's max/min # % change)
|
# TODO: "swing %" might be handy here (data's max/min
|
||||||
self._contents = [
|
# # % change)?
|
||||||
|
self._contents: list[str] = [
|
||||||
'change: {pchng:.2f} %',
|
'change: {pchng:.2f} %',
|
||||||
'range: {rng:.2f}',
|
'range: {rng:.2f}',
|
||||||
'bars: {nbars}',
|
'bars: {nbars}',
|
||||||
|
@ -298,25 +342,30 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
'sigma: {std:.2f}',
|
'sigma: {std:.2f}',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
self.add_to_view(viewbox)
|
||||||
|
|
||||||
def add_to_view(
|
def add_to_view(
|
||||||
self,
|
self,
|
||||||
view: ChartView,
|
view: ChartView,
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
Self-defined view hookup impl.
|
Self-defined view hookup impl which will
|
||||||
|
also re-assign the internal ref.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
view.addItem(
|
view.addItem(
|
||||||
self,
|
self,
|
||||||
ignoreBounds=True,
|
ignoreBounds=True,
|
||||||
)
|
)
|
||||||
|
if self.vb is not view:
|
||||||
|
self.vb = view
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def chart(self) -> 'ChartPlotWidget': # noqa
|
def chart(self) -> ChartPlotWidget: # noqa
|
||||||
return self._chart
|
return self._chart
|
||||||
|
|
||||||
@chart.setter
|
@chart.setter
|
||||||
def chart(self, chart: 'ChartPlotWidget') -> None: # noqa
|
def chart(self, chart: ChartPlotWidget) -> None: # noqa
|
||||||
self._chart = chart
|
self._chart = chart
|
||||||
chart.sigRangeChanged.connect(self.update_on_resize)
|
chart.sigRangeChanged.connect(self.update_on_resize)
|
||||||
palette = self._label.palette()
|
palette = self._label.palette()
|
||||||
|
@ -340,8 +389,10 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
|
|
||||||
def set_scen_pos(
|
def set_scen_pos(
|
||||||
self,
|
self,
|
||||||
p1: QPointF,
|
scen_p1: QPointF,
|
||||||
p2: QPointF
|
scen_p2: QPointF,
|
||||||
|
|
||||||
|
update_label: bool = True,
|
||||||
|
|
||||||
) -> None:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
@ -350,16 +401,50 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
match.
|
match.
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# map to view coords
|
# NOTE XXX: apparently just setting it doesn't work!?
|
||||||
self.set_view_pos(
|
# i have no idea why but it's pretty weird we have to do
|
||||||
self.vb.mapToView(p1),
|
# this transform thing which was basically pulled verbatim
|
||||||
self.vb.mapToView(p2),
|
# 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(
|
def set_view_pos(
|
||||||
self,
|
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:
|
) -> None:
|
||||||
'''
|
'''
|
||||||
|
@ -368,27 +453,80 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
moved to match).
|
moved to match).
|
||||||
|
|
||||||
'''
|
'''
|
||||||
# https://doc.qt.io/qt-5/qgraphicsproxywidget.html
|
if self._chart is None:
|
||||||
if self._label_proxy is None:
|
raise RuntimeError(
|
||||||
self._label_proxy = self.vb.scene(
|
'You MUST assign a `SelectRect.chart: ChartPlotWidget`!'
|
||||||
).addWidget(self._label)
|
)
|
||||||
|
|
||||||
|
if view_rect is None:
|
||||||
|
# ensure point casting
|
||||||
|
start_pos: QPointF = as_point(start_pos)
|
||||||
|
end_pos: QPointF = as_point(end_pos)
|
||||||
|
|
||||||
# map to view coords and update area
|
# map to view coords and update area
|
||||||
r = QtCore.QRectF(start_pos, end_pos)
|
view_rect = QtCore.QRectF(
|
||||||
|
start_pos,
|
||||||
|
end_pos,
|
||||||
|
)
|
||||||
|
|
||||||
# old way; don't need right?
|
self.setPos(view_rect.topLeft())
|
||||||
# lr = QtCore.QRectF(p1, p2)
|
|
||||||
# r = self.vb.childGroup.mapRectFromParent(lr)
|
|
||||||
|
|
||||||
self.setPos(r.topLeft())
|
# NOTE: SERIOUSLY NO IDEA WHY THIS WORKS...
|
||||||
self.resetTransform()
|
# but it does and all the other commented stuff above
|
||||||
self.setRect(r)
|
# 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()
|
self.show()
|
||||||
|
|
||||||
y1, y2 = start_pos.y(), end_pos.y()
|
def init_label(
|
||||||
x1, x2 = start_pos.x(), end_pos.x()
|
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)
|
_, xmn = min(y1, y2), min(x1, x2)
|
||||||
ymx, xmx = max(y1, y2), max(x1, x2)
|
ymx, xmx = max(y1, y2), max(x1, x2)
|
||||||
|
|
||||||
|
@ -398,26 +536,35 @@ class SelectRect(QtWidgets.QGraphicsRectItem):
|
||||||
ixmn, ixmx = round(xmn), round(xmx)
|
ixmn, ixmx = round(xmn), round(xmx)
|
||||||
nbars = ixmx - ixmn + 1
|
nbars = ixmx - ixmn + 1
|
||||||
|
|
||||||
chart = self._chart
|
chart: ChartPlotWidget = self._chart
|
||||||
data = chart.get_viz(chart.name).shm.array[ixmn:ixmx]
|
data: np.ndarray = chart.get_viz(
|
||||||
|
chart.name
|
||||||
|
).shm.array[ixmn:ixmx]
|
||||||
|
|
||||||
if len(data):
|
if len(data):
|
||||||
std = data['close'].std()
|
std: float = data['close'].std()
|
||||||
dmx = data['high'].max()
|
dmx: float = data['high'].max()
|
||||||
dmn = data['low'].min()
|
dmn: float = data['low'].min()
|
||||||
else:
|
else:
|
||||||
dmn = dmx = std = np.nan
|
dmn = dmx = std = np.nan
|
||||||
|
|
||||||
# update label info
|
# update label info
|
||||||
self._label.setText('\n'.join(self._contents).format(
|
label.setText('\n'.join(self._contents).format(
|
||||||
pchng=pchng, rng=rng, nbars=nbars,
|
pchng=pchng,
|
||||||
std=std, dmx=dmx, dmn=dmn,
|
rng=rng,
|
||||||
|
nbars=nbars,
|
||||||
|
std=std,
|
||||||
|
dmx=dmx,
|
||||||
|
dmn=dmn,
|
||||||
))
|
))
|
||||||
|
|
||||||
# print(f'x2, y2: {(x2, y2)}')
|
# print(f'x2, y2: {(x2, y2)}')
|
||||||
# print(f'xmn, ymn: {(xmn, ymx)}')
|
# 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
|
# XXX: in the drag bottom-right -> top-left case we don't
|
||||||
# want the label to overlay the box.
|
# 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(x2, y2 + self._label.height())
|
||||||
# label_anchor = Point(xmn, ymn)
|
# label_anchor = Point(xmn, ymn)
|
||||||
|
|
||||||
self._abs_top_right = label_anchor
|
self._abs_top_right: Point = label_anchor
|
||||||
self._label_proxy.setPos(self.vb.mapFromView(label_anchor))
|
self._label_proxy.setPos(
|
||||||
# self._label.show()
|
cv.mapFromView(label_anchor)
|
||||||
|
)
|
||||||
|
label.show()
|
||||||
|
|
||||||
def clear(self):
|
def clear(self):
|
||||||
'''
|
'''
|
||||||
|
|
Loading…
Reference in New Issue