imgui UIs#

imgui UIs are rendered directly onto the same canvas as the Figure, so the same UI code runs on every GUI backend: glfw, Qt, wx, and jupyter.

imgui support requires imgui-bundle, see the installation section of the user guide. When imgui-bundle is installed fastplotlib.Figure is an ImguiFigure, and every subplot gets a toolbar and a standard right-click menu.

There are two things you can add to a Figure:

  • ImguiWindow - a window drawn within the Figure. It can float over the plots, be fixed to a rect, or occupy space on an edge of the Figure or of a Subplot.

  • ImguiPopup - a popup opened by a right-click on the Figure, a Subplot, or a Graphic.

Both are written in the same way, either as a function or as a subclass.

Floating and fixed windows#

A floating window is drawn over the plots. imgui sizes it to fit its contents, it appears at the top left of the canvas, and the user can move, resize, and collapse it. The function draws the imgui elements and is called on every render, the object it is added to is an optional argument:

import numpy as np
import fastplotlib as fpl
from imgui_bundle import imgui

figure = fpl.Figure(size=(700, 560))
figure[0, 0].add_line(np.random.rand(100), name="line")

@figure.add_imgui_window(location="floating")
def gui(fig):
    line = fig[0, 0]["line"]

    changed, thickness = imgui.slider_float("thickness", v=line.thickness, v_min=2.0, v_max=50.0)
    if changed:
        line.thickness = thickness

    if imgui.button("randomize"):
        line.data[:, 1] = np.random.rand(100)

add_imgui_window can also be given the function directly instead of decorating it, which is useful when the same function is used more than once:

figure.add_imgui_window(gui, location="floating")

A window can instead be fixed to a rect of the canvas, (x, y, width, height), or to an extent, (xmin, xmax, ymin, ymax). These are fractional if the width and height are <= 1, and in pixels otherwise. A fixed window cannot be moved, resized, or collapsed:

@figure.add_imgui_window(extent=(0.6, 0.98, 0.05, 0.25))
def gui():
    imgui.text("fixed to a fractional extent")

Figure edge windows#

An edge window occupies canvas space along one edge of the Figure, so it never covers the plots. location is one of "left", "right", "top", "bottom", and size is the thickness in pixels, which is required:

@figure.add_imgui_window(location="right", size=200, title="controls")
def gui(fig):
    ...

If title is not given no title bar is drawn. The “bottom” and “right” Figure edge windows can be resized by dragging their inner border, and collapsed by double-clicking it.

Subplot edge windows#

You can add imgui windows that are confined to a subplot edge:

@figure[0, 0].add_imgui_window(location="right", size=130, title="image")
def gui(subplot):
    if imgui.button("noise"):
        subplot["image"].data = np.random.rand(128, 128)

Each subplot also has a toolbar, an imgui window at the "toolbar" location that you can append elements to:

from imgui_bundle import icons_fontawesome_6 as fa

@figure[0, 0].append_imgui_window(location="toolbar")
def toolbar_extra(subplot):
    imgui.same_line()
    _, subplot.axes.visible = imgui.checkbox(fa.ICON_FA_RULER_COMBINED, subplot.axes.visible)

subplot.toolbar = False hides it, and add_imgui_window(location="toolbar") replaces it.

Appending, replacing, and removing#

Windows are keyed by location, and add_imgui_window replaces the window at that location. append_imgui_window adds more UI elements to the window that is already there, it raises if there is none:

@figure.append_imgui_window(location="right")
def more(fig):
    imgui.text("appended below the elements of the existing window")

remove_imgui_window removes and returns the window at a location, which can be added again later:

window = figure.remove_imgui_window("right")

figure.imgui_windows and subplot.imgui_windows return the windows keyed by location.

Subclassing ImguiWindow#

Subclass ImguiWindow and implement update() when you need something more complex, such as a UI that keeps state. Pass what the UI needs into __init__, an instance is not bound to a Figure until it is added:

from fastplotlib.ui import ImguiWindow

class Controls(ImguiWindow):
    def __init__(self, line):
        super().__init__()

        self._line = line
        self._ys = line.data[:, 1].copy()
        self._amplitude = 1.0

    def update(self):
        changed, self._amplitude = imgui.slider_float(
            "amplitude", v=self._amplitude, v_min=0.1, v_max=10.0
        )
        if changed:
            self._line.data[:, 1] = self._ys * self._amplitude

figure.add_imgui_window(Controls(line), location="right", size=200, title="controls")

Within update() the window’s pixel rect is available as x, y, width, and height. size is settable, and setting it on an edge or toolbar window triggers a re-layout of the Figure.

fastplotlib.ui.ChangeFlag is useful when several elements modify the same thing. It is a bool that stays True once it has been set to True:

from fastplotlib.ui import ChangeFlag

changed = ChangeFlag(False)
changed.value, vmin = imgui.slider_float("vmin", v=image.vmin, v_min=0, v_max=255)
changed.value, vmax = imgui.slider_float("vmax", v=image.vmax, v_min=0, v_max=255)

if changed:
    image.vmin, image.vmax = vmin, vmax

For full control of the imgui window, override draw() instead of update(). You are then responsible for creating the window with imgui.begin() and imgui.end(), and update() is not used. This is how you use window flags that must be set when the window is created, such as imgui.WindowFlags_.menu_bar for a menu bar, see imgui.WindowFlags_. The examples gallery has a menu bar example.

Right-click popups#

A popup is opened by a right-click. It is not restricted to menu items, any imgui elements can be used.

A popup can be set on the Figure, where it replaces the standard right-click menu, on a Subplot, or on a Graphic. The most specific one wins: the popup of the graphic under the pointer, else the popup of the subplot that was clicked, else the popup of the Figure:

@figure.set_imgui_right_click()
def popup(fig):
    if imgui.menu_item("autoscale all", "", False)[0]:
        for subplot in fig:
            subplot.auto_scale()

@figure[0, 1].set_imgui_right_click()
def subplot_popup(subplot):
    imgui.text(f"subplot: {subplot.name}")

A popup takes the object it is set on as an optional argument, and the function can be passed directly instead of decorating. Each call wraps the function in its own popup, so the same function can be set on any number of graphics:

def contrast(image):
    changed, vals = imgui.slider_float2("vmin / vmax", (image.vmin, image.vmax), 0, 255)
    if changed:
        image.vmin, image.vmax = vals

img1.set_imgui_right_click(contrast)
img2.set_imgui_right_click(contrast)

Only one popup can be set on an object. A graphic must be added to a subplot of an ImguiFigure before a popup can be set on it. append_imgui_right_click adds more UI elements to the popup that is set, remove_imgui_right_click removes and returns it, and imgui_right_click returns the popup that is set.

Extending the standard right-click menu#

The Figure’s popup is a StandardRightClickMenu. Append to it to keep its items and add your own:

@figure.append_imgui_right_click()
def extra_items(fig):
    imgui.separator()
    _, fig.imgui_show_fps = imgui.checkbox("show fps", fig.imgui_show_fps)

Subclassing ImguiPopup#

Subclass ImguiPopup and implement update(), which contains only the imgui elements. subplot and graphic are what the popup was opened on, graphic is None if the click was not on a graphic, and parent is the object the popup is set on:

from fastplotlib.ui import ImguiPopup

class MyPopup(ImguiPopup):
    def update(self):
        imgui.text(f"subplot: {self.subplot.name}")

        if imgui.menu_item("autoscale", "", False)[0]:
            self.subplot.auto_scale()

figure.set_imgui_right_click(MyPopup())

To keep the standard items, subclass StandardRightClickMenu and call super().update():

from fastplotlib.ui import StandardRightClickMenu

class MyMenu(StandardRightClickMenu):
    def update(self):
        super().update()

        imgui.separator()
        if imgui.menu_item("my item", "", False)[0]:
            ...

window_flags can be passed to set_imgui_right_click and is a settable property, see imgui.WindowFlags_. is_open tells you whether the popup is currently open.

A window that must stay open after the popup closes cannot be drawn in update(), which only runs while the popup is open. Override draw() and draw it after the popup:

class MyPopup(ImguiPopup):
    def __init__(self):
        super().__init__()
        self._window_open = False

    def update(self):
        if imgui.menu_item("Open window", "", False)[0]:
            self._window_open = True

    def draw(self):
        super().draw()

        if self._window_open:
            _, self._window_open = imgui.begin("my window", True)
            imgui.text("stays open after the popup closes")
            imgui.end()

Built-in imgui UIs#

  • SubplotToolbar - the toolbar of each subplot.

  • StandardRightClickMenu - the Figure’s default right-click popup: fps, autoscale, center, maintain aspect, flip axes, grids, FOV, and controller options.

  • ImguiColorbar - an ImguiContainer that shows a colorbar for one or more graphics, with draggable handles, a colormap picker, gamma, and an optional precomputed histogram. The handles drive vmin and vmax on an image and cmap_range on a line or scatter:

    from fastplotlib.ui import ImguiColorbar
    
    colorbar = ImguiColorbar(graphics=image, histogram=np.histogram(data, bins=100))
    figure[0, 0].add_imgui_window(colorbar, location="right", size=100)
    

Writing imgui elements#

fastplotlib does not wrap imgui, you call imgui_bundle directly, so any imgui element can be used. The imgui element reference documents each element as it exists in imgui_bundle, with its signature, its arguments, its flags, and an example of what it looks like.

The ImGUI section of the examples gallery has complete examples.