From 720aeb25dcd302d32d6ebe940f12cf97cca70679 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 7 Sep 2023 16:40:33 -0400 Subject: [PATCH 01/10] add jupyter-sidecar as a dependency, update simple notebook --- examples/notebooks/simple.ipynb | 92 +++++++++++++++++++++++++++++---- setup.py | 3 +- 2 files changed, 85 insertions(+), 10 deletions(-) diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb index e994bfba8..601d21aaf 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/simple.ipynb @@ -55,7 +55,8 @@ "source": [ "from fastplotlib import Plot\n", "from ipywidgets import VBox, HBox, IntSlider\n", - "import numpy as np" + "import numpy as np\n", + "from sidecar import Sidecar" ] }, { @@ -76,7 +77,9 @@ "id": "a9b386ac-9218-4f8f-97b3-f29b4201ef55", "metadata": {}, "source": [ - "## Simple image" + "## Simple image\n", + "\n", + "We are going to be using `jupyterlab-sidecar` to render some of the plots on the side. This makes it very easy to interact with your plots without having to constantly scroll up and down :D" ] }, { @@ -97,8 +100,12 @@ "# plot the image data\n", "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", "\n", - "# show the plot\n", - "plot.show()" + "# display plot in sidecar\n", + "sc = Sidecar(title=\"sample image\", layout={'width': '800px'})\n", + "\n", + "with sc:\n", + " # show the plot\n", + " display(plot.show())" ] }, { @@ -429,6 +436,17 @@ "image_graphic == plot[\"sample-image\"]" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "058d9785-a692-46f6-a062-cdec9c040afe", + "metadata": {}, + "outputs": [], + "source": [ + "# close sidecar\n", + "sc.close()" + ] + }, { "cell_type": "markdown", "id": "5694dca1-1041-4e09-a1da-85b293c5af47", @@ -452,7 +470,10 @@ "\n", "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", "\n", - "plot_rgb.show()" + "sc = Sidecar(title=\"RGB image\", layout={'width': '800px'})\n", + "\n", + "with sc:\n", + " display(plot_rgb.show())" ] }, { @@ -500,6 +521,17 @@ "plot_test(\"astronaut_RGB\", plot_rgb)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "8316b4f2-3d6e-46b5-8776-c7c963a7aa99", + "metadata": {}, + "outputs": [], + "source": [ + "# close sidecar\n", + "sc.close()" + ] + }, { "cell_type": "markdown", "id": "1cb03f42-1029-4b16-a16b-35447d9e2955", @@ -541,8 +573,11 @@ "#add this as an animation function\n", "plot_v.add_animations(update_data)\n", "\n", - "# show the plot\n", - "plot_v.show()" + "sc = Sidecar(title=\"random image\", layout={'width': '800px'})\n", + "\n", + "with sc:\n", + " # show the plot\n", + " display(plot_v.show())" ] }, { @@ -615,6 +650,17 @@ "HBox([plot_v.show(), plot_sync.show()])" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "f33f4cd9-02fc-41b7-961b-9dfeb455b63a", + "metadata": {}, + "outputs": [], + "source": [ + "# close sidecar\n", + "sc.close()" + ] + }, { "cell_type": "markdown", "id": "e7859338-8162-408b-ac72-37e606057045", @@ -688,7 +734,10 @@ "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", "sinc_graphic = plot_l.add_line(data=sinc, thickness=5, colors = colors)\n", "\n", - "plot_l.show()" + "sc = Sidecar(title=\"lines\", layout={'width': '800px'})\n", + "\n", + "with sc: \n", + " display(plot_l.show())" ] }, { @@ -971,6 +1020,17 @@ "plot_test(\"lines-underlay\", plot_l)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "bef729ea-f524-4efd-a189-bfca23b39af5", + "metadata": {}, + "outputs": [], + "source": [ + "# close sidecar\n", + "sc.close()" + ] + }, { "cell_type": "markdown", "id": "2c90862e-2f2a-451f-a468-0cf6b857e87a", @@ -1093,7 +1153,10 @@ "# use an alpha value since this will be a lot of points\n", "scatter_graphic = plot_s.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", "\n", - "plot_s.show()" + "sc = Sidecar()\n", + "\n", + "with sc:\n", + " display(plot_s.show())" ] }, { @@ -1159,6 +1222,17 @@ "scatter_graphic.data[n_points:n_points * 2, 0] = np.linspace(-40, 0, n_points)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9ffdde4-4b8e-4ff7-98b3-464cf5462d20", + "metadata": {}, + "outputs": [], + "source": [ + "# close sidecar\n", + "sc.close()" + ] + }, { "cell_type": "markdown", "id": "d9e554de-c436-4684-a46a-ce8a33d409ac", diff --git a/setup.py b/setup.py index 2616093fc..9a89d013b 100644 --- a/setup.py +++ b/setup.py @@ -26,7 +26,8 @@ [ "jupyterlab", "jupyter-rfb>=0.4.1", - "ipywidgets>=8.0.0,<9" + "ipywidgets>=8.0.0,<9", + "sidecar" ], "tests": From eb71003a6ab1b80a1cdeca5b0f80306996686140 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis <69729525+clewis7@users.noreply.github.com> Date: Thu, 7 Sep 2023 18:37:02 -0400 Subject: [PATCH 02/10] Update setup.py --- setup.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 9a89d013b..1d1204a69 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,8 @@ "jupyter-rfb>=0.4.1", "ipywidgets>=8.0.0,<9", "scikit-learn", - "tqdm" + "tqdm", + "sidecar" ] } From 7ba8dc776be05ae5ea8d0ae5cb3043d722bb63a8 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Mon, 11 Sep 2023 09:23:01 -0400 Subject: [PATCH 03/10] add sidecar for plots --- examples/notebooks/simple.ipynb | 70 ++++++++++++++++----------------- fastplotlib/layouts/_plot.py | 33 +++++++++++++++- setup.py | 3 +- 3 files changed, 67 insertions(+), 39 deletions(-) diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb index 601d21aaf..482e87117 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/simple.ipynb @@ -55,8 +55,7 @@ "source": [ "from fastplotlib import Plot\n", "from ipywidgets import VBox, HBox, IntSlider\n", - "import numpy as np\n", - "from sidecar import Sidecar" + "import numpy as np" ] }, { @@ -100,12 +99,8 @@ "# plot the image data\n", "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", "\n", - "# display plot in sidecar\n", - "sc = Sidecar(title=\"sample image\", layout={'width': '800px'})\n", - "\n", - "with sc:\n", - " # show the plot\n", - " display(plot.show())" + "# show the plot\n", + "plot.show(sidecar_kwargs={\"title\": \"sample image\", \"layout\": {'width': '800px'}})" ] }, { @@ -443,8 +438,8 @@ "metadata": {}, "outputs": [], "source": [ - "# close sidecar\n", - "sc.close()" + "# close the sidecar\n", + "plot.sidecar.close()" ] }, { @@ -470,10 +465,8 @@ "\n", "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", "\n", - "sc = Sidecar(title=\"RGB image\", layout={'width': '800px'})\n", - "\n", - "with sc:\n", - " display(plot_rgb.show())" + "# show the plot\n", + "plot_rgb.show(sidecar_kwargs={\"title\": \"RGB image\", \"layout\": {'width': '800px'}})" ] }, { @@ -529,7 +522,7 @@ "outputs": [], "source": [ "# close sidecar\n", - "sc.close()" + "plot_rgb.sidecar.close()" ] }, { @@ -573,11 +566,8 @@ "#add this as an animation function\n", "plot_v.add_animations(update_data)\n", "\n", - "sc = Sidecar(title=\"random image\", layout={'width': '800px'})\n", - "\n", - "with sc:\n", - " # show the plot\n", - " display(plot_v.show())" + "# show the plot\n", + "plot_v.show(sidecar_kwargs={\"title\": \"random image\", \"layout\": {'width': '800px'}})" ] }, { @@ -611,7 +601,7 @@ "\n", "plot_sync.add_animations(update_data_2)\n", "\n", - "plot_sync.show()" + "plot_sync.show(sidecar=False)" ] }, { @@ -637,7 +627,7 @@ "metadata": {}, "outputs": [], "source": [ - "VBox([plot_v.show(), plot_sync.show()])" + "VBox([plot_v.show(sidecar=False), plot_sync.show(sidecar=False)])" ] }, { @@ -647,7 +637,7 @@ "metadata": {}, "outputs": [], "source": [ - "HBox([plot_v.show(), plot_sync.show()])" + "HBox([plot_v.show(sidecar=False), plot_sync.show(sidecar=False)])" ] }, { @@ -658,7 +648,7 @@ "outputs": [], "source": [ "# close sidecar\n", - "sc.close()" + "plot_v.sidecar.close()" ] }, { @@ -734,10 +724,8 @@ "colors = [\"r\"] * 25 + [\"purple\"] * 25 + [\"y\"] * 25 + [\"b\"] * 25\n", "sinc_graphic = plot_l.add_line(data=sinc, thickness=5, colors = colors)\n", "\n", - "sc = Sidecar(title=\"lines\", layout={'width': '800px'})\n", - "\n", - "with sc: \n", - " display(plot_l.show())" + "# show the plot\n", + "plot_l.show(sidecar_kwargs={\"title\": \"lines\", \"layout\": {'width': '800px'}})" ] }, { @@ -1028,7 +1016,7 @@ "outputs": [], "source": [ "# close sidecar\n", - "sc.close()" + "plot_l.sidecar.close()" ] }, { @@ -1090,6 +1078,19 @@ "plot_test(\"lines-3d\", plot_l3d)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "c2c70541-98fe-4e02-a718-ac2857cc25be", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# close sidecar\n", + "plot_l3d.sidecar.close()" + ] + }, { "cell_type": "markdown", "id": "a202b3d0-2a0b-450a-93d4-76d0a1129d1d", @@ -1153,10 +1154,7 @@ "# use an alpha value since this will be a lot of points\n", "scatter_graphic = plot_s.add_scatter(data=cloud, sizes=3, colors=colors, alpha=0.7)\n", "\n", - "sc = Sidecar()\n", - "\n", - "with sc:\n", - " display(plot_s.show())" + "plot_s.show()" ] }, { @@ -1230,7 +1228,7 @@ "outputs": [], "source": [ "# close sidecar\n", - "sc.close()" + "plot_s.sidecar.close()" ] }, { @@ -1250,8 +1248,8 @@ "metadata": {}, "outputs": [], "source": [ - "row1 = HBox([plot.show(), plot_v.show(), plot_sync.show()])\n", - "row2 = HBox([plot_l.show(), plot_l3d.show(), plot_s.show()])\n", + "row1 = HBox([plot.show(sidecar=False), plot_v.show(sidecar=False), plot_sync.show(sidecar=False)])\n", + "row2 = HBox([plot_l.show(sidecar=False), plot_l3d.show(sidecar=False), plot_s.show(sidecar=False)])\n", "\n", "VBox([row1, row2])" ] diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 1f91bb303..4ff8dbd4d 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -4,10 +4,12 @@ import os import pygfx +from IPython.display import display from wgpu.gui.auto import WgpuCanvas, is_jupyter if is_jupyter(): from ipywidgets import HBox, Layout, Button, ToggleButton, VBox + from sidecar import Sidecar from ._subplot import Subplot from ._record_mixin import RecordMixin @@ -64,6 +66,7 @@ def __init__( self._starting_size = size self.toolbar = None + self.sidecar = None def render(self): super(Plot, self).render() @@ -72,7 +75,12 @@ def render(self): self.canvas.request_draw() def show( - self, autoscale: bool = True, maintain_aspect: bool = None, toolbar: bool = True + self, + autoscale: bool = True, + maintain_aspect: bool = None, + toolbar: bool = True, + sidecar: bool = True, + sidecar_kwargs: dict = None ): """ Begins the rendering event loop and returns the canvas @@ -88,6 +96,13 @@ def show( toolbar: bool, default True show toolbar + sidecar: bool, default True + display the plot in a ``jupyterlab-sidecar`` + + sidecar: dict, default None + kwargs for sidecar instance to display plot + i.e. title, layout + Returns ------- WgpuCanvas @@ -117,7 +132,18 @@ def show( self.toolbar = ToolBar(self) self.toolbar.maintain_aspect_button.value = maintain_aspect - return VBox([self.canvas, self.toolbar.widget]) + if not sidecar: + return VBox([self.canvas, self.toolbar.widget]) + + if self.sidecar is None: + # sidecar doesn't like unpacking when kwargs are `None` + if sidecar_kwargs is not None: + self.sidecar = Sidecar(**sidecar_kwargs) + else: + self.sidecar = Sidecar() + + with self.sidecar: + return display(VBox([self.canvas, self.toolbar.widget])) def close(self): """Close Plot""" @@ -126,6 +152,9 @@ def close(self): if self.toolbar is not None: self.toolbar.widget.close() + if self.sidecar is not None: + self.sidecar.close() + class ToolBar: def __init__(self, plot: Plot): diff --git a/setup.py b/setup.py index 9a89d013b..1d1204a69 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,8 @@ "jupyter-rfb>=0.4.1", "ipywidgets>=8.0.0,<9", "scikit-learn", - "tqdm" + "tqdm", + "sidecar" ] } From 1478da8ef7534af8a0f779176daaf5d82bd22dc1 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Tue, 12 Sep 2023 07:40:42 -0400 Subject: [PATCH 04/10] sidecar updates --- examples/notebooks/simple.ipynb | 18 +++++++++++++++--- fastplotlib/layouts/_plot.py | 18 +++++++++++++++--- 2 files changed, 30 insertions(+), 6 deletions(-) diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb index 482e87117..a79f7a954 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/simple.ipynb @@ -100,7 +100,7 @@ "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", "\n", "# show the plot\n", - "plot.show(sidecar_kwargs={\"title\": \"sample image\", \"layout\": {'width': '800px'}})" + "plot.show()" ] }, { @@ -327,6 +327,18 @@ "plot_test(\"astronaut\", plot)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "0bb1cfc7-1a06-4abb-a10a-a877a0d51c6b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot.canvas.get_logical_size()" + ] + }, { "cell_type": "markdown", "id": "b53bc11a-ddf1-4786-8dca-8f3d2eaf993d", @@ -466,7 +478,7 @@ "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", "\n", "# show the plot\n", - "plot_rgb.show(sidecar_kwargs={\"title\": \"RGB image\", \"layout\": {'width': '800px'}})" + "plot_rgb.show()" ] }, { @@ -567,7 +579,7 @@ "plot_v.add_animations(update_data)\n", "\n", "# show the plot\n", - "plot_v.show(sidecar_kwargs={\"title\": \"random image\", \"layout\": {'width': '800px'}})" + "plot_v.show()" ] }, { diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 4ff8dbd4d..32707a960 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -4,17 +4,19 @@ import os import pygfx -from IPython.display import display from wgpu.gui.auto import WgpuCanvas, is_jupyter if is_jupyter(): from ipywidgets import HBox, Layout, Button, ToggleButton, VBox from sidecar import Sidecar + from IPython.display import display from ._subplot import Subplot from ._record_mixin import RecordMixin from ..graphics.selectors import PolygonSelector +PLOT_OPEN = False + class Plot(Subplot, RecordMixin): def __init__( @@ -99,7 +101,7 @@ def show( sidecar: bool, default True display the plot in a ``jupyterlab-sidecar`` - sidecar: dict, default None + sidecar_kwargs: dict, default None kwargs for sidecar instance to display plot i.e. title, layout @@ -109,6 +111,9 @@ def show( the canvas """ + # define global PLOT_OPEN to use from outer scope + global PLOT_OPEN + self.canvas.request_draw(self.render) self.canvas.set_logical_size(*self._starting_size) @@ -135,12 +140,19 @@ def show( if not sidecar: return VBox([self.canvas, self.toolbar.widget]) + # used when plot.show() is being called again but sidecar has been closed via "x" button + # need to force new sidecar instance + # couldn't figure out how to get access to "close" button in order to add observe method on click + if PLOT_OPEN: + self.sidecar = None + if self.sidecar is None: - # sidecar doesn't like unpacking when kwargs are `None` if sidecar_kwargs is not None: self.sidecar = Sidecar(**sidecar_kwargs) + PLOT_OPEN = True else: self.sidecar = Sidecar() + PLOT_OPEN = True with self.sidecar: return display(VBox([self.canvas, self.toolbar.widget])) From 283211ae390070b44933dc548d6bbe6df24f1d84 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 13 Sep 2023 09:00:22 -0400 Subject: [PATCH 05/10] add sidecar to gridplot --- examples/notebooks/gridplot_simple.ipynb | 119 +++++++++++++---------- fastplotlib/layouts/_gridplot.py | 46 ++++++++- fastplotlib/layouts/_plot.py | 6 +- 3 files changed, 113 insertions(+), 58 deletions(-) diff --git a/examples/notebooks/gridplot_simple.ipynb b/examples/notebooks/gridplot_simple.ipynb index f90c0b157..8b50b2701 100644 --- a/examples/notebooks/gridplot_simple.ipynb +++ b/examples/notebooks/gridplot_simple.ipynb @@ -12,7 +12,9 @@ "cell_type": "code", "execution_count": 1, "id": "5171a06e-1bdc-4908-9726-3c1fd45dbb9d", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import numpy as np\n", @@ -23,12 +25,14 @@ "cell_type": "code", "execution_count": 2, "id": "86a2488f-ae1c-4b98-a7c0-18eae8013af1", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "5e4e0c5ca610425b8216db8e30cae997", + "model_id": "f9067cd724094b8c8dfecf60208acbfa", "version_major": 2, "version_minor": 0 }, @@ -40,31 +44,12 @@ "output_type": "display_data" }, { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "1eeb8c42e1b24c4fb40e3b5daa63909a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" + "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": [ @@ -105,15 +90,18 @@ "cell_type": "code", "execution_count": 3, "id": "17c6bc4a-5340-49f1-8597-f54528cfe915", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "text/plain": [ - "unnamed: Subplot @ 0x7fd4cc9bf820\n", - " parent: None\n", + "unnamed: Subplot @ 0x7f15df4f5c50\n", + " parent: fastplotlib.GridPlot @ 0x7f15d3f27890\n", + "\n", " Graphics:\n", - "\t'rand-img': ImageGraphic @ 0x7fd4f675a350" + "\t'rand-img': ImageGraphic @ 0x7f15d3fb5390" ] }, "execution_count": 3, @@ -139,12 +127,14 @@ "cell_type": "code", "execution_count": 4, "id": "34130f12-9ef6-43b0-b929-931de8b7da25", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "text/plain": [ - "('rand-img': ImageGraphic @ 0x7fd4a03295a0,)" + "(,)" ] }, "execution_count": 4, @@ -166,12 +156,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 11, "id": "ef8a29a6-b19c-4e6b-a2ba-fb4823c01451", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ - "grid_plot[0, 1].graphics[0].vmax = 0.5" + "grid_plot[0, 1].graphics[0].cmap.vmax = 0.5" ] }, { @@ -186,7 +178,9 @@ "cell_type": "code", "execution_count": 6, "id": "d6c2fa4b-c634-4dcf-8b61-f1986f7c4918", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# you can give subplots human-readable string names\n", @@ -197,15 +191,18 @@ "cell_type": "code", "execution_count": 7, "id": "2f6b549c-3165-496d-98aa-45b96c3de674", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "text/plain": [ - "top-right-plot: Subplot @ 0x7fd4cca0ffd0\n", - " parent: None\n", + "top-right-plot: Subplot @ 0x7f15d3f769d0\n", + " parent: fastplotlib.GridPlot @ 0x7f15d3f27890\n", + "\n", " Graphics:\n", - "\t'rand-img': ImageGraphic @ 0x7fd4a03716c0" + "\t'rand-img': ImageGraphic @ 0x7f15b83f7250" ] }, "execution_count": 7, @@ -219,9 +216,11 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 12, "id": "be436e04-33a6-4597-8e6a-17e1e5225419", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { @@ -229,7 +228,7 @@ "(0, 2)" ] }, - "execution_count": 8, + "execution_count": 12, "metadata": {}, "output_type": "execute_result" } @@ -241,9 +240,11 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 13, "id": "6699cda6-af86-4258-87f5-1832f989a564", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { @@ -251,7 +252,7 @@ "True" ] }, - "execution_count": 9, + "execution_count": 13, "metadata": {}, "output_type": "execute_result" } @@ -271,9 +272,11 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 14, "id": "545b627b-d794-459a-a75a-3fde44f0ea95", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "grid_plot[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" @@ -281,8 +284,20 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 15, "id": "36432d5b-b76c-4a2a-a32c-097faf5ab269", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "grid_plot.close()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b507b723-1371-44e7-aa6d-6aeb3196b27d", "metadata": {}, "outputs": [], "source": [] @@ -304,7 +319,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index b339e8659..c14769a54 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -13,12 +13,16 @@ if is_jupyter(): from ipywidgets import HBox, Layout, Button, ToggleButton, VBox, Dropdown + from sidecar import Sidecar + from IPython.display import display from ._utils import make_canvas_and_renderer from ._defaults import create_controller from ._subplot import Subplot from ._record_mixin import RecordMixin +PLOT_OPEN = False + def to_array(a) -> np.ndarray: if isinstance(a, np.ndarray): @@ -81,6 +85,7 @@ def __init__( self.shape = shape self.toolbar = None + self.sidecar = None canvas, renderer = make_canvas_and_renderer(canvas, renderer) @@ -294,7 +299,12 @@ def remove_animation(self, func): self._animate_funcs_post.remove(func) def show( - self, autoscale: bool = True, maintain_aspect: bool = None, toolbar: bool = True + self, + autoscale: bool = True, + maintain_aspect: bool = None, + toolbar: bool = True, + sidecar: bool = True, + sidecar_kwargs: dict = None ): """ Begins the rendering event loop and returns the canvas @@ -307,15 +317,24 @@ def show( maintain_aspect: bool, default ``True`` maintain aspect ratio - toolbar: bool, default True + toolbar: bool, default ``True`` show toolbar + sidecar: bool, default ``True`` + display plot in a ``jupyterlab-sidecar`` + + sidecar_kwargs: dict, default ``None`` + kwargs for sidecar instance to display plot + i.e. title, layout + Returns ------- WgpuCanvas the canvas """ + global PLOT_OPEN + self.canvas.request_draw(self.render) self.canvas.set_logical_size(*self._starting_size) @@ -343,7 +362,25 @@ def show( 0, 0 ].camera.maintain_aspect - return VBox([self.canvas, self.toolbar.widget]) + if not sidecar: + return VBox([self.canvas, self.toolbar.widget]) + + # used when plot.show() is being called again but sidecar has been closed via "x" button + # need to force new sidecar instance + # couldn't figure out how to get access to "close" button in order to add observe method on click + if PLOT_OPEN: + self.sidecar = None + + if self.sidecar is None: + if sidecar_kwargs is not None: + self.sidecar = Sidecar(**sidecar_kwargs) + PLOT_OPEN = True + else: + self.sidecar = Sidecar() + PLOT_OPEN = True + + with self.sidecar: + return display(VBox([self.canvas, self.toolbar.widget])) def close(self): """Close the GridPlot""" @@ -352,6 +389,9 @@ def close(self): if self.toolbar is not None: self.toolbar.widget.close() + if self.sidecar is not None: + self.sidecar.close() + def clear(self): """Clear all Subplots""" for subplot in self: diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 32707a960..4869ace61 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -95,13 +95,13 @@ def show( maintain_aspect: bool, default ``None`` maintain aspect ratio, uses ``camera.maintain_aspect`` if ``None`` - toolbar: bool, default True + toolbar: bool, default ``True`` show toolbar - sidecar: bool, default True + sidecar: bool, default ``True`` display the plot in a ``jupyterlab-sidecar`` - sidecar_kwargs: dict, default None + sidecar_kwargs: dict, default ``None`` kwargs for sidecar instance to display plot i.e. title, layout From 3c188c033aa9110d1fa5402fab7b2d22681361c8 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Fri, 15 Sep 2023 03:56:11 -0400 Subject: [PATCH 06/10] add close method to image widget, add sidecar to image widget --- fastplotlib/layouts/_gridplot.py | 12 +++---- fastplotlib/layouts/_plot.py | 13 ++++---- fastplotlib/widgets/image.py | 57 +++++++++++++++++++++++++++----- 3 files changed, 61 insertions(+), 21 deletions(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index c14769a54..b8d5db062 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -21,8 +21,6 @@ from ._subplot import Subplot from ._record_mixin import RecordMixin -PLOT_OPEN = False - def to_array(a) -> np.ndarray: if isinstance(a, np.ndarray): @@ -86,6 +84,7 @@ def __init__( self.shape = shape self.toolbar = None self.sidecar = None + self.plot_open = False canvas, renderer = make_canvas_and_renderer(canvas, renderer) @@ -333,7 +332,6 @@ def show( the canvas """ - global PLOT_OPEN self.canvas.request_draw(self.render) @@ -368,16 +366,16 @@ def show( # used when plot.show() is being called again but sidecar has been closed via "x" button # need to force new sidecar instance # couldn't figure out how to get access to "close" button in order to add observe method on click - if PLOT_OPEN: + if self.plot_open: self.sidecar = None if self.sidecar is None: if sidecar_kwargs is not None: self.sidecar = Sidecar(**sidecar_kwargs) - PLOT_OPEN = True + self.plot_open = True else: self.sidecar = Sidecar() - PLOT_OPEN = True + self.plot_open = True with self.sidecar: return display(VBox([self.canvas, self.toolbar.widget])) @@ -392,6 +390,8 @@ def close(self): if self.sidecar is not None: self.sidecar.close() + self.plot_open = False + def clear(self): """Clear all Subplots""" for subplot in self: diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 4869ace61..9e2f9441f 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -15,8 +15,6 @@ from ._record_mixin import RecordMixin from ..graphics.selectors import PolygonSelector -PLOT_OPEN = False - class Plot(Subplot, RecordMixin): def __init__( @@ -69,6 +67,7 @@ def __init__( self.toolbar = None self.sidecar = None + self.plot_open = False def render(self): super(Plot, self).render() @@ -111,8 +110,6 @@ def show( the canvas """ - # define global PLOT_OPEN to use from outer scope - global PLOT_OPEN self.canvas.request_draw(self.render) @@ -143,16 +140,16 @@ def show( # used when plot.show() is being called again but sidecar has been closed via "x" button # need to force new sidecar instance # couldn't figure out how to get access to "close" button in order to add observe method on click - if PLOT_OPEN: + if self.plot_open: self.sidecar = None if self.sidecar is None: if sidecar_kwargs is not None: self.sidecar = Sidecar(**sidecar_kwargs) - PLOT_OPEN = True + self.plot_open = True else: self.sidecar = Sidecar() - PLOT_OPEN = True + self.plot_open = True with self.sidecar: return display(VBox([self.canvas, self.toolbar.widget])) @@ -167,6 +164,8 @@ def close(self): if self.sidecar is not None: self.sidecar.close() + self.plot_open = False + class ToolBar: def __init__(self, plot: Plot): diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 962a94151..db718b638 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -17,6 +17,8 @@ Play, jslink, ) +from sidecar import Sidecar +from IPython.display import display from ..layouts import GridPlot from ..graphics import ImageGraphic @@ -271,6 +273,8 @@ def __init__( self._names = None self.toolbar = None + self.sidecar = None + self.plot_open = False if isinstance(data, list): # verify that it's a list of np.ndarray @@ -913,7 +917,7 @@ def set_data( if reset_vmin_vmax: self.reset_vmin_vmax() - def show(self, toolbar: bool = True): + def show(self, toolbar: bool = True, sidecar: bool = True, sidecar_kwargs: dict = None): """ Show the widget @@ -930,13 +934,50 @@ def show(self, toolbar: bool = True): if self.toolbar is None: self.toolbar = ImageWidgetToolbar(self) - return VBox( - [ - self.gridplot.show(toolbar=True), - self.toolbar.widget, - self._vbox_sliders, - ] - ) + if not sidecar: + return VBox( + [ + self.gridplot.show(toolbar=True, sidecar=False, sidecar_kwargs=None), + self.toolbar.widget, + self._vbox_sliders, + ] + ) + + if self.plot_open: + self.sidecar = None + + if self.sidecar is None: + if sidecar_kwargs is not None: + self.sidecar = Sidecar(**sidecar_kwargs) + self.plot_open = True + else: + self.sidecar = Sidecar() + self.plot_open = True + + with self.sidecar: + return display(VBox( + [ + self.gridplot.show(toolbar=True, sidecar=False, sidecar_kwargs=None), + self.toolbar.widget, + self._vbox_sliders + ] + ) + ) + + def close(self): + """Close Widget""" + self.gridplot.canvas.close() + + self._vbox_sliders.close() + + if self.toolbar is not None: + self.toolbar.widget.close() + self.gridplot.toolbar.widget.close() + + if self.sidecar is not None: + self.sidecar.close() + + self.plot_open = False class ImageWidgetToolbar: From 4ed458d94e10a28b56118d4527a879e3af6d9765 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Fri, 15 Sep 2023 04:22:57 -0400 Subject: [PATCH 07/10] fix notebook errors --- examples/notebooks/linear_region_selector.ipynb | 6 +++--- examples/notebooks/linear_selector.ipynb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb index 11cd3a490..859893297 100644 --- a/examples/notebooks/linear_region_selector.ipynb +++ b/examples/notebooks/linear_region_selector.ipynb @@ -83,7 +83,7 @@ "ls_x.selection.add_event_handler(set_zoom_x)\n", "ls_y.selection.add_event_handler(set_zoom_y)\n", "\n", - "gp.show()" + "gp.show(sidecar=False)" ] }, { @@ -199,7 +199,7 @@ "\n", "\n", "selector.selection.add_event_handler(update_zoomed_subplots)\n", - "plot.show()" + "plot.show(sidecar=False)" ] }, { @@ -253,7 +253,7 @@ "\n", "\n", "stack_selector.selection.add_event_handler(update_zoomed_stack)\n", - "plot.show()" + "plot.show(sidecar=False)" ] }, { diff --git a/examples/notebooks/linear_selector.ipynb b/examples/notebooks/linear_selector.ipynb index a4d6b97ea..463d7fbfa 100644 --- a/examples/notebooks/linear_selector.ipynb +++ b/examples/notebooks/linear_selector.ipynb @@ -52,7 +52,7 @@ "\n", "plot.auto_scale()\n", "plot.show()\n", - "VBox([plot.show(), ipywidget_slider])" + "VBox([plot.show(sidecar=False), ipywidget_slider])" ] }, { From 1f0fa3782a9257082a7a4ed21dfbe292ed2dbd88 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sat, 23 Sep 2023 17:55:04 -0400 Subject: [PATCH 08/10] fix linear region selector --- examples/notebooks/linear_region_selector.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb index 859893297..11cd3a490 100644 --- a/examples/notebooks/linear_region_selector.ipynb +++ b/examples/notebooks/linear_region_selector.ipynb @@ -83,7 +83,7 @@ "ls_x.selection.add_event_handler(set_zoom_x)\n", "ls_y.selection.add_event_handler(set_zoom_y)\n", "\n", - "gp.show(sidecar=False)" + "gp.show()" ] }, { @@ -199,7 +199,7 @@ "\n", "\n", "selector.selection.add_event_handler(update_zoomed_subplots)\n", - "plot.show(sidecar=False)" + "plot.show()" ] }, { @@ -253,7 +253,7 @@ "\n", "\n", "stack_selector.selection.add_event_handler(update_zoomed_stack)\n", - "plot.show(sidecar=False)" + "plot.show()" ] }, { From faa6154abf6b4e8afcfa556dcb5998ab56277aab Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sat, 23 Sep 2023 18:21:07 -0400 Subject: [PATCH 09/10] add vbox as kwarg for additional ipywidgets when showing plot and gridplot --- fastplotlib/layouts/_gridplot.py | 29 +++++++++++++++++++++++++---- fastplotlib/layouts/_plot.py | 30 ++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 8 deletions(-) diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index b8d5db062..e619722d3 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -12,7 +12,7 @@ from wgpu.gui.auto import WgpuCanvas, is_jupyter if is_jupyter(): - from ipywidgets import HBox, Layout, Button, ToggleButton, VBox, Dropdown + from ipywidgets import HBox, Layout, Button, ToggleButton, VBox, Dropdown, Widget from sidecar import Sidecar from IPython.display import display @@ -84,6 +84,7 @@ def __init__( self.shape = shape self.toolbar = None self.sidecar = None + self.vbox = None self.plot_open = False canvas, renderer = make_canvas_and_renderer(canvas, renderer) @@ -303,7 +304,8 @@ def show( maintain_aspect: bool = None, toolbar: bool = True, sidecar: bool = True, - sidecar_kwargs: dict = None + sidecar_kwargs: dict = None, + vbox: list = None ): """ Begins the rendering event loop and returns the canvas @@ -326,6 +328,9 @@ def show( kwargs for sidecar instance to display plot i.e. title, layout + vbox: list, default ``None`` + list of ipywidgets to be displayed with plot + Returns ------- WgpuCanvas @@ -360,8 +365,18 @@ def show( 0, 0 ].camera.maintain_aspect + # validate vbox if not None + if vbox is not None: + for widget in vbox: + if not isinstance(widget, Widget): + raise ValueError(f"Items in vbox must be ipywidgets. Item: {widget} is of type: {type(widget)}") + self.vbox = VBox(vbox) + if not sidecar: - return VBox([self.canvas, self.toolbar.widget]) + if self.vbox is not None: + return VBox([self.canvas, self.toolbar.widget, self.vbox]) + else: + return VBox([self.canvas, self.toolbar.widget]) # used when plot.show() is being called again but sidecar has been closed via "x" button # need to force new sidecar instance @@ -378,7 +393,10 @@ def show( self.plot_open = True with self.sidecar: - return display(VBox([self.canvas, self.toolbar.widget])) + if self.vbox is not None: + return display(VBox([self.canvas, self.toolbar.widget, self.vbox])) + else: + return display(VBox([self.canvas, self.toolbar.widget])) def close(self): """Close the GridPlot""" @@ -390,6 +408,9 @@ def close(self): if self.sidecar is not None: self.sidecar.close() + if self.vbox is not None: + self.vbox.close() + self.plot_open = False def clear(self): diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 9e2f9441f..9f6df89e0 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -3,11 +3,12 @@ import traceback import os +import ipywidgets import pygfx from wgpu.gui.auto import WgpuCanvas, is_jupyter if is_jupyter(): - from ipywidgets import HBox, Layout, Button, ToggleButton, VBox + from ipywidgets import HBox, Layout, Button, ToggleButton, VBox, Widget from sidecar import Sidecar from IPython.display import display @@ -67,6 +68,7 @@ def __init__( self.toolbar = None self.sidecar = None + self.vbox = None self.plot_open = False def render(self): @@ -81,7 +83,8 @@ def show( maintain_aspect: bool = None, toolbar: bool = True, sidecar: bool = True, - sidecar_kwargs: dict = None + sidecar_kwargs: dict = None, + vbox: list = None ): """ Begins the rendering event loop and returns the canvas @@ -104,6 +107,9 @@ def show( kwargs for sidecar instance to display plot i.e. title, layout + vbox: list, default ``None`` + list of ipywidgets to be displayed with plot + Returns ------- WgpuCanvas @@ -134,8 +140,18 @@ def show( self.toolbar = ToolBar(self) self.toolbar.maintain_aspect_button.value = maintain_aspect + # validate vbox if not None + if vbox is not None: + for widget in vbox: + if not isinstance(widget, Widget): + raise ValueError(f"Items in vbox must be ipywidgets. Item: {widget} is of type: {type(widget)}") + self.vbox = VBox(vbox) + if not sidecar: - return VBox([self.canvas, self.toolbar.widget]) + if self.vbox is not None: + return VBox([self.canvas, self.toolbar.widget, self.vbox]) + else: + return VBox([self.canvas, self.toolbar.widget]) # used when plot.show() is being called again but sidecar has been closed via "x" button # need to force new sidecar instance @@ -152,7 +168,10 @@ def show( self.plot_open = True with self.sidecar: - return display(VBox([self.canvas, self.toolbar.widget])) + if self.vbox is not None: + return display(VBox([self.canvas, self.toolbar.widget, self.vbox])) + else: + return display(VBox([self.canvas, self.toolbar.widget])) def close(self): """Close Plot""" @@ -164,6 +183,9 @@ def close(self): if self.sidecar is not None: self.sidecar.close() + if self.vbox is not None: + self.vbox.close() + self.plot_open = False From ea546cc2d56583ddbba6e4a37f680ef65c97ae60 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sat, 23 Sep 2023 18:33:51 -0400 Subject: [PATCH 10/10] fix notebooks --- .../notebooks/linear_region_selector.ipynb | 275 ++++++++++++++++++ examples/notebooks/linear_selector.ipynb | 144 +++++++++ 2 files changed, 419 insertions(+) create mode 100644 examples/notebooks/linear_region_selector.ipynb create mode 100644 examples/notebooks/linear_selector.ipynb diff --git a/examples/notebooks/linear_region_selector.ipynb b/examples/notebooks/linear_region_selector.ipynb new file mode 100644 index 000000000..43cea4f81 --- /dev/null +++ b/examples/notebooks/linear_region_selector.ipynb @@ -0,0 +1,275 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1db50ec4-8754-4421-9f5e-6ba8ca6b81e3", + "metadata": {}, + "source": [ + "# `LinearRegionSelector` with single lines" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b7bbfeb4-1ad0-47db-9a82-3d3f642a1f63", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "import numpy as np\n", + "from ipywidgets import IntRangeSlider, FloatRangeSlider, VBox\n", + "\n", + "gp = fpl.GridPlot((2, 2))\n", + "\n", + "# preallocated size for zoomed data\n", + "zoomed_prealloc = 1_000\n", + "\n", + "# data to plot\n", + "xs = np.linspace(0, 100, 1_000)\n", + "sine = np.sin(xs) * 20\n", + "\n", + "# make sine along x axis\n", + "sine_graphic_x = gp[0, 0].add_line(sine)\n", + "\n", + "# just something that looks different for line along y-axis\n", + "sine_y = sine\n", + "sine_y[sine_y > 0] = 0\n", + "\n", + "# sine along y axis\n", + "sine_graphic_y = gp[0, 1].add_line(np.column_stack([sine_y, xs]))\n", + "\n", + "# offset the position of the graphic to demonstrate `get_selected_data()` later\n", + "sine_graphic_y.position_x = 50\n", + "sine_graphic_y.position_y = 50\n", + "\n", + "# add linear selectors\n", + "ls_x = sine_graphic_x.add_linear_region_selector() # default axis is \"x\"\n", + "ls_y = sine_graphic_y.add_linear_region_selector(axis=\"y\")\n", + "\n", + "# preallocate array for storing zoomed in data\n", + "zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.random.rand(zoomed_prealloc)])\n", + "\n", + "# make line graphics for displaying zoomed data\n", + "zoomed_x = gp[1, 0].add_line(zoomed_init)\n", + "zoomed_y = gp[1, 1].add_line(zoomed_init)\n", + "\n", + "\n", + "def interpolate(subdata: np.ndarray, axis: int):\n", + " \"\"\"1D interpolation to display within the preallocated data array\"\"\"\n", + " x = np.arange(0, zoomed_prealloc)\n", + " xp = np.linspace(0, zoomed_prealloc, subdata.shape[0])\n", + " \n", + " # interpolate to preallocated size\n", + " return np.interp(x, xp, fp=subdata[:, axis]) # use the y-values\n", + "\n", + "\n", + "def set_zoom_x(ev):\n", + " \"\"\"sets zoomed x selector data\"\"\"\n", + " selected_data = ev.pick_info[\"selected_data\"]\n", + " zoomed_x.data = interpolate(selected_data, axis=1) # use the y-values\n", + " gp[1, 0].auto_scale()\n", + "\n", + "\n", + "def set_zoom_y(ev):\n", + " \"\"\"sets zoomed y selector data\"\"\"\n", + " selected_data = ev.pick_info[\"selected_data\"]\n", + " zoomed_y.data = -interpolate(selected_data, axis=0) # use the x-values\n", + " gp[1, 1].auto_scale()\n", + "\n", + "\n", + "# update zoomed plots when bounds change\n", + "ls_x.selection.add_event_handler(set_zoom_x)\n", + "ls_y.selection.add_event_handler(set_zoom_y)\n", + "\n", + "gp.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0bad4a35-f860-4f85-9061-920154ab682b", + "metadata": {}, + "source": [ + "### On the x-axis we have a 1-1 mapping from the data that we have passed and the line geometry positions. So the `bounds` min max corresponds directly to the data indices." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2c96a3ff-c2e7-4683-8097-8491e97dd6d3", + "metadata": {}, + "outputs": [], + "source": [ + "ls_x.selection()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3ec71e3f-291c-43c6-a954-0a082ba5981c", + "metadata": {}, + "outputs": [], + "source": [ + "ls_x.get_selected_indices()" + ] + }, + { + "cell_type": "markdown", + "id": "1588a89e-1da4-4ada-92e2-7437ba942065", + "metadata": {}, + "source": [ + "### However, for the y-axis line we have passed a 2D array where we've used a linspace, so there is not a 1-1 mapping from the data to the line geometry positions. Use `get_selected_indices()` to get the indices of the data bounded by the current selection. In addition the position of the Graphic is not `(0, 0)`. You must use `get_selected_indices()` whenever you want the indices of the selected data." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18e10277-6d5d-42fe-8715-1733efabefa0", + "metadata": {}, + "outputs": [], + "source": [ + "ls_y.selection()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8e9c42b9-60d2-4544-96c5-c8c6832b79e3", + "metadata": {}, + "outputs": [], + "source": [ + "ls_y.get_selected_indices()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a9583d2e-ec52-405c-a875-f3fec5e3aa16", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "import numpy as np\n", + "\n", + "# data to plot\n", + "xs = np.linspace(0, 100, 1_000)\n", + "sine = np.sin(xs) * 20\n", + "cosine = np.cos(xs) * 20\n", + "\n", + "plot = fpl.GridPlot((5, 1))\n", + "\n", + "# sines and cosines\n", + "sines = [sine] * 2\n", + "cosines = [cosine] * 2\n", + "\n", + "# make line stack\n", + "line_stack = plot[0, 0].add_line_stack(sines + cosines, separation=50)\n", + "\n", + "# make selector\n", + "selector = line_stack.add_linear_region_selector()\n", + "\n", + "# populate subplots with preallocated graphics\n", + "for i, subplot in enumerate(plot):\n", + " if i == 0:\n", + " # skip the first one\n", + " continue\n", + " # make line graphics for displaying zoomed data\n", + " subplot.add_line(zoomed_init, name=\"zoomed\")\n", + "\n", + "\n", + "def update_zoomed_subplots(ev):\n", + " \"\"\"update the zoomed subplots\"\"\"\n", + " zoomed_data = selector.get_selected_data()\n", + " \n", + " for i in range(len(zoomed_data)):\n", + " data = interpolate(zoomed_data[i], axis=1)\n", + " plot[i + 1, 0][\"zoomed\"].data = data\n", + " plot[i + 1, 0].auto_scale()\n", + "\n", + "\n", + "selector.selection.add_event_handler(update_zoomed_subplots)\n", + "plot.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0fa051b5-d6bc-4e4e-8f12-44f638a00c88", + "metadata": {}, + "source": [ + "# Large line stack with selector" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5ffb678-c989-49ee-85a9-4fd7822f033c", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "import numpy as np\n", + "\n", + "# data to plot\n", + "xs = np.linspace(0, 250, 10_000)\n", + "sine = np.sin(xs) * 20\n", + "cosine = np.cos(xs) * 20\n", + "\n", + "plot = fpl.GridPlot((1, 2))\n", + "\n", + "# sines and cosines\n", + "sines = [sine] * 1_00\n", + "cosines = [cosine] * 1_00\n", + "\n", + "# make line stack\n", + "line_stack = plot[0, 0].add_line_stack(sines + cosines, separation=50)\n", + "\n", + "# make selector\n", + "stack_selector = line_stack.add_linear_region_selector(padding=200)\n", + "\n", + "zoomed_line_stack = plot[0, 1].add_line_stack([zoomed_init] * 2_000, separation=50, name=\"zoomed\")\n", + " \n", + "def update_zoomed_stack(ev):\n", + " \"\"\"update the zoomed subplots\"\"\"\n", + " zoomed_data = stack_selector.get_selected_data()\n", + " \n", + " for i in range(len(zoomed_data)):\n", + " data = interpolate(zoomed_data[i], axis=1)\n", + " zoomed_line_stack.graphics[i].data = data\n", + " \n", + " plot[0, 1].auto_scale()\n", + "\n", + "\n", + "stack_selector.selection.add_event_handler(update_zoomed_stack)\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbcd6309-fb47-4941-9fd1-2b091feb3ae7", + "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.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/notebooks/linear_selector.ipynb b/examples/notebooks/linear_selector.ipynb new file mode 100644 index 000000000..9382ffa63 --- /dev/null +++ b/examples/notebooks/linear_selector.ipynb @@ -0,0 +1,144 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "a06e1fd9-47df-42a3-a76c-19e23d7b89fd", + "metadata": {}, + "source": [ + "## `LinearSelector`, draggable selector that can optionally associated with an ipywidget." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "eb95ba19-14b5-4bf4-93d9-05182fa500cb", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "from fastplotlib.graphics.selectors import Synchronizer\n", + "\n", + "import numpy as np\n", + "from ipywidgets import VBox, IntSlider, FloatSlider\n", + "\n", + "plot = fpl.Plot()\n", + "\n", + "# data to plot\n", + "xs = np.linspace(0, 100, 1000)\n", + "sine = np.sin(xs) * 20\n", + "\n", + "# make sine along x axis\n", + "sine_graphic = plot.add_line(np.column_stack([xs, sine]).astype(np.float32))\n", + "\n", + "# make some selectors\n", + "selector = sine_graphic.add_linear_selector()\n", + "selector2 = sine_graphic.add_linear_selector(20)\n", + "selector3 = sine_graphic.add_linear_selector(40)\n", + "\n", + "ss = Synchronizer(selector, selector2, selector3)\n", + "\n", + "def set_color_at_index(ev):\n", + " # changes the color at the index where the slider is\n", + " ix = ev.pick_info[\"selected_index\"]\n", + " g = ev.pick_info[\"graphic\"].parent\n", + " g.colors[ix] = \"green\"\n", + "\n", + "selector.selection.add_event_handler(set_color_at_index)\n", + "\n", + "# fastplotlib LineSelector can make an ipywidget slider and return it :D \n", + "ipywidget_slider = selector.make_ipywidget_slider()\n", + "ipywidget_slider.description = \"slider1\"\n", + "\n", + "# or you can make your own ipywidget sliders and connect them to the linear selector\n", + "ipywidget_slider2 = IntSlider(min=0, max=100, description=\"slider2\")\n", + "ipywidget_slider3 = FloatSlider(min=0, max=100, description=\"slider3\")\n", + "\n", + "selector2.add_ipywidget_handler(ipywidget_slider2, step=5)\n", + "selector3.add_ipywidget_handler(ipywidget_slider3, step=0.1)\n", + "\n", + "plot.auto_scale()\n", + "plot.show(vbox=[ipywidget_slider])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ab9f141-f92f-4c4c-808b-97dafd64ca25", + "metadata": {}, + "outputs": [], + "source": [ + "selector.step = 0.1" + ] + }, + { + "cell_type": "markdown", + "id": "3b0f448f-bbe4-4b87-98e3-093f561c216c", + "metadata": {}, + "source": [ + "### Drag linear selectors with the mouse, hold \"Shift\" to synchronize movement of all the selectors" + ] + }, + { + "cell_type": "markdown", + "id": "c6f041b7-8779-46f1-8454-13cec66f53fd", + "metadata": {}, + "source": [ + "## Also works for line collections" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e36da217-f82a-4dfa-9556-1f4a2c7c4f1c", + "metadata": {}, + "outputs": [], + "source": [ + "sines = [sine] * 10\n", + "\n", + "plot = fpl.Plot()\n", + "\n", + "sine_stack = plot.add_line_stack(sines)\n", + "\n", + "colors = \"y\", \"blue\", \"red\", \"green\"\n", + "\n", + "selectors = list()\n", + "for i, c in enumerate(colors):\n", + " sel = sine_stack.add_linear_selector(i * 100, color=c, name=str(i))\n", + " selectors.append(sel)\n", + " \n", + "ss = Synchronizer(*selectors)\n", + "\n", + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71ae4fca-f644-4d4f-8f32-f9d069bbc2f1", + "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.11.3" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +}