Skip to content

Commit

Permalink
Add atom table and set cell tab as default (#1059)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>
  • Loading branch information
AndresOrtegaGuerrero and superstar54 committed Jan 7, 2025
1 parent 94f23c7 commit a1105f1
Show file tree
Hide file tree
Showing 3 changed files with 134 additions and 1 deletion.
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -9,11 +12,57 @@ 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("""
<h4 style='margin: 10px 0;'>
Structure table information: Atom coordinates in Å
</h4>
<p style='margin: 5px 0; color: #555;'>
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.
</p>
""")
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
# the original dimensions.
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
83 changes: 83 additions & 0 deletions src/aiidalab_qe/common/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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 = '<table><tr>' + data[0].map(header => `<th>${header}</th>`).join('') + '</tr>';
for (let i = 1; i < data.length; i++) {
innerHTML += '<tr>' + data[i].map(cell => `<td>${cell}</td>`).join('') + '</tr>';
}
innerHTML += "</table>";
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)

0 comments on commit a1105f1

Please sign in to comment.