6.6 KiB
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:
# Key modules to review:
piker/ui/_curve.py # FlowGraphic, Curve, StepCurve
piker/ui/_editors.py # ArrowEditor, SelectRect
piker/ui/_annotate.py # Custom batch renderersLook 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:
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 primitivesSearch 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
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
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:
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:
# 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:
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
- Don’t mix coordinate systems within single paint call
- Decide per-primitive: data coords or scene coords
- Use
p.transform()/p.resetTransform()carefully
- Don’t forget bounding rect updates
- Override
.boundingRect()to include all primitives - Update when geometry changes via
.prepareGeometryChange()
- Override
- Don’t use ItemCoordinateCache for dynamic content
- Use
DeviceCoordinateCachefor frequently updated items - Or
NoCacheduring interactive operations
- Use
- Don’t trigger updates per-item in loops
- Batch all changes, then single
.update()call
- Batch all changes, then single
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 patternspiker/ui/_annotate.py- GapAnnotations batch rendererpyqtgraph/graphicsItems/BarGraphItem.py- PrimitiveArraypyqtgraph/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