Skip to content

.base

  • Base class(es) for ALL explorer implementations.

    BokehBaseExplorer

    Base class for visually exploring data with Bokeh.

    Assumes:

    • in supplied dataframes
    • (always) xy coordinates in x and y columns;
    • (always) an index for the rows;
    • (always) classification label (or ABSTAIN) in a label column.

    Does not assume:

    • a specific form of data;
    • what the map serves to do.

    __init__(self, df_dict, **kwargs) special

    Constructor shared by all child classes.
    Param Type Description
    df_dict dict str -> DataFrame mapping
    **kwargs forwarded to bokeh.plotting.figure
    1. settle the figure settings by using child class defaults & kwargs overrides
    2. settle the glyph settings by using child class defaults
    3. create widgets that child classes can override
    4. create data sources the correspond to class-specific data subsets.
    5. activate builtin search callbacks depending on the child class.
    6. initialize a figure under the settings above
    Source code in hover/core/explorer/base.py
    def __init__(self, df_dict, **kwargs):
        """
        ???+ note "Constructor shared by all child classes."
            | Param       | Type   | Description                  |
            | :---------- | :----- | :--------------------------- |
            | `df_dict`   | `dict` | `str` -> `DataFrame` mapping |
            | `**kwargs`  |        | forwarded to `bokeh.plotting.figure` |
    
            1. settle the figure settings by using child class defaults & kwargs overrides
            2. settle the glyph settings by using child class defaults
            3. create widgets that child classes can override
            4. create data sources the correspond to class-specific data subsets.
            5. activate builtin search callbacks depending on the child class.
            6. initialize a figure under the settings above
        """
        self.figure_kwargs = {
            "tools": STANDARD_PLOT_TOOLS,
            "tooltips": self._build_tooltip(kwargs.pop("tooltips", "")),
            # bokeh recommends webgl for scalability
            "output_backend": "webgl",
        }
        self.figure_kwargs.update(kwargs)
        self.figure = figure(**self.figure_kwargs)
        self.glyph_kwargs = {
            _key: _dict["constant"].copy()
            for _key, _dict in self.__class__.SUBSET_GLYPH_KWARGS.items()
        }
        self._setup_dfs(df_dict)
        self._setup_sources()
        self._setup_widgets()
        self._activate_search_builtin()
    
    Left to child classes that have a specific feature format.
    Param Type Description
    subset str the subset to activate search on
    kwargs bool kwargs for the plot to add to
    altered_param tuple (attribute, positive, negative, default)
    Source code in hover/core/explorer/base.py
    @abstractmethod
    def activate_search(self, subset, kwargs, altered_param=("size", 10, 5, 7)):
        """
        ???+ note "Left to child classes that have a specific feature format."
    
            | Param           | Type    | Description                   |
            | :-------------- | :------ | :---------------------------  |
            | `subset`        | `str`   | the subset to activate search on |
            | `kwargs`        | `bool`  | kwargs for the plot to add to |
            | `altered_param` | `tuple` | (attribute, positive, negative, default) |
        """
        pass
    

    auto_color_mapping(self)

    Find all labels and an appropriate color for each.
    Source code in hover/core/explorer/base.py
    def auto_color_mapping(self):
        """
        ???+ note "Find all labels and an appropriate color for each."
        """
        from hover.utils.bokeh_helper import auto_label_color
    
        labels = set()
        for _key in self.dfs.keys():
            labels = labels.union(set(self.dfs[_key]["label"].values))
    
        return auto_label_color(labels)
    

    from_dataset(dataset, subset_mapping, *args, **kwargs) classmethod

    Alternative constructor from a SupervisableDataset.
    Param Type Description
    dataset SupervisableDataset dataset with DataFrames
    subset_mapping dict dataset -> explorer subset mapping
    *args forwarded to the constructor
    **kwargs forwarded to the constructor
    Source code in hover/core/explorer/base.py
    @classmethod
    def from_dataset(cls, dataset, subset_mapping, *args, **kwargs):
        """
        ???+ note "Alternative constructor from a `SupervisableDataset`."
            | Param            | Type   | Description                  |
            | :--------------- | :----- | :--------------------------- |
            | `dataset`        | `SupervisableDataset` | dataset with `DataFrame`s |
            | `subset_mapping` | `dict` | `dataset` -> `explorer` subset mapping |
            | `*args`          |        | forwarded to the constructor |
            | `**kwargs`       |        | forwarded to the constructor |
        """
        # local import to avoid import cycles
        from hover.core.dataset import SupervisableDataset
    
        assert isinstance(dataset, SupervisableDataset)
        df_dict = {_v: dataset.dfs[_k] for _k, _v in subset_mapping.items()}
        return cls(df_dict, *args, **kwargs)
    
    Synchronize the selected indices between specified sources.
    Param Type Description
    key str the key of the subset to link
    other BokehBaseExplorer the other explorer
    other_key str the key of the other subset
    Source code in hover/core/explorer/base.py
    def link_selection(self, key, other, other_key):
        """
        ???+ note "Synchronize the selected indices between specified sources."
            | Param   | Type    | Description                    |
            | :------ | :------ | :----------------------------- |
            | `key`   | `str`   | the key of the subset to link  |
            | `other` | `BokehBaseExplorer` | the other explorer |
            | `other_key` | `str` | the key of the other subset  |
        """
        self._prelink_check(other)
        # link selection in a bidirectional manner
        sl, sr = self.sources[key], other.sources[other_key]
    
        # deprecated: use js_link to sync attributes
        # sl.selected.js_link("indices", sr.selected, "indices")
        # sr.selected.js_link("indices", sl.selected, "indices")
        def left_to_right(attr, old, new):
            sr.selected.indices = sl.selected.indices[:]
    
        def right_to_left(attr, old, new):
            sl.selected.indices = sr.selected.indices[:]
    
        sl.selected.on_change("indices", left_to_right)
        sr.selected.on_change("indices", right_to_left)
    
        # link last manual selections (pointing to the same set)
        self._last_selections[key].union(other._last_selections[other_key])
    
        # link selection filter functions (pointing to the same set)
        self._selection_filters[key].data.update(
            other._selection_filters[other_key].data
        )
        self._selection_filters[key].union(other._selection_filters[other_key])
    
    Synchronize plotting ranges on the xy-plane.
    Param Type Description
    other BokehBaseExplorer the other explorer
    Source code in hover/core/explorer/base.py
    def link_xy_range(self, other):
        """
        ???+ note "Synchronize plotting ranges on the xy-plane."
            | Param   | Type    | Description                    |
            | :------ | :------ | :----------------------------- |
            | `other` | `BokehBaseExplorer` | the other explorer |
        """
        self._prelink_check(other)
        # link coordinate ranges in a bidirectional manner
        for _attr in ["start", "end"]:
            self.figure.x_range.js_link(_attr, other.figure.x_range, _attr)
            self.figure.y_range.js_link(_attr, other.figure.y_range, _attr)
            other.figure.x_range.js_link(_attr, self.figure.x_range, _attr)
            other.figure.y_range.js_link(_attr, self.figure.y_range, _attr)
    

    plot(self, *args, **kwargs)

    Plot something onto the figure.

    Implemented in child classes based on their functionalities. | Param | Type | Description | | :--------- | :---- | :-------------------- | | *args | | left to child classes | | **kwargs | | left to child classes |

    Source code in hover/core/explorer/base.py
    @abstractmethod
    def plot(self, *args, **kwargs):
        """
        ???+ note "Plot something onto the figure."
            Implemented in child classes based on their functionalities.
            | Param      | Type  | Description           |
            | :--------- | :---- | :-------------------- |
            | `*args`    |       | left to child classes |
            | `**kwargs` |       | left to child classes |
        """
        pass
    

    value_patch(self, col_original, col_patch, **kwargs)

    Allow source values to be dynamically patched through a slider.
    Param Type Description
    col_original str column of values before the patch
    col_patch str column of list of values to use as patches
    **kwargs forwarded to the slider

    Reference

    Source code in hover/core/explorer/base.py
    def value_patch(self, col_original, col_patch, **kwargs):
        """
        ???+ note "Allow source values to be dynamically patched through a slider."
            | Param            | Type   | Description                  |
            | :--------------- | :----- | :--------------------------- |
            | `col_original`   | `str`  | column of values before the patch |
            | `col_patch`      | `str`  | column of list of values to use as patches |
            | `**kwargs`       |        | forwarded to the slider |
    
            [Reference](https://github.com/bokeh/bokeh/blob/2.3.0/examples/howto/patch_app.py)
        """
        # add a patch slider to widgets, if none exist
        if "patch_slider" not in self._dynamic_widgets:
            slider = Slider(start=0, end=1, value=0, step=1, **kwargs)
            slider.disabled = True
            self._dynamic_widgets["patch_slider"] = slider
        else:
            slider = self._dynamic_widgets["patch_slider"]
    
        # create a slider-adjusting callback exposed to the outside
        def adjust_slider():
            """
            Infer slider length from the number of patch values.
            """
            num_patches = None
            for _key, _df in self.dfs.items():
                assert (
                    col_patch in _df.columns
                ), f"Subset {_key} expecting column {col_patch} among columns, got {_df.columns}"
                # find all array lengths; note that the data subset can be empty
                _num_patches_seen = _df[col_patch].apply(len).values
                assert (
                    len(set(_num_patches_seen)) <= 1
                ), f"Expecting consistent number of patches, got {_num_patches_seen}"
                _num_patches = _num_patches_seen[0] if _df.shape[0] > 0 else None
    
                # if a previous subset has implied the number of patches, run a consistency check
                if num_patches is None:
                    num_patches = _num_patches
                else:
                    assert (
                        num_patches == _num_patches
                    ), f"Conflicting number of patches: {num_patches} vs {_num_patches}"
    
            assert num_patches >= 2, f"Expecting at least 2 patches, got {num_patches}"
            slider.end = num_patches - 1
            slider.disabled = False
    
        self._dynamic_callbacks["adjust_patch_slider"] = adjust_slider
    
        # create the callback for patching values
        def update_patch(attr, old, new):
            for _key, _df in self.dfs.items():
                # calculate the patch corresponding to slider value
                _value = [_arr[new] for _arr in _df[col_patch].values]
                _slice = slice(_df.shape[0])
                _patch = {col_original: [(_slice, _value)]}
                self.sources[_key].patch(_patch)
    
        slider.on_change("value", update_patch)
        self._good(f"Patching {col_original} using {col_patch}")
    

    view(self)

    Define the high-level visual layout of the whole explorer.
    Source code in hover/core/explorer/base.py
    def view(self):
        """
        ???+ note "Define the high-level visual layout of the whole explorer."
        """
        from bokeh.layouts import column
    
        return column(self._layout_widgets(), self.figure)