Add user defined anchor support to label; reorg mod
							parent
							
								
									2d787901f9
								
							
						
					
					
						commit
						2fd7ea812a
					
				| 
						 | 
				
			
			@ -32,7 +32,6 @@ import trio
 | 
			
		|||
from ..log import get_logger
 | 
			
		||||
from ._style import _min_points_to_show
 | 
			
		||||
from ._editors import SelectRect
 | 
			
		||||
from ._window import main_window
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
log = get_logger(__name__)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -19,7 +19,7 @@ Non-shitty labels that don't re-invent the wheel.
 | 
			
		|||
 | 
			
		||||
"""
 | 
			
		||||
from inspect import isfunction
 | 
			
		||||
from typing import Callable
 | 
			
		||||
from typing import Callable, Optional
 | 
			
		||||
 | 
			
		||||
import pyqtgraph as pg
 | 
			
		||||
from PyQt5 import QtGui, QtWidgets
 | 
			
		||||
| 
						 | 
				
			
			@ -31,6 +31,200 @@ from ._style import (
 | 
			
		|||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Label:
 | 
			
		||||
    """
 | 
			
		||||
    A plain ol' "scene label" using an underlying ``QGraphicsTextItem``.
 | 
			
		||||
 | 
			
		||||
    After hacking for many days on multiple "label" systems inside
 | 
			
		||||
    ``pyqtgraph`` yet again we're left writing our own since it seems
 | 
			
		||||
    all of those are over complicated, ad-hoc, transform-mangling,
 | 
			
		||||
    messes which can't accomplish the simplest things via their inputs
 | 
			
		||||
    (such as pinning to the left hand side of a view box).
 | 
			
		||||
 | 
			
		||||
    Here we do the simple thing where the label uses callables to figure
 | 
			
		||||
    out the (x, y) coordinate "pin point": nice and simple.
 | 
			
		||||
 | 
			
		||||
    This type is another effort (see our graphics) to start making
 | 
			
		||||
    small, re-usable label components that can actually be used to build
 | 
			
		||||
    production grade UIs...
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
 | 
			
		||||
        self,
 | 
			
		||||
        view: pg.ViewBox,
 | 
			
		||||
        fmt_str: str,
 | 
			
		||||
 | 
			
		||||
        color: str = 'bracket',
 | 
			
		||||
        x_offset: float = 0,
 | 
			
		||||
        font_size: str = 'small',
 | 
			
		||||
        opacity: float = 0.666,
 | 
			
		||||
        fields: dict = {},
 | 
			
		||||
        update_on_range_change: bool = True,
 | 
			
		||||
 | 
			
		||||
    ) -> None:
 | 
			
		||||
 | 
			
		||||
        vb = self.vb = view
 | 
			
		||||
        self._fmt_str = fmt_str
 | 
			
		||||
        self._view_xy = QPointF(0, 0)
 | 
			
		||||
 | 
			
		||||
        self.scene_anchor: Optional[Callable[..., QPointF]] = None
 | 
			
		||||
 | 
			
		||||
        self._x_offset = x_offset
 | 
			
		||||
 | 
			
		||||
        txt = self.txt = QtWidgets.QGraphicsTextItem()
 | 
			
		||||
        vb.scene().addItem(txt)
 | 
			
		||||
 | 
			
		||||
        # configure font size based on DPI
 | 
			
		||||
        dpi_font = DpiAwareFont(
 | 
			
		||||
            font_size=font_size,
 | 
			
		||||
        )
 | 
			
		||||
        dpi_font.configure_to_dpi()
 | 
			
		||||
        txt.setFont(dpi_font.font)
 | 
			
		||||
 | 
			
		||||
        txt.setOpacity(opacity)
 | 
			
		||||
 | 
			
		||||
        # register viewbox callbacks
 | 
			
		||||
        if update_on_range_change:
 | 
			
		||||
            vb.sigRangeChanged.connect(self.on_sigrange_change)
 | 
			
		||||
 | 
			
		||||
        self._hcolor: str = ''
 | 
			
		||||
        self.color = color
 | 
			
		||||
 | 
			
		||||
        self.fields = fields
 | 
			
		||||
        self.orient_v = 'bottom'
 | 
			
		||||
 | 
			
		||||
        self._anchor_func = self.txt.pos().x
 | 
			
		||||
 | 
			
		||||
        # not sure if this makes a diff
 | 
			
		||||
        self.txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
 | 
			
		||||
 | 
			
		||||
        # TODO: edit and selection support
 | 
			
		||||
        # https://doc.qt.io/qt-5/qt.html#TextInteractionFlag-enum
 | 
			
		||||
        # self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def color(self):
 | 
			
		||||
        return self._hcolor
 | 
			
		||||
 | 
			
		||||
    @color.setter
 | 
			
		||||
    def color(self, color: str) -> None:
 | 
			
		||||
        self.txt.setDefaultTextColor(pg.mkColor(hcolor(color)))
 | 
			
		||||
        self._hcolor = color
 | 
			
		||||
 | 
			
		||||
    def update(self) -> None:
 | 
			
		||||
        '''Update this label either by invoking its
 | 
			
		||||
        user defined anchoring function, or by positioning
 | 
			
		||||
        to the last recorded data view coordinates.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        # move label in scene coords to desired position
 | 
			
		||||
        anchor = self.scene_anchor
 | 
			
		||||
        if anchor:
 | 
			
		||||
            self.txt.setPos(anchor())
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            # position based on last computed view coordinate
 | 
			
		||||
            self.set_view_pos(self._view_xy.y())
 | 
			
		||||
 | 
			
		||||
    def on_sigrange_change(self, vr, r) -> None:
 | 
			
		||||
        return self.update()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def w(self) -> float:
 | 
			
		||||
        return self.txt.boundingRect().width()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def h(self) -> float:
 | 
			
		||||
        return self.txt.boundingRect().height()
 | 
			
		||||
 | 
			
		||||
    def vbr(self) -> QRectF:
 | 
			
		||||
        return self.vb.boundingRect()
 | 
			
		||||
 | 
			
		||||
    def set_x_anchor_func(
 | 
			
		||||
        self,
 | 
			
		||||
        func: Callable,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        assert isinstance(func(), float)
 | 
			
		||||
        self._anchor_func = func
 | 
			
		||||
 | 
			
		||||
    def set_view_pos(
 | 
			
		||||
        self,
 | 
			
		||||
 | 
			
		||||
        y: float,
 | 
			
		||||
        x: Optional[float] = None,
 | 
			
		||||
 | 
			
		||||
    ) -> None:
 | 
			
		||||
 | 
			
		||||
        if x is None:
 | 
			
		||||
            scene_x = self._anchor_func() or self.txt.pos().x()
 | 
			
		||||
            x = self.vb.mapToView(QPointF(scene_x, scene_x)).x()
 | 
			
		||||
 | 
			
		||||
        # get new (inside the) view coordinates / position
 | 
			
		||||
        self._view_xy = QPointF(x, y)
 | 
			
		||||
 | 
			
		||||
        # map back to the outer UI-land "scene" coordinates
 | 
			
		||||
        s_xy = self.vb.mapFromView(self._view_xy)
 | 
			
		||||
 | 
			
		||||
        if self.orient_v == 'top':
 | 
			
		||||
            s_xy = QPointF(s_xy.x(), s_xy.y() - self.h)
 | 
			
		||||
 | 
			
		||||
        # move label in scene coords to desired position
 | 
			
		||||
        self.txt.setPos(s_xy)
 | 
			
		||||
 | 
			
		||||
        assert s_xy == self.txt.pos()
 | 
			
		||||
 | 
			
		||||
    # def orient_on(self, h: str, v: str) -> None:
 | 
			
		||||
    #     pass
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def fmt_str(self) -> str:
 | 
			
		||||
        return self._fmt_str
 | 
			
		||||
 | 
			
		||||
    @fmt_str.setter
 | 
			
		||||
    def fmt_str(self, fmt_str: str) -> None:
 | 
			
		||||
        self._fmt_str = fmt_str
 | 
			
		||||
 | 
			
		||||
    def format(
 | 
			
		||||
        self,
 | 
			
		||||
        **fields: dict
 | 
			
		||||
 | 
			
		||||
    ) -> str:
 | 
			
		||||
 | 
			
		||||
        out = {}
 | 
			
		||||
 | 
			
		||||
        # this is hacky support for single depth
 | 
			
		||||
        # calcs of field data from field data
 | 
			
		||||
        # ex. to calculate a $value = price * size
 | 
			
		||||
        for k, v in fields.items():
 | 
			
		||||
 | 
			
		||||
            if isfunction(v):
 | 
			
		||||
                out[k] = v(fields)
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                out[k] = v
 | 
			
		||||
 | 
			
		||||
        text = self._fmt_str.format(**out)
 | 
			
		||||
 | 
			
		||||
        # for large numbers with a thousands place
 | 
			
		||||
        text = text.replace(',', ' ')
 | 
			
		||||
 | 
			
		||||
        self.txt.setPlainText(text)
 | 
			
		||||
 | 
			
		||||
    def render(self) -> None:
 | 
			
		||||
        self.format(**self.fields)
 | 
			
		||||
 | 
			
		||||
    def show(self) -> None:
 | 
			
		||||
        self.txt.show()
 | 
			
		||||
 | 
			
		||||
    def hide(self) -> None:
 | 
			
		||||
        self.txt.hide()
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        self.vb.scene().removeItem(self.txt)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def vbr_left(label) -> Callable[..., float]:
 | 
			
		||||
    """Return a closure which gives the scene x-coordinate for the
 | 
			
		||||
    leftmost point of the containing view box.
 | 
			
		||||
| 
						 | 
				
			
			@ -85,170 +279,3 @@ def right_axis(
 | 
			
		|||
            return ryaxis.pos().x()  # + axis_offset - 2
 | 
			
		||||
 | 
			
		||||
        return on_axis
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class Label:
 | 
			
		||||
    """
 | 
			
		||||
    A plain ol' "scene label" using an underlying ``QGraphicsTextItem``.
 | 
			
		||||
 | 
			
		||||
    After hacking for many days on multiple "label" systems inside
 | 
			
		||||
    ``pyqtgraph`` yet again we're left writing our own since it seems
 | 
			
		||||
    all of those are over complicated, ad-hoc, transform-mangling,
 | 
			
		||||
    messes which can't accomplish the simplest things via their inputs
 | 
			
		||||
    (such as pinning to the left hand side of a view box).
 | 
			
		||||
 | 
			
		||||
    Here we do the simple thing where the label uses callables to figure
 | 
			
		||||
    out the (x, y) coordinate "pin point": nice and simple.
 | 
			
		||||
 | 
			
		||||
    This type is another effort (see our graphics) to start making
 | 
			
		||||
    small, re-usable label components that can actually be used to build
 | 
			
		||||
    production grade UIs...
 | 
			
		||||
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
 | 
			
		||||
        self,
 | 
			
		||||
        view: pg.ViewBox,
 | 
			
		||||
 | 
			
		||||
        fmt_str: str,
 | 
			
		||||
        color: str = 'bracket',
 | 
			
		||||
        x_offset: float = 0,
 | 
			
		||||
        font_size: str = 'small',
 | 
			
		||||
        opacity: float = 0.666,
 | 
			
		||||
        fields: dict = {}
 | 
			
		||||
 | 
			
		||||
    ) -> None:
 | 
			
		||||
 | 
			
		||||
        vb = self.vb = view
 | 
			
		||||
        self._fmt_str = fmt_str
 | 
			
		||||
        self._view_xy = QPointF(0, 0)
 | 
			
		||||
 | 
			
		||||
        self._x_offset = x_offset
 | 
			
		||||
 | 
			
		||||
        txt = self.txt = QtWidgets.QGraphicsTextItem()
 | 
			
		||||
        vb.scene().addItem(txt)
 | 
			
		||||
 | 
			
		||||
        # configure font size based on DPI
 | 
			
		||||
        dpi_font = DpiAwareFont(
 | 
			
		||||
            font_size=font_size,
 | 
			
		||||
        )
 | 
			
		||||
        dpi_font.configure_to_dpi()
 | 
			
		||||
        txt.setFont(dpi_font.font)
 | 
			
		||||
 | 
			
		||||
        txt.setOpacity(opacity)
 | 
			
		||||
 | 
			
		||||
        # register viewbox callbacks
 | 
			
		||||
        vb.sigRangeChanged.connect(self.on_sigrange_change)
 | 
			
		||||
 | 
			
		||||
        self._hcolor: str = ''
 | 
			
		||||
        self.color = color
 | 
			
		||||
 | 
			
		||||
        self.fields = fields
 | 
			
		||||
        self.orient_v = 'bottom'
 | 
			
		||||
 | 
			
		||||
        self._anchor_func = self.txt.pos().x
 | 
			
		||||
 | 
			
		||||
        # not sure if this makes a diff
 | 
			
		||||
        self.txt.setCacheMode(QtWidgets.QGraphicsItem.DeviceCoordinateCache)
 | 
			
		||||
 | 
			
		||||
        # TODO: edit and selection support
 | 
			
		||||
        # https://doc.qt.io/qt-5/qt.html#TextInteractionFlag-enum
 | 
			
		||||
        # self.setTextInteractionFlags(QtGui.Qt.TextEditorInteraction)
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def color(self):
 | 
			
		||||
        return self._hcolor
 | 
			
		||||
 | 
			
		||||
    @color.setter
 | 
			
		||||
    def color(self, color: str) -> None:
 | 
			
		||||
        self.txt.setDefaultTextColor(pg.mkColor(hcolor(color)))
 | 
			
		||||
        self._hcolor = color
 | 
			
		||||
 | 
			
		||||
    def on_sigrange_change(self, vr, r) -> None:
 | 
			
		||||
        self.set_view_y(self._view_xy.y())
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def w(self) -> float:
 | 
			
		||||
        return self.txt.boundingRect().width()
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def h(self) -> float:
 | 
			
		||||
        return self.txt.boundingRect().height()
 | 
			
		||||
 | 
			
		||||
    def vbr(self) -> QRectF:
 | 
			
		||||
        return self.vb.boundingRect()
 | 
			
		||||
 | 
			
		||||
    def set_x_anchor_func(
 | 
			
		||||
        self,
 | 
			
		||||
        func: Callable,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        assert isinstance(func(), float)
 | 
			
		||||
        self._anchor_func = func
 | 
			
		||||
 | 
			
		||||
    def set_view_y(
 | 
			
		||||
        self,
 | 
			
		||||
        y: float,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
 | 
			
		||||
        scene_x = self._anchor_func() or self.txt.pos().x()
 | 
			
		||||
 | 
			
		||||
        # get new (inside the) view coordinates / position
 | 
			
		||||
        self._view_xy = QPointF(
 | 
			
		||||
            self.vb.mapToView(QPointF(scene_x, scene_x)).x(),
 | 
			
		||||
            y,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # map back to the outer UI-land "scene" coordinates
 | 
			
		||||
        s_xy = self.vb.mapFromView(self._view_xy)
 | 
			
		||||
 | 
			
		||||
        if self.orient_v == 'top':
 | 
			
		||||
            s_xy = QPointF(s_xy.x(), s_xy.y() - self.h)
 | 
			
		||||
 | 
			
		||||
        # move label in scene coords to desired position
 | 
			
		||||
        self.txt.setPos(s_xy)
 | 
			
		||||
 | 
			
		||||
        assert s_xy == self.txt.pos()
 | 
			
		||||
 | 
			
		||||
    def orient_on(self, h: str, v: str) -> None:
 | 
			
		||||
        pass
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def fmt_str(self) -> str:
 | 
			
		||||
        return self._fmt_str
 | 
			
		||||
 | 
			
		||||
    @fmt_str.setter
 | 
			
		||||
    def fmt_str(self, fmt_str: str) -> None:
 | 
			
		||||
        self._fmt_str = fmt_str
 | 
			
		||||
 | 
			
		||||
    def format(self, **fields: dict) -> str:
 | 
			
		||||
 | 
			
		||||
        out = {}
 | 
			
		||||
 | 
			
		||||
        # this is hacky support for single depth
 | 
			
		||||
        # calcs of field data from field data
 | 
			
		||||
        # ex. to calculate a $value = price * size
 | 
			
		||||
        for k, v in fields.items():
 | 
			
		||||
            if isfunction(v):
 | 
			
		||||
                out[k] = v(fields)
 | 
			
		||||
            else:
 | 
			
		||||
                out[k] = v
 | 
			
		||||
 | 
			
		||||
        text = self._fmt_str.format(**out)
 | 
			
		||||
 | 
			
		||||
        # for large numbers with a thousands place
 | 
			
		||||
        text = text.replace(',', ' ')
 | 
			
		||||
 | 
			
		||||
        self.txt.setPlainText(text)
 | 
			
		||||
 | 
			
		||||
    def render(self) -> None:
 | 
			
		||||
        self.format(**self.fields)
 | 
			
		||||
 | 
			
		||||
    def show(self) -> None:
 | 
			
		||||
        self.txt.show()
 | 
			
		||||
 | 
			
		||||
    def hide(self) -> None:
 | 
			
		||||
        self.txt.hide()
 | 
			
		||||
 | 
			
		||||
    def delete(self) -> None:
 | 
			
		||||
        self.vb.scene().removeItem(self.txt)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue