diff --git a/.utils/README_template.md.j2 b/.utils/README_template.md.j2 index b05897f..21a21d7 100644 --- a/.utils/README_template.md.j2 +++ b/.utils/README_template.md.j2 @@ -18,6 +18,9 @@ Below is a list of all tutorials available in this repository: {%- if tutorial.visualize_url %} - 📊 [Visualize]({{ tutorial.visualize_url }}) {%- endif %} +{%- if tutorial.colab_url %} +- 📓 [Open in Colab]({{ tutorial.colab_url }}) +{%- endif %} {%- endfor %} {% endfor %} Join the Foxglove [Discord](https://discord.gg/UEuytgVkks) and follow [our blog](https://foxglove.dev/blog) for more ideas on how to integrate Foxglove into your robotics development workflows. diff --git a/.utils/generate_readme.py b/.utils/generate_readme.py index 84d46c3..404df0b 100644 --- a/.utils/generate_readme.py +++ b/.utils/generate_readme.py @@ -55,6 +55,7 @@ def scan_tutorials(): "blog_post_url": metadata.get("blog_post_url", ""), "video_url": metadata.get("video_url", ""), "visualize_url": metadata.get("visualize_url", ""), + "colab_url": metadata.get("colab_url", ""), }) return sorted(tutorials, key=lambda t: t["path"]) diff --git a/README.md b/README.md index c587800..d086a04 100644 --- a/README.md +++ b/README.md @@ -93,5 +93,8 @@ Below is a list of all tutorials available in this repository: ### [Analyze Your Robotics Data with Jupyter Notebooks](jupyter_notebooks/data_platform/README.md) - 📝 Load and analyze data in Jupyter Notebooks using Foxglove Data Management - 🔗 [Related Blog Post](https://foxglove.dev/blog/analyze-your-robotics-data-with-jupyter-notebooks) +### [Get Started with the Foxglove Notebook Integration](jupyter_notebooks/getting_started/README.md) +- 📝 Embed the Foxglove viewer in a Jupyter / Colab notebook and drive it with programmatic layouts +- 📓 [Open in Colab](https://colab.research.google.com/github/foxglove/tutorials/blob/main/jupyter_notebooks/getting_started/GettingStarted.ipynb) Join the Foxglove [Discord](https://discord.gg/UEuytgVkks) and follow [our blog](https://foxglove.dev/blog) for more ideas on how to integrate Foxglove into your robotics development workflows. \ No newline at end of file diff --git a/jupyter_notebooks/getting_started/GettingStarted.ipynb b/jupyter_notebooks/getting_started/GettingStarted.ipynb new file mode 100644 index 0000000..eda1b90 --- /dev/null +++ b/jupyter_notebooks/getting_started/GettingStarted.ipynb @@ -0,0 +1,334 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "3cb6f824", + "metadata": {}, + "source": [ + "# Get Started with the Foxglove Notebook Integration\n", + "\n", + "[Foxglove](https://foxglove.dev/) is a multimodal robotics observability platform. The [Python SDK](https://docs.foxglove.dev/docs/getting-started/python) ships an optional **notebook integration** that lets you embed a fully-featured Foxglove viewer directly inside Jupyter, JupyterLab, Google Colab, or VS Code.\n", + "\n", + "This notebook is a runnable companion to the [Foxglove notebook integration docs](https://docs.foxglove.dev/docs/notebook). It walks through:\n", + "\n", + "1. Installing the integration\n", + "2. Creating a notebook buffer and logging messages\n", + "3. Building layouts programmatically with the `foxglove.layouts` API\n" + ] + }, + { + "cell_type": "markdown", + "id": "f09dc6bc", + "metadata": {}, + "source": [ + "## Install the integration\n", + "\n", + "The notebook integration is shipped as the optional `notebook` extra of the [`foxglove-sdk`](https://pypi.org/project/foxglove-sdk/) package. In Colab or any other fresh kernel, install it like this:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b47856b0", + "metadata": {}, + "outputs": [], + "source": [ + "%pip install -q \"foxglove-sdk[notebook]\"" + ] + }, + { + "cell_type": "markdown", + "id": "b56daf55", + "metadata": {}, + "source": [ + "## Using the integration\n", + "\n", + "Once the integration is installed, create a notebook buffer with [`foxglove.init_notebook_buffer()`](https://docs.foxglove.dev/docs/notebook#using-the-integration). The buffer collects every message you log to the SDK's default context. When you're ready to visualize the data, call `nb_buffer.show()` — **it must be the last expression in the cell** so Jupyter renders the embedded widget.\n", + "\n", + "> ⚠️ **Always set `log_time` explicitly** when calling `foxglove.log(...)`. If you don't, the SDK uses the wall-clock time at the moment each message is logged, which can lead to surprising playback behavior in the viewer.\n", + "\n", + "The example below logs 300 `count` messages on the `/hello` topic, spaced 33 ms apart (roughly 10 seconds of buffered data), then displays the embedded Foxglove viewer." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d4860aa2", + "metadata": {}, + "outputs": [], + "source": [ + "import foxglove\n", + "from foxglove.layouts import Layout, RawMessagesConfig, RawMessagesPanel\n", + "\n", + "# Create a notebook buffer to collect messages.\n", + "nb_buffer = foxglove.init_notebook_buffer()\n", + "\n", + "# Log 300 messages spaced 33ms apart (~10 seconds of data).\n", + "for t in range(10 * 30):\n", + " timestamp = t * 0.033\n", + " foxglove.log(\"/hello\", {\"count\": t}, log_time=int(timestamp * 1e9))\n", + "\n", + "# Pre-configure a Raw Messages panel pointed at /hello so the embedded\n", + "# viewer shows the buffered data immediately instead of an empty default layout.\n", + "layout = Layout(\n", + " content=RawMessagesPanel(\n", + " title=\"/hello\",\n", + " config=RawMessagesConfig(topic_path=\"/hello\"),\n", + " ),\n", + ")\n", + "\n", + "# Display the data in the embedded Foxglove viewer.\n", + "# This must be the last expression in the cell for the widget to render.\n", + "nb_buffer.show(layout=layout)" + ] + }, + { + "cell_type": "markdown", + "id": "435a2b05", + "metadata": {}, + "source": [ + "### Logging richer data\n", + "\n", + "The SDK ships typed channels for the [Foxglove schemas](https://docs.foxglove.dev/docs/visualization/message-schemas/introduction) so you can log 3D scenes, images, point clouds, and more — and they all render in the embedded viewer just like they would in the desktop app.\n", + "\n", + "Here we log an animated cube on the `/scene` topic. We'll reuse this data later when we build a custom layout." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7e59a45e", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "import foxglove\n", + "from foxglove.channels import SceneUpdateChannel\n", + "from foxglove.layouts import (\n", + " BaseRendererGridLayerSettings,\n", + " BaseRendererSceneUpdateTopicSettings,\n", + " Layout,\n", + " ThreeDeeConfig,\n", + " ThreeDeePanel,\n", + ")\n", + "from foxglove.messages import (\n", + " Color,\n", + " CubePrimitive,\n", + " SceneEntity,\n", + " SceneUpdate,\n", + " Vector3,\n", + ")\n", + "\n", + "# Start with a fresh buffer so this cell stands on its own.\n", + "nb_buffer = foxglove.init_notebook_buffer()\n", + "\n", + "scene_channel = SceneUpdateChannel(\"/scene\")\n", + "\n", + "# Animate a cube whose size pulses over ~10 seconds.\n", + "for t in range(10 * 30):\n", + " timestamp = t * 0.033\n", + " size = abs(math.sin(timestamp)) + 1.0\n", + "\n", + " # Also log a scalar so we have something to plot later.\n", + " foxglove.log(\"/hello\", {\"count\": t}, log_time=int(timestamp * 1e9))\n", + "\n", + " scene_channel.log(\n", + " SceneUpdate(\n", + " entities=[\n", + " SceneEntity(\n", + " cubes=[\n", + " CubePrimitive(\n", + " size=Vector3(x=size, y=size, z=size),\n", + " color=Color(r=1.0, g=0.8, b=0.0, a=1.0),\n", + " ),\n", + " ],\n", + " ),\n", + " ],\n", + " ),\n", + " log_time=int(timestamp * 1e9),\n", + " )\n", + "\n", + "# Pre-configure a 3D panel subscribed to /scene so the embedded viewer\n", + "# immediately renders the animated cube instead of an empty default layout.\n", + "layout = Layout(\n", + " content=ThreeDeePanel(\n", + " title=\"Scene\",\n", + " config=ThreeDeeConfig(\n", + " layers={\"grid\": BaseRendererGridLayerSettings()},\n", + " topics={\"/scene\": BaseRendererSceneUpdateTopicSettings(visible=True)},\n", + " ),\n", + " ),\n", + ")\n", + "\n", + "nb_buffer.show(layout=layout)" + ] + }, + { + "cell_type": "markdown", + "id": "11460217", + "metadata": {}, + "source": [ + "## Layout management\n", + "\n", + "A [layout](https://docs.foxglove.dev/docs/visualization/layouts/) is the arrangement of panels — 3D scenes, plots, images, raw messages, etc. — that turns raw data into an opinionated view of your robot. The SDK ships a Python API in [`foxglove.layouts`](https://docs.foxglove.dev/docs/notebook/layouts) for building layouts programmatically and passing them to the embedded viewer via `nb_buffer.show(layout=...)`.\n", + "\n", + "The simplest layout is a single panel. Below we build one with a `MarkdownPanel` plus a global variable, mirroring the example from the docs." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a874a71", + "metadata": {}, + "outputs": [], + "source": [ + "import foxglove\n", + "from foxglove.layouts import Layout, MarkdownConfig, MarkdownPanel\n", + "\n", + "layout = Layout(\n", + " content=MarkdownPanel(\n", + " config=MarkdownConfig(markdown=\"Hello, world!\"),\n", + " ),\n", + " variables={\"my_variable\": 1},\n", + ")\n", + "\n", + "nb_buffer = foxglove.init_notebook_buffer()\n", + "nb_buffer.show(layout=layout)" + ] + }, + { + "cell_type": "markdown", + "id": "cb40cd1d", + "metadata": {}, + "source": [ + "### Composing panels with containers\n", + "\n", + "Real layouts usually have multiple panels arranged side-by-side or in tabs. The `foxglove.layouts` module ships three containers for that:\n", + "\n", + "- [`SplitContainer`](https://docs.foxglove.dev/docs/notebook/layouts) — arranges children side-by-side in a `\"row\"` or `\"column\"`.\n", + "- [`TabContainer`](https://docs.foxglove.dev/docs/notebook/layouts) — arranges children in tabs, with one visible at a time.\n", + "- [`StackContainer`](https://docs.foxglove.dev/docs/notebook/layouts) — arranges children in a vertically-scrolling list.\n", + "\n", + "Below we re-log the cube and counter from the previous cells, then build a layout that puts the 3D scene and a plot of `/hello.count` side-by-side inside a tab. This gives us a small but realistic example of nested containers driving real data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "821f6f93", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "import foxglove\n", + "from foxglove.channels import SceneUpdateChannel\n", + "from foxglove.layouts import (\n", + " BaseRendererGridLayerSettings,\n", + " BaseRendererSceneUpdateTopicSettings,\n", + " Layout,\n", + " PlotConfig,\n", + " PlotPanel,\n", + " PlotSeries,\n", + " SplitContainer,\n", + " SplitItem,\n", + " TabContainer,\n", + " TabItem,\n", + " ThreeDeeConfig,\n", + " ThreeDeePanel,\n", + ")\n", + "from foxglove.messages import (\n", + " Color,\n", + " CubePrimitive,\n", + " SceneEntity,\n", + " SceneUpdate,\n", + " Vector3,\n", + ")\n", + "\n", + "# Fresh buffer + re-log the data so this cell is self-contained.\n", + "nb_buffer = foxglove.init_notebook_buffer()\n", + "scene_channel = SceneUpdateChannel(\"/scene\")\n", + "\n", + "for t in range(10 * 30):\n", + " timestamp = t * 0.033\n", + " size = abs(math.sin(timestamp)) + 1.0\n", + "\n", + " foxglove.log(\"/hello\", {\"count\": t}, log_time=int(timestamp * 1e9))\n", + "\n", + " scene_channel.log(\n", + " SceneUpdate(\n", + " entities=[\n", + " SceneEntity(\n", + " cubes=[\n", + " CubePrimitive(\n", + " size=Vector3(x=size, y=size, z=size),\n", + " color=Color(r=0.1, g=0.7, b=1.0, a=1.0),\n", + " ),\n", + " ],\n", + " ),\n", + " ],\n", + " ),\n", + " log_time=int(timestamp * 1e9),\n", + " )\n", + "\n", + "# A 3D panel showing the cube on /scene, with a base grid layer for context.\n", + "scene_panel = ThreeDeePanel(\n", + " title=\"Scene\",\n", + " config=ThreeDeeConfig(\n", + " layers={\"grid\": BaseRendererGridLayerSettings()},\n", + " topics={\"/scene\": BaseRendererSceneUpdateTopicSettings(visible=True)},\n", + " ),\n", + ")\n", + "\n", + "# A plot panel showing /hello.count over time.\n", + "plot_panel = PlotPanel(\n", + " title=\"Counter\",\n", + " config=PlotConfig(\n", + " paths=[PlotSeries(value=\"/hello.count\", enabled=True)],\n", + " show_legend=True,\n", + " ),\n", + ")\n", + "\n", + "# Side-by-side row, wrapped in a single-tab TabContainer to demonstrate both APIs.\n", + "split = SplitContainer(\n", + " direction=\"row\",\n", + " items=[\n", + " SplitItem(content=scene_panel),\n", + " SplitItem(content=plot_panel),\n", + " ],\n", + ")\n", + "tabs = TabContainer(tabs=[TabItem(title=\"Overview\", content=split)])\n", + "\n", + "layout = Layout(content=tabs)\n", + "nb_buffer.show(layout=layout)" + ] + }, + { + "cell_type": "markdown", + "id": "191d273b", + "metadata": {}, + "source": [ + "## Where to next\n", + "\n", + "- 📖 [Notebook integration docs](https://docs.foxglove.dev/docs/notebook) — the full reference, including buffer management and updating an existing viewer with `viewer.refresh()`.\n", + "- 🧱 [Layouts API reference](https://docs.foxglove.dev/docs/notebook/layouts) — every panel and container the `foxglove.layouts` module ships.\n", + "- 🐍 [Python SDK getting started](https://docs.foxglove.dev/docs/getting-started/python) — using the SDK outside of notebooks.\n", + "- 📊 [`data_platform` notebook](../data_platform/README.md) — a more advanced example that pulls data from Foxglove Data Management for offline analysis.\n", + "- 💬 [Foxglove Discord](https://foxglove.dev/community) — questions, feedback, and what we're working on next.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/jupyter_notebooks/getting_started/README.md b/jupyter_notebooks/getting_started/README.md new file mode 100644 index 0000000..52ce511 --- /dev/null +++ b/jupyter_notebooks/getting_started/README.md @@ -0,0 +1,34 @@ +--- +title: "Get Started with the Foxglove Notebook Integration" +short_description: "Embed the Foxglove viewer in a Jupyter / Colab notebook and drive it with programmatic layouts" +colab_url: "https://colab.research.google.com/github/foxglove/tutorials/blob/main/jupyter_notebooks/getting_started/GettingStarted.ipynb" +--- +# Foxglove Notebook Integration — Getting Started + +A minimal, runnable companion to the [Foxglove notebook integration docs](https://docs.foxglove.dev/docs/notebook). + +This notebook walks through: + +- Installing `foxglove-sdk[notebook]` +- Creating a notebook buffer with `foxglove.init_notebook_buffer()` +- Logging messages and rendering them with the embedded Foxglove viewer +- Building layouts programmatically with the [`foxglove.layouts`](https://docs.foxglove.dev/docs/notebook/layouts) API, including nested `SplitContainer` and `TabContainer` examples driving real data + +## Run in Colab + +Launch the sample notebook directly in Colab. + +[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/foxglove/tutorials/blob/main/jupyter_notebooks/getting_started/GettingStarted.ipynb) + +## Run locally + +```bash +pip install "foxglove-sdk[notebook]" jupyterlab +jupyter lab GettingStarted.ipynb +``` + +The notebook works in JupyterLab, classic Jupyter, VS Code, and Google Colab — anywhere [`anywidget`](https://anywidget.dev/) is supported. + +## Stay in touch + +Join our [Discord](https://foxglove.dev/community) to ask questions, share feedback, and stay up to date on what our team is working on.