Skip to content

Cleanup #259

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Jun 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
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
12 changes: 9 additions & 3 deletions fastplotlib/__init__.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
from pathlib import Path

from wgpu.gui.auto import run
from .layouts import Plot, GridPlot

from .plot import Plot
from .layouts import GridPlot
from wgpu.gui.auto import run

try:
import ipywidgets
Expand All @@ -15,3 +14,10 @@

with open(Path(__file__).parent.joinpath("VERSION"), "r") as f:
__version__ = f.read().split("\n")[0]

__all__ = [
"Plot",
"GridPlot",
"run",
"ImageWidget"
]
2 changes: 1 addition & 1 deletion fastplotlib/graphics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,5 @@
"HeatmapGraphic",
"LineCollection",
"LineStack",
"TextGraphic"
"TextGraphic",
]
147 changes: 84 additions & 63 deletions fastplotlib/graphics/_base.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,14 @@
from typing import *
import weakref
from warnings import warn
from abc import ABC, abstractmethod
from dataclasses import dataclass

import numpy as np

from .features._base import cleanup_slice

from pygfx import WorldObject, Group
from .features import GraphicFeature, PresentFeature, GraphicFeatureIndexable

from abc import ABC, abstractmethod
from dataclasses import dataclass
from pygfx import WorldObject

from ._features import GraphicFeature, PresentFeature, GraphicFeatureIndexable

# dict that holds all world objects for a given python kernel/session
# Graphic objects only use proxies to WorldObjects
Expand All @@ -37,21 +34,22 @@
class BaseGraphic:
def __init_subclass__(cls, **kwargs):
"""set the type of the graphic in lower case like "image", "line_collection", etc."""
cls.type = cls.__name__\
.lower()\
.replace("graphic", "")\
.replace("collection", "_collection")\
cls.type = (
cls.__name__.lower()
.replace("graphic", "")
.replace("collection", "_collection")
.replace("stack", "_stack")
)

super().__init_subclass__(**kwargs)


class Graphic(BaseGraphic):
def __init__(
self,
name: str = None,
metadata: Any = None,
collection_index: int = None,
self,
name: str = None,
metadata: Any = None,
collection_index: int = None,
):
"""

Expand Down Expand Up @@ -163,6 +161,7 @@ def __del__(self):

class Interaction(ABC):
"""Mixin class that makes graphics interactive"""

@abstractmethod
def _set_feature(self, feature: str, new_data: Any, indices: Any):
pass
Expand All @@ -172,13 +171,13 @@ def _reset_feature(self, feature: str):
pass

def link(
self,
event_type: str,
target: Any,
feature: str,
new_data: Any,
callback: callable = None,
bidirectional: bool = False
self,
event_type: str,
target: Any,
feature: str,
new_data: Any,
callback: callable = None,
bidirectional: bool = False,
):
"""
Link this graphic to another graphic upon an ``event_type`` to change the ``feature``
Expand All @@ -192,24 +191,30 @@ def link(
or appropriate feature event (ex. colors, data, etc.) associated with the graphic (can use
``graphic_instance.feature_events`` to get a tuple of the valid feature events for the
graphic)

target: Any
graphic to be linked to

feature: str
feature (ex. colors, data, etc.) of the target graphic that will change following
the event

new_data: Any
appropriate data that will be changed in the feature of the target graphic after
the event occurs

callback: callable, optional
user-specified callable that will handle event,
the callable must take the following four arguments
| ''source'' - this graphic instance
| ''target'' - the graphic to be changed following the event
| ''event'' - the ''pygfx event'' or ''feature event'' that occurs
| ''new_data'' - the appropriate data of the ''target'' that will be changed

bidirectional: bool, default False
if True, the target graphic is also linked back to this graphic instance using the
same arguments

For example:
.. code-block::python

Expand All @@ -231,21 +236,32 @@ def link(
feature_instance.add_event_handler(self._event_handler)

else:
raise ValueError(f"Invalid event, valid events are: {PYGFX_EVENTS + self.feature_events}")
raise ValueError(
f"Invalid event, valid events are: {PYGFX_EVENTS + self.feature_events}"
)

# 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}")
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()

callback_data = CallbackData(target=target, feature=feature, new_data=new_data, callback_function=callback)
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")
warn(
"linkage already exists for given event, target, and data, skipping"
)
return

self.registered_callbacks[event_type].append(callback_data)
Expand All @@ -254,15 +270,15 @@ def link(
if event_type in PYGFX_EVENTS:
warn("cannot use bidirectional link for pygfx events")
return

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.
# this instance .link(), and then it will happen again etc.
)

def _event_handler(self, event):
Expand All @@ -271,7 +287,12 @@ def _event_handler(self, event):
for target_info in self.registered_callbacks[event.type]:
if target_info.callback_function is not None:
# if callback_function is not None, then callback function should handle the entire event
target_info.callback_function(source=self, target=target_info.target, event=event, new_data=target_info.new_data)
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
Expand All @@ -288,16 +309,24 @@ def _event_handler(self, event):
# the real world object in the pick_info and not the proxy
if wo 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)
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)
target_info.target._set_feature(
feature=target_info.feature,
new_data=target_info.new_data,
indices=None,
)


@dataclass
class CallbackData:
"""Class for keeping track of the info necessary for interactivity after event occurs."""

target: Any
feature: str
new_data: Any
Expand Down Expand Up @@ -329,6 +358,7 @@ def __eq__(self, other):
@dataclass
class PreviouslyModifiedData:
"""Class for keeping track of previously modified data at indices"""

data: Any
indices: Any

Expand All @@ -350,7 +380,9 @@ def __init__(self, name: str = None):
def graphics(self) -> np.ndarray[Graphic]:
"""The Graphics within this collection. Always returns a proxy to the Graphics."""
if self._graphics_changed:
proxies = [weakref.proxy(COLLECTION_GRAPHICS[loc]) for loc in self._graphics]
proxies = [
weakref.proxy(COLLECTION_GRAPHICS[loc]) for loc in self._graphics
]
self._graphics_array = np.array(proxies)
self._graphics_array.flags["WRITEABLE"] = False
self._graphics_changed = False
Expand Down Expand Up @@ -395,15 +427,14 @@ def __getitem__(self, key):
return CollectionIndexer(
parent=self,
selection=self.graphics[key],
# selection_indices=key
)

def __del__(self):
self.world_object.clear()

for loc in self._graphics:
del COLLECTION_GRAPHICS[loc]

super().__del__()

def _reset_index(self):
Expand All @@ -420,11 +451,11 @@ def __repr__(self):

class CollectionIndexer:
"""Collection Indexer"""

def __init__(
self,
parent: GraphicCollection,
selection: List[Graphic],
# selection_indices: Union[list, range],
self,
parent: GraphicCollection,
selection: List[Graphic],
):
"""

Expand All @@ -436,26 +467,22 @@ def __init__(
selection: list of Graphics
a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices``

selection_indices: Union[list, range]
the corresponding indices from the parent GraphicCollection that were selected
"""

self._parent = weakref.proxy(parent)
self._selection = selection
# self._selection_indices = selection_indices

# we use parent.graphics[0] instead of selection[0]
# because the selection can be empty
for attr_name in self._parent.graphics[0].__dict__.keys():
attr = getattr(self._parent.graphics[0], attr_name)
if isinstance(attr, GraphicFeature):
collection_feature = CollectionFeature(
parent,
self._selection,
# selection_indices=self._selection_indices,
feature=attr_name
self._selection, feature=attr_name
)
collection_feature.__doc__ = (
f"indexable <{attr_name}> feature for collection"
)
collection_feature.__doc__ = f"indexable <{attr_name}> feature for collection"
setattr(self, attr_name, collection_feature)

@property
Expand All @@ -476,31 +503,26 @@ def __len__(self):
return len(self._selection)

def __repr__(self):
return f"{self.__class__.__name__} @ {hex(id(self))}\n" \
f"Selection of <{len(self._selection)}> {self._selection[0].__class__.__name__}"
return (
f"{self.__class__.__name__} @ {hex(id(self))}\n"
f"Selection 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
):

def __init__(self, selection: List[Graphic], feature: str):
"""
parent: GraphicCollection
GraphicCollection feature instance that is being indexed
selection: list of Graphics
a list of the selected Graphics from the parent GraphicCollection based on the ``selection_indices``
selection_indices: Union[list, range]
the corresponding indices from the parent GraphicCollection that were selected

feature: str
feature of Graphics in the GraphicCollection being indexed

"""

self._selection = selection
# self._selection_indices = selection_indices
self._feature = feature

self._feature_instances: List[GraphicFeature] = list()
Expand Down Expand Up @@ -550,4 +572,3 @@ def block_events(self, b: bool):

def __repr__(self):
return f"Collection feature for: <{self._feature}>"

21 changes: 21 additions & 0 deletions fastplotlib/graphics/_features/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
from ._colors import ColorFeature, CmapFeature, ImageCmapFeature, HeatmapCmapFeature
from ._data import PointsDataFeature, ImageDataFeature, HeatmapDataFeature
from ._present import PresentFeature
from ._thickness import ThicknessFeature
from ._base import GraphicFeature, GraphicFeatureIndexable, FeatureEvent, to_gpu_supported_dtype

__all__ = [
"ColorFeature",
"CmapFeature",
"ImageCmapFeature",
"HeatmapCmapFeature",
"PointsDataFeature",
"ImageDataFeature",
"HeatmapDataFeature",
"PresentFeature",
"ThicknessFeature",
"GraphicFeature",
"GraphicFeatureIndexable",
"FeatureEvent",
"to_gpu_supported_dtype"
]
Loading