Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
2 changes: 1 addition & 1 deletion fastplotlib/graphics/selectors/_highlight_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -681,7 +681,7 @@ def selection(self) -> tuple[int | None, ...] | dict[str, tuple]:
return {k: tuple(v) for k, v in self._selection.items()}

@selection.setter
def selection(self, value: Iterable[int] | dict[Literal["rows", "cols", "pixels"], list]) -> None:
def selection(self, value: Iterable[int] | dict[Literal["rows", "cols", "pixels"], list] | None) -> None:
Comment thread
kushalkolar marked this conversation as resolved.
Outdated
if self._selection_options is not None:
if value is None:
self._selected_indices = list()
Expand Down
2 changes: 1 addition & 1 deletion fastplotlib/graphics/selectors/_linear.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ def selection(self) -> float:
return self._selection.value

@selection.setter
def selection(self, value: int):
def selection(self, value: float):
graphic = self._parent

if isinstance(graphic, GraphicCollection):
Expand Down
118 changes: 78 additions & 40 deletions fastplotlib/graphics/selectors/_selection_vector.py
Original file line number Diff line number Diff line change
@@ -1,89 +1,127 @@
from collections.abc import Callable
from functools import partial
from typing import Any, Sequence

from numbers import Integral
import numpy as np
from ._protocols import SelectorProtocol, MultiSelectorProtocol

from fastplotlib.graphics.features._base import GraphicFeatureEvent
Comment thread
kushalkolar marked this conversation as resolved.
Outdated

def identity(val: Any) -> Any:
return val


class SelectionVector:
"""
A class for performing coordinated selections across multiple selectors.
For each selector in the selection vector, the user specifies how the global indices (shared across selectors)
maps to the local indices.

The SelectionVector manages everything else, including the coordinated updating of indices whenever a selection changes
"""
def __init__(self, max_size: int = None):
# selector -> (map, map_inv)
self._selectors: dict[
SelectorProtocol | MultiSelectorProtocol, tuple[Callable, Callable]
SelectorProtocol | MultiSelectorProtocol, tuple[Callable, Callable, list]
Comment thread
kushalkolar marked this conversation as resolved.
Outdated
] = dict()
self._selection: list[Any] = list()
self._block_reentrance = False

@property
def selection(self) -> tuple[Any]:
return tuple(self._selection)

@selection.setter
def selection(self, new: Sequence[Any]):
# iterate through each selector that operates in its own "local" space
for selector_local, (map_, map_inv) in self._selectors.items():
indices_local = map_(new)
selector_local.selection = indices_local
def selection(self, new: Integral | Sequence[Any]):
if self._block_reentrance:
return
else:
self._block_reentrance = True
if isinstance(new, Integral):
new = [new]
self._selection = [i for i in new]
Comment thread
kushalkolar marked this conversation as resolved.
Outdated
Comment thread
kushalkolar marked this conversation as resolved.
Outdated
# iterate through each selector that operates in its own "local" space
for selector_local, (map_, map_inv, handler) in self._selectors.items():
cumulated_output = []
Comment thread
kushalkolar marked this conversation as resolved.
Outdated
for value in new:
curr_indices = map_(value)
cumulated_output.append(curr_indices)
# indices_local = map_(new)
Comment thread
kushalkolar marked this conversation as resolved.
Outdated
selector_local.selection = cumulated_output
self._block_reentrance = False

def append(self, index):
self._selection.append(index)
for selector, (map_, map_inv) in self._selectors.items():
for selector, (map_, map_inv, handler_list) in self._selectors.items():
if not isinstance(selector, MultiSelectorProtocol):
continue

index_local = map_([index])
Comment thread
kushalkolar marked this conversation as resolved.
Outdated
selector.append(index_local[0])

def clear(self):
self._selection.clear()
# TODO: clear selectors
selector.append(index_local)

def add_selector(
self,
new: (
SelectorProtocol
| tuple[SelectorProtocol, Callable]
| tuple[SelectorProtocol, Callable, Callable]
| tuple[SelectorProtocol, np.ndarray | dict[int, int]]
),
):
"""
User specifies (1) the selector and (2) The master --> local index mapping. This
Comment thread
kushalkolar marked this conversation as resolved.
mapping is given either as:
- A 1D np.ndarray of integers. The array index is the global index, and the array value is the local index
- A dictionary where keys (master indices) and values (local indices) are both integers
"""
selector: SelectorProtocol
map_: Callable
map_inv: Callable

if isinstance(new, (tuple, list)):
if not isinstance(new[0], SelectorProtocol):
raise TypeError

if len(new) not in (2, 3):
raise TypeError

if not all(callable(c) for c in new[1:]):
if len(new) != 2:
raise TypeError

selector = new[0]
map_ = new[1]
map_inv = new[2] if len(new) == 3 else identity
master_to_local = new[1]
if isinstance(master_to_local, np.ndarray):
if not master_to_local.ndim == 1:
raise ValueError("If you pass in an array mapping, it must be 1-D")
master_to_local = dict(enumerate(master_to_local))

## Construct inverse mapping
inverse_dict = dict()
for key, val in master_to_local.items():
inverse_dict[int(val)] = int(key)

## Define the partial functions
master_to_local_map = lambda x:master_to_local[int(x)] if int(x) in master_to_local else None
local_to_master_map = lambda x:inverse_dict[int(x)] if x in inverse_dict and x is not None else None

elif isinstance(new, SelectorProtocol):
selector, map_, map_inv = new, identity, identity
selector, master_to_local_map, local_to_master_map = new, identity, identity

else:
raise ValueError

selector.add_event_handler(partial(self._inv_handler, map_inv))

self._selectors[selector] = (map_, map_inv)
handler = selector.add_event_handler(partial(self._inv_handler, local_to_master_map))
self._selectors[selector] = (master_to_local_map, local_to_master_map, [handler])

def _inv_handler(self, map_inv: Callable, local_selection):
return
# when a selectable changes its selection, set global index change using map inverse
# self._selection = map_inv(local_selection)

def remove(self):
pass

def clear_selectables(self):
self._selectors.clear()
def _inv_handler(self, map_inv: Callable, local_selection: dict | GraphicFeatureEvent):
if isinstance(local_selection, dict):
input_to_map = local_selection['value']
# local_selection = list(local_selection.items())[0][1]
elif isinstance(local_selection, GraphicFeatureEvent):
input_to_map = local_selection.info['value']
else:
raise ValueError("Input to inverse handler should either be dictionary or GraphicFeatureEvent")
self.selection = [map_inv(input_to_map[i]) for i in range(len(input_to_map))]

def remove_selector(self, selector: SelectorProtocol | MultiSelectorProtocol):
if selector in self._selectors:
map, map_inv, handler_list = self._selectors.pop(selector)
for handler in handler_list:
selector.remove_event_handler(handler)
if isinstance(selector, MultiSelectorProtocol):
selector.clear()

def clear_selectors(self):
for selector in self._selectors.keys():
if isinstance(selector, MultiSelectorProtocol):
selector.clear()