Issues with dynamic ui.Plot updates

Operating System:
Windows
Kit Version:
110 (Kit App Template)
Kit Template:
USD Composer
GPU Hardware:
50 series
GPU Driver:
Latest

Work Flow:

I am trying to plot multiple lines with data provided via events using the ui.Plot widgets in a ZStack. When using static data, this works well, but does not work with dynamic data.

Main Issue:

When trying to update the plot data by using ui.Plot.set_xy_data on a LINE2D plot from an event handler, the plot does not update properly and gets stuck on a constant value. If I call set_xy_data synchronously it works and sets the plot data properly.

I tried many different setups to get this to work but no luck. Even to get to the point where I can plot the static data, I had to find this post on the forum (link) - ui.Plot seems quite poorly documented and not intuitive.

Correct behavior of set_xy_data:

Incorrect behavior when I try to use events:

Reproduction Steps:

Try the following code (issue also happened with Kit 108).

In the console you can clearly see that the data for sin functions is present in the lists, yet the constant value is shown.

Code:

Here is a bare-bones extension code which showcases the issue (some indentation might be incorrect because I cannot get it to paste into this editor properly):

import omni.ext
import omni.ui as ui
from omni.ui import color as cl
from carb.eventdispatcher import ObserverGuard, get_eventdispatcher, Event
import time
import numpy as np
import omni.kit.app

NCOLS = 5

class MyExtension(omni.ext.IExt):
def on_startup(self, _ext_id):
    print("[test.uiplot] Extension startup")    
    self._window = ui.Window(
        "UI Plot Test", width=400, height=400
    )

    self.sub_ui_update = self.create_data_stream(NCOLS)
    self.sub_event_stream: ObserverGuard = None

    with self._window.frame:
        with ui.VStack(width=0, height=0):
            ui.Button("Create stream Plot", clicked_fn=self._create_stream_plot)
            ui.Button("Create basic Plot", clicked_fn=self._create_basic_plot)

            self.plot_frame = ui.Frame(width=300, height=300)

def create_data_stream(self, ncols):
    def on_update(_event: Event):
        get_eventdispatcher().dispatch_event(
            f"data_visualiser.update_data_stream_N_{ncols}", 
            {"data": [np.sin(time.time() + i) for i in range(ncols)]}
        )

    sub_update_event: ObserverGuard = get_eventdispatcher().observe_event(
        observer_name=f"Main thread update data stream publisher N={ncols}",
        event_name=omni.kit.app.GLOBAL_EVENT_UPDATE,
        on_event=on_update
    )  

    return sub_update_event

def clear_plots(self):
    self.plot_frame.clear()
    self.column_plots: list[ui.Plot] = []
    self.column_data = [[] for _ in range(NCOLS)]

def _create_basic_plot(self):
    self._destroy_sub_event()

    self.clear_plots()
    with self.plot_frame:
        with ui.VStack(width=0, height=0):
            ui.Label(f"Observing data stream with {NCOLS} columns", alignment=ui.Alignment.CENTER)
            with ui.ZStack(width=0, height=0):
                for col in range(NCOLS):
                    col_plot = ui.Plot(ui.Type.LINE2D, title=f"Column {col + 1}", width=800, height=500, style={"color": cl.red, "background_color": cl(0,0,0,0)})
                    self.column_plots.append(col_plot)

    for col in range(NCOLS):
        col_plot = self.column_plots[col]
        col_xy_data = [(float(i)/1000, 10*np.sin(float(i + 10*col)/100)) for i in range(1000)]
        col_plot.set_xy_data(col_xy_data)

    print(f"Finished ui plot setup")

def _create_stream_plot(self):
    self._destroy_sub_event()

    self.clear_plots()
    with self.plot_frame:
        with ui.VStack(width=0, height=0):
            ui.Label(f"Observing data stream with {NCOLS} columns", alignment=ui.Alignment.CENTER)
            with ui.ZStack(width=0, height=0):
                for col in range(NCOLS):
                    col_plot = ui.Plot(ui.Type.LINE2D, title=f"Column {col + 1}", width=800, height=500, style={"color": cl.red, "background_color": cl(0,0,0,0)})
                    self.column_plots.append(col_plot)

    print(f"Finished ui plot setup")

    def on_data(event: Event):
        col_vals = event.get("data")
        if len(col_vals) != NCOLS:
            print(f"Received data length {len(col_vals)} does not match expected number of columns {NCOLS}.")
            return

        data_length = len(self.column_data[0]) + 1
        for col in range(NCOLS):
            self.column_data[col].append(col_vals[col])
            col_xy_data = [(float(i)/data_length, 10*float(self.column_data[col][i])) for i in range(data_length)]
            print(f"Updating plot for column {col + 1} with data: {col_xy_data}")
            self.column_plots[col].set_xy_data(col_xy_data)

    print(f"Creating observer for data stream with {NCOLS} columns")
    event_name = f"data_visualiser.update_data_stream_N_{NCOLS}"
    self.sub_event_stream: ObserverGuard = get_eventdispatcher().observe_event(
        observer_name=f"Data stream observer N={NCOLS}",
        event_name=event_name,
        on_event=lambda event: on_data(event)
    )  

    print(f"Started observing data stream with {NCOLS} columns. Listening for events named '{event_name}'.")

def _destroy_sub_event(self):
    if self.sub_event_stream is not None:
        self.sub_event_stream.reset()
        self.sub_event_stream = None

def _destroy_sub_update(self):
    if self.sub_ui_update is not None:
        self.sub_ui_update.reset()
        self.sub_ui_update = None

def _destroy_subs(self):
    self._destroy_sub_event()
    self._destroy_sub_update()

def on_shutdown(self):
    print("[test.uiplot] Extension shutdown")
    self._destroy_subs()

After additional debugging I found out that the `.set_xy_data` function only ever takes an effect once, further calls have no effect at all. I tried “forcing” the Plot widget to update by updating it’s other attributes or setting the data to an empty array first, but nothing works. For now, I resorted to destroying and recreating the Plot objects ever few frames, but this approach both makes the image flicker and obviously is not a great solution.

In addition, the ui.Plot widget forces it’s on-hover datatip even when I try to block it.

Please fix these issues NVIDIA Kit team, thank you.

May I ask what this application of 2D data is for? This is not our primary focus for Omniverse.

Dear Richard,

We simulate various robotic processes and want to visualize the evolution of certain variables over time with support for also visualizing data from previous runs (stored in some files) in the same way.

We also want to support some “expected” features for plotting like re-scaling the axis, taking various measurements etc.

So if this is all about conventional 2D data, why use Omniverse, a complete 3D system, with limited support for 2D, especially when this would be very easy to do with standard 2D data tools?

Only certain parts of data visualization are 2D, most of the application relies heavily on Omniverse’s 3D capabilities, but we want to ship the entire solution without other external programs. That’s why we want to also handle the 2D plotting part within Omniverse.