diff --git a/README.md b/README.md index 64e1649e8..5c4eb2d45 100644 --- a/README.md +++ b/README.md @@ -55,21 +55,19 @@ Questions, issues, ideas? Post an [issue](https://github.com/fastplotlib/fastplo # Installation -Install using `pip`. - ### Minimal, use with your own `Qt` or `glfw` applications ```bash pip install fastplotlib ``` -**This does not give you `Qt` or `glfw`, you will have to install one of them yourself depending on your preference**. +**This does not give you `PyQt`/`PySide` or `glfw`, you will have to install your preferred GUI framework separately**. ### Notebook ```bash pip install "fastplotlib[notebook]" ``` -**Recommended: install `simplejpeg` for much faster notebook visualization, this requires you to first install [libjpeg-turbo](https://libjpeg-turbo.org/)** +**Strongly recommended: install `simplejpeg` for much faster notebook visualization, this requires you to first install [libjpeg-turbo](https://libjpeg-turbo.org/)** ```bash pip install simplejpeg @@ -77,9 +75,12 @@ pip install simplejpeg > **Note** > -> `fastplotlib` and `pygfx` are fast evolving projects, the version available through pip might be outdated, you will need to follow the "For developers" instructions below if you want the latest features. You can find the release history on pypi here: https://pypi.org/project/fastplotlib/#history +> `fastplotlib` and `pygfx` are fast evolving projects, the version available through pip might be outdated, you will need to follow the "For developers" instructions below if you want the latest features. You can find the release history here: https://github.com/fastplotlib/fastplotlib/releases ### For developers + +Make sure you have [git-lfs](https://github.com/git-lfs/git-lfs#installing) installed. + ```bash git clone https://github.com/fastplotlib/fastplotlib.git cd fastplotlib @@ -88,16 +89,20 @@ cd fastplotlib pip install -e ".[notebook,docs,tests]" ``` +Se [Contributing](https://github.com/fastplotlib/fastplotlib?tab=readme-ov-file#heart-contributing) for more details on development + # Examples -> **Note** -> -> `fastplotlib` and `pygfx` are fast evolving, you may require the latest `pygfx` and `fastplotlib` from github to use the examples in the main branch. +> **Note:** `fastplotlib` and `pygfx` are fast evolving, you will probably require the latest `pygfx` and `fastplotlib` from github to use the examples in the main branch. + +`fastplotlib` code is identical across notebook (`jupyter`), and desktop use with `Qt`/`PySide` or `glfw`. -Note that `fastplotlib` code is basically identical between desktop and notebook usage. The differences are: +Even if you do not intend to use notebooks with `fastplotlib`, the `quickstart.ipynb` notebook is currently the best way to get familiar with the API: https://github.com/fastplotlib/fastplotlib/tree/main/examples/notebooks/quickstart.ipynb + +The specifics for running `fastplotlib` in different GUI frameworks are: - Running in `glfw` requires a `fastplotlib.run()` call (which is really just a `wgpu` `run()` call) -- To use it in `Qt` you must encapsulate it within a `QApplication`, see `examples/qt` -- Notebooks plots have ipywidget-based toolbars and widgets 😄 +- With `Qt` you can encapsulate it within a `QApplication`, see `examples/qt` +- Notebooks plots have ipywidget-based toolbars and widgets. There are plans to move toward an identical in-canvas toolbar with UI elements across all supported frameworks 😄 ### Desktop examples using `glfw` or `Qt` @@ -120,7 +125,7 @@ Notebook examples are here: https://github.com/fastplotlib/fastplotlib/tree/main/examples/notebooks -**Start with `simple.ipynb`.** +**Start with `quickstart.ipynb`.** Some of the examples require imageio: ``` @@ -135,7 +140,9 @@ Our SciPy 2023 talk walks through numerous demos: https://github.com/fastplotlib You will need a relatively modern GPU (newer integrated GPUs in CPUs are usually fine). Generally if your GPU is from 2017 or later it should be fine. -For more information see: https://wgpu-py.readthedocs.io/en/stable/start.html#platform-requirements +For more detailed information, such as use on cloud computing infrastructure, see: https://wgpu-py.readthedocs.io/en/stable/start.html#platform-requirements + +Some more information on GPUs is here: https://fastplotlib.readthedocs.io/en/latest/user_guide/gpu.html ### Windows: Vulkan drivers should be installed by default on Windows 11, but you will need to install your GPU manufacturer's driver package (Nvidia or AMD). If you have an integrated GPU within your CPU, you might still need to install a driver package too, check your CPU manufacturer's info. @@ -162,7 +169,7 @@ sudo apt install llvm-dev libturbojpeg* libgl1-mesa-dev libgl1-mesa-glx libglapi ``` ### Mac OSX: -WGPU uses Metal instead of Vulkan on Mac. You will need at least Mac OSX 10.13. The OS should come with Metal pre-installed so you should be good to go! +WGPU uses Metal instead of Vulkan on Mac. You will need at least Mac OSX 10.13. The OS should come with Metal pre-installed, so you should be good to go! # :heart: Contributing diff --git a/examples/notebooks/nb_test_utils.py b/examples/notebooks/nb_test_utils.py index cb84a5271..791640fe2 100644 --- a/examples/notebooks/nb_test_utils.py +++ b/examples/notebooks/nb_test_utils.py @@ -21,6 +21,13 @@ # store all the failures to allow the nb to proceed to test other examples FAILURES = list() +if "FASTPLOTLIB_NB_TESTS" not in os.environ.keys(): + TESTING = False + +else: + if os.environ["FASTPLOTLIB_NB_TESTS"] == "1": + TESTING = True + # TODO: consolidate testing functions into one module so we don't have this separate one for notebooks @@ -83,18 +90,8 @@ def normalize_image(img): return img -def _run_tests(): - if "FASTPLOTLIB_NB_TESTS" not in os.environ.keys(): - return False - - if os.environ["FASTPLOTLIB_NB_TESTS"] == "1": - return True - - return False - - def plot_test(name, fig: fpl.Figure): - if not _run_tests(): + if not TESTING: return snapshot = fig.canvas.snapshot() @@ -157,7 +154,7 @@ def get_diffs_rgba(slicer): def notebook_finished(): - if not _run_tests(): + if not TESTING: return if len(FAILURES) > 0: diff --git a/examples/notebooks/simple.ipynb b/examples/notebooks/quickstart.ipynb similarity index 65% rename from examples/notebooks/simple.ipynb rename to examples/notebooks/quickstart.ipynb index 3b42385c8..c40e5c9ff 100644 --- a/examples/notebooks/simple.ipynb +++ b/examples/notebooks/quickstart.ipynb @@ -7,9 +7,9 @@ "tags": [] }, "source": [ - "# Introduction to `fastplotlib`\n", + "# Introduction to `fastplotlib` 🚀\n", "\n", - "This notebook goes through the basic components of the `fastplotlib` API, image, image updates, line plots, and scatter plots. " + "This notebook goes through the basic components of the `fastplotlib` API, image, line, scatter plots, subplots and simple animations" ] }, { @@ -37,6 +37,7 @@ "execution_count": null, "id": "5c50e177-5800-4e19-a4f6-d0e0a082e4cd", "metadata": { + "is_executing": true, "tags": [] }, "outputs": [], @@ -68,7 +69,7 @@ "outputs": [], "source": [ "# this is only for testing, you do not need this to use fastplotlib\n", - "from nb_test_utils import plot_test, notebook_finished" + "from nb_test_utils import plot_test, notebook_finished, TESTING" ] }, { @@ -683,16 +684,16 @@ "xs = np.linspace(-10, 10, 100)\n", "# sine wave\n", "ys = np.sin(xs)\n", - "sine = np.dstack([xs, ys])[0]\n", + "sine = np.column_stack([xs, ys])\n", "\n", "# cosine wave\n", "ys = np.cos(xs) + 5\n", - "cosine = np.dstack([xs, ys])[0]\n", + "cosine = np.column_stack([xs, ys])\n", "\n", "# sinc function\n", "a = 0.5\n", "ys = np.sinc(xs) * 3 + 8\n", - "sinc = np.dstack([xs, ys])[0]" + "sinc = np.column_stack([xs, ys])" ] }, { @@ -1009,6 +1010,104 @@ "fig_lines.close()" ] }, + { + "cell_type": "markdown", + "id": "3ada943c-f02c-419b-b384-3865ecbe25fb", + "metadata": {}, + "source": [ + "# Animation example with lines" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8fb64bed-3b47-43e3-9ef5-f8223005b7d2", + "metadata": {}, + "outputs": [], + "source": [ + "# just another example of animations\n", + "start, stop = 0, 2 * np.pi\n", + "increment = (2 * np.pi) / 50\n", + "\n", + "# make a simple sine wave\n", + "xs = np.linspace(start, stop, 100)\n", + "ys = np.sin(xs)\n", + "\n", + "fig = fpl.Figure()\n", + "fig[0, 0].add_line(ys, name=\"sine\")\n", + "\n", + "fig.show(maintain_aspect=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3002bc55-2612-40c8-a088-c07e138b739a", + "metadata": {}, + "outputs": [], + "source": [ + "# increment along the x-axis on each render loop :D \n", + "def update_line(subplot):\n", + " global increment, start, stop\n", + " xs = np.linspace(start + increment, stop + increment, 100)\n", + " ys = np.sin(xs)\n", + " \n", + " start += increment\n", + " stop += increment\n", + "\n", + " # change only the y-axis values of the line\n", + " subplot[\"sine\"].data[:, 1] = ys\n", + "\n", + "\n", + "fig[0, 0].add_animations(update_line)" + ] + }, + { + "cell_type": "markdown", + "id": "fc8c68af-810a-4564-b97d-020054b57f37", + "metadata": {}, + "source": [ + "You can remove an animation" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6321c989-60b2-4c9b-a638-a7ac1a2e4a84", + "metadata": {}, + "outputs": [], + "source": [ + "fig[0, 0].remove_animation(update_line)" + ] + }, + { + "cell_type": "markdown", + "id": "21bb17a8-cfca-4f4b-adc9-614fcffad447", + "metadata": {}, + "source": [ + "And add it back" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7749a085-d853-4859-bf99-f2bbecb50306", + "metadata": {}, + "outputs": [], + "source": [ + "fig[0, 0].add_animations(update_line)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2cf02a6c-cb1a-4972-ae96-09e6ba37e9dd", + "metadata": {}, + "outputs": [], + "source": [ + "fig.close()" + ] + }, { "cell_type": "markdown", "id": "2c90862e-2f2a-451f-a468-0cf6b857e87a", @@ -1036,7 +1135,7 @@ "\n", "# use 3D data\n", "# note: you usually mix 3D and 2D graphics on the same plot\n", - "spiral = np.dstack([xs, ys, zs])[0]\n", + "spiral = np.column_stack([xs, ys, zs])\n", "\n", "fig_l3d[0, 0].add_line(data=spiral, thickness=2, cmap='winter')\n", "\n", @@ -1103,6 +1202,17 @@ "fig_l3d[0, 0].controller = \"panzoom\"" ] }, + { + "cell_type": "code", + "execution_count": null, + "id": "11577230-8268-4b1d-a384-62d4c7f2483f", + "metadata": {}, + "outputs": [], + "source": [ + "# or an orbit controller\n", + "fig_l3d[0, 0].controller = \"orbit\"" + ] + }, { "cell_type": "code", "execution_count": null, @@ -1116,6 +1226,141 @@ "fig_l3d.close()" ] }, + { + "cell_type": "markdown", + "id": "4221ecae-74dc-464c-addf-f4fe91614a26", + "metadata": {}, + "source": [ + "# A travelling electromagnetic wave :D " + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "34139d49-af6e-4dc9-90cc-fbf193a64e7f", + "metadata": {}, + "outputs": [], + "source": [ + "import fastplotlib as fpl\n", + "import numpy as np" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0716a5c9-cf7b-417c-b809-032f5a217c4d", + "metadata": {}, + "outputs": [], + "source": [ + "fig_em = fpl.Figure(\n", + " cameras=\"3d\", \n", + " controller_types=\"orbit\", \n", + " size=(700, 400)\n", + ")\n", + "\n", + "start, stop = 0, 4 * np.pi\n", + "\n", + "# let's define the x, y and z axes for each with direction of wave propogation along the z-axis\n", + "# electric field in the xz plane travelling along\n", + "zs = np.linspace(start, stop, 200)\n", + "e_ys = np.zeros(200)\n", + "e_xs = np.sin(zs)\n", + "electric = np.column_stack([e_xs, e_ys, zs])\n", + "\n", + "# magnetic field in the yz plane\n", + "zs = np.linspace(start, stop, 200)\n", + "m_ys = np.sin(zs)\n", + "m_xs = np.zeros(200)\n", + "magnetic = np.column_stack([m_xs, m_ys, zs])\n", + "\n", + "# add the lines\n", + "fig_em[0, 0].add_line(electric, colors=\"blue\", thickness=2, name=\"e\")\n", + "fig_em[0, 0].add_line(magnetic, colors=\"red\", thickness=2, name=\"m\")\n", + "\n", + "# draw vector line at every 10th position\n", + "electric_vectors = [np.array([[0, 0, z], [x, 0, z]]) for (x, z) in zip(e_xs[::10], zs[::10])]\n", + "magnetic_vectors = [np.array([[0, 0, z], [0, y, z]]) for (y, z) in zip(m_ys[::10], zs[::10])]\n", + "\n", + "# add as a line collection\n", + "fig_em[0, 0].add_line_collection(electric_vectors, colors=\"blue\", thickness=1.5, name=\"e-vec\", z_offset=0)\n", + "fig_em[0, 0].add_line_collection(magnetic_vectors, colors=\"red\", thickness=1.5, name=\"m-vec\", z_offset=0)\n", + "# note that the z_offset in `add_line_collection` is not data-related\n", + "# it is the z-offset for where to place the *graphic*, by default with Orthographic cameras (i.e. 2D views)\n", + "# it will increment by 1 for each line in the collection, we want to disable this so set z_position=0\n", + "\n", + "# axes are a WIP, just draw a white line along z for now\n", + "z_axis = np.array([[0, 0, 0], [0, 0, stop]])\n", + "fig_em[0, 0].add_line(z_axis, colors=\"w\", thickness=1)\n", + "\n", + "# just a pre-saved camera state\n", + "state = {\n", + " 'position': np.array([-8.0 , 6.0, -2.0]),\n", + " 'rotation': np.array([0.09, 0.9 , 0.2, -0.5]),\n", + " 'scale': np.array([1., 1., 1.]),\n", + " 'reference_up': np.array([0., 1., 0.]),\n", + " 'fov': 50.0,\n", + " 'width': 12,\n", + " 'height': 12,\n", + " 'zoom': 1.35,\n", + " 'maintain_aspect': True,\n", + " 'depth_range': None\n", + "}\n", + "\n", + "\n", + "fig_em[0, 0].camera.set_state(state)\n", + "\n", + "fig_em.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "faa52d55-2631-422d-8836-ec371be728c0", + "metadata": {}, + "outputs": [], + "source": [ + "fig_em[0, 0].camera.zoom = 1.5" + ] + }, + { + "cell_type": "markdown", + "id": "d886c63b-7bcb-40d4-b315-dffff71f82f0", + "metadata": {}, + "source": [ + "## Animation for the EM wave" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7ae54258-0c5a-40cc-9ca0-40b47d25de9a", + "metadata": {}, + "outputs": [], + "source": [ + "increment = np.pi * 4 / 100\n", + "\n", + "# moves the wave one step along the z-axis\n", + "def tick(subplot):\n", + " global increment, start, stop, zs\n", + " new_zs = np.linspace(start, stop, 200)\n", + " new_data = np.sin(new_zs)\n", + "\n", + " # just change the x-axis vals for the electric field\n", + " subplot[\"e\"].data[:, 0] = new_data\n", + " # and y-axis vals for magnetic field\n", + " subplot[\"m\"].data[:, 1] = new_data\n", + "\n", + " # update the vector lines\n", + " for i, (value, z) in enumerate(zip(new_data[::10], zs[::10])):\n", + " subplot[\"e-vec\"].graphics[i].data = np.array([[0, 0, z], [value, 0, z]])\n", + " subplot[\"m-vec\"].graphics[i].data = np.array([[0, 0, z], [0, value, z]])\n", + " \n", + " start += increment\n", + " stop += increment\n", + "\n", + "fig_em[0, 0].add_animations(tick)" + ] + }, { "cell_type": "markdown", "id": "a202b3d0-2a0b-450a-93d4-76d0a1129d1d", @@ -1263,6 +1508,437 @@ "fig_scatter.close()" ] }, + { + "cell_type": "markdown", + "id": "b354b04d", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "## More subplots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e0797523", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# Figure of shape 2 x 3 with all controllers synced\n", + "figure_grid = fpl.Figure(shape=(2, 3), controller_ids=\"sync\")\n", + "\n", + "# Make a random image graphic for each subplot\n", + "for subplot in figure_grid:\n", + " # create image data\n", + " data = np.random.rand(512, 512)\n", + " # add an image to the subplot\n", + " subplot.add_image(data, name=\"rand-img\")\n", + "\n", + "# Define a function to update the image graphics with new data\n", + "# add_animations will pass the gridplot to the animation function\n", + "def update_data(f):\n", + " for subplot in f:\n", + " new_data = np.random.rand(512, 512)\n", + " # index the image graphic by name and set the data\n", + " subplot[\"rand-img\"].data = new_data\n", + "\n", + "# add the animation function\n", + "figure_grid.add_animations(update_data)\n", + "\n", + "# show the gridplot\n", + "figure_grid.show()" + ] + }, + { + "cell_type": "markdown", + "id": "0c5b20e5", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "### Slicing GridPlot" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a14b7e90", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# positional indexing\n", + "# row 0 and col 0\n", + "figure_grid[0, 0]" + ] + }, + { + "cell_type": "markdown", + "id": "45f29bed", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "You can get the graphics within a subplot, just like with simple `Plot`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "fbe632aa", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[0, 1].graphics" + ] + }, + { + "cell_type": "markdown", + "id": "44ccf745", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "and change their properties" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "85e6bf84", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[0, 1].graphics[0].vmax = 0.5" + ] + }, + { + "cell_type": "markdown", + "id": "fb4155b9", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "more slicing with `GridPlot`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "e6c3af07", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# you can give subplots human-readable string names\n", + "figure_grid[0, 2].name = \"top-right-plot\"" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8848486b", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[\"top-right-plot\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cb7566a5", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# view its position\n", + "figure_grid[\"top-right-plot\"].position" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a002a426", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# these are really the same\n", + "figure_grid[\"top-right-plot\"] is figure_grid[0, 2]" + ] + }, + { + "cell_type": "markdown", + "id": "df361421", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "Indexing with subplot name and graphic name" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c9915469", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[\"top-right-plot\"][\"rand-img\"].vmin = 0.5" + ] + }, + { + "cell_type": "markdown", + "id": "219648d3", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "## Figure subplot customization" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1dcfe24c", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# 2 rows and 3 columns\n", + "shape = (2, 3)\n", + "\n", + "# pan-zoom controllers for each view\n", + "# views are synced if they have the\n", + "# same controller ID\n", + "controller_ids = [\n", + " [0, 3, 1], # id each controller with an integer\n", + " [2, 2, 3]\n", + "]\n", + "\n", + "\n", + "# you can give string names for each subplot within the gridplot\n", + "names = [\n", + " [\"subplot0\", \"subplot1\", \"subplot2\"],\n", + " [\"subplot3\", \"subplot4\", \"subplot5\"]\n", + "]\n", + "\n", + "# Create the grid plot\n", + "figure_grid = fpl.Figure(\n", + " shape=shape,\n", + " controller_ids=controller_ids,\n", + " names=names,\n", + ")\n", + "\n", + "\n", + "# Make a random image graphic for each subplot\n", + "for subplot in figure_grid:\n", + " data = np.random.rand(512, 512)\n", + " # create and add an ImageGraphic\n", + " subplot.add_image(data=data, name=\"rand-image\")\n", + "\n", + "\n", + "# Define a function to update the image graphics\n", + "# with new randomly generated data\n", + "def set_random_frame(gp):\n", + " for subplot in gp:\n", + " new_data = np.random.rand(512, 512)\n", + " subplot[\"rand-image\"].data = new_data\n", + "\n", + "# add the animation\n", + "figure_grid.add_animations(set_random_frame)\n", + "figure_grid.show()" + ] + }, + { + "cell_type": "markdown", + "id": "be699284", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "Indexing the gridplot to access subplots" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "212a6e4f", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# can access subplot by name\n", + "figure_grid[\"subplot0\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b758b240", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# can access subplot by index\n", + "figure_grid[0, 0]" + ] + }, + { + "cell_type": "markdown", + "id": "868f0de4", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "**subplots also support indexing!**\n", + "\n", + "this can be used to get graphics if they are named" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "bc14549d", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "# can access graphic directly via name\n", + "figure_grid[\"subplot0\"][\"rand-image\"]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99e3726e", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[\"subplot0\"][\"rand-image\"].vmin = 0.6\n", + "figure_grid[\"subplot0\"][\"rand-image\"].vmax = 0.8" + ] + }, + { + "cell_type": "markdown", + "id": "e3350b37", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "source": [ + "positional indexing also works event if subplots have string names" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3b1986f5", + "metadata": { + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], + "source": [ + "figure_grid[1, 0][\"rand-image\"].vim = 0.1\n", + "figure_grid[1, 0][\"rand-image\"].vmax = 0.3" + ] + }, { "cell_type": "code", "execution_count": null, diff --git a/fastplotlib/__init__.py b/fastplotlib/__init__.py index c1e4d2f2c..8b46dcc0b 100644 --- a/fastplotlib/__init__.py +++ b/fastplotlib/__init__.py @@ -1,6 +1,6 @@ from pathlib import Path -from .utils.gui import run +from .utils.gui import run # noqa from .graphics import * from .graphics.selectors import * from .legends import * @@ -14,4 +14,16 @@ __version__ = f.read().split("\n")[0] if len(enumerate_adapters()) < 1: - raise IndexError("No WGPU adapters found, fastplotlib will not work.") + raise IndexError( + f"WGPU could not enumerate any adapters, fastplotlib will not work.\n" + f"This is caused by one of the following:\n" + f"1. You do not have a hardware GPU installed and you do not have " + f"software rendering (ex. lavapipe) installed either\n" + f"2. Your GPU drivers are not installed or something is wrong with your GPU driver installation, " + f"re-installing the latest drivers from your hardware vendor (probably Nvidia or AMD) may help.\n" + f"3. You are missing system libraries that are required for WGPU to access GPU(s), this is " + f"common in cloud computing environments.\n" + f"These two links can help you troubleshoot:\n" + f"https://wgpu-py.readthedocs.io/en/stable/start.html#platform-requirements\n" + f"https://fastplotlib.readthedocs.io/en/latest/user_guide/gpu.html\n" + ) diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 1c2e151e8..da74cc54e 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -19,7 +19,7 @@ class LineCollection(GraphicCollection, Interaction): def __init__( self, data: List[np.ndarray], - z_position: Iterable[float] | float = None, + z_offset: Iterable[float | int] | float | int = None, thickness: float | Iterable[float] = 2.0, colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", alpha: float = 1.0, @@ -39,9 +39,9 @@ def __init__( List of line data to plot, each element must be a 1D, 2D, or 3D numpy array if elements are 2D, interpreted as [y_vals, n_lines] - z_position: Iterable of float or float, optional - | if ``float``, single position will be used for all lines - | if ``list`` of ``float``, each value will apply to the individual lines + z_offset: Iterable of float or float, optional + | if ``float`` | ``int``, single offset will be used for all lines + | if ``list`` of ``float`` | ``int``, each value will apply to the individual lines thickness: float or Iterable of float, default 2.0 | if ``float``, single thickness will be used for all lines @@ -90,8 +90,8 @@ def __init__( super().__init__(name) - if not isinstance(z_position, float) and z_position is not None: - if len(data) != len(z_position): + if not isinstance(z_offset, (float, int)) and z_offset is not None: + if len(data) != len(z_offset): raise ValueError( "z_position must be a single float or an iterable with same length as data" ) @@ -178,10 +178,10 @@ def __init__( self._set_world_object(pygfx.Group()) for i, d in enumerate(data): - if isinstance(z_position, list): - _z = z_position[i] + if isinstance(z_offset, list): + _z = z_offset[i] else: - _z = 1.0 + _z = z_offset if isinstance(thickness, list): _s = thickness[i] @@ -478,7 +478,7 @@ class LineStack(LineCollection): def __init__( self, data: List[np.ndarray], - z_position: Iterable[float] | float = None, + z_offset: Iterable[float] | float = None, thickness: float | Iterable[float] = 2.0, colors: str | Iterable[str] | np.ndarray | Iterable[np.ndarray] = "w", alpha: float = 1.0, @@ -500,8 +500,8 @@ def __init__( List of line data to plot, each element must be a 1D, 2D, or 3D numpy array if elements are 2D, interpreted as [y_vals, n_lines] - z_position: Iterable of float or float, optional - | if ``float``, single position will be used for all lines + z_offset: Iterable of float or float, optional + | if ``float``, single offset will be used for all lines | if ``list`` of ``float``, each value will apply to the individual lines thickness: float or Iterable of float, default 2.0 @@ -550,7 +550,7 @@ def __init__( """ super().__init__( data=data, - z_position=z_position, + z_offset=z_offset, thickness=thickness, colors=colors, alpha=alpha, diff --git a/fastplotlib/layouts/_figure.py b/fastplotlib/layouts/_figure.py index 7e83e103a..1c7439613 100644 --- a/fastplotlib/layouts/_figure.py +++ b/fastplotlib/layouts/_figure.py @@ -235,30 +235,23 @@ def __init__( # `create_controller()` will auto-determine controller for each subplot based on defaults controller_types = np.array(["default"] * len(self)).reshape(self.shape) - elif isinstance(controller_types, str): - if controller_types not in valid_controller_types.keys(): - raise ValueError( - f"invalid controller_types argument, you may pass either a single controller type as a str, or an" - f"iterable of controller types from the selection: {valid_controller_types.keys()}" - ) - # valid controller types + if isinstance(controller_types, str): + controller_types = [[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) - ): + if controller_type not in valid_str: 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]}" + f"You have passed the invalid `controller_type`: {controller_type}. " + f"Valid `controller_types` arguments are:\n {valid_str}" ) controller_types: np.ndarray[pygfx.Controller] = np.asarray( diff --git a/fastplotlib/layouts/_graphic_methods_mixin.py b/fastplotlib/layouts/_graphic_methods_mixin.py index a7acb5eec..9f82cfed5 100644 --- a/fastplotlib/layouts/_graphic_methods_mixin.py +++ b/fastplotlib/layouts/_graphic_methods_mixin.py @@ -178,7 +178,7 @@ def add_image( def add_line_collection( self, data: List[numpy.ndarray], - z_position: Union[Iterable[float], float] = None, + z_offset: Union[Iterable[float], float] = None, thickness: Union[float, Iterable[float]] = 2.0, colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", alpha: float = 1.0, @@ -199,8 +199,8 @@ def add_line_collection( List of line data to plot, each element must be a 1D, 2D, or 3D numpy array if elements are 2D, interpreted as [y_vals, n_lines] - z_position: Iterable of float or float, optional - | if ``float``, single position will be used for all lines + z_offset: Iterable of float or float, optional + | if ``float``, single offset will be used for all lines | if ``list`` of ``float``, each value will apply to the individual lines thickness: float or Iterable of float, default 2.0 @@ -251,7 +251,7 @@ def add_line_collection( return self._create_graphic( LineCollection, data, - z_position, + z_offset, thickness, colors, alpha, @@ -348,7 +348,7 @@ def add_line( def add_line_stack( self, data: List[numpy.ndarray], - z_position: Union[Iterable[float], float] = None, + z_offset: Union[Iterable[float], float] = None, thickness: Union[float, Iterable[float]] = 2.0, colors: Union[str, Iterable[str], numpy.ndarray, Iterable[numpy.ndarray]] = "w", alpha: float = 1.0, @@ -371,8 +371,8 @@ def add_line_stack( List of line data to plot, each element must be a 1D, 2D, or 3D numpy array if elements are 2D, interpreted as [y_vals, n_lines] - z_position: Iterable of float or float, optional - | if ``float``, single position will be used for all lines + z_offset: Iterable of float or float, optional + | if ``float``, single offset will be used for all lines | if ``list`` of ``float``, each value will apply to the individual lines thickness: float or Iterable of float, default 2.0 @@ -423,7 +423,7 @@ def add_line_stack( return self._create_graphic( LineStack, data, - z_position, + z_offset, thickness, colors, alpha, diff --git a/fastplotlib/layouts/_plot_area.py b/fastplotlib/layouts/_plot_area.py index bbc5b0e15..6ff07a748 100644 --- a/fastplotlib/layouts/_plot_area.py +++ b/fastplotlib/layouts/_plot_area.py @@ -466,7 +466,10 @@ def add_graphic(self, graphic: Graphic, center: bool = True): self._add_or_insert_graphic(graphic=graphic, center=center, action="add") - graphic.position_z = len(self) + if self.camera.fov == 0: + # for orthographic positions stack objects along the z-axis + # for perspective projections we assume the user wants full 3D control + graphic.position_z = len(self) def insert_graphic( self, @@ -505,10 +508,13 @@ def insert_graphic( graphic=graphic, center=center, action="insert", index=index ) - if z_position is None: - graphic.position_z = index - else: - graphic.position_z = z_position + if self.camera.fov == 0: + # for orthographic positions stack objects along the z-axis + # for perspective projections we assume the user wants full 3D control + if z_position is None: + graphic.position_z = index + else: + graphic.position_z = z_position def _add_or_insert_graphic( self,
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: