diff --git a/examples/buttons.ipynb b/examples/buttons.ipynb new file mode 100644 index 000000000..60419a229 --- /dev/null +++ b/examples/buttons.ipynb @@ -0,0 +1,278 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "6725ce7d-eea7-44f7-bedc-813e8ce5bf4f", + "metadata": {}, + "outputs": [], + "source": [ + "from fastplotlib import Plot\n", + "from ipywidgets import HBox, Checkbox, Image, VBox, Layout, ToggleButton, Button\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "33bf59c4-14e5-43a8-8a16-69b6859864c5", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "95be7ab3326347359f783946ec8d9339", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/features/_base.py:33: UserWarning: converting float64 array to float32\n", + " warn(f\"converting {array.dtype} array to float32\")\n" + ] + }, + { + "data": { + "text/plain": [ + "" + ] + }, + "execution_count": 2, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot = Plot()\n", + "xs = np.linspace(-10, 10, 100)\n", + "# sine wave\n", + "ys = np.sin(xs)\n", + "sine = np.dstack([xs, ys])[0]\n", + "plot.add_line(sine)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "68ea8011-d6fd-448f-9bf6-34073164d271", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "cecbc6a1fec54d03876ac5a8609e5200", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" + ] + }, + "execution_count": 3, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0bbb459c-cb49-448e-b0b8-c541e55da313", + "metadata": {}, + "outputs": [], + "source": [ + "from fastplotlib import GridPlot\n", + "from ipywidgets import Dropdown" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "91a31531-818b-46a2-9587-5d9ef5b59b93", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "05c6d9d2f62846e8ab6135a3d218964c", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "gp = GridPlot(\n", + " shape=(1,2),\n", + " names=[['plot1', 'plot2']])" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "e96bbda7-3693-42f2-bd52-f668f39134f6", + "metadata": {}, + "outputs": [], + "source": [ + "img = np.random.rand(512,512)\n", + "for subplot in gp:\n", + " subplot.add_image(data=img)" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "03b877ba-cf9c-47d9-a0e5-b3e694274a28", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9b792858ff24411db756810bb8eea00f", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" + ] + }, + "execution_count": 7, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gp.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "afc0cd52-fb24-4561-9876-50fbdf784502", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(0, 1)" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gp[0,1].position" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "36f5e040-cc58-4b0a-beb1-1f66ea02ccb9", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f902391ceb614f2a812725371184ce81", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "gp2 = GridPlot(shape=(1,2))" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "c6753d45-a0ae-4c96-8ed5-7638c4cf24e3", + "metadata": {}, + "outputs": [], + "source": [ + "for subplot in gp2:\n", + " subplot.add_image(data=img)" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "5a769c0f-6d95-4969-ad9d-24636fc74b18", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "65a2a30036f44e22a479c6edd215e25a", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" + ] + }, + "execution_count": 10, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "gp2.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cd3e7b3c-f4d2-44c7-931c-d172c7bdad36", + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.9.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index b625622ef..c121e42f2 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -1,3 +1,4 @@ +import itertools from itertools import product import numpy as np from typing import * @@ -7,6 +8,8 @@ from wgpu.gui.auto import WgpuCanvas from ._defaults import create_controller from ._subplot import Subplot +from ipywidgets import HBox, Layout, Button, ToggleButton, VBox, Dropdown +from wgpu.gui.jupyter import JupyterWgpuCanvas from ._record_mixin import RecordMixin @@ -69,6 +72,7 @@ def __init__( """ self.shape = shape + self.toolbar = None if isinstance(cameras, str): if cameras not in valid_cameras: @@ -259,7 +263,7 @@ def remove_animation(self, func): if func in self._animate_funcs_post: self._animate_funcs_post.remove(func) - def show(self): + def show(self, toolbar: bool = True): """ begins the rendering event loop and returns the canvas @@ -273,10 +277,20 @@ def show(self): for subplot in self: subplot.auto_scale(maintain_aspect=True, zoom=0.95) - + self.canvas.set_logical_size(*self._starting_size) - return self.canvas + # check if in jupyter notebook or not + if not isinstance(self.canvas, JupyterWgpuCanvas): + return self.canvas + + if toolbar and self.toolbar is None: + self.toolbar = GridPlotToolBar(self).widget + return VBox([self.canvas, self.toolbar]) + elif toolbar and self.toolbar is not None: + return VBox([self.canvas, self.toolbar]) + else: + return self.canvas def close(self): self.canvas.close() @@ -294,3 +308,94 @@ def __next__(self) -> Subplot: def __repr__(self): return f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}\n" + + +class GridPlotToolBar: + def __init__(self, + plot: GridPlot): + """ + Basic toolbar for a GridPlot instance. + + Parameters + ---------- + plot: + """ + self.plot = plot + + self.autoscale_button = Button(value=False, disabled=False, icon='expand-arrows-alt', + layout=Layout(width='auto'), tooltip='auto-scale scene') + self.center_scene_button = Button(value=False, disabled=False, icon='align-center', + layout=Layout(width='auto'), tooltip='auto-center scene') + self.panzoom_controller_button = ToggleButton(value=True, disabled=False, icon='hand-pointer', + layout=Layout(width='auto'), tooltip='panzoom controller') + self.maintain_aspect_button = ToggleButton(value=True, disabled=False, description="1:1", + layout=Layout(width='auto'), tooltip='maintain aspect') + self.maintain_aspect_button.style.font_weight = "bold" + self.flip_camera_button = Button(value=False, disabled=False, icon='sync-alt', + layout=Layout(width='auto'), tooltip='flip') + + positions = list(product(range(self.plot.shape[0]), range(self.plot.shape[1]))) + values = list() + for pos in positions: + if self.plot[pos].name is not None: + values.append(self.plot[pos].name) + else: + values.append(str(pos)) + self.dropdown = Dropdown(options=values, disabled=False, description='Subplots:') + + self.widget = HBox([self.autoscale_button, + self.center_scene_button, + self.panzoom_controller_button, + self.maintain_aspect_button, + self.flip_camera_button, + self.dropdown]) + + self.panzoom_controller_button.observe(self.panzoom_control, 'value') + self.autoscale_button.on_click(self.auto_scale) + self.center_scene_button.on_click(self.center_scene) + self.maintain_aspect_button.observe(self.maintain_aspect, 'value') + self.flip_camera_button.on_click(self.flip_camera) + + self.plot.renderer.add_event_handler(self.update_current_subplot, "click") + + @property + def current_subplot(self) -> Subplot: + # parses dropdown value as plot name or position + current = self.dropdown.value + if current[0] == "(": + return self.plot[eval(current)] + else: + return self.plot[current] + + def auto_scale(self, obj): + current = self.current_subplot + current.auto_scale(maintain_aspect=current.camera.maintain_aspect) + + def center_scene(self, obj): + current = self.current_subplot + current.center_scene() + + def panzoom_control(self, obj): + current = self.current_subplot + current.controller.enabled = self.panzoom_controller_button.value + + def maintain_aspect(self, obj): + current = self.current_subplot + current.camera.maintain_aspect = self.maintain_aspect_button.value + + def flip_camera(self, obj): + current = self.current_subplot + current.camera.scale.y = -1 * current.camera.scale.y + + def update_current_subplot(self, ev): + for subplot in self.plot: + pos = subplot.map_screen_to_world((ev.x, ev.y)) + if pos is not None: + # update self.dropdown + if subplot.name is None: + self.dropdown.value = str(subplot.position) + else: + self.dropdown.value = subplot.name + self.panzoom_controller_button.value = subplot.controller.enabled + self.maintain_aspect_button.value = subplot.camera.maintain_aspect + diff --git a/fastplotlib/layouts/_toolbar.py b/fastplotlib/layouts/_toolbar.py new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/fastplotlib/layouts/_toolbar.py @@ -0,0 +1 @@ + diff --git a/fastplotlib/plot.py b/fastplotlib/plot.py index 0e286d588..105dbfb96 100644 --- a/fastplotlib/plot.py +++ b/fastplotlib/plot.py @@ -3,6 +3,8 @@ from wgpu.gui.auto import WgpuCanvas from .layouts._subplot import Subplot +from ipywidgets import HBox, Layout, Button, ToggleButton, VBox +from wgpu.gui.jupyter import JupyterWgpuCanvas from .layouts._record_mixin import RecordMixin @@ -92,13 +94,15 @@ def __init__( self._starting_size = size + self.toolbar = None + def render(self): super(Plot, self).render() self.renderer.flush() self.canvas.request_draw() - def show(self, autoscale: bool = True): + def show(self, autoscale: bool = True, toolbar: bool = True): """ begins the rendering event loop and returns the canvas @@ -111,10 +115,74 @@ def show(self, autoscale: bool = True): self.canvas.request_draw(self.render) if autoscale: self.auto_scale(maintain_aspect=True, zoom=0.95) - + self.canvas.set_logical_size(*self._starting_size) - return self.canvas + # check if in jupyter notebook or not + if not isinstance(self.canvas, JupyterWgpuCanvas): + return self.canvas + + if toolbar and self.toolbar is None: + self.toolbar = ToolBar(self).widget + return VBox([self.canvas, self.toolbar]) + elif toolbar and self.toolbar is not None: + return VBox([self.canvas, self.toolbar]) + else: + return self.canvas def close(self): self.canvas.close() + + +class ToolBar: + def __init__(self, + plot: Plot): + """ + Basic toolbar for a Plot instance. + + Parameters + ---------- + plot: encapsulated plot instance that will be manipulated using the toolbar buttons + """ + self.plot = plot + + self.autoscale_button = Button(value=False, disabled=False, icon='expand-arrows-alt', + layout=Layout(width='auto'), tooltip='auto-scale scene') + self.center_scene_button = Button(value=False, disabled=False, icon='align-center', + layout=Layout(width='auto'), tooltip='auto-center scene') + self.panzoom_controller_button = ToggleButton(value=True, disabled=False, icon='hand-pointer', + layout=Layout(width='auto'), tooltip='panzoom controller') + self.maintain_aspect_button = ToggleButton(value=True, disabled=False, description="1:1", + layout=Layout(width='auto'), + tooltip='maintain aspect') + self.maintain_aspect_button.style.font_weight = "bold" + self.flip_camera_button = Button(value=False, disabled=False, icon='sync-alt', + layout=Layout(width='auto'), tooltip='flip') + + self.widget = HBox([self.autoscale_button, + self.center_scene_button, + self.panzoom_controller_button, + self.maintain_aspect_button, + self.flip_camera_button]) + + self.panzoom_controller_button.observe(self.panzoom_control, 'value') + self.autoscale_button.on_click(self.auto_scale) + self.center_scene_button.on_click(self.center_scene) + self.maintain_aspect_button.observe(self.maintain_aspect, 'value') + self.flip_camera_button.on_click(self.flip_camera) + + def auto_scale(self, obj): + self.plot.auto_scale(maintain_aspect=self.plot.camera.maintain_aspect) + + def center_scene(self, obj): + self.plot.center_scene() + + def panzoom_control(self, obj): + self.plot.controller.enabled = self.panzoom_controller_button.value + + def maintain_aspect(self, obj): + self.plot.camera.maintain_aspect = self.maintain_aspect_button.value + + def flip_camera(self, obj): + self.plot.camera.scale.y = -1 * self.plot.camera.scale.y + \ No newline at end of file