Skip to content
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
Next Next commit
Full selection vector implementation
  • Loading branch information
apasarkar committed Jun 11, 2026
commit c6308e340ae07e58c921c888514b4acecd74c64b
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
95 changes: 57 additions & 38 deletions fastplotlib/graphics/selectors/_selection_vector.py
Original file line number Diff line number Diff line change
@@ -1,89 +1,108 @@
from collections.abc import Callable
from functools import partial
from typing import Any, Sequence

import numpy as np
from ._protocols import SelectorProtocol, MultiSelectorProtocol


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


class SelectionVector:
"""
A class for performing coordinated selections across multiple selectors.
The user specifies the selectors (via add_selector) and mappings from global selection indices to each individual selector's local indices.
"""
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
if self._block_reentrance:
return
else:
self._block_reentrance = True
# 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
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[val] = key

## Define the partial functions
master_to_local_map = lambda x:master_to_local[x] if x in master_to_local else None
local_to_master_map = lambda x:inverse_dict[x] if x in inverse_dict 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()
self._selection = map_inv(local_selection)

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()