From 6f63f64117a04ffb3e5bf3c93d3e24464036713e Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 11 Dec 2022 21:25:32 -0500 Subject: [PATCH 01/13] beginning base logic for interactivity impl --- examples/event_handler.ipynb | 391 ++++++++++++++++++++++++++++ examples/single_contour_event.ipynb | 251 ++++++++++++++++++ fastplotlib/graphics/_base.py | 30 +++ 3 files changed, 672 insertions(+) create mode 100644 examples/event_handler.ipynb create mode 100644 examples/single_contour_event.ipynb diff --git a/examples/event_handler.ipynb b/examples/event_handler.ipynb new file mode 100644 index 000000000..a96977c9f --- /dev/null +++ b/examples/event_handler.ipynb @@ -0,0 +1,391 @@ +{ + "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": {}, + "outputs": [], + "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", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + " \n", + "
algoitem_nameinput_movie_pathparamsoutputscommentsuuid
0mcorrmy_movieexample_movies/Sue_2x_3000_40_-46.tif{'main': {'max_shifts': (24, 24), 'strides': (...{'mean-projection-path': 1ed8feb3-9fc8-4a78-8f...None1ed8feb3-9fc8-4a78-8f6d-164620822016
1cnmfmy_movie1ed8feb3-9fc8-4a78-8f6d-164620822016/1ed8feb3-...{'main': {'fr': 30, 'p': 1, 'nb': 2, 'merge_th...{'mean-projection-path': 5f4e3e27-ac1f-4ede-90...None5f4e3e27-ac1f-4ede-903b-be43bd81fddc
\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/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/_base.py b/fastplotlib/graphics/_base.py index 3cf358451..a54cd6920 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -79,3 +79,33 @@ def __repr__(self): return f"'{self.name}' fastplotlib.{self.__class__.__name__} @ {hex(id(self))}" else: return f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}" +<<<<<<< HEAD +======= + +class Interaction: + # make them abstract properties + @property + def indices(self) -> Any: + pass + + @indices.setter + def indices(self, indices: Any): + pass + + @property + def features(self) -> List[str]: + pass + + def _set_feature(self, name: str, new_data: Any, indices: Any): + pass + + def link(self, event: str, feature: Any, feature_data: Any, target: Graphic, target_feature: Any, target_data: Any, indices_mapper: Any): + # event occurs, causes change in feature of current graphic to data indices from pick_info, + # also causes change in target graphic to target feature at target data with corresponding or mapped + # indices based on the indice_mapper function + + # indice mapper takes in source features and maps to target features + pass + + +>>>>>>> 2c00596 (beginning base logic for interactivity impl) From 2ccb19b393d0e23848f06993fbfc8b878770b03b Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Sun, 11 Dec 2022 21:38:43 -0500 Subject: [PATCH 02/13] further changes and ideas --- fastplotlib/graphics/_base.py | 10 +++++++++- fastplotlib/graphics/heatmap.py | 22 +++++++++++++++++++++- 2 files changed, 30 insertions(+), 2 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a54cd6920..f0cbc3bf5 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -5,6 +5,7 @@ from .features import GraphicFeature, PresentFeature +from abc import ABC, abstractmethod class BaseGraphic: def __init_subclass__(cls, **kwargs): @@ -82,28 +83,35 @@ def __repr__(self): <<<<<<< HEAD ======= -class Interaction: +class Interaction(ABC): # make them abstract properties @property + @abstractmethod def indices(self) -> Any: pass @indices.setter + @abstractmethod def indices(self, indices: Any): pass @property + @abstractmethod def features(self) -> List[str]: pass + @abstractmethod def _set_feature(self, name: str, new_data: Any, indices: Any): pass + @abstractmethod def link(self, event: str, feature: Any, feature_data: Any, target: Graphic, target_feature: Any, target_data: Any, indices_mapper: Any): # event occurs, causes change in feature of current graphic to data indices from pick_info, # also causes change in target graphic to target feature at target data with corresponding or mapped # indices based on the indice_mapper function + # events can be feature changes, when feature changes want to trigger an event + # indice mapper takes in source features and maps to target features pass diff --git a/fastplotlib/graphics/heatmap.py b/fastplotlib/graphics/heatmap.py index 2c33564db..3a0510382 100644 --- a/fastplotlib/graphics/heatmap.py +++ b/fastplotlib/graphics/heatmap.py @@ -1,10 +1,15 @@ import numpy as np import pygfx from typing import * + +from fastplotlib.graphics._base import Graphic + from .image import ImageGraphic from ..utils import quick_min_max, get_cmap_texture +from ._base import Interaction + default_selection_options = { "mode": "single", @@ -40,7 +45,22 @@ def __init__( self.callbacks = callbacks -class HeatmapGraphic(ImageGraphic): +class HeatmapGraphic(ImageGraphic, Interaction): + @property + def indices(self) -> Any: + pass + + @property + def features(self) -> List[str]: + pass + + def _set_feature(self, name: str, new_data: Any, indices: Any): + pass + + def link(self, event: str, feature: Any, feature_data: Any, target: Graphic, target_feature: Any, target_data: Any, + indices_mapper: Any): + pass + def __init__( self, data: np.ndarray, From 835197a0c72c1507f749c73c49e22b607eec79e0 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 14 Dec 2022 21:25:49 -0500 Subject: [PATCH 03/13] oops --- examples/event_single_contour.ipynb | 225 ++++++++++++++++++++++++++++ 1 file changed, 225 insertions(+) create mode 100644 examples/event_single_contour.ipynb diff --git a/examples/event_single_contour.ipynb b/examples/event_single_contour.ipynb new file mode 100644 index 000000000..dcb23a9e2 --- /dev/null +++ b/examples/event_single_contour.ipynb @@ -0,0 +1,225 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "2d14adc9-1ba0-4a5c-ab7d-d31d5fad238d", + "metadata": {}, + "source": [ + "# working on event interactivity for a single contour" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "id": "a934868b-3792-48da-83e7-793350545a09", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "id": "4420c0dc-23f9-4405-bc0b-b2b745061326", + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "2022-12-14 21:15:10.071022: 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-14 21:15:10.183652: 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-14 21:15:10.196607: 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-14 21:15:10.196623: 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-14 21:15:10.703225: 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-14 21:15:10.703287: 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-14 21:15:10.703292: 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" + ] + } + ], + "source": [ + "import numpy as np\n", + "from fastplotlib.plot import Plot\n", + "from fastplotlib.graphics import LineGraphic\n", + "import pygfx\n", + "import pickle\n", + "from mesmerize_core import *\n", + "import tifffile" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "id": "3eb77356-5314-4004-ac26-0628a19c038b", + "metadata": {}, + "outputs": [], + "source": [ + "contours = pickle.load(open(\"/home/caitlin/Downloads/contours.pickle\", \"rb\"))" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "id": "0b2090b9-9fe6-407e-b517-2b8b28d38d2e", + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "155" + ] + }, + "execution_count": 4, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "len(contours)" + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "id": "90373974-c3a1-468e-8cf2-8c31061284a6", + "metadata": {}, + "outputs": [], + "source": [ + "single_contour = LineGraphic(data=contours[0], size=10.0)" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "id": "df16cf45-a767-40ce-a1f5-efac891b57f5", + "metadata": {}, + "outputs": [ + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "15944fb7c4d34bd28fa6ef1a73965b1d", + "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": 7, + "id": "038eb7d8-f4cf-4d0e-8aa5-1e52c65d0fe3", + "metadata": {}, + "outputs": [], + "source": [ + "plot.add_graphic(single_contour)" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "id": "c6ffada3-4b52-4a54-815e-d07581240433", + "metadata": {}, + "outputs": [ + { + "data": { + "text/html": [ + "
initial snapshot
" + ], + "text/plain": [ + "" + ] + }, + "metadata": {}, + "output_type": "display_data" + }, + { + "data": { + "application/vnd.jupyter.widget-view+json": { + "model_id": "1ad382c3d2c94b3bae8b66a0be34e7f7", + "version_major": 2, + "version_minor": 0 + }, + "text/plain": [ + "JupyterWgpuCanvas()" + ] + }, + "execution_count": 8, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "plot.show()" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "id": "73ab4a53-f26b-4668-8603-eba027449d82", + "metadata": {}, + "outputs": [], + "source": [ + "red = np.array([1.0, 1.0, 1.0, 1.0]) " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "id": "3803b5d4-0c1e-4b0d-803c-7bb769a310d2", + "metadata": {}, + "outputs": [], + "source": [ + "single_contour.link(event=\"click\", target=single_contour, feature=\"color\", new_data=red)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ba6339e2-247a-4ae6-b2a7-0f105c2511c9", + "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 +} From 0dd74aa1a3cb7d57708af5b5b8d5064d39b1bff9 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Mon, 19 Dec 2022 12:22:16 -0500 Subject: [PATCH 04/13] accidentally deleted heatmap file --- fastplotlib/graphics/heatmap.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/fastplotlib/graphics/heatmap.py b/fastplotlib/graphics/heatmap.py index 3a0510382..42afe0ad4 100644 --- a/fastplotlib/graphics/heatmap.py +++ b/fastplotlib/graphics/heatmap.py @@ -1,15 +1,21 @@ import numpy as np import pygfx from typing import * +<<<<<<< HEAD from fastplotlib.graphics._base import Graphic +======= +>>>>>>> c523c28 (accidentally deleted heatmap file) from .image import ImageGraphic from ..utils import quick_min_max, get_cmap_texture +<<<<<<< HEAD from ._base import Interaction +======= +>>>>>>> c523c28 (accidentally deleted heatmap file) default_selection_options = { "mode": "single", @@ -45,6 +51,7 @@ def __init__( self.callbacks = callbacks +<<<<<<< HEAD class HeatmapGraphic(ImageGraphic, Interaction): @property def indices(self) -> Any: @@ -61,6 +68,9 @@ def link(self, event: str, feature: Any, feature_data: Any, target: Graphic, tar indices_mapper: Any): pass +======= +class HeatmapGraphic(ImageGraphic): +>>>>>>> c523c28 (accidentally deleted heatmap file) def __init__( self, data: np.ndarray, @@ -73,12 +83,16 @@ def __init__( ): """ Create a Heatmap Graphic +<<<<<<< HEAD +======= +>>>>>>> c523c28 (accidentally deleted heatmap file) 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 +<<<<<<< HEAD vmin: int, optional minimum value for color scaling, calculated from data if not provided @@ -91,6 +105,15 @@ def __init__( selection_options +======= + 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 +>>>>>>> c523c28 (accidentally deleted heatmap file) args: additional arguments passed to Graphic kwargs: @@ -160,4 +183,8 @@ def add_highlight(self, event): self.world_object.add(self.selection_graphic) self._highlights.append(self.selection_graphic) +<<<<<<< HEAD + return rval +======= return rval +>>>>>>> c523c28 (accidentally deleted heatmap file) From cfa0aa9c664d4aa23f0b3116af1d80645a328514 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 28 Dec 2022 11:36:22 -0500 Subject: [PATCH 05/13] rebase --- fastplotlib/graphics/_base.py | 75 ++++++++++++++++---------- fastplotlib/graphics/heatmap.py | 55 +------------------ fastplotlib/graphics/image.py | 15 ------ fastplotlib/graphics/line.py | 36 ++++++++++++- fastplotlib/graphics/linecollection.py | 3 +- 5 files changed, 84 insertions(+), 100 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index f0cbc3bf5..f633b782f 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -29,6 +29,7 @@ def __init__( """ self.name = name + self.registered_callbacks = dict() self.present = PresentFeature(parent=self) valid_features = ["visible"] @@ -80,40 +81,58 @@ def __repr__(self): return f"'{self.name}' fastplotlib.{self.__class__.__name__} @ {hex(id(self))}" else: return f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}" -<<<<<<< HEAD -======= -class Interaction(ABC): - # make them abstract properties - @property - @abstractmethod - def indices(self) -> Any: - pass - - @indices.setter - @abstractmethod - def indices(self, indices: Any): - pass - - @property - @abstractmethod - def features(self) -> List[str]: - pass +class Interaction(ABC): @abstractmethod - def _set_feature(self, name: str, new_data: Any, indices: Any): + def _set_feature(self, feature: str, new_data: Any, indices: Any): pass @abstractmethod - def link(self, event: str, feature: Any, feature_data: Any, target: Graphic, target_feature: Any, target_data: Any, indices_mapper: Any): - # event occurs, causes change in feature of current graphic to data indices from pick_info, - # also causes change in target graphic to target feature at target data with corresponding or mapped - # indices based on the indice_mapper function - - # events can be feature changes, when feature changes want to trigger an event - - # indice mapper takes in source features and maps to target features + 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") ->>>>>>> 2c00596 (beginning base logic for interactivity impl) + 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 \ No newline at end of file diff --git a/fastplotlib/graphics/heatmap.py b/fastplotlib/graphics/heatmap.py index 42afe0ad4..103a0fc2e 100644 --- a/fastplotlib/graphics/heatmap.py +++ b/fastplotlib/graphics/heatmap.py @@ -1,21 +1,10 @@ import numpy as np import pygfx from typing import * -<<<<<<< HEAD - -from fastplotlib.graphics._base import Graphic - -======= ->>>>>>> c523c28 (accidentally deleted heatmap file) from .image import ImageGraphic from ..utils import quick_min_max, get_cmap_texture -<<<<<<< HEAD -from ._base import Interaction - -======= ->>>>>>> c523c28 (accidentally deleted heatmap file) default_selection_options = { "mode": "single", @@ -51,26 +40,7 @@ def __init__( self.callbacks = callbacks -<<<<<<< HEAD -class HeatmapGraphic(ImageGraphic, Interaction): - @property - def indices(self) -> Any: - pass - - @property - def features(self) -> List[str]: - pass - - def _set_feature(self, name: str, new_data: Any, indices: Any): - pass - - def link(self, event: str, feature: Any, feature_data: Any, target: Graphic, target_feature: Any, target_data: Any, - indices_mapper: Any): - pass - -======= class HeatmapGraphic(ImageGraphic): ->>>>>>> c523c28 (accidentally deleted heatmap file) def __init__( self, data: np.ndarray, @@ -83,29 +53,11 @@ def __init__( ): """ Create a Heatmap Graphic -<<<<<<< HEAD - -======= ->>>>>>> c523c28 (accidentally deleted heatmap file) 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 -<<<<<<< HEAD - - 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 - -======= vmin: int, optional minimum value for color scaling, calculated from data if not provided vmax: int, optional @@ -113,7 +65,6 @@ def __init__( cmap: str, optional colormap to use to display the image data, default is ``"plasma"`` selection_options ->>>>>>> c523c28 (accidentally deleted heatmap file) args: additional arguments passed to Graphic kwargs: @@ -183,8 +134,4 @@ def add_highlight(self, event): self.world_object.add(self.selection_graphic) self._highlights.append(self.selection_graphic) -<<<<<<< HEAD - return rval -======= - return rval ->>>>>>> c523c28 (accidentally deleted heatmap file) + return rval \ No newline at end of file diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 48459c63e..25cbc0aa6 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -20,50 +20,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) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index cb0224007..a8462076b 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -2,12 +2,12 @@ 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): def __init__( self, data: Any, @@ -83,3 +83,35 @@ 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 = {} + 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 \ No newline at end of file diff --git a/fastplotlib/graphics/linecollection.py b/fastplotlib/graphics/linecollection.py index 49b123028..58eea19ca 100644 --- a/fastplotlib/graphics/linecollection.py +++ b/fastplotlib/graphics/linecollection.py @@ -2,13 +2,14 @@ import pygfx from typing import * +from ._base import Interaction, PreviouslyModifiedData from ._collection import GraphicCollection from .line import LineGraphic from ..utils import get_colors from typing import * -class LineCollection(GraphicCollection): +class LineCollection(GraphicCollection, Interaction): """Line Collection graphic""" child_type = LineGraphic From a39ceaa9d97fb6366444d2e26003f9e5389081c3 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 28 Dec 2022 11:50:50 -0500 Subject: [PATCH 06/13] further rebase --- examples/event_single_contour.ipynb | 225 ------------------------- fastplotlib/graphics/_base.py | 9 +- fastplotlib/graphics/linecollection.py | 6 + 3 files changed, 12 insertions(+), 228 deletions(-) delete mode 100644 examples/event_single_contour.ipynb diff --git a/examples/event_single_contour.ipynb b/examples/event_single_contour.ipynb deleted file mode 100644 index dcb23a9e2..000000000 --- a/examples/event_single_contour.ipynb +++ /dev/null @@ -1,225 +0,0 @@ -{ - "cells": [ - { - "cell_type": "markdown", - "id": "2d14adc9-1ba0-4a5c-ab7d-d31d5fad238d", - "metadata": {}, - "source": [ - "# working on event interactivity for a single contour" - ] - }, - { - "cell_type": "code", - "execution_count": 1, - "id": "a934868b-3792-48da-83e7-793350545a09", - "metadata": {}, - "outputs": [], - "source": [ - "%load_ext autoreload\n", - "%autoreload 2" - ] - }, - { - "cell_type": "code", - "execution_count": 2, - "id": "4420c0dc-23f9-4405-bc0b-b2b745061326", - "metadata": {}, - "outputs": [ - { - "name": "stderr", - "output_type": "stream", - "text": [ - "2022-12-14 21:15:10.071022: 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-14 21:15:10.183652: 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-14 21:15:10.196607: 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-14 21:15:10.196623: 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-14 21:15:10.703225: 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-14 21:15:10.703287: 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-14 21:15:10.703292: 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" - ] - } - ], - "source": [ - "import numpy as np\n", - "from fastplotlib.plot import Plot\n", - "from fastplotlib.graphics import LineGraphic\n", - "import pygfx\n", - "import pickle\n", - "from mesmerize_core import *\n", - "import tifffile" - ] - }, - { - "cell_type": "code", - "execution_count": 3, - "id": "3eb77356-5314-4004-ac26-0628a19c038b", - "metadata": {}, - "outputs": [], - "source": [ - "contours = pickle.load(open(\"/home/caitlin/Downloads/contours.pickle\", \"rb\"))" - ] - }, - { - "cell_type": "code", - "execution_count": 4, - "id": "0b2090b9-9fe6-407e-b517-2b8b28d38d2e", - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "155" - ] - }, - "execution_count": 4, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "len(contours)" - ] - }, - { - "cell_type": "code", - "execution_count": 5, - "id": "90373974-c3a1-468e-8cf2-8c31061284a6", - "metadata": {}, - "outputs": [], - "source": [ - "single_contour = LineGraphic(data=contours[0], size=10.0)" - ] - }, - { - "cell_type": "code", - "execution_count": 6, - "id": "df16cf45-a767-40ce-a1f5-efac891b57f5", - "metadata": {}, - "outputs": [ - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "15944fb7c4d34bd28fa6ef1a73965b1d", - "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": 7, - "id": "038eb7d8-f4cf-4d0e-8aa5-1e52c65d0fe3", - "metadata": {}, - "outputs": [], - "source": [ - "plot.add_graphic(single_contour)" - ] - }, - { - "cell_type": "code", - "execution_count": 8, - "id": "c6ffada3-4b52-4a54-815e-d07581240433", - "metadata": {}, - "outputs": [ - { - "data": { - "text/html": [ - "
initial snapshot
" - ], - "text/plain": [ - "" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "application/vnd.jupyter.widget-view+json": { - "model_id": "1ad382c3d2c94b3bae8b66a0be34e7f7", - "version_major": 2, - "version_minor": 0 - }, - "text/plain": [ - "JupyterWgpuCanvas()" - ] - }, - "execution_count": 8, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "plot.show()" - ] - }, - { - "cell_type": "code", - "execution_count": 9, - "id": "73ab4a53-f26b-4668-8603-eba027449d82", - "metadata": {}, - "outputs": [], - "source": [ - "red = np.array([1.0, 1.0, 1.0, 1.0]) " - ] - }, - { - "cell_type": "code", - "execution_count": 10, - "id": "3803b5d4-0c1e-4b0d-803c-7bb769a310d2", - "metadata": {}, - "outputs": [], - "source": [ - "single_contour.link(event=\"click\", target=single_contour, feature=\"color\", new_data=red)" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "id": "ba6339e2-247a-4ae6-b2a7-0f105c2511c9", - "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 f633b782f..33cfb4104 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -6,6 +6,7 @@ from .features import GraphicFeature, PresentFeature from abc import ABC, abstractmethod +from dataclasses import dataclass class BaseGraphic: def __init_subclass__(cls, **kwargs): @@ -32,13 +33,15 @@ def __init__( self.registered_callbacks = dict() 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) -> WorldObject: diff --git a/fastplotlib/graphics/linecollection.py b/fastplotlib/graphics/linecollection.py index 58eea19ca..82f023f31 100644 --- a/fastplotlib/graphics/linecollection.py +++ b/fastplotlib/graphics/linecollection.py @@ -114,3 +114,9 @@ def __init__( ) self.add_graphic(lg, reset_index=False) + + def _set_feature(self, feature: str, new_data: Any, indices: Any): + pass + + def _reset_feature(self, feature: str): + pass From 557c611bf6b17bee3e11a550e628d0e2b7047f03 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 28 Dec 2022 12:38:58 -0500 Subject: [PATCH 07/13] single contour example --- examples/single_contour_event.ipynb | 60 ++++++++--------------------- 1 file changed, 17 insertions(+), 43 deletions(-) diff --git a/examples/single_contour_event.ipynb b/examples/single_contour_event.ipynb index 3c88b72c8..a4b7f1f91 100644 --- a/examples/single_contour_event.ipynb +++ b/examples/single_contour_event.ipynb @@ -13,7 +13,7 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 1, "id": "589eaea4-e749-46ff-ac3d-e22aa4f75641", "metadata": {}, "outputs": [], @@ -26,7 +26,7 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 2, "id": "650279ac-e7df-4c6f-aac1-078ae4287028", "metadata": {}, "outputs": [], @@ -36,7 +36,7 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 3, "id": "49dc6123-39d8-4f60-b14b-9cfd9a008940", "metadata": {}, "outputs": [], @@ -46,14 +46,14 @@ }, { "cell_type": "code", - "execution_count": 5, + "execution_count": 4, "id": "776e916f-16c9-4114-b1ff-7ea209aa7b04", "metadata": {}, "outputs": [ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "ef7e51b0da07486faf42b012582be35e", + "model_id": "b9c85f639ca34c6c882396fc4753818b", "version_major": 2, "version_minor": 0 }, @@ -79,7 +79,7 @@ }, { "cell_type": "code", - "execution_count": 6, + "execution_count": 5, "id": "203768f0-6bb4-4ba9-b099-395f2bdd2a8c", "metadata": {}, "outputs": [], @@ -89,14 +89,14 @@ }, { "cell_type": "code", - "execution_count": 7, + "execution_count": 6, "id": "4e46d687-d81a-4b6f-bece-c9edf3606d4f", "metadata": {}, "outputs": [ { "data": { "text/html": [ - "
initial snapshot
" + "
initial snapshot
" ], "text/plain": [ "" @@ -108,7 +108,7 @@ { "data": { "application/vnd.jupyter.widget-view+json": { - "model_id": "9b0cb33f7b674585b2f5f58c7d1af28f", + "model_id": "730d202ce0424559b30cd4d7d5f3b77b", "version_major": 2, "version_minor": 0 }, @@ -116,7 +116,7 @@ "JupyterWgpuCanvas()" ] }, - "execution_count": 7, + "execution_count": 6, "metadata": {}, "output_type": "execute_result" } @@ -127,17 +127,17 @@ }, { "cell_type": "code", - "execution_count": 8, + "execution_count": 7, "id": "dbcda1d6-3f21-4a5e-b60f-75bf9103fbe6", "metadata": {}, "outputs": [], "source": [ - "white = np.ones(shape=single_contour.colors.shape)" + "white = np.ones(shape=single_contour.colors.feature_data.shape)" ] }, { "cell_type": "code", - "execution_count": 10, + "execution_count": 8, "id": "6db453cf-5856-4a7b-879f-83032cb9e9ac", "metadata": {}, "outputs": [], @@ -147,14 +147,14 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 9, "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", + "{'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", @@ -180,36 +180,10 @@ " [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))]}" + " [1., 1., 1., 1.]]), indices_mapper=None)]}" ] }, - "execution_count": 12, + "execution_count": 9, "metadata": {}, "output_type": "execute_result" } From 76a82f187f8611d159b14da2066ac7456e33b81c Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 28 Dec 2022 15:43:53 -0500 Subject: [PATCH 08/13] interactivity modification --- fastplotlib/graphics/_base.py | 43 ++++++++++++-------------- fastplotlib/graphics/image.py | 19 ++++++++++-- fastplotlib/graphics/line.py | 28 ++++++++--------- fastplotlib/graphics/linecollection.py | 38 ++++++++++++++++++++--- 4 files changed, 84 insertions(+), 44 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 33cfb4104..a7d45ff6d 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -4,6 +4,7 @@ from pygfx.linalg import Vector3 from .features import GraphicFeature, PresentFeature +#from ._collection import GraphicCollection from abc import ABC, abstractmethod from dataclasses import dataclass @@ -16,6 +17,10 @@ def __init_subclass__(cls, **kwargs): class Graphic(BaseGraphic): + pygfx_events = [ + "click" + ] + def __init__( self, name: str = None @@ -33,16 +38,6 @@ def __init__( self.registered_callbacks = dict() self.present = PresentFeature(parent=self) - #valid_features = ["visible"] - self._feature_events = list() - for attr_name in self.__dict__.keys(): - attr = getattr(self, attr_name) - if isinstance(attr, GraphicFeature): - self._feature_events.append(attr_name) - - self._feature_events = tuple(self._feature_events) - self._pygfx_events = ("click",) - @property def world_object(self) -> WorldObject: return self._world_object @@ -95,10 +90,10 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any): 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: + 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: + elif event_type in self.feature_events: feature = getattr(self, event_type) feature.add_event_handler(self.event_handler, event_type) else: @@ -106,24 +101,24 @@ def link(self, event_type: str, target: Any, feature: str, new_data: Any, indice 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)) + 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, indices_mapper=indices_mapper)) + CallbackData(target=target, feature=feature, new_data=new_data, callback_function=callback_function)) 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) + 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): + # indices = event.pick_info["collection_index"] + # target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, indices=indices) 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) - + target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, + indices=None) @dataclass class CallbackData: @@ -131,7 +126,7 @@ class CallbackData: target: Any feature: str new_data: Any - indices_mapper: callable = None + callback_function: callable = None @dataclass diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 25cbc0aa6..805588252 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -2,12 +2,17 @@ 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-changed", + "color-changed", + "cmap-changed", + ] def __init__( self, data: Any, @@ -88,3 +93,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 a8462076b..84827a30f 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -89,21 +89,21 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any = None): 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) + # 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: - raise ValueError("name arg is not a valid feature") + 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(): diff --git a/fastplotlib/graphics/linecollection.py b/fastplotlib/graphics/linecollection.py index 82f023f31..6c1533557 100644 --- a/fastplotlib/graphics/linecollection.py +++ b/fastplotlib/graphics/linecollection.py @@ -12,7 +12,11 @@ class LineCollection(GraphicCollection, Interaction): """Line Collection graphic""" child_type = LineGraphic - + feature_events = [ + "data-changed", + "color-changed", + "cmap-changed", + ] def __init__( self, data: List[np.ndarray], @@ -115,8 +119,34 @@ def __init__( self.add_graphic(lg, reset_index=False) - def _set_feature(self, feature: str, new_data: Any, indices: Any): - pass + def _set_feature(self, feature: str, new_data: Any, indices: Union[int, range]): + if not hasattr(self, "_previous_data"): + self._previous_data = {} + elif hasattr(self, "_previous_data"): + self._reset_feature(feature) + #if feature in # need a way to check if feature is in + 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): - pass + 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 \ No newline at end of file From af8cd2ea5b2cd12f51e4d51493177f637727e894 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Wed, 28 Dec 2022 15:54:17 -0500 Subject: [PATCH 09/13] refactoring classes --- fastplotlib/graphics/_base.py | 179 +++++++++++++++++++++- fastplotlib/graphics/_collection.py | 172 --------------------- fastplotlib/graphics/features/__init__.py | 2 +- fastplotlib/graphics/linecollection.py | 3 +- 4 files changed, 174 insertions(+), 182 deletions(-) delete mode 100644 fastplotlib/graphics/_collection.py diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index a7d45ff6d..ba347484e 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,10 +1,11 @@ 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 ._collection import GraphicCollection +from .features import GraphicFeature, PresentFeature, GraphicFeatureIndexable from abc import ABC, abstractmethod from dataclasses import dataclass @@ -113,9 +114,9 @@ def event_handler(self, event): 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): - # indices = event.pick_info["collection_index"] - # target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, indices=indices) + elif isinstance(self, GraphicCollection): + indices = event.pick_info["collection_index"] + target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, indices=indices) else: target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, indices=None) @@ -133,4 +134,168 @@ class CallbackData: class PreviouslyModifiedData: """Class for keeping track of previously modified data at indices""" previous_data: Any - previous_indices: Any \ No newline at end of file + previous_indices: Any + + +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/_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/linecollection.py b/fastplotlib/graphics/linecollection.py index 6c1533557..caaedf10f 100644 --- a/fastplotlib/graphics/linecollection.py +++ b/fastplotlib/graphics/linecollection.py @@ -2,8 +2,7 @@ import pygfx from typing import * -from ._base import Interaction, PreviouslyModifiedData -from ._collection import GraphicCollection +from ._base import Interaction, PreviouslyModifiedData, GraphicCollection from .line import LineGraphic from ..utils import get_colors from typing import * From 0517d5c6b74953b491a74676ec04f03bc9e358ac Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 29 Dec 2022 13:39:58 -0500 Subject: [PATCH 10/13] single contour updates/fixes --- fastplotlib/graphics/_base.py | 4 +++- fastplotlib/graphics/image.py | 5 ++--- fastplotlib/graphics/line.py | 28 ++++++++++++++-------------- 3 files changed, 19 insertions(+), 18 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index ba347484e..27bf36220 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -114,10 +114,12 @@ def event_handler(self, event): 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): + elif isinstance(target_info.target, GraphicCollection): + # if target is a GraphicCollection, then indices will be stored in collection_index indices = event.pick_info["collection_index"] 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) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 805588252..2826d4804 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -9,9 +9,8 @@ class ImageGraphic(Graphic, Interaction): feature_events = [ - "data-changed", - "color-changed", - "cmap-changed", + "data", + "cmap", ] def __init__( self, diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 84827a30f..ab32d3ea5 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -8,6 +8,11 @@ class LineGraphic(Graphic, Interaction): + feature_events = [ + "data", + "colors", + "cmap", + ] def __init__( self, data: Any, @@ -86,32 +91,27 @@ def __init__( def _set_feature(self, feature: str, new_data: Any, indices: Any = None): if not hasattr(self, "_previous_data"): - self._previous_data = {} + self._previous_data = dict() 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 + previous = feature_instance._data.copy() + feature_instance._set(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") + prev_ixs = self._previous_data[feature].previous_indices + feature_instance = getattr(self, feature) + if prev_ixs is not None: + feature_instance[prev_ixs] = self._previous_data[feature].previous_data 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 \ No newline at end of file + feature_instance._set(self._previous_data[feature].previous_data) \ No newline at end of file From e2eb1c8787eb95c48d410d7465da369401b9f40e Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 29 Dec 2022 15:32:39 -0500 Subject: [PATCH 11/13] line collection, fixing feature event names --- examples/line_collection_event.ipynb | 212 ++++++++++++++++++++++ fastplotlib/graphics/_base.py | 11 +- fastplotlib/graphics/features/_base.py | 2 +- fastplotlib/graphics/features/_colors.py | 4 +- fastplotlib/graphics/features/_data.py | 4 +- fastplotlib/graphics/features/_present.py | 2 +- fastplotlib/graphics/image.py | 2 +- fastplotlib/graphics/line.py | 1 - fastplotlib/graphics/linecollection.py | 44 ++--- 9 files changed, 247 insertions(+), 35 deletions(-) create mode 100644 examples/line_collection_event.ipynb 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/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index 27bf36220..acaadbc2b 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -114,10 +114,11 @@ def event_handler(self, event): 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(target_info.target, GraphicCollection): + elif isinstance(self, GraphicCollection): # if target is a GraphicCollection, then indices will be stored in collection_index - indices = event.pick_info["collection_index"] - target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, indices=indices) + # indices = event.pick_info["collection_index"] + # target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, indices=indices) + print(event.pick_info) 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, @@ -140,10 +141,14 @@ class PreviouslyModifiedData: class GraphicCollection(BaseGraphic): + pygfx_events = [ + "click" + ] """Graphic Collection base class""" def __init__(self, name: str = None): self.name = name self._items: List[Graphic] = list() + self.registered_callbacks = dict() @property def world_object(self) -> Group: diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index 3aec126df..a1e7e154f 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], diff --git a/fastplotlib/graphics/features/_colors.py b/fastplotlib/graphics/features/_colors.py index 7304c2b21..85c6d01bb 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="color", 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/image.py b/fastplotlib/graphics/image.py index 2826d4804..d1842ff85 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -10,7 +10,7 @@ class ImageGraphic(Graphic, Interaction): feature_events = [ "data", - "cmap", + "colors", ] def __init__( self, diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index ab32d3ea5..d5aa69bbb 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -11,7 +11,6 @@ class LineGraphic(Graphic, Interaction): feature_events = [ "data", "colors", - "cmap", ] def __init__( self, diff --git a/fastplotlib/graphics/linecollection.py b/fastplotlib/graphics/linecollection.py index caaedf10f..4b7817598 100644 --- a/fastplotlib/graphics/linecollection.py +++ b/fastplotlib/graphics/linecollection.py @@ -6,15 +6,15 @@ from .line import LineGraphic from ..utils import get_colors from typing import * +from copy import deepcopy class LineCollection(GraphicCollection, Interaction): """Line Collection graphic""" child_type = LineGraphic feature_events = [ - "data-changed", - "color-changed", - "cmap-changed", + "data", + "colors", ] def __init__( self, @@ -118,34 +118,30 @@ def __init__( self.add_graphic(lg, reset_index=False) - def _set_feature(self, feature: str, new_data: Any, indices: Union[int, range]): + def _set_feature(self, feature: str, new_data: Any, indices: Any): if not hasattr(self, "_previous_data"): - self._previous_data = {} + self._previous_data = dict() elif hasattr(self, "_previous_data"): self._reset_feature(feature) - #if feature in # need a way to check if feature is in - 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 + + coll_feature = getattr(self[indices], feature) + + data = list() + for cf in coll_feature: + data += cf + data = np.array(data) + + previous = data + coll_feature._set(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 \ No newline at end of file + # implemented for a single index at moment + prev_ixs = self._previous_data[feature].previous_indices + coll_feature = getattr(self[prev_ixs], feature) + coll_feature._set(self._previous_data[feature].previous_data) \ No newline at end of file From fa208cac210a840055952250f3ccd2687c3eb101 Mon Sep 17 00:00:00 2001 From: Caitlin Lewis Date: Thu, 29 Dec 2022 15:59:25 -0500 Subject: [PATCH 12/13] minor changes --- examples/collection.ipynb | 377 ------------------------ examples/event_handler.ipynb | 391 ------------------------- fastplotlib/graphics/linecollection.py | 7 +- 3 files changed, 3 insertions(+), 772 deletions(-) delete mode 100644 examples/collection.ipynb delete mode 100644 examples/event_handler.ipynb 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/event_handler.ipynb b/examples/event_handler.ipynb deleted file mode 100644 index a96977c9f..000000000 --- a/examples/event_handler.ipynb +++ /dev/null @@ -1,391 +0,0 @@ -{ - "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": {}, - "outputs": [], - "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", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - " \n", - "
algoitem_nameinput_movie_pathparamsoutputscommentsuuid
0mcorrmy_movieexample_movies/Sue_2x_3000_40_-46.tif{'main': {'max_shifts': (24, 24), 'strides': (...{'mean-projection-path': 1ed8feb3-9fc8-4a78-8f...None1ed8feb3-9fc8-4a78-8f6d-164620822016
1cnmfmy_movie1ed8feb3-9fc8-4a78-8f6d-164620822016/1ed8feb3-...{'main': {'fr': 30, 'p': 1, 'nb': 2, 'merge_th...{'mean-projection-path': 5f4e3e27-ac1f-4ede-90...None5f4e3e27-ac1f-4ede-903b-be43bd81fddc
\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/fastplotlib/graphics/linecollection.py b/fastplotlib/graphics/linecollection.py index 4b7817598..c0ef08c2d 100644 --- a/fastplotlib/graphics/linecollection.py +++ b/fastplotlib/graphics/linecollection.py @@ -127,11 +127,10 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any): coll_feature = getattr(self[indices], feature) data = list() - for cf in coll_feature: - data += cf - data = np.array(data) + for fea in coll_feature._feature_instances: + data.append(fea._data) - previous = data + previous = data[0].copy() coll_feature._set(new_data) if feature in self._previous_data.keys(): From 118b3a851ea12ef196a0d70adfd0672921c13d3f Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Thu, 29 Dec 2022 18:44:19 -0500 Subject: [PATCH 13/13] interactivity works, image -> lines -> lines with colors --- fastplotlib/graphics/_base.py | 36 ++++++++++++++++++------ fastplotlib/graphics/features/_base.py | 8 ++++++ fastplotlib/graphics/features/_colors.py | 2 +- fastplotlib/graphics/line.py | 13 +++++---- fastplotlib/graphics/linecollection.py | 18 +++++++----- 5 files changed, 54 insertions(+), 23 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index acaadbc2b..e5606f781 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -10,6 +10,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass + class BaseGraphic: def __init_subclass__(cls, **kwargs): """set the type of the graphic in lower case like "image", "line_collection", etc.""" @@ -94,9 +95,15 @@ def _reset_feature(self, feature: str): 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: - feature = getattr(self, event_type) - feature.add_event_handler(self.event_handler, event_type) + 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") @@ -116,9 +123,15 @@ def event_handler(self, 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 - # indices = event.pick_info["collection_index"] - # target_info.target._set_feature(feature=target_info.feature, new_data=target_info.new_data, indices=indices) - print(event.pick_info) + 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, @@ -136,15 +149,17 @@ class CallbackData: @dataclass class PreviouslyModifiedData: """Class for keeping track of previously modified data at indices""" - previous_data: Any - previous_indices: Any + data: Any + indices: Any class GraphicCollection(BaseGraphic): + """Graphic Collection base class""" + pygfx_events = [ "click" ] - """Graphic Collection base class""" + def __init__(self, name: str = None): self.name = name self._items: List[Graphic] = list() @@ -294,7 +309,6 @@ def __setitem__(self, 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: @@ -304,5 +318,9 @@ 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/features/_base.py b/fastplotlib/graphics/features/_base.py index a1e7e154f..d392efa7e 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -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 85c6d01bb..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", pick_info=pick_info) + event_data = FeatureEvent(type="colors", pick_info=pick_info) self._call_event_handlers(event_data) diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index d5aa69bbb..9df940b52 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -12,6 +12,7 @@ class LineGraphic(Graphic, Interaction): "data", "colors", ] + def __init__( self, data: Any, @@ -102,15 +103,15 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any = None): previous = feature_instance._data.copy() feature_instance._set(new_data) if feature in self._previous_data.keys(): - self._previous_data[feature].previous_data = previous - self._previous_data[feature].previous_indices = indices + self._previous_data[feature].data = previous + self._previous_data[feature].indices = indices else: - self._previous_data[feature] = PreviouslyModifiedData(previous_data=previous, previous_indices=indices) + self._previous_data[feature] = PreviouslyModifiedData(data=previous, indices=indices) def _reset_feature(self, feature: str): - prev_ixs = self._previous_data[feature].previous_indices + 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].previous_data + feature_instance[prev_ixs] = self._previous_data[feature].data else: - feature_instance._set(self._previous_data[feature].previous_data) \ No newline at end of file + 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 c0ef08c2d..d17f9c241 100644 --- a/fastplotlib/graphics/linecollection.py +++ b/fastplotlib/graphics/linecollection.py @@ -5,7 +5,6 @@ from ._base import Interaction, PreviouslyModifiedData, GraphicCollection from .line import LineGraphic from ..utils import get_colors -from typing import * from copy import deepcopy @@ -16,6 +15,7 @@ class LineCollection(GraphicCollection, Interaction): "data", "colors", ] + def __init__( self, data: List[np.ndarray], @@ -130,17 +130,21 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any): for fea in coll_feature._feature_instances: data.append(fea._data) - previous = data[0].copy() + # 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].previous_data = previous - self._previous_data[feature].previous_indices = indices + self._previous_data[feature].data = previous + self._previous_data[feature].indices = indices else: - self._previous_data[feature] = PreviouslyModifiedData(previous_data=previous, previous_indices=indices) + 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].previous_indices + prev_ixs = self._previous_data[feature].indices coll_feature = getattr(self[prev_ixs], feature) - coll_feature._set(self._previous_data[feature].previous_data) \ No newline at end of file + + coll_feature.block_events(True) + coll_feature._set(self._previous_data[feature].data) + coll_feature.block_events(False) \ No newline at end of file