diff --git a/.gitignore b/.gitignore index 68bc17f..3c63430 100644 --- a/.gitignore +++ b/.gitignore @@ -158,3 +158,7 @@ cython_debug/ # and can be added to the global gitignore or merged into this file. For a more nuclear # option (not recommended) you can uncomment the following to ignore the entire idea folder. #.idea/ + +fpl-scipy2023-data.zip +fpl-scipy2023-data/ +.idea/ \ No newline at end of file diff --git a/README.md b/README.md index 5172e8d..67b25a7 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,51 @@ -# fastplotlib-scipy2023 -fastplotlib demos at scipiy 2023 +# fastplotlib SciPy 2023 + +Materials for the [`fastplotlib`](https://github.com/kushalkolar/fastplotlib/) talk at SciPy 2023. This repo includes installation instructions and the demo notebooks covered in our talk. + +For more info on `fastplotlib` see the repo: https://github.com/kushalkolar/fastplotlib/ + +The demos cover the following topics: +1. `Images` - plotting simple images, feature changes, image updates, `ImageWidget` +2. `Lines` - 2D line plots, fancy indexing of features, `LineCollection`, `LineStack`, `LinearSelector`, `LinearRegionSelector`, `Heatmap` +4. Interactivity - high-level interactivity system to link graphics and their features together, addding event handlers for further interaction +5. `GridPlots` - grid of `Subplots` +6. `Scatters` - 2D and 3D scatter plots + +# Installation instructions + +See the `fastplotlib` repo for [installation](https://github.com/kushalkolar/fastplotlib#installation). + +In order to run the notebooks you will also need to have `imageio` and `zarr` installed. These are not dependencies of `fastplotlib`, but are being used in these demos. + +### Install using pip +``` +# other packages specifically used for this demo +# jupyterlab v3 because of sidecar, and pin pylingalg because of pygfx +pip install "jupyterlab<4" imageio "imageio[pyav]" zarr sidecar pylinalg==0.4.0 scikit-image glfw + +# optional, you'll need C compilers +pip install simplejpeg + +# fastplotlib with notebook dependencies +pip install "fastplotlib[notebook]" +``` + +# General `fastplotlib` API +## 1. Graphics - objects that are drawn +- `Image`, `Line`, `Scatter`, `Heatmap` +- Collections - `LineCollection`, `LineStack` (ex: neural timeseries data) +- Interactions +## 2. Layouts +- `Plot` - a single plot area +- `GridPlot` - a grid of `Subplots` +## 3. Widgets - high level widgets to make repetitive UIs easier +- `ImageWidget`- n-dimensional widget for Image data +- Sliders, support window functions, `GridPlot`, etc. + +# Docs +For a more in-depth look at our API, please visit our [docs](https://fastplotlib.readthedocs.io/en/). + +# Contributions +`fastplotlib` is a relatively new library, and we are always looking for feedback or help! Please see the [contributing guide](https://github.com/kushalkolar/fastplotlib/blob/master/CONTRIBUTING.md). + +You can also look at our [Roadmap for 2023](https://github.com/kushalkolar/fastplotlib/issues/55) and [Issues](https://github.com/kushalkolar/fastplotlib/issues) for more ideas on how to contribute. diff --git a/download_data.py b/download_data.py new file mode 100644 index 0000000..197d69b --- /dev/null +++ b/download_data.py @@ -0,0 +1,39 @@ +import os +import requests +from tqdm import tqdm +from pathlib import Path +from zipfile import ZipFile + +# make data dir if not exists +data_dir = Path(os.path.dirname(os.path.abspath(__file__)), "fpl-scipy2023-data") +os.makedirs(data_dir, exist_ok=True) + +data_file = Path( + os.path.dirname(os.path.abspath(__file__)), "fpl-scipy2023-data.zip" +) + + +def download_data(): + + # if data already exists, return + if os.path.exists(data_dir) and len(list(data_dir.iterdir())) != 0: + print("data already exists") + return + + # else download + print(f"Downloading data") + url = f"https://zenodo.org/record/8140484/files/fpl-scipy2023-data.zip" + + # basically from https://stackoverflow.com/questions/37573483/progress-bar-while-download-file-over-http-with-requests/37573701 + response = requests.get(url, stream=True) + total_size_in_bytes = int(response.headers.get("content-length", 0)) + block_size = 1024 # 1 Kibibyte + progress_bar = tqdm(total=total_size_in_bytes, unit="iB", unit_scale=True) + + with open(data_file, "wb") as file: + for data in response.iter_content(block_size): + progress_bar.update(len(data)) + file.write(data) + progress_bar.close() + + ZipFile(data_file).extractall(data_dir.parent) diff --git a/fastplotlib scipy 2023.pdf b/fastplotlib scipy 2023.pdf new file mode 100644 index 0000000..92ed279 Binary files /dev/null and b/fastplotlib scipy 2023.pdf differ diff --git a/gridplot.ipynb b/gridplot.ipynb deleted file mode 100644 index b6e89aa..0000000 --- a/gridplot.ipynb +++ /dev/null @@ -1,333 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "5968b66a-8e78-488e-a2a0-1b2ac72cbb28", - "metadata": {}, - "source": [ - "# GridPlot\n", - "\n", - "Allows for proper subplotting. Create graphics and add them to a `GridPlot`." - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "66eaaf8e-3324-4e77-9cc4-f52682c7caea", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import fastplotlib as fpl\n", - "import numpy as np\n", - "from sidecar import Sidecar" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "e481359b-6b49-40c5-86dd-b1c177314cd6", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "01f8aa22c8c94c8a81eba580d256bb4b", - "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" - ] - } - ], - "source": [ - "# GridPlot of shape 2 x 3 with all controllers synced\n", - "grid_plot = fpl.GridPlot(shape=(2, 3), controllers=\"sync\")\n", - "\n", - "# Make a random image graphic for each subplot\n", - "for subplot in grid_plot:\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(gp):\n", - " for sp in gp:\n", - " new_data = np.random.rand(512, 512)\n", - " # index the image graphic by name and set the data\n", - " sp[\"rand-img\"].data = new_data\n", - " \n", - "# add the animation function\n", - "grid_plot.add_animations(update_data)\n", - "\n", - "sc = Sidecar(title=\"gridplot\")\n", - "\n", - "# show the gridplot \n", - "with sc:\n", - " display(grid_plot.show())" - ] - }, - { - "cell_type": "markdown", - "id": "30596ad1-ffb7-4beb-a723-7cb5a2f45eac", - "metadata": {}, - "source": [ - "## Accessing subplots within `GridPlot`" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "ed86a89d-0cb9-44e6-97ac-56499c9a0a2e", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "unnamed: Subplot @ 0x7fac550680d0\n", - " parent: fastplotlib.GridPlot @ 0x7fac550a0d10\n", - "\n", - " Graphics:\n", - "\t'rand-img': ImageGraphic @ 0x7fac3c169310" - ] - }, - "execution_count": 3, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# positional indexing\n", - "# row 0 and col 0\n", - "grid_plot[0, 0]" - ] - }, - { - "cell_type": "markdown", - "id": "cd073684-8f3a-4283-b9c1-aad271c82752", - "metadata": {}, - "source": [ - "### You can get the graphics within a subplot, just like with simple `Plot`" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "bf671e47-70b7-428b-bfca-55123e99344d", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(,)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grid_plot[0, 1].graphics" - ] - }, - { - "cell_type": "markdown", - "id": "25a0ef00-05f3-484a-9b38-91d72104e736", - "metadata": {}, - "source": [ - "### and change their properties" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "2b9f4996-9321-488c-aa7d-99129a8a0dd0", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "grid_plot[0, 1].graphics[0].cmap.vmax = 0.5" - ] - }, - { - "cell_type": "markdown", - "id": "5337a9cf-4823-43f3-9fa5-9641f4026d50", - "metadata": {}, - "source": [ - "### more indexing with `GridPlot`" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "fb186973-1ce2-4361-bb97-0f50342f9861", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# you can give subplots human-readable string names\n", - "grid_plot[0, 2].name = \"top-right-plot\"" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "403d2d0a-7cbf-4e43-a75a-dfff98d40cf2", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "top-right-plot: Subplot @ 0x7fac3c092e50\n", - " parent: fastplotlib.GridPlot @ 0x7fac550a0d10\n", - "\n", - " Graphics:\n", - "\t'rand-img': ImageGraphic @ 0x7fac04727310" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "grid_plot[\"top-right-plot\"]" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "4f296891-a8b4-47d1-8d17-7554c36b00ed", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(0, 2)" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# view its position\n", - "grid_plot[\"top-right-plot\"].position" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "85f06940-3eec-49e9-97a0-00054ad27d8a", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 9, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "# these are really the same\n", - "grid_plot[\"top-right-plot\"] is grid_plot[0, 2]" - ] - }, - { - "cell_type": "markdown", - "id": "33ced14b-f958-474e-85a9-5f395a8de82b", - "metadata": {}, - "source": [ - "Indexing with subplot name and graphic name" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "75ecdd19-bd9a-4d93-bb18-a6216e3fde1b", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "grid_plot[\"top-right-plot\"][\"rand-img\"].cmap.vmin = 0.5" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "4f5f7fab-d7f2-4b6b-8f2d-c0745f9193cb", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "sc.close()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "204385ee-b164-4e74-8b03-e2f0013f9e47", - "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/image.ipynb b/image.ipynb index 0d2dd71..9f5bb5b 100644 --- a/image.ipynb +++ b/image.ipynb @@ -13,7 +13,10 @@ { "cell_type": "markdown", "id": "807a45e5-5473-477b-9cb3-41a03a8135c7", - "metadata": {}, + "metadata": { + "jp-MarkdownHeadingCollapsed": true, + "tags": [] + }, "source": [ "## `fastplotlib` API\n", "\n", @@ -33,6 +36,21 @@ " " ] }, + { + "cell_type": "markdown", + "id": "03169ecd-8916-4ddd-a023-066f7be7d648", + "metadata": {}, + "source": [ + "# Running remotely on a workstation on the East Coast!!\n", + "\n", + "Using Jupyter remote frame buffer: https://github.com/vispy/jupyter_rfb\n", + "\n", + "Rendering all being done on remote GPU. We are only displaying a rasterized image in the Jupyter canvas.\n", + "\n", + "### GPU: Nvidia RTX 3090\n", + "### CPU: AMD Ryzen 16 core (5950X)\n" + ] + }, { "cell_type": "markdown", "id": "b95759bc-b567-4327-b825-68fe871c3473", @@ -54,81 +72,71 @@ { "cell_type": "code", "execution_count": 1, - "id": "775411ec-15da-4231-a67f-9300265c6ac6", + "id": "9aeb2e39-cc13-4662-ada0-cfed7ee2b26c", "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: imageio in /home/clewis7/venvs/mescore/lib/python3.11/site-packages (2.28.1)\n", - "Requirement already satisfied: numpy in /home/clewis7/venvs/mescore/lib/python3.11/site-packages (from imageio) (1.23.5)\n", - "Requirement already satisfied: pillow>=8.3.2 in /home/clewis7/venvs/mescore/lib/python3.11/site-packages (from imageio) (9.5.0)\n" - ] - } - ], - "source": [ - "!pip install imageio" - ] - }, - { - "cell_type": "markdown", - "id": "f6dc8a89-f6c3-4e22-84c5-7e615b452a1e", - "metadata": {}, + "outputs": [], "source": [ - "**An example video used later required the use of tiffile to load it into memory, but tiffile is not required to use fastplotlib**" + "import imageio.v3 as iio\n", + "\n", + "# likewise there is an image which we will load with tifffile\n", + "import tifffile" ] }, { "cell_type": "code", - "execution_count": 37, - "id": "c74ee3e0-572f-4619-b875-d313de81d7ef", + "execution_count": 2, + "id": "fb57c3d3-f20d-4d88-9e7a-04b9309bc637", "metadata": { "tags": [] }, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Requirement already satisfied: tiffile in /home/clewis7/venvs/mescore/lib/python3.11/site-packages (2018.10.18)\n", - "Requirement already satisfied: tifffile in /home/clewis7/venvs/mescore/lib/python3.11/site-packages (from tiffile) (2023.4.12)\n", - "Requirement already satisfied: numpy in /home/clewis7/venvs/mescore/lib/python3.11/site-packages (from tifffile->tiffile) (1.23.5)\n" - ] - } - ], + "outputs": [], "source": [ - "!pip install tiffile" + "import fastplotlib as fpl\n", + "from ipywidgets import VBox, HBox, IntSlider\n", + "import numpy as np\n", + "from sidecar import Sidecar" + ] + }, + { + "cell_type": "markdown", + "id": "0f02c2fe-e77d-447d-9483-da66574a3950", + "metadata": {}, + "source": [ + "**Additionally, the data we are using throughout these demos can be downloaded from Zenodo.**" ] }, { "cell_type": "code", - "execution_count": 40, - "id": "9aeb2e39-cc13-4662-ada0-cfed7ee2b26c", + "execution_count": 3, + "id": "99f0c624-85a6-4a42-92b0-30b0b04075b4", "metadata": { "tags": [] }, "outputs": [], "source": [ - "import imageio.v3 as iio\n", - "import tifffile" + "from download_data import download_data" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "fb57c3d3-f20d-4d88-9e7a-04b9309bc637", + "execution_count": 4, + "id": "630e4762-de01-47fe-be9d-547182a1fbf9", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "data already exists\n" + ] + } + ], "source": [ - "import fastplotlib as fpl\n", - "from ipywidgets import VBox, HBox, IntSlider\n", - "import numpy as np\n", - "from sidecar import Sidecar" + "download_data()" ] }, { @@ -141,7 +149,7 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 5, "id": "237823b7-e2c0-4e2f-9ee8-e3fc2b4453c4", "metadata": { "tags": [] @@ -150,7 +158,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "224fd3b3c63449e3956c393bfa930772", + "model_id": "06aa06e6af134d61bb3a40df62a601c2", "version_major": 2, "version_minor": 0 }, @@ -164,7 +172,7 @@ ], "source": [ "# create a `Plot` instance\n", - "plot = fpl.Plot()\n", + "plot = fpl.Plot(size=(600, 500))\n", "\n", "# get a grayscale image\n", "data = iio.imread(\"imageio:camera.png\")\n", @@ -172,8 +180,8 @@ "# plot the image data\n", "image_graphic = plot.add_image(data=data, name=\"sample-image\")\n", "\n", - "# display plot in side car\n", - "sc = Sidecar(title=\"sample image\")\n", + "# display plot in sidecar\n", + "sc = Sidecar(title=\"sample image\", layout={'width': '800px'})\n", "\n", "with sc:\n", " # show the plot\n", @@ -206,7 +214,7 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 6, "id": "de816c88-1c4a-4071-8a5e-c46c93671ef5", "metadata": { "tags": [] @@ -219,434 +227,105 @@ { "cell_type": "markdown", "id": "7303677c-9f7a-44dd-8660-eced0cdca565", - "metadata": {}, - "source": [ - "### Slicing data\n", - "\n", - "**Most features, such as `data`, support slicing!**\n", - "\n", - "Our image data is of shape [n_rows, n_columns]" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "83b2db1b-2783-4e89-bcf3-66bb6e09e18a", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic.data[::15, :] = 1\n", - "image_graphic.data[:, ::15] = 1" - ] - }, - { - "cell_type": "markdown", - "id": "98e41780-eb84-46d4-8c06-ec59b3238572", - "metadata": {}, - "source": [ - "**Fancy indexing**" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "3e298c1c-7551-4401-ade0-b9af7d2bbe23", "metadata": { "tags": [] }, - "outputs": [], "source": [ - "image_graphic.data[data > 175] = 255" - ] - }, - { - "cell_type": "markdown", - "id": "958b4e14-d820-4acb-af75-3485c844de17", - "metadata": {}, - "source": [ - "**Adjust vmin vmax**" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "ea100318-023c-4963-b859-8c2a8fcce301", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic.cmap.vmin = 50\n", - "image_graphic.cmap.vmax = 150" - ] - }, - { - "cell_type": "markdown", - "id": "ba4be6e6-454b-465f-9eac-3cde424963aa", - "metadata": {}, - "source": [ - "**Set the entire data array again**\n", + "### Slicing data\n", "\n", - "Note: The shape of the new data array must match the current data shown in the `Graphic`" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "f6f6a977-f96b-43f4-8235-0f4fce1f6f57", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(512, 512, 3)" - ] - }, - "execution_count": 12, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "new_data = iio.imread(\"imageio:astronaut.png\")\n", - "new_data.shape" - ] - }, - { - "cell_type": "markdown", - "id": "38b76440-9b10-42dd-8ca5-2530d2a1da6e", - "metadata": {}, - "source": [ - "This is an RGB image, convert to grayscale to maintain the shape of (512, 512)" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "7a3dab00-6a92-4ba7-a19a-a4a3c23f3421", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(512, 512)" - ] - }, - "execution_count": 13, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "gray = new_data.dot([0.3, 0.6, 0.1])\n", - "gray.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "ef731a15-59bb-4bbf-9955-c6069d1c9a7e", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "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": [ - "image_graphic.data = gray" - ] - }, - { - "cell_type": "markdown", - "id": "ea76f601-f655-43a2-a4ab-2730643fc6a2", - "metadata": {}, - "source": [ - "**reset vmin vmax**" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "a1320f9f-fff5-4b9b-9a49-0695042ab8ab", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "image_graphic.cmap.reset_vmin_vmax()" - ] - }, - { - "cell_type": "markdown", - "id": "67b92ffd-40cc-43fe-9df9-0e0d94763d8e", - "metadata": {}, - "source": [ - "### Indexing plots\n", + "**Most features, such as `data`, support slicing!**\n", "\n", - "**Plots are indexable and give you their graphics by name**" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "e6ba689c-ff4a-44ef-9663-f2c8755072c4", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "unnamed: Plot @ 0x7f0c4b8be810\n", - " parent: None\n", - " Graphics:\n", - "\t'sample-image': ImageGraphic @ 0x7f0c044ffed0" - ] - }, - "execution_count": 16, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot" - ] - }, - { - "cell_type": "code", - "execution_count": 17, - "id": "5b18f4e3-e13b-46d5-af1f-285c5a7fdc12", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 17, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot[\"sample-image\"]" - ] - }, - { - "cell_type": "markdown", - "id": "490f0040-0462-4fac-b91e-7537e2d0b2c2", - "metadata": {}, - "source": [ - "**You can also use numerical indexing on `plot.graphics`**" - ] - }, - { - "cell_type": "code", - "execution_count": 18, - "id": "2b5c1321-1fd4-44bc-9433-7439ad3e22cf", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(,)" - ] - }, - "execution_count": 18, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot.graphics" - ] - }, - { - "cell_type": "code", - "execution_count": 19, - "id": "930ccff4-b678-4e21-a8b1-7f77a232d811", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 19, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot.graphics[0]" - ] - }, - { - "cell_type": "markdown", - "id": "57cfdeae-b6ae-409b-abe7-4b76155f1f39", - "metadata": {}, - "source": [ - "The `Graphic` instance is also returned when you call `plot.add_`" - ] - }, - { - "cell_type": "code", - "execution_count": 20, - "id": "b12bf75e-4e93-4930-9146-e96324fdf3f6", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "" - ] - }, - "execution_count": 20, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "image_graphic " - ] - }, - { - "cell_type": "code", - "execution_count": 21, - "id": "b56420be-7121-4275-9444-803308400a63", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "True" - ] - }, - "execution_count": 21, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "image_graphic == plot[\"sample-image\"]" + "Our image data is of shape [n_rows, n_columns]" ] }, { - "cell_type": "code", - "execution_count": 22, - "id": "4c369d3d-7219-4231-80cd-ce82b6a67a20", + "cell_type": "code", + "execution_count": 7, + "id": "83b2db1b-2783-4e89-bcf3-66bb6e09e18a", "metadata": { "tags": [] }, "outputs": [], "source": [ - "# close plot and sidecar\n", - "plot.close()\n", - "sc.close()" + "image_graphic.data[::15, :] = 1\n", + "image_graphic.data[:, ::15] = 1" ] }, { "cell_type": "markdown", - "id": "070e92b9-ac90-426d-b3d5-bd74a9a8a0be", + "id": "98e41780-eb84-46d4-8c06-ec59b3238572", "metadata": {}, "source": [ - "**Note: if you have a good enough gpu, you do not need to close the plots. We are doing this here for gpu conservation.**" + "**Fancy indexing**" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "3e298c1c-7551-4401-ade0-b9af7d2bbe23", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "image_graphic.data[data > 175] = 255" ] }, { "cell_type": "markdown", - "id": "f7ed56a9-c613-4a4a-9147-7dc135a00ed2", + "id": "958b4e14-d820-4acb-af75-3485c844de17", "metadata": {}, "source": [ - "### RGB images are also supported\n", - "\n", - "`cmap` arguments are ignored for rgb images, but vmin vmax still works" + "**Adjust vmin vmax**" ] }, { "cell_type": "code", - "execution_count": 23, - "id": "7f545e43-d4de-4eab-ba8d-6d98c651ffd5", + "execution_count": 9, + "id": "ea100318-023c-4963-b859-8c2a8fcce301", "metadata": { "tags": [] }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "3c3a7147853a4bdc947e17744c8ab3cc", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ - "plot_rgb = fpl.Plot()\n", - "\n", - "plot_rgb.add_image(new_data, name=\"rgb-image\")\n", - "\n", - "sc = Sidecar(title=\"rgb image\")\n", - "\n", - "with sc:\n", - " display(plot_rgb.show())" + "image_graphic.cmap.vmin = 50\n", + "image_graphic.cmap.vmax = 150" ] }, { "cell_type": "code", - "execution_count": 24, - "id": "de797c6c-fafd-4db4-9c6b-ef48b71cd9b5", + "execution_count": 10, + "id": "a1320f9f-fff5-4b9b-9a49-0695042ab8ab", "metadata": { "tags": [] }, "outputs": [], "source": [ - "plot_rgb[\"rgb-image\"].cmap.vmin = 100" + "image_graphic.cmap.reset_vmin_vmax()" ] }, { "cell_type": "code", - "execution_count": 25, - "id": "d5de925b-1dfc-4d53-9f7b-eb85b8bb7791", + "execution_count": 11, + "id": "4c369d3d-7219-4231-80cd-ce82b6a67a20", "metadata": { "tags": [] }, "outputs": [], "source": [ - "plot_rgb.close()\n", + "# close plot and sidecar\n", + "plot.close()\n", "sc.close()" ] }, + { + "cell_type": "markdown", + "id": "070e92b9-ac90-426d-b3d5-bd74a9a8a0be", + "metadata": {}, + "source": [ + "**Note: if you have a good enough gpu, you do not need to close the plots. We are doing this here for gpu conservation.**" + ] + }, { "cell_type": "markdown", "id": "1cb03f42-1029-4b16-a16b-35447d9e2955", @@ -661,7 +340,7 @@ }, { "cell_type": "code", - "execution_count": 26, + "execution_count": 12, "id": "aadd757f-6379-4f52-a709-46aa57c56216", "metadata": { "tags": [] @@ -670,7 +349,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "423592513ddd4ac7afc01e60c217614f", + "model_id": "140a3876e95c46c784ca047e1aeb8fab", "version_major": 2, "version_minor": 0 }, @@ -680,11 +359,19 @@ }, "metadata": {}, "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/_features/_base.py:34: UserWarning: converting float64 array to float32\n", + " warn(f\"converting {array.dtype} array to float32\")\n" + ] } ], "source": [ "# create another `Plot` instance\n", - "plot_v = fpl.Plot()\n", + "plot_v = fpl.Plot(size=(600, 500))\n", "\n", "plot.canvas.max_buffered_frames = 1\n", "\n", @@ -711,168 +398,17 @@ " display(plot_v.show())" ] }, - { - "cell_type": "markdown", - "id": "b313eda1-6e6c-466f-9fd5-8b70c1d3c110", - "metadata": {}, - "source": [ - "### We can share controllers across plots\n", - "\n", - "This example creates a new plot, but it synchronizes the pan-zoom controller" - ] - }, - { - "cell_type": "code", - "execution_count": 27, - "id": "86e70b1e-4328-4035-b992-70dff16d2a69", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "887a2d48c313467197aac3074ca57f25", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "340c5d8ac0fb4d86b72dc7e822bd7e1a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layout(width='auto'…" - ] - }, - "execution_count": 27, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot_sync = fpl.Plot(controller=plot_v.controller)\n", - "\n", - "data = np.random.rand(512, 512)\n", - "\n", - "image_graphic_instance = plot_sync.add_image(data=data, cmap=\"viridis\")\n", - "\n", - "# you will need to define a new animation function for this graphic\n", - "def update_data_2():\n", - " new_data = np.random.rand(512, 512)\n", - " # alternatively, you can use the stored reference to the graphic as well instead of indexing the Plot\n", - " image_graphic_instance.data = new_data\n", - "\n", - "plot_sync.add_animations(update_data_2)\n", - "\n", - "plot_sync.show()" - ] - }, - { - "cell_type": "markdown", - "id": "f226c9c2-8d0e-41ab-9ab9-1ae31fd91de5", - "metadata": {}, - "source": [ - "#### Keeping a reference to the Graphic instance, as shown above `image_graphic_instance`, is useful if you're creating something where you need flexibility in the naming of the graphics" - ] - }, - { - "cell_type": "code", - "execution_count": 28, - "id": "bd073e07-5721-4957-8a16-0767b9d64d25", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "sc.close()" - ] - }, - { - "cell_type": "markdown", - "id": "d11fabb7-7c76-4e94-893d-80ed9ee3be3d", - "metadata": {}, - "source": [ - "### You can also use `ipywidgets.VBox` and `HBox` to stack plots." - ] - }, - { - "cell_type": "code", - "execution_count": 29, - "id": "ef9743b3-5f81-4b79-9502-fa5fca08e56d", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "2296fe63359b499398f2384f3bd54664", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "VBox(children=(VBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 59, 'timestamp': 1688730919.6128232, …" - ] - }, - "execution_count": 29, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "VBox([plot_v.show(), plot_sync.show()])" - ] - }, - { - "cell_type": "code", - "execution_count": 30, - "id": "11839d95-8ff7-444c-ae13-6b072c3112c5", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "ffc38bd05bc549dd843d30576e15e1e1", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "HBox(children=(VBox(children=(JupyterWgpuCanvas(frame_feedback={'index': 76, 'timestamp': 1688730921.1709845, …" - ] - }, - "execution_count": 30, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "HBox([plot_v.show(), plot_sync.show()])" - ] - }, { "cell_type": "code", - "execution_count": 31, - "id": "e8b3468a-0c92-42b0-8158-6c3c4cb83fa1", + "execution_count": 13, + "id": "99d234c6-220a-4093-a6a0-6036d2e40000", "metadata": { "tags": [] }, "outputs": [], "source": [ - "# close plots\n", "plot_v.close()\n", - "plot_sync.close()" + "sc.close()" ] }, { @@ -895,7 +431,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 14, "id": "5139ad7c-2d72-402b-9239-27dcf5bd0c89", "metadata": { "tags": [] @@ -908,7 +444,7 @@ }, { "cell_type": "code", - "execution_count": 33, + "execution_count": 15, "id": "207632a8-9f7d-4931-bebc-b43a49e63b9e", "metadata": { "tags": [] @@ -917,7 +453,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "3fdc5bdd21194da0808e4742f7895609", + "model_id": "2b0b5a73f52744eaa29a75685d10ed7c", "version_major": 2, "version_minor": 0 }, @@ -931,7 +467,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "540ce3502bc5435f9a067fa2a3dd7a43", + "model_id": "0edf24e9f6764619877928989b16e4ce", "version_major": 2, "version_minor": 0 }, @@ -939,7 +475,7 @@ "VBox(children=(VBox(children=(JupyterWgpuCanvas(), HBox(children=(Button(icon='expand-arrows-alt', layout=Layo…" ] }, - "execution_count": 33, + "execution_count": 15, "metadata": {}, "output_type": "execute_result" } @@ -976,14 +512,15 @@ }, { "cell_type": "code", - "execution_count": 34, + "execution_count": 16, "id": "7f32b582-b01d-41ab-bcfb-88ddcf1fd3be", "metadata": { "tags": [] }, "outputs": [], "source": [ - "plot_movie.close()" + "plot_movie.close()\n", + "slider.close()" ] }, { @@ -991,23 +528,29 @@ "id": "f672dcd8-f55b-4476-b72a-d7a292fee32e", "metadata": {}, "source": [ - "## Single image sequence `ImageWidget` using zebrafish data" + "## `ImageWidget` using zebrafish whole-brain data\n", + "\n", + "## Supports numpy-like array objects and large array formats including: memmaps, zarr, hdf5, etc. via lazy loading\n", + "\n", + "#### Visualization is limited by file-formats and file system access performance: it will work with files of arbitrary size!" ] }, { "cell_type": "code", - "execution_count": 42, + "execution_count": 17, "id": "d64091d5-4071-49b0-b852-348657e9abbd", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "# load in zebrafish data\n", - "movie = tifffile.memmap('/home/clewis7/Downloads/zfish_proj/images/CNMF: H2BGc6s_FB_3Rep_161121_2-_--_-f0887281-5b83-4a22-8fd2-6480bafd8865.tiff')" + "movie = tifffile.memmap('./fpl-scipy2023-data/neural_data/zfish_vol.tiff')" ] }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 18, "id": "0377bd4e-ba0a-48e9-a647-a8ee7d770afa", "metadata": { "tags": [] @@ -1019,7 +562,7 @@ "(1309, 30, 512, 512)" ] }, - "execution_count": 43, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } @@ -1028,10 +571,26 @@ "movie.shape" ] }, + { + "cell_type": "markdown", + "id": "95063299-e5e2-4298-99d4-05fe827258f6", + "metadata": {}, + "source": [ + "This is `[t, z, x, y]`" + ] + }, + { + "cell_type": "markdown", + "id": "a1992f2b-abb5-42c8-990b-78d3a1698811", + "metadata": {}, + "source": [ + "## View each plane in a separate subplot" + ] + }, { "cell_type": "code", - "execution_count": 44, - "id": "7a76119e-2035-48ba-9a88-a8a6f64a3365", + "execution_count": 19, + "id": "770f8242-e8ff-40fc-86c3-9d44abca85d6", "metadata": { "tags": [] }, @@ -1039,7 +598,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "eac6cbc632384e3192274d301edd04b3", + "model_id": "92df5a7b047e44e18374c9f85ec2ac14", "version_major": 2, "version_minor": 0 }, @@ -1052,28 +611,34 @@ } ], "source": [ + "# get a list of txy arrays for each plane\n", + "planes = [movie[:, i] for i in range(movie.shape[1])]\n", + "\n", "iw = fpl.ImageWidget(\n", - " data=movie,\n", - " vmin_vmax_sliders=True,\n", - " dims_order=\"tzxy\",\n", - " slider_dims=[\"t\", \"z\"],\n", + " data=planes,\n", + " vmin_vmax_sliders=False,\n", + " grid_plot_kwargs={\"size\": (600, 500), \"controllers\": \"sync\"},\n", " cmap=\"gnuplot2\"\n", - ")" + ")\n", + "\n", + "\n", + "sc = Sidecar(title=\"zfish planes\")\n", + "\n", + "with sc:\n", + " display(iw.show())" ] }, { "cell_type": "code", - "execution_count": 45, - "id": "6cac96f4-a8e9-481a-8014-2d2570c04db3", + "execution_count": 20, + "id": "4329ac46-7820-4a4b-8b03-dbbc1d7b545e", "metadata": { "tags": [] }, "outputs": [], "source": [ - "sc = Sidecar(title=\"zebra fish\")\n", - "\n", - "with sc:\n", - " display(iw.show())" + "for subplot in iw.gridplot:\n", + " subplot[\"image_widget_managed\"].cmap.reset_vmin_vmax()" ] }, { @@ -1088,7 +653,7 @@ }, { "cell_type": "code", - "execution_count": 46, + "execution_count": 21, "id": "d29c857c-78f8-4bc5-a00f-83d9985eaeb7", "metadata": { "tags": [] @@ -1096,52 +661,27 @@ "outputs": [], "source": [ "# must be in the form of {dim: (func, window_size)}\n", - "iw.window_funcs = {\"t\": (np.mean, 13)}" - ] - }, - { - "cell_type": "code", - "execution_count": 47, - "id": "31542e8e-9549-4878-bbea-25136fd1b3ad", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# change the winow size\n", - "iw.window_funcs[\"t\"].window_size = 23" - ] - }, - { - "cell_type": "code", - "execution_count": 48, - "id": "2433e117-8b99-4ca4-89fb-b6283c007620", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "# change the function\n", - "iw.window_funcs[\"t\"].func = np.max" + "iw.window_funcs = {\"t\": (np.mean, 3)}" ] }, { "cell_type": "code", - "execution_count": 49, - "id": "58695549-089f-4af3-abab-289ad47c0681", + "execution_count": 22, + "id": "48951d83-c273-451d-84ce-be14cd7fc1c3", "metadata": { "tags": [] }, "outputs": [], "source": [ - "# or set it again\n", - "iw.window_funcs = {\"t\": (np.min, 11)}" + "for subplot in iw.gridplot:\n", + " subplot[\"image_widget_managed\"].cmap.vmin = -0.12\n", + " subplot[\"image_widget_managed\"].cmap.vmax = 2.3" ] }, { "cell_type": "code", - "execution_count": 50, - "id": "86261ca9-1cd8-43ee-ad72-7a05b3489510", + "execution_count": 23, + "id": "e49e7b96-c1a7-4c1f-956a-1bab482b0ce0", "metadata": { "tags": [] }, @@ -1154,15 +694,7 @@ { "cell_type": "code", "execution_count": null, - "id": "03837ed8-05b1-4518-85b3-8fdf7263b52e", - "metadata": {}, - "outputs": [], - "source": [] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "26b9725e-93c0-45cf-b7af-8d3efe58dabf", + "id": "ee57b4be-7849-43c1-bc80-bfd9dc684acb", "metadata": {}, "outputs": [], "source": [] diff --git a/interactivity.ipynb b/interactivity.ipynb index 4028929..93576f8 100644 --- a/interactivity.ipynb +++ b/interactivity.ipynb @@ -35,7 +35,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "6193b269-fbac-46de-9404-e9fc4097c068", "metadata": { "tags": [] @@ -43,82 +43,13 @@ "outputs": [], "source": [ "# reconstructed movie of CaImAn Sue demo movie after running motion correction and CNMF algorithms\n", - "movie = np.load('/home/clewis7/Desktop/scipy_demos/rcm.npy')\n", + "movie = np.load('./fpl-scipy2023-data/neural_data/rcm.npy')\n", "\n", "# temporal traces of identified neurons\n", - "temporal = np.load('/home/clewis7/Desktop/scipy_demos/temporal.npy')\n", + "temporal = np.load('./fpl-scipy2023-data/neural_data/temporal.npy')\n", "\n", "# contours of identified neurons\n", - "contours = np.load('/home/clewis7/Desktop/scipy_demos/contours.npy', allow_pickle=True)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "6017327f-b197-4d1a-a6d0-36d9712957b1", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(3000, 170, 170)" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "movie.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "f0df4095-d3cb-42b0-8330-a54681ed4b7f", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(155, 3000)" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "temporal.shape" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "0d236133-7a66-42a3-b493-c999368e9fac", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "text/plain": [ - "(155,)" - ] - }, - "execution_count": 7, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "contours.shape" + "contours = np.load(\"./fpl-scipy2023-data/neural_data/contours.npy\", allow_pickle=True)" ] }, { @@ -131,7 +62,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": 3, "id": "4315315a-32cc-4bae-939b-d2bfd300aefa", "metadata": { "tags": [] @@ -172,7 +103,7 @@ }, { "cell_type": "code", - "execution_count": 43, + "execution_count": 4, "id": "93e26775-3d24-4eb4-8831-fae5cb5fd71c", "metadata": { "tags": [] @@ -181,7 +112,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "e46e5e356d3b4023a03315b2d7b66458", + "model_id": "7a304a953bff41fd8716352cb1b733d9", "version_major": 2, "version_minor": 0 }, @@ -192,10 +123,18 @@ "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" + ] + }, { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "467e387e8eba41ccb072b6d4be597909", + "model_id": "fd443f94b80340bc94b30561abb3ce0b", "version_major": 2, "version_minor": 0 }, @@ -209,19 +148,52 @@ ], "source": [ "# for the image data and contours\n", - "cnmf_iw = fpl.ImageWidget(movie, vmin_vmax_sliders=True, cmap=\"gnuplot2\")\n", - "\n", - "# add contours to the plot within the widget\n", - "contours_graphic = cnmf_iw.gridplot[0,0].add_line_collection(contours, colors=\"cyan\", name=\"contours\")\n", + "cnmf_iw = fpl.ImageWidget(\n", + " movie, \n", + " vmin_vmax_sliders=True, \n", + " grid_plot_kwargs={\"size\": (600, 400)},\n", + " cmap=\"gnuplot2\",\n", + ")\n", "\n", "# temporal plot\n", "plot_temporal = fpl.Plot(size=(600,100))\n", + "plot_temporal.add_line(temporal[0], colors=\"magenta\")\n", "\n", "# stack the plots and show them in sidecar\n", "sc = Sidecar(title=\"interactivity\")\n", "\n", "with sc:\n", - " display(VBox([cnmf_iw.show(), plot_temporal.show()]))" + " display(VBox([cnmf_iw.show(), plot_temporal.show(maintain_aspect=False)]))" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "f99342b0-27c0-4b1a-9091-0ec3cde9632f", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# add contours\n", + "contours_graphic = cnmf_iw.gridplot[0,0].add_line_collection(\n", + " contours, \n", + " colors=\"red\", \n", + " thickness=2, \n", + " name=\"contours\"\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "99a3d096-3b72-4c20-98c8-a5fb27ab8fe3", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "cnmf_iw.vmin_vmax_sliders[0].value = (-5, 15)" ] }, { @@ -234,7 +206,7 @@ }, { "cell_type": "code", - "execution_count": 44, + "execution_count": 7, "id": "0a4968fc-1926-4eb7-85a5-2bedf1581b7f", "metadata": { "tags": [] @@ -248,7 +220,7 @@ " \"click\",\n", " target=contours_graphic,\n", " feature=\"colors\", \n", - " new_data=\"w\", \n", + " new_data=\"cyan\", \n", " callback=euclidean\n", ")\n", "\n", @@ -257,6 +229,7 @@ " ix = ev.pick_info[\"selected_index\"]\n", " cnmf_iw.sliders[\"t\"].value = ix\n", "\n", + "\n", "# callback function to display correct temporal trace\n", "def generate_temporal(ev):\n", " # clear the plot \n", @@ -278,12 +251,12 @@ "contours_graphic[:].colors.add_event_handler(generate_temporal)\n", "\n", "# thickness of contour\n", - "contours_graphic.link(\"colors\", target=contours_graphic, feature=\"thickness\", new_data=3)" + "contours_graphic.link(\"colors\", target=contours_graphic, feature=\"thickness\", new_data=5)" ] }, { "cell_type": "code", - "execution_count": 45, + "execution_count": 8, "id": "f7b78a88-7aa5-4de7-a796-4a99514631c8", "metadata": { "tags": [] @@ -298,7 +271,7 @@ { "cell_type": "code", "execution_count": null, - "id": "8f2a0763-5868-4b67-9c3e-3fa7a7531d8a", + "id": "8baf21c1-036e-4882-8a1c-4c5ab2e5f605", "metadata": {}, "outputs": [], "source": [] diff --git a/lines.ipynb b/lines.ipynb index 50dbe4b..ca8e926 100644 --- a/lines.ipynb +++ b/lines.ipynb @@ -20,9 +20,11 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "177e0cc3-870c-4570-bf59-af3528ba94bf", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [], "source": [ "import fastplotlib as fpl\n", @@ -44,7 +46,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 3, "id": "9bf0017a-e319-4f4c-8bf8-91cff3755099", "metadata": { "tags": [] @@ -77,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 4, "id": "970a5baf-a27d-4030-9659-247fccb4eff6", "metadata": { "tags": [] @@ -86,7 +88,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "d5c72fb7208b4cd7bc2b00e754e65a70", + "model_id": "2d4371cb3aad4a3591c998144d28127f", "version_major": 2, "version_minor": 0 }, @@ -108,7 +110,7 @@ ], "source": [ "# Create a plot instance\n", - "plot_l = fpl.Plot()\n", + "plot_l = fpl.Plot(size=(600, 300))\n", "\n", "# plot sine wave, use a single color\n", "sine_graphic = plot_l.add_line(data=sine, thickness=5, colors=\"magenta\")\n", @@ -128,75 +130,35 @@ }, { "cell_type": "markdown", - "id": "6f2fb6e9-a4c8-473f-969f-9e0e1fe26184", - "metadata": {}, - "source": [ - "### \"stretching\" the camera, useful for large timeseries data\n", - "\n", - "Set `maintain_aspect = False` on a camera, and then use the right mouse button and move the mouse to stretch and squeeze the view!\n", - "\n", - "You can also click the `1:1` button to toggle this." - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "ccda4b46-147e-41d5-a9b2-4e36c307ca16", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "plot_l.camera.maintain_aspect = False" - ] - }, - { - "cell_type": "markdown", - "id": "543bca00-ac5b-4ce1-89aa-2b4a9a4071c8", + "id": "2200da08-205e-4ae0-a3e7-9ec4049606c9", "metadata": {}, "source": [ - "### reset the plot area" + "### Graphic features support slicing! :D" ] }, { "cell_type": "code", "execution_count": 5, - "id": "3d330edd-8455-4897-b1a5-9d1a344f108b", + "id": "633aa156-9443-41c4-beeb-b0db4e9a1099", "metadata": { "tags": [] }, "outputs": [], "source": [ - "plot_l.auto_scale(maintain_aspect=True)" - ] - }, - { - "cell_type": "markdown", - "id": "2200da08-205e-4ae0-a3e7-9ec4049606c9", - "metadata": {}, - "source": [ - "### Graphic features support slicing! :D" + "# indexing of colors\n", + "cosine_graphic.colors[:40] = \"magenta\"" ] }, { "cell_type": "code", "execution_count": 6, - "id": "94af10a4-5ed5-419a-b3d9-c409f7de5a23", + "id": "df876d25-42e1-4a19-ba00-25cbaf87b706", "metadata": { "tags": [] }, "outputs": [], "source": [ - "# indexing of colors\n", - "cosine_graphic.colors[:15] = \"magenta\"\n", - "cosine_graphic.colors[90:] = \"red\"\n", - "cosine_graphic.colors[60] = \"w\"\n", - "\n", - "# indexing to assign colormaps to entire lines or segments\n", - "sinc_graphic.cmap[10:50] = \"gray\"\n", - "sine_graphic.cmap = \"seismic\"\n", - "\n", - "# more complex indexing, set the blue value directly from an array\n", + "# more complex indexing, set the red value directly from an array\n", "cosine_graphic.colors[65:90, 0] = np.linspace(0, 1, 90-65)" ] }, @@ -236,9 +198,9 @@ "name": "stdout", "output_type": "stream", "text": [ - "FeatureEvent @ 0x7f7db3de2ad0\n", + "FeatureEvent @ 0x7efe4bfa4e50\n", "type: colors\n", - "pick_info: {'index': range(15, 50, 3), 'collection-index': None, 'world_object': , 'new_data': array([[0., 1., 1., 1.],\n", + "pick_info: {'index': range(15, 50, 3), 'collection-index': None, 'world_object': , 'new_data': array([[0., 1., 1., 1.],\n", " [0., 1., 1., 1.],\n", " [0., 1., 1., 1.],\n", " [0., 1., 1., 1.],\n", @@ -256,136 +218,13 @@ ], "source": [ "# more complex indexing of colors\n", - "# from point 15 - 30, set every 3rd point as \"cyan\"\n", + "# from point 15 - 50, set every 3rd point as \"cyan\"\n", "cosine_graphic.colors[15:50:3] = \"cyan\"" ] }, - { - "cell_type": "markdown", - "id": "d67fcec7-ca2c-4403-9b79-23fefeb8c0d1", - "metadata": {}, - "source": [ - "### Graphic data is itself also indexable" - ] - }, { "cell_type": "code", "execution_count": 9, - "id": "fa7ecf35-bd91-4209-8d3f-d554977b5ff1", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/_features/_base.py:31: UserWarning: converting int64 array to int32\n", - " warn(f\"converting {array.dtype} array to int32\")\n" - ] - } - ], - "source": [ - "cosine_graphic.data[10:50:5, :2] = sine[10:50:5]\n", - "cosine_graphic.data[90:, 1] = 7\n", - "cosine_graphic.data[0] = np.array([[-10, 0, 0]])" - ] - }, - { - "cell_type": "markdown", - "id": "bfbc8c70-1173-4143-8638-78716b9a86c7", - "metadata": {}, - "source": [ - "### Toggle the presence of a graphic within the scene" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "8617b111-9d09-4bee-a875-79771c2e38d3", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "sinc_graphic.present = False" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "d2486714-9cba-4ab9-bf17-9c0c52587c1c", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "sinc_graphic.present = True" - ] - }, - { - "cell_type": "markdown", - "id": "c398cdad-acae-4fb8-a254-44debaaacdef", - "metadata": {}, - "source": [ - "### You can create callbacks to this too, for example to re-scale the plot w.r.t. graphics that are present in the scene\n" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "d03b1b6e-c7d3-40d3-97ab-a855c821e277", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "sinc_graphic.present.add_event_handler(plot_l.auto_scale)\n", - "\n", - "sinc_graphic.present = False" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "36a4e681-6a21-4dc4-a3b5-9f454095bce7", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "sinc_graphic.present = True" - ] - }, - { - "cell_type": "markdown", - "id": "98cf3630-9eff-4d77-a798-ad53bd6e52aa", - "metadata": {}, - "source": [ - "### You can set the z-positions of graphics to have them appear under other graphics" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "29db2967-b260-4bb9-a503-d9406294e31e", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "img = np.random.rand(20, 100)\n", - "\n", - "plot_l.add_image(img, name=\"image\", cmap=\"gray\")\n", - "\n", - "# z axix position -1 so it is below all the lines\n", - "plot_l[\"image\"].position_z = -1\n", - "plot_l[\"image\"].position_x = -50" - ] - }, - { - "cell_type": "code", - "execution_count": 15, "id": "44d55357-4bd9-47ba-9f39-001c12e83eb5", "metadata": { "tags": [] @@ -401,12 +240,12 @@ "id": "d9db0e4b-c747-4e0f-b2e8-1bee48d03f02", "metadata": {}, "source": [ - "### Can also have fancy indexing of colormaps" + "### colormaps" ] }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 10, "id": "2c6eda00-3883-48d2-af14-31c5a4b6bcd1", "metadata": { "tags": [] @@ -415,7 +254,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "04cfb9f0dcf2450ea46ddda837d50587", + "model_id": "0c3f05008a0249869be6eea987829958", "version_major": 2, "version_minor": 0 }, @@ -428,7 +267,7 @@ } ], "source": [ - "plot = fpl.Plot()\n", + "plot = fpl.Plot(size=(600, 300))\n", "\n", "plot.add_line(sine, thickness=10, name=\"sine\")\n", "\n", @@ -440,68 +279,104 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 11, "id": "e3d190b5-a2c7-4ec4-918d-b934270bf1c4", "metadata": { "tags": [] }, "outputs": [], "source": [ + "# set cmap \n", "plot[\"sine\"].cmap = \"jet\"" ] }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 12, "id": "ff9eff43-16fb-477e-b201-da728f18d7e1", "metadata": { "tags": [] }, "outputs": [], "source": [ + "# set cmap values equal sine wave y-values \n", "plot[\"sine\"].cmap.values = sine[:, 1]" ] }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 13, "id": "74a592de-76be-4726-ae29-4a0b93ae9456", "metadata": { "tags": [] }, "outputs": [], "source": [ + "# set cmap values equal to cosine wave y-values\n", "plot[\"sine\"].cmap.values = cosine[:, 1]" ] }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 14, "id": "10be65e2-5dce-49d1-8bb7-c12bec835a2b", "metadata": { "tags": [] }, "outputs": [], "source": [ + "# change the cmap, cmap values remain the same\n", "plot[\"sine\"].cmap = \"viridis\"" ] }, + { + "cell_type": "markdown", + "id": "66de4d43-11e6-4662-be23-57b8e400ab75", + "metadata": { + "tags": [] + }, + "source": [ + "**Qualitative data**" + ] + }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 15, "id": "6865697d-159e-47c1-a372-9fc350e02d31", "metadata": { "tags": [] }, "outputs": [], "source": [ + "# qualitatively set the cmap values\n", "cmap_values = [0] * 25 + [1] * 5 + [2] * 50 + [3] * 20\n", "plot[\"sine\"].cmap.values = cmap_values" ] }, + { + "cell_type": "markdown", + "id": "b2924933-e217-4f57-8617-52032d807489", + "metadata": {}, + "source": [ + "**Qualitative cmap**" + ] + }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 16, + "id": "53c12b6b-485e-4623-a111-46127206223b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# now change the cmap to new cmap with those same cmap values\n", + "plot[\"sine\"].cmap = \"tab10\"" + ] + }, + { + "cell_type": "code", + "execution_count": 17, "id": "bf9fbc9a-a887-45c9-921b-54d037aa1b57", "metadata": { "tags": [] @@ -525,12 +400,12 @@ "id": "817355c4-0150-41de-a1d9-4c1f3680a7bc", "metadata": {}, "source": [ - "Generate some data" + "**Generate some data, 4 x 4 circles**" ] }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 18, "id": "5d92ecd2-2177-4cd9-8643-c8691ee1e988", "metadata": { "tags": [] @@ -561,12 +436,14 @@ "id": "fbed2426-5d27-4f8b-91bd-5a0fe514a0ed", "metadata": {}, "source": [ - "### set cmap values as LUT" + "### Qualitative cmap\n", + "\n", + "Useful for clustering or classification" ] }, { "cell_type": "code", - "execution_count": 15, + "execution_count": 19, "id": "c9f83313-60ef-4b58-baaf-bcbf3564eb31", "metadata": { "tags": [] @@ -575,7 +452,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "f42e9011e33a44dab509830d294e0495", + "model_id": "2ec123f30def4c7f9fa7a196fde3f490", "version_major": 2, "version_minor": 0 }, @@ -588,17 +465,22 @@ } ], "source": [ - "plot = fpl.Plot()\n", + "plot = fpl.Plot(size=(600, 600))\n", "\n", - "# highest values, lowest values, mid-high values, mid values\n", - "cmap_values = cmap_values = [\n", + "# things like class labels, cluster labels, etc.\n", + "cmap_values = [\n", " 0, 1, 1, 2,\n", " 0, 0, 1, 1,\n", " 2, 2, 3, 3,\n", " 1, 1, 1, 5\n", "]\n", "\n", - "plot.add_line_collection(circles, cmap=\"tab10\", cmap_values=cmap_values, thickness=5)\n", + "plot.add_line_collection(\n", + " circles, \n", + " cmap=\"tab10\", \n", + " cmap_values=cmap_values, \n", + " thickness=15\n", + ")\n", "\n", "sc = Sidecar(title=\"lines collection\")\n", "\n", @@ -608,7 +490,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 20, "id": "981bbd25-ca42-492d-a722-185ffc877462", "metadata": { "tags": [] @@ -621,16 +503,32 @@ }, { "cell_type": "markdown", - "id": "c55c8340-7e85-46cb-aedc-8112138c1b75", + "id": "54374981-9a79-4887-a297-8a5012ebb4b7", "metadata": {}, "source": [ - "### can also set colors" + "## neuro data example" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "6901f4b7-d1f8-4747-b8b0-de078ae4c825", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# load in movie \n", + "movie = np.load('./fpl-scipy2023-data/neural_data/rcm.npy')\n", + "\n", + "# load in identified nuerons\n", + "contours = np.load('./fpl-scipy2023-data/neural_data/contours.npy', allow_pickle=True)" ] }, { "cell_type": "code", "execution_count": 22, - "id": "ca7e277f-8630-4691-96e9-44da495ca57e", + "id": "162a1602-2201-45be-bcc2-6fd7550cf0e4", "metadata": { "tags": [] }, @@ -638,7 +536,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ccbde8ee3c504de9aced0c3021124121", + "model_id": "03ad937b478444248aac85fa2ae4c62d", "version_major": 2, "version_minor": 0 }, @@ -651,126 +549,282 @@ } ], "source": [ - "plot = fpl.Plot()\n", + "# for the image data and contours\n", + "cnmf_iw = fpl.ImageWidget(\n", + " movie, \n", + " vmin_vmax_sliders=True, \n", + " grid_plot_kwargs={\"size\": (600, 500)},\n", + " cmap=\"gray\"\n", + ")\n", "\n", - "colors = [\"green\"] * 4 + [\"red\"] * 4 + [\"yellow\"] * 4 + [\"w\"] * 4\n", - "\n", - "plot.add_line_collection(circles, colors=colors, thickness=5)\n", - "\n", - "sc = Sidecar(title=\"lines collection\")\n", + "# stack the plots and show them in sidecar\n", + "sc = Sidecar(title=\"good/bad comps\")\n", "\n", "with sc:\n", - " display(plot.show())" + " display(cnmf_iw.show())" ] }, { "cell_type": "code", "execution_count": 23, - "id": "9792dea6-a5cd-4b11-abd3-0cf4c2b8661b", + "id": "2e34929f-55dd-43d5-8736-8253ad11d96e", "metadata": { "tags": [] }, "outputs": [], "source": [ - "plot.close()\n", - "sc.close()" + "cnmf_iw.vmin_vmax_sliders[0].value = (-5, 15)" ] }, { "cell_type": "markdown", - "id": "a1a34cfd-3746-489e-aeff-7373d538ab8b", + "id": "3189906a-d7e7-40b5-8bb5-a438ca7ecb0a", "metadata": {}, "source": [ - "## Line Stack" + "### Colors based on classifier output" ] }, { - "cell_type": "markdown", - "id": "e4ea3e6c-39f7-4796-b784-9ca175318356", - "metadata": {}, + "cell_type": "code", + "execution_count": 24, + "id": "6a9005e6-2c6b-4561-8890-0a576273e609", + "metadata": { + "tags": [] + }, + "outputs": [], "source": [ - "Generate some data" + "# indices of accepted_ixs components\n", + "accepted_ixs = np.load('./fpl-scipy2023-data/neural_data/good_ixs.npy')" ] }, { "cell_type": "code", - "execution_count": 24, - "id": "d7d5e7ca-f32d-4d3b-8ca3-3cfa72852614", + "execution_count": 25, + "id": "63c95616-80d2-42aa-948b-2c5ea48c349d", "metadata": { "tags": [] }, - "outputs": [], + "outputs": [ + { + "data": { + "text/plain": [ + "array([1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 1, 1, 0, 0, 0, 1, 1,\n", + " 0, 0, 0, 0, 1, 1, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 1, 0, 1, 1, 1,\n", + " 1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1, 0, 1, 1, 1, 1, 0, 0, 1, 1, 0,\n", + " 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 1,\n", + " 1, 0, 0, 0, 1, 0, 0, 1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1,\n", + " 0, 0, 1, 0, 1, 1, 1, 0, 0, 0, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0,\n", + " 1, 1, 0, 0, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 1, 0, 1, 0, 1, 0, 1, 1,\n", + " 1])" + ] + }, + "execution_count": 25, + "metadata": {}, + "output_type": "execute_result" + } + ], "source": [ - "xs = np.linspace(0, 100, 1000)\n", - "# sine wave\n", - "ys = np.sin(xs) * 20\n", + "classifier = np.zeros(len(contours), dtype=int)\n", + "classifier[accepted_ixs] = 1\n", "\n", - "# make 25 lines\n", - "data = np.vstack([ys] * 25)" + "classifier" ] }, { "cell_type": "code", "execution_count": 26, - "id": "a70bf880-5497-476d-ba43-4c1687b49abc", + "id": "83cb0f1b-7ade-4237-afb3-ad4516f893cd", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "# add contours to the plot within the widget\n", + "contours_graphic = cnmf_iw.gridplot[0,0].add_line_collection(\n", + " contours, \n", + " cmap=\"Set1\",\n", + " cmap_values=classifier,\n", + " thickness=3, \n", + " name=\"contours\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "9ee60d93-8be8-4cf3-8b4c-75e521379d4e", + "metadata": {}, + "source": [ + "## Red: class 0 - rejected\n", + "\n", + "## Blue: class 1 - accepted" + ] + }, + { + "cell_type": "markdown", + "id": "7a254ed4-370c-4b5a-8c55-8b9603af7816", + "metadata": {}, + "source": [ + "### cmaps for quantitative metrics " + ] + }, + { + "cell_type": "markdown", + "id": "90e7e79a-2879-454a-9293-e8abda55808e", + "metadata": {}, + "source": [ + "#### yet another measure, signal-to-noise measure" + ] + }, + { + "cell_type": "code", + "execution_count": 27, + "id": "f7490d2f-f667-445f-aeb8-d8d83de77372", "metadata": { "tags": [] }, "outputs": [ { "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "fe4fb92bdeab4eb3bc8744efea40b9b1", - "version_major": 2, - "version_minor": 0 - }, "text/plain": [ - "RFBOutputContext()" + "array([ 1.30693739, 0.98832133, 0.61094959, 0.60680478, 0.78905172,\n", + " 0.75528032, 0.07143519, 0.20501458, 0.82659848, 0.72253894,\n", + " 0.50721547, 0.06808205, 0.09170267, 0.16848598, 1.21936687,\n", + " 0.60382365, 0.46201108, 0.62335868, 0.31698763, 0.6426985 ,\n", + " 0.60684258, 0.5431985 , 0.59940984, 0.64724762, 0.29722249,\n", + " 0.29662902, 0.61015591, 0.31969486, 1.19550207, 0.5710063 ,\n", + " 0.46383772, 1.02528406, 0.72630303, 0.40776261, 0.19654701,\n", + " 0.52004469, 0.09369996, -0.14752256, 1.05310796, 0.37688607,\n", + " 0.41622687, 1.17317543, 0.73611689, 0.42182707, 0.3150909 ,\n", + " 0.21921744, 0.3789719 , -0.06830937, 0.35099327, -0.08058595,\n", + " 0.71089304, 0.44862497, 0.34431778, 0.03256675, 0.28882971,\n", + " 0.61887717, 0.76572043, 0.51032459, 0.63065994, 0.95790937,\n", + " 0.63282506, 0.65043055, 0.44677766, 0.31964022, 0.41866778,\n", + " 0.18621914, 0.90540626, 0.7797958 , 0.3603792 , 0.51412954,\n", + " 0.32145975, 1.16006419, 0.05515471, 0.46310168, 0.66458612,\n", + " 0.58251201, 0.33845468, 0.46522393, 0.01540801, -0.14980613,\n", + " 0.14016509, 0.39933608, 0.16530968, 0.15592701, 0.20202153,\n", + " -0.41106995, 0.1119999 , 0.42393534, 1.22942341, 0.13633389,\n", + " 0.22779662, 0.24890452, 0.212523 , 0.49994003, 0.19132364,\n", + " 0.72399517, 0.47901872, 0.01965678, 0.18721191, 0.54839699,\n", + " 0.10548312, 0.36355986, -0.30731671, 0.57231072, 0.28601665,\n", + " -0.3082514 , 0.23907098, 1.03830112, 0.14206076, 0.55663581,\n", + " -0.21548934, 0.38826583, 0.31809138, 0.14230872, 0.87761188,\n", + " 0.83902665, 0.58369984, 0.419747 , 0.47968704, 0.31675268,\n", + " 0.84700962, 0.21892391, 0.56579811, -0.28278322, 0.61361115,\n", + " 1.40618154, 1.23015299, 0.64253497, 1.120344 , 1.0578779 ,\n", + " 1.09035117, 0.52459236, 0.89245759, 0.574191 , 0.52985482,\n", + " 0.80768994, 0.56405192, 0.35819001, 0.69566203, 0.56113173,\n", + " 0.97317729, 1.10191489, 0.88968622, 0.66573856, 0.79040525,\n", + " 0.58058598, 0.71475437, 0.56642353, 0.74560174, 0.72704435,\n", + " 0.53325174, 0.82447437, 0.59551337, 0.01806211, 1.07601128])" ] }, + "execution_count": 27, "metadata": {}, - "output_type": "display_data" + "output_type": "execute_result" } ], "source": [ - "plot = fpl.Plot()\n", - "\n", - "# line stack takes all the same arguments as line collection and behaves similarly\n", - "plot.add_line_stack(data, cmap=\"jet\")\n", - "\n", - "sc = Sidecar(title=\"line stack\")\n", + "# load in SNR comps\n", + "snr_comps = np.load('./fpl-scipy2023-data/neural_data/SNR_comps.npy')\n", + "np.log10(snr_comps)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "id": "d5b21e2e-c9af-4459-86ac-2b61996ee55b", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "contours_graphic.cmap = \"spring\"\n", + "contours_graphic.cmap_values = np.log10(snr_comps)" + ] + }, + { + "cell_type": "markdown", + "id": "eb48230f-258f-48b1-b131-90f03ce8ca75", + "metadata": {}, + "source": [ + "## low signal to noise: purple\n", "\n", - "with sc:\n", - " display(plot.show(maintain_aspect=False))" + "## high signal to noise: yellow" ] }, { "cell_type": "code", - "execution_count": 27, - "id": "7a3b8014-a89e-4138-98ac-ed3cbf1f0d51", + "execution_count": 30, + "id": "32ed59ef-d956-4e83-b12f-8c496e1579e4", "metadata": { "tags": [] }, "outputs": [], "source": [ - "plot.close()\n", + "cnmf_iw.gridplot.close()\n", "sc.close()" ] }, { "cell_type": "markdown", - "id": "be5725e1-5f43-4c81-a35a-d86b658ed709", + "id": "a1a34cfd-3746-489e-aeff-7373d538ab8b", + "metadata": {}, + "source": [ + "## Line Stack - useful for time series data" + ] + }, + { + "cell_type": "markdown", + "id": "e4ea3e6c-39f7-4796-b784-9ca175318356", "metadata": {}, "source": [ - "## Linear Selector\n", + "### 3 Generate some sine data, 3 million points" + ] + }, + { + "cell_type": "code", + "execution_count": 50, + "id": "d7d5e7ca-f32d-4d3b-8ca3-3cfa72852614", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "xs = np.linspace(0, 2_000, 3_000)\n", + "# sine wave\n", + "ys = np.sin(xs) * 20\n", "\n", - "Creates a horizontal or vertical line slider. Can be synced to an `ipywidget IntSlider`. Like other `Graphic` types, it can be linked to events. " + "data = np.vstack([ys] * 1_000)" ] }, { "cell_type": "code", "execution_count": 51, - "id": "f5ca2643-f1c2-4cf4-ab05-daac52261830", + "id": "ed90fe0e-38ee-4ffd-97f7-3b62ef4dc6db", + "metadata": { + "tags": [] + }, + "outputs": [ + { + "data": { + "text/plain": [ + "(1000, 3000)" + ] + }, + "execution_count": 51, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "data.shape" + ] + }, + { + "cell_type": "code", + "execution_count": 52, + "id": "a70bf880-5497-476d-ba43-4c1687b49abc", "metadata": { "tags": [] }, @@ -778,7 +832,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "0b1b4be802384e489eaf4c7a37ef0a86", + "model_id": "af520ea5065b442c9e193784e656eb62", "version_major": 2, "version_minor": 0 }, @@ -791,64 +845,85 @@ } ], "source": [ - "plot = fpl.Plot()\n", - "\n", - "# data to plot\n", - "xs = np.linspace(0, 100, 1000)\n", - "sine = np.sin(xs) * 20\n", + "plot = fpl.Plot(size=(600, 500), canvas=\"jupyter\")\n", "\n", - "# make sine along x axis\n", - "sine_graphic = plot.add_line(np.column_stack([xs, sine]).astype(np.float32))\n", + "# line stack takes all the same arguments as line collection\n", + "# and behaves similarly\n", + "plot.add_line_stack(data, cmap=\"jet\", separation=15, name=\"many-lines\")\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", - "# add ability to synchronize selectors\n", - "ss = Synchronizer(selector, selector2, selector3)\n", - "\n", - "plot.auto_scale()\n", - "\n", - "# fastplotlib LineSelector can make an ipywidget slider and return it :D \n", - "ipywidget_slider = selector.make_ipywidget_slider()\n", - "\n", - "sc = Sidecar(title=\"linear selectors\")\n", + "sc = Sidecar(title=\"line stack\")\n", "\n", "with sc:\n", - " display(VBox([plot.show(), ipywidget_slider]))" + " display(plot.show(maintain_aspect=False))" ] }, { "cell_type": "markdown", - "id": "338d362b-521b-4abf-9148-cc0d8d2a55db", + "id": "ff71b41c-3dae-43ad-906b-db6e34a13b6b", "metadata": {}, "source": [ - "### can add an event handler to a selector" + "## Performant changes" ] }, { "cell_type": "code", - "execution_count": 52, - "id": "16ae2820-4808-4890-9ca1-3507fb0e0799", + "execution_count": 53, + "id": "ba65f98c-42db-4c7c-80a6-daf3bad54d78", "metadata": { "tags": [] }, "outputs": [], "source": [ - "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] = \"magenta\"\n", + "plot[\"many-lines\"].cmap = \"viridis\"" + ] + }, + { + "cell_type": "markdown", + "id": "8e39322e-909a-41c1-8b07-ef12e0f67213", + "metadata": {}, + "source": [ + "**more numpy-like indexing :D**\n", "\n", - "selector.selection.add_event_handler(set_color_at_index)" + "Set the cmap of the first 500 individual lines to `plasma`. \n", + "This sets the cmap _along the line_" ] }, { "cell_type": "code", - "execution_count": 53, - "id": "858477e5-4b7a-4e63-b711-72f01b3a4941", + "execution_count": 54, + "id": "963daef5-3701-49bf-832a-74c85efcdaed", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot[\"many-lines\"][:500].cmap = \"plasma\"" + ] + }, + { + "cell_type": "markdown", + "id": "394a267f-ff08-43df-844b-0a311e009dfd", + "metadata": {}, + "source": [ + "Every 5th datapoint to \"white\" or \"black\"" + ] + }, + { + "cell_type": "code", + "execution_count": 55, + "id": "f05b4023-f949-4043-b9ef-82d9728512e4", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "plot[\"many-lines\"][:500].colors[::5] = \"w\"" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "id": "7a3b8014-a89e-4138-98ac-ed3cbf1f0d51", "metadata": { "tags": [] }, @@ -871,7 +946,25 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 38, + "id": "68403bc0-262c-42b9-986c-8cad25152f39", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "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" + ] + }, + { + "cell_type": "code", + "execution_count": 39, "id": "87670f5c-ec83-4d71-87ec-1d13bb05dc47", "metadata": { "tags": [] @@ -880,7 +973,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "09099d4b37aa45bcbf8ff4208b914cdd", + "model_id": "77b6cedc1b2c45eb8b2f70e80a825af5", "version_major": 2, "version_minor": 0 }, @@ -890,43 +983,51 @@ }, "metadata": {}, "output_type": "display_data" - }, - { - "name": "stderr", - "output_type": "stream", - "text": [ - "/home/clewis7/repos/fastplotlib/fastplotlib/graphics/_features/_base.py:34: UserWarning: converting float64 array to float32\n", - " warn(f\"converting {array.dtype} array to float32\")\n" - ] } ], "source": [ "# data to plot\n", - "xs = np.linspace(0, 250, 500)\n", + "xs = np.linspace(0, 100, 1_000)\n", "sine = np.sin(xs) * 20\n", "cosine = np.cos(xs) * 20\n", "\n", - "plot = fpl.Plot()\n", + "plot = fpl.GridPlot((5, 1), size=(600, 600))\n", + "\n", + "# preallocated size for zoomed data\n", + "zoomed_prealloc = 1_000\n", + "zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.random.rand(zoomed_prealloc)])\n", "\n", "# sines and cosines\n", - "sines = [sine] * 5\n", - "cosines = [cosine] * 5\n", + "sines = [sine] * 2\n", + "cosines = [cosine] * 2\n", "\n", "# make line stack\n", - "line_stack = plot.add_line_stack(sines + cosines, separation=50)\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()\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", - "def update_color(ev):\n", - " selected_indices = stack_selector.get_selected_indices()\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(selected_indices)):\n", - " line_stack.graphics[i].colors = \"w\"\n", - " line_stack.graphics[i].colors[selected_indices[i]] = \"magenta\"\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", - "stack_selector.selection.add_event_handler(update_color)\n", "\n", + "selector.selection.add_event_handler(update_zoomed_subplots)\n", + "plot.show()\n", "sc = Sidecar(title=\"linear region selector\")\n", "\n", "with sc: \n", @@ -935,7 +1036,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 40, "id": "89b9ed5b-8fdb-4fa7-95f7-f955cec9ed74", "metadata": { "tags": [] @@ -956,14 +1057,28 @@ }, { "cell_type": "code", - "execution_count": 39, + "execution_count": 4, + "id": "d201138f-a44f-4f87-b456-33b0ad7f41ae", + "metadata": { + "tags": [] + }, + "outputs": [], + "source": [ + "temporal = np.load('./fpl-scipy2023-data/neural_data/temporal.npy')" + ] + }, + { + "cell_type": "code", + "execution_count": 11, "id": "a24b06d3-f9db-406a-a63d-0159007a09c6", - "metadata": {}, + "metadata": { + "tags": [] + }, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "b55410322ef945c78344b240a007c105", + "model_id": "5a8c28a77f5c407d8aea02964e8a6988", "version_major": 2, "version_minor": 0 }, @@ -975,65 +1090,91 @@ "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" - ] + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "547a8fc6813e4931a7441670b792a3b5", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" } ], "source": [ "# create plot\n", - "plot = fpl.GridPlot(shape=(1,2), names=[[\"heatmap\", \"traces\"]])\n", + "plot = fpl.GridPlot(shape=(1,2), names=[[\"heatmap\", \"timeseries\"]])\n", + "\n", + "def normalize(array):\n", + " out = np.zeros(array.shape, dtype=array.dtype)\n", + " for i in range(array.shape[0]):\n", + " a = array[i]\n", + " out[i] = ((a - np.min(a)) / (np.max(a - np.min(a))))\n", + " \n", + " return out\n", "\n", + "temporal_norm = normalize(temporal)\n", + " \n", "# add temporal traces as heatmap\n", - "heatmap = plot[0,0].add_heatmap(temporal)\n", + "heatmap = plot[0,0].add_heatmap(temporal_norm)\n", "\n", "# add temporal traces as line stack\n", "temporal_stack = plot[0,1].add_line_stack(temporal, colors=\"magenta\")\n", "\n", - "# add linear selector to heatmap\n", - "selector = heatmap.add_linear_region_selector()\n", + "for subplot in plot:\n", + " subplot.camera.maintain_aspect = False\n", + "\n", + "# heatmap support the same selectors as lines!\n", + "heatmap_ls = heatmap.add_linear_selector()\n", + "temporal_stack_ls = temporal_stack.add_linear_selector()\n", + "temporal_stack_ls.position_z = 10\n", + "\n", + "# the corresponding imaging data and some callbacks to update frames\n", + "cnmf_iw = fpl.ImageWidget(movie, vmin_vmax_sliders=True, cmap=\"gray\")\n", + "\n", + "# update the linear selectors with current frame index\n", + "def update_linear_selector(change):\n", + " ix = change[\"new\"]\n", + " \n", + " heatmap_ls.selection = ix\n", + " temporal_stack_ls.selection = ix\n", + " \n", + "def update_ipywidget(ev):\n", + " ix = ev.pick_info[\"selected_index\"]\n", + " # for line collection sends as list\n", + " if not isinstance(ix, int):\n", + " ix = ix[0]\n", + " cnmf_iw.sliders[\"t\"].value = ix\n", + " \n", + "cnmf_iw.sliders[\"t\"].observe(update_linear_selector, \"value\")\n", + "heatmap_ls.selection.add_event_handler(update_ipywidget)\n", + "temporal_stack_ls.selection.add_event_handler(update_ipywidget)\n", "\n", + " \n", "sc = Sidecar(title=\"heatmap interactive\")\n", "\n", "with sc:\n", - " display(plot.show())" - ] - }, - { - "cell_type": "markdown", - "id": "50c5c9bf-7881-4f19-bcdc-ab30cc96e7c7", - "metadata": {}, - "source": [ - "### plot interaction using event handlers" + " display(VBox([cnmf_iw.show(), plot.show()]))" ] }, { "cell_type": "code", - "execution_count": 40, - "id": "273a2d45-0423-470d-9192-8b14b3d08d3a", + "execution_count": 12, + "id": "c509263b-7120-4e0c-a2cd-f68b6525994c", "metadata": { "tags": [] }, "outputs": [], "source": [ - "# define event handler\n", - "def color_change(ev):\n", - " selected_indices = ev.pick_info[\"selected_indices\"]\n", - " \n", - " for i in range(len(temporal_stack.graphics)):\n", - " temporal_stack.graphics[i].colors = \"magenta\"\n", - " temporal_stack.graphics[i].colors[selected_indices] = \"white\"\n", - "\n", - "# add event handler\n", - "selector.selection.add_event_handler(color_change)" + "cnmf_iw.vmin_vmax_sliders[0].value = (-5, 15)" ] }, { "cell_type": "code", - "execution_count": 41, + "execution_count": 7, "id": "4447805e-c477-48b4-8dc1-8740c2a70232", "metadata": { "tags": [] @@ -1043,6 +1184,14 @@ "plot.close()\n", "sc.close()" ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0095e961-b72b-4e41-8502-696f03b2a6e4", + "metadata": {}, + "outputs": [], + "source": [] } ], "metadata": { diff --git a/scatterplot.ipynb b/scatterplot.ipynb deleted file mode 100644 index d6ea93a..0000000 --- a/scatterplot.ipynb +++ /dev/null @@ -1,247 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "cb36b8f4-62d9-45a1-9c0b-dff34c08472b", - "metadata": {}, - "source": [ - "## Scatterplot\n", - "\n", - "`GridPlot` layout of scatter plots with a mix of 2d an 3d cameras" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "c1f92c66-b30d-4c45-8033-addc1c3231eb", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "import fastplotlib as fpl\n", - "import numpy as np\n", - "from sidecar import Sidecar" - ] - }, - { - "cell_type": "markdown", - "id": "13604307-8fb9-4aae-beca-195b57a5cddb", - "metadata": {}, - "source": [ - "Generate data" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "743c1d94-e733-4e03-954f-d55125bf781b", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "n_points = 10_000\n", - "\n", - "dims = (n_points, 3)\n", - "\n", - "offset = 15\n", - "\n", - "normal = np.random.normal(size=dims, scale=5)\n", - "cloud = np.vstack(\n", - " [\n", - " normal - offset,\n", - " normal,\n", - " normal + offset,\n", - " ]\n", - ")\n" - ] - }, - { - "cell_type": "markdown", - "id": "f2df0522-4b3e-4d93-a461-e86e6187f698", - "metadata": {}, - "source": [ - "Create `GridPlot` instance" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "c3437acf-74f0-4820-86f1-fec0a79f8a6d", - "metadata": { - "tags": [] - }, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "9ffb8e467c6046a482d0ccc504609e6c", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "colors = [\"yellow\"] * n_points + [\"cyan\"] * n_points + [\"magenta\"] * n_points\n", - "\n", - "# grid with 2 rows and 2 columns\n", - "shape = (2, 2)\n", - "\n", - "# define the camera\n", - "# a mix of 2d and 3d\n", - "cameras = [\n", - " ['2d', '3d'], \n", - " ['3d', '2d']\n", - "]\n", - "\n", - "# pan-zoom controllers for each view\n", - "# views are synced if they have the \n", - "# 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", - " [0, 1],\n", - " [1, 0]\n", - "]\n", - "\n", - "# create the grid plot\n", - "grid_plot = fpl.GridPlot(\n", - " shape=shape,\n", - " cameras=cameras,\n", - " controllers=controllers\n", - ")\n", - "\n", - "# add scatter graphics to subplots\n", - "for subplot in grid_plot:\n", - " subplot.add_scatter(data=cloud, colors=colors, alpha=0.7, sizes=5)\n", - " \n", - " subplot.set_axes_visibility(True)\n", - " subplot.set_grid_visibility(True)\n", - "\n", - "\n", - "# view with sidecar \n", - "sc = Sidecar(title=\"scatter plot\")\n", - "\n", - "with sc:\n", - " display(grid_plot.show())" - ] - }, - { - "cell_type": "markdown", - "id": "7e44e2cf-6c72-46eb-9902-717a61b5c20e", - "metadata": {}, - "source": [ - "### Feature changes" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "794a8d9a-0ee9-4e94-b60a-0bdf5feb1818", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "grid_plot[0, 1].graphics[0].colors[n_points:int(n_points * 1.5)] = \"r\"" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "ca6c0825-3350-45a9-b620-5809d46f1af9", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "grid_plot[0, 1].graphics[0].colors[:n_points:10] = \"blue\"" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "18540716-08fe-49fb-8400-69d7860ac0be", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "grid_plot[0, 0].graphics[0].colors[n_points:] = \"green\"" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "db46264d-96bf-4870-a5ca-94d14134002f", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "grid_plot[0, 1].graphics[0].colors[n_points:, -1] = 0" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "034fede4-9e63-482f-b77d-ea1fe2d09577", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "grid_plot[0, 1].graphics[0].data[:n_points] = grid_plot[0, 1].graphics[0].data[n_points * 2:]" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "4b9e10c3-46f4-4b5f-abf8-b869d1385e5b", - "metadata": { - "tags": [] - }, - "outputs": [], - "source": [ - "gridplot.close()\n", - "sc.close()" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "763d8832-3a44-49e9-bd09-000aecc0e62f", - "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 -}