--- 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*