From 3ae3f98654bd6c973be82d7a32d1da2ebc2c715c Mon Sep 17 00:00:00 2001 From: AndresOrtegaGuerrero Date: Tue, 7 Jan 2025 12:33:45 +0000 Subject: [PATCH 1/3] splitting editors --- src/aiidalab_qe/app/structure/__init__.py | 4 +- src/aiidalab_qe/common/__init__.py | 8 ++- src/aiidalab_qe/common/widgets.py | 78 ++++++++++++++--------- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/src/aiidalab_qe/app/structure/__init__.py b/src/aiidalab_qe/app/structure/__init__.py index f58d1f2bc..286bdd379 100644 --- a/src/aiidalab_qe/app/structure/__init__.py +++ b/src/aiidalab_qe/app/structure/__init__.py @@ -13,6 +13,7 @@ AddingTagsEditor, LazyLoadedOptimade, LazyLoadedStructureBrowser, + PeriodicityEditor, ) from aiidalab_qe.common.infobox import InAppGuide from aiidalab_qe.common.widgets import CategorizedStructureExamplesWidget, QeWizardStep @@ -80,7 +81,8 @@ def _render(self): editors = [ BasicCellEditor(title="Edit cell"), BasicStructureEditor(title="Edit structure"), - AddingTagsEditor(title="Edit StructureData"), + AddingTagsEditor(title="Set atom tags"), + PeriodicityEditor(title="Set periodicity"), ] plugin_editors = get_entry_items("aiidalab_qe.properties", "editor") diff --git a/src/aiidalab_qe/common/__init__.py b/src/aiidalab_qe/common/__init__.py index 70dc6de9f..a82e29dd2 100644 --- a/src/aiidalab_qe/common/__init__.py +++ b/src/aiidalab_qe/common/__init__.py @@ -1,7 +1,12 @@ # trigger registration of the viewer widget: from .node_view import CalcJobNodeViewerWidget # noqa: F401 from .process import QeAppWorkChainSelector, WorkChainSelector -from .widgets import AddingTagsEditor, LazyLoadedOptimade, LazyLoadedStructureBrowser +from .widgets import ( + AddingTagsEditor, + LazyLoadedOptimade, + LazyLoadedStructureBrowser, + PeriodicityEditor, +) __all__ = [ "AddingTagsEditor", @@ -9,4 +14,5 @@ "LazyLoadedStructureBrowser", "QeAppWorkChainSelector", "WorkChainSelector", + "PeriodicityEditor", ] diff --git a/src/aiidalab_qe/common/widgets.py b/src/aiidalab_qe/common/widgets.py index 5b101eb7d..e2c2a3ce8 100644 --- a/src/aiidalab_qe/common/widgets.py +++ b/src/aiidalab_qe/common/widgets.py @@ -474,21 +474,6 @@ def __init__(self, title="", **kwargs): button_style="warning", layout={"width": "initial"}, ) - self.periodicity = ipw.RadioButtons( - options=[ - ("3D (bulk systems)", "xyz"), - ("2D (surfaces, slabs, ...)", "xy"), - ("1D (wires)", "x"), - ("0D (molecules)", "molecule"), - ], - value="xyz", - layout={"width": "initial"}, - ) - self.apply_periodicity = ipw.Button( - description="Apply", - button_style="primary", - layout={"width": "100px"}, - ) self.scroll_note = ipw.HTML( value="

Note: The table is scrollable.

", layout={"visibility": "hidden"}, @@ -501,7 +486,6 @@ def __init__(self, title="", **kwargs): self.add_tags.on_click(self._display_table) self.reset_tags.on_click(self._display_table) self.reset_all_tags.on_click(self._display_table) - self.apply_periodicity.on_click(self._select_periodicity) super().__init__( children=[ @@ -532,21 +516,6 @@ def __init__(self, title="", **kwargs): self.scroll_note, ipw.HBox([self.add_tags, self.reset_tags, self.reset_all_tags]), self._status_message, - ipw.HTML( - '
Set structure periodicity
' - ), - ipw.HTML(""" -

Select the periodicity of your system.

-

NOTE:

- - - """), - self.periodicity, - self.apply_periodicity, ], **kwargs, ) @@ -653,6 +622,53 @@ def _reset_all_tags(self, _=None): self.input_selection = None self.input_selection = deepcopy(self.selection) + +class PeriodicityEditor(ipw.VBox): + """Editor for changing periodicity of structures.""" + + structure = traitlets.Instance(ase.Atoms, allow_none=True) + + def __init__(self, title="", **kwargs): + self.title = title + + self.periodicity = ipw.RadioButtons( + options=[ + ("3D (bulk systems)", "xyz"), + ("2D (surfaces, slabs, ...)", "xy"), + ("1D (wires)", "x"), + ("0D (molecules)", "molecule"), + ], + value="xyz", + layout={"width": "initial"}, + ) + self.apply_periodicity = ipw.Button( + description="Apply", + button_style="primary", + layout={"width": "100px"}, + ) + self.apply_periodicity.on_click(self._select_periodicity) + + super().__init__( + children=[ + ipw.HTML( + '
Set structure periodicity
' + ), + ipw.HTML(""" +

Select the periodicity of your system.

+

NOTE:

+ + + """), + self.periodicity, + self.apply_periodicity, + ], + **kwargs, + ) + def _select_periodicity(self, _=None): """Select periodicity.""" periodicity_options = { From be88af355fb0b0ddf92044a6a3704fbaef4880ca Mon Sep 17 00:00:00 2001 From: Andres Ortega-Guerrero <34098967+AndresOrtegaGuerrero@users.noreply.github.com> Date: Tue, 7 Jan 2025 14:41:19 +0100 Subject: [PATCH 2/3] Add atom table and set cell tab as default (#1059) * Introduced a table to display atom coordinates and set the cell tab as the default view. * Implemented a new `TableWidget` class for an interactive and user-friendly table experience. Co-authored-by: Xing Wang --- setup.cfg | 1 + .../components/viewer/structure/structure.py | 51 +++++++++++- src/aiidalab_qe/common/widgets.py | 83 +++++++++++++++++++ 3 files changed, 134 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 190886ef1..c594b09fd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -33,6 +33,7 @@ install_requires = importlib-resources~=5.2 aiida-wannier90-workflows==2.3.0 pymatgen==2024.5.1 + anywidget==0.9.13 python_requires = >=3.9 [options.packages.find] diff --git a/src/aiidalab_qe/app/result/components/viewer/structure/structure.py b/src/aiidalab_qe/app/result/components/viewer/structure/structure.py index 3cae7cb85..6ccf6f19b 100644 --- a/src/aiidalab_qe/app/result/components/viewer/structure/structure.py +++ b/src/aiidalab_qe/app/result/components/viewer/structure/structure.py @@ -1,4 +1,7 @@ +import ipywidgets as ipw + from aiidalab_qe.common.panel import ResultsPanel +from aiidalab_qe.common.widgets import TableWidget from aiidalab_widgets_base.viewers import StructureDataViewer from .model import StructureResultsModel @@ -9,7 +12,26 @@ def _render(self): if not hasattr(self, "widget"): structure = self._model.get_structure() self.widget = StructureDataViewer(structure=structure) - self.results_container.children = [self.widget] + # Select the Cell tab by default + self.widget.configuration_box.selected_index = 2 + self.table_description = ipw.HTML(""" +

+ Structure table information: Atom coordinates in Å +

+

+ You can click on a row to select an atom. Multiple atoms + can be selected by clicking on additional rows. To unselect + an atom, click on the selected row again. +

+ """) + self.atom_coordinates_table = TableWidget() + self._generate_table(structure.get_ase()) + self.results_container.children = [ + self.widget, + self.table_description, + self.atom_coordinates_table, + ] + self.atom_coordinates_table.observe(self._change_selection, "selected_rows") # HACK to resize the NGL viewer in cases where it auto-rendered when its # container was not displayed, which leads to a null width. This hack restores @@ -17,3 +39,30 @@ def _render(self): ngl = self.widget._viewer ngl._set_size("100%", "300px") ngl.control.zoom(0.0) + + def _generate_table(self, structure): + data = [ + [ + "Atom index", + "Chemical symbol", + "Tag", + "x (Å)", + "y (Å)", + "z (Å)", + ] + ] + positions = structure.positions + chemical_symbols = structure.get_chemical_symbols() + tags = structure.get_tags() + + for index, (symbol, tag, position) in enumerate( + zip(chemical_symbols, tags, positions), start=1 + ): + # Format position values to two decimal places + formatted_position = [f"{coord:.2f}" for coord in position] + data.append([index, symbol, tag, *formatted_position]) + self.atom_coordinates_table.data = data + + def _change_selection(self, _): + selected_indices = self.atom_coordinates_table.selected_rows + self.widget.displayed_selection = selected_indices diff --git a/src/aiidalab_qe/common/widgets.py b/src/aiidalab_qe/common/widgets.py index 5b101eb7d..f4da89cba 100644 --- a/src/aiidalab_qe/common/widgets.py +++ b/src/aiidalab_qe/common/widgets.py @@ -13,6 +13,7 @@ from threading import Event, Lock, Thread from time import time +import anywidget import ase import ipywidgets as ipw import numpy as np @@ -1219,3 +1220,85 @@ def _show_missing_information_warning(self): def _hide_missing_information_warning(self): self.children = self.previous_children + + +class TableWidget(anywidget.AnyWidget): + _esm = """ + function render({ model, el }) { + let domElement = document.createElement("div"); + el.classList.add("custom-table"); + let selectedIndices = []; + + function drawTable() { + const data = model.get("data"); + domElement.innerHTML = ""; + let innerHTML = '' + data[0].map(header => ``).join('') + ''; + + for (let i = 1; i < data.length; i++) { + innerHTML += '' + data[i].map(cell => ``).join('') + ''; + } + + innerHTML += "
${header}
${cell}
"; + domElement.innerHTML = innerHTML; + + const rows = domElement.querySelectorAll('tr'); + rows.forEach((row, index) => { + if (index > 0) { + row.addEventListener('click', () => { + const rowIndex = index - 1; + if (selectedIndices.includes(rowIndex)) { + selectedIndices = selectedIndices.filter(i => i !== rowIndex); + row.classList.remove('selected-row'); + } else { + selectedIndices.push(rowIndex); + row.classList.add('selected-row'); + } + model.set('selected_rows', [...selectedIndices]); + model.save_changes(); + }); + + row.addEventListener('mouseover', () => { + if (!row.classList.contains('selected-row')) { + row.classList.add('hover-row'); + } + }); + + row.addEventListener('mouseout', () => { + row.classList.remove('hover-row'); + }); + } + }); + } + + drawTable(); + model.on("change:data", drawTable); + el.appendChild(domElement); + } + export default { render }; + """ + _css = """ + .custom-table table, .custom-table th, .custom-table td { + border: 1px solid black; + border-collapse: collapse; + text-align: left; + padding: 4px; + } + .custom-table th, .custom-table td { + min-width: 50px; + word-wrap: break-word; + } + .custom-table table { + width: 70%; + font-size: 1.0em; + } + /* Hover effect with light gray background */ + .custom-table tr.hover-row:not(.selected-row) { + background-color: #f0f0f0; + } + /* Selected row effect with light green background */ + .custom-table tr.selected-row { + background-color: #DFF0D8; + } + """ + data = traitlets.List().tag(sync=True) + selected_rows = traitlets.List().tag(sync=True) From 00a9c09c339419c0c583a350939de44cca142da5 Mon Sep 17 00:00:00 2001 From: AndresOrtegaGuerrero Date: Tue, 7 Jan 2025 14:54:42 +0000 Subject: [PATCH 3/3] updating titles and removing redundant text --- src/aiidalab_qe/app/structure/__init__.py | 4 ++-- src/aiidalab_qe/common/widgets.py | 6 ------ 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/aiidalab_qe/app/structure/__init__.py b/src/aiidalab_qe/app/structure/__init__.py index 286bdd379..4b42b7990 100644 --- a/src/aiidalab_qe/app/structure/__init__.py +++ b/src/aiidalab_qe/app/structure/__init__.py @@ -81,8 +81,8 @@ def _render(self): editors = [ BasicCellEditor(title="Edit cell"), BasicStructureEditor(title="Edit structure"), - AddingTagsEditor(title="Set atom tags"), - PeriodicityEditor(title="Set periodicity"), + AddingTagsEditor(title="Edit atom tags"), + PeriodicityEditor(title="Edit periodicity"), ] plugin_editors = get_entry_items("aiidalab_qe.properties", "editor") diff --git a/src/aiidalab_qe/common/widgets.py b/src/aiidalab_qe/common/widgets.py index e2c2a3ce8..1f4e82449 100644 --- a/src/aiidalab_qe/common/widgets.py +++ b/src/aiidalab_qe/common/widgets.py @@ -489,9 +489,6 @@ def __init__(self, title="", **kwargs): super().__init__( children=[ - ipw.HTML( - "Set custom tags for atoms", - ), ipw.HTML( """

@@ -650,9 +647,6 @@ def __init__(self, title="", **kwargs): super().__init__( children=[ - ipw.HTML( - '

Set structure periodicity
' - ), ipw.HTML("""

Select the periodicity of your system.

NOTE: