Skip to content

Commit

Permalink
Merge branch 'dev' into herd_update
Browse files Browse the repository at this point in the history
  • Loading branch information
mavaylon1 authored Dec 4, 2023
2 parents 350d165 + 434433e commit 5338ece
Show file tree
Hide file tree
Showing 6 changed files with 181 additions and 97 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,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 @@ 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 += "<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>"
elif len(self) > nrows + 1:
inside += f"<p>... and {len(self) - nrows} more rows.</p>"

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 @@ 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 """
<style>
.container-fields {
font-family: "Open Sans", Arial, sans-serif;
Expand All @@ -581,7 +583,10 @@ def _repr_html_(self):
</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 @@ def _repr_html_(self):
});
</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)
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)

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'<span class="field-key">{value}</span>'
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>'

@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

0 comments on commit 5338ece

Please sign in to comment.