Skip to content

beginning base logic for interactivity impl #61

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

Closed
wants to merge 23 commits into from
Closed
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
302 changes: 302 additions & 0 deletions examples/linecollection_event.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion examples/lineplot.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -178,7 +178,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.5"
"version": "3.9.2"
}
},
"nbformat": 4,
Expand Down
4 changes: 2 additions & 2 deletions fastplotlib/graphics/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,6 @@
"LineGraphic",
"HistogramGraphic",
"HeatmapGraphic",
"LineCollection",
"TextGraphic"
"TextGraphic",
"LineCollection"
]
74 changes: 74 additions & 0 deletions fastplotlib/graphics/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
from ..utils import get_colors
from .features import GraphicFeature, DataFeature, ColorFeature, PresentFeature

from abc import ABC, abstractmethod
from dataclasses import dataclass

class Graphic:
def __init__(
Expand Down Expand Up @@ -46,6 +48,7 @@ def __init__(
self.colors = None

self.name = name
self.registered_callbacks = dict()

if n_colors is None:
n_colors = self.data.feature_data.shape[0]
Expand Down Expand Up @@ -104,3 +107,74 @@ def __repr__(self):
return f"'{self.name}' fastplotlib.{self.__class__.__name__} @ {hex(id(self))}"
else:
return f"fastplotlib.{self.__class__.__name__} @ {hex(id(self))}"


class Interaction(ABC):
@property
def indices(self) -> Any:
return self.indices

@indices.setter
def indices(self, indices: Any):
self.indices = indices

@property
@abstractmethod
def features(self) -> List[str]:
pass

@abstractmethod
def _set_feature(self, feature: str, new_data: Any, indices: Any):
pass

@abstractmethod
def _reset_feature(self, feature: str, old_data: Any):
pass

def link(self, event_type: str, target: Any, feature: str, new_data: Any, indices_mapper: callable = None):
valid_events = ["click"]
if event_type in valid_events:
self.world_object.add_event_handler(self.event_handler, event_type)
else:
raise ValueError("event not possible")

if isinstance(target.data, List):
old_data = list()
for line in target.data:
old_data.append(getattr(line, feature).copy())
else:
old_data = getattr(target, feature).copy()

if event_type in self.registered_callbacks.keys():
self.registered_callbacks[event_type].append(
CallbackData(target=target, feature=feature, new_data=new_data, old_data=old_data, indices_mapper=indices_mapper))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

old_data shouldn't be in CallbackData, old_data should be managed by the target graphic event time it handles an event. When the target graphic handles an event, it should have a copy of the feature data at the indices that are being modified and reset the feature at those indices, and then set the feature w.r.t. to the new CallbackData.

To elaborate:

  1. The target graphic keeps a private dict of (features, orig_vals) that were modified at certain indices from the previous callback, something in the form {"feature": (indices, orig_vals_at_indices)}, you could call it self._previous_modified. And with the new GraphicFeature class from indexable graphic attributes #78 we won't even need a dict for it, we can just add _modified_indices and _orig_vals_at_modified_indices as private attributes to GraphicFeature 😄 .

  2. In _set_feature() it first uses the information from above to reset the feature at those indices to their original values

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

with #78 it might be possible for us to just pass the GraphicFeature instance directly instead of the string name when calling link, for example:

graphic.link(event_type, target=some_graphic, feature=some_graphic.color...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If info for resetting feature is moved to _set_feature() graphic, then we will no longer need method for _reset_feature()?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should still have a _reset_feature() to separate functionality

else:
self.registered_callbacks[event_type] = list()
self.registered_callbacks[event_type].append(
CallbackData(target=target, feature=feature, new_data=new_data, old_data=old_data, indices_mapper=indices_mapper))

def event_handler(self, event):
if event.type == "click":
# storing click information for each click in self.indices
#self.indices(np.array(event.pick_info["index"]))
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]:
# need to map the indices to the target using indices_mapper
if target_info.indices_mapper is not None:
indices = target_info.indices_mapper(target=target_info.target, indices=click_info)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

an indices_mapper function should be given source and target, something like this:

indices_mapper(source=self, target=target_info.target, event_indices=click_info

And the user can write their function to take in all 3 and do whatever they want.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, I assumed that some general form would need to be established

else:
indices = None
# reset feature of target using stored old data
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah I see you've done it like this, but it's better for the target graphic to handle old data because we want the "feature resetting at previously modified indices" to happen regardless of the event type. For example, if we've registered different types of callbacks to keyboard events, and different mouse events, we want the same reset to happen regardless of what previous event.type triggered the previous feature data to change.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I agree! Just wasn't sure at the time the best way to implement, but now that graphic features exist it should be a lot easier to store old data!

target_info.target._reset_feature(feature=target_info.feature, old_data=target_info.old_data)
# 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=indices)

@dataclass
class CallbackData:
"""Class for keeping track of the info necessary for interactivity after event occurs."""
target: Any
feature: str
new_data: Any
old_data: Any
indices_mapper: callable = None
8 changes: 1 addition & 7 deletions fastplotlib/graphics/heatmap.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,24 +53,18 @@ def __init__(
):
"""
Create a Heatmap Graphic

Parameters
----------
data: array-like, must be 2-dimensional
| array-like, usually numpy.ndarray, must support ``memoryview()``
| Tensorflow Tensors also work _I think_, but not thoroughly tested

vmin: int, optional
minimum value for color scaling, calculated from data if not provided

vmax: int, optional
maximum value for color scaling, calculated from data if not provided

cmap: str, optional
colormap to use to display the image data, default is ``"plasma"``

selection_options

args:
additional arguments passed to Graphic
kwargs:
Expand Down Expand Up @@ -140,4 +134,4 @@ def add_highlight(self, event):
self.world_object.add(self.selection_graphic)
self._highlights.append(self.selection_graphic)

return rval
return rval
14 changes: 12 additions & 2 deletions fastplotlib/graphics/image.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
import numpy as np
import pygfx

from ._base import Graphic
from ._base import Graphic, Interaction
from ..utils import quick_min_max, get_cmap_texture


class ImageGraphic(Graphic):
class ImageGraphic(Graphic, Interaction):
def __init__(
self,
data: Any,
Expand Down Expand Up @@ -72,6 +72,16 @@ def __init__(
pygfx.ImageBasicMaterial(clim=(vmin, vmax), map=get_cmap_texture(cmap))
)

@property
def features(self) -> List[str]:
return ["cmap", "data"]

def _set_feature(self, feature: str, new_data: Any, indices: Any):
pass

def _reset_feature(self, feature: str, old_data: Any):
pass

@property
def clim(self) -> Tuple[float, float]:
return self.world_object.material.clim
Expand Down
19 changes: 17 additions & 2 deletions fastplotlib/graphics/line.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
from typing import *
import numpy as np
import pygfx
from typing import *

from ._base import Graphic
from ._base import Graphic, CallbackData, Interaction


class LineGraphic(Graphic):
Expand Down Expand Up @@ -57,5 +58,19 @@ def __init__(
geometry=pygfx.Geometry(positions=self.data.feature_data, colors=self.colors.feature_data),
material=material(thickness=size, vertex_colors=True)
)

self.world_object.position.z = z_position

def _set_feature(self, feature: str, new_data: Any, indices: Any = None):
if feature in self.features:
update_func = getattr(self, f"update_{feature}")
update_func(new_data)
else:
raise ValueError("name arg is not a valid feature")

def _reset_feature(self, feature: str, old_data: Any):
if feature in self.features:
update_func = getattr(self, f"update_{feature}")
update_func(old_data)
else:
raise ValueError("name arg is not a valid feature")
54 changes: 46 additions & 8 deletions fastplotlib/graphics/linecollection.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,23 @@
import numpy as np
import pygfx
from typing import Union
from .line import LineGraphic
from typing import Union, List

from fastplotlib.graphics.line import LineGraphic
from typing import *
from fastplotlib.graphics._base import Interaction
from abc import ABC, abstractmethod


class LineCollection:
def __init__(self, data: List[np.ndarray],
z_position: Union[List[float], float] = None,
size: Union[float, List[float]] = 2.0,
colors: Union[List[np.ndarray], np.ndarray] = None,
cmap: Union[List[str], str] = None,
*args,
**kwargs):

class LineCollection():
def __init__(self, data: List[np.ndarray], z_position: Union[List[float], float] = None, size: Union[float, List[float]] = 2.0, colors: Union[List[np.ndarray], np.ndarray] = None,
cmap: Union[List[str], str] = None, *args, **kwargs):
self.name = None

if not isinstance(z_position, float) and z_position is not None:
if not len(data) == len(z_position):
Expand All @@ -22,7 +32,8 @@ def __init__(self, data: List[np.ndarray], z_position: Union[List[float], float]
if not len(data) == len(cmap):
raise ValueError("args must be the same length")

self.collection = list()
self.data = list()
self._world_object = pygfx.Group()

for i, d in enumerate(data):
if isinstance(z_position, list):
Expand All @@ -45,10 +56,37 @@ def __init__(self, data: List[np.ndarray], z_position: Union[List[float], float]
else:
_cmap = cmap

self.collection.append(LineGraphic(d, _z, _size, _colors, _cmap))
lg = LineGraphic(d, _z, _size, _colors, _cmap)
self.data.append(lg)
self._world_object.add(lg.world_object)

# TODO: make a base class for Collection graphics and put this as a base method
@property
def world_object(self) -> pygfx.WorldObject:
return self._world_object

@property
def features(self) -> List[str]:
return ["colors", "data"]

def _set_feature(self, feature: str, new_data: Any, indices: Any):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that for the _set_feature method in each Graphic or GraphicCollection we can replace Any for the indices to the indices that are appropriate for that graphic type.

if feature in self.features:
update_func = getattr(self.data[indices], f"update_{feature}")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the way it's written it'll only work with single integer indices, we can chat about it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

once we implement this it'll be much easier #76 (comment)

it would become getattr(self, feature)[indices] = new_data

# if indices is a single indices or list of indices
self.data[indices].update_colors(new_data)
else:
raise ValueError("name arg is not a valid feature")

def _reset_feature(self, feature: str, old_data: Any):
if feature in self.features:
#update_func = getattr(self, f"update_{feature}")
for i, line in enumerate(self.data):
line.update_colors(old_data[i])
else:
raise ValueError("name arg is not a valid feature")

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



pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy