Announcing Programmatic Layouts in Foxglove

Define, compose, and reuse Foxglove layouts entirely from code in Notebooks

product release

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.

The problem

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:

  • Data-dependent layouts: What if you want your layout to depend on the data? For example, adding a Plot panel with a series for each topic you're logging -- which may vary from experiment to experiment.
  • Composability: What if you want to combine pieces of layouts to create a new layout for a specific visualization? Merging exported JSON files by hand is tedious and error-prone, since the internal format includes implementation details that aren't meant for manual editing.
  • Reproducibility: What if you want your notebook to be fully self-contained, so that anyone running it sees exactly the same visualization without needing a separate layout file?

Getting started with programmatic layouts

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().

A simple layout

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)

Arranging panels with splits

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.

Tabs for organizing related views

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()),
        ]
    )
)

Data-dependent layouts

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)

Composing layout fragments

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.

Available panels

The layout API includes typed classes for all of Foxglove's built-in panels:

Panel Class Description
3DThreeDeePanelDisplay markers, meshes, URDFs, and more in a 3D scene
ImageImagePanelDisplay annotated camera images and video
PlotPlotPanelPlot numerical values over time or other values
State TransitionsStateTransitionsPanelTrack when values change over time
Raw MessagesRawMessagesPanelInspect topic messages
LogLogPanelDisplay logs by node and severity level
MapMapPanelDisplay GPS points on a map
IndicatorIndicatorPanelDisplay a colored indicator based on a threshold value
GaugeGaugePanelDisplay a colored gauge based on a continuous value
MarkdownMarkdownPanelWrite documentation and notes in Markdown
TableTablePanelDisplay topic messages in a tabular format
Diagnostics SummaryDiagnosticsSummaryPanelDisplay a summary of ROS DiagnosticArray messages
Diagnostics DetailDiagnosticsDetailPanelDisplay detailed ROS DiagnosticArray messages
TeleopTeleopPanelTeleoperate a robot over a live connection
PublishPublishPanelPublish messages to the data source
Service CallServiceCallPanelCall a service and view the result
AudioAudioPanelPlay audio streams
User ScriptsUserScriptsPanelWrite custom data transformations in TypeScript
ParametersParametersPanelRead and set parameters for a data source
Variable SliderVariableSliderPanelUpdate numerical variable values
Data Source InfoDataSourceInfoPanelView details for the current data source
Topic GraphTopicGraphPanelDisplay a graph of active nodes, topics, and services
Transform TreeTransformTreePanelDisplay the transform tree

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},
)

Stay tuned

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.

Read more

Start building with Foxglove.

Get started for free