220 lines
5.5 KiB
Markdown
220 lines
5.5 KiB
Markdown
|
|
---
|
||
|
|
name: pyqtgraph-optimization
|
||
|
|
description: >
|
||
|
|
PyQtGraph batch rendering optimization patterns
|
||
|
|
for piker's UI. Apply when optimizing graphics
|
||
|
|
performance, adding new chart annotations, or
|
||
|
|
working with `QGraphicsItem` subclasses.
|
||
|
|
user-invocable: false
|
||
|
|
---
|
||
|
|
|
||
|
|
# PyQtGraph Rendering Optimization
|
||
|
|
|
||
|
|
Skill for researching and optimizing `pyqtgraph`
|
||
|
|
graphics primitives by leveraging `piker`'s
|
||
|
|
existing extensions and production-ready patterns.
|
||
|
|
|
||
|
|
## Research Flow
|
||
|
|
|
||
|
|
When tasked with optimizing rendering performance
|
||
|
|
(particularly for large datasets), follow this
|
||
|
|
systematic approach:
|
||
|
|
|
||
|
|
### 1. Study Piker's Existing Primitives
|
||
|
|
|
||
|
|
Start by examining `piker.ui._curve` and related
|
||
|
|
modules:
|
||
|
|
|
||
|
|
```python
|
||
|
|
# Key modules to review:
|
||
|
|
piker/ui/_curve.py # FlowGraphic, Curve
|
||
|
|
piker/ui/_editors.py # ArrowEditor, SelectRect
|
||
|
|
piker/ui/_annotate.py # Custom batch renderers
|
||
|
|
```
|
||
|
|
|
||
|
|
**Look for:**
|
||
|
|
- Use of `QPainterPath` for batch path rendering
|
||
|
|
- `QGraphicsItem` subclasses with custom `.paint()`
|
||
|
|
- Cache mode settings (`.setCacheMode()`)
|
||
|
|
- Coordinate system transformations
|
||
|
|
- Custom bounding rect calculations
|
||
|
|
|
||
|
|
### 2. Identify Upstream PyQtGraph Patterns
|
||
|
|
|
||
|
|
**Key upstream modules:**
|
||
|
|
```python
|
||
|
|
pyqtgraph/graphicsItems/BarGraphItem.py
|
||
|
|
# PrimitiveArray for batch rect rendering
|
||
|
|
|
||
|
|
pyqtgraph/graphicsItems/ScatterPlotItem.py
|
||
|
|
# Fragment-based rendering for point clouds
|
||
|
|
|
||
|
|
pyqtgraph/functions.py
|
||
|
|
# Utility fns like makeArrowPath()
|
||
|
|
|
||
|
|
pyqtgraph/Qt/internals.py
|
||
|
|
# PrimitiveArray for batch drawing primitives
|
||
|
|
```
|
||
|
|
|
||
|
|
**Search for:**
|
||
|
|
- `PrimitiveArray` usage (batch rect/point)
|
||
|
|
- `QPainterPath` batching patterns
|
||
|
|
- Shared pen/brush reuse across items
|
||
|
|
- Coordinate transformation strategies
|
||
|
|
|
||
|
|
### 3. Core Batch Patterns
|
||
|
|
|
||
|
|
**Core optimization principle:**
|
||
|
|
Creating individual `QGraphicsItem` instances is
|
||
|
|
expensive. Batch rendering eliminates per-item
|
||
|
|
overhead.
|
||
|
|
|
||
|
|
#### Pattern: Batch Rectangle Rendering
|
||
|
|
|
||
|
|
```python
|
||
|
|
import pyqtgraph as pg
|
||
|
|
from pyqtgraph.Qt import QtCore
|
||
|
|
|
||
|
|
class BatchRectRenderer(pg.GraphicsObject):
|
||
|
|
def __init__(self, n_items):
|
||
|
|
super().__init__()
|
||
|
|
|
||
|
|
# allocate rect array once
|
||
|
|
self._rectarray = (
|
||
|
|
pg.Qt.internals.PrimitiveArray(
|
||
|
|
QtCore.QRectF, 4,
|
||
|
|
)
|
||
|
|
)
|
||
|
|
|
||
|
|
# shared pen/brush (not per-item!)
|
||
|
|
self._pen = pg.mkPen(
|
||
|
|
'dad_blue', width=1,
|
||
|
|
)
|
||
|
|
self._brush = (
|
||
|
|
pg.functions.mkBrush('dad_blue')
|
||
|
|
)
|
||
|
|
|
||
|
|
def paint(self, p, opt, w):
|
||
|
|
# batch draw all rects in single call
|
||
|
|
p.setPen(self._pen)
|
||
|
|
p.setBrush(self._brush)
|
||
|
|
drawargs = self._rectarray.drawargs()
|
||
|
|
p.drawRects(*drawargs) # all at once!
|
||
|
|
```
|
||
|
|
|
||
|
|
#### Pattern: Batch Path Rendering
|
||
|
|
|
||
|
|
```python
|
||
|
|
class BatchPathRenderer(pg.GraphicsObject):
|
||
|
|
def __init__(self):
|
||
|
|
super().__init__()
|
||
|
|
self._path = QtGui.QPainterPath()
|
||
|
|
|
||
|
|
def paint(self, p, opt, w):
|
||
|
|
# single path draw for all geometry
|
||
|
|
p.setPen(self._pen)
|
||
|
|
p.setBrush(self._brush)
|
||
|
|
p.drawPath(self._path)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 4. Handle Coordinate Systems Carefully
|
||
|
|
|
||
|
|
**Scene vs Data vs Pixel coordinates:**
|
||
|
|
|
||
|
|
```python
|
||
|
|
def paint(self, p, opt, w):
|
||
|
|
# save original transform (data -> scene)
|
||
|
|
orig_tr = p.transform()
|
||
|
|
|
||
|
|
# draw rects in data coordinates
|
||
|
|
p.setPen(self._rect_pen)
|
||
|
|
p.drawRects(*self._rectarray.drawargs())
|
||
|
|
|
||
|
|
# reset to scene coords for pixel-perfect
|
||
|
|
p.resetTransform()
|
||
|
|
|
||
|
|
# build arrow path in scene/pixel coords
|
||
|
|
for spec in self._specs:
|
||
|
|
scene_pt = orig_tr.map(
|
||
|
|
QPointF(x_data, y_data),
|
||
|
|
)
|
||
|
|
sx, sy = scene_pt.x(), scene_pt.y()
|
||
|
|
|
||
|
|
# arrow geometry in pixels (zoom-safe!)
|
||
|
|
arrow_poly = QtGui.QPolygonF([
|
||
|
|
QPointF(sx, sy), # tip
|
||
|
|
QPointF(sx - 2, sy - 10), # left
|
||
|
|
QPointF(sx + 2, sy - 10), # right
|
||
|
|
])
|
||
|
|
arrow_path.addPolygon(arrow_poly)
|
||
|
|
|
||
|
|
p.drawPath(arrow_path)
|
||
|
|
|
||
|
|
# restore data coordinate system
|
||
|
|
p.setTransform(orig_tr)
|
||
|
|
```
|
||
|
|
|
||
|
|
### 5. Minimize Redundant State
|
||
|
|
|
||
|
|
**Share resources across all items:**
|
||
|
|
```python
|
||
|
|
# GOOD: one pen/brush for all items
|
||
|
|
self._shared_pen = pg.mkPen(color, width=1)
|
||
|
|
self._shared_brush = (
|
||
|
|
pg.functions.mkBrush(color)
|
||
|
|
)
|
||
|
|
|
||
|
|
# BAD: creating per-item (memory + time waste!)
|
||
|
|
for item in items:
|
||
|
|
item.setPen(pg.mkPen(color, width=1)) # NO!
|
||
|
|
```
|
||
|
|
|
||
|
|
## Common Pitfalls
|
||
|
|
|
||
|
|
1. **Don't mix coordinate systems within single
|
||
|
|
paint call** - decide per-primitive: data coords
|
||
|
|
or scene coords. Use `p.transform()` /
|
||
|
|
`p.resetTransform()` carefully.
|
||
|
|
|
||
|
|
2. **Don't forget bounding rect updates** -
|
||
|
|
override `.boundingRect()` to include all
|
||
|
|
primitives. Update when geometry changes via
|
||
|
|
`.prepareGeometryChange()`.
|
||
|
|
|
||
|
|
3. **Don't use ItemCoordinateCache for dynamic
|
||
|
|
content** - use `DeviceCoordinateCache` for
|
||
|
|
frequently updated items or `NoCache` during
|
||
|
|
interactive operations.
|
||
|
|
|
||
|
|
4. **Don't trigger updates per-item in loops** -
|
||
|
|
batch all changes, then single `.update()`.
|
||
|
|
|
||
|
|
## Performance Expectations
|
||
|
|
|
||
|
|
**Individual items (baseline):**
|
||
|
|
- 1000+ items: ~5+ seconds to create
|
||
|
|
- Each item: ~5ms overhead (Qt object creation)
|
||
|
|
|
||
|
|
**Batch rendering (optimized):**
|
||
|
|
- 1000+ items: <100ms to create
|
||
|
|
- Single item: ~0.01ms per primitive in batch
|
||
|
|
- **Expected: 50-100x speedup**
|
||
|
|
|
||
|
|
## References
|
||
|
|
|
||
|
|
- `piker/ui/_curve.py` - Production FlowGraphic
|
||
|
|
- `piker/ui/_annotate.py` - GapAnnotations batch
|
||
|
|
- `pyqtgraph/graphicsItems/BarGraphItem.py` -
|
||
|
|
PrimitiveArray
|
||
|
|
- `pyqtgraph/graphicsItems/ScatterPlotItem.py` -
|
||
|
|
Fragments
|
||
|
|
- Qt docs: QGraphicsItem caching modes
|
||
|
|
|
||
|
|
See [examples.md](examples.md) for real-world
|
||
|
|
optimization case studies.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
*Last updated: 2026-01-31*
|
||
|
|
*Session: Batch gap annotation optimization*
|