diff --git a/examples/buttons.ipynb b/examples/buttons.ipynb index 60419a229..b46e09d5f 100644 --- a/examples/buttons.ipynb +++ b/examples/buttons.ipynb @@ -4,11 +4,13 @@ "cell_type": "code", "execution_count": 1, "id": "6725ce7d-eea7-44f7-bedc-813e8ce5bf4f", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "from fastplotlib import Plot\n", - "from ipywidgets import HBox, Checkbox, Image, VBox, Layout, ToggleButton, Button\n", + "from fastplotlib import Plot, GridPlot, ImageWidget\n", + "from ipywidgets import HBox, Checkbox, Image, VBox, Layout, ToggleButton, Button, Dropdown\n", "import numpy as np" ] }, @@ -16,12 +18,14 @@ "cell_type": "code", "execution_count": 2, "id": "33bf59c4-14e5-43a8-8a16-69b6859864c5", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "95be7ab3326347359f783946ec8d9339", + "model_id": "5039947949ac4d5da76e561e082da8c2", "version_major": 2, "version_minor": 0 }, @@ -36,14 +40,14 @@ "name": "stderr", "output_type": "stream", "text": [ - "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/features/_base.py:33: UserWarning: converting float64 array to float32\n", + "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/features/_base.py:34: UserWarning: converting float64 array to float32\n", " warn(f\"converting {array.dtype} array to float32\")\n" ] }, { "data": { "text/plain": [ - "" + "" ] }, "execution_count": 2, @@ -64,12 +68,14 @@ "cell_type": "code", "execution_count": 3, "id": "68ea8011-d6fd-448f-9bf6-34073164d271", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "cecbc6a1fec54d03876ac5a8609e5200", + "model_id": "5819995040a04300b5ccf10abac8b69e", "version_major": 2, "version_minor": 0 }, @@ -89,24 +95,15 @@ { "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": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "05c6d9d2f62846e8ab6135a3d218964c", + "model_id": "2f318829d0a4419798a008c5fe2d6677", "version_major": 2, "version_minor": 0 }, @@ -126,9 +123,11 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "e96bbda7-3693-42f2-bd52-f668f39134f6", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "img = np.random.rand(512,512)\n", @@ -138,14 +137,16 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "03b877ba-cf9c-47d9-a0e5-b3e694274a28", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9b792858ff24411db756810bb8eea00f", + "model_id": "ff5064077bb944c6a5248f135f052668", "version_major": 2, "version_minor": 0 }, @@ -153,7 +154,7 @@ "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -162,16 +163,63 @@ "gp.show()" ] }, + { + "cell_type": "code", + "execution_count": 7, + "id": "36f5e040-cc58-4b0a-beb1-1f66ea02ccb9", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "f21de0c607b24fd281364a7bec8ad837", + "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": 8, + "id": "c6753d45-a0ae-4c96-8ed5-7638c4cf24e3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "for subplot in gp2:\n", + " subplot.add_image(data=img)" + ] + }, { "cell_type": "code", "execution_count": 9, - "id": "afc0cd52-fb24-4561-9876-50fbdf784502", - "metadata": {}, + "id": "5a769c0f-6d95-4969-ad9d-24636fc74b18", + "metadata": { + "tags": [] + }, "outputs": [ { "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "fcb816a9aaab42b3a7a7e443607ad127", + "version_major": 2, + "version_minor": 0 + }, "text/plain": [ - "(0, 1)" + "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" ] }, "execution_count": 9, @@ -180,19 +228,33 @@ } ], "source": [ - "gp[0,1].position" + "gp2.show()" ] }, { "cell_type": "code", - "execution_count": 8, - "id": "36f5e040-cc58-4b0a-beb1-1f66ea02ccb9", - "metadata": {}, + "execution_count": 2, + "id": "3958829a-1a2b-4aa2-8c9d-408dce9ccf30", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "data = np.random.rand(500, 512, 512)" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "9f60c0b0-d0ee-4ea1-b961-708aff3b91ae", + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f902391ceb614f2a812725371184ce81", + "model_id": "433485ec12b44aa68082a93c264e613c", "version_major": 2, "version_minor": 0 }, @@ -202,53 +264,115 @@ }, "metadata": {}, "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/features/_base.py:34: UserWarning: converting float64 array to float32\n", + " warn(f\"converting {array.dtype} array to float32\")\n" + ] } ], "source": [ - "gp2 = GridPlot(shape=(1,2))" + "iw = ImageWidget(data=data, vmin_vmax_sliders=True)" ] }, { "cell_type": "code", - "execution_count": 9, - "id": "c6753d45-a0ae-4c96-8ed5-7638c4cf24e3", - "metadata": {}, - "outputs": [], + "execution_count": 4, + "id": "346f241c-4bd0-4f90-afd5-3fa617d96dad", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "a1bdd59532f240948d33ae440d61c4a5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layo…" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "for subplot in gp2:\n", - " subplot.add_image(data=img)" + "iw.show()" ] }, { "cell_type": "code", - "execution_count": 10, - "id": "5a769c0f-6d95-4969-ad9d-24636fc74b18", - "metadata": {}, + "execution_count": 5, + "id": "0563ddfb-8fd3-4a99-bcee-3a83ae5d0f32", + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "65a2a30036f44e22a479c6edd215e25a", + "model_id": "79e924078e7f4cf69be71eaf12a94854", "version_major": 2, "version_minor": 0 }, "text/plain": [ - "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], + "source": [ + "iw2 = ImageWidget(data=[data, data, data, data], grid_plot_kwargs={'controllers': np.array([[0, 1], [2, 3]])}, slider_dims=\"t\", vmin_vmax_sliders=True)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "d7fb94f8-825f-4447-b161-c9dafa1a068a", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "bb3b7dd92b4d4f74ace4adfbca35aaf3", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "VBox(children=(VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layo…" ] }, - "execution_count": 10, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ - "gp2.show()" + "iw2.show()" ] }, { "cell_type": "code", "execution_count": null, - "id": "cd3e7b3c-f4d2-44c7-931c-d172c7bdad36", + "id": "3743219d-6702-468a-bea6-0e4c4549e9e4", + "metadata": {}, + "outputs": [], + "source": [] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68bafb86-db5a-4681-8176-37ec72ce04a8", "metadata": {}, "outputs": [], "source": [] @@ -270,7 +394,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.9.2" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 57cd15a1d..439cba484 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -138,6 +138,9 @@ def remove_event_handler(self, handler: callable): self._event_handlers.remove(handler) + def clear_event_handlers(self): + self._event_handlers.clear() + #TODO: maybe this can be implemented right here in the base class @abstractmethod def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): @@ -227,10 +230,12 @@ def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]: # return slice(int(start), int(stop), int(step)) -def cleanup_array_slice(key: np.ndarray, upper_bound) -> np.ndarray: +def cleanup_array_slice(key: np.ndarray, upper_bound) -> Union[np.ndarray, None]: """ Cleanup numpy array used for fancy indexing, make sure key[-1] <= upper_bound. + Returns None if nothing to change. + Parameters ---------- key: np.ndarray @@ -254,6 +259,9 @@ def cleanup_array_slice(key: np.ndarray, upper_bound) -> np.ndarray: if key.dtype == bool: key = np.nonzero(key)[0] + if key.size < 1: + return None + # make sure indices within bounds of feature buffer range if key[-1] > upper_bound: raise IndexError(f"Index: `{key[-1]}` out of bounds for feature array of size: `{upper_bound}`") diff --git a/fastplotlib/graphics/features/_colors.py b/fastplotlib/graphics/features/_colors.py index 5ff82ca72..50bb0ce3f 100644 --- a/fastplotlib/graphics/features/_colors.py +++ b/fastplotlib/graphics/features/_colors.py @@ -129,6 +129,9 @@ def __setitem__(self, key, value): elif isinstance(key, np.ndarray): key = cleanup_array_slice(key, self._upper_bound) + if key is None: + return + indices = key else: diff --git a/fastplotlib/graphics/selectors/_base_selector.py b/fastplotlib/graphics/selectors/_base_selector.py index 64934b6fa..7d108efc6 100644 --- a/fastplotlib/graphics/selectors/_base_selector.py +++ b/fastplotlib/graphics/selectors/_base_selector.py @@ -324,4 +324,10 @@ def __del__(self): self._plot_area.renderer.remove_event_handler(self._key_up, "key_up") # remove animation func - self._plot_area.remove_animation(self._key_hold) \ No newline at end of file + self._plot_area.remove_animation(self._key_hold) + + if hasattr(self, "feature_events"): + feature_names = getattr(self, "feature_events") + for n in feature_names: + fea = getattr(self, n) + fea.clear_event_handlers() diff --git a/fastplotlib/graphics/selectors/_linear_region.py b/fastplotlib/graphics/selectors/_linear_region.py index 30f223fad..17751f51e 100644 --- a/fastplotlib/graphics/selectors/_linear_region.py +++ b/fastplotlib/graphics/selectors/_linear_region.py @@ -11,6 +11,9 @@ class LinearBoundsFeature(GraphicFeature): + feature_events = ( + "data", + ) """ Feature for a linearly bounding region @@ -121,10 +124,6 @@ def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): class LinearRegionSelector(Graphic, BaseSelector): - feature_events = ( - "bounds" - ) - def __init__( self, bounds: Tuple[int, int], diff --git a/fastplotlib/layouts/_base.py b/fastplotlib/layouts/_base.py index f3781a4e7..105eabb64 100644 --- a/fastplotlib/layouts/_base.py +++ b/fastplotlib/layouts/_base.py @@ -394,6 +394,9 @@ def clear(self): for g in self.graphics: self.delete_graphic(g) + for s in self.selectors: + self.delete_graphic(s) + def __getitem__(self, name: str): for graphic in self.graphics: if graphic.name == name: diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 734d004d4..517bf5b53 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -1,4 +1,5 @@ -import itertools +import traceback +from datetime import datetime from itertools import product import numpy as np from typing import * @@ -169,8 +170,8 @@ def __init__( self._current_iter = None self._starting_size = size - - super(RecordMixin, self).__init__() + + RecordMixin.__init__(self) def __getitem__(self, index: Union[Tuple[int, int], str]) -> Subplot: if isinstance(index, str): @@ -277,24 +278,24 @@ def show(self, toolbar: bool = True): for subplot in self: subplot.auto_scale(maintain_aspect=True, zoom=0.95) - + self.canvas.set_logical_size(*self._starting_size) - # check if in jupyter notebook or not - if not isinstance(self.canvas, JupyterWgpuCanvas): + # check if in jupyter notebook, or if toolbar is False + if (not isinstance(self.canvas, JupyterWgpuCanvas)) or (not toolbar): 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 + if self.toolbar is None: + self.toolbar = GridPlotToolBar(self) + + return VBox([self.canvas, self.toolbar.widget]) def close(self): self.canvas.close() + if self.toolbar is not None: + self.toolbar.widget.close() + def _get_iterator(self): return product(range(self.shape[0]), range(self.shape[1])) @@ -331,9 +332,12 @@ def __init__(self, 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', + self.flip_camera_button = Button(value=False, disabled=False, icon='arrows-v', layout=Layout(width='auto'), tooltip='flip') + self.record_button = ToggleButton(value=False, disabled=False, icon='video', + layout=Layout(width='auto'), tooltip='record') + positions = list(product(range(self.plot.shape[0]), range(self.plot.shape[1]))) values = list() for pos in positions: @@ -341,13 +345,15 @@ def __init__(self, values.append(self.plot[pos].name) else: values.append(str(pos)) - self.dropdown = Dropdown(options=values, disabled=False, description='Subplots:') + self.dropdown = Dropdown(options=values, disabled=False, description='Subplots:', + layout=Layout(width='200px')) self.widget = HBox([self.autoscale_button, self.center_scene_button, self.panzoom_controller_button, self.maintain_aspect_button, self.flip_camera_button, + self.record_button, self.dropdown]) self.panzoom_controller_button.observe(self.panzoom_control, 'value') @@ -355,6 +361,7 @@ def __init__(self, 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.record_button.observe(self.record_plot, 'value') self.plot.renderer.add_event_handler(self.update_current_subplot, "click") @@ -399,3 +406,12 @@ def update_current_subplot(self, ev): self.panzoom_controller_button.value = subplot.controller.enabled self.maintain_aspect_button.value = subplot.camera.maintain_aspect + def record_plot(self, obj): + if self.record_button.value: + try: + self.plot.record_start(f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4") + except Exception: + traceback.print_exc() + self.record_button.value = False + else: + self.plot.record_stop() diff --git a/fastplotlib/layouts/_toolbar.py b/fastplotlib/layouts/_toolbar.py deleted file mode 100644 index 8b1378917..000000000 --- a/fastplotlib/layouts/_toolbar.py +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fastplotlib/plot.py b/fastplotlib/plot.py index 6f8e1bc44..3514d95fa 100644 --- a/fastplotlib/plot.py +++ b/fastplotlib/plot.py @@ -1,11 +1,12 @@ from typing import * import pygfx 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 +from datetime import datetime +import traceback class Plot(Subplot, RecordMixin): @@ -90,7 +91,7 @@ def __init__( controller=controller, **kwargs ) - super(RecordMixin, self).__init__() + RecordMixin.__init__(self) self._starting_size = size @@ -118,22 +119,22 @@ def show(self, autoscale: bool = True, toolbar: bool = True): self.canvas.set_logical_size(*self._starting_size) - # check if in jupyter notebook or not - if not isinstance(self.canvas, JupyterWgpuCanvas): + # check if in jupyter notebook, or if toolbar is False + if (not isinstance(self.canvas, JupyterWgpuCanvas)) or (not toolbar): 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 + if self.toolbar is None: + self.toolbar = ToolBar(self) + + return VBox([self.canvas, self.toolbar.widget]) def close(self): self.canvas.close() - + if self.toolbar is not None: + self.toolbar.widget.close() + + class ToolBar: def __init__(self, plot: Plot): @@ -156,20 +157,24 @@ def __init__(self, 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', + self.flip_camera_button = Button(value=False, disabled=False, icon='arrows-v', layout=Layout(width='auto'), tooltip='flip') + self.record_button = ToggleButton(value=False, disabled=False, icon='video', + layout=Layout(width='auto'), tooltip='record') self.widget = HBox([self.autoscale_button, self.center_scene_button, self.panzoom_controller_button, self.maintain_aspect_button, - self.flip_camera_button]) + self.flip_camera_button, + self.record_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) + self.record_button.observe(self.record_plot, 'value') def auto_scale(self, obj): self.plot.auto_scale(maintain_aspect=self.plot.camera.maintain_aspect) @@ -185,3 +190,13 @@ def maintain_aspect(self, obj): def flip_camera(self, obj): self.plot.camera.world.scale_y *= -1 + + def record_plot(self, obj): + if self.record_button.value: + try: + self.plot.record_start(f"./{datetime.now().isoformat(timespec='seconds').replace(':', '_')}.mp4") + except Exception: + traceback.print_exc() + self.record_button.value = False + else: + self.plot.record_stop() diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 32f444a32..fc82a719c 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -1,15 +1,22 @@ +import traceback +from datetime import datetime +from itertools import product + +from ipywidgets import Dropdown +from wgpu.gui.jupyter import JupyterWgpuCanvas + +from ..layouts._subplot import Subplot from ..plot import Plot from ..layouts import GridPlot from ..graphics import ImageGraphic from ..utils import quick_min_max -from ipywidgets.widgets import IntSlider, VBox, HBox, Layout, FloatRangeSlider +from ipywidgets.widgets import IntSlider, VBox, HBox, Layout, FloatRangeSlider, Button, BoundedIntText, Play, jslink import numpy as np from typing import * from warnings import warn from functools import partial from copy import deepcopy - DEFAULT_DIMS_ORDER = \ { 2: "xy", @@ -238,6 +245,7 @@ def __init__( passed to fastplotlib.graphics.Image """ self._names = None + self.toolbar = None if isinstance(data, list): # verify that it's a list of np.ndarray @@ -335,7 +343,8 @@ def __init__( f"index {data_ix} out of bounds for `dims_order`, the bounds are 0 - {len(self.data)}" ) else: - raise TypeError(f"`dims_order` must be a or , you have passed a: <{type(dims_order)}>") + raise TypeError( + f"`dims_order` must be a or , you have passed a: <{type(dims_order)}>") if not len(self.dims_order[0]) == self.ndim: raise ValueError( @@ -589,8 +598,7 @@ def __init__( self.block_sliders: bool = False # TODO: So just stack everything vertically for now - self.widget = VBox([ - self.plot.canvas, + self._vbox_sliders = VBox([ *list(self._sliders.values()), *self.vmin_vmax_sliders ]) @@ -847,7 +855,7 @@ def reset_vmin_vmax(self): self.vmin_vmax_sliders[i].set_state(state) - def show(self): + def show(self, toolbar: bool = True): """ Show the widget @@ -856,7 +864,62 @@ def show(self): VBox ``ipywidgets.VBox`` stacking the plotter and sliders in a vertical layout """ - # start render loop - self.plot.show() - return self.widget + if not isinstance(self.plot.canvas, JupyterWgpuCanvas): + raise TypeError("ImageWidget is currently not supported outside of Jupyter") + + # check if in jupyter notebook, or if toolbar is False + if (not isinstance(self.plot.canvas, JupyterWgpuCanvas)) or (not toolbar): + return VBox([self.plot.show(toolbar=False), self._vbox_sliders]) + + if self.toolbar is None: + self.toolbar = ImageWidgetToolbar(self) + + return VBox( + [ + self.plot.show(toolbar=True), + self.toolbar.widget, + self._vbox_sliders, + ] + ) + + +class ImageWidgetToolbar: + def __init__(self, + iw: ImageWidget): + """ + Basic toolbar for a ImageWidget instance. + + Parameters + ---------- + plot: + """ + self.iw = iw + self.plot = iw.plot + + self.reset_vminvmax_button = Button(value=False, disabled=False, icon='adjust', + layout=Layout(width='auto'), tooltip='reset vmin/vmax') + + self.step_size_setter = BoundedIntText(value=1, min=1, max=self.iw.sliders['t'].max, step=1, + description='Step Size:', disabled=False, + description_tooltip='set slider step', layout=Layout(width='150px')) + self.play_button = Play( + value=0, + min=iw.sliders["t"].min, + max=iw.sliders["t"].max, + step=iw.sliders["t"].step, + description="play/pause", + disabled=False) + + self.widget = HBox([self.reset_vminvmax_button, self.play_button, self.step_size_setter]) + + self.reset_vminvmax_button.on_click(self.reset_vminvmax) + self.step_size_setter.observe(self.change_stepsize, 'value') + jslink((self.play_button, 'value'), (self.iw.sliders["t"], 'value')) + + def reset_vminvmax(self, obj): + if len(self.iw.vmin_vmax_sliders) != 0: + self.iw.reset_vmin_vmax() + + def change_stepsize(self, obj): + self.iw.sliders['t'].step = self.step_size_setter.value