Source code for fastplotlib.graphics.selectors._selection_vector

from collections.abc import Callable
from functools import partial
from typing import Any, Sequence, TypeAlias
from numbers import Integral

import numpy as np

from ._protocols import SelectorProtocol, MultiSelectorProtocol

Mapping = np.ndarray | dict[int, int] | Callable


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


def array_map(arr: np.ndarray, index: Integral):
    """
    Used to map local to global indices
    """
    return None if np.isnan(arr[index]) else int(arr[index])


def inv_array_map(arr: np.ndarray, value: int) -> None | int:
    """
    arr[i] gives the global index
    """
    x = np.flatnonzero(arr == value)
    return None if x.size == 0 else int(x[0])


def dict_map(my_dict: dict, key: Integral):
    if key is None:
        return None
    elif int(key) not in my_dict:
        return None
    else:
        return my_dict[key]


[docs] 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 (each selector has its own local index space). The SelectionVector coordinates across individual selectors, including the coordinated updating of indices whenever a selection changes """ def __init__(self): # selector -> (map, map_inv) ## Key is a selector, value is a (1) local to global index map (2) global to local index map (3) list of event handlers self._selectors: dict[ SelectorProtocol | MultiSelectorProtocol, tuple[Callable, Callable, list[Callable]], ] = 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: Integral | Sequence[Any]): if self._block_reentrance: return else: self._block_reentrance = True if isinstance(new, Integral): new = [new] self._selection = list(new) for value in new: if value < 0: raise ValueError("Only nonnegative selection indices are allowed") # iterate through each selector that operates in its own "local" space for selector_local, (map_, map_inv, handler) in self._selectors.items(): local_indices = [] for value in new: curr_indices = map_(value) local_indices.append(curr_indices) selector_local.selection = local_indices self._block_reentrance = False
[docs] def append(self, index): self._selection.append(index) for selector, (map_, map_inv, handler_list) in self._selectors.items(): if not isinstance(selector, MultiSelectorProtocol): continue index_local = map_(index) selector.append(index_local)
[docs] def add_selector( self, new: ( SelectorProtocol | tuple[SelectorProtocol, dict] | tuple[SelectorProtocol, np.ndarray] | tuple[SelectorProtocol, Callable, Callable] ), ): """ User specifies (1) the selector and (2) The master --> local index mapping. This 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 - Two callables. The first callable defines the global index --> local index map, the second specifies the local index --> global index map. All callables take as input nonnegative integers and output nonnegative integers. """ if isinstance(new, (tuple, list)): if not isinstance(new[0], SelectorProtocol): raise TypeError if len(new) == 3: if isinstance(new[1], Callable) and isinstance(new[2], Callable): master_to_local = new[1] local_to_master = new[2] else: raise ValueError( f"Both index mappings must be Callables, you provided {type(new[1])} and {type(new[2])}" ) elif len(new) == 2: if isinstance(new[1], dict): ## Construct inverse mapping inverse_dict = dict() for key, val in new[1].items(): inverse_dict[int(val)] = int(key) master_to_local = partial(dict_map, new[1]) local_to_master = partial(dict_map, inverse_dict) elif isinstance(new[1], np.ndarray): if not new[1].ndim == 1: raise ValueError( "If you pass in an array mapping, it must be 1-D" ) master_to_local = partial(array_map, new[1]) local_to_master = partial(inv_array_map, new[1]) else: raise ValueError( f"Must either provide a single dict or numpy array specifying the local to global index mapping, or two callables" f"specifying the mapping in both directions" ) selector = new[0] elif isinstance(new, SelectorProtocol): selector, master_to_local, local_to_master = new, identity, identity else: raise ValueError handler = selector.add_event_handler( partial(self._inv_handler, local_to_master) ) self._selectors[selector] = (master_to_local, local_to_master, [handler])
def _inv_handler(self, map_inv: Callable, local_selection: dict): """ HighlightSelector and VisibilitySelector emit a dictionary with keys selector and value """ input_to_map = local_selection["value"] for i in range(len(input_to_map)): if isinstance(input_to_map[i], Integral) and input_to_map[i] < 0: raise ValueError( "You can only provide nonnegative values as local indices to a selector" ) self.selection = [map_inv(input_to_map[i]) for i in range(len(input_to_map))]
[docs] 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()
[docs] def clear_selectors(self): for selector in self._selectors.keys(): if isinstance(selector, MultiSelectorProtocol): selector.clear()