osscar-org / widget-periodictable

A jupyter widget to select chemical elements from the periodic table.
Other
10 stars 3 forks source link

Feature: only one element can be choose #23

Open unkcpz opened 2 years ago

unkcpz commented 2 years ago

Like in the aiidalab sssp app and commonwf-oxides app, we want the periodic table can only have one element been chosen. The following code works well for my sssp app, I assume it can be inside the widget and set by option with the periodic table instance.

The code now implemented is (copy from commonwf-oxides):

last_selected = ipw_periodic.selected_elements
def on_element_select(event):
    global last_selected

    if event['name'] == 'selected_elements' and event['type'] == 'change':
        if tuple(event['new'].keys()) == ('Du', ):
            last_selected = event['old']
        elif tuple(event['old'].keys()) == ('Du', ):
            #print(last_selected, event['new'])
            if len(event['new']) != 1:
                # Reset to only one element only if there is more than one selected,
                # to avoid infinite loops
                newly_selected = set(event['new']).difference(last_selected)
                # If this is empty it's ok, unselect all
                # If there is more than one, that's weird... to avoid problems, anyway, I pick one of the two
                if newly_selected:
                    ipw_periodic.selected_elements = {list(newly_selected)[0]: 0}
                else:
                    ipw_periodic.selected_elements = {}
                # To have the correct 'last' value for next calls
                last_selected = ipw_periodic.selected_elements
            replot()

ipw_periodic.observe(on_element_select)
unkcpz commented 2 years ago

Hi @dou-du, I extend this as a class. I think we can have a discussion and make the decision on how to integrate _on_element_select so can be controlled with one parameter.

class PeriodicTable(ipw.VBox):
    """Wrapper-widget for PTableWidget"""

    selected_element = traitlets.Unicode(allow_none=True)

    def __init__(self, cache_folder, **kwargs):
        self._disabled = kwargs.get("disabled", False)
        self._cache_folder = cache_folder

        self.ptable = PTableWidget(states=1, selected_colors=["green"], **kwargs)
        self._last_selected = None
        self.ptable.observe(self._on_element_select)

        # if cache empty run update: first time
        if os.path.exists(os.path.join(cache_folder, _DB_FOLDER)):
            self._update_db(download=False)
        else:
            self._update_db(download=True)

        disable_elements = [
            e for e in self.ptable.allElements if e not in self.elements
        ]
        self.ptable.disabled_elements = disable_elements
        db_update = ipw.Button(
            description="Update Database.",
        )
        db_update.on_click(self._update_db)

        super().__init__(
            children=(
                self.ptable,
                db_update,
            ),
            layout=kwargs.get("layout", {}),
        )

    def _on_element_select(self, event):
        if event["name"] == "selected_elements" and event["type"] == "change":
            if tuple(event["new"].keys()) == ("Du",):
                self._last_selected = event["old"]
            elif tuple(event["old"].keys()) == ("Du",):
                if len(event["new"]) != 1:
                    # Reset to only one element only if there is more than one selected,
                    # to avoid infinite loops
                    newly_selected = set(event["new"]).difference(self._last_selected)
                    # If this is empty it's ok, unselect all
                    # If there is more than one, that's weird... to avoid problems, anyway, I pick one of the two
                    if newly_selected:
                        element = list(newly_selected)[0]
                        self.ptable.selected_elements = {element: 0}
                        self.selected_element = element
                    else:
                        self.ptable.selected_elements = {}
                        self.selected_element = None
                    # To have the correct 'last' value for next calls
                    self._last_selected = self.ptable.selected_elements
                else:
                    # first time set: len(event['new']) -> 1
                    self.selected_element = list(event["new"])[0]

    def _update_db(self, _=None, download=True):
        """update cached db fetch from remote. and update ptable"""
        # download from remote
        if download:
            self._download(self._cache_folder)

        self.elements = self._get_enabled_elements(self._cache_folder)
        disable_elements = [
            e for e in self.ptable.allElements if e not in self.elements
        ]
        self.ptable.disabled_elements = disable_elements

    @staticmethod
    def _get_enabled_elements(cache_folder):
        elements = []
        for fn in os.listdir(os.path.join(cache_folder, _DB_FOLDER)):
            if "band" not in fn:
                elements.append(fn.split(".")[0])

        return elements

    @staticmethod
    def _download(cache_folder):
        """
        The original sssp_db folder is deleted and re-downloaded from
        source and extracted.

        :params cache_folder: folder where cache stored
        """
        # Purge whole db folder filst
        db_dir = f"{cache_folder}/sssp_db"
        if os.path.exists(db_dir) and os.path.isdir(db_dir):
            shutil.rmtree(db_dir)

        # download DB tar file from source
        tar_file = f"{cache_folder}/sssp_db.tar.gz"
        request.urlretrieve(_DB_URL, tar_file)

        # decompress to the db folder
        tar = tarfile.open(tar_file)
        os.chdir(cache_folder)
        tar.extractall()
        tar.close()

        os.remove(tar_file)

    @property
    def value(self) -> dict:
        """Return value for wrapped PTableWidget"""

        return not self.select_any_all.value, self.ptable.selected_elements.copy()

    @property
    def disabled(self) -> None:
        """Disable widget"""
        return self._disabled

    @disabled.setter
    def disabled(self, value: bool) -> None:
        """Disable widget"""
        if not isinstance(value, bool):
            raise TypeError("disabled must be a boolean")

        self.select_any_all.disabled = self.ptable.disabled = value

    def reset(self):
        """Reset widget"""
        self.select_any_all.value = False
        self.ptable.selected_elements = {}
        self.selected_element = None

    def freeze(self):
        """Disable widget"""
        self.disabled = True

    def unfreeze(self):
        """Activate widget (in its current state)"""
        self.disabled = False