commit
						ba2e1e04cd
					
				|  | @ -18,7 +18,10 @@ | |||
| Profiling wrappers for internal libs. | ||||
| 
 | ||||
| """ | ||||
| import os | ||||
| import sys | ||||
| import time | ||||
| from time import perf_counter | ||||
| from functools import wraps | ||||
| 
 | ||||
| # NOTE: you can pass a flag to enable this: | ||||
|  | @ -44,3 +47,184 @@ def timeit(fn): | |||
|         return res | ||||
| 
 | ||||
|     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 | ||||
|  |  | |||
|  | @ -56,6 +56,7 @@ if TYPE_CHECKING: | |||
| 
 | ||||
| from .feed import maybe_open_feed | ||||
| from ..log import get_logger, get_console_log | ||||
| from .._profile import Profiler | ||||
| 
 | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
|  | @ -645,7 +646,7 @@ async def tsdb_history_update( | |||
|     #   * the original data feed arch blurb: | ||||
|     #     - https://github.com/pikers/piker/issues/98 | ||||
|     # | ||||
|     profiler = pg.debug.Profiler( | ||||
|     profiler = Profiler( | ||||
|         disabled=False,  # not pg_profile_enabled(), | ||||
|         delayed=False, | ||||
|     ) | ||||
|  |  | |||
|  | @ -44,6 +44,7 @@ from ._api import ( | |||
|     _load_builtins, | ||||
|     _Token, | ||||
| ) | ||||
| from .._profile import Profiler | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
| 
 | ||||
|  | @ -91,7 +92,7 @@ async def fsp_compute( | |||
| 
 | ||||
| ) -> None: | ||||
| 
 | ||||
|     profiler = pg.debug.Profiler( | ||||
|     profiler = Profiler( | ||||
|         delayed=False, | ||||
|         disabled=True | ||||
|     ) | ||||
|  | @ -262,7 +263,7 @@ async def cascade( | |||
|     destination shm array buffer. | ||||
| 
 | ||||
|     ''' | ||||
|     profiler = pg.debug.Profiler( | ||||
|     profiler = Profiler( | ||||
|         delayed=False, | ||||
|         disabled=False | ||||
|     ) | ||||
|  |  | |||
|  | @ -111,7 +111,8 @@ class LevelMarker(QGraphicsPathItem): | |||
| 
 | ||||
|         # get polygon and scale | ||||
|         super().__init__() | ||||
|         self.scale(size, size) | ||||
|         # self.setScale(size, size) | ||||
|         self.setScale(size) | ||||
| 
 | ||||
|         # interally generates path | ||||
|         self._style = None | ||||
|  |  | |||
|  | @ -78,6 +78,8 @@ async def _async_main( | |||
| 
 | ||||
|     """ | ||||
|     from . import _display | ||||
|     from ._pg_overrides import _do_overrides | ||||
|     _do_overrides() | ||||
| 
 | ||||
|     godwidget = main_widget | ||||
| 
 | ||||
|  |  | |||
|  | @ -39,12 +39,17 @@ class Axis(pg.AxisItem): | |||
|     ''' | ||||
|     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__( | ||||
|         self, | ||||
|         linkedsplits, | ||||
|         typical_max_str: str = '100 000.000', | ||||
|         text_color: str = 'bracket', | ||||
|         lru_cache_tick_strings: bool = True, | ||||
|         **kwargs | ||||
| 
 | ||||
|     ) -> None: | ||||
|  | @ -91,6 +96,34 @@ class Axis(pg.AxisItem): | |||
|         # size the pertinent axis dimension to a "typical value" | ||||
|         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 | ||||
|     def text_color(self) -> str: | ||||
|         return self._text_color | ||||
|  |  | |||
|  | @ -73,6 +73,8 @@ from .._profile import pg_profile_enabled, ms_slower_then | |||
| from ._overlay import PlotItemOverlay | ||||
| from ._flows import Flow | ||||
| from ._search import SearchWidget | ||||
| from . import _pg_overrides as pgo | ||||
| from .._profile import Profiler | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from ._display import DisplayState | ||||
|  | @ -831,6 +833,7 @@ class ChartPlotWidget(pg.PlotWidget): | |||
| 
 | ||||
|         static_yrange: Optional[tuple[float, float]] = None, | ||||
| 
 | ||||
|         parent=None, | ||||
|         **kwargs, | ||||
|     ): | ||||
|         ''' | ||||
|  | @ -848,12 +851,15 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|         # source of our custom interactions | ||||
|         self.cv = cv = self.mk_vb(name) | ||||
| 
 | ||||
|         pi = pgo.PlotItem(viewBox=cv, **kwargs) | ||||
|         super().__init__( | ||||
|             background=hcolor(view_color), | ||||
|             viewBox=cv, | ||||
|             # parent=None, | ||||
|             # plotItem=None, | ||||
|             # antialias=True, | ||||
|             parent=parent, | ||||
|             plotItem=pi, | ||||
|             **kwargs | ||||
|         ) | ||||
|         # give viewbox as reference to chart | ||||
|  | @ -1144,7 +1150,7 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|         axis_side: str = 'right', | ||||
|         axis_kwargs: dict = {}, | ||||
| 
 | ||||
|     ) -> pg.PlotItem: | ||||
|     ) -> pgo.PlotItem: | ||||
| 
 | ||||
|         # Custom viewbox impl | ||||
|         cv = self.mk_vb(name) | ||||
|  | @ -1153,13 +1159,14 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|         allowed_sides = {'left', 'right'} | ||||
|         if axis_side not in allowed_sides: | ||||
|             raise ValueError(f'``axis_side``` must be in {allowed_sides}') | ||||
| 
 | ||||
|         yaxis = PriceAxis( | ||||
|             orientation=axis_side, | ||||
|             linkedsplits=self.linked, | ||||
|             **axis_kwargs, | ||||
|         ) | ||||
| 
 | ||||
|         pi = pg.PlotItem( | ||||
|         pi = pgo.PlotItem( | ||||
|             parent=self.plotItem, | ||||
|             name=name, | ||||
|             enableMenu=False, | ||||
|  | @ -1246,7 +1253,7 @@ class ChartPlotWidget(pg.PlotWidget): | |||
| 
 | ||||
|         # TODO: this probably needs its own method? | ||||
|         if overlay: | ||||
|             if isinstance(overlay, pg.PlotItem): | ||||
|             if isinstance(overlay, pgo.PlotItem): | ||||
|                 if overlay not in self.pi_overlay.overlays: | ||||
|                     raise RuntimeError( | ||||
|                             f'{overlay} must be from `.plotitem_overlay()`' | ||||
|  | @ -1405,7 +1412,7 @@ class ChartPlotWidget(pg.PlotWidget): | |||
|         If ``bars_range`` is provided use that range. | ||||
| 
 | ||||
|         ''' | ||||
|         profiler = pg.debug.Profiler( | ||||
|         profiler = Profiler( | ||||
|             msg=f'`{str(self)}.maxmin(name={name})`: `{self.name}`', | ||||
|             disabled=not pg_profile_enabled(), | ||||
|             ms_threshold=ms_slower_then, | ||||
|  |  | |||
|  | @ -44,6 +44,7 @@ from ._style import hcolor | |||
| #     ds_m4, | ||||
| # ) | ||||
| from ..log import get_logger | ||||
| from .._profile import Profiler | ||||
| 
 | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
|  | @ -331,7 +332,7 @@ class Curve(pg.GraphicsObject): | |||
| 
 | ||||
|     ) -> None: | ||||
| 
 | ||||
|         profiler = pg.debug.Profiler( | ||||
|         profiler = Profiler( | ||||
|             msg=f'Curve.paint(): `{self._name}`', | ||||
|             disabled=not pg_profile_enabled(), | ||||
|             ms_threshold=ms_slower_then, | ||||
|  | @ -466,7 +467,7 @@ class StepCurve(Curve): | |||
|     def sub_paint( | ||||
|         self, | ||||
|         p: QPainter, | ||||
|         profiler: pg.debug.Profiler, | ||||
|         profiler: Profiler, | ||||
| 
 | ||||
|     ) -> None: | ||||
|         # p.drawLines(*tuple(filter(bool, self._last_step_lines))) | ||||
|  |  | |||
|  | @ -66,6 +66,7 @@ from .._profile import ( | |||
|     ms_slower_then, | ||||
| ) | ||||
| from ..log import get_logger | ||||
| from .._profile import Profiler | ||||
| 
 | ||||
| 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? | ||||
|     hist_chart = ds.godwidget.hist_linked.chart | ||||
| 
 | ||||
|     profiler = pg.debug.Profiler( | ||||
|     profiler = Profiler( | ||||
|         msg=f'Graphics loop cycle for: `{chart.name}`', | ||||
|         delayed=True, | ||||
|         disabled=not pg_profile_enabled(), | ||||
|  |  | |||
|  | @ -26,7 +26,19 @@ from typing import ( | |||
| ) | ||||
| 
 | ||||
| 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 PyQt5.QtCore import QPointF | ||||
| import numpy as np | ||||
|  | @ -240,7 +252,7 @@ class LineEditor(Struct): | |||
|         return lines | ||||
| 
 | ||||
| 
 | ||||
| class SelectRect(QtGui.QGraphicsRectItem): | ||||
| class SelectRect(QtWidgets.QGraphicsRectItem): | ||||
| 
 | ||||
|     def __init__( | ||||
|         self, | ||||
|  | @ -249,12 +261,12 @@ class SelectRect(QtGui.QGraphicsRectItem): | |||
|     ) -> None: | ||||
|         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._chart: 'ChartPlotWidget' = None  # noqa | ||||
| 
 | ||||
|         # override selection box color | ||||
|         color = QtGui.QColor(hcolor(color)) | ||||
|         color = QColor(hcolor(color)) | ||||
|         self.setPen(fn.mkPen(color, width=1)) | ||||
|         color.setAlpha(66) | ||||
|         self.setBrush(fn.mkBrush(color)) | ||||
|  | @ -262,7 +274,7 @@ class SelectRect(QtGui.QGraphicsRectItem): | |||
|         self.hide() | ||||
|         self._label = None | ||||
| 
 | ||||
|         label = self._label = QtGui.QLabel() | ||||
|         label = self._label = QLabel() | ||||
|         label.setTextFormat(0)  # markdown | ||||
|         label.setFont(_font.font) | ||||
|         label.setMargin(0) | ||||
|  | @ -299,8 +311,8 @@ class SelectRect(QtGui.QGraphicsRectItem): | |||
|         # TODO: get bg color working | ||||
|         palette.setColor( | ||||
|             self._label.backgroundRole(), | ||||
|             # QtGui.QColor(chart.backgroundBrush()), | ||||
|             QtGui.QColor(hcolor('papas_special')), | ||||
|             # QColor(chart.backgroundBrush()), | ||||
|             QColor(hcolor('papas_special')), | ||||
|         ) | ||||
| 
 | ||||
|     def update_on_resize(self, vr, r): | ||||
|  | @ -348,7 +360,7 @@ class SelectRect(QtGui.QGraphicsRectItem): | |||
| 
 | ||||
|         self.setPos(r.topLeft()) | ||||
|         self.resetTransform() | ||||
|         self.scale(r.width(), r.height()) | ||||
|         self.setRect(r) | ||||
|         self.show() | ||||
| 
 | ||||
|         y1, y2 = start_pos.y(), end_pos.y() | ||||
|  |  | |||
|  | @ -20,19 +20,24 @@ Trio - Qt integration | |||
| Run ``trio`` in guest mode on top of the Qt event loop. | ||||
| All global Qt runtime settings are mostly defined here. | ||||
| """ | ||||
| from __future__ import annotations | ||||
| from typing import ( | ||||
|     Callable, | ||||
|     Any, | ||||
|     Type, | ||||
|     TYPE_CHECKING, | ||||
| ) | ||||
| import platform | ||||
| import traceback | ||||
| 
 | ||||
| # Qt specific | ||||
| import PyQt5  # noqa | ||||
| from pyqtgraph import QtGui | ||||
| from PyQt5.QtWidgets import ( | ||||
|     QWidget, | ||||
|     QMainWindow, | ||||
|     QApplication, | ||||
| ) | ||||
| from PyQt5 import QtCore | ||||
| # from PyQt5.QtGui import QLabel, QStatusBar | ||||
| from PyQt5.QtCore import ( | ||||
|     pyqtRemoveInputHook, | ||||
|     Qt, | ||||
|  | @ -49,6 +54,7 @@ from ..log import get_logger | |||
| from ._pg_overrides import _do_overrides | ||||
| from . import _style | ||||
| 
 | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
| 
 | ||||
| # pyqtgraph global config | ||||
|  | @ -76,17 +82,17 @@ if platform.system() == "Windows": | |||
| def run_qtractor( | ||||
|     func: Callable, | ||||
|     args: tuple, | ||||
|     main_widget_type: Type[QtGui.QWidget], | ||||
|     main_widget_type: Type[QWidget], | ||||
|     tractor_kwargs: dict[str, Any] = {}, | ||||
|     window_type: QtGui.QMainWindow = None, | ||||
|     window_type: QMainWindow = None, | ||||
| 
 | ||||
| ) -> None: | ||||
|     # avoids annoying message when entering debugger from qt loop | ||||
|     pyqtRemoveInputHook() | ||||
| 
 | ||||
|     app = QtGui.QApplication.instance() | ||||
|     app = QApplication.instance() | ||||
|     if app is None: | ||||
|         app = PyQt5.QtWidgets.QApplication([]) | ||||
|         app = QApplication([]) | ||||
| 
 | ||||
|     # TODO: we might not need this if it's desired | ||||
|     # to cancel the tractor machinery on Qt loop | ||||
|  |  | |||
|  | @ -59,6 +59,7 @@ from ._curve import ( | |||
|     FlattenedOHLC, | ||||
| ) | ||||
| from ..log import get_logger | ||||
| from .._profile import Profiler | ||||
| 
 | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
|  | @ -130,7 +131,7 @@ def render_baritems( | |||
|         int, int, np.ndarray, | ||||
|         int, int, np.ndarray, | ||||
|     ], | ||||
|     profiler: pg.debug.Profiler, | ||||
|     profiler: Profiler, | ||||
|     **kwargs, | ||||
| 
 | ||||
| ) -> None: | ||||
|  | @ -517,7 +518,7 @@ class Flow(msgspec.Struct):  # , frozen=True): | |||
|         render: bool = True, | ||||
|         array_key: Optional[str] = None, | ||||
| 
 | ||||
|         profiler: Optional[pg.debug.Profiler] = None, | ||||
|         profiler: Optional[Profiler] = None, | ||||
|         do_append: bool = True, | ||||
| 
 | ||||
|         **kwargs, | ||||
|  | @ -528,7 +529,7 @@ class Flow(msgspec.Struct):  # , frozen=True): | |||
|         render to graphics. | ||||
| 
 | ||||
|         ''' | ||||
|         profiler = pg.debug.Profiler( | ||||
|         profiler = Profiler( | ||||
|             msg=f'Flow.update_graphics() for {self.name}', | ||||
|             disabled=not pg_profile_enabled(), | ||||
|             ms_threshold=4, | ||||
|  | @ -948,7 +949,7 @@ class Renderer(msgspec.Struct): | |||
| 
 | ||||
|         new_read, | ||||
|         array_key: str, | ||||
|         profiler: pg.debug.Profiler, | ||||
|         profiler: Profiler, | ||||
|         uppx: float = 1, | ||||
| 
 | ||||
|         # redraw and ds flags | ||||
|  |  | |||
|  | @ -59,6 +59,7 @@ from ..fsp._volume import ( | |||
|     flow_rates, | ||||
| ) | ||||
| from ..log import get_logger | ||||
| from .._profile import Profiler | ||||
| 
 | ||||
| log = get_logger(__name__) | ||||
| 
 | ||||
|  | @ -190,7 +191,7 @@ async def open_fsp_actor_cluster( | |||
| 
 | ||||
|     from tractor._clustering import open_actor_cluster | ||||
| 
 | ||||
|     # profiler = pg.debug.Profiler( | ||||
|     # profiler = Profiler( | ||||
|     #     delayed=False, | ||||
|     #     disabled=False | ||||
|     # ) | ||||
|  | @ -212,7 +213,7 @@ async def run_fsp_ui( | |||
|     target: Fsp, | ||||
|     conf: dict[str, dict], | ||||
|     loglevel: str, | ||||
|     # profiler: pg.debug.Profiler, | ||||
|     # profiler: Profiler, | ||||
|     # _quote_throttle_rate: int = 58, | ||||
| 
 | ||||
| ) -> None: | ||||
|  | @ -746,6 +747,8 @@ async def open_vlm_displays( | |||
|                 }, | ||||
|             ) | ||||
| 
 | ||||
|             dvlm_pi.hideAxis('left') | ||||
|             dvlm_pi.hideAxis('bottom') | ||||
|             # all to be overlayed curve names | ||||
|             fields = [ | ||||
|                'dolla_vlm', | ||||
|  | @ -878,6 +881,7 @@ async def open_vlm_displays( | |||
|                 # keep both regular and dark vlm in view | ||||
|                 names=trade_rate_fields, | ||||
|             ) | ||||
|             tr_pi.hideAxis('bottom') | ||||
| 
 | ||||
|             chart_curves( | ||||
|                 trade_rate_fields, | ||||
|  | @ -951,7 +955,7 @@ async def start_fsp_displays( | |||
|         #     }, | ||||
|         # }, | ||||
|     } | ||||
|     profiler = pg.debug.Profiler( | ||||
|     profiler = Profiler( | ||||
|         delayed=False, | ||||
|         disabled=False | ||||
|     ) | ||||
|  |  | |||
|  | @ -33,6 +33,7 @@ import numpy as np | |||
| import trio | ||||
| 
 | ||||
| from ..log import get_logger | ||||
| from .._profile import Profiler | ||||
| from .._profile import pg_profile_enabled, ms_slower_then | ||||
| # from ._style import _min_points_to_show | ||||
| from ._editors import SelectRect | ||||
|  | @ -779,7 +780,7 @@ class ChartView(ViewBox): | |||
|         ''' | ||||
|         name = self.name | ||||
|         # print(f'YRANGE ON {name}') | ||||
|         profiler = pg.debug.Profiler( | ||||
|         profiler = Profiler( | ||||
|             msg=f'`ChartView._set_yrange()`: `{name}`', | ||||
|             disabled=not pg_profile_enabled(), | ||||
|             ms_threshold=ms_slower_then, | ||||
|  | @ -916,7 +917,7 @@ class ChartView(ViewBox): | |||
|         autoscale_overlays: bool = True, | ||||
|     ): | ||||
| 
 | ||||
|         profiler = pg.debug.Profiler( | ||||
|         profiler = Profiler( | ||||
|             msg=f'ChartView.maybe_downsample_graphics() for {self.name}', | ||||
|             disabled=not pg_profile_enabled(), | ||||
| 
 | ||||
|  |  | |||
|  | @ -32,6 +32,7 @@ from PyQt5.QtGui import QPainterPath | |||
| from .._profile import pg_profile_enabled, ms_slower_then | ||||
| from ._style import hcolor | ||||
| from ..log import get_logger | ||||
| from .._profile import Profiler | ||||
| 
 | ||||
| if TYPE_CHECKING: | ||||
|     from ._chart import LinkedSplits | ||||
|  | @ -170,7 +171,7 @@ class BarItems(pg.GraphicsObject): | |||
| 
 | ||||
|     ) -> None: | ||||
| 
 | ||||
|         profiler = pg.debug.Profiler( | ||||
|         profiler = Profiler( | ||||
|             disabled=not pg_profile_enabled(), | ||||
|             ms_threshold=ms_slower_then, | ||||
|         ) | ||||
|  |  | |||
|  | @ -15,11 +15,15 @@ | |||
| # 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. | ||||
| 
 | ||||
| """ | ||||
| from typing import Optional | ||||
| 
 | ||||
| import pyqtgraph as pg | ||||
| 
 | ||||
| 
 | ||||
|  | @ -46,3 +50,211 @@ def _do_overrides() -> None: | |||
|     """ | ||||
|     # we don't care about potential fp issues inside Qt | ||||
|     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) | ||||
|  |  | |||
|  | @ -28,10 +28,19 @@ from typing import ( | |||
| ) | ||||
| import uuid | ||||
| 
 | ||||
| from pyqtgraph import QtGui | ||||
| 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 ._style import _font_small, hcolor | ||||
| from ._chart import GodWidget | ||||
|  | @ -153,7 +162,7 @@ class MultiStatus: | |||
|             self.bar.clearMessage() | ||||
| 
 | ||||
| 
 | ||||
| class MainWindow(QtGui.QMainWindow): | ||||
| class MainWindow(QMainWindow): | ||||
| 
 | ||||
|     # XXX: for tiling wms this should scale | ||||
|     # with the alloted window size. | ||||
|  | @ -176,12 +185,12 @@ class MainWindow(QtGui.QMainWindow): | |||
|         self._size: Optional[tuple[int, int]] = None | ||||
| 
 | ||||
|     @property | ||||
|     def mode_label(self) -> QtGui.QLabel: | ||||
|     def mode_label(self) -> QLabel: | ||||
| 
 | ||||
|         # init mode label | ||||
|         if not self._status_label: | ||||
| 
 | ||||
|             self._status_label = label = QtGui.QLabel() | ||||
|             self._status_label = label = QLabel() | ||||
|             label.setStyleSheet( | ||||
|                 f"""QLabel {{ | ||||
|                     color : {hcolor('gunmetal')}; | ||||
|  | @ -203,8 +212,7 @@ class MainWindow(QtGui.QMainWindow): | |||
| 
 | ||||
|     def closeEvent( | ||||
|         self, | ||||
| 
 | ||||
|         event: QtGui.QCloseEvent, | ||||
|         event: QCloseEvent, | ||||
| 
 | ||||
|     ) -> None: | ||||
|         '''Cancel the root actor asap. | ||||
|  | @ -244,8 +252,8 @@ class MainWindow(QtGui.QMainWindow): | |||
|     def on_focus_change( | ||||
|         self, | ||||
| 
 | ||||
|         last: QtGui.QWidget, | ||||
|         current: QtGui.QWidget, | ||||
|         last: QWidget, | ||||
|         current: QWidget, | ||||
| 
 | ||||
|     ) -> None: | ||||
| 
 | ||||
|  | @ -256,12 +264,12 @@ class MainWindow(QtGui.QMainWindow): | |||
|             name = getattr(current, 'mode_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). | ||||
| 
 | ||||
|         ''' | ||||
|         app = QtGui.QApplication.instance() | ||||
|         app = QApplication.instance() | ||||
| 
 | ||||
|         for _ in range(3): | ||||
|             screen = app.screenAt(self.pos()) | ||||
|  | @ -294,7 +302,7 @@ class MainWindow(QtGui.QMainWindow): | |||
|         ''' | ||||
|         # https://stackoverflow.com/a/18975846 | ||||
|         if not size and not self._size: | ||||
|             # app = QtGui.QApplication.instance() | ||||
|             # app = QApplication.instance() | ||||
|             geo = self.current_screen().geometry() | ||||
|             h, w = geo.height(), geo.width() | ||||
|             # use approx 1/3 of the area of the screen by default | ||||
|  | @ -331,7 +339,7 @@ class MainWindow(QtGui.QMainWindow): | |||
| 
 | ||||
| 
 | ||||
| # singleton app per actor | ||||
| _qt_win: QtGui.QMainWindow = None | ||||
| _qt_win: QMainWindow = None | ||||
| 
 | ||||
| 
 | ||||
| def main_window() -> MainWindow: | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ | |||
| # `pyqtgraph` peeps keep breaking, fixing, improving so might as well | ||||
| # pin this to a dev branch that we have more control over especially | ||||
| # 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) | ||||
| -e git+https://github.com/pikers/anyio-marketstore.git@master#egg=anyio-marketstore | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue