janfreyberg / superintendent

Practical active learning in python
https://superintendent.readthedocs.io
189 stars 18 forks source link

Multiple labels for Input (not multiclass) #51

Open DanWertheimer opened 4 years ago

DanWertheimer commented 4 years ago

I'd like to create a widget that allows you to label text with two labels.

These would be along the lines of: data: Some weird store name label 1: Groceries label 2: Tesco

these labels are two seperate things but both realted to the data shown. How would I implement this? I've created a dual dropdown widget for showing these things, the issue that I have is that options is a list but now I'll have two lists with different values.

janfreyberg commented 4 years ago

Ah interesting - so it's almost a hierarchical setting? That makes sense to me, and using two dropdown widgets should work (with a bit of custom configuration). Can you post the code you have so far?

DanWertheimer commented 4 years ago

Honestly, I haven't done much other than create the widget. I'm trying to figure out the best way for the labeller to accept to options because the current code requires a single list. What would you suggest?

Code below. It's very rough right now (kinda new to working with such complete packages)

from collections import defaultdict
from typing import Callable, DefaultDict, Sequence,Tuple, Any

import ipywidgets as widgets
import traitlets

class DualDropdownButton(widgets.VBox):
    def __init__(self, options: Tuple[list,list], *args, **kwargs):
        """Create a dropdown button.
        Parameters
        ----------
        options : Sequence[str]
            The options to display in the widget.
        """
        super().__init__(*args, **kwargs)

        self.submit_callbacks: List[Callable[[Any], None]] = []
        self.undo_callbacks: List[Callable[[], None]] = []
        self.skip_callbacks: List[Callable[[], None]] = []

        self.options= options

        self.option_one = options[0]
        self.option_two = options[1]

        self.dropdown_one = widgets.Dropdown(
            options=[option for option in self.option_one],
            description="Label:",
        )

        self.dropdown_two = widgets.Dropdown(
            options=[option for option in self.option_two],
            description="Label:",
        )

        self.button = widgets.Button(
            description="Submit.",
            tooltip="Submit label.",
            button_style="success",
        )

        self.button.on_click(self._handle_click)

        self.children = [widgets.HBox([self.dropdown_one, self.dropdown_two , self.button])]

    def on_click(self, func: Callable) -> None:
            """Add a function to the list of calls made after a click.
            Parameters
            ----------
            func : Callable
                The function to call when the button is clicked.
            """
            if not callable(func):
                raise ValueError(
                    "You need to provide a callable object, but you provided "
                    + str(func)
                    + "."
                )
            self.submission_functions.append(func)

    def on_submit(self, callback: Callable[[Any], None]):
        """Register a callback to handle data when the user clicks "Submit".
        .. note::
            Callbacks are called in order of registration - first registered,
            first called.
        Parameters
        ----------
        callback : Callable[[Any], None]
            A function that takes in data. Usually, this data is a list of
            dictionaries, but you are able to define data post-processors when
            you create an annotator that get called before this callback is
            called. Any return values are ignored.
        """
        self.submit_callbacks.append(callback)

    def on_undo(self, callback: Callable[[], None]):
        """Register a callback to handle when the user clicks "Undo".
        Note that any callback registered here is only called when the canvas
        is empty - while there are annotations on the canvas, "Undo" actually
        undoes the annotations, until the canvas is empty.
        Parameters
        ----------
        callback : Callable[[], None]
            A function to be called when users press "Undo". This should be
            a function that takes in no arguments; any return values are
            ignored.
        """
        self.undo_callbacks.append(callback)

    def on_skip(self, callback: Callable[[], None]):
        """Register a callback to handle when the user clicks "Skip".
        Parameters
        ----------
        callback : Callable[[], None]
            The function to be called when the user clicks "Skip". It should
            take no arguments, and any return values are ignored.
        """
        self.skip_callbacks.append(callback)

    def _handle_click(self, owner: widgets.Button) -> None:
        for func in self.submission_functions:
            func(owner)        

Very open to help and for constructive criticism