diff --git a/examples/event_handler.ipynb b/examples/event_handler.ipynb
new file mode 100644
index 000000000..69c835e2e
--- /dev/null
+++ b/examples/event_handler.ipynb
@@ -0,0 +1,430 @@
+{
+ "cells": [
+ {
+ "cell_type": "markdown",
+ "id": "a0b458c1-53c6-43d9-a72a-41f51bfe493d",
+ "metadata": {},
+ "source": [
+ "### notebook for learning event handler system"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "638b6a65-6d78-459c-ac88-35312233d22a",
+ "metadata": {},
+<<<<<<< HEAD
+ "outputs": [],
+=======
+ "outputs": [
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "2022-12-19 11:35:14.246180: I tensorflow/core/platform/cpu_feature_guard.cc:193] This TensorFlow binary is optimized with oneAPI Deep Neural Network Library (oneDNN) to use the following CPU instructions in performance-critical operations: AVX2 AVX512F AVX512_VNNI FMA\n",
+ "To enable them in other operations, rebuild TensorFlow with the appropriate compiler flags.\n",
+ "2022-12-19 11:35:14.498269: I tensorflow/core/util/port.cc:104] oneDNN custom operations are on. You may see slightly different numerical results due to floating-point round-off errors from different computation orders. To turn them off, set the environment variable `TF_ENABLE_ONEDNN_OPTS=0`.\n",
+ "2022-12-19 11:35:14.542615: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libcudart.so.11.0'; dlerror: libcudart.so.11.0: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /home/caitlin/venvs/mescore/lib/python3.9/site-packages/cv2/../../lib64:\n",
+ "2022-12-19 11:35:14.542629: I tensorflow/compiler/xla/stream_executor/cuda/cudart_stub.cc:29] Ignore above cudart dlerror if you do not have a GPU set up on your machine.\n",
+ "2022-12-19 11:35:15.350591: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer.so.7'; dlerror: libnvinfer.so.7: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /home/caitlin/venvs/mescore/lib/python3.9/site-packages/cv2/../../lib64:\n",
+ "2022-12-19 11:35:15.350720: W tensorflow/compiler/xla/stream_executor/platform/default/dso_loader.cc:64] Could not load dynamic library 'libnvinfer_plugin.so.7'; dlerror: libnvinfer_plugin.so.7: cannot open shared object file: No such file or directory; LD_LIBRARY_PATH: /home/caitlin/venvs/mescore/lib/python3.9/site-packages/cv2/../../lib64:\n",
+ "2022-12-19 11:35:15.350726: W tensorflow/compiler/tf2tensorrt/utils/py_utils.cc:38] TF-TRT Warning: Cannot dlopen some TensorRT libraries. If you would like to use Nvidia GPU with TensorRT, please make sure the missing libraries mentioned above are installed properly.\n"
+ ]
+ },
+ {
+ "ename": "ImportError",
+ "evalue": "cannot import name 'Graphic' from partially initialized module 'fastplotlib.graphics._base' (most likely due to a circular import) (/home/caitlin/repos/fastplotlib/fastplotlib/graphics/_base.py)",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mImportError\u001b[0m Traceback (most recent call last)",
+ "Cell \u001b[0;32mIn[1], line 5\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mmatplotlib\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m pyplot \u001b[38;5;28;01mas\u001b[39;00m plt\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpandas\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mpd\u001b[39;00m\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mfastplotlib\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m GridPlot, Image, Plot, Line, Heatmap\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mscipy\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mspatial\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m distance\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mipywidgets\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mwidgets\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m IntSlider, VBox\n",
+ "File \u001b[0;32m~/repos/fastplotlib/fastplotlib/__init__.py:1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mplot\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Plot\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpathlib\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Path\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mwgpu\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mgui\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mauto\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m run\n",
+ "File \u001b[0;32m~/repos/fastplotlib/fastplotlib/plot.py:3\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpygfx\u001b[39;00m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mwgpu\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mgui\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mauto\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m WgpuCanvas\n\u001b[0;32m----> 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mlayouts\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_subplot\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Subplot\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m graphics\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mfunctools\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m partial\n",
+ "File \u001b[0;32m~/repos/fastplotlib/fastplotlib/layouts/__init__.py:1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_gridplot\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m GridPlot\n\u001b[1;32m 3\u001b[0m __all__ \u001b[38;5;241m=\u001b[39m [\n\u001b[1;32m 4\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mGridPlot\u001b[39m\u001b[38;5;124m\"\u001b[39m\n\u001b[1;32m 5\u001b[0m ]\n",
+ "File \u001b[0;32m~/repos/fastplotlib/fastplotlib/layouts/_gridplot.py:5\u001b[0m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpygfx\u001b[39;00m\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_defaults\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m create_controller\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_subplot\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Subplot\n\u001b[1;32m 6\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mtyping\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mwgpu\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mgui\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mauto\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m WgpuCanvas\n",
+ "File \u001b[0;32m~/repos/fastplotlib/fastplotlib/layouts/_subplot.py:3\u001b[0m\n\u001b[1;32m 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mpygfx\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Scene, OrthographicCamera, PanZoomController, OrbitOrthoController, \\\n\u001b[1;32m 2\u001b[0m AxesHelper, GridHelper, WgpuRenderer, Background, BackgroundMaterial\n\u001b[0;32m----> 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mgraphics\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m HeatmapGraphic\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_defaults\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m create_camera, create_controller\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mtyping\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n",
+ "File \u001b[0;32m~/repos/fastplotlib/fastplotlib/graphics/__init__.py:1\u001b[0m\n\u001b[0;32m----> 1\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mhistogram\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m HistogramGraphic\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mline\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m LineGraphic\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mscatter\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m ScatterGraphic\n",
+ "File \u001b[0;32m~/repos/fastplotlib/fastplotlib/graphics/histogram.py:7\u001b[0m\n\u001b[1;32m 4\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mnumpy\u001b[39;00m \u001b[38;5;28;01mas\u001b[39;00m \u001b[38;5;21;01mnp\u001b[39;00m\n\u001b[1;32m 5\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpygfx\u001b[39;00m\n\u001b[0;32m----> 7\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_base\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Graphic\n\u001b[1;32m 10\u001b[0m \u001b[38;5;28;01mclass\u001b[39;00m \u001b[38;5;21;01m_HistogramBin\u001b[39;00m(pygfx\u001b[38;5;241m.\u001b[39mMesh):\n\u001b[1;32m 11\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__int__\u001b[39m(\u001b[38;5;28mself\u001b[39m, \u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n",
+ "File \u001b[0;32m~/repos/fastplotlib/fastplotlib/graphics/_base.py:10\u001b[0m\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mabc\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m ABC, abstractmethod\n\u001b[1;32m 9\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mdataclasses\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m dataclass\n\u001b[0;32m---> 10\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mlinecollection\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m LineCollection\n\u001b[1;32m 12\u001b[0m \u001b[38;5;28;01mclass\u001b[39;00m \u001b[38;5;21;01mGraphic\u001b[39;00m:\n\u001b[1;32m 13\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__init__\u001b[39m(\n\u001b[1;32m 14\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 15\u001b[0m data,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 20\u001b[0m name: \u001b[38;5;28mstr\u001b[39m \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n\u001b[1;32m 21\u001b[0m ):\n",
+ "File \u001b[0;32m~/repos/fastplotlib/fastplotlib/graphics/linecollection.py:5\u001b[0m\n\u001b[1;32m 2\u001b[0m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;21;01mpygfx\u001b[39;00m\n\u001b[1;32m 3\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mtyping\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Union, List\n\u001b[0;32m----> 5\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mfastplotlib\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mgraphics\u001b[39;00m\u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01m_base\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m Graphic\n\u001b[1;32m 7\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01m.\u001b[39;00m\u001b[38;5;21;01mline\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m LineGraphic\n\u001b[1;32m 8\u001b[0m \u001b[38;5;28;01mfrom\u001b[39;00m \u001b[38;5;21;01mtyping\u001b[39;00m \u001b[38;5;28;01mimport\u001b[39;00m \u001b[38;5;241m*\u001b[39m\n",
+ "\u001b[0;31mImportError\u001b[0m: cannot import name 'Graphic' from partially initialized module 'fastplotlib.graphics._base' (most likely due to a circular import) (/home/caitlin/repos/fastplotlib/fastplotlib/graphics/_base.py)"
+ ]
+ }
+ ],
+>>>>>>> 24fb5a5 (blah)
+ "source": [
+ "from mesmerize_core import *\n",
+ "import numpy as np\n",
+ "from matplotlib import pyplot as plt\n",
+ "import pandas as pd\n",
+ "from fastplotlib import GridPlot, Image, Plot, Line, Heatmap\n",
+ "from scipy.spatial import distance\n",
+ "from ipywidgets.widgets import IntSlider, VBox\n",
+ "import pygfx"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "80938adc-1775-4751-94dd-89fb94ed673a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "class Contour_Selection():\n",
+ " def __init__(\n",
+ " self,\n",
+ " gp: GridPlot,\n",
+ " coms,\n",
+ " ):\n",
+ " self.gp = gp\n",
+ " self.heatmap = self.gp.subplots[0, 1].scene.children[0]\n",
+ " self.image = None\n",
+ " self._contour_index = None \n",
+ " \n",
+ " for child in self.gp.subplots[0, 0].scene.children:\n",
+ " if isinstance(child, pygfx.Image):\n",
+ " self.image = child\n",
+ " break;\n",
+ " if self.image == None:\n",
+ " raise ValueError(\"No image found!\")\n",
+ " self.coms = np.array(coms)\n",
+ " \n",
+ " self.image.add_event_handler(self.event_handler, \"click\")\n",
+ " \n",
+ " # first need to add event handler for when contour is clicked on\n",
+ " # should also trigger highlighting in heatmap\n",
+ " def event_handler(self, event):\n",
+ " if self._contour_index is not None:\n",
+ " self.remove_highlight()\n",
+ " self.add_highlight(event)\n",
+ " else:\n",
+ " self.add_highlight(event)\n",
+ " \n",
+ " def add_highlight(self, event):\n",
+ " click_location = np.array(event.pick_info[\"index\"])\n",
+ " self._contour_index = np.linalg.norm((self.coms - click_location), axis=1).argsort()[0] + 1\n",
+ " line = self.gp.subplots[0, 0].scene.children[self._contour_index]\n",
+ " line.geometry.colors.data[:] = np.array([1.0, 1.0, 1.0, 1.0]) \n",
+ " line.geometry.colors.update_range()\n",
+ " #self.heatmap.add_highlight(self._contour_index)\n",
+ " \n",
+ " def remove_highlight(self):\n",
+ " # change color of highlighted index back to normal\n",
+ " line = self.gp.subplots[0, 0].scene.children[self._contour_index]\n",
+ " line.geometry.colors.data[:] = np.array([1., 0., 0., 0.7]) \n",
+ " line.geometry.colors.update_range()\n",
+ " # for h in self.heatmap._highlights:\n",
+ " # self.heatmap.remove_highlight(h)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "7b3129e6-5d82-4f39-b887-e584421c3a74",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "set_parent_raw_data_path(\"/home/kushal/caiman_data/\")\n",
+ "\n",
+ "batch_path = \"/home/clewis7/caiman_data/cnmf_practice/batch.pickle\"\n",
+ "\n",
+ "movie_path = \"/home/kushal/caiman_data/example_movies/Sue_2x_3000_40_-46.tif\""
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "776c6dd8-cf07-47ee-bcc4-5fe84bc030f5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df = load_batch(batch_path)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "b22fd79b-5a7a-48fb-898c-4d3f3a34c118",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ " \n",
+ " | \n",
+ " algo | \n",
+ " item_name | \n",
+ " input_movie_path | \n",
+ " params | \n",
+ " outputs | \n",
+ " comments | \n",
+ " uuid | \n",
+ "
\n",
+ " \n",
+ " \n",
+ " \n",
+ " 0 | \n",
+ " mcorr | \n",
+ " my_movie | \n",
+ " example_movies/Sue_2x_3000_40_-46.tif | \n",
+ " {'main': {'max_shifts': (24, 24), 'strides': (... | \n",
+ " {'mean-projection-path': 1ed8feb3-9fc8-4a78-8f... | \n",
+ " None | \n",
+ " 1ed8feb3-9fc8-4a78-8f6d-164620822016 | \n",
+ "
\n",
+ " \n",
+ " 1 | \n",
+ " cnmf | \n",
+ " my_movie | \n",
+ " 1ed8feb3-9fc8-4a78-8f6d-164620822016/1ed8feb3-... | \n",
+ " {'main': {'fr': 30, 'p': 1, 'nb': 2, 'merge_th... | \n",
+ " {'mean-projection-path': 5f4e3e27-ac1f-4ede-90... | \n",
+ " None | \n",
+ " 5f4e3e27-ac1f-4ede-903b-be43bd81fddc | \n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
"
+ ],
+ "text/plain": [
+ " algo item_name input_movie_path \\\n",
+ "0 mcorr my_movie example_movies/Sue_2x_3000_40_-46.tif \n",
+ "1 cnmf my_movie 1ed8feb3-9fc8-4a78-8f6d-164620822016/1ed8feb3-... \n",
+ "\n",
+ " params \\\n",
+ "0 {'main': {'max_shifts': (24, 24), 'strides': (... \n",
+ "1 {'main': {'fr': 30, 'p': 1, 'nb': 2, 'merge_th... \n",
+ "\n",
+ " outputs comments \\\n",
+ "0 {'mean-projection-path': 1ed8feb3-9fc8-4a78-8f... None \n",
+ "1 {'mean-projection-path': 5f4e3e27-ac1f-4ede-90... None \n",
+ "\n",
+ " uuid \n",
+ "0 1ed8feb3-9fc8-4a78-8f6d-164620822016 \n",
+ "1 5f4e3e27-ac1f-4ede-903b-be43bd81fddc "
+ ]
+ },
+ "execution_count": 5,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "03045957-837e-49e3-83aa-3e069af977a1",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "3b283ef0f3084d828f80fd3a6cb25413",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "RFBOutputContext()"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "8b104ac5f10e4d53866953a4401593df",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "VBox(children=(JupyterWgpuCanvas(), IntSlider(value=0, max=2999)))"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
+ "source": [
+ "gp = GridPlot(shape=(1,2))\n",
+ "\n",
+ "contours, coms = df.iloc[-1].cnmf.get_contours()\n",
+ "movie = df.iloc[-1].cnmf.get_input_memmap()\n",
+ "temporal = df.iloc[-1].cnmf.get_temporal()\n",
+ "\n",
+ "contour_graphic = Image(movie[0].T, cmap=\"gnuplot2\")\n",
+ "heatmap = Heatmap(data=temporal[:,0:1000], cmap=\"jet\")\n",
+ "\n",
+ "slider = IntSlider(value=0, min=0, max=movie.shape[0] - 1, step=1)\n",
+ "\n",
+ "gp.subplots[0,0].add_graphic(contour_graphic)\n",
+ "gp.subplots[0,1].add_graphic(heatmap)\n",
+ "\n",
+ "for coor in contours:\n",
+ " # line data has to be 3D\n",
+ " zs = np.ones(coor.shape[0]) # this will place it above the image graphic\n",
+ " c3d = [coor[:, 0], coor[:, 1], zs]\n",
+ " coors_3d = np.dstack(c3d)[0]\n",
+ "\n",
+ " # make all the lines red, [R, G, B, A] array\n",
+ " colors = np.vstack([[1., 0., 0., 0.7]] * coors_3d.shape[0])\n",
+ " line_graphic = Line(data=coors_3d, colors=colors, zlevel=1)\n",
+ " gp.subplots[0, 0].add_graphic(line_graphic)\n",
+ "\n",
+ "previous_slider_value = 0\n",
+ "def update_frame(): \n",
+ " if slider.value == previous_slider_value:\n",
+ " return\n",
+ " contour_graphic.update_data(data=movie[slider.value].T)\n",
+ "\n",
+ "gp.add_animations([update_frame])\n",
+ "\n",
+ "VBox([gp.show(), slider])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "2bf4a1ea-3559-4846-bc32-4dddaca2d470",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "contour_selection = Contour_Selection(gp=gp, coms=coms)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "5ecdbdea-4e77-462f-a18d-5ed9b45faada",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "(155, 3000)"
+ ]
+ },
+ "execution_count": 11,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "temporal.shape"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 21,
+ "id": "51b3feea-af91-4158-97a2-8dbadd2480b5",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "numpy.ndarray"
+ ]
+ },
+ "execution_count": 21,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "type(coms[0])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 22,
+ "id": "3b2011f4-046b-4396-a592-81a5128d2482",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([7.12861818, 9.84114483])"
+ ]
+ },
+ "execution_count": 22,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "coms[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "c5791bf4-a116-4093-bce1-eee7bc25221c",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "gp.subplots[0, 1]"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ae9cce16-370f-44d2-b532-dc442cce379d",
+ "metadata": {},
+ "source": [
+ "next steps:\n",
+ " clicking on a contour should highlight it and the heatmap row"
+ ]
+ }
+ ],
+ "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/linecollection_event.ipynb b/examples/linecollection_event.ipynb
new file mode 100644
index 000000000..d5aaabacf
--- /dev/null
+++ b/examples/linecollection_event.ipynb
@@ -0,0 +1,302 @@
+{
+ "cells": [
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "0c32716f-320e-4021-ad60-1c142fe6fd56",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "%load_ext autoreload\n",
+ "%autoreload 2"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 1,
+ "id": "6d72f23c-0f3a-4b2c-806d-3b239237c725",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "from fastplotlib.graphics import ImageGraphic, LineCollection\n",
+ "from fastplotlib import GridPlot\n",
+ "import pickle"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 2,
+ "id": "a8514bb3-eef5-4fd1-bcbf-9c50173a9a3c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def auto_scale(p):\n",
+ " p.camera.maintain_aspect = False\n",
+ " width, height, depth = np.ptp(p.scene.get_world_bounding_box(), axis=0)\n",
+ " p.camera.width = width\n",
+ " p.camera.height = height\n",
+ "\n",
+ " p.controller.distance = 0\n",
+ " \n",
+ " p.controller.zoom(0.8 / p.controller.zoom_value)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 3,
+ "id": "2fb13990-63fc-4fc6-b5c1-93f8ec4c1572",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "contours = pickle.load(open(\"/home/kushal/caiman_data/contours.pickle\", \"rb\"))[0]\n",
+ "temporal = pickle.load(open(\"/home/kushal/caiman_data/temporal.pickle\", \"rb\"))\n",
+ "temporal += temporal.min()\n",
+ "\n",
+ "# make it a stack of traces\n",
+ "y_zero = 0\n",
+ "sep = 10\n",
+ "for i in range(1, temporal.shape[0]):\n",
+ " y_zero = temporal[i - 1].max()\n",
+ " temporal[i] += y_zero + sep\n",
+ "\n",
+ "# random colors\n",
+ "colors = np.random.rand(len(contours), 4).astype(np.float32)\n",
+ "colors[:, -1] = 1"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "id": "654da73f-d20c-4a0f-bd99-13c1a52f5f5a",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "98ad6155b7c34241bd705d0f40bce8c0",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "RFBOutputContext()"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "text/html": [
+ "
initial snapshot
"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "8906e4b8e78c465ca05f088f105de2fc",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "JupyterWgpuCanvas()"
+ ]
+ },
+ "execution_count": 4,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "# img and contour plot\n",
+ "plot = GridPlot(shape=(1, 2))\n",
+ "\n",
+ "data = np.ones(shape=(175, 175))\n",
+ "\n",
+ "line_collection = LineCollection(data=contours, z_position=[[1]] * len(contours), colors=colors.tolist())\n",
+ "plot[0, 0].add_graphic(line_collection)\n",
+ "\n",
+ "img = ImageGraphic(data=data)\n",
+ "plot[0, 0].add_graphic(img)\n",
+ "\n",
+ "\n",
+ "temporal_coll = LineCollection(data=temporal, colors=colors.tolist())\n",
+ "plot[0, 1].add_graphic(temporal_coll)\n",
+ "\n",
+ "plot.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 16,
+ "id": "0836f4fc-fb3b-44c6-8515-ab8d63dff52b",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ ""
+ ]
+ },
+ "execution_count": 16,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "line_collection._world_object.parent"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "ae5fe95b-88be-48c7-a4a6-51d2818fbff0",
+ "metadata": {},
+ "source": [
+ "# you need to run this to make the stacked lineplot visible, it's easier in the latest master with camera auto-scaling"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 5,
+ "id": "8597de09-94aa-44cd-b480-acc1758a198c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "plot[0, 1].controller.distance = 0\n",
+ "auto_scale(plot[0, 1])"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 6,
+ "id": "e321cf0d-52f7-4da2-983a-ff10653093bb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "white = list()\n",
+ "for contour in line_collection:\n",
+ " white.append(np.ones(shape=contour.colors.shape))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "id": "01296977-1664-40ae-86fb-ed515fa96f4a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "white_temporal = np.ones((len(contours), 4)).astype(np.float32)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "4430e805-54db-4218-967a-30290ced8ca9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from typing import *"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 9,
+ "id": "c02210db-5347-4e97-a551-f4f362d3910a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def indices_mapper(target: Any, indices: np.array) -> int:\n",
+ " # calculate coms of line collection \n",
+ " \n",
+ " coms = list()\n",
+ "\n",
+ " for contour in target.data:\n",
+ " coors = contour.data[~np.isnan(contour.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",
+ " #return that index to set feature \n",
+ " return ix"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 11,
+ "id": "0fa4033f-323b-421c-84ad-da34a0ac177c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# until we create an event \"color-changed\" (and for other graphic features)\n",
+ "# later we can just use the \"color-changed\" event from contour to change the lineplot or heatmap etc.\n",
+ "def indices_mapper_temporal(target, indices):\n",
+ " # global since we don't have something like \"color changed\"\n",
+ " # as an event which we can used for stakced line plots\n",
+ " global contours\n",
+ " coms = list()\n",
+ "\n",
+ " for contour in contours:\n",
+ " coors = contour[~np.isnan(contour.data).any(axis=1)]\n",
+ " com = coors.mean(axis=0)\n",
+ " coms.append(com)\n",
+ " \n",
+ " ix = np.linalg.norm((np.array(coms) - np.array(indices)), axis=1).argsort()[0]\n",
+ " print(ix)\n",
+ " \n",
+ " #return that index to set feature \n",
+ " return ix"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 12,
+ "id": "0d3a3665-c9e5-42e4-9d61-c02f1f401ee2",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "img.link(event_type=\"click\", target=line_collection, feature=\"colors\", new_data=white, indices_mapper=indices_mapper)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 13,
+ "id": "14fdbae1-31b7-4b58-a7e7-a50589f0ff0d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "img.link(event_type=\"click\", target=temporal_coll, feature=\"colors\", new_data=white_temporal, indices_mapper=indices_mapper_temporal)"
+ ]
+ }
+ ],
+ "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/lineplot.ipynb b/examples/lineplot.ipynb
index 7561efe88..d00346daf 100644
--- a/examples/lineplot.ipynb
+++ b/examples/lineplot.ipynb
@@ -178,7 +178,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
- "version": "3.10.5"
+ "version": "3.9.2"
}
},
"nbformat": 4,
diff --git a/examples/single_contour_event.ipynb b/examples/single_contour_event.ipynb
new file mode 100644
index 000000000..3c88b72c8
--- /dev/null
+++ b/examples/single_contour_event.ipynb
@@ -0,0 +1,251 @@
+{
+ "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": 2,
+ "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": 3,
+ "id": "650279ac-e7df-4c6f-aac1-078ae4287028",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "contours = pickle.load(open(\"/home/caitlin/Downloads/contours.pickle\", \"rb\"))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 4,
+ "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": 5,
+ "id": "776e916f-16c9-4114-b1ff-7ea209aa7b04",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "ef7e51b0da07486faf42b012582be35e",
+ "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": 6,
+ "id": "203768f0-6bb4-4ba9-b099-395f2bdd2a8c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "plot.add_graphic(single_contour)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 7,
+ "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": "9b0cb33f7b674585b2f5f58c7d1af28f",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "JupyterWgpuCanvas()"
+ ]
+ },
+ "execution_count": 7,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "plot.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 8,
+ "id": "dbcda1d6-3f21-4a5e-b60f-75bf9103fbe6",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "white = np.ones(shape=single_contour.colors.shape)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 10,
+ "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": 12,
+ "id": "712c3f43-3339-4d1b-9d64-fe4f4d6bd672",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "{'click': [CallbackData(target=fastplotlib.LineGraphic @ 0x7ff9ce507d30, 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.]]), old_data=array([[0. , 0. , 0.5 , 1. ],\n",
+ " [0. , 0. , 0.6604278 , 1. ],\n",
+ " [0. , 0. , 0.8386809 , 1. ],\n",
+ " [0. , 0. , 1. , 1. ],\n",
+ " [0. , 0.11176471, 1. , 1. ],\n",
+ " [0. , 0.26862746, 1. , 1. ],\n",
+ " [0. , 0.40980393, 1. , 1. ],\n",
+ " [0. , 0.56666666, 1. , 1. ],\n",
+ " [0. , 0.7235294 , 1. , 1. ],\n",
+ " [0. , 0.88039213, 0.9835547 , 1. ],\n",
+ " [0.11068944, 1. , 0.8570525 , 1. ],\n",
+ " [0.22454143, 1. , 0.7432005 , 1. ],\n",
+ " [0.35104364, 1. , 0.61669827, 1. ],\n",
+ " [0.47754586, 1. , 0.49019608, 1. ],\n",
+ " [0.6040481 , 1. , 0.36369386, 1. ],\n",
+ " [0.7305503 , 1. , 0.23719165, 1. ],\n",
+ " [0.84440225, 1. , 0.12333966, 1. ],\n",
+ " [0.97090447, 0.95933187, 0. , 1. ],\n",
+ " [1. , 0.8140886 , 0. , 1. ],\n",
+ " [1. , 0.6688453 , 0. , 1. ],\n",
+ " [1. , 0.523602 , 0. , 1. ],\n",
+ " [1. , 0.3928831 , 0. , 1. ],\n",
+ " [1. , 0.24763979, 0. , 1. ],\n",
+ " [1. , 0.10239651, 0. , 1. ],\n",
+ " [0.8565062 , 0. , 0. , 1. ],\n",
+ " [0.6782531 , 0. , 0. , 1. ],\n",
+ " [0.5 , 0. , 0. , 1. ]], dtype=float32))]}"
+ ]
+ },
+ "execution_count": 12,
+ "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/__init__.py b/fastplotlib/graphics/__init__.py
index cad6de8c7..6294637aa 100644
--- a/fastplotlib/graphics/__init__.py
+++ b/fastplotlib/graphics/__init__.py
@@ -12,6 +12,6 @@
"LineGraphic",
"HistogramGraphic",
"HeatmapGraphic",
- "LineCollection",
- "TextGraphic"
+ "TextGraphic",
+ "LineCollection"
]
diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py
index a1a2633b9..b729eb2a5 100644
--- a/fastplotlib/graphics/_base.py
+++ b/fastplotlib/graphics/_base.py
@@ -1,10 +1,13 @@
from typing import *
+import numpy as np
import pygfx
from ..utils import get_colors
from .features import GraphicFeature, DataFeature, ColorFeature, PresentFeature
+from abc import ABC, abstractmethod
+from dataclasses import dataclass
class Graphic:
def __init__(
@@ -46,6 +49,7 @@ def __init__(
self.colors = None
self.name = name
+ self.registered_callbacks = dict()
if n_colors is None:
n_colors = self.data.feature_data.shape[0]
@@ -60,23 +64,20 @@ def __init__(
# useful for bbox calculations to ignore these Graphics
self.present = PresentFeature(parent=self)
- valid_features = ["visible"]
+ #valid_features = ["visible"]
+ self._feature_events = list()
for attr_name in self.__dict__.keys():
attr = getattr(self, attr_name)
if isinstance(attr, GraphicFeature):
- valid_features.append(attr_name)
+ self._feature_events.append(attr_name)
- self._valid_features = tuple(valid_features)
+ self._feature_events = tuple(self._feature_events)
+ self._pygfx_events = ("click",)
@property
def world_object(self) -> pygfx.WorldObject:
return self._world_object
- @property
- def interact_features(self) -> Tuple[str]:
- """The features for this ``Graphic`` that support interaction."""
- return self._valid_features
-
@property
def visible(self) -> bool:
return self.world_object.visible
@@ -104,3 +105,55 @@ 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, indices_mapper: 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:
+ feature = getattr(self, event_type)
+ feature.add_event_handler(self.event_handler, event_type)
+ 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, indices_mapper=indices_mapper))
+ else:
+ self.registered_callbacks[event_type] = list()
+ self.registered_callbacks[event_type].append(
+ CallbackData(target=target, feature=feature, new_data=new_data, indices_mapper=indices_mapper))
+
+ def event_handler(self, event):
+ event_info = event.pick_info
+ #click_info = np.array(event.pick_info["index"])
+ if event.type in self.registered_callbacks.keys():
+ for target_info in self.registered_callbacks[event.type]:
+ if target_info.indices_mapper is not None:
+ indices = target_info.indices_mapper(source=self, target=target_info.target, indices=click_info)
+ else:
+ indices = None
+ # set feature of target at indice using new data
+ target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, indices=indices)
+
+@dataclass
+class CallbackData:
+ """Class for keeping track of the info necessary for interactivity after event occurs."""
+ target: Any
+ feature: str
+ new_data: Any
+ indices_mapper: callable = None
+
+@dataclass
+class PreviouslyModifiedData:
+ """Class for keeping track of previously modified data at indices"""
+ previous_data: Any
+ previous_indices: Any
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 77c531c8a..ae7e47ca6 100644
--- a/fastplotlib/graphics/image.py
+++ b/fastplotlib/graphics/image.py
@@ -3,11 +3,11 @@
import numpy as np
import pygfx
-from ._base import Graphic
+from ._base import Graphic, Interaction
from ..utils import quick_min_max, get_cmap_texture
-class ImageGraphic(Graphic):
+class ImageGraphic(Graphic, Interaction):
def __init__(
self,
data: Any,
@@ -72,6 +72,12 @@ def __init__(
pygfx.ImageBasicMaterial(clim=(vmin, vmax), map=get_cmap_texture(cmap))
)
+ def _set_feature(self, feature: str, new_data: Any, indices: Any):
+ pass
+
+ def _reset_feature(self, feature: str, old_data: Any):
+ pass
+
@property
def clim(self) -> Tuple[float, float]:
return self.world_object.material.clim
diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py
index edf99e43c..32df62efc 100644
--- a/fastplotlib/graphics/line.py
+++ b/fastplotlib/graphics/line.py
@@ -2,10 +2,10 @@
import numpy as np
import pygfx
-from ._base import Graphic
+from ._base import Graphic, Interaction, PreviouslyModifiedData
-class LineGraphic(Graphic):
+class LineGraphic(Graphic, Interaction):
def __init__(
self,
data: Any,
@@ -18,25 +18,19 @@ def __init__(
):
"""
Create a line Graphic, 2d or 3d
-
Parameters
----------
data: array-like
Line data to plot, 2D must be of shape [n_points, 2], 3D must be of shape [n_points, 3]
-
z_position: float, optional
z-axis position for placing the graphic
-
size: float, optional
thickness of the line
-
colors: str, array, or iterable
specify colors as a single human readable string, a single RGBA array,
or an iterable of strings or RGBA arrays
-
cmap: str, optional
apply a colormap to the line instead of assigning colors manually
-
args
passed to Graphic
kwargs
@@ -59,3 +53,36 @@ def __init__(
)
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 = {}
+ elif hasattr(self, "_previous_data"):
+ self._reset_feature(feature)
+ if feature in self._feature_events:
+ feature_instance = getattr(self, feature)
+ if indices is not None:
+ previous = feature_instance[indices].copy()
+ feature_instance[indices] = new_data
+ else:
+ previous = feature_instance[:].copy()
+ feature_instance[:] = new_data
+ if feature in self._previous_data.keys():
+ self._previous_data[feature].previous_data = previous
+ self._previous_data[feature].previous_indices = indices
+ else:
+ self._previous_data[feature] = PreviouslyModifiedData(previous_data=previous, previous_indices=indices)
+ else:
+ raise ValueError("name arg is not a valid feature")
+
+
+ def _reset_feature(self, feature: str):
+ if feature not in self._previous_data.keys():
+ raise ValueError("no previous data registered for this feature")
+ else:
+ feature_instance = getattr(self, feature)
+ if self._previous_data[feature].previous_indices is not None:
+ feature_instance[self._previous_data[feature].previous_indices] = self._previous_data[feature].previous_data
+ else:
+ feature_instance[:] = self._previous_data[feature].previous_data
+
diff --git a/fastplotlib/graphics/linecollection.py b/fastplotlib/graphics/linecollection.py
index ec4b1e4dd..7f0d24a33 100644
--- a/fastplotlib/graphics/linecollection.py
+++ b/fastplotlib/graphics/linecollection.py
@@ -1,13 +1,22 @@
import numpy as np
import pygfx
-from typing import Union
-from .line import LineGraphic
+from typing import Union, List
+
+from fastplotlib.graphics.line import LineGraphic
from typing import *
+from fastplotlib.graphics._base import Interaction
+from abc import ABC, abstractmethod
+class LineCollection:
+ def __init__(self, data: List[np.ndarray],
+ z_position: Union[List[float], float] = None,
+ size: Union[float, List[float]] = 2.0,
+ colors: Union[List[np.ndarray], np.ndarray] = None,
+ cmap: Union[List[str], str] = None,
+ *args,
+ **kwargs):
-class LineCollection():
- def __init__(self, data: List[np.ndarray], z_position: Union[List[float], float] = None, size: Union[float, List[float]] = 2.0, colors: Union[List[np.ndarray], np.ndarray] = None,
- cmap: Union[List[str], str] = None, *args, **kwargs):
+ self.name = None
if not isinstance(z_position, float) and z_position is not None:
if not len(data) == len(z_position):
@@ -22,7 +31,8 @@ def __init__(self, data: List[np.ndarray], z_position: Union[List[float], float]
if not len(data) == len(cmap):
raise ValueError("args must be the same length")
- self.collection = list()
+ self.data = list()
+ self._world_object = pygfx.Group()
for i, d in enumerate(data):
if isinstance(z_position, list):
@@ -45,10 +55,33 @@ def __init__(self, data: List[np.ndarray], z_position: Union[List[float], float]
else:
_cmap = cmap
- self.collection.append(LineGraphic(d, _z, _size, _colors, _cmap))
+ lg = LineGraphic(d, _z, _size, _colors, _cmap)
+ self.data.append(lg)
+ self._world_object.add(lg.world_object)
+
+ # TODO: make a base class for Collection graphics and put this as a base method
+ @property
+ def world_object(self) -> pygfx.WorldObject:
+ return self._world_object
+
+ def _set_feature(self, feature: str, new_data: Any, indices: Any):
+ if feature in self.features:
+ update_func = getattr(self.data[indices], f"update_{feature}")
+ # if indices is a single indices or list of indices
+ self.data[indices].update_colors(new_data)
+ else:
+ raise ValueError("name arg is not a valid feature")
+
+ def _reset_feature(self, feature: str, old_data: Any):
+ if feature in self.features:
+ #update_func = getattr(self, f"update_{feature}")
+ for i, line in enumerate(self.data):
+ line.update_colors(old_data[i])
+ else:
+ raise ValueError("name arg is not a valid feature")
def __getitem__(self, item):
- return self.collection[item]
+ return self.data[item]