Skip to content

Commit 88f33c3

Browse files
committed
WIP on plot frame
1 parent 18d2389 commit 88f33c3

File tree

7 files changed

+349
-1
lines changed

7 files changed

+349
-1
lines changed

fastplotlib/layouts/_frame/__init__.py

Whitespace-only changes.
Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import numpy as np
2+
3+
from .._subplot import Subplot
4+
5+
class BaseFrame:
6+
"""Mixin class for Plot and GridPlot that gives them the toolbar"""
7+
def __init__(self, canvas, toolbar):
8+
"""
9+
10+
Parameters
11+
----------
12+
plot:
13+
`Plot` or `GridPlot`
14+
toolbar
15+
"""
16+
self._canvas = canvas
17+
self._toolbar = toolbar
18+
19+
# default points upwards
20+
self._y_axis: int = 1
21+
22+
self._plot_type = self.__class__.__name__
23+
24+
@property
25+
def selected_subplot(self) -> Subplot:
26+
if self._plot_type == "GridPlot":
27+
return self.toolbar.selected_subplot
28+
else:
29+
return self
30+
31+
@property
32+
def toolbar(self):
33+
return self._toolbar
34+
35+
@property
36+
def panzoom(self) -> bool:
37+
return self.selected_subplot.controller.enabled
38+
39+
@property
40+
def maintain_aspect(self) -> bool:
41+
return self.selected_subplot.camera.maintain_aspect
42+
43+
@property
44+
def y_axis(self) -> int:
45+
return int(np.sign(self.selected_subplot.camera.local.scale_y))
46+
47+
@y_axis.setter
48+
def y_axis(self, value: int):
49+
"""
50+
51+
Parameters
52+
----------
53+
value: 1 or -1
54+
1: points upwards, -1: points downwards
55+
56+
"""
57+
value = int(value) # in case we had a float 1.0
58+
59+
if value not in [1, -1]:
60+
raise ValueError("y_axis value must be 1 or -1")
61+
62+
sign = np.sign(self.selected_subplot.camera.local.scale_y)
63+
64+
if sign == value:
65+
# desired y-axis is already set
66+
return
67+
68+
# otherwise flip it
69+
self.selected_subplot.camera.local.scale_y *= -1
70+
71+
def render(self):
72+
raise NotImplemented
73+
74+
def _autoscale_init(self, maintain_aspect: bool):
75+
"""autoscale function that is called only during show()"""
76+
if self._plot_type == "GridPlot":
77+
for subplot in self:
78+
if maintain_aspect is None:
79+
_maintain_aspect = subplot.camera.maintain_aspect
80+
else:
81+
_maintain_aspect = maintain_aspect
82+
subplot.auto_scale(maintain_aspect=_maintain_aspect, zoom=0.95)
83+
else:
84+
if maintain_aspect is None:
85+
maintain_aspect = self.camera.maintain_aspect
86+
self.auto_scale(maintain_aspect=maintain_aspect, zoom=0.95)
87+
88+
def show(self):
89+
raise NotImplemented("Must be implemented in subclass")

fastplotlib/layouts/_frame/_frame_desktop.py

Whitespace-only changes.
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
import os
2+
3+
4+
from ._frame_base import BaseFrame
5+
from ._toolbar import ToolBar
6+
7+
8+
class FrameNotebook(BaseFrame):
9+
def show(
10+
self,
11+
autoscale: bool = True,
12+
maintain_aspect: bool = None,
13+
toolbar: bool = True,
14+
sidecar: bool = True,
15+
sidecar_kwargs: dict = None,
16+
vbox: list = None
17+
):
18+
"""
19+
Begins the rendering event loop and returns the canvas
20+
21+
Parameters
22+
----------
23+
autoscale: bool, default ``True``
24+
autoscale the Scene
25+
26+
maintain_aspect: bool, default ``True``
27+
maintain aspect ratio
28+
29+
toolbar: bool, default ``True``
30+
show toolbar
31+
32+
sidecar: bool, default ``True``
33+
display plot in a ``jupyterlab-sidecar``
34+
35+
sidecar_kwargs: dict, default ``None``
36+
kwargs for sidecar instance to display plot
37+
i.e. title, layout
38+
39+
vbox: list, default ``None``
40+
list of ipywidgets to be displayed with plot
41+
42+
Returns
43+
-------
44+
WgpuCanvas
45+
the canvas
46+
47+
"""
48+
49+
self._canvas.request_draw(self.render)
50+
51+
self._canvas.set_logical_size(*self._starting_size)
52+
53+
if autoscale:
54+
self._autoscale_init(maintain_aspect)
55+
56+
if "NB_SNAPSHOT" in os.environ.keys():
57+
# used for docs
58+
if os.environ["NB_SNAPSHOT"] == "1":
59+
return self.canvas.snapshot()
60+
61+
# check if in jupyter notebook, or if toolbar is False
62+
if (self.canvas.__class__.__name__ != "JupyterWgpuCanvas") or (not toolbar):
63+
return self.canvas
64+
65+
if self.toolbar is None:
66+
self.toolbar = ToolBar(self)
67+
self.toolbar.maintain_aspect_button.value = self[
68+
0, 0
69+
].camera.maintain_aspect
70+
71+
# validate vbox if not None
72+
if vbox is not None:
73+
for widget in vbox:
74+
if not isinstance(widget, Widget):
75+
raise ValueError(f"Items in vbox must be ipywidgets. Item: {widget} is of type: {type(widget)}")
76+
self.vbox = VBox(vbox)
77+
78+
if not sidecar:
79+
if self.vbox is not None:
80+
return VBox([self.canvas, self.toolbar.widget, self.vbox])
81+
else:
82+
return VBox([self.canvas, self.toolbar.widget])
83+
84+
# used when plot.show() is being called again but sidecar has been closed via "x" button
85+
# need to force new sidecar instance
86+
# couldn't figure out how to get access to "close" button in order to add observe method on click
87+
if self.plot_open:
88+
self.sidecar = None
89+
90+
if self.sidecar is None:
91+
if sidecar_kwargs is not None:
92+
self.sidecar = Sidecar(**sidecar_kwargs)
93+
self.plot_open = True
94+
else:
95+
self.sidecar = Sidecar()
96+
self.plot_open = True
97+
98+
with self.sidecar:
99+
if self.vbox is not None:
100+
return display(VBox([self.canvas, self.toolbar.widget, self.vbox]))
101+
else:
102+
return display(VBox([self.canvas, self.toolbar.widget]))
103+
Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
from datetime import datetime
2+
from itertools import product
3+
import traceback
4+
5+
from ipywidgets import HBox, Layout, Button, ToggleButton, VBox, Dropdown, Widget
6+
7+
from fastplotlib.layouts._subplot import Subplot
8+
9+
10+
class ToolBar:
11+
def __init__(self, plot):
12+
"""
13+
Basic toolbar for a GridPlot instance.
14+
15+
Parameters
16+
----------
17+
plot:
18+
"""
19+
self.plot = plot
20+
21+
self.autoscale_button = Button(
22+
value=False,
23+
disabled=False,
24+
icon="expand-arrows-alt",
25+
layout=Layout(width="auto"),
26+
tooltip="auto-scale scene",
27+
)
28+
self.center_scene_button = Button(
29+
value=False,
30+
disabled=False,
31+
icon="align-center",
32+
layout=Layout(width="auto"),
33+
tooltip="auto-center scene",
34+
)
35+
self.panzoom_controller_button = ToggleButton(
36+
value=True,
37+
disabled=False,
38+
icon="hand-pointer",
39+
layout=Layout(width="auto"),
40+
tooltip="panzoom controller",
41+
)
42+
self.maintain_aspect_button = ToggleButton(
43+
value=True,
44+
disabled=False,
45+
description="1:1",
46+
layout=Layout(width="auto"),
47+
tooltip="maintain aspect",
48+
)
49+
self.maintain_aspect_button.style.font_weight = "bold"
50+
self.flip_camera_button = Button(
51+
value=False,
52+
disabled=False,
53+
icon="arrow-up",
54+
layout=Layout(width="auto"),
55+
tooltip="y-axis direction",
56+
)
57+
58+
self.record_button = ToggleButton(
59+
value=False,
60+
disabled=False,
61+
icon="video",
62+
layout=Layout(width="auto"),
63+
tooltip="record",
64+
)
65+
66+
positions = list(product(range(self.plot.shape[0]), range(self.plot.shape[1])))
67+
values = list()
68+
for pos in positions:
69+
if self.plot[pos].name is not None:
70+
values.append(self.plot[pos].name)
71+
else:
72+
values.append(str(pos))
73+
self.dropdown = Dropdown(
74+
options=values,
75+
disabled=False,
76+
description="Subplots:",
77+
layout=Layout(width="200px"),
78+
)
79+
80+
self.widget = HBox(
81+
[
82+
self.autoscale_button,
83+
self.center_scene_button,
84+
self.panzoom_controller_button,
85+
self.maintain_aspect_button,
86+
self.flip_camera_button,
87+
self.record_button,
88+
self.dropdown,
89+
]
90+
)
91+
92+
self.panzoom_controller_button.observe(self.panzoom_control, "value")
93+
self.autoscale_button.on_click(self.auto_scale)
94+
self.center_scene_button.on_click(self.center_scene)
95+
self.maintain_aspect_button.observe(self.maintain_aspect, "value")
96+
self.flip_camera_button.on_click(self.flip_camera)
97+
self.record_button.observe(self.record_plot, "value")
98+
99+
self.plot.renderer.add_event_handler(self.update_current_subplot, "click")
100+
101+
@property
102+
def current_subplot(self) -> Subplot:
103+
# parses dropdown value as plot name or position
104+
current = self.dropdown.value
105+
if current[0] == "(":
106+
return self.plot[eval(current)]
107+
else:
108+
return self.plot[current]
109+
110+
def auto_scale(self, obj):
111+
current = self.current_subplot
112+
current.auto_scale(maintain_aspect=current.camera.maintain_aspect)
113+
114+
def center_scene(self, obj):
115+
current = self.current_subplot
116+
current.center_scene()
117+
118+
def panzoom_control(self, obj):
119+
current = self.current_subplot
120+
current.controller.enabled = self.panzoom_controller_button.value
121+
122+
def maintain_aspect(self, obj):
123+
current = self.current_subplot
124+
current.camera.maintain_aspect = self.maintain_aspect_button.value
125+
126+
def flip_camera(self, obj):
127+
current = self.current_subplot
128+
current.camera.local.scale_y *= -1
129+
if current.camera.local.scale_y == -1:
130+
self.flip_camera_button.icon = "arrow-down"
131+
else:
132+
self.flip_camera_button.icon = "arrow-up"
133+
134+
def update_current_subplot(self, ev):
135+
for subplot in self.plot:
136+
pos = subplot.map_screen_to_world((ev.x, ev.y))
137+
if pos is not None:
138+
# update self.dropdown
139+
if subplot.name is None:
140+
self.dropdown.value = str(subplot.position)
141+
else:
142+
self.dropdown.value = subplot.name
143+
self.panzoom_controller_button.value = subplot.controller.enabled
144+
self.maintain_aspect_button.value = subplot.camera.maintain_aspect
145+
146+
def record_plot(self, obj):
147+
if self.record_button.value:
148+
try:
149+
self.plot.record_start(
150+
f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4"
151+
)
152+
except Exception:
153+
traceback.print_exc()
154+
self.record_button.value = False
155+
else:
156+
self.plot.record_stop()

fastplotlib/layouts/_subplot.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
from ..graphics import TextGraphic
1818
from ._utils import make_canvas_and_renderer
19-
from ._base import PlotArea
19+
from ._plot_area import PlotArea
2020
from ._defaults import create_camera, create_controller
2121
from .graphic_methods_mixin import GraphicMethodsMixin
2222

0 commit comments

Comments
 (0)