From ad891464cbd0c021d3f93c52b5216e3176945285 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 8 Jan 2023 03:02:53 -0500 Subject: [PATCH 1/3] bugfix for feature event hanlders which are methods --- fastplotlib/graphics/features/_base.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/fastplotlib/graphics/features/_base.py b/fastplotlib/graphics/features/_base.py index bb8cd4031..a9aafc2b7 100644 --- a/fastplotlib/graphics/features/_base.py +++ b/fastplotlib/graphics/features/_base.py @@ -107,8 +107,13 @@ def _call_event_handlers(self, event_data: FeatureEvent): for func in self._event_handlers: try: - if len(getfullargspec(func).args) > 0: - func(event_data) + args = getfullargspec(func).args + + if len(args) > 0: + if args[0] == "self" and not len(args) > 1: + func() + else: + func(event_data) else: func() except: From 7342150c6ef280af924e7bd3e32032227689da56 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 8 Jan 2023 03:03:31 -0500 Subject: [PATCH 2/3] bugfix, image has cmap as feature not colors --- fastplotlib/graphics/image.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fastplotlib/graphics/image.py b/fastplotlib/graphics/image.py index 50ec3ad30..8ab1db0cd 100644 --- a/fastplotlib/graphics/image.py +++ b/fastplotlib/graphics/image.py @@ -10,8 +10,9 @@ class ImageGraphic(Graphic, Interaction): feature_events = [ "data", - "colors", + "cmap", ] + def __init__( self, data: Any, From 1d3ca3ee1f48da259041b55e95d373f65f8688a1 Mon Sep 17 00:00:00 2001 From: kushalkolar Date: Sun, 8 Jan 2023 03:13:27 -0500 Subject: [PATCH 3/3] Graphic.link() can be bidirection, fix feature_events cls attrs, allow 1D numpy array to index GraphicCollection --- fastplotlib/graphics/_base.py | 85 +++++++++++++++++++++---- fastplotlib/graphics/line.py | 3 + fastplotlib/graphics/line_collection.py | 16 ++++- 3 files changed, 89 insertions(+), 15 deletions(-) diff --git a/fastplotlib/graphics/_base.py b/fastplotlib/graphics/_base.py index df4181ee4..eeefe0919 100644 --- a/fastplotlib/graphics/_base.py +++ b/fastplotlib/graphics/_base.py @@ -1,4 +1,7 @@ from typing import * +from warnings import warn + +import numpy as np from .features._base import cleanup_slice @@ -92,10 +95,19 @@ 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, callback_function: callable = None): + def link( + self, + event_type: str, + target: Any, + feature: str, + new_data: Any, + callback: callable = None, + bidirectional: bool = False + ): if event_type in self.pygfx_events: self.world_object.add_event_handler(self.event_handler, event_type) + # make sure event is valid elif event_type in self.feature_events: if isinstance(self, GraphicCollection): feature_instance = getattr(self[:], event_type) @@ -105,15 +117,35 @@ def link(self, event_type: str, target: Any, feature: str, new_data: Any, callba feature_instance.add_event_handler(self.event_handler) else: - raise ValueError("event not possible") + raise ValueError(f"Invalid event, valid events are: {self.pygfx_events + self.feature_events}") - if event_type in self.registered_callbacks.keys(): - self.registered_callbacks[event_type].append( - CallbackData(target=target, feature=feature, new_data=new_data, callback_function=callback_function)) - else: + # make sure target feature is valid + if feature is not None: + if feature not in target.feature_events: + raise ValueError(f"Invalid feature for target, valid features are: {target.feature_events}") + + if event_type not in self.registered_callbacks.keys(): self.registered_callbacks[event_type] = list() - self.registered_callbacks[event_type].append( - CallbackData(target=target, feature=feature, new_data=new_data, callback_function=callback_function)) + + callback_data = CallbackData(target=target, feature=feature, new_data=new_data, callback_function=callback) + + for existing_callback_data in self.registered_callbacks[event_type]: + if existing_callback_data == callback_data: + warn("linkage already exists for given event, target, and data, skipping") + return + + self.registered_callbacks[event_type].append(callback_data) + + if bidirectional: + target.link( + event_type=event_type, + target=self, + feature=feature, + new_data=new_data, + callback=callback, + bidirectional=False # else infinite recursion, otherwise target will call + # this instance .link(), and then it will happen again etc. + ) def event_handler(self, event): if event.type in self.registered_callbacks.keys(): @@ -145,6 +177,28 @@ class CallbackData: new_data: Any callback_function: callable = None + def __eq__(self, other): + if not isinstance(other, CallbackData): + raise TypeError("Can only compare against other types") + + if other.target is not self.target: + return False + + if not other.feature == self.feature: + return False + + if not other.new_data == self.new_data: + return False + + if (self.callback_function is None) and (other.callback_function is None): + return True + + if other.callback_function is self.callback_function: + return True + + else: + return False + @dataclass class PreviouslyModifiedData: @@ -156,10 +210,6 @@ class PreviouslyModifiedData: class GraphicCollection(Graphic): """Graphic Collection base class""" - pygfx_events = [ - "click" - ] - def __init__(self, name: str = None): super(GraphicCollection, self).__init__(name) self._items: List[Graphic] = list() @@ -207,14 +257,21 @@ def __getitem__(self, key): selection = self._items[key] # fancy-ish indexing - elif isinstance(key, (tuple, list)): + elif isinstance(key, (tuple, list, np.ndarray)): + if isinstance(key, np.ndarray): + if not key.ndim == 1: + raise TypeError(f"{self.__class__.__name__} indexing supports " + f"1D numpy arrays, int, slice, tuple or list of integers, " + f"your numpy arrays has <{key.ndim}> dimensions.") selection = list() + for ix in key: selection.append(self._items[ix]) selection_indices = key else: - raise TypeError(f"Graphic Collection indexing supports int, slice, tuple or list of integers, " + raise TypeError(f"{self.__class__.__name__} indexing supports " + f"1D numpy arrays, int, slice, tuple or list of integers, " f"you have passed a <{type(key)}>") return CollectionIndexer( diff --git a/fastplotlib/graphics/line.py b/fastplotlib/graphics/line.py index 9f180a095..1d8310743 100644 --- a/fastplotlib/graphics/line.py +++ b/fastplotlib/graphics/line.py @@ -11,6 +11,9 @@ class LineGraphic(Graphic, Interaction): feature_events = [ "data", "colors", + "cmap", + "thickness", + "present" ] def __init__( diff --git a/fastplotlib/graphics/line_collection.py b/fastplotlib/graphics/line_collection.py index 2953423e7..f34937255 100644 --- a/fastplotlib/graphics/line_collection.py +++ b/fastplotlib/graphics/line_collection.py @@ -14,6 +14,9 @@ class LineCollection(GraphicCollection, Interaction): feature_events = [ "data", "colors", + "cmap", + "thickness", + "present" ] def __init__( @@ -122,6 +125,13 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any): if not hasattr(self, "_previous_data"): self._previous_data = dict() elif hasattr(self, "_previous_data"): + if feature in self._previous_data.keys(): + # for now assume same index won't be changed with diff data + # I can't think of a usecase where we'd have to check the data too + # so unless there is bug we keep it like this + if self._previous_data[feature].indices == indices: + return # nothing to change, and this allows bidirectional linking without infinite recusion + self._reset_feature(feature) coll_feature = getattr(self[indices], feature) @@ -132,7 +142,6 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any): # later we can think about multi-index events previous = deepcopy(data[0]) - coll_feature._set(new_data) if feature in self._previous_data.keys(): self._previous_data[feature].data = previous @@ -140,6 +149,11 @@ def _set_feature(self, feature: str, new_data: Any, indices: Any): else: self._previous_data[feature] = PreviouslyModifiedData(data=previous, indices=indices) + # finally set the new data + # this MUST occur after setting the previous data attribute to prevent recursion + # since calling `feature._set()` triggers all the feature callbacks + coll_feature._set(new_data) + def _reset_feature(self, feature: str): if feature not in self._previous_data.keys(): return