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`!'
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        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
 | 
			
		||||
        r = QtCore.QRectF(start_pos, end_pos)
 | 
			
		||||
            view_rect = QtCore.QRectF(
 | 
			
		||||
                start_pos,
 | 
			
		||||
                end_pos,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # old way; don't need right?
 | 
			
		||||
        # lr = QtCore.QRectF(p1, p2)
 | 
			
		||||
        # r = self.vb.childGroup.mapRectFromParent(lr)
 | 
			
		||||
        self.setPos(view_rect.topLeft())
 | 
			
		||||
 | 
			
		||||
        self.setPos(r.topLeft())
 | 
			
		||||
        self.resetTransform()
 | 
			
		||||
        self.setRect(r)
 | 
			
		||||
        # 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