Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
features now trigger events, similar format as pygfx events, tested a…
…nd works!
  • Loading branch information
kushalkolar committed Dec 22, 2022
commit 78aeb2e58aa7864c56bead79ba5cfecd0387820f
208 changes: 171 additions & 37 deletions 208 examples/simple.ipynb

Large diffs are not rendered by default.

86 changes: 70 additions & 16 deletions 86 fastplotlib/graphics/features/_base.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,39 @@
from abc import ABC, abstractmethod
from inspect import getfullargspec
from typing import *

import numpy as np
from pygfx import Buffer


class FeatureEvent:
"""
type: <feature_name>-<changed>, example: "color-changed"
pick_info: dict in the form:
{
"index": indices where feature data was changed, ``range`` object or List[int],
"world_object": world object the feature belongs to,
"new_values": the new values
}
"""
def __init__(self, type: str, pick_info: dict):
self.type = type
self.pick_info = pick_info

def __repr__(self):
return f"{self.__class__.__name__} @ {hex(id(self))}\n" \
f"type: {self.type}\n" \
f"pick_info: {self.pick_info}\n"


class GraphicFeature(ABC):
def __init__(self, parent, data: Any):
self._parent = parent
if isinstance(data, np.ndarray):
data = data.astype(np.float32)

self._data = data
self._event_handlers = list()

@property
def feature_data(self):
Expand All @@ -26,20 +48,55 @@ def _set(self, value):
def __repr__(self):
pass

def add_event_handler(self, handler: callable):
"""
Add an event handler. All added event handlers are called when this feature changes.
The `handler` can optionally accept ``FeatureEvent`` as the first and only argument.
The ``FeatureEvent`` only has two attributes, `type` which denotes the type of event
as a str in the form of "<feature_name>-changed", such as "color-changed".

Parameters
----------
handler: callable
a function to call when this feature changes

"""
if not callable(handler):
raise TypeError("event handler must be callable")
self._event_handlers.append(handler)

#TODO: maybe this can be implemented right here in the base class
@abstractmethod
def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any):
"""Called whenever a feature changes, and it calls all funcs in self._event_handlers"""
pass

def _call_event_handlers(self, event_data: FeatureEvent):
for func in self._event_handlers:
if len(getfullargspec(func).args) > 0:
func(event_data)
else:
func()

def cleanup_slice(slice_obj: slice, upper_bound) -> slice:
if isinstance(slice_obj, tuple):
if isinstance(slice_obj[0], slice):
slice_obj = slice_obj[0]

def cleanup_slice(key: Union[int, slice], upper_bound) -> Union[slice, int]:
if isinstance(key, int):
return key

if isinstance(key, tuple):
# if tuple of slice we only need the first obj
# since the first obj is the datapoint indices
if isinstance(key[0], slice):
key = key[0]
else:
raise TypeError("Tuple slicing must have slice object in first position")

if not isinstance(slice_obj, slice):
raise TypeError("Must pass slice object")
if not isinstance(key, slice):
raise TypeError("Must pass slice or int object")

start = slice_obj.start
stop = slice_obj.stop
step = slice_obj.step
start = key.start
stop = key.stop
step = key.step
for attr in [start, stop, step]:
if attr is None:
continue
Expand All @@ -55,7 +112,7 @@ def cleanup_slice(slice_obj: slice, upper_bound) -> slice:
elif stop > upper_bound:
raise IndexError("Index out of bounds")

step = slice_obj.step
step = key.step
if step is None:
step = 1

Expand Down Expand Up @@ -91,16 +148,13 @@ def _upper_bound(self) -> int:

def _update_range_indices(self, key):
"""Currently used by colors and data"""
key = cleanup_slice(key, self._upper_bound)

if isinstance(key, int):
self._buffer.update_range(key, size=1)
return

# else assume it's a slice or tuple of slice
# if tuple of slice we only need the first obj
# since the first obj is the datapoint indices
key = cleanup_slice(key, self._upper_bound)

# else if it's a single slice
# else if it's a slice obj
if isinstance(key, slice):
if key.step == 1: # we cleaned up the slice obj so step of None becomes 1
# update range according to size using the offset
Expand Down
54 changes: 30 additions & 24 deletions 54 fastplotlib/graphics/features/_colors.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
import numpy as np

from ._base import GraphicFeatureIndexable, cleanup_slice
from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent
from pygfx import Color


class ColorFeature(GraphicFeatureIndexable):
@property
def _buffer(self):
return self._parent.world_object.geometry.colors

def __getitem__(self, item):
return self._buffer.data[item]

def __repr__(self):
return repr(self._buffer.data)

def __init__(self, parent, colors, n_colors, alpha: float = 1.0):
"""
ColorFeature
Expand Down Expand Up @@ -77,10 +87,6 @@ def __init__(self, parent, colors, n_colors, alpha: float = 1.0):

super(ColorFeature, self).__init__(parent, data)

@property
def _buffer(self):
return self._parent.world_object.geometry.colors

def __setitem__(self, key, value):
# parse numerical slice indices
if isinstance(key, slice):
Expand Down Expand Up @@ -111,6 +117,7 @@ def __setitem__(self, key, value):
# key[1] is going to be RGBA so get rid of it to pass to _update_range
# _key = cleanup_slice(key[0], self._upper_bound)
self._update_range(key)
self._feature_changed(key, value)
return

else:
Expand Down Expand Up @@ -155,27 +162,26 @@ def __setitem__(self, key, value):
self._buffer.data[key] = new_colors

self._update_range(key)
self._feature_changed(key, new_colors)

def _update_range(self, key):
self._update_range_indices(key)
# if isinstance(key, int):
# self._buffer.update_range(key, size=1)
# return
#
# # else assume it's a slice
# if key.step == 1: # we cleaned up the slice obj so step of None becomes 1
# # update range according to size using the offset
# self._buffer.update_range(offset=key.start, size=key.stop - key.start)
#
# else:
# step = key.step
# # convert slice to indices
# ixs = range(key.start, key.stop, step)
# for ix in ixs:
# self._buffer.update_range(ix, size=1)

def __getitem__(self, item):
return self._buffer.data[item]
def _feature_changed(self, key, new_data):
key = cleanup_slice(key, self._upper_bound)
if isinstance(key, int):
indices = [key]
elif isinstance(key, slice):
indices = range(key.start, key.stop, key.step)
else:
raise TypeError("feature changed key must be slice or int")

def __repr__(self):
return repr(self._buffer.data)
pick_info = {
"index": indices,
"world_object": self._parent.world_object,
"new_data": new_data,
}

event_data = FeatureEvent(type="color-changed", pick_info=pick_info)

self._call_event_handlers(event_data)
30 changes: 27 additions & 3 deletions 30 fastplotlib/graphics/features/_data.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ._base import GraphicFeatureIndexable
from ._base import GraphicFeatureIndexable, cleanup_slice, FeatureEvent
from pygfx import Buffer
from typing import *
from ...utils import fix_data, to_float32
Expand Down Expand Up @@ -28,6 +28,9 @@ def _buffer_name(self) -> str:
if hasattr(self._parent.world_object.geometry, buffer_name):
return buffer_name

def __repr__(self):
return repr(self._buffer.data)

def __getitem__(self, item):
return self._buffer.data[item]

Expand All @@ -44,12 +47,33 @@ def __setitem__(self, key, value):
def _update_range(self, key):
if self._buffer_name == "grid":
self._update_range_grid(key)
self._feature_changed(key=None, new_data=None)
elif self._buffer_name == "positions":
self._update_range_indices(key)
self._feature_changed(key=key, new_data=None)

def _update_range_grid(self, key):
# image data
self._buffer.update_range((0, 0, 0), self._buffer.size)

def __repr__(self):
return repr(self._buffer.data)
def _feature_changed(self, key, new_data):
# for now if key=None that means all data changed, i.e. ImageGraphic
# also for now new data isn't stored for DataFeature
if key is not None:
key = cleanup_slice(key, self._upper_bound)
if isinstance(key, int):
indices = [key]
elif isinstance(key, slice):
indices = range(key.start, key.stop, key.step)
elif key is None:
indices = None

pick_info = {
"index": indices,
"world_object": self._parent.world_object,
"new_data": new_data
}

event_data = FeatureEvent(type="data-changed", pick_info=pick_info)

self._call_event_handlers(event_data)
17 changes: 16 additions & 1 deletion 17 fastplotlib/graphics/features/_present.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
from ._base import GraphicFeature
from ._base import GraphicFeature, FeatureEvent
from pygfx import Scene


Expand Down Expand Up @@ -32,5 +32,20 @@ def _set(self, present: bool):
if self._parent.world_object in self._scene.children:
self._scene.remove(self._parent.world_object)

self._feature_changed(key=None, new_data=present)

def __repr__(self):
return repr(self.feature_data)

def _feature_changed(self, key, new_data):
# this is a non-indexable feature so key=None

pick_info = {
"index": None,
"world_object": self._parent.world_object,
"new_data": new_data
}

event_data = FeatureEvent(type="present-changed", pick_info=pick_info)

self._call_event_handlers(event_data)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.