Merge pull request #413 from pikers/pg_exts_fork

Pg exts fork
no_signal_pi_overlays
goodboy 2022-11-08 12:47:32 -05:00 committed by GitHub
commit ba2e1e04cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 528 additions and 52 deletions

View File

@ -18,7 +18,10 @@
Profiling wrappers for internal libs. Profiling wrappers for internal libs.
""" """
import os
import sys
import time import time
from time import perf_counter
from functools import wraps from functools import wraps
# NOTE: you can pass a flag to enable this: # NOTE: you can pass a flag to enable this:
@ -44,3 +47,184 @@ def timeit(fn):
return res return res
return wrapper return wrapper
# Modified version of ``pyqtgraph.debug.Profiler`` that
# core seems hesitant to land in:
# https://github.com/pyqtgraph/pyqtgraph/pull/2281
class Profiler(object):
'''
Simple profiler allowing measurement of multiple time intervals.
By default, profilers are disabled. To enable profiling, set the
environment variable `PYQTGRAPHPROFILE` to a comma-separated list of
fully-qualified names of profiled functions.
Calling a profiler registers a message (defaulting to an increasing
counter) that contains the time elapsed since the last call. When the
profiler is about to be garbage-collected, the messages are passed to the
outer profiler if one is running, or printed to stdout otherwise.
If `delayed` is set to False, messages are immediately printed instead.
Example:
def function(...):
profiler = Profiler()
... do stuff ...
profiler('did stuff')
... do other stuff ...
profiler('did other stuff')
# profiler is garbage-collected and flushed at function end
If this function is a method of class C, setting `PYQTGRAPHPROFILE` to
"C.function" (without the module name) will enable this profiler.
For regular functions, use the qualified name of the function, stripping
only the initial "pyqtgraph." prefix from the module.
'''
_profilers = os.environ.get("PYQTGRAPHPROFILE", None)
_profilers = _profilers.split(",") if _profilers is not None else []
_depth = 0
# NOTE: without this defined at the class level
# you won't see apprpriately "nested" sub-profiler
# instance calls.
_msgs = []
# set this flag to disable all or individual profilers at runtime
disable = False
class DisabledProfiler(object):
def __init__(self, *args, **kwds):
pass
def __call__(self, *args):
pass
def finish(self):
pass
def mark(self, msg=None):
pass
_disabledProfiler = DisabledProfiler()
def __new__(
cls,
msg=None,
disabled='env',
delayed=True,
ms_threshold: float = 0.0,
):
"""Optionally create a new profiler based on caller's qualname.
``ms_threshold`` can be set to value in ms for which, if the
total measured time of the lifetime of this profiler is **less
than** this value, then no profiling messages will be printed.
Setting ``delayed=False`` disables this feature since messages
are emitted immediately.
"""
if (
disabled is True
or (
disabled == 'env'
and len(cls._profilers) == 0
)
):
return cls._disabledProfiler
# determine the qualified name of the caller function
caller_frame = sys._getframe(1)
try:
caller_object_type = type(caller_frame.f_locals["self"])
except KeyError: # we are in a regular function
qualifier = caller_frame.f_globals["__name__"].split(".", 1)[-1]
else: # we are in a method
qualifier = caller_object_type.__name__
func_qualname = qualifier + "." + caller_frame.f_code.co_name
if disabled == 'env' and func_qualname not in cls._profilers:
# don't do anything
return cls._disabledProfiler
# create an actual profiling object
cls._depth += 1
obj = super(Profiler, cls).__new__(cls)
obj._name = msg or func_qualname
obj._delayed = delayed
obj._markCount = 0
obj._finished = False
obj._firstTime = obj._lastTime = perf_counter()
obj._mt = ms_threshold
obj._newMsg("> Entering " + obj._name)
return obj
def __call__(self, msg=None):
"""Register or print a new message with timing information.
"""
if self.disable:
return
if msg is None:
msg = str(self._markCount)
self._markCount += 1
newTime = perf_counter()
ms = (newTime - self._lastTime) * 1000
self._newMsg(" %s: %0.4f ms", msg, ms)
self._lastTime = newTime
def mark(self, msg=None):
self(msg)
def _newMsg(self, msg, *args):
msg = " " * (self._depth - 1) + msg
if self._delayed:
self._msgs.append((msg, args))
else:
print(msg % args)
def __del__(self):
self.finish()
def finish(self, msg=None):
"""Add a final message; flush the message list if no parent profiler.
"""
if self._finished or self.disable:
return
self._finished = True
if msg is not None:
self(msg)
tot_ms = (perf_counter() - self._firstTime) * 1000
self._newMsg(
"< Exiting %s, total time: %0.4f ms",
self._name,
tot_ms,
)
if tot_ms < self._mt:
# print(f'{tot_ms} < {self._mt}, clearing')
# NOTE: this list **must** be an instance var to avoid
# deleting common messages during GC I think?
self._msgs.clear()
# else:
# print(f'{tot_ms} > {self._mt}, not clearing')
# XXX: why is this needed?
# don't we **want to show** nested profiler messages?
if self._msgs: # and self._depth < 1:
# if self._msgs:
print("\n".join([m[0] % m[1] for m in self._msgs]))
# clear all entries
self._msgs.clear()
# type(self)._msgs = []
type(self)._depth -= 1

View File

@ -56,6 +56,7 @@ if TYPE_CHECKING:
from .feed import maybe_open_feed from .feed import maybe_open_feed
from ..log import get_logger, get_console_log from ..log import get_logger, get_console_log
from .._profile import Profiler
log = get_logger(__name__) log = get_logger(__name__)
@ -645,7 +646,7 @@ async def tsdb_history_update(
# * the original data feed arch blurb: # * the original data feed arch blurb:
# - https://github.com/pikers/piker/issues/98 # - https://github.com/pikers/piker/issues/98
# #
profiler = pg.debug.Profiler( profiler = Profiler(
disabled=False, # not pg_profile_enabled(), disabled=False, # not pg_profile_enabled(),
delayed=False, delayed=False,
) )

View File

@ -44,6 +44,7 @@ from ._api import (
_load_builtins, _load_builtins,
_Token, _Token,
) )
from .._profile import Profiler
log = get_logger(__name__) log = get_logger(__name__)
@ -91,7 +92,7 @@ async def fsp_compute(
) -> None: ) -> None:
profiler = pg.debug.Profiler( profiler = Profiler(
delayed=False, delayed=False,
disabled=True disabled=True
) )
@ -262,7 +263,7 @@ async def cascade(
destination shm array buffer. destination shm array buffer.
''' '''
profiler = pg.debug.Profiler( profiler = Profiler(
delayed=False, delayed=False,
disabled=False disabled=False
) )

View File

@ -111,7 +111,8 @@ class LevelMarker(QGraphicsPathItem):
# get polygon and scale # get polygon and scale
super().__init__() super().__init__()
self.scale(size, size) # self.setScale(size, size)
self.setScale(size)
# interally generates path # interally generates path
self._style = None self._style = None

View File

@ -78,6 +78,8 @@ async def _async_main(
""" """
from . import _display from . import _display
from ._pg_overrides import _do_overrides
_do_overrides()
godwidget = main_widget godwidget = main_widget

View File

@ -39,12 +39,17 @@ class Axis(pg.AxisItem):
''' '''
A better axis that sizes tick contents considering font size. A better axis that sizes tick contents considering font size.
Also includes tick values lru caching originally proposed in but never
accepted upstream:
https://github.com/pyqtgraph/pyqtgraph/pull/2160
''' '''
def __init__( def __init__(
self, self,
linkedsplits, linkedsplits,
typical_max_str: str = '100 000.000', typical_max_str: str = '100 000.000',
text_color: str = 'bracket', text_color: str = 'bracket',
lru_cache_tick_strings: bool = True,
**kwargs **kwargs
) -> None: ) -> None:
@ -91,6 +96,34 @@ class Axis(pg.AxisItem):
# size the pertinent axis dimension to a "typical value" # size the pertinent axis dimension to a "typical value"
self.size_to_values() self.size_to_values()
# NOTE: requires override ``.tickValues()`` method seen below.
if lru_cache_tick_strings:
self.tickStrings = lru_cache(
maxsize=2**20
)(self.tickStrings)
# NOTE: only overriden to cast tick values entries into tuples
# for use with the lru caching.
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
@property @property
def text_color(self) -> str: def text_color(self) -> str:
return self._text_color return self._text_color

View File

@ -73,6 +73,8 @@ from .._profile import pg_profile_enabled, ms_slower_then
from ._overlay import PlotItemOverlay from ._overlay import PlotItemOverlay
from ._flows import Flow from ._flows import Flow
from ._search import SearchWidget from ._search import SearchWidget
from . import _pg_overrides as pgo
from .._profile import Profiler
if TYPE_CHECKING: if TYPE_CHECKING:
from ._display import DisplayState from ._display import DisplayState
@ -831,6 +833,7 @@ class ChartPlotWidget(pg.PlotWidget):
static_yrange: Optional[tuple[float, float]] = None, static_yrange: Optional[tuple[float, float]] = None,
parent=None,
**kwargs, **kwargs,
): ):
''' '''
@ -848,12 +851,15 @@ class ChartPlotWidget(pg.PlotWidget):
# source of our custom interactions # source of our custom interactions
self.cv = cv = self.mk_vb(name) self.cv = cv = self.mk_vb(name)
pi = pgo.PlotItem(viewBox=cv, **kwargs)
super().__init__( super().__init__(
background=hcolor(view_color), background=hcolor(view_color),
viewBox=cv, viewBox=cv,
# parent=None, # parent=None,
# plotItem=None, # plotItem=None,
# antialias=True, # antialias=True,
parent=parent,
plotItem=pi,
**kwargs **kwargs
) )
# give viewbox as reference to chart # give viewbox as reference to chart
@ -1144,7 +1150,7 @@ class ChartPlotWidget(pg.PlotWidget):
axis_side: str = 'right', axis_side: str = 'right',
axis_kwargs: dict = {}, axis_kwargs: dict = {},
) -> pg.PlotItem: ) -> pgo.PlotItem:
# Custom viewbox impl # Custom viewbox impl
cv = self.mk_vb(name) cv = self.mk_vb(name)
@ -1153,13 +1159,14 @@ class ChartPlotWidget(pg.PlotWidget):
allowed_sides = {'left', 'right'} allowed_sides = {'left', 'right'}
if axis_side not in allowed_sides: if axis_side not in allowed_sides:
raise ValueError(f'``axis_side``` must be in {allowed_sides}') raise ValueError(f'``axis_side``` must be in {allowed_sides}')
yaxis = PriceAxis( yaxis = PriceAxis(
orientation=axis_side, orientation=axis_side,
linkedsplits=self.linked, linkedsplits=self.linked,
**axis_kwargs, **axis_kwargs,
) )
pi = pg.PlotItem( pi = pgo.PlotItem(
parent=self.plotItem, parent=self.plotItem,
name=name, name=name,
enableMenu=False, enableMenu=False,
@ -1246,7 +1253,7 @@ class ChartPlotWidget(pg.PlotWidget):
# TODO: this probably needs its own method? # TODO: this probably needs its own method?
if overlay: if overlay:
if isinstance(overlay, pg.PlotItem): if isinstance(overlay, pgo.PlotItem):
if overlay not in self.pi_overlay.overlays: if overlay not in self.pi_overlay.overlays:
raise RuntimeError( raise RuntimeError(
f'{overlay} must be from `.plotitem_overlay()`' f'{overlay} must be from `.plotitem_overlay()`'
@ -1405,7 +1412,7 @@ class ChartPlotWidget(pg.PlotWidget):
If ``bars_range`` is provided use that range. If ``bars_range`` is provided use that range.
''' '''
profiler = pg.debug.Profiler( profiler = Profiler(
msg=f'`{str(self)}.maxmin(name={name})`: `{self.name}`', msg=f'`{str(self)}.maxmin(name={name})`: `{self.name}`',
disabled=not pg_profile_enabled(), disabled=not pg_profile_enabled(),
ms_threshold=ms_slower_then, ms_threshold=ms_slower_then,

View File

@ -44,6 +44,7 @@ from ._style import hcolor
# ds_m4, # ds_m4,
# ) # )
from ..log import get_logger from ..log import get_logger
from .._profile import Profiler
log = get_logger(__name__) log = get_logger(__name__)
@ -331,7 +332,7 @@ class Curve(pg.GraphicsObject):
) -> None: ) -> None:
profiler = pg.debug.Profiler( profiler = Profiler(
msg=f'Curve.paint(): `{self._name}`', msg=f'Curve.paint(): `{self._name}`',
disabled=not pg_profile_enabled(), disabled=not pg_profile_enabled(),
ms_threshold=ms_slower_then, ms_threshold=ms_slower_then,
@ -466,7 +467,7 @@ class StepCurve(Curve):
def sub_paint( def sub_paint(
self, self,
p: QPainter, p: QPainter,
profiler: pg.debug.Profiler, profiler: Profiler,
) -> None: ) -> None:
# p.drawLines(*tuple(filter(bool, self._last_step_lines))) # p.drawLines(*tuple(filter(bool, self._last_step_lines)))

View File

@ -66,6 +66,7 @@ from .._profile import (
ms_slower_then, ms_slower_then,
) )
from ..log import get_logger from ..log import get_logger
from .._profile import Profiler
log = get_logger(__name__) log = get_logger(__name__)
@ -441,7 +442,7 @@ def graphics_update_cycle(
# TODO: just pass this as a direct ref to avoid so many attr accesses? # TODO: just pass this as a direct ref to avoid so many attr accesses?
hist_chart = ds.godwidget.hist_linked.chart hist_chart = ds.godwidget.hist_linked.chart
profiler = pg.debug.Profiler( profiler = Profiler(
msg=f'Graphics loop cycle for: `{chart.name}`', msg=f'Graphics loop cycle for: `{chart.name}`',
delayed=True, delayed=True,
disabled=not pg_profile_enabled(), disabled=not pg_profile_enabled(),

View File

@ -26,7 +26,19 @@ from typing import (
) )
import pyqtgraph as pg import pyqtgraph as pg
from pyqtgraph import ViewBox, Point, QtCore, QtGui from pyqtgraph import (
ViewBox,
Point,
QtCore,
QtWidgets,
)
from PyQt5.QtGui import (
QColor,
)
from PyQt5.QtWidgets import (
QLabel,
)
from pyqtgraph import functions as fn from pyqtgraph import functions as fn
from PyQt5.QtCore import QPointF from PyQt5.QtCore import QPointF
import numpy as np import numpy as np
@ -240,7 +252,7 @@ class LineEditor(Struct):
return lines return lines
class SelectRect(QtGui.QGraphicsRectItem): class SelectRect(QtWidgets.QGraphicsRectItem):
def __init__( def __init__(
self, self,
@ -249,12 +261,12 @@ class SelectRect(QtGui.QGraphicsRectItem):
) -> None: ) -> None:
super().__init__(0, 0, 1, 1) super().__init__(0, 0, 1, 1)
# self.rbScaleBox = QtGui.QGraphicsRectItem(0, 0, 1, 1) # self.rbScaleBox = QGraphicsRectItem(0, 0, 1, 1)
self.vb = viewbox self.vb = viewbox
self._chart: 'ChartPlotWidget' = None # noqa self._chart: 'ChartPlotWidget' = None # noqa
# override selection box color # override selection box color
color = QtGui.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))
@ -262,7 +274,7 @@ class SelectRect(QtGui.QGraphicsRectItem):
self.hide() self.hide()
self._label = None self._label = None
label = self._label = QtGui.QLabel() label = self._label = QLabel()
label.setTextFormat(0) # markdown label.setTextFormat(0) # markdown
label.setFont(_font.font) label.setFont(_font.font)
label.setMargin(0) label.setMargin(0)
@ -299,8 +311,8 @@ class SelectRect(QtGui.QGraphicsRectItem):
# TODO: get bg color working # TODO: get bg color working
palette.setColor( palette.setColor(
self._label.backgroundRole(), self._label.backgroundRole(),
# QtGui.QColor(chart.backgroundBrush()), # QColor(chart.backgroundBrush()),
QtGui.QColor(hcolor('papas_special')), QColor(hcolor('papas_special')),
) )
def update_on_resize(self, vr, r): def update_on_resize(self, vr, r):
@ -348,7 +360,7 @@ class SelectRect(QtGui.QGraphicsRectItem):
self.setPos(r.topLeft()) self.setPos(r.topLeft())
self.resetTransform() self.resetTransform()
self.scale(r.width(), r.height()) self.setRect(r)
self.show() self.show()
y1, y2 = start_pos.y(), end_pos.y() y1, y2 = start_pos.y(), end_pos.y()

View File

@ -20,19 +20,24 @@ Trio - Qt integration
Run ``trio`` in guest mode on top of the Qt event loop. Run ``trio`` in guest mode on top of the Qt event loop.
All global Qt runtime settings are mostly defined here. All global Qt runtime settings are mostly defined here.
""" """
from __future__ import annotations
from typing import ( from typing import (
Callable, Callable,
Any, Any,
Type, Type,
TYPE_CHECKING,
) )
import platform import platform
import traceback import traceback
# Qt specific # Qt specific
import PyQt5 # noqa import PyQt5 # noqa
from pyqtgraph import QtGui from PyQt5.QtWidgets import (
QWidget,
QMainWindow,
QApplication,
)
from PyQt5 import QtCore from PyQt5 import QtCore
# from PyQt5.QtGui import QLabel, QStatusBar
from PyQt5.QtCore import ( from PyQt5.QtCore import (
pyqtRemoveInputHook, pyqtRemoveInputHook,
Qt, Qt,
@ -49,6 +54,7 @@ from ..log import get_logger
from ._pg_overrides import _do_overrides from ._pg_overrides import _do_overrides
from . import _style from . import _style
log = get_logger(__name__) log = get_logger(__name__)
# pyqtgraph global config # pyqtgraph global config
@ -76,17 +82,17 @@ if platform.system() == "Windows":
def run_qtractor( def run_qtractor(
func: Callable, func: Callable,
args: tuple, args: tuple,
main_widget_type: Type[QtGui.QWidget], main_widget_type: Type[QWidget],
tractor_kwargs: dict[str, Any] = {}, tractor_kwargs: dict[str, Any] = {},
window_type: QtGui.QMainWindow = None, window_type: QMainWindow = None,
) -> None: ) -> None:
# avoids annoying message when entering debugger from qt loop # avoids annoying message when entering debugger from qt loop
pyqtRemoveInputHook() pyqtRemoveInputHook()
app = QtGui.QApplication.instance() app = QApplication.instance()
if app is None: if app is None:
app = PyQt5.QtWidgets.QApplication([]) app = QApplication([])
# TODO: we might not need this if it's desired # TODO: we might not need this if it's desired
# to cancel the tractor machinery on Qt loop # to cancel the tractor machinery on Qt loop

View File

@ -59,6 +59,7 @@ from ._curve import (
FlattenedOHLC, FlattenedOHLC,
) )
from ..log import get_logger from ..log import get_logger
from .._profile import Profiler
log = get_logger(__name__) log = get_logger(__name__)
@ -130,7 +131,7 @@ def render_baritems(
int, int, np.ndarray, int, int, np.ndarray,
int, int, np.ndarray, int, int, np.ndarray,
], ],
profiler: pg.debug.Profiler, profiler: Profiler,
**kwargs, **kwargs,
) -> None: ) -> None:
@ -517,7 +518,7 @@ class Flow(msgspec.Struct): # , frozen=True):
render: bool = True, render: bool = True,
array_key: Optional[str] = None, array_key: Optional[str] = None,
profiler: Optional[pg.debug.Profiler] = None, profiler: Optional[Profiler] = None,
do_append: bool = True, do_append: bool = True,
**kwargs, **kwargs,
@ -528,7 +529,7 @@ class Flow(msgspec.Struct): # , frozen=True):
render to graphics. render to graphics.
''' '''
profiler = pg.debug.Profiler( profiler = Profiler(
msg=f'Flow.update_graphics() for {self.name}', msg=f'Flow.update_graphics() for {self.name}',
disabled=not pg_profile_enabled(), disabled=not pg_profile_enabled(),
ms_threshold=4, ms_threshold=4,
@ -948,7 +949,7 @@ class Renderer(msgspec.Struct):
new_read, new_read,
array_key: str, array_key: str,
profiler: pg.debug.Profiler, profiler: Profiler,
uppx: float = 1, uppx: float = 1,
# redraw and ds flags # redraw and ds flags

View File

@ -59,6 +59,7 @@ from ..fsp._volume import (
flow_rates, flow_rates,
) )
from ..log import get_logger from ..log import get_logger
from .._profile import Profiler
log = get_logger(__name__) log = get_logger(__name__)
@ -190,7 +191,7 @@ async def open_fsp_actor_cluster(
from tractor._clustering import open_actor_cluster from tractor._clustering import open_actor_cluster
# profiler = pg.debug.Profiler( # profiler = Profiler(
# delayed=False, # delayed=False,
# disabled=False # disabled=False
# ) # )
@ -212,7 +213,7 @@ async def run_fsp_ui(
target: Fsp, target: Fsp,
conf: dict[str, dict], conf: dict[str, dict],
loglevel: str, loglevel: str,
# profiler: pg.debug.Profiler, # profiler: Profiler,
# _quote_throttle_rate: int = 58, # _quote_throttle_rate: int = 58,
) -> None: ) -> None:
@ -746,6 +747,8 @@ async def open_vlm_displays(
}, },
) )
dvlm_pi.hideAxis('left')
dvlm_pi.hideAxis('bottom')
# all to be overlayed curve names # all to be overlayed curve names
fields = [ fields = [
'dolla_vlm', 'dolla_vlm',
@ -878,6 +881,7 @@ async def open_vlm_displays(
# keep both regular and dark vlm in view # keep both regular and dark vlm in view
names=trade_rate_fields, names=trade_rate_fields,
) )
tr_pi.hideAxis('bottom')
chart_curves( chart_curves(
trade_rate_fields, trade_rate_fields,
@ -951,7 +955,7 @@ async def start_fsp_displays(
# }, # },
# }, # },
} }
profiler = pg.debug.Profiler( profiler = Profiler(
delayed=False, delayed=False,
disabled=False disabled=False
) )

View File

@ -33,6 +33,7 @@ import numpy as np
import trio import trio
from ..log import get_logger from ..log import get_logger
from .._profile import Profiler
from .._profile import pg_profile_enabled, ms_slower_then from .._profile import pg_profile_enabled, ms_slower_then
# from ._style import _min_points_to_show # from ._style import _min_points_to_show
from ._editors import SelectRect from ._editors import SelectRect
@ -779,7 +780,7 @@ class ChartView(ViewBox):
''' '''
name = self.name name = self.name
# print(f'YRANGE ON {name}') # print(f'YRANGE ON {name}')
profiler = pg.debug.Profiler( profiler = Profiler(
msg=f'`ChartView._set_yrange()`: `{name}`', msg=f'`ChartView._set_yrange()`: `{name}`',
disabled=not pg_profile_enabled(), disabled=not pg_profile_enabled(),
ms_threshold=ms_slower_then, ms_threshold=ms_slower_then,
@ -916,7 +917,7 @@ class ChartView(ViewBox):
autoscale_overlays: bool = True, autoscale_overlays: bool = True,
): ):
profiler = pg.debug.Profiler( profiler = Profiler(
msg=f'ChartView.maybe_downsample_graphics() for {self.name}', msg=f'ChartView.maybe_downsample_graphics() for {self.name}',
disabled=not pg_profile_enabled(), disabled=not pg_profile_enabled(),

View File

@ -32,6 +32,7 @@ from PyQt5.QtGui import QPainterPath
from .._profile import pg_profile_enabled, ms_slower_then from .._profile import pg_profile_enabled, ms_slower_then
from ._style import hcolor from ._style import hcolor
from ..log import get_logger from ..log import get_logger
from .._profile import Profiler
if TYPE_CHECKING: if TYPE_CHECKING:
from ._chart import LinkedSplits from ._chart import LinkedSplits
@ -170,7 +171,7 @@ class BarItems(pg.GraphicsObject):
) -> None: ) -> None:
profiler = pg.debug.Profiler( profiler = Profiler(
disabled=not pg_profile_enabled(), disabled=not pg_profile_enabled(),
ms_threshold=ms_slower_then, ms_threshold=ms_slower_then,
) )

View File

@ -15,11 +15,15 @@
# 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.
""" """
from typing import Optional
import pyqtgraph as pg import pyqtgraph as pg
@ -46,3 +50,211 @@ 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
# 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 = pg.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)

View File

@ -28,10 +28,19 @@ from typing import (
) )
import uuid import uuid
from pyqtgraph import QtGui
from PyQt5 import QtCore from PyQt5 import QtCore
from PyQt5.QtWidgets import QLabel, QStatusBar from PyQt5.QtWidgets import (
QWidget,
QMainWindow,
QApplication,
QLabel,
QStatusBar,
)
from PyQt5.QtGui import (
QScreen,
QCloseEvent,
)
from ..log import get_logger from ..log import get_logger
from ._style import _font_small, hcolor from ._style import _font_small, hcolor
from ._chart import GodWidget from ._chart import GodWidget
@ -153,7 +162,7 @@ class MultiStatus:
self.bar.clearMessage() self.bar.clearMessage()
class MainWindow(QtGui.QMainWindow): class MainWindow(QMainWindow):
# XXX: for tiling wms this should scale # XXX: for tiling wms this should scale
# with the alloted window size. # with the alloted window size.
@ -176,12 +185,12 @@ class MainWindow(QtGui.QMainWindow):
self._size: Optional[tuple[int, int]] = None self._size: Optional[tuple[int, int]] = None
@property @property
def mode_label(self) -> QtGui.QLabel: def mode_label(self) -> QLabel:
# init mode label # init mode label
if not self._status_label: if not self._status_label:
self._status_label = label = QtGui.QLabel() self._status_label = label = QLabel()
label.setStyleSheet( label.setStyleSheet(
f"""QLabel {{ f"""QLabel {{
color : {hcolor('gunmetal')}; color : {hcolor('gunmetal')};
@ -203,8 +212,7 @@ class MainWindow(QtGui.QMainWindow):
def closeEvent( def closeEvent(
self, self,
event: QCloseEvent,
event: QtGui.QCloseEvent,
) -> None: ) -> None:
'''Cancel the root actor asap. '''Cancel the root actor asap.
@ -244,8 +252,8 @@ class MainWindow(QtGui.QMainWindow):
def on_focus_change( def on_focus_change(
self, self,
last: QtGui.QWidget, last: QWidget,
current: QtGui.QWidget, current: QWidget,
) -> None: ) -> None:
@ -256,12 +264,12 @@ class MainWindow(QtGui.QMainWindow):
name = getattr(current, 'mode_name', '') name = getattr(current, 'mode_name', '')
self.set_mode_name(name) self.set_mode_name(name)
def current_screen(self) -> QtGui.QScreen: def current_screen(self) -> QScreen:
''' '''
Get a frickin screen (if we can, gawd). Get a frickin screen (if we can, gawd).
''' '''
app = QtGui.QApplication.instance() app = QApplication.instance()
for _ in range(3): for _ in range(3):
screen = app.screenAt(self.pos()) screen = app.screenAt(self.pos())
@ -294,7 +302,7 @@ class MainWindow(QtGui.QMainWindow):
''' '''
# https://stackoverflow.com/a/18975846 # https://stackoverflow.com/a/18975846
if not size and not self._size: if not size and not self._size:
# app = QtGui.QApplication.instance() # app = QApplication.instance()
geo = self.current_screen().geometry() geo = self.current_screen().geometry()
h, w = geo.height(), geo.width() h, w = geo.height(), geo.width()
# use approx 1/3 of the area of the screen by default # use approx 1/3 of the area of the screen by default
@ -331,7 +339,7 @@ class MainWindow(QtGui.QMainWindow):
# singleton app per actor # singleton app per actor
_qt_win: QtGui.QMainWindow = None _qt_win: QMainWindow = None
def main_window() -> MainWindow: def main_window() -> MainWindow:

View File

@ -6,7 +6,7 @@
# `pyqtgraph` peeps keep breaking, fixing, improving so might as well # `pyqtgraph` peeps keep breaking, fixing, improving so might as well
# pin this to a dev branch that we have more control over especially # pin this to a dev branch that we have more control over especially
# as more graphics stuff gets hashed out. # as more graphics stuff gets hashed out.
-e git+https://github.com/pikers/pyqtgraph.git@piker_pin#egg=pyqtgraph -e git+https://github.com/pikers/pyqtgraph.git@master#egg=pyqtgraph
# our async client for ``marketstore`` (the tsdb) # our async client for ``marketstore`` (the tsdb)
-e git+https://github.com/pikers/anyio-marketstore.git@master#egg=anyio-marketstore -e git+https://github.com/pikers/anyio-marketstore.git@master#egg=anyio-marketstore