diff --git a/examples/collection.ipynb b/examples/collection.ipynb deleted file mode 100644 index 796471c5d..000000000 --- a/examples/collection.ipynb +++ /dev/null @@ -1,377 +0,0 @@ -{ - "cells": [ - { - "cell_type": "code", - "execution_count": 1, - "id": "fa7d8f38-12cd-4eab-9e02-a793cc663408", - "metadata": {}, - "outputs": [], - "source": [ - "from fastplotlib import Plot\n", - "from ipywidgets import VBox, HBox, IntSlider\n", - "import numpy as np" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "b05bd9e7-6248-46da-bb68-07ae1582ab53", - "metadata": {}, - "outputs": [], - "source": [ - "# linspace, create 100 evenly spaced x values from -10 to 10\n", - "xs = np.linspace(-10, 50, 100)\n", - "# sine wave\n", - "ys = np.sin(xs)\n", - "sine = np.dstack([xs, ys])[0]\n", - "\n", - "# cosine wave\n", - "ys = np.cos(xs)\n", - "cosine = np.dstack([xs, ys])[0]" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "9c5a0750-f5c4-44c7-988b-64e9fab5f7c3", - "metadata": {}, - "outputs": [], - "source": [ - "coll = list()\n", - "for i in range(0, 10, 1):\n", - " s = np.copy(sine)\n", - " s[:, 1] += i * 5\n", - " coll.append(s)\n", - " \n", - " c = np.copy(cosine)\n", - " c[:, 1] += (i * 5) + 3\n", - " coll.append(c)" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "04e0146c-3864-4b36-9bf3-fb5b99b69250", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "df7c6fbec06545f099161547b726f93a", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "RFBOutputContext()" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], - "source": [ - "plot = Plot()" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "9e7dc422-6596-4859-bf80-52b8685cb33a", - "metadata": {}, - "outputs": [], - "source": [ - "lines = plot.add_line_collection(coll, cmap=\"jet\")" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "d4bdf613-5930-419a-8072-8feb1a37037a", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "6f2d142f99d343c7a7d5fe7316e8b980", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 6, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 7, - "id": "5cd3062c-e633-4c33-ad2b-1d74818da2a8", - "metadata": {}, - "outputs": [], - "source": [ - "lines[:].present.add_event_handler(plot.auto_scale)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "dcebf81e-8d34-4025-87ac-34d62d93b756", - "metadata": {}, - "outputs": [], - "source": [ - "lines[10:].present = False\n", - "lines[:5].present = False" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "62698b0a-4f9b-4989-8bed-452848aa46a2", - "metadata": {}, - "outputs": [], - "source": [ - "lines[:].present = True" - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "39cb889a-6cb8-44b8-89c4-f1025f6be1a6", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "CollectionIndexer @ 0x7fe4a3749b40\n", - "Collection of <5> LineGraphic" - ] - }, - "execution_count": 10, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "lines[10:15]" - ] - }, - { - "cell_type": "code", - "execution_count": 11, - "id": "ba189ba5-b0b1-4129-be9b-129578bc794d", - "metadata": {}, - "outputs": [], - "source": [ - "lines[10:15].colors = \"g\"" - ] - }, - { - "cell_type": "code", - "execution_count": 12, - "id": "5f5a557d-4563-44bd-9343-4319f64e9bdb", - "metadata": {}, - "outputs": [], - "source": [ - "lines[10:15].colors[30:60] = \"cyan\"" - ] - }, - { - "cell_type": "code", - "execution_count": 13, - "id": "9a0ead4d-b714-49f7-b8d2-733cd5a71eaa", - "metadata": {}, - "outputs": [], - "source": [ - "lines[[1, 2, 5]].colors = \"w\"" - ] - }, - { - "cell_type": "code", - "execution_count": 14, - "id": "9f8fe42b-348c-4ca4-b12a-2f0192fb9a39", - "metadata": {}, - "outputs": [], - "source": [ - "lines[[16, 19]].colors[::2] = \"w\"" - ] - }, - { - "cell_type": "code", - "execution_count": 15, - "id": "b8cee8c7-070b-40af-bf63-f2cf29e3db19", - "metadata": {}, - "outputs": [], - "source": [ - "lines[2:6].colors.add_event_handler(lambda x: print(x))" - ] - }, - { - "cell_type": "code", - "execution_count": 16, - "id": "d0a4c6b3-5fa0-43ef-a7a8-baaada70bc19", - "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "FeatureEvent @ 0x7fe4cd37dc30\n", - "type: color-changed\n", - "pick_info: {'index': range(0, 100), 'collection-index': 3, 'world_object': , 'new_data': array([[1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.],\n", - " [1., 0., 1., 1.]], dtype=float32)}\n", - "\n" - ] - } - ], - "source": [ - "lines[3].colors = \"magenta\"" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "5ec14ed9-8151-4bfa-b21f-c3acc48bd968", - "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.10.5" - } - }, - "nbformat": 4, - "nbformat_minor": 5 -} diff --git a/examples/line_collection_event.ipynb b/examples/line_collection_event.ipynb new file mode 100644 index 000000000..c88971042 --- /dev/null +++ b/examples/line_collection_event.ipynb @@ -0,0 +1,212 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "f02f7349-ac1a-49b7-b304-d5b701723e0f", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "d9f54448-7718-4212-ac6d-163a2d3be146", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from fastplotlib.graphics import LineGraphic, LineCollection, ImageGraphic\n", + "from fastplotlib.plot import Plot\n", + "import pickle" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "b683c6d1-d926-43e3-a221-4ec6450e3677", + "metadata": {}, + "outputs": [], + "source": [ + "contours = pickle.load(open(\"/home/caitlin/Downloads/contours.pickle\", \"rb\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "26ced005-c52f-4696-903d-a6974ae6cefc", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "2bc75fa52d484cd1b03006a6530f10b3", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "MESA-INTEL: warning: Performance support disabled, consider sysctl dev.i915.perf_stream_paranoid=0\n", + "\n" + ] + } + ], + "source": [ + "plot = Plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "1aea0a61-db63-41e1-b330-5a922da4bac5", + "metadata": {}, + "outputs": [], + "source": [ + "line_collection = LineCollection(contours, cmap=\"Oranges\")" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "310fd5b3-49f2-4dbb-831e-cb9f369d58c8", + "metadata": {}, + "outputs": [], + "source": [ + "plot.add_graphic(line_collection)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "661a5991-0de3-44d7-a626-2ae72704dcec", + "metadata": {}, + "outputs": [], + "source": [ + "image = ImageGraphic(data=np.ones((180, 180)))" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "816f6382-f4ea-4cd4-b9f3-2dc5c232b0a5", + "metadata": {}, + "outputs": [], + "source": [ + "plot.add_graphic(image)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8187fd25-18e1-451f-b2fe-8cd2e7785c8b", + "metadata": {}, + "outputs": [], + "source": [ + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d5ddd3c4-84e2-44a3-a14f-74871aa0bb8f", + "metadata": {}, + "outputs": [], + "source": [ + "black = np.array([0.0, 0.0, 0.0, 1.0])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ac64f1c-21e0-4c21-b968-3953e7858848", + "metadata": {}, + "outputs": [], + "source": [ + "from typing import *" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0d0c971-aac9-4c77-b88c-de9d79c7d74e", + "metadata": {}, + "outputs": [], + "source": [ + "def callback_function(source: Any, target: Any, event, new_data: Any):\n", + " # calculate coms of line collection\n", + " indices = np.array(event.pick_info[\"index\"])\n", + " \n", + " coms = list()\n", + "\n", + " for contour in target.items:\n", + " coors = contour.data.feature_data[~np.isnan(contour.data.feature_data).any(axis=1)]\n", + " com = coors.mean(axis=0)\n", + " coms.append(com)\n", + "\n", + " # euclidean distance to find closest index of com \n", + " indices = np.append(indices, [0])\n", + " \n", + " ix = np.linalg.norm((coms - indices), axis=1).argsort()[0]\n", + " \n", + " ix = int(ix)\n", + " \n", + " print(ix)\n", + " \n", + " target._set_feature(feature=\"colors\", new_data=new_data, indices=ix)\n", + " \n", + " return None" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1a46d148-3007-4c7a-bf2b-91057eba855d", + "metadata": {}, + "outputs": [], + "source": [ + "image.link(event_type=\"click\", target=line_collection, feature=\"colors\", new_data=black, callback_function=callback_function)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8040456e-24a5-423b-8822-99a20e7ea470", + "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.9.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/examples/single_contour_event.ipynb b/examples/single_contour_event.ipynb new file mode 100644 index 000000000..a4b7f1f91 --- /dev/null +++ b/examples/single_contour_event.ipynb @@ -0,0 +1,225 @@ +{ + "cells": [ + { + "cell_type": "code", + "execution_count": 1, + "id": "3a992f41-b157-4b6f-9630-ef370389f318", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "589eaea4-e749-46ff-ac3d-e22aa4f75641", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from fastplotlib.graphics import LineGraphic\n", + "from fastplotlib.plot import Plot\n", + "import pickle" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "650279ac-e7df-4c6f-aac1-078ae4287028", + "metadata": {}, + "outputs": [], + "source": [ + "contours = pickle.load(open(\"/home/caitlin/Downloads/contours.pickle\", \"rb\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "49dc6123-39d8-4f60-b14b-9cfd9a008940", + "metadata": {}, + "outputs": [], + "source": [ + "single_contour = LineGraphic(data=contours[0], size=10.0, cmap=\"jet\")" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "776e916f-16c9-4114-b1ff-7ea209aa7b04", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "b9c85f639ca34c6c882396fc4753818b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "RFBOutputContext()" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "name": "stderr", + "output_type": "stream", + "text": [ + "MESA-INTEL: warning: Performance support disabled, consider sysctl dev.i915.perf_stream_paranoid=0\n", + "\n" + ] + } + ], + "source": [ + "plot = Plot()" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "203768f0-6bb4-4ba9-b099-395f2bdd2a8c", + "metadata": {}, + "outputs": [], + "source": [ + "plot.add_graphic(single_contour)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "4e46d687-d81a-4b6f-bece-c9edf3606d4f", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
initial snapshot
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "730d202ce0424559b30cd4d7d5f3b77b", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "JupyterWgpuCanvas()" + ] + }, + "execution_count": 6, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "id": "dbcda1d6-3f21-4a5e-b60f-75bf9103fbe6", + "metadata": {}, + "outputs": [], + "source": [ + "white = np.ones(shape=single_contour.colors.feature_data.shape)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "6db453cf-5856-4a7b-879f-83032cb9e9ac", + "metadata": {}, + "outputs": [], + "source": [ + "single_contour.link(event_type=\"click\", target=single_contour, feature=\"colors\", new_data=white, indices_mapper=None)" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "712c3f43-3339-4d1b-9d64-fe4f4d6bd672", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "{'click': [CallbackData(target=fastplotlib.LineGraphic @ 0x7f516cc02880, feature='colors', new_data=array([[1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.],\n", + " [1., 1., 1., 1.]]), indices_mapper=None)]}" + ] + }, + "execution_count": 9, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "single_contour.registered_callbacks" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9d353d7b-a0d0-4629-a8c0-87b767d99bd2", + "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.9.2" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 3cf358451..e5606f781 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,9 +1,14 @@ from typing import * -from pygfx import WorldObject +from .features._base import cleanup_slice + +from pygfx import WorldObject, Group from pygfx.linalg import Vector3 -from .features import GraphicFeature, PresentFeature +from .features import GraphicFeature, PresentFeature, GraphicFeatureIndexable + +from abc import ABC, abstractmethod +from dataclasses import dataclass class BaseGraphic: @@ -14,6 +19,10 @@ def __init_subclass__(cls, **kwargs): class Graphic(BaseGraphic): + pygfx_events = [ + "click" + ] + def __init__( self, name: str = None @@ -28,16 +37,9 @@ def __init__( """ self.name = name + self.registered_callbacks = dict() self.present = PresentFeature(parent=self) - valid_features = ["visible"] - for attr_name in self.__dict__.keys(): - attr = getattr(self, attr_name) - if isinstance(attr, GraphicFeature): - valid_features.append(attr_name) - - self._valid_features = tuple(valid_features) - @property def world_object(self) -> WorldObject: return self._world_object @@ -79,3 +81,246 @@ def __repr__(self): return f"'{self.name}' fastplotlib.{self.__class__.__name__} @ {hex(id(self))}" else: return f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}" + + +class Interaction(ABC): + @abstractmethod + def _set_feature(self, feature: str, new_data: Any, indices: Any): + pass + + @abstractmethod + def _reset_feature(self, feature: str): + pass + + def link(self, event_type: str, target: Any, feature: str, new_data: Any, callback_function: callable = None): + if event_type in self.pygfx_events: + self.world_object.add_event_handler(self.event_handler, event_type) + + elif event_type in self.feature_events: + if isinstance(self, GraphicCollection): + feature_instance = getattr(self[:], event_type) + else: + feature_instance = getattr(self, event_type) + + feature_instance.add_event_handler(self.event_handler) + + else: + raise ValueError("event not possible") + + if event_type in self.registered_callbacks.keys(): + self.registered_callbacks[event_type].append( + CallbackData(target=target, feature=feature, new_data=new_data, callback_function=callback_function)) + else: + self.registered_callbacks[event_type] = list() + self.registered_callbacks[event_type].append( + CallbackData(target=target, feature=feature, new_data=new_data, callback_function=callback_function)) + + def event_handler(self, event): + if event.type in self.registered_callbacks.keys(): + for target_info in self.registered_callbacks[event.type]: + if target_info.callback_function is not None: + # if callback_function is not None, then callback function should handle the entire event + target_info.callback_function(source=self, target=target_info.target, event=event, new_data=target_info.new_data) + elif isinstance(self, GraphicCollection): + # if target is a GraphicCollection, then indices will be stored in collection_index + if event.type in self.feature_events: + indices = event.pick_info["collection-index"] + + # for now we only have line collections so this works + else: + for i, item in enumerate(self._items): + if item.world_object is event.pick_info["world_object"]: + indices = i + target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, indices=indices) + else: + # if target is a single graphic, then indices do not matter + target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, + indices=None) + +@dataclass +class CallbackData: + """Class for keeping track of the info necessary for interactivity after event occurs.""" + target: Any + feature: str + new_data: Any + callback_function: callable = None + + +@dataclass +class PreviouslyModifiedData: + """Class for keeping track of previously modified data at indices""" + data: Any + indices: Any + + +class GraphicCollection(BaseGraphic): + """Graphic Collection base class""" + + pygfx_events = [ + "click" + ] + + def __init__(self, name: str = None): + self.name = name + self._items: List[Graphic] = list() + self.registered_callbacks = dict() + + @property + def world_object(self) -> Group: + return self._world_object + + @property + def items(self) -> Tuple[Graphic]: + """Get the Graphic instances within this collection""" + return tuple(self._items) + + def add_graphic(self, graphic: Graphic, reset_index: True): + """Add a graphic to the collection""" + if not isinstance(graphic, self.child_type): + raise TypeError( + f"Can only add graphics of the same type to a collection, " + f"You can only add {self.child_type} to a {self.__class__.__name__}, " + f"you are trying to add a {graphic.__class__.__name__}." + ) + self._items.append(graphic) + if reset_index: + self._reset_index() + self.world_object.add(graphic.world_object) + + def remove_graphic(self, graphic: Graphic, reset_index: True): + """Remove a graphic from the collection""" + self._items.remove(graphic) + if reset_index: + self._reset_index() + self.world_object.remove(graphic) + + def _reset_index(self): + for new_index, graphic in enumerate(self._items): + graphic.collection_index = new_index + + def __getitem__(self, key): + if isinstance(key, int): + key = [key] + + if isinstance(key, slice): + key = cleanup_slice(key, upper_bound=len(self)) + selection_indices = range(key.start, key.stop, key.step) + selection = self._items[key] + + # fancy-ish indexing + elif isinstance(key, (tuple, list)): + selection = list() + for ix in key: + selection.append(self._items[ix]) + + selection_indices = key + else: + raise TypeError("Graphic Collection indexing supports int, slice, tuple or list of integers") + return CollectionIndexer( + parent=self, + selection=selection, + selection_indices=selection_indices + ) + + def __len__(self): + return len(self._items) + + +class CollectionIndexer: + """Collection Indexer""" + def __init__( + self, + parent: GraphicCollection, + selection: List[Graphic], + selection_indices: Union[list, range], + ): + """ + + Parameters + ---------- + parent + selection + selection_indices: Union[list, range] + """ + self._selection = selection + self._selection_indices = selection_indices + + for attr_name in self._selection[0].__dict__.keys(): + attr = getattr(self._selection[0], attr_name) + if isinstance(attr, GraphicFeature): + collection_feature = CollectionFeature( + parent, + self._selection, + selection_indices=selection_indices, + feature=attr_name + ) + collection_feature.__doc__ = f"indexable {attr_name} feature for collection" + setattr(self, attr_name, collection_feature) + + def __setattr__(self, key, value): + if hasattr(self, key): + attr = getattr(self, key) + if isinstance(attr, CollectionFeature): + attr._set(value) + return + + super().__setattr__(key, value) + + def __repr__(self): + return f"{self.__class__.__name__} @ {hex(id(self))}\n" \ + f"Collection of <{len(self._selection)}> {self._selection[0].__class__.__name__}" + + +class CollectionFeature: + """Collection Feature""" + def __init__( + self, + parent: GraphicCollection, + selection: List[Graphic], + selection_indices, feature: str + ): + self._selection = selection + self._selection_indices = selection_indices + self._feature = feature + + self._feature_instances: List[GraphicFeature] = list() + + for graphic in self._selection: + fi = getattr(graphic, self._feature) + self._feature_instances.append(fi) + + if isinstance(fi, GraphicFeatureIndexable): + self._indexable = True + else: + self._indexable = False + + def _set(self, value): + self[:] = value + + def __getitem__(self, item): + # only for indexable graphic features + return [fi[item] for fi in self._feature_instances] + + def __setitem__(self, key, value): + if self._indexable: + for fi in self._feature_instances: + fi[key] = value + + else: + for fi in self._feature_instances: + fi._set(value) + + def add_event_handler(self, handler: callable): + for fi in self._feature_instances: + fi.add_event_handler(handler) + + def remove_event_handler(self, handler: callable): + for fi in self._feature_instances: + fi.remove_event_handler(handler) + + def block_events(self, b: bool): + for fi in self._feature_instances: + fi.block_events(b) + + def __repr__(self): + return f"Collection feature for: <{self._feature}>" diff --git a/fastplotlib/graphics/_collection.py b/fastplotlib/graphics/_collection.py deleted file mode 100644 index 15cc26a60..000000000 --- a/fastplotlib/graphics/_collection.py +++ /dev/null @@ -1,172 +0,0 @@ -from typing import * - -import numpy as np - -from pygfx import Group - -from ._base import BaseGraphic, Graphic -from .features._base import GraphicFeature, GraphicFeatureIndexable, cleanup_slice - - -class GraphicCollection(BaseGraphic): - """Graphic Collection base class""" - def __init__(self, name: str = None): - self.name = name - self._items: List[Graphic] = list() - - @property - def world_object(self) -> Group: - return self._world_object - - @property - def items(self) -> Tuple[Graphic]: - """Get the Graphic instances within this collection""" - return tuple(self._items) - - def add_graphic(self, graphic: Graphic, reset_index: True): - """Add a graphic to the collection""" - if not isinstance(graphic, self.child_type): - raise TypeError( - f"Can only add graphics of the same type to a collection, " - f"You can only add {self.child_type} to a {self.__class__.__name__}, " - f"you are trying to add a {graphic.__class__.__name__}." - ) - self._items.append(graphic) - if reset_index: - self._reset_index() - self.world_object.add(graphic.world_object) - - def remove_graphic(self, graphic: Graphic, reset_index: True): - """Remove a graphic from the collection""" - self._items.remove(graphic) - if reset_index: - self._reset_index() - self.world_object.remove(graphic) - - def _reset_index(self): - for new_index, graphic in enumerate(self._items): - graphic.collection_index = new_index - - def __getitem__(self, key): - if isinstance(key, int): - key = [key] - - if isinstance(key, slice): - key = cleanup_slice(key, upper_bound=len(self)) - selection_indices = range(key.start, key.stop, key.step) - selection = self._items[key] - - # fancy-ish indexing - elif isinstance(key, (tuple, list)): - selection = list() - for ix in key: - selection.append(self._items[ix]) - - selection_indices = key - else: - raise TypeError("Graphic Collection indexing supports int, slice, tuple or list of integers") - return CollectionIndexer( - parent=self, - selection=selection, - selection_indices=selection_indices - ) - - def __len__(self): - return len(self._items) - - -class CollectionIndexer: - """Collection Indexer""" - def __init__( - self, - parent: GraphicCollection, - selection: List[Graphic], - selection_indices: Union[list, range], - ): - """ - - Parameters - ---------- - parent - selection - selection_indices: Union[list, range] - """ - self._selection = selection - self._selection_indices = selection_indices - - for attr_name in self._selection[0].__dict__.keys(): - attr = getattr(self._selection[0], attr_name) - if isinstance(attr, GraphicFeature): - collection_feature = CollectionFeature( - parent, - self._selection, - selection_indices=selection_indices, - feature=attr_name - ) - collection_feature.__doc__ = f"indexable {attr_name} feature for collection" - setattr(self, attr_name, collection_feature) - - def __setattr__(self, key, value): - if hasattr(self, key): - attr = getattr(self, key) - if isinstance(attr, CollectionFeature): - attr._set(value) - return - - super().__setattr__(key, value) - - def __repr__(self): - return f"{self.__class__.__name__} @ {hex(id(self))}\n" \ - f"Collection of <{len(self._selection)}> {self._selection[0].__class__.__name__}" - - -class CollectionFeature: - """Collection Feature""" - def __init__( - self, - parent: GraphicCollection, - selection: List[Graphic], - selection_indices, feature: str - ): - self._selection = selection - self._selection_indices = selection_indices - self._feature = feature - - self._feature_instances: List[GraphicFeature] = list() - - for graphic in self._selection: - fi = getattr(graphic, self._feature) - self._feature_instances.append(fi) - - if isinstance(fi, GraphicFeatureIndexable): - self._indexable = True - else: - self._indexable = False - - def _set(self, value): - self[:] = value - - def __getitem__(self, item): - # only for indexable graphic features - return [fi[item] for fi in self._feature_instances] - - def __setitem__(self, key, value): - if self._indexable: - for fi in self._feature_instances: - fi[key] = value - - else: - for fi in self._feature_instances: - fi._set(value) - key = None - - def add_event_handler(self, handler: callable): - for fi in self._feature_instances: - fi.add_event_handler(handler) - - def remove_event_handler(self, handler: callable): - for fi in self._feature_instances: - fi.remove_event_handler(handler) - - def __repr__(self): - return f"Collection feature for: <{self._feature}>" diff --git a/fastplotlib/graphics/features/__init__.py b/fastplotlib/graphics/features/__init__.py index 1fcb71246..f1df7a042 100644 --- a/fastplotlib/graphics/features/__init__.py +++ b/fastplotlib/graphics/features/__init__.py @@ -1,4 +1,4 @@ from ._colors import ColorFeature, CmapFeature, ImageCmapFeature from ._data import PointsDataFeature, ImageDataFeature from ._present import PresentFeature -from ._base import GraphicFeature +from ._base import GraphicFeature, GraphicFeatureIndexable diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 3aec126df..d392efa7e 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -9,7 +9,7 @@ class FeatureEvent: """ - type: -, example: "color-changed" + type: , example: "colors" pick_info: dict in the form: { "index": indices where feature data was changed, ``range`` object or List[int], @@ -50,6 +50,11 @@ def __init__(self, parent, data: Any, collection_index: int = None): self._collection_index = collection_index self._event_handlers = list() + self._block_events = False + + def block_events(self, b: bool): + self._block_events = b + @property def feature_data(self): """graphic feature data managed by fastplotlib, do not modify directly""" @@ -98,6 +103,9 @@ def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any): pass def _call_event_handlers(self, event_data: FeatureEvent): + if self._block_events: + return + for func in self._event_handlers: try: if len(getfullargspec(func).args) > 0: diff --git a/fastplotlib/graphics/features/_colors.py b/fastplotlib/graphics/features/_colors.py index 7304c2b21..067687aca 100644 --- a/fastplotlib/graphics/features/_colors.py +++ b/fastplotlib/graphics/features/_colors.py @@ -189,7 +189,7 @@ def _feature_changed(self, key, new_data): "new_data": new_data, } - event_data = FeatureEvent(type="color-changed", pick_info=pick_info) + event_data = FeatureEvent(type="colors", pick_info=pick_info) self._call_event_handlers(event_data) @@ -241,6 +241,6 @@ def _feature_changed(self, key, new_data): "new_data": new_data } - event_data = FeatureEvent(type="cmap-changed", pick_info=pick_info) + event_data = FeatureEvent(type="cmap", pick_info=pick_info) self._call_event_handlers(event_data) diff --git a/fastplotlib/graphics/features/_data.py b/fastplotlib/graphics/features/_data.py index 923524bb1..3039c7931 100644 --- a/fastplotlib/graphics/features/_data.py +++ b/fastplotlib/graphics/features/_data.py @@ -88,7 +88,7 @@ def _feature_changed(self, key, new_data): "new_data": new_data } - event_data = FeatureEvent(type="data-changed", pick_info=pick_info) + event_data = FeatureEvent(type="data", pick_info=pick_info) self._call_event_handlers(event_data) @@ -145,6 +145,6 @@ def _feature_changed(self, key, new_data): "new_data": new_data } - event_data = FeatureEvent(type="data-changed", pick_info=pick_info) + event_data = FeatureEvent(type="data", pick_info=pick_info) self._call_event_handlers(event_data) diff --git a/fastplotlib/graphics/features/_present.py b/fastplotlib/graphics/features/_present.py index 269ef1a6b..d028bd102 100644 --- a/fastplotlib/graphics/features/_present.py +++ b/fastplotlib/graphics/features/_present.py @@ -50,6 +50,6 @@ def _feature_changed(self, key, new_data): "new_data": new_data } - event_data = FeatureEvent(type="present-changed", pick_info=pick_info) + event_data = FeatureEvent(type="present", pick_info=pick_info) self._call_event_handlers(event_data) \ No newline at end of file diff --git a/fastplotlib/graphics/heatmap.py b/fastplotlib/graphics/heatmap.py index 2c33564db..103a0fc2e 100644 --- a/fastplotlib/graphics/heatmap.py +++ b/fastplotlib/graphics/heatmap.py @@ -53,24 +53,18 @@ def __init__( ): """ Create a Heatmap Graphic - Parameters ---------- data: array-like, must be 2-dimensional | array-like, usually numpy.ndarray, must support ``memoryview()`` | Tensorflow Tensors also work _I think_, but not thoroughly tested - vmin: int, optional minimum value for color scaling, calculated from data if not provided - vmax: int, optional maximum value for color scaling, calculated from data if not provided - cmap: str, optional colormap to use to display the image data, default is ``"plasma"`` - selection_options - args: additional arguments passed to Graphic kwargs: @@ -140,4 +134,4 @@ def add_highlight(self, event): self.world_object.add(self.selection_graphic) self._highlights.append(self.selection_graphic) - return rval + return rval \ No newline at end of file diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 48459c63e..d1842ff85 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -2,12 +2,16 @@ import pygfx -from ._base import Graphic +from ._base import Graphic, Interaction, PreviouslyModifiedData from .features import ImageCmapFeature, ImageDataFeature from ..utils import quick_min_max -class ImageGraphic(Graphic): +class ImageGraphic(Graphic, Interaction): + feature_events = [ + "data", + "colors", + ] def __init__( self, data: Any, @@ -20,50 +24,35 @@ def __init__( ): """ Create an Image Graphic - Parameters ---------- data: array-like, must be 2-dimensional | array-like, usually numpy.ndarray, must support ``memoryview()`` | Tensorflow Tensors also work _I think_, but not thoroughly tested - vmin: int, optional minimum value for color scaling, calculated from data if not provided - vmax: int, optional maximum value for color scaling, calculated from data if not provided - cmap: str, optional, default "nearest" colormap to use to display the image data, default is ``"plasma"`` - filter: str, optional, default "nearest" interpolation filter, one of "nearest" or "linear" - args: additional arguments passed to Graphic - kwargs: additional keyword arguments passed to Graphic - Examples -------- - .. code-block:: python - from fastplotlib import Plot - # create a `Plot` instance plot = Plot() - # make some random 2D image data data = np.random.rand(512, 512) - # plot the image data plot.add_image(data=data) - # show the plot plot.show() - """ super().__init__(*args, **kwargs) @@ -103,3 +92,13 @@ def vmax(self, value: float): self.world_object.material.clim[0], value ) + + def _set_feature(self, feature: str, new_data: Any, indices: Any): + pass + + def _reset_feature(self, feature: str): + pass + + + + diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index cb0224007..9df940b52 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -2,12 +2,17 @@ import numpy as np import pygfx -from ._base import Graphic +from ._base import Graphic, Interaction, PreviouslyModifiedData from .features import PointsDataFeature, ColorFeature, CmapFeature from ..utils import get_colors -class LineGraphic(Graphic): +class LineGraphic(Graphic, Interaction): + feature_events = [ + "data", + "colors", + ] + def __init__( self, data: Any, @@ -83,3 +88,30 @@ def __init__( if z_position is not None: self.world_object.position.z = z_position + + def _set_feature(self, feature: str, new_data: Any, indices: Any = None): + if not hasattr(self, "_previous_data"): + self._previous_data = dict() + elif hasattr(self, "_previous_data"): + self._reset_feature(feature) + + feature_instance = getattr(self, feature) + if indices is not None: + previous = feature_instance[indices].copy() + feature_instance[indices] = new_data + else: + previous = feature_instance._data.copy() + feature_instance._set(new_data) + if feature in self._previous_data.keys(): + self._previous_data[feature].data = previous + self._previous_data[feature].indices = indices + else: + self._previous_data[feature] = PreviouslyModifiedData(data=previous, indices=indices) + + def _reset_feature(self, feature: str): + prev_ixs = self._previous_data[feature].indices + feature_instance = getattr(self, feature) + if prev_ixs is not None: + feature_instance[prev_ixs] = self._previous_data[feature].data + else: + feature_instance._set(self._previous_data[feature].data) \ No newline at end of file diff --git a/fastplotlib/graphics/linecollection.py b/fastplotlib/graphics/linecollection.py index 49b123028..d17f9c241 100644 --- a/fastplotlib/graphics/linecollection.py +++ b/fastplotlib/graphics/linecollection.py @@ -2,15 +2,19 @@ import pygfx from typing import * -from ._collection import GraphicCollection +from ._base import Interaction, PreviouslyModifiedData, GraphicCollection from .line import LineGraphic from ..utils import get_colors -from typing import * +from copy import deepcopy -class LineCollection(GraphicCollection): +class LineCollection(GraphicCollection, Interaction): """Line Collection graphic""" child_type = LineGraphic + feature_events = [ + "data", + "colors", + ] def __init__( self, @@ -113,3 +117,34 @@ def __init__( ) self.add_graphic(lg, reset_index=False) + + def _set_feature(self, feature: str, new_data: Any, indices: Any): + if not hasattr(self, "_previous_data"): + self._previous_data = dict() + elif hasattr(self, "_previous_data"): + self._reset_feature(feature) + + coll_feature = getattr(self[indices], feature) + + data = list() + for fea in coll_feature._feature_instances: + data.append(fea._data) + + # later we can think about multi-index events + previous = deepcopy(data[0]) + coll_feature._set(new_data) + + if feature in self._previous_data.keys(): + self._previous_data[feature].data = previous + self._previous_data[feature].indices = indices + else: + self._previous_data[feature] = PreviouslyModifiedData(data=previous, indices=indices) + + def _reset_feature(self, feature: str): + # implemented for a single index at moment + prev_ixs = self._previous_data[feature].indices + coll_feature = getattr(self[prev_ixs], feature) + + coll_feature.block_events(True) + coll_feature._set(self._previous_data[feature].data) + coll_feature.block_events(False) \ No newline at end of file