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