piker/.claude/skills/pyqtgraph_rendering_optimiz...

6.6 KiB
Raw Blame History

PyQtGraph Rendering Optimization Skill

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

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

  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() 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 pikers rendering - Coordinate system edge cases encountered - New Qt/pyqtgraph APIs become available


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