Source code for ipyannotations.generic.multiclassification

"""Widgets to assign multiple classes to data points."""

import time
from typing import Iterable

import ipywidgets as widgets
import traitlets

from ..base import LabellingWidgetMixin
from ..controls.togglebuttongroup import ToggleButtonGroup
from .generic_mixin import GenericWidgetMixin, default_display_function


[docs]class MulticlassLabeller( GenericWidgetMixin, LabellingWidgetMixin, widgets.VBox ): """ A multi-class data labelling widget. Label an arbitrary data point with as many or as few labels as necessary. Toggle labels by either clicking on the corresponding button, or by using the hotkeys 1-0 (mapped in order in which labels are on screen). Submit by either clicking the "Submit" button, or by pressing the Enter key. """ allow_freetext = traitlets.Bool(True) options = traitlets.List( trait=traitlets.Unicode(), default_value=list(), allow_none=True ) max_buttons = traitlets.Integer(12) data = traitlets.List(trait=traitlets.Unicode(), default_value=list()) def __init__( self, options: Iterable[str] = (), allow_freetext: bool = True, display_function=default_display_function, *args, **kwargs, ): """ Create a multi-class labelling widget. Parameters ---------- options : list, tuple, optional The class label options. max_buttons : int The number buttons you want to display. If len(options) > max_buttons, the options will be displayed in a dropdown instead. allow_freetext : bool, optional Whether the widget should contain a text box for users to type in a value not present in options. display_function : callable The function called on each datapoint to display it. """ super().__init__( allow_freetext=allow_freetext, display_function=display_function, *args, **kwargs, ) # type: ignore self.options = [str(option) for option in options] self.class_selector = ToggleButtonGroup(options=self.options) traitlets.link((self, "data"), (self.class_selector, "value")) traitlets.link((self, "options"), (self.class_selector, "options")) self._fixed_options = [option for option in self.options] self._freetext_timestamp = 0.0 self.freetext_widget.on_submit(self.freetext_submission) self.children = [ widgets.Box( (self.display_widget,), layout=widgets.Layout( justify_content="center", padding="2.5% 0", display="flex", width="100%", ), ), self.class_selector, widgets.HBox( [ self.freetext_widget, widgets.HBox( [ self.skip_button, self.undo_button, self.submit_button, ] ), ], layout=widgets.Layout(justify_content="space-between"), ), self.event_watcher, ] def _handle_keystroke(self, event): if event["key"] == "Enter": if self._freetext_timestamp > time.time() - 0.1: return super()._handle_keystroke(event) keys = [str(i) for i in range(1, 10)] + ["0"] for key, option in zip(keys, self.class_selector.options): if event.get("key") == key: self.class_selector._toggle(option) def freetext_submission(self, sender: widgets.Text): """Handle a submission by the free-text widget. This is a separate method from `submit`, because it doesn't actually submit the data. Instead, it adds the free-text as an option, and toggles that option to `True`. Parameters ---------- sender : widgets.Text """ if sender is self.freetext_widget and sender.value: value = sender.value # check if this is a new option: if value not in self.options: self.options = self.options + [value] def _undo_callback(): self.options = [ opt for opt in self.options if opt != value ] self._undo_queue.append(_undo_callback) if value not in self.data: self.data = self.data + [value] sender.value = "" self._freetext_timestamp = time.time()
[docs] def undo(self, sender=None): # noqa: D001 """Undo the last action. This will undo the addition of any free-text, and if none are in the queue, functions registered with `on_undo` are called. """ if self._undo_queue: last_undo_fn = self._undo_queue.pop() last_undo_fn() else: for callback in self.undo_fns: callback()