piker/.claude/skills/pyqtgraph-optimization/SKILL.md

5.5 KiB
Raw Permalink Blame History

PyQtGraph Rendering Optimization

Skill for researching and optimizing pyqtgraph graphics primitives by leveraging pikers existing extensions and production-ready patterns.

Research Flow

When tasked with optimizing rendering performance (particularly for large datasets), follow this systematic approach:

1. Study Pikers Existing Primitives

Start by examining piker.ui._curve and related modules:

# 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:

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

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

# 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. Dont mix coordinate systems within single paint call - decide per-primitive: data coords or scene coords. Use p.transform() / p.resetTransform() carefully.

  2. Dont forget bounding rect updates - override .boundingRect() to include all primitives. Update when geometry changes via .prepareGeometryChange().

  3. Dont use ItemCoordinateCache for dynamic content - use DeviceCoordinateCache for frequently updated items or NoCache during interactive operations.

  4. Dont 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 for real-world optimization case studies.


Last updated: 2026-01-31 Session: Batch gap annotation optimization