Skip to content

Chapter 4: XVIZPanel#

Welcome back to the streetscape.gl tutorial! So far, we've learned how to load autonomous vehicle (AV) log data using the XVIZ Loader Interface (Chapter 1), visualize the main 3D scene in the LogViewer (Chapter 2), and control playback through time with PlaybackControl (Chapter 3).

These components are great for seeing the vehicle's environment and trajectory, but AV logs contain a lot more information than just 3D positions! They include metrics like speed, acceleration, system status, detailed object properties, plots of values over time, and even raw camera feeds. How do you display all this additional, non-3D data alongside the main visualization?

This is where the XVIZPanel component comes in.

Think of the XVIZPanel as a dynamic dashboard module that can display various types of information widgets. But here's the clever part: what widgets are shown and how they are organized is defined within the XVIZ log data itself. This is done using something called the Declarative UI specification.

The Declarative UI allows the creator of the XVIZ log to define specific UI elements – like metrics, plots, tables, or videos – and group them into named "panels". When streetscape.gl loads the log, it reads this definition from the log's metadata. The XVIZPanel component then uses this definition to automatically build and display the specified UI elements, populated with data from the log at the current playback time.

This means you don't have to hardcode complex dashboards in your application for every single log format. If the log includes Declarative UI definitions, XVIZPanel can render it automatically, providing a rich, data-driven user interface alongside your 3D view.

What is XVIZPanel?#

XVIZPanel is a React component that reads a specific "panel" definition from the loaded XVIZ log's metadata and renders the corresponding UI widgets. It's the part of streetscape.gl that brings the Declarative UI from the log into your application's interface.

Its primary job is to:

  1. Look up a panel definition by its name in the log's metadata.
  2. Interpret the structure and components defined in that panel.
  3. Render the appropriate streetscape.gl UI components (like components for metrics, plots, tables, videos) based on the definition.
  4. Ensure these components are populated with data from the log's current frame.

Using the XVIZPanel#

To use the XVIZPanel component, you need two main things:

  1. An instance of an object that implements the XVIZ Loader Interface (your logLoader from Chapter 1).
  2. The name of the specific panel you want to display, as defined in the XVIZ log's metadata.

Here's how you might add an XVIZPanel to your application, typically alongside the LogViewer and PlaybackControl:

import React from 'react';
import { LogViewer, PlaybackControl, XVIZPanel } from 'streetscape.gl';
import { XVIZFileLoader } from 'streetscape.gl'; // Or your preferred loader

// Assume 'logLoader' is already created and connected
// const logLoader = new XVIZFileLoader({...});
// logLoader.connect();

// Dummy loader for example structure - replace with your actual loader setup
const logLoader = new XVIZFileLoader({ /* ... options ... */ });
logLoader.connect();

const MAPBOX_TOKEN = 'YOUR_MAPBOX_ACCESS_TOKEN';

function CompleteLogDisplayWithPanels() {
  // Assume the loaded log has Declarative UI panels named "Metrics" and "Camera"
  return (
    <div>
      <div style={{ display: 'flex', height: '60vh' }}>
        {/* The LogViewer for the 3D scene */}
        <div style={{ flex: 1 }}>
          <LogViewer
            log={logLoader}
            mapboxApiAccessToken={MAPBOX_TOKEN}
            // ... other LogViewer props ...
          />
        </div>

        {/* The container for your XVIZ Panels */}
        <div style={{ width: '300px', overflowY: 'auto', padding: '10px' }}>
          {/* Render the "Metrics" panel */}
          <XVIZPanel log={logLoader} name="Metrics" />
          <hr /> {/* Separator */}
          {/* Render the "Camera" panel */}
          <XVIZPanel log={logLoader} name="Camera" />
          {/* You could add more panels here */}
        </div>
      </div>

      {/* The PlaybackControl */}
      <div style={{ width: '100%', padding: '0 20px' }}>
        <PlaybackControl log={logLoader} />
      </div>
    </div>
  );
}

In this example:

  1. We import XVIZPanel.
  2. We render two <XVIZPanel /> components.
  3. Both panels receive the same logLoader instance via the log prop.
  4. Each panel specifies which definition to use from the log's metadata using the name prop ("Metrics" or "Camera").

When this component renders and the logLoader is ready: * Each XVIZPanel will look up the panel definition for its given name in the log's metadata. * It will then render the sequence of widgets defined within that panel. * As the log plays or the user seeks (controlled by the PlaybackControl), the XVIZPanel and its child widgets will automatically update to display the data corresponding to the loader's getCurrentTime().

This pattern is consistent with LogViewer and PlaybackControl: components connect to the same XVIZ Loader Interface instance, which acts as the central source of truth for the log's state and data.

XVIZ Declarative UI: The Blueprint#

The content that XVIZPanel renders comes from the ui_config section within the XVIZ log's metadata. This ui_config is structured as an object where keys are panel names (like "Metrics", "Camera") and values are the definitions for those panels.

Each panel definition is essentially a tree-like structure composed of different component types. streetscape.gl's XVIZPanel knows how to translate the most common XVIZ Declarative UI components into corresponding React components:

XVIZ Declarative UI Type Description Rendered by XVIZPanel using...
PANEL A top-level container for a panel. XVIZPanel itself or XVIZContainer (internal)
CONTAINER Groups other components, horizontal or vertical layout. XVIZContainer (internal)
METRIC Displays numerical values, often over time as a chart. _XVIZMetric (internal)
PLOT Displays data series plotted against an independent variable. _XVIZPlot (internal)
TABLE / TREETABLE Displays tabular data. _XVIZTable (internal)
VIDEO Displays image streams (e.g., camera feeds). _XVIZVideo (internal)

When XVIZPanel receives a panel definition, it walks through this structure. For each component type it finds (like METRIC or VIDEO), it renders the corresponding internal component (_XVIZMetric, _XVIZVideo). These internal components then connect to the log instance passed down by XVIZPanel to fetch the specific data they need (metrics, image frames, table rows) from the current frame.

This is why XVIZPanel only needs the log instance and the panel name. It uses the log to get both the static panel definition (from metadata) and the dynamic data (from getCurrentFrame()).

Under the Hood (A Simple Look)#

Let's trace how XVIZPanel gets its definition and renders content.

  1. Connection: Like other streetscape.gl components, XVIZPanel uses a wrapper mechanism (the connectToLog helper, which we'll dive into in Chapter 5) to connect to the log instance provided via its log prop.
  2. Getting UI Configuration: The connectToLog wrapper calls a special function (getLogState) provided by XVIZPanel. This function accesses the log's metadata and looks for the ui_config section. It then finds the specific panel definition that matches the name prop passed to XVIZPanel. This definition is then passed as a prop (uiConfig) to the internal XVIZPanelComponent.
  3. Rendering Panel Structure: The XVIZPanelComponent receives the uiConfig prop. If a definition is found, it iterates through the children defined in the uiConfig.
  4. Rendering Child Widgets: For each child item in the definition (e.g., a METRIC or VIDEO), XVIZPanelComponent looks up the appropriate internal React component (_XVIZMetric, _XVIZVideo) from a map (DEFAULT_COMPONENTS).
  5. Passing Data Source: It renders the found component, passing the same log prop it received down to the child. This is crucial, as the child components will also use connectToLog (or directly access log if needed) to get the specific data they require for their type (e.g., _XVIZMetric fetches metric values from log.getCurrentFrame().variables).

Here's a simplified data flow:

sequenceDiagram
    participant App as Your App
    participant XVIZPanel as XVIZPanel Component
    participant ConnectToLog as connectToLog (Wrapper)
    participant Loader as XVIZ Loader Instance
    participant InternalWidget as Internal Widget<br>(e.g., _XVIZMetric)

    App->>XVIZPanel: Render <XVIZPanel log={loader} name="..." />
    XVIZPanel->>ConnectToLog: Pass log instance and getLogState function
    ConnectToLog->>Loader: getMetadata()
    Loader->>ConnectToLog: metadata (including ui_config)
    ConnectToLog->>ConnectToLog: Call getLogState(metadata, name)
    ConnectToLog->>XVIZPanel: Provide uiConfig prop

    XVIZPanel->>XVIZPanel: Read uiConfig prop
    loop For each item in uiConfig.children
        XVIZPanel->>XVIZPanel: Determine internal component type
        XVIZPanel->>InternalWidget: Render <InternalWidget log={loader} ...item_props />
        InternalWidget->>ConnectToLog: (or directly) Access log prop
        loop On log update / seek
            InternalWidget->>Loader: getCurrentFrame() / getStreams() etc.
            Loader->>InternalWidget: Relevant data for widget
            InternalWidget->>InternalWidget: Render specific widget UI
        end
    end
Note: The connectToLog step for internal widgets is simplified in this diagram for clarity.

Let's look at snippets from the streetscape.gl source code (modules/core/src/components/declarative-ui/xviz-panel.js) to see parts of this process.

First, how XVIZPanel gets the uiConfig using connectToLog:

// From modules/core/src/components/declarative-ui/xviz-panel.js (simplified)

// This function is used by connectToLog to get state from the log
const getLogState = (log, ownProps) => {
  const metadata = log.getMetadata();
  // Look up the panel definition by name in the metadata's ui_config
  const panel = metadata && metadata.ui_config && metadata.ui_config[ownProps.name];
  // Return the definition (or its 'config' property, depending on internal structure)
  return {
    uiConfig: (panel && panel.config) || panel
  };
};

// Connect the component to the log using this function
export default connectToLog({getLogState, Component: XVIZPanelComponent});
This shows that the key uiConfig prop is populated by calling log.getMetadata() within the getLogState function and looking for the panel matching the component's name prop.

Next, how the XVIZPanelComponent renders the child widgets:

// From modules/core/src/components/declarative-ui/xviz-panel.js (simplified)

// Map XVIZ UI types to internal React components
const DEFAULT_COMPONENTS = {
  container: XVIZContainer, // Internal container component
  metric: XVIZMetric,       // Points to _XVIZMetric via default export setup
  plot: XVIZPlot,           // Points to _XVIZPlot
  video: XVIZVideo,         // Points to _XVIZVideo
  table: XVIZTable,         // Points to _XVIZTable
  treetable: XVIZTable      // Points to _XVIZTable
};

class XVIZPanelComponent extends PureComponent {
  // ... propTypes, defaultProps ...

  _renderItem = (item, i) => {
    const {components, componentProps, log, style} = this.props; // Note log prop is available!
    const type = item.type.toLowerCase();
    // Find the component renderer, prioritize custom ones
    const XVIZComponent = components[type] || DEFAULT_COMPONENTS[type];
    const customProps = componentProps[type];

    if (!XVIZComponent) {
      console.warn(`Unknown XVIZ Declarative UI component type: ${item.type}`); // Handle unknown types
      return null;
    }

    // Render the child component, passing down the log instance
    return (
      <XVIZComponent key={i} {...customProps} {...item} log={log} style={style[item.type]}>
        {/* Recursively render children for container types */}
        {item.children && item.children.map(this._renderItem)}
      </XVIZComponent>
    );
  };

  render() {
    const {uiConfig} = this.props;

    // If uiConfig is loaded, render its children
    return uiConfig ? (
      <div>{uiConfig.children && uiConfig.children.map(this._renderItem)}</div>
    ) : null; // Show nothing if no uiConfig is found for the name
  }
}
This snippet shows that the XVIZPanelComponent's render method maps the uiConfig structure to React components using the _renderItem helper. Crucially, it passes the log prop down to each child component, allowing them to connect to the loader and retrieve the specific data streams or variables they need from the current frame.

For example, the internal _XVIZMetric component (simplified from modules/core/src/components/declarative-ui/xviz-metric.js) would use connectToLog and log.getStreams() or log.getCurrentFrame().variables to get the numerical data it needs to render a chart:

// From modules/core/src/components/declarative-ui/xviz-metric.js (simplified)

const getLogState = log => ({
  currentTime: log.getCurrentTime(), // Needed for highlighting current point
  streamsMetadata: log.getStreamsMetadata(), // Might need for formatting
  logStreams: log.getStreams(),     // Get ALL stream data currently in buffer
  // Or specifically for metrics, might need log.getCurrentFrame().variables
  startTime: log.getBufferStartTime(), // For chart axis domain
  endTime: log.getBufferEndTime()     // For chart axis domain
});

// Connects XVIZMetricComponent to the log
export default connectToLog({getLogState, Component: XVIZMetricComponent});

// ... Inside XVIZMetricComponent render method ...
// It would use props like 'logStreams' or 'variables' and 'currentTime'
// to render the MetricChart component from the monochrome library.
This demonstrates how the log prop, passed down by XVIZPanel, enables the internal components to fetch their required data.

Conclusion#

The XVIZPanel component provides a powerful way to display auxiliary data from your XVIZ logs using the Declarative UI specification defined within the log metadata. By simply providing the log instance and the panel name, you can automatically generate dynamic dashboard elements like metrics, plots, tables, and video feeds, all driven by the log data itself. This complements the 3D visualization from the LogViewer and the time control from PlaybackControl to create a comprehensive viewer experience.

We've seen how multiple components (LogViewer, PlaybackControl, XVIZPanel, and its internal children) all rely on connecting to the single XVIZ Loader Interface instance to get their state and data. In the next chapter, we'll examine the connectToLog helper function that makes this seamless data flow and automatic updating possible.

ConnectToLog


Generated by AI Codebase Knowledge Builder. References: 1(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/docs/api-reference/xviz-panel.md), 2(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/docs/get-started/starter-kit.md), 3(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/components/declarative-ui/xviz-container.js), 4(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/components/declarative-ui/xviz-metric.js), 5(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/components/declarative-ui/xviz-panel.js), 6(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/components/declarative-ui/xviz-plot.js), 7(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/components/declarative-ui/xviz-table.js), 8(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/components/declarative-ui/xviz-video.js), 9(https://github.com/aurora-opensource/streetscape.gl/blob/befae1354ca8605c9f6cb1229b494858a8690e4f/modules/core/src/index.js)