Skip to content

Commit 54deeaf

Browse files
authored
Fix rect manager (#820)
* bugfix imgui right click menu * almost fixed rect manager * revert, just disallow left and top edges for now * cleanup docs guide * remove file, fix
1 parent 29d4a87 commit 54deeaf

File tree

5 files changed

+207
-53
lines changed

5 files changed

+207
-53
lines changed

docs/source/user_guide/guide.rst

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -549,10 +549,15 @@ between imgui and ipywidgets, Qt, and wx is the imgui UI can be rendered directl
549549
i.e. it will work in jupyter, Qt, glfw and wx windows! The programming model is different from Qt and ipywidgets, there
550550
are no callbacks, but it is easy to learn if you see a few examples.
551551

552+
.. image:: ../_static/guide_imgui.png
553+
552554
We specifically use `imgui-bundle <https://github.com/pthom/imgui_bundle>`_ for the python bindings in fastplotlib.
553555
There is large community and many resources out there on building UIs using imgui.
554556

555-
For examples on integrating imgui with a fastplotlib Figure please see the examples gallery.
557+
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.
558+
559+
Fastplotlib comes built-in with imgui UIs for subplot toolbars and a standard right-click menu with a number of options.
560+
You can also make custom GUIs and embed them within the canvas, see the examples gallery for detailed examples.
556561

557562
**Some tips:**
558563

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

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

665-
Imgui
666-
-----
667-
668-
Fastplotlib uses `imgui_bundle <https://github.com/pthom/imgui_bundle>`_ to provide within-canvas UI elemenents if you
669-
installed ``fastplotlib`` using the ``imgui`` toggle, i.e. ``fastplotlib[imgui]``, or installed ``imgui_bundle`` afterwards.
670-
671-
Fastplotlib comes built-in with imgui UIs for subplot toolbars and a standard right-click menu with a number of options.
672-
You can also make custom GUIs and embed them within the canvas, see the examples gallery for detailed examples.
673-
674-
.. note::
675-
Imgui is optional, you can use other GUI frameworks such at Qt or ipywidgets with fastplotlib. You can also of course
676-
use imgui and Qt or ipywidgets.
677-
678-
.. image:: ../_static/guide_imgui.png
679-
680670
Using ``fastplotlib`` in an interactive shell
681671
---------------------------------------------
682672

examples/guis/sine_cosine_funcs.py

Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
"""
2+
Sine and Cosine functions
3+
=========================
4+
5+
Identical to the Unit Circle example but you can change the angular frequencies using a UI
6+
7+
"""
8+
9+
# test_example = false
10+
# sphinx_gallery_pygfx_docs = 'screenshot'
11+
12+
import glfw
13+
import numpy as np
14+
import fastplotlib as fpl
15+
from fastplotlib.ui import EdgeWindow
16+
from imgui_bundle import imgui
17+
18+
19+
# initial frequency coefficients for sine and cosine functions
20+
P = 1
21+
Q = 1
22+
23+
24+
# helper function to make a circle
25+
def make_circle(center, radius: float, p, q, n_points: int) -> np.ndarray:
26+
theta = np.linspace(0, 2 * np.pi, n_points)
27+
xs = radius * np.cos(theta * p)
28+
ys = radius * np.sin(theta * q)
29+
30+
return np.column_stack([xs, ys]) + center
31+
32+
33+
# we can define this layout using "extents", i.e. min and max ranges on the canvas
34+
# (x_min, x_max, y_min, y_max)
35+
# extents can be defined as fractions as shown here
36+
extents = [
37+
(0, 0.5, 0, 1), # circle subplot
38+
(0.5, 1, 0, 0.5), # sine subplot
39+
(0.5, 1, 0.5, 1), # cosine subplot
40+
]
41+
42+
# create a figure with 3 subplots
43+
figure = fpl.Figure(
44+
extents=extents,
45+
names=["circle", "sin", "cos"],
46+
size=(700, 560)
47+
)
48+
49+
# set more descriptive figure titles
50+
figure["circle"].title = "sin(x*p) over cos(x*q)"
51+
figure["sin"].title = "sin(x * p)"
52+
figure["cos"].title = "cos(x * q)"
53+
54+
# set the axes to intersect at (0, 0, 0) to better illustrate the unit circle
55+
for subplot in figure:
56+
subplot.axes.intersection = (0, 0, 0)
57+
subplot.toolbar = False # reduce clutter
58+
59+
figure["sin"].camera.maintain_aspect = False
60+
figure["cos"].camera.maintain_aspect = False
61+
62+
# create sine and cosine data
63+
xs = np.linspace(0, 2 * np.pi, 360)
64+
sine = np.sin(xs * P)
65+
cosine = np.cos(xs * Q)
66+
67+
# circle data
68+
circle_data = make_circle(center=(0, 0), p=P, q=Q, radius=1, n_points=360)
69+
70+
# make the circle line graphic, set the cmap transform using the sine function
71+
circle_graphic = figure["circle"].add_line(
72+
circle_data, thickness=4, cmap="bwr", cmap_transform=sine
73+
)
74+
75+
# line to show the circle radius
76+
# use it to indicate the current position of the sine and cosine selctors (below)
77+
radius_data = np.array([[0, 0, 0], [*circle_data[0], 0]])
78+
circle_radius_graphic = figure["circle"].add_line(
79+
radius_data, thickness=6, colors="magenta"
80+
)
81+
82+
# sine line graphic, cmap transform set from the sine function
83+
sine_graphic = figure["sin"].add_line(
84+
sine, thickness=10, cmap="bwr", cmap_transform=sine
85+
)
86+
87+
# cosine line graphic, cmap transform set from the sine function
88+
# illustrates the sine function values on the cosine graphic
89+
cosine_graphic = figure["cos"].add_line(
90+
cosine, thickness=10, cmap="bwr", cmap_transform=sine
91+
)
92+
93+
# add linear selectors to the sine and cosine line graphics
94+
sine_selector = sine_graphic.add_linear_selector()
95+
cosine_selector = cosine_graphic.add_linear_selector()
96+
97+
98+
def set_circle_cmap(ev):
99+
# sets the cmap transforms
100+
101+
cmap_transform = ev.graphic.data[:, 1] # y-val data of the sine or cosine graphic
102+
for g in [sine_graphic, cosine_graphic]:
103+
g.cmap.transform = cmap_transform
104+
105+
# set circle cmap transform
106+
circle_graphic.cmap.transform = cmap_transform
107+
108+
# when the sine or cosine graphic is clicked, the cmap_transform
109+
# of the sine, cosine and circle line graphics are all set from
110+
# the y-values of the clicked line
111+
sine_graphic.add_event_handler(set_circle_cmap, "click")
112+
cosine_graphic.add_event_handler(set_circle_cmap, "click")
113+
114+
115+
def set_x_val(ev):
116+
# used to sync the two selectors
117+
value = ev.info["value"]
118+
index = ev.get_selected_index()
119+
120+
sine_selector.selection = value
121+
cosine_selector.selection = value
122+
123+
circle_radius_graphic.data[1, :-1] = circle_data[index]
124+
125+
# add same event handler to both graphics
126+
sine_selector.add_event_handler(set_x_val, "selection")
127+
cosine_selector.add_event_handler(set_x_val, "selection")
128+
129+
# initial selection value
130+
sine_selector.selection = 50
131+
132+
133+
class GUIWindow(EdgeWindow):
134+
def __init__(self, figure, size, location, title):
135+
super().__init__(figure=figure, size=size, location=location, title=title)
136+
137+
self._p = 1
138+
self._q = 1
139+
140+
def _set_data(self):
141+
global sine_graphic, cosine_graphic, circle_graphic, circle_radius_graphic, circle_data
142+
143+
# make new data
144+
sine = np.sin(xs * self._p)
145+
cosine = np.cos(xs * self._q)
146+
circle_data = make_circle(center=(0, 0), p=self._p, q=self._q, radius=1, n_points=360)
147+
148+
149+
# set the graphics
150+
sine_graphic.data[:, 1] = sine
151+
cosine_graphic.data[:, 1] = cosine
152+
circle_graphic.data[:, :2] = circle_data
153+
circle_radius_graphic.data[1, :-1] = circle_data[sine_selector.get_selected_index()]
154+
155+
def update(self):
156+
flag_set_data = False
157+
158+
changed, self._p = imgui.input_int("P", v=self._p, step_fast=2)
159+
if changed:
160+
flag_set_data = True
161+
162+
changed, self._q = imgui.input_int("Q", v=self._q, step_fast=2)
163+
if changed:
164+
flag_set_data = True
165+
166+
if flag_set_data:
167+
self._set_data()
168+
169+
170+
gui = GUIWindow(
171+
figure=figure,
172+
size=100,
173+
location="right",
174+
title="Freq. coeffs"
175+
)
176+
177+
figure.add_gui(gui)
178+
179+
figure.show()
180+
181+
182+
# NOTE: `if __name__ == "__main__"` is NOT how to use fastplotlib interactively
183+
# please see our docs for using fastplotlib interactively in ipython and jupyter
184+
if __name__ == "__main__":
185+
print(__doc__)
186+
fpl.loop.run()

fastplotlib/layouts/_imgui_figure.py

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -150,7 +150,7 @@ def _draw_imgui(self) -> imgui.ImDrawData:
150150

151151
def add_gui(self, gui: EdgeWindow):
152152
"""
153-
Add a GUI to the Figure. GUIs can be added to the top, bottom, left or right edge.
153+
Add a GUI to the Figure. GUIs can be added to the left or bottom edge.
154154
155155
Parameters
156156
----------
@@ -191,25 +191,15 @@ def get_pygfx_render_area(self, *args) -> tuple[int, int, int, int]:
191191

192192
width, height = self.canvas.get_logical_size()
193193

194-
for edge in ["left", "right"]:
194+
for edge in ["right"]:
195195
if self.guis[edge]:
196196
width -= self._guis[edge].size
197197

198-
for edge in ["top", "bottom"]:
198+
for edge in ["bottom"]:
199199
if self.guis[edge]:
200200
height -= self._guis[edge].size
201201

202-
if self.guis["left"]:
203-
xpos = self.guis["left"].size
204-
else:
205-
xpos = 0
206-
207-
if self.guis["top"]:
208-
ypos = self.guis["top"].size
209-
else:
210-
ypos = 0
211-
212-
return xpos, ypos, max(1, width), max(1, height)
202+
return 0, 0, max(1, width), max(1, height)
213203

214204
def register_popup(self, popup: Popup.__class__):
215205
"""

fastplotlib/ui/_base.py

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from ..layouts._figure import Figure
77

88

9-
GUI_EDGES = ["top", "right", "bottom", "left"]
9+
GUI_EDGES = ["right", "bottom"]
1010

1111

1212
class BaseGUI:
@@ -40,15 +40,15 @@ def __init__(
4040
self,
4141
figure: Figure,
4242
size: int,
43-
location: Literal["top", "bottom", "left", "right"],
43+
location: Literal["bottom", "right"],
4444
title: str,
4545
window_flags: int = imgui.WindowFlags_.no_collapse
4646
| imgui.WindowFlags_.no_resize,
4747
*args,
4848
**kwargs,
4949
):
5050
"""
51-
A base class for imgui windows displayed at one of the four edges of a Figure
51+
A base class for imgui windows displayed at the bottom or top edge of a Figure
5252
5353
Parameters
5454
----------
@@ -58,7 +58,7 @@ def __init__(
5858
size: int
5959
width or height of the window, depending on its location
6060
61-
location: str, "top" | "bottom" | "left" | "right"
61+
location: str, "bottom" | "right"
6262
location of the window
6363
6464
title: str
@@ -168,33 +168,15 @@ def get_rect(self) -> tuple[int, int, int, int]:
168168
width_canvas, height_canvas = self._figure.canvas.get_logical_size()
169169

170170
match self._location:
171-
case "top":
172-
x_pos, y_pos = (0, 0)
173-
width, height = (width_canvas, self.size)
174-
175171
case "bottom":
176172
x_pos = 0
177173
y_pos = height_canvas - self.size
178174
width, height = (width_canvas, self.size)
179175

180176
case "right":
181177
x_pos, y_pos = (width_canvas - self.size, 0)
182-
183-
if self._figure.guis["top"]:
184-
# if there is a GUI in the top edge, make this one below
185-
y_pos += self._figure.guis["top"].size
186-
187178
width, height = (self.size, height_canvas)
188-
if self._figure.guis["bottom"] is not None:
189-
height -= self._figure.guis["bottom"].size
190179

191-
case "left":
192-
x_pos, y_pos = (0, 0)
193-
if self._figure.guis["top"]:
194-
# if there is a GUI in the top edge, make this one below
195-
y_pos += self._figure.guis["top"].size
196-
197-
width, height = (self.size, height_canvas)
198180
if self._figure.guis["bottom"] is not None:
199181
height -= self._figure.guis["bottom"].size
200182

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

210195
# begin window

fastplotlib/ui/right_click_menus/_standard_menu.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@ def __init__(self, figure, fa_icons):
3131
# whether the right click menu is currently open or not
3232
self.is_open: bool = False
3333

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

43+
# not inside a subplot
44+
return False
45+
4346
def cleanup(self):
4447
"""called when the popup disappears"""
4548
self.is_open = False

0 commit comments

Comments
 (0)