From 009697d25bc8393831e3f8b05681c8b8859cf372 Mon Sep 17 00:00:00 2001 From: bendichter Date: Thu, 9 Nov 2023 11:50:40 -0500 Subject: [PATCH 1/8] improve html display: * improve the title code * render tables correctly --- src/hdmf/common/table.py | 11 +++++ src/hdmf/container.py | 103 +++++++++++++++++++++------------------ 2 files changed, 66 insertions(+), 48 deletions(-) diff --git a/src/hdmf/common/table.py b/src/hdmf/common/table.py index e6f492022..b2b9d3f82 100644 --- a/src/hdmf/common/table.py +++ b/src/hdmf/common/table.py @@ -1192,6 +1192,17 @@ def to_dataframe(self, **kwargs): ret = self.__get_selection_as_df(sel) return ret + def generate_html_repr(self, level=0, access_code=""): + 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) + out += ( + f'
table{self.to_dataframe().to_html()}
' + ) + return out + @classmethod @docval( {'name': 'df', 'type': pd.DataFrame, 'doc': 'source DataFrame'}, diff --git a/src/hdmf/container.py b/src/hdmf/container.py index c83f85e1c..ccc7a3df7 100644 --- a/src/hdmf/container.py +++ b/src/hdmf/container.py @@ -562,8 +562,10 @@ def __repr__(self): 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 """ """ - JS_SCRIPT = """ + @property + def js_script(self) -> str: + """JavaScript for the HTML representation.""" + return """ """ - 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 += "
" - html_repr += ( - f"

{header_text}

" - ) - html_repr += self._generate_html_repr(self.fields) + html_repr += f"

{header_text}

" + html_repr += self._generate_html_repr(self.fields, is_field=True) html_repr += "
" 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'
{label}' - ) - 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 += "
" - else: - html_repr += ( - f'
{key}: {value}
' - ) + 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'
{str(item)}
' - ) + 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", "
") - html_repr += ( - f'
{str_}
' - ) + html_repr += self._generate_array_html(fields, level) 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'
{key}: {value}
' + + if hasattr(value, "generate_html_repr"): + html_content = value.generate_html_repr(level + 1, access_code) + + elif hasattr(value, '__repr_html__'): + html_content = value.__repr_html__() + + 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'{value}' + html_repr = ( + f'
{key}' + ) + html_repr += html_content + html_repr += "
" + + return html_repr + + def _generate_array_html(self, array, level): + """Generates HTML for a NumPy array.""" + str_ = str(array).replace("\n", "
") + return f'
{str_}
' + @staticmethod def __smart_str(v, num_indent): """ From 3df17940a2678ae1367d7dbe98fd094bba63617a Mon Sep 17 00:00:00 2001 From: Ben Dichter Date: Thu, 9 Nov 2023 11:55:17 -0500 Subject: [PATCH 2/8] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 47c9c2d7f..6bb99fdc7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) ## HDMF 3.11.0 (October 30, 2023) From e19bb1ccfbf377770d714266618a63efa1ea8697 Mon Sep 17 00:00:00 2001 From: bendichter Date: Tue, 21 Nov 2023 19:38:10 -0500 Subject: [PATCH 3/8] restrict table rendering to first 4 rows --- src/hdmf/common/table.py | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/hdmf/common/table.py b/src/hdmf/common/table.py index b2b9d3f82..e11f4c4c8 100644 --- a/src/hdmf/common/table.py +++ b/src/hdmf/common/table.py @@ -1192,14 +1192,32 @@ def to_dataframe(self, **kwargs): ret = self.__get_selection_as_df(sel) return ret - def generate_html_repr(self, level=0, access_code=""): + 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 += "
" + html_repr += f"

{header_text}

" + html_repr += self.generate_html_repr() + html_repr += "
" + 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 += f"

... and 1 more row.

" + elif len(self) > nrows + 1: + inside += f"

... and {len(self) - nrows} more rows.

" + out += ( f'
table{self.to_dataframe().to_html()}
' + f'class="container-fields field-key" title="{access_code}">table{inside}' ) return out From ebb872e799895cd6603ffb77fed57705f48ce1e7 Mon Sep 17 00:00:00 2001 From: bendichter Date: Fri, 1 Dec 2023 11:17:57 +0100 Subject: [PATCH 4/8] fix container test --- tests/unit/test_container.py | 51 +++++++++++++++++++----------------- 1 file changed, 27 insertions(+), 24 deletions(-) diff --git a/tests/unit/test_container.py b/tests/unit/test_container.py index 311093aa0..c0bd1d893 100644 --- a/tests/unit/test_container.py +++ b/tests/unit/test_container.py @@ -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 @@ -446,29 +446,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 \n \n \n' - '

test ' - 'name (ContainerWithChildAndData)

child
data
1
2
3
<' - 'div style="margin-left: 0px;" class="container-fields">st' - 'r: hello
' + '\n \n \n ' + '\n

test name (' + 'ContainerWithChildAndData)

child
data
0: 1
1: 2
2: ' + '3
str: hello
' ) From d43a29ea468c68b2be2a23232d0ff1263431f72b Mon Sep 17 00:00:00 2001 From: bendichter Date: Fri, 1 Dec 2023 11:35:59 +0100 Subject: [PATCH 5/8] add repr_html test for DynamicTable --- tests/unit/common/test_table.py | 50 ++++++++++++++++++++++++++++++--- tests/unit/test_container.py | 1 - 2 files changed, 46 insertions(+), 5 deletions(-) diff --git a/tests/unit/common/test_table.py b/tests/unit/common/test_table.py index 1854fae22..58603764a 100644 --- a/tests/unit/common/test_table.py +++ b/tests/unit/common/test_table.py @@ -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 @@ -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 \n \n \n

with_spec (DynamicTable)

description: a test table
table\n \n \n ' \ + '\n \n \n \n \n \n ' \ + ' \n \n \n \n \n \n ' \ + '\n \n
foobarbaz
id
' + + def test_add_column_existing_attr(self): table = self.with_table_columns() attrs = ['name', 'description', 'parent', 'id', 'fields'] # just a few diff --git a/tests/unit/test_container.py b/tests/unit/test_container.py index c0bd1d893..0e3fbc9eb 100644 --- a/tests/unit/test_container.py +++ b/tests/unit/test_container.py @@ -420,7 +420,6 @@ def __init__(self, **kwargs): obj.set_data_io("data2", H5DataIO, chunks=True) - class TestHTMLRepr(TestCase): class ContainerWithChildAndData(Container): From c201cc19270665e121cec3a547c2e4d619160c37 Mon Sep 17 00:00:00 2001 From: bendichter Date: Fri, 1 Dec 2023 11:51:59 +0100 Subject: [PATCH 6/8] fix repr_html test for MultiContainerInterface --- tests/unit/test_multicontainerinterface.py | 43 ++++++++++++---------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/tests/unit/test_multicontainerinterface.py b/tests/unit/test_multicontainerinterface.py index 4b1dc0c87..c705d0a6e 100644 --- a/tests/unit/test_multicontainerinterface.py +++ b/tests/unit/test_multicontainerinterface.py @@ -330,26 +330,29 @@ def test_repr_html_(self): self.assertEqual( foo._repr_html_(), ( - '\n \n \n \n

FooSingle

containers (2)
obj1
obj2
' + '\n \n \n ' + ' \n

FooSingle

containers
obj1
obj2
' ) ) From e58663be7e79157080332edf01c1b9badd6ca065 Mon Sep 17 00:00:00 2001 From: bendichter Date: Fri, 1 Dec 2023 15:40:09 +0100 Subject: [PATCH 7/8] formatting --- tests/unit/common/test_table.py | 48 ++++++++++++++++----------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/tests/unit/common/test_table.py b/tests/unit/common/test_table.py index 58603764a..3f358d22a 100644 --- a/tests/unit/common/test_table.py +++ b/tests/unit/common/test_table.py @@ -788,30 +788,30 @@ def test_repr_html(self): table = self.with_spec() html = table._repr_html_() - assert html == '\n \n \n \n

with_spec (DynamicTable)

description: a test table
table\n \n \n ' \ - '\n \n \n \n \n \n ' \ - ' \n \n \n \n \n \n ' \ - '\n \n
foobarbaz
id
' + assert html == ( + '\n \n \n ' + '\n

with_spec (DynamicTable)

description: ' + 'a test table
table\n \n \n \n ' + '\n \n \n \n \n \n ' + '\n \n \n \n \n \n ' + '\n
foobarbaz
id
' + ) def test_add_column_existing_attr(self): From f008d3bf8b1fefa7a4993b35c7ddf2ec3d25ffe0 Mon Sep 17 00:00:00 2001 From: bendichter Date: Fri, 1 Dec 2023 15:59:56 +0100 Subject: [PATCH 8/8] fix ruff --- src/hdmf/common/table.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hdmf/common/table.py b/src/hdmf/common/table.py index e11f4c4c8..767bf4f2e 100644 --- a/src/hdmf/common/table.py +++ b/src/hdmf/common/table.py @@ -1211,7 +1211,7 @@ def generate_html_repr(self, level: int = 0, access_code: str = "", nrows: int = inside = f"{self[:min(nrows, len(self))].to_html()}" if len(self) == nrows + 1: - inside += f"

... and 1 more row.

" + inside += "

... and 1 more row.

" elif len(self) > nrows + 1: inside += f"

... and {len(self) - nrows} more rows.

"