240 lines
6.6 KiB
Markdown
240 lines
6.6 KiB
Markdown
# PyQtGraph Rendering Optimization Skill
|
|
|
|
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 to
|
|
understand existing optimization patterns:
|
|
|
|
```python
|
|
# Key modules to review:
|
|
piker/ui/_curve.py # FlowGraphic, Curve, StepCurve
|
|
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()` methods
|
|
- Cache mode settings (`.setCacheMode()`)
|
|
- Coordinate system transformations (scene vs data vs pixel)
|
|
- Custom bounding rect calculations
|
|
|
|
### 2. Identify Upstream PyQtGraph Patterns
|
|
|
|
Once you understand piker's approach, search `pyqtgraph`
|
|
upstream for similar patterns:
|
|
|
|
**Key upstream modules:**
|
|
```python
|
|
pyqtgraph/graphicsItems/BarGraphItem.py
|
|
# Uses PrimitiveArray for batch rect rendering
|
|
|
|
pyqtgraph/graphicsItems/ScatterPlotItem.py
|
|
# Fragment-based rendering for large point clouds
|
|
|
|
pyqtgraph/functions.py
|
|
# Utility functions like makeArrowPath()
|
|
|
|
pyqtgraph/Qt/internals.py
|
|
# PrimitiveArray for batch drawing primitives
|
|
```
|
|
|
|
**Search techniques:**
|
|
- Look for `PrimitiveArray` usage (batch rect/point rendering)
|
|
- Find `QPainterPath` batching patterns
|
|
- Identify shared pen/brush reuse across items
|
|
- Check for coordinate transformation strategies
|
|
|
|
### 3. Apply Batch Rendering 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 (zoom-sensitive)
|
|
p.setPen(self._rect_pen)
|
|
p.drawRects(*self._rectarray.drawargs())
|
|
|
|
# reset to scene coords for pixel-perfect arrows
|
|
p.resetTransform()
|
|
|
|
# build arrow path in scene/pixel coordinates
|
|
for spec in self._specs:
|
|
# transform data coords to scene
|
|
scene_pt = orig_tr.map(QPointF(x_data, y_data))
|
|
sx, sy = scene_pt.x(), scene_pt.y()
|
|
|
|
# arrow geometry in pixels (zoom-invariant!)
|
|
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!
|
|
```
|
|
|
|
### 6. Positioning and Updates
|
|
|
|
**For annotations that need repositioning:**
|
|
```python
|
|
def reposition(self, array):
|
|
'''
|
|
Update positions based on new array data.
|
|
|
|
'''
|
|
# vectorized timestamp lookups (not linear scans!)
|
|
time_to_row = self._build_lookup(array)
|
|
|
|
# update rect array in-place
|
|
rect_memory = self._rectarray.ndarray()
|
|
for i, spec in enumerate(self._specs):
|
|
row = time_to_row.get(spec['time'])
|
|
if row:
|
|
rect_memory[i, 0] = row['index'] # x
|
|
rect_memory[i, 1] = row['close'] # y
|
|
# ... width, height
|
|
|
|
# trigger repaint
|
|
self.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**
|
|
|
|
## 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()` call
|
|
|
|
## Example: Real-World Optimization
|
|
|
|
**Before (1285 individual pg.ArrowItem + SelectRect):**
|
|
```
|
|
Total creation time: 6.6 seconds
|
|
Per-item overhead: ~5ms
|
|
```
|
|
|
|
**After (single GapAnnotations batch renderer):**
|
|
```
|
|
Total creation time: 104ms (server) + 376ms (client)
|
|
Effective per-item: ~0.08ms
|
|
Speedup: ~36x client, ~180x server
|
|
```
|
|
|
|
## References
|
|
|
|
- `piker/ui/_curve.py` - Production FlowGraphic patterns
|
|
- `piker/ui/_annotate.py` - GapAnnotations batch renderer
|
|
- `pyqtgraph/graphicsItems/BarGraphItem.py` - PrimitiveArray
|
|
- `pyqtgraph/graphicsItems/ScatterPlotItem.py` - Fragments
|
|
- Qt docs: QGraphicsItem caching modes
|
|
|
|
## Skill Maintenance
|
|
|
|
Update this skill when:
|
|
- New batch rendering patterns discovered in pyqtgraph
|
|
- Performance bottlenecks identified in piker's rendering
|
|
- Coordinate system edge cases encountered
|
|
- New Qt/pyqtgraph APIs become available
|
|
|
|
---
|
|
|
|
*Last updated: 2026-01-31*
|
|
*Session: Batch gap annotation optimization*
|