Define, compose, and reuse Foxglove layouts entirely from code in Notebooks
Robotics experiments often involve a wide variety of data -- 3D scenes, camera feeds, sensor plots, state transitions, and more. Foxglove's layouts let you arrange panels to visualize all of this data at once, but until now, creating those layouts required manual configuration in the app or loading opaque JSON files exported from a previous session.
Foxglove's new programmatic layout API lets you define layouts entirely in Python. Build layouts that adapt to your data, compose reusable layout fragments, and pass them directly to the embedded Foxglove viewer in your Jupyter notebooks -- all without ever leaving your code.
Until now, using our Jupyter notebook integration has required that you either manually arrange panels in the embedded viewer or preload a layout exported from the app as a JSON file. This approach has some limitations:
The foxglove.layouts module provides a collection of Python dataclasses for constructing layouts. Import the classes you need, compose them into a Layout, and pass it to nb_buffer.show().
Here's the simplest possible layout -- a single Raw Messages panel:
import foxglove
from foxglove.layouts import Layout, RawMessagesPanel
nb_buffer = foxglove.init_notebook_buffer()
# ... log some data ...
raw_message_layout = Layout(content=RawMessagesPanel())
nb_buffer.show(layout=raw_message_layout)

Use SplitContainer to arrange panels side by side (in a row) or stacked vertically (in a column). Each SplitItem specifies the content and what proportion of space it should occupy.
from foxglove.layouts import (
Layout,
SplitContainer,
SplitItem,
ThreeDeePanel,
PlotPanel,
PlotConfig,
PlotSeries,
ImagePanel,
ImageConfig,
ImageModeConfig,
)
layout = Layout(
content=SplitContainer(
direction="row",
items=[
# 3D view on the left, taking up 2/3 of the width
SplitItem(
proportion=2,
content=ThreeDeePanel(),
),
# Right column with image and plot stacked vertically
SplitItem(
proportion=1,
content=SplitContainer(
direction="column",
items=[
SplitItem(
content=ImagePanel(
config=ImageConfig(
image_mode=ImageModeConfig(
image_topic="/camera/rgb"
)
)
),
),
SplitItem(
content=PlotPanel(
config=PlotConfig(
paths=[
PlotSeries(value="/velocity.x", label="vx"),
PlotSeries(value="/velocity.y", label="vy"),
]
)
),
),
],
),
),
],
)
)
nb_buffer.show(layout=layout)
This produces a layout with a 3D panel on the left taking up two-thirds of the width, and a vertically-split right column with a camera image on top and a velocity plot on the bottom.

TabContainer lets you group panels into tabs, which is useful when you have many views but limited screen space:
from foxglove.layouts import (
Layout,
TabContainer,
TabItem,
ThreeDeePanel,
MapPanel,
LogPanel,
)
layout = Layout(
content=TabContainer(
tabs=[
TabItem(title="3D Scene", content=ThreeDeePanel()),
TabItem(title="Map", content=MapPanel()),
TabItem(title="Logs", content=LogPanel()),
]
)
)

One of the most powerful aspects of programmatic layouts is the ability to generate panel configurations based on your data. For example, if you're logging a variable number of sensor topics, you can create a Plot series for each one automatically:
from foxglove.layouts import Layout, PlotPanel, PlotConfig, PlotSeries
# These topics might vary from experiment to experiment
sensor_topics = ["/sensor/temperature", "/sensor/pressure", "/sensor/humidity"]
layout = Layout(
content=PlotPanel(
config=PlotConfig(
paths=[
PlotSeries(value=topic, label=topic.split("/")[-1])
for topic in sensor_topics
]
)
)
)
nb_buffer.show(layout=layout)
Since layouts are just Python objects, you can write functions that return layout fragments and compose them together:
from foxglove.layouts import (
SplitContainer,
SplitItem,
PlotPanel,
PlotConfig,
PlotSeries,
ThreeDeePanel,
ThreeDeeConfig,
ImagePanel,
ImageConfig,
ImageModeConfig,
Content,
)
def make_camera_panel(topic: str) -> ImagePanel:
return ImagePanel(
config=ImageConfig(
image_mode=ImageModeConfig(image_topic=topic)
)
)
def make_plot_for_topics(topics: list[str]) -> PlotPanel:
return PlotPanel(
config=PlotConfig(
paths=[
PlotSeries(value=t, label=t.split("/")[-1])
for t in topics
]
)
)
def robot_debug_layout(
camera_topic: str,
plot_topics: list[str],
follow_frame: str = "base_link",
) -> Content:
return SplitContainer(
direction="row",
items=[
SplitItem(
proportion=2,
content=ThreeDeePanel(
config=ThreeDeeConfig(follow_tf=follow_frame)
),
),
SplitItem(
proportion=1,
content=SplitContainer(
direction="column",
items=[
SplitItem(content=make_camera_panel(camera_topic)),
SplitItem(content=make_plot_for_topics(plot_topics)),
],
),
),
],
)
Now you can call robot_debug_layout() with different parameters for each experiment and get a tailored visualization every time -- no manual layout editing required.
The layout API includes typed classes for all of Foxglove's built-in panels:
Each panel class has a typed config parameter, so your editor's autocomplete and type checking will guide you through the available options.
For panels from custom extensions or any panel type not yet covered by a dedicated class, you can use the generic Panel class:
from foxglove.layouts import Panel
my_panel = Panel(
panel_type="MyCustomPanel",
version=1,
config={"customSetting": True},
)
We're excited about the workflows this unlocks—from fully reproducible experiment notebooks to layout libraries shared across teams. We'll continue expanding the layout API as we add new panel types and configuration options.
Check out the layout API documentation and the Python SDK docs for the full reference.
You can also join our Discord community or follow us on X and LinkedIn to stay up-to-date on all Foxglove news and releases.