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/>.
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Customization of ``pyqtgraph`` core routines to speed up our use mostly
|
Customization of ``pyqtgraph`` core routines and various types normally
|
||||||
based on not requiring "scentific precision" for pixel perfect view
|
for speedups.
|
||||||
transforms.
|
|
||||||
|
Generally, our does not require "scentific precision" for pixel perfect
|
||||||
|
view transforms.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
import functools
|
||||||
|
from typing import Optional
|
||||||
|
|
||||||
import pyqtgraph as pg
|
import pyqtgraph as pg
|
||||||
|
|
||||||
|
|
||||||
|
@ -46,3 +51,268 @@ def _do_overrides() -> None:
|
||||||
"""
|
"""
|
||||||
# we don't care about potential fp issues inside Qt
|
# we don't care about potential fp issues inside Qt
|
||||||
pg.functions.invertQTransform = invertQTransform
|
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