diff --git a/docs/source/quickstart.ipynb b/docs/source/quickstart.ipynb index 6a3afec33..0de4667bf 100644 --- a/docs/source/quickstart.ipynb +++ b/docs/source/quickstart.ipynb @@ -97,21 +97,7 @@ "id": "be5b408f-dd91-4e36-807a-8c22c8d7d216", "metadata": {}, "source": [ - "**In live notebooks or desktop applications, you can use the handle on the bottom right corner of the _canvas_ to resize it. You can also pan and zoom using your mouse!**\n", - "\n", - "By default the origin is on the bottom left, you can click the flip button to flip the y-axis, or use `plot.camera.world.scale_y *= -1`" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "9b4e977e-2a7d-4e2b-aee4-cfc36767b3c6", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot.camera.world.scale_y *= -1" + "**In live notebooks or desktop applications, you can use the handle on the bottom right corner of the _canvas_ to resize it. You can also pan and zoom using your mouse!**" ] }, { @@ -510,18 +496,6 @@ "plot_rgb.show()" ] }, - { - "cell_type": "code", - "execution_count": null, - "id": "7e4b5a30-4293-4ae3-87dc-06a1355bb2c7", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot_rgb.camera.world.scale_y *= -1" - ] - }, { "cell_type": "code", "execution_count": null, @@ -1361,7 +1335,7 @@ "outputs": [], "source": [ "# GridPlot of shape 2 x 3 with all controllers synced\n", - "grid_plot = fpl.GridPlot(shape=(2, 3), controllers=\"sync\")\n", + "grid_plot = fpl.GridPlot(shape=(2, 3), controller_ids=\"sync\")\n", "\n", "# Make a random image graphic for each subplot\n", "for subplot in grid_plot:\n", @@ -1549,7 +1523,7 @@ "# pan-zoom controllers for each view\n", "# views are synced if they have the \n", "# same controller ID\n", - "controllers = [\n", + "controller_ids = [\n", " [0, 3, 1], # id each controller with an integer\n", " [2, 2, 3]\n", "]\n", @@ -1564,7 +1538,7 @@ "# Create the grid plot\n", "grid_plot = fpl.GridPlot(\n", " shape=grid_shape,\n", - " controllers=controllers,\n", + " controller_ids=controller_ids,\n", " names=names,\n", ")\n", "\n", @@ -1678,18 +1652,6 @@ "grid_plot[1, 0][\"rand-image\"].vim = 0.1\n", "grid_plot[1, 0][\"rand-image\"].vmax = 0.3" ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "8a5753b9-ee71-4ed1-bb0d-52bdb4ea365f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "grid_plot[1, 0][\"rand-image\"].type" - ] } ], "metadata": { diff --git a/examples/desktop/gridplot/gridplot_non_square.py b/examples/desktop/gridplot/gridplot_non_square.py index a41bcd9f4..fe43a3c04 100644 --- a/examples/desktop/gridplot/gridplot_non_square.py +++ b/examples/desktop/gridplot/gridplot_non_square.py @@ -10,7 +10,7 @@ import imageio.v3 as iio -plot = fpl.GridPlot(shape=(2, 2), controllers="sync") +plot = fpl.GridPlot(shape=(2, 2), controller_ids="sync") # to force a specific framework such as glfw: # plot = fpl.GridPlot(canvas="glfw") diff --git a/examples/desktop/scatter/scatter_size.py b/examples/desktop/scatter/scatter_size.py index 5b6987b7c..2ad995584 100644 --- a/examples/desktop/scatter/scatter_size.py +++ b/examples/desktop/scatter/scatter_size.py @@ -9,16 +9,7 @@ import fastplotlib as fpl # grid with 2 rows and 3 columns -grid_shape = (2,1) - -# pan-zoom controllers for each view -# views are synced if they have the -# same controller ID -controllers = [ - [0], - [0] -] - +grid_shape = (2, 1) # you can give string names for each subplot within the gridplot names = [ @@ -29,7 +20,6 @@ # Create the grid plot plot = fpl.GridPlot( shape=grid_shape, - controllers=controllers, names=names, size=(1000, 1000) ) @@ -53,4 +43,4 @@ if __name__ == "__main__": print(__doc__) - fpl.run() \ No newline at end of file + fpl.run() diff --git a/examples/notebooks/gridplot.ipynb b/examples/notebooks/gridplot.ipynb index cfcae3705..f1ceb2180 100644 --- a/examples/notebooks/gridplot.ipynb +++ b/examples/notebooks/gridplot.ipynb @@ -10,7 +10,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "a635b3b3-33fa-48f0-b1cc-bf83b1e883ab", "metadata": {}, "outputs": [], @@ -21,52 +21,10 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "8de931e2-bdb3-44a3-9538-e0b3965779af", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "62f67ebf3b00494c826d92fc5b8bbf76", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "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": "b6262056cff84619aea08fc48d00eb79", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 2, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# grid with 2 rows and 3 columns\n", "grid_shape = (2, 3)\n", @@ -74,9 +32,15 @@ "# pan-zoom controllers for each view\n", "# views are synced if they have the \n", "# same controller ID\n", - "controllers = [\n", - " [0, 3, 1], # id each controller with an integer\n", - " [2, 2, 3]\n", + "controller_ids = [\n", + " [0, -3, 1], # id each controller with an integer\n", + " [2, 2, -3]\n", + "]\n", + "\n", + "# another way to set controller_ids\n", + "controller_ids = [\n", + " [\"subplot0\", \"subplot4\"],\n", + " [\"subplot1\", \"subplot2\", \"subplot5\"],\n", "]\n", "\n", "\n", @@ -89,7 +53,7 @@ "# Create the grid plot\n", "grid_plot = GridPlot(\n", " shape=grid_shape,\n", - " controllers=controllers,\n", + " controller_ids=controller_ids,\n", " names=names,\n", ")\n", "\n", @@ -123,24 +87,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "2a6f7eb5-776e-42a6-b6c2-c26009a26795", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "subplot0: Subplot @ 0x7fd496b32830\n", - " parent: None\n", - " Graphics:\n", - "\t'rand-image': ImageGraphic @ 0x7fd496b327d0" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# can access subplot by name\n", "grid_plot[\"subplot0\"]" @@ -148,24 +98,10 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "45e83bca-5a44-48ce-874f-9ae9ca444233", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "subplot0: Subplot @ 0x7fd496b32830\n", - " parent: None\n", - " Graphics:\n", - "\t'rand-image': ImageGraphic @ 0x7fd496b327d0" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# can access subplot by index\n", "grid_plot[0, 0]" @@ -182,21 +118,10 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": null, "id": "c8cf9bfd-e0cc-4173-b64e-a9f2c87bb2c6", "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'rand-image': ImageGraphic @ 0x7fd496b327d0" - ] - }, - "execution_count": 5, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# can access graphic directly via name\n", "grid_plot[\"subplot0\"][\"rand-image\"]" @@ -204,13 +129,13 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "1cfd1d45-8a60-4fc1-b873-46caa966fe6f", "metadata": {}, "outputs": [], "source": [ - "grid_plot[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", - "grid_plot[\"subplot0\"][\"rand-image\"].vmax = 0.8" + "grid_plot[\"subplot0\"][\"rand-image\"].cmap.vmin = 0.6\n", + "grid_plot[\"subplot0\"][\"rand-image\"].cmap.vmax = 0.8" ] }, { @@ -223,40 +148,19 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "2fafe992-4783-40f2-b044-26a2835dd50a", "metadata": {}, "outputs": [], "source": [ - "grid_plot[1, 0][\"rand-image\"].vim = 0.1\n", - "grid_plot[1, 0][\"rand-image\"].vmax = 0.3" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "a025b76c-77f8-4aeb-ac33-5bb6d0bb5a9a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "'image'" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grid_plot[1, 0][\"rand-image\"].type" + "grid_plot[1, 0][\"rand-image\"].cmap.vim = 0.1\n", + "grid_plot[1, 0][\"rand-image\"].cmap.vmax = 0.3" ] }, { "cell_type": "code", "execution_count": null, - "id": "62584f91-a8ed-4317-98ee-28ad6d1800d6", + "id": "a61e34a5-ee1b-4abb-8718-ec4715ffaa52", "metadata": {}, "outputs": [], "source": [] @@ -278,7 +182,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.10.5" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/notebooks/gridplot_simple.ipynb b/examples/notebooks/gridplot_simple.ipynb index 8b50b2701..74807f55a 100644 --- a/examples/notebooks/gridplot_simple.ipynb +++ b/examples/notebooks/gridplot_simple.ipynb @@ -10,9 +10,13 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "5171a06e-1bdc-4908-9726-3c1fd45dbb9d", "metadata": { + "ExecuteTime": { + "end_time": "2023-11-26T04:01:19.120171Z", + "start_time": "2023-11-26T04:01:18.618087Z" + }, "tags": [] }, "outputs": [], @@ -23,38 +27,19 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "86a2488f-ae1c-4b98-a7c0-18eae8013af1", "metadata": { + "ExecuteTime": { + "end_time": "2023-11-26T04:01:19.467264Z", + "start_time": "2023-11-26T04:01:19.121813Z" + }, "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "f9067cd724094b8c8dfecf60208acbfa", - "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:34: UserWarning: converting float64 array to float32\n", - " warn(f\"converting {array.dtype} array to float32\")\n" - ] - } - ], + "outputs": [], "source": [ "# GridPlot of shape 2 x 3 with all controllers synced\n", - "grid_plot = GridPlot(shape=(2, 3), controllers=\"sync\")\n", + "grid_plot = GridPlot(shape=(2, 3), controller_ids=\"sync\")\n", "\n", "# Make a random image graphic for each subplot\n", "for subplot in grid_plot:\n", @@ -88,27 +73,22 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, + "id": "52163bc7-2c77-4699-b7b0-e455a0ed7584", + "metadata": {}, + "outputs": [], + "source": [ + "grid_plot" + ] + }, + { + "cell_type": "code", + "execution_count": null, "id": "17c6bc4a-5340-49f1-8597-f54528cfe915", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "unnamed: Subplot @ 0x7f15df4f5c50\n", - " parent: fastplotlib.GridPlot @ 0x7f15d3f27890\n", - "\n", - " Graphics:\n", - "\t'rand-img': ImageGraphic @ 0x7f15d3fb5390" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# positional indexing\n", "# row 0 and col 0\n", @@ -125,23 +105,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": null, "id": "34130f12-9ef6-43b0-b929-931de8b7da25", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "(,)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "grid_plot[0, 1].graphics" ] @@ -156,7 +125,7 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "ef8a29a6-b19c-4e6b-a2ba-fb4823c01451", "metadata": { "tags": [] @@ -176,7 +145,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": null, "id": "d6c2fa4b-c634-4dcf-8b61-f1986f7c4918", "metadata": { "tags": [] @@ -189,50 +158,24 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": null, "id": "2f6b549c-3165-496d-98aa-45b96c3de674", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "top-right-plot: Subplot @ 0x7f15d3f769d0\n", - " parent: fastplotlib.GridPlot @ 0x7f15d3f27890\n", - "\n", - " Graphics:\n", - "\t'rand-img': ImageGraphic @ 0x7f15b83f7250" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "grid_plot[\"top-right-plot\"]" ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "be436e04-33a6-4597-8e6a-17e1e5225419", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "(0, 2)" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# view its position\n", "grid_plot[\"top-right-plot\"].position" @@ -240,23 +183,12 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "6699cda6-af86-4258-87f5-1832f989a564", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# these are really the same\n", "grid_plot[\"top-right-plot\"] is grid_plot[0, 2]" @@ -272,19 +204,19 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "545b627b-d794-459a-a75a-3fde44f0ea95", "metadata": { "tags": [] }, "outputs": [], "source": [ - "grid_plot[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" + "grid_plot[\"top-right-plot\"][\"rand-img\"].cmap.vmin = 0.5" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "36432d5b-b76c-4a2a-a32c-097faf5ab269", "metadata": { "tags": [] diff --git a/examples/notebooks/image_widget.ipynb b/examples/notebooks/image_widget.ipynb index 296126f5e..56d5c8a81 100644 --- a/examples/notebooks/image_widget.ipynb +++ b/examples/notebooks/image_widget.ipynb @@ -235,6 +235,16 @@ "iw_movie.gridplot[0, 0].auto_scale()# sidecar is optional" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "3d323f67-4717-4241-ad84-5091e6caf2cd", + "metadata": {}, + "outputs": [], + "source": [ + "iw_movie.close()" + ] + }, { "cell_type": "markdown", "id": "aca22179-1b1f-4c51-97bf-ce2d7044e451", @@ -296,7 +306,7 @@ }, { "cell_type": "markdown", - "id": "0721dc40-677e-431d-94c6-da59606199cb", + "id": "037d899e-9e7c-4d58-a021-5f5b71e1db90", "metadata": {}, "source": [ "pan-zoom controllers are all synced across subplots in a `ImageWidget`" @@ -473,7 +483,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.2" + "version": "3.11.3" } }, "nbformat": 4, diff --git a/examples/notebooks/lineplot.ipynb b/examples/notebooks/lineplot.ipynb index e156a7150..667cae178 100644 --- a/examples/notebooks/lineplot.ipynb +++ b/examples/notebooks/lineplot.ipynb @@ -12,7 +12,7 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": null, "id": "9c974494-712e-4981-bae2-a3ee176a6b20", "metadata": {}, "outputs": [], @@ -23,7 +23,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": null, "id": "c3d8f967-f60f-4f0b-b6ba-21b1251b4856", "metadata": {}, "outputs": [], @@ -41,60 +41,10 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": null, "id": "78cffe56-1147-4255-82c1-53cec6bc986a", "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "7993e0a4358f4678a7343b78b3b0b24c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/kushalk/repos/fastplotlib/fastplotlib/layouts/_base.py:214: UserWarning: `center_scene()` not yet implemented for `PerspectiveCamera`\n", - " warn(\"`center_scene()` not yet implemented for `PerspectiveCamera`\")\n" - ] - }, - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "898a109f489741a5b4624be77bd27db0", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], + "outputs": [], "source": [ "# grid with 2 rows and 2 columns\n", "shape = (2, 2)\n", @@ -104,7 +54,7 @@ "# same controller ID\n", "# in this example the first view has its own controller\n", "# and the last 3 views are synced\n", - "controllers = [\n", + "controller_ids = [\n", " [0, 1], # id each controller with an integer\n", " [1, 1]\n", "]\n", @@ -113,7 +63,7 @@ "grid_plot = GridPlot(\n", " shape=shape,\n", " cameras='3d', # 3D view for all subplots within the grid\n", - " controllers=controllers\n", + " controller_ids=controller_ids\n", ")\n", "\n", "for i, subplot in enumerate(grid_plot):\n", diff --git a/examples/notebooks/scatter.ipynb b/examples/notebooks/scatter.ipynb index 948403f11..9d7ff099f 100644 --- a/examples/notebooks/scatter.ipynb +++ b/examples/notebooks/scatter.ipynb @@ -78,7 +78,7 @@ "# same controller ID\n", "# you can only sync controllers that use the same camera type\n", "# i.e. you cannot sync between 2d and 3d subplots\n", - "controllers = [\n", + "controller_ids = [\n", " [0, 1],\n", " [1, 0]\n", "]\n", @@ -87,7 +87,7 @@ "grid_plot = GridPlot(\n", " shape=shape,\n", " cameras=cameras,\n", - " controllers=controllers\n", + " controller_ids=controller_ids\n", ")\n", "\n", "for subplot in grid_plot:\n", @@ -163,15 +163,7 @@ { "cell_type": "code", "execution_count": null, - "id": "fb49930f-b795-4b41-bbc6-014a27c2f463", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "7d8aac54-4f36-41d4-8e5b-8d8da2f0d17d", + "id": "a18e7a17-c2af-4674-a499-bf5f3b27c8ca", "metadata": {}, "outputs": [], "source": [] diff --git a/examples/notebooks/scatter_sizes_animation.ipynb b/examples/notebooks/scatter_sizes_animation.ipynb index 061f444d6..06a6b11a2 100644 --- a/examples/notebooks/scatter_sizes_animation.ipynb +++ b/examples/notebooks/scatter_sizes_animation.ipynb @@ -2,9 +2,39 @@ "cells": [ { "cell_type": "code", - "execution_count": null, + "execution_count": 1, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "5d9f9913391a42af95d4d43d07c17b19", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "9cd08c319b814934a09fd266a1b6322b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "JupyterOutputContext(children=(JupyterWgpuCanvas(), IpywidgetToolBar(children=(Button(icon='expand-arrows-alt'…" + ] + }, + "execution_count": 1, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ "from time import time\n", "\n", @@ -21,8 +51,6 @@ " current_time = time()\n", " newPositions = points + np.sin(((current_time / 4) % 1)*np.pi)\n", " plot.graphics[0].data = newPositions\n", - " plot.camera.width = 4*np.max(newPositions[0,:])\n", - " plot.camera.height = 4*np.max(newPositions[1,:])\n", "\n", "def update_sizes():\n", " current_time = time()\n", @@ -30,12 +58,11 @@ " size_delta = sin_sample*size_delta_scales\n", " plot.graphics[0].sizes = min_sizes + size_delta\n", "\n", - "points = np.array([[0,0], \n", - " [1,1], \n", - " [2,2]])\n", "scatter = plot.add_scatter(points, colors=[\"red\", \"green\", \"blue\"], sizes=12)\n", "plot.add_animations(update_positions, update_sizes)\n", - "plot.show(autoscale=True)" + "\n", + "plot.camera.width = 12\n", + "plot.show(autoscale=False)" ] }, { @@ -48,7 +75,7 @@ ], "metadata": { "kernelspec": { - "display_name": "fastplotlib-dev", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -62,10 +89,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" - }, - "orig_nbformat": 4 + "version": "3.11.3" + } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/examples/notebooks/scatter_sizes_grid.ipynb b/examples/notebooks/scatter_sizes_grid.ipynb index ff64184f7..e152056c9 100644 --- a/examples/notebooks/scatter_sizes_grid.ipynb +++ b/examples/notebooks/scatter_sizes_grid.ipynb @@ -19,15 +19,6 @@ "# grid with 2 rows and 3 columns\n", "grid_shape = (2,1)\n", "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", - "# same controller ID\n", - "controllers = [\n", - " [0],\n", - " [0]\n", - "]\n", - "\n", - "\n", "# you can give string names for each subplot within the gridplot\n", "names = [\n", " [\"scalar_size\"],\n", @@ -37,7 +28,6 @@ "# Create the grid plot\n", "plot = fpl.GridPlot(\n", " shape=grid_shape,\n", - " controllers=controllers,\n", " names=names,\n", " size=(1000, 1000)\n", ")\n", @@ -59,11 +49,18 @@ "\n", "plot.show()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { "kernelspec": { - "display_name": "fastplotlib-dev", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, @@ -77,10 +74,9 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.11.4" - }, - "orig_nbformat": 4 + "version": "3.11.3" + } }, "nbformat": 4, - "nbformat_minor": 2 + "nbformat_minor": 4 } diff --git a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png index 33798861e..61c3aeb8c 100644 --- a/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png +++ b/examples/notebooks/screenshots/nb-image-widget-movie-set_data.png @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:df2f6e73fdb0946d915f40df1e27f2ae89ba6c6e5d0dadb84406fe8b753735ab -size 34974 +oid sha256:60318615e4850d37a4ffae16ca1e3bbf2985ddafd0dd65ba6fae997e1d123d67 +size 31251 diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/simple.ipynb index 681980d39..f5901080b 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()" + "plot.show(sidecar=True)" ] }, { @@ -589,7 +589,7 @@ "\n", "plot_sync.add_animations(update_data_2)\n", "\n", - "plot_sync.show(sidecar=False)" + "plot_sync.show()" ] }, { @@ -704,7 +704,7 @@ "sinc_graphic = plot_l.add_line(data=sinc, thickness=5, colors = colors)\n", "\n", "# show the plot\n", - "plot_l.show(sidecar_kwargs={\"title\": \"lines\", \"layout\": {'width': '800px'}})" + "plot_l.show(sidecar=True, sidecar_kwargs={\"title\": \"lines\"})" ] }, { @@ -1020,6 +1020,14 @@ "plot_l3d.show()" ] }, + { + "cell_type": "markdown", + "id": "29f07af0-cdcb-47cc-bbb3-2fa4449fa084", + "metadata": {}, + "source": [ + "**Use WASD keys and the mouse to move around, just like in a game :D. Use the mouse weel to control the speed of movement.**" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1045,6 +1053,17 @@ "plot_test(\"lines-3d\", plot_l3d)" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "7a6da884-8b40-4ebf-837f-929b3e9cf4c4", + "metadata": {}, + "outputs": [], + "source": [ + "# change the FOV of the persepctive camera\n", + "plot_l3d.camera.fov = 70" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1187,6 +1206,25 @@ "scatter_graphic.data[n_points:n_points * 2, 0] = np.linspace(-40, 0, n_points)" ] }, + { + "cell_type": "markdown", + "id": "5f3e206d-97af-4e07-9969-94f2fdb41004", + "metadata": {}, + "source": [ + "**Switch to a fly controller to move around the plot in 3D!**" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c67944ca-52e7-4213-b820-6572cc3f76f0", + "metadata": {}, + "outputs": [], + "source": [ + "plot_s.camera = \"3d\"\n", + "plot_s.controller = \"fly\"" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/fastplotlib/layouts/_defaults.py b/fastplotlib/layouts/_defaults.py index 9a223855f..b28b04f64 100644 --- a/fastplotlib/layouts/_defaults.py +++ b/fastplotlib/layouts/_defaults.py @@ -1,46 +1,3 @@ -import pygfx -from typing import * -camera_types = { - "2d": pygfx.OrthographicCamera, - "3d": pygfx.PerspectiveCamera, -} -controller_types = { - "2d": pygfx.PanZoomController, - "3d": pygfx.OrbitController, - pygfx.OrthographicCamera: pygfx.PanZoomController, - pygfx.PerspectiveCamera: pygfx.OrbitController, -} - -def create_camera( - camera_type: str, big_camera: bool = False -) -> Union[pygfx.OrthographicCamera, pygfx.PerspectiveCamera]: - camera_type = camera_type.split("-") - - # kinda messy but works for now - if len(camera_type) > 1: - if camera_type[1] == "big": - big_camera = True - - camera_type = camera_type[0] - else: - camera_type = camera_type[0] - - cls = camera_types[camera_type] - - if cls is pygfx.OrthographicCamera: - if big_camera: - return cls(1024, 1024, -8192, 8192) - else: - return cls(1024, 1024) - - else: - return cls() - - -def create_controller(controller_type: str): - controller_type = controller_type.split("-")[0] - - return controller_types[controller_type]() diff --git a/fastplotlib/layouts/_gridplot.py b/fastplotlib/layouts/_gridplot.py index 473196f78..459aca5fd 100644 --- a/fastplotlib/layouts/_gridplot.py +++ b/fastplotlib/layouts/_gridplot.py @@ -1,4 +1,4 @@ -from itertools import product +from itertools import product, chain import numpy as np from typing import * from inspect import getfullargspec @@ -9,8 +9,8 @@ from wgpu.gui.auto import WgpuCanvas from ._frame import Frame -from ._utils import make_canvas_and_renderer -from ._defaults import create_controller +from ._utils import make_canvas_and_renderer, create_controller, create_camera +from ._utils import controller_types as valid_controller_types from ._subplot import Subplot from ._record_mixin import RecordMixin @@ -25,43 +25,47 @@ def to_array(a) -> np.ndarray: return np.array(a) -valid_cameras = ["2d", "2d-big", "3d", "3d-big"] - - class GridPlot(Frame, RecordMixin): def __init__( self, shape: Tuple[int, int], - cameras: Union[np.ndarray, str] = "2d", - controllers: Union[np.ndarray, str] = None, + cameras: Union[str, list, np.ndarray] = "2d", + controller_types: Union[str, list, np.ndarray] = None, + controller_ids: Union[str, list, np.ndarray] = None, canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, renderer: pygfx.WgpuRenderer = None, size: Tuple[int, int] = (500, 300), - **kwargs, + names: Union[list, np.ndarray] = None, ): """ A grid of subplots. Parameters ---------- - shape: tuple of int + shape: (int, int) (n_rows, n_cols) - cameras: np.ndarray or str, optional + cameras: str, list, or np.ndarray, optional | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D cameras for all subplots - | OR - | Array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot + | list/array of ``2d`` and/or ``3d`` that specifies the camera type for each subplot + | list/array of pygfx.PerspectiveCamera instances + + controller_types: str, list or np.ndarray, optional + list or array that specifies the controller type for each subplot, or list/array of + pygfx.Controller instances - controllers: np.ndarray or str, optional + controller_ids: str, list or np.ndarray of int or str ids, optional | If `None` a unique controller is created for each subplot | If "sync" all the subplots use the same controller | If ``numpy.array``, its shape must be the same as ``grid_shape``. This allows custom assignment of controllers - | Example: - | unique controllers for a 2x2 gridplot: np.array([[0, 1], [2, 3]]) - | same controllers for first 2 plots and last 2 plots: np.array([[0, 0, 1], [2, 3, 3]]) + | Example with integers: + | sync first 2 plots, and sync last 2 plots: [[0, 0, 1], [2, 3, 3]] + | Example with str subplot names: + | list of lists of subplot names, each sublist is synced: [[subplot_a, subplot_b], [subplot_f, subplot_c]] + | this syncs subplot_a and subplot_b together; syncs subplot_f and subplot_c together canvas: WgpuCanvas, optional Canvas for drawing @@ -69,70 +73,147 @@ def __init__( renderer: pygfx.Renderer, optional pygfx renderer instance - size: (int, int) + size: (int, int), optional starting size of canvas, default (500, 300) + names: list or array of str, optional + subplot names """ self.shape = shape + if names is not None: + if len(list(chain(*names))) != self.shape[0] * self.shape[1]: + raise ValueError("must provide same number of subplot `names` as specified by gridplot shape") + + self.names = to_array(names).reshape(self.shape) + else: + self.names = None + canvas, renderer = make_canvas_and_renderer(canvas, renderer) if isinstance(cameras, str): - if cameras not in valid_cameras: - raise ValueError( - f"If passing a str, `cameras` must be one of: {valid_cameras}" - ) # create the array representing the views for each subplot in the grid cameras = np.array([cameras] * self.shape[0] * self.shape[1]).reshape( self.shape ) - if isinstance(controllers, str): - if controllers == "sync": - controllers = np.zeros( - self.shape[0] * self.shape[1], dtype=int - ).reshape(self.shape) + # list -> array if necessary + cameras = to_array(cameras).reshape(self.shape) - if controllers is None: - controllers = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) + if cameras.shape != self.shape: + raise ValueError("Number of cameras does not match the number of subplots") - controllers = to_array(controllers) + # create the cameras + self._cameras = np.empty(self.shape, dtype=object) + for i, j in product(range(self.shape[0]), range(self.shape[1])): + self._cameras[i, j] = create_camera(camera_type=cameras[i, j]) - if controllers.shape != self.shape: - raise ValueError + if controller_ids is None: + # individual controller for each subplot + controller_ids = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) - cameras = to_array(cameras) + elif isinstance(controller_ids, str): + if controller_ids == "sync": + controller_ids = np.zeros(self.shape, dtype=int) + else: + raise ValueError( + f"`controller_ids` must be one of 'sync', an array/list of subplot names, or an array/list of " + f"integer ids. See the docstring for more details." + ) - self._controllers = np.empty(shape=cameras.shape, dtype=object) + # list controller_ids + elif isinstance(controller_ids, (list, np.ndarray)): + ids_flat = list(chain(*controller_ids)) - if cameras.shape != self.shape: - raise ValueError - - # create controllers if the arguments were integers - if np.issubdtype(controllers.dtype, np.integer): - if not np.all( - np.sort(np.unique(controllers)) - == np.arange(np.unique(controllers).size) - ): - raise ValueError("controllers must be consecutive integers") - - for controller in np.unique(controllers): - cam = np.unique(cameras[controllers == controller]) - if cam.size > 1: + # list of str of subplot names, convert this to integer ids + if all([isinstance(item, str) for item in ids_flat]): + if self.names is None: + raise ValueError("must specify subplot `names` to use list of str for `controller_ids`") + + # make sure each controller_id str is a subplot name + if not all([n in self.names for n in ids_flat]): + raise KeyError( + f"all `controller_ids` strings must be one of the subplot names" + ) + + if len(ids_flat) > len(set(ids_flat)): raise ValueError( - f"Controller id: {controller} has been assigned to multiple different camera types" + "id strings must not appear twice in `controller_ids`" ) - self._controllers[controllers == controller] = create_controller(cam[0]) - # else assume it's a single pygfx.Controller instance or a list of controllers - else: - if isinstance(controllers, pygfx.Controller): - self._controllers = np.array( - [controllers] * shape[0] * shape[1] - ).reshape(shape) + # initialize controller_ids array + ids_init = np.arange(self.shape[0] * self.shape[1]).reshape(self.shape) + + # set id based on subplot position for each synced sublist + for i, sublist in enumerate(controller_ids): + for name in sublist: + ids_init[self.names == name] = -(i + 1) # use negative numbers because why not + + controller_ids = ids_init + + # integer ids + elif all([isinstance(item, (int, np.integer)) for item in ids_flat]): + controller_ids = to_array(controller_ids).reshape(self.shape) + else: - self._controllers = np.array(controllers).reshape(shape) + raise TypeError( + f"list argument to `controller_ids` must be a list of `str` or `int`, " + f"you have passed: {controller_ids}" + ) + + if controller_ids.shape != self.shape: + raise ValueError("Number of controller_ids does not match the number of subplots") + + if controller_types is None: + # `create_controller()` will auto-determine controller for each subplot based on defaults + controller_types = np.array(["default"] * self.shape[0] * self.shape[1]).reshape(self.shape) + + # validate controller types + types_flat = list(chain(*controller_types)) + # str controller_type or pygfx instances + valid_str = list(valid_controller_types.keys()) + ["default"] + valid_instances = tuple(valid_controller_types.values()) + + # make sure each controller type is valid + for controller_type in types_flat: + if controller_type is None: + continue + + if (controller_type not in valid_str) and (not isinstance(controller_type, valid_instances)): + raise ValueError( + f"You have passed an invalid controller type, valid controller_types arguments are:\n" + f"{valid_str} or instances of {[c.__name__ for c in valid_instances]}" + ) + + controller_types = to_array(controller_types).reshape(self.shape) + + # make the real controllers for each subplot + self._controllers = np.empty(shape=self.shape, dtype=object) + for cid in np.unique(controller_ids): + cont_type = controller_types[controller_ids == cid] + if np.unique(cont_type).size > 1: + raise ValueError( + "Multiple controller types have been assigned to the same controller id. " + "All controllers with the same id must use the same type of controller." + ) + + cont_type = cont_type[0] + + # get all the cameras that use this controller + cams = self._cameras[controller_ids == cid].ravel() + + if cont_type == "default": + # hacky fix for now because of how `create_controller()` works + cont_type = None + _controller = create_controller(controller_type=cont_type, camera=cams[0]) + + self._controllers[controller_ids == cid] = _controller + + # add the other cameras that go with this controller + if cams.size > 1: + for cam in cams[1:]: + _controller.add_camera(cam) if canvas is None: canvas = WgpuCanvas() @@ -140,13 +221,6 @@ def __init__( if renderer is None: renderer = pygfx.renderers.WgpuRenderer(canvas) - if "names" in kwargs.keys(): - self.names = to_array(kwargs["names"]) - if self.names.shape != self.shape: - raise ValueError - else: - self.names = None - self._canvas = canvas self._renderer = renderer @@ -158,7 +232,7 @@ def __init__( for i, j in self._get_iterator(): position = (i, j) - camera = cameras[i, j] + camera = self._cameras[i, j] controller = self._controllers[i, j] if self.names is not None: @@ -304,5 +378,15 @@ def __next__(self) -> Subplot: pos = self._current_iter.__next__() return self._subplots[pos] + def __str__(self): + return f"{self.__class__.__name__} @ {hex(id(self))}" + def __repr__(self): - return f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}\n" + newline = "\n\t" + + return ( + f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}\n" + f" Subplots:\n" + f"\t{newline.join(subplot.__str__() for subplot in self)}" + f"\n" + ) diff --git a/fastplotlib/layouts/_plot.py b/fastplotlib/layouts/_plot.py index 5aa04bb76..34027a276 100644 --- a/fastplotlib/layouts/_plot.py +++ b/fastplotlib/layouts/_plot.py @@ -11,10 +11,10 @@ class Plot(Subplot, Frame, RecordMixin): def __init__( self, - canvas: WgpuCanvas = None, + canvas: Union[str, WgpuCanvas] = None, renderer: pygfx.WgpuRenderer = None, - camera: str = "2d", - controller: Union[pygfx.PanZoomController, pygfx.OrbitController] = None, + camera: Union[str, pygfx.PerspectiveCamera] = "2d", + controller: Union[str, pygfx.Controller] = None, size: Tuple[int, int] = (500, 300), **kwargs, ): @@ -29,12 +29,14 @@ def __init__( renderer: pygfx.Renderer, optional pygfx renderer instance - camera:str, optional + camera: str or pygfx.PerspectiveCamera, optional | One of ``"2d"`` or ``"3d"`` indicating 2D or 3D camera - controller: None, PanZoomController or OrbitOrthoController, optional + controller: str or pygfx.Controller, optional Usually ``None``, you can pass an existing controller from another - ``Plot`` or ``Subplot`` within a ``GridPlot`` to synchronize them. + ``Plot`` or ``Subplot`` to synchronize them. + + You can also pass str arguments of valid controller names, see Subplot docstring for valid names size: (int, int) starting size of canvas, default (500, 300) diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index dec5d891e..9522832d3 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -6,18 +6,10 @@ import numpy as np import pygfx -from pygfx import ( - Scene, - OrthographicCamera, - PerspectiveCamera, - PanZoomController, - OrbitController, - Viewport, - WgpuRenderer, -) from pylinalg import vec_transform, vec_unproject from wgpu.gui.auto import WgpuCanvas +from ._utils import create_camera, create_controller from ..graphics._base import Graphic from ..graphics.selectors._base_selector import BaseSelector @@ -33,11 +25,11 @@ def __init__( self, parent, position: Any, - camera: Union[OrthographicCamera, PerspectiveCamera], - controller: Union[PanZoomController, OrbitController], - scene: Scene, + camera: Union[pygfx.PerspectiveCamera], + controller: Union[pygfx.Controller], + scene: pygfx.Scene, canvas: WgpuCanvas, - renderer: WgpuRenderer, + renderer: pygfx.WgpuRenderer, name: str = None, ): """ @@ -53,25 +45,23 @@ def __init__( typical use will be for ``subplots`` in a ``gridplot``, position would correspond to the ``[row, column]`` location of the ``subplot`` in its ``gridplot`` - camera: pygfx OrthographicCamera or pygfx PerspectiveCamera - ``OrthographicCamera`` type is used to visualize 2D content and ``PerspectiveCamera`` type is used to view - 3D content, used to view the scene + camera: pygfx.PerspectiveCamera + Use perspective camera for both perspective and orthographic views. Set fov = 0 for orthographic mode. - controller: pygfx PanZoomController or pygfx OrbitController - ``PanZoomController`` type is used for 2D pan-zoom camera control and ``OrbitController`` type is used for - rotating the camera around a center position, used to control the camera + controller: pygfx.Controller + One of the pygfx controllers, panzoom, fly, orbit, or trackball - scene: pygfx Scene + scene: pygfx.Scene represents the root of a scene graph, will be viewed by the given ``camera`` canvas: WgpuCanvas provides surface on which a scene will be rendered - renderer: WgpuRenderer - object used to render scenes using wgpu + renderer: pygfx.WgpuRenderer + renders the scene onto the canvas name: str, optional - name of ``subplot`` or ``plot`` subclass being instantiated + name this ``subplot`` or ``plot`` """ @@ -82,14 +72,14 @@ def __init__( self._canvas = canvas self._renderer = renderer if parent is None: - self._viewport: Viewport = Viewport(renderer) + self._viewport: pygfx.Viewport = pygfx.Viewport(renderer) else: - self._viewport = Viewport(parent.renderer) + self._viewport = pygfx.Viewport(parent.renderer) self._camera = camera self._controller = controller - self.controller.add_camera(self.camera) + self.controller.add_camera(self._camera) self.controller.register_events( self.viewport, ) @@ -126,7 +116,7 @@ def position(self) -> Union[Tuple[int, int], Any]: return self._position @property - def scene(self) -> Scene: + def scene(self) -> pygfx.Scene: """The Scene where Graphics lie in this plot area""" return self._scene @@ -136,26 +126,82 @@ def canvas(self) -> WgpuCanvas: return self._canvas @property - def renderer(self) -> WgpuRenderer: + def renderer(self) -> pygfx.WgpuRenderer: """Renderer associated to the plot area""" return self._renderer @property - def viewport(self) -> Viewport: + def viewport(self) -> pygfx.Viewport: """The rectangular area of the renderer associated to this plot area""" return self._viewport @property - def camera(self) -> Union[OrthographicCamera, PerspectiveCamera]: + def camera(self) -> pygfx.PerspectiveCamera: """camera used to view the scene""" return self._camera + @camera.setter + def camera(self, new_camera: Union[str, pygfx.PerspectiveCamera]): + # user wants to set completely new camera, remove current camera from controller + if isinstance(new_camera, pygfx.PerspectiveCamera): + self.controller.remove_camera(self._camera) + # add new camera to controller + self.controller.add_camera(new_camera) + + self._camera = new_camera + + # modify FOV if necessary + elif isinstance(new_camera, str): + if new_camera == "2d": + self._camera.fov = 0 + + elif new_camera == "3d": + # orthographic -> perspective only if fov = 0, i.e. if camera is in ortho mode + # otherwise keep same FOV + if self._camera.fov == 0: + self._camera.fov = 50 + + else: + raise ValueError("camera must be one of '2d', '3d' or a pygfx.PerspectiveCamera instance") + else: + raise ValueError("camera must be one of '2d', '3d' or a pygfx.PerspectiveCamera instance") + # in the future we can think about how to allow changing the controller @property - def controller(self) -> Union[PanZoomController, OrbitController]: - """controller used to control camera""" + def controller(self) -> pygfx.Controller: + """controller used to control the camera""" return self._controller + @controller.setter + def controller(self, new_controller: Union[str, pygfx.Controller]): + new_controller = create_controller(new_controller, self._camera) + + cameras_list = list() + + # remove all the cameras associated to this controller + for camera in self._controller.cameras: + self._controller.remove_camera(camera) + cameras_list.append(camera) + + # add the associated cameras to the new controller + for camera in cameras_list: + new_controller.add_camera(camera) + + new_controller.register_events( + self.viewport + ) + + # TODO: monkeypatch until we figure out a better + # pygfx plans on refactoring viewports anyways + if self.parent is not None: + if self.parent.__class__.__name__ == "GridPlot": + for subplot in self.parent: + if subplot.camera in cameras_list: + new_controller.register_events(subplot.viewport) + subplot._controller = new_controller + + self._controller = new_controller + @property def graphics(self) -> Tuple[Graphic, ...]: """Graphics in the plot area. Always returns a proxy to the Graphic instances.""" @@ -663,7 +709,7 @@ def __repr__(self): return ( f"{self}\n" - f" parent: {self.parent}\n" + f" parent: {self.parent.__str__()}\n" f" Graphics:\n" f"\t{newline.join(graphic.__repr__() for graphic in self.graphics)}" f"\n" diff --git a/fastplotlib/layouts/_subplot.py b/fastplotlib/layouts/_subplot.py index c178c0fca..e9eae7603 100644 --- a/fastplotlib/layouts/_subplot.py +++ b/fastplotlib/layouts/_subplot.py @@ -2,22 +2,13 @@ import numpy as np -from pygfx import ( - Scene, - OrthographicCamera, - PanZoomController, - OrbitController, - AxesHelper, - GridHelper, - WgpuRenderer, - Texture, -) +import pygfx + from wgpu.gui.auto import WgpuCanvas from ..graphics import TextGraphic -from ._utils import make_canvas_and_renderer +from ._utils import make_canvas_and_renderer, create_camera, create_controller from ._plot_area import PlotArea -from ._defaults import create_camera, create_controller from .graphic_methods_mixin import GraphicMethodsMixin @@ -27,12 +18,11 @@ def __init__( parent: Any = None, position: Tuple[int, int] = None, parent_dims: Tuple[int, int] = None, - camera: str = "2d", - controller: Union[PanZoomController, OrbitController] = None, - canvas: Union[str, WgpuCanvas, Texture] = None, - renderer: WgpuRenderer = None, + camera: Union[str, pygfx.PerspectiveCamera] = "2d", + controller: Union[str, pygfx.Controller] = None, + canvas: Union[str, WgpuCanvas, pygfx.Texture] = None, + renderer: pygfx.WgpuRenderer = None, name: str = None, - **kwargs, ): """ General plot object that composes a ``Gridplot``. Each ``Gridplot`` instance will have [n rows, n columns] @@ -43,21 +33,25 @@ def __init__( Parameters ---------- - position: int tuple, optional + parent: Any + parent GridPlot instance + + position: (int, int), optional corresponds to the [row, column] position of the subplot within a ``Gridplot`` - parent_dims: int tuple, optional + parent_dims: (int, int), optional dimensions of the parent ``GridPlot`` - camera: str, default '2d' - indicates the kind of pygfx camera that will be instantiated, '2d' uses pygfx ``OrthographicCamera`` and - '3d' uses pygfx ``PerspectiveCamera`` + camera: str or pygfx.PerspectiveCamera, default '2d' + indicates the FOV for the camera, '2d' sets ``fov = 0``, '3d' sets ``fov = 50``. + ``fov`` can be changed at any time. - controller: PanZoomController or OrbitOrthoController, optional - ``PanZoomController`` type is used for 2D pan-zoom camera control and ``OrbitController`` type is used for - rotating the camera around a center position, used to control the camera + controller: str or pygfx.Controller, optional + | if ``None``, uses a PanZoomController for "2d" camera or FlyController for "3d" camera. + | if ``str``, must be one of: `"panzoom", "fly", "trackball", or "orbit"`. + | also accepts a pygfx.Controller instance - canvas: WgpuCanvas, Texture, or one of "jupyter", "glfw", "qt", optional + canvas: one of "jupyter", "glfw", "qt", WgpuCanvas, or pygfx.Texture, optional Provides surface on which a scene will be rendered. Can optionally provide a WgpuCanvas instance or a str to force the PlotArea to use a specific canvas from one of the following options: "jupyter", "glfw", "qt". Can also provide a pygfx Texture to render to. @@ -82,25 +76,26 @@ def __init__( self.nrows, self.ncols = parent_dims - if controller is None: - controller = create_controller(camera) + camera = create_camera(camera) + + controller = create_controller(controller_type=controller, camera=camera) self._docks = dict() self.spacing = 2 - self._axes: AxesHelper = AxesHelper(size=100) + self._axes: pygfx.AxesHelper = pygfx.AxesHelper(size=100) for arrow in self._axes.children: self._axes.remove(arrow) - self._grid: GridHelper = GridHelper(size=100, thickness=1) + self._grid: pygfx.GridHelper = pygfx.GridHelper(size=100, thickness=1) super(Subplot, self).__init__( parent=parent, position=position, - camera=create_camera(camera), + camera=camera, controller=controller, - scene=Scene(), + scene=pygfx.Scene(), canvas=canvas, renderer=renderer, name=name, @@ -221,9 +216,9 @@ def __init__( super(Dock, self).__init__( parent=parent, position=position, - camera=OrthographicCamera(), - controller=PanZoomController(), - scene=Scene(), + camera=pygfx.OrthographicCamera(), + controller=pygfx.PanZoomController(), + scene=pygfx.Scene(), canvas=parent.canvas, renderer=parent.renderer, ) diff --git a/fastplotlib/layouts/_utils.py b/fastplotlib/layouts/_utils.py index e8201d073..7db1d84c4 100644 --- a/fastplotlib/layouts/_utils.py +++ b/fastplotlib/layouts/_utils.py @@ -1,5 +1,6 @@ from typing import * +import pygfx from pygfx import WgpuRenderer, Texture # default auto-determined canvas @@ -88,3 +89,58 @@ def make_canvas_and_renderer( renderer = WgpuRenderer(canvas) return canvas, renderer + + +def create_camera( + camera_type: Union[pygfx.PerspectiveCamera, str], +) -> pygfx.PerspectiveCamera: + if isinstance(camera_type, pygfx.PerspectiveCamera): + return camera_type + + elif camera_type == "2d": + # use perspective for orthographic, makes it easier to then switch to controllers that make sense with fov > 0 + return pygfx.PerspectiveCamera(fov=0) + + elif camera_type == "3d": + return pygfx.PerspectiveCamera() + + else: + raise ValueError( + "camera must be one of: '2d', '3d' or an instance of pygfx.PerspectiveCamera" + ) + + +controller_types = { + "fly": pygfx.FlyController, + "panzoom": pygfx.PanZoomController, + "trackball": pygfx.TrackballController, + "orbit": pygfx.OrbitController, +} + + +def create_controller( + controller_type: Union[pygfx.Controller, None, str], + camera: pygfx.PerspectiveCamera, +) -> pygfx.Controller: + """ + Creates the controllers and adds the camera to it. + """ + if isinstance(controller_type, pygfx.Controller): + controller_type.add_camera(camera) + return controller_type + + if controller_type is None: + # default controllers + if camera.fov == 0: + # default for orthographic + return pygfx.PanZoomController(camera) + else: + return pygfx.FlyController(camera) + + if controller_type not in controller_types.keys(): + raise KeyError( + f"controller must be a valid pygfx.Controller or one of: " + f"{list(controller_types.keys())}, you have passed: {controller_type}" + ) + + return controller_types[controller_type](camera) diff --git a/fastplotlib/widgets/image.py b/fastplotlib/widgets/image.py index 522b0a962..56aa4aebc 100644 --- a/fastplotlib/widgets/image.py +++ b/fastplotlib/widgets/image.py @@ -550,7 +550,7 @@ def __init__( self._dims_max_bounds[_dim], array.shape[order.index(_dim)] ) - grid_plot_kwargs_default = {"controllers": "sync"} + grid_plot_kwargs_default = {"controller_ids": "sync"} if grid_plot_kwargs is None: grid_plot_kwargs = dict() @@ -885,6 +885,7 @@ def set_data( frame = self._process_frame_apply(frame, i) new_graphic = ImageGraphic(data=frame, name="image_widget_managed") subplot.insert_graphic(graphic=new_graphic) + subplot.docks["right"]["histogram_lut"].image_graphic = new_graphic if new_array.ndim > 2: # to set max of time slider, txy or tzxy