diff --git a/CHANGELOG.md b/CHANGELOG.md
index d6b5013bb..219422bb9 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)
- Improved issue and PR templates. @rly [#1004](https://github.com/hdmf-dev/hdmf/pull/1004)
## HDMF 3.11.0 (October 30, 2023)
diff --git a/src/hdmf/common/table.py b/src/hdmf/common/table.py
index e6f492022..767bf4f2e 100644
--- a/src/hdmf/common/table.py
+++ b/src/hdmf/common/table.py
@@ -1192,6 +1192,35 @@ def to_dataframe(self, **kwargs):
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 += "
"
+ html_repr += f""
+ 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 += "... and 1 more row.
"
+ elif len(self) > nrows + 1:
+ inside += f"... and {len(self) - nrows} more rows.
"
+
+ out += (
+ f'table
{inside} '
+ )
+ 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""
- )
- html_repr += self._generate_html_repr(self.fields)
+ html_repr += f""
+ 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):
"""
diff --git a/tests/unit/common/test_table.py b/tests/unit/common/test_table.py
index 1854fae22..3f358d22a 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 description: '
+ 'a test table
table
\n \n \n | \n '
+ 'foo | \n bar | \n baz | \n
\n \n id | \n '
+ ' | \n | \n | \n
\n \n \n '
+ '\n
'
+ )
+
+
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 311093aa0..0e3fbc9eb 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
@@ -420,7 +420,6 @@ def __init__(self, **kwargs):
obj.set_data_io("data2", H5DataIO, chunks=True)
-
class TestHTMLRepr(TestCase):
class ContainerWithChildAndData(Container):
@@ -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 \n \n \n'
- ' child
data1
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
'
)
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 '
+ '\n \n \n '
+ ' \n '
)
)