Skip to content

Subplots

pv.Plotter(shape=(rows, cols)) lays multiple viewports out in one window — each with its own camera, light kit, and background. The bridge translates each renderer to a tile, runs Cycles per-tile, and composites the tiles into a single output PNG.

Side-by-side comparison

import pyvista as pv

pl = pv.Plotter(shape=(1, 2), off_screen=True, window_size=[1200, 600])

pl.subplot(0, 0)
pl.add_mesh(pv.examples.load_random_hills(), cmap="terrain")
pl.add_text("Terrain", position="upper_edge", font_size=14, color="white")
pl.renderer.set_background("#0d1a2f")

pl.subplot(0, 1)
pl.add_mesh(
    pv.examples.load_random_hills(),
    color="#dec27c",
    pbr=True,
    metallic=0.4,
    roughness=0.3,
)
pl.add_text("Metallic", position="upper_edge", font_size=14, color="white")
pl.renderer.set_background("#1a1d22")

pl.blender.render("hills_comparison.png", samples=64)
pl.close()

Each subplot's camera, lights, and background are translated independently. If you don't set a camera explicitly, the bridge calls renderer.reset_camera() so the content fits the tile — same as pyvista's own screenshot().

Per-renderer state

Each pl.subplot(r, c) makes that renderer active. Calls afterwards target it:

Per-renderer API Effect on the tile
pl.add_mesh(...) / pl.add_glyph(...) Actors that appear in the tile
pl.renderer.set_background(...) Tile background colour or gradient
pl.add_light(pv.Light(...)) Light kit for that tile only
pl.camera_position = [...] / "iso" Tile camera pose
pl.show_axes(), pl.show_bounds() HUD elements composited per tile; see below

Layout shapes

Any shape=(rows, cols) works. The output PNG keeps the configured window_size; tiles get equal-sized portions of that.

# Two rows, three columns — six tiles.
pl = pv.Plotter(shape=(2, 3), off_screen=True, window_size=[1500, 800])
for r in range(2):
    for c in range(3):
        pl.subplot(r, c)
        pl.add_mesh(pv.Cube(), color=f"#{r * 80 + c * 40:02x}aacc")
pl.blender.render("six_tiles.png", samples=16)
pl.close()

HUD overlays on subplots

Per-tile HUD compositing is wired into the subplot tile path. After each tile's Cycles render, the bridge switches the plotter's active renderer to that tile (via plotter.subplot(row, col)) and re-runs the HUD compositor at the tile's resolution, so:

  • pl.add_text(...) / pl.add_title(...) annotations sit inside the right tile, not stretched across the full image.
  • pl.show_axes() triads use the tile's camera basis.
  • pl.show_bounds() boxes track the tile's actors and camera.
import pyvista as pv

pl = pv.Plotter(shape=(1, 2), off_screen=True, window_size=[1200, 600])

pl.subplot(0, 0)
pl.add_mesh(pv.examples.load_random_hills(), cmap="terrain")
pl.add_text("Terrain", position="upper_edge", font_size=14, color="white")
pl.show_axes()

pl.subplot(0, 1)
pl.add_mesh(
    pv.examples.load_random_hills(),
    color="#dec27c",
    pbr=True,
    metallic=0.4,
    roughness=0.3,
)
pl.add_text("Metallic", position="upper_edge", font_size=14, color="white")
pl.show_bounds()

pl.blender.render("hills_comparison.png", samples=64)
pl.close()

Per-tile scalar bars

Each tile only draws the scalar bars added under its own pl.subplot(r, c). Under the hood the bridge walks each renderer's GetActors2D() collection to find the vtkScalarBarActor instances that belong to it, then filters pl.scalar_bars accordingly.

import pyvista as pv

pl = pv.Plotter(shape=(1, 2), off_screen=True, window_size=[1200, 600])

pl.subplot(0, 0)
mesh = pv.Sphere()
mesh["height"] = mesh.points[:, 2]
pl.add_mesh(
    mesh,
    scalars="height",
    cmap="viridis",
    show_scalar_bar=True,
    scalar_bar_args={"title": "Sphere height", "color": "white"},
)

pl.subplot(0, 1)
cube = pv.Cube()
cube.cell_data["heat"] = [1, 2, 3, 4, 5, 6]
pl.add_mesh(
    cube,
    scalars="heat",
    cmap="inferno",
    show_scalar_bar=True,
    scalar_bar_args={"title": "Cube heat", "color": "white"},
)

pl.blender.render("two_bars.png", samples=16)
pl.close()

Left tile shows only "Sphere height" (viridis); right tile shows only "Cube heat" (inferno). No cross-tile leakage.

Interactive subplots

pl.blender.show() still uses the single-renderer fast path. The hybrid drag-VTK / settle-Cycles flow doesn't account for per-tile cameras yet; show() on a subplot plotter renders the active renderer only.