skills/rerun-blueprint/SKILL.md
A blueprint decides how a recording is shown; the recording decides what exists.
Read the data, design a first layout, then iterate from screenshots until it
reads at a glance. The constructors are discoverable (dir(rrb),
help(rrb.Spatial3DView)); this skill covers what you can't guess. Import as
import rerun.blueprint as rrb.
Enumerate every (entity_path, archetype) pair first — the archetype picks the
view, the entity path scopes it. For a dataset (see rerun-catalog-queries):
for c in dataset.schema().component_columns():
print(c.entity_path, c.archetype, c.component_name)
For a local recording, stream it with RrdReader and read entity_path and
the archetype off each chunk (see rerun-chunk-processing).
| Archetype family | View |
|---|---|
Points2D, Image, EncodedImage, Boxes2D, LineStrips2D, Pinhole projections | Spatial2DView |
Points3D, Mesh3D, Boxes3D, LineStrips3D, Transform3D, Asset3D | Spatial3DView |
Scalars, SeriesLines, SeriesPoints | TimeSeriesView |
TextLog / TextDocument / BarChart | TextLogView / TextDocumentView / BarChartView |
Tensor, DepthImage (heatmap) | TensorView |
GeoPoints, GeoLineStrings | MapView |
GraphNodes, GraphEdges | GraphView |
| tabular / catalog data | DataframeView |
This step is just deciding which entities share a view. Sizing and placement are mechanics you tune by eye in the screenshot loop, not rules to settle up front.
DataframeView, or get left out, rather than a pane each.Transform3D tree goes in one
Spatial3DView at the common ancestor; a Pinhole camera gets its own
Spatial2DView rooted at the camera so images inherit the projection.Scalars under a prefix → one TimeSeriesView over
the prefix, not one each. Split only when value ranges clash.column_shares/row_shares are relative weights on a container's
children, equal by default. Tune them from the screenshots.Containers (Grid, Horizontal, Vertical, Tabs) hold views. Default a flat
set to Grid; use the others for a deliberate split, Tabs for alternatives
competing for one slot (left/right/depth cameras). Always set an explicit
origin and name — origin defaults to /, which dumps the whole tree into
one view (the usual cause of an unreadable blob).
blueprint = rrb.Blueprint(
rrb.Horizontal(
rrb.Spatial3DView(origin="/world", name="Scene"),
rrb.Vertical(
rrb.Spatial2DView(origin="/world/camera", name="Camera"),
rrb.TimeSeriesView(origin="/sensors", name="Sensors"),
),
column_shares=[3, 2],
),
rrb.TextLogView(origin="/logs", name="Logs"),
collapse_panels=True,
)
contents defaults to "$origin/**". Scope a view with include/exclude rules,
e.g. contents=["+ $origin/**", "- $origin/internal/**"]. A bare line is an
include; /** is the only wildcard (matches a subtree). Most-specific rule wins,
ties go to the last, unmatched paths are excluded.
Coordinate frames. A spatial view only renders entities it can place relative
to its target frame. Transform3D/Pinhole logged on entities compose down the
tree, so origin is enough. But named frames (CoordinateFrame(frame=...),
common in ROS/MCAP) live in a separate frame graph — point the view at a frame
the data occupies via spatial_information=rrb.SpatialInformation(target_frame="<frame>").
The tell is an empty 3D view with "No transform path from <frame>…" errors:
origin="/" targets the root tf#/, which connects to nothing if the tf tree
was never materialized. Read the CoordinateFrame:frame values, target the one
the main 3D content sits in, and exclude entities in unconnected frames.
Spawn the viewer and load the recording once, then re-send blueprints into the same viewer; each send + screenshot is one cheap iteration. Render, look, revise.
The blueprint binds to the data by store identity, both application id and
recording id. A loaded .rrd keeps its own identity, so build the stream from
the rrd's values (set both; recording_id otherwise defaults to a random one).
Mismatch either and the viewer keeps blueprint and data as separate recordings
and never applies the blueprint.
import time
import rerun as rr
import rerun.blueprint as rrb
from rerun.experimental import ViewerClient, RrdReader
store = RrdReader("segment.rrd").recordings()[0] # the rrd's application id + recording id
with ViewerClient.spawn(headless=True, port=9879) as viewer:
rec = rr.RecordingStream(store.application_id, recording_id=store.recording_id)
rec.connect_grpc(url=viewer.url)
rec.log_file_from_path("segment.rrd")
def shot(blueprint, path):
rec.send_blueprint(blueprint, make_active=True, make_default=True)
time.sleep(2) # let the import finish and a frame render; bump if a view is blank
viewer.save_screenshot(path) # view_id=view.id for a single view
shot(blueprint_v1, "bp_v1.png")
# Read bp_v1.png, revise, send the next one into the same viewer.
shot(blueprint_v2, "bp_v2.png")
To bake a finished blueprint in instead, pass default_blueprint= to rr.init /
spawn / connect_grpc / save, or blueprint= to notebook_show.
DataframeView shows "Unknown timeline" without a query:
query=rrb.archetypes.DataframeQuery(timeline="<timeline>", apply_latest_at=True).rrb.TimePanel(play_state=rrb.components.PlayState.Following)).rrb views and ViewerClient (rerun.experimental) are unstable. Check
help() if a constructor argument is rejected.rerun-data-model — entities, archetypes, timelines.rerun-catalog-queries — enumerate entities in a dataset.