Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improve html display for tables #998

Merged
merged 11 commits into from
Dec 1, 2023
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

### Minor Improvements
- Updated `__gather_columns` to ignore the order of bases when generating columns from the super class. @mavaylon1 [#991](https://github.com/hdmf-dev/hdmf/pull/991)
- Improve HTML rendering of tables. @bendichter [#998](https://github.com/hdmf-dev/hdmf/pull/998)
- Improved issue and PR templates. @rly [#1004](https://github.com/hdmf-dev/hdmf/pull/1004)

## HDMF 3.11.0 (October 30, 2023)
Expand Down
29 changes: 29 additions & 0 deletions src/hdmf/common/table.py
Original file line number Diff line number Diff line change
Expand Up @@ -1192,6 +1192,35 @@
ret = self.__get_selection_as_df(sel)
return ret

def _repr_html_(self) -> str:
"""Generates the HTML representation of the object."""
header_text = self.name if self.name == self.__class__.__name__ else f"{self.name} ({self.__class__.__name__})"
html_repr = self.css_style + self.js_script
html_repr += "<div class='container-wrap'>"
html_repr += f"<div class='container-header'><div class='xr-obj-type'><h3>{header_text}</h3></div></div>"
html_repr += self.generate_html_repr()
html_repr += "</div>"
return html_repr

def generate_html_repr(self, level: int = 0, access_code: str = "", nrows: int = 4):
out = ""
for key, value in self.fields.items():
if key not in ("id", "colnames", "columns"):
out += self._generate_field_html(key, value, level, access_code)

inside = f"{self[:min(nrows, len(self))].to_html()}"

if len(self) == nrows + 1:
inside += "<p>... and 1 more row.</p>"

Check warning on line 1214 in src/hdmf/common/table.py

View check run for this annotation

Codecov / codecov/patch

src/hdmf/common/table.py#L1214

Added line #L1214 was not covered by tests
elif len(self) > nrows + 1:
inside += f"<p>... and {len(self) - nrows} more rows.</p>"

Check warning on line 1216 in src/hdmf/common/table.py

View check run for this annotation

Codecov / codecov/patch

src/hdmf/common/table.py#L1216

Added line #L1216 was not covered by tests

out += (
f'<details><summary style="display: list-item; margin-left: {level * 20}px;" '
f'class="container-fields field-key" title="{access_code}"><b>table</b></summary>{inside}</details>'
)
return out

@classmethod
@docval(
{'name': 'df', 'type': pd.DataFrame, 'doc': 'source DataFrame'},
Expand Down
103 changes: 55 additions & 48 deletions src/hdmf/container.py
Original file line number Diff line number Diff line change
Expand Up @@ -562,8 +562,10 @@
template += " {}: {}\n".format(k, v)
return template

def _repr_html_(self):
CSS_STYLE = """
@property
def css_style(self) -> str:
"""CSS styles for the HTML representation."""
return """
<style>
.container-fields {
font-family: "Open Sans", Arial, sans-serif;
Expand All @@ -581,7 +583,10 @@
</style>
"""

JS_SCRIPT = """
@property
def js_script(self) -> str:
"""JavaScript for the HTML representation."""
return """
<script>
function copyToClipboard(text) {
navigator.clipboard.writeText(text).then(function() {
Expand All @@ -602,67 +607,69 @@
});
</script>
"""
if self.name == self.__class__.__name__:
header_text = self.name
else:
header_text = f"{self.name} ({self.__class__.__name__})"
html_repr = CSS_STYLE
html_repr += JS_SCRIPT

def _repr_html_(self) -> str:
"""Generates the HTML representation of the object."""
header_text = self.name if self.name == self.__class__.__name__ else f"{self.name} ({self.__class__.__name__})"
html_repr = self.css_style + self.js_script
html_repr += "<div class='container-wrap'>"
html_repr += (
f"<div class='container-header'><div class='xr-obj-type'><h3>{header_text}</h3></div></div>"
)
html_repr += self._generate_html_repr(self.fields)
html_repr += f"<div class='container-header'><div class='xr-obj-type'><h3>{header_text}</h3></div></div>"
html_repr += self._generate_html_repr(self.fields, is_field=True)
html_repr += "</div>"
return html_repr

def _generate_html_repr(self, fields, level=0, access_code=".fields"):
def _generate_html_repr(self, fields, level=0, access_code="", is_field=False):
"""Recursively generates HTML representation for fields."""
html_repr = ""

if isinstance(fields, dict):
for key, value in fields.items():
current_access_code = f"{access_code}['{key}']"
if (
isinstance(value, (list, dict, np.ndarray))
or hasattr(value, "fields")
):
label = key
if isinstance(value, dict):
label += f" ({len(value)})"

html_repr += (
f'<details><summary style="display: list-item; margin-left: {level * 20}px;" '
f'class="container-fields field-key" title="{current_access_code}"><b>{label}</b></summary>'
)
if hasattr(value, "fields"):
value = value.fields
current_access_code = current_access_code + ".fields"
html_repr += self._generate_html_repr(
value, level + 1, current_access_code
)
html_repr += "</details>"
else:
html_repr += (
f'<div style="margin-left: {level * 20}px;" class="container-fields"><span class="field-key"'
f' title="{current_access_code}">{key}:</span> <span class="field-value">{value}</span></div>'
)
current_access_code = f"{access_code}.{key}" if is_field else f"{access_code}['{key}']"
html_repr += self._generate_field_html(key, value, level, current_access_code)
elif isinstance(fields, list):
for index, item in enumerate(fields):
current_access_code = f"{access_code}[{index}]"
html_repr += (
f'<div style="margin-left: {level * 20}px;" class="container-fields"><span class="field-value"'
f' title="{current_access_code}">{str(item)}</span></div>'
)
access_code += f'[{index}]'
html_repr += self._generate_field_html(index, item, level, access_code)
elif isinstance(fields, np.ndarray):
str_ = str(fields).replace("\n", "</br>")
html_repr += (
f'<div style="margin-left: {level * 20}px;" class="container-fields">{str_}</div>'
)
html_repr += self._generate_array_html(fields, level)

Check warning on line 634 in src/hdmf/container.py

View check run for this annotation

Codecov / codecov/patch

src/hdmf/container.py#L634

Added line #L634 was not covered by tests
else:
pass

return html_repr

def _generate_field_html(self, key, value, level, access_code):
"""Generates HTML for a single field."""

if isinstance(value, (int, float, str, bool)):
return f'<div style="margin-left: {level * 20}px;" class="container-fields"><span class="field-key"' \
f' title="{access_code}">{key}: </span><span class="field-value">{value}</span></div>'

if hasattr(value, "generate_html_repr"):
html_content = value.generate_html_repr(level + 1, access_code)

Check warning on line 648 in src/hdmf/container.py

View check run for this annotation

Codecov / codecov/patch

src/hdmf/container.py#L648

Added line #L648 was not covered by tests

elif hasattr(value, '__repr_html__'):
html_content = value.__repr_html__()

Check warning on line 651 in src/hdmf/container.py

View check run for this annotation

Codecov / codecov/patch

src/hdmf/container.py#L651

Added line #L651 was not covered by tests

elif hasattr(value, "fields"):
html_content = self._generate_html_repr(value.fields, level + 1, access_code, is_field=True)
elif isinstance(value, (list, dict, np.ndarray)):
html_content = self._generate_html_repr(value, level + 1, access_code, is_field=False)
else:
html_content = f'<span class="field-key">{value}</span>'

Check warning on line 658 in src/hdmf/container.py

View check run for this annotation

Codecov / codecov/patch

src/hdmf/container.py#L658

Added line #L658 was not covered by tests
html_repr = (
f'<details><summary style="display: list-item; margin-left: {level * 20}px;" '
f'class="container-fields field-key" title="{access_code}"><b>{key}</b></summary>'
)
html_repr += html_content
html_repr += "</details>"

return html_repr

def _generate_array_html(self, array, level):
"""Generates HTML for a NumPy array."""
str_ = str(array).replace("\n", "</br>")
return f'<div style="margin-left: {level * 20}px;" class="container-fields">{str_}</div>'

Check warning on line 671 in src/hdmf/container.py

View check run for this annotation

Codecov / codecov/patch

src/hdmf/container.py#L670-L671

Added lines #L670 - L671 were not covered by tests

@staticmethod
def __smart_str(v, num_indent):
"""
Expand Down
50 changes: 46 additions & 4 deletions tests/unit/common/test_table.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,26 @@
from hdmf import TermSet, TermSetWrapper
from hdmf.backends.hdf5 import H5DataIO, HDF5IO
from hdmf.backends.hdf5.h5tools import H5_TEXT, H5PY_3
from hdmf.common import (DynamicTable, VectorData, VectorIndex, ElementIdentifiers, EnumData,
DynamicTableRegion, get_manager, SimpleMultiContainer)
from hdmf.common import (
DynamicTable,
VectorData,
VectorIndex,
ElementIdentifiers,
EnumData,
DynamicTableRegion,
get_manager,
SimpleMultiContainer,
)
from hdmf.testing import TestCase, H5RoundTripMixin, remove_test_file
from hdmf.utils import StrDataset
from hdmf.data_utils import DataChunkIterator

from tests.unit.helpers.utils import (get_temp_filepath, FooExtendDynamicTable0,
FooExtendDynamicTable1, FooExtendDynamicTable2)
from tests.unit.helpers.utils import (
get_temp_filepath,
FooExtendDynamicTable0,
FooExtendDynamicTable1,
FooExtendDynamicTable2,
)

try:
import linkml_runtime # noqa: F401
Expand Down Expand Up @@ -772,6 +784,36 @@ def test_repr(self):
expected = expected % id(table)
self.assertEqual(str(table), expected)

def test_repr_html(self):
table = self.with_spec()
html = table._repr_html_()

assert html == (
'\n <style>\n .container-fields {\n font-family: "Open Sans", Arial, '
'sans-serif;\n }\n .container-fields .field-value {\n color: '
'#00788E;\n }\n .container-fields details > summary {\n cursor: '
'pointer;\n display: list-item;\n }\n .container-fields details > '
'summary:hover {\n color: #0A6EAA;\n }\n </style>\n \n '
'<script>\n function copyToClipboard(text) {\n navigator.clipboard.writeText('
'text).then(function() {\n console.log(\'Copied to clipboard: \' + text);\n '
' }, function(err) {\n console.error(\'Could not copy text: \', err);\n '
' });\n }\n\n document.addEventListener(\'DOMContentLoaded\', function() {\n '
' let fieldKeys = document.querySelectorAll(\'.container-fields .field-key\');\n '
'fieldKeys.forEach(function(fieldKey) {\n fieldKey.addEventListener(\'click\', '
'function() {\n let accessCode = fieldKey.getAttribute(\'title\').replace(\'Access '
'code: \', \'\');\n copyToClipboard(accessCode);\n });\n '
' });\n });\n </script>\n <div class=\'container-wrap\'><div '
'class=\'container-header\'><div class=\'xr-obj-type\'><h3>with_spec (DynamicTable)</h3></div></div><div '
'style="margin-left: 0px;" class="container-fields"><span class="field-key" title="">description: '
'</span><span class="field-value">a test table</span></div><details><summary style="display: list-item; '
'margin-left: 0px;" class="container-fields field-key" title=""><b>table</b></summary><table border="1" '
'class="dataframe">\n <thead>\n <tr style="text-align: right;">\n <th></th>\n '
'<th>foo</th>\n <th>bar</th>\n <th>baz</th>\n </tr>\n <tr>\n <th>id</th>\n '
'<th></th>\n <th></th>\n <th></th>\n </tr>\n </thead>\n <tbody>\n '
'</tbody>\n</table></details></div>'
)


def test_add_column_existing_attr(self):
table = self.with_table_columns()
attrs = ['name', 'description', 'parent', 'id', 'fields'] # just a few
Expand Down
52 changes: 27 additions & 25 deletions tests/unit/test_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
from hdmf.common.resources import HERD
from hdmf.testing import TestCase
from hdmf.utils import docval
from hdmf.common import (DynamicTable, VectorData, DynamicTableRegion)
from hdmf.common import DynamicTable, VectorData, DynamicTableRegion
from hdmf.backends.hdf5.h5tools import HDF5IO


Expand Down Expand Up @@ -420,7 +420,6 @@ def __init__(self, **kwargs):
obj.set_data_io("data2", H5DataIO, chunks=True)



class TestHTMLRepr(TestCase):

class ContainerWithChildAndData(Container):
Expand All @@ -446,29 +445,32 @@ def test_repr_html_(self):
child_obj1 = Container('test child 1')
obj1 = self.ContainerWithChildAndData(child=child_obj1, data=[1, 2, 3], str="hello")
assert obj1._repr_html_() == (
'\n <style>\n .container-fields {\n font-family: "Open Sans", Arial, sans-'
'serif;\n }\n .container-fields .field-value {\n color: #00788E;\n '
' }\n .container-fields details > summary {\n cursor: pointer;\n '
' display: list-item;\n }\n .container-fields details > summary:hover {\n '
' color: #0A6EAA;\n }\n </style>\n \n <script>\n functio'
'n copyToClipboard(text) {\n navigator.clipboard.writeText(text).then(function() {\n '
' console.log(\'Copied to clipboard: \' + text);\n }, function(err) {\n '
' console.error(\'Could not copy text: \', err);\n });\n }\n\n '
' document.addEventListener(\'DOMContentLoaded\', function() {\n let fieldKeys = document.q'
'uerySelectorAll(\'.container-fields .field-key\');\n fieldKeys.forEach(function(fieldKey) {'
'\n fieldKey.addEventListener(\'click\', function() {\n let acces'
'sCode = fieldKey.getAttribute(\'title\').replace(\'Access code: \', \'\');\n copyTo'
'Clipboard(accessCode);\n });\n });\n });\n </script>\n'
' <div class=\'container-wrap\'><div class=\'container-header\'><div class=\'xr-obj-type\'><h3>test '
'name (ContainerWithChildAndData)</h3></div></div><details><summary style="display: list-item; margin-left:'
' 0px;" class="container-fields field-key" title=".fields[\'child\']"><b>child</b></summary></details><deta'
'ils><summary style="display: list-item; margin-left: 0px;" class="container-fields field-key" title=".fiel'
'ds[\'data\']"><b>data</b></summary><div style="margin-left: 20px;" class="container-fields"><span class="f'
'ield-value" title=".fields[\'data\'][0]">1</span></div><div style="margin-left: 20px;" class="container-fi'
'elds"><span class="field-value" title=".fields[\'data\'][1]">2</span></div><div style="margin-left: 20px;"'
' class="container-fields"><span class="field-value" title=".fields[\'data\'][2]">3</span></div></details><'
'div style="margin-left: 0px;" class="container-fields"><span class="field-key" title=".fields[\'str\']">st'
'r:</span> <span class="field-value">hello</span></div></div>'
'\n <style>\n .container-fields {\n font-family: "Open Sans", Arial, '
'sans-serif;\n }\n .container-fields .field-value {\n color: '
'#00788E;\n }\n .container-fields details > summary {\n cursor: '
'pointer;\n display: list-item;\n }\n .container-fields details > '
'summary:hover {\n color: #0A6EAA;\n }\n </style>\n \n '
'<script>\n function copyToClipboard(text) {\n navigator.clipboard.writeText('
'text).then(function() {\n console.log(\'Copied to clipboard: \' + text);\n '
' }, function(err) {\n console.error(\'Could not copy text: \', err);\n '
' });\n }\n\n document.addEventListener(\'DOMContentLoaded\', function() {\n '
' let fieldKeys = document.querySelectorAll(\'.container-fields .field-key\');\n '
'fieldKeys.forEach(function(fieldKey) {\n fieldKey.addEventListener(\'click\', '
'function() {\n let accessCode = fieldKey.getAttribute(\'title\').replace(\'Access '
'code: \', \'\');\n copyToClipboard(accessCode);\n });\n '
' });\n });\n </script>\n <div class=\'container-wrap\'><div '
'class=\'container-header\'><div class=\'xr-obj-type\'><h3>test name ('
'ContainerWithChildAndData)</h3></div></div><details><summary style="display: list-item; margin-left: '
'0px;" class="container-fields field-key" '
'title=".child"><b>child</b></summary></details><details><summary style="display: list-item; margin-left: '
'0px;" class="container-fields field-key" title=".data"><b>data</b></summary><div style="margin-left: '
'20px;" class="container-fields"><span class="field-key" title=".data[0]">0: </span><span '
'class="field-value">1</span></div><div style="margin-left: 20px;" class="container-fields"><span '
'class="field-key" title=".data[0][1]">1: </span><span class="field-value">2</span></div><div '
'style="margin-left: 20px;" class="container-fields"><span class="field-key" title=".data[0][1][2]">2: '
'</span><span class="field-value">3</span></div></details><div style="margin-left: 0px;" '
'class="container-fields"><span class="field-key" title=".str">str: </span><span '
'class="field-value">hello</span></div></div>'
)


Expand Down
Loading