Skip to content

Fix rect manager #820

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
May 19, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 6 additions & 16 deletions docs/source/user_guide/guide.rst
Original file line number Diff line number Diff line change
Expand Up @@ -549,10 +549,15 @@ between imgui and ipywidgets, Qt, and wx is the imgui UI can be rendered directl
i.e. it will work in jupyter, Qt, glfw and wx windows! The programming model is different from Qt and ipywidgets, there
are no callbacks, but it is easy to learn if you see a few examples.

.. image:: ../_static/guide_imgui.png

We specifically use `imgui-bundle <https://github.com/pthom/imgui_bundle>`_ for the python bindings in fastplotlib.
There is large community and many resources out there on building UIs using imgui.

For examples on integrating imgui with a fastplotlib Figure please see the examples gallery.
To install ``fastplotlib`` with ``imgui`` use the ``imgui`` extras option, i.e. ``pip install fastplotlib[imgui]``, or ``pip install imgui_bundle`` if you've already installed fastplotlib.

Fastplotlib comes built-in with imgui UIs for subplot toolbars and a standard right-click menu with a number of options.
You can also make custom GUIs and embed them within the canvas, see the examples gallery for detailed examples.

**Some tips:**

Expand Down Expand Up @@ -662,21 +667,6 @@ There are several spaces to consider when using ``fastplotlib``:

For more information on the various spaces used by rendering engines please see this `article <https://learnopengl.com/Getting-started/Coordinate-Systems>`_

Imgui
-----

Fastplotlib uses `imgui_bundle <https://github.com/pthom/imgui_bundle>`_ to provide within-canvas UI elemenents if you
installed ``fastplotlib`` using the ``imgui`` toggle, i.e. ``fastplotlib[imgui]``, or installed ``imgui_bundle`` afterwards.

Fastplotlib comes built-in with imgui UIs for subplot toolbars and a standard right-click menu with a number of options.
You can also make custom GUIs and embed them within the canvas, see the examples gallery for detailed examples.

.. note::
Imgui is optional, you can use other GUI frameworks such at Qt or ipywidgets with fastplotlib. You can also of course
use imgui and Qt or ipywidgets.

.. image:: ../_static/guide_imgui.png

Using ``fastplotlib`` in an interactive shell
---------------------------------------------

Expand Down
186 changes: 186 additions & 0 deletions examples/guis/sine_cosine_funcs.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
"""
Sine and Cosine functions
=========================

Identical to the Unit Circle example but you can change the angular frequencies using a UI

"""

# test_example = false
# sphinx_gallery_pygfx_docs = 'screenshot'

import glfw
import numpy as np
import fastplotlib as fpl
from fastplotlib.ui import EdgeWindow
from imgui_bundle import imgui


# initial frequency coefficients for sine and cosine functions
P = 1
Q = 1


# helper function to make a circle
def make_circle(center, radius: float, p, q, n_points: int) -> np.ndarray:
theta = np.linspace(0, 2 * np.pi, n_points)
xs = radius * np.cos(theta * p)
ys = radius * np.sin(theta * q)

return np.column_stack([xs, ys]) + center


# we can define this layout using "extents", i.e. min and max ranges on the canvas
# (x_min, x_max, y_min, y_max)
# extents can be defined as fractions as shown here
extents = [
(0, 0.5, 0, 1), # circle subplot
(0.5, 1, 0, 0.5), # sine subplot
(0.5, 1, 0.5, 1), # cosine subplot
]

# create a figure with 3 subplots
figure = fpl.Figure(
extents=extents,
names=["circle", "sin", "cos"],
size=(700, 560)
)

# set more descriptive figure titles
figure["circle"].title = "sin(x*p) over cos(x*q)"
figure["sin"].title = "sin(x * p)"
figure["cos"].title = "cos(x * q)"

# set the axes to intersect at (0, 0, 0) to better illustrate the unit circle
for subplot in figure:
subplot.axes.intersection = (0, 0, 0)
subplot.toolbar = False # reduce clutter

figure["sin"].camera.maintain_aspect = False
figure["cos"].camera.maintain_aspect = False

# create sine and cosine data
xs = np.linspace(0, 2 * np.pi, 360)
sine = np.sin(xs * P)
cosine = np.cos(xs * Q)

# circle data
circle_data = make_circle(center=(0, 0), p=P, q=Q, radius=1, n_points=360)

# make the circle line graphic, set the cmap transform using the sine function
circle_graphic = figure["circle"].add_line(
circle_data, thickness=4, cmap="bwr", cmap_transform=sine
)

# line to show the circle radius
# use it to indicate the current position of the sine and cosine selctors (below)
radius_data = np.array([[0, 0, 0], [*circle_data[0], 0]])
circle_radius_graphic = figure["circle"].add_line(
radius_data, thickness=6, colors="magenta"
)

# sine line graphic, cmap transform set from the sine function
sine_graphic = figure["sin"].add_line(
sine, thickness=10, cmap="bwr", cmap_transform=sine
)

# cosine line graphic, cmap transform set from the sine function
# illustrates the sine function values on the cosine graphic
cosine_graphic = figure["cos"].add_line(
cosine, thickness=10, cmap="bwr", cmap_transform=sine
)

# add linear selectors to the sine and cosine line graphics
sine_selector = sine_graphic.add_linear_selector()
cosine_selector = cosine_graphic.add_linear_selector()


def set_circle_cmap(ev):
# sets the cmap transforms

cmap_transform = ev.graphic.data[:, 1] # y-val data of the sine or cosine graphic
for g in [sine_graphic, cosine_graphic]:
g.cmap.transform = cmap_transform

# set circle cmap transform
circle_graphic.cmap.transform = cmap_transform

# when the sine or cosine graphic is clicked, the cmap_transform
# of the sine, cosine and circle line graphics are all set from
# the y-values of the clicked line
sine_graphic.add_event_handler(set_circle_cmap, "click")
cosine_graphic.add_event_handler(set_circle_cmap, "click")


def set_x_val(ev):
# used to sync the two selectors
value = ev.info["value"]
index = ev.get_selected_index()

sine_selector.selection = value
cosine_selector.selection = value

circle_radius_graphic.data[1, :-1] = circle_data[index]

# add same event handler to both graphics
sine_selector.add_event_handler(set_x_val, "selection")
cosine_selector.add_event_handler(set_x_val, "selection")

# initial selection value
sine_selector.selection = 50


class GUIWindow(EdgeWindow):
def __init__(self, figure, size, location, title):
super().__init__(figure=figure, size=size, location=location, title=title)

self._p = 1
self._q = 1

def _set_data(self):
global sine_graphic, cosine_graphic, circle_graphic, circle_radius_graphic, circle_data

# make new data
sine = np.sin(xs * self._p)
cosine = np.cos(xs * self._q)
circle_data = make_circle(center=(0, 0), p=self._p, q=self._q, radius=1, n_points=360)


# set the graphics
sine_graphic.data[:, 1] = sine
cosine_graphic.data[:, 1] = cosine
circle_graphic.data[:, :2] = circle_data
circle_radius_graphic.data[1, :-1] = circle_data[sine_selector.get_selected_index()]

def update(self):
flag_set_data = False

changed, self._p = imgui.input_int("P", v=self._p, step_fast=2)
if changed:
flag_set_data = True

changed, self._q = imgui.input_int("Q", v=self._q, step_fast=2)
if changed:
flag_set_data = True

if flag_set_data:
self._set_data()


gui = GUIWindow(
figure=figure,
size=100,
location="right",
title="Freq. coeffs"
)

figure.add_gui(gui)

figure.show()


# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
# please see our docs for using fastplotlib interactively in ipython and jupyter
if __name__ == "__main__":
print(__doc__)
fpl.loop.run()
18 changes: 4 additions & 14 deletions fastplotlib/layouts/_imgui_figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,7 @@ def _draw_imgui(self) -> imgui.ImDrawData:

def add_gui(self, gui: EdgeWindow):
"""
Add a GUI to the Figure. GUIs can be added to the top, bottom, left or right edge.
Add a GUI to the Figure. GUIs can be added to the left or bottom edge.

Parameters
----------
Expand Down Expand Up @@ -191,25 +191,15 @@ def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]:

width, height = self.canvas.get_logical_size()

for edge in ["left", "right"]:
for edge in ["right"]:
if self.guis[edge]:
width -= self._guis[edge].size

for edge in ["top", "bottom"]:
for edge in ["bottom"]:
if self.guis[edge]:
height -= self._guis[edge].size

if self.guis["left"]:
xpos = self.guis["left"].size
else:
xpos = 0

if self.guis["top"]:
ypos = self.guis["top"].size
else:
ypos = 0

return xpos, ypos, max(1, width), max(1, height)
return 0, 0, max(1, width), max(1, height)

def register_popup(self, popup: Popup.__class__):
"""
Expand Down
29 changes: 7 additions & 22 deletions fastplotlib/ui/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
from ..layouts._figure import Figure


GUI_EDGES = ["top", "right", "bottom", "left"]
GUI_EDGES = ["right", "bottom"]


class BaseGUI:
Expand Down Expand Up @@ -40,15 +40,15 @@ def __init__(
self,
figure: Figure,
size: int,
location: Literal["top", "bottom", "left", "right"],
location: Literal["bottom", "right"],
title: str,
window_flags: int = imgui.WindowFlags_.no_collapse
| imgui.WindowFlags_.no_resize,
*args,
**kwargs,
):
"""
A base class for imgui windows displayed at one of the four edges of a Figure
A base class for imgui windows displayed at the bottom or top edge of a Figure

Parameters
----------
Expand All @@ -58,7 +58,7 @@ def __init__(
size: int
width or height of the window, depending on its location

location: str, "top" | "bottom" | "left" | "right"
location: str, "bottom" | "right"
location of the window

title: str
Expand Down Expand Up @@ -168,33 +168,15 @@ def get_rect(self) -> tuple[int, int, int, int]:
width_canvas, height_canvas = self._figure.canvas.get_logical_size()

match self._location:
case "top":
x_pos, y_pos = (0, 0)
width, height = (width_canvas, self.size)

case "bottom":
x_pos = 0
y_pos = height_canvas - self.size
width, height = (width_canvas, self.size)

case "right":
x_pos, y_pos = (width_canvas - self.size, 0)

if self._figure.guis["top"]:
# if there is a GUI in the top edge, make this one below
y_pos += self._figure.guis["top"].size

width, height = (self.size, height_canvas)
if self._figure.guis["bottom"] is not None:
height -= self._figure.guis["bottom"].size

case "left":
x_pos, y_pos = (0, 0)
if self._figure.guis["top"]:
# if there is a GUI in the top edge, make this one below
y_pos += self._figure.guis["top"].size

width, height = (self.size, height_canvas)
if self._figure.guis["bottom"] is not None:
height -= self._figure.guis["bottom"].size

Expand All @@ -203,8 +185,11 @@ def get_rect(self) -> tuple[int, int, int, int]:
def draw_window(self):
"""helps simplify using imgui by managing window creation & position, and pushing/popping the ID"""
# window position & size
x, y, w, h = self.get_rect()
imgui.set_next_window_size((self.width, self.height))
imgui.set_next_window_pos((self.x, self.y))
# imgui.set_next_window_pos((x, y))
# imgui.set_next_window_size((w, h))
flags = self._window_flags

# begin window
Expand Down
5 changes: 4 additions & 1 deletion fastplotlib/ui/right_click_menus/_standard_menu.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ def __init__(self, figure, fa_icons):
# whether the right click menu is currently open or not
self.is_open: bool = False

def get_subplot(self) -> PlotArea | bool:
def get_subplot(self) -> PlotArea | bool | None:
"""get the subplot that a click occurred in"""
if self._last_right_click_pos is None:
return False
Expand All @@ -40,6 +40,9 @@ def get_subplot(self) -> PlotArea | bool:
if subplot.viewport.is_inside(*self._last_right_click_pos):
return subplot

# not inside a subplot
return False

def cleanup(self):
"""called when the popup disappears"""
self.is_open = False
Expand Down
Loading
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy