Extract and fork `pyqtgraph` upstream submissions
Fork out our patch set submitted to upstream in multiple PRs (since they aren't moving and/or aren't a priority to core) which can be seen in full from the following diff: https://github.com/pyqtgraph/pyqtgraph/compare/master...pikers:pyqtgraph:graphics_pin Move these type extensions into the internal `.ui._pg_overrides` module. The changes are related to both `pyqtgraph.PlotItem` and `.AxisItem` and were driven for our need for multi-view overlays (overlaid charts with optionally synced axis and interaction controls) as documented in the PR to upstream: https://github.com/pyqtgraph/pyqtgraph/pull/2162 More specifically, - wrt to `AxisItem` we added lru caching of tick values as per: https://github.com/pyqtgraph/pyqtgraph/pull/2160. - wrt to `PlotItem` we adjusted some of the axis management code, namely adding a standalone `.removeAxis()` and modifying the `.setAxisItems()` logic to use it in: https://github.com/pyqtgraph/pyqtgraph/pull/2162 as well as some tweaks to `.updateGrid()` to loop through all possible axes when grid setting.pg_exts_fork
							parent
							
								
									d46945cb09
								
							
						
					
					
						commit
						b524ea5c22
					
				| 
						 | 
				
			
			@ -15,11 +15,16 @@
 | 
			
		|||
# along with this program.  If not, see <https://www.gnu.org/licenses/>.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
Customization of ``pyqtgraph`` core routines to speed up our use mostly
 | 
			
		||||
based on not requiring "scentific precision" for pixel perfect view
 | 
			
		||||
transforms.
 | 
			
		||||
Customization of ``pyqtgraph`` core routines and various types normally
 | 
			
		||||
for speedups.
 | 
			
		||||
 | 
			
		||||
Generally, our does not require "scentific precision" for pixel perfect
 | 
			
		||||
view transforms.
 | 
			
		||||
 | 
			
		||||
"""
 | 
			
		||||
import functools
 | 
			
		||||
from typing import Optional
 | 
			
		||||
 | 
			
		||||
import pyqtgraph as pg
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			@ -46,3 +51,268 @@ def _do_overrides() -> None:
 | 
			
		|||
    """
 | 
			
		||||
    # we don't care about potential fp issues inside Qt
 | 
			
		||||
    pg.functions.invertQTransform = invertQTransform
 | 
			
		||||
    pg.PlotItem = PlotItem
 | 
			
		||||
    pg.AxisItem = AxisItem
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# NOTE: the below customized type contains all our changes on a method
 | 
			
		||||
# by method basis as per the diff:
 | 
			
		||||
# https://github.com/pyqtgraph/pyqtgraph/commit/8e60bc14234b6bec1369ff4192dbfb82f8682920#diff-a2b5865955d2ba703dbc4c35ff01aa761aa28d2aeaac5e68d24e338bc82fb5b1R500
 | 
			
		||||
 | 
			
		||||
class PlotItem(pg.PlotItem):
 | 
			
		||||
    '''
 | 
			
		||||
    Overrides for the core plot object mostly pertaining to overlayed
 | 
			
		||||
    multi-view management as it relates to multi-axis managment.
 | 
			
		||||
 | 
			
		||||
    '''
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        parent=None,
 | 
			
		||||
        name=None,
 | 
			
		||||
        labels=None,
 | 
			
		||||
        title=None,
 | 
			
		||||
        viewBox=None,
 | 
			
		||||
        axisItems=None,
 | 
			
		||||
        default_axes=['left', 'bottom'],
 | 
			
		||||
        enableMenu=True,
 | 
			
		||||
        **kargs
 | 
			
		||||
    ):
 | 
			
		||||
        super().__init__(
 | 
			
		||||
            parent=parent,
 | 
			
		||||
            name=name,
 | 
			
		||||
            labels=labels,
 | 
			
		||||
            title=title,
 | 
			
		||||
            viewBox=viewBox,
 | 
			
		||||
            axisItems=axisItems,
 | 
			
		||||
            # default_axes=default_axes,
 | 
			
		||||
            enableMenu=enableMenu,
 | 
			
		||||
            kargs=kargs,
 | 
			
		||||
        )
 | 
			
		||||
        # self.setAxisItems(
 | 
			
		||||
        #     axisItems,
 | 
			
		||||
        #     default_axes=default_axes,
 | 
			
		||||
        # )
 | 
			
		||||
 | 
			
		||||
    # NOTE: this is an entirely new method not in upstream.
 | 
			
		||||
    def removeAxis(
 | 
			
		||||
        self,
 | 
			
		||||
        name: str,
 | 
			
		||||
        unlink: bool = True,
 | 
			
		||||
 | 
			
		||||
    ) -> Optional[pg.AxisItem]:
 | 
			
		||||
        """
 | 
			
		||||
        Remove an axis from the contained axis items
 | 
			
		||||
        by ```name: str```.
 | 
			
		||||
 | 
			
		||||
        This means the axis graphics object will be removed
 | 
			
		||||
        from the ``.layout: QGraphicsGridLayout`` as well as unlinked
 | 
			
		||||
        from the underlying associated ``ViewBox``.
 | 
			
		||||
 | 
			
		||||
        If the ``unlink: bool`` is set to ``False`` then the axis will
 | 
			
		||||
        stay linked to its view and will only be removed from the
 | 
			
		||||
        layoutonly be removed from the layout.
 | 
			
		||||
 | 
			
		||||
        If no axis with ``name: str`` is found then this is a noop.
 | 
			
		||||
 | 
			
		||||
        Return the axis instance that was removed.
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        entry = self.axes.pop(name, None)
 | 
			
		||||
 | 
			
		||||
        if not entry:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        axis = entry['item']
 | 
			
		||||
        self.layout.removeItem(axis)
 | 
			
		||||
        axis.scene().removeItem(axis)
 | 
			
		||||
        if unlink:
 | 
			
		||||
            axis.unlinkFromView()
 | 
			
		||||
 | 
			
		||||
        self.update()
 | 
			
		||||
 | 
			
		||||
        return axis
 | 
			
		||||
 | 
			
		||||
    # Why do we need to always have all axes created?
 | 
			
		||||
    #
 | 
			
		||||
    # I don't understand this at all.
 | 
			
		||||
    #
 | 
			
		||||
    # Everything seems to work if you just always apply the
 | 
			
		||||
    # set passed to this method **EXCEPT** for some super weird reason
 | 
			
		||||
    # the view box geometry still computes as though the space for the
 | 
			
		||||
    # `'bottom'` axis is always there **UNLESS** you always add that
 | 
			
		||||
    # axis but hide it?
 | 
			
		||||
    #
 | 
			
		||||
    # Why in tf would this be the case!?!?
 | 
			
		||||
    def setAxisItems(
 | 
			
		||||
        self,
 | 
			
		||||
        # XXX: yeah yeah, i know we can't use type annots like this yet.
 | 
			
		||||
        axisItems: Optional[dict[str, pg.AxisItem]] = None,
 | 
			
		||||
        add_to_layout: bool = True,
 | 
			
		||||
        default_axes: list[str] = ['left', 'bottom'],
 | 
			
		||||
    ):
 | 
			
		||||
        """
 | 
			
		||||
        Override axis item setting to only
 | 
			
		||||
 | 
			
		||||
        """
 | 
			
		||||
        axisItems = axisItems or {}
 | 
			
		||||
 | 
			
		||||
        # XXX: wth is is this even saying?!?
 | 
			
		||||
        # Array containing visible axis items
 | 
			
		||||
        # Also containing potentially hidden axes, but they are not
 | 
			
		||||
        # touched so it does not matter
 | 
			
		||||
        # visibleAxes = ['left', 'bottom']
 | 
			
		||||
        # Note that it does not matter that this adds
 | 
			
		||||
        # some values to visibleAxes a second time
 | 
			
		||||
 | 
			
		||||
        # XXX: uhhh wat^ ..?
 | 
			
		||||
 | 
			
		||||
        visibleAxes = list(default_axes) + list(axisItems.keys())
 | 
			
		||||
 | 
			
		||||
        # TODO: we should probably invert the loop here to not loop the
 | 
			
		||||
        # predefined "axis name set" and instead loop the `axisItems`
 | 
			
		||||
        # input and lookup indices from a predefined map.
 | 
			
		||||
        for name, pos in (
 | 
			
		||||
            ('top', (1, 1)),
 | 
			
		||||
            ('bottom', (3, 1)),
 | 
			
		||||
            ('left', (2, 0)),
 | 
			
		||||
            ('right', (2, 2))
 | 
			
		||||
        ):
 | 
			
		||||
            if (
 | 
			
		||||
                name in self.axes and
 | 
			
		||||
                name in axisItems
 | 
			
		||||
            ):
 | 
			
		||||
                # we already have an axis entry for this name
 | 
			
		||||
                # so remove the existing entry.
 | 
			
		||||
                self.removeAxis(name)
 | 
			
		||||
 | 
			
		||||
            # elif name not in axisItems:
 | 
			
		||||
            #     # this axis entry is not provided in this call
 | 
			
		||||
            #     # so remove any old/existing entry.
 | 
			
		||||
            #     self.removeAxis(name)
 | 
			
		||||
 | 
			
		||||
            # Create new axis
 | 
			
		||||
            if name in axisItems:
 | 
			
		||||
                axis = axisItems[name]
 | 
			
		||||
                if axis.scene() is not None:
 | 
			
		||||
                    if (
 | 
			
		||||
                        name not in self.axes
 | 
			
		||||
                        or axis != self.axes[name]["item"]
 | 
			
		||||
                    ):
 | 
			
		||||
                        raise RuntimeError(
 | 
			
		||||
                            "Can't add an axis to multiple plots. Shared axes"
 | 
			
		||||
                            " can be achieved with multiple AxisItem instances"
 | 
			
		||||
                            " and set[X/Y]Link.")
 | 
			
		||||
 | 
			
		||||
            else:
 | 
			
		||||
                # Set up new axis
 | 
			
		||||
 | 
			
		||||
                # XXX: ok but why do we want to add axes for all entries
 | 
			
		||||
                # if not desired by the user? The only reason I can see
 | 
			
		||||
                # adding this is without it there's some weird
 | 
			
		||||
                # ``ViewBox`` geometry bug.. where a gap for the
 | 
			
		||||
                # 'bottom' axis is somehow left in?
 | 
			
		||||
                axis = AxisItem(orientation=name, parent=self)
 | 
			
		||||
 | 
			
		||||
            axis.linkToView(self.vb)
 | 
			
		||||
 | 
			
		||||
            # XXX: shouldn't you already know the ``pos`` from the name?
 | 
			
		||||
            # Oh right instead of a global map that would let you
 | 
			
		||||
            # reasily look that up it's redefined over and over and over
 | 
			
		||||
            # again in methods..
 | 
			
		||||
            self.axes[name] = {'item': axis, 'pos': pos}
 | 
			
		||||
 | 
			
		||||
            # NOTE: in the overlay case the axis may be added to some
 | 
			
		||||
            # other layout and should not be added here.
 | 
			
		||||
            if add_to_layout:
 | 
			
		||||
                self.layout.addItem(axis, *pos)
 | 
			
		||||
 | 
			
		||||
            # place axis above images at z=0, items that want to draw
 | 
			
		||||
            # over the axes should be placed at z>=1:
 | 
			
		||||
            axis.setZValue(0.5)
 | 
			
		||||
            axis.setFlag(
 | 
			
		||||
                axis.GraphicsItemFlag.ItemNegativeZStacksBehindParent
 | 
			
		||||
            )
 | 
			
		||||
            if name in visibleAxes:
 | 
			
		||||
                self.showAxis(name, True)
 | 
			
		||||
            else:
 | 
			
		||||
                # why do we need to insert all axes to ``.axes`` and
 | 
			
		||||
                # only hide the ones the user doesn't specify? It all
 | 
			
		||||
                # seems to work fine without doing this except for this
 | 
			
		||||
                # weird gap for the 'bottom' axis that always shows up
 | 
			
		||||
                # in the view box geometry??
 | 
			
		||||
                self.hideAxis(name)
 | 
			
		||||
 | 
			
		||||
    def updateGrid(
 | 
			
		||||
        self,
 | 
			
		||||
        *args,
 | 
			
		||||
    ):
 | 
			
		||||
        alpha = self.ctrl.gridAlphaSlider.value()
 | 
			
		||||
        x = alpha if self.ctrl.xGridCheck.isChecked() else False
 | 
			
		||||
        y = alpha if self.ctrl.yGridCheck.isChecked() else False
 | 
			
		||||
        for name, dim in (
 | 
			
		||||
            ('top', x),
 | 
			
		||||
            ('bottom', x),
 | 
			
		||||
            ('left', y),
 | 
			
		||||
            ('right', y)
 | 
			
		||||
        ):
 | 
			
		||||
            if name in self.axes:
 | 
			
		||||
                self.getAxis(name).setGrid(dim)
 | 
			
		||||
        # self.getAxis('bottom').setGrid(x)
 | 
			
		||||
        # self.getAxis('left').setGrid(y)
 | 
			
		||||
        # self.getAxis('right').setGrid(y)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# NOTE: overrides to lru_cache the ``.tickStrings()`` output.
 | 
			
		||||
class AxisItem(pg.AxisItem):
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        orientation,
 | 
			
		||||
        pen=None,
 | 
			
		||||
        textPen=None,
 | 
			
		||||
        linkView=None,
 | 
			
		||||
        parent=None,
 | 
			
		||||
        maxTickLength=-5,
 | 
			
		||||
        showValues=True,
 | 
			
		||||
        text='',
 | 
			
		||||
        units='',
 | 
			
		||||
        unitPrefix='',
 | 
			
		||||
        lru_cache_tick_strings: bool = True,
 | 
			
		||||
        **args,
 | 
			
		||||
    ):
 | 
			
		||||
        super().__init__(
 | 
			
		||||
            orientation=orientation,
 | 
			
		||||
            pen=pen,
 | 
			
		||||
            textPen=textPen,
 | 
			
		||||
            linkView=linkView,
 | 
			
		||||
            maxTickLength=maxTickLength,
 | 
			
		||||
            showValues=showValues,
 | 
			
		||||
            text=text,
 | 
			
		||||
            units=units,
 | 
			
		||||
            unitPrefix=unitPrefix,
 | 
			
		||||
            parent=parent,
 | 
			
		||||
        )
 | 
			
		||||
        if lru_cache_tick_strings:
 | 
			
		||||
            self.tickStrings = functools.lru_cache(
 | 
			
		||||
                maxsize=2**20
 | 
			
		||||
            )(self.tickStrings)
 | 
			
		||||
 | 
			
		||||
    def tickValues(
 | 
			
		||||
        self,
 | 
			
		||||
        minVal: float,
 | 
			
		||||
        maxVal: float,
 | 
			
		||||
        size: int,
 | 
			
		||||
 | 
			
		||||
    ) -> list[tuple[float, tuple[str]]]:
 | 
			
		||||
        '''
 | 
			
		||||
        Repack tick values into tuples for lru caching.
 | 
			
		||||
 | 
			
		||||
        '''
 | 
			
		||||
        ticks = []
 | 
			
		||||
        for scalar, values in super().tickValues(minVal, maxVal, size):
 | 
			
		||||
            ticks.append((
 | 
			
		||||
                scalar,
 | 
			
		||||
                tuple(values),  # this
 | 
			
		||||
            ))
 | 
			
		||||
 | 
			
		||||
        return ticks
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
		Reference in New Issue